Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .agents/skills/dialogue-lab-followup/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@ description: Process supplied public content in an existing Israel Facebook Dial
3. When sibling branches already stored in one Case must be tracked independently, prepare one `case-split-branch` command with an outside-Git backup and exact new title/topic. After explicit approval, run `dialogue-lab check`; if it passes, run the split and use its committed Case/Turn mapping.
4. Assign or reuse only `P1`, `P2`, ... or `USER`. Resolve the direct parent from visible context or user confirmation and retain Parent Confidence; ask only when ambiguity changes the reply.
5. Build one incoming Turn payload. A supplied `reply_comment_id` is its strongest duplicate identity; without one, the transaction uses Case ID + Parent Turn ID (including a null root) + Direction + Exact Text. The transaction returns the existing Turn on a duplicate or allocates a Turn ID, validates the complete parent graph, appends, commits, and reads back.
6. Read repository strategy and evidence Markdown only as needed, verify material claims, and answer the operative claim without treating private planning as public context.
7. Return the standard output. After explicit approval, run `dialogue-lab check`; if it passes, run exactly one `dialogue-lab case-followup --case-id <id> <payload> --approved` transaction and report its compact receipt.
6. Read repository strategy and evidence Markdown only as needed and verify material claims.
7. Base the public reply's substantive response only on the supplied public post, target comment chain, and verified relevant facts; never treat private planning, predicted replies, or unrelated conversation context as public content.
8. Return the standard output. After explicit approval, run `dialogue-lab check`; if it passes, run exactly one `dialogue-lab case-followup --case-id <id> <payload> --approved` transaction and report its compact receipt.

## Output

