diff --git a/.agents/pm/features/pm-github-f1at.toon b/.agents/pm/features/pm-github-f1at.toon new file mode 100644 index 00000000..950cc184 --- /dev/null +++ b/.agents/pm/features/pm-github-f1at.toon @@ -0,0 +1,64 @@ +id: pm-github-f1at +title: Fail-closed privacy gate for automated GitHub issue imports and one reusable sync workflow +description: "Automated fleet-wide sync is disabled because the pilot workflow pushes imported issue text to a public branch before any privacy gate inspects the proposed tracker diff. Add a fail-closed scanner over the proposed tracker change, wire it into import as --gate, and ship one reusable workflow_call sync workflow fleet repos can call." +type: Feature +status: in_progress +priority: 1 +tags[5]: "area:ci","area:gates","area:import","area:privacy",changelog-unreleased +created_at: "2026-10-05T06:09:58.912Z" +updated_at: "2026-10-06T17:04:29.518Z" +author: pi-glm +acceptance_criteria: A fresh checkout installs the pinned extension and exercises a nonempty issue import; malicious issue-body credential and personal-data fixtures prevent any public branch push; completeness and idempotency are checked before mutation; a clean import opens a reviewable PR with permanent PM links; existing pilot workflows and future rollouts use one audited implementation or equivalent gate. +blocked_by: pm-github-9cjx +unblock_note: Coverage blocker pm-github-9cjx is closed (exact 100/100/100/100 verified); the feature is complete on PR 108 and waits only for review and merge. +dependencies[2]{id,kind,created_at,author,source_kind,author_source}: + pm-github-9cjx,blocked_by,"2026-10-05T08:43:10.230Z",codex-sol,"cli:update:blocked_by",detected + pm-github-hptv,blocked_by,"2026-10-05T08:43:10.230Z",codex-sol,"cli:update:dep",detected +comments[27]{created_at,author,text}: + "2026-10-05T06:21:37.672Z",pi-glm,"Design settled: new gate.ts holds the pure scanner (credential/personal-data/host-path rules over ADDED lines only, sha256 content-hash allowlist with required written justification, fail-closed on unreadable input/git failure/malformed allowlist); unified-diff parser with toon field attribution + jsonl patch-entry attribution; git collector covers staged+working+untracked under the resolved tracker path and excludes operational locks/extensions/checkpoints subpaths. Registered as 'pm github gate' (--diff, --allowlist) and composed into import via --gate: plan completeness + provenance idempotency verified pre-mutation, tracker-change scan post-write, non-zero exit so a workflow can never push on a finding. Reusable workflow_call sync workflow added with SHA-pinned actions; caller example at docs/sync-workflow-caller.yml." + "2026-10-05T06:33:25.742Z",pi-glm,"Implementation checkpoint: gate.ts (scanner + unified-diff parser with toon/jsonl field attribution + git collector over staged/working/untracked tracker changes, operational locks/extensions/checkpoints excluded + sha256-keyed allowlist with required justification) is registered as 'pm github gate' and composed into import via --gate (plan completeness + provenance idempotency verified pre-mutation, fail-closed scan post-write, non-zero exit). Reusable workflow_call sync workflow with SHA-pinned actions + caller example added. Full package suite green: 347 tests pass, coverage 93.55 lines / 84.05 branches / 93.62 functions (repo thresholds 88/79/89 met), docstring gate 119/119, repo privacy gate clean over 3107 objects. New suites test/gate.test.ts (29) and test/import-gate.test.ts (13) include the adversarial full-sequence replay on a real git repo with a bare remote: token-in-body, token-in-comment, email, host-path, and high-entropy fixtures all fail the gated import before anything can be pushed, and the remote's ref list never gains the sync branch." + "2026-10-05T06:42:46.064Z",codex-sol,"Resumed interrupted run as codex-sol. Preserving implementation commit and README draft. Verification priorities: scanner fail-closed inputs and redaction, full bare-remote sequence, SDK-resolved tracker paths, reusable workflow input safety and existing PR updates, real public issue acceptance, and requested all-source 100 percent coverage. Previous evidence met only the existing lower thresholds; this is not the requested coverage gate." + "2026-10-05T06:44:15.604Z",codex-sol,"Verified inherited code with npm run typecheck, npm run docstring (119 declarations), and node --test test/gate.test.ts test/import-gate.test.ts (42 passed). Preserved README draft, removed host-path examples, and marked rollout references as release placeholders because the current published version predates the gate. Review identified staged-versus-working-tree and same-rule allowlist deduplication bypasses; fixes and regression tests follow." + "2026-10-05T06:58:12.486Z",codex-sol,"Resumed verification found and fixed distinct-content allowlist suppression, staged-content omission, malformed/binary diff acceptance, unreadable optional allowlists, and escaped token/path parsing. Regression suite now passes 47 gate/import tests; lock tests passed in the prior focused run. Workflow now validates same-repository automated branch and exact version inputs through environment variables, imports comments, checks gate availability, verifies the second import has no filesystem changes, and uses a branch-specific force-with-lease. The inherited test commit contained a synthetic PEM header; retained exact-blob provenance documents that false positive without rewriting history." + "2026-10-05T07:00:44.735Z",codex-sol,"Real public acceptance: a fresh scratch pm project installed pinned PM CLI 2026.10.4 and the packed pm-github candidate 2026.10.4. Read-only unbraind/pm-todos import planned 3 issues, imported 3 items, reconciled fetched/planned/skipped as 3/3/0, passed privacy scanning over 6 changed files with zero findings, and completed in 10.84 seconds. A second gated atomic import exited zero and left all tracker item/history bytes identical. No remote or push was used. Final candidate will be rerun with comments and native Bun before reporting completion." + "2026-10-05T07:52:13.195Z",codex-sol,"Verified the actual reusable workflow shell sequence against a real git repository and bare remote: body token, comment token, email, and local-path fixtures prevent push and PR creation; clean imports create a PR and a fresh-checkout repeat updates it safely. Added a post-write provenance check that rejects stale completed SDK journals when persisted items are missing. Focused gate and workflow suite: 53 tests passed; final all-source coverage and native Bun checks are pending." + "2026-10-05T08:03:34.543Z",codex-sol,"Final packed-candidate public acceptance in a fresh scratch PM project: read-only unbraind/pm-todos plan fetched/planned/skipped 3/3/0, first gated atomic import created 3 items, privacy verdict PASS over 6 files with zero findings, Node repeat exited zero with identical item/history bytes, native Bun repeat exited zero with identical bytes, and pinned CLI strict health passed. Runtime 68.69 seconds during concurrent verification; no remote configured or push performed. Separate with-comments acceptance correctly rejected personal email content before persistence. Native Bun focused suite now passes 53 tests with an explicit real-CLI timeout." + "2026-10-05T08:08:23.784Z",codex-sol,"The canonical changelog generator defaults to closed items, while this work must remain open for orchestrator verification. Added a complete SDK read feeding pm-changelog and an explicit changelog-unreleased tag for this candidate only; other open claims stay excluded. The generated Unreleased section now links the feature without fabricating completion. Added regression coverage for staged operational files, nested tracker content, credential-bearing history pointers, and unused REST comment-user metadata." + "2026-10-05T08:12:38.561Z",codex-sol,"Final workflow review found that GitHub runners do not inherit npm's local-bin PATH during standalone shell steps. The dependency step now publishes the checkout's local bin directory through GITHUB_PATH, and executable workflow fixtures prioritize that local CLI. This removes reliance on a globally installed pm executable in fresh-checkout imports. Public candidate receipts remain read-only; fleet callers require a future pinned feature release and audited caller adoption." + "2026-10-05T08:16:55.234Z",codex-sol,"Verified Node and native Bun regressions for gated preview redaction in atomic and non-atomic modes, including imported credential-shaped titles and labels. Corrected launcher acceptance to invoke Node exactly as the package prepare hook declares; a Bun test runner must not silently substitute its own executable for an explicitly Node-based lifecycle hook. Final full suites are being rerun with these corrections." + "2026-10-05T08:20:15.079Z",codex-sol,"Native Bun investigation reproduced a concrete subprocess issue: after changing PATH, spawnSync without an explicit environment still found gh; with an explicit current environment it correctly failed ENOENT. GitHub credential discovery, gh availability checks, and PM subprocesses now pass the current environment explicitly, preserving scoped authentication and CLI selection under Node and Bun. This was verified with credential-presence booleans only; no credential values were logged." + "2026-10-05T08:28:56.085Z",codex-sol,"Frozen packed-candidate public receipt: fetched/planned/skipped 3/3/0, imported 3 items, Node second run exited zero and left item/history bytes identical, privacy PASS over 6 changed files with zero findings, pinned strict health passed, and no remote was configured. The direct native Bun CLI repeat remained active and was terminated; it left tracker bytes identical but has no passing exit/verdict. The entire bounded sequence took 312.01 seconds. This supersedes the earlier native-CLI positive receipt as the current acceptance boundary; native library tests remain separate evidence." + "2026-10-05T08:33:17.927Z",codex-sol,"Expanded the explicit provider signatures to include Slack incoming-webhook credentials and OpenAI service-account keys, with syntactically shaped fake fixtures assembled at runtime. All gate findings still contain only rule, item id, field, and exact-content hash. These additions require a fresh final source-coverage receipt; no threshold or source exemption changed." + "2026-10-05T08:43:11.660Z",codex-sol,"All 375 npm cases and all 375 native Bun cases passed in the full linked runs, but strict coverage failed and the Bun wrapper retained a deliberate failure exit code from operational-script fixture teardown. Corrected teardown to restore the previous numeric exit code, or zero when previously unset; the affected Bun files now exit zero without lowering assertions. The feature remains blocked by exact all-source coverage and the separate installed native CLI acceptance item." + "2026-10-05T08:58:35.322Z",codex-sol,"Final frozen-source checks at 0b7c4ab: npm ran 375 tests with all passing; the full per-file native Bun linked test ran 375 cases and exited zero. Gate/import tests include 57 cases and actual workflow shell execution against bare remotes. Typecheck, strict erasable syntax, ESLint, 123 documented declarations, zero duplication across 12 authored modules, production audit, packing, attested-publish checks, generated changelog, pinned CLI strict health, and Git-object privacy passed. Mandatory 100/100/100/100 coverage remains blocked at 93.40/93.40/84.77/94.94. A latest direct public fetch retry timed out before planning or writing; preserve the earlier three-item Node receipt separately and keep native installed-CLI acceptance blocked." + "2026-10-05T09:03:35.170Z",codex-sol,"Latest packed-candidate real-data acceptance recovered after the transient public API timeout: fresh scratch project with pinned CLI 2026.10.4 and the packed candidate planned/fetched/skipped 3/3/0 public unbraind/pm-todos issues, imported 3, passed privacy over 6 files with zero findings, completed a byte-identical Node repeat, and passed strict health in 12.28 seconds. No remote was configured and no push occurred. This is the final positive Node receipt; native installed-CLI acceptance remains separately bounded and tracked." + "2026-10-05T09:11:07.592Z",codex-sol,"A current quota refresh reports Ollama Cloud, OpenCode Go, and Grok exhausted; Codex remains on pace within the weekly forecast. Started the updated Codex CLI with gpt-6.1-sol at xhigh to continue the existing exact all-source coverage task pm-github-9cjx. Thresholds remain 100 for all four metrics, no source ignores, and no release or rollout is authorized until the gate passes. The latest native installed-CLI repeat also passed in 4.22 seconds without tracker mutation; the earlier intermittent observation remains tracked." + "2026-10-05T09:14:18.425Z",codex-sol,"Additional privacy review reproduced two missed shapes: a credential present only in a proposed tracker filename, and Windows drive paths using forward slashes. Both are now scanned. Filename findings use file_path and sensitive item identifiers are redacted. Real temporary Git regression tests prove a token-bearing filename fails without creating a commit; URL controls remain clean. The combined gate suite passes 40 Node cases." + "2026-10-05T09:16:14.202Z",codex-sol,"The proposed-filename and Windows-separator fixes passed 40 Node gate cases and 40 native Bun gate cases. The newly linked two-case command ran through pm test with progress and exit zero. Strict typecheck, source and test ESLint, 123 documented declarations, and the complete 12-module zero-duplication gate pass. Final broad npm, Bun, coverage, and packed acceptance must be refreshed after the concurrent coverage work settles." + "2026-10-05T09:22:58.633Z",codex-sol,"Further adversarial review reproduced camel-case accessToken/clientSecret assignments, Slack xapp and xoxc/xoxd credentials, and labelled unformatted phone data escaping the rules. The updated rules detect these shapes. Removed the date/UUID exemption because a credential assignment could deliberately include a date or use a UUID key; ordinary noncredential request ids stay clean. The combined gate battery passes 41 cases under Node and native Bun, with URL and machine-comment-id controls." + "2026-10-05T09:31:11.763Z",codex-sol,"The actual reusable workflow shell now rejects nine independent malicious fixtures on real temporary Git repositories with bare remotes: body token, comment token, email, POSIX path, camel-case secret assignment, date-containing secret assignment, Slack app credential, labelled plain phone, and Windows forward-slash path. The linked gate/import suite completed with exit zero; every fixture verified the expected rule, redacted output, no item file, unchanged remote refs, and no PR receipt. Additional encoding review now rejects untracked NUL/UTF-16 content and invalid UTF-8 rather than silently scanning replacement bytes; 42 Node and native Bun gate cases pass." + "2026-10-05T09:55:33.935Z",codex-sol,"Final source ae86e7d passed all 420 npm tests and all 420 native Bun tests with no skips. Exact all-source coverage remains blocked: 8099/8299 lines/statements (97.59 percent), 1859/2083 branches (89.24 percent), and all 200 functions (100 percent) across 12 authored modules; the failing gate removed LCOV. Projects planners reached 100 in all four metrics. Strict typecheck, ESLint, docstrings, zero source duplication, production audit, packing, pm-changelog, release-publish attestation, Git-object privacy, and pinned strict health passed. Latest packed real-data acceptance imported three public pm-todos issues, completed a byte-identical Node repeat, passed privacy over six files with zero findings, and passed strict health in 24.94 seconds; no remote or push. The direct native Bun CLI repeat timed out at 45.06 seconds with unchanged durable bytes, tracked separately in pm-github-hptv. Keep the candidate draft and the coverage/rollout follow-ups open." + "2026-10-06T11:42:32.311Z",codex-sol,"Final continuation retains the audited gate and reusable caller implementation. Added real Git/PM/HTTP behavior coverage, a bounded installed-Bun process-group watchdog and reproducible dangling-lock wait-budget fix. Public all-state snapshot correctly refuses older email findings; the clean recent nonempty snapshot passes import, idempotent repeats and strict health without a remote. Source is now frozen for npm release:check and the full native Bun suite; no downstream sync workflow is enabled or dispatched." + "2026-10-06T11:50:22.874Z",codex-sol,"Full npm run release:check PASS on frozen implementation: all 487 tests pass without skips and all 12 authored modules measure exact 100/100/100/100 (8391 lines/statements, 2218 branches, 208 functions). Typecheck, build, docstrings, lint, zero duplication, Git-object privacy, production dependency audit (zero vulnerabilities), package dry-run, changelog and publish attestation all pass. Pinned PM strict health with required merge drivers passes. Native Bun suite and Bun-invoked full release check remain in progress." + "2026-10-06T12:13:00.910Z",codex-sol,"Full npm and Bun-invoked release:check both PASS on production implementation 5ed8c47: each full run passes 487 tests without failures/skips, exact 100/100/100/100 across all 12 authored modules, and every configured release gate. Counts: 8398 lines/statements, 2219 branches, 208 functions, all covered with zero skipped source. The full native Bun suite is re-running after four test-only mock.method portability corrections; their four focused Node/Bun cases and complete 24-case Node lock suite pass. Packed public acceptance after the transport fix passes nonempty import, gate, unchanged Node/Bun repeats and strict health without a remote." + "2026-10-06T12:42:47.528Z",codex-sol,"Final acceptance complete: npm run release:check and bun run release:check PASS, final npm coverage PASS at exact 100/100/100/100 for all 12 modules with 487 tests/no skips, and complete native npm run test:bun PASS with 487/487 cases across 30 files and exit zero. Fresh packed public snapshot import, unchanged Node/Bun repeats, privacy gate and pinned strict health pass. Nine malicious executable workflow fixtures stop before item writes, pushes or PR writes; clean workflow push and permanent-link PR creation/update pass at the local writer boundary. Existing draft PR https://github.com/unbraind/pm-github/pull/108 will receive the final receipts; no merge, publication or downstream rollout. Leaving all items open and releasing claims for orchestrator verification." +files[11]{path,scope}: + .github/workflows/pm-github-sync.yml,project + docs/sync-workflow-caller.yml,project + gate.ts,project + index.ts,project + scripts/generate-changelog.ts,project + test/gate-paths.test.ts,project + test/gate.test.ts,project + test/generate-changelog.test.ts,project + test/handler-failures.test.ts,project + test/import-gate.test.ts,project + test/prepare-merge-driver.test.ts,project +tests[3]{command,scope,timeout_seconds,provenance{author,created_at,source_kind,source_ref}}: + node --test test/gate.test.ts test/import-gate.test.ts,project,600,codex-sol,"2026-10-05T07:43:22.444Z",local_mutation,feat/import-privacy-gate + "npm run test:bun",project,900,codex-sol,"2026-10-05T08:29:30.824Z",local_mutation,feat/import-privacy-gate + node --test test/gate-paths.test.ts,project,60,codex-sol,"2026-10-05T09:15:00.790Z",local_mutation,feat/import-privacy-gate +docs[2]{path,scope}: + docs/sync-workflow-caller.yml,project + README.md,project +body: "" diff --git a/.agents/pm/history/pm-github-2ao1.jsonl b/.agents/pm/history/pm-github-2ao1.jsonl new file mode 100644 index 00000000..2f0f1327 --- /dev/null +++ b/.agents/pm/history/pm-github-2ao1.jsonl @@ -0,0 +1,5 @@ +{"hash_algorithm":"sha256","ts":"2026-10-06T14:30:31.652Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"f688d14d4e1e36891460ce7a","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-2ao1"},{"op":"add","path":"/metadata/title","value":"PM CLI filtered native Bun tests falsely report an empty run"},{"op":"add","path":"/metadata/description","value":"Pinned PM CLI/SDK 2026.10.4 recognizes the native Bun name filter but its positive execution receipt patterns omit Bun output. A real linked Bun 1.3.14 command executes two tests, reports 2 pass and Ran 2 tests, exits zero, then pm test changes the outcome to empty_run with missing_positive_execution_receipt. Track upstream parser compatibility separately from the package review fixes; do not patch the protected CLI here."},{"op":"add","path":"/metadata/type","value":"Issue"},{"op":"add","path":"/metadata/status","value":"open"},{"op":"add","path":"/metadata/priority","value":2},{"op":"add","path":"/metadata/tags","value":["bun","testing","upstream"]},{"op":"add","path":"/metadata/created_at","value":"2026-10-06T14:30:31.652Z"},{"op":"add","path":"/metadata/updated_at","value":"2026-10-06T14:30:31.652Z"},{"op":"add","path":"/metadata/author","value":"codex-sol"},{"op":"add","path":"/metadata/repro_steps","value":"Link bun test --timeout 600000 -t \"canonical changelog includes|historical fixture provenance reports\" ./test/generate-changelog.test.ts ./test/privacy-gate.test.ts; run the linked test with pm test --run --only-last --progress and compare Bun output with the final run_results row."},{"op":"add","path":"/metadata/expected_result","value":"A positive native Bun execution receipt is accepted, while zero-test filtered Bun runs remain refused."},{"op":"add","path":"/metadata/actual_result","value":"Bun executes and passes two tests with exit zero; pm test reports empty_run after failing to recognize the positive receipt."}],"before_hash":"3cc22dff72be7b14824654a7a64ea62b04799939b2fee54c1b5f52ca60bf6df0","after_hash":"a63e598134ee90fc208b24af4b70ae61cc8925cd30ee77a0cfb220be4f87a85c","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":"ded856e11aa2b142640740d72941f6854a76bbae46dcded6cfdddf28f048defe"} +{"hash_algorithm":"sha256","ts":"2026-10-06T14:32:01.366Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"f688d14d4e1e36891460ce7a","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-06T14:32:01.366Z"},{"op":"add","path":"/metadata/files","value":[{"path":"test/generate-changelog.test.ts","scope":"project"},{"path":"test/privacy-gate.test.ts","scope":"project"}]}],"before_hash":"a63e598134ee90fc208b24af4b70ae61cc8925cd30ee77a0cfb220be4f87a85c","after_hash":"774347b1e14a750b011e2e08b62acd38198ba3bc34785691b354c361fafac6f4","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"a27ddf7305ea0e46dc726a5dfe8050ac1272dfb519cf190d527d15aab8582bf0"} +{"hash_algorithm":"sha256","ts":"2026-10-06T14:32:02.563Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"f688d14d4e1e36891460ce7a","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-06T14:32:02.563Z"},{"op":"add","path":"/metadata/docs","value":[{"path":"README.md","scope":"project"}]}],"before_hash":"774347b1e14a750b011e2e08b62acd38198ba3bc34785691b354c361fafac6f4","after_hash":"48419835e6dabd32a47c30fdd0090e465e135f9bd118e353f1c4c4f892011534","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"ad190a80ae4590c8d543ab9b1cae377da067ac1b18acfc1113e68b3cdb6725f6"} +{"hash_algorithm":"sha256","ts":"2026-10-06T14:32:03.841Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"f688d14d4e1e36891460ce7a","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-06T14:32:03.841Z"},{"op":"add","path":"/metadata/comments","value":[{"created_at":"2026-10-06T14:32:03.841Z","author":"codex-sol","text":"Reproduced through the pinned installed CLI using native Bun 1.3.14: subprocess exit zero, two genuine passing tests and a Ran 2 tests receipt; run_results is rewritten to failed/empty_run with missing_positive_execution_receipt. Read-only SDK inspection confirms the positive receipt patterns accept Node-style tests/pass counters and passed wording, but omit Bun pass-count order and Ran wording. This is an upstream test-runner compatibility follow-up, outside PR round-1 source fixes. No CLI dependency files were changed. The package registers the full native test files instead of the unsupported filtered command."}]}],"before_hash":"48419835e6dabd32a47c30fdd0090e465e135f9bd118e353f1c4c4f892011534","after_hash":"dd4c206896e06204b27c6f10db81d1ea644bfc60f3bd8fb1f32aa035747c136a","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"49649ec8e9822e51053126728f19849c90116852844f2d964c3ba92dc36c1e65"} +{"hash_algorithm":"sha256","ts":"2026-10-06T15:37:56.669Z","author":"claude-orchestrator","author_source":"asserted","agent_harness":"claude-code","agent_model":"claude-opus-5-5","agent_model_source":"probe","agent_instance":"1c3d835edba106c1302b3106","agent_provenance":{"model":{"value":"claude-opus-5-5","source":"probe"},"effort":{"value":"high","source":"environment"},"role":null,"topic":null,"version":{"value":"2.1.291","source":"probe"}},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/1","value":{"created_at":"2026-10-06T15:37:56.669Z","author":"claude-orchestrator","text":"Reproduced by the orchestrator on PM CLI 2026.10.6 + Bun 1.3.5 in a clean tracker: 'bun test --test-name-pattern alpha test/x.test.ts' (2 pass, 1 filtered out, exit 0) is reported status failed, exit_code 1, failure_category empty_run; the unfiltered command passes. Filed upstream: https://github.com/unbraind/pm-cli/issues/1418"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T15:37:56.669Z"}],"before_hash":"dd4c206896e06204b27c6f10db81d1ea644bfc60f3bd8fb1f32aa035747c136a","after_hash":"2c9291043d2ea7325f91f9fc588d43017cdd4bb6554e5a9a9cd0cc181a3b103b","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":"279a26f4fcbb6de6223eba7a79806339601fe144a3f198b22cd4f808600ea4e0"} diff --git a/.agents/pm/history/pm-github-4a03.jsonl b/.agents/pm/history/pm-github-4a03.jsonl new file mode 100644 index 00000000..44629767 --- /dev/null +++ b/.agents/pm/history/pm-github-4a03.jsonl @@ -0,0 +1,2 @@ +{"hash_algorithm":"sha256","ts":"2026-10-05T07:36:59.028Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"0221c1e0bd861e4e03844581","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-4a03"},{"op":"add","path":"/metadata/title","value":"Guard stale completed SDK import journals after local tracker files are reset"},{"op":"add","path":"/metadata/description","value":"A synthetic import into a real Git worktree completes an atomic journal. Resetting the worktree to its pre-import baseline removes the imported files while ignored completed journals persist. A subsequent import can report recovered success without restoring the absent items. The gated importer now verifies durable post-write provenance and refuses any push; follow up with a minimal SDK reproduction and dependency-level recovery repair."},{"op":"add","path":"/metadata/type","value":"Issue"},{"op":"add","path":"/metadata/status","value":"open"},{"op":"add","path":"/metadata/priority","value":1},{"op":"add","path":"/metadata/tags","value":["area:import","area:sdk","type:defect"]},{"op":"add","path":"/metadata/created_at","value":"2026-10-05T07:36:59.028Z"},{"op":"add","path":"/metadata/updated_at","value":"2026-10-05T07:36:59.028Z"},{"op":"add","path":"/metadata/author","value":"codex-sol"}],"before_hash":"3cc22dff72be7b14824654a7a64ea62b04799939b2fee54c1b5f52ca60bf6df0","after_hash":"f2ca4708f9eaa64f7e4cebf68ddd6aff6d0b01706c5c1cff8a50ca81541e91cd","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":"d92330bac09c9a23fb34f301752dc721c6b861e22aa6363c2525edfc1ad27b3e"} +{"hash_algorithm":"sha256","ts":"2026-10-05T07:43:25.513Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"0221c1e0bd861e4e03844581","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-05T07:43:25.513Z"},{"op":"add","path":"/metadata/files","value":[{"path":"index.ts","scope":"project"},{"path":"test/import-gate.test.ts","scope":"project"}]}],"before_hash":"f2ca4708f9eaa64f7e4cebf68ddd6aff6d0b01706c5c1cff8a50ca81541e91cd","after_hash":"e3816454a1fc8898fd26874bf314cc00ad5773a19ccb73ca28fa58c8575bc8b1","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"c5946f717633555adc7f79dd45c3eabfd46c5b1b0139d3e81825f553b8c53217"} diff --git a/.agents/pm/history/pm-github-7d1h.jsonl b/.agents/pm/history/pm-github-7d1h.jsonl index d571c6f3..ba064ee5 100644 --- a/.agents/pm/history/pm-github-7d1h.jsonl +++ b/.agents/pm/history/pm-github-7d1h.jsonl @@ -13,3 +13,4 @@ {"ts":"2026-08-16T02:37:05.630Z","author":"claude-code","author_source":"asserted","agent_harness":"claude-code","agent_instance":"e05f9eb4c601ff7ec7f5cf31","agent_provenance":{"model":null,"effort":{"value":"xhigh","source":"environment"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/2","value":{"created_at":"2026-08-16T02:37:05.630Z","author":"claude-code","text":"Lower-bound mutation, added to the evidence after review: pinning the development CLI to an exact 2026.8.2, one day below the declared floor 2026.8.3, exits 1. This is the case the repository's own values cannot reach, because the pin and the floor are the same version here, so the ordering half of the comparison is never executed by the assertion. It is now driven directly by a test over synthetic pairs, including the lexicographic trap in which 2026.8.7 must not satisfy a floor of 2026.8.15."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-08-16T02:37:05.630Z"}],"before_hash":"40fbf3408c9ebcab5cf1743748eabc726b7d4b6b0e195ac8e61f269c089017bf","after_hash":"46944a81f2d153975f6fced961470f9dd94ec0396a0c7171cd12ddd1252f8502","item_hash_version":2,"context":{"agent_provenance_outcomes":{"model":{"status":"failed","reason":"resolver_failed","resolver":"claude_session_file","rule_version":"v1"},"version":{"status":"failed","reason":"resolver_failed","resolver":"claude_session_file","rule_version":"v1"}}}} {"ts":"2026-08-16T02:37:06.227Z","author":"claude-code","author_source":"asserted","agent_harness":"claude-code","agent_instance":"e05f9eb4c601ff7ec7f5cf31","agent_provenance":{"model":null,"effort":{"value":"xhigh","source":"environment"},"role":null,"topic":null},"op":"note_add","patch":[{"op":"add","path":"/metadata/notes/2","value":{"created_at":"2026-08-16T02:37:06.227Z","author":"claude-code","text":"Correction to the earlier correction note, appended rather than rewritten. That note said this item's title changed twice after closure and that the close reason was corrected. Reading this item's own history stream: it closed at 2026-08-16T01:32:11, 0 update events precede the close and 4 follow it. The post-close updates changed the title and, in the last of them, the resolution field. The close_reason recorded at closure has not been altered. CodeRabbit caught that the note misstated both the field and, on some items, the chronology."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-08-16T02:37:06.227Z"}],"before_hash":"46944a81f2d153975f6fced961470f9dd94ec0396a0c7171cd12ddd1252f8502","after_hash":"f98fcd6828c004e7be9432ce902241dbdaddac689e0a754bdd5913c2ced1d0b5","item_hash_version":2,"context":{"agent_provenance_outcomes":{"model":{"status":"failed","reason":"resolver_failed","resolver":"claude_session_file","rule_version":"v1"},"version":{"status":"failed","reason":"resolver_failed","resolver":"claude_session_file","rule_version":"v1"}}}} {"ts":"2026-08-16T03:00:52.637Z","author":"claude-code","author_source":"asserted","agent_harness":"claude-code","agent_instance":"e05f9eb4c601ff7ec7f5cf31","agent_provenance":{"model":null,"effort":{"value":"xhigh","source":"environment"},"role":null,"topic":null},"op":"note_add","patch":[{"op":"add","path":"/metadata/notes/3","value":{"created_at":"2026-08-16T03:00:52.637Z","author":"claude-code","text":"Third correction, and this one is derived from the patch entries of this item's own history stream rather than written as prose. Both earlier notes were inaccurate about which fields changed and when. The record, read from the stream: closed at 2026-08-16T01:32:11. Update events before the close: 0 (none). Update events after the close: 4 (2026-08-16T01:45:29 changed title; 2026-08-16T01:54:23 changed actual_result, expected_result, resolution, title; 2026-08-16T02:19:32 changed resolution; 2026-08-16T02:31:51 changed actual_result, description, title). Every one of those is an appended event; no prior entry was rewritten. CodeRabbit caught both earlier notes, and the reason both were wrong is the same: a claim about a history stream was composed from recollection of the commands issued instead of being read back out of the stream."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-08-16T03:00:52.637Z"}],"before_hash":"f98fcd6828c004e7be9432ce902241dbdaddac689e0a754bdd5913c2ced1d0b5","after_hash":"3688195888c05804a121acd3a86108a47ec3e8ffddf82bd62fde8b717f6f2687","item_hash_version":2,"context":{"agent_provenance_outcomes":{"model":{"status":"failed","reason":"resolver_failed","resolver":"claude_session_file","rule_version":"v1"},"version":{"status":"failed","reason":"resolver_failed","resolver":"claude_session_file","rule_version":"v1"}}}} +{"hash_algorithm":"sha256","ts":"2026-10-06T15:18:15.655Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fefbe65803605af722707206","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-06T15:18:15.655Z"},{"op":"add","path":"/metadata/release","value":"2026.8.17"}],"before_hash":"3688195888c05804a121acd3a86108a47ec3e8ffddf82bd62fde8b717f6f2687","after_hash":"48a218ffcd9d02dd17e72511da24c301dd012e82bbefb26257868029a935d9db","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"aebe4f968170f85c963b8231e0f6e2a837c0fdcc2c07fd9718e39800e76f8649"} diff --git a/.agents/pm/history/pm-github-9cjx.jsonl b/.agents/pm/history/pm-github-9cjx.jsonl index 89bb3288..ac230517 100644 --- a/.agents/pm/history/pm-github-9cjx.jsonl +++ b/.agents/pm/history/pm-github-9cjx.jsonl @@ -2,3 +2,88 @@ {"ts":"2026-08-17T07:32:38.597Z","author":"codex","author_source":"configured","agent_harness":"codex","agent_instance":"839e601757a3957be5ebc227","agent_provenance":{"model":null,"effort":null,"role":null,"topic":null},"op":"files_add","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-08-17T07:32:38.597Z"},{"op":"add","path":"/metadata/files","value":[{"path":"scripts/coverage-gate.ts","scope":"project"},{"path":"package.json","scope":"project"},{"path":"test","scope":"project"}]}],"before_hash":"a93c9c1d275d8774059e9db2b614ff2d34af31ae22b5ad9cda3ef09a05043dbb","after_hash":"98d7a1141bd8f04501a10b0b2a0621f088f125c743b84fe54ca8a74949d279f5","item_hash_version":2,"message":"Link the gate, manifest, and behavioral test corpus"} {"ts":"2026-08-17T07:32:39.003Z","author":"codex","author_source":"configured","agent_harness":"codex","agent_instance":"839e601757a3957be5ebc227","agent_provenance":{"model":null,"effort":null,"role":null,"topic":null},"op":"tests_add","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-08-17T07:32:39.003Z"},{"op":"add","path":"/metadata/tests","value":[{"command":"npm run coverage","path":"scripts/coverage-gate.ts","scope":"project","timeout_seconds":900,"note":"Current ratcheted gate; this item remains open until it measures all executable sources at exact 100/100/100/100."}]}],"before_hash":"98d7a1141bd8f04501a10b0b2a0621f088f125c743b84fe54ca8a74949d279f5","after_hash":"0fb7d5cba0f9816799672ede4abd24b9b7bdb69422ebf0407e4ffe85693d0c55","item_hash_version":2,"message":"Bind the current measurable gate while preserving the exact target"} {"ts":"2026-08-17T07:35:10.745Z","author":"codex","author_source":"configured","agent_harness":"codex","agent_instance":"839e601757a3957be5ebc227","agent_provenance":{"model":null,"effort":null,"role":{"value":"implementer","source":"argv"},"topic":null},"op":"update","patch":[{"op":"remove","path":"/metadata/parent"},{"op":"replace","path":"/metadata/updated_at","value":"2026-08-17T07:35:10.745Z"}],"before_hash":"0fb7d5cba0f9816799672ede4abd24b9b7bdb69422ebf0407e4ffe85693d0c55","after_hash":"ac4dd71075023f44940370388a1a4a8f703709431bf47bc135ae05849fddbe0b","item_hash_version":2,"message":"Keep the exact-coverage follow-up independent from its completed incremental tranche while retaining the related edge"} +{"hash_algorithm":"sha256","ts":"2026-10-05T07:36:57.722Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"0221c1e0bd861e4e03844581","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-05T07:36:57.722Z"},{"op":"add","path":"/metadata/assignee","value":"codex-sol"},{"op":"add","path":"/metadata/claim_principal","value":"codex-sol"}],"before_hash":"ac4dd71075023f44940370388a1a4a8f703709431bf47bc135ae05849fddbe0b","after_hash":"c27758876c2e89bc89d48c6ed27d67a0a8b775ecc09d24f614a52904647ab9d9","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"3998bbf583415f962a236fae9c48d9ec1b778ef897479ee6bffbe7bcacd71fa6"} +{"hash_algorithm":"sha256","ts":"2026-10-05T07:36:57.811Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"0221c1e0bd861e4e03844581","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-05T07:36:57.811Z"},{"op":"replace","path":"/metadata/status","value":"in_progress"}],"before_hash":"c27758876c2e89bc89d48c6ed27d67a0a8b775ecc09d24f614a52904647ab9d9","after_hash":"c6f22dccc0325d31973b41034ff549b822072f0bd6bc25962e20f7e336ad86f7","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"ef76e9038262057df47be8c7b111c61db463453638bed21f4b0d60f54673b473"} +{"hash_algorithm":"sha256","ts":"2026-10-05T07:43:21.227Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"0221c1e0bd861e4e03844581","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/2/path","value":"scripts/coverage-gate.ts"},{"op":"replace","path":"/metadata/files/0/path","value":"eslint.config.js"},{"op":"add","path":"/metadata/files/3","value":{"path":"scripts/duplication-gate.ts","scope":"project"}},{"op":"add","path":"/metadata/files/4","value":{"path":"test","scope":"project"}},{"op":"add","path":"/metadata/files/5","value":{"path":"test/coverage-gate.test.ts","scope":"project"}},{"op":"add","path":"/metadata/files/6","value":{"path":"test/duplication-gate.test.ts","scope":"project"}},{"op":"add","path":"/metadata/files/7","value":{"path":"test/lint-policy.test.ts","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-05T07:43:21.227Z"}],"before_hash":"c6f22dccc0325d31973b41034ff549b822072f0bd6bc25962e20f7e336ad86f7","after_hash":"4a8481808e86f6110699f74a6ade96fa6fecf77e46c7034d42bb0d8bd45c8f96","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"d94104be3cbe961eabbda4402b28c8f575367dfe860c1ce493f5ddfc89357e9c"} +{"hash_algorithm":"sha256","ts":"2026-10-05T07:43:24.148Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"0221c1e0bd861e4e03844581","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"tests_add","patch":[{"op":"add","path":"/metadata/tests/1","value":{"command":"npm run coverage","scope":"project","timeout_seconds":600,"provenance":{"author":"codex-sol","created_at":"2026-10-05T07:43:24.083Z","source_kind":"local_mutation","source_ref":"feat/import-privacy-gate"}}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-05T07:43:24.148Z"}],"before_hash":"4a8481808e86f6110699f74a6ade96fa6fecf77e46c7034d42bb0d8bd45c8f96","after_hash":"bd5da81411fc81fbc52487fb7b72492ddf295b3b7c91f0d6d66dc5da76e3a213","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"7f7b16650d01b32429b7ac1f51b9f260a8cba6471ff0cdf0502695fbc1aa56d4"} +{"hash_algorithm":"sha256","ts":"2026-10-05T07:52:16.974Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"0221c1e0bd861e4e03844581","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-05T07:52:16.974Z"},{"op":"add","path":"/metadata/comments","value":[{"created_at":"2026-10-05T07:52:16.974Z","author":"codex-sol","text":"Replaced the narrowed coverage wrapper with an explicit authored-module inventory, exact 100 percent thresholds for all four dimensions, stale-LCOV invalidation, and missing-file rejection. Added checked operational-script tests, ESLint enforcement, erasable TypeScript syntax, and a jscpd wrapper that verifies its full source inventory. Typecheck, 122 documented declarations, ESLint, and zero duplication passed. Coverage remains a measured gate rather than an approval claim."}]}],"before_hash":"bd5da81411fc81fbc52487fb7b72492ddf295b3b7c91f0d6d66dc5da76e3a213","after_hash":"bd77526b583a2e124c4466b6dde6fc125fdfbb1aa2b64aec5c2b6cdb6b220fd2","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"a8d09331ba278c4bd881b2b28d37f89a86fd48c4f3d30bbade803d30dd5461cd"} +{"hash_algorithm":"sha256","ts":"2026-10-05T08:09:09.409Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"0221c1e0bd861e4e03844581","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"tests_remove","patch":[{"op":"remove","path":"/metadata/tests/1"},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-05T08:09:09.409Z"}],"before_hash":"bd77526b583a2e124c4466b6dde6fc125fdfbb1aa2b64aec5c2b6cdb6b220fd2","after_hash":"c24848e45816d354fa17bb81efe76bb7acf860f060f977aeeb7aef14fecd8404","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"3719fdfced73aa21817c629ecd2bcc0ee88bbd4bd99c5d51d12df515b41888ca"} +{"hash_algorithm":"sha256","ts":"2026-10-05T08:09:10.457Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"0221c1e0bd861e4e03844581","agent_provenance":{"model":{"value":"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-05T08:09:10.457Z","author":"codex-sol","text":"Strict all-authored-module run completed with 369 tests passing and no skipped tests. Coverage remained below the unchanged 100/100/100/100 requirement: lines/statements 93.25 percent, branches 84.41 percent, functions 94.92 percent over 11 authored TS/JS modules including operational scripts. LCOV was invalidated on failure. Most remaining uncovered branches and ten uncovered functions are in the pre-existing GitHub Projects sync handlers; this is a release blocker, not a passing gate. The shell release-date verifier remains outside the V8 denominator and is exercised separately."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-05T08:09:10.457Z"}],"before_hash":"c24848e45816d354fa17bb81efe76bb7acf860f060f977aeeb7aef14fecd8404","after_hash":"99f2f700eb41d34cca150361ec2533c06847033712a83eb01de9fc15b66ba81e","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"fe1e9f0355c5ad1d6e364dce62c2e3e4ebe8d61706264321503cf52798768860"} +{"hash_algorithm":"sha256","ts":"2026-10-05T08:58:34.554Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"0221c1e0bd861e4e03844581","agent_provenance":{"model":{"value":"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-05T08:58:34.554Z","author":"codex-sol","text":"Final frozen-source verification at 0b7c4ab: the linked npm coverage command ran all 375 tests with 375 passing, zero failures and zero skips, then correctly exited nonzero at the unchanged 100 percent gate. Complete inventory: 12 authored TS/JS modules including operational scripts. Lines/statements 7739 of 8285 = 93.40 percent; branches 1548 of 1826 = 84.77 percent; functions 188 of 198 = 94.94 percent. Failed LCOV was removed. No source ignores or lowered thresholds were introduced; release remains blocked."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-05T08:58:34.554Z"}],"before_hash":"99f2f700eb41d34cca150361ec2533c06847033712a83eb01de9fc15b66ba81e","after_hash":"04a72df798cc7a5bbb2061fc1826059b49899392d74eeb1a132b71bffa358ca7","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"a75c03c12276e1694edcaa770d59254bc8b1587e0a749aae7616498cf1875452"} +{"hash_algorithm":"sha256","ts":"2026-10-05T09:08:56.246Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"0221c1e0bd861e4e03844581","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-05T09:08:56.246Z"}],"before_hash":"04a72df798cc7a5bbb2061fc1826059b49899392d74eeb1a132b71bffa358ca7","after_hash":"bfa0a48601982a501827c77d283e7d5748f979ae93b8a6a6c8bab4bea608bf1b","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"22ced199294987201423fef79c087926b66a2fe3632408a64cf5feebb934c458"} +{"hash_algorithm":"sha256","ts":"2026-10-05T09:09:50.717Z","author":"codex-sol-coverage","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"536eb26a0b128152300f6d1e","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"xhigh","source":"probe"},"role":{"value":"implementer","source":"argv"},"topic":null},"op":"claim","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-10-05T09:09:50.717Z"},{"op":"add","path":"/metadata/assignee","value":"codex-sol-coverage"},{"op":"add","path":"/metadata/claim_principal","value":"codex-sol-coverage"}],"before_hash":"bfa0a48601982a501827c77d283e7d5748f979ae93b8a6a6c8bab4bea608bf1b","after_hash":"8753f4e0ec7113cf89c8c5404fa88a5aabcc4b9ae1458a803f7f53b0a1d3eb79","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"fee7ba53715b6749347d97c2b04f827fd90941e8a2037a25af7115a586f15a29"} +{"hash_algorithm":"sha256","ts":"2026-10-05T09:09:52.780Z","author":"codex-sol-coverage","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"536eb26a0b128152300f6d1e","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"xhigh","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/3","value":{"created_at":"2026-10-05T09:09:52.779Z","author":"codex-sol-coverage","text":"Resuming the existing strict 12-module coverage implementation. Iteration will use targeted Node and c8 runs, real scratch PM and git projects, and SDK extension harness behavior tests. The complete npm coverage gate is reserved for the orchestrator. Existing unrelated tracker edits are preserved. First priority is Projects sync and apply behavior, then remaining planner and operational failure boundaries."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-05T09:09:52.780Z"}],"before_hash":"8753f4e0ec7113cf89c8c5404fa88a5aabcc4b9ae1458a803f7f53b0a1d3eb79","after_hash":"03c06ca3442a88e1cddb33160f573d88011b21ab5c7f4f29a0687bc9055fbada","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"408677ca992f51adcf898c708d487deedcab9ab4e42823f395575ed2a147c16f"} +{"hash_algorithm":"sha256","ts":"2026-10-05T09:18:20.026Z","author":"codex-sol-coverage","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"536eb26a0b128152300f6d1e","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"xhigh","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/4","value":{"created_at":"2026-10-05T09:18:20.026Z","author":"codex-sol-coverage","text":"Added 18 behavior cases for Projects sync and planners. Targeted Node c8 execution passed 51 of 51 tests, zero skips. Verified preview-only and scoped execution, dry-run override, both conflict preferences, draft and issue mutations, lifecycle close and cancellation metadata, partial and total failures, and a real concurrent local deletion after remote attachment. PM tag ordering is normalized, so the preservation assertion checks membership. No production behavior, dependencies, thresholds, or source inventory changed."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-05T09:18:20.026Z"}],"before_hash":"03c06ca3442a88e1cddb33160f573d88011b21ab5c7f4f29a0687bc9055fbada","after_hash":"f86b45cee2d2d5e1ea3a54aa24e26687f2c3fbe22783404d42df121134c8846c","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"2aeb3bfec8dd56be0a700fbf47b01d7b445d4bfc476d2fa1244a22e2693200b7"} +{"hash_algorithm":"sha256","ts":"2026-10-05T09:18:34.746Z","author":"codex-sol-coverage","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"536eb26a0b128152300f6d1e","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"xhigh","source":"probe"},"role":null,"topic":null},"op":"files_add","patch":[{"op":"add","path":"/metadata/files/8","value":{"path":"test/project-sync.test.ts","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-05T09:18:34.746Z"}],"before_hash":"f86b45cee2d2d5e1ea3a54aa24e26687f2c3fbe22783404d42df121134c8846c","after_hash":"1ec68b7f09ea7c3a03a1be37559b3cb4b07092f7bab525a0971612b0a3ff7236","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"24fce93fafad6b6d349519f5efb0918c5a972451988475c85c62d4ed4bdab4da"} +{"hash_algorithm":"sha256","ts":"2026-10-05T09:18:35.470Z","author":"codex-sol-coverage","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"536eb26a0b128152300f6d1e","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"xhigh","source":"probe"},"role":null,"topic":null},"op":"files_add","patch":[{"op":"add","path":"/metadata/files/9","value":{"path":"test/projects.test.ts","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-05T09:18:35.470Z"}],"before_hash":"1ec68b7f09ea7c3a03a1be37559b3cb4b07092f7bab525a0971612b0a3ff7236","after_hash":"167d9c8e07e4be3a91bff7c2d11400e59debde776010f64625b767a208e5e1af","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"c5611ee6c153783241a7f58ad8e054cd970bb01c670b2ef94a3282955ac024a4"} +{"hash_algorithm":"sha256","ts":"2026-10-05T09:18:36.268Z","author":"codex-sol-coverage","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"536eb26a0b128152300f6d1e","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"xhigh","source":"probe"},"role":null,"topic":null},"op":"tests_add","patch":[{"op":"add","path":"/metadata/tests/1","value":{"command":"node --test test/project-sync.test.ts test/projects.test.ts","scope":"project","timeout_seconds":600,"provenance":{"author":"codex-sol-coverage","created_at":"2026-10-05T09:18:36.238Z","source_kind":"local_mutation","source_ref":"feat/import-privacy-gate"}}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-05T09:18:36.268Z"}],"before_hash":"167d9c8e07e4be3a91bff7c2d11400e59debde776010f64625b767a208e5e1af","after_hash":"cc928e7902f895328a3cf9ef684a220beeade5ee5feb78f7a2bd07fbb9cf313c","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"6907d7a29becadbc43567ef7955affdbd74a2ac4f57a15e6b328ba7d4b9aceee"} +{"hash_algorithm":"sha256","ts":"2026-10-05T09:26:40.028Z","author":"codex-sol-coverage","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"536eb26a0b128152300f6d1e","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"xhigh","source":"probe"},"role":null,"topic":null},"op":"files_add","patch":[{"op":"replace","path":"/metadata/files/9/path","value":"test/project-sync.test.ts"},{"op":"replace","path":"/metadata/files/8/path","value":"test/lint-policy.test.ts"},{"op":"replace","path":"/metadata/files/7/path","value":"test/helpers/node-scenario.ts"},{"op":"add","path":"/metadata/files/10","value":{"path":"test/projects.test.ts","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-05T09:26:40.028Z"}],"before_hash":"cc928e7902f895328a3cf9ef684a220beeade5ee5feb78f7a2bd07fbb9cf313c","after_hash":"2c4e557a4cc524cf2ea1db6e7d50cd00a3d532e57cf4b8f99fba79e408e94a2a","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"72ce04560694ced439d8f102772e2af49842b499aab0b4c10520cf0df92969dd"} +{"hash_algorithm":"sha256","ts":"2026-10-05T09:26:42.191Z","author":"codex-sol-coverage","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"536eb26a0b128152300f6d1e","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"xhigh","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/5","value":{"created_at":"2026-10-05T09:26:42.190Z","author":"codex-sol-coverage","text":"Native Bun verification passed all 12 Projects harness cases and all 39 planner cases, with per-file timeout 600000. Executable lookup failure fixtures now run in explicit Node subprocesses while registered harness behavior stays native. This follows the existing lifecycle test boundary and prevents temporary PATH changes from reaching the host credential helper. Linked Node tests passed 51 of 51 through pm test; test-result tracking is disabled, so this comment records the receipt."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-05T09:26:42.191Z"}],"before_hash":"2c4e557a4cc524cf2ea1db6e7d50cd00a3d532e57cf4b8f99fba79e408e94a2a","after_hash":"f4937b4b682e42bf2ac080a4ee8a7246d792d01c5eb99a10cfaf7a1fd7c3926b","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"37c412e620c11a288d7c08dd82f53717db25be51dbb37dc884782370afd9a698"} +{"hash_algorithm":"sha256","ts":"2026-10-05T09:29:19.305Z","author":"codex-sol-coverage","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"536eb26a0b128152300f6d1e","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"xhigh","source":"probe"},"role":null,"topic":null},"op":"files_add","patch":[{"op":"replace","path":"/metadata/files/10/path","value":"test/project-sync.test.ts"},{"op":"replace","path":"/metadata/files/9/path","value":"test/lint-policy.test.ts"},{"op":"replace","path":"/metadata/files/8/path","value":"test/helpers/node-scenario.ts"},{"op":"replace","path":"/metadata/files/7/path","value":"test/duplication-gate.test.ts"},{"op":"replace","path":"/metadata/files/6/path","value":"test/coverage-gate.test.ts"},{"op":"replace","path":"/metadata/files/5/path","value":"test"},{"op":"replace","path":"/metadata/files/4/path","value":"scripts/duplication-gate.ts"},{"op":"replace","path":"/metadata/files/3/path","value":"scripts/coverage-gate.ts"},{"op":"replace","path":"/metadata/files/2/path","value":"projects.ts"},{"op":"add","path":"/metadata/files/11","value":{"path":"test/projects.test.ts","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-05T09:29:19.305Z"}],"before_hash":"f4937b4b682e42bf2ac080a4ee8a7246d792d01c5eb99a10cfaf7a1fd7c3926b","after_hash":"0043a3ba4098a06db9652b5bc49a8bd8186ab44ee0babf8176155209f19a9d1f","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"fd912acc129b1906678da9f1bc09c176eee6185e37dfd9f7a8b083b720e52764"} +{"hash_algorithm":"sha256","ts":"2026-10-05T09:29:20.437Z","author":"codex-sol-coverage","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"536eb26a0b128152300f6d1e","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"xhigh","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/6","value":{"created_at":"2026-10-05T09:29:20.437Z","author":"codex-sol-coverage","text":"The Projects planner module now measures exact 100/100/100/100 in the targeted c8 run: 732 of 732 lines and statements, 18 of 18 functions, 181 of 181 branches, with 39 Node tests passing. Removed only the impossible parse-exception catch after validating even-length hex; Buffer accepts that validated input without a parsing exception. Malformed encodings still reject before decoding, and uppercase hex preserves the original case-sensitive id. Full package evidence remains pending the orchestrator run; saved baseline receipts cannot be treated as exact-current after scanner changes by the parallel review work."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-05T09:29:20.437Z"}],"before_hash":"0043a3ba4098a06db9652b5bc49a8bd8186ab44ee0babf8176155209f19a9d1f","after_hash":"87b78955a4c90d66e1143f531793bd008005490c0ea5b8f49e4078233ca9142b","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"ca79526a3e8e0ad89659309fba2d91ecaa286704093744c79d400bf81042615d"} +{"hash_algorithm":"sha256","ts":"2026-10-05T09:29:21.491Z","author":"codex-sol-coverage","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"536eb26a0b128152300f6d1e","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"xhigh","source":"probe"},"role":null,"topic":null},"op":"tests_add","patch":[{"op":"add","path":"/metadata/tests/2","value":{"command":"node --test test/projects.test.ts","scope":"project","timeout_seconds":60,"provenance":{"author":"codex-sol-coverage","created_at":"2026-10-05T09:29:21.443Z","source_kind":"local_mutation","source_ref":"feat/import-privacy-gate"}}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-05T09:29:21.491Z"}],"before_hash":"87b78955a4c90d66e1143f531793bd008005490c0ea5b8f49e4078233ca9142b","after_hash":"ae845aaf21045d4db69f7ea2b6f2c7923da504f8f01e78ccb1d418d8239bd4e9","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"3d77b7633cd5a5cacfce7398697e154747018148bd5941f658f5460502d0e2d6"} +{"hash_algorithm":"sha256","ts":"2026-10-05T09:31:52.268Z","author":"codex-sol-coverage","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"536eb26a0b128152300f6d1e","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"xhigh","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/7","value":{"created_at":"2026-10-05T09:31:52.268Z","author":"codex-sol-coverage","text":"Counter correction for the latest Projects-only run: all 182 of 182 measured branches are covered; the prior comment used the earlier branch total of 181. Lines and statements remain 732 of 732, functions 18 of 18. The linked 39-test Node planner suite passed. Added eight additional real filesystem/process boundary cases; targeted Node and native Bun both pass 8 of 8."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-05T09:31:52.268Z"}],"before_hash":"ae845aaf21045d4db69f7ea2b6f2c7923da504f8f01e78ccb1d418d8239bd4e9","after_hash":"c0f85ca1cc4b1ef4b9ebed5639c3668813b820c3194940e2c7b1168cee3aa860","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"38654780079e90fd11908ea66e06b46aa4a1b7c40ad451841d85d75aa9f83564"} +{"hash_algorithm":"sha256","ts":"2026-10-05T09:34:50.914Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"0221c1e0bd861e4e03844581","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-05T09:34:50.914Z"}],"before_hash":"c0f85ca1cc4b1ef4b9ebed5639c3668813b820c3194940e2c7b1168cee3aa860","after_hash":"b84af3e5a7db15f178d823f9c7e4916f378478b2c7c2c344214cdeddf0910d0b","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"75ce78289443117b65c830baa815c3d826d1a722cae71b3d1e7d733f94fd48bd"} +{"hash_algorithm":"sha256","ts":"2026-10-05T09:34:51.679Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"0221c1e0bd861e4e03844581","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-05T09:34:51.679Z"},{"op":"add","path":"/metadata/assignee","value":"codex-sol"},{"op":"add","path":"/metadata/claim_principal","value":"codex-sol"}],"before_hash":"b84af3e5a7db15f178d823f9c7e4916f378478b2c7c2c344214cdeddf0910d0b","after_hash":"f42eda772fd3401072a74664495c087fbd8585123a3cd6f0a756c4e18ddcef32","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"e6b3b2f962525fc235852ddd93c4de7e0c01a0100f811614d47f40b1502338e9"} +{"hash_algorithm":"sha256","ts":"2026-10-05T09:34:52.515Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"0221c1e0bd861e4e03844581","agent_provenance":{"model":{"value":"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-05T09:34:52.514Z","author":"codex-sol","text":"Stopped the Codex CLI coverage sub-agent immediately after a fresh quota check reported OVER_FORECAST while the other providers remained exhausted, honoring the owner weekly forecast limit. Preserved its focused commits and remaining test files for orchestrator verification. Projects planners reached exact 100 in targeted c8; 12 Projects harness cases, 8 operational boundary cases, and 9 import error cases passed Node and native Bun. Package-wide coverage still requires a fresh frozen-source run and must not be presented as 100. No thresholds, source inventory, or ignores were weakened."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-05T09:34:52.515Z"}],"before_hash":"f42eda772fd3401072a74664495c087fbd8585123a3cd6f0a756c4e18ddcef32","after_hash":"1bd37d2979c231e107a354baa9e90d4cd400c6a60b583ec4d2c37f4aa0c40d9b","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"4027b9e943df0c9f7dff4f83898cf5ee056b9bf202c86b5383d86402688bea1f"} +{"hash_algorithm":"sha256","ts":"2026-10-05T09:36:25.593Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"0221c1e0bd861e4e03844581","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/11/path","value":"test/project-sync.test.ts"},{"op":"replace","path":"/metadata/files/10/path","value":"test/lint-policy.test.ts"},{"op":"replace","path":"/metadata/files/9/path","value":"test/helpers/node-scenario.ts"},{"op":"replace","path":"/metadata/files/8/path","value":"test/duplication-gate.test.ts"},{"op":"replace","path":"/metadata/files/7/path","value":"test/coverage-gate.test.ts"},{"op":"replace","path":"/metadata/files/6/path","value":"test/coverage-boundaries.test.ts"},{"op":"add","path":"/metadata/files/12","value":{"path":"test/projects.test.ts","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-05T09:36:25.593Z"}],"before_hash":"1bd37d2979c231e107a354baa9e90d4cd400c6a60b583ec4d2c37f4aa0c40d9b","after_hash":"124147a39a53846008e8ea267d9f98e468ba11d9dc8b2f1aed26bc309024bc84","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"73881d1bac632272b2994b4f84b9380868f780689476432e7597bd5eb05bafca"} +{"hash_algorithm":"sha256","ts":"2026-10-05T09:36:26.601Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"0221c1e0bd861e4e03844581","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/12/path","value":"test/project-sync.test.ts"},{"op":"replace","path":"/metadata/files/11/path","value":"test/lint-policy.test.ts"},{"op":"replace","path":"/metadata/files/10/path","value":"test/import-errors.test.ts"},{"op":"add","path":"/metadata/files/13","value":{"path":"test/projects.test.ts","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-05T09:36:26.601Z"}],"before_hash":"124147a39a53846008e8ea267d9f98e468ba11d9dc8b2f1aed26bc309024bc84","after_hash":"dd6ccfc3a9c7811068e5442f7df0c5d76be8502eb43b62791789f17919d37f9e","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"9bf3d62192af0c3dd31b1cdf461e26282ce0af4ed0ef0c79a9249f4cb3c40752"} +{"hash_algorithm":"sha256","ts":"2026-10-05T09:36:27.431Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"0221c1e0bd861e4e03844581","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/13/path","value":"test/project-sync.test.ts"},{"op":"replace","path":"/metadata/files/12/path","value":"test/lint-policy.test.ts"},{"op":"replace","path":"/metadata/files/11/path","value":"test/import-errors.test.ts"},{"op":"replace","path":"/metadata/files/10/path","value":"test/helpers/node-scenario.ts"},{"op":"replace","path":"/metadata/files/9/path","value":"test/gate-inputs.test.ts"},{"op":"add","path":"/metadata/files/14","value":{"path":"test/projects.test.ts","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-05T09:36:27.431Z"}],"before_hash":"dd6ccfc3a9c7811068e5442f7df0c5d76be8502eb43b62791789f17919d37f9e","after_hash":"b1856c136d81e45cde5e1d11fdcf33167d640c6062584bb1bfc14512ed409949","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"9579384939d34f388eca18717c079a1904e65e05151c5f06f4f4498afcdf1614"} +{"hash_algorithm":"sha256","ts":"2026-10-05T09:38:02.150Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"0221c1e0bd861e4e03844581","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/coverage-boundaries.test.ts test/import-errors.test.ts test/gate-inputs.test.ts","scope":"project","timeout_seconds":300,"provenance":{"author":"codex-sol","created_at":"2026-10-05T09:38:01.943Z","source_kind":"local_mutation","source_ref":"feat/import-privacy-gate"}}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-05T09:38:02.150Z"}],"before_hash":"b1856c136d81e45cde5e1d11fdcf33167d640c6062584bb1bfc14512ed409949","after_hash":"4c9809281ffc573172a3137cca5043179fb372d16183842bc7284b366cb408f0","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"a780ca07c9534b14fc16ffcbfef07338243d47c7a8456b367474de3e825b9c67"} +{"hash_algorithm":"sha256","ts":"2026-10-05T09:41:56.169Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"0221c1e0bd861e4e03844581","agent_provenance":{"model":{"value":"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-05T09:41:56.169Z","author":"codex-sol","text":"Orchestrator verification preserved the interrupted new coverage tests and corrected their fixture setup: explicit repo-local tracker initialization, filename-derived item id expectations, and removal of the linked host package-root environment for the missing-metadata Windows case. The preserved Projects and planner cases pass Node; all 74 preserved cases pass native Bun. The newly linked boundary/import/diff command is being reverified through the PM runner before final broad checks. These are test-fixture corrections; production rejection behavior and the exact gate remain unchanged."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-05T09:41:56.169Z"}],"before_hash":"4c9809281ffc573172a3137cca5043179fb372d16183842bc7284b366cb408f0","after_hash":"3b2b63dfb6bace7ee8a0148f074b3c7ff7af35d4ee0ab995c58a56fb7e3f130c","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"8d566493b8e8c99689b751d3eecbb7032945f42835029ed767c8088c4e8ae3df"} +{"hash_algorithm":"sha256","ts":"2026-10-05T09:45:17.622Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"0221c1e0bd861e4e03844581","agent_provenance":{"model":{"value":"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-05T09:45:17.622Z","author":"codex-sol","text":"The corrected linked boundary/import/diff command passes all 23 Node cases through pm test with progress and exit zero. The preserved Projects/planner suites also passed their native behavior checks; the complete 74-case preserved set passed Bun before the environment-isolation correction. Final frozen-source full npm coverage and native Bun checks follow. All useful interrupted files are retained and linked to this item."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-05T09:45:17.622Z"}],"before_hash":"3b2b63dfb6bace7ee8a0148f074b3c7ff7af35d4ee0ab995c58a56fb7e3f130c","after_hash":"fc3a3106b78f496cdf1d171d68f7e3b8f2aeef179264fbc5b0df92f84ba3a17a","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"77a69463e72bf085554b1a7ce04edf4a3ddb9fa07115b2e1083759f4b80410d6"} +{"hash_algorithm":"sha256","ts":"2026-10-05T09:55:32.334Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"0221c1e0bd861e4e03844581","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/actual_result","value":"Frozen source ae86e7d: all 420 npm tests pass; lines and statements 8099/8299 (97.59 percent), branches 1859/2083 (89.24 percent), functions 200/200 (100 percent). Exact 100 gate fails and invalidates LCOV. All 420 native Bun tests pass separately."},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-05T09:55:32.334Z"},{"op":"replace","path":"/metadata/status","value":"blocked"},{"op":"replace","path":"/metadata/description","value":"The gate now discovers all 12 authored TypeScript and JavaScript modules, including operational scripts, with exact 100 percent thresholds and no source ignores. Projects planners reach exact 100 in all metrics and every authored function is covered. Aggregate line and branch gaps remain in index.ts, gate.ts, and operational failure boundaries; continue meaningful behavior tests without lowering the gate."},{"op":"add","path":"/metadata/blocked_reason","value":"Exact 100 percent coverage is unfinished; continuation stopped at the owner resource limit."}],"before_hash":"fc3a3106b78f496cdf1d171d68f7e3b8f2aeef179264fbc5b0df92f84ba3a17a","after_hash":"1ab9030fe791ec7d8d1216dc982f7100d3db83e83aa0a52fa12405583b475f49","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"e3899996df20d1ed966e614111aa6d2388b3c1964f5de2f8d0c4f2f43b9cd712"} +{"hash_algorithm":"sha256","ts":"2026-10-05T09:57:29.647Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"0221c1e0bd861e4e03844581","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-05T09:57:29.647Z"}],"before_hash":"1ab9030fe791ec7d8d1216dc982f7100d3db83e83aa0a52fa12405583b475f49","after_hash":"c24fe0c508dc894e30a51ae1e85eafd446aaf16b29218a5785cf7aa8fa909db5","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"3bbe4056c2611f708b6b3b8092e3a8cb961aeb29b847e33545242f2b1d13a60d"} +{"hash_algorithm":"sha256","ts":"2026-10-06T10:08:32.256Z","author":"pi-glm","author_source":"asserted","agent_harness":"pi","agent_model":"glm-5.3:cloud","agent_model_source":"environment","agent_instance":"42a57847eb1811c7ac8ce340","agent_provenance":{"model":{"value":"glm-5.3:cloud","source":"environment"},"effort":null,"role":{"value":"implementer","source":"argv"},"topic":null},"op":"claim","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T10:08:32.256Z"},{"op":"add","path":"/metadata/assignee","value":"pi-glm"},{"op":"add","path":"/metadata/claim_principal","value":"pi-glm"}],"before_hash":"c24fe0c508dc894e30a51ae1e85eafd446aaf16b29218a5785cf7aa8fa909db5","after_hash":"dfcbc6d441f2f573f3dc32fac26ad301bcf6f88f82c151c3210099e87eda939f","item_hash_version":3,"event_class":"substantive","record_hash_version":1,"record_hash":"c6f4deb9e2490275c89bde9065cd8e752ccfd6f42cdc45687c78e8e462a9510a"} +{"hash_algorithm":"sha256","ts":"2026-10-06T10:08:33.058Z","author":"pi-glm","author_source":"asserted","agent_harness":"pi","agent_model":"glm-5.3:cloud","agent_model_source":"environment","agent_instance":"42a57847eb1811c7ac8ce340","agent_provenance":{"model":{"value":"glm-5.3:cloud","source":"environment"},"effort":null,"role":{"value":"implementer","source":"argv"},"topic":null},"op":"update","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T10:08:33.058Z"},{"op":"replace","path":"/metadata/status","value":"in_progress"}],"before_hash":"dfcbc6d441f2f573f3dc32fac26ad301bcf6f88f82c151c3210099e87eda939f","after_hash":"926c1c8cf78f042fce01154313e7d341c1f16d9ac3994ac35ab94e4737c3097d","item_hash_version":3,"event_class":"substantive","record_hash_version":1,"record_hash":"338b42b145b2356cb7421e4ed2a4ce79a63a1923eab5596fdfca37087536a371"} +{"hash_algorithm":"sha256","ts":"2026-10-06T11:12:59.215Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"9aa62e730769acb6625de04f","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/claim_principal","value":"codex-sol"},{"op":"replace","path":"/metadata/assignee","value":"codex-sol"},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T11:12:59.215Z"}],"before_hash":"926c1c8cf78f042fce01154313e7d341c1f16d9ac3994ac35ab94e4737c3097d","after_hash":"183cb1b2f9d8e04e37db995a3c02024055749c0f13559ec39bd22cded9b7f029","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"a4f72efa61c990230ad03a9bb1d005c54eb8d24a3a85fdebcfa82d70269554ee"} +{"hash_algorithm":"sha256","ts":"2026-10-06T11:13:02.345Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"9aa62e730769acb6625de04f","agent_provenance":{"model":{"value":"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-06T11:13:02.345Z","author":"codex-sol","text":"Continuing the interrupted work on the same branch. Reviewing every preserved production change against its invariants, measuring all-source coverage, and adding real Git, filesystem, HTTP, and SDK behavior tests. Exact thresholds stay at 100 in all four dimensions; no ignore pragmas or inventory exclusions. PR 108 already exists and will receive the final measured receipts."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T11:13:02.345Z"}],"before_hash":"183cb1b2f9d8e04e37db995a3c02024055749c0f13559ec39bd22cded9b7f029","after_hash":"f69649f24ce3c89da781699d15313fbd0c5085b4f39be6013b7f9f6701878305","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"c7425857428a4776dcb84cfc028aa0463901bd19caff5f65c10251d69d665c17"} +{"hash_algorithm":"sha256","ts":"2026-10-06T11:17:43.296Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"9aa62e730769acb6625de04f","agent_provenance":{"model":{"value":"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-06T11:17:43.296Z","author":"codex-sol","text":"Reviewed the preserved simplifications. Proofs for removed impossible arms: phone regexes guarantee digits; a context-line branch guarantees its leading space; successful encoded spawnSync gives string stdout/stderr while startup failures populate error; fixed valid spawn arguments report missing tools via error/status rather than throwing; nullish SDK fallbacks execute only for a missing injection; fetchJSON resolves only for 2xx; split on a string always returns at least one element; SDK command/import/preflight/search contexts require their arguments and options. Scanner operations on decoded plain strings and arrays have only the explicit malformed-history GateInputError. Restored the formatGateReport empty-field fallback because its exported typed input can contain an empty string. Fixing fixture setup and replacing an always-true assertion; no thresholds or ignores changed."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T11:17:43.296Z"}],"before_hash":"f69649f24ce3c89da781699d15313fbd0c5085b4f39be6013b7f9f6701878305","after_hash":"b98ccfa839307716c2d544fe5ed018f0a8d1411d3791bb881a16cb2458ca059a","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"30bef2442ac7f9496b19db8832abfe2d3bbafd3af6a9e2c6886e77c7946f5512"} +{"hash_algorithm":"sha256","ts":"2026-10-06T11:23:00.689Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"9aa62e730769acb6625de04f","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"docs_add","patch":[{"op":"add","path":"/metadata/docs/2","value":{"path":"docs/coverage-gap-inventory.md","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T11:23:00.689Z"}],"before_hash":"b98ccfa839307716c2d544fe5ed018f0a8d1411d3791bb881a16cb2458ca059a","after_hash":"f9cfd128fd851fdad7d4c955aacc9685307b8811a917a03a937f9e2dd6fa8234","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"81f6786d4c0e91bb2ebeeea061fd04c179293b5de30b4ab38088375fb17e05bf"} +{"hash_algorithm":"sha256","ts":"2026-10-06T11:23:54.547Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"9aa62e730769acb6625de04f","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/14/path","value":"test/project-sync.test.ts"},{"op":"replace","path":"/metadata/files/13/path","value":"test/lint-policy.test.ts"},{"op":"replace","path":"/metadata/files/12/path","value":"test/import-errors.test.ts"},{"op":"replace","path":"/metadata/files/11/path","value":"test/helpers/node-scenario.ts"},{"op":"replace","path":"/metadata/files/10/path","value":"test/gate-inputs.test.ts"},{"op":"replace","path":"/metadata/files/9/path","value":"test/duplication-gate.test.ts"},{"op":"replace","path":"/metadata/files/8/path","value":"test/coverage-remainder.test.ts"},{"op":"add","path":"/metadata/files/15","value":{"path":"test/projects.test.ts","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T11:23:54.547Z"}],"before_hash":"f9cfd128fd851fdad7d4c955aacc9685307b8811a917a03a937f9e2dd6fa8234","after_hash":"8c440624fab37ef992abbf179452e072c7f59a3ac0fdb7f2a88f2abcd0b066c6","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"5585724c13241fce95dab04e4c29b32a2ab9785e4ed149d96e00baa6dbe685ce"} +{"hash_algorithm":"sha256","ts":"2026-10-06T11:23:57.215Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"9aa62e730769acb6625de04f","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/15/path","value":"test/project-sync.test.ts"},{"op":"replace","path":"/metadata/files/14/path","value":"test/lint-policy.test.ts"},{"op":"replace","path":"/metadata/files/13/path","value":"test/import-errors.test.ts"},{"op":"replace","path":"/metadata/files/12/path","value":"test/helpers/node-scenario.ts"},{"op":"replace","path":"/metadata/files/11/path","value":"test/gate-inputs.test.ts"},{"op":"replace","path":"/metadata/files/10/path","value":"test/duplication-gate.test.ts"},{"op":"replace","path":"/metadata/files/9/path","value":"test/coverage-remainder.test.ts"},{"op":"replace","path":"/metadata/files/8/path","value":"test/coverage-gate.test.ts"},{"op":"replace","path":"/metadata/files/7/path","value":"test/coverage-edges.test.ts"},{"op":"add","path":"/metadata/files/16","value":{"path":"test/projects.test.ts","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T11:23:57.215Z"}],"before_hash":"8c440624fab37ef992abbf179452e072c7f59a3ac0fdb7f2a88f2abcd0b066c6","after_hash":"103579be578d579481be31b6b2cfee12fcd96a2de02d8385fc9dcbca2c23bf6f","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"5262c7cb0b10c31988b38579a0acb4440986ee64d1d401508db1b49dd9c86b02"} +{"hash_algorithm":"sha256","ts":"2026-10-06T11:23:58.883Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"9aa62e730769acb6625de04f","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/16/path","value":"test/project-sync.test.ts"},{"op":"replace","path":"/metadata/files/15/path","value":"test/lint-policy.test.ts"},{"op":"replace","path":"/metadata/files/14/path","value":"test/import-errors.test.ts"},{"op":"replace","path":"/metadata/files/13/path","value":"test/helpers/node-scenario.ts"},{"op":"replace","path":"/metadata/files/12/path","value":"test/gate-inputs.test.ts"},{"op":"replace","path":"/metadata/files/11/path","value":"test/duplication-gate.test.ts"},{"op":"replace","path":"/metadata/files/10/path","value":"test/coverage-remainder.test.ts"},{"op":"replace","path":"/metadata/files/9/path","value":"test/coverage-gate.test.ts"},{"op":"replace","path":"/metadata/files/8/path","value":"test/coverage-edges.test.ts"},{"op":"replace","path":"/metadata/files/7/path","value":"test/coverage-boundaries.test.ts"},{"op":"replace","path":"/metadata/files/6/path","value":"test"},{"op":"replace","path":"/metadata/files/5/path","value":"scripts/main-invocation.ts"},{"op":"add","path":"/metadata/files/17","value":{"path":"test/projects.test.ts","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T11:23:58.883Z"}],"before_hash":"103579be578d579481be31b6b2cfee12fcd96a2de02d8385fc9dcbca2c23bf6f","after_hash":"86f8beb0d01662dab14adc7dad22590ee9fc7e7a36348bf3b79d6ba66d524dc6","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"010d811a97827ada2d77dad0bf87b6bbd99246898aad2f298c298350a962b164"} +{"hash_algorithm":"sha256","ts":"2026-10-06T11:33:54.301Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"9aa62e730769acb6625de04f","agent_provenance":{"model":{"value":"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-06T11:33:54.300Z","author":"codex-sol","text":"The second complete npm run passes all 471 tests with zero skips. Coverage is 99.80 percent lines/statements, 97.74 percent branches, and 100 percent functions. Every operational script and gate.ts now measure exact 100 in all dimensions. Remaining index.ts gaps are enumerated from the JSON receipt. Centralizing thrown-value formatting and the identical command gate error boundary preserves behavior while removing duplicated decisions; real collaborator errors are still redacted and fail closed. Additional proven unreachable arms: a search loop cannot execute with an empty match array; fixed gh version spawn reports failures in its result; an atomic dry-run returns before the non-atomic preview; the private Projects add-issue executor receives coordinates populated by its sole planner. Tests retain defaults exposed by public planners."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T11:33:54.301Z"}],"before_hash":"86f8beb0d01662dab14adc7dad22590ee9fc7e7a36348bf3b79d6ba66d524dc6","after_hash":"38ff0623900bf504f883ed96a7dfdabd0783e2a103cf5f5e8a652695997208f7","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"4a344b84e56a321a89eb1757f0d3a0bb34b2736be12cefff935ea7d92b2a4d74"} +{"hash_algorithm":"sha256","ts":"2026-10-06T11:40:59.548Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"9aa62e730769acb6625de04f","agent_provenance":{"model":{"value":"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-06T11:40:59.548Z","author":"codex-sol","text":"Third full coverage run: all 484 tests pass; statements, lines and functions are 100%, branches 99.59%. Nine remaining branch locations enumerated from coverage-final.json. Proof for deleting two private-path fallbacks: applyPushEntry receives only planner entries; existing project item indices reject empty ids, and both gqlAddDraft and gqlAddIssue throw before returning an empty id (real GraphQL missing-id tests verify this). The redundant no-item-id guard cannot execute. Pull statusSkipped is appended only inside if(pi.statusName), assigning optionName that same truthy string, so its missing-name fallback cannot execute. Complete PM reads validate tags and body/description strings; the private executor only looks up ids from that complete selected corpus. No coverage exclusions or threshold changes."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T11:40:59.548Z"}],"before_hash":"38ff0623900bf504f883ed96a7dfdabd0783e2a103cf5f5e8a652695997208f7","after_hash":"f2a0b96a314ac8adc962e1faa9c4aa3f93c64aa3d7a65286000f42a72b34e096","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"010ff361dc6e7f6633fa625b6cfd19ebd832f3c1867d5907355dc8e54f8b46c4"} +{"hash_algorithm":"sha256","ts":"2026-10-06T11:42:29.772Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"9aa62e730769acb6625de04f","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":"remove","path":"/metadata/blocked_reason"},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T11:42:29.772Z"},{"op":"add","path":"/metadata/unblock_note","value":"Full source coverage closure is being reverified at unchanged exact thresholds."}],"before_hash":"f2a0b96a314ac8adc962e1faa9c4aa3f93c64aa3d7a65286000f42a72b34e096","after_hash":"8c867422210574a18be0fd15e62a8d246c46fac89f3fc886a2bb70a538ed08f9","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"288e63e2296f86e3165e65a24862c4cd888ffec920097c4c10426ba62e46aac8"} +{"hash_algorithm":"sha256","ts":"2026-10-06T11:43:20.592Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"9aa62e730769acb6625de04f","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/17/path","value":"test/project-sync.test.ts"},{"op":"replace","path":"/metadata/files/16/path","value":"test/lint-policy.test.ts"},{"op":"replace","path":"/metadata/files/15/path","value":"test/import-errors.test.ts"},{"op":"replace","path":"/metadata/files/14/path","value":"test/helpers/public-acceptance.ts"},{"op":"add","path":"/metadata/files/18","value":{"path":"test/projects.test.ts","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T11:43:20.592Z"}],"before_hash":"8c867422210574a18be0fd15e62a8d246c46fac89f3fc886a2bb70a538ed08f9","after_hash":"a96b9b7422c4a78a010d23f1b4945691d04fa08365d59e4c8be8f23c5b206da3","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"196a4d2232c667ace10f7f0f3fbe6f28e6a18a80c1beb9ce97469959bf5a57d9"} +{"hash_algorithm":"sha256","ts":"2026-10-06T11:43:22.121Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"9aa62e730769acb6625de04f","agent_provenance":{"model":{"value":"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-06T11:43:22.121Z","author":"codex-sol","text":"The targeted real-command c8 check passes four tests: low unauthenticated quota, human Projects listings, after-command subprocess boundaries, and all four Projects mutation failure boundaries. It covers the remaining reachable branches, including open-board formatting and missing tags. The complete npm release check and all native Bun test files are now running on frozen source. docs/coverage-gap-inventory.md retains every initial uncovered statement and branch plus the final nine-branch inventory and closure proofs."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T11:43:22.121Z"}],"before_hash":"a96b9b7422c4a78a010d23f1b4945691d04fa08365d59e4c8be8f23c5b206da3","after_hash":"0af57d4cd3ae69e08e8bc12fe3798fe09f342cec26544ab22ac30e501b3da6c1","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"defd65fcc089b32e371693cea24fef784138a884a11dfa5eec672b65c020c68b"} +{"hash_algorithm":"sha256","ts":"2026-10-06T11:49:55.681Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"9aa62e730769acb6625de04f","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-06T11:49:55.681Z"},{"op":"replace","path":"/metadata/description","value":"The unchanged all-source gate now measures exact 100 percent lines, statements, branches and functions across all 12 authored TypeScript/JavaScript modules, including operational scripts. The frozen full npm coverage run passes 487 tests with zero failures or skips. Real Git, PM SDK, filesystem and HTTP cases cover reachable paths; removed private-path arms have invariant proofs recorded in comments and docs/coverage-gap-inventory.md. No source ignores or lowered thresholds. Final npm/Bun release receipts and orchestrator review remain separate."}],"before_hash":"0af57d4cd3ae69e08e8bc12fe3798fe09f342cec26544ab22ac30e501b3da6c1","after_hash":"f8bdea8bdb761e9b8c79ee4a7ba6164a8a6e2afa34278104918930ca591982d3","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"1780f60a9e90e4ca0de694f04fb3acac605e377a896731230a79a45cbf1390d1"} +{"hash_algorithm":"sha256","ts":"2026-10-06T11:49:57.012Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"9aa62e730769acb6625de04f","agent_provenance":{"model":{"value":"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-06T11:49:57.012Z","author":"codex-sol","text":"Frozen full npm coverage PASS: 487 tests, zero failures/skips; all 12 authored modules measure exact 100/100/100/100. JSON receipt has zero uncovered statements or branches. Production dependency audit passes with zero vulnerabilities. The linked PM planner command passes all 39 tests through pm test --run --progress; the linked watchdog command previously passed through the PM runner. Full release completion and native Bun verification follow."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T11:49:57.012Z"}],"before_hash":"f8bdea8bdb761e9b8c79ee4a7ba6164a8a6e2afa34278104918930ca591982d3","after_hash":"600174bcd983d6d82781763ce70913c0e691dd2e71ea0d00d29924cd2b312a15","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"3b9ec024fdc86e2ff15b10520bee0b7466bd5f2f1a391db46d2a2c25e35e13f0"} +{"hash_algorithm":"sha256","ts":"2026-10-06T11:55:39.390Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"9aa62e730769acb6625de04f","agent_provenance":{"model":{"value":"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-06T11:55:39.390Z","author":"codex-sol","text":"The first exact-100 full npm release check passed at implementation 40db9b3. Broad native testing then exposed a real HTTP timeout regression, recorded in hptv and fixed without raising deadlines. Source is re-frozen for full npm and native Bun verification; earlier coverage receipts are not claimed for the transport change until remeasured."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T11:55:39.390Z"}],"before_hash":"600174bcd983d6d82781763ce70913c0e691dd2e71ea0d00d29924cd2b312a15","after_hash":"df84709518e2ac54501a659f177fa635074e7957917831e4c5317cf1e274d6ab","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"57ec6eb9c1582ba5936e1bd2bb5bbd35543f86b004b51f5906f87b6ad4bdfea8"} +{"hash_algorithm":"sha256","ts":"2026-10-06T12:03:40.399Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"9aa62e730769acb6625de04f","agent_provenance":{"model":{"value":"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-06T12:03:40.399Z","author":"codex-sol","text":"Full npm release:check at transport-fix implementation 5ed8c47 PASS: 487 tests with zero failures/skips; exact 100/100/100/100 across all 12 authored modules. No uncovered statement or branch counters remain. All release static gates, Git-object privacy, production dependency audit, packing, changelog and publish attestation pass. Starting the full Bun-invoked release command on this same implementation; native Bun full-suite timeout regression already passes at 30.04 seconds."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T12:03:40.399Z"}],"before_hash":"df84709518e2ac54501a659f177fa635074e7957917831e4c5317cf1e274d6ab","after_hash":"02fa7a21a21af39d48a8325f8e379ba80c42742f8d1622935500235a45306463","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"bd211dd2bdd9d0f3dea3dd3177120f0651247bb59bebc7dcbefbdf0819331742"} +{"hash_algorithm":"sha256","ts":"2026-10-06T12:07:15.906Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"9aa62e730769acb6625de04f","agent_provenance":{"model":{"value":"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-06T12:07:15.906Z","author":"codex-sol","text":"Four native lock fixture failures are a test-runner API mismatch: Bun 1.3.5 does not implement node:test mock.method. Replacing only syscall-boundary replacement/restoration with a small explicit helper, retaining real temporary lock files, permission changes, live-peer protection and the same injected I/O failures. Also close the real descriptor after the simulated close failure so the fixture leaves no open descriptor. Production source and coverage thresholds are unchanged; focused Node and native Bun checks are running."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T12:07:15.906Z"}],"before_hash":"02fa7a21a21af39d48a8325f8e379ba80c42742f8d1622935500235a45306463","after_hash":"e32ec43d4819b9af6a8f67bf0f7205387fa6f3305b790e878642bef847b8c881","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"4d62bac4cd20b496e235d5db41aea795aa12fec7222e667f24544a627cea8968"} +{"hash_algorithm":"sha256","ts":"2026-10-06T12:08:41.954Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"9aa62e730769acb6625de04f","agent_provenance":{"model":{"value":"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-06T12:08:41.954Z","author":"codex-sol","text":"All four corrected lock boundary cases pass both Node and native Bun (4/4 each), with the same real filesystem and live-peer assertions. Build:test passes. Production source remains exactly 5ed8c47. The superseded native run with known mock.method fixture failures has been stopped; restarting the entire 30-file native suite with portable fixtures and verifying the complete 24-case Node lock file. The full Bun release command continues on the unchanged production source."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T12:08:41.954Z"}],"before_hash":"e32ec43d4819b9af6a8f67bf0f7205387fa6f3305b790e878642bef847b8c881","after_hash":"9bc1d711a56d3f015c6156f781f6a22e6cb81bb2b074331854d5af31667d5319","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"b2947be756b22b23d2dec6737b16cd6a24ca6236c4ed8e42a7e3665761867f01"} +{"hash_algorithm":"sha256","ts":"2026-10-06T12:19:41.403Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"9aa62e730769acb6625de04f","agent_provenance":{"model":{"value":"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-06T12:19:41.403Z","author":"codex-sol","text":"The complete native rerun exposed two remaining fixture lookup failures: Bun caches executable resolution at startup, so changing process.env.PATH later does not reliably redirect git. These privacy-gate negative controls now launch the same runtime in a real child with the intended PATH from startup, retaining real Git repositories, malformed inventory and failed blob-read assertions. Production sources remain byte-identical to 5ed8c47; no gate logic, ignores or thresholds change. Full 25-case Node/Bun privacy files are being verified before the final native rerun."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T12:19:41.403Z"}],"before_hash":"9bc1d711a56d3f015c6156f781f6a22e6cb81bb2b074331854d5af31667d5319","after_hash":"7356300e53563ced0e8e94686bb7d42b49ce9194941ee7258a35056844a07eb5","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"31e52f1d4a3bc934237e1abc5ba4fcf09128dfaeab9c328e6ef2ad7189368b07"} +{"hash_algorithm":"sha256","ts":"2026-10-06T12:20:40.769Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"9aa62e730769acb6625de04f","agent_provenance":{"model":{"value":"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-06T12:20:40.769Z","author":"codex-sol","text":"The startup-environment privacy fixture confirms an additional Bun lookup detail: an empty PATH falls back to default tool lookup. Using a nonempty scratch directory with no executable git makes the missing-tool case real under both runtimes. Malformed inventory and failed blob-read controls already pass in the native child; the complete Node privacy file passes 25/25. Retrying the full native privacy file with the empty-directory PATH, preserving the actual production gate and real Git fixtures."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T12:20:40.769Z"}],"before_hash":"7356300e53563ced0e8e94686bb7d42b49ce9194941ee7258a35056844a07eb5","after_hash":"2a4ea507d3d188c51c8e2228aaf3b96336e21cb3a43e55d6fa4d8d97b59abc1a","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"9e07aae811ba11cdc4291848424b1771f4963add7d9894edf693ca60ded51d8d"} +{"hash_algorithm":"sha256","ts":"2026-10-06T12:22:15.502Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"9aa62e730769acb6625de04f","agent_provenance":{"model":{"value":"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-06T12:22:15.502Z","author":"codex-sol","text":"Portable privacy fixtures now pass the complete 25-case native Bun file; the complete Node privacy file passes 25/25, and its final two startup-lookup cases pass again after the empty-directory PATH correction. ESLint and build:test pass. Starting the final complete native suite and a fresh npm coverage run with every final fixture, ensuring child-process coverage counters remain complete. Production implementation remains byte-identical to 5ed8c47."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T12:22:15.502Z"}],"before_hash":"2a4ea507d3d188c51c8e2228aaf3b96336e21cb3a43e55d6fa4d8d97b59abc1a","after_hash":"b4f44c0b89ed05420c596cf91d49526431ca7d9c8b88817e2577e808569c9138","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"374210a278cfdf43c356049c4a9bd62715aab96fe97eff39183adf9ccb77d5b3"} +{"hash_algorithm":"sha256","ts":"2026-10-06T12:31:03.577Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"9aa62e730769acb6625de04f","agent_provenance":{"model":{"value":"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-06T12:31:03.576Z","author":"codex-sol","text":"Final fresh npm coverage with all portable fixtures PASS: 487 tests, zero failures/skips; exact 100/100/100/100 across all 12 authored modules, 8398 lines/statements, 2219 branches and 208 functions. JSON contains zero uncovered statement/branch counters. The real same-runtime child fixtures preserve V8 coverage evidence. Complete npm and Bun release checks already pass on identical production sources; the final native suite remains in progress with no failures."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T12:31:03.577Z"}],"before_hash":"b4f44c0b89ed05420c596cf91d49526431ca7d9c8b88817e2577e808569c9138","after_hash":"24c7fb0728529bbdc843359d490abf1edbe8b9b33430e358648cbda789c83031","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"ce0f60dc3aae5e86e26b1ccf48404695ada14fdecd031504fd6ce13784eaff8b"} +{"hash_algorithm":"sha256","ts":"2026-10-06T12:31:38.843Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"9aa62e730769acb6625de04f","agent_provenance":{"model":{"value":"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-06T12:31:38.843Z","author":"codex-sol","text":"Counter correction for the final portable-fixture npm coverage receipt: it contains 2220/2220 covered branches, not 2219. Earlier complete release receipts contain 2219/2219. Both retain all 12 modules, exact 100 percent, zero uncovered counters and zero skipped source. Final lines/statements are 8398/8398 and functions 208/208. Production source is unchanged; the latest receipt is authoritative for the final fixture run."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T12:31:38.843Z"}],"before_hash":"24c7fb0728529bbdc843359d490abf1edbe8b9b33430e358648cbda789c83031","after_hash":"a3948c1f59a6f3f8e238fe81ff93b45fc1d48ad4214908eb5703004df12311b7","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"f315af4e5344ebf3ea78e0e12ffc1c90df92168f9a42479a2650da3964bf8017"} +{"hash_algorithm":"sha256","ts":"2026-10-06T12:33:58.453Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"9aa62e730769acb6625de04f","agent_provenance":{"model":{"value":"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-06T12:33:58.453Z","author":"codex-sol","text":"The complete native suite has all 487 cases passing across 30 files with zero failed assertions, but its shell result is nonzero. Isolated duplication-gate file reproduces exit one despite 3/3 passing: the script-entry negative control sets process.exitCode=1, and restoring undefined does not clear it under Bun. Four touched entry fixtures now restore previous ?? 0, matching existing docstring/attestation fixture practice. Production source remains unchanged. Checking each affected file exit status and restarting native acceptance to require exit zero as well as zero failed cases."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T12:33:58.453Z"}],"before_hash":"a3948c1f59a6f3f8e238fe81ff93b45fc1d48ad4214908eb5703004df12311b7","after_hash":"608e55233d1f2e1b666e929a0f84fc589d3f073c5eb0d79b90f76ed30e26de3a","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"2edf548bd29a9f2f96e7a1f476f3ca516ac333b621586388a7bafdf49300a1ed"} +{"hash_algorithm":"sha256","ts":"2026-10-06T12:35:41.765Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"9aa62e730769acb6625de04f","agent_provenance":{"model":{"value":"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-06T12:35:41.764Z","author":"codex-sol","text":"Exit cleanup verified: all four affected Bun entry-fixture files now return process exit zero with zero failed tests; the combined 38-case Node run also passes with zero skips, and lint/build:test pass. The previous complete native run already passed all 487 assertions across 30 files. Running the complete native command once more to obtain an explicit exit-zero receipt. Production code remains unchanged, with final full coverage exact 100/100/100/100."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T12:35:41.765Z"}],"before_hash":"608e55233d1f2e1b666e929a0f84fc589d3f073c5eb0d79b90f76ed30e26de3a","after_hash":"4eaee77c8ff3151b60d8fa2189698ea9dfa6ae7af624ec8d4d990a9678fcd39d","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"c7f72b751fd9a19e8383b95aceef6e7ee3d92548ab8e0cb8d15f8f6bc99f02ce"} +{"hash_algorithm":"sha256","ts":"2026-10-06T12:42:45.910Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"9aa62e730769acb6625de04f","agent_provenance":{"model":{"value":"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-06T12:42:45.910Z","author":"codex-sol","text":"Final native command npm run test:bun PASS with process exit zero: 487 passing cases across all 30 files, zero failed cases. Final source coverage is exact 100/100/100/100 across all 12 authored modules with no ignores/lowered thresholds and no uncovered counters. The complete npm/Bun release checks pass; final fixture-only changes retain byte-identical production implementation 5ed8c47. Portable syscall, startup-PATH and explicit exit-code cleanup fixtures now work under both runtimes. Claims will be released and the item remains open for orchestrator verification."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T12:42:45.910Z"}],"before_hash":"4eaee77c8ff3151b60d8fa2189698ea9dfa6ae7af624ec8d4d990a9678fcd39d","after_hash":"27d5499e0162b4794affc3da2005a61fa4eb0844d91cc8f57ee0c48a570e2f75","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"781f42596db6d3bd176469860381aefaed2209f341cb02499e696447c2500ee1"} +{"hash_algorithm":"sha256","ts":"2026-10-06T12:42:48.449Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"9aa62e730769acb6625de04f","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-06T12:42:48.449Z"}],"before_hash":"27d5499e0162b4794affc3da2005a61fa4eb0844d91cc8f57ee0c48a570e2f75","after_hash":"b305092e35d9b7d47a7443f762703617a279fdd4f1dd977cf710bc51cf9b825a","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"898bbf9113607fc9f724a802878adfc97fcab06249cfbcb62bb4aec1ba5d54af"} +{"hash_algorithm":"sha256","ts":"2026-10-06T13:52:23.090Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"f688d14d4e1e36891460ce7a","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-06T13:52:23.090Z"},{"op":"add","path":"/metadata/assignee","value":"codex-sol"},{"op":"add","path":"/metadata/claim_principal","value":"codex-sol"}],"before_hash":"b305092e35d9b7d47a7443f762703617a279fdd4f1dd977cf710bc51cf9b825a","after_hash":"f226c3777e964fca97d850dcac56a7b3c8576a74503f69d60fba38d603b0ff9d","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"14faace09be91208ad7ceed1c13e98611dfe0e20a460f1a39240f52c9b69a669"} +{"hash_algorithm":"sha256","ts":"2026-10-06T13:55:53.428Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"f688d14d4e1e36891460ce7a","agent_provenance":{"model":{"value":"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-06T13:55:53.428Z","author":"codex-sol","text":"Historical actual_result described ae86e7d: 420 tests passed but coverage was 97.59 percent lines/statements, 89.24 percent branches, 100 percent functions and exact-100 failed. The latest pushed baseline a849280 subsequently passed exact all-source 100/100/100/100 in fresh clones; replacing the stale current field, with review-round-1 verification pending."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T13:55:53.428Z"}],"before_hash":"f226c3777e964fca97d850dcac56a7b3c8576a74503f69d60fba38d603b0ff9d","after_hash":"9d30f67e40af42939ccdb470a66f462f97e85580d3e5f97bcf62028c8e8d6311","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"f85d737c8a7ffc077d83d3a5ecd326108875a25e2836b23743aab6560aaebda7"} +{"hash_algorithm":"sha256","ts":"2026-10-06T13:55:54.396Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"f688d14d4e1e36891460ce7a","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/actual_result","value":"Pushed baseline a849280: exact all-source 100/100/100/100 coverage across 12 authored modules; exact-100 gate passes in fresh npm and Bun clones with 489 tests. Review-round-1 verification is pending."},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T13:55:54.396Z"}],"before_hash":"9d30f67e40af42939ccdb470a66f462f97e85580d3e5f97bcf62028c8e8d6311","after_hash":"85106bd27b79b036857cc2b905d703785e25f851b657e0e9affabce82fb65fdb","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"c04f083ed8f270fe17588f3128755dac9796735b98c9426e1961485b6b0753bd"} +{"hash_algorithm":"sha256","ts":"2026-10-06T13:58:18.654Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"f688d14d4e1e36891460ce7a","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/18/path","value":"test/lint-policy.test.ts"},{"op":"replace","path":"/metadata/files/17/path","value":"test/import-errors.test.ts"},{"op":"replace","path":"/metadata/files/16/path","value":"test/helpers/public-acceptance.ts"},{"op":"replace","path":"/metadata/files/15/path","value":"test/helpers/node-scenario.ts"},{"op":"replace","path":"/metadata/files/14/path","value":"test/gate-inputs.test.ts"},{"op":"replace","path":"/metadata/files/13/path","value":"test/duplication-gate.test.ts"},{"op":"replace","path":"/metadata/files/12/path","value":"test/coverage-remainder.test.ts"},{"op":"replace","path":"/metadata/files/11/path","value":"test/coverage-gate.test.ts"},{"op":"replace","path":"/metadata/files/10/path","value":"test/coverage-edges.test.ts"},{"op":"replace","path":"/metadata/files/9/path","value":"test/coverage-boundaries.test.ts"},{"op":"replace","path":"/metadata/files/8/path","value":"test"},{"op":"replace","path":"/metadata/files/7/path","value":"scripts/privacy-gate.ts"},{"op":"replace","path":"/metadata/files/6/path","value":"scripts/main-invocation.ts"},{"op":"replace","path":"/metadata/files/5/path","value":"scripts/generate-changelog.ts"},{"op":"add","path":"/metadata/files/19","value":{"path":"test/project-sync.test.ts","scope":"project"}},{"op":"add","path":"/metadata/files/20","value":{"path":"test/projects.test.ts","scope":"project"}},{"op":"add","path":"/metadata/files/21","value":{"path":"test/review-round1.test.ts","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T13:58:18.654Z"}],"before_hash":"85106bd27b79b036857cc2b905d703785e25f851b657e0e9affabce82fb65fdb","after_hash":"76531c607c06a95d05da3d6e2d2535d0f2fbd7ce04589bdd6c8ef7a621132ab6","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"d2841565c23788cbc30f29ee35e44ba2a9de2ea4319476c38620432a99a46a7f"} +{"hash_algorithm":"sha256","ts":"2026-10-06T13:58:20.430Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"f688d14d4e1e36891460ce7a","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"tests_add","patch":[{"op":"add","path":"/metadata/tests/4","value":{"command":"node --test --test-name-pattern=\"README gate links|coverage task current|privacy fixtures never|comment failure fixture|dot-dot names|canonical changelog includes|historical fixture provenance reports|gate override\" test/review-round1.test.ts test/coverage-gate.test.ts test/generate-changelog.test.ts test/privacy-gate.test.ts test/link-deps.test.ts","scope":"project","provenance":{"author":"codex-sol","created_at":"2026-10-06T13:58:20.374Z","source_kind":"local_mutation","source_ref":"feat/import-privacy-gate"}}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T13:58:20.430Z"}],"before_hash":"76531c607c06a95d05da3d6e2d2535d0f2fbd7ce04589bdd6c8ef7a621132ab6","after_hash":"ceb03ccf302d628c1b97f126b327c29d1f55d5162cea3816c5dec3f8517a0236","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"1cbb15af374f042e19ecb6146464658c8d4df7f82280b342e1f56ca0d3a094c3"} +{"hash_algorithm":"sha256","ts":"2026-10-06T14:00:16.233Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"f688d14d4e1e36891460ce7a","agent_provenance":{"model":{"value":"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-06T14:00:16.233Z","author":"codex-sol","text":"Review regressions verified red/green for all 16 findings. Linked focused pm test commands pass 8 tests each; result persistence is disabled by existing project policy, so this comment records the receipt. Build:test, ESLint, 130 documented declarations and zero duplication across all 12 authored modules pass. Canonicalization required replacing nonexistent mocked directories in two prior Git failure tests with real fixture directories; their intended status and containment refusal branches pass."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T14:00:16.233Z"}],"before_hash":"ceb03ccf302d628c1b97f126b327c29d1f55d5162cea3816c5dec3f8517a0236","after_hash":"15ff796552c4b6de30c8f90579c37969e67832e38b00c3eda0042299738f1b6a","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"2957c750c3b54fc84a78b4be37500d9f707432f20e96bab7d3d48ef4d32fc3b7"} +{"hash_algorithm":"sha256","ts":"2026-10-06T14:10:31.229Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"f688d14d4e1e36891460ce7a","agent_provenance":{"model":{"value":"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-06T14:10:31.229Z","author":"codex-sol","text":"Persistent revert receipts: 4196078345 current coverage task field; 4196078470 README anchor; 4196078483 in-repository dot-dot source name; 4196078520 changelog spawn/signal diagnostics; 4196078539 historical provenance Git diagnostic; 4196078563 permission cleanup after a forced sync throw; 4196078603 complete secret literal refusal; 4196078637 explicit gate override. Each reviewed-code negative control failed and restored code passed. No finding was declined. Existing exact-100 source inventory, thresholds and exclusions are unchanged."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T14:10:31.229Z"}],"before_hash":"15ff796552c4b6de30c8f90579c37969e67832e38b00c3eda0042299738f1b6a","after_hash":"cfb86433192d986897d27d25de82e173ff15fd448a8f19e736bbe42eca1c9910","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"12a2bccf27de5cc308d8c5fe6498805a4bb215990f3f61f5a5c5c0a8524a8ee6"} +{"hash_algorithm":"sha256","ts":"2026-10-06T14:20:35.653Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"f688d14d4e1e36891460ce7a","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/actual_result","value":"Corrected implementation c8b9329: fresh npm and Bun release gates pass 503/503 tests with exact all-source 100/100/100/100 across 12 modules. Lines/statements 8406/8406; Node 22 functions 209/209 and branches 2227/2227; Node 26 functions 208/208 and branches 2226/2226. Exact-100 gate passes; final receipt-head clone verification follows."},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T14:20:35.653Z"}],"before_hash":"cfb86433192d986897d27d25de82e173ff15fd448a8f19e736bbe42eca1c9910","after_hash":"2b42015bb3add247a7785308034212011f5518f5cdb251e18fb7de1371f1efda","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"453b7fbaa074dd020fe747c0f130f22166b464eef26d46df7e55441b1c6fbd27"} +{"hash_algorithm":"sha256","ts":"2026-10-06T14:20:36.531Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"f688d14d4e1e36891460ce7a","agent_provenance":{"model":{"value":"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-06T14:20:36.531Z","author":"codex-sol","text":"Fresh-clone release gates at c8b9329 PASS: 503 tests, exact all-source 100/100/100/100, 130 documented declarations, zero duplication, lint, typecheck, privacy, zero production audit vulnerabilities, package dry-run, changelog check and publish attestation. Latest coverage receipts replace the historical failed actual_result; previous measurements remain in comments/history. The portable diagnostic fixture has another failing revert/passing restore proof, and focused Node/native Bun verification passes. Preparing final receipt commit and exact pushed-head clones."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T14:20:36.531Z"}],"before_hash":"2b42015bb3add247a7785308034212011f5518f5cdb251e18fb7de1371f1efda","after_hash":"f725c153432946ac67ff21444d5a7c6c2c225fabc70c398ba5906b53cdf36ea6","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"93e462e682e43ff904596d31a7e70d0e7aa74912159cc55b2e386eaf82454705"} +{"hash_algorithm":"sha256","ts":"2026-10-06T14:20:37.374Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"f688d14d4e1e36891460ce7a","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"tests_add","patch":[{"op":"add","path":"/metadata/tests/5","value":{"command":"bun test --timeout 600000 -t \"canonical changelog includes|historical fixture provenance reports\" ./test/generate-changelog.test.ts ./test/privacy-gate.test.ts","scope":"project","provenance":{"author":"codex-sol","created_at":"2026-10-06T14:20:37.334Z","source_kind":"local_mutation","source_ref":"feat/import-privacy-gate"}}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T14:20:37.374Z"}],"before_hash":"f725c153432946ac67ff21444d5a7c6c2c225fabc70c398ba5906b53cdf36ea6","after_hash":"f8ee2c80a056c69751f80121930543fb14dca0b2a0fffdbf7838514e68070860","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"5cedaf5884855a79503bb50da8830bcdec8b89d3e8907e7b9d0ca39cef5c364e"} +{"hash_algorithm":"sha256","ts":"2026-10-06T14:20:43.544Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"f688d14d4e1e36891460ce7a","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-06T14:20:43.544Z"}],"before_hash":"f8ee2c80a056c69751f80121930543fb14dca0b2a0fffdbf7838514e68070860","after_hash":"8dbcf9afcfec503ebb6592b5eb864ee0c6b47ee7ebce62799a6395c965808af3","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"54013360c772369d113941c6f581299e974b375f59486ca6f4a3f2e6524c3681"} +{"hash_algorithm":"sha256","ts":"2026-10-06T14:30:33.851Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"f688d14d4e1e36891460ce7a","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"tests_remove","patch":[{"op":"remove","path":"/metadata/tests/5"},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T14:30:33.851Z"}],"before_hash":"8dbcf9afcfec503ebb6592b5eb864ee0c6b47ee7ebce62799a6395c965808af3","after_hash":"0f7da4041b742da99fec7b05031f52b1ff020cc37e9f789e136cad93a1f8596e","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"ae65f73acb936cc93582813be17abb6d3290f82f63865e9e917f9f652a765139"} +{"hash_algorithm":"sha256","ts":"2026-10-06T14:30:35.037Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"f688d14d4e1e36891460ce7a","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"tests_add","patch":[{"op":"add","path":"/metadata/tests/5","value":{"command":"bun test --timeout 600000 ./test/generate-changelog.test.ts ./test/privacy-gate.test.ts","scope":"project","provenance":{"author":"codex-sol","created_at":"2026-10-06T14:30:35.002Z","source_kind":"local_mutation","source_ref":"feat/import-privacy-gate"}}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T14:30:35.037Z"}],"before_hash":"0f7da4041b742da99fec7b05031f52b1ff020cc37e9f789e136cad93a1f8596e","after_hash":"98b07b2735ab4799b92610e48141ba305daabe0d62238d82af077318dd6113b0","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"9a25162ece0fdc76d05ad2f10797ba6b8995fd697b7c20eb9d312d5286bfaf98"} +{"hash_algorithm":"sha256","ts":"2026-10-06T14:33:48.844Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"f688d14d4e1e36891460ce7a","agent_provenance":{"model":{"value":"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-06T14:33:48.844Z","author":"codex-sol","text":"Final source/test head f572382 passes fresh npm and Bun release checks: 503/503 tests, exact 100/100/100/100 across 12 modules, 8406/8406 lines/statements; Node 22 has 209/209 functions and 2226/2226 branches, Node 26 has 208/208 functions and 2225/2225 branches. CI test (22) and test (26) pass including the full native Bun suite. Replaced the filtered native linked command after the pinned PM runner falsely classified two passing Bun tests as empty; full native changelog/privacy files pass 29/29 through pm test --run --only-last --progress. Separate follow-up pm-github-2ao1 preserves that upstream limitation; no package source or coverage policy workaround was added."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T14:33:48.844Z"}],"before_hash":"98b07b2735ab4799b92610e48141ba305daabe0d62238d82af077318dd6113b0","after_hash":"072bfb2e35d4927c483e73c065d9cf71f236744738a93ee5ee6bcd9a3e401846","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"76e68cb0b219ffdf61790e8f6ec8ecbe6041c06a36c7001feb45c327835c502e"} +{"hash_algorithm":"sha256","ts":"2026-10-06T14:33:49.525Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"f688d14d4e1e36891460ce7a","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/actual_result","value":"Fresh npm and Bun release gates at f572382 pass 503/503 tests with exact all-source 100/100/100/100 across 12 modules. Lines/statements 8406/8406; Node 22 functions 209/209 and branches 2226/2226; Node 26 functions 208/208 and branches 2225/2225. Exact-100 gate passes. CI Node 22/26 and native Bun pass; full native linked changelog/privacy tests pass 29/29."},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T14:33:49.525Z"}],"before_hash":"072bfb2e35d4927c483e73c065d9cf71f236744738a93ee5ee6bcd9a3e401846","after_hash":"3e59c603b75889f7003f2dfeb81f705291054db010973a8a9bd2cdff0d613a62","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"9d04fa427382030f509bf4b8cb30c70b2d03b283db11464b0e834d96f6a7c1ef"} +{"hash_algorithm":"sha256","ts":"2026-10-06T17:04:26.705Z","author":"claude-orchestrator","author_source":"asserted","agent_harness":"claude-code","agent_model":"claude-opus-5-5","agent_model_source":"probe","agent_instance":"1c3d835edba106c1302b3106","agent_provenance":{"model":{"value":"claude-opus-5-5","source":"probe"},"effort":{"value":"high","source":"environment"},"role":null,"topic":null,"version":{"value":"2.1.291","source":"probe"}},"op":"tests_remove","patch":[{"op":"remove","path":"/metadata/tests/5"},{"op":"replace","path":"/metadata/tests/4/provenance/created_at","value":"2026-10-06T14:30:35.002Z"},{"op":"replace","path":"/metadata/tests/4/command","value":"bun test --timeout 600000 ./test/generate-changelog.test.ts ./test/privacy-gate.test.ts"},{"op":"replace","path":"/metadata/tests/3/provenance/created_at","value":"2026-10-06T13:58:20.374Z"},{"op":"remove","path":"/metadata/tests/3/timeout_seconds"},{"op":"replace","path":"/metadata/tests/3/command","value":"node --test --test-name-pattern=\"README gate links|coverage task current|privacy fixtures never|comment failure fixture|dot-dot names|canonical changelog includes|historical fixture provenance reports|gate override\" test/review-round1.test.ts test/coverage-gate.test.ts test/generate-changelog.test.ts test/privacy-gate.test.ts test/link-deps.test.ts"},{"op":"replace","path":"/metadata/tests/2/provenance/created_at","value":"2026-10-05T09:38:01.943Z"},{"op":"replace","path":"/metadata/tests/2/provenance/author","value":"codex-sol"},{"op":"replace","path":"/metadata/tests/2/timeout_seconds","value":300},{"op":"replace","path":"/metadata/tests/2/command","value":"node --test test/coverage-boundaries.test.ts test/import-errors.test.ts test/gate-inputs.test.ts"},{"op":"replace","path":"/metadata/tests/1/provenance/created_at","value":"2026-10-05T09:29:21.443Z"},{"op":"replace","path":"/metadata/tests/1/timeout_seconds","value":60},{"op":"replace","path":"/metadata/tests/1/command","value":"node --test test/projects.test.ts"},{"op":"remove","path":"/metadata/tests/0/note"},{"op":"replace","path":"/metadata/tests/0/timeout_seconds","value":600},{"op":"remove","path":"/metadata/tests/0/path"},{"op":"replace","path":"/metadata/tests/0/command","value":"node --test test/project-sync.test.ts test/projects.test.ts"},{"op":"add","path":"/metadata/tests/0/provenance","value":{"author":"codex-sol-coverage","created_at":"2026-10-05T09:18:36.238Z","source_kind":"local_mutation","source_ref":"feat/import-privacy-gate"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T17:04:26.705Z"}],"before_hash":"3e59c603b75889f7003f2dfeb81f705291054db010973a8a9bd2cdff0d613a62","after_hash":"94b755ef6b50ee3f938617ca7eaca7b02e8158e705b6e5bcd126f849708a149c","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":"maintenance","record_hash_version":1,"record_hash":"5e916ddae53aa001cfeb1075b56002d51c250ac9dfe81d90244c492ec7a2974a"} +{"hash_algorithm":"sha256","ts":"2026-10-06T17:04:27.468Z","author":"claude-orchestrator","author_source":"asserted","agent_harness":"claude-code","agent_model":"claude-opus-5-5","agent_model_source":"probe","agent_instance":"1c3d835edba106c1302b3106","agent_provenance":{"model":{"value":"claude-opus-5-5","source":"probe"},"effort":{"value":"high","source":"environment"},"role":null,"topic":null,"version":{"value":"2.1.291","source":"probe"}},"op":"tests_add","patch":[{"op":"add","path":"/metadata/tests/5","value":{"command":"npm run coverage","path":"scripts/coverage-gate.ts","scope":"project","timeout_seconds":900,"provenance":{"author":"claude-orchestrator","created_at":"2026-10-06T17:04:27.440Z","source_kind":"local_mutation","source_ref":"feat/import-privacy-gate"},"note":"Exact 100/100/100/100 gate over every executable source; passes in CI (Node 22 and 26) and in fresh-clone npm and Bun release:check."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T17:04:27.468Z"}],"before_hash":"94b755ef6b50ee3f938617ca7eaca7b02e8158e705b6e5bcd126f849708a149c","after_hash":"374de7708e90f1b8f564db04fb0804c814e5928eeaea7734cc479188355c9295","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":"maintenance","record_hash_version":1,"record_hash":"506d2f66ada003267a7fc0f815926ecae990fc623c68b49526023dfc01bd7d9d"} +{"hash_algorithm":"sha256","ts":"2026-10-06T17:04:28.835Z","author":"claude-orchestrator","author_source":"asserted","agent_harness":"claude-code","agent_model":"claude-opus-5-5","agent_model_source":"probe","agent_instance":"1c3d835edba106c1302b3106","agent_provenance":{"model":{"value":"claude-opus-5-5","source":"probe"},"effort":{"value":"high","source":"environment"},"role":{"value":"implementer","source":"argv"},"topic":null,"version":{"value":"2.1.291","source":"probe"}},"op":"update","patch":[{"op":"replace","path":"/metadata/unblock_note","value":"Resolved: exact all-source 100/100/100/100 coverage verified in CI and fresh-clone npm/Bun release gates (PR 108)."},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T17:04:28.835Z"}],"before_hash":"374de7708e90f1b8f564db04fb0804c814e5928eeaea7734cc479188355c9295","after_hash":"fc1e89c54bee0807e82fc448e0bf8c9f98632dec0ebbe66ba03d2db7d5faf6e5","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":"maintenance","record_hash_version":1,"record_hash":"0425e9cfb4b4305cb8aa01093a74f8fa6b8db7f3cd426cd591781303d1530aa0"} +{"hash_algorithm":"sha256","ts":"2026-10-06T17:04:30.299Z","author":"claude-orchestrator","author_source":"asserted","agent_harness":"claude-code","agent_model":"claude-opus-5-5","agent_model_source":"probe","agent_instance":"1c3d835edba106c1302b3106","agent_provenance":{"model":{"value":"claude-opus-5-5","source":"probe"},"effort":{"value":"high","source":"environment"},"role":{"value":"implementer","source":"argv"},"topic":null,"version":{"value":"2.1.291","source":"probe"}},"op":"close","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T17:04:30.299Z"},{"op":"replace","path":"/metadata/status","value":"closed"},{"op":"add","path":"/metadata/closed_at","value":"2026-10-06T17:04:30.277Z"},{"op":"add","path":"/metadata/completed_at","value":"2026-10-06T17:04:30.277Z"},{"op":"add","path":"/metadata/close_reason","value":"Verified by the orchestrator: PR 108 CI test (22)/(26) green with the exact 100/100/100/100 gate, fresh-clone npm and Bun release:check 504/504 at 7a41660 and later heads; no ignores or lowered thresholds."}],"before_hash":"fc1e89c54bee0807e82fc448e0bf8c9f98632dec0ebbe66ba03d2db7d5faf6e5","after_hash":"70087b3898bae50496af62ec1e4eccf1df7219366bcc2ad8abefb811a958f6a5","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":"16bec176ddfb37879d0b056ddb14060985ec30671aadb0623f2f5ac00fcd39a4"} +{"hash_algorithm":"sha256","ts":"2026-10-06T18:17:32.977Z","author":"pi-gh-cr3b","author_source":"asserted","agent_harness":"pi","agent_model":"glm-5.3:cloud","agent_model_source":"environment","agent_instance":"adacbc2ad7298f54bbf2c51f","agent_provenance":{"model":{"value":"glm-5.3:cloud","source":"environment"},"effort":null,"role":{"value":"implementer","source":"argv"},"topic":null},"op":"update","patch":[{"op":"replace","path":"/metadata/actual_result","value":"Fresh npm and Bun release gates pass 504/504 tests at 7a41660 and later heads with exact all-source 100/100/100/100 across 12 modules; a fresh coverage run at the closing branch head passes 504/504 with zero failures or skips. The close_reason's 504/504 receipt is authoritative for the closed item; the earlier 503/503 count described the superseded head f572382. CI Node 22/26 and native Bun pass; full native linked changelog/privacy tests pass 29/29."},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T18:17:32.977Z"}],"before_hash":"70087b3898bae50496af62ec1e4eccf1df7219366bcc2ad8abefb811a958f6a5","after_hash":"e00477774fcda4e51c79290ee87af24cd7ab3f809613a10069534a9f826aef10","item_hash_version":3,"event_class":"maintenance","record_hash_version":1,"record_hash":"27cbbc5eb0b6b97130ae4f1141f3c3357f5fb0f0d9c2f8713d45923b5ab7ce33"} +{"hash_algorithm":"sha256","ts":"2026-10-06T18:17:36.953Z","author":"pi-gh-cr3b","author_source":"asserted","agent_harness":"pi","agent_model":"glm-5.3:cloud","agent_model_source":"environment","agent_instance":"adacbc2ad7298f54bbf2c51f","agent_provenance":{"model":{"value":"glm-5.3:cloud","source":"environment"},"effort":null,"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/34","value":{"created_at":"2026-10-06T18:17:36.953Z","author":"pi-gh-cr3b","text":"CodeRabbit 4198811214: resolved the close_reason 504/504 vs actual_result 503/503 mismatch. A fresh coverage run at the closing head re-measured the exact-100 gate: 504/504 tests pass with zero failures and skips, matching the close_reason receipt for 7a41660 and later heads. The 503/503 count was the earlier f572382 receipt and was stale. The close_reason is the authoritative closing receipt; actual_result now cites 504/504 and states which receipt supersedes which. The item stays closed; no reopen."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T18:17:36.953Z"}],"before_hash":"e00477774fcda4e51c79290ee87af24cd7ab3f809613a10069534a9f826aef10","after_hash":"0a727ed6a0fd41527893d1c68ad2ea85f0ed707f91bbbc0e476035a814da5d24","item_hash_version":3,"event_class":"substantive","record_hash_version":1,"record_hash":"2befd2dcdf901c335e19f1f6aa65333754cab2907d95047174b37deaa064f478"} +{"hash_algorithm":"sha256","ts":"2026-10-06T19:48:19.546Z","author":"claude-orchestrator","author_source":"asserted","agent_harness":"claude-code","agent_model":"claude-opus-5-5","agent_model_source":"probe","agent_instance":"1c3d835edba106c1302b3106","agent_provenance":{"model":{"value":"claude-opus-5-5","source":"probe"},"effort":{"value":"high","source":"environment"},"role":{"value":"implementer","source":"argv"},"topic":null,"version":{"value":"2.1.291","source":"probe"}},"op":"update","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T19:48:19.546Z"},{"op":"replace","path":"/metadata/description","value":"The unchanged all-source gate measures exact 100 percent lines, statements, branches and functions across all 12 authored TypeScript/JavaScript modules, including operational scripts. At the closing head the npm and Bun release gates each pass 505 tests with zero failures or skips (the 487-test frozen run is historical). Real Git, PM SDK, filesystem and HTTP cases cover reachable paths; removed private-path arms have invariant proofs recorded in comments and docs/coverage-gap-inventory.md. No source ignores or lowered thresholds. Verified and closed by the orchestrator."}],"before_hash":"0a727ed6a0fd41527893d1c68ad2ea85f0ed707f91bbbc0e476035a814da5d24","after_hash":"1b16685d2ef3b601d898c16e9bbf72b168bb8332d559ffd0821f0a666ecb6563","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":"c68d68dafd33f8fcf81dbde13d78bb2c478d8fa1c025eb71385bca5f42ed5957"} +{"hash_algorithm":"sha256","ts":"2026-10-06T21:45:33.285Z","author":"claude-orchestrator","author_source":"asserted","agent_harness":"claude-code","agent_model":"claude-opus-5-5","agent_model_source":"probe","agent_instance":"1c3d835edba106c1302b3106","agent_provenance":{"model":{"value":"claude-opus-5-5","source":"probe"},"effort":{"value":"high","source":"environment"},"role":{"value":"implementer","source":"argv"},"topic":null,"version":{"value":"2.1.291","source":"probe"}},"op":"update","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T21:45:33.285Z"},{"op":"replace","path":"/metadata/description","value":"The unchanged all-source gate measures exact 100 percent lines, statements, branches and functions across all 12 authored TypeScript/JavaScript modules, including operational scripts. The closing receipt (actual_result and close_reason) is the fresh npm and Bun release gates passing 504/504 tests at 7a41660; later review rounds add tests without changing that verdict, and the 487-test frozen run is historical. Real Git, PM SDK, filesystem and HTTP cases cover reachable paths; removed private-path arms have invariant proofs recorded in comments and docs/coverage-gap-inventory.md. No source ignores or lowered thresholds. Verified and closed by the orchestrator."}],"before_hash":"1b16685d2ef3b601d898c16e9bbf72b168bb8332d559ffd0821f0a666ecb6563","after_hash":"5cf4411ffb30c67722d66e043812a7522bf33c61ca2287b625749477fd03a740","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":"67ec1938399c1e905219f868b21f67ecc94fd12ab0308b04df5117356dab1f0b"} +{"hash_algorithm":"sha256","ts":"2026-10-06T21:45:34.169Z","author":"claude-orchestrator","author_source":"asserted","agent_harness":"claude-code","agent_model":"claude-opus-5-5","agent_model_source":"probe","agent_instance":"1c3d835edba106c1302b3106","agent_provenance":{"model":{"value":"claude-opus-5-5","source":"probe"},"effort":{"value":"high","source":"environment"},"role":null,"topic":null,"version":{"value":"2.1.291","source":"probe"}},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/35","value":{"created_at":"2026-10-06T21:45:34.169Z","author":"claude-orchestrator","text":"Receipt reconciliation: the authoritative closing count is 504/504 at 7a41660 (actual_result, close_reason). Counts of 503 and 505 seen in earlier comments are historical measurements at other heads."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T21:45:34.169Z"}],"before_hash":"5cf4411ffb30c67722d66e043812a7522bf33c61ca2287b625749477fd03a740","after_hash":"cbab28c1edfcdf416253f5ebaed861a49303a8406983463f72a3e877cdbd51a7","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":"56d6c921e03de522cc8d4b8104181c77cd2989c5c5b27ec0ab1026a9cf4cc1f0"} diff --git a/.agents/pm/history/pm-github-aiok.jsonl b/.agents/pm/history/pm-github-aiok.jsonl new file mode 100644 index 00000000..0ff91d03 --- /dev/null +++ b/.agents/pm/history/pm-github-aiok.jsonl @@ -0,0 +1,5 @@ +{"hash_algorithm":"sha256","ts":"2026-10-05T09:23:42.980Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"0221c1e0bd861e4e03844581","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-aiok"},{"op":"add","path":"/metadata/title","value":"Adopt the released gated sync caller in fleet repositories"},{"op":"add","path":"/metadata/description","value":"After the gated extension is published and its exact quality gates pass, replace the release commit and version placeholders in docs/sync-workflow-caller.yml and migrate the disabled pm-graph, pm-jira, and pm-todos sync callers to the single reusable implementation. Keep automated workflows disabled until the owner authorizes rollout. The current candidate is confined to pm-github and must not modify or dispatch workflows in other repositories. Verify a fresh nonempty import, malicious no-push fixtures, repeat idempotency, and permanent changed-item links in each authorized caller rollout."},{"op":"add","path":"/metadata/type","value":"Task"},{"op":"add","path":"/metadata/status","value":"open"},{"op":"add","path":"/metadata/priority","value":1},{"op":"add","path":"/metadata/tags","value":[]},{"op":"add","path":"/metadata/created_at","value":"2026-10-05T09:23:42.980Z"},{"op":"add","path":"/metadata/updated_at","value":"2026-10-05T09:23:42.980Z"},{"op":"add","path":"/metadata/author","value":"codex-sol"}],"before_hash":"3cc22dff72be7b14824654a7a64ea62b04799939b2fee54c1b5f52ca60bf6df0","after_hash":"6df510d325703fd2bac626c0bb6d60df2ebf3ab22ef8b57fece89d48ff99db46","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":"fb38344f640cca79e2b41a2642d52e80d090872db41ea60eca4beea48c209e77"} +{"hash_algorithm":"sha256","ts":"2026-10-05T09:25:31.280Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"0221c1e0bd861e4e03844581","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-05T09:25:31.280Z"},{"op":"add","path":"/metadata/comments","value":[{"created_at":"2026-10-05T09:25:31.280Z","author":"codex-sol","text":"The caller example intentionally uses release commit and exact version placeholders. The currently published extension predates the gate, and this work is authorized only in the pm-github worktree. No pilot workflow in another repository was changed, enabled, or dispatched. Resolve the placeholders only after a reviewed release and separately authorized caller adoption."}]}],"before_hash":"6df510d325703fd2bac626c0bb6d60df2ebf3ab22ef8b57fece89d48ff99db46","after_hash":"afd27b09ad086b7bc0339128cf19fecc80e654e03c3f8f44b06ee0d0ca5b9ae8","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"b56dd9d675dd91bb0e498090a718e529ee48b8b0244c745ec21b4ebf7b3c0602"} +{"hash_algorithm":"sha256","ts":"2026-10-05T09:25:59.227Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"0221c1e0bd861e4e03844581","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-05T09:25:59.227Z"},{"op":"add","path":"/metadata/files","value":[{"path":"docs/sync-workflow-caller.yml","scope":"project"}]}],"before_hash":"afd27b09ad086b7bc0339128cf19fecc80e654e03c3f8f44b06ee0d0ca5b9ae8","after_hash":"b5f06d9ff353748e0db7b1603a7082e45cc6d3931e045f83f39bfa9f4f38c13c","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"b582f9f159f845f61c14bb7c6f626954807d6a764b5a15b48382001b8034ec0f"} +{"hash_algorithm":"sha256","ts":"2026-10-05T09:26:00.035Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"0221c1e0bd861e4e03844581","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"files_add","patch":[{"op":"replace","path":"/metadata/files/0/path","value":".github/workflows/pm-github-sync.yml"},{"op":"add","path":"/metadata/files/1","value":{"path":"docs/sync-workflow-caller.yml","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-05T09:26:00.035Z"}],"before_hash":"b5f06d9ff353748e0db7b1603a7082e45cc6d3931e045f83f39bfa9f4f38c13c","after_hash":"ace7fe3eed738ae79e924bb2dcd5d737884f92d56c645dd0bfd26feedbae41c0","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"6bce5babc0899063bf4fadb1a8438d2f6d0e543952b1bc83240f32b20d96103d"} +{"hash_algorithm":"sha256","ts":"2026-10-05T09:32:54.787Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"0221c1e0bd861e4e03844581","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-05T09:32:54.787Z"},{"op":"add","path":"/metadata/dependencies","value":[{"id":"pm-github-f1at","kind":"related","created_at":"2026-10-05T09:32:54.728Z","author":"codex-sol","source_kind":"cli:update:dep","author_source":"detected"}]}],"before_hash":"ace7fe3eed738ae79e924bb2dcd5d737884f92d56c645dd0bfd26feedbae41c0","after_hash":"7c5c3f9c474ddf2c45bd4673bfc27442e51863889c3561690fe1400acc597e4a","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"dbe2f513aac4d1acb373693cfdccf5517d680c1ffa049f1a9c29f2b7bc92e2a2"} diff --git a/.agents/pm/history/pm-github-f1at.jsonl b/.agents/pm/history/pm-github-f1at.jsonl new file mode 100644 index 00000000..bca1dd73 --- /dev/null +++ b/.agents/pm/history/pm-github-f1at.jsonl @@ -0,0 +1,50 @@ +{"hash_algorithm":"sha256","ts":"2026-10-05T06:09:58.912Z","author":"pi-glm","author_source":"asserted","agent_harness":"pi","agent_model":"glm-5.3:cloud","agent_model_source":"environment","agent_instance":"5445f482cb2cfed6ee2872fd","agent_provenance":{"model":{"value":"glm-5.3:cloud","source":"environment"},"effort":null,"role":{"value":"implementer","source":"argv"},"topic":null},"op":"create","patch":[{"op":"add","path":"/metadata/id","value":"pm-github-f1at"},{"op":"add","path":"/metadata/title","value":"Fail-closed privacy gate for automated GitHub issue imports and one reusable sync workflow"},{"op":"add","path":"/metadata/description","value":"Automated fleet-wide sync is disabled because the pilot workflow pushes imported issue text to a public branch before any privacy gate inspects the proposed tracker diff. Add a fail-closed scanner over the proposed tracker change, wire it into import as --gate, and ship one reusable workflow_call sync workflow fleet repos can call."},{"op":"add","path":"/metadata/type","value":"Feature"},{"op":"add","path":"/metadata/status","value":"open"},{"op":"add","path":"/metadata/priority","value":1},{"op":"add","path":"/metadata/tags","value":["area:ci","area:gates","area:import","area:privacy"]},{"op":"add","path":"/metadata/created_at","value":"2026-10-05T06:09:58.912Z"},{"op":"add","path":"/metadata/updated_at","value":"2026-10-05T06:09:58.912Z"},{"op":"add","path":"/metadata/author","value":"pi-glm"},{"op":"add","path":"/metadata/acceptance_criteria","value":"A fresh checkout installs the pinned extension and exercises a nonempty issue import; malicious issue-body credential and personal-data fixtures prevent any public branch push; completeness and idempotency are checked before mutation; a clean import opens a reviewable PR with permanent PM links; existing pilot workflows and future rollouts use one audited implementation or equivalent gate."}],"before_hash":"3cc22dff72be7b14824654a7a64ea62b04799939b2fee54c1b5f52ca60bf6df0","after_hash":"bae313dae2c32fec3423ca241d26bfdf707b670a51025eea49962e3a222a2bcc","item_hash_version":3,"message":"Feature item for companion hub item pm-cli-website-8ufh","event_class":"substantive","record_hash_version":1,"record_hash":"5058e73e9e6700e88ee3d3672e6dc5b37d87c2a11a674f0d9c46bf8db2cc348a"} +{"hash_algorithm":"sha256","ts":"2026-10-05T06:10:02.421Z","author":"pi-glm","author_source":"asserted","agent_harness":"pi","agent_model":"glm-5.3:cloud","agent_model_source":"environment","agent_instance":"5445f482cb2cfed6ee2872fd","agent_provenance":{"model":{"value":"glm-5.3:cloud","source":"environment"},"effort":null,"role":{"value":"implementer","source":"argv"},"topic":null},"op":"claim","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-10-05T06:10:02.421Z"},{"op":"add","path":"/metadata/assignee","value":"pi-glm"},{"op":"add","path":"/metadata/claim_principal","value":"pi-glm"}],"before_hash":"bae313dae2c32fec3423ca241d26bfdf707b670a51025eea49962e3a222a2bcc","after_hash":"335c623734318ded1b843440337000c9eaf07ecdde6cf0ce88a5ffee96a14db9","item_hash_version":3,"event_class":"substantive","record_hash_version":1,"record_hash":"5e4a3a09536ac5d0c016fa48f44be7dea4875a7cb7047c82dcf3d68e93984254"} +{"hash_algorithm":"sha256","ts":"2026-10-05T06:10:03.070Z","author":"pi-glm","author_source":"asserted","agent_harness":"pi","agent_model":"glm-5.3:cloud","agent_model_source":"environment","agent_instance":"5445f482cb2cfed6ee2872fd","agent_provenance":{"model":{"value":"glm-5.3:cloud","source":"environment"},"effort":null,"role":{"value":"implementer","source":"argv"},"topic":null},"op":"update","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-10-05T06:10:03.070Z"},{"op":"replace","path":"/metadata/status","value":"in_progress"}],"before_hash":"335c623734318ded1b843440337000c9eaf07ecdde6cf0ce88a5ffee96a14db9","after_hash":"59f797a8e6f17a1253935f0e83942241b1955f827e303b34179f77feb94ac93e","item_hash_version":3,"event_class":"substantive","record_hash_version":1,"record_hash":"f08137fb308a402305140a201aebbc152985c9938c8dd1b5f512bc6a5d98ac20"} +{"hash_algorithm":"sha256","ts":"2026-10-05T06:21:37.676Z","author":"pi-glm","author_source":"asserted","agent_harness":"pi","agent_model":"glm-5.3:cloud","agent_model_source":"environment","agent_instance":"5445f482cb2cfed6ee2872fd","agent_provenance":{"model":{"value":"glm-5.3:cloud","source":"environment"},"effort":null,"role":null,"topic":null},"op":"comment_add","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-10-05T06:21:37.676Z"},{"op":"add","path":"/metadata/comments","value":[{"created_at":"2026-10-05T06:21:37.672Z","author":"pi-glm","text":"Design settled: new gate.ts holds the pure scanner (credential/personal-data/host-path rules over ADDED lines only, sha256 content-hash allowlist with required written justification, fail-closed on unreadable input/git failure/malformed allowlist); unified-diff parser with toon field attribution + jsonl patch-entry attribution; git collector covers staged+working+untracked under the resolved tracker path and excludes operational locks/extensions/checkpoints subpaths. Registered as 'pm github gate' (--diff, --allowlist) and composed into import via --gate: plan completeness + provenance idempotency verified pre-mutation, tracker-change scan post-write, non-zero exit so a workflow can never push on a finding. Reusable workflow_call sync workflow added with SHA-pinned actions; caller example at docs/sync-workflow-caller.yml."}]}],"before_hash":"59f797a8e6f17a1253935f0e83942241b1955f827e303b34179f77feb94ac93e","after_hash":"044358f7928148b2f066ebda13a2815fb02b5d99203de29078ff90b524f66c9a","item_hash_version":3,"event_class":"substantive","record_hash_version":1,"record_hash":"2f1dea2f9d4104d7d13fbb9880da68c0b3db214d3c39f5c7b17b4cdd2d6868fe"} +{"hash_algorithm":"sha256","ts":"2026-10-05T06:22:04.701Z","author":"pi-glm","author_source":"asserted","agent_harness":"pi","agent_model":"glm-5.3:cloud","agent_model_source":"environment","agent_instance":"5445f482cb2cfed6ee2872fd","agent_provenance":{"model":{"value":"glm-5.3:cloud","source":"environment"},"effort":null,"role":null,"topic":null},"op":"files_add","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-10-05T06:22:04.701Z"},{"op":"add","path":"/metadata/files","value":[{"path":"gate.ts","scope":"project"}]}],"before_hash":"044358f7928148b2f066ebda13a2815fb02b5d99203de29078ff90b524f66c9a","after_hash":"ed29ded4b61401bace418957eb24b6a6a8bc668ccaddb3d10af412ba40aba0bf","item_hash_version":3,"event_class":"maintenance","record_hash_version":1,"record_hash":"7ff7880c68f92c0fd63429c8323f4c2faaa9805c1a8bd7af95bb74c99ea0fcc3"} +{"hash_algorithm":"sha256","ts":"2026-10-05T06:22:06.428Z","author":"pi-glm","author_source":"asserted","agent_harness":"pi","agent_model":"glm-5.3:cloud","agent_model_source":"environment","agent_instance":"5445f482cb2cfed6ee2872fd","agent_provenance":{"model":{"value":"glm-5.3:cloud","source":"environment"},"effort":null,"role":null,"topic":null},"op":"files_add","patch":[{"op":"add","path":"/metadata/files/1","value":{"path":"index.ts","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-05T06:22:06.428Z"}],"before_hash":"ed29ded4b61401bace418957eb24b6a6a8bc668ccaddb3d10af412ba40aba0bf","after_hash":"f9567ad77c90f9730a48bda687844b14293d0d68514e01f3a75a5d0df962549b","item_hash_version":3,"event_class":"maintenance","record_hash_version":1,"record_hash":"a377045c5277d0231aaf568cb45545e216b0a29d124e3ddc1c52a259e4fd6165"} +{"hash_algorithm":"sha256","ts":"2026-10-05T06:22:10.138Z","author":"pi-glm","author_source":"asserted","agent_harness":"pi","agent_model":"glm-5.3:cloud","agent_model_source":"environment","agent_instance":"5445f482cb2cfed6ee2872fd","agent_provenance":{"model":{"value":"glm-5.3:cloud","source":"environment"},"effort":null,"role":null,"topic":null},"op":"files_add","patch":[{"op":"replace","path":"/metadata/files/1/path","value":"gate.ts"},{"op":"replace","path":"/metadata/files/0/path","value":".github/workflows/pm-github-sync.yml"},{"op":"add","path":"/metadata/files/2","value":{"path":"index.ts","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-05T06:22:10.138Z"}],"before_hash":"f9567ad77c90f9730a48bda687844b14293d0d68514e01f3a75a5d0df962549b","after_hash":"93dfd939278e2504aa725eba201f7d3689d65376952320b061cbd87b93dcc2ce","item_hash_version":3,"event_class":"maintenance","record_hash_version":1,"record_hash":"9b939a197182161f291ebbefbeb5f28e059983f8acd73ac53e1fe2a6b32e4ea3"} +{"hash_algorithm":"sha256","ts":"2026-10-05T06:22:13.045Z","author":"pi-glm","author_source":"asserted","agent_harness":"pi","agent_model":"glm-5.3:cloud","agent_model_source":"environment","agent_instance":"5445f482cb2cfed6ee2872fd","agent_provenance":{"model":{"value":"glm-5.3:cloud","source":"environment"},"effort":null,"role":null,"topic":null},"op":"files_add","patch":[{"op":"replace","path":"/metadata/files/2/path","value":"gate.ts"},{"op":"replace","path":"/metadata/files/1/path","value":"docs/sync-workflow-caller.yml"},{"op":"add","path":"/metadata/files/3","value":{"path":"index.ts","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-05T06:22:13.045Z"}],"before_hash":"93dfd939278e2504aa725eba201f7d3689d65376952320b061cbd87b93dcc2ce","after_hash":"344e3eb7c8bb017b193b880d69e7c50434d2e36974c154a7e6ff62cb63dc077a","item_hash_version":3,"event_class":"maintenance","record_hash_version":1,"record_hash":"00a71272ca07130fddbaf5d8bd6c7903f1a9dafc9debafd209bd90b044ddcb08"} +{"hash_algorithm":"sha256","ts":"2026-10-05T06:33:25.742Z","author":"pi-glm","author_source":"asserted","agent_harness":"pi","agent_model":"glm-5.3:cloud","agent_model_source":"environment","agent_instance":"5445f482cb2cfed6ee2872fd","agent_provenance":{"model":{"value":"glm-5.3:cloud","source":"environment"},"effort":null,"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/1","value":{"created_at":"2026-10-05T06:33:25.742Z","author":"pi-glm","text":"Implementation checkpoint: gate.ts (scanner + unified-diff parser with toon/jsonl field attribution + git collector over staged/working/untracked tracker changes, operational locks/extensions/checkpoints excluded + sha256-keyed allowlist with required justification) is registered as 'pm github gate' and composed into import via --gate (plan completeness + provenance idempotency verified pre-mutation, fail-closed scan post-write, non-zero exit). Reusable workflow_call sync workflow with SHA-pinned actions + caller example added. Full package suite green: 347 tests pass, coverage 93.55 lines / 84.05 branches / 93.62 functions (repo thresholds 88/79/89 met), docstring gate 119/119, repo privacy gate clean over 3107 objects. New suites test/gate.test.ts (29) and test/import-gate.test.ts (13) include the adversarial full-sequence replay on a real git repo with a bare remote: token-in-body, token-in-comment, email, host-path, and high-entropy fixtures all fail the gated import before anything can be pushed, and the remote's ref list never gains the sync branch."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-05T06:33:25.742Z"}],"before_hash":"344e3eb7c8bb017b193b880d69e7c50434d2e36974c154a7e6ff62cb63dc077a","after_hash":"41e77efd5d7356eadad9f22ed9f2e3ecd38e0ed168e5d3d28a87bf161398bc03","item_hash_version":3,"event_class":"substantive","record_hash_version":1,"record_hash":"2347a96dd786df55718cbc162cea85063b812c684f0df37121c7bfd40419d3a5"} +{"hash_algorithm":"sha256","ts":"2026-10-05T06:42:45.410Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"0221c1e0bd861e4e03844581","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/claim_principal","value":"codex-sol"},{"op":"replace","path":"/metadata/assignee","value":"codex-sol"},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-05T06:42:45.410Z"}],"before_hash":"41e77efd5d7356eadad9f22ed9f2e3ecd38e0ed168e5d3d28a87bf161398bc03","after_hash":"43672ab45f9f03bd1ce5a68951d2594e687f2217b178fa95455beb2917b22032","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"2707def959b5654eb3f2369594a826d66242a1b1abfe8bc041bb9c55723ad949"} +{"hash_algorithm":"sha256","ts":"2026-10-05T06:42:46.064Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"0221c1e0bd861e4e03844581","agent_provenance":{"model":{"value":"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-05T06:42:46.064Z","author":"codex-sol","text":"Resumed interrupted run as codex-sol. Preserving implementation commit and README draft. Verification priorities: scanner fail-closed inputs and redaction, full bare-remote sequence, SDK-resolved tracker paths, reusable workflow input safety and existing PR updates, real public issue acceptance, and requested all-source 100 percent coverage. Previous evidence met only the existing lower thresholds; this is not the requested coverage gate."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-05T06:42:46.064Z"}],"before_hash":"43672ab45f9f03bd1ce5a68951d2594e687f2217b178fa95455beb2917b22032","after_hash":"78edd3d3c7a4336feee764bafe36d7b5165eb6f1886f84775a3b39f4c50a767c","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"b07a6e2b0ef3c315f37de97d9e6da08e714ed63e3234ba1ae99e8e9c725ef9eb"} +{"hash_algorithm":"sha256","ts":"2026-10-05T06:44:14.360Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"0221c1e0bd861e4e03844581","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-05T06:44:14.360Z"},{"op":"add","path":"/metadata/docs","value":[{"path":"README.md","scope":"project"}]}],"before_hash":"78edd3d3c7a4336feee764bafe36d7b5165eb6f1886f84775a3b39f4c50a767c","after_hash":"4f8bd5ff7ad4325253f0c5963b868953d672879dcc8191e8f5dcceef6f200c61","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"86e6a6f5a2319ee88e8e34c4a503e05c248df5a7bd3e615a5231ea48ba086fe9"} +{"hash_algorithm":"sha256","ts":"2026-10-05T06:44:15.604Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"0221c1e0bd861e4e03844581","agent_provenance":{"model":{"value":"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-05T06:44:15.604Z","author":"codex-sol","text":"Verified inherited code with npm run typecheck, npm run docstring (119 declarations), and node --test test/gate.test.ts test/import-gate.test.ts (42 passed). Preserved README draft, removed host-path examples, and marked rollout references as release placeholders because the current published version predates the gate. Review identified staged-versus-working-tree and same-rule allowlist deduplication bypasses; fixes and regression tests follow."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-05T06:44:15.604Z"}],"before_hash":"4f8bd5ff7ad4325253f0c5963b868953d672879dcc8191e8f5dcceef6f200c61","after_hash":"fc351a964b4f1924fe4bb78c0e89d1fa709c62b2926e1e287f114035b4d878e7","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"a240e0a616d75949138cfe34b306536757782ae8cbf47004c7b0435986fbf193"} +{"hash_algorithm":"sha256","ts":"2026-10-05T06:58:12.486Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"0221c1e0bd861e4e03844581","agent_provenance":{"model":{"value":"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-05T06:58:12.486Z","author":"codex-sol","text":"Resumed verification found and fixed distinct-content allowlist suppression, staged-content omission, malformed/binary diff acceptance, unreadable optional allowlists, and escaped token/path parsing. Regression suite now passes 47 gate/import tests; lock tests passed in the prior focused run. Workflow now validates same-repository automated branch and exact version inputs through environment variables, imports comments, checks gate availability, verifies the second import has no filesystem changes, and uses a branch-specific force-with-lease. The inherited test commit contained a synthetic PEM header; retained exact-blob provenance documents that false positive without rewriting history."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-05T06:58:12.486Z"}],"before_hash":"fc351a964b4f1924fe4bb78c0e89d1fa709c62b2926e1e287f114035b4d878e7","after_hash":"148fa83796ce15e541738e84b6bd4e86e911f415c8e1c2845c6d7430b16adbbe","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"a61fe9dd27a82d85fd0114d340c6e1106da794a0ebf2b244f14357003585d18a"} +{"hash_algorithm":"sha256","ts":"2026-10-05T07:00:44.735Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"0221c1e0bd861e4e03844581","agent_provenance":{"model":{"value":"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-05T07:00:44.735Z","author":"codex-sol","text":"Real public acceptance: a fresh scratch pm project installed pinned PM CLI 2026.10.4 and the packed pm-github candidate 2026.10.4. Read-only unbraind/pm-todos import planned 3 issues, imported 3 items, reconciled fetched/planned/skipped as 3/3/0, passed privacy scanning over 6 changed files with zero findings, and completed in 10.84 seconds. A second gated atomic import exited zero and left all tracker item/history bytes identical. No remote or push was used. Final candidate will be rerun with comments and native Bun before reporting completion."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-05T07:00:44.735Z"}],"before_hash":"148fa83796ce15e541738e84b6bd4e86e911f415c8e1c2845c6d7430b16adbbe","after_hash":"bdbeba9345881eea071dcfde94dce31d3b9b9001c59c594f73f8cae6e81b0d9d","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"1af575277b62d08d1ecbbfb4d53667b9b01a0a6a2f830b3b5af484f5e6ec25e1"} +{"hash_algorithm":"sha256","ts":"2026-10-05T07:43:18.871Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"0221c1e0bd861e4e03844581","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"files_add","patch":[{"op":"add","path":"/metadata/files/4","value":{"path":"test/gate.test.ts","scope":"project"}},{"op":"add","path":"/metadata/files/5","value":{"path":"test/import-gate.test.ts","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-05T07:43:18.871Z"}],"before_hash":"bdbeba9345881eea071dcfde94dce31d3b9b9001c59c594f73f8cae6e81b0d9d","after_hash":"54d4dfb4d9ccc081dda088b2ceca3cc5a6d052a5ee1a5a82f07ad042f45d8d5e","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"7d8af0365381f083bd3d069c0cc3ae2ab2d32dc7a3d818e40fe01abab96d9224"} +{"hash_algorithm":"sha256","ts":"2026-10-05T07:43:20.053Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"0221c1e0bd861e4e03844581","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"docs_add","patch":[{"op":"replace","path":"/metadata/docs/0/path","value":"docs/sync-workflow-caller.yml"},{"op":"add","path":"/metadata/docs/1","value":{"path":"README.md","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-05T07:43:20.053Z"}],"before_hash":"54d4dfb4d9ccc081dda088b2ceca3cc5a6d052a5ee1a5a82f07ad042f45d8d5e","after_hash":"e61f7ac94b9217fd1737a4b828e570f64c7571e49483480b1fb627208daf360c","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"e82f57f50b0df4fd1d78424fb6a8753e76b9b094525598c4e52efe84d74302e9"} +{"hash_algorithm":"sha256","ts":"2026-10-05T07:43:22.506Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"0221c1e0bd861e4e03844581","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-05T07:43:22.506Z"},{"op":"add","path":"/metadata/tests","value":[{"command":"node --test test/gate.test.ts test/import-gate.test.ts","scope":"project","timeout_seconds":600,"provenance":{"author":"codex-sol","created_at":"2026-10-05T07:43:22.444Z","source_kind":"local_mutation","source_ref":"feat/import-privacy-gate"}}]}],"before_hash":"e61f7ac94b9217fd1737a4b828e570f64c7571e49483480b1fb627208daf360c","after_hash":"8bb7aea8293f37335d935f7ec23cb5ac7e8a235a2da7e845d63dce6799ab8b82","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"b2b250d9557c00d3d233d34fd1f24c7975d5ecfe75e49dccc828243a8ace0c33"} +{"hash_algorithm":"sha256","ts":"2026-10-05T07:52:13.198Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"0221c1e0bd861e4e03844581","agent_provenance":{"model":{"value":"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-05T07:52:13.195Z","author":"codex-sol","text":"Verified the actual reusable workflow shell sequence against a real git repository and bare remote: body token, comment token, email, and local-path fixtures prevent push and PR creation; clean imports create a PR and a fresh-checkout repeat updates it safely. Added a post-write provenance check that rejects stale completed SDK journals when persisted items are missing. Focused gate and workflow suite: 53 tests passed; final all-source coverage and native Bun checks are pending."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-05T07:52:13.198Z"}],"before_hash":"8bb7aea8293f37335d935f7ec23cb5ac7e8a235a2da7e845d63dce6799ab8b82","after_hash":"32087485cf1bd0bae87033b2245ed09e43ace8ad2b3ab06db047c44811a48911","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"e7ca607751e5979e083dbe383675d8611638a9d505893b6bc87a11d27e9ca908"} +{"hash_algorithm":"sha256","ts":"2026-10-05T08:03:34.543Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"0221c1e0bd861e4e03844581","agent_provenance":{"model":{"value":"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-05T08:03:34.543Z","author":"codex-sol","text":"Final packed-candidate public acceptance in a fresh scratch PM project: read-only unbraind/pm-todos plan fetched/planned/skipped 3/3/0, first gated atomic import created 3 items, privacy verdict PASS over 6 files with zero findings, Node repeat exited zero with identical item/history bytes, native Bun repeat exited zero with identical bytes, and pinned CLI strict health passed. Runtime 68.69 seconds during concurrent verification; no remote configured or push performed. Separate with-comments acceptance correctly rejected personal email content before persistence. Native Bun focused suite now passes 53 tests with an explicit real-CLI timeout."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-05T08:03:34.543Z"}],"before_hash":"32087485cf1bd0bae87033b2245ed09e43ace8ad2b3ab06db047c44811a48911","after_hash":"06fdb7dca7df80262be4d55a3752c92293e95cf3518d75e0e6258c63d59edf8d","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"6241061efcdb342ce971f9526a3f26b344bb998e736176646da2741d7179678c"} +{"hash_algorithm":"sha256","ts":"2026-10-05T08:05:48.301Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"0221c1e0bd861e4e03844581","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-05T08:05:48.301Z"},{"op":"add","path":"/metadata/tags/4","value":"changelog-unreleased"}],"before_hash":"06fdb7dca7df80262be4d55a3752c92293e95cf3518d75e0e6258c63d59edf8d","after_hash":"72ac85a655290aa7c81c04bd2ab6ede6b549db4fe5319f43af69e286902a4de6","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"0b360383c0fd70c36b9fd98e866274f97e389395d02ed9bf9fb09e38bebf4c59"} +{"hash_algorithm":"sha256","ts":"2026-10-05T08:08:22.823Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"0221c1e0bd861e4e03844581","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"files_add","patch":[{"op":"replace","path":"/metadata/files/5/path","value":"test/gate.test.ts"},{"op":"replace","path":"/metadata/files/4/path","value":"scripts/generate-changelog.ts"},{"op":"add","path":"/metadata/files/6","value":{"path":"test/generate-changelog.test.ts","scope":"project"}},{"op":"add","path":"/metadata/files/7","value":{"path":"test/import-gate.test.ts","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-05T08:08:22.823Z"}],"before_hash":"72ac85a655290aa7c81c04bd2ab6ede6b549db4fe5319f43af69e286902a4de6","after_hash":"bf38ec97a108b935d4d8c655107d4dc0ec547a3618860512b5e56b8512ea6ee6","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"e71800baa2387c95babfa20ac0947c03f7a23e72f33495ad733140c7f7851214"} +{"hash_algorithm":"sha256","ts":"2026-10-05T08:08:23.784Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"0221c1e0bd861e4e03844581","agent_provenance":{"model":{"value":"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-05T08:08:23.784Z","author":"codex-sol","text":"The canonical changelog generator defaults to closed items, while this work must remain open for orchestrator verification. Added a complete SDK read feeding pm-changelog and an explicit changelog-unreleased tag for this candidate only; other open claims stay excluded. The generated Unreleased section now links the feature without fabricating completion. Added regression coverage for staged operational files, nested tracker content, credential-bearing history pointers, and unused REST comment-user metadata."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-05T08:08:23.784Z"}],"before_hash":"bf38ec97a108b935d4d8c655107d4dc0ec547a3618860512b5e56b8512ea6ee6","after_hash":"0c706746a4fdaf8d2dcfddf13466cdd93269446adbe4510f7e3f8eb04c406afc","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"fb614f135638e12595a69baa0c35b3752586fa4cbc6a8a4c718e71264420a7e6"} +{"hash_algorithm":"sha256","ts":"2026-10-05T08:12:38.561Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"0221c1e0bd861e4e03844581","agent_provenance":{"model":{"value":"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-05T08:12:38.561Z","author":"codex-sol","text":"Final workflow review found that GitHub runners do not inherit npm's local-bin PATH during standalone shell steps. The dependency step now publishes the checkout's local bin directory through GITHUB_PATH, and executable workflow fixtures prioritize that local CLI. This removes reliance on a globally installed pm executable in fresh-checkout imports. Public candidate receipts remain read-only; fleet callers require a future pinned feature release and audited caller adoption."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-05T08:12:38.561Z"}],"before_hash":"0c706746a4fdaf8d2dcfddf13466cdd93269446adbe4510f7e3f8eb04c406afc","after_hash":"229ffab794b0b9e679854f062a770775df0a0c3193f289771e6ebd289b057d09","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"07a4265dfd7b8780be71c4c44e93702b1d9e2b6405e275ac5119b2f212169829"} +{"hash_algorithm":"sha256","ts":"2026-10-05T08:16:55.234Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"0221c1e0bd861e4e03844581","agent_provenance":{"model":{"value":"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-05T08:16:55.234Z","author":"codex-sol","text":"Verified Node and native Bun regressions for gated preview redaction in atomic and non-atomic modes, including imported credential-shaped titles and labels. Corrected launcher acceptance to invoke Node exactly as the package prepare hook declares; a Bun test runner must not silently substitute its own executable for an explicitly Node-based lifecycle hook. Final full suites are being rerun with these corrections."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-05T08:16:55.234Z"}],"before_hash":"229ffab794b0b9e679854f062a770775df0a0c3193f289771e6ebd289b057d09","after_hash":"2bd668f8ef59aa53a0a09fbd4dfe81809ce3e8768eef0a0f7d0be279a1ce89f3","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"75256ee40d739038e1e7165d35dcf19fb650509763c5609519e470e58888ba9e"} +{"hash_algorithm":"sha256","ts":"2026-10-05T08:20:15.079Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"0221c1e0bd861e4e03844581","agent_provenance":{"model":{"value":"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-05T08:20:15.079Z","author":"codex-sol","text":"Native Bun investigation reproduced a concrete subprocess issue: after changing PATH, spawnSync without an explicit environment still found gh; with an explicit current environment it correctly failed ENOENT. GitHub credential discovery, gh availability checks, and PM subprocesses now pass the current environment explicitly, preserving scoped authentication and CLI selection under Node and Bun. This was verified with credential-presence booleans only; no credential values were logged."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-05T08:20:15.079Z"}],"before_hash":"2bd668f8ef59aa53a0a09fbd4dfe81809ce3e8768eef0a0f7d0be279a1ce89f3","after_hash":"4e29eaed12d3b332bc49284c533187966c85c2b709b9a71ae045f93883dec553","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"a4f4c47cb49800af33f9b18fa615309fff0c309d6abce744eb2b3ea28c35bbd8"} +{"hash_algorithm":"sha256","ts":"2026-10-05T08:28:56.086Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"0221c1e0bd861e4e03844581","agent_provenance":{"model":{"value":"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-05T08:28:56.085Z","author":"codex-sol","text":"Frozen packed-candidate public receipt: fetched/planned/skipped 3/3/0, imported 3 items, Node second run exited zero and left item/history bytes identical, privacy PASS over 6 changed files with zero findings, pinned strict health passed, and no remote was configured. The direct native Bun CLI repeat remained active and was terminated; it left tracker bytes identical but has no passing exit/verdict. The entire bounded sequence took 312.01 seconds. This supersedes the earlier native-CLI positive receipt as the current acceptance boundary; native library tests remain separate evidence."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-05T08:28:56.086Z"}],"before_hash":"4e29eaed12d3b332bc49284c533187966c85c2b709b9a71ae045f93883dec553","after_hash":"cd6a3f103cec75ea21225923b5a6afcadc4583eb66077dbb877dbfd0340e6569","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"fc9e8e2f8cfa11509983c4dd0247127c29c7c78e2b6afef22a7337b853351ef6"} +{"hash_algorithm":"sha256","ts":"2026-10-05T08:29:30.906Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"0221c1e0bd861e4e03844581","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"tests_add","patch":[{"op":"add","path":"/metadata/tests/1","value":{"command":"npm run test:bun","scope":"project","timeout_seconds":900,"provenance":{"author":"codex-sol","created_at":"2026-10-05T08:29:30.824Z","source_kind":"local_mutation","source_ref":"feat/import-privacy-gate"}}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-05T08:29:30.906Z"}],"before_hash":"cd6a3f103cec75ea21225923b5a6afcadc4583eb66077dbb877dbfd0340e6569","after_hash":"5ea72b0a11ca058b7fbe4987633d80012550903b9fcf52e13854da11231f729d","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"fc802cdb166a3a52d28ba626de622459a9d2de28e6c4843be2812da44a64d3b9"} +{"hash_algorithm":"sha256","ts":"2026-10-05T08:29:31.898Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"0221c1e0bd861e4e03844581","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"files_add","patch":[{"op":"replace","path":"/metadata/files/7/path","value":"test/handler-failures.test.ts"},{"op":"add","path":"/metadata/files/8","value":{"path":"test/import-gate.test.ts","scope":"project"}},{"op":"add","path":"/metadata/files/9","value":{"path":"test/prepare-merge-driver.test.ts","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-05T08:29:31.898Z"}],"before_hash":"5ea72b0a11ca058b7fbe4987633d80012550903b9fcf52e13854da11231f729d","after_hash":"da2756bd3520f7150db87356cf936ae38a5c3507fd65222c331794ce95070467","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"f6e5ca7dafb5dfc86e47f12267a267b162f6675fe4127d5b39bf854524601447"} +{"hash_algorithm":"sha256","ts":"2026-10-05T08:33:17.932Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"0221c1e0bd861e4e03844581","agent_provenance":{"model":{"value":"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-05T08:33:17.927Z","author":"codex-sol","text":"Expanded the explicit provider signatures to include Slack incoming-webhook credentials and OpenAI service-account keys, with syntactically shaped fake fixtures assembled at runtime. All gate findings still contain only rule, item id, field, and exact-content hash. These additions require a fresh final source-coverage receipt; no threshold or source exemption changed."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-05T08:33:17.932Z"}],"before_hash":"da2756bd3520f7150db87356cf936ae38a5c3507fd65222c331794ce95070467","after_hash":"32316f280ab9b8ec282507c3257d7311f6c498c302217608ba7e1f1e3706e9b7","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"580c15d5358d7b4d2520762e15c425eadd7cbe7f81260c989caccaea30f10cbf"} +{"hash_algorithm":"sha256","ts":"2026-10-05T08:43:10.321Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"0221c1e0bd861e4e03844581","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-05T08:43:10.321Z"},{"op":"replace","path":"/metadata/status","value":"blocked"},{"op":"add","path":"/metadata/blocked_by","value":"pm-github-9cjx"},{"op":"add","path":"/metadata/dependencies","value":[{"id":"pm-github-9cjx","kind":"blocked_by","created_at":"2026-10-05T08:43:10.230Z","author":"codex-sol","source_kind":"cli:update:blocked_by","author_source":"detected"},{"id":"pm-github-hptv","kind":"blocked_by","created_at":"2026-10-05T08:43:10.230Z","author":"codex-sol","source_kind":"cli:update:dep","author_source":"detected"}]}],"before_hash":"32316f280ab9b8ec282507c3257d7311f6c498c302217608ba7e1f1e3706e9b7","after_hash":"26ab9a6c79667deca62a65f7b348de115aef25d4c65cd64007522c5aca3c7742","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"4edd6db9fa27bdb57f8970733e1185afb1dee5ad7eb39983079776edc1cc3443"} +{"hash_algorithm":"sha256","ts":"2026-10-05T08:43:11.660Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"0221c1e0bd861e4e03844581","agent_provenance":{"model":{"value":"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-05T08:43:11.660Z","author":"codex-sol","text":"All 375 npm cases and all 375 native Bun cases passed in the full linked runs, but strict coverage failed and the Bun wrapper retained a deliberate failure exit code from operational-script fixture teardown. Corrected teardown to restore the previous numeric exit code, or zero when previously unset; the affected Bun files now exit zero without lowering assertions. The feature remains blocked by exact all-source coverage and the separate installed native CLI acceptance item."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-05T08:43:11.660Z"}],"before_hash":"26ab9a6c79667deca62a65f7b348de115aef25d4c65cd64007522c5aca3c7742","after_hash":"4c54c486b83f7efbe2299ccb408d0734d4d5e94d63055c1ae79f4d82f1993be5","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"705ec336c176996896221457961f44f394654f674c665c22bf8d7d3b21480f1a"} +{"hash_algorithm":"sha256","ts":"2026-10-05T08:58:35.322Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"0221c1e0bd861e4e03844581","agent_provenance":{"model":{"value":"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-05T08:58:35.322Z","author":"codex-sol","text":"Final frozen-source checks at 0b7c4ab: npm ran 375 tests with all passing; the full per-file native Bun linked test ran 375 cases and exited zero. Gate/import tests include 57 cases and actual workflow shell execution against bare remotes. Typecheck, strict erasable syntax, ESLint, 123 documented declarations, zero duplication across 12 authored modules, production audit, packing, attested-publish checks, generated changelog, pinned CLI strict health, and Git-object privacy passed. Mandatory 100/100/100/100 coverage remains blocked at 93.40/93.40/84.77/94.94. A latest direct public fetch retry timed out before planning or writing; preserve the earlier three-item Node receipt separately and keep native installed-CLI acceptance blocked."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-05T08:58:35.322Z"}],"before_hash":"4c54c486b83f7efbe2299ccb408d0734d4d5e94d63055c1ae79f4d82f1993be5","after_hash":"bac32bd88e7447eacaac78bc377e28b7d1599246d0e75171ad901d8299ef990a","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"9da008b846bac740ce0bcac0aa9802dc5c324373992e353e777c062b20343a81"} +{"hash_algorithm":"sha256","ts":"2026-10-05T09:03:35.170Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"0221c1e0bd861e4e03844581","agent_provenance":{"model":{"value":"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-05T09:03:35.170Z","author":"codex-sol","text":"Latest packed-candidate real-data acceptance recovered after the transient public API timeout: fresh scratch project with pinned CLI 2026.10.4 and the packed candidate planned/fetched/skipped 3/3/0 public unbraind/pm-todos issues, imported 3, passed privacy over 6 files with zero findings, completed a byte-identical Node repeat, and passed strict health in 12.28 seconds. No remote was configured and no push occurred. This is the final positive Node receipt; native installed-CLI acceptance remains separately bounded and tracked."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-05T09:03:35.170Z"}],"before_hash":"bac32bd88e7447eacaac78bc377e28b7d1599246d0e75171ad901d8299ef990a","after_hash":"8995c9b5f5b7edff39c7ab3fa5c5fb5e951921605b8d8ec7f435c0e23f2468bb","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"e794122680fda532ea108f3e702ef7a96187a7a259c6980e8dabfe465606828f"} +{"hash_algorithm":"sha256","ts":"2026-10-05T09:11:07.592Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"0221c1e0bd861e4e03844581","agent_provenance":{"model":{"value":"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-05T09:11:07.592Z","author":"codex-sol","text":"A current quota refresh reports Ollama Cloud, OpenCode Go, and Grok exhausted; Codex remains on pace within the weekly forecast. Started the updated Codex CLI with gpt-6.1-sol at xhigh to continue the existing exact all-source coverage task pm-github-9cjx. Thresholds remain 100 for all four metrics, no source ignores, and no release or rollout is authorized until the gate passes. The latest native installed-CLI repeat also passed in 4.22 seconds without tracker mutation; the earlier intermittent observation remains tracked."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-05T09:11:07.592Z"}],"before_hash":"8995c9b5f5b7edff39c7ab3fa5c5fb5e951921605b8d8ec7f435c0e23f2468bb","after_hash":"96cb0e1f31a9d95167a03f0a49396352a1c4382198371d48e77797c66a1f1d93","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"7263812d08222ee0fcd92e75f50fae9b6c609580c8dbeff2ffe765246bcbf55b"} +{"hash_algorithm":"sha256","ts":"2026-10-05T09:14:18.425Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"0221c1e0bd861e4e03844581","agent_provenance":{"model":{"value":"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-05T09:14:18.425Z","author":"codex-sol","text":"Additional privacy review reproduced two missed shapes: a credential present only in a proposed tracker filename, and Windows drive paths using forward slashes. Both are now scanned. Filename findings use file_path and sensitive item identifiers are redacted. Real temporary Git regression tests prove a token-bearing filename fails without creating a commit; URL controls remain clean. The combined gate suite passes 40 Node cases."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-05T09:14:18.425Z"}],"before_hash":"96cb0e1f31a9d95167a03f0a49396352a1c4382198371d48e77797c66a1f1d93","after_hash":"66b96321cbd92b0a2ecf6598c274e2e94c0466879900531ce6071e3f963f42f3","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"979f74952699ca05721a798de812e26dd9868c16a3322c104cf461bc6f36e93b"} +{"hash_algorithm":"sha256","ts":"2026-10-05T09:14:20.173Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"0221c1e0bd861e4e03844581","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-gate.test.ts"},{"op":"replace","path":"/metadata/files/8/path","value":"test/handler-failures.test.ts"},{"op":"replace","path":"/metadata/files/7/path","value":"test/generate-changelog.test.ts"},{"op":"replace","path":"/metadata/files/6/path","value":"test/gate.test.ts"},{"op":"replace","path":"/metadata/files/5/path","value":"test/gate-paths.test.ts"},{"op":"add","path":"/metadata/files/10","value":{"path":"test/prepare-merge-driver.test.ts","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-05T09:14:20.173Z"}],"before_hash":"66b96321cbd92b0a2ecf6598c274e2e94c0466879900531ce6071e3f963f42f3","after_hash":"45e46b754ffdf51c1339797ef58d750fac288922a704111d7983b76e148255b8","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"b7d86b6d829116307ce00a1691f94eb1d1da354098976b79a75268d0215fd323"} +{"hash_algorithm":"sha256","ts":"2026-10-05T09:15:00.824Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"0221c1e0bd861e4e03844581","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/gate-paths.test.ts","scope":"project","timeout_seconds":60,"provenance":{"author":"codex-sol","created_at":"2026-10-05T09:15:00.790Z","source_kind":"local_mutation","source_ref":"feat/import-privacy-gate"}}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-05T09:15:00.824Z"}],"before_hash":"45e46b754ffdf51c1339797ef58d750fac288922a704111d7983b76e148255b8","after_hash":"8a0424783a67e7d852e858d0bb7ec6070497751a2d93195ea8690c6655c778c0","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"a11e84fd66c04ae5836e3cdcd32064fc8f135efba9eb96d8147ca605761561f1"} +{"hash_algorithm":"sha256","ts":"2026-10-05T09:16:14.202Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"0221c1e0bd861e4e03844581","agent_provenance":{"model":{"value":"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-05T09:16:14.202Z","author":"codex-sol","text":"The proposed-filename and Windows-separator fixes passed 40 Node gate cases and 40 native Bun gate cases. The newly linked two-case command ran through pm test with progress and exit zero. Strict typecheck, source and test ESLint, 123 documented declarations, and the complete 12-module zero-duplication gate pass. Final broad npm, Bun, coverage, and packed acceptance must be refreshed after the concurrent coverage work settles."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-05T09:16:14.202Z"}],"before_hash":"8a0424783a67e7d852e858d0bb7ec6070497751a2d93195ea8690c6655c778c0","after_hash":"38e4740d0111e9b5d23a85a2b18ed38091240325cdb2301c0c09d719a86a78e5","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"530cf32f9d94692d85986e28d558a8ae8afd460685992bbf6a94d9f4e261d635"} +{"hash_algorithm":"sha256","ts":"2026-10-05T09:22:58.633Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"0221c1e0bd861e4e03844581","agent_provenance":{"model":{"value":"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-05T09:22:58.633Z","author":"codex-sol","text":"Further adversarial review reproduced camel-case accessToken/clientSecret assignments, Slack xapp and xoxc/xoxd credentials, and labelled unformatted phone data escaping the rules. The updated rules detect these shapes. Removed the date/UUID exemption because a credential assignment could deliberately include a date or use a UUID key; ordinary noncredential request ids stay clean. The combined gate battery passes 41 cases under Node and native Bun, with URL and machine-comment-id controls."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-05T09:22:58.633Z"}],"before_hash":"38e4740d0111e9b5d23a85a2b18ed38091240325cdb2301c0c09d719a86a78e5","after_hash":"5e9772d2f477a0dc3f557ce27df1b07e09150d5b3fad16bf682bb664a8322f5f","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"5209f329d3be0c838eebc97cda874d8dc7dd2151326eb44bce9e0961ac68a134"} +{"hash_algorithm":"sha256","ts":"2026-10-05T09:31:11.763Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"0221c1e0bd861e4e03844581","agent_provenance":{"model":{"value":"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-05T09:31:11.763Z","author":"codex-sol","text":"The actual reusable workflow shell now rejects nine independent malicious fixtures on real temporary Git repositories with bare remotes: body token, comment token, email, POSIX path, camel-case secret assignment, date-containing secret assignment, Slack app credential, labelled plain phone, and Windows forward-slash path. The linked gate/import suite completed with exit zero; every fixture verified the expected rule, redacted output, no item file, unchanged remote refs, and no PR receipt. Additional encoding review now rejects untracked NUL/UTF-16 content and invalid UTF-8 rather than silently scanning replacement bytes; 42 Node and native Bun gate cases pass."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-05T09:31:11.763Z"}],"before_hash":"5e9772d2f477a0dc3f557ce27df1b07e09150d5b3fad16bf682bb664a8322f5f","after_hash":"6a4b3b1ebca40f9fec90aedcdc8389497f53e479f9594bde785c2a310ec6d2e9","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"9f1ed2eaa9af402988c23aa2d012961752552f063115f073de40a064761c7f62"} +{"hash_algorithm":"sha256","ts":"2026-10-05T09:55:33.935Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"0221c1e0bd861e4e03844581","agent_provenance":{"model":{"value":"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-05T09:55:33.935Z","author":"codex-sol","text":"Final source ae86e7d passed all 420 npm tests and all 420 native Bun tests with no skips. Exact all-source coverage remains blocked: 8099/8299 lines/statements (97.59 percent), 1859/2083 branches (89.24 percent), and all 200 functions (100 percent) across 12 authored modules; the failing gate removed LCOV. Projects planners reached 100 in all four metrics. Strict typecheck, ESLint, docstrings, zero source duplication, production audit, packing, pm-changelog, release-publish attestation, Git-object privacy, and pinned strict health passed. Latest packed real-data acceptance imported three public pm-todos issues, completed a byte-identical Node repeat, passed privacy over six files with zero findings, and passed strict health in 24.94 seconds; no remote or push. The direct native Bun CLI repeat timed out at 45.06 seconds with unchanged durable bytes, tracked separately in pm-github-hptv. Keep the candidate draft and the coverage/rollout follow-ups open."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-05T09:55:33.935Z"}],"before_hash":"6a4b3b1ebca40f9fec90aedcdc8389497f53e479f9594bde785c2a310ec6d2e9","after_hash":"688959b030c2e94c1ec31f8ac6f7001582fb8dbd270ecb070e36041edc2895ec","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"b6e91b86758038454acce59a48b45986fad2a98cb780b0870d617d8f89666cd0"} +{"hash_algorithm":"sha256","ts":"2026-10-06T11:13:01.253Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"9aa62e730769acb6625de04f","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-06T11:13:01.253Z"},{"op":"replace","path":"/metadata/status","value":"in_progress"}],"before_hash":"688959b030c2e94c1ec31f8ac6f7001582fb8dbd270ecb070e36041edc2895ec","after_hash":"f68b7cd858d6c9968ff1a4f7a6b8e7ccf3de372060502d1efde9f2234c4fd3b6","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"d3b9ba624b1ffa4e77711b5372d3e0e886896020bdc20678d26740556add9ee7"} +{"hash_algorithm":"sha256","ts":"2026-10-06T11:42:31.287Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"9aa62e730769acb6625de04f","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-06T11:42:31.287Z"},{"op":"add","path":"/metadata/unblock_note","value":"Final exact coverage and npm/Bun release checks are underway; PR 108 remains open for review."}],"before_hash":"f68b7cd858d6c9968ff1a4f7a6b8e7ccf3de372060502d1efde9f2234c4fd3b6","after_hash":"2b0f3dd91a2857be40a6bd16e10dccac0c77b0018f46888e1f041a73c50104ee","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"0a8d928f2ec389b81aa2011e94b6792f7a7980696bfe859a0355859b284e9af7"} +{"hash_algorithm":"sha256","ts":"2026-10-06T11:42:32.311Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"9aa62e730769acb6625de04f","agent_provenance":{"model":{"value":"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-06T11:42:32.311Z","author":"codex-sol","text":"Final continuation retains the audited gate and reusable caller implementation. Added real Git/PM/HTTP behavior coverage, a bounded installed-Bun process-group watchdog and reproducible dangling-lock wait-budget fix. Public all-state snapshot correctly refuses older email findings; the clean recent nonempty snapshot passes import, idempotent repeats and strict health without a remote. Source is now frozen for npm release:check and the full native Bun suite; no downstream sync workflow is enabled or dispatched."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T11:42:32.311Z"}],"before_hash":"2b0f3dd91a2857be40a6bd16e10dccac0c77b0018f46888e1f041a73c50104ee","after_hash":"44e925b3f09baf77e2da162e2299fb18d1639a7de1a8f130bccbf92836f411fe","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"e658234c68bc77a35ea6772fa468db7165a13ff3ff867d7b0c2f0cc60fac264a"} +{"hash_algorithm":"sha256","ts":"2026-10-06T11:50:22.874Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"9aa62e730769acb6625de04f","agent_provenance":{"model":{"value":"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-06T11:50:22.874Z","author":"codex-sol","text":"Full npm run release:check PASS on frozen implementation: all 487 tests pass without skips and all 12 authored modules measure exact 100/100/100/100 (8391 lines/statements, 2218 branches, 208 functions). Typecheck, build, docstrings, lint, zero duplication, Git-object privacy, production dependency audit (zero vulnerabilities), package dry-run, changelog and publish attestation all pass. Pinned PM strict health with required merge drivers passes. Native Bun suite and Bun-invoked full release check remain in progress."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T11:50:22.874Z"}],"before_hash":"44e925b3f09baf77e2da162e2299fb18d1639a7de1a8f130bccbf92836f411fe","after_hash":"27d32d634e82e1098b68922da155d144eae0daac7144acb98d74025a33003c41","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"538885b5c7b2e99054922ccbd764f01a2c806578f368f076cfe758627c6e3ecf"} +{"hash_algorithm":"sha256","ts":"2026-10-06T12:13:00.910Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"9aa62e730769acb6625de04f","agent_provenance":{"model":{"value":"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-06T12:13:00.910Z","author":"codex-sol","text":"Full npm and Bun-invoked release:check both PASS on production implementation 5ed8c47: each full run passes 487 tests without failures/skips, exact 100/100/100/100 across all 12 authored modules, and every configured release gate. Counts: 8398 lines/statements, 2219 branches, 208 functions, all covered with zero skipped source. The full native Bun suite is re-running after four test-only mock.method portability corrections; their four focused Node/Bun cases and complete 24-case Node lock suite pass. Packed public acceptance after the transport fix passes nonempty import, gate, unchanged Node/Bun repeats and strict health without a remote."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T12:13:00.910Z"}],"before_hash":"27d32d634e82e1098b68922da155d144eae0daac7144acb98d74025a33003c41","after_hash":"1de7a09376c18a23d28f95fc1be76faed32cc588ba338572bb0bce09da8a7cc3","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"6f2a4e5a5f7f11e3ebbbedbf01eb5d89ec65d96ec3ed2580d6ef5addc5269b18"} +{"hash_algorithm":"sha256","ts":"2026-10-06T12:42:47.528Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"9aa62e730769acb6625de04f","agent_provenance":{"model":{"value":"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-06T12:42:47.528Z","author":"codex-sol","text":"Final acceptance complete: npm run release:check and bun run release:check PASS, final npm coverage PASS at exact 100/100/100/100 for all 12 modules with 487 tests/no skips, and complete native npm run test:bun PASS with 487/487 cases across 30 files and exit zero. Fresh packed public snapshot import, unchanged Node/Bun repeats, privacy gate and pinned strict health pass. Nine malicious executable workflow fixtures stop before item writes, pushes or PR writes; clean workflow push and permanent-link PR creation/update pass at the local writer boundary. Existing draft PR https://github.com/unbraind/pm-github/pull/108 will receive the final receipts; no merge, publication or downstream rollout. Leaving all items open and releasing claims for orchestrator verification."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T12:42:47.528Z"}],"before_hash":"1de7a09376c18a23d28f95fc1be76faed32cc588ba338572bb0bce09da8a7cc3","after_hash":"550919ef8b0fa3dc4e07157e4354f696847f9ae0d623d93adac7e51f3c7db4a7","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"0812a5cf60547f207c72845f31cdc05ddd0d3f3a32a15a48ddfe520fe99abcde"} +{"hash_algorithm":"sha256","ts":"2026-10-06T12:42:50.295Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"9aa62e730769acb6625de04f","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-06T12:42:50.295Z"}],"before_hash":"550919ef8b0fa3dc4e07157e4354f696847f9ae0d623d93adac7e51f3c7db4a7","after_hash":"62cc2a53d6a78d1fc2d6d08013f56f62e6dabdfe09dfe3eedfbc98afa612f9cb","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"f35c22d36c52133ca85e9ae37662754130c39e094d18a8f329c49ee6179f0525"} +{"hash_algorithm":"sha256","ts":"2026-10-06T17:04:29.518Z","author":"claude-orchestrator","author_source":"asserted","agent_harness":"claude-code","agent_model":"claude-opus-5-5","agent_model_source":"probe","agent_instance":"1c3d835edba106c1302b3106","agent_provenance":{"model":{"value":"claude-opus-5-5","source":"probe"},"effort":{"value":"high","source":"environment"},"role":{"value":"implementer","source":"argv"},"topic":null,"version":{"value":"2.1.291","source":"probe"}},"op":"update","patch":[{"op":"replace","path":"/metadata/unblock_note","value":"Coverage blocker pm-github-9cjx is closed (exact 100/100/100/100 verified); the feature is complete on PR 108 and waits only for review and merge."},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T17:04:29.518Z"}],"before_hash":"62cc2a53d6a78d1fc2d6d08013f56f62e6dabdfe09dfe3eedfbc98afa612f9cb","after_hash":"c66c3ede3ff09be1c1a1d773d0a631577543c318e5d384b32de78562d461399d","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":"maintenance","record_hash_version":1,"record_hash":"bc3ebd2135a05ede7ceec0bd9d35f8172c1e84967a9168a1cfb6a85aa607be16"} diff --git a/.agents/pm/history/pm-github-hptv.jsonl b/.agents/pm/history/pm-github-hptv.jsonl new file mode 100644 index 00000000..e9695fa5 --- /dev/null +++ b/.agents/pm/history/pm-github-hptv.jsonl @@ -0,0 +1,111 @@ +{"hash_algorithm":"sha256","ts":"2026-10-05T08:28:54.625Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"0221c1e0bd861e4e03844581","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-hptv"},{"op":"add","path":"/metadata/title","value":"Bound native Bun CLI repeats of a completed public atomic import"},{"op":"add","path":"/metadata/description","value":"The final packed candidate imported three public issues and passed its Node repeat, privacy gate, and strict health. A direct native Bun CLI repeat remained active with substantial CPU use and was terminated after a bounded acceptance run; tracker bytes stayed unchanged. Earlier native Bun acceptance and library harness tests passed, so the installed-host boundary needs a minimal synthetic reproduction before assigning an upstream cause. Keep this separate from passing Node workflow safety and from the all-source coverage blocker."},{"op":"add","path":"/metadata/type","value":"Issue"},{"op":"add","path":"/metadata/status","value":"open"},{"op":"add","path":"/metadata/priority","value":1},{"op":"add","path":"/metadata/tags","value":[]},{"op":"add","path":"/metadata/created_at","value":"2026-10-05T08:28:54.625Z"},{"op":"add","path":"/metadata/updated_at","value":"2026-10-05T08:28:54.625Z"},{"op":"add","path":"/metadata/author","value":"codex-sol"},{"op":"add","path":"/metadata/files","value":[{"path":"index.ts","scope":"project"},{"path":"test/import-gate.test.ts","scope":"project"}]}],"before_hash":"3cc22dff72be7b14824654a7a64ea62b04799939b2fee54c1b5f52ca60bf6df0","after_hash":"056c93385f993ef6eb7da2d726cb82ff7bf23831ac63a1560af2e799307de2f8","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":"9082965b4e00f207c2cd51ff347ed3dc9bd73d55620acabe8a6d58737480af9c"} +{"hash_algorithm":"sha256","ts":"2026-10-05T09:07:19.298Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"0221c1e0bd861e4e03844581","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-05T09:07:19.298Z"},{"op":"add","path":"/metadata/comments","value":[{"created_at":"2026-10-05T09:07:19.298Z","author":"codex-sol","text":"A fresh bounded native Bun CLI repeat of the latest packed candidate passed in 4.22 seconds with exit zero and byte-identical tracker contents. This contrasts with the earlier long-running repeat, so retain the issue as an intermittent installed-host investigation rather than claiming a consistent Bun failure. All 375 native Bun library and operational tests also pass."}]}],"before_hash":"056c93385f993ef6eb7da2d726cb82ff7bf23831ac63a1560af2e799307de2f8","after_hash":"23e7ad9d5d66669f8ed4a7a8e6afe32368855ccbde09873e3dc552cc3fff42a1","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"3e7af639ea88532c4e197868e838f08506c083b7c771d8ccce440fc2c8dc6262"} +{"hash_algorithm":"sha256","ts":"2026-10-05T09:55:33.050Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"0221c1e0bd861e4e03844581","agent_provenance":{"model":{"value":"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-05T09:55:33.050Z","author":"codex-sol","text":"The final packed candidate matched all three authored runtime modules byte-for-byte. Its fresh Node public import passed with three issues, a byte-identical second run, zero gate findings, and strict health in 24.94 seconds. A bounded direct native Bun CLI repeat, using the same pinned host and startup executable path, timed out at 45.06 seconds and was terminated with SIGTERM; durable tracker bytes stayed unchanged. This reproduces the intermittent installed-host boundary alongside all 420 passing native Bun library and operational tests. No scratch remote was configured and no public-data push occurred."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-05T09:55:33.050Z"}],"before_hash":"23e7ad9d5d66669f8ed4a7a8e6afe32368855ccbde09873e3dc552cc3fff42a1","after_hash":"61df9b6599e3139b84900318ac8bac536c89987f41e68d9e54475f64ba6bbe02","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"7b56497a19230648b6d804ce40812fc10edaf668fe4e106a550be2bea3485779"} +{"hash_algorithm":"sha256","ts":"2026-10-06T10:08:33.900Z","author":"pi-glm","author_source":"asserted","agent_harness":"pi","agent_model":"glm-5.3:cloud","agent_model_source":"environment","agent_instance":"42a57847eb1811c7ac8ce340","agent_provenance":{"model":{"value":"glm-5.3:cloud","source":"environment"},"effort":null,"role":{"value":"implementer","source":"argv"},"topic":null},"op":"claim","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T10:08:33.900Z"},{"op":"add","path":"/metadata/assignee","value":"pi-glm"},{"op":"add","path":"/metadata/claim_principal","value":"pi-glm"}],"before_hash":"61df9b6599e3139b84900318ac8bac536c89987f41e68d9e54475f64ba6bbe02","after_hash":"5a96e422ce015d9cf60d8b45936032c502fcc287847890e39dd4ac18886728db","item_hash_version":3,"event_class":"substantive","record_hash_version":1,"record_hash":"ea94086887f33135e18d18b7ac57725969c2ce25d8e41ff316289e219c6ac457"} +{"hash_algorithm":"sha256","ts":"2026-10-06T10:08:35.002Z","author":"pi-glm","author_source":"asserted","agent_harness":"pi","agent_model":"glm-5.3:cloud","agent_model_source":"environment","agent_instance":"42a57847eb1811c7ac8ce340","agent_provenance":{"model":{"value":"glm-5.3:cloud","source":"environment"},"effort":null,"role":{"value":"implementer","source":"argv"},"topic":null},"op":"update","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T10:08:35.002Z"},{"op":"replace","path":"/metadata/status","value":"in_progress"}],"before_hash":"5a96e422ce015d9cf60d8b45936032c502fcc287847890e39dd4ac18886728db","after_hash":"2cbd0c7a86caa29fe4d661494f0e69116f3c377e83e82aaea0bbc591f1ad3cc4","item_hash_version":3,"event_class":"substantive","record_hash_version":1,"record_hash":"a20c9852748affd21b3a504225f19cfcc18d9b17f7ba894147288ee91c17e2d1"} +{"hash_algorithm":"sha256","ts":"2026-10-06T11:13:00.383Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"9aa62e730769acb6625de04f","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/claim_principal","value":"codex-sol"},{"op":"replace","path":"/metadata/assignee","value":"codex-sol"},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T11:13:00.383Z"}],"before_hash":"2cbd0c7a86caa29fe4d661494f0e69116f3c377e83e82aaea0bbc591f1ad3cc4","after_hash":"2562b7358234a6b183d588e76b68a79fc643b61a40dcbfb53495c78a005ac82a","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"97508e4dd7196f0df9f91b56acd4a435db1088a5e96c86c6c6daaa558fde7832"} +{"hash_algorithm":"sha256","ts":"2026-10-06T11:17:41.297Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"9aa62e730769acb6625de04f","agent_provenance":{"model":{"value":"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-06T11:17:41.296Z","author":"codex-sol","text":"Packed installed-CLI reproduction with the pinned host passes three consecutive native Bun repeats under the unchanged 45-second watchdog. The complete fixture test took 26.63 seconds, including packing, installation, Node import, and all three repeats; each retained item bytes and unchanged bare-remote refs. Separately reproduced a real dangling comment-lock symlink: open with exclusive creation returns EEXIST while stat returns ENOENT, causing the old immediate-continue path to busy-spin forever. The preserved fix adds sleep and checks the existing wait budget. This is a proven local lock defect, not yet proof of the historical installed-host timeout cause. Further public-data and diagnostic acceptance follows."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T11:17:41.297Z"}],"before_hash":"2562b7358234a6b183d588e76b68a79fc643b61a40dcbfb53495c78a005ac82a","after_hash":"d0e904bca0fc89a6b8b8449bd682bb8e9f6a0ee3fcd8710bc6bd94d7ec2c8ce8","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"fd13f558e191dda50294884d84ecc90ac93cd08f8c0debb0c1d0acca262d6fa8"} +{"hash_algorithm":"sha256","ts":"2026-10-06T11:24:00.902Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"9aa62e730769acb6625de04f","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"files_add","patch":[{"op":"add","path":"/metadata/files/2","value":{"path":"test/import-lock.test.ts","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T11:24:00.902Z"}],"before_hash":"d0e904bca0fc89a6b8b8449bd682bb8e9f6a0ee3fcd8710bc6bd94d7ec2c8ce8","after_hash":"dc520f4d67ddfaf05cdf5a8b017dcef9985b287402300f1251acc9a6d87da1f4","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"b751a6129c828edc6fa2a6cd07904073a591355fdd306e08484fe12885715263"} +{"hash_algorithm":"sha256","ts":"2026-10-06T11:33:55.510Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"9aa62e730769acb6625de04f","agent_provenance":{"model":{"value":"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-06T11:33:55.510Z","author":"codex-sol","text":"Fresh live public snapshot acceptance passed: one non-PR pm-todos issue updated since 2026-10-01, fetched with paginated gh reads, replayed unchanged through the packed candidate HTTP client and pinned host. Node import 2.49 seconds; byte-identical Node repeat 2.36 seconds; three byte-identical native Bun repeats 2.28, 2.03, and 2.33 seconds. Privacy PASS, strict health PASS, no remote configured. The complete all-state live import separately refused two email findings in older issue content before mutation, as required; no allowlist was added. Direct external HTTPS probes were intermittent, so the public snapshot receipt separates live retrieval from local installed-host execution. No deterministic SDK comment-add flakiness has reproduced; real create/comment/deduplication and permission-failure tests pass."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T11:33:55.510Z"}],"before_hash":"dc520f4d67ddfaf05cdf5a8b017dcef9985b287402300f1251acc9a6d87da1f4","after_hash":"4d8e310e6bb5d1796eeee6202f5c73551e9c8d15dec91de31576dbc059f7602c","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"83310a9b92afab48c884ab68517edc5611ddf38585c14273ca96c687671c861d"} +{"hash_algorithm":"sha256","ts":"2026-10-06T11:37:04.863Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"9aa62e730769acb6625de04f","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-06T11:37:04.863Z"},{"op":"add","path":"/metadata/tests","value":[{"command":"node --test test/bounded-cli.test.ts","scope":"project","timeout_seconds":60,"provenance":{"author":"codex-sol","created_at":"2026-10-06T11:37:04.782Z","source_kind":"local_mutation","source_ref":"feat/import-privacy-gate"}}]}],"before_hash":"4d8e310e6bb5d1796eeee6202f5c73551e9c8d15dec91de31576dbc059f7602c","after_hash":"745972402921ffa41ece7d975d54afdd180cf4594bd0061893346b3c95d3bb15","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"764a2e1b6fd5c7ddf8ee5b62fbaf13daa78575f81ade9e0bccc8b678058e9f81"} +{"hash_algorithm":"sha256","ts":"2026-10-06T11:37:08.553Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"9aa62e730769acb6625de04f","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/2/path","value":"test/import-gate.test.ts"},{"op":"replace","path":"/metadata/files/1/path","value":"test/helpers/bounded-cli.ts"},{"op":"add","path":"/metadata/files/3","value":{"path":"test/import-lock.test.ts","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T11:37:08.553Z"}],"before_hash":"745972402921ffa41ece7d975d54afdd180cf4594bd0061893346b3c95d3bb15","after_hash":"b759439e5fae55dc3db79c6102ff59d6d259e5aa47288d2fc5287d56ae4e6005","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"7ed6096954e5bd7e9271542c133f4fcc3133c5bd1c8314fd487d0ac5b8d0f2b0"} +{"hash_algorithm":"sha256","ts":"2026-10-06T11:37:09.481Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"9aa62e730769acb6625de04f","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/import-gate.test.ts"},{"op":"replace","path":"/metadata/files/2/path","value":"test/helpers/public-acceptance.ts"},{"op":"add","path":"/metadata/files/4","value":{"path":"test/import-lock.test.ts","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T11:37:09.481Z"}],"before_hash":"b759439e5fae55dc3db79c6102ff59d6d259e5aa47288d2fc5287d56ae4e6005","after_hash":"4a3ffe6839f71f58dd59e1e75d4fb968e193a6bc4a6199a97c25579e8986ec97","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"bbd22e51eadc5ed35bad5d6411fa348a5dd7fde4cee354308474e9a1c8f00e8e"} +{"hash_algorithm":"sha256","ts":"2026-10-06T11:37:10.437Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"9aa62e730769acb6625de04f","agent_provenance":{"model":{"value":"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-06T11:37:10.437Z","author":"codex-sol","text":"Acceptance now uses a shared process-group watchdog: the original 45-second deadline sends SIGTERM, then a separate five-second cleanup window sends SIGKILL. A timed-out command is always rejected, even if it exits zero afterward, and no writer step follows. The deterministic negative control uses a real CPU-spinning child that ignores SIGTERM; the watchdog force-kills it with unchanged durable fixture bytes. This bounds the historical installed-host timeout without claiming the dangling-lock defect was its original cause or increasing the execution deadline. The CLI lock defect itself now honors its existing wait budget."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T11:37:10.437Z"}],"before_hash":"4a3ffe6839f71f58dd59e1e75d4fb968e193a6bc4a6199a97c25579e8986ec97","after_hash":"79294125e67c5a9b29efc07c344f1d3fcc18d065b46151bdd01704caa8c0bbea","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"b06f9d072cc5ffa1add0696099e91ce937560c855d92cfd3cc045ed476b52cfc"} +{"hash_algorithm":"sha256","ts":"2026-10-06T11:42:28.704Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"9aa62e730769acb6625de04f","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-06T11:42:28.704Z"},{"op":"replace","path":"/metadata/description","value":"The intermittent historical installed-Bun repeat has not reproduced in three fresh packed repeats and three live-public-snapshot repeats. A real dangling comment-lock symlink independently reproduced an unbounded busy loop, fixed by honoring the existing wait budget. Installed acceptance retains its 45-second execution deadline and now force-kills the process group after a separate five-second cleanup window; timeout always fails even on a later zero exit. Recent repeats preserve durable bytes and strict health. The historical root cause remains unestablished."}],"before_hash":"79294125e67c5a9b29efc07c344f1d3fcc18d065b46151bdd01704caa8c0bbea","after_hash":"45481d6d80d753d675c8f864244f0848436144fb3e11d154dbe04c955e355930","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"825852c99b1ccc65c10dd8b6723752a0fd5e8c5ccced849409b319bc27002533"} +{"hash_algorithm":"sha256","ts":"2026-10-06T11:43:19.195Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"9aa62e730769acb6625de04f","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-06T11:43:19.195Z"},{"op":"add","path":"/metadata/docs","value":[{"path":"README.md","scope":"project"}]}],"before_hash":"45481d6d80d753d675c8f864244f0848436144fb3e11d154dbe04c955e355930","after_hash":"782479376d4d1bb468ca4bb99fe6cc3d6a9f1c4db238e16fa5391f462ee2178a","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"fe2e65b5052ab437613e3b90353c1e8684011b84596ea3b0393b836b1dbe8734"} +{"hash_algorithm":"sha256","ts":"2026-10-06T11:44:34.944Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"9aa62e730769acb6625de04f","agent_provenance":{"model":{"value":"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-06T11:44:34.944Z","author":"codex-sol","text":"Final frozen packed acceptance passes: one live public snapshot issue imported with gate PASS; Node import 4.02 seconds and byte-identical Node repeat 4.33 seconds; three native Bun repeats 3.36, 3.47 and 4.18 seconds with gate PASS and unchanged durable tracker bytes. Strict health passes in the disposable pinned-host project, with no remote configured. The spinning-child watchdog negative control also passes natively under Bun. Historical timeout cause remains unestablished; no deterministic SDK comment-add flakiness reproduced."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T11:44:34.944Z"}],"before_hash":"782479376d4d1bb468ca4bb99fe6cc3d6a9f1c4db238e16fa5391f462ee2178a","after_hash":"2bb3d24b6d9ceafdb7503833c23293c0c1c66fdd33bb93d91639753907f051c7","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"56187f5d5abd085494a5e89cf0adfc2270585a9a7b376c843186dc925b0703b6"} +{"hash_algorithm":"sha256","ts":"2026-10-06T11:52:11.132Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"9aa62e730769acb6625de04f","agent_provenance":{"model":{"value":"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-06T11:52:11.132Z","author":"codex-sol","text":"Native Bun full-suite negative control found a real HTTP boundary failure: a local server accepts the connection and never responds; requestOnce relies on ClientRequest.setTimeout(30000), but under installed Bun 1.3.5 no timeout rejection arrives before the existing 45-second test deadline. The identical full Node case passes. A fresh focused native reproduction is running. Stopped the superseded Bun release check so the transport fix can be verified on frozen source. This is an independently observed request-timeout defect, not proof of the historical public repeat cause."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T11:52:11.132Z"}],"before_hash":"2bb3d24b6d9ceafdb7503833c23293c0c1c66fdd33bb93d91639753907f051c7","after_hash":"cbc1d0c6464da6acf59ae56f2a0b5eafa40bf4a704b22743f2c2980b0905a63a","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"20cfbc6b0da424aa4ee10fe92bbc08202f267360b7cb25be372a0a0fc31bb476"} +{"hash_algorithm":"sha256","ts":"2026-10-06T11:53:54.456Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"9aa62e730769acb6625de04f","agent_provenance":{"model":{"value":"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-06T11:53:54.456Z","author":"codex-sol","text":"Focused native Bun before-fix reproduction fails deterministically: one local silent-server test reaches 45.00 seconds without the 30-second socket-timeout rejection (38 unrelated tests filtered). Replacing reliance on ClientRequest.setTimeout with an explicit 30-second wall-clock timer, cleared on Promise settlement; the timer destroys the request and rejects directly. Regression now exercises two real responses concurrently: no headers, and headers plus a partial body that never completes. No execution deadline or test timeout was raised. Node and native Bun focused validation is underway."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T11:53:54.456Z"}],"before_hash":"cbc1d0c6464da6acf59ae56f2a0b5eafa40bf4a704b22743f2c2980b0905a63a","after_hash":"38de0c4df2daf3e5e2dafbb369523c5bde621428a27178d5393f5fcaef566366","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"848fcbc3a7fd65f99c3d0d206a1e0b7aa5c6553780e0cdf1d142c41d2fd124a1"} +{"hash_algorithm":"sha256","ts":"2026-10-06T11:53:55.312Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"9aa62e730769acb6625de04f","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/4/path","value":"test/import-gate.test.ts"},{"op":"replace","path":"/metadata/files/3/path","value":"test/http-boundary.test.ts"},{"op":"add","path":"/metadata/files/5","value":{"path":"test/import-lock.test.ts","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T11:53:55.312Z"}],"before_hash":"38de0c4df2daf3e5e2dafbb369523c5bde621428a27178d5393f5fcaef566366","after_hash":"ef7d42ae2ee15dafb14382a22484c4909f58bdfa9a5459d495bc650a5ed55129","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"a2a75878b61ea57d3cc61a6e860e934915b6b6b14c88be38925250f291ea93d9"} +{"hash_algorithm":"sha256","ts":"2026-10-06T11:55:13.799Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"9aa62e730769acb6625de04f","agent_provenance":{"model":{"value":"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-06T11:55:13.799Z","author":"codex-sol","text":"The expanded native regression also caught a second concrete behavior: destroying a partial-response request before rejecting can synchronously emit response end under Bun, resolving the incomplete response instead of rejecting. The deadline callback now rejects first, then destroys. The real silent-server and never-ending partial-body cases test that ordering at the original 30-second limit. Node passes the expanded case; final native focused run and complete gates are being reverified. These are transport-boundary observations, not an assertion about the historical public-data repeat."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T11:55:13.799Z"}],"before_hash":"ef7d42ae2ee15dafb14382a22484c4909f58bdfa9a5459d495bc650a5ed55129","after_hash":"fbcbcde97aaff13ff1fdbe8bba856a0e6c806910e34b118ae936c42e6511228c","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"ce741e43ceb74d8c4eae154011c3197369385b3776b38419f4d3680d8c64a1f6"} +{"hash_algorithm":"sha256","ts":"2026-10-06T11:55:38.455Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"9aa62e730769acb6625de04f","agent_provenance":{"model":{"value":"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-06T11:55:38.455Z","author":"codex-sol","text":"After-fix focused real-HTTP regression PASS under both runtimes: Node 30.02 seconds, native Bun 30.02 seconds; both silent headers and partial never-ending body reject with the expected 30-second timeout. The test keeps its 45-second harness deadline and asserts completion below 40 seconds. Build:test and ESLint pass. Explicit deadline rejection precedes destroy and its timer is cleared on every Promise settlement. Restarting full npm release checks and all native Bun files on this frozen fix."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T11:55:38.455Z"}],"before_hash":"fbcbcde97aaff13ff1fdbe8bba856a0e6c806910e34b118ae936c42e6511228c","after_hash":"5ed20f17e6b0acc2742da56979db2527f0b5df6cf4118a7b48968cd5bce70d70","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"40d31eee91d28e4713535f8b947f70f97fe337751f3b405b3e011fdc9eb52e9a"} +{"hash_algorithm":"sha256","ts":"2026-10-06T11:56:36.983Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"9aa62e730769acb6625de04f","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-name-pattern=\"requestOnce fails a request\" test/http-boundary.test.ts","scope":"project","timeout_seconds":60,"provenance":{"author":"codex-sol","created_at":"2026-10-06T11:56:36.862Z","source_kind":"local_mutation","source_ref":"feat/import-privacy-gate"}}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T11:56:36.983Z"}],"before_hash":"5ed20f17e6b0acc2742da56979db2527f0b5df6cf4118a7b48968cd5bce70d70","after_hash":"31bb47f0db1d9652ed6b542a35d1b56d9bdd3ca26058a44472e24d02d63d5353","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"6ea5786b0f2ba13545767e179e35028745a22080cbbec6aaba601e1d9d087611"} +{"hash_algorithm":"sha256","ts":"2026-10-06T11:58:06.723Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"9aa62e730769acb6625de04f","agent_provenance":{"model":{"value":"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-06T11:58:06.722Z","author":"codex-sol","text":"Packed acceptance on transport-fix implementation 5ed8c47 PASS: one live public snapshot issue imported with gate PASS, Node import 6.17 seconds, byte-identical Node repeat 5.15 seconds, native Bun repeats 6.06/8.16/4.80 seconds with byte-identical durable data, strict health PASS and no remote. Timings include concurrent full validation load. The linked real timeout regression passes both isolated add verification and pm test --run --only-last --progress; it is recorded alongside the passing watchdog test. No SDK comment-add flakiness reproduced."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T11:58:06.723Z"}],"before_hash":"31bb47f0db1d9652ed6b542a35d1b56d9bdd3ca26058a44472e24d02d63d5353","after_hash":"d94aea5934884f8cfc724d3facee556f195cc6c68df5fbf9dffb7f3a5c893dc4","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"21c1c64431d6816f6a6c5dbf08bd73d911e64430e19f9cec7ce754981326df09"} +{"hash_algorithm":"sha256","ts":"2026-10-06T12:42:46.684Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"9aa62e730769acb6625de04f","agent_provenance":{"model":{"value":"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-06T12:42:46.684Z","author":"codex-sol","text":"Final timeout assurance: full native suite passes 487/487 across 30 files with exit zero; both full release commands pass. The real silent and never-ending-body HTTP controls reject at the original 30-second deadline under Node and Bun; the dangling comment lock honors its existing wait budget. Packed public acceptance after the transport fix passes one nonempty import, unchanged Node and three Bun repeats, gate and strict health without a remote. Historical 45-second repeat cause remains unestablished, but acceptance keeps 45 seconds plus a separate forced-cleanup window and always rejects a timeout. No deterministic SDK comment-add flakiness reproduced."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T12:42:46.684Z"}],"before_hash":"d94aea5934884f8cfc724d3facee556f195cc6c68df5fbf9dffb7f3a5c893dc4","after_hash":"3a240ed5c657ad1393811560e4c11357f61dbcb7309e24652d97a030eceb447c","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"bf9c1672e15b9d33c35011533c63a43375ecdae2ca8e0239cb1e8c6b8f07cb15"} +{"hash_algorithm":"sha256","ts":"2026-10-06T12:42:49.423Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"9aa62e730769acb6625de04f","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-06T12:42:49.423Z"}],"before_hash":"3a240ed5c657ad1393811560e4c11357f61dbcb7309e24652d97a030eceb447c","after_hash":"aa279e06fcf2a723904601a97bec3be0e0afe41d82d1bd939d86da3c85e644c0","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"96710163c745b2ac7536fd99ef95596e72d00d04667e40b4c5fd26fe34c127a0"} +{"hash_algorithm":"sha256","ts":"2026-10-06T12:53:55.835Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"d37aec6051aa0465fe475819","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-06T12:53:55.835Z"},{"op":"add","path":"/metadata/assignee","value":"codex-sol"},{"op":"add","path":"/metadata/claim_principal","value":"codex-sol"}],"before_hash":"aa279e06fcf2a723904601a97bec3be0e0afe41d82d1bd939d86da3c85e644c0","after_hash":"a45fb7085d6e59ecd594dc87f0d989afef7fc01692d66e8d725edeffd6fd08af","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"ee7227c186f0ef82a559f8bf5881ffc2482aa1411971dea9ec92bc1c65493906"} +{"hash_algorithm":"sha256","ts":"2026-10-06T12:53:57.413Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"d37aec6051aa0465fe475819","agent_provenance":{"model":{"value":"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-06T12:53:57.413Z","author":"codex-sol","text":"Investigating PR #108 clone-only workflow setup failures. Plan: reproduce in a scratch clone, trace the npm lifecycle trigger and Git layout difference, isolate package preparation from the fixture tracker, then verify regression and full release gates in both layouts."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T12:53:57.413Z"}],"before_hash":"a45fb7085d6e59ecd594dc87f0d989afef7fc01692d66e8d725edeffd6fd08af","after_hash":"f597f3b9a49645e3596699b5b21bf550a0157da65112abc29da5ac00e2fe85a5","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"2e81943b829b4e459c69c89bf2fb5049be7bdb051121285eb75945b0abce3c8e"} +{"hash_algorithm":"sha256","ts":"2026-10-06T12:56:15.328Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"d37aec6051aa0465fe475819","agent_provenance":{"model":{"value":"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-06T12:56:15.328Z","author":"codex-sol","text":"Reproduced the reported failure in a fresh clone with Node 22 and npm 10.9.8 after building. The failing command is npm pack --ignore-scripts in prepareWorkflowFixture, before pm package install. npm 10 pacote runs prepare despite ignore-scripts; npm 11 checks ignoreScripts and the same clone passes. The fixture passes its external PM_PATH into the package-root hook. Testing the worktree with npm 10 to verify whether Git layout is causal."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T12:56:15.328Z"}],"before_hash":"f597f3b9a49645e3596699b5b21bf550a0157da65112abc29da5ac00e2fe85a5","after_hash":"5ad9aaf869abe5422dee8034011523a98e25cf53b5710bb81bf741b175a81135","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"a5a2154230c34134a3452170237a35a01637d2a6b451d4367710f39107159e5b"} +{"hash_algorithm":"sha256","ts":"2026-10-06T12:57:10.585Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"d37aec6051aa0465fe475819","agent_provenance":{"model":{"value":"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-06T12:57:10.585Z","author":"codex-sol","text":"Confirmed npm 10 also fails in the package worktree with an external PM_PATH; Git layout and preinstalled merge drivers are not causal. The fix binds package packing to the package-local tracker and leaves caller import commands on their fixture tracker. Added a regression that forces the real prepare hook before packing in a fresh clone and a linked worktree, so npm 11 cannot mask the bug. The reusable caller workflow does not pack this source checkout and its npm ci/merge install run with its own repository tracker."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T12:57:10.585Z"}],"before_hash":"5ad9aaf869abe5422dee8034011523a98e25cf53b5710bb81bf741b175a81135","after_hash":"f9770a62d68e1c460792c6cdaa4105a33a86d6bcac964db1f0ebc1958045286c","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"b2b78bbfbd6f6ac72995c0f37657a2e77b16a818dedfa01d0fa4af21dd26e102"} +{"hash_algorithm":"sha256","ts":"2026-10-06T12:57:13.263Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"d37aec6051aa0465fe475819","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=\"workflow packing isolates\" test/import-gate.test.ts","scope":"project","provenance":{"author":"codex-sol","created_at":"2026-10-06T12:57:13.194Z","source_kind":"local_mutation","source_ref":"feat/import-privacy-gate"}}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T12:57:13.263Z"}],"before_hash":"f9770a62d68e1c460792c6cdaa4105a33a86d6bcac964db1f0ebc1958045286c","after_hash":"70e577ced12c43465062dbd6878e216b73b270b8807498dd622f2fc41e33435f","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"a5cc8290571c1e4cb65f8f8c116403b610ee6a45b6936ad8a77d28b244ac37c4"} +{"hash_algorithm":"sha256","ts":"2026-10-06T12:57:14.645Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"d37aec6051aa0465fe475819","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=\"executable reusable workflow|packed installed CLI\" test/import-gate.test.ts","scope":"project","provenance":{"author":"codex-sol","created_at":"2026-10-06T12:57:14.565Z","source_kind":"local_mutation","source_ref":"feat/import-privacy-gate"}}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T12:57:14.645Z"}],"before_hash":"70e577ced12c43465062dbd6878e216b73b270b8807498dd622f2fc41e33435f","after_hash":"0cbfe9c282091414a3b7218578129c60b546fef91c452a7cf3956fea70f963d9","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"21de7d55a9580397334dfc76cd882f2d7f240390359a93a8d2000a4d42f5b8b6"} +{"hash_algorithm":"sha256","ts":"2026-10-06T12:58:11.661Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"d37aec6051aa0465fe475819","agent_provenance":{"model":{"value":"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-06T12:58:11.661Z","author":"codex-sol","text":"Regression verified red/green: removing only the package-local PM_PATH override fails with the reported outside-repository error; restoring it passes. pm test pm-github-hptv --run --match 'workflow packing isolates' --progress passed. ESLint and test TypeScript build passed. Running all three originally failing acceptance tests plus the layout regression under npm 10 in the worktree before pushing."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T12:58:11.661Z"}],"before_hash":"0cbfe9c282091414a3b7218578129c60b546fef91c452a7cf3956fea70f963d9","after_hash":"1069eac86994a30a51bd29a98b09aa141a4c397e335335a39d34f5d9dbbecc1a","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"424d03341edef447947bacaa21cdc394777f60b99163cb3a282eed963c49c57d"} +{"hash_algorithm":"sha256","ts":"2026-10-06T12:58:12.794Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"d37aec6051aa0465fe475819","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"tests_add","patch":[{"op":"add","path":"/metadata/tests/4","value":{"command":"npm run release:check","scope":"project","provenance":{"author":"codex-sol","created_at":"2026-10-06T12:58:12.756Z","source_kind":"local_mutation","source_ref":"feat/import-privacy-gate"}}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T12:58:12.794Z"}],"before_hash":"1069eac86994a30a51bd29a98b09aa141a4c397e335335a39d34f5d9dbbecc1a","after_hash":"9a13a971accdf03278a334ddc8d1c7e98197e5886d4dc859e4040548cda86676","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"0166972c6e2e3cdb7607c6ee93684f55930fdfef09738efa2fb2df79e12ee7d7"} +{"hash_algorithm":"sha256","ts":"2026-10-06T12:58:13.691Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"d37aec6051aa0465fe475819","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"tests_add","patch":[{"op":"add","path":"/metadata/tests/5","value":{"command":"bun run release:check","scope":"project","provenance":{"author":"codex-sol","created_at":"2026-10-06T12:58:13.623Z","source_kind":"local_mutation","source_ref":"feat/import-privacy-gate"}}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T12:58:13.691Z"}],"before_hash":"9a13a971accdf03278a334ddc8d1c7e98197e5886d4dc859e4040548cda86676","after_hash":"e2a43e1c532dabe9c38e57f60f965fe1fc3a818242fa17045907fb4d04db6a0f","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"699f13d5e2b5900fc4f56c24938bcdeef524eaab70a14d4e681c435c4a6d5454"} +{"hash_algorithm":"sha256","ts":"2026-10-06T12:59:01.408Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"d37aec6051aa0465fe475819","agent_provenance":{"model":{"value":"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-06T12:59:01.408Z","author":"codex-sol","text":"Both layout regressions fail on reverting the PM_PATH override, including the linked worktree with the pinned CLI. npm 10 exposed a second packing detail after fixing the root mismatch: successful prepare stdout contaminates npm pack --json by default. Packing now explicitly uses --foreground-scripts=false to preserve the JSON receipt; the regression also supplies a conflicting inherited foreground-scripts setting."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T12:59:01.408Z"}],"before_hash":"e2a43e1c532dabe9c38e57f60f965fe1fc3a818242fa17045907fb4d04db6a0f","after_hash":"b064af18eac1e18f7be676c8fa89d2e19fb5a35d63a1f3b3bf554d7f640016bc","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"457f5950470c7e3d6e8cbbf0ce2897617fc04340734610d6738932bdef861256"} +{"hash_algorithm":"sha256","ts":"2026-10-06T13:04:14.167Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"d37aec6051aa0465fe475819","agent_provenance":{"model":{"value":"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-06T13:04:14.167Z","author":"codex-sol","text":"Worktree acceptance under Node 22.23.1/npm 10.9.8 passed all four selected tests: clone/worktree prepare isolation, all adversarial workflow cases, clean create/update review workflow, and the packed native Bun repeat with all three repeats inside 45 seconds. The complete selected run finished in 248.81 seconds under concurrent validation load. Implementation fd0834a is pushed; fresh-clone release gates are in progress."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T13:04:14.167Z"}],"before_hash":"b064af18eac1e18f7be676c8fa89d2e19fb5a35d63a1f3b3bf554d7f640016bc","after_hash":"97cda3f189bc10ad3bac30d868a55e9bec933b65ab59f783f13f3a2197f86d9c","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"5049d0b7f3c3a7efde1eba38b306edfb574a6e0ebf6548be18d5ac7ebe1029be"} +{"hash_algorithm":"sha256","ts":"2026-10-06T13:08:15.645Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"d37aec6051aa0465fe475819","agent_provenance":{"model":{"value":"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-06T13:08:15.645Z","author":"codex-sol","text":"Pushed CI run for fd0834a passes all 488 tests on Node 22 and Node 26, including the three repaired workflow cases. Both jobs still fail the strict coverage gate at 99.86 percent branches: index.ts lines 274, 4092, and 4117 are the successful gh credential-helper paths. Existing coverage depended on local gh authentication. Adding a deterministic synthetic gh auth success test with a mock server to cover these branches without host credentials."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T13:08:15.645Z"}],"before_hash":"97cda3f189bc10ad3bac30d868a55e9bec933b65ab59f783f13f3a2197f86d9c","after_hash":"a72c06fe06d42a7dc69effd5d5b081009e12f02a8c3245d9393b160d62826475","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"70256e724f1f437b3972803c64fc93fd5162d0cd59e20cfbb81408f212e7b4df"} +{"hash_algorithm":"sha256","ts":"2026-10-06T13:08:18.658Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"d37aec6051aa0465fe475819","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"files_add","patch":[{"op":"replace","path":"/metadata/files/5/path","value":"test/import-gate.test.ts"},{"op":"replace","path":"/metadata/files/4/path","value":"test/http-boundary.test.ts"},{"op":"replace","path":"/metadata/files/3/path","value":"test/helpers/public-acceptance.ts"},{"op":"replace","path":"/metadata/files/2/path","value":"test/helpers/bounded-cli.ts"},{"op":"replace","path":"/metadata/files/1/path","value":"test/coverage-edges.test.ts"},{"op":"add","path":"/metadata/files/6","value":{"path":"test/import-lock.test.ts","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T13:08:18.658Z"}],"before_hash":"a72c06fe06d42a7dc69effd5d5b081009e12f02a8c3245d9393b160d62826475","after_hash":"894d1a190ef93c45047dd66adfff0fe560024a13bfbaf156073a2b4d6091b581","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"9685de4b92f76e12df0eb94c478425aaf2dda0a3e7994bb14978222d078b2692"} +{"hash_algorithm":"sha256","ts":"2026-10-06T13:09:02.441Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"d37aec6051aa0465fe475819","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"tests_add","patch":[{"op":"add","path":"/metadata/tests/6","value":{"command":"node --test --test-name-pattern=\"validate borrows a synthetic gh credential\" test/coverage-edges.test.ts","scope":"project","provenance":{"author":"codex-sol","created_at":"2026-10-06T13:09:02.337Z","source_kind":"local_mutation","source_ref":"feat/import-privacy-gate"}}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T13:09:02.441Z"}],"before_hash":"894d1a190ef93c45047dd66adfff0fe560024a13bfbaf156073a2b4d6091b581","after_hash":"b3761104d34a676a057afe4872b7fcd50136a534d31ce54e779558b33e80fec5","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"9b1329b1171ca59a3ac7d1cb43c95848a95f932953b0fcf5e705d8bdba2117c9"} +{"hash_algorithm":"sha256","ts":"2026-10-06T13:09:41.700Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"d37aec6051aa0465fe475819","agent_provenance":{"model":{"value":"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-06T13:09:41.700Z","author":"codex-sol","text":"Deterministic credential-helper regression passes directly and through pm test --run --only-last --progress. It removes both env tokens, shadows gh with a synthetic auth helper, checks trimmed-token Authorization at the mock API, and asserts token_source=gh plus credential redaction. ESLint and build:test pass. Obtained the exact CI Bun 1.3.14 runtime in disposable storage for final fresh-clone gates."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T13:09:41.700Z"}],"before_hash":"b3761104d34a676a057afe4872b7fcd50136a534d31ce54e779558b33e80fec5","after_hash":"4df9c79a1508d9ae6e86150aac04d13e10ca434801ab0123da7da3e519dab8d2","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"e83281162930f282e49ec865072918c68baffedde2230fc01bc9e3779cd8b7a4"} +{"hash_algorithm":"sha256","ts":"2026-10-06T13:18:34.181Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"d37aec6051aa0465fe475819","agent_provenance":{"model":{"value":"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-06T13:18:34.181Z","author":"codex-sol","text":"CI run 37468640976 at a849280 passes test (22) and test (26), including exact all-source coverage and the full native Bun 1.3.14 suite. CodeQL and the security scan also pass. Reviewer draft skips are not approvals. Fresh-clone npm and Bun release checks are still running locally with host GitHub credentials removed."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T13:18:34.181Z"}],"before_hash":"4df9c79a1508d9ae6e86150aac04d13e10ca434801ab0123da7da3e519dab8d2","after_hash":"d28547dc7dc1233461e33cbe09ebdce778832963fa073606095edadb0faf357c","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"a3ac2281e40a6f33f51ff5d95b0b9f8415ddfef24062ffc7183547c1c7e5d2dd"} +{"hash_algorithm":"sha256","ts":"2026-10-06T13:28:14.761Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"d37aec6051aa0465fe475819","agent_provenance":{"model":{"value":"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-06T13:28:14.761Z","author":"codex-sol","text":"Final fresh-clone gates at pushed implementation a849280 PASS: npm run release:check under Node 22.23.1/npm 10.9.8, and bun run release:check under Bun 1.3.14 with Node 26.7.0 tooling. Both clones used an empty gh configuration and no environment GitHub tokens. Each passed 489 tests, exact all-source 100/100/100/100 across 12 modules, zero production audit vulnerabilities, package dry-run, changelog check, and publish attestation. Coverage receipts: 8398/8398 lines and statements in both; Node 22 has 209/209 functions and 2223/2223 branches, Node 26 has 208/208 functions and 2222/2222 branches. Packed native Bun repeats retained the original 45-second execution budget. CI test (22) and test (26) also pass. Only tracker receipts remain to commit; the historical intermittent Bun timeout cause remains outside this setup fix."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T13:28:14.761Z"}],"before_hash":"d28547dc7dc1233461e33cbe09ebdce778832963fa073606095edadb0faf357c","after_hash":"a3ee0100b60c50db9579863c5a1aa3cdbbe1204f6bc1cdb1b035722ab9b0c502","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"c5ad59e7743ebe55edc56dec97540846fb79b774bfc2e2e3453aff3b4cd5b7bc"} +{"hash_algorithm":"sha256","ts":"2026-10-06T13:28:15.458Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"d37aec6051aa0465fe475819","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-06T13:28:15.458Z"}],"before_hash":"a3ee0100b60c50db9579863c5a1aa3cdbbe1204f6bc1cdb1b035722ab9b0c502","after_hash":"6548963e668b8bdb99a514b1846f22189257175cc6457d21741bc9673018a7e2","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"3ec95ecd6210e26f9cb26873ad8eb879465be48dc8bab08465fae0163a5a81e0"} +{"hash_algorithm":"sha256","ts":"2026-10-06T13:52:21.692Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"f688d14d4e1e36891460ce7a","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-06T13:52:21.692Z"},{"op":"add","path":"/metadata/assignee","value":"codex-sol"},{"op":"add","path":"/metadata/claim_principal","value":"codex-sol"}],"before_hash":"6548963e668b8bdb99a514b1846f22189257175cc6457d21741bc9673018a7e2","after_hash":"f0b386b80836d6925bf102133aeec4fff5ff46072f91f2aebb0ddc520e61047f","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"c53976f621841bbd2210515867fdd433c8cde0fdc5b73e6efcf117450d226941"} +{"hash_algorithm":"sha256","ts":"2026-10-06T13:52:24.540Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"f688d14d4e1e36891460ce7a","agent_provenance":{"model":{"value":"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-06T13:52:24.540Z","author":"codex-sol","text":"Review round 1: read all 16 inline findings and both review bodies. Verifying complete comment reads, diff hunk parsing, fatal Git decoding, canonical tracker paths, workflow credentials and pin grammar; adding revert-proven regressions before fresh-clone npm and Bun gates."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T13:52:24.540Z"}],"before_hash":"f0b386b80836d6925bf102133aeec4fff5ff46072f91f2aebb0ddc520e61047f","after_hash":"b79be87799362ab3cd910c1213b3475279ed30b4c0a0d9b0b2dcebb41f213797","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"f34927a46baa2659793355b9017738a96709bab645857797ee65d04f38959249"} +{"hash_algorithm":"sha256","ts":"2026-10-06T13:58:16.973Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"f688d14d4e1e36891460ce7a","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"files_add","patch":[{"op":"replace","path":"/metadata/files/6/path","value":"test/helpers/public-acceptance.ts"},{"op":"replace","path":"/metadata/files/5/path","value":"test/helpers/bounded-cli.ts"},{"op":"replace","path":"/metadata/files/4/path","value":"test/gate-inputs.test.ts"},{"op":"replace","path":"/metadata/files/3/path","value":"test/coverage-edges.test.ts"},{"op":"replace","path":"/metadata/files/2/path","value":"index.ts"},{"op":"replace","path":"/metadata/files/1/path","value":"gate.ts"},{"op":"replace","path":"/metadata/files/0/path","value":".github/workflows/pm-github-sync.yml"},{"op":"add","path":"/metadata/files/7","value":{"path":"test/http-boundary.test.ts","scope":"project"}},{"op":"add","path":"/metadata/files/8","value":{"path":"test/import-gate.test.ts","scope":"project"}},{"op":"add","path":"/metadata/files/9","value":{"path":"test/import-lock.test.ts","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T13:58:16.973Z"}],"before_hash":"b79be87799362ab3cd910c1213b3475279ed30b4c0a0d9b0b2dcebb41f213797","after_hash":"fb83882bda02bba83d0fc9f7a8955ea039420690a83087900f8564bc0403f63a","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"6906a5e31ffbb79ac0dfc3cad9a99ad0819c7b2067f746eee5c2087cd5482a8a"} +{"hash_algorithm":"sha256","ts":"2026-10-06T13:58:19.577Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"f688d14d4e1e36891460ce7a","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"tests_add","patch":[{"op":"add","path":"/metadata/tests/7","value":{"command":"node --test --test-name-pattern=\"diff hunk content|Git refuses|canonicalizes a symlinked|gated comment imports|gated pre-write|workflow input shell|workflow credentials\" test/gate-inputs.test.ts test/import-gate.test.ts","scope":"project","provenance":{"author":"codex-sol","created_at":"2026-10-06T13:58:19.539Z","source_kind":"local_mutation","source_ref":"feat/import-privacy-gate"}}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T13:58:19.577Z"}],"before_hash":"fb83882bda02bba83d0fc9f7a8955ea039420690a83087900f8564bc0403f63a","after_hash":"40a8eeb7b5f0a491da747b88348ef69b6cd02cf8b68f3b41468c9f3dde2328b9","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"2f1c43a4dc84a793ea728213f654ff73bcb3af79e7f096d4faa94363e5962eb1"} +{"hash_algorithm":"sha256","ts":"2026-10-06T13:58:29.574Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"f688d14d4e1e36891460ce7a","agent_provenance":{"model":{"value":"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-06T13:58:29.574Z","author":"codex-sol","text":"All 16 review findings verified valid and fixed. Every finding has a regression with a proven nonzero revert run and passing restored run; the two hunk findings share one proof. Real HTTP malformed and non-array second comment pages refuse both atomic and incremental gated imports with unchanged tracker state. Real tracked/staged invalid UTF-8 and symlinked-checkout cases pass. Credentials are step-scoped, install scripts disabled, checkout persistence disabled, and the branch lease uses checkout refs without a pre-push network read."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T13:58:29.574Z"}],"before_hash":"40a8eeb7b5f0a491da747b88348ef69b6cd02cf8b68f3b41468c9f3dde2328b9","after_hash":"6c12eaebf928b994b07c1545d9723acd6b0a0370712dd55b83872f6f14cef778","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"4be7ccab1acb9be4da0d3da1e804f4f51cdb69a5cbb80e7ab9acf332009f60fd"} +{"hash_algorithm":"sha256","ts":"2026-10-06T14:06:25.566Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"f688d14d4e1e36891460ce7a","agent_provenance":{"model":{"value":"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-06T14:06:25.566Z","author":"codex-sol","text":"Full-suite validation caught a decoder correction: spawnSync can leave stderr undefined as well as null on ENOENT, so the Git reader now checks for an available buffer before fatal decoding and preserves the spawn diagnostic otherwise. The existing real missing-Git subprocess regression passes with the correction; tracked/staged invalid UTF-8 refusal remains passing. Superseded full runs were stopped and will be replaced by fresh-clone runs of the corrected pushed head."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T14:06:25.566Z"}],"before_hash":"6c12eaebf928b994b07c1545d9723acd6b0a0370712dd55b83872f6f14cef778","after_hash":"1e382702569b64456d47206573d211f6da5340a2061ebd56aa082b116d01d145","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"1fff20d813a7a7bd8f54466176d6c7b3afe5664e232ed63ec3cbbbbfa41dd169"} +{"hash_algorithm":"sha256","ts":"2026-10-06T14:10:27.146Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"f688d14d4e1e36891460ce7a","agent_provenance":{"model":{"value":"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-06T14:10:27.146Z","author":"codex-sol","text":"Persistent revert receipts: 4196039888 workflow input shell; 4196039928 malformed second comment HTTP pages; 4196039937 and 4196078374 diff hunk content; 4196039948 tracked/staged invalid UTF-8; 4196078360 workflow credential isolation; 4196078420 symlinked checkout; 4196078439 pre-write command exit code. Each negative control exited 1 after reverting to the reviewed code, and restored code exited 0. The missing-Git follow-up also has an independent failing revert and passing restore. Selected native Bun 1.3.14 gate and workflow regressions pass."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T14:10:27.146Z"}],"before_hash":"1e382702569b64456d47206573d211f6da5340a2061ebd56aa082b116d01d145","after_hash":"81ce336a2dec2b26af0dd81a8daa79246453df0d663443b14bcd8ba54dbb4eb8","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"4485fe9fa76ca2ce8d868c5b7ce81ca24b0b20b2a967bfd9daf01143755632b7"} +{"hash_algorithm":"sha256","ts":"2026-10-06T14:20:34.810Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"f688d14d4e1e36891460ce7a","agent_provenance":{"model":{"value":"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-06T14:20:34.810Z","author":"codex-sol","text":"Corrected implementation c8b9329 passes both fresh-clone release commands with 503/503 tests and no skips: npm under Node 22.23.1/npm 10.9.8, Bun 1.3.14 with Node 26.7.0 tooling. Both used empty gh configuration and no environment GitHub credentials. A new diagnostic assertion initially failed native Bun because empty PATH prevents the tracker read before generator startup; the fixture now separately injects generator startup and signal outcomes after restoring PATH. Node and native Bun pass this portable fixture, and reverting the diagnostic implementation still fails. Final pushed-head clones will reverify the bundled tracker receipts and test correction."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T14:20:34.810Z"}],"before_hash":"81ce336a2dec2b26af0dd81a8daa79246453df0d663443b14bcd8ba54dbb4eb8","after_hash":"8d8a8b3d3145d75b8383758f3bad647d26f78e1128080b71cbfed592ab9e272d","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"096a15f77c32a778145fa5eceddab649cf69b9e8122532b1fd6a6511720b93cb"} +{"hash_algorithm":"sha256","ts":"2026-10-06T14:20:42.267Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"f688d14d4e1e36891460ce7a","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-06T14:20:42.267Z"}],"before_hash":"8d8a8b3d3145d75b8383758f3bad647d26f78e1128080b71cbfed592ab9e272d","after_hash":"23f14ba9da41fad94d20b5d7ca614f641e9326ac9500c8e51d7cc4dbaf9c5ce6","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"4db4085c5a3823ee363b5ba18965ee40b9a8b1115af3cd9fecb0992b2e59428d"} +{"hash_algorithm":"sha256","ts":"2026-10-06T14:33:50.266Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"f688d14d4e1e36891460ce7a","agent_provenance":{"model":{"value":"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-06T14:33:50.266Z","author":"codex-sol","text":"Final source/test head f572382 passes both fresh-clone release commands, 503 tests and exact all-source coverage. Strict clone health with merge drivers passes. Both CI Node jobs and native Bun pass. Local privacy separately audits 3581 objects, including unreachable objects, with approved identities and no secret/host-path findings. Separate upstream PM runner follow-up pm-github-2ao1 records a false empty_run on genuinely passing filtered Bun tests; the full 29-test native command is linked and passes. All 16 round-1 findings are fixed with verified failing revert controls. Items remain open and claims released for orchestrator verification."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T14:33:50.266Z"}],"before_hash":"23f14ba9da41fad94d20b5d7ca614f641e9326ac9500c8e51d7cc4dbaf9c5ce6","after_hash":"1bb3345dce0e959c37c6fcd2c69e0b52c86137d658fdfe033349c677a1cee9b1","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"1e331c9a357688eea2325dde833f318dbfb5da69e7e397df8f521bbd237e807e"} +{"hash_algorithm":"sha256","ts":"2026-10-06T15:17:47.617Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fefbe65803605af722707206","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-06T15:17:47.617Z"},{"op":"add","path":"/metadata/assignee","value":"codex-sol"},{"op":"add","path":"/metadata/claim_principal","value":"codex-sol"}],"before_hash":"1bb3345dce0e959c37c6fcd2c69e0b52c86137d658fdfe033349c677a1cee9b1","after_hash":"e27927d911e5533d5a5823ee8a23aaffcd423f0ea2984f48462e31a5af947e61","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"372459e18759e895bdaee44dcfb66393ddca2a1a76651356540f150bd9d513ad"} +{"hash_algorithm":"sha256","ts":"2026-10-06T15:17:49.785Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fefbe65803605af722707206","agent_provenance":{"model":{"value":"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-06T15:17:49.785Z","author":"codex-sol","text":"Review round 2 of PR #108: preserve published changelog binning through explicit item release metadata, add actionable diagnostics and real-shell rejection assertions for reusable workflow inputs, and strengthen the malicious-import tracker no-write assertion with a temporary injected-write negative control. Verify all release gates in fresh clones of the pushed branch; leave the item open for orchestrator verification."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T15:17:49.785Z"}],"before_hash":"e27927d911e5533d5a5823ee8a23aaffcd423f0ea2984f48462e31a5af947e61","after_hash":"b292e7138db95292e46ba21dca408adb2265313ed0c0563f77f8d705cb4acec8","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"6c647426242e164a4f77fd190a087f85d0005e3b9de36741253210550c45a66a"} +{"hash_algorithm":"sha256","ts":"2026-10-06T15:18:27.563Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fefbe65803605af722707206","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-gate.test.ts"},{"op":"replace","path":"/metadata/files/8/path","value":"test/http-boundary.test.ts"},{"op":"replace","path":"/metadata/files/7/path","value":"test/helpers/public-acceptance.ts"},{"op":"replace","path":"/metadata/files/6/path","value":"test/helpers/bounded-cli.ts"},{"op":"replace","path":"/metadata/files/5/path","value":"test/gate-inputs.test.ts"},{"op":"replace","path":"/metadata/files/4/path","value":"test/coverage-edges.test.ts"},{"op":"replace","path":"/metadata/files/3/path","value":"scripts/generate-changelog.ts"},{"op":"add","path":"/metadata/files/10","value":{"path":"test/import-lock.test.ts","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T15:18:27.563Z"}],"before_hash":"b292e7138db95292e46ba21dca408adb2265313ed0c0563f77f8d705cb4acec8","after_hash":"dc5c2ddae9ca95ebc95ae0a7954cd40cf97b347c30f6932d3fe738062662efa8","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"d4064c1a3d8bc7ffa12cc0d4eb2e073452cb808e0922256b6f3f0946444f7476"} +{"hash_algorithm":"sha256","ts":"2026-10-06T15:18:28.555Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fefbe65803605af722707206","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"docs_add","patch":[{"op":"replace","path":"/metadata/docs/0/path","value":"CHANGELOG.md"},{"op":"add","path":"/metadata/docs/1","value":{"path":"README.md","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T15:18:28.555Z"}],"before_hash":"dc5c2ddae9ca95ebc95ae0a7954cd40cf97b347c30f6932d3fe738062662efa8","after_hash":"adefe85a8b68aa4010a349abceac779c95bc3fb5738922eaca602dd87c03a19a","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"4826938055c9b501797db4e8b73803b4e6f4d5483ee63ca1905768397fcf9d7d"} +{"hash_algorithm":"sha256","ts":"2026-10-06T15:18:29.824Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fefbe65803605af722707206","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"tests_add","patch":[{"op":"add","path":"/metadata/tests/8","value":{"command":"node --test --test-name-pattern=\"adversarial: token in a body|workflow input shell\" test/import-gate.test.ts","scope":"project","timeout_seconds":2400,"provenance":{"author":"codex-sol","created_at":"2026-10-06T15:18:29.770Z","source_kind":"local_mutation","source_ref":"feat/import-privacy-gate"}}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T15:18:29.824Z"}],"before_hash":"adefe85a8b68aa4010a349abceac779c95bc3fb5738922eaca602dd87c03a19a","after_hash":"b6863a3e3f9dfdab9f8e1646b86361f148c6509d63e2af11d0fdb43f595ebcef","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"839a1a851d77ba2b62e731401c6ff46c7e5bb4ffe953dd4e99757ac80c85dc9f"} +{"hash_algorithm":"sha256","ts":"2026-10-06T15:19:21.145Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fefbe65803605af722707206","agent_provenance":{"model":{"value":"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-06T15:19:21.145Z","author":"codex-sol","text":"Findings 4197056035, 4197055992, and 4197056044 addressed. Set pm-github-7d1h release=2026.8.17 using the CLI and regenerated with npm run changelog:full. Compared every released section byte-for-byte with origin/main: identical; only the added Unreleased entry differs. Real workflow shell tests pass for numeric pins and every rejected repository/version/branch input with its expected ::error:: diagnostic. The adversarial tracker assertion now checks scoped porcelain stdout; temporarily appending a newline to fixture tracker settings made that test fail with M .agents/pm/settings.json and exit 1, then the injection was removed. Focused suite: 3/3 pass. Docstrings: 130 declarations in 11 files; lint, duplication (zero duplicates at the configured threshold), and typecheck pass. Fresh-clone npm and Bun release checks are next."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T15:19:21.145Z"}],"before_hash":"b6863a3e3f9dfdab9f8e1646b86361f148c6509d63e2af11d0fdb43f595ebcef","after_hash":"4cf389bb35629e76616388913c29d6be8380c232b8b1f6aaeb4322e68292a0dc","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"583dd3c6b2480d4c64d2520c767a79c3078d53fa3b9dfcc48852953778b51096"} +{"hash_algorithm":"sha256","ts":"2026-10-06T15:35:58.572Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fefbe65803605af722707206","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"tests_add","patch":[{"op":"add","path":"/metadata/tests/9","value":{"command":"node --test test/generate-changelog.test.ts","scope":"project","timeout_seconds":60,"provenance":{"author":"codex-sol","created_at":"2026-10-06T15:35:58.476Z","source_kind":"local_mutation","source_ref":"feat/import-privacy-gate"}}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T15:35:58.572Z"}],"before_hash":"4cf389bb35629e76616388913c29d6be8380c232b8b1f6aaeb4322e68292a0dc","after_hash":"c7f0843c6b22878e09d657d71a00b4232638b9812c304c174e8a33ed5145f71f","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"d7ec2dbc22036b68e5ced308ec36ae468e51799a301c88f0c8d8bf004cc88286"} +{"hash_algorithm":"sha256","ts":"2026-10-06T15:36:25.199Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fefbe65803605af722707206","agent_provenance":{"model":{"value":"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-06T15:36:25.199Z","author":"codex-sol","text":"Review round 2 verified at pushed code commit 7a41660f190a108cca4bc10e270eff6b4864aa3e. Separate fresh remote branch clones installed dependencies with npm ci --ignore-scripts. Both npm run release:check and bun run release:check exited 0: 504/504 tests, zero skips/cancellations; complete coverage of all 12 authored modules with lines/statements 8406/8406, functions 208/208, branches 2225/2225 (exact 100/100/100/100). Typecheck, build, docstrings (130 declarations across 11 files), lint, configured duplication gate (zero duplicated lines), privacy (2829 objects), production audit (zero vulnerabilities), pack dry-run, changelog check, and release-publish attestation all passed. Targeted review tests ran via pm test --run --progress and passed 3/3; changelog tests passed 3/3. Tracker write negative control failed as expected and was removed. All released changelog sections match origin/main byte-for-byte; no additional re-binned items. Full release receipts apply to the code commit; the follow-up commit contains only tracker evidence and claim release, with privacy/changelog verification to follow. No new blockers. Keep this item open for orchestrator verification."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T15:36:25.199Z"}],"before_hash":"c7f0843c6b22878e09d657d71a00b4232638b9812c304c174e8a33ed5145f71f","after_hash":"c0a6b74284623b785d16c333252856e05381a3039e77ba7a827c8f7e57411048","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"cc378b6ab2c7ee4b87d2901e0d236f9956d1542a2455fa525e61223b8092599e"} +{"hash_algorithm":"sha256","ts":"2026-10-06T15:36:26.117Z","author":"codex-sol","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"fefbe65803605af722707206","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-06T15:36:26.117Z"}],"before_hash":"c0a6b74284623b785d16c333252856e05381a3039e77ba7a827c8f7e57411048","after_hash":"df5fa02f52cf96397aa2a11f0c3ef62c5b8ea8b5ae62397165b1c70c373ddd30","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"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":"30ae06329711a9abaa417ca298656188a035c011c3b515a624015f707e21ba51"} +{"hash_algorithm":"sha256","ts":"2026-10-06T18:08:43.067Z","author":"pi-gh-cr3b","author_source":"asserted","agent_harness":"pi","agent_model":"glm-5.3:cloud","agent_model_source":"environment","agent_instance":"adacbc2ad7298f54bbf2c51f","agent_provenance":{"model":{"value":"glm-5.3:cloud","source":"environment"},"effort":null,"role":{"value":"implementer","source":"argv"},"topic":null},"op":"claim","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T18:08:43.067Z"},{"op":"add","path":"/metadata/assignee","value":"pi-gh-cr3b"},{"op":"add","path":"/metadata/claim_principal","value":"pi-gh-cr3b"}],"before_hash":"df5fa02f52cf96397aa2a11f0c3ef62c5b8ea8b5ae62397165b1c70c373ddd30","after_hash":"ea7118e31146212fa579fc3e047114d747e08b17ddf4ff77b6da19c9a06cd33c","item_hash_version":3,"event_class":"substantive","record_hash_version":1,"record_hash":"137616f2c2532e412cb8228039b0f849cc886a9d1b4dc3d462f8733abb4aeba1"} +{"hash_algorithm":"sha256","ts":"2026-10-06T18:23:39.081Z","author":"pi-gh-cr3b","author_source":"asserted","agent_harness":"pi","agent_model":"glm-5.3:cloud","agent_model_source":"environment","agent_instance":"adacbc2ad7298f54bbf2c51f","agent_provenance":{"model":{"value":"glm-5.3:cloud","source":"environment"},"effort":null,"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/31","value":{"created_at":"2026-10-06T18:23:39.081Z","author":"pi-gh-cr3b","text":"Review round 3 (4 CodeRabbit findings from the 17:55Z full review) in progress on the package worktree. 4198811214: a fresh coverage run at the closing head re-measured 504/504 tests, exact 100/100/100/100, matching the close_reason receipt; the 503/503 in actual_result was the stale f572382 receipt. actual_result on pm-github-9cjx now cites 504/504 with the supersession note and the item stays closed. 4198811222: the degraded-mode lock test is split so its chmod 0o555 half and the stale-unlink permission-failure test skip visibly via node:test { skip } when process.getuid is 0, using the same non-root pattern the file already had. 4198811228: the source-slicing new Function test is replaced by a behavioral test of a new exported test/helpers read-only-directory helper that restores permissions in finally; coverage-remainder now uses the same helper. 4198811219 docs edit follows the remeasured counts."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T18:23:39.081Z"}],"before_hash":"ea7118e31146212fa579fc3e047114d747e08b17ddf4ff77b6da19c9a06cd33c","after_hash":"b543fc4ab30d47d4a1bbe6bdc82b6b9c6a840ece339f98d3db70692adfdc75f9","item_hash_version":3,"event_class":"substantive","record_hash_version":1,"record_hash":"657e336368f793745f587d6d9b4624315ba7d194ddc1780a5ac35def84be4a20"} +{"hash_algorithm":"sha256","ts":"2026-10-06T18:23:43.379Z","author":"pi-gh-cr3b","author_source":"asserted","agent_harness":"pi","agent_model":"glm-5.3:cloud","agent_model_source":"environment","agent_instance":"adacbc2ad7298f54bbf2c51f","agent_provenance":{"model":{"value":"glm-5.3:cloud","source":"environment"},"effort":null,"role":null,"topic":null},"op":"tests_add","patch":[{"op":"add","path":"/metadata/tests/10","value":{"command":"node --test test/import-lock.test.ts","scope":"project","provenance":{"author":"pi-gh-cr3b","created_at":"2026-10-06T18:23:43.289Z","source_kind":"local_mutation","source_ref":"feat/import-privacy-gate"}}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T18:23:43.379Z"}],"before_hash":"b543fc4ab30d47d4a1bbe6bdc82b6b9c6a840ece339f98d3db70692adfdc75f9","after_hash":"66c3f88b08e6a3e540058dcf9b1f253c539ff72a199247de7955a04184228bad","item_hash_version":3,"event_class":"maintenance","record_hash_version":1,"record_hash":"31651889ce36800fd1f89042d46a106359a442acc65a1f9f1f4605e4506b6e4f"} +{"hash_algorithm":"sha256","ts":"2026-10-06T18:23:44.899Z","author":"pi-gh-cr3b","author_source":"asserted","agent_harness":"pi","agent_model":"glm-5.3:cloud","agent_model_source":"environment","agent_instance":"adacbc2ad7298f54bbf2c51f","agent_provenance":{"model":{"value":"glm-5.3:cloud","source":"environment"},"effort":null,"role":null,"topic":null},"op":"tests_add","patch":[{"op":"add","path":"/metadata/tests/11","value":{"command":"node --test test/review-round1.test.ts test/coverage-remainder.test.ts","scope":"project","provenance":{"author":"pi-gh-cr3b","created_at":"2026-10-06T18:23:44.820Z","source_kind":"local_mutation","source_ref":"feat/import-privacy-gate"}}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T18:23:44.899Z"}],"before_hash":"66c3f88b08e6a3e540058dcf9b1f253c539ff72a199247de7955a04184228bad","after_hash":"8d68c83dd501a97e15909eeb62d5b7524808dd36ca5fa39dd3e79685d990b0f6","item_hash_version":3,"event_class":"maintenance","record_hash_version":1,"record_hash":"b0770b1e57f8fb11cb1c471033f8b7ae7d591273a2b622641dfac3c4d293d4b3"} +{"hash_algorithm":"sha256","ts":"2026-10-06T18:47:10.113Z","author":"pi-gh-cr3b","author_source":"asserted","agent_harness":"pi","agent_model":"glm-5.3:cloud","agent_model_source":"environment","agent_instance":"adacbc2ad7298f54bbf2c51f","agent_provenance":{"model":{"value":"glm-5.3:cloud","source":"environment"},"effort":null,"role":null,"topic":null},"op":"files_add","patch":[{"op":"add","path":"/metadata/files/11","value":{"path":"test/review-round1.test.ts","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T18:47:10.113Z"}],"before_hash":"8d68c83dd501a97e15909eeb62d5b7524808dd36ca5fa39dd3e79685d990b0f6","after_hash":"d7654bb96bc2fc355dec2a88ada07317a07533d7d2b4a89281c0810e5ea4e8ff","item_hash_version":3,"event_class":"maintenance","record_hash_version":1,"record_hash":"163f0106de82554ba06ce3269665ed6bc6e12779723f8c3b03a9c7f4619da482"} +{"hash_algorithm":"sha256","ts":"2026-10-06T18:47:11.175Z","author":"pi-gh-cr3b","author_source":"asserted","agent_harness":"pi","agent_model":"glm-5.3:cloud","agent_model_source":"environment","agent_instance":"adacbc2ad7298f54bbf2c51f","agent_provenance":{"model":{"value":"glm-5.3:cloud","source":"environment"},"effort":null,"role":null,"topic":null},"op":"files_add","patch":[{"op":"replace","path":"/metadata/files/11/path","value":"test/import-lock.test.ts"},{"op":"replace","path":"/metadata/files/10/path","value":"test/import-gate.test.ts"},{"op":"replace","path":"/metadata/files/9/path","value":"test/http-boundary.test.ts"},{"op":"replace","path":"/metadata/files/8/path","value":"test/helpers/public-acceptance.ts"},{"op":"replace","path":"/metadata/files/7/path","value":"test/helpers/bounded-cli.ts"},{"op":"replace","path":"/metadata/files/6/path","value":"test/gate-inputs.test.ts"},{"op":"replace","path":"/metadata/files/5/path","value":"test/coverage-remainder.test.ts"},{"op":"add","path":"/metadata/files/12","value":{"path":"test/review-round1.test.ts","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T18:47:11.175Z"}],"before_hash":"d7654bb96bc2fc355dec2a88ada07317a07533d7d2b4a89281c0810e5ea4e8ff","after_hash":"b8a946dd2e289947a09e876330b7a30a8eed8cadae94adfdcc530f2d4f42a467","item_hash_version":3,"event_class":"maintenance","record_hash_version":1,"record_hash":"3bfed10cf3f436f599c02bcf3928e29c5ff35efb5ac5dde111319a09646ef0e8"} +{"hash_algorithm":"sha256","ts":"2026-10-06T18:47:12.335Z","author":"pi-gh-cr3b","author_source":"asserted","agent_harness":"pi","agent_model":"glm-5.3:cloud","agent_model_source":"environment","agent_instance":"adacbc2ad7298f54bbf2c51f","agent_provenance":{"model":{"value":"glm-5.3:cloud","source":"environment"},"effort":null,"role":null,"topic":null},"op":"files_add","patch":[{"op":"replace","path":"/metadata/files/12/path","value":"test/import-lock.test.ts"},{"op":"replace","path":"/metadata/files/11/path","value":"test/import-gate.test.ts"},{"op":"replace","path":"/metadata/files/10/path","value":"test/http-boundary.test.ts"},{"op":"replace","path":"/metadata/files/9/path","value":"test/helpers/read-only-directory.ts"},{"op":"add","path":"/metadata/files/13","value":{"path":"test/review-round1.test.ts","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T18:47:12.335Z"}],"before_hash":"b8a946dd2e289947a09e876330b7a30a8eed8cadae94adfdcc530f2d4f42a467","after_hash":"4a836766b5a12509430dfb7bee39a208b6c35f4b30f8caa4d03b68101b57cbe0","item_hash_version":3,"event_class":"maintenance","record_hash_version":1,"record_hash":"931feb8f7762eb13c4d869e33d1a0db74faf87bc5463fb47ac1c279fa619326b"} +{"hash_algorithm":"sha256","ts":"2026-10-06T18:47:13.683Z","author":"pi-gh-cr3b","author_source":"asserted","agent_harness":"pi","agent_model":"glm-5.3:cloud","agent_model_source":"environment","agent_instance":"adacbc2ad7298f54bbf2c51f","agent_provenance":{"model":{"value":"glm-5.3:cloud","source":"environment"},"effort":null,"role":null,"topic":null},"op":"files_add","patch":[{"op":"replace","path":"/metadata/files/13/path","value":"test/import-lock.test.ts"},{"op":"replace","path":"/metadata/files/12/path","value":"test/import-gate.test.ts"},{"op":"replace","path":"/metadata/files/11/path","value":"test/http-boundary.test.ts"},{"op":"replace","path":"/metadata/files/10/path","value":"test/helpers/read-only-directory.ts"},{"op":"replace","path":"/metadata/files/9/path","value":"test/helpers/public-acceptance.ts"},{"op":"replace","path":"/metadata/files/8/path","value":"test/helpers/bounded-cli.ts"},{"op":"replace","path":"/metadata/files/7/path","value":"test/gate-inputs.test.ts"},{"op":"replace","path":"/metadata/files/6/path","value":"test/coverage-remainder.test.ts"},{"op":"replace","path":"/metadata/files/5/path","value":"test/coverage-edges.test.ts"},{"op":"replace","path":"/metadata/files/4/path","value":"scripts/generate-changelog.ts"},{"op":"replace","path":"/metadata/files/3/path","value":"index.ts"},{"op":"replace","path":"/metadata/files/2/path","value":"gate.ts"},{"op":"replace","path":"/metadata/files/1/path","value":"docs/coverage-gap-inventory.md"},{"op":"add","path":"/metadata/files/14","value":{"path":"test/review-round1.test.ts","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T18:47:13.683Z"}],"before_hash":"4a836766b5a12509430dfb7bee39a208b6c35f4b30f8caa4d03b68101b57cbe0","after_hash":"bf089c81fd00b7e5798007e7125c04ce06d939289b663b483c5b29c7f42aeb90","item_hash_version":3,"event_class":"maintenance","record_hash_version":1,"record_hash":"0b81b1c82cf1bb320e9afa63abbe38a129556d1dec7824b25befeff45c89c662"} +{"hash_algorithm":"sha256","ts":"2026-10-06T18:47:16.478Z","author":"pi-gh-cr3b","author_source":"asserted","agent_harness":"pi","agent_model":"glm-5.3:cloud","agent_model_source":"environment","agent_instance":"adacbc2ad7298f54bbf2c51f","agent_provenance":{"model":{"value":"glm-5.3:cloud","source":"environment"},"effort":null,"role":null,"topic":null},"op":"docs_add","patch":[{"op":"replace","path":"/metadata/docs/1/path","value":"docs/coverage-gap-inventory.md"},{"op":"add","path":"/metadata/docs/2","value":{"path":"README.md","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T18:47:16.478Z"}],"before_hash":"bf089c81fd00b7e5798007e7125c04ce06d939289b663b483c5b29c7f42aeb90","after_hash":"9760a19ce279703bfd6d523bb1be23e865fd1e38b4b85d8779efe41b29fe193a","item_hash_version":3,"event_class":"maintenance","record_hash_version":1,"record_hash":"4e24a3868e677b46064d39bc95d07fcad84ceefa3b8a67626ea5be3ed4521ffc"} +{"hash_algorithm":"sha256","ts":"2026-10-06T18:54:36.888Z","author":"pi-gh-cr3b","author_source":"asserted","agent_harness":"pi","agent_model":"glm-5.3:cloud","agent_model_source":"environment","agent_instance":"adacbc2ad7298f54bbf2c51f","agent_provenance":{"model":{"value":"glm-5.3:cloud","source":"environment"},"effort":null,"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/32","value":{"created_at":"2026-10-06T18:54:36.888Z","author":"pi-gh-cr3b","text":"Review round 3 receipts, all four findings fixed at b446963 on feat/import-privacy-gate. 4198811214: fresh coverage at the closing head re-measured 504/504, matching close_reason; actual_result on pm-github-9cjx updated to 504/504 with a supersession note (503/503 was the stale f572382 receipt); item stays closed. 4198811219: docs/coverage-gap-inventory.md closing section marked historical (487-count receipts, pre-verification open note) with a current-status line citing the closed item, exact 100/100/100/100, and the closing 504/505 counts. 4198811222: import-lock permission fixtures now skip visibly via node:test { skip } when process.getuid is 0; the degraded-mode test was split so the file-parent half runs everywhere; nothing becomes uncovered because the fixtures still execute in non-root CI. 4198811228: the new Function source-slicing test is gone; the new exported test/helpers/read-only-directory.ts withReadOnlyDirectory helper restores the previous mode in a finally block, is used by coverage-remainder, and is behaviorally tested with a throwing callback plus mode-restored assertion. Focused receipts: node --test test/import-lock.test.ts 25/25; node --test test/review-round1.test.ts test/coverage-remainder.test.ts 21/21; both also pass under bun test. Worktree coverage at the final head: 505/505, zero failures/skips, exact 100/100/100/100. Fresh clones of the pushed branch: npm run release:check PASS and bun run release:check PASS, both 505/505 with the exact 100 gate, 130 documented declarations, zero duplication, privacy, audit, pack, changelog and attestation green; npm run changelog:check PASS. Follow-up pm-github-rap0 records the remaining root-sensitive helper-based fixture."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T18:54:36.888Z"}],"before_hash":"9760a19ce279703bfd6d523bb1be23e865fd1e38b4b85d8779efe41b29fe193a","after_hash":"1ddf9e7c87b0afcf597a8836f4107045f50ade0c8348abd082d406e1215a1a88","item_hash_version":3,"event_class":"substantive","record_hash_version":1,"record_hash":"1c461838869102087c1d7c456982500cc2a7688df1a2451ace983c028ed5a597"} +{"hash_algorithm":"sha256","ts":"2026-10-06T18:54:39.837Z","author":"pi-gh-cr3b","author_source":"asserted","agent_harness":"pi","agent_model":"glm-5.3:cloud","agent_model_source":"environment","agent_instance":"adacbc2ad7298f54bbf2c51f","agent_provenance":{"model":{"value":"glm-5.3:cloud","source":"environment"},"effort":null,"role":null,"topic":null},"op":"tests_add","patch":[{"op":"add","path":"/metadata/tests/12","value":{"command":"npm run coverage","scope":"project","provenance":{"author":"pi-gh-cr3b","created_at":"2026-10-06T18:54:39.808Z","source_kind":"local_mutation","source_ref":"feat/import-privacy-gate"}}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T18:54:39.837Z"}],"before_hash":"1ddf9e7c87b0afcf597a8836f4107045f50ade0c8348abd082d406e1215a1a88","after_hash":"769726dc799709a45f6469ba4574fc5c222c2d83e0d046cef349089de49c8c89","item_hash_version":3,"event_class":"maintenance","record_hash_version":1,"record_hash":"8b6a071fce662ed3f8daa6b5e71620456cefe2dc20a69065fe4df9c619e64f12"} +{"hash_algorithm":"sha256","ts":"2026-10-06T18:54:40.542Z","author":"pi-gh-cr3b","author_source":"asserted","agent_harness":"pi","agent_model":"glm-5.3:cloud","agent_model_source":"environment","agent_instance":"adacbc2ad7298f54bbf2c51f","agent_provenance":{"model":{"value":"glm-5.3:cloud","source":"environment"},"effort":null,"role":null,"topic":null},"op":"tests_add","patch":[{"op":"add","path":"/metadata/tests/13","value":{"command":"npm run release:check && bun run release:check (fresh clones of b446963)","scope":"project","provenance":{"author":"pi-gh-cr3b","created_at":"2026-10-06T18:54:40.508Z","source_kind":"local_mutation","source_ref":"feat/import-privacy-gate"}}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T18:54:40.542Z"}],"before_hash":"769726dc799709a45f6469ba4574fc5c222c2d83e0d046cef349089de49c8c89","after_hash":"ece4608af237c4ab34181347e2477b8e8ad4524521c682c0951217980d8282f6","item_hash_version":3,"event_class":"maintenance","record_hash_version":1,"record_hash":"623d87a78ec797d6589bd182758ff0e466d039c24af2829417ced83075311125"} +{"hash_algorithm":"sha256","ts":"2026-10-06T18:54:49.993Z","author":"pi-gh-cr3b","author_source":"asserted","agent_harness":"pi","agent_model":"glm-5.3:cloud","agent_model_source":"environment","agent_instance":"adacbc2ad7298f54bbf2c51f","agent_provenance":{"model":{"value":"glm-5.3:cloud","source":"environment"},"effort":null,"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-06T18:54:49.993Z"}],"before_hash":"ece4608af237c4ab34181347e2477b8e8ad4524521c682c0951217980d8282f6","after_hash":"1423db69df5441f8a6d8297c25e2f30881c43b19c93bb08aa79b2096af05579f","item_hash_version":3,"event_class":"maintenance","record_hash_version":1,"record_hash":"de16c97368a7be6e2191acb97e5073b5e29093b23d3fe87357e8cc46e42babf4"} +{"hash_algorithm":"sha256","ts":"2026-10-06T19:48:30.209Z","author":"claude-orchestrator","author_source":"asserted","agent_harness":"claude-code","agent_model":"claude-opus-5-5","agent_model_source":"probe","agent_instance":"1c3d835edba106c1302b3106","agent_provenance":{"model":{"value":"claude-opus-5-5","source":"probe"},"effort":{"value":"high","source":"environment"},"role":null,"topic":null,"version":{"value":"2.1.291","source":"probe"}},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/33","value":{"created_at":"2026-10-06T19:48:30.209Z","author":"claude-orchestrator","text":"CodeRabbit full review at 9a8620c (4 findings), handled by the orchestrator: (1) 9cjx description updated to the closing 505-test receipt (487 marked historical). (2) gate.ts git status/diff now run with --literal-pathspecs. Defence in depth: tracker paths always start with a directory, so leading pathspec magic was inert and git literal-matches existing glob-named files; the new guard test 'tracker file names that look like pathspec magic are still scanned' passed before and after. (3) README caller example no longer adds a caller-level concurrency block, matching docs/sync-workflow-caller.yml (the reusable workflow already serializes per repository). (4) Declined: restoring the old nested-directory fallback in resolvePmDataDir; the SDK resolvePmRoot is the canonical tracker resolution and a .agents/pm without settings.json is not a pm tracker."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T19:48:30.209Z"}],"before_hash":"1423db69df5441f8a6d8297c25e2f30881c43b19c93bb08aa79b2096af05579f","after_hash":"1c3a7221e98325601aedfd1de8c9f2e0bee0b90a9c8427368051ba3d6d460756","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":"3f7dd545bdcdbea1f289d7eff9f68e0e473401c408ce7b87f7e412271c0d8dae"} +{"hash_algorithm":"sha256","ts":"2026-10-06T20:42:55.954Z","author":"claude-orchestrator","author_source":"asserted","agent_harness":"claude-code","agent_model":"claude-opus-5-5","agent_model_source":"probe","agent_instance":"1c3d835edba106c1302b3106","agent_provenance":{"model":{"value":"claude-opus-5-5","source":"probe"},"effort":{"value":"high","source":"environment"},"role":null,"topic":null,"version":{"value":"2.1.291","source":"probe"}},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/34","value":{"created_at":"2026-10-06T20:42:55.954Z","author":"claude-orchestrator","text":"CodeRabbit full review at 7c425f2 (2 findings), fixed by the orchestrator: (1) absolute-host-path matched any slash-prefixed word, so GitHub slash commands (/assign), repo-relative links (/docs/setup.md) and API routes (GET /api/v1) failed --gate imports; the rule is now anchored to host-identifying roots (home, Users, root, tmp, var, etc, opt, srv, mnt, media, private, Volumes, usr, run, proc, data, nix, scratch, workspace, builds, Library, System) with a boundary after the root, matching the README's documented scope. Test 'host-path rule flags host-identifying roots, not slash commands, links or API routes' fails on revert; existing home/tmp/srv fixtures stay detected. (2) http-boundary timeout test now awaits server.close()."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T20:42:55.954Z"}],"before_hash":"1c3a7221e98325601aedfd1de8c9f2e0bee0b90a9c8427368051ba3d6d460756","after_hash":"dc4b627c43c47fdcb17ff18747cc9fb50f8cfc3f175080c5688beeea51e94b6a","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":"b2d3906afc0d3b145a03eff16a99bb87311c2c189d5fff5438bfef032103c575"} +{"hash_algorithm":"sha256","ts":"2026-10-06T21:03:24.422Z","author":"claude-orchestrator","author_source":"asserted","agent_harness":"claude-code","agent_model":"claude-opus-5-5","agent_model_source":"probe","agent_instance":"1c3d835edba106c1302b3106","agent_provenance":{"model":{"value":"claude-opus-5-5","source":"probe"},"effort":{"value":"high","source":"environment"},"role":null,"topic":null,"version":{"value":"2.1.291","source":"probe"}},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/35","value":{"created_at":"2026-10-06T21:03:24.422Z","author":"claude-orchestrator","text":"Follow-up to the host-path anchoring: the gate exempted 'structural' JSON-patch pointers (/metadata..., /body...) from host-path rules only because the old unanchored rule matched every pointer. With host-root anchoring no pointer can match (windows rule excludes a preceding '/', JSON pointers escape '~'), so the exemption was dead code. It is removed, and pointers are scanned like data (stricter: a secret in a field key is caught). Coverage back to exact 100/100/100/100."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T21:03:24.422Z"}],"before_hash":"dc4b627c43c47fdcb17ff18747cc9fb50f8cfc3f175080c5688beeea51e94b6a","after_hash":"4e9bde682dfae4a39e245500c9924731da5118a1e3fcddf2d30b7f6aeede39ab","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":"1a03036ff48880eb911901c9f2b59a1a25112bc1c027f50ccdf313e169e50d1b"} +{"hash_algorithm":"sha256","ts":"2026-10-06T21:45:35.057Z","author":"claude-orchestrator","author_source":"asserted","agent_harness":"claude-code","agent_model":"claude-opus-5-5","agent_model_source":"probe","agent_instance":"1c3d835edba106c1302b3106","agent_provenance":{"model":{"value":"claude-opus-5-5","source":"probe"},"effort":{"value":"high","source":"environment"},"role":null,"topic":null,"version":{"value":"2.1.291","source":"probe"}},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/36","value":{"created_at":"2026-10-06T21:45:35.057Z","author":"claude-orchestrator","text":"CodeRabbit full review at ed75fde (4 findings), fixed by the orchestrator: 9cjx description/actual_result/close_reason reconciled to 504/504 at 7a41660; canonical caller (docs + README) no longer hardcodes repository (the reusable workflow refuses foreign repositories and defaults to the caller); email-address rule exempts the git@ SSH service account (git@github.com:owner/repo.git) with positive control gitlover@; unicode-escape decoder uses a sticky regex instead of slicing the suffix per character (linear on large history lines)."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T21:45:35.057Z"}],"before_hash":"4e9bde682dfae4a39e245500c9924731da5118a1e3fcddf2d30b7f6aeede39ab","after_hash":"d3f9d904d631d10526d97cbeb5ffc07e6b75a8ccbe935bfe0a8b3e0933553ec1","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":"f1a9deb8eda065453178eb684951f00cf5300bc92b04ff575694c31e410895f2"} +{"hash_algorithm":"sha256","ts":"2026-10-06T22:03:02.088Z","author":"claude-orchestrator","author_source":"asserted","agent_harness":"claude-code","agent_model":"claude-opus-5-5","agent_model_source":"probe","agent_instance":"1c3d835edba106c1302b3106","agent_provenance":{"model":{"value":"claude-opus-5-5","source":"probe"},"effort":{"value":"high","source":"environment"},"role":null,"topic":null,"version":{"value":"2.1.291","source":"probe"}},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/37","value":{"created_at":"2026-10-06T22:03:02.088Z","author":"claude-orchestrator","text":"Greptile 4200862803 on ae28d9e (valid, introduced by the orchestrator's git@ exemption): exempting every local part 'git' let 'Contact git@example.org' pass. The exemption moved from confirm() into the email pattern: only the SCP SSH URL shape git@host: is skipped (negative lookahead requiring a path character right after the colon), with a lookbehind so matches cannot start inside a local part. Tests: git@ contact addresses (incl. 'git@example.org: thanks', 'agit@github.com:x') stay flagged; fails on revert."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T22:03:02.088Z"}],"before_hash":"d3f9d904d631d10526d97cbeb5ffc07e6b75a8ccbe935bfe0a8b3e0933553ec1","after_hash":"1c155d2dc358a46cfbc5c467833a31118236f54731253a671b78943424829df2","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":"14f27118d70705c66238e040f64c12490d21cb7b751577c50ab15b6c09f19047"} +{"hash_algorithm":"sha256","ts":"2026-10-06T22:59:21.029Z","author":"claude-orchestrator","author_source":"asserted","agent_harness":"claude-code","agent_model":"claude-opus-5-5","agent_model_source":"probe","agent_instance":"1c3d835edba106c1302b3106","agent_provenance":{"model":{"value":"claude-opus-5-5","source":"probe"},"effort":{"value":"high","source":"environment"},"role":null,"topic":null,"version":{"value":"2.1.291","source":"probe"}},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/38","value":{"created_at":"2026-10-06T22:59:21.028Z","author":"claude-orchestrator","text":"CodeRabbit full review at 194cf12, handled by the orchestrator: (1) parseUnifiedDiff skipped every line while no target file was set, so an operator --diff with a hunk after 'diff --git' without ---/+++ or after '+++ /dev/null' passed with an added credential. Hunks are now parsed even without a target, and added lines without one fail closed; genuine deletions still parse. Test cases in 'malformed, truncated, and binary diffs fail closed' fail on revert. (2) Two privacy-gate tests that write POSIX sh git wrappers skip visibly on win32 (CI is Linux). (3) PATH note on the no-git test: informational, no change. (4) hptv stays in_progress on purpose: it closes after PR 108 merges."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T22:59:21.029Z"}],"before_hash":"1c155d2dc358a46cfbc5c467833a31118236f54731253a671b78943424829df2","after_hash":"27ac540e0c35e699be9508fcd9311e65c69bb0a83302758ba4330c13732470ef","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":"9b8180823d33f26ace2916181a2da0b832ad906c20487befd1ae04f89fb4bea5"} +{"hash_algorithm":"sha256","ts":"2026-10-07T00:23:51.656Z","author":"claude-orchestrator","author_source":"asserted","agent_harness":"claude-code","agent_model":"claude-opus-5-5","agent_model_source":"probe","agent_instance":"1c3d835edba106c1302b3106","agent_provenance":{"model":{"value":"claude-opus-5-5","source":"probe"},"effort":{"value":"high","source":"environment"},"role":null,"topic":null,"version":{"value":"2.1.291","source":"probe"}},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/39","value":{"created_at":"2026-10-07T00:23:51.656Z","author":"claude-orchestrator","text":"CodeRabbit 4201687009 (review at d7041ad), reproduced and fixed by the orchestrator: with a caller whose extensions.enabled list omits pm-github, the install edits settings.json and appends a workspace-history event; a no-change sync then failed with 'no item link could be generated' (and a real sync would have committed the setup change). The commit step now restores settings.json and history/_workspace.jsonl to HEAD (or removes them when HEAD lacks them) after the final gate. New workflow test 'an install-only settings change never reaches the sync commit' runs the real step shells from main with an explicit enabled list, asserts the precondition, expects exit 0 / no PR / no pushed branch, and fails on revert. Also added top-level permissions: {} (zizmor excessive-permissions); the job keeps its least-privilege block."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-07T00:23:51.656Z"}],"before_hash":"27ac540e0c35e699be9508fcd9311e65c69bb0a83302758ba4330c13732470ef","after_hash":"fae023ed3af7e62029ce0bf9cbaa7cee70a350a11271e25cdee43eead90bbbb9","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":"64c081044dabeadcabb24336da039f0eef31015d544f5f50e79fb18ef90c25fd"} +{"hash_algorithm":"sha256","ts":"2026-10-07T01:12:40.286Z","author":"claude-orchestrator","author_source":"asserted","agent_harness":"claude-code","agent_model":"claude-opus-5-5","agent_model_source":"probe","agent_instance":"1c3d835edba106c1302b3106","agent_provenance":{"model":{"value":"claude-opus-5-5","source":"probe"},"effort":{"value":"high","source":"environment"},"role":null,"topic":null,"version":{"value":"2.1.291","source":"probe"}},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/40","value":{"created_at":"2026-10-07T01:12:40.286Z","author":"claude-orchestrator","text":"CodeRabbit full review at 0f0c8c9 (2 findings), fixed by the orchestrator: README caller prerequisites now name the repository settings the review-PR step needs (read/write token permissions; 'Allow GitHub Actions to create and approve pull requests') and that GITHUB_TOKEN-opened PRs get no pull_request CI; test/helpers/bounded-cli.ts decodes child streams with setEncoding('utf8') so a multibyte character split across chunks is not corrupted. New test 'multibyte output split across stream chunks decodes intact' fails on revert."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-07T01:12:40.286Z"}],"before_hash":"fae023ed3af7e62029ce0bf9cbaa7cee70a350a11271e25cdee43eead90bbbb9","after_hash":"f83c74e7c812edcb0990beb7c9ba14b9bbcb33845dc4fbe354b3ea18f260bd73","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":"f69d8aa2a5cde7a96d8f926fc6a8ad6772b0a0783a16e0bc82ee0213d33257f0"} +{"hash_algorithm":"sha256","ts":"2026-10-07T02:20:59.122Z","author":"claude-orchestrator","author_source":"asserted","agent_harness":"claude-code","agent_model":"claude-opus-5-5","agent_model_source":"probe","agent_instance":"1c3d835edba106c1302b3106","agent_provenance":{"model":{"value":"claude-opus-5-5","source":"probe"},"effort":{"value":"high","source":"environment"},"role":null,"topic":null,"version":{"value":"2.1.291","source":"probe"}},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/41","value":{"created_at":"2026-10-07T02:20:59.122Z","author":"claude-orchestrator","text":"CodeRabbit 4202386304 (review at d57335e): JSON object keys in history/planned records were never scanned (only values), so a token used as a key passed while the same text in .toon failed. Keys are now scanned as text. Test 'history object keys are scanned like values' fails on revert."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-07T02:20:59.122Z"}],"before_hash":"f83c74e7c812edcb0990beb7c9ba14b9bbcb33845dc4fbe354b3ea18f260bd73","after_hash":"751dec6b0d2c550d208a841efd0ebd4e0eb94610885e0a1603495f30c7dc2393","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":"277ea02c40507b6f2bc83776edd003727a09bfb3c2cb684f6c7f1b30dece7d59"} +{"hash_algorithm":"sha256","ts":"2026-10-07T03:21:18.330Z","author":"claude-orchestrator","author_source":"asserted","agent_harness":"claude-code","agent_model":"claude-opus-5-5","agent_model_source":"probe","agent_instance":"1c3d835edba106c1302b3106","agent_provenance":{"model":{"value":"claude-opus-5-5","source":"probe"},"effort":{"value":"high","source":"environment"},"role":null,"topic":null,"version":{"value":"2.1.291","source":"probe"}},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/42","value":{"created_at":"2026-10-07T03:21:18.330Z","author":"claude-orchestrator","text":"CodeRabbit full review at 61b7ed6 (2 trivial test findings), fixed by the orchestrator: the githubHandler doc comment moved onto githubHandler (it sat above planEntry's), and the idempotency-refusal test now asserts the observable property (no 'plan verified' line before the refusal) instead of a regex that always matched 'Fetching issues'."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-07T03:21:18.330Z"}],"before_hash":"751dec6b0d2c550d208a841efd0ebd4e0eb94610885e0a1603495f30c7dc2393","after_hash":"ab902f4735ad365cb21c94141ddf10bb6f24034aec6b763987c6e48957d57e62","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":"eeb26f02ea1cb5dbeffb7374e59aaaaf5fe8c56cd887d024d8e62b3920e6e900"} +{"hash_algorithm":"sha256","ts":"2026-10-07T04:27:56.381Z","author":"claude-orchestrator","author_source":"asserted","agent_harness":"claude-code","agent_model":"claude-opus-5-5","agent_model_source":"probe","agent_instance":"1c3d835edba106c1302b3106","agent_provenance":{"model":{"value":"claude-opus-5-5","source":"probe"},"effort":{"value":"high","source":"environment"},"role":null,"topic":null,"version":{"value":"2.1.291","source":"probe"}},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/43","value":{"created_at":"2026-10-07T04:27:56.381Z","author":"claude-orchestrator","text":"CodeRabbit Major 4203042480 (review at 08ca152), a real merge blocker: loadFixtureExemptions resolved provenance for every historical_test manifest entry via 'git ls-tree ', and the pinned commit 434df42 is a branch commit that is NOT reachable from main after a squash merge, so the privacy gate would fail on main. Provenance is now resolved only for blobs in the audited object inventory (historical blobs vanish with the squash); present blobs with mismatched or unresolvable provenance still fail closed. The provenance test now covers both cases and fails on revert. Verified by simulating the squash merge in a scratch clone and running the privacy gate."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-07T04:27:56.381Z"}],"before_hash":"ab902f4735ad365cb21c94141ddf10bb6f24034aec6b763987c6e48957d57e62","after_hash":"48b364798b69778300e8bed585f298c8f4fa850bad384457e9436caeed13e1dd","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":"5111ae7e9f72396c1485ebacd0a750489f3e0ef42c8fef0f0b92402a642b6cd8"} +{"hash_algorithm":"sha256","ts":"2026-10-07T05:27:52.940Z","author":"claude-orchestrator","author_source":"asserted","agent_harness":"claude-code","agent_model":"claude-opus-5-5","agent_model_source":"probe","agent_instance":"1c3d835edba106c1302b3106","agent_provenance":{"model":{"value":"claude-opus-5-5","source":"probe"},"effort":{"value":"high","source":"environment"},"role":null,"topic":null,"version":{"value":"2.1.291","source":"probe"}},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/44","value":{"created_at":"2026-10-07T05:27:52.940Z","author":"claude-orchestrator","text":"CodeRabbit Major 4203392922 (review at 66a3a43), reproduced and fixed by the orchestrator: in the gated non-atomic import, a failed update/close/reopen reconciliation only incremented 'skipped', and verifyImportedProvenance passed because the item kept its provenance tag, so the gated import (and the reusable workflow) reported success while the tracker differed from GitHub. The gated path now fails closed when the write count differs from the verified plan. Test 'a gated non-atomic import fails closed when a planned write fails' (one refused close plus one clean update) resolves successfully without the fix and rejects with it."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-07T05:27:52.940Z"}],"before_hash":"48b364798b69778300e8bed585f298c8f4fa850bad384457e9436caeed13e1dd","after_hash":"97c3c136a086adf7252ab0b16fddd4320f53212f65e2add94a4137f59670605e","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":"02cae5fea3cd0dca717f47f07d6766aa8e7669e3f1478915d0cb3e144ce5d5a5"} +{"hash_algorithm":"sha256","ts":"2026-10-07T06:31:47.673Z","author":"pi-glm","author_source":"asserted","agent_harness":"pi","agent_model":"glm-5.3:cloud","agent_model_source":"environment","agent_instance":"ac0bd608a6f38b5b17cafb0d","agent_provenance":{"model":{"value":"glm-5.3:cloud","source":"environment"},"effort":null,"role":{"value":"implementer","source":"argv"},"topic":null},"op":"claim","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-10-07T06:31:47.673Z"},{"op":"add","path":"/metadata/assignee","value":"pi-glm"},{"op":"add","path":"/metadata/claim_principal","value":"pi-glm"}],"before_hash":"97c3c136a086adf7252ab0b16fddd4320f53212f65e2add94a4137f59670605e","after_hash":"34c7ab81f290f008bb03866fd281daf4842d09b64cc74aa2a7b155fe9785569e","item_hash_version":3,"event_class":"substantive","record_hash_version":1,"record_hash":"a0f5031dd4daddbde8eac1a5e8283a6cd14141dd0b20834a878b18f70f2e68bc"} +{"hash_algorithm":"sha256","ts":"2026-10-07T06:31:48.660Z","author":"pi-glm","author_source":"asserted","agent_harness":"pi","agent_model":"glm-5.3:cloud","agent_model_source":"environment","agent_instance":"ac0bd608a6f38b5b17cafb0d","agent_provenance":{"model":{"value":"glm-5.3:cloud","source":"environment"},"effort":null,"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/45","value":{"created_at":"2026-10-07T06:31:48.660Z","author":"pi-glm","text":"Review round follow-up (PR findings 4203767825 + 4203767821), pi-glm: (1) make syncGithubCommentsToAnnotations report a failed count (contended lock, unreadable existing comments, per-comment add failures) and fail gated imports closed when any planned native comment was not written or a created id was unparsable while annotations sync is on — both on the non-atomic and the atomic path; ungated behaviour stays warn-and-continue. (2) docs/coverage-gap-inventory.md: describe the 505/505 figure as a later-head measurement so it agrees with the authoritative 504/504 closing receipt. Regression tests go into test/coverage-remainder.test.ts with the existing helpers."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-07T06:31:48.660Z"}],"before_hash":"34c7ab81f290f008bb03866fd281daf4842d09b64cc74aa2a7b155fe9785569e","after_hash":"8c11fe6651f6faa15c0d5bb6d14471520aea92f9cb85fca9d1e1e35d3c769954","item_hash_version":3,"event_class":"substantive","record_hash_version":1,"record_hash":"9aa36543e81d591fd19d9f9cd0ccf5dfcf7dadbba1a89be0f2feb13ded92d71b"} +{"hash_algorithm":"sha256","ts":"2026-10-07T07:11:23.323Z","author":"pi-glm","author_source":"asserted","agent_harness":"pi","agent_model":"glm-5.3:cloud","agent_model_source":"environment","agent_instance":"ac0bd608a6f38b5b17cafb0d","agent_provenance":{"model":{"value":"glm-5.3:cloud","source":"environment"},"effort":null,"role":null,"topic":null},"op":"tests_add","patch":[{"op":"add","path":"/metadata/tests/14","value":{"command":"node --test test/coverage-remainder.test.ts","scope":"project","provenance":{"author":"pi-glm","created_at":"2026-10-07T07:11:23.202Z","source_kind":"local_mutation","source_ref":"feat/import-privacy-gate"}}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-07T07:11:23.323Z"}],"before_hash":"8c11fe6651f6faa15c0d5bb6d14471520aea92f9cb85fca9d1e1e35d3c769954","after_hash":"4c498c387f92b01cf9b28a351f47b46ad3c4673e37a73b9953a576575a34c624","item_hash_version":3,"event_class":"maintenance","record_hash_version":1,"record_hash":"40b717d5163a66c31f78ff52729add26aec21eccc98a68e5ae6697f61ee00d53"} +{"hash_algorithm":"sha256","ts":"2026-10-07T07:11:24.206Z","author":"pi-glm","author_source":"asserted","agent_harness":"pi","agent_model":"glm-5.3:cloud","agent_model_source":"environment","agent_instance":"ac0bd608a6f38b5b17cafb0d","agent_provenance":{"model":{"value":"glm-5.3:cloud","source":"environment"},"effort":null,"role":null,"topic":null},"op":"tests_add","patch":[{"op":"add","path":"/metadata/tests/15","value":{"command":"npm test","scope":"project","provenance":{"author":"pi-glm","created_at":"2026-10-07T07:11:24.149Z","source_kind":"local_mutation","source_ref":"feat/import-privacy-gate"}}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-07T07:11:24.206Z"}],"before_hash":"4c498c387f92b01cf9b28a351f47b46ad3c4673e37a73b9953a576575a34c624","after_hash":"0d88e769272b0232b860a47900e9820fa5f26863ada0ba5b3f11247992e888fa","item_hash_version":3,"event_class":"maintenance","record_hash_version":1,"record_hash":"e3ef9385534edb4cfc45efb72dc1ac9faa1a939780f33723ee1ebbb84a8ceb4d"} +{"hash_algorithm":"sha256","ts":"2026-10-07T07:11:31.984Z","author":"pi-glm","author_source":"asserted","agent_harness":"pi","agent_model":"glm-5.3:cloud","agent_model_source":"environment","agent_instance":"ac0bd608a6f38b5b17cafb0d","agent_provenance":{"model":{"value":"glm-5.3:cloud","source":"environment"},"effort":null,"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/46","value":{"created_at":"2026-10-07T07:11:31.984Z","author":"pi-glm","text":"Disposition of both review findings (pi-glm): 4203767825 (index.ts) — VALID, fixed. syncGithubCommentsToAnnotations now returns a failed count (contended lock and unreadable existing comments count every planned comment; each refused add counts one). runImport accumulates the failures from every call site and an unparsable created id with annotations sync on counts the planned comments; both the non-atomic and the atomic path route the divergence through one shared failClosedOnPlanDivergence refusal with the existing \"planned write(s) failed\" wording, so a gated --comments-mode annotations|both import now rejects instead of exiting clean. Ungated behaviour is unchanged (warn and continue). 4203767821 (docs/coverage-gap-inventory.md) — VALID, fixed: 504/504 at the closing head 7a41660 is now described as the authoritative closing count and 505/505 explicitly as a later-head measurement. New regression tests in test/coverage-remainder.test.ts: \"a gated import fails closed when the comment-sync lock is contended\", \"a gated import fails closed when a per-comment add fails mid-sync\", \"a gated import fails closed when a created id cannot be parsed with annotations sync on\", \"a gated atomic import fails closed when committed items lack their planned comments\", and the ungated control \"the same comment-sync failures without --gate still import with a warning\". Two existing result-shape assertions (test/import-errors.test.ts, test/import-lock.test.ts) were extended with the new failed field. Each gated test was proven to fail on the reverted production code (4 rejected, control passed) before restoring the fix."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-07T07:11:31.984Z"}],"before_hash":"0d88e769272b0232b860a47900e9820fa5f26863ada0ba5b3f11247992e888fa","after_hash":"a60748cdfa76c886d1eb49d3a1bb0acb027c1686e3aa8bf8f6b8540e2c32c391","item_hash_version":3,"event_class":"substantive","record_hash_version":1,"record_hash":"7e86bf238de089c3f636e016eb728e579bc44a9ab79063ed8cd62240d575f06a"} +{"hash_algorithm":"sha256","ts":"2026-10-07T07:11:36.590Z","author":"codex","author_source":"configured","agent_harness":"pi","agent_model":"glm-5.3:cloud","agent_model_source":"environment","agent_instance":"ac0bd608a6f38b5b17cafb0d","agent_provenance":{"model":{"value":"glm-5.3:cloud","source":"environment"},"effort":null,"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/47","value":{"created_at":"2026-10-07T07:11:36.590Z","author":"codex","text":"Gate results (pi-glm): npm test 516/516 pass; npm run coverage exact 100/100/100/100 across the 12 authored modules; npm run release:check and bun run release:check both exit zero (typecheck, build, docstring, lint, privacy, coverage, audit:prod, pack:dry-run, changelog:check, publish attestation all green). npm run changelog:check: Changelog is up to date. Open risk: none identified for this change; note the contended-lock regression tests each spend the ~30s import-lock wait budget by design (same as the existing contention test)."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-07T07:11:36.590Z"}],"before_hash":"a60748cdfa76c886d1eb49d3a1bb0acb027c1686e3aa8bf8f6b8540e2c32c391","after_hash":"ab1a253336dd659035be36ecac7c25ab2c3ef679e542c6f6ec540ee28140d32a","item_hash_version":3,"event_class":"substantive","record_hash_version":1,"record_hash":"5821ea38713660de10ffd8438b5a452928fcbbaf7a16eb1f4a5fe0741886e80d"} +{"hash_algorithm":"sha256","ts":"2026-10-07T07:11:43.075Z","author":"pi-glm","author_source":"asserted","agent_harness":"pi","agent_model":"glm-5.3:cloud","agent_model_source":"environment","agent_instance":"ac0bd608a6f38b5b17cafb0d","agent_provenance":{"model":{"value":"glm-5.3:cloud","source":"environment"},"effort":null,"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/48","value":{"created_at":"2026-10-07T07:11:43.075Z","author":"pi-glm","text":"Attribution note (pi-glm): the previous gate-results comment was recorded under a default author because the session author env was not exported in that shell; it is mine (pi-glm) and its contents stand as written."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-07T07:11:43.075Z"}],"before_hash":"ab1a253336dd659035be36ecac7c25ab2c3ef679e542c6f6ec540ee28140d32a","after_hash":"70e65eda8c4b79c70338bd0e43dabd3b9195aaf1cfe22c4d5488a8237b9f8e32","item_hash_version":3,"event_class":"substantive","record_hash_version":1,"record_hash":"5b997046409878d7f31f3c54802165f899b518b9a3c27ea619249e3211a9ec5c"} +{"hash_algorithm":"sha256","ts":"2026-10-07T07:11:56.715Z","author":"pi-glm","author_source":"asserted","agent_harness":"pi","agent_model":"glm-5.3:cloud","agent_model_source":"environment","agent_instance":"ac0bd608a6f38b5b17cafb0d","agent_provenance":{"model":{"value":"glm-5.3:cloud","source":"environment"},"effort":null,"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/49","value":{"created_at":"2026-10-07T07:11:56.715Z","author":"pi-glm","text":"Committed as 0ad387f (gate fix + regression tests), pi-glm. The doc-count fix follows in its own commit."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-07T07:11:56.715Z"}],"before_hash":"70e65eda8c4b79c70338bd0e43dabd3b9195aaf1cfe22c4d5488a8237b9f8e32","after_hash":"47bb2e2cdeb16a81e3e68be701d8daa0802fd43dfbd35475833383faa24898f3","item_hash_version":3,"event_class":"substantive","record_hash_version":1,"record_hash":"45a4dd3d1b7b0e7f581d14e2850a40017e90aebbb6414b6b410bf1b99302bf7c"} +{"hash_algorithm":"sha256","ts":"2026-10-07T07:12:00.358Z","author":"pi-glm","author_source":"asserted","agent_harness":"pi","agent_model":"glm-5.3:cloud","agent_model_source":"environment","agent_instance":"ac0bd608a6f38b5b17cafb0d","agent_provenance":{"model":{"value":"glm-5.3:cloud","source":"environment"},"effort":null,"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/50","value":{"created_at":"2026-10-07T07:12:00.358Z","author":"pi-glm","text":"Both findings landed (pi-glm): 0ad387f fail-closed gated comment writes + regression tests; dc87f02 coverage-gap inventory now calls 504/504 at 7a41660 authoritative and 505/505 a later-head measurement. Pushing the branch now."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-07T07:12:00.358Z"}],"before_hash":"47bb2e2cdeb16a81e3e68be701d8daa0802fd43dfbd35475833383faa24898f3","after_hash":"b7600d6feb0cfa6b54359a8cf6b5c39d98aa7adaf05d92ae99c5f2fe32a4614e","item_hash_version":3,"event_class":"substantive","record_hash_version":1,"record_hash":"f9bf9a07cba3c1c2b58a5763c8585f97bed8dbe0c7a556a90175223958b98c0d"} +{"hash_algorithm":"sha256","ts":"2026-10-07T07:12:11.960Z","author":"pi-glm","author_source":"asserted","agent_harness":"pi","agent_model":"glm-5.3:cloud","agent_model_source":"environment","agent_instance":"ac0bd608a6f38b5b17cafb0d","agent_provenance":{"model":{"value":"glm-5.3:cloud","source":"environment"},"effort":null,"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-07T07:12:11.960Z"}],"before_hash":"b7600d6feb0cfa6b54359a8cf6b5c39d98aa7adaf05d92ae99c5f2fe32a4614e","after_hash":"625b7117808c0eef53a536a10c01a92a306b21994c89b5ba28b01fda9d266e18","item_hash_version":3,"event_class":"maintenance","record_hash_version":1,"record_hash":"49cd2a68a22a48e8002fcee8fa870c59580585b842d6b76b066cf39aae6ef2ca"} +{"hash_algorithm":"sha256","ts":"2026-10-07T07:31:19.560Z","author":"claude-orchestrator","author_source":"asserted","agent_harness":"claude-code","agent_model":"claude-opus-5-5","agent_model_source":"probe","agent_instance":"1c3d835edba106c1302b3106","agent_provenance":{"model":{"value":"claude-opus-5-5","source":"probe"},"effort":{"value":"high","source":"environment"},"role":null,"topic":null,"version":{"value":"2.1.291","source":"probe"}},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/51","value":{"created_at":"2026-10-07T07:31:19.560Z","author":"claude-orchestrator","text":"CodeRabbit full review at 011f850 (2 findings), fixed by the orchestrator: review-PR bodies now add a 'proposed' link to each item on SYNC_BRANCH next to the permanent main link (main links don't resolve, or show the old version, until merge); the executed workflow test asserts it and fails on revert. docs/sync-workflow-caller.yml now states the 'Allow GitHub Actions to create and approve pull requests' + read/write workflow-permissions requirement."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-07T07:31:19.560Z"}],"before_hash":"625b7117808c0eef53a536a10c01a92a306b21994c89b5ba28b01fda9d266e18","after_hash":"3f7ab2ed1bb6c3bc989637010577898601783c48b92dcc847439815cab929b3f","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":"04a7fe59f622d366001399fc3e8f8afb391b87e2673b0806f3e03e547929014d"} +{"hash_algorithm":"sha256","ts":"2026-10-07T08:33:52.212Z","author":"claude-orchestrator","author_source":"asserted","agent_harness":"claude-code","agent_model":"claude-opus-5-5","agent_model_source":"probe","agent_instance":"1c3d835edba106c1302b3106","agent_provenance":{"model":{"value":"claude-opus-5-5","source":"probe"},"effort":{"value":"high","source":"environment"},"role":null,"topic":null,"version":{"value":"2.1.291","source":"probe"}},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/52","value":{"created_at":"2026-10-07T08:33:52.212Z","author":"claude-orchestrator","text":"CodeRabbit full review at 7e981cb (2 findings), fixed by the orchestrator: the three read-only-directory comment-failure tests skip visibly under root (closes pm-github-rap0), and executeWorkflow in test/import-gate.test.ts decodes child streams with setEncoding('utf8') like bounded-cli.ts."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-07T08:33:52.212Z"}],"before_hash":"3f7ab2ed1bb6c3bc989637010577898601783c48b92dcc847439815cab929b3f","after_hash":"2b83fae99806151bb69a9b96c2fc2d06df8ff40c2dbccd5b83dcb441a78195c5","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":"d59cc1876e8402815a1f3fe6d7a54e04e7203eec67c5c6e0b9b4529f6371fab2"} +{"hash_algorithm":"sha256","ts":"2026-10-07T09:33:27.435Z","author":"pi-glm","author_source":"asserted","agent_harness":"pi","agent_model":"glm-5.3:cloud","agent_model_source":"environment","agent_instance":"30a5dc57c8ed25af079d0641","agent_provenance":{"model":{"value":"glm-5.3:cloud","source":"environment"},"effort":null,"role":{"value":"implementer","source":"argv"},"topic":null},"op":"claim","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-10-07T09:33:27.435Z"},{"op":"add","path":"/metadata/assignee","value":"pi-glm"},{"op":"add","path":"/metadata/claim_principal","value":"pi-glm"}],"before_hash":"2b83fae99806151bb69a9b96c2fc2d06df8ff40c2dbccd5b83dcb441a78195c5","after_hash":"638f50e88cdab17587016df3daf909549fa53f54fdbad57217ba2960cc4ed1e6","item_hash_version":3,"event_class":"substantive","record_hash_version":1,"record_hash":"9d93c57502ccdec60a1a8b4589f1b9c192a028f02be5556ea5fa4d4ad8177b06"} +{"hash_algorithm":"sha256","ts":"2026-10-07T09:33:28.837Z","author":"pi-glm","author_source":"asserted","agent_harness":"pi","agent_model":"glm-5.3:cloud","agent_model_source":"environment","agent_instance":"30a5dc57c8ed25af079d0641","agent_provenance":{"model":{"value":"glm-5.3:cloud","source":"environment"},"effort":null,"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/53","value":{"created_at":"2026-10-07T09:33:28.837Z","author":"pi-glm","text":"Starting review round 2 (CodeRabbit findings 4205293132, 4205293127, 4205293135): (1) select the review PR by head repository, not just branch name; (2) close an own open sync PR on a no-change run; (3) isolate the public-acceptance npm pack like packWorkflowCandidate."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-07T09:33:28.837Z"}],"before_hash":"638f50e88cdab17587016df3daf909549fa53f54fdbad57217ba2960cc4ed1e6","after_hash":"c6ebd108cacbb1814d7d6ae46a739c61cca8b310c316e9d387c8a487690a3551","item_hash_version":3,"event_class":"substantive","record_hash_version":1,"record_hash":"2f43aed3f293318f7246251f31effff94682842e153fc59dcfb2fc5138c1beef"} +{"hash_algorithm":"sha256","ts":"2026-10-07T10:02:37.579Z","author":"pi-glm","author_source":"asserted","agent_harness":"pi","agent_model":"glm-5.3:cloud","agent_model_source":"environment","agent_instance":"30a5dc57c8ed25af079d0641","agent_provenance":{"model":{"value":"glm-5.3:cloud","source":"environment"},"effort":null,"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/54","value":{"created_at":"2026-10-07T10:02:37.579Z","author":"pi-glm","text":"Review round 2 dispositions. Finding 4205293132 (accepted, Major): the review-PR lookup now asks gh pr list for number,headRepository,headRepositoryOwner,isCrossRepository and selects in the step, with jq, only an OPEN PR whose head repository is ${REPOSITORY} (jq-side first-match, so no head/SIGPIPE hazard under pipefail); a fork PR with the same branch name is never edited and a new own PR is created instead. Finding 4205293127 (accepted, Minor): on a no-change run (`git diff --cached --quiet`), the workflow now closes the own open sync PR (same head-repository rule) with a short comment that the tracker already matches GitHub, then exits 0. Finding 4205293135 (accepted, Minor): the public acceptance helper's npm pack now uses the package-local PM_PATH and --foreground-scripts=false, the same npm-10 isolation as packWorkflowCandidate."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-07T10:02:37.579Z"}],"before_hash":"c6ebd108cacbb1814d7d6ae46a739c61cca8b310c316e9d387c8a487690a3551","after_hash":"cdbc9cf010dc6e625c1eb28b896d635f94a307015fadb1955dfdf9f2a088655f","item_hash_version":3,"event_class":"substantive","record_hash_version":1,"record_hash":"4167530e91a174c705596b3103bf6e794a5f77301d117ebed0ff2b7080546fae"} +{"hash_algorithm":"sha256","ts":"2026-10-07T10:02:38.250Z","author":"pi-glm","author_source":"asserted","agent_harness":"pi","agent_model":"glm-5.3:cloud","agent_model_source":"environment","agent_instance":"30a5dc57c8ed25af079d0641","agent_provenance":{"model":{"value":"glm-5.3:cloud","source":"environment"},"effort":null,"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/55","value":{"created_at":"2026-10-07T10:02:38.250Z","author":"pi-glm","text":"Fail-on-revert proof (temporary workflow copies, no stash): reverting the workflow to the pre-fix first-listed-PR lookup and no-close path makes 'the workflow never edits a fork PR sharing the sync branch name' and 'a no-change run closes an own open sync PR and exits successfully' fail; a partial revert that closes the first listed PR without the head-repository filter makes 'a no-change run with only a fork PR sharing the branch name closes nothing' fail. npm-10 proof for finding 4205293135: with a scratch npm@10 (10.9.9) first on PATH, the helper's pre-fix pack invocation exits 3 because npm 10 still runs prepare with --ignore-scripts and it fails with the fixture PM_PATH (outside the repository); the fixed invocation with the package-local PM_PATH and --foreground-scripts=false exits 0 and the --json receipt parses to the tarball filename."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-07T10:02:38.250Z"}],"before_hash":"cdbc9cf010dc6e625c1eb28b896d635f94a307015fadb1955dfdf9f2a088655f","after_hash":"4adbef406745aba0a9a33deb9ae959d92686a5662c131a90248c997f622ae7c0","item_hash_version":3,"event_class":"substantive","record_hash_version":1,"record_hash":"4e643a03046a3d3eec6d45bca02ebc410ce8bec776019d4cb545b0ed8c434e77"} +{"hash_algorithm":"sha256","ts":"2026-10-07T10:02:41.690Z","author":"pi-glm","author_source":"asserted","agent_harness":"pi","agent_model":"glm-5.3:cloud","agent_model_source":"environment","agent_instance":"30a5dc57c8ed25af079d0641","agent_provenance":{"model":{"value":"glm-5.3:cloud","source":"environment"},"effort":null,"role":null,"topic":null},"op":"tests_add","patch":[{"op":"add","path":"/metadata/tests/16","value":{"command":"node --test --test-name-pattern=\"fork PR|no-change|install-only settings change|clean import and creates\" test/import-gate.test.ts","scope":"project","provenance":{"author":"pi-glm","created_at":"2026-10-07T10:02:41.652Z","source_kind":"local_mutation","source_ref":"feat/import-privacy-gate"}}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-07T10:02:41.690Z"}],"before_hash":"4adbef406745aba0a9a33deb9ae959d92686a5662c131a90248c997f622ae7c0","after_hash":"084643f49dd74674af2fc1e5d869d3c179b99c36534ec32978fcb21576131276","item_hash_version":3,"event_class":"maintenance","record_hash_version":1,"record_hash":"4b29c12302ff91d3f1c9e5d335e5bf51e7b0d2c3f7019548d599302f6c8f47a8"} +{"hash_algorithm":"sha256","ts":"2026-10-07T10:02:42.448Z","author":"pi-glm","author_source":"asserted","agent_harness":"pi","agent_model":"glm-5.3:cloud","agent_model_source":"environment","agent_instance":"30a5dc57c8ed25af079d0641","agent_provenance":{"model":{"value":"glm-5.3:cloud","source":"environment"},"effort":null,"role":null,"topic":null},"op":"tests_add","patch":[{"op":"add","path":"/metadata/tests/17","value":{"command":"npm run release:check && bun run release:check","scope":"project","provenance":{"author":"pi-glm","created_at":"2026-10-07T10:02:42.405Z","source_kind":"local_mutation","source_ref":"feat/import-privacy-gate"}}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-07T10:02:42.448Z"}],"before_hash":"084643f49dd74674af2fc1e5d869d3c179b99c36534ec32978fcb21576131276","after_hash":"0fd3d9d639793c79535f55a4a0cca2b190d48656c39ab00010844b3472a3cd84","item_hash_version":3,"event_class":"maintenance","record_hash_version":1,"record_hash":"5fd567167de09bbfe7347136e138262ed5844fdc2b63778d12d564f4bc243d81"} +{"hash_algorithm":"sha256","ts":"2026-10-07T10:47:24.539Z","author":"pi-glm","author_source":"asserted","agent_harness":"pi","agent_model":"glm-5.3:cloud","agent_model_source":"environment","agent_instance":"30a5dc57c8ed25af079d0641","agent_provenance":{"model":{"value":"glm-5.3:cloud","source":"environment"},"effort":null,"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/56","value":{"created_at":"2026-10-07T10:47:24.539Z","author":"pi-glm","text":"Committed 48c5885 (branch feat/import-privacy-gate, pushed) with all three review-round-2 fixes, the extended fake gh (pr list returns realistic head-repository JSON driven by OWN_OPEN_PR/FORK_OPEN_PR; pr close recorded), the three new guard tests, and tracker evidence. Gates: npm run release:check pass, bun run release:check pass (typecheck, docstring 130 declarations, lint, duplication zero, privacy, coverage 12 authored modules exact 100/100/100/100, 519/519 tests, audit:prod, pack:dry-run, changelog:check, publish-attestation)."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-07T10:47:24.539Z"}],"before_hash":"0fd3d9d639793c79535f55a4a0cca2b190d48656c39ab00010844b3472a3cd84","after_hash":"4c07042dc15b56b6331bb70159e940183a28084fe91f6420f7e825ac29d88d8c","item_hash_version":3,"event_class":"substantive","record_hash_version":1,"record_hash":"164f101433fb4a7747c4882166265156f34bd75f5835ba13d5848fdc16825fbe"} +{"hash_algorithm":"sha256","ts":"2026-10-07T10:47:25.574Z","author":"pi-glm","author_source":"asserted","agent_harness":"pi","agent_model":"glm-5.3:cloud","agent_model_source":"environment","agent_instance":"30a5dc57c8ed25af079d0641","agent_provenance":{"model":{"value":"glm-5.3:cloud","source":"environment"},"effort":null,"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-07T10:47:25.574Z"}],"before_hash":"4c07042dc15b56b6331bb70159e940183a28084fe91f6420f7e825ac29d88d8c","after_hash":"7856603481592282f9f474385c376c3cc43fda36d7852a74ff7285c9947a36c1","item_hash_version":3,"event_class":"maintenance","record_hash_version":1,"record_hash":"20c7b0c8b5b75e576cdfee9db8712256239314f3129663c25346fc5474ea72e8"} +{"hash_algorithm":"sha256","ts":"2026-10-07T10:59:42.057Z","author":"claude-orchestrator","author_source":"asserted","agent_harness":"claude-code","agent_model":"claude-opus-5-5","agent_model_source":"probe","agent_instance":"1c3d835edba106c1302b3106","agent_provenance":{"model":{"value":"claude-opus-5-5","source":"probe"},"effort":{"value":"high","source":"environment"},"role":null,"topic":null,"version":{"value":"2.1.291","source":"probe"}},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/57","value":{"created_at":"2026-10-07T10:59:42.057Z","author":"claude-orchestrator","text":"Orchestrator verification of 48c5885 (gh-wf agent ended before reporting): npm and Bun release:check pass at exact 100/100/100/100 with the head-repository PR selection, no-change PR close, and public-acceptance pack isolation."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-07T10:59:42.057Z"}],"before_hash":"7856603481592282f9f474385c376c3cc43fda36d7852a74ff7285c9947a36c1","after_hash":"b8fa0b79ebb821182e4a23bc057047dec43ac677d113362a18f59d179b128eef","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":"cd41e4d4b9ab22a68a4623012ae36208294725ea16cc6f2795f61d918a091235"} +{"hash_algorithm":"sha256","ts":"2026-10-07T11:33:39.558Z","author":"claude-orchestrator","author_source":"asserted","agent_harness":"claude-code","agent_model":"claude-opus-5-5","agent_model_source":"probe","agent_instance":"1c3d835edba106c1302b3106","agent_provenance":{"model":{"value":"claude-opus-5-5","source":"probe"},"effort":{"value":"high","source":"environment"},"role":null,"topic":null,"version":{"value":"2.1.291","source":"probe"}},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/58","value":{"created_at":"2026-10-07T11:33:39.558Z","author":"claude-orchestrator","text":"CodeRabbit 4206349775 (valid): host paths behind file:// URLs, label colons (cwd:/home/...) and file:///C:/ drive URLs passed the gate. absolute-host-path now uses a lookbehind lead-in (adds file:// and >=2-char label colons, so drive letters stay with the Windows rule and C:/Users is one finding), the matched value is the path itself (removed the slice special case), and windows-host-path accepts file:/// drive URLs. New test fails on revert; URLs with ports, file://server shares, slash commands and API routes stay clean."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-07T11:33:39.558Z"}],"before_hash":"b8fa0b79ebb821182e4a23bc057047dec43ac677d113362a18f59d179b128eef","after_hash":"03b1f72be799a7c8a88cc58bb1f78b7426434c9452b0ecfb88a10f0798496bbf","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":"e38a0c6974e654b2b2ef30bf9868d5645bebd2427d091c9182c8c47b964cda1a"} +{"hash_algorithm":"sha256","ts":"2026-10-07T12:39:25.378Z","author":"claude-orchestrator","author_source":"asserted","agent_harness":"claude-code","agent_model":"claude-opus-5-5","agent_model_source":"probe","agent_instance":"1c3d835edba106c1302b3106","agent_provenance":{"model":{"value":"claude-opus-5-5","source":"probe"},"effort":{"value":"high","source":"environment"},"role":null,"topic":null,"version":{"value":"2.1.291","source":"probe"}},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/59","value":{"created_at":"2026-10-07T12:39:25.378Z","author":"claude-orchestrator","text":"CodeRabbit 4206922126/4206922131 (valid, docs/tracker only): retitled closed pm-github-rap0 so release notes no longer present the fixed root-sensitivity as current; README workflow order now lists the 'Verify repeat import is a no-op' step that WORKFLOW_IMPORT_SEQUENCE and the workflow already run."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-07T12:39:25.378Z"}],"before_hash":"03b1f72be799a7c8a88cc58bb1f78b7426434c9452b0ecfb88a10f0798496bbf","after_hash":"8526913bbb4dd79a59fcf2e6efc06d360b172bb154d3650995da7adc1fb19961","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":"98d91fdf9d0d65e67ec3a96a3a5214649e5ea2b52fba64c4704ceb0ff332edeb"} +{"hash_algorithm":"sha256","ts":"2026-10-07T13:42:15.444Z","author":"claude-orchestrator","author_source":"asserted","agent_harness":"claude-code","agent_model":"claude-opus-5-5","agent_model_source":"probe","agent_instance":"1c3d835edba106c1302b3106","agent_provenance":{"model":{"value":"claude-opus-5-5","source":"probe"},"effort":{"value":"high","source":"environment"},"role":null,"topic":null,"version":{"value":"2.1.291","source":"probe"}},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/60","value":{"created_at":"2026-10-07T13:42:15.444Z","author":"claude-orchestrator","text":"CodeRabbit 4207573690 (valid): three fail-closed refusal messages carried remnants of removed interpolation ('too large to scan ;', 'not valid JSON: (allowlist)', a template literal without interpolation). Messages now name the 8388608-byte scan cap and stay redaction-safe; tests assert the exact text for the size cap and malformed allowlist and fail on revert."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-07T13:42:15.444Z"}],"before_hash":"8526913bbb4dd79a59fcf2e6efc06d360b172bb154d3650995da7adc1fb19961","after_hash":"5c8b476b0eceeac9b0bd17c616fe3a589db695b7dbfa4eabb233cbc45a52e542","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":"7afae704192b183b387599f4938c18537cc78251c80205fbb5ecf8b97979b5fd"} diff --git a/.agents/pm/history/pm-github-rap0.jsonl b/.agents/pm/history/pm-github-rap0.jsonl new file mode 100644 index 00000000..17355017 --- /dev/null +++ b/.agents/pm/history/pm-github-rap0.jsonl @@ -0,0 +1,5 @@ +{"hash_algorithm":"sha256","ts":"2026-10-06T18:29:11.654Z","author":"pi-gh-cr3b","author_source":"asserted","agent_harness":"pi","agent_model":"glm-5.3:cloud","agent_model_source":"environment","agent_instance":"adacbc2ad7298f54bbf2c51f","agent_provenance":{"model":{"value":"glm-5.3:cloud","source":"environment"},"effort":null,"role":{"value":"implementer","source":"argv"},"topic":null},"op":"create","patch":[{"op":"add","path":"/metadata/id","value":"pm-github-rap0"},{"op":"add","path":"/metadata/title","value":"The comment-sync per-comment write-failure test is root-sensitive: withReadOnlyDirectory cannot make the tasks directory unwritable for root"},{"op":"add","path":"/metadata/description","value":"The review-round1 new Function test removal moved the chmod 0o555/finally cleanup of the comment-sync write-failure fixture into the exported withReadOnlyDirectory test helper. Root ignores directory write permissions, so under a root CI runner the helper applies no read-only mode, the fixture write succeeds, and the per-comment write-failure assertions in the comment-sync coverage test fail. Review round 3 guarded the equivalent import-lock fixtures with a visible node:test root skip (CodeRabbit 4198811222); the same non-root-only skip should cover this helper-based fixture so a root run skips visibly instead of failing."},{"op":"add","path":"/metadata/type","value":"Issue"},{"op":"add","path":"/metadata/status","value":"open"},{"op":"add","path":"/metadata/priority","value":2},{"op":"add","path":"/metadata/tags","value":["ci","root","tests"]},{"op":"add","path":"/metadata/created_at","value":"2026-10-06T18:29:11.654Z"},{"op":"add","path":"/metadata/updated_at","value":"2026-10-06T18:29:11.654Z"},{"op":"add","path":"/metadata/author","value":"pi-gh-cr3b"}],"before_hash":"3cc22dff72be7b14824654a7a64ea62b04799939b2fee54c1b5f52ca60bf6df0","after_hash":"b521f20f94b941e62c33b63fb6b3c5032378eaf3f6939757fd00919d735b98b3","item_hash_version":3,"message":"","event_class":"substantive","record_hash_version":1,"record_hash":"7598576e1542a80dd05f4ed7a6b58c49a8ff13661b43f3f0cd49f13aaf5ec023"} +{"hash_algorithm":"sha256","ts":"2026-10-06T18:29:15.904Z","author":"pi-gh-cr3b","author_source":"asserted","agent_harness":"pi","agent_model":"glm-5.3:cloud","agent_model_source":"environment","agent_instance":"adacbc2ad7298f54bbf2c51f","agent_provenance":{"model":{"value":"glm-5.3:cloud","source":"environment"},"effort":null,"role":null,"topic":null},"op":"comment_add","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T18:29:15.904Z"},{"op":"add","path":"/metadata/comments","value":[{"created_at":"2026-10-06T18:29:15.904Z","author":"pi-gh-cr3b","text":"Discovered during review round 3 (CodeRabbit 4198811228): replacing the new Function source-slicing test exposed that the coverage-remainder comment-sync write-failure fixture inherits the same root-sensitivity as the import-lock permission fixtures, but the review only authorized guarding import-lock.test.ts this round. Left as a follow-up so a root CI runner skips it visibly instead of failing the suite."}]}],"before_hash":"b521f20f94b941e62c33b63fb6b3c5032378eaf3f6939757fd00919d735b98b3","after_hash":"fb4a37de012356526686ec5389fa061e7e09b4b457c7d651629ed679aa10f87b","item_hash_version":3,"event_class":"substantive","record_hash_version":1,"record_hash":"d6e4ffba85380536727d94a73eb565065d71705ca79fc081d6f5bd5649ee89ad"} +{"hash_algorithm":"sha256","ts":"2026-10-06T18:54:50.637Z","author":"pi-gh-cr3b","author_source":"asserted","agent_harness":"pi","agent_model":"glm-5.3:cloud","agent_model_source":"environment","agent_instance":"adacbc2ad7298f54bbf2c51f","agent_provenance":{"model":{"value":"glm-5.3:cloud","source":"environment"},"effort":null,"role":{"value":"implementer","source":"argv"},"topic":null},"op":"release","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-10-06T18:54:50.637Z"}],"before_hash":"fb4a37de012356526686ec5389fa061e7e09b4b457c7d651629ed679aa10f87b","after_hash":"b89fdc51e0b524550952821c6d0370da152f5588f961174b46a1b07f4a9d3f07","item_hash_version":3,"event_class":"maintenance","record_hash_version":1,"record_hash":"7d4058fd77608086e097e462cd423e497082ffa4264af979a2e68663c76122bd"} +{"hash_algorithm":"sha256","ts":"2026-10-07T08:33:51.539Z","author":"claude-orchestrator","author_source":"asserted","agent_harness":"claude-code","agent_model":"claude-opus-5-5","agent_model_source":"probe","agent_instance":"1c3d835edba106c1302b3106","agent_provenance":{"model":{"value":"claude-opus-5-5","source":"probe"},"effort":{"value":"high","source":"environment"},"role":{"value":"implementer","source":"argv"},"topic":null,"version":{"value":"2.1.291","source":"probe"}},"op":"close","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-10-07T08:33:51.539Z"},{"op":"replace","path":"/metadata/status","value":"closed"},{"op":"add","path":"/metadata/closed_at","value":"2026-10-07T08:33:51.527Z"},{"op":"add","path":"/metadata/completed_at","value":"2026-10-07T08:33:51.527Z"},{"op":"add","path":"/metadata/close_reason","value":"Resolved on PR #108 by the orchestrator: all three read-only-directory comment-sync fixtures in test/coverage-remainder.test.ts (per-comment add failure gated and ungated, contended lock plus per-comment failure) now use a visible nonRootOnly skip when the suite runs as root, matching test/import-lock.test.ts; non-root CI still runs them (verified 3/3 pass)."}],"before_hash":"b89fdc51e0b524550952821c6d0370da152f5588f961174b46a1b07f4a9d3f07","after_hash":"a4c72770b40bae35167465dbc3b5e2943f9e17fae9b397d4eadc88e96b7e5bef","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":"7ec3019a2020edda3509a89e3c266351cd474f51ad83314d2f1458ddd46bf097"} +{"hash_algorithm":"sha256","ts":"2026-10-07T12:39:24.056Z","author":"claude-orchestrator","author_source":"asserted","agent_harness":"claude-code","agent_model":"claude-opus-5-5","agent_model_source":"probe","agent_instance":"1c3d835edba106c1302b3106","agent_provenance":{"model":{"value":"claude-opus-5-5","source":"probe"},"effort":{"value":"high","source":"environment"},"role":{"value":"implementer","source":"argv"},"topic":null,"version":{"value":"2.1.291","source":"probe"}},"op":"update","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-10-07T12:39:24.056Z"},{"op":"replace","path":"/metadata/title","value":"Root CI runs skip the read-only-directory comment-sync fixtures visibly instead of failing them"}],"before_hash":"a4c72770b40bae35167465dbc3b5e2943f9e17fae9b397d4eadc88e96b7e5bef","after_hash":"5af2b9d7e2249a76bf7ec61e1073349d194808ac7e79710194b59fe4f866b978","item_hash_version":3,"message":"Past-tense, user-visible title for the changelog (CodeRabbit 4206922126)","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":"bf4e28b15733758e5ac485d9c86e3376e5960f824cc1a80551ef4c3579499ae3"} diff --git a/.agents/pm/issues/pm-github-2ao1.toon b/.agents/pm/issues/pm-github-2ao1.toon new file mode 100644 index 00000000..830d72ff --- /dev/null +++ b/.agents/pm/issues/pm-github-2ao1.toon @@ -0,0 +1,22 @@ +id: pm-github-2ao1 +title: PM CLI filtered native Bun tests falsely report an empty run +description: "Pinned PM CLI/SDK 2026.10.4 recognizes the native Bun name filter but its positive execution receipt patterns omit Bun output. A real linked Bun 1.3.14 command executes two tests, reports 2 pass and Ran 2 tests, exits zero, then pm test changes the outcome to empty_run with missing_positive_execution_receipt. Track upstream parser compatibility separately from the package review fixes; do not patch the protected CLI here." +type: Issue +status: open +priority: 2 +tags[3]: bun,testing,upstream +created_at: "2026-10-06T14:30:31.652Z" +updated_at: "2026-10-06T15:37:56.669Z" +author: codex-sol +repro_steps: "Link bun test --timeout 600000 -t \"canonical changelog includes|historical fixture provenance reports\" ./test/generate-changelog.test.ts ./test/privacy-gate.test.ts; run the linked test with pm test --run --only-last --progress and compare Bun output with the final run_results row." +expected_result: "A positive native Bun execution receipt is accepted, while zero-test filtered Bun runs remain refused." +actual_result: Bun executes and passes two tests with exit zero; pm test reports empty_run after failing to recognize the positive receipt. +comments[2]{created_at,author,text}: + "2026-10-06T14:32:03.841Z",codex-sol,"Reproduced through the pinned installed CLI using native Bun 1.3.14: subprocess exit zero, two genuine passing tests and a Ran 2 tests receipt; run_results is rewritten to failed/empty_run with missing_positive_execution_receipt. Read-only SDK inspection confirms the positive receipt patterns accept Node-style tests/pass counters and passed wording, but omit Bun pass-count order and Ran wording. This is an upstream test-runner compatibility follow-up, outside PR round-1 source fixes. No CLI dependency files were changed. The package registers the full native test files instead of the unsupported filtered command." + "2026-10-06T15:37:56.669Z",claude-orchestrator,"Reproduced by the orchestrator on PM CLI 2026.10.6 + Bun 1.3.5 in a clean tracker: 'bun test --test-name-pattern alpha test/x.test.ts' (2 pass, 1 filtered out, exit 0) is reported status failed, exit_code 1, failure_category empty_run; the unfiltered command passes. Filed upstream: https://github.com/unbraind/pm-cli/issues/1418" +files[2]{path,scope}: + test/generate-changelog.test.ts,project + test/privacy-gate.test.ts,project +docs[1]{path,scope}: + README.md,project +body: "" diff --git a/.agents/pm/issues/pm-github-4a03.toon b/.agents/pm/issues/pm-github-4a03.toon new file mode 100644 index 00000000..1ddbd634 --- /dev/null +++ b/.agents/pm/issues/pm-github-4a03.toon @@ -0,0 +1,14 @@ +id: pm-github-4a03 +title: Guard stale completed SDK import journals after local tracker files are reset +description: A synthetic import into a real Git worktree completes an atomic journal. Resetting the worktree to its pre-import baseline removes the imported files while ignored completed journals persist. A subsequent import can report recovered success without restoring the absent items. The gated importer now verifies durable post-write provenance and refuses any push; follow up with a minimal SDK reproduction and dependency-level recovery repair. +type: Issue +status: open +priority: 1 +tags[3]: "area:import","area:sdk","type:defect" +created_at: "2026-10-05T07:36:59.028Z" +updated_at: "2026-10-05T07:43:25.513Z" +author: codex-sol +files[2]{path,scope}: + index.ts,project + test/import-gate.test.ts,project +body: "" diff --git a/.agents/pm/issues/pm-github-7d1h.toon b/.agents/pm/issues/pm-github-7d1h.toon index 3c3ef871..8a73b94b 100644 --- a/.agents/pm/issues/pm-github-7d1h.toon +++ b/.agents/pm/issues/pm-github-7d1h.toon @@ -6,11 +6,12 @@ status: closed priority: 2 tags: [] created_at: "2026-08-16T00:35:13.077Z" -updated_at: "2026-08-16T03:00:52.637Z" +updated_at: "2026-10-06T15:18:15.655Z" closed_at: "2026-08-16T01:32:11.608Z" completed_at: "2026-08-16T01:32:11.608Z" author: claude-code acceptance_criteria: "manifest.json declares a top-level pm_min_version equal to the peerDependencies floor; the devDependency on the CLI is an exact pin at or above that floor; removing the manifest floor, loosening the pin to a caret range, or changing either floor independently each fail the test" +release: 2026.8.17 resolution: "manifest.json declares pm_min_version 2026.8.3, equal to the peerDependencies floor, and devDependencies exact-pins the CLI at 2026.8.15. compatibility-floor.test.ts asserts three properties and each now has its own failing mutation: rewriting the peer range to an exact pin, removing or changing the manifest floor, and loosening the dev pin to a caret range. A fourth mutation covers the parse guard added during review." expected_result: "A host CLI older than the package's declared floor is refused before the extension loads, on every install path — including a globally installed CLI that npm never sees." actual_result: "npm refused a locally resolved dependency below 2026.8.3, while the pm CLI accepted any host at or above 2026.7.28 — a span of releases in which the package loaded against a CLI its own peer declaration says it does not support." diff --git a/.agents/pm/issues/pm-github-hptv.toon b/.agents/pm/issues/pm-github-hptv.toon new file mode 100644 index 00000000..f4ba6a3c --- /dev/null +++ b/.agents/pm/issues/pm-github-hptv.toon @@ -0,0 +1,224 @@ +id: pm-github-hptv +title: Bound native Bun CLI repeats of a completed public atomic import +description: "The intermittent historical installed-Bun repeat has not reproduced in three fresh packed repeats and three live-public-snapshot repeats. A real dangling comment-lock symlink independently reproduced an unbounded busy loop, fixed by honoring the existing wait budget. Installed acceptance retains its 45-second execution deadline and now force-kills the process group after a separate five-second cleanup window; timeout always fails even on a later zero exit. Recent repeats preserve durable bytes and strict health. The historical root cause remains unestablished." +type: Issue +status: in_progress +priority: 1 +tags: [] +created_at: "2026-10-05T08:28:54.625Z" +updated_at: "2026-10-07T13:42:15.444Z" +author: codex-sol +comments[61]{created_at,author,text}: + "2026-10-05T09:07:19.298Z",codex-sol,"A fresh bounded native Bun CLI repeat of the latest packed candidate passed in 4.22 seconds with exit zero and byte-identical tracker contents. This contrasts with the earlier long-running repeat, so retain the issue as an intermittent installed-host investigation rather than claiming a consistent Bun failure. All 375 native Bun library and operational tests also pass." + "2026-10-05T09:55:33.050Z",codex-sol,"The final packed candidate matched all three authored runtime modules byte-for-byte. Its fresh Node public import passed with three issues, a byte-identical second run, zero gate findings, and strict health in 24.94 seconds. A bounded direct native Bun CLI repeat, using the same pinned host and startup executable path, timed out at 45.06 seconds and was terminated with SIGTERM; durable tracker bytes stayed unchanged. This reproduces the intermittent installed-host boundary alongside all 420 passing native Bun library and operational tests. No scratch remote was configured and no public-data push occurred." + "2026-10-06T11:17:41.296Z",codex-sol,"Packed installed-CLI reproduction with the pinned host passes three consecutive native Bun repeats under the unchanged 45-second watchdog. The complete fixture test took 26.63 seconds, including packing, installation, Node import, and all three repeats; each retained item bytes and unchanged bare-remote refs. Separately reproduced a real dangling comment-lock symlink: open with exclusive creation returns EEXIST while stat returns ENOENT, causing the old immediate-continue path to busy-spin forever. The preserved fix adds sleep and checks the existing wait budget. This is a proven local lock defect, not yet proof of the historical installed-host timeout cause. Further public-data and diagnostic acceptance follows." + "2026-10-06T11:33:55.510Z",codex-sol,"Fresh live public snapshot acceptance passed: one non-PR pm-todos issue updated since 2026-10-01, fetched with paginated gh reads, replayed unchanged through the packed candidate HTTP client and pinned host. Node import 2.49 seconds; byte-identical Node repeat 2.36 seconds; three byte-identical native Bun repeats 2.28, 2.03, and 2.33 seconds. Privacy PASS, strict health PASS, no remote configured. The complete all-state live import separately refused two email findings in older issue content before mutation, as required; no allowlist was added. Direct external HTTPS probes were intermittent, so the public snapshot receipt separates live retrieval from local installed-host execution. No deterministic SDK comment-add flakiness has reproduced; real create/comment/deduplication and permission-failure tests pass." + "2026-10-06T11:37:10.437Z",codex-sol,"Acceptance now uses a shared process-group watchdog: the original 45-second deadline sends SIGTERM, then a separate five-second cleanup window sends SIGKILL. A timed-out command is always rejected, even if it exits zero afterward, and no writer step follows. The deterministic negative control uses a real CPU-spinning child that ignores SIGTERM; the watchdog force-kills it with unchanged durable fixture bytes. This bounds the historical installed-host timeout without claiming the dangling-lock defect was its original cause or increasing the execution deadline. The CLI lock defect itself now honors its existing wait budget." + "2026-10-06T11:44:34.944Z",codex-sol,"Final frozen packed acceptance passes: one live public snapshot issue imported with gate PASS; Node import 4.02 seconds and byte-identical Node repeat 4.33 seconds; three native Bun repeats 3.36, 3.47 and 4.18 seconds with gate PASS and unchanged durable tracker bytes. Strict health passes in the disposable pinned-host project, with no remote configured. The spinning-child watchdog negative control also passes natively under Bun. Historical timeout cause remains unestablished; no deterministic SDK comment-add flakiness reproduced." + "2026-10-06T11:52:11.132Z",codex-sol,"Native Bun full-suite negative control found a real HTTP boundary failure: a local server accepts the connection and never responds; requestOnce relies on ClientRequest.setTimeout(30000), but under installed Bun 1.3.5 no timeout rejection arrives before the existing 45-second test deadline. The identical full Node case passes. A fresh focused native reproduction is running. Stopped the superseded Bun release check so the transport fix can be verified on frozen source. This is an independently observed request-timeout defect, not proof of the historical public repeat cause." + "2026-10-06T11:53:54.456Z",codex-sol,"Focused native Bun before-fix reproduction fails deterministically: one local silent-server test reaches 45.00 seconds without the 30-second socket-timeout rejection (38 unrelated tests filtered). Replacing reliance on ClientRequest.setTimeout with an explicit 30-second wall-clock timer, cleared on Promise settlement; the timer destroys the request and rejects directly. Regression now exercises two real responses concurrently: no headers, and headers plus a partial body that never completes. No execution deadline or test timeout was raised. Node and native Bun focused validation is underway." + "2026-10-06T11:55:13.799Z",codex-sol,"The expanded native regression also caught a second concrete behavior: destroying a partial-response request before rejecting can synchronously emit response end under Bun, resolving the incomplete response instead of rejecting. The deadline callback now rejects first, then destroys. The real silent-server and never-ending partial-body cases test that ordering at the original 30-second limit. Node passes the expanded case; final native focused run and complete gates are being reverified. These are transport-boundary observations, not an assertion about the historical public-data repeat." + "2026-10-06T11:55:38.455Z",codex-sol,"After-fix focused real-HTTP regression PASS under both runtimes: Node 30.02 seconds, native Bun 30.02 seconds; both silent headers and partial never-ending body reject with the expected 30-second timeout. The test keeps its 45-second harness deadline and asserts completion below 40 seconds. Build:test and ESLint pass. Explicit deadline rejection precedes destroy and its timer is cleared on every Promise settlement. Restarting full npm release checks and all native Bun files on this frozen fix." + "2026-10-06T11:58:06.722Z",codex-sol,"Packed acceptance on transport-fix implementation 5ed8c47 PASS: one live public snapshot issue imported with gate PASS, Node import 6.17 seconds, byte-identical Node repeat 5.15 seconds, native Bun repeats 6.06/8.16/4.80 seconds with byte-identical durable data, strict health PASS and no remote. Timings include concurrent full validation load. The linked real timeout regression passes both isolated add verification and pm test --run --only-last --progress; it is recorded alongside the passing watchdog test. No SDK comment-add flakiness reproduced." + "2026-10-06T12:42:46.684Z",codex-sol,"Final timeout assurance: full native suite passes 487/487 across 30 files with exit zero; both full release commands pass. The real silent and never-ending-body HTTP controls reject at the original 30-second deadline under Node and Bun; the dangling comment lock honors its existing wait budget. Packed public acceptance after the transport fix passes one nonempty import, unchanged Node and three Bun repeats, gate and strict health without a remote. Historical 45-second repeat cause remains unestablished, but acceptance keeps 45 seconds plus a separate forced-cleanup window and always rejects a timeout. No deterministic SDK comment-add flakiness reproduced." + "2026-10-06T12:53:57.413Z",codex-sol,"Investigating PR #108 clone-only workflow setup failures. Plan: reproduce in a scratch clone, trace the npm lifecycle trigger and Git layout difference, isolate package preparation from the fixture tracker, then verify regression and full release gates in both layouts." + "2026-10-06T12:56:15.328Z",codex-sol,"Reproduced the reported failure in a fresh clone with Node 22 and npm 10.9.8 after building. The failing command is npm pack --ignore-scripts in prepareWorkflowFixture, before pm package install. npm 10 pacote runs prepare despite ignore-scripts; npm 11 checks ignoreScripts and the same clone passes. The fixture passes its external PM_PATH into the package-root hook. Testing the worktree with npm 10 to verify whether Git layout is causal." + "2026-10-06T12:57:10.585Z",codex-sol,"Confirmed npm 10 also fails in the package worktree with an external PM_PATH; Git layout and preinstalled merge drivers are not causal. The fix binds package packing to the package-local tracker and leaves caller import commands on their fixture tracker. Added a regression that forces the real prepare hook before packing in a fresh clone and a linked worktree, so npm 11 cannot mask the bug. The reusable caller workflow does not pack this source checkout and its npm ci/merge install run with its own repository tracker." + "2026-10-06T12:58:11.661Z",codex-sol,"Regression verified red/green: removing only the package-local PM_PATH override fails with the reported outside-repository error; restoring it passes. pm test pm-github-hptv --run --match 'workflow packing isolates' --progress passed. ESLint and test TypeScript build passed. Running all three originally failing acceptance tests plus the layout regression under npm 10 in the worktree before pushing." + "2026-10-06T12:59:01.408Z",codex-sol,"Both layout regressions fail on reverting the PM_PATH override, including the linked worktree with the pinned CLI. npm 10 exposed a second packing detail after fixing the root mismatch: successful prepare stdout contaminates npm pack --json by default. Packing now explicitly uses --foreground-scripts=false to preserve the JSON receipt; the regression also supplies a conflicting inherited foreground-scripts setting." + "2026-10-06T13:04:14.167Z",codex-sol,"Worktree acceptance under Node 22.23.1/npm 10.9.8 passed all four selected tests: clone/worktree prepare isolation, all adversarial workflow cases, clean create/update review workflow, and the packed native Bun repeat with all three repeats inside 45 seconds. The complete selected run finished in 248.81 seconds under concurrent validation load. Implementation fd0834a is pushed; fresh-clone release gates are in progress." + "2026-10-06T13:08:15.645Z",codex-sol,"Pushed CI run for fd0834a passes all 488 tests on Node 22 and Node 26, including the three repaired workflow cases. Both jobs still fail the strict coverage gate at 99.86 percent branches: index.ts lines 274, 4092, and 4117 are the successful gh credential-helper paths. Existing coverage depended on local gh authentication. Adding a deterministic synthetic gh auth success test with a mock server to cover these branches without host credentials." + "2026-10-06T13:09:41.700Z",codex-sol,"Deterministic credential-helper regression passes directly and through pm test --run --only-last --progress. It removes both env tokens, shadows gh with a synthetic auth helper, checks trimmed-token Authorization at the mock API, and asserts token_source=gh plus credential redaction. ESLint and build:test pass. Obtained the exact CI Bun 1.3.14 runtime in disposable storage for final fresh-clone gates." + "2026-10-06T13:18:34.181Z",codex-sol,"CI run 37468640976 at a849280 passes test (22) and test (26), including exact all-source coverage and the full native Bun 1.3.14 suite. CodeQL and the security scan also pass. Reviewer draft skips are not approvals. Fresh-clone npm and Bun release checks are still running locally with host GitHub credentials removed." + "2026-10-06T13:28:14.761Z",codex-sol,"Final fresh-clone gates at pushed implementation a849280 PASS: npm run release:check under Node 22.23.1/npm 10.9.8, and bun run release:check under Bun 1.3.14 with Node 26.7.0 tooling. Both clones used an empty gh configuration and no environment GitHub tokens. Each passed 489 tests, exact all-source 100/100/100/100 across 12 modules, zero production audit vulnerabilities, package dry-run, changelog check, and publish attestation. Coverage receipts: 8398/8398 lines and statements in both; Node 22 has 209/209 functions and 2223/2223 branches, Node 26 has 208/208 functions and 2222/2222 branches. Packed native Bun repeats retained the original 45-second execution budget. CI test (22) and test (26) also pass. Only tracker receipts remain to commit; the historical intermittent Bun timeout cause remains outside this setup fix." + "2026-10-06T13:52:24.540Z",codex-sol,"Review round 1: read all 16 inline findings and both review bodies. Verifying complete comment reads, diff hunk parsing, fatal Git decoding, canonical tracker paths, workflow credentials and pin grammar; adding revert-proven regressions before fresh-clone npm and Bun gates." + "2026-10-06T13:58:29.574Z",codex-sol,"All 16 review findings verified valid and fixed. Every finding has a regression with a proven nonzero revert run and passing restored run; the two hunk findings share one proof. Real HTTP malformed and non-array second comment pages refuse both atomic and incremental gated imports with unchanged tracker state. Real tracked/staged invalid UTF-8 and symlinked-checkout cases pass. Credentials are step-scoped, install scripts disabled, checkout persistence disabled, and the branch lease uses checkout refs without a pre-push network read." + "2026-10-06T14:06:25.566Z",codex-sol,"Full-suite validation caught a decoder correction: spawnSync can leave stderr undefined as well as null on ENOENT, so the Git reader now checks for an available buffer before fatal decoding and preserves the spawn diagnostic otherwise. The existing real missing-Git subprocess regression passes with the correction; tracked/staged invalid UTF-8 refusal remains passing. Superseded full runs were stopped and will be replaced by fresh-clone runs of the corrected pushed head." + "2026-10-06T14:10:27.146Z",codex-sol,"Persistent revert receipts: 4196039888 workflow input shell; 4196039928 malformed second comment HTTP pages; 4196039937 and 4196078374 diff hunk content; 4196039948 tracked/staged invalid UTF-8; 4196078360 workflow credential isolation; 4196078420 symlinked checkout; 4196078439 pre-write command exit code. Each negative control exited 1 after reverting to the reviewed code, and restored code exited 0. The missing-Git follow-up also has an independent failing revert and passing restore. Selected native Bun 1.3.14 gate and workflow regressions pass." + "2026-10-06T14:20:34.810Z",codex-sol,"Corrected implementation c8b9329 passes both fresh-clone release commands with 503/503 tests and no skips: npm under Node 22.23.1/npm 10.9.8, Bun 1.3.14 with Node 26.7.0 tooling. Both used empty gh configuration and no environment GitHub credentials. A new diagnostic assertion initially failed native Bun because empty PATH prevents the tracker read before generator startup; the fixture now separately injects generator startup and signal outcomes after restoring PATH. Node and native Bun pass this portable fixture, and reverting the diagnostic implementation still fails. Final pushed-head clones will reverify the bundled tracker receipts and test correction." + "2026-10-06T14:33:50.266Z",codex-sol,"Final source/test head f572382 passes both fresh-clone release commands, 503 tests and exact all-source coverage. Strict clone health with merge drivers passes. Both CI Node jobs and native Bun pass. Local privacy separately audits 3581 objects, including unreachable objects, with approved identities and no secret/host-path findings. Separate upstream PM runner follow-up pm-github-2ao1 records a false empty_run on genuinely passing filtered Bun tests; the full 29-test native command is linked and passes. All 16 round-1 findings are fixed with verified failing revert controls. Items remain open and claims released for orchestrator verification." + "2026-10-06T15:17:49.785Z",codex-sol,"Review round 2 of PR #108: preserve published changelog binning through explicit item release metadata, add actionable diagnostics and real-shell rejection assertions for reusable workflow inputs, and strengthen the malicious-import tracker no-write assertion with a temporary injected-write negative control. Verify all release gates in fresh clones of the pushed branch; leave the item open for orchestrator verification." + "2026-10-06T15:19:21.145Z",codex-sol,"Findings 4197056035, 4197055992, and 4197056044 addressed. Set pm-github-7d1h release=2026.8.17 using the CLI and regenerated with npm run changelog:full. Compared every released section byte-for-byte with origin/main: identical; only the added Unreleased entry differs. Real workflow shell tests pass for numeric pins and every rejected repository/version/branch input with its expected ::error:: diagnostic. The adversarial tracker assertion now checks scoped porcelain stdout; temporarily appending a newline to fixture tracker settings made that test fail with M .agents/pm/settings.json and exit 1, then the injection was removed. Focused suite: 3/3 pass. Docstrings: 130 declarations in 11 files; lint, duplication (zero duplicates at the configured threshold), and typecheck pass. Fresh-clone npm and Bun release checks are next." + "2026-10-06T15:36:25.199Z",codex-sol,"Review round 2 verified at pushed code commit 7a41660f190a108cca4bc10e270eff6b4864aa3e. Separate fresh remote branch clones installed dependencies with npm ci --ignore-scripts. Both npm run release:check and bun run release:check exited 0: 504/504 tests, zero skips/cancellations; complete coverage of all 12 authored modules with lines/statements 8406/8406, functions 208/208, branches 2225/2225 (exact 100/100/100/100). Typecheck, build, docstrings (130 declarations across 11 files), lint, configured duplication gate (zero duplicated lines), privacy (2829 objects), production audit (zero vulnerabilities), pack dry-run, changelog check, and release-publish attestation all passed. Targeted review tests ran via pm test --run --progress and passed 3/3; changelog tests passed 3/3. Tracker write negative control failed as expected and was removed. All released changelog sections match origin/main byte-for-byte; no additional re-binned items. Full release receipts apply to the code commit; the follow-up commit contains only tracker evidence and claim release, with privacy/changelog verification to follow. No new blockers. Keep this item open for orchestrator verification." + "2026-10-06T18:23:39.081Z",pi-gh-cr3b,"Review round 3 (4 CodeRabbit findings from the 17:55Z full review) in progress on the package worktree. 4198811214: a fresh coverage run at the closing head re-measured 504/504 tests, exact 100/100/100/100, matching the close_reason receipt; the 503/503 in actual_result was the stale f572382 receipt. actual_result on pm-github-9cjx now cites 504/504 with the supersession note and the item stays closed. 4198811222: the degraded-mode lock test is split so its chmod 0o555 half and the stale-unlink permission-failure test skip visibly via node:test { skip } when process.getuid is 0, using the same non-root pattern the file already had. 4198811228: the source-slicing new Function test is replaced by a behavioral test of a new exported test/helpers read-only-directory helper that restores permissions in finally; coverage-remainder now uses the same helper. 4198811219 docs edit follows the remeasured counts." + "2026-10-06T18:54:36.888Z",pi-gh-cr3b,"Review round 3 receipts, all four findings fixed at b446963 on feat/import-privacy-gate. 4198811214: fresh coverage at the closing head re-measured 504/504, matching close_reason; actual_result on pm-github-9cjx updated to 504/504 with a supersession note (503/503 was the stale f572382 receipt); item stays closed. 4198811219: docs/coverage-gap-inventory.md closing section marked historical (487-count receipts, pre-verification open note) with a current-status line citing the closed item, exact 100/100/100/100, and the closing 504/505 counts. 4198811222: import-lock permission fixtures now skip visibly via node:test { skip } when process.getuid is 0; the degraded-mode test was split so the file-parent half runs everywhere; nothing becomes uncovered because the fixtures still execute in non-root CI. 4198811228: the new Function source-slicing test is gone; the new exported test/helpers/read-only-directory.ts withReadOnlyDirectory helper restores the previous mode in a finally block, is used by coverage-remainder, and is behaviorally tested with a throwing callback plus mode-restored assertion. Focused receipts: node --test test/import-lock.test.ts 25/25; node --test test/review-round1.test.ts test/coverage-remainder.test.ts 21/21; both also pass under bun test. Worktree coverage at the final head: 505/505, zero failures/skips, exact 100/100/100/100. Fresh clones of the pushed branch: npm run release:check PASS and bun run release:check PASS, both 505/505 with the exact 100 gate, 130 documented declarations, zero duplication, privacy, audit, pack, changelog and attestation green; npm run changelog:check PASS. Follow-up pm-github-rap0 records the remaining root-sensitive helper-based fixture." + "2026-10-06T19:48:30.209Z",claude-orchestrator,"CodeRabbit full review at 9a8620c (4 findings), handled by the orchestrator: (1) 9cjx description updated to the closing 505-test receipt (487 marked historical). (2) gate.ts git status/diff now run with --literal-pathspecs. Defence in depth: tracker paths always start with a directory, so leading pathspec magic was inert and git literal-matches existing glob-named files; the new guard test 'tracker file names that look like pathspec magic are still scanned' passed before and after. (3) README caller example no longer adds a caller-level concurrency block, matching docs/sync-workflow-caller.yml (the reusable workflow already serializes per repository). (4) Declined: restoring the old nested-directory fallback in resolvePmDataDir; the SDK resolvePmRoot is the canonical tracker resolution and a .agents/pm without settings.json is not a pm tracker." + "2026-10-06T20:42:55.954Z",claude-orchestrator,"CodeRabbit full review at 7c425f2 (2 findings), fixed by the orchestrator: (1) absolute-host-path matched any slash-prefixed word, so GitHub slash commands (/assign), repo-relative links (/docs/setup.md) and API routes (GET /api/v1) failed --gate imports; the rule is now anchored to host-identifying roots (home, Users, root, tmp, var, etc, opt, srv, mnt, media, private, Volumes, usr, run, proc, data, nix, scratch, workspace, builds, Library, System) with a boundary after the root, matching the README's documented scope. Test 'host-path rule flags host-identifying roots, not slash commands, links or API routes' fails on revert; existing home/tmp/srv fixtures stay detected. (2) http-boundary timeout test now awaits server.close()." + "2026-10-06T21:03:24.422Z",claude-orchestrator,"Follow-up to the host-path anchoring: the gate exempted 'structural' JSON-patch pointers (/metadata..., /body...) from host-path rules only because the old unanchored rule matched every pointer. With host-root anchoring no pointer can match (windows rule excludes a preceding '/', JSON pointers escape '~'), so the exemption was dead code. It is removed, and pointers are scanned like data (stricter: a secret in a field key is caught). Coverage back to exact 100/100/100/100." + "2026-10-06T21:45:35.057Z",claude-orchestrator,"CodeRabbit full review at ed75fde (4 findings), fixed by the orchestrator: 9cjx description/actual_result/close_reason reconciled to 504/504 at 7a41660; canonical caller (docs + README) no longer hardcodes repository (the reusable workflow refuses foreign repositories and defaults to the caller); email-address rule exempts the git@ SSH service account (git@github.com:owner/repo.git) with positive control gitlover@; unicode-escape decoder uses a sticky regex instead of slicing the suffix per character (linear on large history lines)." + "2026-10-06T22:03:02.088Z",claude-orchestrator,"Greptile 4200862803 on ae28d9e (valid, introduced by the orchestrator's git@ exemption): exempting every local part 'git' let 'Contact git@example.org' pass. The exemption moved from confirm() into the email pattern: only the SCP SSH URL shape git@host: is skipped (negative lookahead requiring a path character right after the colon), with a lookbehind so matches cannot start inside a local part. Tests: git@ contact addresses (incl. 'git@example.org: thanks', 'agit@github.com:x') stay flagged; fails on revert." + "2026-10-06T22:59:21.028Z",claude-orchestrator,"CodeRabbit full review at 194cf12, handled by the orchestrator: (1) parseUnifiedDiff skipped every line while no target file was set, so an operator --diff with a hunk after 'diff --git' without ---/+++ or after '+++ /dev/null' passed with an added credential. Hunks are now parsed even without a target, and added lines without one fail closed; genuine deletions still parse. Test cases in 'malformed, truncated, and binary diffs fail closed' fail on revert. (2) Two privacy-gate tests that write POSIX sh git wrappers skip visibly on win32 (CI is Linux). (3) PATH note on the no-git test: informational, no change. (4) hptv stays in_progress on purpose: it closes after PR 108 merges." + "2026-10-07T00:23:51.656Z",claude-orchestrator,"CodeRabbit 4201687009 (review at d7041ad), reproduced and fixed by the orchestrator: with a caller whose extensions.enabled list omits pm-github, the install edits settings.json and appends a workspace-history event; a no-change sync then failed with 'no item link could be generated' (and a real sync would have committed the setup change). The commit step now restores settings.json and history/_workspace.jsonl to HEAD (or removes them when HEAD lacks them) after the final gate. New workflow test 'an install-only settings change never reaches the sync commit' runs the real step shells from main with an explicit enabled list, asserts the precondition, expects exit 0 / no PR / no pushed branch, and fails on revert. Also added top-level permissions: {} (zizmor excessive-permissions); the job keeps its least-privilege block." + "2026-10-07T01:12:40.286Z",claude-orchestrator,"CodeRabbit full review at 0f0c8c9 (2 findings), fixed by the orchestrator: README caller prerequisites now name the repository settings the review-PR step needs (read/write token permissions; 'Allow GitHub Actions to create and approve pull requests') and that GITHUB_TOKEN-opened PRs get no pull_request CI; test/helpers/bounded-cli.ts decodes child streams with setEncoding('utf8') so a multibyte character split across chunks is not corrupted. New test 'multibyte output split across stream chunks decodes intact' fails on revert." + "2026-10-07T02:20:59.122Z",claude-orchestrator,"CodeRabbit 4202386304 (review at d57335e): JSON object keys in history/planned records were never scanned (only values), so a token used as a key passed while the same text in .toon failed. Keys are now scanned as text. Test 'history object keys are scanned like values' fails on revert." + "2026-10-07T03:21:18.330Z",claude-orchestrator,"CodeRabbit full review at 61b7ed6 (2 trivial test findings), fixed by the orchestrator: the githubHandler doc comment moved onto githubHandler (it sat above planEntry's), and the idempotency-refusal test now asserts the observable property (no 'plan verified' line before the refusal) instead of a regex that always matched 'Fetching issues'." + "2026-10-07T04:27:56.381Z",claude-orchestrator,"CodeRabbit Major 4203042480 (review at 08ca152), a real merge blocker: loadFixtureExemptions resolved provenance for every historical_test manifest entry via 'git ls-tree ', and the pinned commit 434df42 is a branch commit that is NOT reachable from main after a squash merge, so the privacy gate would fail on main. Provenance is now resolved only for blobs in the audited object inventory (historical blobs vanish with the squash); present blobs with mismatched or unresolvable provenance still fail closed. The provenance test now covers both cases and fails on revert. Verified by simulating the squash merge in a scratch clone and running the privacy gate." + "2026-10-07T05:27:52.940Z",claude-orchestrator,"CodeRabbit Major 4203392922 (review at 66a3a43), reproduced and fixed by the orchestrator: in the gated non-atomic import, a failed update/close/reopen reconciliation only incremented 'skipped', and verifyImportedProvenance passed because the item kept its provenance tag, so the gated import (and the reusable workflow) reported success while the tracker differed from GitHub. The gated path now fails closed when the write count differs from the verified plan. Test 'a gated non-atomic import fails closed when a planned write fails' (one refused close plus one clean update) resolves successfully without the fix and rejects with it." + "2026-10-07T06:31:48.660Z",pi-glm,"Review round follow-up (PR findings 4203767825 + 4203767821), pi-glm: (1) make syncGithubCommentsToAnnotations report a failed count (contended lock, unreadable existing comments, per-comment add failures) and fail gated imports closed when any planned native comment was not written or a created id was unparsable while annotations sync is on — both on the non-atomic and the atomic path; ungated behaviour stays warn-and-continue. (2) docs/coverage-gap-inventory.md: describe the 505/505 figure as a later-head measurement so it agrees with the authoritative 504/504 closing receipt. Regression tests go into test/coverage-remainder.test.ts with the existing helpers." + "2026-10-07T07:11:31.984Z",pi-glm,"Disposition of both review findings (pi-glm): 4203767825 (index.ts) — VALID, fixed. syncGithubCommentsToAnnotations now returns a failed count (contended lock and unreadable existing comments count every planned comment; each refused add counts one). runImport accumulates the failures from every call site and an unparsable created id with annotations sync on counts the planned comments; both the non-atomic and the atomic path route the divergence through one shared failClosedOnPlanDivergence refusal with the existing \"planned write(s) failed\" wording, so a gated --comments-mode annotations|both import now rejects instead of exiting clean. Ungated behaviour is unchanged (warn and continue). 4203767821 (docs/coverage-gap-inventory.md) — VALID, fixed: 504/504 at the closing head 7a41660 is now described as the authoritative closing count and 505/505 explicitly as a later-head measurement. New regression tests in test/coverage-remainder.test.ts: \"a gated import fails closed when the comment-sync lock is contended\", \"a gated import fails closed when a per-comment add fails mid-sync\", \"a gated import fails closed when a created id cannot be parsed with annotations sync on\", \"a gated atomic import fails closed when committed items lack their planned comments\", and the ungated control \"the same comment-sync failures without --gate still import with a warning\". Two existing result-shape assertions (test/import-errors.test.ts, test/import-lock.test.ts) were extended with the new failed field. Each gated test was proven to fail on the reverted production code (4 rejected, control passed) before restoring the fix." + "2026-10-07T07:11:36.590Z",codex,"Gate results (pi-glm): npm test 516/516 pass; npm run coverage exact 100/100/100/100 across the 12 authored modules; npm run release:check and bun run release:check both exit zero (typecheck, build, docstring, lint, privacy, coverage, audit:prod, pack:dry-run, changelog:check, publish attestation all green). npm run changelog:check: Changelog is up to date. Open risk: none identified for this change; note the contended-lock regression tests each spend the ~30s import-lock wait budget by design (same as the existing contention test)." + "2026-10-07T07:11:43.075Z",pi-glm,"Attribution note (pi-glm): the previous gate-results comment was recorded under a default author because the session author env was not exported in that shell; it is mine (pi-glm) and its contents stand as written." + "2026-10-07T07:11:56.715Z",pi-glm,"Committed as 0ad387f (gate fix + regression tests), pi-glm. The doc-count fix follows in its own commit." + "2026-10-07T07:12:00.358Z",pi-glm,"Both findings landed (pi-glm): 0ad387f fail-closed gated comment writes + regression tests; dc87f02 coverage-gap inventory now calls 504/504 at 7a41660 authoritative and 505/505 a later-head measurement. Pushing the branch now." + "2026-10-07T07:31:19.560Z",claude-orchestrator,"CodeRabbit full review at 011f850 (2 findings), fixed by the orchestrator: review-PR bodies now add a 'proposed' link to each item on SYNC_BRANCH next to the permanent main link (main links don't resolve, or show the old version, until merge); the executed workflow test asserts it and fails on revert. docs/sync-workflow-caller.yml now states the 'Allow GitHub Actions to create and approve pull requests' + read/write workflow-permissions requirement." + "2026-10-07T08:33:52.212Z",claude-orchestrator,"CodeRabbit full review at 7e981cb (2 findings), fixed by the orchestrator: the three read-only-directory comment-failure tests skip visibly under root (closes pm-github-rap0), and executeWorkflow in test/import-gate.test.ts decodes child streams with setEncoding('utf8') like bounded-cli.ts." + "2026-10-07T09:33:28.837Z",pi-glm,"Starting review round 2 (CodeRabbit findings 4205293132, 4205293127, 4205293135): (1) select the review PR by head repository, not just branch name; (2) close an own open sync PR on a no-change run; (3) isolate the public-acceptance npm pack like packWorkflowCandidate." + "2026-10-07T10:02:37.579Z",pi-glm,"Review round 2 dispositions. Finding 4205293132 (accepted, Major): the review-PR lookup now asks gh pr list for number,headRepository,headRepositoryOwner,isCrossRepository and selects in the step, with jq, only an OPEN PR whose head repository is ${REPOSITORY} (jq-side first-match, so no head/SIGPIPE hazard under pipefail); a fork PR with the same branch name is never edited and a new own PR is created instead. Finding 4205293127 (accepted, Minor): on a no-change run (`git diff --cached --quiet`), the workflow now closes the own open sync PR (same head-repository rule) with a short comment that the tracker already matches GitHub, then exits 0. Finding 4205293135 (accepted, Minor): the public acceptance helper's npm pack now uses the package-local PM_PATH and --foreground-scripts=false, the same npm-10 isolation as packWorkflowCandidate." + "2026-10-07T10:02:38.250Z",pi-glm,"Fail-on-revert proof (temporary workflow copies, no stash): reverting the workflow to the pre-fix first-listed-PR lookup and no-close path makes 'the workflow never edits a fork PR sharing the sync branch name' and 'a no-change run closes an own open sync PR and exits successfully' fail; a partial revert that closes the first listed PR without the head-repository filter makes 'a no-change run with only a fork PR sharing the branch name closes nothing' fail. npm-10 proof for finding 4205293135: with a scratch npm@10 (10.9.9) first on PATH, the helper's pre-fix pack invocation exits 3 because npm 10 still runs prepare with --ignore-scripts and it fails with the fixture PM_PATH (outside the repository); the fixed invocation with the package-local PM_PATH and --foreground-scripts=false exits 0 and the --json receipt parses to the tarball filename." + "2026-10-07T10:47:24.539Z",pi-glm,"Committed 48c5885 (branch feat/import-privacy-gate, pushed) with all three review-round-2 fixes, the extended fake gh (pr list returns realistic head-repository JSON driven by OWN_OPEN_PR/FORK_OPEN_PR; pr close recorded), the three new guard tests, and tracker evidence. Gates: npm run release:check pass, bun run release:check pass (typecheck, docstring 130 declarations, lint, duplication zero, privacy, coverage 12 authored modules exact 100/100/100/100, 519/519 tests, audit:prod, pack:dry-run, changelog:check, publish-attestation)." + "2026-10-07T10:59:42.057Z",claude-orchestrator,"Orchestrator verification of 48c5885 (gh-wf agent ended before reporting): npm and Bun release:check pass at exact 100/100/100/100 with the head-repository PR selection, no-change PR close, and public-acceptance pack isolation." + "2026-10-07T11:33:39.558Z",claude-orchestrator,"CodeRabbit 4206349775 (valid): host paths behind file:// URLs, label colons (cwd:/home/...) and file:///C:/ drive URLs passed the gate. absolute-host-path now uses a lookbehind lead-in (adds file:// and >=2-char label colons, so drive letters stay with the Windows rule and C:/Users is one finding), the matched value is the path itself (removed the slice special case), and windows-host-path accepts file:/// drive URLs. New test fails on revert; URLs with ports, file://server shares, slash commands and API routes stay clean." + "2026-10-07T12:39:25.378Z",claude-orchestrator,"CodeRabbit 4206922126/4206922131 (valid, docs/tracker only): retitled closed pm-github-rap0 so release notes no longer present the fixed root-sensitivity as current; README workflow order now lists the 'Verify repeat import is a no-op' step that WORKFLOW_IMPORT_SEQUENCE and the workflow already run." + "2026-10-07T13:42:15.444Z",claude-orchestrator,"CodeRabbit 4207573690 (valid): three fail-closed refusal messages carried remnants of removed interpolation ('too large to scan ;', 'not valid JSON: (allowlist)', a template literal without interpolation). Messages now name the 8388608-byte scan cap and stay redaction-safe; tests assert the exact text for the size cap and malformed allowlist and fail on revert." +files[15]{path,scope}: + .github/workflows/pm-github-sync.yml,project + docs/coverage-gap-inventory.md,project + gate.ts,project + index.ts,project + scripts/generate-changelog.ts,project + test/coverage-edges.test.ts,project + test/coverage-remainder.test.ts,project + test/gate-inputs.test.ts,project + test/helpers/bounded-cli.ts,project + test/helpers/public-acceptance.ts,project + test/helpers/read-only-directory.ts,project + test/http-boundary.test.ts,project + test/import-gate.test.ts,project + test/import-lock.test.ts,project + test/review-round1.test.ts,project +tests[18]: + - command: node --test test/bounded-cli.test.ts + scope: project + timeout_seconds: 60 + provenance: + author: codex-sol + created_at: "2026-10-06T11:37:04.782Z" + source_kind: local_mutation + source_ref: feat/import-privacy-gate + - command: "node --test --test-name-pattern=\"requestOnce fails a request\" test/http-boundary.test.ts" + scope: project + timeout_seconds: 60 + provenance: + author: codex-sol + created_at: "2026-10-06T11:56:36.862Z" + source_kind: local_mutation + source_ref: feat/import-privacy-gate + - command: "node --test --test-name-pattern=\"workflow packing isolates\" test/import-gate.test.ts" + scope: project + provenance: + author: codex-sol + created_at: "2026-10-06T12:57:13.194Z" + source_kind: local_mutation + source_ref: feat/import-privacy-gate + - command: "node --test --test-name-pattern=\"executable reusable workflow|packed installed CLI\" test/import-gate.test.ts" + scope: project + provenance: + author: codex-sol + created_at: "2026-10-06T12:57:14.565Z" + source_kind: local_mutation + source_ref: feat/import-privacy-gate + - command: "npm run release:check" + scope: project + provenance: + author: codex-sol + created_at: "2026-10-06T12:58:12.756Z" + source_kind: local_mutation + source_ref: feat/import-privacy-gate + - command: "bun run release:check" + scope: project + provenance: + author: codex-sol + created_at: "2026-10-06T12:58:13.623Z" + source_kind: local_mutation + source_ref: feat/import-privacy-gate + - command: "node --test --test-name-pattern=\"validate borrows a synthetic gh credential\" test/coverage-edges.test.ts" + scope: project + provenance: + author: codex-sol + created_at: "2026-10-06T13:09:02.337Z" + source_kind: local_mutation + source_ref: feat/import-privacy-gate + - command: "node --test --test-name-pattern=\"diff hunk content|Git refuses|canonicalizes a symlinked|gated comment imports|gated pre-write|workflow input shell|workflow credentials\" test/gate-inputs.test.ts test/import-gate.test.ts" + scope: project + provenance: + author: codex-sol + created_at: "2026-10-06T13:58:19.539Z" + source_kind: local_mutation + source_ref: feat/import-privacy-gate + - command: "node --test --test-name-pattern=\"adversarial: token in a body|workflow input shell\" test/import-gate.test.ts" + scope: project + timeout_seconds: 2400 + provenance: + author: codex-sol + created_at: "2026-10-06T15:18:29.770Z" + source_kind: local_mutation + source_ref: feat/import-privacy-gate + - command: node --test test/generate-changelog.test.ts + scope: project + timeout_seconds: 60 + provenance: + author: codex-sol + created_at: "2026-10-06T15:35:58.476Z" + source_kind: local_mutation + source_ref: feat/import-privacy-gate + - command: node --test test/import-lock.test.ts + scope: project + provenance: + author: pi-gh-cr3b + created_at: "2026-10-06T18:23:43.289Z" + source_kind: local_mutation + source_ref: feat/import-privacy-gate + - command: node --test test/review-round1.test.ts test/coverage-remainder.test.ts + scope: project + provenance: + author: pi-gh-cr3b + created_at: "2026-10-06T18:23:44.820Z" + source_kind: local_mutation + source_ref: feat/import-privacy-gate + - command: npm run coverage + scope: project + provenance: + author: pi-gh-cr3b + created_at: "2026-10-06T18:54:39.808Z" + source_kind: local_mutation + source_ref: feat/import-privacy-gate + - command: "npm run release:check && bun run release:check (fresh clones of b446963)" + scope: project + provenance: + author: pi-gh-cr3b + created_at: "2026-10-06T18:54:40.508Z" + source_kind: local_mutation + source_ref: feat/import-privacy-gate + - command: node --test test/coverage-remainder.test.ts + scope: project + provenance: + author: pi-glm + created_at: "2026-10-07T07:11:23.202Z" + source_kind: local_mutation + source_ref: feat/import-privacy-gate + - command: npm test + scope: project + provenance: + author: pi-glm + created_at: "2026-10-07T07:11:24.149Z" + source_kind: local_mutation + source_ref: feat/import-privacy-gate + - command: "node --test --test-name-pattern=\"fork PR|no-change|install-only settings change|clean import and creates\" test/import-gate.test.ts" + scope: project + provenance: + author: pi-glm + created_at: "2026-10-07T10:02:41.652Z" + source_kind: local_mutation + source_ref: feat/import-privacy-gate + - command: "npm run release:check && bun run release:check" + scope: project + provenance: + author: pi-glm + created_at: "2026-10-07T10:02:42.405Z" + source_kind: local_mutation + source_ref: feat/import-privacy-gate +docs[3]{path,scope}: + CHANGELOG.md,project + docs/coverage-gap-inventory.md,project + README.md,project +body: "" diff --git a/.agents/pm/issues/pm-github-rap0.toon b/.agents/pm/issues/pm-github-rap0.toon new file mode 100644 index 00000000..17253070 --- /dev/null +++ b/.agents/pm/issues/pm-github-rap0.toon @@ -0,0 +1,16 @@ +id: pm-github-rap0 +title: Root CI runs skip the read-only-directory comment-sync fixtures visibly instead of failing them +description: "The review-round1 new Function test removal moved the chmod 0o555/finally cleanup of the comment-sync write-failure fixture into the exported withReadOnlyDirectory test helper. Root ignores directory write permissions, so under a root CI runner the helper applies no read-only mode, the fixture write succeeds, and the per-comment write-failure assertions in the comment-sync coverage test fail. Review round 3 guarded the equivalent import-lock fixtures with a visible node:test root skip (CodeRabbit 4198811222); the same non-root-only skip should cover this helper-based fixture so a root run skips visibly instead of failing." +type: Issue +status: closed +priority: 2 +tags[3]: ci,root,tests +created_at: "2026-10-06T18:29:11.654Z" +updated_at: "2026-10-07T12:39:24.056Z" +closed_at: "2026-10-07T08:33:51.527Z" +completed_at: "2026-10-07T08:33:51.527Z" +author: pi-gh-cr3b +comments[1]{created_at,author,text}: + "2026-10-06T18:29:15.904Z",pi-gh-cr3b,"Discovered during review round 3 (CodeRabbit 4198811228): replacing the new Function source-slicing test exposed that the coverage-remainder comment-sync write-failure fixture inherits the same root-sensitivity as the import-lock permission fixtures, but the review only authorized guarding import-lock.test.ts this round. Left as a follow-up so a root CI runner skips it visibly instead of failing the suite." +close_reason: "Resolved on PR #108 by the orchestrator: all three read-only-directory comment-sync fixtures in test/coverage-remainder.test.ts (per-comment add failure gated and ungated, contended lock plus per-comment failure) now use a visible nonRootOnly skip when the suite runs as root, matching test/import-lock.test.ts; non-root CI still runs them (verified 3/3 pass)." +body: "" diff --git a/.agents/pm/tasks/pm-github-9cjx.toon b/.agents/pm/tasks/pm-github-9cjx.toon index fbfbf58b..afc0de0d 100644 --- a/.agents/pm/tasks/pm-github-9cjx.toon +++ b/.agents/pm/tasks/pm-github-9cjx.toon @@ -1,26 +1,139 @@ id: pm-github-9cjx title: Reach exact all-source coverage and include every operational script -description: "The current gate measures index.ts and projects.ts but explicitly skips scripts, and its thresholds are 88/79/89. The latest measured corpus after the complete-read work is 91.83% lines, 82.14% branches, and 91.75% functions across two production files, so a green release check is not the mandated exact 100/100/100/100 all-source result." +description: "The unchanged all-source gate measures exact 100 percent lines, statements, branches and functions across all 12 authored TypeScript/JavaScript modules, including operational scripts. The closing receipt (actual_result and close_reason) is the fresh npm and Bun release gates passing 504/504 tests at 7a41660; later review rounds add tests without changing that verdict, and the 487-test frozen run is historical. Real Git, PM SDK, filesystem and HTTP cases cover reachable paths; removed private-path arms have invariant proofs recorded in comments and docs/coverage-gap-inventory.md. No source ignores or lowered thresholds. Verified and closed by the orchestrator." type: Task -status: open +status: closed priority: 1 tags[5]: all-source,coverage,quality-gate,scripts,tdd created_at: "2026-08-17T07:32:29.666Z" -updated_at: "2026-08-17T07:35:10.745Z" +updated_at: "2026-10-06T21:45:34.169Z" +closed_at: "2026-10-06T17:04:30.277Z" +completed_at: "2026-10-06T17:04:30.277Z" author: codex estimated_minutes: 960 acceptance_criteria: "Automatically discover every executable TypeScript production and operational source; require exact 100% statements, branches, functions, and lines; prohibit ignore pragmas, source allowlist blind spots, threshold weakening, vacuous tests, and mocks of the unit under test; exercise GitHub behavior at the HTTP boundary, pm behavior through real SDK/installed-CLI boundaries, and operational scripts through real filesystem/process behavior; prove the gate fails when a new uncovered source or branch is introduced." +unblock_note: "Resolved: exact all-source 100/100/100/100 coverage verified in CI and fresh-clone npm/Bun release gates (PR 108)." expected_result: "Every executable TypeScript source and operational script is automatically included in measurement and exact statements, branches, functions, and lines all reach 100% through behavioral tests." -actual_result: The source walker skips scripts and aggregate coverage remains 91.83/82.14/91.75 with thresholds fixed at 88/79/89. +actual_result: Fresh npm and Bun release gates pass 504/504 tests at 7a41660 and later heads with exact all-source 100/100/100/100 across 12 modules; a fresh coverage run at the closing branch head passes 504/504 with zero failures or skips. The close_reason's 504/504 receipt is authoritative for the closed item; the earlier 503/503 count described the superseded head f572382. CI Node 22/26 and native Bun pass; full native linked changelog/privacy tests pass 29/29. dependencies[1]{id,kind,created_at,author,source_kind,author_source}: pm-github-n3z3,related,"2026-08-17T07:32:29.666Z",codex,"cli:create:dep",detected -files[3]{path,scope}: - scripts/coverage-gate.ts,project +comments[36]{created_at,author,text}: + "2026-10-05T07:52:16.974Z",codex-sol,"Replaced the narrowed coverage wrapper with an explicit authored-module inventory, exact 100 percent thresholds for all four dimensions, stale-LCOV invalidation, and missing-file rejection. Added checked operational-script tests, ESLint enforcement, erasable TypeScript syntax, and a jscpd wrapper that verifies its full source inventory. Typecheck, 122 documented declarations, ESLint, and zero duplication passed. Coverage remains a measured gate rather than an approval claim." + "2026-10-05T08:09:10.457Z",codex-sol,"Strict all-authored-module run completed with 369 tests passing and no skipped tests. Coverage remained below the unchanged 100/100/100/100 requirement: lines/statements 93.25 percent, branches 84.41 percent, functions 94.92 percent over 11 authored TS/JS modules including operational scripts. LCOV was invalidated on failure. Most remaining uncovered branches and ten uncovered functions are in the pre-existing GitHub Projects sync handlers; this is a release blocker, not a passing gate. The shell release-date verifier remains outside the V8 denominator and is exercised separately." + "2026-10-05T08:58:34.554Z",codex-sol,"Final frozen-source verification at 0b7c4ab: the linked npm coverage command ran all 375 tests with 375 passing, zero failures and zero skips, then correctly exited nonzero at the unchanged 100 percent gate. Complete inventory: 12 authored TS/JS modules including operational scripts. Lines/statements 7739 of 8285 = 93.40 percent; branches 1548 of 1826 = 84.77 percent; functions 188 of 198 = 94.94 percent. Failed LCOV was removed. No source ignores or lowered thresholds were introduced; release remains blocked." + "2026-10-05T09:09:52.779Z",codex-sol-coverage,"Resuming the existing strict 12-module coverage implementation. Iteration will use targeted Node and c8 runs, real scratch PM and git projects, and SDK extension harness behavior tests. The complete npm coverage gate is reserved for the orchestrator. Existing unrelated tracker edits are preserved. First priority is Projects sync and apply behavior, then remaining planner and operational failure boundaries." + "2026-10-05T09:18:20.026Z",codex-sol-coverage,"Added 18 behavior cases for Projects sync and planners. Targeted Node c8 execution passed 51 of 51 tests, zero skips. Verified preview-only and scoped execution, dry-run override, both conflict preferences, draft and issue mutations, lifecycle close and cancellation metadata, partial and total failures, and a real concurrent local deletion after remote attachment. PM tag ordering is normalized, so the preservation assertion checks membership. No production behavior, dependencies, thresholds, or source inventory changed." + "2026-10-05T09:26:42.190Z",codex-sol-coverage,"Native Bun verification passed all 12 Projects harness cases and all 39 planner cases, with per-file timeout 600000. Executable lookup failure fixtures now run in explicit Node subprocesses while registered harness behavior stays native. This follows the existing lifecycle test boundary and prevents temporary PATH changes from reaching the host credential helper. Linked Node tests passed 51 of 51 through pm test; test-result tracking is disabled, so this comment records the receipt." + "2026-10-05T09:29:20.437Z",codex-sol-coverage,"The Projects planner module now measures exact 100/100/100/100 in the targeted c8 run: 732 of 732 lines and statements, 18 of 18 functions, 181 of 181 branches, with 39 Node tests passing. Removed only the impossible parse-exception catch after validating even-length hex; Buffer accepts that validated input without a parsing exception. Malformed encodings still reject before decoding, and uppercase hex preserves the original case-sensitive id. Full package evidence remains pending the orchestrator run; saved baseline receipts cannot be treated as exact-current after scanner changes by the parallel review work." + "2026-10-05T09:31:52.268Z",codex-sol-coverage,"Counter correction for the latest Projects-only run: all 182 of 182 measured branches are covered; the prior comment used the earlier branch total of 181. Lines and statements remain 732 of 732, functions 18 of 18. The linked 39-test Node planner suite passed. Added eight additional real filesystem/process boundary cases; targeted Node and native Bun both pass 8 of 8." + "2026-10-05T09:34:52.514Z",codex-sol,"Stopped the Codex CLI coverage sub-agent immediately after a fresh quota check reported OVER_FORECAST while the other providers remained exhausted, honoring the owner weekly forecast limit. Preserved its focused commits and remaining test files for orchestrator verification. Projects planners reached exact 100 in targeted c8; 12 Projects harness cases, 8 operational boundary cases, and 9 import error cases passed Node and native Bun. Package-wide coverage still requires a fresh frozen-source run and must not be presented as 100. No thresholds, source inventory, or ignores were weakened." + "2026-10-05T09:41:56.169Z",codex-sol,"Orchestrator verification preserved the interrupted new coverage tests and corrected their fixture setup: explicit repo-local tracker initialization, filename-derived item id expectations, and removal of the linked host package-root environment for the missing-metadata Windows case. The preserved Projects and planner cases pass Node; all 74 preserved cases pass native Bun. The newly linked boundary/import/diff command is being reverified through the PM runner before final broad checks. These are test-fixture corrections; production rejection behavior and the exact gate remain unchanged." + "2026-10-05T09:45:17.622Z",codex-sol,The corrected linked boundary/import/diff command passes all 23 Node cases through pm test with progress and exit zero. The preserved Projects/planner suites also passed their native behavior checks; the complete 74-case preserved set passed Bun before the environment-isolation correction. Final frozen-source full npm coverage and native Bun checks follow. All useful interrupted files are retained and linked to this item. + "2026-10-06T11:13:02.345Z",codex-sol,"Continuing the interrupted work on the same branch. Reviewing every preserved production change against its invariants, measuring all-source coverage, and adding real Git, filesystem, HTTP, and SDK behavior tests. Exact thresholds stay at 100 in all four dimensions; no ignore pragmas or inventory exclusions. PR 108 already exists and will receive the final measured receipts." + "2026-10-06T11:17:43.296Z",codex-sol,"Reviewed the preserved simplifications. Proofs for removed impossible arms: phone regexes guarantee digits; a context-line branch guarantees its leading space; successful encoded spawnSync gives string stdout/stderr while startup failures populate error; fixed valid spawn arguments report missing tools via error/status rather than throwing; nullish SDK fallbacks execute only for a missing injection; fetchJSON resolves only for 2xx; split on a string always returns at least one element; SDK command/import/preflight/search contexts require their arguments and options. Scanner operations on decoded plain strings and arrays have only the explicit malformed-history GateInputError. Restored the formatGateReport empty-field fallback because its exported typed input can contain an empty string. Fixing fixture setup and replacing an always-true assertion; no thresholds or ignores changed." + "2026-10-06T11:33:54.300Z",codex-sol,"The second complete npm run passes all 471 tests with zero skips. Coverage is 99.80 percent lines/statements, 97.74 percent branches, and 100 percent functions. Every operational script and gate.ts now measure exact 100 in all dimensions. Remaining index.ts gaps are enumerated from the JSON receipt. Centralizing thrown-value formatting and the identical command gate error boundary preserves behavior while removing duplicated decisions; real collaborator errors are still redacted and fail closed. Additional proven unreachable arms: a search loop cannot execute with an empty match array; fixed gh version spawn reports failures in its result; an atomic dry-run returns before the non-atomic preview; the private Projects add-issue executor receives coordinates populated by its sole planner. Tests retain defaults exposed by public planners." + "2026-10-06T11:40:59.548Z",codex-sol,"Third full coverage run: all 484 tests pass; statements, lines and functions are 100%, branches 99.59%. Nine remaining branch locations enumerated from coverage-final.json. Proof for deleting two private-path fallbacks: applyPushEntry receives only planner entries; existing project item indices reject empty ids, and both gqlAddDraft and gqlAddIssue throw before returning an empty id (real GraphQL missing-id tests verify this). The redundant no-item-id guard cannot execute. Pull statusSkipped is appended only inside if(pi.statusName), assigning optionName that same truthy string, so its missing-name fallback cannot execute. Complete PM reads validate tags and body/description strings; the private executor only looks up ids from that complete selected corpus. No coverage exclusions or threshold changes." + "2026-10-06T11:43:22.121Z",codex-sol,"The targeted real-command c8 check passes four tests: low unauthenticated quota, human Projects listings, after-command subprocess boundaries, and all four Projects mutation failure boundaries. It covers the remaining reachable branches, including open-board formatting and missing tags. The complete npm release check and all native Bun test files are now running on frozen source. docs/coverage-gap-inventory.md retains every initial uncovered statement and branch plus the final nine-branch inventory and closure proofs." + "2026-10-06T11:49:57.012Z",codex-sol,"Frozen full npm coverage PASS: 487 tests, zero failures/skips; all 12 authored modules measure exact 100/100/100/100. JSON receipt has zero uncovered statements or branches. Production dependency audit passes with zero vulnerabilities. The linked PM planner command passes all 39 tests through pm test --run --progress; the linked watchdog command previously passed through the PM runner. Full release completion and native Bun verification follow." + "2026-10-06T11:55:39.390Z",codex-sol,"The first exact-100 full npm release check passed at implementation 40db9b3. Broad native testing then exposed a real HTTP timeout regression, recorded in hptv and fixed without raising deadlines. Source is re-frozen for full npm and native Bun verification; earlier coverage receipts are not claimed for the transport change until remeasured." + "2026-10-06T12:03:40.399Z",codex-sol,"Full npm release:check at transport-fix implementation 5ed8c47 PASS: 487 tests with zero failures/skips; exact 100/100/100/100 across all 12 authored modules. No uncovered statement or branch counters remain. All release static gates, Git-object privacy, production dependency audit, packing, changelog and publish attestation pass. Starting the full Bun-invoked release command on this same implementation; native Bun full-suite timeout regression already passes at 30.04 seconds." + "2026-10-06T12:07:15.906Z",codex-sol,"Four native lock fixture failures are a test-runner API mismatch: Bun 1.3.5 does not implement node:test mock.method. Replacing only syscall-boundary replacement/restoration with a small explicit helper, retaining real temporary lock files, permission changes, live-peer protection and the same injected I/O failures. Also close the real descriptor after the simulated close failure so the fixture leaves no open descriptor. Production source and coverage thresholds are unchanged; focused Node and native Bun checks are running." + "2026-10-06T12:08:41.954Z",codex-sol,"All four corrected lock boundary cases pass both Node and native Bun (4/4 each), with the same real filesystem and live-peer assertions. Build:test passes. Production source remains exactly 5ed8c47. The superseded native run with known mock.method fixture failures has been stopped; restarting the entire 30-file native suite with portable fixtures and verifying the complete 24-case Node lock file. The full Bun release command continues on the unchanged production source." + "2026-10-06T12:19:41.403Z",codex-sol,"The complete native rerun exposed two remaining fixture lookup failures: Bun caches executable resolution at startup, so changing process.env.PATH later does not reliably redirect git. These privacy-gate negative controls now launch the same runtime in a real child with the intended PATH from startup, retaining real Git repositories, malformed inventory and failed blob-read assertions. Production sources remain byte-identical to 5ed8c47; no gate logic, ignores or thresholds change. Full 25-case Node/Bun privacy files are being verified before the final native rerun." + "2026-10-06T12:20:40.769Z",codex-sol,"The startup-environment privacy fixture confirms an additional Bun lookup detail: an empty PATH falls back to default tool lookup. Using a nonempty scratch directory with no executable git makes the missing-tool case real under both runtimes. Malformed inventory and failed blob-read controls already pass in the native child; the complete Node privacy file passes 25/25. Retrying the full native privacy file with the empty-directory PATH, preserving the actual production gate and real Git fixtures." + "2026-10-06T12:22:15.502Z",codex-sol,"Portable privacy fixtures now pass the complete 25-case native Bun file; the complete Node privacy file passes 25/25, and its final two startup-lookup cases pass again after the empty-directory PATH correction. ESLint and build:test pass. Starting the final complete native suite and a fresh npm coverage run with every final fixture, ensuring child-process coverage counters remain complete. Production implementation remains byte-identical to 5ed8c47." + "2026-10-06T12:31:03.576Z",codex-sol,"Final fresh npm coverage with all portable fixtures PASS: 487 tests, zero failures/skips; exact 100/100/100/100 across all 12 authored modules, 8398 lines/statements, 2219 branches and 208 functions. JSON contains zero uncovered statement/branch counters. The real same-runtime child fixtures preserve V8 coverage evidence. Complete npm and Bun release checks already pass on identical production sources; the final native suite remains in progress with no failures." + "2026-10-06T12:31:38.843Z",codex-sol,"Counter correction for the final portable-fixture npm coverage receipt: it contains 2220/2220 covered branches, not 2219. Earlier complete release receipts contain 2219/2219. Both retain all 12 modules, exact 100 percent, zero uncovered counters and zero skipped source. Final lines/statements are 8398/8398 and functions 208/208. Production source is unchanged; the latest receipt is authoritative for the final fixture run." + "2026-10-06T12:33:58.453Z",codex-sol,"The complete native suite has all 487 cases passing across 30 files with zero failed assertions, but its shell result is nonzero. Isolated duplication-gate file reproduces exit one despite 3/3 passing: the script-entry negative control sets process.exitCode=1, and restoring undefined does not clear it under Bun. Four touched entry fixtures now restore previous ?? 0, matching existing docstring/attestation fixture practice. Production source remains unchanged. Checking each affected file exit status and restarting native acceptance to require exit zero as well as zero failed cases." + "2026-10-06T12:35:41.764Z",codex-sol,"Exit cleanup verified: all four affected Bun entry-fixture files now return process exit zero with zero failed tests; the combined 38-case Node run also passes with zero skips, and lint/build:test pass. The previous complete native run already passed all 487 assertions across 30 files. Running the complete native command once more to obtain an explicit exit-zero receipt. Production code remains unchanged, with final full coverage exact 100/100/100/100." + "2026-10-06T12:42:45.910Z",codex-sol,"Final native command npm run test:bun PASS with process exit zero: 487 passing cases across all 30 files, zero failed cases. Final source coverage is exact 100/100/100/100 across all 12 authored modules with no ignores/lowered thresholds and no uncovered counters. The complete npm/Bun release checks pass; final fixture-only changes retain byte-identical production implementation 5ed8c47. Portable syscall, startup-PATH and explicit exit-code cleanup fixtures now work under both runtimes. Claims will be released and the item remains open for orchestrator verification." + "2026-10-06T13:55:53.428Z",codex-sol,"Historical actual_result described ae86e7d: 420 tests passed but coverage was 97.59 percent lines/statements, 89.24 percent branches, 100 percent functions and exact-100 failed. The latest pushed baseline a849280 subsequently passed exact all-source 100/100/100/100 in fresh clones; replacing the stale current field, with review-round-1 verification pending." + "2026-10-06T14:00:16.233Z",codex-sol,"Review regressions verified red/green for all 16 findings. Linked focused pm test commands pass 8 tests each; result persistence is disabled by existing project policy, so this comment records the receipt. Build:test, ESLint, 130 documented declarations and zero duplication across all 12 authored modules pass. Canonicalization required replacing nonexistent mocked directories in two prior Git failure tests with real fixture directories; their intended status and containment refusal branches pass." + "2026-10-06T14:10:31.229Z",codex-sol,"Persistent revert receipts: 4196078345 current coverage task field; 4196078470 README anchor; 4196078483 in-repository dot-dot source name; 4196078520 changelog spawn/signal diagnostics; 4196078539 historical provenance Git diagnostic; 4196078563 permission cleanup after a forced sync throw; 4196078603 complete secret literal refusal; 4196078637 explicit gate override. Each reviewed-code negative control failed and restored code passed. No finding was declined. Existing exact-100 source inventory, thresholds and exclusions are unchanged." + "2026-10-06T14:20:36.531Z",codex-sol,"Fresh-clone release gates at c8b9329 PASS: 503 tests, exact all-source 100/100/100/100, 130 documented declarations, zero duplication, lint, typecheck, privacy, zero production audit vulnerabilities, package dry-run, changelog check and publish attestation. Latest coverage receipts replace the historical failed actual_result; previous measurements remain in comments/history. The portable diagnostic fixture has another failing revert/passing restore proof, and focused Node/native Bun verification passes. Preparing final receipt commit and exact pushed-head clones." + "2026-10-06T14:33:48.844Z",codex-sol,"Final source/test head f572382 passes fresh npm and Bun release checks: 503/503 tests, exact 100/100/100/100 across 12 modules, 8406/8406 lines/statements; Node 22 has 209/209 functions and 2226/2226 branches, Node 26 has 208/208 functions and 2225/2225 branches. CI test (22) and test (26) pass including the full native Bun suite. Replaced the filtered native linked command after the pinned PM runner falsely classified two passing Bun tests as empty; full native changelog/privacy files pass 29/29 through pm test --run --only-last --progress. Separate follow-up pm-github-2ao1 preserves that upstream limitation; no package source or coverage policy workaround was added." + "2026-10-06T18:17:36.953Z",pi-gh-cr3b,"CodeRabbit 4198811214: resolved the close_reason 504/504 vs actual_result 503/503 mismatch. A fresh coverage run at the closing head re-measured the exact-100 gate: 504/504 tests pass with zero failures and skips, matching the close_reason receipt for 7a41660 and later heads. The 503/503 count was the earlier f572382 receipt and was stale. The close_reason is the authoritative closing receipt; actual_result now cites 504/504 and states which receipt supersedes which. The item stays closed; no reopen." + "2026-10-06T21:45:34.169Z",claude-orchestrator,"Receipt reconciliation: the authoritative closing count is 504/504 at 7a41660 (actual_result, close_reason). Counts of 503 and 505 seen in earlier comments are historical measurements at other heads." +files[22]{path,scope}: + eslint.config.js,project package.json,project + projects.ts,project + scripts/coverage-gate.ts,project + scripts/duplication-gate.ts,project + scripts/generate-changelog.ts,project + scripts/main-invocation.ts,project + scripts/privacy-gate.ts,project test,project -tests[1]{command,path,scope,timeout_seconds,note}: - npm run coverage,scripts/coverage-gate.ts,project,900,Current ratcheted gate; this item remains open until it measures all executable sources at exact 100/100/100/100. -docs[2]{path,scope,note}: - "https://github.com/unbraind/pm-cli-companion/blob/main/.agents/pm/epics/pm-cli-website-ktip.toon",global,Fleet exact coverage mandate - "https://github.com/unbraind/pm-cli-companion/blob/main/.agents/pm/issues/pm-cli-website-odki.toon",global,Fleet operational script coverage gap + test/coverage-boundaries.test.ts,project + test/coverage-edges.test.ts,project + test/coverage-gate.test.ts,project + test/coverage-remainder.test.ts,project + test/duplication-gate.test.ts,project + test/gate-inputs.test.ts,project + test/helpers/node-scenario.ts,project + test/helpers/public-acceptance.ts,project + test/import-errors.test.ts,project + test/lint-policy.test.ts,project + test/project-sync.test.ts,project + test/projects.test.ts,project + test/review-round1.test.ts,project +tests[6]: + - command: node --test test/project-sync.test.ts test/projects.test.ts + scope: project + timeout_seconds: 600 + provenance: + author: codex-sol-coverage + created_at: "2026-10-05T09:18:36.238Z" + source_kind: local_mutation + source_ref: feat/import-privacy-gate + - command: node --test test/projects.test.ts + scope: project + timeout_seconds: 60 + provenance: + author: codex-sol-coverage + created_at: "2026-10-05T09:29:21.443Z" + source_kind: local_mutation + source_ref: feat/import-privacy-gate + - command: node --test test/coverage-boundaries.test.ts test/import-errors.test.ts test/gate-inputs.test.ts + scope: project + timeout_seconds: 300 + provenance: + author: codex-sol + created_at: "2026-10-05T09:38:01.943Z" + source_kind: local_mutation + source_ref: feat/import-privacy-gate + - command: "node --test --test-name-pattern=\"README gate links|coverage task current|privacy fixtures never|comment failure fixture|dot-dot names|canonical changelog includes|historical fixture provenance reports|gate override\" test/review-round1.test.ts test/coverage-gate.test.ts test/generate-changelog.test.ts test/privacy-gate.test.ts test/link-deps.test.ts" + scope: project + provenance: + author: codex-sol + created_at: "2026-10-06T13:58:20.374Z" + source_kind: local_mutation + source_ref: feat/import-privacy-gate + - command: bun test --timeout 600000 ./test/generate-changelog.test.ts ./test/privacy-gate.test.ts + scope: project + provenance: + author: codex-sol + created_at: "2026-10-06T14:30:35.002Z" + source_kind: local_mutation + source_ref: feat/import-privacy-gate + - command: npm run coverage + path: scripts/coverage-gate.ts + scope: project + timeout_seconds: 900 + provenance: + author: claude-orchestrator + created_at: "2026-10-06T17:04:27.440Z" + source_kind: local_mutation + source_ref: feat/import-privacy-gate + note: "Exact 100/100/100/100 gate over every executable source; passes in CI (Node 22 and 26) and in fresh-clone npm and Bun release:check." +docs[3]: + - path: "https://github.com/unbraind/pm-cli-companion/blob/main/.agents/pm/epics/pm-cli-website-ktip.toon" + scope: global + note: Fleet exact coverage mandate + - path: "https://github.com/unbraind/pm-cli-companion/blob/main/.agents/pm/issues/pm-cli-website-odki.toon" + scope: global + note: Fleet operational script coverage gap + - path: docs/coverage-gap-inventory.md + scope: project +close_reason: "Verified by the orchestrator: PR 108 CI test (22)/(26) green with the exact 100/100/100/100 gate, fresh-clone npm and Bun release:check 504/504 at 7a41660 and later heads; no ignores or lowered thresholds." body: "" diff --git a/.agents/pm/tasks/pm-github-aiok.toon b/.agents/pm/tasks/pm-github-aiok.toon new file mode 100644 index 00000000..775f9a9e --- /dev/null +++ b/.agents/pm/tasks/pm-github-aiok.toon @@ -0,0 +1,18 @@ +id: pm-github-aiok +title: Adopt the released gated sync caller in fleet repositories +description: "After the gated extension is published and its exact quality gates pass, replace the release commit and version placeholders in docs/sync-workflow-caller.yml and migrate the disabled pm-graph, pm-jira, and pm-todos sync callers to the single reusable implementation. Keep automated workflows disabled until the owner authorizes rollout. The current candidate is confined to pm-github and must not modify or dispatch workflows in other repositories. Verify a fresh nonempty import, malicious no-push fixtures, repeat idempotency, and permanent changed-item links in each authorized caller rollout." +type: Task +status: open +priority: 1 +tags: [] +created_at: "2026-10-05T09:23:42.980Z" +updated_at: "2026-10-05T09:32:54.787Z" +author: codex-sol +dependencies[1]{id,kind,created_at,author,source_kind,author_source}: + pm-github-f1at,related,"2026-10-05T09:32:54.728Z",codex-sol,"cli:update:dep",detected +comments[1]{created_at,author,text}: + "2026-10-05T09:25:31.280Z",codex-sol,"The caller example intentionally uses release commit and exact version placeholders. The currently published extension predates the gate, and this work is authorized only in the pm-github worktree. No pilot workflow in another repository was changed, enabled, or dispatched. Resolve the placeholders only after a reviewed release and separately authorized caller adoption." +files[2]{path,scope}: + .github/workflows/pm-github-sync.yml,project + docs/sync-workflow-caller.yml,project +body: "" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0f1f07c2..2b464dc6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,7 +26,7 @@ jobs: fetch-tags: true - name: Setup Node.js - uses: actions/setup-node@v7 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: ${{ matrix.node-version }} cache: npm @@ -70,6 +70,12 @@ jobs: - name: Verify complete docstring coverage run: npm run docstring + - name: ESLint source policy + run: npm run lint + + - name: Complete source duplication gate + run: npm run duplication + # Fail-closed identity / secret / host-path audit over the local object # store (pm-github-zqad). In CI this is evidence about refs and tags; the # local release:check run additionally covers unreachable objects. @@ -93,3 +99,6 @@ jobs: - name: Verify Bun can install the package graph run: bun install --no-save + + - name: Verify native Bun extension behavior + run: npm run test:bun diff --git a/.github/workflows/pm-github-sync.yml b/.github/workflows/pm-github-sync.yml new file mode 100644 index 00000000..a61545d5 --- /dev/null +++ b/.github/workflows/pm-github-sync.yml @@ -0,0 +1,230 @@ +# Reusable, fail-closed gated sync: GitHub issues → pm tracker → review PR. +# +# Fleet repos call this from their own scheduled caller workflow (see +# docs/sync-workflow-caller.yml); the schedule stays with the caller, the +# audited implementation stays here in pm-github. +# +# Pipeline order is the safety property: validate → dry-run plan → gated import +# (plan completeness + provenance idempotency verified BEFORE the write, then +# the fail-closed privacy gate scans the proposed tracker change AFTER it) → +# strict pm health → and only then commit + push + PR. `--gate` exits non-zero +# on any credential, personal-data, or host-path finding — or on any unreadable +# scan input — so this job can never reach the push step with a leak in the +# change. Actions are pinned by SHA. + +name: Gated GitHub issues → pm sync + +on: + workflow_call: + inputs: + repository: + description: "owner/repo whose issues are imported (defaults to the calling repository)" + type: string + required: false + default: ${{ github.repository }} + pm-github-version: + description: "Exact published pm-github version to install (pin; never a floating tag or latest)" + type: string + required: true + sync-branch: + description: "Branch the gated import commits to and the review PR is opened from" + type: string + required: false + default: "automation/pm-github-sync" + node-version: + description: "Node.js version for the pm CLI" + type: string + required: false + default: "22.18.0" + +concurrency: + group: pm-github-sync-${{ inputs.repository }} + cancel-in-progress: false + +# No workflow-wide grant: the job below requests exactly what it needs. +permissions: {} + +jobs: + gated-sync: + # Read issues, push the sync branch, and create/update its review PR. + permissions: + contents: write + pull-requests: write + issues: read + runs-on: ubuntu-latest + env: + REPOSITORY: ${{ inputs.repository }} + SYNC_BRANCH: ${{ inputs.sync-branch }} + PM_GITHUB_VERSION: ${{ inputs.pm-github-version }} + CALLING_REPOSITORY: ${{ github.repository }} + steps: + - name: Validate sync inputs + shell: bash + run: | + set -euo pipefail + [[ "${REPOSITORY}" == "${CALLING_REPOSITORY}" ]] || { echo "::error::repository must equal the calling repository (${CALLING_REPOSITORY})."; exit 1; } + [[ "${PM_GITHUB_VERSION}" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9]+)?$ ]] || { echo "::error::pm-github-version must be an exact numeric version: N.N.N or N.N.N-N."; exit 1; } + [[ "${SYNC_BRANCH}" == automation/* ]] || { echo "::error::sync-branch must start with automation/."; exit 1; } + git check-ref-format "refs/heads/${SYNC_BRANCH}" + + - name: Checkout main + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + fetch-tags: true + ref: main + persist-credentials: false + + - name: Setup Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: ${{ inputs.node-version }} + cache: npm + + # Always rebuild the sync branch from current main: the branch only ever + # holds automated sync commits, so a reset keeps the PR a clean review of + # exactly this run's proposed change instead of an ever-growing merge. + - name: Prepare sync branch from main + shell: bash + run: | + set -euo pipefail + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + sync_lease="$(git rev-parse --verify "refs/remotes/origin/${SYNC_BRANCH}" 2>/dev/null || true)" + printf 'SYNC_LEASE=%s\n' "${sync_lease}" >> "${GITHUB_ENV}" + git switch --force-create "${SYNC_BRANCH}" origin/main + + # The calling repo's own devDependency provides the pm CLI (npm ci). + - name: Install dependencies + shell: bash + run: | + set -euo pipefail + npm ci --ignore-scripts + printf '%s/node_modules/.bin\n' "${GITHUB_WORKSPACE}" >> "${GITHUB_PATH}" + + - name: Install pinned pm-github extension + shell: bash + run: | + set -euo pipefail + ./node_modules/.bin/pm package install "npm:pm-github@${PM_GITHUB_VERSION}" --project + node -e 'if (require("./.agents/pm/extensions/pm-github/package.json").version !== process.argv[1]) process.exit(1)' "${PM_GITHUB_VERSION}" + + - name: Configure PM merge drivers + run: ./node_modules/.bin/pm merge install + + - name: Verify gate is available + run: ./node_modules/.bin/pm github gate --json + + - name: Validate GitHub access + env: + GH_TOKEN: ${{ github.token }} + run: ./node_modules/.bin/pm github validate --repo "${REPOSITORY}" --json | jq -e ' .ok == true and .repo_accessible == true ' + + - name: Preview GitHub to pm plan + env: + GH_TOKEN: ${{ github.token }} + run: ./node_modules/.bin/pm github import "${REPOSITORY}" --state all --atomic --gate --dry-run --with-comments + + # 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: ./node_modules/.bin/pm github import "${REPOSITORY}" --state all --atomic --gate --with-comments + + - name: Verify repeat import is a no-op + env: + GH_TOKEN: ${{ github.token }} + shell: bash + run: | + set -euo pipefail + paths_file="${RUNNER_TEMP}/pm-github-tracker-paths" + git ls-files -z --modified --deleted --others --exclude-standard -- .agents/pm > "${paths_file}" + node -e 'const fs = require("node:fs"); const file = process.argv[1]; const paths = fs.readFileSync(file, "utf8").split("\0").filter(p => p && !["extensions", "locks", "checkpoints"].includes(p.slice(".agents/pm/".length).split("/")[0])); fs.writeFileSync(file, paths.map(p => p + "\0").join(""));' "${paths_file}" + if [[ -s "${paths_file}" ]]; then + git --literal-pathspecs add --pathspec-from-file="${paths_file}" --pathspec-file-nul + fi + ./node_modules/.bin/pm github import "${REPOSITORY}" --state all --atomic --gate --with-comments + git diff --exit-code -- .agents/pm ':(exclude,glob).agents/pm/extensions/**' ':(exclude,glob).agents/pm/locks/**' ':(exclude,glob).agents/pm/checkpoints/**' + [[ -z "$(git ls-files --others --exclude-standard -- .agents/pm ':(exclude,glob).agents/pm/extensions/**' ':(exclude,glob).agents/pm/locks/**' ':(exclude,glob).agents/pm/checkpoints/**')" ]] + ./node_modules/.bin/pm github gate --json + + - name: Verify strict pm health + run: ./node_modules/.bin/pm health --strict-exit + + - name: Commit gated sync changes and open review PR + env: + GH_TOKEN: ${{ github.token }} + shell: bash + run: | + set -euo pipefail + ./node_modules/.bin/pm github gate --json + # gh pr list --head matches branch names, and a fork can open a PR + # from a branch with the same name, so filter the results to PRs whose + # head repository is this repository (never trust .[0] alone). + own_open_pr() { + gh pr list -R "${REPOSITORY}" --head "${SYNC_BRANCH}" --base main --state open \ + --json number,headRepository,headRepositoryOwner,isCrossRepository \ + | jq -r --arg repository "${REPOSITORY}" \ + '[ .[] | select(.isCrossRepository == false and (.headRepositoryOwner.login + "/" + .headRepository.name) == $repository) | .number ][0] // empty' + } + close_own_open_pr() { + local number + number="$(own_open_pr)" + if [[ -n "${number}" ]]; then + gh pr close "${number}" -R "${REPOSITORY}" --comment "The tracker already matches GitHub, so this sync no longer proposes a change; closing this review PR." + fi + } + # Installing the pinned extension can add it to an explicit + # extensions.enabled list, and pm records that settings change in the + # workspace history. Both are setup state, not synced data, so neither + # reaches the commit (the extension is not needed after the gate). + for setup_path in .agents/pm/settings.json .agents/pm/history/_workspace.jsonl; do + if git cat-file -e "HEAD:${setup_path}" 2>/dev/null; then + git restore --source=HEAD --staged --worktree -- "${setup_path}" + else + git rm -q --cached --ignore-unmatch -- "${setup_path}" + rm -f -- "${setup_path}" + fi + done + if git diff --cached --quiet; then + echo "GitHub issues already match the pm tracker; no PR is needed." + # A stale own review PR would still look actionable although this + # run has no change to propose, so close it before returning. + close_own_open_pr + exit 0 + fi + git diff --cached --check + links_file="${RUNNER_TEMP}/pm-github-sync-links.md" + : > "${links_file}" + while IFS= read -r changed; do + case "${changed}" in + .agents/pm/*/*.toon) + item_id="${changed##*/}" + item_id="${item_id%.toon}" + # The main link is the permanent one after merge; the branch link + # shows the proposed item while the review PR is open. + printf -- '- [%s](https://github.com/%s/blob/main/%s) ([proposed](https://github.com/%s/blob/%s/%s))\n' "${item_id}" "${REPOSITORY}" "${changed}" "${REPOSITORY}" "${SYNC_BRANCH}" "${changed}" >> "${links_file}" + ;; + esac + done < <(git diff --cached --name-only -- .agents/pm) + if [[ ! -s "${links_file}" ]]; then + echo "::error::The sync changed tracker state but no item link could be generated." + exit 1 + fi + git commit -m "chore(pm): sync GitHub issues (gated)" + gh auth setup-git + git push --force-with-lease="refs/heads/${SYNC_BRANCH}:${SYNC_LEASE}" origin "HEAD:refs/heads/${SYNC_BRANCH}" + body_file="${RUNNER_TEMP}/pm-github-sync-pr.md" + { + printf '%s\n\n' "Automated pm-github issue synchronization for ${REPOSITORY}." + printf '%s\n\n' "The import ran with \`--gate\`: the plan was verified for completeness and provenance idempotency before the write, and the proposed tracker change passed the fail-closed privacy gate before anything was committed. \`main\` is never pushed directly." + printf '%s\n\n' "Tracker items changed:" + cat "${links_file}" + } > "${body_file}" + pr_number="$(own_open_pr)" + if [[ -z "${pr_number}" ]]; then + gh pr create -R "${REPOSITORY}" --base main --head "${SYNC_BRANCH}" --title "chore(pm): sync GitHub issues (gated)" --body-file "${body_file}" + else + gh pr edit "${pr_number}" -R "${REPOSITORY}" --body-file "${body_file}" + fi diff --git a/CHANGELOG.md b/CHANGELOG.md index ff88ddc6..0fb08323 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,17 @@ ## Unreleased +### Added + +- Fail-closed privacy gate for automated GitHub issue imports and one reusable sync workflow ([pm-github-f1at](https://github.com/unbraind/pm-github/blob/main/.agents/pm/features/pm-github-f1at.toon)) + +### Fixed + +- Root CI runs skip the read-only-directory comment-sync fixtures visibly instead of failing them ([pm-github-rap0](https://github.com/unbraind/pm-github/blob/main/.agents/pm/issues/pm-github-rap0.toon)) + ### Other +- Reach exact all-source coverage and include every operational script ([pm-github-9cjx](https://github.com/unbraind/pm-github/blob/main/.agents/pm/tasks/pm-github-9cjx.toon)) - Auto-merge green Dependabot updates and group the pm toolchain into one daily PR ([pm-github-u9df](https://github.com/unbraind/pm-github/blob/main/.agents/pm/tasks/pm-github-u9df.toon)) ## 2026.9.26 - 2026-09-26 diff --git a/README.md b/README.md index 03d42b76..d989cce4 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ pm install npm:pm-github --global |---|---| | `importers` | `pm github import ` — idempotent native import pipeline | | `importers` (exporter) | `pm github export` — pm items → GitHub issues (dry-run by default; upsert) | -| `commands` | `pm gh-issues import` (legacy import alias), `pm github sync` (push status), `pm github validate` (diagnostics), `pm github project list\|fields\|import\|sync` (Projects v2) | +| `commands` | `pm gh-issues import` (legacy import alias), `pm github sync` (push status), `pm github gate` (fail-closed pre-push privacy gate), `pm github validate` (diagnostics), `pm github project list\|fields\|import\|sync` (Projects v2) | | `schema` | declares `github_url`, `github_number`, `github_state`, `github_author`, `github_created_at`, `github_updated_at` item fields | | `hooks` | `afterCommand` — opt-in sync reminder (`PM_GITHUB_SYNC`) for linked items | | `preflight` | early warning when a mutating github command lacks a token | @@ -91,6 +91,7 @@ pm github import owner/repo --dry-run | `--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. | | `--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 | | `--type ` | string | Override pm item type (default: Issue) | @@ -248,6 +249,95 @@ pm search "uppercase dashes" --semantic # hits = imported items whose ups Enable it by setting `search.provider` to `"github"` in `.agents/pm/settings.json` and pointing it at a repo via the `PM_GITHUB_REPO` env var. +## Privacy gate + +### `pm github gate` + +GitHub issue text is untrusted: anyone who can open an issue can put a credential, a personal email, or a host path in it. An automated import writes that text into the pm tracker, and a sync pipeline then pushes the tracker to a public branch — publishing the leak with it. `pm github gate` is the fail-closed step between the write and any push: + +```bash +pm github gate # scan the proposed tracker change in the working tree +pm github gate --json # machine-readable report (also via the global --json) +pm github gate --diff sync.patch # scan an explicit unified diff file instead +pm github gate --allowlist .pm-github-gate-allowlist.json +``` + +It scans **only the proposed change** — the added lines and their filenames in the staged/working/untracked diff under the resolved pm tracker path (never a hardcoded `.agents/pm`; the SDK-resolved path wins), or an explicit `--diff` file — so pre-existing reviewed content is not re-litigated on every run, and removed content (which cannot publish anything new) is never scanned. A finding in a filename uses `field: "file_path"` and redacts a sensitive item id. It fails closed (non-zero, machine-readable report with `item_id` + `field` + `rule` + the sha256 `hash` of the matched content, **never the matched content itself**) on: + +- **credentials** — GitHub (`ghp_/gho_/ghu_/ghs_/ghr_` and fine-grained `github_pat_`), npm, AWS access key ids, Slack (including app and cookie credentials), OpenAI, Anthropic tokens, generic `Authorization: Bearer` values, `-----BEGIN … PRIVATE KEY-----` blocks, and high-entropy assignments to secret-named identifiers (`token = "…"`, `api_key: …`, `accessToken: …`) whose value has high measured entropy. UUID-shaped credential values and values containing dates receive the same checks; +- **personal data** — email addresses other than no-reply identities (`@users.noreply.github.com`, `noreply@…`, `@noreply.…`), phone numbers in international (`+…`) or North-American (`(555) 123-4567`, `555-123-4567`) notation, and unformatted numbers under phone/tel/mobile contact labels; +- **host paths** — absolute local filesystem paths (POSIX home/system directories and Windows drive paths with either separator), and named home-directory references. + +Unreadable input, malformed or truncated diffs, binary input, invalid UTF-8, Git failures, malformed allowlists, and scanner errors fail the gate. Untracked operational state in the tracker root (`locks/`, `extensions/`, `checkpoints/`) is excluded. Explicitly staged operational files remain in scope. Staged and unstaged changes are scanned separately, including staged content that the working copy subsequently removed. + +**False positives** are allowlisted by *content hash*, not by pattern: put the sha256 from the finding into `.pm-github-gate-allowlist.json` at the repository root (or pass `--allowlist `) with a written justification. An entry suppresses exactly the reviewed content, can never widen to a pattern, and a missing justification fails the gate: + +```json +{ + "": { "reason": "Reviewed: public support address in the import docs item." } +} +``` + +### `pm github import --gate` + +`--gate` composes the gate into the import pipeline, in the order a sync workflow needs: + +1. **dry-run plan** — every fetched issue is prepared up front; +2. **completeness check** — every fetched issue is accounted for exactly once (planned or explicitly skipped), nothing outside the fetch appears, counts reconcile — all before any mutation; +3. **idempotency check** — no duplicate `gh:owner/repo#N` provenance tags in the existing corpus, every new entry born with its tag, no two plan entries resolving to the same item — so a second run reuses the existing item; +4. **write** — the ordinary (atomic or per-item) import; +5. **gate** — the proposed tracker change is scanned; a finding exits non-zero, so the caller never commits or pushes. + +Rendered values are also scanned in memory before mutation, so a known finding cannot be written to disk. After mutation, the importer verifies that every planned provenance tag exists exactly once, then scans the tracker diff. A completed SDK journal cannot substitute for missing item files. + +The `gate` receipt reports the verdict, scanned file count, findings, and allowlisted count. With `--json`, successful reports go to stdout; failures use the host CLI's JSON error envelope on stderr, with the redacted gate report serialized in `detail` (`jq ' .detail | fromjson ' gate-error.json`). Unreadable inputs produce a nonzero JSON refusal. + +## Automated sync workflow + +Fleet repos sync their issues into pm through ONE audited implementation: the reusable `workflow_call` workflow at [`.github/workflows/pm-github-sync.yml`](.github/workflows/pm-github-sync.yml) in this repository. It runs the gated pipeline in fail-closed order — checkout → install the **pinned** extension → `pm github validate` → dry-run plan → **gated import** → verify the repeat import is a no-op → `pm health --strict-exit` → and only then commit, push the sync branch, and open/update a review PR whose body links every changed item as `https://github.com//blob/main/.agents/pm//.toon`. Because `--gate` exits non-zero on any finding, the job can never reach the push step with a leak in the change. Actions are pinned by commit SHA; permissions are least-privilege (`contents: write`, `pull-requests: write`, `issues: read`). + +A fleet repo owns the schedule and calls it with the pinned versions (see [`docs/sync-workflow-caller.yml`](docs/sync-workflow-caller.yml)): + +```yaml +name: Sync GitHub issues into pm +on: + schedule: + - cron: "17 2 * * *" + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + issues: read + +jobs: + gated-sync: + uses: unbraind/pm-github/.github/workflows/pm-github-sync.yml@RELEASE_COMMIT_SHA + with: + pm-github-version: "RELEASE_VERSION" # required: exact published version +``` + +Replace the two release placeholders after the gated extension is published; the current published version does not contain this feature. The calling repo needs `@unbrained/pm-cli` as a devDependency (the workflow's `npm ci` provides the CLI) and its pm tracker at `.agents/pm` committed on `main`. Under **Settings → Actions → General → Workflow permissions** it must also allow read and write token permissions and enable **Allow GitHub Actions to create and approve pull requests** (an organization can disable this), or the run pushes the sync branch and then fails to open the review PR. A PR that `GITHUB_TOKEN` opens or edits does not trigger `pull_request` workflows, so the sync PR gets no CI checks until a maintainer re-runs them (for example by closing and reopening the PR); the reusable workflow always uses the job's `GITHUB_TOKEN`. + +### Installed CLI timeout acceptance + +`node test/helpers/public-acceptance.ts` packs the candidate, installs it beside +its pinned host CLI in a disposable Git project, retrieves a paginated public +issue/comment snapshot, and replays the unchanged responses through the installed +HTTP client. It verifies a nonempty gated Node import, byte-identical Node and +three native Bun repeats, and strict health. No remote is configured. + +Each import retains the original 45-second execution deadline. The acceptance +watchdog sends SIGTERM to the process group, then SIGKILL after a five-second +cleanup window; any timeout fails acceptance even if the child later exits zero. +A real spinning-child negative control verifies forced cleanup and unchanged +fixture bytes. The HTTP client also enforces a 30-second wall-clock deadline through redirect +and response-body completion, even when native socket timeouts do not fire. +The comment-lock retry honors its existing wait budget when +an exclusive-create collision cannot be statted, including a dangling symlink. +This reproduced lock defect is separate from the intermittent historical Bun +repeat: recent packed repeats pass, but its original cause is not established. + ## Validate / diagnostics ### `pm github validate` @@ -266,7 +356,9 @@ MIT ## Release Automation -This package is release-ready for GitHub, npm, and Bun-compatible installs. CI runs type checking, build, production dependency audit, package packing, Bun install verification, and pm-changelog validation. The daily release workflow publishes only when commits exist after the latest release tag and uses pm-changelog to generate CHANGELOG.md and GitHub release notes. +Release checks require type checking, docstrings, ESLint, zero source duplication, exact 100/100/100/100 coverage across authored TypeScript and JavaScript modules including operational scripts, production dependency audit, package packing, Bun behavior, strict PM health, and pm-changelog validation. Unloaded modules count at zero; no source ignores or lowered thresholds are accepted. Coverage shortfalls block release. The shell changelog-date verifier is exercised separately and is outside the V8 percentage denominator. The daily release workflow publishes only when commits exist after the latest release tag and uses pm-changelog to generate CHANGELOG.md and GitHub release notes. + +`npm run changelog:full` reads the complete tracker through the SDK and feeds pm-changelog. Closed work supplies release history; the explicit `changelog-unreleased` tag includes a pending candidate without closing its PM item. Other open work stays excluded. ## Multi-agent merge safety diff --git a/docs/coverage-gap-inventory.md b/docs/coverage-gap-inventory.md new file mode 100644 index 00000000..c868ea86 --- /dev/null +++ b/docs/coverage-gap-inventory.md @@ -0,0 +1,123 @@ +# Coverage gap inventory and closure evidence + +The baseline test run had four fixture failures, corrected subsequently. This lists every zero-count statement and branch at that measurement; final acceptance must remeasure frozen source. No exemptions or threshold changes. The initial gate.ts receipt overlapped scanner edits; its listed coordinates are historical diagnostics, not exact-source evidence. Subsequent full receipts use frozen authored source. + +## gate.ts + +Uncovered statement lines: 984, 985, 986, 987, 988, 989, 990, 991, 992, 993, 994, 995. + +Uncovered branches: none. + +## index.ts + +Uncovered statement lines: 1764, 1765, 1921, 1922, 1928, 1929, 1998, 1999, 2193, 2194, 2195, 2196, 2197, 2198, 2223, 2224, 2225, 2932, 2933, 2934, 2935, 2936, 2937, 2938, 2942, 2943, 3128, 3129, 3130, 3131, 3132, 3283, 3284, 3285, 3286, 3289, 3290, 3327, 3328, 3329, 3330, 3332, 3333, 3334, 3335, 3336, 3337, 3338, 3339, 3345, 3346, 3913, 3914, 4071, 4072, 4466, 4846, 4847, 4883, 4884. + +Uncovered branches (start line:column to end line:column): 1208:33-1208:60, 1227:32-1227:37, 1404:23-1404:53, 1405:24-1405:53, 1441:27-1441:73, 1468:25-1468:71, 1638:34-1638:46, 1639:57-1639:61, 1640:51-1640:56, 1763:4-1765:3, 1850:28-1850:60, 1851:45-1851:56, 1916:67-1916:80, 1920:6-1922:5, 1927:6-1929:5, 1958:51-1958:64, 2021:55-2021:68, 1997:14-1999:13, 2087:29-2087:34, 2192:42-2198:3, 2206:53-2206:66, 2218:35-2218:46, 2222:8-2225:7, 2285:13-2285:60, 2286:6-2286:16, 2492:18-2492:27, 2493:32-2493:37, 2527:59-2527:64, 2535:33-2535:42, 2537:31-2537:40, 2549:72-2549:77, 2713:34-2713:40, 2727:21-2727:118, 2741:26-2741:67, 2744:61-2744:81, 2848:18-2848:27, 2849:32-2849:37, 2931:4-2938:3, 2941:33-2943:3, 2995:51-2995:64, 3009:39-3009:54, 3009:55-3009:81, 3017:29-3017:57, 3020:-1-3020:99, 3020:100-3020:104, 3127:25-3132:7, 3139:26-3139:107, 3160:102-3160:120, 3168:6-3169:6, 3282:24-3286:9, 3288:27-3290:7, 3311:18-3311:58, 3312:19-3312:58, 3326:23-3330:9, 3331:7-3339:7, 3344:7-3346:7, 3364:22-3364:40, 3380:17-3380:108, 3381:32-3381:58, 3382:88-3382:111, 3238:20-3238:58, 3397:54-3397:86, 3398:41-3398:86, 3415:35-3415:80, 3466:18-3466:27, 3467:32-3467:37, 3469:35-3469:44, 3521:43-3521:84, 3530:31-3530:58, 3546:53-3546:66, 3570:53-3570:66, 3583:31-3583:58, 3587:75-3587:102, 3609:28-3609:33, 3613:22-3613:37, 3644:34-3644:39, 3747:8-3747:28, 3753:44-3753:70, 3866:8-3866:60, 3868:33-3868:53, 3869:8-3869:61, 3873:18-3873:33, 3885:19-3885:64, 3886:31-3886:58, 3912:20-3914:5, 3934:17-3934:62, 3935:29-3935:56, 3851:47-3851:94, 3998:70-3998:73, 4070:4-4072:3, 4129:26-4129:106, 4139:94-4139:107, 4328:36-4328:41, 4330:56-4330:81, 4373:66-4373:71, 4374:42-4374:47, 4396:21-4396:26, 4399:65-4399:76, 4400:83-4400:89, 4406:21-4406:26, 4409:65-4409:76, 4419:14-4419:19, 4461:15-4461:21, 4462:31-4462:36, 4465:35-4465:61, 4465:63-4467:5, 4490:97-4490:102, 4538:23-4538:40, 4681:64-4681:68, 4736:54-4736:111, 4739:58-4739:87, 4802:51-4802:96, 4804:19-4804:109, 4807:23-4807:114, 4830:21-4830:115, 4833:22-4833:128, 4835:21-4835:112, 4845:38-4845:52, 4845:54-4847:3, 4882:69-4884:7, 4889:17-4889:82, 4899:35-4899:40, 4911:71-4911:84, 5057:61-5057:72, 5071:79-5071:90, 5079:107-5079:118, 5088:87-5088:92, 5131:23-5131:28, 5365:43-5365:48, 5404:37-5404:42, 5526:14-5526:71. + +## scripts/coverage-gate.ts + +Uncovered statement lines: none. + +Uncovered branches (start line:column to end line:column): 102:52-102:110. + +## Final branch closure pass + +The subsequent complete run passed all 484 tests with 100% statements, lines, +and functions and 99.59% branches. Its complete remaining branch inventory in +`index.ts` was: 4134:26-106, 4686:64-68, 4809:19-109, 4812:23-114, +4838:22-128, 4840:21-112, 4893:17-82, 5075:79-90, 5408:37-42. +These coordinates refer to that measured source revision, before the last two +private-path removals shifted later lines. + +The real HTTP/PM command cases now exercise unauthenticated low-quota advice, +open and closed board listing, each local Projects mutation failure, and a +successful subprocess returning an object with missing tags. The Projects +missing-identity fixtures verify the upstream helpers reject empty ids before +the private executor can receive them. Its redundant empty-id guard was removed. +The pull planner only appends a skipped status when its name is truthy and +assigns that same name to the entry, proving the missing-name fallback dead. + +Other removed arms have their proofs recorded in `pm-github-9cjx`: validated +phone matches contain digits; fixed subprocess arguments return failure receipts; +nullish SDK bindings only execute for missing injections; successful HTTP calls +are 2xx; a loop over search hits requires nonempty hits; an atomic preview returns +before the plain preview; private project entries have parsed issue coordinates +and originate in a complete SDK corpus with tags and body fields. Exported +planner and report defaults that callers can actually supply remain tested. + +All coverage measurements use the unchanged inventory of 12 authored TypeScript +and JavaScript modules, including operational scripts, with exact thresholds, +zero source ignores, and zero skipped executable source. Final full release +receipts are recorded in the linked PM items. + +The first full npm release acceptance at `40db9b3` passes all 487 tests, with zero failures or skips. +The complete receipt covers 8,391/8,391 statements and lines, +2,218/2,218 branches, and 208/208 functions across the 12-module inventory. +Every statement and branch counter is positive; no uncovered entries remain. +The full release command also passes typecheck, build, docstrings, lint, +zero duplication, Git-object privacy, production audit, packing, changelog, +and publish attestation. Native Bun and the Bun-invoked release command are +reported separately in PM evidence. + +## Native Bun transport timeout regression + +The broad native suite and a focused rerun both showed that a real local +silent server passed the socket timeout and reached the unchanged 45-second +test limit. The HTTP client now enforces the original 30 seconds with an +explicit wall-clock timer, cleared on Promise settlement. The callback rejects +before destroying the request: native Bun can emit response end synchronously +on destruction, which must not resolve a truncated timed-out response as success. +The regression covers silent headers and a partial body that never completes +using two real HTTP connections. Details and before/after receipts are recorded +in `pm-github-hptv`; this independent defect does not establish the historical +installed-public-repeat root cause. Full release gates are remeasured after it. + +The post-fix full npm release check at `5ed8c47` also passes all 487 tests +with zero failures or skips. Exact coverage is 8,398/8,398 lines and statements, +2,219/2,219 branches, and 208/208 functions, with no uncovered counters. +The same 12-module inventory and every 100% threshold remain unchanged. + +The complete `bun run release:check` repeats those same exact counts and all +487 passing tests, with no failures/skips and every configured release gate +passing. Four syscall fault fixtures use explicit method replacement/restoration +because native Bun lacks `node:test`'s `mock.method`; the real files, permission +changes, live-peer assertions and fault outcomes are unchanged. Focused checks +pass 4/4 under Node and Bun, and the complete Node lock file passes 24/24. + +Privacy executable-lookup fixtures start a real same-runtime child with its PATH +set at launch. Bun caches startup lookup and falls back to default tool lookup +for an empty PATH, so the missing-tool control uses a scratch directory with no +Git executable. Malformed inventory and blob-read controls retain real Git +repositories and narrow wrappers that delegate valid operations. The complete +25-case privacy files pass under Node and native Bun; authored production code +has not changed since the full npm/Bun release checks. + +## Historical closing receipts + +The receipts in this closing section are historical: they were measured at the +implementations named above, they cite the 487-test count of that era, and the +"remain open" note predates the orchestrator's verification. + +The final fresh coverage run with every portable fixture again passes 487/487 +without skips, covers all 12 modules at exact 100/100/100/100, and has zero +uncovered statement/branch counters. Its final receipt contains 8,398 lines and +statements, 2,220 branches and 208 functions, all covered. Earlier complete +release receipts contain 2,219/2,219 branches; the final fixture receipt is the +current measurement and remains exact 100 with unchanged production source. + +Entry-fixture cleanup explicitly restores `process.exitCode` to the previous +value or zero. Bun does not clear a prior failure when assigned undefined. +The four affected Bun files each return exit zero; their combined Node run +passes all 38 cases. The final native acceptance requires both all 487 cases +passing across 30 files and a zero command exit status. + +Final `npm run test:bun` acceptance returns process exit zero and passes all +487 cases across all 30 files, with zero failed cases. All final portable +fixtures, the original deadline checks and the executable workflow battery are +included. PM items remain open with released claims for orchestrator review. + +Current status (review round 3): `pm-github-9cjx` is closed by the orchestrator +with the exact all-source 100/100/100/100 gate green across the unchanged +12-module inventory. The authoritative closing count is 504/504 tests at the +closing head `7a41660`; the 505/505 figure is a later-head measurement taken +after later review rounds added tests, with zero failures or skips. diff --git a/docs/sync-workflow-caller.yml b/docs/sync-workflow-caller.yml new file mode 100644 index 00000000..cfa8cd59 --- /dev/null +++ b/docs/sync-workflow-caller.yml @@ -0,0 +1,19 @@ +# Replace release placeholders after the gated extension is published. +# Repository setup: Settings -> Actions -> General -> Workflow permissions must allow +# read and write permissions and enable "Allow GitHub Actions to create and approve +# pull requests" (off by default for new personal repositories; an organization can +# disable it). Without it the sync branch is pushed but the review PR is not opened. +name: Sync GitHub issues into pm +on: + schedule: + - cron: "17 2 * * *" + workflow_dispatch: +permissions: + contents: write + pull-requests: write + issues: read +jobs: + gated-sync: + uses: unbraind/pm-github/.github/workflows/pm-github-sync.yml@RELEASE_COMMIT_SHA + with: + pm-github-version: "RELEASE_VERSION" diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 00000000..a6f9d44b --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,4 @@ +/** Apply the shared strict syntax policy to authored source and tests. */ +import { fleetEslintConfig } from "pm-ops/eslint"; + +export default fleetEslintConfig(); diff --git a/gate.ts b/gate.ts new file mode 100644 index 00000000..30da93a6 --- /dev/null +++ b/gate.ts @@ -0,0 +1,1027 @@ +// pm-github — fail-closed privacy gate over a proposed tracker change. +// +// GitHub issue bodies are untrusted text: anyone who can open an issue can put a +// credential, a personal email, or a host path in it, and an automated import +// writes that text straight into the pm tracker. A sync workflow then pushes the +// tracker to a public branch — publishing the leak with it. This module is the +// gate that runs between the write and any push: it scans ONLY the proposed +// change (the added lines and their filenames in the staged/working/untracked diff under the +// resolved pm tracker path, or an explicit unified diff), so pre-existing +// reviewed content is not re-litigated on every run, and it FAILS CLOSED: +// +// * a credential signature, personal email, phone number, or host path in an +// added line fails the gate; +// * an unreadable input, a git failure, a malformed allowlist, or a scanner +// error fails the gate — a clean verdict can never be the result of the +// scanner not being able to read what it was asked to read; +// * findings name the rule, the item id, the item field, and a content hash — +// never the matched text — so gate logs cannot become the leak they exist +// to prevent; +// * reviewed false positives are suppressed through a content-addressed +// allowlist (sha256 of the matched content), not a pattern, so an entry can +// never accidentally un-suppress a different, genuinely dangerous value. +// +// The scanner rules deliberately mirror the high-confidence signature set the +// repository's own identity/privacy gate (scripts/privacy-gate.ts) enforces over +// Git objects, extended with the personal-data shapes a public branch push would +// leak from imported issue text. + +import { createHash } from "node:crypto"; +import fs from "node:fs"; +import path from "node:path"; +import { spawnSync } from "node:child_process"; +import { TextDecoder } from "node:util"; +import { resolvePmRoot } from "@unbrained/pm-cli/sdk"; + +// --------------------------------------------------------------------------- +// Types +// --------------------------------------------------------------------------- + +/** + * One fail-closed finding produced by the gate. + * + * Deliberately carries no matched text: the report is machine-readable and may + * be echoed into CI logs, so it identifies WHAT fired (rule), WHERE (item id and + * the item field that would publish it), and the content-addressed allowlist + * key (hash) — nothing more. + */ +export interface GateFinding { + /** Stable rule identifier, safe to print (never contains matched content). */ + readonly rule: string; + /** The pm item whose change carries the finding, or `""` when unattributable. */ + readonly item_id: string; + /** The item field the offending added line belongs to, or `""` when unknown. */ + readonly field: string; + /** sha256 hex digest of the matched content — the allowlist key. */ + readonly hash: string; +} + +/** Verdict of one gate run: `fail` whenever any non-allowlisted finding exists. */ +export type GateVerdict = "pass" | "fail"; + +/** Machine-readable result of one gate run over a proposed tracker change. */ +export interface GateReport { + /** `fail` when any finding survived the allowlist; `pass` otherwise. */ + readonly verdict: GateVerdict; + /** Where the scanned change came from: `git` (working tree) or `diff` (explicit file). */ + readonly source: "git" | "diff"; + /** Number of changed tracker files whose added lines were scanned. */ + readonly scanned_files: number; + /** Number of added lines scanned. */ + readonly added_lines: number; + /** Findings that survived the allowlist, ordered file/line/ruleset. */ + readonly findings: readonly GateFinding[]; + /** Findings suppressed because their content hash was allowlisted. */ + readonly allowlisted: number; + /** Path of the allowlist consulted (or `""` when none was resolved). */ + readonly allowlist_path: string; +} + +/** + * Input failure the gate treats as fail-closed: unreadable diff file, not a Git + * work tree, a Git command that failed, a malformed allowlist, or a scanner + * error. Callers translate this into a non-zero exit; it must never surface as a + * clean pass. + */ +export class GateInputError extends Error { + constructor(message: string) { + super(message); + this.name = "GateInputError"; + } +} + +/** One added line of a proposed tracker change, with its attributed item field. */ +interface AddedLine { + /** The added text with the leading diff `+` already stripped. */ + readonly text: string; + /** Best-effort item field the line belongs to (top-level toon key, or `""`). */ + readonly field: string; +} + +/** The proposed change to one tracker file: its repo-relative path plus added lines. */ +interface ChangeFile { + /** Repo-relative or diff-relative path of the changed file. */ + readonly filePath: string; + /** Best-effort pm item id for the file (`.toon` basename, embedded jsonl id, …). */ + readonly itemId: string; + /** The added lines of the proposed change for this file. */ + readonly addedLines: readonly AddedLine[]; +} + +/** Subprocess contract for running Git, injectable so tests can force failures. */ +export type RunGit = (cwd: string, args: readonly string[]) => { ok: boolean; stdout: string; stderr: string }; + +/** + * Collaborators a gate run depends on, injectable for tests. + * + * The production object shells out to Git and reads the filesystem; a test can + * substitute either to exercise the fail-closed paths deterministically (a Git + * failure, an unreadable file) without depending on machine state. + */ +export interface GateRunDependencies { + /** Git runner used to resolve the work tree and collect the change. */ + readonly runGit?: RunGit; + /** File reader used for the explicit diff file and untracked tracker files. */ + readonly readFileSync?: (filePath: string) => string; +} + +/** Resolved input for one gate run: what to scan and which allowlist to consult. */ +export interface TrackerGateInput { + /** pm root as the extension host supplies it (workspace root or data dir). */ + readonly pmRoot: string; + /** Explicit unified diff file to scan instead of the working tree. */ + readonly diffFile?: string; + /** Explicit allowlist file; missing explicit files fail closed. */ + readonly allowlistFile?: string; + /** Injectable collaborators; production defaults shell out to Git and `fs`. */ + readonly dependencies?: GateRunDependencies; + /** Rendered import values scanned in memory before persistence or diagnostics. */ + readonly plannedItems?: readonly { readonly itemId: string; readonly fields: Readonly> }[]; +} + +// --------------------------------------------------------------------------- +// pm tracker path resolution (shared with index.ts; lives here so the gate +// module never needs an import cycle with the command module) +// --------------------------------------------------------------------------- + +/** + * Resolve the pm data dir from the `pmRoot` a command handler receives. + * + * The host may hand either the workspace root (the dir containing `.agents/pm`) + * or the data dir itself — the pm CLI accepts both for `--path`. This returns + * the dir that actually holds `settings.json` and `locks/`, defaulting to + * `pmRoot` unchanged when no nested `.agents/pm` exists. + * + * @param pmRoot - The path supplied by the extension host. + * @returns The resolved pm data directory. + */ +export function resolvePmDataDir(pmRoot: string): string { + return resolvePmRoot(process.cwd(), pmRoot); +} + +// --------------------------------------------------------------------------- +// Scanner rules +// --------------------------------------------------------------------------- + +/** + * A content rule with an optional confirmation predicate. + * + * The confirmation runs on the matched text and prunes values whose raw shape + * matches the pattern but is overwhelmingly prose or structured data (e.g. a + * bearer phrase followed by a single long word with no digits). Rules whose + * signature is already high-confidence leave it unset. + */ +interface ContentRule { + /** Stable rule identifier printed in findings. */ + readonly rule: string; + /** Global regex over one added line. */ + readonly pattern: RegExp; + /** Optional extra confirmation on the matched text. */ + readonly confirm?: (matched: string) => boolean; +} + +/** + * High-confidence credential signatures. Every pattern is shaped so ordinary + * prose, identifiers, or version numbers cannot match it; broad low-confidence + * patterns are deliberately excluded so reviewed imports are not buried in + * noise — the gate stays at a zero false-positive posture on well-formed + * trackers while every listed provider shape still fails closed. + */ +const CREDENTIAL_RULES: readonly ContentRule[] = [ + { rule: "github-token-classic", pattern: /gh[ousrp]_[A-Za-z0-9]{36}/g }, + { rule: "github-token-fine-grained", pattern: /github_pat_[A-Za-z0-9_]{22,}/g }, + { rule: "npm-token", pattern: /npm_[A-Za-z0-9]{36}/g }, + { rule: "aws-access-key-id", pattern: /(?:AKIA|ASIA)[0-9A-Z]{16}/g }, + { rule: "slack-token", pattern: /(?:xox[a-z]|xapp)-[A-Za-z0-9-]{10,}/g }, + { rule: "slack-webhook", pattern: /https:\/\/hooks\.slack\.com\/services\/T[A-Za-z0-9]+\/B[A-Za-z0-9]+\/[A-Za-z0-9]{24,}/g }, + { rule: "openai-legacy-key", pattern: /sk-[A-Za-z0-9]{48}/g }, + { rule: "openai-api-key", pattern: /sk-[A-Za-z0-9_-]{20,}T3BlbkFJ[A-Za-z0-9_-]{20,}/g }, + { rule: "openai-project-key", pattern: /sk-proj-[A-Za-z0-9_-]{20,}/g }, + { rule: "openai-service-account-key", pattern: /sk-svcacct-[A-Za-z0-9_-]{20,}/g }, + { rule: "anthropic-api-key", pattern: /sk-ant-[A-Za-z0-9_-]{20,}/g }, + { + rule: "bearer-token", + pattern: /(?:authorization\s*:\s*bearer\s+[A-Za-z0-9._~+/=-]{8,}|bearer\s+[A-Za-z0-9._~+/=-]{20,})/gi, + + }, + { rule: "private-key-block", pattern: /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----/g }, +]; + +/** + * Personal-data signatures: an email that is not a no-reply address, and phone + * numbers in their two common written shapes. + */ +const PERSONAL_DATA_RULES: readonly ContentRule[] = [ + { + rule: "email-address", + // An SCP-style SSH clone URL (`git@github.com:owner/repo.git`, the colon + // followed directly by a path) names the git service account, not a + // person, so the pattern skips exactly that shape; a bare `git@host` + // contact address, even before a colon and a space, is still matched. + // The lookbehind keeps a match from starting inside a local part. + pattern: /(? { + const [local, domain] = matched.toLowerCase().split("@"); + return !/^(?:no-?reply)$/.test(local!) && !/(?:^|\.)no-?reply(?:\.|$)/.test(domain!); + }, + }, + { + rule: "phone-number", + pattern: /\+\d{1,3}[\s.-]?(?:\d{2,4}[\s.-]?){2,4}\d{2,4}/g, + // International notation needs a real phone digit count; short sequences + // like "+1 2 3" are not phone numbers. The pattern guarantees at least one + // digit, so the match can never be null here. + confirm: (matched) => matched.match(/\d/g)!.length >= 8, + }, + { + rule: "phone-number", + pattern: /\(\d{3}\)\s*\d{3}[\s.-]\d{4}|\b\d{3}[\s.-]\d{3}[\s.-]\d{4}\b/g, + // The North-American grouping is 10 digits by construction; the digit check + // only guards the parenthesized variant against truncation by surrounding + // punctuation. + confirm: (matched) => matched.match(/\d/g)!.length === 10, + }, + { + rule: "phone-number", + pattern: /\b(?:phone|telephone|tel|mobile)\b["']?\s*[:=]\s*["']?\s*\+?\d[\d \t().-]{5,}\d/gi, + // Contact labels distinguish unformatted numbers from issue/comment ids. + confirm: (matched) => { const digits = matched.replace(/\D/g, "").length; return digits >= 7 && digits <= 15; }, + }, +]; + +/** + * Host-identifying path signatures: absolute personal/system filesystem paths + * and home-directory usernames (`~user`, but never the anonymous `~/`), which + * would publish the machine and account that produced an import. The list is + * the POSIX/Windows set a developer workstation actually leaks, mirroring the + * repository's own object-store privacy gate. + */ +const HOST_PATH_RULES: readonly ContentRule[] = [ + { + rule: "absolute-host-path", + // Anchored to roots that identify a host (home, system and mount roots), + // so GitHub slash commands (`/assign`), repository-relative links + // (`/docs/setup.md`) and API routes (`GET /api/v1`) in issue text are not + // flagged; the root must end at a separator or delimiter (`/homework` is + // not `/home`). The lead-in is a lookbehind so the match is the path + // itself; it also admits `file://` URLs and `label:` prefixes (`cwd:/home/…`), + // whose two-character minimum leaves drive letters to the Windows rule. + pattern: /(?<=^|[\s"'`([=,{]|[A-Za-z0-9_]{2}:|file:\/\/)\/(?:home|Users|root|tmp|var|etc|opt|srv|mnt|media|private|Volumes|usr|run|proc|data|nix|scratch|workspace|builds|Library|System)(?=\/|$|[\s"'`<>),;\]}])(?:\/[^\s"'`<>),;\]}]*)?/g, + }, + { + rule: "windows-host-path", + // A drive letter at a token start, or as a `file:///C:/…` URL path. + pattern: /(?:(?),;\]}]+/g, + }, + { + rule: "home-username", + // A leading tilde followed by a name AND a path separator is a username + // leak (`~alice/report.txt`); `~/` (anonymous), `~~strikethrough~~`, and + // prose approximations (`~most users`) are excluded by the boundary and + // the required trailing slash. + pattern: /(?:^|[\s"'`([=,])~[A-Za-z][A-Za-z0-9._-]*\/[^\s"'`<>),;\]}]*/g, + }, +]; + +/** Identifier segments whose presence marks an assignment target as secret-bearing. */ +const SECRET_IDENTIFIER_SEGMENTS: readonly string[] = [ + "token", + "secret", + "password", + "passwd", + "credential", + "apikey", +]; + +/** + * Identifier segment pairs (normalized on `[_-]`) that mark composite assignment + * targets like `api_key` or `private_key` without matching prose such as + * `sort_key` or `author`. + */ +const SECRET_IDENTIFIER_PAIRS: readonly string[] = ["api_key", "access_key", "private_key", "secret_key"]; + +/** Assignment shape for the high-entropy rule: `NAME = "value"` / `name: value`. */ +const HIGH_ENTROPY_ASSIGNMENT = + /(?:^|[^A-Za-z0-9_])([A-Za-z][A-Za-z0-9_-]{0,48})(?:\\?["'`])?\s*[:=]\s*(?:\\?["'`])?([A-Za-z0-9+/_=-]{20,256})(?:\\?["'`])?/g; + +/** + * Shannon entropy per character of a value. + * + * Uniform random base62 has an expected ~5.5 bits/char, random hex ~4.0; natural + * language and dates sit far lower (~3–3.4). The assignment rule uses the gap: + * a long value over a secret-bearing identifier is only a finding when its + * per-character entropy is indistinguishable from random. + * + * @param value - The string to measure. + * @returns Bits per character (0 for the empty string). + */ +export function shannonEntropyPerChar(value: string): number { + if (value.length === 0) return 0; + const counts = new Map(); + for (const ch of value) counts.set(ch, (counts.get(ch) ?? 0) + 1); + let entropy = 0; + for (const count of counts.values()) { + const p = count / value.length; + entropy -= p * Math.log2(p); + } + return entropy; +} + +/** + * Whether an `identifier = "value"` assignment looks like a hardcoded secret. + * + * Fires only when the identifier names a credential (token/secret/password/ + * credential/api_key/access_key/private_key segment) AND the value is long + * (≥20 chars), high-entropy (≥3.9 bits/char — above dates, timestamps, prose), + * and the identifier names a credential. Both halves are required: identifier-only would + * flag every `token: "the token above"` prose reference, entropy-only would + * flag every generated id. + * + * @param identifier - The assignment target, e.g. `AWS_SECRET_ACCESS_KEY`. + * @param value - The assigned value text. + * @returns Whether the pair reads as a hardcoded credential. + */ +export function isHighEntropySecretAssignment(identifier: string, value: string): boolean { + if (value.length < 20) return false; + const segments = identifier.replace(/([A-Z])([A-Z][a-z])/g, "$1_$2") + .replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase().split(/[_-]+/).filter(Boolean); + const namesSecret = + segments.some((segment) => SECRET_IDENTIFIER_SEGMENTS.includes(segment)) || + SECRET_IDENTIFIER_PAIRS.includes(segments.join("_")); + if (!namesSecret) return false; + return shannonEntropyPerChar(value) >= 3.9; +} + +/** One raw hit of a content rule, before item/field attribution. */ +interface RuleHit { + /** Rule identifier that fired. */ + readonly rule: string; + /** The matched text — kept in memory only; never printed or reported. */ + readonly matched: string; + /** Character offset of the match within its line, for jsonl field attribution. */ + readonly index: number; +} + +/** Sticky `\uXXXX` matcher: reads at one offset without copying the line suffix. */ +const UNICODE_ESCAPE = /\\u([0-9a-f]{4})/iy; + +/** + * Scan one added line with every content rule. + * + * Rules are evaluated independently: one line can produce several hits (a + * bearer header wrapping a GitHub token reports both shapes), because each is a + * distinct reviewed rule. The high-entropy assignment rule is regex-scan + + * predicate, so it is driven here rather than in the static table. + * + * @param line - The added line text (diff `+` already stripped). + * @returns Every rule hit on the line, in evaluation order. + */ +export function scanLineForRuleHits(line: string): RuleHit[] { + // Serialized tracker text may escape separators or provider prefixes. Map + // decoded characters back to their original offsets for history attribution. + const offsets: number[] = []; + let decoded = ""; + for (let index = 0; index < line.length; index++) { + // A sticky match at the current offset keeps the decoder linear in the line + // length; slicing the suffix per character is quadratic where slices copy. + UNICODE_ESCAPE.lastIndex = index; + const unicode = line[index] === "\\" ? UNICODE_ESCAPE.exec(line) : null; + if (unicode) { + decoded += String.fromCharCode(Number.parseInt(unicode[1]!, 16)); + offsets.push(index); + index += 5; + } else if (line[index] === "\\" && /[\\/nrt]/.test(line[index + 1] ?? "")) { + decoded += ({ n: "\n", r: "\r", t: "\t" } as Record)[line[index + 1]!] ?? line[index + 1]!; + offsets.push(index); + index++; + } else { + decoded += line[index]!; + offsets.push(index); + } + } + const hits: RuleHit[] = []; + for (const rule of [...CREDENTIAL_RULES, ...PERSONAL_DATA_RULES, ...HOST_PATH_RULES]) { + rule.pattern.lastIndex = 0; + for (let match = rule.pattern.exec(decoded); match; match = rule.pattern.exec(decoded)) { + if (!rule.confirm || rule.confirm(match[0])) { + const matched = rule.rule === "home-username" ? match[0].slice(match[0].indexOf("~")) : match[0]; + hits.push({ rule: rule.rule, matched, index: offsets[match.index]! }); + } + } + } + HIGH_ENTROPY_ASSIGNMENT.lastIndex = 0; + for (let match = HIGH_ENTROPY_ASSIGNMENT.exec(decoded); match; match = HIGH_ENTROPY_ASSIGNMENT.exec(decoded)) { + const [identifier, value] = [match[1]!, match[2]!]; + if (isHighEntropySecretAssignment(identifier, value)) { + hits.push({ rule: "high-entropy-assignment", matched: value, index: offsets[match.index]! }); + } + } + return hits; +} + +// --------------------------------------------------------------------------- +// Item id + field attribution +// --------------------------------------------------------------------------- + +/** Top-level field header of a `.toon` line, e.g. `body:`, `notes[7]{…}:`, `tags[5]:`. */ +const TOON_FIELD_HEADER = /^([a-z][a-z0-9_]*)(?:\[\d+\])?(?:\{[^}]*\})?\s*:/; + +/** One source line of a diff walk, marked added or unchanged context. */ +interface WalkLine { + /** Line text without the diff marker. */ + readonly text: string; + /** Whether the line is an added (`+`) line rather than context. */ + readonly added: boolean; +} + +/** + * Attribute a toon field to every line of a change walk. + * + * Walks the lines in file order maintaining the enclosing top-level field: a + * field header (added OR context) updates the tracker, so an added section row + * (`"ts",author,"text"`) attributes to its section (`notes`, `files`, …) even + * when the section header itself is unchanged context. Added lines that are + * their own field header attribute to that field. + * + * @param lines - Added and context lines in walk order. + * @returns A field name per input line (never `undefined`; `"unknown"` when no header was seen yet). + */ +export function attributeToonFields(lines: readonly WalkLine[]): string[] { + let current = "unknown"; + return lines.map((line) => { + const match = TOON_FIELD_HEADER.exec(line.text); + if (match) current = match[1]!; + return current; + }); +} + +/** Item id statement inside a pm history JSONL event. */ +const JSONL_ITEM_ID = /"path":"\/metadata\/id"\s*,\s*"value":"([^"]+)"/; + +/** + * Resolve the item id for a changed tracker file. + * + * `.toon` files are named `.toon`; history JSONL carries the id inside the + * event (the file itself is also named `.jsonl`, but reading the event makes + * a synthesized diff work identically); other paths have no item id. + * + * @param filePath - The changed file path. + * @param line - The added line being attributed (already needed for JSONL). + * @returns The item id, or `""`. + */ +function itemIdForFile(filePath: string, line: string): string { + const base = path.basename(filePath); + if (base.endsWith(".toon")) return base.slice(0, -".toon".length); + if (base.endsWith(".jsonl")) return JSONL_ITEM_ID.exec(line)?.[1] ?? base.slice(0, -".jsonl".length); + return ""; +} + +// --------------------------------------------------------------------------- +// Change collection — unified diff parsing and Git working-tree collection +// --------------------------------------------------------------------------- + +/** + * Parse a unified diff into per-file added lines with toon field attribution. + * + * Accepts `git diff` (with or without `a/` `b/` prefixes) and plain + * `diff -u` output. Removed lines are skipped — only the proposed ADDITIONS can + * publish new content; deletions cannot leak anything that was not already in + * the tracked state. + * + * @param diffText - The unified diff text. + * @returns One entry per changed file with added lines and attribution. + */ +export function parseUnifiedDiff(diffText: string): ChangeFile[] { + if (diffText.trim() !== "" && !/^\+\+\+ /m.test(diffText) && !/^diff --git /m.test(diffText)) { + throw new GateInputError("pm github gate: input is not a unified diff."); + } + const files: ChangeFile[] = []; + let filePath = ""; + let itemId = ""; + let walk: WalkLine[] = []; + let oldRemaining = 0; + let newRemaining = 0; + let inHunk = false; + const completeHunk = (): void => { + if (oldRemaining !== 0 || newRemaining !== 0) { + throw new GateInputError("pm github gate: unified diff is truncated or malformed."); + } + inHunk = false; + }; + + const flushFile = (): void => { + if (filePath === "" || walk.length === 0) { + walk = []; + return; + } + const fields = attributeToonFields(walk); + const added = walk + .map((line, index) => ({ line, field: fields[index]! })) + .filter((entry) => entry.line.added) + .map((entry) => ({ text: entry.line.text, field: entry.field })); + files.push({ filePath, itemId, addedLines: added }); + walk = []; + }; + + const lines = diffText.split("\n"); + for (const raw of lines) { + if (raw === "GIT binary patch" || raw.startsWith("Binary files ") || raw.includes("\0")) { + throw new GateInputError("pm github gate: binary input cannot be privacy-scanned."); + } + if (raw.startsWith("diff --git ") || (raw.startsWith("--- ") && !(inHunk && oldRemaining > 0))) { + completeHunk(); + flushFile(); + filePath = ""; + continue; + } + if (raw.startsWith("+++ ") && !(inHunk && newRemaining > 0)) { + completeHunk(); + flushFile(); + filePath = raw.slice(4).split("\t")[0]!.trim(); + if (filePath.startsWith('"')) { + try { filePath = JSON.parse(filePath) as string; } catch { + throw new GateInputError("pm github gate: unreadable diff filename."); + } + } + if (filePath === "/dev/null") filePath = ""; + else filePath = filePath.replace(/^[ab]\//, ""); + itemId = ""; + continue; + } + // Header and index lines before a target are metadata, but a hunk is parsed + // even without one: its added lines must never pass unscanned. + if (filePath === "" && !inHunk && !raw.startsWith("@@")) continue; + if (raw.startsWith("@@")) { + completeHunk(); + const hunk = /^@@ -\d+(?:,(\d+))? \+\d+(?:,(\d+))? @@/.exec(raw); + if (!hunk) throw new GateInputError("pm github gate: invalid unified diff hunk."); + oldRemaining = Number(hunk[1] ?? 1); + newRemaining = Number(hunk[2] ?? 1); + inHunk = true; + continue; + } + // Diff metadata (`\ No newline at end of file`) is checked before the marker + // checks: real added content is always `+`-prefixed, so a bare `\\` line is + // never content. + if (raw.startsWith("\\")) continue; + if (raw === "" && !inHunk) continue; + if (!inHunk) throw new GateInputError("pm github gate: content outside a diff hunk."); + if (raw.startsWith("+")) { + // Only `+++ /dev/null` or a header without a ---/+++ pair leaves no + // target; Git never adds lines there, so an operator-supplied --diff that + // does is malformed and fails closed rather than passing vacuously. + if (filePath === "") throw new GateInputError("pm github gate: added diff content has no target file."); + newRemaining--; + const text = raw.slice(1); + if (itemId === "") itemId = itemIdForFile(filePath, text); + walk.push({ text, added: true }); + continue; + } + if (raw.startsWith("-")) { oldRemaining--; continue; } + if (raw.startsWith(" ")) { + oldRemaining--; + newRemaining--; + // The enclosing branch guarantees the leading space. + walk.push({ text: raw.slice(1), added: false }); + continue; + } + if (raw === "" && oldRemaining === 0 && newRemaining === 0) { completeHunk(); continue; } + throw new GateInputError("pm github gate: invalid unified diff content."); + } + completeHunk(); + flushFile(); + + return files; +} + +/** + * Read one whole file as a proposed addition (an untracked tracker file). + * + * A file Git has never tracked has no diff, but its full content is exactly the + * change a commit would publish, so every line is treated as added. + * + * @param filePath - Absolute path of the file to read. + * @param repoRelative - Repo-relative path for attribution. + * @param readFileSync - File reader (injectable). + * @returns The change file with all lines added. + */ +function wholeFileChange( + filePath: string, + repoRelative: string, + readFileSync: (filePath: string) => string, +): ChangeFile { + const content = readFileSync(filePath); + if (content.includes("\0")) throw new GateInputError("pm github gate: binary input cannot be privacy-scanned."); + const lines = content.split("\n").map((text) => ({ text, added: true })); + const fields = attributeToonFields(lines); + let itemId = ""; + const addedLines = lines.map((line, index) => { + if (itemId === "") itemId = itemIdForFile(repoRelative, line.text); + return { text: line.text, field: fields[index]! }; + }); + return { filePath: repoRelative, itemId, addedLines }; +} + +/** + * Operational tracker subpaths the gate does not scan. + * + * `locks/`, `extensions/`, and `checkpoints/` hold transient process state, + * installed extension code, and resumable journals — none of it is pm item + * content, all of it is either gitignored or runtime-managed, and scanning + * installed extension code would re-litigate reviewed package text on every + * run. Item files, history, schema, and settings stay in scope. + */ +const UNTRACKED_SCAN_EXCLUDED = ["locks", "extensions", "checkpoints"]; + +/** + * Whether a repo-relative changed path is operational state, out of gate scope. + * + * @param repoRelative - Repo-relative path of the changed file. + * @returns Whether the path is excluded from scanning. + */ +function isOperationalTrackerPath(repoRelative: string): boolean { + const normalized = repoRelative.replace(/\\/g, "/"); + return UNTRACKED_SCAN_EXCLUDED.includes(normalized.split("/")[0]!); +} + +/** + * Run Git with the production subprocess runner. + * + * Trimmed stdout is returned as-is; a non-zero exit yields `ok: false` with the + * child's stderr so the fail-closed collector can report a diagnosable error. + * + * @param cwd - Directory to run in. + * @param args - Git argument vector. + * @returns The normalized subprocess result. + */ +export function runGitDefault(cwd: string, args: readonly string[]): { ok: boolean; stdout: string; stderr: string } { + const result = spawnSync("git", ["-C", cwd, ...args], { maxBuffer: 64 * 1024 * 1024 }); + return { + ok: result.status === 0, + // stdout/stderr are nullish only when the child never spawned, and spawnSync + // then always reports the failure through `result.error`, so no second + // "git failed" fallback can ever be needed beyond String(result.error). + stdout: new TextDecoder("utf-8", { fatal: true }).decode(result.stdout ?? Buffer.alloc(0)), + stderr: result.stderr ? new TextDecoder("utf-8", { fatal: true }).decode(result.stderr) : String(result.error), + }; +} + +/** + * Read tracker, diff, or allowlist text without silently replacing invalid bytes. + * + * @param filePath - Input file to decode as UTF-8. + * @returns Complete decoded text; malformed input throws and fails the gate. + */ +function readTrackerText(filePath: string): string { + return new TextDecoder("utf-8", { fatal: true }).decode(fs.readFileSync(filePath)); +} + +/** + * Collect the proposed tracker change from the Git working tree. + * + * The proposed change is everything a commit would publish: staged plus unstaged + * modifications of tracked files under the tracker path (their added diff lines) + * and the full content of untracked files under it. Unreachable/deleted paths + * contribute nothing; operational subpaths are excluded by + * {@link isOperationalTrackerPath}. Any Git or read failure throws + * {@link GateInputError} — fail closed, never pass vacuously. + * + * @param pmDataDir - The resolved pm data directory. + * @param dependencies - Injectable Git runner and file reader. + * @returns One change entry per scanned file (empty when the tree is clean). + */ +export function collectTrackerChange( + pmDataDir: string, + dependencies: GateRunDependencies = {}, +): ChangeFile[] { + const runGit = dependencies.runGit ?? runGitDefault; + const readFileSync = dependencies.readFileSync ?? readTrackerText; + + const toplevel = runGit(pmDataDir, ["rev-parse", "--show-toplevel"]); + if (!toplevel.ok || toplevel.stdout.trim() === "") { + throw new GateInputError( + "pm github gate: the pm root is not inside a Git work tree, so the proposed tracker change cannot be read. " + + "Run inside the repository, or pass --diff to scan an explicit diff.", + ); + } + const repoRoot = fs.realpathSync(toplevel.stdout.trim()); + const trackerDir = fs.realpathSync(pmDataDir); + const trackerRel = path.relative(repoRoot, trackerDir); + if (trackerRel.startsWith("..")) { + throw new GateInputError( + "pm github gate: the resolved pm tracker path escapes the Git work tree; refusing to scan an unclear change.", + ); + } + const trackerSpec = trackerRel === "" ? "." : trackerRel; + + // Literal pathspecs: a tracker path must never be read as pathspec magic + // (":(...)", ":!") or a glob, or its diff could come back empty and unread. + const status = runGit(repoRoot, ["--literal-pathspecs", "status", "--porcelain=v1", "-z", "--untracked-files=all", "--", trackerSpec]); + if (!status.ok) { + throw new GateInputError("pm github gate: git status failed; refusing to scan incomplete input."); + } + + const changes: ChangeFile[] = []; + const tokens = status.stdout.split("\0").filter((token) => token !== ""); + for (let index = 0; index < tokens.length; index++) { + const entry = tokens[index]!; + const xy = entry.slice(0, 2); + const filePath = entry.slice(3); + // Rename/copy entries carry the original path as the next NUL token; it is + // provenance of the rename, not a second changed path. + if (xy[0] === "R" || xy[0] === "C" || xy[1] === "R" || xy[1] === "C") index++; + if (xy === "??" && isOperationalTrackerPath(path.relative(trackerDir, path.join(repoRoot, filePath)))) continue; + if (xy === "??") { + const absolute = path.join(repoRoot, filePath); + let size = 0; + let symlink = false; + try { + const stat = fs.lstatSync(absolute); + size = stat.size; + symlink = stat.isSymbolicLink(); + } catch { + throw new GateInputError("pm github gate: a proposed file cannot be inspected."); + } + if (size > UNTRACKED_FILE_BYTE_CAP) { + throw new GateInputError( + `pm github gate: an untracked tracker file exceeds the ${UNTRACKED_FILE_BYTE_CAP}-byte scan cap; refusing to guess.`, + ); + } + changes.push(wholeFileChange(absolute, filePath, symlink ? (file) => fs.readlinkSync(file) : readFileSync)); + continue; + } + if (xy.includes("D") && !/[AMU]/.test(xy)) continue; // pure deletion + const addedLines: AddedLine[] = []; + for (const revision of [["--cached"], []]) { + const diff = runGit(repoRoot, ["--literal-pathspecs", "diff", "--no-color", "--no-ext-diff", "--no-textconv", "--no-renames", "--unified=2147483647", ...revision, "--", filePath]); + if (!diff.ok) { + throw new GateInputError("pm github gate: git diff failed; refusing to scan incomplete input."); + } + for (const parsed of parseUnifiedDiff(diff.stdout)) addedLines.push(...parsed.addedLines); + } + if (addedLines.length > 0) changes.push({ filePath, itemId: itemIdForFile(filePath, ""), addedLines }); + } + return changes; +} + +// --------------------------------------------------------------------------- +// Allowlist +// --------------------------------------------------------------------------- + +/** One reviewed allowlist entry for a matched content hash. */ +interface AllowlistEntry { + /** Human-readable justification recorded next to the exemption in review. */ + readonly reason: string; +} + +/** + * Parse a gate allowlist file. + * + * The file maps the sha256 of REVIEWED matched content to a written + * justification. Entries without a non-empty reason are rejected — an unreviewed + * exemption is how a gate rots. A missing file fails closed only when it was + * passed explicitly; the default location is optional so a clean repo needs no + * allowlist file. + * + * @param allowlistPath - Absolute path of the allowlist JSON. + * @param required - Whether an explicit caller named the file (missing → error). + * @param readFileSync - File reader (injectable). + * @returns Hash → justification entries. + */ +export function readGateAllowlist( + allowlistPath: string, + required: boolean, + readFileSync: (filePath: string) => string = (filePath) => fs.readFileSync(filePath, "utf-8"), +): Map { + let raw: string; + try { + raw = readFileSync(allowlistPath); + } catch (err: unknown) { + if (!required && (err as NodeJS.ErrnoException).code === "ENOENT") return new Map(); + throw new GateInputError("pm github gate: allowlist file is unreadable."); + } + let parsed: unknown; + try { + parsed = JSON.parse(raw); + } catch { + throw new GateInputError("pm github gate: allowlist file is not valid JSON."); + } + if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) { + throw new GateInputError(`pm github gate: allowlist file must be a JSON object keyed by content hash`); + } + const entries = new Map(); + for (const [hash, value] of Object.entries(parsed as Record)) { + if (!/^[0-9a-f]{64}$/.test(hash)) { + throw new GateInputError(`pm github gate: allowlist key is not a sha256 content hash`); + } + const reason = (value as { reason?: unknown } | null)?.reason; + if (typeof reason !== "string" || reason.trim() === "") { + throw new GateInputError(`pm github gate: allowlist entry ${hash.slice(0, 12)}… has no review justification`); + } + entries.set(hash, { reason: reason.trim() }); + } + return entries; +} + +/** Default allowlist filename, resolved at the repository root when one exists. */ +export const GATE_ALLOWLIST_FILENAME = ".pm-github-gate-allowlist.json"; + +/** Byte cap for reading one untracked tracker file whole (8 MiB). */ +const UNTRACKED_FILE_BYTE_CAP = 8 * 1024 * 1024; + +// --------------------------------------------------------------------------- +// Gate run +// --------------------------------------------------------------------------- + +/** + * Scan one change file's added lines and proposed filename and return raw findings. + * + * Each hit is attributed to the changed file's item id; the field comes from the + * toon walk for `.toon` files and from the enclosing JSONL patch entry for + * history files. Duplicate hits of the same rule + content + item + field + * within one file collapse to one finding, so one pasted token repeated in a + * body reads as one finding, not as noise. + * + * @param change - The proposed change for one file. + * @returns Raw findings including matched content (used for hashing only). + */ +function scanChangeFile(change: ChangeFile): Array { + const findings: Array = []; + const seen = new Set(); + const proposedLines = change.addedLines.length === 0 ? [] : [ + { text: change.filePath, field: "file_path" }, ...change.addedLines, + ]; + for (const line of proposedLines) { + const values: Array<{ text: string; field: string }> = []; + if (change.filePath.endsWith(".jsonl") && line.field !== "file_path") { + if (line.text.trim() === "") continue; + // Structural JSON patch paths are pointers, while their values are data. + // Decode data before scanning so escaped separators cannot bypass a rule. + let parsed: unknown; + try { parsed = JSON.parse(line.text); } catch { + throw new GateInputError("pm github gate: malformed history event."); + } + const pending: Array<{ value: unknown; field: string }> = [{ value: parsed, field: "unknown" }]; + while (pending.length > 0) { + const entry = pending.pop()!; + if (typeof entry.value === "string") values.push({ text: entry.value, field: entry.field }); + else if (Array.isArray(entry.value)) { + for (const value of entry.value) pending.push({ value, field: entry.field }); + } else if (entry.value !== null && typeof entry.value === "object") { + const record = entry.value as Record; + const patch = typeof record.op === "string" && ["add", "replace", "remove", "test", "move", "copy"].includes(record.op) && typeof record.path === "string"; + if (patch) { + for (const key of ["path", "from"]) { + // Pointers are scanned like data: the host-path rules are anchored + // to host roots, so `/metadata/...` and `/body` pointers never match, + // and a secret embedded in a field key is still caught. + if (typeof record[key] === "string") values.push({ text: record[key], field: key }); + } + } + if (patch && Object.hasOwn(record, "value")) { + const parts = (record.path as string).split("/").filter(Boolean); + pending.push({ value: record.value, field: parts[0] === "metadata" ? (parts[1] ?? "metadata") : (parts[0] ?? "unknown") }); + } + for (const [key, value] of Object.entries(record)) { + if (patch && ["op", "path", "value", "from"].includes(key)) continue; + // Keys are data too: a credential used as an object key must not pass + // unscanned (a .toon line with the same content is scanned as text). + values.push({ text: key, field: entry.field }); + pending.push({ value, field: key === "metadata" || key === "patch" ? entry.field : key }); + } + } + } + } else values.push({ text: line.text, field: line.field }); + for (const value of values) { + for (const hit of scanLineForRuleHits(value.text)) { + const field = value.field; + const itemId = change.itemId || itemIdForFile(change.filePath, line.text); + const key = `${hit.rule}\0${itemId}\0${field}\0${hit.matched}`; + if (seen.has(key)) continue; + seen.add(key); + findings.push({ + rule: hit.rule, + item_id: scanLineForRuleHits(itemId).length > 0 ? "" : itemId, + field: scanLineForRuleHits(field).length > 0 ? "unknown" : field, + hash: createHash("sha256").update(hit.matched).digest("hex"), + matched: hit.matched, + }); + } + } + } + return findings; +} + +/** + * Run the fail-closed privacy gate over a proposed tracker change. + * + * Resolves the change (explicit `--diff` file, or the staged/working/untracked + * diff under the resolved pm tracker path), scans every added line with the + * credential, personal-data, and host-path rules, and applies the reviewed + * content-hash allowlist. Any input, Git, allowlist, or scanner failure throws + * {@link GateInputError}; a returned report therefore always describes a + * completed scan. + * + * @param input - pm root, optional diff file, optional allowlist file, injectable deps. + * @returns The machine-readable gate report. + */ +export function runTrackerGate(input: TrackerGateInput): GateReport { + const readFileSync = input.dependencies?.readFileSync ?? readTrackerText; + const runGit = input.dependencies?.runGit ?? runGitDefault; + + const pmDataDir = resolvePmDataDir(input.pmRoot); + let source: GateReport["source"]; + let files: ChangeFile[]; + try { + if (input.diffFile) { + source = "diff"; + const diffText = readFileSync(path.resolve(input.diffFile)); + files = parseUnifiedDiff(diffText); + } else { + source = "git"; + files = collectTrackerChange(pmDataDir, { runGit, readFileSync }); + } + for (const item of input.plannedItems ?? []) { + files.push({ filePath: "planned.jsonl", itemId: item.itemId, addedLines: [{ + text: JSON.stringify({ metadata: item.fields }), field: "unknown", + }] }); + } + } catch (err: unknown) { + if (err instanceof GateInputError) throw err; + // A collaborator error that already carries the gate's own message prefix + // (an injected runner raising a gate-scoped diagnostic) must pass through + // unwrapped instead of being re-labelled as an unreadable input. + if (err instanceof Error && err.message.startsWith("pm github gate:")) throw err; + throw new GateInputError( + "pm github gate: could not read the proposed tracker change; input is unavailable.", + ); + } + + let allowlist = new Map(); + let allowlistPath = ""; + if (input.allowlistFile) { + allowlistPath = input.allowlistFile; + allowlist = readGateAllowlist(path.resolve(input.allowlistFile), true, readFileSync); + } else { + const toplevel = runGit(pmDataDir, ["rev-parse", "--show-toplevel"]); + if (toplevel.ok) { + allowlistPath = path.join(toplevel.stdout.trim(), GATE_ALLOWLIST_FILENAME); + allowlist = readGateAllowlist(allowlistPath, false, readFileSync); + } + } + + // The scan phase below only ever throws GateInputError (malformed history + // events); every other operation is pure regex/Map/array work that cannot + // throw, so no defensive wrap is needed and a thrown GateInputError already + // carries the user-facing message. + const findings: GateFinding[] = []; + let allowlisted = 0; + let addedLines = 0; + for (const change of files) { + addedLines += change.addedLines.length; + for (const raw of scanChangeFile(change)) { + if (allowlist.has(raw.hash)) { + allowlisted++; + continue; + } + findings.push({ rule: raw.rule, item_id: raw.item_id, field: raw.field, hash: raw.hash }); + } + } + return { + verdict: findings.length > 0 ? "fail" : "pass", + source, + scanned_files: files.length, + added_lines: addedLines, + findings, + allowlisted, + allowlist_path: allowlistPath ? (input.allowlistFile ? "(explicit allowlist)" : GATE_ALLOWLIST_FILENAME) : "", + }; +} + +/** + * Render a gate report as human-readable lines. + * + * Every line names only the rule, item, field, and short hash prefix — the + * matched content never appears, so gate output is safe to paste anywhere. The + * same lines feed the command's stderr and the import failure message, so the + * two surfaces cannot drift apart. + * + * @param report - The gate report to render. + * @returns One summary line, then one line per finding. + */ +export function formatGateReport(report: GateReport): string[] { + const summary = + `pm github gate: ${report.verdict.toUpperCase()} — ${report.findings.length} finding(s), ` + + `${report.allowlisted} allowlisted, ${report.scanned_files} file(s), ${report.added_lines} added line(s)`; + const lines = [summary]; + for (const finding of report.findings) { + lines.push( + ` ${finding.item_id || "(no item)"} · ${finding.field || "(unknown field)"} · ` + + `${finding.rule} · sha256:${finding.hash.slice(0, 12)}…`, + ); + } + return lines; +} diff --git a/index.ts b/index.ts index 6a4a6d88..22fd735c 100644 --- a/index.ts +++ b/index.ts @@ -2,6 +2,7 @@ // // Capabilities (see manifest.json): // commands — `pm gh-issues import` (legacy) + `pm github sync` + +// `pm github gate` (fail-closed pre-push privacy gate) + // `pm github project list|fields|import|sync` (Projects v2) // importers — `pm github import ` (idempotent native import) // exporters — `pm github export` (render pm items as a GitHub-issues payload) @@ -46,6 +47,18 @@ import { } from "@unbrained/pm-cli/sdk"; import { collectNewOrderingCycleWarnings as sdkCollectNewOrderingCycleWarnings } from "@unbrained/pm-cli/sdk/graph"; +import { + type TrackerGateInput, + type GateReport, + type GateVerdict, + GateInputError, + formatGateReport, + runTrackerGate, + resolvePmDataDir, +} from "./gate.ts"; + +export { resolvePmDataDir } from "./gate.ts"; + import { type ProjectItem, type ProjectItemContent, @@ -58,6 +71,7 @@ import { parseProjectItemTag, parseProjectRef, parseStatusMap, + parseAssignmentMap, projectItemTag, } from "./projects.ts"; @@ -167,6 +181,10 @@ export interface ImportOptions { atomic: boolean; /** When true, run the `--link-deps` dependency-edge second pass after import. */ linkDeps: boolean; + /** When true, verify plan completeness + provenance idempotency before the + * write, and run the fail-closed tracker privacy gate after it, failing the + * import (non-zero) on any credential, personal-data, or host-path finding. */ + gate: boolean; } type CommitItemMutations = ( @@ -246,19 +264,19 @@ export interface FetchResult { export function resolveGitHubToken(): string | undefined { const envToken = process.env.GITHUB_TOKEN || process.env.GH_TOKEN; if (envToken && envToken.trim()) return envToken.trim(); - try { - const result = spawnSync("gh", ["auth", "token"], { encoding: "utf-8" }); - if (result.status === 0) { - const token = result.stdout.trim(); - if (token) return token; - } - } catch { - // gh not installed — fall back to unauthenticated requests. + // spawnSync never throws for a missing executable: it reports ENOENT through + // `result.error`/`status: null` (verified against the Node subprocess + // contract), and the arguments here are fixed and valid, so there is no + // throwable failure left — the status check below is the whole boundary. + const result = spawnSync("gh", ["auth", "token"], { encoding: "utf-8", env: { ...process.env } }); + if (result.status === 0) { + const token = result.stdout.trim(); + if (token) return token; } return undefined; } -const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); +const sleep = (ms: number) => new Promise((r) => { setTimeout(r, ms); }); /** * Decide whether the Authorization token may be forwarded across a redirect. @@ -346,7 +364,8 @@ function requestOnce( payload?: string, redirectsLeft = 5, ): Promise { - return new Promise((resolve, reject) => { + let deadline: ReturnType | undefined; + return new Promise((resolve, reject) => { const headers: Record = { "User-Agent": "pm-github", Accept: "application/vnd.github+json", @@ -363,7 +382,7 @@ function requestOnce( const target = new URL(url); const transport = target.protocol === "http:" ? http : https; const req = transport.request(target, { method, headers }, (res) => { - const status = res.statusCode ?? 0; + const status = res.statusCode!; // Node guarantees an Integer statusCode on every ClientResponse. if (status >= 300 && status < 400 && res.headers.location) { // Drain the redirect response so the socket is returned to the pool. res.resume(); @@ -400,12 +419,18 @@ function requestOnce( }); }); req.on("error", reject); - req.setTimeout(30000, () => { - req.destroy(new Error("request timed out after 30s")); - }); + // A wall-clock deadline also bounds redirects and stalled response bodies. + // Bun's ClientRequest socket timeout can fail to fire on a silent server. + deadline = setTimeout(() => { + const error = new Error("request timed out after 30s"); + // Reject before destroy: Bun can synchronously emit response end during + // destruction, which must never turn a truncated timed-out body into success. + reject(error); + req.destroy(error); + }, 30000); if (payload) req.write(payload); req.end(); - }); + }).finally(() => clearTimeout(deadline)); } /** @@ -518,7 +543,22 @@ export function formatRateLimit(info: RateLimitInfo): string | undefined { // Decide whether a failed HTTP response is worth retrying: 429, any 5xx, or a // 403 that is actually a primary/secondary rate-limit wall (remaining=0). -function isRetryableStatus(status: number, headers: Record): boolean { +// +/** + * Decide whether a failed HTTP response is worth retrying. + * + * Retries 429, any 5xx, and a 403 that is actually a primary/secondary + * rate-limit wall (remaining=0 or a Retry-After). Mixed-case header keys are + * honored defensively: Node's HTTP client lowercases response names, but the + * shared headers record also serves boundary tests that pass other shapes. + * + * @internal Exported only so the HTTP-boundary tests can drive the real + * decision. `stripInternal` keeps it out of the published `.d.ts`. + * @param status - The HTTP status code of the failed response. + * @param headers - The response headers to read the rate-limit hints from. + * @returns True when the request should be retried with backoff. + */ +export function isRetryableStatus(status: number, headers: Record): boolean { if (status === 429) return true; if (status >= 500) return true; // Secondary/primary rate limit surfaces as 403 with remaining=0. @@ -610,7 +650,7 @@ export function optionString(options: Record, ...keys: string[] // Whether an option key was explicitly provided (even if empty/falsey). export function optionProvided(options: Record, ...keys: string[]): boolean { - return keys.some((k) => Object.prototype.hasOwnProperty.call(options, k)); + return keys.some((k) => Object.hasOwn(options, k)); } // Parse a `--since` value into an ISO timestamp the GitHub `since` query param @@ -651,17 +691,7 @@ export function parseLabelMap( ): Map | undefined { const lookup = keys.length > 0 ? keys : ["label-map", "labelMap"]; const raw = optionCsv(options, ...lookup); - if (raw.length === 0) return undefined; - const map = new Map(); - for (const entry of raw) { - const eq = entry.indexOf("="); - if (eq <= 0) continue; // need a non-empty "from" before the '=' - const from = entry.slice(0, eq).trim(); - const to = entry.slice(eq + 1).trim(); - if (!from || !to) continue; - map.set(from, to); - } - return map.size > 0 ? map : undefined; + return parseAssignmentMap(raw); } // Apply a label translation table to a list of labels. Labels with a mapping @@ -1123,7 +1153,7 @@ export function readPmItems( const result = spawnSync( command, args, - { encoding: "utf-8", maxBuffer }, + { encoding: "utf-8", maxBuffer, env: { ...process.env } }, ); // A buffer overrun kills the child with status null and no stderr, so name the // real cause instead of reporting an unexplained failure. @@ -1210,6 +1240,11 @@ export function indexByProvenance(items: PmItem[]): Map { return index; } +/** Render thrown values consistently across subprocess, SDK, and HTTP boundaries. */ +function errorMessage(error: unknown): string { + return error instanceof Error ? error.message : String(error); +} + // --------------------------------------------------------------------------- // Atomic GitHub issue import (pm-cli >= 2026.7.20 commitItemMutations) // --------------------------------------------------------------------------- @@ -1292,18 +1327,22 @@ async function resolveAtomicSdkFunctions(opts: AtomicImportOptions): Promise<{ normalizeItemId: NormalizeItemId; readSettings: ReadSettings; }> { - const needsSdk = !opts.commitItemMutations || !opts.normalizeItemId || !opts.readSettings; return { + // Each `??` fallback runs only when the injected option is absent, so the + // SDK function is always the value handed onward; a separate `needsSdk ?` + // branch inside the fallback can never select anything else (when every + // option is injected the fallbacks do not run at all), so the SDK binding + // is passed directly. commitItemMutations: opts.commitItemMutations ?? assertSdkFunction( - needsSdk ? sdkCommitItemMutations : undefined, + sdkCommitItemMutations, "commitItemMutations", ), normalizeItemId: opts.normalizeItemId ?? assertSdkFunction( - needsSdk ? sdkNormalizeItemId : undefined, + sdkNormalizeItemId, "normalizeItemId", ), readSettings: opts.readSettings ?? assertSdkFunction( - needsSdk ? sdkReadSettings : undefined, + sdkReadSettings, "readSettings", ), }; @@ -1512,7 +1551,7 @@ export async function importGithubAtomic( itemIds, }; } catch (err: unknown) { - const msg = err instanceof Error ? err.message : String(err); + const msg = errorMessage(err); if (err instanceof AggregateError || /compensation failed/i.test(msg)) { throw new CommandError( `Atomic GitHub import failed and compensation was incomplete. The tracker may contain partially applied state; retry the same import to resume transaction ${transactionId}, then inspect its durable journal if recovery still fails. Underlying error: ${msg}`, @@ -1594,9 +1633,9 @@ export async function fetchComments(issue: GhIssue, repo: string, token?: string try { page = JSON.parse(body); } catch { - break; + throw new CommandError("GitHub comments response is not valid JSON."); } - if (!Array.isArray(page)) break; + if (!Array.isArray(page)) throw new CommandError("GitHub comments response must be an array."); comments.push(...(page as GhComment[])); nextUrl = parseNextLink(linkHeader); } @@ -1704,27 +1743,6 @@ export type ImportLockAcquisition = // The lock mechanism itself failed (fs error) — caller may proceed unlocked. | { status: "degraded" }; -/** - * Resolve the pm data dir from the `pmRoot` a command handler receives. - * - * The host may hand either the workspace root (the dir containing `.agents/pm`) - * or the data dir itself — the pm CLI accepts both for `--path`. This returns - * the dir that actually holds `settings.json` and `locks/`, defaulting to - * `pmRoot` unchanged when no nested `.agents/pm` exists. - * - * @param pmRoot - The path supplied by the extension host. - * @returns The resolved pm data directory. - */ -export function resolvePmDataDir(pmRoot: string): string { - const nested = path.join(pmRoot, ".agents", "pm"); - try { - if (fs.statSync(nested).isDirectory()) return nested; - } catch { - // Not the workspace-root form — assume pmRoot already is the data dir. - } - return pmRoot; -} - /** * Absolute lock file path for one item's comment-sync critical section. * @@ -1772,13 +1790,17 @@ function readImportLockPayload(lockPath: string): ImportLockPayload | undefined * * Signals the PID with signal 0 (no effect); `EPERM` means the process exists * but belongs to another user (still alive), while `ESRCH` means it is gone. - * Non-numeric or non-positive PIDs are treated as not-alive. + * Non-positive PIDs are treated as not-alive. The sole production caller already + * rejects non-numeric PIDs before calling, so this function does not re-check the type. * - * @param pid - The recorded owner PID (untyped at the call site). + * @internal Exported so the lock tests can drive the PID boundary directly. + * `stripInternal` keeps it out of the published declarations. + * @param pid - The recorded owner PID. Callers pass a positive integer; non-positive + * values are rejected here so a direct call cannot signal the process group. * @returns True when the PID names a live process. */ -function isLockOwnerAlive(pid: unknown): boolean { - if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0) return false; +export function isLockOwnerAlive(pid: number): boolean { + if (pid <= 0) return false; try { process.kill(pid, 0); return true; @@ -1946,7 +1968,7 @@ export async function acquireImportLock( lockPath = importCommentSyncLockPath(pmRoot, itemId); fs.mkdirSync(path.dirname(lockPath), { recursive: true }); } catch (err: unknown) { - const msg = err instanceof Error ? err.message : String(err); + const msg = errorMessage(err); console.error(`pm-github: comment-sync lock unavailable (${msg}) — proceeding without cross-process serialization`); return { status: "degraded" }; } @@ -2009,17 +2031,22 @@ export async function acquireImportLock( } const code = (err as NodeJS.ErrnoException)?.code; if (code !== "EEXIST") { - const msg = err instanceof Error ? err.message : String(err); + const msg = errorMessage(err); console.error(`pm-github: comment-sync lock failed (${msg}) — proceeding without cross-process serialization`); return { status: "degraded" }; } - // The lock exists. If it vanished before we could stat it, retry at once. + // The lock exists for open(O_EXCL) but cannot be stat'd (it vanished, or + // it is a broken symlink). Retry, but honor the wait budget: a persistent + // stat failure must not busy-spin the process until an external killer. let existing: ImportLockPayload | undefined; let mtimeMs = Number.NaN; try { mtimeMs = fs.statSync(lockPath).mtimeMs; existing = readImportLockPayload(lockPath); } catch { + const vanishedForMs = Date.now() - startedAt; + if (vanishedForMs >= waitMs) return { status: "contended" }; + await sleep(Math.min(backoffMs, Math.max(1, waitMs - vanishedForMs))); continue; } const staleReason = importLockStaleReason(lockPath, existing, mtimeMs, ttlMs); @@ -2157,30 +2184,32 @@ export function parseCreatedItemId(stdout: string): string | undefined { * * Idempotent: comments already present (matched by their marker id) are skipped, * so re-running import never duplicates. Each GitHub comment becomes one pm - * comment authored by the GitHub login. Failures are logged and never abort the - * import; a contended or unavailable lock degrades gracefully (see - * {@link acquireImportLock}). + * comment authored by the GitHub login. Failures are reported in the returned + * `failed` count and logged, never thrown: a contended or unavailable lock, an + * unreadable existing-comment collection, and each refused add all count every + * comment that did not land (see {@link acquireImportLock}). Gated callers turn + * a non-zero `failed` count into a fail-closed refusal; ungated ones continue. * * @param itemId - The pm item to append comments to. * @param comments - The GitHub comments to sync. * @param pmRoot - Workspace root or pm data dir. * @param issueNumber - The source issue number (for log prefixes). - * @returns How many comments were added and how many skipped as duplicates. + * @returns How many comments were added, skipped as duplicates, and not written. */ export async function syncGithubCommentsToAnnotations( itemId: string, comments: GhComment[], pmRoot: string, issueNumber: number, -): Promise<{ added: number; skipped: number }> { - if (comments.length === 0) return { added: 0, skipped: 0 }; +): Promise<{ added: number; skipped: number; failed: number }> { + if (comments.length === 0) return { added: 0, skipped: 0, failed: 0 }; const acquisition = await acquireImportLock(pmRoot, itemId); if (acquisition.status === "contended") { console.error( `#${issueNumber}: comment sync for ${itemId} skipped — another import holds the ` + `comment-sync lock; re-run import to pick up the comments`, ); - return { added: 0, skipped: 0 }; + return { added: 0, skipped: 0, failed: comments.length }; } const release = acquisition.status === "acquired" ? () => acquisition.lock.release() : () => {}; try { @@ -2189,13 +2218,14 @@ export async function syncGithubCommentsToAnnotations( const list: CommentsResult = await pmCommentsFn(itemId, {}, { pmRoot }); existing = list.comments; } catch (err: unknown) { - const msg = err instanceof Error ? err.message : String(err); + const msg = errorMessage(err); console.error(`#${issueNumber}: could not read existing comments for ${itemId} — ${msg}`); - return { added: 0, skipped: 0 }; + return { added: 0, skipped: 0, failed: comments.length }; } const synced = extractSyncedCommentIds(existing); let added = 0; let skipped = 0; + let failed = 0; for (const c of comments) { if (synced.has(c.id)) { skipped++; @@ -2206,11 +2236,12 @@ export async function syncGithubCommentsToAnnotations( await pmCommentsFn(itemId, { add: buildCommentText(c), author }, { pmRoot }); added++; } catch (err: unknown) { - const msg = err instanceof Error ? err.message : String(err); + const msg = errorMessage(err); console.error(`#${issueNumber}: comment ${c.id} sync failed — ${msg}`); + failed++; } } - return { added, skipped }; + return { added, skipped, failed }; } finally { release(); } @@ -2315,6 +2346,7 @@ export function parseImportOptions(options: Record): ImportOpti dryRun: optionEnabled(options, "dry-run", "dryRun"), atomic: optionEnabled(options, "atomic"), linkDeps: optionEnabled(options, "link-deps", "linkDeps"), + gate: optionEnabled(options, "gate"), }; } @@ -2323,7 +2355,7 @@ export function parseImportOptions(options: Record): ImportOpti // error-handling shape. function pmRun(args: string[]): { ok: boolean; stderr: string; stdout: string } { const maxBuffer = pmJsonMaxBuffer(); - const result = spawnSync("pm", args, { encoding: "utf-8", maxBuffer }); + const result = spawnSync("pm", args, { encoding: "utf-8", maxBuffer, env: { ...process.env } }); return { ok: result.status === 0, stderr: result.stderr || "", stdout: result.stdout || "" }; } @@ -2630,7 +2662,7 @@ export async function linkImportedDependencies( linked: 0, unresolved: 0, orderingCycleWarnings: [], - failures: [`dependency linking skipped: ${err instanceof Error ? err.message : String(err)}`], + failures: [`dependency linking skipped: ${errorMessage(err)}`], }; } @@ -2663,7 +2695,7 @@ export async function linkImportedDependencies( for (const w of collect(before, after, id)) warnings.add(w); } } catch (err: unknown) { - failures.push(`ordering-cycle advisory skipped: ${err instanceof Error ? err.message : String(err)}`); + failures.push(`ordering-cycle advisory skipped: ${errorMessage(err)}`); } } return { linked, unresolved, orderingCycleWarnings: [...warnings], failures }; @@ -2708,7 +2740,9 @@ async function prepareGithubImport( try { comments = await fetchIssueComments!(issue, repo, token); } catch (err: unknown) { - const msg = err instanceof Error ? err.message : String(err); + const msg = errorMessage(err); + if (opts.gate) throw new CommandError(`pm github gate: comments for issue #${issue.number} could not be read.`); + // The gated case throws above, so this diagnostic always names the plain path. console.error(`#${issue.number}: failed to fetch comments — ${msg}`); } } @@ -2735,14 +2769,219 @@ async function prepareGithubImport( }; } +/** Receipts from the gated import's completeness check. */ +export interface ImportPlanCompletenessReceipts { + /** Number of issues the fetch delivered after filtering. */ + readonly fetched: number; + /** Number of issues the plan prepares to import or update. */ + readonly planned: number; + /** Number of issues the plan explicitly skips (e.g. empty titles). */ + readonly skipped: number; +} + +/** Receipts from the gated import's idempotency check. */ +export interface ImportIdempotencyReceipts { + /** 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. */ + readonly matched_by_provenance: number; + /** Number of plan entries that will be born new; each must carry its tag. */ + readonly new_entries: number; +} + +/** + * Verify that a gated import's plan accounts for every fetched issue exactly once. + * + * An import that silently drops an issue (or plans one twice) would leave the + * tracker out of sync with GitHub while reporting success, and the sync PR + * would then publish an incomplete state. The check is fail-closed: every + * filtered issue number must appear in the prepared entries or the explicit + * skip list exactly once, nothing outside the fetched set may appear, and the + * counts must reconcile. A violation throws {@link CommandError} before any + * mutation happens. + * + * @param issues - The filtered issues the fetch delivered. + * @param prepared - The plan entries built from them. + * @param skippedNumbers - Issue numbers the plan explicitly skips. + * @returns The reconciled counts on success. + */ +export function verifyImportPlanCompleteness( + issues: readonly GhIssue[], + prepared: readonly PreparedGithubImport[], + skippedNumbers: readonly number[], +): ImportPlanCompletenessReceipts { + const fetched = new Set(issues.map((issue) => issue.number)); + const planned = new Set(); + let duplicates = 0; + for (const entry of prepared) { + if (planned.has(entry.issueNumber)) duplicates++; + planned.add(entry.issueNumber); + } + const skippedSet = new Set(skippedNumbers); + const unaccounted = [...fetched].filter( + (number) => !planned.has(number) && !skippedSet.has(number), + ); + const foreign = [...planned, ...skippedNumbers].filter( + (number) => !fetched.has(number), + ); + const countsReconcile = prepared.length + skippedNumbers.length === issues.length; + const overlapping = skippedNumbers.some((number) => planned.has(number)); + const duplicateFetch = fetched.size !== issues.length; + const duplicateSkip = skippedSet.size !== skippedNumbers.length; + if (overlapping || duplicateFetch || duplicateSkip || duplicates > 0 || unaccounted.length > 0 || foreign.length > 0 || !countsReconcile) { + throw new CommandError( + `pm github gate: import plan is incomplete — fetched ${issues.length}, planned ${prepared.length}, ` + + `skipped ${skippedNumbers.length}; duplicates ${duplicates}, unaccounted [${unaccounted.join(", ")}], ` + + `out-of-plan [${foreign.join(", ")}]. No item was written.`, + EXIT_CODE.GENERIC_FAILURE, + ); + } + return { fetched: issues.length, planned: prepared.length, skipped: skippedNumbers.length }; +} + +/** + * Verify that a gated import's plan keeps a second run a no-op via provenance tags. + * + * Idempotency is only real when every re-import lands on exactly one item: a + * corpus with two items sharing a `gh:owner/repo#N` tag updates one and lets the + * other drift, and a plan entry targeting a second entry's item would write the + * same item twice. The check fails closed on duplicate provenance tags in the + * existing corpus, on plan entries that would create a new item without its + * provenance tag, and on two plan entries resolving to the same existing item. + * + * @param existing - The complete existing pm item corpus. + * @param prepared - The plan entries about to be written. + * @param repo - The `owner/repo` being imported from. + * @returns The provenance idempotency receipts on success. + */ +export function verifyImportIdempotency( + existing: readonly PmItem[], + prepared: readonly PreparedGithubImport[], + repo: string, +): ImportIdempotencyReceipts { + const tagOwners = new Map(); + const duplicateTags = new Set(); + for (const item of existing) { + if (!item.id) continue; + for (const tag of item.tags ?? []) { + const parsed = parseProvenanceTag(tag); + if (!parsed) continue; + const key = `${parsed.repo}#${parsed.number}`; + const prior = tagOwners.get(key); + if (prior && prior !== item.id) duplicateTags.add(key); + else tagOwners.set(key, item.id); + } + } + const matchedIds: string[] = []; + let untaggedNewEntries = 0; + for (const entry of prepared) { + if (entry.match?.id) { + matchedIds.push(entry.match.id); + continue; + } + if (!entry.tags.includes(provenanceTag(repo, entry.issueNumber))) untaggedNewEntries++; + } + const conflictingTargets = matchedIds.length - new Set(matchedIds).size; + if (duplicateTags.size > 0 || untaggedNewEntries > 0 || conflictingTargets > 0) { + throw new CommandError( + `pm github gate: import plan is not idempotent — ${duplicateTags.size} duplicate provenance tag(s) ` + + `in the existing corpus, ${untaggedNewEntries} new entr(y/ies) without a provenance tag, ` + + `${conflictingTargets} conflicting target(s). No item was written.`, + EXIT_CODE.GENERIC_FAILURE, + ); + } + return { + provenance_indexed: tagOwners.size, + matched_by_provenance: matchedIds.length, + new_entries: prepared.length - matchedIds.length, + }; +} + +/** + * Verify the persisted corpus actually contains every issue in the written plan. + * + * A recovered SDK journal alone is insufficient evidence after local files are + * reset or removed. Require unique durable provenance before a caller may push. + * + * @param existing - Complete post-write item corpus. + * @param prepared - Every entry the verified plan required. + * @param repo - Source repository whose provenance must be present. + * @returns Number of durable issue identities verified. + */ +export function verifyImportedProvenance(existing: PmItem[], prepared: readonly PreparedGithubImport[], repo: string): number { + const index = indexByProvenance(existing); + const persisted = prepared.map(entry => ({ ...entry, match: index.get(`${repo.toLowerCase()}#${entry.issueNumber}`) })); + if (persisted.some(entry => !entry.match?.id)) throw new CommandError("pm github gate: persisted corpus does not account for the complete import plan. Nothing may be pushed."); + verifyImportIdempotency(existing, persisted, repo); + return persisted.length; +} + +/** Receipt of the fail-closed tracker privacy gate embedded in an import result. */ +export interface ImportGateReceipt { + /** The gate verdict for the proposed tracker change. */ + verdict: GateVerdict; + /** Number of files whose added lines were scanned. */ + scanned_files: number; + /** Number of findings that survived the allowlist (0 on pass). */ + findings: number; + /** Number of reviewed findings suppressed by the content-hash allowlist. */ + allowlisted: number; +} + +/** + * Execute the real tracker gate with the shared fail-closed command error boundary. + * + * @internal Exported to verify collaborator failures against real Git fixtures. + * @param input - Tracker root and optional gate input dependencies. + * @returns A completed gate report; unexpected errors are redacted. + */ +export function runCommandTrackerGate(input: TrackerGateInput): GateReport { + try { + return runTrackerGate(input); + } catch (error: unknown) { + throw new CommandError(error instanceof GateInputError ? error.message : "pm github gate: scanner error; scan did not complete.", EXIT_CODE.GENERIC_FAILURE); + } +} + +/** + * Run the post-write tracker privacy gate and turn its outcome into a receipt. + * + * Runs the real gate over the proposed tracker change the import just wrote, + * emits the human-readable report to stderr, and throws {@link CommandError} + * with the findings (rule + item + field + hash — never the matched content) + * when the verdict is fail, so a caller higher in the pipeline (a sync workflow) + * never reaches its commit/push step on a gated failure. Gate input failures + * (not a Git work tree, unreadable change, malformed allowlist) fail identically. + * + * @param pmRoot - Workspace root or pm data dir the import wrote to. + * @returns The pass receipt, embedded into the import result. + */ +function gateImportWrites(pmRoot: string): ImportGateReceipt { + const report = runCommandTrackerGate({ pmRoot }); + const lines = formatGateReport(report); + console.error(lines.join("\n")); + if (report.verdict === "fail") { + throw new CommandError(JSON.stringify({ ...report, error: "pm github gate: FAIL" }), EXIT_CODE.GENERIC_FAILURE); + } + return { + verdict: report.verdict, + scanned_files: report.scanned_files, + findings: report.findings.length, + allowlisted: report.allowlisted, + }; +} + /** * Run the full GitHub issue import flow. * * Idempotent: items already linked (by provenance tag) to a fetched issue are * UPDATEd; new issues are created. Honors `--atomic` (one crash-resumable * transaction) versus the per-item `pm` mutation path, optional `--link-deps`, - * and `--dry-run`. Returns a structured result and throws {@link CommandError} - * (with a semantic exit code) on failure. + * and `--dry-run`. With `--gate` the plan is verified (completeness + provenance + * idempotency) BEFORE any mutation, and the proposed tracker change is scanned + * by the fail-closed privacy gate AFTER it — a gated failure exits non-zero so + * a caller never commits or pushes the change. Returns a structured result and + * throws {@link CommandError} (with a semantic exit code) on failure. * * @param repoArg - The `owner/repo` to import from. * @param pmRoot - Workspace root or pm data dir. @@ -2775,7 +3014,7 @@ export async function runImport( try { fetched = await (dependencies.fetchIssues ?? fetchAllIssues)(repo, opts, token); } catch (err: unknown) { - const msg = err instanceof Error ? err.message : String(err); + const msg = errorMessage(err); const hint = !token && /HTTP 403/.test(msg) ? " — set GITHUB_TOKEN/GH_TOKEN or run `gh auth login` to raise the rate limit (60→5000/hr) and reach private repos" : ""; @@ -2787,6 +3026,9 @@ export async function runImport( if (filtered.length === 0) { console.error("No issues found."); + // Even a no-op gated import must prove the workspace is gateable (a Git work + // tree the scanner can read), so the empty-change gate scan runs here too. + const emptyGateReceipt = opts.gate && !opts.dryRun ? gateImportWrites(pmRoot) : undefined; if (opts.atomic && opts.dryRun) { return { dryRun: true, @@ -2796,7 +3038,7 @@ export async function runImport( atomic: true, }; } - return { imported: 0, updated: 0, skipped: 0 }; + return { imported: 0, updated: 0, skipped: 0, ...(emptyGateReceipt ? { gate: emptyGateReceipt } : {}) }; } console.error(`Found ${filtered.length} issue(s).`); @@ -2806,14 +3048,29 @@ export async function runImport( // reported "would import N, skip 0" where the real run performs updates for // already-linked issues. A preview that overstates creates reads as "this will // duplicate my whole tracker" and is the one thing --dry-run exists to rule out. - const existing = indexByProvenance((dependencies.readItems ?? readPmItems)(pmRoot)); + const existingItems = (dependencies.readItems ?? readPmItems)(pmRoot); + const existing = indexByProvenance(existingItems); let imported = 0; let updated = 0; let skipped = 0; - - if (opts.atomic) { - const prepared: PreparedGithubImport[] = []; + // 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 + // (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. + let gatePlan: readonly PreparedGithubImport[] | undefined; + let gateCompleteness: ImportPlanCompletenessReceipts | undefined; + let gateIdempotency: ImportIdempotencyReceipts | undefined; + let gatePlanByNumber: Map | undefined; + if (opts.gate || opts.atomic) { + const planned: PreparedGithubImport[] = []; + const skippedNumbers: number[] = []; for (const issue of filtered) { const entry = await prepareGithubImport( issue, @@ -2824,11 +3081,46 @@ export async function runImport( dependencies.fetchIssueComments, ); if (!entry) { - skipped++; + skippedNumbers.push(issue.number); continue; } - prepared.push(entry); + planned.push(entry); + } + if (opts.gate) { + gateCompleteness = verifyImportPlanCompleteness(filtered, planned, skippedNumbers); + gateIdempotency = verifyImportIdempotency( + existingItems, + planned, + repo, + ); + } + gatePlan = planned; + gatePlanByNumber = new Map(planned.map((entry) => [entry.issueNumber, entry])); + skipped = skippedNumbers.length; + if (opts.gate && !opts.dryRun) { + const preview = runCommandTrackerGate({ pmRoot, plannedItems: planned.map(entry => ({ + itemId: entry.match?.id ?? `github-issue-${entry.issueNumber}`, + fields: { title: entry.title, description: entry.description, body: entry.body, tags: entry.tags, + assignee: entry.assignee, sprint: entry.milestone, + comments: entry.syncAnnotations ? entry.comments.map(comment => ({ + author: comment.user?.login, text: comment.body, created_at: comment.created_at, + })) : [] }, + })) }); + if (preview.verdict === "fail") throw new CommandError(JSON.stringify({ ...preview, error: "pm github gate: FAIL before write" })); } + 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.`, + ); + } + + const gatePlanFields = opts.gate ? { gate: { completeness: gateCompleteness, idempotency: gateIdempotency, scan: "post-write" } } : {}; + + if (opts.atomic) { + // The gated pipeline already prepared every entry (and counted its skips) + // during the pre-mutation plan phase; reuse that plan verbatim so the write + // phase cannot diverge from the verified plan. + const prepared: PreparedGithubImport[] = [...gatePlan!]; if (prepared.length === 0) { if (opts.dryRun) { @@ -2852,7 +3144,7 @@ export async function runImport( updated = prepared.length - imported; for (const entry of prepared) { const action = entry.match?.id ? "update" : "import"; - console.error(` [dry-run][atomic] #${entry.issueNumber} ${action}: ${entry.title} (${entry.status})`); + 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}.`, @@ -2870,22 +3162,32 @@ export async function runImport( wouldSkip: skipped, atomic: true, ...(opts.linkDeps ? { wouldLinkDependencyCandidates: countDependencyRefCandidates(repo, filtered) } : {}), + ...gatePlanFields, }; } const result = await (dependencies.commitAtomic ?? importGithubAtomic)(pmRoot, repo, prepared); + if (opts.gate) verifyImportedProvenance((dependencies.readItems ?? readPmItems)(pmRoot), prepared, repo); + // Planned native comments are part of the verified plan: a contended lock, + // a refused add, or a committed entry whose created id is missing from the + // post-commit routing map all leave planned comments unwritten, so they are + // counted and gated runs fail closed below instead of exiting clean. + let commentWriteFailures = 0; for (const entry of prepared) { if (!entry.syncAnnotations) continue; const itemId = result.itemIds.get(entry.issueNumber); if (itemId) { - await syncGithubCommentsToAnnotations( + commentWriteFailures += (await syncGithubCommentsToAnnotations( itemId, entry.comments, pmRoot, entry.issueNumber, - ); + )).failed; + } else { + commentWriteFailures += entry.comments.length; } } + failClosedOnPlanDivergence(opts.gate, skipped, skipped, commentWriteFailures); if (result.recovered) { console.error( `Atomic import recovered transaction ${result.transactionId} covering ${result.recoveredItems ?? prepared.length} item(s).`, @@ -2901,6 +3203,9 @@ export async function runImport( reportDepLink(atomicDepLink); // itemIds is an internal post-commit routing map for native comments. Maps // serialize as `{}` in JSON, so keep it out of the public command result. + // Gated pipeline, phase 2 — the writes are on disk but nothing is committed + // or pushed; scan the proposed tracker change now and fail closed. + const gateReceipt = opts.gate ? gateImportWrites(pmRoot) : undefined; return { transactionId: result.transactionId, recovered: result.recovered, @@ -2910,20 +3215,28 @@ export async function runImport( skipped, atomic: true, ...depLinkResultFields(atomicDepLink), + ...(gateReceipt ? { gate: gateReceipt } : {}), }; } for (const issue of filtered) { - const prepared = await prepareGithubImport( - issue, - repo, - opts, - token, - existing.get(`${repo.toLowerCase()}#${issue.number}`), - dependencies.fetchIssueComments, - ); + // 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, + ); if (!prepared) { - skipped++; + // A gated run counted its skips during the verified plan phase; only the + // ungated path counts them here. + if (!gatePlanByNumber) skipped++; continue; } @@ -2961,8 +3274,8 @@ export async function runImport( if (opts.dryRun) { const action = match?.id ? "update" : "import"; - const metadata = labels.length > 0 ? `${status}, ${labels.join(",")}` : status; - console.error(` [dry-run] #${issue.number} ${action}: ${title} (${metadata})`); + 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++; else imported++; continue; @@ -3006,7 +3319,7 @@ export async function runImport( } } if (syncAnnotations) { - await syncGithubCommentsToAnnotations(match.id, comments, pmRoot, issue.number); + commentWriteFailures += (await syncGithubCommentsToAnnotations(match.id, comments, pmRoot, issue.number)).failed; } updated++; continue; @@ -3060,9 +3373,13 @@ export async function runImport( } if (syncAnnotations) { if (createdId) { - await syncGithubCommentsToAnnotations(createdId, comments, pmRoot, issue.number); + commentWriteFailures += (await syncGithubCommentsToAnnotations(createdId, comments, pmRoot, issue.number)).failed; } else { + // Without the created id the planned comments cannot be written; the + // gated plan-divergence refusal below turns that into a fail-closed exit + // while ungated runs keep today's warn-and-continue behaviour. console.error(`#${issue.number}: could not parse created item id — comments not synced`); + commentWriteFailures += comments.length; } } imported++; @@ -3081,8 +3398,8 @@ export async function runImport( wouldImport: imported, wouldUpdate: updated, wouldSkip: skipped, - ...(opts.atomic ? { atomic: true } : {}), ...(opts.linkDeps ? { wouldLinkDependencyCandidates: countDependencyRefCandidates(repo, filtered) } : {}), + ...gatePlanFields, }; } @@ -3090,11 +3407,26 @@ export async function runImport( if (imported === 0 && updated === 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 + // (an update, close or reopen reconciliation) or a planned native comment + // that was not written leaves the tracker different from the verified plan, + // and provenance alone cannot see that, because the item still carries its + // tag. Fail closed before anything can be pushed. + failClosedOnPlanDivergence( + opts.gate, + gateCompleteness?.skipped ?? skipped, + skipped, + commentWriteFailures, + ); const depLink = opts.linkDeps ? await linkImportedDependencies(repo, filtered, pmRoot, dependencies) : undefined; reportDepLink(depLink); - return { imported, updated, skipped, ...depLinkResultFields(depLink) }; + // Gated pipeline, phase 2 — the writes are on disk but nothing is committed or + // pushed; scan the proposed tracker change now and fail closed. + if (opts.gate) verifyImportedProvenance((dependencies.readItems ?? readPmItems)(pmRoot), gatePlan!, repo); + const gateReceipt = opts.gate ? gateImportWrites(pmRoot) : undefined; + return { imported, updated, skipped, ...depLinkResultFields(depLink), ...(gateReceipt ? { gate: gateReceipt } : {}) }; } /** @@ -3131,6 +3463,37 @@ function depLinkResultFields( }; } +/** + * Fail a gated import when the writes diverged from the verified plan. + * + * A gated import is all-or-nothing in effect: a planned item write that failed + * (an update, close or reopen reconciliation), a planned native comment that + * was not written, or a comment collection left unreachable by an unparsable + * created id all leave the tracker different from the verified plan — and + * provenance alone cannot see that, because the item still carries its tag and + * missing content cannot leak anything into the post-write privacy scan. Both + * the non-atomic and the atomic import path funnel their divergence here and + * fail closed before anything can be pushed; ungated imports already warned. + * + * @param gate - Whether the import runs gated; ungated runs never throw here. + * @param plannedSkipped - Issues the verified plan already accounted as skipped. + * @param actualSkipped - Issues the write phase actually skipped. + * @param commentFailures - Planned native comments that were not written. + * @throws CommandError when any planned write or comment failed on a gated run. + */ +function failClosedOnPlanDivergence( + gate: boolean, + plannedSkipped: number, + actualSkipped: number, + commentFailures: number, +): void { + const failed = actualSkipped - plannedSkipped + commentFailures; + if (!gate || failed <= 0) return; + throw new CommandError( + `pm github gate: ${failed} planned write(s) failed; the tracker does not match the verified plan. Nothing may be pushed.`, + ); +} + // --------------------------------------------------------------------------- // Sync core — push pm status changes back to GitHub (close / reopen) // --------------------------------------------------------------------------- @@ -3158,6 +3521,20 @@ export interface SyncPlanEntry { to: "open" | "closed"; } +/** + * Require every requested item before a remote sync or export can begin. + * + * @param items - Complete local corpus. + * @param requested - Selected ids, or an empty list for the complete corpus. + * @returns Matched items in corpus order. + * @throws CommandError when any requested identity is absent. + */ +function requireRequestedItems(items: PmItem[], requested: string[]): PmItem[] { + const scoped = scopeItemsByIds(items, requested.length > 0 ? requested : undefined); + if (scoped.missing.length > 0) throw new CommandError(`--ids included unknown pm item id(s): ${scoped.missing.join(", ")}`, EXIT_CODE.NOT_FOUND); + return scoped.selected; +} + // Build the pm → GitHub issue sync plan: for each pm item linked to `repo`, emit // a create-or-update entry keyed by the issue's provenance tag. export function planSync(items: PmItem[], repo: string): SyncPlanEntry[] { @@ -3186,8 +3563,8 @@ export function planSync(items: PmItem[], repo: string): SyncPlanEntry[] { // Command handler for `pm github sync`: preview or apply the pm → GitHub issue // sync plan, scoped by --ids and honoring --dry-run / --apply. async function runSync(ctx: CommandHandlerContext) { - const options = ctx.options || {}; - const repo = optionString(options, "repo") || (ctx.args?.[0] as string | undefined); + const options = ctx.options; + const repo = optionString(options, "repo") || (ctx.args[0] as string | undefined); const dryRun = optionEnabled(options, "dry-run", "dryRun"); const idsProvided = optionProvided(options, "ids"); const scopedIds = optionCsv(options, "ids"); @@ -3215,15 +3592,8 @@ async function runSync(ctx: CommandHandlerContext) { ); } - const allItems = readPmItems(ctx.pm_root); - const scoped = scopeItemsByIds(allItems, scopedIds.length > 0 ? scopedIds : undefined); - if (scoped.missing.length > 0) { - throw new CommandError( - `--ids included unknown pm item id(s): ${scoped.missing.join(", ")}`, - EXIT_CODE.NOT_FOUND, - ); - } - const plan = planSync(scoped.selected, repo); + const selected = requireRequestedItems(readPmItems(ctx.pm_root), scopedIds); + const plan = planSync(selected, repo); if (plan.length === 0) { const scopeNote = scopedIds.length > 0 ? ` from --ids (${scopedIds.join(", ")})` : ""; @@ -3251,7 +3621,7 @@ async function runSync(ctx: CommandHandlerContext) { ); current = JSON.parse(body) as GhIssue; } catch (err: unknown) { - const msg = err instanceof Error ? err.message : String(err); + const msg = errorMessage(err); console.error(`#${entry.number}: could not read upstream state — ${msg}`); skipped++; continue; @@ -3275,7 +3645,7 @@ async function runSync(ctx: CommandHandlerContext) { console.error(`#${entry.number} "${entry.title}": ${current.state} → ${entry.to}`); synced++; } catch (err: unknown) { - const msg = err instanceof Error ? err.message : String(err); + const msg = errorMessage(err); console.error(`#${entry.number}: PATCH failed — ${msg}`); skipped++; } @@ -3450,7 +3820,7 @@ export async function applyExportPlan( } catch (err: unknown) { // Isolate the failure: record it and keep going so one bad item never // abandons the items that follow it (and that may already be writable). - const msg = err instanceof Error ? err.message : String(err); + const msg = errorMessage(err); const label = entry.action === "update" && entry.number !== undefined ? `#${entry.number}` : entry.id ?? `"${p.title}"`; @@ -3530,10 +3900,10 @@ export function applyOutcomeError( * duplicated; `--label-map` translates pm tags to GitHub labels. */ async function runExport(ctx: CommandHandlerContext) { - const options = ctx.options || {}; - const jsonMode = ctx.global?.json === true; + const options = ctx.options; + const jsonMode = ctx.global.json === true; const format = optionString(options, "format") || "json"; - const repo = optionString(options, "repo") || ctx.args?.[0]; + const repo = optionString(options, "repo") || ctx.args[0]; const apply = exportWillApply(options); const idsProvided = optionProvided(options, "ids"); const scopedIds = optionCsv(options, "ids"); @@ -3545,15 +3915,8 @@ async function runExport(ctx: CommandHandlerContext) { ); } - const allItems = readPmItems(ctx.pm_root); - const scoped = scopeItemsByIds(allItems, scopedIds.length > 0 ? scopedIds : undefined); - if (scoped.missing.length > 0) { - throw new CommandError( - `--ids included unknown pm item id(s): ${scoped.missing.join(", ")}`, - EXIT_CODE.NOT_FOUND, - ); - } - const plan = buildExportPlan(scoped.selected, repo, labelMap); + const selected = requireRequestedItems(readPmItems(ctx.pm_root), scopedIds); + const plan = buildExportPlan(selected, repo, labelMap); const creates = plan.filter((e) => e.action === "create").length; const updates = plan.filter((e) => e.action === "update").length; @@ -3578,7 +3941,7 @@ async function runExport(ctx: CommandHandlerContext) { console.error(JSON.stringify(plan, null, 2)); } const scopeNote = scopedIds.length > 0 - ? ` Scoped to ${scoped.selected.length} item(s) via --ids.` + ? ` Scoped to ${selected.length} item(s) via --ids.` : ""; const labelNote = labelMap && labelMap.size > 0 ? ` Label map applied (${labelMap.size} mapping(s)).` @@ -3710,7 +4073,7 @@ export function mapSearchHits( // (0, 1] so hits clear pm's default score threshold. hits.push({ id: item.id, - score: matchedNumbers.length > 0 ? rank / matchedNumbers.length : 1, + score: rank / matchedNumbers.length, matched_fields: [`github:${repoLc}#${number}`], }); rank--; @@ -3779,12 +4142,8 @@ export interface ValidateReport { // Detect whether the `gh` CLI is installed and runnable on PATH. function detectGhCli(): boolean { - try { - const r = spawnSync("gh", ["--version"], { encoding: "utf-8" }); - return r.status === 0; - } catch { - return false; - } + const r = spawnSync("gh", ["--version"], { encoding: "utf-8", env: { ...process.env } }); + return r.status === 0; } // Report which token source is active: `env` (GITHUB_TOKEN/GH_TOKEN), `gh` (gh @@ -3798,8 +4157,8 @@ function detectTokenSource(): "env" | "gh" | "none" { // Command handler for `pm github validate`: checks token source, gh CLI, rate // limit, and the reachable issue counts for the configured repo. async function runValidate(ctx: CommandHandlerContext): Promise { - const options = ctx.options || {}; - const repo = optionString(options, "repo") || (ctx.args?.[0] as string | undefined); + const options = ctx.options; + const repo = optionString(options, "repo") || (ctx.args[0] as string | undefined); const gh_cli = detectGhCli(); const token_source = detectTokenSource(); const token = resolveGitHubToken(); @@ -3829,7 +4188,7 @@ async function runValidate(ctx: CommandHandlerContext): Promise report.repo = repo; try { const res = await fetchJSON(`${githubApiBase()}/repos/${repo}`, token); - report.repo_accessible = res.status >= 200 && res.status < 300; + report.repo_accessible = true; report.repo_status = res.status; const rate = parseRateLimit(res.headers); if (rate.remaining !== undefined) report.rate_limit_remaining = rate.remaining; @@ -3845,16 +4204,13 @@ async function runValidate(ctx: CommandHandlerContext): Promise ".", ); } - if (report.repo_accessible) { - report.messages.push(`Repo ${repo} is accessible (HTTP ${res.status}).`); - } else { - report.ok = false; - report.messages.push(`Repo ${repo} returned HTTP ${res.status}.`); - } + // fetchJSON only resolves for 2xx; every other status throws into the catch + // below, so a returned response is accessible by construction. + report.messages.push(`Repo ${repo} is accessible (HTTP ${res.status}).`); } catch (err: unknown) { report.ok = false; report.repo_accessible = false; - report.messages.push(`Repo ${repo} check failed: ${err instanceof Error ? err.message : String(err)}`); + report.messages.push(`Repo ${repo} check failed: ${errorMessage(err)}`); } } } else { @@ -4205,7 +4561,7 @@ async function gqlAddDraft( token: string | undefined, ): Promise { const q = `mutation($p:ID!,$t:String!,$b:String){ addProjectV2DraftIssue(input:{projectId:$p,title:$t,body:$b}){ projectItem{ id } } }`; - const d = await githubGraphQL(token, q, { p: projectId, t: title, b: body ?? "" }); + const d = await githubGraphQL(token, q, { p: projectId, t: title, b: body }); const id = d.addProjectV2DraftIssue?.projectItem?.id; if (!id) throw new CommandError("addProjectV2DraftIssue returned no item id."); return id; @@ -4373,8 +4729,8 @@ export async function listOwnerProjectsV2Nodes( * @param ctx - The command-handler context. */ async function runProjectList(ctx: CommandHandlerContext) { - const options = ctx.options || {}; - const owner = optionString(options, "owner") || (ctx.args?.[0] as string | undefined); + const options = ctx.options; + const owner = optionString(options, "owner") || (ctx.args[0] as string | undefined); if (!owner) { throw new CommandError( "Usage: pm github project list (a GitHub user or org login)", @@ -4390,7 +4746,7 @@ async function runProjectList(ctx: CommandHandlerContext) { closed: !!n.closed, description: n.shortDescription ?? undefined, })); - if (ctx.global?.json !== true) { + if (ctx.global.json !== true) { if (projects.length === 0) { console.error(`No Projects v2 found for ${owner} (or none accessible with the resolved token).`); } else { @@ -4403,6 +4759,22 @@ async function runProjectList(ctx: CommandHandlerContext) { return { owner, projects }; } +/** + * Require a valid owner/number board reference before a project command can run. + * + * Resolves positional and --project forms identically for fields, import, and + * sync; a malformed reference exits before any GraphQL request or mutation. + * + * @param ctx - The host command context. + * @param usage - Command-specific recovery instruction. + * @returns The parsed project identity. + */ +function requireProjectRef(ctx: CommandHandlerContext, usage: string): ProjectRef { + const ref = parseProjectRef(optionString(ctx.options, "project") || ctx.args[0]); + if (!ref) throw new CommandError(usage, EXIT_CODE.USAGE); + return ref; +} + // --- project fields -------------------------------------------------------- /** @@ -4414,14 +4786,8 @@ async function runProjectList(ctx: CommandHandlerContext) { * @param ctx - The command-handler context. */ async function runProjectFields(ctx: CommandHandlerContext) { - const options = ctx.options || {}; - const ref = parseProjectRef(optionString(options, "project") || (ctx.args?.[0] as string | undefined)); - if (!ref) { - throw new CommandError( - "Usage: pm github project fields (e.g. pm github project fields unbraind/5)", - EXIT_CODE.USAGE, - ); - } + const options = ctx.options; + const ref = requireProjectRef(ctx, "Usage: pm github project fields (e.g. pm github project fields unbraind/5)"); const token = resolveGitHubToken(); const meta = await resolveProject(ref, token); const q = ` @@ -4438,7 +4804,7 @@ async function runProjectFields(ctx: CommandHandlerContext) { type: f.dataType ?? f.__typename, options: Array.isArray(f.options) ? f.options.map((o) => o.name) : undefined, })); - if (ctx.global?.json !== true) { + if (ctx.global.json !== true) { console.error(`Project ${ref.owner}/${ref.number} — ${meta.title} (${meta.ownerType})`); console.error(` ${meta.url}`); console.error(` Status field: ${meta.statusField ? meta.statusField.options.map((o) => o.name).join(" | ") : "(none — pushes cannot set status)"}`); @@ -4462,14 +4828,8 @@ async function runProjectFields(ctx: CommandHandlerContext) { * @param ctx - The command-handler context. */ async function runProjectImport(ctx: CommandHandlerContext) { - const options = ctx.options || {}; - const ref = parseProjectRef(optionString(options, "project") || (ctx.args?.[0] as string | undefined)); - if (!ref) { - throw new CommandError( - "Usage: pm github project import [--dry-run] [--status-map pm=Option,...] [--type ]", - EXIT_CODE.USAGE, - ); - } + const options = ctx.options; + const ref = requireProjectRef(ctx, "Usage: pm github project import [--dry-run] [--status-map pm=Option,...] [--type ]"); const dryRun = optionEnabled(options, "dry-run", "dryRun"); const itemType = optionString(options, "type") || "Task"; const statusMap = parseStatusMap(optionCsv(options, "status-map", "statusMap")); @@ -4591,26 +4951,24 @@ async function applyPushEntry( let itemId = entry.itemId; if (entry.action === "add-draft") { const pm = pmById.get(entry.pmId); - itemId = await gqlAddDraft(meta.id, entry.title, pm?.body || pm?.description, token); + itemId = await gqlAddDraft(meta.id, entry.title, pm!.body || pm!.description!, token); } else if (entry.action === "add-issue") { - if (!entry.issueRepo || typeof entry.issueNumber !== "number") { - return { changed: false, error: "add-issue entry missing issue coordinates" }; - } - const contentId = await gqlResolveIssueNodeId(entry.issueRepo, entry.issueNumber, token); + // The private executor receives only buildProjectPushPlan entries. Its + // add-issue arm always sets both coordinates from a parsed provenance tag. + const contentId = await gqlResolveIssueNodeId(entry.issueRepo!, entry.issueNumber!, token); if (!contentId) return { changed: false, error: `could not resolve node id for ${entry.issueRepo}#${entry.issueNumber}` }; itemId = await gqlAddIssue(meta.id, contentId, token); } - if (!itemId) return { changed: false, error: "no project item id to act on" }; if (entry.targetOptionId && meta.statusField) { - await gqlSetStatus(meta.id, itemId, meta.statusField.id, entry.targetOptionId, token); + await gqlSetStatus(meta.id, itemId!, meta.statusField.id, entry.targetOptionId, token); } // Ensure the pm item carries the project provenance tag so future syncs are // idempotent (never strips existing tags; only adds the missing one). const pm = pmById.get(entry.pmId); if (pm?.id) { - const tag = projectItemTag(ref, itemId); - const existingTags = pm.tags ?? []; + const tag = projectItemTag(ref, itemId!); + const existingTags = pm.tags!; if (!existingTags.includes(tag)) { const upd = pmRun([ "--path", pmRoot, "update", pm.id, @@ -4622,7 +4980,7 @@ async function applyPushEntry( } return { changed: true }; } catch (err: unknown) { - return { changed: false, error: err instanceof Error ? err.message : String(err) }; + return { changed: false, error: errorMessage(err) }; } } @@ -4703,14 +5061,8 @@ interface PullPlanEntryLike { * @param ctx - The command-handler context. */ async function runProjectSync(ctx: CommandHandlerContext) { - const options = ctx.options || {}; - const ref = parseProjectRef(optionString(options, "project") || (ctx.args?.[0] as string | undefined)); - if (!ref) { - throw new CommandError( - "Usage: pm github project sync [--push|--pull] [--apply] [--ids pm-1,..] [--status-map pm=Option,..] [--no-add-missing] [--prefer pm|github]", - EXIT_CODE.USAGE, - ); - } + const options = ctx.options; + const ref = requireProjectRef(ctx, "Usage: pm github project sync [--push|--pull] [--apply] [--ids pm-1,..] [--status-map pm=Option,..] [--no-add-missing] [--prefer pm|github]"); const wantPush = optionEnabled(options, "push"); const wantPull = optionEnabled(options, "pull"); @@ -4788,7 +5140,7 @@ async function runProjectSync(ctx: CommandHandlerContext) { console.error(`[dry-run] pull (project ${ref.owner}/${ref.number} → pm):`); for (const e of pullActionable) console.error(` ${e.pmId} "${e.title}": ${e.fromStatus} → ${e.toStatus}`); for (const s of pullPlan.statusSkipped) { - console.error(` [skip] item ${s.itemId}: board status "${s.optionName ?? "(none)"}" maps to no pm status`); + console.error(` [skip] item ${s.itemId}: board status "${s.optionName}" maps to no pm status`); } if (pullActionable.length === 0) console.error(" (nothing to pull)"); } @@ -4805,7 +5157,7 @@ async function runProjectSync(ctx: CommandHandlerContext) { let pushFailed = 0; if (applyPush && pushPlan) { for (const e of pushActionable) { - if (prefer === "github" && e.action === "set-status" && pullItemIds.has(e.itemId ?? "")) { + if (prefer === "github" && e.action === "set-status" && pullItemIds.has(e.itemId!)) { continue; // pull wins for this linked item } const r = await applyPushEntry(e, meta, ref, pmById, ctx.pm_root, token); @@ -4880,6 +5232,7 @@ const IMPORT_FLAGS = [ { long: "--comments-mode", value_name: "body|annotations|both", description: "How to persist fetched GitHub comments: `body` (default, embed in item body), `annotations` (sync to the pm item's native comments collection), or `both`. `annotations`/`both` are idempotent on re-import (dedupe by GitHub comment id)" }, { long: "--atomic", description: "Commit the complete import as one workspace-writer-locked, crash-resumable transaction (pm-cli >=2026.7.20); compensate applied mutations on failure and report incomplete compensation" }, { long: "--link-deps", description: "After import, map dependency references in issue bodies (`Blocked by #N`, `Depends on owner/repo#N`, `Blocks #N`) to pm dependency edges between the linked items. Idempotent; skips self- and unresolved references; ordering cycles are reported (via the SDK ordering-cycle advisory), not rejected" }, + { long: "--gate", description: "Fail-closed privacy gate: verify plan completeness and provenance-tag idempotency before the write, then scan the proposed tracker change for credentials, personal data, and host paths after it; any finding (or unreadable scan input) fails the import so nothing is ever pushed" }, { long: "--dry-run", description: "Preview without writing" }, { long: "--type", value_name: "type", description: "Override pm item type (default: Issue)" }, ]; @@ -4901,6 +5254,11 @@ const SYNC_FLAGS = [ { long: "--dry-run", description: "Preview the close/reopen plan without mutating GitHub" }, ]; +const GATE_FLAGS = [ + { long: "--diff", value_name: "file", description: "Scan an explicit unified diff file instead of the staged/working tracker change (fail closed when unreadable)" }, + { long: "--allowlist", value_name: "file", description: "Explicit content-hash allowlist of reviewed false positives (default: `/.pm-github-gate-allowlist.json`; a missing EXPLICIT file fails, a missing default is an empty allowlist)" }, +]; + const VALIDATE_FLAGS = [ { long: "--repo", value_name: "owner/repo", description: "Also check this repo is accessible with the resolved token" }, ]; @@ -4982,7 +5340,7 @@ export default defineExtension({ "github project sync", ], run: (ctx: PreflightOverrideContext) => { - if (isMutatingGithubCommand(ctx.command, ctx.options || {})) { + if (isMutatingGithubCommand(ctx.command, ctx.options)) { if (!resolveGitHubToken()) { console.error( "[pm-github preflight] this github command mutates remote state but no GitHub " + @@ -4998,7 +5356,7 @@ export default defineExtension({ // importer — `pm github import ` (idempotent native pipeline) // ----------------------------------------------------------------------- api.registerImporter("github", async (ctx: ImportExportContext) => { - return runImport(ctx.args?.[0], ctx.pm_root, parseImportOptions(ctx.options || {})); + return runImport(ctx.args[0], ctx.pm_root, parseImportOptions(ctx.options)); }, { description: "Fetch GitHub issues from a repo and create/update pm items (idempotent " + @@ -5069,7 +5427,7 @@ export default defineExtension({ api.registerSearchProvider({ name: "github", async query(qctx: SearchProviderQueryContext) { - const repo = resolveSearchRepo(qctx.options || {}); + const repo = resolveSearchRepo(qctx.options); if (!repo) return []; const token = resolveGitHubToken(); let matchedNumbers: number[]; @@ -5104,12 +5462,12 @@ export default defineExtension({ if (!ctx.ok) return; if (ctx.command !== "close" && ctx.command !== "update") return; // Only nudge for items that are actually linked to GitHub. - const id = ctx.args?.[0]; + const id = ctx.args[0]; if (!id || !ctx.pm_root) return; const res = spawnSync( "pm", ["--path", ctx.pm_root, "--json", "show", id], - { encoding: "utf-8", maxBuffer: pmJsonMaxBuffer() }, + { encoding: "utf-8", maxBuffer: pmJsonMaxBuffer(), env: { ...process.env } }, ); if (res.status !== 0) return; let repo: string | undefined; @@ -5196,6 +5554,52 @@ export default defineExtension({ }, }); + // ----------------------------------------------------------------------- + // command — `pm github gate` (fail-closed privacy gate over the proposed + // tracker change). Runs BEFORE any push in a sync pipeline; a fail verdict + // or an unreadable scan input exits non-zero so nothing downstream commits + // or pushes. Findings name rule + item + field + content hash — never the + // matched content. + // ----------------------------------------------------------------------- + api.registerCommand({ + name: "github gate", + description: + "Fail-closed privacy gate over the proposed tracker change (the staged/working/untracked diff under the resolved pm tracker path, or an explicit --diff file). Fails on credentials (GitHub/npm/AWS/Slack/OpenAI/Anthropic tokens, generic bearers, private-key blocks, high-entropy secret assignments), personal data (non-noreply emails, phone numbers), and host paths (absolute local filesystem paths, home-directory usernames). Reviewed false positives are allowlisted by content hash. Unreadable input or a scanner error fails. Read-only; never mutates anything. Use --json for machine output.", + intent: "verify a proposed tracker change before any public branch push", + examples: [ + "pm github gate", + "pm github gate --json", + "pm github gate --diff pm-sync.patch", + "pm github gate --allowlist .pm-github-gate-allowlist.json", + ], + flags: GATE_FLAGS, + failure_hints: [ + "Run inside the repository holding the pm tracker, or pass --diff for an explicit unified diff.", + "A finding names the rule, item, field, and the sha256 of the matched content; allowlist that hash in .pm-github-gate-allowlist.json with a written reason after review.", + "The gate scans only added lines — removed content cannot publish anything new.", + "pm github import --gate composes this gate into an import: plan completeness and provenance idempotency are verified before the write, the scan runs after it.", + ], + async run(ctx: CommandHandlerContext) { + const diffFile = optionString(ctx.options, "diff"); + const allowlistFile = optionString(ctx.options, "allowlist"); + const report = runCommandTrackerGate({ + pmRoot: ctx.pm_root, + ...(diffFile ? { diffFile } : {}), + ...(allowlistFile ? { allowlistFile } : {}), + }); + if (ctx.global.json !== true) { + for (const line of formatGateReport(report)) console.error(line); + } + if (report.verdict === "fail") { + throw new CommandError( + JSON.stringify({ ...report, error: "pm github gate: FAIL" }), + EXIT_CODE.GENERIC_FAILURE, + ); + } + return report; + }, + }); + // ----------------------------------------------------------------------- // command — `pm github validate` (diagnostics: gh/token/repo reachability) // ----------------------------------------------------------------------- @@ -5219,7 +5623,7 @@ export default defineExtension({ ], async run(ctx: CommandHandlerContext) { const report = await runValidate(ctx); - const jsonMode = ctx.global?.json === true; + const jsonMode = ctx.global.json === true; if (!jsonMode) { for (const line of report.messages) console.error(line); } diff --git a/package-lock.json b/package-lock.json index af85a4de..d927e220 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,8 +9,13 @@ "version": "2026.10.4", "license": "MIT", "devDependencies": { + "@babel/eslint-parser": "8.0.5", + "@babel/plugin-syntax-typescript": "8.0.3", "@types/node": "26.6.4", "@unbrained/pm-cli": "2026.10.4", + "c8": "12.0.0", + "eslint": "10.12.0", + "jscpd": "5.4.0", "pm-changelog": "2026.10.4", "pm-ops": "2026.10.4", "typescript": "7.0.2" @@ -22,726 +27,2334 @@ "@unbrained/pm-cli": ">=2026.8.20" } }, - "node_modules/@isaacs/fs-minipass": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", - "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "node_modules/@babel/code-frame": { + "version": "8.0.6", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-8.0.6.tgz", + "integrity": "sha512-uMHhvmDmwFiLuiozaoRYTZHqMFuu3fem/X9JY78lohAuLoh8LSGGC8JqUzo2cVuBsBxkjMZNk56CE7ock8Xxkw==", "dev": true, - "license": "ISC", + "license": "MIT", + "peer": true, "dependencies": { - "minipass": "^7.0.4" + "@babel/helper-validator-identifier": "^8.0.6", + "js-tokens": "^10.0.0" }, "engines": { - "node": ">=18.0.0" + "node": "^22.18.0 || >=24.11.0" } }, - "node_modules/@opentelemetry/api": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz", - "integrity": "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==", + "node_modules/@babel/compat-data": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-8.0.5.tgz", + "integrity": "sha512-YLsYoQMvL8l8WrGpN3Zj7O1wK5LEBN+cQtux7BcuHyxIXve724XG+zuJ1n3U1cUweRtTzQOA4IHbuQw3N34SZw==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", + "peer": true, "engines": { - "node": ">=8.0.0" + "node": "^22.18.0 || >=24.11.0" } }, - "node_modules/@opentelemetry/api-logs": { - "version": "0.220.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.220.0.tgz", - "integrity": "sha512-CmVa4ImJ+ynfrPMNaAXHET6Bhb44SwzmfyVJFq9ni2jgXJR/l7C6gfVFddNmHP+ZOkP9cf4f9DBe68qVLTHc9w==", + "node_modules/@babel/core": { + "version": "8.0.6", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-8.0.6.tgz", + "integrity": "sha512-5zwYt1V4ji3mXKKRIAkohSJ29WepxNwxQi+7y7P5AcVPh1dcxnZBU2cfxHnPwS+u9+lZnXPsv30TQaF2xg+Tfg==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", + "peer": true, "dependencies": { - "@opentelemetry/api": "^1.3.0" + "@babel/code-frame": "^8.0.6", + "@babel/generator": "^8.0.6", + "@babel/helper-compilation-targets": "^8.0.6", + "@babel/helpers": "^8.0.5", + "@babel/parser": "^8.0.6", + "@babel/template": "^8.0.0", + "@babel/traverse": "^8.0.6", + "@babel/types": "^8.0.6", + "@types/gensync": "^1.0.5", + "convert-source-map": "^2.0.0", + "empathic": "^2.0.1", + "gensync": "^1.0.0-beta.2", + "import-meta-resolve": "^4.2.0", + "json5": "^2.2.3", + "obug": "^2.1.1", + "verkit": "^0.3.2" }, "engines": { - "node": ">=8.0.0" + "node": "^22.18.0 || >=24.11.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" } }, - "node_modules/@opentelemetry/core": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.11.0.tgz", - "integrity": "sha512-7YP44XH0tV6+Mb54x2YGf84i7yi+31MBZlE8JwvozkxyTvXbSp10X7cI7YE49ChJ3shMJoBmCJF3+1QFBJctGA==", + "node_modules/@babel/eslint-parser": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-8.0.5.tgz", + "integrity": "sha512-q4c+FpoJ/FKeiLnZ9Os3vWnSlZw3dBmlTUx6vuITRqbd8yn4hgUB0/OOJ+gi5lt+mOx/0IW2rXINR98JJllfag==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "@opentelemetry/semantic-conventions": "^1.29.0" + "eslint-scope": "^9.1.0", + "eslint-visitor-keys": "^5.0.0", + "verkit": "^0.3.2" }, "engines": { - "node": "^18.19.0 || >=20.6.0" + "node": "^22.18.0 || >=24.11.0" }, "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" + "@babel/core": "^8.0.0", + "eslint": "^9.0.0 || ^10.0.0" } }, - "node_modules/@opentelemetry/instrumentation": { - "version": "0.220.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.220.0.tgz", - "integrity": "sha512-xQx3E2WxP1mDvKzxLxX+CTCtNLa560YJZ3087qYHerl2YmiKpv7AH+dAy7vmx+eVrZ5BwhfWUAVoKOoxCNHcpw==", + "node_modules/@babel/generator": { + "version": "8.0.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-8.0.6.tgz", + "integrity": "sha512-/pcanjdRsonCexCuxgGD8fKnBLLzk2i9uM4kmcmGyyev2R5YDs+rafMYZ89RvaGY1mW8T7C5au0TyYaEeHBGng==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", + "peer": true, "dependencies": { - "@opentelemetry/api-logs": "0.220.0", - "import-in-the-middle": "^3.0.0", - "require-in-the-middle": "^8.0.0" + "@babel/parser": "^8.0.6", + "@babel/types": "^8.0.6", + "@jridgewell/gen-mapping": "0.4.0-beta.0", + "@jridgewell/trace-mapping": "^0.3.31", + "@types/jsesc": "^2.5.0", + "jsesc": "^3.0.2" }, "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" + "node": "^22.18.0 || >=24.11.0" } }, - "node_modules/@opentelemetry/resources": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.11.0.tgz", - "integrity": "sha512-Ie7+8q8MDF4FAEQCKVMTx3ReUvxiIAgIiiW3c9JdmP8+HMcDy20puT+AHjexnExgnbvBxjQ9fjkFDWrikJ2jQA==", + "node_modules/@babel/helper-compilation-targets": { + "version": "8.0.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-8.0.6.tgz", + "integrity": "sha512-HTrtZ4+iA0+kzdbCgvdSk1U4pEl3BvpGnlUbualCGNbrVhhQvhgBzurTUmqpWyvyHbWYWaIfdx2mOlL9LJjsew==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", + "peer": true, "dependencies": { - "@opentelemetry/core": "2.11.0", - "@opentelemetry/semantic-conventions": "^1.29.0" + "@babel/compat-data": "^8.0.5", + "@babel/helper-validator-option": "^8.0.0", + "browserslist": "^4.24.0", + "flru": "1.0.2", + "verkit": "^0.3.2" }, "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" + "node": "^22.18.0 || >=24.11.0" } }, - "node_modules/@opentelemetry/sdk-trace": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace/-/sdk-trace-2.11.0.tgz", - "integrity": "sha512-fFnTqGm8/G73GQVnxYi7LXa1ZVYEUvgL6XI1LpvV0bPC7WQ/ZGgKxCSl8FnlZBKto9JHHEFTO6s6CUpvvtwFrA==", + "node_modules/@babel/helper-globals": { + "version": "8.0.6", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-8.0.6.tgz", + "integrity": "sha512-OdnA/N52LhzFpeUuVz5YGuSWjc7TPHiFmVYZbPlSgExocO2GwbxVpSni9MVs5wwW+wZ4kPuwBEBN9pIPDYz1mA==", "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.11.0", - "@opentelemetry/resources": "2.11.0", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, + "license": "MIT", + "peer": true, "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" + "node": "^22.18.0 || >=24.11.0" } }, - "node_modules/@opentelemetry/sdk-trace-base": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.11.0.tgz", - "integrity": "sha512-H19x/TX/LZdqiYOjM7fqtSxwlplC5pgelavqbQdHbhdq0q/AI/TGkM2dfGuuynTXmJPeF2HoZVoPDu+TGoW78A==", + "node_modules/@babel/helper-plugin-utils": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-8.0.1.tgz", + "integrity": "sha512-3PKFgjTyPlhFhorfP+SjKQxLViIL++zWjFOO4hGriYU+Bsm983DxEM1JmDRJVWXV0O9npu+xXRqz7Pbd3mh70g==", "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.11.0", - "@opentelemetry/resources": "2.11.0", - "@opentelemetry/sdk-trace": "2.11.0", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, + "license": "MIT", "engines": { - "node": "^18.19.0 || >=20.6.0" + "node": "^22.18.0 || >=24.11.0" }, "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" + "@babel/core": "^8.0.0" } }, - "node_modules/@opentelemetry/semantic-conventions": { - "version": "1.43.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.43.0.tgz", - "integrity": "sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg==", + "node_modules/@babel/helper-string-parser": { + "version": "8.0.6", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-8.0.6.tgz", + "integrity": "sha512-+6HYEBnsDRZHJtUTAEfMQd0zMDqdFHS7C+5vAsEZqBUr3vEMWOWlHJ0Jz1gS8jJIlCvLsuA6ivmeoqzH936YcA==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", + "peer": true, "engines": { - "node": ">=14" + "node": "^22.18.0 || >=24.11.0" } }, - "node_modules/@sentry/conventions": { - "version": "0.16.0", - "resolved": "https://registry.npmjs.org/@sentry/conventions/-/conventions-0.16.0.tgz", - "integrity": "sha512-fO9PLmHdVURcSPUpWCItWAtgKiMwGdJHbovoSEyLplX5sxs2ugvI4CBPTrkkgqhObnZOD0CnWBKDzSVQYBKEyQ==", + "node_modules/@babel/helper-validator-identifier": { + "version": "8.0.6", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-8.0.6.tgz", + "integrity": "sha512-HYx0YmuchRxRSpnjvcNffnPS8DCauKNmnumIuHG3jnQAVewRA7kfJGwF13ayEpSZs7s4R/oBH4eppuXcAQd5JQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { - "node": ">=14" + "node": "^22.18.0 || >=24.11.0" } }, - "node_modules/@sentry/core": { - "version": "10.75.1", - "resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.75.1.tgz", - "integrity": "sha512-+/+UanewqPK+oJvDQIHWKLUqnsa0iymEThOPCaFBA8ulbQh9k8lsz8V+NtJwP4G3ncclfzp15tzqVwn6iugV4Q==", + "node_modules/@babel/helper-validator-option": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-8.0.0.tgz", + "integrity": "sha512-U4Dybxh4WESWHt5XhBeExi4DrY0/DNK1aHpQbsrQXCUbFHuMweT0TpLEWKvaraV2Y6fS+ZXunsZ8zIuZIgvF2Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/helpers": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-8.0.5.tgz", + "integrity": "sha512-fQtPOXjYOYv85PIdwotp2TJGVYOycX0PQq+l844fFAxOULtBy8BVF35GyeueX0r4KvDthqPH5xAI1clQPk/2uA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { - "@sentry/conventions": "^0.16.0" + "@babel/template": "^8.0.0", + "@babel/types": "^8.0.5" }, "engines": { - "node": ">=18" + "node": "^22.18.0 || >=24.11.0" } }, - "node_modules/@sentry/node": { - "version": "10.75.1", - "resolved": "https://registry.npmjs.org/@sentry/node/-/node-10.75.1.tgz", - "integrity": "sha512-qiWGwh0KiBoq4vEl0FbxFKw0xnOKxuKCaEVdtwMZLAGw1ef77NOCdie7e/Fmcsk9c++Ck4D8NmIjQm6BidRVSQ==", + "node_modules/@babel/parser": { + "version": "8.0.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-8.0.6.tgz", + "integrity": "sha512-LpGDIYJAzc3Y3PT9x+FxBrGYu2PlWxLmyN7SwPTzaX0LSwEmFJBsRnPBqmaXF2r1v+Zhz6lNiEJ+7yKtM5+Ugg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { - "@opentelemetry/api": "^1.9.1", - "@opentelemetry/instrumentation": "^0.220.0", - "@opentelemetry/sdk-trace-base": "^2.9.0", - "@sentry/conventions": "^0.16.0", - "@sentry/core": "10.75.1", - "@sentry/node-core": "10.75.1", - "@sentry/opentelemetry": "10.75.1", - "@sentry/server-utils": "10.75.1", - "import-in-the-middle": "^3.0.0" + "@babel/types": "^8.0.6" + }, + "bin": { + "parser": "bin/babel-parser.js" }, "engines": { - "node": ">=18" + "node": "^22.18.0 || >=24.11.0" } }, - "node_modules/@sentry/node-core": { - "version": "10.75.1", - "resolved": "https://registry.npmjs.org/@sentry/node-core/-/node-core-10.75.1.tgz", - "integrity": "sha512-HR0Iy7oNFOP26cwaRi33ZZnvlUSMFzXbZoRgYtBAkqUHDNQA6LWG1N1OoIdKNRJzDkGu/zefbM50Amqgyy1u1Q==", + "node_modules/@babel/plugin-syntax-typescript": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-8.0.3.tgz", + "integrity": "sha512-jmTPwps7oSQSZaV1SxkQ3C12UWyufGysGc5OzDpZzvPAIX4mO7dJT3hoqkWVrSImvkcMiknir1iLN1SNV/CZzg==", "dev": true, "license": "MIT", "dependencies": { - "@sentry/conventions": "^0.16.0", - "@sentry/core": "10.75.1", - "@sentry/opentelemetry": "10.75.1", - "import-in-the-middle": "^3.0.0" + "@babel/helper-plugin-utils": "^8.0.1" }, "engines": { - "node": ">=18" + "node": "^22.18.0 || >=24.11.0" }, "peerDependencies": { - "@opentelemetry/api": "^1.9.0", - "@opentelemetry/core": "^1.30.1 || ^2.1.0", - "@opentelemetry/exporter-trace-otlp-http": ">=0.57.0 <1", - "@opentelemetry/instrumentation": ">=0.57.1 <1", - "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.1.0" - }, - "peerDependenciesMeta": { - "@opentelemetry/api": { - "optional": true - }, - "@opentelemetry/core": { - "optional": true - }, - "@opentelemetry/exporter-trace-otlp-http": { - "optional": true - }, - "@opentelemetry/instrumentation": { - "optional": true - }, - "@opentelemetry/sdk-trace-base": { - "optional": true - } + "@babel/core": "^8.0.0" } }, - "node_modules/@sentry/opentelemetry": { - "version": "10.75.1", - "resolved": "https://registry.npmjs.org/@sentry/opentelemetry/-/opentelemetry-10.75.1.tgz", - "integrity": "sha512-leRKyFkEgV47Qo2wKCMEIYxPgnp67C+wYFg0GPreX5owRde0l18F5BEmsYUU57WAjBKBDo2idrrEEQaU15uH8g==", + "node_modules/@babel/template": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-8.0.0.tgz", + "integrity": "sha512-eAD0QW/AlbamBbw0FeGiwasbCVPq5ncW0HNVyLP3B9czqLyh4gvw+5JTSNt6le9+ziAU7mqDZsKTHf3jTb4chQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { - "@sentry/conventions": "^0.16.0", - "@sentry/core": "10.75.1" + "@babel/code-frame": "^8.0.0", + "@babel/parser": "^8.0.0", + "@babel/types": "^8.0.0" }, "engines": { - "node": ">=18" + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/traverse": { + "version": "8.0.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-8.0.6.tgz", + "integrity": "sha512-ilYV+qLStGzSKLipJlETdvdtpK3rAuwfCmCoGKv15MU4EoeIuLkespSNoNLFGOPSciEHx9TLWtN5mv0fz5w5mQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^8.0.6", + "@babel/generator": "^8.0.6", + "@babel/helper-globals": "^8.0.6", + "@babel/parser": "^8.0.6", + "@babel/template": "^8.0.0", + "@babel/types": "^8.0.6", + "obug": "^2.1.1" }, - "peerDependencies": { - "@opentelemetry/api": "^1.9.0", - "@opentelemetry/core": "^1.30.1 || ^2.1.0", - "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.1.0" + "engines": { + "node": "^22.18.0 || >=24.11.0" } }, - "node_modules/@sentry/server-utils": { - "version": "10.75.1", - "resolved": "https://registry.npmjs.org/@sentry/server-utils/-/server-utils-10.75.1.tgz", - "integrity": "sha512-HuA/bCtthA5x/AjYD5WUbG6CBcYJb0WAWKNgrgNkUWtll6eMqG3+7LO4PBcGRbZtlg1OFgClJm4iQFgRN7+ZZg==", + "node_modules/@babel/types": { + "version": "8.0.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-8.0.6.tgz", + "integrity": "sha512-c+xgSWboV2pdwXP67BUWDVRHi2BO5Q3KwxyFVs7taVTY+fsK47/L51ZXQoV9rozHJDNHvJ4v4WEsI/IlxV2l2A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { - "@sentry/conventions": "^0.16.0", - "@sentry/core": "10.75.1" + "@babel/helper-string-parser": "^8.0.6", + "@babel/helper-validator-identifier": "^8.0.6" }, + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", "engines": { "node": ">=18" } }, - "node_modules/@toon-format/toon": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@toon-format/toon/-/toon-4.1.1.tgz", - "integrity": "sha512-SGCkS7IjVpwRmGPgnY8ENKpAf0EdAnZDOQkvFW0d2cgOpdn9FEFl7sTgryESyypXrWr0YajHGpwsAUX4zw9ZvA==", + "node_modules/@cacheable/memory": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.2.0.tgz", + "integrity": "sha512-CTLKqLItRCEixEAewD3/j9DB3/o96gpTPD4eJ1v+DGOlxZRZncRQkGYqqnAGCscYd6RNeXfGeiuCphsPtqyIfQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "@cacheable/utils": "^2.5.0", + "@keyv/bigmap": "^1.3.1", + "hookified": "^1.15.1", + "keyv": "^5.6.0" + } }, - "node_modules/@types/node": { - "version": "26.6.4", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.4.tgz", - "integrity": "sha512-ldVPDCzj7fsaGZrLB0NuHuTvJcsNasysBAqMolr/cgxrLd1xbqxIr3XJiPnHHJUCxj5sNF1vnRj9aWnrVh5Jcg==", + "node_modules/@cacheable/utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.5.0.tgz", + "integrity": "sha512-buipgOVDkkPXNR5+xBpDw7Zk2n1EvU7qBJCNUcL7rhQ//kfpOXPAvQ511Os0vpLYJ1pZnvudNytkQt2hst3wqA==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~8.9.0" + "hashery": "^1.5.1", + "keyv": "^5.6.0" } }, - "node_modules/@typescript/typescript-aix-ppc64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz", - "integrity": "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==", - "cpu": [ - "ppc64" - ], + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "aix" - ], + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, "engines": { - "node": ">=16.20.0" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, - "node_modules/@typescript/typescript-darwin-arm64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.0.2.tgz", - "integrity": "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==", - "cpu": [ - "arm64" - ], + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], "engines": { - "node": ">=16.20.0" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/@typescript/typescript-darwin-x64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.0.2.tgz", - "integrity": "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==", - "cpu": [ - "x64" - ], + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], + "license": "MIT", "engines": { - "node": ">=16.20.0" + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, - "node_modules/@typescript/typescript-freebsd-arm64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-arm64/-/typescript-freebsd-arm64-7.0.2.tgz", - "integrity": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==", - "cpu": [ - "arm64" - ], + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", "dev": true, "license": "Apache-2.0", - "optional": true, - "os": [ - "freebsd" - ], + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, "engines": { - "node": ">=16.20.0" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, - "node_modules/@typescript/typescript-freebsd-x64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-x64/-/typescript-freebsd-x64-7.0.2.tgz", - "integrity": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==", - "cpu": [ - "x64" - ], + "node_modules/@eslint/config-helpers": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.7.0.tgz", + "integrity": "sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==", "dev": true, "license": "Apache-2.0", - "optional": true, - "os": [ - "freebsd" - ], + "dependencies": { + "@eslint/core": "^1.2.1" + }, "engines": { - "node": ">=16.20.0" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, - "node_modules/@typescript/typescript-linux-arm": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.0.2.tgz", - "integrity": "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==", - "cpu": [ - "arm" - ], + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", "dev": true, "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@types/json-schema": "^7.0.15" + }, "engines": { - "node": ">=16.20.0" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, - "node_modules/@typescript/typescript-linux-arm64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.0.2.tgz", - "integrity": "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==", - "cpu": [ - "arm64" - ], + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", "dev": true, "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": ">=16.20.0" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, - "node_modules/@typescript/typescript-linux-loong64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-loong64/-/typescript-linux-loong64-7.0.2.tgz", - "integrity": "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==", - "cpu": [ - "loong64" - ], + "node_modules/@eslint/plugin-kit": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.3.tgz", + "integrity": "sha512-IkO+/KEUvwbVpiURZg+P7zF74z5Jxe0UgJxVni+RtoHQ6IZieXaO02kmadomap/q+l6bc/jdPGGqTjhuZnuz1Q==", "dev": true, "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, "engines": { - "node": ">=16.20.0" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, - "node_modules/@typescript/typescript-linux-mips64el": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-mips64el/-/typescript-linux-mips64el-7.0.2.tgz", - "integrity": "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==", - "cpu": [ - "mips64el" - ], + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", "dev": true, "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", + "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.4.0-beta.0", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.4.0-beta.0.tgz", + "integrity": "sha512-JdGNkbE4GlNPYQhM0L95fBQr7ctLZJ276QXQLTad4t1oSdnnCI3fDq9DW3BqYAWv8Wc3+HS+4Gsii1oPMCfz1w==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.6.0-beta.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@keyv/bigmap": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@keyv/bigmap/-/bigmap-1.3.1.tgz", + "integrity": "sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "hashery": "^1.4.0", + "hookified": "^1.15.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "keyv": "^5.6.0" + } + }, + "node_modules/@keyv/serialize": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz", + "integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@opentelemetry/api": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz", + "integrity": "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/api-logs": { + "version": "0.220.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.220.0.tgz", + "integrity": "sha512-CmVa4ImJ+ynfrPMNaAXHET6Bhb44SwzmfyVJFq9ni2jgXJR/l7C6gfVFddNmHP+ZOkP9cf4f9DBe68qVLTHc9w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/core": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.11.0.tgz", + "integrity": "sha512-7YP44XH0tV6+Mb54x2YGf84i7yi+31MBZlE8JwvozkxyTvXbSp10X7cI7YE49ChJ3shMJoBmCJF3+1QFBJctGA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/instrumentation": { + "version": "0.220.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.220.0.tgz", + "integrity": "sha512-xQx3E2WxP1mDvKzxLxX+CTCtNLa560YJZ3087qYHerl2YmiKpv7AH+dAy7vmx+eVrZ5BwhfWUAVoKOoxCNHcpw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.220.0", + "import-in-the-middle": "^3.0.0", + "require-in-the-middle": "^8.0.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/resources": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.11.0.tgz", + "integrity": "sha512-Ie7+8q8MDF4FAEQCKVMTx3ReUvxiIAgIiiW3c9JdmP8+HMcDy20puT+AHjexnExgnbvBxjQ9fjkFDWrikJ2jQA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.11.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace/-/sdk-trace-2.11.0.tgz", + "integrity": "sha512-fFnTqGm8/G73GQVnxYi7LXa1ZVYEUvgL6XI1LpvV0bPC7WQ/ZGgKxCSl8FnlZBKto9JHHEFTO6s6CUpvvtwFrA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.11.0", + "@opentelemetry/resources": "2.11.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-base": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.11.0.tgz", + "integrity": "sha512-H19x/TX/LZdqiYOjM7fqtSxwlplC5pgelavqbQdHbhdq0q/AI/TGkM2dfGuuynTXmJPeF2HoZVoPDu+TGoW78A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.11.0", + "@opentelemetry/resources": "2.11.0", + "@opentelemetry/sdk-trace": "2.11.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/semantic-conventions": { + "version": "1.43.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.43.0.tgz", + "integrity": "sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@sentry/conventions": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@sentry/conventions/-/conventions-0.16.0.tgz", + "integrity": "sha512-fO9PLmHdVURcSPUpWCItWAtgKiMwGdJHbovoSEyLplX5sxs2ugvI4CBPTrkkgqhObnZOD0CnWBKDzSVQYBKEyQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/@sentry/core": { + "version": "10.75.1", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.75.1.tgz", + "integrity": "sha512-+/+UanewqPK+oJvDQIHWKLUqnsa0iymEThOPCaFBA8ulbQh9k8lsz8V+NtJwP4G3ncclfzp15tzqVwn6iugV4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sentry/conventions": "^0.16.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/node": { + "version": "10.75.1", + "resolved": "https://registry.npmjs.org/@sentry/node/-/node-10.75.1.tgz", + "integrity": "sha512-qiWGwh0KiBoq4vEl0FbxFKw0xnOKxuKCaEVdtwMZLAGw1ef77NOCdie7e/Fmcsk9c++Ck4D8NmIjQm6BidRVSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@opentelemetry/api": "^1.9.1", + "@opentelemetry/instrumentation": "^0.220.0", + "@opentelemetry/sdk-trace-base": "^2.9.0", + "@sentry/conventions": "^0.16.0", + "@sentry/core": "10.75.1", + "@sentry/node-core": "10.75.1", + "@sentry/opentelemetry": "10.75.1", + "@sentry/server-utils": "10.75.1", + "import-in-the-middle": "^3.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/node-core": { + "version": "10.75.1", + "resolved": "https://registry.npmjs.org/@sentry/node-core/-/node-core-10.75.1.tgz", + "integrity": "sha512-HR0Iy7oNFOP26cwaRi33ZZnvlUSMFzXbZoRgYtBAkqUHDNQA6LWG1N1OoIdKNRJzDkGu/zefbM50Amqgyy1u1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sentry/conventions": "^0.16.0", + "@sentry/core": "10.75.1", + "@sentry/opentelemetry": "10.75.1", + "import-in-the-middle": "^3.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/core": "^1.30.1 || ^2.1.0", + "@opentelemetry/exporter-trace-otlp-http": ">=0.57.0 <1", + "@opentelemetry/instrumentation": ">=0.57.1 <1", + "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.1.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@opentelemetry/core": { + "optional": true + }, + "@opentelemetry/exporter-trace-otlp-http": { + "optional": true + }, + "@opentelemetry/instrumentation": { + "optional": true + }, + "@opentelemetry/sdk-trace-base": { + "optional": true + } + } + }, + "node_modules/@sentry/opentelemetry": { + "version": "10.75.1", + "resolved": "https://registry.npmjs.org/@sentry/opentelemetry/-/opentelemetry-10.75.1.tgz", + "integrity": "sha512-leRKyFkEgV47Qo2wKCMEIYxPgnp67C+wYFg0GPreX5owRde0l18F5BEmsYUU57WAjBKBDo2idrrEEQaU15uH8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sentry/conventions": "^0.16.0", + "@sentry/core": "10.75.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/core": "^1.30.1 || ^2.1.0", + "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.1.0" + } + }, + "node_modules/@sentry/server-utils": { + "version": "10.75.1", + "resolved": "https://registry.npmjs.org/@sentry/server-utils/-/server-utils-10.75.1.tgz", + "integrity": "sha512-HuA/bCtthA5x/AjYD5WUbG6CBcYJb0WAWKNgrgNkUWtll6eMqG3+7LO4PBcGRbZtlg1OFgClJm4iQFgRN7+ZZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sentry/conventions": "^0.16.0", + "@sentry/core": "10.75.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@toon-format/toon": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@toon-format/toon/-/toon-4.1.1.tgz", + "integrity": "sha512-SGCkS7IjVpwRmGPgnY8ENKpAf0EdAnZDOQkvFW0d2cgOpdn9FEFl7sTgryESyypXrWr0YajHGpwsAUX4zw9ZvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/gensync": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/gensync/-/gensync-1.0.5.tgz", + "integrity": "sha512-MbsRCT7mTikHwKZ0X+LVUTLRrZZRLipTuXEO9qOYO+zmjMVk81axyClMROf6uoPD9MRVu46bx8zoR0Ad9q3NAg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/jsesc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@types/jsesc/-/jsesc-2.5.1.tgz", + "integrity": "sha512-9VN+6yxLOPLOav+7PwjZbxiID2bVaeq0ED4qSQmdQTdjnXJSaCVKTR58t15oqH1H5t8Ng2ZX1SabJVoN9Q34bw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "26.6.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.4.tgz", + "integrity": "sha512-ldVPDCzj7fsaGZrLB0NuHuTvJcsNasysBAqMolr/cgxrLd1xbqxIr3XJiPnHHJUCxj5sNF1vnRj9aWnrVh5Jcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~8.9.0" + } + }, + "node_modules/@typescript/typescript-aix-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz", + "integrity": "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-darwin-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.0.2.tgz", + "integrity": "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-darwin-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.0.2.tgz", + "integrity": "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-freebsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-arm64/-/typescript-freebsd-arm64-7.0.2.tgz", + "integrity": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-freebsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-x64/-/typescript-freebsd-x64-7.0.2.tgz", + "integrity": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-arm": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.0.2.tgz", + "integrity": "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.0.2.tgz", + "integrity": "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-loong64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-loong64/-/typescript-linux-loong64-7.0.2.tgz", + "integrity": "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-mips64el": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-mips64el/-/typescript-linux-mips64el-7.0.2.tgz", + "integrity": "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-ppc64/-/typescript-linux-ppc64-7.0.2.tgz", + "integrity": "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-riscv64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-riscv64/-/typescript-linux-riscv64-7.0.2.tgz", + "integrity": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-s390x": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-s390x/-/typescript-linux-s390x-7.0.2.tgz", + "integrity": "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.0.2.tgz", + "integrity": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-arm64/-/typescript-netbsd-arm64-7.0.2.tgz", + "integrity": "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-x64/-/typescript-netbsd-x64-7.0.2.tgz", + "integrity": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-arm64/-/typescript-openbsd-arm64-7.0.2.tgz", + "integrity": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-x64/-/typescript-openbsd-x64-7.0.2.tgz", + "integrity": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-sunos-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-sunos-x64/-/typescript-sunos-x64-7.0.2.tgz", + "integrity": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.0.2.tgz", + "integrity": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-x64/-/typescript-win32-x64-7.0.2.tgz", + "integrity": "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@unbrained/pm-cli": { + "version": "2026.10.4", + "resolved": "https://registry.npmjs.org/@unbrained/pm-cli/-/pm-cli-2026.10.4.tgz", + "integrity": "sha512-NZLdiUSUKIRcFiMj9lG0L5UubA9G33fq5WzB1JzJjTTlYxYCVjrFFOCV2rtxYTaYTCzX9o/6V/A4fDx6N3mm/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sentry/node": "10.75.1", + "@toon-format/toon": "^4.1.1", + "@types/node": ">=22", + "commander": "^15.0.0", + "fast-json-patch": "^3.1.1", + "npm-package-arg": "^13.0.2", + "tar": "7.5.22", + "tinyglobby": "^0.2.17", + "yaml": "^2.9.1" + }, + "bin": { + "pm": "dist/cli.js", + "pm-cli": "dist/cli.js", + "pm-mcp": "dist/mcp/server.js", + "pm-mcp-http": "dist/mcp/http-server.js" + }, + "engines": { + "node": ">=22.18.0" + } + }, + "node_modules/acorn": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.4.0.tgz", + "integrity": "sha512-KzTVk2tCWAHtYrvvvaP8bJKJq2pVinhLcGEQdtLIYPbmNGNyYe8QwNaTUYQp2J7/vIsUKt5QCqAfUkYyG9DkOw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.27", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.27.tgz", + "integrity": "sha512-ElY12DaROGuan+lMmZ8Cvo/ZUbXPe7Enc/9VU/b1T3Kp4dwytRcNdR8DoSJN5SNJT/CuvcCA0DHDVmMOCePdRQ==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/browserslist": { + "version": "4.29.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.29.3.tgz", + "integrity": "sha512-1R4kiYKXGViqEN0CnoDrXc1StD9niAwu+j2dukWzrD4bJgsD4lDmEp0CRbc6E/vYJIfTHwPmwyaKtVSudICdPA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "baseline-browser-mapping": "^2.11.26", + "caniuse-lite": "^1.0.30001813", + "electron-to-chromium": "^1.5.439", + "node-releases": "^2.0.57", + "update-browserslist-db": "^1.3.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/c8": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/c8/-/c8-12.0.0.tgz", + "integrity": "sha512-4zpJvrd1nKWutnnKC2pXkFmb6iM1l+ffN//o1CzlTNwW7GSOs9a1xrLqkC48nU8oEkjmPZLPiwMsIaOvoF4Pqg==", + "dev": true, + "license": "ISC", + "dependencies": { + "@bcoe/v8-coverage": "^1.0.1", + "@istanbuljs/schema": "^0.1.3", + "find-up": "^5.0.0", + "foreground-child": "^3.1.1", + "istanbul-lib-coverage": "^3.2.0", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.1.6", + "test-exclude": "^8.0.0", + "v8-to-istanbul": "^9.0.0", + "yargs": "^18.0.0", + "yargs-parser": "^21.1.1" + }, + "bin": { + "c8": "bin/c8.js" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + }, + "peerDependencies": { + "monocart-coverage-reports": "^2" + }, + "peerDependenciesMeta": { + "monocart-coverage-reports": { + "optional": true + } + } + }, + "node_modules/cacheable": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.5.0.tgz", + "integrity": "sha512-60cyAOytib/OzBw1JNSoSV/boK1AtHryDIjvVBk7XbN4ugfkM3+Sry7fEjNgPMGgOjuaZPAp8ruZ0Cxafwyq9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cacheable/memory": "^2.2.0", + "@cacheable/utils": "^2.5.0", + "hookified": "^1.15.0", + "keyv": "^5.6.0", + "qified": "^0.10.1" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001814", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001814.tgz", + "integrity": "sha512-/Uaf1lAzr59XcMpW0o96WoEfr+VXK2OX4U9AgFoiSHsVJ4HppnIFUjtYzsyDH2+tgANaQb2/oxYGwCPapN1FpA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0", + "peer": true + }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/cjs-module-lexer": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.2.1.tgz", + "integrity": "sha512-Ca8swihM+/4yKecYHY52kgJd300hi2lADU/a1RxNTRe+RJ9jvqQlESpbz9DnG9mowez8qwXHB8qYdIUw9e+F5Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/cliui/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/commander": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-15.0.0.tgz", + "integrity": "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.444", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.444.tgz", + "integrity": "sha512-5ss/uJfoDYDHT0lfJzT6FbcskIzROIOPf0BbbFkGcvDzoJU7i//9GDrwwIHQVmIsrAGiF3ihpADBRIsrEFt1rQ==", + "dev": true, + "license": "ISC", + "peer": true + }, + "node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/empathic": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.1.0.tgz", + "integrity": "sha512-AnfC1ATldl49/cvZdLPDjBfrRNwbDO05aibiOtzQu3qtlbJtomNLhF30HEtn/7iBz50dlMECqATo3fG0LrdEgw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/es-module-lexer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-3.0.2.tgz", + "integrity": "sha512-BuIB67FngDSyQ/dpQNOZybwdEBDUGJQvOqwWr4ha/ufYiqzuEwPkKO2zLhRAgay28tStRIHUeWmszZAJo3GCOg==", + "dev": true, + "license": "MIT" + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "10.12.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.12.0.tgz", + "integrity": "sha512-npHjrHb2o4ehH3mE+YuxABR+0gyb0aVWCIolgJDArwCiHEIsonBm3ZKjel5b0U5+a+MbEvIyRCGCzD3eKKK2NA==", + "dev": true, + "license": "MIT", + "workspaces": [ + "packages/*" + ], + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.7.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.3", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "11.1.5 || >11.1.6 <12", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-patch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/fast-json-patch/-/fast-json-patch-3.1.1.tgz", + "integrity": "sha512-vf6IHUX2SBcA+5/+4883dsIjpBTqmfBjmYiWK1savxQmFk4JfBMLa7ynTYOs1Rolp/T1betJxHiGD3g1Mn8lUQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/file-entry-cache": { + "version": "11.1.5", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-11.1.5.tgz", + "integrity": "sha512-+PFTHITI08JIGhnNpGNI8T8inUpgZfk3GNEqfT9R2zZV2iFXg3CvqzSl/uEhs7TSGujYRELEANyDvS8Fj7+S7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^6.1.23" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "6.1.23", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.23.tgz", + "integrity": "sha512-f++BY9pTk+983xK1FLzlLpmM0i0z+jHmx3QESGkURMXujQZz1k5wzwX6hjnQ8goaD0B+sYnDK1yZ6MTyZfUaqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cacheable": "^2.5.0", + "flatted": "^3.4.2", + "hookified": "^1.15.0" + } + }, + "node_modules/flatted": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/flru": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/flru/-/flru-1.0.2.tgz", + "integrity": "sha512-kWyh8ADvHBFz6ua5xYOPnUroZTT/bwWfrCeL0Wj1dzG4/YOmOcfJ99W8dOVyyynJN35rZ9aCOtHChqQovV7yog==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.7.0.tgz", + "integrity": "sha512-XjH1AECxf0giL2V1aU8vKyRR2ppRUb5c0EvT7zuJTokQ74bNo52zOtghqdWIqrhUD79fo3x0WfKZdOqxF6LG1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hashery": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/hashery/-/hashery-1.5.1.tgz", + "integrity": "sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "hookified": "^1.15.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/hookified": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.15.1.tgz", + "integrity": "sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/hosted-git-info": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.3.tgz", + "integrity": "sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^11.1.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-in-the-middle": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.5.1.tgz", + "integrity": "sha512-mPKuL8bPQzecui2KK6Gb+M8JvJoHnhS1FeYGa22QopBmlevF5F0FE6ued/B5EgHDeIoMTONIpDWWFKUPOG0DBQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "cjs-module-lexer": "^2.2.0", + "es-module-lexer": "^3.0.2", + "module-details-from-path": "^1.0.4" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/import-meta-resolve": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz", + "integrity": "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==", + "dev": true, + "license": "MIT", + "peer": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, "engines": { - "node": ">=16.20.0" + "node": ">=10" } }, - "node_modules/@typescript/typescript-linux-ppc64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-ppc64/-/typescript-linux-ppc64-7.0.2.tgz", - "integrity": "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==", - "cpu": [ - "ppc64" - ], + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, "engines": { - "node": ">=16.20.0" + "node": ">=8" } }, - "node_modules/@typescript/typescript-linux-riscv64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-riscv64/-/typescript-linux-riscv64-7.0.2.tgz", - "integrity": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==", - "cpu": [ - "riscv64" - ], + "node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], + "license": "MIT", + "peer": true + }, + "node_modules/jscpd": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/jscpd/-/jscpd-5.4.0.tgz", + "integrity": "sha512-6mU8OHff4rPJ3D5lIsQns82/aQhDas22gMsGurHx1wcrfXWOZDNaobi6rKhLevo+77ntWKUXOwtM6lcrkJeL7w==", + "dev": true, + "license": "MIT", + "bin": { + "jscpd": "run-jscpd.js" + }, "engines": { - "node": ">=16.20.0" + "node": ">=18" + }, + "funding": { + "url": "https://opencollective.com/jscpd" + }, + "optionalDependencies": { + "jscpd-darwin-arm64": "5.4.0", + "jscpd-darwin-x64": "5.4.0", + "jscpd-linux-arm64-gnu": "5.4.0", + "jscpd-linux-arm64-musl": "5.4.0", + "jscpd-linux-x64-gnu": "5.4.0", + "jscpd-linux-x64-musl": "5.4.0", + "jscpd-windows-arm64-msvc": "5.4.0", + "jscpd-windows-x64-msvc": "5.4.0" } }, - "node_modules/@typescript/typescript-linux-s390x": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-s390x/-/typescript-linux-s390x-7.0.2.tgz", - "integrity": "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==", + "node_modules/jscpd-darwin-arm64": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/jscpd-darwin-arm64/-/jscpd-darwin-arm64-5.4.0.tgz", + "integrity": "sha512-fNIDtNn9WE4YXoFoqf+3tkHb1+GaKKOeKzh77Wx61Gfa9iDxlCv5rO0Fjy7oWmGehUUKqslDaeXXtn/RoDprCg==", "cpu": [ - "s390x" + "arm64" ], "dev": true, - "license": "Apache-2.0", + "license": "MIT", "optional": true, "os": [ - "linux" - ], - "engines": { - "node": ">=16.20.0" - } + "darwin" + ] }, - "node_modules/@typescript/typescript-linux-x64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.0.2.tgz", - "integrity": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==", + "node_modules/jscpd-darwin-x64": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/jscpd-darwin-x64/-/jscpd-darwin-x64-5.4.0.tgz", + "integrity": "sha512-bxlnCVEb69SGIsas1+PaqZyFY8iYVTVjCBxfjVD74UafX115Ysw+oO9LAlsJaGsKUnK8Zsq5bhM33kBHux6bTA==", "cpu": [ "x64" ], "dev": true, - "license": "Apache-2.0", + "license": "MIT", "optional": true, "os": [ - "linux" - ], - "engines": { - "node": ">=16.20.0" - } + "darwin" + ] }, - "node_modules/@typescript/typescript-netbsd-arm64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-arm64/-/typescript-netbsd-arm64-7.0.2.tgz", - "integrity": "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==", + "node_modules/jscpd-linux-arm64-gnu": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/jscpd-linux-arm64-gnu/-/jscpd-linux-arm64-gnu-5.4.0.tgz", + "integrity": "sha512-c4hX5QjuRyzLFJ+A+jNGHRpGlDvxcx+zzs4RJmZHpl23istuTMIkelwTOWliF4gXOm0RWXDlX/ywWKr9+Lre7w==", "cpu": [ "arm64" ], "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-netbsd-x64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-x64/-/typescript-netbsd-x64-7.0.2.tgz", - "integrity": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==", - "cpu": [ - "x64" + "libc": [ + "glibc" ], - "dev": true, - "license": "Apache-2.0", + "license": "MIT", "optional": true, "os": [ - "netbsd" - ], - "engines": { - "node": ">=16.20.0" - } + "linux" + ] }, - "node_modules/@typescript/typescript-openbsd-arm64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-arm64/-/typescript-openbsd-arm64-7.0.2.tgz", - "integrity": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==", + "node_modules/jscpd-linux-arm64-musl": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/jscpd-linux-arm64-musl/-/jscpd-linux-arm64-musl-5.4.0.tgz", + "integrity": "sha512-p4FUCNyL8WPTlizdZqfjontFwJ/+jnERYM2+YX2miYvm0NxzsIi23MhfzPl/eUHxkiT5wethV0NRYEAkJr999Q==", "cpu": [ "arm64" ], "dev": true, - "license": "Apache-2.0", + "libc": [ + "musl" + ], + "license": "MIT", "optional": true, "os": [ - "openbsd" - ], - "engines": { - "node": ">=16.20.0" - } + "linux" + ] }, - "node_modules/@typescript/typescript-openbsd-x64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-x64/-/typescript-openbsd-x64-7.0.2.tgz", - "integrity": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==", + "node_modules/jscpd-linux-x64-gnu": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/jscpd-linux-x64-gnu/-/jscpd-linux-x64-gnu-5.4.0.tgz", + "integrity": "sha512-44NXS0gtYbjBCc53Gwilgj7PPwnWv9YTF/rGc0ZfnFcpzSoOKKU11yH+qHfCVie1hHte1wHyZ1f9Kjc8nGe2/g==", "cpu": [ "x64" ], "dev": true, - "license": "Apache-2.0", + "libc": [ + "glibc" + ], + "license": "MIT", "optional": true, "os": [ - "openbsd" - ], - "engines": { - "node": ">=16.20.0" - } + "linux" + ] }, - "node_modules/@typescript/typescript-sunos-x64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-sunos-x64/-/typescript-sunos-x64-7.0.2.tgz", - "integrity": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==", + "node_modules/jscpd-linux-x64-musl": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/jscpd-linux-x64-musl/-/jscpd-linux-x64-musl-5.4.0.tgz", + "integrity": "sha512-m9XHj14Zwia9RjWDkHDrl6VJ3W/b7m8qjFqjhcJ668Vkxoj4Ri/qeo0bo6is+fSw+bYxYexV6xxqIKRDolc8Lg==", "cpu": [ "x64" ], "dev": true, - "license": "Apache-2.0", + "libc": [ + "musl" + ], + "license": "MIT", "optional": true, "os": [ - "sunos" - ], - "engines": { - "node": ">=16.20.0" - } + "linux" + ] }, - "node_modules/@typescript/typescript-win32-arm64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.0.2.tgz", - "integrity": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==", + "node_modules/jscpd-windows-arm64-msvc": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/jscpd-windows-arm64-msvc/-/jscpd-windows-arm64-msvc-5.4.0.tgz", + "integrity": "sha512-AoP5RXyb9gIf8xmFwAc+Rgh4XJNyX1upcD+R1A6u4XjsDo2/oF+KKB4dtKnG2DbFQ+0fyFg/ClOcw+asQZJfPg==", "cpu": [ "arm64" ], "dev": true, - "license": "Apache-2.0", + "license": "MIT", "optional": true, "os": [ "win32" - ], - "engines": { - "node": ">=16.20.0" - } + ] }, - "node_modules/@typescript/typescript-win32-x64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-x64/-/typescript-win32-x64-7.0.2.tgz", - "integrity": "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==", + "node_modules/jscpd-windows-x64-msvc": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/jscpd-windows-x64-msvc/-/jscpd-windows-x64-msvc-5.4.0.tgz", + "integrity": "sha512-dYkb5mAo5i7fjrtf2Ft6z9a/I38RJMd4JirEgJiW2BIv5pAGmfMVPDy1xEXL3hJqraXzeMIfnzqJFanefCyC6g==", "cpu": [ "x64" ], "dev": true, - "license": "Apache-2.0", + "license": "MIT", "optional": true, "os": [ "win32" - ], - "engines": { - "node": ">=16.20.0" - } + ] }, - "node_modules/@unbrained/pm-cli": { - "version": "2026.10.4", - "resolved": "https://registry.npmjs.org/@unbrained/pm-cli/-/pm-cli-2026.10.4.tgz", - "integrity": "sha512-NZLdiUSUKIRcFiMj9lG0L5UubA9G33fq5WzB1JzJjTTlYxYCVjrFFOCV2rtxYTaYTCzX9o/6V/A4fDx6N3mm/w==", + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", "dev": true, "license": "MIT", - "dependencies": { - "@sentry/node": "10.75.1", - "@toon-format/toon": "^4.1.1", - "@types/node": ">=22", - "commander": "^15.0.0", - "fast-json-patch": "^3.1.1", - "npm-package-arg": "^13.0.2", - "tar": "7.5.22", - "tinyglobby": "^0.2.17", - "yaml": "^2.9.1" - }, + "peer": true, "bin": { - "pm": "dist/cli.js", - "pm-cli": "dist/cli.js", - "pm-mcp": "dist/mcp/server.js", - "pm-mcp-http": "dist/mcp/http-server.js" + "jsesc": "bin/jsesc" }, "engines": { - "node": ">=22.18.0" + "node": ">=6" } }, - "node_modules/chownr": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", - "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } + "license": "MIT" }, - "node_modules/cjs-module-lexer": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.2.1.tgz", - "integrity": "sha512-Ca8swihM+/4yKecYHY52kgJd300hi2lADU/a1RxNTRe+RJ9jvqQlESpbz9DnG9mowez8qwXHB8qYdIUw9e+F5Q==", + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true, "license": "MIT" }, - "node_modules/commander": { - "version": "15.0.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-15.0.0.tgz", - "integrity": "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==", + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true, "license": "MIT", + "peer": true, + "bin": { + "json5": "lib/cli.js" + }, "engines": { - "node": ">=22.12.0" + "node": ">=6" } }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "node_modules/keyv": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", + "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", "dev": true, "license": "MIT", "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "@keyv/serialize": "^1.1.1" } }, - "node_modules/es-module-lexer": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-3.0.2.tgz", - "integrity": "sha512-BuIB67FngDSyQ/dpQNOZybwdEBDUGJQvOqwWr4ha/ufYiqzuEwPkKO2zLhRAgay28tStRIHUeWmszZAJo3GCOg==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-json-patch": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/fast-json-patch/-/fast-json-patch-3.1.1.tgz", - "integrity": "sha512-vf6IHUX2SBcA+5/+4883dsIjpBTqmfBjmYiWK1savxQmFk4JfBMLa7ynTYOs1Rolp/T1betJxHiGD3g1Mn8lUQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/hosted-git-info": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.3.tgz", - "integrity": "sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg==", + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "lru-cache": "^11.1.0" + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" }, "engines": { - "node": "^20.17.0 || >=22.9.0" + "node": ">= 0.8.0" } }, - "node_modules/import-in-the-middle": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.5.1.tgz", - "integrity": "sha512-mPKuL8bPQzecui2KK6Gb+M8JvJoHnhS1FeYGa22QopBmlevF5F0FE6ued/B5EgHDeIoMTONIpDWWFKUPOG0DBQ==", + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "cjs-module-lexer": "^2.2.0", - "es-module-lexer": "^3.0.2", - "module-details-from-path": "^1.0.4" + "p-locate": "^5.0.0" }, "engines": { - "node": ">=18" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/lru-cache": { @@ -751,7 +2364,39 @@ "dev": true, "license": "BlueOak-1.0.0", "engines": { - "node": "20 || >=22" + "node": "20 || >=22" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/minipass": { @@ -791,6 +2436,24 @@ "dev": true, "license": "MIT" }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.57", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.57.tgz", + "integrity": "sha512-kQK9LGGFiHtrWiNhZtA7Qbw17AQz+dmsEKODRIVTXA9+e5MS/2gZEBhYJt13GrAz5/IOZKddH/0Z3TP/Zgo+yw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + } + }, "node_modules/npm-package-arg": { "version": "13.0.2", "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-13.0.2.tgz", @@ -807,6 +2470,116 @@ "node": "^20.17.0 || >=22.9.0" } }, + "node_modules/obug": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.2.1.tgz", + "integrity": "sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC", + "peer": true + }, "node_modules/pm-changelog": { "version": "2026.10.4", "resolved": "https://registry.npmjs.org/pm-changelog/-/pm-changelog-2026.10.4.tgz", @@ -862,6 +2635,16 @@ } } }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/proc-log": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", @@ -872,6 +2655,36 @@ "node": "^20.17.0 || >=22.9.0" } }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qified": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/qified/-/qified-0.10.1.tgz", + "integrity": "sha512-+Owyggi9IxT1ePKGafcI87ubSmxol6smwJ+RAHDQlx9+9cPwFWDiKFFCPuWhr9ignlGpZ9vDQLw67N4dcTVFEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hookified": "^2.1.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/qified/node_modules/hookified": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-2.2.0.tgz", + "integrity": "sha512-p/LgFzRN5FeoD3DLS6bkUapeye6E4SI6yJs6KetENd18S+FBthqYq2amJUWpt5z0EQwwHemidjY5OqJGEKm5uA==", + "dev": true, + "license": "MIT" + }, "node_modules/require-in-the-middle": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-8.0.1.tgz", @@ -899,6 +2712,88 @@ "node": ">=10" } }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/string-width": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.3.0.tgz", + "integrity": "sha512-ZbmZM0JCihQN91dWnxoipT2KOEyHqEyfRXUyjuRhW8b/xnqPDoq4gWEVApTVa9db2wN8mmoikgFBbjh71+cGeQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/tar": { "version": "7.5.22", "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.22.tgz", @@ -916,6 +2811,21 @@ "node": ">=18" } }, + "node_modules/test-exclude": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-8.0.0.tgz", + "integrity": "sha512-ZOffsNrXYggvU1mDGHk54I96r26P8SyMjO5slMKSc7+IWmtB/MQKnEC2fP51imB3/pT6YK5cT5E8f+Dd9KdyOQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^13.0.6", + "minimatch": "^10.2.2" + }, + "engines": { + "node": "20 || >=22" + } + }, "node_modules/tinyglobby": { "version": "0.2.17", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", @@ -964,6 +2874,19 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/typescript": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz", @@ -1006,6 +2929,63 @@ "dev": true, "license": "MIT" }, + "node_modules/update-browserslist-db": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.3.tgz", + "integrity": "sha512-pJ2sYawQS0R/WI928Gj5GlPhTGzbMelq0+4INtSYNDV9ErKJcX6xjGWkoG/VnB3dpUm00zALaqkrUD77pO5TDQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, "node_modules/validate-npm-package-name": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-7.0.2.tgz", @@ -1016,6 +2996,91 @@ "node": "^20.17.0 || >=22.9.0" } }, + "node_modules/verkit": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/verkit/-/verkit-0.3.2.tgz", + "integrity": "sha512-zj/ob3UsvJGN0whEAKFp53REA5X66hvffVqoCtVQAakJKnKlH+/PcOfMoFwIG/o4rElqLv/ycAFlx8ZlXUorCg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/yallist": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", @@ -1041,6 +3106,57 @@ "funding": { "url": "https://github.com/sponsors/eemeli" } + }, + "node_modules/yargs": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.2.0.tgz", + "integrity": "sha512-9OpKOLeaoNFecEp7P6iYbzze/5CqWoH7N3SMs/6y1XF4nMvFMspEGZzJ6uFi9MmQwXhyzqYoSEKO3tvA3Z/o2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^9.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "string-width": "^8.2.1", + "y18n": "^5.0.5", + "yargs-parser": "^22.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/yargs-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } } } } diff --git a/package.json b/package.json index 2ef3597c..d4be09ab 100644 --- a/package.json +++ b/package.json @@ -30,22 +30,30 @@ "audit:prod": "node --input-type=module -e \"import { spawnSync } from 'node:child_process'; import { devNull } from 'node:os'; const env = { ...process.env, npm_config_userconfig: devNull, NPM_CONFIG_USERCONFIG: devNull }; for (const key of Object.keys(env)) if (key.toLowerCase() === 'npm_config_allow_scripts') delete env[key]; const win = process.platform === 'win32'; const r = spawnSync(win ? 'npm.cmd' : 'npm', ['audit', '--omit=dev', '--ignore-scripts'], { stdio: 'inherit', env, shell: win }); process.exit(r.status ?? 1);\"", "pack:dry-run": "npm pack --dry-run", "verify:release-publish-attestation": "node scripts/verify-release-publish-attestation.ts", - "changelog:full": "pm-changelog --pm-root .agents/pm --pm-arg=--output-limit --pm-arg=unbounded --pm-arg=--output-budget --pm-arg=unbounded --mode replace --output CHANGELOG.md --all-release-tags --release-version-from-package --date-from-version --item-url-base https://github.com/unbraind/pm-github/blob/main/.agents/pm --respect-item-release", + "changelog:full": "node scripts/generate-changelog.ts", "changelog:check": "npm run changelog:full -- --check", - "release:check": "npm run typecheck && npm run build && npm run docstring && npm run privacy && npm run coverage && npm run audit:prod && npm run pack:dry-run && npm run changelog:check && npm run verify:release-publish-attestation", + "release:check": "npm run typecheck && npm run build && npm run docstring && npm run lint && npm run duplication && npm run privacy && npm run coverage && npm run audit:prod && npm run pack:dry-run && npm run changelog:check && npm run verify:release-publish-attestation", "prepublishOnly": "npm run release:check", "release:notes": "pm-changelog --pm-root .agents/pm --pm-arg=--output-limit --pm-arg=unbounded --pm-arg=--output-budget --pm-arg=unbounded --stdout --since-previous-tag --until-release-tag --release-version-from-package --date-from-version --item-url-base https://github.com/unbraind/pm-github/blob/main/.agents/pm --respect-item-release --pm-bin ./node_modules/.bin/pm --github-step-summary", "prepare": "node scripts/prepare-merge-driver.ts", "merge:install": "pm merge install", "coverage": "npm run build && npm run build:test && node scripts/coverage-gate.ts", - "privacy": "node scripts/privacy-gate.ts" + "privacy": "node scripts/privacy-gate.ts", + "lint": "eslint .", + "duplication": "node scripts/duplication-gate.ts", + "test:bun": "bun_status=0; for test_file in test/*.test.ts; do bun test --timeout 600000 \"$test_file\" || bun_status=1; done; exit \"$bun_status\"" }, "peerDependencies": { "@unbrained/pm-cli": ">=2026.8.20" }, "devDependencies": { + "@babel/eslint-parser": "8.0.5", + "@babel/plugin-syntax-typescript": "8.0.3", "@types/node": "26.6.4", "@unbrained/pm-cli": "2026.10.4", + "c8": "12.0.0", + "eslint": "10.12.0", + "jscpd": "5.4.0", "pm-changelog": "2026.10.4", "pm-ops": "2026.10.4", "typescript": "7.0.2" @@ -105,10 +113,15 @@ "test/*.test.ts" ], "thresholds": { - "lines": 88, - "branches": 79, - "functions": 89 + "lines": 100, + "statements": 100, + "branches": 100, + "functions": 100 }, "ignore": [] + }, + "duplicationGate": { + "threshold": 0, + "minTokens": 50 } } diff --git a/projects.ts b/projects.ts index 23b81510..440ff3ac 100644 --- a/projects.ts +++ b/projects.ts @@ -148,11 +148,8 @@ export function decodeItemId(encoded: string): string | undefined { if (encoded.length === 0 || encoded.length % 2 !== 0 || !/^[0-9a-f]+$/i.test(encoded)) { return undefined; } - try { - return Buffer.from(encoded, "hex").toString("utf8"); - } catch { - return undefined; - } + // Validated even-length hex is accepted by Buffer without a parse exception. + return Buffer.from(encoded, "hex").toString("utf8"); } // The project link rides on a machine-parseable tag, like the issue provenance @@ -213,12 +210,27 @@ export const DEFAULT_STATUS_CANDIDATES: Record = { export function parseStatusMap( raw: string[], ): Map | undefined { + return parseAssignmentMap(raw, true); +} + +/** + * Parse nonempty key=value pairs without expanding malformed entries. + * + * Shared by label translation and project status mapping; status keys normalize + * case while label keys preserve the spelling GitHub supplied. + * + * @param raw - Assignment entries collected from a CLI option. + * @param normalizeKeys - Whether the left-hand keys are case-insensitive. + * @returns The usable mappings, or undefined for an empty effective map. + */ +export function parseAssignmentMap(raw: readonly string[], normalizeKeys = false): Map | undefined { if (!raw || raw.length === 0) return undefined; const map = new Map(); for (const entry of raw) { const eq = entry.indexOf("="); if (eq <= 0) continue; - const from = entry.slice(0, eq).trim().toLowerCase(); + const key = entry.slice(0, eq).trim(); + const from = normalizeKeys ? key.toLowerCase() : key; const to = entry.slice(eq + 1).trim(); if (!from || !to) continue; map.set(from, to); diff --git a/scripts/coverage-gate.ts b/scripts/coverage-gate.ts index 365dfd1c..5543a186 100644 --- a/scripts/coverage-gate.ts +++ b/scripts/coverage-gate.ts @@ -1,346 +1,133 @@ /** - * Coverage gate for the package test suite. + * Require exact coverage of every authored TypeScript and JavaScript source. * - * Runs `node --test` with the runtime's built-in V8 coverage against the - * TypeScript sources directly (Node executes `.ts` natively, so the reported - * line numbers are the ones an author edits, not compiled output), enforces a - * per-dimension threshold, and reconciles the reported file list against the - * files actually on disk. - * - * That last step is the reason this script exists rather than a bare - * `node --test --test-coverage-lines=...` invocation. Node only reports files - * that were loaded during the run: a source module with no test at all is - * omitted from the report entirely rather than reported at zero. The published - * percentage is therefore computed over the tested subset, and a package can - * satisfy a 100% threshold while an entire module goes unexercised. Comparing - * the report against a directory walk turns that silent omission into a failure - * naming the missing files, so the threshold cannot be passed by narrowing what - * the suite touches. - * - * Configuration lives in `package.json` under `coverageGate` so the numbers the - * gate enforces are visible in the same file that declares the scripts, and a - * threshold change shows up in review as a deliberate diff. - * - * @example - * ```bash - * node scripts/coverage-gate.ts - * ``` - */ - -import { spawnSync } from "node:child_process"; -import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync } from "node:fs"; -import { isAbsolute, join, relative, resolve, sep } from "node:path"; - -/** - * Minimum acceptable percentage for each coverage dimension Node reports. - * - * Statement coverage is not listed because V8 reports statements as lines; the - * line figure is the statement figure for this runtime. + * Inventory includes operational scripts. c8 combines native V8 receipts from + * test processes and their children, and reports unloaded modules at zero. + * Reports are invalidated before a run; success requires complete source + * receipts and exact covered/total equality in all four dimensions. */ -interface CoverageThresholds { - /** Minimum percentage of executable lines that must be covered. */ - readonly lines: number; - /** Minimum percentage of branch arms that must be taken. */ - readonly branches: number; - /** Minimum percentage of declared functions that must be invoked. */ - readonly functions: number; -} - -/** The `coverageGate` block read from `package.json`. */ -interface CoverageGateConfig { - /** - * Source locations the gate requires to appear in the report. Each entry is - * either a directory, walked recursively for `.ts` files, or a single file. - * - * Prefer a directory — including `"."` for a package whose entrypoint sits at - * the repository root. A directory is enumerated at run time, so a source file - * added later is required automatically. An explicit file list freezes the - * required set at the moment it was written, and a new untested module simply - * never enters it, which is the same blind spot this gate exists to close. - */ +import { spawnSync, type SpawnSyncOptions } from "node:child_process"; +import { mkdirSync, readdirSync, readFileSync, rmSync, statSync } from "node:fs"; +import { createRequire } from "node:module"; +import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path"; +import { isMainInvocation, nodeToolingExecutable } from "./main-invocation.ts"; + +/** Configuration cannot exempt sources or lower the required complete gate. */ +interface CoverageConfig { readonly sources: readonly string[]; - /** - * Directory names skipped while walking, on top of {@link DEFAULT_SKIP_DIRS}. - * Needed only for a source tree with a non-standard non-source directory. - */ - readonly skipDirs?: readonly string[]; - /** Test file arguments handed to `node --test`. */ readonly tests: readonly string[]; - /** Threshold enforced on the aggregate report. */ - readonly thresholds: CoverageThresholds; - /** - * Source files exempt from the presence check, each of which must be - * type-only. A module that erases to nothing emits no coverage counters, so - * requiring it in the report would make the gate unsatisfiable. - */ + readonly thresholds: Readonly>; readonly ignore?: readonly string[]; + readonly skipDirs?: readonly string[]; } -/** Shape of the `package.json` fields this script reads. */ -interface PackageManifest { - readonly coverageGate?: CoverageGateConfig; +/** Exact coverage counters emitted by the standard JSON summary reporter. */ +interface CoverageCounter { + readonly total: number; + readonly covered: number; + readonly skipped: number; } -const repoRoot = resolve(import.meta.dirname, ".."); -const manifest = JSON.parse(readFileSync(join(repoRoot, "package.json"), "utf8")) as PackageManifest; -const config = manifest.coverageGate; +/** A complete per-source receipt contains every required coverage dimension. */ +type CoverageReceipt = Record; -if (!config) { - console.error("coverage-gate: package.json has no `coverageGate` block."); - process.exit(1); -} +/** Test boundary for subprocess failures; production always starts real c8. */ +export type CoverageRunner = (command: string, args: readonly string[], options: SpawnSyncOptions) => { readonly status: number | null; readonly error?: Error }; -/** Compiler paths used to locate a source file's emitted output. */ -interface TsConfig { - readonly compilerOptions?: { readonly outDir?: string; readonly rootDir?: string }; -} +/** Test files, dependencies, tracker data, and generated artifacts are not source. */ +const GENERATED_DIRECTORIES = new Set(["node_modules", "dist", "dist-test", "coverage", "test", "tests", ".agents", ".git", ".github"]); +/** Independent dimensions; equality uses counts, never rounded percentages. */ +const DIMENSIONS = ["lines", "statements", "functions", "branches"] as const; /** - * Resolves the compiler's effective output paths. - * - * Asks `tsc --showConfig` rather than parsing `tsconfig.json` directly: the file - * may be JSONC and may inherit `outDir`/`rootDir` through an `extends` chain, so - * a raw `JSON.parse` can either throw on a valid config or silently read the - * wrong paths. + * Inventory all executable source modules under a configured location. * - * Fails closed if the compiler cannot be reached. This feeds the check that - * decides whether an exempted module is genuinely type-only, and guessing the - * emit layout there could clear an executable module by looking at the wrong - * file — the one outcome this gate must never produce. A package that cannot - * run its own compiler has a problem worth stopping for. + * @param root - Repository root used for stable relative paths. + * @param target - Configured source file or directory. + * @returns Source filenames; declaration modules carry no executable code. */ -function resolveEmitPaths(): { outDir: string; rootDir: string } { - const shown = spawnSync("npx", ["tsc", "--showConfig", "-p", "tsconfig.json"], { - cwd: repoRoot, - encoding: "utf8", - shell: process.platform === "win32", - }); - if (shown.status !== 0 || !shown.stdout) { - console.error( - [ - "coverage-gate: could not resolve the effective tsconfig via `tsc --showConfig`,", - "so the emit layout is unknown and `coverageGate.ignore` entries cannot be verified", - "as type-only. Refusing to guess.", - shown.stderr?.trim() ? `\n${shown.stderr.trim()}` : "", - ].join("\n"), - ); - process.exit(1); +export function collectCoverageSources(root: string, target: string): string[] { + const location = resolve(root, target); + const repoRelative = relative(root, location); + if (isAbsolute(repoRelative) || repoRelative === ".." || repoRelative.startsWith(`..${sep}`)) { + throw new Error("Coverage source escapes the repository."); + } + if (!statSync(location).isDirectory()) { + if (!/\.(?:ts|js)$/.test(location) || location.endsWith(".d.ts")) throw new Error("Coverage source must be an executable module."); + return [repoRelative.split(sep).join("/")]; } - const parsed = JSON.parse(shown.stdout) as TsConfig; - return { - outDir: parsed.compilerOptions?.outDir ?? "dist", - rootDir: parsed.compilerOptions?.rootDir ?? ".", - }; + return readdirSync(location, { withFileTypes: true }).flatMap(entry => { + const next = join(repoRelative, entry.name); + if (entry.isDirectory()) return GENERATED_DIRECTORIES.has(entry.name) ? [] : collectCoverageSources(root, next); + return /\.(?:ts|js)$/.test(entry.name) && !entry.name.endsWith(".d.ts") ? [next.split(sep).join("/")] : []; + }); } /** - * Directories never treated as source, so that `sources: ["."]` works for a - * package whose entrypoint sits at the repository root. - * - * These hold tests, build output, tooling and installed dependencies. None of - * them contain shipped source, and several would otherwise make the required - * set unsatisfiable — a test file cannot appear in its own coverage report. - */ -const DEFAULT_SKIP_DIRS: readonly string[] = [ - "node_modules", - "dist", - "dist-test", - "coverage", - "test", - "tests", - "scripts", - "public", - ".agents", - ".git", - ".github", -]; - -const skipDirs = new Set([...DEFAULT_SKIP_DIRS, ...(config.skipDirs ?? [])]); - -/** - * Collects every TypeScript source file at a configured location. - * - * A file entry resolves to itself; a directory entry is walked recursively with - * {@link DEFAULT_SKIP_DIRS} pruned. Declaration files are skipped either way: - * they carry no runtime code and so can never appear in a coverage report. + * Run the strict all-source gate and return its exit status. * - * @param target - Absolute path to a source file or directory. - * @returns Repository-relative POSIX paths, in directory order. + * @param root - Repository to inventory and test. + * @param runner - Subprocess runner, injectable for fail-closed diagnostics. + * @returns Zero only for passing tests and complete 100/100/100/100 receipts. */ -function collectSources(target: string): string[] { - if (!existsSync(target)) { - console.error( - `coverage-gate: \`coverageGate.sources\` names ${relative(repoRoot, target)}, which does not exist.`, - ); - process.exit(1); - } - if (!statSync(target).isDirectory()) { - if (!target.endsWith(".ts") || target.endsWith(".d.ts")) { - console.error( - `coverage-gate: \`coverageGate.sources\` names ${relative(repoRoot, target)}, which is not a TypeScript source file. A declaration file or non-TypeScript entry can never appear in a coverage report, so requiring it would make the gate unsatisfiable.`, - ); - process.exit(1); +export function runCoverageGate(root: string, runner: CoverageRunner = spawnSync): number { + const reports = join(root, "coverage"); + mkdirSync(reports, { recursive: true }); + for (const file of ["lcov.info", "coverage-summary.json", "coverage-final.json"]) rmSync(join(reports, file), { force: true }); + try { + const manifest = JSON.parse(readFileSync(join(root, "package.json"), "utf8")) as { coverageGate?: CoverageConfig }; + const config = manifest.coverageGate; + if (!config || JSON.stringify(config.sources) !== '["."]' || DIMENSIONS.some(dimension => config.thresholds[dimension] !== 100) || (config.ignore?.length ?? 0) !== 0 || (config.skipDirs?.length ?? 0) !== 0) { + throw new Error("Coverage requires four 100 percent thresholds and no source exemptions."); } - return [relative(repoRoot, target).split(sep).join("/")]; - } - const found: string[] = []; - for (const entry of readdirSync(target, { withFileTypes: true })) { - if (entry.isDirectory()) { - if (!skipDirs.has(entry.name)) { - found.push(...collectSources(join(target, entry.name))); - } - } else if (entry.name.endsWith(".ts") && !entry.name.endsWith(".d.ts")) { - found.push(relative(repoRoot, join(target, entry.name)).split(sep).join("/")); + const sources = [...new Set(config.sources.flatMap(source => collectCoverageSources(root, source)))]; + if (sources.length === 0 || config.tests.length === 0) throw new Error("Coverage inventory or test set is empty."); + const c8 = join(dirname(createRequire(import.meta.url).resolve("c8/package.json")), "bin", "c8.js"); + const coverageEnv: NodeJS.ProcessEnv = { ...process.env, TZ: "UTC" }; + delete coverageEnv.NODE_TEST_CONTEXT; + const node = nodeToolingExecutable(process.versions); + const result = runner(node, [c8, "--100", "--all", "--exclude-after-remap", "--extension=.ts", "--extension=.js", + `--reports-dir=${reports}`, `--temp-directory=${join(reports, "v8")}`, + "--reporter=text", "--reporter=lcov", "--reporter=json-summary", "--reporter=json", + ...sources.map(file => `--include=${file}`), node, "--test", ...config.tests], + { cwd: root, stdio: "inherit", env: coverageEnv }); + if (result.error || result.status !== 0) { + rmSync(join(reports, "lcov.info"), { force: true }); + return result.status ?? 1; + } + const summary = JSON.parse(readFileSync(join(reports, "coverage-summary.json"), "utf8")) as Record; + const receiptFiles = new Set(Object.keys(summary).filter(file => file !== "total").map(file => relative(root, resolve(root, file)).split(sep).join("/"))); + if (sources.some(file => !receiptFiles.has(file))) throw new Error("Coverage receipt omits an authored source."); + const lcovFiles = new Set(readFileSync(join(reports, "lcov.info"), "utf8").split("\n").filter(line => line.startsWith("SF:")).map(line => relative(root, resolve(root, line.slice(3))).split(sep).join("/"))); + if (sources.some(file => !lcovFiles.has(file))) throw new Error("LCOV receipt omits an authored source."); + for (const receipt of Object.values(summary)) { + if (DIMENSIONS.some(dimension => { + const count = receipt[dimension]; + return !count || !Number.isInteger(count.total) || count.total < 0 || count.covered !== count.total || count.skipped !== 0; + })) throw new Error("Coverage receipt is incomplete or contains skipped source."); } + if (!summary.total || summary.total.lines!.total === 0) throw new Error("Coverage receipt has no executable lines."); + console.log(`coverage-gate: ${sources.length} authored modules, exact 100/100/100/100 coverage.`); + return 0; + } catch { + rmSync(join(reports, "lcov.info"), { force: true }); + console.error("coverage-gate: configuration, source inventory, or complete coverage receipt is invalid."); + return 1; } - return found; } -const expected = config.sources.flatMap((source) => collectSources(join(repoRoot, source))); -const exempt = new Set(config.ignore ?? []); -const required = expected.filter((file) => !exempt.has(file)); - /** - * Rejects an `ignore` entry that still carries runtime code. + * Run the coverage gate when this file is the process entry point. * - * The exemption exists for type-only modules, which erase to nothing and so can - * never appear in a coverage report. Left untested, it is also the one way to - * remove an executable module from both the measured set and the required set — - * exactly the escape this gate exists to prevent. TypeScript emits `export {};` - * and nothing else for a module that erases completely, so the compiled output - * settles the question rather than the author's say-so. - */ -const emitPaths = (config.ignore ?? []).length > 0 ? resolveEmitPaths() : { outDir: "dist", rootDir: "." }; - -for (const file of config.ignore ?? []) { - if (!expected.includes(file)) { - console.error(`coverage-gate: \`coverageGate.ignore\` names ${file}, which is not under \`sources\`.`); - process.exit(1); - } - const emitted = join( - repoRoot, - emitPaths.outDir, - relative(join(repoRoot, emitPaths.rootDir), join(repoRoot, file)), - ).replace(/\.ts$/, ".js"); - if (!existsSync(emitted)) { - console.error( - `coverage-gate: cannot verify that ignored file ${file} is type-only — no compiled output at ${relative(repoRoot, emitted)}. Build before running the gate, or correct \`outDir\`/\`rootDir\`.`, - ); - process.exit(1); - } - // Block comments are stripped as well as line comments: tsc carries a - // file-leading JSDoc into the emit, so a documented type-only module would - // otherwise read as runtime code and be rejected for having a comment. - const body = readFileSync(emitted, "utf8") - .replace(/\/\*[\s\S]*?\*\//g, "") - .replace(/^\s*\/\/.*$/gm, "") - .replace(/export\s*\{\s*\}\s*;?/g, "") - .trim(); - if (body.length > 0) { - console.error( - `coverage-gate: \`coverageGate.ignore\` names ${file}, but it emits runtime code to ${relative(repoRoot, emitted)}. Only type-only modules may be exempt; anything executable must be covered.`, - ); - process.exit(1); - } -} - -if (required.length === 0) { - console.error("coverage-gate: source walk found no files; check `coverageGate.sources`."); - process.exit(1); -} - -const lcovPath = join(repoRoot, "coverage", "lcov.info"); -mkdirSync(join(repoRoot, "coverage"), { recursive: true }); -// Delete any previous report first. If this run writes none, a leftover file -// from an earlier, broader run would satisfy the presence check on stale data — -// the gate would pass by reading history rather than by measuring anything. -rmSync(lcovPath, { force: true }); - -const result = spawnSync( - process.execPath, - [ - "--test", - "--experimental-test-coverage", - // Scope the report to exactly the files the presence check requires. Passing - // the enumerated paths rather than a directory glob keeps the two in step by - // construction, and keeps test files and tooling out of the percentages even - // when the source root is the repository root. - ...required.map((file) => `--test-coverage-include=${file}`), - `--test-coverage-lines=${config.thresholds.lines}`, - `--test-coverage-branches=${config.thresholds.branches}`, - `--test-coverage-functions=${config.thresholds.functions}`, - "--test-reporter=spec", - "--test-reporter-destination=stdout", - "--test-reporter=lcov", - `--test-reporter-destination=${lcovPath}`, - ...config.tests, - ], - { - cwd: repoRoot, - stdio: "inherit", - // Pin the timezone so the measurement is reproducible on any machine. - // Code that branches on a timestamp's UTC offset takes different paths under - // a local offset than under UTC, which moves the reported percentage between - // a contributor's machine and CI. A threshold pinned to one machine's number - // then fails on the other for reasons unrelated to the change under review. - env: { ...process.env, TZ: "UTC" }, - }, -); - -if (result.error) { - console.error(`coverage-gate: failed to start the test runner: ${result.error.message}`); - process.exit(1); -} - -// Surface a runner failure before touching the report at all. A failing suite, -// an unmet threshold, or a test file that will not load can each leave the lcov -// output absent or incomplete, and every diagnostic below would then describe a -// coverage-configuration problem the author does not have — burying the test -// failure they need to act on. -if (result.status !== 0) { - process.exit(result.status ?? 1); -} - -/** - * Source files the run actually reported on, read back from the lcov output. + * The check lives in a function so a test can execute the true branch against a + * fixture root. The CLI bottom call uses the real argv and this package root. * - * `SF:` paths are normalised to repository-relative POSIX form so they can be - * compared against the walk. The lcov reporter emits them relative to the - * working directory on Linux, but that is not contractual and Windows runners - * have been seen to emit absolute paths; without normalising, the presence - * check would invert into a permanently red build that blames every source file - * for never loading. + * @param argv - Process argv to compare with the module URL. + * @param moduleUrl - `import.meta.url` of this module. + * @param root - Repository root to inventory and test. */ -const reported = new Set(); -try { - statSync(lcovPath); - for (const line of readFileSync(lcovPath, "utf8").split("\n")) { - if (!line.startsWith("SF:")) continue; - const raw = line.slice(3).trim(); - const abs = isAbsolute(raw) ? raw : join(repoRoot, raw); - reported.add(relative(repoRoot, abs).split(sep).join("/")); - } -} catch { - console.error(`coverage-gate: no coverage report was written to ${relative(repoRoot, lcovPath)}.`); - process.exit(1); -} - -const missing = required.filter((file) => !reported.has(file)); - -if (missing.length > 0) { - console.error( - [ - "", - `coverage-gate: ${missing.length} source file(s) never loaded during the run and were`, - "omitted from the coverage report, so the reported percentages exclude them entirely:", - ...missing.map((file) => ` - ${file}`), - "", - "Import each file from a test (or exercise it through the CLI entrypoint under test).", - "A file that is genuinely type-only belongs in `coverageGate.ignore` in package.json.", - "", - ].join("\n"), - ); - process.exit(1); +export function runCoverageGateIfMain(argv: readonly string[], moduleUrl: string, root: string): void { + if (isMainInvocation(argv, moduleUrl)) process.exitCode = runCoverageGate(root); } -console.log(`\ncoverage-gate: ${required.length} source file(s) reported, thresholds met.`); +runCoverageGateIfMain(process.argv, import.meta.url, resolve(import.meta.dirname, "..")); diff --git a/scripts/docstring-gate.ts b/scripts/docstring-gate.ts index 541339bc..3a013fa6 100644 --- a/scripts/docstring-gate.ts +++ b/scripts/docstring-gate.ts @@ -8,14 +8,13 @@ * beyond its identifier. The analyzer has no ignore list and treats unknown * declaration forms as violations, so a new syntax form fails closed. * - * This script is intentionally self-contained: pm-github has no shared script - * launcher, so the main-invocation guard and the result contract live here - * rather than in an imported helper module. + * The entry-point guard is shared with the other operational gates; the + * analyzer and result contract remain directly testable. */ -import { realpathSync } from "node:fs"; import { join } from "node:path"; -import { fileURLToPath } from "node:url"; +import { isMainInvocation } from "./main-invocation.ts"; +export { isMainInvocation } from "./main-invocation.ts"; import { analyzeDocstringCoverage } from "pm-ops/docstrings"; @@ -87,45 +86,6 @@ export function main(root: string): void { process.exitCode = result.exitCode; } -/** - * Whether this module is the process entry point rather than a test import. - * - * Both sides are canonicalised through `realpathSync` before comparison. A - * launcher reaching this file through a symlink (an npm bin shim, a linked - * workspace) would otherwise compare unequal and skip the gate silently. - * - * Resolving only `argv[1]` would be enough under Node's defaults, where the - * ESM loader realpaths a module before recording `import.meta.url`. It is not - * enough under `--preserve-symlinks`/`--preserve-symlinks-main`, which leave - * `moduleUrl` holding the symlink while `realpathSync(entry)` resolves it. - * The two would then compare unequal on a direct invocation and the gate would - * exit 0 without scanning — the exact silent skip this function exists to - * prevent, reintroduced by a runtime flag. Canonicalising both sides adds a - * second `realpathSync` and removes the dependence on how Node was launched. - * - * An unresolvable `argv[1]` **propagates** rather than returning false. The two - * outcomes are not equally safe: returning false means `npm run docstring` - * exits 0 having scanned nothing, which is a required release check reporting - * success without doing its job — the one failure this gate exists to prevent. - * Letting `realpathSync` throw turns that into a loud non-zero exit. The case - * requires `argv[1]` to stop resolving after Node has already loaded this file, - * so in practice it means the environment is broken, and a broken environment - * must not silently satisfy a gate. - * - * A genuinely different entry path still returns false, which is how a test - * importing this module declines to run the gate. - * - * @param argv - The process argv to inspect. - * @param moduleUrl - The `import.meta.url` of the module that might be main. - * @returns True when `argv[1]` and `moduleUrl` canonicalise to the same path, - * false when they canonicalise to different ones. - * @throws Whatever `realpathSync` throws when either path cannot be resolved. - */ -export function isMainInvocation(argv: readonly string[], moduleUrl: string): boolean { - const entry = argv[1]; - if (entry === undefined) return false; - return realpathSync(entry) === realpathSync(fileURLToPath(moduleUrl)); -} if (isMainInvocation(process.argv, import.meta.url)) { main(repoRoot); diff --git a/scripts/duplication-gate.ts b/scripts/duplication-gate.ts new file mode 100644 index 00000000..080b213a --- /dev/null +++ b/scripts/duplication-gate.ts @@ -0,0 +1,70 @@ +/** + * Run jscpd 5 over the complete authored source inventory using explicit paths. + * + * The engine no longer implements the older --pattern flag. A one-token pass + * verifies that every module was actually scanned before the normal 50-token + * duplication analysis can report success. Tests and generated output do not + * enter the authored-source denominator. + */ +import { spawnSync } from "node:child_process"; +import { mkdirSync, readFileSync, rmSync } from "node:fs"; +import { createRequire } from "node:module"; +import { dirname, join, resolve } from "node:path"; +import { collectCoverageSources, type CoverageRunner } from "./coverage-gate.ts"; +import { isMainInvocation, nodeToolingExecutable } from "./main-invocation.ts"; + +/** Engine counters validated independently of subprocess exit status. */ +interface DuplicationSummary { + readonly statistics: { readonly total: { readonly sources: number; readonly lines: number; readonly duplicatedLines: number } }; +} + +/** + * Require a complete jscpd analysis and zero duplicated authored lines. + * + * @param root - Repository containing the explicit source inventory. + * @param runner - Injectable subprocess boundary for failure-path tests. + * @returns Zero only for complete clean detection with the pinned engine. + */ +export function runDuplicationGate(root: string, runner: CoverageRunner = spawnSync): number { + try { + const sources = collectCoverageSources(root, "."); + if (sources.length === 0) throw new Error("No source modules exist."); + const engine = join(dirname(createRequire(import.meta.url).resolve("jscpd/package.json")), "run-jscpd.js"); + let lines = 0; + for (const minTokens of [1, 50]) { + const output = join(root, "coverage", `duplication-${minTokens}`); + rmSync(output, { recursive: true, force: true }); + mkdirSync(output, { recursive: true }); + const run = runner(nodeToolingExecutable(process.versions), [engine, "--absolute", "--no-gitignore", "--silent", `--threshold=${minTokens === 1 ? 1000000 : 0}`, + `--min-tokens=${minTokens}`, "--min-lines=1", "--reporters=json", `--output=${output}`, ...sources.map(file => join(root, file))], + { cwd: root, stdio: "inherit" }); + if (run.error || run.status !== 0) throw new Error("Detector failed."); + const report = JSON.parse(readFileSync(join(output, "jscpd-report.json"), "utf8")) as DuplicationSummary; + const counters = report.statistics.total; + if (!Number.isInteger(counters.sources) || !Number.isInteger(counters.lines) || !Number.isInteger(counters.duplicatedLines) || counters.lines <= 0 || counters.duplicatedLines < 0) { + throw new Error("Invalid detector counters."); + } + if (minTokens === 1 && counters.sources !== sources.length) throw new Error("Detector omitted source modules."); + if (minTokens === 50 && counters.duplicatedLines !== 0) throw new Error("Authored source contains duplication."); + lines = counters.lines; + } + console.log(`duplication-gate: ${sources.length} authored modules, ${lines} lines, zero duplicated lines.`); + return 0; + } catch { + console.error("duplication-gate: detector failed, omitted source, or found duplicated lines."); + return 1; + } +} + +/** + * Run the duplication gate when this file is the process entry point. + * + * @param argv - Process argv to compare with the module URL. + * @param moduleUrl - `import.meta.url` of this module. + * @param root - Repository root whose authored sources are scanned. + */ +export function runDuplicationGateIfMain(argv: readonly string[], moduleUrl: string, root: string): void { + if (isMainInvocation(argv, moduleUrl)) process.exitCode = runDuplicationGate(root); +} + +runDuplicationGateIfMain(process.argv, import.meta.url, resolve(import.meta.dirname, "..")); diff --git a/scripts/generate-changelog.ts b/scripts/generate-changelog.ts new file mode 100644 index 00000000..c3506f34 --- /dev/null +++ b/scripts/generate-changelog.ts @@ -0,0 +1,58 @@ +/** Generate canonical PM release history plus explicitly tagged pending work. */ +import { spawnSync, type SpawnSyncOptionsWithStringEncoding, type SpawnSyncReturns } from "node:child_process"; +import { createRequire } from "node:module"; +import { dirname, resolve } from "node:path"; +import { listAllComplete } from "@unbrained/pm-cli/sdk"; +import { isMainInvocation } from "./main-invocation.ts"; + +/** Subprocess boundary for deterministic generator startup and signal diagnostics. */ +export type ChangelogRunner = (command: string, args: readonly string[], options: SpawnSyncOptionsWithStringEncoding) => SpawnSyncReturns; + +/** + * Render complete history without marking unfinished candidate items closed. + * + * Only the explicit `changelog-unreleased` tag admits a nonterminal item. Other + * ongoing work cannot acquire a release entry just because it was claimed. + * + * @param root - Repository containing the tracker and release tags. + * @param args - Optional pm-changelog arguments, including its check flag. + * @param runner - Generator subprocess boundary for deterministic failures. + * @returns The generator's status, or a failure when the complete read fails. + */ +export async function generateChangelog(root: string, args: readonly string[], runner: ChangelogRunner = spawnSync): Promise { + try { + const corpus = await listAllComplete({}, { cwd: root, pmRoot: resolve(root, ".agents/pm"), noExtensions: true }); + const items = corpus.items.filter(item => item.status === "closed" || item.tags?.includes("changelog-unreleased")); + const cli = resolve(dirname(createRequire(import.meta.url).resolve("pm-changelog/package.json")), "dist/cli.js"); + const result = runner("node", [cli, "--stdin", "--pm-root", ".agents/pm", "--status", [...new Set(["closed", ...items.map(item => item.status)])].join(","), + "--mode", "replace", "--output", "CHANGELOG.md", "--all-release-tags", "--release-version-from-package", + "--date-from-version", "--item-url-base", "https://github.com/unbraind/pm-github/blob/main/.agents/pm", + "--respect-item-release", ...args], { + cwd: root, input: JSON.stringify({ items }), encoding: "utf8", maxBuffer: 64 * 1024 * 1024, env: { ...process.env }, + }); + if (result.status === null) { + console.error(result.error ? "changelog: generator could not spawn." : `changelog: generator terminated by ${result.signal}.`); + return 1; + } + process.stdout.write(result.stdout); + process.stderr.write(result.stderr); + return result.status; + } catch { + console.error("changelog: complete tracker read or generator invocation failed."); + return 1; + } +} + +/** + * Generate the changelog when this file is the process entry point. + * + * @param argv - Process argv to compare with the module URL. + * @param moduleUrl - `import.meta.url` of this module. + * @param root - Repository containing the tracker and release tags. + * @param args - Arguments forwarded to the changelog generator. + */ +export async function generateChangelogIfMain(argv: readonly string[], moduleUrl: string, root: string, args: readonly string[]): Promise { + if (isMainInvocation(argv, moduleUrl)) process.exitCode = await generateChangelog(root, args); +} + +await generateChangelogIfMain(process.argv, import.meta.url, resolve(import.meta.dirname, ".."), process.argv.slice(2)); diff --git a/scripts/main-invocation.ts b/scripts/main-invocation.ts index ad890b6d..801aece4 100644 --- a/scripts/main-invocation.ts +++ b/scripts/main-invocation.ts @@ -49,3 +49,19 @@ export function isMainInvocation(argv: readonly string[], moduleUrl: string): bo if (entry === undefined) return false; return realpathSync(entry) === realpathSync(fileURLToPath(moduleUrl)); } + +/** + * Resolve the executable that must run Node-based operational tooling. + * + * The gate scripts spawn Node-only tools (c8, jscpd). Under native Bun the + * host executable cannot run them, so the gates spawn Node itself; under Node + * the current executable is already the right interpreter. The versions object + * is a parameter so the Bun and Node selections are directly observable without + * running under a particular host. + * + * @param versions - The process `versions` record to inspect. + * @returns The executable to spawn for Node-based tooling. + */ +export function nodeToolingExecutable(versions: { readonly bun?: string; readonly node?: string }): string { + return versions.bun ? "node" : process.execPath; +} diff --git a/scripts/privacy-gate.ts b/scripts/privacy-gate.ts index 36598894..483aa2b7 100644 --- a/scripts/privacy-gate.ts +++ b/scripts/privacy-gate.ts @@ -68,6 +68,8 @@ interface Finding { interface FixtureEntry { /** Human-readable justification recorded next to the fixture in review. */ readonly justification: string; + /** Exact retained test blob provenance for a historical synthetic header. */ + readonly historical_test?: { readonly commit: string; readonly path: string }; } /** Path of the identity allowlist, relative to the repository root. */ @@ -128,7 +130,7 @@ function parseAllowlist(root: string): Set { * @param root - Absolute repository root holding `test/fixtures/`. * @returns Map from exact Git blob object id to its recorded justification. */ -function loadFixtureManifest(root: string): Map { +function loadFixtureManifest(root: string): Map { let raw: string; try { raw = readFileSync(join(root, FIXTURE_MANIFEST_PATH), "utf8"); @@ -136,7 +138,7 @@ function loadFixtureManifest(root: string): Map { return new Map(); } const parsed: Record = JSON.parse(raw) as Record; - return new Map(Object.entries(parsed).map(([oid, entry]) => [oid, entry.justification])); + return new Map(Object.entries(parsed)); } /** @@ -153,17 +155,9 @@ function loadFixtureManifest(root: string): Map { */ function listFixtureTreeBlobs(root: string): Set { const dir = dirname(FIXTURE_MANIFEST_PATH); - const result = spawnSync("git", ["ls-tree", "-r", "HEAD", "--", dir], { - cwd: root, - encoding: "utf8", - maxBuffer: 16 * 1024 * 1024, - }); - if (result.error !== undefined) throw result.error; - if (result.status !== 0) { - throw new Error(`git ls-tree HEAD -- ${dir} failed: ${result.stderr.trim()}`); - } + const stdout = runGitQuery(root, `ls-tree HEAD -- ${dir}`, ["ls-tree", "-r", "HEAD", "--", dir], 16 * 1024 * 1024); const oids = new Set(); - for (const line of (result.stdout ?? "").split("\n")) { + for (const line of stdout.split("\n")) { const match = /^\d+ blob ([0-9a-f]{40})\t/.exec(line); if (match?.[1] !== undefined) oids.add(match[1]); } @@ -174,13 +168,31 @@ function listFixtureTreeBlobs(root: string): Set { * Builds the effective fixture exemption set: manifest keys intersected with * the blobs actually present under the fixture directory at HEAD. * + * Historical entries are resolved only for blobs the audited object inventory + * holds: a blob absent from the store needs no exemption, and the commit that + * pins its provenance may be unreachable once a branch is squash-merged. A + * present blob whose provenance does not match still fails closed. + * * @param root - Absolute repository root. + * @param presentBlobs - Blob object ids in the audited object inventory. * @returns Map from exemptable Git blob object id to its justification. */ -function loadFixtureExemptions(root: string): Map { +function loadFixtureExemptions(root: string, presentBlobs: ReadonlySet): Map { const manifest = loadFixtureManifest(root); const fixtureBlobs = listFixtureTreeBlobs(root); - return new Map([...manifest].filter(([oid]) => fixtureBlobs.has(oid))); + for (const [oid, entry] of manifest) { + const source = entry.historical_test; + if (!source || !presentBlobs.has(oid)) continue; + if (!/^[0-9a-f]{40}$/.test(source.commit) || !/^test\/[A-Za-z0-9_-]+\.test\.ts$/.test(source.path)) { + throw new Error("Historical fixture provenance must name an exact commit and a test source."); + } + const resolved = runGitQuery(root, "historical fixture provenance", ["ls-tree", source.commit, "--", source.path], 16 * 1024 * 1024); + if (!resolved.startsWith(`100644 blob ${oid}\t`)) { + throw new Error("Historical fixture provenance does not match the reviewed blob."); + } + fixtureBlobs.add(oid); + } + return new Map([...manifest].filter(([oid]) => fixtureBlobs.has(oid)).map(([oid, entry]) => [oid, entry.justification])); } /** @@ -216,6 +228,34 @@ export function extractEmail(line: string): string | undefined { return line.slice(start + 1, end); } +/** + * Run one read-only git query and return its stdout, with one shared + * fail-closed subprocess boundary. + * + * Every caller needs the same guarantees: a spawn failure (missing git, broken + * PATH) propagates as the subprocess error, a non-zero exit propagates as a + * labeled diagnostic, and a successful run returns decoded stdout (a string — + * spawnSync with an encoding always returns a string for a child that ran). + * Centralizing the checks means the spawn-error and status arms are each + * verified by one audited implementation instead of per-site copies. + * + * @param root - Absolute repository root used as the git working directory. + * @param label - Diagnostic label naming the git query for non-zero exits. + * @param args - Git argument vector. + * @param maxBuffer - Largest expected stdout, in bytes. + * @returns Decoded stdout of the successful git query. + * @throws The spawn error when git cannot be started, or an Error naming the + * query when git exits non-zero. + */ +function runGitQuery(root: string, label: string, args: readonly string[], maxBuffer: number): string { + const result = spawnSync("git", args, { cwd: root, encoding: "utf8", maxBuffer }); + if (result.error !== undefined) throw result.error; + if (result.status !== 0) { + throw new Error(`git ${label} failed: ${result.stderr.trim()}`); + } + return result.stdout; +} + /** * Reads every object in the repository's local store via * `git cat-file --batch-all-objects --batch-check`. @@ -230,21 +270,18 @@ export function extractEmail(line: string): string | undefined { * exits non-zero. */ export function listAllObjects(root: string): [oid: string, type: string][] { - const result = spawnSync("git", ["cat-file", "--batch-all-objects", "--batch-check"], { - cwd: root, - encoding: "utf8", - maxBuffer: 64 * 1024 * 1024, - }); - if (result.error !== undefined) throw result.error; - if (result.status !== 0) { - throw new Error(`git cat-file --batch-all-objects failed: ${result.stderr.trim()}`); - } - return (result.stdout ?? "") + return runGitQuery(root, "cat-file --batch-all-objects", ["cat-file", "--batch-all-objects", "--batch-check"], 64 * 1024 * 1024) .split("\n") .filter((line) => line.trim().length > 0) - .map((line) => { + .map((line): [string, string] => { const parts = line.split(" "); - return [parts[0] ?? "", parts[1] ?? ""] as [string, string]; + // A successful cat-file record is " ". Anything else is + // not a readable object inventory, so the gate fails closed instead of + // skipping the record and auditing a partial store. + if (parts.length < 2 || parts[0] === "" || parts[1] === "") { + throw new Error("git cat-file --batch-all-objects returned an unreadable record"); + } + return [parts[0], parts[1]]; }); } @@ -258,16 +295,7 @@ export function listAllObjects(root: string): [oid: string, type: string][] { * @throws Error when git exits non-zero or cannot be spawned. */ export function readObject(root: string, type: string, oid: string): string { - const result = spawnSync("git", ["cat-file", type, oid], { - cwd: root, - encoding: "utf8", - maxBuffer: 64 * 1024 * 1024, - }); - if (result.error !== undefined) throw result.error; - if (result.status !== 0) { - throw new Error(`git cat-file ${type} ${oid} failed: ${result.stderr.trim()}`); - } - return result.stdout ?? ""; + return runGitQuery(root, `cat-file ${type} ${oid}`, ["cat-file", type, oid], 64 * 1024 * 1024); } /** @@ -332,7 +360,7 @@ export function runGate(root: string): PrivacyGateResult { const findings: Finding[] = []; let exemptions: Map; try { - exemptions = loadFixtureExemptions(root); + exemptions = loadFixtureExemptions(root, new Set(objects.filter(([, type]) => type === "blob").map(([oid]) => oid))); } catch (error) { return { exitCode: 1, @@ -347,7 +375,7 @@ export function runGate(root: string): PrivacyGateResult { // Only the header section (everything before the first blank line) can // carry identity headers; message or tag-message lines that merely // begin with an identity keyword must not enter this check. - const headerSection = content.split("\n\n")[0] ?? ""; + const headerSection = content.split("\n\n")[0]!; // split() always yields at least one part. for (const line of headerSection.split("\n")) { // Match the role keyword without requiring a following space: a // crafted object with `author` or a tab separator must still @@ -403,6 +431,15 @@ export function main(root: string): void { const repoRoot = join(import.meta.dirname, ".."); -if (isMainInvocation(process.argv, import.meta.url)) { - main(repoRoot); +/** + * Run the privacy gate when this file is the process entry point. + * + * @param argv - Process argv to compare with the module URL. + * @param moduleUrl - `import.meta.url` of this module. + * @param root - Repository root to audit. + */ +export function runPrivacyGateIfMain(argv: readonly string[], moduleUrl: string, root: string): void { + if (isMainInvocation(argv, moduleUrl)) main(root); } + +runPrivacyGateIfMain(process.argv, import.meta.url, repoRoot); diff --git a/test/atomic.test.ts b/test/atomic.test.ts index 542b7642..19dc0681 100644 --- a/test/atomic.test.ts +++ b/test/atomic.test.ts @@ -4,6 +4,7 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import test from "node:test"; +import * as sdk from "@unbrained/pm-cli/sdk"; import { CommandError, @@ -305,7 +306,6 @@ test("atomic update and close commit together", async () => { test("a failed atomic batch compensates every applied create", async () => { const root = freshTracker(); try { - const sdk = await import("@unbrained/pm-cli/sdk"); const wrappingCommit = async (options: Parameters[0]) => { const settings = await sdk.readSettings(options.pmRoot); const brokenId = sdk.normalizeItemId("github-broken", settings.id_prefix); @@ -346,7 +346,6 @@ test("a failed atomic batch compensates every applied create", async () => { test("a failed mixed batch restores pre-existing updates and closes", async () => { const root = freshTracker(); try { - const sdk = await import("@unbrained/pm-cli/sdk"); const initialEntries = [entry(30, "Original update target"), entry(31, "Original close target")]; const initial = await importGithubAtomic(root, "acme/widgets", initialEntries); const updateId = initial.itemIds.get(30); diff --git a/test/bounded-cli.test.ts b/test/bounded-cli.test.ts new file mode 100644 index 00000000..9c96ae9c --- /dev/null +++ b/test/bounded-cli.test.ts @@ -0,0 +1,37 @@ +/** The acceptance watchdog refuses stalled processes and preserves durable bytes. */ +import assert from "node:assert/strict"; +import test from "node:test"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { boundedCli } from "./helpers/bounded-cli.ts"; + +test("the acceptance watchdog passes normal exits and force-kills a CPU spin ignoring SIGTERM", async t => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "pm-cli-deadline-")); + t.after(() => fs.rmSync(root, { recursive: true, force: true })); + const tracker = path.join(root, "state.toon"); + fs.writeFileSync(tracker, "id: fixture\ntitle: Durable data\n"); + const before = fs.readFileSync(tracker); + const normal = await boundedCli(process.execPath, ["-e", "console.log('clean')"], root, process.env); + assert.equal(normal.code, 0); + assert.equal(normal.timedOut, false); + assert.match(normal.stdout, /clean/); + const stalled = await boundedCli(process.execPath, ["-e", "process.on('SIGTERM',()=>{}); console.log('ready'); while(true){}"], root, process.env, 500, 100); + assert.match(stalled.stdout, /ready/); + assert.equal(stalled.timedOut, true); + assert.equal(stalled.signal, "SIGKILL"); + assert.ok(stalled.seconds < 3); + assert.deepEqual(fs.readFileSync(tracker), before); +}); + +test("multibyte output split across stream chunks decodes intact", async t => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "pm-cli-utf8-")); + t.after(() => fs.rmSync(root, { recursive: true, force: true })); + // Write the two bytes of "é" in separate writes with a pause between them, + // so the parent receives them as two chunks. + const script = "const b=Buffer.from('é');process.stdout.write(b.subarray(0,1));setTimeout(()=>{process.stdout.write(b.subarray(1));process.stderr.write(b.subarray(0,1));setTimeout(()=>process.stderr.write(b.subarray(1)),100)},100)"; + const result = await boundedCli(process.execPath, ["-e", script], root, process.env); + assert.equal(result.code, 0); + assert.equal(result.stdout, "é"); + assert.equal(result.stderr, "é"); +}); diff --git a/test/comments-sync.test.ts b/test/comments-sync.test.ts index 84b30217..b36baa26 100644 --- a/test/comments-sync.test.ts +++ b/test/comments-sync.test.ts @@ -1,3 +1,4 @@ +import { comments as readComments } from "@unbrained/pm-cli/sdk"; // Unit + workspace integration tests for `--comments-mode` (native comment sync). // // The default (`body`) mode must remain byte-identical to the pre-2026.7.14 @@ -276,7 +277,7 @@ test("syncGithubCommentsToAnnotations populates the native comments collection v assert.strictEqual(skipped, 0); // Read back through the public SDK surface and assert structure. - const { comments: pmComments } = await import("@unbrained/pm-cli/sdk").then((m) => m.comments(id, {}, { pmRoot: root })); + const { comments: pmComments } = await readComments(id, {}, { pmRoot: root }); assert.strictEqual(pmComments.length, 2, "two comments should be stored"); assert.strictEqual(pmComments[0].author, "alice"); assert.strictEqual(pmComments[1].author, "bob"); @@ -309,7 +310,7 @@ test("re-running sync does not duplicate comments (dedupe by GitHub comment id)" assert.strictEqual(r2.added, 1, "only the new comment should be added on re-sync"); assert.strictEqual(r2.skipped, 2, "the two already-synced comments should be skipped"); - const { comments: pmComments } = await import("@unbrained/pm-cli/sdk").then((m) => m.comments(id, {}, { pmRoot: root })); + const { comments: pmComments } = await readComments(id, {}, { pmRoot: root }); assert.strictEqual(pmComments.length, 3, "exactly three comments after re-sync — no duplicates"); } finally { rmSync(root, { recursive: true, force: true }); diff --git a/test/coverage-boundaries.test.ts b/test/coverage-boundaries.test.ts new file mode 100644 index 00000000..4ed3c4e6 --- /dev/null +++ b/test/coverage-boundaries.test.ts @@ -0,0 +1,181 @@ +import assert from "node:assert/strict"; +import { spawnSync } from "node:child_process"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import test, { type TestContext } from "node:test"; +import { createExtensionTestHarness } from "@unbrained/pm-cli/sdk/testing"; +import extension, { formatRateLimit, readPmItems } from "../index.ts"; +import { gitBlobOid, readObject, runGate } from "../scripts/privacy-gate.ts"; +import { captureStderr, withEnv } from "./helpers/mock-github-server.ts"; +import { nodeScenario } from "./helpers/node-scenario.ts"; + +const INDEX_URL = new URL("../index.ts", import.meta.url).href; +const PRIVACY_URL = new URL("../scripts/privacy-gate.ts", import.meta.url).href; + +/** Create a temporary directory whose lifetime follows the test. */ +function directory(t: TestContext): string { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "pm-coverage-boundary-")); + t.after(() => fs.rmSync(root, { recursive: true, force: true })); + return root; +} + +/** Run actual git, with every setup failure asserted before exercising the gate. */ +function git(root: string, args: string[], input?: string): string { + const result = spawnSync("git", args, { cwd: root, encoding: "utf8", input }); + assert.equal(result.status, 0, result.stderr); + return result.stdout.trim(); +} + +/** Start an independent git repository with an approving identity allowlist. */ +function repository(t: TestContext, commit = true): string { + const root = directory(t); + git(root, ["init", "-q", "-b", "main"]); + git(root, ["config", "user.name", "Fixture Bot"]); + git(root, ["config", "user.email", "fixture@example.invalid"]); + fs.mkdirSync(path.join(root, ".github")); + fs.writeFileSync(path.join(root, ".github", "approved-git-identities.txt"), "fixture@example.invalid\n"); + if (commit) { + git(root, ["add", ".github/approved-git-identities.txt"]); + git(root, ["commit", "-qm", "Fixture baseline"]); + } + return root; +} + +/** Write reviewed fixture provenance with exact blob, commit, and source names. */ +function manifest(root: string, value: unknown): void { + const folder = path.join(root, "test", "fixtures", "privacy-gate"); + fs.mkdirSync(folder, { recursive: true }); + fs.writeFileSync(path.join(folder, "manifest.json"), JSON.stringify(value)); +} + +test("secure Windows relaunch rejects missing, malformed, and escaping executable metadata", async t => { + const root = directory(t); + await withEnv({ PM_CLI_PACKAGE_ROOT: undefined }, async () => { + assert.throws(() => readPmItems(root, "win32", undefined), /did not publish/); + }); + assert.throws(() => readPmItems(root, "win32", " "), /did not publish/); + assert.throws(() => readPmItems(root, "win32", root), /Could not read/); + fs.writeFileSync(path.join(root, "package.json"), "{"); + assert.throws(() => readPmItems(root, "win32", root), /Could not read/); + for (const metadata of [null, {}, { bin: "cli.js" }, { bin: {} }, { bin: { pm: 7 } }, { bin: { pm: " " } }]) { + fs.writeFileSync(path.join(root, "package.json"), JSON.stringify(metadata)); + assert.throws(() => readPmItems(root, "win32", root), /does not declare/); + } + fs.writeFileSync(path.join(root, "package.json"), JSON.stringify({ bin: { pm: "../outside.js" } })); + assert.throws(() => readPmItems(root, "win32", root), /outside its package root/); +}); + +test("whole-corpus reads distinguish missing executables, empty diagnostics, invalid JSON, and buffer exhaustion", t => { + const root = directory(t); + const executable = spawnSync("node", ["-p", "process.execPath"], { encoding: "utf8" }).stdout.trim(); + assert.ok(executable); + const binary = path.join(root, "pm"); + nodeScenario(` + import assert from 'node:assert/strict'; + import fs from 'node:fs'; + import { readPmItems } from ${JSON.stringify(INDEX_URL)}; + const root = ${JSON.stringify(root)}; + assert.throws(() => readPmItems(root), /pm list --all failed:/); + for (const [body, expected] of [ + ['process.exit(4);', /pm list --all failed$/], + ['process.stdout.write("invalid-json");', /Could not parse/], + ['process.stdout.write("x".repeat(2 * 1024 * 1024));', /exceeded the 1048576 byte read buffer/], + ]) { + fs.writeFileSync(${JSON.stringify(binary)}, ${JSON.stringify(`#!${executable}\n`)} + body, { mode: 0o755 }); + assert.throws(() => readPmItems(root), expected); + } + `, { PATH: root, PM_JSON_MAX_BUFFER: "1048576" }); +}); + +test("token fallback ignores an empty successful credential helper response", t => { + const root = directory(t); + const executable = spawnSync("node", ["-p", "process.execPath"], { encoding: "utf8" }).stdout.trim(); + fs.writeFileSync(path.join(root, "gh"), `#!${executable}\nprocess.stdout.write(" ");\n`, { mode: 0o755 }); + nodeScenario(` + import assert from 'node:assert/strict'; + import { resolveGitHubToken } from ${JSON.stringify(INDEX_URL)}; + assert.equal(resolveGitHubToken(), undefined); + `, { PATH: root, GITHUB_TOKEN: undefined, GH_TOKEN: undefined }); + assert.equal(formatRateLimit({ remaining: 1, reset: Number.MAX_VALUE, low: true }), "GitHub API quota: 1 remaining"); +}); + +test("registered preflight warns only for mutations lacking a token", async t => { + const root = directory(t); + const ext = await createExtensionTestHarness(extension, { capabilities: ["commands", "importers", "schema", "hooks", "preflight", "search"] }); + const context = { command: "github sync", args: [], options: {}, cwd: root, pm_root: root, global: { json: true }, decision: { + enforce_item_format_gate: true, run_preflight_item_format_sync: true, run_extension_migrations: true, enforce_mandatory_migration_gate: true, + } }; + nodeScenario(` + import assert from 'node:assert/strict'; + import { createExtensionTestHarness } from ${JSON.stringify(new URL("../node_modules/@unbrained/pm-cli/dist/sdk/testing.js", import.meta.url).href)}; + import extension from ${JSON.stringify(INDEX_URL)}; + import { captureStderr } from ${JSON.stringify(new URL("./helpers/mock-github-server.ts", import.meta.url).href)}; + const ext = await createExtensionTestHarness(extension, { capabilities: ['commands', 'importers', 'schema', 'hooks', 'preflight', 'search'] }); + const context = ${JSON.stringify(context)}; + const { stderr } = await captureStderr(() => ext.runPreflightOverride(context)); + assert.match(stderr.join(" "), /no GitHub token is resolvable/); + const preview = await captureStderr(() => ext.runPreflightOverride({ ...context, options: { "dry-run": true } })); + assert.deepEqual(preview.stderr, []); + `, { PATH: "", GITHUB_TOKEN: undefined, GH_TOKEN: undefined }); + await withEnv({ GITHUB_TOKEN: "fixture" + "-token" }, async () => { + const { stderr } = await captureStderr(() => ext.runPreflightOverride(context)); + assert.deepEqual(stderr, []); + }); +}); + +test("privacy history gate refuses an unborn HEAD and malformed fixture manifest", t => { + const unborn = repository(t, false); + assert.match(runGate(unborn).stderr, /fixture exemption resolution failed/); + const root = repository(t); + const folder = path.join(root, "test", "fixtures", "privacy-gate"); + fs.mkdirSync(folder, { recursive: true }); + fs.writeFileSync(path.join(folder, "manifest.json"), "{"); + assert.match(runGate(root).stderr, /fixture exemption resolution failed/); +}); + +test("historical fixture exemptions require an exact test blob and valid provenance", t => { + const root = repository(t); + fs.mkdirSync(path.join(root, "test")); + const secret = "ghp_" + "A".repeat(36); + const content = `// synthetic ${secret}\n`; + fs.writeFileSync(path.join(root, "test", "synthetic.test.ts"), content); + git(root, ["add", "test/synthetic.test.ts"]); + git(root, ["commit", "-qm", "Reviewed fixture"]); + const commit = git(root, ["rev-parse", "HEAD"]); + const oid = gitBlobOid(content); + for (const source of [ + { commit: "invalid", path: "test/synthetic.test.ts" }, + { commit, path: "source.ts" }, + { commit, path: "test/missing.test.ts" }, + { commit: "0".repeat(40), path: "test/synthetic.test.ts" }, + ]) { + manifest(root, { [oid]: { justification: "Reviewed synthetic credential", historical_test: source } }); + assert.match(runGate(root).stderr, /fixture exemption resolution failed/); + } + manifest(root, { [oid]: { justification: "Reviewed synthetic credential", historical_test: { commit, path: "test/synthetic.test.ts" } } }); + assert.equal(runGate(root).exitCode, 0); + manifest(root, {}); + const rejected = runGate(root); + assert.equal(rejected.exitCode, 1); + assert.match(rejected.stderr, /github-token-classic/); + assert.ok(!rejected.stderr.includes(secret)); +}); + +test("git object readers propagate missing tools and nonexistent object errors", t => { + const root = repository(t); + assert.throws(() => readObject(root, "blob", "0".repeat(40)), /git cat-file/); + nodeScenario(` + import assert from 'node:assert/strict'; + import { listAllObjects, readObject } from ${JSON.stringify(PRIVACY_URL)}; + const root = ${JSON.stringify(root)}; + assert.throws(() => listAllObjects(root), /ENOENT/); + assert.throws(() => readObject(root, "blob", "0".repeat(40)), /ENOENT/); + `, { PATH: "" }); +}); + +test("docstring CLI performs its real source scan", () => { + const result = spawnSync("node", ["scripts/docstring-gate.ts"], { cwd: path.resolve(import.meta.dirname, ".."), encoding: "utf8" }); + assert.equal(result.status, 0, result.stderr); + assert.match(result.stdout, /declaration\(s\) documented/); +}); diff --git a/test/coverage-edges.test.ts b/test/coverage-edges.test.ts new file mode 100644 index 00000000..961b6a11 --- /dev/null +++ b/test/coverage-edges.test.ts @@ -0,0 +1,272 @@ +/** Behavior at optional-data boundaries, using real trackers and HTTP responses. */ +import assert from "node:assert/strict"; +import test, { type TestContext } from "node:test"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { spawnSync } from "node:child_process"; +import * as sdk from "@unbrained/pm-cli/sdk"; +import { createExtensionTestHarness } from "@unbrained/pm-cli/sdk/testing"; +import extension, * as github from "../index.ts"; +import { formatGateReport } from "../gate.ts"; +import { captureStderr, jsonResponse, withEnv, withMockGithub } from "./helpers/mock-github-server.ts"; +import { projectItemTag } from "../projects.ts"; + +const harness = createExtensionTestHarness(extension, { capabilities: ["commands", "importers", "schema", "hooks", "preflight", "search"] }); + +function tracker(t: TestContext, git = false): string { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "pm-coverage-edges-")); + t.after(() => fs.rmSync(root, { recursive: true, force: true })); + if (git) { + assert.equal(spawnSync("git", ["init", "-q", "-b", "main", root]).status, 0); + assert.equal(spawnSync("git", ["-C", root, "config", "user.name", "Fixture"]).status, 0); + assert.equal(spawnSync("git", ["-C", root, "config", "user.email", "fixture@example.invalid"]).status, 0); + } + const result = spawnSync("pm", ["--path", git ? path.join(root, ".agents", "pm") : root, "init", "fixture"], { encoding: "utf8", cwd: root }); + assert.equal(result.status, 0, result.stderr); + return git ? path.join(root, ".agents", "pm") : root; +} +function issue(extra: Partial = {}): github.GhIssue { + return { number: 1, title: "Widget", body: null, state: "open", labels: [], assignee: null, milestone: null, + created_at: "", updated_at: "", html_url: "https://example.test/acme/widgets/issues/1", user: null, ...extra }; +} +function entry(extra: Partial = {}): github.PreparedGithubImport { + return { issueNumber: 1, title: "Widget", itemType: "Task", status: "open", description: "d", body: "b", + tags: ["gh:acme/widgets#1"], comments: [], syncAnnotations: false, ...extra }; +} + +async function command(root: string, name: string, options: Record = {}, args: string[] = []) { + return (await harness).runCommand({ command: name, options, args, pmRoot: root, global: { json: false } }); +} + +test("optional provenance and dependency data produce complete deterministic plans", () => { + const bare = { id: "pm-empty" }; + assert.equal(github.indexByProvenance([bare]).size, 0); + assert.equal(github.buildProvenanceIndexFromMetadata([{ id: "", tags: [] }, bare as github.DepLinkSnapshotItem]).size, 0); + assert.equal(github.verifyImportIdempotency([{ id: "" }, bare], [], "acme/widgets").matched_by_provenance, 0); + assert.deepEqual(github.planSync([{ id: "" }, bare, { id: "pm-wrong", tags: ["nonsense", "gh:other/repo#1"] }], "acme/widgets"), []); + const provenance = new Map([["acme/widgets#1", "pm-source"], ["acme/widgets#2", "pm-source"], ["acme/widgets#3", "pm-target"], ["acme/widgets#4", "pm-target"]]); + assert.deepEqual(github.planDependencyLinks("acme/widgets", [issue(), issue({ body: "Blocked by #2, #3, #4" })], provenance).edges.map(e => e.targetId), ["pm-target"]); + assert.equal(github.countDependencyRefCandidates("acme/widgets", [issue()]), 0); + const plan = github.buildExportPlan([bare], "acme/widgets"); + assert.equal(plan[0]?.payload.title, "(untitled)"); + assert.deepEqual(plan[0]?.payload.labels, []); + assert.equal(github.isMutatingGithubCommand("", {}), false); + assert.match(github.buildCommentText({ id: 1, body: "", created_at: "", user: null }), /empty comment/); + assert.match(github.composeBody(issue(), [{ id: 1, body: "", created_at: "", user: null }]), /@unknown/); + assert.equal(github.parseImportOptions({ state: "closed" }).state, "closed"); + assert.equal(github.parseImportOptions({ state: "nonsense" }).state, "open"); + const normalized = (id: string) => id; + for (const match of [undefined, { id: "legacy", status: "open" }]) for (const closedAt of [undefined, "2026-10-01T00:00:00Z"]) { + const mutations = github.buildAtomicImportMutations("acme/widgets", entry({ status: "closed", closedAt, assignee: "fixture", milestone: "Sprint", match }), "pm-", normalized).mutations; + assert.ok(mutations.some(m => m.op === "close")); + assert.equal((mutations[0] as unknown as { options: { assignee: string; sprint: string } }).options.assignee, "fixture"); + assert.equal((mutations[0] as unknown as { options: { sprint: string } }).options.sprint, "Sprint"); + } + assert.match(formatGateReport({ verdict: "fail", source: "git", scanned_files: 1, added_lines: 1, allowlisted: 0, allowlist_path: "", + findings: [{ item_id: "", field: "", rule: "email-address", hash: "a".repeat(64) }] }).join("\n"), /unknown field/); +}); + +test("search corpus falls back to the real complete tracker", async t => { + const root = tracker(t); + const created = await sdk.create({ title: "Corpus item", type: "Task" }, { pmRoot: root }); + assert.equal(github.resolveSearchCorpus(undefined, root)[0]?.id, created.item.id); +}); + +test("empty upstream and gated non-atomic imports retain pass receipts", async t => { + const root = tracker(t, true); + for (const atomic of [false, true]) for (const dryRun of [false, true]) { + const { result } = await captureStderr(() => github.runImport("acme/widgets", root, github.parseImportOptions({ gate: true, atomic, dryRun }), { + resolveToken: () => undefined, fetchIssues: async () => [], + })); + if (!dryRun) assert.ok("gate" in result && result.gate && "verdict" in result.gate && result.gate.verdict === "pass"); + } + await withMockGithub((_req, res) => jsonResponse(res, 200, [issue()]), async () => { + const result = await github.runImport("acme/widgets", root, github.parseImportOptions({ gate: true }), { resolveToken: () => undefined }); + assert.ok("gate" in result && result.gate && "verdict" in result.gate && result.gate.verdict === "pass"); + const second = await github.runImport("acme/widgets", root, github.parseImportOptions({ gate: true }), { resolveToken: () => undefined }); + assert.ok("updated" in second && second.updated === 1); + }); +}); + +test("atomic and plain previews report dependency candidates and PR rendering", async t => { + const root = tracker(t); + for (const atomic of [true, false]) { + const { result, stderr } = await captureStderr(() => github.runImport("acme/widgets", root, github.parseImportOptions({ atomic, dryRun: true, "link-deps": true, "include-prs": true, "comments-mode": "annotations" }), { + resolveToken: () => undefined, fetchIssues: async () => [issue({ pull_request: {}, state: "closed", state_reason: "completed" })], fetchIssueComments: async () => [], + })); + assert.ok("wouldLinkDependencyCandidates" in result && result.wouldLinkDependencyCandidates === 0); + assert.match(stderr.join("\n"), /link-deps/); + } + await assert.rejects(github.runImport("acme/widgets", root, github.parseImportOptions({}), { + resolveToken: () => undefined, fetchIssues: async () => { throw "transport unavailable"; }, + }), /transport unavailable/); +}); + +test("sync scoped empty plans and export preview shapes use real local items", async t => { + const root = tracker(t); + const item = await sdk.create({ title: "Local", type: "Task", tags: "gh:acme/widgets#1,bug" }, { pmRoot: root }); + const unlinked = await sdk.create({ title: "Unlinked", type: "Task" }, { pmRoot: root }); + await withEnv({ GITHUB_TOKEN: "fixture-token", GH_TOKEN: undefined }, async () => { + const { result, stderr } = await captureStderr(() => command(root, "github sync", { repo: "acme/widgets", ids: unlinked.item.id })); + assert.ok(result.result); + assert.match(stderr.join("\n"), /from --ids/); + for (const format of ["json", "md", "markdown"]) { + const preview = await captureStderr(() => command(root, "github export", { format, repo: "acme/widgets", ids: item.item.id, "label-map": "bug=defect" })); + assert.match(preview.stderr.join("\n"), /Scoped to 1/); + assert.match(preview.stderr.join("\n"), /Label map applied/); + } + const preview = await captureStderr(() => command(root, "github export", { format: "markdown" })); + assert.match(preview.stderr.join("\n"), /no --repo/); + assert.match(preview.stderr.join("\n"), /create/); + }); +}); + +test("scoped sync and labelled export apply preserve receipts after partial remote failure", async t => { + const root = tracker(t); + const first = await sdk.create({ title: "Linked", type: "Task", status: "open", tags: "gh:acme/widgets#1" }, { pmRoot: root }); + const second = await sdk.create({ title: "New", type: "Task", tags: "bug" }, { pmRoot: root }); + await withEnv({ GITHUB_TOKEN: "fixture-token", GH_TOKEN: undefined }, async () => { + await withMockGithub((req, res) => { + if (req.method === "GET") jsonResponse(res, 200, { state: "closed" }); + else if (req.method === "PATCH") jsonResponse(res, 200, { number: 1 }); + else jsonResponse(res, 422, { message: "refused" }); + }, async () => { + const preview = await captureStderr(() => command(root, "github sync", { repo: "acme/widgets", ids: first.item.id, "dry-run": true })); + assert.ok(preview.result.result); + const synced = await captureStderr(() => command(root, "github sync", { repo: "acme/widgets", ids: first.item.id, apply: true })); + assert.ok(synced.result.result); + const exported = await captureStderr(() => command(root, "github export", { repo: "acme/widgets", ids: `${first.item.id},${second.item.id}`, apply: true, "label-map": "bug=defect" })); + assert.match(exported.stderr.join("\n"), /1 item\(s\) failed/); + }); + }); +}); + +const meta = { id: "board", title: "Board", url: "https://example.test/board", statusField: { id: "status", name: "Status", options: [{ id: "todo", name: "Todo" }, { id: "done", name: "Done" }] } }; +test("Projects HTTP optional content and cursor pages are normalized by the registered handler", async t => { + const root = tracker(t); + let pages = 0; + await withEnv({ GITHUB_TOKEN: "fixture-token", GH_TOKEN: undefined }, async () => { + await withMockGithub((_req, res, body) => { + const call = JSON.parse(body) as { query: string; variables: { cursor?: string } }; + if (call.query.includes("projectV2(number:")) jsonResponse(res, 200, { data: { user: { projectV2: { ...meta, title: null, url: null, statusField: { id: "status", name: "Status" } } } } }); + else if (call.query.includes("items(first:")) { + pages++; + if (!call.variables.cursor) jsonResponse(res, 200, { data: { node: { items: { pageInfo: { hasNextPage: true, endCursor: "next" }, nodes: [null, + { content: { __typename: "Issue", number: 1, title: null, repository: { nameWithOwner: "acme/widgets" } } }, + { id: "pr", content: { __typename: "PullRequest", number: 2, title: null } }, + { id: "draft", content: { __typename: "DraftIssue" } }, + ] } } } }); + else jsonResponse(res, 200, { data: { node: { items: { pageInfo: { hasNextPage: false } } } } }); + } else jsonResponse(res, 200, { data: { node: { fields: { nodes: [{ __typename: "ProjectV2SingleSelectField", name: "Status", options: [{ name: "Todo" }] }] } } } }); + }, async () => { + const fields = await captureStderr(() => command(root, "github project fields", {}, ["acme/5"])); + assert.match(fields.stderr.join("\n"), /Status.*Todo/); + const imported = await captureStderr(() => command(root, "github project import", { "dry-run": true }, ["acme/5"])); + assert.ok(imported.result.result); + assert.equal(pages, 2); + }); + for (const payload of [{}, { data: null, errors: [] }]) await withMockGithub((_req, res) => jsonResponse(res, 200, payload), async () => { + await assert.rejects(command(root, "github project list", {}, ["acme"]), /GraphQL error.*HTTP 200/); + }); + }); +}); + +test("Projects missing inventory connection returns an empty complete preview", async t => { + const root = tracker(t); + await withEnv({ GITHUB_TOKEN: "fixture-token", GH_TOKEN: undefined }, () => withMockGithub((_req, res, body) => { + const query = (JSON.parse(body) as { query: string }).query; + jsonResponse(res, 200, { data: query.includes("projectV2(number:") ? { user: { projectV2: meta } } : { node: null } }); + }, async () => { + const result = await command(root, "github project import", { "dry-run": true }, ["acme/5"]); + assert.ok(result.result); + })); +}); + +test("Projects existing issue import applies mapped open status", async t => { + const root = tracker(t); + const item = await sdk.create({ title: "Existing", type: "Task", tags: projectItemTag({ owner: "acme", number: 5 }, "linked") }, { pmRoot: root }); + await withEnv({ GITHUB_TOKEN: "fixture-token", GH_TOKEN: undefined }, () => withMockGithub((_req, res, body) => { + const query = (JSON.parse(body) as { query: string }).query; + jsonResponse(res, 200, { data: query.includes("projectV2(number:") ? { user: { projectV2: meta } } : { node: { items: { nodes: [ + { id: "linked", fieldValueByName: { name: "Todo", optionId: "todo" }, content: { __typename: "DraftIssue", title: "Updated" } }, + ], pageInfo: {} } } } }); + }, async () => { + await command(root, "github project import", {}, ["acme/5"]); + const persisted = await sdk.get(item.item.id, {}, { pmRoot: root }); + assert.equal(persisted.item.title, "Updated"); + })); +}); + +test("atomic dependency linking records advisory and mutation failures without discarding import", async t => { + const root = tracker(t); + const { result, stderr } = await captureStderr(() => github.runImport("acme/widgets", root, github.parseImportOptions({ atomic: true, "link-deps": true }), { + resolveToken: () => undefined, + fetchIssues: async () => [issue({ number: 1, body: "Blocked by #2" }), issue({ number: 2, body: "Blocked by #1" })], + })); + assert.ok("linkedDependencies" in result); + assert.match(stderr.join("\n"), /link-deps/); + const items = github.readPmItems(root); + assert.equal(items.length, 2); + const retry = await captureStderr(() => github.runImport("acme/widgets", root, github.parseImportOptions({ atomic: true, "link-deps": true }), { + resolveToken: () => undefined, fetchIssues: async () => [issue({ body: "Blocked by #2" })], + applyDependencyLink: () => ({ ok: false, stderr: "fixture write refused" }), + })); + assert.ok("dependencyLinkFailures" in retry.result && Array.isArray(retry.result.dependencyLinkFailures) && retry.result.dependencyLinkFailures.length === 1); + assert.match(retry.stderr.join("\n"), /link failed/); +}); + +test("export isolates a numbered update failure and search accepts omitted remote hits", async t => { + const root = tracker(t); + const { result } = await captureStderr(() => github.applyExportPlan([{ + action: "update", number: 7, payload: { title: "Linked", body: "", labels: [], state: "open" }, + }], "acme/widgets", undefined, async () => { throw new Error("remote write refused"); })); + assert.equal(result.failures[0]?.number, 7); + await withEnv({ GITHUB_TOKEN: "fixture-token" }, () => withMockGithub((_req, res) => jsonResponse(res, 200, {}), async () => { + const hits = await (await harness).runSearchProvider({ provider: "github", operation: "query", context: { query: "widget", options: { "github-repo": "acme/widgets" }, documents: [], pm_root: root } as never }); + assert.deepEqual(hits, []); + })); +}); + +test("public validate warns at low unauthenticated quota without consulting host credentials", async t => { + const root = tracker(t); + const tools = path.join(root, "tools"); + fs.mkdirSync(tools); + fs.writeFileSync(path.join(tools, "gh"), '#!/bin/sh\nif [ "$1" = "--version" ]; then echo fixture; exit 0; fi\nexit 1\n', { mode: 0o755 }); + await withEnv({ GITHUB_TOKEN: undefined, GH_TOKEN: undefined, PATH: tools + path.delimiter + process.env.PATH }, () => withMockGithub((_req, res) => { + jsonResponse(res, 200, {}, { "x-ratelimit-remaining": "1", "x-ratelimit-limit": "60" }); + }, async () => { + const { stderr } = await captureStderr(() => command(root, "github validate", { repo: "acme/widgets" })); + assert.match(stderr.join("\n"), /raise it \(60→5000\/hr\)/); + })); +}); + +test("validate borrows a synthetic gh credential and forwards it only to the mock API", async t => { + const root = tracker(t); + const tools = path.join(root, "credential-tools"); + fs.mkdirSync(tools); + const token = "fixture-helper-token"; + fs.writeFileSync(path.join(tools, "gh"), `#!/bin/sh +case "$1 $2" in + "--version ") printf 'gh fixture\\n' ;; + "auth token") printf ' ${token} \\n' ;; + *) exit 1 ;; +esac +`, { mode: 0o755 }); + await withEnv({ GITHUB_TOKEN: undefined, GH_TOKEN: undefined, PATH: tools + path.delimiter + process.env.PATH }, () => + withMockGithub((req, res) => { + assert.equal(req.headers.authorization, `Bearer ${token}`); + jsonResponse(res, 200, {}, { "x-ratelimit-remaining": "4999", "x-ratelimit-limit": "5000" }); + }, async server => { + const { result, stderr } = await captureStderr(() => command(root, "github validate", { repo: "acme/widgets" })); + const report = result.result as github.ValidateReport; + assert.equal(report.ok, true); + assert.equal(report.gh_cli, true); + assert.equal(report.token, true); + assert.equal(report.token_source, "gh"); + assert.equal(report.repo_accessible, true); + assert.ok(server.requests.length > 0); + assert.match(stderr.join("\n"), /GitHub token resolved via gh CLI/); + assert.ok(!stderr.join("\n").includes(token), "validation never prints the borrowed credential"); + })); +}); diff --git a/test/coverage-gate.test.ts b/test/coverage-gate.test.ts new file mode 100644 index 00000000..450dcbdb --- /dev/null +++ b/test/coverage-gate.test.ts @@ -0,0 +1,137 @@ +/** Regression tests for the all-source denominator and fail-closed receipts. */ +import assert from "node:assert/strict"; +import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import test from "node:test"; +import { fileURLToPath } from "node:url"; +import { collectCoverageSources, runCoverageGate, runCoverageGateIfMain, type CoverageRunner } from "../scripts/coverage-gate.ts"; +import { nodeToolingExecutable } from "../scripts/main-invocation.ts"; + +/** Create an independently runnable project with one branching source and its test. */ +function coverageFixture(): string { + const root = mkdtempSync(join(tmpdir(), "pm-github-coverage-")); + mkdirSync(join(root, "test")); + writeFileSync(join(root, "package.json"), JSON.stringify({ type: "module", coverageGate: { + sources: ["."], tests: ["test/*.test.ts"], thresholds: { lines: 100, statements: 100, branches: 100, functions: 100 }, ignore: [], + } })); + writeFileSync(join(root, "source.ts"), 'export function choose(value: boolean): string { return value ? "yes" : "no"; }\n'); + writeFileSync(join(root, "test", "source.test.ts"), 'import { choose } from "../source.ts"; import assert from "node:assert/strict"; assert.equal(choose(true), "yes"); assert.equal(choose(false), "no");\n'); + return root; +} + +/** Emit explicit receipt variants to test the boundary independently of c8. */ +function receiptRunner(root: string, variant: "complete" | "partial" | "omitted" | "empty" | "skipped" | "no-lcov"): CoverageRunner { + return () => { + const count = { total: variant === "empty" ? 0 : 1, covered: variant === "partial" || variant === "empty" ? 0 : 1, skipped: variant === "skipped" ? 1 : 0 }; + const entry = Object.fromEntries(["lines", "statements", "functions", "branches"].map(dimension => [dimension, count])); + const sources = variant === "omitted" ? {} : { [join(root, "source.ts")]: entry }; + writeFileSync(join(root, "coverage", "coverage-summary.json"), JSON.stringify({ total: entry, ...sources })); + if (variant !== "no-lcov") writeFileSync(join(root, "coverage", "lcov.info"), "SF:source.ts\nDA:1,1\nend_of_record\n"); + return { status: 0 }; + }; +} + +test("coverage inventory includes operational scripts and JavaScript, without generated data", () => { + const root = coverageFixture(); + try { + mkdirSync(join(root, "scripts")); + mkdirSync(join(root, "dist")); + writeFileSync(join(root, "scripts", "operation.ts"), "export const operation = 1;"); + writeFileSync(join(root, "policy.js"), "export const policy = true;"); + writeFileSync(join(root, "dist", "generated.js"), "export const artifact = true;"); + writeFileSync(join(root, "types.d.ts"), "export interface Data { value: string }"); + assert.deepEqual(collectCoverageSources(root, ".").sort(), ["policy.js", "scripts/operation.ts", "source.ts"]); + assert.deepEqual(collectCoverageSources(root, "source.ts"), ["source.ts"]); + assert.throws(() => collectCoverageSources(root, "types.d.ts")); + assert.throws(() => collectCoverageSources(root, "package.json")); + assert.throws(() => collectCoverageSources(root, "../escape")); + } finally { rmSync(root, { recursive: true, force: true }); } +}); + +test("coverage refuses lower thresholds, ignored sources, narrowed roots, and empty tests", () => { + const root = coverageFixture(); + try { + const baseline = JSON.parse(readFileSync(join(root, "package.json"), "utf8")) as { coverageGate: Record }; + for (const change of [{ thresholds: { lines: 99 } }, { ignore: ["source.ts"] }, { skipDirs: ["scripts"] }, { sources: ["source.ts"] }, { tests: [] }]) { + writeFileSync(join(root, "package.json"), JSON.stringify({ coverageGate: { ...baseline.coverageGate, ...change } })); + assert.equal(runCoverageGate(root), 1); + } + writeFileSync(join(root, "package.json"), "{}"); + assert.equal(runCoverageGate(root), 1); + } finally { rmSync(root, { recursive: true, force: true }); } +}); + +test("coverage invalidates stale receipts and rejects startup, process, and report failures", () => { + const root = coverageFixture(); + try { + mkdirSync(join(root, "coverage")); + for (const failure of [{ status: 1 }, { status: null }, { status: null, error: new Error("unavailable") }]) { + writeFileSync(join(root, "coverage", "lcov.info"), "stale full-coverage receipt"); + assert.equal(runCoverageGate(root, () => failure), 1); + assert.equal(existsSync(join(root, "coverage", "lcov.info")), false); + } + assert.equal(runCoverageGate(root, () => ({ status: 0 })), 1, "missing reports cannot pass"); + for (const variant of ["partial", "omitted", "empty", "skipped", "no-lcov"] as const) { + assert.equal(runCoverageGate(root, receiptRunner(root, variant)), 1, variant); + } + assert.equal(runCoverageGate(root, receiptRunner(root, "complete")), 0); + } finally { rmSync(root, { recursive: true, force: true }); } +}); + +test("coverage treats a missing exemption list as empty and rejects an LCOV receipt that omits a source", () => { + const root = coverageFixture(); + try { + const manifest = JSON.parse(readFileSync(join(root, "package.json"), "utf8")) as { coverageGate: Record }; + delete manifest.coverageGate.ignore; + delete manifest.coverageGate.skipDirs; + writeFileSync(join(root, "package.json"), JSON.stringify(manifest)); + assert.equal(runCoverageGate(root, () => ({ status: 1 })), 1, "omitted exemption lists are not a configuration failure"); + const count = { total: 1, covered: 1, skipped: 0 }; + const entry = Object.fromEntries(["lines", "statements", "functions", "branches"].map(dimension => [dimension, count])); + assert.equal(runCoverageGate(root, () => { + writeFileSync(join(root, "coverage", "coverage-summary.json"), JSON.stringify({ total: entry, [join(root, "source.ts")]: entry })); + writeFileSync(join(root, "coverage", "lcov.info"), "SF:other.ts\nDA:1,1\nend_of_record\n"); + return { status: 0 }; + }), 1); + assert.equal(existsSync(join(root, "coverage", "lcov.info")), false); + } finally { rmSync(root, { recursive: true, force: true }); } +}); + +test("node tooling selects Node under Bun and the current executable otherwise", () => { + assert.equal(nodeToolingExecutable({ bun: "1.3.5" }), "node"); + assert.equal(nodeToolingExecutable({}), process.execPath); +}); + +test("coverage gate main entry runs against the supplied root", () => { + const root = mkdtempSync(join(tmpdir(), "pm-github-coverage-main-")); + const previous = process.exitCode; + try { + const moduleUrl = new URL("../scripts/coverage-gate.ts", import.meta.url).href; + runCoverageGateIfMain(["node", fileURLToPath(moduleUrl)], moduleUrl, root); + assert.equal(process.exitCode, 1); + } finally { + process.exitCode = previous ?? 0; + rmSync(root, { recursive: true, force: true }); + } +}); + +test("real c8 requires both branches and rejects a completely unloaded authored module", () => { + const root = coverageFixture(); + try { + assert.equal(runCoverageGate(root), 0); + writeFileSync(join(root, "unloaded.ts"), "export function uncovered(): string { return 'never loaded'; }\n"); + assert.equal(runCoverageGate(root), 1); + assert.equal(existsSync(join(root, "coverage", "lcov.info")), false); + } finally { rmSync(root, { recursive: true, force: true }); } +}); + +test("coverage inventory accepts dot-dot names inside the repository and rejects parent escape", () => { + const root = coverageFixture(); + try { + mkdirSync(join(root, "..cache")); + writeFileSync(join(root, "..cache", "module.ts"), "export const value = 1;\n"); + assert.deepEqual(collectCoverageSources(root, "..cache"), ["..cache/module.ts"]); + assert.throws(() => collectCoverageSources(root, ".."), /escapes the repository/); + } finally { rmSync(root, { recursive: true, force: true }); } +}); diff --git a/test/coverage-remainder.test.ts b/test/coverage-remainder.test.ts new file mode 100644 index 00000000..a4535c1a --- /dev/null +++ b/test/coverage-remainder.test.ts @@ -0,0 +1,806 @@ +/** + * Remaining reachable command, import, and planner paths. + * + * These cases use real temporary git repositories, the installed pm CLI, and + * the local HTTP stand-in. Injected dependencies are the production seams + * (fetch, commit, pm subprocess via PATH), not substitutes for the unit. + */ +import assert from "node:assert/strict"; +import test from "node:test"; +import { spawn, spawnSync } from "node:child_process"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +import { createExtensionTestHarness, type ExtensionTestHarness } from "@unbrained/pm-cli/sdk/testing"; + +import extension, { + applyClientFilters, + applyExportPlan, + buildIssuesUrl, + collectProjectsV2Pages, + importGithubAtomic, + importCommentSyncLockPath, + linkImportedDependencies, + listOwnerProjectsV2Nodes, + parseImportOptions, + runImport, + syncGithubCommentsToAnnotations, + type GhIssue, + type ImportOptions, +} from "../index.ts"; +import { nodeScenario } from "./helpers/node-scenario.ts"; +import { withReadOnlyDirectory } from "./helpers/read-only-directory.ts"; + +// Root ignores directory write permissions, so read-only-directory fixtures +// cannot produce their write failures there; skip them visibly in that case. +const runsAsRoot = typeof process.getuid === "function" && process.getuid() === 0; +const nonRootOnly = { skip: runsAsRoot && "permission-based fixture: root ignores directory write permissions" }; +import { projectItemTag } from "../projects.ts"; +import { captureStderr, jsonResponse, withEnv, withMockGithub } from "./helpers/mock-github-server.ts"; + +const harnessPromise: Promise = createExtensionTestHarness(extension, { + capabilities: ["commands", "importers", "schema", "hooks", "preflight", "search"], +}); + +const REAL_PM = fileURLToPath(new URL("../node_modules/.bin/pm", import.meta.url)); + +function issue(overrides: Partial = {}): GhIssue { + return { + number: 7, + title: "Widget", + body: "clean body", + state: "open", + labels: [{ name: "bug" }, { name: "" }], + assignee: null, + milestone: null, + created_at: "", + updated_at: "", + html_url: "https://example.test/acme/widgets/issues/7", + user: null, + ...overrides, + }; +} + +function opts(extra: Record = {}): ImportOptions { + return parseImportOptions(extra); +} + +function gitTracker(prefix: string): string { + const root = fs.mkdtempSync(path.join(os.tmpdir(), prefix)); + assert.equal(spawnSync("git", ["init", "-q", "-b", "main", root]).status, 0); + assert.equal(spawnSync("git", ["-C", root, "config", "user.email", "fixture@example.com"]).status, 0); + assert.equal(spawnSync("git", ["-C", root, "config", "user.name", "Fixture"]).status, 0); + const tracker = path.join(root, ".agents", "pm"); + fs.mkdirSync(tracker, { recursive: true }); + const init = spawnSync(REAL_PM, ["--path", tracker, "init", "test"], { encoding: "utf8", cwd: root }); + assert.equal(init.status, 0, init.stderr); + return root; +} + +async function withFakePm(body: string, fn: () => Promise): Promise { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pm-github-fake-pm-")); + fs.writeFileSync(path.join(dir, "pm"), `#!/bin/sh\n${body}\n`, { mode: 0o755 }); + const previousPath = process.env.PATH; + const previousReal = process.env.REAL_PM; + process.env.PATH = `${dir}${path.delimiter}${previousPath ?? ""}`; + process.env.REAL_PM = REAL_PM; + try { + await fn(); + } finally { + if (previousPath === undefined) delete process.env.PATH; + else process.env.PATH = previousPath; + if (previousReal === undefined) delete process.env.REAL_PM; + else process.env.REAL_PM = previousReal; + fs.rmSync(dir, { recursive: true, force: true }); + } +} + +test("client filters and issue URL query keep optional import constraints", () => { + const open = issue({ number: 1, milestone: { title: "Now" } }); + const other = issue({ number: 2, milestone: { title: "Later" }, pull_request: {}, draft: true }); + const filtered = applyClientFilters([open, other], opts({ milestone: "Now", "include-prs": true, "skip-drafts": true })); + assert.deepEqual(filtered.map((item) => item.number), [1]); + const url = buildIssuesUrl("Acme/Widgets", opts({ labels: "bug", since: "2026-01-01T00:00:00Z", assignee: "octo", all: true })); + assert.match(url, /labels=bug/); + assert.match(url, /since=/); + assert.match(url, /assignee=octo/); + assert.match(url, /state=all/); +}); + +test("atomic import falls back when settings are unreadable and reports interruption", async () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "pm-github-atomic-settings-")); + try { + const entry = { + issueNumber: 7, + title: "Widget", + itemType: "Task", + status: "open", + description: "d", + body: "b", + tags: ["gh:acme/widgets#7"], + comments: [], + syncAnnotations: false, + }; + const committed = await importGithubAtomic(root, "acme/widgets", [entry], { + readSettings: async () => { throw new Error("settings unreadable"); }, + normalizeItemId: (value) => value, + commitItemMutations: async () => ({ status: "committed", recovered: false, transactionId: "tx", results: {} }), + }); + assert.equal(committed.recovered, false); + const interrupted = new Error("stopped"); + interrupted.name = "WorkspaceTransactionInterruptedError"; + await assert.rejects( + importGithubAtomic(root, "acme/widgets", [entry], { + readSettings: async () => ({}), + normalizeItemId: (value) => value, + commitItemMutations: async () => { throw interrupted; }, + }), + /interrupted/, + ); + await assert.rejects( + importGithubAtomic(root, "acme/widgets", [entry], { + readSettings: async () => ({}), + normalizeItemId: (value) => value, + commitItemMutations: async () => { throw "plain failure"; }, + }), + /plain failure/, + ); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } +}); + +test("resolveGitHubToken does not throw when gh cannot be spawned", () => { + nodeScenario(` + import assert from 'node:assert/strict'; + import { resolveGitHubToken } from ${JSON.stringify(new URL("../index.ts", import.meta.url).href)}; + assert.equal(resolveGitHubToken(), undefined); + `, { GITHUB_TOKEN: undefined, GH_TOKEN: undefined, PATH: "" }); +}); + +test("project page collection stops on a missing connection, null nodes, and a cursorless next page", async () => { + const pages = await collectProjectsV2Pages(async (cursor) => { + if (cursor === undefined) return { nodes: [null, { number: 1, title: "A", url: "u", closed: false }], pageInfo: { hasNextPage: true, endCursor: "c1" } }; + if (cursor === "c1") return { pageInfo: { hasNextPage: true } }; + return null; + }); + assert.equal(pages.length, 1); + const org = await listOwnerProjectsV2Nodes("acme", async () => ({ + user: null, + organization: { projectsV2: { pageInfo: { hasNextPage: false }, nodes: [{ number: 3, title: "Org", url: "o", closed: true }] } }, + })); + assert.equal(org[0]?.number, 3); +}); + +test("export apply records a non-Error failure and an update missing its number", async () => { + const { stderr, result } = await captureStderr(() => applyExportPlan([ + { action: "update", id: "pm-1", payload: { title: "T", body: "", labels: [], state: "open" } }, + { action: "create", payload: { title: "C", body: "", labels: [], state: "open" } }, + ], "acme/widgets", "tok", async () => { throw "exporter down"; })); + assert.equal(result.failed, 2); + assert.match(stderr.join("\n"), /exporter down/); + assert.match(stderr.join("\n"), /missing its GitHub issue number/); +}); + +test("dependency linking reports a non-Error snapshot failure and a detector failure", async () => { + const skipped = await linkImportedDependencies("acme/widgets", [issue({ body: "Blocked by #2" })], "unused", { + listItemMetadata: async () => { throw "metadata down"; }, + }); + assert.match(skipped.failures[0] ?? "", /metadata down/); + const advised = await linkImportedDependencies("acme/widgets", [issue({ number: 1, body: "Blocked by #2" })], "unused", { + listItemMetadata: async () => [ + { id: "pm-1", tags: ["gh:acme/widgets#1"], status: "open" }, + { id: "pm-2", tags: ["gh:acme/widgets#2"], status: "open" }, + ], + applyDependencyLink: () => ({ ok: true, stderr: "" }), + collectOrderingCycleWarnings: () => { throw "detector down"; }, + }); + assert.deepEqual(advised.failures, ["ordering-cycle advisory skipped: detector down"]); + assert.deepEqual(advised.orderingCycleWarnings, []); + assert.equal(advised.linked, 1); +}); + +test("ungated import skips a blank title, reports comment-fetch failure, and previews link-deps", async () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "pm-github-import-preview-")); + try { + const { stderr, result } = await captureStderr(() => runImport("acme/widgets", root, opts({ "link-deps": true, dryRun: true, "with-comments": true }), { + resolveToken: () => "tok", + readItems: () => [], + fetchIssues: async () => [issue({ title: " ", number: 1 }), issue({ number: 2, body: "Blocked by #9" })], + fetchIssueComments: async () => { throw "comments down"; }, + })); + assert.ok("dryRun" in result); + assert.equal(result.dryRun, true); + assert.match(stderr.join("\n"), /failed to fetch comments/); + assert.match(stderr.join("\n"), /comments down/); + assert.match(stderr.join("\n"), /link-deps/); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } +}); + +test("gated atomic import reports a recovered journal and fails closed after a post-write leak", async () => { + const root = gitTracker("pm-github-gate-write-"); + try { + const recovered = await captureStderr(() => runImport("acme/widgets", path.join(root, ".agents", "pm"), opts({ atomic: true }), { + resolveToken: () => "tok", + readItems: () => [], + fetchIssues: async () => [issue()], + commitAtomic: async () => ({ + transactionId: "tx-recovered", + recovered: true, + imported: 0, + updated: 0, + recoveredItems: 1, + itemIds: new Map([[7, "pm-recovered"]]), + }), + })); + assert.match(recovered.stderr.join("\n"), /recovered transaction tx-recovered/); + + await assert.rejects(captureStderr(() => runImport("acme/widgets", path.join(root, ".agents", "pm"), opts({ atomic: true, gate: true }), { + resolveToken: () => "tok", + fetchIssues: async () => [issue({ body: "clean" })], + commitAtomic: async (tracker, repo, entries) => { + const receipt = await importGithubAtomic(tracker, repo, entries); + fs.writeFileSync(path.join(root, ".agents", "pm", "issues", "leak.txt"), `body: "token ${"ghp_" + "b".repeat(36)}"\n`); + return receipt; + }, + })), /FAIL/); + fs.rmSync(path.join(root, ".agents", "pm", "issues", "leak.txt")); + + await assert.rejects(captureStderr(() => runImport("acme/widgets", path.join(root, ".agents", "pm"), opts({ atomic: true, gate: true }), { + resolveToken: () => "tok", + fetchIssues: async () => [issue({ body: "still clean" })], + commitAtomic: async (tracker, repo, entries) => { + const receipt = await importGithubAtomic(tracker, repo, entries); + fs.rmSync(path.join(root, ".git"), { recursive: true, force: true }); + return receipt; + }, + })), /could not read the proposed tracker change|not inside a Git work tree/); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } +}); + +test("a gated non-atomic import fails closed when a planned write fails", async () => { + // The close reconciliation is refused: the item keeps its provenance tag, so + // only the write count can show that the tracker no longer matches the plan. + const root = gitTracker("pm-github-gated-write-fail-"); + try { + await withFakePm(` + for arg in "$@"; do + if [ "$arg" = "close" ]; then echo close-refused >&2; exit 1; fi + done + exit 0 + `, async () => { + await captureStderr(() => assert.rejects(runImport("acme/widgets", root, opts({ gate: true }), { + resolveToken: () => "tok", + readItems: () => [ + { id: "pm-existing", title: "old", status: "open", tags: ["gh:acme/widgets#8"] }, + { id: "pm-clean", title: "old", status: "open", tags: ["gh:acme/widgets#9"] }, + ], + // #9 updates cleanly, so the import is not an all-failed run; only the + // write count can show that #8's close never happened. + fetchIssues: async () => [ + issue({ number: 8, state: "closed", state_reason: "completed", closed_at: "2026-02-01T00:00:00Z" }), + issue({ number: 9 }), + ], + }), /planned write\(s\) failed/)); + }); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } +}); + +// A gated --comments-mode annotations|both import must be all-or-nothing for +// native comments too: a contended comment-sync lock, a refused per-comment +// add, and an unparsable created id all leave planned comments unwritten while +// every other check stays green, so the plan-divergence refusal is the only +// thing that can fail the run closed. The ungated twins pin the unchanged +// warn-and-continue behaviour. + +test("a gated import fails closed when the comment-sync lock is contended", async () => { + const root = gitTracker("pm-github-gated-lock-"); + const holder = spawn(process.execPath, ["-e", "setTimeout(() => {}, 60000)"], { stdio: "ignore" }); + try { + assert.ok(holder.pid && holder.pid > 0); + const lockPath = importCommentSyncLockPath(root, "pm-existing"); + fs.mkdirSync(path.dirname(lockPath), { recursive: true }); + fs.writeFileSync(lockPath, JSON.stringify({ pid: holder.pid, token: "held", created_at: new Date().toISOString() }) + "\n"); + await withFakePm(`exit 0`, async () => { + const gated = await captureStderr(() => assert.rejects(runImport("acme/widgets", root, opts({ gate: true, "comments-mode": "annotations" }), { + resolveToken: () => "tok", + readItems: () => [{ id: "pm-existing", title: "old", status: "open", tags: ["gh:acme/widgets#7"] }], + fetchIssues: async () => [issue({ comments: 1 })], + fetchIssueComments: async () => [{ id: 3, user: { login: "octo" }, created_at: "2026-01-01T00:00:00Z", body: "held note" }], + }), /planned write\(s\) failed/)); + assert.match(gated.stderr.join("\n"), /another import holds the comment-sync lock/); + }); + } finally { + holder.kill("SIGKILL"); + fs.rmSync(root, { recursive: true, force: true }); + } +}); + +test("a gated import fails closed when a per-comment add fails mid-sync", nonRootOnly, async () => { + const root = gitTracker("pm-github-gated-add-fail-"); + const tracker = path.join(root, ".agents", "pm"); + const created = spawnSync(REAL_PM, ["--path", tracker, "create", "task", "Commented", "--tags", "gh:acme/widgets#7", "--description", "d"], { encoding: "utf8" }); + assert.equal(created.status, 0, created.stderr); + const listed = spawnSync(REAL_PM, ["--pm-path", root, "--json", "list", "--full"], { encoding: "utf8" }); + const itemId = (JSON.parse(listed.stdout) as { items?: Array<{ id: string }> }).items?.[0]?.id; + assert.ok(itemId); + const syncedComment = { id: 21, user: { login: "octo" }, created_at: "2026-01-01T00:00:00Z", body: "already synced" }; + const failingComment = { id: 22, user: { login: "octo" }, created_at: "2026-01-02T00:00:00Z", body: "cannot land" }; + // Land the first comment while the tasks directory is still writable so the + // gated run below proves the loop continues past it: one skip, one failure. + await syncGithubCommentsToAnnotations(itemId, [syncedComment], tracker, 7); + const tasksDir = path.join(tracker, "tasks"); + try { + await withReadOnlyDirectory(tasksDir, async () => { + await withFakePm(`exit 0`, async () => { + const gated = await captureStderr(() => assert.rejects(runImport("acme/widgets", root, opts({ gate: true, "comments-mode": "annotations" }), { + resolveToken: () => "tok", + readItems: () => [{ id: itemId, title: "old", status: "open", tags: ["gh:acme/widgets#7"] }], + fetchIssues: async () => [issue({ comments: 2 })], + fetchIssueComments: async () => [syncedComment, failingComment], + }), /planned write\(s\) failed/)); + assert.match(gated.stderr.join("\n"), /comment 22 sync failed/); + }); + }); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } +}); + +test("a gated import fails closed when a created id cannot be parsed with annotations sync on", async () => { + const root = gitTracker("pm-github-gated-unparsed-"); + try { + await withFakePm(` + json=0 + for arg in "$@"; do + if [ "$arg" = "--json" ]; then json=1; fi + done + if [ "$json" = "1" ]; then printf '%s\\n' 'not-json'; exit 0; fi + exit 0 + `, async () => { + const gated = await captureStderr(() => assert.rejects(runImport("acme/widgets", root, opts({ gate: true, "comments-mode": "annotations" }), { + resolveToken: () => "tok", + readItems: () => [], + fetchIssues: async () => [issue({ comments: 1 })], + fetchIssueComments: async () => [{ id: 31, user: { login: "octo" }, created_at: "2026-01-01T00:00:00Z", body: "note" }], + }), /planned write\(s\) failed/)); + assert.match(gated.stderr.join("\n"), /could not parse created item id — comments not synced/); + }); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } +}); + +test("a gated atomic import fails closed when committed items lack their planned comments", async () => { + const root = gitTracker("pm-github-gated-atomic-comments-"); + const tracker = path.join(root, ".agents", "pm"); + try { + await assert.rejects(captureStderr(() => runImport("acme/widgets", tracker, opts({ atomic: true, gate: true, "comments-mode": "annotations" }), { + resolveToken: () => "tok", + readItems: () => [{ id: "pm-fresh", title: "t", status: "open", tags: ["gh:acme/widgets#7"] }], + fetchIssues: async () => [issue({ comments: 1 })], + fetchIssueComments: async () => [{ id: 41, user: { login: "octo" }, created_at: "2026-01-01T00:00:00Z", body: "note" }], + // The commit "succeeds" but routes no created id, so the planned comment + // is unreachable — only the comment-failure count can fail the run closed. + commitAtomic: async () => ({ transactionId: "tx-comments", recovered: false, imported: 1, updated: 0, itemIds: new Map() }), + })), /planned write\(s\) failed/); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } +}); + +test("the same comment-sync failures without --gate still import with a warning", nonRootOnly, async () => { + const root = gitTracker("pm-github-ungated-comments-"); + const tracker = path.join(root, ".agents", "pm"); + const created = spawnSync(REAL_PM, ["--path", tracker, "create", "task", "Commented", "--tags", "gh:acme/widgets#7", "--description", "d"], { encoding: "utf8" }); + assert.equal(created.status, 0, created.stderr); + const listed = spawnSync(REAL_PM, ["--pm-path", root, "--json", "list", "--full"], { encoding: "utf8" }); + const itemId = (JSON.parse(listed.stdout) as { items?: Array<{ id: string }> }).items?.[0]?.id; + assert.ok(itemId); + const failingComment = { id: 52, user: { login: "octo" }, created_at: "2026-01-01T00:00:00Z", body: "cannot land" }; + const holder = spawn(process.execPath, ["-e", "setTimeout(() => {}, 60000)"], { stdio: "ignore" }); + try { + assert.ok(holder.pid && holder.pid > 0); + const lockPath = importCommentSyncLockPath(root, itemId); + fs.mkdirSync(path.dirname(lockPath), { recursive: true }); + fs.writeFileSync(lockPath, JSON.stringify({ pid: holder.pid, token: "held", created_at: new Date().toISOString() }) + "\n"); + await withFakePm(`exit 0`, async () => { + const contended = await captureStderr(() => runImport("acme/widgets", root, opts({ "comments-mode": "annotations" }), { + resolveToken: () => "tok", + readItems: () => [{ id: itemId, title: "old", status: "open", tags: ["gh:acme/widgets#7"] }], + fetchIssues: async () => [issue({ comments: 1 })], + fetchIssueComments: async () => [failingComment], + })); + assert.ok("updated" in contended.result && contended.result.updated === 1); + assert.match(contended.stderr.join("\n"), /another import holds the comment-sync lock/); + }); + fs.rmSync(lockPath, { force: true }); + const tasksDir = path.join(tracker, "tasks"); + await withReadOnlyDirectory(tasksDir, async () => { + await withFakePm(`exit 0`, async () => { + const refused = await captureStderr(() => runImport("acme/widgets", root, opts({ "comments-mode": "annotations" }), { + resolveToken: () => "tok", + readItems: () => [{ id: itemId, title: "old", status: "open", tags: ["gh:acme/widgets#7"] }], + fetchIssues: async () => [issue({ comments: 1 })], + fetchIssueComments: async () => [failingComment], + })); + assert.ok("updated" in refused.result && refused.result.updated === 1); + assert.match(refused.stderr.join("\n"), /comment 52 sync failed/); + }); + }); + } finally { + holder.kill("SIGKILL"); + fs.rmSync(root, { recursive: true, force: true }); + } +}); + +test("non-atomic import reports update, close, reopen, and unparsed-id failures", async () => { + const root = gitTracker("pm-github-import-fail-"); + const listed = spawnSync(REAL_PM, ["--pm-path", root, "--json", "list", "--full"], { encoding: "utf8" }); + assert.equal(listed.status, 0, listed.stderr); + try { + await withFakePm(` + for arg in "$@"; do + if [ "$arg" = "update" ]; then echo update-refused >&2; exit 1; fi + done + exit 0 + `, async () => { + const { stderr } = await captureStderr(() => assert.rejects(runImport("acme/widgets", root, opts(), { + resolveToken: () => "tok", + readItems: () => [{ id: "pm-existing", title: "old", status: "open", tags: ["gh:acme/widgets#7"] }], + fetchIssues: async () => [issue({ assignee: { login: "octo" }, milestone: { title: "Now" } })], + }), /Imported 0 issue/)); + + assert.match(stderr.join("\n"), /update failed/); + }); + + await withFakePm(` + for arg in "$@"; do + if [ "$arg" = "close" ]; then echo close-refused >&2; exit 1; fi + done + exit 0 + `, async () => { + const closed = await captureStderr(() => assert.rejects(runImport("acme/widgets", root, opts(), { + resolveToken: () => "tok", + readItems: () => [{ id: "pm-existing", title: "old", status: "open", tags: ["gh:acme/widgets#8"] }], + fetchIssues: async () => [issue({ number: 8, state: "closed", state_reason: "completed", closed_at: "2026-02-01T00:00:00Z" })], + }), /Imported 0 issue/)); + assert.match(closed.stderr.join("\n"), /close reconciliation failed|close after import failed/); + }); + + await withFakePm(` + for arg in "$@"; do + if [ "$arg" = "open" ]; then echo reopen-refused >&2; exit 1; fi + done + exit 0 + `, async () => { + const reopened = await captureStderr(() => assert.rejects(runImport("acme/widgets", root, opts(), { + resolveToken: () => "tok", + readItems: () => [{ id: "pm-existing", title: "old", status: "closed", tags: ["gh:acme/widgets#9"] }], + fetchIssues: async () => [issue({ number: 9, state: "open" })], + }), /Imported 0 issue/)); + assert.match(reopened.stderr.join("\n"), /reopen reconciliation failed/); + }); + + await withFakePm(` + json=0 + for arg in "$@"; do + if [ "$arg" = "--json" ]; then json=1; fi + done + if [ "$json" = "1" ]; then printf '%s\\n' 'not-json'; exit 0; fi + exit 0 + `, async () => { + const parsed = await captureStderr(() => assert.rejects(runImport("acme/widgets", root, opts({ "comments-mode": "annotations" }), { + resolveToken: () => "tok", + readItems: () => [], + fetchIssues: async () => [issue({ number: 10, state: "closed", comments: 1 })], + fetchIssueComments: async () => [{ id: 1, user: { login: "octo" }, created_at: "2026-01-01T00:00:00Z", body: "note" }], + }), /Imported 0 issue/)); + assert.match(parsed.stderr.join("\n"), /could not parse created item id/); + }); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } +}); + +test("comment sync skips a contended lock and reports a per-comment write failure", nonRootOnly, async () => { + const root = gitTracker("pm-github-comments-"); + const holder = spawn(process.execPath, ["-e", "setTimeout(() => {}, 60000)"], { stdio: "ignore" }); + try { + assert.ok(holder.pid && holder.pid > 0); + const created = spawnSync(REAL_PM, ["--path", root, "create", "task", "Commented", "--description", "d"], { encoding: "utf8" }); + assert.equal(created.status, 0, created.stderr); + const listed = spawnSync(REAL_PM, ["--pm-path", root, "--json", "list", "--full"], { encoding: "utf8" }); + const items = JSON.parse(listed.stdout) as { items?: Array<{ id: string }> }; + const itemId = items.items?.[0]?.id; + assert.ok(itemId); + const lockPath = importCommentSyncLockPath(root, itemId); + fs.mkdirSync(path.dirname(lockPath), { recursive: true }); + fs.writeFileSync(lockPath, JSON.stringify({ pid: holder.pid, token: "held", created_at: new Date().toISOString() }) + "\n"); + const contended = await captureStderr(() => syncGithubCommentsToAnnotations(itemId, [ + { id: 4, user: { login: "octo" }, created_at: "2026-01-01T00:00:00Z", body: "held" }, + ], path.join(root, ".agents", "pm"), 7)); + assert.match(contended.stderr.join("\n"), /another import holds the comment-sync lock/); + fs.rmSync(lockPath, { force: true }); + + const itemFile = fs.readdirSync(path.join(root, ".agents", "pm", "tasks"))[0]; + assert.ok(itemFile); + const tasksDir = path.join(root, ".agents", "pm", "tasks"); + await withReadOnlyDirectory(tasksDir, async () => { + const failed = await captureStderr(() => syncGithubCommentsToAnnotations(itemId, [ + { id: 5, body: "", user: null, created_at: "" }, + ], path.join(root, ".agents", "pm"), 7)); + assert.equal(failed.result.added, 0); + assert.match(failed.stderr.join("\n"), /sync failed/); + }); + } finally { + holder.kill("SIGKILL"); + fs.rmSync(root, { recursive: true, force: true }); + } +}); + +test("registered commands print text diagnostics and reject empty project and export selectors", async () => { + const ext = await harnessPromise; + await withEnv({ GITHUB_TOKEN: "tok", GH_TOKEN: undefined, PM_GITHUB_SYNC: undefined }, async () => { + await assert.rejects( + ext.runCommand({ command: "github project list", args: [], options: {}, global: { json: true } }), + /Usage: pm github project list/, + ); + await assert.rejects( + ext.runCommand({ command: "github export", options: { ids: "" }, global: { json: true }, pmRoot: fs.mkdtempSync(path.join(os.tmpdir(), "pm-github-export-empty-")) }), + /--ids requires at least one/, + ); + + const listed = await captureStderr(() => withMockGithub((_req, res) => { + jsonResponse(res, 200, { data: { + user: { projectsV2: { pageInfo: { hasNextPage: false }, nodes: [ + { number: 5, title: null, url: null, closed: true, shortDescription: null }, + { number: 6, title: "Open", url: "", closed: false }, + ] } }, + organization: null, + } }); + }, () => ext.runCommand({ command: "github project list", args: ["acme"], global: { json: false } }))); + assert.match(listed.stderr.join("\n"), /Projects for acme/); + assert.match(listed.stderr.join("\n"), /\[closed\]/); + + const fields = await captureStderr(() => withMockGithub((_req, res) => { + jsonResponse(res, 200, { data: { + user: { projectV2: { id: "PVT", title: "Board", url: "https://example.test/board", statusField: null } }, + organization: null, + node: { fields: { nodes: [ + { __typename: "ProjectV2FieldCommon", name: "Notes", dataType: "TEXT" }, + null, + ] } }, + } }); + }, () => ext.runCommand({ command: "github project fields", args: ["acme/5"], global: { json: false } }))); + assert.match(fields.stderr.join("\n"), /Status field: \(none/); + + const validated = await captureStderr(() => withMockGithub((_req, res) => { + jsonResponse(res, 404, { message: "missing" }); + }, async () => { + await assert.rejects( + ext.runCommand({ command: "github validate", args: ["acme/missing"], global: { json: false } }), + /check failed/, + ); + })); + assert.match(validated.stderr.join("\n"), /check failed/); + + }); +}); + +test("project import dry-run counts an update and apply fails closed when every create fails", async () => { + const ext = await harnessPromise; + const root = gitTracker("pm-github-project-import-"); + try { + const tag = projectItemTag({ owner: "acme", number: 5 }, "PVTI_known"); + const created = spawnSync(REAL_PM, ["--path", root, "create", "task", "Linked board item", "--tags", tag], { encoding: "utf8" }); + assert.equal(created.status, 0, created.stderr); + await withEnv({ GITHUB_TOKEN: "tok", GH_TOKEN: undefined }, async () => { + const preview = await captureStderr(() => withMockGithub((_req, res) => { + jsonResponse(res, 200, { data: { + user: { projectV2: { id: "PVT", title: "Board", url: "https://example.test/board", statusField: { id: "F", name: "Status", options: [{ id: "o", name: "Todo" }] } } }, + organization: null, + node: { items: { pageInfo: { hasNextPage: false }, nodes: [ + { id: "PVTI_known", content: { __typename: "DraftIssue", title: "Linked", body: "b" } }, + { id: "PVTI_pr", content: { __typename: "PullRequest", title: "PR", number: 4, url: "https://example.test/pr/4", state: "OPEN", repository: { nameWithOwner: "acme/widgets" } } }, + { id: "PVTI_redacted", content: null }, + ] } }, + } }); + }, () => ext.runCommand({ + command: "github project import", + args: ["acme/5"], + options: { "dry-run": true }, + pmRoot: root, + global: { json: false }, + }))); + assert.match(preview.stderr.join("\n"), /dry-run/); + + await withMockGithub((_req, res) => { + jsonResponse(res, 200, { data: { + user: { projectV2: { id: "PVT", title: "Board", url: "https://example.test/board", statusField: null } }, + organization: null, + node: { items: { pageInfo: { hasNextPage: false }, nodes: [ + { id: "PVTI_new", content: { __typename: "DraftIssue", title: "New", body: "b" } }, + ] } }, + } }); + }, async () => { + await withFakePm(`for arg in "$@"; do if [ "$arg" = "create" ]; then echo create-refused >&2; exit 1; fi; done +exec "$REAL_PM" "$@"`, async () => { + await assert.rejects(ext.runCommand({ + command: "github project import", + args: ["acme/5"], + pmRoot: root, + global: { json: true }, + }), /Imported 0 project item/); + }); + }); + }); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } +}); + +test("sync reports a non-retryable PATCH failure and the after-command hook names the upstream repo", async () => { + const ext = await harnessPromise; + const root = gitTracker("pm-github-sync-hook-"); + try { + const created = spawnSync(REAL_PM, ["--path", root, "create", "task", "Linked", "--status", "closed", "--tags", "gh:acme/widgets#5"], { encoding: "utf8" }); + if (created.status !== 0) { + const open = spawnSync(REAL_PM, ["--path", root, "create", "task", "Linked", "--tags", "gh:acme/widgets#5"], { encoding: "utf8" }); + assert.equal(open.status, 0, open.stderr); + const listed = spawnSync(REAL_PM, ["--pm-path", root, "--json", "list", "--full"], { encoding: "utf8" }); + const id = (JSON.parse(listed.stdout) as { items?: Array<{ id: string }> }).items?.[0]?.id; + assert.ok(id); + assert.equal(spawnSync(REAL_PM, ["--path", root, "close", id, "--reason", "fixture closed"], { encoding: "utf8" }).status, 0); + } + await withEnv({ GITHUB_TOKEN: "tok", GH_TOKEN: undefined }, async () => { + const patched = await captureStderr(() => withMockGithub((req, res) => { + if (req.method === "PATCH") jsonResponse(res, 422, { message: "no" }); + else jsonResponse(res, 200, { state: "open", title: "Linked", number: 5 }); + }, () => ext.runCommand({ + command: "github sync", + options: { repo: "acme/widgets" }, + pmRoot: root, + global: { json: false }, + }))); + assert.match(patched.stderr.join("\n"), /PATCH failed/); + }); + + await withFakePm(` + case "$FAKE_PM_MODE" in + fail) exit 1 ;; + badjson) printf '%s\n' 'not-json'; exit 0 ;; + missingtags) printf '%s\n' '{}'; exit 0 ;; + notag) printf '%s\n' '{"tags":["local"]}'; exit 0 ;; + *) printf '%s\n' '{"tags":["gh:acme/widgets#5"]}'; exit 0 ;; + esac + `, async () => { + await withEnv({ PM_GITHUB_SYNC: "1" }, async () => { + const nudged = await captureStderr(() => ext.runHook({ + kind: "after_command", + context: { command: "close", args: ["pm-linked"], pm_root: root, ok: true }, + })); + assert.match(nudged.stderr.join("\n"), /pm github sync --repo acme\/widgets/); + process.env.FAKE_PM_MODE = "fail"; + const silent = await captureStderr(() => ext.runHook({ + kind: "after_command", + context: { command: "close", args: ["pm-linked"], pm_root: root, ok: true }, + })); + assert.equal(silent.stderr.join("\n"), ""); + process.env.FAKE_PM_MODE = "badjson"; + await ext.runHook({ kind: "after_command", context: { command: "update", args: ["pm-linked"], pm_root: root, ok: true } }); + process.env.FAKE_PM_MODE = "notag"; + await ext.runHook({ kind: "after_command", context: { command: "close", args: ["pm-linked"], pm_root: root, ok: true } }); + process.env.FAKE_PM_MODE = "missingtags"; + const missingTags = await captureStderr(() => ext.runHook({ kind: "after_command", context: { command: "close", args: ["pm-linked"], pm_root: root, ok: true } })); + assert.equal(missingTags.stderr.join("\n"), ""); + delete process.env.FAKE_PM_MODE; + await ext.runHook({ kind: "after_command", context: { command: "list", args: ["pm-linked"], pm_root: root, ok: true } }); + await ext.runHook({ kind: "after_command", context: { command: "close", args: [], pm_root: root, ok: true } }); + await ext.runHook({ kind: "after_command", context: { command: "close", args: ["pm-linked"], pm_root: "", ok: true } }); + await ext.runHook({ kind: "after_command", context: { command: "close", args: ["pm-linked"], pm_root: root, ok: false } }); + }); + delete process.env.PM_GITHUB_SYNC; + await ext.runHook({ kind: "after_command", context: { command: "close", args: ["pm-linked"], pm_root: root, ok: true } }); + }); + } finally { + delete process.env.FAKE_PM_MODE; + delete process.env.PM_GITHUB_SYNC; + fs.rmSync(root, { recursive: true, force: true }); + } +}); + +test("plain imports preserve assigned metadata and synchronize existing annotations", async () => { + const root = gitTracker("pm-github-metadata-"); + const tracker = path.join(root, ".agents/pm"); + try { + const common = { resolveToken: () => undefined, fetchIssues: async () => [issue({ assignee: { login: "fixture" }, milestone: { title: "Sprint" }, comments: 1 })], + fetchIssueComments: async () => [{ id: 11, body: "public note", user: null, created_at: "" }] }; + const first = await captureStderr(() => runImport("acme/widgets", tracker, opts({ "comments-mode": "annotations" }), common)); + assert.ok("imported" in first.result && first.result.imported === 1); + const second = await captureStderr(() => runImport("acme/widgets", tracker, opts({ "comments-mode": "annotations" }), common)); + assert.ok("updated" in second.result && second.result.updated === 1); + const items = JSON.parse(spawnSync(REAL_PM, ["--path", tracker, "list", "--json", "--full"], { encoding: "utf8" }).stdout) as { items: Array<{ assignee: string; sprint: string; comments: Array<{ text: string }> }> }; + assert.equal(items.items[0]?.assignee, "fixture"); + assert.equal(items.items[0]?.sprint, "Sprint"); + assert.equal(items.items[0]?.comments.length, 1); + } finally { fs.rmSync(root, { recursive: true, force: true }); } +}); + +test("plain create close failures and unreadable annotation identities report incomplete imports", async () => { + const root = gitTracker("pm-github-create-boundary-"); + const tracker = path.join(root, ".agents/pm"); + try { + await withFakePm(`for arg in "$@"; do if [ "$arg" = "close" ]; then echo close-refused >&2; exit 1; fi; done\nexec "$REAL_PM" "$@"`, async () => { + const { stderr } = await captureStderr(() => assert.rejects(runImport("acme/widgets", tracker, opts(), { + resolveToken: () => undefined, fetchIssues: async () => [issue({ state: "closed", closed_at: null })], + }), /Imported 0 issue/)); + assert.match(stderr.join("\n"), /close after import failed/); + }); + await withFakePm(`for arg in "$@"; do if [ "$arg" = "create" ]; then printf 'not-json'; exit 0; fi; done\nexec "$REAL_PM" "$@"`, async () => { + const { stderr, result } = await captureStderr(() => runImport("acme/widgets", tracker, opts({ "comments-mode": "annotations" }), { + resolveToken: () => undefined, fetchIssues: async () => [issue({ number: 2 })], fetchIssueComments: async () => [], + })); + assert.ok("imported" in result && result.imported === 1); + assert.match(stderr.join("\n"), /comments not synced/); + }); + } finally { fs.rmSync(root, { recursive: true, force: true }); } +}); + +test("gated comment failures stop before mutation and recovered receipts permit omitted item count", async () => { + const root = gitTracker("pm-github-gated-comments-"); + const tracker = path.join(root, ".agents/pm"); + try { + await assert.rejects(runImport("acme/widgets", tracker, opts({ gate: true, atomic: true, "with-comments": true }), { + resolveToken: () => undefined, fetchIssues: async () => [issue()], fetchIssueComments: async () => { throw new Error("comments unavailable"); }, + }), /comments for issue #7 could not be read/); + assert.deepEqual(fs.readdirSync(path.join(tracker, "issues")).filter(name => name.endsWith(".toon")), []); + const recovered = await captureStderr(() => runImport("acme/widgets", tracker, opts({ atomic: true }), { + resolveToken: () => undefined, fetchIssues: async () => [issue()], + commitAtomic: async (root, repo, entries) => { + await importGithubAtomic(root, repo, entries); + const receipt = await importGithubAtomic(root, repo, entries); + const { recoveredItems: _count, ...legacy } = receipt; + return legacy; + }, + })); + assert.match(recovered.stderr.join("\n"), /covering 1 item/); + } finally { fs.rmSync(root, { recursive: true, force: true }); } +}); + +test("Projects import fails closed at each local mutation boundary", async () => { + const root = gitTracker("pm-project-import-boundaries-"); + try { + const created = spawnSync(REAL_PM, ["--path", root, "create", "task", "Linked", "--tags", projectItemTag({ owner: "acme", number: 5 }, "linked")], { encoding: "utf8" }); + assert.equal(created.status, 0, created.stderr); + for (const fixture of [ + { id: "linked", fail: "update", invalidId: false, diagnostic: /update failed/ }, + { id: "linked", fail: "close", invalidId: false, diagnostic: /close failed/ }, + { id: "new", fail: "create", invalidId: true, diagnostic: /could not read created item id/ }, + { id: "new", fail: "close", invalidId: false, diagnostic: /close failed/ }, + ]) { + await withEnv({ GITHUB_TOKEN: "fixture-token", GH_TOKEN: undefined }, () => withMockGithub((_req, res, body) => { + const query = (JSON.parse(body) as { query: string }).query; + jsonResponse(res, 200, { data: query.includes("projectV2(number:") ? { user: { projectV2: { + id: "board", title: "Board", statusField: { id: "status", name: "Status", options: [{ id: "done", name: "Done" }] }, + } } } : { node: { items: { nodes: [{ id: fixture.id, fieldValueByName: { name: "Done", optionId: "done" }, content: { __typename: "DraftIssue", title: "Closed candidate" } }], pageInfo: {} } } } }); + }, () => withFakePm(`for arg in "$@"; do +if [ "$arg" = "${fixture.fail}" ]; then +${fixture.invalidId ? "printf 'invalid-json'; exit 0" : "echo mutation-refused >&2; exit 1"} +fi +done +exec "$REAL_PM" "$@"`, async () => { + const { stderr } = await captureStderr(() => assert.rejects((harnessPromise.then(ext => ext.runCommand({ command: "github project import", args: ["acme/5"], pmRoot: root }))), /Imported 0 project item/)); + assert.match(stderr.join("\n"), fixture.diagnostic); + }))); + } + } finally { fs.rmSync(root, { recursive: true, force: true }); } +}); diff --git a/test/docstring-gate.test.ts b/test/docstring-gate.test.ts index db19d62c..9c1ac53d 100644 --- a/test/docstring-gate.test.ts +++ b/test/docstring-gate.test.ts @@ -70,7 +70,7 @@ test("docstring gate main writes violations to stderr and sets the exit code", ( observedExitCode = process.exitCode; process.stdout.write = originalStdoutWrite; process.stderr.write = originalStderrWrite; - process.exitCode = originalExitCode; + process.exitCode = originalExitCode ?? 0; } assert.equal(observedExitCode, 1); assert.equal(stdout, ""); @@ -156,7 +156,7 @@ test("docstring gate main writes a success line to stdout and exits 0", () => { } finally { observedExitCode = process.exitCode; process.stdout.write = originalStdoutWrite; - process.exitCode = originalExitCode; + process.exitCode = originalExitCode ?? 0; } assert.equal(observedExitCode, 0); assert.match(stdout, /docstring-gate:.*documented\.\n$/); diff --git a/test/duplication-gate.test.ts b/test/duplication-gate.test.ts new file mode 100644 index 00000000..ca810ddc --- /dev/null +++ b/test/duplication-gate.test.ts @@ -0,0 +1,65 @@ +/** Verify that the current detector cannot silently narrow the authored scope. */ +import assert from "node:assert/strict"; +import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import test from "node:test"; +import { fileURLToPath } from "node:url"; +import { runDuplicationGate, runDuplicationGateIfMain } from "../scripts/duplication-gate.ts"; +import type { CoverageRunner } from "../scripts/coverage-gate.ts"; + +/** Create source with enough tokens for both completeness and duplication passes. */ +function duplicationFixture(): string { + const root = mkdtempSync(join(tmpdir(), "pm-github-duplication-")); + writeFileSync(join(root, "source.ts"), 'export function classify(values: readonly string[]): string { const seen = new Set(); const output: string[] = []; for (const entry of values) { const trimmed = entry.trim(); if (trimmed.length === 0) continue; if (seen.has(trimmed)) throw new Error("repeated"); seen.add(trimmed); output.push(trimmed.toUpperCase()); } return output.join(","); }'.replaceAll("; ", ";\n")); + return root; +} + +/** Detector receipt boundary used to force missing or malformed source counts. */ +function detectorReceipt(root: string, sources: number, lines: number, duplicatedLines: number): CoverageRunner { + return (_bin, args) => { + const output = args.find(arg => arg.startsWith("--output="))!.slice("--output=".length); + writeFileSync(join(output, "jscpd-report.json"), JSON.stringify({ statistics: { total: { sources, lines, duplicatedLines } } })); + assert.ok(args.includes(join(root, "source.ts")), "the scanner receives explicit source filenames"); + assert.ok(!args.some(arg => arg.startsWith("--pattern")), "unsupported flags cannot narrow the denominator"); + return { status: 0 }; + }; +} + +test("duplication gate main entry runs against the supplied root", () => { + const root = mkdtempSync(join(tmpdir(), "pm-github-duplication-main-")); + const previous = process.exitCode; + try { + const moduleUrl = new URL("../scripts/duplication-gate.ts", import.meta.url).href; + runDuplicationGateIfMain(["node", fileURLToPath(moduleUrl)], moduleUrl, root); + assert.equal(process.exitCode, 1); + } finally { + process.exitCode = previous ?? 0; + rmSync(root, { recursive: true, force: true }); + } +}); + +test("duplication fails closed on subprocess failures, missing reports, omitted source, and invalid counters", () => { + const root = duplicationFixture(); + try { + for (const result of [{ status: null }, { status: 1 }, { status: 0, error: new Error("unavailable") }, { status: 0 }]) { + assert.equal(runDuplicationGate(root, () => result), 1); + } + for (const [sources, lines, duplicates] of [[0, 10, 0], [1.5, 10, 0], [1, 0, 0], [1, 10, -1], [1, 10, 1]]) { + assert.equal(runDuplicationGate(root, detectorReceipt(root, sources!, lines!, duplicates!)), 1); + } + assert.equal(runDuplicationGate(root, detectorReceipt(root, 1, 10, 0)), 0); + rmSync(join(root, "source.ts")); + assert.equal(runDuplicationGate(root), 1, "empty source cannot pass"); + } finally { rmSync(root, { recursive: true, force: true }); } +}); + +test("real jscpd covers all source files and rejects cloned code", () => { + const root = duplicationFixture(); + try { + assert.equal(runDuplicationGate(root), 0); + mkdirSync(join(root, "scripts")); + writeFileSync(join(root, "scripts", "copy.ts"), readFileSync(join(root, "source.ts"), "utf8").replace("classify", "copied")); + assert.equal(runDuplicationGate(root), 1); + } finally { rmSync(root, { recursive: true, force: true }); } +}); diff --git a/test/fixtures/privacy-gate/manifest.json b/test/fixtures/privacy-gate/manifest.json index 9482924c..80874fc6 100644 --- a/test/fixtures/privacy-gate/manifest.json +++ b/test/fixtures/privacy-gate/manifest.json @@ -19,5 +19,12 @@ }, "c93b7ed28c1dd97c0de125b8bbd34d0b7d0bcc07": { "justification": "Synthetic review fixture test/fixtures/privacy-gate/synthetic-host-path.txt: token and path values are padded placeholder characters documented in README.md; no real credential or host path." + }, + "68d83c8dc9bfa15914bf503eded87573dcb8a619": { + "justification": "Inherited test/gate.test.ts blob in implementation checkpoint contains a literal synthetic private-key header only, without key material. Current fixtures construct the header at runtime. Exact historical blob reviewed; no credential exemption applies to current imports.", + "historical_test": { + "commit": "434df4237bdf7d5fdd3ddbd7ae42ce10d75e13ae", + "path": "test/gate.test.ts" + } } } diff --git a/test/gate-inputs.test.ts b/test/gate-inputs.test.ts new file mode 100644 index 00000000..c3d1ed43 --- /dev/null +++ b/test/gate-inputs.test.ts @@ -0,0 +1,125 @@ +import assert from "node:assert/strict"; +import { spawnSync } from "node:child_process"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import test, { type TestContext } from "node:test"; +import { GateInputError, formatGateReport, parseUnifiedDiff, runTrackerGate, scanLineForRuleHits } from "../gate.ts"; + +/** A real git and PM project for explicit proposals and planned-value scans. */ +function repository(t: TestContext): string { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "pm-gate-input-")); + t.after(() => fs.rmSync(root, { recursive: true, force: true })); + for (const [command, args] of [ + ["git", ["init", "-q", "-b", "main"]], + ["git", ["config", "user.name", "Fixture Bot"]], + ["git", ["config", "user.email", "fixture@example.invalid"]], + ["pm", ["--path", path.join(root, ".agents", "pm"), "init", "fixture"]], + ["git", ["add", ".agents/pm"]], + ["git", ["commit", "-qm", "Fixture baseline"]], + ] as const) { + const result = spawnSync(command, [...args], { cwd: root, encoding: "utf8" }); + assert.equal(result.status, 0, result.stderr); + } + return root; +} + +test("diff parsing accepts quoted filenames and deletion-only proposals", () => { + const changed = parseUnifiedDiff('--- a/item.toon\n+++ "b/item.toon"\n@@ -1 +1 @@\n-title: Before\n+title: After\n'); + assert.deepEqual(changed, [{ filePath: "item.toon", itemId: "item", addedLines: [{ text: "title: After", field: "title" }] }]); + assert.deepEqual(parseUnifiedDiff("--- a/item.toon\n+++ /dev/null\n@@ -1 +0,0 @@\n-title: Before\n"), []); + assert.deepEqual(parseUnifiedDiff("--- a/item.toon\n+++ b/item.toon\n@@ -1,0 +1,0 @@\n\n"), []); +}); + +test("diff parsing rejects unreadable filenames and content outside or after a complete hunk", () => { + for (const [diff, diagnostic] of [ + ['+++ "unterminated\n', /unreadable diff filename/], + ["+++ b/item.toon\nbody: outside\n", /content outside a diff hunk/], + ["+++ b/item.toon\n@@ -0,0 +1 @@\n+body: inside\n?invalid\n", /invalid unified diff content/], + ] as const) assert.throws(() => parseUnifiedDiff(diff), diagnostic); +}); + +test("serialized escapes retain ordinary text and reject short phone lookalikes", () => { + assert.deepEqual(scanLineForRuleHits("ordinary text\\"), []); + assert.deepEqual(scanLineForRuleHits("+1 12 34 56"), []); + assert.deepEqual(scanLineForRuleHits("escaped\\nline\\rreturn\\ttab"), []); +}); + +test("planned scans redact hostile item and field names in every report surface", t => { + const root = repository(t); + const token = "ghp_" + "A".repeat(36); + const report = runTrackerGate({ pmRoot: root, plannedItems: [{ itemId: token, fields: { [token]: token } }] }); + assert.equal(report.verdict, "fail"); + assert.ok(report.findings.some(f => f.item_id === "" && f.field === "unknown")); + assert.ok(!JSON.stringify(report).includes(token)); + assert.ok(!formatGateReport(report).join("\n").includes(token)); +}); + +test("history patch attribution handles root pointers, metadata roots, and copy sources", t => { + const root = repository(t); + const token = "ghp_" + "B".repeat(36); + const rows = [ + { patch: [{ op: "replace", path: "/", value: token }] }, + { patch: [{ op: "add", path: "/metadata", value: token }] }, + { patch: [{ op: "copy", path: "/metadata/body", from: "/metadata/title", value: [null, token] }] }, + ]; + const proposal = path.join(root, "proposal.diff"); + fs.writeFileSync(proposal, `--- /dev/null\n+++ b/history/item.jsonl\n@@ -0,0 +1,3 @@\n${rows.map(row => `+${JSON.stringify(row)}`).join("\n")}\n`); + const report = runTrackerGate({ pmRoot: root, diffFile: proposal }); + assert.equal(report.verdict, "fail"); + assert.deepEqual(new Set(report.findings.map(f => f.field)), new Set(["unknown", "metadata", "body"])); +}); + +test("history object keys are scanned like values", t => { + // A .toon line is scanned as raw text, so a credential used as a JSON object + // key in history must fail the gate the same way. + const root = repository(t); + const token = "ghp_" + "C".repeat(36); + const proposal = path.join(root, "proposal.diff"); + const row = { patch: [{ op: "add", path: "/metadata/x", value: { [token]: "clean" } }] }; + fs.writeFileSync(proposal, `--- /dev/null\n+++ b/history/item.jsonl\n@@ -0,0 +1 @@\n+${JSON.stringify(row)}\n`); + const report = runTrackerGate({ pmRoot: root, diffFile: proposal }); + assert.equal(report.verdict, "fail"); + assert.ok(report.findings.some(f => f.rule === "github-token-classic")); +}); + +test("explicit diff failures preserve gate diagnostics and wrap other filesystem failures", t => { + const root = repository(t); + const proposal = path.join(root, "proposal.diff"); + fs.writeFileSync(proposal, '+++ "unterminated\n'); + assert.throws(() => runTrackerGate({ pmRoot: root, diffFile: proposal }), /unreadable diff filename/); + fs.mkdirSync(path.join(root, "directory.diff")); + assert.throws(() => runTrackerGate({ pmRoot: root, diffFile: path.join(root, "directory.diff") }), GateInputError); +}); + +test("diff hunk content keeps added plus-plus and removed minus-minus prefixes", () => { + const files = parseUnifiedDiff("--- a/item.toon\n+++ b/item.toon\n@@ -1,2 +1,2 @@\n--- old content\n+++ public content\n body: clean\n"); + assert.deepEqual(files[0]!.addedLines, [{ text: "++ public content", field: "unknown" }]); + const token = "ghp_" + "A".repeat(36); + assert.ok(scanLineForRuleHits(parseUnifiedDiff(`+++ b/item.toon\n@@ -0,0 +1 @@\n+++ ${token}\n`)[0]!.addedLines[0]!.text).length > 0); +}); + +test("Git refuses invalid UTF-8 in both tracked and staged changes", t => { + const root = repository(t); + const file = path.join(root, ".agents/pm/notes.txt"); + fs.writeFileSync(file, "public baseline\n"); + assert.equal(spawnSync("git", ["add", ".agents/pm/notes.txt"], { cwd: root }).status, 0); + assert.equal(spawnSync("git", ["commit", "-qm", "Text baseline"], { cwd: root }).status, 0); + fs.writeFileSync(file, Buffer.from([0xc3, 0x28, 0x0a])); + assert.throws(() => runTrackerGate({ pmRoot: root }), GateInputError); + assert.equal(spawnSync("git", ["add", ".agents/pm/notes.txt"], { cwd: root }).status, 0); + assert.throws(() => runTrackerGate({ pmRoot: root }), GateInputError); +}); + +test("Git tracker scanning canonicalizes a symlinked checkout including operational exclusions", t => { + const root = repository(t); + const alias = root + "-alias"; + fs.symlinkSync(root, alias, "dir"); + t.after(() => fs.unlinkSync(alias)); + fs.mkdirSync(path.join(root, ".agents/pm/locks"), { recursive: true }); + fs.writeFileSync(path.join(root, ".agents/pm/locks/transient"), "ghp_" + "A".repeat(36)); + fs.writeFileSync(path.join(root, ".agents/pm/notes.txt"), "public change\n"); + const report = runTrackerGate({ pmRoot: alias }); + assert.equal(report.verdict, "pass"); + assert.equal(report.scanned_files, 1); +}); diff --git a/test/gate-paths.test.ts b/test/gate-paths.test.ts new file mode 100644 index 00000000..7bbad478 --- /dev/null +++ b/test/gate-paths.test.ts @@ -0,0 +1,83 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { spawnSync } from "node:child_process"; + +import { GateInputError, isHighEntropySecretAssignment, runTrackerGate, scanLineForRuleHits } from "../gate.ts"; + +test("a credential in a proposed tracker filename fails without exposing the filename", () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "pm-github-gate-path-")); + const token = "ghp_" + "A".repeat(36); + try { + assert.equal(spawnSync("git", ["init", "--quiet", root]).status, 0); + const tracker = path.join(root, ".agents", "pm"); + fs.mkdirSync(path.join(tracker, "issues"), { recursive: true }); + fs.writeFileSync(path.join(tracker, "settings.json"), "{}\n"); + fs.writeFileSync(path.join(tracker, "issues", `${token}.toon`), "body: public text\n"); + const report = runTrackerGate({ pmRoot: tracker }); + assert.equal(report.verdict, "fail"); + assert.deepEqual(report.findings.map(finding => [finding.item_id, finding.field, finding.rule]), [ + ["", "file_path", "github-token-classic"], + ]); + assert.equal(JSON.stringify(report).includes(token), false); + assert.equal(spawnSync("git", ["-C", root, "rev-parse", "--verify", "HEAD"], { stdio: "ignore" }).status, 128); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } +}); + +test("Windows drive paths accept both separators while public URLs stay clean", () => { + for (const separator of ["/", "\\"]) { + const value = "C:" + separator + ["Users", "fixture", "report.txt"].join(separator); + assert.ok(scanLineForRuleHits(`path="${value}"`).some(hit => hit.rule === "windows-host-path")); + assert.ok(scanLineForRuleHits(JSON.stringify(value)).some(hit => hit.rule === "windows-host-path")); + } + for (const value of ["https://github.com/acme/widgets/issues/1", "https://example.org/a", "C:relative.txt"]) { + assert.equal(scanLineForRuleHits(value).some(hit => hit.rule === "windows-host-path"), false); + } +}); + +test("camel-case secrets, Slack app credentials and labelled plain phones are gated", () => { + const secret = "Zj9kP2mQ7xW4" + "nB8vC5tR1sD"; + for (const name of ["accessToken", "clientSecret", "privateKey", "APIToken", "AWSSecretAccessKey"]) { + assert.equal(isHighEntropySecretAssignment(name, secret), true); + assert.ok(scanLineForRuleHits(`${name} = "${secret}"`).some(hit => hit.rule === "high-entropy-assignment")); + } + for (const value of [secret + "2026-10-05", "a1b2c3d4-e5f6" + "-7890-abcd-ef0123456789"]) { + assert.equal(isHighEntropySecretAssignment("api_key", value), true); + } + assert.equal(isHighEntropySecretAssignment("request_id", "a1b2c3d4-e5f6-7890-abcd-ef0123456789"), false); + for (const prefix of ["xapp", "xoxc", "xoxd"]) { + assert.ok(scanLineForRuleHits(prefix + "-" + "A".repeat(36)).some(hit => hit.rule === "slack-token")); + } + for (const value of ["phone: " + "415" + "555" + "0123", '"mobile": "' + "0044" + "7700" + "900123" + '"']) { + assert.ok(scanLineForRuleHits(value).some(hit => hit.rule === "phone-number")); + } + for (const value of ["github-comment:" + "12345678901", "phone: 123456", "phone: 1-----2", "phone: 1234567890123456"]) { + assert.equal(scanLineForRuleHits(value).some(hit => hit.rule === "phone-number"), false); + } +}); + +test("untracked binary content and undecodable files fail closed", () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "pm-github-gate-encoding-")); + const token = "ghp_" + "A".repeat(36); + try { + assert.equal(spawnSync("git", ["init", "--quiet", root]).status, 0); + const tracker = path.join(root, ".agents", "pm"); + fs.mkdirSync(path.join(tracker, "issues"), { recursive: true }); + fs.writeFileSync(path.join(tracker, "settings.json"), "{}\n"); + const file = path.join(tracker, "issues", "encoded.toon"); + for (const bytes of [Buffer.from("body: " + token, "utf16le"), Buffer.from([0xc3, 0x28]), Buffer.from("body: clean\0data")]) { + fs.writeFileSync(file, bytes); + assert.throws(() => runTrackerGate({ pmRoot: tracker }), error => + error instanceof GateInputError && !error.message.includes(token)); + } + const diff = path.join(root, "proposal.patch"); + fs.writeFileSync(diff, Buffer.from([0xc3, 0x28])); + assert.throws(() => runTrackerGate({ pmRoot: tracker, diffFile: diff }), GateInputError); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } +}); diff --git a/test/gate.test.ts b/test/gate.test.ts new file mode 100644 index 00000000..3b2b30ed --- /dev/null +++ b/test/gate.test.ts @@ -0,0 +1,1066 @@ +// Fail-closed privacy gate over a proposed tracker change — rule battery, +// diff parsing with item/field attribution, git change collection on a REAL +// repository, content-hash allowlisting, the registered `pm github gate` +// command through the extension test harness, and the workflow contract. +// +// Malicious fixtures are syntactically valid but FAKE and built by +// concatenation so no tracked blob ever contains a complete credential or +// host-path shape: the values exist only inside the test process at run time. +// Findings never echo the matched content, so neither this file nor the gate +// output can carry the fixtures verbatim. + +import assert from "node:assert/strict"; +import test from "node:test"; +import { spawnSync } from "node:child_process"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; + +import { createExtensionTestHarness, type ExtensionTestHarness } from "@unbrained/pm-cli/sdk/testing"; +import { captureStderr } from "./helpers/mock-github-server.ts"; + +import { createHash } from "node:crypto"; +import { + GATE_ALLOWLIST_FILENAME, + GateInputError, + attributeToonFields, + collectTrackerChange, + formatGateReport, + isHighEntropySecretAssignment, + parseUnifiedDiff, + readGateAllowlist, + runGitDefault, + runTrackerGate, + scanLineForRuleHits, + shannonEntropyPerChar, + type GateReport, +} from "../gate.ts"; +import { nodeScenario } from "./helpers/node-scenario.ts"; +import extension, { runCommandTrackerGate } from "../index.ts"; + +const MANIFEST_CAPABILITIES = ["commands", "importers", "schema", "hooks", "preflight", "search"] as const; +const harnessPromise: Promise = + createExtensionTestHarness(extension, { capabilities: [...MANIFEST_CAPABILITIES] }); + +test("staged content is scanned even when the working copy restores clean text", () => { + const { root, git } = initGateRepo(); + try { + const file = path.join(root, ".agents", "pm", "issues", "pm-test-aabb.toon"); + const original = fs.readFileSync(file, "utf8"); + fs.writeFileSync(file, `id: pm-test-aabb\nbody: "${GH_TOKEN}"\n`); + assert.equal(git(["add", ".agents/pm/issues/pm-test-aabb.toon"]).status, 0); + fs.writeFileSync(file, original); + assert.equal(runTrackerGate({ pmRoot: root }).verdict, "fail"); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } +}); + +test("operational exclusions cannot hide staged files or nested item content", () => { + const { root, git } = initGateRepo(); + try { + for (const relative of ["locks/receipt.json", "issues/extensions/item.toon"]) { + const file = path.join(root, ".agents/pm", relative); + fs.mkdirSync(path.dirname(file), { recursive: true }); + fs.writeFileSync(file, `body: "${GH_TOKEN}"\n`); + } + assert.equal(git(["add", ".agents/pm/locks/receipt.json"]).status, 0); + const report = runTrackerGate({ pmRoot: root }); + assert.equal(report.verdict, "fail"); + assert.equal(report.scanned_files, 2); + assert.equal(report.findings.length, 2); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } +}); + +test("history pointer syntax never suppresses credentials in a pointer", () => { + const diff = `--- /dev/null\n+++ b/history/item.jsonl\n@@ -0,0 +1 @@\n+${JSON.stringify({ patch: [{ op: "add", path: `/metadata/${GH_TOKEN}`, value: "clean" }] })}\n`; + const { root } = initGateRepo(); + try { + const file = path.join(root, "proposal.diff"); + fs.writeFileSync(file, diff); + const report = runTrackerGate({ pmRoot: root, diffFile: file }); + assert.equal(report.verdict, "fail"); + assert.ok(report.findings.some(finding => finding.rule === "github-token-classic")); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } +}); + +test("an allowlisted value cannot hide another value in the same item field", () => { + const { root } = initGateRepo(); + try { + const other = "ghp_" + "B".repeat(36); + fs.writeFileSync(path.join(root, GATE_ALLOWLIST_FILENAME), JSON.stringify({ + [sha256Hex(GH_TOKEN)]: { reason: "Reviewed synthetic value." }, + })); + fs.writeFileSync(path.join(root, ".agents", "pm", "issues", "pm-test-aabb.toon"), + `id: pm-test-aabb\nbody: "${GH_TOKEN} and ${other}"\n`); + const report = runTrackerGate({ pmRoot: root }); + assert.equal(report.verdict, "fail"); + assert.equal(report.allowlisted, 1); + assert.equal(report.findings.length, 1); + assert.equal(report.findings[0]!.hash, sha256Hex(other)); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } +}); + +test("malformed, truncated, and binary diffs fail closed", () => { + for (const diff of ["garbage", "--- a/item\n+++ b/item\n@@ -0,0 +1,2 @@\n+body: clean", + "diff --git a/item b/item\nBinary files a/item and b/item differ", + "--- a/item\n+++ b/item\n@@ invalid @@\n+body: clean"]) { + assert.throws(() => parseUnifiedDiff(diff), GateInputError); + } + assert.deepEqual(parseUnifiedDiff(""), []); + // Added content with no target file is never skipped: Git cannot emit it, so + // an operator-supplied diff carrying it is malformed rather than clean. + for (const targetless of [`diff --git a/x b/x\n@@ -0,0 +1 @@\n+${GH_TOKEN}`, `--- a/x\n+++ /dev/null\n@@ -0,0 +1 @@\n+${GH_TOKEN}`]) { + assert.throws(() => parseUnifiedDiff(targetless), GateInputError); + } + // A genuine deletion (only `-` lines after +++ /dev/null) still parses. + assert.deepEqual(parseUnifiedDiff("--- a/x\n+++ /dev/null\n@@ -1,2 +0,0 @@\n-one\n-two"), []); +}); + +test("escaped tokens and Windows paths retain redacted findings", () => { + const escapedToken = "\\u0067" + GH_TOKEN.slice(1); + assert.ok(scanLineForRuleHits(escapedToken).some(hit => hit.rule === "github-token-classic")); + assert.ok(scanLineForRuleHits(JSON.stringify(WIN_PATH)).some(hit => hit.rule === "windows-host-path")); + assert.ok(scanLineForRuleHits("Authorization: Bearer " + "A".repeat(32)).some(hit => hit.rule === "bearer-token")); + assert.deepEqual(scanLineForRuleHits("noreply@example.org"), []); + // SSH clone URLs name the git service account, not a person. + assert.deepEqual(scanLineForRuleHits("clone with `git@github.com:acme/widgets.git`"), []); + assert.deepEqual(scanLineForRuleHits("git@gitlab.com:group/project.git"), []); + assert.ok(scanLineForRuleHits("gitlover@example.org").some(hit => hit.rule === "email-address")); + // Only the SSH URL shape is exempt: a git@ contact address is still personal data. + for (const contact of ["Contact git@example.org", "write to git@example.org: thanks", "agit@github.com:x"]) { + assert.ok(scanLineForRuleHits(contact).some(hit => hit.rule === "email-address"), contact); + } + assert.ok(scanLineForRuleHits("person-noreply@example.org").some(hit => hit.rule === "email-address")); +}); + +test("a present but unreadable optional allowlist fails without echoing its error", () => { + assert.throws(() => readGateAllowlist("reviewed.json", false, () => { + const failure = new Error(GH_TOKEN) as NodeJS.ErrnoException; + failure.code = "EACCES"; + throw failure; + }), err => err instanceof GateInputError && !err.message.includes(GH_TOKEN)); +}); + +test("the SDK resolves custom tracker roots and scans symlink destinations", () => { + const { root } = initGateRepo(); + try { + const custom = path.join(root, "tracker"); + fs.renameSync(path.join(root, ".agents", "pm"), custom); + fs.symlinkSync("/" + "srv/private/report", path.join(custom, "issues", "link")); + const report = runTrackerGate({ pmRoot: custom }); + assert.equal(report.verdict, "fail"); + assert.ok(report.findings.some(f => f.rule === "absolute-host-path")); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } +}); + +test("history pointers are structural but encoded history values are scanned", () => { + const { root } = initGateRepo(); + try { + const history = path.join(root, ".agents", "pm", "history"); + fs.mkdirSync(history); + fs.writeFileSync(path.join(history, "pm-test-aabb.jsonl"), JSON.stringify({ patch: [ + { op: "replace", path: "/metadata/body", value: "clean" }, + { op: "add", path: "/metadata/comments", value: [{ text: GH_TOKEN }] }, + ] }) + "\n"); + const report = runTrackerGate({ pmRoot: root }); + assert.equal(report.verdict, "fail"); + assert.ok(report.findings.some(f => f.rule === "github-token-classic")); + assert.ok(!report.findings.some(f => f.rule === "absolute-host-path")); + fs.writeFileSync(path.join(history, "pm-test-aabb.jsonl"), "not json\n"); + assert.throws(() => runTrackerGate({ pmRoot: root }), GateInputError); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } +}); + +// --- Fake-but-well-formed malicious fixtures, assembled from parts so that no +// --- tracked blob holds a complete signature. +const GH_TOKEN = "ghp_" + "A".repeat(36); +const GH_PAT = "github_pat_" + "B".repeat(30); +const NPM_TOKEN = "npm_" + "C".repeat(36); +const AWS_KEY = "AKIA" + "D".repeat(16); +const SLACK_TOKEN = "xoxb-" + "E".repeat(24); +const OPENAI_KEY = "sk-" + "F".repeat(28) + "T3BlbkFJ" + "G".repeat(28); +const ANTHROPIC_KEY = "sk-ant-" + "H".repeat(30); +const HIGH_ENTROPY_SECRET = "Zj9kP2mQ7xW4" + "nB8vC5tR1sD"; +const HOME_PATH = "/" + "home" + "/alice/report.txt"; +const TMP_PATH = "/" + "tmp" + "/scratch.log"; +const WIN_PATH = "C:" + "\\Users" + "\\bob" + "\\debug.log"; +const PERSONAL_EMAIL = "alice.person@example.org"; +const PHONE_US = "(555) 123-4567"; +const PHONE_INTL = "+49 30 5557 9922"; + +/** + * Create a real Git repository with a committed pm tracker, for collection tests. + * + * The gate's production path shells out to Git over the real working tree, so + * the collection tests stage, modify, untrack, and delete real files in a real + * repository rather than mocking subprocess output. + * + * @returns The workspace root and a helper to run Git inside it. + */ +function initGateRepo(): { root: string; git: (args: readonly string[]) => { stdout: string; status: number | null } } { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "pm-github-gate-")); + const git = (args: readonly string[]) => + spawnSync("git", ["-C", root, ...args], { encoding: "utf-8" }); + fs.mkdirSync(path.join(root, ".agents", "pm", "issues"), { recursive: true }); + fs.writeFileSync(path.join(root, ".agents", "pm", "settings.json"), '{"version":1}\n'); + fs.writeFileSync( + path.join(root, ".agents", "pm", "issues", "pm-test-aabb.toon"), + 'id: pm-test-aabb\ntitle: "Baseline item"\nbody: "clean body"\n', + ); + assert.strictEqual(git(["init", "-b", "main"]).status, 0, "git init failed"); + assert.strictEqual(git(["config", "user.name", "Gate Fixture"]).status, 0); + assert.strictEqual(git(["config", "user.email", "fixture@example.invalid"]).status, 0); + assert.strictEqual(git(["add", ".agents/pm"]).status, 0); + assert.strictEqual(git(["commit", "-m", "baseline"]).status, 0, "baseline commit failed"); + return { root, git }; +} + +/** Run the registered `github gate` command through the real dispatch engine. */ +async function runGateCommand( + pmRoot: string, + options: Record = {}, +): Promise { + const ext = await harnessPromise; + const result = await ext.runCommand({ command: "github gate", options, global: { json: true }, pmRoot }); + assert.strictEqual(result.handled, true, "the gate command must be registered"); + return result.result as GateReport; +} + +// --------------------------------------------------------------------------- +// Rule battery — the scanner fires on every listed credential, personal-data, +// and host-path shape, and stays silent on prose, timestamps, and no-reply +// addresses. +// --------------------------------------------------------------------------- + +test("scanner fires on every high-confidence credential signature", () => { + const cases: ReadonlyArray<[string, string]> = [ + ["github-token-classic", `use ${GH_TOKEN} now`], + ["github-token-fine-grained", `token ${GH_PAT}`], + ["npm-token", `publish with ${NPM_TOKEN}`], + ["aws-access-key-id", `export AWS_ACCESS_KEY_ID=${AWS_KEY}`], + ["slack-token", `slack ${SLACK_TOKEN}`], + ["slack-webhook", "https://hooks.slack.com/" + "services/T12345678/B12345678/" + "A".repeat(24)], + ["openai-api-key", `key ${OPENAI_KEY}`], + ["anthropic-api-key", `key ${ANTHROPIC_KEY}`], + ["openai-project-key", "key " + "sk-proj-" + "I".repeat(30)], + ["openai-service-account-key", "key " + "sk-svcacct-" + "I".repeat(30)], + ["private-key-block", "-----BEGIN " + "RSA PRIVATE KEY-----"], + ]; + for (const [rule, line] of cases) { + const hits = scanLineForRuleHits(line); + assert.ok( + hits.some((hit) => hit.rule === rule), + `${rule} should fire on its signature`, + ); + } +}); + +test("bearer rule detects credentials including letter-only values", () => { + assert.deepStrictEqual( + scanLineForRuleHits("Authorization: Bearer " + "aB3cD9eFg1hA7bC2dE4fG").map((hit) => hit.rule), + ["bearer-token"], + ); + assert.deepStrictEqual(scanLineForRuleHits("Use Bearer authentication everywhere"), []); +}); + +test("personal-data rules: emails and phone numbers, with no-reply exemption", () => { + assert.deepStrictEqual( + scanLineForRuleHits(`contact ${PERSONAL_EMAIL} today`).map((hit) => hit.rule), + ["email-address"], + ); + assert.deepStrictEqual( + scanLineForRuleHits("1153461+unbraind@users.noreply.github.com"), + [], + "a GitHub no-reply identity is exempt", + ); + assert.deepStrictEqual( + scanLineForRuleHits(`call ${PHONE_US} please`).map((hit) => hit.rule), + ["phone-number"], + ); + assert.deepStrictEqual( + scanLineForRuleHits(`call ${PHONE_INTL} please`).map((hit) => hit.rule), + ["phone-number"], + "the two phone shapes collapse to one deduped finding", + ); + // An ISO timestamp is 3-2-2-digit groups, never a phone shape. + assert.deepStrictEqual(scanLineForRuleHits("2026-10-05T06:04:48.192Z"), []); +}); + +test("host-path rule flags host-identifying roots, not slash commands, links or API routes", () => { + const rulesOf = (line: string): string[] => scanLineForRuleHits(line).map((hit) => hit.rule); + for (const clean of ["/assign @someone", "/label bug", "please `/approve`", "see [docs](/docs/setup.md)", "GET /api/v1 returns 404", "my /homework folder"]) { + assert.deepStrictEqual(rulesOf(clean), [], clean); + } + for (const leak of ["at /" + "home/someone/project", "cd /" + "Users/someone", "read /" + "etc/passwd", "under /" + "root", "mounted (/" + "mnt/data)"]) { + assert.deepStrictEqual(rulesOf(leak), ["absolute-host-path"], leak); + } +}); + +test("host-path rules see paths behind file:// URLs and label colons, hashing only the path", () => { + const hitsOf = (line: string): string[] => scanLineForRuleHits(line).map((hit) => `${hit.rule} ${hit.matched}`); + const home = "/" + "home/someone/report.txt"; + assert.deepStrictEqual(hitsOf(`open file://${home}`), [`absolute-host-path ${home}`]); + assert.deepStrictEqual(hitsOf("file://" + "/" + "Users/someone/x"), ["absolute-host-path /" + "Users/someone/x"]); + assert.deepStrictEqual(hitsOf(`cwd:${home}`), [`absolute-host-path ${home}`]); + assert.deepStrictEqual(hitsOf("log:/" + "tmp/x"), ["absolute-host-path /" + "tmp/x"]); + const drive = "C:/" + "Users/someone/x"; + assert.deepStrictEqual(hitsOf(`file:///${drive}`), [`windows-host-path ${drive}`]); + // A drive path is one Windows finding, never also a POSIX root after its colon. + assert.deepStrictEqual(hitsOf(`at ${drive}`), [`windows-host-path ${drive}`]); + // URLs, ports, slash commands and API routes stay clean. + for (const clean of ["https://example.com/root/path", "http://localhost:8080/" + "tmp/x", "https://example.com:443/" + "home/x", "file://server/share", "/assign @someone", "GET /api/v1 returns 404"]) { + assert.deepStrictEqual(hitsOf(clean), [], clean); + } +}); + +test("host-path rules: absolute paths, windows paths, and home usernames", () => { + assert.deepStrictEqual( + scanLineForRuleHits(`crash at ${HOME_PATH}`).map((hit) => hit.rule), + ["absolute-host-path"], + ); + assert.deepStrictEqual( + scanLineForRuleHits(`log at ${TMP_PATH}`).map((hit) => hit.rule), + ["absolute-host-path"], + ); + assert.deepStrictEqual( + scanLineForRuleHits(`log at ${WIN_PATH}`).map((hit) => hit.rule), + ["windows-host-path"], + ); + assert.deepStrictEqual( + scanLineForRuleHits("see ~" + "alice/report.txt").map((hit) => hit.rule), + ["home-username"], + ); + // Anonymous home, prose approximations, and URLs stay clean. + assert.deepStrictEqual(scanLineForRuleHits("see ~/report.txt"), []); + assert.deepStrictEqual(scanLineForRuleHits("roughly ~most users hit this"), []); + assert.deepStrictEqual(scanLineForRuleHits("https://example.com/root/path"), []); +}); + +test("high-entropy assignment rule fires only on secret-shaped values", () => { + assert.strictEqual(isHighEntropySecretAssignment("API_TOKEN", HIGH_ENTROPY_SECRET), true); + assert.strictEqual(isHighEntropySecretAssignment("api_key", HIGH_ENTROPY_SECRET), true); + assert.strictEqual(isHighEntropySecretAssignment("private_key", HIGH_ENTROPY_SECRET), true); + // Prose values never carry secret identifiers or entropy. + assert.strictEqual(isHighEntropySecretAssignment("description", "Refuse incomplete pm item corpora"), false); + assert.strictEqual(isHighEntropySecretAssignment("API_TOKEN", "the production deploy token"), false); + // Structured values (timestamps, UUIDs) are excluded even under secret names. + assert.strictEqual(isHighEntropySecretAssignment("created_at", "2026-10-05T06:04:48.192Z"), false); + assert.strictEqual( + isHighEntropySecretAssignment("request_id", "a1b2c3d4-e5f6-7890-abcd-ef0123456789"), + false, + ); + // A non-secret identifier never fires, however random the value. + assert.strictEqual(isHighEntropySecretAssignment("sort_key", HIGH_ENTROPY_SECRET), false); + assert.strictEqual(isHighEntropySecretAssignment("API_TOKEN", "short"), false); + assert.deepStrictEqual( + scanLineForRuleHits(`token: "${HIGH_ENTROPY_SECRET}"`).map((hit) => hit.rule), + ["high-entropy-assignment"], + ); +}); + +test("shannon entropy separates random from structured text", () => { + assert.ok(shannonEntropyPerChar(HIGH_ENTROPY_SECRET) >= 3.9); + assert.ok(shannonEntropyPerChar("2026-10-05T06:04:48.192Z") < 3.9); + assert.strictEqual(shannonEntropyPerChar(""), 0); +}); + +// --------------------------------------------------------------------------- +// Diff parsing + attribution +// --------------------------------------------------------------------------- + +test("parseUnifiedDiff attributes item id and toon fields, and skips removed lines", () => { + const diff = [ + "diff --git a/.agents/pm/issues/pm-x1.toon b/.agents/pm/issues/pm-x1.toon", + "index 111..222 100644", + "--- a/.agents/pm/issues/pm-x1.toon", + "+++ b/.agents/pm/issues/pm-x1.toon", + "@@ -1,2 +1,5 @@", + " id: pm-x1", + '-title: "old"', + '+title: "New title"', + `+body: "token ${GH_TOKEN}"`, + "+notes[1]{created_at,author,text}:", + `+ "2026-01-01T00:00:00Z",alice,"see ${GH_TOKEN}"`, + "\\ No newline at end of file", + "diff --git a/.agents/pm/history/pm-x1.jsonl b/.agents/pm/history/pm-x1.jsonl", + "--- /dev/null", + "+++ b/.agents/pm/history/pm-x1.jsonl", + "@@ -0,0 +1,2 @@", + `+{"op":"add","path":"/metadata/body","value":"mail ${PERSONAL_EMAIL}"}`, + `+{"op":"add","path":"/metadata/id","value":"pm-x1"}`, + ].join("\n"); + + const files = parseUnifiedDiff(diff); + assert.strictEqual(files.length, 2, "one entry per changed file"); + + const toon = files[0]!; + assert.strictEqual(toon.filePath, ".agents/pm/issues/pm-x1.toon"); + assert.strictEqual(toon.itemId, "pm-x1"); + const fields = toon.addedLines.map((line) => line.field); + assert.deepStrictEqual(fields, ["title", "body", "notes", "notes"]); + assert.ok( + toon.addedLines.every((line) => !line.text.startsWith("-")), + "removed lines never enter the change", + ); + + const history = files[1]!; + assert.strictEqual(history.itemId, "pm-x1", "the id is read from the first added jsonl line"); +}); + +test("parseUnifiedDiff attributes jsonl hits to the enclosing patch field", () => { + const historyDiff = [ + "--- /dev/null", + "+++ b/.agents/pm/history/pm-x2.jsonl", + "@@ -0,0 +1 @@", + `+{"op":"add","path":"/metadata/body","value":"crash ${HOME_PATH}"}`, + ].join("\n"); + const files = parseUnifiedDiff(historyDiff); + assert.strictEqual(files.length, 1); + const report = runTrackerGate({ + pmRoot: "/unused-workspace", + diffFile: "synthetic.patch", + dependencies: { + runGit: () => ({ ok: false, stdout: "", stderr: "no default allowlist outside a repo" }), + readFileSync: () => historyDiff, + }, + }); + assert.strictEqual(report.verdict, "fail"); + const finding = report.findings[0]!; + assert.strictEqual(finding.item_id, "pm-x2"); + assert.strictEqual(finding.field, "body"); + assert.strictEqual(finding.rule, "absolute-host-path"); +}); + +test("attributeToonFields tracks the enclosing top-level section", () => { + const fields = attributeToonFields([ + { text: "id: pm-x1", added: false }, + { text: "notes[1]{created_at,author,text}:", added: false }, + { text: ' "2026-01-01T00:00:00Z",alice,"text"', added: true }, + { text: "", added: true }, + { text: "body:", added: true }, + ]); + assert.deepStrictEqual(fields, ["id", "notes", "notes", "notes", "body"]); +}); + +test("scanLineForRuleHits reports every rule firing on one line without dedupe at line level", () => { + const digitBearingToken = "ghp_" + "A9".repeat(18); + const hits = scanLineForRuleHits(`Authorization: Bearer ${digitBearingToken}`); + assert.ok(hits.some((hit) => hit.rule === "bearer-token"), "the bearer shape fires when the value mixes digits"); + assert.ok(hits.some((hit) => hit.rule === "github-token-classic"), "the token shape fires independently"); +}); + +// --------------------------------------------------------------------------- +// Git change collection on a real repository +// --------------------------------------------------------------------------- + +test("tracker file names that look like pathspec magic are still scanned", { skip: process.platform === "win32" && "colons and parentheses are not valid Windows file names" }, () => { + // git reads a leading ":(attr:x)" and glob characters as pathspec magic + // unless told to take paths literally. Tracker paths always start with a + // directory today, which keeps the magic inert; the gate passes + // --literal-pathspecs so that holds for every layout, and this guards it. + const { root, git } = initGateRepo(); + try { + for (const name of [":(attr:x)pm-test-magic.toon", "pm-test-[ab].toon"]) { + fs.writeFileSync(path.join(root, ".agents", "pm", "issues", name), `id: pm-test-magic\nbody: "${GH_TOKEN}"\n`); + assert.equal(git(["--literal-pathspecs", "add", `.agents/pm/issues/${name}`]).status, 0); + } + const report = runTrackerGate({ pmRoot: root }); + assert.strictEqual(report.verdict, "fail"); + assert.strictEqual(report.findings.filter((finding) => finding.rule === "github-token-classic").length, 2); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } +}); + +test("collectTrackerChange scans staged, working, and untracked tracker files", () => { + const { root, git } = initGateRepo(); + try { + const tracker = path.join(root, ".agents", "pm"); + // Staged: a new committed-adjacent file added to the index. + fs.writeFileSync(path.join(tracker, "issues", "pm-test-ccdd.toon"), 'id: pm-test-ccdd\ntitle: "Staged"\n'); + assert.strictEqual(git(["add", ".agents/pm/issues/pm-test-ccdd.toon"]).status, 0); + // Working: modify a tracked file without staging — a secret assignment. + fs.writeFileSync( + path.join(tracker, "issues", "pm-test-aabb.toon"), + `id: pm-test-aabb\ntitle: "Baseline item"\nsecret: "${HIGH_ENTROPY_SECRET}"\n`, + ); + // Untracked: brand new file, never staged. + fs.writeFileSync( + path.join(tracker, "issues", "pm-test-eeff.toon"), + `id: pm-test-eeff\ntitle: "Untracked"\nbody: "contact ${PERSONAL_EMAIL}"\n`, + ); + + const report = runTrackerGate({ pmRoot: root }); + assert.strictEqual(report.source, "git"); + assert.strictEqual(report.scanned_files, 3, "staged + working + untracked"); + assert.strictEqual(report.verdict, "fail"); + const rules = report.findings.map((finding) => finding.rule).sort(); + assert.deepStrictEqual(rules, ["email-address", "high-entropy-assignment"]); + const emailFinding = report.findings.find((finding) => finding.rule === "email-address")!; + assert.strictEqual(emailFinding.item_id, "pm-test-eeff"); + assert.strictEqual(emailFinding.field, "body"); + const entropyFinding = report.findings.find((finding) => finding.rule === "high-entropy-assignment")!; + assert.strictEqual(entropyFinding.item_id, "pm-test-aabb"); + assert.strictEqual(entropyFinding.field, "secret"); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } +}); + +test("collectTrackerChange ignores deletions, non-tracker files, and operational subpaths", () => { + const { root, git } = initGateRepo(); + try { + fs.rmSync(path.join(root, ".agents", "pm", "issues", "pm-test-aabb.toon")); + // Outside the tracker path entirely. + fs.writeFileSync(path.join(root, "notes.txt"), `token ${GH_TOKEN}\n`); + // Operational subpath: a transient lock and an installed extension file. + fs.mkdirSync(path.join(root, ".agents", "pm", "locks"), { recursive: true }); + fs.writeFileSync(path.join(root, ".agents", "pm", "locks", "sync.lock"), `{"pid":1,"token":"${GH_TOKEN}"}\n`); + fs.mkdirSync(path.join(root, ".agents", "pm", "extensions", "pm-github"), { recursive: true }); + fs.writeFileSync( + path.join(root, ".agents", "pm", "extensions", "pm-github", "README.md"), + `token ${GH_TOKEN}\n`, + ); + const report = runTrackerGate({ pmRoot: root }); + assert.strictEqual(report.verdict, "pass", "deletions and excluded paths contribute nothing"); + assert.strictEqual(report.scanned_files, 0); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } +}); + +test("collectTrackerChange fails closed outside a Git work tree and on Git failure", () => { + const plain = fs.mkdtempSync(path.join(os.tmpdir(), "pm-github-plain-")); + try { + assert.throws(() => runTrackerGate({ pmRoot: plain }), GateInputError); + } finally { + fs.rmSync(plain, { recursive: true, force: true }); + } + const failingGit = () => ({ ok: false, stdout: "", stderr: "boom" }); + assert.throws( + () => runTrackerGate({ pmRoot: "/unused", dependencies: { runGit: failingGit } }), + GateInputError, + ); + const { root } = initGateRepo(); + try { + const statusFailingGit = (cwd: string, args: readonly string[]) => + args.includes("status") + ? { ok: false, stdout: "", stderr: "status boom" } + : { ok: true, stdout: fs.realpathSync(root), stderr: "" }; + assert.throws( + () => runTrackerGate({ pmRoot: root, dependencies: { runGit: statusFailingGit } }), + /git status failed/, + ); + } finally { fs.rmSync(root, { recursive: true, force: true }); } +}); + +test("an oversized untracked tracker file fails closed instead of being skipped", () => { + const { root } = initGateRepo(); + try { + const huge = path.join(root, ".agents", "pm", "issues", "pm-test-huge.toon"); + fs.writeFileSync(huge, "x".repeat(8 * 1024 * 1024 + 1)); + assert.throws(() => runTrackerGate({ pmRoot: root }), (err: unknown) => err instanceof GateInputError + && err.message === "pm github gate: an untracked tracker file exceeds the 8388608-byte scan cap; refusing to guess."); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } +}); + +test("the default runner is the real git subprocess contract", () => { + const { root } = initGateRepo(); + try { + const ok = runGitDefault(root, ["rev-parse", "--show-toplevel"]); + assert.strictEqual(ok.ok, true); + assert.strictEqual(path.resolve(ok.stdout.trim()), path.resolve(root)); + const failed = runGitDefault(root, ["nope"]); + assert.strictEqual(failed.ok, false); + assert.ok(failed.stderr.length > 0); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } +}); + + +test("collectTrackerChange fails closed when the tracker escapes the Git work tree", () => { + const { root } = initGateRepo(); + const elsewhere = fs.mkdtempSync(path.join(os.tmpdir(), "pm-gate-elsewhere-")); + try { + assert.throws(() => runTrackerGate({ pmRoot: root, dependencies: { + runGit: () => ({ ok: true, stdout: elsewhere, stderr: "" }), + } }), (err: unknown) => err instanceof GateInputError && /escapes the Git work tree/.test(err.message)); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + fs.rmSync(elsewhere, { recursive: true, force: true }); + } +}); + +test("a vanished untracked tracker file fails closed", () => { + const { root } = initGateRepo(); + try { + assert.throws(() => runTrackerGate({ + pmRoot: root, + dependencies: { + runGit: (cwd, args) => { + if (args.includes("rev-parse")) return { ok: true, stdout: `${cwd}\n`, stderr: "" }; + if (args.includes("status")) { + return { + ok: true, + stdout: "?? .agents/pm/issues/ghost.toon\0", + stderr: "", + }; + } + return { ok: true, stdout: "", stderr: "" }; + }, + }, + }), GateInputError); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } +}); + +test("a Git diff failure for one changed tracker file fails the whole gate", () => { + const { root } = initGateRepo(); + try { + assert.throws( + () => + runTrackerGate({ + pmRoot: root, + dependencies: { + runGit: (cwd, args) => { + if (args.includes("rev-parse")) return { ok: true, stdout: `${cwd}\n`, stderr: "" }; + if (args.includes("status")) { + return { ok: true, stdout: " M .agents/pm/settings.json\0", stderr: "" }; + } + return { ok: false, stdout: "", stderr: "diff boom" }; + }, + }, + }), + (err: unknown) => { + assert.ok(err instanceof GateInputError); + assert.match(err.message, /git diff failed/); + return true; + }, + ); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } +}); + +test("a jsonl line without an id statement falls back to the history file name", () => { + const files = parseUnifiedDiff( + [ + "--- /dev/null", + "+++ b/.agents/pm/history/pm-x3.jsonl", + "@@ -0,0 +1 @@", + '+{"op":"add","path":"/metadata/body","value":"clean"}', + ].join("\n"), + ); + assert.strictEqual(files[0]!.itemId, "pm-x3"); +}); + +// --------------------------------------------------------------------------- +// Allowlist — content-hash keyed, with a written justification required +// --------------------------------------------------------------------------- + +function sha256Hex(value: string): string { + return createHash("sha256").update(value).digest("hex"); +} + +test("an allowlisted content hash is suppressed and counted, never widened to a pattern", () => { + const allowlistDirectory = fs.mkdtempSync(path.join(os.tmpdir(), "gate-allowlist-")); + const allowlist = path.join(allowlistDirectory, "allowlist.json"); + fs.writeFileSync( + allowlist, + JSON.stringify({ + [sha256Hex(GH_TOKEN)]: { reason: "Reviewed: synthetic fixture token in test corpus." }, + }), + ); + try { + const diff = [ + "--- /dev/null", + "+++ b/.agents/pm/issues/pm-allow.toon", + "@@ -0,0 +2,2 @@", + `+body: "token ${GH_TOKEN}"`, + `+body: "token ${NPM_TOKEN}"`, + ].join("\n"); + const report = runTrackerGate({ + pmRoot: "/unused", + diffFile: "synthetic.diff", + allowlistFile: allowlist, + dependencies: { readFileSync: (filePath: string) => (filePath.endsWith(".json") ? fs.readFileSync(filePath, "utf-8") : diff) }, + }); + assert.strictEqual(report.verdict, "fail", "only the reviewed hash is suppressed"); + assert.strictEqual(report.allowlisted, 1); + assert.deepStrictEqual( + report.findings.map((finding) => finding.rule), + ["npm-token"], + ); + } finally { + fs.rmSync(allowlistDirectory, { recursive: true, force: true }); + } +}); + +test("readGateAllowlist fails closed on every malformed shape", () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pm-github-allowlist-")); + try { + const write = (name: string, content: string): string => { + const file = path.join(dir, name); + fs.writeFileSync(file, content); + return file; + }; + // Valid file with a reason parses. + const valid = write("valid.json", JSON.stringify({ [sha256Hex("x")]: { reason: "ok" } })); + assert.strictEqual(readGateAllowlist(valid, true).size, 1); + // Missing optional is an empty allowlist; missing required fails. + const missing = path.join(dir, "missing.json"); + assert.strictEqual(readGateAllowlist(missing, false).size, 0); + assert.throws(() => readGateAllowlist(missing, true), GateInputError); + // Non-JSON, non-object, bad key, and missing reason each fail closed. + assert.throws(() => readGateAllowlist(write("bad.json", "{nope"), true), GateInputError); + assert.throws(() => readGateAllowlist(write("array.json", "[]"), true), GateInputError); + assert.throws(() => readGateAllowlist(write("key.json", JSON.stringify({ nothash: { reason: "x" } })), true), GateInputError); + assert.throws(() => readGateAllowlist(write("noreason.json", JSON.stringify({ [sha256Hex("x")]: {} })), true), GateInputError); + } finally { + fs.rmSync(dir, { recursive: true, force: true }); + } +}); + +test("the default repository-root allowlist is consulted when present", () => { + const { root, git } = initGateRepo(); + try { + fs.writeFileSync( + path.join(root, GATE_ALLOWLIST_FILENAME), + JSON.stringify({ [sha256Hex(PERSONAL_EMAIL)]: { reason: "Reviewed: public support address in docs item." } }), + ); + fs.writeFileSync( + path.join(root, ".agents", "pm", "issues", "pm-test-aabb.toon"), + `id: pm-test-aabb\ntitle: "Baseline item"\nbody: "mail ${PERSONAL_EMAIL}"\n`, + ); + const report = runTrackerGate({ pmRoot: root }); + assert.strictEqual(report.verdict, "pass"); + assert.strictEqual(report.allowlisted, 1); + assert.strictEqual(report.allowlist_path, GATE_ALLOWLIST_FILENAME); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } +}); + +// --------------------------------------------------------------------------- +// Fail-closed inputs and report shape +// --------------------------------------------------------------------------- + +test("an unreadable explicit diff file fails closed with a wrapped error", () => { + const plain = fs.mkdtempSync(path.join(os.tmpdir(), "pm-github-plain-")); + try { + assert.throws( + () => + runTrackerGate({ + pmRoot: plain, + diffFile: "missing.patch", + dependencies: { + runGit: () => ({ ok: false, stdout: "", stderr: "not a repo" }), + readFileSync: () => { + throw new Error("ENOENT"); + }, + }, + }), + (err: unknown) => { + assert.ok(err instanceof GateInputError); + assert.match(err.message, /could not read the proposed tracker change/); + return true; + }, + ); + } finally { + fs.rmSync(plain, { recursive: true, force: true }); + } +}); + +test("a clean change passes and the report is machine-readable without matched content", () => { + const { root } = initGateRepo(); + try { + const report = runTrackerGate({ pmRoot: root }); + assert.strictEqual(report.verdict, "pass"); + assert.deepStrictEqual(report.findings, []); + assert.strictEqual(report.scanned_files, 0); + assert.strictEqual(report.added_lines, 0); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } +}); + +test("formatGateReport findings never contain the matched content", () => { + const { root } = initGateRepo(); + try { + fs.writeFileSync( + path.join(root, ".agents", "pm", "issues", "pm-test-aabb.toon"), + `id: pm-test-aabb\ntitle: "Baseline item"\nbody: "token ${GH_TOKEN}"\n`, + ); + const report = runTrackerGate({ pmRoot: root }); + const lines = formatGateReport(report).join("\n"); + assert.ok(!lines.includes(GH_TOKEN), "the report must never echo the secret"); + assert.match(lines, /github-token-classic/); + assert.match(lines, /pm-test-aabb/); + assert.match(lines, /body/); + assert.match(lines, /sha256:[0-9a-f]{12}…/); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } +}); + +// --------------------------------------------------------------------------- +// Registered command — real dispatch through the extension test harness +// --------------------------------------------------------------------------- + +test("pm github gate passes a clean repo through the registered command", async () => { + const { root } = initGateRepo(); + try { + const report = await runGateCommand(root); + assert.strictEqual(report.verdict, "pass"); + assert.strictEqual(report.source, "git"); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } +}); + +test("pm github gate fails non-zero on a finding, without echoing the secret", async () => { + const ext = await harnessPromise; + const { root } = initGateRepo(); + try { + fs.writeFileSync( + path.join(root, ".agents", "pm", "issues", "pm-test-aabb.toon"), + `id: pm-test-aabb\ntitle: "Baseline item"\nbody: "token ${GH_TOKEN}"\n`, + ); + await assert.rejects( + ext.runCommand({ command: "github gate", global: { json: true }, pmRoot: root }), + (err: unknown) => { + assert.strictEqual((err as { exitCode?: number }).exitCode, 1); + const message = (err as Error).message; + assert.match(message, /FAIL/); + assert.match(message, /github-token-classic/); + assert.match(message, /pm-test-aabb/); + assert.ok(!message.includes(GH_TOKEN), "the error message must never echo the secret"); + return true; + }, + ); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } +}); + +test("pm github gate fails closed outside a Git work tree", async () => { + const ext = await harnessPromise; + const plain = fs.mkdtempSync(path.join(os.tmpdir(), "pm-github-plain-")); + try { + await assert.rejects( + ext.runCommand({ command: "github gate", global: { json: true }, pmRoot: plain }), + (err: unknown) => { + assert.strictEqual((err as { exitCode?: number }).exitCode, 1); + assert.match((err as Error).message, /not inside a Git work tree/); + return true; + }, + ); + } finally { + fs.rmSync(plain, { recursive: true, force: true }); + } +}); + +test("pm github gate scans an explicit --diff file and honors --allowlist", async () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pm-github-diff-")); + try { + const diffFile = path.join(dir, "change.patch"); + fs.writeFileSync( + diffFile, + ["--- /dev/null", "+++ b/.agents/pm/issues/pm-diff.toon", "@@ -0,0 +1 @@", `+body: "mail ${PERSONAL_EMAIL}"`].join("\n"), + ); + await assert.rejects( + runGateCommand("/unused", { diff: diffFile }), + (err: unknown) => { + assert.strictEqual((err as { exitCode?: number }).exitCode, 1); + assert.match((err as Error).message, /email-address/); + return true; + }, + ); + const allowlistFile = path.join(dir, "allow.json"); + fs.writeFileSync( + allowlistFile, + JSON.stringify({ [sha256Hex(PERSONAL_EMAIL)]: { reason: "Reviewed: support address in import docs." } }), + ); + const report = await runGateCommand("/unused", { diff: diffFile, allowlist: allowlistFile }); + assert.strictEqual(report.verdict, "pass"); + assert.strictEqual(report.source, "diff"); + assert.strictEqual(report.allowlisted, 1); + assert.strictEqual(report.scanned_files, 1); + // A malformed allowlist is refused with a clean, redaction-safe diagnostic. + fs.writeFileSync(allowlistFile, "{"); + await assert.rejects(runGateCommand("/unused", { diff: diffFile, allowlist: allowlistFile }), + (err: unknown) => err instanceof Error && err.message === "pm github gate: allowlist file is not valid JSON."); + } finally { + fs.rmSync(dir, { recursive: true, force: true }); + } +}); + +// --------------------------------------------------------------------------- +// Coverage additions: default-dependency collection, rename provenance tokens, +// within-file dedupe, context-only hunks, raw git failure surfaces, and the +// command's non-JSON report surface. +// --------------------------------------------------------------------------- + +test("collectTrackerChange runs against a real work tree with the default dependencies", () => { + const { root } = initGateRepo(); + try { + fs.writeFileSync(path.join(root, ".agents", "pm", "issues", "pm-test-ccdd.toon"), + `id: pm-test-ccdd\ntitle: "Fresh item"\nbody: "clean body"\n`); + const files = collectTrackerChange(path.join(root, ".agents", "pm")); + assert.equal(files.length, 1); + assert.equal(files[0]!.filePath, path.join(".agents", "pm", "issues", "pm-test-ccdd.toon")); + assert.equal(files[0]!.itemId, "pm-test-ccdd"); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } +}); + +test("a rename entry's original path is skipped and the new path is scanned", () => { + const { root, git } = initGateRepo(); + try { + const renamed = path.join(root, ".agents", "pm", "issues", "pm-test-ccdd.toon"); + assert.equal(git(["mv", ".agents/pm/issues/pm-test-aabb.toon", ".agents/pm/issues/pm-test-ccdd.toon"]).status, 0); + fs.writeFileSync(renamed, `id: pm-test-ccdd\ntitle: "Renamed item"\nbody: "token ${GH_TOKEN}"\n`); + const report = runTrackerGate({ pmRoot: root }); + assert.equal(report.verdict, "fail"); + assert.equal(report.findings.length, 1); + assert.equal(report.findings[0]!.item_id, "pm-test-ccdd"); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } +}); + +test("identical repeated findings inside one file are reported once", () => { + const diff = [ + "--- a/.agents/pm/issues/pm-dedupe.jsonl", + "+++ b/.agents/pm/issues/pm-dedupe.jsonl", + "@@ -0,0 +1,2 @@", + `+{"patch":[{"op":"add","path":"/body","value":"token ${GH_TOKEN}"}]}`, + `+{"patch":[{"op":"add","path":"/body","value":"token ${GH_TOKEN}"}]}`, + ].join("\n"); + const { root } = initGateRepo(); + try { + const file = path.join(root, "repeat.diff"); + fs.writeFileSync(file, diff); + const report = runTrackerGate({ pmRoot: root, diffFile: file }); + assert.equal(report.verdict, "fail"); + assert.equal(report.findings.length, 1); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } +}); + +test("a context-only hunk contributes a scanned file with no added lines", () => { + const diff = [ + "--- a/.agents/pm/issues/pm-context.toon", + "+++ b/.agents/pm/issues/pm-context.toon", + "@@ -1,1 +1,1 @@", + " title: \"Unchanged\"", + ].join("\n"); + const { root } = initGateRepo(); + try { + const file = path.join(root, "context.diff"); + fs.writeFileSync(file, diff); + const report = runTrackerGate({ pmRoot: root, diffFile: file }); + assert.equal(report.verdict, "pass"); + assert.equal(report.scanned_files, 1); + assert.equal(report.added_lines, 0); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } +}); + +test("a finding outside any item is rendered as no item and unknown field", () => { + const diff = [ + "--- /dev/null", + "+++ b/notes/general.md", + "@@ -0,0 +1 @@", + `+shared token ${GH_TOKEN}`, + ].join("\n"); + const { root } = initGateRepo(); + try { + const file = path.join(root, "noitem.diff"); + fs.writeFileSync(file, diff); + const report = runTrackerGate({ pmRoot: root, diffFile: file }); + assert.equal(report.verdict, "fail"); + const lines = formatGateReport(report).join("\n"); + assert.match(lines, /\(no item\)/); + assert.match(lines, /github-token-classic/); + assert.ok(!lines.includes(GH_TOKEN)); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } +}); + +test("a gate-scoped collaborator error passes through unwrapped", () => { + const { root } = initGateRepo(); + try { + // An injected reader that raises its own gate-prefixed diagnostic must + // surface verbatim rather than being re-labelled as unreadable input. + assert.throws(() => runTrackerGate({ + pmRoot: root, + diffFile: path.join(root, "change.patch"), + dependencies: { readFileSync: () => { throw new Error("pm github gate: injected reader failure"); } }, + }), (err: unknown) => err instanceof Error && /injected reader failure/.test(err.message)); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } +}); + +test("runGitDefault reports a missing git executable without throwing", () => { + // A child process with a PATH that cannot resolve git proves the raw runner + // is fail-closed: ENOENT becomes ok:false with the spawn error in stderr. + nodeScenario(` + import assert from 'node:assert/strict'; + import { runGitDefault } from ${JSON.stringify(new URL("../gate.ts", import.meta.url).href)}; + const result = runGitDefault(${JSON.stringify(path.resolve(os.tmpdir()))}, ["status"]); + assert.equal(result.ok, false); + assert.equal(result.stdout, ""); + assert.match(result.stderr, /ENOENT/); + `, { PATH: "" }); +}); + +test("the registered gate command prints its report outside JSON mode", async () => { + const ext = await harnessPromise; + const { root } = initGateRepo(); + try { + const { stderr, result } = await captureStderr(async () => + ext.runCommand({ command: "github gate", global: { json: false }, pmRoot: root })); + assert.strictEqual((result as { result: GateReport }).result.verdict, "pass"); + assert.match(stderr.join("\n"), /pm github gate: PASS/); + assert.match(stderr.join("\n"), /0 finding\(s\)/); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } +}); + +test("the shared command gate redacts unexpected Git collaborator errors", () => { + const { root } = initGateRepo(); + try { + const original = runGitDefault; + let topLevelCalls = 0; + assert.throws(() => runCommandTrackerGate({ pmRoot: root, dependencies: { + runGit: (cwd, args) => { + if (args.join(" ") === "rev-parse --show-toplevel" && ++topLevelCalls === 2) throw new Error("private diagnostic fixture"); + return original(cwd, args); + }, + } }), (error: unknown) => error instanceof Error && error.message === "pm github gate: scanner error; scan did not complete."); + assert.throws(() => runCommandTrackerGate({ pmRoot: path.join(root, "missing") }), /pm github gate:/); + assert.equal(runCommandTrackerGate({ pmRoot: root }).verdict, "pass"); + } finally { fs.rmSync(root, { recursive: true, force: true }); } +}); diff --git a/test/generate-changelog.test.ts b/test/generate-changelog.test.ts new file mode 100644 index 00000000..a8b4111a --- /dev/null +++ b/test/generate-changelog.test.ts @@ -0,0 +1,80 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { spawnSync } from "node:child_process"; +import { create, close } from "@unbrained/pm-cli/sdk"; +import { fileURLToPath } from "node:url"; +import { generateChangelog, generateChangelogIfMain } from "../scripts/generate-changelog.ts"; + +test("changelog main entry fails closed when the tracker cannot be read", async () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "pm-github-changelog-main-")); + const previous = process.exitCode; + try { + const moduleUrl = new URL("../scripts/generate-changelog.ts", import.meta.url).href; + await generateChangelogIfMain(["node", fileURLToPath(moduleUrl)], moduleUrl, root, []); + assert.equal(process.exitCode, 1); + } finally { + process.exitCode = previous ?? 0; + fs.rmSync(root, { recursive: true, force: true }); + } +}); + +test("canonical changelog includes tagged pending work without closing it or admitting other claims", async () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "pm-github-changelog-")); + try { + assert.equal(spawnSync("git", ["init", "-b", "main", root]).status, 0); + fs.writeFileSync(path.join(root, "package.json"), JSON.stringify({ version: "2026.10.5" })); + const pmRoot = path.join(root, ".agents/pm"); + const cli = path.resolve("node_modules/.bin/pm"); + assert.equal(spawnSync(cli, ["--pm-path", pmRoot, "init", "--defaults", "--agent-guidance", "skip"], { cwd: root }).status, 0); + const client = { cwd: root, pmRoot, noExtensions: true }; + const pending = await create({ title: "Pending privacy gate", status: "in_progress", tags: "changelog-unreleased" }, client); + await create({ title: "Unrelated open claim", status: "in_progress" }, client); + const shipped = await create({ title: "Shipped behavior" }, client); + await close(shipped.item.id, "Fixture completion", {}, client); + assert.equal(await generateChangelog(root, []), 0); + const text = fs.readFileSync(path.join(root, "CHANGELOG.md"), "utf8"); + assert.ok(text.includes("Pending privacy gate")); + assert.ok(text.includes("Shipped behavior")); + assert.ok(!text.includes("Unrelated open claim")); + assert.equal(await generateChangelog(root, ["--check"]), 0); + fs.appendFileSync(path.join(root, "CHANGELOG.md"), "\nDrift\n"); + assert.equal(await generateChangelog(root, ["--check", "--no-check-diff"]), 1); + const previousPath = process.env.PATH; + const emptyBin = path.join(root, "empty-bin"); + fs.mkdirSync(emptyBin); + process.env.PATH = emptyBin; + const previousError = console.error; + const diagnostics: string[] = []; + console.error = (...values: unknown[]) => { diagnostics.push(values.join(" ")); }; + try { + assert.equal(await generateChangelog(root, []), 1); + process.env.PATH = previousPath; + assert.equal(await generateChangelog(root, [], () => ({ status: null, signal: null, error: new Error("synthetic spawn failure"), pid: 0, output: [], stdout: "", stderr: "" })), 1); + assert.ok(diagnostics.some(line => line.includes("could not spawn"))); + assert.equal(await generateChangelog(root, [], () => ({ status: null, signal: "SIGTERM", pid: 0, output: [], stdout: "", stderr: "" })), 1); + assert.ok(diagnostics.some(line => line.includes("SIGTERM"))); + } finally { + console.error = previousError; + process.env.PATH = previousPath; + } + const itemFile = path.join(pmRoot, "tasks", `${pending.item.id}.toon`); + assert.match(fs.readFileSync(itemFile, "utf8"), /status: in_progress/); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } +}); + +test("canonical changelog fails closed when the tracker read fails", async () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "pm-github-changelog-error-")); + try { + fs.mkdirSync(path.join(root, ".agents"), { recursive: true }); + fs.writeFileSync(path.join(root, ".agents/pm"), "invalid tracker directory"); + assert.equal(await generateChangelog(root, []), 1); + assert.ok(!fs.existsSync(path.join(root, "CHANGELOG.md"))); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } +}); diff --git a/test/handler-failures.test.ts b/test/handler-failures.test.ts index f1436afd..75441367 100644 --- a/test/handler-failures.test.ts +++ b/test/handler-failures.test.ts @@ -74,7 +74,9 @@ function pmSetup(root: string, args: readonly string[]): void { // `pm list --all` still works. Used for the no-token guards whose handlers read // the tracker before checking the token. const PM_BIN_DIR = fileURLToPath(new URL("../node_modules/.bin", import.meta.url)); -const GH_FREE_PATH = `${PM_BIN_DIR}${path.delimiter}${path.dirname(process.execPath)}`; +const nodeExecutable = spawnSync("node", ["-p", "process.execPath"], { encoding: "utf8" }).stdout.trim(); +assert.ok(nodeExecutable, "pm fixture executables require Node"); +const GH_FREE_PATH = `${PM_BIN_DIR}${path.delimiter}${path.dirname(nodeExecutable)}`; /** Create a throwaway pm workspace (`pm init test`) and return its root. */ function freshTracker(): string { diff --git a/test/helpers/bounded-cli.ts b/test/helpers/bounded-cli.ts new file mode 100644 index 00000000..794e92fd --- /dev/null +++ b/test/helpers/bounded-cli.ts @@ -0,0 +1,45 @@ +/** Execute installed-CLI acceptance with a wall-clock deadline and forced cleanup. */ +import { spawn } from "node:child_process"; +export interface BoundedCliResult { + code: number | null; + signal: string | null; + stdout: string; + stderr: string; + seconds: number; + timedOut: boolean; +} +/** Terminate at the original deadline; force-kill after a short cleanup window. */ +export function boundedCli(command: string, args: string[], cwd: string, env: NodeJS.ProcessEnv, + deadlineMs = 45_000, cleanupMs = 5_000): Promise { + const started = performance.now(); + return new Promise((resolve, reject) => { + const child = spawn(command, args, { cwd, env, detached: process.platform !== "win32" }); + let stdout = ""; + let stderr = ""; + let timedOut = false; + let cleanup: ReturnType | undefined; + const kill = (signal: NodeJS.Signals) => { + try { + if (process.platform !== "win32" && child.pid) process.kill(-child.pid, signal); + else child.kill(signal); + } catch { /* The process may exit between deadline and signal delivery. */ } + }; + const deadline = setTimeout(() => { + timedOut = true; + kill("SIGTERM"); + cleanup = setTimeout(() => kill("SIGKILL"), cleanupMs); + }, deadlineMs); + // Decode the streams, not each chunk: a multibyte character split across + // two chunks would otherwise become two U+FFFD replacement characters. + child.stdout.setEncoding("utf8"); + child.stderr.setEncoding("utf8"); + child.stdout.on("data", (chunk: string) => { stdout += chunk; }); + child.stderr.on("data", (chunk: string) => { stderr += chunk; }); + child.on("error", error => { clearTimeout(deadline); if (cleanup) clearTimeout(cleanup); reject(error); }); + child.on("close", (code, signal) => { + clearTimeout(deadline); + if (cleanup) clearTimeout(cleanup); + resolve({ code, signal, stdout, stderr, timedOut, seconds: (performance.now() - started) / 1000 }); + }); + }); +} diff --git a/test/helpers/mock-github-server.ts b/test/helpers/mock-github-server.ts index 072b5685..715f3cf9 100644 --- a/test/helpers/mock-github-server.ts +++ b/test/helpers/mock-github-server.ts @@ -106,7 +106,7 @@ export function startMockGithub(handler: MockGithubHandler): Promise - new Promise((resolveClose) => server.close(() => resolveClose())), + new Promise((resolveClose) => { server.close(() => resolveClose()); }), }); }); }); @@ -127,6 +127,10 @@ export function jsonResponse( for (const [k, v] of Object.entries(headers)) res.setHeader(k, v); } res.setHeader("Content-Type", "application/json"); + // Synchronous real-CLI work can outlast an idle keep-alive socket between + // requests. Close fixture connections explicitly rather than reuse a stale + // socket when the event loop resumes after git/PM subprocesses. + res.setHeader("Connection", "close"); res.end(typeof body === "string" ? body : JSON.stringify(body)); } diff --git a/test/helpers/node-scenario.ts b/test/helpers/node-scenario.ts new file mode 100644 index 00000000..efe8fac6 --- /dev/null +++ b/test/helpers/node-scenario.ts @@ -0,0 +1,21 @@ +import assert from "node:assert/strict"; +import { spawnSync } from "node:child_process"; + +const nodeExecutable = spawnSync("node", ["-p", "process.execPath"], { encoding: "utf8" }).stdout.trim(); +assert.ok(nodeExecutable, "lifecycle fixtures require a Node executable"); + +/** + * Execute lifecycle and executable-lookup scenarios in a fresh Node process. + * + * Bun can retain the original executable search path after process.env changes. + * The child receives its environment at startup, keeping host tools and tokens + * outside failure fixtures. Native harness behavior stays in the parent suite. + */ +export function nodeScenario(code: string, environment: Record = {}): void { + const env = { ...process.env, ...environment }; + delete env.NODE_TEST_CONTEXT; + const result = spawnSync(nodeExecutable, ["--input-type=module", "--eval", code], { + encoding: "utf8", env, timeout: 60000, + }); + assert.equal(result.status, 0, result.error?.message ?? `${result.stderr}\n${result.stdout}`); +} diff --git a/test/helpers/public-acceptance.ts b/test/helpers/public-acceptance.ts new file mode 100644 index 00000000..8196c84d --- /dev/null +++ b/test/helpers/public-acceptance.ts @@ -0,0 +1,92 @@ +/** Packed public-data acceptance; opt-in command, never a public writer. */ +import assert from "node:assert/strict"; +import { spawnSync } from "node:child_process"; +import { createHash } from "node:crypto"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { boundedCli } from "./bounded-cli.ts"; +import { startMockGithub, jsonResponse } from "./mock-github-server.ts"; +import type { GhIssue, GhComment } from "../../index.ts"; + +const packageRoot = path.resolve(import.meta.dirname, "../.."); +const base = fs.mkdtempSync(path.join(os.tmpdir(), "pm-packed-public-")); +const root = path.join(base, "work"); +fs.mkdirSync(root); +const cli = fs.realpathSync(path.join(packageRoot, "node_modules/.bin/pm")); +const env: NodeJS.ProcessEnv & { PM_PATH: string } = { ...process.env, PM_AUTHOR: "codex-sol", PM_PATH: path.join(root, ".agents/pm"), + PATH: path.dirname(cli) + path.delimiter + path.join(packageRoot, "node_modules/.bin") + path.delimiter + process.env.PATH }; +delete env.PM_GITHUB_API_BASE; +delete env.NODE_TEST_CONTEXT; +delete env.NODE_V8_COVERAGE; +/** + * Run a fixture command and require success. + * + * @param bin - Executable to run. + * @param args - Arguments for the executable. + * @param cwd - Working directory (the fixture workspace by default). + * @param environment - Environment for this call (the fixture env by default). + * @returns The command's stdout. + */ +function setup(bin: string, args: string[], cwd = root, environment: NodeJS.ProcessEnv = env): string { + const result = spawnSync(bin, args, { cwd, env: environment, encoding: "utf8", timeout: 45000 }); + assert.equal(result.status, 0, result.stderr); + return result.stdout; +} +async function run(bin: string, args: string[]): Promise<{ seconds: number; output: string }> { + const before = snapshot(); + const result = await boundedCli(bin, args, root, env); + if (result.timedOut) assert.equal(snapshot(), before, "timed-out acceptance must preserve durable tracker bytes"); + assert.equal(result.timedOut, false, "installed CLI exceeded its original 45s deadline"); + assert.equal(result.code, 0, result.stderr); + return { seconds: result.seconds, output: result.stdout }; +} + +function snapshot(): string { + const files = setup("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard", "--", ".agents/pm"]) + .split("\0").filter(file => file && !["extensions", "locks", "runtime", "transactions", "checkpoints"].includes(file.slice(".agents/pm/".length).split("/")[0]!)).sort(); + const digest = createHash("sha256"); + for (const file of files) digest.update(file).update(fs.readFileSync(path.join(root, file))); + return digest.digest("hex"); +} +let server: Awaited> | undefined; +try { + const issues = setup("gh", ["api", "repos/unbraind/pm-todos/issues?state=all&since=2026-10-01T00:00:00Z&per_page=100", "--paginate", "--jq", ".[] | select(.pull_request == null) | @json"]).trim().split("\n").filter(Boolean).map(line => JSON.parse(line) as GhIssue); + assert.ok(issues.length > 0); + const comments = new Map(); + for (const issue of issues) comments.set(issue.number, setup("gh", ["api", `repos/unbraind/pm-todos/issues/${issue.number}/comments?per_page=100`, "--paginate", "--jq", ".[] | @json"]).trim().split("\n").filter(Boolean).map(line => JSON.parse(line) as GhComment)); + server = await startMockGithub((request, response) => { + const match = /issues\/(\d+)\/comments/.exec(request.url ?? ""); + jsonResponse(response, 200, match ? comments.get(Number(match[1])) : issues); + }); + env.PM_GITHUB_API_BASE = server.baseUrl; + setup("git", ["init", "-q", "-b", "main"]); + setup("git", ["config", "user.name", "Fixture"]); + setup("git", ["config", "user.email", "fixture@example.invalid"]); + setup("node", [cli, "--path", env.PM_PATH, "init", "fixture"]); + // npm 10 runs `prepare` even with --ignore-scripts: give it this checkout's + // tracker (not the fixture PM_PATH) and keep its stdout out of the JSON + // receipt, exactly like packWorkflowCandidate in test/import-gate.test.ts. + const packed = JSON.parse(setup("npm", ["pack", "--ignore-scripts", "--foreground-scripts=false", "--json", "--pack-destination", base], packageRoot, + { ...env, PM_PATH: path.join(packageRoot, ".agents", "pm") })) as [{ filename: string }]; + setup("node", [cli, "package", "install", path.join(base, packed[0].filename), "--project"]); + const args = [cli, "github", "import", "unbraind/pm-todos", "--state", "all", "--since", "2026-10-01T00:00:00Z", "--atomic", "--gate", "--with-comments", "--json"]; + console.log("public snapshot: imported through the packed Node/Bun CLI HTTP boundary"); + const first = await run("node", args); + const result = JSON.parse(first.output) as { imported: number; gate: { verdict: string } }; + assert.ok(result.imported > 0); + assert.equal(result.gate.verdict, "pass"); + const before = snapshot(); + const repeats: number[] = []; + for (const bin of ["node", "bun", "bun", "bun"]) { + console.log(`${bin} repeat starting`); + const repeat = await run(bin, bin === "bun" ? ["--bun", ...args] : args); + assert.equal(snapshot(), before, "repeat must preserve all durable tracker bytes"); + assert.equal((JSON.parse(repeat.output) as { gate: { verdict: string } }).gate.verdict, "pass"); + repeats.push(repeat.seconds); + } + await run("node", [cli, "health", "--strict-exit", "--json"]); + assert.equal(setup("git", ["remote"]).trim(), ""); + console.log(JSON.stringify({ imported: result.imported, gate: result.gate.verdict, import_seconds: first.seconds, + node_repeat_seconds: repeats[0], bun_repeat_seconds: repeats.slice(1), strict_health: "pass", live_public_snapshot: true, since: "2026-10-01", no_remote: true })); +} finally { await server?.close(); fs.rmSync(base, { recursive: true, force: true }); } diff --git a/test/helpers/read-only-directory.ts b/test/helpers/read-only-directory.ts new file mode 100644 index 00000000..d363e432 --- /dev/null +++ b/test/helpers/read-only-directory.ts @@ -0,0 +1,26 @@ +import { chmodSync, statSync } from "node:fs"; + +/** + * Run `fn` while `directory` is temporarily read-only, restoring permissions afterwards. + * + * Applies mode 0o555 before invoking `fn` and restores the previous mode in a + * `finally` block, so a throwing callback cannot leave the directory locked. + * The permission change has no effect when the process runs as root. + * + * @param directory - Existing directory to make read-only for the duration. + * @param fn - Callback invoked with the directory in read-only mode. + * @returns Whatever `fn` resolves to; re-throwing its rejection only after the + * original directory mode has been restored. + */ +export async function withReadOnlyDirectory( + directory: string, + fn: () => T | Promise, +): Promise { + const previousMode = statSync(directory).mode & 0o777; + chmodSync(directory, 0o555); + try { + return await fn(); + } finally { + chmodSync(directory, previousMode); + } +} \ No newline at end of file diff --git a/test/http-boundary.test.ts b/test/http-boundary.test.ts index fbe18036..3d91aeff 100644 --- a/test/http-boundary.test.ts +++ b/test/http-boundary.test.ts @@ -16,13 +16,14 @@ // fallback, the 60s cap. // - fetchAllIssues: Link-header pagination (empty/single/multi page), and the // malformed-JSON / non-array response errors. -// - fetchComments: the no-comments short-circuit, pagination, and graceful -// handling of a malformed page mid-stream. +// - fetchComments: the no-comments short-circuit, pagination, and refusal +// of a malformed page mid-stream. // - runImport: the 404 → NOT_FOUND and unauthenticated-403 → token-hint error // mappings (the failure surface the import command exposes to the shell). import assert from "node:assert/strict"; import test from "node:test"; +import { createServer } from "node:http"; import { CommandError, @@ -33,7 +34,11 @@ import { fetchJSON, githubApiBase, parseImportOptions, + isRetryableStatus, + parseRateLimit, runImport, + type GhIssue, + type GhComment, } from "../index.ts"; import { @@ -51,7 +56,7 @@ const IMPORT_OPTS = parseImportOptions({}); // Minimal GitHub issue factory for the pagination fixtures. Typed against the // exported GhIssue so a field rename fails the compile. -function ghIssue(number: number): import("../index.ts").GhIssue { +function ghIssue(number: number): GhIssue { return { number, title: `t${number}`, @@ -66,7 +71,7 @@ function ghIssue(number: number): import("../index.ts").GhIssue { }; } -function ghComment(id: number): import("../index.ts").GhComment { +function ghComment(id: number): GhComment { return { id, user: { login: "alice" }, created_at: "2026-01-01T00:00:00Z", body: `c${id}` }; } @@ -355,7 +360,7 @@ test("fetchAllIssues throws on a non-array (object) response", async () => { test("fetchComments skips the network entirely when the issue has no comments", async () => { await withMockGithub((_req, res) => jsonResponse(res, 200, []), async (server) => { - const issue: import("../index.ts").GhIssue = { + const issue: GhIssue = { number: 1, title: "t", body: null, state: "open", labels: [], assignee: null, milestone: null, created_at: "", updated_at: "", html_url: "", comments: 0, @@ -376,7 +381,7 @@ test("fetchComments pages through the comments Link header", async () => { }); } }, async () => { - const issue: import("../index.ts").GhIssue = { + const issue: GhIssue = { number: 1, title: "t", body: null, state: "open", labels: [], assignee: null, milestone: null, created_at: "", updated_at: "", html_url: "", comments: 3, @@ -386,7 +391,7 @@ test("fetchComments pages through the comments Link header", async () => { }); }); -test("fetchComments tolerates a malformed page mid-stream (keeps earlier pages)", async () => { +test("fetchComments rejects a malformed page mid-stream", async () => { await withMockGithub((req, res, _body, baseUrl) => { if ((req.url ?? "").includes("page=2")) { res.setHeader("Content-Type", "text/plain"); @@ -397,13 +402,12 @@ test("fetchComments tolerates a malformed page mid-stream (keeps earlier pages)" }); } }, async () => { - const issue: import("../index.ts").GhIssue = { + const issue: GhIssue = { number: 1, title: "t", body: null, state: "open", labels: [], assignee: null, milestone: null, created_at: "", updated_at: "", html_url: "", comments: 1, }; - const out = await fetchComments(issue, "a/b", "t"); - assert.deepEqual(out.map((c) => c.id), [1], "page-1 comments are kept; the malformed page breaks the loop"); + await assert.rejects(fetchComments(issue, "a/b", "t"), /not valid JSON/); }); }); @@ -508,3 +512,97 @@ test("githubApiBase strips a trailing slash so paths cannot double up", async () assert.strictEqual(githubApiBase(), "http://127.0.0.1:39291", "a trailing slash would yield //repos/..."); }); }); + +// --------------------------------------------------------------------------- +// Coverage additions: transport dispatch, redirect validation, the 30s +// request timeout, array/mixed-case header shapes, and the comments non-array +// refusal — the remaining reachable arms of the shared request stack. +// --------------------------------------------------------------------------- + +test("fetchJSON reaches an https target through the https transport and reports a refusal", async () => { + // Port 9 (discard) on loopback is guaranteed-unbound: the request is built + // and dispatched through the real https module, then rejected by the OS. + await assert.rejects(fetchJSON("https://127.0.0.1:9/repos/a/b"), /ECONNREFUSED/); +}); + +test("requestOnce rejects an unparseable redirect Location instead of following it", async () => { + await withMockGithub((_req, res) => { + // `http://[` is syntactically invalid as an absolute URL, so resolving the + // Location fails and the request must fail loudly rather than mis-follow. + res.statusCode = 302; + res.setHeader("Location", "http://["); + res.end(); + }, async (server) => { + await assert.rejects(fetchJSON(`${server.baseUrl}/repos/a/b`), /invalid redirect Location/); + }); +}); + +test("requestOnce fails a request after 30s without a response", { timeout: 45_000 }, async () => { + // Real silent headers and a never-ending response body both retain the + // original 30-second deadline, including under native Bun. + const server = createServer((req, res) => { + if (req.url?.endsWith("stream")) { res.writeHead(200); res.write("partial"); } + }); + await new Promise((resolve) => { server.listen(0, "127.0.0.1", resolve); }); + const port = (server.address() as { port: number }).port; + try { + const started = performance.now(); + await Promise.all(["silent", "stream"].map(route => assert.rejects( + fetchJSON(`http://127.0.0.1:${port}/repos/a/${route}`), + /request timed out after 30s/, + ))); + assert.ok(performance.now() - started >= 29_000); + assert.ok(performance.now() - started < 40_000); + } finally { + server.closeAllConnections(); + await new Promise((resolve) => { server.close(() => resolve()); }); + } +}); + +test("computeBackoffMs reads the first value of repeated array headers", () => { + // Node delivers repeated response headers as arrays; the helper must use the + // first value, not the raw array. + assert.equal(computeBackoffMs({ "retry-after": ["3", "9"] }, 0), 3000); + assert.equal(computeBackoffMs({ "retry-after": ["bogus", "2"] }, 0), 1000); +}); + +test("isRetryableStatus honors mixed-case and array rate-limit headers", async () => { + assert.equal(isRetryableStatus(429, {}), true); + assert.equal(isRetryableStatus(500, {}), true); + assert.equal(isRetryableStatus(404, {}), false); + assert.equal(isRetryableStatus(403, {}), false); + // A primary rate limit with a mixed-case header name still reads as retryable. + assert.equal(isRetryableStatus(403, { "X-RateLimit-Remaining": "0" }), true); + assert.equal(isRetryableStatus(403, { "x-ratelimit-remaining": ["0", "5"] }), true); + // A secondary rate limit announced through Retry-After, mixed-case or array. + assert.equal(isRetryableStatus(403, { "Retry-After": "120" }), true); + assert.equal(isRetryableStatus(403, { "retry-after": ["60"] }), true); +}); + +test("parseRateLimit drops non-numeric header values", async () => { + assert.deepEqual( + parseRateLimit({ "x-ratelimit-remaining": "soon", "x-ratelimit-limit": "many", "x-ratelimit-reset": "later" }), + { remaining: undefined, limit: undefined, reset: undefined, low: false }, + ); + assert.equal(parseRateLimit({ "x-ratelimit-remaining": ["4"] }).low, true); +}); + +test("fetchComments rejects a non-array comments page", async () => { + await withMockGithub((req, res, _body, baseUrl) => { + if (parseNextLinkHeaderPage(req.url ?? "") === 0) { + jsonResponse(res, 200, [ghComment(1)], { Link: nextLinkHeader(baseUrl, "/repos/a/b/issues/1/comments?per_page=100&page=2") }); + } else { + // A later non-array page must refuse the incomplete read. + jsonResponse(res, 200, { message: "unexpected shape" }); + } + }, async () => { + const issue: GhIssue = { ...ghIssue(1), comments: 2 }; + await assert.rejects(fetchComments(issue, "a/b", "t"), /must be an array/); + }); +}); + +/** Page index encoded in a mock URL's query string (0 when absent). */ +function parseNextLinkHeaderPage(url: string): number { + const match = /[?&]page=(\d+)/.exec(url); + return match ? Number(match[1]) : 0; +} diff --git a/test/import-errors.test.ts b/test/import-errors.test.ts new file mode 100644 index 00000000..e03be06a --- /dev/null +++ b/test/import-errors.test.ts @@ -0,0 +1,142 @@ +import assert from "node:assert/strict"; +import { spawnSync } from "node:child_process"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import test, { type TestContext } from "node:test"; +import { + EXIT_CODE, acquireImportLock, importCommentSyncLockPath, parseImportOptions, + readPmItems, resolveCommitItemMutations, runImport, syncGithubCommentsToAnnotations, + type CommandError, type GhIssue, +} from "../index.ts"; +import { captureStderr, jsonResponse, withMockGithub } from "./helpers/mock-github-server.ts"; + +/** Create a real local PM tracker with cleanup attached before setup. */ +function tracker(t: TestContext): string { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "pm-import-errors-")); + t.after(() => fs.rmSync(root, { recursive: true, force: true })); + pm(root, ["init", "fixture"]); + return root; +} + +/** Execute the installed PM CLI; setup errors cannot masquerade as failures. */ +function pm(root: string, args: string[]): string { + const result = spawnSync("pm", ["--path", root, ...args], { encoding: "utf8", env: { ...process.env } }); + assert.equal(result.status, 0, result.stderr); + return result.stdout; +} + +/** Synthetic GitHub issue with optional lifecycle and content overrides. */ +function issue(overrides: Partial = {}): GhIssue { + return { number: 1, title: "Remote fixture", body: "Original body", state: "open", labels: [], assignee: null, milestone: null, + html_url: "https://github.com/acme/widgets/issues/1", created_at: "2026-01-01T00:00:00Z", updated_at: "2026-01-02T00:00:00Z", ...overrides }; +} + +test("import preserves semantic fetch errors and gives unauthenticated quota guidance", async t => { + const root = tracker(t); + for (const status of [403, 404]) { + await withMockGithub((_req, res) => jsonResponse(res, status, { message: "Fixture denied" }), async () => { + await assert.rejects(runImport("acme/widgets", root, parseImportOptions({}), { resolveToken: () => undefined }), error => { + assert.equal((error as CommandError).exitCode, status === 404 ? EXIT_CODE.NOT_FOUND : EXIT_CODE.GENERIC_FAILURE); + assert.match((error as Error).message, new RegExp(`HTTP ${status}`)); + if (status === 403) assert.match((error as Error).message, /raise the rate limit/); + return true; + }); + }); + } + assert.deepEqual(readPmItems(root), []); +}); + +test("atomic previews distinguish empty upstream data from entirely skipped issue titles", async t => { + const root = tracker(t); + 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 }); + 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 }); + await assert.rejects(runImport("acme/widgets", root, parseImportOptions({ atomic: true }), { resolveToken: () => undefined }), /Imported 0 issue\(s\); 1 failed/); + }); + assert.deepEqual(readPmItems(root), []); +}); + +test("ungated import tolerates unavailable comments and still writes the issue", async t => { + const root = tracker(t); + await withMockGithub((req, res) => { + if (req.url?.includes("/comments")) jsonResponse(res, 422, { message: "Comments unavailable" }); + 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.match(stderr.join("\n"), /failed to fetch comments/); + }); + assert.equal(readPmItems(root)[0].body, "Original body"); +}); + +test("import reports real mutation failures after a linked local item disappears", async t => { + const root = tracker(t); + const created = JSON.parse(pm(root, ["create", "task", "Linked", "--tags", "gh:acme/widgets#1", "--json"])) as { id: string }; + const snapshot = readPmItems(root); + pm(root, ["delete", created.id, "--message", "Fixture concurrent deletion"]); + await withMockGithub((_req, res) => jsonResponse(res, 200, [issue()]), async () => { + const { stderr } = await captureStderr(async () => { + await assert.rejects(runImport("acme/widgets", root, parseImportOptions({}), { resolveToken: () => undefined, readItems: () => snapshot }), /Imported 0 issue\(s\); 1 failed/); + }); + assert.match(stderr.join("\n"), /update failed/); + }); + assert.deepEqual(readPmItems(root), []); +}); + +test("import rejects an unknown item type and allows a corrected retry", async t => { + const root = tracker(t); + await withMockGithub((_req, res) => jsonResponse(res, 200, [issue()]), async () => { + const { stderr } = await captureStderr(async () => { + await assert.rejects(runImport("acme/widgets", root, parseImportOptions({ type: "UnsupportedFixtureType" }), { resolveToken: () => undefined }), /Imported 0 issue\(s\); 1 failed/); + }); + assert.match(stderr.join("\n"), /create failed/); + const result = await runImport("acme/widgets", root, parseImportOptions({}), { resolveToken: () => undefined }); + assert.ok("imported" in result); + assert.equal(result.imported, 1); + }); +}); + +test("import records failed close reconciliation for an already canceled item", async t => { + const root = tracker(t); + pm(root, ["create", "task", "Canceled", "--status", "canceled", "--tags", "gh:acme/widgets#1"]); + await withMockGithub((_req, res) => jsonResponse(res, 200, [issue({ state: "closed" })]), async () => { + const { stderr } = await captureStderr(async () => { + await assert.rejects(runImport("acme/widgets", root, parseImportOptions({}), { resolveToken: () => undefined }), /Imported 0 issue\(s\); 1 failed/); + }); + assert.match(stderr.join("\n"), /close reconciliation failed/); + }); + assert.equal(readPmItems(root)[0].status, "canceled"); +}); + +test("comment sync degrades on an unavailable lock directory and handles a missing item", async t => { + const root = tracker(t); + fs.rmSync(path.join(root, "locks"), { recursive: true, force: true }); + fs.writeFileSync(path.join(root, "locks"), "Fixture blocks the lock directory"); + const { stderr, result } = await captureStderr(() => syncGithubCommentsToAnnotations("fixture-missing", [{ id: 1, user: null, body: "A comment", created_at: "2026-01-01T00:00:00Z" }], root, 1)); + assert.deepEqual(result, { added: 0, skipped: 0, failed: 1 }); + assert.match(stderr.join("\n"), /lock unavailable/); + assert.match(stderr.join("\n"), /could not read existing comments/); +}); + +test("comment lock contention handles malformed payloads without breaking a fresh lock", async t => { + const root = tracker(t); + const file = importCommentSyncLockPath(root, "fixture-item"); + fs.mkdirSync(path.dirname(file), { recursive: true }); + for (const content of ["null", "[]", "7"]) { + fs.writeFileSync(file, content); + const result = await acquireImportLock(root, "fixture-item", { waitMs: 0 }); + assert.equal(result.status, "contended"); + assert.equal(fs.readFileSync(file, "utf8"), content); + } +}); + +test("atomic SDK resolution accepts an available export and caches the real SDK helper", async () => { + const commit = await resolveCommitItemMutations(); + assert.equal(await resolveCommitItemMutations(), commit); + assert.equal(await resolveCommitItemMutations(async () => ({ commitItemMutations: commit })), commit); +}); diff --git a/test/import-gate.test.ts b/test/import-gate.test.ts new file mode 100644 index 00000000..1fe35d03 --- /dev/null +++ b/test/import-gate.test.ts @@ -0,0 +1,1051 @@ +// Gated import pipeline: plan completeness + provenance idempotency verified +// BEFORE any mutation, the fail-closed tracker privacy gate AFTER it, and the +// full workflow sequence replayed on a REAL Git repository with a bare remote — +// the adversarial cases prove a malicious issue body, comment, email, or host +// path fails the gated import step so nothing is ever committed or pushed to +// the remote. The positive case proves the whole sequence: plan → gated import +// → strict health → commit/push → a second gated run that is a no-op for item +// identity via the provenance tag. +// +// Malicious fixtures are syntactically valid but FAKE and assembled from parts +// at run time, so no tracked blob holds a complete credential or host path. + +import assert from "node:assert/strict"; +import test from "node:test"; +import { spawn, spawnSync } from "node:child_process"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +import { createExtensionTestHarness, type ExtensionTestHarness } from "@unbrained/pm-cli/sdk/testing"; + +import extension, { + CommandError, + parseImportOptions, + runImport, + verifyImportPlanCompleteness, + verifyImportIdempotency, + verifyImportedProvenance, + type GhComment, + type GhIssue, + type ImportGateReceipt, + type PmItem, + type PreparedGithubImport, +} from "../index.ts"; +import { boundedCli } from "./helpers/bounded-cli.ts"; +import { jsonResponse, withMockGithub, type MockGithubHandler } from "./helpers/mock-github-server.ts"; + +const MANIFEST_CAPABILITIES = ["commands", "importers", "schema", "hooks", "preflight", "search"] as const; +const harnessPromise: Promise = + createExtensionTestHarness(extension, { capabilities: [...MANIFEST_CAPABILITIES] }); + +const PM_BIN = process.platform === "win32" ? "pm.cmd" : "pm"; +const PM_SPAWN_OPTS = { encoding: "utf-8" as const, shell: process.platform === "win32" }; +const SYNC_BRANCH = "automation/pm-github-sync"; + +// --- Fake-but-well-formed malicious fixtures, assembled from parts. +const GH_TOKEN = "ghp_" + "A".repeat(36); +const PERSONAL_EMAIL = "alice.person@example.org"; +const HOME_PATH = "/" + "home" + "/alice/report.txt"; +const HIGH_ENTROPY_SECRET = "Zj9kP2mQ7xW4" + "nB8vC5tR1sD"; + +/** + * Build one GitHub issue REST payload for the mock server. + * + * @param number - The issue number. + * @param title - The issue title. + * @param body - The issue body (may carry a malicious fixture). + * @returns A REST-shaped issue object. + */ +function issue(number: number, title: string, body: string | null): GhIssue { + return { + number, + title, + body, + state: "open", + labels: [], + assignee: null, + milestone: null, + user: { login: "octocat" }, + created_at: "2026-10-01T00:00:00Z", + updated_at: "2026-10-02T00:00:00Z", + html_url: `https://github.com/acme/widgets/issues/${number}`, + comments: body === null ? 0 : 1, + }; +} + +/** + * Build one plan entry for the pure verification functions. + * + * Only the fields the verifiers read (issue number, tags, match) vary; the rest + * are the same inert defaults every prepared entry carries. + * + * @param issueNumber - The issue the entry plans. + * @param tags - The provenance/label tags the entry would write. + * @param match - The existing item it would update, when linked. + * @returns A prepared plan entry. + */ +function planEntry(issueNumber: number, tags: string[], match?: PmItem): PreparedGithubImport { + return { + issueNumber, + title: `Title ${issueNumber}`, + itemType: "Issue", + status: "open", + description: "d", + body: "b", + tags, + comments: [], + syncAnnotations: false, + match, + }; +} + +test("recovered journals cannot substitute for a complete persisted provenance corpus", () => { + const prepared = [planEntry(1, ["gh:acme/widgets#1"])]; + assert.throws(() => verifyImportedProvenance([], prepared, "acme/widgets"), CommandError); + assert.equal(verifyImportedProvenance([{ id: "item-a", tags: ["gh:acme/widgets#1"] }], prepared, "acme/widgets"), 1); + assert.throws(() => verifyImportedProvenance([ + { id: "item-a", tags: ["gh:acme/widgets#1"] }, { id: "item-b", tags: ["gh:acme/widgets#1"] }, + ], prepared, "acme/widgets"), CommandError); +}); + +test("gated comment imports scan persisted values without importing REST user metadata", async () => { + const { root, base } = initSyncRepo(); + try { + const comment = { id: 101, body: "Public comment", created_at: "2026-10-02T00:00:00Z", + user: { login: "octocat", events_url: "https://api.github.com/users/octocat/events{/privacy}" } }; + await withMockGithub(githubHandler([issue(1, "Public issue", "Public body")], new Map([[1, [comment]]])), async () => { + const result = await runGatedImport(root, { atomic: true, gate: true, "with-comments": true, "comments-mode": "both" }); + assert.equal(result.imported, 1); + assert.equal((result.gate as ImportGateReceipt).verdict, "pass"); + const tracker = path.join(root, ".agents/pm"); + const item = fs.readdirSync(path.join(tracker, "issues")).find(file => file.endsWith(".toon")); + assert.ok(item); + const text = fs.readFileSync(path.join(tracker, "issues", item), "utf8"); + assert.ok(text.includes("Public comment")); + assert.ok(!text.includes("events_url")); + }); + } finally { + fs.rmSync(base, { recursive: true, force: true }); + } +}); + +test("a real completed SDK journal with reset item files cannot authorize a push", async () => { + const { root, base, bare, git } = initSyncRepo(); + try { + await withMockGithub(githubHandler([issue(1, "Synthetic issue", "Public body")]), async () => { + await runGatedImport(root, { atomic: true, gate: true }); + assert.equal(git(["add", ".agents/pm"]).status, 0); + assert.equal(git(["reset", "--hard", "HEAD"]).status, 0); + await assert.rejects(runGatedImport(root, { atomic: true, gate: true }), /persisted corpus does not account/); + assert.deepEqual(remoteRefs(bare), ["refs/heads/main"]); + }); + } finally { fs.rmSync(base, { recursive: true, force: true }); } +}); + +/** + * Build the mock GitHub handler serving one issues page plus per-issue comments. + * + * Comments are served for every issue so the `--with-comments` paths can embed + * adversarial comment fixtures exactly as the real REST API would deliver them. + * + * @param issues - The issues the "repository" holds. + * @param commentsByNumber - Comment fixtures keyed by issue number. + * @returns The request handler. + */ +function githubHandler( + issues: readonly GhIssue[], + commentsByNumber: ReadonlyMap = new Map(), +): MockGithubHandler { + return (req, res) => { + const url = new URL(req.url ?? "/", "http://127.0.0.1"); + const commentsMatch = /\/repos\/acme\/widgets\/issues\/(\d+)\/comments$/.exec(url.pathname); + if (commentsMatch) { + jsonResponse(res, 200, commentsByNumber.get(Number(commentsMatch[1])) ?? []); + return; + } + jsonResponse(res, 200, issues); + }; +} + +/** + * Create a REAL Git worktree with a pm tracker and a bare remote. + * + * The worktree is committed and pushed to the bare remote exactly like a fleet + * checkout, so the adversarial cases can assert the remote's ref list never + * gains the sync branch when the gate fails, and the positive case can push a + * real commit to it. + * + * @returns The worktree root, the bare remote path, and a git helper bound to the worktree. + */ +function initSyncRepo(): { root: string; bare: string; base: string; git: (args: readonly string[]) => { stdout: string; status: number | null } } { + const base = fs.mkdtempSync(path.join(os.tmpdir(), "pm-github-import-gate-")); + const bare = path.join(base, "remote.git"); + const root = path.join(base, "work"); + fs.mkdirSync(root, { recursive: true }); + const git = (args: readonly string[]) => + spawnSync("git", ["-C", root, ...args], { encoding: "utf-8" }); + + assert.strictEqual(spawnSync("git", ["init", "--bare", "-b", "main", bare]).status, 0, "git init --bare failed"); + assert.strictEqual(git(["init", "-b", "main"]).status, 0, "git init failed"); + // Inside a Git work tree the tracker lives at the repository-local path, + // exactly like a fleet checkout. + const init = spawnSync(PM_BIN, ["--path", path.join(root, ".agents", "pm"), "init", "test"], PM_SPAWN_OPTS); + assert.strictEqual(init.status, 0, `pm init failed: ${init.error?.message ?? init.stderr}`); + assert.strictEqual(git(["config", "user.name", "Sync Fixture"]).status, 0); + assert.strictEqual(git(["config", "user.email", "fixture@example.invalid"]).status, 0); + assert.strictEqual(git(["add", ".agents/pm"]).status, 0); + // The init also writes the repo-root .gitattributes/.gitignore pair; they + // join the baseline so the gated dry-run can assert a pristine tree. + assert.strictEqual(git(["add", ".gitattributes", ".gitignore"]).status, 0); + assert.strictEqual(git(["commit", "-m", "baseline tracker"]).status, 0, "baseline commit failed"); + assert.strictEqual(git(["remote", "add", "origin", bare]).status, 0); + assert.strictEqual(git(["push", "-u", "origin", "main"]).status, 0, "baseline push failed"); + return { root, bare, base, git }; +} + +/** List the refs the bare remote currently holds. */ +function remoteRefs(bare: string): string[] { + const result = spawnSync("git", ["--git-dir", bare, "for-each-ref", "--format=%(refname)"], { encoding: "utf-8" }); + assert.strictEqual(result.status, 0, "for-each-ref failed"); + return result.stdout.trim().split("\n").filter(Boolean); +} + +/** Run the registered gated importer through pm's real dispatch engine. */ +async function runGatedImport( + pmRoot: string, + options: Record, +): Promise> { + const ext = await harnessPromise; + const result = await ext.runImporter({ + importer: "github", + args: ["acme/widgets"], + options, + pmRoot, + }); + assert.strictEqual(result.handled, true); + return result.result as Record; +} + +// --------------------------------------------------------------------------- +// Plan verification — completeness and provenance idempotency, fail-closed +// --------------------------------------------------------------------------- + +test("parseImportOptions reads the gate flag without changing defaults", () => { + assert.strictEqual(parseImportOptions({}).gate, false); + assert.strictEqual(parseImportOptions({ gate: true }).gate, true); + assert.strictEqual(parseImportOptions({ gate: "1" }).gate, true); +}); + +test("gated previews redact issue titles and labels in both import modes", async () => { + const original = console.error; + const messages: string[] = []; + console.error = (...values: unknown[]) => { messages.push(values.join(" ")); }; + try { + for (const atomic of [false, true]) { + const malicious = { ...issue(1, GH_TOKEN, "Public body"), labels: [{ name: GH_TOKEN }] }; + const result = await runImport("acme/widgets", "unused-preview", parseImportOptions({ gate: true, dryRun: true, atomic }), { + resolveToken: () => undefined, fetchIssues: async () => [malicious], readItems: () => [], + }); + assert.ok("wouldImport" in result); + assert.equal(result.wouldImport, 1); + } + assert.ok(messages.some(message => message.includes("(gated title)"))); + assert.ok(!messages.join("\n").includes(GH_TOKEN)); + } finally { + console.error = original; + } +}); + +test("verifyImportPlanCompleteness passes a reconciling plan and fails every drift", () => { + const issues = [issue(1, "One", "b1"), issue(2, "Two", "b2"), issue(3, "Blank", " ")]; + + // Reconciling plan: two prepared entries plus one explicit skip. + const prepared = [planEntry(1, ["gh:acme/widgets#1"]), planEntry(2, ["gh:acme/widgets#2"])]; + const receipts = verifyImportPlanCompleteness(issues, prepared, [3]); + assert.deepStrictEqual(receipts, { fetched: 3, planned: 2, skipped: 1 }); + + // An unaccounted issue (fetch delivered it, the plan lost it). + assert.throws( + () => verifyImportPlanCompleteness(issues, prepared, []), + (err: unknown) => { + assert.ok(err instanceof CommandError); + assert.match(err.message, /import plan is incomplete/); + assert.match(err.message, /unaccounted \[3\]/); + return true; + }, + ); + + // An entry planned outside the fetched set. + assert.throws( + () => + verifyImportPlanCompleteness( + [issue(1, "One", "b1")], + [planEntry(7, ["gh:acme/widgets#7"])], + [], + ), + (err: unknown) => { + assert.ok(err instanceof CommandError); + assert.match(err.message, /out-of-plan \[7\]/); + return true; + }, + ); + + // The same issue planned twice. + assert.throws( + () => + verifyImportPlanCompleteness( + [issue(1, "One", "b1")], + [planEntry(1, ["gh:acme/widgets#1"]), planEntry(1, ["gh:acme/widgets#1"])], + [], + ), + (err: unknown) => { + assert.ok(err instanceof CommandError); + assert.match(err.message, /duplicates 1/); + return true; + }, + ); +}); + +test("verifyImportIdempotency fails duplicate provenance, untagged creates, and conflicting targets", () => { + const existing = [ + { id: "item-a", tags: ["gh:acme/widgets#1"] }, + { id: "item-b", tags: ["gh:acme/widgets#1"] }, // duplicate provenance + ]; + assert.throws( + () => + verifyImportIdempotency(existing, [planEntry(2, ["gh:acme/widgets#2"])], "acme/widgets"), + (err: unknown) => { + assert.ok(err instanceof CommandError); + assert.match(err.message, /1 duplicate provenance tag\(s\)/); + return true; + }, + ); + + // A new entry that would be born without its provenance tag. + assert.throws( + () => + verifyImportIdempotency([], [planEntry(2, [])], "acme/widgets"), + (err: unknown) => { + assert.ok(err instanceof CommandError); + assert.match(err.message, /without a provenance tag/); + return true; + }, + ); + + // Two plan entries resolving to the same existing item. + assert.throws( + () => + verifyImportIdempotency( + [{ id: "item-a", tags: ["gh:acme/widgets#1"] }], + [ + planEntry(1, ["gh:acme/widgets#1"], { id: "item-a", tags: ["gh:acme/widgets#1"] }), + planEntry(2, ["gh:acme/widgets#2"], { id: "item-a", tags: ["gh:acme/widgets#1"] }), + ], + "acme/widgets", + ), + (err: unknown) => { + assert.ok(err instanceof CommandError); + assert.match(err.message, /1 conflicting target/); + return true; + }, + ); + + // A clean plan is proven a no-op: every entry matched by provenance or born tagged. + const receipts = verifyImportIdempotency( + [{ id: "item-a", tags: ["gh:acme/widgets#1"] }], + [ + planEntry(1, ["gh:acme/widgets#1"], { id: "item-a", tags: ["gh:acme/widgets#1"] }), + planEntry(2, ["gh:acme/widgets#2"]), + ], + "acme/widgets", + ); + assert.deepStrictEqual(receipts, { 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 () => { + const issues = [issue(1, "One", "b1"), issue(2, "Two", "b2")]; + const messages: string[] = []; + const originalError = console.error; + console.error = (...values: unknown[]) => messages.push(values.join(" ")); + try { + await assert.rejects( + runImport("acme/widgets", "/unused-gated-workspace", parseImportOptions({ gate: true, dryRun: true }), { + resolveToken: () => undefined, + fetchIssues: async () => issues, + readItems: () => [ + { id: "dup-a", tags: ["gh:acme/widgets#1"] }, + { id: "dup-b", tags: ["gh:acme/widgets#1"] }, + ], + }), + (err: unknown) => { + assert.ok(err instanceof CommandError); + assert.match(err.message, /import plan is not idempotent/); + return true; + }, + ); + assert.ok( + !messages.some((message) => /pm github gate: plan verified/.test(message)), + "the idempotency refusal happens before the plan is reported as verified", + ); + } finally { + console.error = originalError; + } +}); + +// --------------------------------------------------------------------------- +// Adversarial full-sequence — malicious fixtures can never reach the remote +// --------------------------------------------------------------------------- + +test("adversarial: token in a body, token in a comment, email, and host path prevent any push", async () => { + const { root, bare, base, git } = initSyncRepo(); + try { + const malicious = [ + issue(1, "Leaked token in body", `CI config: use ${GH_TOKEN} for deploys`), + issue(2, "Leaked token in comment", "clean body"), + issue(3, "Personal email in body", `contact ${PERSONAL_EMAIL} for the fix`), + issue(4, "Host path in body", `crash log at ${HOME_PATH}`), + ]; + const comments = new Map([ + [2, [{ id: 101, user: { login: "mallory" }, created_at: "2026-10-02T00:00:00Z", body: `psst: ${GH_TOKEN}` }]], + ]); + + await withMockGithub(githubHandler(malicious, comments), async () => { + const ext = await harnessPromise; + + // Workflow step: dry-run plan (no gate failures yet — nothing is written). + const plan = await ext.runImporter({ + importer: "github", + args: ["acme/widgets"], + options: { atomic: true, dryRun: true, "with-comments": true }, + pmRoot: root, + }); + const planResult = plan.result as Record; + assert.strictEqual(planResult.dryRun, true); + assert.strictEqual(planResult.wouldImport, 4); + + // Workflow step: the gated import. It verifies the plan, writes, scans the + // proposed tracker change, and MUST exit non-zero on the findings. + await assert.rejects( + ext.runImporter({ + importer: "github", + args: ["acme/widgets"], + options: { atomic: true, gate: true, "with-comments": true }, + pmRoot: root, + }), + (err: unknown) => { + assert.strictEqual((err as { exitCode?: number }).exitCode, 1); + const message = (err as Error).message; + assert.match(message, /FAIL/); + assert.match(message, /github-token-classic/); + assert.match(message, /email-address/); + assert.match(message, /absolute-host-path/); + assert.ok(!message.includes(GH_TOKEN), "the failure must never echo the token"); + assert.ok(!message.includes(PERSONAL_EMAIL), "the failure must never echo the email"); + return true; + }, + ); + + // The workflow stops here: no commit, no push, and the bare remote still + // holds exactly the baseline ref it started with. + assert.deepStrictEqual(remoteRefs(bare), ["refs/heads/main"]); + + // In-memory preflight rejects the import before sensitive values reach + // disk. The post-write gate remains a separate protection for formatting. + const clean = await ext.runCommand({ command: "github gate", global: { json: true }, pmRoot: root }); + assert.equal((clean.result as { verdict: string }).verdict, "pass"); + + }); + assert.strictEqual(git(["status", "--porcelain", "--", ".agents/pm"]).stdout.trim(), ""); + } finally { + fs.rmSync(base, { recursive: true, force: true }); + } +}); + +test("adversarial: a high-entropy secret assignment in a body fails the gate", async () => { + const { root, bare, base } = initSyncRepo(); + try { + await withMockGithub(githubHandler([issue(9, "Hardcoded secret", `deploy_token: "${HIGH_ENTROPY_SECRET}"`)]), async () => { + const ext = await harnessPromise; + await assert.rejects( + ext.runImporter({ + importer: "github", + args: ["acme/widgets"], + options: { atomic: true, gate: true }, + pmRoot: root, + }), + (err: unknown) => { + assert.match((err as Error).message, /high-entropy-assignment/); + return true; + }, + ); + assert.deepStrictEqual(remoteRefs(bare), ["refs/heads/main"]); + }); + } finally { + fs.rmSync(base, { recursive: true, force: true }); + } +}); + +// --------------------------------------------------------------------------- +// Positive full-sequence — clean import passes the gate, pushes to the remote, +// and a second gated run is a provenance-keyed no-op +// --------------------------------------------------------------------------- + +test("clean flow: gated import passes, health stays strict, the push reaches the remote, and a second run is a no-op", async () => { + const { root, bare, base, git } = initSyncRepo(); + try { + const clean = [issue(1, "Feature one", "needs a flag"), issue(2, "Bug two", "crashes on start")]; + await withMockGithub(githubHandler(clean), async () => { + const ext = await harnessPromise; + + // Gated import: plan checks pass, the write lands, the gate scans and passes. + const first = await runGatedImport(root, { atomic: true, gate: true }); + assert.strictEqual(first.imported, 2); + assert.strictEqual(first.updated, 0); + const receipt = first.gate as ImportGateReceipt; + assert.strictEqual(receipt.verdict, "pass"); + assert.strictEqual(receipt.findings, 0); + assert.ok(receipt.scanned_files >= 2, "the new items and their history were scanned"); + + // Workflow step: strict health over the freshly imported tracker. + const health = spawnSync(PM_BIN, ["--path", root, "health", "--strict-exit"], PM_SPAWN_OPTS); + assert.strictEqual(health.status, 0, `strict health failed: ${health.stdout} ${health.stderr}`); + + // Workflow step: commit the gated change and push the sync branch. + assert.strictEqual(git(["add", ".agents/pm"]).status, 0); + const staged = git(["diff", "--cached", "--name-only", "--", ".agents/pm"]).stdout.trim().split("\n").filter(Boolean); + const toonFiles = staged.filter((changed) => changed.endsWith(".toon")); + assert.strictEqual(toonFiles.length, 2, "one staged .toon per imported issue"); + assert.strictEqual(git(["commit", "-m", "chore(pm): sync GitHub issues (gated)"]).status, 0); + assert.strictEqual(git(["push", "--force", "origin", `main:refs/heads/${SYNC_BRANCH}`]).status, 0); + assert.ok( + remoteRefs(bare).includes(`refs/heads/${SYNC_BRANCH}`), + "the clean gated change reaches the remote", + ); + + // Second gated run: everything matches by provenance — zero creates, all + // updates, no new items, gate passes again. That is the provenance-keyed + // no-op guarantee the plan phase verifies on every gated run. + const second = await runGatedImport(root, { atomic: true, gate: true }); + assert.strictEqual(second.imported, 0); + assert.strictEqual(second.updated, 2); + assert.strictEqual((second.gate as ImportGateReceipt).verdict, "pass"); + + const list = spawnSync( + PM_BIN, + ["--pm-path", root, "--output-include", "full", "--output-limit", "unbounded", "--output-budget", "unbounded", "list", "--all", "--json"], + PM_SPAWN_OPTS, + ); + assert.strictEqual(list.status, 0, "pm list --all failed after the second run"); + const parsed = JSON.parse(list.stdout) as { items?: Array<{ id: string; tags?: string[] }> }; + assert.strictEqual(parsed.items?.length, 2, "no item was duplicated by the re-import"); + const provenance = parsed.items!.flatMap((item) => item.tags ?? []).filter((tag) => tag.startsWith("gh:acme/widgets#")); + assert.deepStrictEqual([...provenance].sort(), ["gh:acme/widgets#1", "gh:acme/widgets#2"]); + }); + } finally { + fs.rmSync(base, { recursive: true, force: true }); + } +}); + +test("gated dry-run embeds the verified plan receipts and writes nothing", async () => { + const { root, base, git } = initSyncRepo(); + try { + await withMockGithub(githubHandler([issue(1, "Feature one", "needs a flag")]), async () => { + const preview = await runGatedImport(root, { atomic: true, gate: true, dryRun: true }); + assert.strictEqual(preview.dryRun, true); + 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.strictEqual(gate.scan, "post-write"); + // Nothing was written: the working tree still matches the baseline commit. + assert.strictEqual(git(["status", "--porcelain"]).stdout.trim(), ""); + }); + } finally { + fs.rmSync(base, { recursive: true, force: true }); + } +}); + +// --------------------------------------------------------------------------- +// The reusable workflow contract — the file a fleet repo actually calls +// --------------------------------------------------------------------------- + +const repoRoot = fileURLToPath(new URL("..", import.meta.url)); +const workflowPath = path.join(repoRoot, ".github", "workflows", "pm-github-sync.yml"); +const workflow = fs.readFileSync(workflowPath, "utf-8"); +const callerExample = fs.readFileSync(path.join(repoRoot, "docs", "sync-workflow-caller.yml"), "utf-8"); + +/** Offset of one named step within the workflow source, for ordering assertions. */ +function stepOffset(name: string): number { + const match = new RegExp(`^\\s*- name: ${name.replace(/[.*+?^${}()|[\]\\\\]/g, "\\$&")}$`, "m").exec(workflow); + assert.ok(match, `workflow should contain the step "${name}"`); + return match.index; +} + +test("the reusable sync workflow is a workflow_call with explicit pinned inputs", () => { + assert.match(workflow, /^on:\n workflow_call:/m); + assert.match(workflow, /pm-github-version:\n\s+description:[^\n]+\n\s+type: string\n\s+required: true/); + assert.match(workflow, /repository:\n\s+description:/); + assert.doesNotMatch(workflow, /^on:\n schedule:/m, "the schedule stays with the caller"); + // Job permissions cover issue reads, branch pushes and review PR writes. + const permissions = /^ permissions:\n contents: write\n pull-requests: write\n issues: read\n/m; + assert.match(workflow, permissions); + assert.ok( + (workflow.match(/^ permissions:/gm) ?? []).length === 1, + "no second, broader permission block", + ); +}); + +test("every action the workflow uses is pinned by commit SHA", () => { + const uses = [...workflow.matchAll(/^\s*uses: ([^\s#]+)/gm)].map((match) => match[1]!); + assert.ok(uses.length >= 2, "the workflow uses actions"); + for (const ref of uses) { + assert.match( + ref, + /@([0-9a-f]{40})\s*$/, + `action ${ref} must be pinned by SHA`, + ); + } +}); + +test("the gated import runs after validate/plan and before health, commit, and push", () => { + const validate = stepOffset("Validate GitHub access"); + const plan = stepOffset("Preview GitHub to pm plan"); + const gated = stepOffset("Gated GitHub to pm import"); + const health = stepOffset("Verify strict pm health"); + const commit = stepOffset("Commit gated sync changes and open review PR"); + const install = stepOffset("Install pinned pm-github extension"); + assert.ok(install < validate, "the pinned extension is installed before it is used"); + assert.ok(validate < plan && plan < gated, "validate and dry-run plan precede the gated import"); + assert.ok(gated < health && health < commit, "the gate precedes health, commit, and push"); + assert.match(workflow, /import "\$\{REPOSITORY\}" --state all --atomic --gate/); + assert.match(workflow, /pm health --strict-exit/); + // No staged catch-all that could commit unscanned content. + assert.doesNotMatch(workflow, /git add -A/); + assert.match(workflow, /git --literal-pathspecs add --pathspec-from-file=/); +}); + +test("the PR body links every changed item as a permanent main-tree pm link", () => { + assert.match( + workflow, + /https:\/\/github\.com\/%s\/blob\/main\/%s/, + "the link template targets the main-tree item path", + ); + assert.match(workflow, /\.agents\/pm\/\*\/\*\.toon/); + assert.match(workflow, /gh pr create/); + assert.match(workflow, /gh pr edit/); + // The push is explicit and force-resets the automation branch from main. + assert.match(workflow, /git push --force-with-lease=/); +}); + +test("the caller example pins the reusable workflow and the extension version", () => { + assert.match(callerExample, /^on:\n schedule:\n - cron:/m, "the caller owns the schedule"); + assert.match( + callerExample, + /uses: unbraind\/pm-github\/\.github\/workflows\/pm-github-sync\.yml@RELEASE_COMMIT_SHA/, + ); + assert.match(callerExample, /pm-github-version: "RELEASE_VERSION"/); + assert.match(callerExample, /^permissions:\n contents: write\n pull-requests: write\n/m); + const lines = callerExample.split("\n").filter((line) => line.trim() !== "" && !line.startsWith("#")); + assert.ok(lines.length <= 40, "the caller stays a small, reviewable file"); +}); +/** Extract executable shell from the workflow so the real push ordering is tested. */ +function workflowShell(names: readonly string[]): string { + return names.map(name => { + const start = stepOffset(name); + const next = workflow.indexOf("\n - name:", start + 1); + const step = workflow.slice(start, next < 0 ? undefined : next); + const block = / run: \|\n([\s\S]*)/.exec(step); + if (block) return block[1]!.split("\n").filter(line => line.startsWith(" ")).map(line => line.slice(10)).join("\n"); + const single = / run: (.+)/.exec(step); + assert.ok(single, `${name} needs executable shell`); + return single[1]!; + }).join("\n"); +} + +/** Run a real workflow shell while allowing the local mock HTTP server to respond. */ +function executeWorkflow(shell: string, root: string, env: NodeJS.ProcessEnv): Promise<{ code: number | null; stdout: string; stderr: string }> { + return new Promise((resolve, reject) => { + const child = spawn("bash", ["-c", "set -euo pipefail\n" + shell], { cwd: root, env }); + let stdout = ""; + let stderr = ""; + // Decode the streams, not each chunk, so a multibyte character split + // across two chunks is not turned into replacement characters. + child.stdout.setEncoding("utf8"); + child.stderr.setEncoding("utf8"); + child.stdout.on("data", (chunk: string) => { stdout += chunk; }); + child.stderr.on("data", (chunk: string) => { stderr += chunk; }); + child.on("error", reject); + child.on("close", code => resolve({ code, stdout, stderr })); + }); +} + +/** Pack from the package tracker, even when npm runs prepare despite --ignore-scripts. */ +function packWorkflowCandidate(packageRoot: string, destination: string, env: NodeJS.ProcessEnv): ReturnType { + // npm 10's directory packer still invokes prepare with --ignore-scripts. + // Its merge installer must use this checkout's tracker, not the caller fixture. + // Keep any lifecycle stdout out of npm's JSON receipt on those versions. + return executeWorkflow(`npm pack --ignore-scripts --foreground-scripts=false --json --pack-destination '${destination.replace(/'/g, "'\\''")}'`, packageRoot, + { ...env, PM_PATH: path.join(packageRoot, ".agents", "pm") }); +} + +/** Install the candidate and create a local gh PR receipt boundary, never a network writer. */ +async function prepareWorkflowFixture(root: string, base: string): Promise { + const bin = path.join(root, "node_modules", ".bin"); + fs.mkdirSync(bin, { recursive: true }); + fs.symlinkSync(path.resolve(import.meta.dirname, "../node_modules/.bin/pm"), path.join(bin, "pm")); + const tools = path.join(base, "tools"); + fs.mkdirSync(tools, { recursive: true }); + fs.writeFileSync(path.join(tools, "gh"), `#!/usr/bin/env bash +set -euo pipefail +if [[ "$1" == "auth" && "$2" == "setup-git" ]]; then exit 0; fi +if [[ "$1" == "--version" ]]; then printf 'gh fixture\\n'; exit 0; fi +case "$1 $2" in + "pr list") + entries="" + if [[ -n "\${OWN_OPEN_PR:-}" ]]; then + entries+="{\\"number\\":\${OWN_OPEN_PR},\\"headRepository\\":{\\"name\\":\\"\${REPOSITORY#*/}\\"},\\"headRepositoryOwner\\":{\\"login\\":\\"\${REPOSITORY%%/*}\\"},\\"isCrossRepository\\":false}," + fi + # A fork can open a PR from a branch with the same name as the sync branch. + if [[ -n "\${FORK_OPEN_PR:-}" ]]; then + entries+="{\\"number\\":\${FORK_OPEN_PR},\\"headRepository\\":{\\"name\\":\\"widgets-fork\\"},\\"headRepositoryOwner\\":{\\"login\\":\\"fixture-fork\\"},\\"isCrossRepository\\":true}," + fi + printf '[%s]' "\${entries%,}" + ;; + "pr create"|"pr edit") + printf '%s\\n' "$2" >> "\${REVIEW_ACTIONS}" + while [[ $# -gt 0 ]]; do + if [[ "$1" == "--body-file" ]]; then cp "$2" "\${REVIEW_RECEIPT}"; break; fi + shift + done + printf 'https://example.invalid/review/1\\n' + ;; + "pr close") + printf 'close\\n' >> "\${REVIEW_ACTIONS}" + while [[ $# -gt 0 ]]; do + if [[ "$1" == "--comment" ]]; then printf '%s\\n' "$2" >> "\${REVIEW_RECEIPT}.close-comment"; break; fi + shift + done + ;; + *) exit 1 ;; +esac +`, { mode: 0o755 }); + const env = { ...process.env, PATH: tools + path.delimiter + bin + path.delimiter + process.env.PATH, + GH_TOKEN: "synthetic-test-token", GITHUB_TOKEN: "synthetic-test-token", PM_AUTHOR: "codex-sol", + PM_PATH: path.join(root, ".agents", "pm"), REPOSITORY: "acme/widgets", CALLING_REPOSITORY: "acme/widgets", + PM_GITHUB_VERSION: "2026.10.4", SYNC_BRANCH, SYNC_LEASE: "", RUNNER_TEMP: base, + GITHUB_ENV: path.join(base, "job-env"), REVIEW_RECEIPT: path.join(base, "review.md"), REVIEW_ACTIONS: path.join(base, "review-actions") }; + const packageRoot = path.resolve(import.meta.dirname, ".."); + const packed = await packWorkflowCandidate(packageRoot, base, env); + assert.equal(packed.code, 0, packed.stderr); + const [{ filename }] = JSON.parse(packed.stdout) as [{ filename: string }]; + const archive = path.join(base, filename).replace(/'/g, "'\\''"); + const installed = await executeWorkflow(`./node_modules/.bin/pm package install '${archive}' --project`, root, env); + assert.equal(installed.code, 0, installed.stderr); + const restored = await executeWorkflow(workflowShell(["Configure PM merge drivers", "Verify gate is available"]), root, env); + assert.equal(restored.code, 0, restored.stderr); + return env; +} + +const WORKFLOW_IMPORT_SEQUENCE = ["Validate GitHub access", "Preview GitHub to pm plan", "Gated GitHub to pm import", + "Verify repeat import is a no-op", "Verify strict pm health", "Commit gated sync changes and open review PR"] as const; + +test("workflow packing isolates prepare from the caller tracker in clones and linked worktrees", async () => { + const { root, base } = initSyncRepo(); + try { + const clone = path.join(base, "package-clone"); + const worktree = path.join(base, "package-worktree"); + assert.equal(spawnSync("git", ["clone", "--quiet", "--no-hardlinks", repoRoot, clone]).status, 0); + assert.equal(spawnSync("git", ["-C", clone, "worktree", "add", "--quiet", "--detach", worktree]).status, 0); + assert.ok(fs.statSync(path.join(clone, ".git")).isDirectory()); + assert.ok(fs.statSync(path.join(worktree, ".git")).isFile()); + const tools = path.join(base, "pack-tools"); + fs.mkdirSync(tools); + const npm = await executeWorkflow("command -v npm", repoRoot, process.env); + assert.equal(npm.code, 0, npm.stderr); + // Exercise npm 10's lifecycle behavior on every npm version: invoke the real + // package hook before packing. No stub replaces pm or its merge installer. + fs.writeFileSync(path.join(tools, "npm"), `#!/usr/bin/env bash +set -euo pipefail +printf '%s\\n' "$PM_PATH" >> "$PACK_PREPARE_RECEIPT" +PATH="$PWD/node_modules/.bin:$PATH" node scripts/prepare-merge-driver.ts >&2 +exec '${npm.stdout.trim().replace(/'/g, "'\\''")}' "$@" +`, { mode: 0o755 }); + const receipt = path.join(base, "prepare-receipt"); + const env = { ...process.env, PM_PATH: path.join(root, ".agents", "pm"), PACK_PREPARE_RECEIPT: receipt, + npm_config_foreground_scripts: "true", + PATH: tools + path.delimiter + process.env.PATH }; + const before = fs.readFileSync(path.join(root, ".agents", "pm", "settings.json"), "utf8"); + for (const packageRoot of [clone, worktree]) { + for (const directory of ["node_modules", "dist"]) { + fs.symlinkSync(path.join(repoRoot, directory), path.join(packageRoot, directory), "dir"); + } + const packed = await packWorkflowCandidate(packageRoot, base, env); + assert.equal(packed.code, 0, packed.stderr); + const [{ filename }] = JSON.parse(packed.stdout) as [{ filename: string }]; + assert.ok(fs.statSync(path.join(base, filename)).size > 0); + assert.equal(fs.readFileSync(path.join(root, ".agents", "pm", "settings.json"), "utf8"), before); + } + assert.deepEqual(fs.readFileSync(receipt, "utf8").trim().split("\n"), + [clone, worktree].map(packageRoot => path.join(packageRoot, ".agents", "pm"))); + assert.equal(env.PM_PATH, path.join(root, ".agents", "pm"), "caller commands keep their own tracker"); + } finally { fs.rmSync(base, { recursive: true, force: true }); } +}); + +test("the executable reusable workflow never publishes each adversarial fixture", async () => { + const cases = [ + { body: GH_TOKEN, comment: "", rule: "github-token-classic" }, + { body: "clean", comment: GH_TOKEN, rule: "github-token-classic" }, + { body: PERSONAL_EMAIL, comment: "", rule: "email-address" }, + { body: HOME_PATH, comment: "", rule: "absolute-host-path" }, + { body: `accessToken = "${HIGH_ENTROPY_SECRET}"`, comment: "", rule: "high-entropy-assignment" }, + { body: `api_key = "${HIGH_ENTROPY_SECRET}2026-10-05"`, comment: "", rule: "high-entropy-assignment" }, + { body: "xapp-" + "1-" + "A".repeat(36), comment: "", rule: "slack-token" }, + { body: "phone: " + "415" + "555" + "0123", comment: "", rule: "phone-number" }, + { body: "C:" + "/" + "Users/fixture/report.txt", comment: "", rule: "windows-host-path" }, + ]; + for (const fixture of cases) { + const { root, base, bare, git } = initSyncRepo(); + try { + const env = await prepareWorkflowFixture(root, base); + assert.equal(git(["switch", "-c", SYNC_BRANCH]).status, 0); + const comments = new Map([[1, [{ id: 100, body: fixture.comment, + user: { login: "fixture" }, created_at: "2026-10-01T00:00:00Z" }]]]); + const issues = [issue(1, "Synthetic issue", fixture.body)]; + const handle = githubHandler(issues, comments); + await withMockGithub((req, res, url, server) => { + if (req.url === "/repos/acme/widgets") jsonResponse(res, 200, { private: false }); + else handle(req, res, url, server); + }, async () => { + const result = await executeWorkflow(workflowShell(WORKFLOW_IMPORT_SEQUENCE), root, { ...env, + PM_GITHUB_API_BASE: process.env.PM_GITHUB_API_BASE }); + assert.notEqual(result.code, 0, "the actual shell must stop at the gate"); + assert.ok(result.stderr.includes(fixture.rule), "the expected privacy rule rejects this fixture"); + for (const sensitive of [GH_TOKEN, HIGH_ENTROPY_SECRET, PERSONAL_EMAIL, HOME_PATH]) { + assert.ok(!result.stdout.includes(sensitive) && !result.stderr.includes(sensitive)); + } + assert.deepEqual(remoteRefs(bare), ["refs/heads/main"]); + assert.deepEqual(fs.readdirSync(path.join(root, ".agents/pm/issues")).filter(file => file.endsWith(".toon")), [], + "preflight never writes malicious issue data to an item file"); + assert.ok(!fs.existsSync(env.REVIEW_RECEIPT!), "no review is opened after a gate failure"); + }); + } finally { fs.rmSync(base, { recursive: true, force: true }); } + } +}); + +test("the executable reusable workflow pushes only a clean import and creates or updates linked reviews", async () => { + const { root, base, bare, git } = initSyncRepo(); + try { + const env = await prepareWorkflowFixture(root, base); + assert.equal(git(["switch", "-c", SYNC_BRANCH]).status, 0); + const handle = githubHandler([issue(1, "Synthetic issue", "Reviewed public body.")]); + await withMockGithub((req, res, url, server) => { + if (req.url === "/repos/acme/widgets") jsonResponse(res, 200, { private: false }); + else handle(req, res, url, server); + }, async () => { + const result = await executeWorkflow(workflowShell(WORKFLOW_IMPORT_SEQUENCE), root, { ...env, + PM_GITHUB_API_BASE: process.env.PM_GITHUB_API_BASE }); + assert.equal(result.code, 0, result.stderr + "\n" + result.stdout); + assert.deepEqual(remoteRefs(bare), ["refs/heads/" + SYNC_BRANCH, "refs/heads/main"].sort()); + const review = fs.readFileSync(env.REVIEW_RECEIPT!, "utf8"); + assert.match(review, /https:\/\/github\.com\/acme\/widgets\/blob\/main\/\.agents\/pm\/issues\/[^)]+\.toon/); + // While the PR is open the main link may not resolve yet, so each item + // also links its proposed version on the sync branch. + assert.ok(review.includes(`([proposed](https://github.com/acme/widgets/blob/${SYNC_BRANCH}/.agents/pm/issues/`), review); + const lease = git(["rev-parse", "HEAD"]).stdout.trim(); + // Each Actions job starts from a fresh checkout, without old local journals. + const nextRoot = path.join(base, "next-work"); + assert.equal(spawnSync("git", ["clone", "--branch", "main", bare, nextRoot], { encoding: "utf8" }).status, 0); + const nextEnv = await prepareWorkflowFixture(nextRoot, base); + assert.equal(spawnSync("git", ["-C", nextRoot, "switch", "-c", SYNC_BRANCH]).status, 0); + assert.equal(spawnSync("git", ["-C", nextRoot, "config", "user.name", "Fixture"]).status, 0); + assert.equal(spawnSync("git", ["-C", nextRoot, "config", "user.email", "fixture@users.noreply.github.com"]).status, 0); + const update = await executeWorkflow(workflowShell(WORKFLOW_IMPORT_SEQUENCE), nextRoot, { ...nextEnv, + PM_GITHUB_API_BASE: process.env.PM_GITHUB_API_BASE, SYNC_LEASE: lease, OWN_OPEN_PR: "1" }); + assert.equal(update.code, 0, update.stderr + "\n" + update.stdout); + assert.deepEqual(fs.readFileSync(env.REVIEW_ACTIONS!, "utf8").trim().split("\n"), ["create", "edit"]); + }); + } finally { fs.rmSync(base, { recursive: true, force: true }); } +}); + +test("the workflow never edits a fork PR sharing the sync branch name", async () => { + const { root, base, bare, git } = initSyncRepo(); + try { + const env = await prepareWorkflowFixture(root, base); + assert.equal(git(["switch", "-c", SYNC_BRANCH]).status, 0); + const handle = githubHandler([issue(1, "Synthetic issue", "Reviewed public body.")]); + await withMockGithub((req, res, url, server) => { + if (req.url === "/repos/acme/widgets") jsonResponse(res, 200, { private: false }); + else handle(req, res, url, server); + }, async () => { + // gh pr list --head matches branch names, so the only listed PR is a + // fork PR opened from a branch with the same name as the sync branch. + const result = await executeWorkflow(workflowShell(WORKFLOW_IMPORT_SEQUENCE), root, { ...env, + PM_GITHUB_API_BASE: process.env.PM_GITHUB_API_BASE, FORK_OPEN_PR: "7" }); + assert.equal(result.code, 0, result.stderr + "\n" + result.stdout); + assert.deepEqual(fs.readFileSync(env.REVIEW_ACTIONS!, "utf8").trim().split("\n"), ["create"], + "the fork PR is never edited; the workflow opens its own review PR instead"); + assert.ok(fs.readFileSync(env.REVIEW_RECEIPT!, "utf8").length > 0); + assert.deepEqual(remoteRefs(bare), ["refs/heads/" + SYNC_BRANCH, "refs/heads/main"].sort()); + }); + } finally { fs.rmSync(base, { recursive: true, force: true }); } +}); + + +/** + * Replay the full import sequence on a fixture whose tracker already matches + * GitHub, so the run proposes no change ("no PR is needed"). + * + * @param prListOverrides - Environment shaping which open PRs `gh pr list` lists. + * @returns The exit code, output, recorded review-PR actions and remote refs. + */ +async function replayNoChangeSequence(prListOverrides: NodeJS.ProcessEnv): Promise<{ + code: number | null; stdout: string; stderr: string; + reviewActions: string[] | undefined; refs: string[]; +}> { + const { root, base, bare, git } = initSyncRepo(); + try { + // The caller restricts extensions to an explicit list without pm-github, so + // installing the pinned extension edits settings.json in the checkout. + const settingsPath = path.join(root, ".agents", "pm", "settings.json"); + const settings = JSON.parse(fs.readFileSync(settingsPath, "utf8")) as { extensions: { enabled: string[] } }; + settings.extensions.enabled = ["pm-other"]; + fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n"); + assert.equal(git(["commit", "-qam", "restrict enabled extensions"]).status, 0); + assert.equal(git(["push", "-q", bare, "main"]).status, 0); + const env = await prepareWorkflowFixture(root, base); + assert.notEqual(git(["diff", "--quiet", "--", ".agents/pm/settings.json"]).status, 0, "precondition: install edits settings.json"); + assert.equal(git(["switch", "-c", SYNC_BRANCH]).status, 0); + let result: Awaited> | undefined; + await withMockGithub((req, res, url, server) => { + if (req.url === "/repos/acme/widgets") jsonResponse(res, 200, { private: false }); + else githubHandler([])(req, res, url, server); + }, async () => { + result = await executeWorkflow(workflowShell(WORKFLOW_IMPORT_SEQUENCE), root, { ...env, + PM_GITHUB_API_BASE: process.env.PM_GITHUB_API_BASE, ...prListOverrides }); + }); + const actionsPath = env.REVIEW_ACTIONS!; + return { code: result!.code, stdout: result!.stdout, stderr: result!.stderr, + reviewActions: fs.existsSync(actionsPath) ? fs.readFileSync(actionsPath, "utf8").trim().split("\n") : undefined, + refs: remoteRefs(bare) }; + } finally { fs.rmSync(base, { recursive: true, force: true }); } +} + +test("an install-only settings change never reaches the sync commit", async () => { + const run = await replayNoChangeSequence({}); + assert.equal(run.code, 0, run.stderr + "\n" + run.stdout); + assert.match(run.stdout, /no PR is needed/); + assert.equal(run.reviewActions, undefined, "no review PR was created, edited or closed"); + assert.deepEqual(run.refs, ["refs/heads/main"]); +}); + +test("a no-change run closes an own open sync PR and exits successfully", async () => { + const run = await replayNoChangeSequence({ OWN_OPEN_PR: "1" }); + assert.equal(run.code, 0, run.stderr + "\n" + run.stdout); + assert.match(run.stdout, /no PR is needed/); + assert.deepEqual(run.reviewActions, ["close"], "the stale own review PR is closed"); + assert.deepEqual(run.refs, ["refs/heads/main"], "a no-change run pushes nothing"); +}); + +test("a no-change run with only a fork PR sharing the branch name closes nothing", async () => { + const run = await replayNoChangeSequence({ FORK_OPEN_PR: "7" }); + assert.equal(run.code, 0, run.stderr + "\n" + run.stdout); + assert.match(run.stdout, /no PR is needed/); + assert.equal(run.reviewActions, undefined, "a foreign fork PR is never touched"); + assert.deepEqual(run.refs, ["refs/heads/main"]); +}); + +test("packed installed CLI repeats a completed gated import under native Bun within 45 seconds", { timeout: 180_000 }, async () => { + const { root, base, bare } = initSyncRepo(); + try { + const env = await prepareWorkflowFixture(root, base); + const handle = githubHandler([issue(1, "Synthetic issue", "Reviewed public body.")]); + await withMockGithub(handle, async () => { + const runEnv = { ...env, PM_GITHUB_API_BASE: process.env.PM_GITHUB_API_BASE }; + const first = await executeWorkflow(workflowShell(["Gated GitHub to pm import"]), root, runEnv); + assert.equal(first.code, 0, first.stderr); + const snapshot = () => fs.readdirSync(path.join(root, ".agents/pm/issues")).filter(file => file.endsWith(".toon")) + .map(file => fs.readFileSync(path.join(root, ".agents/pm/issues", file), "utf8")); + const before = snapshot(); + assert.equal(before.length, 1); + const cli = fs.realpathSync(path.join(root, "node_modules/.bin/pm")); + for (let repeat = 0; repeat < 3; repeat++) { + const started = Date.now(); + const result = await boundedCli("bun", ["--bun", cli, "github", "import", "acme/widgets", "--state", "all", "--atomic", "--gate", "--with-comments", "--json"], root, runEnv); + assert.equal(result.timedOut, false); + assert.equal(result.signal, null, `native Bun repeat ${repeat + 1} exceeded its unchanged 45s budget: ${result.stderr}`); + assert.equal(result.code, 0, result.stderr); + assert.ok(Date.now() - started < 45_000); + assert.deepEqual(snapshot(), before); + assert.deepEqual(remoteRefs(bare), ["refs/heads/main"]); + } + }); + } finally { fs.rmSync(base, { recursive: true, force: true }); } +}); + +test("gated comment imports reject malformed second HTTP pages before writing", async () => { + const { root, base, git } = initSyncRepo(); + try { + for (const badPage of ["not JSON", "{}"] ) { + await withMockGithub((req, res, _body, baseUrl) => { + if ((req.url ?? "").includes("page=2")) { res.end(badPage); return; } + if ((req.url ?? "").includes("/comments")) { + jsonResponse(res, 200, [{ id: 1, body: "public comment" }], { Link: `<${baseUrl}/repos/acme/widgets/issues/1/comments?page=2>; rel="next"` }); + return; + } + jsonResponse(res, 200, [{ ...issue(1, "Public issue", "Public body"), comments: 2 }]); + }, async server => { + for (const atomic of [true, false]) { + await assert.rejects(runGatedImport(root, { gate: true, atomic, "with-comments": true }), /comments for issue #1 could not be read/); + assert.equal(git(["status", "--porcelain", "--", ".agents/pm"]).stdout.trim(), ""); + } + assert.equal(server.requests.filter(req => req.url.includes("page=2")).length, 2); + }); + } + } finally { fs.rmSync(base, { recursive: true, force: true }); } +}); + +test("gated pre-write input errors carry the command exit code", async () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "pm-github-preview-error-")); + try { + await assert.rejects(runImport("acme/widgets", root, parseImportOptions({ gate: true, atomic: true }), { + resolveToken: () => undefined, fetchIssues: async () => [issue(1, "Public", "Body")], readItems: () => [], + }), error => error instanceof CommandError && error.exitCode === 1 && /not inside a Git work tree/.test(error.message)); + } finally { fs.rmSync(root, { recursive: true, force: true }); } +}); + +test("workflow input shell accepts numeric same-day pins and refuses floating versions", async () => { + for (const [version, valid] of [["2026.10.6", true], ["2026.10.6-1", true], ["2026.10.6-12", true], ["latest", false], ["^2026.10.6", false], ["2026.10.6-beta", false], ["2026.10.6-1; true", false]] as const) { + const result = await executeWorkflow(workflowShell(["Validate sync inputs"]), repoRoot, { + ...process.env, REPOSITORY: "acme/widgets", CALLING_REPOSITORY: "acme/widgets", SYNC_BRANCH, PM_GITHUB_VERSION: version, + }); + assert.equal(result.code === 0, valid, version); + if (!valid) assert.match(result.stdout, /::error::pm-github-version must be an exact numeric version: N\.N\.N or N\.N\.N-N\./); + } +}); + +test("workflow input shell diagnoses a foreign repository and branch outside automation/", async () => { + for (const [input, value, diagnostic] of [ + ["REPOSITORY", "acme/other", "::error::repository must equal the calling repository (acme/widgets)."], + ["SYNC_BRANCH", "feature/sync", "::error::sync-branch must start with automation/."], + ] as const) { + const result = await executeWorkflow(workflowShell(["Validate sync inputs"]), repoRoot, { + ...process.env, REPOSITORY: "acme/widgets", CALLING_REPOSITORY: "acme/widgets", SYNC_BRANCH, PM_GITHUB_VERSION: "2026.10.6", + [input]: value, + }); + assert.equal(result.code, 1, input); + assert.ok(result.stdout.includes(diagnostic), result.stdout); + } +}); + +test("workflow credentials stay out of install steps and checkout persistence", () => { + const steps = workflow.split(/\n - name: /).slice(1); + const tokenSteps = steps.filter(step => step.includes("GH_TOKEN:")).map(step => step.split("\n")[0]); + assert.deepEqual(tokenSteps, ["Validate GitHub access", "Preview GitHub to pm plan", "Gated GitHub to pm import", "Verify repeat import is a no-op", "Commit gated sync changes and open review PR"]); + assert.doesNotMatch(workflow.slice(0, stepOffset("Validate sync inputs")), /GH_TOKEN:/); + assert.match(steps.find(step => step.startsWith("Checkout main"))!, /persist-credentials: false/); + assert.match(workflowShell(["Install dependencies"]), /npm ci --ignore-scripts/); + assert.doesNotMatch(workflowShell(["Prepare sync branch from main"]), /git fetch|git ls-remote/); + assert.match(workflowShell(["Commit gated sync changes and open review PR"]), /gh auth setup-git\n\s*git push/); +}); diff --git a/test/import-lock.test.ts b/test/import-lock.test.ts index c7ba3c09..84d25df2 100644 --- a/test/import-lock.test.ts +++ b/test/import-lock.test.ts @@ -1,3 +1,4 @@ +import { comments as readComments } from "@unbrained/pm-cli/sdk"; // Tests for the cross-process comment-sync lock (pm-github-503u). // // Unit tests cover the lock helper in isolation: acquire/release roundtrip @@ -12,12 +13,14 @@ import assert from "node:assert/strict"; import test from "node:test"; import { execFile, execFileSync, spawn, spawnSync } from "node:child_process"; -import { +import fs, { + chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, + symlinkSync, utimesSync, writeFileSync, } from "node:fs"; @@ -29,12 +32,18 @@ import { IMPORT_LOCK_TTL_MS_DEFAULT, acquireImportLock, importCommentSyncLockPath, + isLockOwnerAlive, resolvePmDataDir, syncGithubCommentsToAnnotations, } from "../index.ts"; import type { GhComment } from "../index.ts"; import { waitForBarrier } from "./helpers/barrier.ts"; +// Root ignores directory write permissions, so chmod-based lock fixtures +// cannot produce their EACCES failures; skip them visibly in that case. +const runsAsRoot = typeof process.getuid === "function" && process.getuid() === 0; +const nonRootOnly = { skip: runsAsRoot && "permission-based fixture: root ignores directory write permissions" }; + // Minimal factories + workspace helpers -------------------------------------- function ghComment(overrides: Partial = {}): GhComment { @@ -145,6 +154,7 @@ test("resolvePmDataDir prefers a nested .agents/pm and falls back to pmRoot itse try { assert.strictEqual(resolvePmDataDir(root), root, "no .agents/pm → pmRoot is the data dir"); mkdirSync(join(root, ".agents", "pm"), { recursive: true }); + writeFileSync(join(root, ".agents", "pm", "settings.json"), '{"version":1}\n'); assert.strictEqual(resolvePmDataDir(root), join(root, ".agents", "pm")); // A pmRoot that already IS the data dir keeps working. assert.strictEqual(resolvePmDataDir(join(root, ".agents", "pm")), join(root, ".agents", "pm")); @@ -157,6 +167,7 @@ test("importCommentSyncLockPath lands in locks/ with a pm-github prefix and sani const root = mkdtempSync(join(tmpdir(), "pm-github-lockpath-test-")); try { mkdirSync(join(root, ".agents", "pm"), { recursive: true }); + writeFileSync(join(root, ".agents", "pm", "settings.json"), '{"version":1}\n'); const p = importCommentSyncLockPath(root, "pm-ab12"); assert.strictEqual(p, join(root, ".agents", "pm", "locks", "pm-github.comment-sync.pm-ab12.lock")); // Filename-unsafe characters are replaced, so the lock always stays inside @@ -439,9 +450,7 @@ test("two in-process concurrent syncs of the same comments never duplicate", asy syncGithubCommentsToAnnotations(id, comments, root, 1), ]); assert.strictEqual(r1.added + r2.added, comments.length, "each comment added exactly once across both syncs"); - const { comments: stored } = await import("@unbrained/pm-cli/sdk").then((m) => - m.comments(id, {}, { pmRoot: root }), - ); + const { comments: stored } = await readComments(id, {}, { pmRoot: root }); assert.strictEqual(stored.length, comments.length, "no duplicate comments stored"); } finally { rmSync(root, { recursive: true, force: true }); @@ -502,9 +511,7 @@ test( ); assert.strictEqual(r1.skipped + r2.skipped, comments.length, "the loser of the race skips every duplicate"); - const { comments: stored } = await import("@unbrained/pm-cli/sdk").then((m) => - m.comments(id, {}, { pmRoot: root }), - ); + const { comments: stored } = await readComments(id, {}, { pmRoot: root }); assert.strictEqual(stored.length, comments.length, "exactly one stored comment per GitHub comment"); const markerIds = stored.map((c: { text?: string }) => { const m = //.exec(c.text ?? ""); @@ -518,14 +525,223 @@ test( ); // The loser's re-run must be a clean no-op (idempotency preserved). - const r3 = await new Promise<{ added: number; skipped: number }>((resolve, reject) => { + const r3 = await new Promise<{ added: number; skipped: number; failed: number }>((resolve, reject) => { execFile(process.execPath, [CHILD_SCRIPT, id, root], { env }, (err, stdout) => err ? reject(err) : resolve(JSON.parse(stdout.trim())), ); }); - assert.deepEqual(r3, { added: 0, skipped: comments.length }); + assert.deepEqual(r3, { added: 0, skipped: comments.length, failed: 0 }); } finally { rmSync(root, { recursive: true, force: true }); } }, ); + +test("isLockOwnerAlive rejects non-positive pids and treats EPERM as alive", () => { + assert.equal(isLockOwnerAlive(0), false); + assert.equal(isLockOwnerAlive(-3), false); + assert.equal(isLockOwnerAlive(process.pid), true); + const dead = spawnSync(process.execPath, ["-e", ""]); + assert.ok(dead.pid && dead.pid > 0); + assert.equal(isLockOwnerAlive(dead.pid), false); + // Signal 0 against pid 1 as an unprivileged user is EPERM: the process exists. + if (typeof process.getuid === "function" && process.getuid() !== 0) { + assert.equal(isLockOwnerAlive(1), true); + } +}); + +test("a lock that cannot be stat'd contends within the wait budget instead of spinning", { timeout: 5_000 }, async () => { + const root = mkdtempSync(join(tmpdir(), "pm-github-lock-spin-")); + try { + const lockPath = importCommentSyncLockPath(root, "pm-spin"); + mkdirSync(dirname(lockPath), { recursive: true }); + symlinkSync(join(root, "missing-lock-target"), lockPath); + const started = Date.now(); + const acq = await acquireImportLock(root, "pm-spin", { waitMs: 80 }); + assert.equal(acq.status, "contended"); + assert.ok(Date.now() - started < 2000, "stat failure must not busy-spin past the wait budget"); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); + +test("comment-sync lock degrades when the lock directory cannot be created", async () => { + const root = mkdtempSync(join(tmpdir(), "pm-github-lock-degraded-")); + try { + const blocked = importCommentSyncLockPath(root, "pm-file-parent"); + writeFileSync(dirname(blocked), "not a directory\n"); + const messages = await captureStderr(async () => { + const acq = await acquireImportLock(root, "pm-file-parent", { waitMs: 50 }); + assert.equal(acq.status, "degraded"); + }); + assert.ok(messages.some((line) => line.includes("lock unavailable")), messages.join(" | ")); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); + +test("comment-sync lock degrades when the lock directory is not writable", nonRootOnly, async () => { + const writable = mkdtempSync(join(tmpdir(), "pm-github-lock-unwritable-")); + try { + const lockPath = importCommentSyncLockPath(writable, "pm-mode"); + mkdirSync(dirname(lockPath), { recursive: true }); + chmodSync(dirname(lockPath), 0o555); + const acq = await acquireImportLock(writable, "pm-mode", { waitMs: 50 }); + assert.equal(acq.status, "degraded"); + } finally { + chmodSync(dirname(importCommentSyncLockPath(writable, "pm-mode")), 0o755); + rmSync(writable, { recursive: true, force: true }); + } +}); + +/** Replace only an injected syscall boundary; restore it under either runtime. */ +function replaceMethod(target: T, key: K, implementation: T[K]): () => void { + const original = target[key]; + target[key] = implementation; + return () => { target[key] = original; }; +} + +test("a failed lock write still releases the partial file when close and unlink fail", async () => { + const root = mkdtempSync(join(tmpdir(), "pm-github-lock-write-")); + const originalWrite = fs.writeFileSync; + const originalClose = fs.closeSync; + let descriptor: number | undefined; + let closeCalls = 0; + let unlinkCalls = 0; + const closeMock = replaceMethod(fs, "closeSync", () => { + closeCalls++; + throw Object.assign(new Error("close failed"), { code: "EIO" }); + }); + const unlinkMock = replaceMethod(fs, "unlinkSync", () => { + unlinkCalls++; + throw Object.assign(new Error("unlink failed"), { code: "EIO" }); + }); + const writeMock = replaceMethod(fs, "writeFileSync", ((target: fs.PathOrFileDescriptor, data: string) => { + if (typeof target === "number") { + descriptor = target; + throw Object.assign(new Error("write failed"), { code: "EIO" }); + } + return originalWrite(target, data); + }) as typeof fs.writeFileSync); + try { + const acq = await acquireImportLock(root, "pm-write", { waitMs: 50 }); + assert.equal(acq.status, "degraded"); + assert.ok(closeCalls > 0); + assert.ok(unlinkCalls > 0); + } finally { + writeMock(); + closeMock(); + unlinkMock(); + if (descriptor !== undefined) originalClose(descriptor); + rmSync(root, { recursive: true, force: true }); + } +}); + +test("a vanished breaker sidecar is ignored and a vanished lock is treated as already gone", async () => { + const root = makeWorkspace(); + try { + const dead = spawnSync(process.execPath, ["-e", ""]); + assert.ok(dead.pid && dead.pid > 0); + const lockPath = plantLock(root, "pm-vanish", { + id: "pm-github.comment-sync.pm-vanish", + pid: dead.pid, + owner: "pm-github", + token: "stale", + created_at: new Date().toISOString(), + ttl_seconds: 300, + }); + symlinkSync(join(root, "missing-sidecar"), `${lockPath}.break`); + const blocked = await acquireImportLock(root, "pm-vanish", { waitMs: 80 }); + assert.equal(blocked.status, "contended"); + rmSync(`${lockPath}.break`); + + let stats = 0; + const original = fs.statSync; + const statMock = replaceMethod(fs, "statSync", ((target: fs.PathLike, options?: fs.StatSyncOptions) => { + stats++; + if (stats >= 2 && String(target) === lockPath) { + rmSync(lockPath, { force: true }); + throw Object.assign(new Error("gone"), { code: "ENOENT" }); + } + return original(target, options); + }) as typeof fs.statSync); + try { + const acq = await acquireImportLock(root, "pm-vanish", { waitMs: 500 }); + assert.equal(acq.status, "acquired"); + if (acq.status === "acquired") acq.lock.release(); + } finally { + statMock(); + } + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); + +test("malformed lock payloads use creation time or mtime and release tolerates disappearance", async () => { + const root = mkdtempSync(join(tmpdir(), "pm-lock-payload-")); + try { + const lockPath = importCommentSyncLockPath(root, "pm-payload"); + mkdirSync(dirname(lockPath), { recursive: true }); + writeFileSync(lockPath, JSON.stringify({ created_at: new Date().toISOString(), pid: "missing" })); + assert.equal((await acquireImportLock(root, "pm-payload", { waitMs: 0 })).status, "contended"); + writeFileSync(lockPath, "invalid-json"); + utimesSync(lockPath, new Date(0), new Date(0)); + const acq = await acquireImportLock(root, "pm-payload", { waitMs: 100 }); + assert.equal(acq.status, "acquired"); + if (acq.status === "acquired") { + rmSync(lockPath); + acq.lock.release(); + } + } finally { rmSync(root, { recursive: true, force: true }); } +}); + +test("a peer replacing a stale lock before the breaker recheck remains protected", async () => { + const root = mkdtempSync(join(tmpdir(), "pm-lock-peer-")); + const lockPath = importCommentSyncLockPath(root, "pm-peer"); + mkdirSync(dirname(lockPath), { recursive: true }); + writeFileSync(lockPath, "abandoned"); + utimesSync(lockPath, new Date(0), new Date(0)); + let checked = 0; + const original = fs.statSync; + const boundary = replaceMethod(fs, "statSync", ((target: fs.PathLike, options?: fs.StatSyncOptions) => { + if (String(target) === lockPath && ++checked === 2) { + writeFileSync(lockPath, JSON.stringify({ pid: 1, token: "live-peer", created_at: new Date().toISOString() })); + } + return original(target, options); + }) as typeof fs.statSync); + try { + assert.equal((await acquireImportLock(root, "pm-peer", { waitMs: 80 })).status, "contended"); + assert.match(readFileSync(lockPath, "utf8"), /live-peer/); + } finally { boundary(); rmSync(root, { recursive: true, force: true }); } +}); + +test("real permission failures during stale unlink and token release retain the lock", nonRootOnly, async () => { + const root = mkdtempSync(join(tmpdir(), "pm-lock-permissions-")); + const lockPath = importCommentSyncLockPath(root, "pm-permission"); + mkdirSync(dirname(lockPath), { recursive: true }); + writeFileSync(lockPath, "abandoned"); + utimesSync(lockPath, new Date(0), new Date(0)); + let checked = 0; + const original = fs.statSync; + const boundary = replaceMethod(fs, "statSync", ((target: fs.PathLike, options?: fs.StatSyncOptions) => { + if (String(target) === lockPath && ++checked === 2) chmodSync(dirname(lockPath), 0o555); + return original(target, options); + }) as typeof fs.statSync); + try { + assert.equal((await acquireImportLock(root, "pm-permission", { waitMs: 80 })).status, "contended"); + assert.equal(existsSync(lockPath), true); + chmodSync(dirname(lockPath), 0o755); + boundary(); + rmSync(lockPath); + rmSync(`${lockPath}.break`, { force: true }); + const acq = await acquireImportLock(root, "pm-permission", { waitMs: 80 }); + assert.equal(acq.status, "acquired"); + if (acq.status === "acquired") { + chmodSync(dirname(lockPath), 0o555); + acq.lock.release(); + assert.equal(existsSync(lockPath), true); + } + } finally { + boundary(); chmodSync(dirname(lockPath), 0o755); rmSync(root, { recursive: true, force: true }); + } +}); diff --git a/test/link-deps.test.ts b/test/link-deps.test.ts index 5cc24f08..b973ac42 100644 --- a/test/link-deps.test.ts +++ b/test/link-deps.test.ts @@ -53,6 +53,7 @@ function importOpts(overrides: Partial = {}): ImportOptions { dryRun: false, atomic: false, linkDeps: true, + gate: false, ...overrides, }; } @@ -447,3 +448,8 @@ test("runImport without --link-deps leaves items free of dependency edges", asyn fs.rmSync(root, { recursive: true, force: true }); } }); + +test("dependency import options preserve an explicit gate override", () => { + assert.equal(importOpts().gate, false); + assert.equal(importOpts({ gate: true }).gate, true); +}); diff --git a/test/lint-policy.test.ts b/test/lint-policy.test.ts new file mode 100644 index 00000000..1fc1c020 --- /dev/null +++ b/test/lint-policy.test.ts @@ -0,0 +1,17 @@ +/** Ensure the repo configuration enforces the requested source syntax policy. */ +import assert from "node:assert/strict"; +import test from "node:test"; +import { ESLint } from "eslint"; +import config from "../eslint.config.js"; + +test("canonical ESLint configuration rejects any, inline imports, and non-erasable syntax", async () => { + assert.ok(config.length > 0); + const eslint = new ESLint({ overrideConfigFile: true, overrideConfig: config }); + for (const source of ["type Unsafe = any;", "const dependency = await import('./dependency.ts');", + "type Inline = import('./dependency.ts').Result;", "enum Color { Red, Blue }"]) { + const [result] = await eslint.lintText(source, { filePath: "policy-fixture.ts" }); + assert.ok(result!.messages.some(message => message.ruleId === "no-restricted-syntax"), source); + } + const [valid] = await eslint.lintText("export const source: string = 'checked';", { filePath: "policy-fixture.ts" }); + assert.equal(valid!.errorCount, 0); +}); diff --git a/test/prepare-merge-driver.test.ts b/test/prepare-merge-driver.test.ts index 87621fdb..06b5654e 100644 --- a/test/prepare-merge-driver.test.ts +++ b/test/prepare-merge-driver.test.ts @@ -18,6 +18,10 @@ import test, { after } from "node:test"; const root = process.cwd(); const launcher = join(root, "scripts", "prepare-merge-driver.ts"); const hostPath = `${join(root, "node_modules", ".bin")}${delimiter}${process.env.PATH ?? ""}`; +// The package hook explicitly invokes `node`, including during Bun installs. +// Resolve that executable before fixtures deliberately narrow their PATH. +const nodeExecutable = spawnSync("node", ["-p", "process.execPath"], { encoding: "utf8" }).stdout.trim(); +assert.ok(nodeExecutable, "the package prepare hook requires Node"); const scratch = mkdtempSync(join(tmpdir(), "prepare-merge-driver-")); after(() => rmSync(scratch, { recursive: true, force: true })); @@ -69,7 +73,7 @@ function stubPm(name: string, status: number, body = ""): string { /** Run the launcher as npm's `prepare` hook would: as the entry point, from `cwd`, with `path` as PATH. */ function prepare(cwd: string, path: string): SpawnSyncReturns { - return spawnSync(process.execPath, [launcher], { cwd, encoding: "utf8", env: { ...process.env, PATH: path, NODE_PATH: "" } }); + return spawnSync(nodeExecutable, [launcher], { cwd, encoding: "utf8", env: { ...process.env, PATH: path, NODE_PATH: "" } }); } /** The merge drivers registered in a checkout's LOCAL Git config, by name. */ @@ -124,7 +128,7 @@ test("a looping lookup path preserves the original installer error", () => { const directory = checkout("lookup-loop", "absent"); const lookup = join(directory, "lookup"); symlinkSync(lookup, lookup, "junction"); - const result = spawnSync(process.execPath, [launcher], { + const result = spawnSync(nodeExecutable, [launcher], { cwd: directory, encoding: "utf8", env: { ...process.env, PATH: hostPath, NODE_PATH: lookup }, diff --git a/test/privacy-gate.test.ts b/test/privacy-gate.test.ts index 64c8c16b..348b9210 100644 --- a/test/privacy-gate.test.ts +++ b/test/privacy-gate.test.ts @@ -2,7 +2,9 @@ * Behavioral coverage for the privacy gate script. * * The gate is a fail-closed release gate, so the tests exercise real git - * repositories built in temporary directories — never mocks of git itself: + * repositories built in temporary directories. Narrow executable wrappers + * inject unreadable inventory and blob failures while delegating other queries + * to real Git: * * - the clean path (this repository's rewritten history passes), * - every failure path (unapproved author, committer, tagger; each secret @@ -19,16 +21,19 @@ import assert from "node:assert/strict"; import { execFileSync } from "node:child_process"; -import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { chmodSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; -import { join, resolve } from "node:path"; +import { delimiter, join, resolve } from "node:path"; import test from "node:test"; +import { fileURLToPath } from "node:url"; import { extractEmail, gitBlobOid, type PrivacyGateResult, runGate, + runPrivacyGateIfMain, + main, scanBlob, } from "../scripts/privacy-gate.ts"; @@ -238,7 +243,6 @@ test("extractEmail parses git header shapes and rejects malformed lines", () => }); test("privacy gate CLI entry point writes streams and sets exit code", async () => { - const { main } = await import("../scripts/privacy-gate.ts"); const cleanRoot = repoRoot; let stdout = ""; let stderr = ""; @@ -272,7 +276,7 @@ test("privacy gate CLI entry point writes streams and sets exit code", async () } finally { process.stdout.write = originalWrite; process.stderr.write = originalErrWrite; - process.exitCode = originalExit; + process.exitCode = originalExit ?? 0; } }); @@ -370,6 +374,92 @@ test("privacy gate ignores manifest exemptions for blobs outside the fixture dir } }); +test("privacy gate main entry audits the supplied root", () => { + const root = initRepo("main-entry"); + const previous = process.exitCode; + let stderr = ""; + const original = process.stderr.write.bind(process.stderr); + process.stderr.write = (chunk: Uint8Array | string): boolean => { + stderr += String(chunk); + return true; + }; + try { + const moduleUrl = new URL("../scripts/privacy-gate.ts", import.meta.url).href; + runPrivacyGateIfMain(["node", fileURLToPath(moduleUrl)], moduleUrl, root); + assert.equal(process.exitCode, 1); + assert.match(stderr, /cannot read/); + } finally { + process.stderr.write = original; + process.exitCode = previous ?? 0; + rmSync(root, { recursive: true, force: true }); + } +}); + +/** Launch with a startup PATH: Bun retains the original executable lookup path. */ +function privacyProcess(code: string, environment: Record): void { + const env = { ...process.env, ...environment }; + delete env.NODE_TEST_CONTEXT; + execFileSync(process.execPath, ["--input-type=module", "--eval", ` + import assert from 'node:assert/strict'; + import { execFileSync } from 'node:child_process'; + import { writeFileSync } from 'node:fs'; + import { join } from 'node:path'; + import { runGate, listAllObjects, readObject } from ${JSON.stringify(new URL("../scripts/privacy-gate.ts", import.meta.url).href)}; + ${code} + `], { env, encoding: "utf8", timeout: 30_000 }); +} + +test("privacy gate fails closed when git cannot be spawned", () => { + const root = initRepo("no-git"); + writeAllowlist(root, ["intruder@localhost"]); + try { + privacyProcess(` + const root = ${JSON.stringify(root)}; + const result = runGate(root); + assert.equal(result.exitCode, 1); + assert.match(result.stderr, /object enumeration failed|fixture exemption resolution failed|cannot read/); + assert.throws(() => listAllObjects(root)); + `, { PATH: root }); + } finally { rmSync(root, { recursive: true, force: true }); } +}); + +test("privacy gate fails closed when object inventory or blob reads are unusable", { skip: process.platform === "win32" && "uses POSIX sh git wrappers" }, () => { + const root = initRepo("bad-git"); + const bin = mkdtempSync(join(tmpdir(), "pm-github-privacy-git-")); + try { + writeAllowlist(root, ["intruder@localhost"]); + writeFileSync(join(root, "clean.txt"), "clean\n"); + commitAll(root, "clean"); + const realGit = execFileSync("sh", ["-c", "command -v git"], { encoding: "utf8" }).trim(); + writeFileSync(join(bin, "git"), `#!/bin/sh +if [ "$1" = "cat-file" ] && [ "$2" = "--batch-all-objects" ]; then + printf '%s\n' 'not-a-record' + exit 0 +fi +exec "$REAL_GIT" "$@" +`, { mode: 0o755 }); + chmodSync(join(bin, "git"), 0o755); + privacyProcess(` + const root = ${JSON.stringify(root)}; + const bin = ${JSON.stringify(bin)}; + const realGit = ${JSON.stringify(realGit)}; + assert.throws(() => listAllObjects(root), /unreadable record/); + const listed = execFileSync(realGit, ['cat-file', '--batch-all-objects', '--batch-check'], { cwd: root, encoding: 'utf8' }); + const blob = listed.split('\\n').find(line => line.includes(' blob ')); + assert.ok(blob, 'fixture commit created a blob'); + const oid = blob.split(' ')[0]; + writeFileSync(join(bin, 'git'), '#!/bin/sh\\nif [ "$1" = "cat-file" ] && [ "$2" = "blob" ]; then\\n echo forced-read-failure >&2\\n exit 1\\nfi\\nexec "$REAL_GIT" "$@"\\n', { mode: 0o755 }); + assert.throws(() => readObject(root, 'blob', oid), /forced-read-failure/); + const scanned = runGate(root); + assert.equal(scanned.exitCode, 1); + assert.match(scanned.stderr, /object scan failed/); + `, { PATH: bin + delimiter + process.env.PATH, REAL_GIT: realGit }); + } finally { + rmSync(bin, { recursive: true, force: true }); + rmSync(root, { recursive: true, force: true }); + } +}); + test("privacy gate negative control: a fresh violation introduced after a clean pass fails", () => { const root = initRepo("negative-control"); try { @@ -388,3 +478,34 @@ test("privacy gate negative control: a fresh violation introduced after a clean rmSync(root, { recursive: true, force: true }); } }); + +test("historical fixture provenance reports Git query failures", { skip: process.platform === "win32" && "uses POSIX sh git wrappers" }, () => { + const root = initRepo("historical-query"); + try { + writeAllowlist(root, ["intruder@localhost"]); + writeFileSync(join(root, "clean.txt"), "public\n"); + commitAll(root, "baseline"); + const bin = join(root, "bin"); + mkdirSync(bin); + const realGit = execFileSync("sh", ["-c", "command -v git"], { encoding: "utf8" }).trim(); + const commit = "a".repeat(40); + const manifestDir = join(root, "test/fixtures/privacy-gate"); + mkdirSync(manifestDir, { recursive: true }); + // A blob the repository holds must have resolvable provenance. + const present = execFileSync("git", ["-C", root, "hash-object", "clean.txt"], { encoding: "utf8" }).trim(); + writeFileSync(join(manifestDir, "manifest.json"), JSON.stringify({ [present]: { justification: "Synthetic", historical_test: { commit, path: "test/gate.test.ts" } } })); + writeFileSync(join(bin, "git"), `#!/bin/sh\nif [ "$1" = "ls-tree" ] && [ "$2" = "${commit}" ]; then\n echo forced-provenance-failure >&2\n exit 1\nfi\nexec "$REAL_GIT" "$@"\n`, { mode: 0o755 }); + privacyProcess(` + const result = runGate(${JSON.stringify(root)}); + assert.equal(result.exitCode, 1); + assert.match(result.stderr, /git historical fixture provenance failed: forced-provenance-failure/); + `, { PATH: bin + delimiter + process.env.PATH, REAL_GIT: realGit }); + // A blob absent from the object store needs no exemption, so its provenance + // commit is never queried: after a squash merge that commit is unreachable. + writeFileSync(join(manifestDir, "manifest.json"), JSON.stringify({ ["b".repeat(40)]: { justification: "Synthetic", historical_test: { commit, path: "test/gate.test.ts" } } })); + privacyProcess(` + const result = runGate(${JSON.stringify(root)}); + assert.doesNotMatch(result.stderr, /historical fixture provenance/); + `, { PATH: bin + delimiter + process.env.PATH, REAL_GIT: realGit }); + } finally { rmSync(root, { recursive: true, force: true }); } +}); diff --git a/test/project-sync.test.ts b/test/project-sync.test.ts new file mode 100644 index 00000000..16cdd10d --- /dev/null +++ b/test/project-sync.test.ts @@ -0,0 +1,334 @@ +import assert from "node:assert/strict"; +import { spawnSync } from "node:child_process"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import test, { type TestContext } from "node:test"; +import { createExtensionTestHarness } from "@unbrained/pm-cli/sdk/testing"; +import extension from "../index.ts"; +import { projectItemTag } from "../projects.ts"; +import { captureStderr, jsonResponse, withEnv, withMockGithub } from "./helpers/mock-github-server.ts"; +import type { MockGithubHandler } from "./helpers/mock-github-server.ts"; +import { nodeScenario } from "./helpers/node-scenario.ts"; + +const harness = createExtensionTestHarness(extension, { + capabilities: ["commands", "importers", "schema", "hooks", "preflight", "search"], +}); +const REF = { owner: "acme", number: 5 }; +const OPTIONS = [ + { id: "todo", name: "Todo" }, + { id: "doing", name: "In Progress" }, + { id: "done", name: "Done" }, + { id: "cancel", name: "Canceled" }, +]; +interface LocalItem { + id: string; + title: string; + status: string; + tags?: string[]; + body?: string; + close_reason?: string; +} +interface BoardNode { + id?: string; + fieldValueByName?: { name: string; optionId: string }; + content?: Record | null; +} +interface GraphqlCall { + query: string; + variables: Record; +} + +/** Run the installed PM CLI against a disposable workspace, checking setup. */ +function pm(root: string, args: string[]): string { + const result = spawnSync(process.platform === "win32" ? "pm.cmd" : "pm", ["--path", root, ...args], { + encoding: "utf8", shell: process.platform === "win32", + }); + assert.equal(result.status, 0, result.stderr); + return result.stdout; +} + +/** Allocate a real tracker and remove it even after a failed assertion. */ +function tracker(t: TestContext): string { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "pm-project-sync-")); + t.after(() => fs.rmSync(root, { recursive: true, force: true })); + pm(root, ["init", "fixture"]); + return root; +} + +/** Read authoritative item state through the installed CLI. */ +function items(root: string): LocalItem[] { + const result = JSON.parse(pm(root, ["list", "--all", "--json", "--output-include", "full"])) as { items: LocalItem[] }; + return result.items; +} + +/** Create a local item and return its generated identity. */ +function create(root: string, title: string, tags: string[] = [], status = "open", extra: string[] = []): string { + pm(root, ["create", "task", title, "--status", status, "--tags", tags.join(","), ...extra]); + const item = items(root).find(item => item.title === title); + assert.ok(item); + return item.id; +} + +/** Serve board discovery and inventory, handing actual mutations to the caller. */ +function board(nodes: BoardNode[], mutate?: MockGithubHandler, statusField = true): MockGithubHandler { + return (req, res, body, base) => { + const call = JSON.parse(body) as GraphqlCall; + if (call.query.includes("projectV2(number:")) { + jsonResponse(res, 200, { data: { organization: { projectV2: { + id: "board", title: "Fixture board", statusField: statusField ? { id: "status", name: "Status", options: OPTIONS } : null, + } } } }); + } else if (call.query.includes("items(first:")) { + jsonResponse(res, 200, { data: { node: { items: { nodes, pageInfo: { hasNextPage: false } } } } }); + } else if (mutate) { + mutate(req, res, body, base); + } else { + jsonResponse(res, 400, { errors: [{ message: "Unexpected mutation during preview" }] }); + } + }; +} + +/** A linked draft with an explicit board status. */ +function draft(id: string, name = "Done", optionId = "done"): BoardNode { + return { id, fieldValueByName: { name, optionId }, content: { __typename: "DraftIssue", title: id } }; +} + +/** Invoke the registered Projects command under a synthetic credential. */ +async function sync(root: string, options: Record = {}) { + const ext = await harness; + return withEnv({ GITHUB_TOKEN: "fixture" + "-token", GH_TOKEN: undefined }, () => ext.runCommand({ + command: "github project sync", args: ["acme/5"], options, pmRoot: root, global: { json: true }, + })); +} + +test("project sync previews both directions without mutation, including unmapped statuses", async t => { + const root = tracker(t); + create(root, "Linked", [projectItemTag(REF, "linked")]); + create(root, "Draft", [], "in_progress"); + create(root, "Issue", ["gh:acme/widgets#7"]); + create(root, "Unmapped", [projectItemTag(REF, "unknown")], "blocked"); + const before = items(root); + await withMockGithub(board([draft("linked"), draft("unknown", "Custom", "custom")]), async server => { + const { stderr, result } = await captureStderr(() => sync(root)); + assert.deepEqual(result.result, { dryRun: true, project: "acme/5", push: { actionable: 3, statusSkipped: 1 }, pull: { actionable: 1, statusSkipped: 1 } }); + assert.match(stderr.join("\n"), /add draft @ In Progress/); + assert.match(stderr.join("\n"), /add issue acme\/widgets#7 @ Todo/); + assert.match(stderr.join("\n"), /maps to no board option/); + assert.match(stderr.join("\n"), /maps to no pm status/); + assert.equal(server.requests.length, 2); + }); + assert.deepEqual(items(root), before); +}); + +test("project sync scopes previews, honors dry-run over apply, and rejects empty or unknown ids", async t => { + const root = tracker(t); + const id = create(root, "Scoped"); + create(root, "Other"); + await assert.rejects(sync(root, { ids: "" }), /requires at least one/); + await withMockGithub(board([]), async server => { + await assert.rejects(sync(root, { ids: "fixture-missing" }), /unknown pm item/); + const result = await sync(root, { ids: id, apply: true, "dry-run": true, push: true }); + assert.deepEqual(result.result, { dryRun: true, project: "acme/5", push: { actionable: 1, statusSkipped: 0 }, pull: undefined }); + assert.equal(server.requests.length, 4); + }); +}); + +test("project sync applies draft and issue additions, status writes, and preserves local tags", async t => { + const root = tracker(t); + const draftId = create(root, "Draft", ["keep"], "in_progress", ["--body", "Local body"]); + const issueId = create(root, "Issue", ["gh:acme/widgets#7", "keep"]); + const linkedId = create(root, "Linked", [projectItemTag(REF, "linked")]); + const calls: GraphqlCall[] = []; + await withMockGithub(board([draft("linked")], (_req, res, body) => { + const call = JSON.parse(body) as GraphqlCall; + calls.push(call); + const data = call.query.includes("addProjectV2DraftIssue") ? { addProjectV2DraftIssue: { projectItem: { id: "new-draft" } } } + : call.query.includes("issueOrPullRequest") ? { repository: { issueOrPullRequest: { id: "issue-node" } } } + : call.query.includes("addProjectV2ItemById") ? { addProjectV2ItemById: { item: { id: "new-issue" } } } + : { updateProjectV2ItemFieldValue: { projectV2Item: { id: call.variables.i } } }; + jsonResponse(res, 200, { data }); + }), async () => { + const result = await sync(root, { apply: true }); + assert.deepEqual(result.result, { project: "acme/5", pushed: 3, pushFailed: 0, pulled: 0, pullFailed: 0, prefer: "pm" }); + }); + assert.equal(calls.filter(c => c.query.includes("updateProjectV2ItemFieldValue")).length, 3); + assert.deepEqual(calls.find(c => c.query.includes("addProjectV2DraftIssue"))?.variables, { p: "board", t: "Draft", b: "Local body" }); + assert.deepEqual(calls.find(c => c.query.includes("issueOrPullRequest"))?.variables, { o: "acme", n: "widgets", num: 7 }); + assert.deepEqual(calls.find(c => c.query.includes("addProjectV2ItemById"))?.variables, { p: "board", c: "issue-node" }); + const after = items(root); + assert.deepEqual(new Set(after.find(i => i.id === draftId)?.tags), new Set(["keep", projectItemTag(REF, "new-draft")])); + assert.ok(after.find(i => i.id === issueId)?.tags?.includes(projectItemTag(REF, "new-issue"))); + assert.deepEqual(after.find(i => i.id === linkedId)?.tags, [projectItemTag(REF, "linked")]); +}); + +for (const prefer of ["pm", "github"]) { + test(`project sync resolves simultaneous status conflicts in favor of ${prefer}`, async t => { + const root = tracker(t); + create(root, "Linked", [projectItemTag(REF, "linked")]); + let mutations = 0; + await withMockGithub(board([draft("linked")], (_req, res) => { + mutations++; + jsonResponse(res, 200, { data: {} }); + }), async () => { + const result = await sync(root, { push: true, pull: true, apply: true, prefer }); + assert.deepEqual(result.result, { project: "acme/5", pushed: prefer === "pm" ? 1 : 0, pushFailed: 0, pulled: prefer === "github" ? 1 : 0, pullFailed: 0, prefer }); + }); + assert.equal(mutations, prefer === "pm" ? 1 : 0); + const local = items(root)[0]; + assert.equal(local.status, prefer === "pm" ? "open" : "closed"); + if (prefer === "github") assert.equal(local.close_reason, "GitHub project status → closed"); + }); +} + +test("project pull preserves canceled lifecycle metadata and reports a failed terminal close", async t => { + const root = tracker(t); + create(root, "Cancel", [projectItemTag(REF, "cancel")]); + const closedId = create(root, "Terminal", [projectItemTag(REF, "terminal")]); + pm(root, ["update", closedId, "--status", "canceled", "--close-reason", "Fixture cancellation"]); + await withMockGithub(board([draft("cancel", "Canceled", "cancel"), draft("terminal")]), async () => { + const { stderr, result } = await captureStderr(() => sync(root, { pull: true, apply: true })); + assert.deepEqual(result.result, { project: "acme/5", pushed: 0, pushFailed: 0, pulled: 1, pullFailed: 1, prefer: "pm" }); + assert.match(stderr.join("\n"), /1 failed/); + }); + const local = items(root).find(i => i.title === "Cancel"); + assert.equal(local?.status, "canceled"); + assert.equal(local?.close_reason, "GitHub project status → canceled"); +}); + +test("project push continues after remote failures and fails when the entire batch fails", async t => { + const root = tracker(t); + const missing = create(root, "Missing issue", ["gh:acme/widgets#99"]); + create(root, "Rejected draft"); + create(root, "No id draft"); + const ok = create(root, "Good draft"); + await withMockGithub(board([], (_req, res, body) => { + const call = JSON.parse(body) as GraphqlCall; + if (call.query.includes("issueOrPullRequest")) jsonResponse(res, 200, { data: { repository: null } }); + else if (call.variables.t === "Rejected draft") jsonResponse(res, 200, { errors: [{ message: "Rejected by board" }] }); + else if (call.variables.t === "No id draft") jsonResponse(res, 200, { data: { addProjectV2DraftIssue: null } }); + else jsonResponse(res, 200, { data: { addProjectV2DraftIssue: { projectItem: { id: "good" } } } }); + }), async () => { + const { result, stderr } = await captureStderr(() => sync(root, { push: true, apply: true })); + assert.deepEqual(result.result, { project: "acme/5", pushed: 1, pushFailed: 3, pulled: 0, pullFailed: 0, prefer: "pm" }); + assert.match(stderr.join("\n"), /could not resolve node id/); + assert.match(stderr.join("\n"), /returned no item id/); + await assert.rejects(sync(root, { push: true, apply: true, ids: missing }), /Sync wrote nothing; 1 operation/); + }); + assert.ok(items(root).find(i => i.id === ok)?.tags?.includes(projectItemTag(REF, "good"))); + assert.deepEqual(items(root).find(i => i.id === missing)?.tags, ["gh:acme/widgets#99"]); +}); + +test("project push attaches an issue without a Status field and rejects a missing attachment id", async t => { + const root = tracker(t); + const id = create(root, "Issue", ["gh:acme/widgets#7"]); + let omitId = true; + await withMockGithub(board([], (_req, res, body) => { + const call = JSON.parse(body) as GraphqlCall; + const data = call.query.includes("issueOrPullRequest") ? { repository: { issueOrPullRequest: { id: "issue" } } } + : { addProjectV2ItemById: omitId ? {} : { item: { id: "attached" } } }; + jsonResponse(res, 200, { data }); + }, false), async server => { + await assert.rejects(sync(root, { apply: true, push: true }), /Sync wrote nothing/); + omitId = false; + const result = await sync(root, { apply: true, push: true }); + assert.equal((result.result as { pushed: number }).pushed, 1); + assert.ok(server.requests.every(r => !r.body.includes("updateProjectV2ItemFieldValue"))); + }); + assert.ok(items(root).find(i => i.id === id)?.tags?.includes(projectItemTag(REF, "attached"))); +}); + +test("empty project sync previews report nothing to push or pull", async t => { + const root = tracker(t); + await withMockGithub(board([]), async () => { + const { stderr, result } = await captureStderr(() => sync(root, { "no-add-missing": true })); + assert.deepEqual(result.result, { dryRun: true, project: "acme/5", push: { actionable: 0, statusSkipped: 0 }, pull: { actionable: 0, statusSkipped: 0 } }); + assert.match(stderr.join("\n"), /nothing to push/); + assert.match(stderr.join("\n"), /nothing to pull/); + }); +}); + +test("project sync reports a lost local item after a successful remote attachment", async t => { + const root = tracker(t); + const id = create(root, "Concurrent deletion"); + await withMockGithub(board([], (_req, res, body) => { + const call = JSON.parse(body) as GraphqlCall; + if (call.query.includes("addProjectV2DraftIssue")) { + pm(root, ["delete", id, "--message", "Fixture concurrent deletion"]); + jsonResponse(res, 200, { data: { addProjectV2DraftIssue: { projectItem: { id: "remote-created" } } } }); + } else jsonResponse(res, 200, { data: {} }); + }), async () => { + const { stderr, result } = await captureStderr(() => sync(root, { apply: true })); + assert.deepEqual(result.result, { project: "acme/5", pushed: 1, pushFailed: 0, pulled: 0, pullFailed: 0, prefer: "pm" }); + assert.match(stderr.join("\n"), /linked but tag write failed/); + }); + assert.deepEqual(items(root), []); +}); + +test("project sync validates the token and reference before contacting GitHub", async t => { + const root = tracker(t); + const ext = await harness; + await assert.rejects(ext.runCommand({ command: "github project sync", args: ["invalid"], pmRoot: root }), /Usage:/); + nodeScenario(` + import assert from 'node:assert/strict'; + import { createExtensionTestHarness } from ${JSON.stringify(new URL("../node_modules/@unbrained/pm-cli/dist/sdk/testing.js", import.meta.url).href)}; + import extension from ${JSON.stringify(new URL("../index.ts", import.meta.url).href)}; + const ext = await createExtensionTestHarness(extension, { capabilities: ['commands', 'importers', 'schema', 'hooks', 'preflight', 'search'] }); + await assert.rejects(ext.runCommand({ command: 'github project sync', args: ['acme/5'], pmRoot: ${JSON.stringify(root)} }), /needs a GitHub token/); + `, { GITHUB_TOKEN: undefined, GH_TOKEN: undefined, PATH: "" }); +}); + +test("project sync previews additions without Status and a linked item without an option", async t => { + const root = tracker(t); + create(root, "Draft"); + create(root, "Issue", ["gh:acme/widgets#7"]); + create(root, "Linked", [projectItemTag(REF, "linked")]); + await withMockGithub(board([{ id: "linked", content: { __typename: "DraftIssue" } }], undefined, false), async () => { + const { stderr, result } = await captureStderr(() => sync(root, { push: true })); + assert.equal((result.result as { push: { actionable: number } }).push.actionable, 2); + assert.match(stderr.join("\n"), /add draft\n/); + assert.match(stderr.join("\n"), /add issue acme\/widgets#7\n/); + }); +}); + +test("project sync reports a missing mutation id and malformed linked issue coordinates", async t => { + const root = tracker(t); + create(root, "Draft with missing remote id"); + create(root, "Malformed issue", ["gh:broken#7"]); + await withMockGithub(board([], (_req, res) => jsonResponse(res, 200, { data: { addProjectV2DraftIssue: { projectItem: {} } } })), async () => { + const { stderr } = await captureStderr(() => assert.rejects(sync(root, { push: true, apply: true }), /failed/)); + assert.match(stderr.join("\n"), /returned no item id/); + assert.match(stderr.join("\n"), /could not resolve node id for broken#7/); + }); +}); + +test("project pull-only preview treats an absent board status as no information", async t => { + const root = tracker(t); + create(root, "Linked", [projectItemTag(REF, "linked")]); + await withMockGithub(board([{ id: "linked", content: { __typename: "DraftIssue" } }]), async () => { + const { result, stderr } = await captureStderr(() => sync(root, { pull: true })); + assert.equal((result.result as { push?: unknown }).push, undefined); + assert.match(stderr.join("\n"), /nothing to pull/); + }); +}); + +test("project push preview names a previously unset Status option", async t => { + const root = tracker(t); + create(root, "Linked", [projectItemTag(REF, "linked")]); + await withMockGithub(board([{ id: "linked", content: { __typename: "DraftIssue" } }]), async () => { + const { stderr } = await captureStderr(() => sync(root, { push: true })); + assert.match(stderr.join("\n"), /\(none\) → Todo/); + }); +}); + + +test("project push refuses a missing identity from the real attachment response", async t => { + const root = tracker(t); + create(root, "Linked issue", ["gh:acme/widgets#7"]); + await withMockGithub(board([{ fieldValueByName: { name: "Done", optionId: "done" }, content: { __typename: "Issue", number: 7, repository: { nameWithOwner: "acme/widgets" } } }], (_req, res, body) => { + const query = (JSON.parse(body) as GraphqlCall).query; + jsonResponse(res, 200, { data: query.includes("issueOrPullRequest") ? { repository: { issueOrPullRequest: { id: "issue" } } } : { addProjectV2ItemById: { item: {} } } }); + }), async () => { + const { stderr } = await captureStderr(() => assert.rejects(sync(root, { push: true, apply: true }), /failed/)); + assert.match(stderr.join("\n"), /returned no item id/); + }); +}); diff --git a/test/projects.test.ts b/test/projects.test.ts index d1dd6956..29221efb 100644 --- a/test/projects.test.ts +++ b/test/projects.test.ts @@ -6,12 +6,14 @@ import { buildProjectImportPlan, buildProjectPullPlan, buildProjectPushPlan, + decodeItemId, indexPmByIssue, indexPmByProjectItem, indexProjectItemsByIssue, mapOptionNameToPmStatus, parseProjectRef, parseProjectItemTag, + parseAssignmentMap, parseStatusMap, projectItemTag, resolveOptionForStatus, @@ -47,6 +49,71 @@ test("parseProjectRef parses owner/number, owner#number, and URLs", () => { }); }); +test("project provenance rejects incomplete hex encodings without accepting another board", () => { + for (const invalid of ["", "a", "gg", "00z0"]) assert.equal(decodeItemId(invalid), undefined); + assert.equal(decodeItemId(Buffer.from("PVTI_fixture").toString("hex").toUpperCase()), "PVTI_fixture"); + assert.equal(parseProjectItemTag("gh-project:acme/5#a"), undefined); + assert.equal(parseProjectRef(" "), undefined); + assert.equal(parseProjectRef("acme/" + "9".repeat(400)), undefined); +}); + +test("status mapping falls back only when the board did not provide an explicit matching override", () => { + assert.equal(resolveOptionForStatus(undefined, STATUS_FIELD.options)?.id, "opt_todo"); + assert.equal(resolveOptionForStatus("custom", STATUS_FIELD.options)?.id, "opt_todo"); + assert.equal(mapOptionNameToPmStatus("Done", new Map([["open", "Queued"]])), "closed"); + assert.equal(mapOptionNameToPmStatus("Unrecognized", new Map()), undefined); + assert.equal(parseStatusMap([" = Todo", "open = "]), undefined); + assert.deepEqual(parseAssignmentMap(["Bug=defect"]), new Map([["Bug", "defect"]])); +}); + +test("project indexes omit redacted content and tolerate missing local provenance", () => { + assert.equal(indexPmByProjectItem([{ id: "unlinked" }], REF).size, 0); + assert.equal(indexPmByIssue([{ id: "unlinked" }]).size, 0); + assert.equal(indexProjectItemsByIssue([{ id: "redacted", content: null } as unknown as ProjectItem]).size, 0); + assert.equal(indexProjectItemsByIssue([{ id: "no-number", content: { typename: "Issue", title: "", repo: "acme/widgets" } }]).size, 0); +}); + +test("push planning reconciles issue links, stale board links, missing metadata, and boards without Status", () => { + const board: ProjectItem[] = [{ id: "issue", content: { typename: "Issue", title: "", repo: "acme/widgets", number: 7 } }]; + const local = [ + null, {}, { id: "default" }, + { id: "issue", tags: ["unrelated", "gh:acme/widgets#7"] }, + { id: "stale", tags: [projectItemTag({ owner: "acme", number: 2 }, "foreign"), projectItemTag(REF, "gone")] }, + ]; + const plan = buildProjectPushPlan(local, REF, board, undefined); + assert.equal(plan.entries.length, 3); + assert.equal(plan.entries[0].title, "(untitled)"); + assert.equal(plan.entries[0].action, "add-draft"); + assert.equal(plan.entries[1].action, "noop"); + assert.equal(plan.entries[1].reason, "project has no Status field"); + assert.equal(plan.entries[2].action, "add-draft"); + const unmapped = buildProjectPushPlan([{ id: "linked", status: "blocked", tags: ["gh:acme/widgets#7"] }], REF, board, STATUS_FIELD); + assert.equal(unmapped.entries[0].reason, 'pm status "blocked" has no matching board option'); +}); + +test("pull planning uses issue provenance with missing local metadata and leaves absent statuses alone", () => { + const local = [{ id: "linked", tags: ["gh:acme/widgets#7"] }]; + const plan = buildProjectPullPlan(local, REF, [ + { id: "issue", statusName: "Done", content: { typename: "Issue", title: "", repo: "ACME/widgets", number: 7 } }, + { id: "no-status", content: { typename: "Issue", title: "", repo: "acme/widgets", number: 7 } }, + { id: "unlinked", content: { typename: "Issue", title: "", repo: "acme/widgets" } }, + ]); + assert.deepEqual(plan.entries, [{ itemId: "issue", pmId: "linked", title: "(untitled)", fromStatus: "open", toStatus: "closed" }]); + assert.deepEqual(plan.statusSkipped, []); +}); + +test("import planning recovers untitled and canceled items using issue provenance", () => { + const plan = buildProjectImportPlan([ + { id: "cancel", content: { typename: "Issue", title: "", repo: "acme/widgets", number: 7, state: "closed", stateReason: "not_planned" } }, + { id: "blank", content: { typename: "DraftIssue", title: " " } }, + ], REF, [{ id: "linked", tags: ["gh:acme/widgets#7"] }]); + assert.equal(plan[0].action, "update"); + assert.equal(plan[0].pmId, "linked"); + assert.equal(plan[0].status, "canceled"); + assert.equal(plan[0].title, "(project item cancel)"); + assert.equal(plan[1].title, "(project item blank)"); +}); + test("parseProjectRef rejects garbage", () => { assert.equal(parseProjectRef(undefined), undefined); assert.equal(parseProjectRef(""), undefined); diff --git a/test/review-round1.test.ts b/test/review-round1.test.ts new file mode 100644 index 00000000..19c3416f --- /dev/null +++ b/test/review-round1.test.ts @@ -0,0 +1,52 @@ +/** Regressions for reviewed documentation and fixture hygiene contracts. */ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import test from "node:test"; + +import { withReadOnlyDirectory } from "./helpers/read-only-directory.ts"; + +const repoRoot = path.resolve(import.meta.dirname, ".."); + +test("README gate links resolve to the published command heading", () => { + const readme = fs.readFileSync(path.join(repoRoot, "README.md"), "utf8"); + assert.match(readme, /### `pm github gate`/); + assert.match(readme, /\[`pm github gate`\]\(#pm-github-gate\)/); + assert.doesNotMatch(readme, /#privacy-gate-pm-github-gate/); +}); + +test("coverage task current result records the verified exact-100 gate", () => { + const task = fs.readFileSync(path.join(repoRoot, ".agents/pm/tasks/pm-github-9cjx.toon"), "utf8"); + const result = /^actual_result: (.*)$/m.exec(task)![1]!; + assert.match(result, /100\/100\/100\/100/); + assert.match(result, /passes/); + assert.doesNotMatch(result, /gate fails/); +}); + +test("privacy fixtures never store the complete high-entropy secret literal", () => { + const value = "Zj9kP2mQ7xW4" + "nB8vC5tR1sD"; + for (const filename of ["gate.test.ts", "gate-paths.test.ts", "import-gate.test.ts"]) { + assert.ok(!fs.readFileSync(path.join(repoRoot, "test", filename), "utf8").includes(value), filename); + } +}); + +test("withReadOnlyDirectory restores directory permissions even when the callback throws", async () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pm-github-cleanup-")); + try { + const before = fs.statSync(dir).mode & 0o777; + const observed: number[] = []; + await assert.rejects( + withReadOnlyDirectory(dir, async () => { + observed.push(fs.statSync(dir).mode & 0o777); + throw new Error("forced sync failure"); + }), + /forced sync failure/, + ); + assert.deepEqual(observed, [0o555], "the directory is read-only while the callback runs"); + assert.equal(fs.statSync(dir).mode & 0o777, before, "the original mode is restored after the throw"); + } finally { + fs.chmodSync(dir, 0o755); + fs.rmSync(dir, { recursive: true, force: true }); + } +}); diff --git a/test/smoke.test.ts b/test/smoke.test.ts index cf011760..bdc6f5a2 100644 --- a/test/smoke.test.ts +++ b/test/smoke.test.ts @@ -1,7 +1,7 @@ import assert from "node:assert/strict"; import test from "node:test"; import { execFileSync } from "node:child_process"; -import { mkdtempSync, rmSync } from "node:fs"; +import { mkdtempSync, rmSync, readFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { fileURLToPath } from "node:url"; @@ -10,6 +10,7 @@ import { createExtensionTestHarness, type ExtensionTestHarness } from "@unbraine import extension, { CommandError, + type PmItem, EXIT_CODE, applyClientFilters, applyExportPlan, @@ -114,6 +115,7 @@ const baseOpts: ImportOptions = { dryRun: false, atomic: false, linkDeps: false, + gate: false, }; test("extension has required shape", () => { @@ -440,7 +442,7 @@ test("scopeItemsByIds selects requested items and reports unknown ids", () => { }); test("mapSearchHits maps remote issue numbers to local items, dropping unmatched", () => { - const index = new Map([ + const index = new Map([ ["owner/repo#10", { id: "pm-a", tags: ["gh:owner/repo#10"] }], ["owner/repo#20", { id: "pm-b", tags: ["gh:owner/repo#20"] }], ]); @@ -989,7 +991,6 @@ test("native github importer advertises --include-comments as an alias for --wit }); test("manifest uses only runtime-supported capability names", async () => { - const { readFileSync } = await import("node:fs"); const manifest = JSON.parse(readFileSync(new URL("../manifest.json", import.meta.url), "utf-8")); assert.ok(!manifest.capabilities.includes("exporters"), "exporters is a registration, not a manifest capability"); }); diff --git a/test/verify-release-publish-attestation.test.ts b/test/verify-release-publish-attestation.test.ts index 90689212..6a3b3370 100644 --- a/test/verify-release-publish-attestation.test.ts +++ b/test/verify-release-publish-attestation.test.ts @@ -351,7 +351,7 @@ test("the entry path produces the package verifier's own report for every publis `${shape.name}: the exit code must follow the verdict`, ); - process.exitCode = savedExitCode; + process.exitCode = savedExitCode ?? 0; const packageOutput = capture(() => { report(verify(fixture), (line) => process.stdout.write(`${line}\n`), (code) => { process.exitCode = code; }); }); @@ -375,7 +375,7 @@ test("the entry path produces the package verifier's own report for every publis assert.doesNotMatch(launcherOutput, /FAIL - /u, `${shape.name}: an attested publish must produce no failure`); } } finally { - process.exitCode = savedExitCode; + process.exitCode = savedExitCode ?? 0; } }, ); diff --git a/tsconfig.json b/tsconfig.json index 2a785d27..9e9950f2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,6 +4,7 @@ "module": "NodeNext", "moduleResolution": "NodeNext", "strict": true, + "erasableSyntaxOnly": true, "outDir": "./dist", "rootDir": ".", "declaration": true, @@ -15,7 +16,7 @@ "sourceMap": true, "esModuleInterop": true, "skipLibCheck": true, - "noEmitOnError": false, + "noEmitOnError": true, "forceConsistentCasingInFileNames": true, "rewriteRelativeImportExtensions": true, "allowImportingTsExtensions": true, diff --git a/tsconfig.test.json b/tsconfig.test.json index d1867bef..961aa394 100644 --- a/tsconfig.test.json +++ b/tsconfig.test.json @@ -1,7 +1,8 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "allowJs": false, + "allowJs": true, + "checkJs": true, "declaration": false, "declarationMap": false, "noEmit": true,