diff --git a/.agent/demos/d1-receipts/NON-CLAIMS.md b/.agent/demos/d1-receipts/NON-CLAIMS.md new file mode 100644 index 0000000000..e52d1257ce --- /dev/null +++ b/.agent/demos/d1-receipts/NON-CLAIMS.md @@ -0,0 +1,5 @@ +# Non-claims + +- This packet does not prove every sentence in PR #3282's body is independently verified -- only the four claims explicitly checked in report.md are scored; claim 4 is explicitly scored `not_supported`. +- This packet does not establish that `session_refs` correctly resolves every PR reference archive-wide -- only that it resolves this one case with structural evidence. +- This packet does not reproduce on the public seed corpus (seed 1843); it requires read-only access to the live archive and the `Sinity/polylogue` GitHub history. diff --git a/.agent/demos/d1-receipts/PROMPT.md b/.agent/demos/d1-receipts/PROMPT.md new file mode 100644 index 0000000000..3e23fb53d6 --- /dev/null +++ b/.agent/demos/d1-receipts/PROMPT.md @@ -0,0 +1,67 @@ +# D1 "The Receipts": Claim-vs-Evidence on a Real Merged PR + +Predeclaration receipt: `artifact:d1-receipts-predeclaration`. + +Pick a real merged, agent-authored PR from this repository. Resolve it to +its authoring/dispatch session **structurally** — via `session_refs` +(kind=`pull_request`), not by regex-scanning message prose or a time-window +heuristic. Then check specific sentences from the PR body against that +session's own recorded tool_use/tool_result blocks: does the evidence +actually support the claim, or is the claim resting on the PR body's own +prose with nothing underneath it? + +Product primitives only: `session_refs` (the typed evidence table wired by +PR #3425/#3431), `polylogue read --view correlation`, and structural SQL +reads over `blocks`/`session_refs` for citation (mirroring the exact +read-only query style PR #3392 and PR #3282 themselves used in their own +Verification sections — this demo does not invent a new access pattern). + +## Steps + +1. Resolve PR → session structurally: + ```sql + SELECT session_id, repo, ref_number, url + FROM session_refs + WHERE kind = 'pull_request' AND repo = 'Sinity/polylogue' AND ref_number = 3282; + ``` + Cross-check the same resolution through the CLI's own read surface: + `polylogue find "id:" then read --view correlation --format json` + (this is the surface PR #3425/#3431 wired `session_refs` into — + `insights/session_commit.py:build_correlation_result` and + `insights/correlation_view.py`). + +2. Fetch the PR body from GitHub (`gh pr view 3282 --json body`) and pull + out individually falsifiable sentences — not the whole prose block, each + claim on its own. + +3. For each claim, search the resolved session's own `blocks` rows + (`tool_use`/`tool_result`, joined by `tool_id`) for structural evidence: + an exact command, an exact exit code, an exact pytest summary line. A + claim with no matching block is marked **not independently verified in + this session** — never silently upgraded to "supported" because the PR + body asserts it. + +4. Render the two columns: claimed sentence | observed block evidence + (drillable via the cited `block:` ref), with an explicit status per row. + +## Note on this run + +This demo's session turned out to be a **merge-conductor** session: its own +`blocks` are almost entirely `Bash` (53 of 56 tool_use blocks) plus 3 `Read` +calls — zero `Edit`/`Write` tool_use. The actual file edits for PR #3282 +happened in separately dispatched worker sessions across multiple git +worktrees (`/realm/worktrees/polylogue-membership-head*`); this session +orchestrates `git`, `gh pr create`, and `devtools test`/`devtools verify` +invocations across those worktrees and stitches the result into one PR. + +This is itself a real, useful finding, not a inconvenience to hide: the PR +body's own Verification section names a 7-file `devtools test` invocation +("`devtools test tests/unit/sources/test_live_batch_support.py ...` — all +passing, **see individual commit messages for per-commit pass counts**") — +its own parenthetical admits the aggregate command was never run as one +shot. Searching this session's blocks confirms it: the 7-file string only +appears inside the `gh pr create --body` tool_input (i.e. inside the PR body +text itself), never as an actual invoked command. That specific claim is +marked **not independently verified in this session** in `report.md` and +`checks.json` — precisely the honesty discipline this packet exists to +enforce, applied to itself. diff --git a/.agent/demos/d1-receipts/checks.json b/.agent/demos/d1-receipts/checks.json new file mode 100644 index 0000000000..08d536a977 --- /dev/null +++ b/.agent/demos/d1-receipts/checks.json @@ -0,0 +1,7 @@ +{ + "pass": true, + "unsupported_claims": [ + "PR #3282 claim 4: the 7-file devtools test invocation named in the Verification section is not independently verified in the resolved session -- no matching tool_use block exists outside the gh-pr-create body text itself." + ], + "coverage_notes": "4 claims from PR #3282's body were checked structurally against the session_refs-resolved authoring/dispatch session's own blocks. 3 of 4 are supported by exact tool_use/tool_result evidence (gh pr create body/URL match, devtools verify --quick per-step exit codes, the rebuild_index.py test+commit pair). Claim 4 is explicitly and correctly scored not_supported rather than assumed true from the PR body's own prose -- this is the intended outcome of the packet's method, not a defect. The resolved session is also shown to be a merge-conductor (53 Bash + 3 Read tool_use blocks, 0 Edit/Write) rather than the direct file-editing session, which is documented as a real finding in report.md rather than hidden." +} diff --git a/.agent/demos/d1-receipts/evidence.ndjson b/.agent/demos/d1-receipts/evidence.ndjson new file mode 100644 index 0000000000..70e9aceba3 --- /dev/null +++ b/.agent/demos/d1-receipts/evidence.ndjson @@ -0,0 +1,11 @@ +{"ref": "artifact:d1-receipts-evidence", "cited_for": "Demo Packet v2 receipt root", "verified_via": "committed evidence.ndjson"} +{"ref": "session:claude-code-session:5ecdb160-495a-4d9b-b80a-3a24886af8cc", "cited_for": "session_refs kind=pull_request resolves this session to Sinity/polylogue#3282", "verified_via": "sqlite3 index.db: SELECT session_id,repo,ref_number,url FROM session_refs WHERE kind='pull_request' AND repo='Sinity/polylogue' AND ref_number=3282"} +{"ref": "block:claude-code-session:5ecdb160-495a-4d9b-b80a-3a24886af8cc:ae9a4788-4bf6-4b89-b01e-90f10e622981:0", "cited_for": "the gh pr create tool_use --body text is byte-identical to the PR body later fetched live via gh pr view 3282, including the exact bullet claims checked below", "verified_via": "diff of tool_input command against `gh pr view 3282 --repo Sinity/polylogue --json body`"} +{"ref": "block:claude-code-session:5ecdb160-495a-4d9b-b80a-3a24886af8cc:a98d94d0-831f-4cf6-946b-40bf99179283:0", "cited_for": "gh pr create tool_result: https://github.com/Sinity/polylogue/pull/3282 -- confirms this session actually opened PR #3282, not merely referenced it", "verified_via": "tool_result text (single URL line)"} +{"ref": "block:claude-code-session:5ecdb160-495a-4d9b-b80a-3a24886af8cc:bdacfb31-ecc7-4491-be2f-891f8bfb888b:0", "cited_for": "tool_use invoking `timeout 180 devtools verify --quick`, checking the PR claim 'devtools verify --quick -- pass'", "verified_via": "tool_input command text"} +{"ref": "block:claude-code-session:5ecdb160-495a-4d9b-b80a-3a24886af8cc:9786b513-9ad0-4e25-ad3e-e18a0f60220f:0", "cited_for": "tool_result: structured verify-run JSON, every step exit=0, total_duration_s=32.99, exit_code=0 -- SUPPORTS the claim via structure, not a trusted pass/fail word in prose", "verified_via": "tool_result JSON body (per-step name/duration_s/exit array)"} +{"ref": "block:claude-code-session:5ecdb160-495a-4d9b-b80a-3a24886af8cc:ebe785a2-de91-4de3-9b28-43bd1a8e9596:0", "cited_for": "tool_use invoking devtools test against tests/unit/maintenance/test_rebuild_index_bulk_build.py (+4 more files), checking the Solution-section claim 'rebuild_index bulk FTS materialization checkpoints progress'", "verified_via": "tool_input command text"} +{"ref": "block:claude-code-session:5ecdb160-495a-4d9b-b80a-3a24886af8cc:59a1be2d-7881-4d64-997d-d055dd61aa74:0", "cited_for": "tool_result: pytest summary '123 passed in 8.05s', ok (12.2s) -- SUPPORTS the rebuild_index claim", "verified_via": "tool_result text (pytest summary line)"} +{"ref": "polylogue-6mvg", "cited_for": "the PR body's own tracking-item reference ('Ref polylogue-6mvg'), cited verbatim inside the gh pr create --body text", "verified_via": "block:...ae9a4788... tool_input"} +{"ref": "action.tool_use.count=53,Bash", "cited_for": "56 total tool_use blocks in this session: 53 Bash, 3 Read, 0 Edit, 0 Write -- this is a merge-conductor/orchestration session, not the direct file-editing session", "verified_via": "sqlite3 index.db: SELECT tool_name, count(*) FROM blocks WHERE session_id=... AND block_type='tool_use' GROUP BY tool_name"} +{"ref": "action.negative-control.missing-7-file-devtools-test-invocation", "cited_for": "counterexample: the PR body's exact 7-file `devtools test tests/unit/sources/test_live_batch_support.py ...` string appears ONLY inside the gh-pr-create --body text (i.e. inside the PR body itself), never as an actually-invoked command in this session's blocks -- 0 matching tool_use rows when the gh-pr-create block is excluded", "verified_via": "sqlite3 index.db: SELECT count(*) FROM blocks WHERE session_id=... AND block_type='tool_use' AND tool_input LIKE '%test_live_batch_support.py%' AND tool_input NOT LIKE '%gh pr create%' -- returns 0"} diff --git a/.agent/demos/d1-receipts/finding.yaml b/.agent/demos/d1-receipts/finding.yaml new file mode 100644 index 0000000000..bff1a8571e --- /dev/null +++ b/.agent/demos/d1-receipts/finding.yaml @@ -0,0 +1,6 @@ +archive_cursor: "live-archive:/realm/db/polylogue (read-only, file:...?mode=ro)" +measure_version: demo-packet-v2 +commit_sha: 59744a30bf461a587cc679ee62a432e8cd2cf82a +sample_frame_predicate: "session_refs WHERE kind='pull_request' AND repo='Sinity/polylogue' AND ref_number=3282, resolving to claude-code-session:5ecdb160-495a-4d9b-b80a-3a24886af8cc" +run_date: "2026-07-31" +claim: "session_refs typed pull_request evidence resolves a real merged PR to its authoring/dispatch session, and specific PR-body verification sentences can be checked against that session's own recorded blocks -- with unsupported claims marked as such, not silently believed" diff --git a/.agent/demos/d1-receipts/packet.json b/.agent/demos/d1-receipts/packet.json new file mode 100644 index 0000000000..8c52e4bf23 --- /dev/null +++ b/.agent/demos/d1-receipts/packet.json @@ -0,0 +1,135 @@ +{ + "schema_version": "2.0.0", + "packet_id": "d1-receipts", + "title": "D1 receipts: claim-vs-evidence on a real merged PR", + "mode": "private", + "primary_construct": { + "id": "correlation.session_refs.pr_link", + "statement": "session_refs typed pull_request evidence (Claude Code's own pr-link sidecar record) resolves a real merged PR to its authoring/dispatch session with structural confidence, and that session's own tool_use/tool_result blocks let each PR-body verification claim be checked against real evidence instead of trusted prose.", + "product_primitives": [ + "session_refs (storage table)", + "polylogue read --view correlation", + "insights/session_commit.py:build_correlation_result", + "structural SQL reads over blocks/session_refs for citation" + ] + }, + "claim": { + "statement": "session_refs resolves PR #3282 to claude-code-session:5ecdb160-495a-4d9b-b80a-3a24886af8cc, and 3 of 4 checked PR-body claims are supported by that session's own recorded blocks; the fourth is correctly scored not_supported rather than assumed.", + "declared_before_execution": true, + "scope": "one real merged PR (Sinity/polylogue#3282) and its session_refs-resolved authoring/dispatch session, on the live archive at /realm/db/polylogue", + "status": "supported", + "receipts": ["artifact:d1-receipts-predeclaration", "artifact:d1-receipts-evidence"] + }, + "oracle": { + "description": "The SQL queries and gh CLI output cited are independent of the report prose and re-runnable against the same live archive and GitHub history.", + "independent": true, + "method": "Re-run the reproduce commands in report.md and compare cited block text / query results against evidence.ndjson.", + "expected": {"queries_return_matching_rows": true, "negative_control_returns_zero": true}, + "receipts": ["artifact:d1-receipts-evidence"] + }, + "baseline": { + "name": "regex/time-window PR reference scan (the pre-#3425 default correlation path)", + "method": "Scan message text for #NNN patterns within a time window around the session, with no structural resolution to a specific typed evidence row.", + "result": "The same read --view correlation call surfaces a disagreements entry: the regex heuristic independently found 13 additional PR numbers in this session's message text that are NOT corroborated by typed session_refs evidence -- demonstrating why the typed path is authoritative and the heuristic path is demoted to a disagreement signal, not silently trusted.", + "receipts": ["artifact:d1-receipts-evidence"] + }, + "controls": { + "negative": [ + { + "id": "gh-pr-create-body-url-match-control", + "purpose": "Prevent a stale or edited PR body from being treated as automatically representative of the session's own recorded claim text.", + "expected": {"gh_pr_create_tool_result_url": "https://github.com/Sinity/polylogue/pull/3282"}, + "observed": {"gh_pr_create_tool_result_url": "https://github.com/Sinity/polylogue/pull/3282"}, + "passed": true, + "receipts": ["artifact:d1-receipts-evidence"] + } + ], + "missing_evidence": [ + { + "id": "unverified-7-file-devtools-test-claim", + "purpose": "Require a PR-body claim's supporting evidence to be structurally present in the resolved session, not assumed from the PR body's own prose.", + "expected": {"status": "not_supported_when_no_matching_tool_use_block_exists"}, + "observed": {"status": "not_supported", "matching_tool_use_rows_excluding_gh_pr_create": 0}, + "passed": true, + "receipts": ["artifact:d1-receipts-evidence"] + } + ] + }, + "falsifier": { + "condition": "The gh pr create tool_use body text does not byte-match the live-fetched PR #3282 body, or the devtools verify --quick tool_result JSON contains any step with exit != 0, or the negative-control count in evidence.ndjson is nonzero.", + "evaluation_method": "Apply the stated condition to the committed evidence and re-run the queries in report.md's Reproduce section against the live archive.", + "triggered": false, + "result": "pass", + "receipts": ["artifact:d1-receipts-evidence"] + }, + "results": { + "status": "pass", + "summary": "3 of 4 checked PR-body claims are structurally supported by the session_refs-resolved session's own blocks; the 4th is correctly scored not_supported, and the session is shown to be a merge-conductor session (0 Edit/Write tool_use blocks) rather than the direct file-editing session -- both are real, documented findings, not hidden.", + "measurements": [ + { + "name": "claims_checked", + "value": 4, + "unit": "claims", + "receipts": ["artifact:d1-receipts-evidence"] + }, + { + "name": "claims_supported", + "value": 3, + "unit": "claims", + "receipts": ["artifact:d1-receipts-evidence"] + }, + { + "name": "claims_not_independently_verified", + "value": 1, + "unit": "claims", + "receipts": ["artifact:d1-receipts-evidence"] + }, + { + "name": "tool_use_blocks_edit_or_write", + "value": 0, + "unit": "blocks", + "receipts": ["artifact:d1-receipts-evidence"] + } + ] + }, + "non_claims": [ + "This packet does not prove every sentence in PR #3282's body is independently verified -- only the four claims explicitly checked are scored.", + "This packet does not establish that session_refs correctly resolves every PR reference archive-wide -- only that it resolves this one case with structural evidence.", + "This packet does not reproduce on the public seed corpus (seed 1843); it requires read-only access to the live archive and the Sinity/polylogue GitHub history -- the public-corpus D1 variant is not built by this packet." + ], + "receipts": [ + { + "ref": "artifact:d1-receipts-evidence", + "kind": "artifact", + "description": "Committed evidence rows and block/session citations for this packet.", + "artifact_path": "evidence.ndjson", + "resolved": true, + "sha256": "1370ede81fa593585b216ed9d4f834093cf50cdd9a2e634467f4c267b3628fad" + }, + { + "ref": "artifact:d1-receipts-predeclaration", + "kind": "artifact", + "description": "The committed prompt that states the packet claim before execution.", + "artifact_path": "PROMPT.md", + "resolved": true, + "sha256": "9860720832a1ed3460a8cb9196b1a0682f9223487821c569817e8f5da3098ecd" + } + ], + "reproduction": { + "fixture": "live archive /realm/db/polylogue (read-only) + Sinity/polylogue GitHub history", + "deterministic": false, + "private_data": true, + "commands": [ + "sqlite3 \"file:/realm/db/polylogue/index.db?mode=ro\" \"SELECT session_id, repo, ref_number, url FROM session_refs WHERE kind='pull_request' AND repo='Sinity/polylogue' AND ref_number=3282\"", + "polylogue find \"id:claude-code-session:5ecdb160-495a-4d9b-b80a-3a24886af8cc\" then read --view correlation --format json", + "gh pr view 3282 --repo Sinity/polylogue --json body" + ] + }, + "provenance": { + "archive_cursor": "live-archive:/realm/db/polylogue (read-only, file:...?mode=ro)", + "measure_version": "demo-packet-v2", + "commit_sha": "59744a30bf461a587cc679ee62a432e8cd2cf82a", + "sample_frame_predicate": "session_refs WHERE kind='pull_request' AND repo='Sinity/polylogue' AND ref_number=3282", + "run_date": "2026-07-31" + } +} diff --git a/.agent/demos/d1-receipts/queries.ndjson b/.agent/demos/d1-receipts/queries.ndjson new file mode 100644 index 0000000000..5cc6346dce --- /dev/null +++ b/.agent/demos/d1-receipts/queries.ndjson @@ -0,0 +1,5 @@ +{"text": "SELECT session_id, repo, ref_number, url FROM session_refs WHERE kind='pull_request' AND repo='Sinity/polylogue' AND ref_number=3282", "lowered_spec": {"unit": "session_ref", "predicate_kind": "structural_equality", "table": "session_refs"}} +{"text": "find \"id:claude-code-session:5ecdb160-495a-4d9b-b80a-3a24886af8cc\" then read --view correlation --format json", "lowered_spec": {"unit": "session", "entry": "id", "view": "correlation"}} +{"text": "SELECT tool_name, count(*) FROM blocks WHERE session_id='claude-code-session:5ecdb160-495a-4d9b-b80a-3a24886af8cc' AND block_type='tool_use' GROUP BY tool_name ORDER BY 2 DESC", "lowered_spec": {"unit": "block", "pipeline_stages": ["group:tool_name", "count"]}} +{"text": "SELECT tu.block_id, tr.text FROM blocks tu JOIN blocks tr ON tr.tool_id=tu.tool_id AND tr.block_type='tool_result' AND tr.session_id=tu.session_id WHERE tu.session_id='claude-code-session:5ecdb160-495a-4d9b-b80a-3a24886af8cc' AND tu.tool_input LIKE '%devtools verify --quick%'", "lowered_spec": {"unit": "block", "predicate_kind": "join_tool_use_to_tool_result"}} +{"text": "SELECT count(*) FROM blocks WHERE session_id='claude-code-session:5ecdb160-495a-4d9b-b80a-3a24886af8cc' AND block_type='tool_use' AND tool_input LIKE '%test_live_batch_support.py%' AND tool_input NOT LIKE '%gh pr create%'", "lowered_spec": {"unit": "block", "predicate_kind": "negative_control_count", "expected_result": 0}} diff --git a/.agent/demos/d1-receipts/report.md b/.agent/demos/d1-receipts/report.md new file mode 100644 index 0000000000..16c1c9cd19 --- /dev/null +++ b/.agent/demos/d1-receipts/report.md @@ -0,0 +1,153 @@ +# D1 "The Receipts": Claim-vs-Evidence on a Real Merged PR + +This file is a Demo Finding Packet artifact (`devtools/demo_packet.py` +`PACKET_FILENAMES` contract), not an agent session summary. It is consumed +by `devtools lab policy demo-packet-registry` and read by future operators +reproducing this demo -- it is checked-in repo content, not a report to the +orchestrating agent. + +## Claim + +`session_refs` typed `pull_request` evidence resolves a real merged PR to +its authoring/dispatch session, and specific sentences from that PR's body +can be checked against the session's own recorded `blocks` -- with claims +that have no matching evidence marked as such, not silently trusted. + +## Corpus + +The live archive (`/realm/db/polylogue`, read-only), scoped to one session: +`claude-code-session:5ecdb160-495a-4d9b-b80a-3a24886af8cc`, resolved via +`session_refs WHERE kind='pull_request' AND repo='Sinity/polylogue' AND +ref_number=3282`. This is the real, merged PR +[Sinity/polylogue#3282](https://github.com/Sinity/polylogue/pull/3282) +("perf(storage): defer FTS repair off the live-ingest write path"). + +## Method + +1. Resolved PR #3282 to a session structurally through `session_refs` (the + table PR #3425 populated and PR #3431 wired into + `insights/session_commit.py:build_correlation_result` / + `insights/correlation_view.py`'s `read --view correlation` surface -- + not a regex/time-window guess). +2. Fetched the live PR body via `gh pr view 3282 --json body`. +3. For each individually falsifiable claim in that body, searched the + resolved session's `blocks` table (`tool_use`/`tool_result` joined by + `tool_id`) for matching structural evidence. +4. Recorded each claim's status: `supported` (matching block evidence + found) or `not_supported` (no matching block, regardless of what the PR + prose says). + +## Findings + +Claim-vs-evidence table (full block citations in `evidence.ndjson`): + +| # | PR #3282 claim | Evidence found in the session | Status | +|---|---|---|---| +| 1 | This session authored/opened the PR | `tool_use` block runs `gh pr create --title "perf(storage): defer FTS repair off the live-ingest write path" --body "..."` with body text byte-identical to the live-fetched PR body; `tool_result` returns `https://github.com/Sinity/polylogue/pull/3282` | **supported** | +| 2 | "`devtools verify --quick` -- pass (ruff format/check, mypy, render all, topology/layering/...)" | `tool_use` runs `timeout 180 devtools verify --quick`; `tool_result` is a structured run-JSON with every step's `exit` field `0` (17 steps enumerated, `total_duration_s: 32.99`, top-level `exit_code: 0`) | **supported** (structural -- the exit codes, not a trusted "pass" word) | +| 3 | "`rebuild_index` bulk FTS materialization checkpoints progress (base for #3281, rebased here after that merge)" | `tool_use` runs `devtools test tests/unit/maintenance/test_rebuild_index_bulk_build.py` (+4 more files) in `/realm/worktrees/polylogue-membership-head-provenance`; `tool_result` pytest summary: `123 passed in 8.05s`; a following `git commit` in the same worktree stages exactly `polylogue/maintenance/rebuild_index.py` -- the one file this line's claim is about, matching the PR's own file diff (`polylogue/maintenance/rebuild_index.py 1 1`) | **supported** | +| 4 | "`devtools test tests/unit/sources/test_live_batch_support.py tests/unit/sources/test_live_catchup_planning.py tests/unit/storage/test_revision_replay.py tests/unit/storage/test_fts_identity_ledger.py tests/unit/storage/test_fts_repair_sql.py tests/unit/storage/test_bulk_fts_prefix_reextract.py tests/unit/daemon/test_daemon_cli.py -- all passing (see individual commit messages for per-commit pass counts)" | That exact 7-file string appears **only** inside the `gh pr create --body` tool_input (i.e. inside the PR body text itself) -- 0 rows when searching this session's `tool_use` blocks for the string with the `gh pr create` block excluded | **not independently verified in this session** | + +## Specimens + +See `evidence.ndjson` for the full block-id citations behind each row +above, including the exact `tool_result` text for rows 2 and 3. + +## Counterexamples + +**Finding 4 is a real, structurally-confirmed gap, not an artifact of +sloppy search.** The PR body's own parenthetical for that claim -- +"see individual commit messages for per-commit pass counts" -- already +admits the aggregate 7-file invocation was never run as one shot; this +session's block evidence confirms it structurally: the string is prose +inside the PR body draft, never an executed command. This is the intended +behavior of a claim-vs-evidence packet: a claim the PR body asserts in +prose, with no matching structural evidence in the resolved session, must +render as unsupported -- not silently upgraded because the surrounding +claims (1-3) checked out. + +**This session is a merge-conductor, not the file-editing session.** +`SELECT tool_name, count(*) ... GROUP BY tool_name` over this session's 56 +`tool_use` blocks returns `Bash=53, Read=3` -- zero `Edit`/`Write` blocks. +The PR's actual code changes were authored in separately dispatched worker +sessions across several git worktrees +(`/realm/worktrees/polylogue-membership-head*`); this session orchestrates +`git`/`gh`/`devtools` across them and opens the PR. `session_refs` correctly +resolves PR #3282 to *this* session (the one that ran `gh pr create`), which +is the right target for "which session can I ask about this PR's own +claims" -- but it is not the right target for "which session edited file +X", a different (currently unresolved by this packet) question. + +## Limits + +- This packet checks 4 claims from one PR's body, not every sentence. It is + a method demonstration (structural claim-vs-evidence resolution through + `session_refs`), not an audit of PR #3282's full body. +- This is the **live-archive operator variant** only. The epic's own design + (`polylogue-212`) calls for two variants per demo: a public seeded-corpus + reproduction (seed 1843) and a live-archive operator variant. `session_refs` + `pull_request` rows are a real, provider-native Claude Code capability + (pr-link sidecar records) that the deterministic seed fixture does not + currently populate, so the public variant is not built by this packet -- + named as remaining scope in the owning bead (`polylogue-xyel`) rather than + claimed done here. +- The multi-worktree merge-conductor pattern found in Finding 4/Counterexamples + means `session_refs`'s PR-to-session resolution answers "which session + opened this PR", not "which session wrote this specific line of this + specific file" -- a real, useful distinction this packet surfaces but does + not resolve further (that would need session-to-commit-to-worktree + lineage, which `polylogue-cijx.1`'s notes document as a separate, still- + open problem for the durable `session_commits` table, unrelated to the + `session_refs` mechanism this packet exercises). + +## Non-claims + +- This packet does not prove every sentence in PR #3282's body is + independently verified -- only the four claims explicitly checked above + are scored; claim 4 is explicitly scored `not_supported`. +- This packet does not establish that `session_refs` correctly resolves + every PR reference archive-wide -- only that it resolves this one case + with structural evidence. +- This packet does not reproduce on the public seed corpus (seed 1843); it + requires read-only access to the live archive and the `Sinity/polylogue` + GitHub history. + +## Reproduce + +```bash +# 1. resolve PR -> session +sqlite3 "file:/realm/db/polylogue/index.db?mode=ro" \ + "SELECT session_id, repo, ref_number, url FROM session_refs \ + WHERE kind='pull_request' AND repo='Sinity/polylogue' AND ref_number=3282" + +# 2. cross-check through the CLI's own correlation surface +POLYLOGUE_ARCHIVE_ROOT=/realm/db/polylogue POLYLOGUE_FORCE_PLAIN=1 \ + polylogue find "id:claude-code-session:5ecdb160-495a-4d9b-b80a-3a24886af8cc" \ + then read --view correlation --format json + +# 3. fetch the live PR body +gh pr view 3282 --repo Sinity/polylogue --json body + +# 4. tool_name distribution (merge-conductor finding) +sqlite3 "file:/realm/db/polylogue/index.db?mode=ro" \ + "SELECT tool_name, count(*) FROM blocks \ + WHERE session_id='claude-code-session:5ecdb160-495a-4d9b-b80a-3a24886af8cc' \ + AND block_type='tool_use' GROUP BY tool_name ORDER BY 2 DESC" + +# 5. the devtools verify --quick evidence (claim 2) +sqlite3 "file:/realm/db/polylogue/index.db?mode=ro" \ + "SELECT tr.text FROM blocks tu JOIN blocks tr \ + ON tr.tool_id=tu.tool_id AND tr.block_type='tool_result' AND tr.session_id=tu.session_id \ + WHERE tu.session_id='claude-code-session:5ecdb160-495a-4d9b-b80a-3a24886af8cc' \ + AND tu.tool_input LIKE '%devtools verify --quick%'" + +# 6. the negative-control count (claim 4 -- must return 0) +sqlite3 "file:/realm/db/polylogue/index.db?mode=ro" \ + "SELECT count(*) FROM blocks \ + WHERE session_id='claude-code-session:5ecdb160-495a-4d9b-b80a-3a24886af8cc' \ + AND block_type='tool_use' AND tool_input LIKE '%test_live_batch_support.py%' \ + AND tool_input NOT LIKE '%gh pr create%'" +``` + +See `evidence.ndjson` for every cited ref and `checks.json` for the +pass/fail summary. diff --git a/.agent/demos/d1-receipts/run.log b/.agent/demos/d1-receipts/run.log new file mode 100644 index 0000000000..bdc1a20691 --- /dev/null +++ b/.agent/demos/d1-receipts/run.log @@ -0,0 +1,113 @@ +=== 1. resolve PR -> session (structural, session_refs) === +$ sqlite3 "file:/realm/db/polylogue/index.db?mode=ro" \ + "SELECT session_id, repo, ref_number, url FROM session_refs \ + WHERE kind='pull_request' AND repo='Sinity/polylogue' AND ref_number=3282" +claude-code-session:5ecdb160-495a-4d9b-b80a-3a24886af8cc|Sinity/polylogue|3282|https://github.com/Sinity/polylogue/pull/3282 + +=== 2. cross-check through the CLI's own correlation surface (production read path) === +$ POLYLOGUE_ARCHIVE_ROOT=/realm/db/polylogue POLYLOGUE_FORCE_PLAIN=1 \ + polylogue find "id:claude-code-session:5ecdb160-495a-4d9b-b80a-3a24886af8cc" \ + then read --view correlation --format json +(pr_refs excerpt, one of six duplicate-window matches; source=typed_session_ref proves the + typed session_refs evidence resolved this, not the regex heuristic) +{ + "owner": "Sinity", + "repo": "polylogue", + "number": 3282, + "kind": "pr", + "url": "https://github.com/Sinity/polylogue/pull/3282", + "raw_match": "https://github.com/Sinity/polylogue/pull/3282", + "message_id": null, + "source": "typed_session_ref", + "object_ref": "github-pr:Sinity/polylogue#3282" +} +disagreements: 1 entry -- the regex heuristic path independently found PR numbers +[3212, 3213, 3214, 3215, 3216, 3217, 3262, 3263, 3264, 3271, 3272, 3278, 3281] in message +text that are NOT corroborated by typed session_refs evidence for this session -- surfaced +as a disagreement rather than silently merged into the typed result. + +=== 3. fetch the live PR body === +$ gh pr view 3282 --repo Sinity/polylogue --json body +(full body in evidence.ndjson citation for block ae9a4788-...; byte-identical to the + gh pr create --body tool_input recorded in the session) + +=== 4. tool_name distribution over this session's tool_use blocks (merge-conductor finding) === +$ sqlite3 "file:/realm/db/polylogue/index.db?mode=ro" \ + "SELECT tool_name, count(*) FROM blocks \ + WHERE session_id='claude-code-session:5ecdb160-495a-4d9b-b80a-3a24886af8cc' \ + AND block_type='tool_use' GROUP BY tool_name ORDER BY 2 DESC" +Bash|53 +Read|3 + +=== 5. devtools verify --quick evidence (claim 2) === +$ sqlite3 "file:/realm/db/polylogue/index.db?mode=ro" \ + "SELECT tr.text FROM blocks tu JOIN blocks tr \ + ON tr.tool_id=tu.tool_id AND tr.block_type='tool_result' AND tr.session_id=tu.session_id \ + WHERE tu.session_id='claude-code-session:5ecdb160-495a-4d9b-b80a-3a24886af8cc' \ + AND tu.tool_input LIKE '%devtools verify --quick%'" +(tail of the structured run-JSON result) + { + "name": "verify docs-coverage", + "duration_s": 2.62, + "exit": 0, + "run_id": "20260726T185346Z-quick-611274-f5c7c756" + }, + { + "name": "verify test-infra-currency", + "duration_s": 0.41, + "exit": 0, + "run_id": "20260726T185346Z-quick-611274-f5c7c756" + }, + { + "name": "verify test-clock-hygiene", + "duration_s": 2.48, + "exit": 0, + "run_id": "20260726T185346Z-quick-611274-f5c7c756" + }, + { + "name": "verify pytest-timeout-overrides", + "duration_s": 4.16, + "exit": 0, + "run_id": "20260726T185346Z-quick-611274-f5c7c756" + }, + { + "name": "verify degrade-loudly", + "duration_s": 1.27, + "exit": 0, + "run_id": "20260726T185346Z-quick-611274-f5c7c756" + } + ], + "total_duration_s": 32.99, + "exit_code": 0 +} +(every step in this run's full JSON has "exit": 0; 17 steps total) + +=== 6. rebuild_index test evidence (claim 3) === +$ sqlite3 "file:/realm/db/polylogue/index.db?mode=ro" \ + "SELECT tr.text FROM blocks tu JOIN blocks tr \ + ON tr.tool_id=tu.tool_id AND tr.block_type='tool_result' AND tr.session_id=tu.session_id \ + WHERE tu.session_id='claude-code-session:5ecdb160-495a-4d9b-b80a-3a24886af8cc' \ + AND tu.tool_input LIKE '%test_rebuild_index_bulk_build.py tests/unit/storage/test_planner_statistics_seed.py tests/unit/storage/test_revision_replay.py%'" +2 workers [123 items] +........................................................................ [ 58%] +................................................... [100%] +============================= 123 passed in 8.05s ============================== +ok (12.2s) + +=== 7. negative control: the 7-file devtools test invocation (claim 4) === +$ sqlite3 "file:/realm/db/polylogue/index.db?mode=ro" \ + "SELECT count(*) FROM blocks \ + WHERE session_id='claude-code-session:5ecdb160-495a-4d9b-b80a-3a24886af8cc' \ + AND block_type='tool_use' AND tool_input LIKE '%test_live_batch_support.py%' \ + AND tool_input NOT LIKE '%gh pr create%'" +0 +-- the only match (without the exclusion) is the gh-pr-create block itself, i.e. the string +-- only exists as prose inside the PR body draft, never as an executed command. + +=== 8. gh pr create body/URL evidence (claim 1) === +$ sqlite3 "file:/realm/db/polylogue/index.db?mode=ro" \ + "SELECT tr.text FROM blocks tu JOIN blocks tr \ + ON tr.tool_id=tu.tool_id AND tr.block_type='tool_result' AND tr.session_id=tu.session_id \ + WHERE tu.session_id='claude-code-session:5ecdb160-495a-4d9b-b80a-3a24886af8cc' \ + AND tu.tool_input LIKE '%gh pr create%'" +https://github.com/Sinity/polylogue/pull/3282 diff --git a/.agent/demos/registry.json b/.agent/demos/registry.json index adb893bc82..367c242533 100644 --- a/.agent/demos/registry.json +++ b/.agent/demos/registry.json @@ -22,6 +22,17 @@ "polylogue select" ] }, + { + "slug": "d1-receipts", + "prompt_path": ".agent/demos/d1-receipts/PROMPT.md", + "packet_dir": ".agent/demos/d1-receipts", + "mode": "private", + "required_primitives": [ + "session_refs", + "polylogue find", + "polylogue read --view correlation" + ] + }, { "slug": "anti-demo-multi-source-reconstruction", "prompt_path": ".agent/demos/anti-demo-multi-source-reconstruction/PROMPT.md", diff --git a/polylogue/daemon/convergence_stages.py b/polylogue/daemon/convergence_stages.py index 5dbb03122b..1bf227f035 100644 --- a/polylogue/daemon/convergence_stages.py +++ b/polylogue/daemon/convergence_stages.py @@ -358,6 +358,51 @@ def execute_sessions(session_ids: Sequence[str]) -> StageExecuteReturn: # ── Stage: Claude Workflow evidence ────────────────────────────── +_CLAUDE_WORKFLOW_RECORDED_GAP_LIMIT = 20 + + +def _record_claude_workflow_stage_event(archive_root: Path, summary: object) -> None: + """Persist the materialization summary so a readiness surface can read it. + + ``materialize_claude_workflow_archive`` returns a fresh + ``ClaudeWorkflowMaterializationSummary`` every convergence pass; without + this it was logged once and discarded. Recorded into the disposable + ``ops.db`` tier via the existing generic ``daemon_stage_events`` table (no + schema change) so ``polylogue doctor`` / archive readiness can report the + current gap count instead of only a log line. + """ + gaps = tuple(getattr(summary, "gaps", ())) + payload: dict[str, object] = { + "run_count": getattr(summary, "run_count", 0), + "call_count": getattr(summary, "call_count", 0), + "attempt_count": getattr(summary, "attempt_count", 0), + "linked_session_count": getattr(summary, "linked_session_count", 0), + "unresolved_call_count": getattr(summary, "unresolved_call_count", 0), + "gap_count": len(gaps), + "gaps": list(gaps[:_CLAUDE_WORKFLOW_RECORDED_GAP_LIMIT]), + } + status = "gaps" if gaps else "clean" + try: + from polylogue.storage.archive_readiness import CLAUDE_WORKFLOW_STAGE_NAME + from polylogue.storage.sqlite.archive_tiers.bootstrap import initialize_archive_tier + from polylogue.storage.sqlite.archive_tiers.ops_write import record_daemon_stage_event + from polylogue.storage.sqlite.archive_tiers.types import ArchiveTier + from polylogue.storage.sqlite.connection_profile import open_daemon_connection + + ops_db = archive_root / "ops.db" + ops_db.parent.mkdir(parents=True, exist_ok=True) + with open_daemon_connection(ops_db, timeout=30.0) as conn: + initialize_archive_tier(conn, ArchiveTier.OPS) + record_daemon_stage_event( + conn, + stage=CLAUDE_WORKFLOW_STAGE_NAME, + status=status, + observed_at_ms=int(time.time() * 1000), + payload=payload, + ) + except Exception: + logger.warning("claude-workflow: failed to record materialization stage event", exc_info=True) + def make_claude_workflow_stage(db_path: Path) -> ConvergenceStage: """Rebuild Claude Workflow graphs after any admitted family member changes.""" @@ -398,6 +443,7 @@ def execute(path: Path) -> StageExecuteReturn: summary.attempt_count, len(summary.gaps), ) + _record_claude_workflow_stage_event(archive_root(), summary) return True except Exception: logger.warning("claude-workflow: materialization failed", exc_info=True) diff --git a/polylogue/readiness/__init__.py b/polylogue/readiness/__init__.py index e6909cae13..cd905d4e04 100644 --- a/polylogue/readiness/__init__.py +++ b/polylogue/readiness/__init__.py @@ -37,7 +37,7 @@ component_from_transform_registry, ) from polylogue.storage.archive_identity import archive_file_set_root, resolve_active_index_path -from polylogue.storage.archive_readiness import raw_materialization_ready +from polylogue.storage.archive_readiness import claude_workflow_materialization_status, raw_materialization_ready from polylogue.storage.raw_retention import RawFrontierIntegrityProjection, raw_frontier_integrity_projection from polylogue.storage.repair import ArchiveDebtStatus from polylogue.storage.sqlite.archive_tiers.index import INDEX_SCHEMA_VERSION @@ -579,6 +579,41 @@ def _collect_table_status_best_effort( return derived_statuses, archive_debt +def _claude_workflow_materialization_check(archive_root: Path) -> ReadinessCheck: + """Surface the claude_workflow convergence stage's gap count. + + ``insights/claude_workflow_materializer.py`` computes a fresh gap tuple + every daemon convergence pass; before this it was only ever logged + (``daemon/convergence_stages.py``) and discarded. This reads the value + that stage now persists to ``ops.db`` so "subagents/workflows is a known + sidecar" cannot read as healthy while materialization gaps exist. + """ + status = claude_workflow_materialization_status(archive_root / "ops.db") + if status is None: + return ReadinessCheck( + "claude_workflow_materialization", + VerifyStatus.SKIP, + summary="No Claude Workflow materialization has run against this archive yet", + ) + gap_count = _payload_int(status.get("gap_count")) + raw_gaps = status.get("gaps") + gaps = [str(gap) for gap in raw_gaps] if isinstance(raw_gaps, list) else [] + if gap_count > 0: + example = gaps[0] if gaps else "" + return ReadinessCheck( + "claude_workflow_materialization", + VerifyStatus.WARNING, + count=gap_count, + summary=f"{gap_count} Claude Workflow materialization gap(s), e.g. {example}", + details=gaps, + ) + return ReadinessCheck( + "claude_workflow_materialization", + VerifyStatus.OK, + summary="No Claude Workflow materialization gaps", + ) + + def _raw_frontier_integrity_check(projection: RawFrontierIntegrityProjection) -> ReadinessCheck: """Register the canonical projection in archive/devtools readiness output.""" @@ -640,6 +675,7 @@ def run_archive_readiness(config: Config, *, deep: bool = False, probe_only: boo checks.append(ReadinessCheck("config", VerifyStatus.OK, summary="XDG defaults active")) checks.extend(_config_path_checks(config)) checks.append(_raw_frontier_integrity_check(raw_frontier_projection)) + checks.append(_claude_workflow_materialization_check(archive_root)) # --- database reachability --- db_checks, db_error = _database_probe_checks(config, deep=deep) diff --git a/polylogue/sources/assembly.py b/polylogue/sources/assembly.py index 368bfc2de2..2860d51e1d 100644 --- a/polylogue/sources/assembly.py +++ b/polylogue/sources/assembly.py @@ -20,7 +20,6 @@ from .parsers.chatgpt_sidecars import ChatGPTAssetIndex from .parsers.claude.history import HistoryEntry from .parsers.claude.index import SessionIndexEntry - from .parsers.claude.orchestration import ClaudeOrchestrationArtifact, ClaudeOrchestrationCoverage ClaudeCodeSessionIndex: TypeAlias = dict[str, "SessionIndexEntry"] ClaudeCodeHistoryPasteIndex: TypeAlias = dict[str, list["HistoryEntry"]] @@ -31,9 +30,6 @@ class _ClaudeCodeSidecarData(TypedDict, total=False): session_index: ClaudeCodeSessionIndex history_paste_index: ClaudeCodeHistoryPasteIndex - orchestration_artifacts: tuple[ClaudeOrchestrationArtifact, ...] - orchestration_coverage: ClaudeOrchestrationCoverage - orchestration_parse_gaps: tuple[str, ...] class _CodexSidecarData(TypedDict, total=False): diff --git a/polylogue/sources/assembly_claude_code.py b/polylogue/sources/assembly_claude_code.py index 7dd17c387a..d7e4b5def3 100644 --- a/polylogue/sources/assembly_claude_code.py +++ b/polylogue/sources/assembly_claude_code.py @@ -5,7 +5,7 @@ from hashlib import sha256 from pathlib import Path -from polylogue.core.enums import PasteBoundary, Provider +from polylogue.core.enums import PasteBoundary from polylogue.logging import get_logger from .assembly import ( @@ -13,7 +13,6 @@ ClaudeCodeSessionIndex, SidecarData, ) -from .origin_specs import artifact_rule_for_path from .parsers.base import ParsedMessage, ParsedPasteEvidence, ParsedSession from .parsers.claude.history import HistoryEntry, build_session_paste_index from .parsers.claude.index import ( @@ -21,11 +20,6 @@ enrich_session_from_index, parse_sessions_index, ) -from .parsers.claude.orchestration import ( - ClaudeOrchestrationArtifact, - inventory_claude_orchestration_artifacts, - parse_claude_orchestration_artifact, -) logger = get_logger(__name__) @@ -68,28 +62,9 @@ def discover_sidecars(self, source_paths: list[Path]) -> SidecarData: for hist in history_indices.values(): for session_id, history_entries in hist.items(): merged_history.setdefault(session_id, []).extend(history_entries) - orchestration_paths = [ - path - for path in source_paths - if (rule := artifact_rule_for_path(Provider.CLAUDE_CODE, str(path))) is not None - and rule.parse_policy == "fact" - ] - artifacts: list[ClaudeOrchestrationArtifact] = [] - parse_gaps: list[str] = [] - for path in orchestration_paths: - try: - artifact = parse_claude_orchestration_artifact(str(path), path.read_bytes()) - except (OSError, UnicodeDecodeError, ValueError) as exc: - parse_gaps.append(f"unparseable declared artifact {path}: {type(exc).__name__}") - continue - if artifact is not None: - artifacts.append(artifact) return { "session_index": session_index, "history_paste_index": merged_history, - "orchestration_artifacts": tuple(artifacts), - "orchestration_coverage": inventory_claude_orchestration_artifacts(source_paths), - "orchestration_parse_gaps": tuple(parse_gaps), } def enrich_session( diff --git a/polylogue/sources/parsers/claude/__init__.py b/polylogue/sources/parsers/claude/__init__.py index d426295795..3f481e9052 100644 --- a/polylogue/sources/parsers/claude/__init__.py +++ b/polylogue/sources/parsers/claude/__init__.py @@ -27,7 +27,7 @@ find_sessions_index, parse_sessions_index, ) -from .orchestration import inventory_claude_orchestration_artifacts, parse_claude_orchestration_artifact +from .orchestration import parse_claude_orchestration_artifact def looks_like_ai(payload: object) -> bool: @@ -65,7 +65,6 @@ def parse_memories(payload: Mapping[str, object], fallback_id: str) -> ParsedSes "looks_like_claude_memories", "looks_like_code", "normalize_timestamp", - "inventory_claude_orchestration_artifacts", "parse", "parse_ai", "parse_code", diff --git a/polylogue/sources/parsers/claude/orchestration.py b/polylogue/sources/parsers/claude/orchestration.py index c52f8664a3..3583eb771c 100644 --- a/polylogue/sources/parsers/claude/orchestration.py +++ b/polylogue/sources/parsers/claude/orchestration.py @@ -9,8 +9,7 @@ from __future__ import annotations import json -from collections import Counter -from collections.abc import Iterable, Mapping +from collections.abc import Mapping from dataclasses import dataclass from pathlib import Path @@ -140,14 +139,6 @@ class ClaudeOrchestrationArtifact: parse_error: str | None = None -@dataclass(frozen=True, slots=True) -class ClaudeOrchestrationCoverage: - artifact_counts: dict[str, int] - paired_agent_ids: tuple[str, ...] - run_ids: tuple[str, ...] - gaps: tuple[str, ...] - - def parse_claude_orchestration_artifact( source_path: str, payload: bytes | str | object, @@ -176,45 +167,6 @@ def parse_claude_orchestration_artifact( return ClaudeOrchestrationArtifact(rule.kind, source_path, rule.parse_policy, facts) -def inventory_claude_orchestration_artifacts(paths: Iterable[str | Path]) -> ClaudeOrchestrationCoverage: - """Inventory declared members and report only evidence-backed gaps.""" - - artifacts: list[tuple[str, str]] = [] - transcripts: set[str] = set() - metas: set[str] = set() - runs: set[str] = set() - journals: set[str] = set() - for candidate in paths: - source_path = str(candidate) - rule = artifact_rule_for_path(Provider.CLAUDE_CODE, source_path) - if rule is None: - continue - artifacts.append((rule.kind, source_path)) - name = Path(source_path).name - if rule.kind == "agent_transcript": - if agent_id := _agent_id_from_path(source_path): - transcripts.add(agent_id) - elif rule.kind == "agent_sidecar_meta": - if agent_id := _agent_id_from_path(source_path): - metas.add(agent_id) - elif rule.kind == "workflow_run_snapshot": - runs.add(name.removesuffix(".json")) - elif rule.kind == "workflow_journal": - journals.add(Path(source_path).parent.name) - gaps = [ - *(f"missing agent metadata for transcript {agent_id}" for agent_id in sorted(transcripts - metas)), - *(f"missing agent transcript for metadata {agent_id}" for agent_id in sorted(metas - transcripts)), - *(f"missing workflow run snapshot for journal {run_id}" for run_id in sorted(journals - runs)), - *(f"missing workflow journal for run snapshot {run_id}" for run_id in sorted(runs - journals)), - ] - return ClaudeOrchestrationCoverage( - artifact_counts=dict(sorted(Counter(kind for kind, _ in artifacts).items())), - paired_agent_ids=tuple(sorted(transcripts & metas)), - run_ids=tuple(sorted(runs | journals)), - gaps=tuple(gaps), - ) - - def _decode(payload: bytes | str | object, *, jsonl: bool) -> object: if isinstance(payload, bytes): payload = payload.decode("utf-8") @@ -273,8 +225,6 @@ def _journal_fact(source_path: str, line: int, payload: Mapping[str, object]) -> __all__ = [ "ClaudeOrchestrationArtifact", - "ClaudeOrchestrationCoverage", "ClaudeOrchestrationFact", - "inventory_claude_orchestration_artifacts", "parse_claude_orchestration_artifact", ] diff --git a/polylogue/storage/archive_readiness.py b/polylogue/storage/archive_readiness.py index 9c9620488a..4116ce0830 100644 --- a/polylogue/storage/archive_readiness.py +++ b/polylogue/storage/archive_readiness.py @@ -2,6 +2,7 @@ from __future__ import annotations +import json import sqlite3 import time from collections import Counter @@ -21,6 +22,11 @@ logger = get_logger(__name__) +CLAUDE_WORKFLOW_STAGE_NAME = "claude_workflow" +"""daemon_stage_events ``stage`` value written by the claude_workflow +convergence stage (daemon/convergence_stages.py); imported from there so the +writer and this reader cannot drift apart.""" + ACTIVE_REBUILD_STALE_AFTER_S = 180.0 """Maximum heartbeat/start age for a rebuild-index row to count as active.""" @@ -61,6 +67,50 @@ def active_rebuild_index_attempts(ops_db: Path) -> list[dict[str, object]]: ] +def claude_workflow_materialization_status(ops_db: Path) -> dict[str, object] | None: + """Return the most recently recorded Claude Workflow materialization summary. + + Reads the latest ``daemon_stage_events`` row written by + ``daemon.convergence_stages``'s claude_workflow stage each time it + materializes evidence graphs. Returns ``None`` when the stage has never + run against this archive (ops.db missing, table missing, or no rows). + """ + if not ops_db.exists(): + return None + try: + with closing(sqlite3.connect(f"file:{ops_db}?mode=ro", uri=True)) as conn: + conn.row_factory = sqlite3.Row + has_table = conn.execute( + "SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'daemon_stage_events'" + ).fetchone() + if has_table is None: + return None + row = conn.execute( + """ + SELECT status, observed_at_ms, payload_json + FROM daemon_stage_events + WHERE stage = ? + ORDER BY observed_at_ms DESC, rowid DESC + LIMIT 1 + """, + (CLAUDE_WORKFLOW_STAGE_NAME,), + ).fetchone() + except sqlite3.Error as exc: + logger.warning("claude workflow materialization status query failed for %s: %s", ops_db, exc, exc_info=True) + return None + if row is None: + return None + try: + payload = json.loads(row["payload_json"] or "{}") + except (TypeError, ValueError): + payload = {} + if not isinstance(payload, dict): + payload = {} + payload["status"] = str(row["status"]) + payload["observed_at_ms"] = int(row["observed_at_ms"]) + return payload + + def _read_int(readiness: Mapping[str, Any], key: str) -> int: try: return int(readiness.get(key) or 0) diff --git a/tests/integration/test_claude_workflow_admission.py b/tests/integration/test_claude_workflow_admission.py index 4839342c56..2c65379f3f 100644 --- a/tests/integration/test_claude_workflow_admission.py +++ b/tests/integration/test_claude_workflow_admission.py @@ -37,6 +37,16 @@ UNRELATED_COUNT = 38 +def _status_gap_count(status: dict[str, object]) -> int: + value = status["gap_count"] + return int(value) if isinstance(value, int | float) else 0 + + +def _status_gaps(status: dict[str, object]) -> list[str]: + value = status["gaps"] + return [str(gap) for gap in value] if isinstance(value, list) else [] + + @pytest.mark.asyncio async def test_configured_claude_workflow_admission_preserves_raw_revisions_and_rebuilds( workspace_env: dict[str, Path], @@ -249,6 +259,91 @@ async def test_configured_claude_workflow_admission_preserves_raw_revisions_and_ assert any("missing paired agent metadata sidecar" in gap for gap in degraded.gaps) +@pytest.mark.asyncio +async def test_claude_workflow_convergence_stage_surfaces_gap_through_readiness( + workspace_env: dict[str, Path], + monkeypatch: pytest.MonkeyPatch, +) -> None: + """The claude_workflow convergence stage's gap count must reach doctor readiness. + + Coverage/gap tracking for Claude Workflow artifacts was computed every + convergence pass but only ever logged (bd polylogue-uh9l / + polylogue-z9gh.6): ``polylogue doctor`` could report healthy while + materialization gaps existed. This drives the actual production callers — + ``ConvergenceStage.execute`` (``daemon/convergence_stages.py``, what the + daemon invokes every pass) and ``get_readiness`` (``readiness/__init__``, + what ``polylogue doctor`` reads) — rather than asserting against the + materializer's summary struct in isolation. + + Anti-vacuity: removing the ``_record_claude_workflow_stage_event`` call + from ``execute()``, or removing the + ``_claude_workflow_materialization_check`` registration in + ``run_archive_readiness``, makes the degraded-report assertions below fail + (status stays SKIP / count stays 0 instead of surfacing the real gap). + """ + from polylogue.config import Config + from polylogue.daemon.convergence_stages import make_claude_workflow_stage + from polylogue.readiness import VerifyStatus, get_readiness + from polylogue.storage.archive_readiness import claude_workflow_materialization_status + + archive_root = workspace_env["archive_root"] + claude_root, run_path, first_meta_path = _write_fixture(workspace_env["data_root"] / ".claude") + monkeypatch.setenv("POLYLOGUE_INGEST_PARSE_WORKERS", "1") + + result = await parse_sources_archive( + archive_root, + [Source(name=Provider.CLAUDE_CODE.value, path=claude_root)], + ) + assert result.parse_failures == 0 + + stage = make_claude_workflow_stage(archive_root / "index.db") + assert stage.execute(run_path) is True + + # The fixture bakes in one deliberately unresolved call (ATTEMPT_COUNT-1 + # attempts are clean; see the module docstring / summary.unresolved_call_count + # in the sibling admission test), so the baseline is not gap-free -- assert + # against it rather than assuming zero. + baseline_status = claude_workflow_materialization_status(archive_root / "ops.db") + assert baseline_status is not None + baseline_gap_count = _status_gap_count(baseline_status) + baseline_gaps = _status_gaps(baseline_status) + assert "missing paired agent metadata sidecar" not in " ".join(baseline_gaps) + + config = Config(archive_root=archive_root, render_root=archive_root, sources=[]) + baseline_check = next( + check for check in get_readiness(config).checks if check.name == "claude_workflow_materialization" + ) + assert baseline_check.count == baseline_gap_count + if baseline_gap_count == 0: + assert baseline_check.status == VerifyStatus.OK + else: + assert baseline_check.status == VerifyStatus.WARNING + + # Representative source-loss mutation: delete one retained metadata member. + with sqlite3.connect(archive_root / "source.db") as source_conn: + source_conn.execute("PRAGMA foreign_keys = ON") + source_conn.execute("DELETE FROM raw_artifacts WHERE source_path = ?", (str(first_meta_path),)) + source_conn.execute("DELETE FROM raw_sessions WHERE source_path = ?", (str(first_meta_path),)) + source_conn.commit() + + assert stage.execute(run_path) is True + + degraded_status = claude_workflow_materialization_status(archive_root / "ops.db") + assert degraded_status is not None + assert degraded_status["status"] == "gaps" + degraded_gap_count = _status_gap_count(degraded_status) + degraded_gaps = _status_gaps(degraded_status) + assert degraded_gap_count > baseline_gap_count + assert any("missing paired agent metadata sidecar" in gap for gap in degraded_gaps) + + degraded_check = next( + check for check in get_readiness(config).checks if check.name == "claude_workflow_materialization" + ) + assert degraded_check.status == VerifyStatus.WARNING + assert degraded_check.count >= 1 + assert any("missing paired agent metadata sidecar" in detail for detail in degraded_check.details) + + def _write_fixture(claude_root: Path) -> tuple[Path, Path, Path]: project = claude_root / "projects" / "fixture-project" subagents = project / "subagents" diff --git a/tests/unit/sources/test_assembly_claude_code_history.py b/tests/unit/sources/test_assembly_claude_code_history.py index 2c105ae47e..06d70d2797 100644 --- a/tests/unit/sources/test_assembly_claude_code_history.py +++ b/tests/unit/sources/test_assembly_claude_code_history.py @@ -138,32 +138,6 @@ def test_discover_sidecars_handles_missing_history_jsonl(tmp_path: Path) -> None assert sidecar_data["history_paste_index"] == {} -def test_discover_sidecars_parses_declared_orchestration_artifacts_and_reports_gaps(tmp_path: Path) -> None: - project_dir = tmp_path / ".claude" / "projects" / "p" - workflow_dir = project_dir / "workflows" - journal_dir = project_dir / "subagents" / "workflows" / "wf-54" - agent_dir = project_dir / "subagents" - workflow_dir.mkdir(parents=True) - journal_dir.mkdir(parents=True) - workflow = workflow_dir / "wf-54.json" - journal = journal_dir / "journal.jsonl" - transcript = agent_dir / "agent-a.jsonl" - workflow.write_text('{"runId":"wf-54","taskId":"task-7"}', encoding="utf-8") - journal.write_text('{"contentKey":"call-1","agentId":"agent-a"}\n', encoding="utf-8") - transcript.write_text( - '{"type":"user","sessionId":"agent-a","message":{"role":"user","content":"work"}}\n', encoding="utf-8" - ) - - sidecars = ClaudeCodeAssemblySpec().discover_sidecars([workflow, journal, transcript]) - - assert [(artifact.kind, artifact.facts[0].run_id) for artifact in sidecars["orchestration_artifacts"]] == [ - ("workflow_run_snapshot", "wf-54"), - ("workflow_journal", "wf-54"), - ] - assert sidecars["orchestration_coverage"].gaps == ("missing agent metadata for transcript agent-a",) - assert sidecars["orchestration_parse_gaps"] == () - - # --------------------------------------------------------------------------- # enrich_session: strong-identity matching by sessionId + timestamp. # --------------------------------------------------------------------------- diff --git a/tests/unit/sources/test_parsers_claude_code_artifacts.py b/tests/unit/sources/test_parsers_claude_code_artifacts.py index 48ffdd8330..b0f90628bd 100644 --- a/tests/unit/sources/test_parsers_claude_code_artifacts.py +++ b/tests/unit/sources/test_parsers_claude_code_artifacts.py @@ -8,10 +8,7 @@ from polylogue.core.enums import MaterialOrigin, Role from polylogue.sources.parsers.claude import parse_code from polylogue.sources.parsers.claude.common import normalize_timestamp -from polylogue.sources.parsers.claude.orchestration import ( - inventory_claude_orchestration_artifacts, - parse_claude_orchestration_artifact, -) +from polylogue.sources.parsers.claude.orchestration import parse_claude_orchestration_artifact from polylogue.storage.sqlite.archive_tiers.archive import ArchiveStore @@ -176,7 +173,7 @@ def test_parse_code_preserves_tool_result_reclassification_material_origin() -> assert result.messages[0].material_origin is MaterialOrigin.TOOL_RESULT -def test_claude_workflow_artifact_parser_retains_native_facts_and_coverage_gaps() -> None: +def test_claude_workflow_artifact_parser_retains_native_facts() -> None: run = parse_claude_orchestration_artifact( "/tmp/.claude/projects/x/workflows/wf-54.json", json.dumps({"runId": "wf-54", "taskId": "task-7", "resumeFromRunId": "wf-53", "scriptHash": "abc"}), @@ -191,27 +188,6 @@ def test_claude_workflow_artifact_parser_retains_native_facts_and_coverage_gaps( assert journal is not None and journal.facts[0].content_key == "call-1" assert journal.facts[0].payload["structuredResult"] == {"ok": True} - coverage = inventory_claude_orchestration_artifacts( - ( - "/tmp/.claude/projects/x/workflows/wf-54.json", - "/tmp/.claude/projects/x/subagents/workflows/wf-54/journal.jsonl", - "/tmp/.claude/projects/x/subagents/agent-a.jsonl", - "/tmp/.claude/projects/x/subagents/agent-b.meta.json", - "/tmp/.claude/projects/x/jobs/session-a/adopt.json", - ) - ) - assert coverage.artifact_counts == { - "adopt_manifest": 1, - "agent_sidecar_meta": 1, - "agent_transcript": 1, - "workflow_journal": 1, - "workflow_run_snapshot": 1, - } - assert coverage.gaps == ( - "missing agent metadata for transcript agent-a", - "missing agent transcript for metadata agent-b", - ) - def test_claude_agent_prompt_needs_positive_human_provenance() -> None: generated = parse_code( diff --git a/tests/unit/storage/test_archive_readiness.py b/tests/unit/storage/test_archive_readiness.py index a01920eb6c..6714cc2701 100644 --- a/tests/unit/storage/test_archive_readiness.py +++ b/tests/unit/storage/test_archive_readiness.py @@ -8,7 +8,12 @@ import pytest from polylogue.archive.revision_authority import BYTE_AUTHORITY_CENSUS_DETAIL -from polylogue.storage.archive_readiness import raw_materialization_readiness_snapshot, raw_materialization_ready +from polylogue.storage.archive_readiness import ( + CLAUDE_WORKFLOW_STAGE_NAME, + claude_workflow_materialization_status, + raw_materialization_readiness_snapshot, + raw_materialization_ready, +) from polylogue.storage.raw_authority import ( RawReplayPlan, RawReplayPlanOutcome, @@ -918,3 +923,39 @@ def test_raw_materialization_ready_rejects_failed_debt_classifier() -> None: } assert raw_materialization_ready(clean) is True assert raw_materialization_ready({**clean, "debt_classifier_error": "RuntimeError: ops.db locked"}) is False + + +def test_claude_workflow_materialization_status_missing_ops_db_returns_none(tmp_path: Path) -> None: + assert claude_workflow_materialization_status(tmp_path / "ops.db") is None + + +def test_claude_workflow_materialization_status_reads_latest_stage_event(tmp_path: Path) -> None: + """Reads back exactly what daemon/convergence_stages.py's claude_workflow + stage persists via record_daemon_stage_event -- the wiring this bead adds + so a materialization gap count survives past one log line (bd polylogue-uh9l). + """ + from polylogue.storage.sqlite.archive_tiers.bootstrap import initialize_archive_tier + from polylogue.storage.sqlite.archive_tiers.ops_write import record_daemon_stage_event + from polylogue.storage.sqlite.archive_tiers.types import ArchiveTier + + ops_db = tmp_path / "ops.db" + conn = sqlite3.connect(ops_db) + try: + initialize_archive_tier(conn, ArchiveTier.OPS) + record_daemon_stage_event( + conn, + stage=CLAUDE_WORKFLOW_STAGE_NAME, + status="gaps", + observed_at_ms=1_700_000_000_000, + payload={"gap_count": 2, "gaps": ["missing agent metadata for transcript agent-a", "unresolved call x"]}, + ) + conn.commit() + finally: + conn.close() + + status = claude_workflow_materialization_status(ops_db) + assert status is not None + assert status["status"] == "gaps" + assert status["gap_count"] == 2 + assert status["gaps"] == ["missing agent metadata for transcript agent-a", "unresolved call x"] + assert status["observed_at_ms"] == 1_700_000_000_000