Expand Down
79 changes: 79 additions & 0 deletions AGENTS.history.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,85 @@
"original_sha256": "a13eb777209419f0fd81ee29bde8c2c3091041ec8e737d3e1970529f7c18c0ca",
"rules_sha256": "0c66144eed98d6320a6d0ccbad65e84257a28f7856e28e3bc0e50b4b763f6497",
"global_rules_sha256": "8ef798b3330613534c7b03ca75aeb7fb9413a4c6774e2f7388d258a09140ed42"
},
{
"id": "2026-07-22-complete-public-replies",
"date": "2026-07-22",
"rules": [
"DL-REPLY-01"
],
"related_global_rules": [
"OUT-03"
],
"reason": "A recommended public reply was delivered as staged fragments and was later expanded using private planning instead of as one complete response to the supplied public exchange.",
"preserved": "Reply brevity, one-pivotal-point focus, verified external fact use, and the follow-up skill's public-context boundary remain unchanged.",
"introduced": "DL-REPLY-01 requires complete ready-to-post text for the supplied post and target comment chain unless the user requests partial or staged content.",
"validation": "Replayed the fragmentary-reply and private-context failures; the rule requires a complete public-exchange reply while retaining user-requested fragments and verified external facts.",
"global_rules_sha256": "8ef798b3330613534c7b03ca75aeb7fb9413a4c6774e2f7388d258a09140ed42",
"rules_sha256": "8cb6faeeee366a2b175440bc30f776821acb9739a06ed47765ab59d78e18dfea",
"source_history": "Targeted Git history was used because no prior local AGENTS.history.json existed."
},
{
"id": "2026-07-22-unconditional-complete-public-replies",
"date": "2026-07-22",
"rules": [
"DL-REPLY-01"
],
"related_global_rules": [
"OUT-03"
],
"reason": "The user required every proposed public Dialogue Lab reply to be complete and ready to post, without a partial-or-staged exception.",
"preserved": "Self-contained ready-to-post output remains required; fact verification and public-context isolation remain separate in the follow-up skill.",
"introduced": "DL-REPLY-01 now unconditionally requires one complete, self-contained, ready-to-post response.",
"validation": "Replayed full and partial reply outputs; a complete ready-to-post response passes, while partial or staged output fails regardless of request wording.",
"global_rules_sha256": "8ef798b3330613534c7b03ca75aeb7fb9413a4c6774e2f7388d258a09140ed42",
"rules_sha256": "cbf29456442e949912c4af8e0419f2cb36cf3b8f10d2c5400486d848734dc4ba",
"source_history": "Prior structured local history and the related global OUT-03 history were consulted."
},
{
"id": "2026-07-22-reapply-complete-public-replies-to-current-release",
"date": "2026-07-22",
"rules": [
"DL-REPLY-01"
],
"related_global_rules": [
"OUT-03"
],
"reason": "Apply the approved complete-public-reply rule and public-context boundary to release/local rebuilt from current main.",
"preserved": "All shipped Case and Turn identity rules, earlier history records, current global output brevity, fact verification, and unrelated Dialogue Lab behavior remain unchanged.",
"introduced": "The final DL-REPLY-01 wording and matching follow-up skill boundary coexist with the shipped identity model on release/local.",
"validation": "Current global OUT-03 and every existing local rule were re-read; concise output remains compatible with one complete ready-to-post public reply, and no prior history relation was rewritten.",
"global_rules_sha256": "b0df68cb05e31cf808cbcc932b71241c672471146720d99601de168c2e4ce5ce",
"rules_sha256": "9f6fd346849cf39880007660957e0a432944004f18c5bf5e80118eb5fbea824e",
"source_history": "The uncommitted full-reply-rule history, shipped local history, and current global OUT-03 history were consulted."
},
{
"id": "2026-07-22-active-rule-relation-metadata",
"date": "2026-07-22",
"rules": [
"HASBARA-CHECK-01",
"HASBARA-IDENTITY-01",
"HASBARA-IDENTITY-02",
"HASBARA-OPEN-CASES-01",
"DL-REPLY-01"
],
"relation_neighbors": [
"HASBARA-CANONICAL-01",
"HASBARA-IDENTITY-01",
"HASBARA-IDENTITY-02",
"HASBARA-OPEN-CASES-01",
"OUT-03"
],
"related_global_rules": [
"OUT-03"
],
"reason": "Make current rule interactions queryable instead of inferring them from historical entries.",
"preserved": "Every prior history entry remains unchanged, and HASBARA-CHECK-01 has no current relation to HASBARA-OPEN-CASES-01.",
"introduced": "Active requires and limits metadata for canonical state, Case and Turn identity, open-Case presentation, and complete public replies.",
"validation": "The rule-history helper resolves direct current neighbors across the local and global rule sources without unknown IDs.",
"global_rules_sha256": "b0df68cb05e31cf808cbcc932b71241c672471146720d99601de168c2e4ce5ce",
"original_sha256": "9f6fd346849cf39880007660957e0a432944004f18c5bf5e80118eb5fbea824e",
"rules_sha256": "ae6738bd2deca5db34ee3c1ec0832aec84c6d84d0c2c0bfb0080fb93fe830a68"
}
]
}
8 changes: 8 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,19 @@
## HasbaraTops

- [HASBARA-CHECK-01] Run `dialogue-lab check` before a canonical write, after a failed write or readiness check, or when database state is uncertain; ordinary read-only queries must reuse fresh sufficient evidence.
- requires: HASBARA-CANONICAL-01
- [HASBARA-CANONICAL-01] Treat repository Markdown as canonical governance, strategy, and evidence content and the configured SQLite database as canonical Case and Turn state; do not use MCP for Dialogue Lab work.
- [HASBARA-IDENTITY-01] Use `config/storage.toml`; preserve the SQLite schema version; treat Case ID as definitive, allow multiple Cases per `Post ID + Root Comment ID`, and treat root lookup as candidate discovery only.
- requires: HASBARA-CANONICAL-01
- [HASBARA-IDENTITY-02] Deduplicate Turns by supplied `reply_comment_id`; when absent, use Case ID + Parent Turn ID (including null roots) + Direction + Exact Text; never use mutable latest-reply state as identity.
- requires: HASBARA-IDENTITY-01
- limits: HASBARA-OPEN-CASES-01
- [HASBARA-OPEN-CASES-01] When presenting open Cases, use each Case's latest public Turn supplied exact URL; never substitute the Case root URL, and mark a missing link explicitly.
- requires: HASBARA-CANONICAL-01
- Invoke the matching Dialogue Lab skill for intake, follow-up, posting confirmation, closeout, or strategy review.
- [DL-REPLY-01] When proposing a public Dialogue Lab reply, provide one
complete, self-contained, ready-to-post response
- limits: OUT-03
- Never edit, move, rename, replace, delete, import, or summarize `General responses` without the user's explicit instruction for that exact action.
- Perform canonical writes only through explicit-approval `dialogue-lab` commands that use SQLite transactions and committed read-back verification.
- After a failed canonical write, verify rollback and database integrity; block further writes while either remains unresolved.
Expand Down
Binary file added General responses.docx
Binary file not shown.
66 changes: 38 additions & 28 deletions src/dialogue_lab/storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -795,37 +795,47 @@ def find_turn_duplicate(self, turn: TurnRecord) -> TurnRecord | None:
return _find_turn_duplicate(connection, turn)

