diff --git a/.agents/skills/dialogue-lab-followup/SKILL.md b/.agents/skills/dialogue-lab-followup/SKILL.md index aca248e..25c2e62 100644 --- a/.agents/skills/dialogue-lab-followup/SKILL.md +++ b/.agents/skills/dialogue-lab-followup/SKILL.md @@ -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 --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 --approved` transaction and report its compact receipt. ## Output diff --git a/AGENTS.history.json b/AGENTS.history.json index 4fd6d60..4d2a605 100644 --- a/AGENTS.history.json +++ b/AGENTS.history.json @@ -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" } ] } diff --git a/AGENTS.md b/AGENTS.md index a901737..eb44d8e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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. diff --git a/General responses.docx b/General responses.docx new file mode 100644 index 0000000..183bd80 Binary files /dev/null and b/General responses.docx differ diff --git a/src/dialogue_lab/storage.py b/src/dialogue_lab/storage.py index 2ff94fd..2383030 100644 --- a/src/dialogue_lab/storage.py +++ b/src/dialogue_lab/storage.py @@ -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: diff --git a/tests/test_sqlite_storage.py b/tests/test_sqlite_storage.py index 6c49551..eddaec9 100644 --- a/tests/test_sqlite_storage.py +++ b/tests/test_sqlite_storage.py @@ -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",