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..98785d25 --- /dev/null +++ b/.agents/pm/features/pm-github-cited-issue-adoption.toon @@ -0,0 +1,110 @@ +id: pm-github-cited-issue-adoption +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-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[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." + "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." + "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." + "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." + "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." + "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." + "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." + "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." + "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." + "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 + 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 + 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[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 new file mode 100644 index 00000000..c0d03ae6 --- /dev/null +++ b/.agents/pm/history/pm-github-cited-issue-adoption.jsonl @@ -0,0 +1,91 @@ +{"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"} +{"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"} +{"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"} +{"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"} +{"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"} +{"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"} +{"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"} +{"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"} +{"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"} +{"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"} +{"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"} +{"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"} +{"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"} +{"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/.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 3f7ffbe0..047f44fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Added +- 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 0659e698..734ecf78 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. | +| `--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 | @@ -368,6 +369,74 @@ 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. +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`, `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. + +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. `--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. @@ -781,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. | @@ -806,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 new file mode 100644 index 00000000..158a9c78 --- /dev/null +++ b/docs/cited-issue-adoption-validation.md @@ -0,0 +1,169 @@ +# Cited issue adoption validation + +## 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. + +## Regression evidence + +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. + +| 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. +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. + +## 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 +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 | + +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. + +## Public tracker replays + +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 | + +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", + "github_url": "https://github.com/unbraind/pm-review/issues/30", + "identity": { "via": "citation" } +} +``` + +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 5723ba00..e27d93a0 100644 --- a/import-fidelity.ts +++ b/import-fidelity.ts @@ -1,10 +1,11 @@ /** 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, } 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. @@ -42,12 +43,172 @@ 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[] } -/** Trim exporter punctuation in one backwards pass. @internal Shared with parity tests. */ -export function trimLocatorPunctuation(locator: string): string { +/** 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" | "structured_owner"; +} + +/** 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; + 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[], 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; + } +} + +/** 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); + for (const token of tokens) { + const raw = token[0]; + if (/^https?:/i.test(raw)) { + try { + const url = new URL(trimLocatorPunctuation(raw, "),.;:!]}")); + const path = /^\/([\w.-]+\/[\w.-]+)\/(issues|pull)\/([1-9]\d*)\/?$/i.exec(url.pathname); + if (["github.com", "www.github.com"].includes(url.hostname) && path) { + references.push({ key: `${path[1]!.toLowerCase()}#${path[3]}`, index: token.index, ownsIssue: path[2]!.toLowerCase() === "issues" }); + } + } catch { /* Malformed URL tokens never expose shorter hash references inside them. */ } + } else { + const [repo, number] = raw.split("#"); + references.push({ key: `${repo || repoHint}#${number}`.toLowerCase(), index: token.index, ownsIssue: repo !== "" }); + } + } + return references; +} + +/** Decode nested representations before either ownership or exclusion; an exhausted bound refuses adoption. */ +function normalizeCitationText(input: string): { text: string; source: string; stable: boolean } { + let text = input; + for (let pass = 0; pass < 8; pass++) { + 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; + } + 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 { + let text = source; + if (source.includes("[")) { + const definitions = new Map(); + 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(/(?"'`[\]()*{}]*/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; +} + +/** Read the immediately preceding repository without making every character a regex start position. */ +function citationRepositoryBefore(text: string, index: number, hash: boolean): string | undefined { + if (!hash && text[index - 1] !== "/") return undefined; + const end = hash ? index : index - 1; + let repoStart = end; + while (repoStart > 0 && /[\w.-]/.test(text[repoStart - 1]!)) repoStart--; + if (repoStart === end || text[repoStart - 1] !== "/") return undefined; + let ownerStart = repoStart - 1; + while (ownerStart > 0 && /[\w.-]/.test(text[ownerStart - 1]!)) ownerStart--; + if (ownerStart === repoStart - 1 || (!hash && text.slice(Math.max(0, ownerStart - 11), ownerStart).toLowerCase() !== "github.com/")) return undefined; + 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 new Set(texts)) { + // Check both retained source and joined soft lines; joining must not discard link labels or destinations. + for (const text of new Set([source, source.replace(/(?`, 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; +} + +/** 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. */ +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); } @@ -79,8 +240,29 @@ 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 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. + */ +function hasStructuredOwnership(item: PmItem): boolean { + const issueLocator = /^https:\/\/github\.com\/[^/]+\/[^/]+\/issues\/\d+$/; + const filled = (value: unknown) => value !== undefined && value !== null && value !== false && !(typeof value === "string" && value.trim() === ""); + return Object.entries(item).some(([key, value]) => filled(value) && (/^github/i.test(key) && !/^github_?link_?only$/i.test(key) || + /^(?: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))); +} + /** 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 = 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. */ @@ -88,14 +270,35 @@ 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.`); - const item = candidates[0]; + let item = candidates[0]; + let cited = false; + const incidental: CitationCandidate[] = []; + if (!item && !issue.pull_request) { + const repo = issue.html_url.split("/").slice(3, 5).join("/").toLowerCase(); + const 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 === 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)); + 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)), - via: item ? (provider ? "provider-link" : native ? "github-declaration" : item.id === tagged?.id ? "provenance-tag" : "github-url") : "new" }; + ...(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" }; } /** Read calendar release tags from the selected tracker checkout, including annotated tag dates. */ diff --git a/index.ts b/index.ts index 16b02ffb..f7ce0254 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 CitationCandidate, 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 { + /** 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. */ @@ -1329,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; } @@ -2732,6 +2736,7 @@ export function parseImportOptions(options: Record): ImportOpti atomic: optionEnabled(options, "atomic"), linkDeps: optionEnabled(options, "link-deps", "linkDeps"), gate: optionEnabled(options, "gate"), + adoptCited: optionEnabled(options, "adopt-cited", "adoptCited"), }; } @@ -3113,7 +3118,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.adoptCited === true); match = identity.item; const timeline = await fidelityContext.timeline(issue, repo, token); const fidelity = buildImportFidelity(issue, fidelityContext.releases, timeline); @@ -3151,7 +3156,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, @@ -3160,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, }; } @@ -3176,6 +3182,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 +3280,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 +3295,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 +3452,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 +3481,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 +3497,50 @@ export async function runImport( let gateCompleteness: ImportPlanCompletenessReceipts | undefined; let gateIdempotency: ImportIdempotencyReceipts | undefined; let gatePlanByNumber: Map | undefined; - if (opts.gate || opts.atomic) { + const citationCandidates: (CitationCandidate & { issue: number })[] = []; + let citationReceipt: { citation_candidates?: typeof citationCandidates } = {}; + { 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); + for (const candidate of error.entry.citation_candidates ?? []) citationCandidates.push({ issue: issue.number, ...candidate }); + continue; + } if (!entry) { skippedNumbers.push(issue.number); 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, ...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, + ...(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 +3558,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,16 +3567,19 @@ 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", 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 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 || citationCandidates.length > 0 ? { plan: fidelityPlan() } : {}), ...citationReceipt }; + 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 +3594,7 @@ export async function runImport( dryRun: true, wouldImport: 0, wouldUpdate: 0, + wouldAdopt: 0, wouldSkip: skipped, atomic: true, ...fetchFields, @@ -3574,13 +3608,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 +3626,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 +3679,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 +3728,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 +3742,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 +3791,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 +3860,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 +3871,7 @@ export async function runImport( dryRun: true, wouldImport: imported, wouldUpdate: updated, + wouldAdopt: adopted, wouldSkip: skipped, ...(opts.linkDeps ? { wouldLinkDependencyCandidates: countDependencyRefCandidates(repo, filtered) } : {}), ...gatePlanFields, @@ -3854,8 +3879,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 +3902,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 +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/atomic.test.ts b/test/atomic.test.ts index 23e4571c..2d1902c8 100644 --- a/test/atomic.test.ts +++ b/test/atomic.test.ts @@ -130,6 +130,7 @@ test("atomic dry-run previews creates and updates without invoking the SDK", asy dryRun: true, wouldImport: 1, wouldUpdate: 1, + wouldAdopt: 0, wouldSkip: 1, atomic: true, }); diff --git a/test/cited-issue-adoption.test.ts b/test/cited-issue-adoption.test.ts new file mode 100644 index 00000000..596ef2d9 --- /dev/null +++ b/test/cited-issue-adoption.test.ts @@ -0,0 +1,661 @@ +/** Citation adoption exercises real initialized trackers, including a PATH without git. */ +import assert from "node:assert/strict"; +import { spawnSync } from "node:child_process"; +import { mkdtempSync, mkdirSync, rmSync, symlinkSync } from "node:fs"; +import { tmpdir } from "node:os"; +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 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"; +const issue: GhIssue = { number: 30, title: "Fixed behavior", body: "Upstream fix", state: "closed", state_reason: "completed", + labels: [{ name: "bug" }], assignee: null, milestone: null, user: null, + html_url: `https://github.com/${repo}/issues/30`, created_at: "2026-10-01T00:00:00Z", + updated_at: "2026-10-03T00:00:00Z", closed_at: "2026-10-02T00:00:00Z" }; + +/** Initialize an isolated tracker with no enclosing repository and optionally allow a real gate. */ +async function tracker(run: (pmRoot: string) => 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({ adoptCited: true, ...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: `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); + 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: (${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); + 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(`${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(`${atomic ? "atomic" : "plain"} trailing-slash second reference preserves the roadmap in a non-git tracker`, async () => 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(`${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(`${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", () => { + 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, "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, reference.includes("/pull/") ? "new" : "citation", reference); + } +}); + +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, 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: "" }; + 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("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).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); + } + 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, "new"); +}); + +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`, `Tracks:${repo}#30`, `${repo}#30?`, `ACME/Widgets#30;`]) { + const local = { id: "local", [field]: `Fixed ${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"); + 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: `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 }; + 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({}).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); +})); + +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("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, "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"]) { + 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\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); + 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 () => { + 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`)); +}); + +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"); +}); + +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"); +}); + +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"] }, + { id: "ref", description: `Fixes ${repo}#30`, external_ref: "gh:acme/widgets#31" }, + { id: "linear", description: `Fixes ${repo}#30`, external_ref: "linear:ENG-31" }, + { id: "source-owner", description: `Fixes ${repo}#30`, source_owner: "linear", status: "closed" }, + { id: "provider-field", description: `Fixes ${repo}#30`, jira_issue_key: "ENG-31" }, + { id: "github-field", description: `Fixes ${repo}#30`, github_state: "closed" }, + { id: "githuburl", description: `Fixes ${repo}#30`, githuburl: "https://github.com/acme/widgets/issues/31" }, + { id: "upper-tag", title: `${repo}#30`, description: `Fixes ${repo}#30`, tags: ["GH:other/widgets#31"] }, + { id: "not-link-only", description: `Fixes ${repo}#30`, github_link_only: false }, + ]; + for (const item of owned) { + 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); +}); 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..eacc5526 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); })); } @@ -286,12 +283,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", 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 037e454f..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, { @@ -430,7 +431,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 +630,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(), ""); @@ -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 fe23e9cb..1ff7a641 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,13 @@ 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, "new"); + assert.deepEqual(citing.citation_candidates, [{ id: "citing", reason: "missing_intent" }]); + assert.equal(citing.linkOnly, false); }); test("locator punctuation trim parity table", () => { @@ -51,6 +55,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 => { @@ -83,6 +89,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"]) { diff --git a/test/import-selection.test.ts b/test/import-selection.test.ts index df973101..8f81aa49 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, /--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,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: "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", "--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); }); }));