diff --git a/.agents/pm/history/pm-github-closeout-attribution.jsonl b/.agents/pm/history/pm-github-closeout-attribution.jsonl new file mode 100644 index 00000000..78507a4c --- /dev/null +++ b/.agents/pm/history/pm-github-closeout-attribution.jsonl @@ -0,0 +1,96 @@ +{"hash_algorithm":"sha256","ts":"2026-10-24T06:16:55.856Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"2856729d2704e2f19d4897af","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":{"value":"implementer","source":"argv"},"topic":null},"op":"create","patch":[{"op":"add","path":"/metadata/id","value":"pm-github-closeout-attribution"},{"op":"add","path":"/metadata/title","value":"Hold ambiguous merged-PR delivery until attribution is verified"},{"op":"add","path":"/metadata/description","value":"Follow up the completed declaration matcher with explicit item delivery links, acceptance-criteria chronology and needs_review candidates; pm-ado issue 33 and PR 32 expose incorrect durable delivery provenance."},{"op":"add","path":"/metadata/type","value":"Issue"},{"op":"add","path":"/metadata/status","value":"open"},{"op":"add","path":"/metadata/priority","value":1},{"op":"add","path":"/metadata/tags","value":["changelog-unreleased","closeout","gh:unbraind/pm-ado#33"]},{"op":"add","path":"/metadata/created_at","value":"2026-10-24T06:16:55.856Z"},{"op":"add","path":"/metadata/updated_at","value":"2026-10-24T06:16:55.856Z"},{"op":"add","path":"/metadata/author","value":"codex-agent"},{"op":"add","path":"/metadata/acceptance_criteria","value":"Explicit delivery link wins; otherwise exactly one claimed merged PR after the latest criteria change may close; ambiguous items remain open with all candidates reported; completion uses the chosen merge timestamp."}],"before_hash":"3cc22dff72be7b14824654a7a64ea62b04799939b2fee54c1b5f52ca60bf6df0","after_hash":"23fb53c9f884128ce63c4d8053843e615c62296fcb6dacb9feec7621289ed465","item_hash_version":3,"message":"","context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"c50cd6c64ee972b8837fa232d039a8a546056500c8dacbdb05c8a03105d8b1e0"} +{"hash_algorithm":"sha256","ts":"2026-10-24T06:16:56.929Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"2856729d2704e2f19d4897af","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":{"value":"implementer","source":"argv"},"topic":null},"op":"claim","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T06:16:56.929Z"},{"op":"add","path":"/metadata/assignee","value":"codex-agent"},{"op":"add","path":"/metadata/claim_principal","value":"codex-agent"}],"before_hash":"23fb53c9f884128ce63c4d8053843e615c62296fcb6dacb9feec7621289ed465","after_hash":"c9f108cc11beb6b5e273e4a57ed7b340ff7050183605d5d8aa7080737efa8f25","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"ac2b06a6865e8083526a7f313e847b108d333a9671d7e374fc4cb40e0207f131"} +{"hash_algorithm":"sha256","ts":"2026-10-24T06:16:58.175Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"2856729d2704e2f19d4897af","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":{"value":"implementer","source":"argv"},"topic":null},"op":"update","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T06:16:58.175Z"},{"op":"replace","path":"/metadata/status","value":"in_progress"}],"before_hash":"c9f108cc11beb6b5e273e4a57ed7b340ff7050183605d5d8aa7080737efa8f25","after_hash":"76fa49c0892ba861eaa4a6dbc7628dfd8a5e620bb54e98dac6af3d8666b9343c","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"9e60de32bb74e8f79326feb11c60fbd3c813afd6507899c42684e30415eb6102"} +{"hash_algorithm":"sha256","ts":"2026-10-24T06:16:59.176Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"2856729d2704e2f19d4897af","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T06:16:59.176Z"},{"op":"add","path":"/metadata/comments","value":[{"created_at":"2026-10-24T06:16:59.176Z","author":"codex-agent","text":"Read pm-ado issue 33 and dirty PR 32. Existing pm-github-uqjf is closed and covers declarations; this follow-up replaces its latest-merge tie-break with explicit delivery attribution and acceptance-history checks. Plan: real non-git tracker regressions, function-only revert proof, focused local gates and npm/Bun CI release gates. PR 32 will be closed with cross-links after the fix PR exists; issue 33 stays open."}]}],"before_hash":"76fa49c0892ba861eaa4a6dbc7628dfd8a5e620bb54e98dac6af3d8666b9343c","after_hash":"a3b65a8a92ce55971bee99d5166384fd7b91b217ba49a35f181246a9ecbebceb","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"c579258d5bfb626750348cf119038c519ff1d0d46a1d374b627b908b6dc7fa54"} +{"hash_algorithm":"sha256","ts":"2026-10-24T06:20:57.502Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"2856729d2704e2f19d4897af","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"files_add","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T06:20:57.502Z"},{"op":"add","path":"/metadata/files","value":[{"path":".github/workflows/pm-github-closeout.yml","scope":"project"},{"path":"closeout.ts","scope":"project"},{"path":"test/closeout-attribution.test.ts","scope":"project"},{"path":"test/closeout-selection.test.ts","scope":"project"},{"path":"test/closeout-workflow.test.ts","scope":"project"},{"path":"test/closeout.test.ts","scope":"project"}]}],"before_hash":"a3b65a8a92ce55971bee99d5166384fd7b91b217ba49a35f181246a9ecbebceb","after_hash":"4606005df390f1c0fbe1a91ce3824d0da3d5c7f14678709f61c5e9a4b4de2109","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"maintenance","record_hash_version":1,"record_hash":"0071a774e4152352b133364a5e77a0b3af99cc574248e68f0ea3692745c67082"} +{"hash_algorithm":"sha256","ts":"2026-10-24T06:20:58.864Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"2856729d2704e2f19d4897af","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"docs_add","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T06:20:58.864Z"},{"op":"add","path":"/metadata/docs","value":[{"path":"docs/explicit-closeout-attribution.md","scope":"project"},{"path":"README.md","scope":"project"}]}],"before_hash":"4606005df390f1c0fbe1a91ce3824d0da3d5c7f14678709f61c5e9a4b4de2109","after_hash":"92303e4a379a434dbe62e159d1a70ecb6341c473f1a92f7aec45ddc3d82193f0","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"maintenance","record_hash_version":1,"record_hash":"a882bcadbe434d138879d104bb149cc3ae50656c7a2708135a62493586029fa1"} +{"hash_algorithm":"sha256","ts":"2026-10-24T06:21:00.239Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"2856729d2704e2f19d4897af","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"tests_add","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T06:21:00.239Z"},{"op":"add","path":"/metadata/tests","value":[{"command":"node --test test/closeout-selection.test.ts","scope":"project","provenance":{"author":"codex-agent","created_at":"2026-10-24T06:21:00.205Z","source_kind":"local_mutation","source_ref":"fix/closeout-delivery-attribution"}}]}],"before_hash":"92303e4a379a434dbe62e159d1a70ecb6341c473f1a92f7aec45ddc3d82193f0","after_hash":"504feaf046be451c818b4c54be15cee82c886e636cacfadb2c92022d97c06b50","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"maintenance","record_hash_version":1,"record_hash":"78a52998aea763fc5363f9f2f563fbcb4811536e4a7dfe17541a47106a0e32e3"} +{"hash_algorithm":"sha256","ts":"2026-10-24T06:21:01.545Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"2856729d2704e2f19d4897af","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/1","value":{"created_at":"2026-10-24T06:21:01.545Z","author":"codex-agent","text":"Implemented explicit delivery-pr URL tags, immutable SDK acceptance-history cutoff, unique body/commit claims and needs_review rows retaining all candidates. Check success no longer breaks ties or falls back to old delivery. Review-only workflow runs persist a privacy-gated closeout-review.json report and open a PR without closing any item. Initial non-git selection suite passes 13/13; existing chronological tie-break assertions updated to require review. Dependency pins and gate thresholds unchanged."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T06:21:01.545Z"}],"before_hash":"504feaf046be451c818b4c54be15cee82c886e636cacfadb2c92022d97c06b50","after_hash":"3b23991031a840ec870b378f37c65ff47daab0544103971a93d45ef81099c4e2","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"6458dc7431d1a3a4b1e02c57e02ec8f3273c39f9f4d6e84ac5764df8d63ab0fd"} +{"hash_algorithm":"sha256","ts":"2026-10-24T06:24:07.508Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"2856729d2704e2f19d4897af","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/2","value":{"created_at":"2026-10-24T06:24:07.508Z","author":"codex-agent","text":"Pinned pm test run passed selection 13/13 with no failures or skips; test-result storage is disabled by existing project policy, so this comment retains the receipt. Independently inspected pm-ado PRs 16 and 19: 16 explicitly leaves statement enforcement open; 19 declares the item and merged at 2026-10-08T01:19:51Z. Added a review-only workflow report; held attribution also disables automatic landing after dispatching caller checks."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T06:24:07.508Z"}],"before_hash":"3b23991031a840ec870b378f37c65ff47daab0544103971a93d45ef81099c4e2","after_hash":"d0d4c997431500594016e4170ab5ea9b3ebda28765a500a51118abb7f1050d9a","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"a9ba1a3f863eb96d47e78cb152f4f8013158ffc215c5ad4756e221957f3c075e"} +{"hash_algorithm":"sha256","ts":"2026-10-24T06:25:34.878Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"2856729d2704e2f19d4897af","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/3","value":{"created_at":"2026-10-24T06:25:34.878Z","author":"codex-agent","text":"Behavioral revert proof: replaced only runCloseout with its f4c29e8 origin/main implementation, preserving current helpers and tests. Ran node --test --test-name-pattern='ambiguous linked merged PRs|explicit delivery link selects|a claim before or equal' test/closeout-selection.test.ts. Reverted function: exit 1, 0/3 pass and 3 ERR_ASSERTION failures (ambiguous delivery closed, explicit override ignored, pre-criteria claim closed). Restored function: exit 0, 3/3 pass, no skips. Tests import TypeScript source directly; no dist rebuild is needed. Focused c8 reports closeout.ts at exact 100/100/100/100; one pre-update array-order assertion was corrected and its full attribution/selection suites now pass 25/25."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T06:25:34.878Z"}],"before_hash":"d0d4c997431500594016e4170ab5ea9b3ebda28765a500a51118abb7f1050d9a","after_hash":"760016eafc3925098d9a06c863f9e0f7aefdff05a16e14ce042fdca9ba5e8257","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"3453e232a838ee510beea245b4ea67d7e03a24cbb780744f20f842098ca558fe"} +{"hash_algorithm":"sha256","ts":"2026-10-24T06:25:36.443Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"2856729d2704e2f19d4897af","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"tests_add","patch":[{"op":"add","path":"/metadata/tests/1","value":{"command":"node --test test/closeout.test.ts test/closeout-attribution.test.ts test/closeout-selection.test.ts","scope":"project","provenance":{"author":"codex-agent","created_at":"2026-10-24T06:25:36.399Z","source_kind":"local_mutation","source_ref":"fix/closeout-delivery-attribution"}}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T06:25:36.443Z"}],"before_hash":"760016eafc3925098d9a06c863f9e0f7aefdff05a16e14ce042fdca9ba5e8257","after_hash":"7522961484744d1d4c65d530335bb5507dbd98fde032d013ec1dfdbfe121c479","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"maintenance","record_hash_version":1,"record_hash":"5b6351794d01a186d878c9a3d42895195787bae8645d2fbf61aa1536eeb83497"} +{"hash_algorithm":"sha256","ts":"2026-10-24T06:25:37.829Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"2856729d2704e2f19d4897af","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"tests_add","patch":[{"op":"add","path":"/metadata/tests/2","value":{"command":"node --test test/closeout-workflow.test.ts","scope":"project","provenance":{"author":"codex-agent","created_at":"2026-10-24T06:25:37.785Z","source_kind":"local_mutation","source_ref":"fix/closeout-delivery-attribution"}}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T06:25:37.829Z"}],"before_hash":"7522961484744d1d4c65d530335bb5507dbd98fde032d013ec1dfdbfe121c479","after_hash":"15f2cea7db843a928538a016456ad7eab94a0c052a2656e3e33061b7332f7185","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"maintenance","record_hash_version":1,"record_hash":"d6d78ffa1fbf6a85c20980335f6f2cef9abbbf79e22c3a448d6ca9e9d14439d7"} +{"hash_algorithm":"sha256","ts":"2026-10-24T06:29:51.380Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"2856729d2704e2f19d4897af","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/4","value":{"created_at":"2026-10-24T06:29:51.379Z","author":"codex-agent","text":"PR https://github.com/unbraind/pm-github/pull/174 is ready for review. Closed DIRTY pm-ado PR 32 with links to this fix and pm-ado issue 33; verified state CLOSED with mergedAt null and issue 33 still OPEN. No pm-ado branch or tracker edits. Added a completeness guard: unreadable commit messages on a qualifying linked mention may hide a competing claim, so they hold automatic selection. Narrow guard-only revert made the new regression fail ERR_ASSERTION (closed 1 instead of 0, exit 1); restored guard and pre-criteria negative control pass 2/2."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T06:29:51.380Z"}],"before_hash":"15f2cea7db843a928538a016456ad7eab94a0c052a2656e3e33061b7332f7185","after_hash":"f3d99c7eba44b6f7255be01ae57a90d87858a5367c77f888c8563944ac6b0c1f","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"503615b1a545ac50cd1dec269ec42b29b9de68a009e7a5972d145e6b2da8ea4f"} +{"hash_algorithm":"sha256","ts":"2026-10-24T06:30:38.823Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"2856729d2704e2f19d4897af","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/5","value":{"created_at":"2026-10-24T06:30:38.823Z","author":"codex-agent","text":"Final focused command node --test test/closeout.test.ts test/closeout-attribution.test.ts test/closeout-selection.test.ts passes 117/117, zero failures/cancellations/skips. Focused c8 measures exact 100/100/100/100 for closeout.ts at the completed implementation. Full workflow suite passes 34/34, plus the updated automatic-landing hold regression passes 1/1. Typecheck, test typecheck, lint, docstrings (201 declarations), zero source duplication, changelog checks and clean-environment strict tracker health pass. Initial CI is running on the earlier source commit; final npm/Bun release-gate evidence will be reported in PR 174 checks and body after this last implementation update. Item stays in progress for orchestrator verification."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T06:30:38.823Z"}],"before_hash":"f3d99c7eba44b6f7255be01ae57a90d87858a5367c77f888c8563944ac6b0c1f","after_hash":"6e97fccf4013139eeee4c633910b88c7d8828e20914b8c9aa1afd693f053bc34","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"3bde2285c607df907e753152da32f99a9a556f240472c983ef813fa2003fb4b9"} +{"hash_algorithm":"sha256","ts":"2026-10-24T06:30:39.859Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"2856729d2704e2f19d4897af","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":{"value":"implementer","source":"argv"},"topic":null},"op":"release","patch":[{"op":"remove","path":"/metadata/claim_principal"},{"op":"remove","path":"/metadata/assignee"},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T06:30:39.859Z"}],"before_hash":"6e97fccf4013139eeee4c633910b88c7d8828e20914b8c9aa1afd693f053bc34","after_hash":"9e87ec035d4fa0a56a993ee370fec10a7a6b56e81594f9ac4d8d39850b37239e","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"maintenance","record_hash_version":1,"record_hash":"f7f4a7dd2caf0e77d4ca894de36247eb2fa4230fe233a468c39b96f31a2b39fc"} +{"hash_algorithm":"sha256","ts":"2026-10-24T06:42:47.598Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"2856729d2704e2f19d4897af","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":{"value":"implementer","source":"argv"},"topic":null},"op":"claim","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T06:42:47.598Z"},{"op":"add","path":"/metadata/assignee","value":"codex-agent"},{"op":"add","path":"/metadata/claim_principal","value":"codex-agent"}],"before_hash":"9e87ec035d4fa0a56a993ee370fec10a7a6b56e81594f9ac4d8d39850b37239e","after_hash":"bcf72f38cdc6206b30037d89a23b8936179d4550ef8ce8aba456d4307be26be2","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"6ebbbc17603d44d0ca6ad01dacb5fb278803026a5b162f7e20594bcdbc19413f"} +{"hash_algorithm":"sha256","ts":"2026-10-24T06:42:48.830Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"2856729d2704e2f19d4897af","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/6","value":{"created_at":"2026-10-24T06:42:48.829Z","author":"codex-agent","text":"CI run 40070500891 exposed three cross-suite fixture/contract failures: Node 26 release gate ran 1121 tests, 1118 passed and 3 failed; all authored sources still measured exact 100/100/100/100. Two changelog-placement fixtures create the item after the synthetic delivery date; the maintenance equality assertion does not account for the new closeout-only attribution hold. Correcting fixture chronology with the deterministic SDK clock and preserving exact parity for the remaining maintenance shell. This failed run is not full-gate approval."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T06:42:48.830Z"}],"before_hash":"bcf72f38cdc6206b30037d89a23b8936179d4550ef8ce8aba456d4307be26be2","after_hash":"9960f2a899743a12573b229703797f5cf7ec7f3ed716f217ce4f535e48889292","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"2427ee067569d6dcdd62f89009811995080990efcc8ff8721cc04154846c2116"} +{"hash_algorithm":"sha256","ts":"2026-10-24T06:42:49.937Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"2856729d2704e2f19d4897af","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"files_add","patch":[{"op":"replace","path":"/metadata/files/5/path","value":"test/closeout-selection.test.ts"},{"op":"replace","path":"/metadata/files/4/path","value":"test/closeout-attribution.test.ts"},{"op":"replace","path":"/metadata/files/3/path","value":"test/changelog-placement.test.ts"},{"op":"replace","path":"/metadata/files/2/path","value":"test/bot-pr-workflow.test.ts"},{"op":"add","path":"/metadata/files/6","value":{"path":"test/closeout-workflow.test.ts","scope":"project"}},{"op":"add","path":"/metadata/files/7","value":{"path":"test/closeout.test.ts","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T06:42:49.937Z"}],"before_hash":"9960f2a899743a12573b229703797f5cf7ec7f3ed716f217ce4f535e48889292","after_hash":"bca8e3eb673848f038e0f68e84e990666c6456a0d711884948884d36adb3f897","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"maintenance","record_hash_version":1,"record_hash":"b48cc300874bff8ca88e0ffb50b32dee8ee4b79f0ed49ae359775f4ffd60c4ce"} +{"hash_algorithm":"sha256","ts":"2026-10-24T06:42:51.036Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"2856729d2704e2f19d4897af","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"tests_add","patch":[{"op":"add","path":"/metadata/tests/3","value":{"command":"node --test test/bot-pr-workflow.test.ts test/changelog-placement.test.ts","scope":"project","provenance":{"author":"codex-agent","created_at":"2026-10-24T06:42:51.005Z","source_kind":"local_mutation","source_ref":"fix/closeout-delivery-attribution"}}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T06:42:51.036Z"}],"before_hash":"bca8e3eb673848f038e0f68e84e990666c6456a0d711884948884d36adb3f897","after_hash":"60ab92ca6348b10af948cfc874c27e4d7cb14e120784031f0d050341bda5db39","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"maintenance","record_hash_version":1,"record_hash":"d334009cef5e7196e4de121ed49c8bf805b96bed526bcad0df814990f7dc6b1f"} +{"hash_algorithm":"sha256","ts":"2026-10-24T06:43:28.268Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"2856729d2704e2f19d4897af","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/7","value":{"created_at":"2026-10-24T06:43:28.268Z","author":"codex-agent","text":"Cross-suite corrections pass: node --test test/bot-pr-workflow.test.ts test/changelog-placement.test.ts = 59/59, zero failures/cancellations/skips. Changelog fixtures now create items with a deterministic September SDK clock before October delivery; commit evidence correctly returns an empty list. The parity assertion requires exactly one closeout attribution-hold block and exact equality for all remaining maintenance code. Existing end-to-end review-only acceptance proves this hold dispatches checks without enabling automatic landing. Test typecheck and lint pass. Runtime source remains identical to 8b0dc30; full CI will rerun on the corrected test head."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T06:43:28.268Z"}],"before_hash":"60ab92ca6348b10af948cfc874c27e4d7cb14e120784031f0d050341bda5db39","after_hash":"d0979502746d61c048b705404cd496b24f448c93a496217247e40fbdc43b9cbf","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"c6e21dc73172ed952386a67ae6543f4518668c2a7b2e2c7ec4e45a08df8e5479"} +{"hash_algorithm":"sha256","ts":"2026-10-24T06:43:29.290Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"2856729d2704e2f19d4897af","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":{"value":"implementer","source":"argv"},"topic":null},"op":"release","patch":[{"op":"remove","path":"/metadata/claim_principal"},{"op":"remove","path":"/metadata/assignee"},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T06:43:29.290Z"}],"before_hash":"d0979502746d61c048b705404cd496b24f448c93a496217247e40fbdc43b9cbf","after_hash":"b1674ab9450c52589ec40bc20cecb0e25c3ce9cef239be56e4cb63c1634a28a4","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"maintenance","record_hash_version":1,"record_hash":"ad63b6ccffdc608b8b5a41f9161495e837ed2f23f6733814d4b85e7d73053712"} +{"hash_algorithm":"sha256","ts":"2026-10-24T07:45:59.969Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"c26d6c83612a7d502815d5fb","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":{"value":"implementer","source":"argv"},"topic":null},"op":"claim","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T07:45:59.969Z"},{"op":"add","path":"/metadata/assignee","value":"codex-agent"},{"op":"add","path":"/metadata/claim_principal","value":"codex-agent"}],"before_hash":"b1674ab9450c52589ec40bc20cecb0e25c3ce9cef239be56e4cb63c1634a28a4","after_hash":"1eedc5c8077a6d087ab9f5af0e1958ad684be3e2333ec9b3a60b04e8d7d4d7a6","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"18513237982f81e475483a02524d1076d306c9803ae13939dab0b4db3c0d4b00"} +{"hash_algorithm":"sha256","ts":"2026-10-24T07:46:02.119Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"c26d6c83612a7d502815d5fb","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/8","value":{"created_at":"2026-10-24T07:46:02.118Z","author":"codex-agent","text":"Review round for PR 174: retain every plausible linked merged candidate independently of evidence quality, report evidence gaps and hold ambiguity before mutation; disarm reused workflow PRs before updates and on all exit paths. Add table-driven real-tracker and stateful workflow regressions, record fix-only revert proofs, then run targeted checks and unchanged npm/Bun release gates. Remote branch is current and contains origin/main."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T07:46:02.119Z"}],"before_hash":"1eedc5c8077a6d087ab9f5af0e1958ad684be3e2333ec9b3a60b04e8d7d4d7a6","after_hash":"734f878569b137ece4bad9b1492f2d38a7ed1c6b2452c7ff14b79b66bac6ce02","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"91f4c3ce01024fc3f05fc4b0c1ac4c7f69e30e5f7e1fcc281d2fd1399305aa20"} +{"hash_algorithm":"sha256","ts":"2026-10-24T07:53:26.797Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"c26d6c83612a7d502815d5fb","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/9","value":{"created_at":"2026-10-24T07:53:26.797Z","author":"codex-agent","text":"Class 1 revert proof: replaced only runCloseout with the pre-round implementation, retained current helpers and regression table, and ran node --test --test-name-pattern=\"incomplete candidate holds attribution\" test/closeout-selection.test.ts. Exit 1 with assertion failures, including 1 closure instead of 0 for missing merge SHA plus another valid claim; fetch-error cases also exposed the prior abort instead of a listed hold. Restored runCloseout. The full restored selection suite passed 35/35, zero failures, cancellations or skips. Class 2 revert proof: replaced only both explicit --disable-auto operations with no-ops, retained the workflow and stateful harness, and ran node --test --test-name-pattern=\"earlier armed auto-merge is disabled\" test/closeout-workflow.test.ts. Exit 1 with ERR_ASSERTION true !== false: the earlier request remained armed after the later hold. Restored both disable operations. Further restored-suite validation is in progress."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T07:53:26.797Z"}],"before_hash":"734f878569b137ece4bad9b1492f2d38a7ed1c6b2452c7ff14b79b66bac6ce02","after_hash":"6a37e4057b993a03d84100a9b8dcb8bce218fc8d2de2b5e2736cc1ad8a4e7cc6","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"18948e8a5273003dc577f327f61e88cb0850d216c0a1ba629a73bfe8fcb8c76d"} +{"hash_algorithm":"sha256","ts":"2026-10-24T08:00:32.159Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"c26d6c83612a7d502815d5fb","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/10","value":{"created_at":"2026-10-24T08:00:32.158Z","author":"codex-agent","text":"Evidence-gap rows now preserve all candidate numbers and list missing fields/codes in compact output and the privacy-gated workflow artifact. Unknown merged status/time is not a chronology exclusion; a failed PR read conservatively holds eligible active items because linkage cannot be disproved. The workflow revokes before any guard or update and always cleans up after hold, no publication, disabled CI, failure or cancellation; initial revocation denial blocks publication. Reviewed all exit classes and documented the audit. Maintenance/changelog contracts passed 59/59; typecheck, test typecheck, lint, docstrings and zero duplication passed. Focused coverage measured 100/100/100/100 before the final merge-field reporting helper; full gates will certify the final source. A new parent-scope control correctly held the eligible child as well, so its assertion now checks that the parent is absent from review rows."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T08:00:32.159Z"}],"before_hash":"6a37e4057b993a03d84100a9b8dcb8bce218fc8d2de2b5e2736cc1ad8a4e7cc6","after_hash":"aaed5a2b19b1e2eec0d83ef3549b37d36361d751f57f1f47b432af44652a835d","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"af95c1eb7360317f1f07032dbf6452edaee1183855c2baf2453aedb99bd16867"} +{"hash_algorithm":"sha256","ts":"2026-10-24T08:08:32.104Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"c26d6c83612a7d502815d5fb","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/11","value":{"created_at":"2026-10-24T08:08:32.103Z","author":"codex-agent","text":"Additional class 1 pagination proof: PR commit totals now certify the complete commit-page set. Missing, invalid or mismatched counts hold candidates, including a body mention whose omitted commit could claim delivery. Removed only the commit-count completeness guard and ran node --test --test-name-pattern=\"incomplete candidate holds attribution: .*commits-count\" test/closeout-selection.test.ts: 4/4 failed, exit 1, including 1 closure instead of 0 with another valid claim. Restored the guard. The stateful hold regression and unarmed-PR control pass 2/2 after filtering actual autoMergeRequest state; the corrected fresh-job no-op control passes 1/1. The harness keeps remote PR state across runs and resets only job environment state, matching Actions semantics. Final npm gate is being restarted for this complete pagination fix; prior interrupted gates are not counted as evidence."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T08:08:32.104Z"}],"before_hash":"aaed5a2b19b1e2eec0d83ef3549b37d36361d751f57f1f47b432af44652a835d","after_hash":"bf8cf780c40195fa39da02046896321c22b8c558ee28c68c3c275d60d5fc295d","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"213993a32979790d240d02c94d5b22f7e46b5bd15bb0ad9f3743cfd33db2646e"} +{"hash_algorithm":"sha256","ts":"2026-10-24T08:10:08.391Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"c26d6c83612a7d502815d5fb","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/12","value":{"created_at":"2026-10-24T08:10:08.391Z","author":"codex-agent","text":"Final candidate table contains 20 disqualifying evidence conditions crossed with another valid claim or none (40 cases). The pinned pm linked selection run passed 59/59 before the two negative-count controls were added; those additional controls passed 2/2 separately. Final compiled workflow hold/unarmed/no-op acceptance passed 3/3. The 59-test maintenance/changelog contract suite passed; final typecheck, test typecheck and lint passed. Full npm release-check is running on the complete source and test set, with unchanged thresholds, pins and coverage inventory. Origin main has not advanced. No PM CLI/SDK defect was found; linked test storage remains disabled by existing project policy, so comments retain the receipts."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T08:10:08.391Z"}],"before_hash":"bf8cf780c40195fa39da02046896321c22b8c558ee28c68c3c275d60d5fc295d","after_hash":"1c7eba8737ae69240b21e26bda40774711c0b00ee8aaa26244cb977ac3a2b999","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"0db2a3998dc72c97406026c790f2e40d913247c51506f57f53c26a9acfabc20d"} +{"hash_algorithm":"sha256","ts":"2026-10-24T08:28:53.542Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"c26d6c83612a7d502815d5fb","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/13","value":{"created_at":"2026-10-24T08:28:53.542Z","author":"codex-agent","text":"CI run https://github.com/unbraind/pm-github/actions/runs/40076990035 at e795426: both bun run release:check jobs (Node 22 and 26) completed successfully, each 1177/1177 tests, zero failures and exact 100/100/100/100 across 19 authored modules. Both Complete npm release gate steps also succeeded; their jobs are now running additional native Bun acceptance, so overall CI is not yet approval. The independent local npm gate has completed 1177/1177 tests with zero failures and is finishing coverage/release processing. The final small source change makes the new commit-count property optional for callers of checkCloseoutHead, preserving the head-check API; decoded closeout PRs still require verified totals. Typecheck and test typecheck passed with this compatibility adjustment. Implementation is complete; remaining work is read-only final CI verification. Final receipts will be retained in PR 174 body and the handoff summary. The tracker item remains in progress for orchestrator verification."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T08:28:53.542Z"}],"before_hash":"1c7eba8737ae69240b21e26bda40774711c0b00ee8aaa26244cb977ac3a2b999","after_hash":"c757d1d8c9a2d91edb58c962702558175e7a576896aab7c66d3c5035cc170766","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"9cf49ff607b1db1c677c64193052979e78e44bd443119c0624764ede32b7f463"} +{"hash_algorithm":"sha256","ts":"2026-10-24T08:28:55.908Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"c26d6c83612a7d502815d5fb","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":{"value":"implementer","source":"argv"},"topic":null},"op":"release","patch":[{"op":"remove","path":"/metadata/claim_principal"},{"op":"remove","path":"/metadata/assignee"},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T08:28:55.908Z"}],"before_hash":"c757d1d8c9a2d91edb58c962702558175e7a576896aab7c66d3c5035cc170766","after_hash":"2c766caba0d854674176bbf7ecb6d2f7693667b16eb680b7f2a055088fcb7b6e","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"maintenance","record_hash_version":1,"record_hash":"ec3b397c1e5103038e9bcba919d18c107318759246ceeca5c06220420193d479"} +{"hash_algorithm":"sha256","ts":"2026-10-24T09:04:42.661Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"c26d6c83612a7d502815d5fb","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":{"value":"implementer","source":"argv"},"topic":null},"op":"claim","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T09:04:42.661Z"},{"op":"add","path":"/metadata/assignee","value":"codex-agent"},{"op":"add","path":"/metadata/claim_principal","value":"codex-agent"}],"before_hash":"2c766caba0d854674176bbf7ecb6d2f7693667b16eb680b7f2a055088fcb7b6e","after_hash":"b84412dac254e0901f87b0c5d7daccc4ea8262e6a8920dabf95e1d262dc03da4","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"f6c6b1349cbf25a8dd9ec5ca47ac035c8ee81e1fb9c80329627a5297869006b9"} +{"hash_algorithm":"sha256","ts":"2026-10-24T09:04:43.750Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"c26d6c83612a7d502815d5fb","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/14","value":{"created_at":"2026-10-24T09:04:43.750Z","author":"codex-agent","text":"Final consistency review found a report-state defect in the new unreadable-PR path: an explicit valid delivery tag can resolve attribution and close its item, while the non-selected unreadable PR retains an initial needs_review action without candidate details. Initialize that row as a refusal until attribution is resolved, as for all other candidate rows; unresolved groups still promote every row to needs_review. Add a real-tracker regression that a resolved explicit link cannot leave a held row for its closed item. Supersede pending CI with the corrected final head; do not count the old run as final approval."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T09:04:43.750Z"}],"before_hash":"b84412dac254e0901f87b0c5d7daccc4ea8262e6a8920dabf95e1d262dc03da4","after_hash":"5f6de90f50082b5f5c8f5b12393933bdbda53cf815be9d5bed6e3202657c6137","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"faef51b50cd6d89c585ebe280d752c5797d73a5e3f15063ab15c43d0c0c4a499"} +{"hash_algorithm":"sha256","ts":"2026-10-24T09:14:11.717Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"c26d6c83612a7d502815d5fb","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/15","value":{"created_at":"2026-10-24T09:14:11.717Z","author":"codex-agent","text":"Candidate-source audit also found that unreadable commit sets with reference-free descriptions could hide a competing commit-only claim. Added six unreadable commit conditions crossed with another valid claim or none (12 controls); all 12 fail against the prior candidate collection, including closures using the other claim. Retain possible commit-only links for every eligible active item when commit evidence is incomplete; use the known merge time to exclude only provably pre-criteria claims. Explicit valid delivery tags keep their existing attribution role. The earlier stale-review correction passed 164/164 focused tests at exact closeout coverage, pinned PM selection 62/62 and native Bun 1.3.5 selection 62/62; an isolated action-only reversion failed ERR_ASSERTION and restoration passed 1/1. Final combined source is now frozen for validation."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T09:14:11.717Z"}],"before_hash":"5f6de90f50082b5f5c8f5b12393933bdbda53cf815be9d5bed6e3202657c6137","after_hash":"3a89e43959d04e80816921f6b2485317bd8df8f24f7c14cfcd2bc8a3117717ec","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"be15136e5154c38ac0bf45c6988384fcc2a0c17a40e8922bb8bf6f1437a25807"} +{"hash_algorithm":"sha256","ts":"2026-10-24T09:22:57.812Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"c26d6c83612a7d502815d5fb","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/16","value":{"created_at":"2026-10-24T09:22:57.811Z","author":"codex-agent","text":"Final combined regressions pass: focused closeout command node --test test/closeout.test.ts test/closeout-selection.test.ts test/closeout-attribution.test.ts = 176/176, zero failures/cancellations/skips, exact 100/100/100/100 for closeout.ts. Pinned pm test selection = 74/74; native Bun 1.3.5 selection = 74/74. Latest compiled workflow hold/unarmed/no-op controls = 3/3. Typecheck, test typecheck, lint, docstrings, zero source duplication and privacy passed. Isolated fix-only retention-guard reversion failed 12/12 commit-only controls (including one closure instead of zero), restoration passed 12/12. Isolated action-only reversion failed the stale-review regression with ERR_ASSERTION, restoration passed 1/1. Together with the prior function-only 20-case, count-guard 4-case and auto-merge-request proofs these cover both classes. Main advanced only for release metadata; merge it and regenerate the changelog inside the merge before final CI. Earlier superseded CI runs are not final-head approval."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T09:22:57.812Z"}],"before_hash":"3a89e43959d04e80816921f6b2485317bd8df8f24f7c14cfcd2bc8a3117717ec","after_hash":"0c2c5357a659c859ff32d38508e1c7368aa9a997f34cc5f2f03d610db97e458a","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"05ac531c02e5a64d595d5eea42065797dab3df6c49027baa95e7385966ee1319"} +{"hash_algorithm":"sha256","ts":"2026-10-24T09:27:12.157Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"c26d6c83612a7d502815d5fb","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/17","value":{"created_at":"2026-10-24T09:27:12.156Z","author":"codex-agent","text":"Merged origin/main release 2026.10.24 with the merge still pending; regenerated CHANGELOG.md inside the merge and its check passed. Release metadata changes only index version, manifest/package versions and changelog; closeout implementation and regression source are unchanged from the 176/176 focused, 74/74 PM-linked and 74/74 native Bun receipts. Post-merge typecheck, lint and build passed. Strict pm health including required merge drivers passed with no merge-receipt reconciliation needed. Both classes and all identified candidate-source gaps are implemented and verified locally; final immutable npm/Bun CI will validate the merge head. Final CI receipts will be retained in PR 174 body and the final summary. Item remains in progress for orchestrator verification; release the claim at handoff, never close it."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T09:27:12.157Z"}],"before_hash":"0c2c5357a659c859ff32d38508e1c7368aa9a997f34cc5f2f03d610db97e458a","after_hash":"e292cc5b54e82ad9f1d76704f4078177c1c33800732b38096c0f02991eb3c31d","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"de33f39c067ccbd1d57f09c89151706fb01bdbf7f2272b1909232ddfdc731772"} +{"hash_algorithm":"sha256","ts":"2026-10-24T09:27:13.219Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"c26d6c83612a7d502815d5fb","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":{"value":"implementer","source":"argv"},"topic":null},"op":"release","patch":[{"op":"remove","path":"/metadata/claim_principal"},{"op":"remove","path":"/metadata/assignee"},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T09:27:13.219Z"}],"before_hash":"e292cc5b54e82ad9f1d76704f4078177c1c33800732b38096c0f02991eb3c31d","after_hash":"1098e97fe76da83a928574673b270935ee202d5de7405256bb4c430e61be858d","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"maintenance","record_hash_version":1,"record_hash":"d1c184f7ed9bad802289a6d3cc76711574c395799b2f6af0d787d77042c1aa0a"} +{"hash_algorithm":"sha256","ts":"2026-10-24T10:10:09.159Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"c26d6c83612a7d502815d5fb","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":{"value":"implementer","source":"argv"},"topic":null},"op":"claim","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T10:10:09.159Z"},{"op":"add","path":"/metadata/assignee","value":"codex-agent"},{"op":"add","path":"/metadata/claim_principal","value":"codex-agent"}],"before_hash":"1098e97fe76da83a928574673b270935ee202d5de7405256bb4c430e61be858d","after_hash":"17788598b58a5e2bed7ab7e31dc84b6c722c9761ac6bf9686b0bb009cd3f619d","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"3090e47efcaa2d9d5405f8b0803e7f3b55b0adcf752fa946d04f49a9f997d4af"} +{"hash_algorithm":"sha256","ts":"2026-10-24T10:10:12.774Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"c26d6c83612a7d502815d5fb","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/18","value":{"created_at":"2026-10-24T10:10:12.774Z","author":"codex-agent","text":"Integration audit of the new null-timestamp selection found that confirmed unmerged PRs can reach the workflow incomplete-merge refusal even when another PR uniquely delivers the item. Separate known-unmerged routine skips from incomplete evidence about potentially merged PRs, and add an actual workflow regression with an unmerged reference plus a valid merged delivery. This preserves the prior since-range behavior while retaining unknown merge timestamps. Supersede old CI only after reproducing this integration regression; do not treat earlier results as approval for the correction."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T10:10:12.774Z"}],"before_hash":"17788598b58a5e2bed7ab7e31dc84b6c722c9761ac6bf9686b0bb009cd3f619d","after_hash":"2f68b8b8d4b198b350c1244c674a3202dcb321355a75273a1c14b2321d167d39","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"ee53eeea2591a959d6d4d9b4ec9b6db8289f098d15381b951521ddc3cd8dc361"} +{"hash_algorithm":"sha256","ts":"2026-10-24T10:23:40.276Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"c26d6c83612a7d502815d5fb","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"tests_add","patch":[{"op":"add","path":"/metadata/tests/4","value":{"command":"node --test --test-name-pattern=\"confirmed unmerged references\" test/closeout-workflow.test.ts","scope":"project","provenance":{"author":"codex-agent","created_at":"2026-10-24T10:23:40.236Z","source_kind":"local_mutation","source_ref":"fix/closeout-delivery-attribution"}}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T10:23:40.276Z"}],"before_hash":"2f68b8b8d4b198b350c1244c674a3202dcb321355a75273a1c14b2321d167d39","after_hash":"b4184c2e6c36c0b4f68949d84d589f5cef63b0f1de1cdd8ed0b9d355d3a90df9","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"maintenance","record_hash_version":1,"record_hash":"58ff7f0000e1e4bb49593e4996a6bfc815aa610b16e0f29a8b4c12090aacd075"} +{"hash_algorithm":"sha256","ts":"2026-10-24T10:32:28.062Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"c26d6c83612a7d502815d5fb","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/19","value":{"created_at":"2026-10-24T10:32:28.062Z","author":"codex-agent","text":"Known-unmerged integration regression verified and fixed: raw false merge status is a routine not_merged skip, distinct from incomplete evidence about possibly merged PRs. Both CLI compaction and workflow summary count it; the workflow can publish the unique merged delivery. Final-source focused closeout suite passed 176/176 with exact 100/100/100/100; held/unarmed/no-op/known-unmerged workflow controls passed 4/4. New filtered workflow command is linked and pinned pm test --run passed 1/1. Reverted only the confirmed-unmerged classifier, rebuilt the package, and ran node --test --test-name-pattern=\"confirmed unmerged references\" test/closeout-workflow.test.ts: ERR_ASSERTION exit 1, publication refused with 1 instead of expected 0. Restored classifier, rebuilt, and passed 1/1. Existing ambiguity, commit-only and auto-merge revert proofs remain recorded above. Typecheck, test typecheck, lint and build passed; final immutable CI will certify all-source coverage and native Bun on the corrected head. No PM CLI/SDK defect was found. Keep the item in progress; release the claim for orchestrator verification without closing it."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T10:32:28.062Z"}],"before_hash":"b4184c2e6c36c0b4f68949d84d589f5cef63b0f1de1cdd8ed0b9d355d3a90df9","after_hash":"40c361a386183f5402228f4901c6a8eb6eee67fba78b9b2cc185548bb5d789b5","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"23080f11c15f561f7ee2b964962a9715a30332cb7631603d09c97de5377b9476"} +{"hash_algorithm":"sha256","ts":"2026-10-24T10:32:29.092Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"c26d6c83612a7d502815d5fb","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":{"value":"implementer","source":"argv"},"topic":null},"op":"release","patch":[{"op":"remove","path":"/metadata/claim_principal"},{"op":"remove","path":"/metadata/assignee"},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T10:32:29.092Z"}],"before_hash":"40c361a386183f5402228f4901c6a8eb6eee67fba78b9b2cc185548bb5d789b5","after_hash":"a7af72cb71939b4d7a8bc3c5ac5affc701345062061619be9bdedc5f1d416cf4","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"maintenance","record_hash_version":1,"record_hash":"29518d206d65389768f3990897ec3634b8b682da88ac1de09a5464d5786ad31d"} +{"hash_algorithm":"sha256","ts":"2026-10-24T11:49:08.963Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"2a9f9fc28825b23269e28d86","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":{"value":"implementer","source":"argv"},"topic":null},"op":"claim","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T11:49:08.963Z"},{"op":"add","path":"/metadata/assignee","value":"codex-agent"},{"op":"add","path":"/metadata/claim_principal","value":"codex-agent"}],"before_hash":"a7af72cb71939b4d7a8bc3c5ac5affc701345062061619be9bdedc5f1d416cf4","after_hash":"21c8b33bf58dd78f341113d8dd77c5b8487f2793067491e53283b8082d9e85e9","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"0293d327072e20824c94ba2a23663955fa9c2e9fa18d874f901a49f097cb194f"} +{"hash_algorithm":"sha256","ts":"2026-10-24T11:49:11.401Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"2a9f9fc28825b23269e28d86","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/20","value":{"created_at":"2026-10-24T11:49:11.401Z","author":"codex-agent","text":"Review round 2: audit all closeout read-to-write decisions, bind delivery selection and close provenance to the selected item revision, and exercise peer edits with real SDK mutations in a non-git tracker. Remote branch and main are current; pinned SDK remains unchanged."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T11:49:11.401Z"}],"before_hash":"21c8b33bf58dd78f341113d8dd77c5b8487f2793067491e53283b8082d9e85e9","after_hash":"6e470f682cfb5b28930481f9487ec0e41b5963f8fc89e37fc72371e5e5ce8a82","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"2a6b55076350dfc5608dc52dd1e33f2e0b044992fef9e14f772245937b380ad8"} +{"hash_algorithm":"sha256","ts":"2026-10-24T11:57:13.247Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"2a9f9fc28825b23269e28d86","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/21","value":{"created_at":"2026-10-24T11:57:13.246Z","author":"codex-agent","text":"SDK audit: pinned 2026.10.20 exposes no expectedRevision/ifUnchanged on close or BulkItemCloseMutation; BulkItemUpdateMutation.match applies only to updates, and runWithItemMutationPrecondition is internal, not a public export. Use public runWithActiveExtensions plus setActiveExtensionHooks beforeMutation to compare hashDocument(context.before) inside the SDK per-item lock before persistence. This preserves normal close validation, workflow policy, history and atomic close evidence. Needed upstream API: close(id, reason, { expectedItemHash }, clientOptions), checked under the same item lock with a stable revision-conflict refusal. Retry selection only for the conflicted item once. Audit: tags, links, criteria chronology, status, comments and body are read-only inputs covered by the exact document hash; close writes completed_at, resolution, expected_result, actual_result and close_reason together. No independent tag/link/comment/criteria mutation path exists in closeout."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T11:57:13.247Z"}],"before_hash":"6e470f682cfb5b28930481f9487ec0e41b5963f8fc89e37fc72371e5e5ce8a82","after_hash":"92a8b7d12cdf957d0dfb0db23556cae45a2187f1ea66a2ba0657b34f29d20330","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"19c060787895acf0420e524ec7fba2854719c0499a068d4ffac1df0d132e1651"} +{"hash_algorithm":"sha256","ts":"2026-10-24T12:07:08.618Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"2a9f9fc28825b23269e28d86","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/22","value":{"created_at":"2026-10-24T12:07:08.618Z","author":"codex-agent","text":"Revert proof: restored only closeout.ts from the PR head before this revision, kept the regression test, and ran node --test --test-name-pattern=\"retag at 1:pull\" test/closeout-revision.test.ts. Result: 0 pass, 1 fail; stale closure selected PR 1 after delivery-pr became PR 2 (assertion 1 !== 2). Restored the fix and reran the identical command: 1 pass, 0 fail. The guard now also covers SDK implicit dependent unblocking; concurrent prerequisite edits cannot be overwritten, and refused dependent writes receive one fresh delivery selection or an explicit hold."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T12:07:08.618Z"}],"before_hash":"92a8b7d12cdf957d0dfb0db23556cae45a2187f1ea66a2ba0657b34f29d20330","after_hash":"f343c86cf75b469cde6e77ddf24e38b2ba9747627b32460fe8ab7d91dc2a023f","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"afd2d6e69922620d3c686ff7543ec8d4c9ec22821d3aefc06d8e823b53bbdd86"} +{"hash_algorithm":"sha256","ts":"2026-10-24T12:08:18.673Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"2a9f9fc28825b23269e28d86","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"files_add","patch":[{"op":"replace","path":"/metadata/files/7/path","value":"test/closeout-workflow.test.ts"},{"op":"replace","path":"/metadata/files/6/path","value":"test/closeout-selection.test.ts"},{"op":"replace","path":"/metadata/files/5/path","value":"test/closeout-revision.test.ts"},{"op":"add","path":"/metadata/files/8","value":{"path":"test/closeout.test.ts","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T12:08:18.673Z"}],"before_hash":"f343c86cf75b469cde6e77ddf24e38b2ba9747627b32460fe8ab7d91dc2a023f","after_hash":"ed24f3ec1e3f697c557baf13e6ee9bc43d673694fa9149b01885c324b98cbc52","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"maintenance","record_hash_version":1,"record_hash":"df0b5b54d0ce92d6b8568e955978b0fdee7cc1a5fb36bade9eebd52b658c76df"} +{"hash_algorithm":"sha256","ts":"2026-10-24T12:08:24.709Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"2a9f9fc28825b23269e28d86","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/23","value":{"created_at":"2026-10-24T12:08:24.709Z","author":"codex-agent","text":"Final-source guard-only revert proof in an isolated source copy (active gate source unchanged): removed only the under-lock hash comparison, retained all observers, and ran node --test --test-name-pattern=\"retag at 1:pull|dependent update: changed\" test/closeout-revision.test.ts. Both failed (0/2): stale PR 1 provenance after retag to PR 2, and a peer prerequisite edit overwritten by automatic unblock (open instead of blocked). Restoring only that comparison passed 2/2. The ordinary SDK close lock and lifecycle implementation were real in both runs."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T12:08:24.709Z"}],"before_hash":"ed24f3ec1e3f697c557baf13e6ee9bc43d673694fa9149b01885c324b98cbc52","after_hash":"6165da006f9b617fd06ac35fae2369add4e8d598ce99897228ab5767f0f5bb82","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"0b2d6b23a35fd5e79f53d0fbf0b03b002469f5db40f0929f3eb83e45300d11e8"} +{"hash_algorithm":"sha256","ts":"2026-10-24T12:08:26.189Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"2a9f9fc28825b23269e28d86","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"tests_add","patch":[{"op":"add","path":"/metadata/tests/5","value":{"command":"node --test test/closeout-revision.test.ts","scope":"project","timeout_seconds":300,"provenance":{"author":"codex-agent","created_at":"2026-10-24T12:08:26.122Z","source_kind":"local_mutation","source_ref":"fix/closeout-delivery-attribution"}}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T12:08:26.189Z"}],"before_hash":"6165da006f9b617fd06ac35fae2369add4e8d598ce99897228ab5767f0f5bb82","after_hash":"7e5d03cd3e6d345a34524b7f3f466c57c8ef3466ab2198d3772ad04941ccd989","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"maintenance","record_hash_version":1,"record_hash":"bcc4e21a9e7a6cc96eb9249f5c90dc151bda7b93f4bd2375594cb678142cc775"} +{"hash_algorithm":"sha256","ts":"2026-10-24T12:15:43.152Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"2a9f9fc28825b23269e28d86","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/24","value":{"created_at":"2026-10-24T12:15:43.152Z","author":"codex-agent","text":"The pinned PM-linked node --test test/closeout-revision.test.ts run passed 109/109 with zero failures, skips or cancellations. Further audit found a document/history read boundary: a peer SDK update writes the item before appending history while retaining its lock. Acceptance chronology now uses verifyHistoryEntries(history, selectedDocument), so a new document cannot borrow the previous revision criteria cutoff. A real-SDK history_append scheduling barrier exercises this partial-write boundary. The superseded local broad gate was stopped before changing source; only final-source receipts will be used."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T12:15:43.152Z"}],"before_hash":"7e5d03cd3e6d345a34524b7f3f466c57c8ef3466ab2198d3772ad04941ccd989","after_hash":"7dfc4a9cc5f16c68741fe5006bb332f4bb0e2e86a4875ed4c19f5827c5b47a9d","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"1cbb0a14682099499462b65d18dd29084795debc9bdeec887379693599837474"} +{"hash_algorithm":"sha256","ts":"2026-10-24T12:15:46.288Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"2a9f9fc28825b23269e28d86","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"tests_add","patch":[{"op":"add","path":"/metadata/tests/6","value":{"command":"node node_modules/c8/bin/c8.js --100 --include=closeout.ts --reporter=text --reporter=json --reports-dir=coverage/closeout-revision --temp-directory=coverage/closeout-revision-v8 node --test test/closeout.test.ts test/closeout-attribution.test.ts test/closeout-selection.test.ts test/closeout-revision.test.ts","scope":"project","timeout_seconds":600,"provenance":{"author":"codex-agent","created_at":"2026-10-24T12:15:46.250Z","source_kind":"local_mutation","source_ref":"fix/closeout-delivery-attribution"}}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T12:15:46.288Z"}],"before_hash":"7dfc4a9cc5f16c68741fe5006bb332f4bb0e2e86a4875ed4c19f5827c5b47a9d","after_hash":"c99effd7570cac2b4352054d3564d537e13268c61be844d6d02416eff8debc86","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"maintenance","record_hash_version":1,"record_hash":"f0aa0a24a6107ac5c4cd16406e9a4db3a1846528e71040394d93c80c7a0b6448"} +{"hash_algorithm":"sha256","ts":"2026-10-24T12:17:28.572Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"2a9f9fc28825b23269e28d86","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/25","value":{"created_at":"2026-10-24T12:17:28.572Z","author":"codex-agent","text":"History-binding revert proof: in an isolated source copy changed only verifyHistoryEntries(history, selectedDocument) back to verifyHistoryEntries(history), retained the real SDK partial-write scheduling barrier, and ran node --test --test-name-pattern=\"binds acceptance history\" test/closeout-revision.test.ts. Reverted: 0 pass, 1 fail (closed 1 instead of 0 using an older criteria cutoff for the newer document). Restored: 1 pass, 0 fail. The final local targeted command covers all four closeout suites and enforces exact 100/100/100/100 coverage for closeout.ts."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T12:17:28.572Z"}],"before_hash":"c99effd7570cac2b4352054d3564d537e13268c61be844d6d02416eff8debc86","after_hash":"00b3c2b978121c58a2a4bae1e40c821e764fff7c860bb62d95b5ba98d37325e1","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"85b9087c427cc73d2372c09c723924689d9c23bd46511aab245639b738021b14"} +{"hash_algorithm":"sha256","ts":"2026-10-24T12:19:39.759Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"2a9f9fc28825b23269e28d86","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/26","value":{"created_at":"2026-10-24T12:19:39.759Z","author":"codex-agent","text":"Final-source local validation passed: the PM-linked c8 --100 command ran all four closeout suites, 286/286 tests, zero failures/cancellations/skips, exact 100/100/100/100 for closeout.ts. The revision suite contributes 110 tests, including 17-by-6 peer-edit boundaries, bounded retry, guard isolation, removed snapshots, implicit dependent updates and document/history binding. Typecheck, build, test typecheck and lint passed; docstrings, zero-duplication, privacy and canonical changelog checks passed. Strict health with required merge drivers passed with isolated HOME; ambient global extension version skew is absent in the CI-equivalent environment. PM test-result storage is disabled by existing policy, so this comment retains the receipt. Full npm/Bun release evidence will come from CI on the pushed head; thresholds and dependency pins remain unchanged."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T12:19:39.759Z"}],"before_hash":"00b3c2b978121c58a2a4bae1e40c821e764fff7c860bb62d95b5ba98d37325e1","after_hash":"7ffa7896fa48266019a4c9818b978dd6cc0e3d28da3d1e26d9b18d3ef47c8bcc","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"6a44085313bbc7e687d84f9460a611407d20dd78dd6c410c0eeacc3547687cc6"} +{"hash_algorithm":"sha256","ts":"2026-10-24T12:19:40.568Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"2a9f9fc28825b23269e28d86","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":{"value":"implementer","source":"argv"},"topic":null},"op":"release","patch":[{"op":"remove","path":"/metadata/claim_principal"},{"op":"remove","path":"/metadata/assignee"},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T12:19:40.568Z"}],"before_hash":"7ffa7896fa48266019a4c9818b978dd6cc0e3d28da3d1e26d9b18d3ef47c8bcc","after_hash":"f5be3329725edf2b97197835b56cf9e8f5c9bd720b79753642e0fdce5e782055","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"maintenance","record_hash_version":1,"record_hash":"f7a9586041bd41dbfab214591a2f610dcd1e8573d9783da406e9ee093eadc571"} +{"hash_algorithm":"sha256","ts":"2026-10-24T13:51:59.383Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"a5f9cfc035cd6c24601c6c47","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":{"value":"implementer","source":"argv"},"topic":null},"op":"claim","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T13:51:59.383Z"},{"op":"add","path":"/metadata/assignee","value":"codex-agent"},{"op":"add","path":"/metadata/claim_principal","value":"codex-agent"}],"before_hash":"f5be3329725edf2b97197835b56cf9e8f5c9bd720b79753642e0fdce5e782055","after_hash":"a63812d82fb24162f340207c8949c528efd9193a5ea487d0edc054977cb67242","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"9bca18bc0215780099e8ffa9b8432529c3d5cd2fe712d5dbf014c04a479c9f6e"} +{"hash_algorithm":"sha256","ts":"2026-10-24T13:52:01.376Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"a5f9cfc035cd6c24601c6c47","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/27","value":{"created_at":"2026-10-24T13:52:01.374Z","author":"codex-agent","text":"Review round 3: reproduce selected-delivery publication refusal, then derive all candidate rows from one item decision: chosen, superseded informational, or blocking hold. Add a 3 x 6 x 2 end-to-end decision table through the actual workflow publication gate, including peer retags. Record behavioral revert proof; run targeted tests, typecheck and lint locally, and require npm plus Bun full release gates in CI on the pushed head."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T13:52:01.376Z"}],"before_hash":"a63812d82fb24162f340207c8949c528efd9193a5ea487d0edc054977cb67242","after_hash":"7797c2e870078a0e6373d18bfec365cf246ce09c22313ab90b7b33323d6dad54","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"6fb6a5dbeb0db5cbfa5f1aaf087bbb573ab133e9b01ee5007eba950d5d5a4925"} +{"hash_algorithm":"sha256","ts":"2026-10-24T13:57:55.701Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"a5f9cfc035cd6c24601c6c47","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"tests_add","patch":[{"op":"add","path":"/metadata/tests/7","value":{"command":"node --test --test-name-pattern=\"item decision end to end\" test/closeout-workflow.test.ts","scope":"project","timeout_seconds":1200,"provenance":{"author":"codex-agent","created_at":"2026-10-24T13:57:55.580Z","source_kind":"local_mutation","source_ref":"fix/closeout-delivery-attribution"}}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T13:57:55.701Z"}],"before_hash":"7797c2e870078a0e6373d18bfec365cf246ce09c22313ab90b7b33323d6dad54","after_hash":"f94c636f0367e6f91a84ed243216a8aa67e81f97e3818911f8c6e3472e970c92","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"maintenance","record_hash_version":1,"record_hash":"8658dadb8abd69a69cb2baa1a114a4ccff20446e81e3ec5a65364f16971f1325"} +{"hash_algorithm":"sha256","ts":"2026-10-24T13:57:57.689Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"a5f9cfc035cd6c24601c6c47","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/28","value":{"created_at":"2026-10-24T13:57:57.687Z","author":"codex-agent","text":"Reproduced the exact round-3 blocker before editing runtime source: the explicit-delivery/missing-merge-SHA/no-peer end-to-end test fails ERR_ASSERTION with exit 1 because the real workflow rejects incomplete merged-PR metadata after local closeout. ItemDecision now projects chosen or blocking-hold outcomes across every candidate; all nonchosen candidates become duplicate_selection informational rows, regardless of original evidence failures. Failed chosen checks and SDK refusals hold all peers. Typecheck, test typecheck, lint, docstrings and zero-duplication checks pass. The 36-cell workflow matrix and existing non-git SDK suites are running."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T13:57:57.689Z"}],"before_hash":"f94c636f0367e6f91a84ed243216a8aa67e81f97e3818911f8c6e3472e970c92","after_hash":"c8705473b385648aa8a7ec48a931cd20bb728a8307bef9199fb2abf2d86e5748","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"e9b3382ff58b7415411636043e0e3edf0b7943b44627775312606cdcf4793bed"} +{"hash_algorithm":"sha256","ts":"2026-10-24T13:58:35.105Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"a5f9cfc035cd6c24601c6c47","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"tests_add","patch":[{"op":"add","path":"/metadata/tests/8","value":{"command":"node --test --test-name-pattern=\"item decision end to end: explicit delivery tag / (missing merge SHA|failed evidence fetch) / none\" test/closeout-workflow.test.ts","scope":"project","timeout_seconds":300,"provenance":{"author":"codex-agent","created_at":"2026-10-24T13:58:34.975Z","source_kind":"local_mutation","source_ref":"fix/closeout-delivery-attribution"}}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T13:58:35.105Z"}],"before_hash":"c8705473b385648aa8a7ec48a931cd20bb728a8307bef9199fb2abf2d86e5748","after_hash":"ec4373fdaa5bc79f585c89ec883ff516a686d431fa1843ce69b5f88a160e39a7","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"maintenance","record_hash_version":1,"record_hash":"ca3cc4b22453937e2619ed2661e6848374737376d05048142d6dcbf119bd0f57"} +{"hash_algorithm":"sha256","ts":"2026-10-24T14:06:16.810Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"a5f9cfc035cd6c24601c6c47","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/29","value":{"created_at":"2026-10-24T14:06:16.808Z","author":"codex-agent","text":"Focused PM-linked c8 verification passed 286/286 tests, zero failures, cancellations or skips, with exact 100/100/100/100 for closeout.ts. Exact command: node node_modules/c8/bin/c8.js --100 --include=closeout.ts --reporter=text --reporter=json --reports-dir=coverage/closeout-revision --temp-directory=coverage/closeout-revision-v8 node --test test/closeout.test.ts test/closeout-attribution.test.ts test/closeout-selection.test.ts test/closeout-revision.test.ts. Existing result-storage policy is disabled; this comment retains the receipt. The peer-retag table explicitly allows a safe hold when a peer invalidates the selected document/history revision before attribution; explicit re-selection with a valid retag closes with fresh provenance."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T14:06:16.810Z"}],"before_hash":"ec4373fdaa5bc79f585c89ec883ff516a686d431fa1843ce69b5f88a160e39a7","after_hash":"a3d688b577e8b18ab47e419cff68614592642170dbba6fc803970b19035258d8","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"06d24d3a63546b855f67ddf5b58b570f2c041fd281ca93ece524dc09cb90069b"} +{"hash_algorithm":"sha256","ts":"2026-10-24T14:07:20.953Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"a5f9cfc035cd6c24601c6c47","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/30","value":{"created_at":"2026-10-24T14:07:20.953Z","author":"codex-agent","text":"Round-3 fix-only behavioral revert proof: restored only closeout.ts to 4874023 (the pre-round-3 runtime, retaining all round-2 revision guards), rebuilt dist with npm run build, and retained the new end-to-end tests plus workflow unchanged. Command: node --test --test-name-pattern='item decision end to end: explicit delivery tag / (missing merge SHA|failed evidence fetch) / none' test/closeout-workflow.test.ts. Reverted result: 0/2 passed, 2 ERR_ASSERTION failures, exit 1. Both regressions fail at the workflow publication gate after closeout rather than at load/build: incomplete merge or unavailable evidence leaves a fatal competing refusal. Restored the fix byte-for-byte, rebuilt, then ran the same command through pm test --run --only-index 9 --progress with isolated HOME and Git configuration: 2/2 passed, zero failures/cancellations/skips, exit 0. No tests, workflow gates or thresholds were reverted."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T14:07:20.953Z"}],"before_hash":"a3d688b577e8b18ab47e419cff68614592642170dbba6fc803970b19035258d8","after_hash":"8148f3f336b66c681eaa41ac6df20b6df831ad83ee2731d3d3a6068ce269cc4c","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"d3b95e702a69c5ce8d683bc65108dfd70390f435681f9330237c5a0bede5c351"} +{"hash_algorithm":"sha256","ts":"2026-10-24T14:07:22.836Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"a5f9cfc035cd6c24601c6c47","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"tests_add","patch":[{"op":"add","path":"/metadata/tests/9","value":{"command":"node --test --test-name-pattern=\"item decision end to end: single post-criteria claim / valid / retag mid-run\" test/closeout-workflow.test.ts","scope":"project","timeout_seconds":300,"provenance":{"author":"codex-agent","created_at":"2026-10-24T14:07:22.709Z","source_kind":"local_mutation","source_ref":"fix/closeout-delivery-attribution"}}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T14:07:22.836Z"}],"before_hash":"8148f3f336b66c681eaa41ac6df20b6df831ad83ee2731d3d3a6068ce269cc4c","after_hash":"d3e7025d2133d8b21e712fe6cbbf6f14885123eeaf9d40716771387e2fd78510","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"maintenance","record_hash_version":1,"record_hash":"ec1697ea7cf2d37c0ab388887df1eae60d72f6195d16b7824fd1b6d77490a584"} +{"hash_algorithm":"sha256","ts":"2026-10-24T14:10:58.557Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"a5f9cfc035cd6c24601c6c47","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"files_add","patch":[{"op":"replace","path":"/metadata/files/8/path","value":"test/closeout-workflow.test.ts"},{"op":"replace","path":"/metadata/files/7/path","value":"test/closeout-selection.test.ts"},{"op":"replace","path":"/metadata/files/6/path","value":"test/closeout-revision.test.ts"},{"op":"replace","path":"/metadata/files/5/path","value":"test/closeout-attribution.test.ts"},{"op":"replace","path":"/metadata/files/4/path","value":"test/changelog-placement.test.ts"},{"op":"replace","path":"/metadata/files/3/path","value":"test/bot-pr-workflow.test.ts"},{"op":"replace","path":"/metadata/files/2/path","value":"closeout.ts"},{"op":"replace","path":"/metadata/files/1/path","value":".github/workflows/pm-github-closeout.yml"},{"op":"replace","path":"/metadata/files/0/path","value":".github/workflows/ci.yml"},{"op":"add","path":"/metadata/files/9","value":{"path":"test/closeout.test.ts","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T14:10:58.557Z"}],"before_hash":"d3e7025d2133d8b21e712fe6cbbf6f14885123eeaf9d40716771387e2fd78510","after_hash":"0d395bd77f32041f146fd142dccd44090f611280317816ac228e8afd7ad6b1c9","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"maintenance","record_hash_version":1,"record_hash":"822bd2a2e05c2c1aa91600d0eecc71bab8dd073fd32f77ca8080e8d8c446263f"} +{"hash_algorithm":"sha256","ts":"2026-10-24T14:10:59.425Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"a5f9cfc035cd6c24601c6c47","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/31","value":{"created_at":"2026-10-24T14:10:59.424Z","author":"codex-agent","text":"Round-3 final local validation: PM-linked node --test --test-name-pattern='item decision end to end' test/closeout-workflow.test.ts passed 36/36, zero failures/cancellations/skips (3 selection sources x 6 competitor states x 2 peer schedules). The decision table independently expects 12 closed-and-published outcomes with chosen provenance and 24 open-and-published holds listing every candidate. The restored two publication regressions passed 2/2 with isolated HOME/Git configuration; the explicit safe-history hold control passed 1/1. Focused c8 passed 286/286 with exact 100/100/100/100 for closeout.ts. npm run typecheck, npm run build, npm run build:test, npm run lint, npm run docstring, npm run duplication, npm run privacy and changelog generation/check passed. Strict health with required merge drivers passed with isolated HOME; two existing advisory findings do not fail strict health. Source inventory, dependency pins, exclusions and thresholds remain unchanged. The item stays in progress for orchestrator verification, with the claim released before push. Remaining verification is read-only CI: npm run release:check and bun run release:check on Node 22/26, plus native Bun acceptance; exact final-head run URLs/counts will be retained in PR 174 and the handoff. No new PM CLI/SDK defect was encountered in this round."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T14:10:59.425Z"}],"before_hash":"0d395bd77f32041f146fd142dccd44090f611280317816ac228e8afd7ad6b1c9","after_hash":"8dac4879fa32d16697d4e1db647c38c872d42671ef00afba3ede23181d1ac464","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"80d3ae2285d6453eb22b121ac8994ae2e79b4568ae677604177f86006970b075"} +{"hash_algorithm":"sha256","ts":"2026-10-24T14:11:00.212Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"a5f9cfc035cd6c24601c6c47","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":{"value":"implementer","source":"argv"},"topic":null},"op":"release","patch":[{"op":"remove","path":"/metadata/claim_principal"},{"op":"remove","path":"/metadata/assignee"},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T14:11:00.212Z"}],"before_hash":"8dac4879fa32d16697d4e1db647c38c872d42671ef00afba3ede23181d1ac464","after_hash":"152810885cff2f7106db114e3e9bea242b8a688ddf31499aeec23fe6d9c0995d","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"maintenance","record_hash_version":1,"record_hash":"11dfa108b18064d5823c7ddbd02eae3f68cb540048aab8236456ecd421739104"} +{"hash_algorithm":"sha256","ts":"2026-10-24T14:31:40.279Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"a5f9cfc035cd6c24601c6c47","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":{"value":"implementer","source":"argv"},"topic":null},"op":"claim","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T14:31:40.279Z"},{"op":"add","path":"/metadata/assignee","value":"codex-agent"},{"op":"add","path":"/metadata/claim_principal","value":"codex-agent"}],"before_hash":"152810885cff2f7106db114e3e9bea242b8a688ddf31499aeec23fe6d9c0995d","after_hash":"12503391383ef6509a5bf8549850e08c56dc5c4cca6fc4eb5e27b56bd325d24d","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"9d9301c0b80d0da53a651a6a1ddbafc6fd61147723effa0158a73e9f8fe1a21a"} +{"hash_algorithm":"sha256","ts":"2026-10-24T14:31:43.103Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"a5f9cfc035cd6c24601c6c47","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/32","value":{"created_at":"2026-10-24T14:31:43.103Z","author":"codex-agent","text":"CI run https://github.com/unbraind/pm-github/actions/runs/40101464708 at 30b94f1: both Node 26 jobs passed all 1337 tests with zero test failures, then c8 exhausted its unchanged 8192 MiB heap during coverage aggregation. These are failed release gates, not approval. Reduce redundant matrix fixture initialization and unrelated repeated workflow steps while retaining all 36 actual closeout/publication-gate cases, public provenance assertions and the existing full workflow controls. Keep all source/coverage thresholds, heap resources and dependency pins unchanged; rerun focused proof and exact-head CI."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T14:31:43.103Z"}],"before_hash":"12503391383ef6509a5bf8549850e08c56dc5c4cca6fc4eb5e27b56bd325d24d","after_hash":"b341a6719881524d3d1a1787b97d1a4a6f370ac89748454a923b99e2ab0892b9","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"1743541d12f2aec3ddb6a16c7760dab019228cc21828c309c217a4da58f3a157"} +{"hash_algorithm":"sha256","ts":"2026-10-24T14:34:26.609Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"a5f9cfc035cd6c24601c6c47","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/33","value":{"created_at":"2026-10-24T14:34:26.606Z","author":"codex-agent","text":"All four first-head CI release jobs failed during aggregation at the unchanged 8 GiB heap; the existing broader follow-up pm-github-vhi6 now retains that receipt. Matrix harness optimization reuses four immutable input baselines and executes the unchanged workflow reset before each cell, with fresh PR receipts so an earlier publication cannot satisfy a later cell. Every cell still runs the real CLI, actual closeout refusal gate, publication privacy gates, leased Git push, and public body/provenance assertions. Existing full workflow tests continue to own changelog, strict-health, credential and dispatch controls. No runtime source or workflow gate is changed by this optimization."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T14:34:26.609Z"}],"before_hash":"b341a6719881524d3d1a1787b97d1a4a6f370ac89748454a923b99e2ab0892b9","after_hash":"98ce127880b70ee676a53878b603184d2da5843bc920a4f13515e93e80977e4a","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"db3cdf431c33bd2cf5ca22bc592e9ba3b9cdfc2f2cb81e4e9d65e4185cd7bd73"} +{"hash_algorithm":"sha256","ts":"2026-10-24T14:38:10.087Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"a5f9cfc035cd6c24601c6c47","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/34","value":{"created_at":"2026-10-24T14:38:10.086Z","author":"codex-agent","text":"Optimized-harness revert proof repeated with independent fixture reset before each cell: only closeout.ts restored to 4874023 and rebuilt, retaining current matrix/workflow. The same two-test publication command failed 0/2 passed, exit 1, both ERR_ASSERTION errors at the expected workflow gates: incomplete merged-PR metadata and unavailable head evidence. Restoring the fix byte-for-byte and rebuilding leaves runtime source identical to 30b94f1. This additionally proves a failed earlier case cannot contaminate the next case's evidence. The optimized 36-cell matrix and restored controls are running through PM with isolated HOME/Git configuration; thresholds, heap resources and workflow code remain unchanged."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T14:38:10.087Z"}],"before_hash":"98ce127880b70ee676a53878b603184d2da5843bc920a4f13515e93e80977e4a","after_hash":"46a43b70955f18c4798c86f7ba1a96fe534a90eae9e3acc9bb177119a2cc1260","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"e24c1b055e8a28e98cdbf8c21edacc4cec5b75cce7960d7e3a560a49f3b65125"} +{"hash_algorithm":"sha256","ts":"2026-10-24T14:43:27.629Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"a5f9cfc035cd6c24601c6c47","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/35","value":{"created_at":"2026-10-24T14:43:27.629Z","author":"codex-agent","text":"Final optimized-harness local receipt: PM-linked 36-cell matrix passed 36/36 with zero failures/cancellations/skips and the same 12 closed-and-published / 24 open-and-published-hold expectations. The independent-reset fix-only revert fails both expected publication-gate assertions (0/2, exit 1); restored PM-linked controls pass 2/2 (exit 0). Both PM runs used isolated HOME and Git configuration. Test typecheck, lint, production typecheck and docstrings pass; privacy and changelog checks pass. Runtime source is byte-identical to 30b94f1, whose four CI test phases each passed 1337/1337 before aggregation failed. This follow-up changes only matrix setup and evidence/docs; scripts/coverage-gate.ts, package files, workflow code, source inventory, exclusions, four 100% thresholds and 8192 MiB heap remain unchanged. Release the claim before the follow-up push; retain final exact-head CI receipts in PR 174 and the handoff. Broader aggregation work remains open as pm-github-vhi6."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T14:43:27.629Z"}],"before_hash":"46a43b70955f18c4798c86f7ba1a96fe534a90eae9e3acc9bb177119a2cc1260","after_hash":"fa50c57b6624201329ca9ddf7305c7fb40a65abc963aa014917ce9692dca821b","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"1415ce5aa27b7c0fc6a52b9be9da8182362d724f2a62006e270ed6d5681b711a"} +{"hash_algorithm":"sha256","ts":"2026-10-24T14:43:28.944Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"a5f9cfc035cd6c24601c6c47","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":{"value":"implementer","source":"argv"},"topic":null},"op":"release","patch":[{"op":"remove","path":"/metadata/claim_principal"},{"op":"remove","path":"/metadata/assignee"},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T14:43:28.944Z"}],"before_hash":"fa50c57b6624201329ca9ddf7305c7fb40a65abc963aa014917ce9692dca821b","after_hash":"abf9bcbcbe82056dd9dc52e8245d96b53d292e67595fa9f998e571a520abe610","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"maintenance","record_hash_version":1,"record_hash":"b5ffd6c1c2c5e48684f49a5bd75dfedc8714bb8dc10bb1ee83bce9b38764ed60"} +{"hash_algorithm":"sha256","ts":"2026-10-24T15:03:19.891Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"a5f9cfc035cd6c24601c6c47","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":{"value":"implementer","source":"argv"},"topic":null},"op":"claim","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T15:03:19.891Z"},{"op":"add","path":"/metadata/assignee","value":"codex-agent"},{"op":"add","path":"/metadata/claim_principal","value":"codex-agent"}],"before_hash":"abf9bcbcbe82056dd9dc52e8245d96b53d292e67595fa9f998e571a520abe610","after_hash":"679c7e90e99e658be17b4e25262687038157bad4bde56b643c75054cc757a33f","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"240f21a061a66ee02880a812a58e5608f4ea52e8d30bc28dde70c00843035ff5"} +{"hash_algorithm":"sha256","ts":"2026-10-24T15:03:21.409Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"a5f9cfc035cd6c24601c6c47","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/36","value":{"created_at":"2026-10-24T15:03:21.409Z","author":"codex-agent","text":"Second exact-head CI https://github.com/unbraind/pm-github/actions/runs/40103921945 at ad2e7cb: completed npm Node 26 and Bun Node 22 test phases each pass 1337/1337, then c8 again exhausts the unchanged 8192 MiB heap. The gates remain failed. Four shared callers still repeated package setup for every cell; prepare each immutable baseline once and execute only the real branch preparation, closeout and publication steps per cell. Preserve independent resets, fresh PR receipts, all 36 cases and every outcome/provenance assertion. Keep runtime source, actual workflow code, coverage resources and thresholds unchanged."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T15:03:21.409Z"}],"before_hash":"679c7e90e99e658be17b4e25262687038157bad4bde56b643c75054cc757a33f","after_hash":"08bc2b456f9c82235558320b12ffd6bbd780a25b31cd5bb714063b57d919f895","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"656d3e8f26a019d380cb117f22ffbd5ef77c1c5bc0a59b52bca18651e5898b5f"} +{"hash_algorithm":"sha256","ts":"2026-10-24T15:10:02.207Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"a5f9cfc035cd6c24601c6c47","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/37","value":{"created_at":"2026-10-24T15:10:02.206Z","author":"codex-agent","text":"Prepared-baseline matrix local run passed 36/36, zero failures/cancellations/skips, in about 100 seconds. Setup now runs once for four immutable baseline callers; each case restores its input and prepared workflow-owned setup/environment, clears PR receipts, and executes the real Prepare sync branch, Close delivered tracker items and Commit gated closeout changes steps. The real publication privacy/refusal gates and leased push remain inside those unchanged steps. Repeating the runtime-only revert against the prepared harness fails 0/2 passed at the two expected workflow gates (exit 1); runtime was restored byte-for-byte and rebuilt. Final PM-linked matrix/restored controls are running with isolated HOME/Git configuration. Production source and gate configuration remain identical to 30b94f1."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T15:10:02.207Z"}],"before_hash":"08bc2b456f9c82235558320b12ffd6bbd780a25b31cd5bb714063b57d919f895","after_hash":"e329ddc5c600ff0fc03d235a19cf707c5f66124c27247dd14408ba3513426a22","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"c19a98aad052014dba0191aa780386f375a9a795fa4d0997569538f564e2247d"} +{"hash_algorithm":"sha256","ts":"2026-10-24T15:12:48.724Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"a5f9cfc035cd6c24601c6c47","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/38","value":{"created_at":"2026-10-24T15:12:48.722Z","author":"codex-agent","text":"Final prepared-baseline PM receipt: 36/36 matrix tests pass with zero failures/cancellations/skips; all 12 closed-and-published and 24 open-and-published-hold expectations remain asserted. PM-linked restored publication controls pass 2/2; runtime-only revert fails 0/2 at the expected gates, exit 1. The matrix runs actual package/scope setup once per four immutable baselines and restores workflow-owned setup/environment plus independent tracker input before every actual branch-preparation, closeout and publication attempt. Fresh receipts prohibit stale publication from satisfying a case. Production source, coverage script, package files and workflows remain unchanged from 30b94f1; typecheck, test typecheck, lint and docstrings pass. Both earlier CI runs passed all 1337 tests in all four jobs but failed coverage aggregation, recorded on pm-github-vhi6. No gate resource, inventory, exclusion or threshold is relaxed. Release claim before push; final exact-head full-gate evidence remains read-only CI and will be retained in PR 174 and handoff."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T15:12:48.724Z"}],"before_hash":"e329ddc5c600ff0fc03d235a19cf707c5f66124c27247dd14408ba3513426a22","after_hash":"f460958a34405c0ad1620c7d563b74d73b3bf54f0c6df6cd62853092212398c1","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"d54333178c64f895658c38b33d3ce8363b793a09874cdc0c603318bf690fb768"} +{"hash_algorithm":"sha256","ts":"2026-10-24T15:12:49.741Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"a5f9cfc035cd6c24601c6c47","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":{"value":"implementer","source":"argv"},"topic":null},"op":"release","patch":[{"op":"remove","path":"/metadata/claim_principal"},{"op":"remove","path":"/metadata/assignee"},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T15:12:49.741Z"}],"before_hash":"f460958a34405c0ad1620c7d563b74d73b3bf54f0c6df6cd62853092212398c1","after_hash":"b22bf7a3e0e92fd37a977d3d8373060d693dc6717fc8715880affa8cc113f662","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"maintenance","record_hash_version":1,"record_hash":"231694887a2342e3920673bfcc8a4209f75b96fec0e25bb3e0169edcd1b489dc"} +{"hash_algorithm":"sha256","ts":"2026-10-24T15:35:58.574Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"a5f9cfc035cd6c24601c6c47","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":{"value":"implementer","source":"argv"},"topic":null},"op":"claim","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T15:35:58.574Z"},{"op":"add","path":"/metadata/assignee","value":"codex-agent"},{"op":"add","path":"/metadata/claim_principal","value":"codex-agent"}],"before_hash":"b22bf7a3e0e92fd37a977d3d8373060d693dc6717fc8715880affa8cc113f662","after_hash":"26c519e3fd42d58d83e3ffc825c47d4b8355ebab23dc2502d8789684f09fe7bc","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"18e3b4a8f1341269a953fbc1b8a07d3af1c509002095e6ad7bc959308e6ba038"} +{"hash_algorithm":"sha256","ts":"2026-10-24T15:38:01.859Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"a5f9cfc035cd6c24601c6c47","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"files_add","patch":[{"op":"replace","path":"/metadata/files/9/path","value":"test/closeout-workflow.test.ts"},{"op":"replace","path":"/metadata/files/8/path","value":"test/closeout-selection.test.ts"},{"op":"replace","path":"/metadata/files/7/path","value":"test/closeout-revision.test.ts"},{"op":"replace","path":"/metadata/files/6/path","value":"test/closeout-attribution.test.ts"},{"op":"replace","path":"/metadata/files/5/path","value":"test/changelog-placement.test.ts"},{"op":"replace","path":"/metadata/files/4/path","value":"test/bot-pr-workflow.test.ts"},{"op":"replace","path":"/metadata/files/3/path","value":"scripts/coverage-gate.ts"},{"op":"add","path":"/metadata/files/10","value":{"path":"test/closeout.test.ts","scope":"project"}},{"op":"add","path":"/metadata/files/11","value":{"path":"test/coverage-gate.test.ts","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T15:38:01.859Z"}],"before_hash":"26c519e3fd42d58d83e3ffc825c47d4b8355ebab23dc2502d8789684f09fe7bc","after_hash":"cdc5c534e3b227c8242d5a5cc3e8de2a271d1f32c755ef0dbc9f60f007215d48","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"maintenance","record_hash_version":1,"record_hash":"433d7d6d4ee5be8ed8cc0b8ee6692314c3d2df5611836adec6b221d2900803dd"} +{"hash_algorithm":"sha256","ts":"2026-10-24T15:39:12.160Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"a5f9cfc035cd6c24601c6c47","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"tests_add","patch":[{"op":"add","path":"/metadata/tests/10","value":{"command":"node --test test/coverage-gate.test.ts","scope":"project","timeout_seconds":300,"provenance":{"author":"codex-agent","created_at":"2026-10-24T15:39:12.100Z","source_kind":"local_mutation","source_ref":"fix/closeout-delivery-attribution"}}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T15:39:12.160Z"}],"before_hash":"cdc5c534e3b227c8242d5a5cc3e8de2a271d1f32c755ef0dbc9f60f007215d48","after_hash":"f5bb67592e1302e9a09ba8518d97f0673b7093f406eaff074073bb6c74d3ffcf","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"maintenance","record_hash_version":1,"record_hash":"6a9a5bbf2f1c85b7e19c23f1582680073400670b3f05069a3ecf15fcce4cf396"} +{"hash_algorithm":"sha256","ts":"2026-10-24T15:42:24.111Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"a5f9cfc035cd6c24601c6c47","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/39","value":{"created_at":"2026-10-24T15:42:24.111Z","author":"codex-agent","text":"The prepared 36-cell matrix and runtime-only revert proof remain unchanged and passing. CI cff0f1c established the complete npm Node 22 gate at the old heap, while Node 26 again passed all 1337 tests and failed aggregation. To complete the requested full-gate evidence, only c8's merge heap ceiling is raised to 12288 MiB on the existing public 16 GB runners. There is no coverage threshold, source inventory, ignore list, dependency or workflow change. The real coverage-gate suite passes 9/9 via PM, including full/partial receipt and flag-routing controls; typecheck, test typecheck, lint, docstrings and zero duplication pass. Release both claims before push; final exact-head CI URL/counts will be retained in PR 174 and handoff. Broader bounded aggregation remains tracked by pm-github-vhi6 and neither item is closed."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T15:42:24.111Z"}],"before_hash":"f5bb67592e1302e9a09ba8518d97f0673b7093f406eaff074073bb6c74d3ffcf","after_hash":"58790e95efb8637f48e8c6e090023529d75060c13fcdfe498fddfa4b2eb4e2b6","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"740e661d73224530dcab07127902b4ff6dc4abd6dc7f3d9f1b93ad84cafb0e3d"} +{"hash_algorithm":"sha256","ts":"2026-10-24T15:42:25.657Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"a5f9cfc035cd6c24601c6c47","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":{"value":"implementer","source":"argv"},"topic":null},"op":"release","patch":[{"op":"remove","path":"/metadata/claim_principal"},{"op":"remove","path":"/metadata/assignee"},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T15:42:25.657Z"}],"before_hash":"58790e95efb8637f48e8c6e090023529d75060c13fcdfe498fddfa4b2eb4e2b6","after_hash":"26c0d83f880396c1b50a7a4df1eb44ca7cd4566fba1a815e0f7e35ec795aa217","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"maintenance","record_hash_version":1,"record_hash":"6d19ffff3b160b89312e0d18eada070d153caf4f452f8ac9d01da710d1f77089"} +{"hash_algorithm":"sha256","ts":"2026-10-24T17:16:36.925Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"cbeeadbdaee69681f3e0be9a","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":{"value":"implementer","source":"argv"},"topic":null},"op":"claim","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T17:16:36.925Z"},{"op":"add","path":"/metadata/assignee","value":"codex-agent"},{"op":"add","path":"/metadata/claim_principal","value":"codex-agent"}],"before_hash":"26c0d83f880396c1b50a7a4df1eb44ca7cd4566fba1a815e0f7e35ec795aa217","after_hash":"1e8ad57fcf912681a113c70ae570f800f5dd9ae9439e41aff276edefcfc51c00","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"71c50f777a58072d781596570655ce0c32b7e2b283b5082ce15c1ab02c7b8aae"} +{"hash_algorithm":"sha256","ts":"2026-10-24T17:16:39.286Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"cbeeadbdaee69681f3e0be9a","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/40","value":{"created_at":"2026-10-24T17:16:39.286Z","author":"codex-agent","text":"Round 4: correct candidacy to require an explicit delivery tag, a closing declaration, or a commit claim. Fully inspected mention/link-only PRs are context even when checks fail; unreadable claim evidence remains unresolved. Extend the existing publication decision table with primary and competitor claim kinds, retaining its 36 original cells and exact expected outcomes. Remote branch was current and already contains origin/main."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T17:16:39.286Z"}],"before_hash":"1e8ad57fcf912681a113c70ae570f800f5dd9ae9439e41aff276edefcfc51c00","after_hash":"4d0fe08b1a9c6a3de879da0850edb00d8e084af763ae68b03e441b28c8f9f257","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"717880a0eff473251a144d381fffa5dc6e153b7e5b3a79531825a43834dcf2c0"} +{"hash_algorithm":"sha256","ts":"2026-10-24T17:20:07.087Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"cbeeadbdaee69681f3e0be9a","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"tests_add","patch":[{"op":"add","path":"/metadata/tests/11","value":{"command":"node --test --test-name-pattern=\"fully inspected context|fully inspected mentions\" test/closeout-selection.test.ts","scope":"project","timeout_seconds":300,"provenance":{"author":"codex-agent","created_at":"2026-10-24T17:20:07.036Z","source_kind":"local_mutation","source_ref":"fix/closeout-delivery-attribution"}}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T17:20:07.087Z"}],"before_hash":"4d0fe08b1a9c6a3de879da0850edb00d8e084af763ae68b03e441b28c8f9f257","after_hash":"c3551f22f0dfe8f84240761cb7355092775c810377beae1d77feb0b6a6b56b0c","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"maintenance","record_hash_version":1,"record_hash":"c2b0e68103dcdbe394f5eab77dc9891174e17eaed3c41cb37e97fd4be421168c"} +{"hash_algorithm":"sha256","ts":"2026-10-24T17:22:14.236Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"cbeeadbdaee69681f3e0be9a","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/41","value":{"created_at":"2026-10-24T17:22:14.236Z","author":"codex-agent","text":"Round-4 fix-only revert proof: restored only closeout.ts from pre-round head 4dd53552f825b3b42959ae96b9e61b27fe96e900; retained every new test and workflow change. Exact command: node --test --test-name-pattern='fully inspected context|fully inspected mentions' test/closeout-selection.test.ts. Reverted: 0/7 passed, 7 assertion failures, exit 1 (valid delivery held or context reported as candidates). Restored closeout.ts byte-for-byte; the same command via the PM runner passes 7/7, exit 0, zero failures/cancellations/skips. Tests import TypeScript source directly; no load failure is counted. The 36 earlier publication table rows pass unchanged; the 150 additional claim-kind rows are running. Isolated tracker health passes; inherited global CLI version skew is absent under isolated configuration."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T17:22:14.236Z"}],"before_hash":"c3551f22f0dfe8f84240761cb7355092775c810377beae1d77feb0b6a6b56b0c","after_hash":"0823e421bc1f0e1f81005ad6f7bf03ee6ca849168b8522416af05e18917bc399","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"8249a13fc3565c86a418fab27c424993e1c5542b1d58080949e2d46a40401f46"} +{"hash_algorithm":"sha256","ts":"2026-10-24T17:25:37.405Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"cbeeadbdaee69681f3e0be9a","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/42","value":{"created_at":"2026-10-24T17:25:37.405Z","author":"codex-agent","text":"Local round-4 checks: typecheck, build, test typecheck, lint, docstrings (206 declarations), zero duplication and privacy passed. Isolated strict tracker health passed with two pre-existing advisory findings. The 186-row decision table expects 71 closed-and-published deliveries, 89 published holds and 26 context-only no-op runs. Every old table row remains. CLI and workflow summaries retain context as skip/not_delivery, without promoting context during selection or SDK refusal."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T17:25:37.405Z"}],"before_hash":"0823e421bc1f0e1f81005ad6f7bf03ee6ca849168b8522416af05e18917bc399","after_hash":"aaa99da37c2f52251dafc69e3a09295db3cae1461c17333b0b708ce12a66c2d5","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"35ffa1c307d6cbcf16612b602a2e2c73855e4ecd692bc0554443958cb245d6ab"} +{"hash_algorithm":"sha256","ts":"2026-10-24T17:26:03.220Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"cbeeadbdaee69681f3e0be9a","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/43","value":{"created_at":"2026-10-24T17:26:03.220Z","author":"codex-agent","text":"PM-linked focused c8 command passed 293/293, zero failures/cancellations/skips, exact 100/100/100/100 for closeout.ts: node node_modules/c8/bin/c8.js --100 --include=closeout.ts --reporter=text --reporter=json --reports-dir=coverage/closeout-revision --temp-directory=coverage/closeout-revision-v8 node --test test/closeout.test.ts test/closeout-attribution.test.ts test/closeout-selection.test.ts test/closeout-revision.test.ts. Existing PM policy disables result storage; this comment preserves the receipt."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T17:26:03.220Z"}],"before_hash":"aaa99da37c2f52251dafc69e3a09295db3cae1461c17333b0b708ce12a66c2d5","after_hash":"ff0c585f7167b3b8837eff3d840438f9ed9b342569d925cb99eb9065b83b4ea9","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"621ee63af0afa69066e8d92112e8d6a5d446b3e07a53d56d1299a05d4465e36d"} +{"hash_algorithm":"sha256","ts":"2026-10-24T17:27:17.957Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"cbeeadbdaee69681f3e0be9a","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/44","value":{"created_at":"2026-10-24T17:27:17.956Z","author":"codex-agent","text":"Final round-4 revert proof adds the review's exact numbering: fully inspected context PR 2 with failing CI cannot block delivery PR 1. Replaced only closeout.ts with 4dd53552f825b3b42959ae96b9e61b27fe96e900, kept all tests/workflow changes, ran node --test --test-name-pattern='fully inspected context|fully inspected mentions' test/closeout-selection.test.ts: 0/8 passed, 8 ERR_ASSERTION failures, exit 1. Restored byte-for-byte; same command via pm test --run --only-last --progress passes 8/8, exit 0, zero failures/cancellations/skips. No load failures counted."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T17:27:17.957Z"}],"before_hash":"ff0c585f7167b3b8837eff3d840438f9ed9b342569d925cb99eb9065b83b4ea9","after_hash":"c343086eb1a863e015dc5cdda00976cbfe8135f59983db14de8c8df507382c3a","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"9a438c8057ed8e5a61f0f6739e079c7f754296569139170e7a188d7191534f3e"} +{"hash_algorithm":"sha256","ts":"2026-10-24T17:30:02.705Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"cbeeadbdaee69681f3e0be9a","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/45","value":{"created_at":"2026-10-24T17:30:02.705Z","author":"codex-agent","text":"Round-4 publication table passed 186/186, zero failures/cancellations/skips/TODOs: node --test --test-name-pattern='item decision end to end' test/closeout-workflow.test.ts. All 36 earlier rows retained, plus 150 rows crossing both five-kind claim dimensions and six evidence states. Outcomes: 71 closures, 89 holds, 26 context-only no-op runs. Exact row actions, codes, candidates, context, tracker/history bytes, chosen provenance and publication summaries all passed. Local release-gate evidence is focused only; full npm/Bun release gates will be taken from CI."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T17:30:02.705Z"}],"before_hash":"c343086eb1a863e015dc5cdda00976cbfe8135f59983db14de8c8df507382c3a","after_hash":"34d9d0ceaf3be81f4cb299d58d2fa9f3aa10e09c8c56804f153238efc4aa45b4","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"24b028d28594f89266eba5ca2c1d170c77a55fb3da4f99cc52ac04152cf2940a"} +{"hash_algorithm":"sha256","ts":"2026-10-24T17:30:04.353Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"cbeeadbdaee69681f3e0be9a","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":{"value":"implementer","source":"argv"},"topic":null},"op":"release","patch":[{"op":"remove","path":"/metadata/claim_principal"},{"op":"remove","path":"/metadata/assignee"},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T17:30:04.353Z"}],"before_hash":"34d9d0ceaf3be81f4cb299d58d2fa9f3aa10e09c8c56804f153238efc4aa45b4","after_hash":"11ac485321a407dd60c51fc14550fce1d934652c6ffba28415af50ac8d853dce","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"maintenance","record_hash_version":1,"record_hash":"16e19a65f29acd9bb805d3c99e1eb334f1ce67a4f4290027cab871b1c3f7cc78"} +{"hash_algorithm":"sha256","ts":"2026-10-24T17:32:04.685Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"cbeeadbdaee69681f3e0be9a","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/46","value":{"created_at":"2026-10-24T17:32:04.684Z","author":"codex-agent","text":"Final focused suite including the exact PR-1-delivery/PR-2-mention reproduction passes 294/294 tests, exact 100/100/100/100 for closeout.ts, zero failures/cancellations/skips. Exact PM-linked c8 command remains linked on this item. Runtime/test head is 13d6ca69aef8f33e7c65df1b38bb5bbd165a0caf. Full npm/Bun CI release gates are running at https://github.com/unbraind/pm-github/actions/runs/40116410974; no full-gate success is claimed yet."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T17:32:04.685Z"}],"before_hash":"11ac485321a407dd60c51fc14550fce1d934652c6ffba28415af50ac8d853dce","after_hash":"95e63e0f20a81a553f83207800e934cdcd92aafc9ab86c3f741a9daba7406147","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"921ba65627f5fde5819a01242669a56e36747b2071d0fb8d9eead1589eac67ed"} +{"hash_algorithm":"sha256","ts":"2026-10-24T17:50:04.948Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"cbeeadbdaee69681f3e0be9a","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/47","value":{"created_at":"2026-10-24T17:50:04.944Z","author":"codex-agent","text":"Final table-input audit: retained the exact earlier ambiguous/no-competitor fixture (mention-only primary plus an unmatched delivery selector), instead of changing that input into a claim. Those two earlier rows must now be context under the corrected rule; 34 other earlier outcomes remain unchanged. Updated exact table totals: 71 closures, 87 holds, 27 context-only no-op publications and one context-only peer-edit publication (186 rows). The peer edit is independently publishable but cannot invent a delivery candidate or close event. Both initial Bun CI release gates passed; the next pushed test head will verify this fixture preservation."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T17:50:04.948Z"}],"before_hash":"95e63e0f20a81a553f83207800e934cdcd92aafc9ab86c3f741a9daba7406147","after_hash":"b05662f3f0798bbaf4db117f58fe7639fc129277cdce185c76342fa056fca2b1","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"5fbaae0d0288814f960e011d52f23b33226e3b332589996329617bd3fd21df9c"} +{"hash_algorithm":"sha256","ts":"2026-10-24T17:50:07.733Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"cbeeadbdaee69681f3e0be9a","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"tests_add","patch":[{"op":"add","path":"/metadata/tests/12","value":{"command":"node --test --test-name-pattern=\"item decision end to end: ambiguous / none\" test/closeout-workflow.test.ts","scope":"project","timeout_seconds":300,"provenance":{"author":"codex-agent","created_at":"2026-10-24T17:50:07.556Z","source_kind":"local_mutation","source_ref":"fix/closeout-delivery-attribution"}}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T17:50:07.733Z"}],"before_hash":"b05662f3f0798bbaf4db117f58fe7639fc129277cdce185c76342fa056fca2b1","after_hash":"948fdb01cf1375bc451ee295b8751e144f675a598c5fe45317b3eb5365f0edca","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"maintenance","record_hash_version":1,"record_hash":"493dc0e59ba1f52ee49c190045f1b95436c88d328ba8390baa562135509a2c6c"} +{"hash_algorithm":"sha256","ts":"2026-10-24T17:51:48.613Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"cbeeadbdaee69681f3e0be9a","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/48","value":{"created_at":"2026-10-24T17:51:48.612Z","author":"codex-agent","text":"Correction to the preceding table-audit expectation: a peer edit alone is not independently published by closeout. The retained ambiguous/no-competitor rows now both pass via the PM runner (2/2, zero failures/cancellations/skips): node --test --test-name-pattern='item decision end to end: ambiguous / none' test/closeout-workflow.test.ts. Both report context and no candidate/close/review publication; the retag remains local while remote input bytes remain intact. Final exact outcomes are 71 closures, 87 holds, 28 context-only runs without publication (186 rows). All earlier inputs are retained. Test typecheck and lint pass. Initial CI Bun release gates passed on Node 22 and 26, each 1495/1495 tests and exact 100/100/100/100 across 19 authored modules; npm jobs remain in progress at https://github.com/unbraind/pm-github/actions/runs/40116410974."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T17:51:48.613Z"}],"before_hash":"948fdb01cf1375bc451ee295b8751e144f675a598c5fe45317b3eb5365f0edca","after_hash":"6d6e1c957a94f4281a836bf104c0ca7d1170c52510cb4fdb9da8d6452af1e866","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"0905f549363af1dc4c67eb40c202a9e6785e06f50798a7f242e802c21ef0a4b1"} +{"hash_algorithm":"sha256","ts":"2026-10-24T17:52:52.997Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"cbeeadbdaee69681f3e0be9a","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":{"value":"implementer","source":"argv"},"topic":null},"op":"claim","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T17:52:52.997Z"},{"op":"add","path":"/metadata/assignee","value":"codex-agent"},{"op":"add","path":"/metadata/claim_principal","value":"codex-agent"}],"before_hash":"6d6e1c957a94f4281a836bf104c0ca7d1170c52510cb4fdb9da8d6452af1e866","after_hash":"21a1c8fd14d7669a42a882f7b35d7870ac97c8326c1c9f1eb78ceb39e67682d4","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"8e5b98cef737a552e596bf97dab3f66b2e603fbca58e57f5bebd78719d9016d0"} +{"hash_algorithm":"sha256","ts":"2026-10-24T18:08:40.553Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"cbeeadbdaee69681f3e0be9a","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/49","value":{"created_at":"2026-10-24T18:08:40.552Z","author":"codex-agent","text":"Final preserved-input decision table passed via pm test --run --only-index 8 --progress --timeout 1200: 186/186 tests, zero failures/cancellations/skips/TODOs, exit 0. Exact linked command: node --test --test-name-pattern='item decision end to end' test/closeout-workflow.test.ts. All 36 earlier inputs retained, including mention-only primary with unmatched selector; both such rows correctly stay context. Final outcomes: 71 closures, 87 holds, 28 context-only runs without publication. The peer-retag context row keeps the peer edit local and verifies unchanged remote input bytes. Core source remains byte-identical to 13d6ca69aef8f33e7c65df1b38bb5bbd165a0caf; pending changes preserve table inputs and validation receipts only."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T18:08:40.553Z"}],"before_hash":"21a1c8fd14d7669a42a882f7b35d7870ac97c8326c1c9f1eb78ceb39e67682d4","after_hash":"c379df4202a8c02d4bd11cde154060083d4fa90b67e353e05e96aa0dc2b5cd64","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"384b2f1a457c576b212a43b3129dbb7d35bfaae5e2b1759dc9d59e336933a7e9"} +{"hash_algorithm":"sha256","ts":"2026-10-24T18:12:17.873Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"cbeeadbdaee69681f3e0be9a","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/50","value":{"created_at":"2026-10-24T18:12:17.873Z","author":"codex-agent","text":"Initial CI release-gate receipts at code head 13d6ca69aef8f33e7c65df1b38bb5bbd165a0caf: https://github.com/unbraind/pm-github/actions/runs/40116410974. Complete npm release gate on Node 22 and complete Bun release gates on Node 22/26 passed, each 1495/1495 tests, zero failures/cancellations/skips, exact 100/100/100/100 across all 19 authored modules. Native Bun 1.3.14 acceptance in the npm/Node-22 job also passed 1495/1495 across 52 files, zero failures. All other stages in those complete gates passed. npm/Node-26 job remains running, so the whole initial run is not yet claimed green. Final preserved-input table is independently green 186/186 through PM; final pushed-head CI will be verified through the PR checks. No new PM CLI/SDK bug was discovered; existing result-storage policy remains unchanged."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T18:12:17.873Z"}],"before_hash":"c379df4202a8c02d4bd11cde154060083d4fa90b67e353e05e96aa0dc2b5cd64","after_hash":"02daae2a7eecab7a4c12d3e721f93b3f73482f960c9d8cfabe2ebf9f7d7ce4c8","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"f5416f4c4eab0a648ef3e0af2e49ac2780d94623dc471c84feec147d6ed56b8a"} +{"hash_algorithm":"sha256","ts":"2026-10-24T18:12:19.996Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"cbeeadbdaee69681f3e0be9a","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":{"value":"implementer","source":"argv"},"topic":null},"op":"release","patch":[{"op":"remove","path":"/metadata/claim_principal"},{"op":"remove","path":"/metadata/assignee"},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T18:12:19.996Z"}],"before_hash":"02daae2a7eecab7a4c12d3e721f93b3f73482f960c9d8cfabe2ebf9f7d7ce4c8","after_hash":"59bcf8b9ec1c38bd276b1233bd9c1ab37633908796fc472dc29b1ffa0f4adb87","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"maintenance","record_hash_version":1,"record_hash":"739cebd67a0d3e3bab461203193418764b5d80917df2875df32197a770da8896"} diff --git a/.agents/pm/history/pm-github-vhi6.jsonl b/.agents/pm/history/pm-github-vhi6.jsonl index 96e3d3fc..09c34fcd 100644 --- a/.agents/pm/history/pm-github-vhi6.jsonl +++ b/.agents/pm/history/pm-github-vhi6.jsonl @@ -2,3 +2,13 @@ {"hash_algorithm":"sha256","ts":"2026-10-14T14:33:37.749Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"85cc36f7b561a65ec3db0d35","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"files_add","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-10-14T14:33:37.749Z"},{"op":"add","path":"/metadata/files","value":[{"path":"scripts/coverage-gate.ts","scope":"project"}]}],"before_hash":"e042ad93a45aecedbee4768a205aac55b311a4ae649f3646f25e36ed532004dc","after_hash":"0c9c2124cb804d21b63f5e79d99eeb71f1eeab2a1a7da0b05ec57167d47e5c3f","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"maintenance","record_hash_version":1,"record_hash":"881d648f5c871728b5422265648b2d77ef2f1c215b358b2cc529010d7468030a"} {"hash_algorithm":"sha256","ts":"2026-10-14T14:33:39.357Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"85cc36f7b561a65ec3db0d35","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"files_add","patch":[{"op":"add","path":"/metadata/files/1","value":{"path":"test/import-gate.test.ts","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-14T14:33:39.357Z"}],"before_hash":"0c9c2124cb804d21b63f5e79d99eeb71f1eeab2a1a7da0b05ec57167d47e5c3f","after_hash":"077d0a3603074ad6b710b3b804c9800ae9c7a05b8a5bb66d514af161e78875e3","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"maintenance","record_hash_version":1,"record_hash":"10879d5be967a132fe42cf02c04d0e0c923ec4da99b73fafaf02058c66705f64"} {"hash_algorithm":"sha256","ts":"2026-10-14T15:04:27.090Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"85cc36f7b561a65ec3db0d35","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-10-14T15:04:27.090Z"},{"op":"add","path":"/metadata/comments","value":[{"created_at":"2026-10-14T15:04:27.090Z","author":"codex-agent","text":"Confirmed resource workaround: complete identity-isolated npm and Bun release:check each passed 587/587 tests and all 11 checks with NODE_OPTIONS=--max-old-space-size=12288. Coverage remains exact 100/100/100/100 across 12 authored modules, without source exemptions or threshold changes. The earlier default-heap npm run passed all 587 tests before c8 aggregation exhausted the heap. Default-heap CI comparison and bounded aggregation remain follow-up work; the security change does not alter coverage implementation or CI memory settings."}]}],"before_hash":"077d0a3603074ad6b710b3b804c9800ae9c7a05b8a5bb66d514af161e78875e3","after_hash":"b864dfba4ffb8a0939ef768641ce1055d227ead20d466da3906c395dcaf30ab1","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"26f184be7cf9759994ce653ea8f2f3829cd3708a619a83c28813adad7b792303"} +{"hash_algorithm":"sha256","ts":"2026-10-24T14:34:24.911Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"a5f9cfc035cd6c24601c6c47","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/1","value":{"created_at":"2026-10-24T14:34:24.911Z","author":"codex-agent","text":"Additional measured receipt from pm-github-closeout-attribution / PR 174: CI https://github.com/unbraind/pm-github/actions/runs/40101464708 at 30b94f1 failed all four npm/Bun release jobs on Node 22/26 with JavaScript heap exhaustion during c8 aggregation at the existing 8192 MiB limit. The Node 26 test phases each passed 1337/1337, zero failures/cancellations/skips before aggregation failed. No release-gate pass is claimed. This round reduces repeated setup in the new 36-case workflow matrix (shared immutable Git/tracker baselines and only the actual closeout/publication steps); the broader bounded aggregation task remains open, with thresholds, source inventory and heap configuration unchanged."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T14:34:24.911Z"}],"before_hash":"b864dfba4ffb8a0939ef768641ce1055d227ead20d466da3906c395dcaf30ab1","after_hash":"139671df515d94a358240ca8f2e618704f1c7cc8a5640dba10af2bb6b0fe8a10","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"3eeaeedb0b073702070136231fd5dd7d8b439e621a4e4d62186c4f376cac3190"} +{"hash_algorithm":"sha256","ts":"2026-10-24T14:38:08.330Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"a5f9cfc035cd6c24601c6c47","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"files_add","patch":[{"op":"replace","path":"/metadata/files/1/path","value":"test/closeout-workflow.test.ts"},{"op":"add","path":"/metadata/files/2","value":{"path":"test/import-gate.test.ts","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T14:38:08.330Z"}],"before_hash":"139671df515d94a358240ca8f2e618704f1c7cc8a5640dba10af2bb6b0fe8a10","after_hash":"3d0214f1d8bf3749f62561f652be9616690112ad3535b0e97a6386417cddea1f","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"maintenance","record_hash_version":1,"record_hash":"c2082d2b0a602da3361a1bf241a8ed893272e65b9bed3f84786a9dd8adb86c1b"} +{"hash_algorithm":"sha256","ts":"2026-10-24T15:10:00.744Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"a5f9cfc035cd6c24601c6c47","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/2","value":{"created_at":"2026-10-24T15:10:00.744Z","author":"codex-agent","text":"Second receipt: https://github.com/unbraind/pm-github/actions/runs/40103921945 at ad2e7cb passed 1337/1337 tests in every npm/Bun Node 22/26 job, then all four c8 aggregations exhausted the unchanged 8192 MiB heap. Sharing tracker callers alone was insufficient while package setup still repeated per cell. The next harness prepares package/scope setup once per immutable baseline; every one of the 36 cases still restores input, runs actual branch preparation, real CLI closeout, publication privacy/refusal gates and leased push, with fresh receipts and provenance/history assertions. No coverage resource or threshold change is used. The broader bounded aggregation task remains open."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T15:10:00.744Z"}],"before_hash":"3d0214f1d8bf3749f62561f652be9616690112ad3535b0e97a6386417cddea1f","after_hash":"5692989f622c046cbfca1e082eb7b5cd37de6732fcfb429b8d79d75a23ccb349","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"499a11bf72a9d956cb61cb0c7b981d64259d89374964807cf9f09da38673e7f4"} +{"hash_algorithm":"sha256","ts":"2026-10-24T15:38:00.424Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"a5f9cfc035cd6c24601c6c47","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":{"value":"implementer","source":"argv"},"topic":null},"op":"claim","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T15:38:00.424Z"},{"op":"add","path":"/metadata/assignee","value":"codex-agent"},{"op":"add","path":"/metadata/claim_principal","value":"codex-agent"}],"before_hash":"5692989f622c046cbfca1e082eb7b5cd37de6732fcfb429b8d79d75a23ccb349","after_hash":"03798952bd1594d8cbd51eea7dae522ab87a83eb33cbc1a59fdf2b7cb66554d0","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"2a78064dad7f614c98cd5d15ce2c52967b5db805555632cabc268a45a071ac30"} +{"hash_algorithm":"sha256","ts":"2026-10-24T15:38:01.118Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"a5f9cfc035cd6c24601c6c47","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":{"value":"implementer","source":"argv"},"topic":null},"op":"update","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T15:38:01.118Z"},{"op":"replace","path":"/metadata/status","value":"in_progress"}],"before_hash":"03798952bd1594d8cbd51eea7dae522ab87a83eb33cbc1a59fdf2b7cb66554d0","after_hash":"be53ede955a4b7d82ce9ac6dcb1c8578a3071f0a56578ccec551b9fa47107f11","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"89dbe43d5c1b9444ab3e372f3d1bace983e7cdcdf000a54f4b49a49d819b0d03"} +{"hash_algorithm":"sha256","ts":"2026-10-24T15:38:02.602Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"a5f9cfc035cd6c24601c6c47","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"docs_add","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T15:38:02.602Z"},{"op":"add","path":"/metadata/docs","value":[{"path":"docs/explicit-closeout-attribution.md","scope":"project"}]}],"before_hash":"be53ede955a4b7d82ce9ac6dcb1c8578a3071f0a56578ccec551b9fa47107f11","after_hash":"e038bdfe409c66ceb96fc5f9192b27bd7bd33daa0408697acfffa85367ff5883","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"maintenance","record_hash_version":1,"record_hash":"396c11244ca6a8278f081fde1365f8aba901db64459aad4ca964a446c47dfd46"} +{"hash_algorithm":"sha256","ts":"2026-10-24T15:38:03.326Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"a5f9cfc035cd6c24601c6c47","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"tests_add","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T15:38:03.326Z"},{"op":"add","path":"/metadata/tests","value":[{"command":"node --test test/coverage-gate.test.ts","scope":"project","timeout_seconds":300,"provenance":{"author":"codex-agent","created_at":"2026-10-24T15:38:03.298Z","source_kind":"local_mutation","source_ref":"fix/closeout-delivery-attribution"}}]}],"before_hash":"e038bdfe409c66ceb96fc5f9192b27bd7bd33daa0408697acfffa85367ff5883","after_hash":"8a9867a30979a5c2c4476232bd22f7074f36afbfd0e069685038cb2a6ce64d4b","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"maintenance","record_hash_version":1,"record_hash":"5120cc3f5294fd5aabc67c11d79c8ea24ba894d5ae1d8b558a72c72a89aa0547"} +{"hash_algorithm":"sha256","ts":"2026-10-24T15:38:04.069Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"a5f9cfc035cd6c24601c6c47","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/3","value":{"created_at":"2026-10-24T15:38:04.069Z","author":"codex-agent","text":"Third CI https://github.com/unbraind/pm-github/actions/runs/40106191758 at cff0f1c passed the complete npm release gate on Node 22 after preparing four baseline callers once. Both Node 26 jobs still passed 1337/1337 tests and exhausted c8 at 8192 MiB. Fixture reductions alone cannot establish Node 26 full-gate acceptance. Adjust only the c8 merge heap ceiling to 12288 MiB, leaving headroom on this public repository's documented 16 GB ubuntu-latest runners (https://docs.github.com/en/actions/reference/runners/github-hosted-runners#standard-github-hosted-runners-for-public-repositories). All four coverage thresholds remain 100, the complete 19-module inventory, zero-exclusion policy, test set and dependency pins stay unchanged. Earlier comments preserving the 8 GiB ceiling describe failed earlier attempts; measured Node 26 failures now justify this resource-only revision. General bounded aggregation remains unfinished and this item will not be closed."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T15:38:04.069Z"}],"before_hash":"8a9867a30979a5c2c4476232bd22f7074f36afbfd0e069685038cb2a6ce64d4b","after_hash":"361bee48aa58e0e359eb541ed565ff36a96ce743add8573e5b984b172715a9de","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"e3ccad79af2b41c2f6c1bec1842a5aaadeada9c09c839d7b01bf956404d65b39"} +{"hash_algorithm":"sha256","ts":"2026-10-24T15:42:23.333Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"a5f9cfc035cd6c24601c6c47","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/4","value":{"created_at":"2026-10-24T15:42:23.333Z","author":"codex-agent","text":"Resource-only local verification: PM-linked node --test test/coverage-gate.test.ts passes 9/9, zero failures/cancellations/skips. These tests run real c8 fixture reports and preserve threshold-refusal, missing-source, invalid-receipt and pre-entry heap-routing controls. Production typecheck, test typecheck, lint, docstrings and zero duplication pass. Only COVERAGE_HEAP_FLAG changes from 8192 to 12288 MiB, plus explanatory comments. Full CI is still required; this is not bounded-memory certification and the general task stays in progress for orchestrator follow-up."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T15:42:23.333Z"}],"before_hash":"361bee48aa58e0e359eb541ed565ff36a96ce743add8573e5b984b172715a9de","after_hash":"37eb09267646185de1cc7f4aa56d79a6b7dd14fe780a8c2ba767be5739ab4b8d","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"07108c6fadab5698a28734ede0cebdd862c7dbba92c8501dbc2fa6197060ec04"} +{"hash_algorithm":"sha256","ts":"2026-10-24T15:42:24.889Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"a5f9cfc035cd6c24601c6c47","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":{"value":"implementer","source":"argv"},"topic":null},"op":"release","patch":[{"op":"remove","path":"/metadata/claim_principal"},{"op":"remove","path":"/metadata/assignee"},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-24T15:42:24.889Z"}],"before_hash":"37eb09267646185de1cc7f4aa56d79a6b7dd14fe780a8c2ba767be5739ab4b8d","after_hash":"2caedacd5d8ecbf6cd6548d9101d8ee9acf28349a9e82816303b8c3a62a17b3e","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"maintenance","record_hash_version":1,"record_hash":"ef39ef5ddfed9dae35c33023268fa8b9e3ae28d2e9c1bf624ada6784a3984cc3"} diff --git a/.agents/pm/issues/pm-github-closeout-attribution.toon b/.agents/pm/issues/pm-github-closeout-attribution.toon new file mode 100644 index 00000000..12ee8d2e --- /dev/null +++ b/.agents/pm/issues/pm-github-closeout-attribution.toon @@ -0,0 +1,180 @@ +id: pm-github-closeout-attribution +title: Hold ambiguous merged-PR delivery until attribution is verified +description: "Follow up the completed declaration matcher with explicit item delivery links, acceptance-criteria chronology and needs_review candidates; pm-ado issue 33 and PR 32 expose incorrect durable delivery provenance." +type: Issue +status: in_progress +priority: 1 +tags[3]: changelog-unreleased,closeout,"gh:unbraind/pm-ado#33" +created_at: "2026-10-24T06:16:55.856Z" +updated_at: "2026-10-24T18:12:19.996Z" +author: codex-agent +acceptance_criteria: Explicit delivery link wins; otherwise exactly one claimed merged PR after the latest criteria change may close; ambiguous items remain open with all candidates reported; completion uses the chosen merge timestamp. +comments[51]{created_at,author,text}: + "2026-10-24T06:16:59.176Z",codex-agent,"Read pm-ado issue 33 and dirty PR 32. Existing pm-github-uqjf is closed and covers declarations; this follow-up replaces its latest-merge tie-break with explicit delivery attribution and acceptance-history checks. Plan: real non-git tracker regressions, function-only revert proof, focused local gates and npm/Bun CI release gates. PR 32 will be closed with cross-links after the fix PR exists; issue 33 stays open." + "2026-10-24T06:21:01.545Z",codex-agent,"Implemented explicit delivery-pr URL tags, immutable SDK acceptance-history cutoff, unique body/commit claims and needs_review rows retaining all candidates. Check success no longer breaks ties or falls back to old delivery. Review-only workflow runs persist a privacy-gated closeout-review.json report and open a PR without closing any item. Initial non-git selection suite passes 13/13; existing chronological tie-break assertions updated to require review. Dependency pins and gate thresholds unchanged." + "2026-10-24T06:24:07.508Z",codex-agent,"Pinned pm test run passed selection 13/13 with no failures or skips; test-result storage is disabled by existing project policy, so this comment retains the receipt. Independently inspected pm-ado PRs 16 and 19: 16 explicitly leaves statement enforcement open; 19 declares the item and merged at 2026-10-08T01:19:51Z. Added a review-only workflow report; held attribution also disables automatic landing after dispatching caller checks." + "2026-10-24T06:25:34.878Z",codex-agent,"Behavioral revert proof: replaced only runCloseout with its f4c29e8 origin/main implementation, preserving current helpers and tests. Ran node --test --test-name-pattern='ambiguous linked merged PRs|explicit delivery link selects|a claim before or equal' test/closeout-selection.test.ts. Reverted function: exit 1, 0/3 pass and 3 ERR_ASSERTION failures (ambiguous delivery closed, explicit override ignored, pre-criteria claim closed). Restored function: exit 0, 3/3 pass, no skips. Tests import TypeScript source directly; no dist rebuild is needed. Focused c8 reports closeout.ts at exact 100/100/100/100; one pre-update array-order assertion was corrected and its full attribution/selection suites now pass 25/25." + "2026-10-24T06:29:51.379Z",codex-agent,"PR https://github.com/unbraind/pm-github/pull/174 is ready for review. Closed DIRTY pm-ado PR 32 with links to this fix and pm-ado issue 33; verified state CLOSED with mergedAt null and issue 33 still OPEN. No pm-ado branch or tracker edits. Added a completeness guard: unreadable commit messages on a qualifying linked mention may hide a competing claim, so they hold automatic selection. Narrow guard-only revert made the new regression fail ERR_ASSERTION (closed 1 instead of 0, exit 1); restored guard and pre-criteria negative control pass 2/2." + "2026-10-24T06:30:38.823Z",codex-agent,"Final focused command node --test test/closeout.test.ts test/closeout-attribution.test.ts test/closeout-selection.test.ts passes 117/117, zero failures/cancellations/skips. Focused c8 measures exact 100/100/100/100 for closeout.ts at the completed implementation. Full workflow suite passes 34/34, plus the updated automatic-landing hold regression passes 1/1. Typecheck, test typecheck, lint, docstrings (201 declarations), zero source duplication, changelog checks and clean-environment strict tracker health pass. Initial CI is running on the earlier source commit; final npm/Bun release-gate evidence will be reported in PR 174 checks and body after this last implementation update. Item stays in progress for orchestrator verification." + "2026-10-24T06:42:48.829Z",codex-agent,"CI run 40070500891 exposed three cross-suite fixture/contract failures: Node 26 release gate ran 1121 tests, 1118 passed and 3 failed; all authored sources still measured exact 100/100/100/100. Two changelog-placement fixtures create the item after the synthetic delivery date; the maintenance equality assertion does not account for the new closeout-only attribution hold. Correcting fixture chronology with the deterministic SDK clock and preserving exact parity for the remaining maintenance shell. This failed run is not full-gate approval." + "2026-10-24T06:43:28.268Z",codex-agent,"Cross-suite corrections pass: node --test test/bot-pr-workflow.test.ts test/changelog-placement.test.ts = 59/59, zero failures/cancellations/skips. Changelog fixtures now create items with a deterministic September SDK clock before October delivery; commit evidence correctly returns an empty list. The parity assertion requires exactly one closeout attribution-hold block and exact equality for all remaining maintenance code. Existing end-to-end review-only acceptance proves this hold dispatches checks without enabling automatic landing. Test typecheck and lint pass. Runtime source remains identical to 8b0dc30; full CI will rerun on the corrected test head." + "2026-10-24T07:46:02.118Z",codex-agent,"Review round for PR 174: retain every plausible linked merged candidate independently of evidence quality, report evidence gaps and hold ambiguity before mutation; disarm reused workflow PRs before updates and on all exit paths. Add table-driven real-tracker and stateful workflow regressions, record fix-only revert proofs, then run targeted checks and unchanged npm/Bun release gates. Remote branch is current and contains origin/main." + "2026-10-24T07:53:26.797Z",codex-agent,"Class 1 revert proof: replaced only runCloseout with the pre-round implementation, retained current helpers and regression table, and ran node --test --test-name-pattern=\"incomplete candidate holds attribution\" test/closeout-selection.test.ts. Exit 1 with assertion failures, including 1 closure instead of 0 for missing merge SHA plus another valid claim; fetch-error cases also exposed the prior abort instead of a listed hold. Restored runCloseout. The full restored selection suite passed 35/35, zero failures, cancellations or skips. Class 2 revert proof: replaced only both explicit --disable-auto operations with no-ops, retained the workflow and stateful harness, and ran node --test --test-name-pattern=\"earlier armed auto-merge is disabled\" test/closeout-workflow.test.ts. Exit 1 with ERR_ASSERTION true !== false: the earlier request remained armed after the later hold. Restored both disable operations. Further restored-suite validation is in progress." + "2026-10-24T08:00:32.158Z",codex-agent,"Evidence-gap rows now preserve all candidate numbers and list missing fields/codes in compact output and the privacy-gated workflow artifact. Unknown merged status/time is not a chronology exclusion; a failed PR read conservatively holds eligible active items because linkage cannot be disproved. The workflow revokes before any guard or update and always cleans up after hold, no publication, disabled CI, failure or cancellation; initial revocation denial blocks publication. Reviewed all exit classes and documented the audit. Maintenance/changelog contracts passed 59/59; typecheck, test typecheck, lint, docstrings and zero duplication passed. Focused coverage measured 100/100/100/100 before the final merge-field reporting helper; full gates will certify the final source. A new parent-scope control correctly held the eligible child as well, so its assertion now checks that the parent is absent from review rows." + "2026-10-24T08:08:32.103Z",codex-agent,"Additional class 1 pagination proof: PR commit totals now certify the complete commit-page set. Missing, invalid or mismatched counts hold candidates, including a body mention whose omitted commit could claim delivery. Removed only the commit-count completeness guard and ran node --test --test-name-pattern=\"incomplete candidate holds attribution: .*commits-count\" test/closeout-selection.test.ts: 4/4 failed, exit 1, including 1 closure instead of 0 with another valid claim. Restored the guard. The stateful hold regression and unarmed-PR control pass 2/2 after filtering actual autoMergeRequest state; the corrected fresh-job no-op control passes 1/1. The harness keeps remote PR state across runs and resets only job environment state, matching Actions semantics. Final npm gate is being restarted for this complete pagination fix; prior interrupted gates are not counted as evidence." + "2026-10-24T08:10:08.391Z",codex-agent,"Final candidate table contains 20 disqualifying evidence conditions crossed with another valid claim or none (40 cases). The pinned pm linked selection run passed 59/59 before the two negative-count controls were added; those additional controls passed 2/2 separately. Final compiled workflow hold/unarmed/no-op acceptance passed 3/3. The 59-test maintenance/changelog contract suite passed; final typecheck, test typecheck and lint passed. Full npm release-check is running on the complete source and test set, with unchanged thresholds, pins and coverage inventory. Origin main has not advanced. No PM CLI/SDK defect was found; linked test storage remains disabled by existing project policy, so comments retain the receipts." + "2026-10-24T08:28:53.542Z",codex-agent,"CI run https://github.com/unbraind/pm-github/actions/runs/40076990035 at e795426: both bun run release:check jobs (Node 22 and 26) completed successfully, each 1177/1177 tests, zero failures and exact 100/100/100/100 across 19 authored modules. Both Complete npm release gate steps also succeeded; their jobs are now running additional native Bun acceptance, so overall CI is not yet approval. The independent local npm gate has completed 1177/1177 tests with zero failures and is finishing coverage/release processing. The final small source change makes the new commit-count property optional for callers of checkCloseoutHead, preserving the head-check API; decoded closeout PRs still require verified totals. Typecheck and test typecheck passed with this compatibility adjustment. Implementation is complete; remaining work is read-only final CI verification. Final receipts will be retained in PR 174 body and the handoff summary. The tracker item remains in progress for orchestrator verification." + "2026-10-24T09:04:43.750Z",codex-agent,"Final consistency review found a report-state defect in the new unreadable-PR path: an explicit valid delivery tag can resolve attribution and close its item, while the non-selected unreadable PR retains an initial needs_review action without candidate details. Initialize that row as a refusal until attribution is resolved, as for all other candidate rows; unresolved groups still promote every row to needs_review. Add a real-tracker regression that a resolved explicit link cannot leave a held row for its closed item. Supersede pending CI with the corrected final head; do not count the old run as final approval." + "2026-10-24T09:14:11.717Z",codex-agent,"Candidate-source audit also found that unreadable commit sets with reference-free descriptions could hide a competing commit-only claim. Added six unreadable commit conditions crossed with another valid claim or none (12 controls); all 12 fail against the prior candidate collection, including closures using the other claim. Retain possible commit-only links for every eligible active item when commit evidence is incomplete; use the known merge time to exclude only provably pre-criteria claims. Explicit valid delivery tags keep their existing attribution role. The earlier stale-review correction passed 164/164 focused tests at exact closeout coverage, pinned PM selection 62/62 and native Bun 1.3.5 selection 62/62; an isolated action-only reversion failed ERR_ASSERTION and restoration passed 1/1. Final combined source is now frozen for validation." + "2026-10-24T09:22:57.811Z",codex-agent,"Final combined regressions pass: focused closeout command node --test test/closeout.test.ts test/closeout-selection.test.ts test/closeout-attribution.test.ts = 176/176, zero failures/cancellations/skips, exact 100/100/100/100 for closeout.ts. Pinned pm test selection = 74/74; native Bun 1.3.5 selection = 74/74. Latest compiled workflow hold/unarmed/no-op controls = 3/3. Typecheck, test typecheck, lint, docstrings, zero source duplication and privacy passed. Isolated fix-only retention-guard reversion failed 12/12 commit-only controls (including one closure instead of zero), restoration passed 12/12. Isolated action-only reversion failed the stale-review regression with ERR_ASSERTION, restoration passed 1/1. Together with the prior function-only 20-case, count-guard 4-case and auto-merge-request proofs these cover both classes. Main advanced only for release metadata; merge it and regenerate the changelog inside the merge before final CI. Earlier superseded CI runs are not final-head approval." + "2026-10-24T09:27:12.156Z",codex-agent,"Merged origin/main release 2026.10.24 with the merge still pending; regenerated CHANGELOG.md inside the merge and its check passed. Release metadata changes only index version, manifest/package versions and changelog; closeout implementation and regression source are unchanged from the 176/176 focused, 74/74 PM-linked and 74/74 native Bun receipts. Post-merge typecheck, lint and build passed. Strict pm health including required merge drivers passed with no merge-receipt reconciliation needed. Both classes and all identified candidate-source gaps are implemented and verified locally; final immutable npm/Bun CI will validate the merge head. Final CI receipts will be retained in PR 174 body and the final summary. Item remains in progress for orchestrator verification; release the claim at handoff, never close it." + "2026-10-24T10:10:12.774Z",codex-agent,"Integration audit of the new null-timestamp selection found that confirmed unmerged PRs can reach the workflow incomplete-merge refusal even when another PR uniquely delivers the item. Separate known-unmerged routine skips from incomplete evidence about potentially merged PRs, and add an actual workflow regression with an unmerged reference plus a valid merged delivery. This preserves the prior since-range behavior while retaining unknown merge timestamps. Supersede old CI only after reproducing this integration regression; do not treat earlier results as approval for the correction." + "2026-10-24T10:32:28.062Z",codex-agent,"Known-unmerged integration regression verified and fixed: raw false merge status is a routine not_merged skip, distinct from incomplete evidence about possibly merged PRs. Both CLI compaction and workflow summary count it; the workflow can publish the unique merged delivery. Final-source focused closeout suite passed 176/176 with exact 100/100/100/100; held/unarmed/no-op/known-unmerged workflow controls passed 4/4. New filtered workflow command is linked and pinned pm test --run passed 1/1. Reverted only the confirmed-unmerged classifier, rebuilt the package, and ran node --test --test-name-pattern=\"confirmed unmerged references\" test/closeout-workflow.test.ts: ERR_ASSERTION exit 1, publication refused with 1 instead of expected 0. Restored classifier, rebuilt, and passed 1/1. Existing ambiguity, commit-only and auto-merge revert proofs remain recorded above. Typecheck, test typecheck, lint and build passed; final immutable CI will certify all-source coverage and native Bun on the corrected head. No PM CLI/SDK defect was found. Keep the item in progress; release the claim for orchestrator verification without closing it." + "2026-10-24T11:49:11.401Z",codex-agent,"Review round 2: audit all closeout read-to-write decisions, bind delivery selection and close provenance to the selected item revision, and exercise peer edits with real SDK mutations in a non-git tracker. Remote branch and main are current; pinned SDK remains unchanged." + "2026-10-24T11:57:13.246Z",codex-agent,"SDK audit: pinned 2026.10.20 exposes no expectedRevision/ifUnchanged on close or BulkItemCloseMutation; BulkItemUpdateMutation.match applies only to updates, and runWithItemMutationPrecondition is internal, not a public export. Use public runWithActiveExtensions plus setActiveExtensionHooks beforeMutation to compare hashDocument(context.before) inside the SDK per-item lock before persistence. This preserves normal close validation, workflow policy, history and atomic close evidence. Needed upstream API: close(id, reason, { expectedItemHash }, clientOptions), checked under the same item lock with a stable revision-conflict refusal. Retry selection only for the conflicted item once. Audit: tags, links, criteria chronology, status, comments and body are read-only inputs covered by the exact document hash; close writes completed_at, resolution, expected_result, actual_result and close_reason together. No independent tag/link/comment/criteria mutation path exists in closeout." + "2026-10-24T12:07:08.618Z",codex-agent,"Revert proof: restored only closeout.ts from the PR head before this revision, kept the regression test, and ran node --test --test-name-pattern=\"retag at 1:pull\" test/closeout-revision.test.ts. Result: 0 pass, 1 fail; stale closure selected PR 1 after delivery-pr became PR 2 (assertion 1 !== 2). Restored the fix and reran the identical command: 1 pass, 0 fail. The guard now also covers SDK implicit dependent unblocking; concurrent prerequisite edits cannot be overwritten, and refused dependent writes receive one fresh delivery selection or an explicit hold." + "2026-10-24T12:08:24.709Z",codex-agent,"Final-source guard-only revert proof in an isolated source copy (active gate source unchanged): removed only the under-lock hash comparison, retained all observers, and ran node --test --test-name-pattern=\"retag at 1:pull|dependent update: changed\" test/closeout-revision.test.ts. Both failed (0/2): stale PR 1 provenance after retag to PR 2, and a peer prerequisite edit overwritten by automatic unblock (open instead of blocked). Restoring only that comparison passed 2/2. The ordinary SDK close lock and lifecycle implementation were real in both runs." + "2026-10-24T12:15:43.152Z",codex-agent,"The pinned PM-linked node --test test/closeout-revision.test.ts run passed 109/109 with zero failures, skips or cancellations. Further audit found a document/history read boundary: a peer SDK update writes the item before appending history while retaining its lock. Acceptance chronology now uses verifyHistoryEntries(history, selectedDocument), so a new document cannot borrow the previous revision criteria cutoff. A real-SDK history_append scheduling barrier exercises this partial-write boundary. The superseded local broad gate was stopped before changing source; only final-source receipts will be used." + "2026-10-24T12:17:28.572Z",codex-agent,"History-binding revert proof: in an isolated source copy changed only verifyHistoryEntries(history, selectedDocument) back to verifyHistoryEntries(history), retained the real SDK partial-write scheduling barrier, and ran node --test --test-name-pattern=\"binds acceptance history\" test/closeout-revision.test.ts. Reverted: 0 pass, 1 fail (closed 1 instead of 0 using an older criteria cutoff for the newer document). Restored: 1 pass, 0 fail. The final local targeted command covers all four closeout suites and enforces exact 100/100/100/100 coverage for closeout.ts." + "2026-10-24T12:19:39.759Z",codex-agent,"Final-source local validation passed: the PM-linked c8 --100 command ran all four closeout suites, 286/286 tests, zero failures/cancellations/skips, exact 100/100/100/100 for closeout.ts. The revision suite contributes 110 tests, including 17-by-6 peer-edit boundaries, bounded retry, guard isolation, removed snapshots, implicit dependent updates and document/history binding. Typecheck, build, test typecheck and lint passed; docstrings, zero-duplication, privacy and canonical changelog checks passed. Strict health with required merge drivers passed with isolated HOME; ambient global extension version skew is absent in the CI-equivalent environment. PM test-result storage is disabled by existing policy, so this comment retains the receipt. Full npm/Bun release evidence will come from CI on the pushed head; thresholds and dependency pins remain unchanged." + "2026-10-24T13:52:01.374Z",codex-agent,"Review round 3: reproduce selected-delivery publication refusal, then derive all candidate rows from one item decision: chosen, superseded informational, or blocking hold. Add a 3 x 6 x 2 end-to-end decision table through the actual workflow publication gate, including peer retags. Record behavioral revert proof; run targeted tests, typecheck and lint locally, and require npm plus Bun full release gates in CI on the pushed head." + "2026-10-24T13:57:57.687Z",codex-agent,"Reproduced the exact round-3 blocker before editing runtime source: the explicit-delivery/missing-merge-SHA/no-peer end-to-end test fails ERR_ASSERTION with exit 1 because the real workflow rejects incomplete merged-PR metadata after local closeout. ItemDecision now projects chosen or blocking-hold outcomes across every candidate; all nonchosen candidates become duplicate_selection informational rows, regardless of original evidence failures. Failed chosen checks and SDK refusals hold all peers. Typecheck, test typecheck, lint, docstrings and zero-duplication checks pass. The 36-cell workflow matrix and existing non-git SDK suites are running." + "2026-10-24T14:06:16.808Z",codex-agent,"Focused PM-linked c8 verification passed 286/286 tests, zero failures, cancellations or skips, with exact 100/100/100/100 for closeout.ts. Exact command: node node_modules/c8/bin/c8.js --100 --include=closeout.ts --reporter=text --reporter=json --reports-dir=coverage/closeout-revision --temp-directory=coverage/closeout-revision-v8 node --test test/closeout.test.ts test/closeout-attribution.test.ts test/closeout-selection.test.ts test/closeout-revision.test.ts. Existing result-storage policy is disabled; this comment retains the receipt. The peer-retag table explicitly allows a safe hold when a peer invalidates the selected document/history revision before attribution; explicit re-selection with a valid retag closes with fresh provenance." + "2026-10-24T14:07:20.953Z",codex-agent,"Round-3 fix-only behavioral revert proof: restored only closeout.ts to 4874023 (the pre-round-3 runtime, retaining all round-2 revision guards), rebuilt dist with npm run build, and retained the new end-to-end tests plus workflow unchanged. Command: node --test --test-name-pattern='item decision end to end: explicit delivery tag / (missing merge SHA|failed evidence fetch) / none' test/closeout-workflow.test.ts. Reverted result: 0/2 passed, 2 ERR_ASSERTION failures, exit 1. Both regressions fail at the workflow publication gate after closeout rather than at load/build: incomplete merge or unavailable evidence leaves a fatal competing refusal. Restored the fix byte-for-byte, rebuilt, then ran the same command through pm test --run --only-index 9 --progress with isolated HOME and Git configuration: 2/2 passed, zero failures/cancellations/skips, exit 0. No tests, workflow gates or thresholds were reverted." + "2026-10-24T14:10:59.424Z",codex-agent,"Round-3 final local validation: PM-linked node --test --test-name-pattern='item decision end to end' test/closeout-workflow.test.ts passed 36/36, zero failures/cancellations/skips (3 selection sources x 6 competitor states x 2 peer schedules). The decision table independently expects 12 closed-and-published outcomes with chosen provenance and 24 open-and-published holds listing every candidate. The restored two publication regressions passed 2/2 with isolated HOME/Git configuration; the explicit safe-history hold control passed 1/1. Focused c8 passed 286/286 with exact 100/100/100/100 for closeout.ts. npm run typecheck, npm run build, npm run build:test, npm run lint, npm run docstring, npm run duplication, npm run privacy and changelog generation/check passed. Strict health with required merge drivers passed with isolated HOME; two existing advisory findings do not fail strict health. Source inventory, dependency pins, exclusions and thresholds remain unchanged. The item stays in progress for orchestrator verification, with the claim released before push. Remaining verification is read-only CI: npm run release:check and bun run release:check on Node 22/26, plus native Bun acceptance; exact final-head run URLs/counts will be retained in PR 174 and the handoff. No new PM CLI/SDK defect was encountered in this round." + "2026-10-24T14:31:43.103Z",codex-agent,"CI run https://github.com/unbraind/pm-github/actions/runs/40101464708 at 30b94f1: both Node 26 jobs passed all 1337 tests with zero test failures, then c8 exhausted its unchanged 8192 MiB heap during coverage aggregation. These are failed release gates, not approval. Reduce redundant matrix fixture initialization and unrelated repeated workflow steps while retaining all 36 actual closeout/publication-gate cases, public provenance assertions and the existing full workflow controls. Keep all source/coverage thresholds, heap resources and dependency pins unchanged; rerun focused proof and exact-head CI." + "2026-10-24T14:34:26.606Z",codex-agent,"All four first-head CI release jobs failed during aggregation at the unchanged 8 GiB heap; the existing broader follow-up pm-github-vhi6 now retains that receipt. Matrix harness optimization reuses four immutable input baselines and executes the unchanged workflow reset before each cell, with fresh PR receipts so an earlier publication cannot satisfy a later cell. Every cell still runs the real CLI, actual closeout refusal gate, publication privacy gates, leased Git push, and public body/provenance assertions. Existing full workflow tests continue to own changelog, strict-health, credential and dispatch controls. No runtime source or workflow gate is changed by this optimization." + "2026-10-24T14:38:10.086Z",codex-agent,"Optimized-harness revert proof repeated with independent fixture reset before each cell: only closeout.ts restored to 4874023 and rebuilt, retaining current matrix/workflow. The same two-test publication command failed 0/2 passed, exit 1, both ERR_ASSERTION errors at the expected workflow gates: incomplete merged-PR metadata and unavailable head evidence. Restoring the fix byte-for-byte and rebuilding leaves runtime source identical to 30b94f1. This additionally proves a failed earlier case cannot contaminate the next case's evidence. The optimized 36-cell matrix and restored controls are running through PM with isolated HOME/Git configuration; thresholds, heap resources and workflow code remain unchanged." + "2026-10-24T14:43:27.629Z",codex-agent,"Final optimized-harness local receipt: PM-linked 36-cell matrix passed 36/36 with zero failures/cancellations/skips and the same 12 closed-and-published / 24 open-and-published-hold expectations. The independent-reset fix-only revert fails both expected publication-gate assertions (0/2, exit 1); restored PM-linked controls pass 2/2 (exit 0). Both PM runs used isolated HOME and Git configuration. Test typecheck, lint, production typecheck and docstrings pass; privacy and changelog checks pass. Runtime source is byte-identical to 30b94f1, whose four CI test phases each passed 1337/1337 before aggregation failed. This follow-up changes only matrix setup and evidence/docs; scripts/coverage-gate.ts, package files, workflow code, source inventory, exclusions, four 100% thresholds and 8192 MiB heap remain unchanged. Release the claim before the follow-up push; retain final exact-head CI receipts in PR 174 and the handoff. Broader aggregation work remains open as pm-github-vhi6." + "2026-10-24T15:03:21.409Z",codex-agent,"Second exact-head CI https://github.com/unbraind/pm-github/actions/runs/40103921945 at ad2e7cb: completed npm Node 26 and Bun Node 22 test phases each pass 1337/1337, then c8 again exhausts the unchanged 8192 MiB heap. The gates remain failed. Four shared callers still repeated package setup for every cell; prepare each immutable baseline once and execute only the real branch preparation, closeout and publication steps per cell. Preserve independent resets, fresh PR receipts, all 36 cases and every outcome/provenance assertion. Keep runtime source, actual workflow code, coverage resources and thresholds unchanged." + "2026-10-24T15:10:02.206Z",codex-agent,"Prepared-baseline matrix local run passed 36/36, zero failures/cancellations/skips, in about 100 seconds. Setup now runs once for four immutable baseline callers; each case restores its input and prepared workflow-owned setup/environment, clears PR receipts, and executes the real Prepare sync branch, Close delivered tracker items and Commit gated closeout changes steps. The real publication privacy/refusal gates and leased push remain inside those unchanged steps. Repeating the runtime-only revert against the prepared harness fails 0/2 passed at the two expected workflow gates (exit 1); runtime was restored byte-for-byte and rebuilt. Final PM-linked matrix/restored controls are running with isolated HOME/Git configuration. Production source and gate configuration remain identical to 30b94f1." + "2026-10-24T15:12:48.722Z",codex-agent,"Final prepared-baseline PM receipt: 36/36 matrix tests pass with zero failures/cancellations/skips; all 12 closed-and-published and 24 open-and-published-hold expectations remain asserted. PM-linked restored publication controls pass 2/2; runtime-only revert fails 0/2 at the expected gates, exit 1. The matrix runs actual package/scope setup once per four immutable baselines and restores workflow-owned setup/environment plus independent tracker input before every actual branch-preparation, closeout and publication attempt. Fresh receipts prohibit stale publication from satisfying a case. Production source, coverage script, package files and workflows remain unchanged from 30b94f1; typecheck, test typecheck, lint and docstrings pass. Both earlier CI runs passed all 1337 tests in all four jobs but failed coverage aggregation, recorded on pm-github-vhi6. No gate resource, inventory, exclusion or threshold is relaxed. Release claim before push; final exact-head full-gate evidence remains read-only CI and will be retained in PR 174 and handoff." + "2026-10-24T15:42:24.111Z",codex-agent,"The prepared 36-cell matrix and runtime-only revert proof remain unchanged and passing. CI cff0f1c established the complete npm Node 22 gate at the old heap, while Node 26 again passed all 1337 tests and failed aggregation. To complete the requested full-gate evidence, only c8's merge heap ceiling is raised to 12288 MiB on the existing public 16 GB runners. There is no coverage threshold, source inventory, ignore list, dependency or workflow change. The real coverage-gate suite passes 9/9 via PM, including full/partial receipt and flag-routing controls; typecheck, test typecheck, lint, docstrings and zero duplication pass. Release both claims before push; final exact-head CI URL/counts will be retained in PR 174 and handoff. Broader bounded aggregation remains tracked by pm-github-vhi6 and neither item is closed." + "2026-10-24T17:16:39.286Z",codex-agent,"Round 4: correct candidacy to require an explicit delivery tag, a closing declaration, or a commit claim. Fully inspected mention/link-only PRs are context even when checks fail; unreadable claim evidence remains unresolved. Extend the existing publication decision table with primary and competitor claim kinds, retaining its 36 original cells and exact expected outcomes. Remote branch was current and already contains origin/main." + "2026-10-24T17:22:14.236Z",codex-agent,"Round-4 fix-only revert proof: restored only closeout.ts from pre-round head 4dd53552f825b3b42959ae96b9e61b27fe96e900; retained every new test and workflow change. Exact command: node --test --test-name-pattern='fully inspected context|fully inspected mentions' test/closeout-selection.test.ts. Reverted: 0/7 passed, 7 assertion failures, exit 1 (valid delivery held or context reported as candidates). Restored closeout.ts byte-for-byte; the same command via the PM runner passes 7/7, exit 0, zero failures/cancellations/skips. Tests import TypeScript source directly; no load failure is counted. The 36 earlier publication table rows pass unchanged; the 150 additional claim-kind rows are running. Isolated tracker health passes; inherited global CLI version skew is absent under isolated configuration." + "2026-10-24T17:25:37.405Z",codex-agent,"Local round-4 checks: typecheck, build, test typecheck, lint, docstrings (206 declarations), zero duplication and privacy passed. Isolated strict tracker health passed with two pre-existing advisory findings. The 186-row decision table expects 71 closed-and-published deliveries, 89 published holds and 26 context-only no-op runs. Every old table row remains. CLI and workflow summaries retain context as skip/not_delivery, without promoting context during selection or SDK refusal." + "2026-10-24T17:26:03.220Z",codex-agent,"PM-linked focused c8 command passed 293/293, zero failures/cancellations/skips, exact 100/100/100/100 for closeout.ts: node node_modules/c8/bin/c8.js --100 --include=closeout.ts --reporter=text --reporter=json --reports-dir=coverage/closeout-revision --temp-directory=coverage/closeout-revision-v8 node --test test/closeout.test.ts test/closeout-attribution.test.ts test/closeout-selection.test.ts test/closeout-revision.test.ts. Existing PM policy disables result storage; this comment preserves the receipt." + "2026-10-24T17:27:17.956Z",codex-agent,"Final round-4 revert proof adds the review's exact numbering: fully inspected context PR 2 with failing CI cannot block delivery PR 1. Replaced only closeout.ts with 4dd53552f825b3b42959ae96b9e61b27fe96e900, kept all tests/workflow changes, ran node --test --test-name-pattern='fully inspected context|fully inspected mentions' test/closeout-selection.test.ts: 0/8 passed, 8 ERR_ASSERTION failures, exit 1. Restored byte-for-byte; same command via pm test --run --only-last --progress passes 8/8, exit 0, zero failures/cancellations/skips. No load failures counted." + "2026-10-24T17:30:02.705Z",codex-agent,"Round-4 publication table passed 186/186, zero failures/cancellations/skips/TODOs: node --test --test-name-pattern='item decision end to end' test/closeout-workflow.test.ts. All 36 earlier rows retained, plus 150 rows crossing both five-kind claim dimensions and six evidence states. Outcomes: 71 closures, 89 holds, 26 context-only no-op runs. Exact row actions, codes, candidates, context, tracker/history bytes, chosen provenance and publication summaries all passed. Local release-gate evidence is focused only; full npm/Bun release gates will be taken from CI." + "2026-10-24T17:32:04.684Z",codex-agent,"Final focused suite including the exact PR-1-delivery/PR-2-mention reproduction passes 294/294 tests, exact 100/100/100/100 for closeout.ts, zero failures/cancellations/skips. Exact PM-linked c8 command remains linked on this item. Runtime/test head is 13d6ca69aef8f33e7c65df1b38bb5bbd165a0caf. Full npm/Bun CI release gates are running at https://github.com/unbraind/pm-github/actions/runs/40116410974; no full-gate success is claimed yet." + "2026-10-24T17:50:04.944Z",codex-agent,"Final table-input audit: retained the exact earlier ambiguous/no-competitor fixture (mention-only primary plus an unmatched delivery selector), instead of changing that input into a claim. Those two earlier rows must now be context under the corrected rule; 34 other earlier outcomes remain unchanged. Updated exact table totals: 71 closures, 87 holds, 27 context-only no-op publications and one context-only peer-edit publication (186 rows). The peer edit is independently publishable but cannot invent a delivery candidate or close event. Both initial Bun CI release gates passed; the next pushed test head will verify this fixture preservation." + "2026-10-24T17:51:48.612Z",codex-agent,"Correction to the preceding table-audit expectation: a peer edit alone is not independently published by closeout. The retained ambiguous/no-competitor rows now both pass via the PM runner (2/2, zero failures/cancellations/skips): node --test --test-name-pattern='item decision end to end: ambiguous / none' test/closeout-workflow.test.ts. Both report context and no candidate/close/review publication; the retag remains local while remote input bytes remain intact. Final exact outcomes are 71 closures, 87 holds, 28 context-only runs without publication (186 rows). All earlier inputs are retained. Test typecheck and lint pass. Initial CI Bun release gates passed on Node 22 and 26, each 1495/1495 tests and exact 100/100/100/100 across 19 authored modules; npm jobs remain in progress at https://github.com/unbraind/pm-github/actions/runs/40116410974." + "2026-10-24T18:08:40.552Z",codex-agent,"Final preserved-input decision table passed via pm test --run --only-index 8 --progress --timeout 1200: 186/186 tests, zero failures/cancellations/skips/TODOs, exit 0. Exact linked command: node --test --test-name-pattern='item decision end to end' test/closeout-workflow.test.ts. All 36 earlier inputs retained, including mention-only primary with unmatched selector; both such rows correctly stay context. Final outcomes: 71 closures, 87 holds, 28 context-only runs without publication. The peer-retag context row keeps the peer edit local and verifies unchanged remote input bytes. Core source remains byte-identical to 13d6ca69aef8f33e7c65df1b38bb5bbd165a0caf; pending changes preserve table inputs and validation receipts only." + "2026-10-24T18:12:17.873Z",codex-agent,"Initial CI release-gate receipts at code head 13d6ca69aef8f33e7c65df1b38bb5bbd165a0caf: https://github.com/unbraind/pm-github/actions/runs/40116410974. Complete npm release gate on Node 22 and complete Bun release gates on Node 22/26 passed, each 1495/1495 tests, zero failures/cancellations/skips, exact 100/100/100/100 across all 19 authored modules. Native Bun 1.3.14 acceptance in the npm/Node-22 job also passed 1495/1495 across 52 files, zero failures. All other stages in those complete gates passed. npm/Node-26 job remains running, so the whole initial run is not yet claimed green. Final preserved-input table is independently green 186/186 through PM; final pushed-head CI will be verified through the PR checks. No new PM CLI/SDK bug was discovered; existing result-storage policy remains unchanged." +files[12]{path,scope}: + .github/workflows/ci.yml,project + .github/workflows/pm-github-closeout.yml,project + closeout.ts,project + scripts/coverage-gate.ts,project + test/bot-pr-workflow.test.ts,project + test/changelog-placement.test.ts,project + test/closeout-attribution.test.ts,project + test/closeout-revision.test.ts,project + test/closeout-selection.test.ts,project + test/closeout-workflow.test.ts,project + test/closeout.test.ts,project + test/coverage-gate.test.ts,project +tests[13]: + - command: node --test test/closeout-selection.test.ts + scope: project + provenance: + author: codex-agent + created_at: "2026-10-24T06:21:00.205Z" + source_kind: local_mutation + source_ref: fix/closeout-delivery-attribution + - command: node --test test/closeout.test.ts test/closeout-attribution.test.ts test/closeout-selection.test.ts + scope: project + provenance: + author: codex-agent + created_at: "2026-10-24T06:25:36.399Z" + source_kind: local_mutation + source_ref: fix/closeout-delivery-attribution + - command: node --test test/closeout-workflow.test.ts + scope: project + provenance: + author: codex-agent + created_at: "2026-10-24T06:25:37.785Z" + source_kind: local_mutation + source_ref: fix/closeout-delivery-attribution + - command: node --test test/bot-pr-workflow.test.ts test/changelog-placement.test.ts + scope: project + provenance: + author: codex-agent + created_at: "2026-10-24T06:42:51.005Z" + source_kind: local_mutation + source_ref: fix/closeout-delivery-attribution + - command: "node --test --test-name-pattern=\"confirmed unmerged references\" test/closeout-workflow.test.ts" + scope: project + provenance: + author: codex-agent + created_at: "2026-10-24T10:23:40.236Z" + source_kind: local_mutation + source_ref: fix/closeout-delivery-attribution + - command: node --test test/closeout-revision.test.ts + scope: project + timeout_seconds: 300 + provenance: + author: codex-agent + created_at: "2026-10-24T12:08:26.122Z" + source_kind: local_mutation + source_ref: fix/closeout-delivery-attribution + - command: node node_modules/c8/bin/c8.js --100 --include=closeout.ts --reporter=text --reporter=json --reports-dir=coverage/closeout-revision --temp-directory=coverage/closeout-revision-v8 node --test test/closeout.test.ts test/closeout-attribution.test.ts test/closeout-selection.test.ts test/closeout-revision.test.ts + scope: project + timeout_seconds: 600 + provenance: + author: codex-agent + created_at: "2026-10-24T12:15:46.250Z" + source_kind: local_mutation + source_ref: fix/closeout-delivery-attribution + - command: "node --test --test-name-pattern=\"item decision end to end\" test/closeout-workflow.test.ts" + scope: project + timeout_seconds: 1200 + provenance: + author: codex-agent + created_at: "2026-10-24T13:57:55.580Z" + source_kind: local_mutation + source_ref: fix/closeout-delivery-attribution + - command: "node --test --test-name-pattern=\"item decision end to end: explicit delivery tag / (missing merge SHA|failed evidence fetch) / none\" test/closeout-workflow.test.ts" + scope: project + timeout_seconds: 300 + provenance: + author: codex-agent + created_at: "2026-10-24T13:58:34.975Z" + source_kind: local_mutation + source_ref: fix/closeout-delivery-attribution + - command: "node --test --test-name-pattern=\"item decision end to end: single post-criteria claim / valid / retag mid-run\" test/closeout-workflow.test.ts" + scope: project + timeout_seconds: 300 + provenance: + author: codex-agent + created_at: "2026-10-24T14:07:22.709Z" + source_kind: local_mutation + source_ref: fix/closeout-delivery-attribution + - command: node --test test/coverage-gate.test.ts + scope: project + timeout_seconds: 300 + provenance: + author: codex-agent + created_at: "2026-10-24T15:39:12.100Z" + source_kind: local_mutation + source_ref: fix/closeout-delivery-attribution + - command: "node --test --test-name-pattern=\"fully inspected context|fully inspected mentions\" test/closeout-selection.test.ts" + scope: project + timeout_seconds: 300 + provenance: + author: codex-agent + created_at: "2026-10-24T17:20:07.036Z" + source_kind: local_mutation + source_ref: fix/closeout-delivery-attribution + - command: "node --test --test-name-pattern=\"item decision end to end: ambiguous / none\" test/closeout-workflow.test.ts" + scope: project + timeout_seconds: 300 + provenance: + author: codex-agent + created_at: "2026-10-24T17:50:07.556Z" + source_kind: local_mutation + source_ref: fix/closeout-delivery-attribution +docs[2]{path,scope}: + docs/explicit-closeout-attribution.md,project + README.md,project +body: "" diff --git a/.agents/pm/tasks/pm-github-vhi6.toon b/.agents/pm/tasks/pm-github-vhi6.toon index 67a32d35..c2ca918f 100644 --- a/.agents/pm/tasks/pm-github-vhi6.toon +++ b/.agents/pm/tasks/pm-github-vhi6.toon @@ -2,15 +2,24 @@ id: pm-github-vhi6 title: Bound coverage aggregation memory and compare default-heap CI behavior description: "During the PR #122 privacy-scope regression work, an identity-isolated npm release:check completed 587/587 tests with zero failures, skips or cancellations, then c8 report aggregation exhausted Node's default 4 GiB heap. Native coverage receipts occupied approximately 2.9 GiB. The security fix is being validated with NODE_OPTIONS=--max-old-space-size=12288 and unchanged source inventory and four 100-percent thresholds. Compare default-heap CI behavior, investigate bounded report aggregation or documented resource requirements, and retain complete authored-source and real CLI coverage. Earlier local heap-exhaustion observations also appear in pm-github-fmrf; this task tracks current default-heap acceptance separately from the scope fix in pm-github-wrmt." type: Task -status: open +status: in_progress priority: 2 tags[3]: coverage,follow-up,gates created_at: "2026-10-14T14:32:34.672Z" -updated_at: "2026-10-14T15:04:27.090Z" +updated_at: "2026-10-24T15:42:24.889Z" author: codex-agent -comments[1]{created_at,author,text}: +comments[5]{created_at,author,text}: "2026-10-14T15:04:27.090Z",codex-agent,"Confirmed resource workaround: complete identity-isolated npm and Bun release:check each passed 587/587 tests and all 11 checks with NODE_OPTIONS=--max-old-space-size=12288. Coverage remains exact 100/100/100/100 across 12 authored modules, without source exemptions or threshold changes. The earlier default-heap npm run passed all 587 tests before c8 aggregation exhausted the heap. Default-heap CI comparison and bounded aggregation remain follow-up work; the security change does not alter coverage implementation or CI memory settings." -files[2]{path,scope}: + "2026-10-24T14:34:24.911Z",codex-agent,"Additional measured receipt from pm-github-closeout-attribution / PR 174: CI https://github.com/unbraind/pm-github/actions/runs/40101464708 at 30b94f1 failed all four npm/Bun release jobs on Node 22/26 with JavaScript heap exhaustion during c8 aggregation at the existing 8192 MiB limit. The Node 26 test phases each passed 1337/1337, zero failures/cancellations/skips before aggregation failed. No release-gate pass is claimed. This round reduces repeated setup in the new 36-case workflow matrix (shared immutable Git/tracker baselines and only the actual closeout/publication steps); the broader bounded aggregation task remains open, with thresholds, source inventory and heap configuration unchanged." + "2026-10-24T15:10:00.744Z",codex-agent,"Second receipt: https://github.com/unbraind/pm-github/actions/runs/40103921945 at ad2e7cb passed 1337/1337 tests in every npm/Bun Node 22/26 job, then all four c8 aggregations exhausted the unchanged 8192 MiB heap. Sharing tracker callers alone was insufficient while package setup still repeated per cell. The next harness prepares package/scope setup once per immutable baseline; every one of the 36 cases still restores input, runs actual branch preparation, real CLI closeout, publication privacy/refusal gates and leased push, with fresh receipts and provenance/history assertions. No coverage resource or threshold change is used. The broader bounded aggregation task remains open." + "2026-10-24T15:38:04.069Z",codex-agent,"Third CI https://github.com/unbraind/pm-github/actions/runs/40106191758 at cff0f1c passed the complete npm release gate on Node 22 after preparing four baseline callers once. Both Node 26 jobs still passed 1337/1337 tests and exhausted c8 at 8192 MiB. Fixture reductions alone cannot establish Node 26 full-gate acceptance. Adjust only the c8 merge heap ceiling to 12288 MiB, leaving headroom on this public repository's documented 16 GB ubuntu-latest runners (https://docs.github.com/en/actions/reference/runners/github-hosted-runners#standard-github-hosted-runners-for-public-repositories). All four coverage thresholds remain 100, the complete 19-module inventory, zero-exclusion policy, test set and dependency pins stay unchanged. Earlier comments preserving the 8 GiB ceiling describe failed earlier attempts; measured Node 26 failures now justify this resource-only revision. General bounded aggregation remains unfinished and this item will not be closed." + "2026-10-24T15:42:23.333Z",codex-agent,"Resource-only local verification: PM-linked node --test test/coverage-gate.test.ts passes 9/9, zero failures/cancellations/skips. These tests run real c8 fixture reports and preserve threshold-refusal, missing-source, invalid-receipt and pre-entry heap-routing controls. Production typecheck, test typecheck, lint, docstrings and zero duplication pass. Only COVERAGE_HEAP_FLAG changes from 8192 to 12288 MiB, plus explanatory comments. Full CI is still required; this is not bounded-memory certification and the general task stays in progress for orchestrator follow-up." +files[3]{path,scope}: scripts/coverage-gate.ts,project + test/closeout-workflow.test.ts,project test/import-gate.test.ts,project +tests[1]{command,scope,timeout_seconds,provenance{author,created_at,source_kind,source_ref}}: + node --test test/coverage-gate.test.ts,project,300,codex-agent,"2026-10-24T15:38:03.298Z",local_mutation,fix/closeout-delivery-attribution +docs[1]{path,scope}: + docs/explicit-closeout-attribution.md,project body: "" diff --git a/.github/workflows/pm-github-closeout.yml b/.github/workflows/pm-github-closeout.yml index dd9d7b4b..2dba3b77 100644 --- a/.github/workflows/pm-github-closeout.yml +++ b/.github/workflows/pm-github-closeout.yml @@ -78,6 +78,25 @@ jobs: CI_WORKFLOWS: ${{ inputs.ci-workflows }} CALLING_REPOSITORY: ${{ github.repository }} steps: + # Revoke a previous run's request before guards, installs, evidence reads or pushes. + - name: Disarm previous closeout auto-merge + env: + GH_TOKEN: ${{ github.token }} + shell: bash + run: | + set -euo pipefail + printf 'CLOSEOUT_AUTOMERGE_ALLOWED=false\n' >> "${GITHUB_ENV}" + # Use the caller identity and fixed branch, even when input validation fails. + # Filter same-repository heads; a fork using the same branch is never ours. + numbers="$(gh pr list -R "${CALLING_REPOSITORY}" --head pm-github/closeout --state open \ + --json number,headRepository,headRepositoryOwner,isCrossRepository,autoMergeRequest \ + | jq -r --arg repository "${CALLING_REPOSITORY}" \ + '.[] | select(.isCrossRepository == false and (.headRepositoryOwner.login + "/" + .headRepository.name) == $repository and .autoMergeRequest != null) | .number')" + while IFS= read -r number; do + [[ -n "${number}" ]] || continue + gh pr merge "${number}" -R "${CALLING_REPOSITORY}" --disable-auto + done <<< "${numbers}" + - name: Require caller default branch id: caller_branch env: @@ -372,6 +391,15 @@ jobs: echo "::error::Closeout refused; incomplete merged-PR metadata blocks a linked local tracker item. No branch was published." exit 1 fi + # Persist only safe attribution facts so an all-held run can still open a review PR. + review_file="${PM_SYNC_TRACKER}/closeout-review.json" + review_count="$(jq '[.plan[] | select(.action == "needs_review")] | length' "${RUNNER_TEMP}/pm-github-closeout.json")" + if [[ "${review_count}" != 0 ]]; then + jq '{needs_review: ([.plan[] | select(.action == "needs_review") | {item, action, candidates} + (if .missing_evidence then {missing_evidence} else {} end)] | unique_by(.item))}' "${RUNNER_TEMP}/pm-github-closeout.json" > "${review_file}" + elif [[ -f "${review_file}" ]]; then + rm -- "${review_file}" + fi + printf 'CLOSEOUT_REVIEW=%s\n' "${review_count}" >> "${GITHUB_ENV}" printf 'CLOSEOUT_CLOSED=%s\n' "$(jq -r '.closed' "${RUNNER_TEMP}/pm-github-closeout.json")" >> "${GITHUB_ENV}" - name: Run caller post-closeout script @@ -474,7 +502,7 @@ jobs: gh pr close "${number}" -R "${REPOSITORY}" --comment "No delivered tracker items remain to close; closing this review PR." fi } - if [[ "${CLOSEOUT_CLOSED}" == 0 ]] || git diff --cached --quiet -- "${sync_scope[0]}" "${sync_scope[@]:2}"; then + if [[ "${CLOSEOUT_CLOSED}" == 0 && "${CLOSEOUT_REVIEW:-0}" == 0 ]] || git diff --cached --quiet -- "${sync_scope[0]}" "${sync_scope[@]:2}"; then # Changelog drift alone belongs to the caller's changelog check; # publish regenerated output only alongside tracker changes. if ! git diff --cached --quiet -- CHANGELOG.md; then @@ -505,13 +533,16 @@ jobs: printf -- '- [%s](https://github.com/%s/blob/%s/%s) ([proposed](https://github.com/%s/blob/%s/%s))\n' "${item_id}" "${REPOSITORY}" "${DEFAULT_BRANCH}" "${changed}" "${REPOSITORY}" "${SYNC_BRANCH}" "${changed}" >> "${links_file}" fi done < <(git diff --cached --name-only -- "${sync_scope[0]}" "${sync_scope[@]:2}") + if [[ "${CLOSEOUT_REVIEW:-0}" != 0 ]]; then + printf -- '- [Delivery attribution needing review](https://github.com/%s/blob/%s/%s/closeout-review.json)\n' "${REPOSITORY}" "${SYNC_BRANCH}" "${PM_SYNC_TRACKER}" >> "${links_file}" + fi if [[ ! -s "${links_file}" ]]; then echo "::error::Closeout changed tracker state but no item link could be generated." exit 1 fi # Publish only actionable decisions and counts, with no API prose. summary_file="${RUNNER_TEMP}/pm-github-closeout-summary.json" - jq '{plan: [.plan[] | select(.code != "already_terminal" and .code != "other_repository" and .code != "duplicate_selection" and .code != "explicit_skip") | {item, pr, action: (if .action == "close" then "close" else "refuse" end), code}], skipped: ([.plan[] | select(.code == "already_terminal" or .code == "other_repository" or .code == "duplicate_selection" or .code == "explicit_skip")] | group_by(.code) | map({key: .[0].code, value: length}) | from_entries)}' "${RUNNER_TEMP}/pm-github-closeout.json" > "${summary_file}" + jq '{plan: [.plan[] | select(.code != "already_terminal" and .code != "other_repository" and .code != "duplicate_selection" and .code != "explicit_skip" and .code != "not_merged") | {item, pr, action: (if .action == "skip" and .code != "not_delivery" then "refuse" else .action end), code} + (if .candidates then {candidates} else {} end) + (if .missing_evidence then {missing_evidence} else {} end)], skipped: ([.plan[] | select(.code == "already_terminal" or .code == "other_repository" or .code == "duplicate_selection" or .code == "explicit_skip" or .code == "not_merged")] | group_by(.code) | map({key: .[0].code, value: length}) | from_entries)}' "${RUNNER_TEMP}/pm-github-closeout.json" > "${summary_file}" # Scan the public summary as added diff content before any publication. summary_patch="${RUNNER_TEMP}/pm-github-closeout-summary.patch" { @@ -532,7 +563,10 @@ jobs: body_file="${RUNNER_TEMP}/pm-github-sync-pr.md" { printf '%s\n\n' "Automated pm-github merged-PR closeout for ${REPOSITORY}." - printf '%s\n\n' "The closeout command verified merged-PR delivery and head checks. The proposed tracker change and regenerated changelog passed the fail-closed privacy gate before anything was committed. \`${DEFAULT_BRANCH}\` is never pushed directly." + printf '%s\n\n' "The closeout command verified delivery and head checks for closed items; needs_review items remain open with every candidate listed below. The proposed tracker change and regenerated changelog passed the fail-closed privacy gate before anything was committed. \`${DEFAULT_BRANCH}\` is never pushed directly." + if [[ "${CLOSEOUT_REVIEW:-0}" != 0 ]]; then + printf '%s\n\n' "Automatic merge is disabled because delivery attribution needs_review; every candidate and evidence gap requires human review before landing. Any previous auto-merge request was explicitly disabled before this run." + fi printf '%s\n\n' "Tracker items changed:" cat "${links_file}" printf '\n%s\n\n%s\n' 'Closeout plan (routine skips counted by reason):' '```json' @@ -624,7 +658,14 @@ jobs: } head_sha="$(git rev-parse HEAD)" dispatch_checks + if [[ "${CLOSEOUT_REVIEW:-0}" != 0 ]]; then + echo "::notice::Delivery attribution needs review; caller checks were dispatched and automatic landing is disabled." + exit 0 + fi enable_auto_merge + if [[ -n "${APP_DEADLINE:-}" ]]; then + printf 'CLOSEOUT_AUTOMERGE_ALLOWED=true\n' >> "${GITHUB_ENV}" + fi parked_runs="$(gh run list -R "${REPOSITORY}" --commit "${head_sha}" --status action_required --limit 100 --json url --jq '.[].url')" while IFS= read -r run_url; do [[ -n "${run_url}" ]] || continue @@ -681,3 +722,23 @@ jobs: sleep 60 done echo "::notice::Bot PR maintenance reached its configured bound; auto-merge remains enabled when allowed. The next scheduled run rebuilds from the current default branch. Unresolved conversations still require resolution." + + # Also revoke on dispatch/maintenance failure, cancellation, disabled CI, + # no publication, or a hold. This runs even when an earlier step failed. + - name: Disarm closeout auto-merge on unsuccessful landing + if: ${{ always() && (failure() || cancelled() || env.CLOSEOUT_AUTOMERGE_ALLOWED != 'true') }} + env: + GH_TOKEN: ${{ steps.automation_token.outputs.token || github.token }} + shell: bash + run: | + set -euo pipefail + # Use the caller identity and fixed branch, even when input validation fails. + # Filter same-repository heads; a fork using the same branch is never ours. + numbers="$(gh pr list -R "${CALLING_REPOSITORY}" --head pm-github/closeout --state open \ + --json number,headRepository,headRepositoryOwner,isCrossRepository,autoMergeRequest \ + | jq -r --arg repository "${CALLING_REPOSITORY}" \ + '.[] | select(.isCrossRepository == false and (.headRepositoryOwner.login + "/" + .headRepository.name) == $repository and .autoMergeRequest != null) | .number')" + while IFS= read -r number; do + [[ -n "${number}" ]] || continue + gh pr merge "${number}" -R "${CALLING_REPOSITORY}" --disable-auto + done <<< "${numbers}" diff --git a/CHANGELOG.md b/CHANGELOG.md index 29a7768f..6b220722 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Fixed + +- Hold ambiguous merged-PR delivery until attribution is verified ([pm-github-closeout-attribution](https://github.com/unbraind/pm-github/blob/main/.agents/pm/issues/pm-github-closeout-attribution.toon)) + ## 2026.10.24 - 2026-10-24 ### Fixed diff --git a/README.md b/README.md index 9c13fc3a..cf08c560 100644 --- a/README.md +++ b/README.md @@ -48,17 +48,41 @@ PR body, including links in follow-up lists (`PM:`), can produce `not_delivery` rows for otherwise eligible items with the reason "Mentioned, not delivered". Tracker PR URL metadata with no body reference or declaration produces no row. -When several merged PRs declare delivery, closeout selects the latest merge with -complete merge facts and passing head checks. Equal merge times use the higher -PR number. Other qualifying claims receive `duplicate_selection`; mention-only -PRs retain `not_delivery`. All completion fields and the appended close event use -the selected PR's merge time, merge SHA, head SHA and check evidence. +A merged PR is a delivery candidate only when it claims the item through an explicit +`delivery-pr:` tag, a closing declaration, or a commit claim naming the item. +A fully inspected mention-only or link-only PR is context: it remains listed as +`skip` / `not_delivery`, never enters the candidate set, and its merge/check gaps +cannot hold another delivery. Unreadable PR or commit evidence remains unresolved +because it may hide a claim; a known candidate's incomplete evidence still holds. + +Closeout never breaks a delivery tie by PR number or merge order. Prefer one explicit +item tag linking the delivery PR, for example: + +```sh +pm update pm-x --add-tags delivery-pr:https://github.com/example/project/pull/19 +``` + +The tagged PR must be in the selected PR set and pass the merge and head-check gates. +Ordinary `pr_url` metadata remains contextual. Without an explicit tag, exactly one +linked merged PR must declare the item in its description or commit messages and +merge strictly after the last acceptance-criteria edit in the item's SDK history. +Creation is the cutoff when no later criteria edit exists; comments and unrelated +updates do not move it. Missing or unverifiable history cannot establish chronology. +Unreadable commits on an otherwise qualifying mention also hold selection: they may hide another claim. A failed check on the unique PR never permits fallback to an earlier delivery. + +Unresolved attribution produces `needs_review` rows listing every linked candidate +PR number, leaving the item and its history unchanged. Multiple or unmatched explicit +tags also require review. The reusable workflow includes these rows and candidates +in the closeout PR body. An all-held run proposes only the privacy-gated +`closeout-review.json` report in the tracker, so it can still open a review PR. +All completion fields and the appended close event use the selected PR's merge +time, merge SHA, head SHA and check evidence. **A mention is not delivery.** PR bodies routinely link follow-ups, epics and related work for context, so a link alone never closes an item. An item is closed only when: -- its delivery is explicitly declared; a canonical link may be preceded on the same +- its delivery is selected by an explicit item tag or a unique claim after the last criteria edit; a canonical link may be preceded on the same line by a GitHub-style closing keyword: `Closes`, `Fixes`, `Resolves` or `Delivers` (and their tense forms), for example `- Closes [pm-x](…/pm-x.toon)`; - it is not an `Epic`; epics close when their children are done; diff --git a/closeout.ts b/closeout.ts index 625f7476..1514a5ca 100644 --- a/closeout.ts +++ b/closeout.ts @@ -1,5 +1,5 @@ /** Close delivered tracker items using merged pull requests and head-check evidence. */ -import { close, listAllComplete, certifyCompleteListResult, PmCliError } from "@unbrained/pm-cli/sdk"; +import { close, hashDocument, locateItem, readLocatedItem, readSettings, resolveItemTypeRegistry, runWithActiveExtensions, setActiveExtensionHooks, listAllComplete, certifyCompleteListResult, getHistoryPath, readHistoryEntries, verifyHistoryEntries, PmCliError, EXIT_CODE, type ItemDocument } from "@unbrained/pm-cli/sdk"; /** Existing GitHub REST transport, supplied by the extension entry point. */ export interface CloseoutApi { @@ -12,15 +12,17 @@ export interface CloseoutApi { export interface CloseoutRow { item: string; pr: number; - action: "close" | "skip"; + action: "close" | "skip" | "needs_review"; + candidates?: number[]; + missing_evidence?: { pr: number; code: CloseoutCode; reason: string }[]; reason: string; code: CloseoutCode; } /** Stable decisions used by automation and compact skip counts. */ export type CloseoutCode = "already_terminal" | "other_repository" | "duplicate_selection" | "explicit_skip" - | "unknown_item" | "epic" | "active_children" | "not_delivery" | "close_failed" - | "head_checks_passed" | "head_checks_failed" | "head_evidence_unavailable" | "required_checks_unverified" | "incomplete_merge"; + | "needs_review" | "unknown_item" | "epic" | "active_children" | "not_delivery" | "close_failed" + | "head_checks_passed" | "head_checks_failed" | "head_evidence_unavailable" | "required_checks_unverified" | "not_merged" | "incomplete_merge"; /** Which head-check policy was evaluated for one selected pull request. */ interface CheckGate { @@ -36,9 +38,10 @@ interface CheckGate { interface PullRequest { number: number; body: string | null; - merged: boolean; + merged: boolean | null; merged_at: string | null; merge_commit_sha: string | null; + commits?: number | null; html_url: string; head: { sha: string }; base: { ref: string; repo: { full_name: string } }; @@ -131,23 +134,38 @@ function pullRequest(value: unknown, repo: string, number: number): PullRequest const raw = record(value); const base = record(raw.base); const baseRepo = record(base.repo); - const head = record(raw.head); + const head = raw.head === null || raw.head === undefined ? {} : record(raw.head); if (raw.number !== number || text(baseRepo.full_name).toLowerCase() !== repo.toLowerCase() || text(raw.html_url).toLowerCase() !== `https://github.com/${repo}/pull/${number}`.toLowerCase() || (raw.body !== null && typeof raw.body !== "string")) throw new Error("Pull-request identity or body is unverifiable."); return { - number, body: raw.body as string | null, merged: raw.merged === true, + number, body: raw.body as string | null, merged: typeof raw.merged === "boolean" ? raw.merged : null, + commits: Number.isSafeInteger(raw.commits) && Number(raw.commits) >= 0 ? Number(raw.commits) : null, merged_at: raw.merged_at as string | null, merge_commit_sha: raw.merge_commit_sha as string | null, - html_url: raw.html_url as string, head: { sha: text(head.sha) }, + html_url: raw.html_url as string, head: { sha: typeof head.sha === "string" ? head.sha : "" }, base: { ref: text(base.ref), repo: { full_name: baseRepo.full_name as string } }, }; } +/** Read a validated merge time; missing or malformed chronology remains unknown. */ +function mergeTime(value: unknown): number { + try { return Date.parse(isoDate(text(value))); } + catch { return Number.NaN; } +} + +/** Name each missing merge fact without copying untrusted API values. */ +function mergeEvidenceGaps(pr: PullRequest): string[] { + return [ + ...(pr.merged === true ? [] : ["merged"]), + ...(Number.isFinite(mergeTime(pr.merged_at)) ? [] : ["merged_at"]), + ...(typeof pr.merge_commit_sha === "string" && /^[a-f0-9]{40}$/.test(pr.merge_commit_sha) ? [] : ["merge_commit_sha"]), + ...(/^[a-f0-9]{40}$/.test(pr.head.sha) ? [] : ["head.sha"]), + ]; +} + /** Require complete, well-formed merge facts before any check queries or writes. */ function mergeEvidence(pr: PullRequest): boolean { - return pr.merged && typeof pr.merged_at === "string" && isoDate(pr.merged_at).length > 0 - && typeof pr.merge_commit_sha === "string" && /^[a-f0-9]{40}$/.test(pr.merge_commit_sha) - && /^[a-f0-9]{40}$/.test(pr.head.sha); + return mergeEvidenceGaps(pr).length === 0; } /** Treat only permission and not-found responses as protection metadata gaps. */ @@ -335,6 +353,48 @@ export function extractCloseoutItems(body: string): { item: string; repo: string }).map(({ repo, item, declared }) => ({ repo, item, declared })); } +/** Read the last criteria edit from immutable SDK history; unavailable chronology cannot select delivery. */ +async function acceptanceChangedAt(pmRoot: string, id: string, document: ItemDocument): Promise { + try { + const history = await readHistoryEntries(getHistoryPath(pmRoot, id), id); + if (history.length === 0 || !verifyHistoryEntries(history, document).ok) return Number.NaN; + let changedAt = Date.parse(document.metadata.created_at); + for (const event of history) { + if (event.patch.some(patch => ["", "/metadata", "/metadata/acceptance_criteria"].includes(patch.path))) { + changedAt = Math.max(changedAt, Date.parse(event.ts)); + } + } + return changedAt; + } catch { return Number.NaN; } +} + +/** Extract exact item declarations from descriptions or commit messages using the same grammar. */ +function deliveryReferences(body: string, repo: string, ids: Iterable) { + const refs = extractCloseoutItems(body); + const declarations = new Set(); + for (const match of body.matchAll(/^(?:[-*][ \t]+)?([a-z]+|pm-github:|PM item:)[ \t]+(\S+)[ \t]*$/gim)) { + const keyword = match[1]!.toLowerCase(); + if (!CLOSING_KEYWORDS.has(keyword) && keyword !== "pm-github:" && keyword !== "pm item:") continue; + const id = declarationKey(match[2]!); + declarations.add(id); + if (/[.!]$/.test(id)) declarations.add(id.slice(0, -1)); + } + for (const id of ids) { + if (declarations.has(declarationKey(id))) { + const ref = refs.find(ref => ref.item === id && ref.repo.toLowerCase() === repo.toLowerCase()); + if (ref) ref.declared = true; + else refs.push({ item: id, repo, declared: true }); + } + } + return refs; +} + +/** Classify delivery once: only claims are candidates; unreadable claims remain unresolved. */ +function deliveryRole(declared: boolean, tagged: boolean, commitsReadable: boolean): "candidate" | "unresolved" | "context" { + if (declared || tagged) return "candidate"; + return commitsReadable ? "context" : "unresolved"; +} + /** Render only the allowed GitHub facts; never include body, title or comment text. */ function closeReason(pr: PullRequest, passed: string[]): string { return JSON.stringify({ pr: pr.number, url: pr.html_url, merge_commit_sha: pr.merge_commit_sha, @@ -375,8 +435,37 @@ export function checkEvidence(gate: CheckGate): string { return `no readable required checks, so every completed head check run had to succeed, be neutral or be skipped; ${successes}`; } -/** Plan all selected PRs before closing eligible items through the real SDK. */ -export async function runCloseout(pmRoot: string, repo: string | undefined, options: Record, api: CloseoutApi) { +/** The final item outcome controls reporting; superseded PR evidence is informational. */ +type ItemDecision = { kind: "chosen"; row: CloseoutRow } + | { kind: "blocking_hold"; reason: string; missing: NonNullable }; + +/** Project one item decision onto all its candidates, including incomplete competitors. */ +function reportItemDecision(rows: CloseoutRow[], decision: ItemDecision): void { + const candidates = rows.map(row => row.pr).sort((a, b) => a - b); + for (const row of rows) { + if (decision.kind === "chosen") { + if (row === decision.row) continue; + Object.assign(row, { action: "skip", code: "duplicate_selection", + reason: "Another merged PR uniquely supplies the delivery evidence for this item." }); + } else { + Object.assign(row, { action: "needs_review", code: "needs_review", reason: decision.reason, candidates }); + if (decision.missing.length > 0) row.missing_evidence = decision.missing; + } + } +} + +/** Deterministic scheduling observer; it never replaces SDK reads or writes. */ +export type CloseoutObserver = (boundary: "listed" | "snapshot" | "history" | "selection" | "before_close" | "locked") => Promise; + +/** Plan selected PRs and apply only decisions whose exact item snapshot still matches. */ +export function runCloseout(pmRoot: string, repo: string | undefined, options: Record, api: CloseoutApi, observer?: CloseoutObserver) { + return closeoutAttempt(pmRoot, repo, options, api, true, observer); +} + +/** Re-select a conflicted item once; each attempt installs a lock-scoped SDK precondition. */ +async function closeoutAttempt(pmRoot: string, repo: string | undefined, options: Record, api: CloseoutApi, retry: boolean, observer?: CloseoutObserver, onlyItem?: string): Promise<{ + command: string; repo: string; dry_run: boolean; plan: CloseoutRow[]; checks: CheckGate[]; closed: number; failed: number; +}> { if (!repo || !/^(?!\.{1,2}\/)[A-Za-z0-9_.-]+\/(?!\.{1,2}$)[A-Za-z0-9_.-]+$/.test(repo)) throw new CloseoutUsageError("closeout requires --repo owner/repo or PM_GITHUB_REPO."); const numbers = usage(() => repeated(options, "pr")).map(value => { if (!/^[1-9]\d*$/.test(value) || !Number.isSafeInteger(Number(value))) throw new CloseoutUsageError("--pr requires positive pull-request numbers."); @@ -394,60 +483,89 @@ export async function runCloseout(pmRoot: string, repo: string | undefined, opti const older = (row: unknown) => typeof row === "object" && row !== null && Date.parse(String((row as { updated_at?: unknown }).updated_at)) < cutoff; for (const value of await pages(api, `/repos/${repo}/pulls?state=closed&sort=updated&direction=desc&per_page=100`, false, older)) { const pr = record(value); - if (pr.merged_at !== null && Date.parse(isoDate(text(pr.merged_at))) >= Date.parse(since)) { + // A missing or malformed merge time is uncertainty, never proof of an unmerged PR. + if (!Number.isFinite(mergeTime(pr.merged_at)) || mergeTime(pr.merged_at) >= cutoff) { if (!Number.isSafeInteger(pr.number) || Number(pr.number) < 1) throw new Error("Invalid selected pull-request number."); numbers.push(Number(pr.number)); } } } const corpus = certifyCompleteListResult(await listAllComplete({}, { pmRoot, noExtensions: true })); + await observer?.("listed"); + const settings = await readSettings(pmRoot); + const types = resolveItemTypeRegistry(settings).type_to_folder; + const snapshots = new Map(); + const documents = new Map(); const items = new Map(corpus.items.map(item => [item.id, item])); + // Selection and its guard must use the same authoritative document, including collections and body. + for (const id of items.keys()) { + const located = await locateItem(pmRoot, id, settings.id_prefix, settings.item_format, types); + if (!located) throw new Error("Closeout item snapshot unavailable."); + const { document } = await readLocatedItem(located, { schema: settings.schema }); + snapshots.set(id, hashDocument(document)); + documents.set(id, document); + items.set(id, document.metadata); + } + await observer?.("snapshot"); // Containers close when their children are done, never because a pull request mentions them. const activeChildren = new Map(); - for (const item of corpus.items) { + for (const item of items.values()) { const parent = (item as { parent?: unknown }).parent; if (typeof parent === "string" && !TERMINAL_STATUSES.includes(item.status)) activeChildren.set(parent, (activeChildren.get(parent) ?? 0) + 1); } const plan: CloseoutRow[] = []; const checks: CheckGate[] = []; const closures: { row: CloseoutRow; reason: string; at: string; resolution: string; expectedResult: string; actualResult: string }[] = []; + const candidates = new Map(); + const unreadable: number[] = []; for (const number of [...new Set(numbers)].sort((a, b) => a - b)) { - const pr = pullRequest(await payload(api, `/repos/${repo}/pulls/${number}`), repo, number); - let gate: CheckGate = { pr: number, code: "incomplete_merge", rule: "unavailable", ok: false, passed: [], reason: "Pull request is not merged or merge evidence is incomplete." }; + let evidence: unknown; + try { evidence = await payload(api, `/repos/${repo}/pulls/${number}`); } + catch { unreadable.push(number); continue; } + const pr = pullRequest(evidence, repo, number); + let gate: CheckGate = { pr: number, code: "incomplete_merge", rule: "unavailable", ok: false, passed: [], reason: `Missing or invalid merge evidence: ${mergeEvidenceGaps(pr).join(", ")}.` }; if (mergeEvidence(pr)) gate = await checkCloseoutHead(api, repo, pr, declared); checks.push(gate); - const refs = extractCloseoutItems(pr.body ?? ""); - // Scan declarations once per PR, retaining case-insensitive IDs and optional punctuation. - const declarations = new Set(); - for (const match of (pr.body ?? "").matchAll(/^(?:[-*][ \t]+)?([a-z]+|pm-github:|PM item:)[ \t]+(\S+)[ \t]*$/gim)) { - const keyword = match[1]!.toLowerCase(); - if (!CLOSING_KEYWORDS.has(keyword) && keyword !== "pm-github:" && keyword !== "pm item:") continue; - const id = declarationKey(match[2]!); - declarations.add(id); - if (/[.!]$/.test(id)) declarations.add(id.slice(0, -1)); + const refs = deliveryReferences(pr.body ?? "", repo, items.keys()); + // Commit messages can independently claim delivery. Missing commit evidence never upgrades a mention. + let messages: string[] = []; + let commitsReadable = false; + try { + if (pr.merged !== false) { + const commits = await pages(api, `/repos/${repo}/pulls/${number}/commits?per_page=100`); + if (pr.commits === null || commits.length !== pr.commits) throw new Error("Incomplete commit evidence."); + messages = commits.map(value => text(record(record(value).commit).message)); + commitsReadable = true; + } + } catch { /* Retain description references; unresolved commit evidence is held during attribution. */ } + for (const ref of deliveryReferences(messages.join("\n"), repo, items.keys())) { + const existing = refs.find(entry => entry.item === ref.item && entry.repo.toLowerCase() === ref.repo.toLowerCase()); + if (existing) existing.declared ||= ref.declared; + else refs.push(ref); } + // An unreadable commit set cannot prove that an item has no commit-only delivery link. for (const item of items.values()) { - // Delivery must be declared in this PR body, never inferred from tracker metadata. - if (declarations.has(declarationKey(item.id))) { - const ref = refs.find(ref => ref.item === item.id && ref.repo.toLowerCase() === repo.toLowerCase()); - if (ref) ref.declared = true; - else refs.push({ item: item.id, repo, declared: true }); + if ((item.tags.includes(`delivery-pr:${pr.html_url}`) || (!commitsReadable && pr.merged !== false)) && !refs.some(ref => ref.item === item.id && ref.repo.toLowerCase() === repo.toLowerCase())) { + refs.push({ item: item.id, repo, declared: false }); } } for (const ref of refs) { + const role = deliveryRole(ref.declared, items.get(ref.item)?.tags.includes(`delivery-pr:${pr.html_url}`) === true, commitsReadable); let reason: string; let code: CloseoutCode; if (ref.repo.toLowerCase() !== repo.toLowerCase()) { code = "other_repository"; reason = "Link points to another repository."; } else if (!items.has(ref.item)) { code = "unknown_item"; reason = "Item does not exist in the tracker."; } else if (skip.has(ref.item)) { code = "explicit_skip"; reason = "Explicitly excluded by --skip."; } else if (TERMINAL_STATUSES.includes(items.get(ref.item)!.status)) { code = "already_terminal"; reason = "Item is already closed or canceled."; } - else if (!gate.ok) { code = gate.code; reason = gate.reason; } + else if (pr.merged === false) { code = "not_merged"; reason = "Pull request is confirmed unmerged and cannot deliver this item."; } else if (items.get(ref.item)!.type === "Epic") { code = "epic"; reason = "Epics close when their children are done, not by a pull request."; } else if (activeChildren.has(ref.item)) { code = "active_children"; reason = `Item has ${activeChildren.get(ref.item)} non-terminal child item(s).`; } - else if (!ref.declared) { + else if (role === "context") { code = "not_delivery"; - reason = "Mentioned, not delivered. A mention is not delivery. Declare Closes, Fixes, Resolves or Delivers, or use a structured PM item: or pm-github: declaration."; + reason = "Mentioned, not delivered. A mention is not delivery. This fully inspected PR has no delivery claim and is context only."; } + else if (!gate.ok) { code = gate.code; reason = gate.reason; } + else if (role === "unresolved") { code = "head_evidence_unavailable"; reason = "Commit delivery evidence unavailable or incomplete."; } else { reason = gate.reason; const row: CloseoutRow = { item: ref.item, pr: number, action: "close", code: gate.code, reason }; @@ -459,57 +577,145 @@ export async function runCloseout(pmRoot: string, repo: string | undefined, opti actualResult: `Merged at ${pr.merged_at!}; ${checkEvidence(gate)}`, }); plan.push(row); + const group = candidates.get(ref.item) ?? []; + group.push({ row, pr, declared: ref.declared, commitsReadable }); + candidates.set(ref.item, group); continue; } - plan.push({ item: ref.item, pr: number, action: "skip", code, reason }); + const row: CloseoutRow = { item: ref.item, pr: number, action: "skip", code, reason }; + plan.push(row); + if (role !== "context" && pr.merged !== false && items.has(ref.item) && ref.repo.toLowerCase() === repo.toLowerCase() + && !skip.has(ref.item) && !TERMINAL_STATUSES.includes(items.get(ref.item)!.status) + && items.get(ref.item)!.type !== "Epic" && !activeChildren.has(ref.item)) { + const group = candidates.get(ref.item) ?? []; + group.push({ row, pr, declared: ref.declared, commitsReadable }); + candidates.set(ref.item, group); + } } } - // Choose one complete, passing delivery before any SDK write. Merge chronology, not PR number, - // determines the winner; an equal timestamp uses the higher PR number for stable selection. - const deliveries = new Map(); - for (const entry of closures) { - const previous = deliveries.get(entry.row.item); - if (!previous || Date.parse(entry.at) >= Date.parse(previous.at)) { - deliveries.set(entry.row.item, entry); + // A failed PR read can hide linkage and a competing claim. Retain its number for + // every active eligible item; without the PR evidence no item can be proved unrelated. + for (const number of unreadable) { + const url = `https://github.com/${repo}/pull/${number}`; + for (const item of items.values()) { + if (skip.has(item.id) || TERMINAL_STATUSES.includes(item.status) || item.type === "Epic" || activeChildren.has(item.id)) continue; + const row: CloseoutRow = { item: item.id, pr: number, action: "skip", code: "head_evidence_unavailable", reason: "Pull-request fetch failed; merge, linkage and delivery evidence unavailable." }; + plan.push(row); + const group = candidates.get(item.id) ?? []; + group.push({ row, pr: { number, html_url: url, body: null, merged: true, merged_at: null, merge_commit_sha: null, commits: null, + head: { sha: "" }, base: { ref: "", repo: { full_name: repo } } }, declared: false, commitsReadable: false }); + candidates.set(item.id, group); } } - for (const entry of closures) { - if (deliveries.get(entry.row.item) !== entry) { - entry.row.action = "skip"; - entry.row.code = "duplicate_selection"; - entry.row.reason = "A later qualifying merged PR declares delivery for this item."; + // Resolve attribution before any SDK mutation; passing checks never break a delivery tie. + for (const [id, group] of candidates) { + const item = items.get(id)!; + const explicit = item.tags.filter(tag => tag.startsWith("delivery-pr:")); + const cutoff = await acceptanceChangedAt(pmRoot, id, documents.get(id)!); + await observer?.("history"); + const plausible = group.filter(entry => !Number.isFinite(mergeTime(entry.pr.merged_at)) || mergeTime(entry.pr.merged_at) > cutoff); + const eligible = explicit.length > 0 + ? group.filter(entry => explicit.length === 1 && explicit[0] === `delivery-pr:${entry.pr.html_url}`) + : plausible.filter(entry => entry.declared); + const uncertain = explicit.length === 1 && eligible.length === 1 ? eligible : plausible; + const missing = uncertain.flatMap(entry => { + const gaps = ["incomplete_merge", "head_evidence_unavailable", "required_checks_unverified", "head_checks_failed"].includes(entry.row.code) + ? [{ pr: entry.pr.number, code: entry.row.code, reason: entry.row.reason }] : []; + if (!entry.commitsReadable) gaps.push({ pr: entry.pr.number, code: "head_evidence_unavailable", reason: "Commit delivery evidence unavailable or incomplete." }); + return gaps; + }); + // One item decision owns every candidate status. Evidence codes describe PR facts, + // but an unchosen PR cannot remain a fatal refusal after another PR wins. + const chosen = eligible.length === 1 && missing.length === 0 ? eligible[0]!.row : undefined; + if (chosen) reportItemDecision(group.map(entry => entry.row), { kind: "chosen", row: chosen }); + else if (group.length > 1 || explicit.length > 0 || missing.length > 0 || group[0]!.row.code !== "not_delivery") { + reportItemDecision(group.map(entry => entry.row), { kind: "blocking_hold", + reason: "Delivery attribution or chosen PR evidence is unresolved; set one delivery-pr: tag or verify a unique delivery claim after the last acceptance-criteria change." + + missing.map(gap => ` PR #${gap.pr}: ${gap.reason}`).join(""), missing }); } } + await observer?.("selection"); let closed = 0; let failed = 0; + const handled = new Set(); + const dependentConflicts = new Map(); + /** Replace stale decisions with one fresh selection, retaining a reason when linkage disappeared. */ + async function reselect(id: string, pr: number): Promise { + handled.add(id); + const fresh = await closeoutAttempt(pmRoot, repo, options, api, false, observer, id); + checks.splice(0, checks.length, ...fresh.checks); + const replacement = fresh.plan; + if (!replacement.some(row => row.item === id)) replacement.push({ item: id, pr, action: "needs_review", code: "needs_review", + candidates: [pr], reason: "Item revision changed; fresh selection found no delivery evidence. Hold for review." }); + const replaced = new Set(replacement.map(row => row.item)); + const retained = plan.filter(row => !replaced.has(row.item)); + plan.splice(0, plan.length, ...retained, ...replacement); + closed += fresh.closed; + failed += fresh.failed; + } if (apply) { for (const entry of closures) { - if (entry.row.action !== "close") continue; + if (entry.row.action !== "close" || (onlyItem !== undefined && entry.row.item !== onlyItem)) continue; try { - await close(entry.row.item, entry.reason, { author: "pm-github", completedAt: entry.at, - resolution: entry.resolution, expectedResult: entry.expectedResult, actualResult: entry.actualResult, - }, { pmRoot, noExtensions: true }); + await observer?.("before_close"); + // The pinned SDK has no public close(expectedRevision). Its transactional hook runs + // inside the same per-item lock as lifecycle validation, persistence and history. + await runWithActiveExtensions({ path: pmRoot, noExtensions: true }, async () => { + setActiveExtensionHooks({ beforeCommand: [], afterCommand: [], onRead: [], onWrite: [], onIndex: [], + beforeMutation: [{ layer: "project", name: "closeout-revision", run: async context => { + await observer?.("locked"); + // SDK close may also auto-unblock dependents. Every resulting write needs its own snapshot. + const before = context.before!; + const id = before.metadata.id; + if (hashDocument(before) !== snapshots.get(id)) { + if (id !== entry.row.item) dependentConflicts.set(id, entry.row.pr); + throw new PmCliError("Closeout item revision changed.", 1, { code: "closeout_revision_changed" }); + } + } }], + }); + await close(entry.row.item, entry.reason, { author: "pm-github", completedAt: entry.at, + resolution: entry.resolution, expectedResult: entry.expectedResult, actualResult: entry.actualResult, + }, { pmRoot, noExtensions: true }); + }); closed++; + handled.add(entry.row.item); } catch (error: unknown) { + if (error instanceof PmCliError && (error.code === "closeout_revision_changed" + || (error.code === undefined && error.exitCode === EXIT_CODE.CONFLICT))) { + if (retry) { + await reselect(entry.row.item, entry.row.pr); + continue; + } + reportItemDecision(plan.filter(row => row.item === entry.row.item && row.code !== "not_delivery"), { kind: "blocking_hold", + reason: "Item revision changed again after one delivery re-selection; hold for review.", missing: [] }); + continue; + } failed++; - entry.row.action = "skip"; - entry.row.code = "close_failed"; - entry.row.reason = closeoutFailureReason(error); + const reason = closeoutFailureReason(error); + reportItemDecision(plan.filter(row => row.item === entry.row.item && row.code !== "not_delivery"), { kind: "blocking_hold", reason, + missing: [{ pr: entry.row.pr, code: "close_failed", reason }] }); } } + for (const [id, pr] of dependentConflicts) { + if (handled.has(id)) continue; + if (retry) await reselect(id, pr); + else plan.push({ item: id, pr, action: "needs_review", code: "needs_review", candidates: [pr], + reason: "Dependent item revision changed during re-selection; automatic unblock held for review." }); + } } - return { command: "github closeout", repo, dry_run: !apply, plan, checks, closed, failed }; + return { command: "github closeout", repo, dry_run: !apply, + plan: onlyItem === undefined ? plan : plan.filter(row => row.item === onlyItem || dependentConflicts.has(row.item)), checks, closed, failed }; } /** Keep actionable decisions and their checks while counting routine skips by stable code. */ export function compactCloseoutReport(report: Awaited>) { const skipped: Partial> = {}; const plan = report.plan.flatMap(row => { - if (["already_terminal", "other_repository", "duplicate_selection", "explicit_skip"].includes(row.code)) { + if (["already_terminal", "other_repository", "duplicate_selection", "explicit_skip", "not_merged"].includes(row.code)) { skipped[row.code] = (skipped[row.code] ?? 0) + 1; return []; } - return [{ ...row, action: row.action === "close" ? "close" : "refuse" }]; + return [{ ...row, action: row.action === "skip" && row.code !== "not_delivery" ? "refuse" : row.action }]; }); const prs = new Set(plan.map(row => row.pr)); return { ...report, plan, skipped, checks: report.checks.filter(gate => prs.has(gate.pr)) }; diff --git a/docs/explicit-closeout-attribution.md b/docs/explicit-closeout-attribution.md index e2ce95db..c076a16d 100644 --- a/docs/explicit-closeout-attribution.md +++ b/docs/explicit-closeout-attribution.md @@ -11,12 +11,16 @@ | Undeclared canonical body link, including a gap report, follow-up list, or `tracked in` reference, for an otherwise eligible item | `not_delivery`: Mentioned, not delivered | | Tracker PR URL metadata with no body reference or declaration | No row | | Closing declaration for a longer ID with the same prefix | Does not deliver the shorter ID | -| Several eligible declarations | Latest merge time wins; equal times use the higher PR number | +| Several merged PRs claiming delivery | One explicit `delivery-pr:` item tag wins; otherwise require exactly one claim strictly after the last acceptance-criteria edit; unresolved cases become `needs_review` with all candidates | | No eligible declaration | No closeout | Declaration keywords are close, fix, resolve and deliver, including their existing tense forms. Whitespace between declaration and ID is horizontal; a newline cannot join them. Canonical links retain repository, encoding, traversal and exact-ID checks. `PM:` contextual lists do not act as `PM item:` declarations. -The complete selected PR set is evaluated before SDK writes. Only merged PRs with complete merge facts, passing check policy and eligible tracker items enter delivery selection. Failed checks never win. Earlier qualifying rows become `duplicate_selection`; mention-only rows retain `not_delivery`. A single selected closure supplies completed_at, resolution, actual_result, close_reason and the appended close event. The existing epic, child, terminal, explicit-skip, privacy and strict-validation safeguards remain. +The complete selected PR set is evaluated before SDK writes. A `delivery-pr:` tag is an explicit item-owned delivery link and can supply attribution without a body declaration. Multiple tags or a tag outside the selected set cannot silently fall back. Generic `pr_url` metadata remains contextual. Without an explicit delivery link, immutable SDK history determines the last acceptance-criteria change (or item creation when no later edit exists). Exactly one linked merged PR must claim the exact item in its description or commit messages and merge strictly after that cutoff. Unrelated later edits do not move it. Unavailable history holds delivery. + +Passing checks do not resolve attribution. Competing claims, equal merge times and multiple post-criteria claims require review even if one has failing checks. A uniquely attributed PR must still satisfy the existing merge and check policy. The selected PR supplies completed_at, resolution, actual_result, close_reason and the close event consistently. All candidates appear in `needs_review` plan rows and the compact closeout PR body; the item stays open. For an all-held workflow run, a privacy-gated tracker `closeout-review.json` artifact provides a reviewable diff without changing any item or item history. + +`test/closeout-selection.test.ts` uses real SDK trackers, deterministic SDK clocks for creation and criteria edits, synthetic PR/commit metadata, `GIT_CEILING_DIRECTORIES`, and a PATH containing no git. It covers one PR, an earlier claim before changed criteria plus later implementation, ambiguity, explicit overrides, missing history, and commit claims. The historical receipts below describe earlier matcher revisions; their latest-merge behavior is superseded by this policy. ## Regression evidence @@ -40,7 +44,7 @@ Both `npm run release:check` and `bun run release:check` completed with exit 0 a ## Review reporting contract -Thread `PRRT_kwDOTbN4pM6ub0vv` identified the metadata-only reporting error in this table and README. Candidate rows come from canonical body references or standalone declarations. Tracker PR URL metadata alone never creates a candidate; undeclared body references reach `not_delivery` only after the other eligibility checks pass. The documentation now states both conditions. +Thread `PRRT_kwDOTbN4pM6ub0vv` identified the metadata-only reporting error in this table and README. References come from canonical body links or standalone declarations. Tracker PR URL metadata alone never creates a candidate. Round 4 corrects candidacy: fully inspected undeclared references are context, independently of merge/check gaps. The documentation now states both conditions. The additional non-git SDK regression verifies metadata-only input produces an empty plan, leaves the item in progress and appends no close event. Two documentation contract checks cover both corrected locations. Restoring either document alone from `9c08fc2` makes its check fail with `ERR_ASSERTION` (exit 1). Restoring only `runCloseout` from main at review start (`4e18c93`) makes the new SDK regression fail with `ERR_ASSERTION` (one closure instead of zero, exit 1). All reversions were restored. The full PR diff was reviewed for the same reporting-contract error; no additional runtime correction was needed. @@ -49,3 +53,81 @@ The additional non-git SDK regression verifies metadata-only input produces an e Both full release gates passed at review code/test head `2fa4571247b5f86b984c588f32b494538a2f89ee`, sequentially with isolated git identity: `npm run release:check` and `bun run release:check` each exited 0 with 963/963 tests, zero failures, cancellations or skips. Each measured exact 100/100/100/100 across all 16 authored modules: lines/statements 11037/11037, functions 299/299 and branches 3510/3510. All remaining release stages passed with unchanged pins, inventory, exclusions and thresholds. Subsequent changes contain only documentation and append-only pm receipts; runtime source and tests remain identical to this gate head. The item remains in progress for orchestrator verification; its claim is released at handoff. No pm-ado record or history is edited. No merge or package release is performed. + +## Acceptance-aware selection regression proof + +Only `runCloseout` was replaced with its `f4c29e8` implementation while retaining current helpers and tests. `node --test --test-name-pattern='ambiguous linked merged PRs|explicit delivery link selects|a claim before or equal' test/closeout-selection.test.ts` failed all three assertions (exit 1): ambiguity closed, the explicit override was ignored, and a claim at/before changed criteria closed. Restoring the function passed 3/3 (exit 0). These tests import TypeScript source directly. The restored attribution/selection suites passed 25/25; focused c8 measured exact 100/100/100/100 for `closeout.ts` without exclusions. The review-only workflow regression also passed and proved no automatic landing request. + +Unreadable commit messages on a linked mention can hide a competing post-criteria claim. Such uncertainty also holds attribution; an explicit delivery link can resolve it, and a PR merged before the criteria cutoff cannot become a qualifying claim. Removing only the unknown-claim selection guard makes `unreadable later commit claims never permit fallback to an earlier description claim` fail an assertion (exit 1, one closure instead of zero). Restoring it passes that regression and the pre-criteria control, 2/2. + + +## Incomplete candidates and persistent auto-merge requests + +Candidate collection is independent of merge/check eligibility. A merged PR claiming delivery with missing or invalid merge facts, head checks, protection metadata, or commit pages remains in the ambiguity set. A fully inspected mention/link-only PR does not enter that set. Unknown merged flags and timestamps also remain plausible; `--since` fetches uncertain listing entries instead of discarding them. Only a validated timestamp can place a claim before the criteria cutoff. Commit pagination must match the PR detail response's declared commit count; missing counts or truncated pages remain unreadable evidence, including a cut that could hide a declaration after a body mention. Unreadable commit sets also retain possible commit-only links when descriptions contain no references. A failed PR read cannot prove linkage is absent, so it conservatively holds eligible active items with the unreadable PR number. Foreign, terminal, explicitly excluded, epic and active-parent items remain ineligible. An explicit valid item-owned delivery link can still resolve competing candidates, but cannot waive its chosen PR's evidence requirements. + +Review rows retain every candidate number and `missing_evidence` entries with the PR number, evidence code and safe reason. Missing merge evidence names the affected fields. The compact PR summary and review artifact retain these gaps. Complete evidence of a failing check holds an otherwise uniquely attributed PR; passing another PR never resolves competing claims. + +The workflow explicitly calls `gh pr merge --disable-auto` for owned closeout PRs before any guard, installation, evidence query or branch update. Only an existing `autoMergeRequest` is revoked; an unarmed PR needs no disabling mutation. An `always()` cleanup repeats revocation on a hold, no publication, disabled CI, failure or cancellation, including failure after auto-merge was armed during maintenance. It uses the caller repository identity and fixed closeout branch, filters out fork heads, and fails before publication if initial revocation is denied. Successful App runs may arm only after caller checks are dispatched and the review hold is clear. Held PR bodies state that automatic merge is disabled because attribution needs review. + +The early-exit audit covers invalid default refs and inputs, dependency/setup failures, evidence refusals, privacy/health failures, invalid publication credentials, no-op publication, push/PR-write denial, disabled or missing caller workflows, dispatch denial, and maintenance failures. Initial revocation protects all pre-publication paths; final cleanup covers failures and cancellation after arming. No existing request is carried across a held update. + +The table-driven `incomplete candidate holds attribution` regressions cross merge status, merge SHA, merge timestamp, head SHA, checks, required metadata, PR/commit fetches and pagination failures with another valid claim or none, using real non-git SDK trackers and unchanged-history assertions. `earlier armed auto-merge is disabled before a later hold updates the reused PR` runs the actual workflow bash twice against a stateful GitHub boundary; it verifies revocation before PR editing, final disabled state and the public explanation. `previous auto-merge is revoked on workflow exit` exercises guard, setup, evidence, privacy, no-op, empty-CI, dispatch, maintenance and disable-denial paths. + +Fix-only revert evidence is recorded on `pm-github-closeout-attribution`: replacing only `runCloseout` with the pre-round implementation failed the initial 20-case matrix (20 failures, exit 1), including an assertion that one item closed instead of zero. Replacing only both explicit disable operations with no-ops failed the two-run workflow regression with `ERR_ASSERTION` (`true !== false`, exit 1). Both implementations were restored. These controls execute the retained tests and do not count test-load errors as regression proof. + + +A resolved explicit delivery link cannot leave a `needs_review` row for its closed item. Unreadable PR rows begin as refusals, and only unresolved attribution promotes them to review together with candidate details. Additional real-tracker controls cover six unreadable commit-set conditions with reference-free descriptions, crossed with another valid claim or none; these hold possible commit-only delivery instead of treating missing messages as proof of no link. + +Confirmed unmerged PRs discovered while checking uncertain `--since` timestamps use the routine `not_merged` decision. They cannot supply delivery or trigger the workflow's incomplete-merge refusal. `confirmed unmerged references cannot block publication of a unique merged delivery` executes the real workflow with both PR kinds and verifies publication, closure and the compact skip count. + +## Item revision binding (review round 2) + +Closeout reads an authoritative SDK document for each item before fetching PR evidence. Selection uses that document's metadata and retains its complete `hashDocument` hash, including tags, criteria, annotations, links and body. Immediately before SDK persistence, a request-scoped `beforeMutation` guard compares `context.before` with that hash under the SDK's existing per-item lock. Refusal changes neither the item nor its history. A new read outside the lock cannot substitute for this check. + +The pinned SDK 2026.10.20 has no public `close` expected-revision option. `BulkItemUpdateMutation.match` guards updates only; `BulkItemCloseMutation` has no match field. The internal `runWithItemMutationPrecondition` is not exported publicly. The implementation therefore uses the public `runWithActiveExtensions` / `setActiveExtensionHooks` guard API and the ordinary SDK close lock. The upstream API needed is `close(id, reason, { expectedItemHash }, clientOptions)`, with an atomic hash comparison inside the close lock and a stable revision-conflict code. Dependency pins remain unchanged. + +On revision refusal, closeout re-runs the full evidence and attribution selection once for that item. A unique fresh selection can close with fresh criteria and provenance; competing claims remain held. A second refusal marks all candidate rows `needs_review` with the revision-change reason. A peer's terminal closure is preserved and reported as `already_terminal`. Re-selection cannot close unrelated items or repeat their closures. + +The read-to-write audit covers delivery tags, generic PR links, body/commit declarations, acceptance-criteria chronology, terminal status, annotations and linked resources. Closeout does not independently mutate tags, links, criteria or comments. The main write path is SDK `close`, which writes completion time, resolution, expected/actual results and close reason together with its immutable history event. SDK close can also auto-unblock dependents: those writes compare each dependent's own snapshot under its item lock. A refused dependent write gets one fresh delivery selection or an explicit hold; a new prerequisite cannot be erased by stale unblocking. Normal SDK close validation, ownership and workflow policies remain active. The guard is request-scoped and cannot leak into an independent caller or tracker. + +`test/closeout-revision.test.ts` schedules six peer edits (retag, acceptance criteria, second delivery claim, peer closure, comment, linked documentation) at 17 boundaries: the document snapshot, every PR/protection/rules/checks/statuses/commits fetch for two PRs, history read, completed selection, immediately before close, and inside the close lock. All use real SDK operations in a non-git tracker. The locked edits must be refused; successful close history must chain to the final selected revision and retain matching delivery tag, criteria and PR provenance. Additional controls cover a second change, independent concurrent writes, item-scoped retry, a deleted snapshot and four dependent-update scenarios (unchanged, concurrent prerequisite edit, conflict during re-selection and an independently handled closure). The suite contains 110 tests. A real SDK `history_append` barrier also pauses a peer after its item write but before its history append. `verifyHistoryEntries(history, selectedDocument)` refuses mixed document/history revisions, so old criteria chronology cannot authorize new criteria. + +A guard-only behavioral revert retains the observers and removes only the locked hash comparison. The retag and dependent-update regressions both fail: PR 1 provenance survives a retag to PR 2, and stale automatic unblocking erases a peer prerequisite. Restoring the comparison passes both tests. The PM comments retain the exact commands and counts. + + +## One item decision through publication (review round 3) + +Candidate evidence and item decisions have different roles. `CheckGate` retains the original merge/check facts. One `ItemDecision` projects the final item outcome onto every candidate row, including incomplete competitors: + +| Final item decision | Candidate role | Report action / code | Publication | +| --- | --- | --- | --- | +| Chosen, with complete passing evidence | Chosen PR | `close` / `head_checks_passed` | Closed item and its chosen provenance | +| Chosen, with complete passing evidence | Every other candidate, regardless of evidence gaps | `skip` / `duplicate_selection` (superseded informational) | Routine skip count, without a refusal or review hold | +| Blocking hold | Every candidate | `needs_review` / `needs_review`, all candidate numbers and applicable evidence gaps | Open item and review artifact | + +A failed chosen check cannot designate a winner or hide its peers. A close refusal or second revision conflict also projects a hold across the whole item group. Standalone proven mentions remain `not_delivery`; they do not claim delivery. Confirmed unmerged and otherwise ineligible references keep their existing routine decisions. Re-selection replaces the old item decision before publication. + +`item decision end to end: / / ` in `test/closeout-workflow.test.ts` has **36 cells**: three selection sources (explicit delivery tag, single post-criteria claim, ambiguous), six competitor states (none, valid, missing merge SHA, failed evidence fetch, failed checks, pagination cut), and two peer schedules (none, retag mid-run). The single-claim case changes acceptance criteria between the two merges. Ambiguity with no competitor uses a delivery selector outside the selected set; otherwise both PRs claim delivery after the criteria cutoff. The peer uses the real SDK to retag to PR 1 during PR 2's evidence fetch, after the item snapshot. A stale attempt that could close must re-select; an already unresolved attempt can remain held. + +Each cell runs the real closeout CLI and actual workflow bash through its publication gate, privacy scan, local Git push, and PR-body boundary. It verifies the published tracker bytes and history, one close event or none, closure count, all chosen provenance fields, and the public summary. Exactly one result is allowed: closed item with published chosen-PR provenance, or open item with a published hold listing every candidate. A closure cannot coexist with a refusal or review hold; a held item cannot acquire a close event or request automatic landing. The existing non-git SDK selection/revision tests continue to cover operation without a Git repository. + +The fix-only revert restores `closeout.ts` from `4874023`, preserving the previous revision guards and all new tests/workflow code, then rebuilds the package. `node --test --test-name-pattern='item decision end to end: explicit delivery tag / (missing merge SHA|failed evidence fetch) / none' test/closeout-workflow.test.ts` fails both assertions at the workflow gate (0/2 passed, exit 1). Restoring the fix byte-for-byte and rebuilding passes the same two tests through the PM runner with isolated HOME and Git configuration (2/2, exit 0). The exact revert receipt is a comment on `pm-github-closeout-attribution`. The PM-linked focused c8 command passes 286/286 tests and exact 100/100/100/100 for `closeout.ts`, with zero failures, cancellations or skips. + +CI runs `40101464708` and `40103921945` each passed 1337/1337 tests in every npm/Bun job on Node 22/26, then all four coverage aggregations exhausted the existing 8192 MiB heap. Those failed gates are retained on `pm-github-vhi6`. The final matrix performs package and scope setup once for each of four immutable disposable input baselines. Before every cell it restores the input and workflow-owned setup environment, clears prior PR receipts, and executes the actual branch-preparation, closeout and publication steps. This preserves the real refusal/privacy gates, leased Git push and body/provenance assertions without repeating package installation, changelog, strict-health or dispatch controls for every table cell. The existing full workflow tests retain those controls. All-source inventory, thresholds and exclusions are unchanged. The independent-reset fix-only proof still fails both tests at their expected publication gates before restoration. + +Run `40106191758` passed the complete npm release gate on Node 22 with the prepared baselines, but both Node 26 jobs again passed 1337/1337 tests and exhausted the 8 GiB aggregation heap. The c8 merge process now receives a 12288 MiB (12 GiB) heap ceiling before its entry point. The repository is public and its `ubuntu-latest` jobs use [standard public Linux runners with 16 GB RAM](https://docs.github.com/en/actions/reference/runners/github-hosted-runners#standard-github-hosted-runners-for-public-repositories). This resource adjustment retains every test, source, exclusion policy and exact 100% threshold; `pm-github-vhi6` continues to track bounded aggregation. The existing heap-routing regression verifies the flag reaches c8 itself, and the real coverage-fixture tests retain missing-source, incomplete-receipt and threshold-refusal controls. + + +## Delivery candidacy (review round 4) + +`deliveryRole` defines candidacy in one place. A delivery candidate claims this item through an explicit `delivery-pr:` tag, a closing declaration referencing it, or a commit claim naming it (including the existing structured trailer grammar). Fully inspected mention-only and link-only PRs are context, reported as `skip` / `not_delivery` in both CLI and publication summaries. Their evidence gaps never hold the item, and neither a later item hold nor an SDK refusal promotes context into a candidate. Unreadable PR or commit evidence remains unresolved because a claim may be hidden. Incomplete evidence on an actual candidate still holds, preserving round 1's safety rule for candidates. + +The end-to-end decision table now has **186 cells**: all 36 original selection/evidence/peer rows, plus all 25 ordered primary/competitor claim-kind pairs crossed with the same six competitor evidence states (150 additional rows). Each PR has a claim kind from {closing keyword, delivery tag, commit claim, mention-only, link-only}. The original rows retain their inputs, including the mention-only primary with an unmatched selector in the two ambiguous/no-competitor rows. Those two rows now expect context rather than an invented candidate hold; the other 34 retain their outcomes. Explicit selection tags remain an independent selector. The claim-kind rows use creation as the acceptance cutoff and no peer edit to isolate candidacy. The original rows retain changed-criteria and mid-run retag schedules. Expected totals are 71 closed-and-published deliveries, 87 published holds and 28 context-only runs without publication. + +Each row declares its exact expected chosen PR, held candidate numbers and context numbers from its inputs, then asserts closure count, every row's action/code/candidate set, immutable history, all chosen provenance fields, the workflow publication result and public summary. Context-only rows expect no publication. In the retained unmatched-selector/peer-retag row, the independent peer edit stays local; the remote tracker retains its input bytes and no review artifact or close event is published. Missing PR fetches remain unresolved even when fixture intent was context; fully readable merge/check gaps cannot change context into a candidate. A context competitor is listed in a published closure or hold without a refusal code or candidate number. + +The non-git `fully inspected context cannot hold a unique delivery` regressions additionally cross missing merge SHA, missing/failed checks, unavailable required metadata, check fetch failure and pagination cuts. `fully inspected mentions are context even without any delivery candidate` verifies an unchanged tracker and no invented ambiguity. Earlier mention-only attribution tests now assert context rows explicitly rather than the incorrect candidate set. + +Fix-only revert proof replaces only `closeout.ts` with its pre-round `4dd53552f825b3b42959ae96b9e61b27fe96e900` contents, retaining every new test and workflow change. `node --test --test-name-pattern='fully inspected context|fully inspected mentions' test/closeout-selection.test.ts` then fails 8/8 assertions (exit 1). Restoring the file byte-for-byte passes 8/8 through the PM runner (exit 0). The retained tests import TypeScript source directly, so no rebuild is needed for this proof. The PM comment records the complete receipt. + +The PM-linked focused c8 command across `test/closeout.test.ts`, `test/closeout-attribution.test.ts`, `test/closeout-selection.test.ts` and `test/closeout-revision.test.ts` passes **294/294 tests**, exact **100/100/100/100 for closeout.ts**, with zero failures, cancellations or skips. diff --git a/scripts/coverage-gate.ts b/scripts/coverage-gate.ts index 0186c8b6..b012896b 100644 --- a/scripts/coverage-gate.ts +++ b/scripts/coverage-gate.ts @@ -63,8 +63,8 @@ export function collectCoverageSources(root: string, target: string): string[] { }); } -/** Heap ceiling for the c8 merge process; GitHub-hosted Linux runners have 16 GiB. */ -export const COVERAGE_HEAP_FLAG = "--max-old-space-size=8192"; +/** Heap ceiling for c8 aggregation on public Linux runners with 16 GB RAM. */ +export const COVERAGE_HEAP_FLAG = "--max-old-space-size=12288"; /** * Run the strict all-source gate and return its exit status. @@ -91,9 +91,9 @@ export function runCoverageGate(root: string, runner: CoverageRunner = spawnSync const node = nodeToolingExecutable(process.versions); // c8 merges the raw V8 coverage of every spawned process, including each // real pm CLI child the sync-pipeline tests start. That merge exceeded Node's - // default ~4 GiB heap on CI ("Reached heap limit"), after every test had - // passed. The larger heap is a resource limit only: thresholds, includes - // and the test set are unchanged. + // former 8 GiB ceiling on Node 26 after all 1,337 tests passed, even with + // matrix setup shared. Leave runner headroom with a 12 GiB heap ceiling. + // This changes resources only: thresholds, includes and the test set stay unchanged. const result = runner(node, [COVERAGE_HEAP_FLAG, c8, "--100", "--all", "--exclude-after-remap", "--extension=.ts", "--extension=.js", `--reports-dir=${reports}`, `--temp-directory=${join(reports, "v8")}`, "--reporter=text", "--reporter=lcov", "--reporter=json-summary", "--reporter=json", diff --git a/test/bot-pr-workflow.test.ts b/test/bot-pr-workflow.test.ts index 90259845..38e63dd2 100644 --- a/test/bot-pr-workflow.test.ts +++ b/test/bot-pr-workflow.test.ts @@ -27,7 +27,7 @@ function fixture(run: (f: { dir: string; env: NodeJS.ProcessEnv; receipts: () => const env: NodeJS.ProcessEnv & { RECEIPTS: string } = { ...process.env, GIT_CONFIG_GLOBAL: "/dev/null", GIT_CONFIG_NOSYSTEM: "1", GIT_AUTHOR_NAME: "Fixture", GIT_COMMITTER_NAME: "Fixture", GIT_AUTHOR_EMAIL: "fixture@users.noreply.github.com", GIT_COMMITTER_EMAIL: "fixture@users.noreply.github.com", APP_DEADLINE: "1700002400", REPOSITORY: "acme/widgets", DEFAULT_BRANCH: "trunk", SYNC_BRANCH: "automation/proposal", SYNC_PUBLISHED: "true", SYNC_PR_NUMBER: "7", CI_WORKFLOWS: "ci.yml codeql.yml", - RECEIPTS: join(dir, "receipts"), STATE: join(dir, "state"), PATH: bin + ":" + process.env.PATH }; + GITHUB_ENV: join(dir, "job-env"), RECEIPTS: join(dir, "receipts"), STATE: join(dir, "state"), PATH: bin + ":" + process.env.PATH }; const git = (...args: string[]) => { const r = spawnSync("git", args, { cwd: dir, env, encoding: "utf8" }); assert.equal(r.status, 0, r.stderr); return r.stdout.trim(); @@ -205,7 +205,17 @@ for (const kind of ["sync", "closeout"]) { })); } -test("sync and closeout execute identical PR maintenance", () => assert.equal(maintenance("sync"), maintenance("closeout"))); +test("sync and closeout share PR maintenance apart from the closeout attribution hold", () => { + const hold = 'if [[ "${CLOSEOUT_REVIEW:-0}" != 0 ]]; then\n' + + ' echo "::notice::Delivery attribution needs review; caller checks were dispatched and automatic landing is disabled."\n' + + ' exit 0\nfi\n'; + const closeout = maintenance("closeout"); + assert.equal(closeout.split(hold).length, 2); + const landingReceipt = 'if [[ -n "${APP_DEADLINE:-}" ]]; then\n' + + ' printf \'CLOSEOUT_AUTOMERGE_ALLOWED=true\\n\' >> "${GITHUB_ENV}"\nfi\n'; + assert.equal(closeout.split(landingReceipt).length, 2); + assert.equal(maintenance("sync"), closeout.replace(hold, "").replace(landingReceipt, "")); +}); for (const kind of ["sync", "closeout"]) { test(`${kind}: App publication uses a pinned repo-scoped token after gates and bounds its lifetime`, () => { @@ -224,7 +234,7 @@ for (const kind of ["sync", "closeout"]) { assert.match(mint, /skip-token-revoke: false/); assert.ok(source.indexOf("name: Verify strict pm health") < source.indexOf("name: Create automation app token")); assert.ok(source.indexOf("name: Validate publication credentials") < source.indexOf("name: Commit gated")); - assert.equal(source.split("GH_TOKEN: ${{ steps.automation_token.outputs.token || github.token }}").length - 1, 2); + assert.equal(source.split("GH_TOKEN: ${{ steps.automation_token.outputs.token || github.token }}").length - 1, kind === "closeout" ? 3 : 2); assert.match(source, /MAINTENANCE_ATTEMPTS: \$\{\{ steps.automation_token.outputs.token != '' && '40' \|\| '120' \}\}/); assert.equal((source.match(/secrets\.automation_app_private_key/g) ?? []).length, 2); assert.match(source, /APP_DEADLINE: \$\{\{ steps.publication_auth.outputs.deadline \}\}/); diff --git a/test/changelog-placement.test.ts b/test/changelog-placement.test.ts index 4263a9c4..0ba2637b 100644 --- a/test/changelog-placement.test.ts +++ b/test/changelog-placement.test.ts @@ -5,7 +5,7 @@ import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "nod import { tmpdir } from "node:os"; import { join, resolve } from "node:path"; import test from "node:test"; -import { create, get } from "@unbrained/pm-cli/sdk"; +import { create, get, runWithReproducibleProcessEnvironment } from "@unbrained/pm-cli/sdk"; import { runCloseout, type CloseoutApi } from "../closeout.ts"; import { parseImportOptions, runImport, type GhIssue } from "../index.ts"; @@ -43,7 +43,7 @@ function evidence(id: string): CloseoutApi { const head = "a".repeat(40); return { base: "https://api.github.com", next: () => undefined, async get(url) { let body: unknown = []; - if (url.includes("/pulls/")) body = { number: 1, body: `Delivers ${id}`, merged: true, + if (url.endsWith("/pulls/1")) body = { number: 1, body: `Delivers ${id}`, merged: true, commits: 0, merged_at: completed, merge_commit_sha: "b".repeat(40), html_url: `https://github.com/${repo}/pull/1`, head: { sha: head }, base: { ref: "main", repo: { full_name: repo } } }; else if (url.includes("/protection/")) body = { contexts: ["CI"], checks: [] }; @@ -71,7 +71,8 @@ for (const kind of ["closeout", "sync"]) { custom: 'node -e "require(\'node:fs\').writeFileSync(\'custom.txt\', JSON.stringify(process.argv.slice(1)))"', } })); // The CLI's stdin mode avoids spawning an unrelated pm installation. - const item = await create({ title: kind === "closeout" ? "October delivery" : "Unrelated native item", type: "Issue" }, { pmRoot, noExtensions: true }); + const item = await runWithReproducibleProcessEnvironment({ PM_CLOCK: "2026-09-01T00:00:00Z", PM_SEED: "placement" }, () => + create({ title: kind === "closeout" ? "October delivery" : "Unrelated native item", type: "Issue" }, { pmRoot, noExtensions: true })); assert.equal(spawnSync("git", ["add", ".agents/pm", "package.json"], { cwd: root, env }).status, 0); assert.equal(spawnSync("git", ["commit", "-qm", "September release"], { cwd: root, env }).status, 0); if (tagged) assert.equal(spawnSync("git", ["tag", "v2026.9.2"], { cwd: root, env }).status, 0); diff --git a/test/closeout-attribution.test.ts b/test/closeout-attribution.test.ts index c13e7ba4..bce03fd7 100644 --- a/test/closeout-attribution.test.ts +++ b/test/closeout-attribution.test.ts @@ -5,7 +5,7 @@ import { mkdtempSync, readFileSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { dirname, join } from "node:path"; import test from "node:test"; -import { create, get, update, commitImportedItem, getItemPath, readSettings } from "@unbrained/pm-cli/sdk"; +import { runWithReproducibleProcessEnvironment, create, get, update, commitImportedItem, getItemPath, readSettings } from "@unbrained/pm-cli/sdk"; import { runCloseout, type CloseoutApi } from "../closeout.ts"; import { withEnv } from "./helpers/mock-github-server.ts"; @@ -17,6 +17,7 @@ const recorded = [16, 19].map(n => JSON.parse(readFileSync(new URL(`./fixtures/c /** Real SDK tracker plus complete HTTP-boundary evidence; neither SDK nor mutation is mocked. */ async function scenario(linked: boolean, bodies?: string[], facts: Partial> = {}) { + return runWithReproducibleProcessEnvironment({ PM_CLOCK: "2026-10-01T00:00:00Z", PM_SEED: "closeout", PM_CLOCK_TICK_MS: "1" }, async () => { const root = mkdtempSync(join(tmpdir(), "closeout-attribution-")); try { const init = spawnSync(join(import.meta.dirname, "../node_modules/.bin/pm"), ["--pm-path", root, "init", "fixture"], { cwd: root, encoding: "utf8", @@ -33,8 +34,8 @@ async function scenario(linked: boolean, bodies?: string[], facts: Partial p.number === n)!.body).replaceAll("pm-ado-5w3m", item.id), - merged: true, merged_at: facts[n]?.at ?? (n === 16 ? "2026-10-04T12:58:08Z" : "2026-10-08T01:19:51Z"), merge_commit_sha: n.toString(16).repeat(20), + const value = endpoint.includes("/commits?") ? [] : endpoint.includes("/pulls/") ? { number: n, body: (bodies?.[[16, 19, 20].indexOf(n)] ?? recorded.find(p => p.number === n)!.body).replaceAll("pm-ado-5w3m", item.id), + merged: true, commits: 0, merged_at: facts[n]?.at ?? (n === 16 ? "2026-10-04T12:58:08Z" : "2026-10-08T01:19:51Z"), merge_commit_sha: n.toString(16).repeat(20), html_url: url(n), head: { sha: evidenceHead }, base: { ref: "main", repo: { full_name: repo } } } : endpoint.includes("/protection/") ? { contexts: ["CI"], checks: [] } : endpoint.includes("/check-runs") ? { total_count: 2, check_runs: [{ name: "CI", head_sha: evidenceHead, status: "completed", conclusion: facts[evidenceNumber]?.success === false ? "failure" : "success", app: { id: 7 } }, @@ -46,6 +47,7 @@ async function scenario(linked: boolean, bodies?: string[], facts: Partial { @@ -68,8 +70,7 @@ test("mention only remains open even with tracker PR metadata", async () => { const mention = "Leaves pm-ado-5w3m open; follow-up gap tracked in https://github.com/unbraind/pm-ado/blob/main/.agents/pm/issues/pm-ado-5w3m.toon"; const { report, item, history } = await scenario(true, [mention, mention]); assert.equal(report.closed, 0); - assert.deepEqual(report.plan.map(row => row.code), ["not_delivery", "not_delivery"]); - assert.ok(report.plan.every(row => /mentioned, not delivered/i.test(row.reason))); + assert.deepEqual(report.plan.map(row => [row.action, row.code, row.candidates]), [["skip", "not_delivery", undefined], ["skip", "not_delivery", undefined]]); assert.equal(item.status, "in_progress"); assert.ok(!history.includes('"op":"close"')); }); @@ -126,27 +127,27 @@ test("multiline keywords and trailers never declare delivery", async () => { } }); -test("latest qualifying delivery wins while a third mention remains not_delivery", async () => { +test("competing delivery claims require review with every linked candidate", async () => { const mention = "See https://github.com/unbraind/pm-ado/blob/main/.agents/pm/issues/pm-ado-5w3m.toon"; const { report, item } = await scenario(false, ["Closes pm-ado-5w3m", "pm-github: pm-ado-5w3m", mention]); - assert.equal(report.closed, 1); - assert.deepEqual(report.plan.map(row => [row.pr, row.code]), [[16, "duplicate_selection"], [19, "head_checks_passed"], [20, "not_delivery"]]); - assert.equal(item.status, "closed"); + assert.equal(report.closed, 0); + assert.deepEqual(report.plan.map(row => [row.pr, row.action, row.code, row.candidates]), [[16, "needs_review", "needs_review", [16, 19]], [19, "needs_review", "needs_review", [16, 19]], [20, "skip", "not_delivery", undefined]]); + assert.equal(item.status, "in_progress"); }); -test("merge time selects delivery independently of PR number and passing checks remain required", async () => { +test("merge chronology and passing checks cannot resolve competing delivery claims", async () => { for (const facts of [ { 16: { at: "2026-10-09T00:00:00Z" } }, { 19: { success: false } }, ]) { const { report, item } = await scenario(false, ["Closes pm-ado-5w3m", "pm-github: pm-ado-5w3m"], facts); - assert.equal(report.closed, 1); - assert.equal(report.plan.find(row => row.action === "close")!.pr, 16); - assert.match(item.resolution!, /pull\/16/); + assert.equal(report.closed, 0); + assert.ok(report.plan.every(row => row.action === "needs_review")); + assert.equal(item.status, "in_progress"); } }); -test("many items and PRs preserve declaration boundaries, casing, punctuation and latest delivery", async () => { +test("many items and PRs preserve declaration boundaries, casing, punctuation and ambiguous delivery", async () => { const root = mkdtempSync(join(tmpdir(), "closeout-many-declarations-")); try { const init = spawnSync(join(import.meta.dirname, "../node_modules/.bin/pm"), ["--pm-path", root, "init", "bulk"], { cwd: root, encoding: "utf8" }); @@ -170,8 +171,8 @@ test("many items and PRs preserve declaration boundaries, casing, punctuation an }); const api: CloseoutApi = { base: "https://api.github.com", next: () => undefined, get: async endpoint => { const n = Number(endpoint.split("/").at(-1)); - const value = endpoint.includes("/pulls/") ? { number: n, body: bodies[n - 1], merged: true, - merged_at: "2026-10-08T01:19:51Z", merge_commit_sha: "b".repeat(40), html_url: url(n), + const value = endpoint.includes("/commits?") ? [] : endpoint.includes("/pulls/") ? { number: n, body: bodies[n - 1], merged: true, commits: 0, + merged_at: "2099-10-08T01:19:51Z", merge_commit_sha: "b".repeat(40), html_url: url(n), head: { sha: head }, base: { ref: "main", repo: { full_name: repo } } } : endpoint.includes("/protection/") ? { contexts: ["CI"], checks: [] } : endpoint.includes("/check-runs") ? { total_count: 1, check_runs: [{ name: "CI", head_sha: head, status: "completed", conclusion: "success", app: { id: 7 } }] } : []; @@ -182,10 +183,10 @@ test("many items and PRs preserve declaration boundaries, casing, punctuation an assert.equal(report.failed, 0); assert.equal(report.checks.length, 12); assert.equal(report.plan.length, 38); - expected.push(`2:${ids[24]}`); + assert.deepEqual(report.plan.filter(row => row.action === "close").map(row => `${row.pr}:${row.item}`).sort(), expected.sort()); - assert.deepEqual(report.plan.filter(row => row.code === "duplicate_selection").map(row => [row.pr, row.item]), [[1, ids[24]]]); - assert.deepEqual(report.plan.filter(row => row.code === "not_delivery").map(row => [row.pr, row.item]), verbs.map((_, i) => [i + 1, ids[25]])); + assert.deepEqual(report.plan.filter(row => row.code === "needs_review").map(row => [row.pr, row.item]), [[1, ids[24]], [2, ids[24]]]); + assert.deepEqual(report.plan.filter(row => row.item === ids[25]).map(row => [row.pr, row.code]), verbs.map((_, i) => [i + 1, "not_delivery"])); for (const id of ids) assert.equal((await get(id, {}, sdk)).item.status, "open"); } finally { rmSync(root, { recursive: true, force: true }); } }); diff --git a/test/closeout-revision.test.ts b/test/closeout-revision.test.ts new file mode 100644 index 00000000..b28e5036 --- /dev/null +++ b/test/closeout-revision.test.ts @@ -0,0 +1,241 @@ +/** Schedule peer edits at read/write boundaries while retaining all real SDK persistence. */ +import assert from "node:assert/strict"; +import { spawnSync } from "node:child_process"; +import { mkdtempSync, readFileSync, rmSync, existsSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join } from "node:path"; +import test from "node:test"; +import { create, get, update, close, comments, docs, deleteItem, runWithActiveExtensions, setActiveExtensionServices, runWithReproducibleProcessEnvironment, PmCliError } from "@unbrained/pm-cli/sdk"; +import { runCloseout, type CloseoutApi, type CloseoutObserver } from "../closeout.ts"; + +const repo = "fixture/project"; +const prUrl = (number: number) => `https://github.com/${repo}/pull/${number}`; +const boundaries = ["snapshot", ...[1, 2].flatMap(n => ["pull", "protection", "rules", "checks", "statuses", "commits"].map(step => `${n}:${step}`)), "history", "selection", "before_close", "locked"]; +const edits = ["retag", "criteria", "second_claim", "peer_close", "comment", "links"] as const; + +/** Initialize one non-git tracker and inject edits at an observed real operation boundary. */ +async function interleave(boundary: string, edit: typeof edits[number], twice = false, extraItems = false) { + const root = mkdtempSync(join(tmpdir(), "closeout-revision-")); + const sdk = { pmRoot: root, noExtensions: true }; + try { + const initialized = spawnSync(join(import.meta.dirname, "../node_modules/.bin/pm"), ["--pm-path", root, "init", "fixture"], { + cwd: root, encoding: "utf8", env: { ...process.env, GIT_CEILING_DIRECTORIES: dirname(root) }, + }); + assert.equal(initialized.status, 0, initialized.stderr); + assert.equal(existsSync(join(root, ".git")), false); + const item = (await create({ title: "Revision-bound delivery", type: "Issue", author: "fixture", acceptanceCriteria: "Original criteria" }, sdk)).item; + await update(item.id, { status: "in_progress", tags: `delivery-pr:${prUrl(1)}` }, sdk); + const others: string[] = []; + if (extraItems) for (let n = 0; n < 2; n++) { + const extra = (await create({ title: `Independent item ${n}`, type: "Issue", author: "fixture" }, sdk)).item; + await update(extra.id, { status: "in_progress", tags: n === 0 ? `delivery-pr:${prUrl(1)}` : undefined }, sdk); + others.push(extra.id); + } + let independentClosed = false; + let injected = 0; + let lockedRefusals = 0; + let fetches = 0; + /** A genuine peer operation; lock contention must refuse it before any persistence. */ + const peer = async () => { + if (edit === "peer_close") await close(item.id, "Peer closure", { author: "peer", resolution: "Peer", expectedResult: "Peer", actualResult: "Peer" }, sdk); + else if (edit === "comment") await comments(item.id, { add: "Peer annotation", author: "peer" }, sdk); + else if (edit === "links") await docs(item.id, { add: ["README.md"], author: "peer" }, sdk); + else await update(item.id, edit === "retag" ? { tags: `delivery-pr:${prUrl(2)}` } + : edit === "second_claim" ? { addTags: [`delivery-pr:${prUrl(2)}`] } + : { acceptanceCriteria: `Peer criteria ${injected}` }, sdk); + }; + const observe = async (point: string) => { + if (point === "locked" && extraItems && !independentClosed) { + independentClosed = true; + await runWithActiveExtensions({ path: root, noExtensions: true }, async () => { + await update(others[1]!, { title: "Independent peer update" }, sdk); + await close(others[1]!, "Independent peer closure", { resolution: "Peer", expectedResult: "Peer", actualResult: "Peer" }, sdk); + }); + } + if (point !== boundary || injected >= (twice ? 2 : 1)) return; + injected++; + if (point === "locked") { + await assert.rejects(peer, (error: unknown) => error instanceof PmCliError && error.code === "lock_conflict"); + lockedRefusals++; + } else await peer(); + }; + const observer: CloseoutObserver = observe; + let currentPr = 1; + const api: CloseoutApi = { base: "https://api.github.com", next: () => undefined, get: async endpoint => { + fetches++; + const number = Number(/\/pulls\/(\d+)/.exec(endpoint)?.[1]); + const kind = endpoint.includes("/commits?") ? "commits" : endpoint.includes("/pulls/") ? "pull" + : endpoint.includes("/protection/") ? "protection" : endpoint.includes("/rules/") ? "rules" + : endpoint.includes("/check-runs") ? "checks" : "statuses"; + if (kind === "pull") currentPr = number; + const head = currentPr.toString().repeat(40); + const value = kind === "pull" ? { number, body: `Closes ${item.id}${extraItems ? `\nCloses ${others[0]}` : ""}`, merged: true, commits: 0, + merged_at: "2099-10-08T00:00:00Z", merge_commit_sha: "b".repeat(40), html_url: prUrl(number), + head: { sha: head }, base: { ref: "main", repo: { full_name: repo } } } + : kind === "protection" ? { contexts: ["CI"], checks: [] } + : kind === "checks" ? { total_count: 1, check_runs: [{ name: "CI", head_sha: head, status: "completed", conclusion: "success", app: { id: 7 } }] } : []; + await observe(`${currentPr}:${kind}`); + return { body: JSON.stringify(value) }; + } }; + const report = await runCloseout(root, repo, { pr: ["1", "2"], apply: true }, api, observer); + const final = (await get(item.id, { depth: "full" }, sdk)).item; + const history = readFileSync(join(root, "history", `${item.id}.jsonl`), "utf8").trim().split("\n").map(line => JSON.parse(line) as { op: string; author: string; before_hash: string; after_hash: string }); + assert.equal(injected, twice ? 2 : 1); + assert.equal(report.failed, 0); + if (extraItems) { + assert.equal((await get(others[0]!, {}, sdk)).item.status, "closed"); + const otherHistory = readFileSync(join(root, "history", `${others[0]}.jsonl`), "utf8"); + assert.equal(otherHistory.split('"op":"close"').length - 1, 1); + assert.equal((await get(others[1]!, {}, sdk)).item.close_reason, "Independent peer closure"); + } + if (boundary === "locked") { + assert.equal(lockedRefusals, 1); + assert.equal(report.closed, extraItems ? 2 : 1); + } else if (edit === "peer_close") { + assert.equal(report.closed, 0); + assert.equal(final.close_reason, "Peer closure"); + assert.ok(report.plan.every(row => row.code === "already_terminal")); + } else if (edit === "second_claim" || twice) { + assert.equal(report.closed, 0); + assert.equal(final.status, "in_progress"); + assert.ok(report.plan.every(row => row.action === "needs_review")); + if (twice) assert.match(report.plan[0]!.reason, /changed again/); + } else assert.equal(report.closed, extraItems ? 2 : 1); + if (report.closed) { + const chosen = (JSON.parse(final.close_reason!) as { pr: number }).pr; + assert.equal(chosen, boundary !== "locked" && edit === "retag" ? 2 : 1); + assert.deepEqual(final.tags, [`delivery-pr:${prUrl(chosen)}`]); + assert.equal(final.expected_result, final.acceptance_criteria); + assert.match(final.resolution!, new RegExp(`pull/${chosen} `)); + const eventIndex = history.findIndex(event => event.op === "close" && event.author === "pm-github"); + assert.equal(history[eventIndex]!.before_hash, history[eventIndex - 1]!.after_hash); + assert.equal(history.filter(event => event.op === "close" && event.author === "pm-github").length, 1); + } else assert.equal(history.filter(event => event.op === "close" && event.author === "pm-github").length, 0); + assert.ok(fetches <= 24, "at most one re-selection"); + return report; + } finally { rmSync(root, { recursive: true, force: true }); } +} + +for (const boundary of boundaries) for (const edit of edits) { + test(`revision-bound closeout: ${edit} at ${boundary}`, async () => { await interleave(boundary, edit); }); +} +test("revision-bound closeout holds a second concurrent change after one re-selection", async () => { await interleave("before_close", "criteria", true); }); + +test("revision-bound closeout retries only its item and isolates its guard from other writes", async () => { + await interleave("1:pull", "retag", false, true); +}); + +test("closeout refuses a removed authoritative snapshot before fetching or mutating", async () => { + const root = mkdtempSync(join(tmpdir(), "closeout-removed-")); + try { + const sdk = { pmRoot: root, noExtensions: true }; + const initialized = spawnSync(join(import.meta.dirname, "../node_modules/.bin/pm"), ["--pm-path", root, "init", "fixture"], { cwd: root, encoding: "utf8" }); + assert.equal(initialized.status, 0, initialized.stderr); + const item = (await create({ title: "Removed peer item", type: "Issue", author: "fixture" }, sdk)).item; + const api: CloseoutApi = { base: "https://api.github.com", next: () => undefined, get: async () => { throw new Error("Must not fetch"); } }; + await assert.rejects(() => runCloseout(root, repo, { pr: "1", apply: true }, api, async point => { + if (point === "listed") await deleteItem(item.id, { author: "peer" }, sdk); + }), /snapshot unavailable/); + } finally { rmSync(root, { recursive: true, force: true }); } +}); + +/** Exercise implicit SDK unblocking, including refused side effects during a re-selection. */ +async function dependentScenario(mode: "stable" | "changed" | "retry_changed" | "handled") { + const root = mkdtempSync(join(tmpdir(), "closeout-dependent-")); + const sdk = { pmRoot: root, noExtensions: true }; + try { + const initialized = spawnSync(join(import.meta.dirname, "../node_modules/.bin/pm"), ["--pm-path", root, "init", "fixture"], { cwd: root, encoding: "utf8" }); + assert.equal(initialized.status, 0, initialized.stderr); + const blocker = (await create({ title: "Blocker delivery", type: "Issue", author: "fixture" }, sdk)).item.id; + const dependent = (await create({ title: "Dependent delivery", type: "Issue", author: "fixture" }, sdk)).item.id; + const other = (await create({ title: "Peer prerequisite", type: "Issue", author: "fixture" }, sdk)).item.id; + await update(blocker, { status: "in_progress" }, sdk); + await update(dependent, { status: "blocked", blockedBy: blocker }, sdk); + let locks = 0; + let attempts = 0; + const api: CloseoutApi = { base: "https://api.github.com", next: () => undefined, get: async endpoint => { + const head = "a".repeat(40); + const value = endpoint.includes("/commits?") ? [] : endpoint.includes("/pulls/") ? { number: 1, + body: `Closes https://github.com/${repo}/blob/main/.agents/pm/issues/${blocker}.toon${mode === "handled" ? `\nCloses https://github.com/${repo}/blob/main/.agents/pm/issues/${dependent}.toon` : ""}`, merged: true, commits: 0, + merged_at: "2099-10-08T00:00:00Z", merge_commit_sha: "b".repeat(40), html_url: prUrl(1), + head: { sha: head }, base: { ref: "main", repo: { full_name: repo } } } + : endpoint.includes("/protection/") ? { contexts: ["CI"], checks: [] } + : endpoint.includes("/check-runs") ? { total_count: 1, check_runs: [{ name: "CI", head_sha: head, status: "completed", conclusion: "success", app: { id: 7 } }] } : []; + return { body: JSON.stringify(value) }; + } }; + const report = await runCloseout(root, repo, { pr: "1", apply: true }, api, async point => { + if (point === "before_close" && ++attempts === 1 && mode === "retry_changed") await update(blocker, { title: "Peer edit before lock" }, sdk); + if (point !== "locked") return; + locks++; + if ((mode === "changed" || mode === "handled") && locks === 1 || mode === "retry_changed" && locks === 2) { + await runWithActiveExtensions({ path: root, noExtensions: true }, () => update(dependent, { blockedBy: other }, sdk)); + } + }); + const item = (await get(dependent, { depth: "full" }, sdk)).item; + assert.equal(report.failed, 0); + assert.equal(report.closed, mode === "handled" ? 2 : 1); + assert.equal(item.status, mode === "stable" ? "open" : mode === "handled" ? "closed" : "blocked"); + if (mode === "changed" || mode === "retry_changed") { + assert.equal(item.blocked_by, other); + const held = report.plan.find(row => row.item === dependent)!; + assert.equal(held.action, "needs_review"); + assert.match(held.reason, mode === "changed" ? /no delivery evidence/ : /automatic unblock held/); + } + assert.equal(report.plan.filter(row => row.action === "close").length, report.closed); + } finally { rmSync(root, { recursive: true, force: true }); } +} + +for (const mode of ["stable", "changed", "retry_changed", "handled"] as const) test(`revision-bound SDK dependent update: ${mode}`, async () => { + await dependentScenario(mode); +}); + + +/** Pause the real SDK between its item write and history append, without replacing either write. */ +test("closeout binds acceptance history to the same authoritative document revision", async () => { + const root = mkdtempSync(join(tmpdir(), "closeout-history-revision-")); + const sdk = { pmRoot: root, noExtensions: true }; + let releasePeer = () => {}; + let peer: Promise | undefined; + try { + const initialized = spawnSync(join(import.meta.dirname, "../node_modules/.bin/pm"), ["--pm-path", root, "init", "fixture"], { cwd: root, encoding: "utf8" }); + assert.equal(initialized.status, 0, initialized.stderr); + const id = await runWithReproducibleProcessEnvironment({ PM_CLOCK: "2026-10-01T00:00:00Z", PM_SEED: "history-revision", PM_CLOCK_TICK_MS: "1" }, async () => { + const item = (await create({ title: "Acceptance history revision", type: "Issue", acceptanceCriteria: "Original criteria", author: "fixture" }, sdk)).item; + await update(item.id, { status: "in_progress" }, sdk); + return item.id; + }); + let signalWritten = () => {}; + const written = new Promise(resolve => { signalWritten = resolve; }); + const resume = new Promise(resolve => { releasePeer = resolve; }); + peer = runWithActiveExtensions({ path: root, noExtensions: true }, async () => { + setActiveExtensionServices({ overrides: [{ layer: "project", name: "history-append-barrier", service: "history_append", run: async () => { + signalWritten(); + await resume; + return { handled: false }; + } }] }); + await runWithReproducibleProcessEnvironment({ PM_CLOCK: "2026-10-20T00:00:00Z", PM_SEED: "peer-history" }, () => + update(id, { acceptanceCriteria: "Peer criteria after the merge" }, sdk)); + }); + await written; + assert.equal((await get(id, {}, sdk)).item.acceptance_criteria, "Peer criteria after the merge"); + const head = "a".repeat(40); + const api: CloseoutApi = { base: "https://api.github.com", next: () => undefined, get: async endpoint => { + const value = endpoint.includes("/commits?") ? [] : endpoint.includes("/pulls/") ? { number: 1, body: `Closes ${id}`, merged: true, commits: 0, + merged_at: "2026-10-08T00:00:00Z", merge_commit_sha: "b".repeat(40), html_url: prUrl(1), head: { sha: head }, base: { ref: "main", repo: { full_name: repo } } } + : endpoint.includes("/protection/") ? { contexts: ["CI"], checks: [] } + : endpoint.includes("/check-runs") ? { total_count: 1, check_runs: [{ name: "CI", head_sha: head, status: "completed", conclusion: "success", app: { id: 7 } }] } : []; + return { body: JSON.stringify(value) }; + } }; + const report = await runCloseout(root, repo, { pr: "1", apply: true }, api, async point => { + if (point === "selection") { releasePeer(); await peer; } + }); + assert.equal(report.closed, 0); + assert.equal(report.plan[0]!.action, "needs_review"); + assert.equal((await get(id, {}, sdk)).item.status, "in_progress"); + assert.ok(!readFileSync(join(root, "history", `${id}.jsonl`), "utf8").includes('"op":"close"')); + } finally { + releasePeer(); + await peer; + rmSync(root, { recursive: true, force: true }); + } +}); diff --git a/test/closeout-selection.test.ts b/test/closeout-selection.test.ts new file mode 100644 index 00000000..1d6a4092 --- /dev/null +++ b/test/closeout-selection.test.ts @@ -0,0 +1,280 @@ +/** Non-git, real-tracker delivery selection and fail-closed ambiguity regressions. */ +import assert from "node:assert/strict"; +import { spawnSync } from "node:child_process"; +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join } from "node:path"; +import test from "node:test"; +import { create, get, update, runWithReproducibleProcessEnvironment } from "@unbrained/pm-cli/sdk"; +import { compactCloseoutReport, runCloseout, type CloseoutApi } from "../closeout.ts"; +import { withEnv } from "./helpers/mock-github-server.ts"; + +const repo = "example/delivery"; +const prUrl = (n: number) => `https://github.com/${repo}/pull/${n}`; +const at = (day: number) => `2026-10-${String(day).padStart(2, "0")}T00:00:00Z`; +/** Fixture controls only external evidence; item creation, edits and closeout use the SDK. */ +interface SelectionOptions { + dates: number[]; + criteriaDay?: number; + tags?: number[]; + body?: "claim" | "mention" | "none"; + commits?: "claim" | "mention" | "invalid" | "error"; + history?: "missing" | "invalid" | "unverified"; + failure?: number; + mentionPr?: number; + unlinkedPr?: number; + errorPr?: number; + since?: boolean; + status?: "canceled"; + excluded?: boolean; + container?: "epic" | "children"; + gap?: "missing-merged" | "merge-sha" | "invalid-merge-sha" | "merge-time" | "invalid-time" | "head-sha" | "missing-head" | "null-head" | "missing-checks" | "failed-checks" | "required-metadata" | "checks-fetch" | "checks-pagination" | "commits-fetch" | "invalid-commits" | "commits-pagination" | "commits-count" | "missing-commits-count" | "invalid-commits-count" | "pr-fetch"; +} + +/** Run selection with git absent from PATH and inspect all durable completion evidence. */ +async function selection(options: SelectionOptions) { + const root = mkdtempSync(join(tmpdir(), "delivery-selection-")); + const sdk = { pmRoot: root, noExtensions: true }; + try { + const init = spawnSync(join(import.meta.dirname, "../node_modules/.bin/pm"), ["--pm-path", root, "init", "fixture"], { + cwd: root, encoding: "utf8", env: { ...process.env, GIT_CEILING_DIRECTORIES: dirname(root) }, + }); + assert.equal(init.status, 0, init.stderr); + const item = await runWithReproducibleProcessEnvironment({ PM_CLOCK: at(1), PM_SEED: "delivery" }, async () => + (await create({ title: "Enforce acceptance criteria", type: options.container === "epic" ? "Epic" : "Issue", author: "fixture", acceptanceCriteria: "Initial criteria" }, sdk)).item); + if (options.criteriaDay) await runWithReproducibleProcessEnvironment({ PM_CLOCK: at(options.criteriaDay), PM_SEED: "criteria" }, () => + update(item.id, { acceptanceCriteria: "Updated acceptance criteria" }, sdk)); + // A later unrelated edit must not move the acceptance cutoff. + await runWithReproducibleProcessEnvironment({ PM_CLOCK: at(20), PM_SEED: "annotation" }, () => + update(item.id, { status: "in_progress", tags: options.tags?.map(n => `delivery-pr:${prUrl(n)}`).join(",") }, sdk)); + if (options.container === "children") await create({ title: "Undelivered child", type: "Task", parent: item.id, author: "fixture" }, sdk); + if (options.status) await update(item.id, { status: options.status }, sdk); + const historyPath = join(root, "history", `${item.id}.jsonl`); + const validHistory = readFileSync(historyPath, "utf8"); + if (options.history === "missing") rmSync(historyPath); + if (options.history === "invalid") writeFileSync(historyPath, "{invalid\n"); + if (options.history === "unverified") { + const events = readFileSync(historyPath, "utf8").trim().split("\n"); + const first = JSON.parse(events[0]!) as { after_hash: string }; + first.after_hash = "broken"; + events[0] = JSON.stringify(first); + writeFileSync(historyPath, events.join("\n") + "\n"); + } + const reference = `https://github.com/${repo}/blob/main/.agents/pm/issues/${item.id}.toon`; + const api: CloseoutApi = { base: "https://api.github.com", next: link => link, get: async endpoint => { + const number = Number(/\/pulls\/(\d+)/.exec(endpoint)?.[1]); + const headNumber = Number.parseInt(/\/commits\/(\w{2})/.exec(endpoint)?.[1] ?? "00", 16); + if (endpoint.includes("/pulls?")) return { body: JSON.stringify(options.dates.map((day, i) => ({ number: i + 1, merged_at: options.gap === "merge-time" ? null : at(day), updated_at: at(day) }))) }; + const affected = number === 1 || headNumber === 1; + if (number === 1 && options.gap === "pr-fetch" && !endpoint.includes("/commits?")) throw new Error("PR unavailable"); + if (options.gap === "required-metadata" && endpoint.includes("/branches/gap/")) throw new Error("HTTP 403"); + if (headNumber === 1 && options.gap === "checks-fetch" && endpoint.includes("/check-runs")) throw new Error("Checks unavailable"); + if (endpoint.includes("/commits?")) { + if (number === 1 && options.gap === "invalid-commits") return { body: "[{}]" }; + if (number === 1 && options.gap === "commits-fetch") throw new Error("Commits unavailable"); + if (number === 1 && options.gap === "commits-pagination") return { body: "[]", linkHeader: endpoint }; + if (options.commits === "error" && (options.errorPr === undefined || options.errorPr === number)) throw new Error("Unavailable commit evidence"); + return { body: JSON.stringify(options.commits === "invalid" ? [{}] : options.commits ? [{ commit: { + message: options.commits === "claim" ? `Closes ${item.id}\nSee ${reference}` : `See ${reference}`, + } }] : []) }; + } + const value = endpoint.includes("/pulls/") ? { + number, commits: affected && options.gap === "invalid-commits-count" ? -1 : affected && options.gap === "missing-commits-count" ? undefined : affected && (options.gap === "commits-count" || options.gap === "invalid-commits") ? 1 : options.commits ? 1 : 0, merged: affected && options.gap === "missing-merged" ? undefined : true, merged_at: affected && options.gap === "merge-time" ? null : affected && options.gap === "invalid-time" ? "invalid" : at(options.dates[number - 1]!), merge_commit_sha: affected && options.gap === "merge-sha" ? null : affected && options.gap === "invalid-merge-sha" ? "invalid" : "b".repeat(40), html_url: prUrl(number), + body: options.body === "none" || options.unlinkedPr === number ? "Routine work" : `${options.body === "mention" || options.mentionPr === number ? "See" : "Closes"} ${reference}`, + head: affected && options.gap === "missing-head" ? undefined : affected && options.gap === "null-head" ? null : { sha: affected && options.gap === "head-sha" ? "invalid" : number.toString(16).padStart(2, "0").repeat(20) }, base: { ref: affected && options.gap === "required-metadata" ? "gap" : "main", repo: { full_name: repo } }, + } : endpoint.includes("/protection/") ? { contexts: ["CI"], checks: [] } + : endpoint.includes("/check-runs") ? { total_count: 1, check_runs: [{ name: "CI", head_sha: headNumber.toString(16).padStart(2, "0").repeat(20), + status: "completed", conclusion: options.failure === headNumber || (headNumber === 1 && options.gap === "failed-checks") ? "failure" : "success", app: { id: 1 } }] } : []; + if (headNumber === 1 && endpoint.includes("/check-runs")) { + if (options.gap === "missing-checks") return { body: JSON.stringify({ total_count: 0, check_runs: [] }) }; + if (options.gap === "checks-pagination") return { body: JSON.stringify({ total_count: 2, check_runs: [] }) }; + } + return { body: JSON.stringify(value) }; + } }; + const before = options.history === "missing" ? "" : readFileSync(historyPath, "utf8"); + const report = await withEnv({ PATH: root, GIT_CEILING_DIRECTORIES: dirname(root) }, () => + runCloseout(root, repo, { ...(options.since ? { since: at(1) } : { pr: options.dates.map((_, i) => String(i + 1)) }), skip: options.excluded ? item.id : undefined, apply: true }, api)); + if (options.history !== "missing") assert.equal(report.closed === 0 ? readFileSync(historyPath, "utf8") : before, before); + if (options.history === "invalid") writeFileSync(historyPath, validHistory); + const result = (await get(item.id, {}, sdk)).item; + if (report.closed === 0 && !options.history) assert.equal(readFileSync(historyPath, "utf8"), before); + return { report, item: result, history: options.history ? "" : readFileSync(historyPath, "utf8") }; + } finally { rmSync(root, { recursive: true, force: true }); } +} + +test("one claimed merged PR after acceptance closes without git at its merge time", async () => { + const { report, item } = await selection({ dates: [8] }); + assert.equal(report.closed, 1); + assert.equal(item.completed_at, "2026-10-08T00:00:00.000Z"); +}); + +test("only the later PR after the last acceptance change supplies all close evidence", async () => { + const { report, item, history } = await selection({ dates: [4, 8], criteriaDay: 7 }); + assert.equal(report.closed, 1); + assert.deepEqual(report.plan.map(row => [row.pr, row.action]), [[1, "skip"], [2, "close"]]); + assert.equal(item.completed_at, "2026-10-08T00:00:00.000Z"); + assert.match(item.resolution!, /pull\/2/); + assert.match(item.actual_result!, /Merged at 2026-10-08/); + assert.equal((JSON.parse(item.close_reason!) as { pr: number }).pr, 2); + const event = history.trim().split("\n").map(line => JSON.parse(line) as { op: string; patch: { path: string; value: unknown }[] }).find(event => event.op === "close")!; + for (const field of ["completed_at", "resolution", "actual_result", "close_reason"] as const) { + assert.equal(event.patch.find(patch => patch.path === `/metadata/${field}`)!.value, item[field]); + } +}); + +test("ambiguous linked merged PRs stay open with every candidate in compact review output", async () => { + const { report, item } = await selection({ dates: [4, 8] }); + assert.equal(report.closed, 0); + assert.equal(item.status, "in_progress"); + assert.ok(compactCloseoutReport(report).plan.every(row => row.action === "needs_review")); + assert.deepEqual(report.plan.map(row => row.candidates), [[1, 2], [1, 2]]); +}); + +test("explicit delivery link selects its PR even when another claim merged later", async () => { + const { report, item } = await selection({ dates: [4, 8], tags: [1] }); + assert.equal(report.closed, 1); + assert.equal(item.completed_at, "2026-10-04T00:00:00.000Z"); +}); + +test("explicit delivery link supplies attribution without description references", async () => { + const { report } = await selection({ dates: [4, 8], tags: [2], body: "none" }); + assert.equal(report.closed, 1); + assert.equal(report.plan[0]!.pr, 2); +}); + +for (const tags of [[1, 2], [99]]) test(`unresolved explicit delivery links hold all candidates: ${tags}`, async () => { + const { report } = await selection({ dates: [4, 8], tags }); + assert.equal(report.closed, 0); + assert.ok(report.plan.every(row => row.code === "needs_review")); +}); + +for (const history of ["missing", "invalid", "unverified"] as const) test(`unavailable acceptance history holds delivery: ${history}`, async () => { + const { report } = await selection({ dates: [8], history }); + assert.equal(report.closed, 0); + assert.equal(report.plan[0]!.code, "needs_review"); +}); + +test("a claim before or equal to the acceptance change stays open", async () => { + for (const day of [8, 9]) { + const { report } = await selection({ dates: [8], criteriaDay: day }); + assert.equal(report.closed, 0); + assert.equal(report.plan[0]!.code, "needs_review"); + } +}); + +test("a unique later claim with failing checks never falls back to an earlier PR", async () => { + const { report } = await selection({ dates: [4, 8], criteriaDay: 7, failure: 2 }); + assert.equal(report.closed, 0); + assert.deepEqual(report.plan.map(row => row.code), ["needs_review", "needs_review"]); + assert.ok(report.plan.every(row => row.missing_evidence?.some(gap => gap.pr === 2 && gap.code === "head_checks_failed"))); +}); + +test("commit claims can supply unique delivery, while mentions and unavailable commits cannot", async () => { + const { report } = await selection({ dates: [8], body: "none", commits: "claim" }); + assert.equal(report.closed, 1); + const upgraded = await selection({ dates: [8], body: "mention", commits: "claim" }); + assert.equal(upgraded.report.closed, 1); + for (const commits of ["mention", "invalid", "error"] as const) { + const { report } = await selection({ dates: [8], body: "mention", commits }); + assert.equal(report.closed, 0); + assert.equal(report.plan[0]!.code, commits === "mention" ? "not_delivery" : "needs_review"); + } +}); + + +test("unreadable later commit claims never permit fallback to an earlier description claim", async () => { + const { report } = await selection({ dates: [4, 8], mentionPr: 2, commits: "error" }); + assert.equal(report.closed, 0); + assert.ok(report.plan.every(row => row.code === "needs_review")); + assert.deepEqual(report.plan[0]!.candidates, [1, 2]); +}); + +test("unreadable commits before changed criteria cannot supply another qualifying claim", async () => { + const { report } = await selection({ dates: [4, 8], criteriaDay: 7, mentionPr: 1, commits: "error", errorPr: 1 }); + assert.equal(report.closed, 1); + assert.equal(report.plan.find(row => row.action === "close")!.pr, 2); +}); + + +for (const gap of ["missing-merged", "merge-sha", "invalid-merge-sha", "merge-time", "invalid-time", "head-sha", "missing-head", "null-head", "missing-checks", "failed-checks", "required-metadata", "checks-fetch", "checks-pagination", "commits-fetch", "invalid-commits", "commits-pagination", "commits-count", "missing-commits-count", "invalid-commits-count", "pr-fetch"] as const) { + for (const other of [true, false]) test(`incomplete candidate holds attribution: ${gap}, other valid claim=${other}`, async () => { + const { report, item } = await selection({ dates: other ? [8, 9] : [8], gap, mentionPr: gap === "commits-count" || gap === "missing-commits-count" || gap === "invalid-commits-count" ? 1 : undefined, tags: gap === "pr-fetch" && !other ? [1] : undefined }); + assert.equal(report.closed, 0); + assert.equal(item.status, "in_progress"); + assert.ok(report.plan.length > 0); + for (const row of report.plan) { + assert.equal(row.action, "needs_review"); + assert.deepEqual(row.candidates, other ? [1, 2] : [1]); + assert.ok(row.missing_evidence?.some(gap => gap.pr === 1), JSON.stringify(row)); + } + }); +} + + +test("since retains missing merge timestamps and holds the linked candidate", async () => { + const { report } = await selection({ dates: [8, 9], gap: "merge-time", since: true }); + assert.equal(report.closed, 0); + assert.deepEqual(report.plan[0]!.candidates, [1, 2]); + assert.ok(report.plan[0]!.missing_evidence?.some(gap => gap.pr === 1)); +}); + +test("an explicit valid delivery link resolves an incomplete competing candidate", async () => { + const { report } = await selection({ dates: [8, 9], gap: "merge-sha", tags: [2] }); + assert.equal(report.closed, 1); + assert.equal(report.plan.find(row => row.action === "close")!.pr, 2); +}); + +for (const scope of ["terminal", "excluded", "epic", "children"] as const) test(`unreadable PRs cannot select ineligible items: ${scope}`, async () => { + const { report, item } = await selection({ dates: [8], gap: "pr-fetch", status: scope === "terminal" ? "canceled" : undefined, + excluded: scope === "excluded", container: scope === "epic" || scope === "children" ? scope : undefined }); + assert.equal(report.closed, 0); + assert.ok(report.plan.every(row => row.item !== item.id)); +}); + + +test("explicit delivery link cannot leave a held row for its closed item after a competing PR fetch fails", async () => { + const { report, item } = await selection({ dates: [8, 9], gap: "pr-fetch", tags: [2] }); + assert.equal(report.closed, 1); + assert.equal(item.status, "closed"); + assert.ok(report.plan.every(row => row.action !== "needs_review")); +}); + + +for (const gap of ["commits-fetch", "invalid-commits", "commits-pagination", "commits-count", "missing-commits-count", "invalid-commits-count"] as const) { + for (const other of [true, false]) test(`unreadable commit-only candidates hold attribution: ${gap}, other valid claim=${other}`, async () => { + const { report, item } = await selection({ dates: other ? [8, 9] : [8], gap, unlinkedPr: 1 }); + assert.equal(report.closed, 0); + assert.equal(item.status, "in_progress"); + assert.ok(report.plan.length > 0); + for (const row of report.plan) { + assert.equal(row.action, "needs_review"); + assert.deepEqual(row.candidates, other ? [1, 2] : [1]); + assert.ok(row.missing_evidence?.some(gap => gap.pr === 1)); + } + }); +} + +for (const gap of ["merge-sha", "missing-checks", "failed-checks", "required-metadata", "checks-fetch", "checks-pagination"] as const) { + test(`fully inspected context cannot hold a unique delivery: ${gap}`, async () => { + const { report, item } = await selection({ dates: [8, 9], gap, mentionPr: 1 }); + assert.equal(report.closed, 1); + assert.equal(item.status, "closed"); + assert.deepEqual(report.plan.map(row => [row.pr, row.action, row.code, row.candidates]), [[1, "skip", "not_delivery", undefined], [2, "close", "head_checks_passed", undefined]]); + assert.deepEqual(compactCloseoutReport(report).plan.map(row => [row.pr, row.action, row.code]), [[1, "skip", "not_delivery"], [2, "close", "head_checks_passed"]]); + }); +} + +test("fully inspected mentions are context even without any delivery candidate", async () => { + const { report, item } = await selection({ dates: [8, 9], body: "mention", failure: 1 }); + assert.equal(report.closed, 0); + assert.equal(item.status, "in_progress"); + assert.deepEqual(report.plan.map(row => [row.action, row.code, row.candidates]), [["skip", "not_delivery", undefined], ["skip", "not_delivery", undefined]]); +}); + +test("fully inspected context PR 2 with failing CI cannot block delivery PR 1", async () => { + const { report, item } = await selection({ dates: [8, 9], mentionPr: 2, failure: 2 }); + assert.equal(report.closed, 1); + assert.equal(item.status, "closed"); + assert.deepEqual(report.plan.map(row => [row.pr, row.action, row.code, row.candidates]), [[1, "close", "head_checks_passed", undefined], [2, "skip", "not_delivery", undefined]]); + assert.equal((JSON.parse(item.close_reason!) as { pr: number }).pr, 1); +}); diff --git a/test/closeout-workflow.test.ts b/test/closeout-workflow.test.ts index ea86e871..0c635352 100644 --- a/test/closeout-workflow.test.ts +++ b/test/closeout-workflow.test.ts @@ -3,9 +3,11 @@ import assert from "node:assert/strict"; import { spawn, spawnSync } from "node:child_process"; import { appendFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; +import type { ServerResponse } from "node:http"; import { join, resolve } from "node:path"; import test from "node:test"; -import { create, get, update } from "@unbrained/pm-cli/sdk"; +import { runWithReproducibleProcessEnvironment, create, get, update } from "@unbrained/pm-cli/sdk"; +import type { runCloseout } from "../closeout.ts"; import { jsonResponse, startMockGithub } from "./helpers/mock-github-server.ts"; const repoRoot = resolve(import.meta.dirname, ".."); @@ -59,9 +61,60 @@ test.before(async () => { }); test.after(() => rmSync(packedRoot, { recursive: true, force: true })); +const claimKinds = ["closing keyword", "delivery tag", "commit claim", "mention-only", "link-only"] as const; +type ClaimKind = typeof claimKinds[number]; + +/** One cell in the end-to-end item-decision table; only external evidence is synthetic. */ +interface DecisionCase { + selection: "explicit delivery tag" | "single post-criteria claim" | "ambiguous" | "claim kinds"; + primaryClaim: ClaimKind; + competitorClaim: ClaimKind; + competitor: "none" | "valid" | "missing merge SHA" | "failed evidence fetch" | "failed checks" | "pagination cut"; + peer: "none" | "retag mid-run"; +} + +/** Serve distinct PR provenance and schedule a real peer SDK retag after the item snapshot. */ +async function decisionEvidence(url: string, res: ServerResponse, cell: DecisionCase, id: string, pmRoot: string, env: NodeJS.ProcessEnv) { + const number = Number(/\/pulls\/(\d+)/.exec(url)?.[1]); + const headNumber = Number(/\/commits\/([12])/.exec(url)?.[1]); + const head = String(headNumber).repeat(40); + const at = (day: number) => `2026-10-${String(day).padStart(2, "0")}T00:00:00Z`; + if (url.includes("/pulls?")) { + jsonResponse(res, 200, (cell.competitor === "none" ? [2] : [2, 1]).map(n => ({ number: n, merged_at: at(n === 2 ? 8 : 4), updated_at: at(8) }))); + } else if (url.includes("/commits?")) { + const kind = number === 2 ? cell.primaryClaim : cell.competitorClaim; + jsonResponse(res, 200, kind === "commit claim" ? [{ commit: { message: `PM item: ${id}` } }] : []); + } + else if (number) { + if (number === 1 && cell.competitor === "failed evidence fetch") { jsonResponse(res, 403, {}); return; } + if (number === 2 && cell.peer === "retag mid-run" && env.PEER_RETAG_APPLIED !== "true") { + env.PEER_RETAG_APPLIED = "true"; + await update(id, { tags: "delivery-pr:https://github.com/acme/widgets/pull/1", author: "peer" }, { pmRoot, noExtensions: true }); + } + const kind = number === 2 ? cell.primaryClaim : cell.competitorClaim; + const link = `https://github.com/acme/widgets/blob/main/.agents/pm/tasks/${id}.toon`; + const body = kind === "closing keyword" ? `Closes ${link}` : kind === "mention-only" ? `See ${link}` : kind === "link-only" ? link : "Routine work"; + jsonResponse(res, 200, { number, body, + commits: kind === "commit claim" ? 1 : 0, merged: true, merged_at: at(number === 2 ? 8 : 4), + merge_commit_sha: number === 1 && cell.competitor === "missing merge SHA" ? null : (number === 1 ? "a" : "b").repeat(40), + html_url: `https://github.com/acme/widgets/pull/${number}`, head: { sha: String(number).repeat(40) }, base: { ref: "trunk", repo: { full_name: "acme/widgets" } } }); + } else if (url.includes("/protection/")) jsonResponse(res, 200, { contexts: ["CI"], checks: [] }); + else if (url.includes("/check-runs")) jsonResponse(res, 200, { + total_count: headNumber === 1 && cell.competitor === "pagination cut" ? 2 : 1, + check_runs: [{ name: "CI", head_sha: head, status: "completed", conclusion: headNumber === 1 && cell.competitor === "failed checks" ? "failure" : "success", app: { id: 7 } }], + }); + else jsonResponse(res, 200, []); +} + +/** The real caller and publication boundary exposed to workflow assertions. */ +interface WorkflowFixture { + root: string; base: string; pmRoot: string; id: string; env: NodeJS.ProcessEnv; + run: (names: string[], source?: string) => ReturnType; + git: (...args: string[]) => string; +} + /** A real caller with one eligible item, nested npm dependencies, and a local publication boundary. */ -async function fixture(fn: (f: { root: string; base: string; pmRoot: string; id: string; env: NodeJS.ProcessEnv; - run: (names: string[], source?: string) => ReturnType; git: (...args: string[]) => string }) => Promise) { +async function fixture(fn: (f: WorkflowFixture) => Promise, cell?: DecisionCase) { const base = mkdtempSync(join(tmpdir(), "closeout-caller-")); const root = join(base, "caller"); const remote = join(base, "remote.git"); @@ -76,9 +129,19 @@ async function fixture(fn: (f: { root: string; base: string; pmRoot: string; id: command(root, "git", ["init", "--bare", remote], env); git("init", "-b", "trunk"); command(root, cli, ["--pm-path", pmRoot, "init", "fixture"], env); - const item = await create({ title: "Verified delivery", type: "Task", author: "fixture", acceptanceCriteria: "Delivery verified" }, { pmRoot, noExtensions: true }); + const item = await runWithReproducibleProcessEnvironment({ PM_CLOCK: "2026-10-01T00:00:00Z", PM_SEED: "workflow" }, () => create({ title: "Verified delivery", type: "Task", author: "fixture", acceptanceCriteria: "Delivery verified" }, { pmRoot, noExtensions: true })); const id = item.item.id; await update(id, { status: "in_progress" }, { pmRoot, noExtensions: true }); + if (cell?.selection === "single post-criteria claim") await runWithReproducibleProcessEnvironment({ PM_CLOCK: "2026-10-07T00:00:00Z", PM_SEED: "table-criteria" }, () => + update(id, { acceptanceCriteria: "Criteria revised between the two merges" }, { pmRoot, noExtensions: true })); + if (cell?.selection === "explicit delivery tag") await update(id, { tags: "delivery-pr:https://github.com/acme/widgets/pull/2" }, { pmRoot, noExtensions: true }); + if (cell?.selection === "claim kinds") { + const tags = [cell.primaryClaim === "delivery tag" ? 2 : 0, cell.competitor !== "none" && cell.competitorClaim === "delivery tag" ? 1 : 0] + .filter(Boolean).map(n => `delivery-pr:https://github.com/acme/widgets/pull/${n}`); + if (tags.length) await update(id, { tags: tags.join(",") }, { pmRoot, noExtensions: true }); + } + // With no competitor, ambiguity means an explicit selector outside the selected set. + if (cell?.selection === "ambiguous" && cell.competitor === "none") await update(id, { tags: "delivery-pr:https://github.com/acme/widgets/pull/99" }, { pmRoot, noExtensions: true }); const project = join(root, "tools/oracle"); mkdirSync(join(project, "node_modules/@unbrained"), { recursive: true }); mkdirSync(join(project, "node_modules/.bin"), { recursive: true }); @@ -120,20 +183,29 @@ case "$1 $2" in "auth setup-git") ;; "pr list") if [[ -f "$PR_STATE" ]]; then - printf '[{"number":7,"headRepository":{"name":"widgets"},"headRepositoryOwner":{"login":"acme"},"isCrossRepository":false}]' + printf '[{"number":7,"headRepository":{"name":"widgets"},"headRepositoryOwner":{"login":"acme"},"isCrossRepository":false,"autoMergeRequest":%s}]' "$(if [[ -f "$AUTO_STATE" ]]; then printf '{}'; else printf null; fi)" else printf '[{"number":99,"isCrossRepository":true}]'; fi ;; "pr create"|"pr edit") [[ "\${PR_DENIED:-false}" != true ]] || exit 1 touch "$PR_STATE" + if [[ -f "$AUTO_STATE" ]]; then touch "$ARMED_EDIT"; fi while [[ $# -gt 0 ]]; do if [[ "$1" == --body-file ]]; then cp "$2" "$PR_BODY"; break; fi shift done ;; - "pr merge") [[ "\${AUTO_MERGE_DENIED:-false}" != true ]] || exit 1 ;; + "pr merge") + if [[ "$*" == *--disable-auto* ]]; then + [[ "\${DISABLE_DENIED:-false}" != true ]] || exit 1 + [[ -f "$AUTO_STATE" ]] || exit 1 + rm -f "$AUTO_STATE" + else + [[ "\${AUTO_MERGE_DENIED:-false}" != true ]] || exit 1 + touch "$AUTO_STATE" + fi ;; "pr view") printf '{"state":"MERGED"}\\n' ;; "pr close") rm -f "$PR_STATE" ;; "workflow run") [[ "\${DISPATCH_DENIED:-false}" != true ]] || exit 1 ;; - "run list") ;; + "run list") [[ "\${RUN_LIST_DENIED:-false}" != true ]] || exit 1 ;; *) exit 96 ;; esac `, { mode: 0o755 }); @@ -144,16 +216,21 @@ esac EVENT_DEFAULT_BRANCH: "", GITHUB_WORKSPACE: root, NPM_PROJECT_DIR: "tools/oracle", PM_GITHUB_VERSION: version, SYNC_BRANCH: branch, POST_CLOSEOUT_SCRIPT: "changelog:full", CI_WORKFLOWS: "ci.yml", RUNNER_TEMP: base, GITHUB_ENV: join(base, "job-env"), GITHUB_OUTPUT: join(base, "job-output"), GITHUB_PATH: join(base, "job-path"), - PR_STATE: join(base, "pr-state"), PR_BODY: join(base, "pr-body"), RECEIPTS: join(base, "receipts") }); + AUTO_STATE: join(base, "auto-state"), ARMED_EDIT: join(base, "armed-edit"), PR_STATE: join(base, "pr-state"), PR_BODY: join(base, "pr-body"), RECEIPTS: join(base, "receipts") }); const server = await startMockGithub((req, res) => { const url = req.url!; + if (cell) { + void decisionEvidence(url, res, cell, id, pmRoot, env).catch((error: Error) => res.destroy(error)); + return; + } if (url.includes("/check-runs")) env.HEAD_LOOKUPS = String(Number(env.HEAD_LOOKUPS ?? 0) + 1); if (env.PROTECTION_DENIED === "true" && (url.includes("/protection/") || url.includes("/rules/branches/"))) { jsonResponse(res, 403, {}); return; } if (env.READ_DENIED === "true" || (env.CHECKS_DENIED === "true" && url.includes("/check-runs"))) { jsonResponse(res, 403, { message: "Resource not accessible by integration" }); return; } const sha = "a".repeat(40); - if (url.includes("/pulls?")) jsonResponse(res, 200, env.NO_DELIVERY === "true" ? [] : [{ number: 42, merged_at: "2026-10-15T00:00:00Z", updated_at: "2026-10-15T00:00:00Z" }]); - else if (url.endsWith("/pulls/42")) jsonResponse(res, 200, { number: 42, body: env.NO_LINKS === "true" ? "Routine maintenance" : `Closes https://github.com/${env.FOREIGN_LINK === "true" ? "other/project" : "acme/widgets"}/blob/main/.agents/pm/tasks/${id}.toon${env.SUMMARY_ROWS === "true" ? `\nhttps://github.com/other/project/blob/main/.agents/pm/tasks/${id}.toon` : ""}${env.SUMMARY_ITEM ? `\nhttps://github.com/acme/widgets/blob/main/.agents/pm/tasks/${env.SUMMARY_ITEM}.toon` : ""}`, - merged: true, merged_at: "2026-10-15T00:00:00Z", merge_commit_sha: env.INCOMPLETE_MERGE === "true" ? null : "b".repeat(40), html_url: "https://github.com/acme/widgets/pull/42", + if (url.includes("/pulls?")) jsonResponse(res, 200, env.NO_DELIVERY === "true" ? [] : [...(env.AMBIGUOUS === "true" ? [{ number: 43, merged_at: "2026-10-16T00:00:00Z", updated_at: "2026-10-16T00:00:00Z" }] : []), { number: 42, merged_at: env.UNMERGED_COMPETITOR === "true" ? null : "2026-10-15T00:00:00Z", updated_at: "2026-10-15T00:00:00Z" }]); + else if (url.includes("/commits?")) jsonResponse(res, 200, []); + else if (/\/pulls\/(42|43)$/.test(url)) jsonResponse(res, 200, { number: Number(url.split("/").at(-1)), body: env.NO_LINKS === "true" ? "Routine maintenance" : `Closes https://github.com/${env.FOREIGN_LINK === "true" ? "other/project" : "acme/widgets"}/blob/main/.agents/pm/tasks/${id}.toon${env.SUMMARY_ROWS === "true" ? `\nhttps://github.com/other/project/blob/main/.agents/pm/tasks/${id}.toon` : ""}${env.SUMMARY_ITEM ? `\nhttps://github.com/acme/widgets/blob/main/.agents/pm/tasks/${env.SUMMARY_ITEM}.toon` : ""}`, + merged: !(env.UNMERGED_COMPETITOR === "true" && url.endsWith("/42")), commits: 0, merged_at: env.UNMERGED_COMPETITOR === "true" && url.endsWith("/42") ? null : "2026-10-15T00:00:00Z", merge_commit_sha: env.INCOMPLETE_MERGE === "true" ? null : "b".repeat(40), html_url: `https://github.com/acme/widgets/pull/${url.split("/").at(-1)}`, head: { sha }, base: { ref: "trunk", repo: { full_name: "acme/widgets" } } }); else if (url.includes("/protection/")) jsonResponse(res, 200, { contexts: ["CI"], checks: [] }); else if (url.includes("/rules/branches/")) jsonResponse(res, 200, []); @@ -165,7 +242,21 @@ esac else jsonResponse(res, 404, {}); }); Object.assign(env, { PM_GITHUB_API_BASE: server.baseUrl }); - const run = (names: string[], source = workflow) => bash(names.map(name => `( + // Model the workflow's always() cleanup even when an ordinary step exits early. + const run = (names: string[], source = workflow) => bash((names.includes("Disarm previous closeout auto-merge") + ? ': > "$GITHUB_ENV"\n' : "") + ` +cleanup_landing() { + status=$? + set -a + if [[ -f "$GITHUB_ENV" ]]; then source "$GITHUB_ENV"; fi + set +a + if [[ "$status" != 0 || "\${CLOSEOUT_AUTOMERGE_ALLOWED:-}" != true ]]; then + ( ${shell("Disarm closeout auto-merge on unsuccessful landing", source)} ) || status=1 + fi + exit "$status" +} +trap cleanup_landing EXIT +` + names.map(name => `( set -a if [[ -f "$GITHUB_ENV" ]]; then source "$GITHUB_ENV"; fi set +a @@ -180,7 +271,7 @@ ${shell(name, source)} } } -const setup = ["Require caller default branch", "Validate closeout inputs", "Validate npm project directory", "Prepare sync branch from default branch", +const setup = ["Disarm previous closeout auto-merge", "Require caller default branch", "Validate closeout inputs", "Validate npm project directory", "Prepare sync branch from default branch", "Install dependencies", "Resolve sync commit scope", "Require clean setup artifacts", "Install pinned pm-github extension", "Configure PM merge drivers", "Isolate workflow setup artifacts", "Verify gate is available"]; const finish = ["Close delivered tracker items", "Run caller post-closeout script", "Gate tracker and regenerated changelog", "Verify strict pm health", "Validate publication credentials", "Commit gated closeout changes and open review PR", "Dispatch caller CI and report parked runs"]; @@ -275,9 +366,8 @@ test("permission-limited read token fails closed with a clear diagnostic", async assert.equal((await get(f.id, {}, { pmRoot: f.pmRoot, noExtensions: true })).item.status, "in_progress"); Object.assign(f.env, { READ_DENIED: "false", CHECKS_DENIED: "true" }); const checks = await f.run(finish); - assert.notEqual(checks.code, 0); - assert.match(checks.out, /Closeout refused; check token read access/); - unpublished(f.git); + assert.equal(checks.code, 0, checks.out); + assert.match(readFileSync(f.env.PR_BODY!, "utf8"), /missing_evidence/); assert.equal((await get(f.id, {}, { pmRoot: f.pmRoot, noExtensions: true })).item.status, "in_progress"); })); @@ -342,7 +432,7 @@ test("behavioural revert: removing only the no-op guard opens a PR with zero clo const prepared = await f.run(setup); assert.equal(prepared.code, 0, prepared.out); await update(f.id, { title: "Caller observation without delivery" }, { pmRoot: f.pmRoot, noExtensions: true }); - const mutant = workflow.replace('if [[ "${CLOSEOUT_CLOSED}" == 0 ]] || git diff --cached --quiet -- "${sync_scope[0]}" "${sync_scope[@]:2}"; then', "if false; then"); + const mutant = workflow.replace('if [[ "${CLOSEOUT_CLOSED}" == 0 && "${CLOSEOUT_REVIEW:-0}" == 0 ]] || git diff --cached --quiet -- "${sync_scope[0]}" "${sync_scope[@]:2}"; then', "if false; then"); assert.notEqual(mutant, workflow); const result = await f.run(finish, mutant); assert.equal(result.code, 0, result.out); @@ -388,14 +478,14 @@ for (const mode of ["unlinked", "foreign", "terminal", "local", "incomplete", "u if (mode.includes("incomplete")) f.env.INCOMPLETE_MERGE = "true"; if (mode === "terminal") await update(f.id, { status: "canceled" }, { pmRoot: f.pmRoot, noExtensions: true }); const result = await f.run([...setup, ...finish]); - assert.equal(result.code, mode === "local" || mode === "incomplete" ? 1 : 0, result.out); - if (mode === "local") assert.match(result.out, /token read access to head checks for linked local tracker items/); + assert.equal(result.code, 0, result.out); + if (mode === "local") assert.match(readFileSync(f.env.PR_BODY!, "utf8"), /missing_evidence/); if (mode === "incomplete") { - assert.match(result.out, /incomplete merged-PR metadata blocks a linked local tracker item/); + assert.match(readFileSync(f.env.PR_BODY!, "utf8"), /incomplete_merge/); assert.equal(f.env.HEAD_LOOKUPS, undefined); } - unpublished(f.git); - assert.equal(existsSync(f.env.PR_STATE!), false); + if (mode !== "local" && mode !== "incomplete") unpublished(f.git); + assert.equal(existsSync(f.env.PR_STATE!), mode === "local" || mode === "incomplete"); })); } @@ -428,12 +518,12 @@ for (const mode of ["undeclared", "blank", "missing", "neutral", "skipped", "pen const item = (await get(f.id, {}, { pmRoot: f.pmRoot, noExtensions: true })).item; assert.match(String(item.actual_result), mode === "metadata" ? /required checks satisfied/ : /caller-declared checks concluded success; repository required-check metadata was unavailable; successful head checks: CI/); } else { - assert.equal(report.plan[0]!.code, mode === "undeclared" || mode === "blank" ? "required_checks_unverified" : "head_checks_failed"); - if (mode === "undeclared" || mode === "blank") assert.equal(result.code, 1, result.out); + assert.equal(report.plan[0]!.code, "needs_review"); + assert.equal(result.code, 0, result.out); assert.equal(readFileSync(join(f.pmRoot, "tasks", `${f.id}.toon`), "utf8"), before); - assert.equal(existsSync(f.env.PR_STATE!), false); - unpublished(f.git); - assert.doesNotMatch(readFileSync(f.env.RECEIPTS!, "utf8"), /pr (create|edit)|workflow run/); + assert.equal(existsSync(f.env.PR_STATE!), true); + assert.match(readFileSync(f.env.PR_BODY!, "utf8"), /"action": "needs_review"/); + assert.doesNotMatch(readFileSync(f.env.PR_BODY!, "utf8"), /"action": "close"/); } })); } @@ -459,3 +549,229 @@ for (const [scenario, names, input] of [ assert.equal(existsSync(join(f.root, "injected")), false); })); } + + +test("ambiguous deliveries open a review-only PR listing every candidate and leave the item open", async () => fixture(async f => { + f.env.AMBIGUOUS = "true"; + f.env.APP_DEADLINE = String(Math.floor(Date.now() / 1000) + 600); + const result = await f.run([...setup, ...finish]); + assert.equal(result.code, 0, result.out); + assert.equal((await get(f.id, {}, { pmRoot: f.pmRoot, noExtensions: true })).item.status, "in_progress"); + const body = readFileSync(f.env.PR_BODY!, "utf8"); + assert.match(body, /"action": "needs_review"/); + assert.match(body, /"candidates": \[\s*42,\s*43\s*\]/); + assert.match(body, /closeout-review\.json/); + assert.doesNotMatch(readFileSync(f.env.RECEIPTS!, "utf8"), /pr merge .*--auto/); + assert.match(result.out, /automatic landing is disabled/); + const published = f.git("show", `refs/remotes/origin/${branch}:.agents/pm/closeout-review.json`); + assert.deepEqual(JSON.parse(published), { needs_review: [{ item: f.id, action: "needs_review", candidates: [42, 43] }] }); +})); + + +test("earlier armed auto-merge is disabled before a later hold updates the reused PR", async () => fixture(async f => { + f.env.APP_DEADLINE = String(Math.floor(Date.now() / 1000) + 600); + const first = await f.run([...setup, ...finish]); + assert.equal(first.code, 0, first.out); + assert.equal(existsSync(f.env.AUTO_STATE!), true); + f.env.AMBIGUOUS = "true"; + const second = await f.run([...setup, ...finish]); + assert.equal(second.code, 0, second.out); + assert.equal(existsSync(f.env.AUTO_STATE!), false); + assert.equal(existsSync(f.env.ARMED_EDIT!), false, "held branch must be disarmed before updating its PR"); + assert.match(readFileSync(f.env.PR_BODY!, "utf8"), /Automatic merge is disabled because delivery attribution needs_review/); + assert.match(readFileSync(f.env.RECEIPTS!, "utf8"), /pr merge 7 -R acme\/widgets --disable-auto/); +})); + +for (const mode of ["guard", "setup", "evidence", "privacy", "no-op", "empty-ci", "dispatch", "maintenance", "disable-denied"]) { + test(`previous auto-merge is revoked on workflow exit: ${mode}`, async () => fixture(async f => { + f.env.APP_DEADLINE = String(Math.floor(Date.now() / 1000) + 600); + const first = await f.run([...setup, ...finish]); + assert.equal(first.code, 0, first.out); + assert.equal(existsSync(f.env.AUTO_STATE!), true); + if (mode === "guard") f.env.RUN_REF = "refs/heads/feature"; + if (mode === "setup") f.env.NPM_PROJECT_DIR = "../outside"; + if (mode === "evidence") f.env.READ_DENIED = "true"; + if (mode === "privacy") f.env.SUMMARY_ITEM = "ghp_" + "A".repeat(36); + if (mode === "no-op") f.env.NO_DELIVERY = "true"; + if (mode === "empty-ci") f.env.CI_WORKFLOWS = ""; + if (mode === "dispatch") f.env.DISPATCH_DENIED = "true"; + if (mode === "maintenance") f.env.RUN_LIST_DENIED = "true"; + if (mode === "disable-denied") f.env.DISABLE_DENIED = "true"; + const priorBranch = f.git("ls-remote", "origin", `refs/heads/${branch}`); + const second = await f.run([...setup, ...finish]); + assert.equal(second.code, ["no-op", "empty-ci"].includes(mode) ? 0 : 1, second.out); + assert.equal(existsSync(f.env.AUTO_STATE!), mode === "disable-denied"); + assert.equal(existsSync(f.env.ARMED_EDIT!), false); + if (mode === "disable-denied") assert.equal(f.git("ls-remote", "origin", `refs/heads/${branch}`), priorBranch); + })); +} + + +test("confirmed unmerged references cannot block publication of a unique merged delivery", async () => fixture(async f => { + f.env.AMBIGUOUS = "true"; + f.env.UNMERGED_COMPETITOR = "true"; + const result = await f.run([...setup, ...finish]); + assert.equal(result.code, 0, result.out); + assert.equal((await get(f.id, {}, { pmRoot: f.pmRoot, noExtensions: true })).item.status, "closed"); + const body = readFileSync(f.env.PR_BODY!, "utf8"); + assert.match(body, /"action": "close"/); + assert.doesNotMatch(body, /"action": "needs_review"|"code": "incomplete_merge"/); + assert.match(body, /"not_merged": 1/); +})); + + +/** Reuse immutable input baselines; the workflow resets Git and tracker state before every cell. */ +interface SharedDecisionFixture { + fixture: WorkflowFixture; + cell: DecisionCase; + environment: string; + setupFiles: { path: string; contents: Buffer }[]; + release: () => void; + done: Promise; +} +const decisionFixtures = new Map>(); + +/** Retain real SDK/CLI/publication behavior while avoiding redundant fixture creation. */ +async function decisionFixture(cell: DecisionCase): Promise { + // A missing competitor needs its own unresolved-selector baseline for the ambiguous source. + const key = `${cell.selection}/${cell.selection === "ambiguous" && cell.competitor === "none"}/${cell.selection === "claim kinds" && cell.primaryClaim === "delivery tag"}/${cell.selection === "claim kinds" && cell.competitor !== "none" && cell.competitorClaim === "delivery tag"}`; + let pending = decisionFixtures.get(key); + if (!pending) { + let release = () => {}; + const lifetime = new Promise(resolveLifetime => { release = resolveLifetime; }); + const sharedCell = { ...cell }; + pending = new Promise((resolveReady, reject) => { + const done = fixture(async f => { + const prepared = await f.run(setup); + assert.equal(prepared.code, 0, prepared.out); + const setupFiles = [join(f.pmRoot, "settings.json"), join(f.pmRoot, "history/_workspace.jsonl")] + .filter(existsSync).map(path => ({ path, contents: readFileSync(path) })); + resolveReady({ fixture: f, cell: sharedCell, release, done, + environment: readFileSync(f.env.GITHUB_ENV!, "utf8"), setupFiles }); + await lifetime; + }, sharedCell); + void done.catch(reject); + }); + decisionFixtures.set(key, pending); + } + const shared = await pending; + Object.assign(shared.cell, cell); + const f = shared.fixture; + // Restore the disposable input even when a previous mutation-control cell failed before publication. + f.git("reset", "--hard", "trunk"); + // Restore only workflow-owned setup inputs; publication restores these before committing. + for (const file of shared.setupFiles) writeFileSync(file.path, file.contents); + writeFileSync(f.env.GITHUB_ENV!, shared.environment); + delete f.env.PEER_RETAG_APPLIED; + // A prior cell's PR receipt must never count as publication by the current cell. + for (const path of [f.env.PR_STATE!, f.env.PR_BODY!]) rmSync(path, { force: true }); + writeFileSync(f.env.RECEIPTS!, ""); + return f; +} + +test.after(async () => { + for (const pending of decisionFixtures.values()) { + const shared = await pending; + shared.release(); + await shared.done; + } +}); + + +/** Retain all 36 selection/peer rows and cross both claim dimensions with all six evidence states. */ +const decisionCases: DecisionCase[] = []; +for (const selection of ["explicit delivery tag", "single post-criteria claim", "ambiguous"] as const) { + for (const competitor of ["none", "valid", "missing merge SHA", "failed evidence fetch", "failed checks", "pagination cut"] as const) { + for (const peer of ["none", "retag mid-run"] as const) decisionCases.push({ selection, competitor, peer, primaryClaim: selection === "ambiguous" && competitor === "none" ? "mention-only" : "closing keyword", competitorClaim: "closing keyword" }); + } +} +for (const primaryClaim of claimKinds) for (const competitorClaim of claimKinds) { + for (const competitor of ["none", "valid", "missing merge SHA", "failed evidence fetch", "failed checks", "pagination cut"] as const) { + decisionCases.push({ selection: "claim kinds", competitor, peer: "none", primaryClaim, competitorClaim }); + } +} + +/** Expected winners are defined from the table inputs, independently of the closeout implementation. */ +function expectedDecision(cell: DecisionCase): { chosen?: number; candidates: number[]; context: number[]; held: boolean } { + const { selection, competitor, peer, primaryClaim, competitorClaim } = cell; + if (selection !== "claim kinds") { + // Preserve the earlier mention-only input with an unmatched selector: it is context, not ambiguity. + if (selection === "ambiguous" && competitor === "none") return { candidates: [], context: [2], held: false }; + const closes = selection !== "ambiguous" && (peer === "retag mid-run" ? selection === "explicit delivery tag" && competitor === "valid" + : selection === "explicit delivery tag" || competitor !== "failed evidence fetch"); + return { chosen: closes ? peer === "retag mid-run" ? 1 : 2 : undefined, candidates: competitor === "none" ? [2] : [1, 2], context: [], held: !closes }; + } + const primary = !["mention-only", "link-only"].includes(primaryClaim); + const other = competitor !== "none" && (!["mention-only", "link-only"].includes(competitorClaim) || competitor === "failed evidence fetch"); + const tags = [primaryClaim === "delivery tag" ? 2 : 0, competitor !== "none" && competitorClaim === "delivery tag" ? 1 : 0].filter(Boolean); + const candidates = [...(other ? [1] : []), ...(primary ? [2] : [])]; + const context = [...(competitor !== "none" && !other ? [1] : []), ...(!primary ? [2] : [])]; + const selected = tags.length === 1 ? tags[0] : tags.length === 0 && candidates.length === 1 ? candidates[0] : undefined; + const chosen = selected === 2 || (selected === 1 && competitor === "valid") ? selected : undefined; + return { chosen, candidates, context, held: candidates.length > 0 && chosen === undefined }; +} + +for (const cell of decisionCases) { + const { selection, competitor, peer, primaryClaim, competitorClaim } = cell; + const expected = expectedDecision(cell); + test(`item decision end to end: ${selection} / ${competitor} / ${peer} / primary=${primaryClaim} / competitor=${competitorClaim}`, async () => { + const f = await decisionFixture(cell); + const result = await f.run(["Prepare sync branch from default branch", "Close delivered tracker items", "Commit gated closeout changes and open review PR"]); + const report = JSON.parse(readFileSync(join(f.base, "pm-github-closeout.json"), "utf8")) as Awaited>; + const item = (await get(f.id, {}, { pmRoot: f.pmRoot, noExtensions: true })).item; + assert.equal(f.env.PEER_RETAG_APPLIED === "true", peer === "retag mid-run"); + assert.equal(result.code, 0, result.out); + const closes = expected.chosen !== undefined; + const publishes = closes || expected.held; + assert.equal(existsSync(f.env.PR_STATE!), publishes, "only a close or hold publishes tracker changes"); + assert.equal(item.status, closes ? "closed" : "in_progress", JSON.stringify(report)); + assert.equal(report.closed, closes ? 1 : 0); + assert.equal(report.failed, 0); + const published = publishes ? `refs/remotes/origin/${branch}` : "trunk"; + const itemPath = `.agents/pm/tasks/${f.id}.toon`; + const publishedItem = f.git("show", `${published}:${itemPath}`); + const localItem = readFileSync(join(f.root, itemPath), "utf8").trim(); + if (!publishes && peer === "retag mid-run") { + assert.notEqual(publishedItem, localItem, "a peer edit alone stays local without a closeout publication"); + assert.deepEqual(item.tags, ["delivery-pr:https://github.com/acme/widgets/pull/1"]); + } else assert.equal(publishedItem, localItem); + const history = f.git("show", `${published}:.agents/pm/history/${f.id}.jsonl`); + assert.equal(history.split('"op":"close"').length - 1, closes ? 1 : 0); + const body = publishes ? readFileSync(f.env.PR_BODY!, "utf8") : ""; + assert.deepEqual(report.plan.filter(row => row.code === "not_delivery").map(row => row.pr).sort(), expected.context); + for (const row of report.plan.filter(row => row.code === "not_delivery")) { + assert.equal(row.action, "skip"); + assert.equal(row.candidates, undefined); + assert.equal(row.missing_evidence, undefined); + if (publishes) assert.match(body, new RegExp(`"pr": ${row.pr},\\s*"action": "skip",\\s*"code": "not_delivery"`)); + } + if (closes) { + const chosen = expected.chosen!; + const provenance = JSON.parse(item.close_reason!) as { pr: number; url: string; head_sha: string; merge_commit_sha: string; merged_at: string; checks: string[] }; + assert.deepEqual(provenance, { pr: chosen, url: `https://github.com/acme/widgets/pull/${chosen}`, head_sha: String(chosen).repeat(40), + merge_commit_sha: (chosen === 1 ? "a" : "b").repeat(40), merged_at: `2026-10-0${chosen === 1 ? 4 : 8}T00:00:00Z`, checks: ["CI"] }); + assert.equal(item.completed_at, provenance.merged_at.replace("Z", ".000Z")); + assert.match(item.resolution!, new RegExp(`pull/${chosen} `)); + assert.equal(item.expected_result, item.acceptance_criteria); + assert.ok(item.actual_result!.includes(provenance.merged_at)); + assert.deepEqual(report.plan.filter(row => row.code !== "not_delivery").sort((a, b) => a.pr - b.pr).map(row => [row.pr, row.action, row.code]), + expected.candidates.map(pr => [pr, pr === chosen ? "close" : "skip", pr === chosen ? "head_checks_passed" : "duplicate_selection"])); + assert.match(body, new RegExp(`"pr": ${chosen},\\s*"action": "close"`)); + assert.doesNotMatch(body, /"action": "needs_review"|"action": "refuse"/); + assert.equal(f.git("ls-tree", "--name-only", published, ".agents/pm/closeout-review.json"), ""); + } else if (expected.held) { + const held = report.plan.filter(row => row.code !== "not_delivery").sort((a, b) => a.pr - b.pr); + assert.deepEqual(held.map(row => [row.pr, row.action, row.code, row.candidates]), + expected.candidates.map(pr => [pr, "needs_review", "needs_review", expected.candidates])); + for (const row of held) assert.ok(row.missing_evidence?.every(gap => expected.candidates.includes(gap.pr)) ?? true); + const review = JSON.parse(f.git("show", `${published}:.agents/pm/closeout-review.json`)) as { needs_review: { candidates: number[] }[] }; + assert.deepEqual(review.needs_review.map(row => row.candidates), [expected.candidates]); + assert.match(body, /"action": "needs_review"/); + assert.doesNotMatch(body, /"action": "close"|"action": "refuse"/); + } else { + assert.ok(report.plan.every(row => row.action === "skip" && row.code === "not_delivery")); + assert.equal(f.git("ls-tree", "--name-only", published, ".agents/pm/closeout-review.json"), ""); + } + if (!closes) assert.doesNotMatch(readFileSync(f.env.RECEIPTS!, "utf8"), /pr merge .*--auto/); + }); +} diff --git a/test/closeout.test.ts b/test/closeout.test.ts index 4422b9b4..d4868338 100644 --- a/test/closeout.test.ts +++ b/test/closeout.test.ts @@ -17,7 +17,7 @@ const cli = join(import.meta.dirname, "../node_modules/.bin/pm"); const repo = "fixture/project"; const head = "a".repeat(40); const merge = "b".repeat(40); -const mergedAt = "2026-10-13T12:30:00Z"; +const mergedAt = "2099-10-13T12:30:00Z"; const bareLink = (id: string, repository = repo) => `https://github.com/${repository}/blob/main/.agents/pm/tasks/${id}.toon`; /** Explicit delivery declarations used by all positive closeout fixtures. */ @@ -25,7 +25,7 @@ const link = (id: string, repository = repo) => `Closes ${bareLink(id, repositor /** Complete synthetic merged PR; prose is never trusted as closure evidence. */ function pull(body: string | null, overrides: Record = {}) { - return { number: 42, body, merged: true, merged_at: mergedAt, merge_commit_sha: merge, + return { number: 42, body, merged: true, commits: 0, merged_at: mergedAt, merge_commit_sha: merge, html_url: `https://github.com/${repo}/pull/42`, head: { sha: head }, base: { ref: "main", repo: { full_name: repo } }, ...overrides }; } @@ -42,7 +42,8 @@ function fixture(body: string | null, override?: MockGithubHandler): MockGithubH if (response.writableEnded) return; } const url = request.url!; - if (url.includes("/pulls/")) jsonResponse(response, 200, pull(body, { number: Number(url.split("/").at(-1)), html_url: `https://github.com/${repo}/pull/${url.split("/").at(-1)}` })); + if (url.includes("/commits?")) jsonResponse(response, 200, []); + else if (url.includes("/pulls/")) jsonResponse(response, 200, pull(body, { number: Number(url.split("/").at(-1)), html_url: `https://github.com/${repo}/pull/${url.split("/").at(-1)}` })); else if (url.includes("/rules/branches/")) jsonResponse(response, 200, []); else if (url.includes("/protection/")) jsonResponse(response, 200, { contexts: ["CI"], checks: [{ context: "CI", app_id: 7 }] }); else if (url.includes("/check-runs")) jsonResponse(response, 200, { total_count: 1, check_runs: [run()] }); @@ -142,7 +143,7 @@ test("apply persists only exact GitHub reason facts; repeated selection writes z assert.equal(item.status, "closed"); assert.equal(item.close_reason, JSON.stringify({ pr: 42, url: `https://github.com/${repo}/pull/42`, merge_commit_sha: merge, head_sha: head, merged_at: mergedAt, checks: ["CI"] })); - assert.equal(item.completed_at, "2026-10-13T12:30:00.000Z"); + assert.equal(item.completed_at, "2099-10-13T12:30:00.000Z"); assert.equal(item.resolution, `Delivered by https://github.com/${repo}/pull/42 (merge ${merge.slice(0, 12)}, head ${head.slice(0, 12)})`); assert.equal(item.expected_result, "Verified delivery"); assert.equal(item.actual_result, `Merged at ${mergedAt}; required checks satisfied; successful head checks: CI`); @@ -202,7 +203,7 @@ test("apply continues after lock and policy refusals and counts only successful const result = await closeout(root, { pr: "42", apply: true }); assert.equal(result.closed, 2); assert.equal(result.failed, 2); - assert.deepEqual(result.plan.map(row => row.action), ["skip", "close", "skip", "close"]); + assert.deepEqual(result.plan.map(row => row.action), ["needs_review", "close", "needs_review", "close"]); assert.equal(result.plan[0]!.reason, "SDK close refused: lock_conflict."); assert.match(result.plan[2]!.reason, /workflow_policy/); assert.ok(!JSON.stringify(result).includes("private-lock-owner") && !JSON.stringify(result).includes("Private policy") && !JSON.stringify(result).includes(root)); @@ -231,21 +232,21 @@ test("public apply reports partial results as CommandError exit 1 after remainin const report = JSON.parse((error as Error).message) as { closed: number; failed: number; plan: { action: string; reason: string }[] }; assert.equal(report.closed, 1); assert.equal(report.failed, 1); - assert.deepEqual(report.plan.map(row => row.action), ["skip", "close"]); + assert.deepEqual(report.plan.map(row => row.action), ["needs_review", "close"]); assert.equal(report.plan[0]!.reason, "SDK close refused: lock_conflict."); return true; }); assert.equal((await get(ids[1]!, {}, { pmRoot: root, noExtensions: true })).item.status, "closed"); - assert.equal(server.requests.filter(request => request.url.includes("/pulls/")).length, 1); + assert.equal(server.requests.filter(request => /\/pulls\/\d+$/.test(request.url)).length, 1); })); } finally { await unlock(); } })); -test("equal merge times select the higher PR number before writes", async () => tracker(async (root, ids) => { +test("equal merge times require review before writes", async () => tracker(async (root, ids) => { await withMockGithub(fixture(link(ids[0]!)), async () => { const report = await closeout(root, { pr: ["43", "42"], apply: true }); - assert.equal(report.closed, 1); - assert.deepEqual(report.plan.map(row => [row.pr, row.action, row.code]), [[42, "skip", "duplicate_selection"], [43, "close", "head_checks_passed"]]); + assert.equal(report.closed, 0); + assert.deepEqual(report.plan.map(row => [row.pr, row.action, row.code]), [[42, "needs_review", "needs_review"], [43, "needs_review", "needs_review"]]); }); })); @@ -272,14 +273,14 @@ test("legacy required statuses use newest context and cannot bypass a failed run if (request.url!.includes("/statuses")) jsonResponse(response, 200, [{ context: "legacy", state }, { context: "legacy", state: "failure" }]); }), async () => { const report = await closeout(root); - assert.equal(report.plan[0]!.action, state === "success" ? "close" : "skip"); + assert.equal(report.plan[0]!.action, state === "success" ? "close" : "needs_review"); if (state === "success") assert.deepEqual(report.checks[0]!.passed, ["CI", "legacy"]); }); } await withMockGithub(fixture(link(ids[0]!), (request, response) => { if (request.url!.includes("/protection/")) jsonResponse(response, 200, { contexts: ["CI"], checks: [{ context: "CI", app_id: -1 }] }); if (request.url!.includes("/statuses")) jsonResponse(response, 200, [{ context: "CI", state: "failure" }]); - }), async () => assert.equal((await closeout(root)).plan[0]!.action, "skip")); + }), async () => assert.equal((await closeout(root)).plan[0]!.action, "needs_review")); })); for (const protection of [403, 404, "empty"] as const) { @@ -292,8 +293,8 @@ for (const protection of [403, 404, "empty"] as const) { }), async () => { const report = await closeout(root); assert.equal(report.checks[0]!.rule, protection === "empty" ? "fallback" : "unavailable"); - assert.equal(report.plan[0]!.action, protection !== "empty" || conclusion === "failure" || conclusion === null ? "skip" : "close"); - assert.equal(report.plan[0]!.code, protection !== "empty" ? "required_checks_unverified" : conclusion === "failure" || conclusion === null ? "head_checks_failed" : "head_checks_passed"); + assert.equal(report.plan[0]!.action, protection !== "empty" || conclusion === "failure" || conclusion === null ? "needs_review" : "close"); + assert.equal(report.plan[0]!.code, protection !== "empty" || conclusion === "failure" || conclusion === null ? "needs_review" : "head_checks_passed"); assert.equal(snapshot(root), before); }); } @@ -323,19 +324,24 @@ for (const [name, data] of Object.entries({ noRuns: { total_count: 0, check_runs const before = snapshot(root); const report = await closeout(root, { pr: "42", apply: true }); assert.equal(report.closed, 0); - assert.equal(report.plan[0]!.action, "skip"); + assert.equal(report.plan[0]!.action, "needs_review"); assert.equal(snapshot(root), before); }); })); } -test("unmerged PRs and incomplete merge facts skip; null body contributes no references", async () => tracker(async (root, ids) => { +test("confirmed unmerged PRs are routine skips and incomplete merge facts hold; null body contributes no references", async () => tracker(async (root, ids) => { for (const changes of [{ merged: false }, { merged_at: null }, { merge_commit_sha: null }, { merge_commit_sha: "bad" }, { head: { sha: "bad" } }]) { await withMockGithub(fixture(link(ids[0]!), (request, response) => { if (request.url!.includes("/pulls/")) jsonResponse(response, 200, pull(link(ids[0]!), changes)); }), async server => { - assert.equal((await closeout(root, { pr: "42", apply: true })).closed, 0); - assert.equal(server.requests.length, 1); + const report = await closeout(root, { pr: "42", apply: true }); + assert.equal(report.closed, 0); + if ("merged" in changes && changes.merged === false) { + assert.equal(report.plan[0]!.code, "not_merged"); + assert.deepEqual(compactCloseoutReport(report).skipped, { not_merged: 1 }); + } + assert.equal(server.requests.filter(request => !request.url.includes("/commits?")).length, 1); }); } await withMockGithub(fixture(null), async () => assert.deepEqual((await closeout(root)).plan, [])); @@ -343,15 +349,16 @@ test("unmerged PRs and incomplete merge facts skip; null body contributes no ref test("since drains every page and uses inclusive merge time rather than update time", async () => tracker(async (root, ids) => { await withMockGithub(fixture(link(ids[0]!), (request, response, _body, base) => { + if (request.url!.endsWith("/pulls/40")) jsonResponse(response, 200, pull(link(ids[0]!), { number: 40, html_url: `https://github.com/${repo}/pull/40`, merged: false })); if (request.url!.includes("/pulls?")) { const page = new URL(request.url!, base).searchParams.get("page"); jsonResponse(response, 200, page === "2" ? [{ number: 42, merged_at: mergedAt }] : [ - { number: 40, merged_at: null }, { number: 41, merged_at: "2026-10-12T00:00:00Z" }], + { number: 40, merged_at: null }, { number: 41, merged_at: "2099-10-12T00:00:00Z" }], page === "2" ? {} : { Link: nextLinkHeader(base, "/repos/fixture/project/pulls?state=closed&page=2") }); } }), async server => { const report = await closeout(root, { since: mergedAt }); - assert.equal(report.plan[0]!.pr, 42); + assert.equal(report.plan.find(row => row.action === "close")!.pr, 42); assert.equal(server.requests.filter(request => request.url.includes("/pulls?")).length, 2); }); })); @@ -371,8 +378,8 @@ test("head check and status pagination retains all required evidence", async () test("invalid selectors fail before HTTP or writes", async () => tracker(async (root) => { await withMockGithub(fixture(null), async server => { for (const options of [{}, { pr: [] }, { pr: false }, { pr: "" }, { pr: ["42", 43] }, { pr: "0" }, { pr: "1.2" }, { pr: "9007199254740992" }, - { pr: "42", since: "2026-10-01" }, { since: "yesterday" }, { since: "2026-02-30" }, { since: "2026-10-01T12:00:00" }, - { since: "2026-10-01T99:00:00Z" }, { since: false }, { since: "" }, { pr: "42", skip: [] }, { pr: "42", skip: true }, { pr: "42", "required-checks": ["CI", 42] }, { pr: "42", "required-checks": false }]) { + { pr: "42", since: "2099-10-01" }, { since: "yesterday" }, { since: "2099-02-30" }, { since: "2099-10-01T12:00:00" }, + { since: "2099-10-01T99:00:00Z" }, { since: false }, { since: "" }, { pr: "42", skip: [] }, { pr: "42", skip: true }, { pr: "42", "required-checks": ["CI", 42] }, { pr: "42", "required-checks": false }]) { await assert.rejects(() => closeout(root, options), CloseoutUsageError); } for (const repository of [undefined, "invalid", "bad/repo/name", "./repo", "../repo", "fixture/.", "fixture/.."]) await assert.rejects(() => runCloseout(root, repository, { pr: "42" }, api()), CloseoutUsageError); @@ -446,8 +453,9 @@ test("since validates selected numbers and permits an empty selection", async () await withMockGithub(fixture(null, (request, response) => { if (request.url!.includes("/pulls?")) jsonResponse(response, 200, selected); }), async () => { - if (selected.length === 0) assert.deepEqual((await closeout(root, { since: "2026-10-13" })).plan, []); - else await assert.rejects(() => closeout(root, { since: "2026-10-13" })); + if (selected.length === 0) assert.deepEqual((await closeout(root, { since: "2099-10-13" })).plan, []); + else if (selected[0]?.number === 0) await assert.rejects(() => closeout(root, { since: "2099-10-13" })); + else assert.deepEqual((await closeout(root, { since: "2099-10-13" })).plan, []); }); } })); @@ -459,8 +467,8 @@ test("public command registration reuses repo resolution and shared transport", assert.equal((result.result as { plan: { action: string }[] }).plan[0]!.action, "close"); const compact = await harness.runCommand({ command: "github closeout", pmRoot: root, global: { json: false }, options: { pr: ["42", "43", "44"] } }); - assert.deepEqual((compact.result as { plan: { pr: number; action: string }[] }).plan.map(row => [row.pr, row.action]), [[44, "close"]]); - assert.deepEqual((compact.result as { skipped: object }).skipped, { duplicate_selection: 2 }); + assert.deepEqual((compact.result as { plan: { pr: number; action: string }[] }).plan.map(row => [row.pr, row.action]), [[42, "needs_review"], [43, "needs_review"], [44, "needs_review"]]); + assert.deepEqual((compact.result as { skipped: object }).skipped, {}); const full = await harness.runCommand({ command: "github closeout", pmRoot: root, global: { json: false }, options: { pr: ["42", "43", "44"], full: true } }); assert.equal((full.result as { plan: object[] }).plan.length, 3); @@ -474,14 +482,14 @@ test("public command registration reuses repo resolution and shared transport", (error: unknown) => (error as { exitCode?: number }).exitCode === 2 && /positive pull-request numbers/.test((error as Error).message)); await assert.rejects(harness.runCommand({ command: "github closeout", pmRoot: root, options: { pr: ["42"] } }), (error: unknown) => (error as { exitCode?: number }).exitCode === 1 && /Invalid GitHub evidence object/.test((error as Error).message)); - assert.equal(server.requests.filter(request => request.url.includes("/pulls/")).length, 1); + assert.equal(server.requests.filter(request => /\/pulls\/\d+$/.test(request.url)).length, 1); })); })); test("since stops paging once a row was last updated before the cutoff", async () => tracker(async (root, ids) => { await withMockGithub(fixture(link(ids[0]!), (request, response, _body, base) => { if (request.url!.includes("/pulls?")) { - jsonResponse(response, 200, [{ number: 42, merged_at: mergedAt, updated_at: mergedAt }, { number: 41, merged_at: "2026-10-01T00:00:00Z", updated_at: "2026-10-01T00:00:00Z" }], + jsonResponse(response, 200, [{ number: 42, merged_at: mergedAt, updated_at: mergedAt }, { number: 41, merged_at: "2099-10-01T00:00:00Z", updated_at: "2099-10-01T00:00:00Z" }], { Link: nextLinkHeader(base, "/repos/fixture/project/pulls?state=closed&page=2") }); } }), async server => { @@ -492,6 +500,7 @@ test("since stops paging once a row was last updated before the cutoff", async ( })); test("packed CLI preserves repeatable selectors, TOON/JSON shape and no-op apply", async () => tracker(async (root, ids) => { + for (const id of ids) await update(id, { tags: `delivery-pr:https://github.com/${repo}/pull/43` }, { pmRoot: root, noExtensions: true }); const packageRoot = join(import.meta.dirname, ".."); const packed = await boundedCli("npm", ["pack", "--ignore-scripts", "--foreground-scripts=false", "--json", "--pack-destination", root], packageRoot, { ...process.env, PM_PATH: join(packageRoot, ".agents/pm") }); @@ -541,7 +550,7 @@ test("effective ruleset checks supplement classic required contexts", async () = if (request.url!.includes("/rules/branches/")) jsonResponse(response, 200, [ { type: "deletion" }, { type: "required_status_checks", parameters: { required_status_checks: [{ context: "ruleset", integration_id: app }] } }]); if (request.url!.includes("/check-runs")) jsonResponse(response, 200, { total_count: 2, check_runs: [run(), run({ name: "ruleset", conclusion: state })] }); - }), async () => assert.equal((await closeout(root)).plan[0]!.action, state === "success" ? "close" : "skip")); + }), async () => assert.equal((await closeout(root)).plan[0]!.action, state === "success" ? "close" : "needs_review")); } } })); @@ -574,7 +583,7 @@ test("unreadable effective rules preserve known requirements without hiding auth }), async () => { const report = await closeout(root); assert.equal(report.checks[0]!.rule, "unavailable"); - assert.equal(report.plan[0]!.action, "skip"); + assert.equal(report.plan[0]!.action, "needs_review"); }); } })); @@ -582,7 +591,7 @@ test("unreadable effective rules preserve known requirements without hiding auth test("repository resolution accepts valid dot, underscore and hyphen repository names", async () => tracker(async (root, ids) => { for (const repository of ["fixture/.github", "fixture/_repo", "fixture/-repo"]) { await withMockGithub(fixture(link(ids[0]!, repository), (request, response) => { - if (request.url!.includes("/pulls/")) jsonResponse(response, 200, pull(link(ids[0]!, repository), { + if (/\/pulls\/42$/.test(request.url!)) jsonResponse(response, 200, pull(link(ids[0]!, repository), { html_url: `https://github.com/${repository}/pull/42`, base: { ref: "main", repo: { full_name: repository } }, })); }), async () => { @@ -613,7 +622,7 @@ test("unreadable supplemental rules never relax a known required check to neutra await withMockGithub(fixture(link(ids[0]!), (request, response) => { if (request.url!.includes("/rules/branches/")) jsonResponse(response, status, {}); if (request.url!.includes("/check-runs")) jsonResponse(response, 200, { total_count: 1, check_runs: [run({ conclusion: "neutral" })] }); - }), async () => assert.equal((await closeout(root)).plan[0]!.action, "skip")); + }), async () => assert.equal((await closeout(root)).plan[0]!.action, "needs_review")); } })); @@ -625,7 +634,7 @@ test("unauthorized classic protection cannot invoke the permissive fallback", as const report = await closeout(root, { pr: "42", apply: true }); assert.equal(report.closed, 0); assert.equal(report.checks[0]!.rule, "unavailable"); - assert.equal(report.plan[0]!.action, "skip"); + assert.equal(report.plan[0]!.action, "needs_review"); assert.equal(snapshot(root), before); assert.ok(!JSON.stringify(report).includes("private error text")); }); @@ -642,7 +651,7 @@ test("declared policy preserves readable ruleset requirements when classic prote }), async () => { const report = await closeout(root, { pr: "42", "required-checks": "CI" }); assert.equal(report.checks[0]!.rule, "declared"); - assert.equal(report.plan[0]!.action, conclusion === "success" ? "close" : "skip"); + assert.equal(report.plan[0]!.action, conclusion === "success" ? "close" : "needs_review"); }); } })); @@ -655,7 +664,7 @@ test("unreadable protection sources refuse without hiding invalid authorization" }), async () => { const report = await closeout(root); assert.equal(report.checks[0]!.rule, "unavailable"); - assert.equal(report.plan[0]!.action, "skip"); + assert.equal(report.plan[0]!.action, "needs_review"); }); } })); @@ -717,7 +726,7 @@ test("successful optional checks are never labelled as required", async () => tr }); })); -test("compact report counts routine skips and retains individual refusals with their head evidence", async () => tracker(async (root, ids) => { +test("compact report counts routine skips and retains item holds with their head evidence", async () => tracker(async (root, ids) => { await update(ids[1]!, { status: "canceled" }, { pmRoot: root, noExtensions: true }); await withMockGithub(fixture([link(ids[0]!), link(ids[1]!), link(ids[0]!, "other/project")].join("\n"), (request, response) => { if (request.url!.includes("/check-runs")) jsonResponse(response, 200, { total_count: 1, check_runs: [run({ conclusion: "failure" })] }); @@ -726,8 +735,10 @@ test("compact report counts routine skips and retains individual refusals with t const report = compactCloseoutReport(full); assert.deepEqual(report.skipped, { already_terminal: 1, other_repository: 1 }); assert.equal(report.plan.length, 1); - assert.equal(report.plan[0]!.action, "refuse"); - assert.equal(report.plan[0]!.code, "head_checks_failed"); + assert.equal(report.plan[0]!.action, "needs_review"); + assert.deepEqual(report.plan[0]!.candidates, [42]); + assert.ok(report.plan[0]!.missing_evidence?.some(gap => gap.code === "head_checks_failed")); + assert.equal(report.checks[0]!.code, "head_checks_failed"); assert.equal(report.checks.length, 1); assert.equal(full.plan.length, 3); }); @@ -744,7 +755,7 @@ test("declared contexts verify exact names and latest statuses without requiring for (const names of ["CI\nLegacy", "CI\nmissing"]) { const report = await closeout(root, { pr: "42", apply: true, "required-checks": names }); assert.equal(report.closed, 0); - assert.equal(report.plan[0]!.code, "head_checks_failed"); + assert.equal(report.checks[0]!.code, "head_checks_failed"); assert.equal(snapshot(root), before); } const report = await closeout(root, { pr: "42", apply: true, "required-checks": " CI\nlegacy\nCI " }); @@ -764,7 +775,7 @@ test("declared names cannot bypass a readable app binding", async () => tracker( const before = snapshot(root); const report = await closeout(root, { pr: "42", apply: true, "required-checks": "CI" }); assert.equal(report.closed, 0); - assert.equal(report.plan[0]!.code, "head_checks_failed"); + assert.equal(report.checks[0]!.code, "head_checks_failed"); assert.equal(snapshot(root), before); }); })); @@ -777,12 +788,12 @@ test("newline context arrays preserve literal names and empty declarations fail const before = snapshot(root); for (const names of [[], "", " \n\t\r\n ", ["", "\n"]]) { const report = await closeout(root, { pr: "42", apply: true, "required-checks": names }); - assert.equal(report.plan[0]!.code, "required_checks_unverified"); + assert.equal(report.plan[0]!.code, "needs_review"); assert.equal(snapshot(root), before); } // A former space-separated list is one literal name, so observed runs cannot satisfy it. const missing = await closeout(root, { pr: "42", apply: true, "required-checks": "test (22) test (26)" }); - assert.equal(missing.plan[0]!.code, "head_checks_failed"); + assert.equal(missing.plan[0]!.code, "needs_review"); assert.equal(snapshot(root), before); const report = await closeout(root, { pr: "42", apply: true, requiredChecks: [" test (22)\n\n", "test (26)\r\n"] }); assert.equal(report.closed, 1);