def list_open_summaries(self) -> list[dict[str, object]]:
closed_statuses = tuple(sorted(status.value for status in CLOSED_STATUSES))
closed_placeholders = ", ".join("?" for _ in closed_statuses)
with self._connect() as connection:
rows = connection.execute(
"SELECT * FROM cases "
"ORDER BY CAST(substr(case_id, 6) AS INTEGER)"
).fetchall()
output: list[dict[str, object]] = []
for row in rows:
case = _case_from_row(row)
if case.status in CLOSED_STATUSES:
continue
public_turns = [
turn
for turn in self._get_turns(connection, case.case_id)
if turn.state not in {TurnState.DRAFT, TurnState.REPLACED}
]
latest = max(
public_turns,
key=lambda turn: (turn.observed_at, int(turn.turn_id[1:])),
default=None,
)
permalink = latest.exact_url if latest is not None else None
output.append(
{
"case_id": case.case_id,
"status": case.status.value,
"last_turn_id": latest.turn_id if latest is not None else None,
"last_comment_permalink": permalink,
"permalink_status": "supplied" if permalink else "missing",
}
f"""WITH ranked_public_turns AS (
SELECT case_id, turn_id, exact_url,
ROW_NUMBER() OVER (
PARTITION BY case_id
ORDER BY observed_at DESC,
CAST(substr(turn_id, 2) AS INTEGER) DESC
) AS turn_rank
FROM turns
WHERE state NOT IN (?, ?)
)
return output
SELECT cases.case_id, cases.status,
latest.turn_id AS last_turn_id,
latest.exact_url AS last_comment_permalink
FROM cases
LEFT JOIN ranked_public_turns AS latest
ON latest.case_id = cases.case_id
AND latest.turn_rank = 1
WHERE cases.status NOT IN ({closed_placeholders})
ORDER BY CAST(substr(cases.case_id, 6) AS INTEGER)""",
(
TurnState.DRAFT.value,
TurnState.REPLACED.value,
*closed_statuses,
),
).fetchall()
return [
{
"case_id": str(row["case_id"]),
"status": str(row["status"]),
"last_turn_id": row["last_turn_id"],
"last_comment_permalink": row["last_comment_permalink"],
"permalink_status": (
"supplied" if row["last_comment_permalink"] else "missing"
),
}
for row in rows
]

def strategy_dataset(self) -> Mapping[str, object]:
with self._connect() as connection:
Expand Down
14 changes: 13 additions & 1 deletion tests/test_sqlite_storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,19 @@ def test_open_case_returns_latest_turn_permalink_without_root_fallback(
reply_comment_id="789",
observed_at="2026-07-17 11:00",
)
store.create_case(case, [root, reply], approved=True)
later_draft = make_reply(
"T003",
"T002",
direction=TurnDirection.OUTGOING,
state=TurnState.DRAFT,
exact_url=(
"https://www.facebook.com/example/posts/123"
"?comment_id=456&reply_comment_id=999"
),
reply_comment_id="999",
observed_at="2026-07-17 12:00",
)
store.create_case(case, [root, reply, later_draft], approved=True)
missing_case = make_case(case_id="Case-002")
missing_turn = make_turn(
case_id="Case-002",
Expand Down
Loading