From adc928bafbe0a3a465d464a4af183f3e3dacda39 Mon Sep 17 00:00:00 2001 From: SteveBot <1153461+unbraind@users.noreply.github.com> Date: Fri, 23 Oct 2026 09:42:59 +0200 Subject: [PATCH 01/18] Reuse existing items that cite an imported GitHub issue --- .../pm-github-cited-issue-adoption.toon | 35 ++++ .../pm-github-cited-issue-adoption.jsonl | 16 ++ CHANGELOG.md | 1 + README.md | 27 +++ docs/cited-issue-adoption-validation.md | 81 ++++++++ import-fidelity.ts | 44 ++++- index.ts | 131 +++++++------ test/atomic.test.ts | 1 + test/cited-issue-adoption.test.ts | 174 ++++++++++++++++++ test/dryrun-preview.test.ts | 1 + test/import-errors.test.ts | 6 +- test/import-fidelity.test.ts | 6 +- test/import-gate.test.ts | 4 +- test/import-selection.test.ts | 11 +- 14 files changed, 468 insertions(+), 70 deletions(-) create mode 100644 .agents/pm/features/pm-github-cited-issue-adoption.toon create mode 100644 .agents/pm/history/pm-github-cited-issue-adoption.jsonl create mode 100644 docs/cited-issue-adoption-validation.md create mode 100644 test/cited-issue-adoption.test.ts diff --git a/.agents/pm/features/pm-github-cited-issue-adoption.toon b/.agents/pm/features/pm-github-cited-issue-adoption.toon new file mode 100644 index 00000000..7520b9ec --- /dev/null +++ b/.agents/pm/features/pm-github-cited-issue-adoption.toon @@ -0,0 +1,35 @@ +id: pm-github-cited-issue-adoption +title: Adopt an existing item that cites the imported GitHub issue +description: "Prevent gated imports from duplicating existing prose-citing work. Add exact citation adoption after established identity matching, refuse ambiguous citations, expose receipts and an opt-out, and validate dry runs against public trackers." +type: Feature +status: in_progress +priority: 2 +tags[3]: changelog-unreleased,github,import +created_at: "2026-10-23T07:16:54.041Z" +updated_at: "2026-10-23T07:42:13.982Z" +author: codex-agent +acceptance_criteria: "Exact issue URL or owner/repo#N in title, description or body adopts one untagged item, including closed and canceled items. Bare numbers and other repositories never match; existing structured identity takes precedence. Multiple candidates return typed ambiguous_citation entries listing IDs and a tagging remedy with no create. Gates verify one landing target; receipts count adopts; dry runs show adopts. Import and scheduled sync adopt by default with --no-adopt-cited for reference-only prose. Behavioral revert tests and no-git tracker tests pass. Validate current pm-review main issue 30 and another public repository. Targeted tests, typecheck, lint and npm/Bun CI release gates pass." +comments[7]{created_at,author,text}: + "2026-10-23T07:17:26.861Z",codex-agent,"Overlap review: pm-github-c093 owns structured URL/provider back-link import matching, pm-github-identity-collisions diagnoses and repairs structured ownership, Projects import matches board and issue provenance, and export upserts linked issues. None adopts arbitrary exact prose citations. This feature adds a fallback only after existing import matching, preserving those paths. Scheduled gated sync already invokes import, so it inherits the default. Plan ambiguous citations before writes and refuse the batch with actionable candidate IDs." + "2026-10-23T07:24:01.857Z",codex-agent,"Implementation: strong ownership matching stays first; exact qualified title/description/body citations fall back to a single existing item. All imports now prepare identities before writes, and ambiguous citations expose candidate IDs plus an exact tagging remedy. Adoption retains local tags and reports a separate count; provider-owned records retain link-only behavior. New tests use real trackers with a PATH containing node and the pinned pm CLI but no git, plus real git trackers for write-gate acceptance." + "2026-10-23T07:27:45.693Z",codex-agent,"New citation suite: 13/13 passed, including plain/atomic adoption in non-git trackers, write-gate acceptance, retry identity, ambiguity refusal, punctuation, repo/number boundaries, structured ownership precedence and provider link-only preservation. Live pm-review main aa16d855df1f7b74afd3453617f77e92c44d493e already tags pm-review-withdrawn-lineage for issue 30, so an untouched current-main copy correctly plans update rather than adopt. Preserve this current-state result and locate the pre-tag historical tracker for a separate adoption replay. SDK reads may create runtime caches; verify original tracked file bytes independently." + "2026-10-23T07:36:10.754Z",codex-agent,"Public dry-run evidence: current pm-review main aa16d855df1f7b74afd3453617f77e92c44d493e issue 30 plans 0 creates, 1 update, 0 adopts because PR 33 already added provenance. Actual pre-tag snapshot f72250e4c5ee8dfafaf57ee8afe20ddd3cbc993e plans 0 creates, 0 updates, 1 adopt into pm-review-withdrawn-lineage; gate new_entries=0. Current pm-context main 8d9576778a3609f156e9444ed659ca7c2e2aa9e7 plans 5 issues: 2 creates, 3 updates, 0 adopts, 0 skips, not truncated. All three are plain-directory archive copies. SHA-256 comparisons verify 106/106, 106/106 and 181/181 original files unchanged. Detailed receipts are in docs/cited-issue-adoption-validation.md." + "2026-10-23T07:38:52.136Z",codex-agent,"Behavioral revert proof: an isolated scratch package retained the new planner, receipts and tests, but restored only matchImportedIssue from base main. node --test test/cited-issue-adoption.test.ts exited 1 with the expected adoption and ambiguity assertion failures. Tests import live TypeScript, so no stale build could hide the revert. The fixed worktree was never reverted and its 14 adoption regressions pass. Full npm/Bun release evidence will come from the PR CI release gates at unchanged thresholds." + "2026-10-23T07:40:47.558Z",codex-agent,"Local checks passed: npm run typecheck; npm run build; npm run build:test; npm run lint; npm run docstring (17 files, 187 documented declarations); npm run duplication (18 authored modules, zero duplicated lines); changelog generation/check and strict tracker health. Behavioral revert: 14 tests, 10 fail and 4 pass with only the old matcher restored; fixed matcher: 14 pass, zero skips. Broader import regressions and the mandatory linked-test run are in progress. The current dependency pin is unchanged; registry CLI version was verified as 2026.10.23." + "2026-10-23T07:42:06.837Z",codex-agent,"Mandatory linked verification completed: pm test pm-github-cited-issue-adoption --run --progress passed its one linked command, node --test test/cited-issue-adoption.test.ts (14 tests pass, zero failures/skips). Public tracker dry runs and behavioral revert evidence are complete. The ready-for-review PR will use CI npm run release:check and bun run release:check, unchanged thresholds, as full release-gate evidence; the final report will link the exact-head run and its counts. No merge or release is authorized." +files[9]{path,scope}: + import-fidelity.ts,project + index.ts,project + test/atomic.test.ts,project + test/cited-issue-adoption.test.ts,project + test/dryrun-preview.test.ts,project + test/import-errors.test.ts,project + test/import-fidelity.test.ts,project + test/import-gate.test.ts,project + test/import-selection.test.ts,project +tests[1]{command,scope,timeout_seconds,provenance{author,created_at,source_kind,source_ref}}: + node --test test/cited-issue-adoption.test.ts,project,900,codex-agent,"2026-10-23T07:35:49.405Z",local_mutation,feat/cited-issue-adoption +docs[2]{path,scope}: + docs/cited-issue-adoption-validation.md,project + README.md,project +body: "" diff --git a/.agents/pm/history/pm-github-cited-issue-adoption.jsonl b/.agents/pm/history/pm-github-cited-issue-adoption.jsonl new file mode 100644 index 00000000..0980dd27 --- /dev/null +++ b/.agents/pm/history/pm-github-cited-issue-adoption.jsonl @@ -0,0 +1,16 @@ +{"hash_algorithm":"sha256","ts":"2026-10-23T07:16:54.041Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","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-cited-issue-adoption"},{"op":"add","path":"/metadata/title","value":"Adopt an existing item that cites the imported GitHub issue"},{"op":"add","path":"/metadata/description","value":"Prevent gated imports from duplicating existing prose-citing work. Add exact citation adoption after established identity matching, refuse ambiguous citations, expose receipts and an opt-out, and validate dry runs against public trackers."},{"op":"add","path":"/metadata/type","value":"Feature"},{"op":"add","path":"/metadata/status","value":"open"},{"op":"add","path":"/metadata/priority","value":2},{"op":"add","path":"/metadata/tags","value":["changelog-unreleased","github","import"]},{"op":"add","path":"/metadata/created_at","value":"2026-10-23T07:16:54.041Z"},{"op":"add","path":"/metadata/updated_at","value":"2026-10-23T07:16:54.041Z"},{"op":"add","path":"/metadata/author","value":"codex-agent"}],"before_hash":"3cc22dff72be7b14824654a7a64ea62b04799939b2fee54c1b5f52ca60bf6df0","after_hash":"0f1cd4f163319fcd62e4ee3df56c86ed36ed7c5cc6c92b5af71ce19e91d97f62","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":"2f9f5836d0a62f4dba7385ce2fc0789a5a08c02aa5c265381c0ee43dadd88cb6"} +{"hash_algorithm":"sha256","ts":"2026-10-23T07:17:18.650Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":{"value":"implementer","source":"argv"},"topic":null},"op":"claim","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T07:17:18.650Z"},{"op":"add","path":"/metadata/assignee","value":"codex-agent"},{"op":"add","path":"/metadata/claim_principal","value":"codex-agent"}],"before_hash":"0f1cd4f163319fcd62e4ee3df56c86ed36ed7c5cc6c92b5af71ce19e91d97f62","after_hash":"825476ab0c4fff0c21c615aea8bac31aa6c407b86d21037eecdf7fd15c2ee3d4","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"e36d32b9176324aa6852e8ea1cbd0dd3397df09e7a7b1e27a6180e53108fb747"} +{"hash_algorithm":"sha256","ts":"2026-10-23T07:17:23.013Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":{"value":"implementer","source":"argv"},"topic":null},"op":"update","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T07:17:23.013Z"},{"op":"replace","path":"/metadata/status","value":"in_progress"}],"before_hash":"825476ab0c4fff0c21c615aea8bac31aa6c407b86d21037eecdf7fd15c2ee3d4","after_hash":"be8d77a6ccd0b721c362770cd4f55940f958148e0b6c45e1c60bc052cde5b2dd","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"39b8c14d85867af2262d732dce881a916900e8e6e5e0329fac5f9d2f4957c281"} +{"hash_algorithm":"sha256","ts":"2026-10-23T07:17:26.861Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T07:17:26.861Z"},{"op":"add","path":"/metadata/comments","value":[{"created_at":"2026-10-23T07:17:26.861Z","author":"codex-agent","text":"Overlap review: pm-github-c093 owns structured URL/provider back-link import matching, pm-github-identity-collisions diagnoses and repairs structured ownership, Projects import matches board and issue provenance, and export upserts linked issues. None adopts arbitrary exact prose citations. This feature adds a fallback only after existing import matching, preserving those paths. Scheduled gated sync already invokes import, so it inherits the default. Plan ambiguous citations before writes and refuse the batch with actionable candidate IDs."}]}],"before_hash":"be8d77a6ccd0b721c362770cd4f55940f958148e0b6c45e1c60bc052cde5b2dd","after_hash":"b0d866b3522795ab4331956db1fb6e4bf2fcdc766c0651180e9b53749117954c","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"47cc2ba37813b050978316bb03db14d5d5fa9b405107f9855f72173b507d568a"} +{"hash_algorithm":"sha256","ts":"2026-10-23T07:20:13.964Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":{"value":"implementer","source":"argv"},"topic":null},"op":"update","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T07:20:13.964Z"},{"op":"add","path":"/metadata/acceptance_criteria","value":"Exact issue URL or owner/repo#N in title, description or body adopts one untagged item, including closed and canceled items. Bare numbers and other repositories never match; existing structured identity takes precedence. Multiple candidates return typed ambiguous_citation entries listing IDs and a tagging remedy with no create. Gates verify one landing target; receipts count adopts; dry runs show adopts. Import and scheduled sync adopt by default with --no-adopt-cited for reference-only prose. Behavioral revert tests and no-git tracker tests pass. Validate current pm-review main issue 30 and another public repository. Targeted tests, typecheck, lint and npm/Bun CI release gates pass."}],"before_hash":"b0d866b3522795ab4331956db1fb6e4bf2fcdc766c0651180e9b53749117954c","after_hash":"5499ae437e72e3a3e66655bd35b2ac74cd3831fbc4b93667b34bf587197bea9a","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"d7b11466afcc5453440d7f65b04be2fe2ba5e5d84cd993f433bfbe7cbb4295dc"} +{"hash_algorithm":"sha256","ts":"2026-10-23T07:24:01.880Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/1","value":{"created_at":"2026-10-23T07:24:01.857Z","author":"codex-agent","text":"Implementation: strong ownership matching stays first; exact qualified title/description/body citations fall back to a single existing item. All imports now prepare identities before writes, and ambiguous citations expose candidate IDs plus an exact tagging remedy. Adoption retains local tags and reports a separate count; provider-owned records retain link-only behavior. New tests use real trackers with a PATH containing node and the pinned pm CLI but no git, plus real git trackers for write-gate acceptance."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T07:24:01.880Z"}],"before_hash":"5499ae437e72e3a3e66655bd35b2ac74cd3831fbc4b93667b34bf587197bea9a","after_hash":"171f100af641bc608cdb27a31eb719fea70a8ff9241a208eb580501b2a911663","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"dbde545fc0c8e2ac3a1640948a861394517a4f0771b8d6b3238a529295c9141c"} +{"hash_algorithm":"sha256","ts":"2026-10-23T07:27:45.693Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/2","value":{"created_at":"2026-10-23T07:27:45.693Z","author":"codex-agent","text":"New citation suite: 13/13 passed, including plain/atomic adoption in non-git trackers, write-gate acceptance, retry identity, ambiguity refusal, punctuation, repo/number boundaries, structured ownership precedence and provider link-only preservation. Live pm-review main aa16d855df1f7b74afd3453617f77e92c44d493e already tags pm-review-withdrawn-lineage for issue 30, so an untouched current-main copy correctly plans update rather than adopt. Preserve this current-state result and locate the pre-tag historical tracker for a separate adoption replay. SDK reads may create runtime caches; verify original tracked file bytes independently."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T07:27:45.693Z"}],"before_hash":"171f100af641bc608cdb27a31eb719fea70a8ff9241a208eb580501b2a911663","after_hash":"dcba36d3882bb6da13e84f4a7b300775580c474657b7482e69b87220a09af82e","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"b54ff9f306dec6c05562a2afd8fc3bf608739f934a98b5818137cb8d66e2e55b"} +{"hash_algorithm":"sha256","ts":"2026-10-23T07:33:53.780Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"files_add","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T07:33:53.780Z"},{"op":"add","path":"/metadata/files","value":[{"path":"import-fidelity.ts","scope":"project"},{"path":"index.ts","scope":"project"},{"path":"test/cited-issue-adoption.test.ts","scope":"project"},{"path":"test/import-selection.test.ts","scope":"project"}]}],"before_hash":"dcba36d3882bb6da13e84f4a7b300775580c474657b7482e69b87220a09af82e","after_hash":"caf0045c508c55e0357497953b6f7f20cd8107d8c496fc2491e15c8be2eef91f","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"8ffc510e028177f35381f3a3f3df86229a005318200f167a53432b3218fcc1ea"} +{"hash_algorithm":"sha256","ts":"2026-10-23T07:34:02.455Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"docs_add","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T07:34:02.455Z"},{"op":"add","path":"/metadata/docs","value":[{"path":"docs/cited-issue-adoption-validation.md","scope":"project"},{"path":"README.md","scope":"project"}]}],"before_hash":"caf0045c508c55e0357497953b6f7f20cd8107d8c496fc2491e15c8be2eef91f","after_hash":"88dfeaa377efb71d6b866e0d35319e77f73b79444baec0622eab8d772a44cb5c","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"2ec01204c1d1c9889eb5d44e53e00e00f6ff52777dc35f7e28fee7a55dee13ad"} +{"hash_algorithm":"sha256","ts":"2026-10-23T07:35:50.503Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"tests_add","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T07:35:50.503Z"},{"op":"add","path":"/metadata/tests","value":[{"command":"node --test test/cited-issue-adoption.test.ts","scope":"project","timeout_seconds":900,"provenance":{"author":"codex-agent","created_at":"2026-10-23T07:35:49.405Z","source_kind":"local_mutation","source_ref":"feat/cited-issue-adoption"}}]}],"before_hash":"88dfeaa377efb71d6b866e0d35319e77f73b79444baec0622eab8d772a44cb5c","after_hash":"8ea366a34c6408eb4e50f07100997d0c95609ed0177f632fc1be6c810839d529","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"014f2053f571c768d887efbedbf2b84efbfa87ca8721e0e9f6dc342b052b4caf"} +{"hash_algorithm":"sha256","ts":"2026-10-23T07:36:10.754Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/3","value":{"created_at":"2026-10-23T07:36:10.754Z","author":"codex-agent","text":"Public dry-run evidence: current pm-review main aa16d855df1f7b74afd3453617f77e92c44d493e issue 30 plans 0 creates, 1 update, 0 adopts because PR 33 already added provenance. Actual pre-tag snapshot f72250e4c5ee8dfafaf57ee8afe20ddd3cbc993e plans 0 creates, 0 updates, 1 adopt into pm-review-withdrawn-lineage; gate new_entries=0. Current pm-context main 8d9576778a3609f156e9444ed659ca7c2e2aa9e7 plans 5 issues: 2 creates, 3 updates, 0 adopts, 0 skips, not truncated. All three are plain-directory archive copies. SHA-256 comparisons verify 106/106, 106/106 and 181/181 original files unchanged. Detailed receipts are in docs/cited-issue-adoption-validation.md."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T07:36:10.754Z"}],"before_hash":"8ea366a34c6408eb4e50f07100997d0c95609ed0177f632fc1be6c810839d529","after_hash":"c00fab8b5b502d06e5b92b9d1a247bb2c120260fe86cb391b3560edc4fdd6eba","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"cb86cebc6c5060bb7a5b9abcab306ff4cc7b57904683bee2a9bdb4c73989f01c"} +{"hash_algorithm":"sha256","ts":"2026-10-23T07:38:52.137Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/4","value":{"created_at":"2026-10-23T07:38:52.136Z","author":"codex-agent","text":"Behavioral revert proof: an isolated scratch package retained the new planner, receipts and tests, but restored only matchImportedIssue from base main. node --test test/cited-issue-adoption.test.ts exited 1 with the expected adoption and ambiguity assertion failures. Tests import live TypeScript, so no stale build could hide the revert. The fixed worktree was never reverted and its 14 adoption regressions pass. Full npm/Bun release evidence will come from the PR CI release gates at unchanged thresholds."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T07:38:52.137Z"}],"before_hash":"c00fab8b5b502d06e5b92b9d1a247bb2c120260fe86cb391b3560edc4fdd6eba","after_hash":"e4a8b368dfb0f39200a6675f1895df9502a61787f58379bf051d363fa6c26a3c","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"71bc885da9f7a3cd4865fb92443cf3d4864aeb5abb851095c4e60cec7c83848b"} +{"hash_algorithm":"sha256","ts":"2026-10-23T07:40:40.059Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","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/3/path","value":"test/cited-issue-adoption.test.ts"},{"op":"replace","path":"/metadata/files/2/path","value":"test/atomic.test.ts"},{"op":"add","path":"/metadata/files/4","value":{"path":"test/dryrun-preview.test.ts","scope":"project"}},{"op":"add","path":"/metadata/files/5","value":{"path":"test/import-errors.test.ts","scope":"project"}},{"op":"add","path":"/metadata/files/6","value":{"path":"test/import-fidelity.test.ts","scope":"project"}},{"op":"add","path":"/metadata/files/7","value":{"path":"test/import-gate.test.ts","scope":"project"}},{"op":"add","path":"/metadata/files/8","value":{"path":"test/import-selection.test.ts","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T07:40:40.059Z"}],"before_hash":"e4a8b368dfb0f39200a6675f1895df9502a61787f58379bf051d363fa6c26a3c","after_hash":"cd707eb466020e9e2fd73e60e087610788a7de030b31bb2d2521d0148ee69536","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"610b2aa81dce434408a10db777587ebc096dc9f9d55ac3c5fa71616b43f8ce7c"} +{"hash_algorithm":"sha256","ts":"2026-10-23T07:40:47.559Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/5","value":{"created_at":"2026-10-23T07:40:47.558Z","author":"codex-agent","text":"Local checks passed: npm run typecheck; npm run build; npm run build:test; npm run lint; npm run docstring (17 files, 187 documented declarations); npm run duplication (18 authored modules, zero duplicated lines); changelog generation/check and strict tracker health. Behavioral revert: 14 tests, 10 fail and 4 pass with only the old matcher restored; fixed matcher: 14 pass, zero skips. Broader import regressions and the mandatory linked-test run are in progress. The current dependency pin is unchanged; registry CLI version was verified as 2026.10.23."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T07:40:47.559Z"}],"before_hash":"cd707eb466020e9e2fd73e60e087610788a7de030b31bb2d2521d0148ee69536","after_hash":"18ebc3c22a0879a77edf6ae70df7bc016994f8ff7b049d12ff6854d2e91ee106","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"a2eb6e79616e5011dbe85f307f684e7b46d03187294d105b8fae05b1745b2340"} +{"hash_algorithm":"sha256","ts":"2026-10-23T07:42:06.838Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/6","value":{"created_at":"2026-10-23T07:42:06.837Z","author":"codex-agent","text":"Mandatory linked verification completed: pm test pm-github-cited-issue-adoption --run --progress passed its one linked command, node --test test/cited-issue-adoption.test.ts (14 tests pass, zero failures/skips). Public tracker dry runs and behavioral revert evidence are complete. The ready-for-review PR will use CI npm run release:check and bun run release:check, unchanged thresholds, as full release-gate evidence; the final report will link the exact-head run and its counts. No merge or release is authorized."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T07:42:06.838Z"}],"before_hash":"18ebc3c22a0879a77edf6ae70df7bc016994f8ff7b049d12ff6854d2e91ee106","after_hash":"fc43fc3166a06185f926b5273e40c995918dfb4322223adb12c297846f9b5564","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"b714aa2aa79b8e9a82efad3a873b552ffc02027b4e29aed8887ba1adf5eccc7d"} +{"hash_algorithm":"sha256","ts":"2026-10-23T07:42:13.982Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":{"value":"implementer","source":"argv"},"topic":null},"op":"release","patch":[{"op":"remove","path":"/metadata/claim_principal"},{"op":"remove","path":"/metadata/assignee"},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T07:42:13.982Z"}],"before_hash":"fc43fc3166a06185f926b5273e40c995918dfb4322223adb12c297846f9b5564","after_hash":"70f669ce6defd83fc68714d8258cab990d2ac62dfa4437c1666a0f48237d3233","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"81602d17837c1b21003c92cd6f13e37c8302b578ba99758d16ffe797d3882239"} diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f7ffbe0..56dd97db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Added +- Adopt an existing item that cites the imported GitHub issue ([pm-github-cited-issue-adoption](https://github.com/unbraind/pm-github/blob/main/.agents/pm/features/pm-github-cited-issue-adoption.toon)) - Explore provider identity collisions and approve scoped provenance repairs ([pm-github-identity-collisions](https://github.com/unbraind/pm-github/blob/main/.agents/pm/features/pm-github-identity-collisions.toon)) - Reconcile authenticated GitHub webhooks through a durable deduplicating inbox ([pm-github-webhook-inbox](https://github.com/unbraind/pm-github/blob/main/.agents/pm/features/pm-github-webhook-inbox.toon)) - Let reusable workflows carry their own release version ([pm-github-znig](https://github.com/unbraind/pm-github/blob/main/.agents/pm/features/pm-github-znig.toon)) diff --git a/README.md b/README.md index 0659e698..5e9eb959 100644 --- a/README.md +++ b/README.md @@ -361,6 +361,7 @@ 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. | +| `--no-adopt-cited` | boolean | Disable default citation adoption when prose references describe related work rather than the item's own issue. Existing provenance/URL matching still applies. | | `--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. | | `--dry-run` | boolean | Preview without writing | @@ -368,6 +369,32 @@ pm github import owner/repo --dry-run 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. +Before creating an issue item, import looks for an existing item citing exactly +`https://github.com/owner/repo/issues/N` or `owner/repo#N` in its title, +description or body. Trailing punctuation such as `.` and `)` is accepted; +bare `#N`, another repository and longer issue numbers never match. Existing +tag/URL/provider ownership matching takes precedence. Items with an established +`gh:` issue tag are excluded from this fallback. Closed and canceled items remain +eligible because their fix may already have landed. + +One candidate produces an `adopt` plan entry: the existing ID receives the +provenance tag and `github_*` fields through the normal upsert, retaining local +tags and any established provider ownership. Later imports use the structured +identity. Two or more candidates produce `ambiguous_citation`, candidate IDs and +a remedy: tag the correct item with `gh:owner/repo#N`, then retry. The entire +batch refuses writes until resolved; dry-run returns `blocked: true` and the +refusal entries with zero planned write counts. + +Dry runs show `adopt` entries and `wouldAdopt`; write receipts report `adopted` +separately from `imported` and `updated`. Gated idempotency counts an adopt as one +existing landing item with its provenance tag, never a new entry. This is on by +default for manual imports and scheduled gated syncs, whose import step uses the +same planner. Use `--no-adopt-cited` when citations are reference-only and a +separate imported item is intentional. The legacy import alias shares this flag. +Status push (`github sync`), export and Projects matching retain their existing +scope. Citation adoption also works in a non-git tracker; actual `--gate` writes +still require a Git worktree for the proposed-change privacy scan. + `--issue` deduplicates references and fetches each selected issue directly, including closed issues, without listing the repository. Every selected number is fetched and checked before any tracker write. A missing number fails the whole import; `--dry-run` instead reports `missingIssues` and previews the available selection. Pull request numbers require `--include-prs`. Selection combines with comment modes, dependency linking, atomic imports, gates, dry runs, and custom item types, preserving provenance-based re-import idempotency. `--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. diff --git a/docs/cited-issue-adoption-validation.md b/docs/cited-issue-adoption-validation.md new file mode 100644 index 00000000..f01e00da --- /dev/null +++ b/docs/cited-issue-adoption-validation.md @@ -0,0 +1,81 @@ +# Cited issue adoption validation + +The importer first resolves established provenance tags, structured URLs and +provider back-links. Only an issue without an established owner reaches exact +title/description/body citation matching. One candidate is adopted; multiple +candidates refuse the batch before any writes and report `ambiguous_citation` +with candidate IDs and a tagging remedy. Status push, export and Projects +matching retain their existing scope. + +## Public tracker dry runs + +Validation used GitHub API tracker archives copied into plain directories with +no Git repository. Original archive files were made read-only and every original +file's SHA-256 was checked after the dry run. SDK runtime caches are separate +from those original tracked files. No source repository or tracker was changed. +Live issue and closure-timeline data came from GitHub; no issue payload was +rewritten for the replay. + +| Repository and tracker revision | Issues fetched/planned | Create | Update | Adopt | Skip | Original files unchanged | +| --- | ---: | ---: | ---: | ---: | ---: | ---: | +| pm-review current main `aa16d855df1f7b74afd3453617f77e92c44d493e` | 1 / 1 | 0 | 1 | 0 | 0 | 106 / 106 | +| pm-review pre-tag history `f72250e4c5ee8dfafaf57ee8afe20ddd3cbc993e` | 1 / 1 | 0 | 0 | 1 | 0 | 106 / 106 | +| pm-context current main `8d9576778a3609f156e9444ed659ca7c2e2aa9e7` | 5 / 5 | 2 | 3 | 0 | 0 | 181 / 181 | + +The requested pm-review current-main check now correctly plans an **update**: +[PR 33](https://github.com/unbraind/pm-review/pull/33) already added the issue's +provenance tag after the duplicate-import incident. Claiming an adopt on that +untouched snapshot would contradict structured-identity precedence. Replaying +the actual [pre-tag tracker](https://github.com/unbraind/pm-review/tree/f72250e4c5ee8dfafaf57ee8afe20ddd3cbc993e/.agents/pm) +produces this adoption entry: + +```json +{ + "issue": 30, + "item": "pm-review-withdrawn-lineage", + "action": "adopt", + "status": "closed", + "github_url": "https://github.com/unbraind/pm-review/issues/30", + "github_number": 30, + "identity": { + "via": "citation", + "conflict": { + "local": "in_progress", + "github": "closed", + "rule": "github-source-wins" + } + } +} +``` + +Its gated idempotency receipt reports `adopted: 1`, `matched_by_provenance: 0` +and `new_entries: 0`. The current-main receipt reports `adopted: 0`, +`matched_by_provenance: 1` and `new_entries: 0`. + +Both pm-review runs used `github import unbraind/pm-review --issue 30 --atomic +--gate --dry-run`. The second public repository used `github import +unbraind/pm-context --state all --limit 5 --atomic --gate --dry-run`; it fetched +issues 154, 153, 136, 53 and 81, reported `truncated: false`, and preserved the +`not_planned` issue's canceled status. These were planner dry runs, not +post-write privacy-scan receipts. + +## Regression evidence + +`test/cited-issue-adoption.test.ts` uses real initialized trackers and real CLI +or SDK writes. Plain and atomic non-git cases run with a PATH containing only +Node and the pinned pm CLI. Gate write cases use disposable Git repositories. +Assertions cover single-candidate adoption, terminal candidates, no duplicate +on retry, ambiguity before any batch write, URL punctuation, repository and +issue-number boundaries, structured ownership precedence, provider link-only +behavior, separate adoption counts, opt-out, and conflicting batch targets. + +`test/import-selection.test.ts` installs the built package into a disposable +consumer and exercises the actual CLI dispatch, including the advertised +`--no-adopt-cited` flag and its create-versus-adopt behavior. + +Behavioral revert proof copied the current source and test into an isolated +scratch package, then restored **only** `matchImportedIssue` from base main. +`node --test test/cited-issue-adoption.test.ts` exited 1 with 10 failures and +4 passes, versus 14 passes with the fix; neither run skipped tests. The fixed +worktree was untouched. Tests import live TypeScript, so no compiled artifact +could mask the revert. diff --git a/import-fidelity.ts b/import-fidelity.ts index 5723ba00..9b9b636d 100644 --- a/import-fidelity.ts +++ b/import-fidelity.ts @@ -4,7 +4,7 @@ import { acquireLock, appendHistoryEntry, canonicalDocument, createHistoryEntry, getHistoryPath, locateItem, normalizeItemId, readLocatedItem, readSettings, resolveItemTypeRegistry, resolvePmRoot, runActiveOnWriteHooks, serializeItemDocument, writeFileAtomic, } from "@unbrained/pm-cli/sdk"; -import { CommandError, fetchJSON, githubApiBase, githubGraphQL, parseNextLink, type GhIssue, type PmItem } from "./index.ts"; +import { CommandError, fetchJSON, githubApiBase, githubGraphQL, parseNextLink, parseProvenanceTag, type GhIssue, type PmItem } from "./index.ts"; /** * Select the latest external edit or different lifecycle generation in immutable history. @@ -44,6 +44,33 @@ export interface ImportFidelity { /** Identity decision recorded before any import writes. */ export interface ImportMatch { item?: PmItem; linkOnly: boolean; via: string } +/** A citation cannot pick an owner until the user tags exactly one candidate. */ +export interface AmbiguousCitationPlan { + action: "ambiguous_citation"; + issue: number; + candidates: string[]; + remedy: string; +} + +/** Carry a typed refusal from identity resolution into the batch's reviewable plan. */ +export class AmbiguousCitationError extends Error { + readonly entry: AmbiguousCitationPlan; + /** Retain candidate IDs and the exact provenance tag needed to resolve the conflict. */ + constructor(repo: string, number: number, candidates: string[]) { + const entry: AmbiguousCitationPlan = { action: "ambiguous_citation", issue: number, candidates, + remedy: `Tag the correct item with gh:${repo}#${number}, then retry.` }; + super(JSON.stringify(entry)); + this.entry = entry; + } +} + +/** Match complete, repository-qualified prose references, never bare numbers or URL substrings. */ +function citesIssue(item: PmItem, repo: string, number: number): boolean { + const text = [item.title, item.description, item.body].join("\n"); + const references = text.matchAll(/(? (match[1] ?? match[2]!).toLowerCase() === repo && Number(match[3]) === number); +} + /** Trim exporter punctuation in one backwards pass. @internal Shared with parity tests. */ export function trimLocatorPunctuation(locator: string): string { let end = locator.length; @@ -80,7 +107,7 @@ function providerProvenance(item: PmItem): string { } /** Match GitHub locators or provider back-links before creation, refusing multiple owners. */ -export function matchImportedIssue(items: readonly PmItem[], issue: GhIssue, tagged?: PmItem): ImportMatch { +export function matchImportedIssue(items: readonly PmItem[], issue: GhIssue, tagged?: PmItem, adoptCited = true): ImportMatch { const issueUrl = locators(issue.html_url)[0]!; const bodyUrls = new Set(locators(issue.body ?? "")); /** Recognize leading native ownership declarations; later prose cannot transfer ownership. */ @@ -91,11 +118,20 @@ export function matchImportedIssue(items: readonly PmItem[], issue: GhIssue, tag Object.entries(item).some(([key, value]) => /^(?:github_?url|provenance|external_?(?:links?|url)|links?)$/i.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 item = candidates[0]; + let item = candidates[0]; + let cited = false; + if (!item && adoptCited && !issue.pull_request) { + const repo = issue.html_url.split("/").slice(3, 5).join("/").toLowerCase(); + const citing = items.filter(candidate => candidate.id && + !candidate.tags?.some(tag => parseProvenanceTag(tag) !== undefined) && citesIssue(candidate, repo, issue.number)); + if (citing.length > 1) throw new AmbiguousCitationError(repo, issue.number, citing.map(candidate => candidate.id!).sort()); + item = citing[0]; + cited = Boolean(item); + } const provider = item && (Boolean(providerProvenance(item)) || (copiedProviderMarkersOwnItem(item) && item.tags?.some(tag => /^(linear|jira|ado):/i.test(tag)))); const native = item && declaredOwner(item); return { item, linkOnly: Boolean(item && (provider || native || item.github_link_only === true)), - via: 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.id === tagged?.id ? "provenance-tag" : "github-url") : "new" }; } /** Read calendar release tags from the selected tracker checkout, including annotated tag dates. */ diff --git a/index.ts b/index.ts index 16b02ffb..ac46dec9 100644 --- a/index.ts +++ b/index.ts @@ -25,8 +25,8 @@ import type { } from "@unbrained/pm-cli/sdk/authoring"; import { CloseoutUsageError, compactCloseoutReport, runCloseout } from "./closeout.ts"; import { - buildImportFidelity, fetchImportTimeline, importFidelityFields, matchImportedIssue, persistImportFidelity, readImportReleases, selectImportContextIndex, validateImportLifetime, - type ImportFidelity, + AmbiguousCitationError, buildImportFidelity, fetchImportTimeline, importFidelityFields, matchImportedIssue, persistImportFidelity, readImportReleases, selectImportContextIndex, validateImportLifetime, + type AmbiguousCitationPlan, type ImportFidelity, } from "./import-fidelity.ts"; import { refreshChanges, runRefresh } from "./refresh.ts"; import { exploreGithubIdentities, planGithubIdentityRepair, applyGithubIdentityRepair } from "./identity.ts"; @@ -175,6 +175,8 @@ const COMMENTS_MODES: readonly CommentsMode[] = ["body", "annotations", "both"]; * of the import path consumes a typed shape rather than re-parsing strings. */ export interface ImportOptions { + /** Disable fallback adoption when citations are references rather than ownership evidence. */ + noAdoptCited?: boolean; /** Explicit comma-separated issue numbers or same-repository references. */ issue?: string; /** Positive cap on list-mode issues, ordered by latest update. */ @@ -2732,6 +2734,7 @@ export function parseImportOptions(options: Record): ImportOpti atomic: optionEnabled(options, "atomic"), linkDeps: optionEnabled(options, "link-deps", "linkDeps"), gate: optionEnabled(options, "gate"), + noAdoptCited: optionEnabled(options, "no-adopt-cited", "noAdoptCited"), }; } @@ -3113,7 +3116,7 @@ export async function prepareGithubImport( const title = issue.title.trim(); if (!title) return undefined; - const identity = matchImportedIssue(fidelityContext.items, issue, match); + const identity = matchImportedIssue(fidelityContext.items, issue, match, !opts.noAdoptCited); match = identity.item; const timeline = await fidelityContext.timeline(issue, repo, token); const fidelity = buildImportFidelity(issue, fidelityContext.releases, timeline); @@ -3151,7 +3154,7 @@ 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: [...new Set([...(identity.linkOnly ? match!.tags! : []), ...labels, tag, ...(ghAuthorTag ? [ghAuthorTag] : [])])], + tags: [...new Set([...(identity.linkOnly || identity.via === "citation" ? match!.tags ?? [] : []), ...labels, tag, ...(ghAuthorTag ? [ghAuthorTag] : [])])], assignee: issue.assignee?.login, milestone: issue.milestone?.title, comments, @@ -3176,6 +3179,8 @@ export interface ImportPlanCompletenessReceipts { /** Receipts from the gated import's idempotency check. */ 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. */ readonly provenance_indexed: number; /** Number of plan entries matched to an existing item via its provenance tag. */ @@ -3272,6 +3277,7 @@ export function verifyImportIdempotency( for (const entry of prepared) { if (entry.match?.id) { matchedIds.push(entry.match.id); + if (entry.identity?.via === "citation" && !entry.tags.includes(provenanceTag(repo, entry.issueNumber))) untaggedNewEntries++; continue; } if (!entry.tags.includes(provenanceTag(repo, entry.issueNumber))) untaggedNewEntries++; @@ -3286,8 +3292,9 @@ export function verifyImportIdempotency( ); } return { + adopted: prepared.filter(entry => entry.identity?.via === "citation").length, provenance_indexed: tagOwners.size, - matched_by_provenance: matchedIds.length, + matched_by_provenance: prepared.filter(entry => entry.match?.id && entry.identity?.via !== "citation").length, new_entries: prepared.length - matchedIds.length, }; } @@ -3442,12 +3449,13 @@ export async function runImport( dryRun: true, wouldImport: 0, wouldUpdate: 0, + wouldAdopt: 0, wouldSkip: 0, ...(opts.atomic ? { atomic: true } : {}), ...fetchFields, }; } - return { imported: 0, updated: 0, skipped: 0, ...fetchFields, ...(emptyGateReceipt ? { gate: emptyGateReceipt } : {}) }; + return { imported: 0, updated: 0, adopted: 0, skipped: 0, ...fetchFields, ...(emptyGateReceipt ? { gate: emptyGateReceipt } : {}) }; } console.error(`Found ${filtered.length} issue(s).`); @@ -3470,14 +3478,15 @@ export async function runImport( let imported = 0; let updated = 0; + let adopted = 0; let skipped = 0; // Planned native comments the write phase could not land (contended lock, // refused add, unparsable created id). Ungated runs warn and continue; gated // runs add this to the plan-divergence refusal at the end. let commentWriteFailures = 0; - // Gated pipeline, phase 1 — plan BEFORE any mutation. The same prepare step - // the write paths use builds every plan entry up front, so completeness + // Plan every import BEFORE any mutation, including ungated citation refusals. + // The same prepare step builds every entry up front, so gated completeness // (every fetched issue accounted for exactly once) and idempotency (every // re-import lands on exactly one provenance-tagged item) are proven while // the tracker is still untouched. Both checks throw before any write. @@ -3485,32 +3494,45 @@ export async function runImport( let gateCompleteness: ImportPlanCompletenessReceipts | undefined; let gateIdempotency: ImportIdempotencyReceipts | undefined; let gatePlanByNumber: Map | undefined; - if (opts.gate || opts.atomic) { + { const planned: PreparedGithubImport[] = []; const skippedNumbers: number[] = []; + const ambiguous: AmbiguousCitationPlan[] = []; for (const issue of filtered) { - const entry = await prepareGithubImport( - issue, - repo, - opts, - token, - existing.get(`${repo.toLowerCase()}#${issue.number}`), - dependencies.fetchIssueComments, - fidelityContext, - ); + let entry: PreparedGithubImport | undefined; + try { + entry = await prepareGithubImport( + issue, + repo, + opts, + token, + existing.get(`${repo.toLowerCase()}#${issue.number}`), + dependencies.fetchIssueComments, + fidelityContext, + ); + } catch (error) { + if (!(error instanceof AmbiguousCitationError)) throw error; + ambiguous.push(error.entry); + continue; + } if (!entry) { skippedNumbers.push(issue.number); continue; } planned.push(entry); } + if (ambiguous.length > 0) { + const refusal = { code: "ambiguous_citation", blocked: true, plan: ambiguous }; + if (!opts.dryRun) throw new CommandError(JSON.stringify(refusal)); + for (const entry of ambiguous) console.error(`[dry-run] #${entry.issue} ambiguous_citation: ${entry.candidates.join(", ")}. ${entry.remedy}`); + return { dryRun: true, wouldImport: 0, wouldUpdate: 0, wouldAdopt: 0, wouldSkip: skippedNumbers.length, + ...(opts.atomic ? { atomic: true } : {}), ...refusal, ...fetchFields }; + } if (opts.gate) { - gateCompleteness = verifyImportPlanCompleteness(filtered, planned, skippedNumbers); - gateIdempotency = verifyImportIdempotency( - existingItems, - planned, - repo, - ); + gateCompleteness = verifyImportPlanCompleteness(filtered, planned, skippedNumbers); + } + if (opts.gate || planned.some(entry => entry.identity?.via === "citation")) { + gateIdempotency = verifyImportIdempotency(existingItems, planned, repo); } gatePlan = planned; gatePlanByNumber = new Map(planned.map((entry) => [entry.issueNumber, entry])); @@ -3528,7 +3550,7 @@ export async function runImport( } if (opts.gate) console.error( `pm github gate: plan verified — ${gateCompleteness!.planned} planned, ${gateCompleteness!.skipped} skipped, ` + - `${gateIdempotency!.matched_by_provenance} matched by provenance, ${gateIdempotency!.new_entries} new.`, + `${gateIdempotency!.matched_by_provenance} matched by provenance, ${gateIdempotency!.adopted} adopted, ${gateIdempotency!.new_entries} new.`, ); } @@ -3537,7 +3559,7 @@ export async function runImport( const source = entry.fidelity!; const reason = source.github_close_reason.split(" (", 1)[0]!; return { issue: entry.issueNumber, item: entry.match?.id, - action: entry.linkOnly ? "link" : entry.match?.id ? "update" : "create", status: entry.status, + action: 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, updated_at_policy: "sdk-mutation-clock; source in github_updated_at", @@ -3546,7 +3568,9 @@ export async function runImport( release: entry.linkOnly ? "preserved-provider-owner" : source.github_release, identity: entry.identity }; }); - const gatePlanFields = opts.gate ? { plan: fidelityPlan(), gate: { completeness: gateCompleteness, idempotency: gateIdempotency, scan: "post-write" } } : {}; + const adoptionCount = gatePlan!.filter(entry => entry.identity?.via === "citation").length; + const planFields = opts.gate || adoptionCount > 0 ? { plan: fidelityPlan() } : {}; + const gatePlanFields = { ...planFields, ...(opts.gate ? { gate: { completeness: gateCompleteness, idempotency: gateIdempotency, scan: "post-write" } } : {}) }; if (opts.atomic) { // The gated pipeline already prepared every entry (and counted its skips) @@ -3561,6 +3585,7 @@ export async function runImport( dryRun: true, wouldImport: 0, wouldUpdate: 0, + wouldAdopt: 0, wouldSkip: skipped, atomic: true, ...fetchFields, @@ -3574,13 +3599,13 @@ export async function runImport( if (opts.dryRun) { imported = prepared.filter((entry) => !entry.match?.id).length; - updated = prepared.length - imported; + updated = prepared.length - imported - adoptionCount; for (const entry of prepared) { - const action = entry.match?.id ? "update" : "import"; + const action = entry.identity?.via === "citation" ? "adopt" : entry.match?.id ? "update" : "import"; console.error(` [dry-run][atomic] #${entry.issueNumber} ${action}: ${opts.gate ? "(gated title)" : entry.title} (${entry.status})`); } console.error( - `[dry-run] Atomic plan would import ${imported}, update ${updated}, skip ${skipped}.`, + `[dry-run] Atomic plan would import ${imported}, update ${updated}, skip ${skipped}. Adopt ${adoptionCount}.`, ); if (opts.linkDeps) { console.error( @@ -3592,6 +3617,7 @@ export async function runImport( dryRun: true, wouldImport: imported, wouldUpdate: updated, + wouldAdopt: adoptionCount, wouldSkip: skipped, atomic: true, ...(opts.linkDeps ? { wouldLinkDependencyCandidates: countDependencyRefCandidates(repo, filtered) } : {}), @@ -3644,36 +3670,22 @@ export async function runImport( transactionId: result.transactionId, recovered: result.recovered, imported: result.imported, - updated: result.updated, + updated: result.updated - (result.recovered ? 0 : adoptionCount), + adopted: result.recovered ? 0 : adoptionCount, ...(result.recoveredItems !== undefined ? { recoveredItems: result.recoveredItems } : {}), skipped, atomic: true, ...depLinkResultFields(atomicDepLink), ...fetchFields, - ...(opts.gate ? { plan: fidelityPlan() } : {}), + ...planFields, ...(gateReceipt ? { gate: gateReceipt } : {}), }; } for (const issue of filtered) { - // With --gate, the pre-mutation plan phase already prepared (or explicitly - // skipped) every issue; reuse the verified plan instead of re-preparing so - // the write phase cannot diverge from it. - const prepared = gatePlanByNumber - ? gatePlanByNumber.get(issue.number) - : await prepareGithubImport( - issue, - repo, - opts, - token, - existing.get(`${repo.toLowerCase()}#${issue.number}`), - dependencies.fetchIssueComments, - fidelityContext, - ); + // Every import reuses the pre-mutation identity plan, including ungated runs. + const prepared = gatePlanByNumber!.get(issue.number); if (!prepared) { - // A gated run counted its skips during the verified plan phase; only the - // ungated path counts them here. - if (!gatePlanByNumber) skipped++; continue; } @@ -3707,10 +3719,11 @@ export async function runImport( }; if (opts.dryRun) { - const action = match?.id ? "update" : "import"; + const action = prepared.identity?.via === "citation" ? "adopt" : match?.id ? "update" : "import"; const metadata = !opts.gate && labels.length > 0 ? `${status}, ${labels.join(",")}` : status; console.error(` [dry-run] #${issue.number} ${action}: ${opts.gate ? "(gated title)" : title} (${metadata})`); - if (match?.id) updated++; + if (prepared.identity?.via === "citation") adopted++; + else if (match?.id) updated++; else imported++; continue; } @@ -3720,7 +3733,8 @@ export async function runImport( const linked = pmRun(["--path", pmRoot, "update", ownerId, "--add-tags", tags.join(",")]); if (!linked.ok) throw new CommandError(`Could not link GitHub issue #${issue.number} to its existing owner.`); await persistImportFidelity(pmRoot, ownerId, prepared.fidelity!, true); - updated++; + if (prepared.identity?.via === "citation") adopted++; + else updated++; continue; } if (match?.id) { @@ -3768,7 +3782,8 @@ export async function runImport( commentWriteFailures += (await syncGithubCommentsToAnnotations(match.id, comments, pmRoot, issue.number)).failed; } await persistImportFidelity(pmRoot, match.id, prepared.fidelity!, false); - updated++; + if (prepared.identity?.via === "citation") adopted++; + else updated++; continue; } @@ -3836,7 +3851,7 @@ export async function runImport( } if (opts.dryRun) { - console.error(`[dry-run] Would import ${imported}, update ${updated}, skip ${skipped}.`); + console.error(`[dry-run] Would import ${imported}, update ${updated}, skip ${skipped}. Adopt ${adopted}.`); if (opts.linkDeps) { console.error( `[dry-run] --link-deps: ${countDependencyRefCandidates(repo, filtered)} candidate reference(s) parsed; ` + @@ -3847,6 +3862,7 @@ export async function runImport( dryRun: true, wouldImport: imported, wouldUpdate: updated, + wouldAdopt: adopted, wouldSkip: skipped, ...(opts.linkDeps ? { wouldLinkDependencyCandidates: countDependencyRefCandidates(repo, filtered) } : {}), ...gatePlanFields, @@ -3854,8 +3870,8 @@ export async function runImport( }; } - console.error(`Imported ${imported} new, updated ${updated} existing, skipped ${skipped}.`); - if (imported === 0 && updated === 0 && skipped > 0) { + console.error(`Imported ${imported} new, updated ${updated} existing, adopted ${adopted}, skipped ${skipped}.`); + if (imported === 0 && updated === 0 && adopted === 0 && skipped > 0) { throw new CommandError(`Imported 0 issue(s); ${skipped} failed.`); } // A gated import is all-or-nothing in effect: a planned write that failed @@ -3877,7 +3893,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, skipped, ...fetchFields, ...depLinkResultFields(depLink), ...(opts.gate ? { plan: fidelityPlan() } : {}), ...(gateReceipt ? { gate: gateReceipt } : {}) }; + return { imported, updated, adopted, skipped, ...fetchFields, ...depLinkResultFields(depLink), ...planFields, ...(gateReceipt ? { gate: gateReceipt } : {}) }; } /** @@ -6603,6 +6619,7 @@ export function isMutatingGithubCommand(command: string, options: Record Promise, git = false): Promise { + const directory = mkdtempSync(join(tmpdir(), "pm-cited-issue-")); + const pmRoot = join(directory, ".agents/pm"); + const bin = join(directory, "bin"); + mkdirSync(bin); + symlinkSync(process.execPath, join(bin, "node")); + symlinkSync(cli, join(bin, "pm")); + try { + const env = { ...process.env, GIT_CEILING_DIRECTORIES: tmpdir(), PM_GLOBAL_PATH: join(directory, "global") }; + if (git) assert.equal(spawnSync("git", ["init", "-q", directory], { env }).status, 0); + const initialized = spawnSync(cli, ["--no-extensions", "--path", pmRoot, "init", "fixture"], { env, encoding: "utf8" }); + assert.equal(initialized.status, 0, initialized.stderr); + await withEnv({ PM_PATH: pmRoot, PM_AUTHOR: "fixture", PM_GLOBAL_PATH: env.PM_GLOBAL_PATH, + GIT_CEILING_DIRECTORIES: tmpdir(), PATH: git ? join(root, "node_modules/.bin") + delimiter + process.env.PATH : bin }, () => run(pmRoot)); + } finally { rmSync(directory, { recursive: true, force: true }); } +} + +/** Import injected REST data through the real planner and SDK or CLI mutation path. */ +async function importing(pmRoot: string, options: Record, issues = [issue]) { + return captureStderr(async () => await runImport(repo, pmRoot, parseImportOptions(options), { + resolveToken: () => undefined, fetchIssues: async () => issues, + }) as Record); +} + +/** Read a certified complete corpus for identity and no-write assertions. */ +async function items(pmRoot: string) { + const result = await listAllComplete({ includeBody: true }, { pmRoot, noExtensions: true }); + certifyCompleteListResult(result); + return result.items; +} + +for (const atomic of [false, true]) { + test(`${atomic ? "atomic" : "plain"} citation adopts a terminal item without git, then re-import updates one item`, async () => tracker(async pmRoot => { + const existing = (await create({ type: "Task", title: "Local fix", description: `Fixed (${issue.html_url}).`, + body: `Fix for ${repo}#30`, tags: "local", status: atomic ? "canceled" : "closed", closeReason: "Landed locally" }, { pmRoot })).item; + const before = await items(pmRoot); + const preview = await importing(pmRoot, { atomic, "dry-run": true }); + assert.equal(preview.result.wouldImport, 0); + assert.equal(preview.result.wouldUpdate, 0); + assert.equal(preview.result.wouldAdopt, 1); + assert.match(preview.stderr.join("\n"), /#30 adopt:/); + assert.deepEqual((preview.result.plan as { action: string; item: string }[]).map(entry => [entry.action, entry.item]), [["adopt", existing.id]]); + assert.deepEqual(await items(pmRoot), before); + const landed = await importing(pmRoot, { atomic }); + assert.equal(landed.result.adopted, 1); + assert.equal(landed.result.imported, 0); + assert.equal(landed.result.updated, 0); + const actual = (await get(existing.id, {}, { pmRoot })).item; + assert.ok(actual.tags!.includes(`gh:${repo}#30`)); + assert.ok(actual.tags!.includes("local")); + assert.equal(actual.github_url, issue.html_url); + assert.equal(actual.github_state, "closed"); + assert.equal(actual.github_closed_at, issue.closed_at); + assert.equal(actual.status, "closed"); + const repeated = await importing(pmRoot, { atomic }); + assert.equal(repeated.result.adopted, 0); + assert.equal(repeated.result.imported, 0); + assert.equal((await items(pmRoot)).length, 1); + })); + + test(`${atomic ? "atomic" : "plain"} ambiguity lists both candidates and refuses the entire batch before writes`, async () => tracker(async pmRoot => { + const first = (await create({ type: "Task", title: `Fix ${repo}#30` }, { pmRoot })).item; + const second = (await create({ type: "Task", title: "Other fix", body: `${issue.html_url})` }, { pmRoot })).item; + const before = await items(pmRoot); + const batch = [{ ...issue, number: 31, html_url: issue.html_url.replace("/30", "/31") }, issue]; + const preview = await importing(pmRoot, { atomic, gate: true, "dry-run": true }, batch); + assert.equal(preview.result.blocked, true); + assert.equal(preview.result.wouldImport, 0); + 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(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); + await assert.rejects(importing(pmRoot, { atomic }, [{ ...issue, title: "", number: 31 }, issue]), /ambiguous_citation/); + })); + + test(`${atomic ? "atomic" : "plain"} gate counts an adopt as one existing landing item`, async () => tracker(async pmRoot => { + const existing = (await create({ type: "Task", title: `Fixed ${repo}#30` }, { pmRoot })).item; + const preview = await importing(pmRoot, { atomic, gate: true, "dry-run": true }); + const gate = preview.result.gate as { idempotency: Record }; + assert.deepEqual(gate.idempotency, { adopted: 1, provenance_indexed: 0, matched_by_provenance: 0, new_entries: 0 }); + await importing(pmRoot, { atomic, gate: true }); + assert.equal((await items(pmRoot))[0]!.id, existing.id); + assert.equal((await items(pmRoot)).length, 1); + }, true)); +} + +test("citation matching accepts exact qualified references across prose fields and trailing punctuation", () => { + for (const field of ["title", "description", "body"]) { + for (const citation of [issue.html_url, `${issue.html_url}.`, `(${issue.html_url})`, `${repo}#30`, `ACME/Widgets#30;`]) { + const local = { id: "local", [field]: `Fixed ${citation}` }; + assert.equal(matchImportedIssue([local], issue).item, local, `${field}: ${citation}`); + } + } + assert.equal(matchImportedIssue([{ id: "local", tags: ["gh:invalid"], title: `${repo}#30` }], issue).via, "citation"); + assert.equal(matchImportedIssue([{ id: "local", title: `${repo}#30` }], { ...issue, pull_request: {} }).via, "new"); +}); + +test("bare numbers, other repos, longer identities, URL paths and different issue tags cannot adopt", () => { + for (const body of ["#30", "https://github.com/other/widgets/issues/30", "other/widgets#30", `${repo}#300`, + `${issue.html_url}0`, `${issue.html_url}/extra`, `${issue.html_url}?view=all`, `${issue.html_url}.json`, + `https://other.test/${repo}#30`, `x${issue.html_url}`, "http://github.com/acme/widgets/issues/30"]) { + assert.equal(matchImportedIssue([{ id: "local", body }], issue).via, "new", body); + } + assert.equal(matchImportedIssue([{ id: "tagged-other", body: issue.html_url, tags: [`gh:${repo}#31`] }], issue).via, "new"); + assert.equal(matchImportedIssue([{ title: repo + "#30" }], issue).via, "new"); +}); + +test("tagged and URL-linked owners take precedence over even multiple prose citations", () => { + const citing = ["first", "second"].map(id => ({ id, body: issue.html_url })); + const tagged = { id: "tagged", 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); + assert.throws(() => matchImportedIssue(citing, issue), AmbiguousCitationError); +}); + +test("opt-out preserves create semantics for reference-only prose", async () => tracker(async pmRoot => { + await create({ type: "Task", title: `References ${repo}#30` }, { pmRoot }); + assert.equal(parseImportOptions({}).noAdoptCited, false); + assert.equal(parseImportOptions({ noAdoptCited: true }).noAdoptCited, true); + const preview = await importing(pmRoot, { "dry-run": true, "no-adopt-cited": true }); + assert.equal(preview.result.wouldImport, 1); + assert.equal(preview.result.wouldAdopt, 0); +})); + +test("provider-owned citation adoption links without replacing local context", async () => tracker(async pmRoot => { + const owner = (await create({ type: "Task", title: `Provider fix ${repo}#30`, body: "Native context", tags: "linear:fixture", status: "in_progress" }, { pmRoot })).item; + const result = await importing(pmRoot, {}); + assert.equal(result.result.adopted, 1); + const actual = (await get(owner.id, {}, { pmRoot })).item; + assert.equal(actual.title, owner.title); + assert.equal(actual.status, owner.status); + assert.equal(actual.github_url, issue.html_url); +})); + +test("gate rejects adopting the same local item for two issues", async () => tracker(async pmRoot => { + await create({ type: "Task", title: `${repo}#30 and ${repo}#31` }, { pmRoot }); + for (const gate of [false, true]) await assert.rejects(importing(pmRoot, { gate, "dry-run": true }, [issue, { ...issue, number: 31 }]), /conflicting target/); +})); + +test("a citation without a local tag array still receives the issue provenance tag", async () => { + const entry = await prepareGithubImport(issue, repo, parseImportOptions({}), undefined, undefined, undefined, + { items: [{ id: "local", title: `${repo}#30` }], releases: [], timeline: async () => [] }); + assert.ok(entry!.tags.includes(`gh:${repo}#30`)); +}); + +test("idempotency requires the provenance tag on an adopt too", () => { + const entry = { issueNumber: 30, tags: [], identity: { via: "citation" }, match: { id: "local" } } as unknown as PreparedGithubImport; + assert.throws(() => verifyImportIdempotency([], [entry], repo), /without a provenance tag/); +}); diff --git a/test/dryrun-preview.test.ts b/test/dryrun-preview.test.ts index 3175604a..0129d9d4 100644 --- a/test/dryrun-preview.test.ts +++ b/test/dryrun-preview.test.ts @@ -53,6 +53,7 @@ test("non-atomic --dry-run previews updates for already-linked issues instead of dryRun: true, wouldImport: 1, wouldUpdate: 1, + wouldAdopt: 0, wouldSkip: 0, }); assert.ok( diff --git a/test/import-errors.test.ts b/test/import-errors.test.ts index 1b1b0b87..fd3ff5b5 100644 --- a/test/import-errors.test.ts +++ b/test/import-errors.test.ts @@ -52,10 +52,10 @@ test("atomic previews distinguish empty upstream data from entirely skipped issu let data: GhIssue[] = []; await withMockGithub((_req, res) => jsonResponse(res, 200, data), async () => { assert.deepEqual(await runImport("acme/widgets", root, parseImportOptions({ atomic: true, "dry-run": true }), { resolveToken: () => undefined }), - { dryRun: true, wouldImport: 0, wouldUpdate: 0, wouldSkip: 0, atomic: true }); + { dryRun: true, wouldImport: 0, wouldUpdate: 0, wouldAdopt: 0, wouldSkip: 0, atomic: true }); data = [issue({ title: " " })]; assert.deepEqual(await runImport("acme/widgets", root, parseImportOptions({ atomic: true, "dry-run": true }), { resolveToken: () => undefined }), - { dryRun: true, wouldImport: 0, wouldUpdate: 0, wouldSkip: 1, atomic: true }); + { dryRun: true, wouldImport: 0, wouldUpdate: 0, wouldAdopt: 0, wouldSkip: 1, atomic: true }); await assert.rejects(runImport("acme/widgets", root, parseImportOptions({ atomic: true }), { resolveToken: () => undefined }), /Imported 0 issue\(s\); 1 failed/); }); assert.deepEqual(readPmItems(root), []); @@ -68,7 +68,7 @@ test("ungated import tolerates unavailable comments and still writes the issue", else jsonResponse(res, 200, [issue({ comments: 1 })]); }, async () => { const { stderr, result } = await captureStderr(() => runImport("acme/widgets", root, parseImportOptions({ "comments-mode": "both" }), { resolveToken: () => undefined })); - assert.deepEqual(result, { imported: 1, updated: 0, skipped: 0 }); + assert.deepEqual(result, { imported: 1, updated: 0, adopted: 0, skipped: 0 }); assert.match(stderr.join("\n"), /failed to fetch comments/); }); assert.equal(readPmItems(root)[0].body, "Original body"); diff --git a/test/import-fidelity.test.ts b/test/import-fidelity.test.ts index c92fb1ee..b24ad32e 100644 --- a/test/import-fidelity.test.ts +++ b/test/import-fidelity.test.ts @@ -286,12 +286,12 @@ test("identity accepts nested GitHub links, provider back-links, exact URLs and assert.throws(() => matchImportedIssue([{ id: "one", links: issue.html_url }, { id: "two", links: issue.html_url }], issue), /multiple existing owners/); }); -test("identity excludes descriptions even when multiple items mention another issue", () => { +test("citation opt-out excludes prose while retaining structured identity matching", () => { const issue = recorded("pm-linear-134"); const references = ["one", "two"].map(id => ({ id, description: `Blocked by ${issue.html_url}#issuecomment-1`, body: issue.html_url, source_description: issue.html_url, unrelated_url: issue.html_url })); - assert.doesNotThrow(() => matchImportedIssue(references, issue)); - assert.deepEqual(matchImportedIssue(references, issue), { item: undefined, linkOnly: false, via: "new" }); + assert.doesNotThrow(() => matchImportedIssue(references, issue, undefined, false)); + assert.deepEqual(matchImportedIssue(references, issue, undefined, false), { item: undefined, linkOnly: false, via: "new" }); const owner = { id: "owner", github_url: issue.html_url }; assert.equal(matchImportedIssue([...references, owner], issue).item, owner); assert.throws(() => matchImportedIssue([owner, { ...owner, id: "competitor" }], issue), /multiple existing owners/); diff --git a/test/import-gate.test.ts b/test/import-gate.test.ts index 037e454f..a524ea10 100644 --- a/test/import-gate.test.ts +++ b/test/import-gate.test.ts @@ -430,7 +430,7 @@ test("verifyImportIdempotency fails duplicate provenance, untagged creates, and ], "acme/widgets", ); - assert.deepStrictEqual(receipts, { provenance_indexed: 1, matched_by_provenance: 1, new_entries: 1 }); + assert.deepStrictEqual(receipts, { adopted: 0, provenance_indexed: 1, matched_by_provenance: 1, new_entries: 1 }); }); test("a gated import refuses to write when the corpus cannot yield an idempotent plan", async () => { @@ -629,7 +629,7 @@ test("gated dry-run embeds the verified plan receipts and writes nothing", async assert.strictEqual(preview.wouldImport, 1); const gate = preview.gate as Record; assert.deepStrictEqual(gate.completeness, { fetched: 1, planned: 1, skipped: 0 }); - assert.deepStrictEqual(gate.idempotency, { provenance_indexed: 0, matched_by_provenance: 0, new_entries: 1 }); + assert.deepStrictEqual(gate.idempotency, { adopted: 0, provenance_indexed: 0, matched_by_provenance: 0, new_entries: 1 }); assert.strictEqual(gate.scan, "post-write"); // Nothing was written: the working tree still matches the baseline commit. assert.strictEqual(git(["status", "--porcelain"]).stdout.trim(), ""); diff --git a/test/import-selection.test.ts b/test/import-selection.test.ts index df973101..2293fef3 100644 --- a/test/import-selection.test.ts +++ b/test/import-selection.test.ts @@ -4,7 +4,7 @@ import { mkdtempSync, readFileSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join, delimiter } from "node:path"; import test from "node:test"; -import { comments as readComments } from "@unbrained/pm-cli/sdk"; +import { comments as readComments, create } from "@unbrained/pm-cli/sdk"; import { CommandError, EXIT_CODE, fetchAllIssues, parseImportOptions, parseIssueNumbers, readPmItems, runImport, type GhIssue, type GhComment, @@ -254,6 +254,7 @@ test("built package dispatch advertises and honors selectors and limits through assert.equal(help.code, 0, help.stderr); assert.match(help.stdout, /--issue/); assert.match(help.stdout, /--limit/); + assert.match(help.stdout, /--no-adopt-cited/); const selected = await call(["unbraind/pm-web", "--issue", "189", "--dry-run", "--json"]); assert.equal(selected.code, 0, selected.stderr); assert.equal((JSON.parse(selected.stdout) as { wouldImport: number }).wouldImport, 1); @@ -263,5 +264,13 @@ test("built package dispatch advertises and honors selectors and limits through assert.equal(server.requests.length, 2); assert.equal(server.requests[0]!.url, "/repos/unbraind/pm-web/issues/189"); assert.deepEqual(readPmItems(pmRoot), []); + await create({ type: "Task", title: "Fix unbraind/pm-web#189" }, { pmRoot }); + const adopted = await call(["unbraind/pm-web", "--issue", "189", "--dry-run", "--json"]); + assert.equal(adopted.code, 0, adopted.stderr); + assert.equal((JSON.parse(adopted.stdout) as { wouldAdopt: number }).wouldAdopt, 1); + const optedOut = await call(["unbraind/pm-web", "--issue", "189", "--dry-run", "--no-adopt-cited", "--json"]); + assert.equal(optedOut.code, 0, optedOut.stderr); + assert.equal((JSON.parse(optedOut.stdout) as { wouldImport: number }).wouldImport, 1); + assert.equal(readPmItems(pmRoot).length, 1); }); })); From 32f856ce220059d4001c6c112748442180394bdd Mon Sep 17 00:00:00 2001 From: SteveBot <1153461+unbraind@users.noreply.github.com> Date: Fri, 23 Oct 2026 09:51:40 +0200 Subject: [PATCH 02/18] Verify imports refuse ambiguous batches before any writes --- .agents/pm/features/pm-github-cited-issue-adoption.toon | 6 ++++-- .agents/pm/history/pm-github-cited-issue-adoption.jsonl | 4 ++++ test/import-fidelity.test.ts | 7 ++----- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/.agents/pm/features/pm-github-cited-issue-adoption.toon b/.agents/pm/features/pm-github-cited-issue-adoption.toon index 7520b9ec..f1b33663 100644 --- a/.agents/pm/features/pm-github-cited-issue-adoption.toon +++ b/.agents/pm/features/pm-github-cited-issue-adoption.toon @@ -6,10 +6,10 @@ status: in_progress priority: 2 tags[3]: changelog-unreleased,github,import created_at: "2026-10-23T07:16:54.041Z" -updated_at: "2026-10-23T07:42:13.982Z" +updated_at: "2026-10-23T07:49:54.254Z" author: codex-agent acceptance_criteria: "Exact issue URL or owner/repo#N in title, description or body adopts one untagged item, including closed and canceled items. Bare numbers and other repositories never match; existing structured identity takes precedence. Multiple candidates return typed ambiguous_citation entries listing IDs and a tagging remedy with no create. Gates verify one landing target; receipts count adopts; dry runs show adopts. Import and scheduled sync adopt by default with --no-adopt-cited for reference-only prose. Behavioral revert tests and no-git tracker tests pass. Validate current pm-review main issue 30 and another public repository. Targeted tests, typecheck, lint and npm/Bun CI release gates pass." -comments[7]{created_at,author,text}: +comments[9]{created_at,author,text}: "2026-10-23T07:17:26.861Z",codex-agent,"Overlap review: pm-github-c093 owns structured URL/provider back-link import matching, pm-github-identity-collisions diagnoses and repairs structured ownership, Projects import matches board and issue provenance, and export upserts linked issues. None adopts arbitrary exact prose citations. This feature adds a fallback only after existing import matching, preserving those paths. Scheduled gated sync already invokes import, so it inherits the default. Plan ambiguous citations before writes and refuse the batch with actionable candidate IDs." "2026-10-23T07:24:01.857Z",codex-agent,"Implementation: strong ownership matching stays first; exact qualified title/description/body citations fall back to a single existing item. All imports now prepare identities before writes, and ambiguous citations expose candidate IDs plus an exact tagging remedy. Adoption retains local tags and reports a separate count; provider-owned records retain link-only behavior. New tests use real trackers with a PATH containing node and the pinned pm CLI but no git, plus real git trackers for write-gate acceptance." "2026-10-23T07:27:45.693Z",codex-agent,"New citation suite: 13/13 passed, including plain/atomic adoption in non-git trackers, write-gate acceptance, retry identity, ambiguity refusal, punctuation, repo/number boundaries, structured ownership precedence and provider link-only preservation. Live pm-review main aa16d855df1f7b74afd3453617f77e92c44d493e already tags pm-review-withdrawn-lineage for issue 30, so an untouched current-main copy correctly plans update rather than adopt. Preserve this current-state result and locate the pre-tag historical tracker for a separate adoption replay. SDK reads may create runtime caches; verify original tracked file bytes independently." @@ -17,6 +17,8 @@ comments[7]{created_at,author,text}: "2026-10-23T07:38:52.136Z",codex-agent,"Behavioral revert proof: an isolated scratch package retained the new planner, receipts and tests, but restored only matchImportedIssue from base main. node --test test/cited-issue-adoption.test.ts exited 1 with the expected adoption and ambiguity assertion failures. Tests import live TypeScript, so no stale build could hide the revert. The fixed worktree was never reverted and its 14 adoption regressions pass. Full npm/Bun release evidence will come from the PR CI release gates at unchanged thresholds." "2026-10-23T07:40:47.558Z",codex-agent,"Local checks passed: npm run typecheck; npm run build; npm run build:test; npm run lint; npm run docstring (17 files, 187 documented declarations); npm run duplication (18 authored modules, zero duplicated lines); changelog generation/check and strict tracker health. Behavioral revert: 14 tests, 10 fail and 4 pass with only the old matcher restored; fixed matcher: 14 pass, zero skips. Broader import regressions and the mandatory linked-test run are in progress. The current dependency pin is unchanged; registry CLI version was verified as 2026.10.23." "2026-10-23T07:42:06.837Z",codex-agent,"Mandatory linked verification completed: pm test pm-github-cited-issue-adoption --run --progress passed its one linked command, node --test test/cited-issue-adoption.test.ts (14 tests pass, zero failures/skips). Public tracker dry runs and behavioral revert evidence are complete. The ready-for-review PR will use CI npm run release:check and bun run release:check, unchanged thresholds, as full release-gate evidence; the final report will link the exact-head run and its counts. No merge or release is authorized." + "2026-10-23T07:46:57.681Z",codex-agent,"Broader local regressions found a legacy plain-import assertion that expected an earlier issue to be written before a later structured ownership conflict. Universal identity preplanning intentionally removes that partial-write boundary. Updated the regression to require the unchanged tracker in plain, gated and atomic modes; structured matching and precedence remain unchanged. No production-code change was required. PR: https://github.com/unbraind/pm-github/pull/169." + "2026-10-23T07:49:35.957Z",codex-agent,"Focused write-boundary regression passed: node --test --test-name-pattern=\"ambiguous later issue\" test/import-fidelity.test.ts — 3 tests pass, zero failures/skips (plain, gated, atomic). This updates the old partial-write expectation to the fail-closed preflight contract. The initial CI run still tests the prior assertion; the next pushed head will contain this test-only correction and tracker receipt." files[9]{path,scope}: import-fidelity.ts,project index.ts,project diff --git a/.agents/pm/history/pm-github-cited-issue-adoption.jsonl b/.agents/pm/history/pm-github-cited-issue-adoption.jsonl index 0980dd27..efdd6920 100644 --- a/.agents/pm/history/pm-github-cited-issue-adoption.jsonl +++ b/.agents/pm/history/pm-github-cited-issue-adoption.jsonl @@ -14,3 +14,7 @@ {"hash_algorithm":"sha256","ts":"2026-10-23T07:40:47.559Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/5","value":{"created_at":"2026-10-23T07:40:47.558Z","author":"codex-agent","text":"Local checks passed: npm run typecheck; npm run build; npm run build:test; npm run lint; npm run docstring (17 files, 187 documented declarations); npm run duplication (18 authored modules, zero duplicated lines); changelog generation/check and strict tracker health. Behavioral revert: 14 tests, 10 fail and 4 pass with only the old matcher restored; fixed matcher: 14 pass, zero skips. Broader import regressions and the mandatory linked-test run are in progress. The current dependency pin is unchanged; registry CLI version was verified as 2026.10.23."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T07:40:47.559Z"}],"before_hash":"cd707eb466020e9e2fd73e60e087610788a7de030b31bb2d2521d0148ee69536","after_hash":"18ebc3c22a0879a77edf6ae70df7bc016994f8ff7b049d12ff6854d2e91ee106","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"a2eb6e79616e5011dbe85f307f684e7b46d03187294d105b8fae05b1745b2340"} {"hash_algorithm":"sha256","ts":"2026-10-23T07:42:06.838Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/6","value":{"created_at":"2026-10-23T07:42:06.837Z","author":"codex-agent","text":"Mandatory linked verification completed: pm test pm-github-cited-issue-adoption --run --progress passed its one linked command, node --test test/cited-issue-adoption.test.ts (14 tests pass, zero failures/skips). Public tracker dry runs and behavioral revert evidence are complete. The ready-for-review PR will use CI npm run release:check and bun run release:check, unchanged thresholds, as full release-gate evidence; the final report will link the exact-head run and its counts. No merge or release is authorized."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T07:42:06.838Z"}],"before_hash":"18ebc3c22a0879a77edf6ae70df7bc016994f8ff7b049d12ff6854d2e91ee106","after_hash":"fc43fc3166a06185f926b5273e40c995918dfb4322223adb12c297846f9b5564","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"b714aa2aa79b8e9a82efad3a873b552ffc02027b4e29aed8887ba1adf5eccc7d"} {"hash_algorithm":"sha256","ts":"2026-10-23T07:42:13.982Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":{"value":"implementer","source":"argv"},"topic":null},"op":"release","patch":[{"op":"remove","path":"/metadata/claim_principal"},{"op":"remove","path":"/metadata/assignee"},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T07:42:13.982Z"}],"before_hash":"fc43fc3166a06185f926b5273e40c995918dfb4322223adb12c297846f9b5564","after_hash":"70f669ce6defd83fc68714d8258cab990d2ac62dfa4437c1666a0f48237d3233","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"81602d17837c1b21003c92cd6f13e37c8302b578ba99758d16ffe797d3882239"} +{"hash_algorithm":"sha256","ts":"2026-10-23T07:46:53.286Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":{"value":"implementer","source":"argv"},"topic":null},"op":"claim","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T07:46:53.286Z"},{"op":"add","path":"/metadata/assignee","value":"codex-agent"},{"op":"add","path":"/metadata/claim_principal","value":"codex-agent"}],"before_hash":"70f669ce6defd83fc68714d8258cab990d2ac62dfa4437c1666a0f48237d3233","after_hash":"f1b341c097f780d8ec03e4e66b330299c50ecbf77cb842115643a5238ad29db9","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"e86f75a0d3affd879d7fdb51c95fe8b95d8c0a468dc922da11b72064e0bdda5b"} +{"hash_algorithm":"sha256","ts":"2026-10-23T07:46:57.682Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/7","value":{"created_at":"2026-10-23T07:46:57.681Z","author":"codex-agent","text":"Broader local regressions found a legacy plain-import assertion that expected an earlier issue to be written before a later structured ownership conflict. Universal identity preplanning intentionally removes that partial-write boundary. Updated the regression to require the unchanged tracker in plain, gated and atomic modes; structured matching and precedence remain unchanged. No production-code change was required. PR: https://github.com/unbraind/pm-github/pull/169."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T07:46:57.682Z"}],"before_hash":"f1b341c097f780d8ec03e4e66b330299c50ecbf77cb842115643a5238ad29db9","after_hash":"e96af058310e6d576b3232f4aee3c726be2348fdb7eabcde42cc025e644bd8bb","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"d7467262e25840963057c5c1a728dd6c429b0f28ff5d6a8ccdb8e1d8764382fa"} +{"hash_algorithm":"sha256","ts":"2026-10-23T07:49:35.957Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/8","value":{"created_at":"2026-10-23T07:49:35.957Z","author":"codex-agent","text":"Focused write-boundary regression passed: node --test --test-name-pattern=\"ambiguous later issue\" test/import-fidelity.test.ts — 3 tests pass, zero failures/skips (plain, gated, atomic). This updates the old partial-write expectation to the fail-closed preflight contract. The initial CI run still tests the prior assertion; the next pushed head will contain this test-only correction and tracker receipt."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T07:49:35.957Z"}],"before_hash":"e96af058310e6d576b3232f4aee3c726be2348fdb7eabcde42cc025e644bd8bb","after_hash":"435b44a383e13fce7c0a3c124fe81ac8ae36222d8930f9ba17ed234ad32785f2","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"c3599412fa1226e6893cae1064eb9a0cb65e55fa0d057f83d2df33c7b3b0eba8"} +{"hash_algorithm":"sha256","ts":"2026-10-23T07:49:54.254Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":{"value":"implementer","source":"argv"},"topic":null},"op":"release","patch":[{"op":"remove","path":"/metadata/claim_principal"},{"op":"remove","path":"/metadata/assignee"},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T07:49:54.254Z"}],"before_hash":"435b44a383e13fce7c0a3c124fe81ac8ae36222d8930f9ba17ed234ad32785f2","after_hash":"535e7d70087b2323413d07c82d184261311f09f1d67a99c1ff76a1f2cc668b25","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"40663805fd470df17bf899cb2783c793c8921f7dcff11d429b64667de3f7b545"} diff --git a/test/import-fidelity.test.ts b/test/import-fidelity.test.ts index b24ad32e..cd83f045 100644 --- a/test/import-fidelity.test.ts +++ b/test/import-fidelity.test.ts @@ -202,7 +202,7 @@ for (const atomic of [false, true]) { } for (const mode of ["plain", "gate", "atomic"] as const) { - test(`ambiguous later issue documents write boundary (${mode})`, async () => tracker(async (_directory, pmRoot) => { + test(`ambiguous later issue refuses the batch before writes (${mode})`, async () => tracker(async (_directory, pmRoot) => { const second = { ...recorded("pm-csv-72"), state: "open" as const, closed_at: null }; const first = { ...second, number: 1, html_url: "https://github.com/unbraind/pm-csv/issues/1", title: "Earlier issue" }; await identityItem(pmRoot, { github_url: second.html_url }); @@ -212,10 +212,7 @@ for (const mode of ["plain", "gate", "atomic"] as const) { parseImportOptions({ state: "all", gate: mode === "gate", atomic: mode === "atomic" }), { fetchIssues: async () => [first, second], resolveToken: () => undefined })), /multiple existing owners/); const after = await listItems(pmRoot); - if (mode === "plain") { - assert.equal(after.length, before.length + 1); - assert.equal(after.find(item => item.github_url === first.html_url)?.title, first.title); - } else assert.deepEqual(after, before); + assert.deepEqual(after, before); })); } From bbe571892f6b383d5c9b79433d4c176f17142a93 Mon Sep 17 00:00:00 2001 From: SteveBot <1153461+unbraind@users.noreply.github.com> Date: Fri, 23 Oct 2026 10:30:15 +0200 Subject: [PATCH 03/18] Recognize issue citations beside prose labels and question marks --- .agents/pm/features/pm-github-cited-issue-adoption.toon | 6 ++++-- .agents/pm/history/pm-github-cited-issue-adoption.jsonl | 4 ++++ docs/cited-issue-adoption-validation.md | 9 ++++++++- import-fidelity.ts | 2 +- test/cited-issue-adoption.test.ts | 3 ++- 5 files changed, 19 insertions(+), 5 deletions(-) diff --git a/.agents/pm/features/pm-github-cited-issue-adoption.toon b/.agents/pm/features/pm-github-cited-issue-adoption.toon index f1b33663..c8d6259a 100644 --- a/.agents/pm/features/pm-github-cited-issue-adoption.toon +++ b/.agents/pm/features/pm-github-cited-issue-adoption.toon @@ -6,10 +6,10 @@ status: in_progress priority: 2 tags[3]: changelog-unreleased,github,import created_at: "2026-10-23T07:16:54.041Z" -updated_at: "2026-10-23T07:49:54.254Z" +updated_at: "2026-10-23T08:29:21.353Z" author: codex-agent acceptance_criteria: "Exact issue URL or owner/repo#N in title, description or body adopts one untagged item, including closed and canceled items. Bare numbers and other repositories never match; existing structured identity takes precedence. Multiple candidates return typed ambiguous_citation entries listing IDs and a tagging remedy with no create. Gates verify one landing target; receipts count adopts; dry runs show adopts. Import and scheduled sync adopt by default with --no-adopt-cited for reference-only prose. Behavioral revert tests and no-git tracker tests pass. Validate current pm-review main issue 30 and another public repository. Targeted tests, typecheck, lint and npm/Bun CI release gates pass." -comments[9]{created_at,author,text}: +comments[11]{created_at,author,text}: "2026-10-23T07:17:26.861Z",codex-agent,"Overlap review: pm-github-c093 owns structured URL/provider back-link import matching, pm-github-identity-collisions diagnoses and repairs structured ownership, Projects import matches board and issue provenance, and export upserts linked issues. None adopts arbitrary exact prose citations. This feature adds a fallback only after existing import matching, preserving those paths. Scheduled gated sync already invokes import, so it inherits the default. Plan ambiguous citations before writes and refuse the batch with actionable candidate IDs." "2026-10-23T07:24:01.857Z",codex-agent,"Implementation: strong ownership matching stays first; exact qualified title/description/body citations fall back to a single existing item. All imports now prepare identities before writes, and ambiguous citations expose candidate IDs plus an exact tagging remedy. Adoption retains local tags and reports a separate count; provider-owned records retain link-only behavior. New tests use real trackers with a PATH containing node and the pinned pm CLI but no git, plus real git trackers for write-gate acceptance." "2026-10-23T07:27:45.693Z",codex-agent,"New citation suite: 13/13 passed, including plain/atomic adoption in non-git trackers, write-gate acceptance, retry identity, ambiguity refusal, punctuation, repo/number boundaries, structured ownership precedence and provider link-only preservation. Live pm-review main aa16d855df1f7b74afd3453617f77e92c44d493e already tags pm-review-withdrawn-lineage for issue 30, so an untouched current-main copy correctly plans update rather than adopt. Preserve this current-state result and locate the pre-tag historical tracker for a separate adoption replay. SDK reads may create runtime caches; verify original tracked file bytes independently." @@ -19,6 +19,8 @@ comments[9]{created_at,author,text}: "2026-10-23T07:42:06.837Z",codex-agent,"Mandatory linked verification completed: pm test pm-github-cited-issue-adoption --run --progress passed its one linked command, node --test test/cited-issue-adoption.test.ts (14 tests pass, zero failures/skips). Public tracker dry runs and behavioral revert evidence are complete. The ready-for-review PR will use CI npm run release:check and bun run release:check, unchanged thresholds, as full release-gate evidence; the final report will link the exact-head run and its counts. No merge or release is authorized." "2026-10-23T07:46:57.681Z",codex-agent,"Broader local regressions found a legacy plain-import assertion that expected an earlier issue to be written before a later structured ownership conflict. Universal identity preplanning intentionally removes that partial-write boundary. Updated the regression to require the unchanged tracker in plain, gated and atomic modes; structured matching and precedence remain unchanged. No production-code change was required. PR: https://github.com/unbraind/pm-github/pull/169." "2026-10-23T07:49:35.957Z",codex-agent,"Focused write-boundary regression passed: node --test --test-name-pattern=\"ambiguous later issue\" test/import-fidelity.test.ts — 3 tests pass, zero failures/skips (plain, gated, atomic). This updates the old partial-write expectation to the fail-closed preflight contract. The initial CI run still tests the prior assertion; the next pushed head will contain this test-only correction and tracker receipt." + "2026-10-23T08:27:20.623Z",codex-agent,"Final exact-citation audit reproduced an additional boundary miss: Issue: followed immediately by the issue URL, Reference:owner/repo#N, and a sentence-ending question mark returned new instead of citation. These are qualified prose citations under the acceptance criteria. Adjusted only the regex punctuation boundaries; retained negative URL query/path and longer-number cases. Added the forms to the existing pure matching regression, so test counts stay unchanged. The final source head will receive a fresh full npm/Bun CI run." + "2026-10-23T08:29:20.672Z",codex-agent,"Punctuation boundary verification: typecheck, test typecheck and targeted lint passed. Four pure matching checks passed with the fix. Isolated behavioral revert restored only citesIssue to the preceding implementation: 1 of 4 checks failed (3 passed), proving the expanded boundary cases detect this correction. The general adoption revert remains 10 failures / 4 passes when only matchImportedIssue is restored to base main. All mutations remain covered by the real-tracker suite and the fresh full CI run." files[9]{path,scope}: import-fidelity.ts,project index.ts,project diff --git a/.agents/pm/history/pm-github-cited-issue-adoption.jsonl b/.agents/pm/history/pm-github-cited-issue-adoption.jsonl index efdd6920..ea03c550 100644 --- a/.agents/pm/history/pm-github-cited-issue-adoption.jsonl +++ b/.agents/pm/history/pm-github-cited-issue-adoption.jsonl @@ -18,3 +18,7 @@ {"hash_algorithm":"sha256","ts":"2026-10-23T07:46:57.682Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/7","value":{"created_at":"2026-10-23T07:46:57.681Z","author":"codex-agent","text":"Broader local regressions found a legacy plain-import assertion that expected an earlier issue to be written before a later structured ownership conflict. Universal identity preplanning intentionally removes that partial-write boundary. Updated the regression to require the unchanged tracker in plain, gated and atomic modes; structured matching and precedence remain unchanged. No production-code change was required. PR: https://github.com/unbraind/pm-github/pull/169."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T07:46:57.682Z"}],"before_hash":"f1b341c097f780d8ec03e4e66b330299c50ecbf77cb842115643a5238ad29db9","after_hash":"e96af058310e6d576b3232f4aee3c726be2348fdb7eabcde42cc025e644bd8bb","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"d7467262e25840963057c5c1a728dd6c429b0f28ff5d6a8ccdb8e1d8764382fa"} {"hash_algorithm":"sha256","ts":"2026-10-23T07:49:35.957Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/8","value":{"created_at":"2026-10-23T07:49:35.957Z","author":"codex-agent","text":"Focused write-boundary regression passed: node --test --test-name-pattern=\"ambiguous later issue\" test/import-fidelity.test.ts — 3 tests pass, zero failures/skips (plain, gated, atomic). This updates the old partial-write expectation to the fail-closed preflight contract. The initial CI run still tests the prior assertion; the next pushed head will contain this test-only correction and tracker receipt."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T07:49:35.957Z"}],"before_hash":"e96af058310e6d576b3232f4aee3c726be2348fdb7eabcde42cc025e644bd8bb","after_hash":"435b44a383e13fce7c0a3c124fe81ac8ae36222d8930f9ba17ed234ad32785f2","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"c3599412fa1226e6893cae1064eb9a0cb65e55fa0d057f83d2df33c7b3b0eba8"} {"hash_algorithm":"sha256","ts":"2026-10-23T07:49:54.254Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":{"value":"implementer","source":"argv"},"topic":null},"op":"release","patch":[{"op":"remove","path":"/metadata/claim_principal"},{"op":"remove","path":"/metadata/assignee"},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T07:49:54.254Z"}],"before_hash":"435b44a383e13fce7c0a3c124fe81ac8ae36222d8930f9ba17ed234ad32785f2","after_hash":"535e7d70087b2323413d07c82d184261311f09f1d67a99c1ff76a1f2cc668b25","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"40663805fd470df17bf899cb2783c793c8921f7dcff11d429b64667de3f7b545"} +{"hash_algorithm":"sha256","ts":"2026-10-23T08:27:18.635Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":{"value":"implementer","source":"argv"},"topic":null},"op":"claim","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T08:27:18.635Z"},{"op":"add","path":"/metadata/assignee","value":"codex-agent"},{"op":"add","path":"/metadata/claim_principal","value":"codex-agent"}],"before_hash":"535e7d70087b2323413d07c82d184261311f09f1d67a99c1ff76a1f2cc668b25","after_hash":"914e1a48e2b48a8ec1550f7c50dc36690b9250cbc64ed8722522e5404954a82a","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"c41cf850575bd68d41264561f52926f06257492429d3c9774ec99ef7bc8ec818"} +{"hash_algorithm":"sha256","ts":"2026-10-23T08:27:20.623Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/9","value":{"created_at":"2026-10-23T08:27:20.623Z","author":"codex-agent","text":"Final exact-citation audit reproduced an additional boundary miss: Issue: followed immediately by the issue URL, Reference:owner/repo#N, and a sentence-ending question mark returned new instead of citation. These are qualified prose citations under the acceptance criteria. Adjusted only the regex punctuation boundaries; retained negative URL query/path and longer-number cases. Added the forms to the existing pure matching regression, so test counts stay unchanged. The final source head will receive a fresh full npm/Bun CI run."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T08:27:20.623Z"}],"before_hash":"914e1a48e2b48a8ec1550f7c50dc36690b9250cbc64ed8722522e5404954a82a","after_hash":"1ecae54642b46d66db19d949e640a107782a882c8abcb7d54db0e57b2b2a0cb8","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"9e5130fbe1923b79ec5ad5f429aa01d46f62ca2271f0904a6e92af7158f8792c"} +{"hash_algorithm":"sha256","ts":"2026-10-23T08:29:20.672Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/10","value":{"created_at":"2026-10-23T08:29:20.672Z","author":"codex-agent","text":"Punctuation boundary verification: typecheck, test typecheck and targeted lint passed. Four pure matching checks passed with the fix. Isolated behavioral revert restored only citesIssue to the preceding implementation: 1 of 4 checks failed (3 passed), proving the expanded boundary cases detect this correction. The general adoption revert remains 10 failures / 4 passes when only matchImportedIssue is restored to base main. All mutations remain covered by the real-tracker suite and the fresh full CI run."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T08:29:20.672Z"}],"before_hash":"1ecae54642b46d66db19d949e640a107782a882c8abcb7d54db0e57b2b2a0cb8","after_hash":"9872a6735f44706faeead52305b5f7b6e57747a5098b9baf2407984b2f596466","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"b254d1dc60b57343a2c1b3190f044dd743048aec1a4287c6bcfe81e319ce853e"} +{"hash_algorithm":"sha256","ts":"2026-10-23T08:29:21.353Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":{"value":"implementer","source":"argv"},"topic":null},"op":"release","patch":[{"op":"remove","path":"/metadata/claim_principal"},{"op":"remove","path":"/metadata/assignee"},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T08:29:21.353Z"}],"before_hash":"9872a6735f44706faeead52305b5f7b6e57747a5098b9baf2407984b2f596466","after_hash":"f5e0cdba977c92d7bdcf2ed6786c791c3c5f833bf1f37bc2f4539be2b2a6a272","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"7fdd2032de1b3e40a5c2305df103cab788d1a112c4b50134ed034411d66ad0dd"} diff --git a/docs/cited-issue-adoption-validation.md b/docs/cited-issue-adoption-validation.md index f01e00da..4ed8ea8a 100644 --- a/docs/cited-issue-adoption-validation.md +++ b/docs/cited-issue-adoption-validation.md @@ -66,7 +66,8 @@ or SDK writes. Plain and atomic non-git cases run with a PATH containing only Node and the pinned pm CLI. Gate write cases use disposable Git repositories. Assertions cover single-candidate adoption, terminal candidates, no duplicate on retry, ambiguity before any batch write, URL punctuation, repository and -issue-number boundaries, structured ownership precedence, provider link-only +issue-number boundaries, colon-prefixed labels and sentence-ending question marks, +structured ownership precedence, provider link-only behavior, separate adoption counts, opt-out, and conflicting batch targets. `test/import-selection.test.ts` installs the built package into a disposable @@ -79,3 +80,9 @@ scratch package, then restored **only** `matchImportedIssue` from base main. 4 passes, versus 14 passes with the fix; neither run skipped tests. The fixed worktree was untouched. Tests import live TypeScript, so no compiled artifact could mask the revert. + +A focused punctuation revert restored only `citesIssue` to the preceding +implementation while retaining adoption and the expanded regression cases. +The four pure matching checks then produced 1 failure and 3 passes; the fixed +helper passes all four. This specifically proves colon-prefixed labels and +sentence-ending question marks are accepted without admitting URL queries. diff --git a/import-fidelity.ts b/import-fidelity.ts index 9b9b636d..6674de37 100644 --- a/import-fidelity.ts +++ b/import-fidelity.ts @@ -67,7 +67,7 @@ export class AmbiguousCitationError extends Error { /** Match complete, repository-qualified prose references, never bare numbers or URL substrings. */ function citesIssue(item: PmItem, repo: string, number: number): boolean { const text = [item.title, item.description, item.body].join("\n"); - const references = text.matchAll(/(? (match[1] ?? match[2]!).toLowerCase() === repo && Number(match[3]) === number); } diff --git a/test/cited-issue-adoption.test.ts b/test/cited-issue-adoption.test.ts index e5f317c3..8da6de04 100644 --- a/test/cited-issue-adoption.test.ts +++ b/test/cited-issue-adoption.test.ts @@ -110,7 +110,8 @@ for (const atomic of [false, true]) { test("citation matching accepts exact qualified references across prose fields and trailing punctuation", () => { for (const field of ["title", "description", "body"]) { - for (const citation of [issue.html_url, `${issue.html_url}.`, `(${issue.html_url})`, `${repo}#30`, `ACME/Widgets#30;`]) { + for (const citation of [issue.html_url, `${issue.html_url}.`, `(${issue.html_url})`, `${issue.html_url}?`, + `Issue:${issue.html_url}`, `${repo}#30`, `Reference:${repo}#30`, `${repo}#30?`, `ACME/Widgets#30;`]) { const local = { id: "local", [field]: `Fixed ${citation}` }; assert.equal(matchImportedIssue([local], issue).item, local, `${field}: ${citation}`); } From 4220a37e7d2d67ae02ff110a907f273dddd8e2f4 Mon Sep 17 00:00:00 2001 From: SteveBot <1153461+unbraind@users.noreply.github.com> Date: Fri, 23 Oct 2026 10:52:03 +0200 Subject: [PATCH 04/18] Keep ownership parity checks scoped to citation opt-out --- .agents/pm/features/pm-github-cited-issue-adoption.toon | 9 ++++++--- .agents/pm/history/pm-github-cited-issue-adoption.jsonl | 5 +++++ test/import-regex.test.ts | 9 ++++++--- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/.agents/pm/features/pm-github-cited-issue-adoption.toon b/.agents/pm/features/pm-github-cited-issue-adoption.toon index c8d6259a..b47deab7 100644 --- a/.agents/pm/features/pm-github-cited-issue-adoption.toon +++ b/.agents/pm/features/pm-github-cited-issue-adoption.toon @@ -6,10 +6,10 @@ status: in_progress priority: 2 tags[3]: changelog-unreleased,github,import created_at: "2026-10-23T07:16:54.041Z" -updated_at: "2026-10-23T08:29:21.353Z" +updated_at: "2026-10-23T08:51:29.270Z" author: codex-agent acceptance_criteria: "Exact issue URL or owner/repo#N in title, description or body adopts one untagged item, including closed and canceled items. Bare numbers and other repositories never match; existing structured identity takes precedence. Multiple candidates return typed ambiguous_citation entries listing IDs and a tagging remedy with no create. Gates verify one landing target; receipts count adopts; dry runs show adopts. Import and scheduled sync adopt by default with --no-adopt-cited for reference-only prose. Behavioral revert tests and no-git tracker tests pass. Validate current pm-review main issue 30 and another public repository. Targeted tests, typecheck, lint and npm/Bun CI release gates pass." -comments[11]{created_at,author,text}: +comments[13]{created_at,author,text}: "2026-10-23T07:17:26.861Z",codex-agent,"Overlap review: pm-github-c093 owns structured URL/provider back-link import matching, pm-github-identity-collisions diagnoses and repairs structured ownership, Projects import matches board and issue provenance, and export upserts linked issues. None adopts arbitrary exact prose citations. This feature adds a fallback only after existing import matching, preserving those paths. Scheduled gated sync already invokes import, so it inherits the default. Plan ambiguous citations before writes and refuse the batch with actionable candidate IDs." "2026-10-23T07:24:01.857Z",codex-agent,"Implementation: strong ownership matching stays first; exact qualified title/description/body citations fall back to a single existing item. All imports now prepare identities before writes, and ambiguous citations expose candidate IDs plus an exact tagging remedy. Adoption retains local tags and reports a separate count; provider-owned records retain link-only behavior. New tests use real trackers with a PATH containing node and the pinned pm CLI but no git, plus real git trackers for write-gate acceptance." "2026-10-23T07:27:45.693Z",codex-agent,"New citation suite: 13/13 passed, including plain/atomic adoption in non-git trackers, write-gate acceptance, retry identity, ambiguity refusal, punctuation, repo/number boundaries, structured ownership precedence and provider link-only preservation. Live pm-review main aa16d855df1f7b74afd3453617f77e92c44d493e already tags pm-review-withdrawn-lineage for issue 30, so an untouched current-main copy correctly plans update rather than adopt. Preserve this current-state result and locate the pre-tag historical tracker for a separate adoption replay. SDK reads may create runtime caches; verify original tracked file bytes independently." @@ -21,7 +21,9 @@ comments[11]{created_at,author,text}: "2026-10-23T07:49:35.957Z",codex-agent,"Focused write-boundary regression passed: node --test --test-name-pattern=\"ambiguous later issue\" test/import-fidelity.test.ts — 3 tests pass, zero failures/skips (plain, gated, atomic). This updates the old partial-write expectation to the fail-closed preflight contract. The initial CI run still tests the prior assertion; the next pushed head will contain this test-only correction and tracker receipt." "2026-10-23T08:27:20.623Z",codex-agent,"Final exact-citation audit reproduced an additional boundary miss: Issue: followed immediately by the issue URL, Reference:owner/repo#N, and a sentence-ending question mark returned new instead of citation. These are qualified prose citations under the acceptance criteria. Adjusted only the regex punctuation boundaries; retained negative URL query/path and longer-number cases. Added the forms to the existing pure matching regression, so test counts stay unchanged. The final source head will receive a fresh full npm/Bun CI run." "2026-10-23T08:29:20.672Z",codex-agent,"Punctuation boundary verification: typecheck, test typecheck and targeted lint passed. Four pure matching checks passed with the fix. Isolated behavioral revert restored only citesIssue to the preceding implementation: 1 of 4 checks failed (3 passed), proving the expanded boundary cases detect this correction. The general adoption revert remains 10 failures / 4 passes when only matchImportedIssue is restored to base main. All mutations remain covered by the real-tracker suite and the fresh full CI run." -files[9]{path,scope}: + "2026-10-23T08:50:08.005Z",codex-agent,"CI identified one remaining legacy matcher parity assertion: GitHub issue: immediately followed by a URL was expected to remain new. The declared-ownership parser remains unchanged, but the new exact-citation fallback correctly adopts that form. Scoped both parity-table invocations to explicit adoption opt-out, preserving the original ownership grammar contract; default colon-citation behavior remains asserted in the new adoption suite. This is a test-only correction. The failed job passed 1034/1035 tests and reported four 100 percent coverage dimensions over all 18 authored modules." + "2026-10-23T08:51:27.741Z",codex-agent,"Focused verification passed after the parity correction: node --test test/import-regex.test.ts test/cited-issue-adoption.test.ts — 21/21 pass, zero failures/skips. The full regex file was rerun with an explicit default-citation assertion for GitHub issue:URL (citation, linkOnly=false): 7/7 pass. Test typecheck and targeted lint passed. Reviewed every matchImportedIssue test call; strong ownership tests now explicitly opt out only where they assert the legacy grammar. Production source remains unchanged from bbe5718." +files[10]{path,scope}: import-fidelity.ts,project index.ts,project test/atomic.test.ts,project @@ -30,6 +32,7 @@ files[9]{path,scope}: test/import-errors.test.ts,project test/import-fidelity.test.ts,project test/import-gate.test.ts,project + test/import-regex.test.ts,project test/import-selection.test.ts,project tests[1]{command,scope,timeout_seconds,provenance{author,created_at,source_kind,source_ref}}: node --test test/cited-issue-adoption.test.ts,project,900,codex-agent,"2026-10-23T07:35:49.405Z",local_mutation,feat/cited-issue-adoption diff --git a/.agents/pm/history/pm-github-cited-issue-adoption.jsonl b/.agents/pm/history/pm-github-cited-issue-adoption.jsonl index ea03c550..32b1792a 100644 --- a/.agents/pm/history/pm-github-cited-issue-adoption.jsonl +++ b/.agents/pm/history/pm-github-cited-issue-adoption.jsonl @@ -22,3 +22,8 @@ {"hash_algorithm":"sha256","ts":"2026-10-23T08:27:20.623Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/9","value":{"created_at":"2026-10-23T08:27:20.623Z","author":"codex-agent","text":"Final exact-citation audit reproduced an additional boundary miss: Issue: followed immediately by the issue URL, Reference:owner/repo#N, and a sentence-ending question mark returned new instead of citation. These are qualified prose citations under the acceptance criteria. Adjusted only the regex punctuation boundaries; retained negative URL query/path and longer-number cases. Added the forms to the existing pure matching regression, so test counts stay unchanged. The final source head will receive a fresh full npm/Bun CI run."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T08:27:20.623Z"}],"before_hash":"914e1a48e2b48a8ec1550f7c50dc36690b9250cbc64ed8722522e5404954a82a","after_hash":"1ecae54642b46d66db19d949e640a107782a882c8abcb7d54db0e57b2b2a0cb8","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"9e5130fbe1923b79ec5ad5f429aa01d46f62ca2271f0904a6e92af7158f8792c"} {"hash_algorithm":"sha256","ts":"2026-10-23T08:29:20.672Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/10","value":{"created_at":"2026-10-23T08:29:20.672Z","author":"codex-agent","text":"Punctuation boundary verification: typecheck, test typecheck and targeted lint passed. Four pure matching checks passed with the fix. Isolated behavioral revert restored only citesIssue to the preceding implementation: 1 of 4 checks failed (3 passed), proving the expanded boundary cases detect this correction. The general adoption revert remains 10 failures / 4 passes when only matchImportedIssue is restored to base main. All mutations remain covered by the real-tracker suite and the fresh full CI run."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T08:29:20.672Z"}],"before_hash":"1ecae54642b46d66db19d949e640a107782a882c8abcb7d54db0e57b2b2a0cb8","after_hash":"9872a6735f44706faeead52305b5f7b6e57747a5098b9baf2407984b2f596466","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"b254d1dc60b57343a2c1b3190f044dd743048aec1a4287c6bcfe81e319ce853e"} {"hash_algorithm":"sha256","ts":"2026-10-23T08:29:21.353Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":{"value":"implementer","source":"argv"},"topic":null},"op":"release","patch":[{"op":"remove","path":"/metadata/claim_principal"},{"op":"remove","path":"/metadata/assignee"},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T08:29:21.353Z"}],"before_hash":"9872a6735f44706faeead52305b5f7b6e57747a5098b9baf2407984b2f596466","after_hash":"f5e0cdba977c92d7bdcf2ed6786c791c3c5f833bf1f37bc2f4539be2b2a6a272","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"7fdd2032de1b3e40a5c2305df103cab788d1a112c4b50134ed034411d66ad0dd"} +{"hash_algorithm":"sha256","ts":"2026-10-23T08:50:06.560Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":{"value":"implementer","source":"argv"},"topic":null},"op":"claim","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T08:50:06.560Z"},{"op":"add","path":"/metadata/assignee","value":"codex-agent"},{"op":"add","path":"/metadata/claim_principal","value":"codex-agent"}],"before_hash":"f5e0cdba977c92d7bdcf2ed6786c791c3c5f833bf1f37bc2f4539be2b2a6a272","after_hash":"b280d971e5a2c243c6cfbd36daca1ed4368dca8a0d538e3c862007ecad7e25f0","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"0a6e5020de93604a8ce05006a98eacb6f149dbae7a415ae6867caefb64f0a9eb"} +{"hash_algorithm":"sha256","ts":"2026-10-23T08:50:08.005Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/11","value":{"created_at":"2026-10-23T08:50:08.005Z","author":"codex-agent","text":"CI identified one remaining legacy matcher parity assertion: GitHub issue: immediately followed by a URL was expected to remain new. The declared-ownership parser remains unchanged, but the new exact-citation fallback correctly adopts that form. Scoped both parity-table invocations to explicit adoption opt-out, preserving the original ownership grammar contract; default colon-citation behavior remains asserted in the new adoption suite. This is a test-only correction. The failed job passed 1034/1035 tests and reported four 100 percent coverage dimensions over all 18 authored modules."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T08:50:08.005Z"}],"before_hash":"b280d971e5a2c243c6cfbd36daca1ed4368dca8a0d538e3c862007ecad7e25f0","after_hash":"3c78348493cbdb52f90e6674771ba7b30af153d2335b6850cd58b9af036ebae2","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"521a9275a915c848d7e802c95f9dc1b9387e8f19b3d3e1dd9a0cb2e1fe310ea5"} +{"hash_algorithm":"sha256","ts":"2026-10-23T08:50:08.926Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"files_add","patch":[{"op":"replace","path":"/metadata/files/8/path","value":"test/import-regex.test.ts"},{"op":"add","path":"/metadata/files/9","value":{"path":"test/import-selection.test.ts","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T08:50:08.926Z"}],"before_hash":"3c78348493cbdb52f90e6674771ba7b30af153d2335b6850cd58b9af036ebae2","after_hash":"e5c4b339886c7661109ac0234c08c0065912c93f052b1c5577dbf5830390ae56","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"f705f5b70be11f0b4b9c625aa1f5e585f035583e15fb6283e3c70871df9a81ed"} +{"hash_algorithm":"sha256","ts":"2026-10-23T08:51:27.744Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/12","value":{"created_at":"2026-10-23T08:51:27.741Z","author":"codex-agent","text":"Focused verification passed after the parity correction: node --test test/import-regex.test.ts test/cited-issue-adoption.test.ts — 21/21 pass, zero failures/skips. The full regex file was rerun with an explicit default-citation assertion for GitHub issue:URL (citation, linkOnly=false): 7/7 pass. Test typecheck and targeted lint passed. Reviewed every matchImportedIssue test call; strong ownership tests now explicitly opt out only where they assert the legacy grammar. Production source remains unchanged from bbe5718."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T08:51:27.744Z"}],"before_hash":"e5c4b339886c7661109ac0234c08c0065912c93f052b1c5577dbf5830390ae56","after_hash":"2e4e7f29f50271804d643f2fc47aeea9d54ec73fbc0871e39a80922cd0856338","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"0917a6c46257de0374c148aebc75d8e9335def9071b00ed13ab75693ef000325"} +{"hash_algorithm":"sha256","ts":"2026-10-23T08:51:29.270Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":{"value":"implementer","source":"argv"},"topic":null},"op":"release","patch":[{"op":"remove","path":"/metadata/claim_principal"},{"op":"remove","path":"/metadata/assignee"},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T08:51:29.270Z"}],"before_hash":"2e4e7f29f50271804d643f2fc47aeea9d54ec73fbc0871e39a80922cd0856338","after_hash":"1dcc1b66f70732e65723933c6d46d2c6070686d8c16c1a57ec5c7ae80009c385","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"5a3020f96471a7757fedea1392204d3348e22223a58fe6310bc28735b7fe51fc"} diff --git a/test/import-regex.test.ts b/test/import-regex.test.ts index fe23e9cb..80294883 100644 --- a/test/import-regex.test.ts +++ b/test/import-regex.test.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: colon, no colon, whitespace and missing URL", () => { +test("ownership declaration parity with citation adoption disabled: 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], @@ -24,7 +24,7 @@ test("ownership declaration parity: colon, no colon, whitespace and missing URL" for (const [description, expected] of rows) { const old = /^GitHub issue\s*:?\s+(https?:\/\/[^\s]+)\s*/i.exec(description)?.[1]; assert.equal(old === url, expected, description); - assert.equal(matchImportedIssue([{ id: "owner", description }], issue).via, + assert.equal(matchImportedIssue([{ id: "owner", description }], issue, undefined, false).via, expected ? "github-declaration" : "new", description); } // Enumerate separators on both sides of the optional colon, including all JS whitespace. @@ -32,9 +32,12 @@ test("ownership declaration parity: colon, no colon, whitespace and missing URL" for (const before of whitespace) for (const colon of ["", ":"]) for (const after of whitespace) { const description = `GitHub issue${before}${colon}${after}${url}`; const old = /^GitHub issue\s*:?\s+(https?:\/\/[^\s]+)\s*/i.exec(description)?.[1]; - assert.equal(matchImportedIssue([{ id: "owner", description }], issue).via, + assert.equal(matchImportedIssue([{ id: "owner", description }], issue, undefined, false).via, old === url ? "github-declaration" : "new", JSON.stringify(description)); } + const citing = matchImportedIssue([{ id: "citing", description: `GitHub issue:${url}` }], issue); + assert.equal(citing.via, "citation"); + assert.equal(citing.linkOnly, false); }); test("locator punctuation trim parity table", () => { From cce11c44776386d619385c368c1113ad2be64a39 Mon Sep 17 00:00:00 2001 From: SteveBot <1153461+unbraind@users.noreply.github.com> Date: Fri, 23 Oct 2026 12:24:12 +0200 Subject: [PATCH 05/18] Adopt only single-issue items with explicit fix or tracking intent --- .../pm-github-cited-issue-adoption.toon | 9 +- .../pm-github-cited-issue-adoption.jsonl | 6 ++ README.md | 41 ++++++- docs/cited-issue-adoption-validation.md | 55 ++++++---- import-fidelity.ts | 46 ++++++-- index.ts | 19 +++- test/cited-issue-adoption.test.ts | 100 ++++++++++++++++-- 7 files changed, 229 insertions(+), 47 deletions(-) diff --git a/.agents/pm/features/pm-github-cited-issue-adoption.toon b/.agents/pm/features/pm-github-cited-issue-adoption.toon index b47deab7..2d825322 100644 --- a/.agents/pm/features/pm-github-cited-issue-adoption.toon +++ b/.agents/pm/features/pm-github-cited-issue-adoption.toon @@ -6,10 +6,10 @@ status: in_progress priority: 2 tags[3]: changelog-unreleased,github,import created_at: "2026-10-23T07:16:54.041Z" -updated_at: "2026-10-23T08:51:29.270Z" +updated_at: "2026-10-23T10:18:04.984Z" author: codex-agent -acceptance_criteria: "Exact issue URL or owner/repo#N in title, description or body adopts one untagged item, including closed and canceled items. Bare numbers and other repositories never match; existing structured identity takes precedence. Multiple candidates return typed ambiguous_citation entries listing IDs and a tagging remedy with no create. Gates verify one landing target; receipts count adopts; dry runs show adopts. Import and scheduled sync adopt by default with --no-adopt-cited for reference-only prose. Behavioral revert tests and no-git tracker tests pass. Validate current pm-review main issue 30 and another public repository. Targeted tests, typecheck, lint and npm/Bun CI release gates pass." -comments[13]{created_at,author,text}: +acceptance_criteria: "Preserve established tag/URL/provider identity matching. Adopt only an untagged item with exactly one distinct repository-qualified GitHub issue across title, description and body, plus explicit fix/close/resolve intent, Issue:/Tracks:/GH issue labeling, or the reference in its title. Incidental and multi-issue citations never transfer ownership; return citation_candidates IDs and reasons while importing normally. Keep ambiguity refusal for two eligible intent-bearing items. Closed incidental items must not reopen. Test qualified references and punctuation, positive fix/label/title cases, related-only mentions and multi-issue roadmaps, non-git trackers, typed receipts, idempotency and opt-out. Record behavioral revert proofs. Repeat public dry runs including pre-tag pm-review adoption and pm-context counts. Local targeted tests/typecheck/lint and exact-head npm/Bun CI release gates pass." +comments[16]{created_at,author,text}: "2026-10-23T07:17:26.861Z",codex-agent,"Overlap review: pm-github-c093 owns structured URL/provider back-link import matching, pm-github-identity-collisions diagnoses and repairs structured ownership, Projects import matches board and issue provenance, and export upserts linked issues. None adopts arbitrary exact prose citations. This feature adds a fallback only after existing import matching, preserving those paths. Scheduled gated sync already invokes import, so it inherits the default. Plan ambiguous citations before writes and refuse the batch with actionable candidate IDs." "2026-10-23T07:24:01.857Z",codex-agent,"Implementation: strong ownership matching stays first; exact qualified title/description/body citations fall back to a single existing item. All imports now prepare identities before writes, and ambiguous citations expose candidate IDs plus an exact tagging remedy. Adoption retains local tags and reports a separate count; provider-owned records retain link-only behavior. New tests use real trackers with a PATH containing node and the pinned pm CLI but no git, plus real git trackers for write-gate acceptance." "2026-10-23T07:27:45.693Z",codex-agent,"New citation suite: 13/13 passed, including plain/atomic adoption in non-git trackers, write-gate acceptance, retry identity, ambiguity refusal, punctuation, repo/number boundaries, structured ownership precedence and provider link-only preservation. Live pm-review main aa16d855df1f7b74afd3453617f77e92c44d493e already tags pm-review-withdrawn-lineage for issue 30, so an untouched current-main copy correctly plans update rather than adopt. Preserve this current-state result and locate the pre-tag historical tracker for a separate adoption replay. SDK reads may create runtime caches; verify original tracked file bytes independently." @@ -23,6 +23,9 @@ comments[13]{created_at,author,text}: "2026-10-23T08:29:20.672Z",codex-agent,"Punctuation boundary verification: typecheck, test typecheck and targeted lint passed. Four pure matching checks passed with the fix. Isolated behavioral revert restored only citesIssue to the preceding implementation: 1 of 4 checks failed (3 passed), proving the expanded boundary cases detect this correction. The general adoption revert remains 10 failures / 4 passes when only matchImportedIssue is restored to base main. All mutations remain covered by the real-tracker suite and the fresh full CI run." "2026-10-23T08:50:08.005Z",codex-agent,"CI identified one remaining legacy matcher parity assertion: GitHub issue: immediately followed by a URL was expected to remain new. The declared-ownership parser remains unchanged, but the new exact-citation fallback correctly adopts that form. Scoped both parity-table invocations to explicit adoption opt-out, preserving the original ownership grammar contract; default colon-citation behavior remains asserted in the new adoption suite. This is a test-only correction. The failed job passed 1034/1035 tests and reported four 100 percent coverage dimensions over all 18 authored modules." "2026-10-23T08:51:27.741Z",codex-agent,"Focused verification passed after the parity correction: node --test test/import-regex.test.ts test/cited-issue-adoption.test.ts — 21/21 pass, zero failures/skips. The full regex file was rerun with an explicit default-citation assertion for GitHub issue:URL (citation, linkOnly=false): 7/7 pass. Test typecheck and targeted lint passed. Reviewed every matchImportedIssue test call; strong ownership tests now explicitly opt out only where they assert the legacy grammar. Production source remains unchanged from bbe5718." + "2026-10-23T09:59:05.116Z",codex-agent,"Review follow-up on PR 169: broad citation matching could overwrite or reopen a related-only closed item, and selecting only one issue could adopt a multi-issue roadmap. Tighten the fallback to one distinct qualified issue across all prose fields and explicit fix/track intent or a title reference. Preserve established ownership matching, report rejected incidental candidates with IDs/reasons, and retain ambiguity refusal only for two eligible owners. Remote PR head was pulled first; merged the one newer main commit containing CodeQL action updates." + "2026-10-23T10:14:59.028Z",codex-agent,"Review regressions: node --test test/cited-issue-adoption.test.ts test/import-regex.test.ts passed 28/28 with zero failures/skips. Both plain and atomic closed-related items remain byte-equivalent in SDK fields while an open GitHub issue imports separately; roadmaps are never adopted when either issue is selected alone or both are selected. Positive closing words/labels/title cases and ambiguity among eligible owners remain covered. Behavioral revert in an isolated source copy restored only citesIssue and matchImportedIssue from pre-review head 4220a37, leaving planner/receipt code and SDK persistence real: the closed-incidental pattern failed 2/2, and the roadmap-only pattern failed 2/2, both exit 1 with no skips. Typecheck, test typecheck, lint and docstrings passed; the additional sorted multi-candidate receipt assertion passed its targeted check." + "2026-10-23T10:18:02.884Z",codex-agent,"Real-data revalidation after the ownership guards: actual pre-tag pm-review f72250e4c5ee8dfafaf57ee8afe20ddd3cbc993e still plans one adopt into pm-review-withdrawn-lineage, zero creates (Fix URL description plus Issue: URL body, one distinct issue). Fresh pm-review main e1fc83a81507ca6a91f1675b74863c050f1a89f1 plans one provenance update. Fresh public pm-context main 7d7479ae77c13d01c54b2b4560ad54091ffed788 plans five issues: two creates, three updates, zero adopts/skips. Original file hashes remain unchanged 106/106, 106/106 and 181/181. The mandatory linked adoption test run passed 21/21. Strict tracker health passed after merging main; no merge maintenance was needed. Updated README examples and the validation document describe eligibility, incidental receipts and both review revert proofs." files[10]{path,scope}: import-fidelity.ts,project index.ts,project diff --git a/.agents/pm/history/pm-github-cited-issue-adoption.jsonl b/.agents/pm/history/pm-github-cited-issue-adoption.jsonl index 32b1792a..bcb40384 100644 --- a/.agents/pm/history/pm-github-cited-issue-adoption.jsonl +++ b/.agents/pm/history/pm-github-cited-issue-adoption.jsonl @@ -27,3 +27,9 @@ {"hash_algorithm":"sha256","ts":"2026-10-23T08:50:08.926Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"files_add","patch":[{"op":"replace","path":"/metadata/files/8/path","value":"test/import-regex.test.ts"},{"op":"add","path":"/metadata/files/9","value":{"path":"test/import-selection.test.ts","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T08:50:08.926Z"}],"before_hash":"3c78348493cbdb52f90e6674771ba7b30af153d2335b6850cd58b9af036ebae2","after_hash":"e5c4b339886c7661109ac0234c08c0065912c93f052b1c5577dbf5830390ae56","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"f705f5b70be11f0b4b9c625aa1f5e585f035583e15fb6283e3c70871df9a81ed"} {"hash_algorithm":"sha256","ts":"2026-10-23T08:51:27.744Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/12","value":{"created_at":"2026-10-23T08:51:27.741Z","author":"codex-agent","text":"Focused verification passed after the parity correction: node --test test/import-regex.test.ts test/cited-issue-adoption.test.ts — 21/21 pass, zero failures/skips. The full regex file was rerun with an explicit default-citation assertion for GitHub issue:URL (citation, linkOnly=false): 7/7 pass. Test typecheck and targeted lint passed. Reviewed every matchImportedIssue test call; strong ownership tests now explicitly opt out only where they assert the legacy grammar. Production source remains unchanged from bbe5718."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T08:51:27.744Z"}],"before_hash":"e5c4b339886c7661109ac0234c08c0065912c93f052b1c5577dbf5830390ae56","after_hash":"2e4e7f29f50271804d643f2fc47aeea9d54ec73fbc0871e39a80922cd0856338","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"0917a6c46257de0374c148aebc75d8e9335def9071b00ed13ab75693ef000325"} {"hash_algorithm":"sha256","ts":"2026-10-23T08:51:29.270Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":{"value":"implementer","source":"argv"},"topic":null},"op":"release","patch":[{"op":"remove","path":"/metadata/claim_principal"},{"op":"remove","path":"/metadata/assignee"},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T08:51:29.270Z"}],"before_hash":"2e4e7f29f50271804d643f2fc47aeea9d54ec73fbc0871e39a80922cd0856338","after_hash":"1dcc1b66f70732e65723933c6d46d2c6070686d8c16c1a57ec5c7ae80009c385","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"5a3020f96471a7757fedea1392204d3348e22223a58fe6310bc28735b7fe51fc"} +{"hash_algorithm":"sha256","ts":"2026-10-23T09:59:03.467Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":{"value":"implementer","source":"argv"},"topic":null},"op":"claim","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T09:59:03.467Z"},{"op":"add","path":"/metadata/assignee","value":"codex-agent"},{"op":"add","path":"/metadata/claim_principal","value":"codex-agent"}],"before_hash":"1dcc1b66f70732e65723933c6d46d2c6070686d8c16c1a57ec5c7ae80009c385","after_hash":"0cc317b38af7e1e168b0a6ffea3d463bb06387d66035e1448b40427d0bebb85a","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"193deb0e0863cd4e839971dc0c3a85de89b7233a3eeb8b392520c1771e7e52de"} +{"hash_algorithm":"sha256","ts":"2026-10-23T09:59:04.514Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","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/acceptance_criteria","value":"Preserve established tag/URL/provider identity matching. Adopt only an untagged item with exactly one distinct repository-qualified GitHub issue across title, description and body, plus explicit fix/close/resolve intent, Issue:/Tracks:/GH issue labeling, or the reference in its title. Incidental and multi-issue citations never transfer ownership; return citation_candidates IDs and reasons while importing normally. Keep ambiguity refusal for two eligible intent-bearing items. Closed incidental items must not reopen. Test qualified references and punctuation, positive fix/label/title cases, related-only mentions and multi-issue roadmaps, non-git trackers, typed receipts, idempotency and opt-out. Record behavioral revert proofs. Repeat public dry runs including pre-tag pm-review adoption and pm-context counts. Local targeted tests/typecheck/lint and exact-head npm/Bun CI release gates pass."},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T09:59:04.514Z"}],"before_hash":"0cc317b38af7e1e168b0a6ffea3d463bb06387d66035e1448b40427d0bebb85a","after_hash":"33879d2aa142ba89f4c44f00ecd6287a1a9081ef68bb1646fa7aaf2e9685877b","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"b2eec38a7b681529a18e24739b049894073fba9cba9c018664be9566beb08854"} +{"hash_algorithm":"sha256","ts":"2026-10-23T09:59:05.116Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/13","value":{"created_at":"2026-10-23T09:59:05.116Z","author":"codex-agent","text":"Review follow-up on PR 169: broad citation matching could overwrite or reopen a related-only closed item, and selecting only one issue could adopt a multi-issue roadmap. Tighten the fallback to one distinct qualified issue across all prose fields and explicit fix/track intent or a title reference. Preserve established ownership matching, report rejected incidental candidates with IDs/reasons, and retain ambiguity refusal only for two eligible owners. Remote PR head was pulled first; merged the one newer main commit containing CodeQL action updates."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T09:59:05.116Z"}],"before_hash":"33879d2aa142ba89f4c44f00ecd6287a1a9081ef68bb1646fa7aaf2e9685877b","after_hash":"38bd385ef3d071df2901d1d2f58a84461bfd5ea19d7ef6c8195d664b17ac6016","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"69d4efa1ba4049725c166f7321312e174468c94844ab89685fae640844d9b8dc"} +{"hash_algorithm":"sha256","ts":"2026-10-23T10:14:59.028Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/14","value":{"created_at":"2026-10-23T10:14:59.028Z","author":"codex-agent","text":"Review regressions: node --test test/cited-issue-adoption.test.ts test/import-regex.test.ts passed 28/28 with zero failures/skips. Both plain and atomic closed-related items remain byte-equivalent in SDK fields while an open GitHub issue imports separately; roadmaps are never adopted when either issue is selected alone or both are selected. Positive closing words/labels/title cases and ambiguity among eligible owners remain covered. Behavioral revert in an isolated source copy restored only citesIssue and matchImportedIssue from pre-review head 4220a37, leaving planner/receipt code and SDK persistence real: the closed-incidental pattern failed 2/2, and the roadmap-only pattern failed 2/2, both exit 1 with no skips. Typecheck, test typecheck, lint and docstrings passed; the additional sorted multi-candidate receipt assertion passed its targeted check."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T10:14:59.028Z"}],"before_hash":"38bd385ef3d071df2901d1d2f58a84461bfd5ea19d7ef6c8195d664b17ac6016","after_hash":"5ae5384a797a9ae57ab4f27ab46093c81490e830a87cfb156608b1a4e516c7fe","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"ff85f7331da128686dea811679ae4071a218c85901ecef4c08f7ead3ee581d78"} +{"hash_algorithm":"sha256","ts":"2026-10-23T10:18:02.894Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/15","value":{"created_at":"2026-10-23T10:18:02.884Z","author":"codex-agent","text":"Real-data revalidation after the ownership guards: actual pre-tag pm-review f72250e4c5ee8dfafaf57ee8afe20ddd3cbc993e still plans one adopt into pm-review-withdrawn-lineage, zero creates (Fix URL description plus Issue: URL body, one distinct issue). Fresh pm-review main e1fc83a81507ca6a91f1675b74863c050f1a89f1 plans one provenance update. Fresh public pm-context main 7d7479ae77c13d01c54b2b4560ad54091ffed788 plans five issues: two creates, three updates, zero adopts/skips. Original file hashes remain unchanged 106/106, 106/106 and 181/181. The mandatory linked adoption test run passed 21/21. Strict tracker health passed after merging main; no merge maintenance was needed. Updated README examples and the validation document describe eligibility, incidental receipts and both review revert proofs."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T10:18:02.894Z"}],"before_hash":"5ae5384a797a9ae57ab4f27ab46093c81490e830a87cfb156608b1a4e516c7fe","after_hash":"f8fec69399b531ac78db7f9ab129b358fbec4bd929f7d8b49942c36fca344683","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"c80ada0821edebedc882d18cb198facd76082771d1bf49cc7e749c0776ddb315"} +{"hash_algorithm":"sha256","ts":"2026-10-23T10:18:04.984Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":{"value":"implementer","source":"argv"},"topic":null},"op":"release","patch":[{"op":"remove","path":"/metadata/claim_principal"},{"op":"remove","path":"/metadata/assignee"},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T10:18:04.984Z"}],"before_hash":"f8fec69399b531ac78db7f9ab129b358fbec4bd929f7d8b49942c36fca344683","after_hash":"9e7fd744feb1ff16980adb1478dd3d1a6a9d223c181ce3d4215050b78db8ec5e","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"d4af50651e3b96bd5c5a6ea69e8fb8d20b005200d6e53576bb86663bf87e69df"} diff --git a/README.md b/README.md index 5e9eb959..07278cb0 100644 --- a/README.md +++ b/README.md @@ -361,7 +361,7 @@ 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. | -| `--no-adopt-cited` | boolean | Disable default citation adoption when prose references describe related work rather than the item's own issue. Existing provenance/URL matching still applies. | +| `--no-adopt-cited` | boolean | Require explicit provenance even for eligible intent-bearing prose, such as copied titles or fix phrases. Existing provenance/URL matching still applies. | | `--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. | | `--dry-run` | boolean | Preview without writing | @@ -377,10 +377,39 @@ tag/URL/provider ownership matching takes precedence. Items with an established `gh:` issue tag are excluded from this fallback. Closed and canceled items remain eligible because their fix may already have landed. -One candidate produces an `adopt` plan entry: the existing ID receives the +Adoption requires **both** of these ownership signals: + +- The combined title, description and body cite exactly one distinct GitHub + issue, counting qualified references to every repository. Repeated URLs and + shorthand for the same issue count once. A second issue disqualifies the item + even when only one issue is selected for import. +- The target reference appears in the title, or directly follows explicit intent: + `fix`/`fixes`/`fixed`, `close`/`closes`/`closed`, + `resolve`/`resolves`/`resolved`, `Issue:`, `Tracks:` or `GH issue`. + Keywords and labels ignore case; punctuation and opening Markdown delimiters + around the reference are accepted. The closing words follow + [GitHub's supported keyword list](https://docs.github.com/en/issues/tracking-your-work-with-issues/using-issues/linking-a-pull-request-to-an-issue). + +| Existing item prose | Decision for issue 30 | +| --- | --- | +| Body: `Fixes acme/widgets#30` | Eligible for adoption | +| Body: `Issue: https://github.com/acme/widgets/issues/30` | Eligible for adoption | +| Title: `Handle acme/widgets#30` | Eligible if no other issue is cited | +| Body: `Related: acme/widgets#30` | Incidental; `missing_intent` | +| Title: `Roadmap acme/widgets#30 and acme/widgets#31` | Incidental for either issue; `multiple_issues` | +| Description: `Fixes acme/widgets#30`; body: `See other/repo#8` | Incidental; `multiple_issues` | + +Incidental mentions never transfer ownership. The issue imports normally into +its own item, leaving the incidental item's status, content and fields intact, +including a closed item's completion state. Plans include +`citation_candidates: [{id, reason}]`; aggregate dry-run and write receipts +include `citation_candidates: [{issue, id, reason}]`. These hints let an agent +explicitly tag the correct item when the prose does not establish ownership. + +One eligible candidate produces an `adopt` plan entry: the existing ID receives the provenance tag and `github_*` fields through the normal upsert, retaining local tags and any established provider ownership. Later imports use the structured -identity. Two or more candidates produce `ambiguous_citation`, candidate IDs and +identity. Two or more eligible candidates produce `ambiguous_citation`, candidate IDs and a remedy: tag the correct item with `gh:owner/repo#N`, then retry. The entire batch refuses writes until resolved; dry-run returns `blocked: true` and the refusal entries with zero planned write counts. @@ -389,8 +418,10 @@ Dry runs show `adopt` entries and `wouldAdopt`; write receipts report `adopted` separately from `imported` and `updated`. Gated idempotency counts an adopt as one existing landing item with its provenance tag, never a new entry. This is on by default for manual imports and scheduled gated syncs, whose import step uses the -same planner. Use `--no-adopt-cited` when citations are reference-only and a -separate imported item is intentional. The legacy import alias shares this flag. +same planner. Use `--no-adopt-cited` to require explicit provenance even for +intent-bearing prose, for example when titles or fix phrases are copied from +other work. Incidental mentions are already excluded by default. The legacy +import alias shares this flag. Status push (`github sync`), export and Projects matching retain their existing scope. Citation adoption also works in a non-git tracker; actual `--gate` writes still require a Git worktree for the proposed-change privacy scan. diff --git a/docs/cited-issue-adoption-validation.md b/docs/cited-issue-adoption-validation.md index 4ed8ea8a..f5b2403c 100644 --- a/docs/cited-issue-adoption-validation.md +++ b/docs/cited-issue-adoption-validation.md @@ -2,11 +2,16 @@ The importer first resolves established provenance tags, structured URLs and provider back-links. Only an issue without an established owner reaches exact -title/description/body citation matching. One candidate is adopted; multiple -candidates refuse the batch before any writes and report `ambiguous_citation` +title/description/body citation matching. Only a single-issue item with explicit +fix/track intent or a title reference is eligible. One eligible candidate is +adopted; multiple eligible candidates refuse the batch before any writes and report `ambiguous_citation` with candidate IDs and a tagging remedy. Status push, export and Projects matching retain their existing scope. +Incidental references carry `citation_candidates` hints with item IDs and +`missing_intent` or `multiple_issues` reasons. They never hand status or fields +to the importer; the GitHub issue imports normally into its own item. + ## Public tracker dry runs Validation used GitHub API tracker archives copied into plain directories with @@ -18,9 +23,9 @@ rewritten for the replay. | Repository and tracker revision | Issues fetched/planned | Create | Update | Adopt | Skip | Original files unchanged | | --- | ---: | ---: | ---: | ---: | ---: | ---: | -| pm-review current main `aa16d855df1f7b74afd3453617f77e92c44d493e` | 1 / 1 | 0 | 1 | 0 | 0 | 106 / 106 | +| pm-review current main `e1fc83a81507ca6a91f1675b74863c050f1a89f1` | 1 / 1 | 0 | 1 | 0 | 0 | 106 / 106 | | pm-review pre-tag history `f72250e4c5ee8dfafaf57ee8afe20ddd3cbc993e` | 1 / 1 | 0 | 0 | 1 | 0 | 106 / 106 | -| pm-context current main `8d9576778a3609f156e9444ed659ca7c2e2aa9e7` | 5 / 5 | 2 | 3 | 0 | 0 | 181 / 181 | +| pm-context current main `7d7479ae77c13d01c54b2b4560ad54091ffed788` | 5 / 5 | 2 | 3 | 0 | 0 | 181 / 181 | The requested pm-review current-main check now correctly plans an **update**: [PR 33](https://github.com/unbraind/pm-review/pull/33) already added the issue's @@ -29,6 +34,10 @@ untouched snapshot would contradict structured-identity precedence. Replaying the actual [pre-tag tracker](https://github.com/unbraind/pm-review/tree/f72250e4c5ee8dfafaf57ee8afe20ddd3cbc993e/.agents/pm) produces this adoption entry: +The pre-tag item's description says `Fix` followed by the issue URL and its body +says `Issue:` followed by the same URL. Those are two references to one distinct +issue with explicit intent, so the tightened ownership rule still adopts it. + ```json { "issue": 30, @@ -67,22 +76,32 @@ Node and the pinned pm CLI. Gate write cases use disposable Git repositories. Assertions cover single-candidate adoption, terminal candidates, no duplicate on retry, ambiguity before any batch write, URL punctuation, repository and issue-number boundaries, colon-prefixed labels and sentence-ending question marks, -structured ownership precedence, provider link-only -behavior, separate adoption counts, opt-out, and conflicting batch targets. +structured ownership precedence, provider link-only behavior, separate adoption +counts and opt-out. Review regressions also prove these ownership boundaries: + +| Finding | Fixed behavior and persistence evidence | +| --- | --- | +| A closed item says only `Related: acme/widgets#30` while issue 30 is open | Import creates the issue item and reports `missing_intent`; the original item's entire SDK projection, including status/body/closure fields, is unchanged | +| A roadmap cites issues 30 and 31 | Selecting either issue alone creates its own item and reports `multiple_issues`; the roadmap stays open and unchanged | +| References span repositories or fields | All distinct identities count, including title references and URL query/fragment forms; repeated URL/shorthand for the same identity counts once | +| Intent-bearing single-issue owners compete | Two eligible owners still refuse writes; incidental hints are reported separately and never create ambiguity | `test/import-selection.test.ts` installs the built package into a disposable consumer and exercises the actual CLI dispatch, including the advertised `--no-adopt-cited` flag and its create-versus-adopt behavior. -Behavioral revert proof copied the current source and test into an isolated -scratch package, then restored **only** `matchImportedIssue` from base main. -`node --test test/cited-issue-adoption.test.ts` exited 1 with 10 failures and -4 passes, versus 14 passes with the fix; neither run skipped tests. The fixed -worktree was untouched. Tests import live TypeScript, so no compiled artifact -could mask the revert. - -A focused punctuation revert restored only `citesIssue` to the preceding -implementation while retaining adoption and the expanded regression cases. -The four pure matching checks then produced 1 failure and 3 passes; the fixed -helper passes all four. This specifically proves colon-prefixed labels and -sentence-ending question marks are accepted without admitting URL queries. +Review revert proofs used an isolated source copy and restored only +`citesIssue` and `matchImportedIssue` from pre-review head `4220a37`, leaving the +new planner/receipt code and real SDK persistence unchanged. Neither proof +changed the package worktree or rebuilt a stale artifact: tests import live TS. + +| Exact test selection | Reverted result | Fixed result | +| --- | --- | --- | +| `node --test --test-name-pattern='closed incidental item' test/cited-issue-adoption.test.ts` | 2 failures, 0 passes, exit 1 | Both plain/atomic cases pass | +| `node --test --test-name-pattern='roadmap never adopts' test/cited-issue-adoption.test.ts` | 2 failures, 0 passes, exit 1 | Both plain/atomic cases pass | + +`node --test test/cited-issue-adoption.test.ts test/import-regex.test.ts` passes +28 tests with zero failures/skips. The PM-linked adoption command also passes +21 tests. Typecheck, test typecheck, build, lint, docstrings and strict tracker +health pass locally. Full npm/Bun release-gate evidence is recorded in PR 169 +and the final handoff at the exact pushed head; thresholds remain unchanged. diff --git a/import-fidelity.ts b/import-fidelity.ts index 6674de37..d7cc55e2 100644 --- a/import-fidelity.ts +++ b/import-fidelity.ts @@ -42,7 +42,13 @@ export interface ImportFidelity { github_release: string; } /** Identity decision recorded before any import writes. */ -export interface ImportMatch { item?: PmItem; linkOnly: boolean; via: string } +export interface ImportMatch { item?: PmItem; linkOnly: boolean; via: string; citation_candidates?: CitationCandidate[] } + +/** Incidental references are review hints, never permission to transfer an item's ownership. */ +export interface CitationCandidate { + id: string; + reason: "multiple_issues" | "missing_intent"; +} /** A citation cannot pick an owner until the user tags exactly one candidate. */ export interface AmbiguousCitationPlan { @@ -50,25 +56,37 @@ export interface AmbiguousCitationPlan { issue: number; candidates: string[]; remedy: string; + citation_candidates?: CitationCandidate[]; } /** Carry a typed refusal from identity resolution into the batch's reviewable plan. */ export class AmbiguousCitationError extends Error { readonly entry: AmbiguousCitationPlan; /** Retain candidate IDs and the exact provenance tag needed to resolve the conflict. */ - constructor(repo: string, number: number, candidates: string[]) { + 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.` }; super(JSON.stringify(entry)); this.entry = entry; } } -/** Match complete, repository-qualified prose references, never bare numbers or URL substrings. */ -function citesIssue(item: PmItem, repo: string, number: number): boolean { - const text = [item.title, item.description, item.body].join("\n"); - const references = text.matchAll(/(? (match[1] ?? match[2]!).toLowerCase() === repo && Number(match[3]) === number); +/** Require one distinct issue across all prose fields and a title reference or explicit fix/track intent. */ +function citesIssue(item: PmItem, repo: string, number: number): "eligible" | CitationCandidate["reason"] | undefined { + const target = `${repo}#${number}`; + const identities = new Set(); + let intended = false; + for (const [field, text] of [item.title ?? "", item.description ?? "", item.body ?? ""].entries()) { + const references = text.matchAll(/(? 1 ? "multiple_issues" : intended ? "eligible" : "missing_intent"; } /** Trim exporter punctuation in one backwards pass. @internal Shared with parity tests. */ @@ -120,17 +138,25 @@ export function matchImportedIssue(items: readonly PmItem[], issue: GhIssue, tag 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.`); let item = candidates[0]; let cited = false; + const incidental: CitationCandidate[] = []; if (!item && adoptCited && !issue.pull_request) { const repo = issue.html_url.split("/").slice(3, 5).join("/").toLowerCase(); - const citing = items.filter(candidate => candidate.id && - !candidate.tags?.some(tag => parseProvenanceTag(tag) !== undefined) && citesIssue(candidate, repo, issue.number)); - if (citing.length > 1) throw new AmbiguousCitationError(repo, issue.number, citing.map(candidate => candidate.id!).sort()); + const citing: PmItem[] = []; + for (const candidate of items) { + if (!candidate.id || candidate.tags?.some(tag => parseProvenanceTag(tag) !== undefined)) continue; + const assessment = citesIssue(candidate, repo, issue.number); + if (assessment === "eligible") citing.push(candidate); + else if (assessment !== undefined) incidental.push({ id: candidate.id, reason: assessment }); + } + incidental.sort((left, right) => left.id.localeCompare(right.id)); + if (citing.length > 1) throw new AmbiguousCitationError(repo, issue.number, citing.map(candidate => candidate.id!).sort(), incidental); item = citing[0]; cited = Boolean(item); } const provider = item && (Boolean(providerProvenance(item)) || (copiedProviderMarkersOwnItem(item) && item.tags?.some(tag => /^(linear|jira|ado):/i.test(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" }; } diff --git a/index.ts b/index.ts index ac46dec9..140f4914 100644 --- a/index.ts +++ b/index.ts @@ -26,7 +26,7 @@ import type { import { CloseoutUsageError, compactCloseoutReport, runCloseout } from "./closeout.ts"; import { AmbiguousCitationError, buildImportFidelity, fetchImportTimeline, importFidelityFields, matchImportedIssue, persistImportFidelity, readImportReleases, selectImportContextIndex, validateImportLifetime, - type AmbiguousCitationPlan, type ImportFidelity, + type AmbiguousCitationPlan, type CitationCandidate, type ImportFidelity, } from "./import-fidelity.ts"; import { refreshChanges, runRefresh } from "./refresh.ts"; import { exploreGithubIdentities, planGithubIdentityRepair, applyGithubIdentityRepair } from "./identity.ts"; @@ -1331,6 +1331,8 @@ export interface PreparedGithubImport { linkOnly?: boolean; /** Auditable identity and lifecycle conflict decision. */ identity?: { via: string; conflict?: { local: string; github: string; rule: string } }; + /** Rejected prose references remain visible for explicit manual provenance tagging. */ + citation_candidates?: CitationCandidate[]; match?: PmItem; } @@ -3163,6 +3165,7 @@ export async function prepareGithubImport( fidelity, linkOnly: identity.linkOnly, identity: { via: identity.via, ...(conflict ? { conflict } : {}) }, + ...(identity.citation_candidates ? { citation_candidates: identity.citation_candidates } : {}), match, }; } @@ -3494,6 +3497,8 @@ export async function runImport( let gateCompleteness: ImportPlanCompletenessReceipts | undefined; let gateIdempotency: ImportIdempotencyReceipts | undefined; let gatePlanByNumber: Map | undefined; + const citationCandidates: (CitationCandidate & { issue: number })[] = []; + let citationReceipt: { citation_candidates?: typeof citationCandidates } = {}; { const planned: PreparedGithubImport[] = []; const skippedNumbers: number[] = []; @@ -3513,6 +3518,7 @@ export async function runImport( } catch (error) { if (!(error instanceof AmbiguousCitationError)) throw error; ambiguous.push(error.entry); + for (const candidate of error.entry.citation_candidates ?? []) citationCandidates.push({ issue: issue.number, ...candidate }); continue; } if (!entry) { @@ -3520,9 +3526,11 @@ export async function runImport( continue; } planned.push(entry); + for (const candidate of entry.citation_candidates ?? []) citationCandidates.push({ issue: issue.number, ...candidate }); } + if (citationCandidates.length > 0) citationReceipt = { citation_candidates: citationCandidates }; if (ambiguous.length > 0) { - const refusal = { code: "ambiguous_citation", blocked: true, plan: ambiguous }; + const refusal = { code: "ambiguous_citation", blocked: true, plan: ambiguous, ...citationReceipt }; if (!opts.dryRun) throw new CommandError(JSON.stringify(refusal)); for (const entry of ambiguous) console.error(`[dry-run] #${entry.issue} ambiguous_citation: ${entry.candidates.join(", ")}. ${entry.remedy}`); return { dryRun: true, wouldImport: 0, wouldUpdate: 0, wouldAdopt: 0, wouldSkip: skippedNumbers.length, @@ -3566,10 +3574,11 @@ export async function runImport( 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, - identity: entry.identity }; + 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 ? { plan: fidelityPlan() } : {}; + const planFields = { ...(opts.gate || adoptionCount > 0 || citationCandidates.length > 0 ? { plan: fidelityPlan() } : {}), ...citationReceipt }; const gatePlanFields = { ...planFields, ...(opts.gate ? { gate: { completeness: gateCompleteness, idempotency: gateIdempotency, scan: "post-write" } } : {}) }; if (opts.atomic) { @@ -6619,7 +6628,7 @@ export function isMutatingGithubCommand(command: string, options: Record tracker(async pmRoot => { const first = (await create({ type: "Task", title: `Fix ${repo}#30` }, { pmRoot })).item; - const second = (await create({ type: "Task", title: "Other fix", body: `${issue.html_url})` }, { pmRoot })).item; + const second = (await create({ type: "Task", title: "Other fix", body: `Issue: (${issue.html_url})` }, { pmRoot })).item; const before = await items(pmRoot); const batch = [{ ...issue, number: 31, html_url: issue.html_url.replace("/30", "/31") }, issue]; const preview = await importing(pmRoot, { atomic, gate: true, "dry-run": true }, batch); @@ -106,12 +106,50 @@ for (const atomic of [false, true]) { assert.equal((await items(pmRoot))[0]!.id, existing.id); assert.equal((await items(pmRoot)).length, 1); }, true)); + + test(`${atomic ? "atomic" : "plain"} closed incidental item is reported but never overwritten or reopened`, async () => tracker(async pmRoot => { + const local = (await create({ type: "Task", title: "Completed related work", description: `Related: ${repo}#30`, + body: "Retain this completed work and its context", status: "closed", closeReason: "Landed independently", tags: "local" }, { pmRoot })).item; + const before = (await get(local.id, {}, { pmRoot })).item; + const opened = { ...issue, state: "open" as const, state_reason: "reopened" as const, closed_at: null }; + const preview = (await importing(pmRoot, { atomic, "dry-run": true }, [opened])).result; + assert.equal(preview.wouldAdopt, 0); + assert.equal(preview.wouldImport, 1); + assert.deepEqual(preview.citation_candidates, [{ issue: 30, id: local.id, reason: "missing_intent" }]); + const plan = (preview.plan as { action: string; citation_candidates: unknown }[])[0]!; + assert.equal(plan.action, "create"); + assert.deepEqual(plan.citation_candidates, [{ id: local.id, reason: "missing_intent" }]); + const result = (await importing(pmRoot, { atomic }, [opened])).result; + assert.equal(result.imported, 1); + assert.equal(result.adopted, 0); + assert.deepEqual(result.citation_candidates, preview.citation_candidates); + assert.deepEqual((await get(local.id, {}, { pmRoot })).item, before); + assert.equal((await items(pmRoot)).length, 2); + })); + + test(`${atomic ? "atomic" : "plain"} roadmap never adopts when either issue is imported alone`, async () => tracker(async pmRoot => { + const local = (await create({ type: "Task", title: `Roadmap: ${repo}#30 and ${repo}#31`, + body: "Independent roadmap context", status: "open" }, { pmRoot })).item; + const before = (await get(local.id, {}, { pmRoot })).item; + for (const number of [30, 31]) { + const source = { ...issue, number, html_url: `https://github.com/${repo}/issues/${number}` }; + const preview = (await importing(pmRoot, { atomic, gate: true, "dry-run": true }, [source])).result; + assert.equal(preview.wouldAdopt, 0); + assert.equal(preview.wouldImport, 1); + assert.deepEqual(preview.citation_candidates, [{ issue: number, id: local.id, reason: "multiple_issues" }]); + const landed = (await importing(pmRoot, { atomic }, [source])).result; + assert.equal(landed.adopted, 0); + assert.equal(landed.imported, 1); + assert.deepEqual((await get(local.id, {}, { pmRoot })).item, before); + } + assert.equal((await items(pmRoot)).length, 3); + })); } test("citation matching accepts exact qualified references across prose fields and trailing punctuation", () => { for (const field of ["title", "description", "body"]) { for (const citation of [issue.html_url, `${issue.html_url}.`, `(${issue.html_url})`, `${issue.html_url}?`, - `Issue:${issue.html_url}`, `${repo}#30`, `Reference:${repo}#30`, `${repo}#30?`, `ACME/Widgets#30;`]) { + `Issue:${issue.html_url}`, `${repo}#30`, `Tracks:${repo}#30`, `${repo}#30?`, `ACME/Widgets#30;`]) { const local = { id: "local", [field]: `Fixed ${citation}` }; assert.equal(matchImportedIssue([local], issue).item, local, `${field}: ${citation}`); } @@ -131,7 +169,7 @@ test("bare numbers, other repos, longer identities, URL paths and different issu }); test("tagged and URL-linked owners take precedence over even multiple prose citations", () => { - const citing = ["first", "second"].map(id => ({ id, body: issue.html_url })); + const citing = ["first", "second"].map(id => ({ id, body: `Fixes ${issue.html_url}` })); const tagged = { id: "tagged", tags: [`gh:${repo}#30`] }; assert.equal(matchImportedIssue([...citing, tagged], issue, tagged).item, tagged); const linked = { id: "linked", github_url: issue.html_url }; @@ -158,9 +196,59 @@ test("provider-owned citation adoption links without replacing local context", a assert.equal(actual.github_url, issue.html_url); })); -test("gate rejects adopting the same local item for two issues", async () => tracker(async pmRoot => { - await create({ type: "Task", title: `${repo}#30 and ${repo}#31` }, { pmRoot }); - for (const gate of [false, true]) await assert.rejects(importing(pmRoot, { gate, "dry-run": true }, [issue, { ...issue, number: 31 }]), /conflicting target/); +test("batch plans both issues normally instead of transferring a roadmap", async () => tracker(async pmRoot => { + const roadmap = (await create({ type: "Task", title: `${repo}#30 and ${repo}#31` }, { pmRoot })).item; + for (const gate of [false, true]) { + const result = (await importing(pmRoot, { gate, "dry-run": true }, [issue, { ...issue, number: 31, html_url: issue.html_url.replace("/30", "/31") }])).result; + assert.equal(result.wouldAdopt, 0); + assert.equal(result.wouldImport, 2); + assert.deepEqual(result.citation_candidates, [30, 31].map(number => ({ issue: number, id: roadmap.id, reason: "multiple_issues" }))); + } +})); + +test("only explicit closing keywords, issue labels or a title reference establish citation intent", () => { + for (const reference of [issue.html_url, `${repo}#30`]) { + for (const prefix of ["Fix", "fixes", "FIXED:", "Close", "Closes", "Closed", "Resolve", "Resolves", "Resolved", "Issue:", "Tracks:", "GH issue", "GH issue:"]) { + for (const field of ["description", "body"]) assert.equal(matchImportedIssue([{ id: "local", [field]: `${prefix} (${reference})` }], issue).via, "citation", `${field}: ${prefix}`); + } + assert.equal(matchImportedIssue([{ id: "local", title: `Work for ${reference}`, body: "Related context" }], issue).via, "citation"); + for (const prefix of ["Related:", "See", "References:", "Fix for", "Tracks", "Tracking:", "PrefixFixes"]) { + const result = matchImportedIssue([{ id: "local", description: `${prefix} ${reference}` }], issue); + assert.equal(result.via, "new", prefix); + assert.deepEqual(result.citation_candidates, [{ id: "local", reason: "missing_intent" }]); + } + } +}); + +test("distinct issue count includes other repositories and all three fields but deduplicates repeated identity", () => { + for (const other of [`${repo}#31`, "Other/widgets#30", "https://github.com/other/widgets/issues/31?view=all#fragment", "http://www.github.com/other/widgets/issues/31"]) { + for (const field of ["title", "description", "body"]) { + const result = matchImportedIssue([{ id: "local", description: `Fixes ${issue.html_url}`, [field]: `Tracks: ${other}`, + body: `Issue: ${repo}#30${field === "body" ? ` and ${other}` : ""}` }], issue); + assert.equal(result.via, "new"); + assert.deepEqual(result.citation_candidates, [{ id: "local", reason: "multiple_issues" }]); + } + } + const local = { id: "local", title: `Fix ${repo}#30`, description: `Issue: ${issue.html_url}`, body: "Tracks: ACME/Widgets#30" }; + assert.equal(matchImportedIssue([local], issue).item, local); + const hints = matchImportedIssue([{ id: "z", body: `Related: ${repo}#30` }, + { id: "a", title: `${repo}#30 and other/widgets#31` }], issue); + assert.deepEqual(hints.citation_candidates, [{ id: "a", reason: "multiple_issues" }, { id: "z", reason: "missing_intent" }]); +}); + +test("incidental hints do not create ambiguity and remain visible alongside eligible owners or refusal", async () => tracker(async pmRoot => { + const related = (await create({ type: "Task", title: "Related work", body: `Related: ${issue.html_url}` }, { pmRoot })).item; + const eligible = (await create({ type: "Task", title: `Fixes ${repo}#30` }, { pmRoot })).item; + const preview = (await importing(pmRoot, { "dry-run": true })).result; + assert.equal(preview.wouldAdopt, 1); + assert.equal((preview.plan as { item: string }[])[0]!.item, eligible.id); + assert.deepEqual(preview.citation_candidates, [{ issue: 30, id: related.id, reason: "missing_intent" }]); + await create({ type: "Task", title: "Another owner", body: `Tracks: ${repo}#30` }, { pmRoot }); + const refused = (await importing(pmRoot, { "dry-run": true })).result; + assert.equal(refused.blocked, true); + assert.deepEqual(refused.citation_candidates, preview.citation_candidates); + assert.deepEqual((refused.plan as { citation_candidates: unknown }[])[0]!.citation_candidates, [{ id: related.id, reason: "missing_intent" }]); + assert.equal(new AmbiguousCitationError(repo, 30, ["a", "b"]).entry.citation_candidates, undefined); })); test("a citation without a local tag array still receives the issue provenance tag", async () => { From ef325ba70fd6d3fc5a47b5fa955cc035a9617958 Mon Sep 17 00:00:00 2001 From: SteveBot <1153461+unbraind@users.noreply.github.com> Date: Fri, 23 Oct 2026 13:15:11 +0200 Subject: [PATCH 06/18] Count all GitHub reference forms before adopting issue items --- .../pm-github-cited-issue-adoption.toon | 9 ++-- .../pm-github-cited-issue-adoption.jsonl | 6 +++ README.md | 13 +++-- docs/cited-issue-adoption-validation.md | 35 ++++++++++++- import-fidelity.ts | 32 +++++++++--- test/cited-issue-adoption.test.ts | 52 +++++++++++++++++++ 6 files changed, 134 insertions(+), 13 deletions(-) diff --git a/.agents/pm/features/pm-github-cited-issue-adoption.toon b/.agents/pm/features/pm-github-cited-issue-adoption.toon index 2d825322..d747f830 100644 --- a/.agents/pm/features/pm-github-cited-issue-adoption.toon +++ b/.agents/pm/features/pm-github-cited-issue-adoption.toon @@ -6,10 +6,10 @@ status: in_progress priority: 2 tags[3]: changelog-unreleased,github,import created_at: "2026-10-23T07:16:54.041Z" -updated_at: "2026-10-23T10:18:04.984Z" +updated_at: "2026-10-23T11:14:35.611Z" author: codex-agent -acceptance_criteria: "Preserve established tag/URL/provider identity matching. Adopt only an untagged item with exactly one distinct repository-qualified GitHub issue across title, description and body, plus explicit fix/close/resolve intent, Issue:/Tracks:/GH issue labeling, or the reference in its title. Incidental and multi-issue citations never transfer ownership; return citation_candidates IDs and reasons while importing normally. Keep ambiguity refusal for two eligible intent-bearing items. Closed incidental items must not reopen. Test qualified references and punctuation, positive fix/label/title cases, related-only mentions and multi-issue roadmaps, non-git trackers, typed receipts, idempotency and opt-out. Record behavioral revert proofs. Repeat public dry runs including pre-tag pm-review adoption and pm-context counts. Local targeted tests/typecheck/lint and exact-head npm/Bun CI release gates pass." -comments[16]{created_at,author,text}: +acceptance_criteria: "Preserve established tag/URL/provider identity matching. Adopt only an untagged item with exactly one distinct repository-qualified GitHub issue across title, description and body, plus explicit fix/close/resolve intent, Issue:/Tracks:/GH issue labeling, or the reference in its title. Incidental and multi-issue citations never transfer ownership; return citation_candidates IDs and reasons while importing normally. Keep ambiguity refusal for two eligible intent-bearing items. Closed incidental items must not reopen. Test qualified references and punctuation, positive fix/label/title cases, related-only mentions and multi-issue roadmaps, non-git trackers, typed receipts, idempotency and opt-out. Record behavioral revert proofs. Repeat public dry runs including pre-tag pm-review adoption and pm-context counts. Local targeted tests/typecheck/lint and exact-head npm/Bun CI release gates pass.; Use one reference parser for ownership intent and distinct-target counting across qualified shorthand, repository-hinted bare numbers, issue/PR URLs with optional trailing slash, queries, fragments and punctuation, with property-style second-reference regressions and a behavioral revert proof" +comments[19]{created_at,author,text}: "2026-10-23T07:17:26.861Z",codex-agent,"Overlap review: pm-github-c093 owns structured URL/provider back-link import matching, pm-github-identity-collisions diagnoses and repairs structured ownership, Projects import matches board and issue provenance, and export upserts linked issues. None adopts arbitrary exact prose citations. This feature adds a fallback only after existing import matching, preserving those paths. Scheduled gated sync already invokes import, so it inherits the default. Plan ambiguous citations before writes and refuse the batch with actionable candidate IDs." "2026-10-23T07:24:01.857Z",codex-agent,"Implementation: strong ownership matching stays first; exact qualified title/description/body citations fall back to a single existing item. All imports now prepare identities before writes, and ambiguous citations expose candidate IDs plus an exact tagging remedy. Adoption retains local tags and reports a separate count; provider-owned records retain link-only behavior. New tests use real trackers with a PATH containing node and the pinned pm CLI but no git, plus real git trackers for write-gate acceptance." "2026-10-23T07:27:45.693Z",codex-agent,"New citation suite: 13/13 passed, including plain/atomic adoption in non-git trackers, write-gate acceptance, retry identity, ambiguity refusal, punctuation, repo/number boundaries, structured ownership precedence and provider link-only preservation. Live pm-review main aa16d855df1f7b74afd3453617f77e92c44d493e already tags pm-review-withdrawn-lineage for issue 30, so an untouched current-main copy correctly plans update rather than adopt. Preserve this current-state result and locate the pre-tag historical tracker for a separate adoption replay. SDK reads may create runtime caches; verify original tracked file bytes independently." @@ -26,6 +26,9 @@ comments[16]{created_at,author,text}: "2026-10-23T09:59:05.116Z",codex-agent,"Review follow-up on PR 169: broad citation matching could overwrite or reopen a related-only closed item, and selecting only one issue could adopt a multi-issue roadmap. Tighten the fallback to one distinct qualified issue across all prose fields and explicit fix/track intent or a title reference. Preserve established ownership matching, report rejected incidental candidates with IDs/reasons, and retain ambiguity refusal only for two eligible owners. Remote PR head was pulled first; merged the one newer main commit containing CodeQL action updates." "2026-10-23T10:14:59.028Z",codex-agent,"Review regressions: node --test test/cited-issue-adoption.test.ts test/import-regex.test.ts passed 28/28 with zero failures/skips. Both plain and atomic closed-related items remain byte-equivalent in SDK fields while an open GitHub issue imports separately; roadmaps are never adopted when either issue is selected alone or both are selected. Positive closing words/labels/title cases and ambiguity among eligible owners remain covered. Behavioral revert in an isolated source copy restored only citesIssue and matchImportedIssue from pre-review head 4220a37, leaving planner/receipt code and SDK persistence real: the closed-incidental pattern failed 2/2, and the roadmap-only pattern failed 2/2, both exit 1 with no skips. Typecheck, test typecheck, lint and docstrings passed; the additional sorted multi-candidate receipt assertion passed its targeted check." "2026-10-23T10:18:02.884Z",codex-agent,"Real-data revalidation after the ownership guards: actual pre-tag pm-review f72250e4c5ee8dfafaf57ee8afe20ddd3cbc993e still plans one adopt into pm-review-withdrawn-lineage, zero creates (Fix URL description plus Issue: URL body, one distinct issue). Fresh pm-review main e1fc83a81507ca6a91f1675b74863c050f1a89f1 plans one provenance update. Fresh public pm-context main 7d7479ae77c13d01c54b2b4560ad54091ffed788 plans five issues: two creates, three updates, zero adopts/skips. Original file hashes remain unchanged 106/106, 106/106 and 181/181. The mandatory linked adoption test run passed 21/21. Strict tracker health passed after merging main; no merge maintenance was needed. Updated README examples and the validation document describe eligibility, incidental receipts and both review revert proofs." + "2026-10-23T11:04:06.852Z",codex-agent,"Second review follow-up on PR 169: the distinct-issue regex omitted a trailing-slash issue URL, allowing a two-reference item to transfer ownership. The remote branch was pulled first and was unchanged. Replace this counting grammar with one parser used for intent and distinct identities, covering qualified shorthand, repository-hinted bare numbers, issue/PR URLs with trailing slash, fragments, query strings and prose punctuation. Add a property-style second-reference matrix and a real non-git persistence regression, then record a behavioral revert and fresh exact-head npm/Bun CI evidence." + "2026-10-23T11:10:14.740Z",codex-agent,"The shared parser now normalizes issue/PR URL identities including trailing slash, query strings, comment fragments and Markdown punctuation. Repository-hinted bare references count toward additional targets but bare numbers and PR links alone cannot establish issue ownership. The property matrix checks 8925 second-reference combinations across all three fields plus 51 positive controls. Plain/atomic real non-git tracker regressions preserve the entire roadmap projection while creating the issue item. Combined adoption/regex suite passed 32/32 before the additional adjacent-Markdown boundary case, which also passes. Behavioral revert restored only citesIssue from cce11c4, retaining the new parser, receipts and real persistence: the property test plus both trailing-slash persistence tests fail (3 failures, 0 passes/skips, exit 1). Fixed cases pass. Typecheck, test typecheck, lint, docstrings and zero-duplication checks passed." + "2026-10-23T11:14:34.439Z",codex-agent,"Merged the newly advanced main toolchain update in 92f595e and installed its existing dependency pins (CLI, pm-ops and pm-changelog 2026.10.20). Reverified against those pins: mandatory linked adoption suite 25/25, regex suite 7/7, typecheck, test typecheck, build, lint, docstrings, zero duplication and strict tracker health pass. The current parser property matrix covers 8925 second-reference combinations plus 51 positive controls. The three-test behavioral revert was repeated against the updated toolchain and still fails 3/3 with zero passes/skips. Scoped coverage inspection found no uncovered branches in the new parser. The updated README and validation document explain the reference grammar and proof. Full unchanged-threshold npm/Bun release evidence will come from CI at the pushed correction head; no merge or release is authorized." files[10]{path,scope}: import-fidelity.ts,project index.ts,project diff --git a/.agents/pm/history/pm-github-cited-issue-adoption.jsonl b/.agents/pm/history/pm-github-cited-issue-adoption.jsonl index bcb40384..b9e4bc72 100644 --- a/.agents/pm/history/pm-github-cited-issue-adoption.jsonl +++ b/.agents/pm/history/pm-github-cited-issue-adoption.jsonl @@ -33,3 +33,9 @@ {"hash_algorithm":"sha256","ts":"2026-10-23T10:14:59.028Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/14","value":{"created_at":"2026-10-23T10:14:59.028Z","author":"codex-agent","text":"Review regressions: node --test test/cited-issue-adoption.test.ts test/import-regex.test.ts passed 28/28 with zero failures/skips. Both plain and atomic closed-related items remain byte-equivalent in SDK fields while an open GitHub issue imports separately; roadmaps are never adopted when either issue is selected alone or both are selected. Positive closing words/labels/title cases and ambiguity among eligible owners remain covered. Behavioral revert in an isolated source copy restored only citesIssue and matchImportedIssue from pre-review head 4220a37, leaving planner/receipt code and SDK persistence real: the closed-incidental pattern failed 2/2, and the roadmap-only pattern failed 2/2, both exit 1 with no skips. Typecheck, test typecheck, lint and docstrings passed; the additional sorted multi-candidate receipt assertion passed its targeted check."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T10:14:59.028Z"}],"before_hash":"38bd385ef3d071df2901d1d2f58a84461bfd5ea19d7ef6c8195d664b17ac6016","after_hash":"5ae5384a797a9ae57ab4f27ab46093c81490e830a87cfb156608b1a4e516c7fe","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"ff85f7331da128686dea811679ae4071a218c85901ecef4c08f7ead3ee581d78"} {"hash_algorithm":"sha256","ts":"2026-10-23T10:18:02.894Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/15","value":{"created_at":"2026-10-23T10:18:02.884Z","author":"codex-agent","text":"Real-data revalidation after the ownership guards: actual pre-tag pm-review f72250e4c5ee8dfafaf57ee8afe20ddd3cbc993e still plans one adopt into pm-review-withdrawn-lineage, zero creates (Fix URL description plus Issue: URL body, one distinct issue). Fresh pm-review main e1fc83a81507ca6a91f1675b74863c050f1a89f1 plans one provenance update. Fresh public pm-context main 7d7479ae77c13d01c54b2b4560ad54091ffed788 plans five issues: two creates, three updates, zero adopts/skips. Original file hashes remain unchanged 106/106, 106/106 and 181/181. The mandatory linked adoption test run passed 21/21. Strict tracker health passed after merging main; no merge maintenance was needed. Updated README examples and the validation document describe eligibility, incidental receipts and both review revert proofs."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T10:18:02.894Z"}],"before_hash":"5ae5384a797a9ae57ab4f27ab46093c81490e830a87cfb156608b1a4e516c7fe","after_hash":"f8fec69399b531ac78db7f9ab129b358fbec4bd929f7d8b49942c36fca344683","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"c80ada0821edebedc882d18cb198facd76082771d1bf49cc7e749c0776ddb315"} {"hash_algorithm":"sha256","ts":"2026-10-23T10:18:04.984Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":{"value":"implementer","source":"argv"},"topic":null},"op":"release","patch":[{"op":"remove","path":"/metadata/claim_principal"},{"op":"remove","path":"/metadata/assignee"},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T10:18:04.984Z"}],"before_hash":"f8fec69399b531ac78db7f9ab129b358fbec4bd929f7d8b49942c36fca344683","after_hash":"9e7fd744feb1ff16980adb1478dd3d1a6a9d223c181ce3d4215050b78db8ec5e","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"d4af50651e3b96bd5c5a6ea69e8fb8d20b005200d6e53576bb86663bf87e69df"} +{"hash_algorithm":"sha256","ts":"2026-10-23T11:04:05.596Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":{"value":"implementer","source":"argv"},"topic":null},"op":"claim","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T11:04:05.596Z"},{"op":"add","path":"/metadata/assignee","value":"codex-agent"},{"op":"add","path":"/metadata/claim_principal","value":"codex-agent"}],"before_hash":"9e7fd744feb1ff16980adb1478dd3d1a6a9d223c181ce3d4215050b78db8ec5e","after_hash":"64278204299385a7df1e717ae66c743646352c2073d5bf838292747bbb19688e","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"62bcae24427173f6736ec6fa18611d599f5cade8700f95aba04982400b7969bb"} +{"hash_algorithm":"sha256","ts":"2026-10-23T11:04:06.853Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/16","value":{"created_at":"2026-10-23T11:04:06.852Z","author":"codex-agent","text":"Second review follow-up on PR 169: the distinct-issue regex omitted a trailing-slash issue URL, allowing a two-reference item to transfer ownership. The remote branch was pulled first and was unchanged. Replace this counting grammar with one parser used for intent and distinct identities, covering qualified shorthand, repository-hinted bare numbers, issue/PR URLs with trailing slash, fragments, query strings and prose punctuation. Add a property-style second-reference matrix and a real non-git persistence regression, then record a behavioral revert and fresh exact-head npm/Bun CI evidence."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T11:04:06.853Z"}],"before_hash":"64278204299385a7df1e717ae66c743646352c2073d5bf838292747bbb19688e","after_hash":"40abee07d43018d4da40279ce7de1a214e48b75f1f08650c238f7e7fd8ce0659","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"27dfffee67cd393bd7127d3f80051d5c23742ec99f4f30d2ac226b98bfa1cbc0"} +{"hash_algorithm":"sha256","ts":"2026-10-23T11:10:14.214Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","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/acceptance_criteria","value":"Preserve established tag/URL/provider identity matching. Adopt only an untagged item with exactly one distinct repository-qualified GitHub issue across title, description and body, plus explicit fix/close/resolve intent, Issue:/Tracks:/GH issue labeling, or the reference in its title. Incidental and multi-issue citations never transfer ownership; return citation_candidates IDs and reasons while importing normally. Keep ambiguity refusal for two eligible intent-bearing items. Closed incidental items must not reopen. Test qualified references and punctuation, positive fix/label/title cases, related-only mentions and multi-issue roadmaps, non-git trackers, typed receipts, idempotency and opt-out. Record behavioral revert proofs. Repeat public dry runs including pre-tag pm-review adoption and pm-context counts. Local targeted tests/typecheck/lint and exact-head npm/Bun CI release gates pass.; Use one reference parser for ownership intent and distinct-target counting across qualified shorthand, repository-hinted bare numbers, issue/PR URLs with optional trailing slash, queries, fragments and punctuation, with property-style second-reference regressions and a behavioral revert proof"},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T11:10:14.214Z"}],"before_hash":"40abee07d43018d4da40279ce7de1a214e48b75f1f08650c238f7e7fd8ce0659","after_hash":"c6b621f16e5d8ff985df4697f88f3cac5ab100f9b932f47e9034c0bd1afd1809","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"26db06151e6f5f1394262fa8e21f73ceaa095978d4087b6ce118ffbd190f7528"} +{"hash_algorithm":"sha256","ts":"2026-10-23T11:10:14.740Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/17","value":{"created_at":"2026-10-23T11:10:14.740Z","author":"codex-agent","text":"The shared parser now normalizes issue/PR URL identities including trailing slash, query strings, comment fragments and Markdown punctuation. Repository-hinted bare references count toward additional targets but bare numbers and PR links alone cannot establish issue ownership. The property matrix checks 8925 second-reference combinations across all three fields plus 51 positive controls. Plain/atomic real non-git tracker regressions preserve the entire roadmap projection while creating the issue item. Combined adoption/regex suite passed 32/32 before the additional adjacent-Markdown boundary case, which also passes. Behavioral revert restored only citesIssue from cce11c4, retaining the new parser, receipts and real persistence: the property test plus both trailing-slash persistence tests fail (3 failures, 0 passes/skips, exit 1). Fixed cases pass. Typecheck, test typecheck, lint, docstrings and zero-duplication checks passed."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T11:10:14.740Z"}],"before_hash":"c6b621f16e5d8ff985df4697f88f3cac5ab100f9b932f47e9034c0bd1afd1809","after_hash":"3f93a741be0b7c74cbceae272bd81587f5a3298ce17f776f714cbf040fcfdb5a","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"e9d4b152c17cdb5b768eb72e53230bc4ad341deae842ce34ffc876fdbb30d8cb"} +{"hash_algorithm":"sha256","ts":"2026-10-23T11:14:34.439Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/18","value":{"created_at":"2026-10-23T11:14:34.439Z","author":"codex-agent","text":"Merged the newly advanced main toolchain update in 92f595e and installed its existing dependency pins (CLI, pm-ops and pm-changelog 2026.10.20). Reverified against those pins: mandatory linked adoption suite 25/25, regex suite 7/7, typecheck, test typecheck, build, lint, docstrings, zero duplication and strict tracker health pass. The current parser property matrix covers 8925 second-reference combinations plus 51 positive controls. The three-test behavioral revert was repeated against the updated toolchain and still fails 3/3 with zero passes/skips. Scoped coverage inspection found no uncovered branches in the new parser. The updated README and validation document explain the reference grammar and proof. Full unchanged-threshold npm/Bun release evidence will come from CI at the pushed correction head; no merge or release is authorized."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T11:14:34.439Z"}],"before_hash":"3f93a741be0b7c74cbceae272bd81587f5a3298ce17f776f714cbf040fcfdb5a","after_hash":"516d91de934d23a04426756c5590e4d659dc8df412d51a874e468a40f789f8a3","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"a74a5e61db7e9dbb88b0fd08237e964054367483677c17bb59b0fd87c4cf674d"} +{"hash_algorithm":"sha256","ts":"2026-10-23T11:14:35.611Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":{"value":"implementer","source":"argv"},"topic":null},"op":"release","patch":[{"op":"remove","path":"/metadata/claim_principal"},{"op":"remove","path":"/metadata/assignee"},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T11:14:35.611Z"}],"before_hash":"516d91de934d23a04426756c5590e4d659dc8df412d51a874e468a40f789f8a3","after_hash":"100730232e40dff3029140f98636a71425e7cc0238d339c4c41053627402cbaf","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"5bdac600adb18c70acb3f4770c04af719dfefc254665d279e9555000e0150de1"} diff --git a/README.md b/README.md index 07278cb0..4d992b73 100644 --- a/README.md +++ b/README.md @@ -380,9 +380,13 @@ eligible because their fix may already have landed. Adoption requires **both** of these ownership signals: - The combined title, description and body cite exactly one distinct GitHub - issue, counting qualified references to every repository. Repeated URLs and - shorthand for the same issue count once. A second issue disqualifies the item - even when only one issue is selected for import. + issue or PR, counting references to every repository. One parser handles + `owner/repo#N`, issue and `/pull/N` URLs, optional trailing `/`, query strings, + fragments such as `#issuecomment-123`, and trailing prose punctuation. + Bare `#N` uses the imported repository as a hint when counting additional + targets, but cannot establish ownership on its own; neither can a PR URL. + Repeated URLs and shorthand for the same identity count once. A second + issue or PR disqualifies the item even when only one issue is selected. - The target reference appears in the title, or directly follows explicit intent: `fix`/`fixes`/`fixed`, `close`/`closes`/`closed`, `resolve`/`resolves`/`resolved`, `Issue:`, `Tracks:` or `GH issue`. @@ -398,6 +402,9 @@ Adoption requires **both** of these ownership signals: | Body: `Related: acme/widgets#30` | Incidental; `missing_intent` | | Title: `Roadmap acme/widgets#30 and acme/widgets#31` | Incidental for either issue; `multiple_issues` | | Description: `Fixes acme/widgets#30`; body: `See other/repo#8` | Incidental; `multiple_issues` | +| Body: `Fixes acme/widgets#30 and https://github.com/acme/widgets/issues/31/` | Incidental; `multiple_issues` | +| Body: `Fixes acme/widgets#30 and #31` (importing `acme/widgets`) | Incidental; `multiple_issues` | +| Body: `Fixes acme/widgets#30 and https://github.com/acme/widgets/pull/31#issuecomment-123` | Incidental; `multiple_issues` | Incidental mentions never transfer ownership. The issue imports normally into its own item, leaving the incidental item's status, content and fields intact, diff --git a/docs/cited-issue-adoption-validation.md b/docs/cited-issue-adoption-validation.md index f5b2403c..abb6e568 100644 --- a/docs/cited-issue-adoption-validation.md +++ b/docs/cited-issue-adoption-validation.md @@ -2,7 +2,7 @@ The importer first resolves established provenance tags, structured URLs and provider back-links. Only an issue without an established owner reaches exact -title/description/body citation matching. Only a single-issue item with explicit +title/description/body citation matching. Only a single-target item with explicit fix/track intent or a title reference is eligible. One eligible candidate is adopted; multiple eligible candidates refuse the batch before any writes and report `ambiguous_citation` with candidate IDs and a tagging remedy. Status push, export and Projects @@ -105,3 +105,36 @@ changed the package worktree or rebuilt a stale artifact: tests import live TS. 21 tests. Typecheck, test typecheck, build, lint, docstrings and strict tracker health pass locally. Full npm/Bun release-gate evidence is recorded in PR 169 and the final handoff at the exact pushed head; thresholds remain unchanged. + +## Reference-parser review follow-up + +The second review found that `Fixes acme/widgets#30 and +https://github.com/acme/widgets/issues/31/` could still transfer ownership: +the earlier counting regex skipped the trailing-slash URL. A shared parser now +produces the identities and source positions used by both distinct-target +counting and intent checks. It recognizes qualified shorthand, repository-hinted +bare `#N`, issue and PR URLs, optional trailing slashes, query strings, comment +fragments, prose punctuation and Markdown link boundaries. Foreign and malformed +URLs are consumed as URL tokens so their interior hash text cannot masquerade +as a separate reference. Bare numbers and PR URLs count toward additional +targets but cannot establish issue ownership on their own. + +The property-style regression exercises 17 ownership forms against 35 second +reference forms with five punctuation/Markdown wrappers in all three prose +fields: **8,925 disqualification combinations**, plus **51 positive ownership +controls**. The matrix includes issue and PR links, other repositories, slash, +query and fragment combinations, and Markdown links adjacent to following text. +Repeated URL/shorthand/bare references to the same identity remain one target. + +Plain and atomic real non-git tracker regressions reproduce the reported +trailing-slash example. They require one create, zero adopts and a +`multiple_issues` candidate hint, then verify that the entire original roadmap +projection remains unchanged after import. + +The isolated behavioral revert restores only `citesIssue` from `cce11c4`, leaving +the new parser, planner, receipts and real SDK persistence present. Running +`node --test --test-name-pattern='every accepted second-reference|trailing-slash second reference' test/cited-issue-adoption.test.ts` +then produces **3 failures, 0 passes, 0 skips, exit 1**. The fixed cases pass. +The complete adoption/regex selection passes **32/32**; the linked adoption +command contains **25 tests**. Fresh exact-head npm/Bun release evidence is +recorded in PR 169 when CI completes, without changing coverage thresholds. diff --git a/import-fidelity.ts b/import-fidelity.ts index d7cc55e2..db08ed5f 100644 --- a/import-fidelity.ts +++ b/import-fidelity.ts @@ -72,17 +72,37 @@ export class AmbiguousCitationError extends Error { } } -/** Require one distinct issue across all prose fields and a title reference or explicit fix/track intent. */ +/** Parse issue/PR identities once, resolving bare numbers only for conservative counting. */ +function citationReferences(text: string, repoHint: string): { key: string; index: number; ownsIssue: boolean }[] { + const references: { key: string; index: number; ownsIssue: boolean }[] = []; + const tokens = text.matchAll(/(?"'`\])}]+|(?:[\w.-]+\/[\w.-]+)?#[1-9]\d*(?![\w/=&%-]|\.\w))/gi); + for (const token of tokens) { + const raw = token[0]; + if (/^https?:/i.test(raw)) { + try { + const url = new URL(raw.replace(/[)\].,;:!}]+$/, "")); + 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. */ } + } else { + const [repo, number] = raw.split("#"); + references.push({ key: `${repo || repoHint}#${number}`.toLowerCase(), index: token.index, ownsIssue: repo !== "" }); + } + } + return references; +} + +/** Require one distinct target across all prose fields and a qualified issue reference with explicit intent. */ function citesIssue(item: PmItem, repo: string, number: number): "eligible" | CitationCandidate["reason"] | undefined { const target = `${repo}#${number}`; const identities = new Set(); let intended = false; for (const [field, text] of [item.title ?? "", item.description ?? "", item.body ?? ""].entries()) { - const references = text.matchAll(/(? tracker(async pmRoot => { + const local = (await create({ type: "Task", title: "Independent roadmap", description: `Fixes ${repo}#30 and https://github.com/${repo}/issues/31/`, + body: "Keep the roadmap context", status: "open" }, { pmRoot })).item; + const before = (await get(local.id, {}, { pmRoot })).item; + const preview = (await importing(pmRoot, { atomic, "dry-run": true })).result; + assert.equal(preview.wouldImport, 1); + assert.equal(preview.wouldAdopt, 0); + assert.deepEqual(preview.citation_candidates, [{ issue: 30, id: local.id, reason: "multiple_issues" }]); + assert.equal((preview.plan as { action: string }[])[0]!.action, "create"); + const landed = (await importing(pmRoot, { atomic })).result; + assert.equal(landed.imported, 1); + assert.equal(landed.adopted, 0); + assert.deepEqual(landed.citation_candidates, preview.citation_candidates); + assert.deepEqual((await get(local.id, {}, { pmRoot })).item, before); + assert.equal((await items(pmRoot)).length, 2); + })); } +test("every accepted second-reference shape disqualifies a fix item across every prose field", () => { + const suffixes = ["", "/", "#issuecomment-123", "/#issuecomment-123", "?view=all", "/?view=all", "?view=all#issuecomment-123", "/?view=all#issuecomment-123"]; + const firstShapes = [`${repo}#30`, ...suffixes.flatMap(suffix => [issue.html_url + suffix, `http://www.github.com/${repo}/issues/30${suffix}`])]; + const secondShapes = [`${repo}#31`, "#31", "other/widgets#31", ...["issues", "pull"].flatMap(kind => suffixes.flatMap(suffix => [ + `https://github.com/${repo}/${kind}/31${suffix}`, `http://www.github.com/other/widgets/${kind}/31${suffix}`, + ]))]; + for (const first of firstShapes) { + for (const field of ["title", "description", "body"] as const) { + assert.equal(matchImportedIssue([{ id: "local", [field]: `Fixes ${first}` }], issue).via, "citation", `${field}: ${first}`); + for (const second of secondShapes) { + for (const text of [second, `${second}.`, `(${second})`, `[reference](${second}).`, `[reference](${second})next`]) { + const local = { id: "local", title: "Local work", description: `Fixes ${first}`, body: "" }; + local[field] += ` and ${text}`; + const result = matchImportedIssue([local], issue); + assert.equal(result.via, "new", `${first}; ${field}: ${text}`); + assert.deepEqual(result.citation_candidates, [{ id: "local", reason: "multiple_issues" }]); + } + } + } + } +}); + +test("one parser deduplicates equivalent locators without granting ownership to bare numbers or PR links", () => { + const local = { id: "local", title: `Fixes ${repo}#30`, description: `Issue: ${issue.html_url}/?view=all#issuecomment-123`, + body: `Same target #30 and ${issue.html_url}#issuecomment-456` }; + assert.equal(matchImportedIssue([local], issue).item, local); + for (const text of ["#30", `https://github.com/${repo}/pull/30/`, `https://github.com/${repo}/issues/30/extra`, + `${issue.html_url}.json`, "https://github.com/other/widgets/issues/30/", "https://other.test/acme/widgets/issues/30/", "https://["]) { + assert.equal(matchImportedIssue([{ id: "local", title: `Fixes ${text}` }], issue).via, "new", text); + } + for (const embedded of ["https://other.test/path#31", "https://other.test/acme/widgets#31", "https://github.com/search?q=other/widgets%2331", "https://[bad#31"]) { + assert.equal(matchImportedIssue([{ id: "local", description: `Fixes ${repo}#30 and ${embedded}` }], issue).via, "citation", embedded); + } +}); + test("citation matching accepts exact qualified references across prose fields and trailing punctuation", () => { for (const field of ["title", "description", "body"]) { for (const citation of [issue.html_url, `${issue.html_url}.`, `(${issue.html_url})`, `${issue.html_url}?`, From 56580ecf7637a99ac4ddad6031d9cef3dd63a385 Mon Sep 17 00:00:00 2001 From: SteveBot <1153461+unbraind@users.noreply.github.com> Date: Fri, 23 Oct 2026 13:49:08 +0200 Subject: [PATCH 07/18] Trim citation URL punctuation in one backward pass --- .../features/pm-github-cited-issue-adoption.toon | 10 +++++++--- .../history/pm-github-cited-issue-adoption.jsonl | 6 ++++++ docs/cited-issue-adoption-validation.md | 14 +++++++++++++- import-fidelity.ts | 8 ++++---- test/import-regex.test.ts | 16 ++++++++++++++++ 5 files changed, 46 insertions(+), 8 deletions(-) diff --git a/.agents/pm/features/pm-github-cited-issue-adoption.toon b/.agents/pm/features/pm-github-cited-issue-adoption.toon index d747f830..b36f8b8d 100644 --- a/.agents/pm/features/pm-github-cited-issue-adoption.toon +++ b/.agents/pm/features/pm-github-cited-issue-adoption.toon @@ -6,10 +6,10 @@ status: in_progress priority: 2 tags[3]: changelog-unreleased,github,import created_at: "2026-10-23T07:16:54.041Z" -updated_at: "2026-10-23T11:14:35.611Z" +updated_at: "2026-10-23T11:48:41.657Z" author: codex-agent acceptance_criteria: "Preserve established tag/URL/provider identity matching. Adopt only an untagged item with exactly one distinct repository-qualified GitHub issue across title, description and body, plus explicit fix/close/resolve intent, Issue:/Tracks:/GH issue labeling, or the reference in its title. Incidental and multi-issue citations never transfer ownership; return citation_candidates IDs and reasons while importing normally. Keep ambiguity refusal for two eligible intent-bearing items. Closed incidental items must not reopen. Test qualified references and punctuation, positive fix/label/title cases, related-only mentions and multi-issue roadmaps, non-git trackers, typed receipts, idempotency and opt-out. Record behavioral revert proofs. Repeat public dry runs including pre-tag pm-review adoption and pm-context counts. Local targeted tests/typecheck/lint and exact-head npm/Bun CI release gates pass.; Use one reference parser for ownership intent and distinct-target counting across qualified shorthand, repository-hinted bare numbers, issue/PR URLs with optional trailing slash, queries, fragments and punctuation, with property-style second-reference regressions and a behavioral revert proof" -comments[19]{created_at,author,text}: +comments[22]{created_at,author,text}: "2026-10-23T07:17:26.861Z",codex-agent,"Overlap review: pm-github-c093 owns structured URL/provider back-link import matching, pm-github-identity-collisions diagnoses and repairs structured ownership, Projects import matches board and issue provenance, and export upserts linked issues. None adopts arbitrary exact prose citations. This feature adds a fallback only after existing import matching, preserving those paths. Scheduled gated sync already invokes import, so it inherits the default. Plan ambiguous citations before writes and refuse the batch with actionable candidate IDs." "2026-10-23T07:24:01.857Z",codex-agent,"Implementation: strong ownership matching stays first; exact qualified title/description/body citations fall back to a single existing item. All imports now prepare identities before writes, and ambiguous citations expose candidate IDs plus an exact tagging remedy. Adoption retains local tags and reports a separate count; provider-owned records retain link-only behavior. New tests use real trackers with a PATH containing node and the pinned pm CLI but no git, plus real git trackers for write-gate acceptance." "2026-10-23T07:27:45.693Z",codex-agent,"New citation suite: 13/13 passed, including plain/atomic adoption in non-git trackers, write-gate acceptance, retry identity, ambiguity refusal, punctuation, repo/number boundaries, structured ownership precedence and provider link-only preservation. Live pm-review main aa16d855df1f7b74afd3453617f77e92c44d493e already tags pm-review-withdrawn-lineage for issue 30, so an untouched current-main copy correctly plans update rather than adopt. Preserve this current-state result and locate the pre-tag historical tracker for a separate adoption replay. SDK reads may create runtime caches; verify original tracked file bytes independently." @@ -29,6 +29,9 @@ comments[19]{created_at,author,text}: "2026-10-23T11:04:06.852Z",codex-agent,"Second review follow-up on PR 169: the distinct-issue regex omitted a trailing-slash issue URL, allowing a two-reference item to transfer ownership. The remote branch was pulled first and was unchanged. Replace this counting grammar with one parser used for intent and distinct identities, covering qualified shorthand, repository-hinted bare numbers, issue/PR URLs with trailing slash, fragments, query strings and prose punctuation. Add a property-style second-reference matrix and a real non-git persistence regression, then record a behavioral revert and fresh exact-head npm/Bun CI evidence." "2026-10-23T11:10:14.740Z",codex-agent,"The shared parser now normalizes issue/PR URL identities including trailing slash, query strings, comment fragments and Markdown punctuation. Repository-hinted bare references count toward additional targets but bare numbers and PR links alone cannot establish issue ownership. The property matrix checks 8925 second-reference combinations across all three fields plus 51 positive controls. Plain/atomic real non-git tracker regressions preserve the entire roadmap projection while creating the issue item. Combined adoption/regex suite passed 32/32 before the additional adjacent-Markdown boundary case, which also passes. Behavioral revert restored only citesIssue from cce11c4, retaining the new parser, receipts and real persistence: the property test plus both trailing-slash persistence tests fail (3 failures, 0 passes/skips, exit 1). Fixed cases pass. Typecheck, test typecheck, lint, docstrings and zero-duplication checks passed." "2026-10-23T11:14:34.439Z",codex-agent,"Merged the newly advanced main toolchain update in 92f595e and installed its existing dependency pins (CLI, pm-ops and pm-changelog 2026.10.20). Reverified against those pins: mandatory linked adoption suite 25/25, regex suite 7/7, typecheck, test typecheck, build, lint, docstrings, zero duplication and strict tracker health pass. The current parser property matrix covers 8925 second-reference combinations plus 51 positive controls. The three-test behavioral revert was repeated against the updated toolchain and still fails 3/3 with zero passes/skips. Scoped coverage inspection found no uncovered branches in the new parser. The updated README and validation document explain the reference grammar and proof. Full unchanged-threshold npm/Bun release evidence will come from CI at the pushed correction head; no merge or release is authorized." + "2026-10-23T11:43:37.620Z",codex-agent,"Exact-head CodeQL found a further issue in the shared parser: trimming punctuation with an unanchored suffix regex can backtrack quadratically on hostile URL text. The npm/Bun release phases pass, but this independent finding is valid. Replace that trim with the existing backwards scanner, preserve the default structured-locator punctuation behavior, and extend the existing adversarial regex regression harness. No review replies will be posted. The corrected source head will receive fresh release and CodeQL evidence." + "2026-10-23T11:46:42.028Z",codex-agent,"CodeQL correction uses the existing backwards punctuation scanner with a citation delimiter set, preserving the default structured-locator behavior. The new real-matcher performance regression uses 50000 exclamation marks followed by a non-punctuation sentinel. Fixed best-of-three local time is 0.172 ms, under the existing 50 ms bound. Isolated behavioral revert restored only citationReferences from ef325ba while retaining the configurable scanner and test: 1 failure, 0 passes/skips, exit 1 at 3670.058 ms. This complements the three-failure ownership revert. Typecheck, test typecheck, lint, the property matrix and scanner parity pass. Full affected suites and fresh exact-head release/CodeQL checks will verify the final correction." + "2026-10-23T11:48:41.158Z",codex-agent,"Final local checks after the CodeQL trim correction pass: PM-linked node --test test/cited-issue-adoption.test.ts (25/25) and node --test test/import-regex.test.ts (8/8), with zero failures/skips. The second command is now registered on this item and was run through pm test --only-last --run --progress. Typecheck, test typecheck, build, lint, docstrings, zero duplication, privacy and strict tracker health pass. Performance revert command node --test --test-name-pattern=\"citation URL punctuation trigger\" test/import-regex.test.ts fails 1/1 with only citationReferences from ef325ba restored; fixed best-of-three 0.172 ms versus reverted 3670.058 ms. The earlier three-failure ownership revert remains recorded. The next pushed source head requires fresh npm/Bun release and CodeQL success before handoff." files[10]{path,scope}: import-fidelity.ts,project index.ts,project @@ -40,8 +43,9 @@ files[10]{path,scope}: test/import-gate.test.ts,project test/import-regex.test.ts,project test/import-selection.test.ts,project -tests[1]{command,scope,timeout_seconds,provenance{author,created_at,source_kind,source_ref}}: +tests[2]{command,scope,timeout_seconds,provenance{author,created_at,source_kind,source_ref}}: node --test test/cited-issue-adoption.test.ts,project,900,codex-agent,"2026-10-23T07:35:49.405Z",local_mutation,feat/cited-issue-adoption + node --test test/import-regex.test.ts,project,900,codex-agent,"2026-10-23T11:47:54.745Z",local_mutation,feat/cited-issue-adoption docs[2]{path,scope}: docs/cited-issue-adoption-validation.md,project README.md,project diff --git a/.agents/pm/history/pm-github-cited-issue-adoption.jsonl b/.agents/pm/history/pm-github-cited-issue-adoption.jsonl index b9e4bc72..9656bdd9 100644 --- a/.agents/pm/history/pm-github-cited-issue-adoption.jsonl +++ b/.agents/pm/history/pm-github-cited-issue-adoption.jsonl @@ -39,3 +39,9 @@ {"hash_algorithm":"sha256","ts":"2026-10-23T11:10:14.740Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/17","value":{"created_at":"2026-10-23T11:10:14.740Z","author":"codex-agent","text":"The shared parser now normalizes issue/PR URL identities including trailing slash, query strings, comment fragments and Markdown punctuation. Repository-hinted bare references count toward additional targets but bare numbers and PR links alone cannot establish issue ownership. The property matrix checks 8925 second-reference combinations across all three fields plus 51 positive controls. Plain/atomic real non-git tracker regressions preserve the entire roadmap projection while creating the issue item. Combined adoption/regex suite passed 32/32 before the additional adjacent-Markdown boundary case, which also passes. Behavioral revert restored only citesIssue from cce11c4, retaining the new parser, receipts and real persistence: the property test plus both trailing-slash persistence tests fail (3 failures, 0 passes/skips, exit 1). Fixed cases pass. Typecheck, test typecheck, lint, docstrings and zero-duplication checks passed."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T11:10:14.740Z"}],"before_hash":"c6b621f16e5d8ff985df4697f88f3cac5ab100f9b932f47e9034c0bd1afd1809","after_hash":"3f93a741be0b7c74cbceae272bd81587f5a3298ce17f776f714cbf040fcfdb5a","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"e9d4b152c17cdb5b768eb72e53230bc4ad341deae842ce34ffc876fdbb30d8cb"} {"hash_algorithm":"sha256","ts":"2026-10-23T11:14:34.439Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/18","value":{"created_at":"2026-10-23T11:14:34.439Z","author":"codex-agent","text":"Merged the newly advanced main toolchain update in 92f595e and installed its existing dependency pins (CLI, pm-ops and pm-changelog 2026.10.20). Reverified against those pins: mandatory linked adoption suite 25/25, regex suite 7/7, typecheck, test typecheck, build, lint, docstrings, zero duplication and strict tracker health pass. The current parser property matrix covers 8925 second-reference combinations plus 51 positive controls. The three-test behavioral revert was repeated against the updated toolchain and still fails 3/3 with zero passes/skips. Scoped coverage inspection found no uncovered branches in the new parser. The updated README and validation document explain the reference grammar and proof. Full unchanged-threshold npm/Bun release evidence will come from CI at the pushed correction head; no merge or release is authorized."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T11:14:34.439Z"}],"before_hash":"3f93a741be0b7c74cbceae272bd81587f5a3298ce17f776f714cbf040fcfdb5a","after_hash":"516d91de934d23a04426756c5590e4d659dc8df412d51a874e468a40f789f8a3","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"a74a5e61db7e9dbb88b0fd08237e964054367483677c17bb59b0fd87c4cf674d"} {"hash_algorithm":"sha256","ts":"2026-10-23T11:14:35.611Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":{"value":"implementer","source":"argv"},"topic":null},"op":"release","patch":[{"op":"remove","path":"/metadata/claim_principal"},{"op":"remove","path":"/metadata/assignee"},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T11:14:35.611Z"}],"before_hash":"516d91de934d23a04426756c5590e4d659dc8df412d51a874e468a40f789f8a3","after_hash":"100730232e40dff3029140f98636a71425e7cc0238d339c4c41053627402cbaf","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"5bdac600adb18c70acb3f4770c04af719dfefc254665d279e9555000e0150de1"} +{"hash_algorithm":"sha256","ts":"2026-10-23T11:43:36.311Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":{"value":"implementer","source":"argv"},"topic":null},"op":"claim","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T11:43:36.311Z"},{"op":"add","path":"/metadata/assignee","value":"codex-agent"},{"op":"add","path":"/metadata/claim_principal","value":"codex-agent"}],"before_hash":"100730232e40dff3029140f98636a71425e7cc0238d339c4c41053627402cbaf","after_hash":"256d3080828c65d91eee53760df66b0927df0c89190f7df8b69a4ac33b3e5cc8","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"ef4c4bf006833b5aeed7ef6297434b20c9ebfe7e26fdc2ac697929513f7c3c96"} +{"hash_algorithm":"sha256","ts":"2026-10-23T11:43:37.620Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/19","value":{"created_at":"2026-10-23T11:43:37.620Z","author":"codex-agent","text":"Exact-head CodeQL found a further issue in the shared parser: trimming punctuation with an unanchored suffix regex can backtrack quadratically on hostile URL text. The npm/Bun release phases pass, but this independent finding is valid. Replace that trim with the existing backwards scanner, preserve the default structured-locator punctuation behavior, and extend the existing adversarial regex regression harness. No review replies will be posted. The corrected source head will receive fresh release and CodeQL evidence."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T11:43:37.620Z"}],"before_hash":"256d3080828c65d91eee53760df66b0927df0c89190f7df8b69a4ac33b3e5cc8","after_hash":"06de12d81e93d83d3d928ae21ba11df2b8363ac639f506fbab9227f858453bd6","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"af2806eeca5b2208d077daee85e768ffecb1163d3a801c671eeb5b386e79c6f4"} +{"hash_algorithm":"sha256","ts":"2026-10-23T11:46:42.028Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/20","value":{"created_at":"2026-10-23T11:46:42.028Z","author":"codex-agent","text":"CodeQL correction uses the existing backwards punctuation scanner with a citation delimiter set, preserving the default structured-locator behavior. The new real-matcher performance regression uses 50000 exclamation marks followed by a non-punctuation sentinel. Fixed best-of-three local time is 0.172 ms, under the existing 50 ms bound. Isolated behavioral revert restored only citationReferences from ef325ba while retaining the configurable scanner and test: 1 failure, 0 passes/skips, exit 1 at 3670.058 ms. This complements the three-failure ownership revert. Typecheck, test typecheck, lint, the property matrix and scanner parity pass. Full affected suites and fresh exact-head release/CodeQL checks will verify the final correction."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T11:46:42.028Z"}],"before_hash":"06de12d81e93d83d3d928ae21ba11df2b8363ac639f506fbab9227f858453bd6","after_hash":"e424b3a9812f56e3704810c275f27ed3fe8a8044beb0479c8324fd19feaa3ccc","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"6ef2e4f29c5c64c1769cfeb3b63d7675a40cdc9538edda60ab7dfc5885c77c01"} +{"hash_algorithm":"sha256","ts":"2026-10-23T11:47:54.795Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"tests_add","patch":[{"op":"add","path":"/metadata/tests/1","value":{"command":"node --test test/import-regex.test.ts","scope":"project","timeout_seconds":900,"provenance":{"author":"codex-agent","created_at":"2026-10-23T11:47:54.745Z","source_kind":"local_mutation","source_ref":"feat/cited-issue-adoption"}}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T11:47:54.795Z"}],"before_hash":"e424b3a9812f56e3704810c275f27ed3fe8a8044beb0479c8324fd19feaa3ccc","after_hash":"708ef0b6f7e3583205b086f34b54c058b9a22f97020dea5d3a0f5331e03abea5","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"6d986448235acf1718f940dad18e7d14b174a4aa7796d59269206231a485bb1a"} +{"hash_algorithm":"sha256","ts":"2026-10-23T11:48:41.159Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/21","value":{"created_at":"2026-10-23T11:48:41.158Z","author":"codex-agent","text":"Final local checks after the CodeQL trim correction pass: PM-linked node --test test/cited-issue-adoption.test.ts (25/25) and node --test test/import-regex.test.ts (8/8), with zero failures/skips. The second command is now registered on this item and was run through pm test --only-last --run --progress. Typecheck, test typecheck, build, lint, docstrings, zero duplication, privacy and strict tracker health pass. Performance revert command node --test --test-name-pattern=\"citation URL punctuation trigger\" test/import-regex.test.ts fails 1/1 with only citationReferences from ef325ba restored; fixed best-of-three 0.172 ms versus reverted 3670.058 ms. The earlier three-failure ownership revert remains recorded. The next pushed source head requires fresh npm/Bun release and CodeQL success before handoff."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T11:48:41.159Z"}],"before_hash":"708ef0b6f7e3583205b086f34b54c058b9a22f97020dea5d3a0f5331e03abea5","after_hash":"eb74b93a85cf138a77b23f8a83ef11ac2dd54e9c80b67bf94d34fa613a8b6043","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"fc1cff27d9163c902deaa400a7b7d6897e980eca062b49aa72113779a9cfba67"} +{"hash_algorithm":"sha256","ts":"2026-10-23T11:48:41.657Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":{"value":"implementer","source":"argv"},"topic":null},"op":"release","patch":[{"op":"remove","path":"/metadata/claim_principal"},{"op":"remove","path":"/metadata/assignee"},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T11:48:41.657Z"}],"before_hash":"eb74b93a85cf138a77b23f8a83ef11ac2dd54e9c80b67bf94d34fa613a8b6043","after_hash":"d1df10c2db2ca6daf0eb89aa60c8d119b954581c974d64f63e8dfd2031a99e08","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"6195e038c0f056ca7c8f7a029fa3e93cfcb4af21b064534fffe2ec2bd9c01328"} diff --git a/docs/cited-issue-adoption-validation.md b/docs/cited-issue-adoption-validation.md index abb6e568..fdcafb18 100644 --- a/docs/cited-issue-adoption-validation.md +++ b/docs/cited-issue-adoption-validation.md @@ -135,6 +135,18 @@ The isolated behavioral revert restores only `citesIssue` from `cce11c4`, leavin the new parser, planner, receipts and real SDK persistence present. Running `node --test --test-name-pattern='every accepted second-reference|trailing-slash second reference' test/cited-issue-adoption.test.ts` then produces **3 failures, 0 passes, 0 skips, exit 1**. The fixed cases pass. -The complete adoption/regex selection passes **32/32**; the linked adoption +The complete adoption/regex selection passes **33/33**; the linked adoption command contains **25 tests**. Fresh exact-head npm/Bun release evidence is recorded in PR 169 when CI completes, without changing coverage thresholds. + +CodeQL then identified quadratic backtracking in the new URL punctuation +suffix regex. The parser now calls the existing backwards punctuation scanner +with its citation delimiter set, leaving the default structured-locator set +unchanged. The additional adversarial test sends a URL followed by 50,000 +exclamation marks and a non-punctuation sentinel through the real matcher. Its +best of three local trials is **0.172 ms**, below the existing 50 ms guard. +Restoring only `citationReferences` from `ef325ba` retains the new scanner and +test but restores the vulnerable regex call: the test then fails at +**3,670.058 ms** (one failure, zero passes/skips, exit 1). These timings are local +regression evidence, not a deployment benchmark. A fresh pushed head receives +new release-gate and CodeQL evidence. diff --git a/import-fidelity.ts b/import-fidelity.ts index db08ed5f..703bf6d7 100644 --- a/import-fidelity.ts +++ b/import-fidelity.ts @@ -80,7 +80,7 @@ function citationReferences(text: string, repoHint: string): { key: string; inde const raw = token[0]; if (/^https?:/i.test(raw)) { try { - const url = new URL(raw.replace(/[)\].,;:!}]+$/, "")); + 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" }); @@ -109,10 +109,10 @@ function citesIssue(item: PmItem, repo: string, number: number): "eligible" | Ci return identities.size > 1 ? "multiple_issues" : intended ? "eligible" : "missing_intent"; } -/** Trim exporter punctuation in one backwards pass. @internal Shared with parity tests. */ -export function trimLocatorPunctuation(locator: string): string { +/** Trim configurable exporter punctuation in one backwards pass. @internal Shared with parity tests. */ +export function trimLocatorPunctuation(locator: string, punctuation = "),.;"): string { let end = locator.length; - while (end > 0 && "),.;".includes(locator[end - 1]!)) end--; + while (end > 0 && punctuation.includes(locator[end - 1]!)) end--; return locator.slice(0, end); } diff --git a/test/import-regex.test.ts b/test/import-regex.test.ts index 80294883..915572af 100644 --- a/test/import-regex.test.ts +++ b/test/import-regex.test.ts @@ -54,6 +54,8 @@ test("locator punctuation trim parity table", () => { } assert.equal(matchImportedIssue([{ id: "owner", links: url + "),.;" }], issue).item?.id, "owner"); assert.equal(matchImportedIssue([{ id: "owner", links: url + "))x" }], issue).item, undefined); + assert.equal(trimLocatorPunctuation(url + ":!]}", "),.;:!]}"), url); + assert.equal(trimLocatorPunctuation(url + ":!]}"), url + ":!]}"); }); test("ownership whitespace trigger: 200000 spaces finish under 50 ms", t => { @@ -86,6 +88,20 @@ test("locator punctuation trigger: 200000 parentheses finish under 50 ms", t => assert.equal(trimLocatorPunctuation(url + ")".repeat(200_000)), url); }); +test("citation URL punctuation trigger: 50000 exclamations plus sentinel finish under 50 ms", t => { + const description = `Fixes ${url}${"!".repeat(50_000)}x`; + matchImportedIssue([{ id: "warmup", description: `Fixes ${url}` }], issue); + let elapsed = Infinity; + for (let trial = 0; trial < 3; trial++) { + const start = performance.now(); + const result = matchImportedIssue([{ id: "owner", description }], issue); + elapsed = Math.min(elapsed, performance.now() - start); + assert.equal(result.item, undefined); + } + t.diagnostic(`citation URL 50000 exclamations plus sentinel (best of 3): ${elapsed.toFixed(3)} ms`); + assert.ok(elapsed < 50, `citation URL took ${elapsed.toFixed(3)} ms; limit 50 ms`); +}); + test("privacy phone whitespace parity and 20000-space trigger", t => { const old = /\b(?:phone|telephone|tel|mobile)\b["']?\s*[:=]\s*["']?\s*\+?\d[\d \t().-]{5,}\d/gi; for (const name of ["phone", "telephone", "tel", "mobile"]) { From aa86b216db7a90b72214520566376cd0e6aa2996 Mon Sep 17 00:00:00 2001 From: SteveBot <1153461+unbraind@users.noreply.github.com> Date: Fri, 23 Oct 2026 14:41:25 +0200 Subject: [PATCH 08/18] Refuse adoption when raw text mentions another issue number --- .../pm-github-cited-issue-adoption.toon | 9 +- .../pm-github-cited-issue-adoption.jsonl | 6 ++ README.md | 34 ++++--- docs/cited-issue-adoption-validation.md | 70 ++++++++++++--- import-fidelity.ts | 21 ++++- test/cited-issue-adoption.test.ts | 89 ++++++++++++++++++- 6 files changed, 198 insertions(+), 31 deletions(-) diff --git a/.agents/pm/features/pm-github-cited-issue-adoption.toon b/.agents/pm/features/pm-github-cited-issue-adoption.toon index b36f8b8d..1e5cc8c8 100644 --- a/.agents/pm/features/pm-github-cited-issue-adoption.toon +++ b/.agents/pm/features/pm-github-cited-issue-adoption.toon @@ -6,10 +6,10 @@ status: in_progress priority: 2 tags[3]: changelog-unreleased,github,import created_at: "2026-10-23T07:16:54.041Z" -updated_at: "2026-10-23T11:48:41.657Z" +updated_at: "2026-10-23T12:41:07.257Z" author: codex-agent -acceptance_criteria: "Preserve established tag/URL/provider identity matching. Adopt only an untagged item with exactly one distinct repository-qualified GitHub issue across title, description and body, plus explicit fix/close/resolve intent, Issue:/Tracks:/GH issue labeling, or the reference in its title. Incidental and multi-issue citations never transfer ownership; return citation_candidates IDs and reasons while importing normally. Keep ambiguity refusal for two eligible intent-bearing items. Closed incidental items must not reopen. Test qualified references and punctuation, positive fix/label/title cases, related-only mentions and multi-issue roadmaps, non-git trackers, typed receipts, idempotency and opt-out. Record behavioral revert proofs. Repeat public dry runs including pre-tag pm-review adoption and pm-context counts. Local targeted tests/typecheck/lint and exact-head npm/Bun CI release gates pass.; Use one reference parser for ownership intent and distinct-target counting across qualified shorthand, repository-hinted bare numbers, issue/PR URLs with optional trailing slash, queries, fragments and punctuation, with property-style second-reference regressions and a behavioral revert proof" -comments[22]{created_at,author,text}: +acceptance_criteria: "Preserve established tag/URL/provider identity matching. Adopt only an untagged item with exactly one distinct repository-qualified GitHub issue across title, description and body, plus explicit fix/close/resolve intent, Issue:/Tracks:/GH issue labeling, or the reference in its title. Incidental and multi-issue citations never transfer ownership; return citation_candidates IDs and reasons while importing normally. Keep ambiguity refusal for two eligible intent-bearing items. Closed incidental items must not reopen. Test qualified references and punctuation, positive fix/label/title cases, related-only mentions and multi-issue roadmaps, non-git trackers, typed receipts, idempotency and opt-out. Record behavioral revert proofs. Repeat public dry runs including pre-tag pm-review adoption and pm-context counts. Local targeted tests/typecheck/lint and exact-head npm/Bun CI release gates pass.; Raw title, description and body exclusion scans every issues/digits, pull/digits and hash-digits substring without Markdown or trailing-character boundaries and vetoes adoption for any non-target digit string, while strict qualified parsing and explicit intent remain required for a positive match and earlier repository-identity safeguards remain in force" +comments[25]{created_at,author,text}: "2026-10-23T07:17:26.861Z",codex-agent,"Overlap review: pm-github-c093 owns structured URL/provider back-link import matching, pm-github-identity-collisions diagnoses and repairs structured ownership, Projects import matches board and issue provenance, and export upserts linked issues. None adopts arbitrary exact prose citations. This feature adds a fallback only after existing import matching, preserving those paths. Scheduled gated sync already invokes import, so it inherits the default. Plan ambiguous citations before writes and refuse the batch with actionable candidate IDs." "2026-10-23T07:24:01.857Z",codex-agent,"Implementation: strong ownership matching stays first; exact qualified title/description/body citations fall back to a single existing item. All imports now prepare identities before writes, and ambiguous citations expose candidate IDs plus an exact tagging remedy. Adoption retains local tags and reports a separate count; provider-owned records retain link-only behavior. New tests use real trackers with a PATH containing node and the pinned pm CLI but no git, plus real git trackers for write-gate acceptance." "2026-10-23T07:27:45.693Z",codex-agent,"New citation suite: 13/13 passed, including plain/atomic adoption in non-git trackers, write-gate acceptance, retry identity, ambiguity refusal, punctuation, repo/number boundaries, structured ownership precedence and provider link-only preservation. Live pm-review main aa16d855df1f7b74afd3453617f77e92c44d493e already tags pm-review-withdrawn-lineage for issue 30, so an untouched current-main copy correctly plans update rather than adopt. Preserve this current-state result and locate the pre-tag historical tracker for a separate adoption replay. SDK reads may create runtime caches; verify original tracked file bytes independently." @@ -32,6 +32,9 @@ comments[22]{created_at,author,text}: "2026-10-23T11:43:37.620Z",codex-agent,"Exact-head CodeQL found a further issue in the shared parser: trimming punctuation with an unanchored suffix regex can backtrack quadratically on hostile URL text. The npm/Bun release phases pass, but this independent finding is valid. Replace that trim with the existing backwards scanner, preserve the default structured-locator punctuation behavior, and extend the existing adversarial regex regression harness. No review replies will be posted. The corrected source head will receive fresh release and CodeQL evidence." "2026-10-23T11:46:42.028Z",codex-agent,"CodeQL correction uses the existing backwards punctuation scanner with a citation delimiter set, preserving the default structured-locator behavior. The new real-matcher performance regression uses 50000 exclamation marks followed by a non-punctuation sentinel. Fixed best-of-three local time is 0.172 ms, under the existing 50 ms bound. Isolated behavioral revert restored only citationReferences from ef325ba while retaining the configurable scanner and test: 1 failure, 0 passes/skips, exit 1 at 3670.058 ms. This complements the three-failure ownership revert. Typecheck, test typecheck, lint, the property matrix and scanner parity pass. Full affected suites and fresh exact-head release/CodeQL checks will verify the final correction." "2026-10-23T11:48:41.158Z",codex-agent,"Final local checks after the CodeQL trim correction pass: PM-linked node --test test/cited-issue-adoption.test.ts (25/25) and node --test test/import-regex.test.ts (8/8), with zero failures/skips. The second command is now registered on this item and was run through pm test --only-last --run --progress. Typecheck, test typecheck, build, lint, docstrings, zero duplication, privacy and strict tracker health pass. Performance revert command node --test --test-name-pattern=\"citation URL punctuation trigger\" test/import-regex.test.ts fails 1/1 with only citationReferences from ef325ba restored; fixed best-of-three 0.172 ms versus reverted 3670.058 ms. The earlier three-failure ownership revert remains recorded. The next pushed source head requires fresh npm/Bun release and CodeQL success before handoff." + "2026-10-23T12:25:44.341Z",codex-agent,"Third review follow-up on PR 169: Markdown emphasis can hide a second URL from the strict parser. The remote branch was pulled first and was unchanged. Replace strict-parser exclusion with a deliberately broad raw-text veto for issues/digits, pull/digits and hash-digits substrings in all three prose fields, independent of wrappers or trailing characters. Keep the strict parser and explicit intent for positive ownership, preserve prior safeguards, and add fixed Markdown cases plus a deterministic random-decoration property regression. Over-counting must refuse adoption and return candidate hints. Record a function-only revert proof, then require fresh exact-head release and CodeQL evidence without replying to review threads." + "2026-10-23T12:38:07.314Z",codex-agent,"Raw exclusion regressions pass: 192 fixed decoration/field cases, 4096 seeded nested-Markdown samples across all three fields (12288 property cases), 36 deliberate over-count cases and four repeated-number controls. The earlier 8925-case matrix and all earlier inputs remain; three foreign/malformed URL expectations now refuse their literal hash-31 substrings under the new policy. Mandatory linked commands pass 30/30 adoption tests and 8/8 regex/performance tests. Function-only revert restored citesIssue from 56580ec while retaining the new raw scanner, strict parser, receipts and real persistence: node --test --test-name-pattern=\"raw exclusion\" test/cited-issue-adoption.test.ts fails 5/5, zero passes/skips, exit 1. Fixed cases pass. Read-only saved-snapshot replay with the new rule preserves pm-review pre-tag one-adopt and tagged one-update plans; the five pm-context issues remain two creates/three updates with three incidental hints. All 393 original files retain their hashes. Typecheck, test typecheck and lint pass." + "2026-10-23T12:41:03.214Z",codex-agent,"Local verification is complete for the raw exclusion design: both linked commands pass 38 tests total, and typecheck, test typecheck, build, lint, docstrings, zero duplication, privacy and strict tracker health pass. Scoped coverage inspection found no uncovered branches in the new raw scanner. README and validation evidence now distinguish the broad negative veto from strict positive ownership and explain intentional over-counting. No dependency pins or coverage thresholds changed. The next pushed head requires fresh npm/Bun release gates and CodeQL; the final CI URLs and exact counts will be added to PR 169." files[10]{path,scope}: import-fidelity.ts,project index.ts,project diff --git a/.agents/pm/history/pm-github-cited-issue-adoption.jsonl b/.agents/pm/history/pm-github-cited-issue-adoption.jsonl index 9656bdd9..61e42fa4 100644 --- a/.agents/pm/history/pm-github-cited-issue-adoption.jsonl +++ b/.agents/pm/history/pm-github-cited-issue-adoption.jsonl @@ -45,3 +45,9 @@ {"hash_algorithm":"sha256","ts":"2026-10-23T11:47:54.795Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"tests_add","patch":[{"op":"add","path":"/metadata/tests/1","value":{"command":"node --test test/import-regex.test.ts","scope":"project","timeout_seconds":900,"provenance":{"author":"codex-agent","created_at":"2026-10-23T11:47:54.745Z","source_kind":"local_mutation","source_ref":"feat/cited-issue-adoption"}}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T11:47:54.795Z"}],"before_hash":"e424b3a9812f56e3704810c275f27ed3fe8a8044beb0479c8324fd19feaa3ccc","after_hash":"708ef0b6f7e3583205b086f34b54c058b9a22f97020dea5d3a0f5331e03abea5","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"6d986448235acf1718f940dad18e7d14b174a4aa7796d59269206231a485bb1a"} {"hash_algorithm":"sha256","ts":"2026-10-23T11:48:41.159Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/21","value":{"created_at":"2026-10-23T11:48:41.158Z","author":"codex-agent","text":"Final local checks after the CodeQL trim correction pass: PM-linked node --test test/cited-issue-adoption.test.ts (25/25) and node --test test/import-regex.test.ts (8/8), with zero failures/skips. The second command is now registered on this item and was run through pm test --only-last --run --progress. Typecheck, test typecheck, build, lint, docstrings, zero duplication, privacy and strict tracker health pass. Performance revert command node --test --test-name-pattern=\"citation URL punctuation trigger\" test/import-regex.test.ts fails 1/1 with only citationReferences from ef325ba restored; fixed best-of-three 0.172 ms versus reverted 3670.058 ms. The earlier three-failure ownership revert remains recorded. The next pushed source head requires fresh npm/Bun release and CodeQL success before handoff."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T11:48:41.159Z"}],"before_hash":"708ef0b6f7e3583205b086f34b54c058b9a22f97020dea5d3a0f5331e03abea5","after_hash":"eb74b93a85cf138a77b23f8a83ef11ac2dd54e9c80b67bf94d34fa613a8b6043","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"fc1cff27d9163c902deaa400a7b7d6897e980eca062b49aa72113779a9cfba67"} {"hash_algorithm":"sha256","ts":"2026-10-23T11:48:41.657Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":{"value":"implementer","source":"argv"},"topic":null},"op":"release","patch":[{"op":"remove","path":"/metadata/claim_principal"},{"op":"remove","path":"/metadata/assignee"},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T11:48:41.657Z"}],"before_hash":"eb74b93a85cf138a77b23f8a83ef11ac2dd54e9c80b67bf94d34fa613a8b6043","after_hash":"d1df10c2db2ca6daf0eb89aa60c8d119b954581c974d64f63e8dfd2031a99e08","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"6195e038c0f056ca7c8f7a029fa3e93cfcb4af21b064534fffe2ec2bd9c01328"} +{"hash_algorithm":"sha256","ts":"2026-10-23T12:25:42.298Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":{"value":"implementer","source":"argv"},"topic":null},"op":"claim","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T12:25:42.298Z"},{"op":"add","path":"/metadata/assignee","value":"codex-agent"},{"op":"add","path":"/metadata/claim_principal","value":"codex-agent"}],"before_hash":"d1df10c2db2ca6daf0eb89aa60c8d119b954581c974d64f63e8dfd2031a99e08","after_hash":"5358f2c3452048d56cf177566e62246c1f460fb3628df4c9e9b534f1265445b1","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"1d4fe9699c77e7fb47d09c82d50248b87284beb05b7cdc7a6124815663a38618"} +{"hash_algorithm":"sha256","ts":"2026-10-23T12:25:44.342Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/22","value":{"created_at":"2026-10-23T12:25:44.341Z","author":"codex-agent","text":"Third review follow-up on PR 169: Markdown emphasis can hide a second URL from the strict parser. The remote branch was pulled first and was unchanged. Replace strict-parser exclusion with a deliberately broad raw-text veto for issues/digits, pull/digits and hash-digits substrings in all three prose fields, independent of wrappers or trailing characters. Keep the strict parser and explicit intent for positive ownership, preserve prior safeguards, and add fixed Markdown cases plus a deterministic random-decoration property regression. Over-counting must refuse adoption and return candidate hints. Record a function-only revert proof, then require fresh exact-head release and CodeQL evidence without replying to review threads."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T12:25:44.342Z"}],"before_hash":"5358f2c3452048d56cf177566e62246c1f460fb3628df4c9e9b534f1265445b1","after_hash":"c9ff5c1a504f64995b96c5fd66ba2a8a881106624731aa54ee9ecb9bf11c42b8","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"b0f0af1577d119bc59d07ad1e297a8108ee146924eddea460efc1557209d30ff"} +{"hash_algorithm":"sha256","ts":"2026-10-23T12:32:42.265Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","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/acceptance_criteria","value":"Preserve established tag/URL/provider identity matching. Adopt only an untagged item with exactly one distinct repository-qualified GitHub issue across title, description and body, plus explicit fix/close/resolve intent, Issue:/Tracks:/GH issue labeling, or the reference in its title. Incidental and multi-issue citations never transfer ownership; return citation_candidates IDs and reasons while importing normally. Keep ambiguity refusal for two eligible intent-bearing items. Closed incidental items must not reopen. Test qualified references and punctuation, positive fix/label/title cases, related-only mentions and multi-issue roadmaps, non-git trackers, typed receipts, idempotency and opt-out. Record behavioral revert proofs. Repeat public dry runs including pre-tag pm-review adoption and pm-context counts. Local targeted tests/typecheck/lint and exact-head npm/Bun CI release gates pass.; Raw title, description and body exclusion scans every issues/digits, pull/digits and hash-digits substring without Markdown or trailing-character boundaries and vetoes adoption for any non-target digit string, while strict qualified parsing and explicit intent remain required for a positive match and earlier repository-identity safeguards remain in force"},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T12:32:42.265Z"}],"before_hash":"c9ff5c1a504f64995b96c5fd66ba2a8a881106624731aa54ee9ecb9bf11c42b8","after_hash":"f66cd4efe845d7bd781f94fce73623dd938d59962cb6f9d0eaec4cc58d649c06","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"8c323daf15fe7a4da586e41337f4dacf21926087956bb7d923128a84e9a78b4f"} +{"hash_algorithm":"sha256","ts":"2026-10-23T12:38:07.315Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/23","value":{"created_at":"2026-10-23T12:38:07.314Z","author":"codex-agent","text":"Raw exclusion regressions pass: 192 fixed decoration/field cases, 4096 seeded nested-Markdown samples across all three fields (12288 property cases), 36 deliberate over-count cases and four repeated-number controls. The earlier 8925-case matrix and all earlier inputs remain; three foreign/malformed URL expectations now refuse their literal hash-31 substrings under the new policy. Mandatory linked commands pass 30/30 adoption tests and 8/8 regex/performance tests. Function-only revert restored citesIssue from 56580ec while retaining the new raw scanner, strict parser, receipts and real persistence: node --test --test-name-pattern=\"raw exclusion\" test/cited-issue-adoption.test.ts fails 5/5, zero passes/skips, exit 1. Fixed cases pass. Read-only saved-snapshot replay with the new rule preserves pm-review pre-tag one-adopt and tagged one-update plans; the five pm-context issues remain two creates/three updates with three incidental hints. All 393 original files retain their hashes. Typecheck, test typecheck and lint pass."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T12:38:07.315Z"}],"before_hash":"f66cd4efe845d7bd781f94fce73623dd938d59962cb6f9d0eaec4cc58d649c06","after_hash":"9b411d49d4fc4c5f48af24c96ef1814a2d5a39b878a5ca916faffe3fa78a8bdf","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"516a5425de59679c986adf82e9c39aba293162b668cf672863d2193634d7515f"} +{"hash_algorithm":"sha256","ts":"2026-10-23T12:41:03.214Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/24","value":{"created_at":"2026-10-23T12:41:03.214Z","author":"codex-agent","text":"Local verification is complete for the raw exclusion design: both linked commands pass 38 tests total, and typecheck, test typecheck, build, lint, docstrings, zero duplication, privacy and strict tracker health pass. Scoped coverage inspection found no uncovered branches in the new raw scanner. README and validation evidence now distinguish the broad negative veto from strict positive ownership and explain intentional over-counting. No dependency pins or coverage thresholds changed. The next pushed head requires fresh npm/Bun release gates and CodeQL; the final CI URLs and exact counts will be added to PR 169."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T12:41:03.214Z"}],"before_hash":"9b411d49d4fc4c5f48af24c96ef1814a2d5a39b878a5ca916faffe3fa78a8bdf","after_hash":"9fb702dc9827d839e44963a3743b8c3dfa36869af25842d45560d2a27edd3d62","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"8d5f6bd38c73eec87fda7c1d8f47132d021347b1e4ba2f5ab87513f294620002"} +{"hash_algorithm":"sha256","ts":"2026-10-23T12:41:07.257Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":{"value":"implementer","source":"argv"},"topic":null},"op":"release","patch":[{"op":"remove","path":"/metadata/claim_principal"},{"op":"remove","path":"/metadata/assignee"},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T12:41:07.257Z"}],"before_hash":"9fb702dc9827d839e44963a3743b8c3dfa36869af25842d45560d2a27edd3d62","after_hash":"85b62763f9c09b49e6e3a9c5be2a14ea4a1d026a2f1ed6521614a2343fb1a753","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"0beb45c54ee5111eeedc50b0a62f9e92765dec0bb41c7a3b8fee0df2bb066802"} diff --git a/README.md b/README.md index 4d992b73..3bb906b5 100644 --- a/README.md +++ b/README.md @@ -372,28 +372,36 @@ Each imported item records GitHub provenance: the `gh:owner/repo#N` idempotency Before creating an issue item, import looks for an existing item citing exactly `https://github.com/owner/repo/issues/N` or `owner/repo#N` in its title, description or body. Trailing punctuation such as `.` and `)` is accepted; -bare `#N`, another repository and longer issue numbers never match. Existing +bare `#N`, another repository and longer issue numbers never establish ownership. Existing tag/URL/provider ownership matching takes precedence. Items with an established `gh:` issue tag are excluded from this fallback. Closed and canceled items remain eligible because their fix may already have landed. -Adoption requires **both** of these ownership signals: - -- The combined title, description and body cite exactly one distinct GitHub - issue or PR, counting references to every repository. One parser handles - `owner/repo#N`, issue and `/pull/N` URLs, optional trailing `/`, query strings, - fragments such as `#issuecomment-123`, and trailing prose punctuation. - Bare `#N` uses the imported repository as a hint when counting additional - targets, but cannot establish ownership on its own; neither can a PR URL. - Repeated URLs and shorthand for the same identity count once. A second - issue or PR disqualifies the item even when only one issue is selected. -- The target reference appears in the title, or directly follows explicit intent: +Citation adoption uses a conservative exclusion check followed by a strict +positive ownership check: + +- **Exclusion:** scan the raw title, description and body for every substring + matching `issues/`, `pull/` or `#`. Every digit string + must equal the imported issue's canonical number. No Markdown is stripped, + no repository is required, and no trailing-character boundary is applied. + A non-target number vetoes adoption even inside emphasis, code spans, links, + angle brackets, malformed URLs or unrelated text. This deliberately + over-counts: refusing adoption can leave the usual duplicate import, while + missing a second reference could overwrite another item's content or status. +- **Positive ownership:** the strict parser must identify the imported issue's + qualified reference and find no other repository/issue identity. The reference + must appear in the title, or directly follow explicit intent: `fix`/`fixes`/`fixed`, `close`/`closes`/`closed`, `resolve`/`resolves`/`resolved`, `Issue:`, `Tracks:` or `GH issue`. Keywords and labels ignore case; punctuation and opening Markdown delimiters around the reference are accepted. The closing words follow [GitHub's supported keyword list](https://docs.github.com/en/issues/tracking-your-work-with-issues/using-issues/linking-a-pull-request-to-an-issue). +The strict parser still handles qualified shorthand, issue/PR URLs with trailing +slashes, queries, fragments and punctuation. Bare numbers and PR links cannot +establish issue ownership on their own. Repeated references to the same target +are allowed. Passing the broad check alone never authorizes adoption. + | Existing item prose | Decision for issue 30 | | --- | --- | | Body: `Fixes acme/widgets#30` | Eligible for adoption | @@ -405,6 +413,8 @@ Adoption requires **both** of these ownership signals: | Body: `Fixes acme/widgets#30 and https://github.com/acme/widgets/issues/31/` | Incidental; `multiple_issues` | | Body: `Fixes acme/widgets#30 and #31` (importing `acme/widgets`) | Incidental; `multiple_issues` | | Body: `Fixes acme/widgets#30 and https://github.com/acme/widgets/pull/31#issuecomment-123` | Incidental; `multiple_issues` | +| Body: `Fixes acme/widgets#30 and **https://github.com/acme/widgets/issues/31**` | Incidental; `multiple_issues` | +| Body: `Fixes acme/widgets#30`; unrelated code contains `prefix#31suffix` | Incidental; `multiple_issues` | Incidental mentions never transfer ownership. The issue imports normally into its own item, leaving the incidental item's status, content and fields intact, diff --git a/docs/cited-issue-adoption-validation.md b/docs/cited-issue-adoption-validation.md index fdcafb18..2deb4b41 100644 --- a/docs/cited-issue-adoption-validation.md +++ b/docs/cited-issue-adoption-validation.md @@ -8,6 +8,15 @@ adopted; multiple eligible candidates refuse the batch before any writes and rep with candidate IDs and a tagging remedy. Status push, export and Projects matching retain their existing scope. +Citation adoption now has a separate conservative exclusion check: every raw +`issues/`, `pull/` and `#` substring across the three +fields must use the imported issue's canonical number. Markup, foreign URLs, +code spans and trailing characters never hide a number from that check. +The strict parser and explicit intent still establish positive ownership; +passing the raw veto alone is insufficient. Strict repository identities +continue to disqualify references to another repository even with the same +number. This intentionally prefers a normal create over taking another item. + Incidental references carry `citation_candidates` hints with item IDs and `missing_intent` or `multiple_issues` reasons. They never hand status or fields to the importer; the GitHub issue imports normally into its own item. @@ -101,8 +110,9 @@ changed the package worktree or rebuilt a stale artifact: tests import live TS. | `node --test --test-name-pattern='roadmap never adopts' test/cited-issue-adoption.test.ts` | 2 failures, 0 passes, exit 1 | Both plain/atomic cases pass | `node --test test/cited-issue-adoption.test.ts test/import-regex.test.ts` passes -28 tests with zero failures/skips. The PM-linked adoption command also passes -21 tests. Typecheck, test typecheck, build, lint, docstrings and strict tracker +38 tests with zero failures/skips. The PM-linked adoption command contains +30 tests and the linked regex/performance command contains eight. Typecheck, +test typecheck, build, lint, docstrings and strict tracker health pass locally. Full npm/Bun release-gate evidence is recorded in PR 169 and the final handoff at the exact pushed head; thresholds remain unchanged. @@ -110,14 +120,14 @@ and the final handoff at the exact pushed head; thresholds remain unchanged. The second review found that `Fixes acme/widgets#30 and https://github.com/acme/widgets/issues/31/` could still transfer ownership: -the earlier counting regex skipped the trailing-slash URL. A shared parser now -produces the identities and source positions used by both distinct-target -counting and intent checks. It recognizes qualified shorthand, repository-hinted +the earlier counting regex skipped the trailing-slash URL. A shared strict parser +produces identities and source positions for positive ownership checks. It +recognizes qualified shorthand, repository-hinted bare `#N`, issue and PR URLs, optional trailing slashes, query strings, comment fragments, prose punctuation and Markdown link boundaries. Foreign and malformed -URLs are consumed as URL tokens so their interior hash text cannot masquerade -as a separate reference. Bare numbers and PR URLs count toward additional -targets but cannot establish issue ownership on their own. +URLs are consumed as URL tokens during strict parsing. The independent raw +exclusion deliberately counts their interior number substrings anyway. Bare +numbers and PR URLs cannot establish issue ownership on their own. The property-style regression exercises 17 ownership forms against 35 second reference forms with five punctuation/Markdown wrappers in all three prose @@ -135,8 +145,8 @@ The isolated behavioral revert restores only `citesIssue` from `cce11c4`, leavin the new parser, planner, receipts and real SDK persistence present. Running `node --test --test-name-pattern='every accepted second-reference|trailing-slash second reference' test/cited-issue-adoption.test.ts` then produces **3 failures, 0 passes, 0 skips, exit 1**. The fixed cases pass. -The complete adoption/regex selection passes **33/33**; the linked adoption -command contains **25 tests**. Fresh exact-head npm/Bun release evidence is +The current adoption/regex selection contains **38 tests**; the linked adoption +command contains **30 tests**. Fresh exact-head npm/Bun release evidence is recorded in PR 169 when CI completes, without changing coverage thresholds. CodeQL then identified quadratic backtracking in the new URL punctuation @@ -150,3 +160,43 @@ test but restores the vulnerable regex call: the test then fails at **3,670.058 ms** (one failure, zero passes/skips, exit 1). These timings are local regression evidence, not a deployment benchmark. A fresh pushed head receives new release-gate and CodeQL evidence. + +## Raw exclusion review follow-up + +The third review showed that bold emphasis could hide a second URL from strict +parsing. Exclusion now scans raw text separately: every `issues/`, +`pull/` and `#` substring must equal the imported issue's +canonical number. The scan has no markup or trailing-character boundaries and +does not require a GitHub host or repository. Noncanonical digit spellings can +also veto adoption conservatively. The strict parser and explicit intent remain +the positive ownership check, with the existing repository-identity safeguard. + +Eight fixed decorations (bold, italic, underscore, code span, code fence, angle +brackets, Markdown link and image link) wrap eight reference cores in every +field: **192 fixed cases**. A deterministic xorshift generator with seed +`0x6c8e9cf5` produces **4,096 nested decoration samples**, each checked in title, +description and body: **12,288 property cases**. Another 36 cases require +over-counting unrelated text, foreign/malformed URLs, trailing letters, zero, +noncanonical spelling and very large digit strings. Four repeated-number +controls remain eligible. The earlier 8,925-case shape matrix and all earlier +regression inputs remain present. Three old foreign/malformed-URL expectations +now require refusal because the raw policy intentionally counts their `#31` +substrings; the encoded query case remains unchanged. + +Plain and atomic non-git tracker tests reproduce the exact bold-URL example. +The gated preview plans one create, zero adopts and a `multiple_issues` hint. +Actual imports preserve the entire original open item's SDK projection and +create a separate issue item. + +The behavioral revert restores only `citesIssue` from `56580ec`, retaining the +new raw scanner, strict parser, current tests, receipts and real SDK persistence. +`node --test --test-name-pattern='raw exclusion' test/cited-issue-adoption.test.ts` +then fails **5/5**, with zero passes/skips and exit 1. Fixed linked commands pass +**30/30 adoption tests** and **8/8 regex/performance tests**. + +The saved public tracker snapshots were replayed with this raw exclusion and +live GitHub reads: pm-review pre-tag still plans one adopt; its tagged snapshot +plans one update; the same five pm-context issues plan two creates and three +updates, with three `missing_intent` hints and no adopts/skips. All **393/393 +original files** remain unchanged. Fresh release-gate and CodeQL evidence is +linked from PR 169 at the final pushed head. diff --git a/import-fidelity.ts b/import-fidelity.ts index 703bf6d7..e1b543f7 100644 --- a/import-fidelity.ts +++ b/import-fidelity.ts @@ -72,7 +72,7 @@ export class AmbiguousCitationError extends Error { } } -/** Parse issue/PR identities once, resolving bare numbers only for conservative counting. */ +/** Parse strict issue/PR identities and source positions for positive ownership checks. */ function citationReferences(text: string, repoHint: string): { key: string; index: number; ownsIssue: boolean }[] { const references: { key: string; index: number; ownsIssue: boolean }[] = []; const tokens = text.matchAll(/(?"'`\])}]+|(?:[\w.-]+\/[\w.-]+)?#[1-9]\d*(?![\w/=&%-]|\.\w))/gi); @@ -94,19 +94,32 @@ function citationReferences(text: string, repoHint: string): { key: string; inde return references; } -/** Require one distinct target across all prose fields and a qualified issue reference with explicit intent. */ +/** Veto any raw non-target number, deliberately ignoring markup, repositories and token boundaries. */ +function hasOtherCitationNumber(texts: readonly string[], number: number): boolean { + const expected = String(number); + for (const text of texts) { + for (const reference of text.matchAll(/(?:issues\/|pull\/|#)([0-9]+)/gi)) { + if (reference[1] !== expected) return true; + } + } + return false; +} + +/** Apply the raw-number veto before allowing strict single-issue ownership with explicit intent. */ function citesIssue(item: PmItem, repo: string, number: number): "eligible" | CitationCandidate["reason"] | undefined { const target = `${repo}#${number}`; + const texts = [item.title ?? "", item.description ?? "", item.body ?? ""]; + const otherNumber = hasOtherCitationNumber(texts, number); const identities = new Set(); let intended = false; - for (const [field, text] of [item.title ?? "", item.description ?? "", item.body ?? ""].entries()) { + for (const [field, text] of texts.entries()) { for (const reference of citationReferences(text, repo)) { identities.add(reference.key); if (reference.key === target && reference.ownsIssue && (field === 0 || /(?:\b(?:fix|fixes|fixed|close|closes|closed|resolve|resolves|resolved)\b[\s:([<`*_]*|\b(?:issue|tracks)\s*:[\s([<`*_]*|\bgh\s+issue\b[\s:([<`*_]*)$/i.test(text.slice(0, reference.index)))) intended = true; } } if (!identities.has(target)) return undefined; - return identities.size > 1 ? "multiple_issues" : intended ? "eligible" : "missing_intent"; + return otherNumber || identities.size > 1 ? "multiple_issues" : intended ? "eligible" : "missing_intent"; } /** Trim configurable exporter punctuation in one backwards pass. @internal Shared with parity tests. */ diff --git a/test/cited-issue-adoption.test.ts b/test/cited-issue-adoption.test.ts index 04ec9889..a94c3795 100644 --- a/test/cited-issue-adoption.test.ts +++ b/test/cited-issue-adoption.test.ts @@ -161,8 +161,90 @@ for (const atomic of [false, true]) { assert.deepEqual((await get(local.id, {}, { pmRoot })).item, before); assert.equal((await items(pmRoot)).length, 2); })); + + test(`${atomic ? "atomic" : "plain"} raw exclusion preserves an item with a bold second URL without git`, async () => tracker(async pmRoot => { + const local = (await create({ type: "Task", title: "Protected roadmap", description: `Fixes ${repo}#30 and **https://github.com/${repo}/issues/31**`, + body: "Keep all roadmap context", status: "open", tags: "local" }, { pmRoot })).item; + const before = (await get(local.id, {}, { pmRoot })).item; + const preview = (await importing(pmRoot, { atomic, gate: true, "dry-run": true })).result; + assert.equal(preview.wouldImport, 1); + assert.equal(preview.wouldAdopt, 0); + assert.deepEqual(preview.citation_candidates, [{ issue: 30, id: local.id, reason: "multiple_issues" }]); + assert.equal((preview.plan as { action: string }[])[0]!.action, "create"); + const landed = (await importing(pmRoot, { atomic })).result; + assert.equal(landed.imported, 1); + assert.equal(landed.adopted, 0); + assert.deepEqual(landed.citation_candidates, preview.citation_candidates); + assert.deepEqual((await get(local.id, {}, { pmRoot })).item, before); + assert.equal((await items(pmRoot)).length, 2); + })); } +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", + "issues/31suffix", "pull/31suffix", "prefix#31suffix", "https://foreign.example/anything#31"]; + for (const [left, right] of wrappers) for (const reference of references) { + for (const field of ["title", "description", "body"] as const) { + const local = { id: "local", title: "Local work", description: `Fixes ${repo}#30`, body: "" }; + local[field] += ` and ${left}${reference}${right}`; + const result = matchImportedIssue([local], issue); + assert.equal(result.via, "new", `${field}: ${left}${reference}${right}`); + assert.deepEqual(result.citation_candidates, [{ id: "local", reason: "multiple_issues" }]); + } + } +}); + +test("raw exclusion property rejects second numbers wrapped in seeded random Markdown decorations", t => { + let seed = 0x6c8e9cf5; + /** Reproduce the same generated decoration stream on every supported runtime. */ + const next = (bound: number): number => { + seed ^= seed << 13; + seed ^= seed >>> 17; + seed ^= seed << 5; + return (seed >>> 0) % bound; + }; + const wrappers = [["**", "**"], ["*", "*"], ["__", "__"], ["_", "_"], ["`", "`"], ["```text\n", "\n```"], + ["<", ">"], ["[reference](", ")"], ["![reference](", ")"], ["~~", "~~"], ["\\*", "\\*"], ["> ", "\n"]]; + const tails = ["", "next", ".", ")", "**tail", "_suffix", "/extra", "?query=anything"]; + for (let sample = 0; sample < 4096; sample++) { + const number = 31 + next(10000); + const references = [`https://github.com/${repo}/issues/${number}`, `https://github.com/${repo}/pull/${number}`, `${repo}#${number}`, + `#${number}`, `issues/${number}tail`, `pull/${number}tail`, `prefix#${number}tail`, `https://foreign.example/path#${number}`]; + let decorated = references[next(references.length)]!; + const depth = 1 + next(8); + for (let layer = 0; layer < depth; layer++) { + const [left, right] = wrappers[next(wrappers.length)]!; + decorated = left + decorated + right; + } + decorated += tails[next(tails.length)]; + for (const field of ["title", "description", "body"] as const) { + const local = { id: "local", title: "Local work", description: `Fixes ${repo}#30`, body: "" }; + local[field] += ` and ${decorated}`; + const result = matchImportedIssue([local], issue); + assert.equal(result.via, "new", `seed 0x6c8e9cf5, sample ${sample}, ${field}: ${decorated}`); + assert.deepEqual(result.citation_candidates, [{ id: "local", reason: "multiple_issues" }]); + } + } + t.diagnostic("seed 0x6c8e9cf5: 4096 generated decorations, 12288 field cases"); +}); + +test("raw exclusion deliberately over-counts unrelated substrings and trailing characters", () => { + for (const text of ["issues/31letters", "pull/31/anything", "#31letters", "prefix#31suffix", "nestedissues/31tail", "ISSUES/31tail", + "notgithub/pull/31anything", "https://foreign.example/path#31", "https://[bad#31", "#0", "#030", "#999999999999999999999999999999999999"]) { + for (const field of ["title", "description", "body"] as const) { + const local = { id: "local", title: "Local work", description: `Fixes ${repo}#30`, body: "" }; + local[field] += ` and ${text}`; + const result = matchImportedIssue([local], issue); + assert.equal(result.via, "new", `${field}: ${text}`); + assert.deepEqual(result.citation_candidates, [{ id: "local", reason: "multiple_issues" }]); + } + } + for (const text of ["**issues/30**", "prefix#30suffix", "pull/30/anything", "https://foreign.example/path#30"]) { + assert.equal(matchImportedIssue([{ id: "local", description: `Fixes ${repo}#30 and ${text}` }], issue).via, "citation", text); + } +}); + test("every accepted second-reference shape disqualifies a fix item across every prose field", () => { const suffixes = ["", "/", "#issuecomment-123", "/#issuecomment-123", "?view=all", "/?view=all", "?view=all#issuecomment-123", "/?view=all#issuecomment-123"]; const firstShapes = [`${repo}#30`, ...suffixes.flatMap(suffix => [issue.html_url + suffix, `http://www.github.com/${repo}/issues/30${suffix}`])]; @@ -193,9 +275,12 @@ test("one parser deduplicates equivalent locators without granting ownership to `${issue.html_url}.json`, "https://github.com/other/widgets/issues/30/", "https://other.test/acme/widgets/issues/30/", "https://["]) { assert.equal(matchImportedIssue([{ id: "local", title: `Fixes ${text}` }], issue).via, "new", text); } - for (const embedded of ["https://other.test/path#31", "https://other.test/acme/widgets#31", "https://github.com/search?q=other/widgets%2331", "https://[bad#31"]) { - assert.equal(matchImportedIssue([{ id: "local", description: `Fixes ${repo}#30 and ${embedded}` }], issue).via, "citation", embedded); + for (const embedded of ["https://other.test/path#31", "https://other.test/acme/widgets#31", "https://[bad#31"]) { + const result = matchImportedIssue([{ id: "local", description: `Fixes ${repo}#30 and ${embedded}` }], issue); + assert.equal(result.via, "new", embedded); + assert.deepEqual(result.citation_candidates, [{ id: "local", reason: "multiple_issues" }]); } + assert.equal(matchImportedIssue([{ id: "local", description: `Fixes ${repo}#30 and https://github.com/search?q=other/widgets%2331` }], issue).via, "citation"); }); test("citation matching accepts exact qualified references across prose fields and trailing punctuation", () => { From 0b4bc293aa3403443364b5fa2ef3ff7158073c8e Mon Sep 17 00:00:00 2001 From: SteveBot <1153461+unbraind@users.noreply.github.com> Date: Fri, 23 Oct 2026 15:25:16 +0200 Subject: [PATCH 09/18] Refuse cited issue adoption across repository boundaries --- .../pm-github-cited-issue-adoption.toon | 11 ++- .../pm-github-cited-issue-adoption.jsonl | 8 ++ README.md | 30 ++++-- docs/cited-issue-adoption-validation.md | 72 +++++++++++++-- import-fidelity.ts | 22 +++-- test/cited-issue-adoption.test.ts | 92 ++++++++++++++++++- 6 files changed, 203 insertions(+), 32 deletions(-) diff --git a/.agents/pm/features/pm-github-cited-issue-adoption.toon b/.agents/pm/features/pm-github-cited-issue-adoption.toon index 1e5cc8c8..07c3d901 100644 --- a/.agents/pm/features/pm-github-cited-issue-adoption.toon +++ b/.agents/pm/features/pm-github-cited-issue-adoption.toon @@ -6,10 +6,10 @@ status: in_progress priority: 2 tags[3]: changelog-unreleased,github,import created_at: "2026-10-23T07:16:54.041Z" -updated_at: "2026-10-23T12:41:07.257Z" +updated_at: "2026-10-23T13:25:01.522Z" author: codex-agent -acceptance_criteria: "Preserve established tag/URL/provider identity matching. Adopt only an untagged item with exactly one distinct repository-qualified GitHub issue across title, description and body, plus explicit fix/close/resolve intent, Issue:/Tracks:/GH issue labeling, or the reference in its title. Incidental and multi-issue citations never transfer ownership; return citation_candidates IDs and reasons while importing normally. Keep ambiguity refusal for two eligible intent-bearing items. Closed incidental items must not reopen. Test qualified references and punctuation, positive fix/label/title cases, related-only mentions and multi-issue roadmaps, non-git trackers, typed receipts, idempotency and opt-out. Record behavioral revert proofs. Repeat public dry runs including pre-tag pm-review adoption and pm-context counts. Local targeted tests/typecheck/lint and exact-head npm/Bun CI release gates pass.; Raw title, description and body exclusion scans every issues/digits, pull/digits and hash-digits substring without Markdown or trailing-character boundaries and vetoes adoption for any non-target digit string, while strict qualified parsing and explicit intent remain required for a positive match and earlier repository-identity safeguards remain in force" -comments[25]{created_at,author,text}: +acceptance_criteria: "Preserve established tag/URL/provider identity matching. Adopt only an untagged item with exactly one distinct repository-qualified GitHub issue across title, description and body, plus explicit fix/close/resolve intent, Issue:/Tracks:/GH issue labeling, or the reference in its title. Incidental and multi-issue citations never transfer ownership; return citation_candidates IDs and reasons while importing normally. Keep ambiguity refusal for two eligible intent-bearing items. Closed incidental items must not reopen. Test qualified references and punctuation, positive fix/label/title cases, related-only mentions and multi-issue roadmaps, non-git trackers, typed receipts, idempotency and opt-out. Record behavioral revert proofs. Repeat public dry runs including pre-tag pm-review adoption and pm-context counts. Local targeted tests/typecheck/lint and exact-head npm/Bun CI release gates pass.; Raw exclusion retains repository and canonical number for every GitHub issue or PR path and qualified shorthand regardless of Markdown, vetoes any foreign repository, other number or UNKNOWN reference including bare hashes without verified owning-repository context, and requires at least one target reference plus strict positive ownership and intent" +comments[30]{created_at,author,text}: "2026-10-23T07:17:26.861Z",codex-agent,"Overlap review: pm-github-c093 owns structured URL/provider back-link import matching, pm-github-identity-collisions diagnoses and repairs structured ownership, Projects import matches board and issue provenance, and export upserts linked issues. None adopts arbitrary exact prose citations. This feature adds a fallback only after existing import matching, preserving those paths. Scheduled gated sync already invokes import, so it inherits the default. Plan ambiguous citations before writes and refuse the batch with actionable candidate IDs." "2026-10-23T07:24:01.857Z",codex-agent,"Implementation: strong ownership matching stays first; exact qualified title/description/body citations fall back to a single existing item. All imports now prepare identities before writes, and ambiguous citations expose candidate IDs plus an exact tagging remedy. Adoption retains local tags and reports a separate count; provider-owned records retain link-only behavior. New tests use real trackers with a PATH containing node and the pinned pm CLI but no git, plus real git trackers for write-gate acceptance." "2026-10-23T07:27:45.693Z",codex-agent,"New citation suite: 13/13 passed, including plain/atomic adoption in non-git trackers, write-gate acceptance, retry identity, ambiguity refusal, punctuation, repo/number boundaries, structured ownership precedence and provider link-only preservation. Live pm-review main aa16d855df1f7b74afd3453617f77e92c44d493e already tags pm-review-withdrawn-lineage for issue 30, so an untouched current-main copy correctly plans update rather than adopt. Preserve this current-state result and locate the pre-tag historical tracker for a separate adoption replay. SDK reads may create runtime caches; verify original tracked file bytes independently." @@ -35,6 +35,11 @@ comments[25]{created_at,author,text}: "2026-10-23T12:25:44.341Z",codex-agent,"Third review follow-up on PR 169: Markdown emphasis can hide a second URL from the strict parser. The remote branch was pulled first and was unchanged. Replace strict-parser exclusion with a deliberately broad raw-text veto for issues/digits, pull/digits and hash-digits substrings in all three prose fields, independent of wrappers or trailing characters. Keep the strict parser and explicit intent for positive ownership, preserve prior safeguards, and add fixed Markdown cases plus a deterministic random-decoration property regression. Over-counting must refuse adoption and return candidate hints. Record a function-only revert proof, then require fresh exact-head release and CodeQL evidence without replying to review threads." "2026-10-23T12:38:07.314Z",codex-agent,"Raw exclusion regressions pass: 192 fixed decoration/field cases, 4096 seeded nested-Markdown samples across all three fields (12288 property cases), 36 deliberate over-count cases and four repeated-number controls. The earlier 8925-case matrix and all earlier inputs remain; three foreign/malformed URL expectations now refuse their literal hash-31 substrings under the new policy. Mandatory linked commands pass 30/30 adoption tests and 8/8 regex/performance tests. Function-only revert restored citesIssue from 56580ec while retaining the new raw scanner, strict parser, receipts and real persistence: node --test --test-name-pattern=\"raw exclusion\" test/cited-issue-adoption.test.ts fails 5/5, zero passes/skips, exit 1. Fixed cases pass. Read-only saved-snapshot replay with the new rule preserves pm-review pre-tag one-adopt and tagged one-update plans; the five pm-context issues remain two creates/three updates with three incidental hints. All 393 original files retain their hashes. Typecheck, test typecheck and lint pass." "2026-10-23T12:41:03.214Z",codex-agent,"Local verification is complete for the raw exclusion design: both linked commands pass 38 tests total, and typecheck, test typecheck, build, lint, docstrings, zero duplication, privacy and strict tracker health pass. Scoped coverage inspection found no uncovered branches in the new raw scanner. README and validation evidence now distinguish the broad negative veto from strict positive ownership and explain intentional over-counting. No dependency pins or coverage thresholds changed. The next pushed head requires fresh npm/Bun release gates and CodeQL; the final CI URLs and exact counts will be added to PR 169." + "2026-10-23T13:19:12.986Z",codex-agent,"Fourth review follow-up: replace the number-only raw veto with conservative repository-and-number attribution. Preserve strict positive ownership and intent. Add decorated foreign same-number issue and PR regressions, seeded repository/number/decoration fuzzing, real no-git persistence, a function-only revert proof, public snapshot replays and exact-head npm/Bun CI gates." + "2026-10-23T13:23:58.277Z",codex-agent,"Repository veto implementation preserves qualified repository/number pairs while consuming their embedded fallback once. Every foreign, different-number or UNKNOWN reference refuses adoption with a multiple_issues candidate hint. Bare hashes remain UNKNOWN because the item projection carries no verified owning repository, and the import argument or another citation cannot supply it. Strict positive parsing/intent and established ownership precedence are unchanged. Fixed tests include 144 foreign same-number decoration/field cases, 4096 seeded repository/number/nested-decoration samples checked in all three fields (12288 cases), and plain/atomic no-git imports that preserve a completed local item instead of reopening it. Earlier inputs remain, with five now-UNKNOWN expectations updated to refusal." + "2026-10-23T13:23:58.822Z",codex-agent,"Behavioral revert proof: in an isolated source copy restore only hasOtherCitationNumber and citesIssue from aa86b21, retaining current tests, strict parser, receipts and real SDK persistence. Command: node --test --test-name-pattern='repository veto' test/cited-issue-adoption.test.ts. Reverted: 5 failures, 0 passes, 0 skips, exit 1, including both exact-reproduction persistence tests and fixed/fuzz/UNKNOWN tests. Fixed linked commands: node --test test/cited-issue-adoption.test.ts passes 35/35 and node --test test/import-regex.test.ts passes 8/8, run through pm test --run --progress. No test skips or failures." + "2026-10-23T13:23:59.592Z",codex-agent,"Real-data dry runs re-fetched live GitHub issue/timeline reads against the saved read-only public tracker snapshots. pm-review pre-tag f72250e4c5ee8dfafaf57ee8afe20ddd3cbc993e: 0 create, 0 update, 1 adopt into pm-review-withdrawn-lineage, 0 skip. Tagged pm-review snapshot e1fc83a81507ca6a91f1675b74863c050f1a89f1: 0 create, 1 update, 0 adopt, 0 skip. pm-context snapshot 7d7479ae77c13d01c54b2b4560ad54091ffed788, issues 154,153,136,53,81: 2 creates, 3 updates, 0 adopts/skips. Three candidate hints now use multiple_issues because raw unattributable references veto before intent. All 393/393 original tracker files retain identical SHA-256 hashes." + "2026-10-23T13:25:00.390Z",codex-agent,"Local verification complete: targeted commands pass 35/35 and 8/8 with zero failures/skips, including the final qualified-repeat control. Typecheck, build:test, build, lint, docstring (17 files, 187 declarations), duplication (18 modules, zero duplicated lines), privacy, strict tracker health and diff checks pass. Scoped coverage preflight found no uncovered branches in the changed veto functions. README and validation evidence document conservative UNKNOWN handling, fixed/property counts, function-only revert proof and public replay. Releasing the claim while leaving the item in_progress for orchestrator verification. Full npm/Bun release-gate counts and run links will be reported on PR 169 at the final pushed head after completion." files[10]{path,scope}: import-fidelity.ts,project index.ts,project diff --git a/.agents/pm/history/pm-github-cited-issue-adoption.jsonl b/.agents/pm/history/pm-github-cited-issue-adoption.jsonl index 61e42fa4..5a61acf2 100644 --- a/.agents/pm/history/pm-github-cited-issue-adoption.jsonl +++ b/.agents/pm/history/pm-github-cited-issue-adoption.jsonl @@ -51,3 +51,11 @@ {"hash_algorithm":"sha256","ts":"2026-10-23T12:38:07.315Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/23","value":{"created_at":"2026-10-23T12:38:07.314Z","author":"codex-agent","text":"Raw exclusion regressions pass: 192 fixed decoration/field cases, 4096 seeded nested-Markdown samples across all three fields (12288 property cases), 36 deliberate over-count cases and four repeated-number controls. The earlier 8925-case matrix and all earlier inputs remain; three foreign/malformed URL expectations now refuse their literal hash-31 substrings under the new policy. Mandatory linked commands pass 30/30 adoption tests and 8/8 regex/performance tests. Function-only revert restored citesIssue from 56580ec while retaining the new raw scanner, strict parser, receipts and real persistence: node --test --test-name-pattern=\"raw exclusion\" test/cited-issue-adoption.test.ts fails 5/5, zero passes/skips, exit 1. Fixed cases pass. Read-only saved-snapshot replay with the new rule preserves pm-review pre-tag one-adopt and tagged one-update plans; the five pm-context issues remain two creates/three updates with three incidental hints. All 393 original files retain their hashes. Typecheck, test typecheck and lint pass."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T12:38:07.315Z"}],"before_hash":"f66cd4efe845d7bd781f94fce73623dd938d59962cb6f9d0eaec4cc58d649c06","after_hash":"9b411d49d4fc4c5f48af24c96ef1814a2d5a39b878a5ca916faffe3fa78a8bdf","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"516a5425de59679c986adf82e9c39aba293162b668cf672863d2193634d7515f"} {"hash_algorithm":"sha256","ts":"2026-10-23T12:41:03.214Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/24","value":{"created_at":"2026-10-23T12:41:03.214Z","author":"codex-agent","text":"Local verification is complete for the raw exclusion design: both linked commands pass 38 tests total, and typecheck, test typecheck, build, lint, docstrings, zero duplication, privacy and strict tracker health pass. Scoped coverage inspection found no uncovered branches in the new raw scanner. README and validation evidence now distinguish the broad negative veto from strict positive ownership and explain intentional over-counting. No dependency pins or coverage thresholds changed. The next pushed head requires fresh npm/Bun release gates and CodeQL; the final CI URLs and exact counts will be added to PR 169."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T12:41:03.214Z"}],"before_hash":"9b411d49d4fc4c5f48af24c96ef1814a2d5a39b878a5ca916faffe3fa78a8bdf","after_hash":"9fb702dc9827d839e44963a3743b8c3dfa36869af25842d45560d2a27edd3d62","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"8d5f6bd38c73eec87fda7c1d8f47132d021347b1e4ba2f5ab87513f294620002"} {"hash_algorithm":"sha256","ts":"2026-10-23T12:41:07.257Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":{"value":"implementer","source":"argv"},"topic":null},"op":"release","patch":[{"op":"remove","path":"/metadata/claim_principal"},{"op":"remove","path":"/metadata/assignee"},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T12:41:07.257Z"}],"before_hash":"9fb702dc9827d839e44963a3743b8c3dfa36869af25842d45560d2a27edd3d62","after_hash":"85b62763f9c09b49e6e3a9c5be2a14ea4a1d026a2f1ed6521614a2343fb1a753","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"0beb45c54ee5111eeedc50b0a62f9e92765dec0bb41c7a3b8fee0df2bb066802"} +{"hash_algorithm":"sha256","ts":"2026-10-23T13:19:12.425Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":{"value":"implementer","source":"argv"},"topic":null},"op":"claim","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T13:19:12.425Z"},{"op":"add","path":"/metadata/assignee","value":"codex-agent"},{"op":"add","path":"/metadata/claim_principal","value":"codex-agent"}],"before_hash":"85b62763f9c09b49e6e3a9c5be2a14ea4a1d026a2f1ed6521614a2343fb1a753","after_hash":"555f3876c78405b1a5d79d9f1a6cc0564fda3f879eca24c9c323ad01aed3e9a6","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"0ac1c2d1d0d430838ce10b15382dee7374fc3ea82f215844c341899fb503ef9c"} +{"hash_algorithm":"sha256","ts":"2026-10-23T13:19:12.987Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/25","value":{"created_at":"2026-10-23T13:19:12.986Z","author":"codex-agent","text":"Fourth review follow-up: replace the number-only raw veto with conservative repository-and-number attribution. Preserve strict positive ownership and intent. Add decorated foreign same-number issue and PR regressions, seeded repository/number/decoration fuzzing, real no-git persistence, a function-only revert proof, public snapshot replays and exact-head npm/Bun CI gates."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T13:19:12.987Z"}],"before_hash":"555f3876c78405b1a5d79d9f1a6cc0564fda3f879eca24c9c323ad01aed3e9a6","after_hash":"065deb31bd498a58d17e4fb41511db52fcdb2e3bba8d458020e0a163fdbf9350","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"ba4f29ce12b6355f092c1348f7321e48cc2649eab4d4af53196cc96f90f9b1df"} +{"hash_algorithm":"sha256","ts":"2026-10-23T13:23:57.689Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","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/acceptance_criteria","value":"Preserve established tag/URL/provider identity matching. Adopt only an untagged item with exactly one distinct repository-qualified GitHub issue across title, description and body, plus explicit fix/close/resolve intent, Issue:/Tracks:/GH issue labeling, or the reference in its title. Incidental and multi-issue citations never transfer ownership; return citation_candidates IDs and reasons while importing normally. Keep ambiguity refusal for two eligible intent-bearing items. Closed incidental items must not reopen. Test qualified references and punctuation, positive fix/label/title cases, related-only mentions and multi-issue roadmaps, non-git trackers, typed receipts, idempotency and opt-out. Record behavioral revert proofs. Repeat public dry runs including pre-tag pm-review adoption and pm-context counts. Local targeted tests/typecheck/lint and exact-head npm/Bun CI release gates pass.; Raw exclusion retains repository and canonical number for every GitHub issue or PR path and qualified shorthand regardless of Markdown, vetoes any foreign repository, other number or UNKNOWN reference including bare hashes without verified owning-repository context, and requires at least one target reference plus strict positive ownership and intent"},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T13:23:57.689Z"}],"before_hash":"065deb31bd498a58d17e4fb41511db52fcdb2e3bba8d458020e0a163fdbf9350","after_hash":"128c63e7d09dd799273fd99be722482659f0874da2b9e72a5a20016c97626c9f","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"386a4e2773a89c3001bd0e3a2fbe4e5d9b5bd03cff999751af7df4d5e791db05"} +{"hash_algorithm":"sha256","ts":"2026-10-23T13:23:58.277Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/26","value":{"created_at":"2026-10-23T13:23:58.277Z","author":"codex-agent","text":"Repository veto implementation preserves qualified repository/number pairs while consuming their embedded fallback once. Every foreign, different-number or UNKNOWN reference refuses adoption with a multiple_issues candidate hint. Bare hashes remain UNKNOWN because the item projection carries no verified owning repository, and the import argument or another citation cannot supply it. Strict positive parsing/intent and established ownership precedence are unchanged. Fixed tests include 144 foreign same-number decoration/field cases, 4096 seeded repository/number/nested-decoration samples checked in all three fields (12288 cases), and plain/atomic no-git imports that preserve a completed local item instead of reopening it. Earlier inputs remain, with five now-UNKNOWN expectations updated to refusal."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T13:23:58.277Z"}],"before_hash":"128c63e7d09dd799273fd99be722482659f0874da2b9e72a5a20016c97626c9f","after_hash":"2bbf64512000ee1a00d7cba8e204a561e606d8510903b1a0a8d2c5eaf6e75903","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"f115b74a47af47f48661fb390847e2f493b5beb0b5120941b6b0892666350248"} +{"hash_algorithm":"sha256","ts":"2026-10-23T13:23:58.823Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/27","value":{"created_at":"2026-10-23T13:23:58.822Z","author":"codex-agent","text":"Behavioral revert proof: in an isolated source copy restore only hasOtherCitationNumber and citesIssue from aa86b21, retaining current tests, strict parser, receipts and real SDK persistence. Command: node --test --test-name-pattern='repository veto' test/cited-issue-adoption.test.ts. Reverted: 5 failures, 0 passes, 0 skips, exit 1, including both exact-reproduction persistence tests and fixed/fuzz/UNKNOWN tests. Fixed linked commands: node --test test/cited-issue-adoption.test.ts passes 35/35 and node --test test/import-regex.test.ts passes 8/8, run through pm test --run --progress. No test skips or failures."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T13:23:58.823Z"}],"before_hash":"2bbf64512000ee1a00d7cba8e204a561e606d8510903b1a0a8d2c5eaf6e75903","after_hash":"5db6cbe9dc6e6b0d51c5350e3853477cc3880318bcc8267b6bd415ffd802f78a","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"00aedf83d1033abe6861f070b01cfe26126584a5312cda0dbf2d57be2e4127d7"} +{"hash_algorithm":"sha256","ts":"2026-10-23T13:23:59.592Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/28","value":{"created_at":"2026-10-23T13:23:59.592Z","author":"codex-agent","text":"Real-data dry runs re-fetched live GitHub issue/timeline reads against the saved read-only public tracker snapshots. pm-review pre-tag f72250e4c5ee8dfafaf57ee8afe20ddd3cbc993e: 0 create, 0 update, 1 adopt into pm-review-withdrawn-lineage, 0 skip. Tagged pm-review snapshot e1fc83a81507ca6a91f1675b74863c050f1a89f1: 0 create, 1 update, 0 adopt, 0 skip. pm-context snapshot 7d7479ae77c13d01c54b2b4560ad54091ffed788, issues 154,153,136,53,81: 2 creates, 3 updates, 0 adopts/skips. Three candidate hints now use multiple_issues because raw unattributable references veto before intent. All 393/393 original tracker files retain identical SHA-256 hashes."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T13:23:59.592Z"}],"before_hash":"5db6cbe9dc6e6b0d51c5350e3853477cc3880318bcc8267b6bd415ffd802f78a","after_hash":"906eb2be0d67f8a23d9f904a4d39611f61c90a88d604a437af156a3c0152c7f2","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"757818b2e6a1ebef4244b5024eb6aff9f7927f9cfd3a44744ca7dfa41a4cfa1d"} +{"hash_algorithm":"sha256","ts":"2026-10-23T13:25:00.390Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/29","value":{"created_at":"2026-10-23T13:25:00.390Z","author":"codex-agent","text":"Local verification complete: targeted commands pass 35/35 and 8/8 with zero failures/skips, including the final qualified-repeat control. Typecheck, build:test, build, lint, docstring (17 files, 187 declarations), duplication (18 modules, zero duplicated lines), privacy, strict tracker health and diff checks pass. Scoped coverage preflight found no uncovered branches in the changed veto functions. README and validation evidence document conservative UNKNOWN handling, fixed/property counts, function-only revert proof and public replay. Releasing the claim while leaving the item in_progress for orchestrator verification. Full npm/Bun release-gate counts and run links will be reported on PR 169 at the final pushed head after completion."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T13:25:00.390Z"}],"before_hash":"906eb2be0d67f8a23d9f904a4d39611f61c90a88d604a437af156a3c0152c7f2","after_hash":"f7234ec7b2971fe48fbf8eeccf0e962974aea354251049f5c22f914708352f20","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"a20633b5684522e20109c60dcc7f756c0c91a245adc0bd6fa5110a95f4120fe9"} +{"hash_algorithm":"sha256","ts":"2026-10-23T13:25:01.522Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":{"value":"implementer","source":"argv"},"topic":null},"op":"release","patch":[{"op":"remove","path":"/metadata/claim_principal"},{"op":"remove","path":"/metadata/assignee"},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T13:25:01.522Z"}],"before_hash":"f7234ec7b2971fe48fbf8eeccf0e962974aea354251049f5c22f914708352f20","after_hash":"4cd900fd842408b85ad44b6e88f50914b5e4898c7676e6c6e62feb24420baba0","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"8e1bd3605752cc7adff53695a1f0a5b0589db71d478db1d0d33e978a48b9e598"} diff --git a/README.md b/README.md index 3bb906b5..2b8e3041 100644 --- a/README.md +++ b/README.md @@ -380,14 +380,21 @@ eligible because their fix may already have landed. Citation adoption uses a conservative exclusion check followed by a strict positive ownership check: -- **Exclusion:** scan the raw title, description and body for every substring - matching `issues/`, `pull/` or `#`. Every digit string - must equal the imported issue's canonical number. No Markdown is stripped, - no repository is required, and no trailing-character boundary is applied. - A non-target number vetoes adoption even inside emphasis, code spans, links, - angle brackets, malformed URLs or unrelated text. This deliberately - over-counts: refusing adoption can leave the usual duplicate import, while - missing a second reference could overwrite another item's content or status. +- **Exclusion:** scan the raw title, description and body for repository-qualified + `github.com/owner/repo/issues/`, `github.com/owner/repo/pull/` + and `owner/repo#` references, plus unattributed `issues/`, + `pull/` and `#` substrings. Consume a qualified reference as + one reference, retaining its repository (case-insensitive) and canonical + number. Every reference must name the importing repository and target number; + any foreign repository, different number or UNKNOWN repository vetoes adoption. + No Markdown is stripped and no trailing-character boundary is applied, so + emphasis, code spans, links and angle brackets cannot hide another repository + even when its issue number is identical. Bare hashes are UNKNOWN: the item + projection has no verified owning-repository context, and the import argument + or another prose citation does not supply it. They cannot be assumed to name + the target. This deliberately over-counts: refusing adoption can leave the + usual duplicate import, while missing another reference could overwrite + another item's content or status. - **Positive ownership:** the strict parser must identify the imported issue's qualified reference and find no other repository/issue identity. The reference must appear in the title, or directly follow explicit intent: @@ -400,7 +407,9 @@ positive ownership check: The strict parser still handles qualified shorthand, issue/PR URLs with trailing slashes, queries, fragments and punctuation. Bare numbers and PR links cannot establish issue ownership on their own. Repeated references to the same target -are allowed. Passing the broad check alone never authorizes adoption. +are allowed when repository-qualified. A PR link with the same repository and +number counts as that same reference, while a foreign PR link vetoes adoption. +Passing the broad check alone never authorizes adoption. | Existing item prose | Decision for issue 30 | | --- | --- | @@ -415,6 +424,9 @@ are allowed. Passing the broad check alone never authorizes adoption. | Body: `Fixes acme/widgets#30 and https://github.com/acme/widgets/pull/31#issuecomment-123` | Incidental; `multiple_issues` | | Body: `Fixes acme/widgets#30 and **https://github.com/acme/widgets/issues/31**` | Incidental; `multiple_issues` | | Body: `Fixes acme/widgets#30`; unrelated code contains `prefix#31suffix` | Incidental; `multiple_issues` | +| Body: `Fixes acme/widgets#30 and **https://github.com/other/widgets/issues/30**` | Incidental; foreign repository, `multiple_issues` | +| Body: `Fixes acme/widgets#30 and **https://github.com/other/widgets/pull/30**` | Incidental; foreign repository, `multiple_issues` | +| Body: `Fixes acme/widgets#30 and #30` | Incidental; UNKNOWN repository, `multiple_issues` | Incidental mentions never transfer ownership. The issue imports normally into its own item, leaving the incidental item's status, content and fields intact, diff --git a/docs/cited-issue-adoption-validation.md b/docs/cited-issue-adoption-validation.md index 2deb4b41..89f267e4 100644 --- a/docs/cited-issue-adoption-validation.md +++ b/docs/cited-issue-adoption-validation.md @@ -8,14 +8,16 @@ adopted; multiple eligible candidates refuse the batch before any writes and rep with candidate IDs and a tagging remedy. Status push, export and Projects matching retain their existing scope. -Citation adoption now has a separate conservative exclusion check: every raw -`issues/`, `pull/` and `#` substring across the three -fields must use the imported issue's canonical number. Markup, foreign URLs, -code spans and trailing characters never hide a number from that check. -The strict parser and explicit intent still establish positive ownership; -passing the raw veto alone is insufficient. Strict repository identities -continue to disqualify references to another repository even with the same -number. This intentionally prefers a normal create over taking another item. +Citation adoption now has a separate conservative exclusion check. Every raw +reference must carry the importing repository and canonical number. The scan +collects GitHub issue/PR paths and qualified shorthand regardless of Markdown; +unqualified `issues/`, `pull/` and `#` have UNKNOWN +repository and veto adoption. The item projection carries no verified owning +repository, so the import argument cannot assign bare hashes to this repository. +Foreign references with the same number also veto adoption. The strict parser +and explicit intent still establish positive ownership; passing the raw veto +alone is insufficient. This intentionally prefers a normal create over taking +another item. Incidental references carry `citation_candidates` hints with item IDs and `missing_intent` or `multiple_issues` reasons. They never hand status or fields @@ -178,7 +180,8 @@ field: **192 fixed cases**. A deterministic xorshift generator with seed description and body: **12,288 property cases**. Another 36 cases require over-counting unrelated text, foreign/malformed URLs, trailing letters, zero, noncanonical spelling and very large digit strings. Four repeated-number -controls remain eligible. The earlier 8,925-case shape matrix and all earlier +controls were eligible under that historical number-only rule; the fourth +review below changes them to UNKNOWN-repository refusals. The earlier 8,925-case shape matrix and all earlier regression inputs remain present. Three old foreign/malformed-URL expectations now require refusal because the raw policy intentionally counts their `#31` substrings; the encoded query case remains unchanged. @@ -200,3 +203,54 @@ plans one update; the same five pm-context issues plan two creates and three updates, with three `missing_intent` hints and no adopts/skips. All **393/393 original files** remain unchanged. Fresh release-gate and CodeQL evidence is linked from PR 169 at the final pushed head. + +## Repository-aware raw exclusion + +The fourth review reproduced a foreign reference hidden in bold with the same +number as the target: `Fixes acme/widgets#30 and +**https://github.com/other/widgets/issues/30**`. A number-only veto could not +protect that item. Raw exclusion now retains each qualified reference's +repository and canonical number. GitHub issue/PR URL substrings and qualified +shorthand are consumed before their embedded fallback, so they are counted +once. Every collected reference must equal the target repository and number; +foreign repositories, other numbers and UNKNOWN repositories all veto adoption. +The raw path/hash fallback is retained even for malformed or unrelated text. +No Markdown normalization or trailing-character boundary is used. + +The item projection carries no verified owning repository. Bare hashes therefore +remain UNKNOWN, even beside a qualified target citation. The import repository +is not proof of the item's repository. Four historical number-only controls and +the prior bare-hash deduplication input now require refusal; their inputs remain +in the suite. Qualified repeated references, case-insensitive repository names +and same-repository/same-number PR links remain allowed by exclusion, while the +strict positive parser and explicit issue ownership intent remain necessary. + +New fixed coverage checks four foreign same-number issue/PR/shorthand references +inside all twelve decoration pairs from the property test, in title, +description and body: **144 cases**. A deterministic xorshift generator seeded +with `0x78b3e2d1` creates **4,096 random repository/number/nested-decoration +samples**, each checked in all three fields: **12,288 cases**. Every generated +second reference has a foreign repository, another number or UNKNOWN attribution +and must refuse adoption. The earlier shape/property matrices remain present. +Plain and atomic no-git imports test the exact bold-foreign-URL reproduction +against a closed local item and an open issue. Both preview a create and +`multiple_issues` candidate hint, then create a separate item without reopening +or changing the completed local item. + +Behavioral revert: restore only `hasOtherCitationNumber` and `citesIssue` from +`aa86b21` in an isolated source copy, keeping current tests, strict parsing, +receipts and real SDK persistence. Run +`node --test --test-name-pattern='repository veto' test/cited-issue-adoption.test.ts`. +The revert gives **5 failures, 0 passes, 0 skips, exit 1**. Fixed linked commands +pass **35/35 adoption tests** and **8/8 regex/performance tests**. Typecheck, +TypeScript builds, lint, docstrings and duplication checks pass locally; release +gates on both npm and Bun are checked in CI at the final pushed head. + +Public replay with this rule still plans the pre-tag pm-review adopt into +`pm-review-withdrawn-lineage`; its tagged snapshot plans one update. The same +five pm-context issues still plan two creates and three updates, with no +adopts/skips. The three candidate hints now report `multiple_issues` because raw +UNKNOWN references veto before intent. All **393/393 original tracker files** +retain their SHA-256 hashes. These are the saved main/pre-tag revisions listed +above, replayed with live GitHub issue/timeline reads; no current-main claim is +made for the replay. Exact-head CI run links and counts are in PR 169. diff --git a/import-fidelity.ts b/import-fidelity.ts index e1b543f7..f25157bb 100644 --- a/import-fidelity.ts +++ b/import-fidelity.ts @@ -94,22 +94,28 @@ function citationReferences(text: string, repoHint: string): { key: string; inde return references; } -/** Veto any raw non-target number, deliberately ignoring markup, repositories and token boundaries. */ -function hasOtherCitationNumber(texts: readonly string[], number: number): boolean { +/** Require every raw reference to name the target repository and canonical number, ignoring markup. */ +function hasOtherCitationReference(texts: readonly string[], repo: string, number: number): boolean { const expected = String(number); + let found = false; for (const text of texts) { - for (const reference of text.matchAll(/(?:issues\/|pull\/|#)([0-9]+)/gi)) { - if (reference[1] !== expected) return true; + // Consume qualified forms before their embedded path/hash fallback. Bare hashes + // remain UNKNOWN: this projection carries no verified owning-repository context. + for (const reference of text.matchAll(/github\.com\/([\w.-]+\/[\w.-]+)\/(?:issues|pull)\/([0-9]+)|(?(); let intended = false; for (const [field, text] of texts.entries()) { @@ -119,7 +125,7 @@ function citesIssue(item: PmItem, repo: string, number: number): "eligible" | Ci } } if (!identities.has(target)) return undefined; - return otherNumber || identities.size > 1 ? "multiple_issues" : intended ? "eligible" : "missing_intent"; + return otherReference || identities.size > 1 ? "multiple_issues" : intended ? "eligible" : "missing_intent"; } /** Trim configurable exporter punctuation in one backwards pass. @internal Shared with parity tests. */ diff --git a/test/cited-issue-adoption.test.ts b/test/cited-issue-adoption.test.ts index a94c3795..c341d0a3 100644 --- a/test/cited-issue-adoption.test.ts +++ b/test/cited-issue-adoption.test.ts @@ -178,6 +178,25 @@ for (const atomic of [false, true]) { assert.deepEqual((await get(local.id, {}, { pmRoot })).item, before); assert.equal((await items(pmRoot)).length, 2); })); + + test(`${atomic ? "atomic" : "plain"} repository veto preserves the exact foreign same-number bold URL reproduction without git`, async () => tracker(async pmRoot => { + const local = (await create({ type: "Task", title: "Protected work", description: "Fixes acme/widgets#30 and **https://github.com/other/widgets/issues/30**", + status: "closed", closeReason: "Independent completed work", body: "Preserve local context", tags: "local" }, { pmRoot })).item; + const before = (await get(local.id, {}, { pmRoot })).item; + const opened = { ...issue, state: "open" as const, state_reason: "reopened" as const, closed_at: null }; + const preview = (await importing(pmRoot, { atomic, gate: true, "dry-run": true }, [opened])).result; + assert.equal(preview.wouldAdopt, 0); + assert.equal(preview.wouldImport, 1); + assert.deepEqual(preview.citation_candidates, [{ issue: 30, id: local.id, reason: "multiple_issues" }]); + assert.deepEqual((await get(local.id, {}, { pmRoot })).item, before); + const landed = (await importing(pmRoot, { atomic }, [opened])).result; + assert.equal(landed.adopted, 0); + assert.equal(landed.imported, 1); + assert.deepEqual(landed.citation_candidates, preview.citation_candidates); + assert.deepEqual((await get(local.id, {}, { pmRoot })).item, before); + assert.equal((await items(pmRoot)).length, 2); + })); + } test("raw exclusion rejects Markdown-wrapped second numbers across every prose field", () => { @@ -241,7 +260,72 @@ test("raw exclusion deliberately over-counts unrelated substrings and trailing c } } for (const text of ["**issues/30**", "prefix#30suffix", "pull/30/anything", "https://foreign.example/path#30"]) { - assert.equal(matchImportedIssue([{ id: "local", description: `Fixes ${repo}#30 and ${text}` }], issue).via, "citation", text); + assert.equal(matchImportedIssue([{ id: "local", description: `Fixes ${repo}#30 and ${text}` }], issue).via, "new", text); + } +}); + +test("repository veto rejects foreign same-number issues and PRs in every property decoration", () => { + const wrappers = [["**", "**"], ["*", "*"], ["__", "__"], ["_", "_"], ["`", "`"], ["```text\n", "\n```"], + ["<", ">"], ["[reference](", ")"], ["![reference](", ")"], ["~~", "~~"], ["\\*", "\\*"], ["> ", "\n"]]; + const references = ["https://github.com/other/widgets/issues/30", "https://github.com/other/widgets/pull/30", + "http://www.github.com/ACME/other/issues/30/?query=all#issuecomment-99", "other/widgets#30"]; + for (const [left, right] of wrappers) for (const reference of references) { + for (const field of ["title", "description", "body"] as const) { + const local = { id: "local", title: "Local work", description: `Fixes ${repo}#30`, body: "" }; + local[field] += ` and ${left}${reference}${right}`; + const result = matchImportedIssue([local], issue); + assert.equal(result.via, "new", `${field}: ${left}${reference}${right}`); + assert.deepEqual(result.citation_candidates, [{ id: "local", reason: "multiple_issues" }]); + } + } +}); + +test("repository veto fuzzes random repositories, numbers and nested Markdown decorations", t => { + let seed = 0x78b3e2d1; + /** Keep generated repository and decoration combinations reproducible across runtimes. */ + const next = (bound: number): number => { + seed ^= seed << 13; + seed ^= seed >>> 17; + seed ^= seed << 5; + return (seed >>> 0) % bound; + }; + const wrappers = [["**", "**"], ["*", "*"], ["__", "__"], ["_", "_"], ["`", "`"], ["```text\n", "\n```"], + ["<", ">"], ["[reference](", ")"], ["![reference](", ")"], ["~~", "~~"], ["\\*", "\\*"], ["> ", "\n"]]; + const tails = ["", "/", ".", ")", "**tail", "suffix", "?query=all", "#issuecomment-99"]; + for (let sample = 0; sample < 4096; sample++) { + const sourceRepo = next(2) === 0 ? repo : `owner-${next(10000)}/repo-${next(10000)}`; + const number = sourceRepo === repo ? 31 + next(10000) : next(2) === 0 ? 30 : 31 + next(10000); + const references = [`https://github.com/${sourceRepo}/issues/${number}`, `http://www.github.com/${sourceRepo}/pull/${number}`, + `${sourceRepo}#${number}`, `#${number}`, `issues/${number}`, `pull/${number}`]; + let decorated = references[next(references.length)]!; + const depth = 1 + next(8); + for (let layer = 0; layer < depth; layer++) { + const [left, right] = wrappers[next(wrappers.length)]!; + decorated = left + decorated + right; + } + decorated += tails[next(tails.length)]; + for (const field of ["title", "description", "body"] as const) { + const local = { id: "local", title: "Local work", description: `Fixes ${repo}#30`, body: "" }; + local[field] += ` and ${decorated}`; + const result = matchImportedIssue([local], issue); + assert.equal(result.via, "new", `seed 0x78b3e2d1, sample ${sample}, ${sourceRepo}#${number}, ${field}: ${decorated}`); + assert.deepEqual(result.citation_candidates, [{ id: "local", reason: "multiple_issues" }]); + } + } + t.diagnostic("seed 0x78b3e2d1: 4096 repository/number/decoration samples, 12288 field cases"); +}); + +test("repository veto refuses UNKNOWN references and retains qualified same-target controls", () => { + for (const reference of ["#30", "**#30**", "issues/30", "pull/30", "prefix#30suffix", "https://foreign.example/path#30", + "https://github.com/other/widgets/issues/30", "https://github.com/other/widgets/pull/30", "other/widgets#30", + "https://github.com//widgets/issues/30", "https://github.com/acme//issues/30"]) { + const result = matchImportedIssue([{ id: "local", description: `Fixes ${repo}#30 and ${reference}` }], issue); + assert.equal(result.via, "new", reference); + assert.deepEqual(result.citation_candidates, [{ id: "local", reason: "multiple_issues" }]); + } + for (const reference of [`${repo}#30`, "ACME/Widgets#30", `**https://github.com/${repo}/issues/30**`, + `https://github.com/${repo}/pull/30`, `http://www.github.com/${repo}/issues/30/?query=all#issuecomment-123`]) { + assert.equal(matchImportedIssue([{ id: "local", description: `Fixes ${repo}#30 and ${reference}` }], issue).via, "citation", reference); } }); @@ -267,10 +351,12 @@ test("every accepted second-reference shape disqualifies a fix item across every } }); -test("one parser deduplicates equivalent locators without granting ownership to bare numbers or PR links", () => { +test("strict parsing deduplicates qualified locators without granting ownership to bare numbers or PR links", () => { const local = { id: "local", title: `Fixes ${repo}#30`, description: `Issue: ${issue.html_url}/?view=all#issuecomment-123`, body: `Same target #30 and ${issue.html_url}#issuecomment-456` }; - assert.equal(matchImportedIssue([local], issue).item, local); + assert.equal(matchImportedIssue([local], issue).via, "new"); + assert.deepEqual(matchImportedIssue([local], issue).citation_candidates, [{ id: "local", reason: "multiple_issues" }]); + assert.equal(matchImportedIssue([{ ...local, body: `Same target ${repo}#30 and ${issue.html_url}#issuecomment-456` }], issue).via, "citation"); for (const text of ["#30", `https://github.com/${repo}/pull/30/`, `https://github.com/${repo}/issues/30/extra`, `${issue.html_url}.json`, "https://github.com/other/widgets/issues/30/", "https://other.test/acme/widgets/issues/30/", "https://["]) { assert.equal(matchImportedIssue([{ id: "local", title: `Fixes ${text}` }], issue).via, "new", text); From 6adb23491cde9f9d190c24701cdb049e7e6a7084 Mon Sep 17 00:00:00 2001 From: SteveBot <1153461+unbraind@users.noreply.github.com> Date: Fri, 23 Oct 2026 16:25:50 +0200 Subject: [PATCH 10/18] Require explicit opt-in for normalized cited issue adoption --- .../pm-github-cited-issue-adoption.toon | 59 ++- .../pm-github-cited-issue-adoption.jsonl | 15 + .github/workflows/pm-github-sync.yml | 24 +- CHANGELOG.md | 2 +- README.md | 154 ++++---- docs/cited-issue-adoption-validation.md | 356 ++++++------------ docs/sync-workflow-caller.yml | 1 + import-fidelity.ts | 115 ++++-- index.ts | 10 +- package-lock.json | 15 + package.json | 3 + test/cited-issue-adoption.test.ts | 166 +++++++- test/import-fidelity.test.ts | 2 +- test/import-gate.test.ts | 36 ++ test/import-regex.test.ts | 3 +- test/import-selection.test.ts | 9 +- 16 files changed, 577 insertions(+), 393 deletions(-) diff --git a/.agents/pm/features/pm-github-cited-issue-adoption.toon b/.agents/pm/features/pm-github-cited-issue-adoption.toon index 07c3d901..4ae4cc67 100644 --- a/.agents/pm/features/pm-github-cited-issue-adoption.toon +++ b/.agents/pm/features/pm-github-cited-issue-adoption.toon @@ -1,15 +1,15 @@ id: pm-github-cited-issue-adoption -title: Adopt an existing item that cites the imported GitHub issue -description: "Prevent gated imports from duplicating existing prose-citing work. Add exact citation adoption after established identity matching, refuse ambiguous citations, expose receipts and an opt-out, and validate dry runs against public trackers." +title: Require opt-in before adopting cited GitHub issue items +description: "Keep normal import creates and structured identity matching by default while reporting read-only citation candidates. Require explicit opt-in, bounded text normalization and anchored ownership checks before reusing a cited item. Document provenance tags at item creation as primary prevention, and validate encoded references, no-git persistence, workflow propagation and public dry runs." type: Feature status: in_progress priority: 2 tags[3]: changelog-unreleased,github,import created_at: "2026-10-23T07:16:54.041Z" -updated_at: "2026-10-23T13:25:01.522Z" +updated_at: "2026-10-23T14:25:02.020Z" author: codex-agent -acceptance_criteria: "Preserve established tag/URL/provider identity matching. Adopt only an untagged item with exactly one distinct repository-qualified GitHub issue across title, description and body, plus explicit fix/close/resolve intent, Issue:/Tracks:/GH issue labeling, or the reference in its title. Incidental and multi-issue citations never transfer ownership; return citation_candidates IDs and reasons while importing normally. Keep ambiguity refusal for two eligible intent-bearing items. Closed incidental items must not reopen. Test qualified references and punctuation, positive fix/label/title cases, related-only mentions and multi-issue roadmaps, non-git trackers, typed receipts, idempotency and opt-out. Record behavioral revert proofs. Repeat public dry runs including pre-tag pm-review adoption and pm-context counts. Local targeted tests/typecheck/lint and exact-head npm/Bun CI release gates pass.; Raw exclusion retains repository and canonical number for every GitHub issue or PR path and qualified shorthand regardless of Markdown, vetoes any foreign repository, other number or UNKNOWN reference including bare hashes without verified owning-repository context, and requires at least one target reference plus strict positive ownership and intent" -comments[30]{created_at,author,text}: +acceptance_criteria: "Adoption is off by default across CLI, SDK and reusable sync, enabled only with --adopt-cited, adoptCited true or adopt-cited true respectively. Default import retains normal create/structured-match behavior and read-only citation_candidates without fallback ambiguity refusal. Document creating items with gh:owner/repo#N as primary duplicate prevention; Normalize HTML named/numeric entities and percent bytes repeatedly until stable with a bounded refusal, apply NFKC and remove invisible/bidi controls, unwrap Markdown links/reference definitions and join soft breaks before anchored ownership checks while retaining source for conservative exclusion; Only title qualified references, leading description closing-keyword references, or whole body Issue:/Tracks: URL lines can authorize opt-in adoption. Any other normalized GitHub URL, foreign repository, other number or unattributed reference refuses adoption and reports candidate hints. Preserve structured identity precedence, single-owner upsert, eligible-owner ambiguity refusal, receipts, idempotency and non-git support; Keep all review reproductions and prior shape inputs, add encoded-reference fuzzing and explicit pm-review Issue label positives/default creates, record behavioral function/workflow reverts and public dry-run receipts, run targeted checks and wait for exact-head npm/Bun CI release gates" +comments[37]{created_at,author,text}: "2026-10-23T07:17:26.861Z",codex-agent,"Overlap review: pm-github-c093 owns structured URL/provider back-link import matching, pm-github-identity-collisions diagnoses and repairs structured ownership, Projects import matches board and issue provenance, and export upserts linked issues. None adopts arbitrary exact prose citations. This feature adds a fallback only after existing import matching, preserving those paths. Scheduled gated sync already invokes import, so it inherits the default. Plan ambiguous citations before writes and refuse the batch with actionable candidate IDs." "2026-10-23T07:24:01.857Z",codex-agent,"Implementation: strong ownership matching stays first; exact qualified title/description/body citations fall back to a single existing item. All imports now prepare identities before writes, and ambiguous citations expose candidate IDs plus an exact tagging remedy. Adoption retains local tags and reports a separate count; provider-owned records retain link-only behavior. New tests use real trackers with a PATH containing node and the pinned pm CLI but no git, plus real git trackers for write-gate acceptance." "2026-10-23T07:27:45.693Z",codex-agent,"New citation suite: 13/13 passed, including plain/atomic adoption in non-git trackers, write-gate acceptance, retry identity, ambiguity refusal, punctuation, repo/number boundaries, structured ownership precedence and provider link-only preservation. Live pm-review main aa16d855df1f7b74afd3453617f77e92c44d493e already tags pm-review-withdrawn-lineage for issue 30, so an untouched current-main copy correctly plans update rather than adopt. Preserve this current-state result and locate the pre-tag historical tracker for a separate adoption replay. SDK reads may create runtime caches; verify original tracked file bytes independently." @@ -40,9 +40,19 @@ comments[30]{created_at,author,text}: "2026-10-23T13:23:58.822Z",codex-agent,"Behavioral revert proof: in an isolated source copy restore only hasOtherCitationNumber and citesIssue from aa86b21, retaining current tests, strict parser, receipts and real SDK persistence. Command: node --test --test-name-pattern='repository veto' test/cited-issue-adoption.test.ts. Reverted: 5 failures, 0 passes, 0 skips, exit 1, including both exact-reproduction persistence tests and fixed/fuzz/UNKNOWN tests. Fixed linked commands: node --test test/cited-issue-adoption.test.ts passes 35/35 and node --test test/import-regex.test.ts passes 8/8, run through pm test --run --progress. No test skips or failures." "2026-10-23T13:23:59.592Z",codex-agent,"Real-data dry runs re-fetched live GitHub issue/timeline reads against the saved read-only public tracker snapshots. pm-review pre-tag f72250e4c5ee8dfafaf57ee8afe20ddd3cbc993e: 0 create, 0 update, 1 adopt into pm-review-withdrawn-lineage, 0 skip. Tagged pm-review snapshot e1fc83a81507ca6a91f1675b74863c050f1a89f1: 0 create, 1 update, 0 adopt, 0 skip. pm-context snapshot 7d7479ae77c13d01c54b2b4560ad54091ffed788, issues 154,153,136,53,81: 2 creates, 3 updates, 0 adopts/skips. Three candidate hints now use multiple_issues because raw unattributable references veto before intent. All 393/393 original tracker files retain identical SHA-256 hashes." "2026-10-23T13:25:00.390Z",codex-agent,"Local verification complete: targeted commands pass 35/35 and 8/8 with zero failures/skips, including the final qualified-repeat control. Typecheck, build:test, build, lint, docstring (17 files, 187 declarations), duplication (18 modules, zero duplicated lines), privacy, strict tracker health and diff checks pass. Scoped coverage preflight found no uncovered branches in the changed veto functions. README and validation evidence document conservative UNKNOWN handling, fixed/property counts, function-only revert proof and public replay. Releasing the claim while leaving the item in_progress for orchestrator verification. Full npm/Bun release-gate counts and run links will be reported on PR 169 at the final pushed head after completion." -files[10]{path,scope}: + "2026-10-23T13:58:03.276Z",codex-agent,"Fifth review changes the design: adoption becomes explicit opt-in across CLI, SDK and reusable workflow. Default imports retain create behavior with read-only citation hints. Normalize encoded and formatted text before conservative exclusion and anchored positive ownership. Keep earlier reproductions, add encoded-reference and seeded encoding fuzz regressions, verify the pre-tag public case with explicit opt-in, record behavioral reverts, and use exact-head CI npm/Bun gates." + "2026-10-23T14:20:47.429Z",codex-agent,"Fifth-round implementation: CLI --adopt-cited, SDK adoptCited true and reusable input adopt-cited true are required. Defaults are false and preserve creates with read-only candidate hints, including competing eligible owners. A pinned entities 8.1.0 runtime decoder handles the full named/numeric HTML table and nested percent bytes over at most eight passes, with NFKC and default-ignorable control removal. Anchored title/leading-description/body-label ownership is separate from conservative normalized source/destination/soft-line exclusion. Malformed bytes and exhausted normalization refuse adoption. Tag items gh:owner/repo#N at creation as primary prevention. Source and link labels are retained for exclusion so unwrapping cannot hide another issue. Existing structured matching is unchanged." + "2026-10-23T14:23:46.657Z",codex-agent,"Behavioral reverts from 0b4bc29 in an isolated source copy: restoring only matchImportedIssue makes node --test --test-name-pattern='explicit opt-in (false|defaults)' test/cited-issue-adoption.test.ts fail 3/3, zero passes/skips, exit 1. Restoring only citesIssue plus prior hasOtherCitationReference makes node --test --test-name-pattern='normalized allowlist' test/cited-issue-adoption.test.ts fail 4/4, zero passes/skips, exit 1. Tests retain current normalization helpers, CLI options, receipts and real SDK persistence. All fixed selections pass." + "2026-10-23T14:23:47.306Z",codex-agent,"Workflow behavioral revert restores only .github/workflows/pm-github-sync.yml from 0b4bc29 while packing a real copy of current compiled output and the pinned CLI/SDK. Command: node --test --test-name-pattern='reusable workflow.*(explicit opt-in|adoption input)' test/import-gate.test.ts. Reverted: 2 failures, 2 passes, 0 skips, exit 1. Opt-in true imports rather than adopting and the false-default input is absent, while omitted/false executions still pass. Fixed: 4/4. An initial scratch packaging attempt omitted symlinked dist and was discarded as infrastructure failure before this real compiled-copy proof." + "2026-10-23T14:23:47.925Z",codex-agent,"Validation complete: all four pm-linked commands passed through pm test --run --progress: adoption 44/44, regex/performance 8/8, executable workflow 4/4, packed CLI dispatch 1/1. Additional selector/fidelity sweep passed 80/80. All prior review inputs remain, with explicit opt-in and anchored-policy refusal expectations. New seed 0x53ce71a9 generates 4096 repository/number/decoration/encoding samples checked in all three fields (12288 cases). Default-off modes preserve local fields/status; closed incidental items remain closed. Scoped coverage found no uncovered new-helper branches." + "2026-10-23T14:23:48.538Z",codex-agent,"Public dry runs replay saved read-only tracker snapshots with live issue/timeline data in both modes. pm-review pre-tag f72250e4c5ee8dfafaf57ee8afe20ddd3cbc993e: default 1 create with adoption_disabled hint, opt-in 1 adopt into pm-review-withdrawn-lineage. Tagged e1fc83a81507ca6a91f1675b74863c050f1a89f1: 1 update in both. pm-context 7d7479ae77c13d01c54b2b4560ad54091ffed788, issues 154,153,136,53,81: 2 creates and 3 updates with 3 multiple_issues hints in both. Zero skips, all393 original files retain SHA-256 hashes in both modes. README and reusable caller docs emphasize tag-at-creation and default-off opt-in policy." + "2026-10-23T14:25:00.965Z",codex-agent,"Final local checks pass: typecheck, build:test, build, lint, docstrings, zero duplication across 18 authored modules, privacy, strict tracker health and diff checks. All four linked test commands passed with zero failures/skips. Releasing the claim and leaving the item in_progress for orchestrator verification. Full npm/Bun CI release-gate counts and run links will be recorded in PR 169 after completion at the pushed head. No dependency pins were bumped, and no coverage thresholds changed." +files[13]{path,scope}: + .github/workflows/pm-github-sync.yml,project import-fidelity.ts,project index.ts,project + package-lock.json,project + package.json,project test/atomic.test.ts,project test/cited-issue-adoption.test.ts,project test/dryrun-preview.test.ts,project @@ -51,10 +61,39 @@ files[10]{path,scope}: test/import-gate.test.ts,project test/import-regex.test.ts,project test/import-selection.test.ts,project -tests[2]{command,scope,timeout_seconds,provenance{author,created_at,source_kind,source_ref}}: - node --test test/cited-issue-adoption.test.ts,project,900,codex-agent,"2026-10-23T07:35:49.405Z",local_mutation,feat/cited-issue-adoption - node --test test/import-regex.test.ts,project,900,codex-agent,"2026-10-23T11:47:54.745Z",local_mutation,feat/cited-issue-adoption -docs[2]{path,scope}: +tests[4]: + - command: node --test test/cited-issue-adoption.test.ts + scope: project + timeout_seconds: 900 + provenance: + author: codex-agent + created_at: "2026-10-23T07:35:49.405Z" + source_kind: local_mutation + source_ref: feat/cited-issue-adoption + - command: node --test test/import-regex.test.ts + scope: project + timeout_seconds: 900 + provenance: + author: codex-agent + created_at: "2026-10-23T11:47:54.745Z" + source_kind: local_mutation + source_ref: feat/cited-issue-adoption + - command: node --test --test-name-pattern='reusable workflow.*(explicit opt-in|adoption input)' test/import-gate.test.ts + scope: project + provenance: + author: codex-agent + created_at: "2026-10-23T14:20:46.067Z" + source_kind: local_mutation + source_ref: feat/cited-issue-adoption + - command: node --test --test-name-pattern='built package dispatch' test/import-selection.test.ts + scope: project + provenance: + author: codex-agent + created_at: "2026-10-23T14:20:46.727Z" + source_kind: local_mutation + source_ref: feat/cited-issue-adoption +docs[3]{path,scope}: docs/cited-issue-adoption-validation.md,project + docs/sync-workflow-caller.yml,project README.md,project body: "" diff --git a/.agents/pm/history/pm-github-cited-issue-adoption.jsonl b/.agents/pm/history/pm-github-cited-issue-adoption.jsonl index 5a61acf2..c9b306af 100644 --- a/.agents/pm/history/pm-github-cited-issue-adoption.jsonl +++ b/.agents/pm/history/pm-github-cited-issue-adoption.jsonl @@ -59,3 +59,18 @@ {"hash_algorithm":"sha256","ts":"2026-10-23T13:23:59.592Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/28","value":{"created_at":"2026-10-23T13:23:59.592Z","author":"codex-agent","text":"Real-data dry runs re-fetched live GitHub issue/timeline reads against the saved read-only public tracker snapshots. pm-review pre-tag f72250e4c5ee8dfafaf57ee8afe20ddd3cbc993e: 0 create, 0 update, 1 adopt into pm-review-withdrawn-lineage, 0 skip. Tagged pm-review snapshot e1fc83a81507ca6a91f1675b74863c050f1a89f1: 0 create, 1 update, 0 adopt, 0 skip. pm-context snapshot 7d7479ae77c13d01c54b2b4560ad54091ffed788, issues 154,153,136,53,81: 2 creates, 3 updates, 0 adopts/skips. Three candidate hints now use multiple_issues because raw unattributable references veto before intent. All 393/393 original tracker files retain identical SHA-256 hashes."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T13:23:59.592Z"}],"before_hash":"5db6cbe9dc6e6b0d51c5350e3853477cc3880318bcc8267b6bd415ffd802f78a","after_hash":"906eb2be0d67f8a23d9f904a4d39611f61c90a88d604a437af156a3c0152c7f2","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"757818b2e6a1ebef4244b5024eb6aff9f7927f9cfd3a44744ca7dfa41a4cfa1d"} {"hash_algorithm":"sha256","ts":"2026-10-23T13:25:00.390Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/29","value":{"created_at":"2026-10-23T13:25:00.390Z","author":"codex-agent","text":"Local verification complete: targeted commands pass 35/35 and 8/8 with zero failures/skips, including the final qualified-repeat control. Typecheck, build:test, build, lint, docstring (17 files, 187 declarations), duplication (18 modules, zero duplicated lines), privacy, strict tracker health and diff checks pass. Scoped coverage preflight found no uncovered branches in the changed veto functions. README and validation evidence document conservative UNKNOWN handling, fixed/property counts, function-only revert proof and public replay. Releasing the claim while leaving the item in_progress for orchestrator verification. Full npm/Bun release-gate counts and run links will be reported on PR 169 at the final pushed head after completion."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T13:25:00.390Z"}],"before_hash":"906eb2be0d67f8a23d9f904a4d39611f61c90a88d604a437af156a3c0152c7f2","after_hash":"f7234ec7b2971fe48fbf8eeccf0e962974aea354251049f5c22f914708352f20","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"a20633b5684522e20109c60dcc7f756c0c91a245adc0bd6fa5110a95f4120fe9"} {"hash_algorithm":"sha256","ts":"2026-10-23T13:25:01.522Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":{"value":"implementer","source":"argv"},"topic":null},"op":"release","patch":[{"op":"remove","path":"/metadata/claim_principal"},{"op":"remove","path":"/metadata/assignee"},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T13:25:01.522Z"}],"before_hash":"f7234ec7b2971fe48fbf8eeccf0e962974aea354251049f5c22f914708352f20","after_hash":"4cd900fd842408b85ad44b6e88f50914b5e4898c7676e6c6e62feb24420baba0","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"8e1bd3605752cc7adff53695a1f0a5b0589db71d478db1d0d33e978a48b9e598"} +{"hash_algorithm":"sha256","ts":"2026-10-23T13:58:02.591Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":{"value":"implementer","source":"argv"},"topic":null},"op":"claim","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T13:58:02.591Z"},{"op":"add","path":"/metadata/assignee","value":"codex-agent"},{"op":"add","path":"/metadata/claim_principal","value":"codex-agent"}],"before_hash":"4cd900fd842408b85ad44b6e88f50914b5e4898c7676e6c6e62feb24420baba0","after_hash":"d205cd931c1c32724bbe8c286af139ca6604d4d0a52d615f157dc8154a621020","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"1e7beee1a3780ca6679bf352c2052db3215d76b3a39e7b51e1a07aad99aca8b1"} +{"hash_algorithm":"sha256","ts":"2026-10-23T13:58:03.277Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/30","value":{"created_at":"2026-10-23T13:58:03.276Z","author":"codex-agent","text":"Fifth review changes the design: adoption becomes explicit opt-in across CLI, SDK and reusable workflow. Default imports retain create behavior with read-only citation hints. Normalize encoded and formatted text before conservative exclusion and anchored positive ownership. Keep earlier reproductions, add encoded-reference and seeded encoding fuzz regressions, verify the pre-tag public case with explicit opt-in, record behavioral reverts, and use exact-head CI npm/Bun gates."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T13:58:03.277Z"}],"before_hash":"d205cd931c1c32724bbe8c286af139ca6604d4d0a52d615f157dc8154a621020","after_hash":"0e18ce1fbfc10d048afce7985b12bb85dff1f56e437a00f99922b7d6424d94b8","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"c7f671ba9a470a9e76cf21b1dba17e28b642dee3d96d870d4748a598efc0065a"} +{"hash_algorithm":"sha256","ts":"2026-10-23T14:20:43.832Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","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/acceptance_criteria","value":"Adoption is off by default across CLI, SDK and reusable sync, enabled only with --adopt-cited, adoptCited true or adopt-cited true respectively. Default import retains normal create/structured-match behavior and read-only citation_candidates without fallback ambiguity refusal. Document creating items with gh:owner/repo#N as primary duplicate prevention; Normalize HTML named/numeric entities and percent bytes repeatedly until stable with a bounded refusal, apply NFKC and remove invisible/bidi controls, unwrap Markdown links/reference definitions and join soft breaks before anchored ownership checks while retaining source for conservative exclusion; Only title qualified references, leading description closing-keyword references, or whole body Issue:/Tracks: URL lines can authorize opt-in adoption. Any other normalized GitHub URL, foreign repository, other number or unattributed reference refuses adoption and reports candidate hints. Preserve structured identity precedence, single-owner upsert, eligible-owner ambiguity refusal, receipts, idempotency and non-git support; Keep all review reproductions and prior shape inputs, add encoded-reference fuzzing and explicit pm-review Issue label positives/default creates, record behavioral function/workflow reverts and public dry-run receipts, run targeted checks and wait for exact-head npm/Bun CI release gates"},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T14:20:43.832Z"}],"before_hash":"0e18ce1fbfc10d048afce7985b12bb85dff1f56e437a00f99922b7d6424d94b8","after_hash":"3cf18444274c111ae80990be5d9d83bb24dc11b11236f72a265910017e1f19eb","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"ea6141e26d180244ad60f01a84f21c9cef8c944269a9a343e0b58f4267d35df0"} +{"hash_algorithm":"sha256","ts":"2026-10-23T14:20:44.539Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"files_add","patch":[{"op":"replace","path":"/metadata/files/9/path","value":"test/import-fidelity.test.ts"},{"op":"replace","path":"/metadata/files/8/path","value":"test/import-errors.test.ts"},{"op":"replace","path":"/metadata/files/7/path","value":"test/dryrun-preview.test.ts"},{"op":"replace","path":"/metadata/files/6/path","value":"test/cited-issue-adoption.test.ts"},{"op":"replace","path":"/metadata/files/5/path","value":"test/atomic.test.ts"},{"op":"replace","path":"/metadata/files/4/path","value":"package.json"},{"op":"replace","path":"/metadata/files/3/path","value":"package-lock.json"},{"op":"replace","path":"/metadata/files/2/path","value":"index.ts"},{"op":"replace","path":"/metadata/files/1/path","value":"import-fidelity.ts"},{"op":"replace","path":"/metadata/files/0/path","value":".github/workflows/pm-github-sync.yml"},{"op":"add","path":"/metadata/files/10","value":{"path":"test/import-gate.test.ts","scope":"project"}},{"op":"add","path":"/metadata/files/11","value":{"path":"test/import-regex.test.ts","scope":"project"}},{"op":"add","path":"/metadata/files/12","value":{"path":"test/import-selection.test.ts","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T14:20:44.539Z"}],"before_hash":"3cf18444274c111ae80990be5d9d83bb24dc11b11236f72a265910017e1f19eb","after_hash":"3baa31311dad62f2f3eaeb17ff0df063e8c3cf139d4189ea0b9965a5d32a1dc7","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"c8c9af235f54c2972af3892d532e89f5eaff0e2d2981d6ac93e5613390adfd80"} +{"hash_algorithm":"sha256","ts":"2026-10-23T14:20:45.297Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","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/1/path","value":"docs/sync-workflow-caller.yml"},{"op":"add","path":"/metadata/docs/2","value":{"path":"README.md","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T14:20:45.297Z"}],"before_hash":"3baa31311dad62f2f3eaeb17ff0df063e8c3cf139d4189ea0b9965a5d32a1dc7","after_hash":"847caf72ea688b21cc5a6cfc6d1975277f77b63c982a08e164119dc9a7bebd12","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"197fd661bf6e1fd40d349dbd5c24a374bfa86fb65a3837805f29882dce47204f"} +{"hash_algorithm":"sha256","ts":"2026-10-23T14:20:46.128Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"tests_add","patch":[{"op":"add","path":"/metadata/tests/2","value":{"command":"node --test --test-name-pattern='reusable workflow.*(explicit opt-in|adoption input)' test/import-gate.test.ts","scope":"project","provenance":{"author":"codex-agent","created_at":"2026-10-23T14:20:46.067Z","source_kind":"local_mutation","source_ref":"feat/cited-issue-adoption"}}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T14:20:46.128Z"}],"before_hash":"847caf72ea688b21cc5a6cfc6d1975277f77b63c982a08e164119dc9a7bebd12","after_hash":"0bda561c0cd4f1a1305fd95cd49cd6c9057a485799a0183895b2473f3122742d","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"290ad4b7f73a6c5e1f6474fa712273a801d26578fa97f057295e9e6b6237f347"} +{"hash_algorithm":"sha256","ts":"2026-10-23T14:20:46.799Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"tests_add","patch":[{"op":"add","path":"/metadata/tests/3","value":{"command":"node --test --test-name-pattern='built package dispatch' test/import-selection.test.ts","scope":"project","provenance":{"author":"codex-agent","created_at":"2026-10-23T14:20:46.727Z","source_kind":"local_mutation","source_ref":"feat/cited-issue-adoption"}}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T14:20:46.799Z"}],"before_hash":"0bda561c0cd4f1a1305fd95cd49cd6c9057a485799a0183895b2473f3122742d","after_hash":"16aeffd19613969a2757cb605facd6df065158242d376f970f2b80b55dd6a5a3","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"22a04b2f613e91325db8972a3955c0883116a24f8111a9e85f16bc4346db7841"} +{"hash_algorithm":"sha256","ts":"2026-10-23T14:20:47.429Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/31","value":{"created_at":"2026-10-23T14:20:47.429Z","author":"codex-agent","text":"Fifth-round implementation: CLI --adopt-cited, SDK adoptCited true and reusable input adopt-cited true are required. Defaults are false and preserve creates with read-only candidate hints, including competing eligible owners. A pinned entities 8.1.0 runtime decoder handles the full named/numeric HTML table and nested percent bytes over at most eight passes, with NFKC and default-ignorable control removal. Anchored title/leading-description/body-label ownership is separate from conservative normalized source/destination/soft-line exclusion. Malformed bytes and exhausted normalization refuse adoption. Tag items gh:owner/repo#N at creation as primary prevention. Source and link labels are retained for exclusion so unwrapping cannot hide another issue. Existing structured matching is unchanged."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T14:20:47.429Z"}],"before_hash":"16aeffd19613969a2757cb605facd6df065158242d376f970f2b80b55dd6a5a3","after_hash":"c08395fc7784bed5f083b632a87329bd359aa922e74d12cce82453812e876fbe","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"cdba276ee99864a0ca8b4f34a3daf9926e498a7e204511df09b946836987c0b5"} +{"hash_algorithm":"sha256","ts":"2026-10-23T14:23:46.658Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/32","value":{"created_at":"2026-10-23T14:23:46.657Z","author":"codex-agent","text":"Behavioral reverts from 0b4bc29 in an isolated source copy: restoring only matchImportedIssue makes node --test --test-name-pattern='explicit opt-in (false|defaults)' test/cited-issue-adoption.test.ts fail 3/3, zero passes/skips, exit 1. Restoring only citesIssue plus prior hasOtherCitationReference makes node --test --test-name-pattern='normalized allowlist' test/cited-issue-adoption.test.ts fail 4/4, zero passes/skips, exit 1. Tests retain current normalization helpers, CLI options, receipts and real SDK persistence. All fixed selections pass."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T14:23:46.658Z"}],"before_hash":"c08395fc7784bed5f083b632a87329bd359aa922e74d12cce82453812e876fbe","after_hash":"717fb4c990d7c30aee2330cfd994c467910ef55759f8c5199475c769df9a87e6","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"be57ad5eeced75ff028fd65981b800be55b26b3ac90ad97daf09729ca385d9a2"} +{"hash_algorithm":"sha256","ts":"2026-10-23T14:23:47.307Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/33","value":{"created_at":"2026-10-23T14:23:47.306Z","author":"codex-agent","text":"Workflow behavioral revert restores only .github/workflows/pm-github-sync.yml from 0b4bc29 while packing a real copy of current compiled output and the pinned CLI/SDK. Command: node --test --test-name-pattern='reusable workflow.*(explicit opt-in|adoption input)' test/import-gate.test.ts. Reverted: 2 failures, 2 passes, 0 skips, exit 1. Opt-in true imports rather than adopting and the false-default input is absent, while omitted/false executions still pass. Fixed: 4/4. An initial scratch packaging attempt omitted symlinked dist and was discarded as infrastructure failure before this real compiled-copy proof."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T14:23:47.307Z"}],"before_hash":"717fb4c990d7c30aee2330cfd994c467910ef55759f8c5199475c769df9a87e6","after_hash":"85a105893e03ebb34d36996e6c88db7b0b07b6203843fc463931a03dcee316b0","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"c5a0dd14bceeee0bd52c8907fd234fd03fc2fa1eba2af0a1cca4259c097e5cbe"} +{"hash_algorithm":"sha256","ts":"2026-10-23T14:23:47.925Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/34","value":{"created_at":"2026-10-23T14:23:47.925Z","author":"codex-agent","text":"Validation complete: all four pm-linked commands passed through pm test --run --progress: adoption 44/44, regex/performance 8/8, executable workflow 4/4, packed CLI dispatch 1/1. Additional selector/fidelity sweep passed 80/80. All prior review inputs remain, with explicit opt-in and anchored-policy refusal expectations. New seed 0x53ce71a9 generates 4096 repository/number/decoration/encoding samples checked in all three fields (12288 cases). Default-off modes preserve local fields/status; closed incidental items remain closed. Scoped coverage found no uncovered new-helper branches."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T14:23:47.925Z"}],"before_hash":"85a105893e03ebb34d36996e6c88db7b0b07b6203843fc463931a03dcee316b0","after_hash":"c0fa1dc1f724698ac0d01a7a36f47caa177c8322dee4f5ee9cf9c8dcbc6f071b","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"dcb4eea6ab2e8615c548a828975a2653794c4d1563c22471a03060f42b4a510f"} +{"hash_algorithm":"sha256","ts":"2026-10-23T14:23:48.538Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/35","value":{"created_at":"2026-10-23T14:23:48.538Z","author":"codex-agent","text":"Public dry runs replay saved read-only tracker snapshots with live issue/timeline data in both modes. pm-review pre-tag f72250e4c5ee8dfafaf57ee8afe20ddd3cbc993e: default 1 create with adoption_disabled hint, opt-in 1 adopt into pm-review-withdrawn-lineage. Tagged e1fc83a81507ca6a91f1675b74863c050f1a89f1: 1 update in both. pm-context 7d7479ae77c13d01c54b2b4560ad54091ffed788, issues 154,153,136,53,81: 2 creates and 3 updates with 3 multiple_issues hints in both. Zero skips, all393 original files retain SHA-256 hashes in both modes. README and reusable caller docs emphasize tag-at-creation and default-off opt-in policy."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T14:23:48.538Z"}],"before_hash":"c0fa1dc1f724698ac0d01a7a36f47caa177c8322dee4f5ee9cf9c8dcbc6f071b","after_hash":"d59c7004ed67adad5af1ca415026120e8804492b5766a66d53caa4f5642005f3","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"8d28f7aa5080b07b6c02bfbe4dcd705144da7a7e8bb41d95804e9a32b16f766e"} +{"hash_algorithm":"sha256","ts":"2026-10-23T14:24:59.806Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":{"value":"implementer","source":"argv"},"topic":null},"op":"update","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T14:24:59.806Z"},{"op":"replace","path":"/metadata/description","value":"Keep normal import creates and structured identity matching by default while reporting read-only citation candidates. Require explicit opt-in, bounded text normalization and anchored ownership checks before reusing a cited item. Document provenance tags at item creation as primary prevention, and validate encoded references, no-git persistence, workflow propagation and public dry runs."},{"op":"replace","path":"/metadata/title","value":"Require opt-in before adopting cited GitHub issue items"}],"before_hash":"d59c7004ed67adad5af1ca415026120e8804492b5766a66d53caa4f5642005f3","after_hash":"26994279418d0574b586d316f37e7a8ef3f5357ef40c65ae2fa1b64c2bea1fe4","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"192405f05826b99abb7e760a7e8afe629cd6e26d36968cfd82c9796616bfdeda"} +{"hash_algorithm":"sha256","ts":"2026-10-23T14:25:00.966Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/36","value":{"created_at":"2026-10-23T14:25:00.965Z","author":"codex-agent","text":"Final local checks pass: typecheck, build:test, build, lint, docstrings, zero duplication across 18 authored modules, privacy, strict tracker health and diff checks. All four linked test commands passed with zero failures/skips. Releasing the claim and leaving the item in_progress for orchestrator verification. Full npm/Bun CI release-gate counts and run links will be recorded in PR 169 after completion at the pushed head. No dependency pins were bumped, and no coverage thresholds changed."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T14:25:00.966Z"}],"before_hash":"26994279418d0574b586d316f37e7a8ef3f5357ef40c65ae2fa1b64c2bea1fe4","after_hash":"fff1b815f777d065c58a3105bb3a386eddac4580446983ab5c67403ec89d0afe","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"750373fd281efe29e200ca35ad9fc2410116a2fb5913fc9e265afc45f216b514"} +{"hash_algorithm":"sha256","ts":"2026-10-23T14:25:02.020Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":{"value":"implementer","source":"argv"},"topic":null},"op":"release","patch":[{"op":"remove","path":"/metadata/claim_principal"},{"op":"remove","path":"/metadata/assignee"},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T14:25:02.020Z"}],"before_hash":"fff1b815f777d065c58a3105bb3a386eddac4580446983ab5c67403ec89d0afe","after_hash":"d40bfcafd910c162bb29ee088273bfebcbf883e876c995e7258a02d228ae1894","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"000e7a949fed18738be676f2f113b771cfe07b66a5e72c8f3ca4b898420c0c0f"} diff --git a/.github/workflows/pm-github-sync.yml b/.github/workflows/pm-github-sync.yml index cad7def7..781c59d8 100644 --- a/.github/workflows/pm-github-sync.yml +++ b/.github/workflows/pm-github-sync.yml @@ -23,6 +23,11 @@ on: type: string required: false default: ${{ github.repository }} + adopt-cited: + description: "Opt in to conservative citation adoption; tag new items gh:owner/repo#N instead" + type: boolean + required: false + default: false pm-github-version: description: "Deprecated: fallback only for a non-release commit; pin a release SHA instead" type: string @@ -82,6 +87,7 @@ jobs: runs-on: ubuntu-latest env: REPOSITORY: ${{ inputs.repository }} + ADOPT_CITED: ${{ inputs.adopt-cited }} SYNC_BRANCH: ${{ inputs.sync-branch }} REQUESTED_PM_GITHUB_VERSION: ${{ inputs.pm-github-version }} NPM_PROJECT_DIR: ${{ inputs.npm-project-dir }} @@ -334,14 +340,24 @@ jobs: - name: Preview GitHub to pm plan env: GH_TOKEN: ${{ github.token }} - run: pm github import "${REPOSITORY}" --state all --atomic --gate --dry-run --with-comments + shell: bash + run: | + set -euo pipefail + adopt_args=() + if [[ "${ADOPT_CITED:-false}" == true ]]; then adopt_args+=(--adopt-cited); fi + pm github import "${REPOSITORY}" --state all --atomic --gate --dry-run --with-comments "${adopt_args[@]}" # 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. - name: Gated GitHub to pm import env: GH_TOKEN: ${{ github.token }} - run: pm github import "${REPOSITORY}" --state all --atomic --gate --with-comments + shell: bash + run: | + set -euo pipefail + adopt_args=() + if [[ "${ADOPT_CITED:-false}" == true ]]; then adopt_args+=(--adopt-cited); fi + pm github import "${REPOSITORY}" --state all --atomic --gate --with-comments "${adopt_args[@]}" - name: Verify repeat import is a no-op env: @@ -355,7 +371,9 @@ jobs: if [[ -s "${paths_file}" ]]; then git --literal-pathspecs add --pathspec-from-file="${paths_file}" --pathspec-file-nul fi - pm github import "${REPOSITORY}" --state all --atomic --gate --with-comments + adopt_args=() + if [[ "${ADOPT_CITED:-false}" == true ]]; then adopt_args+=(--adopt-cited); fi + pm github import "${REPOSITORY}" --state all --atomic --gate --with-comments "${adopt_args[@]}" git diff --exit-code -- "${sync_scope[0]}" "${sync_scope[@]:2}" [[ -z "$(git ls-files --others --exclude-standard -- "${sync_scope[0]}" "${sync_scope[@]:2}")" ]] pm github gate --json diff --git a/CHANGELOG.md b/CHANGELOG.md index 56dd97db..047f44fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ ### Added -- Adopt an existing item that cites the imported GitHub issue ([pm-github-cited-issue-adoption](https://github.com/unbraind/pm-github/blob/main/.agents/pm/features/pm-github-cited-issue-adoption.toon)) +- Require opt-in before adopting cited GitHub issue items ([pm-github-cited-issue-adoption](https://github.com/unbraind/pm-github/blob/main/.agents/pm/features/pm-github-cited-issue-adoption.toon)) - Explore provider identity collisions and approve scoped provenance repairs ([pm-github-identity-collisions](https://github.com/unbraind/pm-github/blob/main/.agents/pm/features/pm-github-identity-collisions.toon)) - Reconcile authenticated GitHub webhooks through a durable deduplicating inbox ([pm-github-webhook-inbox](https://github.com/unbraind/pm-github/blob/main/.agents/pm/features/pm-github-webhook-inbox.toon)) - Let reusable workflows carry their own release version ([pm-github-znig](https://github.com/unbraind/pm-github/blob/main/.agents/pm/features/pm-github-znig.toon)) diff --git a/README.md b/README.md index 2b8e3041..f7f9abcf 100644 --- a/README.md +++ b/README.md @@ -361,7 +361,7 @@ 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. | -| `--no-adopt-cited` | boolean | Require explicit provenance even for eligible intent-bearing prose, such as copied titles or fix phrases. Existing provenance/URL matching still applies. | +| `--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. | | `--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. | | `--dry-run` | boolean | Preview without writing | @@ -369,90 +369,72 @@ pm github import owner/repo --dry-run 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. -Before creating an issue item, import looks for an existing item citing exactly -`https://github.com/owner/repo/issues/N` or `owner/repo#N` in its title, -description or body. Trailing punctuation such as `.` and `)` is accepted; -bare `#N`, another repository and longer issue numbers never establish ownership. Existing -tag/URL/provider ownership matching takes precedence. Items with an established -`gh:` issue tag are excluded from this fallback. Closed and canceled items remain -eligible because their fix may already have landed. - -Citation adoption uses a conservative exclusion check followed by a strict -positive ownership check: - -- **Exclusion:** scan the raw title, description and body for repository-qualified - `github.com/owner/repo/issues/`, `github.com/owner/repo/pull/` - and `owner/repo#` references, plus unattributed `issues/`, - `pull/` and `#` substrings. Consume a qualified reference as - one reference, retaining its repository (case-insensitive) and canonical - number. Every reference must name the importing repository and target number; - any foreign repository, different number or UNKNOWN repository vetoes adoption. - No Markdown is stripped and no trailing-character boundary is applied, so - emphasis, code spans, links and angle brackets cannot hide another repository - even when its issue number is identical. Bare hashes are UNKNOWN: the item - projection has no verified owning-repository context, and the import argument - or another prose citation does not supply it. They cannot be assumed to name - the target. This deliberately over-counts: refusing adoption can leave the - usual duplicate import, while missing another reference could overwrite - another item's content or status. -- **Positive ownership:** the strict parser must identify the imported issue's - qualified reference and find no other repository/issue identity. The reference - must appear in the title, or directly follow explicit intent: - `fix`/`fixes`/`fixed`, `close`/`closes`/`closed`, - `resolve`/`resolves`/`resolved`, `Issue:`, `Tracks:` or `GH issue`. - Keywords and labels ignore case; punctuation and opening Markdown delimiters - around the reference are accepted. The closing words follow - [GitHub's supported keyword list](https://docs.github.com/en/issues/tracking-your-work-with-issues/using-issues/linking-a-pull-request-to-an-issue). - -The strict parser still handles qualified shorthand, issue/PR URLs with trailing -slashes, queries, fragments and punctuation. Bare numbers and PR links cannot -establish issue ownership on their own. Repeated references to the same target -are allowed when repository-qualified. A PR link with the same repository and -number counts as that same reference, while a foreign PR link vetoes adoption. -Passing the broad check alone never authorizes adoption. - -| Existing item prose | Decision for issue 30 | -| --- | --- | -| Body: `Fixes acme/widgets#30` | Eligible for adoption | -| Body: `Issue: https://github.com/acme/widgets/issues/30` | Eligible for adoption | -| Title: `Handle acme/widgets#30` | Eligible if no other issue is cited | -| Body: `Related: acme/widgets#30` | Incidental; `missing_intent` | -| Title: `Roadmap acme/widgets#30 and acme/widgets#31` | Incidental for either issue; `multiple_issues` | -| Description: `Fixes acme/widgets#30`; body: `See other/repo#8` | Incidental; `multiple_issues` | -| Body: `Fixes acme/widgets#30 and https://github.com/acme/widgets/issues/31/` | Incidental; `multiple_issues` | -| Body: `Fixes acme/widgets#30 and #31` (importing `acme/widgets`) | Incidental; `multiple_issues` | -| Body: `Fixes acme/widgets#30 and https://github.com/acme/widgets/pull/31#issuecomment-123` | Incidental; `multiple_issues` | -| Body: `Fixes acme/widgets#30 and **https://github.com/acme/widgets/issues/31**` | Incidental; `multiple_issues` | -| Body: `Fixes acme/widgets#30`; unrelated code contains `prefix#31suffix` | Incidental; `multiple_issues` | -| Body: `Fixes acme/widgets#30 and **https://github.com/other/widgets/issues/30**` | Incidental; foreign repository, `multiple_issues` | -| Body: `Fixes acme/widgets#30 and **https://github.com/other/widgets/pull/30**` | Incidental; foreign repository, `multiple_issues` | -| Body: `Fixes acme/widgets#30 and #30` | Incidental; UNKNOWN repository, `multiple_issues` | - -Incidental mentions never transfer ownership. The issue imports normally into -its own item, leaving the incidental item's status, content and fields intact, -including a closed item's completion state. Plans include -`citation_candidates: [{id, reason}]`; aggregate dry-run and write receipts -include `citation_candidates: [{issue, id, reason}]`. These hints let an agent -explicitly tag the correct item when the prose does not establish ownership. - -One eligible candidate produces an `adopt` plan entry: the existing ID receives the -provenance tag and `github_*` fields through the normal upsert, retaining local -tags and any established provider ownership. Later imports use the structured -identity. Two or more eligible candidates produce `ambiguous_citation`, candidate IDs and -a remedy: tag the correct item with `gh:owner/repo#N`, then retry. The entire -batch refuses writes until resolved; dry-run returns `blocked: true` and the -refusal entries with zero planned write counts. - -Dry runs show `adopt` entries and `wouldAdopt`; write receipts report `adopted` -separately from `imported` and `updated`. Gated idempotency counts an adopt as one -existing landing item with its provenance tag, never a new entry. This is on by -default for manual imports and scheduled gated syncs, whose import step uses the -same planner. Use `--no-adopt-cited` to require explicit provenance even for -intent-bearing prose, for example when titles or fix phrases are copied from -other work. Incidental mentions are already excluded by default. The legacy -import alias shares this flag. -Status push (`github sync`), export and Projects matching retain their existing -scope. Citation adoption also works in a non-git tracker; actual `--gate` writes +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. + +**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 +correct item, including when several candidates exist. No candidate is adopted +or ambiguity-blocked by default. + +Use `--adopt-cited` on the CLI, `adoptCited: true` in SDK import options, or +`adopt-cited: true` on the reusable sync workflow to enable the fallback. The +fallback deliberately favors refusal over overwriting an unrelated item: + +1. **Normalize before checking.** Decode complete named/numeric HTML entities and + percent-encoded bytes repeatedly until stable, bounded to eight passes. + Apply NFKC and remove zero-width/bidi control characters. Unwrap Markdown + link destinations and reference definitions and join soft line breaks for + intent matching. Retain normalized source as well as the unwrapped view for + exclusion, so a link label cannot hide a second reference. Joined-line views + also expose split references. A normalization bound exhaustion is a refusal. +2. **Allowlisted ownership.** A qualified target issue reference must appear in + the title, directly follow `Fix`, `Fixes`, `Fixed`, `Close`, `Closes`, `Closed`, + `Resolve`, `Resolves` or `Resolved` at the start of the description, or be a + URL on a whole body line `Issue: ` or `Tracks: `. Matching ignores + case. Body fix phrases, `Related:` mentions, unanchored phrases and bare + numbers cannot authorize adoption. +3. **Exclude every other reference.** Across all normalized fields, any other + `github.com` URL, foreign repository, different `#` or + `issues`/`pull` token followed by another number vetoes adoption. Unattributed + paths/bare hashes also refuse adoption: this item projection has no verified + owning-repository context. PR URLs, including a same-number PR URL, are other + URLs. The scan over-counts code and incidental text deliberately. These + checks reduce risk; they do not make arbitrary prose an ownership contract. + +| Existing prose | Default | With `--adopt-cited` | +| --- | --- | --- | +| Description: `Fixes acme/widgets#30` | Create + `adoption_disabled` hint | Eligible if no other reference | +| Body line: `Issue: https://github.com/acme/widgets/issues/30` | Create + hint | Eligible if no other reference | +| Title: `Handle acme/widgets#30` | Create + hint | Eligible if no other reference | +| Body: `Fixes acme/widgets#30` | Create + `missing_intent` hint | Create + hint | +| Body: `Related: acme/widgets#30` | Create + `missing_intent` hint | Create + hint | +| Title cites issues 30 and 31 | Create + `multiple_issues` hint | Create + hint | +| Description fixes issue 30; body has `**https://github.com/other/widgets/issues/30**` | Create + hint | Create + `multiple_issues` hint | +| Description fixes issue 30; body has `[other](https://github.com/other/widgets/issues/31)` or `issues%2F31` | Create + hint | Create + `multiple_issues` hint | + +Plans include `citation_candidates: [{id, reason}]`; aggregate dry-run and write +receipts include `citation_candidates: [{issue, id, reason}]`. Reasons are +`adoption_disabled`, `missing_intent`, `multiple_issues` or `normalization_limit`. +Candidate reporting never changes the referenced item's status, content or +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. +The default path never applies that fallback ambiguity refusal. Existing +structured-identity collision checks still apply in both modes. + +Dry runs show `adopt` and `wouldAdopt`; write receipts report `adopted` separately +from `imported`/`updated`. Gated idempotency counts an opt-in adopt as one landing +item. Status push, export and Projects matching retain their scope. Citation +reporting and opt-in adoption work in non-git trackers; actual `--gate` writes still require a Git worktree for the proposed-change privacy scan. `--issue` deduplicates references and fetches each selected issue directly, including closed issues, without listing the repository. Every selected number is fetched and checked before any tracker write. A missing number fails the whole import; `--dry-run` instead reports `missingIssues` and previews the available selection. Pull request numbers require `--include-prs`. Selection combines with comment modes, dependency linking, atomic imports, gates, dry runs, and custom item types, preserving provenance-based re-import idempotency. @@ -868,6 +850,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. | | `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. | @@ -893,6 +876,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. secrets: automation_app_private_key: ${{ secrets.PM_GITHUB_APP_PRIVATE_KEY }} ``` diff --git a/docs/cited-issue-adoption-validation.md b/docs/cited-issue-adoption-validation.md index 89f267e4..f9cfc610 100644 --- a/docs/cited-issue-adoption-validation.md +++ b/docs/cited-issue-adoption-validation.md @@ -1,256 +1,138 @@ # Cited issue adoption validation -The importer first resolves established provenance tags, structured URLs and -provider back-links. Only an issue without an established owner reaches exact -title/description/body citation matching. Only a single-target item with explicit -fix/track intent or a title reference is eligible. One eligible candidate is -adopted; multiple eligible candidates refuse the batch before any writes and report `ambiguous_citation` -with candidate IDs and a tagging remedy. Status push, export and Projects -matching retain their existing scope. +## 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 +`--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 +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. + +Before citation checks, named/numeric HTML entities and percent bytes are +repeatedly decoded with a bound of eight passes. NFKC normalization and +zero-width/bidi control removal apply too. Markdown destinations/reference +definitions are unwrapped and soft lines joined for ownership matching. The +normalized source is retained alongside the unwrapped/joined views for +exclusion so labels, definitions and split references cannot disappear. A bound +exhaustion refuses adoption with `normalization_limit`. + +An opt-in adoption requires a qualified reference in the title, a closing +keyword followed by the reference at the start of the description, or a whole +body line `Issue: ` / `Tracks: `. Closing keywords are +Fix/Fixes/Fixed, Close/Closes/Closed and Resolve/Resolves/Resolved. Related +mentions, body fix phrases and unanchored descriptions do not authorize +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. + +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` / +`adopted`; gated idempotency counts one existing landing item. Status push, +export and Projects matching retain their scope. -Citation adoption now has a separate conservative exclusion check. Every raw -reference must carry the importing repository and canonical number. The scan -collects GitHub issue/PR paths and qualified shorthand regardless of Markdown; -unqualified `issues/`, `pull/` and `#` have UNKNOWN -repository and veto adoption. The item projection carries no verified owning -repository, so the import argument cannot assign bare hashes to this repository. -Foreign references with the same number also veto adoption. The strict parser -and explicit intent still establish positive ownership; passing the raw veto -alone is insufficient. This intentionally prefers a normal create over taking -another item. +## Regression evidence -Incidental references carry `citation_candidates` hints with item IDs and -`missing_intent` or `multiple_issues` reasons. They never hand status or fields -to the importer; the GitHub issue imports normally into its own item. +All earlier review reproductions remain, with adoption scenarios explicitly +opting in and former permissive positives retained as refusal inputs where the +anchored policy requires it. Prior shape/property matrices remain present. -## Public tracker dry runs +| Review case | Opt-in result | +| --- | --- | +| Closed item with only `Related: acme/widgets#30` | Create with `missing_intent`, never reopen local work | +| Roadmap cites issues 30 and 31 | Create for either issue, `multiple_issues` | +| Second issue URL ends in `/` | Create with candidate hint | +| Second URL wrapped in bold, italic, code, angles or links | Create with candidate hint | +| Foreign repository shares number 30 | Create with candidate hint | +| `[other](https://github.com/other/widgets/issues/31)` | Decode then refuse adoption | +| `issues%2F31` | Decode then refuse adoption | +| pm-review shape `Issue: https://github.com/acme/widgets/issues/30` | Adopt only with explicit opt-in | + +The new encoding property uses deterministic seed `0x53ce71a9`: **4,096 +repository/number/decoration/encoding samples**, each tested in title, +description and body (**12,288 field cases**). It combines qualified issue/PR +URLs, shorthand, bare hashes and unqualified paths with named/numeric entities, +percent layers, invisible controls, soft breaks and the twelve earlier Markdown +decorations. Every non-target or UNKNOWN second reference must refuse adoption. +Positive controls cover all closing words, body URL labels, title references, +HTML/percent nesting, NFKC, control removal, inline and reference-style links, +soft label breaks and normalization-bound refusal. + +Real initialized non-git tracker tests cover plain and atomic imports with the +option both false and true. Default imports create a separate issue item and +preserve the local item's full SDK projection. Opt-in imports adopt the same +ID and repeat against established provenance. SDK defaults report competing +eligible candidates without refusing. The packed CLI checks `--adopt-cited` +against a body-only `Issue:` item, while omission creates normally with a hint. + +The mandatory PM-linked commands all pass without failures or skips: + +| Command | Pass | +| --- | ---: | +| `node --test test/cited-issue-adoption.test.ts` | 44 / 44 | +| `node --test test/import-regex.test.ts` | 8 / 8 | +| `node --test --test-name-pattern='reusable workflow.*(explicit opt-in\|adoption input)' test/import-gate.test.ts` | 4 / 4 | +| `node --test --test-name-pattern='built package dispatch' test/import-selection.test.ts` | 1 / 1 | + +An additional complete selector/fidelity sweep passed **80/80**. Typecheck, +both TypeScript builds, lint, docstrings and zero duplication pass locally. +Scoped coverage preflight found no uncovered branches in the new normalization, +exclusion and anchored ownership functions. Full npm/Bun release-gate evidence +and CodeQL run links are recorded on PR 169 at the final pushed head. + +## Behavioral revert proofs + +Each proof uses an isolated source copy with current tests and real SDK +persistence. Source tests import live TypeScript. The workflow proof packs a +real copy of current compiled output and retains the current pinned CLI/SDK; +only the reusable YAML is restored. No SDK mocks replace persistence. + +| Restored from `0b4bc29` | Exact command | Reverted result | +| --- | --- | --- | +| Only `matchImportedIssue` | `node --test --test-name-pattern='explicit opt-in (false\|defaults)' test/cited-issue-adoption.test.ts` | 3 failures, 0 passes/skips, exit 1 | +| Only `citesIssue` and its prior `hasOtherCitationReference` helper | `node --test --test-name-pattern='normalized allowlist' test/cited-issue-adoption.test.ts` | 4 failures, 0 passes/skips, exit 1 | +| Only `.github/workflows/pm-github-sync.yml` | `node --test --test-name-pattern='reusable workflow.*(explicit opt-in\|adoption input)' test/import-gate.test.ts` | 2 failures, 2 passes, 0 skips, exit 1 | -Validation used GitHub API tracker archives copied into plain directories with -no Git repository. Original archive files were made read-only and every original -file's SHA-256 was checked after the dry run. SDK runtime caches are separate -from those original tracked files. No source repository or tracker was changed. -Live issue and closure-timeline data came from GitHub; no issue payload was -rewritten for the replay. +The workflow's opt-in true execution imports instead of adopting after the +revert, while omitted/false executions still pass; the input-default check also +fails. These are behavioral assertions, not compilation or infrastructure +failures. Fixed selections all pass. Earlier review proofs remain in the PM +item's append-only history. -| Repository and tracker revision | Issues fetched/planned | Create | Update | Adopt | Skip | Original files unchanged | -| --- | ---: | ---: | ---: | ---: | ---: | ---: | -| pm-review current main `e1fc83a81507ca6a91f1675b74863c050f1a89f1` | 1 / 1 | 0 | 1 | 0 | 0 | 106 / 106 | -| pm-review pre-tag history `f72250e4c5ee8dfafaf57ee8afe20ddd3cbc993e` | 1 / 1 | 0 | 0 | 1 | 0 | 106 / 106 | -| pm-context current main `7d7479ae77c13d01c54b2b4560ad54091ffed788` | 5 / 5 | 2 | 3 | 0 | 0 | 181 / 181 | +## Public tracker replays -The requested pm-review current-main check now correctly plans an **update**: -[PR 33](https://github.com/unbraind/pm-review/pull/33) already added the issue's -provenance tag after the duplicate-import incident. Claiming an adopt on that -untouched snapshot would contradict structured-identity precedence. Replaying -the actual [pre-tag tracker](https://github.com/unbraind/pm-review/tree/f72250e4c5ee8dfafaf57ee8afe20ddd3cbc993e/.agents/pm) -produces this adoption entry: +Saved public GitHub tracker archives were copied into plain non-git directories. +Original files are read-only and SHA-256 checked after each dry run. Live issue +and closure-timeline data are fetched from GitHub; no source tracker or issue is +changed. These are saved revisions, not a claim about latest repository mains. + +| Tracker revision / selection | Adoption off: create / update / adopt | Opt-in: create / update / adopt | +| --- | --- | --- | +| pm-review pre-tag `f72250e4c5ee8dfafaf57ee8afe20ddd3cbc993e`, issue 30 | 1 / 0 / 0 | 0 / 0 / 1 | +| pm-review tagged `e1fc83a81507ca6a91f1675b74863c050f1a89f1`, issue 30 | 0 / 1 / 0 | 0 / 1 / 0 | +| pm-context `7d7479ae77c13d01c54b2b4560ad54091ffed788`, issues 154,153,136,53,81 | 2 / 3 / 0 | 2 / 3 / 0 | -The pre-tag item's description says `Fix` followed by the issue URL and its body -says `Issue:` followed by the same URL. Those are two references to one distinct -issue with explicit intent, so the tightened ownership rule still adopts it. +All modes report zero skips. The pre-tag default receipt reports +`pm-review-withdrawn-lineage` with `adoption_disabled`; explicit opt-in plans: ```json { "issue": 30, "item": "pm-review-withdrawn-lineage", "action": "adopt", - "status": "closed", "github_url": "https://github.com/unbraind/pm-review/issues/30", - "github_number": 30, - "identity": { - "via": "citation", - "conflict": { - "local": "in_progress", - "github": "closed", - "rule": "github-source-wins" - } - } + "identity": { "via": "citation" } } ``` -Its gated idempotency receipt reports `adopted: 1`, `matched_by_provenance: 0` -and `new_entries: 0`. The current-main receipt reports `adopted: 0`, -`matched_by_provenance: 1` and `new_entries: 0`. - -Both pm-review runs used `github import unbraind/pm-review --issue 30 --atomic ---gate --dry-run`. The second public repository used `github import -unbraind/pm-context --state all --limit 5 --atomic --gate --dry-run`; it fetched -issues 154, 153, 136, 53 and 81, reported `truncated: false`, and preserved the -`not_planned` issue's canceled status. These were planner dry runs, not -post-write privacy-scan receipts. - -## Regression evidence - -`test/cited-issue-adoption.test.ts` uses real initialized trackers and real CLI -or SDK writes. Plain and atomic non-git cases run with a PATH containing only -Node and the pinned pm CLI. Gate write cases use disposable Git repositories. -Assertions cover single-candidate adoption, terminal candidates, no duplicate -on retry, ambiguity before any batch write, URL punctuation, repository and -issue-number boundaries, colon-prefixed labels and sentence-ending question marks, -structured ownership precedence, provider link-only behavior, separate adoption -counts and opt-out. Review regressions also prove these ownership boundaries: - -| Finding | Fixed behavior and persistence evidence | -| --- | --- | -| A closed item says only `Related: acme/widgets#30` while issue 30 is open | Import creates the issue item and reports `missing_intent`; the original item's entire SDK projection, including status/body/closure fields, is unchanged | -| A roadmap cites issues 30 and 31 | Selecting either issue alone creates its own item and reports `multiple_issues`; the roadmap stays open and unchanged | -| References span repositories or fields | All distinct identities count, including title references and URL query/fragment forms; repeated URL/shorthand for the same identity counts once | -| Intent-bearing single-issue owners compete | Two eligible owners still refuse writes; incidental hints are reported separately and never create ambiguity | - -`test/import-selection.test.ts` installs the built package into a disposable -consumer and exercises the actual CLI dispatch, including the advertised -`--no-adopt-cited` flag and its create-versus-adopt behavior. - -Review revert proofs used an isolated source copy and restored only -`citesIssue` and `matchImportedIssue` from pre-review head `4220a37`, leaving the -new planner/receipt code and real SDK persistence unchanged. Neither proof -changed the package worktree or rebuilt a stale artifact: tests import live TS. - -| Exact test selection | Reverted result | Fixed result | -| --- | --- | --- | -| `node --test --test-name-pattern='closed incidental item' test/cited-issue-adoption.test.ts` | 2 failures, 0 passes, exit 1 | Both plain/atomic cases pass | -| `node --test --test-name-pattern='roadmap never adopts' test/cited-issue-adoption.test.ts` | 2 failures, 0 passes, exit 1 | Both plain/atomic cases pass | - -`node --test test/cited-issue-adoption.test.ts test/import-regex.test.ts` passes -38 tests with zero failures/skips. The PM-linked adoption command contains -30 tests and the linked regex/performance command contains eight. Typecheck, -test typecheck, build, lint, docstrings and strict tracker -health pass locally. Full npm/Bun release-gate evidence is recorded in PR 169 -and the final handoff at the exact pushed head; thresholds remain unchanged. - -## Reference-parser review follow-up - -The second review found that `Fixes acme/widgets#30 and -https://github.com/acme/widgets/issues/31/` could still transfer ownership: -the earlier counting regex skipped the trailing-slash URL. A shared strict parser -produces identities and source positions for positive ownership checks. It -recognizes qualified shorthand, repository-hinted -bare `#N`, issue and PR URLs, optional trailing slashes, query strings, comment -fragments, prose punctuation and Markdown link boundaries. Foreign and malformed -URLs are consumed as URL tokens during strict parsing. The independent raw -exclusion deliberately counts their interior number substrings anyway. Bare -numbers and PR URLs cannot establish issue ownership on their own. - -The property-style regression exercises 17 ownership forms against 35 second -reference forms with five punctuation/Markdown wrappers in all three prose -fields: **8,925 disqualification combinations**, plus **51 positive ownership -controls**. The matrix includes issue and PR links, other repositories, slash, -query and fragment combinations, and Markdown links adjacent to following text. -Repeated URL/shorthand/bare references to the same identity remain one target. - -Plain and atomic real non-git tracker regressions reproduce the reported -trailing-slash example. They require one create, zero adopts and a -`multiple_issues` candidate hint, then verify that the entire original roadmap -projection remains unchanged after import. - -The isolated behavioral revert restores only `citesIssue` from `cce11c4`, leaving -the new parser, planner, receipts and real SDK persistence present. Running -`node --test --test-name-pattern='every accepted second-reference|trailing-slash second reference' test/cited-issue-adoption.test.ts` -then produces **3 failures, 0 passes, 0 skips, exit 1**. The fixed cases pass. -The current adoption/regex selection contains **38 tests**; the linked adoption -command contains **30 tests**. Fresh exact-head npm/Bun release evidence is -recorded in PR 169 when CI completes, without changing coverage thresholds. - -CodeQL then identified quadratic backtracking in the new URL punctuation -suffix regex. The parser now calls the existing backwards punctuation scanner -with its citation delimiter set, leaving the default structured-locator set -unchanged. The additional adversarial test sends a URL followed by 50,000 -exclamation marks and a non-punctuation sentinel through the real matcher. Its -best of three local trials is **0.172 ms**, below the existing 50 ms guard. -Restoring only `citationReferences` from `ef325ba` retains the new scanner and -test but restores the vulnerable regex call: the test then fails at -**3,670.058 ms** (one failure, zero passes/skips, exit 1). These timings are local -regression evidence, not a deployment benchmark. A fresh pushed head receives -new release-gate and CodeQL evidence. - -## Raw exclusion review follow-up - -The third review showed that bold emphasis could hide a second URL from strict -parsing. Exclusion now scans raw text separately: every `issues/`, -`pull/` and `#` substring must equal the imported issue's -canonical number. The scan has no markup or trailing-character boundaries and -does not require a GitHub host or repository. Noncanonical digit spellings can -also veto adoption conservatively. The strict parser and explicit intent remain -the positive ownership check, with the existing repository-identity safeguard. - -Eight fixed decorations (bold, italic, underscore, code span, code fence, angle -brackets, Markdown link and image link) wrap eight reference cores in every -field: **192 fixed cases**. A deterministic xorshift generator with seed -`0x6c8e9cf5` produces **4,096 nested decoration samples**, each checked in title, -description and body: **12,288 property cases**. Another 36 cases require -over-counting unrelated text, foreign/malformed URLs, trailing letters, zero, -noncanonical spelling and very large digit strings. Four repeated-number -controls were eligible under that historical number-only rule; the fourth -review below changes them to UNKNOWN-repository refusals. The earlier 8,925-case shape matrix and all earlier -regression inputs remain present. Three old foreign/malformed-URL expectations -now require refusal because the raw policy intentionally counts their `#31` -substrings; the encoded query case remains unchanged. - -Plain and atomic non-git tracker tests reproduce the exact bold-URL example. -The gated preview plans one create, zero adopts and a `multiple_issues` hint. -Actual imports preserve the entire original open item's SDK projection and -create a separate issue item. - -The behavioral revert restores only `citesIssue` from `56580ec`, retaining the -new raw scanner, strict parser, current tests, receipts and real SDK persistence. -`node --test --test-name-pattern='raw exclusion' test/cited-issue-adoption.test.ts` -then fails **5/5**, with zero passes/skips and exit 1. Fixed linked commands pass -**30/30 adoption tests** and **8/8 regex/performance tests**. - -The saved public tracker snapshots were replayed with this raw exclusion and -live GitHub reads: pm-review pre-tag still plans one adopt; its tagged snapshot -plans one update; the same five pm-context issues plan two creates and three -updates, with three `missing_intent` hints and no adopts/skips. All **393/393 -original files** remain unchanged. Fresh release-gate and CodeQL evidence is -linked from PR 169 at the final pushed head. - -## Repository-aware raw exclusion - -The fourth review reproduced a foreign reference hidden in bold with the same -number as the target: `Fixes acme/widgets#30 and -**https://github.com/other/widgets/issues/30**`. A number-only veto could not -protect that item. Raw exclusion now retains each qualified reference's -repository and canonical number. GitHub issue/PR URL substrings and qualified -shorthand are consumed before their embedded fallback, so they are counted -once. Every collected reference must equal the target repository and number; -foreign repositories, other numbers and UNKNOWN repositories all veto adoption. -The raw path/hash fallback is retained even for malformed or unrelated text. -No Markdown normalization or trailing-character boundary is used. - -The item projection carries no verified owning repository. Bare hashes therefore -remain UNKNOWN, even beside a qualified target citation. The import repository -is not proof of the item's repository. Four historical number-only controls and -the prior bare-hash deduplication input now require refusal; their inputs remain -in the suite. Qualified repeated references, case-insensitive repository names -and same-repository/same-number PR links remain allowed by exclusion, while the -strict positive parser and explicit issue ownership intent remain necessary. - -New fixed coverage checks four foreign same-number issue/PR/shorthand references -inside all twelve decoration pairs from the property test, in title, -description and body: **144 cases**. A deterministic xorshift generator seeded -with `0x78b3e2d1` creates **4,096 random repository/number/nested-decoration -samples**, each checked in all three fields: **12,288 cases**. Every generated -second reference has a foreign repository, another number or UNKNOWN attribution -and must refuse adoption. The earlier shape/property matrices remain present. -Plain and atomic no-git imports test the exact bold-foreign-URL reproduction -against a closed local item and an open issue. Both preview a create and -`multiple_issues` candidate hint, then create a separate item without reopening -or changing the completed local item. - -Behavioral revert: restore only `hasOtherCitationNumber` and `citesIssue` from -`aa86b21` in an isolated source copy, keeping current tests, strict parsing, -receipts and real SDK persistence. Run -`node --test --test-name-pattern='repository veto' test/cited-issue-adoption.test.ts`. -The revert gives **5 failures, 0 passes, 0 skips, exit 1**. Fixed linked commands -pass **35/35 adoption tests** and **8/8 regex/performance tests**. Typecheck, -TypeScript builds, lint, docstrings and duplication checks pass locally; release -gates on both npm and Bun are checked in CI at the final pushed head. - -Public replay with this rule still plans the pre-tag pm-review adopt into -`pm-review-withdrawn-lineage`; its tagged snapshot plans one update. The same -five pm-context issues still plan two creates and three updates, with no -adopts/skips. The three candidate hints now report `multiple_issues` because raw -UNKNOWN references veto before intent. All **393/393 original tracker files** -retain their SHA-256 hashes. These are the saved main/pre-tag revisions listed -above, replayed with live GitHub issue/timeline reads; no current-main claim is -made for the replay. Exact-head CI run links and counts are in PR 169. +The tagged snapshot correctly updates because provenance already exists. +pm-context retains three `multiple_issues` hints in both modes. All +**393/393 original files** (106 + 106 + 181) remain unchanged after both-mode +replays. The PM item records receipts, decisions and revert results. diff --git a/docs/sync-workflow-caller.yml b/docs/sync-workflow-caller.yml index 241bd44d..e3cd23af 100644 --- a/docs/sync-workflow-caller.yml +++ b/docs/sync-workflow-caller.yml @@ -31,6 +31,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 # Primary prevention: tag new items gh:owner/repo#N. npm-project-dir: "." # For pm-rust, use: npm-project-dir: tests/oracle # Defaults shown explicitly; use "" to disable an optional action; empty CI also disables auto-merge. post-import-script: "changelog:full" diff --git a/import-fidelity.ts b/import-fidelity.ts index f25157bb..6e9b9dac 100644 --- a/import-fidelity.ts +++ b/import-fidelity.ts @@ -1,5 +1,6 @@ /** Source lifecycle, release-window and cross-provider identity rules for issue imports. */ import { spawnSync } from "node:child_process"; +import { decodeHTML } from "entities"; import { acquireLock, appendHistoryEntry, canonicalDocument, createHistoryEntry, getHistoryPath, locateItem, normalizeItemId, readLocatedItem, readSettings, resolveItemTypeRegistry, resolvePmRoot, runActiveOnWriteHooks, serializeItemDocument, writeFileAtomic, @@ -47,7 +48,7 @@ export interface ImportMatch { item?: PmItem; linkOnly: boolean; via: string; ci /** Incidental references are review hints, never permission to transfer an item's ownership. */ export interface CitationCandidate { id: string; - reason: "multiple_issues" | "missing_intent"; + reason: "multiple_issues" | "missing_intent" | "adoption_disabled" | "normalization_limit"; } /** A citation cannot pick an owner until the user tags exactly one candidate. */ @@ -94,38 +95,90 @@ function citationReferences(text: string, repoHint: string): { key: string; inde return references; } -/** Require every raw reference to name the target repository and canonical number, ignoring markup. */ -function hasOtherCitationReference(texts: readonly string[], repo: string, number: number): boolean { - const expected = String(number); - let found = false; - for (const text of texts) { - // Consume qualified forms before their embedded path/hash fallback. Bare hashes - // remain UNKNOWN: this projection carries no verified owning-repository context. - for (const reference of text.matchAll(/github\.com\/([\w.-]+\/[\w.-]+)\/(?:issues|pull)\/([0-9]+)|(? + Buffer.from(encoded.slice(1).split("%").map(byte => Number.parseInt(byte, 16))).toString("utf8"))) + .normalize("NFKC").replace(/\p{Default_Ignorable_Code_Point}/gu, "").replace(/\r\n?/g, "\n"); + if (decoded === text) return { text: citationDisplayText(text), source: text, stable: true }; + text = decoded; + } + return { text: citationDisplayText(text), source: text, stable: false }; +} + +/** Unwrap link destinations/definitions for anchored intent, retaining the normalized source for exclusion. */ +function citationDisplayText(source: string): string { + const definitions = new Map(); + const text = source.replace(/^[ \t]{0,3}\[([^[\]\n]+)\]:[ \t]*(?:<([^>\n]+)>|(\S+)).*$/gm, + (_raw: string, label: string, angle: string | undefined, destination: string | undefined) => { + definitions.set(label.trim().toLowerCase(), angle ?? destination!); + return angle ?? destination!; + }); + return text.replace(/!?\[([^[\]\n]*)\]\((?:<([^<>\n]+)>|([^()[\]\n]*))\)/g, + (_raw: string, _label: string, angle: string | undefined, destination: string | undefined) => angle ?? destination!.trim()) + .replace(/\[([^[\]\n]+)\](?:\[([^[\]\n]*)\])?/g, + (raw: string, label: string, key: string | undefined) => definitions.get((key || label).trim().toLowerCase()) ?? raw) + .replace(/\\\n/g, "").replace(/(?"'`[\]()*{}]*/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; + } + return false; +} + +/** Inspect normalized source without trusting Markdown interpretation or an assumed repository for bare hashes. */ +function inspectCitationReferences(texts: readonly string[], repo: string, number: number): { mentioned: boolean; unsafe: boolean } { + let mentioned = false; + let unsafe = false; + for (const source of texts) { + // Check both retained source and joined soft lines; joining must not discard link labels or destinations. + for (const text of [source, source.replace(/(?(); - let intended = false; - for (const [field, text] of texts.entries()) { - for (const reference of citationReferences(text, repo)) { - identities.add(reference.key); - if (reference.key === target && reference.ownsIssue && (field === 0 || /(?:\b(?:fix|fixes|fixed|close|closes|closed|resolve|resolves|resolved)\b[\s:([<`*_]*|\b(?:issue|tracks)\s*:[\s([<`*_]*|\bgh\s+issue\b[\s:([<`*_]*)$/i.test(text.slice(0, reference.index)))) intended = true; +/** Permit only title references, leading description closing words, or whole body issue-label lines. */ +function hasAnchoredCitationIntent(text: string, field: number, repo: string, number: number): boolean { + for (const reference of citationReferences(text, repo)) { + if (reference.key !== `${repo}#${number}` || !reference.ownsIssue) continue; + if (field === 0) return true; + const prefix = text.slice(0, reference.index); + if (field === 1 && /^(?:fix|fixes|fixed|close|closes|closed|resolve|resolves|resolved)[ \t]+$/i.test(prefix)) return true; + if (field === 2 && /^(?:Issue|Tracks):[ \t]*]*)?>?[.)]*[ \t]*$/i.test(line)) return true; } } - if (!identities.has(target)) return undefined; - return otherReference || identities.size > 1 ? "multiple_issues" : intended ? "eligible" : "missing_intent"; + return false; +} + +/** Normalize first, exclude other/unknown references, then apply the anchored ownership allowlist. */ +function citesIssue(item: PmItem, repo: string, number: number): "eligible" | CitationCandidate["reason"] | undefined { + const normalized = [item.title ?? "", item.description ?? "", item.body ?? ""].map(normalizeCitationText); + const assessment = inspectCitationReferences(normalized.flatMap(value => [value.source, value.text]), repo, number); + if (!assessment.mentioned) return undefined; + if (normalized.some(value => !value.stable)) return "normalization_limit"; + const intended = normalized.some((value, field) => hasAnchoredCitationIntent(value.text, field, repo, number)); + if (assessment.unsafe) return "multiple_issues"; + return intended ? "eligible" : "missing_intent"; } /** Trim configurable exporter punctuation in one backwards pass. @internal Shared with parity tests. */ @@ -164,7 +217,7 @@ function providerProvenance(item: PmItem): string { } /** Match GitHub locators or provider back-links before creation, refusing multiple owners. */ -export function matchImportedIssue(items: readonly PmItem[], issue: GhIssue, tagged?: PmItem, adoptCited = true): ImportMatch { +export function matchImportedIssue(items: readonly PmItem[], issue: GhIssue, tagged?: PmItem, adoptCited = false): ImportMatch { const issueUrl = locators(issue.html_url)[0]!; const bodyUrls = new Set(locators(issue.body ?? "")); /** Recognize leading native ownership declarations; later prose cannot transfer ownership. */ @@ -178,14 +231,14 @@ export function matchImportedIssue(items: readonly PmItem[], issue: GhIssue, tag let item = candidates[0]; let cited = false; const incidental: CitationCandidate[] = []; - if (!item && adoptCited && !issue.pull_request) { + if (!item && !issue.pull_request) { const repo = issue.html_url.split("/").slice(3, 5).join("/").toLowerCase(); const citing: PmItem[] = []; for (const candidate of items) { if (!candidate.id || candidate.tags?.some(tag => parseProvenanceTag(tag) !== undefined)) continue; const assessment = citesIssue(candidate, repo, issue.number); - if (assessment === "eligible") citing.push(candidate); - else if (assessment !== undefined) incidental.push({ id: candidate.id, reason: assessment }); + if (assessment === "eligible" && adoptCited) citing.push(candidate); + else if (assessment !== undefined) incidental.push({ id: candidate.id, reason: assessment === "eligible" ? "adoption_disabled" : assessment }); } incidental.sort((left, right) => left.id.localeCompare(right.id)); if (citing.length > 1) throw new AmbiguousCitationError(repo, issue.number, citing.map(candidate => candidate.id!).sort(), incidental); diff --git a/index.ts b/index.ts index 140f4914..f7ce0254 100644 --- a/index.ts +++ b/index.ts @@ -175,8 +175,8 @@ const COMMENTS_MODES: readonly CommentsMode[] = ["body", "annotations", "both"]; * of the import path consumes a typed shape rather than re-parsing strings. */ export interface ImportOptions { - /** Disable fallback adoption when citations are references rather than ownership evidence. */ - noAdoptCited?: boolean; + /** Explicitly enable conservative citation adoption; default imports only report candidates. */ + adoptCited?: boolean; /** Explicit comma-separated issue numbers or same-repository references. */ issue?: string; /** Positive cap on list-mode issues, ordered by latest update. */ @@ -2736,7 +2736,7 @@ export function parseImportOptions(options: Record): ImportOpti atomic: optionEnabled(options, "atomic"), linkDeps: optionEnabled(options, "link-deps", "linkDeps"), gate: optionEnabled(options, "gate"), - noAdoptCited: optionEnabled(options, "no-adopt-cited", "noAdoptCited"), + adoptCited: optionEnabled(options, "adopt-cited", "adoptCited"), }; } @@ -3118,7 +3118,7 @@ export async function prepareGithubImport( const title = issue.title.trim(); if (!title) return undefined; - const identity = matchImportedIssue(fidelityContext.items, issue, match, !opts.noAdoptCited); + const identity = matchImportedIssue(fidelityContext.items, issue, match, opts.adoptCited === true); match = identity.item; const timeline = await fidelityContext.timeline(issue, repo, token); const fidelity = buildImportFidelity(issue, fidelityContext.releases, timeline); @@ -6628,7 +6628,7 @@ export function isMutatingGithubCommand(command: string, options: Record=14" } }, + "node_modules/entities": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.1.0.tgz", + "integrity": "sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", diff --git a/package.json b/package.json index f7bddb95..5dd67751 100644 --- a/package.json +++ b/package.json @@ -124,5 +124,8 @@ "duplicationGate": { "threshold": 0, "minTokens": 50 + }, + "dependencies": { + "entities": "8.1.0" } } diff --git a/test/cited-issue-adoption.test.ts b/test/cited-issue-adoption.test.ts index c341d0a3..12c55c85 100644 --- a/test/cited-issue-adoption.test.ts +++ b/test/cited-issue-adoption.test.ts @@ -7,9 +7,12 @@ import { delimiter, join } from "node:path"; 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 } from "../import-fidelity.ts"; +import { AmbiguousCitationError, matchImportedIssue as resolveImportedIssue } from "../import-fidelity.ts"; import { captureStderr, withEnv } from "./helpers/mock-github-server.ts"; +/** Existing adoption regressions opt in explicitly; default-off cases call the production matcher directly. */ +const matchImportedIssue: typeof resolveImportedIssue = (items, issue, tagged, adoptCited = true) => resolveImportedIssue(items, issue, tagged, adoptCited); + const root = join(import.meta.dirname, ".."); const cli = join(root, "node_modules/.bin/pm"); const repo = "acme/widgets"; @@ -38,7 +41,7 @@ async function tracker(run: (pmRoot: string) => Promise, git = false): Pro /** Import injected REST data through the real planner and SDK or CLI mutation path. */ async function importing(pmRoot: string, options: Record, issues = [issue]) { - return captureStderr(async () => await runImport(repo, pmRoot, parseImportOptions(options), { + return captureStderr(async () => await runImport(repo, pmRoot, parseImportOptions({ adoptCited: true, ...options }), { resolveToken: () => undefined, fetchIssues: async () => issues, }) as Record); } @@ -53,7 +56,7 @@ async function items(pmRoot: string) { for (const atomic of [false, true]) { test(`${atomic ? "atomic" : "plain"} citation adopts a terminal item without git, then re-import updates one item`, async () => tracker(async pmRoot => { const existing = (await create({ type: "Task", title: "Local fix", description: `Fixed (${issue.html_url}).`, - body: `Fix for ${repo}#30`, tags: "local", status: atomic ? "canceled" : "closed", closeReason: "Landed locally" }, { pmRoot })).item; + body: `Issue: ${issue.html_url}\n\nFix for ${repo}#30`, tags: "local", status: atomic ? "canceled" : "closed", closeReason: "Landed locally" }, { pmRoot })).item; const before = await items(pmRoot); const preview = await importing(pmRoot, { atomic, "dry-run": true }); assert.equal(preview.result.wouldImport, 0); @@ -81,7 +84,7 @@ for (const atomic of [false, true]) { test(`${atomic ? "atomic" : "plain"} ambiguity lists both candidates and refuses the entire batch before writes`, async () => tracker(async pmRoot => { const first = (await create({ type: "Task", title: `Fix ${repo}#30` }, { pmRoot })).item; - const second = (await create({ type: "Task", title: "Other fix", body: `Issue: (${issue.html_url})` }, { pmRoot })).item; + const second = (await create({ type: "Task", title: "Other fix", body: `Issue: (${issue.html_url})\n\nIssue: ${issue.html_url}` }, { pmRoot })).item; const before = await items(pmRoot); const batch = [{ ...issue, number: 31, html_url: issue.html_url.replace("/30", "/31") }, issue]; const preview = await importing(pmRoot, { atomic, gate: true, "dry-run": true }, batch); @@ -325,7 +328,7 @@ test("repository veto refuses UNKNOWN references and retains qualified same-targ } for (const reference of [`${repo}#30`, "ACME/Widgets#30", `**https://github.com/${repo}/issues/30**`, `https://github.com/${repo}/pull/30`, `http://www.github.com/${repo}/issues/30/?query=all#issuecomment-123`]) { - assert.equal(matchImportedIssue([{ id: "local", description: `Fixes ${repo}#30 and ${reference}` }], issue).via, "citation", reference); + assert.equal(matchImportedIssue([{ id: "local", description: `Fixes ${repo}#30 and ${reference}` }], issue).via, reference.includes("/pull/") ? "new" : "citation", reference); } }); @@ -337,7 +340,7 @@ test("every accepted second-reference shape disqualifies a fix item across every ]))]; for (const first of firstShapes) { for (const field of ["title", "description", "body"] as const) { - assert.equal(matchImportedIssue([{ id: "local", [field]: `Fixes ${first}` }], issue).via, "citation", `${field}: ${first}`); + assert.equal(matchImportedIssue([{ id: "local", [field]: `Fixes ${first}` }], issue).via, field === "body" ? "new" : "citation", `${field}: ${first}`); for (const second of secondShapes) { for (const text of [second, `${second}.`, `(${second})`, `[reference](${second}).`, `[reference](${second})next`]) { const local = { id: "local", title: "Local work", description: `Fixes ${first}`, body: "" }; @@ -366,7 +369,7 @@ test("strict parsing deduplicates qualified locators without granting ownership assert.equal(result.via, "new", embedded); assert.deepEqual(result.citation_candidates, [{ id: "local", reason: "multiple_issues" }]); } - assert.equal(matchImportedIssue([{ id: "local", description: `Fixes ${repo}#30 and https://github.com/search?q=other/widgets%2331` }], issue).via, "citation"); + assert.equal(matchImportedIssue([{ id: "local", description: `Fixes ${repo}#30 and https://github.com/search?q=other/widgets%2331` }], issue).via, "new"); }); test("citation matching accepts exact qualified references across prose fields and trailing punctuation", () => { @@ -374,7 +377,7 @@ test("citation matching accepts exact qualified references across prose fields a for (const citation of [issue.html_url, `${issue.html_url}.`, `(${issue.html_url})`, `${issue.html_url}?`, `Issue:${issue.html_url}`, `${repo}#30`, `Tracks:${repo}#30`, `${repo}#30?`, `ACME/Widgets#30;`]) { const local = { id: "local", [field]: `Fixed ${citation}` }; - assert.equal(matchImportedIssue([local], issue).item, local, `${field}: ${citation}`); + assert.equal(matchImportedIssue([local], issue).item, field === "body" || (field === "description" && (/^(?:Issue:|Tracks:)/.test(citation) || citation.startsWith("("))) ? undefined : local, `${field}: ${citation}`); } } assert.equal(matchImportedIssue([{ id: "local", tags: ["gh:invalid"], title: `${repo}#30` }], issue).via, "citation"); @@ -392,7 +395,7 @@ test("bare numbers, other repos, longer identities, URL paths and different issu }); test("tagged and URL-linked owners take precedence over even multiple prose citations", () => { - const citing = ["first", "second"].map(id => ({ id, body: `Fixes ${issue.html_url}` })); + 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`] }; assert.equal(matchImportedIssue([...citing, tagged], issue, tagged).item, tagged); const linked = { id: "linked", github_url: issue.html_url }; @@ -402,9 +405,9 @@ test("tagged and URL-linked owners take precedence over even multiple prose cita test("opt-out preserves create semantics for reference-only prose", async () => tracker(async pmRoot => { await create({ type: "Task", title: `References ${repo}#30` }, { pmRoot }); - assert.equal(parseImportOptions({}).noAdoptCited, false); - assert.equal(parseImportOptions({ noAdoptCited: true }).noAdoptCited, true); - const preview = await importing(pmRoot, { "dry-run": true, "no-adopt-cited": true }); + assert.equal(parseImportOptions({}).adoptCited, false); + assert.equal(parseImportOptions({ adoptCited: true }).adoptCited, true); + const preview = await importing(pmRoot, { "dry-run": true, adoptCited: false }); assert.equal(preview.result.wouldImport, 1); assert.equal(preview.result.wouldAdopt, 0); })); @@ -432,7 +435,7 @@ test("batch plans both issues normally instead of transferring a roadmap", async test("only explicit closing keywords, issue labels or a title reference establish citation intent", () => { for (const reference of [issue.html_url, `${repo}#30`]) { for (const prefix of ["Fix", "fixes", "FIXED:", "Close", "Closes", "Closed", "Resolve", "Resolves", "Resolved", "Issue:", "Tracks:", "GH issue", "GH issue:"]) { - for (const field of ["description", "body"]) assert.equal(matchImportedIssue([{ id: "local", [field]: `${prefix} (${reference})` }], issue).via, "citation", `${field}: ${prefix}`); + for (const field of ["description", "body"]) assert.equal(matchImportedIssue([{ id: "local", [field]: `${prefix} (${reference})` }], issue).via, "new", `${field}: ${prefix}`); } assert.equal(matchImportedIssue([{ id: "local", title: `Work for ${reference}`, body: "Related context" }], issue).via, "citation"); for (const prefix of ["Related:", "See", "References:", "Fix for", "Tracks", "Tracking:", "PrefixFixes"]) { @@ -466,7 +469,7 @@ test("incidental hints do not create ambiguity and remain visible alongside elig assert.equal(preview.wouldAdopt, 1); assert.equal((preview.plan as { item: string }[])[0]!.item, eligible.id); assert.deepEqual(preview.citation_candidates, [{ issue: 30, id: related.id, reason: "missing_intent" }]); - await create({ type: "Task", title: "Another owner", body: `Tracks: ${repo}#30` }, { pmRoot }); + await create({ type: "Task", title: "Another owner", body: `Tracks: ${repo}#30\n\nIssue: ${issue.html_url}` }, { pmRoot }); const refused = (await importing(pmRoot, { "dry-run": true })).result; assert.equal(refused.blocked, true); assert.deepEqual(refused.citation_candidates, preview.citation_candidates); @@ -475,7 +478,7 @@ test("incidental hints do not create ambiguity and remain visible alongside elig })); test("a citation without a local tag array still receives the issue provenance tag", async () => { - const entry = await prepareGithubImport(issue, repo, parseImportOptions({}), undefined, undefined, undefined, + const entry = await prepareGithubImport(issue, repo, parseImportOptions({ adoptCited: true }), undefined, undefined, undefined, { items: [{ id: "local", title: `${repo}#30` }], releases: [], timeline: async () => [] }); assert.ok(entry!.tags.includes(`gh:${repo}#30`)); }); @@ -484,3 +487,136 @@ test("idempotency requires the provenance tag on an adopt too", () => { const entry = { issueNumber: 30, tags: [], identity: { via: "citation" }, match: { id: "local" } } as unknown as PreparedGithubImport; assert.throws(() => verifyImportIdempotency([], [entry], repo), /without a provenance tag/); }); + +for (const atomic of [false, true]) for (const adoptCited of [false, true]) { + test(`${atomic ? "atomic" : "plain"} explicit opt-in ${adoptCited} controls the pm-review Issue label in a non-git tracker`, async () => tracker(async pmRoot => { + const local = (await create({ type: "Task", title: "Completed local fix", body: `Issue: ${issue.html_url}`, + status: "closed", closeReason: "Already landed", tags: "local" }, { pmRoot })).item; + const before = (await get(local.id, {}, { pmRoot })).item; + const preview = (await importing(pmRoot, { atomic, adoptCited, gate: true, "dry-run": true })).result; + assert.equal(preview.wouldAdopt, adoptCited ? 1 : 0); + assert.equal(preview.wouldImport, adoptCited ? 0 : 1); + assert.deepEqual((await get(local.id, {}, { pmRoot })).item, before); + const result = (await importing(pmRoot, { atomic, adoptCited })).result; + assert.equal(result.adopted, adoptCited ? 1 : 0); + assert.equal(result.imported, adoptCited ? 0 : 1); + assert.equal((await items(pmRoot)).length, adoptCited ? 1 : 2); + if (!adoptCited) { + assert.deepEqual(preview.citation_candidates, [{ issue: 30, id: local.id, reason: "adoption_disabled" }]); + assert.deepEqual(result.citation_candidates, preview.citation_candidates); + assert.deepEqual((await get(local.id, {}, { pmRoot })).item, before); + } + })); +} + +test("explicit opt-in defaults off in SDK options and reports competing eligible candidates without refusing", () => { + assert.equal(parseImportOptions({}).adoptCited, false); + assert.equal(parseImportOptions({ "adopt-cited": true }).adoptCited, true); + assert.equal(parseImportOptions({ adoptCited: true }).adoptCited, true); + const owners = [{ id: "a", description: `Fixes ${repo}#30` }, { id: "b", body: `Issue: ${issue.html_url}` }]; + const result = resolveImportedIssue(owners, issue); + assert.equal(result.via, "new"); + assert.deepEqual(result.citation_candidates, owners.map(owner => ({ id: owner.id, reason: "adoption_disabled" }))); + assert.throws(() => resolveImportedIssue(owners, issue, undefined, true), AmbiguousCitationError); +}); + +test("normalized allowlist refuses all five review rounds with explicit opt-in", () => { + const cases = [ + { description: `Related: ${repo}#30`, reason: "missing_intent" }, + { description: `Fixes ${repo}#30 and ${repo}#31`, reason: "multiple_issues" }, + ...["https://github.com/acme/widgets/issues/31/", "**https://github.com/acme/widgets/issues/31**", + "**https://github.com/other/widgets/issues/30**", "[other](https://github.com/other/widgets/issues/31)", + "[other](https://github.com/other/widgets/issues%2F31)", "issues%2F31"].map(other => ({ description: `Fixes ${repo}#30 and ${other}`, reason: "multiple_issues" })), + ]; + for (const { description, reason } of cases) { + for (const enabled of [false, true]) { + const result = resolveImportedIssue([{ id: "local", status: "closed", description }], issue, undefined, enabled); + assert.equal(result.via, "new", description); + assert.deepEqual(result.citation_candidates, [{ id: "local", reason }], description); + } + } +}); + +test("normalized allowlist accepts only anchored description, body URL label and title forms", () => { + for (const keyword of ["Fix", "Fixes", "Fixed", "Close", "Closes", "Closed", "Resolve", "Resolves", "Resolved"]) { + for (const reference of [`${repo}#30`, issue.html_url]) { + assert.equal(matchImportedIssue([{ id: "local", description: `${keyword} ${reference} . Additional context` }], issue).via, "citation"); + for (const description of [`Context: ${keyword} ${reference}`, `${keyword}: ${reference}`]) { + assert.equal(matchImportedIssue([{ id: "local", description }], issue).via, "new", description); + } + assert.equal(matchImportedIssue([{ id: "local", body: `${keyword} ${reference}` }], issue).via, "new"); + } + } + for (const label of ["Issue:", "Tracks:"]) { + assert.equal(matchImportedIssue([{ id: "local", body: `${label} ${issue.html_url}` }], issue).via, "citation"); + assert.equal(matchImportedIssue([{ id: "local", body: `${label} <${issue.html_url}>` }], issue).via, "citation"); + for (const body of [`${label} ${repo}#30`, `Context ${label} ${issue.html_url}`, `${label} ${issue.html_url} additional text`]) { + assert.equal(matchImportedIssue([{ id: "local", body }], issue).via, "new", body); + } + } +}); + +test("normalized allowlist decodes named/numeric entities, percent layers, Unicode and Markdown destinations", () => { + const references = [issue.html_url.replaceAll("/", "/"), issue.html_url.replaceAll("/", "/"), + issue.html_url.replaceAll("/", "/"), encodeURIComponent(issue.html_url), encodeURIComponent(encodeURIComponent(issue.html_url)), + issue.html_url.replaceAll("/", "&sol;"), issue.html_url.replace("github", "git\u200bh\u202eub"), + [...issue.html_url].map(char => String.fromCharCode(char.charCodeAt(0) + 0xfee0)).join(""), + issue.html_url.replace("issues/", "issues%FF%2F"), `[target](${issue.html_url})`, `[target](<${issue.html_url}>)`]; + for (const reference of references) { + const result = matchImportedIssue([{ id: "local", body: `Issue: ${reference}` }], issue); + assert.equal(result.via, reference.includes("%FF") ? "new" : "citation", reference); + } + for (const body of [`Issue: [target][issue]\n\n[issue]: ${issue.html_url}`, `Issue: [issue][]\n\n[issue]: <${issue.html_url}>`, + `Issue: [issue]\n\n[issue]: ${issue.html_url}`, `Issue: ${issue.html_url}\n\n[unused] text`, `Issue: ${issue.html_url}\r\n\r\nContext`, `Issue:\n${issue.html_url}`]) { + assert.equal(matchImportedIssue([{ id: "local", body }], issue).via, "citation", body); + } + for (const other of ["https://github.com/other/widgets/issues/3\n1", "https://github.com/other/\nwidgets/issues/30", + "https://github.com/other/widgets/issues/31", "[other/widgets#31](https://github.com/acme/widgets/issues/30)", + "https://github.com/acme/widgets/pull/30", "https://github.com/acme/widgets", "https://github.com/other/widgets/issues%FF%2F31"]) { + const result = matchImportedIssue([{ id: "local", description: `Fixes ${repo}#30`, body: other }], issue); + assert.equal(result.via, "new", other); + assert.deepEqual(result.citation_candidates, [{ id: "local", reason: "multiple_issues" }]); + } + const deeplyEncoded = "&" + "amp;".repeat(12) + "sol;31"; + const result = matchImportedIssue([{ id: "local", description: `Fixes ${repo}#30`, body: `issues${deeplyEncoded}` }], issue); + assert.equal(result.via, "new"); + assert.deepEqual(result.citation_candidates, [{ id: "local", reason: "normalization_limit" }]); +}); + +test("normalized allowlist fuzz rejects random repository, number, decoration and encoding second references", t => { + let seed = 0x53ce71a9; + /** Reproduce encoded, decorated second-reference samples on Node and Bun. */ + const next = (bound: number): number => { + seed ^= seed << 13; seed ^= seed >>> 17; seed ^= seed << 5; + return (seed >>> 0) % bound; + }; + const encoders = [(value: string) => value, (value: string) => value.replaceAll("/", "/"), + (value: string) => value.replaceAll("/", "/"), (value: string) => value.replaceAll("/", "/"), + (value: string) => encodeURIComponent(value), (value: string) => value.replaceAll("/", "%2F"), + (value: string) => [...value].map(char => `&#${char.codePointAt(0)!};`).join(""), + (value: string) => [...value].join("\u200b\u202e"), (value: string) => value.replace("/issues/", "/issues/\n")]; + const wrappers = [["**", "**"], ["*", "*"], ["__", "__"], ["_", "_"], ["`", "`"], ["```text\n", "\n```"], + ["<", ">"], ["[reference](", ")"], ["![reference](", ")"], ["~~", "~~"], ["\\*", "\\*"], ["> ", "\n"]]; + for (let sample = 0; sample < 4096; sample++) { + const sourceRepo = next(2) ? repo : `owner-${next(10000)}/repo-${next(10000)}`; + const number = sourceRepo === repo ? 31 + next(10000) : next(2) ? 30 : 31 + next(10000); + const references = [`https://github.com/${sourceRepo}/issues/${number}`, `https://github.com/${sourceRepo}/pull/${number}`, + `${sourceRepo}#${number}`, `#${number}`, `issues/${number}`, `pull/${number}`]; + let other = references[next(references.length)]!; + const layers = 1 + next(3); + for (let layer = 0; layer < layers; layer++) other = encoders[next(encoders.length)]!(other); + const depth = 1 + next(5); + for (let layer = 0; layer < depth; layer++) { + const [left, right] = wrappers[next(wrappers.length)]!; + other = left + other + right; + } + for (const field of ["title", "description", "body"] as const) { + const local = { id: "local", title: "Local fix", description: `Fixes ${repo}#30`, body: "" }; + local[field] += ` and ${other}`; + const result = matchImportedIssue([local], issue); + assert.equal(result.via, "new", `seed 0x53ce71a9, sample ${sample}, ${field}: ${other}`); + assert.equal(result.citation_candidates?.[0]?.id, "local"); + } + } + t.diagnostic("seed 0x53ce71a9: 4096 encoded decoration samples, 12288 field cases"); +}); diff --git a/test/import-fidelity.test.ts b/test/import-fidelity.test.ts index cd83f045..eacc5526 100644 --- a/test/import-fidelity.test.ts +++ b/test/import-fidelity.test.ts @@ -288,7 +288,7 @@ test("citation opt-out excludes prose while retaining structured identity matchi const references = ["one", "two"].map(id => ({ id, description: `Blocked by ${issue.html_url}#issuecomment-1`, body: issue.html_url, source_description: issue.html_url, unrelated_url: issue.html_url })); assert.doesNotThrow(() => matchImportedIssue(references, issue, undefined, false)); - assert.deepEqual(matchImportedIssue(references, issue, undefined, false), { item: undefined, linkOnly: false, via: "new" }); + assert.deepEqual(matchImportedIssue(references, issue, undefined, false), { item: undefined, linkOnly: false, via: "new", citation_candidates: [{ id: "one", reason: "missing_intent" }, { id: "two", reason: "missing_intent" }] }); const owner = { id: "owner", github_url: issue.html_url }; assert.equal(matchImportedIssue([...references, owner], issue).item, owner); assert.throws(() => matchImportedIssue([owner, { ...owner, id: "competitor" }], issue), /multiple existing owners/); diff --git a/test/import-gate.test.ts b/test/import-gate.test.ts index a524ea10..f48e9935 100644 --- a/test/import-gate.test.ts +++ b/test/import-gate.test.ts @@ -19,6 +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 { createExtensionTestHarness, type ExtensionTestHarness } from "@unbrained/pm-cli/sdk/testing"; import extension, { @@ -1702,3 +1703,38 @@ test("dispatch handles disabled, invalid, failed, and unpublished caller workflo } } finally { fs.rmSync(base, { recursive: true, force: true }); } }); + + +for (const adoptCited of [undefined, "false", "true"]) { + test(`reusable workflow explicit opt-in ${adoptCited ?? "unset"} governs preview import and repeat`, async () => { + const { root, base, git } = initSyncRepo(); + try { + const env = await prepareWorkflowFixture(root, base); + const pmRoot = path.join(root, ".agents/pm"); + const local = (await create({ type: "Issue", title: "Local completed fix", body: "Issue: https://github.com/acme/widgets/issues/1", + status: "closed", closeReason: "Completed locally" }, { pmRoot })).item; + const before = (await get(local.id, {}, { pmRoot })).item; + assert.equal(git(["add", ".agents/pm"]).status, 0); + assert.equal(git(["commit", "-qm", "local issue context"]).status, 0); + const handle = githubHandler([issue(1, "Upstream issue", "Public issue context")]); + await withMockGithub(handle, async () => { + const result = await executeWorkflow(workflowShell(["Resolve sync commit scope", "Preview GitHub to pm plan", + "Gated GitHub to pm import", "Verify repeat import is a no-op"]), root, + { ...env, ADOPT_CITED: adoptCited, PM_GITHUB_API_BASE: process.env.PM_GITHUB_API_BASE }); + assert.equal(result.code, 0, result.stdout + result.stderr); + assert.match(result.stderr, adoptCited === "true" ? /#1 adopt:/ : /#1 import:/); + const actual = (await get(local.id, {}, { pmRoot })).item; + if (adoptCited === "true") assert.equal(actual.github_url, "https://github.com/acme/widgets/issues/1"); + else assert.deepEqual(actual, before); + const files = fs.readdirSync(path.join(pmRoot, "issues")).filter(file => file.endsWith(".toon")); + assert.equal(files.length, adoptCited === "true" ? 1 : 2); + }); + } finally { fs.rmSync(base, { recursive: true, force: true }); } + }); +} + +test("reusable workflow adoption input defaults false and the example retains that default", () => { + assert.match(workflow, /adopt-cited:\n description: [^\n]+\n type: boolean\n required: false\n default: false/); + assert.match(workflow, /ADOPT_CITED: \$\{\{ inputs.adopt-cited \}\}/); + assert.match(callerExample, /adopt-cited: false/); +}); diff --git a/test/import-regex.test.ts b/test/import-regex.test.ts index 915572af..1ff7a641 100644 --- a/test/import-regex.test.ts +++ b/test/import-regex.test.ts @@ -36,7 +36,8 @@ test("ownership declaration parity with citation adoption disabled: colon, white old === url ? "github-declaration" : "new", JSON.stringify(description)); } const citing = matchImportedIssue([{ id: "citing", description: `GitHub issue:${url}` }], issue); - assert.equal(citing.via, "citation"); + assert.equal(citing.via, "new"); + assert.deepEqual(citing.citation_candidates, [{ id: "citing", reason: "missing_intent" }]); assert.equal(citing.linkOnly, false); }); diff --git a/test/import-selection.test.ts b/test/import-selection.test.ts index 2293fef3..8f81aa49 100644 --- a/test/import-selection.test.ts +++ b/test/import-selection.test.ts @@ -254,7 +254,7 @@ test("built package dispatch advertises and honors selectors and limits through assert.equal(help.code, 0, help.stderr); assert.match(help.stdout, /--issue/); assert.match(help.stdout, /--limit/); - assert.match(help.stdout, /--no-adopt-cited/); + assert.match(help.stdout, /--adopt-cited/); const selected = await call(["unbraind/pm-web", "--issue", "189", "--dry-run", "--json"]); assert.equal(selected.code, 0, selected.stderr); assert.equal((JSON.parse(selected.stdout) as { wouldImport: number }).wouldImport, 1); @@ -264,13 +264,14 @@ test("built package dispatch advertises and honors selectors and limits through assert.equal(server.requests.length, 2); assert.equal(server.requests[0]!.url, "/repos/unbraind/pm-web/issues/189"); assert.deepEqual(readPmItems(pmRoot), []); - await create({ type: "Task", title: "Fix unbraind/pm-web#189" }, { pmRoot }); - const adopted = await call(["unbraind/pm-web", "--issue", "189", "--dry-run", "--json"]); + await create({ type: "Task", title: "Local issue fix", body: "Issue: https://github.com/unbraind/pm-web/issues/189" }, { pmRoot }); + const adopted = await call(["unbraind/pm-web", "--issue", "189", "--dry-run", "--adopt-cited", "--json"]); assert.equal(adopted.code, 0, adopted.stderr); assert.equal((JSON.parse(adopted.stdout) as { wouldAdopt: number }).wouldAdopt, 1); - const optedOut = await call(["unbraind/pm-web", "--issue", "189", "--dry-run", "--no-adopt-cited", "--json"]); + const optedOut = await call(["unbraind/pm-web", "--issue", "189", "--dry-run", "--json"]); assert.equal(optedOut.code, 0, optedOut.stderr); assert.equal((JSON.parse(optedOut.stdout) as { wouldImport: number }).wouldImport, 1); + assert.equal((JSON.parse(optedOut.stdout) as { citation_candidates: { reason: string }[] }).citation_candidates[0]!.reason, "adoption_disabled"); assert.equal(readPmItems(pmRoot).length, 1); }); })); From 51dea17e5e70448fc771211a4e571732572a3bb0 Mon Sep 17 00:00:00 2001 From: SteveBot <1153461+unbraind@users.noreply.github.com> Date: Fri, 23 Oct 2026 16:35:35 +0200 Subject: [PATCH 11/18] Bound Markdown definition normalization to its destination --- .agents/pm/features/pm-github-cited-issue-adoption.toon | 6 ++++-- .agents/pm/history/pm-github-cited-issue-adoption.jsonl | 4 ++++ docs/cited-issue-adoption-validation.md | 4 ++++ import-fidelity.ts | 2 +- 4 files changed, 13 insertions(+), 3 deletions(-) diff --git a/.agents/pm/features/pm-github-cited-issue-adoption.toon b/.agents/pm/features/pm-github-cited-issue-adoption.toon index 4ae4cc67..f597fa48 100644 --- a/.agents/pm/features/pm-github-cited-issue-adoption.toon +++ b/.agents/pm/features/pm-github-cited-issue-adoption.toon @@ -6,10 +6,10 @@ status: in_progress priority: 2 tags[3]: changelog-unreleased,github,import created_at: "2026-10-23T07:16:54.041Z" -updated_at: "2026-10-23T14:25:02.020Z" +updated_at: "2026-10-23T14:35:15.541Z" author: codex-agent acceptance_criteria: "Adoption is off by default across CLI, SDK and reusable sync, enabled only with --adopt-cited, adoptCited true or adopt-cited true respectively. Default import retains normal create/structured-match behavior and read-only citation_candidates without fallback ambiguity refusal. Document creating items with gh:owner/repo#N as primary duplicate prevention; Normalize HTML named/numeric entities and percent bytes repeatedly until stable with a bounded refusal, apply NFKC and remove invisible/bidi controls, unwrap Markdown links/reference definitions and join soft breaks before anchored ownership checks while retaining source for conservative exclusion; Only title qualified references, leading description closing-keyword references, or whole body Issue:/Tracks: URL lines can authorize opt-in adoption. Any other normalized GitHub URL, foreign repository, other number or unattributed reference refuses adoption and reports candidate hints. Preserve structured identity precedence, single-owner upsert, eligible-owner ambiguity refusal, receipts, idempotency and non-git support; Keep all review reproductions and prior shape inputs, add encoded-reference fuzzing and explicit pm-review Issue label positives/default creates, record behavioral function/workflow reverts and public dry-run receipts, run targeted checks and wait for exact-head npm/Bun CI release gates" -comments[37]{created_at,author,text}: +comments[39]{created_at,author,text}: "2026-10-23T07:17:26.861Z",codex-agent,"Overlap review: pm-github-c093 owns structured URL/provider back-link import matching, pm-github-identity-collisions diagnoses and repairs structured ownership, Projects import matches board and issue provenance, and export upserts linked issues. None adopts arbitrary exact prose citations. This feature adds a fallback only after existing import matching, preserving those paths. Scheduled gated sync already invokes import, so it inherits the default. Plan ambiguous citations before writes and refuse the batch with actionable candidate IDs." "2026-10-23T07:24:01.857Z",codex-agent,"Implementation: strong ownership matching stays first; exact qualified title/description/body citations fall back to a single existing item. All imports now prepare identities before writes, and ambiguous citations expose candidate IDs plus an exact tagging remedy. Adoption retains local tags and reports a separate count; provider-owned records retain link-only behavior. New tests use real trackers with a PATH containing node and the pinned pm CLI but no git, plus real git trackers for write-gate acceptance." "2026-10-23T07:27:45.693Z",codex-agent,"New citation suite: 13/13 passed, including plain/atomic adoption in non-git trackers, write-gate acceptance, retry identity, ambiguity refusal, punctuation, repo/number boundaries, structured ownership precedence and provider link-only preservation. Live pm-review main aa16d855df1f7b74afd3453617f77e92c44d493e already tags pm-review-withdrawn-lineage for issue 30, so an untouched current-main copy correctly plans update rather than adopt. Preserve this current-state result and locate the pre-tag historical tracker for a separate adoption replay. SDK reads may create runtime caches; verify original tracked file bytes independently." @@ -47,6 +47,8 @@ comments[37]{created_at,author,text}: "2026-10-23T14:23:47.925Z",codex-agent,"Validation complete: all four pm-linked commands passed through pm test --run --progress: adoption 44/44, regex/performance 8/8, executable workflow 4/4, packed CLI dispatch 1/1. Additional selector/fidelity sweep passed 80/80. All prior review inputs remain, with explicit opt-in and anchored-policy refusal expectations. New seed 0x53ce71a9 generates 4096 repository/number/decoration/encoding samples checked in all three fields (12288 cases). Default-off modes preserve local fields/status; closed incidental items remain closed. Scoped coverage found no uncovered new-helper branches." "2026-10-23T14:23:48.538Z",codex-agent,"Public dry runs replay saved read-only tracker snapshots with live issue/timeline data in both modes. pm-review pre-tag f72250e4c5ee8dfafaf57ee8afe20ddd3cbc993e: default 1 create with adoption_disabled hint, opt-in 1 adopt into pm-review-withdrawn-lineage. Tagged e1fc83a81507ca6a91f1675b74863c050f1a89f1: 1 update in both. pm-context 7d7479ae77c13d01c54b2b4560ad54091ffed788, issues 154,153,136,53,81: 2 creates and 3 updates with 3 multiple_issues hints in both. Zero skips, all393 original files retain SHA-256 hashes in both modes. README and reusable caller docs emphasize tag-at-creation and default-off opt-in policy." "2026-10-23T14:25:00.965Z",codex-agent,"Final local checks pass: typecheck, build:test, build, lint, docstrings, zero duplication across 18 authored modules, privacy, strict tracker health and diff checks. All four linked test commands passed with zero failures/skips. Releasing the claim and leaving the item in_progress for orchestrator verification. Full npm/Bun CI release-gate counts and run links will be recorded in PR 169 after completion at the pushed head. No dependency pins were bumped, and no coverage thresholds changed." + "2026-10-23T14:29:16.789Z",codex-agent,"Exact-head CodeQL alert check found js/polynomial-redos in Markdown reference-definition normalization, despite the CodeQL analysis workflow completing. Remove the unnecessary overlapping trailing match instead of suppressing the alert, rebuild and re-run affected checks, then push a new head and require fresh npm/Bun and CodeQL evidence. The prior head is not final gate evidence." + "2026-10-23T14:35:09.432Z",codex-agent,"Removed only the unnecessary trailing wildcard from Markdown reference-definition normalization, avoiding the overlapping regex reported by CodeQL without suppression or threshold changes. Rebuilt and re-ran all four linked commands through pm test --run --progress: 44 adoption, 8 regex/performance, 4 executable workflow and 1 packed CLI tests passed, no failures/skips/timeouts. Typecheck, builds, lint, docstrings, zero duplication and diff checks pass. Shared-host contention increased wall time but all original budgets passed. Fresh CI release and CodeQL evidence is required on the next pushed head." files[13]{path,scope}: .github/workflows/pm-github-sync.yml,project import-fidelity.ts,project diff --git a/.agents/pm/history/pm-github-cited-issue-adoption.jsonl b/.agents/pm/history/pm-github-cited-issue-adoption.jsonl index c9b306af..dcc807b6 100644 --- a/.agents/pm/history/pm-github-cited-issue-adoption.jsonl +++ b/.agents/pm/history/pm-github-cited-issue-adoption.jsonl @@ -74,3 +74,7 @@ {"hash_algorithm":"sha256","ts":"2026-10-23T14:24:59.806Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":{"value":"implementer","source":"argv"},"topic":null},"op":"update","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T14:24:59.806Z"},{"op":"replace","path":"/metadata/description","value":"Keep normal import creates and structured identity matching by default while reporting read-only citation candidates. Require explicit opt-in, bounded text normalization and anchored ownership checks before reusing a cited item. Document provenance tags at item creation as primary prevention, and validate encoded references, no-git persistence, workflow propagation and public dry runs."},{"op":"replace","path":"/metadata/title","value":"Require opt-in before adopting cited GitHub issue items"}],"before_hash":"d59c7004ed67adad5af1ca415026120e8804492b5766a66d53caa4f5642005f3","after_hash":"26994279418d0574b586d316f37e7a8ef3f5357ef40c65ae2fa1b64c2bea1fe4","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"192405f05826b99abb7e760a7e8afe629cd6e26d36968cfd82c9796616bfdeda"} {"hash_algorithm":"sha256","ts":"2026-10-23T14:25:00.966Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/36","value":{"created_at":"2026-10-23T14:25:00.965Z","author":"codex-agent","text":"Final local checks pass: typecheck, build:test, build, lint, docstrings, zero duplication across 18 authored modules, privacy, strict tracker health and diff checks. All four linked test commands passed with zero failures/skips. Releasing the claim and leaving the item in_progress for orchestrator verification. Full npm/Bun CI release-gate counts and run links will be recorded in PR 169 after completion at the pushed head. No dependency pins were bumped, and no coverage thresholds changed."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T14:25:00.966Z"}],"before_hash":"26994279418d0574b586d316f37e7a8ef3f5357ef40c65ae2fa1b64c2bea1fe4","after_hash":"fff1b815f777d065c58a3105bb3a386eddac4580446983ab5c67403ec89d0afe","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"750373fd281efe29e200ca35ad9fc2410116a2fb5913fc9e265afc45f216b514"} {"hash_algorithm":"sha256","ts":"2026-10-23T14:25:02.020Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":{"value":"implementer","source":"argv"},"topic":null},"op":"release","patch":[{"op":"remove","path":"/metadata/claim_principal"},{"op":"remove","path":"/metadata/assignee"},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T14:25:02.020Z"}],"before_hash":"fff1b815f777d065c58a3105bb3a386eddac4580446983ab5c67403ec89d0afe","after_hash":"d40bfcafd910c162bb29ee088273bfebcbf883e876c995e7258a02d228ae1894","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"000e7a949fed18738be676f2f113b771cfe07b66a5e72c8f3ca4b898420c0c0f"} +{"hash_algorithm":"sha256","ts":"2026-10-23T14:29:11.659Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":{"value":"implementer","source":"argv"},"topic":null},"op":"claim","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T14:29:11.659Z"},{"op":"add","path":"/metadata/assignee","value":"codex-agent"},{"op":"add","path":"/metadata/claim_principal","value":"codex-agent"}],"before_hash":"d40bfcafd910c162bb29ee088273bfebcbf883e876c995e7258a02d228ae1894","after_hash":"ebb29b129fdb407296e20f891752ea77c0d80530e5f117f09f739572823e7f9e","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"93d98dd20f0d5c84d6c06ad8ce4a0ed57d9b84970ae143fd5d1c0a78f099f666"} +{"hash_algorithm":"sha256","ts":"2026-10-23T14:29:16.797Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/37","value":{"created_at":"2026-10-23T14:29:16.789Z","author":"codex-agent","text":"Exact-head CodeQL alert check found js/polynomial-redos in Markdown reference-definition normalization, despite the CodeQL analysis workflow completing. Remove the unnecessary overlapping trailing match instead of suppressing the alert, rebuild and re-run affected checks, then push a new head and require fresh npm/Bun and CodeQL evidence. The prior head is not final gate evidence."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T14:29:16.797Z"}],"before_hash":"ebb29b129fdb407296e20f891752ea77c0d80530e5f117f09f739572823e7f9e","after_hash":"a9c9c2681ac10a47b680c2882a2791db9f1c82ac400e1558fd23f7bb0d7df17b","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"03bd00aae0c05258d4cc02d576f16108b8ebc2621dba312864d4aae7a626e424"} +{"hash_algorithm":"sha256","ts":"2026-10-23T14:35:09.447Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/38","value":{"created_at":"2026-10-23T14:35:09.432Z","author":"codex-agent","text":"Removed only the unnecessary trailing wildcard from Markdown reference-definition normalization, avoiding the overlapping regex reported by CodeQL without suppression or threshold changes. Rebuilt and re-ran all four linked commands through pm test --run --progress: 44 adoption, 8 regex/performance, 4 executable workflow and 1 packed CLI tests passed, no failures/skips/timeouts. Typecheck, builds, lint, docstrings, zero duplication and diff checks pass. Shared-host contention increased wall time but all original budgets passed. Fresh CI release and CodeQL evidence is required on the next pushed head."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T14:35:09.447Z"}],"before_hash":"a9c9c2681ac10a47b680c2882a2791db9f1c82ac400e1558fd23f7bb0d7df17b","after_hash":"c4a192d5a1dcd153ee8249d514d1174a3f50ce50d9783ad201f7af3c927f90d5","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"f60d483237d6a536227d1c4faa5075aefd563e1412aa6a9933671206f0e2d549"} +{"hash_algorithm":"sha256","ts":"2026-10-23T14:35:15.541Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":{"value":"implementer","source":"argv"},"topic":null},"op":"release","patch":[{"op":"remove","path":"/metadata/claim_principal"},{"op":"remove","path":"/metadata/assignee"},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T14:35:15.541Z"}],"before_hash":"c4a192d5a1dcd153ee8249d514d1174a3f50ce50d9783ad201f7af3c927f90d5","after_hash":"640d1feb69f7a62eb3044b9fb0adc1437f7b31d4d0966821df3537f8e3f89903","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"7f1f163e992b884feade87570757aae55391da982b7ad0756e5cd9375b4179ab"} diff --git a/docs/cited-issue-adoption-validation.md b/docs/cited-issue-adoption-validation.md index f9cfc610..37b86153 100644 --- a/docs/cited-issue-adoption-validation.md +++ b/docs/cited-issue-adoption-validation.md @@ -86,6 +86,10 @@ both TypeScript builds, lint, docstrings and zero duplication pass locally. Scoped coverage preflight found no uncovered branches in the new normalization, exclusion and anchored ownership functions. Full npm/Bun release-gate evidence and CodeQL run links are recorded on PR 169 at the final pushed head. +The initial CodeQL alert check flagged an overlapping trailing wildcard in +reference-definition normalization. The unnecessary trailing match was removed +without suppressing the rule or changing thresholds; current normalization and +workflow regressions are rechecked before collecting fresh exact-head evidence. ## Behavioral revert proofs diff --git a/import-fidelity.ts b/import-fidelity.ts index 6e9b9dac..bd57ced9 100644 --- a/import-fidelity.ts +++ b/import-fidelity.ts @@ -111,7 +111,7 @@ function normalizeCitationText(input: string): { text: string; source: string; s /** Unwrap link destinations/definitions for anchored intent, retaining the normalized source for exclusion. */ function citationDisplayText(source: string): string { const definitions = new Map(); - const text = source.replace(/^[ \t]{0,3}\[([^[\]\n]+)\]:[ \t]*(?:<([^>\n]+)>|(\S+)).*$/gm, + const text = source.replace(/^[ \t]{0,3}\[([^[\]\n]+)\]:[ \t]*(?:<([^>\n]+)>|(\S+))/gm, (_raw: string, label: string, angle: string | undefined, destination: string | undefined) => { definitions.set(label.trim().toLowerCase(), angle ?? destination!); return angle ?? destination!; From 894dbdb7a38a7d44af890d472c9557d615b610af Mon Sep 17 00:00:00 2001 From: SteveBot <1153461+unbraind@users.noreply.github.com> Date: Fri, 23 Oct 2026 17:23:36 +0200 Subject: [PATCH 12/18] Avoid redundant scans during citation normalization --- .../pm-github-cited-issue-adoption.toon | 7 ++- .../pm-github-cited-issue-adoption.jsonl | 5 ++ docs/cited-issue-adoption-validation.md | 27 +++++++++ import-fidelity.ts | 56 ++++++++++++------- 4 files changed, 74 insertions(+), 21 deletions(-) diff --git a/.agents/pm/features/pm-github-cited-issue-adoption.toon b/.agents/pm/features/pm-github-cited-issue-adoption.toon index f597fa48..edbb7bda 100644 --- a/.agents/pm/features/pm-github-cited-issue-adoption.toon +++ b/.agents/pm/features/pm-github-cited-issue-adoption.toon @@ -6,10 +6,10 @@ status: in_progress priority: 2 tags[3]: changelog-unreleased,github,import created_at: "2026-10-23T07:16:54.041Z" -updated_at: "2026-10-23T14:35:15.541Z" +updated_at: "2026-10-23T15:23:05.243Z" author: codex-agent acceptance_criteria: "Adoption is off by default across CLI, SDK and reusable sync, enabled only with --adopt-cited, adoptCited true or adopt-cited true respectively. Default import retains normal create/structured-match behavior and read-only citation_candidates without fallback ambiguity refusal. Document creating items with gh:owner/repo#N as primary duplicate prevention; Normalize HTML named/numeric entities and percent bytes repeatedly until stable with a bounded refusal, apply NFKC and remove invisible/bidi controls, unwrap Markdown links/reference definitions and join soft breaks before anchored ownership checks while retaining source for conservative exclusion; Only title qualified references, leading description closing-keyword references, or whole body Issue:/Tracks: URL lines can authorize opt-in adoption. Any other normalized GitHub URL, foreign repository, other number or unattributed reference refuses adoption and reports candidate hints. Preserve structured identity precedence, single-owner upsert, eligible-owner ambiguity refusal, receipts, idempotency and non-git support; Keep all review reproductions and prior shape inputs, add encoded-reference fuzzing and explicit pm-review Issue label positives/default creates, record behavioral function/workflow reverts and public dry-run receipts, run targeted checks and wait for exact-head npm/Bun CI release gates" -comments[39]{created_at,author,text}: +comments[42]{created_at,author,text}: "2026-10-23T07:17:26.861Z",codex-agent,"Overlap review: pm-github-c093 owns structured URL/provider back-link import matching, pm-github-identity-collisions diagnoses and repairs structured ownership, Projects import matches board and issue provenance, and export upserts linked issues. None adopts arbitrary exact prose citations. This feature adds a fallback only after existing import matching, preserving those paths. Scheduled gated sync already invokes import, so it inherits the default. Plan ambiguous citations before writes and refuse the batch with actionable candidate IDs." "2026-10-23T07:24:01.857Z",codex-agent,"Implementation: strong ownership matching stays first; exact qualified title/description/body citations fall back to a single existing item. All imports now prepare identities before writes, and ambiguous citations expose candidate IDs plus an exact tagging remedy. Adoption retains local tags and reports a separate count; provider-owned records retain link-only behavior. New tests use real trackers with a PATH containing node and the pinned pm CLI but no git, plus real git trackers for write-gate acceptance." "2026-10-23T07:27:45.693Z",codex-agent,"New citation suite: 13/13 passed, including plain/atomic adoption in non-git trackers, write-gate acceptance, retry identity, ambiguity refusal, punctuation, repo/number boundaries, structured ownership precedence and provider link-only preservation. Live pm-review main aa16d855df1f7b74afd3453617f77e92c44d493e already tags pm-review-withdrawn-lineage for issue 30, so an untouched current-main copy correctly plans update rather than adopt. Preserve this current-state result and locate the pre-tag historical tracker for a separate adoption replay. SDK reads may create runtime caches; verify original tracked file bytes independently." @@ -49,6 +49,9 @@ comments[39]{created_at,author,text}: "2026-10-23T14:25:00.965Z",codex-agent,"Final local checks pass: typecheck, build:test, build, lint, docstrings, zero duplication across 18 authored modules, privacy, strict tracker health and diff checks. All four linked test commands passed with zero failures/skips. Releasing the claim and leaving the item in_progress for orchestrator verification. Full npm/Bun CI release-gate counts and run links will be recorded in PR 169 after completion at the pushed head. No dependency pins were bumped, and no coverage thresholds changed." "2026-10-23T14:29:16.789Z",codex-agent,"Exact-head CodeQL alert check found js/polynomial-redos in Markdown reference-definition normalization, despite the CodeQL analysis workflow completing. Remove the unnecessary overlapping trailing match instead of suppressing the alert, rebuild and re-run affected checks, then push a new head and require fresh npm/Bun and CodeQL evidence. The prior head is not final gate evidence." "2026-10-23T14:35:09.432Z",codex-agent,"Removed only the unnecessary trailing wildcard from Markdown reference-definition normalization, avoiding the overlapping regex reported by CodeQL without suppression or threshold changes. Rebuilt and re-ran all four linked commands through pm test --run --progress: 44 adoption, 8 regex/performance, 4 executable workflow and 1 packed CLI tests passed, no failures/skips/timeouts. Typecheck, builds, lint, docstrings, zero duplication and diff checks pass. Shared-host contention increased wall time but all original budgets passed. Fresh CI release and CodeQL evidence is required on the next pushed head." + "2026-10-23T15:06:40.793Z",codex-agent,"All four release gates passed at 51dea17, but native Bun on Node 26 failed the unchanged 200000-space ownership performance budget: 85.562 ms against 50 ms, while all44 adoption tests passed. The exclusion scanner repeats identical source/display/joined strings four times when normalization changes nothing. Deduplicate equal views using sets, preserving every distinct normalized/source/joined view and all ownership decisions. Keep the timing limit and gate thresholds unchanged, verify native Bun and a function-only performance revert, then require a fresh CI pass." + "2026-10-23T15:21:29.341Z",codex-agent,"Performance fix preserves all normalized views but scans equal views once, finds issue/hash markers before reading the adjacent repository, and skips only identity Unicode/Markdown/soft-break transforms after decoding. Node source selection via c8 passes52/52 with no uncovered new-helper branches. Native local Bun1.3.5 regex/performance suite passes8/8, ownership200000-space probe22.892ms against unchanged50ms. Typecheck, builds, lint, docstrings and zero duplication across18 modules pass. Source-copy revert restores only normalizeCitationText, citationDisplayText and inspectCitationReferences from51dea17: bun test --test-name-pattern=\\\"ownership whitespace trigger\\\" test/import-regex.test.ts fails the selected test at112.322ms,1 failure,0 passes,7 filtered,exit1. Current anchored checks, tests, adjacent-repository helper and SDK paths remain. CI pinned Bun1.3.14 must provide its own fresh pass. No limits were relaxed." + "2026-10-23T15:22:57.445Z",codex-agent,"Final optimized-code replay confirms unchanged public results in both modes: pre-tag pm-review creates1 by default with adoption_disabled and adopts1 with explicit opt-in, tagged pm-review updates1 in both, pm-context creates2/updates3 in both, zero skips, all393 original files unchanged. Revert command: bun test --test-name-pattern='ownership whitespace trigger' test/import-regex.test.ts. Reverted three performance functions fail1 selected test at112.322ms with7 filtered; fixed native suite passes8/8 at22.892ms for that probe. Node c8 source selection passes52/52, no uncovered new-helper branches. Typecheck, builds, lint, docstrings, zero duplication, strict tracker health and diff checks pass. Release the claim, retain in_progress, push the final optimization and require fresh CI npm/Bun/native gates at the unchanged limits." files[13]{path,scope}: .github/workflows/pm-github-sync.yml,project import-fidelity.ts,project diff --git a/.agents/pm/history/pm-github-cited-issue-adoption.jsonl b/.agents/pm/history/pm-github-cited-issue-adoption.jsonl index dcc807b6..7d7af1f1 100644 --- a/.agents/pm/history/pm-github-cited-issue-adoption.jsonl +++ b/.agents/pm/history/pm-github-cited-issue-adoption.jsonl @@ -78,3 +78,8 @@ {"hash_algorithm":"sha256","ts":"2026-10-23T14:29:16.797Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/37","value":{"created_at":"2026-10-23T14:29:16.789Z","author":"codex-agent","text":"Exact-head CodeQL alert check found js/polynomial-redos in Markdown reference-definition normalization, despite the CodeQL analysis workflow completing. Remove the unnecessary overlapping trailing match instead of suppressing the alert, rebuild and re-run affected checks, then push a new head and require fresh npm/Bun and CodeQL evidence. The prior head is not final gate evidence."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T14:29:16.797Z"}],"before_hash":"ebb29b129fdb407296e20f891752ea77c0d80530e5f117f09f739572823e7f9e","after_hash":"a9c9c2681ac10a47b680c2882a2791db9f1c82ac400e1558fd23f7bb0d7df17b","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"03bd00aae0c05258d4cc02d576f16108b8ebc2621dba312864d4aae7a626e424"} {"hash_algorithm":"sha256","ts":"2026-10-23T14:35:09.447Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/38","value":{"created_at":"2026-10-23T14:35:09.432Z","author":"codex-agent","text":"Removed only the unnecessary trailing wildcard from Markdown reference-definition normalization, avoiding the overlapping regex reported by CodeQL without suppression or threshold changes. Rebuilt and re-ran all four linked commands through pm test --run --progress: 44 adoption, 8 regex/performance, 4 executable workflow and 1 packed CLI tests passed, no failures/skips/timeouts. Typecheck, builds, lint, docstrings, zero duplication and diff checks pass. Shared-host contention increased wall time but all original budgets passed. Fresh CI release and CodeQL evidence is required on the next pushed head."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T14:35:09.447Z"}],"before_hash":"a9c9c2681ac10a47b680c2882a2791db9f1c82ac400e1558fd23f7bb0d7df17b","after_hash":"c4a192d5a1dcd153ee8249d514d1174a3f50ce50d9783ad201f7af3c927f90d5","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"f60d483237d6a536227d1c4faa5075aefd563e1412aa6a9933671206f0e2d549"} {"hash_algorithm":"sha256","ts":"2026-10-23T14:35:15.541Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":{"value":"implementer","source":"argv"},"topic":null},"op":"release","patch":[{"op":"remove","path":"/metadata/claim_principal"},{"op":"remove","path":"/metadata/assignee"},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T14:35:15.541Z"}],"before_hash":"c4a192d5a1dcd153ee8249d514d1174a3f50ce50d9783ad201f7af3c927f90d5","after_hash":"640d1feb69f7a62eb3044b9fb0adc1437f7b31d4d0966821df3537f8e3f89903","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"7f1f163e992b884feade87570757aae55391da982b7ad0756e5cd9375b4179ab"} +{"hash_algorithm":"sha256","ts":"2026-10-23T15:06:35.554Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":{"value":"implementer","source":"argv"},"topic":null},"op":"claim","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T15:06:35.554Z"},{"op":"add","path":"/metadata/assignee","value":"codex-agent"},{"op":"add","path":"/metadata/claim_principal","value":"codex-agent"}],"before_hash":"640d1feb69f7a62eb3044b9fb0adc1437f7b31d4d0966821df3537f8e3f89903","after_hash":"8cd5e7b5c79bbfc0329428a960797fc3230ece47282285598dcb11d2d5080ea7","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"9843497a6c5475a4f9c1d7a80b6ad7b26c781da61938fdaa7bcf5367bb816b2f"} +{"hash_algorithm":"sha256","ts":"2026-10-23T15:06:40.793Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/39","value":{"created_at":"2026-10-23T15:06:40.793Z","author":"codex-agent","text":"All four release gates passed at 51dea17, but native Bun on Node 26 failed the unchanged 200000-space ownership performance budget: 85.562 ms against 50 ms, while all44 adoption tests passed. The exclusion scanner repeats identical source/display/joined strings four times when normalization changes nothing. Deduplicate equal views using sets, preserving every distinct normalized/source/joined view and all ownership decisions. Keep the timing limit and gate thresholds unchanged, verify native Bun and a function-only performance revert, then require a fresh CI pass."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T15:06:40.793Z"}],"before_hash":"8cd5e7b5c79bbfc0329428a960797fc3230ece47282285598dcb11d2d5080ea7","after_hash":"9c3ba7c4b1fd3f8fa84a747a865d78b019c23b8d0835b0f62f4b6fe3eeee3e9a","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"b33d96230cd972946bcd39ee9d1cc310ef0ef94385304a6e08b2d0ee083bcfa0"} +{"hash_algorithm":"sha256","ts":"2026-10-23T15:21:29.341Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/40","value":{"created_at":"2026-10-23T15:21:29.341Z","author":"codex-agent","text":"Performance fix preserves all normalized views but scans equal views once, finds issue/hash markers before reading the adjacent repository, and skips only identity Unicode/Markdown/soft-break transforms after decoding. Node source selection via c8 passes52/52 with no uncovered new-helper branches. Native local Bun1.3.5 regex/performance suite passes8/8, ownership200000-space probe22.892ms against unchanged50ms. Typecheck, builds, lint, docstrings and zero duplication across18 modules pass. Source-copy revert restores only normalizeCitationText, citationDisplayText and inspectCitationReferences from51dea17: bun test --test-name-pattern=\\\"ownership whitespace trigger\\\" test/import-regex.test.ts fails the selected test at112.322ms,1 failure,0 passes,7 filtered,exit1. Current anchored checks, tests, adjacent-repository helper and SDK paths remain. CI pinned Bun1.3.14 must provide its own fresh pass. No limits were relaxed."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T15:21:29.341Z"}],"before_hash":"9c3ba7c4b1fd3f8fa84a747a865d78b019c23b8d0835b0f62f4b6fe3eeee3e9a","after_hash":"ab023bab71128894687d28ab62dede64aa1e0c5b56a54eee76da954600f6810a","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"77ae12303e1afde2b74226e9f7cff915b00a48815faf8b8cd161b64db06b1458"} +{"hash_algorithm":"sha256","ts":"2026-10-23T15:22:57.445Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/41","value":{"created_at":"2026-10-23T15:22:57.445Z","author":"codex-agent","text":"Final optimized-code replay confirms unchanged public results in both modes: pre-tag pm-review creates1 by default with adoption_disabled and adopts1 with explicit opt-in, tagged pm-review updates1 in both, pm-context creates2/updates3 in both, zero skips, all393 original files unchanged. Revert command: bun test --test-name-pattern='ownership whitespace trigger' test/import-regex.test.ts. Reverted three performance functions fail1 selected test at112.322ms with7 filtered; fixed native suite passes8/8 at22.892ms for that probe. Node c8 source selection passes52/52, no uncovered new-helper branches. Typecheck, builds, lint, docstrings, zero duplication, strict tracker health and diff checks pass. Release the claim, retain in_progress, push the final optimization and require fresh CI npm/Bun/native gates at the unchanged limits."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T15:22:57.445Z"}],"before_hash":"ab023bab71128894687d28ab62dede64aa1e0c5b56a54eee76da954600f6810a","after_hash":"cc37244bd446038a19d91313d105223a37f542813e8955ff70bc397003244543","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"3704840961e8719a0aafecba6d1d9c7fbe8fc46bf1d00e9e0df4bd9ebc876727"} +{"hash_algorithm":"sha256","ts":"2026-10-23T15:23:05.243Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":{"value":"implementer","source":"argv"},"topic":null},"op":"release","patch":[{"op":"remove","path":"/metadata/claim_principal"},{"op":"remove","path":"/metadata/assignee"},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T15:23:05.243Z"}],"before_hash":"cc37244bd446038a19d91313d105223a37f542813e8955ff70bc397003244543","after_hash":"676ce3af9a64253a45404e967fd0d67541ca712562f7d3372a086a282a6427de","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"1cf34d7632a92973a166960687cf79136cea2e1e63d754eaacaa1e1d161c5453"} diff --git a/docs/cited-issue-adoption-validation.md b/docs/cited-issue-adoption-validation.md index 37b86153..158a9c78 100644 --- a/docs/cited-issue-adoption-validation.md +++ b/docs/cited-issue-adoption-validation.md @@ -91,6 +91,33 @@ reference-definition normalization. The unnecessary trailing match was removed without suppressing the rule or changing thresholds; current normalization and workflow regressions are rechecked before collecting fresh exact-head evidence. +## Normalization performance + +The first full run passed all four release gates, but native Bun on Node 26 +measured **85.562 ms** on the existing 200,000-space ownership probe against its +unchanged **50 ms** limit. Normalized source/display/joined views were scanned +repeatedly even when identical, and Unicode/Markdown transforms did work on +plain text where they are identity operations. + +The optimized path keeps every distinct view, scans issue/hash marker positions +before reading their adjacent repository, and skips only identity transforms: +Unicode transforms on printable ASCII/line whitespace, Markdown link processing +when no `[` exists, and soft-line joining when no newline exists. Entity/percent +decoding still occurs before these decisions. No ownership check is bypassed. +All existing shape, encoding, positive and non-git cases remain. Local native +Bun **1.3.5** passes **8/8**, with the ownership probe at **22.892 ms**; CI uses +the pinned **1.3.14** and must pass its own unchanged budget. The updated scoped +Node selection passes **52/52**, with no uncovered new-helper branches. + +A source-copy revert restores only `normalizeCitationText`, `citationDisplayText` +and `inspectCitationReferences` from `51dea17`, keeping current tests, anchored +ownership checks, the new adjacent-repository helper and real SDK paths. Run +`bun test --test-name-pattern='ownership whitespace trigger' test/import-regex.test.ts`. +The selected test fails at **112.322 ms**, **1 failure / 0 passes / 7 filtered**, +exit 1. The fixed suite passes at the same 50 ms limit. No thresholds or timeouts +were relaxed. This is supplemental local evidence; completed CI at the final +pushed head supplies the full npm/Bun gate evidence. + ## Behavioral revert proofs Each proof uses an isolated source copy with current tests and real SDK diff --git a/import-fidelity.ts b/import-fidelity.ts index bd57ced9..40282a61 100644 --- a/import-fidelity.ts +++ b/import-fidelity.ts @@ -99,9 +99,12 @@ function citationReferences(text: string, repoHint: string): { key: string; inde function normalizeCitationText(input: string): { text: string; source: string; stable: boolean } { let text = input; for (let pass = 0; pass < 8; pass++) { - const decoded = decodeHTML(text.replace(/(?:%[a-f0-9]{2})+/gi, encoded => - Buffer.from(encoded.slice(1).split("%").map(byte => Number.parseInt(byte, 16))).toString("utf8"))) - .normalize("NFKC").replace(/\p{Default_Ignorable_Code_Point}/gu, "").replace(/\r\n?/g, "\n"); + const entities = decodeHTML(text.replace(/(?:%[a-f0-9]{2})+/gi, encoded => + Buffer.from(encoded.slice(1).split("%").map(byte => Number.parseInt(byte, 16))).toString("utf8"))); + // Both Unicode transforms are identity on printable ASCII and line whitespace. + const unicode = /[^\x20-\x7e\t\r\n]/.test(entities) + ? entities.normalize("NFKC").replace(/\p{Default_Ignorable_Code_Point}/gu, "") : entities; + const decoded = unicode.replace(/\r\n?/g, "\n"); if (decoded === text) return { text: citationDisplayText(text), source: text, stable: true }; text = decoded; } @@ -110,17 +113,19 @@ function normalizeCitationText(input: string): { text: string; source: string; s /** Unwrap link destinations/definitions for anchored intent, retaining the normalized source for exclusion. */ function citationDisplayText(source: string): string { - const definitions = new Map(); - const text = source.replace(/^[ \t]{0,3}\[([^[\]\n]+)\]:[ \t]*(?:<([^>\n]+)>|(\S+))/gm, - (_raw: string, label: string, angle: string | undefined, destination: string | undefined) => { - definitions.set(label.trim().toLowerCase(), angle ?? destination!); - return angle ?? destination!; - }); - return text.replace(/!?\[([^[\]\n]*)\]\((?:<([^<>\n]+)>|([^()[\]\n]*))\)/g, - (_raw: string, _label: string, angle: string | undefined, destination: string | undefined) => angle ?? destination!.trim()) - .replace(/\[([^[\]\n]+)\](?:\[([^[\]\n]*)\])?/g, - (raw: string, label: string, key: string | undefined) => definitions.get((key || label).trim().toLowerCase()) ?? raw) - .replace(/\\\n/g, "").replace(/(?(); + text = source.replace(/^[ \t]{0,3}\[([^[\]\n]+)\]:[ \t]*(?:<([^>\n]+)>|(\S+))/gm, + (_raw: string, label: string, angle: string | undefined, destination: string | undefined) => { + definitions.set(label.trim().toLowerCase(), angle ?? destination!); + return angle ?? destination!; + }).replace(/!?\[([^[\]\n]*)\]\((?:<([^<>\n]+)>|([^()[\]\n]*))\)/g, + (_raw: string, _label: string, angle: string | undefined, destination: string | undefined) => angle ?? destination!.trim()) + .replace(/\[([^[\]\n]+)\](?:\[([^[\]\n]*)\])?/g, + (raw: string, label: string, key: string | undefined) => definitions.get((key || label).trim().toLowerCase()) ?? raw); + } + return text.includes("\n") ? text.replace(/\\\n/g, "").replace(/(? 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; + return text.slice(ownerStart, end).toLowerCase(); +} + /** Inspect normalized source without trusting Markdown interpretation or an assumed repository for bare hashes. */ function inspectCitationReferences(texts: readonly string[], repo: string, number: number): { mentioned: boolean; unsafe: boolean } { let mentioned = false; let unsafe = false; - for (const source of texts) { + for (const source of new Set(texts)) { // Check both retained source and joined soft lines; joining must not discard link labels or destinations. - for (const text of [source, source.replace(/(? Date: Fri, 23 Oct 2026 17:44:56 +0200 Subject: [PATCH 13/18] Trim issue-label URL tails without an overlapping regex CodeQL flagged the anchored issue-label URL pattern as a polynomial regular expression: the optional query group and the trailing [.)]* tail could both match the same characters. The allowed tail (>, then . or ), then spaces/tabs) is now trimmed by hand before an anchored pattern without overlapping quantifiers. A test pins the accepted and refused tails and a 50,000-character hostile input. --- .../features/pm-github-cited-issue-adoption.toon | 5 +++-- .../history/pm-github-cited-issue-adoption.jsonl | 1 + import-fidelity.ts | 8 +++++++- test/cited-issue-adoption.test.ts | 14 ++++++++++++++ 4 files changed, 25 insertions(+), 3 deletions(-) diff --git a/.agents/pm/features/pm-github-cited-issue-adoption.toon b/.agents/pm/features/pm-github-cited-issue-adoption.toon index edbb7bda..41fed46b 100644 --- a/.agents/pm/features/pm-github-cited-issue-adoption.toon +++ b/.agents/pm/features/pm-github-cited-issue-adoption.toon @@ -6,10 +6,10 @@ status: in_progress priority: 2 tags[3]: changelog-unreleased,github,import created_at: "2026-10-23T07:16:54.041Z" -updated_at: "2026-10-23T15:23:05.243Z" +updated_at: "2026-10-23T15:44:53.218Z" author: codex-agent acceptance_criteria: "Adoption is off by default across CLI, SDK and reusable sync, enabled only with --adopt-cited, adoptCited true or adopt-cited true respectively. Default import retains normal create/structured-match behavior and read-only citation_candidates without fallback ambiguity refusal. Document creating items with gh:owner/repo#N as primary duplicate prevention; Normalize HTML named/numeric entities and percent bytes repeatedly until stable with a bounded refusal, apply NFKC and remove invisible/bidi controls, unwrap Markdown links/reference definitions and join soft breaks before anchored ownership checks while retaining source for conservative exclusion; Only title qualified references, leading description closing-keyword references, or whole body Issue:/Tracks: URL lines can authorize opt-in adoption. Any other normalized GitHub URL, foreign repository, other number or unattributed reference refuses adoption and reports candidate hints. Preserve structured identity precedence, single-owner upsert, eligible-owner ambiguity refusal, receipts, idempotency and non-git support; Keep all review reproductions and prior shape inputs, add encoded-reference fuzzing and explicit pm-review Issue label positives/default creates, record behavioral function/workflow reverts and public dry-run receipts, run targeted checks and wait for exact-head npm/Bun CI release gates" -comments[42]{created_at,author,text}: +comments[43]{created_at,author,text}: "2026-10-23T07:17:26.861Z",codex-agent,"Overlap review: pm-github-c093 owns structured URL/provider back-link import matching, pm-github-identity-collisions diagnoses and repairs structured ownership, Projects import matches board and issue provenance, and export upserts linked issues. None adopts arbitrary exact prose citations. This feature adds a fallback only after existing import matching, preserving those paths. Scheduled gated sync already invokes import, so it inherits the default. Plan ambiguous citations before writes and refuse the batch with actionable candidate IDs." "2026-10-23T07:24:01.857Z",codex-agent,"Implementation: strong ownership matching stays first; exact qualified title/description/body citations fall back to a single existing item. All imports now prepare identities before writes, and ambiguous citations expose candidate IDs plus an exact tagging remedy. Adoption retains local tags and reports a separate count; provider-owned records retain link-only behavior. New tests use real trackers with a PATH containing node and the pinned pm CLI but no git, plus real git trackers for write-gate acceptance." "2026-10-23T07:27:45.693Z",codex-agent,"New citation suite: 13/13 passed, including plain/atomic adoption in non-git trackers, write-gate acceptance, retry identity, ambiguity refusal, punctuation, repo/number boundaries, structured ownership precedence and provider link-only preservation. Live pm-review main aa16d855df1f7b74afd3453617f77e92c44d493e already tags pm-review-withdrawn-lineage for issue 30, so an untouched current-main copy correctly plans update rather than adopt. Preserve this current-state result and locate the pre-tag historical tracker for a separate adoption replay. SDK reads may create runtime caches; verify original tracked file bytes independently." @@ -52,6 +52,7 @@ comments[42]{created_at,author,text}: "2026-10-23T15:06:40.793Z",codex-agent,"All four release gates passed at 51dea17, but native Bun on Node 26 failed the unchanged 200000-space ownership performance budget: 85.562 ms against 50 ms, while all44 adoption tests passed. The exclusion scanner repeats identical source/display/joined strings four times when normalization changes nothing. Deduplicate equal views using sets, preserving every distinct normalized/source/joined view and all ownership decisions. Keep the timing limit and gate thresholds unchanged, verify native Bun and a function-only performance revert, then require a fresh CI pass." "2026-10-23T15:21:29.341Z",codex-agent,"Performance fix preserves all normalized views but scans equal views once, finds issue/hash markers before reading the adjacent repository, and skips only identity Unicode/Markdown/soft-break transforms after decoding. Node source selection via c8 passes52/52 with no uncovered new-helper branches. Native local Bun1.3.5 regex/performance suite passes8/8, ownership200000-space probe22.892ms against unchanged50ms. Typecheck, builds, lint, docstrings and zero duplication across18 modules pass. Source-copy revert restores only normalizeCitationText, citationDisplayText and inspectCitationReferences from51dea17: bun test --test-name-pattern=\\\"ownership whitespace trigger\\\" test/import-regex.test.ts fails the selected test at112.322ms,1 failure,0 passes,7 filtered,exit1. Current anchored checks, tests, adjacent-repository helper and SDK paths remain. CI pinned Bun1.3.14 must provide its own fresh pass. No limits were relaxed." "2026-10-23T15:22:57.445Z",codex-agent,"Final optimized-code replay confirms unchanged public results in both modes: pre-tag pm-review creates1 by default with adoption_disabled and adopts1 with explicit opt-in, tagged pm-review updates1 in both, pm-context creates2/updates3 in both, zero skips, all393 original files unchanged. Revert command: bun test --test-name-pattern='ownership whitespace trigger' test/import-regex.test.ts. Reverted three performance functions fail1 selected test at112.322ms with7 filtered; fixed native suite passes8/8 at22.892ms for that probe. Node c8 source selection passes52/52, no uncovered new-helper branches. Typecheck, builds, lint, docstrings, zero duplication, strict tracker health and diff checks pass. Release the claim, retain in_progress, push the final optimization and require fresh CI npm/Bun/native gates at the unchanged limits." + "2026-10-23T15:44:53.218Z",claude-orchestrator,"CodeQL (js/polynomial-redos, import-fidelity.ts:185) flagged the issue-label URL tail regex: its optional query group and the [.)]* tail overlap. The tail (>, then . or ), then spaces/tabs) is now trimmed by hand and the remaining anchored pattern has no overlapping quantifiers. New test pins the tail grammar (9 accepted tails, 4 refused tails, 50k-')' hostile input). Adoption suite 45/45, typecheck and lint pass." files[13]{path,scope}: .github/workflows/pm-github-sync.yml,project import-fidelity.ts,project diff --git a/.agents/pm/history/pm-github-cited-issue-adoption.jsonl b/.agents/pm/history/pm-github-cited-issue-adoption.jsonl index 7d7af1f1..36c49bc8 100644 --- a/.agents/pm/history/pm-github-cited-issue-adoption.jsonl +++ b/.agents/pm/history/pm-github-cited-issue-adoption.jsonl @@ -83,3 +83,4 @@ {"hash_algorithm":"sha256","ts":"2026-10-23T15:21:29.341Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/40","value":{"created_at":"2026-10-23T15:21:29.341Z","author":"codex-agent","text":"Performance fix preserves all normalized views but scans equal views once, finds issue/hash markers before reading the adjacent repository, and skips only identity Unicode/Markdown/soft-break transforms after decoding. Node source selection via c8 passes52/52 with no uncovered new-helper branches. Native local Bun1.3.5 regex/performance suite passes8/8, ownership200000-space probe22.892ms against unchanged50ms. Typecheck, builds, lint, docstrings and zero duplication across18 modules pass. Source-copy revert restores only normalizeCitationText, citationDisplayText and inspectCitationReferences from51dea17: bun test --test-name-pattern=\\\"ownership whitespace trigger\\\" test/import-regex.test.ts fails the selected test at112.322ms,1 failure,0 passes,7 filtered,exit1. Current anchored checks, tests, adjacent-repository helper and SDK paths remain. CI pinned Bun1.3.14 must provide its own fresh pass. No limits were relaxed."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T15:21:29.341Z"}],"before_hash":"9c3ba7c4b1fd3f8fa84a747a865d78b019c23b8d0835b0f62f4b6fe3eeee3e9a","after_hash":"ab023bab71128894687d28ab62dede64aa1e0c5b56a54eee76da954600f6810a","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"77ae12303e1afde2b74226e9f7cff915b00a48815faf8b8cd161b64db06b1458"} {"hash_algorithm":"sha256","ts":"2026-10-23T15:22:57.445Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/41","value":{"created_at":"2026-10-23T15:22:57.445Z","author":"codex-agent","text":"Final optimized-code replay confirms unchanged public results in both modes: pre-tag pm-review creates1 by default with adoption_disabled and adopts1 with explicit opt-in, tagged pm-review updates1 in both, pm-context creates2/updates3 in both, zero skips, all393 original files unchanged. Revert command: bun test --test-name-pattern='ownership whitespace trigger' test/import-regex.test.ts. Reverted three performance functions fail1 selected test at112.322ms with7 filtered; fixed native suite passes8/8 at22.892ms for that probe. Node c8 source selection passes52/52, no uncovered new-helper branches. Typecheck, builds, lint, docstrings, zero duplication, strict tracker health and diff checks pass. Release the claim, retain in_progress, push the final optimization and require fresh CI npm/Bun/native gates at the unchanged limits."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T15:22:57.445Z"}],"before_hash":"ab023bab71128894687d28ab62dede64aa1e0c5b56a54eee76da954600f6810a","after_hash":"cc37244bd446038a19d91313d105223a37f542813e8955ff70bc397003244543","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"3704840961e8719a0aafecba6d1d9c7fbe8fc46bf1d00e9e0df4bd9ebc876727"} {"hash_algorithm":"sha256","ts":"2026-10-23T15:23:05.243Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":{"value":"implementer","source":"argv"},"topic":null},"op":"release","patch":[{"op":"remove","path":"/metadata/claim_principal"},{"op":"remove","path":"/metadata/assignee"},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T15:23:05.243Z"}],"before_hash":"cc37244bd446038a19d91313d105223a37f542813e8955ff70bc397003244543","after_hash":"676ce3af9a64253a45404e967fd0d67541ca712562f7d3372a086a282a6427de","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"1cf34d7632a92973a166960687cf79136cea2e1e63d754eaacaa1e1d161c5453"} +{"hash_algorithm":"sha256","ts":"2026-10-23T15:44:53.218Z","author":"claude-orchestrator","author_source":"asserted","agent_harness":"claude-code","agent_model":"claude-opus-5-5","agent_model_source":"probe","agent_instance":"73b8b11d9a2002b7bdeea5cd","agent_provenance":{"model":{"value":"claude-opus-5-5","source":"probe"},"effort":{"value":"xhigh","source":"environment"},"role":null,"topic":null,"version":{"value":"2.1.309","source":"probe"}},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/42","value":{"created_at":"2026-10-23T15:44:53.218Z","author":"claude-orchestrator","text":"CodeQL (js/polynomial-redos, import-fidelity.ts:185) flagged the issue-label URL tail regex: its optional query group and the [.)]* tail overlap. The tail (>, then . or ), then spaces/tabs) is now trimmed by hand and the remaining anchored pattern has no overlapping quantifiers. New test pins the tail grammar (9 accepted tails, 4 refused tails, 50k-')' hostile input). Adoption suite 45/45, typecheck and lint pass."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T15:44:53.218Z"}],"before_hash":"676ce3af9a64253a45404e967fd0d67541ca712562f7d3372a086a282a6427de","after_hash":"881101b2384b997897e48d7b4fa03dcc0bfbf1999b521b414474d991a79f108f","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":"ebf21121201a32c8af8ec536fb48ba0d18241cda62336d2fe3edc25110a6f964"} diff --git a/import-fidelity.ts b/import-fidelity.ts index 40282a61..bcd5c222 100644 --- a/import-fidelity.ts +++ b/import-fidelity.ts @@ -182,7 +182,13 @@ function hasAnchoredCitationIntent(text: string, field: number, repo: string, nu if (field === 1 && /^(?:fix|fixes|fixed|close|closes|closed|resolve|resolves|resolved)[ \t]+$/i.test(prefix)) return true; if (field === 2 && /^(?:Issue|Tracks):[ \t]*]*)?>?[.)]*[ \t]*$/i.test(line)) return true; + // Trim the allowed tail (`>`, then `.`/`)`, then spaces/tabs) by hand: a regex tail that can also match the + // query's last characters backtracks polynomially on a run of `)`. + let end = line.length; + 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; } } return false; diff --git a/test/cited-issue-adoption.test.ts b/test/cited-issue-adoption.test.ts index 12c55c85..688abaf0 100644 --- a/test/cited-issue-adoption.test.ts +++ b/test/cited-issue-adoption.test.ts @@ -620,3 +620,17 @@ test("normalized allowlist fuzz rejects random repository, number, decoration an } t.diagnostic("seed 0x53ce71a9: 4096 encoded decoration samples, 12288 field cases"); }); + +test("issue-label URL tails keep the anchored tail grammar after the regex tail was replaced by linear trimming", () => { + for (const tail of ["", "/", ">", ">.", ")", ").", "> \t", "?q=1).", "#issuecomment-1)>"]) { + const local = { id: "local", body: `Issue: ${issue.html_url}${tail}` }; + assert.equal(matchImportedIssue([local], issue).item, local, `tail ${JSON.stringify(tail)}`); + } + for (const tail of [" .", ">>", "x", ") x"]) { + assert.equal(matchImportedIssue([{ id: "local", body: `Issue: ${issue.html_url}${tail}` }], issue).item, undefined, `tail ${JSON.stringify(tail)}`); + } + const started = performance.now(); + const hostile = { id: "hostile", body: `Issue: ${issue.html_url}${")".repeat(50_000)}x` }; + assert.equal(matchImportedIssue([hostile], issue).item, undefined); + assert.ok(performance.now() - started < 1_000, "a long run of ')' must not backtrack polynomially"); +}); From 748389fc44beb337ed83e0205d347244af4a32ea Mon Sep 17 00:00:00 2001 From: SteveBot <1153461+unbraind@users.noreply.github.com> Date: Fri, 23 Oct 2026 17:53:44 +0200 Subject: [PATCH 14/18] Never adopt cited issues into items that already own another identity With --adopt-cited, an untagged item whose description said 'Fixes acme/widgets#30' while its github_url named issue 31 was adopted for #30, overwriting the identity of #31. Items with structured ownership (a GitHub issue URL in github_url, provenance or external links, a github_number, provider provenance, or a native 'GitHub issue ' declaration) are now reported as citation candidates with reason structured_owner and are never adopted from prose. --- .../pm-github-cited-issue-adoption.toon | 5 +++-- .../pm-github-cited-issue-adoption.jsonl | 1 + README.md | 2 +- import-fidelity.ts | 17 +++++++++++++++-- test/cited-issue-adoption.test.ts | 17 +++++++++++++++++ 5 files changed, 37 insertions(+), 5 deletions(-) diff --git a/.agents/pm/features/pm-github-cited-issue-adoption.toon b/.agents/pm/features/pm-github-cited-issue-adoption.toon index 41fed46b..17f0f14f 100644 --- a/.agents/pm/features/pm-github-cited-issue-adoption.toon +++ b/.agents/pm/features/pm-github-cited-issue-adoption.toon @@ -6,10 +6,10 @@ status: in_progress priority: 2 tags[3]: changelog-unreleased,github,import created_at: "2026-10-23T07:16:54.041Z" -updated_at: "2026-10-23T15:44:53.218Z" +updated_at: "2026-10-23T15:53:40.243Z" author: codex-agent acceptance_criteria: "Adoption is off by default across CLI, SDK and reusable sync, enabled only with --adopt-cited, adoptCited true or adopt-cited true respectively. Default import retains normal create/structured-match behavior and read-only citation_candidates without fallback ambiguity refusal. Document creating items with gh:owner/repo#N as primary duplicate prevention; Normalize HTML named/numeric entities and percent bytes repeatedly until stable with a bounded refusal, apply NFKC and remove invisible/bidi controls, unwrap Markdown links/reference definitions and join soft breaks before anchored ownership checks while retaining source for conservative exclusion; Only title qualified references, leading description closing-keyword references, or whole body Issue:/Tracks: URL lines can authorize opt-in adoption. Any other normalized GitHub URL, foreign repository, other number or unattributed reference refuses adoption and reports candidate hints. Preserve structured identity precedence, single-owner upsert, eligible-owner ambiguity refusal, receipts, idempotency and non-git support; Keep all review reproductions and prior shape inputs, add encoded-reference fuzzing and explicit pm-review Issue label positives/default creates, record behavioral function/workflow reverts and public dry-run receipts, run targeted checks and wait for exact-head npm/Bun CI release gates" -comments[43]{created_at,author,text}: +comments[44]{created_at,author,text}: "2026-10-23T07:17:26.861Z",codex-agent,"Overlap review: pm-github-c093 owns structured URL/provider back-link import matching, pm-github-identity-collisions diagnoses and repairs structured ownership, Projects import matches board and issue provenance, and export upserts linked issues. None adopts arbitrary exact prose citations. This feature adds a fallback only after existing import matching, preserving those paths. Scheduled gated sync already invokes import, so it inherits the default. Plan ambiguous citations before writes and refuse the batch with actionable candidate IDs." "2026-10-23T07:24:01.857Z",codex-agent,"Implementation: strong ownership matching stays first; exact qualified title/description/body citations fall back to a single existing item. All imports now prepare identities before writes, and ambiguous citations expose candidate IDs plus an exact tagging remedy. Adoption retains local tags and reports a separate count; provider-owned records retain link-only behavior. New tests use real trackers with a PATH containing node and the pinned pm CLI but no git, plus real git trackers for write-gate acceptance." "2026-10-23T07:27:45.693Z",codex-agent,"New citation suite: 13/13 passed, including plain/atomic adoption in non-git trackers, write-gate acceptance, retry identity, ambiguity refusal, punctuation, repo/number boundaries, structured ownership precedence and provider link-only preservation. Live pm-review main aa16d855df1f7b74afd3453617f77e92c44d493e already tags pm-review-withdrawn-lineage for issue 30, so an untouched current-main copy correctly plans update rather than adopt. Preserve this current-state result and locate the pre-tag historical tracker for a separate adoption replay. SDK reads may create runtime caches; verify original tracked file bytes independently." @@ -53,6 +53,7 @@ comments[43]{created_at,author,text}: "2026-10-23T15:21:29.341Z",codex-agent,"Performance fix preserves all normalized views but scans equal views once, finds issue/hash markers before reading the adjacent repository, and skips only identity Unicode/Markdown/soft-break transforms after decoding. Node source selection via c8 passes52/52 with no uncovered new-helper branches. Native local Bun1.3.5 regex/performance suite passes8/8, ownership200000-space probe22.892ms against unchanged50ms. Typecheck, builds, lint, docstrings and zero duplication across18 modules pass. Source-copy revert restores only normalizeCitationText, citationDisplayText and inspectCitationReferences from51dea17: bun test --test-name-pattern=\\\"ownership whitespace trigger\\\" test/import-regex.test.ts fails the selected test at112.322ms,1 failure,0 passes,7 filtered,exit1. Current anchored checks, tests, adjacent-repository helper and SDK paths remain. CI pinned Bun1.3.14 must provide its own fresh pass. No limits were relaxed." "2026-10-23T15:22:57.445Z",codex-agent,"Final optimized-code replay confirms unchanged public results in both modes: pre-tag pm-review creates1 by default with adoption_disabled and adopts1 with explicit opt-in, tagged pm-review updates1 in both, pm-context creates2/updates3 in both, zero skips, all393 original files unchanged. Revert command: bun test --test-name-pattern='ownership whitespace trigger' test/import-regex.test.ts. Reverted three performance functions fail1 selected test at112.322ms with7 filtered; fixed native suite passes8/8 at22.892ms for that probe. Node c8 source selection passes52/52, no uncovered new-helper branches. Typecheck, builds, lint, docstrings, zero duplication, strict tracker health and diff checks pass. Release the claim, retain in_progress, push the final optimization and require fresh CI npm/Bun/native gates at the unchanged limits." "2026-10-23T15:44:53.218Z",claude-orchestrator,"CodeQL (js/polynomial-redos, import-fidelity.ts:185) flagged the issue-label URL tail regex: its optional query group and the [.)]* tail overlap. The tail (>, then . or ), then spaces/tabs) is now trimmed by hand and the remaining anchored pattern has no overlapping quantifiers. New test pins the tail grammar (9 accepted tails, 4 refused tails, 50k-')' hostile input). Adoption suite 45/45, typecheck and lint pass." + "2026-10-23T15:53:40.243Z",claude-orchestrator,"Review round 6 (codex) BLOCK: with --adopt-cited, an untagged item whose description says 'Fixes acme/widgets#30' but whose github_url names issue 31 was adopted for #30, overwriting #31's identity. Fix: an item with structured ownership (GitHub issue URL in github_url/provenance/external links, github_number, provider provenance, or a native 'GitHub issue ' declaration) is never adopted from prose; it is reported as a citation candidate with reason structured_owner. Regression test fails on revert (0/1) and passes with the fix; adoption suite 46/46; typecheck, lint, docstring, build pass. README lists the new reason." files[13]{path,scope}: .github/workflows/pm-github-sync.yml,project import-fidelity.ts,project diff --git a/.agents/pm/history/pm-github-cited-issue-adoption.jsonl b/.agents/pm/history/pm-github-cited-issue-adoption.jsonl index 36c49bc8..447bbb99 100644 --- a/.agents/pm/history/pm-github-cited-issue-adoption.jsonl +++ b/.agents/pm/history/pm-github-cited-issue-adoption.jsonl @@ -84,3 +84,4 @@ {"hash_algorithm":"sha256","ts":"2026-10-23T15:22:57.445Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/41","value":{"created_at":"2026-10-23T15:22:57.445Z","author":"codex-agent","text":"Final optimized-code replay confirms unchanged public results in both modes: pre-tag pm-review creates1 by default with adoption_disabled and adopts1 with explicit opt-in, tagged pm-review updates1 in both, pm-context creates2/updates3 in both, zero skips, all393 original files unchanged. Revert command: bun test --test-name-pattern='ownership whitespace trigger' test/import-regex.test.ts. Reverted three performance functions fail1 selected test at112.322ms with7 filtered; fixed native suite passes8/8 at22.892ms for that probe. Node c8 source selection passes52/52, no uncovered new-helper branches. Typecheck, builds, lint, docstrings, zero duplication, strict tracker health and diff checks pass. Release the claim, retain in_progress, push the final optimization and require fresh CI npm/Bun/native gates at the unchanged limits."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T15:22:57.445Z"}],"before_hash":"ab023bab71128894687d28ab62dede64aa1e0c5b56a54eee76da954600f6810a","after_hash":"cc37244bd446038a19d91313d105223a37f542813e8955ff70bc397003244543","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"3704840961e8719a0aafecba6d1d9c7fbe8fc46bf1d00e9e0df4bd9ebc876727"} {"hash_algorithm":"sha256","ts":"2026-10-23T15:23:05.243Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":{"value":"implementer","source":"argv"},"topic":null},"op":"release","patch":[{"op":"remove","path":"/metadata/claim_principal"},{"op":"remove","path":"/metadata/assignee"},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T15:23:05.243Z"}],"before_hash":"cc37244bd446038a19d91313d105223a37f542813e8955ff70bc397003244543","after_hash":"676ce3af9a64253a45404e967fd0d67541ca712562f7d3372a086a282a6427de","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"1cf34d7632a92973a166960687cf79136cea2e1e63d754eaacaa1e1d161c5453"} {"hash_algorithm":"sha256","ts":"2026-10-23T15:44:53.218Z","author":"claude-orchestrator","author_source":"asserted","agent_harness":"claude-code","agent_model":"claude-opus-5-5","agent_model_source":"probe","agent_instance":"73b8b11d9a2002b7bdeea5cd","agent_provenance":{"model":{"value":"claude-opus-5-5","source":"probe"},"effort":{"value":"xhigh","source":"environment"},"role":null,"topic":null,"version":{"value":"2.1.309","source":"probe"}},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/42","value":{"created_at":"2026-10-23T15:44:53.218Z","author":"claude-orchestrator","text":"CodeQL (js/polynomial-redos, import-fidelity.ts:185) flagged the issue-label URL tail regex: its optional query group and the [.)]* tail overlap. The tail (>, then . or ), then spaces/tabs) is now trimmed by hand and the remaining anchored pattern has no overlapping quantifiers. New test pins the tail grammar (9 accepted tails, 4 refused tails, 50k-')' hostile input). Adoption suite 45/45, typecheck and lint pass."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T15:44:53.218Z"}],"before_hash":"676ce3af9a64253a45404e967fd0d67541ca712562f7d3372a086a282a6427de","after_hash":"881101b2384b997897e48d7b4fa03dcc0bfbf1999b521b414474d991a79f108f","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":"ebf21121201a32c8af8ec536fb48ba0d18241cda62336d2fe3edc25110a6f964"} +{"hash_algorithm":"sha256","ts":"2026-10-23T15:53:40.243Z","author":"claude-orchestrator","author_source":"asserted","agent_harness":"claude-code","agent_model":"claude-opus-5-5","agent_model_source":"probe","agent_instance":"73b8b11d9a2002b7bdeea5cd","agent_provenance":{"model":{"value":"claude-opus-5-5","source":"probe"},"effort":{"value":"xhigh","source":"environment"},"role":null,"topic":null,"version":{"value":"2.1.309","source":"probe"}},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/43","value":{"created_at":"2026-10-23T15:53:40.243Z","author":"claude-orchestrator","text":"Review round 6 (codex) BLOCK: with --adopt-cited, an untagged item whose description says 'Fixes acme/widgets#30' but whose github_url names issue 31 was adopted for #30, overwriting #31's identity. Fix: an item with structured ownership (GitHub issue URL in github_url/provenance/external links, github_number, provider provenance, or a native 'GitHub issue ' declaration) is never adopted from prose; it is reported as a citation candidate with reason structured_owner. Regression test fails on revert (0/1) and passes with the fix; adoption suite 46/46; typecheck, lint, docstring, build pass. README lists the new reason."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T15:53:40.243Z"}],"before_hash":"881101b2384b997897e48d7b4fa03dcc0bfbf1999b521b414474d991a79f108f","after_hash":"b080666adbafa52ea80f79fc387c8f79001213323cf7193c108560aa8b4cf10d","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":"4216721d022288bc641d79fafc2492db0ca85d52253800d3d884ef81fa8034d8"} diff --git a/README.md b/README.md index f7f9abcf..a07d3910 100644 --- a/README.md +++ b/README.md @@ -420,7 +420,7 @@ fallback deliberately favors refusal over overwriting an unrelated item: Plans include `citation_candidates: [{id, reason}]`; aggregate dry-run and write receipts include `citation_candidates: [{issue, id, reason}]`. Reasons are -`adoption_disabled`, `missing_intent`, `multiple_issues` or `normalization_limit`. +`adoption_disabled`, `missing_intent`, `multiple_issues`, `normalization_limit` or `structured_owner` (the item already carries a GitHub issue URL field, issue number, provider provenance or native declaration, so prose citing another issue never re-owns it). Candidate reporting never changes the referenced item's status, content or fields, including a closed item's completion state. diff --git a/import-fidelity.ts b/import-fidelity.ts index bcd5c222..940412ae 100644 --- a/import-fidelity.ts +++ b/import-fidelity.ts @@ -48,7 +48,7 @@ export interface ImportMatch { item?: PmItem; linkOnly: boolean; via: string; ci /** Incidental references are review hints, never permission to transfer an item's ownership. */ export interface CitationCandidate { id: string; - reason: "multiple_issues" | "missing_intent" | "adoption_disabled" | "normalization_limit"; + reason: "multiple_issues" | "missing_intent" | "adoption_disabled" | "normalization_limit" | "structured_owner"; } /** A citation cannot pick an owner until the user tags exactly one candidate. */ @@ -240,6 +240,15 @@ function providerProvenance(item: PmItem): string { .map(([, value]) => typeof value === "string" ? value : JSON.stringify(value)).join("\n"); } +/** True when an item already carries a structured GitHub or provider identity of its own. */ +function hasStructuredOwnership(item: PmItem): boolean { + const issueLocator = /^https:\/\/github\.com\/[^/]+\/[^/]+\/issues\/\d+$/; + return item.github_number !== undefined || Boolean(providerProvenance(item)) || + (typeof item.description === "string" && /^GitHub issue(?:\s*:\s+|\s+)https?:\/\//i.test(item.description)) || + Object.entries(item).some(([key, value]) => /^(?:github_?url|provenance|external_?(?:links?|url)|links?)$/i.test(key) && + locators(value).some(url => issueLocator.test(url))); +} + /** 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 { const issueUrl = locators(issue.html_url)[0]!; @@ -261,7 +270,11 @@ export function matchImportedIssue(items: readonly PmItem[], issue: GhIssue, tag for (const candidate of items) { if (!candidate.id || candidate.tags?.some(tag => parseProvenanceTag(tag) !== undefined)) continue; const assessment = citesIssue(candidate, repo, issue.number); - if (assessment === "eligible" && adoptCited) citing.push(candidate); + if (assessment === undefined) continue; + // Structured ownership (a GitHub issue URL field, an issue number, provider provenance or a native declaration) + // already names an identity; prose citing another issue must never overwrite it. + if (assessment === "eligible" && hasStructuredOwnership(candidate)) incidental.push({ id: candidate.id, reason: "structured_owner" }); + else if (assessment === "eligible" && adoptCited) citing.push(candidate); else if (assessment !== undefined) incidental.push({ id: candidate.id, reason: assessment === "eligible" ? "adoption_disabled" : assessment }); } incidental.sort((left, right) => left.id.localeCompare(right.id)); diff --git a/test/cited-issue-adoption.test.ts b/test/cited-issue-adoption.test.ts index 688abaf0..cd9fa516 100644 --- a/test/cited-issue-adoption.test.ts +++ b/test/cited-issue-adoption.test.ts @@ -634,3 +634,20 @@ test("issue-label URL tails keep the anchored tail grammar after the regex tail assert.equal(matchImportedIssue([hostile], issue).item, undefined); assert.ok(performance.now() - started < 1_000, "a long run of ')' must not backtrack polynomially"); }); + +test("citation adoption never overwrites an item that already carries structured ownership", () => { + 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"] }, + ]; + for (const item of owned) { + const result = matchImportedIssue([item], issue); + assert.equal(result.item, undefined, item.id); + assert.deepEqual(result.citation_candidates, [{ id: item.id, reason: "structured_owner" }], item.id); + } + 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"] }; + assert.equal(matchImportedIssue([docs], issue).item, docs); +}); From 06365ae13fe3beff803ea31449b9f73fd9afeef9 Mon Sep 17 00:00:00 2001 From: SteveBot <1153461+unbraind@users.noreply.github.com> Date: Fri, 23 Oct 2026 17:58:50 +0200 Subject: [PATCH 15/18] Count any core external_ref as structured ownership for citation adoption An untagged item whose external_ref was gh:acme/widgets#31 could still be adopted for #30 from a 'Fixes acme/widgets#30' description. Any non-empty external_ref (gh:, linear:, jira:, ado: or other providers) now marks the item as already owned, so prose never re-owns it. --- .agents/pm/features/pm-github-cited-issue-adoption.toon | 5 +++-- .agents/pm/history/pm-github-cited-issue-adoption.jsonl | 1 + README.md | 2 +- import-fidelity.ts | 3 ++- test/cited-issue-adoption.test.ts | 2 ++ 5 files changed, 9 insertions(+), 4 deletions(-) diff --git a/.agents/pm/features/pm-github-cited-issue-adoption.toon b/.agents/pm/features/pm-github-cited-issue-adoption.toon index 17f0f14f..e4d7bbbb 100644 --- a/.agents/pm/features/pm-github-cited-issue-adoption.toon +++ b/.agents/pm/features/pm-github-cited-issue-adoption.toon @@ -6,10 +6,10 @@ status: in_progress priority: 2 tags[3]: changelog-unreleased,github,import created_at: "2026-10-23T07:16:54.041Z" -updated_at: "2026-10-23T15:53:40.243Z" +updated_at: "2026-10-23T15:58:46.328Z" author: codex-agent acceptance_criteria: "Adoption is off by default across CLI, SDK and reusable sync, enabled only with --adopt-cited, adoptCited true or adopt-cited true respectively. Default import retains normal create/structured-match behavior and read-only citation_candidates without fallback ambiguity refusal. Document creating items with gh:owner/repo#N as primary duplicate prevention; Normalize HTML named/numeric entities and percent bytes repeatedly until stable with a bounded refusal, apply NFKC and remove invisible/bidi controls, unwrap Markdown links/reference definitions and join soft breaks before anchored ownership checks while retaining source for conservative exclusion; Only title qualified references, leading description closing-keyword references, or whole body Issue:/Tracks: URL lines can authorize opt-in adoption. Any other normalized GitHub URL, foreign repository, other number or unattributed reference refuses adoption and reports candidate hints. Preserve structured identity precedence, single-owner upsert, eligible-owner ambiguity refusal, receipts, idempotency and non-git support; Keep all review reproductions and prior shape inputs, add encoded-reference fuzzing and explicit pm-review Issue label positives/default creates, record behavioral function/workflow reverts and public dry-run receipts, run targeted checks and wait for exact-head npm/Bun CI release gates" -comments[44]{created_at,author,text}: +comments[45]{created_at,author,text}: "2026-10-23T07:17:26.861Z",codex-agent,"Overlap review: pm-github-c093 owns structured URL/provider back-link import matching, pm-github-identity-collisions diagnoses and repairs structured ownership, Projects import matches board and issue provenance, and export upserts linked issues. None adopts arbitrary exact prose citations. This feature adds a fallback only after existing import matching, preserving those paths. Scheduled gated sync already invokes import, so it inherits the default. Plan ambiguous citations before writes and refuse the batch with actionable candidate IDs." "2026-10-23T07:24:01.857Z",codex-agent,"Implementation: strong ownership matching stays first; exact qualified title/description/body citations fall back to a single existing item. All imports now prepare identities before writes, and ambiguous citations expose candidate IDs plus an exact tagging remedy. Adoption retains local tags and reports a separate count; provider-owned records retain link-only behavior. New tests use real trackers with a PATH containing node and the pinned pm CLI but no git, plus real git trackers for write-gate acceptance." "2026-10-23T07:27:45.693Z",codex-agent,"New citation suite: 13/13 passed, including plain/atomic adoption in non-git trackers, write-gate acceptance, retry identity, ambiguity refusal, punctuation, repo/number boundaries, structured ownership precedence and provider link-only preservation. Live pm-review main aa16d855df1f7b74afd3453617f77e92c44d493e already tags pm-review-withdrawn-lineage for issue 30, so an untouched current-main copy correctly plans update rather than adopt. Preserve this current-state result and locate the pre-tag historical tracker for a separate adoption replay. SDK reads may create runtime caches; verify original tracked file bytes independently." @@ -54,6 +54,7 @@ comments[44]{created_at,author,text}: "2026-10-23T15:22:57.445Z",codex-agent,"Final optimized-code replay confirms unchanged public results in both modes: pre-tag pm-review creates1 by default with adoption_disabled and adopts1 with explicit opt-in, tagged pm-review updates1 in both, pm-context creates2/updates3 in both, zero skips, all393 original files unchanged. Revert command: bun test --test-name-pattern='ownership whitespace trigger' test/import-regex.test.ts. Reverted three performance functions fail1 selected test at112.322ms with7 filtered; fixed native suite passes8/8 at22.892ms for that probe. Node c8 source selection passes52/52, no uncovered new-helper branches. Typecheck, builds, lint, docstrings, zero duplication, strict tracker health and diff checks pass. Release the claim, retain in_progress, push the final optimization and require fresh CI npm/Bun/native gates at the unchanged limits." "2026-10-23T15:44:53.218Z",claude-orchestrator,"CodeQL (js/polynomial-redos, import-fidelity.ts:185) flagged the issue-label URL tail regex: its optional query group and the [.)]* tail overlap. The tail (>, then . or ), then spaces/tabs) is now trimmed by hand and the remaining anchored pattern has no overlapping quantifiers. New test pins the tail grammar (9 accepted tails, 4 refused tails, 50k-')' hostile input). Adoption suite 45/45, typecheck and lint pass." "2026-10-23T15:53:40.243Z",claude-orchestrator,"Review round 6 (codex) BLOCK: with --adopt-cited, an untagged item whose description says 'Fixes acme/widgets#30' but whose github_url names issue 31 was adopted for #30, overwriting #31's identity. Fix: an item with structured ownership (GitHub issue URL in github_url/provenance/external links, github_number, provider provenance, or a native 'GitHub issue ' declaration) is never adopted from prose; it is reported as a citation candidate with reason structured_owner. Regression test fails on revert (0/1) and passes with the fix; adoption suite 46/46; typecheck, lint, docstring, build pass. README lists the new reason." + "2026-10-23T15:58:46.327Z",claude-orchestrator,"Delta review (codex) BLOCK: hasStructuredOwnership missed the core external_ref field, so an untagged item with external_ref gh:acme/widgets#31 citing #30 could still be adopted. Any non-empty external_ref (gh:, linear:, jira:, ado: or other) now counts as structured ownership. Tests for gh: and linear: external_ref; the structured-ownership test fails on revert and passes with the fix; suite 46/46; typecheck and lint pass." files[13]{path,scope}: .github/workflows/pm-github-sync.yml,project import-fidelity.ts,project diff --git a/.agents/pm/history/pm-github-cited-issue-adoption.jsonl b/.agents/pm/history/pm-github-cited-issue-adoption.jsonl index 447bbb99..1f21a4b0 100644 --- a/.agents/pm/history/pm-github-cited-issue-adoption.jsonl +++ b/.agents/pm/history/pm-github-cited-issue-adoption.jsonl @@ -85,3 +85,4 @@ {"hash_algorithm":"sha256","ts":"2026-10-23T15:23:05.243Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"8866c4db82badc284b2d3dfd","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":{"value":"implementer","source":"argv"},"topic":null},"op":"release","patch":[{"op":"remove","path":"/metadata/claim_principal"},{"op":"remove","path":"/metadata/assignee"},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T15:23:05.243Z"}],"before_hash":"cc37244bd446038a19d91313d105223a37f542813e8955ff70bc397003244543","after_hash":"676ce3af9a64253a45404e967fd0d67541ca712562f7d3372a086a282a6427de","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"1cf34d7632a92973a166960687cf79136cea2e1e63d754eaacaa1e1d161c5453"} {"hash_algorithm":"sha256","ts":"2026-10-23T15:44:53.218Z","author":"claude-orchestrator","author_source":"asserted","agent_harness":"claude-code","agent_model":"claude-opus-5-5","agent_model_source":"probe","agent_instance":"73b8b11d9a2002b7bdeea5cd","agent_provenance":{"model":{"value":"claude-opus-5-5","source":"probe"},"effort":{"value":"xhigh","source":"environment"},"role":null,"topic":null,"version":{"value":"2.1.309","source":"probe"}},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/42","value":{"created_at":"2026-10-23T15:44:53.218Z","author":"claude-orchestrator","text":"CodeQL (js/polynomial-redos, import-fidelity.ts:185) flagged the issue-label URL tail regex: its optional query group and the [.)]* tail overlap. The tail (>, then . or ), then spaces/tabs) is now trimmed by hand and the remaining anchored pattern has no overlapping quantifiers. New test pins the tail grammar (9 accepted tails, 4 refused tails, 50k-')' hostile input). Adoption suite 45/45, typecheck and lint pass."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T15:44:53.218Z"}],"before_hash":"676ce3af9a64253a45404e967fd0d67541ca712562f7d3372a086a282a6427de","after_hash":"881101b2384b997897e48d7b4fa03dcc0bfbf1999b521b414474d991a79f108f","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":"ebf21121201a32c8af8ec536fb48ba0d18241cda62336d2fe3edc25110a6f964"} {"hash_algorithm":"sha256","ts":"2026-10-23T15:53:40.243Z","author":"claude-orchestrator","author_source":"asserted","agent_harness":"claude-code","agent_model":"claude-opus-5-5","agent_model_source":"probe","agent_instance":"73b8b11d9a2002b7bdeea5cd","agent_provenance":{"model":{"value":"claude-opus-5-5","source":"probe"},"effort":{"value":"xhigh","source":"environment"},"role":null,"topic":null,"version":{"value":"2.1.309","source":"probe"}},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/43","value":{"created_at":"2026-10-23T15:53:40.243Z","author":"claude-orchestrator","text":"Review round 6 (codex) BLOCK: with --adopt-cited, an untagged item whose description says 'Fixes acme/widgets#30' but whose github_url names issue 31 was adopted for #30, overwriting #31's identity. Fix: an item with structured ownership (GitHub issue URL in github_url/provenance/external links, github_number, provider provenance, or a native 'GitHub issue ' declaration) is never adopted from prose; it is reported as a citation candidate with reason structured_owner. Regression test fails on revert (0/1) and passes with the fix; adoption suite 46/46; typecheck, lint, docstring, build pass. README lists the new reason."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T15:53:40.243Z"}],"before_hash":"881101b2384b997897e48d7b4fa03dcc0bfbf1999b521b414474d991a79f108f","after_hash":"b080666adbafa52ea80f79fc387c8f79001213323cf7193c108560aa8b4cf10d","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":"4216721d022288bc641d79fafc2492db0ca85d52253800d3d884ef81fa8034d8"} +{"hash_algorithm":"sha256","ts":"2026-10-23T15:58:46.328Z","author":"claude-orchestrator","author_source":"asserted","agent_harness":"claude-code","agent_model":"claude-opus-5-5","agent_model_source":"probe","agent_instance":"73b8b11d9a2002b7bdeea5cd","agent_provenance":{"model":{"value":"claude-opus-5-5","source":"probe"},"effort":{"value":"xhigh","source":"environment"},"role":null,"topic":null,"version":{"value":"2.1.309","source":"probe"}},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/44","value":{"created_at":"2026-10-23T15:58:46.327Z","author":"claude-orchestrator","text":"Delta review (codex) BLOCK: hasStructuredOwnership missed the core external_ref field, so an untagged item with external_ref gh:acme/widgets#31 citing #30 could still be adopted. Any non-empty external_ref (gh:, linear:, jira:, ado: or other) now counts as structured ownership. Tests for gh: and linear: external_ref; the structured-ownership test fails on revert and passes with the fix; suite 46/46; typecheck and lint pass."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T15:58:46.328Z"}],"before_hash":"b080666adbafa52ea80f79fc387c8f79001213323cf7193c108560aa8b4cf10d","after_hash":"b51d2d54adef0a6a29ecd734bec1611bfea19d3c89bfb0b2c02e435b6accc119","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":"c6f7451af4f7de49f081f9fef956279d708aa8ae3f38b6b38c75e080eae4c85d"} diff --git a/README.md b/README.md index a07d3910..aa3e0084 100644 --- a/README.md +++ b/README.md @@ -420,7 +420,7 @@ fallback deliberately favors refusal over overwriting an unrelated item: Plans include `citation_candidates: [{id, reason}]`; aggregate dry-run and write receipts include `citation_candidates: [{issue, id, reason}]`. Reasons are -`adoption_disabled`, `missing_intent`, `multiple_issues`, `normalization_limit` or `structured_owner` (the item already carries a GitHub issue URL field, issue number, provider provenance or native declaration, so prose citing another issue never re-owns it). +`adoption_disabled`, `missing_intent`, `multiple_issues`, `normalization_limit` or `structured_owner` (the item already carries a core `external_ref`, a GitHub issue URL field, issue number, provider provenance or native declaration, so prose citing another issue never re-owns it). Candidate reporting never changes the referenced item's status, content or fields, including a closed item's completion state. diff --git a/import-fidelity.ts b/import-fidelity.ts index 940412ae..dae543d2 100644 --- a/import-fidelity.ts +++ b/import-fidelity.ts @@ -240,10 +240,11 @@ function providerProvenance(item: PmItem): string { .map(([, value]) => typeof value === "string" ? value : JSON.stringify(value)).join("\n"); } -/** True when an item already carries a structured GitHub or provider identity of its own. */ +/** True when an item already carries a structured GitHub or provider identity of its own (any core external_ref counts). */ function hasStructuredOwnership(item: PmItem): boolean { const issueLocator = /^https:\/\/github\.com\/[^/]+\/[^/]+\/issues\/\d+$/; return item.github_number !== undefined || Boolean(providerProvenance(item)) || + (typeof item.external_ref === "string" && item.external_ref.trim() !== "") || (typeof item.description === "string" && /^GitHub issue(?:\s*:\s+|\s+)https?:\/\//i.test(item.description)) || Object.entries(item).some(([key, value]) => /^(?:github_?url|provenance|external_?(?:links?|url)|links?)$/i.test(key) && locators(value).some(url => issueLocator.test(url))); diff --git a/test/cited-issue-adoption.test.ts b/test/cited-issue-adoption.test.ts index cd9fa516..3e1a128f 100644 --- a/test/cited-issue-adoption.test.ts +++ b/test/cited-issue-adoption.test.ts @@ -640,6 +640,8 @@ test("citation adoption never overwrites an item that already carries structured { 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" }, ]; for (const item of owned) { const result = matchImportedIssue([item], issue); From 72df4f9790907656c10993a152ab7bf4fd2b374a Mon Sep 17 00:00:00 2001 From: SteveBot <1153461+unbraind@users.noreply.github.com> Date: Fri, 23 Oct 2026 18:07:40 +0200 Subject: [PATCH 16/18] Mirror every identity.ts ownership source before citation adoption source_owner (and provider identity fields) were still not treated as ownership, so an item natively owned by Linear could be adopted and reopened from a 'Fixes acme/widgets#30' description. The adoption guard now recognizes every source identity.ts uses: github_* fields, github_link_only false, external_ref, source_owner, linear_/jira_/ado_ identity fields, provider provenance, native declarations and GitHub issue URLs in links. --- .../features/pm-github-cited-issue-adoption.toon | 5 +++-- .../history/pm-github-cited-issue-adoption.jsonl | 1 + README.md | 2 +- import-fidelity.ts | 15 +++++++++++---- test/cited-issue-adoption.test.ts | 4 ++++ 5 files changed, 20 insertions(+), 7 deletions(-) diff --git a/.agents/pm/features/pm-github-cited-issue-adoption.toon b/.agents/pm/features/pm-github-cited-issue-adoption.toon index e4d7bbbb..3770b3e5 100644 --- a/.agents/pm/features/pm-github-cited-issue-adoption.toon +++ b/.agents/pm/features/pm-github-cited-issue-adoption.toon @@ -6,10 +6,10 @@ status: in_progress priority: 2 tags[3]: changelog-unreleased,github,import created_at: "2026-10-23T07:16:54.041Z" -updated_at: "2026-10-23T15:58:46.328Z" +updated_at: "2026-10-23T16:07:35.434Z" author: codex-agent acceptance_criteria: "Adoption is off by default across CLI, SDK and reusable sync, enabled only with --adopt-cited, adoptCited true or adopt-cited true respectively. Default import retains normal create/structured-match behavior and read-only citation_candidates without fallback ambiguity refusal. Document creating items with gh:owner/repo#N as primary duplicate prevention; Normalize HTML named/numeric entities and percent bytes repeatedly until stable with a bounded refusal, apply NFKC and remove invisible/bidi controls, unwrap Markdown links/reference definitions and join soft breaks before anchored ownership checks while retaining source for conservative exclusion; Only title qualified references, leading description closing-keyword references, or whole body Issue:/Tracks: URL lines can authorize opt-in adoption. Any other normalized GitHub URL, foreign repository, other number or unattributed reference refuses adoption and reports candidate hints. Preserve structured identity precedence, single-owner upsert, eligible-owner ambiguity refusal, receipts, idempotency and non-git support; Keep all review reproductions and prior shape inputs, add encoded-reference fuzzing and explicit pm-review Issue label positives/default creates, record behavioral function/workflow reverts and public dry-run receipts, run targeted checks and wait for exact-head npm/Bun CI release gates" -comments[45]{created_at,author,text}: +comments[46]{created_at,author,text}: "2026-10-23T07:17:26.861Z",codex-agent,"Overlap review: pm-github-c093 owns structured URL/provider back-link import matching, pm-github-identity-collisions diagnoses and repairs structured ownership, Projects import matches board and issue provenance, and export upserts linked issues. None adopts arbitrary exact prose citations. This feature adds a fallback only after existing import matching, preserving those paths. Scheduled gated sync already invokes import, so it inherits the default. Plan ambiguous citations before writes and refuse the batch with actionable candidate IDs." "2026-10-23T07:24:01.857Z",codex-agent,"Implementation: strong ownership matching stays first; exact qualified title/description/body citations fall back to a single existing item. All imports now prepare identities before writes, and ambiguous citations expose candidate IDs plus an exact tagging remedy. Adoption retains local tags and reports a separate count; provider-owned records retain link-only behavior. New tests use real trackers with a PATH containing node and the pinned pm CLI but no git, plus real git trackers for write-gate acceptance." "2026-10-23T07:27:45.693Z",codex-agent,"New citation suite: 13/13 passed, including plain/atomic adoption in non-git trackers, write-gate acceptance, retry identity, ambiguity refusal, punctuation, repo/number boundaries, structured ownership precedence and provider link-only preservation. Live pm-review main aa16d855df1f7b74afd3453617f77e92c44d493e already tags pm-review-withdrawn-lineage for issue 30, so an untouched current-main copy correctly plans update rather than adopt. Preserve this current-state result and locate the pre-tag historical tracker for a separate adoption replay. SDK reads may create runtime caches; verify original tracked file bytes independently." @@ -55,6 +55,7 @@ comments[45]{created_at,author,text}: "2026-10-23T15:44:53.218Z",claude-orchestrator,"CodeQL (js/polynomial-redos, import-fidelity.ts:185) flagged the issue-label URL tail regex: its optional query group and the [.)]* tail overlap. The tail (>, then . or ), then spaces/tabs) is now trimmed by hand and the remaining anchored pattern has no overlapping quantifiers. New test pins the tail grammar (9 accepted tails, 4 refused tails, 50k-')' hostile input). Adoption suite 45/45, typecheck and lint pass." "2026-10-23T15:53:40.243Z",claude-orchestrator,"Review round 6 (codex) BLOCK: with --adopt-cited, an untagged item whose description says 'Fixes acme/widgets#30' but whose github_url names issue 31 was adopted for #30, overwriting #31's identity. Fix: an item with structured ownership (GitHub issue URL in github_url/provenance/external links, github_number, provider provenance, or a native 'GitHub issue ' declaration) is never adopted from prose; it is reported as a citation candidate with reason structured_owner. Regression test fails on revert (0/1) and passes with the fix; adoption suite 46/46; typecheck, lint, docstring, build pass. README lists the new reason." "2026-10-23T15:58:46.327Z",claude-orchestrator,"Delta review (codex) BLOCK: hasStructuredOwnership missed the core external_ref field, so an untagged item with external_ref gh:acme/widgets#31 citing #30 could still be adopted. Any non-empty external_ref (gh:, linear:, jira:, ado: or other) now counts as structured ownership. Tests for gh: and linear: external_ref; the structured-ownership test fails on revert and passes with the fix; suite 46/46; typecheck and lint pass." + "2026-10-23T16:07:35.434Z",claude-orchestrator,"Enumerating review (codex) BLOCK: source_owner missed. hasStructuredOwnership now mirrors every ownership source identity.ts recognizes: any filled github_* field (except github_link_only true), github_link_only false, core external_ref and source_owner, linear_/jira_/ado_ id/key/url/identifier fields, provider provenance, native 'GitHub issue ' declaration, GitHub issue URLs in provenance/external links. Tests cover source_owner, jira_issue_key, github_state and github_link_only false; suite 46/46; typecheck, lint, docstring pass." files[13]{path,scope}: .github/workflows/pm-github-sync.yml,project import-fidelity.ts,project diff --git a/.agents/pm/history/pm-github-cited-issue-adoption.jsonl b/.agents/pm/history/pm-github-cited-issue-adoption.jsonl index 1f21a4b0..dd2d2bdf 100644 --- a/.agents/pm/history/pm-github-cited-issue-adoption.jsonl +++ b/.agents/pm/history/pm-github-cited-issue-adoption.jsonl @@ -86,3 +86,4 @@ {"hash_algorithm":"sha256","ts":"2026-10-23T15:44:53.218Z","author":"claude-orchestrator","author_source":"asserted","agent_harness":"claude-code","agent_model":"claude-opus-5-5","agent_model_source":"probe","agent_instance":"73b8b11d9a2002b7bdeea5cd","agent_provenance":{"model":{"value":"claude-opus-5-5","source":"probe"},"effort":{"value":"xhigh","source":"environment"},"role":null,"topic":null,"version":{"value":"2.1.309","source":"probe"}},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/42","value":{"created_at":"2026-10-23T15:44:53.218Z","author":"claude-orchestrator","text":"CodeQL (js/polynomial-redos, import-fidelity.ts:185) flagged the issue-label URL tail regex: its optional query group and the [.)]* tail overlap. The tail (>, then . or ), then spaces/tabs) is now trimmed by hand and the remaining anchored pattern has no overlapping quantifiers. New test pins the tail grammar (9 accepted tails, 4 refused tails, 50k-')' hostile input). Adoption suite 45/45, typecheck and lint pass."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T15:44:53.218Z"}],"before_hash":"676ce3af9a64253a45404e967fd0d67541ca712562f7d3372a086a282a6427de","after_hash":"881101b2384b997897e48d7b4fa03dcc0bfbf1999b521b414474d991a79f108f","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":"ebf21121201a32c8af8ec536fb48ba0d18241cda62336d2fe3edc25110a6f964"} {"hash_algorithm":"sha256","ts":"2026-10-23T15:53:40.243Z","author":"claude-orchestrator","author_source":"asserted","agent_harness":"claude-code","agent_model":"claude-opus-5-5","agent_model_source":"probe","agent_instance":"73b8b11d9a2002b7bdeea5cd","agent_provenance":{"model":{"value":"claude-opus-5-5","source":"probe"},"effort":{"value":"xhigh","source":"environment"},"role":null,"topic":null,"version":{"value":"2.1.309","source":"probe"}},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/43","value":{"created_at":"2026-10-23T15:53:40.243Z","author":"claude-orchestrator","text":"Review round 6 (codex) BLOCK: with --adopt-cited, an untagged item whose description says 'Fixes acme/widgets#30' but whose github_url names issue 31 was adopted for #30, overwriting #31's identity. Fix: an item with structured ownership (GitHub issue URL in github_url/provenance/external links, github_number, provider provenance, or a native 'GitHub issue ' declaration) is never adopted from prose; it is reported as a citation candidate with reason structured_owner. Regression test fails on revert (0/1) and passes with the fix; adoption suite 46/46; typecheck, lint, docstring, build pass. README lists the new reason."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T15:53:40.243Z"}],"before_hash":"881101b2384b997897e48d7b4fa03dcc0bfbf1999b521b414474d991a79f108f","after_hash":"b080666adbafa52ea80f79fc387c8f79001213323cf7193c108560aa8b4cf10d","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":"4216721d022288bc641d79fafc2492db0ca85d52253800d3d884ef81fa8034d8"} {"hash_algorithm":"sha256","ts":"2026-10-23T15:58:46.328Z","author":"claude-orchestrator","author_source":"asserted","agent_harness":"claude-code","agent_model":"claude-opus-5-5","agent_model_source":"probe","agent_instance":"73b8b11d9a2002b7bdeea5cd","agent_provenance":{"model":{"value":"claude-opus-5-5","source":"probe"},"effort":{"value":"xhigh","source":"environment"},"role":null,"topic":null,"version":{"value":"2.1.309","source":"probe"}},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/44","value":{"created_at":"2026-10-23T15:58:46.327Z","author":"claude-orchestrator","text":"Delta review (codex) BLOCK: hasStructuredOwnership missed the core external_ref field, so an untagged item with external_ref gh:acme/widgets#31 citing #30 could still be adopted. Any non-empty external_ref (gh:, linear:, jira:, ado: or other) now counts as structured ownership. Tests for gh: and linear: external_ref; the structured-ownership test fails on revert and passes with the fix; suite 46/46; typecheck and lint pass."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T15:58:46.328Z"}],"before_hash":"b080666adbafa52ea80f79fc387c8f79001213323cf7193c108560aa8b4cf10d","after_hash":"b51d2d54adef0a6a29ecd734bec1611bfea19d3c89bfb0b2c02e435b6accc119","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":"c6f7451af4f7de49f081f9fef956279d708aa8ae3f38b6b38c75e080eae4c85d"} +{"hash_algorithm":"sha256","ts":"2026-10-23T16:07:35.434Z","author":"claude-orchestrator","author_source":"asserted","agent_harness":"claude-code","agent_model":"claude-opus-5-5","agent_model_source":"probe","agent_instance":"73b8b11d9a2002b7bdeea5cd","agent_provenance":{"model":{"value":"claude-opus-5-5","source":"probe"},"effort":{"value":"xhigh","source":"environment"},"role":null,"topic":null,"version":{"value":"2.1.309","source":"probe"}},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/45","value":{"created_at":"2026-10-23T16:07:35.434Z","author":"claude-orchestrator","text":"Enumerating review (codex) BLOCK: source_owner missed. hasStructuredOwnership now mirrors every ownership source identity.ts recognizes: any filled github_* field (except github_link_only true), github_link_only false, core external_ref and source_owner, linear_/jira_/ado_ id/key/url/identifier fields, provider provenance, native 'GitHub issue ' declaration, GitHub issue URLs in provenance/external links. Tests cover source_owner, jira_issue_key, github_state and github_link_only false; suite 46/46; typecheck, lint, docstring pass."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T16:07:35.434Z"}],"before_hash":"b51d2d54adef0a6a29ecd734bec1611bfea19d3c89bfb0b2c02e435b6accc119","after_hash":"ff153f6fb54b17f74b0bf6b992c52d69f1c3b669d11446c383e418c523ad8e95","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":"3e6fd0d51ed8d8a8da9a5302d0a28c07715dd17ba5d67850450c79749e21d036"} diff --git a/README.md b/README.md index aa3e0084..734ecf78 100644 --- a/README.md +++ b/README.md @@ -420,7 +420,7 @@ fallback deliberately favors refusal over overwriting an unrelated item: Plans include `citation_candidates: [{id, reason}]`; aggregate dry-run and write receipts include `citation_candidates: [{issue, id, reason}]`. Reasons are -`adoption_disabled`, `missing_intent`, `multiple_issues`, `normalization_limit` or `structured_owner` (the item already carries a core `external_ref`, a GitHub issue URL field, issue number, provider provenance or native declaration, so prose citing another issue never re-owns it). +`adoption_disabled`, `missing_intent`, `multiple_issues`, `normalization_limit` or `structured_owner` (the item already carries a `github_*` field, a core `external_ref` or `source_owner`, a `linear_*`/`jira_*`/`ado_*` identity field, provider provenance, a GitHub issue URL in its links or a native declaration, so prose citing another issue never re-owns it). Candidate reporting never changes the referenced item's status, content or fields, including a closed item's completion state. diff --git a/import-fidelity.ts b/import-fidelity.ts index dae543d2..406b6d88 100644 --- a/import-fidelity.ts +++ b/import-fidelity.ts @@ -240,13 +240,20 @@ function providerProvenance(item: PmItem): string { .map(([, value]) => typeof value === "string" ? value : JSON.stringify(value)).join("\n"); } -/** True when an item already carries a structured GitHub or provider identity of its own (any core external_ref counts). */ +/** + * True when an item already carries a structured GitHub or provider identity of its own. Mirrors every ownership source + * identity.ts recognizes: any `github_*` field, 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. + */ function hasStructuredOwnership(item: PmItem): boolean { const issueLocator = /^https:\/\/github\.com\/[^/]+\/[^/]+\/issues\/\d+$/; - return item.github_number !== undefined || Boolean(providerProvenance(item)) || - (typeof item.external_ref === "string" && item.external_ref.trim() !== "") || + 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) && key !== "github_link_only" || + /^(?:external_ref|source_owner)$/.test(key) || /^(?:linear|jira|ado)_(?:.*_)?(?:id|key|url|identifier)$/i.test(key))) || + item.github_link_only === false || Boolean(providerProvenance(item)) || (typeof item.description === "string" && /^GitHub issue(?:\s*:\s+|\s+)https?:\/\//i.test(item.description)) || - Object.entries(item).some(([key, value]) => /^(?:github_?url|provenance|external_?(?:links?|url)|links?)$/i.test(key) && + Object.entries(item).some(([key, value]) => /^(?:provenance|external_?(?:links?|url)|links?)$/i.test(key) && locators(value).some(url => issueLocator.test(url))); } diff --git a/test/cited-issue-adoption.test.ts b/test/cited-issue-adoption.test.ts index 3e1a128f..aec85b5a 100644 --- a/test/cited-issue-adoption.test.ts +++ b/test/cited-issue-adoption.test.ts @@ -642,6 +642,10 @@ test("citation adoption never overwrites an item that already carries structured { 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: "not-link-only", description: `Fixes ${repo}#30`, github_link_only: false }, ]; for (const item of owned) { const result = matchImportedIssue([item], issue); From b4a7d0d75cb36d07af5f417ef821a7a7c5ca2bd8 Mon Sep 17 00:00:00 2001 From: SteveBot <1153461+unbraind@users.noreply.github.com> Date: Fri, 23 Oct 2026 18:19:29 +0200 Subject: [PATCH 17/18] Share one owner-link field pattern between the owner matcher and the adoption guard The owner matcher accepts a githuburl alias that the adoption guard did not treat as ownership. Any github-prefixed field (except github_link_only) now counts as ownership, and the owner-link field pattern is a single constant used by both checks so they cannot drift. --- .../pm/features/pm-github-cited-issue-adoption.toon | 5 +++-- .../pm/history/pm-github-cited-issue-adoption.jsonl | 1 + import-fidelity.ts | 11 +++++++---- test/cited-issue-adoption.test.ts | 1 + 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/.agents/pm/features/pm-github-cited-issue-adoption.toon b/.agents/pm/features/pm-github-cited-issue-adoption.toon index 3770b3e5..24dcc4c8 100644 --- a/.agents/pm/features/pm-github-cited-issue-adoption.toon +++ b/.agents/pm/features/pm-github-cited-issue-adoption.toon @@ -6,10 +6,10 @@ status: in_progress priority: 2 tags[3]: changelog-unreleased,github,import created_at: "2026-10-23T07:16:54.041Z" -updated_at: "2026-10-23T16:07:35.434Z" +updated_at: "2026-10-23T16:19:17.184Z" author: codex-agent acceptance_criteria: "Adoption is off by default across CLI, SDK and reusable sync, enabled only with --adopt-cited, adoptCited true or adopt-cited true respectively. Default import retains normal create/structured-match behavior and read-only citation_candidates without fallback ambiguity refusal. Document creating items with gh:owner/repo#N as primary duplicate prevention; Normalize HTML named/numeric entities and percent bytes repeatedly until stable with a bounded refusal, apply NFKC and remove invisible/bidi controls, unwrap Markdown links/reference definitions and join soft breaks before anchored ownership checks while retaining source for conservative exclusion; Only title qualified references, leading description closing-keyword references, or whole body Issue:/Tracks: URL lines can authorize opt-in adoption. Any other normalized GitHub URL, foreign repository, other number or unattributed reference refuses adoption and reports candidate hints. Preserve structured identity precedence, single-owner upsert, eligible-owner ambiguity refusal, receipts, idempotency and non-git support; Keep all review reproductions and prior shape inputs, add encoded-reference fuzzing and explicit pm-review Issue label positives/default creates, record behavioral function/workflow reverts and public dry-run receipts, run targeted checks and wait for exact-head npm/Bun CI release gates" -comments[46]{created_at,author,text}: +comments[47]{created_at,author,text}: "2026-10-23T07:17:26.861Z",codex-agent,"Overlap review: pm-github-c093 owns structured URL/provider back-link import matching, pm-github-identity-collisions diagnoses and repairs structured ownership, Projects import matches board and issue provenance, and export upserts linked issues. None adopts arbitrary exact prose citations. This feature adds a fallback only after existing import matching, preserving those paths. Scheduled gated sync already invokes import, so it inherits the default. Plan ambiguous citations before writes and refuse the batch with actionable candidate IDs." "2026-10-23T07:24:01.857Z",codex-agent,"Implementation: strong ownership matching stays first; exact qualified title/description/body citations fall back to a single existing item. All imports now prepare identities before writes, and ambiguous citations expose candidate IDs plus an exact tagging remedy. Adoption retains local tags and reports a separate count; provider-owned records retain link-only behavior. New tests use real trackers with a PATH containing node and the pinned pm CLI but no git, plus real git trackers for write-gate acceptance." "2026-10-23T07:27:45.693Z",codex-agent,"New citation suite: 13/13 passed, including plain/atomic adoption in non-git trackers, write-gate acceptance, retry identity, ambiguity refusal, punctuation, repo/number boundaries, structured ownership precedence and provider link-only preservation. Live pm-review main aa16d855df1f7b74afd3453617f77e92c44d493e already tags pm-review-withdrawn-lineage for issue 30, so an untouched current-main copy correctly plans update rather than adopt. Preserve this current-state result and locate the pre-tag historical tracker for a separate adoption replay. SDK reads may create runtime caches; verify original tracked file bytes independently." @@ -56,6 +56,7 @@ comments[46]{created_at,author,text}: "2026-10-23T15:53:40.243Z",claude-orchestrator,"Review round 6 (codex) BLOCK: with --adopt-cited, an untagged item whose description says 'Fixes acme/widgets#30' but whose github_url names issue 31 was adopted for #30, overwriting #31's identity. Fix: an item with structured ownership (GitHub issue URL in github_url/provenance/external links, github_number, provider provenance, or a native 'GitHub issue ' declaration) is never adopted from prose; it is reported as a citation candidate with reason structured_owner. Regression test fails on revert (0/1) and passes with the fix; adoption suite 46/46; typecheck, lint, docstring, build pass. README lists the new reason." "2026-10-23T15:58:46.327Z",claude-orchestrator,"Delta review (codex) BLOCK: hasStructuredOwnership missed the core external_ref field, so an untagged item with external_ref gh:acme/widgets#31 citing #30 could still be adopted. Any non-empty external_ref (gh:, linear:, jira:, ado: or other) now counts as structured ownership. Tests for gh: and linear: external_ref; the structured-ownership test fails on revert and passes with the fix; suite 46/46; typecheck and lint pass." "2026-10-23T16:07:35.434Z",claude-orchestrator,"Enumerating review (codex) BLOCK: source_owner missed. hasStructuredOwnership now mirrors every ownership source identity.ts recognizes: any filled github_* field (except github_link_only true), github_link_only false, core external_ref and source_owner, linear_/jira_/ado_ id/key/url/identifier fields, provider provenance, native 'GitHub issue ' declaration, GitHub issue URLs in provenance/external links. Tests cover source_owner, jira_issue_key, github_state and github_link_only false; suite 46/46; typecheck, lint, docstring pass." + "2026-10-23T16:19:17.181Z",claude-orchestrator,"Round 9 review (codex) BLOCK: githuburl alias (accepted by the owner matcher) was not treated as ownership. The guard now treats any github-prefixed field (except github_link_only) as ownership, and the owner-link field pattern is one shared constant (OWNER_LINK_FIELD) used by both the owner matcher and the adoption guard so they cannot drift again. Test adds githuburl; suite 46/46; typecheck, lint, docstring pass." files[13]{path,scope}: .github/workflows/pm-github-sync.yml,project import-fidelity.ts,project diff --git a/.agents/pm/history/pm-github-cited-issue-adoption.jsonl b/.agents/pm/history/pm-github-cited-issue-adoption.jsonl index dd2d2bdf..8e5fc9a9 100644 --- a/.agents/pm/history/pm-github-cited-issue-adoption.jsonl +++ b/.agents/pm/history/pm-github-cited-issue-adoption.jsonl @@ -87,3 +87,4 @@ {"hash_algorithm":"sha256","ts":"2026-10-23T15:53:40.243Z","author":"claude-orchestrator","author_source":"asserted","agent_harness":"claude-code","agent_model":"claude-opus-5-5","agent_model_source":"probe","agent_instance":"73b8b11d9a2002b7bdeea5cd","agent_provenance":{"model":{"value":"claude-opus-5-5","source":"probe"},"effort":{"value":"xhigh","source":"environment"},"role":null,"topic":null,"version":{"value":"2.1.309","source":"probe"}},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/43","value":{"created_at":"2026-10-23T15:53:40.243Z","author":"claude-orchestrator","text":"Review round 6 (codex) BLOCK: with --adopt-cited, an untagged item whose description says 'Fixes acme/widgets#30' but whose github_url names issue 31 was adopted for #30, overwriting #31's identity. Fix: an item with structured ownership (GitHub issue URL in github_url/provenance/external links, github_number, provider provenance, or a native 'GitHub issue ' declaration) is never adopted from prose; it is reported as a citation candidate with reason structured_owner. Regression test fails on revert (0/1) and passes with the fix; adoption suite 46/46; typecheck, lint, docstring, build pass. README lists the new reason."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T15:53:40.243Z"}],"before_hash":"881101b2384b997897e48d7b4fa03dcc0bfbf1999b521b414474d991a79f108f","after_hash":"b080666adbafa52ea80f79fc387c8f79001213323cf7193c108560aa8b4cf10d","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":"4216721d022288bc641d79fafc2492db0ca85d52253800d3d884ef81fa8034d8"} {"hash_algorithm":"sha256","ts":"2026-10-23T15:58:46.328Z","author":"claude-orchestrator","author_source":"asserted","agent_harness":"claude-code","agent_model":"claude-opus-5-5","agent_model_source":"probe","agent_instance":"73b8b11d9a2002b7bdeea5cd","agent_provenance":{"model":{"value":"claude-opus-5-5","source":"probe"},"effort":{"value":"xhigh","source":"environment"},"role":null,"topic":null,"version":{"value":"2.1.309","source":"probe"}},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/44","value":{"created_at":"2026-10-23T15:58:46.327Z","author":"claude-orchestrator","text":"Delta review (codex) BLOCK: hasStructuredOwnership missed the core external_ref field, so an untagged item with external_ref gh:acme/widgets#31 citing #30 could still be adopted. Any non-empty external_ref (gh:, linear:, jira:, ado: or other) now counts as structured ownership. Tests for gh: and linear: external_ref; the structured-ownership test fails on revert and passes with the fix; suite 46/46; typecheck and lint pass."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T15:58:46.328Z"}],"before_hash":"b080666adbafa52ea80f79fc387c8f79001213323cf7193c108560aa8b4cf10d","after_hash":"b51d2d54adef0a6a29ecd734bec1611bfea19d3c89bfb0b2c02e435b6accc119","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":"c6f7451af4f7de49f081f9fef956279d708aa8ae3f38b6b38c75e080eae4c85d"} {"hash_algorithm":"sha256","ts":"2026-10-23T16:07:35.434Z","author":"claude-orchestrator","author_source":"asserted","agent_harness":"claude-code","agent_model":"claude-opus-5-5","agent_model_source":"probe","agent_instance":"73b8b11d9a2002b7bdeea5cd","agent_provenance":{"model":{"value":"claude-opus-5-5","source":"probe"},"effort":{"value":"xhigh","source":"environment"},"role":null,"topic":null,"version":{"value":"2.1.309","source":"probe"}},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/45","value":{"created_at":"2026-10-23T16:07:35.434Z","author":"claude-orchestrator","text":"Enumerating review (codex) BLOCK: source_owner missed. hasStructuredOwnership now mirrors every ownership source identity.ts recognizes: any filled github_* field (except github_link_only true), github_link_only false, core external_ref and source_owner, linear_/jira_/ado_ id/key/url/identifier fields, provider provenance, native 'GitHub issue ' declaration, GitHub issue URLs in provenance/external links. Tests cover source_owner, jira_issue_key, github_state and github_link_only false; suite 46/46; typecheck, lint, docstring pass."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T16:07:35.434Z"}],"before_hash":"b51d2d54adef0a6a29ecd734bec1611bfea19d3c89bfb0b2c02e435b6accc119","after_hash":"ff153f6fb54b17f74b0bf6b992c52d69f1c3b669d11446c383e418c523ad8e95","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":"3e6fd0d51ed8d8a8da9a5302d0a28c07715dd17ba5d67850450c79749e21d036"} +{"hash_algorithm":"sha256","ts":"2026-10-23T16:19:17.184Z","author":"claude-orchestrator","author_source":"asserted","agent_harness":"claude-code","agent_model":"claude-opus-5-5","agent_model_source":"probe","agent_instance":"73b8b11d9a2002b7bdeea5cd","agent_provenance":{"model":{"value":"claude-opus-5-5","source":"probe"},"effort":{"value":"xhigh","source":"environment"},"role":null,"topic":null,"version":{"value":"2.1.309","source":"probe"}},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/46","value":{"created_at":"2026-10-23T16:19:17.181Z","author":"claude-orchestrator","text":"Round 9 review (codex) BLOCK: githuburl alias (accepted by the owner matcher) was not treated as ownership. The guard now treats any github-prefixed field (except github_link_only) as ownership, and the owner-link field pattern is one shared constant (OWNER_LINK_FIELD) used by both the owner matcher and the adoption guard so they cannot drift again. Test adds githuburl; suite 46/46; typecheck, lint, docstring pass."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T16:19:17.184Z"}],"before_hash":"ff153f6fb54b17f74b0bf6b992c52d69f1c3b669d11446c383e418c523ad8e95","after_hash":"eb918288b30aa879cdb370da2bad7a819c42171727b9763538f204fd84e40a06","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":"88c2b4a307625089c6c87cbc6716f9a1c2cd6a3e65c0448749084c4bde10d434"} diff --git a/import-fidelity.ts b/import-fidelity.ts index 406b6d88..e1acec91 100644 --- a/import-fidelity.ts +++ b/import-fidelity.ts @@ -240,20 +240,23 @@ 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. */ +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 recognizes: any `github_*` field, a core `external_ref`, a native `source_owner`, provider identity fields + * identity.ts and the owner matcher recognize: any `github*` field (`github_url`, `githuburl`, ...), 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. */ 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) && key !== "github_link_only" || + return Object.entries(item).some(([key, value]) => filled(value) && (/^github/i.test(key) && !/^github_?link_?only$/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)) || (typeof item.description === "string" && /^GitHub issue(?:\s*:\s+|\s+)https?:\/\//i.test(item.description)) || - Object.entries(item).some(([key, value]) => /^(?:provenance|external_?(?:links?|url)|links?)$/i.test(key) && + Object.entries(item).some(([key, value]) => OWNER_LINK_FIELD.test(key) && locators(value).some(url => issueLocator.test(url))); } @@ -266,7 +269,7 @@ export function matchImportedIssue(items: readonly PmItem[], issue: GhIssue, tag 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]) => /^(?:github_?url|provenance|external_?(?:links?|url)|links?)$/i.test(key) && locators(value).includes(issueUrl)) || + 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.`); let item = candidates[0]; diff --git a/test/cited-issue-adoption.test.ts b/test/cited-issue-adoption.test.ts index aec85b5a..dff61cc5 100644 --- a/test/cited-issue-adoption.test.ts +++ b/test/cited-issue-adoption.test.ts @@ -645,6 +645,7 @@ test("citation adoption never overwrites an item that already carries structured { 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: "not-link-only", description: `Fixes ${repo}#30`, github_link_only: false }, ]; for (const item of owned) { From 87d9dc707ab4ab4165ecee2fdec6fb5f597e2c2b Mon Sep 17 00:00:00 2001 From: SteveBot <1153461+unbraind@users.noreply.github.com> Date: Fri, 23 Oct 2026 18:28:53 +0200 Subject: [PATCH 18/18] Treat GitHub identity tags in any case as ownership before citation adoption identity.ts recognizes GH:owner/repo#N case-insensitively, while the adoption filter only parsed lowercase provenance tags, so an item tagged GH:other/widgets#31 could be adopted for another issue. Any well-formed GitHub identity tag, in any case, now marks the item as owned. --- .agents/pm/features/pm-github-cited-issue-adoption.toon | 5 +++-- .agents/pm/history/pm-github-cited-issue-adoption.jsonl | 1 + import-fidelity.ts | 3 ++- test/cited-issue-adoption.test.ts | 1 + 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.agents/pm/features/pm-github-cited-issue-adoption.toon b/.agents/pm/features/pm-github-cited-issue-adoption.toon index 24dcc4c8..98785d25 100644 --- a/.agents/pm/features/pm-github-cited-issue-adoption.toon +++ b/.agents/pm/features/pm-github-cited-issue-adoption.toon @@ -6,10 +6,10 @@ status: in_progress priority: 2 tags[3]: changelog-unreleased,github,import created_at: "2026-10-23T07:16:54.041Z" -updated_at: "2026-10-23T16:19:17.184Z" +updated_at: "2026-10-23T16:28:46.870Z" author: codex-agent acceptance_criteria: "Adoption is off by default across CLI, SDK and reusable sync, enabled only with --adopt-cited, adoptCited true or adopt-cited true respectively. Default import retains normal create/structured-match behavior and read-only citation_candidates without fallback ambiguity refusal. Document creating items with gh:owner/repo#N as primary duplicate prevention; Normalize HTML named/numeric entities and percent bytes repeatedly until stable with a bounded refusal, apply NFKC and remove invisible/bidi controls, unwrap Markdown links/reference definitions and join soft breaks before anchored ownership checks while retaining source for conservative exclusion; Only title qualified references, leading description closing-keyword references, or whole body Issue:/Tracks: URL lines can authorize opt-in adoption. Any other normalized GitHub URL, foreign repository, other number or unattributed reference refuses adoption and reports candidate hints. Preserve structured identity precedence, single-owner upsert, eligible-owner ambiguity refusal, receipts, idempotency and non-git support; Keep all review reproductions and prior shape inputs, add encoded-reference fuzzing and explicit pm-review Issue label positives/default creates, record behavioral function/workflow reverts and public dry-run receipts, run targeted checks and wait for exact-head npm/Bun CI release gates" -comments[47]{created_at,author,text}: +comments[48]{created_at,author,text}: "2026-10-23T07:17:26.861Z",codex-agent,"Overlap review: pm-github-c093 owns structured URL/provider back-link import matching, pm-github-identity-collisions diagnoses and repairs structured ownership, Projects import matches board and issue provenance, and export upserts linked issues. None adopts arbitrary exact prose citations. This feature adds a fallback only after existing import matching, preserving those paths. Scheduled gated sync already invokes import, so it inherits the default. Plan ambiguous citations before writes and refuse the batch with actionable candidate IDs." "2026-10-23T07:24:01.857Z",codex-agent,"Implementation: strong ownership matching stays first; exact qualified title/description/body citations fall back to a single existing item. All imports now prepare identities before writes, and ambiguous citations expose candidate IDs plus an exact tagging remedy. Adoption retains local tags and reports a separate count; provider-owned records retain link-only behavior. New tests use real trackers with a PATH containing node and the pinned pm CLI but no git, plus real git trackers for write-gate acceptance." "2026-10-23T07:27:45.693Z",codex-agent,"New citation suite: 13/13 passed, including plain/atomic adoption in non-git trackers, write-gate acceptance, retry identity, ambiguity refusal, punctuation, repo/number boundaries, structured ownership precedence and provider link-only preservation. Live pm-review main aa16d855df1f7b74afd3453617f77e92c44d493e already tags pm-review-withdrawn-lineage for issue 30, so an untouched current-main copy correctly plans update rather than adopt. Preserve this current-state result and locate the pre-tag historical tracker for a separate adoption replay. SDK reads may create runtime caches; verify original tracked file bytes independently." @@ -57,6 +57,7 @@ comments[47]{created_at,author,text}: "2026-10-23T15:58:46.327Z",claude-orchestrator,"Delta review (codex) BLOCK: hasStructuredOwnership missed the core external_ref field, so an untagged item with external_ref gh:acme/widgets#31 citing #30 could still be adopted. Any non-empty external_ref (gh:, linear:, jira:, ado: or other) now counts as structured ownership. Tests for gh: and linear: external_ref; the structured-ownership test fails on revert and passes with the fix; suite 46/46; typecheck and lint pass." "2026-10-23T16:07:35.434Z",claude-orchestrator,"Enumerating review (codex) BLOCK: source_owner missed. hasStructuredOwnership now mirrors every ownership source identity.ts recognizes: any filled github_* field (except github_link_only true), github_link_only false, core external_ref and source_owner, linear_/jira_/ado_ id/key/url/identifier fields, provider provenance, native 'GitHub issue ' declaration, GitHub issue URLs in provenance/external links. Tests cover source_owner, jira_issue_key, github_state and github_link_only false; suite 46/46; typecheck, lint, docstring pass." "2026-10-23T16:19:17.181Z",claude-orchestrator,"Round 9 review (codex) BLOCK: githuburl alias (accepted by the owner matcher) was not treated as ownership. The guard now treats any github-prefixed field (except github_link_only) as ownership, and the owner-link field pattern is one shared constant (OWNER_LINK_FIELD) used by both the owner matcher and the adoption guard so they cannot drift again. Test adds githuburl; suite 46/46; typecheck, lint, docstring pass." + "2026-10-23T16:28:46.869Z",claude-orchestrator,"Round 10 review (codex) BLOCK: an uppercase GH:owner/repo#N tag (recognized by identity.ts) did not mark ownership because the adoption filter's provenance-tag parser is lowercase-only. The guard now treats any well-formed GitHub identity tag in any case (identity.ts grammar, /^gh:owner/repo#N$/i) as ownership; malformed gh: tags keep today's behaviour. Test adds GH:other/widgets#31; suite 46/46; typecheck, lint, docstring pass." files[13]{path,scope}: .github/workflows/pm-github-sync.yml,project import-fidelity.ts,project diff --git a/.agents/pm/history/pm-github-cited-issue-adoption.jsonl b/.agents/pm/history/pm-github-cited-issue-adoption.jsonl index 8e5fc9a9..c0d03ae6 100644 --- a/.agents/pm/history/pm-github-cited-issue-adoption.jsonl +++ b/.agents/pm/history/pm-github-cited-issue-adoption.jsonl @@ -88,3 +88,4 @@ {"hash_algorithm":"sha256","ts":"2026-10-23T15:58:46.328Z","author":"claude-orchestrator","author_source":"asserted","agent_harness":"claude-code","agent_model":"claude-opus-5-5","agent_model_source":"probe","agent_instance":"73b8b11d9a2002b7bdeea5cd","agent_provenance":{"model":{"value":"claude-opus-5-5","source":"probe"},"effort":{"value":"xhigh","source":"environment"},"role":null,"topic":null,"version":{"value":"2.1.309","source":"probe"}},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/44","value":{"created_at":"2026-10-23T15:58:46.327Z","author":"claude-orchestrator","text":"Delta review (codex) BLOCK: hasStructuredOwnership missed the core external_ref field, so an untagged item with external_ref gh:acme/widgets#31 citing #30 could still be adopted. Any non-empty external_ref (gh:, linear:, jira:, ado: or other) now counts as structured ownership. Tests for gh: and linear: external_ref; the structured-ownership test fails on revert and passes with the fix; suite 46/46; typecheck and lint pass."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T15:58:46.328Z"}],"before_hash":"b080666adbafa52ea80f79fc387c8f79001213323cf7193c108560aa8b4cf10d","after_hash":"b51d2d54adef0a6a29ecd734bec1611bfea19d3c89bfb0b2c02e435b6accc119","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":"c6f7451af4f7de49f081f9fef956279d708aa8ae3f38b6b38c75e080eae4c85d"} {"hash_algorithm":"sha256","ts":"2026-10-23T16:07:35.434Z","author":"claude-orchestrator","author_source":"asserted","agent_harness":"claude-code","agent_model":"claude-opus-5-5","agent_model_source":"probe","agent_instance":"73b8b11d9a2002b7bdeea5cd","agent_provenance":{"model":{"value":"claude-opus-5-5","source":"probe"},"effort":{"value":"xhigh","source":"environment"},"role":null,"topic":null,"version":{"value":"2.1.309","source":"probe"}},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/45","value":{"created_at":"2026-10-23T16:07:35.434Z","author":"claude-orchestrator","text":"Enumerating review (codex) BLOCK: source_owner missed. hasStructuredOwnership now mirrors every ownership source identity.ts recognizes: any filled github_* field (except github_link_only true), github_link_only false, core external_ref and source_owner, linear_/jira_/ado_ id/key/url/identifier fields, provider provenance, native 'GitHub issue ' declaration, GitHub issue URLs in provenance/external links. Tests cover source_owner, jira_issue_key, github_state and github_link_only false; suite 46/46; typecheck, lint, docstring pass."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T16:07:35.434Z"}],"before_hash":"b51d2d54adef0a6a29ecd734bec1611bfea19d3c89bfb0b2c02e435b6accc119","after_hash":"ff153f6fb54b17f74b0bf6b992c52d69f1c3b669d11446c383e418c523ad8e95","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":"3e6fd0d51ed8d8a8da9a5302d0a28c07715dd17ba5d67850450c79749e21d036"} {"hash_algorithm":"sha256","ts":"2026-10-23T16:19:17.184Z","author":"claude-orchestrator","author_source":"asserted","agent_harness":"claude-code","agent_model":"claude-opus-5-5","agent_model_source":"probe","agent_instance":"73b8b11d9a2002b7bdeea5cd","agent_provenance":{"model":{"value":"claude-opus-5-5","source":"probe"},"effort":{"value":"xhigh","source":"environment"},"role":null,"topic":null,"version":{"value":"2.1.309","source":"probe"}},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/46","value":{"created_at":"2026-10-23T16:19:17.181Z","author":"claude-orchestrator","text":"Round 9 review (codex) BLOCK: githuburl alias (accepted by the owner matcher) was not treated as ownership. The guard now treats any github-prefixed field (except github_link_only) as ownership, and the owner-link field pattern is one shared constant (OWNER_LINK_FIELD) used by both the owner matcher and the adoption guard so they cannot drift again. Test adds githuburl; suite 46/46; typecheck, lint, docstring pass."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T16:19:17.184Z"}],"before_hash":"ff153f6fb54b17f74b0bf6b992c52d69f1c3b669d11446c383e418c523ad8e95","after_hash":"eb918288b30aa879cdb370da2bad7a819c42171727b9763538f204fd84e40a06","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":"88c2b4a307625089c6c87cbc6716f9a1c2cd6a3e65c0448749084c4bde10d434"} +{"hash_algorithm":"sha256","ts":"2026-10-23T16:28:46.870Z","author":"claude-orchestrator","author_source":"asserted","agent_harness":"claude-code","agent_model":"claude-opus-5-5","agent_model_source":"probe","agent_instance":"73b8b11d9a2002b7bdeea5cd","agent_provenance":{"model":{"value":"claude-opus-5-5","source":"probe"},"effort":{"value":"xhigh","source":"environment"},"role":null,"topic":null,"version":{"value":"2.1.309","source":"probe"}},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/47","value":{"created_at":"2026-10-23T16:28:46.869Z","author":"claude-orchestrator","text":"Round 10 review (codex) BLOCK: an uppercase GH:owner/repo#N tag (recognized by identity.ts) did not mark ownership because the adoption filter's provenance-tag parser is lowercase-only. The guard now treats any well-formed GitHub identity tag in any case (identity.ts grammar, /^gh:owner/repo#N$/i) as ownership; malformed gh: tags keep today's behaviour. Test adds GH:other/widgets#31; suite 46/46; typecheck, lint, docstring pass."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T16:28:46.870Z"}],"before_hash":"eb918288b30aa879cdb370da2bad7a819c42171727b9763538f204fd84e40a06","after_hash":"932987795f75d6844ecea99266993c0028032a488f670a226da9d81283d52758","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":"558d3d19ca405443841d7661705f6079d82c9bd5ccf9a5d15da71388de921233"} diff --git a/import-fidelity.ts b/import-fidelity.ts index e1acec91..e27d93a0 100644 --- a/import-fidelity.ts +++ b/import-fidelity.ts @@ -245,7 +245,7 @@ const OWNER_LINK_FIELD = /^(?:github_?url|provenance|external_?(?:links?|url)|li /** * 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 core `external_ref`, a native `source_owner`, provider identity fields + * 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. */ @@ -255,6 +255,7 @@ function hasStructuredOwnership(item: PmItem): boolean { return Object.entries(item).some(([key, value]) => filled(value) && (/^github/i.test(key) && !/^github_?link_?only$/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))); diff --git a/test/cited-issue-adoption.test.ts b/test/cited-issue-adoption.test.ts index dff61cc5..596ef2d9 100644 --- a/test/cited-issue-adoption.test.ts +++ b/test/cited-issue-adoption.test.ts @@ -646,6 +646,7 @@ test("citation adoption never overwrites an item that already carries structured { 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) {