diff --git a/.agents/pm/history/pm-github-blocker-format-followup.jsonl b/.agents/pm/history/pm-github-blocker-format-followup.jsonl new file mode 100644 index 00000000..eb57e82e --- /dev/null +++ b/.agents/pm/history/pm-github-blocker-format-followup.jsonl @@ -0,0 +1,16 @@ +{"hash_algorithm":"sha256","ts":"2026-11-11T18:20:20.837Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-blocker-format-followup"},{"op":"add","path":"/metadata/title","value":"Retain formatted GitHub blocker URLs in prose tokenization"},{"op":"add","path":"/metadata/description","value":"Review follow-up from PR #233 thread PRRT_kwDOTbN4pM62SJvl: githubUrlTokens retains trailing Markdown emphasis and punctuation such as ! and :, so formatted issue URLs can disappear from blocker scans. Separate from round-4 durable-state publication; normalize prose wrappers without weakening complete structured identity validation."},{"op":"add","path":"/metadata/type","value":"Issue"},{"op":"add","path":"/metadata/status","value":"open"},{"op":"add","path":"/metadata/priority","value":3},{"op":"add","path":"/metadata/tags","value":["gh:unbraind/pm-github#233","review-followup"]},{"op":"add","path":"/metadata/created_at","value":"2026-11-11T18:20:20.837Z"},{"op":"add","path":"/metadata/updated_at","value":"2026-11-11T18:20:20.837Z"},{"op":"add","path":"/metadata/author","value":"codex-github230-1111"}],"before_hash":"3cc22dff72be7b14824654a7a64ea62b04799939b2fee54c1b5f52ca60bf6df0","after_hash":"9376c8e8e83e8f524dca9c4e541bbeabb046cc7811dbf59c4ccb0f6f3bf594bf","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":"33bd1c238bbfeccefe0450db24c79ebad7b3bbe40df4b05df24b50fbfe6651af"} +{"hash_algorithm":"sha256","ts":"2026-11-11T18:22:32.167Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-11T18:22:32.167Z"},{"op":"add","path":"/metadata/files","value":[{"path":"github-url.ts","scope":"project"}]}],"before_hash":"9376c8e8e83e8f524dca9c4e541bbeabb046cc7811dbf59c4ccb0f6f3bf594bf","after_hash":"a09d167ff7fa967dddd6291c5b59782dca3ff8de2e767132de61ae628b1d421a","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":"a5eb784ff0734cd1a117a08c904531c94b0a2fed21adbc602bcc9103f521d6a0"} +{"hash_algorithm":"sha256","ts":"2026-11-11T18:22:33.572Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-11T18:22:33.572Z"},{"op":"add","path":"/metadata/docs","value":[{"path":"https://github.com/unbraind/pm-github/pull/233#discussion_r4541099510","scope":"project"}]}],"before_hash":"a09d167ff7fa967dddd6291c5b59782dca3ff8de2e767132de61ae628b1d421a","after_hash":"b31c62d95701cefd0b4f4398b30f8bb51c8c974a652b6c33c5e72ac34734bd8a","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":"d07890c5544461f4f190d60b18636f4b76787e0d25feba3cfa8692bbcec2aba0"} +{"hash_algorithm":"sha256","ts":"2026-11-11T20:13:12.046Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-11T20:13:12.046Z"},{"op":"add","path":"/metadata/assignee","value":"codex-github230-1111"},{"op":"add","path":"/metadata/claim_principal","value":"codex-github230-1111"}],"before_hash":"b31c62d95701cefd0b4f4398b30f8bb51c8c974a652b6c33c5e72ac34734bd8a","after_hash":"7155b12d672d8f07a58e259852671f013b498c74e14370c3263a689755132151","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":"472f140d456d2fc9b6d99d76fd2c7e57e208567bec456187f1d4191b04b6599c"} +{"hash_algorithm":"sha256","ts":"2026-11-11T20:13:13.806Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-11T20:13:13.806Z"},{"op":"replace","path":"/metadata/status","value":"in_progress"}],"before_hash":"7155b12d672d8f07a58e259852671f013b498c74e14370c3263a689755132151","after_hash":"2fc09b1307f693906b4a527dcc6b4e7e904b90089ed04e866a19fba28d04a3a8","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":"69030981b9a02d4131e0d5f2bc68a6dab475b22046a362ef0c0ce63995d65e58"} +{"hash_algorithm":"sha256","ts":"2026-11-11T20:17:11.380Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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":"index.ts","scope":"project"}},{"op":"add","path":"/metadata/files/2","value":{"path":"test/github-url.test.ts","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-11T20:17:11.380Z"}],"before_hash":"2fc09b1307f693906b4a527dcc6b4e7e904b90089ed04e866a19fba28d04a3a8","after_hash":"ee943e753f4135fc92c4db55e6173ea178a660f8238a4450f4a80d0183fe49ca","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":"d688c4aa384078cebace8cd7dd6fd8cc630ec82ea09cd76f4451f61ee08d13b3"} +{"hash_algorithm":"sha256","ts":"2026-11-11T20:20:07.168Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-11T20:20:07.168Z"},{"op":"add","path":"/metadata/tests","value":[{"command":"node --test --test-concurrency=1 --test-name-pattern \"^(Markdown|Unparsed)\" test/github-url.test.ts","scope":"project","timeout_seconds":1200,"provenance":{"author":"codex-github230-1111","created_at":"2026-11-11T20:20:07.122Z","source_kind":"local_mutation","source_ref":"fix/reference-tags-are-not-import-identity"}}]}],"before_hash":"ee943e753f4135fc92c4db55e6173ea178a660f8238a4450f4a80d0183fe49ca","after_hash":"5dc7a8212a04868fccf04c1956703fd9bdfbf203c5ad188bb1f1d6571130bff6","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":"5773f9f1065eeca48739ede6e7e9fa5bad951e4f18b8e2ac5bcec4ad1dace588"} +{"hash_algorithm":"sha256","ts":"2026-11-11T20:20:09.732Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-11T20:20:09.732Z"},{"op":"add","path":"/metadata/comments","value":[{"created_at":"2026-11-11T20:20:09.731Z","author":"codex-github230-1111","text":"Round 5 implements a shared linear prose URL extractor with balanced delimiters and matching emphasis, while complete structured URL identity checks stay strict. Unknown URL candidates remain visible as unparsed_url/unparsed references without guessed numbers; they prevent clearance and cleared annotations. Focused real SDK tracker cases cover ten wrapper forms, six punctuation marks, the exact false-clear reproduction and eight unknown URL cases. Node and native Bun focused files pass 30/30 including alias cases. Full dependent-file gates and behavioral reverts remain in progress."}]}],"before_hash":"5dc7a8212a04868fccf04c1956703fd9bdfbf203c5ad188bb1f1d6571130bff6","after_hash":"b1914619e771f766081468d8c34f27f24e15fe9d833c9a0e038225b058d6fe4c","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":"0f1dc4b6c2d7eaaf18017235a6247062636f018fae344302c3cec43f5a3dd3e8"} +{"hash_algorithm":"sha256","ts":"2026-11-11T20:30:20.945Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"tests_remove","patch":[{"op":"remove","path":"/metadata/tests"},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-11T20:30:20.945Z"}],"before_hash":"b1914619e771f766081468d8c34f27f24e15fe9d833c9a0e038225b058d6fe4c","after_hash":"7291e9656afea275ae37cffb4bfa651f2a4cbe395ec134c418fd07818fc7afcf","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":"04d5fafa7d22495de4934fa5638bae1499e2fadf159751003bfc818fa353d0f7"} +{"hash_algorithm":"sha256","ts":"2026-11-11T20:30:22.245Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-11T20:30:22.245Z"},{"op":"add","path":"/metadata/tests","value":[{"command":"node --test --test-concurrency=1 --test-name-pattern \"^(Markdown|Unparsed|Reference)\" test/github-url.test.ts","scope":"project","timeout_seconds":1200,"provenance":{"author":"codex-github230-1111","created_at":"2026-11-11T20:30:22.210Z","source_kind":"local_mutation","source_ref":"fix/reference-tags-are-not-import-identity"}}]}],"before_hash":"7291e9656afea275ae37cffb4bfa651f2a4cbe395ec134c418fd07818fc7afcf","after_hash":"99625b3c24d9c0d62c50080fa9f2f51b9b440f79b3f80725bf0bdadcdc647dc7","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":"0c5737f187258cae03179be1404b06a5cfb6fd546d58acd1132a769e94819bf5"} +{"hash_algorithm":"sha256","ts":"2026-11-11T21:06:55.905Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-11T21:06:55.904Z","author":"codex-github230-1111","text":"Round 5 completed: Markdown-aware URL extraction handles every requested wrapper and punctuation while retaining balanced URL data. Unsupported HTTP candidates remain unparsed_url/unparsed without invented numbers; masking prevents fragment fallback. Only actual ownership excludes self, so reference tags cannot hide open blockers. The linked node --test --test-concurrency=1 --test-name-pattern \"^(Markdown|Unparsed|Reference)\" test/github-url.test.ts run passes 26/26; full blockers file passes 35/35. Behavioral reverts fail all 17 Markdown cases, all 8 unparsed cases and the reference-tag self-exclusion case; restored full Node/Bun file passes 31/31. Evidence maps to PRRT_kwDOTbN4pM62SJvl. Releasing this author claim and leaving the item in progress."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-11T21:06:55.905Z"}],"before_hash":"99625b3c24d9c0d62c50080fa9f2f51b9b440f79b3f80725bf0bdadcdc647dc7","after_hash":"99fc01411b2a0c58e0e01f1390eff397d9a963c75ea6d3e72f89599d57f35a97","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":"02a7b9f7a98adf030774ccd268e12c455bfa59dd08710095a1f996a8e5ce91c4"} +{"hash_algorithm":"sha256","ts":"2026-11-11T21:06:58.322Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-11T21:06:58.322Z"}],"before_hash":"99fc01411b2a0c58e0e01f1390eff397d9a963c75ea6d3e72f89599d57f35a97","after_hash":"81dd287e71d6fbabd49b2c64c9b4a87bc0736eb9fd7143b434e4f6ed033ddbb0","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":"a01f4bcc4a61c782997107252d083e2ee7827b1af74dfa901298897bf56f77aa"} +{"hash_algorithm":"sha256","ts":"2026-11-11T22:21:14.447Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-11T22:21:14.447Z","author":"codex-github230-1111","text":"Additional outside-diff review reproduction from the latest CodeRabbit body, retained outside round-6 ownership-write scope: an unowned blocked item tagged gh:a/r#7 says Blocked by #2, while the command supplies --repo b/r. Tag-based repository hinting queries a/r#2; if closed while b/r#2 is open it can falsely clear and annotate. Resolve repository hints from authoritative ownership or the explicit command hint; a reference tag must not choose the repository. No hint behavior changed in round 6."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-11T22:21:14.447Z"}],"before_hash":"81dd287e71d6fbabd49b2c64c9b4a87bc0736eb9fd7143b434e4f6ed033ddbb0","after_hash":"7839f25ce72fb8b4e67c3771ccb1c535bb586687b9444e4e8fa8388717ffbabe","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":"2f28882cba6c6e0de765ea864fc05871b3737694c4fb1b630b5edc4c44747035"} +{"hash_algorithm":"sha256","ts":"2026-11-12T08:53:46.787Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-12T08:53:46.787Z"},{"op":"remove","path":"/metadata/tags/1"},{"op":"replace","path":"/metadata/tags/0","value":"review-followup"}],"before_hash":"7839f25ce72fb8b4e67c3771ccb1c535bb586687b9444e4e8fa8388717ffbabe","after_hash":"d010685912495f14677189136222f5b073cf0bf6950e8ae9537e6df7df141bf3","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":"e4cb1b3a734cd53761b5a5833426168ee914dbc6085c18d6cee667fa928bdd6a"} +{"hash_algorithm":"sha256","ts":"2026-11-12T08:53:47.563Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"docs_add","patch":[{"op":"add","path":"/metadata/docs/0/note","value":"Context reference; this follow-up is not the referenced issue or PR"},{"op":"replace","path":"/metadata/docs/0/path","value":"https://github.com/unbraind/pm-github/pull/233"},{"op":"add","path":"/metadata/docs/1","value":{"path":"https://github.com/unbraind/pm-github/pull/233#discussion_r4541099510","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T08:53:47.563Z"}],"before_hash":"d010685912495f14677189136222f5b073cf0bf6950e8ae9537e6df7df141bf3","after_hash":"ee2418757d31cc7a5e6a174fa3fe1ea15a03ba1681105a193dfcea8d196630c3","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":"fc7db9db0a208da76842e20a197a434b56bc6ce760e08283ff403d3b853a127b"} +{"hash_algorithm":"sha256","ts":"2026-11-12T08:53:48.352Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-12T08:53:48.352Z","author":"codex-github230-1111","text":"Round 9 tracker audit: moved the parent issue or PR reference from a gh tag to a docs link. This is reference hygiene only; deferred implementation remains out of scope and this item stays open."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T08:53:48.352Z"}],"before_hash":"ee2418757d31cc7a5e6a174fa3fe1ea15a03ba1681105a193dfcea8d196630c3","after_hash":"7b712689cc37042da18b32f73711b32ae38aef12418b887c9f51a74d7400408c","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":"0d1f479f229aa813ebd9293cfb7943bfe11e4a5e37fca4952147b16e42b4f4b9"} diff --git a/.agents/pm/history/pm-github-curated-reopen-followup.jsonl b/.agents/pm/history/pm-github-curated-reopen-followup.jsonl new file mode 100644 index 00000000..ca655fe1 --- /dev/null +++ b/.agents/pm/history/pm-github-curated-reopen-followup.jsonl @@ -0,0 +1,7 @@ +{"hash_algorithm":"sha256","ts":"2026-11-11T18:20:18.830Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-curated-reopen-followup"},{"op":"add","path":"/metadata/title","value":"Distinguish source-assigned completion dates when curated items reopen"},{"op":"add","path":"/metadata/description","value":"Review follow-up from PR #233 thread PRRT_kwDOTbN4pM62Q98t: preserve completion dates that predate linking, but evaluate clearing dates assigned by a prior GitHub closure when the source reopens. Separate from round-4 durable-state publication; requires explicit provenance for the completion date and real SDK reopen tests."},{"op":"add","path":"/metadata/type","value":"Issue"},{"op":"add","path":"/metadata/status","value":"open"},{"op":"add","path":"/metadata/priority","value":3},{"op":"add","path":"/metadata/tags","value":["gh:unbraind/pm-github#233","review-followup"]},{"op":"add","path":"/metadata/created_at","value":"2026-11-11T18:20:18.830Z"},{"op":"add","path":"/metadata/updated_at","value":"2026-11-11T18:20:18.830Z"},{"op":"add","path":"/metadata/author","value":"codex-github230-1111"}],"before_hash":"3cc22dff72be7b14824654a7a64ea62b04799939b2fee54c1b5f52ca60bf6df0","after_hash":"ce2e022746c44908ab0f9ce25d59b4c7c2885742245aefa0ba8e70890bdd2938","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":"b17f762acf4fdb7b5079665e7f316b7835ecc45e80538012b5e54b80823ae6e0"} +{"hash_algorithm":"sha256","ts":"2026-11-11T18:22:26.826Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-11T18:22:26.826Z"},{"op":"add","path":"/metadata/files","value":[{"path":"import-fidelity.ts","scope":"project"}]}],"before_hash":"ce2e022746c44908ab0f9ce25d59b4c7c2885742245aefa0ba8e70890bdd2938","after_hash":"08edf34d036da3e1a647e0b51a24e55b049eaa525818134e88839d1288f81d9f","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":"445f72e7b05d0f2e3fc986d3ed4f8f477b001ece2be7ad32431d9c3e0c716015"} +{"hash_algorithm":"sha256","ts":"2026-11-11T18:22:28.358Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-11T18:22:28.358Z"},{"op":"add","path":"/metadata/docs","value":[{"path":"https://github.com/unbraind/pm-github/pull/233#discussion_r4540585133","scope":"project"}]}],"before_hash":"08edf34d036da3e1a647e0b51a24e55b049eaa525818134e88839d1288f81d9f","after_hash":"4cf58686d5a748109e8e97a0cb2b9d41f17c499c4fafa06c21218b03686a3458","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":"e4c90f369ed1dc6280d5a83b35f2c3c6d69933c7971b2d191874319a9f65f458"} +{"hash_algorithm":"sha256","ts":"2026-11-11T22:07:08.273Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-11T22:07:08.273Z"},{"op":"add","path":"/metadata/comments","value":[{"created_at":"2026-11-11T22:07:08.272Z","author":"codex-github230-1111","text":"Round 6 review reproduction retained for later work (PRRT_kwDOTbN4pM62Q98t): start with a curated linked item with no completed_at, import a GitHub closure so completed_at is assigned from github_closed_at, then reopen the source and re-sync. The item becomes open and closed_at is cleared, but the source-assigned completed_at remains. A pre-link local completion date must still be preserved. Do not clear every completed_at; track source assignment distinctly before implementing reopen cleanup. This round intentionally changes no completion-date behavior."}]}],"before_hash":"4cf58686d5a748109e8e97a0cb2b9d41f17c499c4fafa06c21218b03686a3458","after_hash":"2e5c6d77c7062488f21de5c059fe631a55489c6e0aaa00e99889806ac0368f5e","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":"89347634a635ce536296cec6807f0a9d6b238a8747b313c4e91a65cda13896fa"} +{"hash_algorithm":"sha256","ts":"2026-11-12T08:53:49.201Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-12T08:53:49.201Z"},{"op":"remove","path":"/metadata/tags/1"},{"op":"replace","path":"/metadata/tags/0","value":"review-followup"}],"before_hash":"2e5c6d77c7062488f21de5c059fe631a55489c6e0aaa00e99889806ac0368f5e","after_hash":"61993f35367968b024c72ee107b23d5b9633388dfb1cee3bf9993c3350c6715a","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":"4233a7ba6bdf5192ffadaca52d61a205c342f3f9238d61ff747de1ece16618ba"} +{"hash_algorithm":"sha256","ts":"2026-11-12T08:53:50.020Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"docs_add","patch":[{"op":"add","path":"/metadata/docs/0/note","value":"Context reference; this follow-up is not the referenced issue or PR"},{"op":"replace","path":"/metadata/docs/0/path","value":"https://github.com/unbraind/pm-github/pull/233"},{"op":"add","path":"/metadata/docs/1","value":{"path":"https://github.com/unbraind/pm-github/pull/233#discussion_r4540585133","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T08:53:50.020Z"}],"before_hash":"61993f35367968b024c72ee107b23d5b9633388dfb1cee3bf9993c3350c6715a","after_hash":"de3e43d2c908683f1aae4c4caeddab5e499d1105fb1b3885a3c7ab91dab869bb","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":"ac60a233d5d59e409bf1a9590bba90f7886188e9550b658f41babba492f265ae"} +{"hash_algorithm":"sha256","ts":"2026-11-12T08:53:50.742Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-12T08:53:50.741Z","author":"codex-github230-1111","text":"Round 9 tracker audit: moved the parent issue or PR reference from a gh tag to a docs link. This is reference hygiene only; deferred implementation remains out of scope and this item stays open."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T08:53:50.742Z"}],"before_hash":"de3e43d2c908683f1aae4c4caeddab5e499d1105fb1b3885a3c7ab91dab869bb","after_hash":"38ff08cd447d1882279aa7c99544418c7ef7f988ff02e06b1ef595f22b815415","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":"9ba587af8fe64b1c27108f15b0ddf8a9d05565a9b29ee0251fb32783cddc7229"} diff --git a/.agents/pm/history/pm-github-pr-url-alias-followup.jsonl b/.agents/pm/history/pm-github-pr-url-alias-followup.jsonl new file mode 100644 index 00000000..18259c92 --- /dev/null +++ b/.agents/pm/history/pm-github-pr-url-alias-followup.jsonl @@ -0,0 +1,13 @@ +{"hash_algorithm":"sha256","ts":"2026-11-11T18:20:19.928Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-pr-url-alias-followup"},{"op":"add","path":"/metadata/title","value":"Exclude PR owners in every supported explicit URL field during refresh"},{"op":"add","path":"/metadata/description","value":"Review follow-up from PR #233 thread PRRT_kwDOTbN4pM62Q99D: identity resolution recognizes GitHubURL aliases, while refresh PR exclusion checks github_url only. Evaluate all supported explicit URL fields before the issue-only GraphQL query. Separate from round-4 durable-state publication; retain real tracker tests for aliases and issue owners."},{"op":"add","path":"/metadata/type","value":"Issue"},{"op":"add","path":"/metadata/status","value":"open"},{"op":"add","path":"/metadata/priority","value":3},{"op":"add","path":"/metadata/tags","value":["gh:unbraind/pm-github#233","review-followup"]},{"op":"add","path":"/metadata/created_at","value":"2026-11-11T18:20:19.928Z"},{"op":"add","path":"/metadata/updated_at","value":"2026-11-11T18:20:19.928Z"},{"op":"add","path":"/metadata/author","value":"codex-github230-1111"}],"before_hash":"3cc22dff72be7b14824654a7a64ea62b04799939b2fee54c1b5f52ca60bf6df0","after_hash":"4e2d3fe3175c52ad9101a8467d6ae6bcee12f30a40fe62bc8a607123b0aef26c","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":"fe13a35702ebbc05ff94a1e02756b684e79920ef27df87ca7f9139932b14bee6"} +{"hash_algorithm":"sha256","ts":"2026-11-11T18:22:29.658Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-11T18:22:29.658Z"},{"op":"add","path":"/metadata/files","value":[{"path":"refresh.ts","scope":"project"}]}],"before_hash":"4e2d3fe3175c52ad9101a8467d6ae6bcee12f30a40fe62bc8a607123b0aef26c","after_hash":"8bc4f66ebe5688e3493da88077a6d1cebbfbc2ed1162c45822c35985ff914738","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":"07b82a827225b83dbaa84a37d467d153a5726f74dd4bbe6b9352a6c8b88453ca"} +{"hash_algorithm":"sha256","ts":"2026-11-11T18:22:31.188Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-11T18:22:31.188Z"},{"op":"add","path":"/metadata/docs","value":[{"path":"https://github.com/unbraind/pm-github/pull/233#discussion_r4540585165","scope":"project"}]}],"before_hash":"8bc4f66ebe5688e3493da88077a6d1cebbfbc2ed1162c45822c35985ff914738","after_hash":"0ad45e700481d73ce24201dc5d528149f1598b4768da9209ef55208a839488d3","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":"e6a14c3cab62556c2f0561cda2838579d245e331a44c82367fcd73557ddcef9f"} +{"hash_algorithm":"sha256","ts":"2026-11-11T20:13:12.899Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-11T20:13:12.899Z"},{"op":"add","path":"/metadata/assignee","value":"codex-github230-1111"},{"op":"add","path":"/metadata/claim_principal","value":"codex-github230-1111"}],"before_hash":"0ad45e700481d73ce24201dc5d528149f1598b4768da9209ef55208a839488d3","after_hash":"3b2b6cd5710015b7c5a5175153d0b1737ffc54ae38f329fcfa3878a23fdae200","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":"58472e18abcb330cf81639a9e61b4a29d2d6c1d79642cb3da58975411fd25d91"} +{"hash_algorithm":"sha256","ts":"2026-11-11T20:13:14.644Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-11T20:13:14.644Z"},{"op":"replace","path":"/metadata/status","value":"in_progress"}],"before_hash":"3b2b6cd5710015b7c5a5175153d0b1737ffc54ae38f329fcfa3878a23fdae200","after_hash":"36ffd98ecd742ef8c6c77c3af775f220dda27fca82ce0f0489bf6eec665d72b7","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":"725bd314dc8c3ecc0c81315f58df126c1a0c80dde7bdc4ebf7f17bfec6d7fefe"} +{"hash_algorithm":"sha256","ts":"2026-11-11T20:17:13.319Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/0/path","value":"import-fidelity.ts"},{"op":"add","path":"/metadata/files/1","value":{"path":"refresh.ts","scope":"project"}},{"op":"add","path":"/metadata/files/2","value":{"path":"test/github-url.test.ts","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-11T20:17:13.319Z"}],"before_hash":"36ffd98ecd742ef8c6c77c3af775f220dda27fca82ce0f0489bf6eec665d72b7","after_hash":"2400269e07321e2139882eabf6087e74164a53338c05c01a7446b33a7a90df1d","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":"efc2fa0146bcf770d0301049d94ef461d3d02f4708fd9f0cfcfe171f62f8c572"} +{"hash_algorithm":"sha256","ts":"2026-11-11T20:20:08.518Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-11T20:20:08.518Z"},{"op":"add","path":"/metadata/tests","value":[{"command":"node --test --test-concurrency=1 --test-name-pattern \"^Refresh excludes PRs\" test/github-url.test.ts","scope":"project","timeout_seconds":1200,"provenance":{"author":"codex-github230-1111","created_at":"2026-11-11T20:20:08.486Z","source_kind":"local_mutation","source_ref":"fix/reference-tags-are-not-import-identity"}}]}],"before_hash":"2400269e07321e2139882eabf6087e74164a53338c05c01a7446b33a7a90df1d","after_hash":"628b5d0757ed8bd5b315fb662e728eb446c9bd2d943cc1d06a77d0588426b3a1","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":"a2f4d278ff4df24ca97c6a91a91fa6c7ee25c09443d6b69d65ea093b2d9c4709"} +{"hash_algorithm":"sha256","ts":"2026-11-11T20:20:11.030Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-11T20:20:11.030Z"},{"op":"add","path":"/metadata/comments","value":[{"created_at":"2026-11-11T20:20:11.029Z","author":"codex-github230-1111","text":"Round 5 centralizes recognized explicit URL aliases in explicitGithubIdentities and makes ownership matching and refresh consume the same parsed kind. Five real SDK alias scenarios verify PRs are excluded before querying, canonical PR controls stay excluded, issue aliases still refresh, and curated titles stay unchanged. The selector/model also checks all 1024 case/optional-underscore spellings. Focused Node/Bun files pass 30/30; full gates and reverts remain in progress."}]}],"before_hash":"628b5d0757ed8bd5b315fb662e728eb446c9bd2d943cc1d06a77d0588426b3a1","after_hash":"4a57679f56ab4d4a526c43e13bdc62f27fd5f5df87996e09c93c151bfb5b006a","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":"9f0e42e56abf08ac778e7a0632b5f5c953dc1185f5566eee9ea8ba7f29d1402c"} +{"hash_algorithm":"sha256","ts":"2026-11-11T21:06:56.713Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-11T21:06:56.712Z","author":"codex-github230-1111","text":"Round 5 completed: explicitGithubIdentities shares every recognized URL alias and parsed kind between import ownership and refresh PR selection. Five real SDK tracker scenarios cover retained aliases, canonical PR controls, issue aliases and unchanged curated titles; the selector also checks all 1024 accepted case/optional-underscore spellings. Linked node --test --test-concurrency=1 --test-name-pattern \"^Refresh excludes PRs\" test/github-url.test.ts passes 5/5. Reverting only the selector makes all 5 fail; restored Node/Bun file passes 31/31. Evidence maps to PRRT_kwDOTbN4pM62Q99D. Releasing this author claim and leaving the item in progress."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-11T21:06:56.713Z"}],"before_hash":"4a57679f56ab4d4a526c43e13bdc62f27fd5f5df87996e09c93c151bfb5b006a","after_hash":"c09238f530b099bc8a5cc90a5f525b01829ba207436b3164a1c4ee1f56775938","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":"2da6f310e4edf4dc3c26187df75034376e9223c6baea38251b21b83de3072c29"} +{"hash_algorithm":"sha256","ts":"2026-11-11T21:06:59.068Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-11T21:06:59.068Z"}],"before_hash":"c09238f530b099bc8a5cc90a5f525b01829ba207436b3164a1c4ee1f56775938","after_hash":"6101a6f76ac6336de1db6eb9d722c640e827a03e4b0175c9f07858bb0ba8b030","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":"ab0b0d5c0d12928f9cd94ae43af168847516eef0dbb337c834a608724aa2261f"} +{"hash_algorithm":"sha256","ts":"2026-11-12T08:53:51.508Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-12T08:53:51.508Z"},{"op":"remove","path":"/metadata/tags/1"},{"op":"replace","path":"/metadata/tags/0","value":"review-followup"}],"before_hash":"6101a6f76ac6336de1db6eb9d722c640e827a03e4b0175c9f07858bb0ba8b030","after_hash":"e0023ff75087dcdd7471bf2cb9f4e9b011b2e6847977d3b0deffeb6e8900f748","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":"726464db17153890ec964bba6772cf512bc671d1d261f49985e338ed856f0730"} +{"hash_algorithm":"sha256","ts":"2026-11-12T08:53:52.318Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"docs_add","patch":[{"op":"add","path":"/metadata/docs/0/note","value":"Context reference; this follow-up is not the referenced issue or PR"},{"op":"replace","path":"/metadata/docs/0/path","value":"https://github.com/unbraind/pm-github/pull/233"},{"op":"add","path":"/metadata/docs/1","value":{"path":"https://github.com/unbraind/pm-github/pull/233#discussion_r4540585165","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T08:53:52.318Z"}],"before_hash":"e0023ff75087dcdd7471bf2cb9f4e9b011b2e6847977d3b0deffeb6e8900f748","after_hash":"e794c1fe912d3b75676a9a635879eafd3c24f7e8f4bc7e0e70cf9b348fd13193","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":"d734f1953dc4cc60ed0b0098bfcebe3f730e3bbf34972367502a061678435ddf"} +{"hash_algorithm":"sha256","ts":"2026-11-12T08:53:53.066Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-12T08:53:53.065Z","author":"codex-github230-1111","text":"Round 9 tracker audit: moved the parent issue or PR reference from a gh tag to a docs link. This is reference hygiene only; deferred implementation remains out of scope and this item stays open."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T08:53:53.066Z"}],"before_hash":"e794c1fe912d3b75676a9a635879eafd3c24f7e8f4bc7e0e70cf9b348fd13193","after_hash":"ef760ca8b36b831b0a4b46b7f220a728cd5df4929da079b4209a5704b1b40393","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":"1e2beab2a38d76c3993c5704eca08440b2de27b6813c11dcf33ba43a5feac449"} diff --git a/.agents/pm/history/pm-github-project-reference-followup.jsonl b/.agents/pm/history/pm-github-project-reference-followup.jsonl new file mode 100644 index 00000000..59c98950 --- /dev/null +++ b/.agents/pm/history/pm-github-project-reference-followup.jsonl @@ -0,0 +1,5 @@ +{"hash_algorithm":"sha256","ts":"2026-11-11T11:09:25.465Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-project-reference-followup"},{"op":"add","path":"/metadata/title","value":"Audit Projects v2 reference fallback and curated item ownership"},{"op":"add","path":"/metadata/description","value":"Static inspection found a separate board-import path: projects.ts indexPmByIssue indexes free-form gh tags, buildProjectImportPlan uses that fallback, and index.ts runProjectImport writes title, description and body on an update. Reproduce with real SDK trackers, then define explicit board ownership and migration-free compatibility for existing gh-project markers. This is separate from the gated REST issue importer fixed by issue 230."},{"op":"add","path":"/metadata/type","value":"Issue"},{"op":"add","path":"/metadata/status","value":"open"},{"op":"add","path":"/metadata/priority","value":2},{"op":"add","path":"/metadata/tags","value":["follow-up","gh:unbraind/pm-github#230","projects","reference-identity"]},{"op":"add","path":"/metadata/created_at","value":"2026-11-11T11:09:25.465Z"},{"op":"add","path":"/metadata/updated_at","value":"2026-11-11T11:09:25.465Z"},{"op":"add","path":"/metadata/author","value":"codex-github230-1111"}],"before_hash":"3cc22dff72be7b14824654a7a64ea62b04799939b2fee54c1b5f52ca60bf6df0","after_hash":"fd703d5c371352d382dd51eab55289fec6f7456aa04959aa5fa9ce15f0b9f843","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":"895a54365ebab3721a04435adf4b9bd240edf7d0ddbed9d5bff53eaa1564ce8e"} +{"hash_algorithm":"sha256","ts":"2026-11-11T11:09:27.552Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-11T11:09:27.552Z"},{"op":"add","path":"/metadata/files","value":[{"path":"index.ts","scope":"project"},{"path":"projects.ts","scope":"project"}]}],"before_hash":"fd703d5c371352d382dd51eab55289fec6f7456aa04959aa5fa9ce15f0b9f843","after_hash":"193ecd379b800fdf33acc335038a48a7808328c7161517e35c0d8d26dc34cf6e","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":"dbbecad677097ae7a0b18577341451f3253b5299848a855bee67fe5761fe4aea"} +{"hash_algorithm":"sha256","ts":"2026-11-12T08:53:53.847Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-12T08:53:53.847Z"},{"op":"remove","path":"/metadata/tags/3"},{"op":"replace","path":"/metadata/tags/2","value":"reference-identity"},{"op":"replace","path":"/metadata/tags/1","value":"projects"}],"before_hash":"193ecd379b800fdf33acc335038a48a7808328c7161517e35c0d8d26dc34cf6e","after_hash":"cd5ddda8c256e7f9363bd0c6ea674d795ea41a8926ce0cbc58de3a8880c442cf","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":"5c03e2a889d4259f4d3bff888e7c60f7e6413b892fbd0891e0c89421499f8e2e"} +{"hash_algorithm":"sha256","ts":"2026-11-12T08:53:54.782Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-12T08:53:54.782Z"},{"op":"add","path":"/metadata/docs","value":[{"path":"https://github.com/unbraind/pm-github/issues/230","scope":"project","note":"Context reference; this follow-up is not the referenced issue or PR"}]}],"before_hash":"cd5ddda8c256e7f9363bd0c6ea674d795ea41a8926ce0cbc58de3a8880c442cf","after_hash":"6067c8f0c1a573361be2af62415e49e797cf4c0ee4f567e7891d475902f8521e","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":"330e142fd9a51dc4755fe72b325321386565745a41751a611432e806d09c4aec"} +{"hash_algorithm":"sha256","ts":"2026-11-12T08:53:55.558Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-12T08:53:55.558Z"},{"op":"add","path":"/metadata/comments","value":[{"created_at":"2026-11-12T08:53:55.558Z","author":"codex-github230-1111","text":"Round 9 tracker audit: moved the parent issue or PR reference from a gh tag to a docs link. This is reference hygiene only; deferred implementation remains out of scope and this item stays open."}]}],"before_hash":"6067c8f0c1a573361be2af62415e49e797cf4c0ee4f567e7891d475902f8521e","after_hash":"598ed864bebf11e3a4f596d28a423c664b42848eb4cd61f7157709f460a8e986","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":"559d8f05d21936bd81ba718726601e60f76cf59d78bdbac638f4b943e8f47b73"} diff --git a/.agents/pm/history/pm-github-reference-identity-followup.jsonl b/.agents/pm/history/pm-github-reference-identity-followup.jsonl new file mode 100644 index 00000000..c170f6ff --- /dev/null +++ b/.agents/pm/history/pm-github-reference-identity-followup.jsonl @@ -0,0 +1,19 @@ +{"hash_algorithm":"sha256","ts":"2026-11-12T02:00:10.688Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-reference-identity-followup"},{"op":"add","path":"/metadata/title","value":"Validate remaining reference-identity review edge cases after PR 233"},{"op":"add","path":"/metadata/description","value":"Single follow-up for additional review observations after the final fix round. Revalidate remaining identity, recovery, lease and reporting edge cases against the final PR head; do not reopen completed rounds or duplicate covered regressions."},{"op":"add","path":"/metadata/type","value":"Issue"},{"op":"add","path":"/metadata/status","value":"open"},{"op":"add","path":"/metadata/priority","value":2},{"op":"add","path":"/metadata/tags","value":["gh:unbraind/pm-github#233","review-followup"]},{"op":"add","path":"/metadata/created_at","value":"2026-11-12T02:00:10.688Z"},{"op":"add","path":"/metadata/updated_at","value":"2026-11-12T02:00:10.688Z"},{"op":"add","path":"/metadata/author","value":"codex-github230-1111"}],"before_hash":"3cc22dff72be7b14824654a7a64ea62b04799939b2fee54c1b5f52ca60bf6df0","after_hash":"15fe6c11e4d1f41c148c1cc0acb2b849d8d423ea5cdca71b2099761c9e4d46dc","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":"837f067d3234b37a9c17f8746434838c52f49893b6a500f2781b531e4ffbb6b2"} +{"hash_algorithm":"sha256","ts":"2026-11-12T02:01:10.923Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-12T02:01:10.923Z"},{"op":"replace","path":"/metadata/tags/1","value":"gh:unbraind/pm-github#236"},{"op":"add","path":"/metadata/tags/2","value":"review-followup"},{"op":"add","path":"/metadata/github_number","value":236},{"op":"add","path":"/metadata/github_url","value":"https://github.com/unbraind/pm-github/issues/236"}],"before_hash":"15fe6c11e4d1f41c148c1cc0acb2b849d8d423ea5cdca71b2099761c9e4d46dc","after_hash":"7e6432ed1aecc1058f43b07f0b0c2b1d2130c950b4cfa13548f50f3a7806c249","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":"fc84ae4c5261bba5c820827444d5bae6ca4b794e631c0df4b198cc42f0109844"} +{"hash_algorithm":"sha256","ts":"2026-11-12T02:01:13.150Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-12T02:01:13.150Z"},{"op":"add","path":"/metadata/comments","value":[{"created_at":"2026-11-12T02:01:13.150Z","author":"codex-github230-1111","text":"Round 7 is the final fix round for #233. This single follow-up consolidates additional review observations, as requested by the orchestrator; it does not assert that every observation reproduces after the round-7 changes. Validate each against the final head, deduplicate covered cases, and retain real SDK must-refuse/must-allow tests plus behavioral revert evidence for any fix.\n\n- [PRRT_kwDOTbN4pM62auBR](https://github.com/unbraind/pm-github/pull/233#discussion_r4544876674): Report import conflicts on the workflow path that produces no tracker diff.\n- [PRRT_kwDOTbN4pM62auBZ](https://github.com/unbraind/pm-github/pull/233#discussion_r4544876686): Validate ambiguous legacy number-only ownership before closeout.\n- [PRRT_kwDOTbN4pM62auBc](https://github.com/unbraind/pm-github/pull/233#discussion_r4544876691): Recover exclusive leases safely when publication succeeds but directory flush fails.\n- [PRRT_kwDOTbN4pM62auBe](https://github.com/unbraind/pm-github/pull/233#discussion_r4544876695): Require an actual matching reference tag before planning tag detachment.\n- [PRRT_kwDOTbN4pM62auBi](https://github.com/unbraind/pm-github/pull/233#discussion_r4544876701): Reconcile contradictory external_ref claims at the import write boundary.\n- [PRRT_kwDOTbN4pM62auBo](https://github.com/unbraind/pm-github/pull/233#discussion_r4544876710): Check partial legacy link markers against an existing repository field.\n- [PRRT_kwDOTbN4pM62auBu](https://github.com/unbraind/pm-github/pull/233#discussion_r4544876718): Avoid retaining an ownership-refused plan when no SDK journal exists.\n- [PRRT_kwDOTbN4pM62auB1](https://github.com/unbraind/pm-github/pull/233#discussion_r4544876728): Scope retained recovery plans to selected refresh IDs without dropping fresh updates.\n- [PRRT_kwDOTbN4pM62auB7](https://github.com/unbraind/pm-github/pull/233#discussion_r4544876736): Handle contradictory identities in blocker self-exclusion; dependency indexing is addressed in round 7.\n- [PRRT_kwDOTbN4pM62auB_](https://github.com/unbraind/pm-github/pull/233#discussion_r4544876746): Use canonical lowercase reference tags in the citation-adoption fixture.\n\nExplicit identity-conflict isolation in closeout and real-tracker dry-run fixtures are already addressed by round 7. Existing curated-reopen, Projects and search follow-ups keep their separate scope. Do not delete append-only tracker evidence or reply to/resolve review threads while implementing this follow-up.\n\nPM: https://github.com/unbraind/pm-github/blob/main/.agents/pm/issues/pm-github-reference-identity-followup.toon"}]}],"before_hash":"7e6432ed1aecc1058f43b07f0b0c2b1d2130c950b4cfa13548f50f3a7806c249","after_hash":"414b866a48b6e0a00935cfc436669467e90e54afb2becf593df2b8ef382cd12e","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":"ff2dfa79bcce3957ee3fcac60156023395e93d88e00b29e7dc0770ffafcc66f9"} +{"hash_algorithm":"sha256","ts":"2026-11-12T02:29:32.293Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-12T02:29:32.293Z"},{"op":"add","path":"/metadata/files","value":[{"path":"closeout.ts","scope":"project"}]}],"before_hash":"414b866a48b6e0a00935cfc436669467e90e54afb2becf593df2b8ef382cd12e","after_hash":"075ff55bb753e56b0b885abb3ff5ede9d755c0c5a7f6b54e614674e1c98befaa","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":"06de874046103c48f27aa21ab640602a21e3b784382c25f16d3e345f10213f02"} +{"hash_algorithm":"sha256","ts":"2026-11-12T02:29:34.932Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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":"durable-state.ts","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T02:29:34.932Z"}],"before_hash":"075ff55bb753e56b0b885abb3ff5ede9d755c0c5a7f6b54e614674e1c98befaa","after_hash":"9a75015598372b959c36c18f489ce72c52f0c5612b500a8ad0cb818d37e99b47","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":"74cce68cb0e59688d59c173db9d38a794c5ec554c1e1b5f65d1a29147ce763f2"} +{"hash_algorithm":"sha256","ts":"2026-11-12T02:29:37.197Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/2","value":{"path":"identity.ts","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T02:29:37.197Z"}],"before_hash":"9a75015598372b959c36c18f489ce72c52f0c5612b500a8ad0cb818d37e99b47","after_hash":"b525f3ac120f6188d69d910b8033dc319de365835e3528b8bc24bfdc90d16e8c","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":"81007a55e26311e6aa9df6d318e734ddb968810b4e67cad24afdfc267ca0f38c"} +{"hash_algorithm":"sha256","ts":"2026-11-12T02:29:40.495Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/3","value":{"path":"import-fidelity.ts","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T02:29:40.495Z"}],"before_hash":"b525f3ac120f6188d69d910b8033dc319de365835e3528b8bc24bfdc90d16e8c","after_hash":"acc48eb862879daf598f102375b6588b3aadf8052e7c59565c8360029dc5de7e","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":"46f24caff33db4743c3b4a155d17017f26eb27fd9be8b768fe0535067787bdb8"} +{"hash_algorithm":"sha256","ts":"2026-11-12T02:29:43.204Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/4","value":{"path":"import-ownership.ts","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T02:29:43.204Z"}],"before_hash":"acc48eb862879daf598f102375b6588b3aadf8052e7c59565c8360029dc5de7e","after_hash":"0789f852390188f0aa0bb08081eff03bb94c8499a0a528fc753fbaa495af3e54","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":"2c0f4f26dae3780fa38d92c17038b9f486dd1ef71ee0f711c8286e75896c84aa"} +{"hash_algorithm":"sha256","ts":"2026-11-12T02:29:46.376Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/5","value":{"path":"index.ts","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T02:29:46.376Z"}],"before_hash":"0789f852390188f0aa0bb08081eff03bb94c8499a0a528fc753fbaa495af3e54","after_hash":"ad839881b1e8704881027588058164f97321da0b57c79270db52577327cffa15","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":"cc8c39fac363bbaa2935ee8f3fe95fc37b24933d47e804d5dcb1406414030a14"} +{"hash_algorithm":"sha256","ts":"2026-11-12T02:29:50.156Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/6","value":{"path":"test/cited-issue-adoption.test.ts","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T02:29:50.156Z"}],"before_hash":"ad839881b1e8704881027588058164f97321da0b57c79270db52577327cffa15","after_hash":"3112422090dd7ad166395412357612d80216d93fe0d746775c39e803f41ab7dd","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":"8f50db77160b18e58d4ca26b11db097a8b1bd3f05610a50015ab872f5d66646d"} +{"hash_algorithm":"sha256","ts":"2026-11-12T02:29:53.108Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/6/path","value":"index.ts"},{"op":"replace","path":"/metadata/files/5/path","value":"import-ownership.ts"},{"op":"replace","path":"/metadata/files/4/path","value":"import-fidelity.ts"},{"op":"replace","path":"/metadata/files/3/path","value":"identity.ts"},{"op":"replace","path":"/metadata/files/2/path","value":"durable-state.ts"},{"op":"replace","path":"/metadata/files/1/path","value":"closeout.ts"},{"op":"replace","path":"/metadata/files/0/path","value":".github/workflows/pm-github-sync.yml"},{"op":"add","path":"/metadata/files/7","value":{"path":"test/cited-issue-adoption.test.ts","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T02:29:53.108Z"}],"before_hash":"3112422090dd7ad166395412357612d80216d93fe0d746775c39e803f41ab7dd","after_hash":"171492f32730caabe628ab01830d1c447758604c98a741aaff8bfd2804464d90","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":"2b34a73c6f2af3de230269f39e539eba0fb301943c95daf34f000fc0a72ccf08"} +{"hash_algorithm":"sha256","ts":"2026-11-12T02:29:56.359Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-12T02:29:56.359Z"},{"op":"add","path":"/metadata/docs","value":[{"path":"docs/identity-collisions.md","scope":"project"}]}],"before_hash":"171492f32730caabe628ab01830d1c447758604c98a741aaff8bfd2804464d90","after_hash":"41b555ea9d39afc68156aa30bf5e9df5d42ac5096ab6f5b1efbe2bf6d1dbe493","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":"5f59b02a0bd228263cdc18212e62d1403ca6cf53bb54c0248ef2d5348ca83adb"} +{"hash_algorithm":"sha256","ts":"2026-11-12T08:46:15.138Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-12T08:46:15.138Z"},{"op":"remove","path":"/metadata/tags/2"},{"op":"replace","path":"/metadata/tags/1","value":"review-followup"},{"op":"replace","path":"/metadata/tags/0","value":"gh:unbraind/pm-github#236"}],"before_hash":"41b555ea9d39afc68156aa30bf5e9df5d42ac5096ab6f5b1efbe2bf6d1dbe493","after_hash":"d2192cab6f2ade250312e68b4232264bba03c1583410c5eef73bdd7ee8965529","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":"933bc8f67a068c1d2abbf38b8ed963702694004f49b7c5ddb340ef7431c9b44b"} +{"hash_algorithm":"sha256","ts":"2026-11-12T08:46:16.027Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"docs_add","patch":[{"op":"add","path":"/metadata/docs/1","value":{"path":"https://github.com/unbraind/pm-github/pull/233","scope":"project","note":"Review source; issue 236 remains the sole GitHub identity"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T08:46:16.027Z"}],"before_hash":"d2192cab6f2ade250312e68b4232264bba03c1583410c5eef73bdd7ee8965529","after_hash":"82b5f464b6506a79d85d6737c4ae78668c399d5915d6795ba98a41227a134b2a","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":"4d626091f4e5a85d78c772f78872b20a0024566561f00e8da3f30020cb3ccbdc"} +{"hash_algorithm":"sha256","ts":"2026-11-12T08:46:16.857Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-12T08:46:16.856Z","author":"codex-github230-1111","text":"Round 9 corrects this record only: removed the auxiliary gh reference to PR 233 so its identity agrees with issue 236. The PR is retained as a docs link. Existing history and the deferred implementation scope are unchanged."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T08:46:16.857Z"}],"before_hash":"82b5f464b6506a79d85d6737c4ae78668c399d5915d6795ba98a41227a134b2a","after_hash":"d366a3c57ef1f0a921ec10659c1dcc403fee8bca17b5836a7a658940348a9d82","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":"45b07db8e5a8e32491420f5a4725c42d65191067646acf9877d0e2215cac1871"} +{"hash_algorithm":"sha256","ts":"2026-11-12T10:16:19.276Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-12T10:16:19.276Z","author":"codex-github230-1111","text":"The priority dependency-index ambiguity from issue 236 is addressed in PR 233 round 10: a second claimant through external_ref is included by shared githubOwnershipClaims, reports ambiguous_github_dependency_owner, and writes no edge. Regression coverage uses real SDK trackers and also checks endpoint ownership under the native mutation lock. Other deferred observations retain their existing scope."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T10:16:19.276Z"}],"before_hash":"d366a3c57ef1f0a921ec10659c1dcc403fee8bca17b5836a7a658940348a9d82","after_hash":"f61edcc388da244378879cbcea2e0d5fd94422ae5e64939f8fc1e4e4000cb7fc","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":"601d3870be92740c3dc946252290132de9a93b70a536c12914787a378c4f7f11"} +{"hash_algorithm":"sha256","ts":"2026-11-12T17:35:01.802Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-12T17:35:01.802Z","author":"codex-github230-1111","text":"Round 13 optional duplication check found a pre-existing five-line exact clone in projects.ts:630-634 and 699-701, in Project ownership-index setup. projects.ts has identical blob 71df0c3606f90feaeb53e1db6d415c57285d3530 at orchestrator base 5f6749f and in this round; no Project production logic was changed. npm run duplication reports one 53-token clone at the unchanged zero threshold. Preserve the selected-item-only scoped pull behavior when consolidating this setup. Deferred here rather than expanding the three requested fixes; required round-13 tests and checks are green."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T17:35:01.802Z"}],"before_hash":"f61edcc388da244378879cbcea2e0d5fd94422ae5e64939f8fc1e4e4000cb7fc","after_hash":"d4dae6b5105709d407517bd361ec6eef814a4b07f4c172e27ff164c8afa82897","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":"9523090724fe0224c8cce43ff9128503c3ff951b39d5673e1cd9173c0c597c6f"} +{"hash_algorithm":"sha256","ts":"2026-11-12T21:39:21.517Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-12T21:39:21.517Z","author":"codex-github230-1111","text":"Verification note for the earlier projects.ts duplication observation: the orchestrator consolidated selected-only Project indexing through indexSelectedByIssue before this round (base 4de7d4c). The merged candidate passes the unchanged zero-threshold npm run duplication gate: 34 authored modules, 15484 lines, zero duplicated lines. projects.ts is unchanged in this fix round; the selected-only scoped behavior is retained. Keep this follow-up open for its remaining observations."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T21:39:21.517Z"}],"before_hash":"d4dae6b5105709d407517bd361ec6eef814a4b07f4c172e27ff164c8afa82897","after_hash":"2d9469319cc133464a55a553bed8b1b17c7b533746c352f1729b10f9719733c9","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":"81dd98e8ccd364fa89dc1d9df60a26c869af4002b468fc9b38e78d64bd3fce0e"} +{"hash_algorithm":"sha256","ts":"2026-11-12T23:05:14.712Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-12T23:05:14.712Z","author":"codex-github230-1111","text":"Read-only correction-round evidence for deferred verification: pm-github state-all atomic gated preview refuses existing public issue-body privacy findings (issues 160 and 202); the released baseline separately refuses duplicate reference tags. No data was applied. This is outside the restored legacy-declaration fix and is not a successful end-to-end gate result. The no-additional-create compatibility comparison passes against accepted pre-correction 4de7d4c across pm-github, pm-review, pm-rust, pm-context and pm-linear. Released origin/main still uses the reference-tag identity rule, so its lower create counts cannot be an ownership-compatible baseline for issue 230. Keep this follow-up open; no review thread was answered or resolved."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T23:05:14.712Z"}],"before_hash":"2d9469319cc133464a55a553bed8b1b17c7b533746c352f1729b10f9719733c9","after_hash":"ae6016ad8fbb3fb9c799dca50fd82e23b97208120342aa2ca769ee73b214c223","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":"f0b266fe1581ec58110df1e471176096ee24451e25615c29ce48c6e88e4c5034"} diff --git a/.agents/pm/history/pm-github-reference-identity.jsonl b/.agents/pm/history/pm-github-reference-identity.jsonl new file mode 100644 index 00000000..713745c6 --- /dev/null +++ b/.agents/pm/history/pm-github-reference-identity.jsonl @@ -0,0 +1,268 @@ +{"hash_algorithm":"sha256","ts":"2026-11-11T10:19:25.073Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-reference-identity"},{"op":"add","path":"/metadata/title","value":"Keep GitHub reference tags separate from import identity"},{"op":"add","path":"/metadata/description","value":"Preserve curated local fields, allow shared reference tags, and retain migration-free matching of existing importer provenance."},{"op":"add","path":"/metadata/type","value":"Issue"},{"op":"add","path":"/metadata/status","value":"open"},{"op":"add","path":"/metadata/priority","value":2},{"op":"add","path":"/metadata/tags","value":["gh:unbraind/pm-github#230"]},{"op":"add","path":"/metadata/created_at","value":"2026-11-11T10:19:25.073Z"},{"op":"add","path":"/metadata/updated_at","value":"2026-11-11T10:19:25.073Z"},{"op":"add","path":"/metadata/author","value":"codex-github230-1111"}],"before_hash":"3cc22dff72be7b14824654a7a64ea62b04799939b2fee54c1b5f52ca60bf6df0","after_hash":"f4f017cab850a9032bb14dfbe90dd40454ce6ca39ae9b79b0df8677dcd6111d4","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":"7e70a1b908179a24f1d331b4048138a60178b9ab72b19f4b676f2ba7994824bc"} +{"hash_algorithm":"sha256","ts":"2026-11-11T10:19:26.036Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-11T10:19:26.036Z"},{"op":"add","path":"/metadata/assignee","value":"codex-github230-1111"},{"op":"add","path":"/metadata/claim_principal","value":"codex-github230-1111"}],"before_hash":"f4f017cab850a9032bb14dfbe90dd40454ce6ca39ae9b79b0df8677dcd6111d4","after_hash":"7def8e53f5a60245a514056737443176e09e21b390ac5b357a281d53a45b3c95","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":"4a21464fc722e1830168bbdb13263a4946edfc3a28a31ca4b1eaee68c9d6519c"} +{"hash_algorithm":"sha256","ts":"2026-11-11T10:19:27.187Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-11T10:19:27.187Z"},{"op":"replace","path":"/metadata/status","value":"in_progress"}],"before_hash":"7def8e53f5a60245a514056737443176e09e21b390ac5b357a281d53a45b3c95","after_hash":"278297d2aee3e40a1c911e20698ac919b61d68673dd05494fc91abc239131cc5","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":"c32c3e53effd8b74271910f36e126a72cba97eee6375c614215484f71724d47c"} +{"hash_algorithm":"sha256","ts":"2026-11-11T10:19:28.757Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-11T10:19:28.757Z"},{"op":"add","path":"/metadata/comments","value":[{"created_at":"2026-11-11T10:19:28.756Z","author":"codex-github230-1111","text":"Read issue 230 and all comments. Branch starts from main; PR229 changes workflows and docs separately. Plan: distinguish explicit importer identity from references, preserve curated linked content, retain legacy persisted provenance, and prove behavior with real SDK trackers and behavioral reverts. Task-specific gate budget applies: focused importer/sync/dependent tests, lint and typecheck; full suite only within 45 minutes."}]}],"before_hash":"278297d2aee3e40a1c911e20698ac919b61d68673dd05494fc91abc239131cc5","after_hash":"4c733d1f30418481d37fa7089d75e6f28099107c67e8b62b33e71f6e3fceb18e","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":"1112fbd69f366d10022296dbc4516043703b7014ba6bcb400bcf65511ef466e7"} +{"hash_algorithm":"sha256","ts":"2026-11-11T10:24:30.654Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-11T10:24:30.654Z","author":"codex-github230-1111","text":"Implemented separate durable import identity lookup; shared gh references no longer select owners or collide. Curated links merge provenance/lifecycle while deterministic importer records mirror content. New real SDK regressions pass 9/9, including no-git curated links and byte-identical legacy resync beside shared references. Reviewing existing tests that assumed tag-only ownership; updating mirror fixtures to explicit importer identities."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-11T10:24:30.654Z"}],"before_hash":"4c733d1f30418481d37fa7089d75e6f28099107c67e8b62b33e71f6e3fceb18e","after_hash":"ce30cbb2852a32f13e61b7000793e31268742debc86b7237821dda95ffce49ef","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":"91b63ff0238e79f17829a069937e7aeb83b6ac0c400ee0da8ef1ea50673b48fe"} +{"hash_algorithm":"sha256","ts":"2026-11-11T10:30:36.318Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-11T10:30:36.318Z"},{"op":"add","path":"/metadata/files","value":[{"path":".github/workflows/pm-github-sync.yml","scope":"project"},{"path":"identity.ts","scope":"project"},{"path":"import-fidelity.ts","scope":"project"},{"path":"import-tags.ts","scope":"project"},{"path":"index.ts","scope":"project"},{"path":"refresh.ts","scope":"project"},{"path":"test/import-conflict-report.test.ts","scope":"project"},{"path":"test/reference-import-identity.test.ts","scope":"project"}]}],"before_hash":"ce30cbb2852a32f13e61b7000793e31268742debc86b7237821dda95ffce49ef","after_hash":"ea20de7e1dbba4d38552e627a4b6ee43e868c7988dfc5159ad601aa18e9ade18","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":"31b10e3bbb6a564bfa036fbcdfc0f29e1b280b94e2bdbcba86faf9be750a063e"} +{"hash_algorithm":"sha256","ts":"2026-11-11T10:30:37.373Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-11T10:30:37.373Z"},{"op":"add","path":"/metadata/docs","value":[{"path":"README.md","scope":"project"}]}],"before_hash":"ea20de7e1dbba4d38552e627a4b6ee43e868c7988dfc5159ad601aa18e9ade18","after_hash":"6ec00d41b0f1d7f8c858c21f6a43b2d3fec8fff93d28a11c0fe8dd805fe431ad","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":"5c96d209699142eec83241357171ba1a0b440c900a38230e47d0690ab869abec"} +{"hash_algorithm":"sha256","ts":"2026-11-11T10:32:07.581Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-11T10:32:07.581Z"},{"op":"add","path":"/metadata/tests","value":[{"command":"node --test --test-concurrency=2 test/reference-import-identity.test.ts test/import-conflict-report.test.ts","scope":"project","timeout_seconds":1200,"provenance":{"author":"codex-github230-1111","created_at":"2026-11-11T10:32:07.555Z","source_kind":"local_mutation","source_ref":"fix/reference-tags-are-not-import-identity"}}]}],"before_hash":"6ec00d41b0f1d7f8c858c21f6a43b2d3fec8fff93d28a11c0fe8dd805fe431ad","after_hash":"1a57b53514d0cd58d467a6a6aefa46dfb73d03ad7789862c3bd7d7ee7b24f99d","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":"e3a34a04a05448400601c2f6045c7e714c60aed0f7a4a5808f64ab577ff7e36e"} +{"hash_algorithm":"sha256","ts":"2026-11-11T10:33:45.353Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-11T10:33:45.352Z","author":"codex-github230-1111","text":"Behavioral revert proof in a disposable package copy: rebuilt after restoring released matchImportedIssue, prepareGithubImport and verifyImportIdempotency plus tag-only identity lookup. node --test --test-concurrency=2 test/reference-import-identity.test.ts: 14 tests, 13 fail, 1 pass. All four required scenarios fail: single curated Chore, shared references, mirror title refresh beside a reference, and byte-identical legacy resync beside a reference. The passing legacy link-marker case is existing supported behavior. Package worktree was never reverted. Fixed regressions are being rerun through pm test; full npm test follows within the task budget."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-11T10:33:45.353Z"}],"before_hash":"1a57b53514d0cd58d467a6a6aefa46dfb73d03ad7789862c3bd7d7ee7b24f99d","after_hash":"7ece1129db9689d5f9e162e734e15e02b44493bc9dd42859481e5695345b87da","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":"6aebb870bce4fb073a2f8977b6584844f1cfbf7de924b96630e89d6b2e29b547"} +{"hash_algorithm":"sha256","ts":"2026-11-11T10:39:18.556Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/cited-issue-adoption.test.ts"},{"op":"replace","path":"/metadata/files/6/path","value":"test/atomic.test.ts"},{"op":"add","path":"/metadata/files/8","value":{"path":"test/dryrun-preview.test.ts","scope":"project"}},{"op":"add","path":"/metadata/files/9","value":{"path":"test/fixtures/refresh-crash.ts","scope":"project"}},{"op":"add","path":"/metadata/files/10","value":{"path":"test/helpers/import-mirror.ts","scope":"project"}},{"op":"add","path":"/metadata/files/11","value":{"path":"test/identity.test.ts","scope":"project"}},{"op":"add","path":"/metadata/files/12","value":{"path":"test/import-conflict-report.test.ts","scope":"project"}},{"op":"add","path":"/metadata/files/13","value":{"path":"test/import-errors.test.ts","scope":"project"}},{"op":"add","path":"/metadata/files/14","value":{"path":"test/import-fidelity.test.ts","scope":"project"}},{"op":"add","path":"/metadata/files/15","value":{"path":"test/import-gate.test.ts","scope":"project"}},{"op":"add","path":"/metadata/files/16","value":{"path":"test/import-projection.test.ts","scope":"project"}},{"op":"add","path":"/metadata/files/17","value":{"path":"test/link-deps.test.ts","scope":"project"}},{"op":"add","path":"/metadata/files/18","value":{"path":"test/reference-import-identity.test.ts","scope":"project"}},{"op":"add","path":"/metadata/files/19","value":{"path":"test/refresh.test.ts","scope":"project"}},{"op":"add","path":"/metadata/files/20","value":{"path":"test/repeat-import-noop.test.ts","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-11T10:39:18.556Z"}],"before_hash":"7ece1129db9689d5f9e162e734e15e02b44493bc9dd42859481e5695345b87da","after_hash":"ee02039a82c8425b69b714d23b8851e5b32d9c5c69e70c2ba204b96159c3032d","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":"5de5d6055d4c838e7df49547374d40bc8a38aab81be2226b020f57bdd6fe63fc"} +{"hash_algorithm":"sha256","ts":"2026-11-11T10:39:21.378Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/docs/0/path","value":"docs/cited-issue-adoption-validation.md"},{"op":"add","path":"/metadata/docs/1","value":{"path":"docs/identity-collisions.md","scope":"project"}},{"op":"add","path":"/metadata/docs/2","value":{"path":"README.md","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-11T10:39:21.378Z"}],"before_hash":"ee02039a82c8425b69b714d23b8851e5b32d9c5c69e70c2ba204b96159c3032d","after_hash":"07e4ee3d36717eab1ba0e2abe2585939376dca7a6708a352ad029475962f9f18","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":"d9720dc1b32b4d4cf5bdcb3712a6817a1ec90c4dbf3634334969e395bb432f25"} +{"hash_algorithm":"sha256","ts":"2026-11-11T10:39:23.619Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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":"npm run lint","scope":"project","timeout_seconds":1200,"provenance":{"author":"codex-github230-1111","created_at":"2026-11-11T10:39:23.496Z","source_kind":"local_mutation","source_ref":"fix/reference-tags-are-not-import-identity"}}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-11T10:39:23.619Z"}],"before_hash":"07e4ee3d36717eab1ba0e2abe2585939376dca7a6708a352ad029475962f9f18","after_hash":"6751c2a04c65d4430707de47745b155118bcef15c44c9c1ccc65f85e32a74f41","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":"e676510c5104e2904082daad8e19e39caa032f2cb81ff85f3cd0ab7d7fa333d6"} +{"hash_algorithm":"sha256","ts":"2026-11-11T10:39:25.629Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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":"npm run typecheck","scope":"project","timeout_seconds":1200,"provenance":{"author":"codex-github230-1111","created_at":"2026-11-11T10:39:25.518Z","source_kind":"local_mutation","source_ref":"fix/reference-tags-are-not-import-identity"}}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-11T10:39:25.629Z"}],"before_hash":"6751c2a04c65d4430707de47745b155118bcef15c44c9c1ccc65f85e32a74f41","after_hash":"3455808d1eb73a52b97efbe3e1dc2e1eff8334295b5e331aae9619efc7391604","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":"65aa73789557b46589976d01a847e65adf3e073b669dadd58fe4de420cbd0161"} +{"hash_algorithm":"sha256","ts":"2026-11-11T10:39:28.500Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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":"npm test","scope":"project","timeout_seconds":2700,"provenance":{"author":"codex-github230-1111","created_at":"2026-11-11T10:39:28.387Z","source_kind":"local_mutation","source_ref":"fix/reference-tags-are-not-import-identity"}}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-11T10:39:28.500Z"}],"before_hash":"3455808d1eb73a52b97efbe3e1dc2e1eff8334295b5e331aae9619efc7391604","after_hash":"16b71ec8990f28a8a71f2f0161f6a65ea3f7cd7f0d7cdaa27877dc294f4fe1df","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":"2b5d4da8284d38aeeffc98a9b49535fcf476f9cb8d8aaa3d9d55ad39140a9198"} +{"hash_algorithm":"sha256","ts":"2026-11-11T10:39:30.904Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-11T10:39:30.901Z","author":"codex-github230-1111","text":"Linked pm test run passed 16/16 (real SDK trackers, both import modes, no-git linking, legacy compatibility, safe PR conflict rendering and dependency routing). Lint and typecheck pass; docstring gate documents 29 files/280 declarations; duplication gate has zero clones at the unchanged 50-token threshold across 30 authored modules. Added seeded preview/apply equivalence coverage. Full npm test is running with two workers and a 45-minute cap; npm/bun release:check and coverage are delegated to CI under the explicit task gate budget."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-11T10:39:30.904Z"}],"before_hash":"16b71ec8990f28a8a71f2f0161f6a65ea3f7cd7f0d7cdaa27877dc294f4fe1df","after_hash":"86e3947b3107d7c557e79d70bd2627c06941079800a700a379c5bb003ce5e920","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":"81d211a9f7a6887015c34fcf7b2ebe03dc0fafb071dbccfd8f168966fa82582d"} +{"hash_algorithm":"sha256","ts":"2026-11-11T10:46:47.851Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-11T10:46:47.851Z"},{"op":"add","path":"/metadata/github_number","value":230},{"op":"add","path":"/metadata/github_url","value":"https://github.com/unbraind/pm-github/issues/230"}],"before_hash":"86e3947b3107d7c557e79d70bd2627c06941079800a700a379c5bb003ce5e920","after_hash":"b6867837c55bc960d5e815fcea467b5541fe504ab04e5d49394887d6c514d5e8","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":"f62c94bff1f3cd54426b63370076dc54df5e90884bfc288ccbc3e457f9e8b6be"} +{"hash_algorithm":"sha256","ts":"2026-11-11T10:55:16.029Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-11T10:55:16.026Z","author":"codex-github230-1111","text":"Optional full npm test was stopped under the task gate budget: an unrelated closeout workflow file had already used over 11 minutes before its large decision matrix. No failures were reported in the observed output before cancellation; this is incomplete evidence, not a passing full suite. Running all 46 required importer/sync/direct-dependent and sync-workflow test files separately now, with two file workers and a hard 1200-second limit per file. CI owns full-suite and release certification."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-11T10:55:16.029Z"}],"before_hash":"b6867837c55bc960d5e815fcea467b5541fe504ab04e5d49394887d6c514d5e8","after_hash":"74b6f70bb21f88cf73b8738091f96fe5de3922a24ba5d52c44f24c6925ce5510","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":"617ccfe5fc5b2d8ef4df85e050d7b3950c97ea97ddf570fb17d3fe3337f33e35"} +{"hash_algorithm":"sha256","ts":"2026-11-11T11:05:45.371Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/20/path","value":"test/link-deps.test.ts"},{"op":"replace","path":"/metadata/files/19/path","value":"test/import-projection.test.ts"},{"op":"replace","path":"/metadata/files/18/path","value":"test/import-gate.test.ts"},{"op":"replace","path":"/metadata/files/17/path","value":"test/import-fidelity.test.ts"},{"op":"replace","path":"/metadata/files/16/path","value":"test/import-errors.test.ts"},{"op":"replace","path":"/metadata/files/15/path","value":"test/import-conflict-report.test.ts"},{"op":"replace","path":"/metadata/files/14/path","value":"test/identity.test.ts"},{"op":"replace","path":"/metadata/files/13/path","value":"test/helpers/import-mirror.ts"},{"op":"replace","path":"/metadata/files/12/path","value":"test/handler-failures.test.ts"},{"op":"replace","path":"/metadata/files/11/path","value":"test/fixtures/refresh-crash.ts"},{"op":"replace","path":"/metadata/files/10/path","value":"test/dryrun-preview.test.ts"},{"op":"replace","path":"/metadata/files/9/path","value":"test/daily-sync-idempotency.test.ts"},{"op":"replace","path":"/metadata/files/8/path","value":"test/coverage-remainder.test.ts"},{"op":"add","path":"/metadata/files/21","value":{"path":"test/reference-import-identity.test.ts","scope":"project"}},{"op":"add","path":"/metadata/files/22","value":{"path":"test/refresh.test.ts","scope":"project"}},{"op":"add","path":"/metadata/files/23","value":{"path":"test/repeat-import-noop.test.ts","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-11T11:05:45.371Z"}],"before_hash":"74b6f70bb21f88cf73b8738091f96fe5de3922a24ba5d52c44f24c6925ce5510","after_hash":"7fc99a1618e95840cca6a0eeda10a2aba49df68086fc2c16ae2f334216728831","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":"17ef35fdd504bdfef2f96f56c3682f87069a01cf36771aa792f86f36c377ba6f"} +{"hash_algorithm":"sha256","ts":"2026-11-11T11:09:29.506Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-11T11:09:29.505Z","author":"codex-github230-1111","text":"The audit also found a separate Projects v2 board-import reference fallback. Tracked static evidence and required real-SDK reproduction in pm-github-project-reference-followup; board ownership/mirroring is a separate model from the gated REST issue importer and was not changed here."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-11T11:09:29.506Z"}],"before_hash":"7fc99a1618e95840cca6a0eeda10a2aba49df68086fc2c16ae2f334216728831","after_hash":"06b5d1b7f3df7f4f3f1f74c1518e89ab3dcd47a1e1800f4ba7cc271f396c62a3","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":"ccd30ce5c176c26e874af4505e18d393934fd3cef168d086092da6d626cd68de"} +{"hash_algorithm":"sha256","ts":"2026-11-11T11:38:47.230Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/23/path","value":"test/refresh.test.ts"},{"op":"replace","path":"/metadata/files/22/path","value":"test/reference-import-identity.test.ts"},{"op":"replace","path":"/metadata/files/21/path","value":"test/link-deps.test.ts"},{"op":"replace","path":"/metadata/files/20/path","value":"test/label-import-model.test.ts"},{"op":"add","path":"/metadata/files/24","value":{"path":"test/repeat-import-noop.test.ts","scope":"project"}},{"op":"add","path":"/metadata/files/25","value":{"path":"test/scheduled-refusals.test.ts","scope":"project"}},{"op":"add","path":"/metadata/files/26","value":{"path":"test/typed-sentinel-normalization.test.ts","scope":"project"}},{"op":"add","path":"/metadata/files/27","value":{"path":"test/webhooks.test.ts","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-11T11:38:47.230Z"}],"before_hash":"06b5d1b7f3df7f4f3f1f74c1518e89ab3dcd47a1e1800f4ba7cc271f396c62a3","after_hash":"4a776892f0dab73146d77a57ae4cfdfd6c2f5075b71f05fa4992b1751f6504bf","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":"99c9ea77bbc0de1f4378f0a0ff4a1ee8b1d4499939d7ac71a2a48af7c6e60dc5"} +{"hash_algorithm":"sha256","ts":"2026-11-11T11:38:48.163Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-11T11:38:48.163Z","author":"codex-github230-1111","text":"Final fixture reruns passed: import-fidelity 53 pass/1 existing SDK quarantine; handler failures 38/38; import projection 6/6; label-model properties 3/3 (including both 500-case SDK corpora); daily-sync properties 3/3. The new regression files pass 18/18, including mixed-case legacy URLs. Corrected an embedded child-script fixture import and verified coverage-remainder 25/25; repeating its final file with explicit pinned-CLI PATH. Standalone new regressions now pin their child CLI too."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-11T11:38:48.163Z"}],"before_hash":"4a776892f0dab73146d77a57ae4cfdfd6c2f5075b71f05fa4992b1751f6504bf","after_hash":"d2e1f4f1f165861f4995dc296114bfc50e7899223d530de6336af79d499ee95a","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":"e58cffe01edb9b13bea698ceec9f15cdfb94313ffca9338683314f40e541a696"} +{"hash_algorithm":"sha256","ts":"2026-11-11T11:42:48.058Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-11T11:42:48.057Z","author":"codex-github230-1111","text":"Final local evidence: 46 selected files attempted with two workers and a 1200-second limit per file. Completed 45 files: 869 tests, 867 pass, zero fail, two unchanged SDK quarantines. import-gate.test.ts reached its 20-minute cap after 63 passes and one canceled test; it is incomplete CI evidence. Every other file passed after fixture corrections. Exact file command was node --test --test-concurrency=1 followed by that file, with the repository CLI first on PATH and global Git configuration disabled. The linked regression command was rerun through pm test --run --progress after the disposable behavioral revert: 18/18 pass. Latest revert: 17 tests, 16 fail, one legacy link-marker pass; all four required scenarios fail. Lint, typecheck, test compilation, 280 documented declarations and zero duplication at the unchanged threshold pass. Full npm test, coverage and npm/Bun release certification remain for CI. No consumer holds or trackers were changed."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-11T11:42:48.058Z"}],"before_hash":"d2e1f4f1f165861f4995dc296114bfc50e7899223d530de6336af79d499ee95a","after_hash":"a0ea1a8951950fa312f76c9a74a3aea5269fc82acc458031d29723f974c61679","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":"fe9b03a83a113644d20d0e9f0d07c016097190ad89e2245e19603fa1d1c8c60a"} +{"hash_algorithm":"sha256","ts":"2026-11-11T11:42:48.949Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-11T11:42:48.949Z"}],"before_hash":"a0ea1a8951950fa312f76c9a74a3aea5269fc82acc458031d29723f974c61679","after_hash":"2264a5aa7ed27e4c86211df781c463191daab45b3bfe3fbf494e3b0c32d42c14","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":"b6d7d7fe640e9a089304e45be9835e656aa1bdce52b531fa1d3577f6ab9c66ab"} +{"hash_algorithm":"sha256","ts":"2026-11-11T11:47:32.447Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-11T11:47:32.447Z"},{"op":"add","path":"/metadata/tags/1","value":"gh:unbraind/pm-github#233"}],"before_hash":"2264a5aa7ed27e4c86211df781c463191daab45b3bfe3fbf494e3b0c32d42c14","after_hash":"c3a0b23dffa963138e6414818f4f2a1ff2f096aa81c86977eacc357b760035b1","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":"3460b3d3607027dd3341b73711fa30e9915e410a3d8b4c13c6735c0b6a36a50e"} +{"hash_algorithm":"sha256","ts":"2026-11-11T11:47:33.245Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-11T11:47:33.244Z","author":"codex-github230-1111","text":"PR: https://github.com/unbraind/pm-github/pull/233 . Code commit e9e0c9a. The claim is released and this item stays in_progress for orchestrator verification. Final privacy scan passed across 9334 Git objects with approved identities and no secret or host-path findings; tracker diff gate passed. Strict health passed with isolated global extension discovery; ambient discovery had an SDK version skew, while integrity/history remained valid. Full-suite, timed-out import-gate completion, coverage and npm/Bun release checks remain for CI. Projects v2 board fallback remains separately tracked in pm-github-project-reference-followup."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-11T11:47:33.245Z"}],"before_hash":"c3a0b23dffa963138e6414818f4f2a1ff2f096aa81c86977eacc357b760035b1","after_hash":"9f6665d8870b5ab50e9c72ec0f6ad6ee225ded04fc72c29258c2d5ef1f28c306","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":"4f06b237b682b4a9e78dbd6036b48eed5d71640c713572de82d35e16a0c92ba8"} +{"hash_algorithm":"sha256","ts":"2026-11-11T14:06:26.271Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-11T14:06:26.271Z"},{"op":"add","path":"/metadata/assignee","value":"codex-github230-1111"},{"op":"add","path":"/metadata/claim_principal","value":"codex-github230-1111"}],"before_hash":"9f6665d8870b5ab50e9c72ec0f6ad6ee225ded04fc72c29258c2d5ef1f28c306","after_hash":"b722e300318d82b3b0fe6e3e311a9846cc8db9fc90a413772eaa4bab2260ec97","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":"d7830374c64e56589f890eebd92db45721943afe6c6e3d1d01fa37f90f39b2f0"} +{"hash_algorithm":"sha256","ts":"2026-11-11T14:06:28.713Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-11T14:06:28.712Z","author":"codex-github230-1111","text":"Round 2 resumes from ed050d3 with a clean worktree. Read all four unresolved threads and the complete latest CodeRabbit review body, including its outside-diff upstream-target finding and scope-check note. Plan: synchronize all non-content side data for curated links; reject contradictory explicit identities before writes with normalized compatibility; persist/replay atomic import plans through retries; and guard outbound status/export targets. Keep review threads open for the orchestrator. Existing Projects follow-up audit records remain append-only as required by the task rules."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-11T14:06:28.713Z"}],"before_hash":"b722e300318d82b3b0fe6e3e311a9846cc8db9fc90a413772eaa4bab2260ec97","after_hash":"766fa6ee362153b734cb3b5504484ba0d6d193c14c49df28d52305832179c674","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":"90a603824754a2780f6e308aefe2319dcb8f5a565c8994f8b0e52fe41f485008"} +{"hash_algorithm":"sha256","ts":"2026-11-11T14:23:38.805Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-11T14:23:38.805Z","author":"codex-github230-1111","text":"Round 2 side-data inventory: after a content merge, the mirror path applies native lifecycle transitions, synchronizes GitHub comments as annotations, persists github_* source dates/state/closure provenance, reports counts, links dependencies, and runs provenance/privacy gates. Curated updates now perform the same non-content steps. Body-only fetched comments route to annotations for curated items. SDK transaction contract uses payload-bound ordered steps: recovery replays the recorded original entries, mutations, targets, comments and dependency inputs before polling new GitHub data; a later normal sync observes changed input. Identity validation shares one normalized explicit-identity path across import, refresh, sync and export; contradictory values refuse before writes. Outbound sync/export now ignore shared references by default and offer explicit historical tag-only opt-in with competing-owner refusal."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-11T14:23:38.805Z"}],"before_hash":"766fa6ee362153b734cb3b5504484ba0d6d193c14c49df28d52305832179c674","after_hash":"3cf4b9690e48ec81ec39d977c5f37d7e47feb295a76416cbd48a5bac1078cfe2","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":"dd48b993f2c078b8972dd71c154a045ef781a1dce22cd381a2475c5362b1a225"} +{"hash_algorithm":"sha256","ts":"2026-11-11T14:32:50.303Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/27/path","value":"test/smoke.test.ts"},{"op":"replace","path":"/metadata/files/26/path","value":"test/scheduled-refusals.test.ts"},{"op":"replace","path":"/metadata/files/25/path","value":"test/review-import-identity.test.ts"},{"op":"add","path":"/metadata/files/28","value":{"path":"test/typed-sentinel-normalization.test.ts","scope":"project"}},{"op":"add","path":"/metadata/files/29","value":{"path":"test/webhooks.test.ts","scope":"project"}},{"op":"add","path":"/metadata/files/30","value":{"path":"test/workspace-resolution.test.ts","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-11T14:32:50.303Z"}],"before_hash":"3cf4b9690e48ec81ec39d977c5f37d7e47feb295a76416cbd48a5bac1078cfe2","after_hash":"548555a399c5622da165494172e40622f3027ce18c31b564266fe1cdbe39340c","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":"132b85c37402d9f2965acc4202b32f3b9a9479616ba6883dc62a598ec548d87b"} +{"hash_algorithm":"sha256","ts":"2026-11-11T14:32:52.624Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-concurrency=1 test/review-import-identity.test.ts","scope":"project","timeout_seconds":1200,"provenance":{"author":"codex-github230-1111","created_at":"2026-11-11T14:32:52.519Z","source_kind":"local_mutation","source_ref":"fix/reference-tags-are-not-import-identity"}}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-11T14:32:52.624Z"}],"before_hash":"548555a399c5622da165494172e40622f3027ce18c31b564266fe1cdbe39340c","after_hash":"0cbd92aad8308c2b6111b54baea06880a32451290ff3ee351aec6e10bbbf74a5","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":"2b5c3d5d29086b3ec93fb6aabf75ae1b63a9c203521c5191627617d45e46e39b"} +{"hash_algorithm":"sha256","ts":"2026-11-11T14:34:18.886Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-11T14:34:18.885Z","author":"codex-github230-1111","text":"Review regression baseline: 21 real-SDK cases passed across curated side data, explicit-identity refusal and spelling compatibility, six original-plan crash boundaries, upstream plan conflicts and actual sync/export HTTP writes. Added a further dependency replay case for an unchanged item in a mixed batch: record the complete original dependency input, interrupt, change the remote body, then recover the original edge. Replacing the prior fake incomplete-compensation test with a real SDK transaction also checks that compensation recovery reuses the saved plan. Broad gates cover 49 affected/import/refresh/identity/sync files, two workers, each invocation capped at 20 minutes; import-gate is partitioned by exhaustive disjoint test-name prefixes."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-11T14:34:18.886Z"}],"before_hash":"0cbd92aad8308c2b6111b54baea06880a32451290ff3ee351aec6e10bbbf74a5","after_hash":"b213042eea60501c5232c2717c51dd48df39277d842e8dd1bc2f2da3babfe079","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":"3a4b55a9e90c31bed80f61d30c94dc5859085c53c7d64c0301222386407111fe"} +{"hash_algorithm":"sha256","ts":"2026-11-11T14:42:51.192Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/30/path","value":"test/webhooks.test.ts"},{"op":"replace","path":"/metadata/files/29/path","value":"test/typed-sentinel-normalization.test.ts"},{"op":"replace","path":"/metadata/files/28/path","value":"test/smoke.test.ts"},{"op":"replace","path":"/metadata/files/27/path","value":"test/scheduled-refusals.test.ts"},{"op":"replace","path":"/metadata/files/26/path","value":"test/review-import-identity.test.ts"},{"op":"replace","path":"/metadata/files/25/path","value":"test/repeat-import-noop.test.ts"},{"op":"replace","path":"/metadata/files/24/path","value":"test/refresh.test.ts"},{"op":"replace","path":"/metadata/files/23/path","value":"test/reference-import-identity.test.ts"},{"op":"replace","path":"/metadata/files/22/path","value":"test/link-deps.test.ts"},{"op":"replace","path":"/metadata/files/21/path","value":"test/label-import-model.test.ts"},{"op":"replace","path":"/metadata/files/20/path","value":"test/import-projection.test.ts"},{"op":"replace","path":"/metadata/files/19/path","value":"test/import-gate.test.ts"},{"op":"replace","path":"/metadata/files/18/path","value":"test/import-fidelity.test.ts"},{"op":"replace","path":"/metadata/files/17/path","value":"test/import-errors.test.ts"},{"op":"replace","path":"/metadata/files/16/path","value":"test/import-conflict-report.test.ts"},{"op":"replace","path":"/metadata/files/15/path","value":"test/identity.test.ts"},{"op":"replace","path":"/metadata/files/14/path","value":"test/helpers/import-mirror.ts"},{"op":"replace","path":"/metadata/files/13/path","value":"test/handler-failures.test.ts"},{"op":"replace","path":"/metadata/files/12/path","value":"test/fixtures/refresh-crash.ts"},{"op":"replace","path":"/metadata/files/11/path","value":"test/dryrun-preview.test.ts"},{"op":"replace","path":"/metadata/files/10/path","value":"test/daily-sync-idempotency.test.ts"},{"op":"replace","path":"/metadata/files/9/path","value":"test/coverage-remainder.test.ts"},{"op":"replace","path":"/metadata/files/8/path","value":"test/coverage-edges.test.ts"},{"op":"add","path":"/metadata/files/31","value":{"path":"test/workspace-resolution.test.ts","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-11T14:42:51.192Z"}],"before_hash":"b213042eea60501c5232c2717c51dd48df39277d842e8dd1bc2f2da3babfe079","after_hash":"b9b50e191480098f2dec6593e141227d0513d4c3da007ff16f5e471fc6796333","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":"6e27a2aa0e4f12dbe1fd73fc57e6f3ebdaa1f88356479bda7e516f495f474ce7"} +{"hash_algorithm":"sha256","ts":"2026-11-11T14:46:32.925Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-concurrency=1 test/atomic.test.ts","scope":"project","timeout_seconds":1200,"provenance":{"author":"codex-github230-1111","created_at":"2026-11-11T14:46:32.891Z","source_kind":"local_mutation","source_ref":"fix/reference-tags-are-not-import-identity"}}},{"op":"add","path":"/metadata/tests/6","value":{"command":"node --test --test-concurrency=1 test/blockers.test.ts","scope":"project","timeout_seconds":1200,"provenance":{"author":"codex-github230-1111","created_at":"2026-11-11T14:46:32.891Z","source_kind":"local_mutation","source_ref":"fix/reference-tags-are-not-import-identity"}}},{"op":"add","path":"/metadata/tests/7","value":{"command":"node --test --test-concurrency=1 test/changelog-placement.test.ts","scope":"project","timeout_seconds":1200,"provenance":{"author":"codex-github230-1111","created_at":"2026-11-11T14:46:32.891Z","source_kind":"local_mutation","source_ref":"fix/reference-tags-are-not-import-identity"}}},{"op":"add","path":"/metadata/tests/8","value":{"command":"node --test --test-concurrency=1 test/cited-issue-adoption.test.ts","scope":"project","timeout_seconds":1200,"provenance":{"author":"codex-github230-1111","created_at":"2026-11-11T14:46:32.891Z","source_kind":"local_mutation","source_ref":"fix/reference-tags-are-not-import-identity"}}},{"op":"add","path":"/metadata/tests/9","value":{"command":"node --test --test-concurrency=1 test/closeout-attribution.test.ts","scope":"project","timeout_seconds":1200,"provenance":{"author":"codex-github230-1111","created_at":"2026-11-11T14:46:32.891Z","source_kind":"local_mutation","source_ref":"fix/reference-tags-are-not-import-identity"}}},{"op":"add","path":"/metadata/tests/10","value":{"command":"node --test --test-concurrency=1 test/closeout-revision.test.ts","scope":"project","timeout_seconds":1200,"provenance":{"author":"codex-github230-1111","created_at":"2026-11-11T14:46:32.891Z","source_kind":"local_mutation","source_ref":"fix/reference-tags-are-not-import-identity"}}},{"op":"add","path":"/metadata/tests/11","value":{"command":"node --test --test-concurrency=1 test/closeout-selection.test.ts","scope":"project","timeout_seconds":1200,"provenance":{"author":"codex-github230-1111","created_at":"2026-11-11T14:46:32.891Z","source_kind":"local_mutation","source_ref":"fix/reference-tags-are-not-import-identity"}}},{"op":"add","path":"/metadata/tests/12","value":{"command":"node --test --test-concurrency=1 test/closeout-workflow.test.ts","scope":"project","timeout_seconds":1200,"provenance":{"author":"codex-github230-1111","created_at":"2026-11-11T14:46:32.891Z","source_kind":"local_mutation","source_ref":"fix/reference-tags-are-not-import-identity"}}},{"op":"add","path":"/metadata/tests/13","value":{"command":"node --test --test-concurrency=1 test/closeout.test.ts","scope":"project","timeout_seconds":1200,"provenance":{"author":"codex-github230-1111","created_at":"2026-11-11T14:46:32.891Z","source_kind":"local_mutation","source_ref":"fix/reference-tags-are-not-import-identity"}}},{"op":"add","path":"/metadata/tests/14","value":{"command":"node --test --test-concurrency=1 test/comments-sync.test.ts","scope":"project","timeout_seconds":1200,"provenance":{"author":"codex-github230-1111","created_at":"2026-11-11T14:46:32.891Z","source_kind":"local_mutation","source_ref":"fix/reference-tags-are-not-import-identity"}}},{"op":"add","path":"/metadata/tests/15","value":{"command":"node --test --test-concurrency=1 test/complete-corpus.test.ts","scope":"project","timeout_seconds":1200,"provenance":{"author":"codex-github230-1111","created_at":"2026-11-11T14:46:32.891Z","source_kind":"local_mutation","source_ref":"fix/reference-tags-are-not-import-identity"}}},{"op":"add","path":"/metadata/tests/16","value":{"command":"node --test --test-concurrency=1 test/coverage-boundaries.test.ts","scope":"project","timeout_seconds":1200,"provenance":{"author":"codex-github230-1111","created_at":"2026-11-11T14:46:32.891Z","source_kind":"local_mutation","source_ref":"fix/reference-tags-are-not-import-identity"}}},{"op":"add","path":"/metadata/tests/17","value":{"command":"node --test --test-concurrency=1 test/coverage-edges.test.ts","scope":"project","timeout_seconds":1200,"provenance":{"author":"codex-github230-1111","created_at":"2026-11-11T14:46:32.891Z","source_kind":"local_mutation","source_ref":"fix/reference-tags-are-not-import-identity"}}},{"op":"add","path":"/metadata/tests/18","value":{"command":"node --test --test-concurrency=1 test/coverage-remainder.test.ts","scope":"project","timeout_seconds":1200,"provenance":{"author":"codex-github230-1111","created_at":"2026-11-11T14:46:32.891Z","source_kind":"local_mutation","source_ref":"fix/reference-tags-are-not-import-identity"}}},{"op":"add","path":"/metadata/tests/19","value":{"command":"node --test --test-concurrency=1 test/daily-sync-alert.test.ts","scope":"project","timeout_seconds":1200,"provenance":{"author":"codex-github230-1111","created_at":"2026-11-11T14:46:32.891Z","source_kind":"local_mutation","source_ref":"fix/reference-tags-are-not-import-identity"}}},{"op":"add","path":"/metadata/tests/20","value":{"command":"node --test --test-concurrency=1 test/daily-sync-idempotency.test.ts","scope":"project","timeout_seconds":1200,"provenance":{"author":"codex-github230-1111","created_at":"2026-11-11T14:46:32.891Z","source_kind":"local_mutation","source_ref":"fix/reference-tags-are-not-import-identity"}}},{"op":"add","path":"/metadata/tests/21","value":{"command":"node --test --test-concurrency=1 test/daily-sync-privacy.test.ts","scope":"project","timeout_seconds":1200,"provenance":{"author":"codex-github230-1111","created_at":"2026-11-11T14:46:32.891Z","source_kind":"local_mutation","source_ref":"fix/reference-tags-are-not-import-identity"}}},{"op":"add","path":"/metadata/tests/22","value":{"command":"node --test --test-concurrency=1 test/discussion-model.test.ts","scope":"project","timeout_seconds":1200,"provenance":{"author":"codex-github230-1111","created_at":"2026-11-11T14:46:32.891Z","source_kind":"local_mutation","source_ref":"fix/reference-tags-are-not-import-identity"}}},{"op":"add","path":"/metadata/tests/23","value":{"command":"node --test --test-concurrency=1 test/discussions.test.ts","scope":"project","timeout_seconds":1200,"provenance":{"author":"codex-github230-1111","created_at":"2026-11-11T14:46:32.891Z","source_kind":"local_mutation","source_ref":"fix/reference-tags-are-not-import-identity"}}},{"op":"add","path":"/metadata/tests/24","value":{"command":"node --test --test-concurrency=1 test/dryrun-preview.test.ts","scope":"project","timeout_seconds":1200,"provenance":{"author":"codex-github230-1111","created_at":"2026-11-11T14:46:32.891Z","source_kind":"local_mutation","source_ref":"fix/reference-tags-are-not-import-identity"}}},{"op":"add","path":"/metadata/tests/25","value":{"command":"node --test --test-concurrency=1 test/gate.test.ts","scope":"project","timeout_seconds":1200,"provenance":{"author":"codex-github230-1111","created_at":"2026-11-11T14:46:32.891Z","source_kind":"local_mutation","source_ref":"fix/reference-tags-are-not-import-identity"}}},{"op":"add","path":"/metadata/tests/26","value":{"command":"node --test --test-concurrency=1 test/handler-failures.test.ts","scope":"project","timeout_seconds":1200,"provenance":{"author":"codex-github230-1111","created_at":"2026-11-11T14:46:32.891Z","source_kind":"local_mutation","source_ref":"fix/reference-tags-are-not-import-identity"}}},{"op":"add","path":"/metadata/tests/27","value":{"command":"node --test --test-concurrency=1 test/http-boundary.test.ts","scope":"project","timeout_seconds":1200,"provenance":{"author":"codex-github230-1111","created_at":"2026-11-11T14:46:32.891Z","source_kind":"local_mutation","source_ref":"fix/reference-tags-are-not-import-identity"}}},{"op":"add","path":"/metadata/tests/28","value":{"command":"node --test --test-concurrency=1 test/identity.test.ts","scope":"project","timeout_seconds":1200,"provenance":{"author":"codex-github230-1111","created_at":"2026-11-11T14:46:32.891Z","source_kind":"local_mutation","source_ref":"fix/reference-tags-are-not-import-identity"}}},{"op":"add","path":"/metadata/tests/29","value":{"command":"node --test --test-concurrency=1 test/import-conflict-report.test.ts","scope":"project","timeout_seconds":1200,"provenance":{"author":"codex-github230-1111","created_at":"2026-11-11T14:46:32.891Z","source_kind":"local_mutation","source_ref":"fix/reference-tags-are-not-import-identity"}}},{"op":"add","path":"/metadata/tests/30","value":{"command":"node --test --test-concurrency=1 test/import-errors.test.ts","scope":"project","timeout_seconds":1200,"provenance":{"author":"codex-github230-1111","created_at":"2026-11-11T14:46:32.891Z","source_kind":"local_mutation","source_ref":"fix/reference-tags-are-not-import-identity"}}},{"op":"add","path":"/metadata/tests/31","value":{"command":"node --test --test-concurrency=1 test/import-fidelity.test.ts","scope":"project","timeout_seconds":1200,"provenance":{"author":"codex-github230-1111","created_at":"2026-11-11T14:46:32.891Z","source_kind":"local_mutation","source_ref":"fix/reference-tags-are-not-import-identity"}}},{"op":"add","path":"/metadata/tests/32","value":{"command":"node --test --test-concurrency=1 test/import-lock.test.ts","scope":"project","timeout_seconds":1200,"provenance":{"author":"codex-github230-1111","created_at":"2026-11-11T14:46:32.891Z","source_kind":"local_mutation","source_ref":"fix/reference-tags-are-not-import-identity"}}},{"op":"add","path":"/metadata/tests/33","value":{"command":"node --test --test-concurrency=1 test/import-projection.test.ts","scope":"project","timeout_seconds":1200,"provenance":{"author":"codex-github230-1111","created_at":"2026-11-11T14:46:32.891Z","source_kind":"local_mutation","source_ref":"fix/reference-tags-are-not-import-identity"}}},{"op":"add","path":"/metadata/tests/34","value":{"command":"node --test --test-concurrency=1 test/import-regex-sampling.test.ts","scope":"project","timeout_seconds":1200,"provenance":{"author":"codex-github230-1111","created_at":"2026-11-11T14:46:32.891Z","source_kind":"local_mutation","source_ref":"fix/reference-tags-are-not-import-identity"}}},{"op":"add","path":"/metadata/tests/35","value":{"command":"node --test --test-concurrency=1 test/import-regex.test.ts","scope":"project","timeout_seconds":1200,"provenance":{"author":"codex-github230-1111","created_at":"2026-11-11T14:46:32.891Z","source_kind":"local_mutation","source_ref":"fix/reference-tags-are-not-import-identity"}}},{"op":"add","path":"/metadata/tests/36","value":{"command":"node --test --test-concurrency=1 test/import-selection.test.ts","scope":"project","timeout_seconds":1200,"provenance":{"author":"codex-github230-1111","created_at":"2026-11-11T14:46:32.891Z","source_kind":"local_mutation","source_ref":"fix/reference-tags-are-not-import-identity"}}},{"op":"add","path":"/metadata/tests/37","value":{"command":"node --test --test-concurrency=1 test/issue-forms.test.ts","scope":"project","timeout_seconds":1200,"provenance":{"author":"codex-github230-1111","created_at":"2026-11-11T14:46:32.891Z","source_kind":"local_mutation","source_ref":"fix/reference-tags-are-not-import-identity"}}},{"op":"add","path":"/metadata/tests/38","value":{"command":"node --test --test-concurrency=1 test/label-import-model.test.ts","scope":"project","timeout_seconds":1200,"provenance":{"author":"codex-github230-1111","created_at":"2026-11-11T14:46:32.891Z","source_kind":"local_mutation","source_ref":"fix/reference-tags-are-not-import-identity"}}},{"op":"add","path":"/metadata/tests/39","value":{"command":"node --test --test-concurrency=1 test/labels.test.ts","scope":"project","timeout_seconds":1200,"provenance":{"author":"codex-github230-1111","created_at":"2026-11-11T14:46:32.891Z","source_kind":"local_mutation","source_ref":"fix/reference-tags-are-not-import-identity"}}},{"op":"add","path":"/metadata/tests/40","value":{"command":"node --test --test-concurrency=1 test/link-deps.test.ts","scope":"project","timeout_seconds":1200,"provenance":{"author":"codex-github230-1111","created_at":"2026-11-11T14:46:32.891Z","source_kind":"local_mutation","source_ref":"fix/reference-tags-are-not-import-identity"}}},{"op":"add","path":"/metadata/tests/41","value":{"command":"node --test --test-concurrency=1 test/own-property.test.ts","scope":"project","timeout_seconds":1200,"provenance":{"author":"codex-github230-1111","created_at":"2026-11-11T14:46:32.891Z","source_kind":"local_mutation","source_ref":"fix/reference-tags-are-not-import-identity"}}},{"op":"add","path":"/metadata/tests/42","value":{"command":"node --test --test-concurrency=1 test/project-sync.test.ts","scope":"project","timeout_seconds":1200,"provenance":{"author":"codex-github230-1111","created_at":"2026-11-11T14:46:32.891Z","source_kind":"local_mutation","source_ref":"fix/reference-tags-are-not-import-identity"}}},{"op":"add","path":"/metadata/tests/43","value":{"command":"node --test --test-concurrency=1 test/reference-import-identity.test.ts","scope":"project","timeout_seconds":1200,"provenance":{"author":"codex-github230-1111","created_at":"2026-11-11T14:46:32.891Z","source_kind":"local_mutation","source_ref":"fix/reference-tags-are-not-import-identity"}}},{"op":"add","path":"/metadata/tests/44","value":{"command":"node --test --test-concurrency=1 test/refresh.test.ts","scope":"project","timeout_seconds":1200,"provenance":{"author":"codex-github230-1111","created_at":"2026-11-11T14:46:32.891Z","source_kind":"local_mutation","source_ref":"fix/reference-tags-are-not-import-identity"}}},{"op":"add","path":"/metadata/tests/45","value":{"command":"node --test --test-concurrency=1 test/repeat-import-noop.test.ts","scope":"project","timeout_seconds":1200,"provenance":{"author":"codex-github230-1111","created_at":"2026-11-11T14:46:32.891Z","source_kind":"local_mutation","source_ref":"fix/reference-tags-are-not-import-identity"}}},{"op":"add","path":"/metadata/tests/46","value":{"command":"node --test --test-concurrency=1 test/scheduled-refusals.test.ts","scope":"project","timeout_seconds":1200,"provenance":{"author":"codex-github230-1111","created_at":"2026-11-11T14:46:32.891Z","source_kind":"local_mutation","source_ref":"fix/reference-tags-are-not-import-identity"}}},{"op":"add","path":"/metadata/tests/47","value":{"command":"node --test --test-concurrency=1 test/sdk-adoption.test.ts","scope":"project","timeout_seconds":1200,"provenance":{"author":"codex-github230-1111","created_at":"2026-11-11T14:46:32.891Z","source_kind":"local_mutation","source_ref":"fix/reference-tags-are-not-import-identity"}}},{"op":"add","path":"/metadata/tests/48","value":{"command":"node --test --test-concurrency=1 test/smoke.test.ts","scope":"project","timeout_seconds":1200,"provenance":{"author":"codex-github230-1111","created_at":"2026-11-11T14:46:32.891Z","source_kind":"local_mutation","source_ref":"fix/reference-tags-are-not-import-identity"}}},{"op":"add","path":"/metadata/tests/49","value":{"command":"node --test --test-concurrency=1 test/typed-sentinel-normalization.test.ts","scope":"project","timeout_seconds":1200,"provenance":{"author":"codex-github230-1111","created_at":"2026-11-11T14:46:32.891Z","source_kind":"local_mutation","source_ref":"fix/reference-tags-are-not-import-identity"}}},{"op":"add","path":"/metadata/tests/50","value":{"command":"node --test --test-concurrency=1 test/webhooks.test.ts","scope":"project","timeout_seconds":1200,"provenance":{"author":"codex-github230-1111","created_at":"2026-11-11T14:46:32.891Z","source_kind":"local_mutation","source_ref":"fix/reference-tags-are-not-import-identity"}}},{"op":"add","path":"/metadata/tests/51","value":{"command":"node --test --test-concurrency=1 test/workspace-resolution.test.ts","scope":"project","timeout_seconds":1200,"provenance":{"author":"codex-github230-1111","created_at":"2026-11-11T14:46:32.891Z","source_kind":"local_mutation","source_ref":"fix/reference-tags-are-not-import-identity"}}},{"op":"add","path":"/metadata/tests/52","value":{"command":"node --test --test-concurrency=1 --test-name-pattern '^[a-fA-F]' test/import-gate.test.ts","scope":"project","timeout_seconds":1200,"provenance":{"author":"codex-github230-1111","created_at":"2026-11-11T14:46:32.891Z","source_kind":"local_mutation","source_ref":"fix/reference-tags-are-not-import-identity"}}},{"op":"add","path":"/metadata/tests/53","value":{"command":"node --test --test-concurrency=1 --test-name-pattern '^[g-mG-M]' test/import-gate.test.ts","scope":"project","timeout_seconds":1200,"provenance":{"author":"codex-github230-1111","created_at":"2026-11-11T14:46:32.891Z","source_kind":"local_mutation","source_ref":"fix/reference-tags-are-not-import-identity"}}},{"op":"add","path":"/metadata/tests/54","value":{"command":"node --test --test-concurrency=1 --test-name-pattern '^[n-sN-S]' test/import-gate.test.ts","scope":"project","timeout_seconds":1200,"provenance":{"author":"codex-github230-1111","created_at":"2026-11-11T14:46:32.891Z","source_kind":"local_mutation","source_ref":"fix/reference-tags-are-not-import-identity"}}},{"op":"add","path":"/metadata/tests/55","value":{"command":"node --test --test-concurrency=1 --test-name-pattern '^[t-zT-Z]' test/import-gate.test.ts","scope":"project","timeout_seconds":1200,"provenance":{"author":"codex-github230-1111","created_at":"2026-11-11T14:46:32.891Z","source_kind":"local_mutation","source_ref":"fix/reference-tags-are-not-import-identity"}}},{"op":"add","path":"/metadata/tests/56","value":{"command":"node --test --test-concurrency=1 --test-name-pattern '^[^a-zA-Z]' test/import-gate.test.ts","scope":"project","timeout_seconds":1200,"provenance":{"author":"codex-github230-1111","created_at":"2026-11-11T14:46:32.891Z","source_kind":"local_mutation","source_ref":"fix/reference-tags-are-not-import-identity"}}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-11T14:46:32.925Z"}],"before_hash":"b9b50e191480098f2dec6593e141227d0513d4c3da007ff16f5e471fc6796333","after_hash":"c5123aa4b96e1536c8ef75f7c0a0d28f4b7328235cd1fd599f8803e21e052227","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":"7af2aa8fac9d0e2f334d38c2d928419c7ba183bba7d33a8b92a03112131f33ea"} +{"hash_algorithm":"sha256","ts":"2026-11-11T15:02:10.328Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"tests_remove","patch":[{"op":"remove","path":"/metadata/tests/56"},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-11T15:02:10.328Z"}],"before_hash":"c5123aa4b96e1536c8ef75f7c0a0d28f4b7328235cd1fd599f8803e21e052227","after_hash":"1ba5b2fe80c516f73b3c921905578bb9bb0c39269d5405a76fe86927e6412ff1","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":"4e6dee34af1324c55b8432139d8bf270798a192ab3176c4277106bdafc4e3a6b"} +{"hash_algorithm":"sha256","ts":"2026-11-11T15:09:38.146Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-11T15:09:38.145Z","author":"codex-github230-1111","text":"Final review regression run through pm test --run --progress passed 29/29 cases, including generated preview/apply agreement. Import-gate completed 73/73 cases across four alphabetic name partitions; all 56 declaration prefixes were checked for coverage by those partitions. A redundant non-letter pattern matched the Node file wrapper, was stopped, removed from linked tests and excluded from counts. The closeout workflow matrix reached its 20-minute cap after 143 passing cases with no assertion failures. Real compensation testing follows the public SDK contract: delete compensation retains history and reserves the original ID. Recovery now keeps the plan and returns github_import_plan_drift instead of applying changed input; explicit SDK restore of the original birth version then permits the exact saved plan to finish. The atomic file passed 11/11 including this refusal-and-restore case. Read-only identity exploration still exposes contradictory stored claims, while write planners refuse them."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-11T15:09:38.146Z"}],"before_hash":"1ba5b2fe80c516f73b3c921905578bb9bb0c39269d5405a76fe86927e6412ff1","after_hash":"370a5322ea8d85ba781e000c5cd78e577370d8fe05ffb98b5ef1646295a7d0d8","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":"9a5455ff524901619f408106840468f57940d0191c9e59b2f320cd603d940ee3"} +{"hash_algorithm":"sha256","ts":"2026-11-11T15:10:46.699Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-11T15:10:46.699Z","author":"codex-github230-1111","text":"Behavioral revert proof, executed only in a disposable package copy with real temp SDK trackers: restoring round-one prepareGithubImport/runImport causes 4 of 6 curated side-data cases to fail (2 atomic annotation compatibility controls pass); restoring importIdentityKeys/matchImportedIssue causes all 7 explicit identity refusal/normalization cases to fail; restoring runImport and disabling persisted-plan dispatch in importGithubAtomic causes all 12 crash/dependency/missing-plan cases to fail; restoring planSync/buildExportPlan causes all 3 upstream planning, wire-write and generated preview/apply cases to fail. The same atomic replay revert makes the real incomplete-compensation/explicit-restore test fail (1/1). Every mutation was restored in a finally block; the fixed package copy is running the full review regression file again. Review thread mapping: PRRT_kwDOTbN4pM62J6R5 and PRRT_kwDOTbN4pM62KLhP use curated side-data tests; PRRT_kwDOTbN4pM62KLhK uses explicit identity cases; PRRT_kwDOTbN4pM62KLhO uses original-plan replay plus compensation. Latest CodeRabbit outside-diff upstream comment uses the three upstream cases. No review thread was replied to, resolved or reacted to."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-11T15:10:46.699Z"}],"before_hash":"370a5322ea8d85ba781e000c5cd78e577370d8fe05ffb98b5ef1646295a7d0d8","after_hash":"eb79c4e95e83e0693f338c6f35a58764be29e411e0611a16c748460b42e8baeb","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":"5aef7cb5caaa072833e5c56e57b8e24ee8e99615342f3f3be9e859a4f2b06fb8"} +{"hash_algorithm":"sha256","ts":"2026-11-11T15:17:36.077Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-11T15:17:36.076Z","author":"codex-github230-1111","text":"Final round-2 evidence: fixed disposable source copy matches the package code byte-for-byte and passes 30/30 review regressions after restoration. An additional mutation-payload drift test fails 1/1 when only commitGithubImportTransaction is restored to round one; fixed code returns github_import_plan_drift before native writes and succeeds once the original saved plan is restored. Final unique completed gates cover 48 files (51 bounded invocations): 1121 tests, 1119 pass, 0 fail, 2 existing pm-cli#1656 quarantines. Import-gate is complete at 73/73; closeout-workflow is the sole capped file, 143 passes then 1 cancellation at 20 minutes. Fresh atomic gate is 11/11, refresh 34 pass plus 1 existing quarantine, import fidelity 53 pass plus 1 existing quarantine; packed npm/native Bun import projection is 6/6. Lint, typecheck, builds, docstrings and strict health with isolated global discovery pass. Full npm/Bun release and exact 100-percent coverage remain for CI under the requested gate budget; prior head CI reported 99.94-percent branch coverage, which is not certification for this change. Counts exclude the stopped redundant regex run and superseded failures fixed in compatibility fixtures. CI handoff: https://github.com/unbraind/pm-github/pull/233/checks . Original Projects follow-up remains open because task rules require out-of-scope tracking and append-only audit records. No Projects code changed in round two. The main item stays in_progress; releasing only this agent claim before commit and push."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-11T15:17:36.077Z"}],"before_hash":"eb79c4e95e83e0693f338c6f35a58764be29e411e0611a16c748460b42e8baeb","after_hash":"0068cb324a8261d61c6ba73e9ee3bf6ab3a59d306f7d4ffacc892a8a31ff1d8b","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":"38c58648125e6c62fa08af6553de9905053abe86f983c7fbfe42a7b8dd108c2d"} +{"hash_algorithm":"sha256","ts":"2026-11-11T15:17:37.812Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-11T15:17:37.812Z"}],"before_hash":"0068cb324a8261d61c6ba73e9ee3bf6ab3a59d306f7d4ffacc892a8a31ff1d8b","after_hash":"ed319633793419d12b1ba93b9d369ca980f29786397f43fe1351ade7ecbddfaf","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":"8fb1b6dd4d298e1af4ed3cb949cb9e020890ed8aa98f1b8643054d4cc44a7bdf"} +{"hash_algorithm":"sha256","ts":"2026-11-11T16:07:59.293Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-11T16:07:59.293Z"},{"op":"add","path":"/metadata/assignee","value":"codex-github230-1111"},{"op":"add","path":"/metadata/claim_principal","value":"codex-github230-1111"}],"before_hash":"ed319633793419d12b1ba93b9d369ca980f29786397f43fe1351ade7ecbddfaf","after_hash":"6171dacd00965af65b4965d9520d4b01b99ca4bb5d97f2b2e01aea92a28a78a2","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":"f701254885e8948604578a5270583b400a25dcf101f273ffb943b92dfa0e3540"} +{"hash_algorithm":"sha256","ts":"2026-11-11T16:08:01.289Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-11T16:08:01.288Z","author":"codex-github230-1111","text":"Round 3 starts from clean head 080f1e6; round-2 work is accepted. Read the three orchestrator findings in full. Plan: replace package-wide GitHub issue URL parsing with one URL/path-segment helper; persist and enforce privacy preflight verdicts before recovery writes; and allow legacy number-only promotion only for one implied repository, reporting ambiguous legacy references as typed summary conflicts. Preserve accepted side-data and original-plan recovery behavior, add real SDK tracker regressions and disposable behavioral reverts, run bounded affected-file gates, then release the claim and push."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-11T16:08:01.289Z"}],"before_hash":"6171dacd00965af65b4965d9520d4b01b99ca4bb5d97f2b2e01aea92a28a78a2","after_hash":"4b7f0b62ebd68870a612c1936a4974b05909ee3214e21c7c3592270aed6766d0","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":"e9887429b198b0b5ee320b77250ed10ddfd07fbe2abd894d2394d03ec72a0c58"} +{"hash_algorithm":"sha256","ts":"2026-11-11T16:22:16.097Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-11T16:22:16.093Z","author":"codex-github230-1111","text":"Read the current GitHub review snapshot: 7 threads, 7 complete comments/diffs with no further comment pages, and 4 review bodies. Three newer threads include the rejected-recovery finding, retained committed refresh work (PRRT_kwDOTbN4pM62PHLJ), and CodeQL polynomial URL-related parsing (PRRT_kwDOTbN4pM62PrYX). The shared URL parser removes path replacement and whole-path issue/PR regex parsing; repository suffix normalization now uses a linear scan. Recovery now saves privacy verdicts and requires both a saved pass and a fresh pass before replay, including refresh. Retained committed plans are discovered before refresh polls new data, closing the related side-data replay class. Initial round-3 focused suite passed 26/26 real-SDK cases, including reserved repository/owner names, decorated URLs, legacy ambiguity, and failed/missing/forged privacy preflights with unchanged documents and histories."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-11T16:22:16.097Z"}],"before_hash":"4b7f0b62ebd68870a612c1936a4974b05909ee3214e21c7c3592270aed6766d0","after_hash":"8a559f40d934fd480781be75bbbffc7569d3d3fb81d4816c58a59822447dd946","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":"ce4773857af509cc820825c691e21aa3906064bf19c2ad8937a2a3417e5484b1"} +{"hash_algorithm":"sha256","ts":"2026-11-11T16:30:51.245Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/31/path","value":"test/review-round3-identity.test.ts"},{"op":"replace","path":"/metadata/files/30/path","value":"test/review-import-identity.test.ts"},{"op":"replace","path":"/metadata/files/29/path","value":"test/repeat-import-noop.test.ts"},{"op":"replace","path":"/metadata/files/28/path","value":"test/refresh.test.ts"},{"op":"replace","path":"/metadata/files/27/path","value":"test/reference-import-identity.test.ts"},{"op":"replace","path":"/metadata/files/26/path","value":"test/link-deps.test.ts"},{"op":"replace","path":"/metadata/files/25/path","value":"test/label-import-model.test.ts"},{"op":"replace","path":"/metadata/files/24/path","value":"test/import-projection.test.ts"},{"op":"replace","path":"/metadata/files/23/path","value":"test/import-gate.test.ts"},{"op":"replace","path":"/metadata/files/22/path","value":"test/import-fidelity.test.ts"},{"op":"replace","path":"/metadata/files/21/path","value":"test/import-errors.test.ts"},{"op":"replace","path":"/metadata/files/20/path","value":"test/import-conflict-report.test.ts"},{"op":"replace","path":"/metadata/files/19/path","value":"test/identity.test.ts"},{"op":"replace","path":"/metadata/files/18/path","value":"test/helpers/import-mirror.ts"},{"op":"replace","path":"/metadata/files/17/path","value":"test/handler-failures.test.ts"},{"op":"replace","path":"/metadata/files/16/path","value":"test/fixtures/refresh-crash.ts"},{"op":"replace","path":"/metadata/files/15/path","value":"test/dryrun-preview.test.ts"},{"op":"replace","path":"/metadata/files/14/path","value":"test/daily-sync-idempotency.test.ts"},{"op":"replace","path":"/metadata/files/13/path","value":"test/coverage-remainder.test.ts"},{"op":"replace","path":"/metadata/files/12/path","value":"test/coverage-edges.test.ts"},{"op":"replace","path":"/metadata/files/11/path","value":"test/cited-issue-adoption.test.ts"},{"op":"replace","path":"/metadata/files/10/path","value":"test/atomic.test.ts"},{"op":"replace","path":"/metadata/files/9/path","value":"refresh.ts"},{"op":"replace","path":"/metadata/files/8/path","value":"projects.ts"},{"op":"replace","path":"/metadata/files/7/path","value":"index.ts"},{"op":"replace","path":"/metadata/files/6/path","value":"import-tags.ts"},{"op":"replace","path":"/metadata/files/5/path","value":"import-fidelity.ts"},{"op":"replace","path":"/metadata/files/4/path","value":"identity.ts"},{"op":"replace","path":"/metadata/files/3/path","value":"github-url.ts"},{"op":"replace","path":"/metadata/files/2/path","value":"discussions.ts"},{"op":"replace","path":"/metadata/files/1/path","value":"closeout.ts"},{"op":"add","path":"/metadata/files/32","value":{"path":"test/scheduled-refusals.test.ts","scope":"project"}},{"op":"add","path":"/metadata/files/33","value":{"path":"test/smoke.test.ts","scope":"project"}},{"op":"add","path":"/metadata/files/34","value":{"path":"test/typed-sentinel-normalization.test.ts","scope":"project"}},{"op":"add","path":"/metadata/files/35","value":{"path":"test/webhooks.test.ts","scope":"project"}},{"op":"add","path":"/metadata/files/36","value":{"path":"test/workspace-resolution.test.ts","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-11T16:30:51.245Z"}],"before_hash":"8a559f40d934fd480781be75bbbffc7569d3d3fb81d4816c58a59822447dd946","after_hash":"31f752a05e2be2f4b683bbb20c9a0ee7f492e6b6ac138feb26181610df4ad69c","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":"0f7dc35d488fe4483d0a63ffeac83c4c40c4c666eedc9f093c99a2057fa67994"} +{"hash_algorithm":"sha256","ts":"2026-11-11T16:30:52.395Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/56","value":{"command":"node --test --test-concurrency=1 test/review-round3-identity.test.ts","scope":"project","timeout_seconds":1200,"provenance":{"author":"codex-github230-1111","created_at":"2026-11-11T16:30:52.324Z","source_kind":"local_mutation","source_ref":"fix/reference-tags-are-not-import-identity"}}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-11T16:30:52.395Z"}],"before_hash":"31f752a05e2be2f4b683bbb20c9a0ee7f492e6b6ac138feb26181610df4ad69c","after_hash":"f0e9f359c5a9632bea38a136029ef58ec94b5b748440924c56c1661df1e7e4e9","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":"336d4cb0bf1d2e56ee5eb190496ef4b4160821e368eb540f3925669e80e1a717"} +{"hash_algorithm":"sha256","ts":"2026-11-11T16:38:38.650Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/57","value":{"command":"node --test --test-concurrency=1 test/projects.test.ts","scope":"project","timeout_seconds":1200,"provenance":{"author":"codex-github230-1111","created_at":"2026-11-11T16:38:38.609Z","source_kind":"local_mutation","source_ref":"fix/reference-tags-are-not-import-identity"}}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-11T16:38:38.650Z"}],"before_hash":"f0e9f359c5a9632bea38a136029ef58ec94b5b748440924c56c1661df1e7e4e9","after_hash":"d5cec4aea4aa87f1ad250cf04f28a912a684f3d5e9efba13f2e33e9b7a8ac5b7","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":"0548aafc8e1cfe6662559339f13acd91c42c9bde37dc58fdbc3a7942c12d4c0f"} +{"hash_algorithm":"sha256","ts":"2026-11-11T16:55:04.657Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-11T16:55:04.657Z","author":"codex-github230-1111","text":"Round 3: all GitHub issue/PR identity paths now share URL and fixed-segment parsing. Ambiguous number-only legacy provenance is a mention with typed ambiguous_legacy_github_identity summary evidence; a single repository or explicit repository still resolves. Every replay requires a saved passing preflight plus a fresh scan before mutation. Also fixed retained committed refresh side-data discovery (Greptile PRRT_kwDOTbN4pM62PHLJ) and replaced the slash-suffix regex flagged by CodeQL alert 12 with linear trimming. Lint, typecheck, docstrings (30 files, 276 declarations), and duplication (zero authored duplicated lines) pass. Broad dependent-file gates remain running with two workers and 20-minute invocation caps; closeout-workflow reached its cap and needs CI certification."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-11T16:55:04.657Z"}],"before_hash":"d5cec4aea4aa87f1ad250cf04f28a912a684f3d5e9efba13f2e33e9b7a8ac5b7","after_hash":"b69f1b2f29af09ec5108b46dc07b85980a8e1c49c27e365c63184002efb4e2c5","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":"5f8beedf57a7ad65de76387d21422347c61f68ec208464f20aad42ef3a1b830c"} +{"hash_algorithm":"sha256","ts":"2026-11-11T17:04:52.431Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-11T17:04:52.430Z","author":"codex-github230-1111","text":"Round 3 read receipt: all 3 supplied blocking findings, 7 review threads with 7 complete comments/proposed diffs, and 4 complete review bodies were read (no remaining pages), plus the full CodeQL alert 12 payload. Accepted round-2 fixes remain intact. The shared URL parser is used by import identity, refresh, sync/export ownership, diagnostics, blockers, closeout, discussions and Projects URL inputs; transport pagination/origin checks remain transport-only. Repository normalization preserves released opaque keys while removing the quadratic suffix regex. Strict pm health passes with two advisory warnings. Full npm/Bun release and 100% coverage certification remain CI work under the explicit gate budget."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-11T17:04:52.431Z"}],"before_hash":"b69f1b2f29af09ec5108b46dc07b85980a8e1c49c27e365c63184002efb4e2c5","after_hash":"c8c6d7baca6506d43eb74608d92297f41b18dcba870823b3dbddd1273fe819ea","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":"8c9a253a4723ab7290dc28a2dab1d94679177652adbd98454bfcce8083d1ef7f"} +{"hash_algorithm":"sha256","ts":"2026-11-11T17:07:56.466Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/58","value":{"command":"node --test --test-concurrency=1 test/atomic-finalization-quarantine.test.ts","scope":"project","timeout_seconds":1200,"provenance":{"author":"codex-github230-1111","created_at":"2026-11-11T17:07:56.203Z","source_kind":"local_mutation","source_ref":"fix/reference-tags-are-not-import-identity"}}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-11T17:07:56.466Z"}],"before_hash":"c8c6d7baca6506d43eb74608d92297f41b18dcba870823b3dbddd1273fe819ea","after_hash":"cfc1dee60577bd7108c4a2e4a87d6c425f16c52f3ab9c0330114212d2221e14d","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":"6edecac1724e70d56692d59b6f1a427b09fdc5d8b8ddd422e67ce1d4e17a2143"} +{"hash_algorithm":"sha256","ts":"2026-11-11T17:16:29.057Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-11T17:16:29.057Z","author":"codex-github230-1111","text":"Round 3 behavioral revert proof (real SDK trackers in a disposable package copy): restoring only explicitGithubKey to 080f1e6 produces 6 failures and 12 compatibility passes in the URL segments cases; bypassing assertGithubImportReplayPreflight produces 3 privacy/no-write failures; restoring only importIdentityKeys and matchImportedIssue produces 1 ambiguous-legacy failure with 2 compatibility controls passing. Restoring runRefresh produces 2 retained-annotation recovery failures. Replacing normalizeGithubRepository with the released suffix regex produces 1 bounded adversarial-input failure; reverting only the workflow conflict projection produces 1 formatter failure. Commands: node --test --test-concurrency=1 --test-name-pattern test/review-round3-identity.test.ts (prefixes: URL segments, recovery preflight, number-only legacy, refresh recovers, repository normalization), plus the sync PR includes pattern in test/import-conflict-report.test.ts. The fixed implementation was restored in finally; both files pass 30/30, and 11 implementation/test/workflow files match the package source byte-for-byte. All four importer-gate partitions pass: 16+6+33+18 = 73 cases."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-11T17:16:29.057Z"}],"before_hash":"cfc1dee60577bd7108c4a2e4a87d6c425f16c52f3ab9c0330114212d2221e14d","after_hash":"2ada4f50c979283e46612f246560f994d8db9828f07c217b0a278ecef6dc7026","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":"088cf8900cbf9406dd66239b3b55be443069af454a27b754cd5dfeaad5ec8d1a"} +{"hash_algorithm":"sha256","ts":"2026-11-11T17:19:33.716Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-11T17:19:33.716Z","author":"codex-github230-1111","text":"Round 3 final local gates: 51 completed affected/required files across 54 bounded invocations, 1189 passing tests, zero failures, two existing pm-cli#1656 skips. This includes all 73 importer-gate cases, refresh 34 passes plus one skip, identity 20, cited adoption 46, atomic 11, quarantine guard 2, accepted review regressions 30 and new round-3 regressions 29. Initial compatibility failures were fixed and their complete files rerun successfully. The linked pm test run passes 29/29; restored disposable proof files pass 30/30 separately. closeout-workflow reached its 20-minute cap after 62 passes and one canceled case, without assertion failures; CI must finish it. Build, lint, typecheck, docstrings, duplication, strict tracker health and privacy audit pass. Full npm/Bun release checks and exact 100% coverage remain uncertified locally under the explicit budget. No outstanding implementation blocker or new follow-up was found; the existing Projects v2 follow-up remains open. Releasing only this author claim and leaving the issue in progress."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-11T17:19:33.716Z"}],"before_hash":"2ada4f50c979283e46612f246560f994d8db9828f07c217b0a278ecef6dc7026","after_hash":"1d0838a8815d71617e57d919a0a9106865957dc6f1e47e636ca83a1719d38261","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":"cc59920e02bad2d276fe7353a50dd4854b894eec4730230a04757bf20a98610c"} +{"hash_algorithm":"sha256","ts":"2026-11-11T17:19:34.616Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-11T17:19:34.616Z"}],"before_hash":"1d0838a8815d71617e57d919a0a9106865957dc6f1e47e636ca83a1719d38261","after_hash":"4a1b8dd91d1766e1eccb9bb855b4fd01f020f49ba4dd13216c94100c328bf4f0","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":"56cc93ec153f47771a8a7d14f80ecd3b4e01b052e5869f7967fed90df6335334"} +{"hash_algorithm":"sha256","ts":"2026-11-11T18:07:25.570Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-11T18:07:25.570Z"},{"op":"add","path":"/metadata/assignee","value":"codex-github230-1111"},{"op":"add","path":"/metadata/claim_principal","value":"codex-github230-1111"}],"before_hash":"4a1b8dd91d1766e1eccb9bb855b4fd01f020f49ba4dd13216c94100c328bf4f0","after_hash":"e8f4358392bfe058af4214fd9371253dc7ad7f06f677f75d83d72f0c64b61597","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":"14cd4904a90344c2e2a5e1b84d9e09226ae003675c50679b6e568693d0d9b151"} +{"hash_algorithm":"sha256","ts":"2026-11-11T18:13:07.803Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-11T18:13:07.803Z","author":"codex-github230-1111","text":"Round 4 plan: SDK declaration audit found public writeFileAtomic(targetPath, contents, {createParentDirectories?}) in sdk/index.d.ts and core/fs/fs-utils.d.ts. Use that helper for same-directory staging, fsync the completed candidate, publish by rename (replacement) or atomic no-replace link (plans and leases), then fsync directories. Audit found recovery plans, comment-sync leases, breaker-election markers, and webhook delivery/cursor/completion state. SDK-owned native history and transaction journals remain SDK-managed; scripts only emit derived output or disposable test mutations. Corrupt/unreadable plan contents will receive github_import_plan_corrupt with explicit restore/journal reconciliation steps; only ENOENT means missing. Add real SDK tracker regressions for native partial-write faults, crash before publication, concurrent readers, and clean zero-item retries. Round-3 implementation is accepted and retained."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-11T18:13:07.803Z"}],"before_hash":"e8f4358392bfe058af4214fd9371253dc7ad7f06f677f75d83d72f0c64b61597","after_hash":"120f44f4404bb91dfc21dfc04798c58082f0bbecfb88dd7650884cc554b89a40","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":"c8d736ac0be7b735473308595feb79f4f75bff76780bc85b2bc243cdac65d1cb"} +{"hash_algorithm":"sha256","ts":"2026-11-11T18:15:04.046Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/36/path","value":"test/scheduled-refusals.test.ts"},{"op":"replace","path":"/metadata/files/35/path","value":"test/review-round3-identity.test.ts"},{"op":"replace","path":"/metadata/files/34/path","value":"test/review-import-identity.test.ts"},{"op":"replace","path":"/metadata/files/33/path","value":"test/repeat-import-noop.test.ts"},{"op":"replace","path":"/metadata/files/32/path","value":"test/refresh.test.ts"},{"op":"replace","path":"/metadata/files/31/path","value":"test/reference-import-identity.test.ts"},{"op":"replace","path":"/metadata/files/30/path","value":"test/link-deps.test.ts"},{"op":"replace","path":"/metadata/files/29/path","value":"test/label-import-model.test.ts"},{"op":"replace","path":"/metadata/files/28/path","value":"test/import-projection.test.ts"},{"op":"replace","path":"/metadata/files/27/path","value":"test/import-lock.test.ts"},{"op":"replace","path":"/metadata/files/26/path","value":"test/import-gate.test.ts"},{"op":"replace","path":"/metadata/files/25/path","value":"test/import-fidelity.test.ts"},{"op":"replace","path":"/metadata/files/24/path","value":"test/import-errors.test.ts"},{"op":"replace","path":"/metadata/files/23/path","value":"test/import-conflict-report.test.ts"},{"op":"replace","path":"/metadata/files/22/path","value":"test/identity.test.ts"},{"op":"replace","path":"/metadata/files/21/path","value":"test/helpers/import-mirror.ts"},{"op":"replace","path":"/metadata/files/20/path","value":"test/handler-failures.test.ts"},{"op":"replace","path":"/metadata/files/19/path","value":"test/fixtures/refresh-crash.ts"},{"op":"replace","path":"/metadata/files/18/path","value":"test/fixtures/atomic-plan-crash.ts"},{"op":"replace","path":"/metadata/files/17/path","value":"test/durable-recovery.test.ts"},{"op":"replace","path":"/metadata/files/16/path","value":"test/dryrun-preview.test.ts"},{"op":"replace","path":"/metadata/files/15/path","value":"test/daily-sync-idempotency.test.ts"},{"op":"replace","path":"/metadata/files/14/path","value":"test/coverage-remainder.test.ts"},{"op":"replace","path":"/metadata/files/13/path","value":"test/coverage-edges.test.ts"},{"op":"replace","path":"/metadata/files/12/path","value":"test/cited-issue-adoption.test.ts"},{"op":"replace","path":"/metadata/files/11/path","value":"test/atomic.test.ts"},{"op":"replace","path":"/metadata/files/10/path","value":"refresh.ts"},{"op":"replace","path":"/metadata/files/9/path","value":"projects.ts"},{"op":"replace","path":"/metadata/files/8/path","value":"index.ts"},{"op":"replace","path":"/metadata/files/7/path","value":"import-tags.ts"},{"op":"replace","path":"/metadata/files/6/path","value":"import-fidelity.ts"},{"op":"replace","path":"/metadata/files/5/path","value":"identity.ts"},{"op":"replace","path":"/metadata/files/4/path","value":"github-url.ts"},{"op":"replace","path":"/metadata/files/3/path","value":"durable-state.ts"},{"op":"add","path":"/metadata/files/37","value":{"path":"test/smoke.test.ts","scope":"project"}},{"op":"add","path":"/metadata/files/38","value":{"path":"test/typed-sentinel-normalization.test.ts","scope":"project"}},{"op":"add","path":"/metadata/files/39","value":{"path":"test/webhooks.test.ts","scope":"project"}},{"op":"add","path":"/metadata/files/40","value":{"path":"test/workspace-resolution.test.ts","scope":"project"}},{"op":"add","path":"/metadata/files/41","value":{"path":"webhooks.ts","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-11T18:15:04.046Z"}],"before_hash":"120f44f4404bb91dfc21dfc04798c58082f0bbecfb88dd7650884cc554b89a40","after_hash":"6d05900cfc7327279db9db6169cb0580ed4c042c6d6abe767f975c08ea1f29d4","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":"0a448b110804d52befe2353df909cc7e025c192dc772d8b01ffb058d551c70b6"} +{"hash_algorithm":"sha256","ts":"2026-11-11T18:15:05.003Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/59","value":{"command":"node --test --test-concurrency=1 test/durable-recovery.test.ts","scope":"project","timeout_seconds":1200,"provenance":{"author":"codex-github230-1111","created_at":"2026-11-11T18:15:04.959Z","source_kind":"local_mutation","source_ref":"fix/reference-tags-are-not-import-identity"}}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-11T18:15:05.003Z"}],"before_hash":"6d05900cfc7327279db9db6169cb0580ed4c042c6d6abe767f975c08ea1f29d4","after_hash":"994a63074e3a0b2c5b234d4396a5fe24154c4b7163470a1a649331e009108583","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":"db612efb22454a08610187a2963c36b3ef09906edc33917c12ddeab830f42631"} +{"hash_algorithm":"sha256","ts":"2026-11-11T18:20:22.259Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-11T18:20:22.259Z","author":"codex-github230-1111","text":"Round 4 review read receipt: 13 threads with 13 comments/diffs and 6 review bodies enumerated without remaining pages; all 6 new thread texts/diffs and the new CodeRabbit body read, carrying forward the 7 already accepted round-3 threads. Q98c and Q98i are already fixed by 90d530f; Q984 is marked addressed there. New unrelated minor findings are tracked in pm-github-curated-reopen-followup, pm-github-pr-url-alias-followup and pm-github-blocker-format-followup after searching existing items. The outside-diff README owner-linking instructions now use github_url. Retain required append-only tracking evidence despite the bot scope suggestion to delete the existing Projects follow-up. Round-4 code remains scoped to durable publication and typed corrupt-plan recovery."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-11T18:20:22.259Z"}],"before_hash":"994a63074e3a0b2c5b234d4396a5fe24154c4b7163470a1a649331e009108583","after_hash":"782986f81a12510fea9190d82578149600399200c91a4df908d1601e43051743","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":"2dfbadf36d97f7c837c2d1382e365f330914c62eaf0a07bb17eeff8269f36b43"} +{"hash_algorithm":"sha256","ts":"2026-11-11T18:37:25.733Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-11T18:37:25.732Z","author":"codex-github230-1111","text":"Round 4 focused expanded corpus passes 11/11 on real non-git SDK trackers. Native faults write actual partial bytes only to hidden candidates; empty-source retry and subsequent real import still work. SIGKILL at the flushed-candidate hook leaves zero native items and no discoverable final plan. Concurrent readers observe previous complete state during publication, failure and interruption; retry succeeds. Empty, unparseable and structurally incomplete JSON all produce github_import_plan_corrupt with restore/journal recovery instructions. The same validateGithubImportPlan function checks both reads and apply. Build, lint, typecheck, docstrings (31 files, 277 declarations), zero-duplication gate and strict health pass. The initial privacy audit passes 9459 objects; final commit will be audited again. Bounded dependent-file gates are running without failures so far."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-11T18:37:25.733Z"}],"before_hash":"782986f81a12510fea9190d82578149600399200c91a4df908d1601e43051743","after_hash":"d8d8dd0a90d85591020bb72c20195a31f5bfcaa3b595db35d2b4c85c0a7b97ab","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":"1da6ae1f2b473224956e3cfc91279251d20f75e86ed63b98871fe6ed47cd28c7"} +{"hash_algorithm":"sha256","ts":"2026-11-11T18:37:26.712Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/60","value":{"command":"bun test --timeout 60000 test/durable-recovery.test.ts","scope":"project","timeout_seconds":1200,"provenance":{"author":"codex-github230-1111","created_at":"2026-11-11T18:37:26.637Z","source_kind":"local_mutation","source_ref":"fix/reference-tags-are-not-import-identity"}}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-11T18:37:26.712Z"}],"before_hash":"d8d8dd0a90d85591020bb72c20195a31f5bfcaa3b595db35d2b4c85c0a7b97ab","after_hash":"231b8b47d1b6924de417d56f722a5e72b8e26dd4f89b70a428331629787c6ec2","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":"af6c0b690a47f35b1ec5401bc6ebbd5160f31948ca2f796832e81c63e7de002c"} +{"hash_algorithm":"sha256","ts":"2026-11-11T18:42:15.476Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-11T18:42:15.474Z","author":"codex-github230-1111","text":"Round 4 bounded sweep found one compatibility edge: an existing comment lease in a read-only directory must report contention rather than degrade because staging cannot write there. Added an early lstat existence hint, retaining the final exclusive hard link as the authoritative race check. The full import-lock file will be rerun after the remaining sweep; the real permission test and cross-process contention tests cover the preserved contract. No SDK or dependency change was needed."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-11T18:42:15.476Z"}],"before_hash":"231b8b47d1b6924de417d56f722a5e72b8e26dd4f89b70a428331629787c6ec2","after_hash":"f865eb803839a907f18eda2e4a665c88c04c1c56d503f4fff56ec6a5b54be1d5","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":"9dfdfba24fadcf849e9aef7a74bc97aa9d74609a8957e5cfa35703bb58293aab"} +{"hash_algorithm":"sha256","ts":"2026-11-11T19:01:16.776Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/docs/2/path","value":"https://github.com/unbraind/pm-github/pull/233/checks"},{"op":"add","path":"/metadata/docs/3","value":{"path":"README.md","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-11T19:01:16.776Z"}],"before_hash":"f865eb803839a907f18eda2e4a665c88c04c1c56d503f4fff56ec6a5b54be1d5","after_hash":"a7c36a9d879e315404165fe6ca5459984ef7d83f35586df7335a82db6d51ae61","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":"7357d0f442fb193b48d03a448debdac3acf9cdf0bef1d8c66bbd6cfcbd831cbc"} +{"hash_algorithm":"sha256","ts":"2026-11-11T19:05:49.795Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-11T19:05:49.794Z","author":"codex-github230-1111","text":"Round 4 behavioral revert proof in a disposable package copy, using real SDK trackers and only native filesystem fault injection: old persistGithubImportPlan behavior (retaining the observation hook) gives 2 failures: partial final JSON poisons an empty-source retry, and SIGKILL exposes a final plan before publication. Old readGithubImportPlan gives 6 missing typed-corruption refusals. Replacing writeGithubStateAtomic with the prior direct-write behavior gives 3 concurrent-reader failures (publication, write failure, interruption). Old acquireImportLock gives 1 observed empty reserved lease. Revert commands use node --test --test-concurrency=1 with the matching partial native plan/crash after plan, corrupt recovery plan, concurrent state readers, and failed atomic lock publication name patterns. Total: 12 expected failures, no cancellations. Restored fixed code passes the complete durable-recovery file 11/11 plus the lease regression 1/1; all 9 code/docs/test files match the package implementation byte-for-byte. The package worktree was never mutated by these reverts."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-11T19:05:49.795Z"}],"before_hash":"a7c36a9d879e315404165fe6ca5459984ef7d83f35586df7335a82db6d51ae61","after_hash":"4ec74ada7d0d6cbac1b03146bb123628c0a2061650481b6b7b0d62f71c15bd26","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":"38f4d6b7cb3d8991541df80e883d3104c46bb22e60bd6f62203d97a527843888"} +{"hash_algorithm":"sha256","ts":"2026-11-11T19:16:39.154Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-11T19:16:39.154Z","author":"codex-github230-1111","text":"Round 4 final portable regression proof: the initial Node-only filesystem replacement harness did not inject faults under Bun, so fault injection now uses the explicit prepublication hook with real partial candidate writes and injected EIO. No pm SDK or filesystem APIs are mocked in the new durable-recovery corpus. Node and native Bun each pass 11/11, and the lease regression passes under Bun. Repeated behavioral reverts preserve the observation hooks while restoring unsafe direct writer/lease behavior: plan writer 2 failures, old reader 6, direct state publisher 3, direct lease publisher 1. All are behavioral failures (poisoned zero-source retry, premature final publication, absent corruption refusal, or partial bytes observed). Restored source passes 11 durable cases plus 1 lease case and matches all 9 package code/docs/test files byte-for-byte."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-11T19:16:39.154Z"}],"before_hash":"4ec74ada7d0d6cbac1b03146bb123628c0a2061650481b6b7b0d62f71c15bd26","after_hash":"66c67d792c90190895614f5e417cca6f8edd3f40ef3489cdd92f9c299e7b3fd5","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":"74933d8c5ee782349c1e41cb91a0953510f98cd6901f8290e97e0fac093ddf50"} +{"hash_algorithm":"sha256","ts":"2026-11-11T19:16:39.998Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-11T19:16:39.997Z","author":"codex-github230-1111","text":"Round 4 final gates: 51 completed files / 54 bounded invocations, 1161 passes, zero failures, two existing pm-cli#1656 skips. All 73 importer-gate cases pass; full import-lock recheck passes 25/25 after preserving read-only contention. Refresh is 34 passes plus one existing skip; accepted review files pass 30/30 and 29/29. Linked pm runs of node --test --test-concurrency=1 test/durable-recovery.test.ts and bun test --timeout 60000 test/durable-recovery.test.ts each pass 11/11. Automatic test-result tracking is disabled by project policy, so these outcomes and revert evidence are recorded in comments. closeout-workflow reached its 20-minute cap after 65 passes and one cancellation without assertion failures. Full npm/Bun release and exact 100% coverage remain CI certification at the linked PR checks page. Six logical writes now use four shared atomic-publication entry points; SDK-owned native journals/history remain SDK-managed. No new SDK gap or dependency change. Three minor review follow-ups remain open. Releasing this author claim and leaving the main issue in progress."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-11T19:16:39.998Z"}],"before_hash":"66c67d792c90190895614f5e417cca6f8edd3f40ef3489cdd92f9c299e7b3fd5","after_hash":"c64dfe741c2207a331ca1af502d2b43e4a3ac855d6278dfcf625d05cfb367821","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":"c72a40fb38dc103a9b9ba4d58e0ff83ce2746ea1a6b3ee449f7b2874d04d0b58"} +{"hash_algorithm":"sha256","ts":"2026-11-11T19:16:40.876Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-11T19:16:40.876Z"}],"before_hash":"c64dfe741c2207a331ca1af502d2b43e4a3ac855d6278dfcf625d05cfb367821","after_hash":"6c9756c0e2477949da04f5b1f04ea146af31dff4f11990b58bb4baa0f6e071bc","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":"2b2a9b20560efb43ab90179ee09d0c4b3a7a1f4ce2bcc9bd66341a27ea06f2de"} +{"hash_algorithm":"sha256","ts":"2026-11-11T20:01:19.907Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-11T20:01:19.907Z"},{"op":"add","path":"/metadata/assignee","value":"codex-github230-1111"},{"op":"add","path":"/metadata/claim_principal","value":"codex-github230-1111"}],"before_hash":"6c9756c0e2477949da04f5b1f04ea146af31dff4f11990b58bb4baa0f6e071bc","after_hash":"81299111eb6dab7f5d6e3960b087b044b6627fdda13cde7b66fc6d4ce5280239","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":"9babf9ea2bec8edd0dcea1d33fd4cd5fd2f481078c4ff2f9c7ff6eae46dea8a6"} +{"hash_algorithm":"sha256","ts":"2026-11-11T20:13:15.527Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-11T20:13:15.527Z","author":"codex-github230-1111","text":"Round 5: claimed the main item and the two existing follow-ups now in scope, without creating duplicates. Keep structured URL identity parsing strict. Prose extraction tracks balanced brackets, matching emphasis and trailing punctuation. Every unsupported HTTP URL becomes a visible unparsed_url reference, never a guessed hash from its interior, so it cannot make an item cleared or produce a cleared comment. Explicit URL alias resolution is shared by ownership and refresh, retaining the parsed issue/PR kind. New real non-git SDK tracker tests cover all requested wrappers, punctuation, the exact mixed-reference reproduction, unsupported URLs and case/underscore aliases. Review snapshot: 21 threads/comments and 9 reviews enumerated without further pages; the two targeted full texts/diffs were read (SJvl and Q99D). Accepted round-4 changes remain intact."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-11T20:13:15.527Z"}],"before_hash":"81299111eb6dab7f5d6e3960b087b044b6627fdda13cde7b66fc6d4ce5280239","after_hash":"5e13641ddda713757cb00bbf616a36b32eda4a135433f3a7f1f7f6da6123f359","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":"6e8185cad1e573ea0ea26766c0788e79bcb321efa1276047aa5fd46b5393a721"} +{"hash_algorithm":"sha256","ts":"2026-11-11T20:17:09.439Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/41/path","value":"test/workspace-resolution.test.ts"},{"op":"replace","path":"/metadata/files/40/path","value":"test/webhooks.test.ts"},{"op":"replace","path":"/metadata/files/39/path","value":"test/typed-sentinel-normalization.test.ts"},{"op":"replace","path":"/metadata/files/38/path","value":"test/smoke.test.ts"},{"op":"replace","path":"/metadata/files/37/path","value":"test/scheduled-refusals.test.ts"},{"op":"replace","path":"/metadata/files/36/path","value":"test/review-round3-identity.test.ts"},{"op":"replace","path":"/metadata/files/35/path","value":"test/review-import-identity.test.ts"},{"op":"replace","path":"/metadata/files/34/path","value":"test/repeat-import-noop.test.ts"},{"op":"replace","path":"/metadata/files/33/path","value":"test/refresh.test.ts"},{"op":"replace","path":"/metadata/files/32/path","value":"test/reference-import-identity.test.ts"},{"op":"replace","path":"/metadata/files/31/path","value":"test/link-deps.test.ts"},{"op":"replace","path":"/metadata/files/30/path","value":"test/label-import-model.test.ts"},{"op":"replace","path":"/metadata/files/29/path","value":"test/import-projection.test.ts"},{"op":"replace","path":"/metadata/files/28/path","value":"test/import-lock.test.ts"},{"op":"replace","path":"/metadata/files/27/path","value":"test/import-gate.test.ts"},{"op":"replace","path":"/metadata/files/26/path","value":"test/import-fidelity.test.ts"},{"op":"replace","path":"/metadata/files/25/path","value":"test/import-errors.test.ts"},{"op":"replace","path":"/metadata/files/24/path","value":"test/import-conflict-report.test.ts"},{"op":"replace","path":"/metadata/files/23/path","value":"test/identity.test.ts"},{"op":"replace","path":"/metadata/files/22/path","value":"test/helpers/import-mirror.ts"},{"op":"replace","path":"/metadata/files/21/path","value":"test/handler-failures.test.ts"},{"op":"replace","path":"/metadata/files/20/path","value":"test/github-url.test.ts"},{"op":"add","path":"/metadata/files/42","value":{"path":"webhooks.ts","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-11T20:17:09.439Z"}],"before_hash":"5e13641ddda713757cb00bbf616a36b32eda4a135433f3a7f1f7f6da6123f359","after_hash":"be6988f6822f5c415bdb642297f21278d36647ea094f798ac16acf370549ba4d","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":"84a2a1347a0ef19b5da4e31cb594c77d87f93e9d8f63f254f0fda54955664a0d"} +{"hash_algorithm":"sha256","ts":"2026-11-11T20:17:14.834Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/61","value":{"command":"node --test --test-concurrency=1 test/github-url.test.ts","scope":"project","timeout_seconds":1200,"provenance":{"author":"codex-github230-1111","created_at":"2026-11-11T20:17:14.785Z","source_kind":"local_mutation","source_ref":"fix/reference-tags-are-not-import-identity"}}},{"op":"add","path":"/metadata/tests/62","value":{"command":"bun test --timeout 60000 test/github-url.test.ts","scope":"project","timeout_seconds":1200,"provenance":{"author":"codex-github230-1111","created_at":"2026-11-11T20:17:14.785Z","source_kind":"local_mutation","source_ref":"fix/reference-tags-are-not-import-identity"}}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-11T20:17:14.834Z"}],"before_hash":"be6988f6822f5c415bdb642297f21278d36647ea094f798ac16acf370549ba4d","after_hash":"6be5f1574264a673e0761579c44ddba1245b0bc91c9b15ead169f0bb98b894d5","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":"c1a7daac050a4c52f655f2930c21c28da0c27b2e9bb4241380fd926f37f1aa01"} +{"hash_algorithm":"sha256","ts":"2026-11-11T20:30:23.327Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-11T20:30:23.327Z","author":"codex-github230-1111","text":"Round 5 false-clear path audit also found self-exclusion treated every gh: tag as ownership. collectBlockerReferences now uses the same authoritative importIdentityKeys resolver to exclude only actual self identity, retaining reference-tagged blockers. Added a real tracker reproduction with gh:o/r#2 plus the mixed closed/open Markdown links; it must retain open #2. Legacy generated owner markers still resolve through the existing compatibility rule. No other ownership or content merge behavior changed. The focused file now contains 31 cases; behavioral reverts will cover the collector independently."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-11T20:30:23.327Z"}],"before_hash":"6be5f1574264a673e0761579c44ddba1245b0bc91c9b15ead169f0bb98b894d5","after_hash":"14e3ecf64432bcb75f91656966c1c9f4175f8b7a94de8496fec81744a98a3ac7","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":"aaa66b0e302f0182bf638b942c86983410e6c1c3744e469b9c1265e32ae15a30"} +{"hash_algorithm":"sha256","ts":"2026-11-11T21:06:54.226Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-11T21:06:54.225Z","author":"codex-github230-1111","text":"Round 5 final behavioral revert proof in a disposable package copy with real SDK trackers and real HTTP transport: node --test --test-concurrency=1 --test-name-pattern PATTERN test/github-url.test.ts. Restoring only the previous tokenizer body (retaining span metadata) makes all 17 ^Markdown cases fail; restoring parseBlockerReferences makes all 8 ^Unparsed cases fail; restoring collectBlockerReferences makes the ^Reference tags case fail; restoring selectRefreshTargets makes all 5 ^Refresh excludes PRs cases fail. Total 31 expected behavioral failures, zero cancellations. Each required wrapper and punctuation case fails on revert, including the supplied mixed closed/open URL reproduction. The restored implementation passes 31/31 and all seven code/docs/test files match the package implementation byte-for-byte. The package worktree was never reverted."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-11T21:06:54.226Z"}],"before_hash":"14e3ecf64432bcb75f91656966c1c9f4175f8b7a94de8496fec81744a98a3ac7","after_hash":"9a365d6e7998d845b9c9fbb9e732787e51e6654121fb689f3167458a11030b1c","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":"9cf88ea09f287cb6b0f41e1a45c38be94a9e62b48d7387dce4725c66ddcda2ef"} +{"hash_algorithm":"sha256","ts":"2026-11-11T21:06:55.106Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-11T21:06:55.106Z","author":"codex-github230-1111","text":"Round 5 final gates: 54 selected test files / 57 bounded invocations, 56 completed invocations, 1231 passes, zero assertion failures, 2 existing pm-cli#1656 skips. All 73 import-gate cases pass across four disjoint name partitions. closeout-workflow reached its 20-minute cap after 69 passes and one cancellation; it remains incomplete CI evidence. Final blockers recheck passes 35/35. Linked pm runs of node --test --test-concurrency=1 test/github-url.test.ts and bun test --timeout 60000 test/github-url.test.ts each pass 31/31; follow-up pm runs pass 26 blocker cases and 5 alias cases. Project policy disables automatic test-result writes, so exact outcomes are recorded here. Lint, typecheck, build, test compilation, docstrings (31 files / 278 declarations), zero duplication and strict tracker health pass (two existing advisories). Full npm/Bun release and exact 100% coverage remain CI certification under the task budget. Read receipt: two supplied findings; 21 threads / 21 comments and 9 reviews enumerated without remaining pages, with the two targeted full texts/diffs read (PRRT_kwDOTbN4pM62SJvl and PRRT_kwDOTbN4pM62Q99D). The accepted rounds remain intact. Releasing all three scoped claims; items remain in progress. Curated reopen and Projects follow-ups remain unchanged."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-11T21:06:55.106Z"}],"before_hash":"9a365d6e7998d845b9c9fbb9e732787e51e6654121fb689f3167458a11030b1c","after_hash":"cbe7bec59e0dfa0a2770b75852651833b2fd3934110eda0af4ce81a373ccd602","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":"e223513d813dfd2ebeff0dca405cd49f5de89018165983a1334b5fe930b6e342"} +{"hash_algorithm":"sha256","ts":"2026-11-11T21:06:57.443Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-11T21:06:57.443Z"}],"before_hash":"cbe7bec59e0dfa0a2770b75852651833b2fd3934110eda0af4ce81a373ccd602","after_hash":"df0959c602b13dd566cafca67181aa8d1e2c81979b28627b1e6fd97d69e075f5","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":"8638d9e71253470e354e4a9624ad9bfa51ee5b97cfe5b9cf175e04144e6a51c8"} +{"hash_algorithm":"sha256","ts":"2026-11-11T22:07:03.844Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-11T22:07:03.844Z"},{"op":"add","path":"/metadata/assignee","value":"codex-github230-1111"},{"op":"add","path":"/metadata/claim_principal","value":"codex-github230-1111"}],"before_hash":"df0959c602b13dd566cafca67181aa8d1e2c81979b28627b1e6fd97d69e075f5","after_hash":"5acc5c169778152f3ea2af383ac0bcf739f74eccaf3fb55c64eaf259cc274e33","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":"36b353a20585af1f27a02f44ab65dd1d3a2364b9d379c51eb038a1cd75929ea2"} +{"hash_algorithm":"sha256","ts":"2026-11-11T22:07:06.776Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-11T22:07:06.775Z","author":"codex-github230-1111","text":"Round 6: accepted head rechecked clean. Audit every existing-item write target against exactly one authoritative GitHub owner, including deterministic upsert collisions, URL/alias matches, refresh, closeout and durable replay/finalization. Choose a typed collision refusal rather than silently reassigning an existing deterministic ID; preserve byte-identical item content on refusal and ordinary owner-matching idempotency. Real SDK tracker matrix and isolated behavioral reverts will cover each path. The completion-date reopen defect remains deferred. Current review receipt: one supplied blocking finding; 21 threads/comments and 10 reviews enumerated without further pages. The curated-reopen thread and latest CodeRabbit body were read in full."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-11T22:07:06.776Z"}],"before_hash":"5acc5c169778152f3ea2af383ac0bcf739f74eccaf3fb55c64eaf259cc274e33","after_hash":"952bbe693105492c61e765cfd39994b4c77315a2cdf10f56bc98b7c1473bec07","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":"3f692b1f01c2aa6d1ad2907313c9dad2ab5bdaf609b400cf785778de30c07e30"} +{"hash_algorithm":"sha256","ts":"2026-11-11T22:21:10.789Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/63","value":{"command":"node --test --test-concurrency=1 test/import-ownership.test.ts","scope":"project","provenance":{"author":"codex-github230-1111","created_at":"2026-11-11T22:21:10.719Z","source_kind":"local_mutation","source_ref":"fix/reference-tags-are-not-import-identity"}}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-11T22:21:10.789Z"}],"before_hash":"952bbe693105492c61e765cfd39994b4c77315a2cdf10f56bc98b7c1473bec07","after_hash":"b5fc51d79ac15d700aec02d63d1d7c5a068fc258e8fd0f782f74ebeca242f5d5","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":"04b7d32a50a550ecfbc613976483ba762ab5b6c697224ff95b8935b1a55a6e8c"} +{"hash_algorithm":"sha256","ts":"2026-11-11T22:21:12.108Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/64","value":{"command":"bun test --timeout 60000 test/import-ownership.test.ts","scope":"project","provenance":{"author":"codex-github230-1111","created_at":"2026-11-11T22:21:12.037Z","source_kind":"local_mutation","source_ref":"fix/reference-tags-are-not-import-identity"}}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-11T22:21:12.108Z"}],"before_hash":"b5fc51d79ac15d700aec02d63d1d7c5a068fc258e8fd0f782f74ebeca242f5d5","after_hash":"d9c0510137df4e328edfb33cc57958a728adfd17955f6fff940b38c8e493fd94","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":"1150194714b5537373a522397a2cd382659a6c8b09b772b73a8aff6f7ed47408"} +{"hash_algorithm":"sha256","ts":"2026-11-11T22:21:13.228Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-11T22:21:13.228Z","author":"codex-github230-1111","text":"Round 6 implementation: typed github_import_owner_conflict refuses occupied deterministic IDs rather than allocating suffixes. One shared positive owner assertion protects preflight and every SDK item-lock write, including replay, fidelity and annotations; bound delegates retain ambient mutation hooks. Plain CLI writes and dependency edges carry --expect-hash from the same live ownership read. URL-only, alias-only and number-only targets are checked independently. Closeout holds imported owners outside its delivery repository while retaining explicit native delivery for local items. Real SDK non-git matrix passes 21/21 under Node and Bun, including matching-owner idempotency and ambient hook controls. The bounded dependent sweep is now running with two workers."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-11T22:21:13.228Z"}],"before_hash":"d9c0510137df4e328edfb33cc57958a728adfd17955f6fff940b38c8e493fd94","after_hash":"e4311a3a885628fac7603a6559b482174558855a212d2c9c07befacb2b5d8fd6","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":"a3c01eb7238061e0af8b1289eac78d8187c32b6dbf762aeb5bdee966cac88cc2"} +{"hash_algorithm":"sha256","ts":"2026-11-11T22:22:18.018Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/42/path","value":"test/workspace-resolution.test.ts"},{"op":"replace","path":"/metadata/files/41/path","value":"test/webhooks.test.ts"},{"op":"replace","path":"/metadata/files/40/path","value":"test/typed-sentinel-normalization.test.ts"},{"op":"replace","path":"/metadata/files/39/path","value":"test/smoke.test.ts"},{"op":"replace","path":"/metadata/files/38/path","value":"test/scheduled-refusals.test.ts"},{"op":"replace","path":"/metadata/files/37/path","value":"test/review-round3-identity.test.ts"},{"op":"replace","path":"/metadata/files/36/path","value":"test/review-import-identity.test.ts"},{"op":"replace","path":"/metadata/files/35/path","value":"test/repeat-import-noop.test.ts"},{"op":"replace","path":"/metadata/files/34/path","value":"test/refresh.test.ts"},{"op":"replace","path":"/metadata/files/33/path","value":"test/reference-import-identity.test.ts"},{"op":"replace","path":"/metadata/files/32/path","value":"test/link-deps.test.ts"},{"op":"replace","path":"/metadata/files/31/path","value":"test/label-import-model.test.ts"},{"op":"replace","path":"/metadata/files/30/path","value":"test/import-projection.test.ts"},{"op":"replace","path":"/metadata/files/29/path","value":"test/import-lock.test.ts"},{"op":"replace","path":"/metadata/files/28/path","value":"test/import-gate.test.ts"},{"op":"replace","path":"/metadata/files/27/path","value":"test/import-fidelity.test.ts"},{"op":"replace","path":"/metadata/files/26/path","value":"test/import-errors.test.ts"},{"op":"replace","path":"/metadata/files/25/path","value":"test/import-conflict-report.test.ts"},{"op":"replace","path":"/metadata/files/24/path","value":"test/identity.test.ts"},{"op":"replace","path":"/metadata/files/23/path","value":"test/helpers/import-mirror.ts"},{"op":"replace","path":"/metadata/files/22/path","value":"test/handler-failures.test.ts"},{"op":"replace","path":"/metadata/files/21/path","value":"test/github-url.test.ts"},{"op":"replace","path":"/metadata/files/20/path","value":"test/fixtures/refresh-crash.ts"},{"op":"replace","path":"/metadata/files/19/path","value":"test/fixtures/atomic-plan-crash.ts"},{"op":"replace","path":"/metadata/files/18/path","value":"test/durable-recovery.test.ts"},{"op":"replace","path":"/metadata/files/17/path","value":"test/dryrun-preview.test.ts"},{"op":"replace","path":"/metadata/files/16/path","value":"test/daily-sync-idempotency.test.ts"},{"op":"replace","path":"/metadata/files/15/path","value":"test/coverage-remainder.test.ts"},{"op":"replace","path":"/metadata/files/14/path","value":"test/coverage-edges.test.ts"},{"op":"replace","path":"/metadata/files/13/path","value":"test/cited-issue-adoption.test.ts"},{"op":"replace","path":"/metadata/files/12/path","value":"test/atomic.test.ts"},{"op":"replace","path":"/metadata/files/11/path","value":"refresh.ts"},{"op":"replace","path":"/metadata/files/10/path","value":"projects.ts"},{"op":"replace","path":"/metadata/files/9/path","value":"index.ts"},{"op":"replace","path":"/metadata/files/8/path","value":"import-tags.ts"},{"op":"replace","path":"/metadata/files/7/path","value":"import-ownership.ts"},{"op":"add","path":"/metadata/files/43","value":{"path":"webhooks.ts","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-11T22:22:18.018Z"}],"before_hash":"e4311a3a885628fac7603a6559b482174558855a212d2c9c07befacb2b5d8fd6","after_hash":"ce5840596922b4afff4f253dd6ac1aa6e658a354e923b360311bda95fa52bdfc","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":"2a23be5c8026d58eed57d14d37b573a00eb33822c6cd1d185860f3ce4915faad"} +{"hash_algorithm":"sha256","ts":"2026-11-11T22:22:19.270Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/43/path","value":"test/workspace-resolution.test.ts"},{"op":"replace","path":"/metadata/files/42/path","value":"test/webhooks.test.ts"},{"op":"replace","path":"/metadata/files/41/path","value":"test/typed-sentinel-normalization.test.ts"},{"op":"replace","path":"/metadata/files/40/path","value":"test/smoke.test.ts"},{"op":"replace","path":"/metadata/files/39/path","value":"test/scheduled-refusals.test.ts"},{"op":"replace","path":"/metadata/files/38/path","value":"test/review-round3-identity.test.ts"},{"op":"replace","path":"/metadata/files/37/path","value":"test/review-import-identity.test.ts"},{"op":"replace","path":"/metadata/files/36/path","value":"test/repeat-import-noop.test.ts"},{"op":"replace","path":"/metadata/files/35/path","value":"test/refresh.test.ts"},{"op":"replace","path":"/metadata/files/34/path","value":"test/reference-import-identity.test.ts"},{"op":"replace","path":"/metadata/files/33/path","value":"test/link-deps.test.ts"},{"op":"replace","path":"/metadata/files/32/path","value":"test/label-import-model.test.ts"},{"op":"replace","path":"/metadata/files/31/path","value":"test/import-projection.test.ts"},{"op":"replace","path":"/metadata/files/30/path","value":"test/import-ownership.test.ts"},{"op":"add","path":"/metadata/files/44","value":{"path":"webhooks.ts","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-11T22:22:19.270Z"}],"before_hash":"ce5840596922b4afff4f253dd6ac1aa6e658a354e923b360311bda95fa52bdfc","after_hash":"7893b821dcaaf3e2fef6d75fa38126585daffd66865ec2817b9f3c99d17c2c0c","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":"6c09011011309edbb0c055a342e2007bfc9a712c167109460ceb404e1fc9b4a1"} +{"hash_algorithm":"sha256","ts":"2026-11-11T22:22:20.424Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/44/path","value":"test/workspace-resolution.test.ts"},{"op":"replace","path":"/metadata/files/43/path","value":"test/webhooks.test.ts"},{"op":"replace","path":"/metadata/files/42/path","value":"test/typed-sentinel-normalization.test.ts"},{"op":"replace","path":"/metadata/files/41/path","value":"test/smoke.test.ts"},{"op":"replace","path":"/metadata/files/40/path","value":"test/scheduled-refusals.test.ts"},{"op":"replace","path":"/metadata/files/39/path","value":"test/review-round3-identity.test.ts"},{"op":"replace","path":"/metadata/files/38/path","value":"test/review-import-identity.test.ts"},{"op":"replace","path":"/metadata/files/37/path","value":"test/repeat-import-noop.test.ts"},{"op":"replace","path":"/metadata/files/36/path","value":"test/refresh.test.ts"},{"op":"replace","path":"/metadata/files/35/path","value":"test/reference-import-identity.test.ts"},{"op":"replace","path":"/metadata/files/34/path","value":"test/link-deps.test.ts"},{"op":"replace","path":"/metadata/files/33/path","value":"test/label-import-model.test.ts"},{"op":"replace","path":"/metadata/files/32/path","value":"test/import-projection.test.ts"},{"op":"replace","path":"/metadata/files/31/path","value":"test/import-ownership.test.ts"},{"op":"replace","path":"/metadata/files/30/path","value":"test/import-lock.test.ts"},{"op":"replace","path":"/metadata/files/29/path","value":"test/import-gate.test.ts"},{"op":"replace","path":"/metadata/files/28/path","value":"test/import-fidelity.test.ts"},{"op":"replace","path":"/metadata/files/27/path","value":"test/import-errors.test.ts"},{"op":"replace","path":"/metadata/files/26/path","value":"test/import-conflict-report.test.ts"},{"op":"replace","path":"/metadata/files/25/path","value":"test/identity.test.ts"},{"op":"replace","path":"/metadata/files/24/path","value":"test/helpers/import-mirror.ts"},{"op":"replace","path":"/metadata/files/23/path","value":"test/handler-failures.test.ts"},{"op":"replace","path":"/metadata/files/22/path","value":"test/github-url.test.ts"},{"op":"replace","path":"/metadata/files/21/path","value":"test/fixtures/refresh-crash.ts"},{"op":"replace","path":"/metadata/files/20/path","value":"test/fixtures/ownership-peer.ts"},{"op":"add","path":"/metadata/files/45","value":{"path":"webhooks.ts","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-11T22:22:20.424Z"}],"before_hash":"7893b821dcaaf3e2fef6d75fa38126585daffd66865ec2817b9f3c99d17c2c0c","after_hash":"cd016b0aa1cb47f4718e8013a1292a42fd38c6a48cac333e17e3dc1a508e9c1c","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":"6cf3981883a9cf42e00f5b4807555d1742c088d838d2116c348fe72d879a573c"} +{"hash_algorithm":"sha256","ts":"2026-11-11T22:30:41.005Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"tests_update","patch":[{"op":"add","path":"/metadata/tests/63/timeout_seconds","value":1200},{"op":"add","path":"/metadata/tests/63/updated_by","value":"codex-github230-1111"},{"op":"add","path":"/metadata/tests/63/updated_at","value":"2026-11-11T22:30:40.987Z"},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-11T22:30:41.005Z"},{"op":"add","path":"/metadata/pm_format_version","value":2}],"before_hash":"cd016b0aa1cb47f4718e8013a1292a42fd38c6a48cac333e17e3dc1a508e9c1c","after_hash":"d33582cbe00622f0bd621d0f35c27876a74b5b0087cc0d2a45afea8bcaffc5e2","item_hash_version":3,"context":{"linked_test_update":{"index":64,"fields":["timeout_seconds"]},"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":"fa8e0ee511ee269e46401cd189488782e417809c359aa86988274f18600ee4c0"} +{"hash_algorithm":"sha256","ts":"2026-11-11T22:30:42.151Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"tests_update","patch":[{"op":"add","path":"/metadata/tests/64/timeout_seconds","value":1200},{"op":"add","path":"/metadata/tests/64/updated_by","value":"codex-github230-1111"},{"op":"add","path":"/metadata/tests/64/updated_at","value":"2026-11-11T22:30:42.144Z"},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-11T22:30:42.151Z"}],"before_hash":"d33582cbe00622f0bd621d0f35c27876a74b5b0087cc0d2a45afea8bcaffc5e2","after_hash":"e140d89fd42afbaf8bca265ac400d6736ea775ae2fb8e1e7ad5dbf28667e4443","item_hash_version":3,"context":{"linked_test_update":{"index":65,"fields":["timeout_seconds"]},"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":"82d49f4c43a015ec78fb8fd27b34f98a4010d4eb050c430d8fb03b3a216ae48b"} +{"hash_algorithm":"sha256","ts":"2026-11-11T23:34:32.278Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/45/path","value":"test/workspace-resolution.test.ts"},{"op":"replace","path":"/metadata/files/44/path","value":"test/webhooks.test.ts"},{"op":"replace","path":"/metadata/files/43/path","value":"test/typed-sentinel-normalization.test.ts"},{"op":"replace","path":"/metadata/files/42/path","value":"test/smoke.test.ts"},{"op":"replace","path":"/metadata/files/41/path","value":"test/sdk-adoption.test.ts"},{"op":"add","path":"/metadata/files/46","value":{"path":"webhooks.ts","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-11T23:34:32.278Z"}],"before_hash":"e140d89fd42afbaf8bca265ac400d6736ea775ae2fb8e1e7ad5dbf28667e4443","after_hash":"796ec8a5c6ffa152463dc125586bdc5a0b931c317a9eb5d163fad325ab7c3d25","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":"7290692062eed75a636268c652ec6ac6d189a250f0678261d1f6561327599ac1"} +{"hash_algorithm":"sha256","ts":"2026-11-11T23:34:33.425Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/65","value":{"command":"node --test --test-concurrency=1 --test-name-pattern \"^(atomic import falls back|non-atomic import reports)\" test/coverage-remainder.test.ts","scope":"project","timeout_seconds":1200,"provenance":{"author":"codex-github230-1111","created_at":"2026-11-11T23:34:33.357Z","source_kind":"local_mutation","source_ref":"fix/reference-tags-are-not-import-identity"}}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-11T23:34:33.425Z"}],"before_hash":"796ec8a5c6ffa152463dc125586bdc5a0b931c317a9eb5d163fad325ab7c3d25","after_hash":"89e9ef29f14c95b36c691d8649167cb58bc53495a3ceaa29498ac8d8f1f26478","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":"3621b4bcc9596edceba319cbe8314acbfdff38ce0a16df2239ecebdcfa4661c3"} +{"hash_algorithm":"sha256","ts":"2026-11-11T23:34:34.406Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/66","value":{"command":"node --test --test-concurrency=1 --test-name-pattern \"^runImport (closes a linked|reopens a linked)\" test/handler-failures.test.ts","scope":"project","timeout_seconds":1200,"provenance":{"author":"codex-github230-1111","created_at":"2026-11-11T23:34:34.355Z","source_kind":"local_mutation","source_ref":"fix/reference-tags-are-not-import-identity"}}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-11T23:34:34.406Z"}],"before_hash":"89e9ef29f14c95b36c691d8649167cb58bc53495a3ceaa29498ac8d8f1f26478","after_hash":"420737be19dbfe120d2e62ce5551d394cb60128b25dd2b562377cd987283ce09","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":"ae06013ed5f5dc50d578591157aaa4f0658ba48ceb864740c7e00881980afc88"} +{"hash_algorithm":"sha256","ts":"2026-11-11T23:34:35.333Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/67","value":{"command":"node --test --test-concurrency=1 --test-name-pattern \"^import (refuses before mutation|reconciles a canceled)\" test/import-errors.test.ts","scope":"project","timeout_seconds":1200,"provenance":{"author":"codex-github230-1111","created_at":"2026-11-11T23:34:35.286Z","source_kind":"local_mutation","source_ref":"fix/reference-tags-are-not-import-identity"}}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-11T23:34:35.333Z"}],"before_hash":"420737be19dbfe120d2e62ce5551d394cb60128b25dd2b562377cd987283ce09","after_hash":"d4ef324e4a7709f9b51dc01aa4ae2ec049094474e7bd142ebafd57ee1224979d","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":"a8da870b41fb1180e6671a74680a50cd1bb8966186676f7447d65830d1571f7f"} +{"hash_algorithm":"sha256","ts":"2026-11-11T23:34:36.391Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/68","value":{"command":"node --test --test-concurrency=1 --test-name-pattern \"^shared importer conversion\" test/label-import-model.test.ts","scope":"project","timeout_seconds":1200,"provenance":{"author":"codex-github230-1111","created_at":"2026-11-11T23:34:36.332Z","source_kind":"local_mutation","source_ref":"fix/reference-tags-are-not-import-identity"}}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-11T23:34:36.391Z"}],"before_hash":"d4ef324e4a7709f9b51dc01aa4ae2ec049094474e7bd142ebafd57ee1224979d","after_hash":"65c21a513c2d34d2784dc4ed98a1d544bac0fbe2c0a946a3c540dd679d80d078","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":"93c32ae330f6754076089a4cf04ca35908696cb4668346741238673ac3536389"} +{"hash_algorithm":"sha256","ts":"2026-11-11T23:34:37.525Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/69","value":{"command":"node --test --test-concurrency=1 --test-name-pattern \"^packed npm\" test/import-projection.test.ts","scope":"project","timeout_seconds":1200,"provenance":{"author":"codex-github230-1111","created_at":"2026-11-11T23:34:37.462Z","source_kind":"local_mutation","source_ref":"fix/reference-tags-are-not-import-identity"}}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-11T23:34:37.525Z"}],"before_hash":"65c21a513c2d34d2784dc4ed98a1d544bac0fbe2c0a946a3c540dd679d80d078","after_hash":"4361cef81083edfaa2c9b41810905d0ff3f7acca2556eb132a66e58ec78b2310","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":"ac9d48e2764644bd70e4db530456f92fcd64bd696f6c03e64fa54ab9d7dc4aef"} +{"hash_algorithm":"sha256","ts":"2026-11-11T23:34:38.745Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-11T23:34:38.727Z","author":"codex-github230-1111","text":"Round 6 refinement: deterministic IDs, even alongside a matching reference tag, no longer confer ownership. Released source-number/link markers and generated source descriptions still resolve without migration; a real pre-fidelity marker control updates its original ID and then re-syncs byte-identically. Creation/content plans must retain the generated source marker before SDK updates, otherwise github_import_plan_ownership_invalid refuses before journal publication. Existing-item checks use live normalized ownership, locked SDK hooks, and CLI expected revisions. First-link provider/citation authorization is preserved without overriding explicit GitHub owners. Collision remedy restores intended provenance only for the intended item; otherwise select a different configured ID prefix or tracker and retain existing items/history. New relink fixtures use mutateItem and verify complete history chains plus the current item hash, rather than the birth-only import adapter. Both same-repository and cross-repository closeout transfers are held."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-11T23:34:38.745Z"}],"before_hash":"4361cef81083edfaa2c9b41810905d0ff3f7acca2556eb132a66e58ec78b2310","after_hash":"f79c2ee2a18698ba4a96ac0639e396dbcf283a08e3e4d7e006ac8af4a98318e8","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":"741366f3d8e1e7bfdb7833c7970874cf6cbcc67b23ce747a82faec3b64063e2d"} +{"hash_algorithm":"sha256","ts":"2026-11-11T23:48:50.052Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-11T23:48:50.034Z","author":"codex-github230-1111","text":"Round 6 final behavioral revert proof in disposable package copies only: disabling assertGithubImportOwner alone makes all 19 shared-guard cases fail; removing only ownedPmRun expected-hash arguments makes the real peer/CLI race fail with changed native body/history; disabling assertCloseoutGithubOwner makes both same-repository and cross-repository delivery cases fail. The source matrix has 22 refusal cases and four must-allow/ambient controls. The exact gated atomic upsert reproduction uses a valid URL-only transfer to other/project#8 and compares native item/history bytes before checking the refusal, so a late gate error cannot hide an overwrite. Curated collisions run without Git and include a matching reference tag, plus a second incoming issue to prove whole-batch refusal. Every relink verifies the full native history chain and its current item hash. Source revert commands are node --test --test-concurrency=1 --test-name-pattern PATTERN test/import-ownership.test.ts, with patterns ^Ownership (?!plain CLI|closeout|matching-owner|guard preserves), ^Ownership plain CLI, and ^Ownership closeout. Restored source passes 26/26. Rebuilding the artifact with only the owner assertion disabled makes the packed npm consumer regression fail; restoring and rebuilding makes that exact packed case pass 1/1. Its command is node --test --test-concurrency=1 --test-name-pattern \"^packed npm\" test/import-projection.test.ts. The packed case verifies the public CLI JSON github_import_owner_conflict code, both owner values and unchanged native bytes. All production code/docs and the relevant tests match their restored copies. No pm SDK mocks are used in the new matrix."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-11T23:48:50.052Z"}],"before_hash":"f79c2ee2a18698ba4a96ac0639e396dbcf283a08e3e4d7e006ac8af4a98318e8","after_hash":"0fccc96bcdc7a4023772e1237580ee45c129e95b35e33e9961b13fb369a8ba49","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":"570ede6682c7402d0076352e5206fb1621fdfe5e17cfdd881f342798d48f1c36"} +{"hash_algorithm":"sha256","ts":"2026-11-11T23:48:51.495Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-11T23:48:51.488Z","author":"codex-github230-1111","text":"Round 6 final bounded gate receipt: 54 selected test files / 57 initial invocations, corrected with linked/full or targeted fixture rechecks; 53 completed files / 56 complete invocations contain 1220 tests: 1218 pass, zero remaining failures and two existing pm-cli#1656 skips. The importer gate passes all 73 cases in four disjoint partitions. closeout-workflow reached its 20-minute cap after 74 passes and one cancellation without an assertion failure; CI must certify that file and the full npm/Bun release/coverage gate. Both 500-case label models pass, as does their corrected shared-conversion case. Final linked pm runs of node --test --test-concurrency=1 test/import-ownership.test.ts and bun test --timeout 60000 test/import-ownership.test.ts each pass 26/26 with valid SDK mutation histories. Finalization, refresh, idempotency, SDK hooks, literal-field and packed consumer rechecks pass. Existing command-failure fixtures now seed real owners; deleted targets refuse at the SDK tombstone read before mutation and retain their history. Build, test compilation, lint, typecheck, docstrings (32 files / 287 declarations), zero duplication (33 authored modules) and strict tracker health pass with two existing advisories. No dependency or SDK pin changed; no new SDK gap or quarantine was introduced. Project policy disables automatic test-result records, so outcomes are recorded in comments. Current review receipt: one supplied blocking finding; 21 threads / 21 comments and 10 reviews enumerated without remaining pages. The curated-reopen thread PRRT_kwDOTbN4pM62Q98t and the latest CodeRabbit body were read in full. Its completion-date reproduction is recorded on the deferred item without changing behavior. The two outside-diff read-path findings are tracked separately. CI evidence remains at https://github.com/unbraind/pm-github/pull/233/checks. Releasing this author claim; the main item remains in progress and follow-ups stay open or in progress."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-11T23:48:51.495Z"}],"before_hash":"0fccc96bcdc7a4023772e1237580ee45c129e95b35e33e9961b13fb369a8ba49","after_hash":"6abcc515b311898db45313ed913259313060e986727814b01d0905cfd8009620","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":"d7c3a92bc5dfed21de97a54fa1dbf4a0a0553fba66fe66e22009dbed072f9a2d"} +{"hash_algorithm":"sha256","ts":"2026-11-11T23:48:52.667Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-11T23:48:52.667Z"}],"before_hash":"6abcc515b311898db45313ed913259313060e986727814b01d0905cfd8009620","after_hash":"65bab560c2f11c67be8d5e0a50db2ca5370d8f0004a86f0c2f93e881e90371d4","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":"96cf7e8ae7039334bcd4568900371290f21917f0888dbddc5c69175a5cae71b0"} +{"hash_algorithm":"sha256","ts":"2026-11-11T23:54:20.849Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-11T23:54:20.849Z"},{"op":"add","path":"/metadata/assignee","value":"codex-github230-1111"},{"op":"add","path":"/metadata/claim_principal","value":"codex-github230-1111"}],"before_hash":"65bab560c2f11c67be8d5e0a50db2ca5370d8f0004a86f0c2f93e881e90371d4","after_hash":"1ade4c38af11321fa8d838e98d112d3ea087b08ac72f5b76fe6be3ac84f75cd7","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":"bd4d1aacecd990dfd5f0de4c2cfe3f9c4d5f4ba63cde786d6a24d5441fddbb31"} +{"hash_algorithm":"sha256","ts":"2026-11-11T23:54:22.056Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-11T23:54:22.048Z","author":"codex-github230-1111","text":"Final fixture tightening: the dependency-race regression now delegates listing to the real SDK and uses the real default ordering-cycle collector. The exact command node --test --test-concurrency=1 --test-name-pattern \"^Ownership dependency writes\" test/import-ownership.test.ts passes 1/1. In a disposable package copy, disabling only assertGithubImportOwner makes that same case fail 1/1; restoring it passes 1/1. Final test compilation and lint pass. This strengthens the existing 22-case behavioral revert proof without changing production code, test counts or the gate receipt. All tracker histories remain byte-prefix extensions of the accepted base and origin/main. Releasing only this author claim before commit; no item is closed."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-11T23:54:22.056Z"}],"before_hash":"1ade4c38af11321fa8d838e98d112d3ea087b08ac72f5b76fe6be3ac84f75cd7","after_hash":"e9b08c6db3cac6be92c371948fff4aff1bac189c66ffba108bdbf52ad5fe8b2f","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":"acdaa7c0911918473cee5a85a45460ffa20537efb39fb5bcd0c93c6f4c47cbe1"} +{"hash_algorithm":"sha256","ts":"2026-11-11T23:54:22.939Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-11T23:54:22.939Z"}],"before_hash":"e9b08c6db3cac6be92c371948fff4aff1bac189c66ffba108bdbf52ad5fe8b2f","after_hash":"835b667a1b8fc1809eb26fb4e9126509d142f19261ef44048d96d57a06bdbf89","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":"3463885636e0e7834b8734a898131983bea1b66a653d5fcad5319037c7b7bbb2"} +{"hash_algorithm":"sha256","ts":"2026-11-12T01:29:04.952Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-12T01:29:04.952Z"},{"op":"add","path":"/metadata/assignee","value":"codex-github230-1111"},{"op":"add","path":"/metadata/claim_principal","value":"codex-github230-1111"}],"before_hash":"835b667a1b8fc1809eb26fb4e9126509d142f19261ef44048d96d57a06bdbf89","after_hash":"9f133d4fb9872be148f7b24a84f6ed1fa13608acc00539988d3e2a79bb79e61c","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":"0147daa132830a04a86c88bb9cd54ed713e0ac3a04e2198324c06a6ece542b86"} +{"hash_algorithm":"sha256","ts":"2026-11-12T01:29:06.716Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-12T01:29:06.709Z","author":"codex-github230-1111","text":"Round 7 starts from accepted ownership head eeee95d. Merged current origin/main before new work. Read the supplied atomic dry-run review and all eight requested full thread bodies, AI-agent prompts and diffs. Plan: use a real initialized tracker for apply-equivalent dry-run ownership checks; unify atomic annotation accounting and exactly-once synchronization; reject ambiguous dependency owners; make durable file flushing Windows-compatible; reject backslash URL paths; align fresh/replayed privacy preflight; isolate closeout identity conflicts to eligible item rows. Preserve accepted ownership and all earlier regressions. Every fix gets behavioral revert evidence in disposable copies, real SDK controls and bounded affected-file gates. No review threads will be answered or resolved."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T01:29:06.716Z"}],"before_hash":"9f133d4fb9872be148f7b24a84f6ed1fa13608acc00539988d3e2a79bb79e61c","after_hash":"b35f5d95ab4b1abdc4646b94600ed27981f788c36b7066f463ebe19a6d457f24","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":"3cac41b3b23b39350bfc6c9611458f2074d1bd66949d3bbd23f3c0d4121bf961"} +{"hash_algorithm":"sha256","ts":"2026-11-12T01:42:21.438Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/46/path","value":"test/workspace-resolution.test.ts"},{"op":"replace","path":"/metadata/files/45/path","value":"test/webhooks.test.ts"},{"op":"replace","path":"/metadata/files/44/path","value":"test/typed-sentinel-normalization.test.ts"},{"op":"replace","path":"/metadata/files/43/path","value":"test/smoke.test.ts"},{"op":"replace","path":"/metadata/files/42/path","value":"test/sdk-adoption.test.ts"},{"op":"replace","path":"/metadata/files/41/path","value":"test/scheduled-refusals.test.ts"},{"op":"replace","path":"/metadata/files/40/path","value":"test/review-round3-identity.test.ts"},{"op":"replace","path":"/metadata/files/39/path","value":"test/review-import-identity.test.ts"},{"op":"replace","path":"/metadata/files/38/path","value":"test/repeat-import-noop.test.ts"},{"op":"replace","path":"/metadata/files/37/path","value":"test/refresh.test.ts"},{"op":"replace","path":"/metadata/files/36/path","value":"test/reference-import-identity.test.ts"},{"op":"replace","path":"/metadata/files/35/path","value":"test/link-deps.test.ts"},{"op":"replace","path":"/metadata/files/34/path","value":"test/label-import-model.test.ts"},{"op":"replace","path":"/metadata/files/33/path","value":"test/import-projection.test.ts"},{"op":"replace","path":"/metadata/files/32/path","value":"test/import-ownership.test.ts"},{"op":"replace","path":"/metadata/files/31/path","value":"test/import-lock.test.ts"},{"op":"replace","path":"/metadata/files/30/path","value":"test/import-gate.test.ts"},{"op":"replace","path":"/metadata/files/29/path","value":"test/import-fidelity.test.ts"},{"op":"replace","path":"/metadata/files/28/path","value":"test/import-errors.test.ts"},{"op":"replace","path":"/metadata/files/27/path","value":"test/import-conflict-report.test.ts"},{"op":"replace","path":"/metadata/files/26/path","value":"test/identity.test.ts"},{"op":"replace","path":"/metadata/files/25/path","value":"test/helpers/import-preview.ts"},{"op":"add","path":"/metadata/files/47","value":{"path":"webhooks.ts","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T01:42:21.438Z"}],"before_hash":"b35f5d95ab4b1abdc4646b94600ed27981f788c36b7066f463ebe19a6d457f24","after_hash":"57bd7703d8a194f55d45857a8b6a7a0c9de056e1bcb62a4078e1a30e97dcdf3f","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":"fcbd93179734e3cf8b41ddd0c5ae89d6348f3442c2fdb5056a071b1437067c9b"} +{"hash_algorithm":"sha256","ts":"2026-11-12T01:42:23.134Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/47/path","value":"test/workspace-resolution.test.ts"},{"op":"replace","path":"/metadata/files/46/path","value":"test/webhooks.test.ts"},{"op":"replace","path":"/metadata/files/45/path","value":"test/typed-sentinel-normalization.test.ts"},{"op":"replace","path":"/metadata/files/44/path","value":"test/smoke.test.ts"},{"op":"replace","path":"/metadata/files/43/path","value":"test/sdk-adoption.test.ts"},{"op":"replace","path":"/metadata/files/42/path","value":"test/scheduled-refusals.test.ts"},{"op":"replace","path":"/metadata/files/41/path","value":"test/round-seven.test.ts"},{"op":"add","path":"/metadata/files/48","value":{"path":"webhooks.ts","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T01:42:23.134Z"}],"before_hash":"57bd7703d8a194f55d45857a8b6a7a0c9de056e1bcb62a4078e1a30e97dcdf3f","after_hash":"1363e33f47a1d4ee5b996c729ea1a049e9491e43defa4a066107607bb46a454b","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":"ac35635ee6cd619701311c99e759175a5d40501b91ca3de6e35a8d51dc5aac97"} +{"hash_algorithm":"sha256","ts":"2026-11-12T01:42:24.640Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/48/path","value":"test/workspace-resolution.test.ts"},{"op":"replace","path":"/metadata/files/47/path","value":"test/webhooks.test.ts"},{"op":"replace","path":"/metadata/files/46/path","value":"test/typed-sentinel-normalization.test.ts"},{"op":"replace","path":"/metadata/files/45/path","value":"test/smoke.test.ts"},{"op":"replace","path":"/metadata/files/44/path","value":"test/sdk-adoption.test.ts"},{"op":"replace","path":"/metadata/files/43/path","value":"test/scheduled-refusals.test.ts"},{"op":"replace","path":"/metadata/files/42/path","value":"test/round-seven.test.ts"},{"op":"replace","path":"/metadata/files/41/path","value":"test/review-round3-identity.test.ts"},{"op":"replace","path":"/metadata/files/40/path","value":"test/review-import-identity.test.ts"},{"op":"replace","path":"/metadata/files/39/path","value":"test/repeat-import-noop.test.ts"},{"op":"replace","path":"/metadata/files/38/path","value":"test/refresh.test.ts"},{"op":"replace","path":"/metadata/files/37/path","value":"test/reference-import-identity.test.ts"},{"op":"replace","path":"/metadata/files/36/path","value":"test/link-deps.test.ts"},{"op":"replace","path":"/metadata/files/35/path","value":"test/label-import-model.test.ts"},{"op":"replace","path":"/metadata/files/34/path","value":"test/import-projection.test.ts"},{"op":"replace","path":"/metadata/files/33/path","value":"test/import-ownership.test.ts"},{"op":"replace","path":"/metadata/files/32/path","value":"test/import-lock.test.ts"},{"op":"replace","path":"/metadata/files/31/path","value":"test/import-gate.test.ts"},{"op":"replace","path":"/metadata/files/30/path","value":"test/import-fidelity.test.ts"},{"op":"replace","path":"/metadata/files/29/path","value":"test/import-errors.test.ts"},{"op":"replace","path":"/metadata/files/28/path","value":"test/import-conflict-report.test.ts"},{"op":"replace","path":"/metadata/files/27/path","value":"test/identity.test.ts"},{"op":"replace","path":"/metadata/files/26/path","value":"test/helpers/import-preview.ts"},{"op":"replace","path":"/metadata/files/25/path","value":"test/helpers/import-mirror.ts"},{"op":"replace","path":"/metadata/files/24/path","value":"test/handler-failures.test.ts"},{"op":"replace","path":"/metadata/files/23/path","value":"test/github-url.test.ts"},{"op":"replace","path":"/metadata/files/22/path","value":"test/fixtures/refresh-crash.ts"},{"op":"replace","path":"/metadata/files/21/path","value":"test/fixtures/ownership-peer.ts"},{"op":"replace","path":"/metadata/files/20/path","value":"test/fixtures/atomic-plan-crash.ts"},{"op":"replace","path":"/metadata/files/19/path","value":"test/durable-state.test.ts"},{"op":"add","path":"/metadata/files/49","value":{"path":"webhooks.ts","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T01:42:24.640Z"}],"before_hash":"1363e33f47a1d4ee5b996c729ea1a049e9491e43defa4a066107607bb46a454b","after_hash":"bab2ed2da1868f9b04b7e5c4b3a786241167f7d54f7f31c6dad847cf6503a7ac","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":"7e0fef0c522f07fe036570d359d888c7b39d38c567c984d640282fc9c7bf9eae"} +{"hash_algorithm":"sha256","ts":"2026-11-12T01:42:31.744Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/70","value":{"command":"node --test --test-concurrency=1 test/round-seven.test.ts","scope":"project","timeout_seconds":1200,"provenance":{"author":"codex-github230-1111","created_at":"2026-11-12T01:42:31.586Z","source_kind":"local_mutation","source_ref":"fix/reference-tags-are-not-import-identity"}}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T01:42:31.744Z"}],"before_hash":"bab2ed2da1868f9b04b7e5c4b3a786241167f7d54f7f31c6dad847cf6503a7ac","after_hash":"7322710b2cd2a03bfa79b6377c99927f3bb52561aba055a769651051b25d9f0d","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":"a1f2a5d26d15589c74c4137a807d8bed2d5bf9484259277d0a872dacc3b9b777"} +{"hash_algorithm":"sha256","ts":"2026-11-12T01:42:33.636Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/71","value":{"command":"bun test --timeout 60000 test/round-seven.test.ts","scope":"project","timeout_seconds":1200,"provenance":{"author":"codex-github230-1111","created_at":"2026-11-12T01:42:33.411Z","source_kind":"local_mutation","source_ref":"fix/reference-tags-are-not-import-identity"}}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T01:42:33.636Z"}],"before_hash":"7322710b2cd2a03bfa79b6377c99927f3bb52561aba055a769651051b25d9f0d","after_hash":"7a7d79c7149078fc74a8ec084864cb49dec680e94817a679a83949ad2fe95ca7","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":"46d0071ef7e845a21dc57348b92123acb02dd7823d0846b3e75dd35c1eb8bb9b"} +{"hash_algorithm":"sha256","ts":"2026-11-12T01:42:35.576Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/72","value":{"command":"node --test --test-concurrency=1 test/durable-state.test.ts","scope":"project","timeout_seconds":1200,"provenance":{"author":"codex-github230-1111","created_at":"2026-11-12T01:42:35.397Z","source_kind":"local_mutation","source_ref":"fix/reference-tags-are-not-import-identity"}}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T01:42:35.576Z"}],"before_hash":"7a7d79c7149078fc74a8ec084864cb49dec680e94817a679a83949ad2fe95ca7","after_hash":"b9e44b46f972ba94c2740d0208942fc922f4f820708d7c4477f8c30a33df38db","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":"48c67eb90b1a5f1b4351f355ab38bbf900758b4d050ef5b412a342f7f93fceb6"} +{"hash_algorithm":"sha256","ts":"2026-11-12T01:42:41.640Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-12T01:42:41.626Z","author":"codex-github230-1111","text":"Round 7 decisions and implementation: dry-run needs an initialized real tracker so preview checks the same destination ownership as apply; atomic and plain preview fixtures now seed native SDK owners, remove fabricated pm reads/commit APIs, and assert item/history bytes and transaction-plan absence. Committed atomic imports return commentWriteFailures and annotationIssueNumbers, remove the plan despite best-effort comment refusals and let gated callers refuse incomplete side data through the shared divergence check. The outer loop handles only entries not already synchronized. Dependency indexing retains only unique explicit owners, excludes contradictory claims, and reports ambiguous_github_dependency_owner with all claimant IDs without linking. Both fresh gated and ungated atomic plans require passing privacy preflight before publication; legacy failed/missing/forged-plan regressions now seed a real interrupted clean journal before constructing legacy recovery inputs. GitHub path parsing rejects raw/encoded backslashes before WHATWG normalization while allowing query/fragment text. File fsync uses writable r+ handles; directory fsync is explicitly skipped on win32. The package-wide flush search found a second instance in webhook receipt recovery; it now uses the same helper. Tests stub only platform selection and keep filesystem operations real. Closeout catches identity and ownership conflicts as per-item outcomes and does not check explicitly skipped, context-only or foreign references; unrelated valid deliveries still close. Existing curated-reopen behavior remains deferred and no review thread was answered or resolved."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T01:42:41.640Z"}],"before_hash":"b9e44b46f972ba94c2740d0208942fc922f4f820708d7c4477f8c30a33df38db","after_hash":"54cdff90b9c43a653347c0cc7d58bea576e5b47f091acbeef80f5828be4cdcb1","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":"53291e6c17ec919a875f591d27a9831955832bb062f0be5c3bcb34aab68550e8"} +{"hash_algorithm":"sha256","ts":"2026-11-12T01:42:43.653Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-12T01:42:43.636Z","author":"codex-github230-1111","text":"Round 7 behavioral revert receipt, disposable copies only: reverting commitGithubImportTransactionUnchecked causes 2 atomic annotation-failure regressions to fail while 2 plain controls pass; removing the outer annotation receipt skip causes 2 changed/mixed exactly-once failures while the unchanged control passes. Reverting buildProvenanceIndexFromMetadata causes both ambiguous source/target regressions to fail while the unique-owner control passes. Restoring the previous read-only file-sync policy causes all 3 platform regressions to fail; restoring unconditional directory sync causes the win32 case to fail and POSIX controls to pass. Reverting parseGithubUrl causes 3 backslash-normalization regressions to fail, with 2 previously rejected encodings plus must-allow query/trailing-slash controls retained. Reinstating gated-only fresh preflight causes the ungated regression to fail while the gated control passes. Reverting closeoutAttempt causes all 4 eligible/skipped/context/foreign scenarios to abort instead of closing the unrelated valid delivery. Restoring the original atomic dry-run fixture reproduces tracker_root_missing (1 failure); fixed fixtures use native SDK init/create/mutateItem. Restored focused tests pass 21/21. Commands use node --test --test-concurrency=1 with the Round7, durable-state, backslash and atomic dry-run name patterns; source/package production files were never reverted in the real worktree. Whole atomic.test.ts passed 11/11; broad bounded dependent sweep is underway with a maximum of 2 workers."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T01:42:43.653Z"}],"before_hash":"54cdff90b9c43a653347c0cc7d58bea576e5b47f091acbeef80f5828be4cdcb1","after_hash":"053a431462b8a1338dd8cb55c4f082fa90b6dd555902292f53911883f2b7d529","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":"95d4a11bf8243752fdd20825986a5ff61f35bde2a0db9da43166f4baa3bb44a7"} +{"hash_algorithm":"sha256","ts":"2026-11-12T02:01:16.040Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-12T02:01:16.029Z","author":"codex-github230-1111","text":"Read the newest nonempty CodeRabbit review body in full (20 reviews enumerated, 34 threads / 42 comments, no further thread pages). The 10 additional observations beyond covered explicit-closeout and dry-run cases are consolidated, pending validation, in https://github.com/unbraind/pm-github/issues/236 and pm-github-reference-identity-followup, following the instruction to use one follow-up instead of round 8. All 10 full comment bodies and AI-agent prompts were read. The dependency-index half of auB7 is already addressed; its blocker self-exclusion half stays in that follow-up. The review request to remove earlier accepted scope and mandatory tracker files is not applied: prior rounds are accepted and tracker history must remain append-only. No review thread was replied to or resolved. Three older annotation fixtures in coverage-remainder relied on fabricated pm snapshots/commit results and stopped at provenance validation; they now use real stored owners and a native SDK policy refusal, with explicit annotation-failure counts and committed-plan cleanup assertions. Final regression rechecks will cover those fixtures, direct atomic APIs, replay, real HTTP dry runs and all closeout eligibility exclusions."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T02:01:16.040Z"}],"before_hash":"053a431462b8a1338dd8cb55c4f082fa90b6dd555902292f53911883f2b7d529","after_hash":"d5f9024cc87d8456ca54a24dd47be41a1669529661148f8a618205430b855799","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":"9d52e7c572c2379e54c64713af92471140809929f67254d79165c1e44e211e88"} +{"hash_algorithm":"sha256","ts":"2026-11-12T02:50:42.103Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-12T02:50:42.074Z","author":"codex-github230-1111","text":"Round 7 final self-review and test receipt: all 73 import-gate cases pass in four disjoint name partitions. The 57-file affected/required sweep has 60 bounded invocations; 56 completed files / 59 completed invocations contain 1290 tests: 1288 pass, zero remaining failures, two existing pm-cli#1656 skips. closeout-workflow reaches its 20-minute cap after 42 passes and one cancellation without an assertion failure; CI certifies that file and the full npm/Bun release/coverage gate. Final whole-file rechecks pass atomic 11/11, ownership 26/26, closeout 93/93, corrected coverage-remainder 25/25, and real HTTP dry-run preview 2/2. Linked pm runs of node --test --test-concurrency=1 test/round-seven.test.ts and bun test --timeout 60000 test/round-seven.test.ts pass 23/23 each; node --test --test-concurrency=1 test/durable-state.test.ts passes 3/3. Project policy disables automatic run artifacts, so results remain in comments. Bun exposed event coalescing in a filesystem-watcher assertion; the test now observes actual native SDK mutations, advancing only its incoming fixture cursor so an unwanted second pass would append an extra annotation. No pm API or filesystem is mocked. Unchanged and mixed entries have explicit controls. Extra tests cover committed-plan replay, direct gated atomic APIs, contradictory dependency identities, shared reference-only mentions, and all closeout exclusions including terminal items and epics. Production code was frozen before these final rechecks; additional observations are consolidated in issue #236."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T02:50:42.103Z"}],"before_hash":"d5f9024cc87d8456ca54a24dd47be41a1669529661148f8a618205430b855799","after_hash":"7f95e2078d323394630e9bf0814caab224b66a906be2401d3330c391beb50873","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":"01dd79f97b8f3dc5268ff7413cd7b9c45462812dab476784337314a857ad7264"} +{"hash_algorithm":"sha256","ts":"2026-11-12T02:50:43.898Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-12T02:50:43.885Z","author":"codex-github230-1111","text":"Round 7 final behavioral revert evidence in disposable copies: reverting commitGithubImportTransactionUnchecked produces six atomic annotation/refusal failures with two plain controls passing; removing only the outer receipt skip produces two exactly-once failures with the unchanged control passing. Reverting the dependency index produces three failures (duplicate source, duplicate target, contradictory identity), while the unique owner plus reference-only mentions passes. Restoring read-only file flushing produces three platform failures; restoring unconditional directory flushing produces one win32 failure with POSIX controls passing. Reverting the URL parser produces three backslash normalization failures and retains two existing must-refuse controls. Restoring gated-only fresh privacy preflight produces one ungated failure while the gated control passes. Reverting closeoutAttempt produces six failures across eligible, explicit-skip, context, foreign, terminal and epic references, each with an unrelated valid delivery. Restoring the old atomic dry-run fixture produces tracker_root_missing (one failure). Restored focused tests pass 28/28. After replacing filesystem event counting, repeated the exactly-once revert against the final fixture: both changed/mixed cases fail and the unchanged control passes; restored focused tests again pass 28/28. Source commands are node --test --test-concurrency=1 --test-name-pattern PATTERN FILE with ^Round7 annotation refusal, ^Round7 atomic annotations, ^Round7 dependency, ^Round7 failed atomic preflight, ^Round7 closeout, ^GitHub ownership rejects backslash, ^atomic dry-run, plus test/durable-state.test.ts. No tracked implementation was reverted or native audit history rewritten. Main and base histories remain append-only. CI handoff: https://github.com/unbraind/pm-github/pull/233/checks . Releasing only this author claim; the main item stays in progress and the consolidated follow-up remains open and unclaimed."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T02:50:43.898Z"}],"before_hash":"7f95e2078d323394630e9bf0814caab224b66a906be2401d3330c391beb50873","after_hash":"9a1580c3ace3189a73d28744a6b9ec91bdf2c4e73cb9611f59b814511d50f939","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":"7e2e7d1e4a256570294b8fdb347aa1c1c725811d78aab671fa0dfbb31b93d955"} +{"hash_algorithm":"sha256","ts":"2026-11-12T02:50:45.087Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-12T02:50:45.087Z"}],"before_hash":"9a1580c3ace3189a73d28744a6b9ec91bdf2c4e73cb9611f59b814511d50f939","after_hash":"3a852bcd74e02878816653c5fc7f93d5951724ef672bb4cac28026684ac95ee8","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":"5db9bf55566ad5b6e4618d054da5645fd124f052f51ae84b042c16f9030b3b58"} +{"hash_algorithm":"sha256","ts":"2026-11-12T06:02:13.761Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-12T06:02:13.761Z"},{"op":"add","path":"/metadata/assignee","value":"codex-github230-1111"},{"op":"add","path":"/metadata/claim_principal","value":"codex-github230-1111"}],"before_hash":"3a852bcd74e02878816653c5fc7f93d5951724ef672bb4cac28026684ac95ee8","after_hash":"f7757abe5a2c902400646cdcd6eaa609dae1508721f1f658bc12a81fc3c711f8","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":"3dd9c7c7cc3e17847c5672383aee7679320e37c55c1837bcc2818d98902bbfa5"} +{"hash_algorithm":"sha256","ts":"2026-11-12T06:13:06.012Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-12T06:13:06.006Z","author":"codex-github230-1111","text":"Round 8 starts from accepted head f252e3e; merged current main before implementation. Read the four blocking threads, the added ownership-preflight cleanup thread and outside-diff deletion-durability comment in review 5868419634 in full, including agent prompts. Scope is only shared ownership consistency/uniqueness at locked writes plus safe plan cleanup and the existing directory-flush policy. Other findings remain in issue #236 and no review thread will be answered or resolved. Asked for clarification of tag-only references versus the new claim rule; pending an answer, preserve the original requirement that reference-only items are not owners, while validating all claims on provenance-bearing items. Add one real SDK matrix across matching, closeout and replay, plus native concurrent-peer and cleanup/journal controls, then bounded affected-file gates and behavioral reverts."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T06:13:06.012Z"}],"before_hash":"f7757abe5a2c902400646cdcd6eaa609dae1508721f1f658bc12a81fc3c711f8","after_hash":"16a2dd78ca5456b72114cfb4ecab4d97d4f112b1fd78c5669ecd8892e3fcfb90","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":"e19586780ebfd10ea6bab0e987d8fdfdca1a151244d77569eb2cd800cbffa29e"} +{"hash_algorithm":"sha256","ts":"2026-11-12T06:49:20.109Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-12T06:49:20.097Z","author":"codex-github230-1111","text":"Round 8: merged current main and read all five scoped review threads, including agent prompts and proposed changes, plus review 5868419634 outside-index-1745. Shared normalized claims now include external_ref, supported legacy link fields, explicit URL aliases, partial repository/number provenance and tags on provenance-bearing records. Reference-only tags remain non-owning, preserving the original import contract. Every native update checks current agreement and competing claims under the SDK lock; replay also checks persisted provenance before gated success. The 77-case real-tracker matrix passes, including 14 contradiction/ambiguity shapes across match, plain import, atomic import, closeout and gated replay, consistent legacy/full controls, late peer claims, no-journal cleanup, journal retention and native directory-flush failure. Scoped gates are running with two total workers and per-command 20-minute limits."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T06:49:20.109Z"}],"before_hash":"16a2dd78ca5456b72114cfb4ecab4d97d4f112b1fd78c5669ecd8892e3fcfb90","after_hash":"6d0c966d0a5ad6b622dadb46dec6d10b9008080fb963498c309ac8ddf31467e3","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":"1f87bbbaa05675e491e9c7dc94e4ad5b979fd954c56be5e13108b719f799739d"} +{"hash_algorithm":"sha256","ts":"2026-11-12T06:51:24.936Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/49/path","value":"test/workspace-resolution.test.ts"},{"op":"replace","path":"/metadata/files/48/path","value":"test/webhooks.test.ts"},{"op":"replace","path":"/metadata/files/47/path","value":"test/typed-sentinel-normalization.test.ts"},{"op":"replace","path":"/metadata/files/46/path","value":"test/smoke.test.ts"},{"op":"replace","path":"/metadata/files/45/path","value":"test/sdk-adoption.test.ts"},{"op":"replace","path":"/metadata/files/44/path","value":"test/scheduled-refusals.test.ts"},{"op":"add","path":"/metadata/files/43/note","value":"Unified typed ownership refusal"},{"op":"replace","path":"/metadata/files/43/path","value":"test/round-seven.test.ts"},{"op":"replace","path":"/metadata/files/42/path","value":"test/review-round3-identity.test.ts"},{"op":"replace","path":"/metadata/files/41/path","value":"test/review-import-identity.test.ts"},{"op":"replace","path":"/metadata/files/40/path","value":"test/repeat-import-noop.test.ts"},{"op":"replace","path":"/metadata/files/39/path","value":"test/refresh.test.ts"},{"op":"replace","path":"/metadata/files/38/path","value":"test/reference-import-identity.test.ts"},{"op":"add","path":"/metadata/files/37/note","value":"Round 8 native ownership matrix"},{"op":"replace","path":"/metadata/files/37/path","value":"test/ownership-consistency.test.ts"},{"op":"add","path":"/metadata/files/33/note","value":"Native SDK revision and independent peer fixtures"},{"op":"add","path":"/metadata/files/9/note","value":"Replay provenance and journal-aware durable cleanup"},{"op":"add","path":"/metadata/files/7/note","value":"Current claims checked under SDK item locks"},{"op":"add","path":"/metadata/files/6/note","value":"Consistent partial legacy and legacy links"},{"op":"add","path":"/metadata/files/5/note","value":"Reuse normalized GitHub claim helper"},{"op":"add","path":"/metadata/files/4/note","value":"Shared external reference normalization"},{"op":"add","path":"/metadata/files/1/note","value":"Per-item ambiguity and duplicate-owner refusal"},{"op":"add","path":"/metadata/files/50","value":{"path":"webhooks.ts","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T06:51:24.936Z"}],"before_hash":"6d0c966d0a5ad6b622dadb46dec6d10b9008080fb963498c309ac8ddf31467e3","after_hash":"2a04a952746a3696d3fb9e6c689db2a344fce82acae4409e9cdccc1e838ea9eb","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":"4ab94f8e33114e42b4b3ddc27d227d1d2043cc0030ddba18ed960be98dd08cde"} +{"hash_algorithm":"sha256","ts":"2026-11-12T06:51:26.723Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/73","value":{"command":"timeout 1200 node --test --test-concurrency=1 test/ownership-consistency.test.ts","scope":"project","provenance":{"author":"codex-github230-1111","created_at":"2026-11-12T06:51:26.645Z","source_kind":"local_mutation","source_ref":"fix/reference-tags-are-not-import-identity"}}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T06:51:26.723Z"}],"before_hash":"2a04a952746a3696d3fb9e6c689db2a344fce82acae4409e9cdccc1e838ea9eb","after_hash":"b3c2303d6ea0bccf12e5528586fea3aa300e5e643f0eb5152b3a565f72677375","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":"41ac9c61c3ecde86c553436734ab9fbb833875c9d7ac331eca6968c992291c54"} +{"hash_algorithm":"sha256","ts":"2026-11-12T06:51:28.122Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"docs_add","patch":[{"op":"add","path":"/metadata/docs/3/note","value":"All owner claims must agree and remain unique; journal-aware cleanup"},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T06:51:28.122Z"}],"before_hash":"b3c2303d6ea0bccf12e5528586fea3aa300e5e643f0eb5152b3a565f72677375","after_hash":"194f2c9b0aa22fdbdd8c5162539b4e772bcdbb98d3b1c9392fc66a5e92025eea","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":"fda1b3c11a0fdeafddeb6b7efb3d84909714de6feda7aba6204617ec4fb7bfcd"} +{"hash_algorithm":"sha256","ts":"2026-11-12T07:09:18.039Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/42/note","value":"Contradictory legacy claims refuse without duplicate import"},{"op":"add","path":"/metadata/files/41/note","value":"Unified typed write refusal with readable legacy diagnostics"},{"op":"add","path":"/metadata/files/15/note","value":"Native SDK policy refusal fixtures for guarded plain writes"},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T07:09:18.039Z"}],"before_hash":"194f2c9b0aa22fdbdd8c5162539b4e772bcdbb98d3b1c9392fc66a5e92025eea","after_hash":"fa422d35e89432e9384fb5a170fa3ac51e6aeaea6892640ca2c3eddf5c346a18","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":"74c5a38ebc7ac62561eafdded3eb31dd3a56c9935aab9c9bb3d7bfc2027379a1"} +{"hash_algorithm":"sha256","ts":"2026-11-12T07:20:14.806Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-12T07:20:14.799Z","author":"codex-github230-1111","text":"Round 8 compatibility note: the stricter claim rule intentionally changes earlier permissive cases where a provenance-bearing item referenced a different issue or repository. Those records now refuse with github_import_owner_conflict, keep their native item/history bytes and do not create a competing import. Reference-only records remain shareable. Updated the earlier round-two/three and reference regression expectations to match this extended contract. Plain creates and all owned mutations now use guarded native SDK writes; command-failure fixtures use real SDK policy refusals instead of child-CLI interception. The aggregate closeout workflow command reached its 20-minute cap after 49 passing cases and one canceled file wrapper; CI must certify its remaining workflow matrix."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T07:20:14.806Z"}],"before_hash":"fa422d35e89432e9384fb5a170fa3ac51e6aeaea6892640ca2c3eddf5c346a18","after_hash":"960ca053e09274e323734be99d2153af854f4b5abdc20c75be0f646a5218690c","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":"c02406de456e5dc98afb8cc66f9aaad5b026d38f325f1935e76495e996a90097"} +{"hash_algorithm":"sha256","ts":"2026-11-12T07:43:51.700Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-12T07:43:51.700Z"},{"op":"remove","path":"/metadata/tags/1"}],"before_hash":"960ca053e09274e323734be99d2153af854f4b5abdc20c75be0f646a5218690c","after_hash":"2af3d00bfd9b4e00626c50e7f7aad3204d42f8ac3bc3f960596fd31490ab4fbd","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":"c4595129814f0483d7fd0579585c167c749d9e07bf204bb0e26dd25794c70430"} +{"hash_algorithm":"sha256","ts":"2026-11-12T07:43:52.583Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"docs_add","patch":[{"op":"remove","path":"/metadata/docs/3/note"},{"op":"replace","path":"/metadata/docs/3/path","value":"https://github.com/unbraind/pm-github/pull/233/checks"},{"op":"add","path":"/metadata/docs/2/note","value":"Implementation and scoped review evidence"},{"op":"replace","path":"/metadata/docs/2/path","value":"https://github.com/unbraind/pm-github/pull/233"},{"op":"add","path":"/metadata/docs/4","value":{"path":"README.md","scope":"project","note":"All owner claims must agree and remain unique; journal-aware cleanup"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T07:43:52.583Z"}],"before_hash":"2af3d00bfd9b4e00626c50e7f7aad3204d42f8ac3bc3f960596fd31490ab4fbd","after_hash":"36bd764c5d5ab72d0c10ff2c1aed43f60d4d41f545e67fc113566eb0502825c5","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":"c8e930617813801f57e8d87c5d87df8c09b11dbb2e34bd54f30eb81bc7f1ade1"} +{"hash_algorithm":"sha256","ts":"2026-11-12T07:43:53.593Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-12T07:43:53.586Z","author":"codex-github230-1111","text":"Round 8 revert proof, using disposable source copies and real SDK trackers: node --test --test-concurrency=1 --test-name-pattern \"^Ownership matrix (?:match|plain|atomic|closeout|replay): refuses\" test/ownership-consistency.test.ts fails 70/70 when the changed ownership functions are reverted, including matchImportedIssue and the closeout check. Reverting only uniqueness in assertGithubOwnershipConsistency makes all four \"^Ownership matrix locked\" cases fail. Reverting commitGithubImportTransaction plan cleanup makes \"^Ownership preflight removes\" fail 1/1. Reverting removeGithubImportPlan directory flushing makes \"^Recovery-plan deletion flushes\" fail 1/1. Restoring the implementation passes 82/82. The same 82-case matrix passed through pm test --run --only-index 74 --progress --timeout 1200; native policy-failure coverage passes 25/25. Removed the auxiliary PR tag from this issue-owned item so every identity claim agrees on issue 230; PR 233 remains a docs reference. Item/history and native state are never repaired or rewritten."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T07:43:53.593Z"}],"before_hash":"36bd764c5d5ab72d0c10ff2c1aed43f60d4d41f545e67fc113566eb0502825c5","after_hash":"5f23eb68de3f850cafb1489ed64628512db56c7e4977dfb6b993466c0b7145ec","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":"32f08fb1f788f6f17b0d4fa75e6abd7084d0b2fac90319060145942a3c88bf97"} +{"hash_algorithm":"sha256","ts":"2026-11-12T07:43:54.562Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/74","value":{"command":"timeout 1200 bun test --timeout 60000 test/ownership-consistency.test.ts","scope":"project","timeout_seconds":1200,"provenance":{"author":"codex-github230-1111","created_at":"2026-11-12T07:43:54.484Z","source_kind":"local_mutation","source_ref":"fix/reference-tags-are-not-import-identity"}}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T07:43:54.562Z"}],"before_hash":"5f23eb68de3f850cafb1489ed64628512db56c7e4977dfb6b993466c0b7145ec","after_hash":"e7f7a1338218e41421b932c4629cffcf0ab78cbe859da33dc9f608b1ee349903","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":"1ce1d827404e6958ea0597ea391e9f3df88b427b853c4ebb42ec1f983f65c016"} +{"hash_algorithm":"sha256","ts":"2026-11-12T07:47:09.912Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/38/note","value":"Contradictory linked references refuse without mutation"},{"op":"add","path":"/metadata/files/31/note","value":"Real SDK dry-run fixtures and typed competing-owner refusal"},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T07:47:09.912Z"}],"before_hash":"e7f7a1338218e41421b932c4629cffcf0ab78cbe859da33dc9f608b1ee349903","after_hash":"2fe68a09e7d8efb14bcdba73b8a79993b22c8ea0f3af1c8bac83ecd6963dac32","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":"76eeca92eaaaf94142d321b04852deab0908b8fa5116dd50ea6074029071a2ed"} +{"hash_algorithm":"sha256","ts":"2026-11-12T07:47:10.733Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-12T07:47:10.726Z","author":"codex-github230-1111","text":"Round 8 final local evidence: 58 selected test files, using at most two total workers and a 1200-second command cap. Effective distinct outcomes after the documented reruns: 1422 reported tests, 1419 passed, zero assertion failures, two existing SDK-related skips, one canceled aggregate closeout-workflow wrapper at its 20-minute cap (49 workflow cases passed before cancellation). CI certifies the remaining workflow decision matrix and full release gates. The 82-case ownership matrix passes through pm on both Node and Bun; 76 refusal/durability assertions fail on their corresponding behavioral reverts (70 ownership shapes, four late claims, plan cleanup, directory flush), and restoration passes 82/82. Native policy-failure coverage passes 25/25. Build, test compilation, lint, typecheck, docstrings and duplication passed; 33 authored modules have zero duplicated lines. All 61 initial scoped command variants are already linked among the 75 item tests. Read the complete 38-thread snapshot, all five scoped threads with agent prompts, and the one scoped outside-diff comment. Deferred findings remain in issue 236; no review replies or resolutions were made. The ownership corpus read can create derived SDK caches, so fresh atomic privacy preflight now runs before that read; the real-tracker fresh/replayed privacy regressions pass 29/29. Existing audit histories remain append-only. Exact-head CI evidence: https://github.com/unbraind/pm-github/pull/233/checks ."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T07:47:10.733Z"}],"before_hash":"2fe68a09e7d8efb14bcdba73b8a79993b22c8ea0f3af1c8bac83ecd6963dac32","after_hash":"f5c5a0f68311c7c95522d81702664a4ef24a3ce112aa82fbccc8897fef4eeb68","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":"48b73f7a1152e18a40250bb23afaf4549928e917710de665cf73fbb8ee7bfadd"} +{"hash_algorithm":"sha256","ts":"2026-11-12T07:47:11.557Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-12T07:47:11.557Z"}],"before_hash":"f5c5a0f68311c7c95522d81702664a4ef24a3ce112aa82fbccc8897fef4eeb68","after_hash":"ccb3a70ca9778e8c27fa38e06940f7220aaedb15b19401697e4f885721713130","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":"2fcec02dbb671d934580d7688a1f96ec3ba07a6c9334c99394938bc5ebf30e5f"} +{"hash_algorithm":"sha256","ts":"2026-11-12T08:41:00.461Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-12T08:41:00.461Z"},{"op":"add","path":"/metadata/assignee","value":"codex-github230-1111"},{"op":"add","path":"/metadata/claim_principal","value":"codex-github230-1111"}],"before_hash":"ccb3a70ca9778e8c27fa38e06940f7220aaedb15b19401697e4f885721713130","after_hash":"5ff17ad6ff65d9ce65e73de39c4e826f692d498a98eaf1376ca9beba4db69e9b","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":"d48de1db643a9e89a901b95e590e9bc2b69c433997bbf90c64845302fb161f79"} +{"hash_algorithm":"sha256","ts":"2026-11-12T08:41:04.206Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-12T08:41:04.182Z","author":"codex-github230-1111","text":"Round 9 resumes from bd8d52d with a clean worktree. Current main is already merged. Read the three blocking CodeRabbit threads and agent prompts: follow-up reference tag, durable creation proof, and SDK 2026.10.17 compilation. Scope stays limited to those findings; the three non-blocking observations remain in issue 236. Plan: replace stray PR tags with docs references across changed tracker records, separate creation provenance from item naming and issue ownership, and retain revision fencing through a typed minimum-SDK fallback. Run the four scoped real-tracker suites plus current/floor typecheck, lint and changelog checks under 20-minute command limits."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T08:41:04.206Z"}],"before_hash":"5ff17ad6ff65d9ce65e73de39c4e826f692d498a98eaf1376ca9beba4db69e9b","after_hash":"04449ea61ae7cd693e2c402a1fb237bb3237418b46db8b86105313eda27dbc02","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":"2ef0c7cf170a91410a58e97aab36cc5339fc381d4e84946d01d8afd9ebed24fc"} +{"hash_algorithm":"sha256","ts":"2026-11-12T08:59:05.513Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-12T08:59:05.505Z","author":"codex-github230-1111","text":"Round 9: the 90-case matrix passes. Audited all seven tracker records added or changed by this PR; moved six contextual parent issue/PR tags to docs links, retaining only matching issue-230 and issue-236 identity tags. Creation proof now uses package author or an explicit github_import boolean, never an ID. New creates record pm-github as creator, and locked writes refuse an original mirroring plan if its target loses creation proof. Unknown-origin legacy records keep identity compatibility but preserve curated content. The isolated minimum-SDK job found both the GetResult.item_hash field and PmClient.withExpectedItemRevisions method unavailable at SDK 2026.10.17. Replaced them with native document/hash reads and a locked revision check using APIs available at that floor. The packed consumer now compiles all production/operational sources and declarations, exercises an owned update, and refuses a stale revision at SDK 2026.10.17. No SDK floor or dependency pins were raised. Scoped suite reruns retain the existing diagnostics for curated collisions and missing items."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T08:59:05.513Z"}],"before_hash":"04449ea61ae7cd693e2c402a1fb237bb3237418b46db8b86105313eda27dbc02","after_hash":"114043cd536ff13e72c4e8c8c22410dfc0cb4ea689258c644aa2cb90971ecef9","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":"d614b1004c92cd514c9717401ecdb92fa34647fab71a567c634914ea4c156de6"} +{"hash_algorithm":"sha256","ts":"2026-11-12T09:05:20.425Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/50/path","value":"test/workspace-resolution.test.ts"},{"op":"replace","path":"/metadata/files/49/path","value":"test/webhooks.test.ts"},{"op":"replace","path":"/metadata/files/48/path","value":"test/typed-sentinel-normalization.test.ts"},{"op":"replace","path":"/metadata/files/47/path","value":"test/smoke.test.ts"},{"op":"replace","path":"/metadata/files/46/path","value":"test/sdk-adoption.test.ts"},{"op":"replace","path":"/metadata/files/45/path","value":"test/scheduled-refusals.test.ts"},{"op":"add","path":"/metadata/files/44/note","value":"Unified typed ownership refusal"},{"op":"replace","path":"/metadata/files/44/path","value":"test/round-seven.test.ts"},{"op":"replace","path":"/metadata/files/43/note","value":"Contradictory legacy claims refuse without duplicate import"},{"op":"replace","path":"/metadata/files/43/path","value":"test/review-round3-identity.test.ts"},{"op":"replace","path":"/metadata/files/42/note","value":"Unified typed write refusal with readable legacy diagnostics"},{"op":"replace","path":"/metadata/files/42/path","value":"test/review-import-identity.test.ts"},{"op":"remove","path":"/metadata/files/41/note"},{"op":"replace","path":"/metadata/files/41/path","value":"test/repeat-import-noop.test.ts"},{"op":"replace","path":"/metadata/files/40/path","value":"test/refresh.test.ts"},{"op":"add","path":"/metadata/files/39/note","value":"Contradictory linked references refuse without mutation"},{"op":"replace","path":"/metadata/files/39/path","value":"test/reference-import-identity.test.ts"},{"op":"replace","path":"/metadata/files/38/note","value":"Creator proof and all changed tracker record reference audits"},{"op":"replace","path":"/metadata/files/38/path","value":"test/ownership-consistency.test.ts"},{"op":"replace","path":"/metadata/files/37/note","value":"Packed SDK floor typecheck and guarded write runtime probe"},{"op":"replace","path":"/metadata/files/37/path","value":"test/minimum-sdk-consumer.test.ts"},{"op":"add","path":"/metadata/files/25/note","value":"Real importer creation provenance in mirror fixtures"},{"op":"replace","path":"/metadata/files/9/note","value":"Persist package creator and bind mirroring permission"},{"op":"replace","path":"/metadata/files/7/note","value":"Minimum-SDK native document hash and locked revision fence"},{"op":"replace","path":"/metadata/files/6/note","value":"Durable creator provenance replaces naming-based mirroring"},{"op":"add","path":"/metadata/files/51","value":{"path":"webhooks.ts","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T09:05:20.425Z"}],"before_hash":"114043cd536ff13e72c4e8c8c22410dfc0cb4ea689258c644aa2cb90971ecef9","after_hash":"1a520ce744817c7517d67bec9ec658fae9f129a564342e6d008b8a0a4b14fb51","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":"e14bfc71d3b6dbcc51d1097a04a4fa9baa235735c9ec2cc39a33faa6073556a9"} +{"hash_algorithm":"sha256","ts":"2026-11-12T09:05:21.406Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/75","value":{"command":"timeout 1200 npm run test:minimum-sdk","scope":"project","timeout_seconds":1200,"provenance":{"author":"codex-github230-1111","created_at":"2026-11-12T09:05:21.342Z","source_kind":"local_mutation","source_ref":"fix/reference-tags-are-not-import-identity"}}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T09:05:21.406Z"}],"before_hash":"1a520ce744817c7517d67bec9ec658fae9f129a564342e6d008b8a0a4b14fb51","after_hash":"1eb2dfc4b9c773fcd4a770a3fb91079d207ca65a2ad2b45b4518f42913464f50","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":"84c57259476d3e4422ccf16674119da522e88934eaf9b171d7d5f940e6a8173c"} +{"hash_algorithm":"sha256","ts":"2026-11-12T09:15:18.719Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/51","value":{"created_at":"2026-11-12T09:15:18.711Z","author":"codex-github230-1111","text":"Round 9 revert proof in disposable copies: adding the former PR-233 tag back through the SDK makes the PR tracker audit fail 1/1; removing it restores 1/1. Reverting only importerCreatedItem to its ID-suffix implementation makes all seven Creation proof tests fail: two curated-ID cases, four provenance-backed non-pattern-ID controls, and a stale mirroring-plan refusal. Restoring it passes 7/7. Restoring the previous readGithubImportOwner and mutateGithubOwnedItem makes timeout 1200 npm run test:minimum-sdk fail with TS2339 for both GetResult.item_hash and PmClient.withExpectedItemRevisions at SDK 2026.10.17; restoring the native document/hash and locked revision guard passes the same floor job. Final restored matrix: 90/90. The data revert and restoration used native append-only SDK mutations; no existing history record was rewritten."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T09:15:18.719Z"}],"before_hash":"1eb2dfc4b9c773fcd4a770a3fb91079d207ca65a2ad2b45b4518f42913464f50","after_hash":"55951596457a880c5227bfaba09116c5055034fcf7c3f3c7e986452c6244af0e","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":"6ebfaffb7cfe359ab7d06402a0fd7740588013df2e221e06310d4844aaf1a6e0"} +{"hash_algorithm":"sha256","ts":"2026-11-12T09:15:19.541Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/52","value":{"created_at":"2026-11-12T09:15:19.533Z","author":"codex-github230-1111","text":"Round 9 final local gates: ownership-consistency 90/90 on Node and Bun through pm; import-ownership 26/26; import-fidelity 53 passed plus one existing pm-cli#1656 skip; closeout 93/93. Four-file total: 263 reported, 262 passed, one existing skip, no failures or cancellations. timeout 1200 npm run test:minimum-sdk passes 1/1 through pm against exact SDK 2026.10.17, including all authored source/declaration typechecks, a guarded owned update and stale-revision refusal. Current build, test compilation, lint and typecheck pass; docstrings, zero duplication and privacy checks pass. All test commands stayed below 20 minutes with at most two workers. Read 45 complete thread inventory entries and the full three scoped blocking threads with their agent prompts. No thread replies or resolutions were made. All seven PR tracker records were audited; six contextual references became docs links, while the matching issue-230/236 tags remain. The other three findings stay deferred to issue 236. Exact-head release certification remains CI: https://github.com/unbraind/pm-github/pull/233/checks ."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T09:15:19.541Z"}],"before_hash":"55951596457a880c5227bfaba09116c5055034fcf7c3f3c7e986452c6244af0e","after_hash":"dd02285bed43ceb706b62f9369d421430c405ce76c14fb5aa5f54975db67f078","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":"e5de36698153cd91e2c9ac56117caf2c41c2fc7b6992e10df5cd0d38a251607a"} +{"hash_algorithm":"sha256","ts":"2026-11-12T09:15:20.311Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-12T09:15:20.311Z"}],"before_hash":"dd02285bed43ceb706b62f9369d421430c405ce76c14fb5aa5f54975db67f078","after_hash":"101a9adbcf9d0fde9abc99dfe17e72c6c3d764b1cf5c417bb6ca4f8e8bb00340","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":"7ef37266ec0bcbc1e6d22b2deea771ee38054107d9afee5af0c2a4afcfeea53d"} +{"hash_algorithm":"sha256","ts":"2026-11-12T10:01:47.396Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-12T10:01:47.396Z"},{"op":"add","path":"/metadata/assignee","value":"codex-github230-1111"},{"op":"add","path":"/metadata/claim_principal","value":"codex-github230-1111"}],"before_hash":"101a9adbcf9d0fde9abc99dfe17e72c6c3d764b1cf5c417bb6ca4f8e8bb00340","after_hash":"302171f9d7953fa6eaa3e8542164bade81d621855974d39d383de79b8b10d8e6","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":"babc084c5e430fae121660ff1e00a1f9826842c0034af243a8a869d4030c8634"} +{"hash_algorithm":"sha256","ts":"2026-11-12T10:01:49.074Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/53","value":{"created_at":"2026-11-12T10:01:49.068Z","author":"codex-github230-1111","text":"Round 10 starts from d9d9bc7 with a clean worktree; fetched main is already merged. Read the complete named codex verdict and issue 236. Inventory first: added docs/identity-ownership-inventory.md with every ownership-helper call and direct identity-field access grouped by enclosing function. Confirmed bypasses in upstreamTargets and dependency indexing; the audit also identifies refresh, Projects issue association, search association, diagnostics, blocker self-exclusion, and remote execution rechecks. Route target selectors through shared claims while documenting explicit-source and reference-only operations that do not select owned targets. Tests will use real SDK trackers and HTTP transport, including consistent controls and revert proofs."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T10:01:49.074Z"}],"before_hash":"302171f9d7953fa6eaa3e8542164bade81d621855974d39d383de79b8b10d8e6","after_hash":"e13b6e805513a92df5be03b5d4c8e437e673fa079f342ed6d8f74f89e8e8398f","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":"cd85e32fdfbcdfd49f6cef269d03fb56a25ed17a58d4e47147420a9471228653"} +{"hash_algorithm":"sha256","ts":"2026-11-12T10:16:18.209Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/54","value":{"created_at":"2026-11-12T10:16:18.201Z","author":"codex-github230-1111","text":"Round 10 inventory identified direct selectors in outbound sync/export, dependency indexing, refresh, search, Projects issue association and blocker self-exclusion. Operational selectors now share ownership claims and consistency/uniqueness checks. Dependency endpoints are checked again inside the native source mutation lock; outbound writes re-read current tracker ownership. The initial 47 outbound/dependency SDK and real-HTTP regressions pass. Broad importer-consumer checks are running with two workers and a 20-minute per-file limit."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T10:16:18.209Z"}],"before_hash":"e13b6e805513a92df5be03b5d4c8e437e673fa079f342ed6d8f74f89e8e8398f","after_hash":"311b87b080ba6363a47104acac4456cd7eeff6d7efe97a59ac4516e7dd8460f6","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":"beaa8cd80d332cd31c8b27d8780def7f544ee71dabafed7e05f532ace6a81f0c"} +{"hash_algorithm":"sha256","ts":"2026-11-12T10:16:21.155Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"docs_add","patch":[{"op":"remove","path":"/metadata/docs/4/note"},{"op":"replace","path":"/metadata/docs/4/path","value":"https://github.com/unbraind/pm-github/pull/233/checks"},{"op":"add","path":"/metadata/docs/3/note","value":"Implementation and scoped review evidence"},{"op":"replace","path":"/metadata/docs/3/path","value":"https://github.com/unbraind/pm-github/pull/233"},{"op":"remove","path":"/metadata/docs/2/note"},{"op":"replace","path":"/metadata/docs/2/path","value":"docs/identity-ownership-inventory.md"},{"op":"add","path":"/metadata/docs/5","value":{"path":"README.md","scope":"project","note":"All owner claims must agree and remain unique; journal-aware cleanup"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T10:16:21.155Z"}],"before_hash":"311b87b080ba6363a47104acac4456cd7eeff6d7efe97a59ac4516e7dd8460f6","after_hash":"520f232fc37a49853edc2b35a116fcb6262f7dd9e30fa1d2125e886427d1bb46","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":"b3bb1138ae5303d08b3b898f7fed7bb794ea7ec7280d7a72d117477b1ca8cbf8"} +{"hash_algorithm":"sha256","ts":"2026-11-12T10:36:11.653Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/76","value":{"command":"node --test --test-concurrency=1 test/ownership-consistency.test.ts","scope":"project","timeout_seconds":1200,"provenance":{"author":"codex-github230-1111","created_at":"2026-11-12T10:36:11.532Z","source_kind":"local_mutation","source_ref":"fix/reference-tags-are-not-import-identity"}}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T10:36:11.653Z"}],"before_hash":"520f232fc37a49853edc2b35a116fcb6262f7dd9e30fa1d2125e886427d1bb46","after_hash":"2cc5efdd8a12168a9f71b770cba3a29654945e119bf23fdc151d83bc7cb23c63","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":"0430b56a48fc8ff08b24bf9c36df97bd88e064c40d6bfc8c8bc7e7ffac7753cd"} +{"hash_algorithm":"sha256","ts":"2026-11-12T10:46:00.609Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/55","value":{"created_at":"2026-11-12T10:46:00.601Z","author":"codex-github230-1111","text":"Round 10 ownership matrix passes 161/161 through pm test (one selected command, source workspace; project policy keeps test-result tracking disabled). The completed source inventory has 83 function groups and 248 helper/field/parser sites, plus five script/workflow text matches reviewed with no independent owner selector. Direct importIdentityKeys consumption is now restricted to the shared claims implementation. Shared URL-kind and direct-provenance facets preserve native declaration lifecycle classification and exclude legacy PR links from issue refresh. Search, Projects issue association, blocker self-exclusion and sync hints also use shared claims; unrelated reference parsing, labels, board IDs and explicit source arguments are documented as non-owning. Real-tracker label-model and literal-assignment compatibility fixtures now declare importer provenance instead of relying on naming. Disposable behavioral reverts and remaining bounded consumer gates are running."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T10:46:00.609Z"}],"before_hash":"2cc5efdd8a12168a9f71b770cba3a29654945e119bf23fdc151d83bc7cb23c63","after_hash":"10af17818e3e72f982d806445c4b903ebaae56ea668716c1a92b62666ce35b96","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":"7a5713765ce077f54bb58ffdf2491af3efb1a9116f24eb96957dae8818437a24"} +{"hash_algorithm":"sha256","ts":"2026-11-12T10:46:01.753Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/51/path","value":"test/webhooks.test.ts"},{"op":"replace","path":"/metadata/files/50/path","value":"test/typed-sentinel-normalization.test.ts"},{"op":"replace","path":"/metadata/files/49/path","value":"test/smoke.test.ts"},{"op":"replace","path":"/metadata/files/48/path","value":"test/sdk-adoption.test.ts"},{"op":"replace","path":"/metadata/files/47/path","value":"test/scheduled-refusals.test.ts"},{"op":"add","path":"/metadata/files/46/note","value":"Unified typed ownership refusal"},{"op":"replace","path":"/metadata/files/46/path","value":"test/round-seven.test.ts"},{"op":"add","path":"/metadata/files/45/note","value":"Contradictory legacy claims refuse without duplicate import"},{"op":"replace","path":"/metadata/files/45/path","value":"test/review-round3-identity.test.ts"},{"op":"replace","path":"/metadata/files/44/note","value":"Unified typed write refusal with readable legacy diagnostics"},{"op":"replace","path":"/metadata/files/44/path","value":"test/review-import-identity.test.ts"},{"op":"remove","path":"/metadata/files/43/note"},{"op":"replace","path":"/metadata/files/43/path","value":"test/repeat-import-noop.test.ts"},{"op":"remove","path":"/metadata/files/42/note"},{"op":"replace","path":"/metadata/files/42/path","value":"test/refresh.test.ts"},{"op":"add","path":"/metadata/files/41/note","value":"Contradictory linked references refuse without mutation"},{"op":"replace","path":"/metadata/files/41/path","value":"test/reference-import-identity.test.ts"},{"op":"replace","path":"/metadata/files/40/path","value":"test/project-sync.test.ts"},{"op":"replace","path":"/metadata/files/39/note","value":"Creator proof and all changed tracker record reference audits"},{"op":"replace","path":"/metadata/files/39/path","value":"test/ownership-consistency.test.ts"},{"op":"replace","path":"/metadata/files/38/note","value":"Packed SDK floor typecheck and guarded write runtime probe"},{"op":"replace","path":"/metadata/files/38/path","value":"test/minimum-sdk-consumer.test.ts"},{"op":"remove","path":"/metadata/files/37/note"},{"op":"replace","path":"/metadata/files/37/path","value":"test/link-deps.test.ts"},{"op":"replace","path":"/metadata/files/36/path","value":"test/label-import-model.test.ts"},{"op":"replace","path":"/metadata/files/35/path","value":"test/import-projection.test.ts"},{"op":"add","path":"/metadata/files/34/note","value":"Native SDK revision and independent peer fixtures"},{"op":"replace","path":"/metadata/files/34/path","value":"test/import-ownership.test.ts"},{"op":"remove","path":"/metadata/files/33/note"},{"op":"replace","path":"/metadata/files/33/path","value":"test/import-lock.test.ts"},{"op":"add","path":"/metadata/files/32/note","value":"Real SDK dry-run fixtures and typed competing-owner refusal"},{"op":"replace","path":"/metadata/files/32/path","value":"test/import-gate.test.ts"},{"op":"remove","path":"/metadata/files/31/note"},{"op":"replace","path":"/metadata/files/31/path","value":"test/import-fidelity.test.ts"},{"op":"replace","path":"/metadata/files/30/path","value":"test/import-errors.test.ts"},{"op":"replace","path":"/metadata/files/29/path","value":"test/import-conflict-report.test.ts"},{"op":"replace","path":"/metadata/files/28/path","value":"test/identity.test.ts"},{"op":"replace","path":"/metadata/files/27/path","value":"test/helpers/import-preview.ts"},{"op":"add","path":"/metadata/files/26/note","value":"Real importer creation provenance in mirror fixtures"},{"op":"replace","path":"/metadata/files/26/path","value":"test/helpers/import-mirror.ts"},{"op":"remove","path":"/metadata/files/25/note"},{"op":"replace","path":"/metadata/files/25/path","value":"test/handler-failures.test.ts"},{"op":"replace","path":"/metadata/files/24/path","value":"test/github-url.test.ts"},{"op":"replace","path":"/metadata/files/23/path","value":"test/fixtures/refresh-crash.ts"},{"op":"replace","path":"/metadata/files/22/path","value":"test/fixtures/ownership-peer.ts"},{"op":"replace","path":"/metadata/files/21/path","value":"test/fixtures/atomic-plan-crash.ts"},{"op":"replace","path":"/metadata/files/20/path","value":"test/durable-state.test.ts"},{"op":"replace","path":"/metadata/files/19/path","value":"test/durable-recovery.test.ts"},{"op":"replace","path":"/metadata/files/18/path","value":"test/dryrun-preview.test.ts"},{"op":"replace","path":"/metadata/files/17/path","value":"test/daily-sync-idempotency.test.ts"},{"op":"add","path":"/metadata/files/16/note","value":"Native SDK policy refusal fixtures for guarded plain writes"},{"op":"replace","path":"/metadata/files/16/path","value":"test/coverage-remainder.test.ts"},{"op":"remove","path":"/metadata/files/15/note"},{"op":"replace","path":"/metadata/files/15/path","value":"test/coverage-edges.test.ts"},{"op":"replace","path":"/metadata/files/14/path","value":"test/cited-issue-adoption.test.ts"},{"op":"replace","path":"/metadata/files/13/path","value":"test/blockers.test.ts"},{"op":"add","path":"/metadata/files/52","value":{"path":"test/workspace-resolution.test.ts","scope":"project"}},{"op":"add","path":"/metadata/files/53","value":{"path":"webhooks.ts","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T10:46:01.753Z"}],"before_hash":"10af17818e3e72f982d806445c4b903ebaae56ea668716c1a92b62666ce35b96","after_hash":"6e69fbaefd8b5e0cf8cb34cb347c3ab7dac2e92e45155e0a672bb05cc14664e4","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":"975728fb8442de82bd336a298d6c55009ced1b3f0cfca628e2df6c5253b97c0f"} +{"hash_algorithm":"sha256","ts":"2026-11-12T10:49:29.648Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/56","value":{"created_at":"2026-11-12T10:49:29.639Z","author":"codex-github230-1111","text":"Round 10 behavioral revert proof in a disposable source copy: restoring the complete index.ts from d9d9bc7 makes every selected new case fail: 34/34 ^Outbound ownership, 13/13 ^Dependency ownership, 16/16 ^Ownership routing inventory:, and 2/2 ^Ownership sync hint. Restoring only selectRefreshTargets from that head makes 1/1 ^Refresh ownership kind: fail. Commands use node --test --test-concurrency=1 --test-name-pattern PATTERN test/ownership-consistency.test.ts, each capped at 1200 seconds. Every failure is an assertion/observable diagnostic or missing-refusal failure, with zero import/syntax setup failures and zero cancellations. Restoring all fixed source files makes the combined 66/66 cases pass. The package worktree was never reverted; native tracker storage and the real HTTP transport/server are used. The exact partial github_repo-versus-URL reproduction sends no HTTP writes with the fix."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T10:49:29.648Z"}],"before_hash":"6e69fbaefd8b5e0cf8cb34cb347c3ab7dac2e92e45155e0a672bb05cc14664e4","after_hash":"d43968926b6df4bcd1ba4f758fff99c6da4c98d67205b7f7611a5cfe8df9921b","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":"c1636e855a4a7caacead12fbbde8ccde9cf6e584c77281c467f9e069b41e5a74"} +{"hash_algorithm":"sha256","ts":"2026-11-12T11:01:33.172Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/57","value":{"created_at":"2026-11-12T11:01:33.165Z","author":"codex-github230-1111","text":"Round 10 final bounded gate evidence: 52 unique test files, 1439 cases, 1437 pass, 0 fail, 2 existing pm-cli#1656 quarantines, 0 cancellations. All 48 direct consumers of changed modules were read and tested, plus three additional closeout files and the minimum-SDK consumer. Commands are timeout 1200 node --test --test-concurrency=1 TEST_FILE, with at most two total workers. Ownership matrix: 161/161 through pm test. Link-deps: 28/28; import-ownership: 26/26; import-fidelity: 53 pass plus one existing skip; refresh: 34 pass plus one existing skip; closeout core/attribution/revision/selection: 93/12/110/82 passes; Projects: 39/39 and integration 19/19; packed npm/native Bun projection: 6/6. The full import-gate file completed 73 cases within its 20-minute cap; its two old diagnostic assertions were updated and rerun by exact names, yielding 73/73 final case evidence. Fixture corrections retain curated safety and explicitly mark genuine importer mirrors. npm run typecheck, npm run build:test, npm run lint and npm run docstring pass. npm run test:minimum-sdk passes its exact 2026.10.17 packed consumer (all production/operational modules, declarations, owned write and stale revision refusal); no dependency or engine floor changed. Privacy scanned 8749 existing objects with approved identities and no secret/host-path findings. Changelog is regenerated after this final tracker write and claim release. Full npm/Bun release and exact 100-percent coverage remain for CI: https://github.com/unbraind/pm-github/pull/233/checks . The priority external_ref dependency ambiguity in issue 236 is fixed here; other deferred observations remain there. Two supplied codex review findings were read in full; no review threads were replied to or resolved. Releasing only this author claim and leaving the item in_progress."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T11:01:33.172Z"}],"before_hash":"d43968926b6df4bcd1ba4f758fff99c6da4c98d67205b7f7611a5cfe8df9921b","after_hash":"91e8756f516f8bbaac3f24ac8fc2b850659227dbbbf3b23e6040ea1e0d15b94e","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":"9be8978a50ea5b6d55cee7c737edabc64a60db44df8e817ce9558e96e9a98343"} +{"hash_algorithm":"sha256","ts":"2026-11-12T11:01:34.120Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-12T11:01:34.120Z"}],"before_hash":"91e8756f516f8bbaac3f24ac8fc2b850659227dbbbf3b23e6040ea1e0d15b94e","after_hash":"6d0a77c3c11ae67f5121261c53a27086d077fb1c4d47a6ad8f977cc8a6e828dd","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":"34881b063cf296f7165ebee87b193202af2b7a7c656049d49d108ec3368f8768"} +{"hash_algorithm":"sha256","ts":"2026-11-12T11:17:16.626Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-12T11:17:16.626Z"},{"op":"add","path":"/metadata/assignee","value":"codex-github230-1111"},{"op":"add","path":"/metadata/claim_principal","value":"codex-github230-1111"}],"before_hash":"6d0a77c3c11ae67f5121261c53a27086d077fb1c4d47a6ad8f977cc8a6e828dd","after_hash":"6f44b965f0c875d828ff893452e37c48d90f5b43b80b3e33aee84809041a805d","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":"99d7191f860911e817a3078c95631d23c556cf636692b0e17b54d60e2e6b922c"} +{"hash_algorithm":"sha256","ts":"2026-11-12T11:17:19.002Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/58","value":{"created_at":"2026-11-12T11:17:18.992Z","author":"codex-github230-1111","text":"Round 11: reread issue 230 Expected and revert-test contract, plus all five scoped review threads and AI-agent prompts. The round-8 tag-claim rule is replaced: plain gh reference tags never confer ownership or conflicts, including on linked items. Preserve explicit provenance conflicts and legacy importer-created IDs with their verified repository hash. Build ownership indexes once per snapshot, share pre-journal corpus reads and transaction-local locked reads, and retain fresh outbound checks. Add real SDK regressions and deterministic N=40 cost counts; align URL mention normalization and complete preview byte snapshots. Public scratch-clone previews are read-only, capped at 15 minutes and about 60 API calls, with cleanup. Main has not moved from the existing merged ancestor."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T11:17:19.002Z"}],"before_hash":"6f44b965f0c875d828ff893452e37c48d90f5b43b80b3e33aee84809041a805d","after_hash":"a1a034911d186a852feada2817b5f0aa4f447cf19568f208ce91c0cfb27719df","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":"20a745ad2334964c860533a0fc25014f4c31c2f7f706e04b4dc57b21633c8779"} +{"hash_algorithm":"sha256","ts":"2026-11-12T11:47:48.612Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/59","value":{"created_at":"2026-11-12T11:47:48.604Z","author":"codex-github230-1111","text":"Round 11 contract: plain gh: tags are mentions on every item and never promoted, constrained or enabled by the deprecated upstream flag. Proven importer IDs use the shared repository hash plus creation provenance, preserving legacy records without github_number. Ownership snapshots now index claims once, batch native preflight reads peers once and locked checks reuse one snapshot; outbound writes read one fresh corpus and validate only the current target. Added native N=40 work-count regressions, API mention grouping, full byte-snapshot controls and tag/legacy compatibility cases. Public preview completed with unchanged clone bytes and zero legacy duplicates; detailed counts and privacy refusals will be recorded after final validation."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T11:47:48.612Z"}],"before_hash":"a1a034911d186a852feada2817b5f0aa4f447cf19568f208ce91c0cfb27719df","after_hash":"d8afebf6582f7d6470f7c048d60958c91de93b963cf47057b3ec0639907c2474","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":"c11a858fcd80f22f92bbd2a9fec7d1e11018091aa6793fc04f281d8988a1db3e"} +{"hash_algorithm":"sha256","ts":"2026-11-12T11:51:05.869Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/60","value":{"created_at":"2026-11-12T11:51:05.856Z","author":"codex-github230-1111","text":"Read-only public-check discrepancy: pm-context and pm-linear proposals have zero legacy-ID duplicates and no curated-field replacements, but their existing GitHub bodies trigger batch privacy refusals (two and five findings respectively). pm-review has no privacy/ownership refusal and no title replacements; its proven pm-github-authored importer record for issue 14 would restore source created_at, consistent with the accepted mirroring rule but different from the brief expectation that all four referenced records are curated. The other three retain curated timestamps. No clone was committed, pushed or left behind; completed checks preserve file bytes. We will report this exception explicitly rather than change importer mirroring to hide it."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T11:51:05.869Z"}],"before_hash":"d8afebf6582f7d6470f7c048d60958c91de93b963cf47057b3ec0639907c2474","after_hash":"a14a781b42cba74f0118030ba2a46a7df39812d4a0b3aaccf60ba5c4045e354b","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":"16b74298d2b3f01e1396d48068cb071d942c6e83115c30fc44a7c531fd89deae"} +{"hash_algorithm":"sha256","ts":"2026-11-12T11:57:30.929Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/61","value":{"created_at":"2026-11-12T11:57:30.922Z","author":"codex-github230-1111","text":"Final-build public previews reused cached public REST/timeline payloads without additional API calls: pm-context 221 creates / 3 matching updates / one batch privacy refusal; pm-linear 217 / 1 / one batch privacy refusal; pm-review 82 / 1 / zero refusals. Zero ownership refusals, zero legacy duplicates and no curated-field replacement in all three. pm-linear-toolchain-1104 stays untouched. The four named review records keep their titles; the proven importer-authored issue-14 record would restore its source created_at, while the three curated records keep theirs. All three clone byte snapshots match and all clones were deleted. Total public calls: 59; active read-only checks stayed under 15 minutes. The early attempt that did not preserve clone bytes was discarded and rerun with read-only clones and optional Git index refresh disabled; only the completed byte-identical previews support these results."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T11:57:30.929Z"}],"before_hash":"a14a781b42cba74f0118030ba2a46a7df39812d4a0b3aaccf60ba5c4045e354b","after_hash":"c2b863b1f279ec8598d51b09d6e4d1b15cfffd8fdc5c27ca8b98c887fbec26aa","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":"9f99d055e11db78eb4bdaf6a74aeef6a4f63c92a889e02fadfa6093214cdde05"} +{"hash_algorithm":"sha256","ts":"2026-11-12T12:02:45.347Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/62","value":{"created_at":"2026-11-12T12:02:45.295Z","author":"codex-github230-1111","text":"Self-review of the inventory found an old closeout suffix-only destination fence beside the new shared helper. Removed that independent identity check: only a repository-hash-verified importer ID constrains ownership; a naming pattern for an unrelated repository cannot contradict otherwise consistent explicit provenance. Added a real-SDK closeout must-allow control and scoped legacy-ID resolution to the known delivery/project/dependency repositories. Creation proof still requires importer author or github_import metadata, and genuine explicit/ID conflicts remain refused. The native cost test now also exercises nested metadata finalizers inside its actual SDK transaction."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T12:02:45.347Z"}],"before_hash":"c2b863b1f279ec8598d51b09d6e4d1b15cfffd8fdc5c27ca8b98c887fbec26aa","after_hash":"015f6c317307d21afa408a9f73eab6b9b4c874637cec6d3c1ef512eb4174cc32","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":"6001c889ae4c974d1a95b449fb1517d95e6a404058b84979afdcb05184a13f38"} +{"hash_algorithm":"sha256","ts":"2026-11-12T12:32:44.307Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/63","value":{"created_at":"2026-11-12T12:32:44.275Z","author":"codex-github230-1111","text":"Round 11 core gates: ownership-consistency 142/142; reference-import-identity 17/17; N=40 native batch including nested finalizers uses exactly two corpus reads and 360 claim parses; outbound 40-item sync uses 1640 claim parses, preserving fresh per-write checks. The first import-gate command reached its 1200-second cap after 64 passes and one runner cancellation; the nine uncompleted workflow cases are scheduled separately. All scoped threads and AI-agent prompts were read in full (five threads); the deferred durable-reader observation stays in issue 236. No review threads were replied to or resolved. The routing ledger has 88 function groups and 259 helper-call/field/parser sites."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T12:32:44.307Z"}],"before_hash":"015f6c317307d21afa408a9f73eab6b9b4c874637cec6d3c1ef512eb4174cc32","after_hash":"ff0d80ce7ebbaf89a9f06370e768c255e46f67c336349f90f7313c5a97de70aa","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":"4a751c15b4e524216439d00f9987e9413ebe0d054e3aa2b915d768af37c19171"} +{"hash_algorithm":"sha256","ts":"2026-11-12T12:44:41.276Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/64","value":{"created_at":"2026-11-12T12:44:41.254Z","author":"codex-github230-1111","text":"Round 11 behavioral revert proof, entirely in a disposable package copy: restoring tag-as-owner claims fails 5/5 Reference contract cases and all 6/6 issue-230 Chore/shared-reference/mirror cases. Disabling verified importer-ID ownership fails 1/1 legacy resync case. Restoring the prior native ownership wrapper fails 1/1 N=40 work bound; restoring prior upstream target rebuilding fails 1/1 outbound bound. Restoring the old closeout suffix fence fails 1/1 hash control, the old API mention key fails 1/1 grouping case, and the old preview file filter fails 2/2 byte-snapshot cases. Total: 18 expected assertion failures across eight groups, zero setup/import errors or cancellations. Restoring the fix passes 17/17 combined controls plus 6/6 issue-230 cases. Commands: timeout 1200 node --test --test-concurrency=1 --test-name-pattern PATTERN TEST_FILE, using ownership-consistency.test.ts or reference-import-identity.test.ts. The package worktree was never reverted. The complete 142-case matrix also passed through pm test --run --only-index 77 --progress; npm run test:minimum-sdk passed the exact 2026.10.17 packed consumer."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T12:44:41.276Z"}],"before_hash":"ff0d80ce7ebbaf89a9f06370e768c255e46f67c336349f90f7313c5a97de70aa","after_hash":"97271dafa189baf4cdd9e0637e55970f16596e109827e48fdf9eea946b1a5a1a","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":"ce3c3212918e66d55e6e428c2553e8c60f66bb5898c8c91a753ccda8b7fb9f5a"} +{"hash_algorithm":"sha256","ts":"2026-11-12T12:50:46.096Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/65","value":{"created_at":"2026-11-12T12:50:46.076Z","author":"codex-github230-1111","text":"Round 11 final bounded evidence: all 51 direct/dependent test files plus the minimum-SDK consumer were read and exercised. Final case evidence across 52 unique files: 1420 tests, 1418 pass, zero remaining failures/cancellations, two unchanged pm-cli#1656 SDK quarantines (import-fidelity and refresh). The first import-gate attempt capped at 1200 seconds after 64 passes; all nine remaining cases passed separately, giving 73/73 distinct workflow cases. Final-source closeout core/attribution/revision/selection pass 93/12/110/82; Projects integration 19/19; importer ownership 26/26; reference identity 17/17; matrix 142/142 through pm test. Corrected legacy tag/declaration fixtures use explicit links or proven importer creation, and curated no-op fixtures assert retained local dates. Every command used at most two total test workers and a 1200-second cap. npm run typecheck, npm run build:test, npm run lint and npm run docstring pass (301 documented declarations). npm run test:minimum-sdk passes the exact declared 2026.10.17 packed consumer. Privacy audited 8787 existing Git objects with approved identities and no secret/host-path matches. Behavioral proof remains 18/18 failures on eight reverts and 23/23 after restoration. Public preview totals remain 59 API calls, zero legacy duplicates/ownership refusals and unchanged clone bytes; pm-context and pm-linear have existing-source privacy refusals, while the proven importer-created pm-review issue-14 record would restore source created_at. No public tracker or hold was changed. All seven listed per-repository reference-tag cleanup workarounds are unnecessary under this rule. The deferred reader thread stays in issue 236. Releasing only this claim; this item stays in_progress for orchestrator verification. Full release, coverage, npm/Bun and exact-head CI certification remain at https://github.com/unbraind/pm-github/pull/233/checks . Changelog is regenerated after this final tracker write and release."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T12:50:46.096Z"}],"before_hash":"97271dafa189baf4cdd9e0637e55970f16596e109827e48fdf9eea946b1a5a1a","after_hash":"ab638e1314e24cfea8ee3aab04a26e5aa99a4f262aed3834edd4f68f7ed54ee1","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":"268d1e65bc8cf4bbd932494185cc6f8546ea650d9c629e365371e7b7e44e0d66"} +{"hash_algorithm":"sha256","ts":"2026-11-12T12:50:47.924Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-12T12:50:47.924Z"}],"before_hash":"ab638e1314e24cfea8ee3aab04a26e5aa99a4f262aed3834edd4f68f7ed54ee1","after_hash":"63bd02ad732e404375c42f3157bd981a0789d5eed20ffa3fd02b853f93e4576f","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":"89fa59af2e164a0c6825f18b80b3d8c8dc46012f3027b07347d29d21ee2d9905"} +{"hash_algorithm":"sha256","ts":"2026-11-12T14:36:25.549Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-12T14:36:25.549Z"},{"op":"add","path":"/metadata/assignee","value":"codex-github230-1111"},{"op":"add","path":"/metadata/claim_principal","value":"codex-github230-1111"}],"before_hash":"63bd02ad732e404375c42f3157bd981a0789d5eed20ffa3fd02b853f93e4576f","after_hash":"29341c06acabda9bea3ca710eb0b1111d0f580057a24d9d844daa10ca161f0d8","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":"78edfe0f882003fcfa19c32f7d88a17e91125b7a8efa71f8c5a703d80c78560a"} +{"hash_algorithm":"sha256","ts":"2026-11-12T14:41:29.953Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/66","value":{"created_at":"2026-11-12T14:41:29.946Z","author":"codex-github230-1111","text":"Round 12 resumes from 3d5f91b with a clean worktree; fetched main is unchanged and already merged. Read all ten scoped/outdated threads, every attached AI-agent prompt, the README outside-diff comment, and orchestrator comment 6824496720. The scheduled command excludes PRs; its five-repo results supersede the round-11 PR-inclusive previews. Plan: guard atomic comments, reject API-only owner candidates, retain verified PR kind, refuse stranded retained plans, flush created directories on failure, scan the exact persisted recovery payload, correct guard/post-write docs, and resolve remaining scoped-pull/concurrency gaps. The project-index performance issue is already fixed by the shared snapshot index. All new tests use native SDK storage and local HTTP transport. Real end-to-end validation will pack/install this build in five disposable public clones, apply the exact scheduled import locally, and run its post-write checks without commits or pushes."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T14:41:29.953Z"}],"before_hash":"29341c06acabda9bea3ca710eb0b1111d0f580057a24d9d844daa10ca161f0d8","after_hash":"e691c854267602389d0ea85cea8e4df682e7722a137f0e2363bef16c444d4f5d","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":"7572417911b5102ff4a5cf12fba42afdad4325b7c30d816017b63f25ba2c11f0"} +{"hash_algorithm":"sha256","ts":"2026-11-12T15:00:57.240Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/67","value":{"created_at":"2026-11-12T15:00:57.204Z","author":"codex-github230-1111","text":"Round 12 implementation: guard atomic annotations with their import target; API-only link candidates receive no lifecycle writes; verified importer IDs retain matching generated PR kind; orphaned same-repository refresh plans refuse explicitly; durable state flushes newly created ancestors even after failure; privacy scans the entire recovery payload before staging. The two outdated ownership findings remain present at the starting head: native writes now share a reentrant tracker ownership lease acquired before SDK locks, and scoped Project pulls plan against complete peers and revalidate inside native writes. The third outdated finding is already fixed: 40 native Project records take exactly 120 claim parses across three planners. Focused native regressions pass 27/27; lint and typecheck pass. Representative scheduled-sync E2E and complete affected-file gates remain running."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T15:00:57.240Z"}],"before_hash":"e691c854267602389d0ea85cea8e4df682e7722a137f0e2363bef16c444d4f5d","after_hash":"a23e294fb09eada1235e6d47df0a81055d011fd7b52e1a1beb2cc3d37898d193","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":"d1d0420e5d34be98c9f275f75cbd370453484eb6cae66dfebf238f0c03efa78a"} +{"hash_algorithm":"sha256","ts":"2026-11-12T15:03:09.827Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/53/path","value":"test/workspace-resolution.test.ts"},{"op":"replace","path":"/metadata/files/52/path","value":"test/webhooks.test.ts"},{"op":"replace","path":"/metadata/files/51/path","value":"test/typed-sentinel-normalization.test.ts"},{"op":"replace","path":"/metadata/files/50/path","value":"test/smoke.test.ts"},{"op":"replace","path":"/metadata/files/49/path","value":"test/sdk-adoption.test.ts"},{"op":"replace","path":"/metadata/files/48/path","value":"test/scheduled-refusals.test.ts"},{"op":"replace","path":"/metadata/files/47/path","value":"test/round-twelve.test.ts"},{"op":"add","path":"/metadata/files/54","value":{"path":"webhooks.ts","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T15:03:09.827Z"}],"before_hash":"a23e294fb09eada1235e6d47df0a81055d011fd7b52e1a1beb2cc3d37898d193","after_hash":"723757b865ccc8d863b844cd2348ee8ef86fae54525d760da223223b8a56a3b5","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":"e01ceba84c6c55ee930f483ecd58192163e6d74b1ec0a55067e4489aec689d7b"} +{"hash_algorithm":"sha256","ts":"2026-11-12T15:03:14.698Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/77","value":{"command":"timeout 1200 node --test --test-concurrency=1 test/round-twelve.test.ts","scope":"project","provenance":{"author":"codex-github230-1111","created_at":"2026-11-12T15:03:14.468Z","source_kind":"local_mutation","source_ref":"fix/reference-tags-are-not-import-identity"}}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T15:03:14.698Z"}],"before_hash":"723757b865ccc8d863b844cd2348ee8ef86fae54525d760da223223b8a56a3b5","after_hash":"9c3fd0c4bd2829eced1fea0afb706e640164be9ff50dd5b88452b1ca879080df","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":"fa5679c37b5a2330e312ee8909e6c94c6b88c0317567c372f5f3433fd8f29058"} +{"hash_algorithm":"sha256","ts":"2026-11-12T15:15:05.472Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/68","value":{"created_at":"2026-11-12T15:15:05.453Z","author":"codex-github230-1111","text":"Broad gates identified and corrected a missing PullPlan docstring and preserved enumerated privacy rule names in the new typed preflight refusal. Direct REST end-to-end checks passed gated import and repeat-import for pm-context and pm-linear before the bounded API budget stopped remaining source reads. To finish all five without further per-issue calls, one additional batched live GitHub GraphQL read supplies complete, pagination-checked public issue/comment/timeline snapshots to a read-only transport cache. Exact installed CLI commands, native tracker writes, workflow post-write gates and strict health remain unchanged; no GitHub writes are allowed. The rerun already passes every post-write step for pm-context, with zero curated file diff."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T15:15:05.472Z"}],"before_hash":"9c3fd0c4bd2829eced1fea0afb706e640164be9ff50dd5b88452b1ca879080df","after_hash":"ccfcf3c518ff7d22ee2885dabc8e6e584bca2825942585facd8181ca45af853f","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":"9ca1c06abddf367ea3b4dbe1e02ef2ec375b39d519134658e92ce3406964c4f5"} +{"hash_algorithm":"sha256","ts":"2026-11-12T15:18:49.809Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/69","value":{"created_at":"2026-11-12T15:18:49.783Z","author":"codex-github230-1111","text":"Round 12 real-data end-to-end: packed candidate 779e67d with npm pack --ignore-scripts, installed it project-locally in five fresh public scratch clones, and verified installed build bytes. Every repo passed the exact scheduled issue-only preview, atomic gated import with comments, repeat-import no-op, standalone gate, caller changelog, staged full-context diff gate and strict health. Counts (create/update): pm-context 7/3; pm-linear 8/1; pm-review 3/1; pm-brief 5/2 plus one unchanged; pm-rl 6/0. All five have zero ownership refusals and zero curated item/history diff. Real public source snapshots were fetched once through a pagination-checked batched GitHub GraphQL read and projected only to the transport response shapes, avoiding repeated per-issue calls; core REST-based import/repeat also passed for context and linear. Conservative total budget debit: 81 API calls including earlier harness attempts. All scratch clones were deleted; no commits, pushes or PRs were made from them."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T15:18:49.809Z"}],"before_hash":"ccfcf3c518ff7d22ee2885dabc8e6e584bca2825942585facd8181ca45af853f","after_hash":"8a9e54109e2c714fe4bf32c9a8e218f8f1c1eafc5da697fd65265ff91ffb5e82","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":"a875a5309b5d7c228d2aed000bf4ed51c5c9912e3d26ca1be9ec9b11552a0885"} +{"hash_algorithm":"sha256","ts":"2026-11-12T15:21:21.450Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/70","value":{"created_at":"2026-11-12T15:21:21.439Z","author":"codex-github230-1111","text":"Round 12 read receipt: 10 scoped/outdated threads, 10 complete comments including proposed patches and AI prompts, no remaining comment pages, plus the latest outside-diff README review instruction. Real-data check finished in 926 seconds including harness retries, below its 25-minute cap: all five repos passed all 13 setup/import/post-write steps, 29 creates, 7 updates and one unchanged issue, zero refusals, zero curated item/history changes. API accounting is a conservative 81-call debit including unsuccessful setup attempts and one final complete live GraphQL snapshot. The snapshot projection changes transport field names only; issue bodies, comments, owners, numbers and timestamps are real public source data. All scratch clones are deleted."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T15:21:21.450Z"}],"before_hash":"8a9e54109e2c714fe4bf32c9a8e218f8f1c1eafc5da697fd65265ff91ffb5e82","after_hash":"60153dd611e118a4dcb6c2e19086588c660598a02609d9b9244f6e6362a0c133","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":"01b7015dc50c945f565db2f288ab8e0407b4ac289d6eacc2b18095d9f25a83f3"} +{"hash_algorithm":"sha256","ts":"2026-11-12T15:22:12.517Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/71","value":{"created_at":"2026-11-12T15:22:12.473Z","author":"codex-github230-1111","text":"Round 12 behavioral reverts in disposable package copies: atomic annotation target guard 1 failure; API owner-link matcher 4; verified legacy PR kind 1; retained refresh orphan detection 1; failure-path ancestor flush 1; complete recovery-payload privacy scan 4; cross-item ownership serialization 1; full-corpus scoped Project planning 1; native Project write freshness 1; inventory contract 1; README post-write contract 1. Total 17 expected failures across 11 isolated function/document reverts; no setup/import/syntax failures or cancellations. Fixed implementation restored: all 29 native Round-12 cases pass, including must-allow twins. The privacy payload matrix now uses a custom field retained in the durable match snapshot, ensuring that case tests the newly added whole-plan scan rather than duplicating the existing projected-description scan. The already-fixed N=40 Project index control passes with exactly 120 claim parses."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T15:22:12.517Z"}],"before_hash":"60153dd611e118a4dcb6c2e19086588c660598a02609d9b9244f6e6362a0c133","after_hash":"c9508c7a3ba7078a2f2a64a4945b3d8669ee89160146c3726964b5dc1024f89f","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":"49d14a1d7f1eda2d484304bd87ca7b17267749e523fd02ff201edd296ee3aa22"} +{"hash_algorithm":"sha256","ts":"2026-11-12T15:26:27.571Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/72","value":{"created_at":"2026-11-12T15:26:27.563Z","author":"codex-github230-1111","text":"The declared SDK floor correctly rejected the newly exported acquireLockLease API. Replaced that dependency with the floor-exported acquireLock API (verified in the published 2026.10.17 declarations), retaining tracker-wide serialization, native mutation ownership checks, reentrancy and guaranteed release. No dependency pin or minimum version changed. Re-running the packed minimum-SDK consumer, native Round-12 linked test, and affected compatibility files. The existing complete consumer probe executes a real non-git owned write and stale-revision refusal on the minimum SDK, in addition to linking all exports and compiling every source/declaration."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T15:26:27.571Z"}],"before_hash":"c9508c7a3ba7078a2f2a64a4945b3d8669ee89160146c3726964b5dc1024f89f","after_hash":"64b7e80e0a1769512304b515136c6c2147c761e96f56bd764fb6ed04266e3451","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":"a117d5c29646cb2e226cf7adfb207ec031d5f80c15ee5d1c509977794cb3b727"} +{"hash_algorithm":"sha256","ts":"2026-11-12T15:35:18.728Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/73","value":{"created_at":"2026-11-12T15:35:18.714Z","author":"codex-github230-1111","text":"Final SDK compatibility preserves newer-host generation fencing through typed feature detection and uses the exported acquireLock fallback on SDK 2026.10.17. The final packed minimum-SDK test passes 1/1, compiling all production/operational sources and declarations and exercising a native owned write plus stale-revision refusal. The current linked Round-12 command passes 29/29 again. All 17 behavioral revert failures remain proved after restoration; lint and current-SDK typecheck pass. Privacy diagnostics retain rule names and the existing FAIL before write wording; complete daily-sync and import-fidelity compatibility reruns pass (3/3 and 53 plus one existing quarantine). The remaining affected-file sweep continues under per-command limits."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T15:35:18.728Z"}],"before_hash":"64b7e80e0a1769512304b515136c6c2147c761e96f56bd764fb6ed04266e3451","after_hash":"76160ecabe4b8a44dd450912efe9985963807255004a102dbc060e4a148e059e","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":"da22e299ed7f74db9f61ab1312293d89607a801823fea83e718c774361fed591"} +{"hash_algorithm":"sha256","ts":"2026-11-12T15:55:23.695Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/74","value":{"created_at":"2026-11-12T15:55:23.633Z","author":"codex-github230-1111","text":"Round 12 privacy preflight preserves the shared redacted JSON finding envelope, including field names, offsets and hashes, while retaining the typed github_import_preflight_failed code. Added a native unsafe/clean receipt control; the linked Round-12 file now passes 30/30. Older recovery fixtures now sanitize sourceBody as well as rendered body before seeding a deliberately clean journal; unsafe atomic previews correctly refuse before staging. The 53-file sweep finished; failed compatibility cases are being replaced by successful complete or exact-name reruns. The workflow file's first command capped at 1200 seconds with 62 passes and two assertion failures; an explicit positive selector covers those failures and the nine unrun cases. A redundant negative selector matched the file container and was stopped, and its counts are excluded. A separately imported closeout decision-consumer file is also being exercised under the same 20-minute cap."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T15:55:23.695Z"}],"before_hash":"76160ecabe4b8a44dd450912efe9985963807255004a102dbc060e4a148e059e","after_hash":"562db75ec057d338a2d1422360709b82bfa2abc341bdf715be64a42268037ab3","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":"1e90a3f4faf78c3e71801dd0a0f1eb09be02cc619db35d8b7d8b683a89f998ed"} +{"hash_algorithm":"sha256","ts":"2026-11-12T16:26:46.255Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/75","value":{"created_at":"2026-11-12T16:26:46.246Z","author":"codex-github230-1111","text":"Round 12 final verification: 55 affected/required runnable files, 1640 distinct cases, 1638 passed, zero remaining failures or cancellations, two unchanged pm-cli#1656 quarantines (import-fidelity and refresh). Commands use timeout 1200 node --test --test-concurrency=1 TEST_FILE; at most two test workers. Import-gate completed 73/73 by retaining 62 passes and rerunning two failures plus nine unrun cases with explicit positive names. The separately imported closeout workflow consumer completed 187/187 with 171 retained passes and 16 remaining positive-name cases after its 1200-second cap. The shared native hook adapter removes six duplicated setup lines without changing guard callbacks; final-source native checks pass Round12 30/30 through pm, ownership matrix 142/142, import ownership 26/26, atomic 11/11, Round7 hooks 23/23, and packed minimum SDK 1/1. Lint, production/test typechecks, docstrings (308 declarations), and zero authored duplication pass. All 12 isolated behavioral/document reverts produce 18 expected failures; restoring the implementation passes 30/30. No threshold was reduced and no new test was quarantined."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T16:26:46.255Z"}],"before_hash":"562db75ec057d338a2d1422360709b82bfa2abc341bdf715be64a42268037ab3","after_hash":"7b974fbee9f91ad778c1b8de846a61d6a4637eb64903fa197c93fcdb48def4b1","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":"cad1f37d7a5eee1eef482ae46bdd8e535c7339d11965bcc769deefd2bb0f140b"} +{"hash_algorithm":"sha256","ts":"2026-11-12T16:26:47.222Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/76","value":{"created_at":"2026-11-12T16:26:47.215Z","author":"codex-github230-1111","text":"Round 12 final real-data receipt: packed code 64f79ca was installed and byte-checked in five fresh public scratch clones. Exact scheduled issue-only preview/import/repeat plus every post-write workflow verification passed: context 7 creates/3 updates; linear 8/1; review 3/1; brief 5/2 plus one unchanged; rl 6/0. Every post-write gate passed and every curated item/history git diff was empty. Both real-data passes used a conservative 82 API-call debit and 1225 cumulative active seconds, below the 25-minute check budget; all clones are deleted. Live public GitHub GraphQL snapshots were pagination-checked and projected to the read-only transport cache; native installed CLI commands and SDK persistence were real. The subsequent hook-only extraction was revalidated by the native guard suites and minimum-SDK probe. Read all 10 scoped/outdated threads and 10 full comments/prompts plus the outside README instruction; outdated l6Wk and l6W2 were still present at the starting head and are fixed, while l6Ws already used a shared index and its 40-record control proves 120 parses. The inventory now records 91 groups, 273 sites and 236 helper calls. All seven prior per-repo reference-tag cleanup workarounds remain unnecessary. Deferred findings stay in issue 236; no review reply or resolution was made. Full release, coverage and exact pushed-head CI remain uncertified locally. Releasing only this claim and retaining in_progress for orchestrator verification."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T16:26:47.222Z"}],"before_hash":"7b974fbee9f91ad778c1b8de846a61d6a4637eb64903fa197c93fcdb48def4b1","after_hash":"75784a24bb259e3a3e1b51a2cf2d1db7b97337375ca5c4ba4aaac05475fa1068","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":"b5b3b0ff36d31540a503e9c7d3eaa806ffadbf65226c441b910f1dbcdb631481"} +{"hash_algorithm":"sha256","ts":"2026-11-12T16:26:48.246Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-12T16:26:48.246Z"}],"before_hash":"75784a24bb259e3a3e1b51a2cf2d1db7b97337375ca5c4ba4aaac05475fa1068","after_hash":"50538d4de14301ad919a1a62a8a01111d8e2172716d9ceb9b9e9d1e3d9c8fc37","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":"5e0e74c1bf1ea4518ffc96c67517c64c0f1cb2e546763b86d8cc7aef5a110e9c"} +{"hash_algorithm":"sha256","ts":"2026-11-12T16:35:59.028Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-12T16:35:59.028Z"},{"op":"add","path":"/metadata/assignee","value":"codex-github230-1111"},{"op":"add","path":"/metadata/claim_principal","value":"codex-github230-1111"}],"before_hash":"50538d4de14301ad919a1a62a8a01111d8e2172716d9ceb9b9e9d1e3d9c8fc37","after_hash":"555149b7488ed28a1360465ea24743e4bd83336886985ebee445fbe5493c5b6a","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":"5943d9c627bf1b7cc2ae778227d4b839cbea2b64a6770c99b1cd7df04343e275"} +{"hash_algorithm":"sha256","ts":"2026-11-12T16:36:00.041Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/77","value":{"created_at":"2026-11-12T16:36:00.026Z","author":"codex-github230-1111","text":"Final additional local environment check: strict health refuses extension_host_pm_cli_version_skew because the package-pinned CLI is 2026.11.6 while the installed local pm-github extension host reports 2026.11.12. There are also six stale in-progress advisory items. Tracker authority, integrity and history checks are clean. This is not a passing local strict-health certification; no dependency pin or existing extension installation was changed. All five isolated scratch-clone strict-health checks passed. The final committed privacy audit passed 8902 Git objects with approved identities and no secret or host-path matches. Requested lint/typecheck/changelog and bounded test evidence remains passing; full release and exact-head CI certification remain pending. Releasing this bookkeeping claim and keeping the item in_progress."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T16:36:00.041Z"}],"before_hash":"555149b7488ed28a1360465ea24743e4bd83336886985ebee445fbe5493c5b6a","after_hash":"e9dfd9bbc0217aee5d6c9d09b57c2e440800ff242428e3ccf53978dcbde6aef6","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":"0f70abbfca3d31ee7b10c607fdc081427d232422e56b67ee953fb180c3be8f48"} +{"hash_algorithm":"sha256","ts":"2026-11-12T16:36:01.096Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-12T16:36:01.096Z"}],"before_hash":"e9dfd9bbc0217aee5d6c9d09b57c2e440800ff242428e3ccf53978dcbde6aef6","after_hash":"778722e8aba9ff939974960e6df62b634ac9e88fa031be7e8d9dbed486c2d42b","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":"f0e69cfe24d60e2d351703436da0494c1b6c5b4cebc88b51288c0bf1ff6f273d"} +{"hash_algorithm":"sha256","ts":"2026-11-12T16:52:34.581Z","author":"steve-orchestrator","author_source":"asserted","agent_harness":"claude-code","agent_model":"claude-opus-5-5","agent_model_source":"probe","agent_instance":"45d551c0e5ac79602511fdd7","agent_provenance":{"model":{"value":"claude-opus-5-5","source":"probe"},"effort":{"value":"xhigh","source":"environment"},"role":null,"topic":null,"version":{"value":"2.1.322","source":"probe"}},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/78","value":{"created_at":"2026-11-12T16:52:34.574Z","author":"steve-orchestrator","text":"Orchestrator, Greptile thread PRRT_kwDOTbN4pM62uYBV (projects.ts:631) on f82ecf5: buildProjectPullPlan built its issue lookup from the whole corpus, so a scoped --ids pull aborted when two unselected items claimed an unrelated issue. The lookup is now built from the selected items, still checked against the complete-corpus ownership index. Regression in test/round-twelve.test.ts: conflicts confined to unselected items no longer abort (failed before the fix with 'multiple existing owners'), and the must-refuse twin, an unselected peer claiming the selected item's own issue, still throws. round-twelve, projects, ownership-consistency and workspace-resolution 215/215; lint, typecheck, docstring and changelog:check pass. Orchestrator also repeated the scheduled sync end to end on f82ecf5 for pm-review (PR comment 6829934405): 3 creates and 1 update, all importer-created; repeat import a no-op; gate passes."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T16:52:34.581Z"}],"before_hash":"778722e8aba9ff939974960e6df62b634ac9e88fa031be7e8d9dbed486c2d42b","after_hash":"921c9a28acba2ca0f67612bf9c738afe582336e6bb9bc38ab8ea6c71b6e3128e","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"claude_session_file","rule_version":"v1"},"version":{"status":"resolved","resolver":"claude_session_file","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"4aebe3231fe3aae978664181a7b90c07f716a8441f2f7a70722d2bb9a00949e1"} +{"hash_algorithm":"sha256","ts":"2026-11-12T17:01:07.020Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-12T17:01:07.020Z"},{"op":"add","path":"/metadata/assignee","value":"codex-github230-1111"},{"op":"add","path":"/metadata/claim_principal","value":"codex-github230-1111"}],"before_hash":"921c9a28acba2ca0f67612bf9c738afe582336e6bb9bc38ab8ea6c71b6e3128e","after_hash":"1dbfe6edfe8520ec9fc2e26f65f98263fe734b1a35e9a9aa4b096027230127dd","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":"91c616334802a0e8e85286592f6db40a16e620e2ae0c07fe89d7cd46f2ab096c"} +{"hash_algorithm":"sha256","ts":"2026-11-12T17:04:26.957Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/79","value":{"created_at":"2026-11-12T17:04:26.945Z","author":"codex-github230-1111","text":"Round 13 starts from the orchestrator's 5f6749f on a clean worktree; origin/main remains 6ccb293 and is already an ancestor. Preserving the scoped Project pull fix. Read all three new threads, comments, proposed changes and AI prompts, with no remaining pages. Plan: classify pending-create recovery targets separately, report/defer them to import, and restrict lost-owner validation to selected refresh work; make Project fixture ownership explicit and add a valid reference-only command control. Published SDK audit finds acquireLockLease first exported in 2026.10.23, absent in 2026.10.22. Fleet lock pins all export it: review 2026.10.28, context/linear/brief 2026.11.4, rl 2026.11.3. Choosing the authorized minimum bump to 2026.10.23, deleting the unfenced fallback while retaining the development pin. Native SDK regressions, isolated reverts and the bounded requested gates follow."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T17:04:26.957Z"}],"before_hash":"1dbfe6edfe8520ec9fc2e26f65f98263fe734b1a35e9a9aa4b096027230127dd","after_hash":"1a3140aeacd5885b768453ee0efc06e41fbddbf12725c3bb479a2675fdb846d3","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":"bb71a5554233d24a65d27431dfb2cb696eeedb9a4bf993d5c9db7b31ddd5d5a1"} +{"hash_algorithm":"sha256","ts":"2026-11-12T17:16:56.514Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"docs_add","patch":[{"op":"remove","path":"/metadata/docs/5/note"},{"op":"replace","path":"/metadata/docs/5/path","value":"https://github.com/unbraind/pm-github/pull/233/checks"},{"op":"add","path":"/metadata/docs/4/note","value":"Implementation and scoped review evidence"},{"op":"replace","path":"/metadata/docs/4/path","value":"https://github.com/unbraind/pm-github/pull/233"},{"op":"remove","path":"/metadata/docs/3/note"},{"op":"replace","path":"/metadata/docs/3/path","value":"docs/sdk-minimum.md"},{"op":"add","path":"/metadata/docs/6","value":{"path":"README.md","scope":"project","note":"All owner claims must agree and remain unique; journal-aware cleanup"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T17:16:56.514Z"}],"before_hash":"1a3140aeacd5885b768453ee0efc06e41fbddbf12725c3bb479a2675fdb846d3","after_hash":"b2538ce038529a5c06ca23b783f4fb1e9902599f2779de96a4fd6ca102770b7c","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":"0e36293c651fc18c57eeaa50e29b343acc5e86bc3c81b79830aecf402fa26063"} +{"hash_algorithm":"sha256","ts":"2026-11-12T17:21:41.338Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/80","value":{"created_at":"2026-11-12T17:21:41.331Z","author":"codex-github230-1111","text":"Round 13 focused SDK-backed refresh regressions pass 6/6: pending creates are reported while unrelated items preview/apply, both with an SDK journal and with a restored producer plan alone; selected existing ownership loss refuses without item/history changes, while an unrelated scope proceeds. Recovery now uses validated original target IDs rather than entry.match alone, and deferred atomic-plan members are excluded from refresh writes. The Project fixture helper no longer infers ownership from tags; six issue-attachment call sites explicitly set github_url, with new draft/owner command controls. A type-only narrowing preserves the orchestrator's unselected-conflict regression while allowing test compilation."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T17:21:41.338Z"}],"before_hash":"b2538ce038529a5c06ca23b783f4fb1e9902599f2779de96a4fd6ca102770b7c","after_hash":"469c170f1ce49c7fd4875ea94713f8b65defa9089cb73b53275a0d3729730992","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":"ff866ae2aea0f679216b5f57d3731a152fb66211d94271078631f8cb8c32b0ba"} +{"hash_algorithm":"sha256","ts":"2026-11-12T17:21:42.210Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/78","value":{"command":"timeout 1200 node --test --test-concurrency=1 --test-name-pattern \"^(pending import creates|retained existing ownership loss)\" test/refresh.test.ts","scope":"project","provenance":{"author":"codex-github230-1111","created_at":"2026-11-12T17:21:42.143Z","source_kind":"local_mutation","source_ref":"fix/reference-tags-are-not-import-identity"}}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T17:21:42.210Z"}],"before_hash":"469c170f1ce49c7fd4875ea94713f8b65defa9089cb73b53275a0d3729730992","after_hash":"ccb545478ab6e1fd8c9a56f0d83ed17cf07b9b6c9d0628907417b41cf6dff863","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":"869fba58302b85cc9a857e6cd0c2b823da18071a2e7b196f7070429f0c541b91"} +{"hash_algorithm":"sha256","ts":"2026-11-12T17:26:00.094Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-12T17:26:00.094Z"},{"op":"replace","path":"/metadata/tags/0","value":"changelog-unreleased"},{"op":"add","path":"/metadata/tags/1","value":"gh:unbraind/pm-github#230"},{"op":"replace","path":"/metadata/title","value":"Keep GitHub reference tags separate from import identity (BREAKING: PM CLI 2026.10.23 minimum)"}],"before_hash":"ccb545478ab6e1fd8c9a56f0d83ed17cf07b9b6c9d0628907417b41cf6dff863","after_hash":"9df287750ecb8157470aef21825ccf7150e3a1274b361ed7591e1756aeb490bf","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":"7745a5e69e5b81ac42f6f9079fe549fe2acb90c9fab3a15f6235ad71df82044b"} +{"hash_algorithm":"sha256","ts":"2026-11-12T17:28:11.176Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/54/path","value":"test/workspace-resolution.test.ts"},{"op":"replace","path":"/metadata/files/53/path","value":"test/webhooks.test.ts"},{"op":"replace","path":"/metadata/files/52/path","value":"test/typed-sentinel-normalization.test.ts"},{"op":"replace","path":"/metadata/files/51/path","value":"test/smoke.test.ts"},{"op":"replace","path":"/metadata/files/50/path","value":"test/sdk-adoption.test.ts"},{"op":"replace","path":"/metadata/files/49/path","value":"test/scheduled-refusals.test.ts"},{"op":"replace","path":"/metadata/files/48/path","value":"test/round-twelve.test.ts"},{"op":"add","path":"/metadata/files/47/note","value":"Unified typed ownership refusal"},{"op":"replace","path":"/metadata/files/47/path","value":"test/round-seven.test.ts"},{"op":"replace","path":"/metadata/files/46/note","value":"Contradictory legacy claims refuse without duplicate import"},{"op":"replace","path":"/metadata/files/46/path","value":"test/review-round3-identity.test.ts"},{"op":"replace","path":"/metadata/files/45/note","value":"Unified typed write refusal with readable legacy diagnostics"},{"op":"replace","path":"/metadata/files/45/path","value":"test/review-import-identity.test.ts"},{"op":"remove","path":"/metadata/files/44/note"},{"op":"replace","path":"/metadata/files/44/path","value":"test/repeat-import-noop.test.ts"},{"op":"replace","path":"/metadata/files/43/path","value":"test/refresh.test.ts"},{"op":"add","path":"/metadata/files/42/note","value":"Contradictory linked references refuse without mutation"},{"op":"replace","path":"/metadata/files/42/path","value":"test/reference-import-identity.test.ts"},{"op":"remove","path":"/metadata/files/41/note"},{"op":"replace","path":"/metadata/files/41/path","value":"test/project-sync.test.ts"},{"op":"add","path":"/metadata/files/40/note","value":"Creator proof and all changed tracker record reference audits"},{"op":"replace","path":"/metadata/files/40/path","value":"test/ownership-consistency.test.ts"},{"op":"replace","path":"/metadata/files/39/note","value":"Packed SDK floor typecheck and guarded write runtime probe"},{"op":"replace","path":"/metadata/files/39/path","value":"test/minimum-sdk-consumer.test.ts"},{"op":"remove","path":"/metadata/files/38/note"},{"op":"replace","path":"/metadata/files/38/path","value":"test/link-deps.test.ts"},{"op":"replace","path":"/metadata/files/37/path","value":"test/label-import-model.test.ts"},{"op":"replace","path":"/metadata/files/36/path","value":"test/import-projection.test.ts"},{"op":"add","path":"/metadata/files/35/note","value":"Native SDK revision and independent peer fixtures"},{"op":"replace","path":"/metadata/files/35/path","value":"test/import-ownership.test.ts"},{"op":"remove","path":"/metadata/files/34/note"},{"op":"replace","path":"/metadata/files/34/path","value":"test/import-lock.test.ts"},{"op":"add","path":"/metadata/files/33/note","value":"Real SDK dry-run fixtures and typed competing-owner refusal"},{"op":"replace","path":"/metadata/files/33/path","value":"test/import-gate.test.ts"},{"op":"remove","path":"/metadata/files/32/note"},{"op":"replace","path":"/metadata/files/32/path","value":"test/import-fidelity.test.ts"},{"op":"replace","path":"/metadata/files/31/path","value":"test/import-errors.test.ts"},{"op":"replace","path":"/metadata/files/30/path","value":"test/import-conflict-report.test.ts"},{"op":"replace","path":"/metadata/files/29/path","value":"test/identity.test.ts"},{"op":"replace","path":"/metadata/files/28/path","value":"test/helpers/import-preview.ts"},{"op":"add","path":"/metadata/files/27/note","value":"Real importer creation provenance in mirror fixtures"},{"op":"replace","path":"/metadata/files/27/path","value":"test/helpers/import-mirror.ts"},{"op":"remove","path":"/metadata/files/26/note"},{"op":"replace","path":"/metadata/files/26/path","value":"test/handler-failures.test.ts"},{"op":"replace","path":"/metadata/files/25/path","value":"test/github-url.test.ts"},{"op":"replace","path":"/metadata/files/24/path","value":"test/fixtures/refresh-crash.ts"},{"op":"replace","path":"/metadata/files/23/path","value":"test/fixtures/ownership-peer.ts"},{"op":"replace","path":"/metadata/files/22/path","value":"test/fixtures/atomic-plan-crash.ts"},{"op":"replace","path":"/metadata/files/21/path","value":"test/durable-state.test.ts"},{"op":"replace","path":"/metadata/files/20/path","value":"test/durable-recovery.test.ts"},{"op":"replace","path":"/metadata/files/19/path","value":"test/dryrun-preview.test.ts"},{"op":"replace","path":"/metadata/files/18/path","value":"test/daily-sync-idempotency.test.ts"},{"op":"add","path":"/metadata/files/17/note","value":"Native SDK policy refusal fixtures for guarded plain writes"},{"op":"replace","path":"/metadata/files/17/path","value":"test/coverage-remainder.test.ts"},{"op":"remove","path":"/metadata/files/16/note"},{"op":"replace","path":"/metadata/files/16/path","value":"test/coverage-edges.test.ts"},{"op":"replace","path":"/metadata/files/15/path","value":"test/compatibility-floor.test.ts"},{"op":"add","path":"/metadata/files/55","value":{"path":"webhooks.ts","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T17:28:11.176Z"}],"before_hash":"9df287750ecb8157470aef21825ccf7150e3a1274b361ed7591e1756aeb490bf","after_hash":"cc56120b8df411abb3be5465d4647e7504aed9d2fe83159a21aed6c970460f90","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":"f7e14fa9c43156872b5f763f8e4debd900881875e4c705fa70f6c8fac334da66"} +{"hash_algorithm":"sha256","ts":"2026-11-12T17:31:46.379Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/55/path","value":"test/workspace-resolution.test.ts"},{"op":"replace","path":"/metadata/files/54/path","value":"test/webhooks.test.ts"},{"op":"replace","path":"/metadata/files/53/path","value":"test/typed-sentinel-normalization.test.ts"},{"op":"replace","path":"/metadata/files/52/path","value":"test/smoke.test.ts"},{"op":"replace","path":"/metadata/files/51/path","value":"test/sdk-adoption.test.ts"},{"op":"replace","path":"/metadata/files/50/path","value":"test/scheduled-refusals.test.ts"},{"op":"replace","path":"/metadata/files/49/path","value":"test/round-twelve.test.ts"},{"op":"add","path":"/metadata/files/48/note","value":"Unified typed ownership refusal"},{"op":"replace","path":"/metadata/files/48/path","value":"test/round-seven.test.ts"},{"op":"replace","path":"/metadata/files/47/note","value":"Contradictory legacy claims refuse without duplicate import"},{"op":"replace","path":"/metadata/files/47/path","value":"test/review-round3-identity.test.ts"},{"op":"replace","path":"/metadata/files/46/note","value":"Unified typed write refusal with readable legacy diagnostics"},{"op":"replace","path":"/metadata/files/46/path","value":"test/review-import-identity.test.ts"},{"op":"remove","path":"/metadata/files/45/note"},{"op":"replace","path":"/metadata/files/45/path","value":"test/repeat-import-noop.test.ts"},{"op":"replace","path":"/metadata/files/44/path","value":"test/refresh.test.ts"},{"op":"add","path":"/metadata/files/43/note","value":"Contradictory linked references refuse without mutation"},{"op":"replace","path":"/metadata/files/43/path","value":"test/reference-import-identity.test.ts"},{"op":"remove","path":"/metadata/files/42/note"},{"op":"replace","path":"/metadata/files/42/path","value":"test/project-sync.test.ts"},{"op":"add","path":"/metadata/files/41/note","value":"Creator proof and all changed tracker record reference audits"},{"op":"replace","path":"/metadata/files/41/path","value":"test/ownership-consistency.test.ts"},{"op":"replace","path":"/metadata/files/40/note","value":"Packed SDK floor typecheck and guarded write runtime probe"},{"op":"replace","path":"/metadata/files/40/path","value":"test/minimum-sdk-consumer.test.ts"},{"op":"remove","path":"/metadata/files/39/note"},{"op":"replace","path":"/metadata/files/39/path","value":"test/link-deps.test.ts"},{"op":"replace","path":"/metadata/files/38/path","value":"test/label-import-model.test.ts"},{"op":"replace","path":"/metadata/files/37/path","value":"test/import-projection.test.ts"},{"op":"add","path":"/metadata/files/36/note","value":"Native SDK revision and independent peer fixtures"},{"op":"replace","path":"/metadata/files/36/path","value":"test/import-ownership.test.ts"},{"op":"remove","path":"/metadata/files/35/note"},{"op":"replace","path":"/metadata/files/35/path","value":"test/import-lock.test.ts"},{"op":"add","path":"/metadata/files/34/note","value":"Real SDK dry-run fixtures and typed competing-owner refusal"},{"op":"replace","path":"/metadata/files/34/path","value":"test/import-gate.test.ts"},{"op":"remove","path":"/metadata/files/33/note"},{"op":"replace","path":"/metadata/files/33/path","value":"test/import-fidelity.test.ts"},{"op":"replace","path":"/metadata/files/32/path","value":"test/import-errors.test.ts"},{"op":"replace","path":"/metadata/files/31/path","value":"test/import-conflict-report.test.ts"},{"op":"replace","path":"/metadata/files/30/path","value":"test/identity.test.ts"},{"op":"replace","path":"/metadata/files/29/path","value":"test/helpers/import-preview.ts"},{"op":"add","path":"/metadata/files/28/note","value":"Real importer creation provenance in mirror fixtures"},{"op":"replace","path":"/metadata/files/28/path","value":"test/helpers/import-mirror.ts"},{"op":"remove","path":"/metadata/files/27/note"},{"op":"replace","path":"/metadata/files/27/path","value":"test/handler-failures.test.ts"},{"op":"replace","path":"/metadata/files/26/path","value":"test/github-url.test.ts"},{"op":"replace","path":"/metadata/files/25/path","value":"test/fixtures/refresh-crash.ts"},{"op":"replace","path":"/metadata/files/24/path","value":"test/fixtures/ownership-peer.ts"},{"op":"replace","path":"/metadata/files/23/path","value":"test/fixtures/atomic-plan-crash.ts"},{"op":"replace","path":"/metadata/files/22/path","value":"test/durable-state.test.ts"},{"op":"replace","path":"/metadata/files/21/path","value":"test/durable-recovery.test.ts"},{"op":"replace","path":"/metadata/files/20/path","value":"test/dryrun-preview.test.ts"},{"op":"replace","path":"/metadata/files/19/path","value":"test/daily-sync-idempotency.test.ts"},{"op":"add","path":"/metadata/files/18/note","value":"Native SDK policy refusal fixtures for guarded plain writes"},{"op":"replace","path":"/metadata/files/18/path","value":"test/coverage-remainder.test.ts"},{"op":"remove","path":"/metadata/files/17/note"},{"op":"replace","path":"/metadata/files/17/path","value":"test/coverage-edges.test.ts"},{"op":"replace","path":"/metadata/files/16/path","value":"test/compatibility-floor.test.ts"},{"op":"replace","path":"/metadata/files/15/path","value":"test/cited-issue-adoption.test.ts"},{"op":"replace","path":"/metadata/files/14/path","value":"test/blockers.test.ts"},{"op":"replace","path":"/metadata/files/13/path","value":"test/atomic.test.ts"},{"op":"replace","path":"/metadata/files/12/path","value":"refresh.ts"},{"op":"replace","path":"/metadata/files/11/path","value":"projects.ts"},{"op":"replace","path":"/metadata/files/10/path","value":"manifest.json"},{"op":"add","path":"/metadata/files/56","value":{"path":"webhooks.ts","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T17:31:46.379Z"}],"before_hash":"cc56120b8df411abb3be5465d4647e7504aed9d2fe83159a21aed6c970460f90","after_hash":"0f639d7f606cc960458365a9541d4786bbbbb4a93cc9b736a852245e6b018251","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":"e9246f33b01e82924ec720689316cdb898e6f7013becbe28edd55ec75c4ff36f"} +{"hash_algorithm":"sha256","ts":"2026-11-12T17:31:47.672Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/56/path","value":"test/workspace-resolution.test.ts"},{"op":"replace","path":"/metadata/files/55/path","value":"test/webhooks.test.ts"},{"op":"replace","path":"/metadata/files/54/path","value":"test/typed-sentinel-normalization.test.ts"},{"op":"replace","path":"/metadata/files/53/path","value":"test/smoke.test.ts"},{"op":"replace","path":"/metadata/files/52/path","value":"test/sdk-adoption.test.ts"},{"op":"replace","path":"/metadata/files/51/path","value":"test/scheduled-refusals.test.ts"},{"op":"replace","path":"/metadata/files/50/path","value":"test/round-twelve.test.ts"},{"op":"add","path":"/metadata/files/49/note","value":"Unified typed ownership refusal"},{"op":"replace","path":"/metadata/files/49/path","value":"test/round-seven.test.ts"},{"op":"replace","path":"/metadata/files/48/note","value":"Contradictory legacy claims refuse without duplicate import"},{"op":"replace","path":"/metadata/files/48/path","value":"test/review-round3-identity.test.ts"},{"op":"replace","path":"/metadata/files/47/note","value":"Unified typed write refusal with readable legacy diagnostics"},{"op":"replace","path":"/metadata/files/47/path","value":"test/review-import-identity.test.ts"},{"op":"remove","path":"/metadata/files/46/note"},{"op":"replace","path":"/metadata/files/46/path","value":"test/repeat-import-noop.test.ts"},{"op":"replace","path":"/metadata/files/45/path","value":"test/refresh.test.ts"},{"op":"add","path":"/metadata/files/44/note","value":"Contradictory linked references refuse without mutation"},{"op":"replace","path":"/metadata/files/44/path","value":"test/reference-import-identity.test.ts"},{"op":"remove","path":"/metadata/files/43/note"},{"op":"replace","path":"/metadata/files/43/path","value":"test/project-sync.test.ts"},{"op":"add","path":"/metadata/files/42/note","value":"Creator proof and all changed tracker record reference audits"},{"op":"replace","path":"/metadata/files/42/path","value":"test/ownership-consistency.test.ts"},{"op":"replace","path":"/metadata/files/41/note","value":"Packed SDK floor typecheck and guarded write runtime probe"},{"op":"replace","path":"/metadata/files/41/path","value":"test/minimum-sdk-consumer.test.ts"},{"op":"remove","path":"/metadata/files/40/note"},{"op":"replace","path":"/metadata/files/40/path","value":"test/link-deps.test.ts"},{"op":"replace","path":"/metadata/files/39/path","value":"test/label-import-model.test.ts"},{"op":"replace","path":"/metadata/files/38/path","value":"test/import-projection.test.ts"},{"op":"add","path":"/metadata/files/37/note","value":"Native SDK revision and independent peer fixtures"},{"op":"replace","path":"/metadata/files/37/path","value":"test/import-ownership.test.ts"},{"op":"remove","path":"/metadata/files/36/note"},{"op":"replace","path":"/metadata/files/36/path","value":"test/import-lock.test.ts"},{"op":"add","path":"/metadata/files/35/note","value":"Real SDK dry-run fixtures and typed competing-owner refusal"},{"op":"replace","path":"/metadata/files/35/path","value":"test/import-gate.test.ts"},{"op":"remove","path":"/metadata/files/34/note"},{"op":"replace","path":"/metadata/files/34/path","value":"test/import-fidelity.test.ts"},{"op":"replace","path":"/metadata/files/33/path","value":"test/import-errors.test.ts"},{"op":"replace","path":"/metadata/files/32/path","value":"test/import-conflict-report.test.ts"},{"op":"replace","path":"/metadata/files/31/path","value":"test/identity.test.ts"},{"op":"replace","path":"/metadata/files/30/path","value":"test/helpers/import-preview.ts"},{"op":"add","path":"/metadata/files/29/note","value":"Real importer creation provenance in mirror fixtures"},{"op":"replace","path":"/metadata/files/29/path","value":"test/helpers/import-mirror.ts"},{"op":"remove","path":"/metadata/files/28/note"},{"op":"replace","path":"/metadata/files/28/path","value":"test/handler-failures.test.ts"},{"op":"replace","path":"/metadata/files/27/path","value":"test/github-url.test.ts"},{"op":"replace","path":"/metadata/files/26/path","value":"test/fixtures/refresh-crash.ts"},{"op":"replace","path":"/metadata/files/25/path","value":"test/fixtures/ownership-peer.ts"},{"op":"replace","path":"/metadata/files/24/path","value":"test/fixtures/atomic-plan-crash.ts"},{"op":"replace","path":"/metadata/files/23/path","value":"test/durable-state.test.ts"},{"op":"replace","path":"/metadata/files/22/path","value":"test/durable-recovery.test.ts"},{"op":"replace","path":"/metadata/files/21/path","value":"test/dryrun-preview.test.ts"},{"op":"replace","path":"/metadata/files/20/path","value":"test/daily-sync-idempotency.test.ts"},{"op":"add","path":"/metadata/files/19/note","value":"Native SDK policy refusal fixtures for guarded plain writes"},{"op":"replace","path":"/metadata/files/19/path","value":"test/coverage-remainder.test.ts"},{"op":"remove","path":"/metadata/files/18/note"},{"op":"replace","path":"/metadata/files/18/path","value":"test/coverage-edges.test.ts"},{"op":"replace","path":"/metadata/files/17/path","value":"test/compatibility-floor.test.ts"},{"op":"replace","path":"/metadata/files/16/path","value":"test/cited-issue-adoption.test.ts"},{"op":"replace","path":"/metadata/files/15/path","value":"test/blockers.test.ts"},{"op":"replace","path":"/metadata/files/14/path","value":"test/atomic.test.ts"},{"op":"replace","path":"/metadata/files/13/path","value":"refresh.ts"},{"op":"replace","path":"/metadata/files/12/path","value":"projects.ts"},{"op":"replace","path":"/metadata/files/11/path","value":"package.json"},{"op":"add","path":"/metadata/files/57","value":{"path":"webhooks.ts","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T17:31:47.672Z"}],"before_hash":"0f639d7f606cc960458365a9541d4786bbbbb4a93cc9b736a852245e6b018251","after_hash":"3595599dd2a0877aa398e426ff6d30ddcc1f33d19ec9b57245f3b9c950f12a6c","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":"d54523d83d95319e11aab874267e7c8614c3339b327b3a204716af6fb91997a5"} +{"hash_algorithm":"sha256","ts":"2026-11-12T17:31:48.979Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/57/path","value":"test/workspace-resolution.test.ts"},{"op":"replace","path":"/metadata/files/56/path","value":"test/webhooks.test.ts"},{"op":"replace","path":"/metadata/files/55/path","value":"test/typed-sentinel-normalization.test.ts"},{"op":"replace","path":"/metadata/files/54/path","value":"test/smoke.test.ts"},{"op":"replace","path":"/metadata/files/53/path","value":"test/sdk-adoption.test.ts"},{"op":"replace","path":"/metadata/files/52/path","value":"test/scheduled-refusals.test.ts"},{"op":"replace","path":"/metadata/files/51/path","value":"test/round-twelve.test.ts"},{"op":"add","path":"/metadata/files/50/note","value":"Unified typed ownership refusal"},{"op":"replace","path":"/metadata/files/50/path","value":"test/round-seven.test.ts"},{"op":"replace","path":"/metadata/files/49/note","value":"Contradictory legacy claims refuse without duplicate import"},{"op":"replace","path":"/metadata/files/49/path","value":"test/review-round3-identity.test.ts"},{"op":"replace","path":"/metadata/files/48/note","value":"Unified typed write refusal with readable legacy diagnostics"},{"op":"replace","path":"/metadata/files/48/path","value":"test/review-import-identity.test.ts"},{"op":"remove","path":"/metadata/files/47/note"},{"op":"replace","path":"/metadata/files/47/path","value":"test/repeat-import-noop.test.ts"},{"op":"replace","path":"/metadata/files/46/path","value":"test/refresh.test.ts"},{"op":"add","path":"/metadata/files/45/note","value":"Contradictory linked references refuse without mutation"},{"op":"replace","path":"/metadata/files/45/path","value":"test/reference-import-identity.test.ts"},{"op":"remove","path":"/metadata/files/44/note"},{"op":"replace","path":"/metadata/files/44/path","value":"test/project-sync.test.ts"},{"op":"add","path":"/metadata/files/43/note","value":"Creator proof and all changed tracker record reference audits"},{"op":"replace","path":"/metadata/files/43/path","value":"test/ownership-consistency.test.ts"},{"op":"replace","path":"/metadata/files/42/note","value":"Packed SDK floor typecheck and guarded write runtime probe"},{"op":"replace","path":"/metadata/files/42/path","value":"test/minimum-sdk-consumer.test.ts"},{"op":"remove","path":"/metadata/files/41/note"},{"op":"replace","path":"/metadata/files/41/path","value":"test/link-deps.test.ts"},{"op":"replace","path":"/metadata/files/40/path","value":"test/label-import-model.test.ts"},{"op":"replace","path":"/metadata/files/39/path","value":"test/import-projection.test.ts"},{"op":"add","path":"/metadata/files/38/note","value":"Native SDK revision and independent peer fixtures"},{"op":"replace","path":"/metadata/files/38/path","value":"test/import-ownership.test.ts"},{"op":"remove","path":"/metadata/files/37/note"},{"op":"replace","path":"/metadata/files/37/path","value":"test/import-lock.test.ts"},{"op":"add","path":"/metadata/files/36/note","value":"Real SDK dry-run fixtures and typed competing-owner refusal"},{"op":"replace","path":"/metadata/files/36/path","value":"test/import-gate.test.ts"},{"op":"remove","path":"/metadata/files/35/note"},{"op":"replace","path":"/metadata/files/35/path","value":"test/import-fidelity.test.ts"},{"op":"replace","path":"/metadata/files/34/path","value":"test/import-errors.test.ts"},{"op":"replace","path":"/metadata/files/33/path","value":"test/import-conflict-report.test.ts"},{"op":"replace","path":"/metadata/files/32/path","value":"test/identity.test.ts"},{"op":"replace","path":"/metadata/files/31/path","value":"test/helpers/import-preview.ts"},{"op":"add","path":"/metadata/files/30/note","value":"Real importer creation provenance in mirror fixtures"},{"op":"replace","path":"/metadata/files/30/path","value":"test/helpers/import-mirror.ts"},{"op":"remove","path":"/metadata/files/29/note"},{"op":"replace","path":"/metadata/files/29/path","value":"test/handler-failures.test.ts"},{"op":"replace","path":"/metadata/files/28/path","value":"test/github-url.test.ts"},{"op":"replace","path":"/metadata/files/27/path","value":"test/fixtures/refresh-crash.ts"},{"op":"replace","path":"/metadata/files/26/path","value":"test/fixtures/ownership-peer.ts"},{"op":"replace","path":"/metadata/files/25/path","value":"test/fixtures/atomic-plan-crash.ts"},{"op":"replace","path":"/metadata/files/24/path","value":"test/durable-state.test.ts"},{"op":"replace","path":"/metadata/files/23/path","value":"test/durable-recovery.test.ts"},{"op":"replace","path":"/metadata/files/22/path","value":"test/dryrun-preview.test.ts"},{"op":"replace","path":"/metadata/files/21/path","value":"test/daily-sync-idempotency.test.ts"},{"op":"add","path":"/metadata/files/20/note","value":"Native SDK policy refusal fixtures for guarded plain writes"},{"op":"replace","path":"/metadata/files/20/path","value":"test/coverage-remainder.test.ts"},{"op":"remove","path":"/metadata/files/19/note"},{"op":"replace","path":"/metadata/files/19/path","value":"test/coverage-edges.test.ts"},{"op":"replace","path":"/metadata/files/18/path","value":"test/compatibility-floor.test.ts"},{"op":"replace","path":"/metadata/files/17/path","value":"test/cited-issue-adoption.test.ts"},{"op":"replace","path":"/metadata/files/16/path","value":"test/blockers.test.ts"},{"op":"replace","path":"/metadata/files/15/path","value":"test/atomic.test.ts"},{"op":"replace","path":"/metadata/files/14/path","value":"refresh.ts"},{"op":"replace","path":"/metadata/files/13/path","value":"projects.ts"},{"op":"replace","path":"/metadata/files/12/path","value":"package.json"},{"op":"replace","path":"/metadata/files/11/path","value":"package-lock.json"},{"op":"add","path":"/metadata/files/58","value":{"path":"webhooks.ts","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T17:31:48.979Z"}],"before_hash":"3595599dd2a0877aa398e426ff6d30ddcc1f33d19ec9b57245f3b9c950f12a6c","after_hash":"d1bb96ce2757731b190c984615a06912627ffdb1c87412364575e7278bb98cdf","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":"97b3b3beaef0f102c394d009e41ddc36b01a2f9c91cef924e9346d3472579915"} +{"hash_algorithm":"sha256","ts":"2026-11-12T17:31:50.246Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/81","value":{"created_at":"2026-11-12T17:31:50.235Z","author":"codex-github230-1111","text":"Round 13 review evidence: read all three full threads and AI-agent prompts (PRRT_kwDOTbN4pM62uujJ, PRRT_kwDOTbN4pM62uui6, PRRT_kwDOTbN4pM62uujV), three comments, no truncated comment pages. origin/main remains 6ccb293 and is an ancestor; preserve orchestrator scoped-project change 5f6749f. Pending CREATE plans are deferred with transaction, item IDs and import resume argv; unrelated previews/applies proceed with or without an SDK journal. Existing targets that lose ownership refuse when selected, with item/history bytes unchanged. Persisted canonical target IDs also scope import recovery, preventing an unrelated refresh from replaying a journal-free CREATE. Project fixtures now set explicit ownership at their six issue-attachment call sites; valid mention-only tags plan drafts, explicit owners plan attachments. The new minimum is a deliberate breaking contract, disclosed in the generated changelog; the development SDK pin is unchanged."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T17:31:50.246Z"}],"before_hash":"d1bb96ce2757731b190c984615a06912627ffdb1c87412364575e7278bb98cdf","after_hash":"072a07b1674f0614609e6abf35a960729bc8f22281cbea858e2a362a5afc92cf","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":"31bc49bd38584a4566dda89afc34b336e63d8a002f298ca3401faf9d3da46015"} +{"hash_algorithm":"sha256","ts":"2026-11-12T17:35:00.881Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/82","value":{"created_at":"2026-11-12T17:35:00.873Z","author":"codex-github230-1111","text":"Round 13 revert proof in a disposable package copy, based on 5f6749f: reverting only runRefresh gives 4/4 pending-create failures and 1/1 unselected-orphan failure; its selected-orphan refusal control passes. Reverting only resumePendingGithubImport gives 1/1 journal-free unrelated-refresh failure because the pending plan is replayed. Reverting only the Project fixture create helper gives 1/1 reference-only draft failure; explicit-owner control passes. Reverting the minimum declarations to 2026.10.17 gives 1/1 exact-floor contract failure, before consumer installation. This last proof is a declaration revert, not a function-only lease test: the old helper also uses leases when available. Total 8 expected failures, 2 controls pass. Restoring all fixes passes refresh 6/6, Project controls 2/2 and floor declarations 4/4; the real packed consumer separately passes at 2026.10.23, including live-lease success, released-lease lock_lost without callback, owned mutation and stale-revision refusal. Commands: timeout 1200 node --test --test-concurrency=1 --test-name-pattern \"^(pending import creates|retained existing ownership loss)\" test/refresh.test.ts; timeout 1200 node --test --test-concurrency=1 --test-name-pattern \"^project sync keeps a valid reference-only\" test/project-sync.test.ts; timeout 1200 npm run test:minimum-sdk."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T17:35:00.881Z"}],"before_hash":"072a07b1674f0614609e6abf35a960729bc8f22281cbea858e2a362a5afc92cf","after_hash":"50f5feccfd0b33097c43a05c468cf968c897ec2906c6b621e571b067b494e670","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":"c32a7a124d8f5c75110e8ba3b43ced82113bf08da3925ad807bbc42c6d4c66f7"} +{"hash_algorithm":"sha256","ts":"2026-11-12T17:36:18.238Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/79","value":{"command":"timeout 1200 node --test --test-concurrency=1 test/project-sync.test.ts","scope":"project","provenance":{"author":"codex-github230-1111","created_at":"2026-11-12T17:36:18.168Z","source_kind":"local_mutation","source_ref":"fix/reference-tags-are-not-import-identity"}}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T17:36:18.238Z"}],"before_hash":"50f5feccfd0b33097c43a05c468cf968c897ec2906c6b621e571b067b494e670","after_hash":"355cf752c3d929073b9b9515dac87c8d1ba31e41b4233defad61cf18367cf017","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":"cc07ba19a966a67ab0ee8f3c6321a1801b4a03ef143ff34c980cb28cf050d99e"} +{"hash_algorithm":"sha256","ts":"2026-11-12T17:36:19.242Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/80","value":{"command":"timeout 1200 node --test --test-concurrency=1 test/compatibility-floor.test.ts","scope":"project","provenance":{"author":"codex-github230-1111","created_at":"2026-11-12T17:36:19.170Z","source_kind":"local_mutation","source_ref":"fix/reference-tags-are-not-import-identity"}}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T17:36:19.242Z"}],"before_hash":"355cf752c3d929073b9b9515dac87c8d1ba31e41b4233defad61cf18367cf017","after_hash":"f08c781b304374d82496529f2950dd0bf0376e0b5a13ff98caf46468ecf8c427","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":"7c2386937246a50d174115af7ac7885154746824468bd8001b0f05e73f3d91a6"} +{"hash_algorithm":"sha256","ts":"2026-11-12T17:36:20.176Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/83","value":{"created_at":"2026-11-12T17:36:20.170Z","author":"codex-github230-1111","text":"Round 13 required test inventory: 7 files, 267 cases, 266 passes, one unchanged pm-cli#1656 recovered-compensation quarantine, zero failures or cancellations; each command capped at 20 minutes, at most two test workers. The final refresh rerun and final packed minimum-SDK rerun also pass after the immutable initial-selection lint correction. Exact commands/results:\ntimeout 1200 node --test --test-concurrency=1 test/refresh.test.ts => 40 passed, 1 skipped\ntimeout 1200 node --test --test-concurrency=1 test/import-ownership.test.ts => 26 passed, 0 skipped\ntimeout 1200 node --test --test-concurrency=1 test/ownership-consistency.test.ts => 142 passed, 0 skipped\ntimeout 1200 node --test --test-concurrency=1 test/round-twelve.test.ts => 32 passed, 0 skipped\ntimeout 1200 node --test --test-concurrency=1 test/project-sync.test.ts => 21 passed, 0 skipped\ntimeout 1200 npm run test:minimum-sdk => 1 passed, 0 skipped\ntimeout 1200 node --test --test-concurrency=1 test/compatibility-floor.test.ts => 4 passed, 0 skipped\nThe new refresh command was also run through pm test --only-last --run --progress: 6/6 passed. timeout 1200 npm run lint, timeout 1200 npm run typecheck, timeout 1200 npm run build:test and timeout 1200 npm run docstring pass; docstrings cover 32 files and 308 declarations. Inventory regenerated: 91 groups, 273 sites, 236 calls. Optional zero-threshold duplication check remains red only for the unchanged orchestrator-base projects.ts clone; recorded on pm-github-reference-identity-followup. CI certifies the full release/coverage suite. The claim will be released, without closure, before the final generated changelog check and commit."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T17:36:20.176Z"}],"before_hash":"f08c781b304374d82496529f2950dd0bf0376e0b5a13ff98caf46468ecf8c427","after_hash":"6ed184521acb723f7cd0f32d6d46273dd6a32f63b61bc86f1da164f19e28dc31","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":"14212a02b06a285241a4ad98ad480356192c5a5d32594f26e8d1a787904ebf55"} +{"hash_algorithm":"sha256","ts":"2026-11-12T17:36:47.105Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/84","value":{"created_at":"2026-11-12T17:36:47.097Z","author":"codex-github230-1111","text":"Final round-13 safety evidence: timeout 1200 npm run privacy passes (8921 objects audited; approved identities, no secret or host-path matches). origin/main was refreshed again and remains 6ccb293, already merged as an ancestor. All three review threads were read in full; none were replied to, resolved or reacted to. The two pm items touched are pm-github-reference-identity and its existing follow-up, where the unchanged base duplication finding is recorded. Release only this author claim; keep the main item in_progress and the follow-up open. Changelog generation/check and explicit-path commit follow this final tracker write."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T17:36:47.105Z"}],"before_hash":"6ed184521acb723f7cd0f32d6d46273dd6a32f63b61bc86f1da164f19e28dc31","after_hash":"d1875e3bb209981bfe6c83e84288b6d6034052db19ba1d5cd096b81a8f8752e1","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":"e1e9be7b9ba7599f665c83ebf8e58829841b1231b3522544b3a948dc6cbd8da1"} +{"hash_algorithm":"sha256","ts":"2026-11-12T17:36:48.065Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-12T17:36:48.065Z"}],"before_hash":"d1875e3bb209981bfe6c83e84288b6d6034052db19ba1d5cd096b81a8f8752e1","after_hash":"45c7bc401e251102a23bf21f8207fac42d11a9705e16c9ea8293cde77a6e4942","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":"9838cef4dc82dab819934e7cf78e7d5d10440e038aafd95bb0d41ef0d7013092"} +{"hash_algorithm":"sha256","ts":"2026-11-12T17:43:51.048Z","author":"steve-orchestrator","author_source":"asserted","agent_harness":"claude-code","agent_model":"claude-opus-5-5","agent_model_source":"probe","agent_instance":"45d551c0e5ac79602511fdd7","agent_provenance":{"model":{"value":"claude-opus-5-5","source":"probe"},"effort":{"value":"xhigh","source":"environment"},"role":null,"topic":null,"version":{"value":"2.1.322","source":"probe"}},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/85","value":{"created_at":"2026-11-12T17:43:51.039Z","author":"steve-orchestrator","text":"Orchestrator: 5f6749f duplicated the board ownership-index setup between buildProjectPullPlan and buildProjectImportPlan (one 5-line exact clone), which fails the duplication gate inside release:check. Both now call one documented helper, indexSelectedByIssue(selected, corpus, projectItems). Behaviour is unchanged: the import plan passes pmItems as both selection and corpus. Duplication gate: zero duplicated lines; typecheck, lint and docstring pass; round-twelve, projects, ownership-consistency, workspace-resolution and project-sync 236/236. Also audited the new SDK floor 2026.10.23 against every fleet checkout's locked pm: every pm-github sync caller locks 2026.11.5 or newer; only pm-observer (2026.10.18) is lower, and it has no sync workflow."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T17:43:51.048Z"}],"before_hash":"45c7bc401e251102a23bf21f8207fac42d11a9705e16c9ea8293cde77a6e4942","after_hash":"cd35ed8ac3ff1f9ec6b1a55a09d0e06f8c9933f38bd9bae878465928269d9feb","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"claude_session_file","rule_version":"v1"},"version":{"status":"resolved","resolver":"claude_session_file","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"f5a6b38b74f462f00f4e76923ce4316bebb8541389eb992e885525eb99782528"} +{"hash_algorithm":"sha256","ts":"2026-11-12T19:38:07.028Z","author":"steve-orchestrator","author_source":"asserted","agent_harness":"claude-code","agent_model":"claude-opus-5-5","agent_model_source":"probe","agent_instance":"45d551c0e5ac79602511fdd7","agent_provenance":{"model":{"value":"claude-opus-5-5","source":"probe"},"effort":{"value":"xhigh","source":"environment"},"role":null,"topic":null,"version":{"value":"2.1.322","source":"probe"}},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/86","value":{"created_at":"2026-11-12T19:38:07.020Z","author":"steve-orchestrator","text":"Round 14 (orchestrator, CodeRabbit review of f037df1): (1) withGithubOwnershipLock and withGithubImportOwnersUnderLease now clear a mutable active flag on their AsyncLocalStorage scope when it ends, so detached work that outlives the scope takes a fresh lease and a fresh ownership read instead of reusing a released lease or closed owner hooks; two regressions in test/import-ownership.test.ts fail on revert (lease probe false; corpusReads 1 instead of 2), and their nested-in-scope twins still reuse the lease and the outer read. (2) inspectCitationReferences lowercases each non-path view once and passes it to citationRepositoryBefore. (3) docs/identity-ownership-inventory.md states that matchImportedIssue reads legacyGithubLinkIdentities as raw first-link evidence and that the chosen item still passes the shared ownership checks; 101 line anchors remapped, all 100 named-call anchors verified. Gate: import-fidelity, import-ownership, ownership-consistency, refresh, round-twelve, cited-issue-adoption (343 tests, 0 fail), tsc, eslint, duplication (0 lines), changelog:check."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T19:38:07.028Z"}],"before_hash":"cd35ed8ac3ff1f9ec6b1a55a09d0e06f8c9933f38bd9bae878465928269d9feb","after_hash":"93eed664e2733bd8fcadf36391260a06dcd2c62d736b55c58beddcaaba29cd19","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"claude_session_file","rule_version":"v1"},"version":{"status":"resolved","resolver":"claude_session_file","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"c4ff0c96a983d9cbdd70fa0acc1a3f949d6a3a7c71de11ef366e1a60285ba9df"} +{"hash_algorithm":"sha256","ts":"2026-11-12T21:00:58.170Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-12T21:00:58.170Z"},{"op":"add","path":"/metadata/assignee","value":"codex-github230-1111"},{"op":"add","path":"/metadata/claim_principal","value":"codex-github230-1111"}],"before_hash":"93eed664e2733bd8fcadf36391260a06dcd2c62d736b55c58beddcaaba29cd19","after_hash":"34fb4d7ef084c05c92f66965a401a74651378f5b534abf276fe4463ac345a65e","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":"156b51bdc37694ee7a935e03177d28e5529b1fe0adddc412c7fa60dd0ef855f7"} +{"hash_algorithm":"sha256","ts":"2026-11-12T21:02:52.250Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/87","value":{"created_at":"2026-11-12T21:02:52.243Z","author":"codex-github230-1111","text":"Merged origin/main release be28728 in d28bd75: retained version 2026.11.12 in all manifests/extension/lock roots, retained released changelog sections, regenerated using npm run changelog:full. Read both full review comments and AI-agent prompts for PRRT_kwDOTbN4pM62z9yn and PRRT_kwDOTbN4pM62z9yf (two comments, no more pages). Plan: whole-value explicit URL ownership, reference-only generic link extraction, retained dedicated legacy link-only marker compatibility, native SDK regressions and function reverts; mechanically regenerate and validate every inventory anchor against the merged code."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T21:02:52.250Z"}],"before_hash":"34fb4d7ef084c05c92f66965a401a74651378f5b534abf276fe4463ac345a65e","after_hash":"34472f3ac66e68b6ce355d7dc9b2798b2188d1f422fc1647c6f5a5c4a9edf423","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":"ee028efc0ddd2e2c670b4e822a02ee0021fcf46ddd1a92ad9c087f2b8cc39872"} +{"hash_algorithm":"sha256","ts":"2026-11-12T21:12:37.408Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/58/path","value":"test/workspace-resolution.test.ts"},{"op":"replace","path":"/metadata/files/57/path","value":"test/webhooks.test.ts"},{"op":"replace","path":"/metadata/files/56/path","value":"test/typed-sentinel-normalization.test.ts"},{"op":"replace","path":"/metadata/files/55/path","value":"test/smoke.test.ts"},{"op":"replace","path":"/metadata/files/54/path","value":"test/sdk-adoption.test.ts"},{"op":"replace","path":"/metadata/files/53/path","value":"test/scheduled-refusals.test.ts"},{"op":"replace","path":"/metadata/files/52/path","value":"test/round-twelve.test.ts"},{"op":"add","path":"/metadata/files/51/note","value":"Unified typed ownership refusal"},{"op":"replace","path":"/metadata/files/51/path","value":"test/round-seven.test.ts"},{"op":"replace","path":"/metadata/files/50/note","value":"Contradictory legacy claims refuse without duplicate import"},{"op":"replace","path":"/metadata/files/50/path","value":"test/review-round3-identity.test.ts"},{"op":"replace","path":"/metadata/files/49/note","value":"Unified typed write refusal with readable legacy diagnostics"},{"op":"replace","path":"/metadata/files/49/path","value":"test/review-import-identity.test.ts"},{"op":"remove","path":"/metadata/files/48/note"},{"op":"replace","path":"/metadata/files/48/path","value":"test/repeat-import-noop.test.ts"},{"op":"replace","path":"/metadata/files/47/path","value":"test/refresh.test.ts"},{"op":"add","path":"/metadata/files/46/note","value":"Contradictory linked references refuse without mutation"},{"op":"replace","path":"/metadata/files/46/path","value":"test/reference-import-identity.test.ts"},{"op":"remove","path":"/metadata/files/45/note"},{"op":"replace","path":"/metadata/files/45/path","value":"test/project-sync.test.ts"},{"op":"add","path":"/metadata/files/44/note","value":"Creator proof and all changed tracker record reference audits"},{"op":"replace","path":"/metadata/files/44/path","value":"test/ownership-consistency.test.ts"},{"op":"replace","path":"/metadata/files/43/note","value":"Packed SDK floor typecheck and guarded write runtime probe"},{"op":"replace","path":"/metadata/files/43/path","value":"test/minimum-sdk-consumer.test.ts"},{"op":"remove","path":"/metadata/files/42/note"},{"op":"replace","path":"/metadata/files/42/path","value":"test/link-deps.test.ts"},{"op":"replace","path":"/metadata/files/41/path","value":"test/label-import-model.test.ts"},{"op":"replace","path":"/metadata/files/40/path","value":"test/import-projection.test.ts"},{"op":"add","path":"/metadata/files/39/note","value":"Native SDK revision and independent peer fixtures"},{"op":"replace","path":"/metadata/files/39/path","value":"test/import-ownership.test.ts"},{"op":"remove","path":"/metadata/files/38/note"},{"op":"replace","path":"/metadata/files/38/path","value":"test/import-lock.test.ts"},{"op":"add","path":"/metadata/files/37/note","value":"Real SDK dry-run fixtures and typed competing-owner refusal"},{"op":"replace","path":"/metadata/files/37/path","value":"test/import-gate.test.ts"},{"op":"remove","path":"/metadata/files/36/note"},{"op":"replace","path":"/metadata/files/36/path","value":"test/import-fidelity.test.ts"},{"op":"replace","path":"/metadata/files/35/path","value":"test/import-errors.test.ts"},{"op":"replace","path":"/metadata/files/34/path","value":"test/import-conflict-report.test.ts"},{"op":"replace","path":"/metadata/files/33/path","value":"test/identity.test.ts"},{"op":"replace","path":"/metadata/files/32/path","value":"test/helpers/import-preview.ts"},{"op":"add","path":"/metadata/files/31/note","value":"Real importer creation provenance in mirror fixtures"},{"op":"replace","path":"/metadata/files/31/path","value":"test/helpers/import-mirror.ts"},{"op":"remove","path":"/metadata/files/30/note"},{"op":"replace","path":"/metadata/files/30/path","value":"test/handler-failures.test.ts"},{"op":"replace","path":"/metadata/files/29/path","value":"test/github-url.test.ts"},{"op":"replace","path":"/metadata/files/28/path","value":"test/fixtures/refresh-crash.ts"},{"op":"replace","path":"/metadata/files/27/path","value":"test/fixtures/ownership-peer.ts"},{"op":"replace","path":"/metadata/files/26/path","value":"test/fixtures/atomic-plan-crash.ts"},{"op":"replace","path":"/metadata/files/25/path","value":"test/durable-state.test.ts"},{"op":"replace","path":"/metadata/files/24/path","value":"test/durable-recovery.test.ts"},{"op":"replace","path":"/metadata/files/23/path","value":"test/dryrun-preview.test.ts"},{"op":"replace","path":"/metadata/files/22/path","value":"test/daily-sync-idempotency.test.ts"},{"op":"add","path":"/metadata/files/21/note","value":"Native SDK policy refusal fixtures for guarded plain writes"},{"op":"replace","path":"/metadata/files/21/path","value":"test/coverage-remainder.test.ts"},{"op":"remove","path":"/metadata/files/20/note"},{"op":"replace","path":"/metadata/files/20/path","value":"test/coverage-edges.test.ts"},{"op":"replace","path":"/metadata/files/19/path","value":"test/compatibility-floor.test.ts"},{"op":"replace","path":"/metadata/files/18/path","value":"test/cited-issue-adoption.test.ts"},{"op":"replace","path":"/metadata/files/17/path","value":"test/blockers.test.ts"},{"op":"replace","path":"/metadata/files/16/path","value":"test/atomic.test.ts"},{"op":"replace","path":"/metadata/files/15/path","value":"scripts/identity-inventory.ts"},{"op":"add","path":"/metadata/files/59","value":{"path":"webhooks.ts","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T21:12:37.408Z"}],"before_hash":"34472f3ac66e68b6ce355d7dc9b2798b2188d1f422fc1647c6f5a5c4a9edf423","after_hash":"2f4a277958bf633b66e3f9655537913484f4b0e54fae6f31071f90ad2570b92d","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":"31e4fa8e20712300c6680eef399c8f8aa5c1a4fdb60a35cd490e36b0706a2a02"} +{"hash_algorithm":"sha256","ts":"2026-11-12T21:12:39.606Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/59/path","value":"test/workspace-resolution.test.ts"},{"op":"replace","path":"/metadata/files/58/path","value":"test/webhooks.test.ts"},{"op":"replace","path":"/metadata/files/57/path","value":"test/typed-sentinel-normalization.test.ts"},{"op":"replace","path":"/metadata/files/56/path","value":"test/smoke.test.ts"},{"op":"replace","path":"/metadata/files/55/path","value":"test/sdk-adoption.test.ts"},{"op":"replace","path":"/metadata/files/54/path","value":"test/scheduled-refusals.test.ts"},{"op":"replace","path":"/metadata/files/53/path","value":"test/round-twelve.test.ts"},{"op":"add","path":"/metadata/files/52/note","value":"Unified typed ownership refusal"},{"op":"replace","path":"/metadata/files/52/path","value":"test/round-seven.test.ts"},{"op":"replace","path":"/metadata/files/51/note","value":"Contradictory legacy claims refuse without duplicate import"},{"op":"replace","path":"/metadata/files/51/path","value":"test/review-round3-identity.test.ts"},{"op":"replace","path":"/metadata/files/50/note","value":"Unified typed write refusal with readable legacy diagnostics"},{"op":"replace","path":"/metadata/files/50/path","value":"test/review-import-identity.test.ts"},{"op":"remove","path":"/metadata/files/49/note"},{"op":"replace","path":"/metadata/files/49/path","value":"test/repeat-import-noop.test.ts"},{"op":"replace","path":"/metadata/files/48/path","value":"test/refresh.test.ts"},{"op":"add","path":"/metadata/files/47/note","value":"Contradictory linked references refuse without mutation"},{"op":"replace","path":"/metadata/files/47/path","value":"test/reference-import-identity.test.ts"},{"op":"remove","path":"/metadata/files/46/note"},{"op":"replace","path":"/metadata/files/46/path","value":"test/project-sync.test.ts"},{"op":"add","path":"/metadata/files/45/note","value":"Creator proof and all changed tracker record reference audits"},{"op":"replace","path":"/metadata/files/45/path","value":"test/ownership-consistency.test.ts"},{"op":"remove","path":"/metadata/files/44/note"},{"op":"replace","path":"/metadata/files/44/path","value":"test/owner-field-values.test.ts"},{"op":"add","path":"/metadata/files/60","value":{"path":"webhooks.ts","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T21:12:39.606Z"}],"before_hash":"2f4a277958bf633b66e3f9655537913484f4b0e54fae6f31071f90ad2570b92d","after_hash":"7b22ddb22d442cce6f0854c2d52defafa59a14cbe0929761277997f5b2c479fa","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":"bd32d3977f3cd4295686223755d41587ac783286e603b94abd7ddc78a5a2f7c0"} +{"hash_algorithm":"sha256","ts":"2026-11-12T21:12:42.668Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/60/path","value":"test/workspace-resolution.test.ts"},{"op":"replace","path":"/metadata/files/59/path","value":"test/webhooks.test.ts"},{"op":"replace","path":"/metadata/files/58/path","value":"test/typed-sentinel-normalization.test.ts"},{"op":"replace","path":"/metadata/files/57/path","value":"test/smoke.test.ts"},{"op":"replace","path":"/metadata/files/56/path","value":"test/sdk-adoption.test.ts"},{"op":"replace","path":"/metadata/files/55/path","value":"test/scheduled-refusals.test.ts"},{"op":"replace","path":"/metadata/files/54/path","value":"test/round-twelve.test.ts"},{"op":"add","path":"/metadata/files/53/note","value":"Unified typed ownership refusal"},{"op":"replace","path":"/metadata/files/53/path","value":"test/round-seven.test.ts"},{"op":"replace","path":"/metadata/files/52/note","value":"Contradictory legacy claims refuse without duplicate import"},{"op":"replace","path":"/metadata/files/52/path","value":"test/review-round3-identity.test.ts"},{"op":"replace","path":"/metadata/files/51/note","value":"Unified typed write refusal with readable legacy diagnostics"},{"op":"replace","path":"/metadata/files/51/path","value":"test/review-import-identity.test.ts"},{"op":"remove","path":"/metadata/files/50/note"},{"op":"replace","path":"/metadata/files/50/path","value":"test/repeat-import-noop.test.ts"},{"op":"replace","path":"/metadata/files/49/path","value":"test/refresh.test.ts"},{"op":"add","path":"/metadata/files/48/note","value":"Contradictory linked references refuse without mutation"},{"op":"replace","path":"/metadata/files/48/path","value":"test/reference-import-identity.test.ts"},{"op":"remove","path":"/metadata/files/47/note"},{"op":"replace","path":"/metadata/files/47/path","value":"test/project-sync.test.ts"},{"op":"add","path":"/metadata/files/46/note","value":"Creator proof and all changed tracker record reference audits"},{"op":"replace","path":"/metadata/files/46/path","value":"test/ownership-consistency.test.ts"},{"op":"remove","path":"/metadata/files/45/note"},{"op":"replace","path":"/metadata/files/45/path","value":"test/owner-field-values.test.ts"},{"op":"add","path":"/metadata/files/44/note","value":"Packed SDK floor typecheck and guarded write runtime probe"},{"op":"replace","path":"/metadata/files/44/path","value":"test/minimum-sdk-consumer.test.ts"},{"op":"remove","path":"/metadata/files/43/note"},{"op":"replace","path":"/metadata/files/43/path","value":"test/link-deps.test.ts"},{"op":"replace","path":"/metadata/files/42/path","value":"test/label-import-model.test.ts"},{"op":"replace","path":"/metadata/files/41/path","value":"test/import-projection.test.ts"},{"op":"add","path":"/metadata/files/40/note","value":"Native SDK revision and independent peer fixtures"},{"op":"replace","path":"/metadata/files/40/path","value":"test/import-ownership.test.ts"},{"op":"remove","path":"/metadata/files/39/note"},{"op":"replace","path":"/metadata/files/39/path","value":"test/import-lock.test.ts"},{"op":"add","path":"/metadata/files/38/note","value":"Real SDK dry-run fixtures and typed competing-owner refusal"},{"op":"replace","path":"/metadata/files/38/path","value":"test/import-gate.test.ts"},{"op":"remove","path":"/metadata/files/37/note"},{"op":"replace","path":"/metadata/files/37/path","value":"test/import-fidelity.test.ts"},{"op":"replace","path":"/metadata/files/36/path","value":"test/import-errors.test.ts"},{"op":"replace","path":"/metadata/files/35/path","value":"test/import-conflict-report.test.ts"},{"op":"replace","path":"/metadata/files/34/path","value":"test/identity.test.ts"},{"op":"replace","path":"/metadata/files/33/path","value":"test/identity-inventory.test.ts"},{"op":"add","path":"/metadata/files/61","value":{"path":"webhooks.ts","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T21:12:42.668Z"}],"before_hash":"7b22ddb22d442cce6f0854c2d52defafa59a14cbe0929761277997f5b2c479fa","after_hash":"5792f6c1a003f5ede7321bfbb5a70ea6e5655493d9302d02ada0759b063d935a","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":"78b2965cb0f9848e72b88e2f751d526beb59afa191360f9d3a0098ce9def8e18"} +{"hash_algorithm":"sha256","ts":"2026-11-12T21:12:46.332Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/88","value":{"created_at":"2026-11-12T21:12:46.296Z","author":"codex-github230-1111","text":"Whole-value regression focus passes 18/18 on real non-git SDK trackers and the registered github import command over the HTTP transport harness. Cases cover prose prefixes, trailing text, URL aliases, arrays/objects in explicit URL fields, exact-URL controls, four generic reference fields, explicit citation-adoption controls and dedicated legacy link-only compatibility. Existing fixtures that treated external_links as ownership are being converted to reference controls; explicit-owner freshness refusals stay intact. Added a reproducible syntax-tree inventory tool: node scripts/identity-inventory.ts --write; --check verifies all root and auxiliary anchors. The Project pull/import rows are derived at 640/706 and route through indexSelectedByIssue; a separate source-shift fixture verifies every Project anchor moves mechanically."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T21:12:46.332Z"}],"before_hash":"5792f6c1a003f5ede7321bfbb5a70ea6e5655493d9302d02ada0759b063d935a","after_hash":"7e2cf81040e5e9d456f93eacdf0bc0be19a2930826ab86d4082d79b6c52aacc3","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":"7573d5f3eb18654394b105aa4c98ba4f6433acbf6e8f2176c89fd5114b56f7c3"} +{"hash_algorithm":"sha256","ts":"2026-11-12T21:16:37.881Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/61/path","value":"test/workspace-resolution.test.ts"},{"op":"replace","path":"/metadata/files/60/path","value":"test/webhooks.test.ts"},{"op":"replace","path":"/metadata/files/59/path","value":"test/typed-sentinel-normalization.test.ts"},{"op":"replace","path":"/metadata/files/58/path","value":"test/smoke.test.ts"},{"op":"replace","path":"/metadata/files/57/path","value":"test/sdk-adoption.test.ts"},{"op":"replace","path":"/metadata/files/56/path","value":"test/scheduled-refusals.test.ts"},{"op":"replace","path":"/metadata/files/55/path","value":"test/round-twelve.test.ts"},{"op":"add","path":"/metadata/files/54/note","value":"Unified typed ownership refusal"},{"op":"replace","path":"/metadata/files/54/path","value":"test/round-seven.test.ts"},{"op":"replace","path":"/metadata/files/53/note","value":"Contradictory legacy claims refuse without duplicate import"},{"op":"replace","path":"/metadata/files/53/path","value":"test/review-round3-identity.test.ts"},{"op":"replace","path":"/metadata/files/52/note","value":"Unified typed write refusal with readable legacy diagnostics"},{"op":"replace","path":"/metadata/files/52/path","value":"test/review-import-identity.test.ts"},{"op":"remove","path":"/metadata/files/51/note"},{"op":"replace","path":"/metadata/files/51/path","value":"test/repeat-import-noop.test.ts"},{"op":"replace","path":"/metadata/files/50/path","value":"test/refresh.test.ts"},{"op":"add","path":"/metadata/files/49/note","value":"Contradictory linked references refuse without mutation"},{"op":"replace","path":"/metadata/files/49/path","value":"test/reference-import-identity.test.ts"},{"op":"remove","path":"/metadata/files/48/note"},{"op":"replace","path":"/metadata/files/48/path","value":"test/project-sync.test.ts"},{"op":"add","path":"/metadata/files/47/note","value":"Creator proof and all changed tracker record reference audits"},{"op":"replace","path":"/metadata/files/47/path","value":"test/ownership-consistency.test.ts"},{"op":"remove","path":"/metadata/files/46/note"},{"op":"replace","path":"/metadata/files/46/path","value":"test/owner-field-values.test.ts"},{"op":"add","path":"/metadata/files/45/note","value":"Packed SDK floor typecheck and guarded write runtime probe"},{"op":"replace","path":"/metadata/files/45/path","value":"test/minimum-sdk-consumer.test.ts"},{"op":"remove","path":"/metadata/files/44/note"},{"op":"replace","path":"/metadata/files/44/path","value":"test/link-deps.test.ts"},{"op":"replace","path":"/metadata/files/43/path","value":"test/label-import-model.test.ts"},{"op":"replace","path":"/metadata/files/42/path","value":"test/import-regex.test.ts"},{"op":"add","path":"/metadata/files/62","value":{"path":"webhooks.ts","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T21:16:37.881Z"}],"before_hash":"7e2cf81040e5e9d456f93eacdf0bc0be19a2930826ab86d4082d79b6c52aacc3","after_hash":"736b29f167875974a20b5077c74fc250f98852c2a4f754dbd1259216dbb3d128","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":"aa5413cdae7f4ea79e8a5d282b221e69a46ff14d92dea63617c4fa49e3db40a8"} +{"hash_algorithm":"sha256","ts":"2026-11-12T21:16:40.750Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/81","value":{"command":"timeout 1200 node --test --test-concurrency=1 test/owner-field-values.test.ts","scope":"project","provenance":{"author":"codex-github230-1111","created_at":"2026-11-12T21:16:40.603Z","source_kind":"local_mutation","source_ref":"fix/reference-tags-are-not-import-identity"}}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T21:16:40.750Z"}],"before_hash":"736b29f167875974a20b5077c74fc250f98852c2a4f754dbd1259216dbb3d128","after_hash":"d586bcc5b9783c3d6ec0c4445926c7b1a55cdfa61a4958fd1468f383021a40f4","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":"825a93f5fa3b7ec34f8b6da5aa4efda16a47c3c3dc894222a3d95c8e0e621a17"} +{"hash_algorithm":"sha256","ts":"2026-11-12T21:25:05.323Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/89","value":{"created_at":"2026-11-12T21:25:05.303Z","author":"codex-github230-1111","text":"Function-only revert proof against merged baseline d28bd75, in a disposable package copy: reverting legacyGithubLinkIdentities produces 12/12 expected failures for explicit URL prose, trailing text, aliases, non-string URL fields and four generic reference fields. Reverting hasStructuredOwnership produces the expected citation-adoption failure while its explicit-owner control passes. Reverting matchImportedIssue produces four expected failures for unmarked/trailing description prose and a shared GitHub URL in provider evidence; both genuine marker/provider controls pass. Reverting only the inventory document produces 3/3 expected stale-anchor failures. Total: 20 expected failures and three must-allow controls passed. Restored code/document pass owner-field-values 22/22 and identity-inventory 3/3. Exact proof commands: timeout 1200 node --test --test-concurrency=1 --test-name-pattern \"^(Whole identity field refuses|Other link fields)\" test/owner-field-values.test.ts; same command with pattern \"^Reference prose does not bar\"; same command with pattern \"^(Native link declaration|Provider backlink ignores)\"; timeout 1200 node --test --test-concurrency=1 test/identity-inventory.test.ts. No SDK APIs were doubled. SDK/API version read: public PM CLI 2026.11.12; the development pin and fenced minimum remain unchanged. Self-review also excludes GitHub URLs from provider-backlink fallback; real provider backlinks remain authorized first links, with native command controls."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T21:25:05.323Z"}],"before_hash":"d586bcc5b9783c3d6ec0c4445926c7b1a55cdfa61a4958fd1468f383021a40f4","after_hash":"ead6d2cecde3e1e902950de9728814560b5e8343cf51f6250de59901a2c091d7","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":"b0309de3b280c40b37737b6401ef1f5505dfa4029bb6a57dccf4cc1a9cecfe01"} +{"hash_algorithm":"sha256","ts":"2026-11-12T21:36:30.591Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/90","value":{"created_at":"2026-11-12T21:36:30.554Z","author":"codex-github230-1111","text":"Completed class inventory of claim constructors: import-ownership.ts is the authoritative collector; the remaining independent GitHub claim construction was identity.ts graph(). It now takes GitHub claims only from githubOwnershipClaims and reads generic/prose/API links as mention edges through githubLinkReferences; valid opaque provider refs retain their namespace. The added graph-only function revert gives 5 expected failures and a passing native-marker control. Updated total across four function-only reverts plus the stale-document revert: 25 expected failures, four must-allow controls passed, restored native field tests 24/24 and inventory tests 3/3. The focused existing identity file also passes 20/20. The reference tests omit redundant tag hints when checking that link fields themselves supply a mention; original reference-tag regressions remain in the final gate. All production changes are now frozen for a complete final affected-file rerun."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T21:36:30.591Z"}],"before_hash":"ead6d2cecde3e1e902950de9728814560b5e8343cf51f6250de59901a2c091d7","after_hash":"a8ffc43b312a077bd9cdf941011135a0954b362892ec389a38ee74297bbd3df3","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":"a7eb6b1404db690816c677fdd5294a0f5deb2e46c427c51c94d8e09633e8c992"} +{"hash_algorithm":"sha256","ts":"2026-11-12T21:47:50.554Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/82","value":{"command":"timeout 1200 node --test --test-concurrency=1 test/identity-inventory.test.ts","scope":"project","provenance":{"author":"codex-github230-1111","created_at":"2026-11-12T21:47:50.360Z","source_kind":"local_mutation","source_ref":"fix/reference-tags-are-not-import-identity"}}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T21:47:50.554Z"}],"before_hash":"a8ffc43b312a077bd9cdf941011135a0954b362892ec389a38ee74297bbd3df3","after_hash":"d75c8505abf951f800208f6ba7f311efebaf0ac16cb87e011de03a3f9cc33418","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":"1b16ff9084b9ac11499f2a9e1d44225731b3a5e608c80ed53af738e9f8f8e5dc"} +{"hash_algorithm":"sha256","ts":"2026-11-12T21:49:12.201Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/91","value":{"created_at":"2026-11-12T21:49:12.182Z","author":"codex-github230-1111","text":"Final affected-file gate: 22 test files/jobs, 549 cases, 547 passes, two unchanged pm-cli#1656 quarantines, no failures/cancellations. Each command capped at 20 minutes; at most two test workers. All production changes were frozen before this complete rerun. Exact commands/results:\ntimeout 1200 node --test --test-concurrency=1 test/cited-issue-adoption.test.ts => 46 passed, 0 skipped\ntimeout 1200 node --test --test-concurrency=1 test/github-url.test.ts => 36 passed, 0 skipped\ntimeout 1200 node --test --test-concurrency=1 test/identity-inventory.test.ts => 3 passed, 0 skipped\ntimeout 1200 node --test --test-concurrency=1 test/identity.test.ts => 20 passed, 0 skipped\ntimeout 1200 node --test --test-concurrency=1 test/import-fidelity.test.ts => 53 passed, 1 skipped\ntimeout 1200 node --test --test-concurrency=1 test/import-ownership.test.ts => 28 passed, 0 skipped\ntimeout 1200 node --test --test-concurrency=1 test/import-projection.test.ts => 6 passed, 0 skipped\ntimeout 1200 node --test --test-concurrency=1 test/import-regex.test.ts => 8 passed, 0 skipped\ntimeout 1200 node --test --test-concurrency=1 test/label-import-model.test.ts => 3 passed, 0 skipped\ntimeout 1200 node --test --test-concurrency=1 test/owner-field-values.test.ts => 24 passed, 0 skipped\ntimeout 1200 node --test --test-concurrency=1 test/ownership-consistency.test.ts => 133 passed, 0 skipped\ntimeout 1200 node --test --test-concurrency=1 test/reference-import-identity.test.ts => 17 passed, 0 skipped\ntimeout 1200 node --test --test-concurrency=1 test/refresh.test.ts => 40 passed, 1 skipped\ntimeout 1200 node --test --test-concurrency=1 test/repeat-import-noop.test.ts => 9 passed, 0 skipped\ntimeout 1200 node --test --test-concurrency=1 test/review-import-identity.test.ts => 30 passed, 0 skipped\ntimeout 1200 node --test --test-concurrency=1 test/review-round3-identity.test.ts => 29 passed, 0 skipped\ntimeout 1200 node --test --test-concurrency=1 test/round-twelve.test.ts => 32 passed, 0 skipped\ntimeout 1200 node --test --test-concurrency=1 test/scheduled-refusals.test.ts => 20 passed, 0 skipped\ntimeout 1200 node --test --test-concurrency=1 test/sdk-adoption.test.ts => 4 passed, 0 skipped\ntimeout 1200 node --test --test-concurrency=1 test/typed-sentinel-normalization.test.ts => 3 passed, 0 skipped\ntimeout 1200 node --test --test-concurrency=1 test/workspace-resolution.test.ts => 2 passed, 0 skipped\ntimeout 1200 npm run test:minimum-sdk => 1 passed, 0 skipped\nThe new native 24-case owner-field regression and three inventory checks also pass through pm test --only-last --run --progress. timeout 1200 npm run lint, timeout 1200 npm run typecheck, timeout 1200 npm run build:test, timeout 1200 npm run docstring and timeout 1200 npm run duplication pass (33 files/312 documented declarations; zero duplicated lines). Inventory generated and checked by node scripts/identity-inventory.ts --write / --check: 93 groups, 278 syntax sites plus five auxiliary anchors. A supplementary grep found two optional field reads; the parser now covers optional syntax too. The final stale-ledger proof was repeated after that tool change: 3/3 expected failures, restored 3/3 passes. Whole-field class total remains 25 expected revert failures, four passing must-allow controls, restored focus 27/27. Release own claim without closure, regenerate/check changelog after final PM writes, then audit privacy and commit/push. Full release/coverage certification remains CI work. Both review threads and AI-agent prompts read in full; no review replies, resolutions or reactions."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T21:49:12.201Z"}],"before_hash":"d75c8505abf951f800208f6ba7f311efebaf0ac16cb87e011de03a3f9cc33418","after_hash":"2c8fe3d098677a332f3b2de0d5b71e89dfe9294fc5d97e2cbe7097ae11b9918c","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":"9d88896504a255b756031bd5487d1b53e01690e654342529ed71a432134d6d39"} +{"hash_algorithm":"sha256","ts":"2026-11-12T21:49:13.923Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-12T21:49:13.923Z"}],"before_hash":"2c8fe3d098677a332f3b2de0d5b71e89dfe9294fc5d97e2cbe7097ae11b9918c","after_hash":"bf4406f287ed628d2ac93a27bb71de803d0c44bb7b95e207ab7dfd1484a9e286","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":"3352a25eea36dde91391e3c4fa277ffb790d06694908cf340da4378fa36bb980"} +{"hash_algorithm":"sha256","ts":"2026-11-12T21:53:04.469Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-12T21:53:04.469Z"},{"op":"add","path":"/metadata/assignee","value":"codex-github230-1111"},{"op":"add","path":"/metadata/claim_principal","value":"codex-github230-1111"}],"before_hash":"bf4406f287ed628d2ac93a27bb71de803d0c44bb7b95e207ab7dfd1484a9e286","after_hash":"1d95659244b630cae9fa7f17480ed3fe5dfa74742215c000a346748a2cce9477","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":"c1b908acb4b164eb67268b2b9633b2fb7fa0247023939d8653a90c01c39b1600"} +{"hash_algorithm":"sha256","ts":"2026-11-12T22:01:55.724Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/62/path","value":"test/workspace-resolution.test.ts"},{"op":"replace","path":"/metadata/files/61/path","value":"test/webhooks.test.ts"},{"op":"replace","path":"/metadata/files/60/path","value":"test/typed-sentinel-normalization.test.ts"},{"op":"replace","path":"/metadata/files/59/path","value":"test/smoke.test.ts"},{"op":"replace","path":"/metadata/files/58/path","value":"test/sdk-adoption.test.ts"},{"op":"replace","path":"/metadata/files/57/path","value":"test/scheduled-refusals.test.ts"},{"op":"replace","path":"/metadata/files/56/path","value":"test/round-twelve.test.ts"},{"op":"add","path":"/metadata/files/55/note","value":"Unified typed ownership refusal"},{"op":"replace","path":"/metadata/files/55/path","value":"test/round-seven.test.ts"},{"op":"replace","path":"/metadata/files/54/note","value":"Contradictory legacy claims refuse without duplicate import"},{"op":"replace","path":"/metadata/files/54/path","value":"test/review-round3-identity.test.ts"},{"op":"replace","path":"/metadata/files/53/note","value":"Unified typed write refusal with readable legacy diagnostics"},{"op":"replace","path":"/metadata/files/53/path","value":"test/review-import-identity.test.ts"},{"op":"remove","path":"/metadata/files/52/note"},{"op":"replace","path":"/metadata/files/52/path","value":"test/repeat-import-noop.test.ts"},{"op":"replace","path":"/metadata/files/51/path","value":"test/refresh.test.ts"},{"op":"add","path":"/metadata/files/50/note","value":"Contradictory linked references refuse without mutation"},{"op":"replace","path":"/metadata/files/50/path","value":"test/reference-import-identity.test.ts"},{"op":"remove","path":"/metadata/files/49/note"},{"op":"replace","path":"/metadata/files/49/path","value":"test/project-sync.test.ts"},{"op":"add","path":"/metadata/files/48/note","value":"Creator proof and all changed tracker record reference audits"},{"op":"replace","path":"/metadata/files/48/path","value":"test/ownership-consistency.test.ts"},{"op":"remove","path":"/metadata/files/47/note"},{"op":"replace","path":"/metadata/files/47/path","value":"test/owner-field-values.test.ts"},{"op":"add","path":"/metadata/files/46/note","value":"Packed SDK floor typecheck and guarded write runtime probe"},{"op":"replace","path":"/metadata/files/46/path","value":"test/minimum-sdk-consumer.test.ts"},{"op":"remove","path":"/metadata/files/45/note"},{"op":"replace","path":"/metadata/files/45/path","value":"test/link-deps.test.ts"},{"op":"replace","path":"/metadata/files/44/path","value":"test/label-import-model.test.ts"},{"op":"replace","path":"/metadata/files/43/path","value":"test/import-regex.test.ts"},{"op":"replace","path":"/metadata/files/42/path","value":"test/import-projection.test.ts"},{"op":"add","path":"/metadata/files/41/note","value":"Native SDK revision and independent peer fixtures"},{"op":"replace","path":"/metadata/files/41/path","value":"test/import-ownership.test.ts"},{"op":"remove","path":"/metadata/files/40/note"},{"op":"replace","path":"/metadata/files/40/path","value":"test/import-lock.test.ts"},{"op":"add","path":"/metadata/files/39/note","value":"Real SDK dry-run fixtures and typed competing-owner refusal"},{"op":"replace","path":"/metadata/files/39/path","value":"test/import-gate.test.ts"},{"op":"remove","path":"/metadata/files/38/note"},{"op":"replace","path":"/metadata/files/38/path","value":"test/import-fidelity.test.ts"},{"op":"replace","path":"/metadata/files/37/path","value":"test/import-errors.test.ts"},{"op":"replace","path":"/metadata/files/36/path","value":"test/import-conflict-report.test.ts"},{"op":"replace","path":"/metadata/files/35/path","value":"test/identity.test.ts"},{"op":"replace","path":"/metadata/files/34/path","value":"test/identity-inventory.test.ts"},{"op":"replace","path":"/metadata/files/33/path","value":"test/helpers/import-preview.ts"},{"op":"add","path":"/metadata/files/32/note","value":"Real importer creation provenance in mirror fixtures"},{"op":"replace","path":"/metadata/files/32/path","value":"test/helpers/import-mirror.ts"},{"op":"remove","path":"/metadata/files/31/note"},{"op":"replace","path":"/metadata/files/31/path","value":"test/handler-failures.test.ts"},{"op":"replace","path":"/metadata/files/30/path","value":"test/github-url.test.ts"},{"op":"replace","path":"/metadata/files/29/path","value":"test/fixtures/refresh-crash.ts"},{"op":"replace","path":"/metadata/files/28/path","value":"test/fixtures/ownership-peer.ts"},{"op":"replace","path":"/metadata/files/27/path","value":"test/fixtures/atomic-plan-crash.ts"},{"op":"replace","path":"/metadata/files/26/path","value":"test/durable-state.test.ts"},{"op":"replace","path":"/metadata/files/25/path","value":"test/durable-recovery.test.ts"},{"op":"replace","path":"/metadata/files/24/path","value":"test/dryrun-preview.test.ts"},{"op":"replace","path":"/metadata/files/23/path","value":"test/daily-sync-idempotency.test.ts"},{"op":"add","path":"/metadata/files/22/note","value":"Native SDK policy refusal fixtures for guarded plain writes"},{"op":"replace","path":"/metadata/files/22/path","value":"test/coverage-remainder.test.ts"},{"op":"remove","path":"/metadata/files/21/note"},{"op":"replace","path":"/metadata/files/21/path","value":"test/coverage-edges.test.ts"},{"op":"replace","path":"/metadata/files/20/path","value":"test/compatibility-floor.test.ts"},{"op":"replace","path":"/metadata/files/19/path","value":"test/cited-issue-adoption.test.ts"},{"op":"replace","path":"/metadata/files/18/path","value":"test/blockers.test.ts"},{"op":"replace","path":"/metadata/files/17/path","value":"test/atomic.test.ts"},{"op":"replace","path":"/metadata/files/16/path","value":"scripts/identity-inventory.ts"},{"op":"replace","path":"/metadata/files/15/path","value":"scripts/generate-changelog.ts"},{"op":"add","path":"/metadata/files/63","value":{"path":"webhooks.ts","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T22:01:55.724Z"}],"before_hash":"1d95659244b630cae9fa7f17480ed3fe5dfa74742215c000a346748a2cce9477","after_hash":"50add66b46ef459b4e1756b98a6a559ba8e3f39d7fa733366458df051f1e9b57","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":"610bbb2182169a0327039a7a1f22e2842fb59dc22ec4102c4861c5d8ab4e44b3"} +{"hash_algorithm":"sha256","ts":"2026-11-12T22:01:56.659Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/63/path","value":"test/workspace-resolution.test.ts"},{"op":"replace","path":"/metadata/files/62/path","value":"test/webhooks.test.ts"},{"op":"replace","path":"/metadata/files/61/path","value":"test/typed-sentinel-normalization.test.ts"},{"op":"replace","path":"/metadata/files/60/path","value":"test/smoke.test.ts"},{"op":"replace","path":"/metadata/files/59/path","value":"test/sdk-adoption.test.ts"},{"op":"replace","path":"/metadata/files/58/path","value":"test/scheduled-refusals.test.ts"},{"op":"replace","path":"/metadata/files/57/path","value":"test/round-twelve.test.ts"},{"op":"add","path":"/metadata/files/56/note","value":"Unified typed ownership refusal"},{"op":"replace","path":"/metadata/files/56/path","value":"test/round-seven.test.ts"},{"op":"replace","path":"/metadata/files/55/note","value":"Contradictory legacy claims refuse without duplicate import"},{"op":"replace","path":"/metadata/files/55/path","value":"test/review-round3-identity.test.ts"},{"op":"replace","path":"/metadata/files/54/note","value":"Unified typed write refusal with readable legacy diagnostics"},{"op":"replace","path":"/metadata/files/54/path","value":"test/review-import-identity.test.ts"},{"op":"remove","path":"/metadata/files/53/note"},{"op":"replace","path":"/metadata/files/53/path","value":"test/repeat-import-noop.test.ts"},{"op":"replace","path":"/metadata/files/52/path","value":"test/refresh.test.ts"},{"op":"add","path":"/metadata/files/51/note","value":"Contradictory linked references refuse without mutation"},{"op":"replace","path":"/metadata/files/51/path","value":"test/reference-import-identity.test.ts"},{"op":"remove","path":"/metadata/files/50/note"},{"op":"replace","path":"/metadata/files/50/path","value":"test/project-sync.test.ts"},{"op":"add","path":"/metadata/files/49/note","value":"Creator proof and all changed tracker record reference audits"},{"op":"replace","path":"/metadata/files/49/path","value":"test/ownership-consistency.test.ts"},{"op":"remove","path":"/metadata/files/48/note"},{"op":"replace","path":"/metadata/files/48/path","value":"test/owner-field-values.test.ts"},{"op":"add","path":"/metadata/files/47/note","value":"Packed SDK floor typecheck and guarded write runtime probe"},{"op":"replace","path":"/metadata/files/47/path","value":"test/minimum-sdk-consumer.test.ts"},{"op":"remove","path":"/metadata/files/46/note"},{"op":"replace","path":"/metadata/files/46/path","value":"test/link-deps.test.ts"},{"op":"replace","path":"/metadata/files/45/path","value":"test/label-import-model.test.ts"},{"op":"replace","path":"/metadata/files/44/path","value":"test/import-regex.test.ts"},{"op":"replace","path":"/metadata/files/43/path","value":"test/import-projection.test.ts"},{"op":"add","path":"/metadata/files/42/note","value":"Native SDK revision and independent peer fixtures"},{"op":"replace","path":"/metadata/files/42/path","value":"test/import-ownership.test.ts"},{"op":"remove","path":"/metadata/files/41/note"},{"op":"replace","path":"/metadata/files/41/path","value":"test/import-lock.test.ts"},{"op":"add","path":"/metadata/files/40/note","value":"Real SDK dry-run fixtures and typed competing-owner refusal"},{"op":"replace","path":"/metadata/files/40/path","value":"test/import-gate.test.ts"},{"op":"remove","path":"/metadata/files/39/note"},{"op":"replace","path":"/metadata/files/39/path","value":"test/import-fidelity.test.ts"},{"op":"replace","path":"/metadata/files/38/path","value":"test/import-errors.test.ts"},{"op":"replace","path":"/metadata/files/37/path","value":"test/import-conflict-report.test.ts"},{"op":"replace","path":"/metadata/files/36/path","value":"test/identity.test.ts"},{"op":"replace","path":"/metadata/files/35/path","value":"test/identity-inventory.test.ts"},{"op":"replace","path":"/metadata/files/34/path","value":"test/helpers/import-preview.ts"},{"op":"add","path":"/metadata/files/33/note","value":"Real importer creation provenance in mirror fixtures"},{"op":"replace","path":"/metadata/files/33/path","value":"test/helpers/import-mirror.ts"},{"op":"remove","path":"/metadata/files/32/note"},{"op":"replace","path":"/metadata/files/32/path","value":"test/handler-failures.test.ts"},{"op":"replace","path":"/metadata/files/31/path","value":"test/github-url.test.ts"},{"op":"replace","path":"/metadata/files/30/path","value":"test/generate-changelog.test.ts"},{"op":"add","path":"/metadata/files/64","value":{"path":"webhooks.ts","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T22:01:56.659Z"}],"before_hash":"50add66b46ef459b4e1756b98a6a559ba8e3f39d7fa733366458df051f1e9b57","after_hash":"52257d77e471d4ddc861b9ea5ba6a8a0208f0c5461ba4acbeccf2db957a775c0","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":"4a21ef862371494dca8538adeb9c0db7bba6448e0ad5dc813fb35a4a29c673f0"} +{"hash_algorithm":"sha256","ts":"2026-11-12T22:01:57.556Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/83","value":{"command":"timeout 1200 node --test --test-concurrency=1 test/generate-changelog.test.ts","scope":"project","provenance":{"author":"codex-github230-1111","created_at":"2026-11-12T22:01:57.492Z","source_kind":"local_mutation","source_ref":"fix/reference-tags-are-not-import-identity"}}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T22:01:57.556Z"}],"before_hash":"52257d77e471d4ddc861b9ea5ba6a8a0208f0c5461ba4acbeccf2db957a775c0","after_hash":"08339056a1d996b748ba75bf8536d710270085cccbf338af76368373de181660","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":"46483f7a6ae89300254d3f7b91d0f01a51a6f9ac1a6764ad8735f8aeaab26654"} +{"hash_algorithm":"sha256","ts":"2026-11-12T22:01:58.495Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/92","value":{"created_at":"2026-11-12T22:01:58.486Z","author":"codex-github230-1111","text":"Final merge check caught that the original generator omitted main's released jscpd note as soon as this tagged pending PM item made the current window nonempty. Kept the native release-window convention and enabled pm-changelog's documented --dependency-updates flag; no custom history splicing, pending-window override or workflow change. This retains every main changelog line and mechanically adds canonical historical dependency entries. The real Git/SDK regression uses a dependency-shaped commit plus pending PM work and fails when only generateChangelog is reverted; restored generator/workspace tests pass 6/6. The new generator test command is linked; its raw CLI stdout contains disposable absolute paths, so it is run outside PM capture and only sanitized counts are recorded here. The already executed PM-linked owner/inventory commands pass 24/24 and 3/3 without capturing such paths. Claim re-acquired for this necessary merge repair; it will be released again after the final receipts."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T22:01:58.495Z"}],"before_hash":"08339056a1d996b748ba75bf8536d710270085cccbf338af76368373de181660","after_hash":"fbd4c938cbe533e6b359d81e3325094a6055b105eb82ff272293b4aaa41d1087","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":"2b3c58dba62449e5125dcf3fc14b05187b5422ff7261d71249ecfbad9a25455c"} +{"hash_algorithm":"sha256","ts":"2026-11-12T22:08:25.907Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/93","value":{"created_at":"2026-11-12T22:08:25.901Z","author":"codex-github230-1111","text":"Final merge-integration receipts: timeout 1200 node --test --test-concurrency=1 test/generate-changelog.test.ts passes 4/4; the affected generator/workspace combined run passes 6/6. Reverting only generateChangelog loses the released dependency note and fails its real Git/SDK regression; restored case passes. This adds one expected revert failure to the 25 identity/inventory outcomes (26 total). Unique gate inventory is now 23 files/jobs, 553 cases, 551 passes and two unchanged pm-cli#1656 quarantines. No remaining test failures/cancellations in that final inventory. An optional additional SDK install stalled before tracker initialization; its process tree was stopped, and the supported PM_GITHUB_TEST_NETWORK_ATTEMPT_TIMEOUT_MS=1000 retry passed in 73 seconds. Exact command: PM_GITHUB_TEST_NETWORK_ATTEMPT_TIMEOUT_MS=1000 timeout 1200 npm run test:minimum-sdk. The published SDK floor remains 2026.10.23; all production/operational source and shipped declaration checks plus native artifact controls pass. Lint, production/test typechecks, docstrings and zero-duplication checks pass after the generator adjustment. Line-by-line comparison confirms all main changelog content survives, with generated additions only; all version fields remain 2026.11.12. Final changelog regeneration/check and privacy audit follow this final tracker write. Release only this author claim, never close either PR item. CI certifies the full release/coverage gate; no review thread was replied to or resolved."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T22:08:25.907Z"}],"before_hash":"fbd4c938cbe533e6b359d81e3325094a6055b105eb82ff272293b4aaa41d1087","after_hash":"320e61371008bb1c35d4dba23bbd7c106d82064119100542688507148ac3942a","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":"09f304da7029a580ad00fd2decef900d5561306079fb52511b062170173ffec0"} +{"hash_algorithm":"sha256","ts":"2026-11-12T22:08:27.147Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-12T22:08:27.147Z"}],"before_hash":"320e61371008bb1c35d4dba23bbd7c106d82064119100542688507148ac3942a","after_hash":"30ebdb070a2283a354967611d7c28ee661dcae313f6e82de9e234ea3c9d9363e","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":"b7561993076d17a0ce97eddea99508240dfb8b5674eed4b71723684045c134a5"} +{"hash_algorithm":"sha256","ts":"2026-11-12T22:25:02.544Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-12T22:25:02.544Z"},{"op":"add","path":"/metadata/assignee","value":"codex-github230-1111"},{"op":"add","path":"/metadata/claim_principal","value":"codex-github230-1111"}],"before_hash":"30ebdb070a2283a354967611d7c28ee661dcae313f6e82de9e234ea3c9d9363e","after_hash":"235a67cd26e86be39a8cd557b1696115d44aef91e29d97f4cf9d3e2455d2832a","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":"47feb43026e62e45322032c2d34212c0f5f33e04478009eb14963c088e2cb3f9"} +{"hash_algorithm":"sha256","ts":"2026-11-12T22:31:37.524Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/94","value":{"created_at":"2026-11-12T22:31:37.517Z","author":"codex-github230-1111","text":"Corrected round-14 contract: retain whole-value github_url parsing and reference-only tags/generic link fields, while restoring leading native description declarations without a link-only marker or whole-description anchor. Preserve copied-provider-marker classification and direct-owner precedence. Add native SDK allow/refuse tests and compare exact issue-only atomic gated previews of five copied fleet trackers against origin/main, without applying or changing fleet files. Keep dependency-updates only with the existing released-note preservation regression. Check shared-runner hold before pushing."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T22:31:37.524Z"}],"before_hash":"235a67cd26e86be39a8cd557b1696115d44aef91e29d97f4cf9d3e2455d2832a","after_hash":"6c497f332349ad917b16c4215ed105d1a8df8c243e7554ccfc32afc3f945ddc5","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":"240134f1b5053e8c109e2d851871b056567e590aa55dc42b6a7e930cebf410af"} +{"hash_algorithm":"sha256","ts":"2026-11-12T22:45:59.653Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/95","value":{"created_at":"2026-11-12T22:45:59.647Z","author":"codex-github230-1111","text":"Corrected compatibility: leading native GitHub issue/GH issue declarations own their first URL without a link-only flag or end-of-description requirement. Complete github_url parsing and reference-only generic links remain. Copied-provider classification keeps main precedence, with explicit provenance and verified importer identities protected. Eight focused native SDK cases pass, including all three reported legacy shapes, middle-description/prose-field/tag-only refusals, and copied-provider controls. The baseline pm-github gated preview refuses its pre-existing duplicate-tag corpus; collect its separate read-only planner diagnostic and label it explicitly rather than altering copied records."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T22:45:59.653Z"}],"before_hash":"6c497f332349ad917b16c4215ed105d1a8df8c243e7554ccfc32afc3f945ddc5","after_hash":"9dfd3feb86e6a74fe24101c8bc824595c3dca63822ca2f818085b2f6ae51b503","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":"45041db6ae5bedb294d0f2c0713018848888de5fb730e6b090197993ae01e0ef"} +{"hash_algorithm":"sha256","ts":"2026-11-12T22:47:31.242Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/64/path","value":"test/workspace-resolution.test.ts"},{"op":"replace","path":"/metadata/files/63/path","value":"test/webhooks.test.ts"},{"op":"replace","path":"/metadata/files/62/path","value":"test/typed-sentinel-normalization.test.ts"},{"op":"replace","path":"/metadata/files/61/path","value":"test/smoke.test.ts"},{"op":"replace","path":"/metadata/files/60/path","value":"test/sdk-adoption.test.ts"},{"op":"replace","path":"/metadata/files/59/path","value":"test/scheduled-refusals.test.ts"},{"op":"replace","path":"/metadata/files/58/path","value":"test/round-twelve.test.ts"},{"op":"add","path":"/metadata/files/57/note","value":"Unified typed ownership refusal"},{"op":"replace","path":"/metadata/files/57/path","value":"test/round-seven.test.ts"},{"op":"replace","path":"/metadata/files/56/note","value":"Contradictory legacy claims refuse without duplicate import"},{"op":"replace","path":"/metadata/files/56/path","value":"test/review-round3-identity.test.ts"},{"op":"replace","path":"/metadata/files/55/note","value":"Unified typed write refusal with readable legacy diagnostics"},{"op":"replace","path":"/metadata/files/55/path","value":"test/review-import-identity.test.ts"},{"op":"remove","path":"/metadata/files/54/note"},{"op":"replace","path":"/metadata/files/54/path","value":"test/repeat-import-noop.test.ts"},{"op":"replace","path":"/metadata/files/53/path","value":"test/refresh.test.ts"},{"op":"add","path":"/metadata/files/52/note","value":"Contradictory linked references refuse without mutation"},{"op":"replace","path":"/metadata/files/52/path","value":"test/reference-import-identity.test.ts"},{"op":"remove","path":"/metadata/files/51/note"},{"op":"replace","path":"/metadata/files/51/path","value":"test/project-sync.test.ts"},{"op":"add","path":"/metadata/files/50/note","value":"Creator proof and all changed tracker record reference audits"},{"op":"replace","path":"/metadata/files/50/path","value":"test/ownership-consistency.test.ts"},{"op":"remove","path":"/metadata/files/49/note"},{"op":"replace","path":"/metadata/files/49/path","value":"test/owner-field-values.test.ts"},{"op":"add","path":"/metadata/files/48/note","value":"Packed SDK floor typecheck and guarded write runtime probe"},{"op":"replace","path":"/metadata/files/48/path","value":"test/minimum-sdk-consumer.test.ts"},{"op":"remove","path":"/metadata/files/47/note"},{"op":"replace","path":"/metadata/files/47/path","value":"test/link-deps.test.ts"},{"op":"replace","path":"/metadata/files/46/path","value":"test/legacy-description-compatibility.test.ts"},{"op":"add","path":"/metadata/files/65","value":{"path":"webhooks.ts","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T22:47:31.242Z"}],"before_hash":"9dfd3feb86e6a74fe24101c8bc824595c3dca63822ca2f818085b2f6ae51b503","after_hash":"352fa200f118697f889f67043cfa6c114c8f715200ba4d0ded07d1f13c5cda96","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":"f17e319cac658795615552d4fe0ee6393983617c2a1fa3ba03478ccf247dabe8"} +{"hash_algorithm":"sha256","ts":"2026-11-12T22:47:32.425Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/84","value":{"command":"timeout 1200 node --test --test-concurrency=1 test/legacy-description-compatibility.test.ts","scope":"project","provenance":{"author":"codex-github230-1111","created_at":"2026-11-12T22:47:32.281Z","source_kind":"local_mutation","source_ref":"fix/reference-tags-are-not-import-identity"}}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T22:47:32.425Z"}],"before_hash":"352fa200f118697f889f67043cfa6c114c8f715200ba4d0ded07d1f13c5cda96","after_hash":"67dffd8f3221bf3f0273dc2ae523e67b347c8c50bcf3c5ec62e46bf7bc424e84","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":"988ce3fec65cea32b5572dfeeb18e67f538fd6f61f289f5fa5e9c66d7792eb60"} +{"hash_algorithm":"sha256","ts":"2026-11-12T22:55:56.290Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/96","value":{"created_at":"2026-11-12T22:55:56.283Z","author":"codex-github230-1111","text":"Revert proof in a disposable package copy: reverting legacyGithubLinkIdentities to 017b62c fails all three native declaration compatibility cases; reverting matchImportedIssue to 017b62c fails all three curated classification cases. Retained refusals: reverting whole-value URL collection to 4de7d4c fails the prose github_url case; reverting the matcher to origin/main fails the tag-only case. Removing the start anchor (a guard mutation, not a parent commit revert) fails the middle-description case. Removing copied-provider precedence fails the direct-owner control while its legacy twin passes. Restored native compatibility file: 8/8 pass. Keeping --dependency-updates is necessary: reverting generateChangelog to the pre-flag implementation loses the released dependency note beside pending PM work (1 failure); restored regression passes. npm run lint, npm run typecheck and npm run docstring pass; minimum SDK 2026.10.23 consumer passes with the supported 1000ms network-family timer after stopping a stalled default-timer install."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T22:55:56.290Z"}],"before_hash":"67dffd8f3221bf3f0273dc2ae523e67b347c8c50bcf3c5ec62e46bf7bc424e84","after_hash":"e930147a37a8f00e55fcbf7dc4555a27ed828468dd9391473cf0f37ad5e25320","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":"360a55522e3fa7de4d193854ba0f97b6ee1e3e3126a51c7f166cd03b02a40908"} +{"hash_algorithm":"sha256","ts":"2026-11-12T23:05:12.399Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/97","value":{"created_at":"2026-11-12T23:05:12.393Z","author":"codex-github230-1111","text":"Final affected-file gates: 24 files, 561 cases, 559 pass, two unchanged pm-cli#1656 quarantines, zero failures or cancellations after environment retries. Every command was capped at 20 minutes with at most two workers. The packed npm/Bun projection file passes 6/6 with the 1000ms Node network-family timer; the declared-minimum SDK consumer passes 1/1 with its supported timer. The linked native compatibility command also passes 8/8 through pm test --run --only-last --progress; project policy disables automatic result tracking, so this comment records the result. Lint, current typecheck, docstrings and mechanically checked identity inventory pass. Full release/coverage certification remains with CI."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T23:05:12.399Z"}],"before_hash":"e930147a37a8f00e55fcbf7dc4555a27ed828468dd9391473cf0f37ad5e25320","after_hash":"d4ddfa8a6078c307af4a820e0c4b8518df278905128cbbe0d8d8e38d62c07bdc","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":"b320223c5e755eacbb57a826a35551d1805b08ba55b494aa24e1b2a6863c99af"} +{"hash_algorithm":"sha256","ts":"2026-11-12T23:05:13.642Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/98","value":{"created_at":"2026-11-12T23:05:13.631Z","author":"codex-github230-1111","text":"Read-only real-data comparison: fresh public default-branch tracker copies, origin/main code be28728 versus the candidate, exact state-all atomic gated dry-run command with JSON, and complete public issue-only snapshots projected to read-only transport shapes. Counts create/update/unchanged: pm-github 8/16/0 versus 22/2/0; pm-review 0/4/0 versus 2/2/0; pm-rust 6/1/0 versus 6/1/0; pm-context 5/5/0 versus 7/3/0; pm-linear 3/6/0 versus 8/1/0. All candidate curated-content projections are unchanged. Four candidate gates pass. pm-github baseline refuses duplicate provenance references and the candidate refuses existing public source-body privacy findings; its counts are separately labeled production-planner diagnostics, not successful gated output. The literal no-extra-creates bound against released origin/main is not met because that release still binds reference-only items, the defect in issue 230. Supplemental native matcher comparison against the accepted pre-correction 4de7d4c shows creates 22,3,7,7,8 versus candidate 22,2,6,7,8, with no additional created issue in any repository. The three requested legacy examples resolve to their curated owner. Original tracker hashes were verified, no apply/commit/push was performed in copies, all scratch copies were deleted, and 25 GitHub API reads were used in total."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T23:05:13.642Z"}],"before_hash":"d4ddfa8a6078c307af4a820e0c4b8518df278905128cbbe0d8d8e38d62c07bdc","after_hash":"811664b814dc589b44f95e132f501e349753b8f277cafa9c1b7b8fabde341688","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":"98df90512f0c72d03f5552210b29660ec848672c8edaf307cad830dd7b62ab0d"} +{"hash_algorithm":"sha256","ts":"2026-11-12T23:05:15.741Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-12T23:05:15.741Z"}],"before_hash":"811664b814dc589b44f95e132f501e349753b8f277cafa9c1b7b8fabde341688","after_hash":"d68920178f4f80c87fffd33ea9cb93bc3b83932f342206a234b99c2c19a81e5a","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":"b1fe18ef02a86621e4bb9b48f2ac307f667d4b0aaebe5666b7029b1d5e6742c3"} +{"hash_algorithm":"sha256","ts":"2026-11-12T23:07:48.872Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-12T23:07:48.872Z"},{"op":"add","path":"/metadata/assignee","value":"codex-github230-1111"},{"op":"add","path":"/metadata/claim_principal","value":"codex-github230-1111"}],"before_hash":"d68920178f4f80c87fffd33ea9cb93bc3b83932f342206a234b99c2c19a81e5a","after_hash":"16142539d3ca7cbed6b6b056fc7af510178eccc5c9292f43cb32e6e396ee2a81","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":"c331c5371a877a0f762f5761adeba72a421ad816fd5c4baab37d080bc4b00106"} +{"hash_algorithm":"sha256","ts":"2026-11-12T23:09:19.330Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/99","value":{"created_at":"2026-11-12T23:09:19.319Z","author":"codex-github230-1111","text":"Final self-review also corrected the inventory generator's prose migration: it now replaces both the original collector wording and the already-generated ownership paragraph, so the published inventory actually states unmarked leading legacy declaration compatibility. The existing three-case inventory file passes; reverting only that prose migration causes its round-trip contract assertion to fail, and restoration passes. Final lint and current typecheck pass after this change. The staged privacy audit passed across 9033 Git objects with approved identities and no secret or host-path findings. Releasing the renewed self-review claim; both PM issues remain open for orchestration."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-12T23:09:19.330Z"}],"before_hash":"16142539d3ca7cbed6b6b056fc7af510178eccc5c9292f43cb32e6e396ee2a81","after_hash":"f280c9340718b0694dcb412c7da44470b79b074efd7286e77d41362a6d62a426","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":"00e50b76112c78bb9b5245bbda9da42a965e337dec9b0aa193be91af54dbaf6a"} +{"hash_algorithm":"sha256","ts":"2026-11-12T23:09:20.557Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-12T23:09:20.557Z"}],"before_hash":"f280c9340718b0694dcb412c7da44470b79b074efd7286e77d41362a6d62a426","after_hash":"f459d44225ee167ef57514dc9637388ae197f72bb2e240c1d7421e350115e916","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":"fc56bdd5592fdff12fc3200e7762038a57999faeebcc559f1a3981b48927a1ee"} +{"hash_algorithm":"sha256","ts":"2026-11-13T00:30:19.936Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-13T00:30:19.936Z"},{"op":"add","path":"/metadata/assignee","value":"codex-github230-1111"},{"op":"add","path":"/metadata/claim_principal","value":"codex-github230-1111"}],"before_hash":"f459d44225ee167ef57514dc9637388ae197f72bb2e240c1d7421e350115e916","after_hash":"5c44b76a90e9004bd1cb88bac76cc67a94d27c024f223b2a7619d1bf596ac910","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":"7fe1a06b1629f8bc5917be3026700e236847c79aaeccc5d47abb318a5a461ef8"} +{"hash_algorithm":"sha256","ts":"2026-11-13T00:30:21.514Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/100","value":{"created_at":"2026-11-13T00:30:21.508Z","author":"codex-github230-1111","text":"Round 16 starts from clean b29cfb1; origin/main remains be28728 and is already merged. Read all seven named review threads in full, including every AI-agent prompt. Inventory index-builder callers and their scope; retain the complete peer index while refusing only in-scope targets, warning on unrelated conflicts. Validate every declared/importer number as a consistent positive safe integer; preserve legacy ownership and classify its actual declaration for repair planning. Add the reference-only webhook control, correct existing-owner docs, test native SDK must-refuse/must-allow cases and behavioral reverts, compare five read-only tracker previews with round 15, run bounded affected gates and both typechecks, then release the claim and honor the shared-runner hold before pushing. No review replies or resolutions."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-13T00:30:21.514Z"}],"before_hash":"5c44b76a90e9004bd1cb88bac76cc67a94d27c024f223b2a7619d1bf596ac910","after_hash":"4a73a2a38d3cfa5c7f7b6fde70927bac4ea254d556e63352802c5099668c31b7","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":"6fab32ee8a8a48163656a93efec8ed68db10dd8358e50e6ef920ba299bb85bdb"} +{"hash_algorithm":"sha256","ts":"2026-11-13T00:38:49.187Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/101","value":{"created_at":"2026-11-13T00:38:49.164Z","author":"codex-github230-1111","text":"Round 16 implementation: selected sync/export items and board-matched project candidates validate against the complete peer index; unrelated contradictory or duplicate owners emit structured warnings with both IDs. Import matching, persisted-plan receipts, refresh issue scopes and search-hit association now use their actual target scope. Native declarations must agree with their URL number and importer suffixes must be positive safe integers. Marker-free declarations remain repair owners; reference tags remain mentions. Native SDK regression matrix and webhook byte-preservation control added; focused verification and behavioural reverts underway."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-13T00:38:49.187Z"}],"before_hash":"4a73a2a38d3cfa5c7f7b6fde70927bac4ea254d556e63352802c5099668c31b7","after_hash":"56d7c25bfae010fc47ed7f284525a8587491c2fe1a9bb71a115178970473ed8e","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":"a89d63e43cf630df31d8e1643c554fd19067061bc259696dbea615d704aa3a5a"} +{"hash_algorithm":"sha256","ts":"2026-11-13T00:43:06.405Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/102","value":{"created_at":"2026-11-13T00:43:06.393Z","author":"codex-github230-1111","text":"Gate follow-up: blockers coverage had an old expectation that clearing github_url also removed ownership despite retaining a valid leading GH issue declaration. The corrected legacy contract keeps that declaration authoritative. Added the declaration-only control and made the reference-only variant explicitly use middle-of-description prose; production blocker selection is unchanged. Current-sdk typecheck, test compilation, lint and docstring pass."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-13T00:43:06.405Z"}],"before_hash":"56d7c25bfae010fc47ed7f284525a8587491c2fe1a9bb71a115178970473ed8e","after_hash":"d48b82479bf72794063486d15f8c30b6da41684f3d5318441c379c86622675c1","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":"96b774448d4730e4b30d558bf05c2c16ad56d4f87e06dbd44076673a01c1cd1d"} +{"hash_algorithm":"sha256","ts":"2026-11-13T00:46:01.479Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/103","value":{"created_at":"2026-11-13T00:46:01.466Z","author":"codex-github230-1111","text":"Read-only public-data replay of round-15 tracker commits and complete issue snapshots, native CLI/SDK, exact state-all atomic gated preview: pm-github 22 create / 2 update / 0 unchanged; pm-review 2/2/0; pm-rust 6/1/0; pm-context 7/3/0; pm-linear 8/1/0. All counts identical to round 15, no new creates and zero curated-field changes; item/history bytes unchanged and scratch copies deleted. No new GitHub API reads were needed. The existing pm-github source-body privacy refusal remains; its counts come from the read-only production planner, not a successful gate. The other four native gates passed."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-13T00:46:01.479Z"}],"before_hash":"d48b82479bf72794063486d15f8c30b6da41684f3d5318441c379c86622675c1","after_hash":"b2920f4efa0b1f2a89d0733be8b12d5119694c2d1b0b3612b6c6591b4b184a74","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":"d834cead52defa772dbf7deebec7bdb89867404683ca4234152c6529d49020ce"} +{"hash_algorithm":"sha256","ts":"2026-11-13T01:03:06.814Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/104","value":{"created_at":"2026-11-13T01:03:06.802Z","author":"codex-github230-1111","text":"The import-gate receipt fixture expected duplicate owners for issue 1 to refuse an import of issue 2, exactly the out-of-scope behavior this round removes. Corrected its must-refuse branch to target issue 1 and added a must-allow receipt for issue 2. The native SDK matrix independently covers the scoped import flow; the unchanged receipt checks still reject missing provenance and two entries targeting one item. No production adjustment was needed after this assertion."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-13T01:03:06.814Z"}],"before_hash":"b2920f4efa0b1f2a89d0733be8b12d5119694c2d1b0b3612b6c6591b4b184a74","after_hash":"0fa4948d7494f9a984a2dbd36b6505d667242708ed2123b46bba302b534d6c80","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":"5780791c252b7f2de3552d46411d4bdbaad50a1ef19be1e10ef551dc227c84a7"} +{"hash_algorithm":"sha256","ts":"2026-11-13T01:11:09.747Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/105","value":{"created_at":"2026-11-13T01:11:09.739Z","author":"codex-github230-1111","text":"The packed consumer test hit its existing subprocess timeout while the broader gate was busy; its rerun is queued with the public-registry transport timer. The source-level mixed-import projection cases passed. Final ownership behavior preserves the accepted precedence of valid explicit/importer provenance over copied description markers, while malformed legacy number declarations yield no declaration claim and an invalid_github_identity_number warning. Scope and positive safe-integer fixes remain the only production behavior changes in this round."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-13T01:11:09.747Z"}],"before_hash":"0fa4948d7494f9a984a2dbd36b6505d667242708ed2123b46bba302b534d6c80","after_hash":"060aea11a78066209fe35f238ff7d89cfc5d138db8446ed39401f429e97238df","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":"08fb11e5f14bc1ecefee8e4707b354e388a314da0efb2a8da8ca46d49eb05444"} +{"hash_algorithm":"sha256","ts":"2026-11-13T01:40:15.205Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/85","value":{"command":"timeout 1200 node --test --test-concurrency=1 test/scoped-ownership.test.ts","scope":"project","provenance":{"author":"codex-github230-1111","created_at":"2026-11-13T01:40:15.131Z","source_kind":"local_mutation","source_ref":"fix/reference-tags-are-not-import-identity"}}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-13T01:40:15.205Z"}],"before_hash":"060aea11a78066209fe35f238ff7d89cfc5d138db8446ed39401f429e97238df","after_hash":"affaa1805a78eed1b1ecd4e2bee696d2f6f2d40499def5abe82120c22c1948cb","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":"32f7eb0257a697b2da8f0fae22f7d5f8c9ce57104b11906c471f8d0d63c86648"} +{"hash_algorithm":"sha256","ts":"2026-11-13T01:45:47.172Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/106","value":{"created_at":"2026-11-13T01:45:47.164Z","author":"codex-github230-1111","text":"Round 16 behavioral revert proof (disposable source copies; native SDK trackers; fixed source restored afterward): 11 mutants produced 17 expected assertion failures. Function versions came from b29cfb1 for planSync/buildExportPlan, legacyGithubDescriptionIdentity, githubImporterIdentity, normalizeGithubClaim, graph, indexByImportIdentity, indexByProvenance and selectRefreshTargets. The board mutation re-expanded indexPmByIssue to all selected tracker items; the peer control disabled the competitor refusal; the webhook mutation restored tag-derived ownership. No mutation touched the package worktree.\noutbound-scope: timeout 1200 node --test --test-concurrency=1 \"--test-name-pattern=--ids allows\" test/scoped-ownership.test.ts => 0 pass, 2 expected failures.\nboard-scope: timeout 1200 node --test --test-concurrency=1 \"--test-name-pattern=^board-(import|pull) allows\" test/scoped-ownership.test.ts => 0 pass, 2 expected failures.\ndeclared-number: timeout 1200 node --test --test-concurrency=1 \"--test-name-pattern=^native declaration rejects\" test/scoped-ownership.test.ts => 2 pass, 2 expected failures.\nunsafe-suffix: timeout 1200 node --test --test-concurrency=1 \"--test-name-pattern=^verified importer suffix\" test/scoped-ownership.test.ts => 1 pass, 1 expected failures.\nexternal-number: timeout 1200 node --test --test-concurrency=1 \"--test-name-pattern=^external_ref number\" test/scoped-ownership.test.ts => 1 pass, 1 expected failures.\nrepair-declaration: timeout 1200 node --test --test-concurrency=1 \"--test-name-pattern=^matching marker-free declaration\" test/scoped-ownership.test.ts => 0 pass, 1 expected failures.\nselected-peer-control: timeout 1200 node --test --test-concurrency=1 \"--test-name-pattern=still refuses a peer\" test/scoped-ownership.test.ts => 0 pass, 4 expected failures.\nimport-target-scope: timeout 1200 node --test --test-concurrency=1 \"--test-name-pattern=^gated-import scopes\" test/scoped-ownership.test.ts => 0 pass, 1 expected failures.\nsearch-target-scope: timeout 1200 node --test --test-concurrency=1 \"--test-name-pattern=^search scopes\" test/scoped-ownership.test.ts => 0 pass, 1 expected failures.\nrefresh-target-scope: timeout 1200 node --test --test-concurrency=1 \"--test-name-pattern=^refresh scopes\" test/scoped-ownership.test.ts => 0 pass, 1 expected failures.\nwebhook-reference-only: timeout 1200 node --test --test-concurrency=1 \"--test-name-pattern=^old reopened events\" test/webhooks.test.ts => 0 pass, 1 expected failures.\nrestored-matrix: timeout 1200 node --test --test-concurrency=1 test/scoped-ownership.test.ts => 24 pass, 0 expected failures.\nrestored-webhook: timeout 1200 node --test --test-concurrency=1 \"--test-name-pattern=^old reopened events\" test/webhooks.test.ts => 1 pass, 0 expected failures."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-13T01:45:47.172Z"}],"before_hash":"affaa1805a78eed1b1ecd4e2bee696d2f6f2d40499def5abe82120c22c1948cb","after_hash":"4f884c5056cbe4055077148b6c15b25e5562226f856298a3efda50c246d308de","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":"6ca7654255375cd585b810cae291247d80118d4fc56f2306cd8f5811390742f9"} +{"hash_algorithm":"sha256","ts":"2026-11-13T01:45:48.085Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/107","value":{"created_at":"2026-11-13T01:45:48.078Z","author":"codex-github230-1111","text":"Round 16 gate receipts: 52 complete affected test files: 1,232 passed, 2 pre-existing pm-cli#1656 SDK compensation quarantines, zero failures. Every command capped at 1,200 seconds with at most two simultaneous workers. The import-gate file reached its 20-minute cap after 60 passes, one stale receipt assertion and one cancellation; the corrected receipt assertion subsequently passed separately (1/1). The remaining workflow cases are for CI. A transient packed projection timeout passed on the full retry (6/6).\ntimeout 1200 node --test --test-concurrency=1 test/atomic.test.ts => 11 passed, 0 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/changelog-placement.test.ts => 5 passed, 0 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/cited-issue-adoption.test.ts => 46 passed, 0 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/blockers.test.ts => 35 passed, 0 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/comments-sync.test.ts => 14 passed, 0 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/complete-corpus.test.ts => 6 passed, 0 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/coverage-boundaries.test.ts => 8 passed, 0 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/coverage-edges.test.ts => 13 passed, 0 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/closeout.test.ts => 93 passed, 0 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/coverage-remainder.test.ts => 25 passed, 0 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/daily-sync-idempotency.test.ts => 3 passed, 0 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/discussions.test.ts => 16 passed, 0 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/dryrun-preview.test.ts => 2 passed, 0 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/durable-recovery.test.ts => 11 passed, 0 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/gate.test.ts => 50 passed, 0 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/github-url.test.ts => 36 passed, 0 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/handler-failures.test.ts => 38 passed, 0 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/http-boundary.test.ts => 39 passed, 0 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/identity-inventory.test.ts => 3 passed, 0 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/identity.test.ts => 20 passed, 0 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/import-errors.test.ts => 9 passed, 0 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/discussion-model.test.ts => 1 passed, 0 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/import-fidelity.test.ts => 53 passed, 1 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/import-lock.test.ts => 25 passed, 0 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/import-ownership.test.ts => 28 passed, 0 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/import-projection.test.ts => 6 passed, 0 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/import-regex.test.ts => 8 passed, 0 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/import-selection.test.ts => 26 passed, 0 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/issue-forms.test.ts => 11 passed, 0 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/label-import-model.test.ts => 3 passed, 0 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/legacy-description-compatibility.test.ts => 8 passed, 0 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/link-deps.test.ts => 28 passed, 0 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/own-property.test.ts => 5 passed, 0 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/owner-field-values.test.ts => 24 passed, 0 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/labels.test.ts => 13 passed, 0 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/project-sync.test.ts => 21 passed, 0 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/projects.test.ts => 39 passed, 0 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/reference-import-identity.test.ts => 17 passed, 0 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/refresh.test.ts => 40 passed, 1 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/repeat-import-noop.test.ts => 9 passed, 0 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/review-import-identity.test.ts => 30 passed, 0 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/review-round3-identity.test.ts => 29 passed, 0 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/round-seven.test.ts => 23 passed, 0 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/round-twelve.test.ts => 32 passed, 0 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/scheduled-refusals.test.ts => 20 passed, 0 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/scoped-ownership.test.ts => 24 passed, 0 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/sdk-adoption.test.ts => 4 passed, 0 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/smoke.test.ts => 72 passed, 0 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/typed-sentinel-normalization.test.ts => 3 passed, 0 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/webhooks.test.ts => 12 passed, 0 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/workspace-resolution.test.ts => 2 passed, 0 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/ownership-consistency.test.ts => 133 passed, 0 skipped.\ntimeout 1200 node --test --test-concurrency=1 --test-name-pattern ^verifyImportIdempotency test/import-gate.test.ts => 1 passed.\nnpm run typecheck; npm run build:test; npm run lint; npm run docstring; node scripts/identity-inventory.ts --check; npm run privacy => passed. Docstrings: 33 modules, 314 declarations; inventory: 95 function groups, 295 sites, 5 auxiliary anchors. PM_GITHUB_TEST_NETWORK_ATTEMPT_TIMEOUT_MS=1000 timeout 1200 npm run test:minimum-sdk => 1 passed against the declared 2026.10.23 floor. Pinned pm test --run --only-last --progress executed the linked scoped-ownership command: 24/24 passed. This repo disables automatic test-result tracker writes, so this comment records the result. Full release/coverage/Bun gates remain with CI under the explicit round budget."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-13T01:45:48.085Z"}],"before_hash":"4f884c5056cbe4055077148b6c15b25e5562226f856298a3efda50c246d308de","after_hash":"24b5f3f6e5ed4028ea6ce30ac9dd38632bf2c7666b86309c93240166ab96522a","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":"bf0ba88e2b7c05de81646eb5374394628c3dda1741ed2291e4320de166be2772"} +{"hash_algorithm":"sha256","ts":"2026-11-13T01:45:49.093Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/108","value":{"created_at":"2026-11-13T01:45:49.086Z","author":"codex-github230-1111","text":"Round 16 final scope/data evidence: read all 7 requested threads, all 7 full comments and all 6 AI-agent prompts (Greptile has none). Selected IDs, board candidates and incoming targets validate against complete peers. Both IDs appear in off-scope collision warnings; a peer claiming an in-scope issue still refuses. Native declarations require their stated number to equal the URL number; unsafe importer suffixes and unsafe GitHub external_ref numbers establish no claim. Marker-free declarations remain repair owners, and reference-only webhook items remain byte-identical. The syntax-generated inventory documents every builder caller and its scope in docs/identity-ownership-inventory.md.\nRead-only public-data comparison replays the exact round-15 complete source snapshots against scratch clones pinned to the same public tracker commits; no apply and no new GitHub API reads. Round 15 -> round 16 create/update/unchanged: pm-github 22/2/0 -> 22/2/0; pm-review 2/2/0 -> 2/2/0; pm-rust 6/1/0 -> 6/1/0; pm-context 7/3/0 -> 7/3/0; pm-linear 8/1/0 -> 8/1/0. No extra creates and no curated-field changes. pm-github retains the known privacy preflight refusal on existing public issue bodies, so its counts are the read-only production planner diagnostic; the other four native gated previews pass. Native item/history hashes stayed identical and every scratch clone was deleted. No fleet checkout was written.\nOpen risks: CI must certify the capped import-gate remainder and full gates; two existing SDK compensation quarantines remain; the unchanged public-source privacy refusal remains visible. No new SDK gap or CLI defect was discovered. Claim will be released without closing; changelog regeneration and integrity checks follow the final tracker write."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-13T01:45:49.093Z"}],"before_hash":"24b5f3f6e5ed4028ea6ce30ac9dd38632bf2c7666b86309c93240166ab96522a","after_hash":"3f243dd3b8aaa159c23f26eda64a0e25fe2e4ee9864b44b5f2d4f90dc512fbbf","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":"1a1b0fe4b3b93272da368c78731f7871f58a918b2e8fd1b80aed157c14899469"} +{"hash_algorithm":"sha256","ts":"2026-11-13T01:45:49.995Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-13T01:45:49.995Z"}],"before_hash":"3f243dd3b8aaa159c23f26eda64a0e25fe2e4ee9864b44b5f2d4f90dc512fbbf","after_hash":"7451e33500ed7df3b5d46d8736aa14bbdaaa017d918d72dcb13f0d90ee858a9e","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":"f6b603759eac44c12a8f87f42ca30bd38403d8af3749c616085f09f84eb74393"} +{"hash_algorithm":"sha256","ts":"2026-11-13T01:47:56.635Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/109","value":{"created_at":"2026-11-13T01:47:56.629Z","author":"codex-github230-1111","text":"Final read-only checks: npm run changelog:full, npm run changelog:check and npm run privacy passed. The history remains a byte-prefix extension of b29cfb1; origin/main has no earlier record for this branch-created item. SDK verifyHistoryEntries verified all 233 current events with zero errors. Extra pm health --check-only --skip-vectors --strict-exit --summary reports native integrity OK, but exits 1 for the existing extension_host_pm_cli_version_skew (pinned 2026.11.6 versus host 2026.11.12) and also reports six stale in-progress items. Those are not part of the passing targeted gates and no version, telemetry or unrelated tracker data was changed. Claim is released; item remains in_progress and open."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-13T01:47:56.635Z"}],"before_hash":"7451e33500ed7df3b5d46d8736aa14bbdaaa017d918d72dcb13f0d90ee858a9e","after_hash":"57bb05f95a50262509395a260680c74a3a431b978336ac76e8e087ed7164f67b","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":"05ee515f2ea3ca2b51fd4270ddd8b8f5fbee50ccf85f090dea3094a54183b0eb"} +{"hash_algorithm":"sha256","ts":"2026-11-13T03:52:05.149Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-13T03:52:05.149Z"},{"op":"add","path":"/metadata/assignee","value":"codex-github230-1111"},{"op":"add","path":"/metadata/claim_principal","value":"codex-github230-1111"}],"before_hash":"57bb05f95a50262509395a260680c74a3a431b978336ac76e8e087ed7164f67b","after_hash":"140ee17026fa7bcb65955364427f9f4d001e91db72752eb658fce2876b9d5cc7","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":"32ba92aa71cc2b94d51ded1d17dcfbb38d017f97c1d13166572188fe14cc7d0f"} +{"hash_algorithm":"sha256","ts":"2026-11-13T03:52:07.312Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/110","value":{"created_at":"2026-11-13T03:52:07.302Z","author":"codex-github230-1111","text":"Round 17: reproduce the unchanged 100 percent coverage gate first, then cover all missing behavior and fix the shared declaration diagnostics, deferred webhook deliveries and curated projection fixture. Read all six requested review threads including their agent prompts; behavioral reverts and the same five public snapshot previews will certify the fixes. Keep all thresholds, append-only history and current ownership guards."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-13T03:52:07.312Z"}],"before_hash":"140ee17026fa7bcb65955364427f9f4d001e91db72752eb658fce2876b9d5cc7","after_hash":"022812881ae0195866e7f015e3d6e4e45f59411497c73badd1c453e49248c22b","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":"a768a6a6dc86ffe3af76a810557803c66882dd8332a135c65453b1e3c2dd2890"} +{"hash_algorithm":"sha256","ts":"2026-11-13T03:55:12.935Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/111","value":{"created_at":"2026-11-13T03:55:12.927Z","author":"codex-github230-1111","text":"Round 17 review receipt: five requested threads, five complete comments and four AI-agent prompts read (CodeQL has no agent prompt). This corrects the initial plan count. Baseline bun run coverage is running before source edits, with the original thresholds, real SDK storage and two workers. The diagnostic currently duplicates native declaration parsing, and webhook reconciliation currently acknowledges a delivery even when refresh deferred that issue."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-13T03:55:12.935Z"}],"before_hash":"022812881ae0195866e7f015e3d6e4e45f59411497c73badd1c453e49248c22b","after_hash":"2130a4636e4a2851b9cac62910c449ec05f7d2f8293cf94c8f521d8776f67159","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":"f7afc3802319cd7e9ca72e658be3b8b99f4732a0921900464022b1cb74334172"} +{"hash_algorithm":"sha256","ts":"2026-11-13T04:40:52.121Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/65/path","value":"test/workspace-resolution.test.ts"},{"op":"replace","path":"/metadata/files/64/path","value":"test/webhooks.test.ts"},{"op":"replace","path":"/metadata/files/63/path","value":"test/typed-sentinel-normalization.test.ts"},{"op":"replace","path":"/metadata/files/62/path","value":"test/smoke.test.ts"},{"op":"replace","path":"/metadata/files/61/path","value":"test/sdk-adoption.test.ts"},{"op":"replace","path":"/metadata/files/60/path","value":"test/scheduled-refusals.test.ts"},{"op":"replace","path":"/metadata/files/59/path","value":"test/round-twelve.test.ts"},{"op":"replace","path":"/metadata/files/58/path","value":"test/round-seventeen.test.ts"},{"op":"add","path":"/metadata/files/66","value":{"path":"webhooks.ts","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-13T04:40:52.121Z"}],"before_hash":"2130a4636e4a2851b9cac62910c449ec05f7d2f8293cf94c8f521d8776f67159","after_hash":"1497860bf2b1433537dbddd186f60d30d335e0738a98967d3c955cc868fbbc05","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":"b69deff79d846f9b3ff665b0002d8b52683b0ae6b71ca8960df3669248a340d3"} +{"hash_algorithm":"sha256","ts":"2026-11-13T04:40:53.074Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/86","value":{"command":"timeout 1200 node --test --test-concurrency=2 test/round-seventeen.test.ts test/identity-inventory.test.ts","scope":"project","provenance":{"author":"codex-github230-1111","created_at":"2026-11-13T04:40:53.012Z","source_kind":"local_mutation","source_ref":"fix/reference-tags-are-not-import-identity"}}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-13T04:40:53.074Z"}],"before_hash":"1497860bf2b1433537dbddd186f60d30d335e0738a98967d3c955cc868fbbc05","after_hash":"127ab358b57446a88c8029c574561f4a605e8337ba7716b1a59345102baba49c","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":"88fa295aa8096dacd4f2000fa8365e9c581e9fc50ba08395c5e87009820c8cb8"} +{"hash_algorithm":"sha256","ts":"2026-11-13T04:40:53.911Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/112","value":{"created_at":"2026-11-13T04:40:53.903Z","author":"codex-github230-1111","text":"Round 17 implementation: one shared native-declaration parser now supplies ownership and numeric diagnostics; the local URL regex and quadratic trim are removed. Quoted, bracketed and punctuated valid declarations agree with ownership, while real number mismatches warn. Refresh reports all deferred source issue numbers, and webhook reconciliation leaves their receipts pending while independent deliveries progress. The projection fixture now has explicit provenance and asserts one existing item plus literal tag preservation. Native Node 22 regressions and inventory CLI cases pass 26/26; full SDK reads ensure the performance fixture contains all 20,000 closing parentheses. The initial unfiltered bun coverage attempt reached its 45-minute cap before producing an aggregate report; no local coverage percentage is inferred from that timeout. Main 9a72437 was merged as a5f0273, with the changelog regenerated by the package command."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-13T04:40:53.911Z"}],"before_hash":"127ab358b57446a88c8029c574561f4a605e8337ba7716b1a59345102baba49c","after_hash":"1ca4a534f24301f9786aecd106f9e6bab7fa1550b72fa8d5e5e76ff3ba860736","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":"04839e4342ff5834e743f55bd92845d6b100ebe5892e8b37bee3b47afb825585"} +{"hash_algorithm":"sha256","ts":"2026-11-13T04:40:54.896Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/113","value":{"created_at":"2026-11-13T04:40:54.889Z","author":"codex-github230-1111","text":"Coverage-only simplification proof: native ownership snapshots use non-provisional claims. Their evidence flag implies at least one owner; owners are a subset of claims, and shared consistency refuses more than one claim. Therefore the later closeout evidence-without-one-owner check and its owners.length !== 1 disjunct cannot reject a native item. Refresh also receives at most one validated key, and every accepted collector produces a positive safe number, making the repeated cardinality and parsed-number checks unreachable. The raw github_number validation remains because malformed partial metadata can reach it. Real SDK controls verify unowned/same-repository closeout allowance, foreign/contradictory-owner refusal, invalid raw-number refusal, duplicate-record refusal, and valid refresh selection. No thresholds, source inventories or ignore rules changed."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-13T04:40:54.896Z"}],"before_hash":"1ca4a534f24301f9786aecd106f9e6bab7fa1550b72fa8d5e5e76ff3ba860736","after_hash":"8db33c9cb46e48a19ef02c28535e098ecccc79a04ed420bf90254ec80fa8ca55","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":"4eeaeb087f39897898caa7230da27d732189fcd92edc1b50ba9f4286d71019e1"} +{"hash_algorithm":"sha256","ts":"2026-11-13T04:53:56.803Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/114","value":{"created_at":"2026-11-13T04:53:56.786Z","author":"codex-github230-1111","text":"Round 17 public read-only snapshot replay is complete: round-16 tracker commits and the same public source snapshots were replayed through the current packed build and native CLI/SDK. Create/update/unchanged counts remain pm-github 22/2/0, pm-review 2/2/0, pm-rust 6/1/0, pm-context 7/3/0 and pm-linear 8/1/0. No extra creates or curated-field changes. pm-github retains the existing public-source privacy preflight refusal, so its counts are the read-only production-planner diagnostic; the other four gated previews pass. No import was applied, source item/history hashes remain intact, zero new GitHub API reads were needed, and all scratch clones were deleted. Current typecheck, test-build typecheck, lint and the 33-module/315-declaration docstring gate pass; the minimum-SDK consumer remains queued behind the active two-worker coverage run."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-13T04:53:56.803Z"}],"before_hash":"8db33c9cb46e48a19ef02c28535e098ecccc79a04ed420bf90254ec80fa8ca55","after_hash":"9eb345d3f7149441691f8e4ef8a038f497fc45d18c927082f91895b7696947df","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":"62cb1255765d8c998126b2096de5796af582aa6027de5f6d4bc6cb5def779c6d"} +{"hash_algorithm":"sha256","ts":"2026-11-13T05:31:33.546Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/115","value":{"created_at":"2026-11-13T05:31:33.539Z","author":"codex-github230-1111","text":"Additional coverage reachability proof: legacyGithubIdentityConflict is a retained compatibility API whose body always returns undefined, as required when gh: tags are mentions. Its former positive branches in import, sync and refresh can never execute for any item. Their scans and conditional emissions were removed, while empty typed receipt fields preserve the public optional identity_conflicts type and runtime absence. The shared explicit-ownership and peer checks still run. Minimum-SDK packed consumer validation passes 1/1 against 2026.10.23. The updated native regressions, inventory CLI and entire projection file pass 33/33. Focused instrumentation includes 87 test files and all 33 authored source modules with unchanged four-way 100 percent thresholds; slow suites remain for separate validation. No ignore rule or persistent gate configuration was changed."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-13T05:31:33.546Z"}],"before_hash":"9eb345d3f7149441691f8e4ef8a038f497fc45d18c927082f91895b7696947df","after_hash":"c12573832825caf1332eb4b6cce5a44593c3dc0a471d715a1c84ae775b4f8e28","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":"654777330ac49b98b55424070bcfd37dd3d59a494353008200d06819ed1eb2c0"} +{"hash_algorithm":"sha256","ts":"2026-11-13T06:16:01.497Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/116","value":{"created_at":"2026-11-13T06:16:01.427Z","author":"codex-github230-1111","text":"Coverage completion uses the native SDK and retained native V8 receipts at unchanged 100-percent thresholds for every authored module. Added behavioral outcomes for ownership index delegates and counters, missing dependency endpoints, malformed recovery targets, additive tags, outbound ownership drift, and exclusive recovery-plan publication. Eight targeted closeout revision/dependent-side-effect cases pass. The full baseline coverage command reached its 45-minute cap, so bounded source-complete test batches and the standard native receipt aggregator are used; no module exclusion or receipt URL filtering is introduced."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-13T06:16:01.497Z"}],"before_hash":"c12573832825caf1332eb4b6cce5a44593c3dc0a471d715a1c84ae775b4f8e28","after_hash":"b19cdb22dc80f565c0559f3b40053b9e2cbbf1eeea950076fb1ba6c59eab545a","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":"39cf4ef4cc977e37cabb83445d6321e01c559eb1a3d3ee5444c9d874c1159800"} +{"hash_algorithm":"sha256","ts":"2026-11-13T06:24:43.676Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/117","value":{"created_at":"2026-11-13T06:24:43.669Z","author":"codex-github230-1111","text":"Unreachable-branch proof: ownership index builders and updates always install a states entry together with byId and retain every claimant map, so fallback states/maps in private cache replacement cannot be absent. verifyImportIdempotency first selects a claim in the requested set, then the shared consistency check requires exactly one consistent unique owner; its later different-key/duplicate-owner branches cannot run. The atomic catch already rethrows both ownership codes before cleanup, making its second identical owner check unreachable. Plain citation matches always set linkOnly or preserveContent in prepareGithubImport and leave through that earlier branch, so the later mirroring branch cannot increment adopted. Removed only these redundant paths; typed shared refusals, reference-tag freedom and counters remain."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-13T06:24:43.676Z"}],"before_hash":"b19cdb22dc80f565c0559f3b40053b9e2cbbf1eeea950076fb1ba6c59eab545a","after_hash":"e48cb4115d9dc944e9aaec016a0e8c4032939b4dbffaa2d7c86d65e5db25ac41","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":"4c0dfa54f0fb67773d7a25cc8122b7027378e428de9bb331f8acddaffd7372f0"} +{"hash_algorithm":"sha256","ts":"2026-11-13T06:27:14.020Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/118","value":{"created_at":"2026-11-13T06:27:13.998Z","author":"codex-github230-1111","text":"Native SDK boundary observation: mutateItem permits a low-level callback to remove metadata.id, then listAllComplete correctly refuses that malformed document with list_source_incomplete. The disposable fixture now asserts that refusal and exercises the ownership cache missing-id boundary; no package behavior is weakened and no upstream SDK repair is included."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-13T06:27:14.020Z"}],"before_hash":"e48cb4115d9dc944e9aaec016a0e8c4032939b4dbffaa2d7c86d65e5db25ac41","after_hash":"41e64137b9dc3e54efef8f9e30d97adcfdfdc3736f74883c7c9ddc78fd0c2630","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":"373739a3aea153e09bdda96a3e42986825224357c9cd9e24bfaa3f15a665352a"} +{"hash_algorithm":"sha256","ts":"2026-11-13T06:29:42.670Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/119","value":{"created_at":"2026-11-13T06:29:42.608Z","author":"codex-github230-1111","text":"Behavioral revert proof against 58ae396 in a disposable tracked-source copy, with finally restoration: reverting warnGithubOwnershipScope alone makes 6 of 9 boundary/diagnostic/20000-parenthesis regressions fail (valid controls remain); reverting runRefresh alone makes both deferred-number regressions fail; reverting reconcileGithubWebhooks alone makes the pending-delivery regression fail; removing only the curated fixture github_url seed makes both additive-provider-link cases fail because an extra item is created. Restored implementations pass 42/42 round-seventeen tests plus 2/2 fixture cases, with no setup exceptions or cancellations. Commands use timeout 1200 node --test --test-concurrency=1 and name patterns shared declaration boundaries/shared declaration diagnostics/native declaration diagnostics finish, deferred webhook/legacy retained refresh plans, deferred webhook, and additive provider links respectively."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-13T06:29:42.670Z"}],"before_hash":"41e64137b9dc3e54efef8f9e30d97adcfdfdc3736f74883c7c9ddc78fd0c2630","after_hash":"1755eb3253820cc472dbf2a351ff9a64aeb84c35e1afdd66abc770bfe77dbbac","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":"acd8902f4458da88043b5349205974e8e599fd09482fa0d829de963193d2aac6"} +{"hash_algorithm":"sha256","ts":"2026-11-13T06:33:47.899Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/120","value":{"created_at":"2026-11-13T06:33:47.876Z","author":"codex-github230-1111","text":"Read-only final-source public snapshot replay completed against the same round-16 tracker commits and source snapshots, with the exact scheduled --state all --atomic --gate --dry-run --json command and native installed package. Create/update/unchanged counts are identical: pm-github 22/2/0; pm-review 2/2/0; pm-rust 6/1/0; pm-context 7/3/0; pm-linear 8/1/0. No extra creates, no curated-field changes, all original tracker hashes unchanged, scratch clones removed, zero new GitHub issue API calls. pm-github preserves the already observed public-body privacy-preflight refusal; its identical counts are the read-only production-planner diagnostic, not a claimed passing gated command."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-13T06:33:47.899Z"}],"before_hash":"1755eb3253820cc472dbf2a351ff9a64aeb84c35e1afdd66abc770bfe77dbbac","after_hash":"0e56b1149a874bb71fbec29faa118333a04729b99154040491ec79f588c651fe","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":"f2584170aad2bb03aa173a7b609ea6d75046229cffc5ea7ef9f82adef0c17649"} +{"hash_algorithm":"sha256","ts":"2026-11-13T06:57:20.597Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/121","value":{"created_at":"2026-11-13T06:57:20.579Z","author":"codex-github230-1111","text":"Standalone import-gate.test.ts reached its required 20-minute cap after 70 passing cases and no assertion failures; three tail cases remain for a separate bounded command. Full closeout-revision.test.ts passes 110/110. The final source-complete coverage capture uses the current build, Node 22 for parent and children, two test workers, fresh native V8 receipts and the unchanged runCoverageGate source inventory/threshold validation. Current source and test typechecks, lint and the 33-file/315-declaration docstring gate pass."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-13T06:57:20.597Z"}],"before_hash":"0e56b1149a874bb71fbec29faa118333a04729b99154040491ec79f588c651fe","after_hash":"e15e305b58475557d8f074a47be9a05a9c30bf9c739c4fc415a80e0787754627","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":"456dfd9893b89548cbe13ed5be9c4f43a0d86a2ed610635cb154d769e49fd227"} +{"hash_algorithm":"sha256","ts":"2026-11-13T07:12:37.021Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/122","value":{"created_at":"2026-11-13T07:12:37.014Z","author":"codex-github230-1111","text":"Final frozen-source Node 22 coverage test phase completed: 1879 tests, 1877 passed, two existing SDK-1656 compensation quarantines skipped, zero failures/cancellations, 1655776 ms. This includes all 51 round-seventeen regressions, five inventory tests, both curated fixture modes, and the shared-ownership freshness/cost matrix. Native all-source c8 aggregation is still running; no passing coverage claim yet. Explicit minimum-SDK and complete import-gate reruns start only after those two test workers exit."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-13T07:12:37.021Z"}],"before_hash":"e15e305b58475557d8f074a47be9a05a9c30bf9c739c4fc415a80e0787754627","after_hash":"90ad260cf525d7bf3fbfad0a2a73c1a33296603e3ff43b4114689e45c711ce2e","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":"2754907b011dc3423665c4639656329c8dabcc7e6caeb25843972ea8b482e4ae"} +{"hash_algorithm":"sha256","ts":"2026-11-13T07:15:47.147Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/tests/86/provenance/created_at","value":"2026-11-13T07:15:47.016Z"},{"op":"replace","path":"/metadata/tests/86/command","value":"timeout 1200 node --test --test-concurrency=1 test/round-seventeen.test.ts test/identity-inventory.test.ts"},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-13T07:15:47.147Z"}],"before_hash":"90ad260cf525d7bf3fbfad0a2a73c1a33296603e3ff43b4114689e45c711ce2e","after_hash":"ec8ea7550550f0c682bab1544ba67ed5b62652f42b137e628ae5a856dd21f674","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":"1b6056ecfda07db4e9adf63056352cb03f8b82d1144eb31b1a4e08cdcb215779"} +{"hash_algorithm":"sha256","ts":"2026-11-13T07:46:07.622Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/123","value":{"created_at":"2026-11-13T07:46:07.592Z","author":"codex-github230-1111","text":"Round 17 final gates: unchanged native coverage validator passes all 34 authored modules at exact 100/100/100/100, with complete per-module JSON and LCOV receipts and zero skipped source. The 1879-test source-complete batch passes 1877 with two existing SDK-1656 quarantines; complete import-gate.test.ts passes 73/73 in 1011816 ms; closeout-revision.test.ts passes 110/110; model files pass 4/4; final regressions pass 57/57; pinned pm test --run --match round-seventeen --progress passes 62/62 with tracking disabled. Current source/test typechecks, minimum SDK 2026.10.23 consumer, lint, docstrings (33 TypeScript files/315 declarations), inventory anchors and working-tree privacy pass; privacy audited 9091 objects. All native receipts use the same frozen source, without URL filtering, exclusions, ignore comments or lower thresholds. The unfiltered initial coverage attempt hit the 45-minute cap; bounded native batches plus the standard all-source receipt validator certify local coverage, and CI certifies the full release/Bun suite."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-13T07:46:07.622Z"}],"before_hash":"ec8ea7550550f0c682bab1544ba67ed5b62652f42b137e628ae5a856dd21f674","after_hash":"86bc3a14006f728e00ca00296b0b0f316712fa49de55336776638fcb7621ad38","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":"a7b26b88603b09984499d16fc4d26f7e1d178cbd982f04689705c36243327021"} +{"hash_algorithm":"sha256","ts":"2026-11-13T07:46:09.642Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/124","value":{"created_at":"2026-11-13T07:46:09.617Z","author":"codex-github230-1111","text":"Final behavioural revert refresh: warnGithubOwnershipScope alone reverted to 58ae396 causes 6/9 selected tests to fail, runRefresh alone causes 2/2 deferred-number failures, reconcileGithubWebhooks alone causes 1/1 pending-delivery failure, and removing the explicit github_url fixture seed causes 2/2 additive-provider-link failures. Restoring the implementation passes all 57 regressions and both fixture cases; no setup errors or cancellations. Read all five requested threads in full, five comments and four AI-agent prompts (CodeQL supplied no prompt). No thread replies or resolutions. The accepted mention/ownership contract is unchanged; unrelated deferred work remains in issue 236. Native history verifies and remains a byte-prefix extension of the accepted head."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-13T07:46:09.642Z"}],"before_hash":"86bc3a14006f728e00ca00296b0b0f316712fa49de55336776638fcb7621ad38","after_hash":"a912500b6e5bb303e4aa59ecac878192b726fc415baa4e379c7666a92be87e30","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":"69e1f975d7f9f86d6a565f95d57e8c462122e262973a64a357b33f9eb3a188bf"} +{"hash_algorithm":"sha256","ts":"2026-11-13T07:46:11.636Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-13T07:46:11.636Z"}],"before_hash":"a912500b6e5bb303e4aa59ecac878192b726fc415baa4e379c7666a92be87e30","after_hash":"aedc16a39f3ffdd071377b355d58944f05dd10c073bd6e15bd13f8f2705d3433","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":"dfe7ea14c7aaf25236ce4a1c27dd0293547c20bc27fd46c937cf71a943d4554b"} +{"hash_algorithm":"sha256","ts":"2026-11-13T08:07:10.978Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-13T08:07:10.978Z"},{"op":"add","path":"/metadata/assignee","value":"codex-github230-1111"},{"op":"add","path":"/metadata/claim_principal","value":"codex-github230-1111"}],"before_hash":"aedc16a39f3ffdd071377b355d58944f05dd10c073bd6e15bd13f8f2705d3433","after_hash":"8732e32cd14ebf272920bc24bb863fa708bc1c10c93aaad4eb5bff4f97e63e2a","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":"9522dc0ffd070dbfa81c2c7a0f0c7182fea22aa1f8da5adc1cf33bda5b35a3e4"} +{"hash_algorithm":"sha256","ts":"2026-11-13T08:17:30.389Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/125","value":{"created_at":"2026-11-13T08:17:30.382Z","author":"codex-github230-1111","text":"Round 18: rechecked clean accepted head 948ef40, claimed the existing item, read all three requested threads in full (three comments, two AI prompts), and re-read issue 230 through REST after the gh issue view Classic Projects failure. Main is already merged. Confirmed both races: add-issue validation must require a positive owner after async node resolution, and native comment writes must never reuse peers from a previous lock. Auditing every round-17 guard change with native malformed-data/concurrent-writer proofs; source coverage thresholds remain unchanged."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-13T08:17:30.389Z"}],"before_hash":"8732e32cd14ebf272920bc24bb863fa708bc1c10c93aaad4eb5bff4f97e63e2a","after_hash":"77f15ac98e2aafaf2138a1e279682d2ca5eade71ba53411ce6b7b6c59aa39ae1","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":"cbd15e2bd742a4a445dedd789b1832bea4f068cc7ea5085631746f658954fbb7"} +{"hash_algorithm":"sha256","ts":"2026-11-13T08:43:46.989Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/126","value":{"created_at":"2026-11-13T08:43:46.980Z","author":"codex-github230-1111","text":"Round 18 native race proofs now pass: clearing an add-issue owner during node resolution produces no attach/status/tag write; ordinary SDK peer claims between comments refuse the second annotation. Matching-owner, current-board-tag, draft, and multi-comment controls remain allowed. Restored closeout evidence arity, import duplicate tracking, refresh contradiction/safe-number checks, atomic ownership rethrow, and the reachable importer-author citation adoption counter. Removed the obsolete cross-lock cache updater with the stale cache itself; every independently locked write builds its peer index afresh. Complete native corpus certification rejects malformed missing-id peers before entering a writable batch. Full coverage and behavioral reverts remain pending."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-13T08:43:46.989Z"}],"before_hash":"77f15ac98e2aafaf2138a1e279682d2ca5eade71ba53411ce6b7b6c59aa39ae1","after_hash":"b0d6001d4d8f7e49ca5869f0ba1e1d8d4e8939da730d4d2e1bbd0f4888166a78","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":"d4da25498856f93a367fcbd0134dc5c89d1d03141996e2596c0c560b6a05106d"} +{"hash_algorithm":"sha256","ts":"2026-11-13T08:46:54.904Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/87","value":{"command":"timeout 1200 node --test --test-concurrency=2 test/round-eighteen.test.ts","scope":"project","provenance":{"author":"codex-github230-1111","created_at":"2026-11-13T08:46:54.648Z","source_kind":"local_mutation","source_ref":"fix/reference-tags-are-not-import-identity"}}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-13T08:46:54.904Z"}],"before_hash":"b0d6001d4d8f7e49ca5869f0ba1e1d8d4e8939da730d4d2e1bbd0f4888166a78","after_hash":"d35605743130f328499f37ede2ff2ea54858de736365e33bb82a3a202da21df5","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":"8f6fb2a12c098a5e1f9e8c98c93751effbc60e518d5a4da8f81fa76acfa24277"} +{"hash_algorithm":"sha256","ts":"2026-11-13T08:49:35.139Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/127","value":{"created_at":"2026-11-13T08:49:35.121Z","author":"codex-github230-1111","text":"Round 18 focused native file: 29/29 pass. Lint, current-SDK typecheck, docstring (33 files, 314 documented declarations) and mechanically regenerated ownership inventory pass. Static syntax comparison confirms applyPushEntry body identical at 58ae396 and 948ef40, so the board race predates round 17. The guard-by-guard audit is in docs/round-eighteen-guard-audit.md. Frozen-source all-module coverage is running; no receipt or threshold exceptions were added. The N=40 cost control now requires 81 corpus reads, one preflight plus each separately locked bulk/finalizer write; the stale-cache two-read expectation was removed."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-13T08:49:35.139Z"}],"before_hash":"d35605743130f328499f37ede2ff2ea54858de736365e33bb82a3a202da21df5","after_hash":"68cb344616cb55c5882fe78cd1f84e90b6960b6f17df0cfd5d93d743b420378e","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":"0d2b5af6b5238bdee9e098f63c354d27cbc48277c3457e426207dd1cd93bd5d2"} +{"hash_algorithm":"sha256","ts":"2026-11-13T09:00:42.280Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/128","value":{"created_at":"2026-11-13T09:00:42.252Z","author":"codex-github230-1111","text":"Coverage collection found two pre-existing low-level fixtures with uninitialized directories. Both now initialize native SDK trackers before their unchanged assertions; no ownership guard was relaxed. Production source hashes remain frozen. Strengthened the atomic staged-relink regression to assert zero transaction transitions, unchanged item/history bytes, the original owner-conflict code, and removal of the plan when no SDK journal exists. The CodeQL regression now uses parsed URL host equality and adversarial host/path twins containing the API hostname text."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-13T09:00:42.280Z"}],"before_hash":"68cb344616cb55c5882fe78cd1f84e90b6960b6f17df0cfd5d93d743b420378e","after_hash":"060dbf838215201f3970d2bf6a91271aef89df03aa4f1f92053e1977ccbb8360","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":"7c85dbe653957407f1b97665400d241316bb42bf1f30e018a881916cc02b2243"} +{"hash_algorithm":"sha256","ts":"2026-11-13T09:18:36.732Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/129","value":{"created_at":"2026-11-13T09:18:36.722Z","author":"codex-github230-1111","text":"Behavioral revert proof (disposable copies, credential-free, no coverage receipts): old applyPushEntry fails the URL-cleared race and the removed-board-tag case, with matching-owner/board-tag/draft controls allowed; old locked owner scope fails the ordinary-SDK peer-between-comments case and the ID-removal protection. Reverting closeout and refresh functions fails their native guard diagnostics; reverting verifyImportIdempotency fails the two-native-owner diagnostic; reverting runImport fails only the importer-author citation adoption count. Restoring substring API-host checking fails its adversarial-host control. Ten intended negative assertions fail across these mutants; all 31 round-18 tests and the parsed-host test pass restored. The old atomic early rethrow is an allowed earlier-refusal control; deleting the single retained unchecked rethrow fails the typed ownership assertion. No reviewed checkout files were mutated by the proofs."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-13T09:18:36.732Z"}],"before_hash":"060dbf838215201f3970d2bf6a91271aef89df03aa4f1f92053e1977ccbb8360","after_hash":"056f8b62ef6b219322655d5a4012361bfc2b161e588af24ed40aa48f46b174cb","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":"bb89aca9acdc621ddcbad5afc10838278c626d164d91f348f1c50779bf2e5829"} +{"hash_algorithm":"sha256","ts":"2026-11-13T09:27:20.668Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/130","value":{"created_at":"2026-11-13T09:27:20.632Z","author":"codex-github230-1111","text":"Read-only public snapshot previews match the required baseline exactly: pm-github 22/2/0, pm-review 2/2/0, pm-rust 6/1/0, pm-context 7/3/0, pm-linear 8/1/0 (create/update/unchanged). All planned curated fields remain unchanged, no extra creates, all original tracker hashes unchanged, scratch clones deleted, zero new issue API reads. pm-github retains the known existing-public-body privacy-preflight refusal; its counts are the production-planner diagnostic, not a passing gated command. Scratch extension installation used each available exact project SDK lock version; a package without an SDK lock entry used this package's pinned SDK, with no stale-runtime override. Native closeout plus corrected fixture rerun: 135/135; direct import gate checks: 3/3; seeded models: 4/4; explicit minimum-SDK consumer/typecheck: 1/1 at 2026.10.23. Coverage's only remaining branch was the allowed matching-owner board status path; its native control passes and final native aggregation is running."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-13T09:27:20.668Z"}],"before_hash":"056f8b62ef6b219322655d5a4012361bfc2b161e588af24ed40aa48f46b174cb","after_hash":"252c7d0df4ff2e97148a1b3d133a54ecacb6718d0b57e82e6618a0cfe2131c51","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":"07d6a88e93b425624d25a5b44150134125d33db36e73318d93fc745d9957d81e"} +{"hash_algorithm":"sha256","ts":"2026-11-13T09:35:00.591Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/131","value":{"created_at":"2026-11-13T09:35:00.528Z","author":"codex-github230-1111","text":"Final frozen-source native coverage passes the unchanged gate: 34 authored modules, exact 100/100/100/100 (lines/statements/functions/branches), no exclusions or ignore comments. The main capture executed 1914 tests; its two pre-existing uninitialized fixtures were corrected and the complete fixture file passed in the 135/135 closeout-plus-fixture rerun. Two existing SDK compensation quarantines remain skipped. The final linked pm test run passes 32/32 and reports tracking_disabled, so no run artifacts enter this tracker. Ten deliberate regression assertions fail on their behavioral mutants; all restored controls pass. Full import-gate validation is still running; no full-suite or CI completion claim."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-13T09:35:00.591Z"}],"before_hash":"252c7d0df4ff2e97148a1b3d133a54ecacb6718d0b57e82e6618a0cfe2131c51","after_hash":"e3ee741bf4b9daf0f3bfb4a9192cb6f4554fd23754ec896f7076d144198007a6","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":"689da406c3ee5b8054281a6c5801dadda62e14c0cd82eb2a753e343b92886c0d"} +{"hash_algorithm":"sha256","ts":"2026-11-13T09:55:19.313Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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/132","value":{"created_at":"2026-11-13T09:55:19.299Z","author":"codex-github230-1111","text":"Round 18 final validation complete. All 73 import-gate scenarios passed across capped batches: the initial command completed 64 with no failures before its 20-minute limit, and the nine unfinished cases passed in a 342075 ms name-pattern run. Final linked native regressions: 32/32; complete closeout/fixture rerun: 135/135; direct gate checks: 3/3; seeded model files: 4/4; minimum-SDK packed consumer and authored-source typecheck: 1/1 at 2026.10.23. Current typecheck, test compilation, lint, 314 documented declarations, and inventory anchors pass. Exact coverage is 15549/15549 lines and statements, 530/530 functions, 6725/6725 branches over all 34 authored modules, no skipped source or threshold changes. Ten intentional negative revert assertions fail and restored controls pass. All requested thread text was read (3 threads, 3 comments, 2 AI prompts); no review replies or resolutions were sent. Two existing SDK compensation quarantines and the known public-body privacy refusal remain explicit limits; CI certifies the full release matrix. Releasing the claim in progress, then regenerating changelog, committing paired tracker evidence with code, checking privacy and the runner hold, and pushing."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-13T09:55:19.313Z"}],"before_hash":"e3ee741bf4b9daf0f3bfb4a9192cb6f4554fd23754ec896f7076d144198007a6","after_hash":"76a2e5b4f79e1f3a925fa43df89b5f4ac6fa9d34c41c8d29b87f429ca22124fc","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":"9c50cc45a10b7c6070733e28887547e143403c6bd8cc4dc2fb2db33c63c5ea85"} +{"hash_algorithm":"sha256","ts":"2026-11-13T09:55:21.179Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-13T09:55:21.179Z"}],"before_hash":"76a2e5b4f79e1f3a925fa43df89b5f4ac6fa9d34c41c8d29b87f429ca22124fc","after_hash":"343716378d03e96b16747309682f8f33a55502889284536ccef21ec9795bec28","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":"89e7c1be404f8f79d5fd059c6384d27ab9dcdc46f5cf013f5e4c516daa20c307"} +{"hash_algorithm":"sha256","ts":"2026-11-13T10:41:58.806Z","author":"orchestrator","author_source":"asserted","agent_harness":"claude-code","agent_model":"claude-opus-5-5","agent_model_source":"probe","agent_instance":"31217a101ccd67a43c135f1c","agent_provenance":{"model":{"value":"claude-opus-5-5","source":"probe"},"effort":{"value":"xhigh","source":"environment"},"role":null,"topic":null,"version":{"value":"2.1.322","source":"probe"}},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/133","value":{"created_at":"2026-11-13T10:41:58.789Z","author":"orchestrator","text":"Orchestrator, CodeRabbit PRRT_kwDOTbN4pM63DOtP on 6658b2f. The pull-plan board-consistency guard (projects.ts:659) used only ownership.states.get(pm), so a separately allocated copy of a selected item skipped the owner check, and a board issue o/r#8 could pull its status onto an item that owns o/r#7. It now derives claims with the same githubOwnershipClaims fallback as line 617. The regression in test/projects.test.ts refuses the copy-with-mismatch case (0/1 with the fix reverted) and still pulls when the copy is on its own issue. Inventory anchors were regenerated with scripts/identity-inventory.ts --write. Gates: projects, round-eighteen and import-projection pass 78/78; identity-inventory 5/5; lint and typecheck pass."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-13T10:41:58.806Z"}],"before_hash":"343716378d03e96b16747309682f8f33a55502889284536ccef21ec9795bec28","after_hash":"e34bd349252626ec25ea4d5700ef2a9d3d9db021239457f01ea822de1a7e0ba1","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"claude_session_file","rule_version":"v1"},"version":{"status":"resolved","resolver":"claude_session_file","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"4e0c99f5b7c12eb04b4784b66864144cfef5479e710592b5738cdfbf7b115d67"} +{"hash_algorithm":"sha256","ts":"2026-11-13T11:49:25.058Z","author":"orchestrator","author_source":"asserted","agent_harness":"claude-code","agent_model":"claude-opus-5-5","agent_model_source":"probe","agent_instance":"31217a101ccd67a43c135f1c","agent_provenance":{"model":{"value":"claude-opus-5-5","source":"probe"},"effort":{"value":"xhigh","source":"environment"},"role":null,"topic":null,"version":{"value":"2.1.322","source":"probe"}},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/134","value":{"created_at":"2026-11-13T11:49:25.047Z","author":"orchestrator","text":"Orchestrator, CodeRabbit PRRT_kwDOTbN4pM63ENyF on 9635d75: the push planner had the same identity-keyed lookup as the pull planner. Fixed as a class. The new ownershipStateOf(index, item) in import-ownership.ts reads states by identity and derives claims for a separately allocated copy. Every planner and selector that read ownership.states directly now uses it: projects push, pull and import, refresh selection, import-fidelity targets, index sync/import selection and the scope warning. The two sites with their own fallback parameters stay (assertGithubImportOwner with initialLink, closeout with the delivery repo). New push-planner regression for the copied item (refuses a mismatched board issue, plans set-status on its own issue); the inventory classifies the helper. Gates: 764 tests across the affected files, with only the five inventory tests failing before the classification and 5/5 after; projects 46/46 with the inventory; lint and typecheck pass."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-13T11:49:25.058Z"}],"before_hash":"e34bd349252626ec25ea4d5700ef2a9d3d9db021239457f01ea822de1a7e0ba1","after_hash":"26da8ec498559aa659238496a38b22f7e7ff0edf7969f96d6f533af11ed93787","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"claude_session_file","rule_version":"v1"},"version":{"status":"resolved","resolver":"claude_session_file","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"45130cc75e06f38caa10c33cc06e99b0c389cdecd01aa7b14624e74d9ae0cc97"} +{"hash_algorithm":"sha256","ts":"2026-11-13T14:17:53.658Z","author":"orchestrator","author_source":"asserted","agent_harness":"claude-code","agent_model":"claude-opus-5-5","agent_model_source":"probe","agent_instance":"31217a101ccd67a43c135f1c","agent_provenance":{"model":{"value":"claude-opus-5-5","source":"probe"},"effort":{"value":"xhigh","source":"environment"},"role":null,"topic":null,"version":{"value":"2.1.322","source":"probe"}},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/135","value":{"created_at":"2026-11-13T14:17:53.647Z","author":"orchestrator","text":"Orchestrator, round on 72d2131. (1) CodeRabbit Major PRRT_kwDOTbN4pM63IEZp: hasStructuredOwnership only needs to know whether owners exist, so it now uses the non-throwing githubOwnershipClaims(item).owners. An eligible citation candidate whose importer id (#41) and github_url (#42) disagree no longer aborts matching. The new cited-issue-adoption regression fails with 'Got unwanted exception' when the change is reverted. (2) Greptile P2 PRRT_kwDOTbN4pM63HwYp: the inventory scanner's helper set includes ownershipStateOf, and the regenerated inventory lists its call sites. (3) CodeRabbit Minor PRRT_kwDOTbN4pM63IEZc: docs/identity-collisions.md lists the marker-free legacy description declaration as a claim source. Gates: cited-issue-adoption, identity-inventory, import-fidelity, owner-field-values and projects pass; lint and typecheck pass."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-11-13T14:17:53.658Z"}],"before_hash":"26da8ec498559aa659238496a38b22f7e7ff0edf7969f96d6f533af11ed93787","after_hash":"8b30fa38be34152f1d5e0f038523918399af68c980583a2a8fbda1735deb23c3","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"claude_session_file","rule_version":"v1"},"version":{"status":"resolved","resolver":"claude_session_file","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"14ee5d1027d0a2c8f3814e0c0ce3ca9fe3b73fa95dd75f2260eb1a16e9634e69"} diff --git a/.agents/pm/history/pm-github-search-reference-followup.jsonl b/.agents/pm/history/pm-github-search-reference-followup.jsonl new file mode 100644 index 00000000..e581ca16 --- /dev/null +++ b/.agents/pm/history/pm-github-search-reference-followup.jsonl @@ -0,0 +1,4 @@ +{"hash_algorithm":"sha256","ts":"2026-11-11T22:21:15.600Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-search-reference-followup"},{"op":"add","path":"/metadata/title","value":"Map GitHub search hits through ownership instead of shared references"},{"op":"add","path":"/metadata/description","value":"Latest CodeRabbit outside-diff review of PR 233, indexByProvenance: import o/r#7, then add a curated Chore carrying gh:o/r#7. Tag-based indexing lets the later reference replace the imported owner in GitHub search results. Use authoritative import identities and retain shared mentions without replacing the owner. Deferred from round-6 importer write protection."},{"op":"add","path":"/metadata/type","value":"Issue"},{"op":"add","path":"/metadata/status","value":"open"},{"op":"add","path":"/metadata/priority","value":3},{"op":"add","path":"/metadata/tags","value":["gh:unbraind/pm-github#233"]},{"op":"add","path":"/metadata/created_at","value":"2026-11-11T22:21:15.600Z"},{"op":"add","path":"/metadata/updated_at","value":"2026-11-11T22:21:15.600Z"},{"op":"add","path":"/metadata/author","value":"codex-github230-1111"}],"before_hash":"3cc22dff72be7b14824654a7a64ea62b04799939b2fee54c1b5f52ca60bf6df0","after_hash":"59db7d4f6c5656c828fce3f95a55eabc22aab56823a715b8cef3f8fbf6c99503","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":"33090fdd737f1969041b6369a0d6a0ae751932c36374d7539b37ec50d8ad5d1f"} +{"hash_algorithm":"sha256","ts":"2026-11-12T08:53:56.322Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-12T08:53:56.322Z"},{"op":"remove","path":"/metadata/tags/0"}],"before_hash":"59db7d4f6c5656c828fce3f95a55eabc22aab56823a715b8cef3f8fbf6c99503","after_hash":"b77595a7504e007d8731cc1f7114a0f31b8663da7fb516fc14c533691ac6bcd7","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":"04f06ab02d3194bd8e0a4b84df2b2cb1f26658e87aca66067b84f9d9f299fb80"} +{"hash_algorithm":"sha256","ts":"2026-11-12T08:53:57.076Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-12T08:53:57.076Z"},{"op":"add","path":"/metadata/docs","value":[{"path":"https://github.com/unbraind/pm-github/pull/233","scope":"project","note":"Context reference; this follow-up is not the referenced issue or PR"}]}],"before_hash":"b77595a7504e007d8731cc1f7114a0f31b8663da7fb516fc14c533691ac6bcd7","after_hash":"ab1927aa43ff7516ff2b4032a6a52f16f7da98cccbfcd2bc0a941dfff00a6f5d","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":"4e2f7e8756b35b5557a318ec65adc0b1a65a0fa0360d1296ad6ce93b5c3fed9e"} +{"hash_algorithm":"sha256","ts":"2026-11-12T08:53:57.802Z","author":"codex-github230-1111","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fb26b8f462e36ea161357e8c","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-11-12T08:53:57.802Z"},{"op":"add","path":"/metadata/comments","value":[{"created_at":"2026-11-12T08:53:57.802Z","author":"codex-github230-1111","text":"Round 9 tracker audit: moved the parent issue or PR reference from a gh tag to a docs link. This is reference hygiene only; deferred implementation remains out of scope and this item stays open."}]}],"before_hash":"ab1927aa43ff7516ff2b4032a6a52f16f7da98cccbfcd2bc0a941dfff00a6f5d","after_hash":"b9a7d09aa171ea46f4d2f885e348cad78223e4e5b0dcf8bd99fef8b081deafeb","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":"67f0814c35cb6bae98faa20112bc0267f69c06353426aa3025c8ce8834fadd59"} diff --git a/.agents/pm/issues/pm-github-blocker-format-followup.toon b/.agents/pm/issues/pm-github-blocker-format-followup.toon new file mode 100644 index 00000000..40481679 --- /dev/null +++ b/.agents/pm/issues/pm-github-blocker-format-followup.toon @@ -0,0 +1,28 @@ +id: pm-github-blocker-format-followup +title: Retain formatted GitHub blocker URLs in prose tokenization +description: "Review follow-up from PR #233 thread PRRT_kwDOTbN4pM62SJvl: githubUrlTokens retains trailing Markdown emphasis and punctuation such as ! and :, so formatted issue URLs can disappear from blocker scans. Separate from round-4 durable-state publication; normalize prose wrappers without weakening complete structured identity validation." +type: Issue +status: in_progress +priority: 3 +tags[1]: review-followup +created_at: "2026-11-11T18:20:20.837Z" +updated_at: "2026-11-12T08:53:48.352Z" +author: codex-github230-1111 +comments[4]{created_at,author,text}: + "2026-11-11T20:20:09.731Z",codex-github230-1111,"Round 5 implements a shared linear prose URL extractor with balanced delimiters and matching emphasis, while complete structured URL identity checks stay strict. Unknown URL candidates remain visible as unparsed_url/unparsed references without guessed numbers; they prevent clearance and cleared annotations. Focused real SDK tracker cases cover ten wrapper forms, six punctuation marks, the exact false-clear reproduction and eight unknown URL cases. Node and native Bun focused files pass 30/30 including alias cases. Full dependent-file gates and behavioral reverts remain in progress." + "2026-11-11T21:06:55.904Z",codex-github230-1111,"Round 5 completed: Markdown-aware URL extraction handles every requested wrapper and punctuation while retaining balanced URL data. Unsupported HTTP candidates remain unparsed_url/unparsed without invented numbers; masking prevents fragment fallback. Only actual ownership excludes self, so reference tags cannot hide open blockers. The linked node --test --test-concurrency=1 --test-name-pattern \"^(Markdown|Unparsed|Reference)\" test/github-url.test.ts run passes 26/26; full blockers file passes 35/35. Behavioral reverts fail all 17 Markdown cases, all 8 unparsed cases and the reference-tag self-exclusion case; restored full Node/Bun file passes 31/31. Evidence maps to PRRT_kwDOTbN4pM62SJvl. Releasing this author claim and leaving the item in progress." + "2026-11-11T22:21:14.447Z",codex-github230-1111,"Additional outside-diff review reproduction from the latest CodeRabbit body, retained outside round-6 ownership-write scope: an unowned blocked item tagged gh:a/r#7 says Blocked by #2, while the command supplies --repo b/r. Tag-based repository hinting queries a/r#2; if closed while b/r#2 is open it can falsely clear and annotate. Resolve repository hints from authoritative ownership or the explicit command hint; a reference tag must not choose the repository. No hint behavior changed in round 6." + "2026-11-12T08:53:48.352Z",codex-github230-1111,"Round 9 tracker audit: moved the parent issue or PR reference from a gh tag to a docs link. This is reference hygiene only; deferred implementation remains out of scope and this item stays open." +files[3]{path,scope}: + github-url.ts,project + index.ts,project + test/github-url.test.ts,project +tests[1]{command,scope,timeout_seconds,provenance{author,created_at,source_kind,source_ref}}: + "node --test --test-concurrency=1 --test-name-pattern \"^(Markdown|Unparsed|Reference)\" test/github-url.test.ts",project,1200,codex-github230-1111,"2026-11-11T20:30:22.210Z",local_mutation,fix/reference-tags-are-not-import-identity +docs[2]: + - path: "https://github.com/unbraind/pm-github/pull/233" + scope: project + note: Context reference; this follow-up is not the referenced issue or PR + - path: "https://github.com/unbraind/pm-github/pull/233#discussion_r4541099510" + scope: project +body: "" diff --git a/.agents/pm/issues/pm-github-curated-reopen-followup.toon b/.agents/pm/issues/pm-github-curated-reopen-followup.toon new file mode 100644 index 00000000..9dcee104 --- /dev/null +++ b/.agents/pm/issues/pm-github-curated-reopen-followup.toon @@ -0,0 +1,22 @@ +id: pm-github-curated-reopen-followup +title: Distinguish source-assigned completion dates when curated items reopen +description: "Review follow-up from PR #233 thread PRRT_kwDOTbN4pM62Q98t: preserve completion dates that predate linking, but evaluate clearing dates assigned by a prior GitHub closure when the source reopens. Separate from round-4 durable-state publication; requires explicit provenance for the completion date and real SDK reopen tests." +type: Issue +status: open +priority: 3 +tags[1]: review-followup +created_at: "2026-11-11T18:20:18.830Z" +updated_at: "2026-11-12T08:53:50.742Z" +author: codex-github230-1111 +comments[2]{created_at,author,text}: + "2026-11-11T22:07:08.272Z",codex-github230-1111,"Round 6 review reproduction retained for later work (PRRT_kwDOTbN4pM62Q98t): start with a curated linked item with no completed_at, import a GitHub closure so completed_at is assigned from github_closed_at, then reopen the source and re-sync. The item becomes open and closed_at is cleared, but the source-assigned completed_at remains. A pre-link local completion date must still be preserved. Do not clear every completed_at; track source assignment distinctly before implementing reopen cleanup. This round intentionally changes no completion-date behavior." + "2026-11-12T08:53:50.741Z",codex-github230-1111,"Round 9 tracker audit: moved the parent issue or PR reference from a gh tag to a docs link. This is reference hygiene only; deferred implementation remains out of scope and this item stays open." +files[1]{path,scope}: + import-fidelity.ts,project +docs[2]: + - path: "https://github.com/unbraind/pm-github/pull/233" + scope: project + note: Context reference; this follow-up is not the referenced issue or PR + - path: "https://github.com/unbraind/pm-github/pull/233#discussion_r4540585133" + scope: project +body: "" diff --git a/.agents/pm/issues/pm-github-pr-url-alias-followup.toon b/.agents/pm/issues/pm-github-pr-url-alias-followup.toon new file mode 100644 index 00000000..d88a6395 --- /dev/null +++ b/.agents/pm/issues/pm-github-pr-url-alias-followup.toon @@ -0,0 +1,27 @@ +id: pm-github-pr-url-alias-followup +title: Exclude PR owners in every supported explicit URL field during refresh +description: "Review follow-up from PR #233 thread PRRT_kwDOTbN4pM62Q99D: identity resolution recognizes GitHubURL aliases, while refresh PR exclusion checks github_url only. Evaluate all supported explicit URL fields before the issue-only GraphQL query. Separate from round-4 durable-state publication; retain real tracker tests for aliases and issue owners." +type: Issue +status: in_progress +priority: 3 +tags[1]: review-followup +created_at: "2026-11-11T18:20:19.928Z" +updated_at: "2026-11-12T08:53:53.066Z" +author: codex-github230-1111 +comments[3]{created_at,author,text}: + "2026-11-11T20:20:11.029Z",codex-github230-1111,"Round 5 centralizes recognized explicit URL aliases in explicitGithubIdentities and makes ownership matching and refresh consume the same parsed kind. Five real SDK alias scenarios verify PRs are excluded before querying, canonical PR controls stay excluded, issue aliases still refresh, and curated titles stay unchanged. The selector/model also checks all 1024 case/optional-underscore spellings. Focused Node/Bun files pass 30/30; full gates and reverts remain in progress." + "2026-11-11T21:06:56.712Z",codex-github230-1111,"Round 5 completed: explicitGithubIdentities shares every recognized URL alias and parsed kind between import ownership and refresh PR selection. Five real SDK tracker scenarios cover retained aliases, canonical PR controls, issue aliases and unchanged curated titles; the selector also checks all 1024 accepted case/optional-underscore spellings. Linked node --test --test-concurrency=1 --test-name-pattern \"^Refresh excludes PRs\" test/github-url.test.ts passes 5/5. Reverting only the selector makes all 5 fail; restored Node/Bun file passes 31/31. Evidence maps to PRRT_kwDOTbN4pM62Q99D. Releasing this author claim and leaving the item in progress." + "2026-11-12T08:53:53.065Z",codex-github230-1111,"Round 9 tracker audit: moved the parent issue or PR reference from a gh tag to a docs link. This is reference hygiene only; deferred implementation remains out of scope and this item stays open." +files[3]{path,scope}: + import-fidelity.ts,project + refresh.ts,project + test/github-url.test.ts,project +tests[1]{command,scope,timeout_seconds,provenance{author,created_at,source_kind,source_ref}}: + "node --test --test-concurrency=1 --test-name-pattern \"^Refresh excludes PRs\" test/github-url.test.ts",project,1200,codex-github230-1111,"2026-11-11T20:20:08.486Z",local_mutation,fix/reference-tags-are-not-import-identity +docs[2]: + - path: "https://github.com/unbraind/pm-github/pull/233" + scope: project + note: Context reference; this follow-up is not the referenced issue or PR + - path: "https://github.com/unbraind/pm-github/pull/233#discussion_r4540585165" + scope: project +body: "" diff --git a/.agents/pm/issues/pm-github-project-reference-followup.toon b/.agents/pm/issues/pm-github-project-reference-followup.toon new file mode 100644 index 00000000..d781f9b6 --- /dev/null +++ b/.agents/pm/issues/pm-github-project-reference-followup.toon @@ -0,0 +1,18 @@ +id: pm-github-project-reference-followup +title: Audit Projects v2 reference fallback and curated item ownership +description: "Static inspection found a separate board-import path: projects.ts indexPmByIssue indexes free-form gh tags, buildProjectImportPlan uses that fallback, and index.ts runProjectImport writes title, description and body on an update. Reproduce with real SDK trackers, then define explicit board ownership and migration-free compatibility for existing gh-project markers. This is separate from the gated REST issue importer fixed by issue 230." +type: Issue +status: open +priority: 2 +tags[3]: follow-up,projects,reference-identity +created_at: "2026-11-11T11:09:25.465Z" +updated_at: "2026-11-12T08:53:55.558Z" +author: codex-github230-1111 +comments[1]{created_at,author,text}: + "2026-11-12T08:53:55.558Z",codex-github230-1111,"Round 9 tracker audit: moved the parent issue or PR reference from a gh tag to a docs link. This is reference hygiene only; deferred implementation remains out of scope and this item stays open." +files[2]{path,scope}: + index.ts,project + projects.ts,project +docs[1]{path,scope,note}: + "https://github.com/unbraind/pm-github/issues/230",project,Context reference; this follow-up is not the referenced issue or PR +body: "" diff --git a/.agents/pm/issues/pm-github-reference-identity-followup.toon b/.agents/pm/issues/pm-github-reference-identity-followup.toon new file mode 100644 index 00000000..d2e23df5 --- /dev/null +++ b/.agents/pm/issues/pm-github-reference-identity-followup.toon @@ -0,0 +1,35 @@ +id: pm-github-reference-identity-followup +title: Validate remaining reference-identity review edge cases after PR 233 +description: "Single follow-up for additional review observations after the final fix round. Revalidate remaining identity, recovery, lease and reporting edge cases against the final PR head; do not reopen completed rounds or duplicate covered regressions." +type: Issue +status: open +priority: 2 +tags[2]: "gh:unbraind/pm-github#236",review-followup +created_at: "2026-11-12T02:00:10.688Z" +updated_at: "2026-11-12T23:05:14.712Z" +author: codex-github230-1111 +comments[6]{created_at,author,text}: + "2026-11-12T02:01:13.150Z",codex-github230-1111,"Round 7 is the final fix round for #233. This single follow-up consolidates additional review observations, as requested by the orchestrator; it does not assert that every observation reproduces after the round-7 changes. Validate each against the final head, deduplicate covered cases, and retain real SDK must-refuse/must-allow tests plus behavioral revert evidence for any fix.\n\n- [PRRT_kwDOTbN4pM62auBR](https://github.com/unbraind/pm-github/pull/233#discussion_r4544876674): Report import conflicts on the workflow path that produces no tracker diff.\n- [PRRT_kwDOTbN4pM62auBZ](https://github.com/unbraind/pm-github/pull/233#discussion_r4544876686): Validate ambiguous legacy number-only ownership before closeout.\n- [PRRT_kwDOTbN4pM62auBc](https://github.com/unbraind/pm-github/pull/233#discussion_r4544876691): Recover exclusive leases safely when publication succeeds but directory flush fails.\n- [PRRT_kwDOTbN4pM62auBe](https://github.com/unbraind/pm-github/pull/233#discussion_r4544876695): Require an actual matching reference tag before planning tag detachment.\n- [PRRT_kwDOTbN4pM62auBi](https://github.com/unbraind/pm-github/pull/233#discussion_r4544876701): Reconcile contradictory external_ref claims at the import write boundary.\n- [PRRT_kwDOTbN4pM62auBo](https://github.com/unbraind/pm-github/pull/233#discussion_r4544876710): Check partial legacy link markers against an existing repository field.\n- [PRRT_kwDOTbN4pM62auBu](https://github.com/unbraind/pm-github/pull/233#discussion_r4544876718): Avoid retaining an ownership-refused plan when no SDK journal exists.\n- [PRRT_kwDOTbN4pM62auB1](https://github.com/unbraind/pm-github/pull/233#discussion_r4544876728): Scope retained recovery plans to selected refresh IDs without dropping fresh updates.\n- [PRRT_kwDOTbN4pM62auB7](https://github.com/unbraind/pm-github/pull/233#discussion_r4544876736): Handle contradictory identities in blocker self-exclusion; dependency indexing is addressed in round 7.\n- [PRRT_kwDOTbN4pM62auB_](https://github.com/unbraind/pm-github/pull/233#discussion_r4544876746): Use canonical lowercase reference tags in the citation-adoption fixture.\n\nExplicit identity-conflict isolation in closeout and real-tracker dry-run fixtures are already addressed by round 7. Existing curated-reopen, Projects and search follow-ups keep their separate scope. Do not delete append-only tracker evidence or reply to/resolve review threads while implementing this follow-up.\n\nPM: https://github.com/unbraind/pm-github/blob/main/.agents/pm/issues/pm-github-reference-identity-followup.toon" + "2026-11-12T08:46:16.856Z",codex-github230-1111,"Round 9 corrects this record only: removed the auxiliary gh reference to PR 233 so its identity agrees with issue 236. The PR is retained as a docs link. Existing history and the deferred implementation scope are unchanged." + "2026-11-12T10:16:19.276Z",codex-github230-1111,"The priority dependency-index ambiguity from issue 236 is addressed in PR 233 round 10: a second claimant through external_ref is included by shared githubOwnershipClaims, reports ambiguous_github_dependency_owner, and writes no edge. Regression coverage uses real SDK trackers and also checks endpoint ownership under the native mutation lock. Other deferred observations retain their existing scope." + "2026-11-12T17:35:01.802Z",codex-github230-1111,"Round 13 optional duplication check found a pre-existing five-line exact clone in projects.ts:630-634 and 699-701, in Project ownership-index setup. projects.ts has identical blob 71df0c3606f90feaeb53e1db6d415c57285d3530 at orchestrator base 5f6749f and in this round; no Project production logic was changed. npm run duplication reports one 53-token clone at the unchanged zero threshold. Preserve the selected-item-only scoped pull behavior when consolidating this setup. Deferred here rather than expanding the three requested fixes; required round-13 tests and checks are green." + "2026-11-12T21:39:21.517Z",codex-github230-1111,"Verification note for the earlier projects.ts duplication observation: the orchestrator consolidated selected-only Project indexing through indexSelectedByIssue before this round (base 4de7d4c). The merged candidate passes the unchanged zero-threshold npm run duplication gate: 34 authored modules, 15484 lines, zero duplicated lines. projects.ts is unchanged in this fix round; the selected-only scoped behavior is retained. Keep this follow-up open for its remaining observations." + "2026-11-12T23:05:14.712Z",codex-github230-1111,"Read-only correction-round evidence for deferred verification: pm-github state-all atomic gated preview refuses existing public issue-body privacy findings (issues 160 and 202); the released baseline separately refuses duplicate reference tags. No data was applied. This is outside the restored legacy-declaration fix and is not a successful end-to-end gate result. The no-additional-create compatibility comparison passes against accepted pre-correction 4de7d4c across pm-github, pm-review, pm-rust, pm-context and pm-linear. Released origin/main still uses the reference-tag identity rule, so its lower create counts cannot be an ownership-compatible baseline for issue 230. Keep this follow-up open; no review thread was answered or resolved." +files[8]{path,scope}: + .github/workflows/pm-github-sync.yml,project + closeout.ts,project + durable-state.ts,project + identity.ts,project + import-fidelity.ts,project + import-ownership.ts,project + index.ts,project + test/cited-issue-adoption.test.ts,project +docs[2]: + - path: docs/identity-collisions.md + scope: project + - path: "https://github.com/unbraind/pm-github/pull/233" + scope: project + note: Review source; issue 236 remains the sole GitHub identity +github_number: 236 +github_url: "https://github.com/unbraind/pm-github/issues/236" +body: "" diff --git a/.agents/pm/issues/pm-github-reference-identity.toon b/.agents/pm/issues/pm-github-reference-identity.toon new file mode 100644 index 00000000..492c1c24 --- /dev/null +++ b/.agents/pm/issues/pm-github-reference-identity.toon @@ -0,0 +1,1016 @@ +id: pm-github-reference-identity +title: "Keep GitHub reference tags separate from import identity (BREAKING: PM CLI 2026.10.23 minimum)" +description: "Preserve curated local fields, allow shared reference tags, and retain migration-free matching of existing importer provenance." +type: Issue +pm_format_version: 2 +status: in_progress +priority: 2 +tags[2]: changelog-unreleased,"gh:unbraind/pm-github#230" +created_at: "2026-11-11T10:19:25.073Z" +updated_at: "2026-11-13T14:17:53.658Z" +author: codex-github230-1111 +comments[136]{created_at,author,text}: + "2026-11-11T10:19:28.756Z",codex-github230-1111,"Read issue 230 and all comments. Branch starts from main; PR229 changes workflows and docs separately. Plan: distinguish explicit importer identity from references, preserve curated linked content, retain legacy persisted provenance, and prove behavior with real SDK trackers and behavioral reverts. Task-specific gate budget applies: focused importer/sync/dependent tests, lint and typecheck; full suite only within 45 minutes." + "2026-11-11T10:24:30.654Z",codex-github230-1111,"Implemented separate durable import identity lookup; shared gh references no longer select owners or collide. Curated links merge provenance/lifecycle while deterministic importer records mirror content. New real SDK regressions pass 9/9, including no-git curated links and byte-identical legacy resync beside shared references. Reviewing existing tests that assumed tag-only ownership; updating mirror fixtures to explicit importer identities." + "2026-11-11T10:33:45.352Z",codex-github230-1111,"Behavioral revert proof in a disposable package copy: rebuilt after restoring released matchImportedIssue, prepareGithubImport and verifyImportIdempotency plus tag-only identity lookup. node --test --test-concurrency=2 test/reference-import-identity.test.ts: 14 tests, 13 fail, 1 pass. All four required scenarios fail: single curated Chore, shared references, mirror title refresh beside a reference, and byte-identical legacy resync beside a reference. The passing legacy link-marker case is existing supported behavior. Package worktree was never reverted. Fixed regressions are being rerun through pm test; full npm test follows within the task budget." + "2026-11-11T10:39:30.901Z",codex-github230-1111,"Linked pm test run passed 16/16 (real SDK trackers, both import modes, no-git linking, legacy compatibility, safe PR conflict rendering and dependency routing). Lint and typecheck pass; docstring gate documents 29 files/280 declarations; duplication gate has zero clones at the unchanged 50-token threshold across 30 authored modules. Added seeded preview/apply equivalence coverage. Full npm test is running with two workers and a 45-minute cap; npm/bun release:check and coverage are delegated to CI under the explicit task gate budget." + "2026-11-11T10:55:16.026Z",codex-github230-1111,"Optional full npm test was stopped under the task gate budget: an unrelated closeout workflow file had already used over 11 minutes before its large decision matrix. No failures were reported in the observed output before cancellation; this is incomplete evidence, not a passing full suite. Running all 46 required importer/sync/direct-dependent and sync-workflow test files separately now, with two file workers and a hard 1200-second limit per file. CI owns full-suite and release certification." + "2026-11-11T11:09:29.505Z",codex-github230-1111,The audit also found a separate Projects v2 board-import reference fallback. Tracked static evidence and required real-SDK reproduction in pm-github-project-reference-followup; board ownership/mirroring is a separate model from the gated REST issue importer and was not changed here. + "2026-11-11T11:38:48.163Z",codex-github230-1111,"Final fixture reruns passed: import-fidelity 53 pass/1 existing SDK quarantine; handler failures 38/38; import projection 6/6; label-model properties 3/3 (including both 500-case SDK corpora); daily-sync properties 3/3. The new regression files pass 18/18, including mixed-case legacy URLs. Corrected an embedded child-script fixture import and verified coverage-remainder 25/25; repeating its final file with explicit pinned-CLI PATH. Standalone new regressions now pin their child CLI too." + "2026-11-11T11:42:48.057Z",codex-github230-1111,"Final local evidence: 46 selected files attempted with two workers and a 1200-second limit per file. Completed 45 files: 869 tests, 867 pass, zero fail, two unchanged SDK quarantines. import-gate.test.ts reached its 20-minute cap after 63 passes and one canceled test; it is incomplete CI evidence. Every other file passed after fixture corrections. Exact file command was node --test --test-concurrency=1 followed by that file, with the repository CLI first on PATH and global Git configuration disabled. The linked regression command was rerun through pm test --run --progress after the disposable behavioral revert: 18/18 pass. Latest revert: 17 tests, 16 fail, one legacy link-marker pass; all four required scenarios fail. Lint, typecheck, test compilation, 280 documented declarations and zero duplication at the unchanged threshold pass. Full npm test, coverage and npm/Bun release certification remain for CI. No consumer holds or trackers were changed." + "2026-11-11T11:47:33.244Z",codex-github230-1111,"PR: https://github.com/unbraind/pm-github/pull/233 . Code commit e9e0c9a. The claim is released and this item stays in_progress for orchestrator verification. Final privacy scan passed across 9334 Git objects with approved identities and no secret or host-path findings; tracker diff gate passed. Strict health passed with isolated global extension discovery; ambient discovery had an SDK version skew, while integrity/history remained valid. Full-suite, timed-out import-gate completion, coverage and npm/Bun release checks remain for CI. Projects v2 board fallback remains separately tracked in pm-github-project-reference-followup." + "2026-11-11T14:06:28.712Z",codex-github230-1111,"Round 2 resumes from ed050d3 with a clean worktree. Read all four unresolved threads and the complete latest CodeRabbit review body, including its outside-diff upstream-target finding and scope-check note. Plan: synchronize all non-content side data for curated links; reject contradictory explicit identities before writes with normalized compatibility; persist/replay atomic import plans through retries; and guard outbound status/export targets. Keep review threads open for the orchestrator. Existing Projects follow-up audit records remain append-only as required by the task rules." + "2026-11-11T14:23:38.805Z",codex-github230-1111,"Round 2 side-data inventory: after a content merge, the mirror path applies native lifecycle transitions, synchronizes GitHub comments as annotations, persists github_* source dates/state/closure provenance, reports counts, links dependencies, and runs provenance/privacy gates. Curated updates now perform the same non-content steps. Body-only fetched comments route to annotations for curated items. SDK transaction contract uses payload-bound ordered steps: recovery replays the recorded original entries, mutations, targets, comments and dependency inputs before polling new GitHub data; a later normal sync observes changed input. Identity validation shares one normalized explicit-identity path across import, refresh, sync and export; contradictory values refuse before writes. Outbound sync/export now ignore shared references by default and offer explicit historical tag-only opt-in with competing-owner refusal." + "2026-11-11T14:34:18.885Z",codex-github230-1111,"Review regression baseline: 21 real-SDK cases passed across curated side data, explicit-identity refusal and spelling compatibility, six original-plan crash boundaries, upstream plan conflicts and actual sync/export HTTP writes. Added a further dependency replay case for an unchanged item in a mixed batch: record the complete original dependency input, interrupt, change the remote body, then recover the original edge. Replacing the prior fake incomplete-compensation test with a real SDK transaction also checks that compensation recovery reuses the saved plan. Broad gates cover 49 affected/import/refresh/identity/sync files, two workers, each invocation capped at 20 minutes; import-gate is partitioned by exhaustive disjoint test-name prefixes." + "2026-11-11T15:09:38.145Z",codex-github230-1111,"Final review regression run through pm test --run --progress passed 29/29 cases, including generated preview/apply agreement. Import-gate completed 73/73 cases across four alphabetic name partitions; all 56 declaration prefixes were checked for coverage by those partitions. A redundant non-letter pattern matched the Node file wrapper, was stopped, removed from linked tests and excluded from counts. The closeout workflow matrix reached its 20-minute cap after 143 passing cases with no assertion failures. Real compensation testing follows the public SDK contract: delete compensation retains history and reserves the original ID. Recovery now keeps the plan and returns github_import_plan_drift instead of applying changed input; explicit SDK restore of the original birth version then permits the exact saved plan to finish. The atomic file passed 11/11 including this refusal-and-restore case. Read-only identity exploration still exposes contradictory stored claims, while write planners refuse them." + "2026-11-11T15:10:46.699Z",codex-github230-1111,"Behavioral revert proof, executed only in a disposable package copy with real temp SDK trackers: restoring round-one prepareGithubImport/runImport causes 4 of 6 curated side-data cases to fail (2 atomic annotation compatibility controls pass); restoring importIdentityKeys/matchImportedIssue causes all 7 explicit identity refusal/normalization cases to fail; restoring runImport and disabling persisted-plan dispatch in importGithubAtomic causes all 12 crash/dependency/missing-plan cases to fail; restoring planSync/buildExportPlan causes all 3 upstream planning, wire-write and generated preview/apply cases to fail. The same atomic replay revert makes the real incomplete-compensation/explicit-restore test fail (1/1). Every mutation was restored in a finally block; the fixed package copy is running the full review regression file again. Review thread mapping: PRRT_kwDOTbN4pM62J6R5 and PRRT_kwDOTbN4pM62KLhP use curated side-data tests; PRRT_kwDOTbN4pM62KLhK uses explicit identity cases; PRRT_kwDOTbN4pM62KLhO uses original-plan replay plus compensation. Latest CodeRabbit outside-diff upstream comment uses the three upstream cases. No review thread was replied to, resolved or reacted to." + "2026-11-11T15:17:36.076Z",codex-github230-1111,"Final round-2 evidence: fixed disposable source copy matches the package code byte-for-byte and passes 30/30 review regressions after restoration. An additional mutation-payload drift test fails 1/1 when only commitGithubImportTransaction is restored to round one; fixed code returns github_import_plan_drift before native writes and succeeds once the original saved plan is restored. Final unique completed gates cover 48 files (51 bounded invocations): 1121 tests, 1119 pass, 0 fail, 2 existing pm-cli#1656 quarantines. Import-gate is complete at 73/73; closeout-workflow is the sole capped file, 143 passes then 1 cancellation at 20 minutes. Fresh atomic gate is 11/11, refresh 34 pass plus 1 existing quarantine, import fidelity 53 pass plus 1 existing quarantine; packed npm/native Bun import projection is 6/6. Lint, typecheck, builds, docstrings and strict health with isolated global discovery pass. Full npm/Bun release and exact 100-percent coverage remain for CI under the requested gate budget; prior head CI reported 99.94-percent branch coverage, which is not certification for this change. Counts exclude the stopped redundant regex run and superseded failures fixed in compatibility fixtures. CI handoff: https://github.com/unbraind/pm-github/pull/233/checks . Original Projects follow-up remains open because task rules require out-of-scope tracking and append-only audit records. No Projects code changed in round two. The main item stays in_progress; releasing only this agent claim before commit and push." + "2026-11-11T16:08:01.288Z",codex-github230-1111,"Round 3 starts from clean head 080f1e6; round-2 work is accepted. Read the three orchestrator findings in full. Plan: replace package-wide GitHub issue URL parsing with one URL/path-segment helper; persist and enforce privacy preflight verdicts before recovery writes; and allow legacy number-only promotion only for one implied repository, reporting ambiguous legacy references as typed summary conflicts. Preserve accepted side-data and original-plan recovery behavior, add real SDK tracker regressions and disposable behavioral reverts, run bounded affected-file gates, then release the claim and push." + "2026-11-11T16:22:16.093Z",codex-github230-1111,"Read the current GitHub review snapshot: 7 threads, 7 complete comments/diffs with no further comment pages, and 4 review bodies. Three newer threads include the rejected-recovery finding, retained committed refresh work (PRRT_kwDOTbN4pM62PHLJ), and CodeQL polynomial URL-related parsing (PRRT_kwDOTbN4pM62PrYX). The shared URL parser removes path replacement and whole-path issue/PR regex parsing; repository suffix normalization now uses a linear scan. Recovery now saves privacy verdicts and requires both a saved pass and a fresh pass before replay, including refresh. Retained committed plans are discovered before refresh polls new data, closing the related side-data replay class. Initial round-3 focused suite passed 26/26 real-SDK cases, including reserved repository/owner names, decorated URLs, legacy ambiguity, and failed/missing/forged privacy preflights with unchanged documents and histories." + "2026-11-11T16:55:04.657Z",codex-github230-1111,"Round 3: all GitHub issue/PR identity paths now share URL and fixed-segment parsing. Ambiguous number-only legacy provenance is a mention with typed ambiguous_legacy_github_identity summary evidence; a single repository or explicit repository still resolves. Every replay requires a saved passing preflight plus a fresh scan before mutation. Also fixed retained committed refresh side-data discovery (Greptile PRRT_kwDOTbN4pM62PHLJ) and replaced the slash-suffix regex flagged by CodeQL alert 12 with linear trimming. Lint, typecheck, docstrings (30 files, 276 declarations), and duplication (zero authored duplicated lines) pass. Broad dependent-file gates remain running with two workers and 20-minute invocation caps; closeout-workflow reached its cap and needs CI certification." + "2026-11-11T17:04:52.430Z",codex-github230-1111,"Round 3 read receipt: all 3 supplied blocking findings, 7 review threads with 7 complete comments/proposed diffs, and 4 complete review bodies were read (no remaining pages), plus the full CodeQL alert 12 payload. Accepted round-2 fixes remain intact. The shared URL parser is used by import identity, refresh, sync/export ownership, diagnostics, blockers, closeout, discussions and Projects URL inputs; transport pagination/origin checks remain transport-only. Repository normalization preserves released opaque keys while removing the quadratic suffix regex. Strict pm health passes with two advisory warnings. Full npm/Bun release and 100% coverage certification remain CI work under the explicit gate budget." + "2026-11-11T17:16:29.057Z",codex-github230-1111,"Round 3 behavioral revert proof (real SDK trackers in a disposable package copy): restoring only explicitGithubKey to 080f1e6 produces 6 failures and 12 compatibility passes in the URL segments cases; bypassing assertGithubImportReplayPreflight produces 3 privacy/no-write failures; restoring only importIdentityKeys and matchImportedIssue produces 1 ambiguous-legacy failure with 2 compatibility controls passing. Restoring runRefresh produces 2 retained-annotation recovery failures. Replacing normalizeGithubRepository with the released suffix regex produces 1 bounded adversarial-input failure; reverting only the workflow conflict projection produces 1 formatter failure. Commands: node --test --test-concurrency=1 --test-name-pattern test/review-round3-identity.test.ts (prefixes: URL segments, recovery preflight, number-only legacy, refresh recovers, repository normalization), plus the sync PR includes pattern in test/import-conflict-report.test.ts. The fixed implementation was restored in finally; both files pass 30/30, and 11 implementation/test/workflow files match the package source byte-for-byte. All four importer-gate partitions pass: 16+6+33+18 = 73 cases." + "2026-11-11T17:19:33.716Z",codex-github230-1111,"Round 3 final local gates: 51 completed affected/required files across 54 bounded invocations, 1189 passing tests, zero failures, two existing pm-cli#1656 skips. This includes all 73 importer-gate cases, refresh 34 passes plus one skip, identity 20, cited adoption 46, atomic 11, quarantine guard 2, accepted review regressions 30 and new round-3 regressions 29. Initial compatibility failures were fixed and their complete files rerun successfully. The linked pm test run passes 29/29; restored disposable proof files pass 30/30 separately. closeout-workflow reached its 20-minute cap after 62 passes and one canceled case, without assertion failures; CI must finish it. Build, lint, typecheck, docstrings, duplication, strict tracker health and privacy audit pass. Full npm/Bun release checks and exact 100% coverage remain uncertified locally under the explicit budget. No outstanding implementation blocker or new follow-up was found; the existing Projects v2 follow-up remains open. Releasing only this author claim and leaving the issue in progress." + "2026-11-11T18:13:07.803Z",codex-github230-1111,"Round 4 plan: SDK declaration audit found public writeFileAtomic(targetPath, contents, {createParentDirectories?}) in sdk/index.d.ts and core/fs/fs-utils.d.ts. Use that helper for same-directory staging, fsync the completed candidate, publish by rename (replacement) or atomic no-replace link (plans and leases), then fsync directories. Audit found recovery plans, comment-sync leases, breaker-election markers, and webhook delivery/cursor/completion state. SDK-owned native history and transaction journals remain SDK-managed; scripts only emit derived output or disposable test mutations. Corrupt/unreadable plan contents will receive github_import_plan_corrupt with explicit restore/journal reconciliation steps; only ENOENT means missing. Add real SDK tracker regressions for native partial-write faults, crash before publication, concurrent readers, and clean zero-item retries. Round-3 implementation is accepted and retained." + "2026-11-11T18:20:22.259Z",codex-github230-1111,"Round 4 review read receipt: 13 threads with 13 comments/diffs and 6 review bodies enumerated without remaining pages; all 6 new thread texts/diffs and the new CodeRabbit body read, carrying forward the 7 already accepted round-3 threads. Q98c and Q98i are already fixed by 90d530f; Q984 is marked addressed there. New unrelated minor findings are tracked in pm-github-curated-reopen-followup, pm-github-pr-url-alias-followup and pm-github-blocker-format-followup after searching existing items. The outside-diff README owner-linking instructions now use github_url. Retain required append-only tracking evidence despite the bot scope suggestion to delete the existing Projects follow-up. Round-4 code remains scoped to durable publication and typed corrupt-plan recovery." + "2026-11-11T18:37:25.732Z",codex-github230-1111,"Round 4 focused expanded corpus passes 11/11 on real non-git SDK trackers. Native faults write actual partial bytes only to hidden candidates; empty-source retry and subsequent real import still work. SIGKILL at the flushed-candidate hook leaves zero native items and no discoverable final plan. Concurrent readers observe previous complete state during publication, failure and interruption; retry succeeds. Empty, unparseable and structurally incomplete JSON all produce github_import_plan_corrupt with restore/journal recovery instructions. The same validateGithubImportPlan function checks both reads and apply. Build, lint, typecheck, docstrings (31 files, 277 declarations), zero-duplication gate and strict health pass. The initial privacy audit passes 9459 objects; final commit will be audited again. Bounded dependent-file gates are running without failures so far." + "2026-11-11T18:42:15.474Z",codex-github230-1111,"Round 4 bounded sweep found one compatibility edge: an existing comment lease in a read-only directory must report contention rather than degrade because staging cannot write there. Added an early lstat existence hint, retaining the final exclusive hard link as the authoritative race check. The full import-lock file will be rerun after the remaining sweep; the real permission test and cross-process contention tests cover the preserved contract. No SDK or dependency change was needed." + "2026-11-11T19:05:49.794Z",codex-github230-1111,"Round 4 behavioral revert proof in a disposable package copy, using real SDK trackers and only native filesystem fault injection: old persistGithubImportPlan behavior (retaining the observation hook) gives 2 failures: partial final JSON poisons an empty-source retry, and SIGKILL exposes a final plan before publication. Old readGithubImportPlan gives 6 missing typed-corruption refusals. Replacing writeGithubStateAtomic with the prior direct-write behavior gives 3 concurrent-reader failures (publication, write failure, interruption). Old acquireImportLock gives 1 observed empty reserved lease. Revert commands use node --test --test-concurrency=1 with the matching partial native plan/crash after plan, corrupt recovery plan, concurrent state readers, and failed atomic lock publication name patterns. Total: 12 expected failures, no cancellations. Restored fixed code passes the complete durable-recovery file 11/11 plus the lease regression 1/1; all 9 code/docs/test files match the package implementation byte-for-byte. The package worktree was never mutated by these reverts." + "2026-11-11T19:16:39.154Z",codex-github230-1111,"Round 4 final portable regression proof: the initial Node-only filesystem replacement harness did not inject faults under Bun, so fault injection now uses the explicit prepublication hook with real partial candidate writes and injected EIO. No pm SDK or filesystem APIs are mocked in the new durable-recovery corpus. Node and native Bun each pass 11/11, and the lease regression passes under Bun. Repeated behavioral reverts preserve the observation hooks while restoring unsafe direct writer/lease behavior: plan writer 2 failures, old reader 6, direct state publisher 3, direct lease publisher 1. All are behavioral failures (poisoned zero-source retry, premature final publication, absent corruption refusal, or partial bytes observed). Restored source passes 11 durable cases plus 1 lease case and matches all 9 package code/docs/test files byte-for-byte." + "2026-11-11T19:16:39.997Z",codex-github230-1111,"Round 4 final gates: 51 completed files / 54 bounded invocations, 1161 passes, zero failures, two existing pm-cli#1656 skips. All 73 importer-gate cases pass; full import-lock recheck passes 25/25 after preserving read-only contention. Refresh is 34 passes plus one existing skip; accepted review files pass 30/30 and 29/29. Linked pm runs of node --test --test-concurrency=1 test/durable-recovery.test.ts and bun test --timeout 60000 test/durable-recovery.test.ts each pass 11/11. Automatic test-result tracking is disabled by project policy, so these outcomes and revert evidence are recorded in comments. closeout-workflow reached its 20-minute cap after 65 passes and one cancellation without assertion failures. Full npm/Bun release and exact 100% coverage remain CI certification at the linked PR checks page. Six logical writes now use four shared atomic-publication entry points; SDK-owned native journals/history remain SDK-managed. No new SDK gap or dependency change. Three minor review follow-ups remain open. Releasing this author claim and leaving the main issue in progress." + "2026-11-11T20:13:15.527Z",codex-github230-1111,"Round 5: claimed the main item and the two existing follow-ups now in scope, without creating duplicates. Keep structured URL identity parsing strict. Prose extraction tracks balanced brackets, matching emphasis and trailing punctuation. Every unsupported HTTP URL becomes a visible unparsed_url reference, never a guessed hash from its interior, so it cannot make an item cleared or produce a cleared comment. Explicit URL alias resolution is shared by ownership and refresh, retaining the parsed issue/PR kind. New real non-git SDK tracker tests cover all requested wrappers, punctuation, the exact mixed-reference reproduction, unsupported URLs and case/underscore aliases. Review snapshot: 21 threads/comments and 9 reviews enumerated without further pages; the two targeted full texts/diffs were read (SJvl and Q99D). Accepted round-4 changes remain intact." + "2026-11-11T20:30:23.327Z",codex-github230-1111,"Round 5 false-clear path audit also found self-exclusion treated every gh: tag as ownership. collectBlockerReferences now uses the same authoritative importIdentityKeys resolver to exclude only actual self identity, retaining reference-tagged blockers. Added a real tracker reproduction with gh:o/r#2 plus the mixed closed/open Markdown links; it must retain open #2. Legacy generated owner markers still resolve through the existing compatibility rule. No other ownership or content merge behavior changed. The focused file now contains 31 cases; behavioral reverts will cover the collector independently." + "2026-11-11T21:06:54.225Z",codex-github230-1111,"Round 5 final behavioral revert proof in a disposable package copy with real SDK trackers and real HTTP transport: node --test --test-concurrency=1 --test-name-pattern PATTERN test/github-url.test.ts. Restoring only the previous tokenizer body (retaining span metadata) makes all 17 ^Markdown cases fail; restoring parseBlockerReferences makes all 8 ^Unparsed cases fail; restoring collectBlockerReferences makes the ^Reference tags case fail; restoring selectRefreshTargets makes all 5 ^Refresh excludes PRs cases fail. Total 31 expected behavioral failures, zero cancellations. Each required wrapper and punctuation case fails on revert, including the supplied mixed closed/open URL reproduction. The restored implementation passes 31/31 and all seven code/docs/test files match the package implementation byte-for-byte. The package worktree was never reverted." + "2026-11-11T21:06:55.106Z",codex-github230-1111,"Round 5 final gates: 54 selected test files / 57 bounded invocations, 56 completed invocations, 1231 passes, zero assertion failures, 2 existing pm-cli#1656 skips. All 73 import-gate cases pass across four disjoint name partitions. closeout-workflow reached its 20-minute cap after 69 passes and one cancellation; it remains incomplete CI evidence. Final blockers recheck passes 35/35. Linked pm runs of node --test --test-concurrency=1 test/github-url.test.ts and bun test --timeout 60000 test/github-url.test.ts each pass 31/31; follow-up pm runs pass 26 blocker cases and 5 alias cases. Project policy disables automatic test-result writes, so exact outcomes are recorded here. Lint, typecheck, build, test compilation, docstrings (31 files / 278 declarations), zero duplication and strict tracker health pass (two existing advisories). Full npm/Bun release and exact 100% coverage remain CI certification under the task budget. Read receipt: two supplied findings; 21 threads / 21 comments and 9 reviews enumerated without remaining pages, with the two targeted full texts/diffs read (PRRT_kwDOTbN4pM62SJvl and PRRT_kwDOTbN4pM62Q99D). The accepted rounds remain intact. Releasing all three scoped claims; items remain in progress. Curated reopen and Projects follow-ups remain unchanged." + "2026-11-11T22:07:06.775Z",codex-github230-1111,"Round 6: accepted head rechecked clean. Audit every existing-item write target against exactly one authoritative GitHub owner, including deterministic upsert collisions, URL/alias matches, refresh, closeout and durable replay/finalization. Choose a typed collision refusal rather than silently reassigning an existing deterministic ID; preserve byte-identical item content on refusal and ordinary owner-matching idempotency. Real SDK tracker matrix and isolated behavioral reverts will cover each path. The completion-date reopen defect remains deferred. Current review receipt: one supplied blocking finding; 21 threads/comments and 10 reviews enumerated without further pages. The curated-reopen thread and latest CodeRabbit body were read in full." + "2026-11-11T22:21:13.228Z",codex-github230-1111,"Round 6 implementation: typed github_import_owner_conflict refuses occupied deterministic IDs rather than allocating suffixes. One shared positive owner assertion protects preflight and every SDK item-lock write, including replay, fidelity and annotations; bound delegates retain ambient mutation hooks. Plain CLI writes and dependency edges carry --expect-hash from the same live ownership read. URL-only, alias-only and number-only targets are checked independently. Closeout holds imported owners outside its delivery repository while retaining explicit native delivery for local items. Real SDK non-git matrix passes 21/21 under Node and Bun, including matching-owner idempotency and ambient hook controls. The bounded dependent sweep is now running with two workers." + "2026-11-11T23:34:38.727Z",codex-github230-1111,"Round 6 refinement: deterministic IDs, even alongside a matching reference tag, no longer confer ownership. Released source-number/link markers and generated source descriptions still resolve without migration; a real pre-fidelity marker control updates its original ID and then re-syncs byte-identically. Creation/content plans must retain the generated source marker before SDK updates, otherwise github_import_plan_ownership_invalid refuses before journal publication. Existing-item checks use live normalized ownership, locked SDK hooks, and CLI expected revisions. First-link provider/citation authorization is preserved without overriding explicit GitHub owners. Collision remedy restores intended provenance only for the intended item; otherwise select a different configured ID prefix or tracker and retain existing items/history. New relink fixtures use mutateItem and verify complete history chains plus the current item hash, rather than the birth-only import adapter. Both same-repository and cross-repository closeout transfers are held." + "2026-11-11T23:48:50.034Z",codex-github230-1111,"Round 6 final behavioral revert proof in disposable package copies only: disabling assertGithubImportOwner alone makes all 19 shared-guard cases fail; removing only ownedPmRun expected-hash arguments makes the real peer/CLI race fail with changed native body/history; disabling assertCloseoutGithubOwner makes both same-repository and cross-repository delivery cases fail. The source matrix has 22 refusal cases and four must-allow/ambient controls. The exact gated atomic upsert reproduction uses a valid URL-only transfer to other/project#8 and compares native item/history bytes before checking the refusal, so a late gate error cannot hide an overwrite. Curated collisions run without Git and include a matching reference tag, plus a second incoming issue to prove whole-batch refusal. Every relink verifies the full native history chain and its current item hash. Source revert commands are node --test --test-concurrency=1 --test-name-pattern PATTERN test/import-ownership.test.ts, with patterns ^Ownership (?!plain CLI|closeout|matching-owner|guard preserves), ^Ownership plain CLI, and ^Ownership closeout. Restored source passes 26/26. Rebuilding the artifact with only the owner assertion disabled makes the packed npm consumer regression fail; restoring and rebuilding makes that exact packed case pass 1/1. Its command is node --test --test-concurrency=1 --test-name-pattern \"^packed npm\" test/import-projection.test.ts. The packed case verifies the public CLI JSON github_import_owner_conflict code, both owner values and unchanged native bytes. All production code/docs and the relevant tests match their restored copies. No pm SDK mocks are used in the new matrix." + "2026-11-11T23:48:51.488Z",codex-github230-1111,"Round 6 final bounded gate receipt: 54 selected test files / 57 initial invocations, corrected with linked/full or targeted fixture rechecks; 53 completed files / 56 complete invocations contain 1220 tests: 1218 pass, zero remaining failures and two existing pm-cli#1656 skips. The importer gate passes all 73 cases in four disjoint partitions. closeout-workflow reached its 20-minute cap after 74 passes and one cancellation without an assertion failure; CI must certify that file and the full npm/Bun release/coverage gate. Both 500-case label models pass, as does their corrected shared-conversion case. Final linked pm runs of node --test --test-concurrency=1 test/import-ownership.test.ts and bun test --timeout 60000 test/import-ownership.test.ts each pass 26/26 with valid SDK mutation histories. Finalization, refresh, idempotency, SDK hooks, literal-field and packed consumer rechecks pass. Existing command-failure fixtures now seed real owners; deleted targets refuse at the SDK tombstone read before mutation and retain their history. Build, test compilation, lint, typecheck, docstrings (32 files / 287 declarations), zero duplication (33 authored modules) and strict tracker health pass with two existing advisories. No dependency or SDK pin changed; no new SDK gap or quarantine was introduced. Project policy disables automatic test-result records, so outcomes are recorded in comments. Current review receipt: one supplied blocking finding; 21 threads / 21 comments and 10 reviews enumerated without remaining pages. The curated-reopen thread PRRT_kwDOTbN4pM62Q98t and the latest CodeRabbit body were read in full. Its completion-date reproduction is recorded on the deferred item without changing behavior. The two outside-diff read-path findings are tracked separately. CI evidence remains at https://github.com/unbraind/pm-github/pull/233/checks. Releasing this author claim; the main item remains in progress and follow-ups stay open or in progress." + "2026-11-11T23:54:22.048Z",codex-github230-1111,"Final fixture tightening: the dependency-race regression now delegates listing to the real SDK and uses the real default ordering-cycle collector. The exact command node --test --test-concurrency=1 --test-name-pattern \"^Ownership dependency writes\" test/import-ownership.test.ts passes 1/1. In a disposable package copy, disabling only assertGithubImportOwner makes that same case fail 1/1; restoring it passes 1/1. Final test compilation and lint pass. This strengthens the existing 22-case behavioral revert proof without changing production code, test counts or the gate receipt. All tracker histories remain byte-prefix extensions of the accepted base and origin/main. Releasing only this author claim before commit; no item is closed." + "2026-11-12T01:29:06.709Z",codex-github230-1111,"Round 7 starts from accepted ownership head eeee95d. Merged current origin/main before new work. Read the supplied atomic dry-run review and all eight requested full thread bodies, AI-agent prompts and diffs. Plan: use a real initialized tracker for apply-equivalent dry-run ownership checks; unify atomic annotation accounting and exactly-once synchronization; reject ambiguous dependency owners; make durable file flushing Windows-compatible; reject backslash URL paths; align fresh/replayed privacy preflight; isolate closeout identity conflicts to eligible item rows. Preserve accepted ownership and all earlier regressions. Every fix gets behavioral revert evidence in disposable copies, real SDK controls and bounded affected-file gates. No review threads will be answered or resolved." + "2026-11-12T01:42:41.626Z",codex-github230-1111,"Round 7 decisions and implementation: dry-run needs an initialized real tracker so preview checks the same destination ownership as apply; atomic and plain preview fixtures now seed native SDK owners, remove fabricated pm reads/commit APIs, and assert item/history bytes and transaction-plan absence. Committed atomic imports return commentWriteFailures and annotationIssueNumbers, remove the plan despite best-effort comment refusals and let gated callers refuse incomplete side data through the shared divergence check. The outer loop handles only entries not already synchronized. Dependency indexing retains only unique explicit owners, excludes contradictory claims, and reports ambiguous_github_dependency_owner with all claimant IDs without linking. Both fresh gated and ungated atomic plans require passing privacy preflight before publication; legacy failed/missing/forged-plan regressions now seed a real interrupted clean journal before constructing legacy recovery inputs. GitHub path parsing rejects raw/encoded backslashes before WHATWG normalization while allowing query/fragment text. File fsync uses writable r+ handles; directory fsync is explicitly skipped on win32. The package-wide flush search found a second instance in webhook receipt recovery; it now uses the same helper. Tests stub only platform selection and keep filesystem operations real. Closeout catches identity and ownership conflicts as per-item outcomes and does not check explicitly skipped, context-only or foreign references; unrelated valid deliveries still close. Existing curated-reopen behavior remains deferred and no review thread was answered or resolved." + "2026-11-12T01:42:43.636Z",codex-github230-1111,"Round 7 behavioral revert receipt, disposable copies only: reverting commitGithubImportTransactionUnchecked causes 2 atomic annotation-failure regressions to fail while 2 plain controls pass; removing the outer annotation receipt skip causes 2 changed/mixed exactly-once failures while the unchanged control passes. Reverting buildProvenanceIndexFromMetadata causes both ambiguous source/target regressions to fail while the unique-owner control passes. Restoring the previous read-only file-sync policy causes all 3 platform regressions to fail; restoring unconditional directory sync causes the win32 case to fail and POSIX controls to pass. Reverting parseGithubUrl causes 3 backslash-normalization regressions to fail, with 2 previously rejected encodings plus must-allow query/trailing-slash controls retained. Reinstating gated-only fresh preflight causes the ungated regression to fail while the gated control passes. Reverting closeoutAttempt causes all 4 eligible/skipped/context/foreign scenarios to abort instead of closing the unrelated valid delivery. Restoring the original atomic dry-run fixture reproduces tracker_root_missing (1 failure); fixed fixtures use native SDK init/create/mutateItem. Restored focused tests pass 21/21. Commands use node --test --test-concurrency=1 with the Round7, durable-state, backslash and atomic dry-run name patterns; source/package production files were never reverted in the real worktree. Whole atomic.test.ts passed 11/11; broad bounded dependent sweep is underway with a maximum of 2 workers." + "2026-11-12T02:01:16.029Z",codex-github230-1111,"Read the newest nonempty CodeRabbit review body in full (20 reviews enumerated, 34 threads / 42 comments, no further thread pages). The 10 additional observations beyond covered explicit-closeout and dry-run cases are consolidated, pending validation, in https://github.com/unbraind/pm-github/issues/236 and pm-github-reference-identity-followup, following the instruction to use one follow-up instead of round 8. All 10 full comment bodies and AI-agent prompts were read. The dependency-index half of auB7 is already addressed; its blocker self-exclusion half stays in that follow-up. The review request to remove earlier accepted scope and mandatory tracker files is not applied: prior rounds are accepted and tracker history must remain append-only. No review thread was replied to or resolved. Three older annotation fixtures in coverage-remainder relied on fabricated pm snapshots/commit results and stopped at provenance validation; they now use real stored owners and a native SDK policy refusal, with explicit annotation-failure counts and committed-plan cleanup assertions. Final regression rechecks will cover those fixtures, direct atomic APIs, replay, real HTTP dry runs and all closeout eligibility exclusions." + "2026-11-12T02:50:42.074Z",codex-github230-1111,"Round 7 final self-review and test receipt: all 73 import-gate cases pass in four disjoint name partitions. The 57-file affected/required sweep has 60 bounded invocations; 56 completed files / 59 completed invocations contain 1290 tests: 1288 pass, zero remaining failures, two existing pm-cli#1656 skips. closeout-workflow reaches its 20-minute cap after 42 passes and one cancellation without an assertion failure; CI certifies that file and the full npm/Bun release/coverage gate. Final whole-file rechecks pass atomic 11/11, ownership 26/26, closeout 93/93, corrected coverage-remainder 25/25, and real HTTP dry-run preview 2/2. Linked pm runs of node --test --test-concurrency=1 test/round-seven.test.ts and bun test --timeout 60000 test/round-seven.test.ts pass 23/23 each; node --test --test-concurrency=1 test/durable-state.test.ts passes 3/3. Project policy disables automatic run artifacts, so results remain in comments. Bun exposed event coalescing in a filesystem-watcher assertion; the test now observes actual native SDK mutations, advancing only its incoming fixture cursor so an unwanted second pass would append an extra annotation. No pm API or filesystem is mocked. Unchanged and mixed entries have explicit controls. Extra tests cover committed-plan replay, direct gated atomic APIs, contradictory dependency identities, shared reference-only mentions, and all closeout exclusions including terminal items and epics. Production code was frozen before these final rechecks; additional observations are consolidated in issue #236." + "2026-11-12T02:50:43.885Z",codex-github230-1111,"Round 7 final behavioral revert evidence in disposable copies: reverting commitGithubImportTransactionUnchecked produces six atomic annotation/refusal failures with two plain controls passing; removing only the outer receipt skip produces two exactly-once failures with the unchanged control passing. Reverting the dependency index produces three failures (duplicate source, duplicate target, contradictory identity), while the unique owner plus reference-only mentions passes. Restoring read-only file flushing produces three platform failures; restoring unconditional directory flushing produces one win32 failure with POSIX controls passing. Reverting the URL parser produces three backslash normalization failures and retains two existing must-refuse controls. Restoring gated-only fresh privacy preflight produces one ungated failure while the gated control passes. Reverting closeoutAttempt produces six failures across eligible, explicit-skip, context, foreign, terminal and epic references, each with an unrelated valid delivery. Restoring the old atomic dry-run fixture produces tracker_root_missing (one failure). Restored focused tests pass 28/28. After replacing filesystem event counting, repeated the exactly-once revert against the final fixture: both changed/mixed cases fail and the unchanged control passes; restored focused tests again pass 28/28. Source commands are node --test --test-concurrency=1 --test-name-pattern PATTERN FILE with ^Round7 annotation refusal, ^Round7 atomic annotations, ^Round7 dependency, ^Round7 failed atomic preflight, ^Round7 closeout, ^GitHub ownership rejects backslash, ^atomic dry-run, plus test/durable-state.test.ts. No tracked implementation was reverted or native audit history rewritten. Main and base histories remain append-only. CI handoff: https://github.com/unbraind/pm-github/pull/233/checks . Releasing only this author claim; the main item stays in progress and the consolidated follow-up remains open and unclaimed." + "2026-11-12T06:13:06.006Z",codex-github230-1111,"Round 8 starts from accepted head f252e3e; merged current main before implementation. Read the four blocking threads, the added ownership-preflight cleanup thread and outside-diff deletion-durability comment in review 5868419634 in full, including agent prompts. Scope is only shared ownership consistency/uniqueness at locked writes plus safe plan cleanup and the existing directory-flush policy. Other findings remain in issue #236 and no review thread will be answered or resolved. Asked for clarification of tag-only references versus the new claim rule; pending an answer, preserve the original requirement that reference-only items are not owners, while validating all claims on provenance-bearing items. Add one real SDK matrix across matching, closeout and replay, plus native concurrent-peer and cleanup/journal controls, then bounded affected-file gates and behavioral reverts." + "2026-11-12T06:49:20.097Z",codex-github230-1111,"Round 8: merged current main and read all five scoped review threads, including agent prompts and proposed changes, plus review 5868419634 outside-index-1745. Shared normalized claims now include external_ref, supported legacy link fields, explicit URL aliases, partial repository/number provenance and tags on provenance-bearing records. Reference-only tags remain non-owning, preserving the original import contract. Every native update checks current agreement and competing claims under the SDK lock; replay also checks persisted provenance before gated success. The 77-case real-tracker matrix passes, including 14 contradiction/ambiguity shapes across match, plain import, atomic import, closeout and gated replay, consistent legacy/full controls, late peer claims, no-journal cleanup, journal retention and native directory-flush failure. Scoped gates are running with two total workers and per-command 20-minute limits." + "2026-11-12T07:20:14.799Z",codex-github230-1111,"Round 8 compatibility note: the stricter claim rule intentionally changes earlier permissive cases where a provenance-bearing item referenced a different issue or repository. Those records now refuse with github_import_owner_conflict, keep their native item/history bytes and do not create a competing import. Reference-only records remain shareable. Updated the earlier round-two/three and reference regression expectations to match this extended contract. Plain creates and all owned mutations now use guarded native SDK writes; command-failure fixtures use real SDK policy refusals instead of child-CLI interception. The aggregate closeout workflow command reached its 20-minute cap after 49 passing cases and one canceled file wrapper; CI must certify its remaining workflow matrix." + "2026-11-12T07:43:53.586Z",codex-github230-1111,"Round 8 revert proof, using disposable source copies and real SDK trackers: node --test --test-concurrency=1 --test-name-pattern \"^Ownership matrix (?:match|plain|atomic|closeout|replay): refuses\" test/ownership-consistency.test.ts fails 70/70 when the changed ownership functions are reverted, including matchImportedIssue and the closeout check. Reverting only uniqueness in assertGithubOwnershipConsistency makes all four \"^Ownership matrix locked\" cases fail. Reverting commitGithubImportTransaction plan cleanup makes \"^Ownership preflight removes\" fail 1/1. Reverting removeGithubImportPlan directory flushing makes \"^Recovery-plan deletion flushes\" fail 1/1. Restoring the implementation passes 82/82. The same 82-case matrix passed through pm test --run --only-index 74 --progress --timeout 1200; native policy-failure coverage passes 25/25. Removed the auxiliary PR tag from this issue-owned item so every identity claim agrees on issue 230; PR 233 remains a docs reference. Item/history and native state are never repaired or rewritten." + "2026-11-12T07:47:10.726Z",codex-github230-1111,"Round 8 final local evidence: 58 selected test files, using at most two total workers and a 1200-second command cap. Effective distinct outcomes after the documented reruns: 1422 reported tests, 1419 passed, zero assertion failures, two existing SDK-related skips, one canceled aggregate closeout-workflow wrapper at its 20-minute cap (49 workflow cases passed before cancellation). CI certifies the remaining workflow decision matrix and full release gates. The 82-case ownership matrix passes through pm on both Node and Bun; 76 refusal/durability assertions fail on their corresponding behavioral reverts (70 ownership shapes, four late claims, plan cleanup, directory flush), and restoration passes 82/82. Native policy-failure coverage passes 25/25. Build, test compilation, lint, typecheck, docstrings and duplication passed; 33 authored modules have zero duplicated lines. All 61 initial scoped command variants are already linked among the 75 item tests. Read the complete 38-thread snapshot, all five scoped threads with agent prompts, and the one scoped outside-diff comment. Deferred findings remain in issue 236; no review replies or resolutions were made. The ownership corpus read can create derived SDK caches, so fresh atomic privacy preflight now runs before that read; the real-tracker fresh/replayed privacy regressions pass 29/29. Existing audit histories remain append-only. Exact-head CI evidence: https://github.com/unbraind/pm-github/pull/233/checks ." + "2026-11-12T08:41:04.182Z",codex-github230-1111,"Round 9 resumes from bd8d52d with a clean worktree. Current main is already merged. Read the three blocking CodeRabbit threads and agent prompts: follow-up reference tag, durable creation proof, and SDK 2026.10.17 compilation. Scope stays limited to those findings; the three non-blocking observations remain in issue 236. Plan: replace stray PR tags with docs references across changed tracker records, separate creation provenance from item naming and issue ownership, and retain revision fencing through a typed minimum-SDK fallback. Run the four scoped real-tracker suites plus current/floor typecheck, lint and changelog checks under 20-minute command limits." + "2026-11-12T08:59:05.505Z",codex-github230-1111,"Round 9: the 90-case matrix passes. Audited all seven tracker records added or changed by this PR; moved six contextual parent issue/PR tags to docs links, retaining only matching issue-230 and issue-236 identity tags. Creation proof now uses package author or an explicit github_import boolean, never an ID. New creates record pm-github as creator, and locked writes refuse an original mirroring plan if its target loses creation proof. Unknown-origin legacy records keep identity compatibility but preserve curated content. The isolated minimum-SDK job found both the GetResult.item_hash field and PmClient.withExpectedItemRevisions method unavailable at SDK 2026.10.17. Replaced them with native document/hash reads and a locked revision check using APIs available at that floor. The packed consumer now compiles all production/operational sources and declarations, exercises an owned update, and refuses a stale revision at SDK 2026.10.17. No SDK floor or dependency pins were raised. Scoped suite reruns retain the existing diagnostics for curated collisions and missing items." + "2026-11-12T09:15:18.711Z",codex-github230-1111,"Round 9 revert proof in disposable copies: adding the former PR-233 tag back through the SDK makes the PR tracker audit fail 1/1; removing it restores 1/1. Reverting only importerCreatedItem to its ID-suffix implementation makes all seven Creation proof tests fail: two curated-ID cases, four provenance-backed non-pattern-ID controls, and a stale mirroring-plan refusal. Restoring it passes 7/7. Restoring the previous readGithubImportOwner and mutateGithubOwnedItem makes timeout 1200 npm run test:minimum-sdk fail with TS2339 for both GetResult.item_hash and PmClient.withExpectedItemRevisions at SDK 2026.10.17; restoring the native document/hash and locked revision guard passes the same floor job. Final restored matrix: 90/90. The data revert and restoration used native append-only SDK mutations; no existing history record was rewritten." + "2026-11-12T09:15:19.533Z",codex-github230-1111,"Round 9 final local gates: ownership-consistency 90/90 on Node and Bun through pm; import-ownership 26/26; import-fidelity 53 passed plus one existing pm-cli#1656 skip; closeout 93/93. Four-file total: 263 reported, 262 passed, one existing skip, no failures or cancellations. timeout 1200 npm run test:minimum-sdk passes 1/1 through pm against exact SDK 2026.10.17, including all authored source/declaration typechecks, a guarded owned update and stale-revision refusal. Current build, test compilation, lint and typecheck pass; docstrings, zero duplication and privacy checks pass. All test commands stayed below 20 minutes with at most two workers. Read 45 complete thread inventory entries and the full three scoped blocking threads with their agent prompts. No thread replies or resolutions were made. All seven PR tracker records were audited; six contextual references became docs links, while the matching issue-230/236 tags remain. The other three findings stay deferred to issue 236. Exact-head release certification remains CI: https://github.com/unbraind/pm-github/pull/233/checks ." + "2026-11-12T10:01:49.068Z",codex-github230-1111,"Round 10 starts from d9d9bc7 with a clean worktree; fetched main is already merged. Read the complete named codex verdict and issue 236. Inventory first: added docs/identity-ownership-inventory.md with every ownership-helper call and direct identity-field access grouped by enclosing function. Confirmed bypasses in upstreamTargets and dependency indexing; the audit also identifies refresh, Projects issue association, search association, diagnostics, blocker self-exclusion, and remote execution rechecks. Route target selectors through shared claims while documenting explicit-source and reference-only operations that do not select owned targets. Tests will use real SDK trackers and HTTP transport, including consistent controls and revert proofs." + "2026-11-12T10:16:18.201Z",codex-github230-1111,"Round 10 inventory identified direct selectors in outbound sync/export, dependency indexing, refresh, search, Projects issue association and blocker self-exclusion. Operational selectors now share ownership claims and consistency/uniqueness checks. Dependency endpoints are checked again inside the native source mutation lock; outbound writes re-read current tracker ownership. The initial 47 outbound/dependency SDK and real-HTTP regressions pass. Broad importer-consumer checks are running with two workers and a 20-minute per-file limit." + "2026-11-12T10:46:00.601Z",codex-github230-1111,"Round 10 ownership matrix passes 161/161 through pm test (one selected command, source workspace; project policy keeps test-result tracking disabled). The completed source inventory has 83 function groups and 248 helper/field/parser sites, plus five script/workflow text matches reviewed with no independent owner selector. Direct importIdentityKeys consumption is now restricted to the shared claims implementation. Shared URL-kind and direct-provenance facets preserve native declaration lifecycle classification and exclude legacy PR links from issue refresh. Search, Projects issue association, blocker self-exclusion and sync hints also use shared claims; unrelated reference parsing, labels, board IDs and explicit source arguments are documented as non-owning. Real-tracker label-model and literal-assignment compatibility fixtures now declare importer provenance instead of relying on naming. Disposable behavioral reverts and remaining bounded consumer gates are running." + "2026-11-12T10:49:29.639Z",codex-github230-1111,"Round 10 behavioral revert proof in a disposable source copy: restoring the complete index.ts from d9d9bc7 makes every selected new case fail: 34/34 ^Outbound ownership, 13/13 ^Dependency ownership, 16/16 ^Ownership routing inventory:, and 2/2 ^Ownership sync hint. Restoring only selectRefreshTargets from that head makes 1/1 ^Refresh ownership kind: fail. Commands use node --test --test-concurrency=1 --test-name-pattern PATTERN test/ownership-consistency.test.ts, each capped at 1200 seconds. Every failure is an assertion/observable diagnostic or missing-refusal failure, with zero import/syntax setup failures and zero cancellations. Restoring all fixed source files makes the combined 66/66 cases pass. The package worktree was never reverted; native tracker storage and the real HTTP transport/server are used. The exact partial github_repo-versus-URL reproduction sends no HTTP writes with the fix." + "2026-11-12T11:01:33.165Z",codex-github230-1111,"Round 10 final bounded gate evidence: 52 unique test files, 1439 cases, 1437 pass, 0 fail, 2 existing pm-cli#1656 quarantines, 0 cancellations. All 48 direct consumers of changed modules were read and tested, plus three additional closeout files and the minimum-SDK consumer. Commands are timeout 1200 node --test --test-concurrency=1 TEST_FILE, with at most two total workers. Ownership matrix: 161/161 through pm test. Link-deps: 28/28; import-ownership: 26/26; import-fidelity: 53 pass plus one existing skip; refresh: 34 pass plus one existing skip; closeout core/attribution/revision/selection: 93/12/110/82 passes; Projects: 39/39 and integration 19/19; packed npm/native Bun projection: 6/6. The full import-gate file completed 73 cases within its 20-minute cap; its two old diagnostic assertions were updated and rerun by exact names, yielding 73/73 final case evidence. Fixture corrections retain curated safety and explicitly mark genuine importer mirrors. npm run typecheck, npm run build:test, npm run lint and npm run docstring pass. npm run test:minimum-sdk passes its exact 2026.10.17 packed consumer (all production/operational modules, declarations, owned write and stale revision refusal); no dependency or engine floor changed. Privacy scanned 8749 existing objects with approved identities and no secret/host-path findings. Changelog is regenerated after this final tracker write and claim release. Full npm/Bun release and exact 100-percent coverage remain for CI: https://github.com/unbraind/pm-github/pull/233/checks . The priority external_ref dependency ambiguity in issue 236 is fixed here; other deferred observations remain there. Two supplied codex review findings were read in full; no review threads were replied to or resolved. Releasing only this author claim and leaving the item in_progress." + "2026-11-12T11:17:18.992Z",codex-github230-1111,"Round 11: reread issue 230 Expected and revert-test contract, plus all five scoped review threads and AI-agent prompts. The round-8 tag-claim rule is replaced: plain gh reference tags never confer ownership or conflicts, including on linked items. Preserve explicit provenance conflicts and legacy importer-created IDs with their verified repository hash. Build ownership indexes once per snapshot, share pre-journal corpus reads and transaction-local locked reads, and retain fresh outbound checks. Add real SDK regressions and deterministic N=40 cost counts; align URL mention normalization and complete preview byte snapshots. Public scratch-clone previews are read-only, capped at 15 minutes and about 60 API calls, with cleanup. Main has not moved from the existing merged ancestor." + "2026-11-12T11:47:48.604Z",codex-github230-1111,"Round 11 contract: plain gh: tags are mentions on every item and never promoted, constrained or enabled by the deprecated upstream flag. Proven importer IDs use the shared repository hash plus creation provenance, preserving legacy records without github_number. Ownership snapshots now index claims once, batch native preflight reads peers once and locked checks reuse one snapshot; outbound writes read one fresh corpus and validate only the current target. Added native N=40 work-count regressions, API mention grouping, full byte-snapshot controls and tag/legacy compatibility cases. Public preview completed with unchanged clone bytes and zero legacy duplicates; detailed counts and privacy refusals will be recorded after final validation." + "2026-11-12T11:51:05.856Z",codex-github230-1111,"Read-only public-check discrepancy: pm-context and pm-linear proposals have zero legacy-ID duplicates and no curated-field replacements, but their existing GitHub bodies trigger batch privacy refusals (two and five findings respectively). pm-review has no privacy/ownership refusal and no title replacements; its proven pm-github-authored importer record for issue 14 would restore source created_at, consistent with the accepted mirroring rule but different from the brief expectation that all four referenced records are curated. The other three retain curated timestamps. No clone was committed, pushed or left behind; completed checks preserve file bytes. We will report this exception explicitly rather than change importer mirroring to hide it." + "2026-11-12T11:57:30.922Z",codex-github230-1111,"Final-build public previews reused cached public REST/timeline payloads without additional API calls: pm-context 221 creates / 3 matching updates / one batch privacy refusal; pm-linear 217 / 1 / one batch privacy refusal; pm-review 82 / 1 / zero refusals. Zero ownership refusals, zero legacy duplicates and no curated-field replacement in all three. pm-linear-toolchain-1104 stays untouched. The four named review records keep their titles; the proven importer-authored issue-14 record would restore its source created_at, while the three curated records keep theirs. All three clone byte snapshots match and all clones were deleted. Total public calls: 59; active read-only checks stayed under 15 minutes. The early attempt that did not preserve clone bytes was discarded and rerun with read-only clones and optional Git index refresh disabled; only the completed byte-identical previews support these results." + "2026-11-12T12:02:45.295Z",codex-github230-1111,"Self-review of the inventory found an old closeout suffix-only destination fence beside the new shared helper. Removed that independent identity check: only a repository-hash-verified importer ID constrains ownership; a naming pattern for an unrelated repository cannot contradict otherwise consistent explicit provenance. Added a real-SDK closeout must-allow control and scoped legacy-ID resolution to the known delivery/project/dependency repositories. Creation proof still requires importer author or github_import metadata, and genuine explicit/ID conflicts remain refused. The native cost test now also exercises nested metadata finalizers inside its actual SDK transaction." + "2026-11-12T12:32:44.275Z",codex-github230-1111,"Round 11 core gates: ownership-consistency 142/142; reference-import-identity 17/17; N=40 native batch including nested finalizers uses exactly two corpus reads and 360 claim parses; outbound 40-item sync uses 1640 claim parses, preserving fresh per-write checks. The first import-gate command reached its 1200-second cap after 64 passes and one runner cancellation; the nine uncompleted workflow cases are scheduled separately. All scoped threads and AI-agent prompts were read in full (five threads); the deferred durable-reader observation stays in issue 236. No review threads were replied to or resolved. The routing ledger has 88 function groups and 259 helper-call/field/parser sites." + "2026-11-12T12:44:41.254Z",codex-github230-1111,"Round 11 behavioral revert proof, entirely in a disposable package copy: restoring tag-as-owner claims fails 5/5 Reference contract cases and all 6/6 issue-230 Chore/shared-reference/mirror cases. Disabling verified importer-ID ownership fails 1/1 legacy resync case. Restoring the prior native ownership wrapper fails 1/1 N=40 work bound; restoring prior upstream target rebuilding fails 1/1 outbound bound. Restoring the old closeout suffix fence fails 1/1 hash control, the old API mention key fails 1/1 grouping case, and the old preview file filter fails 2/2 byte-snapshot cases. Total: 18 expected assertion failures across eight groups, zero setup/import errors or cancellations. Restoring the fix passes 17/17 combined controls plus 6/6 issue-230 cases. Commands: timeout 1200 node --test --test-concurrency=1 --test-name-pattern PATTERN TEST_FILE, using ownership-consistency.test.ts or reference-import-identity.test.ts. The package worktree was never reverted. The complete 142-case matrix also passed through pm test --run --only-index 77 --progress; npm run test:minimum-sdk passed the exact 2026.10.17 packed consumer." + "2026-11-12T12:50:46.076Z",codex-github230-1111,"Round 11 final bounded evidence: all 51 direct/dependent test files plus the minimum-SDK consumer were read and exercised. Final case evidence across 52 unique files: 1420 tests, 1418 pass, zero remaining failures/cancellations, two unchanged pm-cli#1656 SDK quarantines (import-fidelity and refresh). The first import-gate attempt capped at 1200 seconds after 64 passes; all nine remaining cases passed separately, giving 73/73 distinct workflow cases. Final-source closeout core/attribution/revision/selection pass 93/12/110/82; Projects integration 19/19; importer ownership 26/26; reference identity 17/17; matrix 142/142 through pm test. Corrected legacy tag/declaration fixtures use explicit links or proven importer creation, and curated no-op fixtures assert retained local dates. Every command used at most two total test workers and a 1200-second cap. npm run typecheck, npm run build:test, npm run lint and npm run docstring pass (301 documented declarations). npm run test:minimum-sdk passes the exact declared 2026.10.17 packed consumer. Privacy audited 8787 existing Git objects with approved identities and no secret/host-path matches. Behavioral proof remains 18/18 failures on eight reverts and 23/23 after restoration. Public preview totals remain 59 API calls, zero legacy duplicates/ownership refusals and unchanged clone bytes; pm-context and pm-linear have existing-source privacy refusals, while the proven importer-created pm-review issue-14 record would restore source created_at. No public tracker or hold was changed. All seven listed per-repository reference-tag cleanup workarounds are unnecessary under this rule. The deferred reader thread stays in issue 236. Releasing only this claim; this item stays in_progress for orchestrator verification. Full release, coverage, npm/Bun and exact-head CI certification remain at https://github.com/unbraind/pm-github/pull/233/checks . Changelog is regenerated after this final tracker write and release." + "2026-11-12T14:41:29.946Z",codex-github230-1111,"Round 12 resumes from 3d5f91b with a clean worktree; fetched main is unchanged and already merged. Read all ten scoped/outdated threads, every attached AI-agent prompt, the README outside-diff comment, and orchestrator comment 6824496720. The scheduled command excludes PRs; its five-repo results supersede the round-11 PR-inclusive previews. Plan: guard atomic comments, reject API-only owner candidates, retain verified PR kind, refuse stranded retained plans, flush created directories on failure, scan the exact persisted recovery payload, correct guard/post-write docs, and resolve remaining scoped-pull/concurrency gaps. The project-index performance issue is already fixed by the shared snapshot index. All new tests use native SDK storage and local HTTP transport. Real end-to-end validation will pack/install this build in five disposable public clones, apply the exact scheduled import locally, and run its post-write checks without commits or pushes." + "2026-11-12T15:00:57.204Z",codex-github230-1111,"Round 12 implementation: guard atomic annotations with their import target; API-only link candidates receive no lifecycle writes; verified importer IDs retain matching generated PR kind; orphaned same-repository refresh plans refuse explicitly; durable state flushes newly created ancestors even after failure; privacy scans the entire recovery payload before staging. The two outdated ownership findings remain present at the starting head: native writes now share a reentrant tracker ownership lease acquired before SDK locks, and scoped Project pulls plan against complete peers and revalidate inside native writes. The third outdated finding is already fixed: 40 native Project records take exactly 120 claim parses across three planners. Focused native regressions pass 27/27; lint and typecheck pass. Representative scheduled-sync E2E and complete affected-file gates remain running." + "2026-11-12T15:15:05.453Z",codex-github230-1111,"Broad gates identified and corrected a missing PullPlan docstring and preserved enumerated privacy rule names in the new typed preflight refusal. Direct REST end-to-end checks passed gated import and repeat-import for pm-context and pm-linear before the bounded API budget stopped remaining source reads. To finish all five without further per-issue calls, one additional batched live GitHub GraphQL read supplies complete, pagination-checked public issue/comment/timeline snapshots to a read-only transport cache. Exact installed CLI commands, native tracker writes, workflow post-write gates and strict health remain unchanged; no GitHub writes are allowed. The rerun already passes every post-write step for pm-context, with zero curated file diff." + "2026-11-12T15:18:49.783Z",codex-github230-1111,"Round 12 real-data end-to-end: packed candidate 779e67d with npm pack --ignore-scripts, installed it project-locally in five fresh public scratch clones, and verified installed build bytes. Every repo passed the exact scheduled issue-only preview, atomic gated import with comments, repeat-import no-op, standalone gate, caller changelog, staged full-context diff gate and strict health. Counts (create/update): pm-context 7/3; pm-linear 8/1; pm-review 3/1; pm-brief 5/2 plus one unchanged; pm-rl 6/0. All five have zero ownership refusals and zero curated item/history diff. Real public source snapshots were fetched once through a pagination-checked batched GitHub GraphQL read and projected only to the transport response shapes, avoiding repeated per-issue calls; core REST-based import/repeat also passed for context and linear. Conservative total budget debit: 81 API calls including earlier harness attempts. All scratch clones were deleted; no commits, pushes or PRs were made from them." + "2026-11-12T15:21:21.439Z",codex-github230-1111,"Round 12 read receipt: 10 scoped/outdated threads, 10 complete comments including proposed patches and AI prompts, no remaining comment pages, plus the latest outside-diff README review instruction. Real-data check finished in 926 seconds including harness retries, below its 25-minute cap: all five repos passed all 13 setup/import/post-write steps, 29 creates, 7 updates and one unchanged issue, zero refusals, zero curated item/history changes. API accounting is a conservative 81-call debit including unsuccessful setup attempts and one final complete live GraphQL snapshot. The snapshot projection changes transport field names only; issue bodies, comments, owners, numbers and timestamps are real public source data. All scratch clones are deleted." + "2026-11-12T15:22:12.473Z",codex-github230-1111,"Round 12 behavioral reverts in disposable package copies: atomic annotation target guard 1 failure; API owner-link matcher 4; verified legacy PR kind 1; retained refresh orphan detection 1; failure-path ancestor flush 1; complete recovery-payload privacy scan 4; cross-item ownership serialization 1; full-corpus scoped Project planning 1; native Project write freshness 1; inventory contract 1; README post-write contract 1. Total 17 expected failures across 11 isolated function/document reverts; no setup/import/syntax failures or cancellations. Fixed implementation restored: all 29 native Round-12 cases pass, including must-allow twins. The privacy payload matrix now uses a custom field retained in the durable match snapshot, ensuring that case tests the newly added whole-plan scan rather than duplicating the existing projected-description scan. The already-fixed N=40 Project index control passes with exactly 120 claim parses." + "2026-11-12T15:26:27.563Z",codex-github230-1111,"The declared SDK floor correctly rejected the newly exported acquireLockLease API. Replaced that dependency with the floor-exported acquireLock API (verified in the published 2026.10.17 declarations), retaining tracker-wide serialization, native mutation ownership checks, reentrancy and guaranteed release. No dependency pin or minimum version changed. Re-running the packed minimum-SDK consumer, native Round-12 linked test, and affected compatibility files. The existing complete consumer probe executes a real non-git owned write and stale-revision refusal on the minimum SDK, in addition to linking all exports and compiling every source/declaration." + "2026-11-12T15:35:18.714Z",codex-github230-1111,"Final SDK compatibility preserves newer-host generation fencing through typed feature detection and uses the exported acquireLock fallback on SDK 2026.10.17. The final packed minimum-SDK test passes 1/1, compiling all production/operational sources and declarations and exercising a native owned write plus stale-revision refusal. The current linked Round-12 command passes 29/29 again. All 17 behavioral revert failures remain proved after restoration; lint and current-SDK typecheck pass. Privacy diagnostics retain rule names and the existing FAIL before write wording; complete daily-sync and import-fidelity compatibility reruns pass (3/3 and 53 plus one existing quarantine). The remaining affected-file sweep continues under per-command limits." + "2026-11-12T15:55:23.633Z",codex-github230-1111,"Round 12 privacy preflight preserves the shared redacted JSON finding envelope, including field names, offsets and hashes, while retaining the typed github_import_preflight_failed code. Added a native unsafe/clean receipt control; the linked Round-12 file now passes 30/30. Older recovery fixtures now sanitize sourceBody as well as rendered body before seeding a deliberately clean journal; unsafe atomic previews correctly refuse before staging. The 53-file sweep finished; failed compatibility cases are being replaced by successful complete or exact-name reruns. The workflow file's first command capped at 1200 seconds with 62 passes and two assertion failures; an explicit positive selector covers those failures and the nine unrun cases. A redundant negative selector matched the file container and was stopped, and its counts are excluded. A separately imported closeout decision-consumer file is also being exercised under the same 20-minute cap." + "2026-11-12T16:26:46.246Z",codex-github230-1111,"Round 12 final verification: 55 affected/required runnable files, 1640 distinct cases, 1638 passed, zero remaining failures or cancellations, two unchanged pm-cli#1656 quarantines (import-fidelity and refresh). Commands use timeout 1200 node --test --test-concurrency=1 TEST_FILE; at most two test workers. Import-gate completed 73/73 by retaining 62 passes and rerunning two failures plus nine unrun cases with explicit positive names. The separately imported closeout workflow consumer completed 187/187 with 171 retained passes and 16 remaining positive-name cases after its 1200-second cap. The shared native hook adapter removes six duplicated setup lines without changing guard callbacks; final-source native checks pass Round12 30/30 through pm, ownership matrix 142/142, import ownership 26/26, atomic 11/11, Round7 hooks 23/23, and packed minimum SDK 1/1. Lint, production/test typechecks, docstrings (308 declarations), and zero authored duplication pass. All 12 isolated behavioral/document reverts produce 18 expected failures; restoring the implementation passes 30/30. No threshold was reduced and no new test was quarantined." + "2026-11-12T16:26:47.215Z",codex-github230-1111,"Round 12 final real-data receipt: packed code 64f79ca was installed and byte-checked in five fresh public scratch clones. Exact scheduled issue-only preview/import/repeat plus every post-write workflow verification passed: context 7 creates/3 updates; linear 8/1; review 3/1; brief 5/2 plus one unchanged; rl 6/0. Every post-write gate passed and every curated item/history git diff was empty. Both real-data passes used a conservative 82 API-call debit and 1225 cumulative active seconds, below the 25-minute check budget; all clones are deleted. Live public GitHub GraphQL snapshots were pagination-checked and projected to the read-only transport cache; native installed CLI commands and SDK persistence were real. The subsequent hook-only extraction was revalidated by the native guard suites and minimum-SDK probe. Read all 10 scoped/outdated threads and 10 full comments/prompts plus the outside README instruction; outdated l6Wk and l6W2 were still present at the starting head and are fixed, while l6Ws already used a shared index and its 40-record control proves 120 parses. The inventory now records 91 groups, 273 sites and 236 helper calls. All seven prior per-repo reference-tag cleanup workarounds remain unnecessary. Deferred findings stay in issue 236; no review reply or resolution was made. Full release, coverage and exact pushed-head CI remain uncertified locally. Releasing only this claim and retaining in_progress for orchestrator verification." + "2026-11-12T16:36:00.026Z",codex-github230-1111,"Final additional local environment check: strict health refuses extension_host_pm_cli_version_skew because the package-pinned CLI is 2026.11.6 while the installed local pm-github extension host reports 2026.11.12. There are also six stale in-progress advisory items. Tracker authority, integrity and history checks are clean. This is not a passing local strict-health certification; no dependency pin or existing extension installation was changed. All five isolated scratch-clone strict-health checks passed. The final committed privacy audit passed 8902 Git objects with approved identities and no secret or host-path matches. Requested lint/typecheck/changelog and bounded test evidence remains passing; full release and exact-head CI certification remain pending. Releasing this bookkeeping claim and keeping the item in_progress." + "2026-11-12T16:52:34.574Z",steve-orchestrator,"Orchestrator, Greptile thread PRRT_kwDOTbN4pM62uYBV (projects.ts:631) on f82ecf5: buildProjectPullPlan built its issue lookup from the whole corpus, so a scoped --ids pull aborted when two unselected items claimed an unrelated issue. The lookup is now built from the selected items, still checked against the complete-corpus ownership index. Regression in test/round-twelve.test.ts: conflicts confined to unselected items no longer abort (failed before the fix with 'multiple existing owners'), and the must-refuse twin, an unselected peer claiming the selected item's own issue, still throws. round-twelve, projects, ownership-consistency and workspace-resolution 215/215; lint, typecheck, docstring and changelog:check pass. Orchestrator also repeated the scheduled sync end to end on f82ecf5 for pm-review (PR comment 6829934405): 3 creates and 1 update, all importer-created; repeat import a no-op; gate passes." + "2026-11-12T17:04:26.945Z",codex-github230-1111,"Round 13 starts from the orchestrator's 5f6749f on a clean worktree; origin/main remains 6ccb293 and is already an ancestor. Preserving the scoped Project pull fix. Read all three new threads, comments, proposed changes and AI prompts, with no remaining pages. Plan: classify pending-create recovery targets separately, report/defer them to import, and restrict lost-owner validation to selected refresh work; make Project fixture ownership explicit and add a valid reference-only command control. Published SDK audit finds acquireLockLease first exported in 2026.10.23, absent in 2026.10.22. Fleet lock pins all export it: review 2026.10.28, context/linear/brief 2026.11.4, rl 2026.11.3. Choosing the authorized minimum bump to 2026.10.23, deleting the unfenced fallback while retaining the development pin. Native SDK regressions, isolated reverts and the bounded requested gates follow." + "2026-11-12T17:21:41.331Z",codex-github230-1111,"Round 13 focused SDK-backed refresh regressions pass 6/6: pending creates are reported while unrelated items preview/apply, both with an SDK journal and with a restored producer plan alone; selected existing ownership loss refuses without item/history changes, while an unrelated scope proceeds. Recovery now uses validated original target IDs rather than entry.match alone, and deferred atomic-plan members are excluded from refresh writes. The Project fixture helper no longer infers ownership from tags; six issue-attachment call sites explicitly set github_url, with new draft/owner command controls. A type-only narrowing preserves the orchestrator's unselected-conflict regression while allowing test compilation." + "2026-11-12T17:31:50.235Z",codex-github230-1111,"Round 13 review evidence: read all three full threads and AI-agent prompts (PRRT_kwDOTbN4pM62uujJ, PRRT_kwDOTbN4pM62uui6, PRRT_kwDOTbN4pM62uujV), three comments, no truncated comment pages. origin/main remains 6ccb293 and is an ancestor; preserve orchestrator scoped-project change 5f6749f. Pending CREATE plans are deferred with transaction, item IDs and import resume argv; unrelated previews/applies proceed with or without an SDK journal. Existing targets that lose ownership refuse when selected, with item/history bytes unchanged. Persisted canonical target IDs also scope import recovery, preventing an unrelated refresh from replaying a journal-free CREATE. Project fixtures now set explicit ownership at their six issue-attachment call sites; valid mention-only tags plan drafts, explicit owners plan attachments. The new minimum is a deliberate breaking contract, disclosed in the generated changelog; the development SDK pin is unchanged." + "2026-11-12T17:35:00.873Z",codex-github230-1111,"Round 13 revert proof in a disposable package copy, based on 5f6749f: reverting only runRefresh gives 4/4 pending-create failures and 1/1 unselected-orphan failure; its selected-orphan refusal control passes. Reverting only resumePendingGithubImport gives 1/1 journal-free unrelated-refresh failure because the pending plan is replayed. Reverting only the Project fixture create helper gives 1/1 reference-only draft failure; explicit-owner control passes. Reverting the minimum declarations to 2026.10.17 gives 1/1 exact-floor contract failure, before consumer installation. This last proof is a declaration revert, not a function-only lease test: the old helper also uses leases when available. Total 8 expected failures, 2 controls pass. Restoring all fixes passes refresh 6/6, Project controls 2/2 and floor declarations 4/4; the real packed consumer separately passes at 2026.10.23, including live-lease success, released-lease lock_lost without callback, owned mutation and stale-revision refusal. Commands: timeout 1200 node --test --test-concurrency=1 --test-name-pattern \"^(pending import creates|retained existing ownership loss)\" test/refresh.test.ts; timeout 1200 node --test --test-concurrency=1 --test-name-pattern \"^project sync keeps a valid reference-only\" test/project-sync.test.ts; timeout 1200 npm run test:minimum-sdk." + "2026-11-12T17:36:20.170Z",codex-github230-1111,"Round 13 required test inventory: 7 files, 267 cases, 266 passes, one unchanged pm-cli#1656 recovered-compensation quarantine, zero failures or cancellations; each command capped at 20 minutes, at most two test workers. The final refresh rerun and final packed minimum-SDK rerun also pass after the immutable initial-selection lint correction. Exact commands/results:\ntimeout 1200 node --test --test-concurrency=1 test/refresh.test.ts => 40 passed, 1 skipped\ntimeout 1200 node --test --test-concurrency=1 test/import-ownership.test.ts => 26 passed, 0 skipped\ntimeout 1200 node --test --test-concurrency=1 test/ownership-consistency.test.ts => 142 passed, 0 skipped\ntimeout 1200 node --test --test-concurrency=1 test/round-twelve.test.ts => 32 passed, 0 skipped\ntimeout 1200 node --test --test-concurrency=1 test/project-sync.test.ts => 21 passed, 0 skipped\ntimeout 1200 npm run test:minimum-sdk => 1 passed, 0 skipped\ntimeout 1200 node --test --test-concurrency=1 test/compatibility-floor.test.ts => 4 passed, 0 skipped\nThe new refresh command was also run through pm test --only-last --run --progress: 6/6 passed. timeout 1200 npm run lint, timeout 1200 npm run typecheck, timeout 1200 npm run build:test and timeout 1200 npm run docstring pass; docstrings cover 32 files and 308 declarations. Inventory regenerated: 91 groups, 273 sites, 236 calls. Optional zero-threshold duplication check remains red only for the unchanged orchestrator-base projects.ts clone; recorded on pm-github-reference-identity-followup. CI certifies the full release/coverage suite. The claim will be released, without closure, before the final generated changelog check and commit." + "2026-11-12T17:36:47.097Z",codex-github230-1111,"Final round-13 safety evidence: timeout 1200 npm run privacy passes (8921 objects audited; approved identities, no secret or host-path matches). origin/main was refreshed again and remains 6ccb293, already merged as an ancestor. All three review threads were read in full; none were replied to, resolved or reacted to. The two pm items touched are pm-github-reference-identity and its existing follow-up, where the unchanged base duplication finding is recorded. Release only this author claim; keep the main item in_progress and the follow-up open. Changelog generation/check and explicit-path commit follow this final tracker write." + "2026-11-12T17:43:51.039Z",steve-orchestrator,"Orchestrator: 5f6749f duplicated the board ownership-index setup between buildProjectPullPlan and buildProjectImportPlan (one 5-line exact clone), which fails the duplication gate inside release:check. Both now call one documented helper, indexSelectedByIssue(selected, corpus, projectItems). Behaviour is unchanged: the import plan passes pmItems as both selection and corpus. Duplication gate: zero duplicated lines; typecheck, lint and docstring pass; round-twelve, projects, ownership-consistency, workspace-resolution and project-sync 236/236. Also audited the new SDK floor 2026.10.23 against every fleet checkout's locked pm: every pm-github sync caller locks 2026.11.5 or newer; only pm-observer (2026.10.18) is lower, and it has no sync workflow." + "2026-11-12T19:38:07.020Z",steve-orchestrator,"Round 14 (orchestrator, CodeRabbit review of f037df1): (1) withGithubOwnershipLock and withGithubImportOwnersUnderLease now clear a mutable active flag on their AsyncLocalStorage scope when it ends, so detached work that outlives the scope takes a fresh lease and a fresh ownership read instead of reusing a released lease or closed owner hooks; two regressions in test/import-ownership.test.ts fail on revert (lease probe false; corpusReads 1 instead of 2), and their nested-in-scope twins still reuse the lease and the outer read. (2) inspectCitationReferences lowercases each non-path view once and passes it to citationRepositoryBefore. (3) docs/identity-ownership-inventory.md states that matchImportedIssue reads legacyGithubLinkIdentities as raw first-link evidence and that the chosen item still passes the shared ownership checks; 101 line anchors remapped, all 100 named-call anchors verified. Gate: import-fidelity, import-ownership, ownership-consistency, refresh, round-twelve, cited-issue-adoption (343 tests, 0 fail), tsc, eslint, duplication (0 lines), changelog:check." + "2026-11-12T21:02:52.243Z",codex-github230-1111,"Merged origin/main release be28728 in d28bd75: retained version 2026.11.12 in all manifests/extension/lock roots, retained released changelog sections, regenerated using npm run changelog:full. Read both full review comments and AI-agent prompts for PRRT_kwDOTbN4pM62z9yn and PRRT_kwDOTbN4pM62z9yf (two comments, no more pages). Plan: whole-value explicit URL ownership, reference-only generic link extraction, retained dedicated legacy link-only marker compatibility, native SDK regressions and function reverts; mechanically regenerate and validate every inventory anchor against the merged code." + "2026-11-12T21:12:46.296Z",codex-github230-1111,"Whole-value regression focus passes 18/18 on real non-git SDK trackers and the registered github import command over the HTTP transport harness. Cases cover prose prefixes, trailing text, URL aliases, arrays/objects in explicit URL fields, exact-URL controls, four generic reference fields, explicit citation-adoption controls and dedicated legacy link-only compatibility. Existing fixtures that treated external_links as ownership are being converted to reference controls; explicit-owner freshness refusals stay intact. Added a reproducible syntax-tree inventory tool: node scripts/identity-inventory.ts --write; --check verifies all root and auxiliary anchors. The Project pull/import rows are derived at 640/706 and route through indexSelectedByIssue; a separate source-shift fixture verifies every Project anchor moves mechanically." + "2026-11-12T21:25:05.303Z",codex-github230-1111,"Function-only revert proof against merged baseline d28bd75, in a disposable package copy: reverting legacyGithubLinkIdentities produces 12/12 expected failures for explicit URL prose, trailing text, aliases, non-string URL fields and four generic reference fields. Reverting hasStructuredOwnership produces the expected citation-adoption failure while its explicit-owner control passes. Reverting matchImportedIssue produces four expected failures for unmarked/trailing description prose and a shared GitHub URL in provider evidence; both genuine marker/provider controls pass. Reverting only the inventory document produces 3/3 expected stale-anchor failures. Total: 20 expected failures and three must-allow controls passed. Restored code/document pass owner-field-values 22/22 and identity-inventory 3/3. Exact proof commands: timeout 1200 node --test --test-concurrency=1 --test-name-pattern \"^(Whole identity field refuses|Other link fields)\" test/owner-field-values.test.ts; same command with pattern \"^Reference prose does not bar\"; same command with pattern \"^(Native link declaration|Provider backlink ignores)\"; timeout 1200 node --test --test-concurrency=1 test/identity-inventory.test.ts. No SDK APIs were doubled. SDK/API version read: public PM CLI 2026.11.12; the development pin and fenced minimum remain unchanged. Self-review also excludes GitHub URLs from provider-backlink fallback; real provider backlinks remain authorized first links, with native command controls." + "2026-11-12T21:36:30.554Z",codex-github230-1111,"Completed class inventory of claim constructors: import-ownership.ts is the authoritative collector; the remaining independent GitHub claim construction was identity.ts graph(). It now takes GitHub claims only from githubOwnershipClaims and reads generic/prose/API links as mention edges through githubLinkReferences; valid opaque provider refs retain their namespace. The added graph-only function revert gives 5 expected failures and a passing native-marker control. Updated total across four function-only reverts plus the stale-document revert: 25 expected failures, four must-allow controls passed, restored native field tests 24/24 and inventory tests 3/3. The focused existing identity file also passes 20/20. The reference tests omit redundant tag hints when checking that link fields themselves supply a mention; original reference-tag regressions remain in the final gate. All production changes are now frozen for a complete final affected-file rerun." + "2026-11-12T21:49:12.182Z",codex-github230-1111,"Final affected-file gate: 22 test files/jobs, 549 cases, 547 passes, two unchanged pm-cli#1656 quarantines, no failures/cancellations. Each command capped at 20 minutes; at most two test workers. All production changes were frozen before this complete rerun. Exact commands/results:\ntimeout 1200 node --test --test-concurrency=1 test/cited-issue-adoption.test.ts => 46 passed, 0 skipped\ntimeout 1200 node --test --test-concurrency=1 test/github-url.test.ts => 36 passed, 0 skipped\ntimeout 1200 node --test --test-concurrency=1 test/identity-inventory.test.ts => 3 passed, 0 skipped\ntimeout 1200 node --test --test-concurrency=1 test/identity.test.ts => 20 passed, 0 skipped\ntimeout 1200 node --test --test-concurrency=1 test/import-fidelity.test.ts => 53 passed, 1 skipped\ntimeout 1200 node --test --test-concurrency=1 test/import-ownership.test.ts => 28 passed, 0 skipped\ntimeout 1200 node --test --test-concurrency=1 test/import-projection.test.ts => 6 passed, 0 skipped\ntimeout 1200 node --test --test-concurrency=1 test/import-regex.test.ts => 8 passed, 0 skipped\ntimeout 1200 node --test --test-concurrency=1 test/label-import-model.test.ts => 3 passed, 0 skipped\ntimeout 1200 node --test --test-concurrency=1 test/owner-field-values.test.ts => 24 passed, 0 skipped\ntimeout 1200 node --test --test-concurrency=1 test/ownership-consistency.test.ts => 133 passed, 0 skipped\ntimeout 1200 node --test --test-concurrency=1 test/reference-import-identity.test.ts => 17 passed, 0 skipped\ntimeout 1200 node --test --test-concurrency=1 test/refresh.test.ts => 40 passed, 1 skipped\ntimeout 1200 node --test --test-concurrency=1 test/repeat-import-noop.test.ts => 9 passed, 0 skipped\ntimeout 1200 node --test --test-concurrency=1 test/review-import-identity.test.ts => 30 passed, 0 skipped\ntimeout 1200 node --test --test-concurrency=1 test/review-round3-identity.test.ts => 29 passed, 0 skipped\ntimeout 1200 node --test --test-concurrency=1 test/round-twelve.test.ts => 32 passed, 0 skipped\ntimeout 1200 node --test --test-concurrency=1 test/scheduled-refusals.test.ts => 20 passed, 0 skipped\ntimeout 1200 node --test --test-concurrency=1 test/sdk-adoption.test.ts => 4 passed, 0 skipped\ntimeout 1200 node --test --test-concurrency=1 test/typed-sentinel-normalization.test.ts => 3 passed, 0 skipped\ntimeout 1200 node --test --test-concurrency=1 test/workspace-resolution.test.ts => 2 passed, 0 skipped\ntimeout 1200 npm run test:minimum-sdk => 1 passed, 0 skipped\nThe new native 24-case owner-field regression and three inventory checks also pass through pm test --only-last --run --progress. timeout 1200 npm run lint, timeout 1200 npm run typecheck, timeout 1200 npm run build:test, timeout 1200 npm run docstring and timeout 1200 npm run duplication pass (33 files/312 documented declarations; zero duplicated lines). Inventory generated and checked by node scripts/identity-inventory.ts --write / --check: 93 groups, 278 syntax sites plus five auxiliary anchors. A supplementary grep found two optional field reads; the parser now covers optional syntax too. The final stale-ledger proof was repeated after that tool change: 3/3 expected failures, restored 3/3 passes. Whole-field class total remains 25 expected revert failures, four passing must-allow controls, restored focus 27/27. Release own claim without closure, regenerate/check changelog after final PM writes, then audit privacy and commit/push. Full release/coverage certification remains CI work. Both review threads and AI-agent prompts read in full; no review replies, resolutions or reactions." + "2026-11-12T22:01:58.486Z",codex-github230-1111,"Final merge check caught that the original generator omitted main's released jscpd note as soon as this tagged pending PM item made the current window nonempty. Kept the native release-window convention and enabled pm-changelog's documented --dependency-updates flag; no custom history splicing, pending-window override or workflow change. This retains every main changelog line and mechanically adds canonical historical dependency entries. The real Git/SDK regression uses a dependency-shaped commit plus pending PM work and fails when only generateChangelog is reverted; restored generator/workspace tests pass 6/6. The new generator test command is linked; its raw CLI stdout contains disposable absolute paths, so it is run outside PM capture and only sanitized counts are recorded here. The already executed PM-linked owner/inventory commands pass 24/24 and 3/3 without capturing such paths. Claim re-acquired for this necessary merge repair; it will be released again after the final receipts." + "2026-11-12T22:08:25.901Z",codex-github230-1111,"Final merge-integration receipts: timeout 1200 node --test --test-concurrency=1 test/generate-changelog.test.ts passes 4/4; the affected generator/workspace combined run passes 6/6. Reverting only generateChangelog loses the released dependency note and fails its real Git/SDK regression; restored case passes. This adds one expected revert failure to the 25 identity/inventory outcomes (26 total). Unique gate inventory is now 23 files/jobs, 553 cases, 551 passes and two unchanged pm-cli#1656 quarantines. No remaining test failures/cancellations in that final inventory. An optional additional SDK install stalled before tracker initialization; its process tree was stopped, and the supported PM_GITHUB_TEST_NETWORK_ATTEMPT_TIMEOUT_MS=1000 retry passed in 73 seconds. Exact command: PM_GITHUB_TEST_NETWORK_ATTEMPT_TIMEOUT_MS=1000 timeout 1200 npm run test:minimum-sdk. The published SDK floor remains 2026.10.23; all production/operational source and shipped declaration checks plus native artifact controls pass. Lint, production/test typechecks, docstrings and zero-duplication checks pass after the generator adjustment. Line-by-line comparison confirms all main changelog content survives, with generated additions only; all version fields remain 2026.11.12. Final changelog regeneration/check and privacy audit follow this final tracker write. Release only this author claim, never close either PR item. CI certifies the full release/coverage gate; no review thread was replied to or resolved." + "2026-11-12T22:31:37.517Z",codex-github230-1111,"Corrected round-14 contract: retain whole-value github_url parsing and reference-only tags/generic link fields, while restoring leading native description declarations without a link-only marker or whole-description anchor. Preserve copied-provider-marker classification and direct-owner precedence. Add native SDK allow/refuse tests and compare exact issue-only atomic gated previews of five copied fleet trackers against origin/main, without applying or changing fleet files. Keep dependency-updates only with the existing released-note preservation regression. Check shared-runner hold before pushing." + "2026-11-12T22:45:59.647Z",codex-github230-1111,"Corrected compatibility: leading native GitHub issue/GH issue declarations own their first URL without a link-only flag or end-of-description requirement. Complete github_url parsing and reference-only generic links remain. Copied-provider classification keeps main precedence, with explicit provenance and verified importer identities protected. Eight focused native SDK cases pass, including all three reported legacy shapes, middle-description/prose-field/tag-only refusals, and copied-provider controls. The baseline pm-github gated preview refuses its pre-existing duplicate-tag corpus; collect its separate read-only planner diagnostic and label it explicitly rather than altering copied records." + "2026-11-12T22:55:56.283Z",codex-github230-1111,"Revert proof in a disposable package copy: reverting legacyGithubLinkIdentities to 017b62c fails all three native declaration compatibility cases; reverting matchImportedIssue to 017b62c fails all three curated classification cases. Retained refusals: reverting whole-value URL collection to 4de7d4c fails the prose github_url case; reverting the matcher to origin/main fails the tag-only case. Removing the start anchor (a guard mutation, not a parent commit revert) fails the middle-description case. Removing copied-provider precedence fails the direct-owner control while its legacy twin passes. Restored native compatibility file: 8/8 pass. Keeping --dependency-updates is necessary: reverting generateChangelog to the pre-flag implementation loses the released dependency note beside pending PM work (1 failure); restored regression passes. npm run lint, npm run typecheck and npm run docstring pass; minimum SDK 2026.10.23 consumer passes with the supported 1000ms network-family timer after stopping a stalled default-timer install." + "2026-11-12T23:05:12.393Z",codex-github230-1111,"Final affected-file gates: 24 files, 561 cases, 559 pass, two unchanged pm-cli#1656 quarantines, zero failures or cancellations after environment retries. Every command was capped at 20 minutes with at most two workers. The packed npm/Bun projection file passes 6/6 with the 1000ms Node network-family timer; the declared-minimum SDK consumer passes 1/1 with its supported timer. The linked native compatibility command also passes 8/8 through pm test --run --only-last --progress; project policy disables automatic result tracking, so this comment records the result. Lint, current typecheck, docstrings and mechanically checked identity inventory pass. Full release/coverage certification remains with CI." + "2026-11-12T23:05:13.631Z",codex-github230-1111,"Read-only real-data comparison: fresh public default-branch tracker copies, origin/main code be28728 versus the candidate, exact state-all atomic gated dry-run command with JSON, and complete public issue-only snapshots projected to read-only transport shapes. Counts create/update/unchanged: pm-github 8/16/0 versus 22/2/0; pm-review 0/4/0 versus 2/2/0; pm-rust 6/1/0 versus 6/1/0; pm-context 5/5/0 versus 7/3/0; pm-linear 3/6/0 versus 8/1/0. All candidate curated-content projections are unchanged. Four candidate gates pass. pm-github baseline refuses duplicate provenance references and the candidate refuses existing public source-body privacy findings; its counts are separately labeled production-planner diagnostics, not successful gated output. The literal no-extra-creates bound against released origin/main is not met because that release still binds reference-only items, the defect in issue 230. Supplemental native matcher comparison against the accepted pre-correction 4de7d4c shows creates 22,3,7,7,8 versus candidate 22,2,6,7,8, with no additional created issue in any repository. The three requested legacy examples resolve to their curated owner. Original tracker hashes were verified, no apply/commit/push was performed in copies, all scratch copies were deleted, and 25 GitHub API reads were used in total." + "2026-11-12T23:09:19.319Z",codex-github230-1111,"Final self-review also corrected the inventory generator's prose migration: it now replaces both the original collector wording and the already-generated ownership paragraph, so the published inventory actually states unmarked leading legacy declaration compatibility. The existing three-case inventory file passes; reverting only that prose migration causes its round-trip contract assertion to fail, and restoration passes. Final lint and current typecheck pass after this change. The staged privacy audit passed across 9033 Git objects with approved identities and no secret or host-path findings. Releasing the renewed self-review claim; both PM issues remain open for orchestration." + "2026-11-13T00:30:21.508Z",codex-github230-1111,"Round 16 starts from clean b29cfb1; origin/main remains be28728 and is already merged. Read all seven named review threads in full, including every AI-agent prompt. Inventory index-builder callers and their scope; retain the complete peer index while refusing only in-scope targets, warning on unrelated conflicts. Validate every declared/importer number as a consistent positive safe integer; preserve legacy ownership and classify its actual declaration for repair planning. Add the reference-only webhook control, correct existing-owner docs, test native SDK must-refuse/must-allow cases and behavioral reverts, compare five read-only tracker previews with round 15, run bounded affected gates and both typechecks, then release the claim and honor the shared-runner hold before pushing. No review replies or resolutions." + "2026-11-13T00:38:49.164Z",codex-github230-1111,"Round 16 implementation: selected sync/export items and board-matched project candidates validate against the complete peer index; unrelated contradictory or duplicate owners emit structured warnings with both IDs. Import matching, persisted-plan receipts, refresh issue scopes and search-hit association now use their actual target scope. Native declarations must agree with their URL number and importer suffixes must be positive safe integers. Marker-free declarations remain repair owners; reference tags remain mentions. Native SDK regression matrix and webhook byte-preservation control added; focused verification and behavioural reverts underway." + "2026-11-13T00:43:06.393Z",codex-github230-1111,"Gate follow-up: blockers coverage had an old expectation that clearing github_url also removed ownership despite retaining a valid leading GH issue declaration. The corrected legacy contract keeps that declaration authoritative. Added the declaration-only control and made the reference-only variant explicitly use middle-of-description prose; production blocker selection is unchanged. Current-sdk typecheck, test compilation, lint and docstring pass." + "2026-11-13T00:46:01.466Z",codex-github230-1111,"Read-only public-data replay of round-15 tracker commits and complete issue snapshots, native CLI/SDK, exact state-all atomic gated preview: pm-github 22 create / 2 update / 0 unchanged; pm-review 2/2/0; pm-rust 6/1/0; pm-context 7/3/0; pm-linear 8/1/0. All counts identical to round 15, no new creates and zero curated-field changes; item/history bytes unchanged and scratch copies deleted. No new GitHub API reads were needed. The existing pm-github source-body privacy refusal remains; its counts come from the read-only production planner, not a successful gate. The other four native gates passed." + "2026-11-13T01:03:06.802Z",codex-github230-1111,"The import-gate receipt fixture expected duplicate owners for issue 1 to refuse an import of issue 2, exactly the out-of-scope behavior this round removes. Corrected its must-refuse branch to target issue 1 and added a must-allow receipt for issue 2. The native SDK matrix independently covers the scoped import flow; the unchanged receipt checks still reject missing provenance and two entries targeting one item. No production adjustment was needed after this assertion." + "2026-11-13T01:11:09.739Z",codex-github230-1111,"The packed consumer test hit its existing subprocess timeout while the broader gate was busy; its rerun is queued with the public-registry transport timer. The source-level mixed-import projection cases passed. Final ownership behavior preserves the accepted precedence of valid explicit/importer provenance over copied description markers, while malformed legacy number declarations yield no declaration claim and an invalid_github_identity_number warning. Scope and positive safe-integer fixes remain the only production behavior changes in this round." + "2026-11-13T01:45:47.164Z",codex-github230-1111,"Round 16 behavioral revert proof (disposable source copies; native SDK trackers; fixed source restored afterward): 11 mutants produced 17 expected assertion failures. Function versions came from b29cfb1 for planSync/buildExportPlan, legacyGithubDescriptionIdentity, githubImporterIdentity, normalizeGithubClaim, graph, indexByImportIdentity, indexByProvenance and selectRefreshTargets. The board mutation re-expanded indexPmByIssue to all selected tracker items; the peer control disabled the competitor refusal; the webhook mutation restored tag-derived ownership. No mutation touched the package worktree.\noutbound-scope: timeout 1200 node --test --test-concurrency=1 \"--test-name-pattern=--ids allows\" test/scoped-ownership.test.ts => 0 pass, 2 expected failures.\nboard-scope: timeout 1200 node --test --test-concurrency=1 \"--test-name-pattern=^board-(import|pull) allows\" test/scoped-ownership.test.ts => 0 pass, 2 expected failures.\ndeclared-number: timeout 1200 node --test --test-concurrency=1 \"--test-name-pattern=^native declaration rejects\" test/scoped-ownership.test.ts => 2 pass, 2 expected failures.\nunsafe-suffix: timeout 1200 node --test --test-concurrency=1 \"--test-name-pattern=^verified importer suffix\" test/scoped-ownership.test.ts => 1 pass, 1 expected failures.\nexternal-number: timeout 1200 node --test --test-concurrency=1 \"--test-name-pattern=^external_ref number\" test/scoped-ownership.test.ts => 1 pass, 1 expected failures.\nrepair-declaration: timeout 1200 node --test --test-concurrency=1 \"--test-name-pattern=^matching marker-free declaration\" test/scoped-ownership.test.ts => 0 pass, 1 expected failures.\nselected-peer-control: timeout 1200 node --test --test-concurrency=1 \"--test-name-pattern=still refuses a peer\" test/scoped-ownership.test.ts => 0 pass, 4 expected failures.\nimport-target-scope: timeout 1200 node --test --test-concurrency=1 \"--test-name-pattern=^gated-import scopes\" test/scoped-ownership.test.ts => 0 pass, 1 expected failures.\nsearch-target-scope: timeout 1200 node --test --test-concurrency=1 \"--test-name-pattern=^search scopes\" test/scoped-ownership.test.ts => 0 pass, 1 expected failures.\nrefresh-target-scope: timeout 1200 node --test --test-concurrency=1 \"--test-name-pattern=^refresh scopes\" test/scoped-ownership.test.ts => 0 pass, 1 expected failures.\nwebhook-reference-only: timeout 1200 node --test --test-concurrency=1 \"--test-name-pattern=^old reopened events\" test/webhooks.test.ts => 0 pass, 1 expected failures.\nrestored-matrix: timeout 1200 node --test --test-concurrency=1 test/scoped-ownership.test.ts => 24 pass, 0 expected failures.\nrestored-webhook: timeout 1200 node --test --test-concurrency=1 \"--test-name-pattern=^old reopened events\" test/webhooks.test.ts => 1 pass, 0 expected failures." + "2026-11-13T01:45:48.078Z",codex-github230-1111,"Round 16 gate receipts: 52 complete affected test files: 1,232 passed, 2 pre-existing pm-cli#1656 SDK compensation quarantines, zero failures. Every command capped at 1,200 seconds with at most two simultaneous workers. The import-gate file reached its 20-minute cap after 60 passes, one stale receipt assertion and one cancellation; the corrected receipt assertion subsequently passed separately (1/1). The remaining workflow cases are for CI. A transient packed projection timeout passed on the full retry (6/6).\ntimeout 1200 node --test --test-concurrency=1 test/atomic.test.ts => 11 passed, 0 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/changelog-placement.test.ts => 5 passed, 0 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/cited-issue-adoption.test.ts => 46 passed, 0 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/blockers.test.ts => 35 passed, 0 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/comments-sync.test.ts => 14 passed, 0 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/complete-corpus.test.ts => 6 passed, 0 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/coverage-boundaries.test.ts => 8 passed, 0 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/coverage-edges.test.ts => 13 passed, 0 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/closeout.test.ts => 93 passed, 0 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/coverage-remainder.test.ts => 25 passed, 0 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/daily-sync-idempotency.test.ts => 3 passed, 0 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/discussions.test.ts => 16 passed, 0 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/dryrun-preview.test.ts => 2 passed, 0 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/durable-recovery.test.ts => 11 passed, 0 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/gate.test.ts => 50 passed, 0 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/github-url.test.ts => 36 passed, 0 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/handler-failures.test.ts => 38 passed, 0 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/http-boundary.test.ts => 39 passed, 0 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/identity-inventory.test.ts => 3 passed, 0 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/identity.test.ts => 20 passed, 0 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/import-errors.test.ts => 9 passed, 0 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/discussion-model.test.ts => 1 passed, 0 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/import-fidelity.test.ts => 53 passed, 1 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/import-lock.test.ts => 25 passed, 0 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/import-ownership.test.ts => 28 passed, 0 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/import-projection.test.ts => 6 passed, 0 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/import-regex.test.ts => 8 passed, 0 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/import-selection.test.ts => 26 passed, 0 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/issue-forms.test.ts => 11 passed, 0 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/label-import-model.test.ts => 3 passed, 0 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/legacy-description-compatibility.test.ts => 8 passed, 0 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/link-deps.test.ts => 28 passed, 0 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/own-property.test.ts => 5 passed, 0 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/owner-field-values.test.ts => 24 passed, 0 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/labels.test.ts => 13 passed, 0 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/project-sync.test.ts => 21 passed, 0 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/projects.test.ts => 39 passed, 0 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/reference-import-identity.test.ts => 17 passed, 0 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/refresh.test.ts => 40 passed, 1 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/repeat-import-noop.test.ts => 9 passed, 0 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/review-import-identity.test.ts => 30 passed, 0 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/review-round3-identity.test.ts => 29 passed, 0 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/round-seven.test.ts => 23 passed, 0 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/round-twelve.test.ts => 32 passed, 0 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/scheduled-refusals.test.ts => 20 passed, 0 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/scoped-ownership.test.ts => 24 passed, 0 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/sdk-adoption.test.ts => 4 passed, 0 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/smoke.test.ts => 72 passed, 0 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/typed-sentinel-normalization.test.ts => 3 passed, 0 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/webhooks.test.ts => 12 passed, 0 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/workspace-resolution.test.ts => 2 passed, 0 skipped.\ntimeout 1200 node --test --test-concurrency=1 test/ownership-consistency.test.ts => 133 passed, 0 skipped.\ntimeout 1200 node --test --test-concurrency=1 --test-name-pattern ^verifyImportIdempotency test/import-gate.test.ts => 1 passed.\nnpm run typecheck; npm run build:test; npm run lint; npm run docstring; node scripts/identity-inventory.ts --check; npm run privacy => passed. Docstrings: 33 modules, 314 declarations; inventory: 95 function groups, 295 sites, 5 auxiliary anchors. PM_GITHUB_TEST_NETWORK_ATTEMPT_TIMEOUT_MS=1000 timeout 1200 npm run test:minimum-sdk => 1 passed against the declared 2026.10.23 floor. Pinned pm test --run --only-last --progress executed the linked scoped-ownership command: 24/24 passed. This repo disables automatic test-result tracker writes, so this comment records the result. Full release/coverage/Bun gates remain with CI under the explicit round budget." + "2026-11-13T01:45:49.086Z",codex-github230-1111,"Round 16 final scope/data evidence: read all 7 requested threads, all 7 full comments and all 6 AI-agent prompts (Greptile has none). Selected IDs, board candidates and incoming targets validate against complete peers. Both IDs appear in off-scope collision warnings; a peer claiming an in-scope issue still refuses. Native declarations require their stated number to equal the URL number; unsafe importer suffixes and unsafe GitHub external_ref numbers establish no claim. Marker-free declarations remain repair owners, and reference-only webhook items remain byte-identical. The syntax-generated inventory documents every builder caller and its scope in docs/identity-ownership-inventory.md.\nRead-only public-data comparison replays the exact round-15 complete source snapshots against scratch clones pinned to the same public tracker commits; no apply and no new GitHub API reads. Round 15 -> round 16 create/update/unchanged: pm-github 22/2/0 -> 22/2/0; pm-review 2/2/0 -> 2/2/0; pm-rust 6/1/0 -> 6/1/0; pm-context 7/3/0 -> 7/3/0; pm-linear 8/1/0 -> 8/1/0. No extra creates and no curated-field changes. pm-github retains the known privacy preflight refusal on existing public issue bodies, so its counts are the read-only production planner diagnostic; the other four native gated previews pass. Native item/history hashes stayed identical and every scratch clone was deleted. No fleet checkout was written.\nOpen risks: CI must certify the capped import-gate remainder and full gates; two existing SDK compensation quarantines remain; the unchanged public-source privacy refusal remains visible. No new SDK gap or CLI defect was discovered. Claim will be released without closing; changelog regeneration and integrity checks follow the final tracker write." + "2026-11-13T01:47:56.629Z",codex-github230-1111,"Final read-only checks: npm run changelog:full, npm run changelog:check and npm run privacy passed. The history remains a byte-prefix extension of b29cfb1; origin/main has no earlier record for this branch-created item. SDK verifyHistoryEntries verified all 233 current events with zero errors. Extra pm health --check-only --skip-vectors --strict-exit --summary reports native integrity OK, but exits 1 for the existing extension_host_pm_cli_version_skew (pinned 2026.11.6 versus host 2026.11.12) and also reports six stale in-progress items. Those are not part of the passing targeted gates and no version, telemetry or unrelated tracker data was changed. Claim is released; item remains in_progress and open." + "2026-11-13T03:52:07.302Z",codex-github230-1111,"Round 17: reproduce the unchanged 100 percent coverage gate first, then cover all missing behavior and fix the shared declaration diagnostics, deferred webhook deliveries and curated projection fixture. Read all six requested review threads including their agent prompts; behavioral reverts and the same five public snapshot previews will certify the fixes. Keep all thresholds, append-only history and current ownership guards." + "2026-11-13T03:55:12.927Z",codex-github230-1111,"Round 17 review receipt: five requested threads, five complete comments and four AI-agent prompts read (CodeQL has no agent prompt). This corrects the initial plan count. Baseline bun run coverage is running before source edits, with the original thresholds, real SDK storage and two workers. The diagnostic currently duplicates native declaration parsing, and webhook reconciliation currently acknowledges a delivery even when refresh deferred that issue." + "2026-11-13T04:40:53.903Z",codex-github230-1111,"Round 17 implementation: one shared native-declaration parser now supplies ownership and numeric diagnostics; the local URL regex and quadratic trim are removed. Quoted, bracketed and punctuated valid declarations agree with ownership, while real number mismatches warn. Refresh reports all deferred source issue numbers, and webhook reconciliation leaves their receipts pending while independent deliveries progress. The projection fixture now has explicit provenance and asserts one existing item plus literal tag preservation. Native Node 22 regressions and inventory CLI cases pass 26/26; full SDK reads ensure the performance fixture contains all 20,000 closing parentheses. The initial unfiltered bun coverage attempt reached its 45-minute cap before producing an aggregate report; no local coverage percentage is inferred from that timeout. Main 9a72437 was merged as a5f0273, with the changelog regenerated by the package command." + "2026-11-13T04:40:54.889Z",codex-github230-1111,"Coverage-only simplification proof: native ownership snapshots use non-provisional claims. Their evidence flag implies at least one owner; owners are a subset of claims, and shared consistency refuses more than one claim. Therefore the later closeout evidence-without-one-owner check and its owners.length !== 1 disjunct cannot reject a native item. Refresh also receives at most one validated key, and every accepted collector produces a positive safe number, making the repeated cardinality and parsed-number checks unreachable. The raw github_number validation remains because malformed partial metadata can reach it. Real SDK controls verify unowned/same-repository closeout allowance, foreign/contradictory-owner refusal, invalid raw-number refusal, duplicate-record refusal, and valid refresh selection. No thresholds, source inventories or ignore rules changed." + "2026-11-13T04:53:56.786Z",codex-github230-1111,"Round 17 public read-only snapshot replay is complete: round-16 tracker commits and the same public source snapshots were replayed through the current packed build and native CLI/SDK. Create/update/unchanged counts remain pm-github 22/2/0, pm-review 2/2/0, pm-rust 6/1/0, pm-context 7/3/0 and pm-linear 8/1/0. No extra creates or curated-field changes. pm-github retains the existing public-source privacy preflight refusal, so its counts are the read-only production-planner diagnostic; the other four gated previews pass. No import was applied, source item/history hashes remain intact, zero new GitHub API reads were needed, and all scratch clones were deleted. Current typecheck, test-build typecheck, lint and the 33-module/315-declaration docstring gate pass; the minimum-SDK consumer remains queued behind the active two-worker coverage run." + "2026-11-13T05:31:33.539Z",codex-github230-1111,"Additional coverage reachability proof: legacyGithubIdentityConflict is a retained compatibility API whose body always returns undefined, as required when gh: tags are mentions. Its former positive branches in import, sync and refresh can never execute for any item. Their scans and conditional emissions were removed, while empty typed receipt fields preserve the public optional identity_conflicts type and runtime absence. The shared explicit-ownership and peer checks still run. Minimum-SDK packed consumer validation passes 1/1 against 2026.10.23. The updated native regressions, inventory CLI and entire projection file pass 33/33. Focused instrumentation includes 87 test files and all 33 authored source modules with unchanged four-way 100 percent thresholds; slow suites remain for separate validation. No ignore rule or persistent gate configuration was changed." + "2026-11-13T06:16:01.427Z",codex-github230-1111,"Coverage completion uses the native SDK and retained native V8 receipts at unchanged 100-percent thresholds for every authored module. Added behavioral outcomes for ownership index delegates and counters, missing dependency endpoints, malformed recovery targets, additive tags, outbound ownership drift, and exclusive recovery-plan publication. Eight targeted closeout revision/dependent-side-effect cases pass. The full baseline coverage command reached its 45-minute cap, so bounded source-complete test batches and the standard native receipt aggregator are used; no module exclusion or receipt URL filtering is introduced." + "2026-11-13T06:24:43.669Z",codex-github230-1111,"Unreachable-branch proof: ownership index builders and updates always install a states entry together with byId and retain every claimant map, so fallback states/maps in private cache replacement cannot be absent. verifyImportIdempotency first selects a claim in the requested set, then the shared consistency check requires exactly one consistent unique owner; its later different-key/duplicate-owner branches cannot run. The atomic catch already rethrows both ownership codes before cleanup, making its second identical owner check unreachable. Plain citation matches always set linkOnly or preserveContent in prepareGithubImport and leave through that earlier branch, so the later mirroring branch cannot increment adopted. Removed only these redundant paths; typed shared refusals, reference-tag freedom and counters remain." + "2026-11-13T06:27:13.998Z",codex-github230-1111,"Native SDK boundary observation: mutateItem permits a low-level callback to remove metadata.id, then listAllComplete correctly refuses that malformed document with list_source_incomplete. The disposable fixture now asserts that refusal and exercises the ownership cache missing-id boundary; no package behavior is weakened and no upstream SDK repair is included." + "2026-11-13T06:29:42.608Z",codex-github230-1111,"Behavioral revert proof against 58ae396 in a disposable tracked-source copy, with finally restoration: reverting warnGithubOwnershipScope alone makes 6 of 9 boundary/diagnostic/20000-parenthesis regressions fail (valid controls remain); reverting runRefresh alone makes both deferred-number regressions fail; reverting reconcileGithubWebhooks alone makes the pending-delivery regression fail; removing only the curated fixture github_url seed makes both additive-provider-link cases fail because an extra item is created. Restored implementations pass 42/42 round-seventeen tests plus 2/2 fixture cases, with no setup exceptions or cancellations. Commands use timeout 1200 node --test --test-concurrency=1 and name patterns shared declaration boundaries/shared declaration diagnostics/native declaration diagnostics finish, deferred webhook/legacy retained refresh plans, deferred webhook, and additive provider links respectively." + "2026-11-13T06:33:47.876Z",codex-github230-1111,"Read-only final-source public snapshot replay completed against the same round-16 tracker commits and source snapshots, with the exact scheduled --state all --atomic --gate --dry-run --json command and native installed package. Create/update/unchanged counts are identical: pm-github 22/2/0; pm-review 2/2/0; pm-rust 6/1/0; pm-context 7/3/0; pm-linear 8/1/0. No extra creates, no curated-field changes, all original tracker hashes unchanged, scratch clones removed, zero new GitHub issue API calls. pm-github preserves the already observed public-body privacy-preflight refusal; its identical counts are the read-only production-planner diagnostic, not a claimed passing gated command." + "2026-11-13T06:57:20.579Z",codex-github230-1111,"Standalone import-gate.test.ts reached its required 20-minute cap after 70 passing cases and no assertion failures; three tail cases remain for a separate bounded command. Full closeout-revision.test.ts passes 110/110. The final source-complete coverage capture uses the current build, Node 22 for parent and children, two test workers, fresh native V8 receipts and the unchanged runCoverageGate source inventory/threshold validation. Current source and test typechecks, lint and the 33-file/315-declaration docstring gate pass." + "2026-11-13T07:12:37.014Z",codex-github230-1111,"Final frozen-source Node 22 coverage test phase completed: 1879 tests, 1877 passed, two existing SDK-1656 compensation quarantines skipped, zero failures/cancellations, 1655776 ms. This includes all 51 round-seventeen regressions, five inventory tests, both curated fixture modes, and the shared-ownership freshness/cost matrix. Native all-source c8 aggregation is still running; no passing coverage claim yet. Explicit minimum-SDK and complete import-gate reruns start only after those two test workers exit." + "2026-11-13T07:46:07.592Z",codex-github230-1111,"Round 17 final gates: unchanged native coverage validator passes all 34 authored modules at exact 100/100/100/100, with complete per-module JSON and LCOV receipts and zero skipped source. The 1879-test source-complete batch passes 1877 with two existing SDK-1656 quarantines; complete import-gate.test.ts passes 73/73 in 1011816 ms; closeout-revision.test.ts passes 110/110; model files pass 4/4; final regressions pass 57/57; pinned pm test --run --match round-seventeen --progress passes 62/62 with tracking disabled. Current source/test typechecks, minimum SDK 2026.10.23 consumer, lint, docstrings (33 TypeScript files/315 declarations), inventory anchors and working-tree privacy pass; privacy audited 9091 objects. All native receipts use the same frozen source, without URL filtering, exclusions, ignore comments or lower thresholds. The unfiltered initial coverage attempt hit the 45-minute cap; bounded native batches plus the standard all-source receipt validator certify local coverage, and CI certifies the full release/Bun suite." + "2026-11-13T07:46:09.617Z",codex-github230-1111,"Final behavioural revert refresh: warnGithubOwnershipScope alone reverted to 58ae396 causes 6/9 selected tests to fail, runRefresh alone causes 2/2 deferred-number failures, reconcileGithubWebhooks alone causes 1/1 pending-delivery failure, and removing the explicit github_url fixture seed causes 2/2 additive-provider-link failures. Restoring the implementation passes all 57 regressions and both fixture cases; no setup errors or cancellations. Read all five requested threads in full, five comments and four AI-agent prompts (CodeQL supplied no prompt). No thread replies or resolutions. The accepted mention/ownership contract is unchanged; unrelated deferred work remains in issue 236. Native history verifies and remains a byte-prefix extension of the accepted head." + "2026-11-13T08:17:30.382Z",codex-github230-1111,"Round 18: rechecked clean accepted head 948ef40, claimed the existing item, read all three requested threads in full (three comments, two AI prompts), and re-read issue 230 through REST after the gh issue view Classic Projects failure. Main is already merged. Confirmed both races: add-issue validation must require a positive owner after async node resolution, and native comment writes must never reuse peers from a previous lock. Auditing every round-17 guard change with native malformed-data/concurrent-writer proofs; source coverage thresholds remain unchanged." + "2026-11-13T08:43:46.980Z",codex-github230-1111,"Round 18 native race proofs now pass: clearing an add-issue owner during node resolution produces no attach/status/tag write; ordinary SDK peer claims between comments refuse the second annotation. Matching-owner, current-board-tag, draft, and multi-comment controls remain allowed. Restored closeout evidence arity, import duplicate tracking, refresh contradiction/safe-number checks, atomic ownership rethrow, and the reachable importer-author citation adoption counter. Removed the obsolete cross-lock cache updater with the stale cache itself; every independently locked write builds its peer index afresh. Complete native corpus certification rejects malformed missing-id peers before entering a writable batch. Full coverage and behavioral reverts remain pending." + "2026-11-13T08:49:35.121Z",codex-github230-1111,"Round 18 focused native file: 29/29 pass. Lint, current-SDK typecheck, docstring (33 files, 314 documented declarations) and mechanically regenerated ownership inventory pass. Static syntax comparison confirms applyPushEntry body identical at 58ae396 and 948ef40, so the board race predates round 17. The guard-by-guard audit is in docs/round-eighteen-guard-audit.md. Frozen-source all-module coverage is running; no receipt or threshold exceptions were added. The N=40 cost control now requires 81 corpus reads, one preflight plus each separately locked bulk/finalizer write; the stale-cache two-read expectation was removed." + "2026-11-13T09:00:42.252Z",codex-github230-1111,"Coverage collection found two pre-existing low-level fixtures with uninitialized directories. Both now initialize native SDK trackers before their unchanged assertions; no ownership guard was relaxed. Production source hashes remain frozen. Strengthened the atomic staged-relink regression to assert zero transaction transitions, unchanged item/history bytes, the original owner-conflict code, and removal of the plan when no SDK journal exists. The CodeQL regression now uses parsed URL host equality and adversarial host/path twins containing the API hostname text." + "2026-11-13T09:18:36.722Z",codex-github230-1111,"Behavioral revert proof (disposable copies, credential-free, no coverage receipts): old applyPushEntry fails the URL-cleared race and the removed-board-tag case, with matching-owner/board-tag/draft controls allowed; old locked owner scope fails the ordinary-SDK peer-between-comments case and the ID-removal protection. Reverting closeout and refresh functions fails their native guard diagnostics; reverting verifyImportIdempotency fails the two-native-owner diagnostic; reverting runImport fails only the importer-author citation adoption count. Restoring substring API-host checking fails its adversarial-host control. Ten intended negative assertions fail across these mutants; all 31 round-18 tests and the parsed-host test pass restored. The old atomic early rethrow is an allowed earlier-refusal control; deleting the single retained unchecked rethrow fails the typed ownership assertion. No reviewed checkout files were mutated by the proofs." + "2026-11-13T09:27:20.632Z",codex-github230-1111,"Read-only public snapshot previews match the required baseline exactly: pm-github 22/2/0, pm-review 2/2/0, pm-rust 6/1/0, pm-context 7/3/0, pm-linear 8/1/0 (create/update/unchanged). All planned curated fields remain unchanged, no extra creates, all original tracker hashes unchanged, scratch clones deleted, zero new issue API reads. pm-github retains the known existing-public-body privacy-preflight refusal; its counts are the production-planner diagnostic, not a passing gated command. Scratch extension installation used each available exact project SDK lock version; a package without an SDK lock entry used this package's pinned SDK, with no stale-runtime override. Native closeout plus corrected fixture rerun: 135/135; direct import gate checks: 3/3; seeded models: 4/4; explicit minimum-SDK consumer/typecheck: 1/1 at 2026.10.23. Coverage's only remaining branch was the allowed matching-owner board status path; its native control passes and final native aggregation is running." + "2026-11-13T09:35:00.528Z",codex-github230-1111,"Final frozen-source native coverage passes the unchanged gate: 34 authored modules, exact 100/100/100/100 (lines/statements/functions/branches), no exclusions or ignore comments. The main capture executed 1914 tests; its two pre-existing uninitialized fixtures were corrected and the complete fixture file passed in the 135/135 closeout-plus-fixture rerun. Two existing SDK compensation quarantines remain skipped. The final linked pm test run passes 32/32 and reports tracking_disabled, so no run artifacts enter this tracker. Ten deliberate regression assertions fail on their behavioral mutants; all restored controls pass. Full import-gate validation is still running; no full-suite or CI completion claim." + "2026-11-13T09:55:19.299Z",codex-github230-1111,"Round 18 final validation complete. All 73 import-gate scenarios passed across capped batches: the initial command completed 64 with no failures before its 20-minute limit, and the nine unfinished cases passed in a 342075 ms name-pattern run. Final linked native regressions: 32/32; complete closeout/fixture rerun: 135/135; direct gate checks: 3/3; seeded model files: 4/4; minimum-SDK packed consumer and authored-source typecheck: 1/1 at 2026.10.23. Current typecheck, test compilation, lint, 314 documented declarations, and inventory anchors pass. Exact coverage is 15549/15549 lines and statements, 530/530 functions, 6725/6725 branches over all 34 authored modules, no skipped source or threshold changes. Ten intentional negative revert assertions fail and restored controls pass. All requested thread text was read (3 threads, 3 comments, 2 AI prompts); no review replies or resolutions were sent. Two existing SDK compensation quarantines and the known public-body privacy refusal remain explicit limits; CI certifies the full release matrix. Releasing the claim in progress, then regenerating changelog, committing paired tracker evidence with code, checking privacy and the runner hold, and pushing." + "2026-11-13T10:41:58.789Z",orchestrator,"Orchestrator, CodeRabbit PRRT_kwDOTbN4pM63DOtP on 6658b2f. The pull-plan board-consistency guard (projects.ts:659) used only ownership.states.get(pm), so a separately allocated copy of a selected item skipped the owner check, and a board issue o/r#8 could pull its status onto an item that owns o/r#7. It now derives claims with the same githubOwnershipClaims fallback as line 617. The regression in test/projects.test.ts refuses the copy-with-mismatch case (0/1 with the fix reverted) and still pulls when the copy is on its own issue. Inventory anchors were regenerated with scripts/identity-inventory.ts --write. Gates: projects, round-eighteen and import-projection pass 78/78; identity-inventory 5/5; lint and typecheck pass." + "2026-11-13T11:49:25.047Z",orchestrator,"Orchestrator, CodeRabbit PRRT_kwDOTbN4pM63ENyF on 9635d75: the push planner had the same identity-keyed lookup as the pull planner. Fixed as a class. The new ownershipStateOf(index, item) in import-ownership.ts reads states by identity and derives claims for a separately allocated copy. Every planner and selector that read ownership.states directly now uses it: projects push, pull and import, refresh selection, import-fidelity targets, index sync/import selection and the scope warning. The two sites with their own fallback parameters stay (assertGithubImportOwner with initialLink, closeout with the delivery repo). New push-planner regression for the copied item (refuses a mismatched board issue, plans set-status on its own issue); the inventory classifies the helper. Gates: 764 tests across the affected files, with only the five inventory tests failing before the classification and 5/5 after; projects 46/46 with the inventory; lint and typecheck pass." + "2026-11-13T14:17:53.647Z",orchestrator,"Orchestrator, round on 72d2131. (1) CodeRabbit Major PRRT_kwDOTbN4pM63IEZp: hasStructuredOwnership only needs to know whether owners exist, so it now uses the non-throwing githubOwnershipClaims(item).owners. An eligible citation candidate whose importer id (#41) and github_url (#42) disagree no longer aborts matching. The new cited-issue-adoption regression fails with 'Got unwanted exception' when the change is reverted. (2) Greptile P2 PRRT_kwDOTbN4pM63HwYp: the inventory scanner's helper set includes ownershipStateOf, and the regenerated inventory lists its call sites. (3) CodeRabbit Minor PRRT_kwDOTbN4pM63IEZc: docs/identity-collisions.md lists the marker-free legacy description declaration as a claim source. Gates: cited-issue-adoption, identity-inventory, import-fidelity, owner-field-values and projects pass; lint and typecheck pass." +files[67]: + - path: .github/workflows/pm-github-sync.yml + scope: project + - path: closeout.ts + scope: project + note: Per-item ambiguity and duplicate-owner refusal + - path: discussions.ts + scope: project + - path: durable-state.ts + scope: project + - path: github-url.ts + scope: project + note: Shared external reference normalization + - path: identity.ts + scope: project + note: Reuse normalized GitHub claim helper + - path: import-fidelity.ts + scope: project + note: Durable creator provenance replaces naming-based mirroring + - path: import-ownership.ts + scope: project + note: Minimum-SDK native document hash and locked revision fence + - path: import-tags.ts + scope: project + - path: index.ts + scope: project + note: Persist package creator and bind mirroring permission + - path: manifest.json + scope: project + - path: package-lock.json + scope: project + - path: package.json + scope: project + - path: projects.ts + scope: project + - path: refresh.ts + scope: project + - path: scripts/generate-changelog.ts + scope: project + - path: scripts/identity-inventory.ts + scope: project + - path: test/atomic.test.ts + scope: project + - path: test/blockers.test.ts + scope: project + - path: test/cited-issue-adoption.test.ts + scope: project + - path: test/compatibility-floor.test.ts + scope: project + - path: test/coverage-edges.test.ts + scope: project + - path: test/coverage-remainder.test.ts + scope: project + note: Native SDK policy refusal fixtures for guarded plain writes + - path: test/daily-sync-idempotency.test.ts + scope: project + - path: test/dryrun-preview.test.ts + scope: project + - path: test/durable-recovery.test.ts + scope: project + - path: test/durable-state.test.ts + scope: project + - path: test/fixtures/atomic-plan-crash.ts + scope: project + - path: test/fixtures/ownership-peer.ts + scope: project + - path: test/fixtures/refresh-crash.ts + scope: project + - path: test/generate-changelog.test.ts + scope: project + - path: test/github-url.test.ts + scope: project + - path: test/handler-failures.test.ts + scope: project + - path: test/helpers/import-mirror.ts + scope: project + note: Real importer creation provenance in mirror fixtures + - path: test/helpers/import-preview.ts + scope: project + - path: test/identity-inventory.test.ts + scope: project + - path: test/identity.test.ts + scope: project + - path: test/import-conflict-report.test.ts + scope: project + - path: test/import-errors.test.ts + scope: project + - path: test/import-fidelity.test.ts + scope: project + - path: test/import-gate.test.ts + scope: project + note: Real SDK dry-run fixtures and typed competing-owner refusal + - path: test/import-lock.test.ts + scope: project + - path: test/import-ownership.test.ts + scope: project + note: Native SDK revision and independent peer fixtures + - path: test/import-projection.test.ts + scope: project + - path: test/import-regex.test.ts + scope: project + - path: test/label-import-model.test.ts + scope: project + - path: test/legacy-description-compatibility.test.ts + scope: project + - path: test/link-deps.test.ts + scope: project + - path: test/minimum-sdk-consumer.test.ts + scope: project + note: Packed SDK floor typecheck and guarded write runtime probe + - path: test/owner-field-values.test.ts + scope: project + - path: test/ownership-consistency.test.ts + scope: project + note: Creator proof and all changed tracker record reference audits + - path: test/project-sync.test.ts + scope: project + - path: test/reference-import-identity.test.ts + scope: project + note: Contradictory linked references refuse without mutation + - path: test/refresh.test.ts + scope: project + - path: test/repeat-import-noop.test.ts + scope: project + - path: test/review-import-identity.test.ts + scope: project + note: Unified typed write refusal with readable legacy diagnostics + - path: test/review-round3-identity.test.ts + scope: project + note: Contradictory legacy claims refuse without duplicate import + - path: test/round-seven.test.ts + scope: project + note: Unified typed ownership refusal + - path: test/round-seventeen.test.ts + scope: project + - path: test/round-twelve.test.ts + scope: project + - path: test/scheduled-refusals.test.ts + scope: project + - path: test/sdk-adoption.test.ts + scope: project + - path: test/smoke.test.ts + scope: project + - path: test/typed-sentinel-normalization.test.ts + scope: project + - path: test/webhooks.test.ts + scope: project + - path: test/workspace-resolution.test.ts + scope: project + - path: webhooks.ts + scope: project +tests[88]: + - command: node --test --test-concurrency=2 test/reference-import-identity.test.ts test/import-conflict-report.test.ts + scope: project + timeout_seconds: 1200 + provenance: + author: codex-github230-1111 + created_at: "2026-11-11T10:32:07.555Z" + source_kind: local_mutation + source_ref: fix/reference-tags-are-not-import-identity + - command: npm run lint + scope: project + timeout_seconds: 1200 + provenance: + author: codex-github230-1111 + created_at: "2026-11-11T10:39:23.496Z" + source_kind: local_mutation + source_ref: fix/reference-tags-are-not-import-identity + - command: npm run typecheck + scope: project + timeout_seconds: 1200 + provenance: + author: codex-github230-1111 + created_at: "2026-11-11T10:39:25.518Z" + source_kind: local_mutation + source_ref: fix/reference-tags-are-not-import-identity + - command: npm test + scope: project + timeout_seconds: 2700 + provenance: + author: codex-github230-1111 + created_at: "2026-11-11T10:39:28.387Z" + source_kind: local_mutation + source_ref: fix/reference-tags-are-not-import-identity + - command: node --test --test-concurrency=1 test/review-import-identity.test.ts + scope: project + timeout_seconds: 1200 + provenance: + author: codex-github230-1111 + created_at: "2026-11-11T14:32:52.519Z" + source_kind: local_mutation + source_ref: fix/reference-tags-are-not-import-identity + - command: node --test --test-concurrency=1 test/atomic.test.ts + scope: project + timeout_seconds: 1200 + provenance: + author: codex-github230-1111 + created_at: "2026-11-11T14:46:32.891Z" + source_kind: local_mutation + source_ref: fix/reference-tags-are-not-import-identity + - command: node --test --test-concurrency=1 test/blockers.test.ts + scope: project + timeout_seconds: 1200 + provenance: + author: codex-github230-1111 + created_at: "2026-11-11T14:46:32.891Z" + source_kind: local_mutation + source_ref: fix/reference-tags-are-not-import-identity + - command: node --test --test-concurrency=1 test/changelog-placement.test.ts + scope: project + timeout_seconds: 1200 + provenance: + author: codex-github230-1111 + created_at: "2026-11-11T14:46:32.891Z" + source_kind: local_mutation + source_ref: fix/reference-tags-are-not-import-identity + - command: node --test --test-concurrency=1 test/cited-issue-adoption.test.ts + scope: project + timeout_seconds: 1200 + provenance: + author: codex-github230-1111 + created_at: "2026-11-11T14:46:32.891Z" + source_kind: local_mutation + source_ref: fix/reference-tags-are-not-import-identity + - command: node --test --test-concurrency=1 test/closeout-attribution.test.ts + scope: project + timeout_seconds: 1200 + provenance: + author: codex-github230-1111 + created_at: "2026-11-11T14:46:32.891Z" + source_kind: local_mutation + source_ref: fix/reference-tags-are-not-import-identity + - command: node --test --test-concurrency=1 test/closeout-revision.test.ts + scope: project + timeout_seconds: 1200 + provenance: + author: codex-github230-1111 + created_at: "2026-11-11T14:46:32.891Z" + source_kind: local_mutation + source_ref: fix/reference-tags-are-not-import-identity + - command: node --test --test-concurrency=1 test/closeout-selection.test.ts + scope: project + timeout_seconds: 1200 + provenance: + author: codex-github230-1111 + created_at: "2026-11-11T14:46:32.891Z" + source_kind: local_mutation + source_ref: fix/reference-tags-are-not-import-identity + - command: node --test --test-concurrency=1 test/closeout-workflow.test.ts + scope: project + timeout_seconds: 1200 + provenance: + author: codex-github230-1111 + created_at: "2026-11-11T14:46:32.891Z" + source_kind: local_mutation + source_ref: fix/reference-tags-are-not-import-identity + - command: node --test --test-concurrency=1 test/closeout.test.ts + scope: project + timeout_seconds: 1200 + provenance: + author: codex-github230-1111 + created_at: "2026-11-11T14:46:32.891Z" + source_kind: local_mutation + source_ref: fix/reference-tags-are-not-import-identity + - command: node --test --test-concurrency=1 test/comments-sync.test.ts + scope: project + timeout_seconds: 1200 + provenance: + author: codex-github230-1111 + created_at: "2026-11-11T14:46:32.891Z" + source_kind: local_mutation + source_ref: fix/reference-tags-are-not-import-identity + - command: node --test --test-concurrency=1 test/complete-corpus.test.ts + scope: project + timeout_seconds: 1200 + provenance: + author: codex-github230-1111 + created_at: "2026-11-11T14:46:32.891Z" + source_kind: local_mutation + source_ref: fix/reference-tags-are-not-import-identity + - command: node --test --test-concurrency=1 test/coverage-boundaries.test.ts + scope: project + timeout_seconds: 1200 + provenance: + author: codex-github230-1111 + created_at: "2026-11-11T14:46:32.891Z" + source_kind: local_mutation + source_ref: fix/reference-tags-are-not-import-identity + - command: node --test --test-concurrency=1 test/coverage-edges.test.ts + scope: project + timeout_seconds: 1200 + provenance: + author: codex-github230-1111 + created_at: "2026-11-11T14:46:32.891Z" + source_kind: local_mutation + source_ref: fix/reference-tags-are-not-import-identity + - command: node --test --test-concurrency=1 test/coverage-remainder.test.ts + scope: project + timeout_seconds: 1200 + provenance: + author: codex-github230-1111 + created_at: "2026-11-11T14:46:32.891Z" + source_kind: local_mutation + source_ref: fix/reference-tags-are-not-import-identity + - command: node --test --test-concurrency=1 test/daily-sync-alert.test.ts + scope: project + timeout_seconds: 1200 + provenance: + author: codex-github230-1111 + created_at: "2026-11-11T14:46:32.891Z" + source_kind: local_mutation + source_ref: fix/reference-tags-are-not-import-identity + - command: node --test --test-concurrency=1 test/daily-sync-idempotency.test.ts + scope: project + timeout_seconds: 1200 + provenance: + author: codex-github230-1111 + created_at: "2026-11-11T14:46:32.891Z" + source_kind: local_mutation + source_ref: fix/reference-tags-are-not-import-identity + - command: node --test --test-concurrency=1 test/daily-sync-privacy.test.ts + scope: project + timeout_seconds: 1200 + provenance: + author: codex-github230-1111 + created_at: "2026-11-11T14:46:32.891Z" + source_kind: local_mutation + source_ref: fix/reference-tags-are-not-import-identity + - command: node --test --test-concurrency=1 test/discussion-model.test.ts + scope: project + timeout_seconds: 1200 + provenance: + author: codex-github230-1111 + created_at: "2026-11-11T14:46:32.891Z" + source_kind: local_mutation + source_ref: fix/reference-tags-are-not-import-identity + - command: node --test --test-concurrency=1 test/discussions.test.ts + scope: project + timeout_seconds: 1200 + provenance: + author: codex-github230-1111 + created_at: "2026-11-11T14:46:32.891Z" + source_kind: local_mutation + source_ref: fix/reference-tags-are-not-import-identity + - command: node --test --test-concurrency=1 test/dryrun-preview.test.ts + scope: project + timeout_seconds: 1200 + provenance: + author: codex-github230-1111 + created_at: "2026-11-11T14:46:32.891Z" + source_kind: local_mutation + source_ref: fix/reference-tags-are-not-import-identity + - command: node --test --test-concurrency=1 test/gate.test.ts + scope: project + timeout_seconds: 1200 + provenance: + author: codex-github230-1111 + created_at: "2026-11-11T14:46:32.891Z" + source_kind: local_mutation + source_ref: fix/reference-tags-are-not-import-identity + - command: node --test --test-concurrency=1 test/handler-failures.test.ts + scope: project + timeout_seconds: 1200 + provenance: + author: codex-github230-1111 + created_at: "2026-11-11T14:46:32.891Z" + source_kind: local_mutation + source_ref: fix/reference-tags-are-not-import-identity + - command: node --test --test-concurrency=1 test/http-boundary.test.ts + scope: project + timeout_seconds: 1200 + provenance: + author: codex-github230-1111 + created_at: "2026-11-11T14:46:32.891Z" + source_kind: local_mutation + source_ref: fix/reference-tags-are-not-import-identity + - command: node --test --test-concurrency=1 test/identity.test.ts + scope: project + timeout_seconds: 1200 + provenance: + author: codex-github230-1111 + created_at: "2026-11-11T14:46:32.891Z" + source_kind: local_mutation + source_ref: fix/reference-tags-are-not-import-identity + - command: node --test --test-concurrency=1 test/import-conflict-report.test.ts + scope: project + timeout_seconds: 1200 + provenance: + author: codex-github230-1111 + created_at: "2026-11-11T14:46:32.891Z" + source_kind: local_mutation + source_ref: fix/reference-tags-are-not-import-identity + - command: node --test --test-concurrency=1 test/import-errors.test.ts + scope: project + timeout_seconds: 1200 + provenance: + author: codex-github230-1111 + created_at: "2026-11-11T14:46:32.891Z" + source_kind: local_mutation + source_ref: fix/reference-tags-are-not-import-identity + - command: node --test --test-concurrency=1 test/import-fidelity.test.ts + scope: project + timeout_seconds: 1200 + provenance: + author: codex-github230-1111 + created_at: "2026-11-11T14:46:32.891Z" + source_kind: local_mutation + source_ref: fix/reference-tags-are-not-import-identity + - command: node --test --test-concurrency=1 test/import-lock.test.ts + scope: project + timeout_seconds: 1200 + provenance: + author: codex-github230-1111 + created_at: "2026-11-11T14:46:32.891Z" + source_kind: local_mutation + source_ref: fix/reference-tags-are-not-import-identity + - command: node --test --test-concurrency=1 test/import-projection.test.ts + scope: project + timeout_seconds: 1200 + provenance: + author: codex-github230-1111 + created_at: "2026-11-11T14:46:32.891Z" + source_kind: local_mutation + source_ref: fix/reference-tags-are-not-import-identity + - command: node --test --test-concurrency=1 test/import-regex-sampling.test.ts + scope: project + timeout_seconds: 1200 + provenance: + author: codex-github230-1111 + created_at: "2026-11-11T14:46:32.891Z" + source_kind: local_mutation + source_ref: fix/reference-tags-are-not-import-identity + - command: node --test --test-concurrency=1 test/import-regex.test.ts + scope: project + timeout_seconds: 1200 + provenance: + author: codex-github230-1111 + created_at: "2026-11-11T14:46:32.891Z" + source_kind: local_mutation + source_ref: fix/reference-tags-are-not-import-identity + - command: node --test --test-concurrency=1 test/import-selection.test.ts + scope: project + timeout_seconds: 1200 + provenance: + author: codex-github230-1111 + created_at: "2026-11-11T14:46:32.891Z" + source_kind: local_mutation + source_ref: fix/reference-tags-are-not-import-identity + - command: node --test --test-concurrency=1 test/issue-forms.test.ts + scope: project + timeout_seconds: 1200 + provenance: + author: codex-github230-1111 + created_at: "2026-11-11T14:46:32.891Z" + source_kind: local_mutation + source_ref: fix/reference-tags-are-not-import-identity + - command: node --test --test-concurrency=1 test/label-import-model.test.ts + scope: project + timeout_seconds: 1200 + provenance: + author: codex-github230-1111 + created_at: "2026-11-11T14:46:32.891Z" + source_kind: local_mutation + source_ref: fix/reference-tags-are-not-import-identity + - command: node --test --test-concurrency=1 test/labels.test.ts + scope: project + timeout_seconds: 1200 + provenance: + author: codex-github230-1111 + created_at: "2026-11-11T14:46:32.891Z" + source_kind: local_mutation + source_ref: fix/reference-tags-are-not-import-identity + - command: node --test --test-concurrency=1 test/link-deps.test.ts + scope: project + timeout_seconds: 1200 + provenance: + author: codex-github230-1111 + created_at: "2026-11-11T14:46:32.891Z" + source_kind: local_mutation + source_ref: fix/reference-tags-are-not-import-identity + - command: node --test --test-concurrency=1 test/own-property.test.ts + scope: project + timeout_seconds: 1200 + provenance: + author: codex-github230-1111 + created_at: "2026-11-11T14:46:32.891Z" + source_kind: local_mutation + source_ref: fix/reference-tags-are-not-import-identity + - command: node --test --test-concurrency=1 test/project-sync.test.ts + scope: project + timeout_seconds: 1200 + provenance: + author: codex-github230-1111 + created_at: "2026-11-11T14:46:32.891Z" + source_kind: local_mutation + source_ref: fix/reference-tags-are-not-import-identity + - command: node --test --test-concurrency=1 test/reference-import-identity.test.ts + scope: project + timeout_seconds: 1200 + provenance: + author: codex-github230-1111 + created_at: "2026-11-11T14:46:32.891Z" + source_kind: local_mutation + source_ref: fix/reference-tags-are-not-import-identity + - command: node --test --test-concurrency=1 test/refresh.test.ts + scope: project + timeout_seconds: 1200 + provenance: + author: codex-github230-1111 + created_at: "2026-11-11T14:46:32.891Z" + source_kind: local_mutation + source_ref: fix/reference-tags-are-not-import-identity + - command: node --test --test-concurrency=1 test/repeat-import-noop.test.ts + scope: project + timeout_seconds: 1200 + provenance: + author: codex-github230-1111 + created_at: "2026-11-11T14:46:32.891Z" + source_kind: local_mutation + source_ref: fix/reference-tags-are-not-import-identity + - command: node --test --test-concurrency=1 test/scheduled-refusals.test.ts + scope: project + timeout_seconds: 1200 + provenance: + author: codex-github230-1111 + created_at: "2026-11-11T14:46:32.891Z" + source_kind: local_mutation + source_ref: fix/reference-tags-are-not-import-identity + - command: node --test --test-concurrency=1 test/sdk-adoption.test.ts + scope: project + timeout_seconds: 1200 + provenance: + author: codex-github230-1111 + created_at: "2026-11-11T14:46:32.891Z" + source_kind: local_mutation + source_ref: fix/reference-tags-are-not-import-identity + - command: node --test --test-concurrency=1 test/smoke.test.ts + scope: project + timeout_seconds: 1200 + provenance: + author: codex-github230-1111 + created_at: "2026-11-11T14:46:32.891Z" + source_kind: local_mutation + source_ref: fix/reference-tags-are-not-import-identity + - command: node --test --test-concurrency=1 test/typed-sentinel-normalization.test.ts + scope: project + timeout_seconds: 1200 + provenance: + author: codex-github230-1111 + created_at: "2026-11-11T14:46:32.891Z" + source_kind: local_mutation + source_ref: fix/reference-tags-are-not-import-identity + - command: node --test --test-concurrency=1 test/webhooks.test.ts + scope: project + timeout_seconds: 1200 + provenance: + author: codex-github230-1111 + created_at: "2026-11-11T14:46:32.891Z" + source_kind: local_mutation + source_ref: fix/reference-tags-are-not-import-identity + - command: node --test --test-concurrency=1 test/workspace-resolution.test.ts + scope: project + timeout_seconds: 1200 + provenance: + author: codex-github230-1111 + created_at: "2026-11-11T14:46:32.891Z" + source_kind: local_mutation + source_ref: fix/reference-tags-are-not-import-identity + - command: "node --test --test-concurrency=1 --test-name-pattern '^[a-fA-F]' test/import-gate.test.ts" + scope: project + timeout_seconds: 1200 + provenance: + author: codex-github230-1111 + created_at: "2026-11-11T14:46:32.891Z" + source_kind: local_mutation + source_ref: fix/reference-tags-are-not-import-identity + - command: "node --test --test-concurrency=1 --test-name-pattern '^[g-mG-M]' test/import-gate.test.ts" + scope: project + timeout_seconds: 1200 + provenance: + author: codex-github230-1111 + created_at: "2026-11-11T14:46:32.891Z" + source_kind: local_mutation + source_ref: fix/reference-tags-are-not-import-identity + - command: "node --test --test-concurrency=1 --test-name-pattern '^[n-sN-S]' test/import-gate.test.ts" + scope: project + timeout_seconds: 1200 + provenance: + author: codex-github230-1111 + created_at: "2026-11-11T14:46:32.891Z" + source_kind: local_mutation + source_ref: fix/reference-tags-are-not-import-identity + - command: "node --test --test-concurrency=1 --test-name-pattern '^[t-zT-Z]' test/import-gate.test.ts" + scope: project + timeout_seconds: 1200 + provenance: + author: codex-github230-1111 + created_at: "2026-11-11T14:46:32.891Z" + source_kind: local_mutation + source_ref: fix/reference-tags-are-not-import-identity + - command: node --test --test-concurrency=1 test/review-round3-identity.test.ts + scope: project + timeout_seconds: 1200 + provenance: + author: codex-github230-1111 + created_at: "2026-11-11T16:30:52.324Z" + source_kind: local_mutation + source_ref: fix/reference-tags-are-not-import-identity + - command: node --test --test-concurrency=1 test/projects.test.ts + scope: project + timeout_seconds: 1200 + provenance: + author: codex-github230-1111 + created_at: "2026-11-11T16:38:38.609Z" + source_kind: local_mutation + source_ref: fix/reference-tags-are-not-import-identity + - command: node --test --test-concurrency=1 test/atomic-finalization-quarantine.test.ts + scope: project + timeout_seconds: 1200 + provenance: + author: codex-github230-1111 + created_at: "2026-11-11T17:07:56.203Z" + source_kind: local_mutation + source_ref: fix/reference-tags-are-not-import-identity + - command: node --test --test-concurrency=1 test/durable-recovery.test.ts + scope: project + timeout_seconds: 1200 + provenance: + author: codex-github230-1111 + created_at: "2026-11-11T18:15:04.959Z" + source_kind: local_mutation + source_ref: fix/reference-tags-are-not-import-identity + - command: bun test --timeout 60000 test/durable-recovery.test.ts + scope: project + timeout_seconds: 1200 + provenance: + author: codex-github230-1111 + created_at: "2026-11-11T18:37:26.637Z" + source_kind: local_mutation + source_ref: fix/reference-tags-are-not-import-identity + - command: node --test --test-concurrency=1 test/github-url.test.ts + scope: project + timeout_seconds: 1200 + provenance: + author: codex-github230-1111 + created_at: "2026-11-11T20:17:14.785Z" + source_kind: local_mutation + source_ref: fix/reference-tags-are-not-import-identity + - command: bun test --timeout 60000 test/github-url.test.ts + scope: project + timeout_seconds: 1200 + provenance: + author: codex-github230-1111 + created_at: "2026-11-11T20:17:14.785Z" + source_kind: local_mutation + source_ref: fix/reference-tags-are-not-import-identity + - command: node --test --test-concurrency=1 test/import-ownership.test.ts + scope: project + timeout_seconds: 1200 + provenance: + author: codex-github230-1111 + created_at: "2026-11-11T22:21:10.719Z" + source_kind: local_mutation + source_ref: fix/reference-tags-are-not-import-identity + updated_by: codex-github230-1111 + updated_at: "2026-11-11T22:30:40.987Z" + - command: bun test --timeout 60000 test/import-ownership.test.ts + scope: project + timeout_seconds: 1200 + provenance: + author: codex-github230-1111 + created_at: "2026-11-11T22:21:12.037Z" + source_kind: local_mutation + source_ref: fix/reference-tags-are-not-import-identity + updated_by: codex-github230-1111 + updated_at: "2026-11-11T22:30:42.144Z" + - command: "node --test --test-concurrency=1 --test-name-pattern \"^(atomic import falls back|non-atomic import reports)\" test/coverage-remainder.test.ts" + scope: project + timeout_seconds: 1200 + provenance: + author: codex-github230-1111 + created_at: "2026-11-11T23:34:33.357Z" + source_kind: local_mutation + source_ref: fix/reference-tags-are-not-import-identity + - command: "node --test --test-concurrency=1 --test-name-pattern \"^runImport (closes a linked|reopens a linked)\" test/handler-failures.test.ts" + scope: project + timeout_seconds: 1200 + provenance: + author: codex-github230-1111 + created_at: "2026-11-11T23:34:34.355Z" + source_kind: local_mutation + source_ref: fix/reference-tags-are-not-import-identity + - command: "node --test --test-concurrency=1 --test-name-pattern \"^import (refuses before mutation|reconciles a canceled)\" test/import-errors.test.ts" + scope: project + timeout_seconds: 1200 + provenance: + author: codex-github230-1111 + created_at: "2026-11-11T23:34:35.286Z" + source_kind: local_mutation + source_ref: fix/reference-tags-are-not-import-identity + - command: "node --test --test-concurrency=1 --test-name-pattern \"^shared importer conversion\" test/label-import-model.test.ts" + scope: project + timeout_seconds: 1200 + provenance: + author: codex-github230-1111 + created_at: "2026-11-11T23:34:36.332Z" + source_kind: local_mutation + source_ref: fix/reference-tags-are-not-import-identity + - command: "node --test --test-concurrency=1 --test-name-pattern \"^packed npm\" test/import-projection.test.ts" + scope: project + timeout_seconds: 1200 + provenance: + author: codex-github230-1111 + created_at: "2026-11-11T23:34:37.462Z" + source_kind: local_mutation + source_ref: fix/reference-tags-are-not-import-identity + - command: node --test --test-concurrency=1 test/round-seven.test.ts + scope: project + timeout_seconds: 1200 + provenance: + author: codex-github230-1111 + created_at: "2026-11-12T01:42:31.586Z" + source_kind: local_mutation + source_ref: fix/reference-tags-are-not-import-identity + - command: bun test --timeout 60000 test/round-seven.test.ts + scope: project + timeout_seconds: 1200 + provenance: + author: codex-github230-1111 + created_at: "2026-11-12T01:42:33.411Z" + source_kind: local_mutation + source_ref: fix/reference-tags-are-not-import-identity + - command: node --test --test-concurrency=1 test/durable-state.test.ts + scope: project + timeout_seconds: 1200 + provenance: + author: codex-github230-1111 + created_at: "2026-11-12T01:42:35.397Z" + source_kind: local_mutation + source_ref: fix/reference-tags-are-not-import-identity + - command: timeout 1200 node --test --test-concurrency=1 test/ownership-consistency.test.ts + scope: project + provenance: + author: codex-github230-1111 + created_at: "2026-11-12T06:51:26.645Z" + source_kind: local_mutation + source_ref: fix/reference-tags-are-not-import-identity + - command: timeout 1200 bun test --timeout 60000 test/ownership-consistency.test.ts + scope: project + timeout_seconds: 1200 + provenance: + author: codex-github230-1111 + created_at: "2026-11-12T07:43:54.484Z" + source_kind: local_mutation + source_ref: fix/reference-tags-are-not-import-identity + - command: "timeout 1200 npm run test:minimum-sdk" + scope: project + timeout_seconds: 1200 + provenance: + author: codex-github230-1111 + created_at: "2026-11-12T09:05:21.342Z" + source_kind: local_mutation + source_ref: fix/reference-tags-are-not-import-identity + - command: node --test --test-concurrency=1 test/ownership-consistency.test.ts + scope: project + timeout_seconds: 1200 + provenance: + author: codex-github230-1111 + created_at: "2026-11-12T10:36:11.532Z" + source_kind: local_mutation + source_ref: fix/reference-tags-are-not-import-identity + - command: timeout 1200 node --test --test-concurrency=1 test/round-twelve.test.ts + scope: project + provenance: + author: codex-github230-1111 + created_at: "2026-11-12T15:03:14.468Z" + source_kind: local_mutation + source_ref: fix/reference-tags-are-not-import-identity + - command: "timeout 1200 node --test --test-concurrency=1 --test-name-pattern \"^(pending import creates|retained existing ownership loss)\" test/refresh.test.ts" + scope: project + provenance: + author: codex-github230-1111 + created_at: "2026-11-12T17:21:42.143Z" + source_kind: local_mutation + source_ref: fix/reference-tags-are-not-import-identity + - command: timeout 1200 node --test --test-concurrency=1 test/project-sync.test.ts + scope: project + provenance: + author: codex-github230-1111 + created_at: "2026-11-12T17:36:18.168Z" + source_kind: local_mutation + source_ref: fix/reference-tags-are-not-import-identity + - command: timeout 1200 node --test --test-concurrency=1 test/compatibility-floor.test.ts + scope: project + provenance: + author: codex-github230-1111 + created_at: "2026-11-12T17:36:19.170Z" + source_kind: local_mutation + source_ref: fix/reference-tags-are-not-import-identity + - command: timeout 1200 node --test --test-concurrency=1 test/owner-field-values.test.ts + scope: project + provenance: + author: codex-github230-1111 + created_at: "2026-11-12T21:16:40.603Z" + source_kind: local_mutation + source_ref: fix/reference-tags-are-not-import-identity + - command: timeout 1200 node --test --test-concurrency=1 test/identity-inventory.test.ts + scope: project + provenance: + author: codex-github230-1111 + created_at: "2026-11-12T21:47:50.360Z" + source_kind: local_mutation + source_ref: fix/reference-tags-are-not-import-identity + - command: timeout 1200 node --test --test-concurrency=1 test/generate-changelog.test.ts + scope: project + provenance: + author: codex-github230-1111 + created_at: "2026-11-12T22:01:57.492Z" + source_kind: local_mutation + source_ref: fix/reference-tags-are-not-import-identity + - command: timeout 1200 node --test --test-concurrency=1 test/legacy-description-compatibility.test.ts + scope: project + provenance: + author: codex-github230-1111 + created_at: "2026-11-12T22:47:32.281Z" + source_kind: local_mutation + source_ref: fix/reference-tags-are-not-import-identity + - command: timeout 1200 node --test --test-concurrency=1 test/scoped-ownership.test.ts + scope: project + provenance: + author: codex-github230-1111 + created_at: "2026-11-13T01:40:15.131Z" + source_kind: local_mutation + source_ref: fix/reference-tags-are-not-import-identity + - command: timeout 1200 node --test --test-concurrency=1 test/round-seventeen.test.ts test/identity-inventory.test.ts + scope: project + provenance: + author: codex-github230-1111 + created_at: "2026-11-13T07:15:47.016Z" + source_kind: local_mutation + source_ref: fix/reference-tags-are-not-import-identity + - command: timeout 1200 node --test --test-concurrency=2 test/round-eighteen.test.ts + scope: project + provenance: + author: codex-github230-1111 + created_at: "2026-11-13T08:46:54.648Z" + source_kind: local_mutation + source_ref: fix/reference-tags-are-not-import-identity +docs[7]: + - path: docs/cited-issue-adoption-validation.md + scope: project + - path: docs/identity-collisions.md + scope: project + - path: docs/identity-ownership-inventory.md + scope: project + - path: docs/sdk-minimum.md + scope: project + - path: "https://github.com/unbraind/pm-github/pull/233" + scope: project + note: Implementation and scoped review evidence + - path: "https://github.com/unbraind/pm-github/pull/233/checks" + scope: project + - path: README.md + scope: project + note: All owner claims must agree and remain unique; journal-aware cleanup +github_number: 230 +github_url: "https://github.com/unbraind/pm-github/issues/230" +body: "" diff --git a/.agents/pm/issues/pm-github-search-reference-followup.toon b/.agents/pm/issues/pm-github-search-reference-followup.toon new file mode 100644 index 00000000..be3d0cb7 --- /dev/null +++ b/.agents/pm/issues/pm-github-search-reference-followup.toon @@ -0,0 +1,15 @@ +id: pm-github-search-reference-followup +title: Map GitHub search hits through ownership instead of shared references +description: "Latest CodeRabbit outside-diff review of PR 233, indexByProvenance: import o/r#7, then add a curated Chore carrying gh:o/r#7. Tag-based indexing lets the later reference replace the imported owner in GitHub search results. Use authoritative import identities and retain shared mentions without replacing the owner. Deferred from round-6 importer write protection." +type: Issue +status: open +priority: 3 +tags: [] +created_at: "2026-11-11T22:21:15.600Z" +updated_at: "2026-11-12T08:53:57.802Z" +author: codex-github230-1111 +comments[1]{created_at,author,text}: + "2026-11-12T08:53:57.802Z",codex-github230-1111,"Round 9 tracker audit: moved the parent issue or PR reference from a gh tag to a docs link. This is reference hygiene only; deferred implementation remains out of scope and this item stays open." +docs[1]{path,scope,note}: + "https://github.com/unbraind/pm-github/pull/233",project,Context reference; this follow-up is not the referenced issue or PR +body: "" diff --git a/.github/workflows/pm-github-sync.yml b/.github/workflows/pm-github-sync.yml index 075eeb15..58f5ec16 100644 --- a/.github/workflows/pm-github-sync.yml +++ b/.github/workflows/pm-github-sync.yml @@ -24,7 +24,7 @@ on: required: false default: ${{ github.repository }} adopt-cited: - description: "Opt in to conservative citation adoption; tag new items gh:owner/repo#N instead" + description: "Opt in to conservative citation adoption; explicit github_url links take precedence" type: boolean required: false default: false @@ -461,6 +461,9 @@ jobs: report_workflow_failure "${RUNNER_TEMP}/pm-github-import.json" "${RUNNER_TEMP}/pm-github-import.err" exit 1 fi + # Keep the first plan's conflicts: a successful apply makes repeat plans unchanged. + jq -r '.plan[]? | select(.identity.title_conflict == true or .identity.conflict != null) | "- Issue #\(.issue): \([if .identity.title_conflict then "title differs (curated title preserved)" else empty end, if .identity.conflict then if .identity.conflict.rule == "preserve-provider-owner" then "state differs (provider state preserved)" else "state differs (GitHub state applied)" end else empty end] | join("; "))"' "${RUNNER_TEMP}/pm-github-import.json" > "${RUNNER_TEMP}/pm-github-import-conflicts.md" + jq -r '([.identity_conflicts[]? | select(.code == "ambiguous_legacy_github_identity")] | length) as $count | if $count > 0 then "- ambiguous_legacy_github_identity: \($count) item(s) with ambiguous number-only provenance preserved; set github_repo or github_url explicitly." else empty end' "${RUNNER_TEMP}/pm-github-import.json" >> "${RUNNER_TEMP}/pm-github-import-conflicts.md" # The gate step. A finding (or unreadable scan input) fails this step, so # the job never reaches the commit/push below with a leak in the change. @@ -658,6 +661,10 @@ jobs: printf '%s\n\n' "The import ran with \`--gate\`: the plan was verified for completeness and provenance idempotency before the write, and the proposed tracker change passed the fail-closed privacy gate before anything was committed. \`${DEFAULT_BRANCH}\` is never pushed directly." printf '%s\n\n' "Tracker items changed:" cat "${links_file}" + if [[ -s "${RUNNER_TEMP}/pm-github-import-conflicts.md" ]]; then + printf '\n%s\n\n' "Import conflicts for review:" + cat "${RUNNER_TEMP}/pm-github-import-conflicts.md" + fi } > "${body_file}" pr_number="$(own_open_pr)" if [[ -z "${pr_number}" ]]; then diff --git a/CHANGELOG.md b/CHANGELOG.md index 11e00dc1..6df7567c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,21 @@ # Changelog +## Unreleased + +### Fixed + +- Keep GitHub reference tags separate from import identity (BREAKING: PM CLI 2026.10.23 minimum) ([pm-github-reference-identity](https://github.com/unbraind/pm-github/blob/main/.agents/pm/issues/pm-github-reference-identity.toon)) + ## 2026.11.12 - 2026-11-12 ### Fixed - The gated sync holds a runner for up to two hours while it waits for caller CI it competes with for the same pool ([pm-github-fxhd](https://github.com/unbraind/pm-github/blob/main/.agents/pm/issues/pm-github-fxhd.toon)) +### Dependencies + +- Bump jscpd ([#232](https://github.com/unbraind/pm-github/pull/232)) + ## 2026.11.11 - 2026-11-11 ### Fixed @@ -17,6 +27,13 @@ - Reusable Dependabot merge workflow with complete commit provenance ([pm-github-dependabot-merge](https://github.com/unbraind/pm-github/blob/main/.agents/pm/features/pm-github-dependabot-merge.toon)) +### Dependencies + +- Bump the pm-toolchain group with 3 updates ([#227](https://github.com/unbraind/pm-github/pull/227)) +- Bump the codeql-action group across 1 directory with 2 updates ([#222](https://github.com/unbraind/pm-github/pull/222)) +- Bump entities from 8.1.0 to 9.0.0 ([#209](https://github.com/unbraind/pm-github/pull/209)) +- Bump the pm-toolchain group across 1 directory with 3 updates ([#194](https://github.com/unbraind/pm-github/pull/194)) + ## 2026.11.4 - 2026-11-04 ### Fixed @@ -31,12 +48,26 @@ - Bound coverage aggregation memory and compare default-heap CI behavior ([pm-github-vhi6](https://github.com/unbraind/pm-github/blob/main/.agents/pm/tasks/pm-github-vhi6.toon)) - Approve or amend the noreply identity on the normalize-release-tag-version branch commit ([pm-github-8mkc](https://github.com/unbraind/pm-github/blob/main/.agents/pm/tasks/pm-github-8mkc.toon)) +### Dependencies + +- Bump eslint ([#217](https://github.com/unbraind/pm-github/pull/217)) + +## 2026.11.2 - 2026-11-02 + +### Dependencies + +- Bump @types/node ([#208](https://github.com/unbraind/pm-github/pull/208)) + ## 2026.11.1 - 2026-11-01 ### Fixed - Scheduled closeout reads required checks from the branch summary instead of failing every fleet caller ([pm-github-6g0m](https://github.com/unbraind/pm-github/blob/main/.agents/pm/issues/pm-github-6g0m.toon)) +### Dependencies + +- Bump the codeql-action group across 1 directory with 2 updates ([#200](https://github.com/unbraind/pm-github/pull/200)) + ## 2026.10.30 - 2026-10-30 ### Fixed @@ -55,6 +86,11 @@ - Preview exact GitHub label migration impacts (BREAKING: PM CLI 2026.10.17 minimum) ([pm-github-2e38](https://github.com/unbraind/pm-github/blob/main/.agents/pm/features/pm-github-2e38.toon)) +### Dependencies + +- Bump the pm-toolchain group with 3 updates ([#191](https://github.com/unbraind/pm-github/pull/191)) +- Bump the pm-toolchain group across 1 directory with 3 updates ([#186](https://github.com/unbraind/pm-github/pull/186)) + ## 2026.10.26 - 2026-10-26 ### Added @@ -69,6 +105,10 @@ - Point the npm homepage at the package catalog and link the author ([pm-github-o846](https://github.com/unbraind/pm-github/blob/main/.agents/pm/chores/pm-github-o846.toon)) +### Dependencies + +- Bump jscpd ([#185](https://github.com/unbraind/pm-github/pull/185)) + ## 2026.10.25 - 2026-10-25 ### Fixed @@ -125,12 +165,24 @@ - Closed: Gate historical identities and host-path privacy across every Git object ([pm-github-zqad](https://github.com/unbraind/pm-github/blob/main/.agents/pm/issues/pm-github-zqad.toon)) - Closed: The daily release cannot build a changelog once the tracker outgrows the default output budget ([pm-github-ypi5](https://github.com/unbraind/pm-github/blob/main/.agents/pm/issues/pm-github-ypi5.toon)) +### Dependencies + +- Bump the pm-toolchain group across 1 directory with 3 updates ([#161](https://github.com/unbraind/pm-github/pull/161)) +- Bump the codeql-action group with 2 updates ([#170](https://github.com/unbraind/pm-github/pull/170)) +- Bump eslint ([#156](https://github.com/unbraind/pm-github/pull/156)) +- Bump the pm-toolchain group with 3 updates ([#155](https://github.com/unbraind/pm-github/pull/155)) + ## 2026.10.19 - 2026-10-19 ### Fixed - Refuse closeout when required-check metadata is unverified ([pm-github-f1ev](https://github.com/unbraind/pm-github/blob/main/.agents/pm/issues/pm-github-f1ev.toon)) +### Dependencies + +- Bump the pm-toolchain group with 3 updates ([#153](https://github.com/unbraind/pm-github/pull/153)) +- Bump the pm-toolchain group with 2 updates ([#135](https://github.com/unbraind/pm-github/pull/135)) + ## 2026.10.16 - 2026-10-16 ### Added @@ -142,6 +194,10 @@ - Refuse reusable sync runs outside the caller default branch ([pm-github-q4uz](https://github.com/unbraind/pm-github/blob/main/.agents/pm/issues/pm-github-q4uz.toon)) +### Dependencies + +- Bump the codeql-action group with 2 updates ([#136](https://github.com/unbraind/pm-github/pull/136)) + ## 2026.10.15 - 2026-10-15 ### Added @@ -153,6 +209,10 @@ - closeout cannot close items in trackers that require structured close fields ([pm-github-2o9k](https://github.com/unbraind/pm-github/blob/main/.agents/pm/issues/pm-github-2o9k.toon)) +### Dependencies + +- Bump @toon-format/toon ([#123](https://github.com/unbraind/pm-github/pull/123)) + ## 2026.10.14 - 2026-10-14 ### Added @@ -160,6 +220,19 @@ - Select GitHub issue imports and cap list volume ([pm-github-6srd](https://github.com/unbraind/pm-github/blob/main/.agents/pm/features/pm-github-6srd.toon)) - Revalidate blocked items against the live state of their GitHub blockers ([pm-github-fmrf](https://github.com/unbraind/pm-github/blob/main/.agents/pm/features/pm-github-fmrf.toon)) +## 2026.10.13 - 2026-10-13 + +### Dependencies + +- Bump the pm-toolchain group across 1 directory with 3 updates ([#117](https://github.com/unbraind/pm-github/pull/117)) +- Bump the dependencies group with 3 updates ([#118](https://github.com/unbraind/pm-github/pull/118)) + +## 2026.10.9 - 2026-10-09 + +### Dependencies + +- Bump pm-ops ([#113](https://github.com/unbraind/pm-github/pull/113)) + ## 2026.10.8 - 2026-10-08 ### Added @@ -179,12 +252,21 @@ - Closed: Root CI runs skip the read-only-directory comment-sync fixtures visibly instead of failing them ([pm-github-rap0](https://github.com/unbraind/pm-github/blob/main/.agents/pm/issues/pm-github-rap0.toon)) +### Dependencies + +- Bump @babel/eslint-parser ([#111](https://github.com/unbraind/pm-github/pull/111)) +- Bump @unbrained/pm-cli ([#110](https://github.com/unbraind/pm-github/pull/110)) + ## 2026.9.26 - 2026-09-26 ### Other - Certify pm CLI 2026.9.23 and adopt the guarded pm-ops merge-driver launcher ([pm-github-d2az](https://github.com/unbraind/pm-github/blob/main/.agents/pm/chores/pm-github-d2az.toon)) +### Dependencies + +- Bump the codeql-action group across 1 directory with 2 updates ([#96](https://github.com/unbraind/pm-github/pull/96)) + ## 2026.9.22 - 2026-09-22 ### Fixed @@ -195,6 +277,12 @@ - Certify pm CLI 2026.9.21 and install merge drivers through the canonical pm-ops launcher ([pm-github-7azo](https://github.com/unbraind/pm-github/blob/main/.agents/pm/tasks/pm-github-7azo.toon)) +## 2026.9.19 - 2026-09-19 + +### Dependencies + +- Bump the codeql-action group across 1 directory with 2 updates ([#90](https://github.com/unbraind/pm-github/pull/90)) + ## 2026.9.18 - 2026-09-18 ### Other @@ -207,6 +295,11 @@ - Certify pm CLI 2026.9.12 and the pm-ops 2026.9.11 auditor ([pm-github-u95z](https://github.com/unbraind/pm-github/blob/main/.agents/pm/chores/pm-github-u95z.toon)) +### Dependencies + +- Bump @types/node from 26.4.0 to 26.5.1 ([#84](https://github.com/unbraind/pm-github/pull/84)) +- Bump pm-changelog from 2026.9.6 to 2026.9.8 ([#85](https://github.com/unbraind/pm-github/pull/85)) + ## 2026.9.11 - 2026-09-11 ### Other @@ -239,6 +332,10 @@ - Pin the pm toolchain and assert the changelog-date flag by difference ([pm-github-c5i4](https://github.com/unbraind/pm-github/blob/main/.agents/pm/chores/pm-github-c5i4.toon)) +### Dependencies + +- Bump pm-ops from 2026.8.17 to 2026.8.31 ([#73](https://github.com/unbraind/pm-github/pull/73)) + ## 2026.9.4 - 2026-09-04 ### Security @@ -251,6 +348,11 @@ - Group codeql-action bumps into one pull request to end the split-PR deadlock ([pm-github-11qm](https://github.com/unbraind/pm-github/blob/main/.agents/pm/issues/pm-github-11qm.toon)) +### Dependencies + +- Bump @types/node from 26.3.0 to 26.4.0 ([#66](https://github.com/unbraind/pm-github/pull/66)) +- Bump the codeql-action group with 2 updates ([#67](https://github.com/unbraind/pm-github/pull/67)) + ## 2026.8.31 - 2026-08-31 ### Closed @@ -271,6 +373,11 @@ - Closed: Validate pm CLI development dependency bump ([pm-github-j8zh](https://github.com/unbraind/pm-github/blob/main/.agents/pm/issues/pm-github-j8zh.toon)) - Closed: The publish-attestation gate misses a publish routed through an unquoted shell variable ([pm-github-tko1](https://github.com/unbraind/pm-github/blob/main/.agents/pm/issues/pm-github-tko1.toon)) +### Dependencies + +- Bump @unbrained/pm-cli from 2026.8.20 to 2026.8.25 ([#58](https://github.com/unbraind/pm-github/pull/58)) +- Bump @types/node from 26.2.0 to 26.3.0 ([#59](https://github.com/unbraind/pm-github/pull/59)) + ## 2026.8.18 - 2026-08-18 ### Fixed @@ -289,6 +396,12 @@ - A github command can silently lose its preflight credential gate when the override scope drifts from the mutating command set ([pm-github-4ga9](https://github.com/unbraind/pm-github/blob/main/.agents/pm/issues/pm-github-4ga9.toon)) +### Dependencies + +- Bump pm-ops from 2026.8.8 to 2026.8.10 ([#37](https://github.com/unbraind/pm-github/pull/37)) +- Bump pm-changelog from 2026.8.6 to 2026.8.12 ([#39](https://github.com/unbraind/pm-github/pull/39)) +- Bump @types/node from 26.1.2 to 26.2.0 ([#38](https://github.com/unbraind/pm-github/pull/38)) + ## 2026.8.15 - 2026-08-15 ### Fixed @@ -326,6 +439,13 @@ - Closed: Fix terminal transitions for pm-cli 2026.8.3 close_reason enforcement ([pm-github-rwq9](https://github.com/unbraind/pm-github/blob/main/.agents/pm/issues/pm-github-rwq9.toon)) +## 2026.8.1 - 2026-08-01 + +### Dependencies + +- Bump @types/node from 26.1.1 to 26.1.2 ([#26](https://github.com/unbraind/pm-github/pull/26)) +- Bump pm-changelog from 2026.7.25 to 2026.7.28 ([#25](https://github.com/unbraind/pm-github/pull/25)) + ## 2026.7.30 - 2026-07-30 ### Other @@ -377,6 +497,11 @@ - Closed: Import dies with an unexplained pm list-all failed on any tracker over 1 MiB, and non-atomic --dry-run previews every issue as a create ([pm-github-tn92](https://github.com/unbraind/pm-github/blob/main/.agents/pm/issues/pm-github-tn92.toon)) - Closed: CHANGELOG omits the shipped --link-deps feature and mislabels 2026.7.23 work as Unreleased ([pm-github-px45](https://github.com/unbraind/pm-github/blob/main/.agents/pm/issues/pm-github-px45.toon)) +### Dependencies + +- Bump pm-changelog from 2026.7.18 to 2026.7.23 ([#10](https://github.com/unbraind/pm-github/pull/10)) +- Bump @unbrained/pm-cli from 2026.7.22 to 2026.7.24 ([#11](https://github.com/unbraind/pm-github/pull/11)) + ## 2026.7.23 - 2026-07-23 ### Added @@ -400,6 +525,14 @@ - Serialize marker-dedupe read-and-append across concurrent imports ([pm-github-503u](https://github.com/unbraind/pm-github/blob/main/.agents/pm/tasks/pm-github-503u.toon)) - Harden release bun-verify so registry-mirror lag cannot block the GitHub release ([pm-github-i6af](https://github.com/unbraind/pm-github/blob/main/.agents/pm/chores/pm-github-i6af.toon)) +## 2026.7.18 - 2026-07-18 + +### Dependencies + +- Bump @unbrained/pm-cli from 2026.7.12 to 2026.7.17 ([#3](https://github.com/unbraind/pm-github/pull/3)) +- Bump pm-changelog from 2026.7.12 to 2026.7.15 ([#2](https://github.com/unbraind/pm-github/pull/2)) +- Bump actions/setup-node from 6 to 7 ([#1](https://github.com/unbraind/pm-github/pull/1)) + ## 2026.7.17 - 2026-07-17 ### Added @@ -437,6 +570,10 @@ - Closed: Export --apply aborts the whole batch on the first failed item ([pm-github-q3lk](https://github.com/unbraind/pm-github/blob/main/.agents/pm/issues/pm-github-q3lk.toon)) - Closed: export --apply exits 0 even when every item fails to write ([pm-github-77wj](https://github.com/unbraind/pm-github/blob/main/.agents/pm/issues/pm-github-77wj.toon)) +### Dependencies + +- Bump pm-changelog from 2026.7.10 to 2026.7.12 ([#30](https://github.com/unbraind/pm-github/pull/30)) + ## 2026.7.11 - 2026-07-11 ### Other diff --git a/README.md b/README.md index 7add449a..c04385c5 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,22 @@ Import issues from any GitHub repo as pm items, export pm items back to GitHub ( --- +## Issue import identity + +A `gh:owner/repo#N` tag always means a mention. Any number of items may share it; it never establishes ownership or causes a conflict, even beside explicit provenance. Ownership comes from whole-value GitHub URL fields, leading legacy native declarations, repository plus issue number, GitHub `external_ref`, or a proven importer-created `-github--` ID. The ID's repository hash must match the importer's SHA-256 repository hash; existing importer author/metadata proves creation. Legacy items without `github_number` therefore still match in place. A reference may supply a repository candidate for checking that hash, but neither its repository nor number becomes a claim. Contradictory explicit/ID claims or competing owners refuse in-scope items with `github_import_owner_conflict`, naming the claims and remedy. Scope is selected IDs, board candidates or incoming import targets; unrelated owner conflicts warn with both item IDs. Declared issue numbers, URL numbers and verified importer suffixes must agree and be positive safe integers; invalid native declarations produce no claim and a warning. Set `github_url` explicitly to link a local item. Its value must be the whole issue/PR URL; prose before or after it never grants ownership. Generic `links`, `external_links`, `external_url` and `provenance` URLs remain references. Legacy descriptions beginning with `GitHub issue ` or `GH issue #N: ` retain ownership through their first URL, including trailing prose and punctuation; they need no `github_link_only` marker. Copied provider markers cannot displace an established direct GitHub owner. + +Full content mirroring requires durable creation provenance (`author: pm-github` or `github_import: true`); an ID pattern never proves who created an item. New imports record their package creator at creation, while records without creation proof retain curated content. Other linked items keep their title, description, type/folder, body, creation time and existing tags; imports merge GitHub provenance, lifecycle, and native comments/annotations (existing completion time stays). Body-mode comments on a curated item are stored as native annotations so its body stays intact. Source closures before local creation stay in `github_closed_at`. Cross-provider links retain their provider lifecycle. Title/state conflicts appear in the sync PR body for review. + +### Occupied import IDs + +A deterministic ID is a destination, never permission to overwrite its occupant, even when it also carries a matching reference tag. Every existing target must still name the incoming issue through authoritative provenance or a proven importer-created ID with a verified repository hash. An occupied ID owned by another issue or a curated/unowned item refuses with `github_import_owner_conflict`, naming the requested and current owners. Restore intended provenance only when the item belongs to this issue; otherwise use a different configured ID prefix or tracker, preserving the existing item and history. The importer does not allocate suffixes, keeping IDs and re-imports stable. The same check protects previews, plain/atomic writes, refresh, recovery, metadata, annotations and dependency updates; closeout checks every eligible item and refuses GitHub owners outside its delivery repository. Native writes recheck agreement and uniqueness against current metadata inside the item lock, including gated replay. First links using supported provider backlinks or explicit citation adoption retain their existing authorization rules, and never override an established GitHub owner. + +Dry runs read a real tracker and validate the same ownership destinations as apply; they never mutate item documents, history or transaction plans. All atomic imports, including ungated ones, require a passing privacy preflight before publishing a plan or starting the transaction. After an item transaction commits, native annotation failures are reported as `commentWriteFailures`, without retaining the committed plan for retry. A later normal sync repairs missing annotations. Gated callers still refuse incomplete side data before publication. Each changed or unchanged entry synchronizes annotations once. Ambiguous dependency owners remain unresolved and are reported as `ambiguous_github_dependency_owner`, rather than selecting the first claimant. + +Contradictory explicit identities remain readable for diagnosis, but ownership checks refuse their import or closure before writes, naming both targets and the correction. Case, trailing slashes, and equivalent issue/PR URL spellings normalize to one identity. Every supported explicit URL alias shares the parser's issue/PR kind, so refresh excludes PRs regardless of alias spelling. GitHub URLs use shared URL/path-segment parsing, including repositories named `issues` or `pull`, `www.`, trailing slashes, queries and fragments. Interrupted atomic imports require a saved passing privacy preflight and a fresh passing scan before replay; failed or missing verdicts refuse before writes. They replay their durably saved original entries, ordered mutations, targets, comments, and dependency inputs before fetching current GitHub data; a missing or invalid original plan refuses recovery. A subsequent normal sync can apply newer source data. Ownership preflight refusals remove a staged plan only when no SDK journal exists; journal-backed plans stay available for recovery. Every plan deletion flushes its directory through the shared durable-state platform policy. + +Status sync and export updates use explicit upstream ownership too. Shared reference tags never select an existing remote target. The deprecated `--allow-reference-tags` flag is accepted for compatibility and has no effect; competing explicit owners refuse before remote writes, including owners outside `--ids`. Exporting a reference-only item proposes a new issue. Import, replay, refresh, closeout, dependency links, outbound sync/export and issue associations all use the same ownership-claims check; conflicting or competing claims refuse before a write. Dependency ambiguity reports `ambiguous_github_dependency_owner` and writes no edge. See the [complete call-site inventory](docs/identity-ownership-inventory.md). + ## Installation ```bash @@ -355,10 +371,11 @@ See [SDK adoption evidence](docs/sdk-adoption-1025.md). ## Revalidate GitHub blockers -**Compatibility:** pm-github requires PM CLI 2026.10.17 or newer. Lossless table +**Compatibility:** pm-github requires PM CLI 2026.10.23 or newer. Lossless table decoding first appeared in 2026.10.9, while the transaction `match` guard used -by identity repair first appeared in 2026.10.17. The declared minimum must -support both. Older hosts refuse installation or activation through matching +by identity repair first appeared in 2026.10.17. Fenced ownership locks first +appeared in 2026.10.23. The declared minimum supports all three contracts. Older +hosts refuse installation or activation through matching peer dependency minimum and the top-level manifest floor. Development uses an independent exact, integrity-locked SDK pin. CI separately installs the exact minimum from npm, type-checks all production and operational SDK imports, then @@ -390,7 +407,7 @@ Verdicts are `cleared` (all references closed or merged), `partially_cleared` (some cleared), `still_blocked` (open blockers, none cleared), and `unresolvable` (no references or only unknown states). GitHub hides nonexistent and inaccessible private references alike: both can appear as `not_found`. Explicit permission -errors remain `error` and cannot clear a blocker. +errors remain `error` and cannot clear a blocker. Prose URLs accept matching Markdown emphasis, code spans, autolinks, links, parentheses and trailing punctuation. Unsupported or malformed HTTP URLs remain visible as `form: unparsed_url`, `state: unparsed`, without an invented issue number. Their path/query fragments never become shorter references, and they prevent a fully cleared verdict or cleared annotation. Reference tags do not exclude a blocker as self; only resolved import ownership does. Structured identity fields still require complete URLs. Exit **0** means nothing cleared, **4** means at least one item has a cleared or partially cleared blocker, **1** means a query/comment failure (takes precedence @@ -451,7 +468,7 @@ checks. ### Keeping a mirror current Use `refresh` when your tracker deliberately mirrors only a few issues from a busy -repository. It selects existing `gh:owner/repo#N` tags or explicit `github_url` +repository. It selects explicit import identities, including `github_url` links (also `github_repo` with `github_number`), fetches those numbers in GraphQL batches of at most 100, and **never creates items**. Only `/issues/N` URLs are refreshable links: the batch query reads issue nodes, so a `/pull/N` URL is not @@ -544,24 +561,20 @@ pm github import owner/repo --dry-run | `--with-comments` | boolean | Fetch issue comments and append them to the item body | | `--comments-mode ` | `body`\|`annotations`\|`both` | How fetched GitHub comments are persisted (default `body`). `annotations` syncs comments into the pm item's native comments collection via the SDK; `both` writes the body section AND native comments. `annotations`/`both` are idempotent on re-import (dedupe by GitHub comment id) | | `--atomic` | boolean | Commit every create, update, close, and reopen in one workspace-writer-locked, crash-resumable transaction (requires pm CLI/SDK >=2026.7.20). Normal failure compensation restores updated/closed items and deletes newly created items; an incomplete compensation is reported explicitly for retry or repair. | -| `--adopt-cited` | boolean | Explicitly opt in to conservative citation adoption (default: off). Tag new items `gh:owner/repo#N` instead. Default imports only report citation candidates. | +| `--adopt-cited` | boolean | Explicitly opt in to conservative citation adoption (default: off). Set `github_url` to link an owner explicitly. Default imports only report citation candidates. | | `--link-deps` | boolean | After import, map dependency references in issue **bodies** (`Blocked by #N`, `Depends on owner/repo#N`, `Blocks #N`) to pm dependency edges between the linked items. Idempotent, best-effort, and re-runnable; see [Dependency linking](#dependency-linking---link-deps) below. | -| `--gate` | boolean | Fail-closed privacy gate: verify plan completeness (every fetched issue accounted for exactly once) and provenance idempotency (every re-import lands on exactly one `gh:owner/repo#N`-tagged item) BEFORE the write, then scan the proposed tracker change with [`pm github gate`](#pm-github-gate) AFTER it. Any finding — or any unreadable scan input — exits non-zero so an automated pipeline can never commit or push the change. Embeds `gate` receipts in the result. | +| `--gate` | boolean | Fail-closed privacy gate: verify plan completeness (every fetched issue accounted for exactly once) and provenance idempotency (every re-import lands on exactly one explicit importer identity) BEFORE the write, then scan the proposed tracker change with [`pm github gate`](#pm-github-gate) AFTER it. Any finding — or any unreadable scan input — exits non-zero so an automated pipeline can never commit or push the change. Embeds `gate` receipts in the result. | | `--dry-run` | boolean | Preview without writing | | `--type ` | string | Override pm item type (default: Issue) | -Each imported item records GitHub provenance: the `gh:owner/repo#N` idempotency tag, a `github_author:` tag, and an enriched description (`author @ · state reason · created · updated `). GitHub issues closed as `not_planned` import as pm `canceled` instead of `closed`, preserving the difference between completed work and deliberately dropped work. The integration declares the `github_url`, `github_number`, `github_state`, `github_author`, `github_created_at`, and `github_updated_at` schema fields. +Each imported item records GitHub provenance: a `gh:owner/repo#N` reference tag, a `github_author:` tag, and an enriched description (`author @ · state reason · created · updated `). GitHub issues closed as `not_planned` import as pm `canceled` instead of `closed`, preserving the difference between completed work and deliberately dropped work. The integration declares the `github_url`, `github_number`, `github_state`, `github_author`, `github_created_at`, and `github_updated_at` schema fields. -The primary way to prevent duplicate imports is to tag an item -`gh:owner/repo#N` **when it is created**, for example -`pm create issue "Fix widget behavior" --tags "gh:acme/widgets#30"`. -For an existing item, use `pm update --add-tags "gh:acme/widgets#30"`. A prose URL is context, not an -ownership declaration. Existing tag/structured URL/provider matching takes -precedence and is unchanged. +To reuse a curated local item, set its `github_url` field to the exact issue URL. +A `gh:owner/repo#N` tag or prose URL remains a reference; it cannot claim import ownership. **Citation adoption is off by default.** Manual imports, the legacy alias and scheduled gated syncs retain normal create behavior when explicit ownership is -absent. They report read-only `citation_candidates` so an agent can tag the +absent. They report read-only `citation_candidates` so an agent can explicitly link the correct item, including when several candidates exist. No candidate is adopted or ambiguity-blocked by default. @@ -610,7 +623,7 @@ fields, including a closed item's completion state. With explicit opt-in, one eligible untagged candidate produces an `adopt` plan: its ID receives the provenance tag and `github_*` fields through normal upsert. Closed/canceled owners remain eligible. Two or more eligible owners refuse the -batch with typed `ambiguous_citation`, IDs and a remedy to tag the correct item. +batch with typed `ambiguous_citation`, IDs and a remedy to set `github_url` on the correct item. The default path never applies that fallback ambiguity refusal. Existing structured-identity collision checks still apply in both modes. @@ -624,11 +637,13 @@ still require a Git worktree for the proposed-change privacy scan. `--limit` bounds the matching list after PR, draft, and milestone filters. Pagination stops once one extra matching issue establishes truncation, so at most `n + 1` eligible issues are retained; GitHub pages may also contain excluded entries. Without a limit, existing list ordering is unchanged. -`--atomic` derives its durable transaction identity from the repository, complete rendered issue state, and exact ordered mutation plan (including target item ids), and derives each create id from the stable `owner/repo#number` external key rather than fetch position. Retrying the same response in a different order therefore resumes without duplicates; changed content, workspace prefixes, or resolved targets create a fresh compatible transaction. Native comment annotations run only after the item transaction commits and retain their cross-process deduplication lock. +`--atomic` derives its durable transaction identity from the repository, complete rendered issue state, and exact ordered mutation plan (including target item ids), and derives each create id from the stable `owner/repo#number` external key rather than fetch position. Every producer mode saves its complete plan before the SDK writes. An interrupted or partially compensated attempt replays that original plan and transaction ID before polling new input, even if content, workspace prefixes or resolved targets have changed. Once recovery finishes, a subsequent sync can plan new input. Released update-only refresh-plan mutation formats remain readable; recovery requires a saved passing privacy preflight and a fresh passing scan. Refresh also discovers retained plans after SDK commit before polling new source data. Plans with not-yet-created targets are reported in `deferred_imports`, with a resume command for the original import; refresh neither creates those items nor partially replays the transaction. Lost ownership on selected existing targets still refuses before replay, while scoped refreshes may continue on unrelated items. If delete compensation leaves an original create ID reserved by SDK history, recovery refuses with `github_import_plan_drift` and retains its plan: inspect `pm history`, explicitly restore the original live version with `pm restore`, then retry the same transaction. Native comment annotations run only after the item transaction commits and retain their cross-process deduplication lock. Package-owned plans, comment leases and webhook receipts/cursors publish complete fsynced state atomically; exclusive publication preserves an existing owner. A corrupt or unreadable original plan refuses with `github_import_plan_corrupt`: restore the verified original plan or reconcile its SDK journal while retaining native history, then retry. Unpublished staging files never become recovery identities. ### Native comment sync (`--comments-mode`) -By default (`--comments-mode body`, or `--with-comments`), fetched GitHub issue comments are flattened into the item body as blockquoted markdown under a `### GitHub comments (N)` heading — the historical behavior, byte-identical across releases. +For importer-created records, by default (`--comments-mode body`, or `--with-comments`), fetched GitHub issue comments are flattened into the item body as blockquoted markdown under a `### GitHub comments (N)` heading — the historical behavior, byte-identical across releases. + +Curated linked items always retain their body: fetched comments use native annotations in every comment mode. `--comments-mode annotations` instead syncs each GitHub comment into the pm item's **native comments collection** via the public SDK `comments()` primitive, so agents get structured, queryable comments (`pm comments `) instead of body-embedded text. Each stored comment carries a hidden marker with the GitHub comment id (``), so re-running import is **idempotent** — already-synced comments are skipped and never duplicated. `--comments-mode both` writes the body section *and* the native comments. @@ -643,7 +658,7 @@ pm github import owner/repo --with-comments --comments-mode annotations # same ### Dependency linking (`--link-deps`) -GitHub issue authors declare cross-issue dependencies in prose — `Blocked by #12`, `Depends on owner/repo#5`, `Blocks #9`. A flat import throws that structure away, leaving pm items with no blocker graph. `--link-deps` runs an opt-in second pass that parses those references from each issue's **body** and materializes them as pm dependency edges between the corresponding pm items, resolved through the same `gh:owner/repo#N` provenance tags the import writes. +GitHub issue authors declare cross-issue dependencies in prose — `Blocked by #12`, `Depends on owner/repo#5`, `Blocks #9`. A flat import throws that structure away, leaving pm items with no blocker graph. `--link-deps` runs an opt-in second pass that parses those references from each issue's **body** and materializes them as pm dependency edges between the corresponding pm items, resolved through the same explicit identities the import writes. The effect is context you can act on: `pm next` and `pm deps` surface the real ready/blocked ordering instead of an undifferentiated list. @@ -676,7 +691,7 @@ The import result gains `linkedDependencies`, `unresolvedDependencyRefs`, `order ### `pm github export` -**Safe by default.** Export previews the create/update plan and writes *nothing* unless you explicitly opt in with `--apply` **and** name a `--repo`. With a `--repo`, items already linked to an issue in that repo (via the `gh:owner/repo#N` provenance tag) are **updated** (upsert) instead of duplicated. +**Safe by default.** Export previews the create/update plan and writes *nothing* unless you explicitly opt in with `--apply` **and** name a `--repo`. With a `--repo`, items with explicit upstream identity in that repo (for example `github_url`) are **updated** (upsert) instead of duplicated. ```bash pm github export --repo owner/repo # DRY-RUN: print the create/update plan, write nothing @@ -690,6 +705,7 @@ pm github export --repo owner/repo --ids pm-12,pm-34 --apply | Flag | Type | Description | |---|---|---| | `--repo ` | string | Target repo; decides create-vs-update and is required for `--apply` | +| `--allow-reference-tags` | boolean | Deprecated compatibility flag; tags remain references | | `--ids ` | string | Scope export to specific pm item IDs (comma-separated); unknown IDs fail fast | | `--format ` | string | Dry-run output format (default: json) | | `--apply` | boolean | Perform real GitHub writes (alias: `--no-dry-run`, legacy `--push`). Requires a token + `--repo` | @@ -699,7 +715,7 @@ pm github export --repo owner/repo --ids pm-12,pm-34 --apply ### `pm github sync` -Push pm status changes back to GitHub: close/reopen the linked issue to match the pm item's status. Requires a token and explicit `--repo`. +Push pm status changes back to GitHub: close/reopen the explicitly linked issue to match the pm item's status. Requires a token and explicit `--repo`. Shared reference tags do not select a target; the deprecated `--allow-reference-tags` flag does not grant ownership. ```bash pm github sync --repo owner/repo --dry-run # preview the close/reopen plan @@ -766,7 +782,7 @@ pm github project sync unbraind/5 --push --apply --no-add-missing # only reconci ### `github` search provider -Registers a `github` search provider so `pm search ... --semantic` can reach GitHub. It asks GitHub which issues in the target repo match your query, then returns hits for the **pm items you've already imported** from those issues (matched by the `gh:owner/repo#N` provenance tag). +Registers a `github` search provider so `pm search ... --semantic` can reach GitHub. It asks GitHub which issues in the target repo match your query, then returns hits for the **pm items you've already imported** from those issues (matched by consistent explicit provenance or a verified importer ID; reference tags alone do not establish ownership). ```bash pm config project set ... # set search.provider = "github" in .agents/pm/settings.json @@ -822,11 +838,11 @@ Unreadable input, malformed or truncated diffs, binary input, invalid UTF-8, Git 1. **dry-run plan** — every fetched issue is prepared up front; 2. **completeness check** — every fetched issue is accounted for exactly once (planned or explicitly skipped), nothing outside the fetch appears, counts reconcile — all before any mutation; -3. **idempotency check** — no duplicate `gh:owner/repo#N` provenance tags in the existing corpus, every new entry born with its tag, no two plan entries resolving to the same item — so a second run reuses the existing item; +3. **idempotency check** — no duplicate explicit importer identities in the existing corpus, every new entry born with its tag, no two plan entries resolving to the same item — so a second run reuses the existing item; 4. **write** — the ordinary (atomic or per-item) import; 5. **gate** — the proposed tracker change is scanned; a finding exits non-zero, so the caller never commits or pushes. -Rendered values are also scanned in memory before mutation, so a known finding cannot be written to disk. After mutation, the importer verifies that every planned provenance tag exists exactly once, then scans the tracker diff. A completed SDK journal cannot substitute for missing item files. +Every field persisted in the recovery plan, including source bodies and dependency inputs, is scanned in memory before staging, so a known finding cannot be written to disk. After mutation, the importer verifies that each planned issue has exactly one consistent importer owner at the intended item, then scans the tracker diff. Any number of items may share its reference tag. A completed SDK journal cannot substitute for missing item files. The `gate` receipt reports the verdict, scanned file count, findings, and allowlisted count. Each finding includes `match_offset` (the starting character offset of the hashed match within the scanned field value, or the serialized added line for tracker text) and `excerpt`. Host-path excerpts show only the root initial, such as `/h***/…`; other matches use `[redacted]`. Human-readable workflow diagnostics include the same evidence. Full paths and secrets never appear. Recognizable bare-root JavaScript regex literals immediately used with `.test()` or `.exec()` are distinguished from paths; quoted host paths and slash-separated host paths remain refused, including inside code and regex source. Ambiguous quote contexts remain refused. Whole JSON-serialized fields retain the same distinction after decoding; TOON wrappers are recognized only in tracker text. @@ -1209,7 +1225,7 @@ Optional inputs: | Input | Default | Behavior | | --- | --- | --- | -| `adopt-cited` | `false` | Explicitly enable conservative normalized citation adoption for preview, import and repeat verification. Primary prevention is creating items with `gh:owner/repo#N` tags. Leave false to retain normal creates with read-only candidate hints. | +| `adopt-cited` | `false` | Explicitly enable conservative normalized citation adoption for preview, import and repeat verification. Use `github_url` to link an owner explicitly. Leave false to retain normal creates with read-only candidate hints. | | `npm-project-dir` | `.` | Relative npm project directory for `npm ci --ignore-scripts`, the CLI `node_modules/.bin` PATH entry, and `post-import-script`. Must have `package.json`, contain no `..` segments, and resolve inside the workspace (including symlinks); invalid paths fail before caching/install. The tracker and publication scope stay at the repository root. | | `post-import-script` | `changelog:full` | Caller npm script name invoked with `npm run --if-present` after import and before the final scan. Override for a different package script, or set `""` to disable. Missing scripts are skipped. Raw shell commands are not accepted. Generated `CHANGELOG.md` is staged with the tracker. Changing any file outside the documented scope fails the run, whether staged, unstaged, or untracked. | | `automation-app-client-id` | `""` | Optional GitHub App client ID, paired with the `automation_app_private_key` workflow-call secret. Enables eligible native PR runs through a fresh repo-scoped token; absent credentials retain GITHUB_TOKEN with a notice. | @@ -1235,7 +1251,7 @@ jobs: uses: unbraind/pm-github/.github/workflows/pm-github-sync.yml@RELEASE_COMMIT_SHA # vRELEASE_VERSION with: automation-app-client-id: ${{ vars.PM_GITHUB_APP_CLIENT_ID }} - adopt-cited: false # Tag new items gh:owner/repo#N; opt in only for deliberate adoption. + adopt-cited: false # Set github_url on intended owners; opt in only for deliberate adoption. secrets: automation_app_private_key: ${{ secrets.PM_GITHUB_APP_PRIVATE_KEY }} ``` @@ -1348,7 +1364,7 @@ github_link_only: false Release attribution uses the actual dates of locally available calendar tags, including annotated tag dates. A closed issue belongs to the first tag at or after its closure: before the first tag goes to that first release, between two tags goes to the second, and after the latest tag remains pending. Fetch the complete tag history before importing; the reusable sync workflow already does this. Regenerate with pm-changelog's `--respect-item-release` so a historical import stays in its declared release window. -Before creating an item, import checks the complete tracker for its provenance tag, GitHub URLs in provenance or link metadata, and Linear/Jira/ADO links referenced by the GitHub issue body. A unique existing external owner receives the GitHub tag and source metadata; its title, body, provider identity, creation/completion dates and release remain intact. A persisted `github_link_only` marker preserves that ownership on subsequent syncs. Free-text descriptions and bodies do not declare GitHub ownership. Ambiguous matches refuse the import before writes with `--gate` or `--atomic`. A plain multi-issue import may write earlier issues before rejecting a later ambiguous match. +Before creating an item, import checks the complete tracker for authoritative ownership: `github_repo` plus `github_number`, a whole-value `github_url` (including supported aliases), a GitHub `external_ref`, a verified importer-created ID, or a compatible leading native `GitHub issue` / `GH issue #N` declaration. Generic provenance, URLs in other link fields, body URLs and `gh:` reference tags alone never select an existing owner. Linear/Jira/ADO backlinks in the incoming issue body can authorize a separate first-link match; a unique external owner then receives source metadata while retaining its curated content and provider identity. A persisted `github_link_only` marker preserves the linked-owner treatment on later syncs. Contradictory or competing claims refuse in-scope targets before writes; conflicts between unrelated, unselected items produce warnings. All import modes resolve the complete identity plan before writing. With `--gate` and a nonempty filtered issue list, both preview and apply results include a `plan` with source dates, closure reason, release, target identity, link/create/update action and state-conflict decision. An empty filtered issue list returns without a `plan`, including with `--gate --dry-run`. GitHub wins lifecycle conflicts for GitHub-owned imports. A canceled item that GitHub later reports as completed uses the SDK's explicit repeat-closure transition with the source reason. A linked external owner keeps its local status, and the plan records `preserve-provider-owner`; `github_state` still records the source state. Atomic transaction identity includes the latest external edit or different lifecycle generation in history. Local state edits and repeated state cycles force reconciliation, while staging and compensation keep crash retries on the same journal. Finalization runs before the SDK commits; its compensation has the known SDK limitation below. It is idempotent and runs again after a recovered atomic journal, so an interrupted finalization can be repaired by retrying the same import before the gate permits a push. diff --git a/closeout.ts b/closeout.ts index 55dbda88..cdc5a8dd 100644 --- a/closeout.ts +++ b/closeout.ts @@ -1,6 +1,8 @@ +import { parseGithubUrl, parseGithubIssueUrl, githubUrlTokens } from "./github-url.ts"; import { readGithubWorkspace, githubStatusIsTerminal } from "./workspace.ts"; import { ownValue } from "./own-record.ts"; -import { normalizeImportValue } from "./import-fidelity.ts"; +import { GithubIdentityConflictError, normalizeImportValue } from "./import-fidelity.ts"; +import { buildGithubOwnershipIndex, type GithubOwnershipIndex, withGithubOwnershipLock, assertGithubOwnershipConsistency, githubOwnershipClaims, GithubImportOwnershipError } from "./import-ownership.ts"; /** Close delivered tracker items using merged pull requests and head-check evidence. */ import { close, hashDocument, locateItem, readLocatedItem, runWithActiveExtensions, setActiveExtensionHooks, listAllComplete, certifyCompleteListResult, getHistoryPath, readHistoryEntries, verifyHistoryEntries, PmCliError, EXIT_CODE, type ItemDocument } from "@unbrained/pm-cli/sdk"; @@ -24,9 +26,32 @@ export interface CloseoutRow { /** Stable decisions used by automation and compact skip counts. */ export type CloseoutCode = "already_terminal" | "other_repository" | "duplicate_selection" | "explicit_skip" + | "github_import_owner_conflict" | "github_identity_conflict" | "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"; +/** Explicit delivery may close local items, but never an issue owned by a different repository. */ +export function assertCloseoutGithubOwner(item: ItemDocument["metadata"], repo: string, peers: readonly ItemDocument["metadata"][] | GithubOwnershipIndex = []): void { + const ownership = Array.isArray(peers) ? buildGithubOwnershipIndex(peers, [repo]) : peers as GithubOwnershipIndex; + const state = ownership.states.get(item) ?? githubOwnershipClaims(item, false, false, [repo]); + const owners = state.owners; + if (state.evidence && owners.length !== 1) throw new GithubImportOwnershipError(item.id, `${repo.toLowerCase()} (delivery repository)`, state.claims); + assertGithubOwnershipConsistency(item, undefined, ownership); + if (owners.length && !owners[0]!.startsWith(`${repo.toLowerCase()}#`)) { + throw new GithubImportOwnershipError(item.id, `${repo.toLowerCase()} (delivery repository)`, owners); + } +} + +/** Convert malformed ownership into one eligible item's outcome, never a run-wide failure. */ +export function closeoutOwnershipConflict(item: ItemDocument["metadata"], repo: string, peers: readonly ItemDocument["metadata"][] | GithubOwnershipIndex): GithubImportOwnershipError | GithubIdentityConflictError | undefined { + try { assertCloseoutGithubOwner(item, repo, peers); } + catch (error: unknown) { + if (!(error instanceof GithubImportOwnershipError || error instanceof GithubIdentityConflictError)) throw error; + return error; + } + return undefined; +} + /** Which head-check policy was evaluated for one selected pull request. */ interface CheckGate { pr: number; @@ -163,8 +188,9 @@ function pullRequest(value: unknown, repo: string, number: number): PullRequest const base = record(raw.base); const baseRepo = record(base.repo); const head = raw.head === null || raw.head === undefined ? {} : record(raw.head); + const locator = parseGithubIssueUrl(raw.html_url); if (raw.number !== number || text(baseRepo.full_name).toLowerCase() !== repo.toLowerCase() - || text(raw.html_url).toLowerCase() !== `https://github.com/${repo}/pull/${number}`.toLowerCase() + || locator?.key !== `${repo.toLowerCase()}#${number}` || locator.kind !== "pull" || locator.api || locator.url.protocol !== "https:" || (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: typeof raw.merged === "boolean" ? raw.merged : null, @@ -373,27 +399,30 @@ export function declaresDelivery(prefix: string, suffix = ""): boolean { /** Extract only canonical tracker links, retaining foreign references solely for skip rows. */ export function extractCloseoutItems(body: string): { item: string; repo: string; declared: boolean }[] { - const links = body.matchAll(/https:\/\/github\.com\/([A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+)\/blob\/main\/\.agents\/pm\/((?:[^/\s?#<>)\]"']+\/)+)([^/\s?#<>)\]"']+)\.toon(?=$|[\s?#<>)\]"'])/g); + const links = githubUrlTokens(body); const seen = new Set(); const refs: { key: string; repo: string; item: string; declared: boolean }[] = []; return [...links].flatMap(match => { + const target = parseGithubUrl(match.raw); + if (!target || target.api || target.url.protocol !== "https:" || target.kind !== "blob" || target.value !== "main" || target.tail[0] !== ".agents" || target.tail[1] !== "pm" || target.tail.length < 4 || !target.tail.at(-1)!.endsWith(".toon")) return []; let components: string[]; - try { - components = [...match[2]!.slice(0, -1).split("/"), match[3]!].map(decodeURIComponent); - } catch { return []; } + try { components = target.tail.slice(2).map(decodeURIComponent); } + catch { return []; } if (components.some(component => /^\.{1,2}$|[/\\\0]/.test(component))) return []; - const item = components.at(-1)!; - const key = `${match[1]!.toLowerCase()}/${item}`; + const item = components.at(-1)!.slice(0, -5); + if (!item) return []; + const repository = `${target.owner}/${target.name}`; + const key = `${target.repo}/${item}`; const lineEnd = body.indexOf("\n", match.index); const declared = declaresDelivery(body.slice(body.lastIndexOf("\n", match.index) + 1, match.index), - body.slice(match.index + match[0].length, lineEnd < 0 ? body.length : lineEnd)); + body.slice(match.index + match.raw.length, lineEnd < 0 ? body.length : lineEnd)); if (seen.has(key)) { // A later "Closes " line still declares an item an earlier line only mentioned. if (declared) for (const entry of refs) if (entry.key === key) entry.declared = true; return []; } seen.add(key); - const entry = { key, repo: match[1]!, item, declared }; + const entry = { key, repo: repository, item, declared }; refs.push(entry); return [entry]; }).map(({ repo, item, declared }) => ({ repo, item, declared })); @@ -562,6 +591,7 @@ async function closeoutAttempt(pmPath: string, repo: string | undefined, options documents.set(id, document); items.set(id, document.metadata); } + const ownership = buildGithubOwnershipIndex([...items.values()], [repo]); await observer?.("snapshot"); // Containers close when their children are done, never because a pull request mentions them. const activeChildren = new Map(); @@ -607,6 +637,7 @@ async function closeoutAttempt(pmPath: string, repo: string | undefined, options } for (const ref of refs) { const role = deliveryRole(ref.declared, items.get(ref.item)?.tags.includes(`delivery-pr:${pr.html_url}`) === true, commitsReadable); + let ownershipConflict: GithubImportOwnershipError | GithubIdentityConflictError | undefined; let reason: string; let code: CloseoutCode; if (ref.repo.toLowerCase() !== repo.toLowerCase()) { code = "other_repository"; reason = "Link points to another repository."; } @@ -620,6 +651,7 @@ async function closeoutAttempt(pmPath: string, repo: string | undefined, options code = "not_delivery"; reason = "Mentioned, not delivered. A mention is not delivery. This fully inspected PR has no delivery claim and is context only."; } + else if ((ownershipConflict = closeoutOwnershipConflict(items.get(ref.item)!, repo, ownership))) { code = ownershipConflict.code; reason = ownershipConflict.message; } 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 { @@ -641,7 +673,7 @@ async function closeoutAttempt(pmPath: string, repo: string | undefined, options 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) && !githubStatusIsTerminal(items.get(ref.item)!.status, workspace.statuses) + && !skip.has(ref.item) && !ownershipConflict && !githubStatusIsTerminal(items.get(ref.item)!.status, workspace.statuses) && !isEpic(items.get(ref.item)!.type) && !activeChildren.has(ref.item)) { const group = candidates.get(ref.item) ?? []; group.push({ row, pr, declared: ref.declared, commitsReadable }); @@ -716,12 +748,13 @@ async function closeoutAttempt(pmPath: string, repo: string | undefined, options 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 () => { + await withGithubOwnershipLock(pmRoot, () => 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!; + assertCloseoutGithubOwner(before.metadata, repo, await context.sdk.list()); const id = before.metadata.id; if (hashDocument(before) !== snapshots.get(id)) { if (id !== entry.row.item) dependentConflicts.set(id, entry.row.pr); @@ -732,10 +765,14 @@ async function closeoutAttempt(pmPath: string, repo: string | undefined, options 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 GithubImportOwnershipError || error instanceof GithubIdentityConflictError) { + entry.row.action = "skip"; entry.row.code = error.code; entry.row.reason = error.message; + handled.add(entry.row.item); continue; + } if (error instanceof PmCliError && (error.code === "closeout_revision_changed" || (error.code === undefined && error.exitCode === EXIT_CODE.CONFLICT))) { if (retry) { diff --git a/discussions.ts b/discussions.ts index a3bf8b49..59ecb836 100644 --- a/discussions.ts +++ b/discussions.ts @@ -1,3 +1,4 @@ +import { parseGithubIssueUrl } from "./github-url.ts"; /** Explicit GitHub issue-comment decisions, persisted only through the host-injected SDK. */ import { createHash } from "node:crypto"; import { createPmCliExpectedError, getHistoryPath, readHistorySnapshot, normalizeItemId, previewItemMutations, locateItem, readLocatedItem } from "@unbrained/pm-cli/sdk"; @@ -49,7 +50,10 @@ async function evidence(input: DiscussionCaptureInput, selected: DiscussionCaptu if (!parsed.success) refuseDiscussion("source_unavailable", "Fetch a complete bounded comment with author, body and update timestamp; preview again."); const source = parsed.data; const url = `https://github.com/${input.repo}/issues/${input.issue}#issuecomment-${selected.comment}`; - if (source.id !== selected.comment || source.issue_url.toLowerCase() !== `https://api.github.com/repos/${input.repo}/issues/${input.issue}` || source.html_url.toLowerCase() !== url) { + const issueUrl = parseGithubIssueUrl(source.issue_url); + const commentUrl = parseGithubIssueUrl(source.html_url); + const key = `${input.repo.toLowerCase()}#${input.issue}`; + if (source.id !== selected.comment || issueUrl?.key !== key || !issueUrl.api || issueUrl.kind !== "issues" || commentUrl?.key !== key || commentUrl.api || commentUrl.kind !== "issues" || commentUrl.url.hash.toLowerCase() !== `#issuecomment-${selected.comment}`) { refuseDiscussion("invalid_selection", "Select a comment belonging to the exact repository and issue; cross-issue and PR passages are refused."); } if (selected.end <= selected.start || source.body.slice(selected.start, selected.end) !== selected.quote || selected.end > source.body.length) { diff --git a/docs/cited-issue-adoption-validation.md b/docs/cited-issue-adoption-validation.md index 158a9c78..7f7b6f0c 100644 --- a/docs/cited-issue-adoption-validation.md +++ b/docs/cited-issue-adoption-validation.md @@ -2,13 +2,12 @@ ## Final policy -The primary duplicate prevention is to create issue work items with -`gh:owner/repo#N` tags. Citation adoption is **off by default**. CLI +Use an explicit `github_url` to link a curated owner; `gh:owner/repo#N` tags are references. Citation adoption is **off by default**. CLI `--adopt-cited`, SDK `adoptCited: true`, or reusable sync input `adopt-cited: true` explicitly enables it. The workflow input defaults to false and is passed to preview, gated import and repeat verification consistently. -Default imports retain normal create behavior and established tag/structured +Default imports retain normal create behavior and established explicit-identity/structured URL/provider matching. They add read-only `citation_candidates` hints. Multiple eligible prose candidates never trigger fallback ambiguity refusal by default. Their local fields and status remain unchanged, including closed items. @@ -30,11 +29,11 @@ ownership. Any other normalized GitHub URL, repository, issue number or unattributable reference refuses adoption. PR URLs are other URLs, even with the same number. Bare hashes remain UNKNOWN because this item projection has no verified owning-repository context. Conservative refusals preserve normal -create behavior; arbitrary prose remains weaker evidence than an explicit tag. +create behavior; arbitrary prose remains weaker evidence than explicit importer provenance. One eligible owner with opt-in receives an `adopt` plan and normal upsert. Multiple eligible owners with opt-in refuse writes with typed -`ambiguous_citation`, IDs and a tagging remedy. Receipts expose `wouldAdopt` / +`ambiguous_citation`, IDs and an explicit-link remedy. Receipts expose `wouldAdopt` / `adopted`; gated idempotency counts one existing landing item. Status push, export and Projects matching retain their scope. diff --git a/docs/identity-collisions.md b/docs/identity-collisions.md index 39d941ce..c5d72f92 100644 --- a/docs/identity-collisions.md +++ b/docs/identity-collisions.md @@ -25,11 +25,14 @@ credentials and provider payloads are absent from the report. Contradictory GitHub claims on one item also count as collisions. The node includes the tracker's explicit `source_owner` as `native_owner` when present. -A claim comes from `external_ref`, an exact `gh:owner/repo#N` tag, `github_url`, -or a leading `GitHub issue URL` declaration. GitHub URL case and issue/pull URL +A claim comes from `external_ref`, durable importer provenance, `github_url` +(the whole value), an explicit `github_link_only` link declaration, or a legacy +native declaration at the start of the description (`GitHub issue ` or +`GH issue #N: `, whose stated number must match the URL) even without +`github_link_only`. Proven importer IDs also claim their hash-verified source issue. GitHub URL case and issue/pull URL forms normalize to one identity. Other external references remain opaque and case-sensitive: this command does not reinterpret Jira or Linear identifiers. -URLs elsewhere in prose are `mention` edges and never establish ownership. +Free-form `gh:owner/repo#N` tags and URLs elsewhere in prose are `mention` edges and never establish ownership. Tags never constrain explicit provenance or become claim edges. Accepted API URL mentions such as `https://api.github.com/repos/o/r/issues/7` group with `gh:o/r#7`; API URLs remain mentions, rather than structured ownership fields. A copied example containing a Linear label and an issue URL remains a local item with a mention. Structured Linear/Jira/ADO id/key/URL fields record real provider evidence. A provider item with `github_link_only: true` retains @@ -37,6 +40,8 @@ provider ownership; a provider item also claiming native GitHub ownership without that marker is `mixed` and requires review. These conservative labels are diagnostic evidence, not a provider migration or a duplicate-close policy. +The [ownership routing inventory](identity-ownership-inventory.md) lists every operational selector, raw collector and non-owning reference reader. The explorer uses the same claims function for diagnostics; it can display contradictions that operational selectors refuse. + ## Review and apply one repair Suppose `example-owner` declares `github_url` for issue 7, while diff --git a/docs/identity-ownership-inventory.md b/docs/identity-ownership-inventory.md new file mode 100644 index 00000000..011cc706 --- /dev/null +++ b/docs/identity-ownership-inventory.md @@ -0,0 +1,126 @@ +# GitHub ownership routing inventory + +Initial inventory: reviewed head `d9d9bc7`. Anchors are regenerated from the merged source with `node scripts/identity-inventory.ts --write` and checked by the same command with `--check`. + +Ownership conflicts refuse only selected IDs, board candidates or import targets. The complete peer index still detects any peer claiming an in-scope issue. Conflicts wholly outside scope produce warnings naming both items. + +Every operational item-to-issue/PR mapping goes through `githubOwnershipClaims` and the shared consistency/uniqueness checks. A provenance-bearing item is writable only when all its claims agree on one issue and no peer claims that issue. Tags never establish ownership or conflicts; the deprecated upstream flag does not change that. + +Inbound import, refresh, atomic replay/finalization, lifecycle metadata, annotations and dependency-source mutations retain the native locked write guard. Dependency endpoints are also revalidated there. Outbound sync/export re-read complete current metadata immediately before each HTTP write; package-built export plans retain their ownership context for direct execution. There is no outbound issue-comment writer: imported comments and blocker annotations write local SDK comments. Projects board/node identities, explicit source arguments and free-form blocker references are separate namespaces, not implied item ownership. + +Ownership work is indexed once per complete snapshot. The tracker ownership namespace is leased before SDK workspace/item locks and held through persistence; nested writes reuse the lease. Native batches read peers once before the journal and once for each separately locked SDK write, including comments and metadata finalizers. Nested operations reuse the live lease, never a previous write’s peer snapshot. Outbound writes read one fresh corpus and check only the current target; N writes over M items use at most O(N*M) claim work. The N=40 native and outbound tests count actual reads/parses without replacing SDK storage or using timing assertions. + +Scope: all authored root TypeScript modules. The inventory includes identity/ownership helper call sites, raw collectors/parsers, direct reads of `github_url`, `github_repo`, `github_number` and `external_ref`, and raw `gh:` reference regexes. Alias reads through `Object.entries` are covered by the raw-collector rows. Schema/type declarations, field emitters and tag merges serialize data rather than choose a target; `buildImportFidelity`, import mutation builders and project provenance emitters are in that category. Owner claims come from whole-value explicit identity fields, external_ref, or a proven importer ID. A leading native GitHub issue or GH issue/PR declaration retains legacy ownership without a link-only marker or whole-description match; direct provenance still protects against copied provider markers. Generic link fields and their prose produce references only. Operational selectors consume the shared ownership index; provider backlinks and explicit opt-in citation adoption keep their separate first-link authorization. + +96 function groups; 306 helper-call/field/parser sites. All sites are listed below with the path they serve or the reason they do not select a GitHub target. + +| File / enclosing function | Call sites and field accesses | Path / ownership treatment | +| --- | --- | --- | +| `closeout.ts` / `assertCloseoutGithubOwner` | `closeout.ts:35` buildGithubOwnershipIndex(); `closeout.ts:36` githubOwnershipClaims(); `closeout.ts:39` assertGithubOwnershipConsistency() | Closeout checks eligible item claims against peers and rechecks inside the native close lock; contradictions become per-item refusal outcomes. Skipped/context-only mentions are not ownership-checked. Proven importer-ID claims are resolved only by the shared hash-verifying helper; suffixes never select a target independently. | +| `closeout.ts` / `closeoutOwnershipConflict` | `closeout.ts:47` assertCloseoutGithubOwner() | Closeout checks eligible item claims against peers and rechecks inside the native close lock; contradictions become per-item refusal outcomes. Skipped/context-only mentions are not ownership-checked. Proven importer-ID claims are resolved only by the shared hash-verifying helper; suffixes never select a target independently. | +| `closeout.ts` / `pullRequest` | `closeout.ts:191` parseGithubIssueUrl() | Parse explicit PR selection/delivery attribution, not tracker ownership; eligible delivery items subsequently pass shared consistency checks. | +| `closeout.ts` / `extractCloseoutItems` | `closeout.ts:406` parseGithubUrl() | Parse explicit PR selection/delivery attribution, not tracker ownership; eligible delivery items subsequently pass shared consistency checks. | +| `closeout.ts` / `closeoutAttempt` | `closeout.ts:594` buildGithubOwnershipIndex(); `closeout.ts:654` closeoutOwnershipConflict(); `closeout.ts:751` withGithubOwnershipLock(); `closeout.ts:757` assertCloseoutGithubOwner() | Closeout checks eligible item claims against peers and rechecks inside the native close lock; contradictions become per-item refusal outcomes. Skipped/context-only mentions are not ownership-checked. Proven importer-ID claims are resolved only by the shared hash-verifying helper; suffixes never select a target independently. | +| `discussions.ts` / `evidence` | `discussions.ts:53` parseGithubIssueUrl(); `discussions.ts:54` parseGithubIssueUrl() | Explicit user-selected discussion/issue/comment source evidence; no existing tracker item is selected as the issue owner and no GitHub comment is written. | +| `github-url.ts` / `parseGithubIssueUrl` | `github-url.ts:48` parseGithubUrl() | Shared structural parser/canonicalizer for both explicit source locators and claims. Parsing alone never establishes ownership. | +| `github-url.ts` / `normalizeGithubClaim` | `github-url.ts:90` parseGithubIssueUrl(); `github-url.ts:92` reference tag regex | Shared structural parser/canonicalizer for both explicit source locators and claims. Parsing alone never establishes ownership. | +| `identity.ts` / `identity` | `identity.ts:42` normalizeGithubClaim() | Offline diagnosis or explicitly approved local tag repair, not a GitHub issue write/target selector. The graph includes shared claims (including contradictions); opaque provider references retain their namespace. | +| `identity.ts` / `graph` | `identity.ts:53` reference tag regex; `identity.ts:54` githubOwnershipClaims(); `identity.ts:58` normalizeGithubClaim(); `identity.ts:60` reference tag regex; `identity.ts:60` parseGithubIssueUrl(); `identity.ts:65` legacyGithubDescriptionIdentity(); `identity.ts:70` githubLinkReferences(); `identity.ts:70` external_ref; `identity.ts:71` parseGithubIssueUrl() | Offline diagnosis or explicitly approved local tag repair, not a GitHub issue write/target selector. The graph includes shared claims (including contradictions); opaque provider references retain their namespace. | +| `identity.ts` / `plan` | `identity.ts:111` github_number; `identity.ts:111` github_number; `identity.ts:123` external_ref; `identity.ts:123` external_ref | Offline diagnosis or explicitly approved local tag repair, not a GitHub issue write/target selector. The graph includes shared claims (including contradictions); opaque provider references retain their namespace. | +| `identity.ts` / `applyGithubIdentityRepair` | `identity.ts:178` external_ref; `identity.ts:178` external_ref | Offline diagnosis or explicitly approved local tag repair, not a GitHub issue write/target selector. The graph includes shared claims (including contradictions); opaque provider references retain their namespace. | +| `import-fidelity.ts` / `citationReferences` | `import-fidelity.ts:91` parseGithubIssueUrl(); `import-fidelity.ts:91` trimLocatorPunctuation() | Parse citation/prose/provider evidence for explicit first-link/adoption planning; matchImportedIssue subjects the resulting item to shared ownership constraints. No independent existing-owner target. | +| `import-fidelity.ts` / `hasOtherGithubUrl` | `import-fidelity.ts:140` trimLocatorPunctuation(); `import-fidelity.ts:141` parseGithubIssueUrl() | Parse citation/prose/provider evidence for explicit first-link/adoption planning; matchImportedIssue subjects the resulting item to shared ownership constraints. No independent existing-owner target. | +| `import-fidelity.ts` / `citationRepositoryBefore` | `import-fidelity.ts:153` parseGithubIssueUrl(); `import-fidelity.ts:153` trimLocatorPunctuation() | Parse citation/prose/provider evidence for explicit first-link/adoption planning; matchImportedIssue subjects the resulting item to shared ownership constraints. No independent existing-owner target. | +| `import-fidelity.ts` / `inspectCitationReferences` | `import-fidelity.ts:172` hasOtherGithubUrl(); `import-fidelity.ts:176` parseGithubIssueUrl(); `import-fidelity.ts:176` trimLocatorPunctuation(); `import-fidelity.ts:184` citationRepositoryBefore() | Parse citation/prose/provider evidence for explicit first-link/adoption planning; matchImportedIssue subjects the resulting item to shared ownership constraints. No independent existing-owner target. | +| `import-fidelity.ts` / `hasAnchoredCitationIntent` | `import-fidelity.ts:196` citationReferences(); `import-fidelity.ts:211` parseGithubIssueUrl() | Parse citation/prose/provider evidence for explicit first-link/adoption planning; matchImportedIssue subjects the resulting item to shared ownership constraints. No independent existing-owner target. | +| `import-fidelity.ts` / `citesIssue` | `import-fidelity.ts:221` inspectCitationReferences(); `import-fidelity.ts:224` hasAnchoredCitationIntent() | Parse citation/prose/provider evidence for explicit first-link/adoption planning; matchImportedIssue subjects the resulting item to shared ownership constraints. No independent existing-owner target. | +| `import-fidelity.ts` / `locators` | `import-fidelity.ts:240` trimLocatorPunctuation(); `import-fidelity.ts:241` parseGithubIssueUrl() | Parse citation/prose/provider evidence for explicit first-link/adoption planning; matchImportedIssue subjects the resulting item to shared ownership constraints. No independent existing-owner target. | +| `import-fidelity.ts` / `copiedProviderMarkersOwnItem` | `import-fidelity.ts:247` githubOwnershipClaims() | Lifecycle/provider classification and first-link eligibility; shared claims decide existing ownership. Raw prose/provider evidence does not independently select a GitHub target. | +| `import-fidelity.ts` / `providerProvenance` | `import-fidelity.ts:255` copiedProviderMarkersOwnItem(); `import-fidelity.ts:261` copiedProviderMarkersOwnItem() | Lifecycle/provider classification and first-link eligibility; shared claims decide existing ownership. Raw prose/provider evidence does not independently select a GitHub target. | +| `import-fidelity.ts` / `hasStructuredOwnership` | `import-fidelity.ts:275` providerProvenance(); `import-fidelity.ts:275` githubOwnershipClaims() | Lifecycle/provider classification and first-link eligibility; shared claims decide existing ownership. Raw prose/provider evidence does not independently select a GitHub target. | +| `import-fidelity.ts` / `legacyGithubLinkClaims` | `import-fidelity.ts:297` legacyGithubLinkIdentities() | Raw collector implementation used by githubOwnershipClaims; not an operational target selector. Partial legacy fields constrain only explicit or verified importer-ID claims, never reference tags. | +| `import-fidelity.ts` / `githubLinkReferences` | `import-fidelity.ts:302` locators(); `import-fidelity.ts:303` parseGithubIssueUrl() | Reference extraction only: supported link fields expose prose and API URL mentions, never owner claims or an import target. | +| `import-fidelity.ts` / `parseLegacyGithubDescription` | `import-fidelity.ts:311` parseGithubIssueUrl(); `import-fidelity.ts:311` trimLocatorPunctuation() | Shared native declaration parser: ownership and diagnostics use the same URL boundaries and matching positive safe issue numbers. | +| `import-fidelity.ts` / `legacyGithubDescriptionIdentity` | `import-fidelity.ts:320` parseLegacyGithubDescription() | Legacy native declaration identity: anchored at description start, first complete issue/PR URL, with trailing punctuation removed. No link-only marker or whole-description match is required. | +| `import-fidelity.ts` / `legacyGithubLinkIdentities` | `import-fidelity.ts:325` explicitGithubIdentities(); `import-fidelity.ts:326` legacyGithubDescriptionIdentity() | Raw collector implementation used by githubOwnershipClaims; not an operational target selector. Partial legacy fields constrain only explicit or verified importer-ID claims, never reference tags. | +| `import-fidelity.ts` / `explicitGithubIdentities` | `import-fidelity.ts:334` parseGithubIssueUrl() | Raw collector implementation used by githubOwnershipClaims; not an operational target selector. Partial legacy fields constrain only explicit or verified importer-ID claims, never reference tags. | +| `import-fidelity.ts` / `importIdentityKeys` | `import-fidelity.ts:356` explicitGithubIdentities(); `import-fidelity.ts:358` normalizeGithubRepository(); `import-fidelity.ts:358` github_repo; `import-fidelity.ts:359` github_number; `import-fidelity.ts:359` github_number; `import-fidelity.ts:359` github_number | Raw collector implementation used by githubOwnershipClaims; not an operational target selector. Partial legacy fields constrain only explicit or verified importer-ID claims, never reference tags. | +| `import-fidelity.ts` / `indexByImportIdentity` | `import-fidelity.ts:371` buildGithubOwnershipIndex(); `import-fidelity.ts:373` ownershipStateOf(); `import-fidelity.ts:373` githubOwnedIdentityKeys() | Inbound identity indexing validates only incoming issue keys (or all supplied items when no target scope is supplied), against the complete peer snapshot. Verification receipts use the persisted plan targets. | +| `import-fidelity.ts` / `matchImportedIssue` | `import-fidelity.ts:379` locators(); `import-fidelity.ts:380` locators(); `import-fidelity.ts:381` parseGithubIssueUrl(); `import-fidelity.ts:383` buildGithubOwnershipIndex(); `import-fidelity.ts:389` ownershipStateOf(); `import-fidelity.ts:390` assertGithubOwnershipConsistency(); `import-fidelity.ts:392` locators(); `import-fidelity.ts:392` providerProvenance(); `import-fidelity.ts:392` parseGithubIssueUrl(); `import-fidelity.ts:399` parseGithubIssueUrl(); `import-fidelity.ts:403` citesIssue(); `import-fidelity.ts:407` hasStructuredOwnership(); `import-fidelity.ts:416` providerProvenance(); `import-fidelity.ts:416` copiedProviderMarkersOwnItem(); `import-fidelity.ts:420` github_url | Inbound issue-to-item resolution uses shared claims and whole-corpus consistency/uniqueness; supported first-link evidence cannot replace an established owner. | +| `import-fidelity.ts` / `declaredOwner` | `import-fidelity.ts:385` copiedProviderMarkersOwnItem(); `import-fidelity.ts:386` legacyGithubDescriptionIdentity() | Lifecycle/provider classification and first-link eligibility; shared claims decide existing ownership. Raw prose/provider evidence does not independently select a GitHub target. | +| `import-fidelity.ts` / `importCloseReason` | `import-fidelity.ts:486` parseGithubUrl(); `import-fidelity.ts:488` parseGithubIssueUrl(); `import-fidelity.ts:500` parseGithubUrl(); `import-fidelity.ts:501` parseGithubIssueUrl(); `import-fidelity.ts:506` parseGithubIssueUrl() | Parse remote timeline/closure evidence to render source lifecycle wording; no tracker identity selected. | +| `import-fidelity.ts` / `persistImportFidelity` | `import-fidelity.ts:610` parseGithubIssueUrl(); `import-fidelity.ts:610` github_url; `import-fidelity.ts:611` github_number; `import-fidelity.ts:612` withGithubImportOwners(); `import-fidelity.ts:613` github_number; `import-fidelity.ts:615` parseGithubIssueUrl(); `import-fidelity.ts:615` github_url; `import-fidelity.ts:616` assertGithubImportOwner(); `import-fidelity.ts:616` github_number; `import-fidelity.ts:628` github_number | Remote source metadata determines the proposed fields; shared positive owner guard checks the existing item at preflight and in the native mutation lock. | +| `import-ownership.ts` / `githubImporterIdentity` | `import-ownership.ts:60` importerCreatedItem(); `import-ownership.ts:62` normalizeGithubRepository(); `import-ownership.ts:62` github_repo; `import-ownership.ts:63` explicitGithubIdentities(); `import-ownership.ts:63` legacyGithubLinkIdentities(); `import-ownership.ts:64` normalizeGithubClaim(); `import-ownership.ts:64` external_ref; `import-ownership.ts:66` parseGithubIssueUrl(); `import-ownership.ts:68` reference tag regex; `import-ownership.ts:71` githubRepositoryHash() | A proven importer creator plus the verified repository hash resolves the ID source number. Tags supply only candidate repository spellings for hash verification; their issue numbers never become claims. | +| `import-ownership.ts` / `readGithubOwnershipSnapshot` | `import-ownership.ts:110` buildGithubOwnershipIndex() | Build or maintain the single parsed claims/claimants index for a complete snapshot. Guards consume this index; it does not independently authorize a write. | +| `import-ownership.ts` / `buildGithubOwnershipIndex` | `import-ownership.ts:117` githubOwnershipClaims() | Build or maintain the single parsed claims/claimants index for a complete snapshot. Guards consume this index; it does not independently authorize a write. | +| `import-ownership.ts` / `ownershipIndex` | `import-ownership.ts:131` buildGithubOwnershipIndex() | Build or maintain the single parsed claims/claimants index for a complete snapshot. Guards consume this index; it does not independently authorize a write. | +| `import-ownership.ts` / `warnGithubOwnershipScope` | `import-ownership.ts:138` parseLegacyGithubDescription(); `import-ownership.ts:140` external_ref; `import-ownership.ts:140` reference tag regex; `import-ownership.ts:140` external_ref; `import-ownership.ts:145` ownershipStateOf() | Diagnostics only: malformed native numbers and conflicts wholly outside selected targets emit warnings naming affected IDs; no warning chooses an owner or authorizes writes. | +| `import-ownership.ts` / `ownershipStateOf` | `import-ownership.ts:160` githubOwnershipClaims() | Identity-safe state read: the states map is keyed by object identity, so a separately allocated copy of a corpus record derives its claims instead of reading as unowned; every planner and selector uses it. | +| `import-ownership.ts` / `githubOwnershipClaims` | `import-ownership.ts:165` importIdentityKeys(); `import-ownership.ts:166` normalizeGithubClaim(); `import-ownership.ts:166` external_ref; `import-ownership.ts:167` githubImporterIdentity(); `import-ownership.ts:170` legacyGithubLinkIdentities(); `import-ownership.ts:172` explicitGithubIdentities(); `import-ownership.ts:173` parseGithubIssueUrl(); `import-ownership.ts:173` external_ref; `import-ownership.ts:179` parseGithubIssueUrl(); `import-ownership.ts:182` normalizeGithubRepository(); `import-ownership.ts:182` github_repo; `import-ownership.ts:183` github_number; `import-ownership.ts:183` github_number; `import-ownership.ts:183` github_number | Shared raw-claim collection, canonicalization, URL-kind and lifecycle-classification facets. Tags never become claims or constraints; legacy importer IDs require creation provenance and a verified repository hash. | +| `import-ownership.ts` / `assertGithubOwnershipConsistency` | `import-ownership.ts:199` ownershipIndex(); `import-ownership.ts:200` githubOwnershipClaims() | Claims-consistency and peer-uniqueness check only: may return an empty owner list for an unowned item; does not enforce positive ownership or revisions. assertGithubImportOwner enforces a matching positive owner, readGithubImportOwner captures the revision, and withGithubImportOwners fences it inside native writes. | +| `import-ownership.ts` / `githubOwnedIdentityKeys` | `import-ownership.ts:212` ownershipIndex(); `import-ownership.ts:213` ownershipStateOf(); `import-ownership.ts:215` assertGithubOwnershipConsistency() | Shared operational selector: validate all claims and competing peers before returning any owned identity. | +| `import-ownership.ts` / `assertGithubImportOwner` | `import-ownership.ts:221` assertGithubOwnershipConsistency(); `import-ownership.ts:222` importerCreatedItem() | Shared positive ownership, uniqueness and revision guard for reads and native create/update/close writes. | +| `import-ownership.ts` / `readGithubImportOwner` | `import-ownership.ts:233` buildGithubOwnershipIndex(); `import-ownership.ts:237` assertGithubOwnershipConsistency(); `import-ownership.ts:241` assertGithubImportOwner() | Shared positive ownership, uniqueness and revision guard for reads and native create/update/close writes. | +| `import-ownership.ts` / `withGithubImportOwners` | `import-ownership.ts:263` withGithubOwnershipLock(); `import-ownership.ts:263` withGithubImportOwnersUnderLease() | Native mutation boundary: recheck before/after documents and dependency endpoints against current complete metadata inside the source item lock. | +| `import-ownership.ts` / `withGithubImportOwnersUnderLease` | `import-ownership.ts:296` buildGithubOwnershipIndex(); `import-ownership.ts:298` readGithubImportOwner(); `import-ownership.ts:301` withGithubOwnershipHooks(); `import-ownership.ts:313` buildGithubOwnershipIndex(); `import-ownership.ts:318` assertGithubImportOwner(); `import-ownership.ts:321` assertGithubImportOwner() | Shared positive ownership, uniqueness and revision guard for reads and native create/update/close writes. | +| `import-ownership.ts` / `mutateGithubOwnedItem` | `import-ownership.ts:333` readGithubImportOwner(); `import-ownership.ts:334` withGithubImportOwners() | Shared positive ownership, uniqueness and revision guard for reads and native create/update/close writes. | +| `import-ownership.ts` / `withGithubItemConsistency` | `import-ownership.ts:344` withGithubOwnershipLock(); `import-ownership.ts:344` withGithubOwnershipHooks(); `import-ownership.ts:346` buildGithubOwnershipIndex(); `import-ownership.ts:348` assertGithubOwnershipConsistency() | Project-pull native boundary: verify the captured planning owners and complete current peer corpus under the ownership lease and native item lock; zero-owner board items remain valid. | +| `index.ts` / `parseProvenanceTag` | `index.ts:863` reference tag regex | Low-level reference parser; operational selectors never use it as authority. Remaining callers parse input hints, filter labels, or diagnose legacy data. | +| `index.ts` / `indexByProvenance` | `index.ts:1289` buildGithubOwnershipIndex(); `index.ts:1290` ownershipStateOf(); `index.ts:1291` warnGithubOwnershipScope(); `index.ts:1292` githubOwnedIdentityKeys() | Search association indexes only remotely matched issue keys, against the full tracker corpus. Other conflicts warn; peers claiming a matched issue still refuse. | +| `index.ts` / `deriveAtomicItemId` | `index.ts:1465` githubRepositoryHash() | Derive the deterministic destination from explicit incoming repository/number; a matching ID is ownership evidence only with importer creation provenance and a verified repository hash. Atomic application checks the occupant through the shared native owner guard. | +| `index.ts` / `buildAtomicImportMutations` | `index.ts:1502` deriveAtomicItemId() | Derive the deterministic destination from explicit incoming repository/number; a matching ID is ownership evidence only with importer creation provenance and a verified repository hash. Atomic application checks the occupant through the shared native owner guard. | +| `index.ts` / `finalize` | `index.ts:1757` githubImportTarget() | Inbound planning, atomic application/finalization/replay or receipts: shared claims select identities; native writes recheck current ownership/uniqueness under the item lock. Persisted plans do not bypass the guard. | +| `index.ts` / `commitGithubImportTransactionUnchecked` | `index.ts:1781` githubImportTarget(); `index.ts:1783` linkImportedDependencies(); `index.ts:1786` github_url | Inbound planning, atomic application/finalization/replay or receipts: shared claims select identities; native writes recheck current ownership/uniqueness under the item lock. Persisted plans do not bypass the guard. | +| `index.ts` / `commitGithubImportTransaction` | `index.ts:1848` githubImportTarget(); `index.ts:1850` withGithubImportOwners() | Inbound planning, atomic application/finalization/replay or receipts: shared claims select identities; native writes recheck current ownership/uniqueness under the item lock. Persisted plans do not bypass the guard. | +| `index.ts` / `githubImportTarget` | `index.ts:1865` githubOwnedIdentityKeys(); `index.ts:1866` matchImportedIssue(); `index.ts:1867` github_url | Inbound planning, atomic application/finalization/replay or receipts: shared claims select identities; native writes recheck current ownership/uniqueness under the item lock. Persisted plans do not bypass the guard. | +| `index.ts` / `importGithubAtomic` | `index.ts:1911` githubImportTarget(); `index.ts:1913` assertGithubImportOwner() | Inbound planning, atomic application/finalization/replay or receipts: shared claims select identities; native writes recheck current ownership/uniqueness under the item lock. Persisted plans do not bypass the guard. | +| `index.ts` / `validateGithubImportFidelity` | `index.ts:1945` github_url; `index.ts:1946` parseGithubIssueUrl(); `index.ts:1946` github_url; `index.ts:1947` github_number | Validate the plan's explicit remote source URL/number; this is serialized input validation, not local item target selection. | +| `index.ts` / `syncGithubCommentsToAnnotations` | `index.ts:2731` withGithubImportOwners() | Native tracker comments/annotations only, protected by the shared owner wrapper; this does not POST comments to GitHub. | +| `index.ts` / `buildProvenanceIndexFromMetadata` | `index.ts:3057` githubOwnershipClaims() | Dependency endpoints use shared claims, including external_ref and legacy links. Ambiguous/inconsistent keys report ambiguous_github_dependency_owner; both endpoints are checked inside the source write lock. | +| `index.ts` / `defaultApplyDependencyLink` | `index.ts:3167` mutateGithubOwnedItem() | Dependency endpoints use shared claims, including external_ref and legacy links. Ambiguous/inconsistent keys report ambiguous_github_dependency_owner; both endpoints are checked inside the source write lock. | +| `index.ts` / `linkImportedDependencies` | `index.ts:3223` buildProvenanceIndexFromMetadata(); `index.ts:3227` planDependencyLinks() | Dependency endpoints use shared claims, including external_ref and legacy links. Ambiguous/inconsistent keys report ambiguous_github_dependency_owner; both endpoints are checked inside the source write lock. | +| `index.ts` / `prepareGithubImport` | `index.ts:3288` matchImportedIssue(); `index.ts:3290` importerCreatedItem() | Shared inbound matcher selects the item; creation provenance determines content mirroring independently of identity or naming. | +| `index.ts` / `verifyImportIdempotency` | `index.ts:3440` buildGithubOwnershipIndex(); `index.ts:3445` ownershipStateOf(); `index.ts:3448` ownershipStateOf(); `index.ts:3459` githubOwnedIdentityKeys() | Receipt scope is the prepared incoming issue keys only. Each relevant claimant is validated against the complete tracker peer index; unrelated owners cannot refuse a batch. | +| `index.ts` / `verifyImportedProvenance` | `index.ts:3499` indexByImportIdentity(); `index.ts:3499` buildGithubOwnershipIndex(); `index.ts:3502` verifyImportIdempotency() | Post-write receipt scope is the persisted plan issue keys. Complete tracker indexing is retained for relevant peer conflicts. | +| `index.ts` / `runImport` | `index.ts:3647` assertGithubImportOwner(); `index.ts:3723` buildGithubOwnershipIndex(); `index.ts:3725` warnGithubOwnershipScope(); `index.ts:3725` ownershipStateOf(); `index.ts:3726` indexByImportIdentity(); `index.ts:3787` readGithubOwnershipSnapshot(); `index.ts:3787` buildGithubOwnershipIndex(); `index.ts:3789` githubImportTarget(); `index.ts:3790` deriveAtomicItemId(); `index.ts:3791` readGithubImportOwner(); `index.ts:3792` assertGithubImportOwner(); `index.ts:3798` verifyImportIdempotency(); `index.ts:3903` verifyImportedProvenance(); `index.ts:3919` githubImportTarget(); `index.ts:3937` linkImportedDependencies(); `index.ts:4001` githubImportTarget(); `index.ts:4023` githubImportTarget(); `index.ts:4025` githubImportTarget(); `index.ts:4065` githubImportTarget(); `index.ts:4079` deriveAtomicItemId(); `index.ts:4101` githubImportTarget(); `index.ts:4143` linkImportedDependencies(); `index.ts:4148` verifyImportedProvenance() | Inbound planning, atomic application/finalization/replay or receipts: shared claims select identities; native writes recheck current ownership/uniqueness under the item lock. Persisted plans do not bypass the guard. | +| `index.ts` / `ownedPmRun` | `index.ts:3985` githubImportTarget(); `index.ts:3986` readGithubImportOwner(); `index.ts:3988` mutateGithubOwnedItem(); `index.ts:3992` readGithubImportOwner() | Inbound planning, atomic application/finalization/replay or receipts: shared claims select identities; native writes recheck current ownership/uniqueness under the item lock. Persisted plans do not bypass the guard. | +| `index.ts` / `upstreamTargets` | `index.ts:4267` githubOwnedIdentityKeys() | Outbound target collection iterates selected IDs only, against the complete tracker ownership index. Unselected collisions warn; selected peers refuse. Each HTTP write refreshes the current target against all peers. | +| `index.ts` / `assertCurrentUpstreamTarget` | `index.ts:4279` buildGithubOwnershipIndex(); `index.ts:4280` githubOwnedIdentityKeys(); `index.ts:4282` githubOwnershipClaims() | Outbound sync/export selects shared unique ownership against the whole corpus (including unselected peers); a fresh complete tracker read rechecks the target immediately before each PATCH/POST. | +| `index.ts` / `planSync` | `index.ts:4287` buildGithubOwnershipIndex(); `index.ts:4288` githubOwnedIdentityKeys(); `index.ts:4289` warnGithubOwnershipScope(); `index.ts:4290` upstreamTargets() | Sync scope is selected IDs (all tracker items if no IDs are given); upstreamTargets receives only those selected items, with complete peers retained for collision checks. | +| `index.ts` / `runSync` | `index.ts:4335` planSync(); `index.ts:4378` assertCurrentUpstreamTarget() | Outbound sync/export selects shared unique ownership against the whole corpus (including unselected peers); a fresh complete tracker read rechecks the target immediately before each PATCH/POST. | +| `index.ts` / `itemToGithubPayload` | `index.ts:4430` parseProvenanceTag(); `index.ts:4430` parseProjectItemTag() | Payload/label serialization only: reference tags are excluded from labels, never used to select an issue. | +| `index.ts` / `buildExportPlan` | `index.ts:4459` buildGithubOwnershipIndex(); `index.ts:4460` githubOwnedIdentityKeys(); `index.ts:4461` warnGithubOwnershipScope(); `index.ts:4462` upstreamTargets() | Export scope is selected IDs (all tracker items if no IDs are given); upstreamTargets receives selected items only, with a complete peer index and fresh per-write checks. | +| `index.ts` / `applyExportPlan` | `index.ts:4541` buildGithubOwnershipIndex(); `index.ts:4542` githubOwnedIdentityKeys(); `index.ts:4544` githubOwnershipClaims() | Package-built plans retain their shared ownership context, revalidated before dispatch; registered export also supplies a fresh tracker check. Caller-authored raw transport plans already specify repo/number and carry no tracker identity fields; their id is a reporting label, not a lookup. | +| `index.ts` / `runExport` | `index.ts:4685` buildExportPlan(); `index.ts:4752` applyExportPlan(); `index.ts:4757` assertCurrentUpstreamTarget() | Outbound sync/export selects shared unique ownership against the whole corpus (including unselected peers); a fresh complete tracker read rechecks the target immediately before each PATCH/POST. | +| `index.ts` / `parseBlockerReferences` | `index.ts:4961` parseGithubIssueUrl() | Parse explicitly named external blocker references from prose, including unparsed references. This queries reference state, not ownership of the containing item. | +| `index.ts` / `collectBlockerReferences` | `index.ts:5056` parseProvenanceTag(); `index.ts:5062` githubOwnedIdentityKeys() | Only shared, validated ownership may exclude self. Tag-derived repository hints resolve bare prose references only; they never grant ownership or select an existing local item. | +| `index.ts` / `runBlockers` | `index.ts:5669` collectBlockerReferences() | Pass the complete peer corpus to shared self-exclusion; live reference-state queries are distinct from issue ownership. | +| `index.ts` / `runProjectImport` | `index.ts:6564` buildProjectImportPlan() | Projects issue associations use shared ownership in plan builders; push rechecks current claims before remote actions. gh-project board/node IDs are their own dedicated namespace; draft/board operations do not derive issue ownership from gh: tags. | +| `index.ts` / `validate` | `index.ts:6683` buildGithubOwnershipIndex(); `index.ts:6684` githubOwnedIdentityKeys(); `index.ts:6686` assertGithubImportOwner(); `index.ts:6687` projectItemTag(); `index.ts:6689` assertGithubOwnershipConsistency() | Projects issue associations use shared ownership in plan builders; push rechecks current claims before remote actions. gh-project board/node IDs are their own dedicated namespace; draft/board operations do not derive issue ownership from gh: tags. | +| `index.ts` / `applyPushEntry` | `index.ts:6712` projectItemTag() | Projects issue associations use shared ownership in plan builders; push rechecks current claims before remote actions. gh-project board/node IDs are their own dedicated namespace; draft/board operations do not derive issue ownership from gh: tags. | +| `index.ts` / `applyPullEntry` | `index.ts:6786` getProjectPullOwners(); `index.ts:6789` withGithubItemConsistency() | Project-pull native SDK update/close: recheck captured planning owners and the complete current peer corpus under the tracker ownership namespace lease and native item lock. | +| `index.ts` / `runProjectSync` | `index.ts:6861` buildProjectPushPlan(); `index.ts:6864` buildProjectPullPlan(); `index.ts:6928` applyPullEntry() | Projects issue associations use shared ownership in plan builders; push rechecks current claims before remote actions. gh-project board/node IDs are their own dedicated namespace; draft/board operations do not derive issue ownership from gh: tags. | +| `index.ts` / `` | `index.ts:7237` resolveSearchCorpus(); `index.ts:7238` indexByProvenance(); `index.ts:7239` mapSearchHits(); `index.ts:7269` githubOwnedIdentityKeys() | Search-provider association and after-command hint use shared ownership; contradictory hint claims are reported. The hint sends no request and performs no mutation. | +| `labels.ts` / `planGithubLabelMigration` | `labels.ts:186` parseProvenanceTag() | Label migration evidence/filtering only; gh: parsing does not select a GitHub issue to read or write. | +| `projects.ts` / `parseProjectRef` | `projects.ts:120` parseGithubUrl() | Dedicated Projects board/node identity, not a GitHub issue/PR owner. Issue association and constraints are resolved separately through shared ownership. | +| `projects.ts` / `indexPmByProjectItem` | `projects.ts:355` parseProjectItemTag() | Board-node mapping indexes only selected tracker candidates matched by a current board node or issue. Project-item tags name board nodes, not GitHub issue ownership. | +| `projects.ts` / `indexPmByIssue` | `projects.ts:377` buildGithubOwnershipIndex(); `projects.ts:378` githubOwnedIdentityKeys() | Validate supplied board candidates only, using a complete peer snapshot. When called without a snapshot, every supplied item is explicitly in scope. | +| `projects.ts` / `buildProjectPushPlan` | `projects.ts:475` buildGithubOwnershipIndex(); `projects.ts:476` warnGithubOwnershipScope(); `projects.ts:477` indexProjectItemMap(); `projects.ts:478` indexProjectItemsByIssue(); `projects.ts:493` parseProjectItemTag(); `projects.ts:500` githubOwnedIdentityKeys(); `projects.ts:504` ownershipStateOf(); `projects.ts:505` assertGithubOwnershipConsistency() | Push scope is selected IDs (all tracker items without an ID filter). Board node/issue indexes contain current board entries only; complete tracker peers detect selected-owner conflicts. | +| `projects.ts` / `indexSelectedByIssue` | `projects.ts:614` buildGithubOwnershipIndex(); `projects.ts:617` ownershipStateOf(); `projects.ts:618` parseProjectItemTag(); `projects.ts:621` warnGithubOwnershipScope(); `projects.ts:622` indexPmByIssue(); `projects.ts:622` indexPmByProjectItem() | Build one complete ownership index, then validate only selected tracker items matched by current board issue keys or project-item IDs. Off-board conflicts warn; a peer claiming a board candidate still refuses. | +| `projects.ts` / `buildProjectPullPlan` | `projects.ts:648` indexSelectedByIssue(); `projects.ts:658` ownershipStateOf(); `projects.ts:659` assertGithubOwnershipConsistency(); `projects.ts:670` githubOwnedIdentityKeys() | Pull scope is the selected IDs intersected with the current board candidates, matched by issue key or project-item ID. Full tracker peers remain checked against each candidate. | +| `projects.ts` / `buildProjectImportPlan` | `projects.ts:713` indexSelectedByIssue(); `projects.ts:740` ownershipStateOf(); `projects.ts:741` assertGithubOwnershipConsistency(); `projects.ts:750` projectItemTag(); `projects.ts:752` issueProvenanceTag() | Import scope is the current board candidates, matched by issue key or project-item ID. Off-board collisions warn; peers claiming a board issue refuse. | +| `refresh.ts` / `selectRefreshTargets` | `refresh.ts:34` buildGithubOwnershipIndex(); `refresh.ts:36` ownershipStateOf(); `refresh.ts:37` warnGithubOwnershipScope(); `refresh.ts:40` ownershipStateOf(); `refresh.ts:42` githubOwnedIdentityKeys(); `refresh.ts:50` github_repo; `refresh.ts:50` github_repo; `refresh.ts:50` github_number; `refresh.ts:50` github_number; `refresh.ts:53` githubOwnedIdentityKeys() | Refresh scope is requested issue numbers, or every repository owner without a number filter. Complete tracker peers remain visible; unrelated conflicts warn. | +| `refresh.ts` / `fetchRefreshBatch` | `refresh.ts:88` parseGithubIssueUrl(); `refresh.ts:88` parseGithubIssueUrl() | Validate remote GraphQL node URL/number/kind against an already selected target; no independent local identity lookup. | +| `refresh.ts` / `runRefresh` | `refresh.ts:169` selectRefreshTargets(); `refresh.ts:250` selectRefreshTargets() | Refresh selection and post-recovery selection use shared ownership and uniqueness, with PR exclusion from the shared URL-kind facet. Import application retains native locked guards. | + +## Scripts and workflow audit + +The identity-field/tag scan finds 5 auxiliary locations. The inventory generator itself names these fields to document them; it selects no GitHub target. All anchors below are derived from the current file contents. + +| Location | Treatment | +| --- | --- | +| `scripts/dependabot-merge.ts:79` | Explicit GitHub CLI transport parameter; targets come from repository/PR arguments. | +| `scripts/dependabot-merge.ts:145` | Explicit GitHub CLI transport parameter; targets come from repository/PR arguments. | +| `scripts/prove-gate-public-forms.ts:4` | Privacy-gate documentation reference; no target lookup. | +| `.github/workflows/pm-github-sync.yml:27` | Importer option documentation or typed diagnostic rendering; no independent target lookup. | +| `.github/workflows/pm-github-sync.yml:466` | Importer option documentation or typed diagnostic rendering; no independent target lookup. | diff --git a/docs/round-eighteen-guard-audit.md b/docs/round-eighteen-guard-audit.md new file mode 100644 index 00000000..a1e1a03c --- /dev/null +++ b/docs/round-eighteen-guard-audit.md @@ -0,0 +1,49 @@ +# Ownership guard audit + +This audit compares the accepted round-16 implementation with round 17 and the current correction. Coverage thresholds, source inventory and source exclusions are unchanged. Native regression fixtures live in `test/round-eighteen.test.ts`; `test/round-seventeen.test.ts` retains the declaration parser controls. + +| Boundary changed in round 17 | Disposition and strongest boundary test | +| --- | --- | +| Closeout: evidence requires exactly one owner | Restored before shared consistency validation. Native contradictory URL/external-ref records reach this guard; unowned and consistent records remain allowed. | +| Closeout: delivery repository prefix | Kept. Native foreign-repository ownership refuses without changing item/history bytes. The redundant arity disjunct stays removed: the restored preceding evidence/arity guard refuses the two-owner native record first. | +| Native declaration: stated number is positive, safe and agrees with URL | Kept in the shared declaration parser, rather than reparsing in the warning caller. Native declarations with zero, negative, unsafe and mismatching numbers are rejected before becoming claims; matching declarations remain owners. | +| Atomic import: rethrow `github_import_owner_conflict` | Restored at the original error propagation boundary, after journal-aware plan cleanup. An ordinary SDK relink in `onPlanStaged` preserves the typed conflict before any transaction transition. | +| Import gate: duplicate identity tracking | Restored. Two native items claiming the target reach the dedicated typed refusal; repeating the same item row is allowed. | +| Import gate: only count requested identity keys | Restored. A planned native owner acquires a second out-of-plan claim; the target filter excludes that key from indexing and shared consistency refuses before writes. | +| Import gate: distinguish repeated same-owner rows from another owner | Restored. Repeated native owner rows index once; an independent native peer refuses. | +| Import gate: refuse nonempty duplicate set | Restored as a dedicated typed ownership refusal before normal idempotency receipts. The test checks that both colliding item IDs are named as claimants. | +| Refresh: conflicting issue numbers | Restored before shared resolution. A native URL for issue 7 combined with explicit number 8 refuses with the refresh conflict diagnostic; consistent 7/7 is allowed. | +| Refresh: issue number safe-integer guard | Restored before target creation, including raw explicit numbers that cannot form an ownership key. Native zero, negative, fractional and unsafe values refuse; 7 remains selectable. | +| Refresh: former raw explicit-number check | Retained semantically in the same preflight safe-number loop; it adds the raw explicit value before validation, without granting ownership to tags or a partial number. | +| Plain import: citation adoption counter in mirroring branch | Restored. This branch is reachable: a native item authored by the importer can contain only an eligible citation. It adopts and mirrors in place; a curated-author twin adopts while retaining its title. | +| Cached index: missing prior state fallback | Removed together with the obsolete cache/updater, not replaced by a non-null assertion. Each separately locked write now certifies and rebuilds its peer index, then validates before mutation. An ordinary SDK peer edit between comments is refused by that fresh check before any old cache could be consulted. | +| Cached index: missing claimant-map fallback | Removed with the same obsolete updater. The native inter-comment race proves the replacement refusal; native index controls verify that every indexed state and claimant map exists. A missing-id document is refused by native completeness certification before the batch callback, and a proposed ID deletion is refused inside the mutation guard before persistence. | +| Import: legacy reference-conflict consumer | Removed: the deprecated helper returns no ownership conflict under issue 230's contract. Native number-only records with multiple same-number and different-number reference tags import once without refusal and without changing those records. | +| Sync: legacy reference-conflict consumer | Removed for the same contract. The same native multi-reference records pass the real extension's sync dry run without an identity-conflict field or any tracker mutation. | +| Refresh: legacy reference-conflict consumer | Removed for the same contract. The same native records remain unowned and refresh selects no target, without conflict or mutation. | + +The board `add-issue` gap predates round 17: its validation body is unchanged between `58ae396` and `948ef40`. The new positive-owner check applies both before and after node resolution. The zero-owner alternatives require a current board tag or draft authorization. + +The native N=40 batch counter now observes one preflight corpus read plus one fresh read for each of 40 transaction writes and 40 metadata finalizers: 81 reads and bounded O(N*M) parsing. Reusing the live namespace lease does not permit reusing peer snapshots across separately locked writes. + +## Public snapshot preview + +The read-only checks replayed the same complete public issue snapshots against the same public tracker revisions as round 16. No import was applied, no original tracker bytes changed, and all scratch clones were removed. Plain reference tags remained mentions. Counts are creates/updates/unchanged; no curated title, description, type, body, creation date or existing tag would change. + +| Public repository | Tracker revision | Creates | Updates | Unchanged | Curated field changes | +| --- | --- | ---: | ---: | ---: | ---: | +| pm-github | `be28728c3092` | 22 | 2 | 0 | 0 | +| pm-review | `402c544c2c78` | 2 | 2 | 0 | 0 | +| pm-rust | `624e1a09014a` | 6 | 1 | 0 | 0 | +| pm-context | `b94eb039de4c` | 7 | 3 | 0 | 0 | +| pm-linear | `0f9dea0279a0` | 8 | 1 | 0 | 0 | + +pm-github retains the previously observed privacy-preflight refusal for existing public source bodies. Its counts are the read-only production-planner diagnostic after that refusal, not a claimed passing gated import. The other four exact gated previews pass. SDK runtimes for scratch extension installation follow each available exact project lock version; the package-pinned SDK is used when no SDK entry exists. No stale-runtime override is used. + +## Behavioral reversions + +Disposable copies reproduce ten intended failing assertions: lost board owner, removed board tag, an intervening peer claim between comments, proposed ID removal, closeout arity, required atomic typed rethrow, duplicate-owner diagnostics, refresh contradiction diagnostics, the mirroring citation counter, and substring-based API-host classification. Matching owners, current board tags, drafts and ordinary multiple comments remain allowed. Restoring the implementation passes the native regressions. Reverting the atomic function to its prior early rethrow remains an allowed earlier-refusal control; deleting its retained rethrow fails the typed-error assertion. + +## Final coverage receipt + +`coverage-gate: 34 authored modules, exact 100/100/100/100 coverage.` Native V8 receipts were collected against unchanged authored source through the bounded test batches and standard c8 reports. The complete source inventory and all four 100% thresholds are unchanged; no ignore comments or exemptions were added. The final pm-linked round-18 file passes 32/32. The large capture’s two uninitialized legacy fixtures were initialized through the SDK and their entire file reran successfully; two existing SDK compensation quarantines remain skipped. diff --git a/docs/sdk-minimum.md b/docs/sdk-minimum.md index 34ffe21a..0b65851e 100644 --- a/docs/sdk-minimum.md +++ b/docs/sdk-minimum.md @@ -1,6 +1,6 @@ # Declared SDK minimum -pm-github requires PM CLI **2026.10.17 or newer**. The top-level +pm-github requires PM CLI **2026.10.23 or newer**. The top-level `manifest.json` `pm_min_version` is the floor enforced by the host. npm peer dependencies declare that minimum. The development dependency is an independent exact pin in `package.json`, with an identical integrity-checked lock resolution. @@ -11,7 +11,8 @@ The minimum covers the entire package contract, rather than one imported name: | --- | --- | --- | | Public `decodePmTableRows` export | Absent in SDK 2026.10.5–2026.10.8; exported through `sdk/index` → `sdk/output` → `sdk/output/table-rows` in 2026.10.9 | 2026.10.9 | | `BulkItemUpdateMutation.match` under-lock revision guard | Absent in published SDK 2026.10.9–2026.10.15; present in 2026.10.17. Version 2026.10.16 was not published. | 2026.10.17 | -| Both contracts, including existing identity repair | Minimum-SDK compilation rejects 2026.10.9 because `match` does not exist; the guard must remain intact | 2026.10.17 | +| `acquireLockLease` with generation fencing | Absent from public SDK exports in 2026.10.22; exported with `LockLease.fence` in 2026.10.23 | 2026.10.23 | +| All contracts, including cross-item import ownership | No unfenced fallback: every ownership-changing operation holds a fenced lease | 2026.10.23 | These declarations were read from published npm tarballs, including [SDK 2026.10.8](https://registry.npmjs.org/@unbrained/pm-cli/-/pm-cli-2026.10.8.tgz), @@ -25,7 +26,7 @@ only after a command has started. Run `npm run test:minimum-sdk` to build and capture a package artifact, install it alongside the **exact declared minimum** from npm, and type-check every production and operational TypeScript module against that installation. This -covers all 80 named SDK imports in 11 modules, including the `sdk`, +covers every production and operational SDK import, including the `sdk`, `sdk/authoring` and `sdk/graph` entry points. It also checks every shipped declaration as an ambient TypeScript source, so skipped dependency declarations cannot hide a missing SDK type. It type-checks a public typed consumer, imports @@ -46,14 +47,40 @@ or required SDK option above the floor fails this check even when the newer development SDK would accept it. The CLI refuses older hosts through the manifest floor before loading the extension's ESM imports. -The breaking minimum is generated into `CHANGELOG.md` from the pending -[pm-github-2e38](https://github.com/unbraind/pm-github/blob/main/.agents/pm/features/pm-github-2e38.toon) -item using its `changelog-unreleased` tag. The item stays unclosed until the -orchestrator verifies the result. +The current breaking minimum is generated into `CHANGELOG.md` from +[pm-github-reference-identity](https://github.com/unbraind/pm-github/blob/main/.agents/pm/issues/pm-github-reference-identity.toon) +using its `changelog-unreleased` tag. Historical minimum changes remain in their +original release entries. The item stays unclosed until orchestrator verification. Validation on 2026-10-27 passed under Node and native Bun 1.3.14 (one real consumer test each). In disposable copies, changing only the manifest floor and matching dependency ranges to 2026.10.5 fails at ESM linking with the missing `decodePmTableRows` export; 2026.10.15 links but fails compilation on the missing -`BulkItemUpdateMutation.match` option. Restoring 2026.10.17 passes the same test. +`BulkItemUpdateMutation.match` option. Restoring 2026.10.17 passed the same test +at that historical head. The current fenced-lock floor is documented below. The reviewed checkout is never mutated by those negative proofs. + +## Fenced ownership floor (2026-11-12) + +Published SDK tarballs establish the adjacent boundary: +[2026.10.22](https://registry.npmjs.org/@unbrained/pm-cli/-/pm-cli-2026.10.22.tgz) +does not export `acquireLockLease`, while +[2026.10.23](https://registry.npmjs.org/@unbrained/pm-cli/-/pm-cli-2026.10.23.tgz) +exports it in both JavaScript and declarations. Its `LockLease.fence` refuses a +released or replaced generation with `lock_lost`, before invoking the callback. +The minimum consumer executes both a live and a released lease on the real SDK. + +All audited fleet caller `package-lock.json` resolutions already export this API: + +| Caller | Locked PM CLI | +| --- | --- | +| pm-review | 2026.10.28 | +| pm-context | 2026.11.4 | +| pm-linear | 2026.11.4 | +| pm-brief | 2026.11.4 | +| pm-rl | 2026.11.3 | + +The manifest and npm peer floor are therefore raised to 2026.10.23. The exact +development dependency remains unchanged. Unsupported hosts are refused by the +package/manifest contract rather than running an ownership write under an +expiring, unfenced lock. Read-only planning remains available on supported hosts. diff --git a/durable-state.ts b/durable-state.ts new file mode 100644 index 00000000..223a55a9 --- /dev/null +++ b/durable-state.ts @@ -0,0 +1,66 @@ +/** Durable package-owned state uses the SDK's sibling-file writer before publication. */ +import { writeFileAtomic } from "@unbrained/pm-cli/sdk"; +import { mkdir, open, link, rename, unlink, lstat } from "node:fs/promises"; +import { dirname, basename, join } from "node:path"; +import { randomUUID } from "node:crypto"; + +/** A fully written and flushed candidate that is still invisible to state readers. */ +export interface StagedGithubState { + file: string; + staged: string; +} + +/** Exclusive publication never overwrites a peer's existing plan or lease. */ +export interface GithubStateWriteOptions { + exclusive?: boolean; + /** Platform seam for portability tests; filesystem operations remain native. */ + platform?: () => NodeJS.Platform; + /** Observation/fault-injection boundary after fsync and before publication. */ + onStaged?: (context: StagedGithubState) => void | Promise; +} + +/** Writable file handles support Windows FlushFileBuffers; Windows cannot fsync directories. */ +export function githubStateSyncMode(directory: boolean, platform: NodeJS.Platform = process.platform): "r" | "r+" | undefined { + return directory ? platform === "win32" ? undefined : "r" : "r+"; +} + +/** Flush a complete file, or a directory only where the platform supports it. */ +export async function syncGithubStatePath(target: string, directory: boolean, platform: NodeJS.Platform = process.platform): Promise { + const mode = githubStateSyncMode(directory, platform); + if (mode === undefined) return; // win32 has no supported directory-fsync path through Node. + const handle = await open(target, mode); + try { await handle.sync(); } finally { await handle.close(); } +} + +/** Publish only complete fsynced bytes; atomic link preserves exclusive-create semantics. */ +export async function writeGithubStateAtomic(file: string, contents: string, options: GithubStateWriteOptions = {}): Promise { + const platform = (options.platform ?? (() => process.platform))(); + const directory = dirname(file); + const created = await mkdir(directory, { recursive: true }); + const staged = join(directory, `.${basename(file)}.${randomUUID()}.stage`); + try { + if (options.exclusive) { + // Match O_EXCL even when an existing owner's directory is read-only. + // This is an early contention hint; link() remains the race-safe authority. + const exists = await lstat(file).then(() => true, (error: NodeJS.ErrnoException) => { + if (error.code === "ENOENT") return false; + throw error; + }); + if (exists) throw Object.assign(new Error("Durable state already exists"), { code: "EEXIST" }); + } + await writeFileAtomic(staged, contents, { createParentDirectories: false }); + await syncGithubStatePath(staged, false, platform); + await options.onStaged?.({ file, staged }); + if (options.exclusive) await link(staged, file); + else await rename(staged, file); + } finally { + // A crash can leave an invisible stage; readers never enumerate it as state. + await unlink(staged).catch(() => undefined); + // A newly created directory also needs its parent entry made durable. + const last = created ? dirname(created) : directory; + for (let current = directory; ; current = dirname(current)) { + await syncGithubStatePath(current, true, platform); + if (current === last || current === dirname(current)) break; + } + } +} diff --git a/github-url.ts b/github-url.ts new file mode 100644 index 00000000..595eb6ce --- /dev/null +++ b/github-url.ts @@ -0,0 +1,94 @@ +/** GitHub URL structure shared by identity, import and outbound validation. */ +export interface GithubUrlParts { + /** Parsed URL, including query and fragment for callers that validate attribution. */ + url: URL; + /** Whether the locator names GitHub's REST host. */ + api: boolean; + /** Original owner and repository spelling; repo is their canonical lookup key. */ + owner: string; + name: string; + repo: string; + /** Fixed path positions following the owner and repository. */ + kind: string | undefined; + value: string | undefined; + tail: string[]; +} + +/** Preserve released repository keys while canonicalizing case and trailing slashes in linear time. */ +export function normalizeGithubRepository(value: unknown): string | undefined { + if (typeof value !== "string") return undefined; + const text = value.trim(); + let end = text.length; + while (end > 0 && text[end - 1] === "/") end--; + return text.slice(0, end).toLowerCase() || undefined; +} + +/** Parse GitHub web or REST URLs by segment position, never by replacing path substrings. */ +export function parseGithubUrl(value: unknown): GithubUrlParts | undefined { + if (typeof value !== "string") return undefined; + try { + // URL resolves dot segments; reject traversal before normalization can hide it. + if (/\\|%5c/i.test(value.split(/[?#]/, 1)[0]!)) return undefined; + if (/(?:\/|%2f)(?:\.|%2e){1,2}(?=\/|%2f|[?#]|$)/i.test(value)) return undefined; + const url = new URL(value.trim()); + if (!["https:", "http:"].includes(url.protocol) || url.username || url.password || url.port || + !["github.com", "www.github.com", "api.github.com"].includes(url.hostname.toLowerCase())) return undefined; + const segments = url.pathname.slice(1).split("/"); + if (segments.at(-1) === "") segments.pop(); + const api = url.hostname.toLowerCase() === "api.github.com"; + if (api && segments.shift()?.toLowerCase() !== "repos") return undefined; + const [owner, name, kind, identifier, ...tail] = segments; + if (!owner || !name || /\s/.test(owner) || /\s/.test(name) || segments.some(part => !part)) return undefined; + return { url, api, owner, name, repo: `${owner}/${name}`.toLowerCase(), kind: kind?.toLowerCase(), value: identifier, tail }; + } catch { return undefined; } +} + +/** Resolve one complete issue or PR URL; issues and pull forms share an identity key. */ +export function parseGithubIssueUrl(value: unknown) { + const parts = parseGithubUrl(value); + if (!parts || !["issues", "pull"].includes(parts.kind ?? "") || parts.tail.length || !/^[1-9]\d*$/.test(parts.value ?? "")) return undefined; + const number = Number(parts.value); + if (!Number.isSafeInteger(number)) return undefined; + return { ...parts, number, key: `${parts.repo}#${number}`, canonical: `https://github.com/${parts.repo}/issues/${number}` }; +} + +/** Extract prose URLs without Markdown wrappers; end retains the original span for masking. */ +export function githubUrlTokens(text: string): Array<{ raw: string; index: number; end: number }> { + const tokens: Array<{ raw: string; index: number; end: number }> = []; + const starts = /https?:\/\//gi; + for (let match = starts.exec(text); match; match = starts.exec(text)) { + const index = match.index; + let end = starts.lastIndex; + const brackets: string[] = []; + for (; end < text.length; end++) { + const character = text[end]!; + if (/[\s<>"'`]/.test(character)) break; + if ("([{".includes(character)) brackets.push(character); + else if (")]}".includes(character)) { + const opener = character === ")" ? "(" : character === "]" ? "[" : "{"; + if (brackets.at(-1) !== opener) break; + brackets.pop(); + } + } + starts.lastIndex = end; + let trimmed = end; + while (trimmed > index && ".,;:!?".includes(text[trimmed - 1]!)) trimmed--; + // Opening emphasis precedes the scheme; closing emphasis reverses its order. + const closing: string[] = []; + for (let prefix = index - 1; prefix >= 0 && "*_".includes(text[prefix]!); prefix--) closing.push(text[prefix]!); + const emphasis = closing.join(""); + if (emphasis && text.slice(index, trimmed).endsWith(emphasis)) trimmed -= emphasis.length; + while (trimmed > index && ".,;:!?".includes(text[trimmed - 1]!)) trimmed--; + tokens.push({ raw: text.slice(index, trimmed), index, end }); + } + return tokens; +} + +/** Canonical GitHub claim key shared by external references and identity diagnostics. */ +export function normalizeGithubClaim(value: unknown): string | undefined { + if (typeof value !== "string") return undefined; + const url = parseGithubIssueUrl(value); + if (url && !url.api) return url.key; + const tag = /^gh:([^/\s]+\/[^#\s]+)#([1-9]\d*)$/i.exec(value); + return tag && Number.isSafeInteger(Number(tag[2])) ? `${tag[1]!.toLowerCase()}#${BigInt(tag[2]!).toString()}` : undefined; +} diff --git a/identity.ts b/identity.ts index 9960eb8f..8d3a0824 100644 --- a/identity.ts +++ b/identity.ts @@ -1,3 +1,4 @@ +import { normalizeGithubClaim, parseGithubIssueUrl, githubUrlTokens } from "./github-url.ts"; import { ownValue } from "./own-record.ts"; /** Offline identity diagnosis and explicitly approved, single-item provenance repairs. */ import { createHash } from "node:crypto"; @@ -8,7 +9,8 @@ import { type BulkItemMutation, type CommitItemMutationsOptions, type ItemMetadata, } from "@unbrained/pm-cli/sdk"; import { readRefreshItems } from "./refresh.ts"; -import { serializeImportTags } from "./import-fidelity.ts"; +import { githubOwnershipClaims } from "./import-ownership.ts"; +import { serializeImportTags, githubLinkReferences, legacyGithubDescriptionIdentity } from "./import-fidelity.ts"; /** Full certified metadata plus the explicitly requested body projection. */ type IdentityItem = ItemMetadata & { body: string }; @@ -37,8 +39,8 @@ export interface IdentityRepairPlan { } /** Canonicalize exact GitHub locators; opaque external references retain provider semantics. */ function identity(value: string): string { - const match = /^(?:gh:([^/\s]+\/[^#\s]+)#|https?:\/\/github\.com\/([^/\s]+\/[^/\s]+)\/(?:issues|pull)\/)([1-9]\d*)(?:\/?(?:[?#].*)?)?$/i.exec(value); - return match ? `gh:${(match[1] ?? match[2]!).toLowerCase()}#${BigInt(match[3]!).toString()}` : value; + const key = normalizeGithubClaim(value); + return key ? `gh:${key}` : value; } /** Build edges from structured identity fields; prose URLs are explicitly weaker evidence. */ function graph(items: readonly IdentityItem[]): { nodes: IdentityNode[]; edges: IdentityEdge[] } { @@ -48,18 +50,28 @@ function graph(items: readonly IdentityItem[]): { nodes: IdentityNode[]; edges: const id = item.id!; const providers = [...new Set(Object.entries(item).filter(([key, value]) => /^(linear|jira|ado)_(?:.*_)?(?:id|key|url|identifier)$/i.test(key) && Boolean(value) && (typeof value !== "string" || value.trim() !== "")).map(([key]) => key.split("_")[0]!.toLowerCase()))].sort(); const claims: IdentityEdge[] = []; - for (const tag of item.tags) if (/^gh:[^/\s]+\/[^#\s]+#[1-9]\d*$/i.test(tag)) claims.push({ item: id, identity: identity(tag), source: "tag", strength: "claim" }); + for (const tag of item.tags) if (/^gh:[^/\s]+\/[^#\s]+#[1-9]\d*$/i.test(tag)) claims.push({ item: id, identity: identity(tag), source: "tag", strength: "mention" }); + const ownership = githubOwnershipClaims(item); for (const source of ["external_ref", "github_url"]) { const value = ownValue(item, source); - if (typeof value === "string" && value.trim()) claims.push({ item: id, identity: identity(value), source, strength: "claim" }); + if (typeof value !== "string") continue; + const key = normalizeGithubClaim(value); + if (key && ownership.claims.includes(key)) claims.push({ item: id, identity: `gh:${key}`, source, strength: "claim" }); + else if (source === "external_ref" && !/^gh:/i.test(value) && !parseGithubIssueUrl(value) && /^[a-z][a-z0-9_-]*:\S+$/i.test(value)) { + claims.push({ item: id, identity: value, source, strength: "claim" }); + } } - const declared = /^GitHub issue(?:\s*:\s+|\s+)(https?:\/\/\S+)/i.exec(item.description); - if (declared && identity(declared[1]!).startsWith("gh:")) claims.push({ item: id, identity: identity(declared[1]!), source: "declaration", strength: "claim" }); - const github = claims.some(edge => edge.identity.startsWith("gh:")); + for (const key of ownership.claims) if (!claims.some(edge => edge.identity === `gh:${key}` && edge.strength === "claim")) { + claims.push({ item: id, identity: `gh:${key}`, source: !ownership.direct && legacyGithubDescriptionIdentity(item)?.key === key ? "declaration" : "import-provenance", strength: "claim" }); + } + const github = claims.some(edge => edge.strength === "claim" && edge.identity.startsWith("gh:")); nodes.push({ id, providers, native_owner: typeof item.source_owner === "string" ? item.source_owner : null, ownership: providers.length ? (github && item.github_link_only !== true ? "mixed" : "provider") : github ? "github" : "local" }); edges.push(...claims); - for (const match of `${item.description}\n${item.body}`.matchAll(/https?:\/\/github\.com\/[^/\s<>]+\/[^/\s<>]+\/(?:issues|pull)\/[1-9]\d*/gi)) { - const key = identity(match[0]); + const references = [...githubLinkReferences(item), ...githubUrlTokens(`${item.description}\n${item.body}\n${item.external_ref ?? ""}`).flatMap(match => { + const parsed = parseGithubIssueUrl(match.raw); return parsed ? [parsed] : []; + })]; + for (const reference of references) { + const key = `gh:${reference.key}`; if (!claims.some(edge => edge.identity === key) && !edges.some(edge => edge.item === id && edge.identity === key && edge.strength === "mention")) edges.push({ item: id, identity: key, source: "prose", strength: "mention" }); } } @@ -102,7 +114,7 @@ function plan(items: IdentityItem[], key: string, owner: string, detach: string) if (!key.startsWith("gh:") || owner === detach || owners.length !== 1 || owners[0] !== owner || !target || nodes.find(node => node.id === owner)!.ownership === "mixed" || claims.some(edge => edge.item !== owner && edge.item !== detach) || - !claims.some(edge => edge.item === detach) || claims.some(edge => edge.item === detach && !["tag", "external_ref"].includes(edge.source)) || + !edges.some(edge => edge.item === detach && edge.identity === key) || claims.some(edge => edge.item === detach && !["tag", "external_ref"].includes(edge.source)) || target.source_owner || edges.some(edge => [owner, detach].includes(edge.item) && edge.strength === "claim" && edge.identity.startsWith("gh:") && edge.identity !== key) || target.github_link_only === false || Object.keys(target).some(field => /^github_/i.test(field))) { diff --git a/import-fidelity.ts b/import-fidelity.ts index fa9281ce..e3da44de 100644 --- a/import-fidelity.ts +++ b/import-fidelity.ts @@ -1,12 +1,14 @@ +import { normalizeGithubRepository, normalizeGithubClaim, parseGithubUrl, parseGithubIssueUrl } from "./github-url.ts"; import { readGithubWorkspace, type GithubStatusRegistry } from "./workspace.ts"; import { ownValue, setOwnValue } from "./own-record.ts"; +import { buildGithubOwnershipIndex, githubOwnedIdentityKeys, assertGithubImportOwner, GithubImportOwnershipError, assertGithubOwnershipConsistency, githubOwnershipClaims, withGithubImportOwners, type GithubOwnershipIndex, type GithubOwnedTarget, ownershipStateOf } from "./import-ownership.ts"; /** Source lifecycle, release-window and cross-provider identity rules for issue imports. */ import { spawnSync } from "node:child_process"; import { decodeHTML } from "entities"; import { canonicalDocument, createHistoryEntry, SETTINGS_DEFAULTS, resolveItemTypeRegistry, mutateItem, normalizeItemId, normalizeStatusInput, parseTags, toNonEmptyImportString, toImportTags, } from "@unbrained/pm-cli/sdk"; -import { CommandError, fetchJSON, githubApiBase, githubGraphQL, mapState, parseNextLink, parseProvenanceTag, type GhIssue, type PmItem } from "./index.ts"; +import { CommandError, fetchJSON, githubApiBase, githubGraphQL, mapState, parseNextLink, type GhIssue, type PmItem } from "./index.ts"; /** * Select the latest external edit or different lifecycle generation in immutable history. @@ -73,7 +75,7 @@ export class AmbiguousCitationError extends Error { constructor(repo: string, number: number, candidates: string[], incidental: CitationCandidate[] = []) { const entry: AmbiguousCitationPlan = { action: "ambiguous_citation", issue: number, candidates, ...(incidental.length > 0 ? { citation_candidates: incidental } : {}), - remedy: `Tag the correct item with gh:${repo}#${number}, then retry.` }; + remedy: `Set github_url to https://github.com/${repo}/issues/${number} on the intended owner, then retry.` }; super(JSON.stringify(entry)); this.entry = entry; } @@ -86,13 +88,8 @@ function citationReferences(text: string, repoHint: string): { key: string; inde for (const token of tokens) { const raw = token[0]; if (/^https?:/i.test(raw)) { - try { - const url = new URL(trimLocatorPunctuation(raw, "),.;:!]}")); - const path = /^\/([\w.-]+\/[\w.-]+)\/(issues|pull)\/([1-9]\d*)\/?$/i.exec(url.pathname); - if (["github.com", "www.github.com"].includes(url.hostname) && path) { - references.push({ key: `${path[1]!.toLowerCase()}#${path[3]}`, index: token.index, ownsIssue: path[2]!.toLowerCase() === "issues" }); - } - } catch { /* Malformed URL tokens never expose shorter hash references inside them. */ } + const target = parseGithubIssueUrl(trimLocatorPunctuation(raw, "),.;:!]}")); + if (target && !target.api) references.push({ key: target.key, index: token.index, ownsIssue: target.kind === "issues" }); } else { const [repo, number] = raw.split("#"); references.push({ key: `${repo || repoHint}#${number}`.toLowerCase(), index: token.index, ownsIssue: repo !== "" }); @@ -141,22 +138,27 @@ function hasOtherGithubUrl(text: string, repo: string, number: number): boolean const segment = text.slice(site.index, sites[index + 1]?.index); const token = /^github\.com[^\s<>"'`[\]()*{}]*/i.exec(segment)![0]; const locator = trimLocatorPunctuation(token, "),.;:!*_~]}"); - const reference = /^github\.com\/([\w.-]+\/[\w.-]+)\/issues\/([1-9]\d*)\/?(?:[?#].*)?$/i.exec(locator); - if (!reference || reference[1]!.toLowerCase() !== repo || reference[2] !== String(number)) return true; + const reference = parseGithubIssueUrl(`https://${locator}`); + if (!reference || reference.kind !== "issues" || reference.repo !== repo || reference.number !== number) return true; } return false; } /** Read the immediately preceding repository without making every character a regex start position. */ -function citationRepositoryBefore(text: string, index: number, hash: boolean): string | undefined { - if (!hash && text[index - 1] !== "/") return undefined; - const end = hash ? index : index - 1; +function citationRepositoryBefore(text: string, index: number, hash: boolean, lowered: string): string | undefined { + if (!hash) { + const start = lowered.lastIndexOf("github.com/", index); + if (start < 0) return undefined; + const token = /^[^\s<>"'`]+/.exec(text.slice(start))?.[0]; + return token ? parseGithubIssueUrl(`https://${trimLocatorPunctuation(token)}`)?.repo : undefined; + } + const end = index; let repoStart = end; while (repoStart > 0 && /[\w.-]/.test(text[repoStart - 1]!)) repoStart--; if (repoStart === end || text[repoStart - 1] !== "/") return undefined; let ownerStart = repoStart - 1; while (ownerStart > 0 && /[\w.-]/.test(text[ownerStart - 1]!)) ownerStart--; - if (ownerStart === repoStart - 1 || (!hash && text.slice(Math.max(0, ownerStart - 11), ownerStart).toLowerCase() !== "github.com/")) return undefined; + if (ownerStart === repoStart - 1) return undefined; return text.slice(ownerStart, end).toLowerCase(); } @@ -168,8 +170,18 @@ function inspectCitationReferences(texts: readonly string[], repo: string, numbe // Check both retained source and joined soft lines; joining must not discard link labels or destinations. for (const text of new Set([source, source.replace(/(?"'`\])}]+/gi, raw => { + const target = parseGithubIssueUrl(trimLocatorPunctuation(raw, "),.;:!*_~]}")); + if (!target || target.api) return raw; + if (target.key === `${repo}#${number}`) mentioned = true; + const suffix = raw.search(/[?#]/); + return suffix < 0 ? " ".repeat(raw.length) : " ".repeat(suffix) + raw.slice(suffix); + }); + const loweredNonPath = nonPath.toLowerCase(); + for (const reference of nonPath.matchAll(/(?:issues[\s/:]*|pull[\s/:]*|#)([0-9]+)/gi)) { + const sourceRepo = citationRepositoryBefore(nonPath, reference.index, reference[0].startsWith("#"), loweredNonPath); const sourceNumber = reference[1]; if (sourceNumber === String(number) && (sourceRepo === repo || sourceRepo === undefined)) mentioned = true; if (sourceRepo !== repo || sourceNumber !== String(number)) unsafe = true; @@ -194,7 +206,10 @@ function hasAnchoredCitationIntent(text: string, field: number, repo: string, nu while (end > 0 && (line[end - 1] === " " || line[end - 1] === "\t")) end--; while (end > 0 && (line[end - 1] === "." || line[end - 1] === ")")) end--; if (end > 0 && line[end - 1] === ">") end--; - if (/^https:\/\/github\.com\/[\w.-]+\/[\w.-]+\/issues\/[1-9]\d*\/?(?:[?#][^\s<>]*)?$/i.test(line.slice(0, end))) return true; + const locator = line.slice(0, end); + if (/[\s<>]/.test(locator)) continue; + const target = parseGithubIssueUrl(locator); + if (target && !target.api && target.url.protocol === "https:" && target.kind === "issues") return true; } } return false; @@ -218,16 +233,18 @@ export function trimLocatorPunctuation(locator: string, punctuation = "),.;"): s return locator.slice(0, end); } -/** Normalize only complete HTTP issue/provider locators, never substring identities. */ -function locators(value: unknown): string[] { +/** Extract source/reference locators; operational owner claims use complete explicit identity fields. */ +function locators(value: unknown, canonicalize = true): string[] { const text = typeof value === "string" ? value : JSON.stringify(value) ?? ""; - return [...text.matchAll(/https?:\/\/[^\s<>"'`]+/gi)].map(match => trimLocatorPunctuation(match[0]!).replace(/\/$/, "").toLowerCase() - .replace(/^https?:\/\/(?:www\.)?github\.com\/([^/]+)\/([^/]+)\/(?:issues|pull)\/(\d+)\/?(?:[?#].*)?$/, "https://github.com/$1/$2/issues/$3")); + return [...text.matchAll(/https?:\/\/[^\s<>"'`]+/gi)].map(match => { + const raw = trimLocatorPunctuation(match[0]!); + return canonicalize ? parseGithubIssueUrl(raw)?.canonical ?? raw.replace(/\/$/, "").toLowerCase() : raw; + }); } /** Copied body text and labels cannot transfer an established GitHub import to another owner. */ function copiedProviderMarkersOwnItem(item: PmItem): boolean { - return item.github_link_only !== false && !item.tags?.some(tag => /^gh:[^/]+\/[^#]+#\d+$/.test(tag)); + return item.github_link_only !== false && !githubOwnershipClaims(item).direct; } /** Extract declared provider fields and the description/body markers used by provider exporters. */ @@ -246,47 +263,143 @@ function providerProvenance(item: PmItem): string { .map(([, value]) => typeof value === "string" ? value : JSON.stringify(value)).join("\n"); } -/** Fields whose GitHub locators name an item's owner; shared by the owner matcher and the adoption guard so they cannot drift. */ +/** Link-bearing fields expose references; only explicit GitHub identity fields establish ownership. */ const OWNER_LINK_FIELD = /^(?:github_?url|provenance|external_?(?:links?|url)|links?)$/i; -/** - * True when an item already carries a structured GitHub or provider identity of its own. Mirrors every ownership source - * identity.ts and the owner matcher recognize: any `github*` field (`github_url`, `githuburl`, ...), a GitHub identity tag in any case (`gh:`, `GH:`), a core `external_ref`, a native `source_owner`, provider identity fields - * (`linear_*`/`jira_*`/`ado_*` id, key, url or identifier), provider provenance, a native "GitHub issue " - * declaration, or a GitHub issue URL in provenance/external links. - */ +/** Existing explicit provenance and provider declarations prevent opt-in citation adoption from replacing an owner. */ function hasStructuredOwnership(item: PmItem): boolean { - const issueLocator = /^https:\/\/github\.com\/[^/]+\/[^/]+\/issues\/\d+$/; const filled = (value: unknown) => value !== undefined && value !== null && value !== false && !(typeof value === "string" && value.trim() === ""); - return Object.entries(item).some(([key, value]) => filled(value) && (/^github/i.test(key) && !/^github_?link_?only$/i.test(key) || + return Object.entries(item).some(([key, value]) => filled(value) && (/^github/i.test(key) && !/^github_?(?:link_?only|url)$/i.test(key) || /^(?:external_ref|source_owner)$/.test(key) || /^(?:linear|jira|ado)_(?:.*_)?(?:id|key|url|identifier)$/i.test(key))) || - item.github_link_only === false || Boolean(providerProvenance(item)) || - Boolean(item.tags?.some(tag => /^gh:[^/\s]+\/[^#\s]+#[1-9]\d*$/i.test(tag))) || - (typeof item.description === "string" && /^GitHub issue(?:\s*:\s+|\s+)https?:\/\//i.test(item.description)) || - Object.entries(item).some(([key, value]) => OWNER_LINK_FIELD.test(key) && - locators(value).some(url => issueLocator.test(url))); + // Existence only: an unrelated item with contradictory aliases must warn elsewhere, never abort citation matching. + item.github_link_only === false || Boolean(providerProvenance(item)) || githubOwnershipClaims(item).owners.length > 0; +} + +/** Typed refusal for contradictory authoritative identities; references never enter this decision. */ +export class GithubIdentityConflictError extends Error { + /** Machine-readable refusal category for command callers. */ + readonly code = "github_identity_conflict"; + /** Treat contradictory operator-owned provenance as a usage conflict. */ + readonly exitCode = 2; + /** Distinct normalized explicit targets named by the item. */ + readonly identities: readonly string[]; + /** Correction to establish one owner while retaining contextual references. */ + readonly remedy = "Correct the explicit GitHub provenance to name one issue. Plain gh: tags are unrestricted references and never claims."; + /** Name both normalized identities and retain the correction instruction. */ + constructor(id: string | undefined, identities: readonly string[]) { + super(`GitHub identity conflict for ${id ?? ""}: ${identities.join(" versus ")}. Correct the explicit GitHub provenance to name one issue. Plain gh: tags are unrestricted references and never claims. No item was written.`); + this.identities = identities; + } +} + +/** Normalize the legacy link fields already accepted by first-link matching. */ +export function legacyGithubLinkClaims(item: PmItem): string[] { + return [...new Set(legacyGithubLinkIdentities(item).map(target => target.key))]; +} + +/** Extract GitHub references from supported links without promoting their prose to ownership. */ +export function githubLinkReferences(item: PmItem) { + const values = Object.entries(item).filter(([field]) => OWNER_LINK_FIELD.test(field)).flatMap(([, value]) => locators(value, false)); + return values.flatMap(value => { const parsed = parseGithubIssueUrl(value); return parsed ? [parsed] : []; }); +} + +/** Parse native declarations once so ownership and diagnostics share URL boundaries and number validation. */ +export function parseLegacyGithubDescription(item: PmItem) { + if (typeof item.description !== "string") return undefined; + const declaration = /^(?:GitHub issue(?:\s*:\s+|\s+)|GH (?:issue|PR) #(-?\d+):\s+)(https?:\/\/[^\s<>"'`]+)/i.exec(item.description); + if (!declaration) return undefined; + const target = parseGithubIssueUrl(trimLocatorPunctuation(declaration[2]!, "),.;:·")); + const stated = declaration[1] === undefined ? undefined : Number(declaration[1]); + const invalidNumber = !target || stated !== undefined && (!Number.isSafeInteger(stated) || stated <= 0 || stated !== target.number); + const identity = !invalidNumber && !target!.api ? target : undefined; + return { declared: declaration[1], target, identity, invalidNumber }; +} + +/** Read only a leading native declaration; later prose is not ownership evidence. */ +export function legacyGithubDescriptionIdentity(item: PmItem) { + return parseLegacyGithubDescription(item)?.identity; +} + +/** Collect complete URL fields and legacy native declarations; generic links remain references. */ +export function legacyGithubLinkIdentities(item: PmItem) { + const explicit = explicitGithubIdentities(item).map(target => ({ ...target, declaration: false })); + const description = legacyGithubDescriptionIdentity(item); + return [...explicit, ...(description ? [{ ...description, declaration: true }] : [])]; +} + +/** Resolve every supported explicit URL alias once, retaining the shared parser's issue/PR kind. */ +export function explicitGithubIdentities(item: PmItem) { + return Object.entries(item).flatMap(([field, value]) => { + if (!/^github_?url$/i.test(field)) return []; + const target = parseGithubIssueUrl(value); + return target && !target.api ? [{ ...target, field }] : []; + }); +} + +/** Ambiguous number-only provenance is diagnostic evidence, never import authority. */ +export interface LegacyGithubIdentityConflict { + code: "ambiguous_legacy_github_identity"; + item: string | undefined; + candidates: string[]; + remedy: string; +} + +/** Retained compatibility receipt; reference tags never imply ownership or conflicts. */ +export function legacyGithubIdentityConflict(_item: PmItem): LegacyGithubIdentityConflict | undefined { + // Reference tags cannot supply or contradict a missing repository. + return undefined; +} + +/** Resolve persisted identity with legacy compatibility; diagnostics may report conflicts that all write planners refuse. */ +export function importIdentityKeys(item: PmItem, options: { allowConflicts?: boolean } = {}): string[] { + const keys = new Set(); + for (const target of explicitGithubIdentities(item)) keys.add(target.key); + const urlKey = [...keys][0]; + const repository = normalizeGithubRepository(item.github_repo) ?? urlKey?.split("#")[0]; + if (repository && Number.isSafeInteger(item.github_number) && Number(item.github_number) > 0) keys.add(`${repository}#${item.github_number}`); + if (keys.size > 1 && !options.allowConflicts) throw new GithubIdentityConflictError(item.id, [...keys].sort()); + + return [...keys]; +} + +/** Creation provenance grants mirroring; source identity and item naming are checked separately. */ +export function importerCreatedItem(item: PmItem, _repo: string, _number: number): boolean { + return item.author === "pm-github" || item.github_import === true; +} + +/** Index durable importer identities while allowing arbitrary shared reference tags. */ +export function indexByImportIdentity(items: readonly PmItem[], ownership = buildGithubOwnershipIndex(items), targets?: ReadonlySet): Map { + const result = new Map(); + for (const item of items) if (item.id && (!targets || ownershipStateOf(ownership, item).claims.some(key => targets.has(key)))) for (const key of githubOwnedIdentityKeys(item, ownership)) if (!targets || targets.has(key)) result.set(key, item); + return result; } /** Match GitHub locators or provider back-links before creation, refusing multiple owners. */ -export function matchImportedIssue(items: readonly PmItem[], issue: GhIssue, tagged?: PmItem, adoptCited = false): ImportMatch { +export function matchImportedIssue(items: readonly PmItem[], issue: GhIssue, _tagged?: PmItem, adoptCited = false, snapshot?: GithubOwnershipIndex): ImportMatch { const issueUrl = locators(issue.html_url)[0]!; const bodyUrls = new Set(locators(issue.body ?? "")); + const repo = parseGithubIssueUrl(issue.html_url)!.repo; + const issueKey = `${repo}#${issue.number}`; + const ownership = snapshot ?? buildGithubOwnershipIndex(items, [repo]); /** Recognize leading native ownership declarations; later prose cannot transfer ownership. */ - const declaredOwner = (item: PmItem) => copiedProviderMarkersOwnItem(item) && typeof item.description === "string" && - locators(/^GitHub issue(?:\s*:\s+|\s+)(https?:\/\/\S+)/i.exec(item.description)?.[1]).includes(issueUrl); - const candidates = items.filter(item => item.id && (item.id === tagged?.id || - declaredOwner(item) || - Object.entries(item).some(([key, value]) => OWNER_LINK_FIELD.test(key) && locators(value).includes(issueUrl)) || - locators(providerProvenance(item)).some(url => bodyUrls.has(url)))); - if (candidates.length > 1) throw new CommandError(`GitHub issue #${issue.number} has multiple existing owners; import plan is not idempotent. No item was written for this issue.`); + const declaredOwner = (item: PmItem) => copiedProviderMarkersOwnItem(item) && + legacyGithubDescriptionIdentity(item)?.canonical === issueUrl; + const candidates = items.filter(item => { + if (!item.id) return false; + const state = ownershipStateOf(ownership, item); + if (state.claims.includes(issueKey)) assertGithubOwnershipConsistency(item, { id: item.id, repo, number: issue.number }, ownership); + if (state.owners.length) return state.owners.includes(issueKey); + return locators(providerProvenance(item)).some(url => !parseGithubIssueUrl(url) && bodyUrls.has(url)); + }); + if (candidates.length > 1) throw new GithubImportOwnershipError(candidates[0]!.id!, issueKey, candidates.map(candidate => `${issueKey} claimed by ${candidate.id}`)); let item = candidates[0]; let cited = false; const incidental: CitationCandidate[] = []; if (!item && !issue.pull_request) { - const repo = issue.html_url.split("/").slice(3, 5).join("/").toLowerCase(); + const repo = parseGithubIssueUrl(issue.html_url)!.repo; const citing: PmItem[] = []; for (const candidate of items) { - if (!candidate.id || candidate.tags?.some(tag => parseProvenanceTag(tag) !== undefined)) continue; + if (!candidate.id) continue; const assessment = citesIssue(candidate, repo, issue.number); if (assessment === undefined) continue; // Structured ownership (a GitHub issue URL field, an issue number, provider provenance or a native declaration) @@ -304,7 +417,7 @@ export function matchImportedIssue(items: readonly PmItem[], issue: GhIssue, tag const native = item && declaredOwner(item); return { item, linkOnly: Boolean(item && (provider || native || item.github_link_only === true)), ...(incidental.length > 0 ? { citation_candidates: incidental } : {}), - via: cited ? "citation" : item ? (provider ? "provider-link" : native ? "github-declaration" : item.id === tagged?.id ? "provenance-tag" : "github-url") : "new" }; + via: cited ? "citation" : item ? (provider ? "provider-link" : native ? "github-declaration" : item.github_url ? "github-url" : "import-provenance") : "new" }; } /** Read calendar release tags from the selected tracker checkout, including annotated tag dates. */ @@ -370,10 +483,9 @@ export function importCloseReason(issue: GhIssue, events: readonly unknown[]): s const records = events.filter((event): event is Record => event !== null && typeof event === "object" && !Array.isArray(event)); const closed = latestClosure(records, issue.closed_at); if (closed && typeof closed.commit_id === "string" && /^[a-f0-9]{40}$/i.test(closed.commit_id)) { - const linkedCommit = typeof closed.commit_url === "string" - ? /^https:\/\/api\.github\.com\/repos\/([^/]+)\/([^/]+)\/commits\/([a-f0-9]{40})$/.exec(closed.commit_url) : null; - const repoUrl = linkedCommit && linkedCommit[3] === closed.commit_id - ? `https://github.com/${linkedCommit[1]}/${linkedCommit[2]}` : issue.html_url.replace(/\/(?:issues|pull)\/\d+$/, ""); + const linkedCommit = parseGithubUrl(closed.commit_url); + const repoUrl = linkedCommit?.api && linkedCommit.kind === "commits" && linkedCommit.value === closed.commit_id && !linkedCommit.tail.length + ? `https://github.com/${linkedCommit.repo}` : `https://github.com/${parseGithubIssueUrl(issue.html_url)!.repo}`; return `${base}; closing commit ${repoUrl}/commit/${closed.commit_id}`; } const graphClosed = latestClosure(records, issue.closed_at, true); @@ -385,12 +497,13 @@ export function importCloseReason(issue: GhIssue, events: readonly unknown[]): s const closer = event.__typename === "ClosedEvent" ? event.closer : event.subject; const fields = closer && typeof closer === "object" ? closer as Record : undefined; const url = ownValue(fields, "url"); - if (typeof url === "string" && /^https:\/\/github\.com\/[^/]+\/[^/]+\/(pull\/\d+|commit\/[a-f0-9]{40})$/.test(url)) { + const target = parseGithubUrl(url); + if (target && !target.api && !target.tail.length && (target.kind === "pull" && Boolean(parseGithubIssueUrl(url)) || target.kind === "commit" && /^[a-f0-9]{40}$/.test(target.value ?? ""))) { const title = Object.hasOwn(fields!, "title") ? ownValue(fields, "title") : ownValue(fields, "messageHeadline"); - return `${base}; ${event.__typename === "ClosedEvent" ? "closing" : "linked"} ${url.includes("/pull/") ? "PR" : "commit"} ${url}${typeof title === "string" ? ` (${JSON.stringify(title)})` : ""}`; + return `${base}; ${event.__typename === "ClosedEvent" ? "closing" : "linked"} ${target.kind === "pull" ? "PR" : "commit"} ${url}${typeof title === "string" ? ` (${JSON.stringify(title)})` : ""}`; } } - const connected = records.find(event => event.event === "connected" && typeof event.url === "string" && /^https:\/\/github\.com\/[^/]+\/[^/]+\/pull\/\d+$/.test(event.url)); + const connected = records.find(event => event.event === "connected" && parseGithubIssueUrl(event.url)?.kind === "pull"); return connected ? `${base}; linked PR ${connected.url}` : base; } @@ -420,7 +533,17 @@ export function buildImportFidelity(issue: GhIssue, releases: readonly ImportRel } /** Project the import-owned metadata; undefined values remove stale native lifecycle fields. */ -export function importFidelityFields(fidelity: ImportFidelity, linkOnly: boolean): Record { +export function importFidelityFields(fidelity: ImportFidelity, linkOnly: boolean, curated?: PmItem): Record { + if (curated && !linkOnly) { + // An issue can predate its local delivery tracker. Retain its exact source + // closure in github_closed_at without inventing an impossible native lifetime. + const nativeClosure = fidelity.github_closed_at && Date.parse(fidelity.github_closed_at) >= Date.parse(typeof curated.created_at === "string" ? curated.created_at : ""); + return { + ...fidelity, github_link_only: false, + ...(fidelity.github_state === "open" ? { closed_at: undefined } : nativeClosure ? { closed_at: fidelity.github_closed_at } : {}), + ...(!curated.completed_at && nativeClosure ? { completed_at: fidelity.github_closed_at } : {}), + }; + } return { ...fidelity, github_link_only: linkOnly, ...(!linkOnly ? { @@ -479,17 +602,21 @@ export function normalizeImportValue(field: string, value: unknown, types = defa * Cross-provider owners retain their creation/completion dates, content and release. * Plain imports restore a source assignee that a native close command clears. */ -export async function persistImportFidelity(pmPath: string, id: string, fidelity: ImportFidelity, linkOnly: boolean, normalizeFields = false, sourceAssignee?: string): Promise { +export async function persistImportFidelity(pmPath: string, id: string, fidelity: ImportFidelity, linkOnly: boolean, normalizeFields = false, sourceAssignee?: string, preserveContent = false, ownership?: GithubOwnedTarget): Promise { const workspace = await readGithubWorkspace(pmPath); const pmRoot = workspace.pmRoot; const { settings } = workspace; id = normalizeItemId(id, settings.id_prefix); - await mutateItem({ pmRoot, settings, id, author: "pm-github", op: "import", skipNoop: true, + const source = parseGithubIssueUrl(fidelity.github_url)!; + const target = { ...ownership, id, repo: source.repo, number: fidelity.github_number }; + await withGithubImportOwners(pmRoot, [target], () => mutateItem({ pmRoot, settings, id, author: "pm-github", op: "import", skipNoop: true, message: `Preserve GitHub source lifecycle for issue #${fidelity.github_number}`, mutate: document => { + const source = parseGithubIssueUrl(fidelity.github_url)!; + assertGithubImportOwner({ ...document.metadata, body: document.body }, { ...ownership, id, repo: source.repo, number: fidelity.github_number }); const before = canonicalDocument(document); - const fields = { ...importFidelityFields(fidelity, linkOnly), - ...(!linkOnly && sourceAssignee ? { assignee: sourceAssignee } : {}), + const fields = { ...importFidelityFields(fidelity, linkOnly, preserveContent ? document.metadata : undefined), + ...(!linkOnly && !preserveContent && sourceAssignee ? { assignee: sourceAssignee } : {}), ...(!linkOnly ? { status: normalizeStatusInput(mapState(fidelity.github_state, fidelity.github_state_reason), workspace.statuses) } : {}) }; for (const [field, value] of Object.entries(fields)) { const current = ownValue(document.metadata, field); @@ -512,5 +639,5 @@ export async function persistImportFidelity(pmPath: string, id: string, fidelity }, }; }, - }); + })); } diff --git a/import-ownership.ts b/import-ownership.ts new file mode 100644 index 00000000..5cf3048b --- /dev/null +++ b/import-ownership.ts @@ -0,0 +1,352 @@ +import { createHash } from "node:crypto"; +/** Positive ownership checks at planning and the SDK's locked mutation boundary. */ +import { AsyncLocalStorage, AsyncResource } from "node:async_hooks"; +import { + PmClient, acquireLockLease, listAllComplete, certifyCompleteListResult, locateItem, readLocatedItem, hashDocument, EXIT_CODE, PmCliError, getActiveExtensionRegistrations, runWithActiveExtensions, setActiveExtensionHooks, + setActiveExtensionRegistrations, runActiveBeforeMutationHooks, runActiveOnReadHooks, + runActiveOnWriteHooks, runActiveOnIndexHooks, +} from "@unbrained/pm-cli/sdk"; +import { normalizeGithubClaim, normalizeGithubRepository, parseGithubIssueUrl } from "./github-url.ts"; +import { importIdentityKeys, parseLegacyGithubDescription, legacyGithubLinkIdentities, explicitGithubIdentities, importerCreatedItem } from "./import-fidelity.ts"; +import { readGithubWorkspace } from "./workspace.ts"; +import type { PmItem } from "./index.ts"; + +const OWNER_REMEDY = "Restore the intended GitHub provenance only if this item belongs to that issue; otherwise use a different configured ID prefix or tracker. Keep the existing item and history. Reference tags do not grant ownership."; + +/** Refuse an occupied target instead of treating an external-key ID as permission. */ +export class GithubImportOwnershipError extends PmCliError { + /** Stable category for automation and live collision refusals. */ + readonly code = "github_import_owner_conflict"; + /** Treat an occupied target as a resolvable usage conflict. */ + readonly exitCode = 2; + /** Existing item whose destination cannot be reused. */ + readonly itemId: string; + /** Normalized source identity requested by this operation. */ + readonly expected: string; + /** Authoritative identities currently attached to the target. */ + readonly owners: readonly string[]; + /** Explicit recovery instruction; never silently reassign a colliding ID. */ + readonly remedy = OWNER_REMEDY; + /** Name the requested identity and every current owner without quoting item content. */ + constructor(itemId: string, expected: string, owners: readonly string[]) { + super(`GitHub import target ${itemId} is reserved for ${expected}, but its current owner is ${owners.join(" versus ") || "curated/unowned"}. ${owners.some(owner => owner.includes("claimed by")) ? "multiple existing owners; import plan is not idempotent. " : ""}${OWNER_REMEDY} No conflicting item was written.`, 2, { code: "github_import_owner_conflict", required: OWNER_REMEDY }); + this.itemId = itemId; this.expected = expected; this.owners = owners; + } +} + +/** One planned target; an explicit first-link decision may establish its initial provenance. */ +export interface GithubOwnedTarget { + id: string; + repo: string; + number: number; + allowCreate?: boolean; + /** Full content updates require durable creation provenance, even on replay. */ + mirrorContent?: boolean; + /** Minimum-SDK revision fencing is evaluated inside the native item lock. */ + expectedHash?: string; + initialLink?: (item: PmItem) => boolean; + /** Dependency endpoints must retain their unique ownership inside the source lock. */ + related?: readonly GithubOwnedTarget[]; +} + +/** Use the same repository hash for import destinations and legacy identity verification. */ +export function githubRepositoryHash(repo: string): string { + return createHash("sha256").update(repo.toLowerCase()).digest("hex").slice(0, 12); +} + +/** Reverse only a proven importer ID. Tags provide repository candidates, never claims or issue numbers. */ +export function githubImporterIdentity(item: PmItem, repositories: readonly string[] = []): string | undefined { + const id = typeof item.id === "string" && /(?:^|-)github-([a-f0-9]{12})-([1-9]\d*)$/.exec(item.id); + if (!id || !Number.isSafeInteger(Number(id[2])) || !importerCreatedItem(item, "", Number(id[2]))) return undefined; + const candidates = new Set(repositories.map(repo => repo.toLowerCase())); + const repo = normalizeGithubRepository(item.github_repo); if (repo) candidates.add(repo); + for (const target of [...explicitGithubIdentities(item), ...legacyGithubLinkIdentities(item)]) candidates.add(target.repo); + const external = normalizeGithubClaim(item.external_ref); if (external) candidates.add(external.split("#")[0]!); + const generated = typeof item.description === "string" && /^GH (?:issue|PR) #[1-9]\d*: (https?:\/\/\S+)(?: ·|$)/i.exec(item.description); + const source = generated && parseGithubIssueUrl(generated[1]); if (source) candidates.add(source.repo); + for (const tag of item.tags ?? []) { + const candidate = /^gh:([^/\s]+\/[^#\s]+)#/i.exec(tag)?.[1]; + if (candidate) candidates.add(candidate.toLowerCase()); + } + for (const candidate of candidates) if (githubRepositoryHash(candidate) === id[1]) return `${candidate}#${id[2]}`; + return undefined; +} + +/** One parsed corpus snapshot, shared by every selected target in a batch. */ +export interface GithubOwnershipIndex { + items: readonly PmItem[]; + repositories: string[]; + states: WeakMap>; + byId: Map; + claimants: Map>; +} + +/** Optional deterministic work counters; production callers need no observer. */ +export interface GithubOwnershipWork { corpusReads?: number; claimParses?: number } + +const ownershipWork = new AsyncLocalStorage(); +// Nested fidelity writes inside one SDK transaction inherit its already installed guard. +// Each store carries a mutable `active` flag that is cleared when its scope ends. AsyncLocalStorage also +// propagates the store into detached work that outlives the scope, and that work must not reuse a released +// lease or the closed owner hooks. +const ownershipScope = new AsyncLocalStorage<{ root: string; targets: readonly GithubOwnedTarget[]; active: boolean }>(); + +/** Count actual claim parses and corpus reads without substituting SDK persistence. */ +export function withGithubOwnershipWork(work: GithubOwnershipWork, run: () => T): T { + return ownershipWork.run(work, run); +} + +/** Certify the complete native corpus before any owner index can authorize a write. */ +async function readGithubOwnerItems(pmRoot: string): Promise { + const listed = await listAllComplete({ includeBody: true }, { pmRoot, noExtensions: true }); + certifyCompleteListResult(listed); + return listed.items; +} + +/** Read one complete native metadata snapshot for a batch or a fresh write check. */ +export async function readGithubOwnershipSnapshot(pmRoot: string, repositories: readonly string[] = []): Promise { + const { pmRoot: root } = await readGithubWorkspace(pmRoot); + const work = ownershipWork.getStore(); if (work) work.corpusReads = (work.corpusReads ?? 0) + 1; + return buildGithubOwnershipIndex(await readGithubOwnerItems(root), repositories); +} + +/** Parse each item once and index peer claims in linear snapshot work. */ +export function buildGithubOwnershipIndex(items: readonly PmItem[], repositories: readonly string[] = []): GithubOwnershipIndex { + const index: GithubOwnershipIndex = { items, repositories: [...repositories], states: new WeakMap(), byId: new Map(), claimants: new Map() }; + for (const item of items) { + const state = githubOwnershipClaims(item, false, false, repositories); + index.states.set(item, state); + if (item.id) index.byId.set(item.id, item); + for (const key of state.claims) { + const peers = index.claimants.get(key) ?? new Map(); + if (item.id) peers.set(item.id, state.claims); + index.claimants.set(key, peers); + } + } + return index; +} + +/** Preserve array-based callers while letting batch paths share one already parsed index. */ +function ownershipIndex(peers: readonly PmItem[] | GithubOwnershipIndex, repositories: readonly string[] = []): GithubOwnershipIndex { + return Array.isArray(peers) ? buildGithubOwnershipIndex(peers, repositories) : peers as GithubOwnershipIndex; +} + +/** Diagnose malformed native numbers and conflicts outside the operation without selecting those items. */ +export function warnGithubOwnershipScope(index: GithubOwnershipIndex, selected: readonly PmItem[]): void { + const scope = new Set(selected.map(item => item.id)); + for (const item of index.items) { + const declaration = parseLegacyGithubDescription(item); + const source = declaration?.target; + const external = typeof item.external_ref === "string" ? /^gh:[^/\s]+\/[^#\s]+#(\d+)$/i.exec(item.external_ref) : null; + const suffix = typeof item.id === "string" ? /(?:^|-)github-[a-f0-9]{12}-(\d+)$/.exec(item.id) : null; + if (declaration?.invalidNumber || suffix && (!Number.isSafeInteger(Number(suffix[1])) || Number(suffix[1]) < 1) || external && (!Number.isSafeInteger(Number(external[1])) || Number(external[1]) < 1)) { + console.error(JSON.stringify({ code: "invalid_github_identity_number", item: item.id, declared: declaration?.declared, url: source ? source.key : undefined, suffix: suffix?.[1], external_number: external?.[1], remedy: "Make every declared issue number a matching positive safe integer; invalid declarations do not establish ownership." })); + } + const state = ownershipStateOf(index, item); + if (!scope.has(item.id) && state.claims.length > 1) console.error(JSON.stringify({ code: "out_of_scope_github_owner_conflict", items: [item.id], claims: state.claims })); + } + for (const [key, claimants] of index.claimants) { + const ids = [...claimants.keys()].sort(); + if (ids.length > 1 && ids.every(id => !scope.has(id))) console.error(JSON.stringify({ code: "out_of_scope_github_owner_conflict", items: ids, identity: key })); + } +} + +/** + * Read an item's ownership state from the index, deriving it for a separately allocated copy. + * The states map is keyed by object identity, so a caller that passes a copy of a corpus record would otherwise read + * it as unowned and skip every guard that depends on its claims. + */ +export function ownershipStateOf(index: Pick, item: PmItem): ReturnType { + return index.states.get(item) ?? githubOwnershipClaims(item, false, false, index.repositories); +} +/** Every asserted identity constrains a provenance-bearing item; references alone do not create an owner. */ +export function githubOwnershipClaims(item: PmItem, provisional = false, _allowReferenceTags = false, repositories: readonly string[] = []): { owners: string[]; claims: string[]; evidence: boolean; kinds: string[]; direct: boolean } { + const work = ownershipWork.getStore(); if (work) work.claimParses = (work.claimParses ?? 0) + 1; + const direct = importIdentityKeys(item, { allowConflicts: true }); + const external = normalizeGithubClaim(item.external_ref); + const imported = githubImporterIdentity(item, repositories); + // Explicit/link-only ownership and verified imports retain precedence over copied description markers. + const hasDirect = direct.length > 0 || Boolean(external) || Boolean(imported); + const legacy = legacyGithubLinkIdentities(item).filter(target => !target.declaration || item.github_link_only === true || !hasDirect); + const owners = new Set([...direct, ...legacy.map(target => target.key)]); + const kinds = new Set([...explicitGithubIdentities(item), ...legacy].map(target => target.kind!)); + const externalTarget = parseGithubIssueUrl(item.external_ref); + if (externalTarget && !externalTarget.api) kinds.add(externalTarget.kind!); + if (external) owners.add(external); + if (imported) { + owners.add(imported); + const generated = typeof item.description === "string" && /^GH (?:issue|PR) #[1-9]\d*: (https?:\/\/\S+)(?: ·|$)/i.exec(item.description); + const source = generated && parseGithubIssueUrl(generated[1]); + if (source && !source.api && source.key === imported) kinds.add(source.kind!); + } + const repository = normalizeGithubRepository(item.github_repo); + const number = Number.isSafeInteger(item.github_number) && Number(item.github_number) > 0 ? Number(item.github_number) : undefined; + const evidence = provisional || owners.size > 0; + const claims = new Set(owners); + if (evidence) { + const keys = [...claims]; + for (const key of keys) { + const [repo, n] = key.split("#"); + if (repository) claims.add(`${repository}#${n}`); + if (number !== undefined) claims.add(`${repo}#${number}`); + } + } + return { owners: [...owners].sort(), claims: [...claims].sort(), evidence, kinds: [...kinds].sort(), direct: direct.length > 0 || Boolean(external) || Boolean(imported) }; +} + +/** Refuse contradictory claims and competing owners from the same current metadata snapshot. */ +export function assertGithubOwnershipConsistency(item: PmItem, target?: GithubOwnedTarget, peers: readonly PmItem[] | GithubOwnershipIndex = [], _allowReferenceTags = false): string[] { + const index = ownershipIndex(peers, target ? [target.repo] : []); + const state = index.states.get(item) ?? githubOwnershipClaims(item, Boolean(target?.initialLink), false, index.repositories); + const expected = target ? `${target.repo.toLowerCase()}#${target.number}` : state.owners[0] ?? state.claims[0] ?? "one consistent GitHub issue"; + if (state.claims.length > 1 || state.evidence && state.claims.some(key => key !== expected)) { + throw new GithubImportOwnershipError(item.id ?? target?.id ?? "", expected, state.claims); + } + const competitors = [...(index.claimants.get(expected) ?? [])].flatMap(([id, claims]) => id !== item.id ? claims.map(key => `${key} claimed by ${id}`) : []); + if (competitors.length) throw new GithubImportOwnershipError(item.id ?? target?.id ?? "", expected, [expected, ...competitors]); + return state.owners; +} + +/** Select targets only after checking every claim and every peer; repository scoping never hides a contradiction. */ +export function githubOwnedIdentityKeys(item: PmItem, peers: readonly PmItem[] | GithubOwnershipIndex = [], repo?: string, _allowReferenceTags = false): string[] { + const index = ownershipIndex(peers, repo ? [repo] : []); + const state = ownershipStateOf(index, item); + if (repo && !state.claims.some(key => key.slice(0, key.lastIndexOf("#")) === repo.toLowerCase())) return []; + return assertGithubOwnershipConsistency(item, undefined, index); +} + +/** Require one matching owner, consistent claims and unique current authority before every write. */ +export function assertGithubImportOwner(item: PmItem, target: GithubOwnedTarget, peers: readonly PmItem[] | GithubOwnershipIndex = []): void { + const expected = `${target.repo.toLowerCase()}#${target.number}`; + const owners = assertGithubOwnershipConsistency(item, target, peers); + if (target.mirrorContent && !importerCreatedItem(item, target.repo, target.number)) { + throw new GithubImportOwnershipError(target.id, expected, [...owners, "curated/unowned for content mirroring; importer creation provenance missing"]); + } + if (owners.length === 1 && owners[0] === expected) return; + if (owners.length === 0 && target.initialLink?.(item)) return; + throw new GithubImportOwnershipError(target.id, expected, owners); +} + +/** Read ownership and the revision used to fence a following native write from the same snapshot. */ +export async function readGithubImportOwner(pmRoot: string, target: GithubOwnedTarget, snapshot?: GithubOwnershipIndex): Promise { + const { pmRoot: root, settings, types } = await readGithubWorkspace(pmRoot); + const peers = snapshot ?? buildGithubOwnershipIndex(await readGithubOwnerItems(root), [target.repo]); + const located = await locateItem(root, target.id, settings.id_prefix, settings.item_format, types.type_to_folder); + if (!located) { + if (!target.allowCreate) throw new PmCliError(`Item ${target.id} not found. GitHub import requires its intended item and history; restore them before retrying.`, EXIT_CODE.NOT_FOUND, { code: "item_not_found" }); + assertGithubOwnershipConsistency({ id: target.id }, target, peers); + return undefined; + } + const { document } = await readLocatedItem(located, { schema: settings.schema }); + assertGithubImportOwner({ ...document.metadata, body: document.body }, target, + peers); + return hashDocument(document); +} + +const ownershipLeaseScope = new AsyncLocalStorage>(); + +/** Serialize the tracker ownership namespace before SDK workspace/item locks; nested writes reuse the lease while it is held. */ +export async function withGithubOwnershipLock(pmRoot: string, run: () => Promise): Promise { + const { pmRoot: root, settings } = await readGithubWorkspace(pmRoot); + const active = ownershipLeaseScope.getStore(); + if (active?.get(root)?.active) return run(); + const lease = await acquireLockLease(root, "github-ownership", settings.locks.ttl_seconds, "pm-github:ownership", false, false, settings.locks.wait_ms); + const held = { active: true }; + const scope = new Map([...(active ?? [])].filter(([, marker]) => marker.active)); + scope.set(root, held); + try { return await lease.fence(() => ownershipLeaseScope.run(scope, run)); } + finally { held.active = false; await lease.release(); } +} + +/** Coordinate all ownership-changing paths before entering their native mutation guards. */ +export async function withGithubImportOwners(pmRoot: string, targets: readonly GithubOwnedTarget[], run: () => Promise, work?: GithubOwnershipWork): Promise { + return withGithubOwnershipLock(pmRoot, () => withGithubImportOwnersUnderLease(pmRoot, targets, run, work)); +} + +/** Infer the mutation callback from the public SDK so the declared minimum remains supported. */ +type GithubOwnershipMutationHook = NonNullable[0]>["beforeMutation"]>[number]["run"]; + +/** Preserve ambient delegates and registrations while installing one native ownership check. */ +async function withGithubOwnershipHooks(pmRoot: string, name: string, delegateName: string, check: GithubOwnershipMutationHook, run: () => Promise): Promise { + const prior = AsyncResource.bind(runActiveBeforeMutationHooks); + const onRead = AsyncResource.bind(runActiveOnReadHooks), onWrite = AsyncResource.bind(runActiveOnWriteHooks), onIndex = AsyncResource.bind(runActiveOnIndexHooks); + const registrations = getActiveExtensionRegistrations(); + return runWithActiveExtensions({ path: pmRoot, noExtensions: true }, async () => { + setActiveExtensionRegistrations(registrations); + setActiveExtensionHooks({ beforeCommand: [], afterCommand: [], + onRead: [{ layer: "project", name: delegateName, run: async context => { await onRead(context); } }], + onWrite: [{ layer: "project", name: delegateName, run: async context => { await onWrite(context); } }], + onIndex: [{ layer: "project", name: delegateName, run: async context => { await onIndex(context); } }], + beforeMutation: [{ layer: "project", name, run: async context => { await prior(context); return check(context); } }], + }); + return run(); + }); +} + +/** Preserve ambient hooks while adding positive ownership checks inside every target item lock. */ +async function withGithubImportOwnersUnderLease(pmRoot: string, targets: readonly GithubOwnedTarget[], run: () => Promise, work?: GithubOwnershipWork): Promise { + const active = ownershipScope.getStore(); + if (active?.active && active.root === pmRoot && targets.every(target => active.targets.some(outer => outer.id === target.id && + outer.repo.toLowerCase() === target.repo.toLowerCase() && outer.number === target.number && + (!target.mirrorContent || outer.mirrorContent) && (!target.expectedHash || target.expectedHash === outer.expectedHash) && !target.related?.length))) return run(); + const { pmRoot: root } = await readGithubWorkspace(pmRoot); + const corpus = await readGithubOwnerItems(root); + const observed = ownershipWork.getStore(); if (observed && observed !== work) observed.corpusReads = (observed.corpusReads ?? 0) + 1; + if (work) work.corpusReads = (work.corpusReads ?? 0) + 1; + const snapshot = buildGithubOwnershipIndex(corpus, targets.map(target => target.repo)); + // Fail the whole selected batch before even creating an SDK journal. + for (const target of targets) await readGithubImportOwner(pmRoot, target, snapshot); + const byId = new Map(targets.map(target => [target.id, target])); + const scope = { root, targets, active: true }; + return withGithubOwnershipHooks(pmRoot, "github-import-owner", "github-owner-delegate", async context => { + const id = (context.before ?? context.after)?.metadata.id; + const target = id && byId.get(id); + if (target) { + try { + if (target.expectedHash && context.before && hashDocument(context.before) !== target.expectedHash) { + return { allow: false, code: "item_revision_conflict", message: `GitHub import target ${target.id} changed after its ownership read (expected revision ${target.expectedHash}, current ${hashDocument(context.before)}).`, remediation: "Read the current item and retry against its intended ownership and revision." }; + } + if (context.after && context.after.metadata.id !== target.id) throw new GithubImportOwnershipError(target.id, `${target.repo.toLowerCase()}#${target.number}`, ["mutation changed or removed the target id"]); + const peers = await readGithubOwnerItems(root).then(items => { + if (work) work.corpusReads = (work.corpusReads ?? 0) + 1; + if (observed && observed !== work) observed.corpusReads = (observed.corpusReads ?? 0) + 1; + return buildGithubOwnershipIndex(items, targets.map(target => target.repo)); + }); + for (const related of target.related ?? []) { + const item = peers.byId.get(related.id); + if (!item) throw new GithubImportOwnershipError(related.id, `${related.repo.toLowerCase()}#${related.number}`, ["missing dependency endpoint"]); + assertGithubImportOwner(item, related, peers); + } + for (const document of [context.before, context.after]) if (document) { + assertGithubImportOwner({ ...document.metadata, body: document.body }, target, peers); + } + } catch (error: unknown) { + if (!(error instanceof GithubImportOwnershipError)) throw error; + return { allow: false, code: error.code, message: error.message, remediation: error.remedy }; + } + } + }, () => ownershipScope.run(scope, async () => { try { return await run(); } finally { scope.active = false; } })); +} + +/** Run plain owned writes through native SDK locks, revisions and the same ownership hook as replay. */ +export async function mutateGithubOwnedItem(pmRoot: string, target: GithubOwnedTarget, action: "create" | "update" | "close", options: Record, expectedHash?: string): Promise { + const hash = expectedHash ?? await readGithubImportOwner(pmRoot, target); + return withGithubImportOwners(pmRoot, [{ ...target, expectedHash: hash }], async () => { + const client = PmClient.forActiveExtensionHost({ pmRoot, noExtensions: true }); + return client.run(action, action === "create" + ? { options: { ...options, id: target.id } } : { id: target.id, options }); + }); +} + + +/** Guard board-selected native changes against the complete owner corpus while allowing genuinely unowned items. */ +export async function withGithubItemConsistency(pmRoot: string, id: string, expected: readonly string[], run: () => Promise): Promise { + return withGithubOwnershipLock(pmRoot, () => withGithubOwnershipHooks(pmRoot, "github-board-owner", "github-board-owner-delegate", async context => { + if ((context.before ?? context.after)?.metadata.id !== id) return; + const peers = buildGithubOwnershipIndex(await context.sdk.list(), expected.map(key => key.split("#")[0]!)); + for (const document of [context.before, context.after]) if (document) { + const keys = assertGithubOwnershipConsistency({ ...document.metadata, body: document.body }, undefined, peers); + if (JSON.stringify(keys) !== JSON.stringify(expected)) throw new GithubImportOwnershipError(id, expected.join(" versus ") || "original unowned project item", keys); + } + }, run)); +} diff --git a/import-tags.ts b/import-tags.ts index 9b832c7b..5be7b8b2 100644 --- a/import-tags.ts +++ b/import-tags.ts @@ -16,7 +16,7 @@ export function nativeTagAdditions(current: readonly string[] | undefined, desir /** Resolve a prepared or replayed import while preserving native tags retained from an existing owner. */ export function importTagValues(entry: PreparedGithubImport): string[] { - const retained = entry.linkOnly || entry.identity?.via === "citation" ? toImportTags(entry.match?.tags) : []; + const retained = entry.linkOnly || entry.preserveContent || entry.identity?.via === "citation" ? toImportTags(entry.match?.tags) : []; const native = new Set(retained); return toImportTags([...retained, ...githubLabelsToTags(entry.tags.filter(tag => !native.has(toImportTags([tag])[0]!)))]); } @@ -24,7 +24,7 @@ export function importTagValues(entry: PreparedGithubImport): string[] { /** Preserve existing owner tags by adding only new provider-derived atoms instead of serializing native state as CSV. */ export function importTagOptions(entry: PreparedGithubImport): { tags?: string; addTags?: string[] } { const tags = importTagValues(entry); - if (entry.match?.id && (entry.linkOnly || entry.identity?.via === "citation")) { + if (entry.match?.id && (entry.linkOnly || entry.preserveContent || entry.identity?.via === "citation")) { return { addTags: nativeTagAdditions(entry.match.tags, tags).map(tag => serializeImportTags([tag])) }; } return { tags: serializeImportTags(tags) }; diff --git a/index.ts b/index.ts index 345a19ec..be0ff609 100644 --- a/index.ts +++ b/index.ts @@ -1,3 +1,6 @@ +import { writeGithubStateAtomic, syncGithubStatePath, type GithubStateWriteOptions } from "./durable-state.ts"; +import { buildGithubOwnershipIndex, warnGithubOwnershipScope, withGithubItemConsistency, readGithubOwnershipSnapshot, githubRepositoryHash, githubOwnershipClaims, githubOwnedIdentityKeys, assertGithubOwnershipConsistency, assertGithubImportOwner, GithubImportOwnershipError, readGithubImportOwner, withGithubImportOwners, mutateGithubOwnedItem, type GithubOwnershipIndex, type GithubOwnedTarget, ownershipStateOf } from "./import-ownership.ts"; +import { parseGithubIssueUrl, githubUrlTokens } from "./github-url.ts"; import { readGithubWorkspace, canonicalGithubStatus, githubStatusKind, githubStatusIsTerminal, type GithubStatusRegistry } from "./workspace.ts"; import { ownValue } from "./own-record.ts"; import { uniqueGithubLabelNames } from "./label-names.ts"; @@ -34,7 +37,7 @@ import { compileIssueForm, completeIssueForm, readRepositoryIssueForm, refuseIss export { compileIssueForm, completeIssueForm, readRepositoryIssueForm } from "./issue-forms.ts"; export type { IssueFormInput, IssueFormSelection, IssueFormContract, IssueFormReport, IssueFormCompletion, IssueFormRefusalCode, FormField, FormRepair, IssueFormReader } from "./issue-forms.ts"; import { - AmbiguousCitationError, buildImportFidelity, fetchImportTimeline, importFidelityFields, matchImportedIssue, normalizeImportValue, persistImportFidelity, readImportReleases, selectImportContextIndex, serializeImportTags, validateImportLifetime, + AmbiguousCitationError, type LegacyGithubIdentityConflict, indexByImportIdentity, importerCreatedItem, buildImportFidelity, fetchImportTimeline, importFidelityFields, matchImportedIssue, normalizeImportValue, persistImportFidelity, readImportReleases, selectImportContextIndex, serializeImportTags, validateImportLifetime, type AmbiguousCitationPlan, type CitationCandidate, type ImportFidelity, } from "./import-fidelity.ts"; import { refreshChanges, runRefresh } from "./refresh.ts"; @@ -60,7 +63,7 @@ import path from "node:path"; import { spawnSync } from "node:child_process"; import { - createPmCliExpectedError, + PmClient, createPmCliExpectedError, comments as pmCommentsFn, commitItemMutations as sdkCommitItemMutations, isExternalDependencyReference, @@ -71,7 +74,6 @@ import { readSettings as sdkReadSettings, resolvePmRoot as sdkResolvePmRoot, toImportTags, - writeFileAtomic, getHistoryPath as sdkGetHistoryPath, readHistorySnapshot as sdkReadHistorySnapshot, type BulkItemMutation, @@ -97,7 +99,7 @@ import { export { resolvePmDataDir } from "./gate.ts"; -import { +import { getProjectPullOwners, type ProjectItem, type ProjectItemContent, type ProjectMeta, @@ -246,8 +248,16 @@ type ReadSettings = (pmRoot: string) => Promise<{ id_prefix?: string }>; * defaults; a test overrides only the ones it needs. */ export interface AtomicImportOptions { + /** Observe a fully flushed recovery plan before its atomic exclusive publication. */ + onPlanStaged?: GithubStateWriteOptions["onStaged"]; /** Refuse unmatched entries and emit updates only, including deterministic imported ids. */ updateOnly?: boolean; + /** Require complete side data for gated callers; every atomic import requires passing privacy preflight. */ + gate?: boolean; + /** Recover issue dependency side effects from the recorded source bodies. */ + linkDeps?: boolean; + /** Complete original source batch, including unchanged entries needing dependency repair. */ + dependencyIssues?: readonly GhIssue[]; /** Author identity stamped onto the transaction's mutations (default `pm-github`). */ atomicAuthor?: string; /** SDK transaction observer; explicit failure injection still exercises real rollback. */ @@ -829,10 +839,8 @@ export class CommandError extends Error { /** * Build the `gh:owner/repo#N` provenance tag linking a pm item to a GitHub issue. * - * The tag is the idempotency key: it round-trips losslessly through - * `pm create --tags` / `pm list --json`, so a re-import can find the existing - * item and UPDATE it instead of duplicating. Provenance also rides on declared - * schema fields and the description, but the tag is what matching keys on. + * Tags are portable references for search and review. Import identity comes + * from importer-owned provenance; shared references never select an owner. * * @param repo - `owner/repo`, lowercased into the tag. * @param issueNumber - The issue/PR number. @@ -1229,8 +1237,7 @@ export function readPmItems( return decodeCompletePmItems(parsed); } -// Index existing pm items by their GitHub provenance tag for O(1) idempotent -// matching on re-import. +// Associate remote search hits with uniquely owned local issue identities. /** * Narrow one runtime search document to a {@link PmItem}. * @@ -1271,21 +1278,18 @@ export function resolveSearchCorpus(documents: unknown, pmRootValue: unknown): P /** * Index complete pm items by their normalized GitHub issue provenance. * - * Items without a stable local id or a valid `gh:owner/repo#N` tag are skipped. - * When legacy data contains duplicate provenance, the last corpus row wins; - * the complete reader prevents duplicate local item ids separately. + * References alone never identify a local owner. Contradictory or competing + * ownership is refused before associating a search result with a tracker item. * * @param items - Complete runtime-validated pm item corpus. * @returns Map keyed by lowercase `owner/repo#N` provenance. */ -export function indexByProvenance(items: PmItem[]): Map { +export function indexByProvenance(items: PmItem[], targets?: ReadonlySet): Map { const index = new Map(); - for (const item of items) { - for (const tag of item.tags ?? []) { - const p = parseProvenanceTag(tag); - if (p && item.id) index.set(`${p.repo}#${p.number}`, item); - } - } + const claims = buildGithubOwnershipIndex(items); + const selected = items.filter(item => !targets || ownershipStateOf(claims, item).claims.some(key => targets.has(key))); + warnGithubOwnershipScope(claims, selected); + for (const item of selected) if (item.id) for (const key of githubOwnedIdentityKeys(item, claims)) if (!targets || targets.has(key)) index.set(key, item); return index; } @@ -1327,6 +1331,8 @@ export interface PreparedGithubImport { status: string; description: string; body: string; + /** Original remote body for dependency replay, independent of body comment rendering. */ + sourceBody?: string; tags: string[]; assignee?: string; milestone?: string; @@ -1338,8 +1344,10 @@ export interface PreparedGithubImport { fidelity?: ImportFidelity; /** Existing external owner receives a link without replacing its provider context. */ linkOnly?: boolean; + /** Existing local content is curated; only source lifecycle and provenance may merge. */ + preserveContent?: boolean; /** Auditable identity and lifecycle conflict decision. */ - identity?: { via: string; conflict?: { local: string; github: string; rule: string } }; + identity?: { via: string; title_conflict?: boolean; conflict?: { local: string; github: string; rule: string } }; /** Rejected prose references remain visible for explicit manual provenance tagging. */ citation_candidates?: CitationCandidate[]; match?: PmItem; @@ -1431,6 +1439,7 @@ export function deriveAtomicTransactionId( milestone: entry.milestone ?? null, fidelity: entry.fidelity ?? null, linkOnly: entry.linkOnly ?? false, + ...(entry.preserveContent ? { preserveContent: true } : {}), })); const payload = crypto .createHash("sha256") @@ -1453,11 +1462,7 @@ export function deriveAtomicItemId( idPrefix: string, normalizeItemId: (input: string, prefix: string) => string, ): string { - const repoToken = crypto - .createHash("sha256") - .update(repo.toLowerCase()) - .digest("hex") - .slice(0, 12); + const repoToken = githubRepositoryHash(repo); return normalizeItemId(`github-${repoToken}-${issueNumber}`, idPrefix); } @@ -1474,7 +1479,7 @@ export function buildAtomicImportMutations( const closing = canonicalGithubStatus(entry.status, statuses) === canonicalGithubStatus("closed", statuses); const changed = updateOnly ? new Set(refreshChanges(entry, statuses).map(change => change.field)) : undefined; const audit = updateOnly ? { comment: [`Refreshed linked GitHub issue ${repo.toLowerCase()}#${entry.issueNumber}`] } : {}; - if (entry.linkOnly && entry.match?.id) { + if ((entry.linkOnly || entry.preserveContent) && entry.match?.id) { return { itemId: entry.match.id, mutations: [{ op: "update", id: entry.match.id, options: { ...(!changed || changed.has("tags") ? importTagOptions(entry) : {}), ...audit } }] }; } const sharedOptions: Record = { @@ -1512,7 +1517,7 @@ export function buildAtomicImportMutations( const mutations: BulkItemMutation[] = [{ op: "create", id: managedItemId, - options: { ...sharedOptions, status: createStatus, message: `pm-github import create ${repo.toLowerCase()}#${entry.issueNumber}` }, + options: { ...sharedOptions, author: "pm-github", status: createStatus, message: `pm-github import create ${repo.toLowerCase()}#${entry.issueNumber}` }, }, { op: "update", id: managedItemId, @@ -1589,6 +1594,12 @@ export interface GithubImportCommitResult { updated: number; /** Entries covered by a recovered journal; present only when recovered. */ recoveredItems?: number; + /** Dependency receipts from the original persisted plan. */ + depLink?: DepLinkResult; + /** Native annotation failures are best-effort after the item transaction commits. */ + commentWriteFailures?: number; + /** Entries whose annotations this transaction already attempted, including failed attempts. */ + annotationIssueNumbers?: number[]; /** Post-commit routing map from GitHub issue number to pm item id. */ itemIds: Map; } @@ -1605,6 +1616,16 @@ export interface UnfinishedGithubImport { /** The exact pre-commit transaction plan persisted beside the SDK journal. */ export interface GithubImportPlanRecord { + /** Original privacy preflight receipt; missing or failed receipts cannot authorize replay. */ + preflight?: ImportGateReceipt; + /** Complete source batch used by the original dependency pass. */ + dependencyIssues?: readonly GhIssue[]; + /** Whether the original caller requested dependency side effects. */ + linkDeps?: boolean; + /** Absent on released refresh plans; those plans are update-only. */ + updateOnly?: boolean; + /** Original target mapping, including deterministic targets not yet created. */ + itemIds?: readonly (readonly [number, string])[]; /** Stable transaction id the plan belongs to. */ transactionId: string; /** Lowercased repository the transaction planned to refresh. */ @@ -1668,30 +1689,57 @@ export async function listUnfinishedGithubImports(pmRoot: string): Promise left.transactionId.localeCompare(right.transactionId)); } +/** Discover retained producer plans even when their SDK journals already committed. */ +export async function listRetainedGithubImportPlans(pmRoot: string): Promise { + const names = await fs.promises.readdir(importPlanDir(pmRoot)).catch((error: NodeJS.ErrnoException) => { + if (error.code === "ENOENT") return []; + throw new GithubImportRecoveryError("github_import_plan_missing", "GitHub recovery plans could not be read; restore readable original plans before retrying."); + }); + const records: GithubImportPlanRecord[] = []; + for (const name of names.filter(name => name.startsWith(ATOMIC_IMPORT_PREFIX) && name.endsWith(".json")).sort()) { + const record = await readGithubImportPlan(pmRoot, name.slice(0, -5)); + if (!record || record.transactionId !== name.slice(0, -5)) throw new GithubImportRecoveryError("github_import_plan_missing", `Retained GitHub transaction ${name.slice(0, -5)} original plan is missing or mismatched; restore its readable original plan before recovery.`); + validateGithubImportPlan(record); + records.push(record); + } + return records; +} + /** Persist the exact pre-commit plan so an interrupted journal can be replayed verbatim. */ -async function persistGithubImportPlan(pmRoot: string, record: GithubImportPlanRecord): Promise { - await fs.promises.mkdir(importPlanDir(pmRoot), { recursive: true }); - await writeFileAtomic(path.join(importPlanDir(pmRoot), `${record.transactionId}.json`), `${JSON.stringify(record, null, 2)}\n`); +async function persistGithubImportPlan(pmRoot: string, record: GithubImportPlanRecord, onStaged?: GithubStateWriteOptions["onStaged"]): Promise { + const file = path.join(importPlanDir(pmRoot), `${record.transactionId}.json`); + try { await writeGithubStateAtomic(file, `${JSON.stringify(record, null, 2)}\n`, { exclusive: true, onStaged }); } + catch (error) { + if ((error as NodeJS.ErrnoException)?.code === "EEXIST") throw new GithubImportRecoveryError("github_import_plan_drift", `Transaction ${record.transactionId} already has a recorded plan. Retry to replay that plan; no new plan was written.`); + throw new GithubImportRecoveryError("github_import_plan_write_failed", `Transaction ${record.transactionId} original plan could not be published durably. Retain any original plan and retry the import; no SDK mutation started.`); + } } -/** Read one persisted plan; null when the tracker holds no plan for that transaction. */ +/** Read one persisted plan; only absence returns null, while corrupt bytes require explicit recovery. */ export async function readGithubImportPlan(pmRoot: string, transactionId: string): Promise { if (!/^[a-zA-Z0-9._-]+$/.test(transactionId)) return null; const file = path.join(importPlanDir(sdkResolvePmRoot(process.cwd(), pmRoot)), `${transactionId}.json`); try { - return JSON.parse(await fs.promises.readFile(file, "utf8")) as GithubImportPlanRecord; - } catch { - return null; + const record: unknown = JSON.parse(await fs.promises.readFile(file, "utf8")); + if (!record || typeof record !== "object" || Array.isArray(record)) throw new Error("Invalid plan document"); + validateGithubImportPlan(record as GithubImportPlanRecord); + if ((record as GithubImportPlanRecord).transactionId !== transactionId) throw new Error("Mismatched recovery identity"); + return record as GithubImportPlanRecord; + } catch (error) { + if ((error as NodeJS.ErrnoException)?.code === "ENOENT") return null; + if (error instanceof GithubImportRecoveryError) throw error; + throw new GithubImportRecoveryError("github_import_plan_corrupt", `Transaction ${transactionId} original plan is corrupt or unreadable. Restore its original plan from a verified copy or reconcile its SDK journal before retrying; retain tracker history and do not start a new import.`); } } /** Remove a persisted plan once its journal reached a durable terminal state. */ async function removeGithubImportPlan(pmRoot: string, transactionId: string): Promise { await fs.promises.rm(path.join(importPlanDir(pmRoot), `${transactionId}.json`), { force: true }); + await syncGithubStatePath(importPlanDir(pmRoot), true); } /** Finalize source metadata and commit one github-import transaction through the SDK. */ -async function commitGithubImportTransaction(config: { +async function commitGithubImportTransactionUnchecked(config: { pmRoot: string; repo: string; entries: readonly PreparedGithubImport[]; @@ -1705,7 +1753,8 @@ async function commitGithubImportTransaction(config: { /** Finalize before the SDK commits, and repair source metadata on recovered journals. */ const finalize = async (): Promise => { for (const entry of entries) { - if (entry.fidelity) await persistImportFidelity(pmRoot, itemIds.get(entry.issueNumber)!, entry.fidelity, Boolean(entry.linkOnly), opts.updateOnly); + if (entry.fidelity) await persistImportFidelity(pmRoot, itemIds.get(entry.issueNumber)!, entry.fidelity, Boolean(entry.linkOnly), opts.updateOnly, undefined, entry.preserveContent, + githubImportTarget(config.repo, entry, itemIds.get(entry.issueNumber)!)); } }; let committed = false; @@ -1727,6 +1776,15 @@ async function commitGithubImportTransaction(config: { }); committed = true; await finalize(); + let commentFailures = 0; + for (const entry of entries) if (entry.syncAnnotations) { + commentFailures += (await syncGithubCommentsToAnnotations(itemIds.get(entry.issueNumber)!, entry.comments, pmRoot, entry.issueNumber, githubImportTarget(config.repo, entry, itemIds.get(entry.issueNumber)!))).failed; + } + const depLink = opts.linkDeps ? await linkImportedDependencies(config.repo, opts.dependencyIssues ?? entries.map(entry => ({ + number: entry.issueNumber, title: entry.title, body: entry.sourceBody ?? entry.body, + state: entry.fidelity?.github_state === "closed" ? "closed" : "open", labels: [], assignee: null, milestone: null, + created_at: entry.fidelity?.github_created_at ?? "", updated_at: entry.fidelity?.github_updated_at ?? "", html_url: entry.fidelity?.github_url ?? "", + })), pmRoot) : undefined; // A recovered journal may include work applied by the interrupted process // as well as steps resumed now. The SDK intentionally returns the durable // final results, not a per-invocation delta, so create/update counts cannot @@ -1735,24 +1793,37 @@ async function commitGithubImportTransaction(config: { return { transactionId, recovered, + ...(entries.some(entry => entry.syncAnnotations) ? { commentWriteFailures: commentFailures, annotationIssueNumbers: entries.filter(entry => entry.syncAnnotations).map(entry => entry.issueNumber) } : {}), + ...(depLink ? { depLink } : {}), imported: recovered ? 0 : entries.filter((entry) => !entry.match?.id).length, updated: recovered ? 0 : entries.filter((entry) => Boolean(entry.match?.id)).length, ...(recovered ? { recoveredItems: entries.length } : {}), itemIds, }; } catch (err: unknown) { - // Retain sensitive replay data only while the durable journal is resumable. - // Apply this to fresh commits and recovery attempts, preserving the original - // failure when journal inspection or cleanup itself is unavailable. - if (opts.updateOnly) { - const pending = await listUnfinishedGithubImports(pmRoot).catch(() => null); - if (pending && !pending.some(journal => journal.transactionId === transactionId)) { + const errorContext = err && typeof err === "object" && "context" in err ? err.context : undefined; + const sdkCode = errorContext && typeof errorContext === "object" && "code" in errorContext ? errorContext.code : undefined; + const reservedIdentity = sdkCode === "item_identity_reserved"; + const mismatchedPlan = sdkCode === "transaction_plan_mismatch"; + // A committed journal can still need source finalization. Annotation failures + // are reported separately and never retain a committed item plan for replay. + // Only known compensation or a failure before journal creation permits cleanup; + // an unreadable journal cannot establish that recovery has finished. + if (!committed && !reservedIdentity && !mismatchedPlan) { + const journal = await fs.promises.readFile(path.join(sdkJournalDir(pmRoot), `${transactionId}.json`), "utf8") + .then(contents => JSON.parse(contents) as SdkTransactionJournal) + .catch((error: NodeJS.ErrnoException) => error.code === "ENOENT" ? undefined : null); + if (journal?.status === "committed") committed = true; + else if (journal === undefined || journal?.status === "compensated" || journal?.status === "abandoned") { await removeGithubImportPlan(pmRoot, transactionId).catch(() => {}); } } const msg = errorMessage(err); + if (err && typeof err === "object" && "code" in err && ["github_import_owner_conflict", "github_identity_conflict"].includes(String(err.code))) throw err; const label = `${opts.updateOnly ? "Refresh" : "Atomic GitHub import"} transaction ${transactionId}`; - if (committed) throw new CommandError(`${label} has a committed journal but source metadata could not be finalized. Retry transaction ${transactionId}; nothing may be pushed. Underlying error: ${msg}`); + if (mismatchedPlan) throw new GithubImportRecoveryError("github_import_plan_drift", `${label} has recorded mutations that no longer match its SDK journal. Restore its original recorded plan before retrying; changed input was not applied.`); + if (reservedIdentity) throw new GithubImportRecoveryError("github_import_plan_drift", `${label} cannot recreate original target(s) ${mutations.filter(mutation => mutation.op === "create").map(mutation => mutation.id).join(", ")}: compensation retained their history. Inspect pm history and explicitly restore their original live versions with pm restore before retrying this same transaction. Keep its recorded plan and history; changed GitHub input was not applied.`); + if (committed) throw new CommandError(`${label} has a committed journal but source side data could not be finalized. Retry transaction ${transactionId}; nothing may be pushed. Underlying error: ${msg}`); if (err instanceof AggregateError || /compensation failed/i.test(msg)) { throw new CommandError( `${label} failed and compensation was incomplete. The tracker may contain partially applied state; retry to resume transaction ${transactionId}, then inspect its durable journal if recovery still fails. Underlying error: ${msg}`, @@ -1772,6 +1843,33 @@ async function commitGithubImportTransaction(config: { } } +/** Bind every recorded target to its source owner before replay and during each locked SDK write. */ +async function commitGithubImportTransaction(config: Parameters[0]): Promise { + const targets = config.entries.map(entry => githubImportTarget(config.repo, entry, config.itemIds.get(entry.issueNumber)!, !config.opts.updateOnly && !entry.match?.id)); + try { return await commitGithubImportTransactionUnchecked({ ...config, + commit: input => withGithubImportOwners(config.pmRoot, targets, () => config.commit(input)), + }); } + catch (error: unknown) { + if (error && typeof error === "object" && "code" in error && ["github_import_owner_conflict", "github_identity_conflict"].includes(String(error.code))) { + const missing = await fs.promises.lstat(path.join(sdkJournalDir(config.pmRoot), `${config.transactionId}.json`)) + .then(() => false, (failure: NodeJS.ErrnoException) => failure.code === "ENOENT"); + if (missing) await removeGithubImportPlan(config.pmRoot, config.transactionId); + } + throw error; + } +} + +/** Carry a verified first-link decision without letting it override an existing GitHub owner. */ +export function githubImportTarget(repo: string, entry: PreparedGithubImport, id: string, allowCreate = false): GithubOwnedTarget { + return { id, repo, number: entry.issueNumber, allowCreate, mirrorContent: !entry.linkOnly && !entry.preserveContent, + ...(entry.match?.id === id && entry.identity?.via && entry.identity.via !== "new" && githubOwnedIdentityKeys(entry.match).length === 0 ? { + initialLink: (item: PmItem) => matchImportedIssue([item], { + number: entry.issueNumber, html_url: entry.fidelity!.github_url, body: entry.sourceBody ?? entry.body, + } as GhIssue, undefined, entry.identity?.via === "citation").item?.id === id, + } : {}), + }; +} + /** Commit a complete issue-import batch under one crash-resumable transaction. */ export async function importGithubAtomic( pmRoot: string, @@ -1780,6 +1878,8 @@ export async function importGithubAtomic( opts: AtomicImportOptions = {}, ): Promise { pmRoot = sdkResolvePmRoot(process.cwd(), pmRoot); + const pending = await resumePendingGithubImport(pmRoot, repo, opts, undefined, opts.updateOnly ? new Set(entries.flatMap(entry => entry.match?.id ? [entry.match.id] : [])) : undefined); + if (pending) return pending; const { commitItemMutations: commit, normalizeItemId, @@ -1806,16 +1906,29 @@ export async function importGithubAtomic( itemIds.set(entry.issueNumber, planned.itemId); mutations.push(...planned.mutations); } + for (const entry of entries) { + const id = itemIds.get(entry.issueNumber)!; + const target = githubImportTarget(repo, entry, id, !opts.updateOnly && !entry.match?.id); + if (!entry.linkOnly && !entry.preserveContent) { + try { assertGithubImportOwner({ ...entry.match, id, author: entry.match?.author ?? "pm-github", description: entry.description, tags: importTagValues(entry) }, target); } + catch (error: unknown) { + if (!(error instanceof GithubImportOwnershipError)) throw error; + throw new GithubImportRecoveryError("github_import_plan_ownership_invalid", `Import plan for ${repo.toLowerCase()}#${entry.issueNumber} does not retain import ownership through its content updates. Build it with prepareGithubImport and retain explicit provenance or its proven importer-created ID. No item was written.`); + } + } + } const transactionId = deriveAtomicTransactionId(repo, entries, mutations, await importContextVersions(pmRoot, entries, workspace.statuses)); - // Update-only transactions derive their steps from the CURRENT item state, - // so a retry after a partial apply would fingerprint a different plan and - // orphan the journal behind a fresh transaction id. Persist the exact plan - // before the SDK touches the tracker so an interrupted refresh can replay it. + // Save all producer modes, before any SDK mutation, with their original targets. if (opts.updateOnly) for (const entry of entries) validateGithubImportFidelity(entry, repo, `GitHub transaction ${transactionId}`); - const plan = opts.updateOnly ? { transactionId, repo: repo.toLowerCase(), entries, mutations } : undefined; - if (plan) await persistGithubImportPlan(pmRoot, plan); + const payload = { transactionId, repo: repo.toLowerCase(), entries, mutations, updateOnly: Boolean(opts.updateOnly), ...(opts.linkDeps ? { linkDeps: true, dependencyIssues: opts.dependencyIssues } : {}), itemIds: [...itemIds] }; + const preflight = scanGithubImportPreflight(pmRoot, entries, mutations, payload); + if (preflight.verdict !== "pass") throw githubImportPreflightRefusal(preflight); + const plan = { ...payload, preflight }; + validateGithubImportPlan(plan); + await persistGithubImportPlan(pmRoot, plan, opts.onPlanStaged); const result = await commitGithubImportTransaction({ pmRoot, repo, entries, mutations, transactionId, itemIds, opts, commit }); - if (plan) await removeGithubImportPlan(pmRoot, transactionId); + await removeGithubImportPlan(pmRoot, transactionId); + failClosedOnPlanDivergence(opts.gate === true, 0, 0, result.commentWriteFailures ?? 0); return result; } @@ -1830,7 +1943,7 @@ export function validateGithubImportFidelity(entry: PreparedGithubImport, repo: if (!fidelity || typeof fidelity !== "object" || Array.isArray(fidelity) || Object.keys(fidelity).some(field => !fields.includes(field)) || typeof fidelity.github_url !== "string" || - fidelity.github_url.toLowerCase() !== `https://github.com/${repo.toLowerCase()}/issues/${entry.issueNumber}` || + parseGithubIssueUrl(fidelity.github_url)?.key !== `${repo.toLowerCase()}#${entry.issueNumber}` || fidelity.github_number !== entry.issueNumber || !["open", "closed"].includes(fidelity.github_state) || typeof fidelity.github_created_at !== "string" || typeof fidelity.github_updated_at !== "string" || @@ -1846,13 +1959,13 @@ export function validateGithubImportFidelity(entry: PreparedGithubImport, repo: } // Missing source closure dates preserve native completion metadata. // Check the effective lifecycle from the current read-only snapshot too. - const current = items?.find(item => item.id === entry.match!.id) ?? entry.match; - const projected = { ...current, ...importFidelityFields(fidelity, Boolean(entry.linkOnly)) }; + const current = items?.find(item => item.id === entry.match?.id) ?? entry.match; + const projected = { ...current, ...importFidelityFields(fidelity, Boolean(entry.linkOnly), entry.preserveContent ? current : undefined) }; if (projected.closed_at && Date.parse(String(projected.closed_at)) < Date.parse(String(projected.created_at))) { throw new CommandError(`${context}: its persisted fidelity conflicts with the native closure date.`); } } - if (entry.linkOnly !== undefined && typeof entry.linkOnly !== "boolean") { + if ((entry.linkOnly !== undefined && typeof entry.linkOnly !== "boolean") || (entry.preserveContent !== undefined && typeof entry.preserveContent !== "boolean")) { throw new CommandError(`${context}: its persisted ownership is invalid.`); } } @@ -1861,24 +1974,33 @@ export function validateGithubImportFidelity(entry: PreparedGithubImport, repo: export function validateGithubImportPlan(record: GithubImportPlanRecord, items?: readonly PmItem[]): Map { if (typeof record?.transactionId !== "string" || !/^[a-zA-Z0-9._-]+$/.test(record.transactionId) || typeof record.repo !== "string" || !/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(record.repo) || - !Array.isArray(record.entries) || !record.entries.length || !Array.isArray(record.mutations) || !record.mutations.length) { + !Array.isArray(record.entries) || !record.entries.length || !Array.isArray(record.mutations) || !record.mutations.length || + (record.updateOnly !== undefined && typeof record.updateOnly !== "boolean") || + (record.linkDeps !== undefined && typeof record.linkDeps !== "boolean")) { throw new CommandError( `Refresh cannot replay unfinished GitHub transaction ${typeof record?.transactionId === "string" ? record.transactionId : ""}: its persisted plan is invalid.`, EXIT_CODE.GENERIC_FAILURE, ); } + if (record.dependencyIssues !== undefined && (!Array.isArray(record.dependencyIssues) || record.dependencyIssues.some(issue => !Number.isSafeInteger(issue?.number) || issue.number < 1 || (issue.body !== null && typeof issue.body !== "string")))) throw new GithubImportRecoveryError("github_import_plan_drift", `Transaction ${record.transactionId} has invalid original dependency inputs. Restore its recorded plan before retrying.`); + const fullImport = record.updateOnly === false; const itemIds = new Map(); + if (fullImport) { + if (!Array.isArray(record.itemIds) || record.itemIds.length !== record.entries.length || record.itemIds.some(pair => !Array.isArray(pair) || pair.length !== 2 || !Number.isSafeInteger(pair[0]) || typeof pair[1] !== "string" || !pair[1])) throw new GithubImportRecoveryError("github_import_plan_drift", `Transaction ${record.transactionId} has invalid original targets. Restore its recorded plan before retrying.`); + for (const [number, id] of record.itemIds) itemIds.set(number, id); + if (itemIds.size !== record.entries.length || new Set(itemIds.values()).size !== itemIds.size) throw new GithubImportRecoveryError("github_import_plan_drift", `Transaction ${record.transactionId} has duplicate original targets. Restore its recorded plan before retrying.`); + } for (const entry of record.entries) { - if (typeof entry?.issueNumber !== "number" || typeof entry?.match?.id !== "string") { + if (typeof entry?.issueNumber !== "number" || (fullImport ? !itemIds.has(entry.issueNumber) || Boolean(entry.match?.id && entry.match.id !== itemIds.get(entry.issueNumber)) : typeof entry?.match?.id !== "string")) { throw new CommandError( `Refresh cannot replay unfinished GitHub transaction ${record.transactionId}: its persisted plan has an unlinked target.`, EXIT_CODE.GENERIC_FAILURE, ); } validateGithubImportFidelity(entry, record.repo, `Refresh cannot replay unfinished GitHub transaction ${record.transactionId}`, items); - itemIds.set(entry.issueNumber, entry.match.id); + if (!fullImport) itemIds.set(entry.issueNumber, entry.match!.id!); } - if (record.mutations.some(mutation => mutation?.op !== "update" || ![...itemIds.values()].includes(mutation.id))) { + if (record.mutations.some(mutation => !(fullImport ? ["create", "update", "close"].includes(mutation?.op) : mutation?.op === "update") || ![...itemIds.values()].includes(mutation.id))) { throw new CommandError(`Refresh cannot replay unfinished GitHub transaction ${record.transactionId}: its persisted mutation targets are invalid.`); } return itemIds; @@ -1899,15 +2021,51 @@ export async function resumeGithubImport( ): Promise { pmRoot = sdkResolvePmRoot(process.cwd(), pmRoot); const itemIds = validateGithubImportPlan(record); + assertGithubImportReplayPreflight(pmRoot, record); const { commitItemMutations: commit } = await resolveAtomicSdkFunctions(opts); const result = await commitGithubImportTransaction({ pmRoot, repo: record.repo, entries: record.entries, mutations: record.mutations, - transactionId: record.transactionId, itemIds, opts: { ...opts, updateOnly: true }, commit, + transactionId: record.transactionId, itemIds, opts: { ...opts, updateOnly: record.updateOnly !== false, linkDeps: record.linkDeps, dependencyIssues: record.dependencyIssues }, commit, }); await removeGithubImportPlan(pmRoot, record.transactionId); + failClosedOnPlanDivergence(opts.gate === true, 0, 0, result.commentWriteFailures ?? 0); return result; } +/** Typed recovery refusal when the original durable plan cannot be safely replayed. */ +export class GithubImportRecoveryError extends CommandError { + /** Machine-readable conflict category for recovery and upstream ownership guards. */ + readonly code: string; + /** Retain the recovery category and its concrete correction instruction. */ + constructor(code: string, message: string) { super(message); this.code = code; } +} + +/** Replay one original repository batch before any current input can replace its steps or finalizer. */ +async function resumePendingGithubImport(pmRoot: string, repo: string, opts: AtomicImportOptions = {}, beforeReplay?: (record: GithubImportPlanRecord) => Promise, selectedIds?: ReadonlySet): Promise { + const pending = await listUnfinishedGithubImports(pmRoot); + const names = await fs.promises.readdir(importPlanDir(pmRoot)).catch((error: NodeJS.ErrnoException) => { + if (error.code === "ENOENT") return []; + throw error; + }); + const ids = new Set([...pending.map(journal => journal.transactionId), ...names.filter(name => name.endsWith(".json")).map(name => name.slice(0, -5))]); + const records: GithubImportPlanRecord[] = []; + for (const id of ids) { + const journal = pending.find(journal => journal.transactionId === id); + if (selectedIds && journal?.itemIds.length && journal.itemIds.every(itemId => !selectedIds.has(itemId))) continue; + const record = await readGithubImportPlan(pmRoot, id); + if (!record || record.transactionId !== id || typeof record.repo !== "string") throw new GithubImportRecoveryError("github_import_plan_missing", `Transaction ${id} has no readable original plan. Restore its original plan or reconcile its journal before retrying; no new import may start.`); + if (record.repo.toLowerCase() !== repo.toLowerCase()) continue; + const originalTargets = validateGithubImportPlan(record); + if (selectedIds && originalTargets.size && [...originalTargets.values()].every(itemId => !selectedIds.has(itemId))) continue; + records.push(record); + } + if (!records.length) return undefined; + if (records.length !== 1) throw new GithubImportRecoveryError("github_import_plan_drift", "Multiple original import plans require recovery. Resume each recorded transaction explicitly before starting another import."); + const record = records[0]!; + await beforeReplay?.(record); + return resumeGithubImport(pmRoot, record, opts); +} + // --------------------------------------------------------------------------- // Shared import core (used by both the legacy command and the importer) // --------------------------------------------------------------------------- @@ -2061,7 +2219,7 @@ export function composeBody(issue: GhIssue, comments: GhComment[]): string { // prefix keeps our namespace disjoint from the CLI's per-item mutation // locks (.lock): we hold our lock ACROSS several CLI mutations, so // sharing a name would make the very mutations we wrap conflict with it. -// - acquire fs.openSync(path, "wx") — atomic O_EXCL create on POSIX. +// - acquire atomic link publishes the fully flushed payload with exclusive-create semantics. // - stale a lock is broken (with a stderr warning) only when its owner // is provably gone: the recorded owner PID is dead, or it equals our own // PID (we know we don't hold it, so the PID was recycled). A lock with a @@ -2209,6 +2367,8 @@ export function isLockOwnerAlive(pid: number): boolean { // same-pid payload that is legitimately ours (in-process contention → wait) // from a leftover written by a dead process whose PID we inherited (→ break). const heldImportLocks = new Set(); +// A complete lease can become visible before its publisher finishes directory fsync. +const publishingImportLockTokens = new Set(); /** * Decide whether a held lock is stale and why. @@ -2238,7 +2398,7 @@ function importLockStaleReason( // Our own PID: either another async task in THIS process holds it (the // in-process race the caller serializes too — contend, don't break), or // we provably don't hold it and the PID was recycled by a dead owner. - return heldImportLocks.has(lockPath) + return heldImportLocks.has(lockPath) || (typeof payload?.token === "string" && publishingImportLockTokens.has(payload.token)) ? undefined : `owner pid ${pid} is this process but the lock is not held in-process (pid recycled, lock abandoned)`; } @@ -2279,11 +2439,10 @@ export const IMPORT_LOCK_BREAKER_TTL_MS = 10_000; * @param reason - Human-readable staleness reason, surfaced in the break warning. * @returns True when the stale lock was removed (retry acquire now); false when the election was lost or the lock is live again. */ -function breakStaleImportLock(lockPath: string, ttlMs: number, reason: string): boolean { +async function breakStaleImportLock(lockPath: string, ttlMs: number, reason: string): Promise { const breakerPath = `${lockPath}.break`; - let bfd: number | undefined; try { - bfd = fs.openSync(breakerPath, "wx"); + await writeGithubStateAtomic(breakerPath, "", { exclusive: true }); } catch (err: unknown) { if ((err as NodeJS.ErrnoException)?.code === "EEXIST") { // Another breaker is active. If IT crashed mid-break, its sidecar is @@ -2299,7 +2458,6 @@ function breakStaleImportLock(lockPath: string, ttlMs: number, reason: string): return false; } try { - fs.closeSync(bfd); // Re-verify under the mutex: the lock we judged stale may have been // broken and re-acquired by a live owner since we looked at it. let payload: ImportLockPayload | undefined; @@ -2337,13 +2495,13 @@ function breakStaleImportLock(lockPath: string, ttlMs: number, reason: string): * * @param pmRoot - Workspace root or pm data dir passed to {@link resolvePmDataDir}. * @param itemId - The pm item whose comment critical section is serialized. - * @param opts - Optional overrides for the staleness TTL and wait budget. + * @param opts - Optional TTL/wait overrides and observation of the candidate before publication. * @returns The acquisition outcome. */ export async function acquireImportLock( pmRoot: string, itemId: string, - opts: { ttlMs?: number; waitMs?: number } = {}, + opts: { ttlMs?: number; waitMs?: number; onStaged?: GithubStateWriteOptions["onStaged"] } = {}, ): Promise { const ttlMs = opts.ttlMs ?? IMPORT_LOCK_TTL_MS_DEFAULT; const waitMs = opts.waitMs ?? IMPORT_LOCK_WAIT_MS_DEFAULT; @@ -2368,12 +2526,9 @@ export async function acquireImportLock( let backoffMs = IMPORT_LOCK_INITIAL_BACKOFF_MS; let staleBreaks = 0; for (;;) { - let fd: number | undefined; try { - fd = fs.openSync(lockPath, "wx"); - fs.writeFileSync(fd, `${JSON.stringify(payload, null, 2)}\n`); - fs.closeSync(fd); - fd = undefined; + publishingImportLockTokens.add(payload.token); + await writeGithubStateAtomic(lockPath, `${JSON.stringify(payload, null, 2)}\n`, { exclusive: true, onStaged: opts.onStaged }); heldImportLocks.add(lockPath); let released = false; return { @@ -2398,28 +2553,13 @@ export async function acquireImportLock( }, }; } catch (err: unknown) { - if (fd !== undefined) { - try { - fs.closeSync(fd); - } catch { - // Ignore close errors on the failure path. - } - // We created the lockfile but failed to write/close it. Remove the - // empty/partial file so other processes are not blocked until the - // TTL expires on a lock that was never validly held. - try { - fs.unlinkSync(lockPath); - } catch { - // Best-effort: already gone. - } - } const code = (err as NodeJS.ErrnoException)?.code; if (code !== "EEXIST") { const msg = errorMessage(err); console.error(`pm-github: comment-sync lock failed (${msg}) — proceeding without cross-process serialization`); return { status: "degraded" }; } - // The lock exists for open(O_EXCL) but cannot be stat'd (it vanished, or + // The lock exists for exclusive publication but cannot be stat'd (it vanished, or // it is a broken symlink). Retry, but honor the wait budget: a persistent // stat failure must not busy-spin the process until an external killer. let existing: ImportLockPayload | undefined; @@ -2436,7 +2576,7 @@ export async function acquireImportLock( const staleReason = importLockStaleReason(lockPath, existing, mtimeMs, ttlMs); if (staleReason && staleBreaks < IMPORT_LOCK_MAX_STALE_BREAKS) { staleBreaks++; - if (breakStaleImportLock(lockPath, ttlMs, staleReason)) continue; + if (await breakStaleImportLock(lockPath, ttlMs, staleReason)) continue; // Lost the breaker election or the lock turned out live on re-check — // fall through to the normal wait/backoff below. } @@ -2446,7 +2586,7 @@ export async function acquireImportLock( const jittered = Math.max(1, Math.round(backoffMs * (0.5 + Math.random()))); await sleep(Math.min(jittered, waitMs - elapsedMs)); backoffMs = Math.min(backoffMs * 2, IMPORT_LOCK_MAX_BACKOFF_MS); - } + } finally { publishingImportLockTokens.delete(payload.token); } } } @@ -2585,8 +2725,10 @@ export async function syncGithubCommentsToAnnotations( comments: GhComment[], pmRoot: string, issueNumber: number, + ownership?: GithubOwnedTarget, ): Promise<{ added: number; skipped: number; failed: number }> { if (comments.length === 0) return { added: 0, skipped: 0, failed: 0 }; + if (ownership) return withGithubImportOwners(pmRoot, [ownership], () => syncGithubCommentsToAnnotations(itemId, comments, pmRoot, issueNumber)); const acquisition = await acquireImportLock(pmRoot, itemId); if (acquisition.status === "contended") { console.error( @@ -2807,7 +2949,7 @@ export interface ResolvedDepEdge { } /** Minimal item-metadata shape the link pass reads (subset of the SDK `ItemMetadata`). */ -export interface DepLinkSnapshotItem { +export interface DepLinkSnapshotItem extends PmItem { id: string; tags: string[]; dependencies?: Array<{ id: string; kind: string }>; @@ -2902,23 +3044,29 @@ export function parseDependencyReferences(body: string, sourceRepo: string): Par return out; } -/** Build a `repo#number` → pm item id index from an item-metadata snapshot. */ +/** Index only unique ownership; expose ambiguous keys without selecting an arbitrary claimant. */ export function buildProvenanceIndexFromMetadata( items: readonly DepLinkSnapshotItem[], + ambiguities: Map = new Map(), + repositories: readonly string[] = [], ): Map { - const index = new Map(); + const owners = new Map>(); + const invalid = new Set(); for (const item of items) { if (!item.id) continue; - for (const tag of item.tags ?? []) { - const p = parseProvenanceTag(tag); - // First writer wins: a provenance tag is a 1:1 issue↔item link, and the - // import guarantees uniqueness; guarding keeps a hand-edited duplicate - // deterministic rather than order-dependent. - if (p && !index.has(`${p.repo}#${p.number}`)) { - index.set(`${p.repo}#${p.number}`, item.id); - } + const state = githubOwnershipClaims(item, false, false, repositories); + for (const key of state.claims) { + const claimants = owners.get(key) ?? new Set(); + claimants.add(item.id); + owners.set(key, claimants); + if (state.claims.length !== 1 || state.owners.length !== 1) invalid.add(key); } } + const index = new Map(); + for (const [key, claimants] of owners) { + if (claimants.size === 1 && !invalid.has(key)) index.set(key, [...claimants][0]!); + else ambiguities.set(key, [...claimants].sort()); + } return index; } @@ -2994,9 +3142,7 @@ function defaultDepLinkSdk(): DepLinkSdk { listAllItemMetadata: async (pmRoot: string) => { const raw = await sdkListAllItemMetadata(pmRoot); return raw.map((i) => ({ - id: i.id, - tags: i.tags, - dependencies: i.dependencies, + ...i, })); }, collectNewOrderingCycleWarnings: @@ -3005,7 +3151,7 @@ function defaultDepLinkSdk(): DepLinkSdk { } /** - * Default edge applier: spawn `pm update --dep` for one resolved edge. + * Default edge applier: update one dependency through the guarded native SDK. * * Records the source issue and the matching phrase in the mutation message so * the resulting dependency is auditable. Returns the ok/stderr shape every edge @@ -3015,13 +3161,16 @@ function defaultDepLinkSdk(): DepLinkSdk { * @param pmRoot - Workspace root for the `pm` invocation. * @returns Whether the write succeeded and any stderr. */ -function defaultApplyDependencyLink(edge: ResolvedDepEdge, pmRoot: string): { ok: boolean; stderr: string } { - const res = pmRun([ - "--path", pmRoot, "update", edge.sourceId, - "--dep", `id=${edge.targetId},kind=${edge.kind}`, - "--message", `Linked from GitHub #${edge.sourceIssue} body ("${edge.phrase}" reference)`, - ]); - return { ok: res.ok, stderr: res.stderr }; +async function defaultApplyDependencyLink(edge: ResolvedDepEdge, pmRoot: string, repo: string, targetKey: string): Promise<{ ok: boolean; stderr: string }> { + try { + const [targetRepo, targetNumber] = targetKey.split("#"); + await mutateGithubOwnedItem(pmRoot, { id: edge.sourceId, repo, number: edge.sourceIssue, + related: [{ id: edge.targetId, repo: targetRepo!, number: Number(targetNumber) }] }, "update", { + dep: [`id=${edge.targetId},kind=${edge.kind}`], + message: `GitHub #${edge.sourceIssue}: ${edge.phrase}`, + }); + return { ok: true, stderr: "" }; + } catch (error: unknown) { return { ok: false, stderr: `${error instanceof GithubImportOwnershipError || (error as { code?: string })?.code === "github_import_owner_conflict" ? "ambiguous_github_dependency_owner: " : ""}${errorMessage(error)}` }; } } /** @@ -3037,7 +3186,7 @@ export async function linkImportedDependencies( pmRoot: string, deps: ImportRunDependencies = {}, ): Promise { - const applyEdge = deps.applyDependencyLink ?? defaultApplyDependencyLink; + const applyEdge = deps.applyDependencyLink ?? ((edge: ResolvedDepEdge, root: string) => defaultApplyDependencyLink(edge, root, repo, [...provenance].find(([, id]) => id === edge.targetId)![0])); const failures: string[] = []; // Resolve the SDK-backed helpers and take the pre-edge snapshot. This runs @@ -3068,7 +3217,13 @@ export async function linkImportedDependencies( }; } - const provenance = buildProvenanceIndexFromMetadata(before); + const ambiguities = new Map(); + const referenced = new Set(issues.flatMap(issue => [`${repo.toLowerCase()}#${issue.number}`, + ...parseDependencyReferences(issue.body ?? "", repo).map(ref => `${ref.repo}#${ref.number}`)])); + const provenance = buildProvenanceIndexFromMetadata(before, ambiguities, [...referenced].map(key => key.split("#")[0]!)); + for (const [key, owners] of ambiguities) if (referenced.has(key)) { + failures.push(`ambiguous_github_dependency_owner: ${key} is claimed by ${owners.join(", ")}; no edge may use this key. Correct the explicit provenance before retrying.`); + } const { edges, unresolved } = planDependencyLinks(repo, issues, provenance); if (edges.length === 0) { return { linked: 0, unresolved, orderingCycleWarnings: [], failures }; @@ -3077,7 +3232,7 @@ export async function linkImportedDependencies( const changedSources = new Set(); let linked = 0; for (const edge of edges) { - const res = applyEdge(edge, pmRoot); + const res = await applyEdge(edge, pmRoot); if (res.ok) { linked++; changedSources.add(edge.sourceId); @@ -3125,24 +3280,25 @@ export async function prepareGithubImport( token: string | undefined, match: PmItem | undefined, fetchIssueComments: ImportRunDependencies["fetchIssueComments"] = fetchComments, - fidelityContext: { items: PmItem[]; releases: ReturnType; timeline: typeof fetchImportTimeline; statuses?: GithubStatusRegistry }, + fidelityContext: { items: PmItem[]; ownership?: GithubOwnershipIndex; releases: ReturnType; timeline: typeof fetchImportTimeline; statuses?: GithubStatusRegistry }, ): Promise { const title = issue.title.trim(); if (!title) return undefined; - const identity = matchImportedIssue(fidelityContext.items, issue, match, opts.adoptCited === true); + const identity = matchImportedIssue(fidelityContext.items, issue, match, opts.adoptCited === true, fidelityContext.ownership); match = identity.item; + const preserveContent = Boolean(match && !importerCreatedItem(match, repo, issue.number)); const timeline = await fidelityContext.timeline(issue, repo, token); const fidelity = buildImportFidelity(issue, fidelityContext.releases, timeline); const targetStatus = mapState(issue.state, issue.state_reason); - const conflict = match?.status && canonicalGithubStatus(match.status, fidelityContext.statuses) !== canonicalGithubStatus(targetStatus, fidelityContext.statuses) && (githubStatusIsTerminal(match.status, fidelityContext.statuses) || issue.state === "closed") + const conflict = match?.status && canonicalGithubStatus(match.status, fidelityContext.statuses) !== canonicalGithubStatus(targetStatus, fidelityContext.statuses) && (preserveContent || githubStatusIsTerminal(match.status, fidelityContext.statuses) || issue.state === "closed") ? { local: match.status, github: issue.state, rule: identity.linkOnly ? "preserve-provider-owner" : "github-source-wins" } : undefined; const kind = issue.pull_request ? "PR" : "issue"; const labels = issue.labels.map((label) => label.name).filter(Boolean); const tag = provenanceTag(repo, issue.number); const ghAuthorTag = authorTag(issue); const author = issue.user?.login; - const syncAnnotations = opts.commentsMode === "annotations" || opts.commentsMode === "both"; + const syncAnnotations = opts.commentsMode === "annotations" || opts.commentsMode === "both" || (preserveContent && opts.withComments); const shouldFetchComments = opts.withComments || syncAnnotations; const writeCommentsToBody = opts.commentsMode === "body" || opts.commentsMode === "both"; let comments: GhComment[] = []; @@ -3168,7 +3324,8 @@ export async function prepareGithubImport( (issue.state_reason ? ` · state reason ${issue.state_reason}` : "") + ` · created ${issue.created_at} · updated ${issue.updated_at}`, body: writeCommentsToBody ? composeBody(issue, comments) : (issue.body || ""), - tags: toImportTags([...(identity.linkOnly || identity.via === "citation" ? match!.tags ?? [] : []), ...githubLabelsToTags([...labels, tag, ...(ghAuthorTag ? [ghAuthorTag] : [])])]), + sourceBody: issue.body ?? "", + tags: toImportTags([...(preserveContent || identity.linkOnly || identity.via === "citation" ? match!.tags ?? [] : []), ...githubLabelsToTags(preserveContent ? [tag] : [...labels, tag, ...(ghAuthorTag ? [ghAuthorTag] : [])])]), assignee: issue.assignee?.login, milestone: issue.milestone?.title, comments, @@ -3176,7 +3333,8 @@ export async function prepareGithubImport( closedAt: issue.closed_at ?? undefined, fidelity, linkOnly: identity.linkOnly, - identity: { via: identity.via, ...(conflict ? { conflict } : {}) }, + ...(preserveContent ? { preserveContent: true } : {}), + identity: { via: identity.via, ...(preserveContent && match!.title !== title ? { title_conflict: true } : {}), ...(conflict ? { conflict } : {}) }, ...(identity.citation_candidates ? { citation_candidates: identity.citation_candidates } : {}), match, }; @@ -3185,7 +3343,7 @@ export async function prepareGithubImport( /** Recognize a complete source projection without treating the SDK mutation clock as content. */ export function importEntryUnchanged(entry: PreparedGithubImport, types?: ReadonlyMap, statuses?: GithubStatusRegistry): boolean { if (!entry.match?.id || !entry.fidelity || refreshChanges(entry, statuses).length !== 0) return false; - if (entry.linkOnly) return true; + if (entry.linkOnly || entry.preserveContent) return true; const fields: Record = { type: entry.itemType, description: entry.description, body: entry.body, ...(entry.assignee ? { assignee: entry.assignee } : {}), ...(entry.milestone ? { sprint: entry.milestone } : {}) }; return Object.entries(fields).every(([field, value]) => normalizeImportValue(field, ownValue(entry.match, field) ?? "", types, statuses) === normalizeImportValue(field, value, types, statuses)); @@ -3205,9 +3363,9 @@ export interface ImportPlanCompletenessReceipts { export interface ImportIdempotencyReceipts { /** Existing untagged items selected by exact prose citation, each with one landing target. */ readonly adopted: number; - /** Distinct `gh:owner/repo#N` provenance keys in the existing corpus. */ + /** Distinct durable import identity keys in the existing corpus. */ readonly provenance_indexed: number; - /** Number of plan entries matched to an existing item via its provenance tag. */ + /** Number of plan entries matched to an existing importer identity. */ readonly matched_by_provenance: number; /** Number of plan entries that will be born new; each must carry its tag. */ readonly new_entries: number; @@ -3264,14 +3422,11 @@ export function verifyImportPlanCompleteness( } /** - * Verify that a gated import's plan keeps a second run a no-op via provenance tags. + * Verify that explicit import identities and planned targets are unique. * - * Idempotency is only real when every re-import lands on exactly one item: a - * corpus with two items sharing a `gh:owner/repo#N` tag updates one and lets the - * other drift, and a plan entry targeting a second entry's item would write the - * same item twice. The check fails closed on duplicate provenance tags in the - * existing corpus, on plan entries that would create a new item without its - * provenance tag, and on two plan entries resolving to the same existing item. + * Shared gh: references are unrestricted. Persisted importer claims must still + * identify one item, and every new entry must carry its reference for review. + * Two planned issues cannot write the same existing local item. * * @param existing - The complete existing pm item corpus. * @param prepared - The plan entries about to be written. @@ -3282,20 +3437,26 @@ export function verifyImportIdempotency( existing: readonly PmItem[], prepared: readonly PreparedGithubImport[], repo: string, + ownership = buildGithubOwnershipIndex(existing, [repo]), ): ImportIdempotencyReceipts { - const tagOwners = new Map(); - const duplicateTags = new Set(); - for (const item of existing) { - if (!item.id) continue; - for (const tag of item.tags ?? []) { - const parsed = parseProvenanceTag(tag); - if (!parsed) continue; - const key = `${parsed.repo}#${parsed.number}`; - const prior = tagOwners.get(key); - if (prior && prior !== item.id) duplicateTags.add(key); - else tagOwners.set(key, item.id); - } - } + const identityOwners = new Map(); + const duplicateIdentities = new Set(); + const targets = new Set(prepared.map(entry => `${repo.toLowerCase()}#${entry.issueNumber}`)); + const selected = existing.filter(item => item.id && ownershipStateOf(ownership, item).claims.some(key => targets.has(key))); + // Record competing claims before the shared resolver refuses them, preserving + // the duplicate-identity gate as an independently exercised batch boundary. + for (const item of selected) for (const key of ownershipStateOf(ownership, item).claims) { + if (!targets.has(key)) continue; + const prior = identityOwners.get(key); + if (prior && prior !== item.id) duplicateIdentities.add(key); + else identityOwners.set(key, item.id!); + } + if (duplicateIdentities.size > 0) { + const key = [...duplicateIdentities][0]!; + const owners = [...ownership.claimants.get(key)!.keys()]; + throw new GithubImportOwnershipError(owners[0]!, key, owners.map(id => `${key} claimed by ${id}`)); + } + for (const item of selected) githubOwnedIdentityKeys(item, ownership); const matchedIds: string[] = []; let untaggedNewEntries = 0; for (const entry of prepared) { @@ -3307,9 +3468,9 @@ export function verifyImportIdempotency( if (!importTagValues(entry).includes(provenanceTag(repo, entry.issueNumber))) untaggedNewEntries++; } const conflictingTargets = matchedIds.length - new Set(matchedIds).size; - if (duplicateTags.size > 0 || untaggedNewEntries > 0 || conflictingTargets > 0) { + if (untaggedNewEntries > 0 || conflictingTargets > 0) { throw new CommandError( - `pm github gate: import plan is not idempotent — ${duplicateTags.size} duplicate provenance tag(s) ` + + `pm github gate: import plan is not idempotent — ${duplicateIdentities.size} duplicate import identity claim(s) ` + `in the existing corpus, ${untaggedNewEntries} new entr(y/ies) without a provenance tag, ` + `${conflictingTargets} conflicting target(s). No item was written.`, EXIT_CODE.GENERIC_FAILURE, @@ -3317,7 +3478,7 @@ export function verifyImportIdempotency( } return { adopted: prepared.filter(entry => entry.identity?.via === "citation").length, - provenance_indexed: tagOwners.size, + provenance_indexed: identityOwners.size, matched_by_provenance: prepared.filter(entry => entry.match?.id && entry.identity?.via !== "citation").length, new_entries: prepared.length - matchedIds.length, }; @@ -3335,7 +3496,7 @@ export function verifyImportIdempotency( * @returns Number of durable issue identities verified. */ export function verifyImportedProvenance(existing: PmItem[], prepared: readonly PreparedGithubImport[], repo: string): number { - const index = indexByProvenance(existing); + const index = indexByImportIdentity(existing, buildGithubOwnershipIndex(existing, [repo]), new Set(prepared.map(entry => `${repo.toLowerCase()}#${entry.issueNumber}`))); const persisted = prepared.map(entry => ({ ...entry, match: index.get(`${repo.toLowerCase()}#${entry.issueNumber}`) })); if (persisted.some(entry => !entry.match?.id)) throw new CommandError("pm github gate: persisted corpus does not account for the complete import plan. Nothing may be pushed."); verifyImportIdempotency(existing, persisted, repo); @@ -3346,6 +3507,8 @@ export function verifyImportedProvenance(existing: PmItem[], prepared: readonly export interface ImportGateReceipt { /** The gate verdict for the proposed tracker change. */ verdict: GateVerdict; + /** Shared redacted evidence, retained only on a failed preflight and never staged. */ + diagnostic?: GateReport; /** Number of files whose added lines were scanned. */ scanned_files: number; /** Number of findings that survived the allowlist (0 on pass). */ @@ -3397,10 +3560,47 @@ function gateImportWrites(pmRoot: string): ImportGateReceipt { }; } +/** Use the apply plan's owned fields for both fresh and recovered privacy previews. */ +function importGatePlannedItems(planned: readonly PreparedGithubImport[]): TrackerGateInput["plannedItems"] { + return planned.map(entry => ({ + itemId: entry.match?.id ?? `github-issue-${entry.issueNumber}`, + fields: { ...entry.fidelity, title: entry.preserveContent ? entry.match!.title : entry.title, description: entry.preserveContent ? entry.match!.description : entry.description, body: entry.preserveContent ? entry.match!.body : entry.body, tags: entry.tags, + assignee: entry.preserveContent ? entry.match!.assignee : entry.assignee, sprint: entry.preserveContent ? entry.match!.sprint : entry.milestone, + comments: entry.syncAnnotations ? entry.comments.map(comment => ({ + author: comment.user?.login, text: comment.body, created_at: comment.created_at, + })) : [] }, + })); +} + +/** Scan both the content projection and exact SDK write payload with the shared privacy gate. */ +export function scanGithubImportPreflight(pmRoot: string, entries: readonly PreparedGithubImport[], mutations: readonly BulkItemMutation[] = [], persisted: unknown = { entries, mutations }): ImportGateReceipt { + const report = runCommandTrackerGate({ pmRoot, planOnly: true, plannedItems: [ + ...importGatePlannedItems(entries)!, + { itemId: "github-recovery-plan", fields: { recovery_plan: persisted } }, + ...mutations.map(mutation => ({ itemId: mutation.id, fields: mutation.op === "create" || mutation.op === "update" ? mutation.options : { ...mutation } })), + ] }); + return { verdict: report.verdict, scanned_files: report.scanned_files, findings: report.findings.length, allowlisted: report.allowlisted, ...(report.findings.length ? { diagnostic: report } : {}) }; +} + +/** Preserve the shared redacted finding envelope in typed no-write privacy refusals. */ +function githubImportPreflightRefusal(preflight: ImportGateReceipt): GithubImportRecoveryError { + return new GithubImportRecoveryError("github_import_preflight_failed", JSON.stringify({ + ...preflight.diagnostic, + error: "pm github gate: FAIL before write (privacy preflight); no item or recovery plan was written.", + })); +} + +/** A saved passing verdict and a fresh passing scan are both required before any replay mutation. */ +export function assertGithubImportReplayPreflight(pmRoot: string, record: GithubImportPlanRecord): void { + if (record.preflight?.verdict !== "pass") throw new GithubImportRecoveryError("github_import_preflight_failed", `Transaction ${record.transactionId} has a failed or missing saved privacy preflight. Review the original plan and establish a passing preflight before explicit recovery; no item was written.`); + const current = scanGithubImportPreflight(pmRoot, record.entries, record.mutations, record); + if (current.verdict !== "pass") throw new GithubImportRecoveryError("github_import_preflight_failed", `Transaction ${record.transactionId} failed its replay privacy preflight; no item was written.`); +} + /** * Run the full GitHub issue import flow. * - * Idempotent: items already linked (by provenance tag) to a fetched issue are + * Idempotent: items already linked by explicit provenance to a fetched issue are * UPDATEd; new issues are created. Honors `--atomic` (one crash-resumable * transaction) versus the per-item `pm` mutation path, optional `--link-deps`, * and `--dry-run`. With `--gate` the plan is verified (completeness + provenance @@ -3431,6 +3631,26 @@ export async function runImport( ); } const repo = repoArg; + const recoveryRoot = sdkResolvePmRoot(process.cwd(), pmPath); + const recovered = await resumePendingGithubImport(recoveryRoot, repo, {}, async record => { + if (opts.dryRun) throw new GithubImportRecoveryError("github_import_plan_pending", `Transaction ${record.transactionId} must replay its original plan before a fresh preview. Retry without --dry-run.`); + assertGithubImportReplayPreflight(recoveryRoot, record); + }); + if (recovered) { + failClosedOnPlanDivergence(opts.gate, 0, 0, recovered.commentWriteFailures ?? 0); + if (opts.gate) { + const corpus = certifyCompleteListResult(await listAllComplete({ includeBody: true }, { pmRoot: recoveryRoot, noExtensions: true })); + for (const [number, id] of recovered.itemIds) { + const items = decodeCompletePmItems(corpus); + const item = items.find(item => item.id === id); + if (!item) throw new GithubImportRecoveryError("github_import_owner_conflict", `Recovered target ${id} is missing; no publication is permitted.`); + assertGithubImportOwner(item, { id, repo, number }, items); + } + gateImportWrites(recoveryRoot); + } + return { transactionId: recovered.transactionId, recovered: recovered.recovered, recoveredItems: recovered.recoveredItems, + imported: recovered.imported, updated: recovered.updated, adopted: 0, skipped: 0, atomic: true, replayedOriginalPlan: true, ...(recovered.commentWriteFailures ? { commentWriteFailures: recovered.commentWriteFailures } : {}), ...depLinkResultFields(recovered.depLink) }; + } const selectedNumbers = opts.issue === undefined ? undefined : parseIssueNumbers(repo, opts.issue); const token = (dependencies.resolveToken ?? resolveGitHubToken)(); @@ -3499,8 +3719,12 @@ export async function runImport( return decodeCompletePmItems(corpus); }; const existingItems = await readImportItems(); - const existing = indexByProvenance(existingItems); - const fidelityContext = { statuses: workspace.statuses, items: existingItems, releases: readImportReleases(pmRoot), + const identityConflictFields: { identity_conflicts?: LegacyGithubIdentityConflict[] } = {}; + const ownership = buildGithubOwnershipIndex(existingItems, [repo]); + const importTargets = new Set(filtered.map(issue => `${repo.toLowerCase()}#${issue.number}`)); + warnGithubOwnershipScope(ownership, existingItems.filter(item => ownershipStateOf(ownership, item).claims.some(key => importTargets.has(key)))); + const existing = indexByImportIdentity(existingItems, ownership, importTargets); + const fidelityContext = { statuses: workspace.statuses, items: existingItems, ownership, releases: readImportReleases(pmRoot), timeline: dependencies.fetchIssueTimeline ?? (dependencies.fetchIssues ? async () => [] : fetchImportTimeline) }; let imported = 0; @@ -3560,24 +3784,28 @@ export async function runImport( return { dryRun: true, wouldImport: 0, wouldUpdate: 0, wouldAdopt: 0, wouldSkip: skippedNumbers.length, ...(opts.atomic ? { atomic: true } : {}), ...refusal, ...fetchFields }; } + const ownership = planned.length ? await readGithubOwnershipSnapshot(pmRoot, [repo]) : buildGithubOwnershipIndex([], [repo]); + for (const entry of planned) { + const target = githubImportTarget(repo, entry, + entry.match?.id ?? deriveAtomicItemId(repo, entry.issueNumber, workspace.settings.id_prefix, sdkNormalizeItemId), !entry.match?.id); + await readGithubImportOwner(pmRoot, target, ownership); + if (!entry.match?.id) assertGithubImportOwner({ id: target.id, author: "pm-github", description: entry.description, tags: importTagValues(entry) }, target); + } if (opts.gate) { gateCompleteness = verifyImportPlanCompleteness(filtered, planned, skippedNumbers); } if (opts.gate || planned.some(entry => entry.identity?.via === "citation")) { - gateIdempotency = verifyImportIdempotency(existingItems, planned, repo); + gateIdempotency = verifyImportIdempotency(existingItems, planned, repo, fidelityContext.ownership); } gatePlan = planned; gatePlanByNumber = new Map(planned.map((entry) => [entry.issueNumber, entry])); skipped = skippedNumbers.length; + if (opts.atomic) { + const preflight = scanGithubImportPreflight(pmRoot, planned, [], { entries: planned, ...(opts.linkDeps ? { dependencyIssues: filtered } : {}) }); + if (preflight.verdict !== "pass") throw githubImportPreflightRefusal(preflight); + } if (opts.gate) { - const preview = runCommandTrackerGate({ pmRoot, planOnly: opts.dryRun, plannedItems: planned.map(entry => ({ - itemId: entry.match?.id ?? `github-issue-${entry.issueNumber}`, - fields: { ...entry.fidelity, title: entry.title, description: entry.description, body: entry.body, tags: entry.tags, - assignee: entry.assignee, sprint: entry.milestone, - comments: entry.syncAnnotations ? entry.comments.map(comment => ({ - author: comment.user?.login, text: comment.body, created_at: comment.created_at, - })) : [] }, - })) }); + const preview = runCommandTrackerGate({ pmRoot, planOnly: opts.dryRun, plannedItems: importGatePlannedItems(planned) }); if (preview.verdict === "fail") throw new CommandError(JSON.stringify({ ...preview, error: "pm github gate: FAIL before write" })); } if (opts.gate) console.error( @@ -3593,16 +3821,16 @@ export async function runImport( return { issue: entry.issueNumber, item: entry.match?.id, action: importEntryUnchanged(entry, typeNames, workspace.statuses) ? "unchanged" : entry.identity?.via === "citation" ? "adopt" : entry.linkOnly ? "link" : entry.match?.id ? "update" : "create", status: entry.status, ...source, github_close_reason: reason, - created_at: entry.linkOnly ? entry.match!.created_at : source.github_created_at, + created_at: entry.linkOnly || entry.preserveContent ? entry.match!.created_at : source.github_created_at, updated_at_policy: "sdk-mutation-clock; source in github_updated_at", - closed_at: entry.linkOnly ? entry.match!.closed_at : source.github_closed_at, - close_reason: entry.linkOnly ? "preserved-provider-owner" : reason, - release: entry.linkOnly ? "preserved-provider-owner" : source.github_release, + closed_at: entry.linkOnly ? entry.match!.closed_at : importFidelityFields(source, false, entry.preserveContent ? entry.match : undefined).closed_at, + close_reason: entry.linkOnly ? "preserved-provider-owner" : entry.preserveContent ? "preserved-local-owner" : reason, + release: entry.linkOnly ? "preserved-provider-owner" : entry.preserveContent ? "preserved-local-owner" : source.github_release, identity: entry.identity, ...(entry.citation_candidates ? { citation_candidates: entry.citation_candidates } : {}) }; }); const adoptionCount = gatePlan!.filter(entry => entry.identity?.via === "citation").length; - const planFields = { ...(opts.gate || adoptionCount > 0 || citationCandidates.length > 0 ? { plan: fidelityPlan() } : {}), ...citationReceipt }; + const planFields = { ...identityConflictFields, ...(opts.gate || adoptionCount > 0 || citationCandidates.length > 0 ? { plan: fidelityPlan() } : {}), ...citationReceipt }; const gatePlanFields = { ...planFields, ...(opts.gate ? { gate: { completeness: gateCompleteness, idempotency: gateIdempotency, scan: opts.dryRun ? "plan" : "post-write" } } : {}) }; if (opts.atomic) { @@ -3670,16 +3898,17 @@ export async function runImport( const unchanged = changed.length === 0; const result = unchanged ? { transactionId: undefined, recovered: false, recoveredItems: undefined, imported: 0, updated: 0, itemIds: new Map(prepared.map(entry => [entry.issueNumber, entry.match!.id!])) } - : await (dependencies.commitAtomic ?? importGithubAtomic)(pmRoot, repo, changed); + : await (dependencies.commitAtomic ? dependencies.commitAtomic(pmRoot, repo, changed) : importGithubAtomic(pmRoot, repo, changed, { gate: opts.gate, linkDeps: opts.linkDeps, dependencyIssues: filtered })); for (const entry of prepared) if (!changed.includes(entry)) result.itemIds.set(entry.issueNumber, entry.match!.id!); if (opts.gate) verifyImportedProvenance(await readImportItems(), prepared, repo); // Planned native comments are part of the verified plan: a contended lock, // a refused add, or a committed entry whose created id is missing from the // post-commit routing map all leave planned comments unwritten, so they are // counted and gated runs fail closed below instead of exiting clean. - let commentWriteFailures = 0; + let commentWriteFailures = "commentWriteFailures" in result ? result.commentWriteFailures ?? 0 : 0; + const synchronized = new Set("annotationIssueNumbers" in result ? result.annotationIssueNumbers : []); for (const entry of prepared) { - if (!entry.syncAnnotations) continue; + if (!entry.syncAnnotations || synchronized.has(entry.issueNumber)) continue; const itemId = result.itemIds.get(entry.issueNumber); if (itemId) { commentWriteFailures += (await syncGithubCommentsToAnnotations( @@ -3687,6 +3916,7 @@ export async function runImport( entry.comments, pmRoot, entry.issueNumber, + githubImportTarget(repo, entry, itemId), )).failed; } else { commentWriteFailures += entry.comments.length; @@ -3703,7 +3933,7 @@ export async function runImport( `Atomically imported ${result.imported} new, updated ${result.updated} existing, skipped ${skipped}.`, ); } - const atomicDepLink = opts.linkDeps + const atomicDepLink = "depLink" in result && result.depLink ? result.depLink : opts.linkDeps ? await linkImportedDependencies(repo, filtered, pmRoot, dependencies) : undefined; reportDepLink(atomicDepLink); @@ -3722,6 +3952,7 @@ export async function runImport( ...(result.recoveredItems !== undefined ? { recoveredItems: result.recoveredItems } : {}), skipped, atomic: true, + ...(commentWriteFailures ? { commentWriteFailures } : {}), ...depLinkResultFields(atomicDepLink), ...fetchFields, ...planFields, @@ -3749,26 +3980,25 @@ export async function runImport( syncAnnotations, } = prepared; - /** - * Build the `pm close` argv for this issue's pm item. - * - * Two paths below close an item for the same reason — reconciling an - * already-matched item whose upstream issue is now closed, and closing an - * item that was just created `open` because pm-cli 2026.8.3 refuses a - * terminal `create --status closed`. Both carry the same source reason. - * Source completion is applied with source creation during finalization, - * avoiding an intermediate completion date before the SDK creation date. - */ - const githubCloseArgs = (id: string): string[] => { - const args = ["--path", pmRoot, "close", id, "--reason", prepared.fidelity!.github_close_reason]; - return args; + /** Native SDK writes retain expected revisions and validate the live corpus inside their item lock. */ + const ownedPmRun = async (id: string, action: "create" | "update" | "close", options: Record) => { + const target = githubImportTarget(repo, prepared, id, action === "create"); + const hash = await readGithubImportOwner(pmRoot, target); + try { + const result = await mutateGithubOwnedItem(pmRoot, target, action, options, hash); + return { ok: true, stdout: JSON.stringify(result), stderr: "" }; + } catch (error: unknown) { + if (error && typeof error === "object" && "code" in error && ["github_import_owner_conflict", "github_identity_conflict"].includes(String(error.code))) throw error; + await readGithubImportOwner(pmRoot, target); + return { ok: false, stdout: "", stderr: errorMessage(error) }; + } }; // Annotation sync still runs when only comments changed; native content // mutations must never advance the SDK clock for an identical projection. if (importEntryUnchanged(prepared, typeNames, workspace.statuses)) { if (!opts.dryRun && syncAnnotations) { - commentWriteFailures += (await syncGithubCommentsToAnnotations(match!.id!, comments, pmRoot, issue.number)).failed; + commentWriteFailures += (await syncGithubCommentsToAnnotations(match!.id!, comments, pmRoot, issue.number, githubImportTarget(repo, prepared, match!.id!))).failed; } continue; } @@ -3783,13 +4013,16 @@ export async function runImport( continue; } - if (prepared.linkOnly) { + if (prepared.linkOnly || prepared.preserveContent) { const ownerId = match!.id!; const nativeTags = importTagArguments(prepared); const linked = nativeTags.length - ? pmRun(["--path", pmRoot, "update", ownerId, ...nativeTags]) : { ok: true }; + ? await ownedPmRun(ownerId, "update", importTagOptions(prepared)) : { ok: true }; if (!linked.ok) throw new CommandError(`Could not link GitHub issue #${issue.number} to its existing owner.`); - await persistImportFidelity(pmRoot, ownerId, prepared.fidelity!, true); + if (syncAnnotations) { + commentWriteFailures += (await syncGithubCommentsToAnnotations(ownerId, comments, pmRoot, issue.number, githubImportTarget(repo, prepared, ownerId))).failed; + } + await persistImportFidelity(pmRoot, ownerId, prepared.fidelity!, Boolean(prepared.linkOnly), false, undefined, prepared.preserveContent, githubImportTarget(repo, prepared, ownerId)); if (prepared.identity?.via === "citation") adopted++; else updated++; continue; @@ -3797,20 +4030,10 @@ export async function runImport( if (match?.id) { // Idempotent update — never duplicate. Status transitions go through the // proper command (close requires a reason; reopen via update --status). - const nativeTags = importTagArguments(prepared); - const updArgs = [ - "--path", pmRoot, "update", match.id, - "--title", title, - "--description", description, - "--body", body, - ...nativeTags, - "--message", `Re-imported from GitHub #${issue.number}`, - ]; - // GH-1614: JSON document fields preserve literal source identities; - // legacy --assignee/--sprint flags interpret none/null as clear tokens. - const upd = pmRun([...updArgs, "--stdin-json"], JSON.stringify({ + const upd = await ownedPmRun(match.id, "update", { + title, description, body, ...importTagOptions(prepared), message: `Re-imported from GitHub #${issue.number}`, ...(assignee ? { assignee } : {}), ...(milestone ? { sprint: milestone } : {}), - })); + }); if (!upd.ok) { console.error(`#${issue.number}: update failed — ${upd.stderr}`); skipped++; @@ -3818,20 +4041,20 @@ export async function runImport( } // Reconcile status separately. if (canonicalGithubStatus(status, workspace.statuses) === canonicalGithubStatus("canceled", workspace.statuses) && canonicalGithubStatus(match.status, workspace.statuses) !== canonicalGithubStatus("canceled", workspace.statuses)) { - const cancel = pmRun(["--path", pmRoot, "update", match.id, "--status", "canceled", "--close-reason", prepared.fidelity!.github_close_reason]); + const cancel = await ownedPmRun(match.id, "update", { status: "canceled", closeReason: prepared.fidelity!.github_close_reason }); if (!cancel.ok) throw new CommandError(`Could not cancel GitHub issue #${issue.number}.`); } if (canonicalGithubStatus(status, workspace.statuses) === canonicalGithubStatus("closed", workspace.statuses) && canonicalGithubStatus(match.status, workspace.statuses) !== canonicalGithubStatus("closed", workspace.statuses)) { // Close through `pm close` so the reason is real provenance; pass the // source completion time as --completed-at when GitHub recorded one. - const close = pmRun([...githubCloseArgs(match.id), ...(githubStatusIsTerminal(match.status, workspace.statuses) ? ["--force"] : [])]); + const close = await ownedPmRun(match.id, "close", { reason: prepared.fidelity!.github_close_reason, ...(githubStatusIsTerminal(match.status, workspace.statuses) ? { force: true } : {}) }); if (!close.ok) { console.error(`#${issue.number}: close reconciliation failed — ${close.stderr}`); skipped++; continue; } } else if (canonicalGithubStatus(status, workspace.statuses) === canonicalGithubStatus("open", workspace.statuses) && githubStatusIsTerminal(match.status, workspace.statuses)) { - const reopen = pmRun(["--path", pmRoot, "update", match.id, "--status", "open", "--message", `GitHub issue #${issue.number} reopened`]); + const reopen = await ownedPmRun(match.id, "update", { status: "open", message: `GitHub issue #${issue.number} reopened` }); if (!reopen.ok) { console.error(`#${issue.number}: reopen reconciliation failed — ${reopen.stderr}`); skipped++; @@ -3839,7 +4062,7 @@ export async function runImport( } } if (syncAnnotations) { - commentWriteFailures += (await syncGithubCommentsToAnnotations(match.id, comments, pmRoot, issue.number)).failed; + commentWriteFailures += (await syncGithubCommentsToAnnotations(match.id, comments, pmRoot, issue.number, githubImportTarget(repo, prepared, match.id))).failed; } await persistImportFidelity(pmRoot, match.id, prepared.fidelity!, false, false, assignee); if (prepared.identity?.via === "citation") adopted++; @@ -3853,59 +4076,30 @@ export async function runImport( // completion timestamp (`closed_at`) as --completed-at provenance. const createStatus = canonicalGithubStatus(status, workspace.statuses) === canonicalGithubStatus("closed", workspace.statuses) ? "open" : status; const mustClose = canonicalGithubStatus(status, workspace.statuses) === canonicalGithubStatus("closed", workspace.statuses); - const createArgs = [ - "--path", pmRoot, "create", - "--title", title, - "--type", opts.itemType, - "--status", createStatus, - "--description", description, - "--body", body, - "--tags", importTagOptions(prepared).tags!, - "--message", `Imported from GitHub #${issue.number}`, - ]; - if (canonicalGithubStatus(status, workspace.statuses) === canonicalGithubStatus("canceled", workspace.statuses)) createArgs.push("--close-reason", prepared.fidelity!.github_close_reason); - // --json lets a following close (or annotations sync) address the freshly - // created item by id without re-scanning the workspace. - createArgs.push("--json"); - const created = pmRun([...createArgs, "--stdin-json"], JSON.stringify({ + const destination = deriveAtomicItemId(repo, issue.number, workspace.settings.id_prefix, sdkNormalizeItemId); + const created = await ownedPmRun(destination, "create", { + title, type: opts.itemType, author: "pm-github", status: createStatus, description, body, ...importTagOptions(prepared), + message: `Imported from GitHub #${issue.number}`, + ...(canonicalGithubStatus(status, workspace.statuses) === canonicalGithubStatus("canceled", workspace.statuses) ? { closeReason: prepared.fidelity!.github_close_reason } : {}), ...(assignee ? { assignee } : {}), ...(milestone ? { sprint: milestone } : {}), - })); + }); if (!created.ok) { console.error(`#${issue.number}: create failed — ${created.stderr}`); skipped++; continue; } - const createdId = parseCreatedItemId(created.stdout); + const createdId = destination; if (mustClose) { - if (createdId) { - const close = pmRun(githubCloseArgs(createdId)); - if (!close.ok) { - console.error(`#${issue.number}: close after import failed — ${close.stderr}`); - skipped++; - continue; - } - } else { - // The item exists but is still open, and without its id nothing here can - // close it. Counting it as imported would report a closed GitHub issue as - // a successfully imported *open* item, so it is reported as skipped — - // the same accounting the close-failure branch above already uses. - console.error(`#${issue.number}: could not parse created item id — left open`); + const close = await ownedPmRun(createdId, "close", { reason: prepared.fidelity!.github_close_reason }); + if (!close.ok) { + console.error(`#${issue.number}: close after import failed — ${close.stderr}`); skipped++; continue; } } if (syncAnnotations) { - if (createdId) { - commentWriteFailures += (await syncGithubCommentsToAnnotations(createdId, comments, pmRoot, issue.number)).failed; - } else { - // Without the created id the planned comments cannot be written; the - // gated plan-divergence refusal below turns that into a fail-closed exit - // while ungated runs keep today's warn-and-continue behaviour. - console.error(`#${issue.number}: could not parse created item id — comments not synced`); - commentWriteFailures += comments.length; - } + commentWriteFailures += (await syncGithubCommentsToAnnotations(createdId, comments, pmRoot, issue.number, githubImportTarget(repo, prepared, createdId))).failed; } - if (!createdId) throw new CommandError(`Could not finalize GitHub issue #${issue.number}: created id missing.`); await persistImportFidelity(pmRoot, createdId, prepared.fidelity!, false, false, assignee); imported++; } @@ -3953,7 +4147,7 @@ export async function runImport( // pushed; scan the proposed tracker change now and fail closed. if (opts.gate) verifyImportedProvenance(await readImportItems(), gatePlan!, repo); const gateReceipt = opts.gate ? gateImportWrites(pmRoot) : undefined; - return { imported, updated, adopted, skipped, ...fetchFields, ...depLinkResultFields(depLink), ...planFields, ...(gateReceipt ? { gate: gateReceipt } : {}) }; + return { imported, updated, adopted, skipped, ...(commentWriteFailures ? { commentWriteFailures } : {}), ...fetchFields, ...depLinkResultFields(depLink), ...planFields, ...(gateReceipt ? { gate: gateReceipt } : {}) }; } /** @@ -4062,29 +4256,44 @@ function requireRequestedItems(items: TItem[], requested: return scoped.selected; } -// Build the pm → GitHub issue sync plan: for each pm item linked to `repo`, emit -// a create-or-update entry keyed by the issue's provenance tag. -export function planSync(items: PmItem[], repo: string, statuses?: GithubStatusRegistry): SyncPlanEntry[] { - const plan: SyncPlanEntry[] = []; - const repoLc = repo.toLowerCase(); +/** Legacy option retained for compatibility; tags always remain references. */ +export interface UpstreamIdentityOptions { allowReferenceTags?: boolean; corpus?: readonly PmItem[] } + +/** Resolve one upstream target per item and reject competing owners before any remote write. */ +function upstreamTargets(items: readonly PmItem[], repo: string, ownership: GithubOwnershipIndex): Map { + const targets = new Map(); for (const item of items) { if (!item.id) continue; - for (const tag of item.tags ?? []) { - const p = parseProvenanceTag(tag); - if (!p || p.repo !== repoLc) continue; - const desired = pmStatusToGithubState(item.status, statuses); - plan.push({ - id: item.id, - number: p.number, - title: item.title ?? "(untitled)", - // `from` is unknown without a fetch; the planner records desired state - // and the executor only PATCHes when GitHub disagrees. - from: desired === "open" ? "closed" : "open", - to: desired, - }); + for (const key of githubOwnedIdentityKeys(item, ownership, repo)) { + if (key.slice(0, key.lastIndexOf("#")) === repo.toLowerCase()) targets.set(item.id, Number(key.slice(key.lastIndexOf("#") + 1))); } } - return plan; + return targets; +} + +/** Re-resolve current ownership immediately before sending an upstream write. */ +export function assertCurrentUpstreamTarget(pmRoot: string, repo: string, entry: { id?: string; number?: number }, options: UpstreamIdentityOptions): void { + const corpus = readPmItems(pmRoot); + const item = corpus.find(item => item.id === entry.id); + if (!item) throw new GithubImportOwnershipError(entry.id ?? "", `${repo.toLowerCase()}#${entry.number ?? "new"}`, ["missing tracker item"]); + const ownership = buildGithubOwnershipIndex(corpus, [repo]); + const keys = githubOwnedIdentityKeys(item, ownership); + const current = keys.filter(key => key.startsWith(`${repo.toLowerCase()}#`)).map(key => Number(key.split("#")[1]))[0]; + if (current !== entry.number) throw new GithubImportOwnershipError(item.id!, `${repo.toLowerCase()}#${entry.number ?? "new"}`, githubOwnershipClaims(item).claims); +} + +// Build the pm → GitHub state plan from the same explicit ownership resolver used by export. +export function planSync(items: PmItem[], repo: string, statuses?: GithubStatusRegistry, options: UpstreamIdentityOptions = {}): SyncPlanEntry[] { + const ownership = buildGithubOwnershipIndex(options.corpus ?? items, repo ? [repo] : []); + for (const item of items) githubOwnedIdentityKeys(item, ownership); + warnGithubOwnershipScope(ownership, items); + const targets = upstreamTargets(items, repo, ownership); + return items.flatMap(item => { + const number = item.id ? targets.get(item.id) : undefined; + if (number === undefined) return []; + const desired = pmStatusToGithubState(item.status, statuses); + return [{ id: item.id!, number, title: item.title ?? "(untitled)", from: desired === "open" ? "closed" : "open", to: desired }]; + }); } // Command handler for `pm github sync`: preview or apply the pm → GitHub issue @@ -4119,17 +4328,20 @@ async function runSync(ctx: CommandHandlerContext) { ); } - const selected = requireRequestedItems(readPmItems(ctx.pm_root), scopedIds); + const corpus = readPmItems(ctx.pm_root); + const conflictFields: { identity_conflicts?: LegacyGithubIdentityConflict[] } = {}; + const selected = requireRequestedItems(corpus, scopedIds); const workspace = await readGithubWorkspace(ctx.pm_root); - const plan = planSync(selected, repo, workspace.statuses); + const plan = planSync(selected, repo, workspace.statuses, { corpus, allowReferenceTags: optionEnabled(options, "allow-reference-tags", "allowReferenceTags") }); if (plan.length === 0) { const scopeNote = scopedIds.length > 0 ? ` from --ids (${scopedIds.join(", ")})` : ""; console.error( `No pm items${scopeNote} linked to ${repo} ` + - `(no \`gh:${repo.toLowerCase()}#N\` provenance tags).`, + `(set github_url explicitly; reference tags never establish links).`, ); return { + ...conflictFields, synced: 0, skipped: 0, planned: 0, @@ -4163,6 +4375,7 @@ async function runSync(ctx: CommandHandlerContext) { synced++; continue; } + assertCurrentUpstreamTarget(ctx.pm_root, repo, entry, { allowReferenceTags: optionEnabled(options, "allow-reference-tags", "allowReferenceTags") }); try { await request( "PATCH", @@ -4182,6 +4395,7 @@ async function runSync(ctx: CommandHandlerContext) { if (dryRun) { console.error(`[dry-run] Would update ${synced} issue(s) on ${repo}; ${skipped} already in sync/failed.`); return { + ...conflictFields, dryRun: true, wouldSync: synced, skipped, @@ -4190,7 +4404,7 @@ async function runSync(ctx: CommandHandlerContext) { }; } console.error(`Synced ${synced} issue(s) on ${repo}; skipped ${skipped}.`); - return { synced, skipped, planned: plan.length, ...(scopedIds.length > 0 ? { scoped_ids: scopedIds } : {}) }; + return { ...conflictFields, synced, skipped, planned: plan.length, ...(scopedIds.length > 0 ? { scoped_ids: scopedIds } : {}) }; } // --------------------------------------------------------------------------- @@ -4232,30 +4446,24 @@ export interface ExportPlanEntry { payload: GithubExportPayload; } -// Build the create/update plan. `repo` (lowercased) decides which provenance -// tags count as an "already exported to THIS repo" link → update; everything -// else is a create. Pure + side-effect free so it can be unit-tested and -// printed verbatim in --dry-run. +// Build create/update entries only for selected items, retaining the complete +// ownership snapshot to refuse competing peers before each upstream write. export function buildExportPlan( items: PmItem[], repo: string | undefined, labelMap?: Map, statuses?: GithubStatusRegistry, + options: UpstreamIdentityOptions = {}, ): ExportPlanEntry[] { const repoLc = repo?.toLowerCase(); + const ownership = buildGithubOwnershipIndex(options.corpus ?? items, repo ? [repo] : []); + for (const item of items) githubOwnedIdentityKeys(item, ownership); + warnGithubOwnershipScope(ownership, items); + const targets = repoLc ? upstreamTargets(items, repoLc, ownership) : new Map(); const plan: ExportPlanEntry[] = []; for (const item of items) { const payload = itemToGithubPayload(item, labelMap, statuses); - let number: number | undefined; - if (repoLc) { - for (const tag of item.tags ?? []) { - const p = parseProvenanceTag(tag); - if (p && p.repo === repoLc) { - number = p.number; - break; - } - } - } + const number = item.id ? targets.get(item.id) : undefined; plan.push({ id: item.id, action: number === undefined ? "create" : "update", @@ -4263,6 +4471,7 @@ export function buildExportPlan( payload, }); } + exportOwnershipContexts.set(plan, { items, options }); return plan; } @@ -4302,6 +4511,9 @@ export type ExportRequestFn = ( payload?: string, ) => Promise; +/** Source snapshots for package-built plans; raw transport plans contain only explicit caller-supplied targets. */ +const exportOwnershipContexts = new WeakMap(); + /** * Apply an already-built export plan to GitHub, one issue at a time. * @@ -4316,11 +4528,22 @@ export async function applyExportPlan( repo: string, token: string | undefined, requestFn: ExportRequestFn, + beforeWrite?: (entry: ExportPlanEntry) => void | Promise, ): Promise { let created = 0; let updated = 0; const failures: ExportApplyFailure[] = []; for (const entry of plan) { + const context = exportOwnershipContexts.get(plan); + if (context) { + const corpus = context.options.corpus ?? context.items; + const item = context.items.find(item => item.id === entry.id); + const ownership = buildGithubOwnershipIndex(corpus, [repo]); + const keys = item ? githubOwnedIdentityKeys(item, ownership) : []; + const number = keys.filter(key => key.startsWith(`${repo.toLowerCase()}#`)).map(key => Number(key.split("#")[1]))[0]; + if (number !== entry.number) throw new GithubImportOwnershipError(entry.id ?? "", `${repo.toLowerCase()}#${entry.number ?? "new"}`, item ? githubOwnershipClaims(item).claims : ["missing tracker item"]); + } + await beforeWrite?.(entry); const p = entry.payload; try { // An "update" entry must carry the issue number; without it we must NOT @@ -4459,7 +4682,7 @@ async function runExport(ctx: CommandHandlerContext) { refuseIssueForm("invalid_form_input", [{ field: "form", reason: "selection", remedy: "Pass --form with --repo and --form-ref to enforce an issue-form contract." }]); } const workspace = await readGithubWorkspace(ctx.pm_root); - const plan = buildExportPlan(selected, repo, labelMap, workspace.statuses); + const plan = buildExportPlan(selected, repo, labelMap, workspace.statuses, { corpus: items, allowReferenceTags: optionEnabled(options, "allow-reference-tags", "allowReferenceTags") }); for (const [index, completion] of completions.entries()) { plan[index]!.payload.body = completion.body; plan[index]!.payload.labels = uniqueGithubLabelNames([...plan[index]!.payload.labels, ...completion.report.contract.labels]); @@ -4531,6 +4754,7 @@ async function runExport(ctx: CommandHandlerContext) { repo, token, request, + entry => assertCurrentUpstreamTarget(ctx.pm_root, repo, entry, { allowReferenceTags: optionEnabled(options, "allow-reference-tags", "allowReferenceTags") }), ); if (!jsonMode) { console.error(`Created ${created} and updated ${updated} issue(s) on ${repo}.`); @@ -4589,14 +4813,15 @@ export type BlockerReferenceForm = | "url" // https://github.com/owner/repo/issues/N or /pull/N | "gh_dash" // `GH-N` | "short_repo_hash" // `name#N` (bare repo short name, resolved against a hint) - | "bare_hash"; // `#N` (resolved against a hint) + | "bare_hash" // `#N` (resolved against a hint) + | "unparsed_url"; // HTTP URL candidate without a supported GitHub identity /** One GitHub reference found in blocker text, before its repo hint is applied. */ export interface ParsedBlockerReference { /** `owner/repo` (lowercase) when the reference carries it; `undefined` for hint-resolved forms. */ repo?: string; - /** Issue or pull-request number. */ - number: number; + /** Issue or PR number; absent for an unparsed URL. */ + number?: number; /** The syntactic form the reference was recognized in. */ form: BlockerReferenceForm; /** The literal matched text (for report provenance). */ @@ -4609,8 +4834,8 @@ export interface BlockerReference { key: string; /** `owner/repo` (lowercase); `undefined` when no hint could resolve the reference. */ repo?: string; - /** Issue or pull-request number. */ - number: number; + /** Issue or PR number; absent for an unparsed URL. */ + number?: number; /** The syntactic form the reference was recognized in. */ form: BlockerReferenceForm; /** The literal matched text (for report provenance). */ @@ -4621,14 +4846,14 @@ export interface BlockerReference { export interface BlockerReferenceState { /** `owner/repo` (lowercase); `undefined` when the reference never resolved to a repo. */ repo?: string; - /** Issue or pull-request number. */ - number: number; + /** Issue or PR number; absent for an unparsed URL. */ + number?: number; /** The syntactic form the reference was recognized in. */ form: BlockerReferenceForm; /** The literal matched text (for report provenance). */ raw: string; /** Live state, or the reason it is unknown. */ - state: "open" | "closed" | "merged" | "not_found" | "error" | "unresolved"; + state: "open" | "closed" | "merged" | "not_found" | "error" | "unresolved" | "unparsed"; /** GitHub close reason for issues (`COMPLETED`, `NOT_PLANNED`, `REOPENED`), when known. */ state_reason?: string; /** ISO timestamp the issue closed, when known. */ @@ -4691,7 +4916,6 @@ export interface BlockersReport { // The prefix class excludes word characters, path separators, dots, and colons, // so `https://github.com/o/r/issues/N` (slash before owner) and // `github.com/o/r#N` never produce a second, shorter match inside a URL. -const BLOCKER_URL_RE = /https?:\/\/(?:www\.)?github\.com\/([A-Za-z0-9][A-Za-z0-9_.-]*)\/([A-Za-z0-9_.-]+)\/(?:issues|pull)\/([1-9]\d{0,8})(?!\w)/gi; const BLOCKER_OWNER_REPO_RE = /(^|[^\w./:-])([A-Za-z0-9][A-Za-z0-9_.-]*\/[A-Za-z0-9_.-]+)#([1-9]\d{0,8})(?!\w)/g; const BLOCKER_GH_DASH_RE = /\bGH-([1-9]\d{0,8})\b/gi; const BLOCKER_SHORT_REPO_RE = /(^|[^\w./:-])([A-Za-z0-9_.-]+)#([1-9]\d{0,8})(?!\w)/g; @@ -4725,24 +4949,33 @@ function maskNonGithubLocators(text: string): string { * never yields the same (form, raw, number) twice. * * @param text - One blocker text source (blocked_reason, description, comment, …). - * @returns Every recognized reference, in form order (URL, owner/repo, GH-N, short, bare). + * @returns Every URL (including unparsed candidates), then owner/repo, GH-N, short and bare forms. */ export function parseBlockerReferences(text: string): ParsedBlockerReference[] { const masked = maskNonGithubLocators(text); const out: ParsedBlockerReference[] = []; - for (const m of masked.matchAll(BLOCKER_URL_RE)) { - out.push({ repo: `${m[1]!.toLowerCase()}/${m[2]!.toLowerCase()}`, number: Number(m[3]), form: "url", raw: m[0] }); - } - for (const m of masked.matchAll(BLOCKER_OWNER_REPO_RE)) { + const tokens = githubUrlTokens(masked); + const pieces: string[] = []; + let previous = 0; + for (const token of tokens) { + const target = parseGithubIssueUrl(token.raw); + if (target && !target.api && target.number <= 999999999 && BLOCKER_REPO_SHAPE.test(target.repo)) out.push({ repo: target.repo, number: target.number, form: "url", raw: token.raw }); + else out.push({ form: "unparsed_url", raw: token.raw }); + pieces.push(masked.slice(previous, token.index), " ".repeat(token.end - token.index)); + previous = token.end; + } + // A malformed URL remains one reference; never guess from a hash/query inside it. + const shorthand = pieces.join("") + masked.slice(previous); + for (const m of shorthand.matchAll(BLOCKER_OWNER_REPO_RE)) { out.push({ repo: m[2]!.toLowerCase(), number: Number(m[3]), form: "repo_hash", raw: `${m[2]}#${m[3]}` }); } - for (const m of masked.matchAll(BLOCKER_GH_DASH_RE)) { + for (const m of shorthand.matchAll(BLOCKER_GH_DASH_RE)) { out.push({ number: Number(m[1]), form: "gh_dash", raw: m[0]!.toUpperCase() }); } - for (const m of masked.matchAll(BLOCKER_SHORT_REPO_RE)) { + for (const m of shorthand.matchAll(BLOCKER_SHORT_REPO_RE)) { out.push({ number: Number(m[3]), form: "short_repo_hash", raw: `${m[2]}#${m[3]}` }); } - for (const m of masked.matchAll(BLOCKER_BARE_HASH_RE)) { + for (const m of shorthand.matchAll(BLOCKER_BARE_HASH_RE)) { out.push({ number: Number(m[2]), form: "bare_hash", raw: `#${m[2]}` }); } return out; @@ -4771,6 +5004,11 @@ export function resolveBlockerReferences( const out: BlockerReference[] = []; const seen = new Set(); for (const ref of parsed) { + if (ref.form === "unparsed_url" || ref.number === undefined) { + const key = `unparsed:${ref.raw}`; + if (!seen.has(key)) { seen.add(key); out.push({ key, form: "unparsed_url", raw: ref.raw }); } + continue; + } let repo = ref.repo; if (repo === undefined) { for (const hint of [itemRepo, fallbackRepo]) { @@ -4797,14 +5035,14 @@ export function resolveBlockerReferences( * item-level `blocked_by` field, `blocked_reason`, `description`, and every * stored comment. A `blocked_reason` is a static string that nothing re-reads — * that is exactly the staleness this command exists to surface — so comments - * (including the latest ones) are scanned too. References matching any of the - * item's own GitHub provenance tags are excluded: an issue cannot block itself. + * (including the latest ones) are scanned too. Only authoritative import identity + * is excluded as self; shareable reference tags never hide a blocker. * * @param item - One complete item from {@link decodeBlockerPmItems}. * @param fallbackRepo - The `--repo` hint, when given. * @returns Deduplicated resolved references for the item. */ -export function collectBlockerReferences(item: BlockerPmItem, fallbackRepo: string | undefined): BlockerReference[] { +export function collectBlockerReferences(item: BlockerPmItem, fallbackRepo: string | undefined, peers: readonly PmItem[] = [item]): BlockerReference[] { const texts: string[] = []; for (const dep of item.dependencies) { if (dep.kind === "blocked_by" && isGithubBlockerDependency(dep.id)) texts.push(dep.id); @@ -4821,10 +5059,7 @@ export function collectBlockerReferences(item: BlockerPmItem, fallbackRepo: stri return undefined; })(); const parsed = texts.flatMap((text) => parseBlockerReferences(text)); - const ownKeys = new Set((item.tags ?? []).flatMap((tag) => { - const provenance = parseProvenanceTag(tag); - return provenance ? [`${provenance.repo}#${provenance.number}`] : []; - })); + const ownKeys = new Set(githubOwnedIdentityKeys(item, peers)); return resolveBlockerReferences(parsed, itemRepo, fallbackRepo).filter((ref) => !ownKeys.has(ref.key)); } @@ -5290,6 +5525,7 @@ export function buildBlockerClearedComment(state: BlockerReferenceState): string const repo = state.repo ?? ""; const { disposition, at } = blockerDispositionAndDate(state); const human = at === "" ? "an unknown date" : at.slice(0, 10); + if (state.number === undefined) throw new CommandError("An unparsed blocker reference cannot be announced as cleared."); const marker = blockerCommentMarker(repo, state.number, disposition, at === "" ? "unknown" : at); return `Blocker ${repo}#${state.number} ${disposition} on ${human}; revalidate.\n\n${marker}`; } @@ -5362,6 +5598,7 @@ export async function appendBlockerComments( const markers = extractBlockerCommentMarkers(existing); const pending = entry.states.filter((state) => { const { disposition, at } = blockerDispositionAndDate(state); + if (state.number === undefined) throw new CommandError("An unparsed blocker reference cannot be announced as cleared."); return !markers.has(blockerCommentMarker(state.repo ?? "", state.number, disposition, at === "" ? "unknown" : at)); }); if (pending.length === 0) { skipped++; continue; } @@ -5429,12 +5666,12 @@ async function runBlockers(ctx: CommandHandlerContext) { const selected = requireRequestedItems(corpus, scopedIds); const workspace = await readGithubWorkspace(ctx.pm_root); const candidates = selectBlockerCandidates(selected, workspace.statuses); - const perItem = candidates.map((item) => ({ item, references: collectBlockerReferences(item, fallbackRepo) })); + const perItem = candidates.map((item) => ({ item, references: collectBlockerReferences(item, fallbackRepo, corpus) })); const unique = new Map(); for (const entry of perItem) { for (const ref of entry.references) { - if (ref.repo !== undefined && !unique.has(ref.key)) { + if (ref.repo !== undefined && ref.number !== undefined && ref.form !== "unparsed_url" && !unique.has(ref.key)) { unique.set(ref.key, { key: ref.key, repo: ref.repo, number: ref.number, form: ref.form, raw: ref.raw }); } } @@ -5447,7 +5684,7 @@ async function runBlockers(ctx: CommandHandlerContext) { const items: BlockerItemReport[] = perItem.map(({ item, references }) => { const refStates: BlockerReferenceState[] = references.map((ref) => stateByKey.get(ref.key) - ?? { repo: ref.repo, number: ref.number, form: ref.form, raw: ref.raw, state: "unresolved" as const }); + ?? { repo: ref.repo, number: ref.number, form: ref.form, raw: ref.raw, state: ref.form === "unparsed_url" ? "unparsed" as const : "unresolved" as const }); return { id: item.id, status: item.status, @@ -6084,7 +6321,7 @@ async function gqlSetStatus( * @param token - GitHub token. * @returns The node id, or `undefined` when not found or the repo ref is malformed. */ -async function gqlResolveIssueNodeId( +export async function gqlResolveIssueNodeId( repo: string, number: number, token: string | undefined, @@ -6426,24 +6663,42 @@ async function runProjectImport(ctx: CommandHandlerContext) { * @param token - GitHub token for the GraphQL calls. * @returns Whether the item changed, plus an optional error message. */ -async function applyPushEntry( +export async function applyPushEntry( entry: ReturnType["entries"][number], meta: ProjectMeta, ref: ProjectRef, pmById: Map, pmRoot: string, token: string | undefined, + projectItems: ProjectItem[], ): Promise<{ changed: boolean; error?: string }> { try { + const validate = () => { + const corpus = readPmItems(pmRoot); + const current = corpus.find(item => item.id === entry.pmId); + if (!current) throw new GithubImportOwnershipError(entry.pmId, "project association", ["missing tracker item"]); + const content = projectItems.find(item => item.id === entry.itemId)?.content; + const repo = entry.issueRepo ?? content?.repo; + const number = entry.issueNumber ?? content?.number; + const ownership = buildGithubOwnershipIndex(corpus, repo ? [repo] : []); + const keys = githubOwnedIdentityKeys(current, ownership); + if (entry.action === "add-issue") { + assertGithubImportOwner(current, { id: current.id!, repo: entry.issueRepo!, number: entry.issueNumber! }, ownership); + } else if (keys.length === 0 && entry.action !== "add-draft" && !current.tags?.includes(projectItemTag(ref, entry.itemId!))) { + throw new GithubImportOwnershipError(entry.pmId, "project association", ["missing current board tag or draft authorization"]); + } else if (repo && number && keys.length) assertGithubOwnershipConsistency(current, { id: current.id!, repo, number }, ownership); + }; + validate(); let itemId = entry.itemId; if (entry.action === "add-draft") { const pm = pmById.get(entry.pmId); itemId = await gqlAddDraft(meta.id, entry.title, pm!.body || pm!.description!, token); } else if (entry.action === "add-issue") { // The private executor receives only buildProjectPushPlan entries. Its - // add-issue arm always sets both coordinates from a parsed provenance tag. + // add-issue arm sets both coordinates from shared, consistent ownership. const contentId = await gqlResolveIssueNodeId(entry.issueRepo!, entry.issueNumber!, token); if (!contentId) return { changed: false, error: `could not resolve node id for ${entry.issueRepo}#${entry.issueNumber}` }; + validate(); itemId = await gqlAddIssue(meta.id, contentId, token); } @@ -6525,9 +6780,19 @@ export function buildPullEntryArgs(entry: PullPlanEntryLike, pmRoot: string, sta // (records closed_at + close_reason); `canceled` keeps its distinct terminal // state via `pm update` while recording `--close-reason`; everything else is a // plain `pm update --status`. -function applyPullEntry(entry: PullPlanEntryLike, pmRoot: string, statuses?: GithubStatusRegistry): { changed: boolean; error?: string } { - const res = pmRun(buildPullEntryArgs(entry, pmRoot, statuses)); - return res.ok ? { changed: true } : { changed: false, error: res.stderr }; +/** Apply a project status only after its original ownership decision passes the native locked guard. @internal */ +export async function applyPullEntry(entry: PullPlanEntryLike, pmRoot: string, statuses?: GithubStatusRegistry): Promise<{ changed: boolean; error?: string }> { + try { + const expected = getProjectPullOwners(entry) ?? []; + const reason = `GitHub project status → ${entry.toStatus}`; + const closed = canonicalGithubStatus(entry.toStatus, statuses) === (statuses?.close_status ?? "closed"); + await withGithubItemConsistency(pmRoot, entry.pmId, expected, () => PmClient.forActiveExtensionHost({ pmRoot, noExtensions: true }).run( + closed ? "close" : "update", { id: entry.pmId, options: closed ? { reason } : { + status: canonicalGithubStatus(entry.toStatus, statuses), message: reason, + ...(githubStatusIsTerminal(entry.toStatus, statuses) ? { closeReason: reason } : {}), + } })); + return { changed: true }; + } catch (error: unknown) { return { changed: false, error: errorMessage(error) }; } } interface PullPlanEntryLike { @@ -6593,10 +6858,10 @@ async function runProjectSync(ctx: CommandHandlerContext) { const workspace = await readGithubWorkspace(ctx.pm_root); const pushPlan = doPush - ? buildProjectPushPlan(scoped.selected, ref, projectItems, meta.statusField, { addMissing, statusMap, statuses: workspace.statuses }) + ? buildProjectPushPlan(scoped.selected, ref, projectItems, meta.statusField, { addMissing, statusMap, statuses: workspace.statuses, corpus: allPm }) : undefined; const pullPlan = doPull - ? buildProjectPullPlan(scoped.selected, ref, projectItems, statusMap, workspace.statuses) + ? buildProjectPullPlan(scoped.selected, ref, projectItems, statusMap, workspace.statuses, allPm) : undefined; // Conflict resolution when applying BOTH directions: a linked item can appear @@ -6648,7 +6913,7 @@ async function runProjectSync(ctx: CommandHandlerContext) { if (prefer === "github" && e.action === "set-status" && pullItemIds.has(e.itemId!)) { continue; // pull wins for this linked item } - const r = await applyPushEntry(e, meta, ref, pmById, ctx.pm_root, token); + const r = await applyPushEntry(e, meta, ref, pmById, ctx.pm_root, token, projectItems); if (r.error && !r.changed) { console.error(` push ${e.pmId} "${e.title}": ${r.error}`); pushFailed++; continue; } if (r.error) console.error(` push ${e.pmId}: ${r.error}`); pushed++; @@ -6660,7 +6925,7 @@ async function runProjectSync(ctx: CommandHandlerContext) { if (applyPull && pullPlan) { for (const e of pullActionable) { if (prefer === "pm" && pushItemIds.has(e.itemId)) continue; // push wins - const r = applyPullEntry(e, ctx.pm_root, workspace.statuses); + const r = await applyPullEntry(e, ctx.pm_root, workspace.statuses); if (!r.changed) { console.error(` pull ${e.pmId} "${e.title}": ${r.error}`); pullFailed++; continue; } pulled++; } @@ -6707,7 +6972,7 @@ export function isMutatingGithubCommand(command: string, options: Record=2026.7.20); compensate applied mutations on failure and report incomplete compensation" }, { long: "--link-deps", description: "After import, map dependency references in issue bodies (`Blocked by #N`, `Depends on owner/repo#N`, `Blocks #N`) to pm dependency edges between the linked items. Idempotent; skips self- and unresolved references; ordering cycles are reported (via the SDK ordering-cycle advisory), not rejected" }, - { long: "--gate", description: "Fail-closed privacy gate: verify plan completeness and provenance-tag idempotency before the write, then scan the proposed tracker change for credentials, personal data, and host paths after it; any finding (or unreadable scan input) fails the import so nothing is ever pushed" }, + { long: "--gate", description: "Fail-closed privacy gate: verify plan completeness and explicit-identity idempotency before the write, then scan the proposed tracker change for credentials, personal data, and host paths after it; any finding (or unreadable scan input) fails the import so nothing is ever pushed" }, { long: "--dry-run", description: "Preview without writing" }, { long: "--type", value_name: "type", description: "Override pm item type (default: Issue)" }, ]; const EXPORT_FLAGS = [ + { long: "--allow-reference-tags", description: "Deprecated compatibility flag; reference tags never establish links. Set github_url explicitly." }, { long: "--form", value_name: "path", description: "Enforce a selected repository issue form before any export writes" }, { long: "--form-ref", value_name: "revision", description: "Explicit repository revision containing the selected form" }, { long: "--form-answers", value_name: "json", description: "JSON map of form answer IDs to strings or string arrays" }, @@ -6744,6 +7010,7 @@ const EXPORT_FLAGS = [ ]; const SYNC_FLAGS = [ + { long: "--allow-reference-tags", description: "Deprecated compatibility flag; reference tags never establish links. Set github_url explicitly." }, { long: "--repo", value_name: "owner/repo", description: "Target GitHub repo (required)" }, { long: "--ids", value_name: "pm-1,pm-2", description: "Only sync these pm item IDs (comma-separated)" }, { long: "--dry-run", description: "Preview the close/reopen plan without mutating GitHub" }, @@ -6865,7 +7132,7 @@ export default defineExtension({ }, { description: "Fetch GitHub issues from a repo and create/update pm items (idempotent " + - "on re-import via the `gh:owner/repo#N` provenance tag). Skips pull " + + "on re-import via persisted GitHub identity). Skips pull " + "requests by default.", intent: "import GitHub issues as pm items", arguments: [ @@ -6964,11 +7231,11 @@ export default defineExtension({ // Network/parse failure → no remote hits; pm degrades to keyword. return []; } - // Map remote matches back to local items via provenance tags. Prefer + // Map remote matches back through shared unique ownership. Prefer // the runtime-provided documents (already the current corpus); fall // back to a fresh read if absent. const docs = resolveSearchCorpus(qctx.documents, qctx.pm_root); - const index = indexByProvenance(docs); + const index = indexByProvenance(docs, new Set(matchedNumbers.map(number => `${repo.toLowerCase()}#${number}`))); return mapSearchHits(matchedNumbers, repo, index); }, }); @@ -6978,7 +7245,7 @@ export default defineExtension({ // hooks — actionable sync reminder for github-linked items. // Safe + no network: only emits a hint, gated on PM_GITHUB_SYNC, and names // the exact command to run. Triggers only when a github-linked item (one - // carrying a `gh:owner/repo#N` provenance tag) is closed/reopened. + // carrying authoritative GitHub provenance) is closed/reopened. // ----------------------------------------------------------------------- api.hooks.afterCommand((ctx: AfterCommandHookContext) => { if (!process.env.PM_GITHUB_SYNC) return; @@ -6995,12 +7262,14 @@ export default defineExtension({ if (res.status !== 0) return; let repo: string | undefined; try { - const item = JSON.parse(res.stdout); - for (const tag of item?.tags ?? []) { - const p = parseProvenanceTag(String(tag)); - if (p) { repo = p.repo; break; } - } - } catch { + JSON.parse(res.stdout); // Compact show output is not ownership evidence. + const corpus = readPmItems(ctx.pm_root); + const item = corpus.find(item => item.id === id); + if (!item) return; + const owners = githubOwnedIdentityKeys(item, corpus); + repo = owners[0]?.split("#")[0]; + } catch (error: unknown) { + if (error instanceof GithubImportOwnershipError) console.error(`[pm-github] ${error.code}: ${error.message}`); return; } if (!repo) return; @@ -7017,7 +7286,7 @@ export default defineExtension({ name: "github sync", description: "Push pm item status changes back to GitHub: close/reopen the linked " + - "issue (matched by the `gh:owner/repo#N` provenance tag) to match the pm " + + "issue (resolved through explicit GitHub provenance) to match the pm " + "item's status. Requires a GitHub token and explicit --repo. Use --dry-run " + "to preview the plan without mutating anything.", intent: "sync pm item status to the linked GitHub issue state", @@ -7031,7 +7300,7 @@ export default defineExtension({ "Set GITHUB_TOKEN/GH_TOKEN or run `gh auth login` (sync mutates remote issues).", "Pass --repo explicitly; sync never guesses the target repo.", "Use --ids to scope sync; unknown IDs fail fast to avoid silent misses.", - "Items must carry a `gh:owner/repo#N` tag — import with `pm github import` first.", + "Link items with github_url; gh: tags remain references.", "Use --dry-run to preview the close/reopen plan before pushing.", ], async run(ctx: CommandHandlerContext) { @@ -7170,7 +7439,7 @@ export default defineExtension({ name: "gh-issues import", description: "Fetch GitHub issues from a repo and create/update pm items (idempotent " + - "on re-import via the `gh:owner/repo#N` provenance tag). Skips pull " + + "on re-import via persisted GitHub identity). Skips pull " + "requests by default. Uses GITHUB_TOKEN/GH_TOKEN (or the authenticated " + "gh CLI) when available for 5000 req/hr and private repos; falls back to " + "the unauthenticated API (60 req/hr). Equivalent to `pm github import`.", diff --git a/manifest.json b/manifest.json index 5d17f434..de4c5f2b 100644 --- a/manifest.json +++ b/manifest.json @@ -4,7 +4,7 @@ "description": "GitHub Issues + Projects v2 integration. Imports issues as pm items (`pm github import`), exports pm items as GitHub issues, syncs issue state, and bidirectionally syncs pm items with a GitHub Projects v2 board (`pm github project import/sync/list/fields`) — mapping pm status to the board Status column with idempotent, no-data-loss provenance.", "author": "@unbraind", "entry": "./dist/index.js", - "pm_min_version": "2026.10.17", + "pm_min_version": "2026.10.23", "priority": 50, "capabilities": [ "commands", diff --git a/package-lock.json b/package-lock.json index b2ddf7c1..95fceeb4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,7 +30,7 @@ "node": ">=22.18.0" }, "peerDependencies": { - "@unbrained/pm-cli": ">=2026.10.17" + "@unbrained/pm-cli": ">=2026.10.23" } }, "node_modules/@babel/code-frame": { diff --git a/package.json b/package.json index c7c0c3e0..f2b516c8 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "test:minimum-sdk": "npm run build && node --test --test-concurrency=1 test/minimum-sdk-consumer.test.ts" }, "peerDependencies": { - "@unbrained/pm-cli": ">=2026.10.17" + "@unbrained/pm-cli": ">=2026.10.23" }, "devDependencies": { "@babel/eslint-parser": "8.0.7", diff --git a/projects.ts b/projects.ts index d5e779c7..1bb4e072 100644 --- a/projects.ts +++ b/projects.ts @@ -1,3 +1,5 @@ +import { type GithubOwnershipIndex, buildGithubOwnershipIndex, warnGithubOwnershipScope, githubOwnedIdentityKeys, githubOwnershipClaims, assertGithubOwnershipConsistency, ownershipStateOf } from "./import-ownership.ts"; +import { parseGithubUrl } from "./github-url.ts"; import { canonicalGithubStatus, githubStatusKind, type GithubStatusRegistry } from "./workspace.ts"; import { ownValue } from "./own-record.ts"; // pm-github — GitHub Projects v2 support (pure logic layer). @@ -90,8 +92,9 @@ export interface ProjectItem { } // A minimal pm item shape (mirrors the one in index.ts; duplicated to keep this -// module dependency-free). +// module independent of command handlers). export interface PmItemLike { + [key: string]: unknown; id?: string; title?: string; status?: string; @@ -114,10 +117,9 @@ export function parseProjectRef(input: string | undefined): ProjectRef | undefin const raw = input.trim(); if (!raw) return undefined; - const urlMatch = - /^https?:\/\/github\.com\/(?:orgs|users)\/([^/\s]+)\/projects\/(\d+)/i.exec(raw); - if (urlMatch) { - return { owner: urlMatch[1], number: Number(urlMatch[2]) }; + const url = parseGithubUrl(raw); + if (url && !url.api && ["orgs", "users"].includes(url.owner.toLowerCase()) && url.kind === "projects" && /^[1-9]\d*$/.test(url.value ?? "") && Number.isSafeInteger(Number(url.value))) { + return { owner: url.name, number: Number(url.value) }; } const shortMatch = /^([^/#\s]+)[/#](\d+)$/.exec(raw); @@ -360,7 +362,7 @@ export function indexPmByProjectItem( } /** - * Index pm items by their `owner/repo#number` issue-provenance tag. + * Index pm items by consistent unique GitHub issue ownership. * * Lets a push attach an already-imported pm item to the existing issue's project * item (via {@link indexProjectItemsByIssue}) rather than creating a duplicate @@ -369,15 +371,11 @@ export function indexPmByProjectItem( * @param items - The pm items to index (nullish entries are ignored). * @returns A map keyed `owner/repo#number` (lowercased owner) to the pm item. */ -export function indexPmByIssue(items: Array): Map { +export function indexPmByIssue(items: Array, snapshot?: GithubOwnershipIndex): Map { const index = new Map(); - for (const item of items) { - if (!item?.id) continue; - for (const tag of item.tags ?? []) { - const m = /^gh:([^#\s]+)#(\d+)$/.exec(tag.trim()); - if (m) index.set(`${m[1].toLowerCase()}#${Number(m[2])}`, item); - } - } + const peers = items.filter((item): item is PmItemLike => Boolean(item)); + const ownership = snapshot ?? buildGithubOwnershipIndex(peers); + for (const item of peers) if (item.id) for (const key of githubOwnedIdentityKeys(item, ownership)) index.set(key, item); return index; } @@ -459,6 +457,8 @@ export interface PushPlanOptions { addMissing?: boolean; statusMap?: Map; statuses?: GithubStatusRegistry; + /** Complete peer corpus when the command scopes selected items. */ + corpus?: PmItemLike[]; } // Build the pm → project plan. Pure and side-effect free so it is unit-testable @@ -472,6 +472,8 @@ export function buildProjectPushPlan( ): PushPlan { const addMissing = opts.addMissing !== false; const options = statusField?.options ?? []; + const ownership = buildGithubOwnershipIndex((opts.corpus ?? pmItems).filter((item): item is PmItemLike => Boolean(item)), projectItems.flatMap(item => item.content.repo ? [item.content.repo] : [])); + warnGithubOwnershipScope(ownership, pmItems.filter((item): item is PmItemLike => Boolean(item))); const byProjectItem = indexProjectItemMap(projectItems); const byIssue = indexProjectItemsByIssue(projectItems); @@ -495,16 +497,12 @@ export function buildProjectPushPlan( break; } } - let issueKey: { repo: string; number: number } | undefined; - if (!linked) { - for (const tag of item.tags ?? []) { - const m = /^gh:([^#\s]+)#(\d+)$/.exec(tag.trim()); - if (m) { - issueKey = { repo: m[1].toLowerCase(), number: Number(m[2]) }; - linked = byIssue.get(`${issueKey.repo}#${issueKey.number}`); - break; - } - } + const keys = githubOwnedIdentityKeys(item, ownership); + const [repository, number] = keys[0]?.split("#") ?? []; + const issueKey = repository && number ? { repo: repository, number: Number(number) } : undefined; + if (!linked && issueKey) linked = byIssue.get(`${issueKey.repo}#${issueKey.number}`); + if (linked?.content.repo && linked.content.number && ownershipStateOf(ownership, item).evidence) { + assertGithubOwnershipConsistency(item, { id: item.id, repo: linked.content.repo, number: linked.content.number }, ownership); } // Resolve the target Status option for this pm status. @@ -606,6 +604,30 @@ export interface PullPlanEntry { toStatus: string; } +/** + * Index the selected items by the issue each one owns, checking their claims against the complete owner + * corpus for the board's repositories. Only selected items are looked up, so a conflict between two + * unselected items that the plan never touches cannot abort it, while a selected item that conflicts with + * an unselected peer still refuses. + */ +function indexSelectedByIssue(selected: PmItemLike[], corpus: PmItemLike[], projectItems: ProjectItem[], ref: ProjectRef): { ownership: GithubOwnershipIndex; pmByIssue: Map; pmByItemId: Map } { + const ownership = buildGithubOwnershipIndex(corpus.filter((item): item is PmItemLike => Boolean(item)), projectItems.flatMap(item => item.content.repo ? [item.content.repo] : [])); + const issues = new Set(projectItems.flatMap(item => item.content.repo && item.content.number ? [`${item.content.repo.toLowerCase()}#${item.content.number}`] : [])); + const nodes = new Set(projectItems.map(item => item.id)); + const candidates = selected.filter(item => ownershipStateOf(ownership, item).claims.some(key => issues.has(key)) || (item.tags ?? []).some(tag => { + const parsed = parseProjectItemTag(tag); + return parsed && parsed.owner === ref.owner.toLowerCase() && parsed.number === ref.number && nodes.has(parsed.itemId); + })); + warnGithubOwnershipScope(ownership, candidates); + return { ownership, pmByIssue: indexPmByIssue(candidates, ownership), pmByItemId: indexPmByProjectItem(candidates, ref) }; +} + +const projectPullOwners = new WeakMap(); + +/** Preserve the planning snapshot's owner decision without adding content to public plan rows. */ +export function getProjectPullOwners(entry: object): readonly string[] | undefined { return projectPullOwners.get(entry); } + +/** Board-to-tracker status changes and board options that require an explicit mapping. */ export interface PullPlan { entries: PullPlanEntry[]; /** project items whose Status option name maps to no known pm status. */ @@ -621,9 +643,9 @@ export function buildProjectPullPlan( projectItems: ProjectItem[], statusMap?: Map, statuses?: GithubStatusRegistry, + corpus: PmItemLike[] = pmItems, ): PullPlan { - const pmByItemId = indexPmByProjectItem(pmItems, ref); - const pmByIssue = indexPmByIssue(pmItems); + const { ownership, pmByIssue, pmByItemId } = indexSelectedByIssue(pmItems, corpus, projectItems, ref); const entries: PullPlanEntry[] = []; const statusSkipped: PullPlan["statusSkipped"] = []; @@ -632,7 +654,10 @@ export function buildProjectPullPlan( if (!pm && pi.content.repo && typeof pi.content.number === "number") { pm = pmByIssue.get(`${pi.content.repo.toLowerCase()}#${pi.content.number}`); } - if (!pm || !pm.id) continue; // unlinked project items are handled by import + if (!pm || !pm.id || !pmItems.some(selected => selected.id === pm!.id)) continue; // unlinked project items are handled by import + if (pi.content.repo && pi.content.number && ownershipStateOf(ownership, pm).evidence) { + assertGithubOwnershipConsistency(pm, { id: pm.id, repo: pi.content.repo, number: pi.content.number }, ownership); + } const toStatus = mapOptionNameToPmStatus(pi.statusName, statusMap, statuses); if (!toStatus) { @@ -641,7 +666,9 @@ export function buildProjectPullPlan( } const fromStatus = canonicalGithubStatus(pm.status ?? "open", statuses); if (fromStatus === toStatus) continue; - entries.push({ itemId: pi.id, pmId: pm.id, title: pm.title ?? "(untitled)", fromStatus, toStatus }); + const entry = { itemId: pi.id, pmId: pm.id, title: pm.title ?? "(untitled)", fromStatus, toStatus }; + projectPullOwners.set(entry, githubOwnedIdentityKeys(pm, ownership)); + entries.push(entry); } return { entries, statusSkipped }; @@ -683,8 +710,7 @@ export function buildProjectImportPlan( statusMap?: Map, statuses?: GithubStatusRegistry, ): ImportPlanEntry[] { - const pmByItemId = indexPmByProjectItem(pmItems, ref); - const pmByIssue = indexPmByIssue(pmItems); + const { ownership, pmByIssue, pmByItemId } = indexSelectedByIssue(pmItems, pmItems, projectItems.filter(item => item.content.typename !== "Unknown"), ref); const plan: ImportPlanEntry[] = []; for (const pi of projectItems) { @@ -711,6 +737,10 @@ export function buildProjectImportPlan( existing = pmByIssue.get(`${c.repo.toLowerCase()}#${c.number}`); } + if (existing?.id && c.repo && c.number && ownershipStateOf(ownership, existing).evidence) { + assertGithubOwnershipConsistency(existing, { id: existing.id, repo: c.repo, number: c.number }, ownership); + } + // Provenance tags this import adds, MERGED with any tags the existing pm item // already carries (labels from a prior `pm github import`, user-added tags). // Preserve native atoms in the preview. The writer adds only missing diff --git a/refresh.ts b/refresh.ts index 13d199ab..4f035f7b 100644 --- a/refresh.ts +++ b/refresh.ts @@ -1,3 +1,5 @@ +import { buildGithubOwnershipIndex, warnGithubOwnershipScope, githubOwnedIdentityKeys, GithubImportOwnershipError, ownershipStateOf } from "./import-ownership.ts"; +import { parseGithubIssueUrl } from "./github-url.ts"; import { readGithubWorkspace, type GithubStatusRegistry } from "./workspace.ts"; import { ownValue } from "./own-record.ts"; import { importTagValues } from "./import-tags.ts"; @@ -8,11 +10,11 @@ import { join, basename } from "node:path"; import { listAllComplete, certifyCompleteListResult, previewItemMutations, resolvePmRoot } from "@unbrained/pm-cli/sdk"; import { CommandError, EXIT_CODE, decodeCompletePmItems, githubGraphQLResponse, importGithubAtomic, - listUnfinishedGithubImports, parseImportOptions, parseProvenanceTag, prepareGithubImport, readGithubImportPlan, - resolveGitHubToken, resumeGithubImport, validateGithubImportFidelity, validateGithubImportPlan, + listUnfinishedGithubImports, listRetainedGithubImportPlans, parseImportOptions, prepareGithubImport, readGithubImportPlan, + resolveGitHubToken, resumeGithubImport, assertGithubImportReplayPreflight, validateGithubImportFidelity, validateGithubImportPlan, type GhIssue, type PmItem, type PreparedGithubImport, } from "./index.ts"; -import { fetchImportTimeline, importFidelityFields, normalizeImportValue, readImportReleases } from "./import-fidelity.ts"; +import { fetchImportTimeline, type LegacyGithubIdentityConflict, importFidelityFields, normalizeImportValue, readImportReleases } from "./import-fidelity.ts"; /** An existing item and its unique linked issue number. */ export interface RefreshTarget { id: string; number: number; item: PmItem } @@ -27,26 +29,29 @@ export interface RefreshEntry { } /** Select only explicit provenance, refusing conflicting links and duplicate local owners. */ -export function selectRefreshTargets(items: readonly PmItem[], repo: string): RefreshTarget[] { +export function selectRefreshTargets(items: readonly PmItem[], repo: string, numbers?: readonly number[]): RefreshTarget[] { const targets = new Map(); - for (const item of items) { + const ownership = buildGithubOwnershipIndex(items, [repo]); + const requested = numbers && new Set(numbers.map(number => `${repo.toLowerCase()}#${number}`)); + const selected = items.filter(item => !requested || ownershipStateOf(ownership, item).claims.some(key => requested.has(key))); + warnGithubOwnershipScope(ownership, selected); + for (const item of selected) { if (!item.id) continue; + const state = ownershipStateOf(ownership, item); + // PRs are excluded only after checking their complete ownership claims. + if (state.kinds.includes("pull")) { githubOwnedIdentityKeys(item, ownership, repo); continue; } const numbers = new Set(); - for (const tag of item.tags ?? []) { - const parsed = parseProvenanceTag(tag); - if (parsed?.repo === repo) numbers.add(parsed.number); - } - if (typeof item.github_url === "string") { - // Only /issues/ locators are refreshable: the batch query reads issue(...) - // nodes, so accepting /pull/ links would report every PR-linked item as - // missing on each run. - const url = /^https:\/\/github\.com\/([^/]+\/[^/]+)\/issues\/(\d+)\/?$/i.exec(item.github_url); - if (url?.[1]!.toLowerCase() === repo) numbers.add(Number(url[2])); + for (const key of state.claims) { + const [repository, number] = key.split("#"); + if (repository === repo) numbers.add(Number(number)); } + // Invalid explicit numbers remain a refusal even when they cannot form a + // valid ownership key. Neither this validation nor tags create ownership. if (typeof item.github_repo === "string" && item.github_repo.toLowerCase() === repo && typeof item.github_number === "number") numbers.add(item.github_number); - if (numbers.size > 1) throw new CommandError(`Refresh found conflicting GitHub identities for ${item.id}.`); + for (const number of numbers) if (!Number.isSafeInteger(number) || number < 1) throw new CommandError(`Refresh found an invalid issue number for ${item.id}.`); + if (numbers.size > 1) throw new GithubImportOwnershipError(item.id, `Refresh found conflicting GitHub identities in ${repo}`, state.claims); + githubOwnedIdentityKeys(item, ownership, repo); for (const number of numbers) { - if (!Number.isSafeInteger(number) || number < 1) throw new CommandError(`Refresh found an invalid issue number for ${item.id}.`); if (targets.has(number)) throw new CommandError(`Refresh found multiple existing owners for issue #${number}.`); targets.set(number, { id: item.id, number, item }); } @@ -80,7 +85,7 @@ async function fetchRefreshBatch(repo: string, targets: readonly RefreshTarget[] if (error && error.type !== "NOT_FOUND") reason = error.type === "FORBIDDEN" ? "inaccessible" : "api_error"; else if (response.data!.repository === null || node === null) reason = "not_found_or_inaccessible"; else if (node === undefined) reason = "incomplete_response"; - else if (node.url.toLowerCase() !== `https://github.com/${repo}/issues/${target.number}`) reason = "transferred"; + else if (parseGithubIssueUrl(node.url)?.key !== `${repo}#${target.number}` || parseGithubIssueUrl(node.url)?.kind !== "issues") reason = "transferred"; else if (node.number !== target.number || !node.title.trim() || !["OPEN", "CLOSED"].includes(node.state)) reason = "invalid_issue"; if (reason) { results.push({ target, reason }); continue; } const issue = node!; @@ -113,8 +118,8 @@ async function fetchRefreshBatch(repo: string, targets: readonly RefreshTarget[] /** Compare only refresh-owned fields; use the same source lifecycle projection as import finalization. */ export function refreshChanges(entry: PreparedGithubImport, statuses?: GithubStatusRegistry): RefreshChange[] { - const desired = { ...importFidelityFields(entry.fidelity!, Boolean(entry.linkOnly)), - ...(entry.linkOnly ? {} : { title: entry.title, status: entry.status }), tags: importTagValues(entry) }; + const desired = { ...importFidelityFields(entry.fidelity!, Boolean(entry.linkOnly), entry.preserveContent ? entry.match : undefined), + ...(entry.linkOnly ? {} : { ...(!entry.preserveContent ? { title: entry.title } : {}), status: entry.status }), tags: importTagValues(entry) }; return Object.entries(desired).flatMap(([field, value]) => { const old = ownValue(entry.match, field) ?? null; const next = value ?? null; @@ -160,23 +165,62 @@ export async function runRefresh(repoArg: string | undefined, pmRoot: string, op const state = await readRefreshState(pmRoot); const workspace = state.workspace; let items = state.items; - let all = selectRefreshTargets(items, repo); - let targets = bounded(all); + const identityConflictFields: { identity_conflicts?: LegacyGithubIdentityConflict[] } = {}; + let all = selectRefreshTargets(items, repo, scope?.numbers); + const initialTargets = bounded(all); const token = resolveGitHubToken(); // Resume interrupted transactions for the selected targets BEFORE the // unchanged filter: a crash after finalization leaves linked items already // matching the source, so the pending journal would otherwise hide behind an // ordinary no-op report and never commit. const pending = (await listUnfinishedGithubImports(pmRoot)) - .filter(journal => !journal.itemIds.length || journal.itemIds.some(id => targets.some(target => target.id === id))); + .filter(journal => !journal.itemIds.length || journal.itemIds.some(id => initialTargets.some(target => target.id === id))); + // Retained side-data plans can outlive a committed SDK journal. Recover those + // before polling, so a newly fetched plan is never reported as the old replay. + const recovery = new Map(pending.map(journal => [journal.transactionId, { transactionId: journal.transactionId, status: String(journal.status) }])); + const deferredImports: Array<{ transaction_id: string; code: string; item_ids: string[]; issue_numbers: number[]; message: string; resume_argv: string[] }> = []; + const currentItems = new Map(items.map(item => [item.id, item])); + const selectedIds = new Set(initialTargets.map(target => target.id)); + const selectableKeys = new Set(all.map(target => `${target.number}:${target.id}`)); + const requestedNumbers = scope && new Set(scope.numbers); + const deferredIds = new Set(); + for (const record of await listRetainedGithubImportPlans(pmRoot)) { + const pairs = record.itemIds ?? record.entries.flatMap(entry => entry.match?.id ? [[entry.issueNumber, entry.match.id] as [number, string]] : []); + const ids = pairs.map(([, id]) => id); + if (record.repo.toLowerCase() === repo) { + // A recorded source number scopes recovery diagnostics; it never grants ownership. + const selected = pairs.filter(([number, id]) => (!requestedNumbers || requestedNumbers.has(number)) && + (opts.limit === undefined || selectedIds.has(id))); + const creates = new Set(record.mutations.filter(mutation => mutation.op === "create").map(mutation => mutation.id)); + const matchedIds = new Set(record.entries.flatMap(entry => entry.match?.id ? [entry.match.id] : [])); + const pendingCreates = new Set(ids.filter(id => !currentItems.has(id) && creates.has(id) && !matchedIds.has(id))); + const selectedRecordIds = new Set(selected.map(([, id]) => id)); + const orphaned = selected.filter(([number, id]) => !pendingCreates.has(id) && !selectableKeys.has(`${number}:${id}`)).map(([, id]) => id); + if (orphaned.length) throw new GithubImportOwnershipError(orphaned.join(", "), `${repo} (retained transaction ${record.transactionId})`, ["recorded target is no longer selectable; restore its intended explicit ownership before retrying recovery"]); + // A refresh cannot create items or partially replay an atomic import outside its scope. + if (pendingCreates.size || selected.length !== pairs.length) { + recovery.delete(record.transactionId); + for (const id of ids) deferredIds.add(id); + deferredImports.push({ transaction_id: record.transactionId, + code: pendingCreates.size ? "github_import_pending_creates" : "github_import_outside_refresh_scope", + item_ids: pendingCreates.size ? [...pendingCreates] : ids.filter(id => !selectedRecordIds.has(id)), + issue_numbers: pairs.map(([number]) => number), + message: "Retained atomic import is deferred; resume the original import before refreshing those targets.", + resume_argv: ["pm", "github", "import", repo, "--atomic"] }); + continue; + } + } + if (ids.some(id => selectedIds.has(id))) recovery.set(record.transactionId, { transactionId: record.transactionId, status: "retained" }); + } + let targets = initialTargets.filter(target => !deferredIds.has(target.id)); let resumed = 0; - if (pending.length) { + if (recovery.size) { if (opts.dryRun) { - throw new CommandError(`Refresh cannot preview while GitHub transaction ${pending.map(journal => journal.transactionId).join(", ")} is unfinished for selected linked items. Re-run without --dry-run to complete it.`); + throw new CommandError(`Refresh cannot preview while GitHub transaction ${[...recovery.keys()].join(", ")} is unfinished for selected linked items. Re-run without --dry-run to complete it.`); } // Validate the complete recovery set before the first transaction can write. const records = []; - for (const journal of pending) { + for (const journal of recovery.values()) { const record = await readGithubImportPlan(pmRoot, journal.transactionId); if (!record || record.transactionId !== journal.transactionId) { throw new CommandError( @@ -185,6 +229,7 @@ export async function runRefresh(repoArg: string | undefined, pmRoot: string, op ); } validateGithubImportPlan(record, items); + assertGithubImportReplayPreflight(pmRoot, record); if (record.repo.toLowerCase() !== repo) { throw new CommandError(`Refresh cannot replay GitHub transaction ${journal.transactionId} for ${record.repo} while refreshing ${repo}. Re-run refresh for ${record.repo} first; nothing was refreshed.`); } @@ -202,8 +247,8 @@ export async function runRefresh(repoArg: string | undefined, pmRoot: string, op // The replay finalized the interrupted targets; re-read so this run // compares the committed state instead of the pre-replay snapshot. items = await readRefreshItems(pmRoot); - all = selectRefreshTargets(items, repo); - targets = bounded(all); + all = selectRefreshTargets(items, repo, scope?.numbers); + targets = bounded(all).filter(target => !deferredIds.has(target.id)); } const plan: RefreshEntry[] = []; const prepared: PreparedGithubImport[] = []; @@ -233,6 +278,6 @@ export async function runRefresh(repoArg: string | undefined, pmRoot: string, op if (opts.atomic) await importGithubAtomic(pmRoot, repo, prepared, { updateOnly: true }); else for (const entry of prepared) await importGithubAtomic(pmRoot, repo, [entry], { updateOnly: true }); } - return { repo, dryRun: opts.dryRun, atomic: opts.atomic, selected: targets.length, filtered, + return { ...(deferredImports.length ? { deferred_imports: deferredImports } : {}), ...identityConflictFields, repo, dryRun: opts.dryRun, atomic: opts.atomic, selected: targets.length, filtered, unchanged, updated: prepared.length, missing, created: 0, resumed, plan }; } diff --git a/scripts/generate-changelog.ts b/scripts/generate-changelog.ts index 4b7262e9..b7d44f18 100644 --- a/scripts/generate-changelog.ts +++ b/scripts/generate-changelog.ts @@ -14,6 +14,7 @@ export type ChangelogRunner = (command: string, args: readonly string[], options * * Only the explicit `changelog-unreleased` tag admits a nonterminal item. Other * ongoing work cannot acquire a release entry just because it was claimed. + * Dependency notes remain visible when a release window also contains PM work. * * @param root - Repository containing the tracker and release tags. * @param args - Optional pm-changelog arguments, including its check flag. @@ -27,7 +28,7 @@ export async function generateChangelog(root: string, args: readonly string[], r const items = corpus.items.filter(item => workspace.statuses.terminal_done_statuses.has(canonicalGithubStatus(item.status, workspace.statuses)) || item.tags?.includes("changelog-unreleased")); const cli = resolve(dirname(createRequire(import.meta.url).resolve("pm-changelog/package.json")), "dist/cli.js"); const result = runner("node", [cli, "--stdin", "--pm-root", ".agents/pm", "--status", [...new Set(["closed", ...items.map(item => item.status)])].join(","), - "--mode", "replace", "--output", "CHANGELOG.md", "--all-release-tags", "--release-version-from-package", + "--mode", "replace", "--output", "CHANGELOG.md", "--all-release-tags", "--release-version-from-package", "--dependency-updates", "--date-from-version", "--item-url-base", "https://github.com/unbraind/pm-github/blob/main/.agents/pm", "--respect-item-release", ...args], { cwd: root, input: JSON.stringify({ items }), encoding: "utf8", maxBuffer: 64 * 1024 * 1024, env: { ...process.env }, diff --git a/scripts/identity-inventory.ts b/scripts/identity-inventory.ts new file mode 100644 index 00000000..2b38a3fa --- /dev/null +++ b/scripts/identity-inventory.ts @@ -0,0 +1,103 @@ +/** Re-derive the ownership ledger's anchors from TypeScript syntax, preserving reviewed path explanations. */ +import { readFileSync, readdirSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; +import { createRequire } from "node:module"; +import { isMainInvocation } from "./main-invocation.ts"; + +interface Site { line: number; label: string } +interface Group { file: string; name: string; sites: Site[] } +const document = "docs/identity-ownership-inventory.md"; +const fields = new Set(["github_url", "github_repo", "github_number", "external_ref"]); +const additional = new Map([ + ["import-fidelity.ts/parseLegacyGithubDescription", "Shared native declaration parser: ownership and diagnostics use the same URL boundaries and matching positive safe issue numbers."], + ["import-fidelity.ts/legacyGithubDescriptionIdentity", "Legacy native declaration identity: anchored at description start, first complete issue/PR URL, with trailing punctuation removed. No link-only marker or whole-description match is required."], + ["import-fidelity.ts/githubLinkReferences", "Reference extraction only: supported link fields expose prose and API URL mentions, never owner claims or an import target."], + ["projects.ts/indexSelectedByIssue", "Build one complete ownership index, then validate only selected tracker items matched by current board issue keys or project-item IDs. Off-board conflicts warn; a peer claiming a board candidate still refuses."], + ["import-ownership.ts/ownershipStateOf", "Identity-safe state read: the states map is keyed by object identity, so a separately allocated copy of a corpus record derives its claims instead of reading as unowned; every planner and selector uses it."], + ["import-ownership.ts/warnGithubOwnershipScope", "Diagnostics only: malformed native numbers and conflicts wholly outside selected targets emit warnings naming affected IDs; no warning chooses an owner or authorizes writes."], +]); + +/** Minimal syntax shape keeps this tooling independent of the compiler's optional JavaScript API. */ +interface SyntaxNode extends Record { type: string; loc?: { start: { line: number } } } +const syntax = createRequire(import.meta.url)("@babel/parser") as { parse: (source: string, options: { sourceType: "module"; plugins: ["typescript"] }) => SyntaxNode }; + +/** Narrow parser children without treating arbitrary metadata or comments as syntax nodes. */ +function syntaxNode(value: unknown): SyntaxNode | undefined { + return value !== null && typeof value === "object" && "type" in value && typeof value.type === "string" ? value as SyntaxNode : undefined; +} + +/** Collect every inventoried call, raw identity-field read and reference regex with its actual enclosing function. */ +function collect(root: string, helpers: ReadonlySet): Group[] { + const groups = new Map(); + for (const file of readdirSync(root).filter(file => file.endsWith(".ts")).sort()) { + const source = syntax.parse(readFileSync(join(root, file), "utf8"), { sourceType: "module", plugins: ["typescript"] }); + /** Walk syntax rather than searching comments or stale line offsets. */ + function visit(node: SyntaxNode, enclosing = "", parent?: SyntaxNode): void { + const id = syntaxNode(node.id), binding = syntaxNode(parent?.id); + if (node.type === "FunctionDeclaration" && typeof id?.name === "string") enclosing = id.name; + if (node.type === "ArrowFunctionExpression" && parent?.type === "VariableDeclarator" && typeof binding?.name === "string") enclosing = binding.name; + const callee = syntaxNode(node.callee), property = syntaxNode(node.property); + let label: string | undefined; + if (["CallExpression", "OptionalCallExpression"].includes(node.type) && callee?.type === "Identifier" && typeof callee.name === "string" && helpers.has(callee.name)) label = `${callee.name}()`; + if (node.type === "RegExpLiteral" && typeof node.pattern === "string" && node.pattern.includes("gh:")) label = "reference tag regex"; + if (["MemberExpression", "OptionalMemberExpression"].includes(node.type) && !node.computed && typeof property?.name === "string" && fields.has(property.name)) label = property.name; + if (label && node.loc) { + const key = `${file}/${enclosing}`, group = groups.get(key) ?? { file, name: enclosing, sites: [] }; + group.sites.push({ line: node.loc.start.line, label }); + groups.set(key, group); + } + for (const [key, value] of Object.entries(node)) { + if (["loc", "comments", "leadingComments", "trailingComments"].includes(key)) continue; + for (const child of Array.isArray(value) ? value : [value]) { + const parsed = syntaxNode(child); if (parsed) visit(parsed, enclosing, node); + } + } + } + visit(source); + } + return [...groups.values()].sort((a, b) => a.file.localeCompare(b.file) || a.sites[0]!.line - b.sites[0]!.line); +} + +/** Render every root anchor and non-root audit anchor from the current files; ledger prose is not used as a source location. */ +export function renderIdentityInventory(root: string, ledger = readFileSync(join(root, document), "utf8")): string { + const explanations = new Map(), helpers = new Set(["ownershipStateOf", "githubLinkReferences", "indexSelectedByIssue", "legacyGithubDescriptionIdentity", "parseLegacyGithubDescription", "warnGithubOwnershipScope"]); + for (const row of ledger.matchAll(/^\| `([^`]+)` \/ `([^`]+)` \| (.*?) \| (.*?) \|$/gm)) { + explanations.set(`${row[1]}/${row[2]}`, row[4]!); + for (const call of row[3]!.matchAll(/\b([A-Za-z_$][\w$]*)\(\)/g)) helpers.add(call[1]!); + } + for (const [key, value] of additional) explanations.set(key, value); + const groups = collect(root, helpers), total = groups.reduce((sum, group) => sum + group.sites.length, 0); + const start = ledger.indexOf("| File / enclosing function |"), end = ledger.indexOf("\n## Scripts and workflow audit"); + if (start < 0 || end < start || groups.length < 80) throw new Error("Ownership inventory template or source corpus is incomplete."); + let intro = ledger.slice(0, start).replace(/Initial inventory:[^\n]+/, "Initial inventory: reviewed head `d9d9bc7`. Anchors are regenerated from the merged source with `node scripts/identity-inventory.ts --write` and checked by the same command with `--check`.") + .replace(/\d+ function groups; \d+ helper-call\/field\/parser sites\./, `${groups.length} function groups; ${total} helper-call/field/parser sites.`); + intro = intro.replace(/(?:Only the raw-collector implementation|Owner claims come from)[^\n]+/, "Owner claims come from whole-value explicit identity fields, external_ref, or a proven importer ID. A leading native GitHub issue or GH issue/PR declaration retains legacy ownership without a link-only marker or whole-description match; direct provenance still protects against copied provider markers. Generic link fields and their prose produce references only. Operational selectors consume the shared ownership index; provider backlinks and explicit opt-in citation adoption keep their separate first-link authorization."); + const missing = groups.filter(group => !explanations.has(`${group.file}/${group.name}`)); + if (missing.length) throw new Error(`Classify ownership groups: ${missing.map(group => `${group.file}/${group.name}`).join(", ")}`); + const rows = groups.map(group => { + const reason = explanations.get(`${group.file}/${group.name}`); + if (!reason) throw new Error(`Classify new ownership group ${group.file}/${group.name} before publishing its inventory.`); + return `| \`${group.file}\` / \`${group.name}\` | ${group.sites.map(site => `\`${group.file}:${site.line}\` ${site.label}`).join("; ")} | ${reason} |`; + }); + const auxiliary: string[] = []; + for (const folder of ["scripts", ".github/workflows"]) for (const file of readdirSync(join(root, folder)).sort()) { + if (!/\.(?:ts|ya?ml)$/.test(file) || file === "identity-inventory.ts") continue; + readFileSync(join(root, folder, file), "utf8").split("\n").forEach((line, index) => { + if (!/gh:|github_url|github_repo|github_number|external_ref/.test(line)) return; + const treatment = file === "dependabot-merge.ts" ? "Explicit GitHub CLI transport parameter; targets come from repository/PR arguments." + : file === "prove-gate-public-forms.ts" ? "Privacy-gate documentation reference; no target lookup." + : file === "pm-github-sync.yml" ? "Importer option documentation or typed diagnostic rendering; no independent target lookup." + : undefined; + if (!treatment) throw new Error(`Classify new auxiliary identity use in ${folder}/${file}.`); + auxiliary.push(`| \`${folder}/${file}:${index + 1}\` | ${treatment} |`); + }); + } + return intro + ["| File / enclosing function | Call sites and field accesses | Path / ownership treatment |", "| --- | --- | --- |", ...rows, "", "## Scripts and workflow audit", "", `The identity-field/tag scan finds ${auxiliary.length} auxiliary locations. The inventory generator itself names these fields to document them; it selects no GitHub target. All anchors below are derived from the current file contents.`, "", "| Location | Treatment |", "| --- | --- |", ...auxiliary, ""].join("\n"); +} + +if (isMainInvocation(process.argv, import.meta.url)) { + const root = process.cwd(), current = readFileSync(join(root, document), "utf8"), generated = renderIdentityInventory(root, current); + if (process.argv.includes("--write")) writeFileSync(join(root, document), generated); + else if (current !== generated) throw new Error("Ownership inventory is stale; run node scripts/identity-inventory.ts --write."); + console.log("Ownership inventory: all syntax-derived and auxiliary anchors verified."); +} diff --git a/test/atomic.test.ts b/test/atomic.test.ts index 2d1902c8..21148471 100644 --- a/test/atomic.test.ts +++ b/test/atomic.test.ts @@ -5,6 +5,8 @@ import os from "node:os"; import path from "node:path"; import test from "node:test"; import * as sdk from "@unbrained/pm-cli/sdk"; +import { withImportPreview } from "./helpers/import-preview.ts"; +import { withMockGithub, jsonResponse } from "./helpers/mock-github-server.ts"; import { CommandError, @@ -13,6 +15,8 @@ import { deriveAtomicItemId, deriveAtomicTransactionId, importGithubAtomic, + listUnfinishedGithubImports, + readGithubImportPlan, parseImportOptions, resolveCommitItemMutations, runImport, @@ -92,7 +96,7 @@ test("parseImportOptions enables atomic mode without changing the default", () = assert.strictEqual(parseImportOptions({ atomic: "1" }).atomic, true); }); -test("atomic dry-run previews creates and updates without invoking the SDK", async () => { +test("atomic dry-run reads real ownership without committing tracker writes", async () => withImportPreview(async root => { const issue = (number: number, title: string): GhIssue => ({ number, title, @@ -106,25 +110,13 @@ test("atomic dry-run previews creates and updates without invoking the SDK", asy updated_at: "2026-07-21T00:00:00Z", html_url: `https://github.com/acme/widgets/issues/${number}`, }); - let sdkCalls = 0; const messages: string[] = []; const originalError = console.error; console.error = (...values: unknown[]) => messages.push(values.join(" ")); try { - const result = await runImport( - "acme/widgets", - "/unused-dry-run-workspace", - parseImportOptions({ atomic: true, dryRun: true }), - { - resolveToken: () => undefined, - fetchIssues: async () => [issue(1, "New"), issue(2, "Existing"), issue(3, " ")], - readItems: () => [{ id: "existing-id", tags: ["gh:acme/widgets#2"] }], - commitAtomic: async () => { - sdkCalls++; - throw new Error("atomic dry-run must not call the SDK commit path"); - }, - }, - ); + const result = await withMockGithub((request, response) => { + jsonResponse(response, 200, request.url?.includes("/timeline") ? [] : [issue(1, "New"), issue(2, "Existing"), issue(3, " ")]); + }, () => runImport("acme/widgets", root, parseImportOptions({ atomic: true, dryRun: true }), { resolveToken: () => undefined })); assert.deepStrictEqual(result, { dryRun: true, @@ -134,12 +126,11 @@ test("atomic dry-run previews creates and updates without invoking the SDK", asy wouldSkip: 1, atomic: true, }); - assert.strictEqual(sdkCalls, 0); assert.ok(messages.some((message) => /Atomic plan would import 1, update 1, skip 1/.test(message))); } finally { console.error = originalError; } -}); +})); test("transaction identity is content-sensitive and independent of fetch order", () => { const first = entry(1, "First"); @@ -403,29 +394,39 @@ test("a failed mixed batch restores pre-existing updates and closes", async () = }); test("incomplete compensation is reported as potentially partial and resumable", async () => { - const incomplete = new AggregateError( - [new Error("apply failed"), new Error("restore failed")], - "Workspace transaction failed and compensation failed: restore failed", - ); - await assert.rejects( - () => importGithubAtomic( - "/unused-incomplete-compensation-workspace", - "acme/widgets", - [entry(50, "Needs repair")], - { - commitItemMutations: async () => { throw incomplete; }, - normalizeItemId: (input, prefix) => `${prefix}${input}`, - readSettings: async () => ({ id_prefix: "test-" }), + const root = freshTracker(); + try { + await assert.rejects( + () => importGithubAtomic(root, "acme/widgets", [entry(50, "Original recovery plan")], { + onTransition: context => { + if (context.transition === "step_recorded") throw new Error("Fixture apply failed"); + if (context.transition === "compensating") throw new Error("Fixture compensation interrupted"); + }, + }), + (err: unknown) => { + assert.ok(err instanceof CommandError); + assert.match(err.message, /compensation was incomplete/); + assert.match(err.message, /may contain partially applied state/); + assert.doesNotMatch(err.message, /no new partial committed state/); + return true; }, - ), - (err: unknown) => { - assert.ok(err instanceof CommandError); - assert.match((err as Error).message, /compensation was incomplete/); - assert.match((err as Error).message, /may contain partially applied state/); - assert.doesNotMatch((err as Error).message, /no new partial committed state/); - return true; - }, - ); + ); + const pending = await listUnfinishedGithubImports(root); + assert.equal(pending.length, 1); + assert.equal(pending[0]!.status, "compensating"); + await assert.rejects(() => importGithubAtomic(root, "acme/widgets", [entry(50, "Changed retry input")]), { code: "github_import_plan_drift" }); + const originalPlan = await readGithubImportPlan(root, pending[0]!.transactionId); + assert.ok(originalPlan, "a compensated create keeps the original plan while its history reserves the identity"); + assert.equal(originalPlan.entries[0]!.title, "Original recovery plan"); + assert.equal(itemCount(root), 0); + const originalId = originalPlan.itemIds![0]![1]; + await sdk.restore(originalId, "1", { author: "fixture", message: "Explicitly restore original transaction birth" }, { pmRoot: root }); + const recovered = await importGithubAtomic(root, "acme/widgets", [entry(50, "Changed retry input")]); + assert.equal(recovered.transactionId, pending[0]!.transactionId); + assert.equal(listItems(root)[0]!.title, "Original recovery plan"); + assert.deepEqual(await listUnfinishedGithubImports(root), []); + } finally { fs.rmSync(root, { recursive: true, force: true }); } + }); test("SDK resolution reports an actionable version error", async () => { diff --git a/test/blockers.test.ts b/test/blockers.test.ts index fa38bba6..64c7054d 100644 --- a/test/blockers.test.ts +++ b/test/blockers.test.ts @@ -22,7 +22,7 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { fileURLToPath } from "node:url"; -import { comments as readComments } from "@unbrained/pm-cli/sdk"; +import { comments as readComments, mutateItem, readSettings } from "@unbrained/pm-cli/sdk"; import { createExtensionTestHarness } from "@unbrained/pm-cli/sdk/testing"; // Decode TOON CLI output with the same public TOON parser the pm CLI itself uses // (@toon-format/toon 4.1.1, the version pm-cli's decoder delegates to). The SDK @@ -194,7 +194,7 @@ test("parseBlockerReferences masks foreign locators and unwraps the github: sche test("parseBlockerReferences rejects lookalikes", () => { assert.deepEqual( github.parseBlockerReferences("issue/854 #854abc ###12 https://example.com/x#4 GH-12345678901"), - [], + [{ form: "unparsed_url", raw: "https://example.com/x#4" }], ); // A markdown heading with a number is not a reference, and a long digit run // cannot partially match. @@ -226,9 +226,9 @@ test("resolveBlockerReferences leaves hint-dependent forms unresolved without a // Candidate selection ---------------------------------------------------------- -test("collectBlockerReferences excludes every own provenance key from all text sources", () => { +test("collectBlockerReferences ignores shared tags but refuses contradictory explicit ownership", () => { const item: github.BlockerPmItem = { - id: "fixture-imported", title: "Imported issue", status: "blocked", + id: "fixture-imported", title: "Imported issue", status: "blocked", github_url: "https://github.com/unbraind/pm-cli/issues/1", tags: ["ordinary", "gh:Unbraind/pm-cli#1", "gh:unbraind/pm-web#2"], description: "GH issue #1: https://github.com/unbraind/pm-cli/issues/1", blockedReason: "unbraind/pm-cli#854 and unbraind/pm-web#2", @@ -236,9 +236,12 @@ test("collectBlockerReferences excludes every own provenance key from all text s dependencies: [{ id: "github:unbraind/pm-web#2", kind: "blocked_by" }], comments: [{ text: "Self: GH-1; actual blocker: #854; other: unbraind/pm-web#854" }], }; + assert.throws(() => github.collectBlockerReferences({ ...item, github_repo: "other/project" }, "other/repo"), { code: "github_import_owner_conflict" }); assert.deepEqual(github.collectBlockerReferences(item, "other/repo").map(ref => ref.key), - ["unbraind/pm-cli#854", "unbraind/pm-web#854"]); - assert.deepEqual(github.collectBlockerReferences({ ...item, tags: ["ordinary"] }, "unbraind/pm-cli") + ["unbraind/pm-cli#854", "unbraind/pm-web#2", "unbraind/pm-web#854"]); + assert.deepEqual(github.collectBlockerReferences({ ...item, github_url: undefined, tags: ["ordinary"] }, "unbraind/pm-cli") + .map(ref => ref.key), ["unbraind/pm-cli#854", "unbraind/pm-web#2", "unbraind/pm-web#854"]); + assert.deepEqual(github.collectBlockerReferences({ ...item, github_url: undefined, description: `Context. ${item.description}`, tags: ["ordinary"] }, "unbraind/pm-cli") .map(ref => ref.key), ["unbraind/pm-cli#1", "unbraind/pm-cli#854", "unbraind/pm-web#2", "unbraind/pm-web#854"]); }); @@ -438,7 +441,13 @@ test("imported blocker candidates clear only real blockers and never their own p "--blocked-reason", "unbraind/pm-cli#854"]); const selfOnly = createItem(root, "Imported closed issue", ["--tags", "gh:unbraind/pm-cli#854", "--description", "GH issue #854: https://github.com/unbraind/pm-cli/issues/854"]); - for (const id of [blocked, selfOnly]) pm(root, ["update", id, "--status", "blocked"]); + for (const [id, number] of [[blocked, 1246], [selfOnly, 854]] as const) { + await mutateItem({ pmRoot: root, id, settings: await readSettings(root), author: "fixture", op: "update", mutate: document => { + document.metadata.github_url = `https://github.com/unbraind/pm-cli/issues/${number}`; + return { changedFields: ["github_url"] }; + } }); + pm(root, ["update", id, "--status", "blocked"]); + } await withEnv({ GH_TOKEN: "ghp-test" }, () => withMockGithub( graphqlFixtureHandler([["query", fixture("issue-854-closed.json")]]), async (server) => { const report = await runBlockers(root, { comment: true }); diff --git a/test/cited-issue-adoption.test.ts b/test/cited-issue-adoption.test.ts index 596ef2d9..1f4976ac 100644 --- a/test/cited-issue-adoption.test.ts +++ b/test/cited-issue-adoption.test.ts @@ -8,6 +8,7 @@ import test from "node:test"; import { create, get, listAllComplete, certifyCompleteListResult } from "@unbrained/pm-cli/sdk"; import { parseImportOptions, prepareGithubImport, runImport, verifyImportIdempotency, type GhIssue, type PreparedGithubImport } from "../index.ts"; import { AmbiguousCitationError, matchImportedIssue as resolveImportedIssue } from "../import-fidelity.ts"; +import { githubRepositoryHash } from "../import-ownership.ts"; import { captureStderr, withEnv } from "./helpers/mock-github-server.ts"; /** Existing adoption regressions opt in explicitly; default-off cases call the production matcher directly. */ @@ -93,7 +94,7 @@ for (const atomic of [false, true]) { const entry = (preview.result.plan as { action: string; candidates: string[]; remedy: string }[])[0]!; assert.equal(entry.action, "ambiguous_citation"); assert.deepEqual(entry.candidates, [first.id, second.id].sort()); - assert.match(entry.remedy, /Tag the correct item with gh:acme\/widgets#30/); + assert.match(entry.remedy, /Set github_url to https:\/\/github.com\/acme\/widgets\/issues\/30/); assert.match(preview.stderr.join("\n"), /ambiguous_citation/); for (const gate of [false, true]) await assert.rejects(importing(pmRoot, { atomic, gate }, batch), /ambiguous_citation/); assert.deepEqual(await items(pmRoot), before); @@ -202,6 +203,17 @@ for (const atomic of [false, true]) { } +test("an unrelated item with contradictory GitHub aliases does not abort citation matching", () => { + // Structured-ownership checks only ask whether owners exist; conflicts outside the target warn instead of refusing. + // The conflicted item cites the imported issue, so it is an eligible citation candidate whose ownership is checked. + // An importer-created id (claims #41) and github_url (claims #42) disagree; the prose cites only the imported #30, + // so the item is an eligible candidate and no other explicit field short-circuits the structured-owner check. + const conflicted = { id: `fixture-github-${githubRepositoryHash(repo)}-41`, author: "pm-github", title: "Elsewhere", github_url: `https://github.com/${repo}/issues/42`, description: `Fixes ${repo}#30` }; + let result: ReturnType | undefined; + assert.doesNotThrow(() => { result = matchImportedIssue([conflicted], issue); }); + assert.equal(result!.via, "new", "an item that already has owners is never adopted by citation"); +}); + test("raw exclusion rejects Markdown-wrapped second numbers across every prose field", () => { const wrappers = [["**", "**"], ["*", "*"], ["_", "_"], ["`", "`"], ["```text\n", "\n```"], ["<", ">"], ["[reference](", ")"], ["![reference](", ")"]]; const references = [`https://github.com/${repo}/issues/31`, `https://github.com/${repo}/pull/31`, `${repo}#31`, "#31", @@ -394,9 +406,9 @@ test("bare numbers, other repos, longer identities, URL paths and different issu assert.equal(matchImportedIssue([{ title: repo + "#30" }], issue).via, "new"); }); -test("tagged and URL-linked owners take precedence over even multiple prose citations", () => { +test("import-provenance and URL-linked owners take precedence over even multiple prose citations", () => { const citing = ["first", "second"].map(id => ({ id, body: `Fixes ${issue.html_url}\n\nIssue: ${issue.html_url}` })); - const tagged = { id: "tagged", tags: [`gh:${repo}#30`] }; + const tagged = { id: "tagged", github_repo: repo, github_number: 30, tags: [`gh:${repo}#30`] }; assert.equal(matchImportedIssue([...citing, tagged], issue, tagged).item, tagged); const linked = { id: "linked", github_url: issue.html_url }; assert.equal(matchImportedIssue([...citing, linked], issue).item, linked); @@ -639,14 +651,12 @@ test("citation adoption never overwrites an item that already carries structured const owned = [ { id: "url", description: `Fixes ${repo}#30`, github_url: "https://github.com/acme/widgets/issues/31" }, { id: "number", description: `Fixes ${repo}#30`, github_number: 31 }, - { id: "links", description: `Fixes ${repo}#30`, external_links: ["https://github.com/acme/widgets/issues/31"] }, { id: "ref", description: `Fixes ${repo}#30`, external_ref: "gh:acme/widgets#31" }, { id: "linear", description: `Fixes ${repo}#30`, external_ref: "linear:ENG-31" }, { id: "source-owner", description: `Fixes ${repo}#30`, source_owner: "linear", status: "closed" }, { id: "provider-field", description: `Fixes ${repo}#30`, jira_issue_key: "ENG-31" }, { id: "github-field", description: `Fixes ${repo}#30`, github_state: "closed" }, { id: "githuburl", description: `Fixes ${repo}#30`, githuburl: "https://github.com/acme/widgets/issues/31" }, - { id: "upper-tag", title: `${repo}#30`, description: `Fixes ${repo}#30`, tags: ["GH:other/widgets#31"] }, { id: "not-link-only", description: `Fixes ${repo}#30`, github_link_only: false }, ]; for (const item of owned) { @@ -654,6 +664,10 @@ test("citation adoption never overwrites an item that already carries structured assert.equal(result.item, undefined, item.id); assert.deepEqual(result.citation_candidates, [{ id: item.id, reason: "structured_owner" }], item.id); } + const linkedReference = { id: "links", description: `Fixes ${repo}#30`, external_links: ["https://github.com/acme/widgets/issues/31"] }; + assert.equal(matchImportedIssue([linkedReference], issue).item, linkedReference, "generic links do not establish another owner"); + const reference = { id: "upper-tag", title: `${repo}#30`, description: `Fixes ${repo}#30`, tags: ["GH:other/widgets#31"] }; + assert.equal(matchImportedIssue([reference], issue).item, reference, "reference tags do not establish another owner"); const native = { id: "native", description: "GitHub issue https://github.com/acme/widgets/issues/31", title: `Fix ${repo}#30` }; assert.equal(matchImportedIssue([native], issue).item, undefined); const docs = { id: "docs", description: `Fixes ${repo}#30`, links: ["https://example.com/design"] }; diff --git a/test/compatibility-floor.test.ts b/test/compatibility-floor.test.ts index 5feb1799..0fdd802e 100644 --- a/test/compatibility-floor.test.ts +++ b/test/compatibility-floor.test.ts @@ -33,7 +33,7 @@ const changelog = readFileSync(resolve(repoRoot, "CHANGELOG.md"), "utf8"); const CLI = "@unbrained/pm-cli"; const EXACT_VERSION = /^\d+\.\d+\.\d+$/; -const SDK_CONTRACT_FLOOR = "2026.10.17"; +const SDK_CONTRACT_FLOOR = "2026.10.23"; /** * Two independent systems enforce the pm CLI compatibility floor, and each reads @@ -98,7 +98,7 @@ test("the extension manifest declares the same floor the CLI actually enforces", assert.strictEqual( declared, SDK_CONTRACT_FLOOR, - "lossless table decoding and under-lock transaction matching require the pm CLI 2026.10.17 host contract", + "lossless table decoding, under-lock transaction matching and fenced ownership leases require the pm CLI 2026.10.23 host contract", ); assert.ok( !Object.hasOwn(extensionManifest, "pm"), @@ -109,7 +109,7 @@ test("the extension manifest declares the same floor the CLI actually enforces", /BREAKING: pm-github now requires pm CLI 2026\.8\.20 or newer; older hosts may fail installation or runtime validation/, "CHANGELOG.md must disclose the compatibility break enforced by the manifest and peer dependency", ); - assert.match(changelog, /BREAKING: PM CLI 2026\.10\.17 minimum/, + assert.match(changelog, /BREAKING: PM CLI 2026\.10\.23 minimum/, "the pending pm item must disclose the current SDK compatibility break"); }); diff --git a/test/coverage-edges.test.ts b/test/coverage-edges.test.ts index 09a12150..ef472a30 100644 --- a/test/coverage-edges.test.ts +++ b/test/coverage-edges.test.ts @@ -28,7 +28,7 @@ function tracker(t: TestContext, git = false): string { } function issue(extra: Partial = {}): github.GhIssue { return { number: 1, title: "Widget", body: null, state: "open", labels: [], assignee: null, milestone: null, - created_at: "2026-01-01T00:00:00Z", updated_at: "2026-10-01T00:00:00Z", html_url: `https://example.test/acme/widgets/issues/${extra.number ?? 1}`, user: null, ...extra }; + created_at: "2026-01-01T00:00:00Z", updated_at: "2026-10-01T00:00:00Z", html_url: `https://github.com/acme/widgets/issues/${extra.number ?? 1}`, user: null, ...extra }; } function entry(extra: Partial = {}): github.PreparedGithubImport { return { issueNumber: 1, title: "Widget", itemType: "Task", status: "open", description: "d", body: "b", @@ -125,6 +125,10 @@ test("sync scoped empty plans and export preview shapes use real local items", a test("scoped sync and labelled export apply preserve receipts after partial remote failure", async t => { const root = tracker(t); const first = await sdk.create({ title: "Linked", type: "Task", status: "open", tags: "gh:acme/widgets#1" }, { pmRoot: root }); + await sdk.mutateItem({ pmRoot: root, id: first.item.id, settings: await sdk.readSettings(root), author: "fixture", op: "update", mutate: document => { + document.metadata.github_url = "https://github.com/acme/widgets/issues/1"; + return { changedFields: ["github_url"] }; + } }); const second = await sdk.create({ title: "New", type: "Task", tags: "bug" }, { pmRoot: root }); await withEnv({ GITHUB_TOKEN: "fixture-token", GH_TOKEN: undefined }, async () => { await withMockGithub((req, res) => { @@ -132,11 +136,11 @@ test("scoped sync and labelled export apply preserve receipts after partial remo else if (req.method === "PATCH") jsonResponse(res, 200, { number: 1 }); else jsonResponse(res, 422, { message: "refused" }); }, async () => { - const preview = await captureStderr(() => command(root, "github sync", { repo: "acme/widgets", ids: first.item.id, "dry-run": true })); + const preview = await captureStderr(() => command(root, "github sync", { repo: "acme/widgets", ids: first.item.id, "allow-reference-tags": true, "dry-run": true })); assert.ok(preview.result.result); - const synced = await captureStderr(() => command(root, "github sync", { repo: "acme/widgets", ids: first.item.id, apply: true })); + const synced = await captureStderr(() => command(root, "github sync", { repo: "acme/widgets", ids: first.item.id, "allow-reference-tags": true, apply: true })); assert.ok(synced.result.result); - const exported = await captureStderr(() => command(root, "github export", { repo: "acme/widgets", ids: `${first.item.id},${second.item.id}`, apply: true, "label-map": "bug=defect" })); + const exported = await captureStderr(() => command(root, "github export", { repo: "acme/widgets", ids: `${first.item.id},${second.item.id}`, "allow-reference-tags": true, apply: true, "label-map": "bug=defect" })); assert.match(exported.stderr.join("\n"), /1 item\(s\) failed/); }); }); diff --git a/test/coverage-remainder.test.ts b/test/coverage-remainder.test.ts index 6035f88d..f53a8f2a 100644 --- a/test/coverage-remainder.test.ts +++ b/test/coverage-remainder.test.ts @@ -5,6 +5,7 @@ * the local HTTP stand-in. Injected dependencies are the production seams * (fetch, commit, pm subprocess via PATH), not substitutes for the unit. */ +import { createImportMirror } from "./helpers/import-mirror.ts"; import assert from "node:assert/strict"; import test from "node:test"; import { spawn, spawnSync } from "node:child_process"; @@ -13,11 +14,11 @@ import os from "node:os"; import path from "node:path"; import { fileURLToPath } from "node:url"; -import { create, listAllComplete, certifyCompleteListResult } from "@unbrained/pm-cli/sdk"; +import { init, normalizeItemId, create, mutateItem, readSettings, listAllComplete, certifyCompleteListResult, runWithActiveExtensions, setActiveExtensionHooks } from "@unbrained/pm-cli/sdk"; import { createExtensionTestHarness, type ExtensionTestHarness } from "@unbrained/pm-cli/sdk/testing"; import extension, { - applyClientFilters, + deriveAtomicItemId, applyClientFilters, applyExportPlan, buildIssuesUrl, collectProjectsV2Pages, @@ -29,7 +30,7 @@ import extension, { runImport, syncGithubCommentsToAnnotations, type GhIssue, - type ImportOptions, + type ImportOptions, type PmItem, } from "../index.ts"; import { nodeScenario } from "./helpers/node-scenario.ts"; import { withReadOnlyDirectory } from "./helpers/read-only-directory.ts"; @@ -46,6 +47,8 @@ const harnessPromise: Promise = createExtensionTestHarness }); const REAL_PM = fileURLToPath(new URL("../node_modules/.bin/pm", import.meta.url)); +// Standalone test invocations use the same pinned CLI as npm test. +process.env.PATH = path.dirname(REAL_PM) + path.delimiter + process.env.PATH; function issue(overrides: Partial = {}): GhIssue { return { @@ -58,7 +61,7 @@ function issue(overrides: Partial = {}): GhIssue { milestone: null, created_at: "2026-01-01T00:00:00Z", updated_at: "2026-10-01T00:00:00Z", - html_url: `https://example.test/acme/widgets/issues/${overrides.number ?? 7}`, + html_url: `https://github.com/acme/widgets/issues/${overrides.number ?? 7}`, user: null, ...overrides, }; @@ -82,10 +85,13 @@ function gitTracker(prefix: string): string { /** Create the real target represented by a command-failure fixture snapshot. */ async function seedGithubItem(root: string, number: number) { - return (await create({ title: "old", type: "Task", tags: `gh:acme/widgets#${number}` }, + return (await createImportMirror({ title: "old", type: "Task", tags: `gh:acme/widgets#${number}` }, { pmRoot: path.join(root, ".agents/pm") })).item; } +/** Explicit importer-generated targets for command-failure snapshots. */ +function mirrorId(number: number): string { return deriveAtomicItemId("acme/widgets", number, "test-", normalizeItemId); } + /** Prime source metadata so a readonly fixture isolates optional annotation writes. */ async function primeSourceTask(root: string): Promise { const tracker = path.join(root, ".agents/pm"); @@ -97,6 +103,16 @@ async function primeSourceTask(root: string): Promise { return result.items[0]!.id; } +/** Exercise a real SDK policy refusal at its locked mutation boundary. */ +async function withNativeRefusal(root: string, refuses: (before: PmItem | undefined, after: PmItem | undefined) => boolean, run: () => Promise) { + await runWithActiveExtensions({ path: path.join(root, ".agents/pm"), noExtensions: true }, async () => { + setActiveExtensionHooks({ beforeCommand: [], afterCommand: [], onRead: [], onWrite: [], onIndex: [], beforeMutation: [{ layer: "project", name: "fixture-native-refusal", run: context => { + if (refuses(context.before?.metadata, context.after?.metadata)) return { allow: false, code: "fixture_refused", remediation: "Fixture policy refusal" }; + } }] }); + await run(); + }); +} + async function withFakePm(body: string, fn: () => Promise): Promise { const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pm-github-fake-pm-")); fs.writeFileSync(path.join(dir, "pm"), `#!/bin/sh\n${body}\n`, { mode: 0o755 }); @@ -130,12 +146,13 @@ test("client filters and issue URL query keep optional import constraints", () = test("atomic import falls back when settings are unreadable and reports interruption", async () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), "pm-github-atomic-settings-")); try { + await init("fixture", { defaults: true, withPackages: false, agentGuidance: "skip" }, { cwd: root, pmRoot: root, noExtensions: true }); const entry = { issueNumber: 7, title: "Widget", itemType: "Task", status: "open", - description: "d", + description: "GH issue #7: https://github.com/acme/widgets/issues/7", body: "b", tags: ["gh:acme/widgets#7"], comments: [], @@ -209,8 +226,8 @@ test("dependency linking reports a non-Error snapshot failure and a detector fai assert.match(skipped.failures[0] ?? "", /metadata down/); const advised = await linkImportedDependencies("acme/widgets", [issue({ number: 1, body: "Blocked by #2" })], "unused", { listItemMetadata: async () => [ - { id: "pm-1", tags: ["gh:acme/widgets#1"], status: "open" }, - { id: "pm-2", tags: ["gh:acme/widgets#2"], status: "open" }, + { id: "pm-1", github_repo: "acme/widgets", github_number: 1, tags: ["gh:acme/widgets#1"], status: "open" }, + { id: "pm-2", github_repo: "acme/widgets", github_number: 2, tags: ["gh:acme/widgets#2"], status: "open" }, ], applyDependencyLink: () => ({ ok: true, stderr: "" }), collectOrderingCycleWarnings: () => { throw "detector down"; }, @@ -223,6 +240,7 @@ test("dependency linking reports a non-Error snapshot failure and a detector fai test("ungated import skips a blank title, reports comment-fetch failure, and previews link-deps", async () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), "pm-github-import-preview-")); try { + await init("fixture", { defaults: true, withPackages: false, agentGuidance: "skip" }, { cwd: root, pmRoot: root, noExtensions: true }); const { stderr, result } = await captureStderr(() => runImport("acme/widgets", root, opts({ "link-deps": true, dryRun: true, "with-comments": true }), { resolveToken: () => "tok", readItems: () => [], @@ -289,12 +307,7 @@ test("a gated non-atomic import fails closed when a planned write fails", async try { const existing = await seedGithubItem(root, 8); const clean = await seedGithubItem(root, 9); - await withFakePm(` - for arg in "$@"; do - if [ "$arg" = "close" ]; then echo close-refused >&2; exit 1; fi - done - exec "$REAL_PM" "$@" - `, async () => { + await withNativeRefusal(root, (_before, after) => after?.id === existing.id && after.status === "closed", async () => { await captureStderr(() => assert.rejects(runImport("acme/widgets", root, opts({ gate: true }), { resolveToken: () => "tok", readItems: () => [existing, clean], @@ -319,16 +332,12 @@ for (const linking of [false, true]) { const source: GhIssue = { ...recorded, state_reason: "not_planned", ...(linking ? { body: "Mirrored from https://linear.app/fixture/issue/ENG-85" } : {}) }; try { - const owner = (await create({ title: "Existing owner", type: "Task", + const owner = (await (linking ? create : createImportMirror)({ title: "Existing owner", type: "Task", description: linking ? "[linear] linear_id=fixture linear_url=https://linear.app/fixture/issue/ENG-85" : "Existing GitHub owner", tags: linking ? "linear:fixture" : "gh:unbraind/pm-beads#85" }, { pmRoot: tracker })).item; - const refused = linking ? "--add-tags" : "canceled"; - await withFakePm(` - for arg in "$@"; do - if [ "$arg" = "${refused}" ]; then echo fixture-refused >&2; exit 1; fi - done - exec "$REAL_PM" "$@" - `, async () => { + await withNativeRefusal(root, (before, after) => after?.id === owner.id && (linking + ? !before?.tags?.includes("gh:unbraind/pm-beads#85") && Boolean(after.tags?.includes("gh:unbraind/pm-beads#85")) + : after.status === "canceled"), async () => { await withMockGithub((request, response) => { jsonResponse(response, 200, request.url?.includes("/timeline") ? [] : [source]); }, async () => { @@ -381,70 +390,50 @@ test("a gated import fails closed when a per-comment add fails mid-sync", nonRoo const root = gitTracker("pm-github-gated-add-fail-"); const tracker = path.join(root, ".agents", "pm"); const itemId = await primeSourceTask(root); - assert.ok(itemId); const syncedComment = { id: 21, user: { login: "octo" }, created_at: "2026-01-01T00:00:00Z", body: "already synced" }; const failingComment = { id: 22, user: { login: "octo" }, created_at: "2026-01-02T00:00:00Z", body: "cannot land" }; - // Land the first comment while the tasks directory is still writable so the - // gated run below proves the loop continues past it: one skip, one failure. await syncGithubCommentsToAnnotations(itemId, [syncedComment], tracker, 7); - const tasksDir = path.join(tracker, "tasks"); try { - await withReadOnlyDirectory(tasksDir, async () => { - await withFakePm(`exit 0`, async () => { - const gated = await captureStderr(() => assert.rejects(runImport("acme/widgets", root, opts({ gate: true, "comments-mode": "annotations" }), { - resolveToken: () => "tok", - readItems: () => [{ id: itemId, title: "old", status: "open", tags: ["gh:acme/widgets#7"] }], - fetchIssues: async () => [issue({ comments: 2 })], - fetchIssueComments: async () => [syncedComment, failingComment], - }), /planned write\(s\) failed/)); - assert.match(gated.stderr.join("\n"), /comment 22 sync failed/); - }); + await withReadOnlyDirectory(path.join(tracker, "tasks"), async () => { + const gated = await captureStderr(() => assert.rejects(runImport("acme/widgets", root, opts({ type: "Task", gate: true, "comments-mode": "annotations" }), { + resolveToken: () => "tok", fetchIssues: async () => [issue({ comments: 2 })], + fetchIssueComments: async () => [syncedComment, failingComment], + }), /planned write\(s\) failed/)); + assert.match(gated.stderr.join("\n"), /comment 22 sync failed/); }); - } finally { - fs.rmSync(root, { recursive: true, force: true }); - } + } finally { fs.rmSync(root, { recursive: true, force: true }); } }); -test("a gated import fails closed when a created id cannot be parsed with annotations sync on", async () => { - const root = gitTracker("pm-github-gated-unparsed-"); +test("native creation returns its planned identity and synchronizes annotations without CLI output parsing", async () => { + const root = gitTracker("pm-github-native-create-"); try { - await withFakePm(` - json=0 - for arg in "$@"; do - if [ "$arg" = "--json" ]; then json=1; fi - done - if [ "$json" = "1" ]; then printf '%s\\n' 'not-json'; exit 0; fi - exit 0 - `, async () => { - const gated = await captureStderr(() => assert.rejects(runImport("acme/widgets", root, opts({ gate: true, "comments-mode": "annotations" }), { - resolveToken: () => "tok", - readItems: () => [], - fetchIssues: async () => [issue({ comments: 1 })], - fetchIssueComments: async () => [{ id: 31, user: { login: "octo" }, created_at: "2026-01-01T00:00:00Z", body: "note" }], - }), /Could not finalize.*created id missing/)); - assert.match(gated.stderr.join("\n"), /could not parse created item id — comments not synced/); - }); - } finally { - fs.rmSync(root, { recursive: true, force: true }); - } + const { result } = await captureStderr(() => runImport("acme/widgets", root, opts({ gate: true, "comments-mode": "annotations" }), { + resolveToken: () => "tok", fetchIssues: async () => [issue({ comments: 1 })], + fetchIssueComments: async () => [{ id: 31, user: { login: "octo" }, created_at: "2026-01-01T00:00:00Z", body: "note" }], + })); + assert.equal("imported" in result && result.imported, 1); + const item = certifyCompleteListResult(await listAllComplete({ includeBody: true }, { pmRoot: path.join(root, ".agents/pm") })).items[0]!; + assert.equal(item.id, mirrorId(7)); + assert.equal(item.comments?.length, 1); + } finally { fs.rmSync(root, { recursive: true, force: true }); } }); test("a gated atomic import fails closed when committed items lack their planned comments", async () => { const root = gitTracker("pm-github-gated-atomic-comments-"); const tracker = path.join(root, ".agents", "pm"); try { - await assert.rejects(captureStderr(() => runImport("acme/widgets", tracker, opts({ atomic: true, gate: true, "comments-mode": "annotations" }), { - resolveToken: () => "tok", - readItems: () => [{ id: "pm-fresh", title: "t", status: "open", tags: ["gh:acme/widgets#7"] }], - fetchIssues: async () => [issue({ comments: 1 })], - fetchIssueComments: async () => [{ id: 41, user: { login: "octo" }, created_at: "2026-01-01T00:00:00Z", body: "note" }], - // The commit "succeeds" but routes no created id, so the planned comment - // is unreachable — only the comment-failure count can fail the run closed. - commitAtomic: async () => ({ transactionId: "tx-comments", recovered: false, imported: 1, updated: 0, itemIds: new Map() }), - })), /planned write\(s\) failed/); - } finally { - fs.rmSync(root, { recursive: true, force: true }); - } + await runWithActiveExtensions({ path: tracker, noExtensions: true }, async () => { + setActiveExtensionHooks({ beforeCommand: [], afterCommand: [], onRead: [], onWrite: [], onIndex: [], beforeMutation: [{ layer: "project", name: "fixture-comments", run: context => { + if (context.operation === "comment_add") return { allow: false, code: "fixture_annotation_refused", remediation: "Repair source comments on a normal sync" }; + } }] }); + await assert.rejects(captureStderr(() => runImport("acme/widgets", tracker, opts({ atomic: true, gate: true, "comments-mode": "annotations" }), { + resolveToken: () => "tok", fetchIssues: async () => [issue({ comments: 1 })], + fetchIssueComments: async () => [{ id: 41, user: { login: "octo" }, created_at: "2026-01-01T00:00:00Z", body: "note" }], + })), /planned write\(s\) failed/); + }); + assert.equal((await listAllComplete({}, { pmRoot: tracker })).items.length, 1); + assert.deepEqual(fs.readdirSync(path.join(tracker, "transactions/pm-github")).filter(name => name.endsWith(".json")), []); + } finally { fs.rmSync(root, { recursive: true, force: true }); } }); test("the same comment-sync failures without --gate still import with a warning", nonRootOnly, async () => { @@ -459,29 +448,29 @@ test("the same comment-sync failures without --gate still import with a warning" const lockPath = importCommentSyncLockPath(root, itemId); fs.mkdirSync(path.dirname(lockPath), { recursive: true }); fs.writeFileSync(lockPath, JSON.stringify({ pid: holder.pid, token: "held", created_at: new Date().toISOString() }) + "\n"); - await withFakePm(`exit 0`, async () => { - const contended = await captureStderr(() => runImport("acme/widgets", root, opts({ "comments-mode": "annotations" }), { + { + const contended = await captureStderr(() => runImport("acme/widgets", root, opts({ type: "Task", "comments-mode": "annotations" }), { resolveToken: () => "tok", - readItems: () => [{ id: itemId, title: "old", status: "open", tags: ["gh:acme/widgets#7"] }], fetchIssues: async () => [issue({ comments: 1 })], fetchIssueComments: async () => [failingComment], })); - assert.ok("updated" in contended.result && contended.result.updated === 1); + assert.ok("updated" in contended.result && contended.result.updated === 0); + assert.ok("commentWriteFailures" in contended.result && contended.result.commentWriteFailures === 1); assert.match(contended.stderr.join("\n"), /another import holds the comment-sync lock/); - }); + } fs.rmSync(lockPath, { force: true }); const tasksDir = path.join(tracker, "tasks"); await withReadOnlyDirectory(tasksDir, async () => { - await withFakePm(`exit 0`, async () => { - const refused = await captureStderr(() => runImport("acme/widgets", root, opts({ "comments-mode": "annotations" }), { + { + const refused = await captureStderr(() => runImport("acme/widgets", root, opts({ type: "Task", "comments-mode": "annotations" }), { resolveToken: () => "tok", - readItems: () => [{ id: itemId, title: "old", status: "open", tags: ["gh:acme/widgets#7"] }], fetchIssues: async () => [issue({ comments: 1 })], fetchIssueComments: async () => [failingComment], })); - assert.ok("updated" in refused.result && refused.result.updated === 1); + assert.ok("updated" in refused.result && refused.result.updated === 0); + assert.ok("commentWriteFailures" in refused.result && refused.result.commentWriteFailures === 1); assert.match(refused.stderr.join("\n"), /comment 52 sync failed/); - }); + } }); } finally { holder.kill("SIGKILL"); @@ -489,73 +478,26 @@ test("the same comment-sync failures without --gate still import with a warning" } }); -test("non-atomic import reports update, close, reopen, and unparsed-id failures", async () => { +test("non-atomic import reports native update, close, reopen and create policy failures", async () => { const root = gitTracker("pm-github-import-fail-"); - const listed = spawnSync(REAL_PM, ["--pm-path", root, "--json", "list", "--full"], { encoding: "utf8" }); - assert.equal(listed.status, 0, listed.stderr); try { - await withFakePm(` - for arg in "$@"; do - if [ "$arg" = "update" ]; then echo update-refused >&2; exit 1; fi - done - exit 0 - `, async () => { + const existing7 = await seedGithubItem(root, 7); + const existing8 = await seedGithubItem(root, 8); + await captureStderr(() => runImport("acme/widgets", root, opts({ atomic: true }), { resolveToken: () => undefined, + fetchIssues: async () => [issue({ number: 9, state: "closed", state_reason: "completed", closed_at: "2026-02-01T00:00:00Z" })], fetchIssueTimeline: async () => [] })); + const cases = [ + { source: issue({ assignee: { login: "octo" }, milestone: { title: "Now" } }), id: existing7.id, message: /update failed/, refuses: (before: PmItem | undefined, after: PmItem | undefined) => before?.title !== after?.title }, + { source: issue({ number: 8, state: "closed", state_reason: "completed", closed_at: "2026-02-01T00:00:00Z" }), id: existing8.id, message: /close reconciliation failed/, refuses: (_before: PmItem | undefined, after: PmItem | undefined) => after?.status === "closed" }, + { source: issue({ number: 9 }), id: mirrorId(9), message: /reopen reconciliation failed/, refuses: (before: PmItem | undefined, after: PmItem | undefined) => before?.status === "closed" && after?.status === "open" }, + { source: issue({ number: 10 }), id: mirrorId(10), message: /create failed/, refuses: (before: PmItem | undefined) => !before }, + ]; + for (const entry of cases) await withNativeRefusal(root, (before, after) => after?.id === entry.id && entry.refuses(before, after), async () => { const { stderr } = await captureStderr(() => assert.rejects(runImport("acme/widgets", root, opts(), { - resolveToken: () => "tok", - readItems: () => [{ id: "pm-existing", title: "old", status: "open", tags: ["gh:acme/widgets#7"] }], - fetchIssues: async () => [issue({ assignee: { login: "octo" }, milestone: { title: "Now" } })], - }), /Imported 0 issue/)); - - assert.match(stderr.join("\n"), /update failed/); - }); - - await withFakePm(` - for arg in "$@"; do - if [ "$arg" = "close" ]; then echo close-refused >&2; exit 1; fi - done - exit 0 - `, async () => { - const closed = await captureStderr(() => assert.rejects(runImport("acme/widgets", root, opts(), { - resolveToken: () => "tok", - readItems: () => [{ id: "pm-existing", title: "old", status: "open", tags: ["gh:acme/widgets#8"] }], - fetchIssues: async () => [issue({ number: 8, state: "closed", state_reason: "completed", closed_at: "2026-02-01T00:00:00Z" })], + resolveToken: () => "tok", fetchIssues: async () => [entry.source], fetchIssueTimeline: async () => [], }), /Imported 0 issue/)); - assert.match(closed.stderr.join("\n"), /close reconciliation failed|close after import failed/); + assert.match(stderr.join("\n"), entry.message); }); - - await withFakePm(` - for arg in "$@"; do - if [ "$arg" = "open" ]; then echo reopen-refused >&2; exit 1; fi - done - exit 0 - `, async () => { - const reopened = await captureStderr(() => assert.rejects(runImport("acme/widgets", root, opts(), { - resolveToken: () => "tok", - readItems: () => [{ id: "pm-existing", title: "old", status: "closed", tags: ["gh:acme/widgets#9"] }], - fetchIssues: async () => [issue({ number: 9, state: "open" })], - }), /Imported 0 issue/)); - assert.match(reopened.stderr.join("\n"), /reopen reconciliation failed/); - }); - - await withFakePm(` - json=0 - for arg in "$@"; do - if [ "$arg" = "--json" ]; then json=1; fi - done - if [ "$json" = "1" ]; then printf '%s\\n' 'not-json'; exit 0; fi - exit 0 - `, async () => { - const parsed = await captureStderr(() => assert.rejects(runImport("acme/widgets", root, opts({ "comments-mode": "annotations" }), { - resolveToken: () => "tok", - readItems: () => [], - fetchIssues: async () => [issue({ number: 10, state: "closed", comments: 1 })], - fetchIssueComments: async () => [{ id: 1, user: { login: "octo" }, created_at: "2026-01-01T00:00:00Z", body: "note" }], - }), /Imported 0 issue/)); - assert.match(parsed.stderr.join("\n"), /could not parse created item id/); - }); - } finally { - fs.rmSync(root, { recursive: true, force: true }); - } + } finally { fs.rmSync(root, { recursive: true, force: true }); } }); test("comment sync skips a contended lock and reports a per-comment write failure", nonRootOnly, async () => { @@ -708,13 +650,20 @@ test("sync reports a non-retryable PATCH failure and the after-command hook name assert.ok(id); assert.equal(spawnSync(REAL_PM, ["--path", root, "close", id, "--reason", "fixture closed"], { encoding: "utf8" }).status, 0); } + const tracker = path.join(root, ".agents/pm"); + const linked = (await listAllComplete({}, { pmRoot: tracker })).items[0]!; + const linkedId = linked.id; + await mutateItem({ pmRoot: tracker, settings: await readSettings(tracker), id: linkedId, author: "fixture", op: "update", mutate: document => { + document.metadata.github_url = "https://github.com/acme/widgets/issues/5"; + return { changedFields: ["github_url"] }; + } }); await withEnv({ GITHUB_TOKEN: "tok", GH_TOKEN: undefined }, async () => { const patched = await captureStderr(() => withMockGithub((req, res) => { if (req.method === "PATCH") jsonResponse(res, 422, { message: "no" }); else jsonResponse(res, 200, { state: "open", title: "Linked", number: 5 }); }, () => ext.runCommand({ command: "github sync", - options: { repo: "acme/widgets" }, + options: { repo: "acme/widgets", "allow-reference-tags": true }, pmRoot: root, global: { json: false }, }))); @@ -722,41 +671,42 @@ test("sync reports a non-retryable PATCH failure and the after-command hook name }); await withFakePm(` + case " $* " in *" list "*) exec "$REAL_PM" "$@" ;; esac case "$FAKE_PM_MODE" in fail) exit 1 ;; badjson) printf '%s\n' 'not-json'; exit 0 ;; missingtags) printf '%s\n' '{}'; exit 0 ;; notag) printf '%s\n' '{"tags":["local"]}'; exit 0 ;; - *) printf '%s\n' '{"tags":["gh:acme/widgets#5"]}'; exit 0 ;; + *) printf '%s\n' '{"id":"pm-linked","github_url":"https://github.com/acme/widgets/issues/5","tags":["gh:acme/widgets#5"]}'; exit 0 ;; esac `, async () => { await withEnv({ PM_GITHUB_SYNC: "1" }, async () => { const nudged = await captureStderr(() => ext.runHook({ kind: "after_command", - context: { command: "close", args: ["pm-linked"], pm_root: root, ok: true }, + context: { command: "close", args: [linkedId], pm_root: root, ok: true }, })); assert.match(nudged.stderr.join("\n"), /pm github sync --repo acme\/widgets/); process.env.FAKE_PM_MODE = "fail"; const silent = await captureStderr(() => ext.runHook({ kind: "after_command", - context: { command: "close", args: ["pm-linked"], pm_root: root, ok: true }, + context: { command: "close", args: [linkedId], pm_root: root, ok: true }, })); assert.equal(silent.stderr.join("\n"), ""); process.env.FAKE_PM_MODE = "badjson"; - await ext.runHook({ kind: "after_command", context: { command: "update", args: ["pm-linked"], pm_root: root, ok: true } }); + await ext.runHook({ kind: "after_command", context: { command: "update", args: [linkedId], pm_root: root, ok: true } }); process.env.FAKE_PM_MODE = "notag"; - await ext.runHook({ kind: "after_command", context: { command: "close", args: ["pm-linked"], pm_root: root, ok: true } }); + await ext.runHook({ kind: "after_command", context: { command: "close", args: [linkedId], pm_root: root, ok: true } }); process.env.FAKE_PM_MODE = "missingtags"; - const missingTags = await captureStderr(() => ext.runHook({ kind: "after_command", context: { command: "close", args: ["pm-linked"], pm_root: root, ok: true } })); - assert.equal(missingTags.stderr.join("\n"), ""); + const missingTags = await captureStderr(() => ext.runHook({ kind: "after_command", context: { command: "close", args: [linkedId], pm_root: root, ok: true } })); + assert.match(missingTags.stderr.join("\n"), /pm github sync --repo acme\/widgets/); delete process.env.FAKE_PM_MODE; - await ext.runHook({ kind: "after_command", context: { command: "list", args: ["pm-linked"], pm_root: root, ok: true } }); + await ext.runHook({ kind: "after_command", context: { command: "list", args: [linkedId], pm_root: root, ok: true } }); await ext.runHook({ kind: "after_command", context: { command: "close", args: [], pm_root: root, ok: true } }); - await ext.runHook({ kind: "after_command", context: { command: "close", args: ["pm-linked"], pm_root: "", ok: true } }); - await ext.runHook({ kind: "after_command", context: { command: "close", args: ["pm-linked"], pm_root: root, ok: false } }); + await ext.runHook({ kind: "after_command", context: { command: "close", args: [linkedId], pm_root: "", ok: true } }); + await ext.runHook({ kind: "after_command", context: { command: "close", args: [linkedId], pm_root: root, ok: false } }); }); delete process.env.PM_GITHUB_SYNC; - await ext.runHook({ kind: "after_command", context: { command: "close", args: ["pm-linked"], pm_root: root, ok: true } }); + await ext.runHook({ kind: "after_command", context: { command: "close", args: [linkedId], pm_root: root, ok: true } }); }); } finally { delete process.env.FAKE_PM_MODE; @@ -782,21 +732,21 @@ test("plain imports preserve assigned metadata and synchronize existing annotati } finally { fs.rmSync(root, { recursive: true, force: true }); } }); -test("plain create close failures and unreadable annotation identities report incomplete imports", async () => { +test("plain create and close policy failures report incomplete imports", async () => { const root = gitTracker("pm-github-create-boundary-"); const tracker = path.join(root, ".agents/pm"); try { - await withFakePm(`for arg in "$@"; do if [ "$arg" = "close" ]; then echo close-refused >&2; exit 1; fi; done\nexec "$REAL_PM" "$@"`, async () => { + await withNativeRefusal(root, (_before, after) => after?.id === mirrorId(7) && after.status === "closed", async () => { const { stderr } = await captureStderr(() => assert.rejects(runImport("acme/widgets", tracker, opts(), { resolveToken: () => undefined, fetchIssues: async () => [issue({ state: "closed", closed_at: null })], }), /Imported 0 issue/)); assert.match(stderr.join("\n"), /close after import failed/); }); - await withFakePm(`for arg in "$@"; do if [ "$arg" = "create" ]; then printf 'not-json'; exit 0; fi; done\nexec "$REAL_PM" "$@"`, async () => { + await withNativeRefusal(root, (before, after) => !before && after?.id === mirrorId(2), async () => { const { stderr } = await captureStderr(() => assert.rejects(runImport("acme/widgets", tracker, opts({ "comments-mode": "annotations" }), { resolveToken: () => undefined, fetchIssues: async () => [issue({ number: 2 })], fetchIssueComments: async () => [], - }), /Could not finalize.*created id missing/)); - assert.match(stderr.join("\n"), /comments not synced/); + }), /Imported 0 issue/)); + assert.match(stderr.join("\n"), /create failed/); }); } finally { fs.rmSync(root, { recursive: true, force: true }); } }); diff --git a/test/daily-sync-idempotency.test.ts b/test/daily-sync-idempotency.test.ts index 721a0d68..3214b659 100644 --- a/test/daily-sync-idempotency.test.ts +++ b/test/daily-sync-idempotency.test.ts @@ -40,7 +40,7 @@ for (const atomic of [false, true]) test(`random issue sets imported twice produ const initialized = spawnSync(join(packageRoot, "node_modules/.bin/pm"), ["--path", root, "init", "fixture"], { encoding: "utf8" }); assert.equal(initialized.status, 0, initialized.stderr); const source = issues(seed); - // Native existing owners and newly created mirrors share one import batch. + // Reference-only native work and newly created mirrors share one import batch. const owner = (await create({ title: "Old native owner", type: "Issue", tags: "gh:fixture/project#1" }, { pmRoot: root, noExtensions: true })).item; const deps = { fetchIssues: async () => source, resolveToken: () => undefined }; const options = parseImportOptions({ atomic }); @@ -53,7 +53,7 @@ for (const atomic of [false, true]) test(`random issue sets imported twice produ await captureStderr(() => runImport("fixture/project", root, options, deps)); const after = snapshot(root); for (const [file, bytes] of before) if (file.includes(owner.id)) assert.equal(after.get(file), bytes); - assert.equal((await get(owner.id, {}, { pmRoot: root, noExtensions: true })).item.title, source[0]!.title); + assert.equal((await get(owner.id, {}, { pmRoot: root, noExtensions: true })).item.title, owner.title, "reference-only work keeps its curated title"); assert.equal("updated" in second && second.updated, 0); const preview = (await captureStderr(() => runImport("fixture/project", root, { ...options, dryRun: true }, deps))).result; assert.equal("wouldUpdate" in preview && preview.wouldUpdate, 0, "preview shares the write path's normalized no-op decision"); diff --git a/test/dryrun-preview.test.ts b/test/dryrun-preview.test.ts index 0129d9d4..cf309f11 100644 --- a/test/dryrun-preview.test.ts +++ b/test/dryrun-preview.test.ts @@ -1,5 +1,7 @@ import assert from "node:assert/strict"; import test from "node:test"; +import { withImportPreview } from "./helpers/import-preview.ts"; +import { withMockGithub, jsonResponse } from "./helpers/mock-github-server.ts"; import { parseImportOptions, runImport } from "../index.ts"; import type { GhIssue } from "../index.ts"; @@ -25,25 +27,19 @@ const issue = (number: number, title: string): GhIssue => ({ }); async function previewImport(atomic: boolean) { + return withImportPreview(async root => { const messages: string[] = []; const originalError = console.error; console.error = (...values: unknown[]) => messages.push(values.join(" ")); try { - const result = await runImport( - "acme/widgets", - "/unused-dry-run-workspace", - parseImportOptions({ dryRun: true, ...(atomic ? { atomic: true } : {}) }), - { - resolveToken: () => undefined, - fetchIssues: async () => [issue(1, "Brand new"), issue(2, "Already linked")], - // #2 is already in the workspace, carrying the provenance tag the import writes. - readItems: () => [{ id: "existing-id", tags: ["gh:acme/widgets#2"] }], - }, - ); + const result = await withMockGithub((request, response) => { + jsonResponse(response, 200, request.url?.includes("/timeline") ? [] : [issue(1, "Brand new"), issue(2, "Already linked")]); + }, () => runImport("acme/widgets", root, parseImportOptions({ dryRun: true, ...(atomic ? { atomic: true } : {}) }), { resolveToken: () => undefined })); return { result, messages }; } finally { console.error = originalError; } + }); } test("non-atomic --dry-run previews updates for already-linked issues instead of counting them as imports", async () => { diff --git a/test/durable-recovery.test.ts b/test/durable-recovery.test.ts new file mode 100644 index 00000000..d3f89287 --- /dev/null +++ b/test/durable-recovery.test.ts @@ -0,0 +1,126 @@ +/** Recovery publication failures use native filesystem faults and real SDK trackers. */ +import assert from "node:assert/strict"; +import test from "node:test"; +import fs, { mkdtempSync, rmSync, readFileSync, readdirSync, writeFileSync, mkdirSync, existsSync } from "node:fs"; +import { join, delimiter } from "node:path"; +import { tmpdir } from "node:os"; +import { spawnSync } from "node:child_process"; +import { init, listAllComplete } from "@unbrained/pm-cli/sdk"; +import { importGithubAtomic, runImport, parseImportOptions, readGithubImportPlan, type PreparedGithubImport } from "../index.ts"; +import { writeGithubStateAtomic } from "../durable-state.ts"; +import { withEnv, captureStderr } from "./helpers/mock-github-server.ts"; + +/** Keep tracker persistence real while excluding inherited extension and global state. */ +async function tracker(run: (root: string) => Promise) { + const directory = mkdtempSync(join(tmpdir(), "github-durable-")); + const root = join(directory, "tracker"); + try { + await init("fixture", { defaults: true, withPackages: false, agentGuidance: "skip" }, { pmRoot: root, cwd: directory, noExtensions: true }); + await withEnv({ PM_PATH: root, PM_AUTHOR: "fixture", PM_TELEMETRY_SOURCE_CONTEXT: "automation", PATH: join(import.meta.dirname, "../node_modules/.bin") + delimiter + process.env.PATH }, () => run(root)); + } finally { rmSync(directory, { recursive: true, force: true }); } +} +/** Native documents and audit bytes must remain untouched before a plan is published. */ +function nativeBytes(root: string) { + return readdirSync(root, { recursive: true, withFileTypes: true }).filter(entry => entry.isFile() && (entry.name.endsWith(".toon") || entry.parentPath.includes("history"))) + .map(entry => [join(entry.parentPath, entry.name).slice(root.length), readFileSync(join(entry.parentPath, entry.name), "utf8")]).sort(); +} +/** A clean source plan shared with the crash fixture. */ +function entry(): PreparedGithubImport { + return { issueNumber: 7, title: "Source issue", itemType: "Issue", status: "open", description: "GH issue #7: https://github.com/fixture/repo/issues/7", + body: "Source body", tags: ["gh:fixture/repo#7"], comments: [], syncAnnotations: false }; +} +/** Zero-source polling must never be poisoned by an unpublished candidate. */ +async function emptyImport(root: string) { + return captureStderr(() => runImport("fixture/repo", root, parseImportOptions({ atomic: true }), { resolveToken: () => undefined, fetchIssues: async () => [] })); +} + +test("partial native plan writes do not poison a zero-item retry", async () => tracker(async root => { + const before = nativeBytes(root); + let injected = false; + let error: unknown; + try { + await importGithubAtomic(root, "fixture/repo", [entry()], { onPlanStaged: async ({ staged }) => { + // Real partial bytes plus an injected I/O failure at the publication boundary. + await fs.promises.writeFile(staged, "{"); + injected = true; + throw Object.assign(new Error("Injected partial plan write failure"), { code: "EIO" }); + } }); + } catch (caught) { error = caught; } + assert.equal(injected, true); + assert.deepEqual(nativeBytes(root), before); + assert.equal((await listAllComplete({}, { pmRoot: root })).items.length, 0); + await emptyImport(root); + assert.equal((error as { code: string }).code, "github_import_plan_write_failed"); + await importGithubAtomic(root, "fixture/repo", [entry()]); + assert.equal((await listAllComplete({}, { pmRoot: root })).items.length, 1); +})); + +test("crash after plan fsync and before publication leaves zero-item imports usable", async () => tracker(async root => { + const before = nativeBytes(root); + const child = spawnSync(process.execPath, [join(import.meta.dirname, "fixtures/atomic-plan-crash.ts"), root], { encoding: "utf8", timeout: 30_000 }); + assert.equal(child.signal, "SIGKILL"); + assert.deepEqual(nativeBytes(root), before); + const names = readdirSync(join(root, "transactions/pm-github")); + assert.equal(names.filter(name => !name.startsWith(".") && name.endsWith(".json")).length, 0); + const staged = names.find(name => name.endsWith(".stage")); + assert.ok(staged, "the interrupted candidate stays invisible to recovery discovery"); + assert.equal(JSON.parse(readFileSync(join(root, "transactions/pm-github", staged), "utf8")).entries[0].issueNumber, 7); + await emptyImport(root); + assert.equal((await listAllComplete({}, { pmRoot: root })).items.length, 0); + await importGithubAtomic(root, "fixture/repo", [entry()]); + assert.equal((await listAllComplete({}, { pmRoot: root })).items.length, 1); +})); + +for (const outcome of ["publish", "failure", "interruption"] as const) test(`concurrent state readers see only complete previous or published bytes (${outcome})`, async () => tracker(async root => { + const file = join(root, "reader-state.json"); + const previous = { sequence: 1, text: "Previous receipt" }; + const next = { sequence: 2, text: "x".repeat(100000) }; + writeFileSync(file, JSON.stringify(previous)); + let release!: () => void; + let entered!: () => void; + const paused = new Promise(resolve => { entered = resolve; }); + const proceed = new Promise(resolve => { release = resolve; }); + const publishing = writeGithubStateAtomic(file, JSON.stringify(next), { onStaged: async ({ staged }) => { + await fs.promises.writeFile(staged, "{"); + entered(); + await proceed; + if (outcome === "failure") throw Object.assign(new Error("Injected partial state write failure"), { code: "EIO" }); + if (outcome === "interruption") throw new Error("Interrupted before rename"); + await fs.promises.writeFile(staged, JSON.stringify(next)); + const handle = await fs.promises.open(staged, "r+"); + try { await handle.sync(); } finally { await handle.close(); } + } }); + const settled = publishing.then(() => undefined, error => error as Error); + try { + await paused; + const reads = await Promise.all(Array.from({ length: 32 }, () => fs.promises.readFile(file, "utf8"))); + assert.ok(reads.every(text => text === JSON.stringify(previous)), "A concurrent reader observed unpublished or partial state"); + } finally { release(); await settled; } + const error = await settled; + assert.equal(Boolean(error), outcome !== "publish"); + assert.deepEqual(JSON.parse(readFileSync(file, "utf8")), outcome === "publish" ? next : previous); + // An exclusive writer must preserve a peer's published state. + await assert.rejects(() => writeGithubStateAtomic(file, "{}", { exclusive: true }), { code: "EEXIST" }); + assert.deepEqual(JSON.parse(readFileSync(file, "utf8")), outcome === "publish" ? next : previous); + await writeGithubStateAtomic(file, JSON.stringify(next)); + assert.deepEqual(JSON.parse(readFileSync(file, "utf8")), next); +})); + +for (const body of ["", "{", "null", "[]", "{}", '{"transactionId":"github-import-partial"}']) test(`corrupt recovery plan has a typed actionable refusal (${JSON.stringify(body)})`, async () => tracker(async root => { + const id = "github-import-1234567890abcdef"; + const directory = join(root, "transactions/pm-github"); + mkdirSync(directory, { recursive: true }); + const file = join(directory, `${id}.json`); + writeFileSync(file, body); + const before = nativeBytes(root); + for (const read of [() => readGithubImportPlan(root, id), () => emptyImport(root)]) await assert.rejects(read, error => { + assert.equal((error as { code: string }).code, "github_import_plan_corrupt"); + assert.match(String(error), new RegExp(`${id}.*Restore its original plan.*reconcile its SDK journal`)); + return true; + }); + assert.deepEqual(nativeBytes(root), before); + assert.equal(readFileSync(file, "utf8"), body); + rmSync(file); + await emptyImport(root); + assert.equal(existsSync(file), false); +})); diff --git a/test/durable-state.test.ts b/test/durable-state.test.ts new file mode 100644 index 00000000..2071beee --- /dev/null +++ b/test/durable-state.test.ts @@ -0,0 +1,29 @@ +/** Platform selection is stubbed; publication and fsync use the real filesystem. */ +import assert from "node:assert/strict"; +import test from "node:test"; +import { mkdtemp, readFile, rm } from "node:fs/promises"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; +import { githubStateSyncMode, syncGithubStatePath, writeGithubStateAtomic } from "../durable-state.ts"; +import { flushGithubWebhookState } from "../webhooks.ts"; + +for (const platform of ["win32", "linux", "darwin"] as const) test(`durable state uses writable file sync and supported directory sync (${platform})`, async () => { + assert.equal(githubStateSyncMode(false, platform), "r+", "FlushFileBuffers requires a writable file handle"); + assert.equal(githubStateSyncMode(true, platform), platform === "win32" ? undefined : "r"); + const root = await mkdtemp(join(tmpdir(), "github-sync-mode-")); + try { + const file = join(root, "nested/state.json"); + let platformChecks = 0; + const options = { platform: () => { platformChecks++; return platform; } }; + await writeGithubStateAtomic(file, '{"sequence":1}', options); + await writeGithubStateAtomic(file, '{"sequence":2}', options); + assert.equal(await readFile(file, "utf8"), '{"sequence":2}'); + assert.equal(githubStateSyncMode(false), "r+"); + await syncGithubStatePath(file, false); + assert.equal(platformChecks, 2); + await flushGithubWebhookState(file, options.platform); + assert.equal(platformChecks, 3); + await assert.rejects(() => writeGithubStateAtomic(file, "collision", { ...options, exclusive: true }), (error: unknown) => (error as NodeJS.ErrnoException).code === "EEXIST"); + assert.equal(await readFile(file, "utf8"), '{"sequence":2}'); + } finally { await rm(root, { recursive: true, force: true }); } +}); diff --git a/test/fixtures/atomic-plan-crash.ts b/test/fixtures/atomic-plan-crash.ts new file mode 100644 index 00000000..716731c4 --- /dev/null +++ b/test/fixtures/atomic-plan-crash.ts @@ -0,0 +1,7 @@ +/** Crash at the flushed-candidate boundary, before any recovery plan or SDK mutation is published. */ +import { importGithubAtomic } from "../../index.ts"; + +await importGithubAtomic(process.argv[2]!, "fixture/repo", [{ issueNumber: 7, title: "Source issue", itemType: "Issue", status: "open", + description: "GH issue #7: https://github.com/fixture/repo/issues/7", body: "Source body", tags: ["gh:fixture/repo#7"], comments: [], syncAnnotations: false }], { + onPlanStaged: () => { process.kill(process.pid, "SIGKILL"); }, +}); diff --git a/test/fixtures/ownership-peer.ts b/test/fixtures/ownership-peer.ts new file mode 100644 index 00000000..3368f281 --- /dev/null +++ b/test/fixtures/ownership-peer.ts @@ -0,0 +1,25 @@ +/** Schedule a real SDK peer relink immediately before delegating to the real pinned CLI. */ +import { readFileSync, writeFileSync, existsSync } from "node:fs"; +import { spawnSync } from "node:child_process"; +import { get, mutateItem, getItemPath, getHistoryPath, readSettings, readHistoryEntries, verifyHistoryEntries } from "@unbrained/pm-cli/sdk"; +import assert from "node:assert/strict"; + +/** The one-shot receipt records the exact peer revision the following CLI must preserve. */ +export async function runOwnershipPeer(planFile: string): Promise { + const plan = JSON.parse(readFileSync(planFile, "utf8")) as { root: string; id: string; receipt: string; realPm: string }; + if (!existsSync(plan.receipt)) { + const item = (await get(plan.id, {}, { pmRoot: plan.root })).item; + await mutateItem({ pmRoot: plan.root, id: plan.id, settings: await readSettings(plan.root), author: "fixture-peer", op: "update", + message: "Peer relink before CLI write", mutate: document => { + document.metadata.github_url = "https://github.com/other/project/issues/8"; + document.metadata.github_repo = "other/project"; document.metadata.github_number = 8; + return { changedFields: ["github_url", "github_repo", "github_number"] }; + } }); + const history = verifyHistoryEntries(await readHistoryEntries(getHistoryPath(plan.root, plan.id), plan.id)); + assert.equal(history.ok, true, history.errors.join("; ")); + assert.equal(history.latest_after_hash, (await get(plan.id, {}, { pmRoot: plan.root })).item_hash); + writeFileSync(plan.receipt, JSON.stringify([readFileSync(getItemPath(plan.root, item.type!, plan.id), "utf8"), readFileSync(getHistoryPath(plan.root, plan.id), "utf8")])); + } + const result = spawnSync(plan.realPm, process.argv.slice(2), { stdio: "inherit" }); + process.exitCode = result.status ?? 1; +} diff --git a/test/fixtures/refresh-crash.ts b/test/fixtures/refresh-crash.ts index a569102a..65e655cc 100644 --- a/test/fixtures/refresh-crash.ts +++ b/test/fixtures/refresh-crash.ts @@ -6,12 +6,12 @@ * test/refresh.test.ts; exits 9 to emulate a hard crash without compensation. */ import { importGithubAtomic, parseImportOptions, prepareGithubImport } from "../../index.ts"; -import { create } from "@unbrained/pm-cli/sdk"; +import { createImportMirror } from "../helpers/import-mirror.ts"; const pmRoot = process.argv[2]!; const boundary = process.argv[3] ?? "committing"; const number = Number(process.argv[4] ?? "1"); -const linked = (await create({ title: "Stale", tags: `gh:fixture/repo#${number}` }, { pmRoot })).item; +const linked = (await createImportMirror({ title: "Stale", tags: `gh:fixture/repo#${number}` }, { pmRoot })).item; const issue = { number, title: "New upstream title", body: null, state: "closed" as const, state_reason: "completed" as const, closed_at: "2026-02-02T00:00:00Z", labels: [{ name: "bug" }], user: { login: "fixture" }, assignee: null, milestone: null, created_at: "2026-01-01T00:00:00Z", updated_at: "2026-02-03T00:00:00Z", html_url: `https://github.com/fixture/repo/issues/${number}` }; const entry = (await prepareGithubImport(issue, "fixture/repo", parseImportOptions({ atomic: true }), "fixture", linked, undefined, diff --git a/test/generate-changelog.test.ts b/test/generate-changelog.test.ts index a8b4111a..121ce58b 100644 --- a/test/generate-changelog.test.ts +++ b/test/generate-changelog.test.ts @@ -78,3 +78,23 @@ test("canonical changelog fails closed when the tracker read fails", async () => fs.rmSync(root, { recursive: true, force: true }); } }); + +test("canonical changelog keeps released dependency notes beside pending PM work", async () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "pm-github-released-changelog-")); + try { + assert.equal(spawnSync("git", ["init", "-b", "main", root]).status, 0); + fs.writeFileSync(path.join(root, "package.json"), JSON.stringify({ version: "2026.11.12" })); + const released = "## 2026.11.12 - 2026-11-12\n\n### Dependencies\n\n- Bump fixture dependency ([#1](https://github.com/unbraind/pm-github/pull/1))\n"; + fs.writeFileSync(path.join(root, "CHANGELOG.md"), "# Changelog\n\n" + released); + assert.equal(spawnSync("git", ["add", "package.json", "CHANGELOG.md"], { cwd: root }).status, 0); + assert.equal(spawnSync("git", ["-c", "user.name=SteveBot", "-c", "user.email=1153461+unbraind@users.noreply.github.com", "commit", "-m", "chore(deps-dev): bump fixture dependency (#1)"], { cwd: root }).status, 0); + const pmRoot = path.join(root, ".agents/pm"), cli = path.resolve("node_modules/.bin/pm"); + assert.equal(spawnSync(cli, ["--pm-path", pmRoot, "init", "--defaults", "--agent-guidance", "skip"], { cwd: root }).status, 0); + await create({ title: "Pending whole-value ownership", status: "in_progress", tags: "changelog-unreleased" }, { cwd: root, pmRoot, noExtensions: true }); + assert.equal(await generateChangelog(root, []), 0); + const output = fs.readFileSync(path.join(root, "CHANGELOG.md"), "utf8"); + assert.ok(output.includes(released.split("### Dependencies")[1]!), "Released dependency notes survive when a PM item joins the window"); + assert.match(output, /## 2026\.11\.12 - 2026-11-12[\s\S]*Pending whole-value ownership/); + assert.equal(await generateChangelog(root, ["--check"]), 0); + } finally { fs.rmSync(root, { recursive: true, force: true }); } +}); diff --git a/test/github-url.test.ts b/test/github-url.test.ts new file mode 100644 index 00000000..401e4dcd --- /dev/null +++ b/test/github-url.test.ts @@ -0,0 +1,174 @@ +/** Markdown extraction and refresh aliases use real SDK trackers and the real HTTP transport. */ +import assert from "node:assert/strict"; +import test from "node:test"; +import { mkdtempSync, rmSync, readFileSync } from "node:fs"; +import { join, delimiter } from "node:path"; +import { tmpdir } from "node:os"; +import { init, create, get, comments, commitImportedItem, readSettings, getItemPath } from "@unbrained/pm-cli/sdk"; +import { createExtensionTestHarness } from "@unbrained/pm-cli/sdk/testing"; +import extension, { parseBlockerReferences, buildBlockerClearedComment, appendBlockerComments, type BlockersReport } from "../index.ts"; +import { githubUrlTokens, parseGithubIssueUrl } from "../github-url.ts"; +import { explicitGithubIdentities, importIdentityKeys } from "../import-fidelity.ts"; +import { runRefresh, selectRefreshTargets } from "../refresh.ts"; +import { withEnv, withMockGithub, jsonResponse } from "./helpers/mock-github-server.ts"; + +const harness = createExtensionTestHarness(extension, { capabilities: ["commands", "importers", "schema", "hooks", "preflight", "search"] }); +const first = "https://github.com/o/r/issues/1"; +const second = "https://github.com/o/r/issues/2"; +const wrappers: Array<[string, (url: string) => string]> = [ + ["bold", url => `**${url}**`], ["double underscore", url => `__${url}__`], + ["star", url => `*${url}*`], ["underscore", url => `_${url}_`], + ["backticks", url => "`" + url + "`"], ["autolink", url => `<${url}>`], + ["Markdown link", url => `[text](${url})`], ["parentheses", url => `(${url})`], + ["nested emphasis", url => `**_${url}_**`], ["double backticks", url => "``" + url + "``"], +]; + +/** Plain non-git trackers ensure extraction never depends on repository discovery. */ +async function tracker(run: (root: string) => Promise) { + const directory = mkdtempSync(join(tmpdir(), "github-markdown-")); + const root = join(directory, "tracker"); + try { + await init("fixture", { defaults: true, withPackages: false, agentGuidance: "skip" }, { pmRoot: root, cwd: directory, noExtensions: true }); + await withEnv({ PM_PATH: root, PM_AUTHOR: "fixture", PM_TELEMETRY_SOURCE_CONTEXT: "automation", GITHUB_TOKEN: "fixture-token", GH_TOKEN: undefined, + PATH: join(import.meta.dirname, "../node_modules/.bin") + delimiter + process.env.PATH }, () => run(root)); + } finally { rmSync(directory, { recursive: true, force: true }); } +} + +/** Revalidate a real blocked item: #1 is closed, every other queried issue is open. */ +async function blockers(root: string, description: string, annotate = false, tags = "") { + const item = (await create({ title: "Curated blocker", type: "Task", description, status: "blocked", tags }, { pmRoot: root })).item; + const file = getItemPath(root, "Task", item.id); + const before = readFileSync(file, "utf8"); + const queried: number[] = []; + const result = await withMockGithub((request, response, body) => { + assert.equal(request.url, "/graphql"); + const query = (JSON.parse(body) as { query: string }).query; + const repository: Record = {}; + for (const match of query.matchAll(/(i(\d+)_(\d+)): issue\(number:\s*(\d+)\)/g)) { + const number = Number(match[4]); queried.push(number); + repository[match[1]!] = { state: number === 1 ? "CLOSED" : "OPEN", stateReason: number === 1 ? "COMPLETED" : null, + closedAt: number === 1 ? "2025-01-01T00:00:00Z" : null, url: `https://github.com/o/r/issues/${number}` }; + repository[`p${match[2]}_${match[3]}`] = null; + } + jsonResponse(response, 200, { data: { r0: repository } }); + }, async () => { + const outcome = await (await harness).runCommand({ command: "github blockers", options: { repo: "o/r", comment: annotate }, pmRoot: root, global: { json: true } }); + assert.equal(outcome.handled, true, outcome.errorMessage ?? "The real blocker handler must succeed"); + return outcome.result as BlockersReport; + }); + return { item, result, queried, unchanged: () => readFileSync(file, "utf8") === before }; +} + +for (const [label, wrap] of wrappers) test(`Markdown wrapper retains every live blocker (${label})`, async () => tracker(async root => { + assert.equal(githubUrlTokens(wrap(second))[0]!.raw, second); + assert.equal(githubUrlTokens(wrap(second + "?q=(value)"))[0]!.raw, second + "?q=(value)", "Balanced URL data is not a Markdown delimiter"); + assert.equal(parseGithubIssueUrl(wrap(second)), undefined, "Structured identities never inherit prose unwrapping"); + const { result, queried, unchanged } = await blockers(root, `Blocked by ${first} and ${wrap(second + "!")}`); + assert.deepEqual(queried, [1, 2]); + assert.equal(result.items[0]!.references.find(ref => ref.number === 2)?.state, "open"); + assert.equal(result.items[0]!.verdict, "partially_cleared"); + assert.equal(result.counts.cleared, 0); + assert.equal(unchanged(), true); +})); + +for (const punctuation of ".,;:!?") test(`Markdown punctuation retains every live blocker (${punctuation})`, async () => tracker(async root => { + const { result, queried } = await blockers(root, `Blocked by ${first} and **${second}**${punctuation}`); + assert.deepEqual(queried, [1, 2]); + assert.equal(result.items[0]!.references.find(ref => ref.number === 2)?.state, "open"); + assert.equal(result.counts.cleared, 0); +})); + +test("Markdown exact mixed-reference reproduction never reports cleared", async () => tracker(async root => { + const { result, queried } = await blockers(root, "Blocked by https://github.com/o/r/issues/1 and **https://github.com/o/r/issues/2**", true); + assert.deepEqual(queried, [1, 2]); + assert.equal(result.items[0]!.references.find(ref => ref.number === 2)?.state, "open"); + assert.equal(result.items[0]!.verdict, "partially_cleared"); + assert.equal(result.comment!.added, 0); +})); + +test("Reference tags cannot hide a parsed open Markdown blocker", async () => tracker(async root => { + const { result, queried, unchanged } = await blockers(root, `Blocked by ${first} and **${second}**`, true, "gh:o/r#2"); + assert.deepEqual(queried, [1, 2]); + assert.equal(result.items[0]!.references.find(ref => ref.number === 2)?.state, "open"); + assert.equal(result.counts.cleared, 0); + assert.equal(result.comment!.added, 0); + assert.equal(unchanged(), true); +})); + +for (const url of ["https://github.com/o/r/issues/nope", "https://github.com/o/r/issues/2/extra", "https://github.com/o/r/issues/999999999999999999999", + "https://api.github.com/repos/o/r/issues/2", "https://example.invalid/opaque", "https://github.com/o/r/issues/nope?target=GH-2#3", + "https://github.com/o/r/issues/0", "https://github.com/o/r/issues/2*"]) { + test(`Unparsed URL remains visible and prevents clearance (${url})`, async () => tracker(async root => { + const { item, result, queried, unchanged } = await blockers(root, `Blocked by ${first} and ${url}`, true); + assert.deepEqual(queried, [1]); + const opaque = result.items[0]!.references.find(ref => ref.state === "unparsed"); + assert.ok(opaque); + assert.equal(opaque.raw, url); + assert.equal(opaque.number, undefined); + assert.equal(result.counts.cleared, 0); + assert.equal(result.items[0]!.verdict, "partially_cleared"); + assert.equal(result.comment!.added, 0); + assert.throws(() => buildBlockerClearedComment(opaque), /unparsed blocker reference/); + await assert.rejects(() => appendBlockerComments(root, [{ itemId: item.id, states: [opaque] }], false), /unparsed blocker reference/); + assert.equal((await comments(item.id, {}, { pmRoot: root })).comments.length, 0); + assert.equal(unchanged(), true); + assert.equal(parseBlockerReferences(url).length, 1, "A URL cannot leak shorter references from its path or query"); + })); +} + +/** Persist released URL aliases through the SDK importer adapter, retaining curated content. */ +async function linked(root: string, alias: string, number: number, kind: "issues" | "pull") { + const item = (await create({ title: "Curated alias", type: "Chore", description: "Curated description" }, { pmRoot: root })).item; + const url = `https://github.com/o/r/${kind}/${number}`; + await commitImportedItem({ pmRoot: root, id: item.id, itemPath: getItemPath(root, "Chore", item.id), settings: await readSettings(root), + document: { metadata: { ...item, [alias]: url, github_repo: "o/r", github_number: number }, body: "Curated body" }, + author: "fixture", message: "Released alias fixture", conflictWarningPrefix: "fixture" }); + const saved = (await get(item.id, {}, { pmRoot: root })).item; + assert.equal(saved[alias], url); + return saved; +} + +for (const alias of ["GitHubURL", "githuburl", "GITHUB_URL", "Github_Url", "github_URL"]) test(`Refresh excludes PRs in every explicit URL alias (${alias})`, async () => tracker(async root => { + const pr = await linked(root, alias, 7, "pull"); + await linked(root, "github_url", 9, "pull"); + await linked(root, alias, 8, "issues"); + assert.equal(explicitGithubIdentities(pr)[0]!.kind, "pull"); + assert.deepEqual(importIdentityKeys(pr), ["o/r#7"]); + const queried: number[] = []; + await withMockGithub((request, response, body) => { + assert.equal(request.url, "/graphql"); + const query = (JSON.parse(body) as { query: string }).query; + const repository: Record = {}; + for (const match of query.matchAll(/i(\d+): issue\(number:\s*(\d+)\)/g)) { + const number = Number(match[2]); queried.push(number); + repository[`i${number}`] = number === 7 ? null : { number, title: "Remote issue", body: "Remote body", state: "OPEN", stateReason: null, + createdAt: "2025-01-01T00:00:00Z", updatedAt: "2025-02-01T00:00:00Z", closedAt: null, url: `https://github.com/o/r/issues/${number}`, author: null, + labels: { nodes: [], pageInfo: { hasNextPage: false, endCursor: null } } }; + } + jsonResponse(response, 200, { data: { repository } }); + }, async () => { + const result = await runRefresh("o/r", root, { "dry-run": true }); + assert.deepEqual(queried, [8]); + assert.equal(result.selected, 1); + assert.equal(result.missing, 0); + }); + assert.equal((await get(pr.id!, {}, { pmRoot: root })).item.title, "Curated alias"); + // The alias contract is case-insensitive, with or without the underscore. + for (let bits = 0; bits < 512; bits++) for (const underscore of [false, true]) { + const letters = [..."githuburl"].map((character, index) => bits & (1 << index) ? character.toUpperCase() : character); + if (underscore) letters.splice(6, 0, "_"); + const field = letters.join(""); + const record = { id: pr.id!, title: pr.title, [field]: "https://github.com/o/r/pull/7" }; + assert.equal(explicitGithubIdentities(record)[0]!.kind, "pull"); + assert.deepEqual(selectRefreshTargets([record], "o/r"), []); + } +})); + +for (const path of ["8\\..\\7", "8/..\\7", "8\\../7", "8\\7", "8%5c..%5c7"]) test(`GitHub ownership rejects backslash paths before URL normalization (${path})`, () => { + for (const prefix of ["https://github.com/o/r/issues/", "https://www.github.com/o/r/pull/", "https://api.github.com/repos/o/r/issues/"]) { + assert.equal(parseGithubIssueUrl(prefix + path), undefined); + } + assert.equal(parseGithubIssueUrl("https://github.com/o/r/issues/7?query=\\example#fragment")?.number, 7); + assert.equal(parseGithubIssueUrl("https://github.com/o/r/pull/7/")?.number, 7); + assert.equal(parseGithubIssueUrl("https://github.com/o/r/issues/8/../7"), undefined); +}); diff --git a/test/handler-failures.test.ts b/test/handler-failures.test.ts index dacbbde4..a9aa5382 100644 --- a/test/handler-failures.test.ts +++ b/test/handler-failures.test.ts @@ -31,10 +31,11 @@ import os from "node:os"; import path from "node:path"; import { fileURLToPath } from "node:url"; +import { normalizeItemId } from "@unbrained/pm-cli/sdk"; import { createExtensionTestHarness, type ExtensionTestHarness } from "@unbrained/pm-cli/sdk/testing"; import type { SearchProviderQueryContext } from "@unbrained/pm-cli/sdk/authoring"; -import extension, { parseImportOptions, runImport, type GhIssue } from "../index.ts"; +import extension, { deriveAtomicItemId, parseProvenanceTag, parseImportOptions, runImport, type GhIssue } from "../index.ts"; import { projectItemTag } from "../projects.ts"; import { captureStderr, @@ -95,11 +96,14 @@ function freshTracker(): string { * through `pm close` with a factual reason. The resulting item still carries * the requested tag and status — only the mechanism changed. */ -function createLinkedItem(root: string, title: string, tag: string, status: string): string { +function createLinkedItem(root: string, title: string, tag: string, status: string, importerOwned = false): string { + const reference = parseProvenanceTag(tag)!; + const ownedId = importerOwned ? ["--id", deriveAtomicItemId(reference.repo, reference.number, "test-", normalizeItemId), + "--author", "pm-github", "--description", `GH issue #${reference.number}: https://github.com/${reference.repo}/issues/${reference.number}`] : []; const createStatus = status === "closed" ? "open" : status; const r = spawnSync( PM_BIN, - ["--path", root, "create", "task", title, "--status", createStatus, "--tags", tag], + ["--path", root, "create", "task", title, "--status", createStatus, "--tags", tag, ...ownedId], PM_SPAWN_OPTS, ); assert.strictEqual(r.status, 0, `pm create failed: ${r.error?.message ?? r.stderr}`); @@ -235,7 +239,7 @@ test("runSync --dry-run previews a divergence (pm open, GitHub closed → would const ext = await harnessPromise; const root = freshTracker(); try { - createLinkedItem(root, "Linked", "gh:a/b#5", "open"); + createLinkedItem(root, "Linked", "gh:a/b#5", "open", true); await withEnv({ GITHUB_TOKEN: "tok", GH_TOKEN: undefined }, async () => { await withMockGithub((_req, res) => { // Upstream issue is closed; pm item is open → desired "open" diverges. @@ -243,7 +247,7 @@ test("runSync --dry-run previews a divergence (pm open, GitHub closed → would }, async () => { const { result } = await ext.runCommand({ command: "github sync", - options: { repo: "a/b", "dry-run": true }, + options: { repo: "a/b", "allow-reference-tags": true, "dry-run": true }, pmRoot: root, global: { json: true }, }); @@ -262,7 +266,7 @@ test("runSync --apply PATCHes GitHub to match pm status", async () => { const ext = await harnessPromise; const root = freshTracker(); try { - createLinkedItem(root, "Linked", "gh:a/b#6", "closed"); + createLinkedItem(root, "Linked", "gh:a/b#6", "closed", true); await withEnv({ GITHUB_TOKEN: "tok", GH_TOKEN: undefined }, async () => { await withMockGithub((req, res) => { if (req.method === "GET") { @@ -273,7 +277,7 @@ test("runSync --apply PATCHes GitHub to match pm status", async () => { }, async (server) => { const { result } = await ext.runCommand({ command: "github sync", - options: { repo: "a/b" }, + options: { repo: "a/b", "allow-reference-tags": true }, pmRoot: root, global: { json: true }, }); @@ -293,14 +297,14 @@ test("runSync skips an issue already in the desired state", async () => { const ext = await harnessPromise; const root = freshTracker(); try { - createLinkedItem(root, "Linked", "gh:a/b#7", "open"); + createLinkedItem(root, "Linked", "gh:a/b#7", "open", true); await withEnv({ GITHUB_TOKEN: "tok", GH_TOKEN: undefined }, async () => { await withMockGithub((_req, res) => { jsonResponse(res, 200, { number: 7, state: "open" }); // already matches }, async (server) => { const { result } = await ext.runCommand({ command: "github sync", - options: { repo: "a/b" }, + options: { repo: "a/b", "allow-reference-tags": true }, pmRoot: root, global: { json: true }, }); @@ -319,7 +323,7 @@ test("runSync tolerates a 404 reading upstream state (skipped, not aborted)", as const ext = await harnessPromise; const root = freshTracker(); try { - createLinkedItem(root, "Linked", "gh:a/b#8", "open"); + createLinkedItem(root, "Linked", "gh:a/b#8", "open", true); await withEnv({ GITHUB_TOKEN: "tok", GH_TOKEN: undefined }, async () => { await withMockGithub((_req, res) => { // The linked issue was deleted upstream. @@ -327,7 +331,7 @@ test("runSync tolerates a 404 reading upstream state (skipped, not aborted)", as }, async (server) => { const { result } = await ext.runCommand({ command: "github sync", - options: { repo: "a/b" }, + options: { repo: "a/b", "allow-reference-tags": true }, pmRoot: root, global: { json: true }, }); @@ -471,7 +475,7 @@ test("search provider maps remote matches to local imported items and drops the context: searchContext({ query: "memory leak", repo: "a/b", - documents: [{ metadata: { id: "pm-1", title: "Linked", tags: ["gh:a/b#5"] }, body: "" }], + documents: [{ metadata: { id: "pm-1", title: "Linked", github_url: "https://github.com/a/b/issues/5", tags: ["gh:a/b#5"] }, body: "" }], }), }); const list = Array.isArray(hits) ? hits : (hits as { hits?: unknown[] }).hits; @@ -709,7 +713,7 @@ test("runSync rejects --ids naming an unknown pm item", async () => { const ext = await harnessPromise; const root = freshTracker(); try { - createLinkedItem(root, "Linked", "gh:a/b#5", "open"); + createLinkedItem(root, "Linked", "gh:a/b#5", "open", true); await withEnv({ GITHUB_TOKEN: "tok", GH_TOKEN: undefined }, async () => { await assert.rejects( ext.runCommand({ @@ -1123,7 +1127,7 @@ function statusForTag(root: string, tag: string): string | undefined { test("runImport closes a linked item whose upstream issue was closed", async () => { const root = freshTracker(); try { - createLinkedItem(root, "Linked", "gh:a/b#5", "open"); + createLinkedItem(root, "Linked", "gh:a/b#5", "open", true); await withEnv({ GITHUB_TOKEN: "tok", GH_TOKEN: undefined }, async () => { await withMockGithub((request, res) => { if (request.url === "/graphql") graphqlOk(res, { repository: { issue: { timelineItems: { nodes: [], pageInfo: { hasNextPage: false, endCursor: null } } } } }); @@ -1176,7 +1180,7 @@ test("runImport creates and closes an unlinked issue that is already closed upst test("runImport reopens a linked item whose upstream issue was reopened", async () => { const root = freshTracker(); try { - createLinkedItem(root, "Linked", "gh:a/b#6", "closed"); + createLinkedItem(root, "Linked", "gh:a/b#6", "closed", true); await withEnv({ GITHUB_TOKEN: "tok", GH_TOKEN: undefined }, async () => { await withMockGithub((_req, res) => { jsonResponse(res, 200, [upstreamIssue(6, "Linked", "open")]); diff --git a/test/helpers/import-mirror.ts b/test/helpers/import-mirror.ts new file mode 100644 index 00000000..7e1f2dac --- /dev/null +++ b/test/helpers/import-mirror.ts @@ -0,0 +1,17 @@ +/** Explicit importer-owned SDK fixtures; a reference tag alone no longer creates a mirror. */ +import { create, normalizeItemId, parseTags, readSettings } from "@unbrained/pm-cli/sdk"; +import { deriveAtomicItemId, parseProvenanceTag } from "../../index.ts"; + +/** Seed package creation provenance and a stable source id for full-mirroring fixtures. */ +export async function createImportMirror(options: NonNullable[0]>, context: Parameters[1]) { + const reference = parseTags(options.tags ?? "").flatMap(tag => { + const parsed = parseProvenanceTag(tag); + return parsed ? [parsed] : []; + })[0]; + if (!reference) throw new Error("Mirror fixture requires a source reference"); + const settings = await readSettings(context!.pmRoot!); + const source = `GH issue #${reference.number}: https://github.com/${reference.repo}/issues/${reference.number}`; + const description = options.description?.startsWith(source) ? options.description + : source + (options.description ? ` · ${options.description}` : ""); + return create({ ...options, author: "pm-github", description, id: options.id ?? deriveAtomicItemId(reference.repo, reference.number, settings.id_prefix, normalizeItemId) }, context); +} diff --git a/test/helpers/import-preview.ts b/test/helpers/import-preview.ts new file mode 100644 index 00000000..e23c33cd --- /dev/null +++ b/test/helpers/import-preview.ts @@ -0,0 +1,30 @@ +/** Dry-run fixtures read the same real tracker destinations that apply validates. */ +import assert from "node:assert/strict"; +import { mkdtempSync, rmSync, readdirSync, readFileSync, existsSync } from "node:fs"; +import { join, delimiter } from "node:path"; +import { tmpdir } from "node:os"; +import { init, create, readSettings, mutateItem, listAllComplete } from "@unbrained/pm-cli/sdk"; +import { withEnv } from "./mock-github-server.ts"; + +/** Seed a native linked owner; previews may read it but must not change its documents or audit history. */ +export async function withImportPreview(run: (root: string) => Promise): Promise { + const root = mkdtempSync(join(tmpdir(), "github-preview-")); + try { + await init("fixture", { defaults: true, withPackages: false, agentGuidance: "skip" }, { pmRoot: root, cwd: root, noExtensions: true }); + const item = (await create({ title: "Original local title", type: "Chore", tags: "keep" }, { pmRoot: root, noExtensions: true })).item; + await mutateItem({ pmRoot: root, settings: await readSettings(root), id: item.id, author: "fixture", op: "update", mutate: document => { + document.metadata.github_url = "https://github.com/acme/widgets/issues/2"; + return { changedFields: ["github_url"] }; + } }); + // Finish native fixture initialization before the no-write baseline, including derived indexes. + await listAllComplete({ includeBody: true }, { pmRoot: root, noExtensions: true }); + const bytes = () => readdirSync(root, { recursive: true, withFileTypes: true }).filter(entry => entry.isFile()).map(entry => + [join(entry.parentPath, entry.name).slice(root.length), readFileSync(join(entry.parentPath, entry.name))]).sort(); + const before = bytes(); + const result = await withEnv({ PM_PATH: root, PM_AUTHOR: "fixture", PM_TELEMETRY_SOURCE_CONTEXT: "automation", + PATH: join(import.meta.dirname, "../../node_modules/.bin") + delimiter + process.env.PATH }, () => run(root)); + assert.deepEqual(bytes(), before, "Preview changed tracker documents or native history"); + assert.equal(existsSync(join(root, "transactions")), false, "Preview must not publish a transaction or recovery plan"); + return result; + } finally { rmSync(root, { recursive: true, force: true }); } +} diff --git a/test/identity-inventory.test.ts b/test/identity-inventory.test.ts new file mode 100644 index 00000000..c180747b --- /dev/null +++ b/test/identity-inventory.test.ts @@ -0,0 +1,75 @@ +/** Syntax-derived documentation anchors stay reproducible when implementation lines move. */ +import assert from "node:assert/strict"; +import test from "node:test"; +import { spawnSync } from "node:child_process"; +import { cpSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync, mkdirSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { renderIdentityInventory } from "../scripts/identity-inventory.ts"; + +const root = join(import.meta.dirname, ".."), path = "docs/identity-ownership-inventory.md"; + +test("ownership inventory matches every current syntax and auxiliary anchor", () => { + const current = readFileSync(join(root, path), "utf8"); + assert.equal(renderIdentityInventory(root, current), current); + const legacyRule = "A leading native GitHub issue or GH issue/PR declaration retains legacy ownership without a link-only marker or whole-description match; direct provenance still protects against copied provider markers."; + assert.ok(current.includes(legacyRule)); + const staleRule = current.replace(legacyRule, "A dedicated legacy github_link_only marker retains its complete native declaration."); + assert.notEqual(staleRule, current); + assert.equal(renderIdentityInventory(root, staleRule), current); + for (const name of ["buildProjectPullPlan", "buildProjectImportPlan"]) { + const row = current.split("\n").find(line => line.startsWith(`| \`projects.ts\` / \`${name}\` |`))!; + assert.match(row, /indexSelectedByIssue\(\)/); + } +}); + +test("ownership inventory refuses stale anchors rather than copying their line numbers", () => { + const current = readFileSync(join(root, path), "utf8"), stale = current.replace(/`projects\.ts:\d+`/g, "`projects.ts:1`"); + assert.notEqual(stale, current); + assert.equal(renderIdentityInventory(root, stale), current); +}); + +/** Exercise the actual source scanner and CLI against disposable source and ledger files. */ +function inventoryCopy(run: (scratch: string, current: string) => void) { + const scratch = mkdtempSync(join(tmpdir(), "github-inventory-")); + try { + for (const file of readdirSync(root).filter(file => file.endsWith(".ts"))) cpSync(join(root, file), join(scratch, file)); + for (const folder of ["scripts", ".github/workflows"]) cpSync(join(root, folder), join(scratch, folder), { recursive: true }); + const current = readFileSync(join(root, path), "utf8"); + mkdirSync(join(scratch, "docs")); writeFileSync(join(scratch, path), current); + run(scratch, current); + } finally { rmSync(scratch, { recursive: true, force: true }); } +} + +test("ownership inventory re-derives all project anchors after a source-line shift", () => inventoryCopy((scratch, current) => { + writeFileSync(join(scratch, "projects.ts"), "// Fixture source-line shift.\n" + readFileSync(join(root, "projects.ts"), "utf8")); + const expected = current.replace(/`projects\.ts:(\d+)`/g, (_match, line: string) => `\`projects.ts:${Number(line) + 1}\``); + assert.equal(renderIdentityInventory(scratch, current), expected); +})); + +test("inventory CLI writes exact anchors and refuses stale ledgers", () => inventoryCopy((scratch, current) => { + const script = join(root, "scripts/identity-inventory.ts"); + writeFileSync(join(scratch, path), current.replace(/`projects\.ts:\d+`/g, "`projects.ts:1`")); + const stale = spawnSync(process.execPath, [script, "--check"], { cwd: scratch, encoding: "utf8" }); + assert.equal(stale.status, 1); assert.match(stale.stderr, /inventory is stale/); + const write = spawnSync(process.execPath, [script, "--write"], { cwd: scratch, encoding: "utf8" }); + assert.equal(write.status, 0, write.stderr); assert.equal(readFileSync(join(scratch, path), "utf8"), current); + assert.equal(renderIdentityInventory(scratch), current); + const check = spawnSync(process.execPath, [script, "--check"], { cwd: scratch, encoding: "utf8" }); + assert.equal(check.status, 0, check.stderr); assert.match(check.stdout, /all syntax-derived and auxiliary anchors verified/); +})); + +test("inventory refuses incomplete templates and unclassified source or auxiliary identity paths", () => inventoryCopy((scratch, current) => { + assert.throws(() => renderIdentityInventory(scratch, "invalid template"), /template or source corpus is incomplete/); + assert.throws(() => renderIdentityInventory(scratch, current.slice(0, current.indexOf("## Scripts and workflow audit"))), /template or source corpus is incomplete/); + mkdirSync(join(scratch, "empty")); + assert.throws(() => renderIdentityInventory(join(scratch, "empty"), current), /template or source corpus is incomplete/); + writeFileSync(join(scratch, "unclassified.ts"), "export function newOwner(item: { external_ref: string }) { return item.external_ref; }\n"); + assert.throws(() => renderIdentityInventory(scratch, current), /Classify ownership groups: unclassified.ts\/newOwner/); + rmSync(join(scratch, "unclassified.ts")); + const first = current.split("\n").find(row => row.startsWith("| `closeout.ts` /"))!; + const cells = first.split(" | "); cells[cells.length - 1] = " |"; + assert.throws(() => renderIdentityInventory(scratch, current.replace(first, cells.join(" | "))), /Classify new ownership group/); + writeFileSync(join(scratch, "scripts/new-owner.ts"), "const field = 'github_url';\n"); + assert.throws(() => renderIdentityInventory(scratch, current), /Classify new auxiliary identity use in scripts\/new-owner.ts/); +})); diff --git a/test/identity.test.ts b/test/identity.test.ts index f12b3734..47582a20 100644 --- a/test/identity.test.ts +++ b/test/identity.test.ts @@ -71,22 +71,23 @@ test("copied prose, structured multi-provider owners, exact refs and bounded gra await item(root, { description: `GitHub issue: ${url.replace("/1", "/5")}` }); await item(root, { description: "See https://github.com/fixture/repo/pull/6" }); await item(root, { description: "GitHub issue https://example.invalid/issue/1" }); - const conflict = await item(root, { github_url: "https://github.com/fixture/repo/issues/8", tags: ["gh:fixture/repo#9"], source_owner: "fixture-provider" }); + const conflict = await item(root, { github_url: "https://github.com/fixture/repo/issues/8", external_ref: "gh:fixture/repo#9", tags: ["gh:fixture/repo#9"], source_owner: "fixture-provider" }); for (const entry of readdirSync(root, { recursive: true, withFileTypes: true }).filter(entry => entry.isFile())) chmodSync(join(entry.parentPath, entry.name), 0o444); const before = snapshot(root); const report = await exploreGithubIdentities(root); assert.equal(report.scanned, 11); assert.equal(report.collisions, 4); - assert.equal(report.mentions, 2); + assert.ok(report.edges.some(edge => edge.item === conflict && edge.identity === "gh:fixture/repo#9" && edge.strength === "claim")); + assert.equal(report.mentions, 5); assert.equal(report.nodes.find(node => node.id === conflict)?.native_owner, "fixture-provider"); assert.equal(report.nodes.find(node => node.id === copied)?.ownership, "local"); assert.equal((await exploreGithubIdentities(root, { identity: "gh:fixture/repo#2" })).nodes.find(node => node.id === provider)?.ownership, "provider"); assert.equal((await exploreGithubIdentities(root, { identity: "gh:fixture/repo#3" })).nodes.find(node => node.id === mixed)?.ownership, "mixed"); assert.equal(report.nodes.find(node => node.id === owner)?.ownership, "github"); const first = await exploreGithubIdentities(root, { limit: 1 }); - assert.equal(first.total, 5); + assert.equal(first.total, 6); assert.equal(first.next, 1); - assert.equal((await exploreGithubIdentities(root, { limit: 1, offset: 4 })).next, null); + assert.equal((await exploreGithubIdentities(root, { limit: 1, offset: 5 })).next, null); assert.deepEqual((await exploreGithubIdentities(root, { identity: "absent" })).edges, []); assert.deepEqual(snapshot(root), before); for (const options of [{ limit: 0 }, { limit: 101 }, { limit: 1.1 }, { offset: -1 }, { offset: NaN }]) await assert.rejects(exploreGithubIdentities(root, options), code("invalid_options")); @@ -135,7 +136,7 @@ test("refuse contradictory owners, source metadata, opaque providers and changed const b = await item(root, { tags: ["gh:fixture/repo#2"] }); await assert.rejects(planGithubIdentityRepair(root, "gh:fixture/repo#2", a, b), code("ambiguous_identity")); const external = await item(root, { external_ref: key }); - await assert.rejects(planGithubIdentityRepair(root, key, owner, external), code("ambiguous_identity")); + await assert.rejects(planGithubIdentityRepair(root, key, owner, external), code("unsupported_repair")); const before = snapshot(root); await item(root, { github_url: url }); const multiple = snapshot(root); @@ -144,9 +145,9 @@ test("refuse contradictory owners, source metadata, opaque providers and changed assert.notDeepEqual(before, multiple); })); -for (const fields of [{ github_url: url }, { github_link_only: false }, { github_number: 1 }, { source_owner: "fixture-provider" }, { tags: [key, "gh:fixture/repo#2"] }]) { +for (const fields of [{ github_url: url }, { github_link_only: false }, { github_number: 1 }, { source_owner: "fixture-provider" }]) { test(`source-owned duplicates require manual review (${Object.keys(fields).join()})`, async () => tracker(async root => { - const owner = await item(root, { description: `GitHub issue ${url}` }); + const owner = await item(root, { github_link_only: true, description: `GitHub issue ${url}` }); const detach = await item(root, { tags: [key], ...fields }); const before = snapshot(root); await assert.rejects(planGithubIdentityRepair(root, key, owner, detach), code("ambiguous_identity")); @@ -161,7 +162,7 @@ test("registered command uses compact host TOON/JSON, read-only graph, repair pr const run = async (options: Record) => (await harness.runCommand({ command: "github identity", pmRoot: root, args: [], options })).result; assert.equal((await run({}) as { scanned: number }).scanned, 2); const report = await run({ limit: "1", offset: "0", identity: key }) as { collisions: number }; - assert.equal(report.collisions, 1); + assert.equal(report.collisions, 0); const proposed = await run({ identity: key, owner, detach }) as { approval: string }; assert.match(proposed.approval, /^[a-f0-9]{64}$/); await assert.rejects(run({ owner }), code("invalid_options")); @@ -298,6 +299,9 @@ test("an owner with a conflicting github_number cannot authorize detaching anoth const owner = await item(root, { github_url: url, github_number: 2 }); const detach = await item(root, { tags: [key] }); const before = snapshot(root); + const report = await exploreGithubIdentities(root); + assert.equal(report.collisions, 2, "read-only diagnostics must still expose both contradictory claims"); + assert.deepEqual(report.edges.filter(edge => edge.item === owner && edge.strength === "claim").map(edge => edge.identity).sort(), [key, "gh:fixture/repo#2"]); await assert.rejects(planGithubIdentityRepair(root, key, owner, detach), code("ambiguous_identity")); await assert.rejects(applyGithubIdentityRepair(root, { identity: key, owner, detach, approval: "a".repeat(64), author: "fixture" }), code("ambiguous_identity")); assert.deepEqual(snapshot(root), before); diff --git a/test/import-conflict-report.test.ts b/test/import-conflict-report.test.ts new file mode 100644 index 00000000..b3558ddb --- /dev/null +++ b/test/import-conflict-report.test.ts @@ -0,0 +1,32 @@ +/** Execute the workflow's conflict projection; source/local text never enters the PR body. */ +import assert from "node:assert/strict"; +import test from "node:test"; +import { readFileSync, mkdtempSync, writeFileSync, rmSync } from "node:fs"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; +import { spawnSync } from "node:child_process"; + +test("sync PR includes title/state conflicts from the pre-apply plan without quoting titles", () => { + const workflow = readFileSync(new URL("../.github/workflows/pm-github-sync.yml", import.meta.url), "utf8"); + const commands = workflow.split("\n").filter(line => line.trim().startsWith("jq -r ") && line.includes("pm-github-import-conflicts.md")); + assert.ok(commands.length >= 1); + const command = commands.map(line => line.trim()).join("\n"); + const directory = mkdtempSync(join(tmpdir(), "import-conflicts-")); + try { + writeFileSync(join(directory, "pm-github-import.json"), JSON.stringify({ plan: [ + { issue: 1, identity: { title_conflict: true }, title: "Do not publish this text" }, + { issue: 2, identity: { conflict: { local: "open", github: "closed" } } }, + { issue: 3, identity: { title_conflict: true, conflict: {} } }, + { issue: 4, identity: {} }, + ], identity_conflicts: [{ code: "ambiguous_legacy_github_identity", item: "Do not publish this item text", candidates: ["Do not publish source references"] }] })); + const result = spawnSync("bash", ["-euc", command], { env: { ...process.env, RUNNER_TEMP: directory }, encoding: "utf8" }); + assert.equal(result.status, 0, result.stderr); + const output = readFileSync(join(directory, "pm-github-import-conflicts.md"), "utf8"); + assert.match(output, /Issue #1: title differs/); + assert.match(output, /Issue #2: state differs/); + assert.match(output, /Issue #3: title differs.*state differs/); + assert.doesNotMatch(output, /Issue #4|Do not publish/); + assert.match(output, /ambiguous_legacy_github_identity: 1 item\(s\).*preserved/); + assert.match(workflow, /cat "\$\{RUNNER_TEMP\}\/pm-github-import-conflicts.md"/); + } finally { rmSync(directory, { recursive: true, force: true }); } +}); diff --git a/test/import-errors.test.ts b/test/import-errors.test.ts index fd3ff5b5..fe306a60 100644 --- a/test/import-errors.test.ts +++ b/test/import-errors.test.ts @@ -1,3 +1,4 @@ +import { createImportMirror } from "./helpers/import-mirror.ts"; import assert from "node:assert/strict"; import { spawnSync } from "node:child_process"; import fs from "node:fs"; @@ -74,18 +75,24 @@ test("ungated import tolerates unavailable comments and still writes the issue", assert.equal(readPmItems(root)[0].body, "Original body"); }); -test("import reports real mutation failures after a linked local item disappears", async t => { +test("import refuses before mutation after a linked local item disappears", async t => { const root = tracker(t); - const created = JSON.parse(pm(root, ["create", "task", "Linked", "--tags", "gh:acme/widgets#1", "--json"])) as { id: string }; + const created = (await createImportMirror({ title: "Linked", type: "Task", tags: "gh:acme/widgets#1" }, { pmRoot: root })).item; const snapshot = readPmItems(root); pm(root, ["delete", created.id, "--message", "Fixture concurrent deletion"]); + const history = fs.readFileSync(path.join(root, "history", `${created.id}.jsonl`), "utf8"); await withMockGithub((_req, res) => jsonResponse(res, 200, [issue()]), async () => { const { stderr } = await captureStderr(async () => { - await assert.rejects(runImport("acme/widgets", root, parseImportOptions({}), { resolveToken: () => undefined, readItems: () => snapshot }), /Imported 0 issue\(s\); 1 failed/); + await assert.rejects(runImport("acme/widgets", root, parseImportOptions({}), { resolveToken: () => undefined, readItems: () => snapshot }), error => { + assert.equal((error as { code: string }).code, "item_not_found"); + assert.match(String(error), /intended item and history; restore them before retrying/); + return true; + }); }); - assert.match(stderr.join("\n"), /update failed/); + assert.doesNotMatch(stderr.join("\n"), /update failed|create failed/); }); assert.deepEqual(readPmItems(root), []); + assert.equal(fs.readFileSync(path.join(root, "history", `${created.id}.jsonl`), "utf8"), history); }); test("import rejects an unknown item type and allows a corrected retry", async t => { @@ -103,7 +110,7 @@ test("import rejects an unknown item type and allows a corrected retry", async t test("import reconciles a canceled item when GitHub reports completion", async t => { const root = tracker(t); - pm(root, ["create", "task", "Canceled", "--status", "canceled", "--tags", "gh:acme/widgets#1"]); + await createImportMirror({ title: "Canceled", type: "Task", status: "canceled", tags: "gh:acme/widgets#1" }, { pmRoot: root }); await withMockGithub((_req, res) => jsonResponse(res, 200, [issue({ state: "closed" })]), async () => { const result = await runImport("acme/widgets", root, parseImportOptions({}), { resolveToken: () => undefined }); assert.ok("updated" in result); diff --git a/test/import-fidelity.test.ts b/test/import-fidelity.test.ts index 5cd82ba2..7356102a 100644 --- a/test/import-fidelity.test.ts +++ b/test/import-fidelity.test.ts @@ -1,4 +1,5 @@ /** Recorded consumer regressions exercised through real temporary PM trackers. */ +import { createImportMirror } from "./helpers/import-mirror.ts"; import assert from "node:assert/strict"; import { spawnSync } from "node:child_process"; import { mkdtempSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs"; @@ -71,7 +72,7 @@ for (const atomic of [false, true]) { const issue = recorded(name); const checkRetry = fixtureIndex < 2; if (checkRetry) { - await create({ title: "Legacy source snapshot", type: "Issue", status: "closed", closeReason: "Legacy generic closure", + await createImportMirror({ title: "Legacy source snapshot", type: "Issue", status: "closed", closeReason: "Legacy generic closure", tags: `gh:${issue.html_url.split("/").slice(3, 5).join("/")}#${issue.number}` }, { pmRoot }); const beforePreview = (await listItems(pmRoot)); const preview = await importing(pmRoot, issue, atomic, true); @@ -113,7 +114,7 @@ for (const atomic of [false, true]) { test(`pm-csv-72: ${atomic ? "atomic" : "plain"} conflicting GitHub-owned record refreshes state and lifecycle together`, async () => tracker(async (_directory, pmRoot) => { const issue = recorded("pm-csv-72"); - const item = (await create({ title: issue.title, type: "Issue", status: "open", tags: "gh:unbraind/pm-csv#72" }, { pmRoot })).item; + const item = (await createImportMirror({ title: issue.title, type: "Issue", status: "open", tags: "gh:unbraind/pm-csv#72" }, { pmRoot })).item; const preview = await importing(pmRoot, issue, atomic, true); assert.deepEqual(((preview.plan as Array<{ identity: { conflict: unknown } }>)[0]!).identity.conflict, { local: "open", github: "closed", rule: "github-source-wins" }); @@ -184,17 +185,18 @@ for (const atomic of [false, true]) { assert.equal((await listItems(pmRoot)).length, 1); const actual = (await get(owner.id, {}, { pmRoot })).item; assert.equal(actual.status, "closed"); - assert.equal(actual.created_at, issue.created_at); - assert.equal(actual.closed_at, issue.closed_at); - assert.equal(actual.completed_at, issue.closed_at); + assert.equal(actual.created_at, owner.created_at); + assert.equal(actual.closed_at, owner.closed_at); + assert.equal(actual.completed_at, owner.completed_at); + assert.equal(actual.title, owner.title); assert.equal(actual.release, "stale-release", "a closure without a matching calendar tag preserves the existing release"); assert.equal(actual.github_link_only, false); - assert.match(actual.close_reason!, /^Closed on GitHub/); + assert.equal(actual.close_reason, owner.close_reason); })); test(`explicit native GitHub issue declaration reuses its owner without changing local delivery (${atomic})`, async () => tracker(async (_directory, pmRoot) => { const issue = recorded("pm-linear-134"); - const owner = await identityItem(pmRoot, { description: `GitHub issue${atomic ? ":" : ""} ${issue.html_url}. Local delivery evidence.`, release: "2026.10.8" }); + const owner = await identityItem(pmRoot, { github_link_only: true, description: `GitHub issue${atomic ? ":" : ""} ${issue.html_url}`, release: "2026.10.8" }); const preview = await importing(pmRoot, issue, atomic, true); const plan = (preview.plan as Array<{ action: string; identity: { via: string } }>)[0]!; assert.equal(plan.identity.via, "github-declaration"); @@ -259,7 +261,7 @@ test("release fixture: two actual tags place closures before, between and after assert.doesNotMatch(unreleased, /Window [12]/); })); -test("identity accepts nested GitHub links, provider back-links, exact URLs and refuses competing owners", () => { +test("identity keeps nested GitHub links as references, accepts provider back-links and refuses competing owners", () => { const issue = recorded("pm-linear-134"); for (const provider of ["linear", "jira", "ado"]) { const value = matchImportedIssue([{ id: "owner", [`${provider}_url`]: "https://linear.app/fixture/issue/ENG-134" }], issue); @@ -271,9 +273,9 @@ test("identity accepts nested GitHub links, provider back-links, exact URLs and assert.equal(matchImportedIssue([{ id: "owner", description: "[linear] linear_id=fixture linear_url=https://linear.app/fixture/issue/ENG-134" }], issue).item?.id, "owner"); assert.equal(matchImportedIssue([{ id: "owner", description: "Jira EX-134: https://linear.app/fixture/issue/ENG-134" }], issue).item?.id, "owner"); assert.equal(matchImportedIssue([{ id: "owner", body: "ado_id=134 ado_url=https://linear.app/fixture/issue/ENG-134" }], issue).item?.id, "owner"); - assert.equal(matchImportedIssue([{ id: "owner", ado_id: 134, provenance: { url: issue.html_url } }], issue).item?.id, "owner"); + assert.equal(matchImportedIssue([{ id: "owner", ado_id: 134, provenance: { url: issue.html_url } }], issue).item, undefined); assert.equal(matchImportedIssue([{ id: "owner", github_url: `${issue.html_url}?view=all#issuecomment-1` }], issue).item?.id, "owner"); - assert.equal(matchImportedIssue([{ id: "owner", external_links: [{ url: issue.html_url }] }], issue).item?.id, "owner"); + assert.equal(matchImportedIssue([{ id: "owner", external_links: [{ url: issue.html_url }] }], issue).item, undefined); assert.equal(matchImportedIssue([{ id: "owner", GitHubURL: issue.html_url }], issue).item?.id, "owner"); assert.equal(matchImportedIssue([{ id: "owner", provenance: { provider: "pm-linear", url: "https://linear.app/fixture/issue/ENG-134" } }], issue).item?.id, "owner"); assert.equal(matchImportedIssue([{ id: "owner", provider: "pm-jira", external_url: "https://bugs.example.test/EX-1" }], { ...issue, body: "Mirrored from https://bugs.example.test/EX-1" }).item?.id, "owner"); @@ -289,7 +291,7 @@ test("identity accepts nested GitHub links, provider back-links, exact URLs and assert.equal(matchImportedIssue([{ id: "unrelated", links: `${issue.html_url}0` }], issue).item, undefined); - assert.throws(() => matchImportedIssue([{ id: "one", links: issue.html_url }, { id: "two", links: issue.html_url }], issue), /multiple existing owners/); + assert.equal(matchImportedIssue([{ id: "one", links: issue.html_url }, { id: "two", links: issue.html_url }], issue).item, undefined); }); test("citation opt-out excludes prose while retaining structured identity matching", () => { @@ -427,14 +429,14 @@ test("closure and identity edge cases retain exact evidence rather than inventin assert.equal(buildImportFidelity({ ...issue, state: "open", state_reason: null }, [], []).github_state_reason, "unspecified"); const tagged = { id: "tagged", tags: ["gh:unbraind/pm-beads#85"] }; assert.equal(matchImportedIssue([tagged], issue, tagged).linkOnly, false); - assert.equal(matchImportedIssue([{ id: "tagged", github_link_only: true }], issue, tagged).linkOnly, true); - assert.equal(matchImportedIssue([{ id: "provider", github_url: issue.html_url, tags: ["jira:EX-1"] }], issue).via, "provider-link"); + assert.equal(matchImportedIssue([{ id: "tagged", github_link_only: true, github_url: issue.html_url }], issue, tagged).linkOnly, true); + assert.equal(matchImportedIssue([{ id: "provider", github_url: issue.html_url, tags: ["jira:EX-1"] }], issue).via, "github-url"); assert.equal(matchImportedIssue([{ links: [issue.html_url] }, { id: "other", github_url: undefined, ado_url: undefined }], { ...issue, body: null }).item, undefined); assert.deepEqual(readImportReleases(tmpdir()), []); }); test("finalization appends history, is a true no-op on retry, restores files on append failure and releases locks", async () => tracker(async (_directory, pmRoot) => { - const item = (await create({ title: "Finalize fixture", type: "Task" }, { pmRoot })).item; + const item = (await createImportMirror({ title: "Finalize fixture", type: "Task", tags: "gh:unbraind/pm-beads#85" }, { pmRoot })).item; const fidelity = buildImportFidelity({ ...recorded("pm-beads-85"), state: "open", closed_at: null }, [], []); const shortId = item.id.slice((await readSettings(pmRoot)).id_prefix.length); await persistImportFidelity(pmRoot, shortId, fidelity, false); @@ -461,7 +463,7 @@ test("finalization appends history, is a true no-op on retry, restores files on /** Build a direct import plan from the same recorded lifecycle used by HTTP replays. */ function atomicEntry(issue: GhIssue): PreparedGithubImport { return { issueNumber: issue.number, title: issue.title, itemType: "Issue", status: "closed", - description: `Recorded ${issue.html_url}`, body: issue.body ?? "", tags: [`gh:unbraind/pm-beads#${issue.number}`], + description: `GH issue #${issue.number}: ${issue.html_url}`, body: issue.body ?? "", tags: [`gh:unbraind/pm-beads#${issue.number}`], comments: [], syncAnnotations: false, closedAt: issue.closed_at ?? undefined, fidelity: buildImportFidelity(issue, [], []) }; } @@ -561,7 +563,7 @@ test("tag-date ordering ignores non-release and undated blob tags and is stable })); test("legacy records without a source closure date retain honest evidence without inventing a closure date", async () => tracker(async (_directory, pmRoot) => { - const item = (await create({ title: "Legacy fixture", type: "Task", status: "closed", closeReason: "Fixture closure" }, { pmRoot })).item; + const item = (await createImportMirror({ title: "Legacy fixture", type: "Task", status: "closed", closeReason: "Fixture closure", tags: "gh:unbraind/pm-beads#85" }, { pmRoot })).item; const fidelity = buildImportFidelity({ ...recorded("pm-beads-85"), closed_at: null }, [], []); await persistImportFidelity(pmRoot, item.id, fidelity, false); const actual = (await get(item.id, {}, { pmRoot })).item; @@ -572,7 +574,7 @@ test("legacy records without a source closure date retain honest evidence withou test("a legacy unknown closure cannot put native closure before source creation", async () => tracker(async (_directory, pmRoot) => { - const item = (await create({ title: "Legacy chronology fixture", type: "Task", status: "closed", closeReason: "Fixture closure" }, { pmRoot })).item; + const item = (await createImportMirror({ title: "Legacy chronology fixture", type: "Task", status: "closed", closeReason: "Fixture closure", tags: "gh:unbraind/pm-beads#85" }, { pmRoot })).item; const future = new Date(Date.now() + 86400000).toISOString(); const fidelity = buildImportFidelity({ ...recorded("pm-beads-85"), created_at: future, updated_at: future, closed_at: null }, [], []); await assert.rejects(persistImportFidelity(pmRoot, item.id, fidelity, false), /needs a source closure date/); @@ -639,7 +641,7 @@ for (const atomic of [false, true]) { for (const copied of ["body", "label"]) { test(`legacy GitHub ownership survives copied provider ${copied} (${atomic})`, async () => tracker(async (_directory, pmRoot) => { const issue = { ...recorded("pm-csv-72"), title: "Upstream revised title" }; - const created = (await create({ title: "Old imported title", type: "Issue", status: "open", + const created = (await createImportMirror({ title: "Old imported title", type: "Issue", status: "open", tags: "gh:unbraind/pm-csv#72", description: `GH issue #72: ${issue.html_url}` }, { pmRoot })).item; await commitImportedItem({ pmRoot, id: created.id, itemPath: getItemPath(pmRoot, "Issue", created.id), document: { metadata: { ...created, ...(copied === "label" ? { tags: [...created.tags!, "linear:documentation"] } : {}) }, @@ -661,14 +663,14 @@ for (const atomic of [false, true]) { } test(`provider-only descriptor preserves external ownership without a locator (${atomic})`, async () => tracker(async (_directory, pmRoot) => { const issue = { ...recorded("pm-csv-72"), labels: [{ name: "fresh" }] }; - const owner = await identityItem(pmRoot, { provider: "pm-jira", tags: ["gh:unbraind/pm-csv#72", "jira:EX-1", " ONE,TWO "], release: "external-release" }); + const owner = await identityItem(pmRoot, { provider: "pm-jira", github_url: issue.html_url, tags: ["gh:unbraind/pm-csv#72", "jira:EX-1", " ONE,TWO "], release: "external-release" }); assert.equal(owner.external_url, undefined); assert.equal(owner.github_link_only, undefined); for (let attempt = 0; attempt < 2; attempt++) { await importing(pmRoot, issue, atomic); const actual = (await get(owner.id, {}, { pmRoot })).item; for (const key of ["title", "status", "created_at", "closed_at", "completed_at", "release", "body"] as const) assert.equal(actual[key], owner[key]); - assert.deepEqual(new Set(actual.tags), new Set([...owner.tags!, "fresh"])); + assert.deepEqual(new Set(actual.tags), new Set(owner.tags)); assert.equal(actual.github_link_only, true); assert.equal((await listItems(pmRoot)).length, 1); } @@ -684,7 +686,8 @@ for (const atomic of [false, true]) { await importing(pmRoot, issue, atomic); const actual = (await get(owner.id, {}, { pmRoot })).item; for (const key of ["title", "status", "created_at", "release"] as const) assert.equal(actual[key], owner[key]); - assert.equal(actual.github_link_only, true); + assert.equal(actual.github_link_only, false); + assert.equal((await listItems(pmRoot)).length, 2, "A reference tag never links its external owner"); })); } } diff --git a/test/import-gate.test.ts b/test/import-gate.test.ts index f29c2838..4258a418 100644 --- a/test/import-gate.test.ts +++ b/test/import-gate.test.ts @@ -19,7 +19,7 @@ import os from "node:os"; import path from "node:path"; import { fileURLToPath } from "node:url"; -import { create, get } from "@unbrained/pm-cli/sdk"; +import { init, create, get, mutateItem, readSettings } from "@unbrained/pm-cli/sdk"; import { createExtensionTestHarness, type ExtensionTestHarness } from "@unbrained/pm-cli/sdk/testing"; import extension, { @@ -111,10 +111,10 @@ function planEntry(issueNumber: number, tags: string[], match?: PmItem): Prepare test("recovered journals cannot substitute for a complete persisted provenance corpus", () => { const prepared = [planEntry(1, ["gh:acme/widgets#1"])]; assert.throws(() => verifyImportedProvenance([], prepared, "acme/widgets"), CommandError); - assert.equal(verifyImportedProvenance([{ id: "item-a", tags: ["gh:acme/widgets#1"] }], prepared, "acme/widgets"), 1); + assert.equal(verifyImportedProvenance([{ id: "item-a", github_url: "https://github.com/acme/widgets/issues/1", tags: ["gh:acme/widgets#1"] }], prepared, "acme/widgets"), 1); assert.throws(() => verifyImportedProvenance([ - { id: "item-a", tags: ["gh:acme/widgets#1"] }, { id: "item-b", tags: ["gh:acme/widgets#1"] }, - ], prepared, "acme/widgets"), CommandError); + { id: "item-a", github_url: "https://github.com/acme/widgets/issues/1", tags: ["gh:acme/widgets#1"] }, { id: "item-b", github_url: "https://github.com/acme/widgets/issues/1", tags: ["gh:acme/widgets#1"] }, + ], prepared, "acme/widgets"), { code: "github_import_owner_conflict" }); }); test("gated comment imports scan persisted values without importing REST user metadata", async () => { @@ -309,14 +309,17 @@ test("parseImportOptions reads the gate flag without changing defaults", () => { }); test("gated previews refuse and redact unsafe issue titles and labels in both import modes", async () => { + const directory = fs.mkdtempSync(path.join(os.tmpdir(), "pm-github-preview-")); + const root = path.join(directory, "tracker"); + await init("fixture", { defaults: true, withPackages: false, agentGuidance: "skip" }, { pmRoot: root, cwd: directory, noExtensions: true }); const original = console.error; const messages: string[] = []; console.error = (...values: unknown[]) => { messages.push(values.join(" ")); }; try { for (const atomic of [false, true]) { const malicious = { ...issue(1, GH_TOKEN, "Public body"), labels: [{ name: GH_TOKEN }] }; - await assert.rejects(runImport("acme/widgets", "unused-preview", parseImportOptions({ gate: true, dryRun: true, atomic }), { - resolveToken: () => undefined, fetchIssues: async () => [malicious], readItems: () => [], + await assert.rejects(runImport("acme/widgets", root, parseImportOptions({ gate: true, dryRun: true, atomic }), { + resolveToken: () => undefined, fetchIssues: async () => [malicious], }), (error: unknown) => { assert.match((error as Error).message, /github-token-classic/); assert.ok(!(error as Error).message.includes(GH_TOKEN)); @@ -326,6 +329,7 @@ test("gated previews refuse and redact unsafe issue titles and labels in both im assert.ok(!messages.join("\n").includes(GH_TOKEN)); } finally { console.error = original; + fs.rmSync(directory, { recursive: true, force: true }); } }); @@ -381,19 +385,22 @@ test("verifyImportPlanCompleteness passes a reconciling plan and fails every dri test("verifyImportIdempotency fails duplicate provenance, untagged creates, and conflicting targets", () => { const existing = [ - { id: "item-a", tags: ["gh:acme/widgets#1"] }, - { id: "item-b", tags: ["gh:acme/widgets#1"] }, // duplicate provenance + { id: "item-a", github_url: "https://github.com/acme/widgets/issues/1", tags: ["gh:acme/widgets#1"] }, + { id: "item-b", github_url: "https://github.com/acme/widgets/issues/1", tags: ["gh:acme/widgets#1"] }, // duplicate provenance ]; assert.throws( () => - verifyImportIdempotency(existing, [planEntry(2, ["gh:acme/widgets#2"])], "acme/widgets"), + verifyImportIdempotency(existing, [planEntry(1, ["gh:acme/widgets#1"])], "acme/widgets"), (err: unknown) => { - assert.ok(err instanceof CommandError); - assert.match(err.message, /1 duplicate provenance tag\(s\)/); + assert.equal((err as { code: string }).code, "github_import_owner_conflict"); + assert.match(String(err), /multiple existing owners/); return true; }, ); + // Unrelated conflicts are outside this receipt's incoming issue scope. + assert.equal(verifyImportIdempotency(existing, [planEntry(2, ["gh:acme/widgets#2"])], "acme/widgets").new_entries, 1); + // A new entry that would be born without its provenance tag. assert.throws( () => @@ -409,10 +416,10 @@ test("verifyImportIdempotency fails duplicate provenance, untagged creates, and assert.throws( () => verifyImportIdempotency( - [{ id: "item-a", tags: ["gh:acme/widgets#1"] }], + [{ id: "item-a", github_url: "https://github.com/acme/widgets/issues/1", tags: ["gh:acme/widgets#1"] }], [ - planEntry(1, ["gh:acme/widgets#1"], { id: "item-a", tags: ["gh:acme/widgets#1"] }), - planEntry(2, ["gh:acme/widgets#2"], { id: "item-a", tags: ["gh:acme/widgets#1"] }), + planEntry(1, ["gh:acme/widgets#1"], { id: "item-a", github_url: "https://github.com/acme/widgets/issues/1", tags: ["gh:acme/widgets#1"] }), + planEntry(2, ["gh:acme/widgets#2"], { id: "item-a", github_url: "https://github.com/acme/widgets/issues/1", tags: ["gh:acme/widgets#1"] }), ], "acme/widgets", ), @@ -425,9 +432,9 @@ test("verifyImportIdempotency fails duplicate provenance, untagged creates, and // A clean plan is proven a no-op: every entry matched by provenance or born tagged. const receipts = verifyImportIdempotency( - [{ id: "item-a", tags: ["gh:acme/widgets#1"] }], + [{ id: "item-a", github_url: "https://github.com/acme/widgets/issues/1", tags: ["gh:acme/widgets#1"] }], [ - planEntry(1, ["gh:acme/widgets#1"], { id: "item-a", tags: ["gh:acme/widgets#1"] }), + planEntry(1, ["gh:acme/widgets#1"], { id: "item-a", github_url: "https://github.com/acme/widgets/issues/1", tags: ["gh:acme/widgets#1"] }), planEntry(2, ["gh:acme/widgets#2"]), ], "acme/widgets", @@ -436,23 +443,33 @@ test("verifyImportIdempotency fails duplicate provenance, untagged creates, and }); test("a gated import refuses to write when the corpus cannot yield an idempotent plan", async () => { + const directory = fs.mkdtempSync(path.join(os.tmpdir(), "pm-github-duplicate-preview-")); + const root = path.join(directory, "tracker"); + await init("fixture", { defaults: true, withPackages: false, agentGuidance: "skip" }, { pmRoot: root, cwd: directory, noExtensions: true }); + const ids: string[] = []; + for (const title of ["Duplicate A", "Duplicate B"]) { + const item = (await create({ title, type: "Task", tags: "gh:acme/widgets#1" }, { pmRoot: root })).item; + ids.push(item.id); + await mutateItem({ pmRoot: root, id: item.id, settings: await readSettings(root), author: "fixture", op: "update", mutate: document => { + document.metadata.github_url = "https://github.com/acme/widgets/issues/1"; + return { changedFields: ["github_url"] }; + } }); + } + const before = await Promise.all(ids.map(id => get(id, {}, { pmRoot: root }))); const issues = [issue(1, "One", "b1"), issue(2, "Two", "b2")]; const messages: string[] = []; const originalError = console.error; console.error = (...values: unknown[]) => messages.push(values.join(" ")); try { await assert.rejects( - runImport("acme/widgets", "/unused-gated-workspace", parseImportOptions({ gate: true, dryRun: true }), { + runImport("acme/widgets", root, parseImportOptions({ gate: true, dryRun: true }), { resolveToken: () => undefined, fetchIssues: async () => issues, - readItems: () => [ - { id: "dup-a", tags: ["gh:acme/widgets#1"] }, - { id: "dup-b", tags: ["gh:acme/widgets#1"] }, - ], }), (err: unknown) => { - assert.ok(err instanceof CommandError); - assert.match(err.message, /import plan is not idempotent/); + assert.equal((err as { code: string }).code, "github_import_owner_conflict"); + assert.match(String(err), /import plan is not idempotent/); + for (const id of ids) assert.ok(String(err).includes(id)); return true; }, ); @@ -460,8 +477,10 @@ test("a gated import refuses to write when the corpus cannot yield an idempotent !messages.some((message) => /pm github gate: plan verified/.test(message)), "the idempotency refusal happens before the plan is reported as verified", ); + assert.deepEqual((await Promise.all(ids.map(id => get(id, {}, { pmRoot: root })))).map(result => result.item_hash), before.map(result => result.item_hash)); } finally { console.error = originalError; + fs.rmSync(directory, { recursive: true, force: true }); } }); @@ -485,19 +504,14 @@ test("adversarial: token in a body, token in a comment, email, and host path pre await withMockGithub(githubHandler(malicious, comments), async () => { const ext = await harnessPromise; - // Workflow step: dry-run plan (no gate failures yet — nothing is written). - const plan = await ext.runImporter({ - importer: "github", - args: ["acme/widgets"], - options: { atomic: true, dryRun: true, "with-comments": true }, - pmRoot: root, - }); - const planResult = plan.result as Record; - assert.strictEqual(planResult.dryRun, true); - assert.strictEqual(planResult.wouldImport, 4); + // Atomic previews inspect the entire future recovery payload, even without --gate. + await assert.rejects(ext.runImporter({ + importer: "github", args: ["acme/widgets"], + options: { atomic: true, dryRun: true, "with-comments": true }, pmRoot: root, + }), { code: "github_import_preflight_failed" }); + assert.equal(git(["status", "--porcelain", "--", ".agents/pm"]).stdout.trim(), ""); - // Workflow step: the gated import. It verifies the plan, writes, scans the - // proposed tracker change, and MUST exit non-zero on the findings. + // The gated import refuses before staging or SDK writes and cannot publish. await assert.rejects( ext.runImporter({ importer: "github", diff --git a/test/import-lock.test.ts b/test/import-lock.test.ts index 9ee812a2..61c56639 100644 --- a/test/import-lock.test.ts +++ b/test/import-lock.test.ts @@ -603,40 +603,25 @@ function replaceMethod(target: T, key: K, i return () => { target[key] = original; }; } -test("a failed lock write still releases the partial file when close and unlink fail", async () => { +test("a failed atomic lock publication leaves no visible partial lease", async () => { const root = mkdtempSync(join(tmpdir(), "pm-github-lock-write-")); - const originalWrite = fs.writeFileSync; - const originalClose = fs.closeSync; - let descriptor: number | undefined; - let closeCalls = 0; - let unlinkCalls = 0; - const closeMock = replaceMethod(fs, "closeSync", () => { - closeCalls++; - throw Object.assign(new Error("close failed"), { code: "EIO" }); - }); - const unlinkMock = replaceMethod(fs, "unlinkSync", () => { - unlinkCalls++; - throw Object.assign(new Error("unlink failed"), { code: "EIO" }); - }); - const writeMock = replaceMethod(fs, "writeFileSync", ((target: fs.PathOrFileDescriptor, data: string) => { - if (typeof target === "number") { - descriptor = target; - throw Object.assign(new Error("write failed"), { code: "EIO" }); - } - return originalWrite(target, data); - }) as typeof fs.writeFileSync); + let partialVisible = false; + let injected = false; try { - const acq = await acquireImportLock(root, "pm-write", { waitMs: 50 }); + const acq = await acquireImportLock(root, "pm-write", { waitMs: 50, onStaged: async ({ file, staged }) => { + await fs.promises.writeFile(staged, "{"); + injected = true; + if (existsSync(file)) { + try { JSON.parse(readFileSync(file, "utf8")); } catch { partialVisible = true; } + } + throw Object.assign(new Error("Injected partial lease publication failure"), { code: "EIO" }); + } }); + if (acq.status === "acquired") acq.lock.release(); + assert.equal(injected, true); + assert.equal(partialVisible, false, "A lease reader observed an empty or partial reserved final file"); assert.equal(acq.status, "degraded"); - assert.ok(closeCalls > 0); - assert.ok(unlinkCalls > 0); - } finally { - writeMock(); - closeMock(); - unlinkMock(); - if (descriptor !== undefined) originalClose(descriptor); - rmSync(root, { recursive: true, force: true }); - } + assert.equal(existsSync(importCommentSyncLockPath(root, "pm-write")), false); + } finally { rmSync(root, { recursive: true, force: true }); } }); test("a vanished breaker sidecar is ignored and a vanished lock is treated as already gone", async () => { diff --git a/test/import-ownership.test.ts b/test/import-ownership.test.ts new file mode 100644 index 00000000..0c148733 --- /dev/null +++ b/test/import-ownership.test.ts @@ -0,0 +1,336 @@ +/** Every importer write path uses real SDK storage, including relinks between planning and commit. */ +import assert from "node:assert/strict"; +import test from "node:test"; +import { mkdtempSync, rmSync, readFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join, delimiter } from "node:path"; +import { spawnSync } from "node:child_process"; +import { + init, create, get, readSettings, mutateItem, getItemPath, getHistoryPath, normalizeItemId, + listAllComplete, listAllItemMetadata, WorkspaceTransactionInterruptedError, runWithActiveExtensions, setActiveExtensionHooks, + readHistoryEntries, verifyHistoryEntries, acquireLockLease, PmCliError, +} from "@unbrained/pm-cli/sdk"; +import { + runImport, parseImportOptions, deriveAtomicItemId, prepareGithubImport, importGithubAtomic, + readGithubImportPlan, resumeGithubImport, listUnfinishedGithubImports, syncGithubCommentsToAnnotations, + linkImportedDependencies, type GhIssue, type PmItem, +} from "../index.ts"; +import { persistImportFidelity, buildImportFidelity } from "../import-fidelity.ts"; +import { mutateGithubOwnedItem, withGithubOwnershipLock, withGithubImportOwners, withGithubOwnershipWork } from "../import-ownership.ts"; +import { runRefresh } from "../refresh.ts"; +import { runCloseout, type CloseoutApi } from "../closeout.ts"; +import { withEnv, captureStderr, withMockGithub, jsonResponse } from "./helpers/mock-github-server.ts"; + +const repo = "o/r"; +const issue: GhIssue = { number: 7, title: "Original source", body: "Original source body", state: "open", labels: [], user: null, + assignee: null, milestone: null, created_at: "2025-01-01T00:00:00Z", updated_at: "2025-02-01T00:00:00Z", html_url: "https://github.com/o/r/issues/7" }; +const changed = { ...issue, title: "Changed source", body: "Changed source body", updated_at: "2025-03-01T00:00:00Z" }; + +/** Isolate native files/history and pin every child CLI; some cases also exercise the Git-backed gate. */ +async function tracker(run: (root: string) => Promise, git = false) { + const directory = mkdtempSync(join(tmpdir(), "github-owner-")); + const root = join(directory, "tracker"); + try { + if (git) assert.equal(spawnSync("git", ["init", "-q"], { cwd: directory }).status, 0); + await init("fixture", { defaults: true, withPackages: false, agentGuidance: "skip" }, { pmRoot: root, cwd: directory, noExtensions: true }); + await withEnv({ PM_PATH: root, PM_AUTHOR: "fixture", PM_TELEMETRY_SOURCE_CONTEXT: "automation", GITHUB_TOKEN: "fixture-token", GH_TOKEN: undefined, + PATH: join(import.meta.dirname, "../node_modules/.bin") + delimiter + process.env.PATH }, () => run(root)); + } finally { rmSync(directory, { recursive: true, force: true }); } +} + +/** Run the real import core; only the external GitHub source is supplied. */ +async function sync(root: string, source: GhIssue = issue, options: Record = {}) { + return captureStderr(() => runImport(repo, root, parseImportOptions(options), { + resolveToken: () => undefined, fetchIssues: async () => [source], fetchIssueTimeline: async () => [], + })); +} + +/** Read both authoritative streams so refusals cannot conceal metadata or history writes. */ +async function bytes(root: string, id: string) { + const item = (await get(id, {}, { pmRoot: root })).item; + return [readFileSync(getItemPath(root, item.type!, id), "utf8"), readFileSync(getHistoryPath(root, id), "utf8")]; +} + +/** An independent SDK request relinks the live item under its normal item/history lock. */ +async function relink(root: string, id: string, owner = "other/project", number = 8, alias = "github_url", urlOnly = false) { + await runWithActiveExtensions({ path: root, noExtensions: true }, async () => mutateItem({ pmRoot: root, id, settings: await readSettings(root), + author: "fixture", op: "update", message: "Explicit relink", mutate: document => { + for (const field of Object.keys(document.metadata)) if (/^github_?url$/i.test(field)) delete document.metadata[field]; + document.metadata.github_repo = owner; document.metadata.github_number = number; + if (alias !== "number_only") document.metadata[alias] = `https://github.com/${owner}/issues/${number}`; + if (urlOnly) { delete document.metadata.github_repo; delete document.metadata.github_number; } + return { changedFields: ["github_url", "github_repo", "github_number", ...(alias !== "number_only" ? [alias] : [])] }; + } })); + const current = await get(id, {}, { pmRoot: root }); + const history = verifyHistoryEntries(await readHistoryEntries(getHistoryPath(root, id), id)); + assert.equal(history.ok, true, history.errors.join("; ")); + assert.equal(history.latest_after_hash, current.item_hash); + return current.item; +} + +/** The original plan sees its legitimate owner; later callers must check the live target again. */ +async function prepared(root: string, item: PmItem) { + return (await prepareGithubImport(changed, repo, parseImportOptions({}), undefined, item, undefined, + { items: [item], releases: [], timeline: async () => [] }))!; +} + +/** Refusals identify both normalized owners and the operator's recovery step. */ +function conflict(error: unknown) { + assert.equal((error as { code: string }).code, "github_import_owner_conflict"); + assert.match(String(error), /o\/r#7/); + assert.match(String(error), /other\/project#8|curated\/unowned/); + assert.match(String(error), /Restore the intended GitHub provenance/); + return true; +} + +for (const atomic of [false, true]) test(`Ownership deterministic upsert refuses a relinked ID (atomic=${atomic})`, async () => tracker(async root => { + await sync(root, issue, { atomic: true }); + const id = deriveAtomicItemId(repo, 7, (await readSettings(root)).id_prefix, normalizeItemId); + await relink(root, id, "other/project", 8, "github_url", true); + const before = await bytes(root, id); + let refusal: unknown; + try { await sync(root, changed, { atomic, gate: true }); } catch (error: unknown) { refusal = error; } + assert.deepEqual(await bytes(root, id), before); + assert.equal((await listAllComplete({}, { pmRoot: root })).items.length, 1); + conflict(refusal); +}, true)); + +for (const referenced of [false, true]) test(`Ownership curated deterministic-ID collision refuses before any batch write (reference=${referenced})`, async () => tracker(async root => { + const id = deriveAtomicItemId(repo, 7, (await readSettings(root)).id_prefix, normalizeItemId); + await create({ id, title: "Curated Chore", type: "Chore", body: "Curated body", tags: referenced ? "keep,gh:o/r#7" : "keep" }, { pmRoot: root }); + const before = await bytes(root, id); + let refusal: unknown; + try { await captureStderr(() => runImport(repo, root, parseImportOptions({ atomic: true }), { + resolveToken: () => undefined, fetchIssues: async () => [{ ...changed, number: 1, html_url: "https://github.com/o/r/issues/1" }, changed], fetchIssueTimeline: async () => [], + })); } catch (error: unknown) { refusal = error; } + assert.deepEqual(await bytes(root, id), before); + assert.equal((await listAllComplete({}, { pmRoot: root })).items.length, 1); + conflict(refusal); +})); + +for (const alias of ["number_only", "github_url", "GitHubURL", "external_links"]) for (const atomic of [false, true]) { + test(`Ownership selected URL target ${alias === "external_links" ? "ignores reference-only links" : "rechecks its live owner"} (${alias}, atomic=${atomic})`, async () => tracker(async root => { + const item = (await create({ title: "Curated URL owner", type: "Chore", body: "Curated body" }, { pmRoot: root })).item; + await relink(root, item.id, repo, 7, alias, alias !== "number_only"); + let before: string[] = []; + const importing = () => captureStderr(() => runImport(repo, root, parseImportOptions({ atomic }), { + resolveToken: () => undefined, fetchIssues: async () => [changed], fetchIssueTimeline: async () => { + // Keep all explicit URL aliases consistent while transferring authority. + const current = await relink(root, item.id, "other/project", 8, alias, alias !== "number_only"); + if (alias === "number_only") assert.equal(current.github_number, 8); + else assert.equal(current[alias], "https://github.com/other/project/issues/8"); + before = await bytes(root, item.id); return []; + }, + })); + if (alias === "external_links") { + await importing(); + assert.equal((await listAllComplete({}, { pmRoot: root })).items.length, 2); + } else await assert.rejects(importing, conflict); + assert.deepEqual(await bytes(root, item.id), before); + })); +} + +test("Ownership locked atomic upsert refuses an occupant arriving after preflight", async () => tracker(async root => { + const entry = (await prepareGithubImport(changed, repo, parseImportOptions({}), undefined, undefined, undefined, + { items: [], releases: [], timeline: async () => [] }))!; + const id = deriveAtomicItemId(repo, 7, (await readSettings(root)).id_prefix, normalizeItemId); + let before: string[] = []; + await assert.rejects(() => importGithubAtomic(root, repo, [entry], { onTransition: async context => { + if (context.transition === "prepared") { + await runWithActiveExtensions({ path: root, noExtensions: true }, () => create({ id, title: "Peer Chore", type: "Chore", body: "Peer body" }, { pmRoot: root })); + await relink(root, id); before = await bytes(root, id); + } + } }), conflict); + assert.deepEqual(await bytes(root, id), before); +})); + +test("Ownership importer ID retains ownership without a generated source description", async () => tracker(async root => { + const entry = (await prepareGithubImport(changed, repo, parseImportOptions({}), undefined, undefined, undefined, + { items: [], releases: [], timeline: async () => [] }))!; + const result = await importGithubAtomic(root, repo, [{ ...entry, description: "Description is content, not identity" }]); + assert.equal((await listAllComplete({}, { pmRoot: root })).items.length, 1); + assert.equal((await get(result.itemIds.get(7)!, {}, { pmRoot: root })).item.github_url, changed.html_url); + assert.deepEqual(await listUnfinishedGithubImports(root), []); +})); + +test("Ownership native SDK revision fence refuses a peer edit after the owner read", async () => tracker(async root => { + await sync(root, issue, { atomic: true }); + const item = (await listAllComplete({}, { pmRoot: root })).items[0]!; + const original = await get(item.id, {}, { pmRoot: root }); + await mutateItem({ pmRoot: root, id: item.id, settings: await readSettings(root), author: "fixture", op: "update", message: "Peer content edit", + mutate: document => { document.metadata.title = "Peer title"; return { changedFields: ["title"] }; } }); + const before = await bytes(root, item.id); + await assert.rejects(() => mutateGithubOwnedItem(root, { id: item.id, repo, number: 7 }, "update", { title: changed.title }, original.item_hash), /revision|hash|changed/i); + assert.deepEqual(await bytes(root, item.id), before); +})); + +test("Ownership refresh refuses a target relinked during its GitHub read", async () => tracker(async root => { + await sync(root, issue, { atomic: true }); + const id = deriveAtomicItemId(repo, 7, (await readSettings(root)).id_prefix, normalizeItemId); + let before: string[] = []; + await withMockGithub(async (_request, response) => { + await relink(root, id); before = await bytes(root, id); + jsonResponse(response, 200, { data: { repository: { i7: { number: 7, title: changed.title, body: changed.body, state: "OPEN", stateReason: null, + createdAt: issue.created_at, updatedAt: changed.updated_at, closedAt: null, url: issue.html_url, author: null, + labels: { nodes: [], pageInfo: { hasNextPage: false, endCursor: null } } } } } }); + }, () => assert.rejects(() => runRefresh(repo, root, {}), conflict)); + assert.deepEqual(await bytes(root, id), before); +})); + +test("Ownership recovery refuses a recorded target that was subsequently relinked", async () => tracker(async root => { + await sync(root, issue, { atomic: true }); + const item = (await listAllComplete({ includeBody: true }, { pmRoot: root })).items[0]!; + const entry = await prepared(root, item); + await assert.rejects(() => importGithubAtomic(root, repo, [entry], { onTransition: context => { + if (context.transition === "prepared") throw new WorkspaceTransactionInterruptedError("Fixture interruption"); + } })); + const journal = (await listUnfinishedGithubImports(root))[0]!; + const record = (await readGithubImportPlan(root, journal.transactionId))!; + await relink(root, item.id); + const before = await bytes(root, item.id); + await assert.rejects(() => resumeGithubImport(root, record), conflict); + assert.deepEqual(await bytes(root, item.id), before); +})); + +test("Ownership source finalizer refuses a different current owner inside its item lock", async () => tracker(async root => { + await sync(root, issue, { atomic: true }); + const item = (await listAllComplete({}, { pmRoot: root })).items[0]!; + await relink(root, item.id); + const before = await bytes(root, item.id); + await assert.rejects(() => persistImportFidelity(root, item.id, buildImportFidelity(changed, [], []), false), conflict); + assert.deepEqual(await bytes(root, item.id), before); +})); + +test("Ownership annotations refuse a different current owner", async () => tracker(async root => { + await sync(root, issue, { atomic: true }); + const item = (await listAllComplete({}, { pmRoot: root })).items[0]!; + await relink(root, item.id); + const before = await bytes(root, item.id); + await assert.rejects(() => syncGithubCommentsToAnnotations(item.id, [{ id: 1, body: "Incoming source note", created_at: issue.updated_at, user: null }], root, 7, + { id: item.id, repo, number: 7 }), conflict); + assert.deepEqual(await bytes(root, item.id), before); +})); + +test("Ownership dependency writes recheck their selected source", async () => tracker(async root => { + await sync(root, issue, { atomic: true }); + await sync(root, { ...issue, number: 9, html_url: "https://github.com/o/r/issues/9" }, { atomic: true }); + const items = (await listAllComplete({}, { pmRoot: root })).items; + const source = items.find(item => item.github_number === 7)!; + let before: string[] = []; + let reads = 0; + const result = await linkImportedDependencies(repo, [{ ...issue, body: "Blocked by #9" }], root, { + listItemMetadata: async pmRoot => { + const snapshot = await listAllItemMetadata(pmRoot); + if (reads++ === 0) { await relink(root, source.id); before = await bytes(root, source.id); } + return snapshot; + }, + }); + assert.equal(result.linked, 0); + assert.match(result.failures.join(""), /other\/project#8/); + assert.deepEqual(await bytes(root, source.id), before); +})); + +/** Supply only external PR evidence; selection, snapshot fences and closure are the real SDK operations. */ +function deliveryApi(id: string): CloseoutApi { + const sha = "a".repeat(40); + return { base: "https://api.github.com", next: () => undefined, get: async endpoint => ({ body: JSON.stringify( + endpoint.includes("/pulls/") ? endpoint.includes("/commits?") ? [] : { + number: 1, commits: 0, merged: true, merged_at: "2027-01-01T00:00:00Z", merge_commit_sha: "b".repeat(40), html_url: `https://github.com/${repo}/pull/1`, + body: `Closes https://github.com/${repo}/blob/main/.agents/pm/issues/${id}.toon`, head: { sha }, 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: sha, status: "completed", conclusion: "success", app: { id: 1 } }] } : []), + }) }; +} + +for (const owner of ["other/project", repo]) test(`Ownership closeout refuses a relinked issue despite passing delivery evidence (${owner})`, async () => tracker(async root => { + await sync(root, issue, { atomic: true }); + const item = (await listAllComplete({}, { pmRoot: root })).items[0]!; + const beforeRelink = await runCloseout(root, repo, { pr: "1" }, deliveryApi(item.id)); + assert.equal(beforeRelink.plan[0]!.action, "close", "Passing evidence must otherwise select this item"); + await relink(root, item.id, owner); + const before = await bytes(root, item.id); + const result = await runCloseout(root, repo, { pr: "1", apply: true }, deliveryApi(item.id)); + assert.equal(result.closed, 0); + assert.equal(result.plan[0]!.code, "github_import_owner_conflict"); + assert.ok(result.plan[0]!.reason.includes(`${owner}#8`)); + assert.deepEqual(await bytes(root, item.id), before); +})); + +for (const atomic of [false, true]) test(`Ownership matching-owner control stays idempotent (atomic=${atomic})`, async () => tracker(async root => { + await sync(root, issue, { atomic }); + const item = (await listAllComplete({}, { pmRoot: root })).items[0]!; + const before = await bytes(root, item.id); + await sync(root, issue, { atomic }); + assert.deepEqual(await bytes(root, item.id), before); + await sync(root, changed, { atomic }); + const all = (await listAllComplete({}, { pmRoot: root })).items; + assert.equal(all.length, 1); assert.equal(all[0]!.id, item.id); assert.equal(all[0]!.title, changed.title); +})); + +test("Ownership matching-owner legacy control retains its generated-source marker", async () => tracker(async root => { + const id = deriveAtomicItemId(repo, 7, (await readSettings(root)).id_prefix, normalizeItemId); + await create({ id, author: "pm-github", title: issue.title, type: "Issue", body: issue.body!, description: `GH issue #7: ${issue.html_url}`, tags: "gh:o/r#7" }, { pmRoot: root }); + await sync(root, issue, { atomic: true }); + assert.equal((await listAllComplete({}, { pmRoot: root })).items.length, 1); + assert.equal((await get(id, {}, { pmRoot: root })).item.github_number, 7); + const before = await bytes(root, id); + await sync(root, issue, { atomic: true }); + assert.deepEqual(await bytes(root, id), before); +})); + +test("Ownership guard preserves ambient mutation guards", async () => tracker(async root => { + await sync(root, issue, { atomic: true }); + const item = (await listAllComplete({ includeBody: true }, { pmRoot: root })).items[0]!; + const entry = await prepared(root, item); + const before = await bytes(root, item.id); + await runWithActiveExtensions({ path: root, noExtensions: true }, async () => { + setActiveExtensionHooks({ beforeCommand: [], afterCommand: [], onRead: [], onWrite: [], onIndex: [], beforeMutation: [{ layer: "project", name: "fixture-policy", run: () => ({ allow: false, code: "fixture_denied", remediation: "Fixture refusal" }) }] }); + await assert.rejects(() => importGithubAtomic(root, repo, [entry]), /fixture_denied|fixture-policy/); + }); + assert.deepEqual(await bytes(root, item.id), before); +})); + +/** Detached work keeps the AsyncLocalStorage stores of a finished scope; it must take a fresh lease and a fresh owner check. */ +async function leaseHeld(root: string): Promise { + try { + const probe = await acquireLockLease(root, "github-ownership", 60, "pm-github:test-probe", false, false, 0); + await probe.release(); + return false; + } catch (error: unknown) { + if (error instanceof PmCliError) return true; + throw error; + } +} + +test("Ownership lease: detached work after the scope ends takes a fresh lease instead of the released one", async () => tracker(async root => { + let resume!: () => void; + const gate = new Promise(resolve => { resume = resolve; }); + let detached!: Promise; + let nestedHeld: boolean | undefined; + await withGithubOwnershipLock(root, async () => { + // Must allow: nested work inside the live scope reuses the held lease. + nestedHeld = await withGithubOwnershipLock(root, () => leaseHeld(root)); + detached = gate.then(() => withGithubOwnershipLock(root, () => leaseHeld(root))); + }); + assert.equal(nestedHeld, true, "nested work inside the live scope runs under the held lease"); + assert.equal(await leaseHeld(root), false, "the outer lease is released when its scope ends"); + resume(); + assert.equal(await detached, true, "detached work acquires its own lease instead of reusing the released one"); +})); + +test("Ownership owners: detached work after the scope ends reads current ownership again", async () => tracker(async root => { + const target = { id: "pm-detached1", repo, number: 41, allowCreate: true }; + const work: { corpusReads?: number } = {}; + let resume!: () => void; + const gate = new Promise(resolve => { resume = resolve; }); + let detached!: Promise; + await withGithubOwnershipWork(work, () => withGithubImportOwners(root, [target], async () => { + // Must allow: a nested write for the same target inside the live scope reuses the installed guard. + await withGithubImportOwners(root, [target], async () => {}); + detached = gate.then(() => withGithubImportOwners(root, [target], async () => {})); + })); + assert.equal(work.corpusReads, 1, "the nested write inside the live scope reuses the outer ownership read"); + resume(); + await detached; + assert.equal(work.corpusReads, 2, "detached work re-reads ownership instead of reusing the closed scope"); +})); diff --git a/test/import-projection.test.ts b/test/import-projection.test.ts index d8f26764..d444866b 100644 --- a/test/import-projection.test.ts +++ b/test/import-projection.test.ts @@ -1,4 +1,5 @@ /** Canonical comparisons must agree with real SDK writers and packed CLI consumers. */ +import { createImportMirror } from "./helpers/import-mirror.ts"; import assert from "node:assert/strict"; import { spawnSync } from "node:child_process"; import { mkdtempSync, mkdirSync, readFileSync, readdirSync, rmSync, symlinkSync, writeFileSync } from "node:fs"; @@ -6,8 +7,8 @@ import { tmpdir } from "node:os"; import { join, relative } from "node:path"; import test from "node:test"; import { decode } from "@toon-format/toon"; -import { certifyCompleteListResult, commitImportedItem, create, get, getItemPath, listAllComplete, parseTags, toImportTags, decodePmTableRows, type PmTableCellEncoding, type PmEncodedTableRows, readSettings, schemaAddStatus, schemaAddType } from "@unbrained/pm-cli/sdk"; -import { decodeCompletePmItems, parseImportOptions, runImport, githubLabelsToTags, planGithubLabelMigration, type GhIssue } from "../index.ts"; +import { normalizeItemId, certifyCompleteListResult, commitImportedItem, create, get, getItemPath, getHistoryPath, listAllComplete, mutateItem, parseTags, toImportTags, decodePmTableRows, readHistoryEntries, verifyHistoryEntries, type PmTableCellEncoding, type PmEncodedTableRows, readSettings, schemaAddStatus, schemaAddType } from "@unbrained/pm-cli/sdk"; +import { deriveAtomicItemId, decodeCompletePmItems, parseImportOptions, runImport, githubLabelsToTags, planGithubLabelMigration, type GhIssue } from "../index.ts"; import { normalizeImportValue, readImportTypeNames, serializeImportTags } from "../import-fidelity.ts"; import { boundedCli } from "./helpers/bounded-cli.ts"; import { withPackedFixture } from "./helpers/packed-fixture.ts"; @@ -93,7 +94,7 @@ test("mixed closed imports are byte-stable for nested workspace SDK aliases and : request.url!.includes("/issues?") ? current : []), async () => { let number = 1; for (const type of ["issue", " ISSUE ", "task", " PROBE "]) for (const labels of labelSets) { - const native = (await create({ title: "Native linked item", type, status: "busy-now", tags: `gh:fixture/project#${number + 1}` }, { pmRoot: root, noExtensions: true })).item; + const native = (await createImportMirror({ title: "Native linked item", type, status: "busy-now", tags: `gh:fixture/project#${number + 1}` }, { pmRoot: root, noExtensions: true })).item; current = [issue(number, labels), issue(number + 1, labels)]; const options = parseImportOptions({ atomic: true, type }); const first = await runImport("fixture/project", workspace, options); @@ -129,15 +130,16 @@ for (const atomic of [false, true]) test(`additive provider links preserve liter const original = ["gh:fixture/project#1", "area,api", '["json"]']; const local = (await create({ title: "Provider owner", type: "Task", tags: JSON.stringify(original), body: "Provider context" }, { pmRoot: root, noExtensions: true })).item; await commitImportedItem({ pmRoot: root, id: local.id, itemPath: getItemPath(root, "Task", local.id), settings: await readSettings(root), - document: { metadata: { ...local, github_link_only: true }, body: "Provider context" }, author: "fixture", message: "Seed existing provider link", conflictWarningPrefix: "fixture" }); + document: { metadata: { ...local, github_url: "https://github.com/fixture/project/issues/1", github_link_only: true }, body: "Provider context" }, author: "fixture", message: "Seed existing provider link", conflictWarningPrefix: "fixture" }); const labels = ["source,extra", '["array"]']; await withMockGithub((request, response) => jsonResponse(response, 200, request.url === "/graphql" ? { data: { repository: { issue: { timelineItems: { nodes: [], pageInfo: { hasNextPage: false, endCursor: null } } } } } } : request.url!.includes("/issues?") ? [issue(1, labels)] : []), async () => { const options = parseImportOptions({ atomic, type: "issue" }); await runImport("fixture/project", root, options); + assert.deepEqual((await listAllComplete({}, { pmRoot: root, noExtensions: true })).items.map(item => item.id), [local.id], "The curated-owner update must not create another item"); const written = (await get(local.id, {}, { pmRoot: root, noExtensions: true })).item; - assert.deepEqual(written.tags, toImportTags([...original, ...toImportTags(labels.join(","))])); + assert.deepEqual(written.tags, toImportTags(original), "curated owners retain tags without importing source labels"); for (const atom of local.tags!) assert.ok(written.tags!.includes(atom), "Native tag atoms must survive provider CSV conversion unchanged."); assert.equal(written.type, "Task"); assert.equal(written.body, "Provider context"); @@ -172,8 +174,11 @@ test("packed npm and native Bun consumers preserve mixed-import bytes in JSON an const packed = JSON.parse(await run("npm", ["pack", "--ignore-scripts", "--foreground-scripts=false", "--json", "--pack-destination", base], packageRoot, { ...env, PM_PATH: join(packageRoot, ".agents/pm") })) as [{ filename: string }]; await run(cli, ["--path", pmRoot, "package", "install", join(base, packed[0].filename), "--project"]); - await run(cli, ["--path", pmRoot, "create", "--title", "Native", "--type", "issue", "--tags", "gh:fixture/project#2"]); + await run(cli, ["--path", pmRoot, "create", "--title", "Native", "--id", deriveAtomicItemId("fixture/project", 2, "fixture-", normalizeItemId), "--type", "issue", "--tags", "gh:fixture/project#2", + "--author", "pm-github", "--description", "GH issue #2: https://github.com/fixture/project/issues/2"]); let payload = [issue(1, labelSets.flat()), issue(2, labelSets.flat())]; + /** Switch source snapshots after the preceding real CLI calls have completed. */ + const selectPayload = (next: GhIssue[]) => { payload = next; }; await withMockGithub((request, response) => jsonResponse(response, 200, request.url === "/graphql" ? { data: { repository: { issue: { timelineItems: { nodes: [], pageInfo: { hasNextPage: false, endCursor: null } } } } } } : request.url!.includes("/issues?") ? payload : request.url === "/repos/fixture/project/issues/7" ? { state: "open" } : []), async server => { @@ -200,7 +205,8 @@ test("packed npm and native Bun consumers preserve mixed-import bytes in JSON an assert.equal(item.tags.filter(tag => tag.startsWith("gh:fixture/project#")).length, 1); } } - await run(cli, ["--path", pmRoot, "create", "--title", "Plain native", "--type", "issue", "--tags", "gh:fixture/project#4"], root, environment); + await run(cli, ["--path", pmRoot, "create", "--title", "Plain native", "--id", deriveAtomicItemId("fixture/project", 4, "fixture-", normalizeItemId), "--type", "issue", "--tags", "gh:fixture/project#4", + "--author", "pm-github", "--description", "GH issue #4: https://github.com/fixture/project/issues/4"], root, environment); payload = [issue(3, labelSets.flat()), issue(4, labelSets.flat())]; const plainArgs = ["--path", pmRoot, "github", "import", "fixture/project", "--type", "issue"]; const plain = JSON.parse(await run("npm", ["exec", "--offline", "--", "pm", ...plainArgs, "--json"], root, environment)) as { imported: number; updated: number }; @@ -220,7 +226,8 @@ test("packed npm and native Bun consumers preserve mixed-import bytes in JSON an await schemaAddType("Investigation", { folder: "investigations", alias: ["probe"] }, { pmRoot: root, noExtensions: true }); await schemaAddStatus("working", { alias: ["busy-now"], role: ["active"] }, { pmRoot: root, noExtensions: true }); await schemaAddStatus("verified", { alias: ["settled"], role: ["terminal", "terminal_done"] }, { pmRoot: root, noExtensions: true }); - await run(cli, ["--path", root, "create", "--title", "Nested native", "--type", "probe", "--status", "busy-now", "--tags", "gh:fixture/project#6"], root, environment); + await run(cli, ["--path", root, "create", "--title", "Nested native", "--id", deriveAtomicItemId("fixture/project", 6, "fixture-", normalizeItemId), "--type", "probe", "--status", "busy-now", "--tags", "gh:fixture/project#6", + "--author", "pm-github", "--description", "GH issue #6: https://github.com/fixture/project/issues/6"], root, environment); payload = [issue(5, labelSets.flat()), issue(6, labelSets.flat())]; const nestedArgs = ["--path", root, "github", "import", "fixture/project", "--atomic", "--type", "probe"]; const nestedFirst = JSON.parse(await run("npm", ["exec", "--offline", "--", "pm", ...nestedArgs, "--json"], root, environment)) as { imported: number; updated: number }; @@ -238,15 +245,38 @@ test("packed npm and native Bun consumers preserve mixed-import bytes in JSON an } const custom = await create({ title: "Terminal custom", type: "probe", status: "busy-now", tags: "gh:fixture/project#7" }, { pmRoot: root, noExtensions: true }); await run(cli, ["--path", root, "update", custom.item.id, "--status", "settled", "--close-reason", "Fixture verification"], root, environment); + await mutateItem({ pmRoot, id: custom.item.id, settings: await readSettings(pmRoot), author: "fixture", op: "update", mutate: document => { + document.metadata.github_url = "https://github.com/fixture/project/issues/7"; + return { changedFields: ["github_url"] }; + } }); const providerBefore = durable(pmRoot); for (const bin of ["npm", "bun"]) for (const json of [true, false]) for (const command of ["sync", "export"]) { - const args = ["--path", root, "github", command, "--repo", "fixture/project", "--ids", custom.item.id, "--dry-run", ...(json ? ["--json"] : [])]; + const args = ["--path", root, "github", command, "--repo", "fixture/project", "--ids", custom.item.id, "--allow-reference-tags", "--dry-run", ...(json ? ["--json"] : [])]; const output = await run(bin, bin === "npm" ? ["exec", "--offline", "--", "pm", ...args] : ["--bun", cli, ...args], root, environment); const receipt = (json ? JSON.parse(output) : decode(output)) as { wouldSync: number; plan: Array<{ payload: { state: string } }> }; if (command === "sync") assert.equal(receipt.wouldSync, 1); else { assert.equal(receipt.plan.length, 1); assert.equal(receipt.plan[0]!.payload.state, "closed"); } assert.ok(durable(pmRoot) === providerBefore, "Workspace provider previews must preserve native items/history."); } + const id = deriveAtomicItemId("fixture/project", 2, "fixture-", normalizeItemId); + await mutateItem({ pmRoot, id, settings: await readSettings(pmRoot), author: "fixture", op: "update", message: "Relink packed consumer target", + mutate: document => { + document.metadata.github_url = "https://github.com/other/project/issues/8"; + document.metadata.github_repo = "other/project"; document.metadata.github_number = 8; + return { changedFields: ["github_url", "github_repo", "github_number"] }; + } }); + const history = verifyHistoryEntries(await readHistoryEntries(getHistoryPath(pmRoot, id), id)); + assert.equal(history.ok, true, history.errors.join("; ")); + assert.equal(history.latest_after_hash, (await get(id, {}, { pmRoot })).item_hash); + selectPayload([{ ...issue(2, []), title: "Changed source after explicit relink" }]); + const relinked = durable(pmRoot); + const refused = await boundedCli(cli, [...args, "--gate", "--json"], root, environment); + assert.equal(refused.code, 2); + const errorJson = refused.stdout.trim() || refused.stderr.slice(refused.stderr.indexOf("{")); + const problem = JSON.parse(errorJson) as { code: string; detail: string }; + assert.equal(problem.code, "github_import_owner_conflict"); + assert.match(problem.detail, /fixture\/project#2.*other\/project#8/); + assert.equal(durable(pmRoot), relinked, "The actual packed CLI must preserve the relinked item and its history"); }); }); }); diff --git a/test/import-regex.test.ts b/test/import-regex.test.ts index 1ff7a641..2a921a6c 100644 --- a/test/import-regex.test.ts +++ b/test/import-regex.test.ts @@ -2,7 +2,7 @@ import assert from "node:assert/strict"; import { performance } from "node:perf_hooks"; import test from "node:test"; -import { matchImportedIssue, trimLocatorPunctuation } from "../import-fidelity.ts"; +import { matchImportedIssue, trimLocatorPunctuation, githubLinkReferences } from "../import-fidelity.ts"; import { scanLineForRuleHits } from "../gate.ts"; import { githubApiBase, type GhIssue } from "../index.ts"; import { withEnv } from "./helpers/mock-github-server.ts"; @@ -11,7 +11,7 @@ const url = "https://github.com/fixture/project/issues/42"; const issue: GhIssue = { number: 42, title: "Fixture", body: "", state: "open", labels: [], assignee: null, milestone: null, created_at: "2026-10-01T00:00:00Z", updated_at: "2026-10-01T00:00:00Z", html_url: url }; -test("ownership declaration parity with citation adoption disabled: colon, whitespace and missing URL", () => { +test("legacy ownership declarations retain first-URL parsing: colon, whitespace and missing URL", () => { const rows: ReadonlyArray = [ [`GitHub issue ${url}`, true], [`GitHub issue ${url}`, true], [`GitHub issue\t${url}`, true], [`GitHub issue: ${url}`, true], @@ -53,7 +53,8 @@ test("locator punctuation trim parity table", () => { assert.equal(input.replace(/[),.;]+$/, ""), expected); assert.equal(trimLocatorPunctuation(input), expected); } - assert.equal(matchImportedIssue([{ id: "owner", links: url + "),.;" }], issue).item?.id, "owner"); + assert.equal(matchImportedIssue([{ id: "owner", links: url + "),.;" }], issue).item, undefined); + assert.deepEqual(githubLinkReferences({ links: url + "),.;" }).map(reference => reference.key), ["fixture/project#42"]); assert.equal(matchImportedIssue([{ id: "owner", links: url + "))x" }], issue).item, undefined); assert.equal(trimLocatorPunctuation(url + ":!]}", "),.;:!]}"), url); assert.equal(trimLocatorPunctuation(url + ":!]}"), url + ":!]}"); diff --git a/test/label-import-model.test.ts b/test/label-import-model.test.ts index c9b0755f..203b396b 100644 --- a/test/label-import-model.test.ts +++ b/test/label-import-model.test.ts @@ -6,7 +6,7 @@ import { tmpdir } from "node:os"; import { dirname, join } from "node:path"; import { decode, encode } from "@toon-format/toon"; import { init, create, update, get, normalizeItemId, readSettings, mutateItem } from "@unbrained/pm-cli/sdk"; -import { buildAtomicImportMutations, githubLabelsToTags, parseImportOptions, planGithubLabelMigration, +import { deriveAtomicItemId, buildAtomicImportMutations, githubLabelsToTags, parseImportOptions, planGithubLabelMigration, prepareGithubImport, validateGithubLabelMigrationReport, verifyImportIdempotency, type GhIssue, type PmItem } from "../index.ts"; import { refreshChanges } from "../refresh.ts"; import { normalizeImportValue } from "../import-fidelity.ts"; @@ -22,8 +22,14 @@ test("shared importer conversion covers legacy refresh deltas and every native m const repo = "fixture/repo"; const raw = [" One,TWO ", "ONE", " GH:FIXTURE/REPO#1 "]; const expected = (await create({ title: "SDK reference", type: "Task", tags: raw.join(","), ...scope }, { pmRoot: root })).item; - const match = (await create({ title: "Legacy target", type: "Task", tags: "old", ...scope }, { pmRoot: root })).item; + const createdMatch = (await create({ id: deriveAtomicItemId("fixture/repo", 1, "fixture-", normalizeItemId), author: "pm-github", title: "Legacy target", type: "Task", tags: "old", + description: "GH issue #1: https://github.com/fixture/repo/issues/1", ...scope }, { pmRoot: root })).item; const settings = await readSettings(root); + await mutateItem({ pmRoot: root, settings, id: createdMatch.id, author: "fixture", op: "update", mutate: document => { + document.metadata.github_url = "https://github.com/fixture/repo/issues/1"; + return { changedFields: ["github_url"] }; + } }); + const match = (await get(createdMatch.id, {}, { pmRoot: root })).item; const issue: GhIssue = { number: 1, title: "Legacy target", body: "", state: "open", labels: [{ name: " One,TWO " }, { name: "ONE" }], assignee: null, milestone: null, created_at: "2026-01-01T00:00:00Z", updated_at: "2026-02-01T00:00:00Z", html_url: `https://github.com/${repo}/issues/1` }; @@ -43,7 +49,7 @@ test("shared importer conversion covers legacy refresh deltas and every native m assert.ok(!refreshChanges({ ...legacy, match: { ...match, tags: expected.tags } }).some(change => change.field === "tags")); for (const updateOnly of [false, true]) { const plan = buildAtomicImportMutations(repo, legacy, settings.id_prefix, normalizeItemId, updateOnly); - const mutation = plan.mutations[0]!; + const mutation = plan.mutations.at(-1)!; assert.equal(mutation.op, "update"); if (mutation.op !== "update") throw new Error("Expected update"); assert.equal(mutation.options.tags, expected.tags!.join(",")); @@ -62,7 +68,7 @@ test("shared importer conversion covers legacy refresh deltas and every native m const retained = { ...prepared, match: before, tags: [...before.tags!, "fresh"], linkOnly, identity: { via: linkOnly ? "provider-link" : "citation" } }; const retainedPlan = buildAtomicImportMutations(repo, retained, settings.id_prefix, normalizeItemId); - const mutation = retainedPlan.mutations[0]!; + const mutation = retainedPlan.mutations.at(-1)!; if (mutation.op !== "update") throw new Error("Expected retained-owner update"); await update(match.id, { ...mutation.options, ...scope }, { pmRoot: root }); const actual = (await get(match.id, {}, { pmRoot: root })).item; @@ -84,7 +90,7 @@ test("500 random label sets predict the importer's actual SDK tag changes and ap try { await withEnv({ PM_PATH: root, PM_AUTHOR: "fixture", GIT_CEILING_DIRECTORIES: dirname(directory), PATH: directory }, async () => { await init("fixture", { defaults: true, withPackages: false, agentGuidance: "skip" }, { pmRoot: root, cwd: directory, noExtensions: true }); - const native = (await create({ title: "Import witness", type: "Task", clearDeps: true, + const native = (await create({ id: deriveAtomicItemId("fixture/repo", 1, "fixture-", normalizeItemId), author: "pm-github", title: "Import witness", type: "Task", clearDeps: true, unset: ["parent", "sprint", "release", "goal", "objective"] }, { pmRoot: root })).item; const settings = await readSettings(root); const options = parseImportOptions({}); @@ -100,8 +106,8 @@ test("500 random label sets predict the importer's actual SDK tag changes and ap const entry = (await prepareGithubImport({ ...issue, labels: names.map(name => ({ name })) }, repo, options, undefined, match, undefined, { items: [match], releases: [], timeline: async () => [] }))!; const plan = buildAtomicImportMutations(repo, entry, settings.id_prefix, normalizeItemId); - assert.equal(plan.mutations.length, 1); - const mutation = plan.mutations[0]!; + assert.equal(plan.mutations.length, 2, "an existing deterministic mirror reuses the create/update upsert plan"); + const mutation = plan.mutations.at(-1)!; assert.equal(mutation.op, "update"); if (mutation.op !== "update") throw new Error("Expected an existing-target import"); await update(native.id, { ...mutation.options, clearDeps: true, @@ -169,7 +175,8 @@ test("500 native tag arrays keep literal identities and refresh decisions equal await withEnv({ PM_PATH: root, PM_AUTHOR: "fixture", GIT_CEILING_DIRECTORIES: dirname(directory), PATH: directory }, async () => { await init("fixture", { defaults: true, withPackages: false, agentGuidance: "skip" }, { pmRoot: root, cwd: directory, noExtensions: true }); const scope = { clearDeps: true, unset: ["parent", "sprint", "release", "goal", "objective"] }; - const target = (await create({ title: "Native witness", type: "Task", ...scope }, { pmRoot: root })).item; + const target = (await create({ id: deriveAtomicItemId("fixture/repo", 1, "fixture-", normalizeItemId), author: "pm-github", title: "Native witness", type: "Task", + description: "GH issue #1: https://github.com/fixture/repo/issues/1", ...scope }, { pmRoot: root })).item; const settings = await readSettings(root); const repo = "fixture/repo"; const provenance = `gh:${repo}#1`; @@ -201,7 +208,7 @@ test("500 native tag arrays keep literal identities and refresh decisions equal const delta = refreshChanges(entry).find(change => change.field === "tags"); const refreshPlan = buildAtomicImportMutations(repo, entry, settings.id_prefix, normalizeItemId, true); const fullPlan = buildAtomicImportMutations(repo, entry, settings.id_prefix, normalizeItemId); - const mutation = fullPlan.mutations[0]!; + const mutation = fullPlan.mutations.at(-1)!; if (mutation.op !== "update") throw new Error("Expected existing-target SDK import"); // Execute the full import independently of the refresh's changed-field decision. await update(target.id, { ...mutation.options, ...scope }, { pmRoot: root }); diff --git a/test/legacy-description-compatibility.test.ts b/test/legacy-description-compatibility.test.ts new file mode 100644 index 00000000..d86ecaaf --- /dev/null +++ b/test/legacy-description-compatibility.test.ts @@ -0,0 +1,86 @@ +/** Legacy declaration ownership and retained reference refusals use native SDK trackers and command dispatch. */ +import assert from "node:assert/strict"; +import test from "node:test"; +import { mkdtempSync, readFileSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { init, create, get, mutateItem, readSettings, getItemPath, getHistoryPath, listAllComplete } from "@unbrained/pm-cli/sdk"; +import { createExtensionTestHarness } from "@unbrained/pm-cli/sdk/testing"; +import extension, { type GhIssue } from "../index.ts"; +import { matchImportedIssue } from "../import-fidelity.ts"; +import { githubOwnershipClaims } from "../import-ownership.ts"; +import { withEnv, withMockGithub, jsonResponse } from "./helpers/mock-github-server.ts"; + +const examples = [ + { name: "pm-github-z5z1", repo: "unbraind/pm-github", number: 129, description: "GitHub issue: https://github.com/unbraind/pm-github/issues/129. Add optional behavior." }, + { name: "pm-review-withdrawn-lineage", repo: "unbraind/pm-review", number: 30, description: "GH issue #30: https://github.com/unbraind/pm-review/issues/30 · author fixture · open" }, + { name: "pm-rust-oqst", repo: "unbraind/pm-rust", number: 86, description: "GitHub issue https://github.com/unbraind/pm-rust/issues/86: centralize behavior." }, +]; + +/** Seed old-style metadata with no explicit number, URL, external ref or importer-created ID. */ +async function tracker(values: Record, run: (root: string, id: string) => Promise) { + const directory = mkdtempSync(join(tmpdir(), "github-legacy-declaration-")), root = join(directory, "tracker"); + try { + await init("fixture", { defaults: true, withPackages: false, agentGuidance: "skip" }, { pmRoot: root, cwd: directory, noExtensions: true }); + await withEnv({ PM_PATH: root, PM_AUTHOR: "fixture", PM_TELEMETRY_SOURCE_CONTEXT: "automation", GITHUB_TOKEN: "fixture-token", GH_TOKEN: undefined }, async () => { + const item = (await create({ title: "Curated Chore", type: "Chore", body: "Local context", status: "in_progress", tags: "keep" }, { pmRoot: root })).item; + await mutateItem({ pmRoot: root, id: item.id, settings: await readSettings(root), author: "fixture", op: "update", mutate: document => { + Object.assign(document.metadata, values); return { changedFields: Object.keys(values) }; + } }); + await run(root, item.id); + }); + } finally { rmSync(directory, { recursive: true, force: true }); } +} + +/** Ordinary API issue input, including the declaration's exact repository and number. */ +function source(repo: string, number: number): GhIssue { + return { number, title: "Source title", body: "Public source", state: "open", labels: [], user: null, assignee: null, milestone: null, + created_at: "2025-01-01T00:00:00Z", updated_at: "2025-02-01T00:00:00Z", html_url: `https://github.com/${repo}/issues/${number}` }; +} + +/** Observe both the document and append-only audit record without relying on SDK projections. */ +function bytes(root: string, id: string) { return [readFileSync(getItemPath(root, "Chore", id), "hex"), readFileSync(getHistoryPath(root, id), "hex")]; } + +/** Only external GitHub transport is substituted; all command, identity and storage operations are real. */ +async function importing(root: string, repo: string, issue: GhIssue) { + return withMockGithub((request, response) => { assert.equal(request.method, "GET"); jsonResponse(response, 200, request.url?.includes("/timeline") ? [] : [issue]); }, async () => { + const harness = await createExtensionTestHarness(extension, { capabilities: ["commands", "importers", "schema", "hooks", "preflight", "search"] }); + return harness.runCommand({ command: "github import", args: [repo], options: { atomic: true }, pmRoot: root, global: { json: true } }); + }); +} + +for (const example of examples) test(`Legacy declaration allows ${example.name} without link-only metadata`, async () => tracker({ description: example.description }, async (root, id) => { + const item = (await get(id, {}, { pmRoot: root })).item, issue = source(example.repo, example.number); + for (const field of ["github_number", "github_url", "external_ref", "github_link_only"] as const) assert.equal(item[field], undefined); + assert.deepEqual(githubOwnershipClaims(item).owners, [`${example.repo}#${example.number}`]); + const match = matchImportedIssue([item], issue); + assert.equal(match.item?.id, id); assert.equal(match.linkOnly, true); + await importing(root, example.repo, issue); + const after = (await get(id, {}, { pmRoot: root })).item; + for (const field of ["title", "description", "type", "body", "created_at", "status"] as const) assert.deepEqual(after[field], item[field], field); + assert.ok(after.tags!.includes("keep")); + assert.equal((await listAllComplete({}, { pmRoot: root })).items.length, 1); + const beforeRepeat = bytes(root, id); await importing(root, example.repo, issue); assert.deepEqual(bytes(root, id), beforeRepeat); +})); + +for (const kind of ["middle-description", "prose-url-field", "reference-tag"] as const) test(`Reference refusal keeps ${kind} unowned`, async () => { + const repo = "o/r", issue = source(repo, 7), url = issue.html_url; + const fields = kind === "middle-description" ? { description: `Context. GitHub issue ${url}` } + : kind === "prose-url-field" ? { github_url: `See ${url}` } : { tags: ["keep", "gh:o/r#7"] }; + await tracker(fields, async (root, id) => { + const item = (await get(id, {}, { pmRoot: root })).item, before = bytes(root, id); + assert.deepEqual(githubOwnershipClaims(item).owners, []); + assert.equal(matchImportedIssue([item], issue, kind === "reference-tag" ? item : undefined).item, undefined); + await importing(root, repo, issue); + assert.deepEqual(bytes(root, id), before); assert.equal((await listAllComplete({}, { pmRoot: root })).items.length, 2); + }); +}); + +for (const explicit of [false, true]) test(`Copied-provider markers retain main's precedence (explicit owner: ${explicit})`, async () => tracker({ + description: "GitHub issue https://github.com/o/r/issues/7. Local evidence", body: "[linear] linear_id=ENG-7 linear_url=https://linear.app/fixture/issue/ENG-7", + ...(explicit ? { github_repo: "o/r", github_number: 7, github_url: "https://github.com/o/r/issues/7", github_link_only: false } : {}), +}, async (root, id) => { + const item = (await get(id, {}, { pmRoot: root })).item; + const match = matchImportedIssue([item], source("o/r", 7)); + assert.equal(match.item?.id, id); assert.equal(match.linkOnly, !explicit); +})); diff --git a/test/link-deps.test.ts b/test/link-deps.test.ts index b973ac42..75df5f2c 100644 --- a/test/link-deps.test.ts +++ b/test/link-deps.test.ts @@ -122,10 +122,10 @@ test("returns [] for empty/undefined body and bodies with no phrase", () => { // buildProvenanceIndexFromMetadata — pure // --------------------------------------------------------------------------- -test("indexes items by their gh provenance tag", () => { +test("indexes items by their explicit import identity", () => { const items: DepLinkSnapshotItem[] = [ - { id: "pm-1", tags: ["bug", "gh:acme/widgets#1"] }, - { id: "pm-2", tags: ["gh:acme/widgets#2"] }, + { id: "pm-1", github_url: "https://github.com/acme/widgets/issues/1", tags: ["bug", "gh:acme/widgets#1"] }, + { id: "pm-2", github_url: "https://github.com/acme/widgets/issues/2", tags: ["gh:acme/widgets#2"] }, { id: "pm-3", tags: ["no-provenance"] }, ]; const index = buildProvenanceIndexFromMetadata(items); @@ -134,12 +134,14 @@ test("indexes items by their gh provenance tag", () => { assert.strictEqual(index.size, 2); }); -test("first writer wins on a duplicated provenance tag", () => { +test("duplicated explicit import identities remain unresolved", () => { const items: DepLinkSnapshotItem[] = [ - { id: "pm-1", tags: ["gh:acme/widgets#1"] }, - { id: "pm-dup", tags: ["gh:acme/widgets#1"] }, + { id: "pm-1", github_url: "https://github.com/acme/widgets/issues/1", tags: ["gh:acme/widgets#1"] }, + { id: "pm-dup", github_url: "https://github.com/acme/widgets/issues/1", tags: ["gh:acme/widgets#1"] }, ]; - assert.strictEqual(buildProvenanceIndexFromMetadata(items).get("acme/widgets#1"), "pm-1"); + const ambiguous = new Map(); + assert.strictEqual(buildProvenanceIndexFromMetadata(items, ambiguous).has("acme/widgets#1"), false); + assert.deepEqual(ambiguous.get("acme/widgets#1"), ["pm-1", "pm-dup"]); }); // --------------------------------------------------------------------------- @@ -209,8 +211,8 @@ test("de-duplicates identical edges across issues", () => { function snapshot(deps: Record> = {}): DepLinkSnapshotItem[] { return [ - { id: "pm-1", tags: ["gh:acme/widgets#1"], dependencies: deps["pm-1"] }, - { id: "pm-2", tags: ["gh:acme/widgets#2"], dependencies: deps["pm-2"] }, + { id: "pm-1", github_url: "https://github.com/acme/widgets/issues/1", tags: ["gh:acme/widgets#1"], dependencies: deps["pm-1"] }, + { id: "pm-2", github_url: "https://github.com/acme/widgets/issues/2", tags: ["gh:acme/widgets#2"], dependencies: deps["pm-2"] }, ]; } diff --git a/test/minimum-sdk-consumer.test.ts b/test/minimum-sdk-consumer.test.ts index 4e5a3e23..dcefa553 100644 --- a/test/minimum-sdk-consumer.test.ts +++ b/test/minimum-sdk-consumer.test.ts @@ -58,6 +58,7 @@ test("packed package type-checks and imports against its exact declared minimum }; const minimum = manifest.pm_min_version; assert.match(minimum, /^\d+\.\d+\.\d+$/); + assert.equal(minimum, "2026.10.23", "Ownership writes require the first published fenced-lock SDK"); assert.equal(pkg.peerDependencies[cli], `>=${minimum}`); assert.match(pkg.devDependencies[cli]!, /^\d+\.\d+\.\d+$/, "the development pin is independent of the consumer floor"); const packed = JSON.parse(run("npm", ["pack", "--ignore-scripts", "--json", "--pack-destination", scratch], repo, environment)) as { filename: string }[]; @@ -110,10 +111,18 @@ test("packed package type-checks and imports against its exact declared minimum import { join } from "node:path"; import { spawnSync } from "node:child_process"; import extension, { planGithubLabelMigration, validateGithubLabelMigrationReport, buildAtomicImportMutations } from "pm-github"; -import { init, create, update, get, mutateItem, readSettings, encodePmTableRows, normalizeItemId } from "@unbrained/pm-cli/sdk"; +import { readGithubImportOwner, mutateGithubOwnedItem } from "pm-github/dist/import-ownership.js"; +import { init, create, update, get, mutateItem, readSettings, encodePmTableRows, normalizeItemId, acquireLockLease } from "@unbrained/pm-cli/sdk"; assert.ok(extension); const root = join(process.cwd(), "tracker"); await init("fixture", { defaults: true, withPackages: false, agentGuidance: "skip" }, { pmRoot: root, cwd: process.cwd(), noExtensions: true }); +const lease = await acquireLockLease(root, "minimum-fence", 30, "fixture", false, false, 0); +let fenced = 0; +await lease.fence(async () => { fenced++; }); +assert.equal(fenced, 1); +await lease.release(); +await assert.rejects(() => lease.fence(async () => { fenced++; }), error => error.code === "lock_lost"); +assert.equal(fenced, 1, "A lost SDK lease must never invoke its write callback"); const scope = { clearDeps: true, unset: ["parent", "sprint", "release", "goal", "objective"] }; const item = (await create({ title: "Minimum SDK witness", type: "Task", tags: "bug", ...scope }, { pmRoot: root })).item; const assigned = spawnSync(process.execPath, [join(process.cwd(), "node_modules/.bin/pm"), "--path", root, "update", item.id, "--stdin-json"], { @@ -149,6 +158,18 @@ assert.ok(preserved.tags.includes("bug, bug")); assert.ok(preserved.tags.includes('["array"]')); assert.ok(!preserved.tags.includes("array")); +await mutateItem({ pmRoot: root, settings, id: item.id, author: "fixture", op: "update", mutate: document => { + document.metadata.github_url = "https://github.com/fixture/repo/issues/1"; + document.metadata.github_repo = "fixture/repo"; document.metadata.github_number = 1; + return { changedFields: ["github_url", "github_repo", "github_number"] }; +} }); +const target = { id: item.id, repo: "fixture/repo", number: 1 }; +const revision = await readGithubImportOwner(root, target); +assert.match(revision, /^[a-f0-9]{64}$/); +await mutateGithubOwnedItem(root, target, "update", { title: "Owned minimum-SDK update" }, revision); +assert.equal((await get(item.id, {}, { pmRoot: root })).item.title, "Owned minimum-SDK update"); +await assert.rejects(() => mutateGithubOwnedItem(root, target, "update", { title: "Stale revision rejected" }, revision), /revision|hash|changed/i); + console.log(JSON.stringify({ sdk: ${JSON.stringify(minimum)}, extension_import: true, encoded_rows: true, native_comma: true, json_tag: true, non_git: true })); `); console.log(`minimum SDK ${minimum}: importing and exercising the installed artifact`); diff --git a/test/owner-field-values.test.ts b/test/owner-field-values.test.ts new file mode 100644 index 00000000..1c5a92f1 --- /dev/null +++ b/test/owner-field-values.test.ts @@ -0,0 +1,119 @@ +/** Whole-value ownership and reference-only links use native trackers and the registered import command. */ +import assert from "node:assert/strict"; +import test from "node:test"; +import { mkdtempSync, readFileSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { init, create, get, mutateItem, readSettings, getItemPath, getHistoryPath, listAllComplete } from "@unbrained/pm-cli/sdk"; +import { createExtensionTestHarness } from "@unbrained/pm-cli/sdk/testing"; +import extension, { type GhIssue } from "../index.ts"; +import { explicitGithubIdentities, githubLinkReferences, legacyGithubLinkIdentities, matchImportedIssue } from "../import-fidelity.ts"; +import { exploreGithubIdentities } from "../identity.ts"; +import { githubOwnershipClaims } from "../import-ownership.ts"; +import { withEnv, withMockGithub, jsonResponse } from "./helpers/mock-github-server.ts"; + +const url = "https://github.com/o/r/issues/7"; +const source: GhIssue = { number: 7, title: "Source title", body: "Public source", state: "open", labels: [], user: null, + assignee: null, milestone: null, created_at: "2025-01-01T00:00:00Z", updated_at: "2025-02-01T00:00:00Z", html_url: url }; + +/** Seed arbitrary old metadata through real SDK persistence without relying on Git. */ +async function tracker(values: Record, run: (root: string, id: string) => Promise, title = "Curated Chore", tags = "keep,gh:o/r#7") { + const directory = mkdtempSync(join(tmpdir(), "github-whole-field-")), root = join(directory, "tracker"); + try { + await init("fixture", { defaults: true, withPackages: false, agentGuidance: "skip" }, { pmRoot: root, cwd: directory, noExtensions: true }); + await withEnv({ PM_PATH: root, PM_AUTHOR: "fixture", PM_TELEMETRY_SOURCE_CONTEXT: "automation", GITHUB_TOKEN: "fixture-token", GH_TOKEN: undefined }, async () => { + const item = (await create({ title, type: "Chore", tags }, { pmRoot: root })).item; + await mutateItem({ pmRoot: root, id: item.id, settings: await readSettings(root), author: "fixture", op: "update", mutate: document => { + Object.assign(document.metadata, values); return { changedFields: Object.keys(values) }; + } }); + await run(root, item.id); + }); + } finally { rmSync(directory, { recursive: true, force: true }); } +} + +/** Snapshot curated documents and append-only history independently of SDK read projections. */ +function bytes(root: string, id: string) { + return [readFileSync(getItemPath(root, "Chore", id), "hex"), readFileSync(getHistoryPath(root, id), "hex")]; +} + +/** Mock external HTTP data only; command dispatch, matching, ownership and writes remain native. */ +async function importing(root: string, adopt = false, issue = source) { + return withMockGithub((request, response) => { + assert.ok(request.method === "GET", "Import must not write to GitHub"); + jsonResponse(response, 200, request.url?.includes("/timeline") ? [] : [issue]); + }, async () => { + const harness = await createExtensionTestHarness(extension, { capabilities: ["commands", "importers", "schema", "hooks", "preflight", "search"] }); + return harness.runCommand({ command: "github import", args: ["o/r"], options: { atomic: true, "adopt-cited": adopt }, pmRoot: root, global: { json: true } }); + }); +} + +for (const field of ["github_url", "GitHubURL"]) for (const value of [`See ${url}`, `${url} followed by text`, [url], { url }]) { + test(`Whole identity field refuses substring ownership: ${field} ${JSON.stringify(value)}`, async () => tracker({ [field]: value }, async (root, id) => { + const item = (await get(id, {}, { pmRoot: root })).item, before = bytes(root, id); + assert.deepEqual(explicitGithubIdentities(item), []); + assert.deepEqual(legacyGithubLinkIdentities(item), []); + assert.deepEqual(githubOwnershipClaims(item).owners, []); + assert.equal(matchImportedIssue([item], source).item, undefined); + const graph = await exploreGithubIdentities(root, { identity: "gh:o/r#7" }); + assert.equal(graph.edges.some(edge => edge.strength === "claim"), false); + assert.ok(graph.edges.some(edge => edge.strength === "mention" && edge.source === "prose")); + await importing(root); + assert.deepEqual(bytes(root, id), before); + assert.equal((await listAllComplete({}, { pmRoot: root })).items.length, 2); + }, "Curated Chore", "keep")); +} + +for (const field of ["github_url", "GitHubURL"]) test(`Whole identity field allows exact URL: ${field}`, async () => tracker({ [field]: url }, async (root, id) => { + const item = (await get(id, {}, { pmRoot: root })).item; + assert.deepEqual(githubOwnershipClaims(item).owners, ["o/r#7"]); + assert.equal(matchImportedIssue([item], source).item?.id, id); + await importing(root); + assert.equal((await listAllComplete({}, { pmRoot: root })).items.length, 1); + const actual = (await get(id, {}, { pmRoot: root })).item; + assert.equal(actual.title, item.title); assert.equal(actual.type, item.type); assert.equal(actual.created_at, item.created_at); +})); + +for (const field of ["links", "external_links", "external_url", "provenance"]) test(`Other link fields preserve references without ownership: ${field}`, async () => tracker({ [field]: `See ${url} for context` }, async (root, id) => { + const item = (await get(id, {}, { pmRoot: root })).item, before = bytes(root, id); + assert.deepEqual(githubLinkReferences(item).map(reference => reference.key), ["o/r#7"]); + assert.deepEqual(githubOwnershipClaims(item).owners, []); + assert.equal(matchImportedIssue([item], source).item, undefined); + const graph = await exploreGithubIdentities(root, { identity: "gh:o/r#7" }); + assert.ok(graph.edges.some(edge => edge.strength === "mention" && edge.source === "prose")); + assert.equal(graph.edges.some(edge => edge.strength === "claim"), false); + await importing(root); + assert.deepEqual(bytes(root, id), before); + assert.equal((await listAllComplete({}, { pmRoot: root })).items.length, 2); +}, "Curated Chore", "keep")); + +for (const explicitOwner of [false, true]) test(`Reference prose does not bar explicit citation adoption (existing owner: ${explicitOwner})`, async () => tracker({ links: "See https://github.com/other/project/issues/8 for context", ...(explicitOwner ? { github_url: "https://github.com/other/project/issues/8" } : {}) }, async (root, id) => { + const item = (await get(id, {}, { pmRoot: root })).item, before = bytes(root, id); + assert.equal(matchImportedIssue([item], source, undefined, true).item?.id, explicitOwner ? undefined : id); + await importing(root, true); + assert.equal((await listAllComplete({}, { pmRoot: root })).items.length, explicitOwner ? 2 : 1); + if (explicitOwner) assert.deepEqual(bytes(root, id), before); +}, "Closes o/r#7")); + +for (const marker of [false, true]) for (const trailing of [false, true]) test(`Native link declaration remains compatible without a marker (${marker}, trailing: ${trailing})`, async () => tracker({ description: `GitHub issue ${url}${trailing ? " followed by prose" : ""}`, ...(marker ? { github_link_only: true } : {}) }, async (root, id) => { + const item = (await get(id, {}, { pmRoot: root })).item; + assert.deepEqual(githubOwnershipClaims(item).owners, ["o/r#7"]); + assert.equal(matchImportedIssue([item], source).item?.id, id); +})); + +for (const external of [false, true]) test(`Provider backlink ignores GitHub references (external provider: ${external})`, async () => { + const backlink = external ? "https://linear.app/fixture/issue/ENG-7" : url; + await tracker({ linear_id: "ENG-7", external_links: `See ${backlink}` }, async (root, id) => { + const issue = { ...source, body: `Mirrored from ${backlink}` }; + const item = (await get(id, {}, { pmRoot: root })).item, before = bytes(root, id); + assert.deepEqual(githubOwnershipClaims(item).owners, []); + assert.equal(matchImportedIssue([item], issue).item?.id, external ? id : undefined); + await importing(root, false, issue); + assert.equal((await listAllComplete({}, { pmRoot: root })).items.length, external ? 1 : 2); + if (!external) assert.deepEqual(bytes(root, id), before); + }); +}); + +for (const marker of [false, true]) test(`Identity graph retains legacy native declaration claims (${marker})`, async () => tracker({ description: `GitHub issue ${url}`, ...(marker ? { github_link_only: true } : {}) }, async root => { + const graph = await exploreGithubIdentities(root, { identity: "gh:o/r#7" }); + assert.equal(graph.edges.some(edge => edge.strength === "claim"), true); +})); diff --git a/test/ownership-consistency.test.ts b/test/ownership-consistency.test.ts new file mode 100644 index 00000000..5af4d0b2 --- /dev/null +++ b/test/ownership-consistency.test.ts @@ -0,0 +1,620 @@ +/** Contradictions and competing claims are refused against native SDK trackers on every write path. */ +import assert from "node:assert/strict"; +import test from "node:test"; +import { mkdtempSync, rmSync, readFileSync, chmodSync, existsSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join, dirname, delimiter } from "node:path"; +import { spawnSync } from "node:child_process"; +import { + init, create, get, commitItemMutations, docs as nativeDocs, readSettings, mutateItem, getItemPath, getHistoryPath, normalizeItemId, + listAllComplete, WorkspaceTransactionInterruptedError, runWithActiveExtensions, setActiveExtensionHooks, +} from "@unbrained/pm-cli/sdk"; +import extension, { + runImport, parseImportOptions, deriveAtomicItemId, prepareGithubImport, importGithubAtomic, + readGithubImportPlan, resumeGithubImport, listUnfinishedGithubImports, fetchJSON, githubApiBase, parseNextLink, + linkImportedDependencies, indexByProvenance, collectBlockerReferences, buildExportPlan, applyExportPlan, + type PmItem, type BlockerPmItem, + type GhIssue, +} from "../index.ts"; +import { createExtensionTestHarness } from "@unbrained/pm-cli/sdk/testing"; +import { fetchImportTimeline, matchImportedIssue } from "../import-fidelity.ts"; +import { mutateGithubOwnedItem, assertGithubOwnershipConsistency, githubOwnershipClaims, withGithubImportOwners, withGithubOwnershipWork, type GithubOwnershipWork } from "../import-ownership.ts"; +import { withImportPreview } from "./helpers/import-preview.ts"; +import { exploreGithubIdentities } from "../identity.ts"; +import { selectRefreshTargets } from "../refresh.ts"; +import { indexPmByIssue, buildProjectPushPlan, buildProjectPullPlan, buildProjectImportPlan } from "../projects.ts"; +import { runCloseout } from "../closeout.ts"; +import { withEnv, captureStderr, withMockGithub, jsonResponse } from "./helpers/mock-github-server.ts"; + +const repo = "o/r"; +const source: GhIssue = { number: 7, title: "Remote updated", body: "Public source body", state: "open", labels: [], user: null, + assignee: null, milestone: null, created_at: "2025-01-01T00:00:00Z", updated_at: "2025-02-01T00:00:00Z", html_url: "https://github.com/o/r/issues/7" }; + +/** Native tracker without git, except the two explicit gated-replay fixtures. */ +async function tracker(run: (root: string, id: string) => Promise, git = false, managed = true, createdByImporter = managed) { + const directory = mkdtempSync(join(tmpdir(), "github-consistency-")); + const root = join(directory, "tracker"); + try { + if (git) assert.equal(spawnSync("git", ["init", "-q"], { cwd: directory }).status, 0); + await init("fixture", { defaults: true, withPackages: false, agentGuidance: "skip" }, { pmRoot: root, cwd: directory, noExtensions: true }); + const id = managed ? deriveAtomicItemId(repo, 7, (await readSettings(root)).id_prefix, normalizeItemId) : normalizeItemId("curated", (await readSettings(root)).id_prefix); + await create({ id, author: createdByImporter ? "pm-github" : "fixture", title: "Original local title", type: !createdByImporter ? "Chore" : "Issue", body: "Original local body", status: "in_progress", + description: `GH issue #7: ${source.html_url}`, tags: "gh:o/r#7" }, { pmRoot: root }); + await change(root, id, { github_repo: repo, github_number: 7, github_url: source.html_url, external_links: ["See https://github.com/other/project/issues/8 for context"] }); + await withEnv({ PM_PATH: root, PM_AUTHOR: "fixture", PM_TELEMETRY_SOURCE_CONTEXT: "automation", GITHUB_TOKEN: "fixture-token", GH_TOKEN: undefined, + PATH: join(import.meta.dirname, "../node_modules/.bin") + delimiter + process.env.PATH }, () => run(root, id)); + } finally { rmSync(directory, { recursive: true, force: true }); } +} + +/** Independent SDK mutation, also used to introduce a peer after the import's initial read. */ +async function change(root: string, id: string, values: Record, reset = false) { + await runWithActiveExtensions({ path: root, noExtensions: true }, async () => mutateItem({ pmRoot: root, settings: await readSettings(root), id, author: "fixture", op: "update", message: "Fixture ownership edit", + mutate: document => { + if (reset) { + for (const key of Object.keys(document.metadata)) if (/^github/i.test(key) || key === "external_ref") delete document.metadata[key]; + document.metadata.description = "Legacy local record"; document.metadata.tags = []; + } + Object.assign(document.metadata, values); + return { changedFields: Object.keys(values) }; + } })); +} + +/** Preserve both native data and audit bytes on a refused write. */ +async function bytes(root: string, id: string) { + const item = (await get(id, {}, { pmRoot: root })).item; + return [readFileSync(getItemPath(root, item.type!, id), "utf8"), readFileSync(getHistoryPath(root, id), "utf8")]; +} + +/** Exercise real HTTP parsing; only GitHub itself is represented by a local server. */ +async function github(id: string, run: () => Promise): Promise { + return withMockGithub((request, response) => { + const path = request.url!; + const sha = "a".repeat(40); + jsonResponse(response, 200, path.includes("/pulls/") ? path.includes("/commits?") ? [] : { + number: 1, commits: 0, merged: true, merged_at: "2099-01-01T00:00:00Z", merge_commit_sha: "b".repeat(40), html_url: `https://github.com/${repo}/pull/1`, + body: `Closes https://github.com/${repo}/blob/main/.agents/pm/issues/${id}.toon`, head: { sha }, base: { ref: "main", repo: { full_name: repo } }, + } : path.includes("/protection/") ? { contexts: ["CI"], checks: [] } + : path.includes("/check-runs") ? { total_count: 1, check_runs: [{ name: "CI", head_sha: sha, status: "completed", conclusion: "success", app: { id: 1 } }] } + : /\/issues\?/.test(path) ? [source] : []); + }, run); +} + +/** Real fetch, matching, mapping and SDK writes. */ +async function sync(root: string, atomic = false, gate = false) { + return captureStderr(() => runImport(repo, root, parseImportOptions({ atomic, gate }))); +} + +/** Persist a real SDK journal and its original recovery plan before introducing the conflict. */ +async function interrupt(root: string, id: string) { + const items = (await listAllComplete({ includeBody: true }, { pmRoot: root })).items; + const entry = (await prepareGithubImport(source, repo, parseImportOptions({}), "fixture-token", items.find(item => item.id === id), undefined, + { items, releases: [], timeline: issue => fetchImportTimeline(issue, repo, "fixture-token") }))!; + await assert.rejects(() => importGithubAtomic(root, repo, [entry], { onTransition: context => { + if (context.transition === "prepared") throw new WorkspaceTransactionInterruptedError("Fixture interruption"); + } })); + const pending = (await listUnfinishedGithubImports(root))[0]!; + const plan = (await readGithubImportPlan(root, pending.transactionId))!; + return plan; +} + +interface Shape { name: string; values?: Record; reset?: boolean; peer?: Record; claim: string } +const shapes: Shape[] = [ + { name: "partial repository versus URL", reset: true, values: { github_repo: "other/project", github_url: source.html_url }, claim: "other/project#7" }, + { name: "repository versus URL", values: { github_repo: "other/project" }, claim: "other/project#7" }, + { name: "number versus URL", values: { github_number: 8 }, claim: "o/r#8" }, + { name: "contradictory URL alias", values: { GitHubURL: "https://github.com/other/project/pull/8" }, claim: "other/project#8" }, + { name: "contradictory native declaration", values: { github_link_only: true, description: "GitHub issue https://github.com/other/project/issues/8" }, claim: "other/project#8" }, + { name: "external-ref repository", values: { external_ref: "gh:other/project#8" }, claim: "other/project#8" }, + { name: "external-ref number", values: { external_ref: "https://github.com/O/R/pull/8/" }, claim: "o/r#8" }, + { name: "generated legacy number contradicts URL", reset: true, values: { github_number: 8, github_url: source.html_url, description: `GH issue #8: ${source.html_url}`, tags: ["gh:o/r#7"] }, claim: "o/r#8" }, + { name: "competing URL alias", peer: { GitHubURL: "https://www.github.com/O/R/pull/7/" }, claim: "claimed by" }, + { name: "competing external-ref", peer: { external_ref: "gh:O/R#7" }, claim: "claimed by" }, +]; + +/** Typed diagnostics name every contradictory claim and the remedy, rather than just refusing a generic collision. */ +function refusal(error: unknown, shape: Shape) { + assert.equal((error as { code: string })?.code, "github_import_owner_conflict"); + assert.match(String(error), /o\/r#7/); + assert.ok(String(error).includes(shape.claim), String(error)); + assert.match(String(error), /Restore the intended GitHub provenance/); + return true; +} + +for (const path of ["match", "plain", "atomic", "closeout", "replay"] as const) for (const shape of shapes) { + test(`Ownership matrix ${path}: refuses ${shape.name}`, async () => tracker(async (root, id) => github(id, async () => { + const plan = path === "replay" ? await interrupt(root, id) : undefined; + if (shape.peer) { + const peer = (await create({ title: "Peer claimant", type: "Chore" }, { pmRoot: root })).item; + await change(root, peer.id, shape.peer); + } else await change(root, id, shape.values!, shape.reset); + const before = await bytes(root, id); + if (path === "closeout") { + const result = await runCloseout(root, repo, { pr: "1", apply: true }, { base: githubApiBase(), next: parseNextLink, get: url => fetchJSON(url, "fixture-token") }); + assert.equal(result.closed, 0); + assert.equal(result.plan[0]!.code, "github_import_owner_conflict"); + assert.ok(result.plan[0]!.reason.includes(shape.claim), result.plan[0]!.reason); + } else if (path === "match") { + const items = (await listAllComplete({ includeBody: true }, { pmRoot: root })).items; + assert.throws(() => matchImportedIssue(items, source), error => refusal(error, shape)); + } else await assert.rejects(() => path === "replay" ? sync(root, true, true) : sync(root, path === "atomic"), error => refusal(error, shape)); + assert.deepEqual(await bytes(root, id), before); + if (plan) assert.deepEqual(await readGithubImportPlan(root, plan.transactionId), plan, "A native journal requires the original plan to remain available"); + }), path === "replay", path !== "closeout")); +} + +for (const legacy of [false, true]) test(`Ownership matrix allows consistent ${legacy ? "legacy" : "full"} record and idempotent re-import`, async () => tracker(async (root, id) => github(id, async () => { + await change(root, id, legacy ? { github_repo: " O/R/// ", github_link_only: false, external_links: ["See https://github.com/other/project/issues/8"], tags: ["gh:O/R#7"] } + : { github_repo: " O/R/// ", github_number: 7, github_url: "https://www.github.com/O/R/pull/7/?a=1#fragment", external_ref: "gh:o/r#7", external_links: ["See https://github.com/other/project/issues/8"], tags: ["gh:O/R#7"] }, legacy); + const items = (await listAllComplete({ includeBody: true }, { pmRoot: root })).items; + assert.equal(matchImportedIssue(items, source).item?.id, id); + await sync(root, true); + const before = await bytes(root, id); + await sync(root, true); + assert.deepEqual(await bytes(root, id), before); + assert.equal((await listAllComplete({}, { pmRoot: root })).items.length, 1); +}))); + +test("Ownership matrix reference-only tags remain non-owning", async () => tracker(async (root, id) => github(id, async () => { + for (let n = 0; n < 2; n++) await create({ title: `Reference Chore ${n}`, type: "Chore", tags: "gh:o/r#7" }, { pmRoot: root }); + await sync(root, true); + assert.equal((await listAllComplete({}, { pmRoot: root })).items.length, 3); + assert.equal((await get(id, {}, { pmRoot: root })).item.title, source.title); +}))); + +for (const replay of [false, true]) test(`Ownership matrix locked ${replay ? "replay" : "plain"} rejects a newly competing owner`, async () => tracker(async (root, id) => github(id, async () => { + const plan = replay ? await interrupt(root, id) : undefined; + const peer = (await create({ title: "Initially unowned peer", type: "Chore" }, { pmRoot: root })).item; + const before = await bytes(root, id); + let introduced = false; + await runWithActiveExtensions({ path: root, noExtensions: true }, async () => { + setActiveExtensionHooks({ beforeCommand: [], afterCommand: [], onRead: [], onWrite: [], onIndex: [], beforeMutation: [{ layer: "project", name: "fixture-peer", run: async context => { + if (!introduced && context.before?.metadata.id === id) { introduced = true; await change(root, peer.id, { external_ref: "gh:o/r#7" }); } + } }] }); + await assert.rejects(() => plan ? resumeGithubImport(root, plan) : mutateGithubOwnedItem(root, { id, repo, number: 7 }, "update", { title: "Rejected title" }), /github_import_owner_conflict|claimed by/); + }); + assert.equal(introduced, true); + assert.deepEqual(await bytes(root, id), before); +}))); + +test("Ownership preflight removes a staged plan only when no SDK journal exists", async () => tracker(async (root, id) => github(id, async () => { + const items = (await listAllComplete({ includeBody: true }, { pmRoot: root })).items; + const entry = (await prepareGithubImport(source, repo, parseImportOptions({}), "fixture-token", items[0], undefined, { items, releases: [], timeline: async () => [] }))!; + let file = ""; let before: string[] = []; + await assert.rejects(() => importGithubAtomic(root, repo, [entry], { onPlanStaged: async context => { + file = context.file; await change(root, id, { external_ref: "gh:other/project#8" }); before = await bytes(root, id); + } }), error => refusal(error, shapes.find(shape => shape.name === "external-ref repository")!)); + assert.equal(existsSync(file), false); + assert.deepEqual(await listUnfinishedGithubImports(root), []); + assert.deepEqual(await bytes(root, id), before); + await change(root, id, { external_ref: "gh:o/r#7" }); + await sync(root, true); + assert.equal((await get(id, {}, { pmRoot: root })).item.title, source.title); +}))); + +test("Recovery-plan deletion flushes its directory through the durable-state policy", { skip: process.platform === "win32" || process.getuid?.() === 0 }, async () => tracker(async (root, id) => github(id, async () => { + const items = (await listAllComplete({ includeBody: true }, { pmRoot: root })).items; + const entry = (await prepareGithubImport(source, repo, parseImportOptions({}), "fixture-token", items[0], undefined, { items, releases: [], timeline: async () => [] }))!; + let file = ""; + try { + await assert.rejects(() => importGithubAtomic(root, repo, [entry], { onPlanStaged: context => { file = context.file; }, onTransition: context => { + if (context.transition === "committed") chmodSync(dirname(file), 0o300); + } }), { code: "EACCES" }); + assert.equal(existsSync(file), false, "Unlink succeeds before the directory flush refusal"); + assert.equal((await get(id, {}, { pmRoot: root })).item.title, source.title); + } finally { if (file) chmodSync(dirname(file), 0o700); } +}))); + +for (const legacy of [false, true]) test(`Ownership matrix closeout allows consistent ${legacy ? "legacy" : "full"} owner`, async () => tracker(async (root, id) => github(id, async () => { + if (legacy) await change(root, id, { tags: ["gh:o/r#7"] }, true); + const result = await runCloseout(root, repo, { pr: "1", apply: true }, { base: githubApiBase(), next: parseNextLink, get: url => fetchJSON(url, "fixture-token") }); + assert.equal(result.closed, 1); + assert.equal(result.plan[0]!.action, "close"); +}), false, legacy, legacy)); + +test("Ownership matrix locked closeout refuses a newly competing owner", async () => tracker(async (root, id) => github(id, async () => { + const peer = (await create({ title: "Initially unowned peer", type: "Chore" }, { pmRoot: root })).item; + const before = await bytes(root, id); + let introduced = false; + const result = await runCloseout(root, repo, { pr: "1", apply: true }, { base: githubApiBase(), next: parseNextLink, get: url => fetchJSON(url, "fixture-token") }, async phase => { + if (!introduced && phase === "locked") { introduced = true; await change(root, peer.id, { external_ref: "gh:o/r#7" }); } + }); + assert.equal(introduced, true); assert.equal(result.closed, 0); + assert.equal(result.plan[0]!.code, "github_import_owner_conflict"); + assert.match(result.plan[0]!.reason, /claimed by/); + assert.deepEqual(await bytes(root, id), before); +}), false, false)); + +test("Ownership matrix gated replay allows a consistent owner and remains idempotent", async () => tracker(async (root, id) => github(id, async () => { + await interrupt(root, id); + await sync(root, true, true); + const before = await bytes(root, id); + await sync(root, true, true); + assert.deepEqual(await bytes(root, id), before); + assert.equal((await listAllComplete({}, { pmRoot: root })).items.length, 1); +}), true)); + +test("Ownership matrix locked plain creation refuses a newly competing owner", async () => tracker(async (root, id) => github(id, async () => { + const peer = (await create({ title: "Initially unowned peer", type: "Chore" }, { pmRoot: root })).item; + const number = 9; + const destination = deriveAtomicItemId(repo, number, (await readSettings(root)).id_prefix, normalizeItemId); + let introduced = false; + await runWithActiveExtensions({ path: root, noExtensions: true }, async () => { + setActiveExtensionHooks({ beforeCommand: [], afterCommand: [], onRead: [], onWrite: [], onIndex: [], beforeMutation: [{ layer: "project", name: "fixture-peer", run: async context => { + if (!introduced && context.after?.metadata.id === destination) { introduced = true; await change(root, peer.id, { external_ref: "gh:o/r#9" }); } + } }] }); + await assert.rejects(() => mutateGithubOwnedItem(root, { id: destination, repo, number, allowCreate: true }, "create", { + title: "Rejected title", type: "Issue", description: "GH issue #9: https://github.com/o/r/issues/9", tags: "gh:o/r#9", + }), /github_import_owner_conflict|claimed by/); + }); + assert.equal(introduced, true); + await assert.rejects(() => get(destination, {}, { pmRoot: root }), /not found/); + assert.equal((await listAllComplete({}, { pmRoot: root })).items.length, 2); +}))); + +for (const atomic of [false, true]) test(`Creation proof: curated deterministic ID never grants mirroring (atomic=${atomic})`, async () => tracker(async (root, id) => github(id, async () => { + await change(root, id, { tags: ["gh:o/r#7", "keep-curated"] }); + const before = (await get(id, {}, { pmRoot: root })).item; + await sync(root, atomic); + const after = (await get(id, {}, { pmRoot: root })).item; + for (const field of ["title", "description", "type", "body", "created_at", "tags"] as const) assert.deepEqual(after[field], before[field], field); + assert.equal(existsSync(getItemPath(root, "Chore", id)), true); + assert.equal(after.github_state, "open"); + assert.equal((await listAllComplete({}, { pmRoot: root })).items.length, 1); +}), false, true, false)); + +for (const atomic of [false, true]) test(`Creation proof: importer author mirrors without a naming pattern (atomic=${atomic})`, async () => tracker(async (root, id) => github(id, async () => { + assert.doesNotMatch(id, /-github-/); + assert.equal((await get(id, {}, { pmRoot: root })).item.author, "pm-github"); + await sync(root, atomic); + const after = (await get(id, {}, { pmRoot: root })).item; + assert.equal(after.title, source.title); + assert.equal(after.body, source.body); + assert.equal(after.author, "pm-github"); + assert.equal((await listAllComplete({}, { pmRoot: root })).items.length, 1); +}), false, false, true)); + +test("Creation proof: an original mirroring plan refuses a late curated linked occupant", async () => tracker(async (root, id) => github(id, async () => { + const items = (await listAllComplete({ includeBody: true }, { pmRoot: root })).items; + const entry = (await prepareGithubImport(source, repo, parseImportOptions({}), "fixture-token", items[0], undefined, { items, releases: [], timeline: async () => [] }))!; + let before: string[] = []; + await assert.rejects(() => importGithubAtomic(root, repo, [entry], { onPlanStaged: async () => { + await change(root, id, { author: "fixture", title: "Curated after planning" }); before = await bytes(root, id); + } }), /importer creation provenance missing/); + assert.deepEqual(await bytes(root, id), before); +}))); + +test("PR tracker records permit arbitrary reference tags without ownership claims", async () => { + const ids = ["pm-github-blocker-format-followup", "pm-github-curated-reopen-followup", "pm-github-pr-url-alias-followup", + "pm-github-project-reference-followup", "pm-github-reference-identity-followup", "pm-github-reference-identity", "pm-github-search-reference-followup"]; + const root = join(import.meta.dirname, "../.agents/pm"); + for (const id of ids) { + const { item } = await get(id, {}, { pmRoot: root, noExtensions: true }); + assert.doesNotThrow(() => assertGithubOwnershipConsistency(item), id); + assert.deepEqual(githubOwnershipClaims({ ...item, tags: [...item.tags ?? [], "gh:unrelated/project#99"] }).claims, + githubOwnershipClaims(item).claims, `${id}: reference tags never constrain ownership`); + } + const references = await nativeDocs("pm-github-reference-identity-followup", {}, { pmRoot: root, noExtensions: true }); + assert.ok(references.docs.some(doc => doc.path === "https://github.com/unbraind/pm-github/pull/233")); +}); + +for (const atomic of [false, true]) test(`Creation proof: dedicated importer metadata mirrors without a naming pattern (atomic=${atomic})`, async () => tracker(async (root, id) => github(id, async () => { + await change(root, id, { github_import: true }); + assert.equal((await get(id, {}, { pmRoot: root })).item.author, "fixture"); + await sync(root, atomic); + const after = (await get(id, {}, { pmRoot: root })).item; + assert.equal(after.title, source.title); assert.equal(after.body, source.body); + assert.equal(after.github_import, true); +}), false, false, false)); + + +const extensionHarness = createExtensionTestHarness(extension, { capabilities: ["commands", "importers", "schema", "hooks", "preflight", "search"] }); + +for (const command of ["sync", "export"] as const) for (const shape of shapes) { + test(`Outbound ownership ${command}: refuses ${shape.name} without HTTP writes`, async () => tracker(async (root, id) => { + if (shape.peer) { + const peer = (await create({ title: "Other upstream claimant", type: "Chore" }, { pmRoot: root })).item; + await change(root, peer.id, shape.peer); + } else await change(root, id, shape.values!, shape.reset); + const before = await bytes(root, id); + const writes: string[] = []; + await withMockGithub((request, response) => { + if (request.method !== "GET") writes.push(`${request.method} ${request.url}`); + jsonResponse(response, 200, { ...source, state: "closed" }); + }, async () => { + const harness = await extensionHarness; + await assert.rejects(() => harness.runCommand({ command: `github ${command}`, options: { repo, ids: id, apply: true }, pmRoot: root, global: { json: true } }), error => refusal(error, shape)); + }); + assert.deepEqual(writes, []); + assert.deepEqual(await bytes(root, id), before); + })); +} + +for (const command of ["sync", "export"] as const) test(`Outbound ownership ${command}: allows consistent external-ref owner in place`, async () => tracker(async (root, id) => { + await change(root, id, { external_ref: "gh:O/R#7" }, true); + const writes: string[] = []; + await withMockGithub((request, response) => { + if (request.method !== "GET") writes.push(`${request.method} ${request.url}`); + jsonResponse(response, 200, { ...source, state: "closed" }); + }, async () => { + const harness = await extensionHarness; + await harness.runCommand({ command: `github ${command}`, options: { repo, ids: id, apply: true }, pmRoot: root, global: { json: true } }); + }); + assert.deepEqual(writes, ["PATCH /repos/o/r/issues/7"]); +})); + +test("Outbound ownership sync: rechecks claims after upstream read before PATCH", async () => tracker(async (root, id) => { + const writes: string[] = []; + let serverFailure: unknown; + await withMockGithub((request, response) => { + if (request.method !== "GET") { writes.push(`${request.method} ${request.url}`); jsonResponse(response, 200, source); return; } + void change(root, id, { github_repo: "other/project" }).then(() => jsonResponse(response, 200, { ...source, state: "closed" }), error => { + serverFailure = error; jsonResponse(response, 500, {}); + }); + }, async () => { + const harness = await extensionHarness; + await assert.rejects(() => harness.runCommand({ command: "github sync", options: { repo, ids: id }, pmRoot: root, global: { json: true } }), error => refusal(error, shapes[0]!)); + }); + assert.equal(serverFailure, undefined); + assert.deepEqual(writes, []); +})); + +/** Both endpoints and every competing claimant come from a real complete SDK metadata read. */ +async function dependencySource(root: string) { + const item = (await create({ author: "pm-github", title: "Dependent issue", type: "Issue" }, { pmRoot: root })).item; + await change(root, item.id, { github_url: "https://github.com/o/r/issues/9" }); + return { id: item.id, issue: { ...source, number: 9, html_url: "https://github.com/o/r/issues/9", body: "Blocked by #7" } }; +} + +for (const shape of shapes.filter(shape => !["repository versus URL", "number versus URL", "contradictory URL alias", "competing URL alias"].includes(shape.name))) test(`Dependency ownership: refuses ${shape.name} without an edge`, async () => tracker(async (root, id) => { + const dependent = await dependencySource(root); + if (shape.peer) { + const peer = (await create({ title: "Other dependency owner", type: "Chore" }, { pmRoot: root })).item; + await change(root, peer.id, shape.peer); + } else await change(root, id, shape.values!, shape.reset); + const before = await bytes(root, dependent.id); + const result = await linkImportedDependencies(repo, [dependent.issue], root); + assert.equal(result.linked, 0); + assert.match(result.failures.join("\n"), /ambiguous_github_dependency_owner: o\/r#7/); + assert.deepEqual(await bytes(root, dependent.id), before); +})); + +test("Dependency ownership: allows one consistent external-ref owner", async () => tracker(async (root, id) => { + const dependent = await dependencySource(root); + await change(root, id, { external_ref: "https://www.github.com/O/R/pull/7/?q=1#fragment" }, true); + const result = await linkImportedDependencies(repo, [dependent.issue], root); + assert.equal(result.linked, 1, JSON.stringify(result)); + assert.deepEqual(result.failures, []); + assert.ok((await get(dependent.id, {}, { pmRoot: root })).item.dependencies!.some(dependency => dependency.id === id)); +})); + +test("Dependency ownership: rechecks endpoint uniqueness inside the source mutation lock", async () => tracker(async (root, id) => { + const dependent = await dependencySource(root); + const peer = (await create({ title: "Late dependency claimant", type: "Chore" }, { pmRoot: root })).item; + const before = await bytes(root, dependent.id); + let introduced = false; + await runWithActiveExtensions({ path: root, noExtensions: true }, async () => { + setActiveExtensionHooks({ beforeCommand: [], afterCommand: [], onRead: [], onWrite: [], onIndex: [], beforeMutation: [{ layer: "project", name: "late-dependency-owner", run: async context => { + if (!introduced && context.before?.metadata.id === dependent.id) { + introduced = true; + await change(root, peer.id, { external_ref: "gh:o/r#7" }); + } + } }] }); + const result = await linkImportedDependencies(repo, [dependent.issue], root); + assert.equal(introduced, true); + assert.equal(result.linked, 0); + assert.match(result.failures.join("\n"), /ambiguous_github_dependency_owner/); + }); + assert.deepEqual(await bytes(root, dependent.id), before); +})); + + +/** The remaining operational indexes share the exact corpus and claims used by import and export. */ +function selectorChecks(items: PmItem[], id: string) { + const item = items.find(item => item.id === id)!; + const board = [{ id: "fixture-board-item", statusName: "Done", content: { typename: "Issue" as const, title: "Remote", repo, number: 7 } }]; + return { + refresh: () => selectRefreshTargets(items, repo), + search: () => indexByProvenance(items), + projects: () => indexPmByIssue(items), + "project push": () => buildProjectPushPlan(items, { owner: "o", number: 1 }, board, undefined), + "project pull": () => buildProjectPullPlan(items, { owner: "o", number: 1 }, board), + "project import": () => buildProjectImportPlan(board, { owner: "o", number: 1 }, items), + "blocker self-exclusion": () => collectBlockerReferences({ ...item, dependencies: [], comments: [], blockedReason: "Blocked by #8" } as BlockerPmItem, repo, items), + }; +} + +for (const shape of shapes) test(`Ownership routing inventory: refuses ${shape.name} in every operational index`, async () => tracker(async (root, id) => { + if (shape.peer) { + const peer = (await create({ title: "Other index claimant", type: "Chore" }, { pmRoot: root })).item; + await change(root, peer.id, shape.peer); + } else await change(root, id, shape.values!, shape.reset); + const items = (await listAllComplete({ includeBody: true }, { pmRoot: root })).items; + for (const [name, run] of Object.entries(selectorChecks(items, id))) { + assert.throws(run, error => { try { return refusal(error, shape); } catch (failure) { throw new Error(`${name}: ${String(failure)}`); } }, name); + } +})); + +test("Ownership routing inventory: consistent external-ref owner and reference-only items remain distinct", async () => tracker(async (root, id) => { + await change(root, id, { external_ref: "gh:O/R#7" }, true); + const reference = (await create({ title: "Mention", type: "Chore", tags: "gh:o/r#7" }, { pmRoot: root })).item; + const items = (await listAllComplete({ includeBody: true }, { pmRoot: root })).items; + assert.equal(selectRefreshTargets(items, repo)[0]!.id, id); + assert.equal(indexByProvenance(items).get("o/r#7")!.id, id); + assert.equal(indexPmByIssue(items).get("o/r#7")!.id, id); + const board = [{ id: "fixture-board-item", statusName: "Done", content: { typename: "Issue" as const, title: "Remote", repo, number: 7 } }]; + const push = buildProjectPushPlan(items, { owner: "o", number: 1 }, board, undefined).entries; + assert.equal(push.find(entry => entry.pmId === id)!.action, "noop"); + assert.equal(push.find(entry => entry.pmId === reference.id)!.action, "add-draft"); + assert.equal(buildProjectPullPlan(items, { owner: "o", number: 1 }, board).entries[0]!.pmId, id); + assert.equal(buildProjectImportPlan(board, { owner: "o", number: 1 }, items)[0]!.pmId, id); +})); + +test("Outbound ownership export: the public executor revalidates its original ownership context", async () => tracker(async (root, id) => { + const items = (await listAllComplete({ includeBody: true }, { pmRoot: root })).items; + const plan = buildExportPlan(items, repo); + items[0]!.github_repo = "other/project"; + await withMockGithub((_request, response) => jsonResponse(response, 200, source), async server => { + await assert.rejects(() => applyExportPlan(plan, repo, "fixture-token", (method, url, token, body) => fetch(url, { method, headers: { authorization: `Bearer ${token}` }, body })), error => refusal(error, shapes[0]!)); + assert.deepEqual(server.requests, []); + }); +})); + +test("Refresh ownership kind: every supported URL owner excludes PRs from issue queries", async () => tracker(async (root, id) => { + for (const values of [{ external_ref: "https://github.com/o/r/pull/7" }, { GitHubURL: "https://github.com/o/r/pull/7" }]) { + await change(root, id, values, true); + const items = (await listAllComplete({ includeBody: true }, { pmRoot: root })).items; + assert.deepEqual(selectRefreshTargets(items, repo), []); + await change(root, id, JSON.parse(JSON.stringify(values).replaceAll("/pull/", "/issues/")) as Record, true); + const issues = (await listAllComplete({ includeBody: true }, { pmRoot: root })).items; + assert.equal(selectRefreshTargets(issues, repo)[0]!.id, id); + } +})); + + +for (const conflict of [false, true]) test(`Ownership sync hint uses shared claims (${conflict ? "conflict" : "consistent external-ref"})`, async () => tracker(async (root, id) => { + await change(root, id, conflict ? { github_repo: "other/project", github_url: source.html_url } : { external_ref: "gh:o/r#7" }, true); + await withEnv({ PM_GITHUB_SYNC: "1" }, async () => { + const harness = await extensionHarness; + const { stderr } = await captureStderr(() => harness.runHook({ kind: "after_command", context: { command: "update", args: [id], pm_root: root, ok: true } })); + assert.match(stderr.join("\n"), conflict ? /github_import_owner_conflict/ : /pm github sync --repo o\/r/); + if (conflict) assert.doesNotMatch(stderr.join("\n"), /is linked to/); + }); +})); + + +for (const atomic of [false, true]) test(`Reference contract curated Chore mentions another issue (${atomic})`, async () => tracker(async (root, id) => { + await change(root, id, { github_url: "https://github.com/o/r/issues/30", github_number: 30, github_repo: repo, github_link_only: true, tags: ["keep", "gh:o/r#7"] }, true); + const before = await bytes(root, id); + const original = (await get(id, {}, { pmRoot: root })).item; + await github(id, () => sync(root, atomic)); + const actual = (await get(id, {}, { pmRoot: root })).item; + assert.deepEqual(await bytes(root, id), before); + for (const key of ["title", "type", "tags", "created_at"] as const) assert.deepEqual(actual[key], original[key]); + assert.equal(actual.type, "Chore"); + assert.equal((await listAllComplete({}, { pmRoot: root })).items.length, 2); +}, false, false)); + +test("Reference contract two linked items share the incoming reference without a duplicate", async () => tracker(async (root, id) => { + await change(root, id, { github_url: "https://github.com/o/r/issues/30", github_link_only: true, tags: ["gh:o/r#7"] }, true); + const peer = (await create({ title: "Second curated Chore", type: "Chore", tags: "gh:o/r#7" }, { pmRoot: root })).item; + await change(root, peer.id, { github_url: "https://github.com/o/r/issues/31", github_link_only: true }); + const before = await bytes(root, id), other = await bytes(root, peer.id); + await github(id, () => sync(root, true)); + assert.deepEqual(await bytes(root, id), before); assert.deepEqual(await bytes(root, peer.id), other); + const items = (await listAllComplete({}, { pmRoot: root })).items; + assert.equal(items.length, 3); assert.equal(items.filter(item => item.github_number === 7).length, 1); +}, false, false)); + +test("Reference contract importer mirror keeps mirroring beside unrelated reference tags", async () => tracker(async (root, id) => { + await change(root, id, { tags: ["gh:o/r#30", "gh:other/project#31"] }); + await github(id, () => sync(root, true)); + assert.equal((await get(id, {}, { pmRoot: root })).item.title, source.title); + assert.equal((await listAllComplete({}, { pmRoot: root })).items.length, 1); +})); + +test("Reference contract legacy importer ID without source number resyncs idempotently", async () => tracker(async (root, id) => { + await change(root, id, { tags: ["gh:o/r#7"] }, true); + assert.equal((await get(id, {}, { pmRoot: root })).item.github_number, undefined); + await github(id, () => sync(root, true)); + const before = await bytes(root, id); + await github(id, () => sync(root, true)); + assert.deepEqual(await bytes(root, id), before); + assert.equal((await listAllComplete({}, { pmRoot: root })).items.length, 1); + await change(root, id, { tags: ["gh:o/r#99"] }, true); + await github(id, () => sync(root, true)); + assert.equal((await listAllComplete({}, { pmRoot: root })).items.length, 1, "The ID supplies the source number, never the reference tag"); +})); + +for (const fields of [ + { github_number: 7, tags: ["gh:o/r#7", "gh:other/project#7"] }, + { github_number: 7, tags: ["gh:o/r#8"] }, + { github_repo: "other/project", github_link_only: false, tags: ["gh:o/r#7"] }, + { github_repo: repo, github_number: 7, tags: ["gh:other/project#8"] }, +]) test(`Reference contract reference tags do not contradict partial or full fields (${JSON.stringify(fields)})`, async () => tracker(async (root, id) => { + await change(root, id, fields, true); + const items = (await listAllComplete({ includeBody: true }, { pmRoot: root })).items; + assert.doesNotThrow(() => assertGithubOwnershipConsistency(items[0]!, undefined, items)); + const outcome = await github(id, () => sync(root, true)); + assert.equal("identity_conflicts" in outcome.result, false, "References do not cause conflict receipts either"); +}, false, false)); + +/** Forty native items and an actual SDK transaction measure work without replacing persistence APIs. */ +test("Ownership cost native batch N=40 reads fresh peers at each separately locked write", async t => tracker(async (root, initial) => { + const targets: Array<{id:string;repo:string;number:number}> = []; + for (let number = 1; number <= 40; number++) { + const id = deriveAtomicItemId(repo, number, (await readSettings(root)).id_prefix, normalizeItemId); + if (id !== initial) await create({ id, author: "pm-github", title: `Source ${number}`, type: "Issue", tags: `gh:o/r#${number}` }, { pmRoot: root }); + targets.push({ id, repo, number }); + } + const work: GithubOwnershipWork = {}; + await withGithubOwnershipWork(work, () => withGithubImportOwners(root, targets, () => commitItemMutations({ + pmRoot: root, author: "pm-github", transactionId: "ownership-cost-40", mutations: targets.map(target => ({ op: "update" as const, id: target.id, options: { title: "Native SDK update" } })), + onTransition: async context => { + if (context.transition === "committing") for (const target of targets) await withGithubImportOwners(root, [target], async () => mutateItem({ + pmRoot: root, settings: await readSettings(root), id: target.id, author: "pm-github", op: "import", mutate: document => { + document.metadata.github_updated_at = source.updated_at; + return { changedFields: ["github_updated_at"] }; + }, + })); + }, + }))); + assert.ok(work.claimParses! <= 40 * (2 * 40 + 12), JSON.stringify(work)); + assert.equal(work.corpusReads, 1 + 2 * 40); + t.diagnostic(JSON.stringify(work)); +})); + +test("Ownership cost outbound N=40 remains quadratic with fresh target checks", async t => tracker(async (root, initial) => { + for (let number = 1; number <= 40; number++) { + const id = deriveAtomicItemId(repo, number, (await readSettings(root)).id_prefix, normalizeItemId); + if (id !== initial) await create({ id, author: "pm-github", title: `Source ${number}`, type: "Issue", tags: `gh:o/r#${number}` }, { pmRoot: root }); + } + const work: GithubOwnershipWork = {}; + await withMockGithub((request, response) => jsonResponse(response, 200, { ...source, state: "closed" }), async server => { + const harness = await extensionHarness; + await withGithubOwnershipWork(work, () => harness.runCommand({ command: "github sync", options: { repo }, pmRoot: root, global: { json: true } })); + assert.equal(server.requests.filter(request => request.method === "PATCH").length, 40); + }); + assert.ok(work.claimParses! <= 40 * 40 + 40 * 3, JSON.stringify(work)); + t.diagnostic(JSON.stringify(work)); +})); + +for (const file of ["settings.json", "derived-index.bin"]) test(`Preview complete byte snapshot rejects ${file} changes`, async () => { + await assert.rejects(() => withImportPreview(async root => { + const target = join(root, file); + writeFileSync(target, file === "settings.json" ? Buffer.concat([readFileSync(target), Buffer.from("\n")]) : Buffer.from([0, 255, 128])); + }), /Preview changed/); +}); +test("Preview complete byte snapshot allows read-only SDK settings", () => withImportPreview(async root => { await readSettings(root); })); + +test("API URL mentions group with web references but never become ownership", async () => tracker(async (root, id) => { + await change(root, id, { description: "See https://api.github.com/repos/o/r/issues/7", external_links: [], tags: [] }, true); + await create({title:"Web reference",type:"Chore",tags:"gh:o/r#7"},{pmRoot:root}); + const report = await exploreGithubIdentities(root, { identity: "gh:o/r#7" }); + assert.equal(report.identities, 1); + assert.ok(report.edges.some(edge => edge.item === id && edge.source === "prose" && edge.identity === "gh:o/r#7" && edge.strength === "mention")); + assert.ok(report.edges.every(edge => edge.strength === "mention")); + await change(root, id, { description: "See https://api.github.com/repos/o/r/issues/7/extra" }, true); + assert.equal((await exploreGithubIdentities(root, { identity: "gh:o/r#7" })).edges.some(edge=>edge.item===id), false); +}, false, false)); + + +test("Reference contract link-only owner 30 mentioning 31 stays untouched while 31 imports", async () => tracker(async (root, id) => { + await change(root, id, { github_repo: repo, github_number: 30, github_link_only: true, tags: ["gh:o/r#31"] }, true); + const before = await bytes(root, id); + const issue = { ...source, number: 31, html_url: "https://github.com/o/r/issues/31" }; + await withMockGithub((request, response) => jsonResponse(response, 200, request.url!.includes("/issues?") ? [issue] : []), () => sync(root, true)); + assert.deepEqual(await bytes(root, id), before); + assert.equal((await listAllComplete({}, { pmRoot: root })).items.length, 2); +}, false, false)); + + +test("Reference contract closeout ignores an unrelated repository hash in an importer naming pattern", async () => tracker(async (root, initial) => { + await change(root, initial, {}, true); + const id = deriveAtomicItemId("other/project", 8, (await readSettings(root)).id_prefix, normalizeItemId); + await create({ id, title: "Explicit owner with an unrelated ID", type: "Issue", author: "pm-github", tags: "gh:o/r#31" }, { pmRoot: root }); + await change(root, id, { github_url: source.html_url, github_repo: repo, github_number: 7 }); + await github(id, async () => { + const result = await runCloseout(root, repo, { pr: "1", apply: true }, { base: githubApiBase(), next: parseNextLink, get: url => fetchJSON(url, "fixture-token") }); + assert.equal(result.closed, 1); + }); +}, false, false)); diff --git a/test/project-sync.test.ts b/test/project-sync.test.ts index 92a7f944..27a0a8bd 100644 --- a/test/project-sync.test.ts +++ b/test/project-sync.test.ts @@ -71,6 +71,14 @@ function create(root: string, title: string, tags: string[] = [], status = "open return item.id; } +/** Establish ownership explicitly; ordinary fixture creation only stores its reference tags. */ +async function own(root: string, id: string, githubUrl: string): Promise { + await mutateItem({ pmRoot: root, id, settings: await readSettings(root), author: "fixture", op: "update", mutate: document => { + document.metadata.github_url = githubUrl; + return { changedFields: ["github_url"] }; + } }); +} + /** Serve board discovery and inventory, handing actual mutations to the caller. */ function board(nodes: BoardNode[], mutate?: MockGithubHandler, statusField = true): MockGithubHandler { return (req, res, body, base) => { @@ -106,7 +114,8 @@ test("project sync previews both directions without mutation, including unmapped const root = tracker(t); create(root, "Linked", [projectItemTag(REF, "linked")]); create(root, "Draft", [], "in_progress"); - create(root, "Issue", ["gh:acme/widgets#7"]); + const owned = create(root, "Issue", ["gh:acme/widgets#7"]); + await own(root, owned, "https://github.com/acme/widgets/issues/7"); create(root, "Unmapped", [projectItemTag(REF, "unknown")], "blocked"); const before = items(root); await withMockGithub(board([draft("linked"), draft("unknown", "Custom", "custom")]), async server => { @@ -121,6 +130,23 @@ test("project sync previews both directions without mutation, including unmapped assert.deepEqual(items(root), before); }); +for (const explicitOwner of [false, true]) test(`project sync keeps a valid reference-only item as a draft (explicit owner: ${explicitOwner})`, async t => { + const root = tracker(t); + const id = create(root, "Mentioned issue", ["gh:acme/widgets#7"]); + if (explicitOwner) await own(root, id, "https://github.com/acme/widgets/issues/7"); + const stored = (await sdkGet(id, {}, { pmRoot: root })).item; + assert.equal(stored.github_url, explicitOwner ? "https://github.com/acme/widgets/issues/7" : undefined); + const before = items(root); + await withMockGithub(board([]), async server => { + const { stderr } = await captureStderr(() => sync(root, { push: true })); + const plan = stderr.join("\n"); + assert.match(plan, explicitOwner ? /add issue acme\/widgets#7/ : /add draft/); + assert.doesNotMatch(plan, explicitOwner ? /add draft/ : /add issue/); + assert.equal(server.requests.length, 2); + }); + assert.deepEqual(items(root), before); +}); + test("project sync scopes previews, honors dry-run over apply, and rejects empty or unknown ids", async t => { const root = tracker(t); const id = create(root, "Scoped"); @@ -139,6 +165,7 @@ test("project sync applies draft and issue additions, status writes, and preserv const retained = ["keep", "area,api", '["json"]']; const draftId = create(root, "Draft", retained, "in_progress", ["--body", "Local body"]); const issueId = create(root, "Issue", ["gh:acme/widgets#7", ...retained]); + await own(root, issueId, "https://github.com/acme/widgets/issues/7"); const linkedId = create(root, "Linked", [projectItemTag(REF, "linked"), ...retained]); const calls: GraphqlCall[] = []; await withMockGithub(board([draft("linked")], (_req, res, body) => { @@ -243,6 +270,7 @@ test("project pull preserves canceled lifecycle metadata and reports a failed te test("project push continues after remote failures and fails when the entire batch fails", async t => { const root = tracker(t); const missing = create(root, "Missing issue", ["gh:acme/widgets#99"]); + await own(root, missing, "https://github.com/acme/widgets/issues/99"); create(root, "Rejected draft"); create(root, "No id draft"); const ok = create(root, "Good draft"); @@ -266,6 +294,7 @@ test("project push continues after remote failures and fails when the entire bat test("project push attaches an issue without a Status field and rejects a missing attachment id", async t => { const root = tracker(t); const id = create(root, "Issue", ["gh:acme/widgets#7"]); + await own(root, id, "https://github.com/acme/widgets/issues/7"); let omitId = true; await withMockGithub(board([], (_req, res, body) => { const call = JSON.parse(body) as GraphqlCall; @@ -325,7 +354,8 @@ test("project sync validates the token and reference before contacting GitHub", test("project sync previews additions without Status and a linked item without an option", async t => { const root = tracker(t); create(root, "Draft"); - create(root, "Issue", ["gh:acme/widgets#7"]); + const owned = create(root, "Issue", ["gh:acme/widgets#7"]); + await own(root, owned, "https://github.com/acme/widgets/issues/7"); create(root, "Linked", [projectItemTag(REF, "linked")]); await withMockGithub(board([{ id: "linked", content: { __typename: "DraftIssue" } }], undefined, false), async () => { const { stderr, result } = await captureStderr(() => sync(root, { push: true })); @@ -335,14 +365,14 @@ test("project sync previews additions without Status and a linked item without a }); }); -test("project sync reports a missing mutation id and malformed linked issue coordinates", async t => { +test("project sync reports missing mutation IDs without treating a malformed reference as ownership", async t => { const root = tracker(t); create(root, "Draft with missing remote id"); create(root, "Malformed issue", ["gh:broken#7"]); await withMockGithub(board([], (_req, res) => jsonResponse(res, 200, { data: { addProjectV2DraftIssue: { projectItem: {} } } })), async () => { const { stderr } = await captureStderr(() => assert.rejects(sync(root, { push: true, apply: true }), /failed/)); assert.match(stderr.join("\n"), /returned no item id/); - assert.match(stderr.join("\n"), /could not resolve node id for broken#7/); + assert.doesNotMatch(stderr.join("\n"), /could not resolve node id for broken#7/); }); }); @@ -368,7 +398,8 @@ test("project push preview names a previously unset Status option", async t => { test("project push refuses a missing identity from the real attachment response", async t => { const root = tracker(t); - create(root, "Linked issue", ["gh:acme/widgets#7"]); + const owned = create(root, "Linked issue", ["gh:acme/widgets#7"]); + await own(root, owned, "https://github.com/acme/widgets/issues/7"); await withMockGithub(board([{ fieldValueByName: { name: "Done", optionId: "done" }, content: { __typename: "Issue", number: 7, repository: { nameWithOwner: "acme/widgets" } } }], (_req, res, body) => { const query = (JSON.parse(body) as GraphqlCall).query; jsonResponse(res, 200, { data: query.includes("issueOrPullRequest") ? { repository: { issueOrPullRequest: { id: "issue" } } } : { addProjectV2ItemById: { item: {} } } }); @@ -400,6 +431,7 @@ test("project writers preserve 500 native comma atoms through import and sync wi }); for (const target of [imported, pushed]) await mutateItem({ pmRoot, settings, id: target.id, author: "fixture", op: "update", mutate: document => { document.metadata.tags = [...native, ...(target.id === imported.id ? ["gh:acme/widgets#7"] : [])]; + if (target.id === imported.id) document.metadata.github_url = "https://github.com/acme/widgets/issues/7"; return { changedFields: ["tags"] }; } }); const beforeImport = (await sdkGet(imported.id, {}, { pmRoot })).item; diff --git a/test/projects.test.ts b/test/projects.test.ts index 29221efb..591daf3c 100644 --- a/test/projects.test.ts +++ b/test/projects.test.ts @@ -77,7 +77,7 @@ test("push planning reconciles issue links, stale board links, missing metadata, const board: ProjectItem[] = [{ id: "issue", content: { typename: "Issue", title: "", repo: "acme/widgets", number: 7 } }]; const local = [ null, {}, { id: "default" }, - { id: "issue", tags: ["unrelated", "gh:acme/widgets#7"] }, + { id: "issue", github_url: "https://github.com/acme/widgets/issues/7", tags: ["unrelated", "gh:acme/widgets#7"] }, { id: "stale", tags: [projectItemTag({ owner: "acme", number: 2 }, "foreign"), projectItemTag(REF, "gone")] }, ]; const plan = buildProjectPushPlan(local, REF, board, undefined); @@ -87,12 +87,12 @@ test("push planning reconciles issue links, stale board links, missing metadata, assert.equal(plan.entries[1].action, "noop"); assert.equal(plan.entries[1].reason, "project has no Status field"); assert.equal(plan.entries[2].action, "add-draft"); - const unmapped = buildProjectPushPlan([{ id: "linked", status: "blocked", tags: ["gh:acme/widgets#7"] }], REF, board, STATUS_FIELD); + const unmapped = buildProjectPushPlan([{ id: "linked", status: "blocked", github_url: "https://github.com/acme/widgets/issues/7", tags: ["gh:acme/widgets#7"] }], REF, board, STATUS_FIELD); assert.equal(unmapped.entries[0].reason, 'pm status "blocked" has no matching board option'); }); test("pull planning uses issue provenance with missing local metadata and leaves absent statuses alone", () => { - const local = [{ id: "linked", tags: ["gh:acme/widgets#7"] }]; + const local = [{ id: "linked", github_url: "https://github.com/acme/widgets/issues/7", tags: ["gh:acme/widgets#7"] }]; const plan = buildProjectPullPlan(local, REF, [ { id: "issue", statusName: "Done", content: { typename: "Issue", title: "", repo: "ACME/widgets", number: 7 } }, { id: "no-status", content: { typename: "Issue", title: "", repo: "acme/widgets", number: 7 } }, @@ -106,7 +106,7 @@ test("import planning recovers untitled and canceled items using issue provenanc const plan = buildProjectImportPlan([ { id: "cancel", content: { typename: "Issue", title: "", repo: "acme/widgets", number: 7, state: "closed", stateReason: "not_planned" } }, { id: "blank", content: { typename: "DraftIssue", title: " " } }, - ], REF, [{ id: "linked", tags: ["gh:acme/widgets#7"] }]); + ], REF, [{ id: "linked", github_url: "https://github.com/acme/widgets/issues/7", tags: ["gh:acme/widgets#7"] }]); assert.equal(plan[0].action, "update"); assert.equal(plan[0].pmId, "linked"); assert.equal(plan[0].status, "canceled"); @@ -193,7 +193,7 @@ test("mapOptionNameToPmStatus classifies option names and skips unknowns", () => test("status and provenance helpers tolerate nullable API collection entries", () => { assert.equal(resolveOptionForStatus("open", [null, undefined, { id: "todo", name: "Todo" }])?.id, "todo"); - assert.equal(indexPmByIssue([null, undefined, { id: "pm-1", tags: ["gh:unbraind/pm-cli#1"] }]).size, 1); + assert.equal(indexPmByIssue([null, undefined, { id: "pm-1", github_url: "https://github.com/unbraind/pm-cli/issues/1", tags: ["gh:unbraind/pm-cli#1"] }]).size, 1); assert.equal(indexPmByProjectItem([null, undefined], REF).size, 0); assert.equal(indexProjectItemsByIssue([null, undefined]).size, 0); }); @@ -249,7 +249,7 @@ test("buildProjectPushPlan: already-in-sync linked item is a noop", () => { }); test("buildProjectPushPlan: issue-linked pm item attaches the existing issue", () => { - const pmItems = [{ id: "pm-1", title: "Real issue", status: "open", tags: ["gh:unbraind/pm-cli#42"] }]; + const pmItems = [{ id: "pm-1", title: "Real issue", status: "open", github_url: "https://github.com/unbraind/pm-cli/issues/42", tags: ["gh:unbraind/pm-cli#42"] }]; const plan = buildProjectPushPlan(pmItems, REF, [], STATUS_FIELD); const e = plan.entries[0]; assert.equal(e.action, "add-issue"); @@ -289,6 +289,28 @@ test("buildProjectPullPlan: linked item with diverging board status yields an up ); }); +test("buildProjectPushPlan: a copied selected item is still checked against its own GitHub owner", () => { + // Same identity class as the pull planner: a copy of a corpus record misses the states map. + const original = { id: "pm-1", title: "P", status: "closed", github_url: "https://github.com/o/r/issues/7", tags: [projectItemTag(REF, "PVTI_c")] }; + const copy = structuredClone(original); + const board: ProjectItem[] = [{ id: "PVTI_c", statusOptionId: "opt_todo", statusName: "Todo", content: { typename: "Issue", repo: "o/r", number: 8, title: "P" } }]; + assert.throws(() => buildProjectPushPlan([copy], REF, board, STATUS_FIELD, { corpus: [original] }), /owner|ownership|o\/r#7|o\/r#8/i); + // Must allow: the copy on its own issue plans a status change. + const own: ProjectItem[] = [{ ...board[0]!, content: { ...board[0]!.content, number: 7 } }]; + assert.ok(buildProjectPushPlan([copy], REF, own, STATUS_FIELD, { corpus: [original] }).entries.some(entry => entry.action === "set-status")); +}); + +test("buildProjectPullPlan: a copied selected item is still checked against its own GitHub owner", () => { + // The corpus holds the original; the selection passes a separately allocated copy, which misses the states map. + const original = { id: "pm-1", title: "P", status: "open", github_url: "https://github.com/o/r/issues/7", tags: [projectItemTag(REF, "PVTI_c")] }; + const copy = structuredClone(original); + const board: ProjectItem[] = [{ id: "PVTI_c", statusOptionId: "opt_done", statusName: "Done", content: { typename: "Issue", repo: "o/r", number: 8, title: "P" } }]; + assert.throws(() => buildProjectPullPlan([copy], REF, board, undefined, undefined, [original]), /owner|ownership|o\/r#7|o\/r#8/i); + // Must allow: the same copy on its own issue still pulls the board status. + const own: ProjectItem[] = [{ ...board[0]!, content: { ...board[0]!.content, number: 7 } }]; + assert.equal(buildProjectPullPlan([copy], REF, own, undefined, undefined, [original]).entries.length, 1); +}); + test("buildProjectPullPlan: unknown board status is skipped, not forced", () => { const id = "PVTI_q"; const projectItems: ProjectItem[] = [ @@ -345,7 +367,7 @@ test("buildProjectImportPlan: update MERGES new provenance tags with existing ta title: "Z", status: "open", // Pre-existing tags: a label + provenance from a prior `pm github import`. - tags: ["bug", "priority/high", "gh:unbraind/pm-cli#3", "github_author:octocat"], + github_url: "https://github.com/unbraind/pm-cli/issues/3", tags: ["bug", "priority/high", "gh:unbraind/pm-cli#3", "github_author:octocat"], }, ]; const plan = buildProjectImportPlan(projectItems, REF, existing); diff --git a/test/reference-import-identity.test.ts b/test/reference-import-identity.test.ts new file mode 100644 index 00000000..16a91dfe --- /dev/null +++ b/test/reference-import-identity.test.ts @@ -0,0 +1,181 @@ +/** Reference tags never transfer curated content ownership; all storage is real SDK storage. */ +import assert from "node:assert/strict"; +import test from "node:test"; +import { mkdtempSync, readFileSync, rmSync, existsSync } from "node:fs"; +import { delimiter, join } from "node:path"; +import { tmpdir } from "node:os"; +import { spawnSync } from "node:child_process"; +import { init, create, get, getItemPath, getHistoryPath, listAllComplete, commitImportedItem, readSettings } from "@unbrained/pm-cli/sdk"; +import { runImport, parseImportOptions, type GhIssue } from "../index.ts"; +import { selectRefreshTargets } from "../refresh.ts"; +import { exploreGithubIdentities } from "../identity.ts"; +import { captureStderr, withEnv } from "./helpers/mock-github-server.ts"; + +const repo = "o/r"; +const tag = "gh:o/r#7"; +const issue: GhIssue = { number: 7, title: "Source issue", body: "Source body", state: "open", labels: [{ name: "source-label" }], + assignee: null, milestone: null, user: null, created_at: "2025-01-01T00:00:00Z", updated_at: "2025-01-02T00:00:00Z", html_url: "https://github.com/o/r/issues/7" }; + +/** Initialize isolated SDK storage, including the supported non-git case. */ +async function tracker(git: boolean, run: (root: string) => Promise) { + const directory = mkdtempSync(join(tmpdir(), "reference-identity-")); + const root = join(directory, "tracker"); + try { + if (git) assert.equal(spawnSync("git", ["init", "-q"], { cwd: directory }).status, 0); + await init("fixture", { defaults: true, withPackages: false, agentGuidance: "skip" }, { pmRoot: root, cwd: directory, noExtensions: true }); + await withEnv({ PM_PATH: root, PM_AUTHOR: "fixture", PATH: join(import.meta.dirname, "../node_modules/.bin") + delimiter + process.env.PATH }, () => run(root)); + } finally { rmSync(directory, { recursive: true, force: true }); } +} +/** Mock transport only; planner, transactions, metadata, history and the gate use their actual implementations. */ +async function sync(root: string, source = issue, atomic = true, gate = true) { + return (await captureStderr(() => runImport(repo, root, parseImportOptions({ state: "all", atomic, gate }), { + resolveToken: () => undefined, fetchIssues: async () => [source], fetchIssueTimeline: async () => [], + }))).result as Record; +} +/** Capture current native bytes and history to prove zero-change compatibility. */ +async function bytes(root: string, id: string) { + const item = (await get(id, {}, { pmRoot: root })).item; + return [readFileSync(getItemPath(root, item.type!, id), "utf8"), readFileSync(getHistoryPath(root, id), "utf8")]; +} + +for (const atomic of [true, false]) { + test(`one reference-only Chore keeps all curated fields (${atomic})`, async () => tracker(true, async root => { + const local = (await create({ title: "Curated local Chore", type: "Chore", description: "Local description", body: "Local body", tags: `${tag},curated` }, { pmRoot: root })).item; + const before = await bytes(root, local.id); + const result = await sync(root, issue, atomic); + assert.deepEqual(await bytes(root, local.id), before); + assert.equal(result.imported, 1); + assert.ok(existsSync(getItemPath(root, "Chore", local.id))); + assert.equal((await get(local.id, {}, { pmRoot: root })).item.created_at, local.created_at); + })); + test(`two shared references import exactly once and do not collide (${atomic})`, async () => tracker(true, async root => { + for (const title of ["Local A", "Local B"]) await create({ title, type: "Task", tags: tag }, { pmRoot: root }); + const result = await sync(root, issue, atomic); + assert.equal(result.imported, 1); + const items = (await listAllComplete({}, { pmRoot: root, noExtensions: true })).items; + assert.equal(items.length, 3); + assert.equal(items.filter(item => item.github_number === 7).length, 1); + assert.equal((await sync(root, issue, atomic)).imported, 0); + assert.equal((await exploreGithubIdentities(root)).collisions, 0); + })); + test(`importer-created mirror still refreshes title beside reference items (${atomic})`, async () => tracker(true, async root => { + await sync(root, issue, atomic); + const imported = (await listAllComplete({}, { pmRoot: root })).items[0]!; + await create({ title: "Related delivery", type: "Chore", tags: tag }, { pmRoot: root }); + const result = await sync(root, { ...issue, title: "New source title" }, atomic); + assert.equal(result.updated, 1); + assert.equal((await get(imported.id, {}, { pmRoot: root })).item.title, "New source title"); + })); +} + +test("released legacy snapshot resyncs with zero changes beside shared references", async () => tracker(true, async root => { + // These are the exact native fields emitted by the released importer, not a plan made by the new code. + const local = (await create({ title: issue.title, type: "Issue", status: "open", tags: `${tag},source-label`, + description: `GH issue #7: ${issue.html_url} · created ${issue.created_at} · updated ${issue.updated_at}`, body: issue.body! }, { pmRoot: root })).item; + await commitImportedItem({ pmRoot: root, id: local.id, itemPath: getItemPath(root, "Issue", local.id), settings: await readSettings(root), + document: { metadata: { ...local, github_number: 7, github_url: issue.html_url, github_link_only: false, + github_state: "open", github_state_reason: "unspecified", github_created_at: issue.created_at, github_updated_at: issue.updated_at, + github_closed_at: "", github_close_reason: "", github_release: "", created_at: issue.created_at }, body: issue.body! }, + author: "fixture", message: "Released importer snapshot", conflictWarningPrefix: "fixture" }); + await create({ title: "Reference to old import", type: "Chore", tags: tag }, { pmRoot: root }); + const before = await bytes(root, local.id); + const result = await sync(root); + assert.equal(result.unchanged, true); + assert.equal(result.imported, 0); + assert.equal(result.updated, 0); + assert.deepEqual(await bytes(root, local.id), before); +})); + +for (const git of [true, false]) test(`explicit local link merges lifecycle while preserving curated content (git=${git})`, async () => tracker(git, async root => { + const local = (await create({ title: "Curated title", type: "Chore", description: "Keep description", body: "Keep body", tags: "keep" }, { pmRoot: root })).item; + await commitImportedItem({ pmRoot: root, id: local.id, itemPath: getItemPath(root, "Chore", local.id), settings: await readSettings(root), + document: { metadata: { ...local, github_url: issue.html_url }, body: "Keep body" }, author: "fixture", message: "Explicit link", conflictWarningPrefix: "fixture" }); + const source = { ...issue, state: "closed" as const, state_reason: "completed" as const, closed_at: "2027-01-01T00:00:00Z", updated_at: "2027-01-01T00:00:00Z" }; + const result = await sync(root, source, true, git); + const actual = (await get(local.id, {}, { pmRoot: root })).item; + for (const field of ["title", "description", "type", "created_at"] as const) assert.equal(actual[field], local[field]); + assert.equal(actual.body, "Keep body"); + assert.deepEqual(actual.tags, [tag, "keep"].sort()); + assert.equal(actual.status, "closed"); + assert.equal(actual.closed_at, source.closed_at); + assert.equal(actual.completed_at, source.closed_at); + assert.equal(((result.plan as Array<{ identity: { title_conflict?: boolean } }> | undefined)?.[0]?.identity.title_conflict) ?? !git, true); + assert.equal((await sync(root, source, true, git)).unchanged, true); + assert.equal(selectRefreshTargets([{ ...actual, tags: [tag, "gh:o/r#99"] }], repo)[0]?.number, 7); + assert.equal(selectRefreshTargets([actual], repo)[0]?.number, 7); +})); + +for (const fields of [ + { github_repo: "O/R", github_number: 7 }, + { github_repo: repo, github_number: 7 }, + { github_link_only: true, github_url: issue.html_url }, + { github_link_only: true, description: `GH issue #7: ${issue.html_url}` }, + { github_link_only: true, description: "GH issue #7: https://github.com/O/R/issues/7" }, +]) test(`legacy explicit provenance resolves its original item: ${Object.keys(fields).join(",")}`, async () => tracker(true, async root => { + const local = (await create({ title: "Legacy curated owner", type: "Chore", tags: tag }, { pmRoot: root })).item; + await commitImportedItem({ pmRoot: root, id: local.id, itemPath: getItemPath(root, "Chore", local.id), settings: await readSettings(root), + document: { metadata: { ...local, ...fields }, body: "Retain legacy context" }, author: "fixture", message: "Legacy link snapshot", conflictWarningPrefix: "fixture" }); + const result = await sync(root); + assert.equal(result.imported, 0); + assert.equal((await listAllComplete({}, { pmRoot: root })).items.length, 1); + const actual = (await get(local.id, {}, { pmRoot: root })).item; + assert.equal(actual.title, local.title); + assert.equal(actual.type, "Chore"); + assert.equal(actual.created_at, local.created_at); + assert.equal((await sync(root)).unchanged, true); +})); + +test("references on an already linked owner permit another import without rebinding", async () => tracker(true, async root => { + const local = (await create({ title: "Owner", type: "Chore", tags: `${tag},gh:o/r#99` }, { pmRoot: root })).item; + await commitImportedItem({ pmRoot: root, id: local.id, itemPath: getItemPath(root, "Chore", local.id), settings: await readSettings(root), + document: { metadata: { ...local, github_url: issue.html_url, github_number: 7, github_link_only: false }, body: "Local" }, author: "fixture", message: "Linked owner with references", conflictWarningPrefix: "fixture" }); + const source = { ...issue, number: 99, html_url: "https://github.com/o/r/issues/99" }; + const before = await get(local.id, {}, { pmRoot: root }); + const history = readFileSync(getHistoryPath(root, local.id), "utf8"); + await sync(root, source); + assert.equal((await get(local.id, {}, { pmRoot: root })).item_hash, before.item_hash); + assert.equal(readFileSync(getHistoryPath(root, local.id), "utf8"), history); + assert.equal((await listAllComplete({}, { pmRoot: root })).items.length, 2); +})); + +test("dependency references resolve importer identities beside related local references", async () => tracker(true, async root => { + await create({ title: "Related local work", type: "Chore", tags: tag }, { pmRoot: root }); + const dependent = { ...issue, number: 8, html_url: "https://github.com/o/r/issues/8", body: "Blocked by #7" }; + await captureStderr(() => runImport(repo, root, parseImportOptions({ atomic: true, gate: true, "link-deps": true }), { + resolveToken: () => undefined, fetchIssues: async () => [issue, dependent], fetchIssueTimeline: async () => [], + })); + const items = (await listAllComplete({}, { pmRoot: root })).items; + const target = items.find(item => item.github_number === 7)!; + const source = items.find(item => item.github_number === 8)!; + assert.equal(source.dependencies?.[0]?.id, target.id); + assert.equal(items.find(item => item.type === "Chore")!.dependencies?.length ?? 0, 0); +})); + +test("seeded curated-link previews agree with the actual SDK lifecycle projection", async () => { + let seed = 230; + for (let sample = 0; sample < 8; sample++) { + seed = (Math.imul(seed, 1664525) + 1013904223) >>> 0; + const closed = (seed & 4) !== 0; + const active = (seed & 16) !== 0; + await tracker(true, async root => { + const local = (await create({ title: `Curated ${seed}`, type: active ? "Chore" : "Task", status: active ? "in_progress" : "open", tags: `native-${seed}` }, { pmRoot: root })).item; + await commitImportedItem({ pmRoot: root, id: local.id, itemPath: getItemPath(root, local.type!, local.id), settings: await readSettings(root), + document: { metadata: { ...local, github_url: issue.html_url }, body: "Native body" }, author: "fixture", message: "Seed curated projection", conflictWarningPrefix: "fixture" }); + const source = { ...issue, title: `Source ${seed}`, state: closed ? "closed" as const : "open" as const, + state_reason: closed ? "completed" as const : null, closed_at: closed ? "2027-01-01T00:00:00Z" : null, updated_at: "2027-01-01T00:00:00Z" }; + const preview = (await captureStderr(() => runImport(repo, root, parseImportOptions({ atomic: true, gate: true, "dry-run": true }), { + resolveToken: () => undefined, fetchIssues: async () => [source], fetchIssueTimeline: async () => [], + }))).result as { plan: Array<{ status: string; created_at: string; closed_at?: string }> }; + const projected = preview.plan[0]!; + await sync(root, source); + const actual = (await get(local.id, {}, { pmRoot: root })).item; + assert.equal(actual.status, projected.status); + assert.equal(actual.created_at, projected.created_at); + assert.equal(actual.closed_at, projected.closed_at); + assert.equal(actual.title, local.title); + assert.equal(actual.type, local.type); + assert.equal(actual.body, "Native body"); + assert.equal((await sync(root, source)).unchanged, true); + }); + } +}); diff --git a/test/refresh.test.ts b/test/refresh.test.ts index 5b21d0f9..d433f1bd 100644 --- a/test/refresh.test.ts +++ b/test/refresh.test.ts @@ -1,15 +1,16 @@ /** Refresh acceptance uses actual initialized trackers and the real HTTP transport. */ +import { createImportMirror } from "./helpers/import-mirror.ts"; import assert from "node:assert/strict"; import test from "node:test"; import { mkdtempSync, readFileSync, readdirSync, rmSync, mkdirSync, cpSync, copyFileSync, symlinkSync, writeFileSync, existsSync, renameSync } from "node:fs"; import { tmpdir } from "node:os"; import { join, delimiter } from "node:path"; import { spawnSync } from "node:child_process"; -import { WorkspaceTransactionInterruptedError, create, update, get, comments, listAllComplete, readHistorySnapshot, getHistoryPath, deleteItem, readSettings, normalizeItemId, commitImportedItem, getItemPath, toImportTags } from "@unbrained/pm-cli/sdk"; +import { WorkspaceTransactionInterruptedError, create, update, get, comments, listAllComplete, readHistorySnapshot, getHistoryPath, deleteItem, readSettings, normalizeItemId, commitImportedItem, getItemPath, toImportTags, mutateItem } from "@unbrained/pm-cli/sdk"; import extension, { buildAtomicImportMutations, deriveAtomicItemId, importGithubAtomic, listUnfinishedGithubImports, parseImportOptions, prepareGithubImport, readGithubImportPlan, resumeGithubImport, - validateGithubImportPlan, type GithubImportPlanRecord, + validateGithubImportPlan, type GithubImportPlanRecord, type GhIssue, } from "../index.ts"; import { fetchImportTimeline } from "../import-fidelity.ts"; import { runRefresh, selectRefreshTargets, refreshChanges } from "../refresh.ts"; @@ -64,7 +65,7 @@ function transport(nodes: Record | null>, errors for (const atomic of [false, true]) { test(`apply then re-apply preserves every byte and history line without cache priming (${atomic})`, async () => tracker(async (_directory, pmRoot) => { - const linked = (await create({ title: "Stale", tags: "gh:fixture/repo#1,area,api" }, { pmRoot })).item; + const linked = (await createImportMirror({ title: "Stale", tags: "gh:fixture/repo#1,area,api" }, { pmRoot })).item; const upstream = node(); upstream.labels.nodes.push({ name: "second-label" }); upstream.labels.nodes.push(...["area,api", '["json"]', 'quote"tag', " É,ß "].map(name => ({ name }))); @@ -103,8 +104,8 @@ for (const atomic of [false, true]) { })); test(`refresh only linked items with zero creates, real dates, labels, audit and reopen (${atomic})`, async () => tracker(async (_directory, pmRoot) => { - const linked = (await create({ title: "Stale", type: "Task", tags: "gh:fixture/repo#1,old-label", body: "keep local body" }, { pmRoot })).item; - const other = (await create({ title: "Other repo", tags: "gh:fixture/other#2" }, { pmRoot })).item; + const linked = (await createImportMirror({ title: "Stale", type: "Task", tags: "gh:fixture/repo#1,old-label", body: "keep local body" }, { pmRoot })).item; + const other = (await createImportMirror({ title: "Other repo", tags: "gh:fixture/other#2" }, { pmRoot })).item; const unrelated = (await create({ title: "Unlinked", body: "See https://github.com/fixture/repo/issues/3" }, { pmRoot })).item; const otherBefore = (await get(other.id, {}, { pmRoot })).item; const unrelatedBefore = (await get(unrelated.id, {}, { pmRoot })).item; @@ -156,7 +157,7 @@ for (const atomic of [false, true]) { } test("GraphQL bot authors retain REST bot-tag spelling across repeated refreshes", async () => tracker(async (_directory, pmRoot) => { - const linked = (await create({ title: "Stale", tags: "gh:fixture/repo#1,github_author:fixture-bot[bot]" }, { pmRoot })).item; + const linked = (await createImportMirror({ title: "Stale", tags: "gh:fixture/repo#1,github_author:fixture-bot[bot]" }, { pmRoot })).item; const upstreams = new Map([[1, { ...node(), author: { login: "fixture-bot", __typename: "Bot" } }]]); await withMockGithub((...args) => transport({ i1: upstreams.get(1)! })(...args), async () => { const first = await runRefresh("fixture/repo", pmRoot, { atomic: true }); @@ -185,15 +186,14 @@ test("explicit fields select existing owners; mentions, wrong repos, malformed a { id: "text", github_number: 9, body: "https://github.com/fixture/repo/issues/9" }, {}, ], "fixture/repo").map(target => [target.id, target.number]), [["url", 2], ["fields", 3]]); for (const items of [ - [{ id: "a", tags: ["gh:fixture/repo#1", "gh:fixture/repo#2"] }], - [{ id: "a", tags: ["gh:fixture/repo#1"] }, { id: "b", github_url: "https://github.com/fixture/repo/issues/1" }], - [{ id: "a", tags: ["gh:fixture/repo#0"] }], + [{ id: "a", github_url: "https://github.com/fixture/repo/issues/1", github_repo: "fixture/repo", github_number: 2 }], + [{ id: "a", github_url: "https://github.com/fixture/repo/issues/1" }, { id: "b", github_url: "https://github.com/fixture/repo/issues/1" }], [{ id: "a", github_repo: "fixture/repo", github_number: Number.MAX_SAFE_INTEGER + 1 }], ]) assert.throws(() => selectRefreshTargets(items, "fixture/repo")); }); test("missing, transferred and forbidden issues have typed reports and no writes", async () => tracker(async (_directory, pmRoot) => { - for (let number = 1; number <= 6; number++) await create({ title: `Local ${number}`, tags: `gh:fixture/repo#${number}` }, { pmRoot }); + for (let number = 1; number <= 6; number++) await createImportMirror({ title: `Local ${number}`, tags: `gh:fixture/repo#${number}` }, { pmRoot }); const upstream = { i1: null, i2: { ...node(2), url: "https://github.com/fixture/other/issues/2" }, i3: null, i4: node(4), i5: { ...node(5), title: " " }, i6: { ...node(6), number: 77 } }; await withMockGithub(transport(upstream, [ @@ -217,7 +217,7 @@ test("100-item batches and limits only request selected linked numbers; since us const id = `${settings.id_prefix}batch-${number}`; await commitImportedItem({ pmRoot, id, itemPath: getItemPath(pmRoot, "Task", id), settings, document: { metadata: { id, title: `Local ${number}`, description: "", type: "Task", status: "open", priority: 2, - tags: [`gh:fixture/repo#${number}`], created_at: "2026-01-01T00:00:00Z", updated_at: "2026-01-01T00:00:00Z" }, body: "" }, + github_url: `https://github.com/fixture/repo/issues/${number}`, tags: [`gh:fixture/repo#${number}`], created_at: "2026-01-01T00:00:00Z", updated_at: "2026-01-01T00:00:00Z" }, body: "" }, author: "fixture", message: "Seed linked batch fixture", conflictWarningPrefix: "fixture" }); nodes[`i${number}`] = node(number); } @@ -239,7 +239,7 @@ test("100-item batches and limits only request selected linked numbers; since us test("update-only import refuses absent matches and cannot upsert a missing deterministic target", async () => tracker(async (_directory, pmRoot) => { const prefix = (await readSettings(pmRoot)).id_prefix; const id = deriveAtomicItemId("fixture/repo", 1, prefix, normalizeItemId); - const local = (await create({ id, title: "Linked", tags: "gh:fixture/repo#1" }, { pmRoot })).item; + const local = (await createImportMirror({ id, title: "Linked", tags: "gh:fixture/repo#1" }, { pmRoot })).item; const entry = (await prepareGithubImport({ number: 1, title: "New", body: null, state: "open", labels: [], assignee: null, milestone: null, created_at: "2026-01-01T00:00:00Z", updated_at: "2026-02-03T00:00:00Z", html_url: "https://github.com/fixture/repo/issues/1" }, "fixture/repo", parseImportOptions({}), "fixture", local, undefined, { items: [local], releases: [], timeline: fetchImportTimeline }))!; @@ -282,7 +282,7 @@ test("registered command executes against a real tracker and rejects usage befor })); test("label pagination is complete and cursor or API failures abort before any item writes", async () => tracker(async (_directory, pmRoot) => { - await create({ title: "Labels", tags: "gh:fixture/repo#1" }, { pmRoot }); + await createImportMirror({ title: "Labels", tags: "gh:fixture/repo#1" }, { pmRoot }); const issue = node(); issue.author = null; issue.labels.pageInfo = { hasNextPage: true, endCursor: "page-1" }; @@ -313,7 +313,7 @@ test("label pagination is complete and cursor or API failures abort before any i })); test("sparse, repository-level and pathless GraphQL failures remain typed and cannot mutate items", async () => tracker(async (_directory, pmRoot) => { - await create({ title: "Linked", tags: "gh:fixture/repo#1" }, { pmRoot }); + await createImportMirror({ title: "Linked", tags: "gh:fixture/repo#1" }, { pmRoot }); for (const [data, errors, reason] of [ [{ repository: {} }, undefined, "incomplete_response"], [{ repository: null }, undefined, "not_found_or_inaccessible"], @@ -363,7 +363,7 @@ test("refresh preserves an existing release when GitHub has no release evidence" const id = `${settings.id_prefix}release-owner`; await commitImportedItem({ pmRoot, id, itemPath: getItemPath(pmRoot, "Task", id), settings, document: { metadata: { id, title: "Release owner", description: "", type: "Task", status: "open", priority: 2, - tags: ["gh:fixture/repo#1"], created_at: "2026-01-01T00:00:00Z", updated_at: "2026-01-01T00:00:00Z", release: "v2026.9.1" }, body: "" }, + github_url: "https://github.com/fixture/repo/issues/1", tags: ["gh:fixture/repo#1"], created_at: "2026-01-01T00:00:00Z", updated_at: "2026-01-01T00:00:00Z", release: "v2026.9.1" }, body: "" }, author: "fixture", message: "Seed release fixture", conflictWarningPrefix: "fixture" }); await withMockGithub(transport({ i1: node() }), async () => { const receipt = await runRefresh("fixture/repo", pmRoot, { atomic: true }); @@ -469,7 +469,7 @@ for (const boundary of ["step_applied", "committing"]) { await assert.rejects(() => runRefresh("fixture/repo", pmRoot, { atomic: true }), new RegExp(`unfinished GitHub transaction ${transactionId}.*original plan is missing`)); writeFileSync(planFile, JSON.stringify({ ...JSON.parse(plan), transactionId: "github-import-wrong" })); - await assert.rejects(() => runRefresh("fixture/repo", pmRoot, { atomic: true }), /original plan is missing or mismatched/); + await assert.rejects(() => runRefresh("fixture/repo", pmRoot, { atomic: true }), /original plan is corrupt or unreadable/); writeFileSync(planFile, plan); const receipt = await runRefresh("fixture/repo", pmRoot, { atomic: true, limit: "1" }); assert.equal(JSON.parse(readFileSync(join(journalDir, `${transactionId}.json`), "utf8")).status, "committed"); @@ -564,8 +564,8 @@ test("persisted fidelity in every recovery plan is validated before any journal for (const atomic of [false, true]) { test(`fresh refresh validates every finalization projection before the first write (${atomic})`, async () => tracker(async (_directory, pmRoot) => { - await create({ title: "Stale first", tags: "gh:fixture/repo#1" }, { pmRoot }); - const local = (await create({ title: "Closed second", tags: "gh:fixture/repo#2", type: "Task" }, { pmRoot })).item; + await createImportMirror({ title: "Stale first", tags: "gh:fixture/repo#1" }, { pmRoot }); + const local = (await createImportMirror({ title: "Closed second", tags: "gh:fixture/repo#2", type: "Task" }, { pmRoot })).item; await commitImportedItem({ pmRoot, id: local.id, itemPath: getItemPath(pmRoot, "Task", local.id), settings: await readSettings(pmRoot), document: { metadata: { ...local, status: "closed", created_at: "2025-01-01T00:00:00Z", closed_at: "2025-02-01T00:00:00Z", completed_at: "2025-02-01T00:00:00Z" }, body: "" }, @@ -611,7 +611,10 @@ test("persisted fidelity accepts empty and nullable source values and legacy ent test("recovery refuses a different repository even when the selected item overlaps", async () => tracker(async (_directory, pmRoot) => { const record = interruptedPlan(pmRoot); const id = record.entries[0]!.match!.id!; - await update(id, { addTags: ["gh:fixture/other#1"] }, { pmRoot }); + const local = (await get(id, {}, { pmRoot })).item; + await commitImportedItem({ pmRoot, id, itemPath: getItemPath(pmRoot, local.type!, id), settings: await readSettings(pmRoot), + document: { metadata: { ...local, title: local.title!, description: "GH issue #1: https://github.com/fixture/other/issues/1", type: local.type!, status: local.status!, priority: local.priority ?? 2, created_at: local.created_at!, updated_at: local.updated_at!, tags: (local.tags ?? []).map(tag => tag === "gh:fixture/repo#1" ? "gh:fixture/other#1" : tag), github_repo: "fixture/other", github_url: "https://github.com/fixture/other/issues/1", github_number: 1 }, body: local.body ?? "" }, + author: "fixture", message: "Seed changed explicit repository", conflictWarningPrefix: "fixture" }); const before = snapshot(pmRoot); await assert.rejects(() => runRefresh("fixture/other", pmRoot, {}), new RegExp(`${record.transactionId}.*fixture/repo.*fixture/other`)); assert.deepEqual(snapshot(pmRoot), before); @@ -641,7 +644,7 @@ test("request failure after recovery preserves committed recovery writes as docu })); test("canceled update mutation carries the required reason only with a status change", async () => tracker(async (_directory, pmRoot) => { - const linked = (await create({ title: "Stale", tags: "gh:fixture/repo#1" }, { pmRoot })).item; + const linked = (await createImportMirror({ title: "Stale", tags: "gh:fixture/repo#1" }, { pmRoot })).item; const entry = (await prepareGithubImport({ number: 1, title: "Stale", body: null, state: "closed", state_reason: "not_planned", closed_at: "2026-02-02T00:00:00Z", labels: [], assignee: null, milestone: null, created_at: "2026-01-01T00:00:00Z", updated_at: "2026-02-03T00:00:00Z", html_url: "https://github.com/fixture/repo/issues/1" }, "fixture/repo", parseImportOptions({}), "fixture", linked, undefined, { items: [linked], releases: [], timeline: async () => [] }))!; @@ -682,6 +685,8 @@ for (const failure of ["interrupted", "inspection", "cleanup", "finalize"]) { const record = interruptedPlan(pmRoot); const planFile = join(pmRoot, "transactions/pm-github", `${record.transactionId}.json`); const sdkDir = join(pmRoot, "transactions/sdk"); + const nativeFile = getItemPath(pmRoot, String(record.entries[0]!.match!.type), record.entries[0]!.match!.id!); + const originalNative = readFileSync(nativeFile, "utf8"); await assert.rejects(() => resumeGithubImport(pmRoot, record, { onTransition: context => { if (context.transition === "committing" && ["interrupted", "inspection"].includes(failure)) { if (failure === "inspection") { @@ -713,7 +718,86 @@ for (const failure of ["interrupted", "inspection", "cleanup", "finalize"]) { await resumeGithubImport(pmRoot, record); assert.ok(!existsSync(planFile)); } else if (failure === "finalize") { - assert.ok(!existsSync(planFile), "a terminal journal cannot keep a sensitive orphan plan"); + assert.ok(existsSync(planFile), "a committed journal retains its original finalization plan until repair succeeds"); + assert.equal((await listUnfinishedGithubImports(pmRoot)).length, 0); + // Restore the missing native file, preserving all append-only SDK history. + writeFileSync(nativeFile, originalNative); + await resumeGithubImport(pmRoot, record); + assert.ok(!existsSync(planFile), "successful finalization removes the sensitive replay plan"); } else assert.ok(existsSync(join(planFile, "held")), "cleanup failure must not mask the transaction error"); })); } + +for (const dryRun of [true, false]) for (const journalPresent of [true, false]) test(`pending import creates are reported while unrelated refresh proceeds (preview: ${dryRun}, journal: ${journalPresent})`, async () => tracker(async (directory, originalRoot) => { + const linked = (await createImportMirror({ title: "Stale unrelated owner", tags: "gh:fixture/repo#2" }, { pmRoot: originalRoot })).item; + const restored = join(directory, "restored-tracker"); + cpSync(originalRoot, restored, { recursive: true }); + const pendingSource = { number: 1, title: "Pending issue", body: "Pending body", state: "open", labels: [], user: null, + assignee: null, milestone: null, created_at: "2026-01-01T00:00:00Z", updated_at: "2026-02-03T00:00:00Z", html_url: "https://github.com/fixture/repo/issues/1" } satisfies GhIssue; + const entry = (await prepareGithubImport(pendingSource, "fixture/repo", parseImportOptions({ atomic: true }), undefined, undefined, undefined, + { items: [], releases: [], timeline: async () => [] }))!; + await assert.rejects(() => importGithubAtomic(originalRoot, "fixture/repo", [entry], { onTransition: context => { + if (context.transition === "prepared") throw new WorkspaceTransactionInterruptedError("Before pending create"); + } }), /interrupted/); + const record = (await listUnfinishedGithubImports(originalRoot))[0]!; + const saved = (await readGithubImportPlan(originalRoot, record.transactionId))!; + const pendingId = saved.itemIds![0]![1]; + const pmRoot = journalPresent ? originalRoot : restored; + if (!journalPresent) { + // Restore the real producer plan with its native pre-transaction tracker snapshot. + // The producer file can survive independently of an SDK journal. + mkdirSync(join(restored, "transactions/pm-github"), { recursive: true }); + copyFileSync(join(originalRoot, "transactions/pm-github", `${record.transactionId}.json`), join(restored, "transactions/pm-github", `${record.transactionId}.json`)); + } + assert.equal((await listAllComplete({}, { pmRoot })).items.some(item => item.id === pendingId), false); + const planBytes = readFileSync(join(pmRoot, "transactions/pm-github", `${record.transactionId}.json`)); + const itemBefore = readFileSync(getItemPath(pmRoot, linked.type!, linked.id)); + const historyBefore = readFileSync(getHistoryPath(pmRoot, linked.id)); + await withMockGithub(transport({ i2: node(2) }), async () => { + const harness = await createExtensionTestHarness(extension, { capabilities: ["commands", "importers", "schema", "hooks", "preflight", "search"] }); + const result = await harness.runCommand({ command: "github refresh", args: ["fixture/repo"], options: { "dry-run": dryRun, limit: "1" }, pmRoot, global: { json: true } }); + const receipt = result.result as Awaited>; + assert.equal(receipt.selected, 1); assert.equal(receipt.resumed, 0); assert.equal(receipt.updated, 1); + assert.equal(receipt.deferred_imports![0]!.code, "github_import_pending_creates"); + assert.deepEqual(receipt.deferred_imports![0]!.item_ids, [pendingId]); + assert.deepEqual(receipt.deferred_imports![0]!.resume_argv, ["pm", "github", "import", "fixture/repo", "--atomic"]); + }); + assert.ok(existsSync(join(pmRoot, "transactions/pm-github", `${record.transactionId}.json`)), "Unrelated refresh must retain the pending-create plan"); + assert.deepEqual(readFileSync(join(pmRoot, "transactions/pm-github", `${record.transactionId}.json`)), planBytes); + assert.equal((await listAllComplete({}, { pmRoot })).items.some(item => item.id === pendingId), false); + if (dryRun) { + assert.deepEqual(readFileSync(getItemPath(pmRoot, linked.type!, linked.id)), itemBefore); + assert.deepEqual(readFileSync(getHistoryPath(pmRoot, linked.id)), historyBefore); + } else assert.equal((await get(linked.id, {}, { pmRoot })).item.title, "Upstream 2"); +})); + +for (const selected of [false, true]) test(`retained existing ownership loss respects refresh scope (${selected})`, async () => tracker(async (_directory, pmRoot) => { + const original = (await create({ title: "Curated owner", type: "Chore" }, { pmRoot })).item; + const settings = await readSettings(pmRoot); + await mutateItem({ pmRoot, id: original.id, settings, author: "fixture", op: "update", mutate: document => { + document.metadata.github_url = "https://github.com/fixture/repo/issues/1"; return { changedFields: ["github_url"] }; + } }); + const stored = (await get(original.id, {}, { pmRoot })).item; + const source = { number: 1, title: "Source", body: "Public", state: "open", labels: [], user: null, assignee: null, milestone: null, + created_at: "2026-01-01T00:00:00Z", updated_at: "2026-02-03T00:00:00Z", html_url: "https://github.com/fixture/repo/issues/1" } satisfies GhIssue; + const entry = (await prepareGithubImport(source, "fixture/repo", parseImportOptions({}), undefined, stored, undefined, + { items: [stored], releases: [], timeline: async () => [] }))!; + await assert.rejects(() => importGithubAtomic(pmRoot, "fixture/repo", [entry], { updateOnly: true, onTransition: context => { + if (context.transition === "committed") throw new WorkspaceTransactionInterruptedError("Retain committed plan"); + } }), /committed journal/); + await mutateItem({ pmRoot, id: original.id, settings, author: "fixture", op: "update", mutate: document => { + for (const key of Object.keys(document.metadata)) if (/^github/i.test(key)) delete document.metadata[key]; + return { changedFields: ["github_url"] }; + } }); + await createImportMirror({ title: "Other linked owner", tags: "gh:fixture/repo#2" }, { pmRoot }); + const before = [readFileSync(getItemPath(pmRoot, original.type!, original.id)), readFileSync(getHistoryPath(pmRoot, original.id))]; + await withMockGithub(transport({ i2: node(2) }), async () => { + if (selected) await assert.rejects(() => runRefresh("fixture/repo", pmRoot, {}, { numbers: [1] }), { code: "github_import_owner_conflict" }); + else { + const receipt = await runRefresh("fixture/repo", pmRoot, {}, { numbers: [2] }); + assert.equal(receipt.updated, 1); assert.equal(receipt.resumed, 0); + assert.equal(receipt.deferred_imports![0]!.code, "github_import_outside_refresh_scope"); + } + }); + assert.deepEqual([readFileSync(getItemPath(pmRoot, original.type!, original.id)), readFileSync(getHistoryPath(pmRoot, original.id))], before); +})); diff --git a/test/repeat-import-noop.test.ts b/test/repeat-import-noop.test.ts index e618f7ce..c937be50 100644 --- a/test/repeat-import-noop.test.ts +++ b/test/repeat-import-noop.test.ts @@ -5,7 +5,7 @@ import { mkdtempSync, readFileSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import test from "node:test"; -import { init, create, get, update, getHistoryPath, getItemPath, WorkspaceTransactionInterruptedError, setActiveExtensionRegistrations, listAllComplete, normalizeItemId } from "@unbrained/pm-cli/sdk"; +import { init, create, get, update, mutateItem, readSettings, getHistoryPath, getItemPath, WorkspaceTransactionInterruptedError, setActiveExtensionRegistrations, listAllComplete, normalizeItemId } from "@unbrained/pm-cli/sdk"; import { createExtensionTestHarness } from "@unbrained/pm-cli/sdk/testing"; import extension, { runImport, parseImportOptions, importGithubAtomic, prepareGithubImport, deriveAtomicItemId, type GhIssue, type GhComment, type AtomicImportOptions } from "../index.ts"; import { refreshChanges } from "../refresh.ts"; @@ -63,12 +63,16 @@ function assertSourceClosures(pmRoot: string, id: string, skip = 0, dates: reado test("mixed new and linked closed issues plus an open issue repeat with identical item and history bytes", async () => tracker(async pmRoot => { const linked = (await create({ title: "Previously linked", type: "Chore", status: "closed", closeReason: "Original closure", - completedAt: "2025-02-01T00:00:00Z", tags: `gh:${repo}#1` }, { pmRoot })).item; - await persistImportFidelity(pmRoot, linked.id, buildImportFidelity(issue(1), [], []), false); + completedAt: "2025-02-01T00:00:00Z", tags: `gh:${repo}#1`, description: `GH issue #1: https://github.com/${repo}/issues/1` }, { pmRoot })).item; + await mutateItem({ pmRoot, id: linked.id, settings: await readSettings(pmRoot), author: "fixture", op: "update", mutate: document => { + document.metadata.github_url = issue(1).html_url; + return { changedFields: ["github_url"] }; + } }); + await persistImportFidelity(pmRoot, linked.id, buildImportFidelity(issue(1), [], []), false, false, undefined, true); const issues = [issue(1), issue(2), issue(3, true), { ...issue(4), state: "open" as const, state_reason: null, closed_at: null }]; const first = await importing(pmRoot, issues); assert.equal(first.imported, 3); - assert.equal(first.updated, 1); + assert.equal(first.updated, 0, "curated local content already has the complete source lifecycle"); // Inspect every target via its normal provenance-aware plan, not result formatting. const items = (await listAllComplete({ includeBody: true }, { pmRoot, noExtensions: true })).items; const before = await Promise.all(items.map(item => bytes(pmRoot, item.id))); @@ -77,13 +81,17 @@ test("mixed new and linked closed issues plus an open issue repeat with identica assert.deepEqual(await Promise.all(items.map(item => bytes(pmRoot, item.id))), before); assert.equal(items.length, 4); for (const item of items) { + if (item.id === linked.id) { + for (const field of ["created_at", "closed_at", "completed_at"] as const) assert.equal(item[field], linked[field]); + continue; + } assert.equal(item.created_at, issue(1).created_at); if (item.status === "closed") { assert.equal(item.closed_at, issue(1).closed_at); assert.equal(item.completed_at, issue(1).closed_at); } } - assert.equal((await get(linked.id, {}, { pmRoot })).item.created_at, issue(1).created_at); + assert.equal((await get(linked.id, {}, { pmRoot })).item.created_at, linked.created_at); for (const item of items) assertSourceClosures(pmRoot, item.id, item.id === linked.id ? 1 : 0); })); diff --git a/test/review-import-identity.test.ts b/test/review-import-identity.test.ts new file mode 100644 index 00000000..a3f6f205 --- /dev/null +++ b/test/review-import-identity.test.ts @@ -0,0 +1,329 @@ +/** Review regressions use real isolated SDK trackers; only GitHub transport is supplied. */ +import assert from "node:assert/strict"; +import test from "node:test"; +import { mkdtempSync, rmSync, readFileSync, readdirSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join, delimiter } from "node:path"; +import { spawnSync } from "node:child_process"; +import { init, create, update, get, comments, commitImportedItem, readSettings, getItemPath, getHistoryPath, WorkspaceTransactionInterruptedError, listAllComplete } from "@unbrained/pm-cli/sdk"; +import extension, { runImport, parseImportOptions, importGithubAtomic, readGithubImportPlan, resumeGithubImport, listUnfinishedGithubImports, planSync, buildExportPlan, type GhIssue, type GhComment } from "../index.ts"; +import { createExtensionTestHarness } from "@unbrained/pm-cli/sdk/testing"; +import { importIdentityKeys } from "../import-fidelity.ts"; +import { selectRefreshTargets } from "../refresh.ts"; +import { captureStderr, withEnv, withMockGithub, jsonResponse } from "./helpers/mock-github-server.ts"; + +const repo = "fixture/review"; +const issue: GhIssue = { number: 7, title: "Remote title", body: "Remote body", state: "open", labels: [], user: null, assignee: null, milestone: null, + created_at: "2025-01-01T00:00:00Z", updated_at: "2025-02-01T00:00:00Z", html_url: `https://github.com/${repo}/issues/7` }; +/** Give every test isolated native files, history, journals and a pinned child CLI. */ +async function tracker(run: (root: string) => Promise, git = true) { + const dir = mkdtempSync(join(tmpdir(), "github-review-")); + const root = join(dir, "tracker"); + try { + if (git) assert.equal(spawnSync("git", ["init", "-q"], { cwd: dir }).status, 0); + await init("fixture", { defaults: true, withPackages: false, agentGuidance: "skip" }, { pmRoot: root, cwd: dir, noExtensions: true }); + await withEnv({ PM_PATH: root, PM_AUTHOR: "fixture", PATH: join(import.meta.dirname, "../node_modules/.bin") + delimiter + process.env.PATH }, () => run(root)); + } finally { rmSync(dir, { recursive: true, force: true }); } +} +/** Seed explicit provenance through the public SDK without relinquishing curated content. */ +async function linked(root: string, fields: Record = {}) { + const item = (await create({ title: "Curated Chore", type: "Chore", description: "Curated description", body: "Curated body", tags: "keep" }, { pmRoot: root })).item; + await commitImportedItem({ pmRoot: root, id: item.id, itemPath: getItemPath(root, "Chore", item.id), settings: await readSettings(root), + document: { metadata: { ...item, github_url: issue.html_url, ...fields }, body: "Curated body" }, author: "fixture", message: "Explicit linked fixture", conflictWarningPrefix: "fixture" }); + return (await get(item.id, {}, { pmRoot: root })).item; +} +/** Read the native item and immutable history together. */ +async function bytes(root: string, id: string) { + const item = (await get(id, {}, { pmRoot: root })).item; + return [readFileSync(getItemPath(root, item.type!, id), "utf8"), readFileSync(getHistoryPath(root, id), "utf8")]; +} + +for (const mode of ["body", "annotations", "both"] as const) for (const atomic of [false, true]) { + test(`curated side data receives new comments without replacing content (${mode}, atomic=${atomic})`, async () => tracker(async root => { + const owner = await linked(root); + let source = issue; + const incoming: GhComment[] = [{ id: 1, body: "First source note", created_at: "2025-02-01T00:00:00Z", user: { login: "fixture" } }]; + const sync = () => captureStderr(() => runImport(repo, root, parseImportOptions({ atomic, gate: true, "with-comments": mode === "body", "comments-mode": mode }), { + resolveToken: () => undefined, fetchIssues: async () => [source], fetchIssueTimeline: async () => [], fetchIssueComments: async () => incoming, + })); + await sync(); + source = { ...issue, updated_at: "2027-01-01T00:00:00Z", closed_at: "2027-01-01T00:00:00Z", state: "closed", state_reason: "completed" }; + incoming.push({ id: 2, body: "Second source note", created_at: source.updated_at, user: { login: "fixture" } }); + await sync(); + const actual = (await get(owner.id, {}, { pmRoot: root })).item; + for (const field of ["title", "type", "description", "body", "created_at"] as const) assert.equal(actual[field], owner[field]); + assert.deepEqual(actual.tags, [`gh:${repo}#7`, "keep"]); + assert.equal(actual.status, "closed"); + assert.equal(actual.closed_at, source.closed_at); + assert.equal(actual.github_state, "closed"); + assert.equal(actual.github_updated_at, source.updated_at); + const notes = (await comments(owner.id, {}, { pmRoot: root })).comments; + assert.equal(notes.length, 2); + assert.match(JSON.stringify(notes), /First source note/); + assert.match(JSON.stringify(notes), /Second source note/); + const before = await bytes(root, owner.id); + await sync(); + assert.deepEqual(await bytes(root, owner.id), before); + })); +} + +for (const git of [false, true]) for (const atomic of [false, true]) { + test(`contradictory explicit identities refuse before writes (git=${git}, atomic=${atomic})`, async () => tracker(async root => { + const owner = await linked(root, { github_repo: repo, github_number: 8 }); + const before = await bytes(root, owner.id); + const refusal = (error: unknown) => { + assert.equal((error as { code: string }).code, "github_identity_conflict"); + assert.match(String(error), /fixture\/review#7.*versus.*fixture\/review#8/); + assert.match(String(error), /Correct the explicit GitHub provenance/); + return true; + }; + assert.throws(() => importIdentityKeys(owner), refusal); + assert.throws(() => selectRefreshTargets([owner], repo), { code: "github_import_owner_conflict" }); + await assert.rejects(() => captureStderr(() => runImport(repo, root, parseImportOptions({ atomic, gate: true }), { + resolveToken: () => undefined, fetchIssues: async () => [issue], fetchIssueTimeline: async () => [], + })), error => { + assert.equal((error as { code: string }).code, "github_import_owner_conflict"); + assert.match(String(error), /fixture\/review#7.*versus.*fixture\/review#8/); + assert.match(String(error), /Restore the intended GitHub provenance/); + return true; + }); + assert.deepEqual(await bytes(root, owner.id), before); + assert.deepEqual(await listUnfinishedGithubImports(root), []); + }, git)); +} + +for (const url of ["HTTPS://GITHUB.COM/Fixture/Review/issues/7/", "https://github.com/FIXTURE/REVIEW/pull/7/?view=1#note"]) { + test(`agreeing normalized explicit identities remain compatible (${url.includes("pull") ? "pull" : "issues"})`, async () => tracker(async root => { + const owner = await linked(root, { github_url: url, GitHubURL: issue.html_url, github_repo: " FIXTURE/Review/ ", github_number: 7 }); + assert.deepEqual(importIdentityKeys(owner), [`${repo}#7`]); + await captureStderr(() => runImport(repo, root, parseImportOptions({}), { + resolveToken: () => undefined, fetchIssues: async () => [issue], fetchIssueTimeline: async () => [], + })); + const all = await listAllComplete({}, { pmRoot: root }); + assert.equal(all.items.length, 1); + assert.equal(all.items[0]!.id, owner.id); + }, false)); +} + +test("two URL fields cannot disagree even without a number field", async () => tracker(async root => { + const owner = await linked(root, { GitHubURL: `https://github.com/${repo}/pull/8` }); + const before = await bytes(root, owner.id); + await assert.rejects(() => captureStderr(() => runImport(repo, root, parseImportOptions({ gate: true }), { + resolveToken: () => undefined, fetchIssues: async () => [issue], fetchIssueTimeline: async () => [], + })), { code: "github_import_owner_conflict" }); + assert.deepEqual(await bytes(root, owner.id), before); +})); + +for (const curated of [true, false]) for (const boundary of ["step_applied", "step_recorded", "committing", "committed"] as const) { + test(`atomic retry replays original input (${curated ? "curated" : "created"}, ${boundary})`, async () => tracker(async root => { + const owner = curated ? await linked(root) : undefined; + let polls = 0; + let source = issue; + let incoming: GhComment[] = [{ id: 1, body: "Original comment", created_at: issue.updated_at, user: { login: "fixture" } }]; + const deps = { resolveToken: () => undefined, fetchIssues: async () => { polls++; return [source]; }, fetchIssueTimeline: async () => [], fetchIssueComments: async () => incoming }; + const options = parseImportOptions({ atomic: true, "with-comments": true, "comments-mode": "annotations" }); + await assert.rejects(() => captureStderr(() => runImport(repo, root, options, { ...deps, + commitAtomic: (pmRoot, repository, entries) => importGithubAtomic(pmRoot, repository, entries, { onTransition: context => { + if (context.transition === boundary) throw new WorkspaceTransactionInterruptedError("Original plan crash fixture"); + } }), + })), /interrupted|committed journal/); + const pending = await listUnfinishedGithubImports(root); + assert.equal(pending.length, boundary === "committed" ? 0 : 1); + const savedNames = readdirSync(join(root, "transactions", "pm-github")); + assert.equal(savedNames.length, 1); + const transactionId = savedNames[0]!.slice(0, -5); + const recorded = await readGithubImportPlan(root, transactionId); + assert.ok(recorded); + assert.equal(recorded.entries[0]!.title, issue.title); + source = { ...issue, title: "Changed after crash", body: "Changed source body", labels: [{ name: "new-label" }], updated_at: "2027-01-01T00:00:00Z", state: "closed", closed_at: "2027-01-01T00:00:00Z", state_reason: "completed" }; + incoming = [{ id: 2, body: "Later comment", created_at: source.updated_at, user: { login: "fixture" } }]; + const { result: recovered } = await captureStderr(() => runImport(repo, root, options, deps)); + assert.ok("replayedOriginalPlan" in recovered); + assert.equal(recovered.transactionId, transactionId); + assert.equal(recovered.replayedOriginalPlan, true); + assert.equal(polls, 1, "recovery must not poll changed input"); + const all = (await listAllComplete({ includeBody: true }, { pmRoot: root })).items; + assert.equal(all.length, 1); + const actual = all[0]!; + assert.equal(actual.title, owner?.title ?? issue.title); + assert.equal(actual.github_state, "open"); + assert.equal(actual.github_updated_at, issue.updated_at); + if (owner) { assert.equal(actual.type, "Chore"); assert.equal(actual.body, "Curated body"); } + const notes = (await comments(actual.id, {}, { pmRoot: root })).comments; + assert.equal(notes.length, 1); + assert.match(JSON.stringify(notes), /Original comment/); + assert.doesNotMatch(JSON.stringify(notes), /Later comment/); + assert.deepEqual(await listUnfinishedGithubImports(root), []); + assert.deepEqual(readdirSync(join(root, "transactions", "pm-github")), []); + await captureStderr(() => runImport(repo, root, options, deps)); + assert.equal(polls, 2); + const refreshed = (await get(actual.id, {}, { pmRoot: root })).item; + assert.equal(refreshed.github_state, "closed"); + assert.equal(refreshed.title, owner?.title ?? source.title); + assert.equal((await comments(actual.id, {}, { pmRoot: root })).comments.length, 2); + }, false)); +} + +test("upstream plans ignore shared references and reject competing explicit owners", async () => tracker(async root => { + const reference = (await create({ title: "Reference Chore", type: "Chore", tags: `gh:${repo}#7` }, { pmRoot: root })).item; + const owner = await linked(root); + const corpus = (await listAllComplete({ includeBody: true }, { pmRoot: root })).items; + assert.deepEqual(planSync(corpus, repo).map(entry => entry.id), [owner.id]); + assert.deepEqual(buildExportPlan([reference], repo).map(entry => entry.action), ["create"]); + assert.deepEqual(buildExportPlan([owner], repo).map(entry => [entry.action, entry.number]), [["update", 7]]); + assert.deepEqual(planSync([reference], repo, undefined, { allowReferenceTags: true }).map(entry => entry.number), []); + assert.equal(buildExportPlan([reference], repo, undefined, undefined, { allowReferenceTags: true })[0]!.action, "create"); + assert.deepEqual(planSync([reference], repo, undefined, { allowReferenceTags: true, corpus }), []); + await linked(root); + const competing = (await listAllComplete({ includeBody: true }, { pmRoot: root })).items; + assert.throws(() => planSync([owner], repo, undefined, { corpus: competing }), { code: "github_import_owner_conflict" }); + assert.throws(() => buildExportPlan([owner], repo, undefined, undefined, { corpus: competing }), { code: "github_import_owner_conflict" }); +}, false)); + +test("sync and applied export send writes only to explicit owners", async () => tracker(async root => { + const reference = (await create({ title: "Reference Chore", type: "Chore", tags: `gh:${repo}#7` }, { pmRoot: root })).item; + const owner = await linked(root); + const harness = await createExtensionTestHarness(extension, { capabilities: ["commands", "importers", "schema", "hooks", "preflight", "search"] }); + await withEnv({ GITHUB_TOKEN: "fixture-token", GH_TOKEN: undefined }, () => withMockGithub((request, response) => { + jsonResponse(response, 200, { ...issue, number: request.method === "POST" ? 9 : 7, state: "closed" }); + }, async server => { + await captureStderr(() => harness.runCommand({ command: "github sync", options: { repo }, pmRoot: root, global: { json: true } })); + assert.deepEqual(server.requests.filter(request => request.method === "PATCH").map(request => request.url), [`/repos/${repo}/issues/7`]); + await captureStderr(() => harness.runCommand({ command: "github export", options: { repo, ids: owner.id, apply: true }, pmRoot: root, global: { json: true } })); + const patches = server.requests.filter(request => request.method === "PATCH"); + assert.equal(patches.length, 2); + assert.equal(JSON.parse(patches[1]!.body).title, "Curated Chore"); + await captureStderr(() => harness.runCommand({ command: "github export", options: { repo, ids: reference.id, apply: true }, pmRoot: root, global: { json: true } })); + assert.equal(server.requests.filter(request => request.method === "PATCH").length, 2, "a shared reference must not PATCH issue #7"); + assert.equal(server.requests.filter(request => request.method === "POST").length, 1, "explicitly exporting an unlinked item creates its own upstream issue"); + await linked(root); + const before = server.requests.length; + for (const command of ["github sync", "github export"]) await assert.rejects(() => harness.runCommand({ command, options: { repo, ids: owner.id, apply: true }, pmRoot: root, global: { json: true } }), { code: "github_import_owner_conflict" }); + assert.equal(server.requests.length, before, "an unselected competing owner must refuse before any remote request"); + })); +}, false)); + +test("atomic recovery replays dependency inputs for unchanged items too", async () => tracker(async root => { + let source = [issue, { ...issue, number: 8, title: "Dependent", body: "Blocked by #7", html_url: `https://github.com/${repo}/issues/8` }]; + const deps = { resolveToken: () => undefined, fetchIssues: async () => source, fetchIssueTimeline: async () => [] }; + await captureStderr(() => runImport(repo, root, parseImportOptions({ atomic: true }), deps)); + const ids = new Map((await listAllComplete({}, { pmRoot: root })).items.map(item => [item.github_number, item.id])); + assert.equal(((await get(ids.get(8)!, {}, { pmRoot: root })).item.dependencies ?? []).length, 0); + source = [{ ...issue, title: "Original retry target", updated_at: "2025-03-01T00:00:00Z" }, source[1]!]; + const options = parseImportOptions({ atomic: true, "link-deps": true }); + await assert.rejects(() => captureStderr(() => runImport(repo, root, options, { ...deps, + commitAtomic: (pmRoot, repository, entries) => importGithubAtomic(pmRoot, repository, entries, { linkDeps: true, dependencyIssues: source, + onTransition: context => { if (context.transition === "step_recorded") throw new WorkspaceTransactionInterruptedError("Dependency recovery fixture"); }, + }), + })), /interrupted/); + source = [source[0]!, { ...source[1]!, body: "Blocked by #9", updated_at: "2025-04-01T00:00:00Z" }]; + await captureStderr(() => runImport(repo, root, options, deps)); + const dependent = (await get(ids.get(8)!, {}, { pmRoot: root })).item; + assert.deepEqual((dependent.dependencies ?? []).map(edge => ({ id: edge.id, kind: edge.kind })), [{ id: ids.get(7)!, kind: "blocked_by" }]); + assert.equal(dependent.github_updated_at, issue.updated_at); +}, false)); + +test("released refresh plans without mode or target-map fields still replay", async () => tracker(async root => { + const owner = await linked(root); + await assert.rejects(() => captureStderr(() => runImport(repo, root, parseImportOptions({ atomic: true }), { + resolveToken: () => undefined, fetchIssues: async () => [issue], fetchIssueTimeline: async () => [], + commitAtomic: (pmRoot, repository, entries) => importGithubAtomic(pmRoot, repository, entries, { updateOnly: true, + onTransition: context => { if (context.transition === "step_recorded") throw new WorkspaceTransactionInterruptedError("Released refresh plan fixture"); }, + }), + })), /interrupted/); + const pending = (await listUnfinishedGithubImports(root))[0]!; + const record = (await readGithubImportPlan(root, pending.transactionId))!; + delete record.updateOnly; + delete record.itemIds; + delete record.linkDeps; + delete record.dependencyIssues; + writeFileSync(join(root, "transactions", "pm-github", `${record.transactionId}.json`), JSON.stringify(record)); + const recovered = await resumeGithubImport(root, record); + assert.equal(recovered.transactionId, pending.transactionId); + const actual = (await get(owner.id, {}, { pmRoot: root })).item; + assert.equal(actual.title, owner.title); + assert.equal(actual.github_number, 7); + assert.deepEqual(await listUnfinishedGithubImports(root), []); +}, false)); + +for (const corruption of ["missing", "unreadable", "mismatched"] as const) { + test(`atomic recovery refuses an unavailable original plan before writes (${corruption})`, async () => tracker(async root => { + const owner = await linked(root); + let polls = 0; + const deps = { resolveToken: () => undefined, fetchIssues: async () => { polls++; return [issue]; }, fetchIssueTimeline: async () => [] }; + const options = parseImportOptions({ atomic: true }); + await assert.rejects(() => captureStderr(() => runImport(repo, root, options, { ...deps, + commitAtomic: (pmRoot, repository, entries) => importGithubAtomic(pmRoot, repository, entries, { + onTransition: context => { if (context.transition === "step_recorded") throw new WorkspaceTransactionInterruptedError("Missing original plan fixture"); }, + }), + })), /interrupted/); + const pending = (await listUnfinishedGithubImports(root))[0]!; + const planFile = join(root, "transactions", "pm-github", `${pending.transactionId}.json`); + const original = readFileSync(planFile, "utf8"); + if (corruption === "missing") rmSync(planFile); + else writeFileSync(planFile, corruption === "unreadable" ? "{" : JSON.stringify({ ...JSON.parse(original), transactionId: "wrong-original-plan" })); + const before = await bytes(root, owner.id); + await assert.rejects(() => captureStderr(() => runImport(repo, root, options, deps)), error => { + assert.equal((error as { code: string }).code, corruption === "missing" ? "github_import_plan_missing" : "github_import_plan_corrupt"); + assert.match(String(error), /Restore its original plan/); + return true; + }); + assert.equal(polls, 1, "a missing original plan cannot become a new import from fresh input"); + assert.deepEqual(await bytes(root, owner.id), before); + writeFileSync(planFile, original); + await captureStderr(() => runImport(repo, root, options, deps)); + assert.equal(polls, 1); + assert.deepEqual(await listUnfinishedGithubImports(root), []); + }, false)); +} + +test("upstream preview and apply agree across native fields while legacy tag opt-in remains a no-op", async () => tracker(async root => { + const owner = await linked(root); + const references = await Promise.all(["First", "Second"].map(title => create({ title, type: "Chore", tags: `gh:${repo}#7` }, { pmRoot: root }))); + const harness = await createExtensionTestHarness(extension, { capabilities: ["commands", "importers", "schema", "hooks", "preflight", "search"] }); + let seed = 42; + await withEnv({ GITHUB_TOKEN: "fixture-token", GH_TOKEN: undefined }, () => withMockGithub((request, response) => jsonResponse(response, 200, { number: request.method === "POST" ? 9 : 7 }), async server => { + for (let iteration = 0; iteration < 8; iteration++) { + seed = (Math.imul(seed, 1664525) + 1013904223) >>> 0; + const allowReferenceTags = Boolean(seed & 1); + await update(owner.id, { title: `Generated owner ${seed}`, body: `Generated context ${seed}` }, { pmRoot: root }); + await update(references[0]!.item.id, { addTags: [`context-${seed}`] }, { pmRoot: root }); + const options = { repo, "allow-reference-tags": allowReferenceTags }; + const start = server.requests.length; + const command = (apply: boolean) => captureStderr(() => harness.runCommand({ command: "github export", options: { ...options, apply }, pmRoot: root, global: { json: true } })); + const preview = (await command(false)).result.result as { plan: Array<{ id: string; action: string; payload: { title: string; body: string; labels: string[]; state: string } }> }; + assert.deepEqual(preview.plan.filter(entry => entry.action === "update").map(entry => entry.id), [owner.id]); + await command(true); + const writes = server.requests.slice(start); + assert.equal(writes.length, 3); + for (const [index, entry] of preview.plan.entries()) { + const write = writes[index]!; + assert.equal(write.method, entry.id === owner.id ? "PATCH" : "POST"); + assert.deepEqual(JSON.parse(write.body), entry.id === owner.id ? entry.payload : { title: entry.payload.title, body: entry.payload.body, labels: entry.payload.labels }); + } + } + })); +}, false)); + +test("atomic recovery refuses altered original mutation payloads before writes", async () => tracker(async root => { + const owner = await linked(root); + const deps = { resolveToken: () => undefined, fetchIssues: async () => [issue], fetchIssueTimeline: async () => [] }; + const options = parseImportOptions({ atomic: true }); + await assert.rejects(() => captureStderr(() => runImport(repo, root, options, { ...deps, + commitAtomic: (pmRoot, repository, entries) => importGithubAtomic(pmRoot, repository, entries, { + onTransition: context => { if (context.transition === "step_recorded") throw new WorkspaceTransactionInterruptedError("Original payload fixture"); }, + }), + })), /interrupted/); + const pending = (await listUnfinishedGithubImports(root))[0]!; + const planFile = join(root, "transactions", "pm-github", `${pending.transactionId}.json`); + const original = readFileSync(planFile, "utf8"); + const changed = JSON.parse(original); + changed.mutations[0].options.title = "Altered original payload"; + writeFileSync(planFile, JSON.stringify(changed)); + const before = await bytes(root, owner.id); + await assert.rejects(() => captureStderr(() => runImport(repo, root, options, deps)), { code: "github_import_plan_drift" }); + assert.deepEqual(await bytes(root, owner.id), before); + writeFileSync(planFile, original); + await captureStderr(() => runImport(repo, root, options, deps)); + assert.equal((await get(owner.id, {}, { pmRoot: root })).item.title, owner.title); +}, false)); diff --git a/test/review-round3-identity.test.ts b/test/review-round3-identity.test.ts new file mode 100644 index 00000000..486b9126 --- /dev/null +++ b/test/review-round3-identity.test.ts @@ -0,0 +1,180 @@ +/** Round-three regressions use real SDK trackers; only GitHub transport is supplied. */ +import assert from "node:assert/strict"; +import test from "node:test"; +import { mkdtempSync, rmSync, readFileSync, readdirSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join, delimiter, relative } from "node:path"; +import { spawnSync } from "node:child_process"; +import { init, create, get, comments, listAllComplete, commitImportedItem, getItemPath, readSettings, WorkspaceTransactionInterruptedError } from "@unbrained/pm-cli/sdk"; +import { runImport, parseImportOptions, importGithubAtomic, readGithubImportPlan, listUnfinishedGithubImports, planSync, buildExportPlan, parseBlockerReferences, exploreGithubIdentities, prepareGithubImport, scanGithubImportPreflight, type GhIssue } from "../index.ts"; +import { importIdentityKeys, legacyGithubIdentityConflict } from "../import-fidelity.ts"; +import { normalizeGithubRepository, parseGithubIssueUrl, parseGithubUrl } from "../github-url.ts"; +import { selectRefreshTargets, runRefresh } from "../refresh.ts"; +import { captureStderr, withEnv, withMockGithub, jsonResponse } from "./helpers/mock-github-server.ts"; + +/** Isolate native documents, histories and journals, including in plain non-git directories. */ +async function tracker(run: (root: string) => Promise, git = false) { + const directory = mkdtempSync(join(tmpdir(), "github-round3-")); + const root = join(directory, "tracker"); + try { + if (git) assert.equal(spawnSync("git", ["init", "-q"], { cwd: directory }).status, 0); + await init("fixture", { defaults: true, withPackages: false, agentGuidance: "skip" }, { pmRoot: root, cwd: directory, noExtensions: true }); + await withEnv({ PM_PATH: root, PM_AUTHOR: "fixture", PM_TELEMETRY_SOURCE_CONTEXT: "automation", GITHUB_TOKEN: "fixture-token", GH_TOKEN: undefined, PATH: join(import.meta.dirname, "../node_modules/.bin") + delimiter + process.env.PATH }, () => run(root)); + } finally { rmSync(directory, { recursive: true, force: true }); } +} +/** Seed released metadata through the public SDK without mocking native persistence. */ +async function local(root: string, fields: Record) { + const item = (await create({ title: "Curated Chore", type: "Chore", description: "Curated description", body: "Curated body", tags: "keep" }, { pmRoot: root })).item; + await commitImportedItem({ pmRoot: root, id: item.id, itemPath: getItemPath(root, "Chore", item.id), settings: await readSettings(root), + document: { metadata: { ...item, ...fields }, body: "Curated body" }, author: "fixture", message: "Released identity fixture", conflictWarningPrefix: "fixture" }); + return (await get(item.id, {}, { pmRoot: root })).item; +} +/** Capture every document, history and journal, excluding only transient locks. */ +function snapshot(root: string) { + return Object.fromEntries(readdirSync(root, { recursive: true, withFileTypes: true }).filter(entry => entry.isFile()).map(entry => join(entry.parentPath, entry.name)) + .filter(file => !relative(root, file).startsWith("locks/")).sort().map(file => [relative(root, file), readFileSync(file, "utf8")])); +} +/** Supply an ordinary source issue with a canonical source repository identity. */ +function issue(repo = "fixture/issues"): GhIssue { + return { number: 7, title: "Remote title", body: "Remote body", state: "open", labels: [], user: null, assignee: null, milestone: null, + created_at: "2025-01-01T00:00:00Z", updated_at: "2025-02-01T00:00:00Z", html_url: `https://github.com/${repo}/issues/7` }; +} + +for (const repo of ["fixture/issues", "fixture/pull", "issues/widgets"]) for (const kind of ["issues", "pull"] as const) for (const form of ["plain", "decorated", "www"] as const) { + test(`URL segments retain owner/repository identity (${repo}, ${kind}, ${form})`, async () => tracker(async root => { + const url = `${form === "www" ? "HTTPS://WWW.GITHUB.COM" : "https://github.com"}/${repo}/${kind}/7${form === "decorated" ? "/?view=all#issuecomment-12" : ""}`; + const item = await local(root, { github_url: url, ...(form === "plain" ? {} : { github_number: 7 }) }); + assert.equal(parseGithubIssueUrl(url)?.key, `${repo}#7`); + assert.deepEqual(importIdentityKeys(item), [`${repo}#7`]); + assert.equal(selectRefreshTargets([item], repo).length, kind === "issues" ? 1 : 0); + assert.equal(planSync([item], repo)[0]!.number, 7); + assert.equal(buildExportPlan([item], repo)[0]!.action, "update"); + assert.equal(parseBlockerReferences(url)[0]!.repo, repo); + const graph = await exploreGithubIdentities(root, { identity: `gh:${repo}#7` }); + assert.ok(graph.edges.some(edge => edge.identity === `gh:${repo}#7` && edge.strength === "claim")); + await captureStderr(() => runImport(repo, root, parseImportOptions({}), { resolveToken: () => undefined, fetchIssues: async () => [issue(repo)], fetchIssueTimeline: async () => [] })); + const all = (await listAllComplete({ includeBody: true }, { pmRoot: root })).items; + assert.equal(all.length, 1); + assert.equal(all[0]!.id, item.id); + assert.equal(all[0]!.type, "Chore"); + assert.equal(all[0]!.title, item.title); + })); +} + +test("URL helper rejects non-GitHub and incomplete identities while preserving API segment positions", () => { + for (const url of ["https://github.com.evil.invalid/o/r/issues/7", "https://fixture@github.com/o/r/issues/7", "https://github.com/o/r/issues/7/extra", "https://github.com/o/r/issues/0", "https://github.com/o/r/pull", "https://github.com/o/r/issues/../issues/7"]) assert.equal(parseGithubIssueUrl(url), undefined); + const api = parseGithubIssueUrl("https://api.github.com/repos/issues/pull/issues/7/?query=1#note")!; + assert.equal(api.key, "issues/pull#7"); assert.equal(api.api, true); + assert.equal(parseGithubUrl("https://github.com/issues/pull/commit/abcd")!.kind, "commit"); +}); + +for (const mode of ["ambiguous", "single", "explicit"] as const) test(`number-only metadata and reference tags never promote or contradict an owner (${mode})`, async () => tracker(async root => { + const tags = mode === "single" ? ["keep", "gh:original/project#7"] : ["keep", "gh:original/project#7", "gh:unrelated/project#7"]; + const item = await local(root, { github_number: 7, tags, ...(mode === "explicit" ? { github_repo: "original/project" } : {}) }); + const before = snapshot(root); + const repo = mode === "single" ? "original/project" : "unrelated/project"; + const incoming = { ...issue(repo), state: "closed" as const, closed_at: "2027-01-01T00:00:00Z", updated_at: "2027-01-01T00:00:00Z", state_reason: "completed" as const }; + const run = () => captureStderr(() => runImport(repo, root, parseImportOptions({}), { resolveToken: () => undefined, fetchIssues: async () => [incoming], fetchIssueTimeline: async () => [] })); + const result = await run(); + assert.equal("identity_conflicts" in result.result, false); + const current = (await get(item.id, {}, { pmRoot: root })).item; + assert.equal(current.title, item.title); + assert.equal(current.status, item.status); + assert.equal(current.github_url, undefined); + for (const [path, value] of Object.entries(before)) if (path.endsWith(`${item.id}.toon`) || path === `history/${item.id}.jsonl`) assert.equal(snapshot(root)[path], value); + assert.equal((await listAllComplete({}, { pmRoot: root })).items.length, 2, "Only explicit provenance or a proven importer ID owns the incoming issue"); + assert.deepEqual(importIdentityKeys(current), mode === "explicit" ? ["original/project#7"] : []); + assert.equal(legacyGithubIdentityConflict(current), undefined); + +})); + +for (const mode of ["failed", "missing", "forged"] as const) test(`recovery preflight refuses before any write (${mode})`, async () => tracker(async root => { + const source = issue(); + // Synthetic credential shape is assembled only inside the disposable fixture. + const unsafe = { ...source, body: "fixture " + "ghp_" + "z".repeat(36) }; + const entry = (await prepareGithubImport(mode === "missing" ? source : unsafe, "fixture/issues", parseImportOptions({ atomic: true }), undefined, undefined, undefined, + { items: [], releases: [], timeline: async () => [] }))!; + if (mode === "failed") { + const initial = snapshot(root); + let enteredTransaction = false; + await assert.rejects(() => importGithubAtomic(root, "fixture/issues", [entry], { gate: true, onTransition: () => { enteredTransaction = true; } }), { code: "github_import_preflight_failed" }); + assert.equal(enteredTransaction, false); + assert.ok(JSON.stringify(snapshot(root)) === JSON.stringify(initial), "Fresh gated refusal must precede transaction and file writes"); + } + // Seed a real interrupted clean transaction, then model a previously persisted unsafe plan. + const clean = { ...entry, body: source.body ?? "", sourceBody: source.body ?? "" }; + await assert.rejects(() => importGithubAtomic(root, "fixture/issues", [clean], { onTransition: context => { + if (context.transition === "prepared") throw new WorkspaceTransactionInterruptedError("Before the first native mutation"); + } }), /interrupted/); + const pending = (await listUnfinishedGithubImports(root))[0]!; + const file = join(root, "transactions/pm-github", `${pending.transactionId}.json`); + const plan = (await readGithubImportPlan(root, pending.transactionId))!; + assert.equal(plan.preflight?.verdict, "pass"); + if (mode !== "missing") { + plan.entries[0]!.body = unsafe.body; + for (const mutation of plan.mutations) if (mutation.op === "create" || mutation.op === "update") mutation.options.body = unsafe.body; + plan.preflight = scanGithubImportPreflight(root, plan.entries, plan.mutations); + assert.equal(plan.preflight.verdict, "fail"); + } + if (mode === "missing") delete plan.preflight; + if (mode === "forged") plan.preflight = { verdict: "pass", scanned_files: 1, findings: 0, allowlisted: 0 }; + writeFileSync(file, JSON.stringify(plan)); + const before = snapshot(root); + let error: unknown; + try { await captureStderr(() => runImport("fixture/issues", root, parseImportOptions({ atomic: true, gate: true }), { fetchIssues: async () => { throw new Error("Recovery cannot poll fresh input"); } })); } + catch (caught) { error = caught; } + assert.ok(JSON.stringify(snapshot(root)) === JSON.stringify(before), "Recovery changed tracker files or history before privacy approval"); + assert.equal((error as { code?: string } | undefined)?.code, "github_import_preflight_failed"); + assert.equal((await listAllComplete({}, { pmRoot: root })).items.length, 0); +}, true)); + +test("passing saved preflight permits exact replay without git", async () => tracker(async root => { + const entry = (await prepareGithubImport(issue(), "fixture/issues", parseImportOptions({ atomic: true }), undefined, undefined, undefined, + { items: [], releases: [], timeline: async () => [] }))!; + await assert.rejects(() => importGithubAtomic(root, "fixture/issues", [entry], { onTransition: context => { + if (context.transition === "prepared") throw new WorkspaceTransactionInterruptedError("Clean preflight fixture"); + } }), /interrupted/); + const { result } = await captureStderr(() => runImport("fixture/issues", root, parseImportOptions({ atomic: true }), { fetchIssues: async () => { throw new Error("Original plan must replay"); } })); + assert.ok("replayedOriginalPlan" in result && result.replayedOriginalPlan); + assert.equal((await listAllComplete({}, { pmRoot: root })).items.length, 1); +})); + +for (const changed of [false, true]) test(`refresh recovers committed side data before fetching fresh input (${changed})`, async () => tracker(async root => { + const source = issue(); + const item = await local(root, { github_url: source.html_url }); + const entry = (await prepareGithubImport(source, "fixture/issues", parseImportOptions({ "comments-mode": "annotations" }), undefined, item, + async () => [{ id: 77, body: "Original planned annotation", created_at: source.updated_at, user: { login: "fixture" } }], + { items: [item], releases: [], timeline: async () => [] }))!; + await assert.rejects(() => importGithubAtomic(root, "fixture/issues", [entry], { updateOnly: true, onTransition: context => { + if (context.transition === "committed") throw new WorkspaceTransactionInterruptedError("Committed before side data"); + } }), /committed journal/); + assert.doesNotMatch(JSON.stringify((await comments(item.id, {}, { pmRoot: root })).comments), /Original planned annotation/); + const updatedAt = changed ? "2025-03-01T00:00:00Z" : source.updated_at; + await withMockGithub((request, response) => jsonResponse(response, 200, request.url === "/graphql" ? { data: { repository: { i7: { + number: 7, title: changed ? "New source title" : source.title, body: source.body, state: "OPEN", stateReason: null, + createdAt: source.created_at, updatedAt, closedAt: null, url: source.html_url, author: null, + labels: { nodes: [], pageInfo: { hasNextPage: false, endCursor: null } }, + } } } } : []), async () => { + const result = await runRefresh("fixture/issues", root, {}); + assert.equal((await comments(item.id, {}, { pmRoot: root })).comments.filter(note => JSON.stringify(note).includes("Original planned annotation")).length, 1); + assert.equal((await get(item.id, {}, { pmRoot: root })).item.github_updated_at, updatedAt); + assert.equal(result.resumed, 1); + assert.equal(result.updated, changed ? 1 : 0); + }); + assert.equal((await comments(item.id, {}, { pmRoot: root })).comments.filter(note => JSON.stringify(note).includes("Original planned annotation")).length, 1); + assert.equal((await get(item.id, {}, { pmRoot: root })).item.github_updated_at, updatedAt); + assert.deepEqual(readdirSync(join(root, "transactions/pm-github")), []); +})); + + +test("repository normalization handles adversarial suffixes while preserving released keys", () => { + assert.equal(normalizeGithubRepository(" FIXTURE/ISSUES" + "/".repeat(50000)), "fixture/issues"); + assert.equal(normalizeGithubRepository("FIXTURE/REPO,PART"), "fixture/repo,part"); + assert.equal(normalizeGithubRepository("fixture/issues/pull"), "fixture/issues/pull"); + // Bound the old quadratic failure in a child; the shared helper runs in linear time. + const helper = new URL("../github-url.ts", import.meta.url).href; + const result = spawnSync(process.execPath, ["--input-type=module", "-e", + `import { normalizeGithubRepository } from ${JSON.stringify(helper)}; const value = "fixture/issues" + "/".repeat(200000) + "x"; if (normalizeGithubRepository(value) !== value) process.exitCode = 1;`], + { timeout: 8000, encoding: "utf8" }); + assert.equal(result.status, 0, "Repository suffix normalization must finish within its bounded child process"); +}); diff --git a/test/round-eighteen.test.ts b/test/round-eighteen.test.ts new file mode 100644 index 00000000..f284d0eb --- /dev/null +++ b/test/round-eighteen.test.ts @@ -0,0 +1,266 @@ +/** Ownership races and defensive boundaries use native non-git SDK trackers. */ +import assert from "node:assert/strict"; +import test from "node:test"; +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { delimiter, join } from "node:path"; +import { createExtensionTestHarness } from "@unbrained/pm-cli/sdk/testing"; +import { init, create, get, listAllComplete, mutateItem, readSettings, getItemPath, getHistoryPath, comments, runWithActiveExtensions, setActiveExtensionHooks, serializeItemDocument } from "@unbrained/pm-cli/sdk"; +import extension, { applyPushEntry, syncGithubCommentsToAnnotations, prepareGithubImport, parseImportOptions, importGithubAtomic, listRetainedGithubImportPlans, verifyImportIdempotency, runImport, type PmItem, type GhComment, type GhIssue } from "../index.ts"; +import { assertCloseoutGithubOwner } from "../closeout.ts"; +import { selectRefreshTargets, runRefresh } from "../refresh.ts"; +import { parseLegacyGithubDescription, legacyGithubIdentityConflict, legacyGithubDescriptionIdentity, matchImportedIssue } from "../import-fidelity.ts"; +import { buildGithubOwnershipIndex, withGithubImportOwners, githubOwnershipClaims } from "../import-ownership.ts"; +import { buildProjectPushPlan, projectItemTag, type ProjectItem, type ProjectMeta } from "../projects.ts"; +import { withEnv, withMockGithub, jsonResponse, captureStderr } from "./helpers/mock-github-server.ts"; + +const repo = "o/r", issueUrl = "https://github.com/o/r/issues/7", ref = { owner: "o", number: 1 }; +const meta: ProjectMeta = { id: "BOARD_NODE", title: "Fixture board", url: "https://github.com/orgs/o/projects/1", ownerType: "organization", + statusField: { id: "STATUS_FIELD", name: "Status", options: [{ id: "TODO_OPTION", name: "Todo" }, { id: "DOING_OPTION", name: "In Progress" }] } }; +/** Isolate native data and prevent ancestor tracker discovery. */ +async function tracker(run: (root: string) => Promise) { + const directory = mkdtempSync(join(tmpdir(), "github-round-eighteen-")), root = join(directory, "tracker"); + try { + await init("fixture", { defaults: true, withPackages: false, agentGuidance: "skip" }, { cwd: directory, pmRoot: root, noExtensions: true }); + await withEnv({ PM_PATH: root, PM_AUTHOR: "fixture", PM_TELEMETRY_SOURCE_CONTEXT: "automation", GITHUB_TOKEN: "fixture", GH_TOKEN: undefined, + PATH: join(import.meta.dirname, "../node_modules/.bin") + delimiter + process.env.PATH }, () => run(root)); + } finally { rmSync(directory, { recursive: true, force: true }); } +} +/** Independent ordinary SDK writers deliberately do not acquire the package ownership lease. */ +async function change(root: string, id: string, fields: Record, unset: readonly string[] = []) { + await runWithActiveExtensions({ path: root, noExtensions: true }, async () => mutateItem({ pmRoot: root, id, settings: await readSettings(root), author: "fixture-peer", op: "update", mutate: document => { + for (const key of unset) delete document.metadata[key]; + for (const [key, value] of Object.entries(fields)) { if (key === "body") document.body = String(value); else document.metadata[key] = value; } return { changedFields: [...Object.keys(fields), ...unset] }; + } })); +} +/** Create and reread full provider metadata through the pinned SDK. */ +async function native(root: string, fields: Record = {}): Promise { + const item = (await create({ title: "Curated delivery", type: "Chore", body: "Local context", tags: "retained", status: "open" }, { pmRoot: root })).item; + await change(root, item.id, fields); + return (await listAllComplete({ includeBody: true }, { pmRoot: root, noExtensions: true })).items.find(row => row.id === item.id)!; +} +/** Refusals preserve authoritative item and history bytes after the peer's own change. */ +function bytes(root: string, item: PmItem) { return [getItemPath(root, String(item.type), item.id!), getHistoryPath(root, item.id!)].map(file => readFileSync(file, "hex")); } + +for (const clear of [false, true]) test(`board add-issue requires a current positive owner after node resolution (${clear})`, async () => tracker(async root => { + const item = await native(root, { github_url: issueUrl }); + const plan = buildProjectPushPlan([item], ref, [], meta.statusField).entries[0]!; + assert.equal(plan.action, "add-issue"); + let peerBytes = bytes(root, item); + await withMockGithub(async (_request, response, body) => { + const query = (JSON.parse(body) as { query: string }).query; + if (query.includes("issueOrPullRequest")) { + if (clear) { await change(root, item.id!, {}, ["github_url"]); peerBytes = bytes(root, item); } + jsonResponse(response, 200, { data: { repository: { issueOrPullRequest: { id: "ISSUE_NODE" } } } }); + } else if (query.includes("addProjectV2ItemById")) jsonResponse(response, 200, { data: { addProjectV2ItemById: { item: { id: "ATTACHED_NODE" } } } }); + else jsonResponse(response, 200, { data: { updateProjectV2ItemFieldValue: { projectV2Item: { id: "ATTACHED_NODE" } } } }); + }, async server => { + const result = await applyPushEntry(plan, meta, ref, new Map([[item.id!, item]]), root, "fixture", []); + if (clear) { + assert.equal(result.changed, false); assert.match(result.error!, /current owner.*curated\/unowned/); + assert.equal(server.requests.length, 1, "Only the read query may reach GitHub; no attachment or status mutation"); + assert.deepEqual(bytes(root, item), peerBytes); + assert.equal((await get(item.id!, {}, { pmRoot: root })).item.tags!.some(tag => tag.startsWith("gh-project:")), false); + } else { + assert.equal(result.changed, true, result.error ?? "Unexpected board refusal"); assert.equal(server.requests.length, 3); + assert.equal((await get(item.id!, {}, { pmRoot: root })).item.github_url, issueUrl); + assert.ok((await get(item.id!, {}, { pmRoot: root })).item.tags!.includes(projectItemTag(ref, "ATTACHED_NODE"))); + } + }); +})); + +for (const removeTag of [false, true]) test(`a zero-owner board status needs its current board tag (${removeTag})`, async () => tracker(async root => { + const tag = projectItemTag(ref, "EXISTING_NODE"), item = await native(root, { tags: ["retained", tag] }); + const board: ProjectItem[] = [{ id: "EXISTING_NODE", statusName: "In Progress", content: { typename: "Issue", title: "Existing issue", repo, number: 7 } }]; + const plan = buildProjectPushPlan([item], ref, board, meta.statusField).entries[0]!; assert.equal(plan.action, "set-status"); + if (removeTag) await change(root, item.id!, { tags: ["retained"] }); + const before = bytes(root, item); + await withMockGithub((_request, response) => jsonResponse(response, 200, { data: { updateProjectV2ItemFieldValue: { projectV2Item: { id: "EXISTING_NODE" } } } }), async server => { + const result = await applyPushEntry(plan, meta, ref, new Map([[item.id!, item]]), root, "fixture", board); + assert.equal(result.changed, !removeTag, result.error ?? "Unexpected board result"); + assert.equal(server.requests.length, removeTag ? 0 : 1); + if (removeTag) assert.match(result.error!, /missing current board tag/); + assert.deepEqual(bytes(root, item), before); + }); +})); + +test("zero-owner draft creation remains authorized without an issue link or existing board tag", async () => tracker(async root => { + const item = await native(root), plan = buildProjectPushPlan([item], ref, [], meta.statusField).entries[0]!; + assert.equal(plan.action, "add-draft"); + await withMockGithub((_request, response, body) => { + const query = (JSON.parse(body) as { query: string }).query; + jsonResponse(response, 200, { data: query.includes("addProjectV2DraftIssue") ? { addProjectV2DraftIssue: { projectItem: { id: "DRAFT_NODE" } } } + : { updateProjectV2ItemFieldValue: { projectV2Item: { id: "DRAFT_NODE" } } } }); + }, async server => { + const result = await applyPushEntry(plan, meta, ref, new Map([[item.id!, item]]), root, "fixture", []); + assert.equal(result.changed, true, result.error ?? "Unexpected board refusal"); assert.equal(server.requests.length, 2); + const after = (await listAllComplete({ includeBody: true }, { pmRoot: root })).items[0]!; + for (const field of ["title", "type", "created_at", "body"] as const) assert.equal(after[field], item[field]); + assert.ok(after.tags!.includes(projectItemTag(ref, "DRAFT_NODE"))); + }); +})); + +for (const competing of [false, true]) test(`each separately locked comment rereads ordinary SDK peer claims (${competing})`, async () => tracker(async root => { + const owner = await native(root, { github_url: issueUrl }), peer = await native(root); + const incoming: GhComment[] = [1, 2].map(id => ({ id, body: `Public comment ${id}`, created_at: "2025-02-01T00:00:00Z", user: { login: "fixture" } })); + let changed = false; + const outcome = await captureStderr(() => runWithActiveExtensions({ path: root, noExtensions: true }, async () => { + setActiveExtensionHooks({ beforeCommand: [], afterCommand: [], beforeMutation: [], onRead: [], onIndex: [], onWrite: [{ layer: "project", name: "ordinary-peer-between-comments", run: async context => { + if (changed || !competing || !context.path.endsWith(`${owner.id}.toon`)) return; + changed = true; await change(root, peer.id!, { external_ref: "gh:o/r#7" }); + } }] }); + return syncGithubCommentsToAnnotations(owner.id!, incoming, root, 7, { id: owner.id!, repo, number: 7 }); + })); + const stored = (await comments(owner.id!, {}, { pmRoot: root })).comments; + assert.equal(outcome.result.added, competing ? 1 : 2); assert.equal(outcome.result.failed, competing ? 1 : 0); + assert.equal(stored.length, competing ? 1 : 2); assert.match(stored[0]!.text!, /Public comment 1/); + if (competing) { assert.equal(changed, true); assert.ok(outcome.stderr.some(line => /multiple existing owners|current owner/.test(line))); } + else assert.match(stored[1]!.text!, /Public comment 2/); +})); + + +/** Provider rows remain synthetic inputs; preparation and every local write use the real SDK. */ +function source(): GhIssue { return { number: 7, title: "Remote source", body: "Public source", state: "open", labels: [], user: null, assignee: null, + milestone: null, created_at: "2025-01-01T00:00:00Z", updated_at: "2025-02-01T00:00:00Z", html_url: issueUrl }; } + +for (const number of ["0", "-1", "9007199254740993", "8", "7"]) test(`declared-number guard validates native declaration before ownership (${number})`, async () => tracker(async root => { + const item = await native(root, { description: `GH issue #${number}: ${issueUrl}` }), before = bytes(root, item); + const parsed = parseLegacyGithubDescription(item)!; + assert.equal(parsed.invalidNumber, number !== "7"); assert.equal(legacyGithubDescriptionIdentity(item)?.key, number === "7" ? "o/r#7" : undefined); + assert.deepEqual(githubOwnershipClaims(item).owners, number === "7" ? ["o/r#7"] : []); + assert.deepEqual(bytes(root, item), before); +})); + +for (const variant of ["unowned", "same", "foreign", "contradictory"] as const) test(`closeout ownership guards run on native claims (${variant})`, async () => tracker(async root => { + const fields = variant === "unowned" ? {} : { github_url: variant === "foreign" ? "https://github.com/other/project/issues/8" : issueUrl, + ...(variant === "contradictory" ? { external_ref: "gh:o/r#8" } : {}) }; + const item = await native(root, fields), before = bytes(root, item); + if (variant === "contradictory") { const state = githubOwnershipClaims(item); assert.equal(state.evidence, true); assert.equal(state.owners.length, 2); } + const run = () => assertCloseoutGithubOwner(item as Parameters[0], repo, buildGithubOwnershipIndex([item], [repo])); + if (variant === "unowned" || variant === "same") assert.doesNotThrow(run); + else assert.throws(run, error => { + assert.equal((error as { code: string }).code, "github_import_owner_conflict"); + assert.equal((error as { expected: string }).expected, "o/r (delivery repository)"); + assert.ok(String(error).includes(variant === "foreign" ? "other/project#8" : "o/r#8")); return true; + }); + assert.deepEqual(bytes(root, item), before); +})); + +for (const invalid of [0, -1, 1.5, Number.MAX_SAFE_INTEGER + 1]) test(`refresh safe-number guard refuses a crafted native number (${invalid})`, async () => tracker(async root => { + const item = await native(root, { github_repo: repo, github_number: invalid }), before = bytes(root, item); + assert.throws(() => selectRefreshTargets([item], repo), /Refresh found an invalid issue number/); + assert.deepEqual(bytes(root, item), before); +})); + +test("refresh conflicting-number guard rejects native contradictory URL and number claims", async () => tracker(async root => { + const item = await native(root, { github_repo: repo, github_number: 8, github_url: issueUrl }), before = bytes(root, item); + assert.throws(() => selectRefreshTargets([item], repo), error => { + assert.equal((error as { code: string }).code, "github_import_owner_conflict"); + assert.match((error as { expected: string }).expected, /Refresh found conflicting GitHub identities/); return true; + }); + assert.deepEqual(bytes(root, item), before); + const consistent = await native(root, { github_repo: repo, github_number: 7, github_url: issueUrl }); + assert.equal(selectRefreshTargets([consistent], repo)[0]!.id, consistent.id); +})); + +test("duplicate identity tracking refuses two native owners and tolerates repeated rows of one owner", async () => tracker(async root => { + const first = await native(root, { github_url: issueUrl }), entry = (await prepareGithubImport(source(), repo, parseImportOptions({}), undefined, first, undefined, { items: [first], releases: [], timeline: async () => [] }))!; + assert.equal(verifyImportIdempotency([first, first], [entry], repo).provenance_indexed, 1); + const peer = await native(root, { external_ref: "gh:o/r#7" }), before = [first, peer].map(item => bytes(root, item)); + assert.throws(() => verifyImportIdempotency([first, peer], [entry], repo), error => { + assert.equal((error as { code: string }).code, "github_import_owner_conflict"); assert.match(String(error), /claimed by/); assert.ok((error as { owners: string[] }).owners.every(owner => owner.includes("claimed by"))); return true; + }); + assert.deepEqual([first, peer].map(item => bytes(root, item)), before); +})); + +test("an out-of-plan claim is filtered before the shared contradiction refusal", async () => tracker(async root => { + const initial = await native(root, { github_url: issueUrl }), entry = (await prepareGithubImport(source(), repo, parseImportOptions({}), undefined, initial, undefined, { items: [initial], releases: [], timeline: async () => [] }))!; + await change(root, initial.id!, { external_ref: "gh:o/r#8" }); + const current = (await listAllComplete({ includeBody: true }, { pmRoot: root })).items[0]!, before = bytes(root, current); + assert.throws(() => verifyImportIdempotency([current], [entry], repo), { code: "github_import_owner_conflict" }); + assert.deepEqual(bytes(root, current), before); +})); + +test("atomic ownership rethrow preserves a native peer-relink refusal before an SDK journal starts", async () => tracker(async root => { + const item = await native(root, { github_url: issueUrl }), entry = (await prepareGithubImport(source(), repo, parseImportOptions({}), undefined, item, undefined, { items: [item], releases: [], timeline: async () => [] }))!; + let starts = 0, peerBytes: string[] | undefined; + await assert.rejects(() => importGithubAtomic(root, repo, [entry], { onPlanStaged: async () => { + await change(root, item.id!, { github_url: "https://github.com/other/project/issues/8" }); peerBytes = bytes(root, item); + }, onTransition: () => { starts++; } }), error => { + assert.equal((error as { code: string }).code, "github_import_owner_conflict"); assert.match(String(error), /other\/project#8/); return true; + }); + assert.equal(starts, 0); assert.deepEqual(bytes(root, item), peerBytes); + assert.deepEqual(await listRetainedGithubImportPlans(root), [], "A pre-journal ownership refusal must remove its staged plan"); +})); + +for (const corrupt of ["missing-id"] as const) test(`certified native corpus refuses ${corrupt} before constructing a writable owner index`, async () => tracker(async root => { + const owner = await native(root, { github_url: issueUrl }), peer = await native(root); + const { id: _id, ...withoutId } = peer; + writeFileSync(getItemPath(root, String(peer.type), peer.id!), serializeItemDocument({ metadata: withoutId as Parameters[0]["metadata"], body: "Local context" })); + const before = [owner, peer].map(item => bytes(root, item)); let started = false; + await assert.rejects(() => withGithubImportOwners(root, [{ id: owner.id!, repo, number: 7 }], async () => { started = true; }), error => /incomplete|duplicate|invalid|id/i.test(String(error))); + assert.equal(started, false); assert.deepEqual([owner, peer].map(item => bytes(root, item)), before); +})); + +test("a native proposal removing its id is refused before any peer snapshot can authorize the write", async () => tracker(async root => { + const item = await native(root), before = bytes(root, item), settings = await readSettings(root); + await assert.rejects(() => withGithubImportOwners(root, [{ id: item.id!, repo, number: 7, initialLink: () => true }], () => mutateItem({ pmRoot: root, id: item.id!, settings, author: "fixture", op: "update", mutate: document => { + delete (document.metadata as Record).id; return { changedFields: ["id"] }; + } })), /changed or removed the target id/); + assert.deepEqual(bytes(root, item), before); + await native(root, { github_url: issueUrl }); + const corpus = (await listAllComplete({ includeBody: true }, { pmRoot: root })).items, index = buildGithubOwnershipIndex(corpus, [repo]); + for (const [id, row] of index.byId) { + assert.ok(index.states.has(row)); + for (const key of index.states.get(row)!.claims) assert.ok(index.claimants.get(key)!.has(id)); + } +})); + +test("legacy reference-conflict consumers are unreachable for the strongest mention-only native records", async () => tracker(async root => { + const reference = await native(root, { github_number: 7, tags: ["gh:o/r#7", "gh:other/project#7", "gh:o/r#8"] }); + const second = await native(root, { tags: ["gh:o/r#7", "gh:other/project#7"] }); + for (const item of [reference, second]) { assert.equal(legacyGithubIdentityConflict(item), undefined); assert.deepEqual(githubOwnershipClaims(item).owners, []); } + const before = [reference, second].map(item => bytes(root, item)); + await withMockGithub((request, response) => jsonResponse(response, 200, request.url!.includes("/issues?") ? [source()] : []), async () => { + const result = await runImport(repo, root, parseImportOptions({ atomic: true, "dry-run": true })); + assert.equal("wouldImport" in result && result.wouldImport, 1); assert.equal("identity_conflicts" in result, false); + assert.equal("identity_conflicts" in await runRefresh(repo, root, { "dry-run": true }), false); + const harness = await createExtensionTestHarness(extension, { capabilities: ["commands", "importers", "schema", "hooks", "preflight", "search"] }); + const sync = await harness.runCommand({ command: "github sync", pmRoot: root, options: { repo, "dry-run": true } }); + assert.equal("identity_conflicts" in sync, false); + }); + assert.deepEqual([reference, second].map(item => bytes(root, item)), before); +})); + +for (const author of ["fixture", "pm-github"]) test(`citation adoption counts the native curated/mirroring branch (${author})`, async () => tracker(async root => { + const item = await native(root, { author, body: `Issue: ${issueUrl}` }); + await withMockGithub((request, response) => jsonResponse(response, 200, request.url!.includes("/issues?") ? [source()] : []), async () => { + const result = await runImport(repo, root, parseImportOptions({ adoptCited: true })); + assert.equal("adopted" in result && result.adopted, 1); + assert.equal("updated" in result && result.updated, 0); + const after = (await listAllComplete({ includeBody: true }, { pmRoot: root })).items; + assert.equal(after.length, 1); assert.equal(after[0]!.id, item.id); + assert.equal(after[0]!.title, author === "pm-github" ? source().title : item.title); + }); +})); + +for (const malformed of [false, true]) test(`Unicode-shifted native citation boundaries preserve the malformed-URL guard (${malformed})`, async () => tracker(async root => { + const item = await native(root, { body: `Issue: ${issueUrl}\n\n${"İ".repeat(11)}${malformed ? "github.com/ issues/7" : " harmless context"}` }), before = bytes(root, item); + const match = matchImportedIssue([item], source(), undefined, true); + assert.equal(match.item?.id, malformed ? undefined : item.id); + if (malformed) assert.deepEqual(match.citation_candidates, [{ id: item.id!, reason: "multiple_issues" }]); + else assert.equal(match.via, "citation"); + assert.deepEqual(bytes(root, item), before); +})); + +test("board status retains a current matching issue owner", async () => tracker(async root => { + const tag = projectItemTag(ref, "EXISTING_NODE"), item = await native(root, { github_url: issueUrl, tags: ["retained", tag] }); + const board: ProjectItem[] = [{ id: "EXISTING_NODE", statusName: "In Progress", content: { typename: "Issue", title: "Existing issue", repo, number: 7 } }]; + const plan = buildProjectPushPlan([item], ref, board, meta.statusField).entries[0]!, before = bytes(root, item); + assert.equal(plan.action, "set-status"); + await withMockGithub((_request, response) => jsonResponse(response, 200, { data: { updateProjectV2ItemFieldValue: { projectV2Item: { id: "EXISTING_NODE" } } } }), async server => { + const result = await applyPushEntry(plan, meta, ref, new Map([[item.id!, item]]), root, "fixture", board); + assert.equal(result.changed, true, result.error ?? "Unexpected matching-owner refusal"); assert.equal(server.requests.length, 1); + assert.deepEqual(bytes(root, item), before); + }); +})); diff --git a/test/round-seven.test.ts b/test/round-seven.test.ts new file mode 100644 index 00000000..7b64f365 --- /dev/null +++ b/test/round-seven.test.ts @@ -0,0 +1,226 @@ +/** Final review classes use real trackers, SDK policies and peer-native state. */ +import assert from "node:assert/strict"; +import test from "node:test"; +import { mkdtempSync, rmSync, readdirSync, readFileSync, existsSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join, delimiter } from "node:path"; +import { spawnSync } from "node:child_process"; +import { init, create, get, mutateItem, readSettings, getItemPath, getHistoryPath, comments, + runWithActiveExtensions, setActiveExtensionHooks, listAllComplete, WorkspaceTransactionInterruptedError } from "@unbrained/pm-cli/sdk"; +import { runImport, parseImportOptions, importGithubAtomic, prepareGithubImport, listUnfinishedGithubImports, + readGithubImportPlan, resumeGithubImport, linkImportedDependencies, type GhIssue } from "../index.ts"; +import { runCloseout, type CloseoutApi } from "../closeout.ts"; +import { captureStderr, withEnv } from "./helpers/mock-github-server.ts"; + +const repo = "o/r"; +const source: GhIssue = { number: 8, title: "Source issue", body: "Public body", state: "open", labels: [], user: null, assignee: null, milestone: null, + created_at: "2025-01-01T00:00:00Z", updated_at: "2025-02-01T00:00:00Z", html_url: "https://github.com/o/r/issues/8" }; +const comment = { id: 71, body: "Public comment", user: { login: "fixture" }, created_at: "2025-02-01T00:00:00Z" }; + +/** Isolate real tracker writes; gated cases also have an empty real Git worktree. */ +async function tracker(run: (root: string) => Promise, git = false) { + const directory = mkdtempSync(join(tmpdir(), "github-r7-")); + const root = join(directory, "tracker"); + try { + if (git) assert.equal(spawnSync("git", ["init", "-q"], { cwd: directory }).status, 0); + await init("fixture", { defaults: true, withPackages: false, agentGuidance: "skip" }, { pmRoot: root, cwd: directory, noExtensions: true }); + await withEnv({ PM_PATH: root, PM_AUTHOR: "fixture", PM_TELEMETRY_SOURCE_CONTEXT: "automation", GITHUB_TOKEN: "fixture-token", GH_TOKEN: undefined, + PATH: join(import.meta.dirname, "../node_modules/.bin") + delimiter + process.env.PATH }, () => run(root)); + } finally { rmSync(directory, { recursive: true, force: true }); } +} + +/** Only remote source data is supplied; every item and annotation mutation is native. */ +async function sync(root: string, atomic: boolean, gate = false, issues = [source]) { + return captureStderr(() => runImport(repo, root, parseImportOptions({ atomic, gate, "with-comments": true, "comments-mode": "annotations" }), { + resolveToken: () => undefined, fetchIssues: async () => issues, fetchIssueComments: async () => [comment], fetchIssueTimeline: async () => [], + })); +} + +/** Change explicit identities with the supported SDK mutation and retained history. */ +async function own(root: string, id: string, url: string, number?: number) { + await mutateItem({ pmRoot: root, id, settings: await readSettings(root), author: "fixture", op: "update", mutate: document => { + document.metadata.github_url = url; + if (number !== undefined) { document.metadata.github_repo = repo; document.metadata.github_number = number; } + return { changedFields: ["github_url", "github_repo", "github_number"] }; + } }); +} + +/** Snapshot the native item and its audit stream together. */ +async function bytes(root: string, id: string) { + const item = (await get(id, {}, { pmRoot: root })).item; + return [readFileSync(getItemPath(root, item.type!, id), "utf8"), readFileSync(getHistoryPath(root, id), "utf8")]; +} + +for (const atomic of [false, true]) for (const gate of [false, true]) test(`Round7 annotation refusal is best-effort after commit (atomic=${atomic}, gate=${gate})`, async () => tracker(async root => { + let attempts = 0; + let error: unknown; + let result: Awaited> | undefined; + await runWithActiveExtensions({ path: root, noExtensions: true }, async () => { + setActiveExtensionHooks({ beforeCommand: [], afterCommand: [], onRead: [], onWrite: [], onIndex: [], beforeMutation: [{ layer: "project", name: "fixture-comments", run: context => { + if (context.operation === "comment_add") { attempts++; return { allow: false, code: "fixture_comment_refused", remediation: "Retry source comments on the next normal sync" }; } + } }] }); + try { result = await sync(root, atomic, gate); } catch (caught) { error = caught; } + }); + assert.equal(attempts, 1, "Each entry must attempt annotation synchronization exactly once"); + const items = (await listAllComplete({}, { pmRoot: root })).items; + assert.equal(items.length, 1); assert.equal(items[0]!.title, source.title); + assert.deepEqual(await listUnfinishedGithubImports(root), []); + const plans = join(root, "transactions/pm-github"); + assert.deepEqual(existsSync(plans) ? readdirSync(plans).filter(name => name.endsWith(".json")) : [], [], "A comment refusal must not retain a committed item plan"); + if (gate) assert.match(String(error), /1 planned write\(s\) failed/); + else { assert.equal(error, undefined); assert.ok(result && "commentWriteFailures" in result.result); assert.equal(result.result.commentWriteFailures, 1); } + await sync(root, atomic, gate); + assert.equal((await comments(items[0]!.id, {}, { pmRoot: root })).comments.length, 1, "A later normal sync repairs side data without duplicating items"); +}, true)); + +for (const gate of [false, true]) test(`Round7 annotation refusal during replay cleans the committed plan (gate=${gate})`, async () => tracker(async root => { + const entry = (await prepareGithubImport(source, repo, parseImportOptions({ "with-comments": true, "comments-mode": "annotations" }), undefined, undefined, + async () => [comment], { items: [], releases: [], timeline: async () => [] }))!; + await assert.rejects(() => importGithubAtomic(root, repo, [entry], { onTransition: context => { + if (context.transition === "committed") throw new WorkspaceTransactionInterruptedError("Committed before annotations"); + } }), /committed journal/); + const item = (await listAllComplete({}, { pmRoot: root })).items[0]!; + const before = await bytes(root, item.id); + let attempts = 0; + let error: unknown; + let result: Awaited> | undefined; + await runWithActiveExtensions({ path: root, noExtensions: true }, async () => { + setActiveExtensionHooks({ beforeCommand: [], afterCommand: [], onRead: [], onWrite: [], onIndex: [], beforeMutation: [{ layer: "project", name: "fixture-replay-comments", run: context => { + if (context.operation === "comment_add") { attempts++; return { allow: false, code: "fixture_comment_refused", remediation: "Retry comments on normal sync" }; } + } }] }); + try { result = (await captureStderr(() => runImport(repo, root, parseImportOptions({ atomic: true, gate }), { + fetchIssues: async () => { throw new Error("Replay cannot fetch changed source input"); }, + }))).result; } catch (caught) { error = caught; } + }); + assert.equal(attempts, 1); + assert.deepEqual(await bytes(root, item.id), before); + assert.deepEqual(readdirSync(join(root, "transactions/pm-github")).filter(name => name.endsWith(".json")), []); + if (gate) assert.match(String(error), /1 planned write\(s\) failed/); + else { assert.equal(error, undefined); assert.ok(result && "commentWriteFailures" in result); assert.equal(result.commentWriteFailures, 1); } + await sync(root, true, gate); + assert.equal((await comments(item.id, {}, { pmRoot: root })).comments.length, 1); +}, true)); + +for (const replay of [false, true]) test(`Round7 annotation refusal through direct atomic API cleans its gated plan (replay=${replay})`, async () => tracker(async root => { + const entry = (await prepareGithubImport(source, repo, parseImportOptions({ "with-comments": true, "comments-mode": "annotations" }), undefined, undefined, + async () => [comment], { items: [], releases: [], timeline: async () => [] }))!; + if (replay) await assert.rejects(() => importGithubAtomic(root, repo, [entry], { onTransition: context => { + if (context.transition === "committed") throw new WorkspaceTransactionInterruptedError("Committed before comments"); + } }), /committed journal/); + let attempts = 0; + await runWithActiveExtensions({ path: root, noExtensions: true }, async () => { + setActiveExtensionHooks({ beforeCommand: [], afterCommand: [], onRead: [], onWrite: [], onIndex: [], beforeMutation: [{ layer: "project", name: "fixture-direct-comments", run: context => { + if (context.operation === "comment_add") { attempts++; return { allow: false, code: "fixture_comment_refused", remediation: "Repair on next normal sync" }; } + } }] }); + await assert.rejects(async () => { + if (!replay) return importGithubAtomic(root, repo, [entry], { gate: true }); + const file = readdirSync(join(root, "transactions/pm-github")).find(name => name.endsWith(".json"))!; + return resumeGithubImport(root, (await readGithubImportPlan(root, file.slice(0, -5)))!, { gate: true }); + }, /1 planned write\(s\) failed/); + }); + assert.equal(attempts, 1); + assert.equal((await listAllComplete({}, { pmRoot: root })).items.length, 1); + assert.deepEqual(readdirSync(join(root, "transactions/pm-github")).filter(name => name.endsWith(".json")), []); +}, true)); + +for (const scenario of ["changed", "unchanged", "mixed"] as const) test(`Round7 atomic annotations synchronize each entry once (${scenario})`, async () => tracker(async root => { + if (scenario !== "changed") await sync(root, true, false, [source]); + const incoming = new Map([8, 9].map(number => [number, { ...comment, id: 100 + number }])); + let attempts = 0; + await runWithActiveExtensions({ path: root, noExtensions: true }, async () => { + setActiveExtensionHooks({ beforeCommand: [], afterCommand: [], onRead: [], onWrite: [], onIndex: [], beforeMutation: [{ layer: "project", name: "annotation-pass-observer", run: context => { + if (context.operation !== "comment_add") return; + attempts++; + // Native comment text is already prepared. Advancing only the incoming + // fixture cursor makes an unwanted second sync produce an extra write. + const remote = incoming.get(Number(context.before?.metadata.github_number)); + assert.ok(remote); + remote.id++; + } }] }); + await captureStderr(() => runImport(repo, root, parseImportOptions({ atomic: true, "with-comments": true, "comments-mode": "annotations" }), { + resolveToken: () => undefined, + fetchIssues: async () => scenario === "mixed" ? [source, { ...source, number: 9, html_url: "https://github.com/o/r/issues/9" }] : [source], + fetchIssueComments: async issue => [incoming.get(issue.number)!], fetchIssueTimeline: async () => [], + })); + }); + assert.equal(attempts, scenario === "mixed" ? 2 : 1, "Every entry must make one annotation pass"); + for (const item of (await listAllComplete({}, { pmRoot: root })).items) { + const stored = (await comments(item.id, {}, { pmRoot: root })).comments; + assert.equal(stored.length, scenario !== "changed" && item.github_number === 8 ? 2 : 1); + assert.ok(stored.some(entry => entry.text.includes(`pm-github:comment:${100 + Number(item.github_number)}`))); + } +})); + +for (const duplicate of ["target", "source", "contradictory", "none"] as const) test(`Round7 dependency ownership is unique (${duplicate})`, async () => tracker(async root => { + const ids: string[] = []; + for (const number of [7, 8, ...(duplicate === "target" ? [7] : duplicate === "source" ? [8] : [])]) { + const item = (await create({ title: `Local ${number}`, type: "Task" }, { pmRoot: root })).item; + await own(root, item.id, `https://github.com/o/r/issues/${number}`); ids.push(item.id); + } + if (duplicate === "contradictory") await own(root, ids[0]!, "https://github.com/o/r/issues/7", 9); + if (duplicate === "none") for (let i = 0; i < 2; i++) { + const mention = (await create({ title: "Shared reference", type: "Chore", tags: "gh:o/r#7" }, { pmRoot: root })).item; + ids.push(mention.id); + } + const before = await Promise.all(ids.map(id => bytes(root, id))); + const result = await linkImportedDependencies(repo, [{ ...source, body: "Blocked by #7" }], root); + if (duplicate === "none") { assert.equal(result.linked, 1); assert.equal(result.failures.length, 0); } + else { + assert.equal(result.linked, 0); + assert.match(result.failures.join("\n"), /ambiguous_github_dependency_owner/); + for (const id of duplicate === "target" ? [ids[0]!, ids[2]!] : duplicate === "source" ? [ids[1]!, ids[2]!] : [ids[0]!]) assert.ok(result.failures.some(failure => failure.includes(id))); + assert.deepEqual(await Promise.all(ids.map(id => bytes(root, id))), before); + } +})); + +for (const gate of [false, true]) test(`Round7 failed atomic preflight never starts a transaction (gate=${gate})`, async () => tracker(async root => { + const unsafe = { ...source, body: "ghp_" + "A".repeat(36) }; + const entry = (await prepareGithubImport(unsafe, repo, parseImportOptions({}), undefined, undefined, undefined, { items: [], releases: [], timeline: async () => [] }))!; + let transitions = 0; + await assert.rejects(() => importGithubAtomic(root, repo, [entry], { gate, onTransition: () => { transitions++; throw new WorkspaceTransactionInterruptedError("Must never reach the first mutation"); } }), + (error: unknown) => (error as { code: string }).code === "github_import_preflight_failed"); + assert.equal(transitions, 0); + assert.equal((await listAllComplete({}, { pmRoot: root })).items.length, 0); + assert.equal(existsSync(join(root, "transactions")), false); + entry.body = "Public body"; + await assert.rejects(() => importGithubAtomic(root, repo, [entry], { gate }), + (error: unknown) => (error as { code: string }).code === "github_import_preflight_failed"); + assert.equal(existsSync(join(root, "transactions")), false); + entry.sourceBody = "Public body"; + let interrupted = false; + await assert.rejects(() => importGithubAtomic(root, repo, [entry], { gate, onTransition: context => { + if (!interrupted && context.transition === "prepared") { interrupted = true; throw new WorkspaceTransactionInterruptedError("Fixture interruption"); } + } }), /interrupted/); + const pending = await listUnfinishedGithubImports(root); + assert.equal(pending.length, 1); + const record = (await readGithubImportPlan(root, pending[0]!.transactionId))!; + assert.equal(record.preflight?.verdict, "pass"); + await resumeGithubImport(root, record); + assert.equal((await listAllComplete({}, { pmRoot: root })).items.length, 1); +})); + +/** Valid delivery evidence is shared by conflicting and unrelated local items. */ +function delivery(body: string): CloseoutApi { + const sha = "a".repeat(40); + return { base: "https://api.github.com", next: () => undefined, get: async endpoint => ({ body: JSON.stringify( + endpoint.includes("/pulls/") ? endpoint.includes("/commits?") ? [] : { + number: 1, commits: 0, merged: true, merged_at: "2027-01-01T00:00:00Z", merge_commit_sha: "b".repeat(40), html_url: `https://github.com/${repo}/pull/1`, + body, head: { sha }, 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: sha, status: "completed", conclusion: "success", app: { id: 1 } }] } : []), + }) }; +} + +for (const mode of ["eligible", "skip", "context", "foreign", "terminal", "epic"] as const) test(`Round7 closeout isolates explicit identity conflicts (${mode})`, async () => tracker(async root => { + const bad = (await create({ title: "Conflicting delivery", type: mode === "epic" ? "Epic" : "Task", status: mode === "terminal" ? "canceled" : "in_progress", ...(mode === "terminal" ? { closeReason: "Fixture canceled delivery" } : {}) }, { pmRoot: root })).item; + const good = (await create({ title: "Unrelated delivery", type: "Task", status: "in_progress" }, { pmRoot: root })).item; + await own(root, bad.id, "https://github.com/other/project/issues/7", 8); + const before = await bytes(root, bad.id); + const link = (id: string) => `https://github.com/${repo}/blob/main/.agents/pm/tasks/${id}.toon`; + const body = `${mode === "context" ? "Context" : "Closes"} ${mode === "foreign" ? link(bad.id).replace("github.com/o/r/", "github.com/other/project/") : link(bad.id)}\n\nCloses ${link(good.id)}`; + const result = await runCloseout(root, repo, { pr: "1", apply: true, ...(mode === "skip" ? { skip: bad.id } : {}) }, delivery(body)); + assert.equal(result.closed, 1); + assert.equal((await get(good.id, {}, { pmRoot: root })).item.status, "closed"); + assert.deepEqual(await bytes(root, bad.id), before); + assert.equal(result.plan.find(row => row.item === bad.id)?.code, mode === "skip" ? "explicit_skip" : mode === "context" ? "not_delivery" : mode === "foreign" ? "other_repository" : mode === "terminal" ? "already_terminal" : mode === "epic" ? "epic" : "github_import_owner_conflict"); +})); diff --git a/test/round-seventeen.test.ts b/test/round-seventeen.test.ts new file mode 100644 index 00000000..fcb6e44b --- /dev/null +++ b/test/round-seventeen.test.ts @@ -0,0 +1,727 @@ +/** Review regressions use native non-git trackers and bounded real transport probes. */ +import assert from "node:assert/strict"; +import test from "node:test"; +import { mkdtempSync, rmSync, readFileSync, readdirSync, writeFileSync, mkdirSync, renameSync, symlinkSync, copyFileSync, watch, existsSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join, delimiter } from "node:path"; +import { createHash, createHmac } from "node:crypto"; +import { init, create, get, listAllComplete, mutateItem, readSettings, getItemPath, getHistoryPath, WorkspaceTransactionInterruptedError, runWithActiveExtensions, setActiveExtensionHooks, runReindex, update, deleteItem } from "@unbrained/pm-cli/sdk"; +import extension, { buildExportPlan, applyExportPlan, applyPullEntry, planSync, assertCurrentUpstreamTarget, acquireImportLock, gqlResolveIssueNodeId, applyPushEntry, githubImportTarget, linkImportedDependencies, runImport, prepareGithubImport, parseImportOptions, importGithubAtomic, readGithubImportPlan, listRetainedGithubImportPlans, listUnfinishedGithubImports, resumeGithubImport, acceptGithubWebhook, reconcileGithubWebhooks, validateGithubImportPlan, type GithubImportPlanRecord, type GhIssue, type PmItem } from "../index.ts"; +import { githubOwnershipClaims, githubRepositoryHash, buildGithubOwnershipIndex, warnGithubOwnershipScope, assertGithubImportOwner, readGithubImportOwner, withGithubImportOwners, withGithubItemConsistency, readGithubOwnershipSnapshot, withGithubOwnershipWork, assertGithubOwnershipConsistency, type GithubOwnershipWork } from "../import-ownership.ts"; +import { legacyGithubDescriptionIdentity, legacyGithubLinkClaims, parseLegacyGithubDescription, matchImportedIssue, githubLinkReferences, GithubIdentityConflictError, buildImportFidelity, importFidelityFields, importCloseReason } from "../import-fidelity.ts"; +import { normalizeGithubRepository, parseGithubUrl, parseGithubIssueUrl, githubUrlTokens } from "../github-url.ts"; +import { writeGithubStateAtomic } from "../durable-state.ts"; +import { buildProjectImportPlan, buildProjectPullPlan, parseProjectRef } from "../projects.ts"; +import { runRefresh, selectRefreshTargets } from "../refresh.ts"; +import { assertCloseoutGithubOwner, closeoutOwnershipConflict, extractCloseoutItems } from "../closeout.ts"; +import { captureStderr, withEnv, withMockGithub, jsonResponse } from "./helpers/mock-github-server.ts"; +import { createExtensionTestHarness } from "@unbrained/pm-cli/sdk/testing"; +import { importTagArguments } from "../import-tags.ts"; +import { boundedCli } from "./helpers/bounded-cli.ts"; + +const repo = "o/r", url = "https://github.com/o/r/issues/7"; +/** Own native tracker lifetime and every child command's package-local SDK. */ +async function tracker(run: (root: string) => Promise) { + const directory = mkdtempSync(join(tmpdir(), "github-round-seventeen-")), root = join(directory, "tracker"); + try { + await init("fixture", { defaults: true, withPackages: false, agentGuidance: "skip" }, { pmRoot: root, cwd: directory, noExtensions: true }); + await withEnv({ PM_PATH: root, PM_AUTHOR: "fixture", PM_TELEMETRY_SOURCE_CONTEXT: "automation", GITHUB_TOKEN: "fixture", GH_TOKEN: undefined, + PATH: join(import.meta.dirname, "../node_modules/.bin") + delimiter + process.env.PATH }, () => run(root)); + } finally { rmSync(directory, { recursive: true, force: true }); } +} +/** Persist and decode legacy metadata through the public SDK. */ +async function native(root: string, fields: Record = {}) { + const local = (await create({ title: "Curated owner", type: "Chore", tags: "retained" }, { pmRoot: root })).item; + await mutateItem({ pmRoot: root, id: local.id, settings: await readSettings(root), author: "fixture", op: "update", mutate: document => { + Object.assign(document.metadata, fields); return { changedFields: Object.keys(fields) }; + } }); + return (await listAllComplete({ includeBody: true }, { pmRoot: root, noExtensions: true })).items.find(item => item.id === local.id)!; +} +/** Snapshot both native authoritative streams around ownership decisions. */ +function bytes(root: string, item: PmItem) { return [getItemPath(root, String(item.type), item.id!), getHistoryPath(root, item.id!)].map(file => readFileSync(file, "hex")); } +/** REST source and refresh GraphQL rows represent the same issue. */ +function source(number = 7): GhIssue { return { number, title: `Source ${number}`, body: "Public source", state: "open", comments: 1, labels: [], user: null, assignee: null, milestone: null, + created_at: "2025-01-01T00:00:00Z", updated_at: "2025-02-01T00:00:00Z", html_url: `https://github.com/o/r/issues/${number}` }; } +/** Sign only a synthetic delivery hint; reconciliation must fetch current provider state. */ +function delivery(number: number) { + const secret = "fixture-webhook-key", payload = Buffer.from(JSON.stringify({ action: "closed", repository: { full_name: repo }, issue: { number } })); + return { repo, delivery: `deferred-${number}`, event: "issues", secret, payload, signature: "sha256=" + createHmac("sha256", secret).update(payload).digest("hex") }; +} + +for (const end of ['"', ">", ".", "<", "'", "`", ")".repeat(20_000)]) test(`shared declaration boundaries produce no warning (${end.length > 1 ? "20000 parentheses" : end})`, async () => tracker(async root => { + const item = await native(root, { description: `GH issue #7: ${url}${end}` }), before = bytes(root, item); + assert.equal(legacyGithubDescriptionIdentity(item)?.key, "o/r#7"); + assert.equal(matchImportedIssue([item], source()).item?.id, item.id); + const result = await captureStderr(async () => warnGithubOwnershipScope(buildGithubOwnershipIndex([item]), [item])); + assert.deepEqual(result.stderr, []); assert.deepEqual(bytes(root, item), before); +})); + +test("shared declaration diagnostics retain mismatches and keep API links reference-only", async () => tracker(async root => { + for (const description of [`GH issue #8: ${url}>`, "GitHub issue https://api.github.com/repos/o/r/issues/7", "GH issue #7: https://github.com/o/r/issues/nope", "GH issue #7: https://api.github.com.example/repos/o/r/issues/7", "GH issue #7: https://github.com/o/r/issues/api.github.com"]) { + const item = await native(root, { description }); + assert.equal(legacyGithubDescriptionIdentity(item), undefined); assert.deepEqual(githubOwnershipClaims(item).owners, []); + const result = await captureStderr(async () => warnGithubOwnershipScope(buildGithubOwnershipIndex([item]), [item])); + assert.equal(result.stderr.length, parseLegacyGithubDescription(item)?.target?.url.host === "api.github.com" ? 0 : 1); + if (result.stderr.length) assert.equal(JSON.parse(result.stderr[0]!).code, "invalid_github_identity_number"); + } + const anonymous = {}; + const empty = await captureStderr(async () => warnGithubOwnershipScope(buildGithubOwnershipIndex([anonymous]), [])); + assert.deepEqual(empty.stderr, []); + assert.equal(parseLegacyGithubDescription(anonymous), undefined); + assert.equal(parseLegacyGithubDescription({ description: `See ${url}` }), undefined); +})); + +test("native declaration diagnostics finish linearly on 20000 closing parentheses followed by text", async () => tracker(async root => { + const item = await native(root, { description: `GH issue #7: ${url}${")".repeat(20_000)}x` }); + const program = `const module = await import(${JSON.stringify(new URL("../import-ownership.ts", import.meta.url).href)}); const item = ${JSON.stringify(item)}; + console.log("probe-ready"); setImmediate(() => { const warnings=[]; console.error = value => warnings.push(JSON.parse(value)); + for(let repeat=0;repeat<8;repeat++) module.warnGithubOwnershipScope(module.buildGithubOwnershipIndex([item]),[item]); + if(warnings.length!==8 || warnings[0].code!=="invalid_github_identity_number") process.exitCode=1; + console.log(JSON.stringify({warnings:warnings.length})); });`; + const env = { ...process.env }; delete env.NODE_V8_COVERAGE; + for (const key of Object.keys(env)) if (/TOKEN|SECRET|PASSWORD|CREDENTIAL|API_?KEY/i.test(key)) delete env[key]; + const result = await boundedCli(process.execPath, ["--input-type=module", "-e", program], root, env, 1200, 200, "probe-ready"); + assert.equal(result.timedOut, false, "The diagnostic must scan the declaration linearly, including a non-punctuation suffix"); + assert.equal(result.code, 0, result.stderr); assert.match(result.stdout, /"warnings":8/); +})); + +test("legacy claim API deduplicates explicit aliases and ignores link-only references", async () => tracker(async root => { + const item = await native(root, { github_url: url, GitHubURL: "https://github.com/O/R/pull/7/", links: `See https://github.com/other/project/issues/8` }); + assert.deepEqual(legacyGithubLinkClaims(item), ["o/r#7"]); + const reference = await native(root, { links: url, tags: ["gh:o/r#7"] }); + assert.deepEqual(legacyGithubLinkClaims(reference), []); +})); + +test("positive owner assertion refuses an unowned native item and allows only an explicit initial link", async () => tracker(async root => { + const item = await native(root), before = bytes(root, item), target = { id: item.id!, repo, number: 7 }; + assert.throws(() => assertGithubImportOwner(item, target), { code: "github_import_owner_conflict" }); + assert.throws(() => assertGithubImportOwner(item, { ...target, initialLink: () => false }), { code: "github_import_owner_conflict" }); + assert.doesNotThrow(() => assertGithubImportOwner(item, { ...target, initialLink: () => true })); + assert.deepEqual(bytes(root, item), before); + await assert.rejects(() => readGithubImportOwner(root, { id: "fixture-missing", repo, number: 7 }), error => (error as { details?: { code?: string }; message: string }).message.includes("not found")); +})); + +test("URL boundary inputs retain safe repositories, project locators and balanced query delimiters", () => { + assert.equal(normalizeGithubRepository(" /// "), undefined); + assert.equal(normalizeGithubRepository(" O/R/// "), "o/r"); + assert.equal(parseGithubUrl("https://github.com/o/r")?.kind, undefined); + assert.equal(parseGithubUrl("https://api.github.com"), undefined); + assert.equal(parseGithubIssueUrl("https://github.com/o/r"), undefined); + const balanced = `${url}?filter=[a]{b}(c)`; + assert.equal(githubUrlTokens(balanced)[0]!.raw, balanced); + assert.deepEqual(parseProjectRef("https://github.com/orgs/o/projects/7"), { owner: "o", number: 7 }); + assert.equal(parseProjectRef("https://github.com/orgs/o/projects"), undefined); +}); + +test("deferred webhook issues stay pending while independent deliveries reconcile and later retry", async () => tracker(async root => { + const owner7 = await native(root, { github_url: url }), owner8 = await native(root, { github_url: "https://github.com/o/r/issues/8" }); + await native(root, { github_url: "https://github.com/o/r/issues/9" }); + const items = [owner7, owner8], entries = await Promise.all(items.map((item, i) => prepareGithubImport(source(7 + i), repo, parseImportOptions({ atomic: true }), undefined, item, undefined, + { items, releases: [], timeline: async () => [] }))); + await assert.rejects(() => importGithubAtomic(root, repo, entries.map(entry => entry!), { onTransition: context => { + if (context.transition === "committed") throw new WorkspaceTransactionInterruptedError("Retain original source plan"); + } }), /committed journal/); + const [retained] = await listRetainedGithubImportPlans(root); assert.ok(retained); + const record = (await readGithubImportPlan(root, retained.transactionId))!; + const before = [owner7, owner8].map(item => bytes(root, item)); + for (const number of [7, 9]) await acceptGithubWebhook(root, delivery(number)); + await withMockGithub((request, response, body) => { + if (request.method === "GET") { jsonResponse(response, 200, []); return; } + const query = (JSON.parse(body) as { query: string }).query; + if (query.includes("timelineItems")) { jsonResponse(response, 200, { data: { repository: { issue: { timelineItems: { nodes: [], pageInfo: { hasNextPage: false, endCursor: null } } } } } }); return; } + const repository = Object.fromEntries([7, 9].filter(number => query.includes(`i${number}:`)).map(number => [`i${number}`, { number, title: `Current ${number}`, body: "Public source", state: "CLOSED", stateReason: "COMPLETED", + createdAt: "2025-01-01T00:00:00Z", updatedAt: "2025-03-01T00:00:00Z", closedAt: "2025-03-01T00:00:00Z", url: `https://github.com/o/r/issues/${number}`, author: null, + labels: { nodes: [], pageInfo: { hasNextPage: false, endCursor: null } } }])); + jsonResponse(response, 200, { data: { repository } }); + }, async () => { + const preview = await runRefresh(repo, root, { "dry-run": true }, { numbers: [7, 9] }); + assert.deepEqual(preview.deferred_imports![0]!.issue_numbers, [7, 8]); assert.deepEqual(preview.plan.map(row => row.number), [9]); + const first = await reconcileGithubWebhooks(root, repo); + assert.equal(first.selected, 2); assert.equal(first.reconciled, 1); assert.equal(first.retry, 1); assert.equal(first.unlinked, 0); + assert.deepEqual(first.proposals.map(row => row.number), [9]); + const pendingHash = createHash("sha256").update("deferred-7").digest("hex"); + assert.equal(readdirSync(join(root, "github-webhooks", createHash("sha256").update(repo).digest("hex"))).some(name => name === `${pendingHash}.done.json`), false); + assert.deepEqual([owner7, owner8].map(item => bytes(root, item)), before); + await resumeGithubImport(root, record); + const retry = await reconcileGithubWebhooks(root, repo); + assert.equal(retry.selected, 1); assert.equal(retry.reconciled, 1); assert.equal(retry.retry, 0); assert.deepEqual(retry.proposals.map(row => row.number), [7]); + }); +})); + + +test("shared consistency keeps closeout repository refusals and valid or unowned controls", async () => tracker(async root => { + const unowned = await native(root), own = await native(root, { github_url: url }), other = await native(root, { github_url: "https://github.com/other/project/issues/7" }); + assert.doesNotThrow(() => assertCloseoutGithubOwner(unowned, repo, [unowned])); + assert.doesNotThrow(() => assertCloseoutGithubOwner(own, repo, buildGithubOwnershipIndex([own]))); + assert.throws(() => assertCloseoutGithubOwner(other, repo, [other]), { code: "github_import_owner_conflict" }); + const contradictory = await native(root, { github_url: url, github_repo: "other/project", github_number: 8 }); + assert.throws(() => assertCloseoutGithubOwner(contradictory, repo, [contradictory]), { code: "github_import_owner_conflict" }); +})); + +test("refresh refuses invalid raw number provenance and duplicate records while retaining a valid control", async () => tracker(async root => { + const invalid = await native(root, { github_url: url, github_repo: repo, github_number: -1 }), before = bytes(root, invalid); + assert.throws(() => selectRefreshTargets([invalid], repo), /invalid issue number/); assert.deepEqual(bytes(root, invalid), before); + const valid = await native(root, { github_url: url, github_repo: repo, github_number: 7 }); + assert.equal(selectRefreshTargets([valid], repo)[0]!.id, valid.id); + assert.throws(() => selectRefreshTargets([valid, valid], repo), /multiple existing owners/); +})); + +test("diagnostics name internal off-scope contradictions and zero-valued external references", async () => tracker(async root => { + const conflict = await native(root, { github_url: url, github_repo: "other/project" }), external = await native(root, { external_ref: "gh:o/r#0" }); + const result = await captureStderr(async () => warnGithubOwnershipScope(buildGithubOwnershipIndex([conflict, external]), [])); + assert.ok(result.stderr.some(line => line.includes("out_of_scope_github_owner_conflict") && line.includes(conflict.id!))); + assert.ok(result.stderr.some(line => line.includes("invalid_github_identity_number") && line.includes(external.id!))); + assert.deepEqual(githubOwnershipClaims(external).owners, []); + const suffix = (await create({ id: `fixture-github-${githubRepositoryHash(repo)}-0`, title: "Invalid importer marker", type: "Task", author: "pm-github", tags: "gh:o/r#7" }, { pmRoot: root })).item; + const invalid = await captureStderr(async () => warnGithubOwnershipScope(buildGithubOwnershipIndex([suffix]), [])); + assert.ok(invalid.stderr.some(line => line.includes("invalid_github_identity_number"))); + assert.deepEqual(githubOwnershipClaims(suffix).owners, []); +})); + + +test("recovery refuses a non-directory plan store before any source request or tracker mutation", async () => tracker(async root => { + const local = await native(root), before = bytes(root, local); + mkdirSync(join(root, "transactions"), { recursive: true }); writeFileSync(join(root, "transactions/pm-github"), "occupied"); + await assert.rejects(() => runImport(repo, root, parseImportOptions({ atomic: true })), error => (error as NodeJS.ErrnoException).code === "ENOTDIR"); + assert.deepEqual(bytes(root, local), before); assert.equal(readFileSync(join(root, "transactions/pm-github"), "utf8"), "occupied"); +})); + +test("a real concurrently retained batch cannot silently lose current planned annotations", async () => tracker(async root => { + let incoming = source(), first = true; + await withMockGithub((request, response) => { + jsonResponse(response, 200, request.url!.includes("/comments") ? [{ id: 71, body: "Public source comment", user: { login: "fixture" }, created_at: "2025-02-01T00:00:00Z" }] + : request.url!.includes("/issues?") ? [incoming] : []); + }, async () => { + const result = await runImport(repo, root, parseImportOptions({ atomic: true, "with-comments": true, "comments-mode": "annotations" }), { + commitAtomic: async (trackerRoot, repository, entries) => { + assert.equal(first, true); first = false; + const pending = (await prepareGithubImport(source(8), repository, parseImportOptions({}), undefined, undefined, undefined, + { items: [], releases: [], timeline: async () => [] }))!; + await assert.rejects(() => importGithubAtomic(trackerRoot, repository, [pending], { onTransition: context => { + if (context.transition === "prepared") throw new WorkspaceTransactionInterruptedError("Concurrent producer interrupted before commit"); + } }), /interrupted/); + return importGithubAtomic(trackerRoot, repository, entries); + }, + }); + assert.equal("commentWriteFailures" in result && result.commentWriteFailures, 1); + const firstItems = (await listAllComplete({}, { pmRoot: root })).items; + assert.equal(firstItems.length, 1); assert.equal(firstItems[0]!.github_number, 8); + incoming = source(9); + const clean = await runImport(repo, root, parseImportOptions({ atomic: true, "with-comments": true, "comments-mode": "annotations" })); + assert.equal("imported" in clean && clean.imported, 1); + assert.equal("commentWriteFailures" in clean ? clean.commentWriteFailures : 0, 0); + assert.equal((await listAllComplete({}, { pmRoot: root })).items.length, 2); + }); +})); + + +test("plain curated owner updates add the source reference without converting retained native tags", async () => tracker(async root => { + const tags = ["area,api", '["json"]'], item = await native(root, { github_url: url, tags }); + await withMockGithub((request, response) => jsonResponse(response, 200, request.url!.includes("/issues?") ? [source()] : []), async () => { + await runImport(repo, root, parseImportOptions({})); + }); + const written = (await get(item.id!, {}, { pmRoot: root })).item; + assert.deepEqual(new Set(written.tags), new Set([...tags, "gh:o/r#7"])); + assert.equal(written.title, item.title); assert.equal(written.type, item.type); assert.equal(written.created_at, item.created_at); + assert.equal((await listAllComplete({}, { pmRoot: root })).items.length, 1); +})); + +test("atomic publication preserves the previous plan when a concurrent writer replaces its parent", async () => { + const root = mkdtempSync(join(tmpdir(), "github-parent-race-")), directory = join(root, "publisher"), held = join(root, "held"), file = join(directory, "plan.json"); + try { + await writeGithubStateAtomic(file, "previous plan"); + let replaced = false; + await assert.rejects(() => writeGithubStateAtomic(file, "new plan", { get exclusive() { + if (!replaced) { renameSync(directory, held); writeFileSync(directory, "concurrent parent replacement"); replaced = true; } + return true; + } }), error => (error as NodeJS.ErrnoException).code === "ENOTDIR"); + assert.equal(readFileSync(join(held, "plan.json"), "utf8"), "previous plan"); + assert.equal(readFileSync(directory, "utf8"), "concurrent parent replacement"); + rmSync(directory); renameSync(held, directory); + await writeGithubStateAtomic(file, "restored publisher"); assert.equal(readFileSync(file, "utf8"), "restored publisher"); + } finally { rmSync(root, { recursive: true, force: true }); } +}); + + +test("legacy retained refresh plans without routing tuples report their source numbers outside scope", async () => tracker(async root => { + const owned = await native(root, { github_url: url }); + await native(root, { github_url: "https://github.com/o/r/issues/9" }); + const entry = (await prepareGithubImport(source(), repo, parseImportOptions({}), undefined, owned, undefined, + { items: [owned], releases: [], timeline: async () => [] }))!; + await assert.rejects(() => importGithubAtomic(root, repo, [entry], { updateOnly: true, onTransition: context => { + if (context.transition === "committed") throw new WorkspaceTransactionInterruptedError("Retain legacy refresh transaction"); + } }), /committed journal/); + const [retained] = await listRetainedGithubImportPlans(root); assert.ok(retained); + const file = join(root, "transactions/pm-github", `${retained.transactionId}.json`), legacy = JSON.parse(readFileSync(file, "utf8")) as Record; + delete legacy.itemIds; writeFileSync(file, JSON.stringify(legacy)); + const before = bytes(root, owned); + await withMockGithub((_request, response) => jsonResponse(response, 200, { data: { repository: { i9: { number: 9, title: "Current source", body: "Public", state: "OPEN", stateReason: null, + createdAt: "2025-01-01T00:00:00Z", updatedAt: "2025-03-01T00:00:00Z", closedAt: null, url: "https://github.com/o/r/issues/9", author: null, + labels: { nodes: [], pageInfo: { hasNextPage: false, endCursor: null } } } } } }), async () => { + const preview = await runRefresh(repo, root, { "dry-run": true }, { numbers: [9] }); + assert.deepEqual(preview.deferred_imports![0]!.issue_numbers, [7]); assert.equal(preview.resumed, 0); assert.equal(preview.selected, 1); + }); + assert.deepEqual(bytes(root, owned), before); assert.deepEqual(JSON.parse(readFileSync(file, "utf8")), legacy); +})); + + +test("off-board metadata projections without tags remain outside board ownership scope", async () => tracker(async root => { + const stored = await native(root, { github_url: "https://github.com/o/r/issues/8" }); + const { tags, ...view } = stored; assert.deepEqual(tags, ["retained"]); + const board = [{ id: "board-seven", content: { typename: "Issue" as const, repo, number: 7, title: "Board issue" } }], ref = { owner: "o", number: 1 }; + assert.deepEqual(buildProjectPullPlan([view], ref, board, undefined, undefined, [stored]).entries, []); + assert.equal(buildProjectImportPlan(board, ref, [view])[0]!.action, "create"); +})); + + +test("fidelity boundary projections retain references without inventing ownership or completion", async () => tracker(async root => { + const item = await native(root, { links: undefined, external_links: ["https://example.invalid/reference"] }); + assert.deepEqual(githubLinkReferences({ ...item, links: undefined }), []); + assert.match(new GithubIdentityConflictError(undefined, ["o/r#7", "x/y#8"]).message, //); + const closed: GhIssue = { ...source(), state: "closed", closed_at: "2025-02-01T00:00:00Z" }; + const { created_at: _created, ...withoutDate } = item; + const fields = importFidelityFields(buildImportFidelity(closed, [], []), false, withoutDate); + assert.equal(Object.hasOwn(fields, "completed_at"), false); assert.equal(Object.hasOwn(fields, "closed_at"), false); + assert.match(importCloseReason(closed, [{ __typename: "ConnectedEvent", subject: { url: "https://github.com/o/r/commit" } }]), /Closed/); + assert.equal(matchImportedIssue([{ ...item, description: "#7 plus /r#8" }], source()).item, undefined); + const expanded = "İ".repeat(25) + " github.com/ o/r issues7"; + assert.equal(matchImportedIssue([{ ...item, description: expanded }], source()).item, undefined); + const first = await native(root, { linear_url: "https://linear.app/fixture/issue/FIX-7" }); + const second = await native(root, { linear_url: "https://linear.app/fixture/issue/FIX-7" }); + assert.throws(() => matchImportedIssue([first, second], { ...source(), body: "https://linear.app/fixture/issue/FIX-7" }), { code: "github_import_owner_conflict" }); +})); + +test("plain tag arguments replace importer tags but add curated tags without loss", async () => tracker(async root => { + const fresh = (await prepareGithubImport(source(), repo, parseImportOptions({}), undefined, undefined, undefined, { items: [], releases: [], timeline: async () => [] }))!; + assert.deepEqual(importTagArguments(fresh), ["--tags", "gh:o/r#7"]); + const item = await native(root, { github_url: url, tags: ["retained"] }); + const curated = (await prepareGithubImport(source(), repo, parseImportOptions({}), undefined, item, undefined, { items: [item], releases: [], timeline: async () => [] }))!; + assert.deepEqual(importTagArguments(curated), ["--add-tags", "gh:o/r#7"]); +})); + +test("ownership diagnostics preserve missing-id caller and peer-conflict details", async () => tracker(async root => { + const item = await native(root, { github_url: url, github_repo: "other/project" }); + const { id: _id, ...anonymous } = item; + for (const target of [undefined, { id: "requested-owner", repo, number: 7 }]) { + assert.throws(() => assertGithubOwnershipConsistency(anonymous, target), error => { + assert.equal((error as { itemId: string }).itemId, target?.id ?? ""); return true; + }); + } + const peer = await native(root, { github_url: url }); + const claimant = { ...anonymous, github_repo: repo }; + for (const target of [undefined, { id: "requested-owner", repo, number: 7 }]) assert.throws(() => assertGithubOwnershipConsistency(claimant, target, [peer]), { code: "github_import_owner_conflict" }); + const imported = { ...peer, id: `fixture-github-${githubRepositoryHash(repo)}-7`, author: "pm-github" }; + const { tags: _tags, ...untagged } = imported; + assert.deepEqual(githubOwnershipClaims(untagged).owners, ["o/r#7"]); +})); + +test("native ownership hooks forward indexing, count real reads and reject missing dependency endpoints", async () => tracker(async root => { + const item = await native(root, { github_url: url }), target = { id: item.id!, repo, number: 7 }; + const observed: GithubOwnershipWork = {}, measured: GithubOwnershipWork = {}; + await withGithubOwnershipWork(observed, async () => { + assert.equal((await readGithubOwnershipSnapshot(root, [repo])).byId.has(item.id!), true); + await withGithubImportOwners(root, [target], async () => { + await withGithubImportOwners(root, [{ ...target, expectedHash: await readGithubImportOwner(root, target) }], async () => { + await update(item.id!, { title: "Allowed current owner" }, { pmRoot: root }); + }, measured); + }, measured); + }); + assert.ok(observed.corpusReads! >= 3); assert.ok(measured.corpusReads! >= 3); + const before = bytes(root, item); + await assert.rejects(() => withGithubImportOwners(root, [{ ...target, related: [{ ...target, id: "fixture-missing" }] }], () => update(item.id!, { title: "Must refuse" }, { pmRoot: root })), /missing dependency endpoint/); + assert.deepEqual(bytes(root, item), before); + let indexed = 0; + await runWithActiveExtensions({ path: root, noExtensions: true }, async () => { + setActiveExtensionHooks({ beforeCommand: [], afterCommand: [], beforeMutation: [], onRead: [], onWrite: [], onIndex: [{ layer: "project", name: "observe-index", run: () => { indexed++; } }] }); + await withGithubImportOwners(root, [target], () => runReindex({ mode: "keyword" }, { path: root })); + }); + assert.ok(indexed > 0, "The native index event must reach the ambient delegate"); +})); + +test("board consistency scopes native mutation hooks and refuses ownership changes", async () => tracker(async root => { + const owner = await native(root, { github_url: url }), unrelated = await native(root); + await withGithubItemConsistency(root, owner.id!, ["o/r#7"], async () => { + await update(unrelated.id!, { title: "Unrelated native update" }, { pmRoot: root }); + await update(owner.id!, { title: "Allowed owner update" }, { pmRoot: root }); + }); + const before = bytes(root, owner); + await assert.rejects(() => withGithubItemConsistency(root, owner.id!, [], () => update(owner.id!, { title: "Refused wrong expected ownership" }, { pmRoot: root })), /original unowned project item/); + assert.deepEqual(bytes(root, owner), before); + const settings = await readSettings(root); + await withGithubItemConsistency(root, unrelated.id!, [], () => create({ title: "Unrelated creation", type: "Chore" }, { pmRoot: root })); + await assert.rejects(() => withGithubItemConsistency(root, unrelated.id!, [], () => mutateItem({ pmRoot: root, id: unrelated.id!, settings, author: "fixture", op: "update", mutate: document => { + document.metadata.github_url = url; return { changedFields: ["github_url"] }; + } })), /original unowned project item|claimed by/); +})); + +test("recovery validation rejects malformed original dependencies and target tuples before writes", async () => tracker(async root => { + const item = await native(root, { github_url: url }), before = bytes(root, item); + const entry = (await prepareGithubImport(source(), repo, parseImportOptions({ atomic: true }), undefined, item, undefined, { items: [item], releases: [], timeline: async () => [] }))!; + await assert.rejects(() => importGithubAtomic(root, repo, [entry], { onPlanStaged: () => { throw new Error("retain original fixture"); } }), /could not be published/); + const files = join(root, "transactions", "pm-github"); + assert.deepEqual(readdirSync(files), []); + await assert.rejects(() => importGithubAtomic(root, repo, [{ ...entry, preserveContent: false, match: { ...item, author: "fixture" } }]), { code: "github_import_plan_ownership_invalid" }); + assert.deepEqual(bytes(root, item), before); + await assert.rejects(() => importGithubAtomic(root, repo, [entry], { onTransition: context => { if (context.transition === "committed") throw new WorkspaceTransactionInterruptedError("retained"); } }), /committed journal/); + const [record] = await listRetainedGithubImportPlans(root); assert.ok(record); + const invalid: GithubImportPlanRecord[] = [ + { ...record, dependencyIssues: [{ ...source(), body: undefined } as unknown as GhIssue] }, + { ...record, itemIds: [[7, ""]] }, + { ...record, entries: [...record.entries, { ...entry, issueNumber: 8 }], itemIds: [[7, item.id!], [8, item.id!]] }, + ]; + for (const plan of invalid) assert.throws(() => validateGithubImportPlan(plan), { code: "github_import_plan_drift" }); + const file = join(files, `${record.transactionId}.json`), saved = readFileSync(file); + writeFileSync(file, JSON.stringify(invalid[0])); + await assert.rejects(() => readGithubImportPlan(root, record.transactionId), { code: "github_import_plan_drift" }); + writeFileSync(file, saved); +})); + +test("recovery discovery refuses non-directory state and atomic dependency pass uses the original entries", async () => tracker(async root => { + mkdirSync(join(root, "transactions"), { recursive: true }); writeFileSync(join(root, "transactions", "pm-github"), "not a directory"); + await assert.rejects(() => listRetainedGithubImportPlans(root), { code: "github_import_plan_missing" }); + rmSync(join(root, "transactions", "pm-github")); + const entry = (await prepareGithubImport(source(), repo, parseImportOptions({ atomic: true }), undefined, undefined, undefined, { items: [], releases: [], timeline: async () => [] }))!; + const result = await importGithubAtomic(root, repo, [entry], { linkDeps: true }); + assert.equal(result.imported, 1); assert.equal((await listAllComplete({}, { pmRoot: root })).items.length, 1); assert.ok(result.depLink); +})); + + +for (const mutation of ["remove", "relink"] as const) test(`sync refuses a native target that disappears or relinks after planning (${mutation})`, async () => tracker(async root => { + const local = await native(root, { github_url: url }), settings = await readSettings(root); + const harness = await createExtensionTestHarness(extension, { capabilities: ["commands", "importers", "schema", "hooks", "preflight", "search"] }); + await withMockGithub(async (_request, response) => { + if (mutation === "remove") await deleteItem(local.id!, {}, { pmRoot: root }); + else await mutateItem({ pmRoot: root, id: local.id!, settings, author: "fixture", op: "update", mutate: document => { + document.metadata.github_url = "https://github.com/o/r/issues/8"; return { changedFields: ["github_url"] }; + } }); + jsonResponse(response, 200, { ...source(), state: "closed" }); + }, async server => { + await assert.rejects(() => harness.runCommand({ command: "github sync", options: { repo, ids: local.id }, pmRoot: root }), { code: "github_import_owner_conflict" }); + assert.equal(server.requests.some(request => request.method !== "GET"), false); + }); +})); + +test("export refuses a disappeared planning owner and distinguishes missing-id drafts", async () => tracker(async root => { + const local = await native(root, { github_url: url }), items = [local]; + const plan = buildExportPlan(items, repo); items.pop(); + await withMockGithub((_request, response) => jsonResponse(response, 200, source()), async server => { + await assert.rejects(() => applyExportPlan(plan, repo, "fixture", (method, target, _token, body) => fetch(target, { method, body })), { code: "github_import_owner_conflict" }); + assert.deepEqual(server.requests, []); + }); + const { id: _id, ...anonymous } = await native(root); + assert.deepEqual(planSync([anonymous], repo), []); + assert.deepEqual(planSync([local], ""), []); + const drafts = buildExportPlan([anonymous], repo); + assert.equal(drafts[0]!.action, "create"); assert.equal(drafts[0]!.id, undefined); + const anonymousItems = [anonymous], malformed = buildExportPlan(anonymousItems, repo); anonymousItems.pop(); + malformed[0]!.action = "update"; malformed[0]!.number = 7; + await assert.rejects(() => applyExportPlan(malformed, repo, "fixture", async () => { throw new Error("Must not write"); }), { code: "github_import_owner_conflict" }); + const missingNumber = buildExportPlan([local], repo); delete missingNumber[0]!.number; + await assert.rejects(() => applyExportPlan(missingNumber, repo, "fixture", async () => { throw new Error("Must not write"); }), { code: "github_import_owner_conflict" }); + await withMockGithub((_request, response) => jsonResponse(response, 201, source()), async server => { + const applied = await applyExportPlan(drafts, repo, "fixture", (method, target, _token, body) => fetch(target, { method, body })); + assert.equal(applied.created, 1); assert.equal(server.requests[0]!.method, "POST"); + }); + const { github_url: _url, ...unowned } = local; + const ownerItems = [local], changed = buildExportPlan(ownerItems, repo); ownerItems[0] = unowned; + await assert.rejects(() => applyExportPlan(changed, repo, "fixture", async () => { throw new Error("Must not request"); }), { code: "github_import_owner_conflict" }); +})); + +test("markdown export preview renders existing owners and project pull accepts a native unowned legacy entry", async () => tracker(async root => { + const local = await native(root, { github_url: url }), harness = await createExtensionTestHarness(extension, { capabilities: ["commands", "importers", "schema", "hooks", "preflight", "search"] }); + const preview = await captureStderr(() => harness.runCommand({ command: "github export", pmRoot: root, options: { repo, ids: local.id, format: "md" }, global: { json: false } })); + assert.ok(preview.stderr.some(line => line.includes("[update #7]"))); + const unowned = await native(root); + const pulled = await applyPullEntry({ itemId: "board-item", pmId: unowned.id!, title: "Native local item", toStatus: "in_progress", fromStatus: "open" }, root); + assert.equal(pulled.changed, true); + assert.equal((await get(unowned.id!, {}, { pmRoot: root })).item.status, "in_progress"); +})); + +test("atomic plan staging refuses a competing publication without replacing original bytes", async () => tracker(async root => { + const entry = (await prepareGithubImport(source(), repo, parseImportOptions({ atomic: true }), undefined, undefined, undefined, { items: [], releases: [], timeline: async () => [] }))!; + let target = ""; + await assert.rejects(() => importGithubAtomic(root, repo, [entry], { onPlanStaged: context => { + target = context.file; writeFileSync(target, "competing durable plan"); + } }), { code: "github_import_plan_drift" }); + assert.equal(readFileSync(target, "utf8"), "competing durable plan"); + assert.equal((await listAllComplete({}, { pmRoot: root })).items.length, 0); +})); + + +test("nested revision guards share only equivalent scopes and preserve unexpected failures", async () => tracker(async root => { + const item = await native(root, { github_url: url }), target = { id: item.id!, repo, number: 7 }, hash = await readGithubImportOwner(root, target); + await withGithubImportOwners(root, [{ ...target, expectedHash: hash }], () => withGithubImportOwners(root, [{ ...target, expectedHash: hash, related: [] }], () => update(item.id!, { title: "Equivalent fenced nested owner" }, { pmRoot: root }))); + const before = bytes(root, item), broken = { ...target }; + Object.defineProperty(broken, "related", { get: () => { throw new Error("caller endpoint inspection failed"); } }); + await assert.rejects(() => withGithubImportOwners(root, [broken], () => update(item.id!, { title: "Must not apply" }, { pmRoot: root })), /Extension mutation guard github-import-owner failed/); + assert.deepEqual(bytes(root, item), before); + const unowned = await native(root), settings = await readSettings(root), unchanged = bytes(root, unowned); + await assert.rejects(() => withGithubImportOwners(root, [{ id: unowned.id!, repo, number: 9, initialLink: () => true }], () => mutateItem({ pmRoot: root, id: unowned.id!, settings, author: "fixture", op: "update", mutate: document => { + delete (document.metadata as Record).id; return { changedFields: ["id"] }; + } })), /changed or removed the target id/); + assert.deepEqual(bytes(root, unowned), unchanged); +})); + + +test("closeout isolates typed ownership conflicts and propagates unrelated parser failures", async () => tracker(async root => { + const item = await native(root), before = bytes(root, item); + const conflict = new GithubIdentityConflictError(item.id, ["o/r#7", "other/project#8"]); + const declared = { ...item }; + Object.defineProperty(declared, "github_url", { enumerable: true, configurable: true, get: () => { throw conflict; } }); + assert.equal(closeoutOwnershipConflict(declared as Parameters[0], repo, []), conflict); + Object.defineProperty(declared, "github_url", { enumerable: true, configurable: true, get: () => { throw new Error("identity reader failed"); } }); + assert.throws(() => closeoutOwnershipConflict(declared as Parameters[0], repo, []), /identity reader failed/); + assert.deepEqual(bytes(root, item), before); + assert.deepEqual(extractCloseoutItems("Closes https://github.com/o/r/blob/main/.agents/pm/issues/.toon"), []); +})); + +test("a dangling retained plan is reported missing without changing tracker bytes", async () => tracker(async root => { + const item = await native(root), before = bytes(root, item), directory = join(root, "transactions", "pm-github"); + mkdirSync(directory, { recursive: true }); symlinkSync("missing-original.json", join(directory, "github-import-dangling.json")); + await assert.rejects(() => listRetainedGithubImportPlans(root), { code: "github_import_plan_missing" }); + assert.deepEqual(bytes(root, item), before); +})); + +test("publication-in-progress lock tokens are live before the final directory sync", async () => tracker(async root => { + let contender: string | undefined; + const owner = await acquireImportLock(root, "fixture-publishing", { waitMs: 0, onStaged: async ({ file, staged }) => { + copyFileSync(staged, file); + contender = (await acquireImportLock(root, "fixture-publishing", { waitMs: 0 })).status; + rmSync(file); + } }); + assert.equal(contender, "contended"); assert.equal(owner.status, "acquired"); + if (owner.status === "acquired") owner.lock.release(); +})); + + +test("citation exclusions retain malformed repository fragments and undefined link fields", async () => tracker(async root => { + const item = await native(root); + assert.deepEqual(githubLinkReferences({ ...item, links: undefined }), []); + for (const text of ["github.com/ issues7", "İ".repeat(11) + "github.com/ issues7", "İ".repeat(30) + "github.com/ issues7", "github.com/o/r/issues/7", "/r#7"]) { + assert.equal(matchImportedIssue([{ ...item, description: text }], source()).item, undefined); + } +})); + +test("owner observers can reset counters before a native locked write", async () => tracker(async root => { + const item = await native(root, { github_url: url }), target = { id: item.id!, repo, number: 7 }, observed: GithubOwnershipWork = {}, work: GithubOwnershipWork = {}; + await withGithubOwnershipWork(observed, () => withGithubImportOwners(root, [target], async () => { + delete observed.corpusReads; delete work.corpusReads; + await update(item.id!, { title: "Count the locked corpus" }, { pmRoot: root }); + }, work)); + assert.equal(observed.corpusReads, 1); assert.equal(work.corpusReads, 1); +})); + +test("a retained import refuses a fresh dry run before fetching changed provider input", async () => tracker(async root => { + const entry = (await prepareGithubImport(source(), repo, parseImportOptions({ atomic: true }), undefined, undefined, undefined, { items: [], releases: [], timeline: async () => [] }))!; + await assert.rejects(() => importGithubAtomic(root, repo, [entry], { onTransition: context => { if (context.transition === "prepared") throw new WorkspaceTransactionInterruptedError("retained original input"); } }), /interrupted/); + let requests = 0; + await withMockGithub((_request, response) => { requests++; jsonResponse(response, 200, [source()]); }, async () => { + await assert.rejects(() => runImport(repo, root, parseImportOptions({ atomic: true, "dry-run": true })), { code: "github_import_plan_pending" }); + }); + assert.equal(requests, 0); assert.equal((await listAllComplete({}, { pmRoot: root })).items.length, 0); +})); + +test("an unexpected identity-field read during planning is preserved before any SDK write", async () => tracker(async root => { + const item = await native(root, { github_url: url }), entry = (await prepareGithubImport(source(), repo, parseImportOptions({ atomic: true }), undefined, item, undefined, { items: [item], releases: [], timeline: async () => [] }))!; + const input = { ...item, author: "pm-github" }; + Object.defineProperty(input, "github_url", { enumerable: true, get: () => { throw new Error("caller identity field unavailable"); } }); + const before = bytes(root, item); + await assert.rejects(() => importGithubAtomic(root, repo, [{ ...entry, identity: undefined, preserveContent: false, match: input }]), /caller identity field unavailable/); + assert.deepEqual(bytes(root, item), before); +})); + + +test("project helpers refuse missing coordinates or native targets before a GraphQL write", async () => tracker(async root => { + await withMockGithub((_request, response) => jsonResponse(response, 200, { data: { repository: { issueOrPullRequest: { id: "issue-node-7" } } } }), async server => { + assert.equal(await gqlResolveIssueNodeId("invalid", 7, "fixture"), undefined); assert.deepEqual(server.requests, []); + assert.equal(await gqlResolveIssueNodeId(repo, 7, "fixture"), "issue-node-7"); + assert.equal(server.requests.length, 1); + const result = await applyPushEntry({ pmId: "fixture-missing", action: "add-draft", title: "Must not write" }, { id: "board-node", title: "Board", url: "https://github.com/orgs/o/projects/1", ownerType: "organization" }, { owner: "o", number: 1 }, new Map(), root, "fixture", []); + assert.equal(result.changed, false); assert.match(result.error!, /missing tracker item/); assert.equal(server.requests.length, 1); + }); +})); + +test("legacy atomic entries retain their body and fallback dependency source dates", async () => tracker(async root => { + const entry = (await prepareGithubImport(source(), repo, parseImportOptions({ atomic: true }), undefined, undefined, undefined, { items: [], releases: [], timeline: async () => [] }))!; + const result = await importGithubAtomic(root, repo, [{ ...entry, sourceBody: undefined, fidelity: undefined }], { linkDeps: true }); + assert.equal(result.imported, 1); assert.equal(result.depLink!.linked, 0); + assert.equal((await listAllComplete({ includeBody: true }, { pmRoot: root })).items[0]!.body, entry.body); + const closedIssue: GhIssue = { ...source(8), state: "closed", closed_at: "2025-02-01T00:00:00Z" }; + const closed = (await prepareGithubImport(closedIssue, repo, parseImportOptions({}), undefined, undefined, undefined, { items: [], releases: [], timeline: async () => [] }))!; + assert.equal((await importGithubAtomic(root, repo, [closed], { linkDeps: true })).imported, 1); +})); + +test("provider-linked legacy plans use the retained body when separate sourceBody is absent", async () => tracker(async root => { + const local = await native(root, { linear_url: "https://linear.app/fixture/issue/FIX-7" }), issue = { ...source(), body: "https://linear.app/fixture/issue/FIX-7" }; + const entry = (await prepareGithubImport(issue, repo, parseImportOptions({ atomic: true }), undefined, local, undefined, { items: [local], releases: [], timeline: async () => [] }))!; + const legacy = { ...entry, sourceBody: undefined }; + assert.equal(githubImportTarget(repo, legacy, local.id!).initialLink?.(local), true); + const result = await importGithubAtomic(root, repo, [legacy]); assert.equal(result.updated, 1); + assert.equal((await listAllComplete({}, { pmRoot: root })).items.length, 1); +})); + + +test("dependency mutation policy failures retain native endpoints and report the SDK refusal", async () => tracker(async root => { + const first = await native(root, { github_url: url }), second = await native(root, { github_url: "https://github.com/o/r/issues/8" }), before = [first, second].map(item => bytes(root, item)); + await runWithActiveExtensions({ path: root, noExtensions: true }, async () => { + setActiveExtensionHooks({ beforeCommand: [], afterCommand: [], onRead: [], onWrite: [], onIndex: [], beforeMutation: [{ layer: "project", name: "dependency-policy", run: () => ({ allow: false, code: "fixture_dependency_denied", remediation: "Keep this native edge unchanged" }) }] }); + const result = await linkImportedDependencies(repo, [{ ...source(), body: "Blocked by #8" }, { ...source(8), body: null }], root); + assert.equal(result.linked, 0); assert.equal(result.failures.length, 1); assert.match(result.failures[0]!, /fixture_dependency_denied|dependency-policy/); + }); + assert.deepEqual([first, second].map(item => bytes(root, item)), before); +})); + +test("native after-command hints tolerate normalized short ids and reference-only items", async () => tracker(async root => { + const owner = await native(root, { github_url: url }), reference = await native(root, { tags: ["gh:o/r#7"] }); + const harness = await createExtensionTestHarness(extension, { capabilities: ["commands", "importers", "schema", "hooks", "preflight", "search"] }); + await withEnv({ PM_GITHUB_SYNC: "1" }, async () => { + for (const id of [owner.id!.slice((await readSettings(root)).id_prefix.length), reference.id!]) { + const result = await captureStderr(() => harness.runHook({ kind: "after_command", context: { command: "update", args: [id], pm_root: root, ok: true } })); + assert.deepEqual(result.stderr, []); + } + }); +})); + +test("atomic refresh skips another target's unfinished journal and refuses an unlinked update", async () => tracker(async root => { + const first = await native(root, { github_url: url }), second = await native(root, { github_url: "https://github.com/o/r/issues/8" }), items = [first, second]; + const entries = await Promise.all(items.map((item, i) => prepareGithubImport(source(7 + i), repo, parseImportOptions({}), undefined, item, undefined, { items, releases: [], timeline: async () => [] }))); + await assert.rejects(() => importGithubAtomic(root, repo, [entries[0]!], { onTransition: context => { if (context.transition === "step_applied") throw new WorkspaceTransactionInterruptedError("Unselected native journal"); } }), /interrupted/); + assert.ok((await listUnfinishedGithubImports(root))[0]!.itemIds.length > 0); + const result = await importGithubAtomic(root, repo, [entries[1]!], { updateOnly: true }); assert.equal(result.updated, 1); + const before = bytes(root, second); + await assert.rejects(() => importGithubAtomic(root, repo, [{ ...entries[1]!, match: undefined }], { updateOnly: true }), /unlinked|requires|match/); + assert.deepEqual(bytes(root, second), before); +})); + +test("two independently retained native plans refuse implicit recovery until selected explicitly", async () => tracker(async root => { + const items = [await native(root, { github_url: url }), await native(root, { github_url: "https://github.com/o/r/issues/8" })]; + const staged: Array<[string, Buffer]> = []; + for (let i = 0; i < items.length; i++) { + const entry = (await prepareGithubImport(source(7 + i), repo, parseImportOptions({}), undefined, items[i], undefined, { items, releases: [], timeline: async () => [] }))!; + await assert.rejects(() => importGithubAtomic(root, repo, [entry], { onTransition: context => { if (context.transition === "committed") throw new WorkspaceTransactionInterruptedError("Retain completed native plan"); } }), /committed journal/); + const [record] = await listRetainedGithubImportPlans(root); assert.ok(record); + const file = join(root, "transactions", "pm-github", `${record.transactionId}.json`); staged.push([file, readFileSync(file)]); rmSync(file); + } + for (const [file, contents] of staged) writeFileSync(file, contents); + const before = items.map(item => bytes(root, item)); + await assert.rejects(() => importGithubAtomic(root, repo, []), { code: "github_import_plan_drift" }); + assert.deepEqual(items.map(item => bytes(root, item)), before); +})); + + +test("upstream pre-write checks refuse absent caller coordinates and mismatched create ownership", async () => tracker(async root => { + const item = await native(root, { github_url: url }), before = bytes(root, item); + assert.throws(() => assertCurrentUpstreamTarget(root, repo, {}, {}), error => { + assert.equal((error as { itemId: string }).itemId, ""); assert.equal((error as { expected: string }).expected, "o/r#new"); return true; + }); + assert.throws(() => assertCurrentUpstreamTarget(root, repo, { id: item.id }, {}), { code: "github_import_owner_conflict" }); + assert.doesNotThrow(() => assertCurrentUpstreamTarget(root, repo, { id: item.id, number: 7 }, {})); + assert.deepEqual(bytes(root, item), before); +})); + + +for (const scoped of [false, true]) test(`a retained native plan replays and reselects its current owner (scoped=${scoped})`, async () => tracker(async root => { + const local = await native(root, { github_url: url }); + const entry = (await prepareGithubImport(source(), repo, parseImportOptions({}), undefined, local, undefined, { items: [local], releases: [], timeline: async () => [] }))!; + await assert.rejects(() => importGithubAtomic(root, repo, [entry], { onTransition: context => { if (context.transition === "committed") throw new WorkspaceTransactionInterruptedError("Retain completed source plan"); } }), /committed journal/); + await withMockGithub((request, response, body) => { + if (request.method === "GET") { jsonResponse(response, 200, []); return; } + const query = (JSON.parse(body) as { query: string }).query; + jsonResponse(response, 200, { data: { repository: query.includes("timelineItems") ? { issue: { timelineItems: { nodes: [], pageInfo: { hasNextPage: false, endCursor: null } } } } : { i7: { + number: 7, title: source().title, body: source().body, state: "OPEN", stateReason: null, + createdAt: source().created_at, updatedAt: source().updated_at, closedAt: null, url, author: null, + labels: { nodes: [], pageInfo: { hasNextPage: false, endCursor: null } }, + } } } }); + }, async () => { + const result = await runRefresh(repo, root, {}, scoped ? { numbers: [7] } : undefined); assert.equal(result.resumed, 1); assert.equal(result.selected, 1); + assert.deepEqual(await listRetainedGithubImportPlans(root), []); + const after = bytes(root, local), repeated = await runRefresh(repo, root, {}, scoped ? { numbers: [7] } : undefined); + assert.equal(repeated.resumed, 0); assert.equal(repeated.updated, 0); assert.deepEqual(bytes(root, local), after); + }); +})); + + +test("a recovered target deleted before publication refuses without a provider request", async () => tracker(async root => { + const local = await native(root, { github_url: url }), entry = (await prepareGithubImport(source(), repo, parseImportOptions({}), undefined, local, undefined, { items: [local], releases: [], timeline: async () => [] }))!; + await assert.rejects(() => importGithubAtomic(root, repo, [entry], { onTransition: context => { if (context.transition === "committed") throw new WorkspaceTransactionInterruptedError("Retain committed source plan"); } }), /committed journal/); + const [record] = await listRetainedGithubImportPlans(root); assert.ok(record); + const directory = join(root, "transactions", "pm-github"), plan = join(directory, `${record.transactionId}.json`), itemFile = getItemPath(root, String(local.type), local.id!), history = readFileSync(getHistoryPath(root, local.id!)); + let deleted = false; + const watcher = watch(directory, () => { if (!existsSync(plan) && !deleted) { deleted = true; rmSync(itemFile); } }); + try { + await withMockGithub((_request, response) => jsonResponse(response, 200, [source()]), async server => { + await assert.rejects(() => runImport(repo, root, parseImportOptions({ atomic: true, gate: true })), { code: "github_import_owner_conflict" }); + assert.deepEqual(server.requests, []); + }); + assert.equal(deleted, true); assert.deepEqual(readFileSync(getHistoryPath(root, local.id!)), history); + } finally { watcher.close(); } +})); + + +test("a different repository journal arriving during fetch preserves the complete new atomic batch", async () => tracker(async root => { + let staged = false; + await withMockGithub(async (request, response) => { + if (request.url!.includes("/issues?") && !staged) { + staged = true; + const other = { ...source(99), html_url: "https://github.com/other/project/issues/99" }; + const entry = (await prepareGithubImport(other, "other/project", parseImportOptions({}), undefined, undefined, undefined, { items: [], releases: [], timeline: async () => [] }))!; + await assert.rejects(() => importGithubAtomic(root, "other/project", [entry], { onTransition: context => { if (context.transition === "step_applied") throw new WorkspaceTransactionInterruptedError("Independent pending producer"); } }), /interrupted/); + } + jsonResponse(response, 200, request.url!.includes("/issues?") ? [source()] : []); + }, async () => { + const result = await runImport(repo, root, parseImportOptions({ atomic: true })); + assert.equal("imported" in result && result.imported, 1); assert.equal(staged, true); + assert.equal((await listRetainedGithubImportPlans(root))[0]!.repo, "other/project"); + }); +})); + + +test("unrelated refresh leaves a malformed empty-target legacy plan and native owner bytes untouched", async () => tracker(async root => { + const local = await native(root, { github_url: url }), other = await native(root, { github_url: "https://github.com/o/r/issues/9" }); + const entry = (await prepareGithubImport(source(), repo, parseImportOptions({}), undefined, local, undefined, { items: [local, other], releases: [], timeline: async () => [] }))!; + await assert.rejects(() => importGithubAtomic(root, repo, [entry], { updateOnly: true, onTransition: context => { if (context.transition === "committed") throw new WorkspaceTransactionInterruptedError("Retain legacy source record"); } }), /committed journal/); + const [record] = await listRetainedGithubImportPlans(root); assert.ok(record); + const file = join(root, "transactions", "pm-github", `${record.transactionId}.json`); + const legacy = { ...record, itemIds: undefined, entries: [{ ...record.entries[0]!, match: { ...record.entries[0]!.match, id: "" } }], mutations: record.mutations.map(mutation => ({ ...mutation, id: "" })) }; + writeFileSync(file, JSON.stringify(legacy)); + const before = [local, other].map(item => bytes(root, item)), saved = readFileSync(file); + await withMockGithub((request, response) => { + assert.equal(request.method, "POST"); + jsonResponse(response, 200, { data: { repository: { i9: { number: 9, title: "Source 9", body: "Public source", state: "OPEN", stateReason: null, + createdAt: "2025-01-01T00:00:00Z", updatedAt: "2025-02-01T00:00:00Z", closedAt: null, url: "https://github.com/o/r/issues/9", author: null, + labels: { nodes: [], pageInfo: { hasNextPage: false, endCursor: null } } } } } }); + }, async () => { assert.equal((await runRefresh(repo, root, { "dry-run": true }, { numbers: [9] })).selected, 1); }); + assert.deepEqual([local, other].map(item => bytes(root, item)), before); assert.deepEqual(readFileSync(file), saved); +})); + + +test("native lease publication catches a peer relink after the plain import's revision read", async () => tracker(async root => { + const local = await native(root, { github_url: url }), settings = await readSettings(root); + let relinked = false, peerBytes: string[] | undefined; + await withMockGithub((request, response) => jsonResponse(response, 200, request.url!.includes("/issues?") ? [source()] : []), async server => { + await runWithActiveExtensions({ path: root, noExtensions: true }, async () => { + setActiveExtensionHooks({ beforeCommand: [], afterCommand: [], beforeMutation: [], onRead: [], onIndex: [], onWrite: [{ layer: "project", name: "peer-at-lease-publication", run: async context => { + if (relinked || !context.path.endsWith("github-ownership.lock")) return; + relinked = true; + await mutateItem({ pmRoot: root, id: local.id!, settings, author: "fixture-peer", op: "update", mutate: document => { + document.metadata.github_url = "https://github.com/other/project/issues/8"; return { changedFields: ["github_url"] }; + } }); + peerBytes = bytes(root, local); + } }] }); + await assert.rejects(() => runImport(repo, root, parseImportOptions({})), { code: "github_import_owner_conflict" }); + }); + assert.equal(relinked, true); assert.deepEqual(bytes(root, local), peerBytes); + assert.equal(server.requests.some(request => request.method !== "GET"), false); + }); +})); + +test("an optional atomic annotation receipt may be undefined without changing actual native writes", async () => tracker(async root => { + await withMockGithub((request, response) => jsonResponse(response, 200, request.url!.includes("/issues?") ? [source()] : []), async () => { + const result = await runImport(repo, root, parseImportOptions({ atomic: true }), { commitAtomic: async (trackerRoot, repository, entries) => ({ + ...await importGithubAtomic(trackerRoot, repository, entries), commentWriteFailures: undefined, + }) }); + assert.equal("imported" in result && result.imported, 1); + assert.equal("commentWriteFailures" in result, false); + assert.equal((await listAllComplete({}, { pmRoot: root })).items.length, 1); + }); +})); diff --git a/test/round-twelve.test.ts b/test/round-twelve.test.ts new file mode 100644 index 00000000..6eedeb9b --- /dev/null +++ b/test/round-twelve.test.ts @@ -0,0 +1,259 @@ +/** Complete review regressions use real SDK trackers, native writes and HTTP transport. */ +import assert from "node:assert/strict"; +import test from "node:test"; +import { mkdtempSync, rmSync, readFileSync, existsSync, chmodSync } from "node:fs"; +import { join, delimiter } from "node:path"; +import { tmpdir } from "node:os"; +import { + init, create, get, comments, mutateItem, readSettings, getItemPath, getHistoryPath, + listAllComplete, normalizeItemId, runWithActiveExtensions, setActiveExtensionHooks, WorkspaceTransactionInterruptedError, +} from "@unbrained/pm-cli/sdk"; +import { createExtensionTestHarness } from "@unbrained/pm-cli/sdk/testing"; +import extension, { runImport, parseImportOptions, prepareGithubImport, importGithubAtomic, deriveAtomicItemId, + readGithubImportPlan, listRetainedGithubImportPlans, applyPullEntry, type GhIssue, type PmItem, type GhComment } from "../index.ts"; +import { selectRefreshTargets, runRefresh } from "../refresh.ts"; +import { githubOwnershipClaims, withGithubImportOwners, withGithubOwnershipWork, type GithubOwnershipWork } from "../import-ownership.ts"; +import { buildProjectPullPlan, buildProjectPushPlan, indexPmByIssue } from "../projects.ts"; +import { writeGithubStateAtomic } from "../durable-state.ts"; +import { captureStderr, withEnv, withMockGithub, jsonResponse } from "./helpers/mock-github-server.ts"; + +const repo = "o/r"; +const source: GhIssue = { number: 7, title: "Public source", body: "Public body", state: "open", labels: [], user: null, + assignee: null, milestone: null, created_at: "2025-01-01T00:00:00Z", updated_at: "2025-02-01T00:00:00Z", html_url: "https://github.com/o/r/issues/7" }; +const harness = createExtensionTestHarness(extension, { capabilities: ["commands", "importers", "schema", "hooks", "preflight", "search"] }); + +/** Real non-git storage also exercises the package's required plain-tracker boundary. */ +async function tracker(run: (root: string) => Promise) { + const directory = mkdtempSync(join(tmpdir(), "github-r12-")), root = join(directory, "tracker"); + try { + await init("fixture", { defaults: true, withPackages: false, agentGuidance: "skip" }, { pmRoot: root, cwd: directory, noExtensions: true }); + await withEnv({ PM_PATH: root, PM_AUTHOR: "fixture", PM_TELEMETRY_SOURCE_CONTEXT: "automation", GITHUB_TOKEN: "fixture-token", GH_TOKEN: undefined, + PATH: join(import.meta.dirname, "../node_modules/.bin") + delimiter + process.env.PATH }, () => run(root)); + } finally { rmSync(directory, { recursive: true, force: true }); } +} +/** Alter stored provenance independently through the SDK, without substituting persistence APIs. */ +async function change(root: string, id: string, fields: Record, reset = false) { + await runWithActiveExtensions({ path: root, noExtensions: true }, async () => mutateItem({ pmRoot: root, id, settings: await readSettings(root), + author: "fixture", op: "update", mutate: document => { + if (reset) for (const key of Object.keys(document.metadata)) if (/^github/i.test(key) || key === "external_ref") delete document.metadata[key]; + Object.assign(document.metadata, fields); return { changedFields: Object.keys(fields) }; + } })); +} +/** Refused mutations preserve the target's complete item and append-only audit bytes. */ +async function bytes(root: string, id: string) { + const item = (await get(id, {}, { pmRoot: root })).item; + return [readFileSync(getItemPath(root, item.type!, id), "hex"), readFileSync(getHistoryPath(root, id), "hex")]; +} +/** Supply only external source data; preparation, ownership and native writes remain real. */ +async function importing(root: string, issue = source, atomic = false) { + return captureStderr(() => runImport(repo, root, parseImportOptions({ atomic }), { + resolveToken: () => undefined, fetchIssues: async () => [issue], fetchIssueTimeline: async () => [], + })); +} + +for (const field of ["github_url", "links", "provenance", "external_url"]) for (const api of [true, false]) test(`Round12 owner links ${field}: ${api || field !== "github_url" ? "reference refuses lifecycle ownership" : "explicit URL allows lifecycle"}`, async () => tracker(async root => { + const local = (await create({ title: "Curated Chore", type: "Chore", status: "open", tags: "keep,gh:o/r#7" }, { pmRoot: root })).item; + await change(root, local.id, { [field]: api ? "https://api.github.com/repos/o/r/issues/7" : "https://www.github.com/O/R/issues/7/" }); + const before = await bytes(root, local.id); + const result = await importing(root, { ...source, state: "closed", updated_at: "2027-01-01T00:00:00Z", closed_at: "2027-01-01T00:00:00Z", state_reason: "completed" }); + const actual = (await get(local.id, {}, { pmRoot: root })).item; + assert.equal(actual.title, local.title); assert.equal(actual.type, "Chore"); + assert.ok("imported" in result.result); assert.equal(result.result.imported, api || field !== "github_url" ? 1 : 0); + if (api || field !== "github_url") assert.deepEqual(await bytes(root, local.id), before); + else assert.equal(actual.status, "closed"); +})); + +for (const kind of ["pull", "issues"]) test(`Round12 verified legacy ID retains source kind ${kind}`, async () => tracker(async root => { + const id = deriveAtomicItemId(repo, 7, (await readSettings(root)).id_prefix, normalizeItemId); + await create({ id, author: "pm-github", title: "Legacy import", type: "Issue", description: `GH ${kind === "pull" ? "PR" : "issue"} #7: https://github.com/o/r/${kind}/7`, tags: "gh:o/r#99" }, { pmRoot: root }); + const item = (await get(id, {}, { pmRoot: root })).item; + assert.equal(item.github_number, undefined); + assert.deepEqual(githubOwnershipClaims(item).owners, ["o/r#7"]); + assert.deepEqual(selectRefreshTargets([item], repo).map(target => target.id), kind === "pull" ? [] : [id]); + await change(root, id, { description: "GH PR #8: https://github.com/o/r/pull/8" }); + assert.equal(githubOwnershipClaims((await get(id, {}, { pmRoot: root })).item).kinds.includes("pull"), false, "A mismatched prose PR does not classify the verified ID"); +})); + +for (const relink of [false, true]) test(`Round12 atomic annotations recheck ownership during comment sync (${relink})`, async () => tracker(async root => { + let committed = false, pending: Promise | undefined; + const id = deriveAtomicItemId(repo, 7, (await readSettings(root)).id_prefix, normalizeItemId); + const incoming = new Proxy([{ id: 1, body: "Source comment", created_at: source.updated_at, user: { login: "fixture" } }], { + get(target, key, receiver) { + if (key === "length" && committed && relink && !pending) pending = change(root, id, { github_repo: "other/project", github_number: 8, github_url: "https://github.com/other/project/issues/8" }); + return Reflect.get(target, key, receiver); + }, + }); + const entry = (await prepareGithubImport(source, repo, parseImportOptions({ "with-comments": true, "comments-mode": "annotations" }), undefined, undefined, async () => incoming, { items: [], releases: [], timeline: async () => [] }))!; + await runWithActiveExtensions({ path: root, noExtensions: true }, async () => { + setActiveExtensionHooks({ beforeCommand: [], afterCommand: [], beforeMutation: [], onWrite: [], onIndex: [], onRead: [{ layer: "project", name: "await-peer-relink", run: async context => { + if (pending && context.path.endsWith(`${id}.toon`)) await pending; + } }] }); + const action = () => importGithubAtomic(root, repo, [entry], { onTransition: context => { if (context.transition === "committed") committed = true; } }); + if (relink) await assert.rejects(action, /current owner|ownership|committed journal/); + else await action(); + }); + if (pending) await pending; + assert.equal((await comments(id, {}, { pmRoot: root })).comments.length, relink ? 0 : 1); +})); + +for (const field of ["sourceBody", "dependencyIssues", "match", "comments"]) test(`Round12 durable preflight scans persisted ${field}`, async () => tracker(async root => { + const entry = (await prepareGithubImport(source, repo, parseImportOptions({}), undefined, undefined, undefined, { items: [], releases: [], timeline: async () => [] }))!; + const secret = "ghp_" + "A".repeat(36); + if (field === "sourceBody") entry.sourceBody = secret; + if (field === "match") { const local = (await create({ title: "Curated", type: "Chore" }, { pmRoot: root })).item; await change(root, local.id, { github_url: source.html_url }); entry.match = { ...local, github_url: source.html_url, extra_source: secret }; entry.preserveContent = true; } + if (field === "comments") { entry.syncAnnotations = false; entry.comments = [{ id: 1, body: secret, created_at: source.updated_at, user: null }]; } + const opts = field === "dependencyIssues" ? { linkDeps: true, dependencyIssues: [{ ...source, body: secret }] } : {}; + let stages = 0; + await assert.rejects(() => importGithubAtomic(root, repo, [entry], { ...opts, onPlanStaged: () => { stages++; } }), { code: "github_import_preflight_failed" }); + assert.equal(stages, 0); assert.equal(existsSync(join(root, "transactions")), false); + const clean = { ...entry, sourceBody: source.body!, comments: [], ...(entry.match ? { match: { ...entry.match, extra_source: "Public" } } : {}) }; + await importGithubAtomic(root, repo, [clean], { ...opts, dependencyIssues: [source], onPlanStaged: () => { stages++; } }); + assert.equal(stages, 1); +})); + +for (const removed of [true, false]) test(`Round12 retained refresh plan never disappears from recovery (${removed})`, async () => tracker(async root => { + const local = (await create({ title: "Curated link", type: "Chore", status: "open" }, { pmRoot: root })).item; + await change(root, local.id, { github_url: source.html_url }); + const items = (await listAllComplete({ includeBody: true }, { pmRoot: root })).items; + const entry = (await prepareGithubImport(source, repo, parseImportOptions({}), undefined, items[0], undefined, { items, releases: [], timeline: async () => [] }))!; + await assert.rejects(() => importGithubAtomic(root, repo, [entry], { updateOnly: true, onTransition: context => { + if (context.transition === "committed") throw new WorkspaceTransactionInterruptedError("Retain finalization plan"); + } }), /committed journal/); + const record = (await listRetainedGithubImportPlans(root))[0]!; + if (removed) await change(root, local.id, { tags: ["gh:o/r#7"] }, true); + const before = await bytes(root, local.id); + await withMockGithub((_request, response) => jsonResponse(response, 200, { data: { repository: { i7: { + number: 7, title: source.title, body: source.body, state: "OPEN", stateReason: null, createdAt: source.created_at, updatedAt: source.updated_at, closedAt: null, + url: source.html_url, author: null, labels: { nodes: [], pageInfo: { hasNextPage: false } }, + } } } }), async () => { + if (removed) { + await assert.rejects(() => runRefresh(repo, root, {}), error => { + assert.equal((error as { code: string }).code, "github_import_owner_conflict"); assert.ok(String(error).includes(record.transactionId)); assert.ok(String(error).includes(local.id)); return true; + }); + assert.deepEqual(await bytes(root, local.id), before); assert.ok(await readGithubImportPlan(root, record.transactionId)); + } else assert.equal((await runRefresh(repo, root, {})).resumed, 1); + }); +})); + +for (const fail of [true, false]) test(`Round12 failed staging flushes created ancestors and exposes sync errors (${fail})`, { skip: process.platform === "win32" || process.getuid?.() === 0 }, async () => tracker(async root => { + const ancestor = join(root, "new"), file = join(ancestor, "nested/state.json"); + try { + await assert.rejects(() => writeGithubStateAtomic(file, "candidate", { onStaged: () => { + if (fail) chmodSync(ancestor, 0o111); + throw Object.assign(new Error("Staging interrupted"), { code: "staged_interruption" }); + } }), (error: unknown) => (error as { code: string }).code === (fail ? "EACCES" : "staged_interruption")); + } finally { if (existsSync(ancestor)) chmodSync(ancestor, 0o755); } + assert.equal(existsSync(file), false); + await writeGithubStateAtomic(file, "complete"); assert.equal(readFileSync(file, "utf8"), "complete"); +})); + +for (const duplicate of [true, false]) test(`Round12 scoped project pull checks unselected ownership peers (${duplicate})`, async () => tracker(async root => { + const first = (await create({ title: "Selected", type: "Chore", status: "open", tags: "gh-project:o/1#board" }, { pmRoot: root })).item; + await change(root, first.id, { github_url: source.html_url }); + const other = (await create({ title: "Peer", type: "Chore" }, { pmRoot: root })).item; + await change(root, other.id, { external_ref: duplicate ? "gh:o/r#7" : "gh:o/r#8" }); + const before = await bytes(root, first.id); + await withMockGithub((_request, response, body) => { + const query = JSON.parse(body).query as string; + jsonResponse(response, 200, query.includes("projectV2(number:") ? { data: { user: { projectV2: { id: "project", title: "Board", url: "https://example.test/board", statusField: { id: "status", name: "Status", options: [{ id: "progress", name: "In Progress" }] } } } } } + : { data: { node: { items: { pageInfo: { hasNextPage: false }, nodes: [{ id: "board", fieldValueByName: { name: "In Progress", optionId: "progress" }, content: { __typename: "Issue", title: "Board source", number: 7, repository: { nameWithOwner: repo } } }] } } } }); + }, async () => { + const action = () => harness.then(h => h.runCommand({ command: "github project sync", args: ["o/1"], options: { pull: true, apply: true, ids: first.id }, pmRoot: root, global: { json: true } })); + if (duplicate) await assert.rejects(action, { code: "github_import_owner_conflict" }); + else await action(); + }); + if (duplicate) assert.deepEqual(await bytes(root, first.id), before); + else assert.equal((await get(first.id, {}, { pmRoot: root })).item.status, "in_progress"); +})); + +for (const same of [true, false]) test(`Round12 concurrent first links serialize across item ids (${same})`, async () => tracker(async root => { + const settings = await readSettings(root); + const locals = await Promise.all(["First", "Second"].map(title => create({ title, type: "Chore" }, { pmRoot: root }))); + const attempts = locals.map((local, i) => { + const number = same ? 7 : 7 + i; + return withGithubImportOwners(root, [{ id: local.item.id, repo, number, initialLink: () => true }], () => mutateItem({ pmRoot: root, id: local.item.id, settings, author: "fixture", op: "update", mutate: document => { + document.metadata.github_url = `https://github.com/o/r/issues/${number}`; + return { changedFields: ["github_url"] }; + } })); + }); + const outcomes = await Promise.allSettled(attempts); + assert.equal(outcomes.filter(result => result.status === "fulfilled").length, same ? 1 : 2); + if (same) assert.equal((outcomes.find(result => result.status === "rejected") as PromiseRejectedResult).reason.code, "github_import_owner_conflict"); +})); + +test("Round12 project planners already parse one claim snapshot per corpus", async () => tracker(async root => { + const items: PmItem[] = []; + for (let i = 1; i <= 40; i++) { const item = (await create({ title: `Owner ${i}`, type: "Chore" }, { pmRoot: root })).item; await change(root, item.id, { github_url: `https://github.com/o/r/issues/${i}` }); items.push((await get(item.id, {}, { pmRoot: root })).item); } + const work: GithubOwnershipWork = {}; + withGithubOwnershipWork(work, () => { assert.equal(indexPmByIssue(items).size, 40); buildProjectPushPlan(items, { owner: "o", number: 1 }, [], undefined); buildProjectPullPlan(items, { owner: "o", number: 1 }, []); }); + assert.equal(work.claimParses, 120); +})); + + +for (const conflicting of [true, false]) test(`Round12 project pull rechecks peer claims inside the native write (${conflicting})`, async () => tracker(async root => { + const local = (await create({ title: "Curated board owner", type: "Chore", status: "open" }, { pmRoot: root })).item; + await change(root, local.id, { github_url: source.html_url }); + const peer = (await create({ title: "Initially unowned peer", type: "Chore" }, { pmRoot: root })).item; + const items = (await listAllComplete({ includeBody: true }, { pmRoot: root })).items; + const entry = buildProjectPullPlan(items, { owner: "o", number: 1 }, [{ id: "board", statusName: "In Progress", content: { typename: "Issue", title: "Board", repo, number: 7 } }]).entries[0]!; + await change(root, peer.id, { external_ref: conflicting ? "gh:o/r#7" : "gh:o/r#8" }); + const before = await bytes(root, local.id); + const result = await applyPullEntry(entry, root); + assert.equal(result.changed, !conflicting); + if (conflicting) { assert.match(result.error!, /claimed by/); assert.deepEqual(await bytes(root, local.id), before); } + else assert.equal((await get(local.id, {}, { pmRoot: root })).item.status, "in_progress"); +})); + +for (const selectedConflicts of [false, true]) test(`Round12 scoped project pull ignores conflicts confined to unselected items (selected conflicts: ${selectedConflicts})`, async () => tracker(async root => { + const selected = (await create({ title: "Selected board owner", type: "Chore", status: "open" }, { pmRoot: root })).item; + await change(root, selected.id, { github_url: source.html_url }); + // Two unselected items claim another issue, which is not on the board. + for (const title of ["Unselected owner A", "Unselected owner B"]) { + const peer = (await create({ title, type: "Chore" }, { pmRoot: root })).item; + await change(root, peer.id, { github_url: selectedConflicts ? source.html_url : "https://github.com/o/r/issues/9" }); + } + const corpus = (await listAllComplete({ includeBody: true }, { pmRoot: root })).items; + const scoped = corpus.filter(item => item.id === selected.id); + const board = [{ id: "board", statusName: "In Progress", content: { typename: "Issue" as const, title: "Board", repo, number: 7 } }]; + if (selectedConflicts) { + // Must refuse: an unselected peer claiming the selected item's own issue still stops the scoped pull. + assert.throws(() => buildProjectPullPlan(scoped, { owner: "o", number: 1 }, board, undefined, undefined, corpus), /claimed by|conflict/i); + } else { + const plan = buildProjectPullPlan(scoped, { owner: "o", number: 1 }, board, undefined, undefined, corpus); + assert.deepEqual(plan.entries.map(entry => entry.pmId), [selected.id]); + } +})); + +// Documentation is part of the public identity contract, including these deliberate limits. +test("Round twelve docs distinguish consistency from positive ownership and revisions", () => { + const inventory = readFileSync(new URL("../docs/identity-ownership-inventory.md", import.meta.url), "utf8"); + assert.match(inventory, /may return an empty owner list for an unowned item/); + assert.match(inventory, /does not enforce positive ownership or revisions/); + assert.match(inventory, /assertGithubImportOwner enforces a matching positive owner/); + assert.match(inventory, /readGithubImportOwner captures the revision/); +}); +test("Round twelve README post-write gate allows shared reference tags", () => { + const readme = readFileSync(new URL("../README.md", import.meta.url), "utf8"); + assert.match(readme, /Any number of items may share its reference tag/); + assert.doesNotMatch(readme, /exactly one provenance tag/); +}); + +test("Round12 preflight refuses with structured redacted evidence and allows clean data", async () => tracker(async root => { + const entry = (await prepareGithubImport(source, repo, parseImportOptions({}), undefined, undefined, undefined, { items: [], releases: [], timeline: async () => [] }))!; + const secret = "ghp_" + "B".repeat(36); + entry.sourceBody = secret; + let staged = 0; + await assert.rejects(() => importGithubAtomic(root, repo, [entry], { onPlanStaged: () => { staged++; } }), error => { + assert.equal((error as { code: string }).code, "github_import_preflight_failed"); + const message = (error as Error).message; + assert.equal(message[0], "{", "typed preflight errors retain the gate JSON envelope"); + const report = JSON.parse(message) as { findings: Array<{ rule: string; field: string; excerpt: string }> }; + assert.ok(Array.isArray(report.findings)); + assert.ok(report.findings.some(hit => hit.rule === "github-token-classic" && hit.field === "sourceBody")); + assert.equal(message.includes(secret), false); + return true; + }); + assert.equal(staged, 0); + await importGithubAtomic(root, repo, [{ ...entry, sourceBody: source.body! }], { onPlanStaged: () => { staged++; } }); + assert.equal(staged, 1); +})); diff --git a/test/scheduled-refusals.test.ts b/test/scheduled-refusals.test.ts index 89ed0711..ba12d8d3 100644 --- a/test/scheduled-refusals.test.ts +++ b/test/scheduled-refusals.test.ts @@ -1,4 +1,5 @@ /** Scheduled failures use real API transports and SDK trackers, including plain directories. */ +import { createImportMirror } from "./helpers/import-mirror.ts"; import assert from "node:assert/strict"; import { spawnSync } from "node:child_process"; import { mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs"; @@ -87,7 +88,7 @@ test("malformed policy objects and non-Error pagination failures never expose pa }); test("scheduled repeat import preserves item and history bytes after identical GitHub input", async () => tracker(async root => { - const local = await create({ type: "Issue", title: "Native linked issue", author: "fixture", tags: "gh:fixture/project#2" }, { pmRoot: root }); + const local = await createImportMirror({ type: "Issue", title: "Native linked issue", author: "fixture", tags: "gh:fixture/project#2" }, { pmRoot: root }); const issue = { number: 1, title: "Imported issue", body: "\n\nPublic content \n", state: "closed", state_reason: "completed", labels: [], assignee: null, milestone: null, created_at: "2026-10-01T00:00:00Z", updated_at: "2026-10-02T00:00:00Z", closed_at: "2026-10-02T00:00:00Z", html_url: "https://github.com/fixture/project/issues/1" }; await withMockGithub((request, response) => jsonResponse(response, 200, request.url === "/graphql" @@ -107,11 +108,17 @@ test("scheduled repeat import preserves item and history bytes after identical G await assert.rejects(commitItemMutations({ ...pending, onTransition: context => { if (context.transition === "committing") throw new WorkspaceTransactionInterruptedError("Fixture interruption"); } }), WorkspaceTransactionInterruptedError); - // Keep the source projection matched while leaving a real journal unfinished. - await runImport("fixture/project", root, opts); - const held = await runImport("fixture/project", root, opts); - assert.ok(!("unchanged" in held), "An unfinished journal cannot hide behind a no-op receipt."); + // A legacy journal without an original plan cannot be replaced by fresh input. + const held = snapshot(root); + for (let attempt = 0; attempt < 2; attempt++) { + await assert.rejects(() => runImport("fixture/project", root, opts), { code: "github_import_plan_missing" }); + assert.equal(snapshot(root), held, "Refusal must preserve all durable tracker bytes."); + } await commitItemMutations(pending); + await runImport("fixture/project", root, opts); + const restored = snapshot(root); + await runImport("fixture/project", root, opts); + assert.equal(snapshot(root), restored); }); })); diff --git a/test/scoped-ownership.test.ts b/test/scoped-ownership.test.ts new file mode 100644 index 00000000..6222af78 --- /dev/null +++ b/test/scoped-ownership.test.ts @@ -0,0 +1,137 @@ +/** Scope and native number claims are exercised on genuine SDK tracker records. */ +import assert from "node:assert/strict"; +import test from "node:test"; +import { mkdtempSync, rmSync, readFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join, delimiter } from "node:path"; +import { init, create, get, mutateItem, readSettings, getItemPath, getHistoryPath } from "@unbrained/pm-cli/sdk"; +import { createExtensionTestHarness } from "@unbrained/pm-cli/sdk/testing"; +import extension, { type PmItem, type GhIssue, planSync, buildExportPlan, deriveAtomicItemId, runImport, parseImportOptions, indexByProvenance } from "../index.ts"; +import { buildProjectImportPlan, buildProjectPullPlan, type ProjectItem } from "../projects.ts"; +import { matchImportedIssue } from "../import-fidelity.ts"; +import { buildGithubOwnershipIndex, githubOwnershipClaims, githubImporterIdentity, warnGithubOwnershipScope } from "../import-ownership.ts"; +import { selectRefreshTargets } from "../refresh.ts"; +import { exploreGithubIdentities, planGithubIdentityRepair } from "../identity.ts"; +import { captureStderr, withEnv, withMockGithub, jsonResponse } from "./helpers/mock-github-server.ts"; + +/** Own every tracker setup and cleanup; there are no substituted SDK calls. */ +async function tracker(run: (root: string) => Promise) { + const directory = mkdtempSync(join(tmpdir(), "github-scoped-owners-")), root = join(directory, "tracker"); + try { + await init("fixture", { defaults: true, withPackages: false, agentGuidance: "skip" }, { pmRoot: root, cwd: directory, noExtensions: true }); + await withEnv({ PM_PATH: root, PM_AUTHOR: "fixture", PM_TELEMETRY_SOURCE_CONTEXT: "automation", PATH: join(import.meta.dirname, "../node_modules/.bin") + delimiter + process.env.PATH }, () => run(root)); + } finally { rmSync(directory, { recursive: true, force: true }); } +} +/** Persist arbitrary legacy metadata via the native SDK and read it back before planning. */ +async function item(root: string, fields: Record = {}, id?: string): Promise { + const created = (await create({ title: "Curated fixture", type: "Task", ...(id ? { id } : {}) }, { pmRoot: root })).item; + await mutateItem({ pmRoot: root, id: created.id, settings: await readSettings(root), author: "fixture", op: "update", mutate: document => { + Object.assign(document.metadata, fields); return { changedFields: Object.keys(fields) }; + } }); + return (await get(created.id, {}, { pmRoot: root })).item; +} +/** Observe both the native item and its audit record around a refused operation. */ +function bytes(root: string, value: PmItem) { return [getItemPath(root, String(value.type), value.id!), getHistoryPath(root, value.id!)].map(file => readFileSync(file, "hex")); } +const url = "https://github.com/o/r/issues/7"; +const issue = { number: 7, html_url: url, body: "", title: "Source", state: "open", labels: [], assignee: null, milestone: null, created_at: "2025-01-01T00:00:00Z", updated_at: "2025-02-01T00:00:00Z" } satisfies GhIssue; +const board: ProjectItem[] = [{ id: "board-item", statusName: "Closed", content: { typename: "Issue", repo: "o/r", number: 7, title: "Source" } }]; +const ref = { owner: "o", number: 1 }; + +for (const command of ["sync", "export"] as const) test(`${command} --ids allows a clean owner while warning about both unselected peers`, async () => tracker(async root => { + const clean = await item(root, { github_url: url }), first = await item(root, { github_url: "https://github.com/o/r/issues/8" }), second = await item(root, { external_ref: "gh:o/r#8" }); + const before = [clean, first, second].map(value => bytes(root, value)); + const harness = await createExtensionTestHarness(extension, { capabilities: ["commands", "importers", "schema", "hooks", "preflight", "search"] }); + const result = await captureStderr(() => harness.runCommand({ command: `github ${command}`, options: { repo: "o/r", ids: clean.id, "dry-run": true }, pmRoot: root, global: { json: true } })); + assert.ok(result.stderr.some(line => line.includes("out_of_scope_github_owner_conflict") && line.includes(first.id!) && line.includes(second.id!))); + assert.deepEqual([clean, first, second].map(value => bytes(root, value)), before); +})); + +for (const path of ["sync", "export", "board-import", "board-pull"] as const) test(`${path} still refuses a peer of the selected owner`, async () => tracker(async root => { + const selected = await item(root, { github_url: url }), peer = await item(root, { external_ref: "gh:o/r#7" }), corpus = [selected, peer], before = corpus.map(value => bytes(root, value)); + const run = () => path === "sync" ? planSync([selected], "o/r", undefined, { corpus }) : path === "export" ? buildExportPlan([selected], "o/r", undefined, undefined, { corpus }) : path === "board-import" ? buildProjectImportPlan(board, ref, corpus) : buildProjectPullPlan([selected], ref, board, undefined, undefined, corpus); + assert.throws(run, error => (error as { code: string }).code === "github_import_owner_conflict"); + assert.deepEqual(corpus.map(value => bytes(root, value)), before); +})); + +for (const path of ["board-import", "board-pull", "import-match"] as const) test(`${path} allows clean targets and reports off-board ownership collisions`, async () => tracker(async root => { + const selected = await item(root, { github_url: url }), first = await item(root, { github_url: "https://github.com/o/r/issues/8" }), second = await item(root, { external_ref: "gh:o/r#8" }), corpus = [selected, first, second]; + const outcome = await captureStderr(async () => { + if (path === "board-import") assert.equal(buildProjectImportPlan(board, ref, corpus)[0]?.pmId, selected.id); + else if (path === "board-pull") assert.equal(buildProjectPullPlan(corpus, ref, board).entries[0]?.pmId, selected.id); + else { const index = buildGithubOwnershipIndex(corpus, ["o/r"]); warnGithubOwnershipScope(index, [selected]); assert.equal(matchImportedIssue(corpus, issue, undefined, false, index).item?.id, selected.id); } + }); + assert.ok(outcome.stderr.some(line => line.includes(first.id!) && line.includes(second.id!))); +})); + +for (const declared of ["8", "0", "-1", "9007199254740993"]) test(`native declaration rejects number ${declared} against URL 7 with a warning`, async () => tracker(async root => { + const value = await item(root, { description: `GH issue #${declared}: ${url} · author fixture` }); + assert.deepEqual(githubOwnershipClaims(value).owners, []); assert.equal(matchImportedIssue([value], issue).item, undefined); + const outcome = await captureStderr(async () => warnGithubOwnershipScope(buildGithubOwnershipIndex([value]), [value])); + assert.ok(outcome.stderr.some(line => line.includes("invalid_github_identity_number") && line.includes(value.id!))); +})); + +for (const suffix of ["9007199254740993", "9007199254740991"]) test(`verified importer suffix ${suffix} must be a safe number`, async () => tracker(async root => { + const prefix = (await readSettings(root)).id_prefix; + const id = deriveAtomicItemId("o/r", 7, prefix, value => value).replace(/-7$/, `-${suffix}`); + const value = await item(root, { author: "pm-github", tags: ["gh:o/r#7"] }, id); + const safe = Number.isSafeInteger(Number(suffix)); + assert.equal(githubImporterIdentity(value, ["o/r"]), safe ? `o/r#${suffix}` : undefined); + assert.deepEqual(githubOwnershipClaims(value, false, false, ["o/r"]).owners, safe ? [`o/r#${suffix}`] : []); + const outcome = await captureStderr(async () => warnGithubOwnershipScope(buildGithubOwnershipIndex([value], ["o/r"]), [value])); + assert.equal(outcome.stderr.some(line => line.includes("invalid_github_identity_number")), !safe); +})); + +test("matching marker-free declaration is a repair owner and resolves the import", async () => tracker(async root => { + const owner = await item(root, { description: `GH issue #7: ${url} · author fixture` }), mention = await item(root, { tags: ["gh:o/r#7"] }); + assert.equal(matchImportedIssue([owner, mention], issue).item?.id, owner.id); + const report = await exploreGithubIdentities(root, { identity: "gh:o/r#7" }); + assert.ok(report.edges.some(edge => edge.item === owner.id && edge.source === "declaration" && edge.strength === "claim")); + assert.equal((await planGithubIdentityRepair(root, "gh:o/r#7", owner.id!, mention.id!)).owner, owner.id); +})); + +for (const path of ["refresh", "search", "gated-import"] as const) test(`${path} scopes ownership to requested issue targets`, async () => tracker(async root => { + const selected = await item(root, { github_url: url }), first = await item(root, { github_url: "https://github.com/o/r/issues/8" }), second = await item(root, { external_ref: "gh:o/r#8" }), corpus = [selected, first, second]; + const outcome = await captureStderr(async () => { + if (path === "refresh") assert.equal(selectRefreshTargets(corpus, "o/r", [7])[0]?.id, selected.id); + else if (path === "search") assert.equal(indexByProvenance(corpus, new Set(["o/r#7"])).get("o/r#7")?.id, selected.id); + else await withEnv({ GITHUB_TOKEN: "fixture-token", GH_TOKEN: undefined }, () => withMockGithub((request, response) => { + assert.equal(request.method, "GET"); jsonResponse(response, 200, request.url?.includes("/timeline") ? [] : [{ ...issue, created_at: "2025-01-01T00:00:00Z", updated_at: "2025-02-01T00:00:00Z" }]); + }, () => runImport("o/r", root, parseImportOptions({ atomic: true, gate: true, "dry-run": true })))); + }); + assert.ok(outcome.stderr.some(line => line.includes(first.id!) && line.includes(second.id!))); +})); + +test("board import command ignores and warns about two off-board peers", async () => tracker(async root => { + const selected = await item(root, { github_url: url }), first = await item(root, { github_url: "https://github.com/o/r/issues/8" }), second = await item(root, { external_ref: "gh:o/r#8" }); + const before = [first, second].map(value => bytes(root, value)); + const harness = await createExtensionTestHarness(extension, { capabilities: ["commands", "importers", "schema", "hooks", "preflight", "search"] }); + const outcome = await captureStderr(() => withEnv({ GITHUB_TOKEN: "fixture-token", GH_TOKEN: undefined }, () => withMockGithub((_request, response, body) => { + const query = (JSON.parse(body) as { query: string }).query; + jsonResponse(response, 200, query.includes("projectV2(number:") ? { data: { organization: { projectV2: { id: "board", title: "Fixture", statusField: null } } } } + : { data: { node: { items: { nodes: [{ id: "board-item", content: { __typename: "Issue", id: "issue-node", number: 7, title: "Source", body: "", state: "OPEN", url, repository: { nameWithOwner: "o/r" } } }], pageInfo: { hasNextPage: false } } } } }); + }, () => harness.runCommand({ command: "github project import", args: ["o/1"], options: { "dry-run": true }, pmRoot: root, global: { json: true } })))); + assert.ok(outcome.stderr.some(line => line.includes(first.id!) && line.includes(second.id!))); + assert.deepEqual([first, second].map(value => bytes(root, value)), before); + assert.equal((await get(selected.id!, {}, { pmRoot: root })).item.github_url, url); +})); + + +test("board scope accepts independently decoded selected records", async () => tracker(async root => { + const selected = await item(root, { github_url: url }); + assert.equal(buildProjectPullPlan([JSON.parse(JSON.stringify(selected))], ref, board, undefined, undefined, [selected]).entries[0]?.pmId, selected.id); +})); + +test("unknown board content is outside an import's ownership scope", async () => tracker(async root => { + const first = await item(root, { github_url: "https://github.com/o/r/issues/8", tags: ["gh-project:o/1#redacted"] }), second = await item(root, { external_ref: "gh:o/r#8" }); + const outcome = await captureStderr(async () => assert.deepEqual(buildProjectImportPlan([{ id: "redacted", content: { typename: "Unknown", title: "" } }], ref, [first, second]), [])); + assert.ok(outcome.stderr.some(line => line.includes(first.id!) && line.includes(second.id!))); +})); + + + +for (const number of ["9007199254740993", "9007199254740991"]) test(`external_ref number ${number} must also be a positive safe integer`, async () => tracker(async root => { + const value = await item(root, { external_ref: `gh:o/r#${number}` }), safe = Number.isSafeInteger(Number(number)); + assert.deepEqual(githubOwnershipClaims(value).owners, safe ? [`o/r#${number}`] : []); + const outcome = await captureStderr(async () => warnGithubOwnershipScope(buildGithubOwnershipIndex([value]), [value])); + assert.equal(outcome.stderr.some(line => line.includes("invalid_github_identity_number")), !safe); +})); diff --git a/test/sdk-adoption.test.ts b/test/sdk-adoption.test.ts index 0d958314..47e94fed 100644 --- a/test/sdk-adoption.test.ts +++ b/test/sdk-adoption.test.ts @@ -1,3 +1,4 @@ +import { createImportMirror as create } from "./helpers/import-mirror.ts"; /** Lifecycle finalization uses real SDK guards and hooks in a tracker without git. */ import assert from "node:assert/strict"; import { spawnSync } from "node:child_process"; @@ -6,7 +7,7 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import test from "node:test"; import { - create, getHistoryPath, getItemPath, readHistorySnapshot, + getHistoryPath, getItemPath, readHistorySnapshot, setActiveExtensionHooks, type ExtensionHookRegistry, } from "@unbrained/pm-cli/sdk"; import { buildImportFidelity, persistImportFidelity } from "../import-fidelity.ts"; @@ -39,7 +40,7 @@ function hooks(): ExtensionHookRegistry { } test("lifecycle finalization honors transactional guards before either durable write", async () => tracker(async pmRoot => { - const item = (await create({ title: "Guarded lifecycle", type: "Task" }, { pmRoot })).item; + const item = (await create({ title: "Guarded lifecycle", type: "Task", tags: "gh:unbraind/pm-beads#85", description: `GH issue #85: ${issue.html_url}` }, { pmRoot })).item; const itemPath = getItemPath(pmRoot, item.type, item.id); const historyPath = getHistoryPath(pmRoot, item.id); const before = [readFileSync(itemPath, "utf8"), readFileSync(historyPath, "utf8")]; @@ -57,7 +58,7 @@ test("lifecycle finalization honors transactional guards before either durable w })); test("lifecycle finalization respects strict tracker ownership", async () => tracker(async pmRoot => { - const item = (await create({ title: "Owned lifecycle", type: "Task", assignee: "fixture-owner" }, { pmRoot })).item; + const item = (await create({ title: "Owned lifecycle", type: "Task", assignee: "fixture-owner", tags: "gh:unbraind/pm-beads#85", description: `GH issue #85: ${issue.html_url}` }, { pmRoot })).item; const settings = JSON.parse(readFileSync(join(pmRoot, "settings.json"), "utf8")) as { governance: Record }; settings.governance = { ...settings.governance, preset: "custom", ownership_enforcement: "strict" }; writeFileSync(join(pmRoot, "settings.json"), JSON.stringify(settings)); @@ -69,7 +70,7 @@ test("lifecycle finalization respects strict tracker ownership", async () => tra })); test("SDK finalization preserves both import write hooks and JSON-pointer fields; retry emits none", async () => tracker(async pmRoot => { - const item = (await create({ title: "Observed lifecycle", type: "Task" }, { pmRoot })).item; + const item = (await create({ title: "Observed lifecycle", type: "Task", tags: "gh:unbraind/pm-beads#85", description: `GH issue #85: ${issue.html_url}` }, { pmRoot })).item; const registry = hooks(); const writes: Parameters[0][] = []; registry.onWrite.push({ layer: "project", name: "fixture-observer", run: context => { @@ -94,7 +95,7 @@ test("SDK finalization preserves both import write hooks and JSON-pointer fields })); test("import hook fields match the actual history patch when the clock advances after mutation", async () => tracker(async pmRoot => { - const item = (await create({ title: "Clock-boundary lifecycle", type: "Task" }, { pmRoot })).item; + const item = (await create({ title: "Clock-boundary lifecycle", type: "Task", tags: "gh:unbraind/pm-beads#85", description: `GH issue #85: ${issue.html_url}` }, { pmRoot })).item; const RealDate = Date; let clock = Date.parse(item.updated_at!); /** Freeze Date until a queued source-read observation advances the clock. */ diff --git a/test/smoke.test.ts b/test/smoke.test.ts index bdc6f5a2..33280cca 100644 --- a/test/smoke.test.ts +++ b/test/smoke.test.ts @@ -357,9 +357,9 @@ test("parseProvenanceTag normalizes repository casing", () => { }); }); -test("planSync matches provenance tags case-insensitively", () => { +test("planSync matches explicit provenance case-insensitively", () => { const plan = planSync([ - { id: "pm-1", status: "closed", tags: ["gh:Owner/Repo#123"] }, + { id: "pm-1", status: "closed", github_url: "https://github.com/Owner/Repo/issues/123", tags: ["gh:Owner/Repo#123"] }, ], "owner/repo"); assert.deepEqual(plan, [{ @@ -387,7 +387,7 @@ test("resolveGitHubToken prefers the GITHUB_TOKEN env var", () => { test("buildExportPlan marks linked items update, unlinked items create", () => { const plan = buildExportPlan( [ - { id: "pm-1", title: "Linked", tags: ["bug", "gh:owner/repo#42"], status: "open" }, + { id: "pm-1", title: "Linked", github_url: "https://github.com/owner/repo/issues/42", tags: ["bug", "gh:owner/repo#42"], status: "open" }, { id: "pm-2", title: "New", tags: ["enhancement"], status: "closed" }, ], "owner/repo", @@ -948,6 +948,7 @@ test("buildExportPlan strips both gh: and gh-project: provenance tags, keeps use { id: "pm-1", title: "Dual-linked", + github_url: "https://github.com/owner/repo/issues/42", // Normal user tag + issue provenance + project provenance + a user // label that merely contains similar text (not a real provenance tag). tags: ["bug", "gh:owner/repo#42", projectTag, "gh-project-notes"], @@ -1396,8 +1397,8 @@ test("searchDocumentToItem skips values matching neither shape", () => { }); test("resolveSearchCorpus (the provider's REAL mapping) handles wrapped, raw and junk documents", () => { - const wrapped = { id: "pm-1", title: "Wrapped", tags: ["gh:acme/repo#7"] }; - const raw = { id: "pm-2", title: "Raw", tags: ["gh:acme/repo#8"] }; + const wrapped = { id: "pm-1", title: "Wrapped", github_url: "https://github.com/acme/repo/issues/7", tags: ["gh:acme/repo#7"] }; + const raw = { id: "pm-2", title: "Raw", github_url: "https://github.com/acme/repo/issues/8", tags: ["gh:acme/repo#8"] }; const documents = [{ metadata: wrapped, body: "" }, raw, undefined, null, {}]; // This is the exact function the search provider calls, so reverting the guard diff --git a/test/typed-sentinel-normalization.test.ts b/test/typed-sentinel-normalization.test.ts index e93a51d7..c1276d75 100644 --- a/test/typed-sentinel-normalization.test.ts +++ b/test/typed-sentinel-normalization.test.ts @@ -4,9 +4,9 @@ import { mkdtempSync, readdirSync, readFileSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { delimiter, join } from "node:path"; import test from "node:test"; -import { getHistoryPath, PmClient } from "@unbrained/pm-cli/sdk"; +import { normalizeItemId, getHistoryPath, PmClient } from "@unbrained/pm-cli/sdk"; import { normalizeImportValue } from "../import-fidelity.ts"; -import { parseImportOptions, runImport, type GhIssue } from "../index.ts"; +import { deriveAtomicItemId, parseImportOptions, runImport, type GhIssue } from "../index.ts"; import { captureStderr, withEnv } from "./helpers/mock-github-server.ts"; test("GH-1614 importer normalization preserves literal typed assignee identities", async (t) => { @@ -33,7 +33,8 @@ test("GH-1614 plain create and update preserve literal assignments and repeat wi const root = join(cwd, "tracker"); const client = new PmClient({ cwd, pmRoot: root, author: "fixture", noExtensions: true }); await client.run("init", { prefix: "fixture", withPackages: false, agentGuidance: "skip" }); - const owner = (await client.create({ title: "Existing owner", type: "Issue", tags: "gh:fixture/project#1" })).item; + const owner = (await client.create({ author: "pm-github", id: deriveAtomicItemId("fixture/project", 1, "fixture-", normalizeItemId), title: "Existing owner", type: "Issue", tags: "gh:fixture/project#1", + description: "GH issue #1: https://github.com/fixture/project/issues/1" })).item; const source: GhIssue[] = ["none", "null", "none", "null"].map((login, index) => ({ number: index + 1, title: `Literal source ${index + 1}`, body: "Source body", state: index < 2 ? "open" : "closed", labels: [], state_reason: index < 2 ? null : "completed", closed_at: index < 2 ? null : "2026-10-02T00:00:00Z", @@ -72,7 +73,8 @@ test("GH-1614 existing owner restores literal source assignee after update then const root = join(cwd, "tracker"); const client = new PmClient({ cwd, pmRoot: root, author: "fixture", noExtensions: true }); await client.run("init", { prefix: "fixture", withPackages: false, agentGuidance: "skip" }); - const owner = (await client.create({ title: "Existing open owner", type: "Issue", assignee: "previous", tags: "gh:fixture/project#1" })).item; + const owner = (await client.create({ author: "pm-github", id: deriveAtomicItemId("fixture/project", 1, "fixture-", normalizeItemId), title: "Existing open owner", type: "Issue", assignee: "previous", tags: "gh:fixture/project#1", + description: "GH issue #1: https://github.com/fixture/project/issues/1" })).item; const historyPath = getHistoryPath(root, owner.id); const historyBefore = readFileSync(historyPath, "utf8"); const source: GhIssue = { diff --git a/test/webhooks.test.ts b/test/webhooks.test.ts index 48cb57fb..b64be455 100644 --- a/test/webhooks.test.ts +++ b/test/webhooks.test.ts @@ -1,4 +1,5 @@ /** Real no-git trackers and HTTP transport prove authenticated durable reconciliation. */ +import { createImportMirror } from "./helpers/import-mirror.ts"; import test from "node:test"; import assert from "node:assert/strict"; import { createHash, createHmac } from "node:crypto"; @@ -6,7 +7,7 @@ import type { ServerResponse } from "node:http"; import { mkdtemp, rm, readdir, readFile, writeFile, mkdir, cp, symlink, open, chmod } from "node:fs/promises"; import { join, dirname } from "node:path"; import { tmpdir } from "node:os"; -import { init, create, get, acquireLock } from "@unbrained/pm-cli/sdk"; +import { init, get, create, acquireLock } from "@unbrained/pm-cli/sdk"; import { createExtensionTestHarness } from "@unbrained/pm-cli/sdk/testing"; import extension, { acceptGithubWebhook, reconcileGithubWebhooks, WebhookRefusal, type GithubWebhookDelivery } from "../index.ts"; import { runRefresh } from "../refresh.ts"; @@ -103,8 +104,10 @@ test("signed unsupported, malformed and cross-repository payloads are refused wi })); test("old reopened events cannot regress lifecycle; proposals read current state and polling recovers", async () => fixture(async root => { - const local = (await create({ title: "Local stale title", tags: "gh:fixture/repo#1" }, { cwd: root, pmRoot: root })).item; - await create({ title: "Unrelated linked issue", tags: "gh:fixture/repo#2" }, { cwd: root, pmRoot: root }); + const local = (await createImportMirror({ title: "Local stale title", tags: "gh:fixture/repo#1" }, { cwd: root, pmRoot: root })).item; + await createImportMirror({ title: "Unrelated linked issue", tags: "gh:fixture/repo#2" }, { cwd: root, pmRoot: root }); + const reference = (await create({ title: "Reference only", type: "Task", tags: "gh:fixture/repo#1" }, { cwd: root, pmRoot: root })).item; + const referenceBefore = await snapshot(root); for (const [id, action] of [["new", "closed"], ["old", "reopened"], ["missing-link", "opened"]]) await acceptGithubWebhook(root, delivery(id, id === "missing-link" ? 99 : 1, action)); let state = "CLOSED"; await withMockGithub((request, response, body) => { @@ -119,6 +122,7 @@ test("old reopened events cannot regress lifecycle; proposals read current state assert.equal(receipt.reconciled, 3); assert.equal(receipt.unlinked, 1); assert.equal(receipt.proposals.length, 1); + assert.ok(receipt.proposals.every(proposal => proposal.id !== reference.id)); assert.deepEqual(receipt.proposals[0]!.lifecycle.find(change => change.field === "status"), { field: "status", old: "open", new: "closed" }); const after = await snapshot(root); for (const [file, bytes] of Object.entries(before)) assert.equal(after[file], bytes, file); @@ -129,6 +133,10 @@ test("old reopened events cannot regress lifecycle; proposals read current state assert.equal(empty.proposals.length, 0); await runRefresh("fixture/repo", root, { atomic: true }, { numbers: [1] }); assert.equal((await get(local.id, {}, { pmRoot: root, cwd: root })).item.status, "closed"); + const referenceAfter = await snapshot(root); + const referenceFiles = Object.entries(referenceBefore).filter(([file]) => file.includes(reference.id)); + assert.ok(referenceFiles.length >= 2, "Both item and history must be observed"); + for (const [file, bytes] of referenceFiles) assert.equal(referenceAfter[file], bytes, file); await acceptGithubWebhook(root, delivery("older-again", 1, "reopened")); assert.equal((await reconcileGithubWebhooks(root, "fixture/repo")).proposals.length, 0); state = "OPEN"; @@ -139,7 +147,7 @@ test("old reopened events cannot regress lifecycle; proposals read current state })); test("missing provider state stays pending, limits drain batches, and API failures retry", async () => fixture(async root => { - await create({ title: "Local", tags: "gh:fixture/repo#1" }, { cwd: root, pmRoot: root }); + await createImportMirror({ title: "Local", tags: "gh:fixture/repo#1" }, { cwd: root, pmRoot: root }); await acceptGithubWebhook(root, delivery("first")); await acceptGithubWebhook(root, delivery("second")); await withMockGithub((_request, response) => jsonResponse(response, 200, { data: { repository: { i1: null } } }), async () => { @@ -167,7 +175,7 @@ test("retry batches rotate durably, wrap, and advance even when provider reads t const ids = ["retry-a", "retry-b", "ready-c"].sort((a, b) => createHash("sha256").update(a).digest("hex").localeCompare(createHash("sha256").update(b).digest("hex"))); for (let index = 0; index < ids.length; index++) { - await create({ title: "Linked", tags: `gh:fixture/repo#${index + 1}` }, { cwd: root, pmRoot: root }); + await createImportMirror({ title: "Linked", tags: `gh:fixture/repo#${index + 1}` }, { cwd: root, pmRoot: root }); await acceptGithubWebhook(root, delivery(ids[index]!, index + 1)); } let unavailable = false; @@ -197,7 +205,7 @@ test("retry batches rotate durably, wrap, and advance even when provider reads t })); test("expired live processor locks cannot be stolen during delayed provider reads", async () => fixture(async root => { - await create({ title: "Linked", tags: "gh:fixture/repo#1" }, { cwd: root, pmRoot: root }); + await createImportMirror({ title: "Linked", tags: "gh:fixture/repo#1" }, { cwd: root, pmRoot: root }); await acceptGithubWebhook(root, delivery("before-read")); let signalRead: (response: ServerResponse) => void; const reading = new Promise(resolve => { signalRead = resolve; }); @@ -239,7 +247,7 @@ test("expired intake locks also require explicit recovery", async () => fixture( })); test("slow provider reconciliation never blocks durable authenticated intake", async () => fixture(async root => { - await create({ title: "Linked", tags: "gh:fixture/repo#1" }, { cwd: root, pmRoot: root }); + await createImportMirror({ title: "Linked", tags: "gh:fixture/repo#1" }, { cwd: root, pmRoot: root }); await acceptGithubWebhook(root, delivery("before-read")); let signalRead: (response: ServerResponse) => void; const reading = new Promise(resolve => { signalRead = resolve; }); @@ -265,7 +273,7 @@ test("slow provider reconciliation never blocks durable authenticated intake", a })); test("provider previews do not copy durable inbox storage", async () => fixture(async root => { - await create({ title: "Linked", tags: "gh:fixture/repo#1" }, { cwd: root, pmRoot: root }); + await createImportMirror({ title: "Linked", tags: "gh:fixture/repo#1" }, { cwd: root, pmRoot: root }); await acceptGithubWebhook(root, delivery()); const opaque = join(root, "github-webhooks", "opaque-receipt"); await writeFile(opaque, "Adapter-owned storage must never enter tracker previews"); diff --git a/test/workspace-resolution.test.ts b/test/workspace-resolution.test.ts index ec73520b..4dd1467b 100644 --- a/test/workspace-resolution.test.ts +++ b/test/workspace-resolution.test.ts @@ -57,7 +57,7 @@ test("workspace path and alias property matches SDK writers across fidelity and const terminal = isTerminalStatus(canonical, resolved.statuses); const item: BlockerPmItem = { id: native.id, title: "Fixture", status: spelling, description: "", tags: ["gh:fixture/project#1"], dependencies: [{ id: "github:fixture/project#8", kind: "blocked_by" }], comments: [] }; - assert.equal(planSync([item], "fixture/project", resolved.statuses)[0]!.to, terminal ? "closed" : "open"); + assert.equal(planSync([{ ...item, github_url: "https://github.com/fixture/project/issues/1" }], "fixture/project", resolved.statuses)[0]!.to, terminal ? "closed" : "open"); assert.equal(buildExportPlan([item], "fixture/project", undefined, resolved.statuses)[0]!.payload.state, terminal ? "closed" : "open"); assert.equal(selectBlockerCandidates([item], resolved.statuses).length, terminal ? 0 : 1); const kind = githubStatusKind(spelling, resolved.statuses); diff --git a/webhooks.ts b/webhooks.ts index 46283ed5..64dd6f67 100644 --- a/webhooks.ts +++ b/webhooks.ts @@ -1,8 +1,9 @@ +import { writeGithubStateAtomic, syncGithubStatePath } from "./durable-state.ts"; /** Durable authenticated issue-event hints; provider reads, never event payloads, produce proposals. */ import { createHash, createHmac, timingSafeEqual } from "node:crypto"; -import { open, mkdir, readdir, readFile } from "node:fs/promises"; +import { mkdir, readdir, readFile } from "node:fs/promises"; import { join, basename } from "node:path"; -import { acquireLock, writeFileAtomic, resolvePmRoot, ensureTrackerInitialized } from "@unbrained/pm-cli/sdk"; +import { acquireLock, resolvePmRoot, ensureTrackerInitialized } from "@unbrained/pm-cli/sdk"; import { runRefresh } from "./refresh.ts"; /** Stable refusal discriminators; no secret, payload or storage path is included in diagnostics. */ @@ -80,14 +81,15 @@ function refusal(error: unknown, code: WebhookRefusalCode, remedy: string): neve } /** Flush published records and the containing directory before acknowledging durable intake. */ async function persist(file: string, value: unknown): Promise { - await writeFileAtomic(file, JSON.stringify(value) + "\n"); - await flush(file); + await writeGithubStateAtomic(file, JSON.stringify(value) + "\n"); + await flushGithubWebhookState(file); } /** Finish a prior publisher's flush as well as a fresh receipt's durable acknowledgment. */ -async function flush(file: string): Promise { - for (const target of [file, join(file, ".."), join(file, "../.."), join(file, "../../..")]) { - const handle = await open(target, "r"); - try { await handle.sync(); } finally { await handle.close(); } +export async function flushGithubWebhookState(file: string, platform: () => NodeJS.Platform = () => process.platform): Promise { + const currentPlatform = platform(); + await syncGithubStatePath(file, false, currentPlatform); + for (const target of [join(file, ".."), join(file, "../.."), join(file, "../../..")]) { + await syncGithubStatePath(target, true, currentPlatform); } } /** Serialize independent writers using the host's token-owned, crash-recoverable tracker lock. */ @@ -135,7 +137,7 @@ export async function acceptGithubWebhook(pmRoot: string, input: GithubWebhookDe const existing = await readRecord(file, repo); if (JSON.stringify(existing) !== JSON.stringify(record)) throw new WebhookRefusal("delivery_conflict", "The delivery identifier already names different bytes; retain both source receipts and investigate the adapter."); // A previous process may have died after rename but before its flush. - await flush(file); + await flushGithubWebhookState(file); return { ok: true, repo, delivery: record.delivery, duplicate: true }; } await persist(file, record); @@ -181,9 +183,11 @@ export async function reconcileGithubWebhooks(pmRoot: string, repoArg: string, l if (row.changes.length) proposals.push({ id: row.id, number: row.number, fields: row.changes.map(change => change.field), lifecycle: row.changes.filter(change => ["status", "github_state", "github_updated_at", "closed_at", "completed_at"].includes(change.field)) }); } + const deferred = new Set(preview?.deferred_imports?.flatMap(entry => entry.issue_numbers) ?? []); let reconciled = 0; let unlinked = 0; for (const record of records) { + if (deferred.has(record.number)) continue; const row = preview!.plan.find(entry => entry.number === record.number); if (row?.reason) continue; if (!row) unlinked++;