From 434df4237bdf7d5fdd3ddbd7ae42ce10d75e13ae Mon Sep 17 00:00:00 2001 From: SteveBot <1153461+unbraind@users.noreply.github.com> Date: Mon, 5 Oct 2026 08:33:33 +0200 Subject: [PATCH 01/60] Add fail-closed privacy gate over proposed tracker changes and gated import pm github gate scans the added lines of the staged/working/untracked diff under the resolved pm tracker path (or an explicit --diff file) and fails closed on credentials, personal data, and host paths; findings name rule + item + field + content hash, never the matched text. Reviewed false positives are allowlisted by sha256 content hash with a written justification. pm github import --gate verifies plan completeness and provenance idempotency before the write and runs the gate after it, so a sync pipeline can never push a leak. Ships the reusable workflow_call sync workflow and a caller example for fleet repos. pm item: pm-github-f1at --- .agents/pm/features/pm-github-f1at.toon | 22 + .agents/pm/history/pm-github-f1at.jsonl | 9 + .github/workflows/pm-github-sync.yml | 160 +++++ docs/sync-workflow-caller.yml | 30 + gate.ts | 900 ++++++++++++++++++++++++ index.ts | 384 +++++++++- test/gate.test.ts | 720 +++++++++++++++++++ test/import-gate.test.ts | 593 ++++++++++++++++ test/link-deps.test.ts | 1 + test/smoke.test.ts | 1 + 10 files changed, 2781 insertions(+), 39 deletions(-) create mode 100644 .agents/pm/features/pm-github-f1at.toon create mode 100644 .agents/pm/history/pm-github-f1at.jsonl create mode 100644 .github/workflows/pm-github-sync.yml create mode 100644 docs/sync-workflow-caller.yml create mode 100644 gate.ts create mode 100644 test/gate.test.ts create mode 100644 test/import-gate.test.ts diff --git a/.agents/pm/features/pm-github-f1at.toon b/.agents/pm/features/pm-github-f1at.toon new file mode 100644 index 00000000..f2a64d9b --- /dev/null +++ b/.agents/pm/features/pm-github-f1at.toon @@ -0,0 +1,22 @@ +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[4]: "area:ci","area:gates","area:import","area:privacy" +created_at: "2026-10-05T06:09:58.912Z" +updated_at: "2026-10-05T06:33:25.742Z" +assignee: pi-glm +claim_principal: pi-glm +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. +comments[2]{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." +files[4]{path,scope}: + .github/workflows/pm-github-sync.yml,project + docs/sync-workflow-caller.yml,project + gate.ts,project + index.ts,project +body: "" diff --git a/.agents/pm/history/pm-github-f1at.jsonl b/.agents/pm/history/pm-github-f1at.jsonl new file mode 100644 index 00000000..314416c2 --- /dev/null +++ b/.agents/pm/history/pm-github-f1at.jsonl @@ -0,0 +1,9 @@ +{"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"} diff --git a/.github/workflows/pm-github-sync.yml b/.github/workflows/pm-github-sync.yml new file mode 100644 index 00000000..2d133509 --- /dev/null +++ b/.github/workflows/pm-github-sync.yml @@ -0,0 +1,160 @@ +# 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" + +# Least privilege for the job's needs: push the sync branch (contents: write) +# and create/update the review PR (pull-requests: write). Nothing else. +permissions: + contents: write + pull-requests: write + +concurrency: + group: pm-github-sync-${{ inputs.repository }} + cancel-in-progress: false + +jobs: + gated-sync: + runs-on: ubuntu-latest + env: + GH_TOKEN: ${{ github.token }} + REPOSITORY: ${{ inputs.repository }} + SYNC_BRANCH: ${{ inputs.sync-branch }} + steps: + - name: Checkout main + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + fetch-tags: true + ref: main + + - 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" + git fetch --force origin main + git switch --force-create "${SYNC_BRANCH}" origin/main + + # The calling repo's own devDependency provides the pm CLI (npm ci). + - name: Install dependencies + run: npm ci + + - name: Install pinned pm-github extension + shell: bash + run: | + set -euo pipefail + ./node_modules/.bin/pm package install "npm:pm-github@${{ inputs.pm-github-version }}" --project + node -e 'if (require("./.agents/pm/extensions/pm-github/package.json").version !== process.argv[1]) process.exit(1)' "${{ inputs.pm-github-version }}" + + # Keep the install's bookkeeping out of the sync diff; the extension tree + # itself is operational state the privacy gate deliberately excludes. + - name: Restore managed extension registry + shell: bash + run: | + set -euo pipefail + managed_registry=".agents/pm/extensions/.managed-extensions.json" + if git ls-files --error-unmatch "${managed_registry}" > /dev/null 2>&1; then + git restore -- "${managed_registry}" + else + rm -f "${managed_registry}" + fi + + - name: Validate GitHub access + run: ./node_modules/.bin/pm github validate --repo "${REPOSITORY}" + + - name: Preview GitHub to pm plan + run: ./node_modules/.bin/pm github import "${REPOSITORY}" --state all --atomic --dry-run + + # 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 + run: ./node_modules/.bin/pm github import "${REPOSITORY}" --state all --atomic --gate + + - name: Verify strict pm health + run: ./node_modules/.bin/pm health --strict-exit + + - name: Commit gated sync changes and open review PR + shell: bash + run: | + set -euo pipefail + git add -- .agents/pm ':(exclude).agents/pm/extensions' ':(exclude).agents/pm/locks' ':(exclude).agents/pm/checkpoints' + if git diff --cached --quiet; then + echo "GitHub issues already match the pm tracker; no PR is needed." + 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}" + printf -- '- [%s](https://github.com/%s/blob/main/%s)\n' "${item_id}" "${REPOSITORY}" "${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)" + git push --force origin "${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="$(gh pr list -R "${REPOSITORY}" --head "${SYNC_BRANCH}" --base main --state open --json number --jq '.[0].number // empty')" + 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 \ No newline at end of file diff --git a/docs/sync-workflow-caller.yml b/docs/sync-workflow-caller.yml new file mode 100644 index 00000000..7e9ef824 --- /dev/null +++ b/docs/sync-workflow-caller.yml @@ -0,0 +1,30 @@ +# Caller workflow for the reusable gated pm-github sync. +# +# A fleet repo copies this file to .github/workflows/pm-github-sync.yml and +# edits exactly three values: its repository (or omits it to use its own), +# the pinned pm-github version, and the schedule. The schedule stays with the +# caller; the audited implementation (gate, plan, health, push, PR) lives in +# one reusable workflow in unbraind/pm-github. + +name: Sync GitHub issues into pm + +on: + schedule: + - cron: "17 2 * * *" # pick a per-repo minute to spread fleet load + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + +concurrency: + group: pm-github-sync + cancel-in-progress: false + +jobs: + gated-sync: + # Pin the reusable workflow to the released pm-github tag that ships it. + uses: unbraind/pm-github/.github/workflows/pm-github-sync.yml@v2026.10.04 + with: + repository: unbraind/pm-graph # default: the calling repository + pm-github-version: "2026.10.4" # required: exact published version \ No newline at end of file diff --git a/gate.ts b/gate.ts new file mode 100644 index 00000000..37623511 --- /dev/null +++ b/gate.ts @@ -0,0 +1,900 @@ +// 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 of 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"; + +// --------------------------------------------------------------------------- +// 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; +} + +// --------------------------------------------------------------------------- +// 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 { + 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; +} + +// --------------------------------------------------------------------------- +// 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[0-9A-Z]{16}/g }, + { rule: "slack-token", pattern: /xox[abprs]-[A-Za-z0-9-]{10,}/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: "anthropic-api-key", pattern: /sk-ant-[A-Za-z0-9_-]{20,}/g }, + { + rule: "bearer-token", + pattern: /[Bb]earer\s+[A-Za-z0-9._~+/=-]{20,}/g, + // A real bearer credential is base64/base62-ish and virtually always mixes + // letters with digits; prose after "Bearer" ("Bearer authentication + // tokens…") is all letters and must not fire. + confirm: (matched) => /[0-9]/.test(matched), + }, + { 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", + pattern: /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g, + // GitHub, GitLab and most mail systems use a "noreply" domain or local part + // for automated identities; anything else in imported issue text is a real + // person's address and fails closed. + confirm: (matched) => !/[.\-@]no-?reply[.\-@]/i.test(matched), + }, + { + 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. + 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, + }, +]; + +/** + * 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", + pattern: /(?:^|[\s"'`(\[=,{])\/(?:home|Users|root|tmp|var|mnt|media|opt|srv|private|www|inetpub)\//g, + }, + { + rule: "windows-host-path", + pattern: /[A-Za-z]:\\(?:Users|home|root|tmp|Windows|Program Files|Inetpub|www)\\/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._-]{1,}\//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; + +/** Value shapes that are structured data, not secrets, even when long. */ +const NON_SECRET_VALUE_SHAPES = + /(?:\d{4}-\d{2}-\d{2})|(?:^\{?[0-9a-f]{8}-[0-9a-f]{4}-)/i; + +/** + * 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 not a date/UUID shape. 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; + if (NON_SECRET_VALUE_SHAPES.test(value)) return false; + const segments = identifier.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; +} + +/** + * 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[] { + 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(line); match; match = rule.pattern.exec(line)) { + if (!rule.confirm || rule.confirm(match[0])) { + hits.push({ rule: rule.rule, matched: match[0], index: match.index }); + } + } + } + HIGH_ENTROPY_ASSIGNMENT.lastIndex = 0; + for (let match = HIGH_ENTROPY_ASSIGNMENT.exec(line); match; match = HIGH_ENTROPY_ASSIGNMENT.exec(line)) { + const [identifier, value] = [match[1]!, match[2]!]; + if (isHighEntropySecretAssignment(identifier, value)) { + hits.push({ rule: "high-entropy-assignment", matched: value, index: 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; + }); +} + +/** Patch-entry shape in a pm history JSONL event: `{"op":"add","path":"/metadata/x","value":"…"}`. */ +const JSONL_PATCH_ENTRY = /"path":"\/(?:metadata\/)?([A-Za-z0-9_]+)"\s*,\s*"value":"/g; + +/** 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 ""; +} + +/** + * Resolve the item field a rule hit landed in, for a non-toon changed file. + * + * History JSONL events change one or more metadata fields per line; the hit is + * attributed to the last patch entry that starts at or before the hit offset. + * Anything else (settings, schema, unknown formats) reports the empty field + * rather than a guess. + * + * @param filePath - The changed file path. + * @param line - The full added line text. + * @param hitIndex - Character offset of the hit within the line. + * @returns The field name, or `""` when unattributable. + */ +function jsonlFieldForHit(filePath: string, line: string, hitIndex: number): string { + if (!filePath.endsWith(".jsonl")) return ""; + JSONL_PATCH_ENTRY.lastIndex = 0; + let field = ""; + let match: RegExpExecArray | null; + while ((match = JSONL_PATCH_ENTRY.exec(line))) { + if (match.index > hitIndex) break; + field = match[1]!; + } + return field; +} + +// --------------------------------------------------------------------------- +// 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[] { + const files: ChangeFile[] = []; + let filePath = ""; + let itemId = ""; + let walk: WalkLine[] = []; + + 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.startsWith("+++ ")) { + flushFile(); + filePath = raw.slice(4).trim(); + if (filePath === "/dev/null") filePath = ""; + else filePath = filePath.replace(/^[ab]\//, ""); + itemId = ""; + continue; + } + if (filePath === "") continue; // headers, index lines, and text before any +++ + if (raw.startsWith("--- ")) continue; + if (raw.startsWith("@@")) 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.startsWith("+")) { + const text = raw.slice(1); + if (itemId === "") itemId = itemIdForFile(filePath, text); + walk.push({ text, added: true }); + continue; + } + if (raw.startsWith("-")) continue; + if (raw.startsWith(" ") || raw === "") { + walk.push({ text: raw.startsWith(" ") ? raw.slice(1) : raw, added: false }); + continue; + } + // Anything else (e.g. `diff --git` line without prior `+++`, index lines) + // ends the current file's context; flush keeps line order intact. + flushFile(); + } + 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); + 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.some((segment) => normalized.includes(segment)); +} + +/** + * 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], { encoding: "utf-8", maxBuffer: 64 * 1024 * 1024 }); + return { + ok: result.status === 0, + stdout: result.stdout ?? "", + stderr: result.stderr ?? String(result.error ?? "git failed"), + }; +} + +/** + * 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 ?? ((filePath: string) => fs.readFileSync(filePath, "utf-8")); + + 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 = toplevel.stdout.trim(); + const trackerRel = path.relative(repoRoot, pmDataDir); + 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; + + const status = runGit(repoRoot, ["status", "--porcelain=v1", "-z", "--untracked-files=all", "--", trackerSpec]); + if (!status.ok) { + throw new GateInputError(`pm github gate: git status failed: ${status.stderr.trim()}`); + } + + 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 (isOperationalTrackerPath(filePath)) continue; + if (xy === "??") { + const absolute = path.join(repoRoot, filePath); + let size = 0; + try { + size = fs.statSync(absolute).size; + } catch { + // Vanished between status and read: nothing exists to publish, so + // there is no input to scan for this path — not a silent pass, the + // file simply contributes no change. + continue; + } + if (size > UNTRACKED_FILE_BYTE_CAP) { + throw new GateInputError( + `pm github gate: untracked tracker file is too large to scan (${filePath}); refusing to guess.`, + ); + } + changes.push(wholeFileChange(absolute, filePath, readFileSync)); + continue; + } + if (xy.includes("D") && !/[AMU]/.test(xy)) continue; // pure deletion + const diff = runGit(repoRoot, ["diff", "--no-color", "--unified=3", "HEAD", "--", filePath]); + if (!diff.ok) { + throw new GateInputError(`pm github gate: git diff failed for ${filePath}: ${diff.stderr.trim()}`); + } + if (diff.stdout.trim() === "") continue; + const [parsedDiff] = parseUnifiedDiff(diff.stdout); + if (parsedDiff) changes.push(parsedDiff); + } + 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 { + if (required) { + throw new GateInputError(`pm github gate: allowlist file is unreadable: ${path.basename(allowlistPath)}`); + } + return new Map(); + } + let parsed: unknown; + try { + parsed = JSON.parse(raw); + } catch { + throw new GateInputError(`pm github gate: allowlist file is not valid JSON: ${path.basename(allowlistPath)}`); + } + 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 "${hash.slice(0, 12)}…" 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 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(); + for (const line of change.addedLines) { + for (const hit of scanLineForRuleHits(line.text)) { + const field = change.filePath.endsWith(".toon") + ? line.field + : jsonlFieldForHit(change.filePath, line.text, hit.index); + const itemId = change.filePath.endsWith(".toon") ? change.itemId : itemIdForFile(change.filePath, line.text); + const key = `${hit.rule}\0${change.itemId || itemId}\0${field}`; + if (seen.has(key)) continue; + seen.add(key); + findings.push({ + rule: hit.rule, + item_id: change.itemId || itemId, + 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 ?? ((filePath: string) => fs.readFileSync(filePath, "utf-8")); + 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 }); + } + } catch (err: unknown) { + if (err instanceof GateInputError) throw err; + 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 — ${err instanceof Error ? err.message : String(err)}`, + ); + } + + 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); + } + } + + try { + 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, + }; + } catch (err: unknown) { + throw new GateInputError( + `pm github gate: scanner error — ${err instanceof Error ? err.message : String(err)}`, + ); + } +} + +/** + * 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; +} \ No newline at end of file diff --git a/index.ts b/index.ts index 6a4a6d88..818b8068 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,17 @@ import { } from "@unbrained/pm-cli/sdk"; import { collectNewOrderingCycleWarnings as sdkCollectNewOrderingCycleWarnings } from "@unbrained/pm-cli/sdk/graph"; +import { + type GateReport, + type GateVerdict, + GateInputError, + formatGateReport, + runTrackerGate, +} from "./gate.ts"; + +export { resolvePmDataDir } from "./gate.ts"; +import { resolvePmDataDir } from "./gate.ts"; + import { type ProjectItem, type ProjectItemContent, @@ -167,6 +179,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 = ( @@ -1704,27 +1720,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. * @@ -2315,6 +2310,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"), }; } @@ -2735,14 +2731,192 @@ 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; + if (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, + }; +} + +/** 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; +} + +/** + * 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 { + let report: GateReport; + try { + report = runTrackerGate({ pmRoot }); + } catch (err: unknown) { + throw new CommandError( + err instanceof GateInputError + ? err.message + : `pm github gate: could not run the gate — ${err instanceof Error ? err.message : String(err)}`, + EXIT_CODE.GENERIC_FAILURE, + ); + } + const lines = formatGateReport(report); + console.error(lines.join("\n")); + if (report.verdict === "fail") { + throw new CommandError(lines.join("\n"), 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. @@ -2787,6 +2961,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, @@ -2794,9 +2971,10 @@ export async function runImport( wouldUpdate: 0, wouldSkip: 0, atomic: true, + ...(emptyGateReceipt ? { gate: emptyGateReceipt } : {}), }; } - 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 +2984,25 @@ 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[] = []; + // 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) { + const planned: PreparedGithubImport[] = []; + const skippedNumbers: number[] = []; for (const issue of filtered) { const entry = await prepareGithubImport( issue, @@ -2824,10 +3013,47 @@ export async function runImport( dependencies.fetchIssueComments, ); if (!entry) { - skipped++; + skippedNumbers.push(issue.number); continue; } - prepared.push(entry); + planned.push(entry); + } + gateCompleteness = verifyImportPlanCompleteness(filtered, planned, skippedNumbers); + gateIdempotency = verifyImportIdempotency( + existingItems, + planned, + repo, + ); + gatePlan = planned; + gatePlanByNumber = new Map(planned.map((entry) => [entry.issueNumber, entry])); + skipped = skippedNumbers.length; + console.error( + `pm github gate: plan verified — ${gateCompleteness.planned} planned, ${gateCompleteness.skipped} skipped, ` + + `${gateIdempotency.matched_by_provenance} matched by provenance, ${gateIdempotency.new_entries} new.`, + ); + } + + 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 ? [...gatePlan] : []; + if (!gatePlan) { + for (const issue of filtered) { + const entry = await prepareGithubImport( + issue, + repo, + opts, + token, + existing.get(`${repo.toLowerCase()}#${issue.number}`), + dependencies.fetchIssueComments, + ); + if (!entry) { + skipped++; + continue; + } + prepared.push(entry); + } } if (prepared.length === 0) { @@ -2870,6 +3096,7 @@ export async function runImport( wouldSkip: skipped, atomic: true, ...(opts.linkDeps ? { wouldLinkDependencyCandidates: countDependencyRefCandidates(repo, filtered) } : {}), + ...(opts.gate ? { gate: { completeness: gateCompleteness, idempotency: gateIdempotency, scan: "post-write" } } : {}), }; } @@ -2901,6 +3128,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 +3140,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; } @@ -3083,6 +3321,7 @@ export async function runImport( wouldSkip: skipped, ...(opts.atomic ? { atomic: true } : {}), ...(opts.linkDeps ? { wouldLinkDependencyCandidates: countDependencyRefCandidates(repo, filtered) } : {}), + ...(opts.gate ? { gate: { completeness: gateCompleteness, idempotency: gateIdempotency, scan: "post-write" } } : {}), }; } @@ -3094,7 +3333,10 @@ export async function runImport( ? 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. + const gateReceipt = opts.gate ? gateImportWrites(pmRoot) : undefined; + return { imported, updated, skipped, ...depLinkResultFields(depLink), ...(gateReceipt ? { gate: gateReceipt } : {}) }; } /** @@ -4880,6 +5122,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 +5144,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" }, ]; @@ -5196,6 +5444,64 @@ 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"); + let report: GateReport; + try { + report = runTrackerGate({ + pmRoot: ctx.pm_root, + ...(diffFile ? { diffFile } : {}), + ...(allowlistFile ? { allowlistFile } : {}), + }); + } catch (err: unknown) { + // GateInputError carries a user-facing message; anything else is an + // unexpected scanner failure. Both fail closed — never pass vacuously. + throw new CommandError( + err instanceof GateInputError + ? err.message + : `pm github gate: scanner error — ${err instanceof Error ? err.message : String(err)}`, + EXIT_CODE.GENERIC_FAILURE, + ); + } + if (ctx.global?.json !== true) { + for (const line of formatGateReport(report)) console.error(line); + } + if (report.verdict === "fail") { + throw new CommandError( + formatGateReport(report).join("\n"), + EXIT_CODE.GENERIC_FAILURE, + ); + } + return report; + }, + }); + // ----------------------------------------------------------------------- // command — `pm github validate` (diagnostics: gh/token/repo reachability) // ----------------------------------------------------------------------- diff --git a/test/gate.test.ts b/test/gate.test.ts new file mode 100644 index 00000000..68d83c8d --- /dev/null +++ b/test/gate.test.ts @@ -0,0 +1,720 @@ +// 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 { createHash } from "node:crypto"; +import { + GATE_ALLOWLIST_FILENAME, + GateInputError, + attributeToonFields, + formatGateReport, + isHighEntropySecretAssignment, + parseUnifiedDiff, + readGateAllowlist, + runGitDefault, + runTrackerGate, + scanLineForRuleHits, + shannonEntropyPerChar, + type GateReport, +} from "../gate.ts"; +import extension from "../index.ts"; + +const MANIFEST_CAPABILITIES = ["commands", "importers", "schema", "hooks", "preflight", "search"] as const; +const harnessPromise: Promise = + createExtensionTestHarness(extension, { capabilities: [...MANIFEST_CAPABILITIES] }); + +// --- 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 = "Zj9kP2mQ7xW4nB8vC5tR1sD"; +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}`], + ["openai-api-key", `key ${OPENAI_KEY}`], + ["anthropic-api-key", `key ${ANTHROPIC_KEY}`], + ["openai-project-key", "key " + "sk-proj-" + "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 requires a digit-bearing credential, not prose", () => { + assert.deepStrictEqual( + scanLineForRuleHits("Authorization: Bearer " + "aB3cD9eFg1hA7bC2dE4fG").map((hit) => hit.rule), + ["bearer-token"], + ); + assert.deepStrictEqual(scanLineForRuleHits("Use Bearer authenticationmechanism 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 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,3 +1,6 @@", + " 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 @@", + `+{"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("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 statusFailingGit = (cwd: string, args: readonly string[]) => + args.includes("status") + ? { ok: false, stdout: "", stderr: "status boom" } + : { ok: true, stdout: path.resolve(cwd), stderr: "" }; + assert.throws( + () => runTrackerGate({ pmRoot: "/unused", dependencies: { runGit: statusFailingGit } }), + GateInputError, + ); +}); + +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 }), GateInputError); + } 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", () => { + assert.throws( + () => + runTrackerGate({ + pmRoot: "/deep/outside/path", + dependencies: { + runGit: (cwd, args) => + args.includes("rev-parse") + ? { ok: true, stdout: path.join(os.tmpdir(), "elsewhere"), stderr: "" } + : { ok: false, stdout: "", stderr: "unused" }, + }, + }), + (err: unknown) => { + assert.ok(err instanceof GateInputError); + assert.match(err.message, /escapes the Git work tree/); + return true; + }, + ); +}); + +test("a vanished untracked tracker file contributes no change instead of failing", () => { + const { root } = initGateRepo(); + try { + const report = 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: "" }; + }, + }, + }); + assert.strictEqual(report.verdict, "pass"); + assert.strictEqual(report.scanned_files, 0); + } 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 allowlist = path.join(os.tmpdir(), `gate-allowlist-${process.pid}.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 @@", + `+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(allowlist, { 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, path.join(root, 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); + } finally { + fs.rmSync(dir, { recursive: true, force: true }); + } +}); \ No newline at end of file diff --git a/test/import-gate.test.ts b/test/import-gate.test.ts new file mode 100644 index 00000000..4c86fe34 --- /dev/null +++ b/test/import-gate.test.ts @@ -0,0 +1,593 @@ +// 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 { 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 { readFileSync } from "node:fs"; + +import { createExtensionTestHarness, type ExtensionTestHarness } from "@unbrained/pm-cli/sdk/testing"; + +import extension, { + CommandError, + parseImportOptions, + runImport, + verifyImportPlanCompleteness, + verifyImportIdempotency, + type GhComment, + type GhIssue, + type ImportGateReceipt, + type PmItem, + type PreparedGithubImport, +} from "../index.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 = "Zj9kP2mQ7xW4nB8vC5tR1sD"; + +/** + * 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 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. + */ +/** + * 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, + }; +} + +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("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|Fetching issues/.test(message)), + "the gate runs its plan phase before failing", + ); + } 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"]); + + // The worktree still holds the uncommitted gated failure, but a retried + // gate command alone reproduces the fail verdict without re-importing. + await assert.rejects( + ext.runCommand({ command: "github gate", global: { json: true }, pmRoot: root }), + (err: unknown) => { + assert.match((err as Error).message, /FAIL/); + assert.match((err as Error).message, /github-token-classic/); + return true; + }, + ); + }); + assert.strictEqual(git(["status", "--porcelain"]).status, 0); + } 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 = readFileSync(workflowPath, "utf-8"); +const callerExample = 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"); + // Least privilege: exactly the two permissions a gated sync needs. + const permissions = /^permissions:\n contents: write\n pull-requests: write\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 add -- \.agents\/pm ':\(exclude\)\.agents\/pm\/extensions'/); +}); + +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 origin "\$\{SYNC_BRANCH\}"/); +}); + +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@v[0-9.]+/, + ); + assert.match(callerExample, /pm-github-version: "[0-9.]+"/); + 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"); +}); \ No newline at end of file diff --git a/test/link-deps.test.ts b/test/link-deps.test.ts index 5cc24f08..ab9632a6 100644 --- a/test/link-deps.test.ts +++ b/test/link-deps.test.ts @@ -54,6 +54,7 @@ function importOpts(overrides: Partial = {}): ImportOptions { atomic: false, linkDeps: true, ...overrides, + gate: false, }; } diff --git a/test/smoke.test.ts b/test/smoke.test.ts index cf011760..e56dbc90 100644 --- a/test/smoke.test.ts +++ b/test/smoke.test.ts @@ -114,6 +114,7 @@ const baseOpts: ImportOptions = { dryRun: false, atomic: false, linkDeps: false, + gate: false, }; test("extension has required shape", () => { From 54ca70c11e4b861380b13e8e57fec90e4c9f1288 Mon Sep 17 00:00:00 2001 From: SteveBot <1153461+unbraind@users.noreply.github.com> Date: Mon, 5 Oct 2026 08:44:15 +0200 Subject: [PATCH 02/60] Document gated imports and preserve interrupted run evidence --- .agents/pm/features/pm-github-f1at.toon | 12 ++-- .agents/pm/history/pm-github-f1at.jsonl | 4 ++ README.md | 75 ++++++++++++++++++++++++- 3 files changed, 86 insertions(+), 5 deletions(-) diff --git a/.agents/pm/features/pm-github-f1at.toon b/.agents/pm/features/pm-github-f1at.toon index f2a64d9b..cfed74c2 100644 --- a/.agents/pm/features/pm-github-f1at.toon +++ b/.agents/pm/features/pm-github-f1at.toon @@ -6,17 +6,21 @@ status: in_progress priority: 1 tags[4]: "area:ci","area:gates","area:import","area:privacy" created_at: "2026-10-05T06:09:58.912Z" -updated_at: "2026-10-05T06:33:25.742Z" -assignee: pi-glm -claim_principal: pi-glm +updated_at: "2026-10-05T06:44:15.604Z" +assignee: codex-sol +claim_principal: codex-sol 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. -comments[2]{created_at,author,text}: +comments[4]{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." files[4]{path,scope}: .github/workflows/pm-github-sync.yml,project docs/sync-workflow-caller.yml,project gate.ts,project index.ts,project +docs[1]{path,scope}: + README.md,project body: "" diff --git a/.agents/pm/history/pm-github-f1at.jsonl b/.agents/pm/history/pm-github-f1at.jsonl index 314416c2..6fdb0442 100644 --- a/.agents/pm/history/pm-github-f1at.jsonl +++ b/.agents/pm/history/pm-github-f1at.jsonl @@ -7,3 +7,7 @@ {"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"} diff --git a/README.md b/README.md index 03d42b76..39249d1b 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`](#privacy-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,78 @@ 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 of 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. 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, OpenAI, Anthropic tokens, generic `Authorization: Bearer` values, `-----BEGIN … PRIVATE KEY-----` blocks, and high-entropy assignments to secret-named identifiers (`token = "…"`, `api_key: …`) whose value has high measured entropy; +- **personal data** — email addresses other than no-reply identities (`@users.noreply.github.com`, `noreply@…`, `@noreply.…`), and phone numbers in international (`+…`) or North-American (`(555) 123-4567`, `555-123-4567`) notation; +- **host paths** — absolute local filesystem paths (POSIX home/system directories and Windows drive paths), and named home-directory references. + +Unreadable input, a Git failure, a malformed allowlist, or a scanner error fails the gate — a clean verdict can never be the result of not being able to read what was scanned. The `locks/`, `extensions/`, and `checkpoints/` tracker subpaths are operational state and are not scanned; item files, history, schema, and settings stay in scope. + +**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. + +The `gate` receipt in the result reports the verdict, scanned file count, findings, and allowlisted count. + +## 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** → `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`). + +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 + +concurrency: + group: pm-github-sync + cancel-in-progress: false + +jobs: + gated-sync: + uses: unbraind/pm-github/.github/workflows/pm-github-sync.yml@RELEASE_COMMIT_SHA + with: + repository: unbraind/pm-graph # default: the calling repository + 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`. + ## Validate / diagnostics ### `pm github validate` From f9cf59d461021fb8ecff8b0126662d072ee74b19 Mon Sep 17 00:00:00 2001 From: SteveBot <1153461+unbraind@users.noreply.github.com> Date: Mon, 5 Oct 2026 09:00:44 +0200 Subject: [PATCH 03/60] Close staged-content and allowlist bypasses before automated pushes --- .agents/pm/features/pm-github-f1at.toon | 6 +- .agents/pm/history/pm-github-f1at.jsonl | 2 + .github/workflows/pm-github-sync.yml | 38 +++++- docs/sync-workflow-caller.yml | 25 +--- gate.ts | 152 +++++++++++++++-------- index.ts | 24 ++-- scripts/privacy-gate.ts | 20 ++- test/fixtures/privacy-gate/manifest.json | 7 ++ test/gate.test.ts | 83 +++++++++++-- test/import-gate.test.ts | 11 +- test/import-lock.test.ts | 11 +- 11 files changed, 262 insertions(+), 117 deletions(-) diff --git a/.agents/pm/features/pm-github-f1at.toon b/.agents/pm/features/pm-github-f1at.toon index cfed74c2..1fc86990 100644 --- a/.agents/pm/features/pm-github-f1at.toon +++ b/.agents/pm/features/pm-github-f1at.toon @@ -6,16 +6,18 @@ status: in_progress priority: 1 tags[4]: "area:ci","area:gates","area:import","area:privacy" created_at: "2026-10-05T06:09:58.912Z" -updated_at: "2026-10-05T06:44:15.604Z" +updated_at: "2026-10-05T07:00:44.735Z" assignee: codex-sol claim_principal: codex-sol 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. -comments[4]{created_at,author,text}: +comments[6]{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." files[4]{path,scope}: .github/workflows/pm-github-sync.yml,project docs/sync-workflow-caller.yml,project diff --git a/.agents/pm/history/pm-github-f1at.jsonl b/.agents/pm/history/pm-github-f1at.jsonl index 6fdb0442..c496af4d 100644 --- a/.agents/pm/history/pm-github-f1at.jsonl +++ b/.agents/pm/history/pm-github-f1at.jsonl @@ -11,3 +11,5 @@ {"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"} diff --git a/.github/workflows/pm-github-sync.yml b/.github/workflows/pm-github-sync.yml index 2d133509..a0f2076d 100644 --- a/.github/workflows/pm-github-sync.yml +++ b/.github/workflows/pm-github-sync.yml @@ -54,7 +54,18 @@ jobs: GH_TOKEN: ${{ github.token }} 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}" ]] + [[ "${PM_GITHUB_VERSION}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] + [[ "${SYNC_BRANCH}" == automation/* ]] + git check-ref-format "refs/heads/${SYNC_BRANCH}" + - name: Checkout main uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: @@ -78,6 +89,8 @@ jobs: git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" git fetch --force origin main + sync_lease="$(git ls-remote --heads origin "refs/heads/${SYNC_BRANCH}" | cut -f1)" + 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). @@ -88,8 +101,8 @@ jobs: shell: bash run: | set -euo pipefail - ./node_modules/.bin/pm package install "npm:pm-github@${{ inputs.pm-github-version }}" --project - node -e 'if (require("./.agents/pm/extensions/pm-github/package.json").version !== process.argv[1]) process.exit(1)' "${{ inputs.pm-github-version }}" + ./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}" # Keep the install's bookkeeping out of the sync diff; the extension tree # itself is operational state the privacy gate deliberately excludes. @@ -104,16 +117,29 @@ jobs: rm -f "${managed_registry}" fi + - name: Verify gate is available + run: ./node_modules/.bin/pm github gate --json + - name: Validate GitHub access run: ./node_modules/.bin/pm github validate --repo "${REPOSITORY}" - name: Preview GitHub to pm plan - run: ./node_modules/.bin/pm github import "${REPOSITORY}" --state all --atomic --dry-run + 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 - run: ./node_modules/.bin/pm github import "${REPOSITORY}" --state all --atomic --gate + run: ./node_modules/.bin/pm github import "${REPOSITORY}" --state all --atomic --gate --with-comments + + - name: Verify repeat import is a no-op + shell: bash + run: | + set -euo pipefail + git add -- .agents/pm ':(exclude).agents/pm/extensions' ':(exclude).agents/pm/locks' ':(exclude).agents/pm/checkpoints' + ./node_modules/.bin/pm github import "${REPOSITORY}" --state all --atomic --gate --with-comments + git diff --exit-code -- .agents/pm ':(exclude).agents/pm/extensions' ':(exclude).agents/pm/locks' ':(exclude).agents/pm/checkpoints' + [[ -z "$(git ls-files --others --exclude-standard -- .agents/pm ':(exclude).agents/pm/extensions' ':(exclude).agents/pm/locks' ':(exclude).agents/pm/checkpoints')" ]] + ./node_modules/.bin/pm github gate --json - name: Verify strict pm health run: ./node_modules/.bin/pm health --strict-exit @@ -144,7 +170,7 @@ jobs: exit 1 fi git commit -m "chore(pm): sync GitHub issues (gated)" - git push --force origin "${SYNC_BRANCH}" + 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}." @@ -157,4 +183,4 @@ jobs: 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 \ No newline at end of file + fi diff --git a/docs/sync-workflow-caller.yml b/docs/sync-workflow-caller.yml index 7e9ef824..6a5eba6f 100644 --- a/docs/sync-workflow-caller.yml +++ b/docs/sync-workflow-caller.yml @@ -1,30 +1,15 @@ -# Caller workflow for the reusable gated pm-github sync. -# -# A fleet repo copies this file to .github/workflows/pm-github-sync.yml and -# edits exactly three values: its repository (or omits it to use its own), -# the pinned pm-github version, and the schedule. The schedule stays with the -# caller; the audited implementation (gate, plan, health, push, PR) lives in -# one reusable workflow in unbraind/pm-github. - +# Replace release placeholders after the gated extension is published. name: Sync GitHub issues into pm - on: schedule: - - cron: "17 2 * * *" # pick a per-repo minute to spread fleet load + - cron: "17 2 * * *" workflow_dispatch: - permissions: contents: write pull-requests: write - -concurrency: - group: pm-github-sync - cancel-in-progress: false - jobs: gated-sync: - # Pin the reusable workflow to the released pm-github tag that ships it. - uses: unbraind/pm-github/.github/workflows/pm-github-sync.yml@v2026.10.04 + uses: unbraind/pm-github/.github/workflows/pm-github-sync.yml@RELEASE_COMMIT_SHA with: - repository: unbraind/pm-graph # default: the calling repository - pm-github-version: "2026.10.4" # required: exact published version \ No newline at end of file + repository: unbraind/pm-graph + pm-github-version: "RELEASE_VERSION" diff --git a/gate.ts b/gate.ts index 37623511..a606972e 100644 --- a/gate.ts +++ b/gate.ts @@ -30,6 +30,7 @@ import { createHash } from "node:crypto"; import fs from "node:fs"; import path from "node:path"; import { spawnSync } from "node:child_process"; +import { resolvePmRoot } from "@unbrained/pm-cli/sdk"; // --------------------------------------------------------------------------- // Types @@ -152,13 +153,7 @@ export interface TrackerGateInput { * @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; + return resolvePmRoot(process.cwd(), pmRoot); } // --------------------------------------------------------------------------- @@ -193,7 +188,7 @@ 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[0-9A-Z]{16}/g }, + { rule: "aws-access-key-id", pattern: /(?:AKIA|ASIA)[0-9A-Z]{16}/g }, { rule: "slack-token", pattern: /xox[abprs]-[A-Za-z0-9-]{10,}/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 }, @@ -201,10 +196,7 @@ const CREDENTIAL_RULES: readonly ContentRule[] = [ { rule: "bearer-token", pattern: /[Bb]earer\s+[A-Za-z0-9._~+/=-]{20,}/g, - // A real bearer credential is base64/base62-ish and virtually always mixes - // letters with digits; prose after "Bearer" ("Bearer authentication - // tokens…") is all letters and must not fire. - confirm: (matched) => /[0-9]/.test(matched), + }, { rule: "private-key-block", pattern: /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----/g }, ]; @@ -220,7 +212,10 @@ const PERSONAL_DATA_RULES: readonly ContentRule[] = [ // GitHub, GitLab and most mail systems use a "noreply" domain or local part // for automated identities; anything else in imported issue text is a real // person's address and fails closed. - confirm: (matched) => !/[.\-@]no-?reply[.\-@]/i.test(matched), + confirm: (matched) => { + const [local, domain] = matched.toLowerCase().split("@"); + return !/^(?:no-?reply)$/.test(local!) && !/(?:^|\.)no-?reply(?:\.|$)/.test(domain!); + }, }, { rule: "phone-number", @@ -249,11 +244,11 @@ const PERSONAL_DATA_RULES: readonly ContentRule[] = [ const HOST_PATH_RULES: readonly ContentRule[] = [ { rule: "absolute-host-path", - pattern: /(?:^|[\s"'`(\[=,{])\/(?:home|Users|root|tmp|var|mnt|media|opt|srv|private|www|inetpub)\//g, + pattern: /(?:^|[\s"'`([=,{])\/(?:home|Users|root|tmp|var|mnt|media|opt|srv|private|www|inetpub|etc|usr|workspace|workspaces)\/[^\s"'`<>),;\]}]*/g, }, { rule: "windows-host-path", - pattern: /[A-Za-z]:\\(?:Users|home|root|tmp|Windows|Program Files|Inetpub|www)\\/g, + pattern: /[A-Za-z]:\\[^\s"'`<>),;\]}]+/g, }, { rule: "home-username", @@ -261,7 +256,7 @@ const HOST_PATH_RULES: readonly ContentRule[] = [ // 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._-]{1,}\//g, + pattern: /(?:^|[\s"'`([=,])~[A-Za-z][A-Za-z0-9._-]*\/[^\s"'`<>),;\]}]*/g, }, ]; @@ -360,20 +355,39 @@ interface RuleHit { * @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++) { + const unicode = /^\\u([0-9a-f]{4})/i.exec(line.slice(index)); + 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(line); match; match = rule.pattern.exec(line)) { + for (let match = rule.pattern.exec(decoded); match; match = rule.pattern.exec(decoded)) { if (!rule.confirm || rule.confirm(match[0])) { - hits.push({ rule: rule.rule, matched: match[0], index: match.index }); + hits.push({ rule: rule.rule, matched: match[0], index: offsets[match.index]! }); } } } HIGH_ENTROPY_ASSIGNMENT.lastIndex = 0; - for (let match = HIGH_ENTROPY_ASSIGNMENT.exec(line); match; match = HIGH_ENTROPY_ASSIGNMENT.exec(line)) { + 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: match.index }); + hits.push({ rule: "high-entropy-assignment", matched: value, index: offsets[match.index]! }); } } return hits; @@ -480,10 +494,22 @@ function jsonlFieldForHit(filePath: string, line: string, hitIndex: number): str * @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) { @@ -501,36 +527,63 @@ export function parseUnifiedDiff(diffText: string): ChangeFile[] { 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("--- ")) { + completeHunk(); + flushFile(); + filePath = ""; + continue; + } if (raw.startsWith("+++ ")) { + completeHunk(); flushFile(); - filePath = raw.slice(4).trim(); + 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; } if (filePath === "") continue; // headers, index lines, and text before any +++ - if (raw.startsWith("--- ")) continue; - if (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("+")) { + newRemaining--; const text = raw.slice(1); if (itemId === "") itemId = itemIdForFile(filePath, text); walk.push({ text, added: true }); continue; } - if (raw.startsWith("-")) continue; - if (raw.startsWith(" ") || raw === "") { + if (raw.startsWith("-")) { oldRemaining--; continue; } + if (raw.startsWith(" ")) { + oldRemaining--; + newRemaining--; walk.push({ text: raw.startsWith(" ") ? raw.slice(1) : raw, added: false }); continue; } - // Anything else (e.g. `diff --git` line without prior `+++`, index lines) - // ends the current file's context; flush keeps line order intact. - flushFile(); + if (raw === "" && oldRemaining === 0 && newRemaining === 0) { completeHunk(); continue; } + throw new GateInputError("pm github gate: invalid unified diff content."); } + completeHunk(); flushFile(); return files; @@ -643,7 +696,7 @@ export function collectTrackerChange( const status = runGit(repoRoot, ["status", "--porcelain=v1", "-z", "--untracked-files=all", "--", trackerSpec]); if (!status.ok) { - throw new GateInputError(`pm github gate: git status failed: ${status.stderr.trim()}`); + throw new GateInputError("pm github gate: git status failed; refusing to scan incomplete input."); } const changes: ChangeFile[] = []; @@ -662,27 +715,26 @@ export function collectTrackerChange( try { size = fs.statSync(absolute).size; } catch { - // Vanished between status and read: nothing exists to publish, so - // there is no input to scan for this path — not a silent pass, the - // file simply contributes no change. - continue; + throw new GateInputError("pm github gate: a proposed file cannot be inspected."); } if (size > UNTRACKED_FILE_BYTE_CAP) { throw new GateInputError( - `pm github gate: untracked tracker file is too large to scan (${filePath}); refusing to guess.`, + `pm github gate: untracked tracker file is too large to scan ; refusing to guess.`, ); } changes.push(wholeFileChange(absolute, filePath, readFileSync)); continue; } if (xy.includes("D") && !/[AMU]/.test(xy)) continue; // pure deletion - const diff = runGit(repoRoot, ["diff", "--no-color", "--unified=3", "HEAD", "--", filePath]); - if (!diff.ok) { - throw new GateInputError(`pm github gate: git diff failed for ${filePath}: ${diff.stderr.trim()}`); + const addedLines: AddedLine[] = []; + for (const revision of [["--cached"], []]) { + const diff = runGit(repoRoot, ["diff", "--no-color", "--no-ext-diff", "--no-textconv", "--no-renames", "--unified=3", ...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 (diff.stdout.trim() === "") continue; - const [parsedDiff] = parseUnifiedDiff(diff.stdout); - if (parsedDiff) changes.push(parsedDiff); + if (addedLines.length > 0) changes.push({ filePath, itemId: itemIdForFile(filePath, ""), addedLines }); } return changes; } @@ -719,17 +771,15 @@ export function readGateAllowlist( let raw: string; try { raw = readFileSync(allowlistPath); - } catch { - if (required) { - throw new GateInputError(`pm github gate: allowlist file is unreadable: ${path.basename(allowlistPath)}`); - } - return new Map(); + } 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: ${path.basename(allowlistPath)}`); + throw new GateInputError(`pm github gate: allowlist file is not valid JSON: (allowlist)`); } 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`); @@ -737,7 +787,7 @@ export function readGateAllowlist( 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 "${hash.slice(0, 12)}…" is not a sha256 content 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() === "") { @@ -779,7 +829,7 @@ function scanChangeFile(change: ChangeFile): Array 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. @@ -626,7 +626,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 @@ -2705,7 +2705,8 @@ async function prepareGithubImport( comments = await fetchIssueComments!(issue, repo, token); } catch (err: unknown) { const msg = err instanceof Error ? err.message : String(err); - console.error(`#${issue.number}: failed to fetch comments — ${msg}`); + if (opts.gate) throw new CommandError(`pm github gate: comments for issue #${issue.number} could not be read.`); + console.error(opts.gate ? `pm github gate: comments for issue #${issue.number} could not be read.` : `#${issue.number}: failed to fetch comments — ${msg}`); } } @@ -2787,7 +2788,10 @@ export function verifyImportPlanCompleteness( (number) => !fetched.has(number), ); const countsReconcile = prepared.length + skippedNumbers.length === issues.length; - if (duplicates > 0 || unaccounted.length > 0 || foreign.length > 0 || !countsReconcile) { + 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(", ")}], ` + @@ -2889,14 +2893,14 @@ function gateImportWrites(pmRoot: string): ImportGateReceipt { throw new CommandError( err instanceof GateInputError ? err.message - : `pm github gate: could not run the gate — ${err instanceof Error ? err.message : String(err)}`, + : `pm github gate: could not run the gate; scan did not complete.`, EXIT_CODE.GENERIC_FAILURE, ); } const lines = formatGateReport(report); console.error(lines.join("\n")); if (report.verdict === "fail") { - throw new CommandError(lines.join("\n"), EXIT_CODE.GENERIC_FAILURE); + throw new CommandError(JSON.stringify({ ...report, error: "pm github gate: FAIL" }), EXIT_CODE.GENERIC_FAILURE); } return { verdict: report.verdict, @@ -3078,7 +3082,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}.`, @@ -5485,7 +5489,7 @@ export default defineExtension({ throw new CommandError( err instanceof GateInputError ? err.message - : `pm github gate: scanner error — ${err instanceof Error ? err.message : String(err)}`, + : `pm github gate: scanner error; scan did not complete.`, EXIT_CODE.GENERIC_FAILURE, ); } @@ -5494,7 +5498,7 @@ export default defineExtension({ } if (report.verdict === "fail") { throw new CommandError( - formatGateReport(report).join("\n"), + JSON.stringify({ ...report, error: "pm github gate: FAIL" }), EXIT_CODE.GENERIC_FAILURE, ); } diff --git a/scripts/privacy-gate.ts b/scripts/privacy-gate.ts index 36598894..3eaf564d 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)); } /** @@ -180,7 +182,19 @@ function listFixtureTreeBlobs(root: string): Set { function loadFixtureExemptions(root: string): 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) 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 = spawnSync("git", ["ls-tree", source.commit, "--", source.path], { cwd: root, encoding: "utf8" }); + if (resolved.status !== 0 || !resolved.stdout.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])); } /** 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.test.ts b/test/gate.test.ts index 68d83c8d..cd26195d 100644 --- a/test/gate.test.ts +++ b/test/gate.test.ts @@ -39,6 +39,65 @@ const MANIFEST_CAPABILITIES = ["commands", "importers", "schema", "hooks", "pref 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("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(""), []); +}); + +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"), []); + 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)); +}); + // --- 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); @@ -110,7 +169,7 @@ test("scanner fires on every high-confidence credential signature", () => { ["openai-api-key", `key ${OPENAI_KEY}`], ["anthropic-api-key", `key ${ANTHROPIC_KEY}`], ["openai-project-key", "key " + "sk-proj-" + "I".repeat(30)], - ["private-key-block", "-----BEGIN RSA PRIVATE KEY-----"], + ["private-key-block", "-----BEGIN " + "RSA PRIVATE KEY-----"], ]; for (const [rule, line] of cases) { const hits = scanLineForRuleHits(line); @@ -121,12 +180,12 @@ test("scanner fires on every high-confidence credential signature", () => { } }); -test("bearer rule requires a digit-bearing credential, not prose", () => { +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 authenticationmechanism everywhere"), []); + assert.deepStrictEqual(scanLineForRuleHits("Use Bearer authentication everywhere"), []); }); test("personal-data rules: emails and phone numbers, with no-reply exemption", () => { @@ -213,7 +272,7 @@ test("parseUnifiedDiff attributes item id and toon fields, and skips removed lin "index 111..222 100644", "--- a/.agents/pm/issues/pm-x1.toon", "+++ b/.agents/pm/issues/pm-x1.toon", - "@@ -1,3 +1,6 @@", + "@@ -1,2 +1,5 @@", " id: pm-x1", '-title: "old"', '+title: "New title"', @@ -224,7 +283,7 @@ test("parseUnifiedDiff attributes item id and toon fields, and skips removed lin "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 @@", + "@@ -0,0 +1,2 @@", `+{"op":"add","path":"/metadata/body","value":"mail ${PERSONAL_EMAIL}"}`, `+{"op":"add","path":"/metadata/id","value":"pm-x1"}`, ].join("\n"); @@ -417,10 +476,10 @@ test("collectTrackerChange fails closed when the tracker escapes the Git work tr ); }); -test("a vanished untracked tracker file contributes no change instead of failing", () => { +test("a vanished untracked tracker file fails closed", () => { const { root } = initGateRepo(); try { - const report = runTrackerGate({ + assert.throws(() => runTrackerGate({ pmRoot: root, dependencies: { runGit: (cwd, args) => { @@ -435,9 +494,7 @@ test("a vanished untracked tracker file contributes no change instead of failing return { ok: true, stdout: "", stderr: "" }; }, }, - }); - assert.strictEqual(report.verdict, "pass"); - assert.strictEqual(report.scanned_files, 0); + }), GateInputError); } finally { fs.rmSync(root, { recursive: true, force: true }); } @@ -503,7 +560,7 @@ test("an allowlisted content hash is suppressed and counted, never widened to a const diff = [ "--- /dev/null", "+++ b/.agents/pm/issues/pm-allow.toon", - "@@ -0,0 +2 @@", + "@@ -0,0 +2,2 @@", `+body: "token ${GH_TOKEN}"`, `+body: "token ${NPM_TOKEN}"`, ].join("\n"); @@ -563,7 +620,7 @@ test("the default repository-root allowlist is consulted when present", () => { const report = runTrackerGate({ pmRoot: root }); assert.strictEqual(report.verdict, "pass"); assert.strictEqual(report.allowlisted, 1); - assert.strictEqual(report.allowlist_path, path.join(root, GATE_ALLOWLIST_FILENAME)); + assert.strictEqual(report.allowlist_path, GATE_ALLOWLIST_FILENAME); } finally { fs.rmSync(root, { recursive: true, force: true }); } @@ -717,4 +774,4 @@ test("pm github gate scans an explicit --diff file and honors --allowlist", asyn } finally { fs.rmSync(dir, { recursive: true, force: true }); } -}); \ No newline at end of file +}); diff --git a/test/import-gate.test.ts b/test/import-gate.test.ts index 4c86fe34..0ed4c610 100644 --- a/test/import-gate.test.ts +++ b/test/import-gate.test.ts @@ -17,7 +17,6 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { fileURLToPath } from "node:url"; -import { readFileSync } from "node:fs"; import { createExtensionTestHarness, type ExtensionTestHarness } from "@unbrained/pm-cli/sdk/testing"; @@ -514,8 +513,8 @@ test("gated dry-run embeds the verified plan receipts and writes nothing", async const repoRoot = fileURLToPath(new URL("..", import.meta.url)); const workflowPath = path.join(repoRoot, ".github", "workflows", "pm-github-sync.yml"); -const workflow = readFileSync(workflowPath, "utf-8"); -const callerExample = readFileSync(path.join(repoRoot, "docs", "sync-workflow-caller.yml"), "utf-8"); +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 { @@ -577,16 +576,16 @@ test("the PR body links every changed item as a permanent main-tree pm link", () 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 origin "\$\{SYNC_BRANCH\}"/); + 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@v[0-9.]+/, + /uses: unbraind\/pm-github\/\.github\/workflows\/pm-github-sync\.yml@RELEASE_COMMIT_SHA/, ); - assert.match(callerExample, /pm-github-version: "[0-9.]+"/); + 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"); diff --git a/test/import-lock.test.ts b/test/import-lock.test.ts index c7ba3c09..3f5658b5 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 @@ -145,6 +146,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 +159,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 +442,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 +503,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 ?? ""); From 5774879f806bb387d16c2176be335b76f045b488 Mon Sep 17 00:00:00 2001 From: SteveBot <1153461+unbraind@users.noreply.github.com> Date: Mon, 5 Oct 2026 09:52:39 +0200 Subject: [PATCH 04/60] Verify persisted provenance and exercise the reusable sync pipeline --- .agents/pm/features/pm-github-f1at.toon | 14 +- .agents/pm/history/pm-github-4a03.jsonl | 2 + .agents/pm/history/pm-github-f1at.jsonl | 4 + .agents/pm/issues/pm-github-4a03.toon | 14 ++ .github/workflows/pm-github-sync.yml | 30 ++-- README.md | 11 +- docs/sync-workflow-caller.yml | 1 + gate.ts | 131 ++++++++++-------- index.ts | 155 +++++++++++---------- projects.ts | 17 ++- test/gate.test.ts | 34 +++++ test/import-gate.test.ts | 175 ++++++++++++++++++++++-- 12 files changed, 419 insertions(+), 169 deletions(-) create mode 100644 .agents/pm/history/pm-github-4a03.jsonl create mode 100644 .agents/pm/issues/pm-github-4a03.toon diff --git a/.agents/pm/features/pm-github-f1at.toon b/.agents/pm/features/pm-github-f1at.toon index 1fc86990..7118a4f8 100644 --- a/.agents/pm/features/pm-github-f1at.toon +++ b/.agents/pm/features/pm-github-f1at.toon @@ -6,23 +6,29 @@ status: in_progress priority: 1 tags[4]: "area:ci","area:gates","area:import","area:privacy" created_at: "2026-10-05T06:09:58.912Z" -updated_at: "2026-10-05T07:00:44.735Z" +updated_at: "2026-10-05T07:52:13.198Z" assignee: codex-sol claim_principal: codex-sol 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. -comments[6]{created_at,author,text}: +comments[7]{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." -files[4]{path,scope}: + "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." +files[6]{path,scope}: .github/workflows/pm-github-sync.yml,project docs/sync-workflow-caller.yml,project gate.ts,project index.ts,project -docs[1]{path,scope}: + test/gate.test.ts,project + test/import-gate.test.ts,project +tests[1]{command,scope,timeout_seconds,provenance{author,created_at,source_kind,source_ref}}: + node --test test/gate.test.ts test/import-gate.test.ts,project,600,codex-sol,"2026-10-05T07:43:22.444Z",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-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-f1at.jsonl b/.agents/pm/history/pm-github-f1at.jsonl index c496af4d..89c9c4d7 100644 --- a/.agents/pm/history/pm-github-f1at.jsonl +++ b/.agents/pm/history/pm-github-f1at.jsonl @@ -13,3 +13,7 @@ {"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"} 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/.github/workflows/pm-github-sync.yml b/.github/workflows/pm-github-sync.yml index a0f2076d..16d72c28 100644 --- a/.github/workflows/pm-github-sync.yml +++ b/.github/workflows/pm-github-sync.yml @@ -42,6 +42,7 @@ on: permissions: contents: write pull-requests: write + issues: read concurrency: group: pm-github-sync-${{ inputs.repository }} @@ -104,24 +105,14 @@ jobs: ./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}" - # Keep the install's bookkeeping out of the sync diff; the extension tree - # itself is operational state the privacy gate deliberately excludes. - - name: Restore managed extension registry - shell: bash - run: | - set -euo pipefail - managed_registry=".agents/pm/extensions/.managed-extensions.json" - if git ls-files --error-unmatch "${managed_registry}" > /dev/null 2>&1; then - git restore -- "${managed_registry}" - else - rm -f "${managed_registry}" - fi + - 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 - run: ./node_modules/.bin/pm github validate --repo "${REPOSITORY}" + run: ./node_modules/.bin/pm github validate --repo "${REPOSITORY}" --json | jq -e ' .ok == true and .repo_accessible == true ' - name: Preview GitHub to pm plan run: ./node_modules/.bin/pm github import "${REPOSITORY}" --state all --atomic --gate --dry-run --with-comments @@ -135,10 +126,15 @@ jobs: shell: bash run: | set -euo pipefail - git add -- .agents/pm ':(exclude).agents/pm/extensions' ':(exclude).agents/pm/locks' ':(exclude).agents/pm/checkpoints' + 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).agents/pm/extensions' ':(exclude).agents/pm/locks' ':(exclude).agents/pm/checkpoints' - [[ -z "$(git ls-files --others --exclude-standard -- .agents/pm ':(exclude).agents/pm/extensions' ':(exclude).agents/pm/locks' ':(exclude).agents/pm/checkpoints')" ]] + 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 @@ -148,7 +144,7 @@ jobs: shell: bash run: | set -euo pipefail - git add -- .agents/pm ':(exclude).agents/pm/extensions' ':(exclude).agents/pm/locks' ':(exclude).agents/pm/checkpoints' + ./node_modules/.bin/pm github gate --json if git diff --cached --quiet; then echo "GitHub issues already match the pm tracker; no PR is needed." exit 0 diff --git a/README.md b/README.md index 39249d1b..bbfdb451 100644 --- a/README.md +++ b/README.md @@ -268,7 +268,7 @@ It scans **only the proposed change** — the added lines of the staged/working/ - **personal data** — email addresses other than no-reply identities (`@users.noreply.github.com`, `noreply@…`, `@noreply.…`), and phone numbers in international (`+…`) or North-American (`(555) 123-4567`, `555-123-4567`) notation; - **host paths** — absolute local filesystem paths (POSIX home/system directories and Windows drive paths), and named home-directory references. -Unreadable input, a Git failure, a malformed allowlist, or a scanner error fails the gate — a clean verdict can never be the result of not being able to read what was scanned. The `locks/`, `extensions/`, and `checkpoints/` tracker subpaths are operational state and are not scanned; item files, history, schema, and settings stay in scope. +Unreadable input, malformed or truncated diffs, binary input, 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: @@ -288,11 +288,13 @@ Unreadable input, a Git failure, a malformed allowlist, or a scanner error fails 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. -The `gate` receipt in the result reports the verdict, scanned file count, findings, and allowlisted count. +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** → `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`). +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** → `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)): @@ -306,6 +308,7 @@ on: permissions: contents: write pull-requests: write + issues: read concurrency: group: pm-github-sync @@ -339,7 +342,7 @@ 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. ## Multi-agent merge safety diff --git a/docs/sync-workflow-caller.yml b/docs/sync-workflow-caller.yml index 6a5eba6f..4489d009 100644 --- a/docs/sync-workflow-caller.yml +++ b/docs/sync-workflow-caller.yml @@ -7,6 +7,7 @@ on: permissions: contents: write pull-requests: write + issues: read jobs: gated-sync: uses: unbraind/pm-github/.github/workflows/pm-github-sync.yml@RELEASE_COMMIT_SHA diff --git a/gate.ts b/gate.ts index a606972e..c06e80e1 100644 --- a/gate.ts +++ b/gate.ts @@ -134,6 +134,8 @@ export interface TrackerGateInput { 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> }[]; } // --------------------------------------------------------------------------- @@ -190,12 +192,13 @@ const CREDENTIAL_RULES: readonly ContentRule[] = [ { 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[abprs]-[A-Za-z0-9-]{10,}/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: "anthropic-api-key", pattern: /sk-ant-[A-Za-z0-9_-]{20,}/g }, { rule: "bearer-token", - pattern: /[Bb]earer\s+[A-Za-z0-9._~+/=-]{20,}/g, + 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 }, @@ -244,7 +247,7 @@ const PERSONAL_DATA_RULES: readonly ContentRule[] = [ const HOST_PATH_RULES: readonly ContentRule[] = [ { rule: "absolute-host-path", - pattern: /(?:^|[\s"'`([=,{])\/(?:home|Users|root|tmp|var|mnt|media|opt|srv|private|www|inetpub|etc|usr|workspace|workspaces)\/[^\s"'`<>),;\]}]*/g, + pattern: /(?:^|[\s"'`([=,{])\/[A-Za-z0-9._-]+(?:\/[^\s"'`<>),;\]}]*)?/g, }, { rule: "windows-host-path", @@ -279,7 +282,7 @@ const SECRET_IDENTIFIER_PAIRS: readonly string[] = ["api_key", "access_key", "pr /** 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; + /(?:^|[^A-Za-z0-9_])([A-Za-z][A-Za-z0-9_-]{0,48})(?:\\?["'`])?\s*[:=]\s*(?:\\?["'`])?([A-Za-z0-9+/_=-]{20,256})(?:\\?["'`])?/g; /** Value shapes that are structured data, not secrets, even when long. */ const NON_SECRET_VALUE_SHAPES = @@ -379,7 +382,9 @@ export function scanLineForRuleHits(line: string): RuleHit[] { rule.pattern.lastIndex = 0; for (let match = rule.pattern.exec(decoded); match; match = rule.pattern.exec(decoded)) { if (!rule.confirm || rule.confirm(match[0])) { - hits.push({ rule: rule.rule, matched: match[0], index: offsets[match.index]! }); + const matched = rule.rule === "absolute-host-path" ? match[0].slice(match[0].indexOf("/")) + : rule.rule === "home-username" ? match[0].slice(match[0].indexOf("~")) : match[0]; + hits.push({ rule: rule.rule, matched, index: offsets[match.index]! }); } } } @@ -429,9 +434,6 @@ export function attributeToonFields(lines: readonly WalkLine[]): string[] { }); } -/** Patch-entry shape in a pm history JSONL event: `{"op":"add","path":"/metadata/x","value":"…"}`. */ -const JSONL_PATCH_ENTRY = /"path":"\/(?:metadata\/)?([A-Za-z0-9_]+)"\s*,\s*"value":"/g; - /** Item id statement inside a pm history JSONL event. */ const JSONL_ITEM_ID = /"path":"\/metadata\/id"\s*,\s*"value":"([^"]+)"/; @@ -453,31 +455,6 @@ function itemIdForFile(filePath: string, line: string): string { return ""; } -/** - * Resolve the item field a rule hit landed in, for a non-toon changed file. - * - * History JSONL events change one or more metadata fields per line; the hit is - * attributed to the last patch entry that starts at or before the hit offset. - * Anything else (settings, schema, unknown formats) reports the empty field - * rather than a guess. - * - * @param filePath - The changed file path. - * @param line - The full added line text. - * @param hitIndex - Character offset of the hit within the line. - * @returns The field name, or `""` when unattributable. - */ -function jsonlFieldForHit(filePath: string, line: string, hitIndex: number): string { - if (!filePath.endsWith(".jsonl")) return ""; - JSONL_PATCH_ENTRY.lastIndex = 0; - let field = ""; - let match: RegExpExecArray | null; - while ((match = JSONL_PATCH_ENTRY.exec(line))) { - if (match.index > hitIndex) break; - field = match[1]!; - } - return field; -} - // --------------------------------------------------------------------------- // Change collection — unified diff parsing and Git working-tree collection // --------------------------------------------------------------------------- @@ -625,7 +602,7 @@ function wholeFileChange( * 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/"]; +const UNTRACKED_SCAN_EXCLUDED = ["locks", "extensions", "checkpoints"]; /** * Whether a repo-relative changed path is operational state, out of gate scope. @@ -635,7 +612,7 @@ const UNTRACKED_SCAN_EXCLUDED = ["/locks/", "/extensions/", "/checkpoints/"]; */ function isOperationalTrackerPath(repoRelative: string): boolean { const normalized = repoRelative.replace(/\\/g, "/"); - return UNTRACKED_SCAN_EXCLUDED.some((segment) => normalized.includes(segment)); + return UNTRACKED_SCAN_EXCLUDED.includes(normalized.split("/")[0]!); } /** @@ -708,12 +685,15 @@ export function collectTrackerChange( // 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 (isOperationalTrackerPath(filePath)) continue; + if (xy === "??" && isOperationalTrackerPath(path.relative(pmDataDir, path.join(repoRoot, filePath)))) continue; if (xy === "??") { const absolute = path.join(repoRoot, filePath); let size = 0; + let symlink = false; try { - size = fs.statSync(absolute).size; + const stat = fs.lstatSync(absolute); + size = stat.size; + symlink = stat.isSymbolicLink(); } catch { throw new GateInputError("pm github gate: a proposed file cannot be inspected."); } @@ -722,13 +702,13 @@ export function collectTrackerChange( `pm github gate: untracked tracker file is too large to scan ; refusing to guess.`, ); } - changes.push(wholeFileChange(absolute, filePath, readFileSync)); + 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, ["diff", "--no-color", "--no-ext-diff", "--no-textconv", "--no-renames", "--unified=3", ...revision, "--", filePath]); + const diff = runGit(repoRoot, ["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."); } @@ -824,21 +804,56 @@ function scanChangeFile(change: ChangeFile): Array = []; const seen = new Set(); for (const line of change.addedLines) { - for (const hit of scanLineForRuleHits(line.text)) { - const field = change.filePath.endsWith(".toon") - ? line.field - : jsonlFieldForHit(change.filePath, line.text, hit.index); - const itemId = change.filePath.endsWith(".toon") ? change.itemId : itemIdForFile(change.filePath, line.text); - const key = `${hit.rule}\0${change.itemId || itemId}\0${field}\0${hit.matched}`; - if (seen.has(key)) continue; - seen.add(key); - findings.push({ - rule: hit.rule, - item_id: change.itemId || itemId, - field, - hash: createHash("sha256").update(hit.matched).digest("hex"), - matched: hit.matched, - }); + const values: Array<{ text: string; field: string; structural?: boolean }> = []; + if (change.filePath.endsWith(".jsonl")) { + 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"]) { + if (typeof record[key] === "string") values.push({ text: record[key], field: key, structural: /^\/(?:metadata(?:\/|$)|body(?:\/|$))/.test(record[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; + 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)) { + if (value.structural && ["absolute-host-path", "windows-host-path", "home-username"].includes(hit.rule)) continue; + 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; @@ -873,6 +888,11 @@ export function runTrackerGate(input: TrackerGateInput): GateReport { 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; if (err instanceof Error && err.message.startsWith("pm github gate:")) throw err; @@ -915,9 +935,10 @@ export function runTrackerGate(input: TrackerGateInput): GateReport { added_lines: addedLines, findings, allowlisted, - allowlist_path: allowlistPath ? path.basename(allowlistPath) : "", + allowlist_path: allowlistPath ? (input.allowlistFile ? "(explicit allowlist)" : GATE_ALLOWLIST_FILENAME) : "", }; } catch (err: unknown) { + if (err instanceof GateInputError) throw err; throw new GateInputError( `pm github gate: scanner error; scan did not complete.`, ); @@ -947,4 +968,4 @@ export function formatGateReport(report: GateReport): string[] { ); } return lines; -} \ No newline at end of file +} diff --git a/index.ts b/index.ts index 20562e1a..8e13a826 100644 --- a/index.ts +++ b/index.ts @@ -70,6 +70,7 @@ import { parseProjectItemTag, parseProjectRef, parseStatusMap, + parseAssignmentMap, projectItemTag, } from "./projects.ts"; @@ -667,17 +668,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 @@ -2860,6 +2851,25 @@ export function verifyImportIdempotency( }; } +/** + * 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. */ @@ -3004,7 +3014,7 @@ export async function runImport( let gateCompleteness: ImportPlanCompletenessReceipts | undefined; let gateIdempotency: ImportIdempotencyReceipts | undefined; let gatePlanByNumber: Map | undefined; - if (opts.gate) { + if (opts.gate || opts.atomic) { const planned: PreparedGithubImport[] = []; const skippedNumbers: number[] = []; for (const issue of filtered) { @@ -3022,43 +3032,38 @@ export async function runImport( } 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; - console.error( - `pm github gate: plan verified — ${gateCompleteness.planned} planned, ${gateCompleteness.skipped} skipped, ` + - `${gateIdempotency.matched_by_provenance} matched by provenance, ${gateIdempotency.new_entries} new.`, + if (opts.gate && !opts.dryRun) { + const preview = runTrackerGate({ 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.comments }, + })) }); + 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 ? [...gatePlan] : []; - if (!gatePlan) { - for (const issue of filtered) { - const entry = await prepareGithubImport( - issue, - repo, - opts, - token, - existing.get(`${repo.toLowerCase()}#${issue.number}`), - dependencies.fetchIssueComments, - ); - if (!entry) { - skipped++; - continue; - } - prepared.push(entry); - } - } + const prepared: PreparedGithubImport[] = [...gatePlan!]; if (prepared.length === 0) { if (opts.dryRun) { @@ -3100,11 +3105,12 @@ export async function runImport( wouldSkip: skipped, atomic: true, ...(opts.linkDeps ? { wouldLinkDependencyCandidates: countDependencyRefCandidates(repo, filtered) } : {}), - ...(opts.gate ? { gate: { completeness: gateCompleteness, idempotency: gateIdempotency, scan: "post-write" } } : {}), + ...gatePlanFields, }; } const result = await (dependencies.commitAtomic ?? importGithubAtomic)(pmRoot, repo, prepared); + if (opts.gate) verifyImportedProvenance((dependencies.readItems ?? readPmItems)(pmRoot), prepared, repo); for (const entry of prepared) { if (!entry.syncAnnotations) continue; const itemId = result.itemIds.get(entry.issueNumber); @@ -3325,7 +3331,7 @@ export async function runImport( wouldSkip: skipped, ...(opts.atomic ? { atomic: true } : {}), ...(opts.linkDeps ? { wouldLinkDependencyCandidates: countDependencyRefCandidates(repo, filtered) } : {}), - ...(opts.gate ? { gate: { completeness: gateCompleteness, idempotency: gateIdempotency, scan: "post-write" } } : {}), + ...gatePlanFields, }; } @@ -3339,6 +3345,7 @@ export async function runImport( reportDepLink(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 } : {}) }; } @@ -3404,6 +3411,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[] { @@ -3461,15 +3482,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(", ")})` : ""; @@ -3791,15 +3805,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; @@ -3824,7 +3831,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)).` @@ -4649,6 +4656,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 -------------------------------------------------------- /** @@ -4661,13 +4684,7 @@ async function runProjectList(ctx: CommandHandlerContext) { */ 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 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 = ` @@ -4709,13 +4726,7 @@ async function runProjectFields(ctx: CommandHandlerContext) { */ 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 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")); @@ -4950,13 +4961,7 @@ interface PullPlanEntryLike { */ 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 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"); diff --git a/projects.ts b/projects.ts index 23b81510..aee5f8ac 100644 --- a/projects.ts +++ b/projects.ts @@ -213,12 +213,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/test/gate.test.ts b/test/gate.test.ts index cd26195d..9610914a 100644 --- a/test/gate.test.ts +++ b/test/gate.test.ts @@ -98,6 +98,40 @@ test("a present but unreadable optional allowlist fails without echoing its erro }), 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); diff --git a/test/import-gate.test.ts b/test/import-gate.test.ts index 0ed4c610..472cf35d 100644 --- a/test/import-gate.test.ts +++ b/test/import-gate.test.ts @@ -12,7 +12,7 @@ import assert from "node:assert/strict"; import test from "node:test"; -import { spawnSync } from "node:child_process"; +import { spawn, spawnSync } from "node:child_process"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; @@ -26,6 +26,7 @@ import extension, { runImport, verifyImportPlanCompleteness, verifyImportIdempotency, + verifyImportedProvenance, type GhComment, type GhIssue, type ImportGateReceipt, @@ -109,6 +110,28 @@ function planEntry(issueNumber: number, tags: string[], match?: PmItem): Prepare }; } +test("recovered journals cannot substitute for a complete persisted provenance corpus", () => { + const prepared = [planEntry(1, ["gh:acme/widgets#1"])]; + assert.throws(() => verifyImportedProvenance([], prepared, "acme/widgets"), CommandError); + assert.equal(verifyImportedProvenance([{ id: "item-a", tags: ["gh:acme/widgets#1"] }], prepared, "acme/widgets"), 1); + assert.throws(() => verifyImportedProvenance([ + { id: "item-a", tags: ["gh:acme/widgets#1"] }, { id: "item-b", tags: ["gh:acme/widgets#1"] }, + ], prepared, "acme/widgets"), CommandError); +}); + +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 }); } +}); + function githubHandler( issues: readonly GhIssue[], commentsByNumber: ReadonlyMap = new Map(), @@ -386,16 +409,11 @@ test("adversarial: token in a body, token in a comment, email, and host path pre // holds exactly the baseline ref it started with. assert.deepStrictEqual(remoteRefs(bare), ["refs/heads/main"]); - // The worktree still holds the uncommitted gated failure, but a retried - // gate command alone reproduces the fail verdict without re-importing. - await assert.rejects( - ext.runCommand({ command: "github gate", global: { json: true }, pmRoot: root }), - (err: unknown) => { - assert.match((err as Error).message, /FAIL/); - assert.match((err as Error).message, /github-token-classic/); - return true; - }, - ); + // 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"]).status, 0); } finally { @@ -563,7 +581,7 @@ test("the gated import runs after validate/plan and before health, commit, and p 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 add -- \.agents\/pm ':\(exclude\)\.agents\/pm\/extensions'/); + 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", () => { @@ -589,4 +607,135 @@ test("the caller example pins the reusable workflow and the extension 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"); -}); \ No newline at end of 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 = ""; + child.stdout.on("data", chunk => { stdout += String(chunk); }); + child.stderr.on("data", chunk => { stderr += String(chunk); }); + child.on("error", reject); + child.on("close", code => resolve({ code, stdout, stderr })); + }); +} + +/** 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" == "--version" ]]; then printf 'gh fixture\\n'; exit 0; fi +case "$1 $2" in + "pr list") printf '%s' "\${EXISTING_PR:-}" ;; + "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' + ;; + *) exit 1 ;; +esac +`, { mode: 0o755 }); + const env = { ...process.env, PATH: tools + 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 executeWorkflow(`npm pack --ignore-scripts --json --pack-destination '${base.replace(/'/g, "'\\''")}'`, packageRoot, 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("the executable reusable workflow never publishes each adversarial fixture", async () => { + const cases = [ + { body: GH_TOKEN, comment: "" }, + { body: "clean", comment: GH_TOKEN }, + { body: PERSONAL_EMAIL, comment: "" }, + { body: HOME_PATH, comment: "" }, + ]; + 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.stdout.includes(GH_TOKEN) && !result.stderr.includes(GH_TOKEN)); + assert.deepEqual(remoteRefs(bare), ["refs/heads/main"]); + 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()); + assert.match(fs.readFileSync(env.REVIEW_RECEIPT!, "utf8"), /https:\/\/github\.com\/acme\/widgets\/blob\/main\/\.agents\/pm\/issues\/[^)]+\.toon/); + 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, EXISTING_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 }); } +}); From b3d7e3ab0b535f190092e012cdd5040270fed3e8 Mon Sep 17 00:00:00 2001 From: SteveBot <1153461+unbraind@users.noreply.github.com> Date: Mon, 5 Oct 2026 09:52:55 +0200 Subject: [PATCH 05/60] Enforce complete source inventory and exact quality thresholds --- .agents/pm/history/pm-github-9cjx.jsonl | 5 + .agents/pm/tasks/pm-github-9cjx.toon | 33 +- .github/workflows/ci.yml | 11 +- eslint.config.js | 4 + package-lock.json | 3074 +++++++++++++++++++---- package.json | 22 +- scripts/coverage-gate.ts | 411 +-- scripts/docstring-gate.ts | 48 +- scripts/duplication-gate.ts | 59 + test/atomic.test.ts | 3 +- test/comments-sync.test.ts | 5 +- test/coverage-gate.test.ts | 88 + test/duplication-gate.test.ts | 51 + test/helpers/mock-github-server.ts | 2 +- test/http-boundary.test.ts | 12 +- test/lint-policy.test.ts | 17 + test/privacy-gate.test.ts | 2 +- test/smoke.test.ts | 6 +- tsconfig.json | 3 +- tsconfig.test.json | 3 +- 20 files changed, 2989 insertions(+), 870 deletions(-) create mode 100644 eslint.config.js create mode 100644 scripts/duplication-gate.ts create mode 100644 test/coverage-gate.test.ts create mode 100644 test/duplication-gate.test.ts create mode 100644 test/lint-policy.test.ts diff --git a/.agents/pm/history/pm-github-9cjx.jsonl b/.agents/pm/history/pm-github-9cjx.jsonl index 89bb3288..990c6fd3 100644 --- a/.agents/pm/history/pm-github-9cjx.jsonl +++ b/.agents/pm/history/pm-github-9cjx.jsonl @@ -2,3 +2,8 @@ {"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"} diff --git a/.agents/pm/tasks/pm-github-9cjx.toon b/.agents/pm/tasks/pm-github-9cjx.toon index fbfbf58b..cfe24835 100644 --- a/.agents/pm/tasks/pm-github-9cjx.toon +++ b/.agents/pm/tasks/pm-github-9cjx.toon @@ -2,11 +2,13 @@ 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." type: Task -status: open +status: in_progress 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-05T07:52:16.974Z" +assignee: codex-sol +claim_principal: codex-sol 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." @@ -14,12 +16,31 @@ expected_result: "Every executable TypeScript source and operational script is a actual_result: The source walker skips scripts and aggregate coverage remains 91.83/82.14/91.75 with thresholds fixed at 88/79/89. 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[1]{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." +files[8]{path,scope}: + eslint.config.js,project package.json,project + scripts/coverage-gate.ts,project + scripts/duplication-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. + test/coverage-gate.test.ts,project + test/duplication-gate.test.ts,project + test/lint-policy.test.ts,project +tests[2]: + - 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. + - 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 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 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0f1f07c2..634c1ec2 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: bun test test/gate.test.ts test/import-gate.test.ts 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/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..dfd1593a 100644 --- a/package.json +++ b/package.json @@ -32,20 +32,27 @@ "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: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" }, "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 +112,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/scripts/coverage-gate.ts b/scripts/coverage-gate.ts index 365dfd1c..88383e27 100644 --- a/scripts/coverage-gate.ts +++ b/scripts/coverage-gate.ts @@ -1,346 +1,119 @@ /** - * 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, join, relative, resolve, sep } from "node:path"; +import { isMainInvocation } 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. + * Inventory all executable source modules under a configured location. * - * 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. - * - * 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 (repoRelative.startsWith("..") || repoRelative === "") { + if (repoRelative !== "") throw new Error("Coverage source escapes the repository."); } - const parsed = JSON.parse(shown.stdout) as TsConfig; - return { - outDir: parsed.compilerOptions?.outDir ?? "dist", - rootDir: parsed.compilerOptions?.rootDir ?? ".", - }; + 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("/")]; + } + 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. + * Run the strict all-source gate and return its exit status. * - * 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. + * @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. */ -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. - * - * @param target - Absolute path to a source file or directory. - * @returns Repository-relative POSIX paths, in directory order. - */ -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 = process.versions.bun ? "node" : process.execPath; + 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. - * - * 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. - * - * `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. - */ -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); } -console.log(`\ncoverage-gate: ${required.length} source file(s) reported, thresholds met.`); +if (isMainInvocation(process.argv, import.meta.url)) process.exitCode = runCoverageGate(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..d7d73bef --- /dev/null +++ b/scripts/duplication-gate.ts @@ -0,0 +1,59 @@ +/** + * 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 } 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(process.versions.bun ? "node" : process.execPath, [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; + } +} + +if (isMainInvocation(process.argv, import.meta.url)) process.exitCode = runDuplicationGate(resolve(import.meta.dirname, "..")); 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/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-gate.test.ts b/test/coverage-gate.test.ts new file mode 100644 index 00000000..e3096716 --- /dev/null +++ b/test/coverage-gate.test.ts @@ -0,0 +1,88 @@ +/** 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 { collectCoverageSources, runCoverageGate, type CoverageRunner } from "../scripts/coverage-gate.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("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 }); } +}); diff --git a/test/duplication-gate.test.ts b/test/duplication-gate.test.ts new file mode 100644 index 00000000..fcb186cb --- /dev/null +++ b/test/duplication-gate.test.ts @@ -0,0 +1,51 @@ +/** 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 { runDuplicationGate } 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 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/helpers/mock-github-server.ts b/test/helpers/mock-github-server.ts index 072b5685..0e8a3b5c 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()); }), }); }); }); diff --git a/test/http-boundary.test.ts b/test/http-boundary.test.ts index fbe18036..753c6355 100644 --- a/test/http-boundary.test.ts +++ b/test/http-boundary.test.ts @@ -34,6 +34,8 @@ import { githubApiBase, parseImportOptions, runImport, + type GhIssue, + type GhComment, } from "../index.ts"; import { @@ -51,7 +53,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 +68,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 +357,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 +378,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, @@ -397,7 +399,7 @@ 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, 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/privacy-gate.test.ts b/test/privacy-gate.test.ts index 64c8c16b..628bb8bf 100644 --- a/test/privacy-gate.test.ts +++ b/test/privacy-gate.test.ts @@ -29,6 +29,7 @@ import { gitBlobOid, type PrivacyGateResult, runGate, + main, scanBlob, } from "../scripts/privacy-gate.ts"; @@ -238,7 +239,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 = ""; diff --git a/test/smoke.test.ts b/test/smoke.test.ts index e56dbc90..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, @@ -441,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"] }], ]); @@ -990,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/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, From a5be720b930b3309df9686e62fff54fa8782037e Mon Sep 17 00:00:00 2001 From: SteveBot <1153461+unbraind@users.noreply.github.com> Date: Mon, 5 Oct 2026 10:12:39 +0200 Subject: [PATCH 06/60] Finish candidate receipts and keep pending changelog work explicit --- .agents/pm/features/pm-github-f1at.toon | 13 ++++-- .agents/pm/history/pm-github-9cjx.jsonl | 2 + .agents/pm/history/pm-github-f1at.jsonl | 5 +++ .agents/pm/tasks/pm-github-9cjx.toon | 21 +++------- .github/workflows/ci.yml | 2 +- .github/workflows/pm-github-sync.yml | 6 ++- CHANGELOG.md | 11 +++-- README.md | 2 + index.ts | 5 ++- package.json | 2 +- scripts/generate-changelog.ts | 39 ++++++++++++++++++ test/gate.test.ts | 32 ++++++++++++++ test/generate-changelog.test.ts | 55 +++++++++++++++++++++++++ test/helpers/mock-github-server.ts | 4 ++ test/import-gate.test.ts | 23 ++++++++++- 15 files changed, 191 insertions(+), 31 deletions(-) create mode 100644 scripts/generate-changelog.ts create mode 100644 test/generate-changelog.test.ts diff --git a/.agents/pm/features/pm-github-f1at.toon b/.agents/pm/features/pm-github-f1at.toon index 7118a4f8..9b556278 100644 --- a/.agents/pm/features/pm-github-f1at.toon +++ b/.agents/pm/features/pm-github-f1at.toon @@ -4,14 +4,14 @@ description: "Automated fleet-wide sync is disabled because the pilot workflow p type: Feature status: in_progress priority: 1 -tags[4]: "area:ci","area:gates","area:import","area:privacy" +tags[5]: "area:ci","area:gates","area:import","area:privacy",changelog-unreleased created_at: "2026-10-05T06:09:58.912Z" -updated_at: "2026-10-05T07:52:13.198Z" +updated_at: "2026-10-05T08:12:38.561Z" assignee: codex-sol claim_principal: codex-sol 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. -comments[7]{created_at,author,text}: +comments[10]{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." @@ -19,12 +19,17 @@ comments[7]{created_at,author,text}: "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." -files[6]{path,scope}: + "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." +files[8]{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.test.ts,project + test/generate-changelog.test.ts,project test/import-gate.test.ts,project tests[1]{command,scope,timeout_seconds,provenance{author,created_at,source_kind,source_ref}}: node --test test/gate.test.ts test/import-gate.test.ts,project,600,codex-sol,"2026-10-05T07:43:22.444Z",local_mutation,feat/import-privacy-gate diff --git a/.agents/pm/history/pm-github-9cjx.jsonl b/.agents/pm/history/pm-github-9cjx.jsonl index 990c6fd3..ca247325 100644 --- a/.agents/pm/history/pm-github-9cjx.jsonl +++ b/.agents/pm/history/pm-github-9cjx.jsonl @@ -7,3 +7,5 @@ {"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"} diff --git a/.agents/pm/history/pm-github-f1at.jsonl b/.agents/pm/history/pm-github-f1at.jsonl index 89c9c4d7..0713f6aa 100644 --- a/.agents/pm/history/pm-github-f1at.jsonl +++ b/.agents/pm/history/pm-github-f1at.jsonl @@ -17,3 +17,8 @@ {"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"} diff --git a/.agents/pm/tasks/pm-github-9cjx.toon b/.agents/pm/tasks/pm-github-9cjx.toon index cfe24835..22dd63de 100644 --- a/.agents/pm/tasks/pm-github-9cjx.toon +++ b/.agents/pm/tasks/pm-github-9cjx.toon @@ -6,7 +6,7 @@ status: in_progress priority: 1 tags[5]: all-source,coverage,quality-gate,scripts,tdd created_at: "2026-08-17T07:32:29.666Z" -updated_at: "2026-10-05T07:52:16.974Z" +updated_at: "2026-10-05T08:09:10.457Z" assignee: codex-sol claim_principal: codex-sol author: codex @@ -16,8 +16,9 @@ expected_result: "Every executable TypeScript source and operational script is a actual_result: The source walker skips scripts and aggregate coverage remains 91.83/82.14/91.75 with thresholds fixed at 88/79/89. 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 -comments[1]{created_at,author,text}: +comments[2]{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." files[8]{path,scope}: eslint.config.js,project package.json,project @@ -27,20 +28,8 @@ files[8]{path,scope}: test/coverage-gate.test.ts,project test/duplication-gate.test.ts,project test/lint-policy.test.ts,project -tests[2]: - - 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. - - 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 +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 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 634c1ec2..99c4b167 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -101,4 +101,4 @@ jobs: run: bun install --no-save - name: Verify native Bun extension behavior - run: bun test test/gate.test.ts test/import-gate.test.ts + run: bun test --timeout 600000 test/gate.test.ts test/import-gate.test.ts diff --git a/.github/workflows/pm-github-sync.yml b/.github/workflows/pm-github-sync.yml index 16d72c28..4f1ec0b3 100644 --- a/.github/workflows/pm-github-sync.yml +++ b/.github/workflows/pm-github-sync.yml @@ -96,7 +96,11 @@ jobs: # The calling repo's own devDependency provides the pm CLI (npm ci). - name: Install dependencies - run: npm ci + shell: bash + run: | + set -euo pipefail + npm ci + printf '%s/node_modules/.bin\n' "${GITHUB_WORKSPACE}" >> "${GITHUB_PATH}" - name: Install pinned pm-github extension shell: bash diff --git a/CHANGELOG.md b/CHANGELOG.md index ff88ddc6..307f03ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## 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)) + ### Other - 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)) @@ -102,16 +106,11 @@ - Refuse incomplete pm item corpora before GitHub imports, exports, and syncs ([pm-github-ep0u](https://github.com/unbraind/pm-github/blob/main/.agents/pm/issues/pm-github-ep0u.toon)) -## 2026.8.17 - 2026-08-17 - -### Fixed - -- The manifest declared a pm CLI floor of 2026.7.28 while peerDependencies required 2026.8.3, so the CLI enforced a weaker minimum than npm ([pm-github-7d1h](https://github.com/unbraind/pm-github/blob/main/.agents/pm/issues/pm-github-7d1h.toon)) - ## 2026.8.16 - 2026-08-16 ### Fixed +- The manifest declared a pm CLI floor of 2026.7.28 while peerDependencies required 2026.8.3, so the CLI enforced a weaker minimum than npm ([pm-github-7d1h](https://github.com/unbraind/pm-github/blob/main/.agents/pm/issues/pm-github-7d1h.toon)) - A github command can silently lose its preflight credential gate when the override scope drifts from the mutating command set ([pm-github-4ga9](https://github.com/unbraind/pm-github/blob/main/.agents/pm/issues/pm-github-4ga9.toon)) ## 2026.8.15 - 2026-08-15 diff --git a/README.md b/README.md index bbfdb451..fbcaebcd 100644 --- a/README.md +++ b/README.md @@ -344,6 +344,8 @@ MIT 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 This repo tracks its project management in `.agents/pm/` and ships a committed `.gitattributes` diff --git a/index.ts b/index.ts index 8e13a826..9dd1c043 100644 --- a/index.ts +++ b/index.ts @@ -3047,7 +3047,10 @@ export async function runImport( const preview = runTrackerGate({ 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.comments }, + 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" })); } diff --git a/package.json b/package.json index dfd1593a..b5ee9ff8 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "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 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", diff --git a/scripts/generate-changelog.ts b/scripts/generate-changelog.ts new file mode 100644 index 00000000..9e759088 --- /dev/null +++ b/scripts/generate-changelog.ts @@ -0,0 +1,39 @@ +/** Generate canonical PM release history plus explicitly tagged pending work. */ +import { spawnSync } 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"; + +/** + * 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. + * @returns The generator's status, or a failure when the complete read fails. + */ +export async function generateChangelog(root: string, args: readonly string[]): 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 = spawnSync("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, + }); + if (result.status === null) 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; + } +} + +if (isMainInvocation(process.argv, import.meta.url)) process.exitCode = await generateChangelog(resolve(import.meta.dirname, ".."), process.argv.slice(2)); diff --git a/test/gate.test.ts b/test/gate.test.ts index 9610914a..31f3c56f 100644 --- a/test/gate.test.ts +++ b/test/gate.test.ts @@ -53,6 +53,38 @@ test("staged content is scanned even when the working copy restores clean text", } }); +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 { diff --git a/test/generate-changelog.test.ts b/test/generate-changelog.test.ts new file mode 100644 index 00000000..01d6a1ae --- /dev/null +++ b/test/generate-changelog.test.ts @@ -0,0 +1,55 @@ +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 { generateChangelog } from "../scripts/generate-changelog.ts"; + +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; + process.env.PATH = ""; + try { + assert.equal(await generateChangelog(root, []), 1); + } finally { + 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/helpers/mock-github-server.ts b/test/helpers/mock-github-server.ts index 0e8a3b5c..715f3cf9 100644 --- a/test/helpers/mock-github-server.ts +++ b/test/helpers/mock-github-server.ts @@ -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/import-gate.test.ts b/test/import-gate.test.ts index 472cf35d..324c0c3f 100644 --- a/test/import-gate.test.ts +++ b/test/import-gate.test.ts @@ -119,6 +119,27 @@ test("recovered journals cannot substitute for a complete persisted provenance c ], 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 { @@ -658,7 +679,7 @@ case "$1 $2" in *) exit 1 ;; esac `, { mode: 0o755 }); - const env = { ...process.env, PATH: tools + path.delimiter + process.env.PATH, + 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, From f2df3a57de2d3497e3994ef61bab275721161312 Mon Sep 17 00:00:00 2001 From: SteveBot <1153461+unbraind@users.noreply.github.com> Date: Mon, 5 Oct 2026 10:16:55 +0200 Subject: [PATCH 07/60] Redact every gated preview and test the declared lifecycle runtime --- .agents/pm/features/pm-github-f1at.toon | 5 +++-- .agents/pm/history/pm-github-f1at.jsonl | 1 + index.ts | 4 ++-- test/import-gate.test.ts | 20 ++++++++++++++++++++ test/prepare-merge-driver.test.ts | 8 ++++++-- 5 files changed, 32 insertions(+), 6 deletions(-) diff --git a/.agents/pm/features/pm-github-f1at.toon b/.agents/pm/features/pm-github-f1at.toon index 9b556278..8abfc60c 100644 --- a/.agents/pm/features/pm-github-f1at.toon +++ b/.agents/pm/features/pm-github-f1at.toon @@ -6,12 +6,12 @@ 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-05T08:12:38.561Z" +updated_at: "2026-10-05T08:16:55.234Z" assignee: codex-sol claim_principal: codex-sol 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. -comments[10]{created_at,author,text}: +comments[11]{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." @@ -22,6 +22,7 @@ comments[10]{created_at,author,text}: "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." files[8]{path,scope}: .github/workflows/pm-github-sync.yml,project docs/sync-workflow-caller.yml,project diff --git a/.agents/pm/history/pm-github-f1at.jsonl b/.agents/pm/history/pm-github-f1at.jsonl index 0713f6aa..97a18ca0 100644 --- a/.agents/pm/history/pm-github-f1at.jsonl +++ b/.agents/pm/history/pm-github-f1at.jsonl @@ -22,3 +22,4 @@ {"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"} diff --git a/index.ts b/index.ts index 9dd1c043..33102490 100644 --- a/index.ts +++ b/index.ts @@ -3212,8 +3212,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; diff --git a/test/import-gate.test.ts b/test/import-gate.test.ts index 324c0c3f..e894a820 100644 --- a/test/import-gate.test.ts +++ b/test/import-gate.test.ts @@ -237,6 +237,26 @@ test("parseImportOptions reads the gate flag without changing defaults", () => { 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", " ")]; 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 }, From 1dad9ad76490c392238e3be1d4747b3e55b7f611 Mon Sep 17 00:00:00 2001 From: SteveBot <1153461+unbraind@users.noreply.github.com> Date: Mon, 5 Oct 2026 10:20:49 +0200 Subject: [PATCH 08/60] Preserve scoped subprocess environments under native Bun --- .agents/pm/features/pm-github-f1at.toon | 5 +++-- .agents/pm/history/pm-github-f1at.jsonl | 1 + .github/workflows/ci.yml | 2 +- index.ts | 8 ++++---- package.json | 3 ++- test/handler-failures.test.ts | 4 +++- 6 files changed, 14 insertions(+), 9 deletions(-) diff --git a/.agents/pm/features/pm-github-f1at.toon b/.agents/pm/features/pm-github-f1at.toon index 8abfc60c..ab5d5817 100644 --- a/.agents/pm/features/pm-github-f1at.toon +++ b/.agents/pm/features/pm-github-f1at.toon @@ -6,12 +6,12 @@ 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-05T08:16:55.234Z" +updated_at: "2026-10-05T08:20:15.079Z" assignee: codex-sol claim_principal: codex-sol 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. -comments[11]{created_at,author,text}: +comments[12]{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." @@ -23,6 +23,7 @@ comments[11]{created_at,author,text}: "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." files[8]{path,scope}: .github/workflows/pm-github-sync.yml,project docs/sync-workflow-caller.yml,project diff --git a/.agents/pm/history/pm-github-f1at.jsonl b/.agents/pm/history/pm-github-f1at.jsonl index 97a18ca0..a0655ef2 100644 --- a/.agents/pm/history/pm-github-f1at.jsonl +++ b/.agents/pm/history/pm-github-f1at.jsonl @@ -23,3 +23,4 @@ {"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"} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 99c4b167..2b464dc6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -101,4 +101,4 @@ jobs: run: bun install --no-save - name: Verify native Bun extension behavior - run: bun test --timeout 600000 test/gate.test.ts test/import-gate.test.ts + run: npm run test:bun diff --git a/index.ts b/index.ts index 33102490..eef9c48c 100644 --- a/index.ts +++ b/index.ts @@ -264,7 +264,7 @@ 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" }); + 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; @@ -1130,7 +1130,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. @@ -2310,7 +2310,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 || "" }; } @@ -4036,7 +4036,7 @@ 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" }); + const r = spawnSync("gh", ["--version"], { encoding: "utf-8", env: { ...process.env } }); return r.status === 0; } catch { return false; diff --git a/package.json b/package.json index b5ee9ff8..d4be09ab 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,8 @@ "coverage": "npm run build && npm run build:test && node scripts/coverage-gate.ts", "privacy": "node scripts/privacy-gate.ts", "lint": "eslint .", - "duplication": "node scripts/duplication-gate.ts" + "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" 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 { From b3bd514472dc87aba8087aaa1d4286c0b7bf2295 Mon Sep 17 00:00:00 2001 From: SteveBot <1153461+unbraind@users.noreply.github.com> Date: Mon, 5 Oct 2026 10:28:56 +0200 Subject: [PATCH 09/60] Verify unavailable-tool failures under both runtimes --- .agents/pm/features/pm-github-f1at.toon | 5 +++-- .agents/pm/history/pm-github-f1at.jsonl | 1 + scripts/generate-changelog.ts | 2 +- test/generate-changelog.test.ts | 4 +++- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/.agents/pm/features/pm-github-f1at.toon b/.agents/pm/features/pm-github-f1at.toon index ab5d5817..dedf7a06 100644 --- a/.agents/pm/features/pm-github-f1at.toon +++ b/.agents/pm/features/pm-github-f1at.toon @@ -6,12 +6,12 @@ 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-05T08:20:15.079Z" +updated_at: "2026-10-05T08:28:56.086Z" assignee: codex-sol claim_principal: codex-sol 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. -comments[12]{created_at,author,text}: +comments[13]{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." @@ -24,6 +24,7 @@ comments[12]{created_at,author,text}: "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." files[8]{path,scope}: .github/workflows/pm-github-sync.yml,project docs/sync-workflow-caller.yml,project diff --git a/.agents/pm/history/pm-github-f1at.jsonl b/.agents/pm/history/pm-github-f1at.jsonl index a0655ef2..68d59608 100644 --- a/.agents/pm/history/pm-github-f1at.jsonl +++ b/.agents/pm/history/pm-github-f1at.jsonl @@ -24,3 +24,4 @@ {"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"} diff --git a/scripts/generate-changelog.ts b/scripts/generate-changelog.ts index 9e759088..469191b2 100644 --- a/scripts/generate-changelog.ts +++ b/scripts/generate-changelog.ts @@ -24,7 +24,7 @@ export async function generateChangelog(root: string, args: readonly string[]): "--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, + cwd: root, input: JSON.stringify({ items }), encoding: "utf8", maxBuffer: 64 * 1024 * 1024, env: { ...process.env }, }); if (result.status === null) return 1; process.stdout.write(result.stdout); diff --git a/test/generate-changelog.test.ts b/test/generate-changelog.test.ts index 01d6a1ae..284c6326 100644 --- a/test/generate-changelog.test.ts +++ b/test/generate-changelog.test.ts @@ -29,7 +29,9 @@ test("canonical changelog includes tagged pending work without closing it or adm fs.appendFileSync(path.join(root, "CHANGELOG.md"), "\nDrift\n"); assert.equal(await generateChangelog(root, ["--check", "--no-check-diff"]), 1); const previousPath = process.env.PATH; - process.env.PATH = ""; + const emptyBin = path.join(root, "empty-bin"); + fs.mkdirSync(emptyBin); + process.env.PATH = emptyBin; try { assert.equal(await generateChangelog(root, []), 1); } finally { From 305edab2ddf6b92a4f498991176a5d7e96ea4dd2 Mon Sep 17 00:00:00 2001 From: SteveBot <1153461+unbraind@users.noreply.github.com> Date: Mon, 5 Oct 2026 10:29:32 +0200 Subject: [PATCH 10/60] Track the native CLI acceptance boundary and complete Bun suite --- .agents/pm/features/pm-github-f1at.toon | 9 ++++++--- .agents/pm/history/pm-github-f1at.jsonl | 2 ++ .agents/pm/history/pm-github-hptv.jsonl | 1 + .agents/pm/issues/pm-github-hptv.toon | 14 ++++++++++++++ 4 files changed, 23 insertions(+), 3 deletions(-) create mode 100644 .agents/pm/history/pm-github-hptv.jsonl create mode 100644 .agents/pm/issues/pm-github-hptv.toon diff --git a/.agents/pm/features/pm-github-f1at.toon b/.agents/pm/features/pm-github-f1at.toon index dedf7a06..deffdfcf 100644 --- a/.agents/pm/features/pm-github-f1at.toon +++ b/.agents/pm/features/pm-github-f1at.toon @@ -6,7 +6,7 @@ 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-05T08:28:56.086Z" +updated_at: "2026-10-05T08:29:31.898Z" assignee: codex-sol claim_principal: codex-sol author: pi-glm @@ -25,7 +25,7 @@ comments[13]{created_at,author,text}: "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." -files[8]{path,scope}: +files[10]{path,scope}: .github/workflows/pm-github-sync.yml,project docs/sync-workflow-caller.yml,project gate.ts,project @@ -33,9 +33,12 @@ files[8]{path,scope}: scripts/generate-changelog.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 -tests[1]{command,scope,timeout_seconds,provenance{author,created_at,source_kind,source_ref}}: + test/prepare-merge-driver.test.ts,project +tests[2]{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 docs[2]{path,scope}: docs/sync-workflow-caller.yml,project README.md,project diff --git a/.agents/pm/history/pm-github-f1at.jsonl b/.agents/pm/history/pm-github-f1at.jsonl index 68d59608..53a0e398 100644 --- a/.agents/pm/history/pm-github-f1at.jsonl +++ b/.agents/pm/history/pm-github-f1at.jsonl @@ -25,3 +25,5 @@ {"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"} diff --git a/.agents/pm/history/pm-github-hptv.jsonl b/.agents/pm/history/pm-github-hptv.jsonl new file mode 100644 index 00000000..09b5667a --- /dev/null +++ b/.agents/pm/history/pm-github-hptv.jsonl @@ -0,0 +1 @@ +{"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"} diff --git a/.agents/pm/issues/pm-github-hptv.toon b/.agents/pm/issues/pm-github-hptv.toon new file mode 100644 index 00000000..efbbd5e3 --- /dev/null +++ b/.agents/pm/issues/pm-github-hptv.toon @@ -0,0 +1,14 @@ +id: pm-github-hptv +title: Bound native Bun CLI repeats of a completed public atomic import +description: "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." +type: Issue +status: open +priority: 1 +tags: [] +created_at: "2026-10-05T08:28:54.625Z" +updated_at: "2026-10-05T08:28:54.625Z" +author: codex-sol +files[2]{path,scope}: + index.ts,project + test/import-gate.test.ts,project +body: "" From 68fd8301936819e02c36e119ed71accf4be30123 Mon Sep 17 00:00:00 2001 From: SteveBot <1153461+unbraind@users.noreply.github.com> Date: Mon, 5 Oct 2026 10:35:04 +0200 Subject: [PATCH 11/60] Cover webhook and service-account credential signatures --- .agents/pm/features/pm-github-f1at.toon | 5 +++-- .agents/pm/history/pm-github-f1at.jsonl | 1 + gate.ts | 2 ++ test/gate.test.ts | 2 ++ 4 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.agents/pm/features/pm-github-f1at.toon b/.agents/pm/features/pm-github-f1at.toon index deffdfcf..95c15701 100644 --- a/.agents/pm/features/pm-github-f1at.toon +++ b/.agents/pm/features/pm-github-f1at.toon @@ -6,12 +6,12 @@ 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-05T08:29:31.898Z" +updated_at: "2026-10-05T08:33:17.932Z" assignee: codex-sol claim_principal: codex-sol 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. -comments[13]{created_at,author,text}: +comments[14]{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." @@ -25,6 +25,7 @@ comments[13]{created_at,author,text}: "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." files[10]{path,scope}: .github/workflows/pm-github-sync.yml,project docs/sync-workflow-caller.yml,project diff --git a/.agents/pm/history/pm-github-f1at.jsonl b/.agents/pm/history/pm-github-f1at.jsonl index 53a0e398..ecdd25c3 100644 --- a/.agents/pm/history/pm-github-f1at.jsonl +++ b/.agents/pm/history/pm-github-f1at.jsonl @@ -27,3 +27,4 @@ {"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"} diff --git a/gate.ts b/gate.ts index c06e80e1..8cd525bd 100644 --- a/gate.ts +++ b/gate.ts @@ -192,9 +192,11 @@ const CREDENTIAL_RULES: readonly ContentRule[] = [ { 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[abprs]-[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", diff --git a/test/gate.test.ts b/test/gate.test.ts index 31f3c56f..b6bb79a9 100644 --- a/test/gate.test.ts +++ b/test/gate.test.ts @@ -232,9 +232,11 @@ test("scanner fires on every high-confidence credential signature", () => { ["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) { From 0b7c4abd56ee9b77fcc04fd79deafe10882eb39e Mon Sep 17 00:00:00 2001 From: SteveBot <1153461+unbraind@users.noreply.github.com> Date: Mon, 5 Oct 2026 10:43:13 +0200 Subject: [PATCH 12/60] Restore deliberate fixture exit codes under native Bun --- .agents/pm/features/pm-github-f1at.toon | 11 ++++++++--- .agents/pm/history/pm-github-f1at.jsonl | 2 ++ test/docstring-gate.test.ts | 4 ++-- test/privacy-gate.test.ts | 2 +- test/verify-release-publish-attestation.test.ts | 4 ++-- 5 files changed, 15 insertions(+), 8 deletions(-) diff --git a/.agents/pm/features/pm-github-f1at.toon b/.agents/pm/features/pm-github-f1at.toon index 95c15701..2d4dfc66 100644 --- a/.agents/pm/features/pm-github-f1at.toon +++ b/.agents/pm/features/pm-github-f1at.toon @@ -2,16 +2,20 @@ 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 +status: blocked 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-05T08:33:17.932Z" +updated_at: "2026-10-05T08:43:11.660Z" assignee: codex-sol claim_principal: codex-sol 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. -comments[14]{created_at,author,text}: +blocked_by: pm-github-9cjx +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[15]{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." @@ -26,6 +30,7 @@ comments[14]{created_at,author,text}: "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." files[10]{path,scope}: .github/workflows/pm-github-sync.yml,project docs/sync-workflow-caller.yml,project diff --git a/.agents/pm/history/pm-github-f1at.jsonl b/.agents/pm/history/pm-github-f1at.jsonl index ecdd25c3..411f1b16 100644 --- a/.agents/pm/history/pm-github-f1at.jsonl +++ b/.agents/pm/history/pm-github-f1at.jsonl @@ -28,3 +28,5 @@ {"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"} 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/privacy-gate.test.ts b/test/privacy-gate.test.ts index 628bb8bf..3ae1c1b8 100644 --- a/test/privacy-gate.test.ts +++ b/test/privacy-gate.test.ts @@ -272,7 +272,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; } }); 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; } }, ); From 0b355a12b9eccf9fa521d0e81e7b507c8aebaff7 Mon Sep 17 00:00:00 2001 From: SteveBot <1153461+unbraind@users.noreply.github.com> Date: Mon, 5 Oct 2026 11:11:08 +0200 Subject: [PATCH 13/60] Record final public acceptance and the bounded native CLI retry --- .agents/pm/features/pm-github-f1at.toon | 7 +++++-- .agents/pm/history/pm-github-f1at.jsonl | 3 +++ .agents/pm/history/pm-github-hptv.jsonl | 1 + .agents/pm/issues/pm-github-hptv.toon | 4 +++- 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/.agents/pm/features/pm-github-f1at.toon b/.agents/pm/features/pm-github-f1at.toon index 2d4dfc66..73903bef 100644 --- a/.agents/pm/features/pm-github-f1at.toon +++ b/.agents/pm/features/pm-github-f1at.toon @@ -6,7 +6,7 @@ status: blocked 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-05T08:43:11.660Z" +updated_at: "2026-10-05T09:11:07.592Z" assignee: codex-sol claim_principal: codex-sol author: pi-glm @@ -15,7 +15,7 @@ blocked_by: pm-github-9cjx 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[15]{created_at,author,text}: +comments[18]{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." @@ -31,6 +31,9 @@ comments[15]{created_at,author,text}: "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." files[10]{path,scope}: .github/workflows/pm-github-sync.yml,project docs/sync-workflow-caller.yml,project diff --git a/.agents/pm/history/pm-github-f1at.jsonl b/.agents/pm/history/pm-github-f1at.jsonl index 411f1b16..ad39a705 100644 --- a/.agents/pm/history/pm-github-f1at.jsonl +++ b/.agents/pm/history/pm-github-f1at.jsonl @@ -30,3 +30,6 @@ {"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"} diff --git a/.agents/pm/history/pm-github-hptv.jsonl b/.agents/pm/history/pm-github-hptv.jsonl index 09b5667a..de6f3f55 100644 --- a/.agents/pm/history/pm-github-hptv.jsonl +++ b/.agents/pm/history/pm-github-hptv.jsonl @@ -1 +1,2 @@ {"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"} diff --git a/.agents/pm/issues/pm-github-hptv.toon b/.agents/pm/issues/pm-github-hptv.toon index efbbd5e3..ecd19fc8 100644 --- a/.agents/pm/issues/pm-github-hptv.toon +++ b/.agents/pm/issues/pm-github-hptv.toon @@ -6,8 +6,10 @@ status: open priority: 1 tags: [] created_at: "2026-10-05T08:28:54.625Z" -updated_at: "2026-10-05T08:28:54.625Z" +updated_at: "2026-10-05T09:07:19.298Z" author: codex-sol +comments[1]{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." files[2]{path,scope}: index.ts,project test/import-gate.test.ts,project From e18a90ed995dff52b4f8c740198fb27d98350e49 Mon Sep 17 00:00:00 2001 From: SteveBot <1153461+unbraind@users.noreply.github.com> Date: Mon, 5 Oct 2026 11:16:14 +0200 Subject: [PATCH 14/60] Scan proposed tracker filenames and both Windows path separators --- .agents/pm/features/pm-github-f1at.toon | 12 +++++--- .agents/pm/history/pm-github-f1at.jsonl | 4 +++ README.md | 4 +-- gate.ts | 13 ++++---- test/gate-paths.test.ts | 40 +++++++++++++++++++++++++ 5 files changed, 62 insertions(+), 11 deletions(-) create mode 100644 test/gate-paths.test.ts diff --git a/.agents/pm/features/pm-github-f1at.toon b/.agents/pm/features/pm-github-f1at.toon index 73903bef..67c9713c 100644 --- a/.agents/pm/features/pm-github-f1at.toon +++ b/.agents/pm/features/pm-github-f1at.toon @@ -6,7 +6,7 @@ status: blocked 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-05T09:11:07.592Z" +updated_at: "2026-10-05T09:16:14.202Z" assignee: codex-sol claim_principal: codex-sol author: pi-glm @@ -15,7 +15,7 @@ blocked_by: pm-github-9cjx 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[18]{created_at,author,text}: +comments[20]{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." @@ -34,20 +34,24 @@ comments[18]{created_at,author,text}: "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." -files[10]{path,scope}: + "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." +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[2]{command,scope,timeout_seconds,provenance{author,created_at,source_kind,source_ref}}: +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 diff --git a/.agents/pm/history/pm-github-f1at.jsonl b/.agents/pm/history/pm-github-f1at.jsonl index ad39a705..88b0d6c1 100644 --- a/.agents/pm/history/pm-github-f1at.jsonl +++ b/.agents/pm/history/pm-github-f1at.jsonl @@ -33,3 +33,7 @@ {"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"} diff --git a/README.md b/README.md index fbcaebcd..605fd622 100644 --- a/README.md +++ b/README.md @@ -262,11 +262,11 @@ pm github gate --diff sync.patch # scan an explicit unified diff file instea pm github gate --allowlist .pm-github-gate-allowlist.json ``` -It scans **only the proposed change** — the added lines of 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. 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: +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, OpenAI, Anthropic tokens, generic `Authorization: Bearer` values, `-----BEGIN … PRIVATE KEY-----` blocks, and high-entropy assignments to secret-named identifiers (`token = "…"`, `api_key: …`) whose value has high measured entropy; - **personal data** — email addresses other than no-reply identities (`@users.noreply.github.com`, `noreply@…`, `@noreply.…`), and phone numbers in international (`+…`) or North-American (`(555) 123-4567`, `555-123-4567`) notation; -- **host paths** — absolute local filesystem paths (POSIX home/system directories and Windows drive paths), and named home-directory references. +- **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, 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. diff --git a/gate.ts b/gate.ts index 8cd525bd..419aeb24 100644 --- a/gate.ts +++ b/gate.ts @@ -5,7 +5,7 @@ // 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 of the staged/working/untracked diff under the +// 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: // @@ -253,7 +253,7 @@ const HOST_PATH_RULES: readonly ContentRule[] = [ }, { rule: "windows-host-path", - pattern: /[A-Za-z]:\\[^\s"'`<>),;\]}]+/g, + pattern: /(?),;\]}]+/g, }, { rule: "home-username", @@ -791,7 +791,7 @@ const UNTRACKED_FILE_BYTE_CAP = 8 * 1024 * 1024; // --------------------------------------------------------------------------- /** - * Scan one change file's added lines and return raw findings. + * 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 @@ -805,9 +805,12 @@ const UNTRACKED_FILE_BYTE_CAP = 8 * 1024 * 1024; function scanChangeFile(change: ChangeFile): Array { const findings: Array = []; const seen = new Set(); - for (const line of change.addedLines) { + 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; structural?: boolean }> = []; - if (change.filePath.endsWith(".jsonl")) { + 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. diff --git a/test/gate-paths.test.ts b/test/gate-paths.test.ts new file mode 100644 index 00000000..f0ee156b --- /dev/null +++ b/test/gate-paths.test.ts @@ -0,0 +1,40 @@ +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 { 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); + } +}); From 37d4a018f4ff83a5f3a4f89431f64a7df81544fc Mon Sep 17 00:00:00 2001 From: SteveBot <1153461+unbraind@users.noreply.github.com> Date: Mon, 5 Oct 2026 11:23:00 +0200 Subject: [PATCH 15/60] Close credential-name and contact-number scanner bypasses --- .agents/pm/features/pm-github-f1at.toon | 5 +++-- .agents/pm/history/pm-github-f1at.jsonl | 1 + README.md | 4 ++-- gate.ts | 18 ++++++++++-------- test/gate-paths.test.ts | 23 ++++++++++++++++++++++- 5 files changed, 38 insertions(+), 13 deletions(-) diff --git a/.agents/pm/features/pm-github-f1at.toon b/.agents/pm/features/pm-github-f1at.toon index 67c9713c..dce95638 100644 --- a/.agents/pm/features/pm-github-f1at.toon +++ b/.agents/pm/features/pm-github-f1at.toon @@ -6,7 +6,7 @@ status: blocked 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-05T09:16:14.202Z" +updated_at: "2026-10-05T09:22:58.633Z" assignee: codex-sol claim_principal: codex-sol author: pi-glm @@ -15,7 +15,7 @@ blocked_by: pm-github-9cjx 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[20]{created_at,author,text}: +comments[21]{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." @@ -36,6 +36,7 @@ comments[20]{created_at,author,text}: "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." files[11]{path,scope}: .github/workflows/pm-github-sync.yml,project docs/sync-workflow-caller.yml,project diff --git a/.agents/pm/history/pm-github-f1at.jsonl b/.agents/pm/history/pm-github-f1at.jsonl index 88b0d6c1..c50bffaa 100644 --- a/.agents/pm/history/pm-github-f1at.jsonl +++ b/.agents/pm/history/pm-github-f1at.jsonl @@ -37,3 +37,4 @@ {"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"} diff --git a/README.md b/README.md index 605fd622..27edd2f9 100644 --- a/README.md +++ b/README.md @@ -264,8 +264,8 @@ 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, OpenAI, Anthropic tokens, generic `Authorization: Bearer` values, `-----BEGIN … PRIVATE KEY-----` blocks, and high-entropy assignments to secret-named identifiers (`token = "…"`, `api_key: …`) whose value has high measured entropy; -- **personal data** — email addresses other than no-reply identities (`@users.noreply.github.com`, `noreply@…`, `@noreply.…`), and phone numbers in international (`+…`) or North-American (`(555) 123-4567`, `555-123-4567`) notation; +- **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, 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. diff --git a/gate.ts b/gate.ts index 419aeb24..7e841b9a 100644 --- a/gate.ts +++ b/gate.ts @@ -191,7 +191,7 @@ const CREDENTIAL_RULES: readonly ContentRule[] = [ { 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[abprs]-[A-Za-z0-9-]{10,}/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 }, @@ -237,6 +237,12 @@ const PERSONAL_DATA_RULES: readonly ContentRule[] = [ // 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; }, + }, ]; /** @@ -286,10 +292,6 @@ const SECRET_IDENTIFIER_PAIRS: readonly string[] = ["api_key", "access_key", "pr 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; -/** Value shapes that are structured data, not secrets, even when long. */ -const NON_SECRET_VALUE_SHAPES = - /(?:\d{4}-\d{2}-\d{2})|(?:^\{?[0-9a-f]{8}-[0-9a-f]{4}-)/i; - /** * Shannon entropy per character of a value. * @@ -319,7 +321,7 @@ export function shannonEntropyPerChar(value: string): number { * 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 not a date/UUID shape. Both halves are required: identifier-only would + * 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. * @@ -329,8 +331,8 @@ export function shannonEntropyPerChar(value: string): number { */ export function isHighEntropySecretAssignment(identifier: string, value: string): boolean { if (value.length < 20) return false; - if (NON_SECRET_VALUE_SHAPES.test(value)) return false; - const segments = identifier.toLowerCase().split(/[_-]+/).filter(Boolean); + 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("_")); diff --git a/test/gate-paths.test.ts b/test/gate-paths.test.ts index f0ee156b..deaf8047 100644 --- a/test/gate-paths.test.ts +++ b/test/gate-paths.test.ts @@ -5,7 +5,7 @@ import os from "node:os"; import path from "node:path"; import { spawnSync } from "node:child_process"; -import { runTrackerGate, scanLineForRuleHits } from "../gate.ts"; +import { 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-")); @@ -38,3 +38,24 @@ test("Windows drive paths accept both separators while public URLs stay clean", 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); + } +}); From 0b979b4df3893259f58fdbb80b12822cbef9bc62 Mon Sep 17 00:00:00 2001 From: SteveBot <1153461+unbraind@users.noreply.github.com> Date: Mon, 5 Oct 2026 11:26:42 +0200 Subject: [PATCH 16/60] Cover Projects sync execution and planner boundaries --- .agents/pm/history/pm-github-9cjx.jsonl | 10 + .agents/pm/tasks/pm-github-9cjx.toon | 33 ++- test/helpers/node-scenario.ts | 21 ++ test/project-sync.test.ts | 291 ++++++++++++++++++++++++ test/projects.test.ts | 64 ++++++ 5 files changed, 412 insertions(+), 7 deletions(-) create mode 100644 test/helpers/node-scenario.ts create mode 100644 test/project-sync.test.ts diff --git a/.agents/pm/history/pm-github-9cjx.jsonl b/.agents/pm/history/pm-github-9cjx.jsonl index ca247325..e41c9d01 100644 --- a/.agents/pm/history/pm-github-9cjx.jsonl +++ b/.agents/pm/history/pm-github-9cjx.jsonl @@ -9,3 +9,13 @@ {"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"} diff --git a/.agents/pm/tasks/pm-github-9cjx.toon b/.agents/pm/tasks/pm-github-9cjx.toon index 22dd63de..25cf18d4 100644 --- a/.agents/pm/tasks/pm-github-9cjx.toon +++ b/.agents/pm/tasks/pm-github-9cjx.toon @@ -6,9 +6,9 @@ status: in_progress priority: 1 tags[5]: all-source,coverage,quality-gate,scripts,tdd created_at: "2026-08-17T07:32:29.666Z" -updated_at: "2026-10-05T08:09:10.457Z" -assignee: codex-sol -claim_principal: codex-sol +updated_at: "2026-10-05T09:26:42.191Z" +assignee: codex-sol-coverage +claim_principal: codex-sol-coverage 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." @@ -16,10 +16,14 @@ expected_result: "Every executable TypeScript source and operational script is a actual_result: The source walker skips scripts and aggregate coverage remains 91.83/82.14/91.75 with thresholds fixed at 88/79/89. 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 -comments[2]{created_at,author,text}: +comments[6]{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." -files[8]{path,scope}: + "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." +files[11]{path,scope}: eslint.config.js,project package.json,project scripts/coverage-gate.ts,project @@ -27,9 +31,24 @@ files[8]{path,scope}: test,project test/coverage-gate.test.ts,project test/duplication-gate.test.ts,project + test/helpers/node-scenario.ts,project test/lint-policy.test.ts,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. + test/project-sync.test.ts,project + test/projects.test.ts,project +tests[2]: + - 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. + - 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 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 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/project-sync.test.ts b/test/project-sync.test.ts new file mode 100644 index 00000000..fdca4911 --- /dev/null +++ b/test/project-sync.test.ts @@ -0,0 +1,291 @@ +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/); + }); +}); diff --git a/test/projects.test.ts b/test/projects.test.ts index d1dd6956..9cdb827e 100644 --- a/test/projects.test.ts +++ b/test/projects.test.ts @@ -6,6 +6,7 @@ import { buildProjectImportPlan, buildProjectPullPlan, buildProjectPushPlan, + decodeItemId, indexPmByIssue, indexPmByProjectItem, indexProjectItemsByIssue, @@ -47,6 +48,69 @@ 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(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); +}); + +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); From f26fb3efc34acb87e0c553164d4104484a7c62b5 Mon Sep 17 00:00:00 2001 From: SteveBot <1153461+unbraind@users.noreply.github.com> Date: Mon, 5 Oct 2026 11:31:11 +0200 Subject: [PATCH 17/60] Reject undecodable tracker inputs and expand the no-push workflow battery --- .agents/pm/features/pm-github-f1at.toon | 5 +++-- .agents/pm/history/pm-github-aiok.jsonl | 4 ++++ .agents/pm/history/pm-github-f1at.jsonl | 1 + .agents/pm/tasks/pm-github-aiok.toon | 16 ++++++++++++++++ README.md | 2 +- gate.ts | 16 ++++++++++++++-- test/gate-paths.test.ts | 24 +++++++++++++++++++++++- test/import-gate.test.ts | 22 ++++++++++++++++------ 8 files changed, 78 insertions(+), 12 deletions(-) create mode 100644 .agents/pm/history/pm-github-aiok.jsonl create mode 100644 .agents/pm/tasks/pm-github-aiok.toon diff --git a/.agents/pm/features/pm-github-f1at.toon b/.agents/pm/features/pm-github-f1at.toon index dce95638..a5f7806e 100644 --- a/.agents/pm/features/pm-github-f1at.toon +++ b/.agents/pm/features/pm-github-f1at.toon @@ -6,7 +6,7 @@ status: blocked 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-05T09:22:58.633Z" +updated_at: "2026-10-05T09:31:11.763Z" assignee: codex-sol claim_principal: codex-sol author: pi-glm @@ -15,7 +15,7 @@ blocked_by: pm-github-9cjx 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[21]{created_at,author,text}: +comments[22]{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." @@ -37,6 +37,7 @@ comments[21]{created_at,author,text}: "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." files[11]{path,scope}: .github/workflows/pm-github-sync.yml,project docs/sync-workflow-caller.yml,project diff --git a/.agents/pm/history/pm-github-aiok.jsonl b/.agents/pm/history/pm-github-aiok.jsonl new file mode 100644 index 00000000..e13f7018 --- /dev/null +++ b/.agents/pm/history/pm-github-aiok.jsonl @@ -0,0 +1,4 @@ +{"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"} diff --git a/.agents/pm/history/pm-github-f1at.jsonl b/.agents/pm/history/pm-github-f1at.jsonl index c50bffaa..995c0e1c 100644 --- a/.agents/pm/history/pm-github-f1at.jsonl +++ b/.agents/pm/history/pm-github-f1at.jsonl @@ -38,3 +38,4 @@ {"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"} diff --git a/.agents/pm/tasks/pm-github-aiok.toon b/.agents/pm/tasks/pm-github-aiok.toon new file mode 100644 index 00000000..5215f11c --- /dev/null +++ b/.agents/pm/tasks/pm-github-aiok.toon @@ -0,0 +1,16 @@ +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:26:00.035Z" +author: codex-sol +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/README.md b/README.md index 27edd2f9..55e56ff2 100644 --- a/README.md +++ b/README.md @@ -268,7 +268,7 @@ It scans **only the proposed change** — the added lines and their filenames in - **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, 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. +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: diff --git a/gate.ts b/gate.ts index 7e841b9a..f1c2784b 100644 --- a/gate.ts +++ b/gate.ts @@ -30,6 +30,7 @@ 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"; // --------------------------------------------------------------------------- @@ -587,6 +588,7 @@ function wholeFileChange( 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 = ""; @@ -638,6 +640,16 @@ export function runGitDefault(cwd: string, args: readonly string[]): { ok: boole }; } +/** + * 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. * @@ -657,7 +669,7 @@ export function collectTrackerChange( dependencies: GateRunDependencies = {}, ): ChangeFile[] { const runGit = dependencies.runGit ?? runGitDefault; - const readFileSync = dependencies.readFileSync ?? ((filePath: string) => fs.readFileSync(filePath, "utf-8")); + const readFileSync = dependencies.readFileSync ?? readTrackerText; const toplevel = runGit(pmDataDir, ["rev-parse", "--show-toplevel"]); if (!toplevel.ok || toplevel.stdout.trim() === "") { @@ -880,7 +892,7 @@ function scanChangeFile(change: ChangeFile): Array fs.readFileSync(filePath, "utf-8")); + const readFileSync = input.dependencies?.readFileSync ?? readTrackerText; const runGit = input.dependencies?.runGit ?? runGitDefault; const pmDataDir = resolvePmDataDir(input.pmRoot); diff --git a/test/gate-paths.test.ts b/test/gate-paths.test.ts index deaf8047..7bbad478 100644 --- a/test/gate-paths.test.ts +++ b/test/gate-paths.test.ts @@ -5,7 +5,7 @@ import os from "node:os"; import path from "node:path"; import { spawnSync } from "node:child_process"; -import { isHighEntropySecretAssignment, runTrackerGate, scanLineForRuleHits } from "../gate.ts"; +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-")); @@ -59,3 +59,25 @@ test("camel-case secrets, Slack app credentials and labelled plain phones are ga 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/import-gate.test.ts b/test/import-gate.test.ts index e894a820..5d631f3f 100644 --- a/test/import-gate.test.ts +++ b/test/import-gate.test.ts @@ -47,7 +47,7 @@ const SYNC_BRANCH = "automation/pm-github-sync"; 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 = "Zj9kP2mQ7xW4nB8vC5tR1sD"; +const HIGH_ENTROPY_SECRET = "Zj9kP2mQ7xW4" + "nB8vC5tR1sD"; /** * Build one GitHub issue REST payload for the mock server. @@ -721,10 +721,15 @@ const WORKFLOW_IMPORT_SEQUENCE = ["Validate GitHub access", "Preview GitHub to p test("the executable reusable workflow never publishes each adversarial fixture", async () => { const cases = [ - { body: GH_TOKEN, comment: "" }, - { body: "clean", comment: GH_TOKEN }, - { body: PERSONAL_EMAIL, comment: "" }, - { body: HOME_PATH, comment: "" }, + { 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(); @@ -742,8 +747,13 @@ test("the executable reusable workflow never publishes each adversarial fixture" 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.stdout.includes(GH_TOKEN) && !result.stderr.includes(GH_TOKEN)); + 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 }); } From 5684809279abbd5d7f02ecb6bafdc07c06bc0bcb Mon Sep 17 00:00:00 2001 From: SteveBot <1153461+unbraind@users.noreply.github.com> Date: Mon, 5 Oct 2026 11:31:52 +0200 Subject: [PATCH 18/60] Remove unreachable hex parsing catch after validation --- .agents/pm/history/pm-github-9cjx.jsonl | 4 ++++ .agents/pm/tasks/pm-github-9cjx.toon | 19 +++++++++++++++---- projects.ts | 7 ++----- test/projects.test.ts | 3 +++ 4 files changed, 24 insertions(+), 9 deletions(-) diff --git a/.agents/pm/history/pm-github-9cjx.jsonl b/.agents/pm/history/pm-github-9cjx.jsonl index e41c9d01..b8bd5e6f 100644 --- a/.agents/pm/history/pm-github-9cjx.jsonl +++ b/.agents/pm/history/pm-github-9cjx.jsonl @@ -19,3 +19,7 @@ {"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"} diff --git a/.agents/pm/tasks/pm-github-9cjx.toon b/.agents/pm/tasks/pm-github-9cjx.toon index 25cf18d4..6bd9cdbf 100644 --- a/.agents/pm/tasks/pm-github-9cjx.toon +++ b/.agents/pm/tasks/pm-github-9cjx.toon @@ -6,7 +6,7 @@ status: in_progress priority: 1 tags[5]: all-source,coverage,quality-gate,scripts,tdd created_at: "2026-08-17T07:32:29.666Z" -updated_at: "2026-10-05T09:26:42.191Z" +updated_at: "2026-10-05T09:31:52.268Z" assignee: codex-sol-coverage claim_principal: codex-sol-coverage author: codex @@ -16,16 +16,19 @@ expected_result: "Every executable TypeScript source and operational script is a actual_result: The source walker skips scripts and aggregate coverage remains 91.83/82.14/91.75 with thresholds fixed at 88/79/89. 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 -comments[6]{created_at,author,text}: +comments[8]{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." -files[11]{path,scope}: + "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." +files[12]{path,scope}: eslint.config.js,project package.json,project + projects.ts,project scripts/coverage-gate.ts,project scripts/duplication-gate.ts,project test,project @@ -35,7 +38,7 @@ files[11]{path,scope}: test/lint-policy.test.ts,project test/project-sync.test.ts,project test/projects.test.ts,project -tests[2]: +tests[3]: - command: npm run coverage path: scripts/coverage-gate.ts scope: project @@ -49,6 +52,14 @@ tests[2]: 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 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 diff --git a/projects.ts b/projects.ts index aee5f8ac..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 diff --git a/test/projects.test.ts b/test/projects.test.ts index 9cdb827e..29221efb 100644 --- a/test/projects.test.ts +++ b/test/projects.test.ts @@ -13,6 +13,7 @@ import { mapOptionNameToPmStatus, parseProjectRef, parseProjectItemTag, + parseAssignmentMap, parseStatusMap, projectItemTag, resolveOptionForStatus, @@ -50,6 +51,7 @@ 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); @@ -61,6 +63,7 @@ test("status mapping falls back only when the board did not provide an explicit 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", () => { From ae86e7dab2efde8f048a1a946373540e535de6c4 Mon Sep 17 00:00:00 2001 From: SteveBot <1153461+unbraind@users.noreply.github.com> Date: Mon, 5 Oct 2026 11:45:17 +0200 Subject: [PATCH 19/60] Preserve and verify interrupted import and operational boundary tests --- .agents/pm/history/pm-github-9cjx.jsonl | 9 ++ .agents/pm/tasks/pm-github-9cjx.toon | 26 +++- test/coverage-boundaries.test.ts | 181 ++++++++++++++++++++++++ test/gate-inputs.test.ts | 80 +++++++++++ test/import-errors.test.ts | 142 +++++++++++++++++++ 5 files changed, 432 insertions(+), 6 deletions(-) create mode 100644 test/coverage-boundaries.test.ts create mode 100644 test/gate-inputs.test.ts create mode 100644 test/import-errors.test.ts diff --git a/.agents/pm/history/pm-github-9cjx.jsonl b/.agents/pm/history/pm-github-9cjx.jsonl index b8bd5e6f..d420607a 100644 --- a/.agents/pm/history/pm-github-9cjx.jsonl +++ b/.agents/pm/history/pm-github-9cjx.jsonl @@ -23,3 +23,12 @@ {"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"} diff --git a/.agents/pm/tasks/pm-github-9cjx.toon b/.agents/pm/tasks/pm-github-9cjx.toon index 6bd9cdbf..60ae8afb 100644 --- a/.agents/pm/tasks/pm-github-9cjx.toon +++ b/.agents/pm/tasks/pm-github-9cjx.toon @@ -6,9 +6,9 @@ status: in_progress priority: 1 tags[5]: all-source,coverage,quality-gate,scripts,tdd created_at: "2026-08-17T07:32:29.666Z" -updated_at: "2026-10-05T09:31:52.268Z" -assignee: codex-sol-coverage -claim_principal: codex-sol-coverage +updated_at: "2026-10-05T09:45:17.622Z" +assignee: codex-sol +claim_principal: codex-sol 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." @@ -16,7 +16,7 @@ expected_result: "Every executable TypeScript source and operational script is a actual_result: The source walker skips scripts and aggregate coverage remains 91.83/82.14/91.75 with thresholds fixed at 88/79/89. 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 -comments[8]{created_at,author,text}: +comments[11]{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." @@ -25,20 +25,26 @@ comments[8]{created_at,author,text}: "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." -files[12]{path,scope}: + "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. +files[15]{path,scope}: eslint.config.js,project package.json,project projects.ts,project scripts/coverage-gate.ts,project scripts/duplication-gate.ts,project test,project + test/coverage-boundaries.test.ts,project test/coverage-gate.test.ts,project test/duplication-gate.test.ts,project + test/gate-inputs.test.ts,project test/helpers/node-scenario.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 -tests[3]: +tests[4]: - command: npm run coverage path: scripts/coverage-gate.ts scope: project @@ -60,6 +66,14 @@ tests[3]: 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 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 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/gate-inputs.test.ts b/test/gate-inputs.test.ts new file mode 100644 index 00000000..92859350 --- /dev/null +++ b/test/gate-inputs.test.ts @@ -0,0 +1,80 @@ +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("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); +}); diff --git a/test/import-errors.test.ts b/test/import-errors.test.ts new file mode 100644 index 00000000..0eba3ad3 --- /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 }); + 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); +}); From 1279e3d5fe2283d65aa1167473930804fab3aca6 Mon Sep 17 00:00:00 2001 From: SteveBot <1153461+unbraind@users.noreply.github.com> Date: Mon, 5 Oct 2026 11:57:29 +0200 Subject: [PATCH 20/60] Record frozen verification and keep unresolved acceptance gates open --- .agents/pm/features/pm-github-f1at.toon | 5 +++-- .agents/pm/history/pm-github-9cjx.jsonl | 2 ++ .agents/pm/history/pm-github-aiok.jsonl | 1 + .agents/pm/history/pm-github-f1at.jsonl | 1 + .agents/pm/history/pm-github-hptv.jsonl | 1 + .agents/pm/issues/pm-github-hptv.toon | 5 +++-- .agents/pm/tasks/pm-github-9cjx.toon | 11 +++++------ .agents/pm/tasks/pm-github-aiok.toon | 4 +++- 8 files changed, 19 insertions(+), 11 deletions(-) diff --git a/.agents/pm/features/pm-github-f1at.toon b/.agents/pm/features/pm-github-f1at.toon index a5f7806e..faec5e57 100644 --- a/.agents/pm/features/pm-github-f1at.toon +++ b/.agents/pm/features/pm-github-f1at.toon @@ -6,7 +6,7 @@ status: blocked 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-05T09:31:11.763Z" +updated_at: "2026-10-05T09:55:33.935Z" assignee: codex-sol claim_principal: codex-sol author: pi-glm @@ -15,7 +15,7 @@ blocked_by: pm-github-9cjx 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[22]{created_at,author,text}: +comments[23]{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." @@ -38,6 +38,7 @@ comments[22]{created_at,author,text}: "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." files[11]{path,scope}: .github/workflows/pm-github-sync.yml,project docs/sync-workflow-caller.yml,project diff --git a/.agents/pm/history/pm-github-9cjx.jsonl b/.agents/pm/history/pm-github-9cjx.jsonl index d420607a..a21e0b91 100644 --- a/.agents/pm/history/pm-github-9cjx.jsonl +++ b/.agents/pm/history/pm-github-9cjx.jsonl @@ -32,3 +32,5 @@ {"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"} diff --git a/.agents/pm/history/pm-github-aiok.jsonl b/.agents/pm/history/pm-github-aiok.jsonl index e13f7018..0ff91d03 100644 --- a/.agents/pm/history/pm-github-aiok.jsonl +++ b/.agents/pm/history/pm-github-aiok.jsonl @@ -2,3 +2,4 @@ {"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 index 995c0e1c..12ebd94f 100644 --- a/.agents/pm/history/pm-github-f1at.jsonl +++ b/.agents/pm/history/pm-github-f1at.jsonl @@ -39,3 +39,4 @@ {"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"} diff --git a/.agents/pm/history/pm-github-hptv.jsonl b/.agents/pm/history/pm-github-hptv.jsonl index de6f3f55..f096b0de 100644 --- a/.agents/pm/history/pm-github-hptv.jsonl +++ b/.agents/pm/history/pm-github-hptv.jsonl @@ -1,2 +1,3 @@ {"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"} diff --git a/.agents/pm/issues/pm-github-hptv.toon b/.agents/pm/issues/pm-github-hptv.toon index ecd19fc8..c2de0290 100644 --- a/.agents/pm/issues/pm-github-hptv.toon +++ b/.agents/pm/issues/pm-github-hptv.toon @@ -6,10 +6,11 @@ status: open priority: 1 tags: [] created_at: "2026-10-05T08:28:54.625Z" -updated_at: "2026-10-05T09:07:19.298Z" +updated_at: "2026-10-05T09:55:33.050Z" author: codex-sol -comments[1]{created_at,author,text}: +comments[2]{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." files[2]{path,scope}: index.ts,project test/import-gate.test.ts,project diff --git a/.agents/pm/tasks/pm-github-9cjx.toon b/.agents/pm/tasks/pm-github-9cjx.toon index 60ae8afb..7baff816 100644 --- a/.agents/pm/tasks/pm-github-9cjx.toon +++ b/.agents/pm/tasks/pm-github-9cjx.toon @@ -1,19 +1,18 @@ 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 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." type: Task -status: in_progress +status: blocked priority: 1 tags[5]: all-source,coverage,quality-gate,scripts,tdd created_at: "2026-08-17T07:32:29.666Z" -updated_at: "2026-10-05T09:45:17.622Z" -assignee: codex-sol -claim_principal: codex-sol +updated_at: "2026-10-05T09:57:29.647Z" 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." +blocked_reason: Exact 100 percent coverage is unfinished; continuation stopped at the owner resource limit. 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: "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." 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 comments[11]{created_at,author,text}: diff --git a/.agents/pm/tasks/pm-github-aiok.toon b/.agents/pm/tasks/pm-github-aiok.toon index 5215f11c..775f9a9e 100644 --- a/.agents/pm/tasks/pm-github-aiok.toon +++ b/.agents/pm/tasks/pm-github-aiok.toon @@ -6,8 +6,10 @@ status: open priority: 1 tags: [] created_at: "2026-10-05T09:23:42.980Z" -updated_at: "2026-10-05T09:26:00.035Z" +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}: From 40db9b314405e5ca9260d55edd5d18447d36a2f5 Mon Sep 17 00:00:00 2001 From: SteveBot <1153461+unbraind@users.noreply.github.com> Date: Tue, 6 Oct 2026 13:50:23 +0200 Subject: [PATCH 21/60] Finish exact import gate coverage and bound installed CLI acceptance --- .agents/pm/features/pm-github-f1at.toon | 9 +- .agents/pm/history/pm-github-9cjx.jsonl | 16 + .agents/pm/history/pm-github-f1at.jsonl | 4 + .agents/pm/history/pm-github-hptv.jsonl | 13 + .agents/pm/issues/pm-github-hptv.toon | 23 +- .agents/pm/tasks/pm-github-9cjx.toon | 36 +- README.md | 17 + docs/coverage-gap-inventory.md | 60 +++ gate.ts | 67 +-- index.ts | 241 ++++----- scripts/coverage-gate.ts | 20 +- scripts/duplication-gate.ts | 17 +- scripts/generate-changelog.ts | 14 +- scripts/main-invocation.ts | 16 + scripts/privacy-gate.ts | 87 ++-- test/bounded-cli.test.ts | 25 + test/coverage-edges.test.ts | 242 +++++++++ test/coverage-gate.test.ts | 41 +- test/coverage-remainder.test.ts | 621 ++++++++++++++++++++++++ test/duplication-gate.test.ts | 16 +- test/gate.test.ts | 160 +++++- test/generate-changelog.test.ts | 16 +- test/helpers/bounded-cli.ts | 41 ++ test/helpers/public-acceptance.ts | 79 +++ test/http-boundary.test.ts | 94 ++++ test/import-gate.test.ts | 30 ++ test/import-lock.test.ts | 197 +++++++- test/privacy-gate.test.ts | 101 +++- test/project-sync.test.ts | 43 ++ 29 files changed, 2136 insertions(+), 210 deletions(-) create mode 100644 docs/coverage-gap-inventory.md create mode 100644 test/bounded-cli.test.ts create mode 100644 test/coverage-edges.test.ts create mode 100644 test/coverage-remainder.test.ts create mode 100644 test/helpers/bounded-cli.ts create mode 100644 test/helpers/public-acceptance.ts diff --git a/.agents/pm/features/pm-github-f1at.toon b/.agents/pm/features/pm-github-f1at.toon index faec5e57..a52bfaba 100644 --- a/.agents/pm/features/pm-github-f1at.toon +++ b/.agents/pm/features/pm-github-f1at.toon @@ -2,20 +2,21 @@ 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: blocked +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-05T09:55:33.935Z" +updated_at: "2026-10-06T11:50:22.874Z" assignee: codex-sol claim_principal: codex-sol 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: Final exact coverage and npm/Bun release checks are underway; PR 108 remains open for review. 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[23]{created_at,author,text}: +comments[25]{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." @@ -39,6 +40,8 @@ comments[23]{created_at,author,text}: "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." files[11]{path,scope}: .github/workflows/pm-github-sync.yml,project docs/sync-workflow-caller.yml,project diff --git a/.agents/pm/history/pm-github-9cjx.jsonl b/.agents/pm/history/pm-github-9cjx.jsonl index a21e0b91..1b16fd0d 100644 --- a/.agents/pm/history/pm-github-9cjx.jsonl +++ b/.agents/pm/history/pm-github-9cjx.jsonl @@ -34,3 +34,19 @@ {"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"} diff --git a/.agents/pm/history/pm-github-f1at.jsonl b/.agents/pm/history/pm-github-f1at.jsonl index 12ebd94f..5f6deb08 100644 --- a/.agents/pm/history/pm-github-f1at.jsonl +++ b/.agents/pm/history/pm-github-f1at.jsonl @@ -40,3 +40,7 @@ {"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"} diff --git a/.agents/pm/history/pm-github-hptv.jsonl b/.agents/pm/history/pm-github-hptv.jsonl index f096b0de..db5bbb8b 100644 --- a/.agents/pm/history/pm-github-hptv.jsonl +++ b/.agents/pm/history/pm-github-hptv.jsonl @@ -1,3 +1,16 @@ {"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"} diff --git a/.agents/pm/issues/pm-github-hptv.toon b/.agents/pm/issues/pm-github-hptv.toon index c2de0290..28f8e8df 100644 --- a/.agents/pm/issues/pm-github-hptv.toon +++ b/.agents/pm/issues/pm-github-hptv.toon @@ -1,17 +1,30 @@ id: pm-github-hptv title: Bound native Bun CLI repeats of a completed public atomic import -description: "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." +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: open +status: in_progress priority: 1 tags: [] created_at: "2026-10-05T08:28:54.625Z" -updated_at: "2026-10-05T09:55:33.050Z" +updated_at: "2026-10-06T11:44:34.944Z" +assignee: codex-sol +claim_principal: codex-sol author: codex-sol -comments[2]{created_at,author,text}: +comments[6]{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." -files[2]{path,scope}: + "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." +files[5]{path,scope}: index.ts,project + test/helpers/bounded-cli.ts,project + test/helpers/public-acceptance.ts,project test/import-gate.test.ts,project + test/import-lock.test.ts,project +tests[1]{command,scope,timeout_seconds,provenance{author,created_at,source_kind,source_ref}}: + node --test test/bounded-cli.test.ts,project,60,codex-sol,"2026-10-06T11:37:04.782Z",local_mutation,feat/import-privacy-gate +docs[1]{path,scope}: + README.md,project body: "" diff --git a/.agents/pm/tasks/pm-github-9cjx.toon b/.agents/pm/tasks/pm-github-9cjx.toon index 7baff816..953ec6ff 100644 --- a/.agents/pm/tasks/pm-github-9cjx.toon +++ b/.agents/pm/tasks/pm-github-9cjx.toon @@ -1,21 +1,23 @@ id: pm-github-9cjx title: Reach exact all-source coverage and include every operational script -description: "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." +description: "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." type: Task -status: blocked +status: in_progress priority: 1 tags[5]: all-source,coverage,quality-gate,scripts,tdd created_at: "2026-08-17T07:32:29.666Z" -updated_at: "2026-10-05T09:57:29.647Z" +updated_at: "2026-10-06T11:49:57.012Z" +assignee: codex-sol +claim_principal: codex-sol 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." -blocked_reason: Exact 100 percent coverage is unfinished; continuation stopped at the owner resource limit. +unblock_note: Full source coverage closure is being reverified at unchanged exact thresholds. 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: "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." 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 -comments[11]{created_at,author,text}: +comments[17]{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." @@ -27,18 +29,28 @@ comments[11]{created_at,author,text}: "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. -files[15]{path,scope}: + "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." +files[19]{path,scope}: eslint.config.js,project package.json,project projects.ts,project scripts/coverage-gate.ts,project scripts/duplication-gate.ts,project + scripts/main-invocation.ts,project test,project 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 @@ -73,7 +85,13 @@ tests[4]: created_at: "2026-10-05T09:38:01.943Z" source_kind: local_mutation source_ref: feat/import-privacy-gate -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 +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 body: "" diff --git a/README.md b/README.md index 55e56ff2..7282650e 100644 --- a/README.md +++ b/README.md @@ -324,6 +324,23 @@ jobs: 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`. +### 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 comment-lock retry also 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` diff --git a/docs/coverage-gap-inventory.md b/docs/coverage-gap-inventory.md new file mode 100644 index 00000000..f4c17aa9 --- /dev/null +++ b/docs/coverage-gap-inventory.md @@ -0,0 +1,60 @@ +# 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. + +## gate.ts + +Uncovered statement lines: 984, 985, 986, 987, 988, 989, 990, 991, 992, 993, 994, 995. + +Uncovered branches (start line:column to end line:column): . + +## 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: . + +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. + +Final npm release acceptance 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. diff --git a/gate.ts b/gate.ts index f1c2784b..4373db78 100644 --- a/gate.ts +++ b/gate.ts @@ -227,8 +227,9 @@ const PERSONAL_DATA_RULES: readonly ContentRule[] = [ 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. - confirm: (matched) => (matched.match(/\d/g) ?? []).length >= 8, + // 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", @@ -236,7 +237,7 @@ const PERSONAL_DATA_RULES: readonly ContentRule[] = [ // 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, + confirm: (matched) => matched.match(/\d/g)!.length === 10, }, { rule: "phone-number", @@ -559,7 +560,8 @@ export function parseUnifiedDiff(diffText: string): ChangeFile[] { if (raw.startsWith(" ")) { oldRemaining--; newRemaining--; - walk.push({ text: raw.startsWith(" ") ? raw.slice(1) : raw, added: false }); + // The enclosing branch guarantees the leading space. + walk.push({ text: raw.slice(1), added: false }); continue; } if (raw === "" && oldRemaining === 0 && newRemaining === 0) { completeHunk(); continue; } @@ -635,8 +637,11 @@ export function runGitDefault(cwd: string, args: readonly string[]): { ok: boole const result = spawnSync("git", ["-C", cwd, ...args], { encoding: "utf-8", 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: result.stdout ?? "", - stderr: result.stderr ?? String(result.error ?? "git failed"), + stderr: result.stderr ?? String(result.error), }; } @@ -914,6 +919,9 @@ export function runTrackerGate(input: TrackerGateInput): GateReport { } } 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.`, @@ -933,35 +941,32 @@ export function runTrackerGate(input: TrackerGateInput): GateReport { } } - try { - 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 }); + // 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) : "", - }; - } catch (err: unknown) { - if (err instanceof GateInputError) throw err; - throw new GateInputError( - `pm github gate: scanner error; scan did not complete.`, - ); } + 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) : "", + }; } /** diff --git a/index.ts b/index.ts index eef9c48c..6c24d048 100644 --- a/index.ts +++ b/index.ts @@ -48,6 +48,7 @@ import { import { collectNewOrderingCycleWarnings as sdkCollectNewOrderingCycleWarnings } from "@unbrained/pm-cli/sdk/graph"; import { + type TrackerGateInput, type GateReport, type GateVerdict, GateInputError, @@ -263,14 +264,14 @@ 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", env: { ...process.env } }); - 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; } @@ -380,7 +381,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(); @@ -535,7 +536,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. @@ -1217,6 +1233,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) // --------------------------------------------------------------------------- @@ -1299,18 +1320,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", ), }; @@ -1519,7 +1544,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}`, @@ -1758,13 +1783,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; @@ -1932,7 +1961,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" }; } @@ -1995,17 +2024,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); @@ -2175,7 +2209,7 @@ 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 }; } @@ -2192,7 +2226,7 @@ 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}`); } } @@ -2617,7 +2651,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)}`], }; } @@ -2650,7 +2684,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 }; @@ -2695,9 +2729,10 @@ 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.`); - console.error(opts.gate ? `pm github gate: comments for issue #${issue.number} could not be read.` : `#${issue.number}: failed to fetch comments — ${msg}`); + // The gated case throws above, so this diagnostic always names the plain path. + console.error(`#${issue.number}: failed to fetch comments — ${msg}`); } } @@ -2882,6 +2917,21 @@ export interface ImportGateReceipt { 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. * @@ -2896,17 +2946,7 @@ export interface ImportGateReceipt { * @returns The pass receipt, embedded into the import result. */ function gateImportWrites(pmRoot: string): ImportGateReceipt { - let report: GateReport; - try { - report = runTrackerGate({ pmRoot }); - } catch (err: unknown) { - throw new CommandError( - err instanceof GateInputError - ? err.message - : `pm github gate: could not run the gate; scan did not complete.`, - EXIT_CODE.GENERIC_FAILURE, - ); - } + const report = runCommandTrackerGate({ pmRoot }); const lines = formatGateReport(report); console.error(lines.join("\n")); if (report.verdict === "fail") { @@ -2963,7 +3003,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" : ""; @@ -2985,7 +3025,6 @@ export async function runImport( wouldUpdate: 0, wouldSkip: 0, atomic: true, - ...(emptyGateReceipt ? { gate: emptyGateReceipt } : {}), }; } return { imported: 0, updated: 0, skipped: 0, ...(emptyGateReceipt ? { gate: emptyGateReceipt } : {}) }; @@ -3332,7 +3371,6 @@ export async function runImport( wouldImport: imported, wouldUpdate: updated, wouldSkip: skipped, - ...(opts.atomic ? { atomic: true } : {}), ...(opts.linkDeps ? { wouldLinkDependencyCandidates: countDependencyRefCandidates(repo, filtered) } : {}), ...gatePlanFields, }; @@ -3456,8 +3494,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"); @@ -3514,7 +3552,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; @@ -3538,7 +3576,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++; } @@ -3713,7 +3751,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}"`; @@ -3793,10 +3831,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"); @@ -3966,7 +4004,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--; @@ -4035,12 +4073,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", env: { ...process.env } }); - 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 @@ -4054,8 +4088,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(); @@ -4085,7 +4119,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; @@ -4101,16 +4135,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 { @@ -4461,7 +4492,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; @@ -4629,8 +4660,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)", @@ -4646,7 +4677,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 { @@ -4670,7 +4701,7 @@ async function runProjectList(ctx: CommandHandlerContext) { * @returns The parsed project identity. */ function requireProjectRef(ctx: CommandHandlerContext, usage: string): ProjectRef { - const ref = parseProjectRef(optionString(ctx.options || {}, "project") || ctx.args?.[0]); + const ref = parseProjectRef(optionString(ctx.options, "project") || ctx.args[0]); if (!ref) throw new CommandError(usage, EXIT_CODE.USAGE); return ref; } @@ -4686,7 +4717,7 @@ function requireProjectRef(ctx: CommandHandlerContext, usage: string): ProjectRe * @param ctx - The command-handler context. */ async function runProjectFields(ctx: CommandHandlerContext) { - const options = ctx.options || {}; + 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); @@ -4704,7 +4735,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)"}`); @@ -4728,7 +4759,7 @@ async function runProjectFields(ctx: CommandHandlerContext) { * @param ctx - The command-handler context. */ async function runProjectImport(ctx: CommandHandlerContext) { - const options = ctx.options || {}; + 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"; @@ -4851,26 +4882,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, @@ -4882,7 +4911,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) }; } } @@ -4963,7 +4992,7 @@ interface PullPlanEntryLike { * @param ctx - The command-handler context. */ async function runProjectSync(ctx: CommandHandlerContext) { - const options = ctx.options || {}; + 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"); @@ -5042,7 +5071,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)"); } @@ -5059,7 +5088,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); @@ -5242,7 +5271,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 " + @@ -5258,7 +5287,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 " + @@ -5329,7 +5358,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[]; @@ -5364,12 +5393,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; @@ -5484,24 +5513,12 @@ export default defineExtension({ async run(ctx: CommandHandlerContext) { const diffFile = optionString(ctx.options, "diff"); const allowlistFile = optionString(ctx.options, "allowlist"); - let report: GateReport; - try { - report = runTrackerGate({ - pmRoot: ctx.pm_root, - ...(diffFile ? { diffFile } : {}), - ...(allowlistFile ? { allowlistFile } : {}), - }); - } catch (err: unknown) { - // GateInputError carries a user-facing message; anything else is an - // unexpected scanner failure. Both fail closed — never pass vacuously. - throw new CommandError( - err instanceof GateInputError - ? err.message - : `pm github gate: scanner error; scan did not complete.`, - EXIT_CODE.GENERIC_FAILURE, - ); - } - if (ctx.global?.json !== true) { + 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") { @@ -5537,7 +5554,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/scripts/coverage-gate.ts b/scripts/coverage-gate.ts index 88383e27..6069ef9d 100644 --- a/scripts/coverage-gate.ts +++ b/scripts/coverage-gate.ts @@ -10,7 +10,7 @@ import { spawnSync, type SpawnSyncOptions } from "node:child_process"; import { mkdirSync, readdirSync, readFileSync, rmSync, statSync } from "node:fs"; import { createRequire } from "node:module"; import { dirname, join, relative, resolve, sep } from "node:path"; -import { isMainInvocation } from "./main-invocation.ts"; +import { isMainInvocation, nodeToolingExecutable } from "./main-invocation.ts"; /** Configuration cannot exempt sources or lower the required complete gate. */ interface CoverageConfig { @@ -85,7 +85,7 @@ export function runCoverageGate(root: string, runner: CoverageRunner = spawnSync 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 = process.versions.bun ? "node" : process.execPath; + 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", @@ -116,4 +116,18 @@ export function runCoverageGate(root: string, runner: CoverageRunner = spawnSync } } -if (isMainInvocation(process.argv, import.meta.url)) process.exitCode = runCoverageGate(resolve(import.meta.dirname, "..")); +/** + * Run the coverage gate when this file is the process entry point. + * + * 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. + * + * @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. + */ +export function runCoverageGateIfMain(argv: readonly string[], moduleUrl: string, root: string): void { + if (isMainInvocation(argv, moduleUrl)) process.exitCode = runCoverageGate(root); +} + +runCoverageGateIfMain(process.argv, import.meta.url, resolve(import.meta.dirname, "..")); diff --git a/scripts/duplication-gate.ts b/scripts/duplication-gate.ts index d7d73bef..080b213a 100644 --- a/scripts/duplication-gate.ts +++ b/scripts/duplication-gate.ts @@ -11,7 +11,7 @@ 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 } from "./main-invocation.ts"; +import { isMainInvocation, nodeToolingExecutable } from "./main-invocation.ts"; /** Engine counters validated independently of subprocess exit status. */ interface DuplicationSummary { @@ -35,7 +35,7 @@ export function runDuplicationGate(root: string, runner: CoverageRunner = spawnS const output = join(root, "coverage", `duplication-${minTokens}`); rmSync(output, { recursive: true, force: true }); mkdirSync(output, { recursive: true }); - const run = runner(process.versions.bun ? "node" : process.execPath, [engine, "--absolute", "--no-gitignore", "--silent", `--threshold=${minTokens === 1 ? 1000000 : 0}`, + 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."); @@ -56,4 +56,15 @@ export function runDuplicationGate(root: string, runner: CoverageRunner = spawnS } } -if (isMainInvocation(process.argv, import.meta.url)) process.exitCode = runDuplicationGate(resolve(import.meta.dirname, "..")); +/** + * 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 index 469191b2..50835841 100644 --- a/scripts/generate-changelog.ts +++ b/scripts/generate-changelog.ts @@ -36,4 +36,16 @@ export async function generateChangelog(root: string, args: readonly string[]): } } -if (isMainInvocation(process.argv, import.meta.url)) process.exitCode = await generateChangelog(resolve(import.meta.dirname, ".."), process.argv.slice(2)); +/** + * 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 3eaf564d..c13054d7 100644 --- a/scripts/privacy-gate.ts +++ b/scripts/privacy-gate.ts @@ -155,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]); } @@ -230,6 +222,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`. @@ -244,21 +264,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]]; }); } @@ -272,16 +289,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); } /** @@ -361,7 +369,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 @@ -417,6 +425,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/bounded-cli.test.ts b/test/bounded-cli.test.ts new file mode 100644 index 00000000..ef6a0f0b --- /dev/null +++ b/test/bounded-cli.test.ts @@ -0,0 +1,25 @@ +/** 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); +}); diff --git a/test/coverage-edges.test.ts b/test/coverage-edges.test.ts new file mode 100644 index 00000000..00e9d3a0 --- /dev/null +++ b/test/coverage-edges.test.ts @@ -0,0 +1,242 @@ +/** 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\)/); + })); +}); diff --git a/test/coverage-gate.test.ts b/test/coverage-gate.test.ts index e3096716..47456f93 100644 --- a/test/coverage-gate.test.ts +++ b/test/coverage-gate.test.ts @@ -4,7 +4,9 @@ import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync import { tmpdir } from "node:os"; import { join } from "node:path"; import test from "node:test"; -import { collectCoverageSources, runCoverageGate, type CoverageRunner } from "../scripts/coverage-gate.ts"; +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 { @@ -77,6 +79,43 @@ test("coverage invalidates stale receipts and rejects startup, process, and repo } 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; + rmSync(root, { recursive: true, force: true }); + } +}); + test("real c8 requires both branches and rejects a completely unloaded authored module", () => { const root = coverageFixture(); try { diff --git a/test/coverage-remainder.test.ts b/test/coverage-remainder.test.ts new file mode 100644 index 00000000..92db96f7 --- /dev/null +++ b/test/coverage-remainder.test.ts @@ -0,0 +1,621 @@ +/** + * 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 { 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("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", 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); + fs.chmodSync(path.join(root, ".agents", "pm", "tasks"), 0o555); + const failed = await captureStderr(() => syncGithubCommentsToAnnotations(itemId, [ + { id: 5, body: "", user: null, created_at: "" }, + ], path.join(root, ".agents", "pm"), 7)); + fs.chmodSync(path.join(root, ".agents", "pm", "tasks"), 0o755); + 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/duplication-gate.test.ts b/test/duplication-gate.test.ts index fcb186cb..babb61a8 100644 --- a/test/duplication-gate.test.ts +++ b/test/duplication-gate.test.ts @@ -4,7 +4,8 @@ import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "nod import { tmpdir } from "node:os"; import { join } from "node:path"; import test from "node:test"; -import { runDuplicationGate } from "../scripts/duplication-gate.ts"; +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. */ @@ -25,6 +26,19 @@ function detectorReceipt(root: string, sources: number, lines: number, duplicate }; } +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; + rmSync(root, { recursive: true, force: true }); + } +}); + test("duplication fails closed on subprocess failures, missing reports, omitted source, and invalid counters", () => { const root = duplicationFixture(); try { diff --git a/test/gate.test.ts b/test/gate.test.ts index b6bb79a9..8a8e06e1 100644 --- a/test/gate.test.ts +++ b/test/gate.test.ts @@ -17,12 +17,14 @@ 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, @@ -33,7 +35,8 @@ import { shannonEntropyPerChar, type GateReport, } from "../gate.ts"; -import extension from "../index.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 = @@ -843,3 +846,158 @@ test("pm github gate scans an explicit --diff file and honors --allowlist", asyn 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 index 284c6326..d96e8a41 100644 --- a/test/generate-changelog.test.ts +++ b/test/generate-changelog.test.ts @@ -5,7 +5,21 @@ 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 { generateChangelog } from "../scripts/generate-changelog.ts"; +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; + 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-")); diff --git a/test/helpers/bounded-cli.ts b/test/helpers/bounded-cli.ts new file mode 100644 index 00000000..905b3a36 --- /dev/null +++ b/test/helpers/bounded-cli.ts @@ -0,0 +1,41 @@ +/** 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); + child.stdout.on("data", chunk => { stdout += String(chunk); }); + child.stderr.on("data", chunk => { stderr += String(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/public-acceptance.ts b/test/helpers/public-acceptance.ts new file mode 100644 index 00000000..affa37b8 --- /dev/null +++ b/test/helpers/public-acceptance.ts @@ -0,0 +1,79 @@ +/** 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; +function setup(bin: string, args: string[], cwd = root): string { + const result = spawnSync(bin, args, { cwd, env, 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"]); + const packed = JSON.parse(setup("npm", ["pack", "--ignore-scripts", "--json", "--pack-destination", base], packageRoot)) 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/http-boundary.test.ts b/test/http-boundary.test.ts index 753c6355..291ee36e 100644 --- a/test/http-boundary.test.ts +++ b/test/http-boundary.test.ts @@ -23,6 +23,7 @@ import assert from "node:assert/strict"; import test from "node:test"; +import { createServer } from "node:http"; import { CommandError, @@ -33,6 +34,8 @@ import { fetchJSON, githubApiBase, parseImportOptions, + isRetryableStatus, + parseRateLimit, runImport, type GhIssue, type GhComment, @@ -510,3 +513,94 @@ 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 +// break — 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 () => { + // A server that accepts the connection and never answers exercises the real + // 30-second socket idle timeout: the request must be destroyed with the + // timeout error, never left hanging. + const server = createServer(() => { /* accept and never respond */ }); + await new Promise((resolve) => { server.listen(0, "127.0.0.1", resolve); }); + const port = (server.address() as { port: number }).port; + try { + await assert.rejects( + fetchJSON(`http://127.0.0.1:${port}/repos/a/b`), + /request timed out after 30s/, + ); + } finally { + server.close(); + } +}); + +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 stops at a non-array comments page but keeps earlier pages", 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 well-formed JSON object (not an array) must end the walk without + // throwing and without losing the already-collected page. + jsonResponse(res, 200, { message: "unexpected shape" }); + } + }, async () => { + const issue: GhIssue = { ...ghIssue(1), comments: 2 }; + const comments = await fetchComments(issue, "a/b", "t"); + assert.deepEqual(comments.map((c) => c.id), [1]); + }); +}); + +/** 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-gate.test.ts b/test/import-gate.test.ts index 5d631f3f..78b53b47 100644 --- a/test/import-gate.test.ts +++ b/test/import-gate.test.ts @@ -33,6 +33,7 @@ import extension, { 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; @@ -790,3 +791,32 @@ test("the executable reusable workflow pushes only a clean import and creates or }); } finally { fs.rmSync(base, { recursive: true, force: true }); } }); + + +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 }); } +}); diff --git a/test/import-lock.test.ts b/test/import-lock.test.ts index 3f5658b5..b3da87d4 100644 --- a/test/import-lock.test.ts +++ b/test/import-lock.test.ts @@ -11,14 +11,16 @@ import { comments as readComments } from "@unbrained/pm-cli/sdk"; // asserts no duplicate comment markers result. import assert from "node:assert/strict"; -import test from "node:test"; +import test, { mock } 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"; @@ -30,6 +32,7 @@ import { IMPORT_LOCK_TTL_MS_DEFAULT, acquireImportLock, importCommentSyncLockPath, + isLockOwnerAlive, resolvePmDataDir, syncGithubCommentsToAnnotations, } from "../index.ts"; @@ -528,3 +531,193 @@ test( } }, ); + +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 or written", 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(" | ")); + + 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 }); + } + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); + +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 closeMock = mock.method(fs, "closeSync", () => { + throw Object.assign(new Error("close failed"), { code: "EIO" }); + }); + const unlinkMock = mock.method(fs, "unlinkSync", () => { + throw Object.assign(new Error("unlink failed"), { code: "EIO" }); + }); + const writeMock = mock.method(fs, "writeFileSync", ((target: fs.PathOrFileDescriptor, data: string) => { + if (typeof target === "number") 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(closeMock.mock.calls.length > 0); + assert.ok(unlinkMock.mock.calls.length > 0); + } finally { + writeMock.mock.restore(); + closeMock.mock.restore(); + unlinkMock.mock.restore(); + 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 = mock.method(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.mock.restore(); + } + } 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 = mock.method(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.mock.restore(); rmSync(root, { recursive: true, force: true }); } +}); + +test("real permission failures during stale unlink and token release retain the lock", 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 = mock.method(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.mock.restore(); + 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.mock.restore(); chmodSync(dirname(lockPath), 0o755); rmSync(root, { recursive: true, force: true }); + } +}); diff --git a/test/privacy-gate.test.ts b/test/privacy-gate.test.ts index 3ae1c1b8..fce7fa25 100644 --- a/test/privacy-gate.test.ts +++ b/test/privacy-gate.test.ts @@ -19,16 +19,20 @@ 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, + listAllObjects, type PrivacyGateResult, + readObject, runGate, + runPrivacyGateIfMain, main, scanBlob, } from "../scripts/privacy-gate.ts"; @@ -370,6 +374,99 @@ 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; + rmSync(root, { recursive: true, force: true }); + } +}); + +test("privacy gate fails closed when git cannot be spawned", () => { + const root = initRepo("no-git"); + writeAllowlist(root, ["intruder@localhost"]); + const previous = process.env.PATH; + process.env.PATH = ""; + try { + 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)); + } finally { + if (previous === undefined) delete process.env.PATH; + else process.env.PATH = previous; + rmSync(root, { recursive: true, force: true }); + } +}); + +test("privacy gate fails closed when object inventory or blob reads are unusable", () => { + const root = initRepo("bad-git"); + try { + writeAllowlist(root, ["intruder@localhost"]); + writeFileSync(join(root, "clean.txt"), "clean\n"); + commitAll(root, "clean"); + const bin = mkdtempSync(join(tmpdir(), "pm-github-privacy-git-")); + 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 +if [ "$1" = "cat-file" ] && [ "$2" = "blob" ]; then + echo forced-read-failure >&2 + exit 1 +fi +exec "$REAL_GIT" "$@" +`, { mode: 0o755 }); + chmodSync(join(bin, "git"), 0o755); + const previous = process.env.PATH; + const previousGit = process.env.REAL_GIT; + process.env.PATH = `${bin}${delimiter}${previous ?? ""}`; + + process.env.REAL_GIT = realGit; + try { + 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]!; + // Point the wrapper at real inventory so the scan reaches the blob read. + writeFileSync(join(bin, "git"), `#!/bin/sh +if [ "$1" = "cat-file" ] && [ "$2" = "blob" ]; then + echo forced-read-failure >&2 + exit 1 +fi +exec "$REAL_GIT" "$@" +`, { 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/); + } finally { + if (previous === undefined) delete process.env.PATH; + else process.env.PATH = previous; + if (previousGit === undefined) delete process.env.REAL_GIT; + else process.env.REAL_GIT = previousGit; + rmSync(bin, { recursive: true, force: true }); + } + } finally { + 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 { diff --git a/test/project-sync.test.ts b/test/project-sync.test.ts index fdca4911..16cdd10d 100644 --- a/test/project-sync.test.ts +++ b/test/project-sync.test.ts @@ -289,3 +289,46 @@ test("project sync previews additions without Status and a linked item without a 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/); + }); +}); From 5ed8c4716b54fb356b696e17ac8a98a0976d062f Mon Sep 17 00:00:00 2001 From: SteveBot <1153461+unbraind@users.noreply.github.com> Date: Tue, 6 Oct 2026 13:55:39 +0200 Subject: [PATCH 22/60] Enforce the HTTP deadline before destroying stalled Bun responses --- .agents/pm/history/pm-github-9cjx.jsonl | 1 + .agents/pm/history/pm-github-hptv.jsonl | 5 +++++ .agents/pm/issues/pm-github-hptv.toon | 11 ++++++++--- .agents/pm/tasks/pm-github-9cjx.toon | 5 +++-- README.md | 4 +++- docs/coverage-gap-inventory.md | 15 ++++++++++++++- index.ts | 17 ++++++++++++----- test/http-boundary.test.ts | 19 ++++++++++++------- 8 files changed, 58 insertions(+), 19 deletions(-) diff --git a/.agents/pm/history/pm-github-9cjx.jsonl b/.agents/pm/history/pm-github-9cjx.jsonl index 1b16fd0d..5c0f9403 100644 --- a/.agents/pm/history/pm-github-9cjx.jsonl +++ b/.agents/pm/history/pm-github-9cjx.jsonl @@ -50,3 +50,4 @@ {"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"} diff --git a/.agents/pm/history/pm-github-hptv.jsonl b/.agents/pm/history/pm-github-hptv.jsonl index db5bbb8b..305b0da4 100644 --- a/.agents/pm/history/pm-github-hptv.jsonl +++ b/.agents/pm/history/pm-github-hptv.jsonl @@ -14,3 +14,8 @@ {"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"} diff --git a/.agents/pm/issues/pm-github-hptv.toon b/.agents/pm/issues/pm-github-hptv.toon index 28f8e8df..0f7c6f4e 100644 --- a/.agents/pm/issues/pm-github-hptv.toon +++ b/.agents/pm/issues/pm-github-hptv.toon @@ -6,21 +6,26 @@ status: in_progress priority: 1 tags: [] created_at: "2026-10-05T08:28:54.625Z" -updated_at: "2026-10-06T11:44:34.944Z" +updated_at: "2026-10-06T11:55:38.455Z" assignee: codex-sol claim_principal: codex-sol author: codex-sol -comments[6]{created_at,author,text}: +comments[10]{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." -files[5]{path,scope}: + "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." +files[6]{path,scope}: index.ts,project test/helpers/bounded-cli.ts,project test/helpers/public-acceptance.ts,project + test/http-boundary.test.ts,project test/import-gate.test.ts,project test/import-lock.test.ts,project tests[1]{command,scope,timeout_seconds,provenance{author,created_at,source_kind,source_ref}}: diff --git a/.agents/pm/tasks/pm-github-9cjx.toon b/.agents/pm/tasks/pm-github-9cjx.toon index 953ec6ff..0f8f0897 100644 --- a/.agents/pm/tasks/pm-github-9cjx.toon +++ b/.agents/pm/tasks/pm-github-9cjx.toon @@ -6,7 +6,7 @@ status: in_progress priority: 1 tags[5]: all-source,coverage,quality-gate,scripts,tdd created_at: "2026-08-17T07:32:29.666Z" -updated_at: "2026-10-06T11:49:57.012Z" +updated_at: "2026-10-06T11:55:39.390Z" assignee: codex-sol claim_principal: codex-sol author: codex @@ -17,7 +17,7 @@ expected_result: "Every executable TypeScript source and operational script is a actual_result: "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." 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 -comments[17]{created_at,author,text}: +comments[18]{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." @@ -35,6 +35,7 @@ comments[17]{created_at,author,text}: "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." files[19]{path,scope}: eslint.config.js,project package.json,project diff --git a/README.md b/README.md index 7282650e..e6446ee8 100644 --- a/README.md +++ b/README.md @@ -336,7 +336,9 @@ 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 comment-lock retry also honors its existing wait budget when +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. diff --git a/docs/coverage-gap-inventory.md b/docs/coverage-gap-inventory.md index f4c17aa9..9a7fd340 100644 --- a/docs/coverage-gap-inventory.md +++ b/docs/coverage-gap-inventory.md @@ -50,7 +50,7 @@ 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. -Final npm release acceptance passes all 487 tests, with zero failures or skips. +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. @@ -58,3 +58,16 @@ 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. diff --git a/index.ts b/index.ts index 6c24d048..89f405f0 100644 --- a/index.ts +++ b/index.ts @@ -364,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", @@ -418,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)); } /** diff --git a/test/http-boundary.test.ts b/test/http-boundary.test.ts index 291ee36e..2eea9aaa 100644 --- a/test/http-boundary.test.ts +++ b/test/http-boundary.test.ts @@ -539,18 +539,23 @@ test("requestOnce rejects an unparseable redirect Location instead of following }); test("requestOnce fails a request after 30s without a response", { timeout: 45_000 }, async () => { - // A server that accepts the connection and never answers exercises the real - // 30-second socket idle timeout: the request must be destroyed with the - // timeout error, never left hanging. - const server = createServer(() => { /* accept and never respond */ }); + // 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 { - await assert.rejects( - fetchJSON(`http://127.0.0.1:${port}/repos/a/b`), + 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(); server.close(); } }); From 54a9c3d650233c39694bfa4c03997972d7f55db8 Mon Sep 17 00:00:00 2001 From: SteveBot <1153461+unbraind@users.noreply.github.com> Date: Tue, 6 Oct 2026 14:42:50 +0200 Subject: [PATCH 23/60] Record complete acceptance and make fault fixtures portable across runtimes --- .agents/pm/features/pm-github-f1at.toon | 8 +-- .agents/pm/history/pm-github-9cjx.jsonl | 12 ++++ .agents/pm/history/pm-github-f1at.jsonl | 3 + .agents/pm/history/pm-github-hptv.jsonl | 4 ++ .agents/pm/issues/pm-github-hptv.toon | 11 ++-- .agents/pm/tasks/pm-github-9cjx.toon | 17 +++-- docs/coverage-gap-inventory.md | 44 ++++++++++++- test/coverage-gate.test.ts | 2 +- test/duplication-gate.test.ts | 2 +- test/generate-changelog.test.ts | 2 +- test/import-lock.test.ts | 51 ++++++++++----- test/privacy-gate.test.ts | 85 ++++++++++++------------- 12 files changed, 159 insertions(+), 82 deletions(-) diff --git a/.agents/pm/features/pm-github-f1at.toon b/.agents/pm/features/pm-github-f1at.toon index a52bfaba..e147a6ce 100644 --- a/.agents/pm/features/pm-github-f1at.toon +++ b/.agents/pm/features/pm-github-f1at.toon @@ -6,9 +6,7 @@ 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-06T11:50:22.874Z" -assignee: codex-sol -claim_principal: codex-sol +updated_at: "2026-10-06T12:42:50.295Z" 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 @@ -16,7 +14,7 @@ unblock_note: Final exact coverage and npm/Bun release checks are underway; PR 1 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[25]{created_at,author,text}: +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." @@ -42,6 +40,8 @@ comments[25]{created_at,author,text}: "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 diff --git a/.agents/pm/history/pm-github-9cjx.jsonl b/.agents/pm/history/pm-github-9cjx.jsonl index 5c0f9403..b185722a 100644 --- a/.agents/pm/history/pm-github-9cjx.jsonl +++ b/.agents/pm/history/pm-github-9cjx.jsonl @@ -51,3 +51,15 @@ {"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"} diff --git a/.agents/pm/history/pm-github-f1at.jsonl b/.agents/pm/history/pm-github-f1at.jsonl index 5f6deb08..56841dd0 100644 --- a/.agents/pm/history/pm-github-f1at.jsonl +++ b/.agents/pm/history/pm-github-f1at.jsonl @@ -44,3 +44,6 @@ {"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"} diff --git a/.agents/pm/history/pm-github-hptv.jsonl b/.agents/pm/history/pm-github-hptv.jsonl index 305b0da4..6c4fdd5b 100644 --- a/.agents/pm/history/pm-github-hptv.jsonl +++ b/.agents/pm/history/pm-github-hptv.jsonl @@ -19,3 +19,7 @@ {"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"} diff --git a/.agents/pm/issues/pm-github-hptv.toon b/.agents/pm/issues/pm-github-hptv.toon index 0f7c6f4e..343a7b5f 100644 --- a/.agents/pm/issues/pm-github-hptv.toon +++ b/.agents/pm/issues/pm-github-hptv.toon @@ -6,11 +6,9 @@ status: in_progress priority: 1 tags: [] created_at: "2026-10-05T08:28:54.625Z" -updated_at: "2026-10-06T11:55:38.455Z" -assignee: codex-sol -claim_principal: codex-sol +updated_at: "2026-10-06T12:42:49.423Z" author: codex-sol -comments[10]{created_at,author,text}: +comments[12]{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." @@ -21,6 +19,8 @@ comments[10]{created_at,author,text}: "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." files[6]{path,scope}: index.ts,project test/helpers/bounded-cli.ts,project @@ -28,8 +28,9 @@ files[6]{path,scope}: test/http-boundary.test.ts,project test/import-gate.test.ts,project test/import-lock.test.ts,project -tests[1]{command,scope,timeout_seconds,provenance{author,created_at,source_kind,source_ref}}: +tests[2]{command,scope,timeout_seconds,provenance{author,created_at,source_kind,source_ref}}: node --test test/bounded-cli.test.ts,project,60,codex-sol,"2026-10-06T11:37:04.782Z",local_mutation,feat/import-privacy-gate + "node --test --test-name-pattern=\"requestOnce fails a request\" test/http-boundary.test.ts",project,60,codex-sol,"2026-10-06T11:56:36.862Z",local_mutation,feat/import-privacy-gate docs[1]{path,scope}: README.md,project body: "" diff --git a/.agents/pm/tasks/pm-github-9cjx.toon b/.agents/pm/tasks/pm-github-9cjx.toon index 0f8f0897..0b4bdcb2 100644 --- a/.agents/pm/tasks/pm-github-9cjx.toon +++ b/.agents/pm/tasks/pm-github-9cjx.toon @@ -6,9 +6,7 @@ status: in_progress priority: 1 tags[5]: all-source,coverage,quality-gate,scripts,tdd created_at: "2026-08-17T07:32:29.666Z" -updated_at: "2026-10-06T11:55:39.390Z" -assignee: codex-sol -claim_principal: codex-sol +updated_at: "2026-10-06T12:42:48.449Z" 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." @@ -17,7 +15,7 @@ expected_result: "Every executable TypeScript source and operational script is a actual_result: "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." 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 -comments[18]{created_at,author,text}: +comments[29]{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." @@ -36,6 +34,17 @@ comments[18]{created_at,author,text}: "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." files[19]{path,scope}: eslint.config.js,project package.json,project diff --git a/docs/coverage-gap-inventory.md b/docs/coverage-gap-inventory.md index 9a7fd340..e03bc228 100644 --- a/docs/coverage-gap-inventory.md +++ b/docs/coverage-gap-inventory.md @@ -1,12 +1,12 @@ # 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 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 (start line:column to end line:column): . +Uncovered branches: none. ## index.ts @@ -16,7 +16,7 @@ Uncovered branches (start line:column to end line:column): 1208:33-1208:60, 1227 ## scripts/coverage-gate.ts -Uncovered statement lines: . +Uncovered statement lines: none. Uncovered branches (start line:column to end line:column): 102:52-102:110. @@ -71,3 +71,41 @@ 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. + +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. diff --git a/test/coverage-gate.test.ts b/test/coverage-gate.test.ts index 47456f93..236c8a85 100644 --- a/test/coverage-gate.test.ts +++ b/test/coverage-gate.test.ts @@ -111,7 +111,7 @@ test("coverage gate main entry runs against the supplied root", () => { runCoverageGateIfMain(["node", fileURLToPath(moduleUrl)], moduleUrl, root); assert.equal(process.exitCode, 1); } finally { - process.exitCode = previous; + process.exitCode = previous ?? 0; rmSync(root, { recursive: true, force: true }); } }); diff --git a/test/duplication-gate.test.ts b/test/duplication-gate.test.ts index babb61a8..ca810ddc 100644 --- a/test/duplication-gate.test.ts +++ b/test/duplication-gate.test.ts @@ -34,7 +34,7 @@ test("duplication gate main entry runs against the supplied root", () => { runDuplicationGateIfMain(["node", fileURLToPath(moduleUrl)], moduleUrl, root); assert.equal(process.exitCode, 1); } finally { - process.exitCode = previous; + process.exitCode = previous ?? 0; rmSync(root, { recursive: true, force: true }); } }); diff --git a/test/generate-changelog.test.ts b/test/generate-changelog.test.ts index d96e8a41..162c4b1c 100644 --- a/test/generate-changelog.test.ts +++ b/test/generate-changelog.test.ts @@ -16,7 +16,7 @@ test("changelog main entry fails closed when the tracker cannot be read", async await generateChangelogIfMain(["node", fileURLToPath(moduleUrl)], moduleUrl, root, []); assert.equal(process.exitCode, 1); } finally { - process.exitCode = previous; + process.exitCode = previous ?? 0; fs.rmSync(root, { recursive: true, force: true }); } }); diff --git a/test/import-lock.test.ts b/test/import-lock.test.ts index b3da87d4..9dd754be 100644 --- a/test/import-lock.test.ts +++ b/test/import-lock.test.ts @@ -11,7 +11,7 @@ import { comments as readComments } from "@unbrained/pm-cli/sdk"; // asserts no duplicate comment markers result. import assert from "node:assert/strict"; -import test, { mock } from "node:test"; +import test from "node:test"; import { execFile, execFileSync, spawn, spawnSync } from "node:child_process"; import fs, { chmodSync, @@ -587,28 +587,45 @@ test("comment-sync lock degrades when the lock directory cannot be created or wr } }); +/** 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 closeMock = mock.method(fs, "closeSync", () => { + 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 = mock.method(fs, "unlinkSync", () => { + const unlinkMock = replaceMethod(fs, "unlinkSync", () => { + unlinkCalls++; throw Object.assign(new Error("unlink failed"), { code: "EIO" }); }); - const writeMock = mock.method(fs, "writeFileSync", ((target: fs.PathOrFileDescriptor, data: string) => { - if (typeof target === "number") throw Object.assign(new Error("write 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(closeMock.mock.calls.length > 0); - assert.ok(unlinkMock.mock.calls.length > 0); + assert.ok(closeCalls > 0); + assert.ok(unlinkCalls > 0); } finally { - writeMock.mock.restore(); - closeMock.mock.restore(); - unlinkMock.mock.restore(); + writeMock(); + closeMock(); + unlinkMock(); + if (descriptor !== undefined) originalClose(descriptor); rmSync(root, { recursive: true, force: true }); } }); @@ -633,7 +650,7 @@ test("a vanished breaker sidecar is ignored and a vanished lock is treated as al let stats = 0; const original = fs.statSync; - const statMock = mock.method(fs, "statSync", ((target: fs.PathLike, options?: fs.StatSyncOptions) => { + const statMock = replaceMethod(fs, "statSync", ((target: fs.PathLike, options?: fs.StatSyncOptions) => { stats++; if (stats >= 2 && String(target) === lockPath) { rmSync(lockPath, { force: true }); @@ -646,7 +663,7 @@ test("a vanished breaker sidecar is ignored and a vanished lock is treated as al assert.equal(acq.status, "acquired"); if (acq.status === "acquired") acq.lock.release(); } finally { - statMock.mock.restore(); + statMock(); } } finally { rmSync(root, { recursive: true, force: true }); @@ -679,7 +696,7 @@ test("a peer replacing a stale lock before the breaker recheck remains protected utimesSync(lockPath, new Date(0), new Date(0)); let checked = 0; const original = fs.statSync; - const boundary = mock.method(fs, "statSync", ((target: fs.PathLike, options?: fs.StatSyncOptions) => { + 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() })); } @@ -688,7 +705,7 @@ test("a peer replacing a stale lock before the breaker recheck remains protected try { assert.equal((await acquireImportLock(root, "pm-peer", { waitMs: 80 })).status, "contended"); assert.match(readFileSync(lockPath, "utf8"), /live-peer/); - } finally { boundary.mock.restore(); rmSync(root, { recursive: true, force: true }); } + } finally { boundary(); rmSync(root, { recursive: true, force: true }); } }); test("real permission failures during stale unlink and token release retain the lock", async () => { @@ -699,7 +716,7 @@ test("real permission failures during stale unlink and token release retain the utimesSync(lockPath, new Date(0), new Date(0)); let checked = 0; const original = fs.statSync; - const boundary = mock.method(fs, "statSync", ((target: fs.PathLike, options?: fs.StatSyncOptions) => { + 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); @@ -707,7 +724,7 @@ test("real permission failures during stale unlink and token release retain the assert.equal((await acquireImportLock(root, "pm-permission", { waitMs: 80 })).status, "contended"); assert.equal(existsSync(lockPath), true); chmodSync(dirname(lockPath), 0o755); - boundary.mock.restore(); + boundary(); rmSync(lockPath); rmSync(`${lockPath}.break`, { force: true }); const acq = await acquireImportLock(root, "pm-permission", { waitMs: 80 }); @@ -718,6 +735,6 @@ test("real permission failures during stale unlink and token release retain the assert.equal(existsSync(lockPath), true); } } finally { - boundary.mock.restore(); chmodSync(dirname(lockPath), 0o755); rmSync(root, { recursive: true, force: true }); + boundary(); chmodSync(dirname(lockPath), 0o755); rmSync(root, { recursive: true, force: true }); } }); diff --git a/test/privacy-gate.test.ts b/test/privacy-gate.test.ts index fce7fa25..11f72c66 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 @@ -28,9 +30,7 @@ import { fileURLToPath } from "node:url"; import { extractEmail, gitBlobOid, - listAllObjects, type PrivacyGateResult, - readObject, runGate, runPrivacyGateIfMain, main, @@ -390,79 +390,72 @@ test("privacy gate main entry audits the supplied root", () => { assert.match(stderr, /cannot read/); } finally { process.stderr.write = original; - process.exitCode = previous; + 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"]); - const previous = process.env.PATH; - process.env.PATH = ""; try { - 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)); - } finally { - if (previous === undefined) delete process.env.PATH; - else process.env.PATH = previous; - rmSync(root, { recursive: true, force: true }); - } + 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", () => { 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 bin = mkdtempSync(join(tmpdir(), "pm-github-privacy-git-")); 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 -if [ "$1" = "cat-file" ] && [ "$2" = "blob" ]; then - echo forced-read-failure >&2 - exit 1 -fi exec "$REAL_GIT" "$@" `, { mode: 0o755 }); chmodSync(join(bin, "git"), 0o755); - const previous = process.env.PATH; - const previousGit = process.env.REAL_GIT; - process.env.PATH = `${bin}${delimiter}${previous ?? ""}`; - - process.env.REAL_GIT = realGit; - try { + 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]!; - // Point the wrapper at real inventory so the scan reaches the blob read. - writeFileSync(join(bin, "git"), `#!/bin/sh -if [ "$1" = "cat-file" ] && [ "$2" = "blob" ]; then - echo forced-read-failure >&2 - exit 1 -fi -exec "$REAL_GIT" "$@" -`, { mode: 0o755 }); - assert.throws(() => readObject(root, "blob", oid), /forced-read-failure/); + 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/); - } finally { - if (previous === undefined) delete process.env.PATH; - else process.env.PATH = previous; - if (previousGit === undefined) delete process.env.REAL_GIT; - else process.env.REAL_GIT = previousGit; - rmSync(bin, { recursive: true, force: true }); - } + `, { PATH: bin + delimiter + process.env.PATH, REAL_GIT: realGit }); } finally { + rmSync(bin, { recursive: true, force: true }); rmSync(root, { recursive: true, force: true }); } }); From fd0834a8a5a5839eac94f4bcb7cadd126c6e7389 Mon Sep 17 00:00:00 2001 From: SteveBot <1153461+unbraind@users.noreply.github.com> Date: Tue, 6 Oct 2026 15:00:21 +0200 Subject: [PATCH 24/60] test: isolate workflow packing from caller tracker --- .agents/pm/history/pm-github-hptv.jsonl | 10 +++++ .agents/pm/issues/pm-github-hptv.toon | 59 ++++++++++++++++++++++--- test/import-gate.test.ts | 53 +++++++++++++++++++++- 3 files changed, 116 insertions(+), 6 deletions(-) diff --git a/.agents/pm/history/pm-github-hptv.jsonl b/.agents/pm/history/pm-github-hptv.jsonl index 6c4fdd5b..2a3524a5 100644 --- a/.agents/pm/history/pm-github-hptv.jsonl +++ b/.agents/pm/history/pm-github-hptv.jsonl @@ -23,3 +23,13 @@ {"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"} diff --git a/.agents/pm/issues/pm-github-hptv.toon b/.agents/pm/issues/pm-github-hptv.toon index 343a7b5f..06523246 100644 --- a/.agents/pm/issues/pm-github-hptv.toon +++ b/.agents/pm/issues/pm-github-hptv.toon @@ -6,9 +6,11 @@ status: in_progress priority: 1 tags: [] created_at: "2026-10-05T08:28:54.625Z" -updated_at: "2026-10-06T12:42:49.423Z" +updated_at: "2026-10-06T12:59:01.408Z" +assignee: codex-sol +claim_principal: codex-sol author: codex-sol -comments[12]{created_at,author,text}: +comments[17]{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." @@ -21,6 +23,11 @@ comments[12]{created_at,author,text}: "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." files[6]{path,scope}: index.ts,project test/helpers/bounded-cli.ts,project @@ -28,9 +35,51 @@ files[6]{path,scope}: test/http-boundary.test.ts,project test/import-gate.test.ts,project test/import-lock.test.ts,project -tests[2]{command,scope,timeout_seconds,provenance{author,created_at,source_kind,source_ref}}: - node --test test/bounded-cli.test.ts,project,60,codex-sol,"2026-10-06T11:37:04.782Z",local_mutation,feat/import-privacy-gate - "node --test --test-name-pattern=\"requestOnce fails a request\" test/http-boundary.test.ts",project,60,codex-sol,"2026-10-06T11:56:36.862Z",local_mutation,feat/import-privacy-gate +tests[6]: + - 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 docs[1]{path,scope}: README.md,project body: "" diff --git a/test/import-gate.test.ts b/test/import-gate.test.ts index 78b53b47..b1241a96 100644 --- a/test/import-gate.test.ts +++ b/test/import-gate.test.ts @@ -677,6 +677,15 @@ function executeWorkflow(shell: string, root: string, env: NodeJS.ProcessEnv): P }); } +/** 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"); @@ -706,7 +715,7 @@ esac 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 executeWorkflow(`npm pack --ignore-scripts --json --pack-destination '${base.replace(/'/g, "'\\''")}'`, packageRoot, env); + 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, "'\\''"); @@ -720,6 +729,48 @@ esac 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" }, From a849280e361e2f2d99fff27a97ff31574cd912ed Mon Sep 17 00:00:00 2001 From: SteveBot <1153461+unbraind@users.noreply.github.com> Date: Tue, 6 Oct 2026 15:09:41 +0200 Subject: [PATCH 25/60] test: cover gh credential fallback without host auth --- .agents/pm/history/pm-github-hptv.jsonl | 5 +++++ .agents/pm/issues/pm-github-hptv.toon | 19 ++++++++++++---- test/coverage-edges.test.ts | 30 +++++++++++++++++++++++++ 3 files changed, 50 insertions(+), 4 deletions(-) diff --git a/.agents/pm/history/pm-github-hptv.jsonl b/.agents/pm/history/pm-github-hptv.jsonl index 2a3524a5..a158601c 100644 --- a/.agents/pm/history/pm-github-hptv.jsonl +++ b/.agents/pm/history/pm-github-hptv.jsonl @@ -33,3 +33,8 @@ {"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"} diff --git a/.agents/pm/issues/pm-github-hptv.toon b/.agents/pm/issues/pm-github-hptv.toon index 06523246..eb5907ff 100644 --- a/.agents/pm/issues/pm-github-hptv.toon +++ b/.agents/pm/issues/pm-github-hptv.toon @@ -6,11 +6,11 @@ status: in_progress priority: 1 tags: [] created_at: "2026-10-05T08:28:54.625Z" -updated_at: "2026-10-06T12:59:01.408Z" +updated_at: "2026-10-06T13:09:41.700Z" assignee: codex-sol claim_principal: codex-sol author: codex-sol -comments[17]{created_at,author,text}: +comments[20]{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." @@ -28,14 +28,18 @@ comments[17]{created_at,author,text}: "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." -files[6]{path,scope}: + "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." +files[7]{path,scope}: index.ts,project + test/coverage-edges.test.ts,project test/helpers/bounded-cli.ts,project test/helpers/public-acceptance.ts,project test/http-boundary.test.ts,project test/import-gate.test.ts,project test/import-lock.test.ts,project -tests[6]: +tests[7]: - command: node --test test/bounded-cli.test.ts scope: project timeout_seconds: 60 @@ -80,6 +84,13 @@ tests[6]: 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 docs[1]{path,scope}: README.md,project body: "" diff --git a/test/coverage-edges.test.ts b/test/coverage-edges.test.ts index 00e9d3a0..961b6a11 100644 --- a/test/coverage-edges.test.ts +++ b/test/coverage-edges.test.ts @@ -240,3 +240,33 @@ test("public validate warns at low unauthenticated quota without consulting host 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"); + })); +}); From 2f04c9a31f4f63b51778f73411e267b701c88db9 Mon Sep 17 00:00:00 2001 From: SteveBot <1153461+unbraind@users.noreply.github.com> Date: Tue, 6 Oct 2026 15:28:18 +0200 Subject: [PATCH 26/60] docs(pm): record fresh-clone release gate results --- .agents/pm/history/pm-github-hptv.jsonl | 3 +++ .agents/pm/issues/pm-github-hptv.toon | 8 ++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/.agents/pm/history/pm-github-hptv.jsonl b/.agents/pm/history/pm-github-hptv.jsonl index a158601c..7d05b49b 100644 --- a/.agents/pm/history/pm-github-hptv.jsonl +++ b/.agents/pm/history/pm-github-hptv.jsonl @@ -38,3 +38,6 @@ {"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"} diff --git a/.agents/pm/issues/pm-github-hptv.toon b/.agents/pm/issues/pm-github-hptv.toon index eb5907ff..8ab723e4 100644 --- a/.agents/pm/issues/pm-github-hptv.toon +++ b/.agents/pm/issues/pm-github-hptv.toon @@ -6,11 +6,9 @@ status: in_progress priority: 1 tags: [] created_at: "2026-10-05T08:28:54.625Z" -updated_at: "2026-10-06T13:09:41.700Z" -assignee: codex-sol -claim_principal: codex-sol +updated_at: "2026-10-06T13:28:15.458Z" author: codex-sol -comments[20]{created_at,author,text}: +comments[22]{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." @@ -31,6 +29,8 @@ comments[20]{created_at,author,text}: "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." files[7]{path,scope}: index.ts,project test/coverage-edges.test.ts,project From 7a6a1f098a4b3757748a8e40cc1035909d1282b5 Mon Sep 17 00:00:00 2001 From: SteveBot <1153461+unbraind@users.noreply.github.com> Date: Tue, 6 Oct 2026 16:00:16 +0200 Subject: [PATCH 27/60] fix: address round-one import gate review findings --- .agents/pm/history/pm-github-9cjx.jsonl | 6 +++ .agents/pm/history/pm-github-hptv.jsonl | 5 +++ .agents/pm/issues/pm-github-hptv.toon | 22 ++++++++-- .agents/pm/tasks/pm-github-9cjx.toon | 24 +++++++--- .github/workflows/pm-github-sync.yml | 32 +++++++++----- README.md | 2 +- gate.ts | 17 ++++---- index.ts | 6 +-- scripts/coverage-gate.ts | 6 +-- scripts/generate-changelog.ts | 15 +++++-- scripts/privacy-gate.ts | 4 +- test/coverage-gate.test.ts | 10 +++++ test/coverage-remainder.test.ts | 18 +++++--- test/gate-inputs.test.ts | 32 ++++++++++++++ test/gate.test.ts | 48 ++++++++++---------- test/generate-changelog.test.ts | 8 ++++ test/http-boundary.test.ts | 13 +++--- test/import-gate.test.ts | 58 +++++++++++++++++++++++-- test/link-deps.test.ts | 7 ++- test/privacy-gate.test.ts | 22 ++++++++++ test/review-round1.test.ts | 50 +++++++++++++++++++++ 21 files changed, 318 insertions(+), 87 deletions(-) create mode 100644 test/review-round1.test.ts diff --git a/.agents/pm/history/pm-github-9cjx.jsonl b/.agents/pm/history/pm-github-9cjx.jsonl index b185722a..2d2dee16 100644 --- a/.agents/pm/history/pm-github-9cjx.jsonl +++ b/.agents/pm/history/pm-github-9cjx.jsonl @@ -63,3 +63,9 @@ {"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"} diff --git a/.agents/pm/history/pm-github-hptv.jsonl b/.agents/pm/history/pm-github-hptv.jsonl index 7d05b49b..c4d23566 100644 --- a/.agents/pm/history/pm-github-hptv.jsonl +++ b/.agents/pm/history/pm-github-hptv.jsonl @@ -41,3 +41,8 @@ {"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"} diff --git a/.agents/pm/issues/pm-github-hptv.toon b/.agents/pm/issues/pm-github-hptv.toon index 8ab723e4..b7b734f4 100644 --- a/.agents/pm/issues/pm-github-hptv.toon +++ b/.agents/pm/issues/pm-github-hptv.toon @@ -6,9 +6,11 @@ status: in_progress priority: 1 tags: [] created_at: "2026-10-05T08:28:54.625Z" -updated_at: "2026-10-06T13:28:15.458Z" +updated_at: "2026-10-06T13:58:29.574Z" +assignee: codex-sol +claim_principal: codex-sol author: codex-sol -comments[22]{created_at,author,text}: +comments[24]{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." @@ -31,15 +33,20 @@ comments[22]{created_at,author,text}: "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." -files[7]{path,scope}: + "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." +files[10]{path,scope}: + .github/workflows/pm-github-sync.yml,project + gate.ts,project index.ts,project test/coverage-edges.test.ts,project + test/gate-inputs.test.ts,project test/helpers/bounded-cli.ts,project test/helpers/public-acceptance.ts,project test/http-boundary.test.ts,project test/import-gate.test.ts,project test/import-lock.test.ts,project -tests[7]: +tests[8]: - command: node --test test/bounded-cli.test.ts scope: project timeout_seconds: 60 @@ -91,6 +98,13 @@ tests[7]: 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 docs[1]{path,scope}: README.md,project body: "" diff --git a/.agents/pm/tasks/pm-github-9cjx.toon b/.agents/pm/tasks/pm-github-9cjx.toon index 0b4bdcb2..1d67ae2e 100644 --- a/.agents/pm/tasks/pm-github-9cjx.toon +++ b/.agents/pm/tasks/pm-github-9cjx.toon @@ -6,16 +6,18 @@ status: in_progress priority: 1 tags[5]: all-source,coverage,quality-gate,scripts,tdd created_at: "2026-08-17T07:32:29.666Z" -updated_at: "2026-10-06T12:42:48.449Z" +updated_at: "2026-10-06T14:00:16.233Z" +assignee: codex-sol +claim_principal: codex-sol 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: Full source coverage closure is being reverified at unchanged exact thresholds. 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: "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." +actual_result: "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." 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 -comments[29]{created_at,author,text}: +comments[31]{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." @@ -45,13 +47,17 @@ comments[29]{created_at,author,text}: "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." -files[19]{path,scope}: + "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." +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 test/coverage-boundaries.test.ts,project test/coverage-edges.test.ts,project @@ -65,7 +71,8 @@ files[19]{path,scope}: test/lint-policy.test.ts,project test/project-sync.test.ts,project test/projects.test.ts,project -tests[4]: + test/review-round1.test.ts,project +tests[5]: - command: npm run coverage path: scripts/coverage-gate.ts scope: project @@ -95,6 +102,13 @@ tests[4]: 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 docs[3]: - path: "https://github.com/unbraind/pm-cli-companion/blob/main/.agents/pm/epics/pm-cli-website-ktip.toon" scope: global diff --git a/.github/workflows/pm-github-sync.yml b/.github/workflows/pm-github-sync.yml index 4f1ec0b3..e3c9e1f8 100644 --- a/.github/workflows/pm-github-sync.yml +++ b/.github/workflows/pm-github-sync.yml @@ -37,22 +37,19 @@ on: required: false default: "22.18.0" -# Least privilege for the job's needs: push the sync branch (contents: write) -# and create/update the review PR (pull-requests: write). Nothing else. -permissions: - contents: write - pull-requests: write - issues: read - concurrency: group: pm-github-sync-${{ inputs.repository }} cancel-in-progress: false 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: - GH_TOKEN: ${{ github.token }} REPOSITORY: ${{ inputs.repository }} SYNC_BRANCH: ${{ inputs.sync-branch }} PM_GITHUB_VERSION: ${{ inputs.pm-github-version }} @@ -63,7 +60,7 @@ jobs: run: | set -euo pipefail [[ "${REPOSITORY}" == "${CALLING_REPOSITORY}" ]] - [[ "${PM_GITHUB_VERSION}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] + [[ "${PM_GITHUB_VERSION}" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9]+)?$ ]] [[ "${SYNC_BRANCH}" == automation/* ]] git check-ref-format "refs/heads/${SYNC_BRANCH}" @@ -73,6 +70,7 @@ jobs: fetch-depth: 0 fetch-tags: true ref: main + persist-credentials: false - name: Setup Node.js uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 @@ -89,8 +87,7 @@ jobs: set -euo pipefail git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git fetch --force origin main - sync_lease="$(git ls-remote --heads origin "refs/heads/${SYNC_BRANCH}" | cut -f1)" + 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 @@ -99,7 +96,7 @@ jobs: shell: bash run: | set -euo pipefail - npm ci + npm ci --ignore-scripts printf '%s/node_modules/.bin\n' "${GITHUB_WORKSPACE}" >> "${GITHUB_PATH}" - name: Install pinned pm-github extension @@ -116,17 +113,25 @@ jobs: 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 @@ -145,6 +150,8 @@ jobs: 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 @@ -170,6 +177,7 @@ jobs: 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" { diff --git a/README.md b/README.md index e6446ee8..2d01bfd1 100644 --- a/README.md +++ b/README.md @@ -91,7 +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`](#privacy-gate-pm-github-gate) AFTER it. Any finding — or any unreadable scan input — exits non-zero so an automated pipeline can never commit or push the change. Embeds `gate` receipts in the result. | +| `--gate` | boolean | Fail-closed privacy gate: verify plan completeness (every fetched issue accounted for exactly once) and provenance idempotency (every re-import lands on exactly one `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) | diff --git a/gate.ts b/gate.ts index 4373db78..468b3b5b 100644 --- a/gate.ts +++ b/gate.ts @@ -513,13 +513,13 @@ export function parseUnifiedDiff(diffText: string): ChangeFile[] { 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("--- ")) { + if (raw.startsWith("diff --git ") || (raw.startsWith("--- ") && !(inHunk && oldRemaining > 0))) { completeHunk(); flushFile(); filePath = ""; continue; } - if (raw.startsWith("+++ ")) { + if (raw.startsWith("+++ ") && !(inHunk && newRemaining > 0)) { completeHunk(); flushFile(); filePath = raw.slice(4).split("\t")[0]!.trim(); @@ -634,14 +634,14 @@ function isOperationalTrackerPath(repoRelative: string): boolean { * @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], { encoding: "utf-8", maxBuffer: 64 * 1024 * 1024 }); + 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: result.stdout ?? "", - stderr: result.stderr ?? String(result.error), + stdout: new TextDecoder("utf-8", { fatal: true }).decode(result.stdout ?? Buffer.alloc(0)), + stderr: result.stderr === null ? String(result.error) : new TextDecoder("utf-8", { fatal: true }).decode(result.stderr), }; } @@ -683,8 +683,9 @@ export function collectTrackerChange( "Run inside the repository, or pass --diff to scan an explicit diff.", ); } - const repoRoot = toplevel.stdout.trim(); - const trackerRel = path.relative(repoRoot, pmDataDir); + 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.", @@ -706,7 +707,7 @@ export function collectTrackerChange( // 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(pmDataDir, path.join(repoRoot, filePath)))) continue; + if (xy === "??" && isOperationalTrackerPath(path.relative(trackerDir, path.join(repoRoot, filePath)))) continue; if (xy === "??") { const absolute = path.join(repoRoot, filePath); let size = 0; diff --git a/index.ts b/index.ts index 89f405f0..b77307ef 100644 --- a/index.ts +++ b/index.ts @@ -1633,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); } @@ -3090,7 +3090,7 @@ export async function runImport( gatePlanByNumber = new Map(planned.map((entry) => [entry.issueNumber, entry])); skipped = skippedNumbers.length; if (opts.gate && !opts.dryRun) { - const preview = runTrackerGate({ pmRoot, plannedItems: planned.map(entry => ({ + 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, diff --git a/scripts/coverage-gate.ts b/scripts/coverage-gate.ts index 6069ef9d..5543a186 100644 --- a/scripts/coverage-gate.ts +++ b/scripts/coverage-gate.ts @@ -9,7 +9,7 @@ import { spawnSync, type SpawnSyncOptions } from "node:child_process"; import { mkdirSync, readdirSync, readFileSync, rmSync, statSync } from "node:fs"; import { createRequire } from "node:module"; -import { dirname, join, relative, resolve, sep } from "node:path"; +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. */ @@ -49,8 +49,8 @@ const DIMENSIONS = ["lines", "statements", "functions", "branches"] as const; export function collectCoverageSources(root: string, target: string): string[] { const location = resolve(root, target); const repoRelative = relative(root, location); - if (repoRelative.startsWith("..") || repoRelative === "") { - if (repoRelative !== "") throw new Error("Coverage source escapes the repository."); + 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."); diff --git a/scripts/generate-changelog.ts b/scripts/generate-changelog.ts index 50835841..c3506f34 100644 --- a/scripts/generate-changelog.ts +++ b/scripts/generate-changelog.ts @@ -1,10 +1,13 @@ /** Generate canonical PM release history plus explicitly tagged pending work. */ -import { spawnSync } from "node:child_process"; +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. * @@ -13,20 +16,24 @@ import { isMainInvocation } from "./main-invocation.ts"; * * @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[]): Promise { +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 = spawnSync("node", [cli, "--stdin", "--pm-root", ".agents/pm", "--status", [...new Set(["closed", ...items.map(item => item.status)])].join(","), + 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) return 1; + 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; diff --git a/scripts/privacy-gate.ts b/scripts/privacy-gate.ts index c13054d7..a9ec4351 100644 --- a/scripts/privacy-gate.ts +++ b/scripts/privacy-gate.ts @@ -180,8 +180,8 @@ function loadFixtureExemptions(root: string): Map { 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 = spawnSync("git", ["ls-tree", source.commit, "--", source.path], { cwd: root, encoding: "utf8" }); - if (resolved.status !== 0 || !resolved.stdout.startsWith(`100644 blob ${oid}\t`)) { + 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); diff --git a/test/coverage-gate.test.ts b/test/coverage-gate.test.ts index 236c8a85..450dcbdb 100644 --- a/test/coverage-gate.test.ts +++ b/test/coverage-gate.test.ts @@ -125,3 +125,13 @@ test("real c8 requires both branches and rejects a completely unloaded authored 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 index 92db96f7..294a076a 100644 --- a/test/coverage-remainder.test.ts +++ b/test/coverage-remainder.test.ts @@ -349,13 +349,17 @@ test("comment sync skips a contended lock and reports a per-comment write failur const itemFile = fs.readdirSync(path.join(root, ".agents", "pm", "tasks"))[0]; assert.ok(itemFile); - fs.chmodSync(path.join(root, ".agents", "pm", "tasks"), 0o555); - const failed = await captureStderr(() => syncGithubCommentsToAnnotations(itemId, [ - { id: 5, body: "", user: null, created_at: "" }, - ], path.join(root, ".agents", "pm"), 7)); - fs.chmodSync(path.join(root, ".agents", "pm", "tasks"), 0o755); - assert.equal(failed.result.added, 0); - assert.match(failed.stderr.join("\n"), /sync failed/); + const tasksDir = path.join(root, ".agents", "pm", "tasks"); + fs.chmodSync(tasksDir, 0o555); + try { + 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 { + fs.chmodSync(tasksDir, 0o755); + } } finally { holder.kill("SIGKILL"); fs.rmSync(root, { recursive: true, force: true }); diff --git a/test/gate-inputs.test.ts b/test/gate-inputs.test.ts index 92859350..bf47043c 100644 --- a/test/gate-inputs.test.ts +++ b/test/gate-inputs.test.ts @@ -78,3 +78,35 @@ test("explicit diff failures preserve gate diagnostics and wrap other filesystem 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.test.ts b/test/gate.test.ts index 8a8e06e1..085e7430 100644 --- a/test/gate.test.ts +++ b/test/gate.test.ts @@ -176,7 +176,7 @@ 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 = "Zj9kP2mQ7xW4nB8vC5tR1sD"; +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"; @@ -491,14 +491,17 @@ test("collectTrackerChange fails closed outside a Git work tree and on Git failu () => runTrackerGate({ pmRoot: "/unused", dependencies: { runGit: failingGit } }), GateInputError, ); - const statusFailingGit = (cwd: string, args: readonly string[]) => - args.includes("status") - ? { ok: false, stdout: "", stderr: "status boom" } - : { ok: true, stdout: path.resolve(cwd), stderr: "" }; - assert.throws( - () => runTrackerGate({ pmRoot: "/unused", dependencies: { runGit: statusFailingGit } }), - 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", () => { @@ -528,23 +531,16 @@ test("the default runner is the real git subprocess contract", () => { test("collectTrackerChange fails closed when the tracker escapes the Git work tree", () => { - assert.throws( - () => - runTrackerGate({ - pmRoot: "/deep/outside/path", - dependencies: { - runGit: (cwd, args) => - args.includes("rev-parse") - ? { ok: true, stdout: path.join(os.tmpdir(), "elsewhere"), stderr: "" } - : { ok: false, stdout: "", stderr: "unused" }, - }, - }), - (err: unknown) => { - assert.ok(err instanceof GateInputError); - assert.match(err.message, /escapes the Git work tree/); - return true; - }, - ); + 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", () => { diff --git a/test/generate-changelog.test.ts b/test/generate-changelog.test.ts index 162c4b1c..b6b81a71 100644 --- a/test/generate-changelog.test.ts +++ b/test/generate-changelog.test.ts @@ -46,9 +46,17 @@ test("canonical changelog includes tagged pending work without closing it or adm 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); + assert.ok(diagnostics.some(line => line.includes("could not spawn"))); + process.env.PATH = previousPath; + 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`); diff --git a/test/http-boundary.test.ts b/test/http-boundary.test.ts index 2eea9aaa..4e5d8fc6 100644 --- a/test/http-boundary.test.ts +++ b/test/http-boundary.test.ts @@ -391,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"); @@ -407,8 +407,7 @@ test("fetchComments tolerates a malformed page mid-stream (keeps earlier pages)" 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/); }); }); @@ -588,19 +587,17 @@ test("parseRateLimit drops non-numeric header values", async () => { assert.equal(parseRateLimit({ "x-ratelimit-remaining": ["4"] }).low, true); }); -test("fetchComments stops at a non-array comments page but keeps earlier pages", async () => { +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 well-formed JSON object (not an array) must end the walk without - // throwing and without losing the already-collected page. + // A later non-array page must refuse the incomplete read. jsonResponse(res, 200, { message: "unexpected shape" }); } }, async () => { const issue: GhIssue = { ...ghIssue(1), comments: 2 }; - const comments = await fetchComments(issue, "a/b", "t"); - assert.deepEqual(comments.map((c) => c.id), [1]); + await assert.rejects(fetchComments(issue, "a/b", "t"), /must be an array/); }); }); diff --git a/test/import-gate.test.ts b/test/import-gate.test.ts index b1241a96..9af85ce3 100644 --- a/test/import-gate.test.ts +++ b/test/import-gate.test.ts @@ -588,11 +588,11 @@ test("the reusable sync workflow is a workflow_call with explicit pinned inputs" 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"); - // Least privilege: exactly the two permissions a gated sync needs. - const permissions = /^permissions:\n contents: write\n pull-requests: write\n/m; + // 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, + (workflow.match(/^ permissions:/gm) ?? []).length === 1, "no second, broader permission block", ); }); @@ -695,6 +695,7 @@ async function prepareWorkflowFixture(root: string, base: string): Promise { + 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); + } +}); + +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/link-deps.test.ts b/test/link-deps.test.ts index ab9632a6..b973ac42 100644 --- a/test/link-deps.test.ts +++ b/test/link-deps.test.ts @@ -53,8 +53,8 @@ function importOpts(overrides: Partial = {}): ImportOptions { dryRun: false, atomic: false, linkDeps: true, - ...overrides, gate: false, + ...overrides, }; } @@ -448,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/privacy-gate.test.ts b/test/privacy-gate.test.ts index 11f72c66..cbbc6376 100644 --- a/test/privacy-gate.test.ts +++ b/test/privacy-gate.test.ts @@ -478,3 +478,25 @@ 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", () => { + 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 }); + writeFileSync(join(manifestDir, "manifest.json"), JSON.stringify({ ["b".repeat(40)]: { 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 }); + } finally { rmSync(root, { recursive: true, force: true }); } +}); diff --git a/test/review-round1.test.ts b/test/review-round1.test.ts new file mode 100644 index 00000000..7dfd2819 --- /dev/null +++ b/test/review-round1.test.ts @@ -0,0 +1,50 @@ +/** 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"; + +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("comment failure fixture restores directory permissions even when sync throws", async () => { + const source = fs.readFileSync(path.join(repoRoot, "test/coverage-remainder.test.ts"), "utf8"); + const modern = source.indexOf(' const tasksDir ='); + const start = modern >= 0 ? modern : source.indexOf(' fs.chmodSync(path.join(root, ".agents", "pm", "tasks"), 0o555);'); + assert.ok(start >= 0, "permission fixture exists"); + const end = source.indexOf('\n } finally {', start); + const block = source.slice(start, end); + const root = fs.mkdtempSync(path.join(os.tmpdir(), "pm-github-cleanup-")); + const tasksDir = path.join(root, ".agents/pm/tasks"); + fs.mkdirSync(tasksDir, { recursive: true }); + try { + const invoke = new Function("fs", "path", "root", "captureStderr", "syncGithubCommentsToAnnotations", "itemId", "assert", `return (async () => { ${block} })();`); + await assert.rejects(invoke(fs, path, root, () => Promise.reject(new Error("forced sync failure")), () => {}, "fixture", assert), /forced sync failure/); + assert.equal(fs.statSync(tasksDir).mode & 0o777, 0o755); + } finally { + fs.chmodSync(tasksDir, 0o755); + fs.rmSync(root, { recursive: true, force: true }); + } +}); From c8b9329bff3a0cb8a32b06cfae92479acb9afa5d Mon Sep 17 00:00:00 2001 From: SteveBot <1153461+unbraind@users.noreply.github.com> Date: Tue, 6 Oct 2026 16:06:25 +0200 Subject: [PATCH 28/60] fix: preserve missing Git diagnostics with fatal decoding --- .agents/pm/history/pm-github-hptv.jsonl | 1 + .agents/pm/issues/pm-github-hptv.toon | 5 +++-- gate.ts | 2 +- test/http-boundary.test.ts | 6 +++--- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/.agents/pm/history/pm-github-hptv.jsonl b/.agents/pm/history/pm-github-hptv.jsonl index c4d23566..5ac86cbd 100644 --- a/.agents/pm/history/pm-github-hptv.jsonl +++ b/.agents/pm/history/pm-github-hptv.jsonl @@ -46,3 +46,4 @@ {"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"} diff --git a/.agents/pm/issues/pm-github-hptv.toon b/.agents/pm/issues/pm-github-hptv.toon index b7b734f4..96de1963 100644 --- a/.agents/pm/issues/pm-github-hptv.toon +++ b/.agents/pm/issues/pm-github-hptv.toon @@ -6,11 +6,11 @@ status: in_progress priority: 1 tags: [] created_at: "2026-10-05T08:28:54.625Z" -updated_at: "2026-10-06T13:58:29.574Z" +updated_at: "2026-10-06T14:06:25.566Z" assignee: codex-sol claim_principal: codex-sol author: codex-sol -comments[24]{created_at,author,text}: +comments[25]{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." @@ -35,6 +35,7 @@ comments[24]{created_at,author,text}: "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." files[10]{path,scope}: .github/workflows/pm-github-sync.yml,project gate.ts,project diff --git a/gate.ts b/gate.ts index 468b3b5b..464d289d 100644 --- a/gate.ts +++ b/gate.ts @@ -641,7 +641,7 @@ export function runGitDefault(cwd: string, args: readonly string[]): { ok: boole // 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 === null ? String(result.error) : new TextDecoder("utf-8", { fatal: true }).decode(result.stderr), + stderr: result.stderr ? new TextDecoder("utf-8", { fatal: true }).decode(result.stderr) : String(result.error), }; } diff --git a/test/http-boundary.test.ts b/test/http-boundary.test.ts index 4e5d8fc6..db9efbd2 100644 --- a/test/http-boundary.test.ts +++ b/test/http-boundary.test.ts @@ -16,8 +16,8 @@ // 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). @@ -516,7 +516,7 @@ test("githubApiBase strips a trailing slash so paths cannot double up", async () // --------------------------------------------------------------------------- // Coverage additions: transport dispatch, redirect validation, the 30s // request timeout, array/mixed-case header shapes, and the comments non-array -// break — the remaining reachable arms of the shared request stack. +// 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 () => { From f572382e64985bd6f18bd917ebeb320363006461 Mon Sep 17 00:00:00 2001 From: SteveBot <1153461+unbraind@users.noreply.github.com> Date: Tue, 6 Oct 2026 16:20:43 +0200 Subject: [PATCH 29/60] test: verify portable diagnostics and record review receipts --- .agents/pm/history/pm-github-9cjx.jsonl | 5 +++++ .agents/pm/history/pm-github-hptv.jsonl | 3 +++ .agents/pm/issues/pm-github-hptv.toon | 8 ++++---- .agents/pm/tasks/pm-github-9cjx.toon | 19 +++++++++++++------ test/generate-changelog.test.ts | 3 ++- 5 files changed, 27 insertions(+), 11 deletions(-) diff --git a/.agents/pm/history/pm-github-9cjx.jsonl b/.agents/pm/history/pm-github-9cjx.jsonl index 2d2dee16..54131368 100644 --- a/.agents/pm/history/pm-github-9cjx.jsonl +++ b/.agents/pm/history/pm-github-9cjx.jsonl @@ -69,3 +69,8 @@ {"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"} diff --git a/.agents/pm/history/pm-github-hptv.jsonl b/.agents/pm/history/pm-github-hptv.jsonl index 5ac86cbd..ceedbbba 100644 --- a/.agents/pm/history/pm-github-hptv.jsonl +++ b/.agents/pm/history/pm-github-hptv.jsonl @@ -47,3 +47,6 @@ {"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"} diff --git a/.agents/pm/issues/pm-github-hptv.toon b/.agents/pm/issues/pm-github-hptv.toon index 96de1963..98c7a737 100644 --- a/.agents/pm/issues/pm-github-hptv.toon +++ b/.agents/pm/issues/pm-github-hptv.toon @@ -6,11 +6,9 @@ status: in_progress priority: 1 tags: [] created_at: "2026-10-05T08:28:54.625Z" -updated_at: "2026-10-06T14:06:25.566Z" -assignee: codex-sol -claim_principal: codex-sol +updated_at: "2026-10-06T14:20:42.267Z" author: codex-sol -comments[25]{created_at,author,text}: +comments[27]{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." @@ -36,6 +34,8 @@ comments[25]{created_at,author,text}: "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." files[10]{path,scope}: .github/workflows/pm-github-sync.yml,project gate.ts,project diff --git a/.agents/pm/tasks/pm-github-9cjx.toon b/.agents/pm/tasks/pm-github-9cjx.toon index 1d67ae2e..c585471e 100644 --- a/.agents/pm/tasks/pm-github-9cjx.toon +++ b/.agents/pm/tasks/pm-github-9cjx.toon @@ -6,18 +6,16 @@ status: in_progress priority: 1 tags[5]: all-source,coverage,quality-gate,scripts,tdd created_at: "2026-08-17T07:32:29.666Z" -updated_at: "2026-10-06T14:00:16.233Z" -assignee: codex-sol -claim_principal: codex-sol +updated_at: "2026-10-06T14:20:43.544Z" 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: Full source coverage closure is being reverified at unchanged exact thresholds. 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: "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." +actual_result: "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." 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 -comments[31]{created_at,author,text}: +comments[33]{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." @@ -49,6 +47,8 @@ comments[31]{created_at,author,text}: "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." files[22]{path,scope}: eslint.config.js,project package.json,project @@ -72,7 +72,7 @@ files[22]{path,scope}: test/project-sync.test.ts,project test/projects.test.ts,project test/review-round1.test.ts,project -tests[5]: +tests[6]: - command: npm run coverage path: scripts/coverage-gate.ts scope: project @@ -109,6 +109,13 @@ tests[5]: created_at: "2026-10-06T13:58:20.374Z" source_kind: local_mutation source_ref: feat/import-privacy-gate + - 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 docs[3]: - path: "https://github.com/unbraind/pm-cli-companion/blob/main/.agents/pm/epics/pm-cli-website-ktip.toon" scope: global diff --git a/test/generate-changelog.test.ts b/test/generate-changelog.test.ts index b6b81a71..a8b4111a 100644 --- a/test/generate-changelog.test.ts +++ b/test/generate-changelog.test.ts @@ -51,8 +51,9 @@ test("canonical changelog includes tagged pending work without closing it or adm console.error = (...values: unknown[]) => { diagnostics.push(values.join(" ")); }; try { assert.equal(await generateChangelog(root, []), 1); - assert.ok(diagnostics.some(line => line.includes("could not spawn"))); 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 { From 5aab5c2946df8bc9ae3a48b06492872a51bbc70f Mon Sep 17 00:00:00 2001 From: SteveBot <1153461+unbraind@users.noreply.github.com> Date: Tue, 6 Oct 2026 16:33:50 +0200 Subject: [PATCH 30/60] docs(pm): record final gates and Bun runner follow-up --- .agents/pm/history/pm-github-2ao1.jsonl | 4 ++++ .agents/pm/history/pm-github-9cjx.jsonl | 4 ++++ .agents/pm/history/pm-github-hptv.jsonl | 1 + .agents/pm/issues/pm-github-2ao1.toon | 21 +++++++++++++++++++++ .agents/pm/issues/pm-github-hptv.toon | 5 +++-- .agents/pm/tasks/pm-github-9cjx.toon | 11 ++++++----- 6 files changed, 39 insertions(+), 7 deletions(-) create mode 100644 .agents/pm/history/pm-github-2ao1.jsonl create mode 100644 .agents/pm/issues/pm-github-2ao1.toon diff --git a/.agents/pm/history/pm-github-2ao1.jsonl b/.agents/pm/history/pm-github-2ao1.jsonl new file mode 100644 index 00000000..7daddeb8 --- /dev/null +++ b/.agents/pm/history/pm-github-2ao1.jsonl @@ -0,0 +1,4 @@ +{"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"} diff --git a/.agents/pm/history/pm-github-9cjx.jsonl b/.agents/pm/history/pm-github-9cjx.jsonl index 54131368..ca852a3a 100644 --- a/.agents/pm/history/pm-github-9cjx.jsonl +++ b/.agents/pm/history/pm-github-9cjx.jsonl @@ -74,3 +74,7 @@ {"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"} diff --git a/.agents/pm/history/pm-github-hptv.jsonl b/.agents/pm/history/pm-github-hptv.jsonl index ceedbbba..f8818f83 100644 --- a/.agents/pm/history/pm-github-hptv.jsonl +++ b/.agents/pm/history/pm-github-hptv.jsonl @@ -50,3 +50,4 @@ {"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"} diff --git a/.agents/pm/issues/pm-github-2ao1.toon b/.agents/pm/issues/pm-github-2ao1.toon new file mode 100644 index 00000000..d7121d31 --- /dev/null +++ b/.agents/pm/issues/pm-github-2ao1.toon @@ -0,0 +1,21 @@ +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-06T14:32:03.841Z" +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[1]{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." +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-hptv.toon b/.agents/pm/issues/pm-github-hptv.toon index 98c7a737..0a6f65aa 100644 --- a/.agents/pm/issues/pm-github-hptv.toon +++ b/.agents/pm/issues/pm-github-hptv.toon @@ -6,9 +6,9 @@ status: in_progress priority: 1 tags: [] created_at: "2026-10-05T08:28:54.625Z" -updated_at: "2026-10-06T14:20:42.267Z" +updated_at: "2026-10-06T14:33:50.266Z" author: codex-sol -comments[27]{created_at,author,text}: +comments[28]{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." @@ -36,6 +36,7 @@ comments[27]{created_at,author,text}: "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." files[10]{path,scope}: .github/workflows/pm-github-sync.yml,project gate.ts,project diff --git a/.agents/pm/tasks/pm-github-9cjx.toon b/.agents/pm/tasks/pm-github-9cjx.toon index c585471e..117a009a 100644 --- a/.agents/pm/tasks/pm-github-9cjx.toon +++ b/.agents/pm/tasks/pm-github-9cjx.toon @@ -6,16 +6,16 @@ status: in_progress priority: 1 tags[5]: all-source,coverage,quality-gate,scripts,tdd created_at: "2026-08-17T07:32:29.666Z" -updated_at: "2026-10-06T14:20:43.544Z" +updated_at: "2026-10-06T14:33:49.525Z" 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: Full source coverage closure is being reverified at unchanged exact thresholds. 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: "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." +actual_result: 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. 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 -comments[33]{created_at,author,text}: +comments[34]{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." @@ -49,6 +49,7 @@ comments[33]{created_at,author,text}: "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." files[22]{path,scope}: eslint.config.js,project package.json,project @@ -109,11 +110,11 @@ tests[6]: created_at: "2026-10-06T13:58:20.374Z" source_kind: local_mutation source_ref: feat/import-privacy-gate - - command: "bun test --timeout 600000 -t \"canonical changelog includes|historical fixture provenance reports\" ./test/generate-changelog.test.ts ./test/privacy-gate.test.ts" + - 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:20:37.334Z" + created_at: "2026-10-06T14:30:35.002Z" source_kind: local_mutation source_ref: feat/import-privacy-gate docs[3]: From 7a41660f190a108cca4bc10e270eff6b4864aa3e Mon Sep 17 00:00:00 2001 From: SteveBot <1153461+unbraind@users.noreply.github.com> Date: Tue, 6 Oct 2026 17:19:32 +0200 Subject: [PATCH 31/60] fix: address privacy-gate review round two --- .agents/pm/history/pm-github-7d1h.jsonl | 1 + .agents/pm/history/pm-github-hptv.jsonl | 6 ++++++ .agents/pm/issues/pm-github-7d1h.toon | 3 ++- .agents/pm/issues/pm-github-hptv.toon | 24 +++++++++++++++++++----- .github/workflows/pm-github-sync.yml | 6 +++--- CHANGELOG.md | 7 ++++++- test/import-gate.test.ts | 17 ++++++++++++++++- 7 files changed, 53 insertions(+), 11 deletions(-) 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-hptv.jsonl b/.agents/pm/history/pm-github-hptv.jsonl index f8818f83..40ee4afe 100644 --- a/.agents/pm/history/pm-github-hptv.jsonl +++ b/.agents/pm/history/pm-github-hptv.jsonl @@ -51,3 +51,9 @@ {"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"} 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 index 0a6f65aa..c39d81ab 100644 --- a/.agents/pm/issues/pm-github-hptv.toon +++ b/.agents/pm/issues/pm-github-hptv.toon @@ -6,9 +6,11 @@ status: in_progress priority: 1 tags: [] created_at: "2026-10-05T08:28:54.625Z" -updated_at: "2026-10-06T14:33:50.266Z" +updated_at: "2026-10-06T15:19:21.145Z" +assignee: codex-sol +claim_principal: codex-sol author: codex-sol -comments[28]{created_at,author,text}: +comments[30]{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." @@ -37,10 +39,13 @@ comments[28]{created_at,author,text}: "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." -files[10]{path,scope}: + "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." +files[11]{path,scope}: .github/workflows/pm-github-sync.yml,project gate.ts,project index.ts,project + scripts/generate-changelog.ts,project test/coverage-edges.test.ts,project test/gate-inputs.test.ts,project test/helpers/bounded-cli.ts,project @@ -48,7 +53,7 @@ files[10]{path,scope}: test/http-boundary.test.ts,project test/import-gate.test.ts,project test/import-lock.test.ts,project -tests[8]: +tests[9]: - command: node --test test/bounded-cli.test.ts scope: project timeout_seconds: 60 @@ -107,6 +112,15 @@ tests[8]: created_at: "2026-10-06T13:58:19.539Z" source_kind: local_mutation source_ref: feat/import-privacy-gate -docs[1]{path,scope}: + - 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 +docs[2]{path,scope}: + CHANGELOG.md,project README.md,project body: "" diff --git a/.github/workflows/pm-github-sync.yml b/.github/workflows/pm-github-sync.yml index e3c9e1f8..72e3ac9e 100644 --- a/.github/workflows/pm-github-sync.yml +++ b/.github/workflows/pm-github-sync.yml @@ -59,9 +59,9 @@ jobs: shell: bash run: | set -euo pipefail - [[ "${REPOSITORY}" == "${CALLING_REPOSITORY}" ]] - [[ "${PM_GITHUB_VERSION}" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9]+)?$ ]] - [[ "${SYNC_BRANCH}" == automation/* ]] + [[ "${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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 307f03ec..8057e591 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -106,11 +106,16 @@ - Refuse incomplete pm item corpora before GitHub imports, exports, and syncs ([pm-github-ep0u](https://github.com/unbraind/pm-github/blob/main/.agents/pm/issues/pm-github-ep0u.toon)) -## 2026.8.16 - 2026-08-16 +## 2026.8.17 - 2026-08-17 ### Fixed - The manifest declared a pm CLI floor of 2026.7.28 while peerDependencies required 2026.8.3, so the CLI enforced a weaker minimum than npm ([pm-github-7d1h](https://github.com/unbraind/pm-github/blob/main/.agents/pm/issues/pm-github-7d1h.toon)) + +## 2026.8.16 - 2026-08-16 + +### Fixed + - A github command can silently lose its preflight credential gate when the override scope drifts from the mutating command set ([pm-github-4ga9](https://github.com/unbraind/pm-github/blob/main/.agents/pm/issues/pm-github-4ga9.toon)) ## 2026.8.15 - 2026-08-15 diff --git a/test/import-gate.test.ts b/test/import-gate.test.ts index 9af85ce3..6d5d71e5 100644 --- a/test/import-gate.test.ts +++ b/test/import-gate.test.ts @@ -457,7 +457,7 @@ test("adversarial: token in a body, token in a comment, email, and host path pre assert.equal((clean.result as { verdict: string }).verdict, "pass"); }); - assert.strictEqual(git(["status", "--porcelain"]).status, 0); + assert.strictEqual(git(["status", "--porcelain", "--", ".agents/pm"]).stdout.trim(), ""); } finally { fs.rmSync(base, { recursive: true, force: true }); } @@ -910,6 +910,21 @@ test("workflow input shell accepts numeric same-day pins and refuses floating ve ...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); } }); From 88e069afd7aa2639babc11007a441a4316b79075 Mon Sep 17 00:00:00 2001 From: SteveBot <1153461+unbraind@users.noreply.github.com> Date: Tue, 6 Oct 2026 17:36:26 +0200 Subject: [PATCH 32/60] chore(pm): record review round two validation receipts --- .agents/pm/history/pm-github-hptv.jsonl | 3 +++ .agents/pm/issues/pm-github-hptv.toon | 17 ++++++++++++----- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/.agents/pm/history/pm-github-hptv.jsonl b/.agents/pm/history/pm-github-hptv.jsonl index 40ee4afe..8c17d72b 100644 --- a/.agents/pm/history/pm-github-hptv.jsonl +++ b/.agents/pm/history/pm-github-hptv.jsonl @@ -57,3 +57,6 @@ {"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"} diff --git a/.agents/pm/issues/pm-github-hptv.toon b/.agents/pm/issues/pm-github-hptv.toon index c39d81ab..c4d8f4a5 100644 --- a/.agents/pm/issues/pm-github-hptv.toon +++ b/.agents/pm/issues/pm-github-hptv.toon @@ -6,11 +6,9 @@ status: in_progress priority: 1 tags: [] created_at: "2026-10-05T08:28:54.625Z" -updated_at: "2026-10-06T15:19:21.145Z" -assignee: codex-sol -claim_principal: codex-sol +updated_at: "2026-10-06T15:36:26.117Z" author: codex-sol -comments[30]{created_at,author,text}: +comments[31]{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." @@ -41,6 +39,7 @@ comments[30]{created_at,author,text}: "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." files[11]{path,scope}: .github/workflows/pm-github-sync.yml,project gate.ts,project @@ -53,7 +52,7 @@ files[11]{path,scope}: test/http-boundary.test.ts,project test/import-gate.test.ts,project test/import-lock.test.ts,project -tests[9]: +tests[10]: - command: node --test test/bounded-cli.test.ts scope: project timeout_seconds: 60 @@ -120,6 +119,14 @@ tests[9]: 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 docs[2]{path,scope}: CHANGELOG.md,project README.md,project From c91a89cf9f824b628ab75b6065cf10e12199f1f7 Mon Sep 17 00:00:00 2001 From: SteveBot <1153461+unbraind@users.noreply.github.com> Date: Tue, 6 Oct 2026 17:38:01 +0200 Subject: [PATCH 33/60] Link the upstream Bun empty_run issue on the runner follow-up (pm-github-2ao1) Reproduced on PM CLI 2026.10.6 with Bun 1.3.5 and filed as unbraind/pm-cli#1418. --- .agents/pm/history/pm-github-2ao1.jsonl | 1 + .agents/pm/issues/pm-github-2ao1.toon | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.agents/pm/history/pm-github-2ao1.jsonl b/.agents/pm/history/pm-github-2ao1.jsonl index 7daddeb8..2f0f1327 100644 --- a/.agents/pm/history/pm-github-2ao1.jsonl +++ b/.agents/pm/history/pm-github-2ao1.jsonl @@ -2,3 +2,4 @@ {"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/issues/pm-github-2ao1.toon b/.agents/pm/issues/pm-github-2ao1.toon index d7121d31..830d72ff 100644 --- a/.agents/pm/issues/pm-github-2ao1.toon +++ b/.agents/pm/issues/pm-github-2ao1.toon @@ -6,13 +6,14 @@ status: open priority: 2 tags[3]: bun,testing,upstream created_at: "2026-10-06T14:30:31.652Z" -updated_at: "2026-10-06T14:32:03.841Z" +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[1]{created_at,author,text}: +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 From 5a49f625890e4d89ec1847cedeffd02bcffd1c8d Mon Sep 17 00:00:00 2001 From: SteveBot <1153461+unbraind@users.noreply.github.com> Date: Tue, 6 Oct 2026 19:04:54 +0200 Subject: [PATCH 34/60] Close the verified coverage task and give the allowlist test its own temp directory CodeRabbit review 5431943706: - pm-github-9cjx and pm-github-f1at still carried "in progress" and "being reverified" notes after their own comments recorded the passing exact-100 receipts. The orchestrator verified the gate (CI Node 22 and 26, fresh-clone npm and Bun release:check) and closed pm-github-9cjx; both unblock notes now state the final state, and the coverage test note no longer says the item stays open. Edited through the pm CLI. - The allowlist test wrote a fixed pid-named file into the shared temp directory; it now uses mkdtempSync like the rest of the file and removes the directory afterwards. The changelog lists the closed task under Unreleased. --- .agents/pm/features/pm-github-f1at.toon | 4 ++-- .agents/pm/history/pm-github-9cjx.jsonl | 4 ++++ .agents/pm/history/pm-github-f1at.jsonl | 1 + .agents/pm/tasks/pm-github-9cjx.toon | 24 ++++++++++++++++-------- CHANGELOG.md | 1 + test/gate.test.ts | 5 +++-- 6 files changed, 27 insertions(+), 12 deletions(-) diff --git a/.agents/pm/features/pm-github-f1at.toon b/.agents/pm/features/pm-github-f1at.toon index e147a6ce..950cc184 100644 --- a/.agents/pm/features/pm-github-f1at.toon +++ b/.agents/pm/features/pm-github-f1at.toon @@ -6,11 +6,11 @@ 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-06T12:42:50.295Z" +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: Final exact coverage and npm/Bun release checks are underway; PR 108 remains open for review. +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 diff --git a/.agents/pm/history/pm-github-9cjx.jsonl b/.agents/pm/history/pm-github-9cjx.jsonl index ca852a3a..cdc5eb7a 100644 --- a/.agents/pm/history/pm-github-9cjx.jsonl +++ b/.agents/pm/history/pm-github-9cjx.jsonl @@ -78,3 +78,7 @@ {"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"} diff --git a/.agents/pm/history/pm-github-f1at.jsonl b/.agents/pm/history/pm-github-f1at.jsonl index 56841dd0..bca1dd73 100644 --- a/.agents/pm/history/pm-github-f1at.jsonl +++ b/.agents/pm/history/pm-github-f1at.jsonl @@ -47,3 +47,4 @@ {"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/tasks/pm-github-9cjx.toon b/.agents/pm/tasks/pm-github-9cjx.toon index 117a009a..5e1a522f 100644 --- a/.agents/pm/tasks/pm-github-9cjx.toon +++ b/.agents/pm/tasks/pm-github-9cjx.toon @@ -2,15 +2,17 @@ id: pm-github-9cjx title: Reach exact all-source coverage and include every operational script description: "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." type: Task -status: in_progress +status: closed priority: 1 tags[5]: all-source,coverage,quality-gate,scripts,tdd created_at: "2026-08-17T07:32:29.666Z" -updated_at: "2026-10-06T14:33:49.525Z" +updated_at: "2026-10-06T17:04:30.299Z" +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: Full source coverage closure is being reverified at unchanged exact thresholds. +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: 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. dependencies[1]{id,kind,created_at,author,source_kind,author_source}: @@ -74,11 +76,6 @@ files[22]{path,scope}: test/projects.test.ts,project test/review-round1.test.ts,project tests[6]: - - 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. - command: node --test test/project-sync.test.ts test/projects.test.ts scope: project timeout_seconds: 600 @@ -117,6 +114,16 @@ tests[6]: 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 @@ -126,4 +133,5 @@ docs[3]: 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/CHANGELOG.md b/CHANGELOG.md index 8057e591..80a23919 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ ### 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/test/gate.test.ts b/test/gate.test.ts index 085e7430..c24978e8 100644 --- a/test/gate.test.ts +++ b/test/gate.test.ts @@ -616,7 +616,8 @@ function sha256Hex(value: string): string { } test("an allowlisted content hash is suppressed and counted, never widened to a pattern", () => { - const allowlist = path.join(os.tmpdir(), `gate-allowlist-${process.pid}.json`); + const allowlistDirectory = fs.mkdtempSync(path.join(os.tmpdir(), "gate-allowlist-")); + const allowlist = path.join(allowlistDirectory, "allowlist.json"); fs.writeFileSync( allowlist, JSON.stringify({ @@ -644,7 +645,7 @@ test("an allowlisted content hash is suppressed and counted, never widened to a ["npm-token"], ); } finally { - fs.rmSync(allowlist, { force: true }); + fs.rmSync(allowlistDirectory, { recursive: true, force: true }); } }); From b446963be37f4999fc74a6ca361da3e6a016ef2d Mon Sep 17 00:00:00 2001 From: SteveBot <1153461+unbraind@users.noreply.github.com> Date: Tue, 6 Oct 2026 20:39:21 +0200 Subject: [PATCH 35/60] fix: address privacy-gate review round three CodeRabbit review round 3 (comments 4198811214, 4198811219, 4198811222, 4198811228): - pm-github-9cjx: actual_result now cites the re-measured 504/504 exact-100 receipt matching the close_reason; the 503/503 count was the stale f572382 receipt. Item stays closed. Follow-up comment records which receipt is authoritative. - docs/coverage-gap-inventory.md: the closing 487-test receipts are marked historical; a current-status line records the closed item and the measured 504/505 counts. - test/import-lock.test.ts: permission-based lock fixtures (chmod 0o555) skip visibly via node:test { skip } when running as root, reusing the existing non-root pattern. The degraded-mode test is split so its file-parent half still runs everywhere. - test/review-round1.test.ts: the source-slicing new Function test is replaced by a behavioral test of a new exported withReadOnlyDirectory helper (test/helpers) that restores the previous mode in a finally block; coverage-remainder now uses the same helper. - pm-github-rap0 records the same root-sensitivity in the helper-based comment-sync fixture as a follow-up. Fresh coverage at this head: 505/505 tests, zero failures or skips, exact 100/100/100/100 across the 12 authored modules. --- .agents/pm/history/pm-github-9cjx.jsonl | 2 ++ .agents/pm/history/pm-github-hptv.jsonl | 4 +++ .agents/pm/history/pm-github-rap0.jsonl | 2 ++ .agents/pm/issues/pm-github-hptv.toon | 23 +++++++++++++--- .agents/pm/issues/pm-github-rap0.toon | 13 +++++++++ .agents/pm/tasks/pm-github-9cjx.toon | 7 ++--- docs/coverage-gap-inventory.md | 11 ++++++++ test/coverage-remainder.test.ts | 8 +++--- test/helpers/read-only-directory.ts | 26 ++++++++++++++++++ test/import-lock.test.ts | 35 +++++++++++++++---------- test/review-round1.test.ts | 32 +++++++++++----------- 11 files changed, 123 insertions(+), 40 deletions(-) create mode 100644 .agents/pm/history/pm-github-rap0.jsonl create mode 100644 .agents/pm/issues/pm-github-rap0.toon create mode 100644 test/helpers/read-only-directory.ts diff --git a/.agents/pm/history/pm-github-9cjx.jsonl b/.agents/pm/history/pm-github-9cjx.jsonl index cdc5eb7a..c2caeccf 100644 --- a/.agents/pm/history/pm-github-9cjx.jsonl +++ b/.agents/pm/history/pm-github-9cjx.jsonl @@ -82,3 +82,5 @@ {"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"} diff --git a/.agents/pm/history/pm-github-hptv.jsonl b/.agents/pm/history/pm-github-hptv.jsonl index 8c17d72b..7c406b0e 100644 --- a/.agents/pm/history/pm-github-hptv.jsonl +++ b/.agents/pm/history/pm-github-hptv.jsonl @@ -60,3 +60,7 @@ {"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"} diff --git a/.agents/pm/history/pm-github-rap0.jsonl b/.agents/pm/history/pm-github-rap0.jsonl new file mode 100644 index 00000000..01a4aba7 --- /dev/null +++ b/.agents/pm/history/pm-github-rap0.jsonl @@ -0,0 +1,2 @@ +{"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"} diff --git a/.agents/pm/issues/pm-github-hptv.toon b/.agents/pm/issues/pm-github-hptv.toon index c4d8f4a5..46df59a4 100644 --- a/.agents/pm/issues/pm-github-hptv.toon +++ b/.agents/pm/issues/pm-github-hptv.toon @@ -6,9 +6,11 @@ status: in_progress priority: 1 tags: [] created_at: "2026-10-05T08:28:54.625Z" -updated_at: "2026-10-06T15:36:26.117Z" +updated_at: "2026-10-06T18:23:44.899Z" +assignee: pi-gh-cr3b +claim_principal: pi-gh-cr3b author: codex-sol -comments[31]{created_at,author,text}: +comments[32]{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." @@ -40,6 +42,7 @@ comments[31]{created_at,author,text}: "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." files[11]{path,scope}: .github/workflows/pm-github-sync.yml,project gate.ts,project @@ -52,7 +55,7 @@ files[11]{path,scope}: test/http-boundary.test.ts,project test/import-gate.test.ts,project test/import-lock.test.ts,project -tests[10]: +tests[12]: - command: node --test test/bounded-cli.test.ts scope: project timeout_seconds: 60 @@ -127,6 +130,20 @@ tests[10]: 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 docs[2]{path,scope}: CHANGELOG.md,project README.md,project diff --git a/.agents/pm/issues/pm-github-rap0.toon b/.agents/pm/issues/pm-github-rap0.toon new file mode 100644 index 00000000..5a06b033 --- /dev/null +++ b/.agents/pm/issues/pm-github-rap0.toon @@ -0,0 +1,13 @@ +id: pm-github-rap0 +title: "The comment-sync per-comment write-failure test is root-sensitive: withReadOnlyDirectory cannot make the tasks directory unwritable for root" +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: open +priority: 2 +tags[3]: ci,root,tests +created_at: "2026-10-06T18:29:11.654Z" +updated_at: "2026-10-06T18:29:15.904Z" +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." +body: "" diff --git a/.agents/pm/tasks/pm-github-9cjx.toon b/.agents/pm/tasks/pm-github-9cjx.toon index 5e1a522f..d134d73f 100644 --- a/.agents/pm/tasks/pm-github-9cjx.toon +++ b/.agents/pm/tasks/pm-github-9cjx.toon @@ -6,7 +6,7 @@ status: closed priority: 1 tags[5]: all-source,coverage,quality-gate,scripts,tdd created_at: "2026-08-17T07:32:29.666Z" -updated_at: "2026-10-06T17:04:30.299Z" +updated_at: "2026-10-06T18:17:36.953Z" closed_at: "2026-10-06T17:04:30.277Z" completed_at: "2026-10-06T17:04:30.277Z" author: codex @@ -14,10 +14,10 @@ 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: 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. +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 -comments[34]{created_at,author,text}: +comments[35]{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." @@ -52,6 +52,7 @@ comments[34]{created_at,author,text}: "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." files[22]{path,scope}: eslint.config.js,project package.json,project diff --git a/docs/coverage-gap-inventory.md b/docs/coverage-gap-inventory.md index e03bc228..d2d869bd 100644 --- a/docs/coverage-gap-inventory.md +++ b/docs/coverage-gap-inventory.md @@ -92,6 +92,12 @@ 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 @@ -109,3 +115,8 @@ 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 closing head passes 504/504 tests; the current head +passes 505/505 with zero failures or skips. diff --git a/test/coverage-remainder.test.ts b/test/coverage-remainder.test.ts index 294a076a..6cbb8b3f 100644 --- a/test/coverage-remainder.test.ts +++ b/test/coverage-remainder.test.ts @@ -31,6 +31,7 @@ import extension, { type ImportOptions, } from "../index.ts"; import { nodeScenario } from "./helpers/node-scenario.ts"; +import { withReadOnlyDirectory } from "./helpers/read-only-directory.ts"; import { projectItemTag } from "../projects.ts"; import { captureStderr, jsonResponse, withEnv, withMockGithub } from "./helpers/mock-github-server.ts"; @@ -350,16 +351,13 @@ test("comment sync skips a contended lock and reports a per-comment write failur const itemFile = fs.readdirSync(path.join(root, ".agents", "pm", "tasks"))[0]; assert.ok(itemFile); const tasksDir = path.join(root, ".agents", "pm", "tasks"); - fs.chmodSync(tasksDir, 0o555); - try { + 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 { - fs.chmodSync(tasksDir, 0o755); - } + }); } finally { holder.kill("SIGKILL"); fs.rmSync(root, { 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/import-lock.test.ts b/test/import-lock.test.ts index 9dd754be..960e25d6 100644 --- a/test/import-lock.test.ts +++ b/test/import-lock.test.ts @@ -39,6 +39,11 @@ import { 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 { @@ -560,7 +565,7 @@ test("a lock that cannot be stat'd contends within the wait budget instead of sp } }); -test("comment-sync lock degrades when the lock directory cannot be created or written", async () => { +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"); @@ -570,23 +575,25 @@ test("comment-sync lock degrades when the lock directory cannot be created or wr assert.equal(acq.status, "degraded"); }); assert.ok(messages.some((line) => line.includes("lock unavailable")), messages.join(" | ")); - - 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 }); - } } 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]; @@ -708,7 +715,7 @@ test("a peer replacing a stale lock before the breaker recheck remains protected } finally { boundary(); rmSync(root, { recursive: true, force: true }); } }); -test("real permission failures during stale unlink and token release retain the lock", async () => { +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 }); diff --git a/test/review-round1.test.ts b/test/review-round1.test.ts index 7dfd2819..19c3416f 100644 --- a/test/review-round1.test.ts +++ b/test/review-round1.test.ts @@ -5,6 +5,8 @@ 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", () => { @@ -29,22 +31,22 @@ test("privacy fixtures never store the complete high-entropy secret literal", () } }); -test("comment failure fixture restores directory permissions even when sync throws", async () => { - const source = fs.readFileSync(path.join(repoRoot, "test/coverage-remainder.test.ts"), "utf8"); - const modern = source.indexOf(' const tasksDir ='); - const start = modern >= 0 ? modern : source.indexOf(' fs.chmodSync(path.join(root, ".agents", "pm", "tasks"), 0o555);'); - assert.ok(start >= 0, "permission fixture exists"); - const end = source.indexOf('\n } finally {', start); - const block = source.slice(start, end); - const root = fs.mkdtempSync(path.join(os.tmpdir(), "pm-github-cleanup-")); - const tasksDir = path.join(root, ".agents/pm/tasks"); - fs.mkdirSync(tasksDir, { recursive: true }); +test("withReadOnlyDirectory restores directory permissions even when the callback throws", async () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pm-github-cleanup-")); try { - const invoke = new Function("fs", "path", "root", "captureStderr", "syncGithubCommentsToAnnotations", "itemId", "assert", `return (async () => { ${block} })();`); - await assert.rejects(invoke(fs, path, root, () => Promise.reject(new Error("forced sync failure")), () => {}, "fixture", assert), /forced sync failure/); - assert.equal(fs.statSync(tasksDir).mode & 0o777, 0o755); + 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(tasksDir, 0o755); - fs.rmSync(root, { recursive: true, force: true }); + fs.chmodSync(dir, 0o755); + fs.rmSync(dir, { recursive: true, force: true }); } }); From a347534e450011eee523e42bc7e477528144582f Mon Sep 17 00:00:00 2001 From: SteveBot <1153461+unbraind@users.noreply.github.com> Date: Tue, 6 Oct 2026 20:54:44 +0200 Subject: [PATCH 36/60] chore(pm): record review round three validation receipts pm-github-hptv carries the round-3 dispositions, focused test receipts, linked files and the fresh-clone npm/Bun release:check results; pm-github-rap0 records the remaining root-sensitive helper-based fixture as a follow-up. --- .agents/pm/history/pm-github-hptv.jsonl | 8 +++++++ .agents/pm/issues/pm-github-hptv.toon | 30 ++++++++++++++++++++----- 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/.agents/pm/history/pm-github-hptv.jsonl b/.agents/pm/history/pm-github-hptv.jsonl index 7c406b0e..a675e0c9 100644 --- a/.agents/pm/history/pm-github-hptv.jsonl +++ b/.agents/pm/history/pm-github-hptv.jsonl @@ -64,3 +64,11 @@ {"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"} diff --git a/.agents/pm/issues/pm-github-hptv.toon b/.agents/pm/issues/pm-github-hptv.toon index 46df59a4..e7878f8e 100644 --- a/.agents/pm/issues/pm-github-hptv.toon +++ b/.agents/pm/issues/pm-github-hptv.toon @@ -6,11 +6,11 @@ status: in_progress priority: 1 tags: [] created_at: "2026-10-05T08:28:54.625Z" -updated_at: "2026-10-06T18:23:44.899Z" +updated_at: "2026-10-06T18:54:40.542Z" assignee: pi-gh-cr3b claim_principal: pi-gh-cr3b author: codex-sol -comments[32]{created_at,author,text}: +comments[33]{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." @@ -43,19 +43,24 @@ comments[32]{created_at,author,text}: "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." -files[11]{path,scope}: + "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." +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 -tests[12]: + test/review-round1.test.ts,project +tests[14]: - command: node --test test/bounded-cli.test.ts scope: project timeout_seconds: 60 @@ -144,7 +149,22 @@ tests[12]: created_at: "2026-10-06T18:23:44.820Z" source_kind: local_mutation source_ref: feat/import-privacy-gate -docs[2]{path,scope}: + - 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 +docs[3]{path,scope}: CHANGELOG.md,project + docs/coverage-gap-inventory.md,project README.md,project body: "" From 9a8620c0f9e698cc79fdc313e0fbd94fdc3659a4 Mon Sep 17 00:00:00 2001 From: SteveBot <1153461+unbraind@users.noreply.github.com> Date: Tue, 6 Oct 2026 20:54:57 +0200 Subject: [PATCH 37/60] chore(pm): release review round three items for orchestrator verification --- .agents/pm/history/pm-github-hptv.jsonl | 1 + .agents/pm/history/pm-github-rap0.jsonl | 1 + .agents/pm/issues/pm-github-hptv.toon | 4 +--- .agents/pm/issues/pm-github-rap0.toon | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.agents/pm/history/pm-github-hptv.jsonl b/.agents/pm/history/pm-github-hptv.jsonl index a675e0c9..139552bf 100644 --- a/.agents/pm/history/pm-github-hptv.jsonl +++ b/.agents/pm/history/pm-github-hptv.jsonl @@ -72,3 +72,4 @@ {"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"} diff --git a/.agents/pm/history/pm-github-rap0.jsonl b/.agents/pm/history/pm-github-rap0.jsonl index 01a4aba7..b9a8331a 100644 --- a/.agents/pm/history/pm-github-rap0.jsonl +++ b/.agents/pm/history/pm-github-rap0.jsonl @@ -1,2 +1,3 @@ {"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"} diff --git a/.agents/pm/issues/pm-github-hptv.toon b/.agents/pm/issues/pm-github-hptv.toon index e7878f8e..30591f8a 100644 --- a/.agents/pm/issues/pm-github-hptv.toon +++ b/.agents/pm/issues/pm-github-hptv.toon @@ -6,9 +6,7 @@ status: in_progress priority: 1 tags: [] created_at: "2026-10-05T08:28:54.625Z" -updated_at: "2026-10-06T18:54:40.542Z" -assignee: pi-gh-cr3b -claim_principal: pi-gh-cr3b +updated_at: "2026-10-06T18:54:49.993Z" author: codex-sol comments[33]{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." diff --git a/.agents/pm/issues/pm-github-rap0.toon b/.agents/pm/issues/pm-github-rap0.toon index 5a06b033..ac8aef3e 100644 --- a/.agents/pm/issues/pm-github-rap0.toon +++ b/.agents/pm/issues/pm-github-rap0.toon @@ -6,7 +6,7 @@ status: open priority: 2 tags[3]: ci,root,tests created_at: "2026-10-06T18:29:11.654Z" -updated_at: "2026-10-06T18:29:15.904Z" +updated_at: "2026-10-06T18:54:50.637Z" 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." From 7c425f2e5ff92b9e3e1c30d460cc71f72fee20ad Mon Sep 17 00:00:00 2001 From: SteveBot <1153461+unbraind@users.noreply.github.com> Date: Tue, 6 Oct 2026 22:00:29 +0200 Subject: [PATCH 38/60] Read tracker paths literally in the gate's git calls and align the caller example MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CodeRabbit full review of 9a8620c: - The gate's `git status` and `git diff` calls now pass --literal-pathspecs, so a tracker path can never be read as pathspec magic or a glob and come back as an empty, unscanned diff. With today's layout every tracker path starts with a directory, so leading magic was inert and git literal-matches existing glob-named files. This is defence in depth, and a guard test stages ":(attr:x)…" and "[ab]" named records holding a token and requires a fail verdict. - The README caller example no longer adds a caller-level concurrency block that docs/sync-workflow-caller.yml does not have; the reusable workflow already serializes per repository. - pm-github-9cjx's description now cites the closing 505-test receipt. Not changed: resolvePmDataDir keeps the SDK's resolvePmRoot, the canonical tracker resolution; a .agents/pm directory without settings.json is not a pm tracker. --- .agents/pm/history/pm-github-9cjx.jsonl | 1 + .agents/pm/history/pm-github-hptv.jsonl | 1 + .agents/pm/issues/pm-github-hptv.toon | 5 +++-- .agents/pm/tasks/pm-github-9cjx.toon | 4 ++-- README.md | 4 ---- gate.ts | 6 ++++-- test/gate.test.ts | 19 +++++++++++++++++++ 7 files changed, 30 insertions(+), 10 deletions(-) diff --git a/.agents/pm/history/pm-github-9cjx.jsonl b/.agents/pm/history/pm-github-9cjx.jsonl index c2caeccf..3c9d2587 100644 --- a/.agents/pm/history/pm-github-9cjx.jsonl +++ b/.agents/pm/history/pm-github-9cjx.jsonl @@ -84,3 +84,4 @@ {"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"} diff --git a/.agents/pm/history/pm-github-hptv.jsonl b/.agents/pm/history/pm-github-hptv.jsonl index 139552bf..e3ff830f 100644 --- a/.agents/pm/history/pm-github-hptv.jsonl +++ b/.agents/pm/history/pm-github-hptv.jsonl @@ -73,3 +73,4 @@ {"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"} diff --git a/.agents/pm/issues/pm-github-hptv.toon b/.agents/pm/issues/pm-github-hptv.toon index 30591f8a..d1d912d8 100644 --- a/.agents/pm/issues/pm-github-hptv.toon +++ b/.agents/pm/issues/pm-github-hptv.toon @@ -6,9 +6,9 @@ status: in_progress priority: 1 tags: [] created_at: "2026-10-05T08:28:54.625Z" -updated_at: "2026-10-06T18:54:49.993Z" +updated_at: "2026-10-06T19:48:30.209Z" author: codex-sol -comments[33]{created_at,author,text}: +comments[34]{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." @@ -42,6 +42,7 @@ comments[33]{created_at,author,text}: "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." files[15]{path,scope}: .github/workflows/pm-github-sync.yml,project docs/coverage-gap-inventory.md,project diff --git a/.agents/pm/tasks/pm-github-9cjx.toon b/.agents/pm/tasks/pm-github-9cjx.toon index d134d73f..e69ff350 100644 --- a/.agents/pm/tasks/pm-github-9cjx.toon +++ b/.agents/pm/tasks/pm-github-9cjx.toon @@ -1,12 +1,12 @@ id: pm-github-9cjx title: Reach exact all-source coverage and include every operational script -description: "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." +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. 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." type: Task status: closed priority: 1 tags[5]: all-source,coverage,quality-gate,scripts,tdd created_at: "2026-08-17T07:32:29.666Z" -updated_at: "2026-10-06T18:17:36.953Z" +updated_at: "2026-10-06T19:48:19.546Z" closed_at: "2026-10-06T17:04:30.277Z" completed_at: "2026-10-06T17:04:30.277Z" author: codex diff --git a/README.md b/README.md index 2d01bfd1..d8e68d86 100644 --- a/README.md +++ b/README.md @@ -310,10 +310,6 @@ permissions: pull-requests: write issues: read -concurrency: - group: pm-github-sync - cancel-in-progress: false - jobs: gated-sync: uses: unbraind/pm-github/.github/workflows/pm-github-sync.yml@RELEASE_COMMIT_SHA diff --git a/gate.ts b/gate.ts index 464d289d..3fd62886 100644 --- a/gate.ts +++ b/gate.ts @@ -693,7 +693,9 @@ export function collectTrackerChange( } const trackerSpec = trackerRel === "" ? "." : trackerRel; - const status = runGit(repoRoot, ["status", "--porcelain=v1", "-z", "--untracked-files=all", "--", trackerSpec]); + // 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."); } @@ -730,7 +732,7 @@ export function collectTrackerChange( if (xy.includes("D") && !/[AMU]/.test(xy)) continue; // pure deletion const addedLines: AddedLine[] = []; for (const revision of [["--cached"], []]) { - const diff = runGit(repoRoot, ["diff", "--no-color", "--no-ext-diff", "--no-textconv", "--no-renames", "--unified=2147483647", ...revision, "--", filePath]); + 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."); } diff --git a/test/gate.test.ts b/test/gate.test.ts index c24978e8..34d32fc2 100644 --- a/test/gate.test.ts +++ b/test/gate.test.ts @@ -422,6 +422,25 @@ test("scanLineForRuleHits reports every rule firing on one line without dedupe a // 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 { From ed75fde94b8665fd0e3a58e2d38c2e3b6b1fcde4 Mon Sep 17 00:00:00 2001 From: SteveBot <1153461+unbraind@users.noreply.github.com> Date: Tue, 6 Oct 2026 23:15:12 +0200 Subject: [PATCH 39/60] Anchor the host-path rule to host roots and scan patch pointers as data CodeRabbit full review of 7c425f2: - absolute-host-path matched any slash-prefixed word, so ordinary issue text failed gated imports: GitHub slash commands (/assign, /label bug), repository-relative links ([docs](/docs/setup.md)) and API routes (GET /api/v1). 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 (/homework is not /home), matching the README's documented scope. The new rule test fails on revert; existing home, tmp and srv fixtures stay detected. - With that anchoring, JSON-patch pointers (/metadata/..., /body) can no longer match any host-path rule, so the "structural pointer" exemption became unreachable. It is removed, and pointers are scanned like data, so a secret embedded in a field key is caught too. - The http-boundary timeout test now awaits server.close(). --- .agents/pm/history/pm-github-hptv.jsonl | 2 ++ .agents/pm/issues/pm-github-hptv.toon | 6 ++++-- gate.ts | 15 +++++++++++---- test/gate.test.ts | 10 ++++++++++ test/http-boundary.test.ts | 2 +- 5 files changed, 28 insertions(+), 7 deletions(-) diff --git a/.agents/pm/history/pm-github-hptv.jsonl b/.agents/pm/history/pm-github-hptv.jsonl index e3ff830f..6fdcd8df 100644 --- a/.agents/pm/history/pm-github-hptv.jsonl +++ b/.agents/pm/history/pm-github-hptv.jsonl @@ -74,3 +74,5 @@ {"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"} diff --git a/.agents/pm/issues/pm-github-hptv.toon b/.agents/pm/issues/pm-github-hptv.toon index d1d912d8..b4523a71 100644 --- a/.agents/pm/issues/pm-github-hptv.toon +++ b/.agents/pm/issues/pm-github-hptv.toon @@ -6,9 +6,9 @@ status: in_progress priority: 1 tags: [] created_at: "2026-10-05T08:28:54.625Z" -updated_at: "2026-10-06T19:48:30.209Z" +updated_at: "2026-10-06T21:03:24.422Z" author: codex-sol -comments[34]{created_at,author,text}: +comments[36]{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." @@ -43,6 +43,8 @@ comments[34]{created_at,author,text}: "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." files[15]{path,scope}: .github/workflows/pm-github-sync.yml,project docs/coverage-gap-inventory.md,project diff --git a/gate.ts b/gate.ts index 3fd62886..f0416b04 100644 --- a/gate.ts +++ b/gate.ts @@ -257,7 +257,12 @@ const PERSONAL_DATA_RULES: readonly ContentRule[] = [ const HOST_PATH_RULES: readonly ContentRule[] = [ { rule: "absolute-host-path", - pattern: /(?:^|[\s"'`([=,{])\/[A-Za-z0-9._-]+(?:\/[^\s"'`<>),;\]}]*)?/g, + // 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`). + pattern: /(?:^|[\s"'`([=,{])\/(?: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", @@ -831,7 +836,7 @@ function scanChangeFile(change: ChangeFile): Array = []; + 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. @@ -851,7 +856,10 @@ function scanChangeFile(change: ChangeFile): Array { + 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: absolute paths, windows paths, and home usernames", () => { assert.deepStrictEqual( scanLineForRuleHits(`crash at ${HOME_PATH}`).map((hit) => hit.rule), diff --git a/test/http-boundary.test.ts b/test/http-boundary.test.ts index db9efbd2..3d91aeff 100644 --- a/test/http-boundary.test.ts +++ b/test/http-boundary.test.ts @@ -555,7 +555,7 @@ test("requestOnce fails a request after 30s without a response", { timeout: 45_0 assert.ok(performance.now() - started < 40_000); } finally { server.closeAllConnections(); - server.close(); + await new Promise((resolve) => { server.close(() => resolve()); }); } }); From ae28d9e56d6e9fb4ce3658ff431e98445f904742 Mon Sep 17 00:00:00 2001 From: SteveBot <1153461+unbraind@users.noreply.github.com> Date: Tue, 6 Oct 2026 23:55:46 +0200 Subject: [PATCH 40/60] Exempt SSH clone accounts, keep the escape decoder linear, and fix the caller template CodeRabbit full review of ed75fde: - email-address flagged git@github.com in SSH clone URLs, which issue text routinely contains, and stopped gated imports. The fixed `git` local part is exempt like no-reply identities; gitlover@example.org stays a positive control. - The unicode-escape decoder sliced the line suffix for every character before an anchored match, which is quadratic where slices copy (native Bun) on large history lines. A sticky regex at the current offset keeps it linear. - The canonical caller (docs/sync-workflow-caller.yml and the README example) hardcoded `repository: unbraind/pm-graph`, which the reusable workflow refuses in any other repository. The input is omitted; it defaults to the calling repository. - pm-github-9cjx's description, actual_result and close_reason now agree on the closing receipt (504/504 at 7a41660); other counts are recorded as historical. --- .agents/pm/history/pm-github-9cjx.jsonl | 2 ++ .agents/pm/history/pm-github-hptv.jsonl | 1 + .agents/pm/issues/pm-github-hptv.toon | 5 +++-- .agents/pm/tasks/pm-github-9cjx.toon | 7 ++++--- README.md | 1 - docs/sync-workflow-caller.yml | 1 - gate.ts | 11 +++++++++-- test/gate.test.ts | 4 ++++ 8 files changed, 23 insertions(+), 9 deletions(-) diff --git a/.agents/pm/history/pm-github-9cjx.jsonl b/.agents/pm/history/pm-github-9cjx.jsonl index 3c9d2587..ac230517 100644 --- a/.agents/pm/history/pm-github-9cjx.jsonl +++ b/.agents/pm/history/pm-github-9cjx.jsonl @@ -85,3 +85,5 @@ {"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-hptv.jsonl b/.agents/pm/history/pm-github-hptv.jsonl index 6fdcd8df..a2229928 100644 --- a/.agents/pm/history/pm-github-hptv.jsonl +++ b/.agents/pm/history/pm-github-hptv.jsonl @@ -76,3 +76,4 @@ {"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"} diff --git a/.agents/pm/issues/pm-github-hptv.toon b/.agents/pm/issues/pm-github-hptv.toon index b4523a71..81c23baa 100644 --- a/.agents/pm/issues/pm-github-hptv.toon +++ b/.agents/pm/issues/pm-github-hptv.toon @@ -6,9 +6,9 @@ status: in_progress priority: 1 tags: [] created_at: "2026-10-05T08:28:54.625Z" -updated_at: "2026-10-06T21:03:24.422Z" +updated_at: "2026-10-06T21:45:35.057Z" author: codex-sol -comments[36]{created_at,author,text}: +comments[37]{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." @@ -45,6 +45,7 @@ comments[36]{created_at,author,text}: "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)." files[15]{path,scope}: .github/workflows/pm-github-sync.yml,project docs/coverage-gap-inventory.md,project diff --git a/.agents/pm/tasks/pm-github-9cjx.toon b/.agents/pm/tasks/pm-github-9cjx.toon index e69ff350..afc0de0d 100644 --- a/.agents/pm/tasks/pm-github-9cjx.toon +++ b/.agents/pm/tasks/pm-github-9cjx.toon @@ -1,12 +1,12 @@ id: pm-github-9cjx title: Reach exact all-source coverage and include every operational script -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. 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." +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: closed priority: 1 tags[5]: all-source,coverage,quality-gate,scripts,tdd created_at: "2026-08-17T07:32:29.666Z" -updated_at: "2026-10-06T19:48:19.546Z" +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 @@ -17,7 +17,7 @@ expected_result: "Every executable TypeScript source and operational script is a 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 -comments[35]{created_at,author,text}: +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." @@ -53,6 +53,7 @@ comments[35]{created_at,author,text}: "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 diff --git a/README.md b/README.md index d8e68d86..ba21fce3 100644 --- a/README.md +++ b/README.md @@ -314,7 +314,6 @@ jobs: gated-sync: uses: unbraind/pm-github/.github/workflows/pm-github-sync.yml@RELEASE_COMMIT_SHA with: - repository: unbraind/pm-graph # default: the calling repository pm-github-version: "RELEASE_VERSION" # required: exact published version ``` diff --git a/docs/sync-workflow-caller.yml b/docs/sync-workflow-caller.yml index 4489d009..528ff480 100644 --- a/docs/sync-workflow-caller.yml +++ b/docs/sync-workflow-caller.yml @@ -12,5 +12,4 @@ jobs: gated-sync: uses: unbraind/pm-github/.github/workflows/pm-github-sync.yml@RELEASE_COMMIT_SHA with: - repository: unbraind/pm-graph pm-github-version: "RELEASE_VERSION" diff --git a/gate.ts b/gate.ts index f0416b04..715456d7 100644 --- a/gate.ts +++ b/gate.ts @@ -220,7 +220,8 @@ const PERSONAL_DATA_RULES: readonly ContentRule[] = [ // person's address and fails closed. confirm: (matched) => { const [local, domain] = matched.toLowerCase().split("@"); - return !/^(?:no-?reply)$/.test(local!) && !/(?:^|\.)no-?reply(?:\.|$)/.test(domain!); + // `git@host` is the SSH service account in clone URLs, not a person. + return !/^(?:no-?reply|git)$/.test(local!) && !/(?:^|\.)no-?reply(?:\.|$)/.test(domain!); }, }, { @@ -357,6 +358,9 @@ interface RuleHit { 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. * @@ -374,7 +378,10 @@ export function scanLineForRuleHits(line: string): RuleHit[] { const offsets: number[] = []; let decoded = ""; for (let index = 0; index < line.length; index++) { - const unicode = /^\\u([0-9a-f]{4})/i.exec(line.slice(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); diff --git a/test/gate.test.ts b/test/gate.test.ts index af75f1c7..d1f3c51d 100644 --- a/test/gate.test.ts +++ b/test/gate.test.ts @@ -122,6 +122,10 @@ test("escaped tokens and Windows paths retain redacted findings", () => { 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")); assert.ok(scanLineForRuleHits("person-noreply@example.org").some(hit => hit.rule === "email-address")); }); From 194cf120fc671c1762beb1a9e31f4a631efe598b Mon Sep 17 00:00:00 2001 From: SteveBot <1153461+unbraind@users.noreply.github.com> Date: Wed, 7 Oct 2026 00:26:01 +0200 Subject: [PATCH 41/60] Exempt only SSH clone URLs, not every git@ address, from the email rule Greptile on ae28d9e: the previous change exempted any address whose local part is `git`, so "Contact git@example.org" cleared the gate and could be published without an allowlist entry. The exemption moves from confirm(), which only sees the matched address, into the pattern: a negative lookahead skips exactly the SCP SSH URL shape, git@host: followed directly by a path. A lookbehind keeps a match from starting inside a local part. git@ contact addresses, including "git@example.org: thanks" and "agit@github.com:x", are flagged again; the test fails on revert. --- .agents/pm/history/pm-github-hptv.jsonl | 1 + .agents/pm/issues/pm-github-hptv.toon | 5 +++-- gate.ts | 10 +++++++--- test/gate.test.ts | 4 ++++ 4 files changed, 15 insertions(+), 5 deletions(-) diff --git a/.agents/pm/history/pm-github-hptv.jsonl b/.agents/pm/history/pm-github-hptv.jsonl index a2229928..667de00b 100644 --- a/.agents/pm/history/pm-github-hptv.jsonl +++ b/.agents/pm/history/pm-github-hptv.jsonl @@ -77,3 +77,4 @@ {"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"} diff --git a/.agents/pm/issues/pm-github-hptv.toon b/.agents/pm/issues/pm-github-hptv.toon index 81c23baa..7e8eb524 100644 --- a/.agents/pm/issues/pm-github-hptv.toon +++ b/.agents/pm/issues/pm-github-hptv.toon @@ -6,9 +6,9 @@ status: in_progress priority: 1 tags: [] created_at: "2026-10-05T08:28:54.625Z" -updated_at: "2026-10-06T21:45:35.057Z" +updated_at: "2026-10-06T22:03:02.088Z" author: codex-sol -comments[37]{created_at,author,text}: +comments[38]{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." @@ -46,6 +46,7 @@ comments[37]{created_at,author,text}: "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." files[15]{path,scope}: .github/workflows/pm-github-sync.yml,project docs/coverage-gap-inventory.md,project diff --git a/gate.ts b/gate.ts index 715456d7..8fe74d2e 100644 --- a/gate.ts +++ b/gate.ts @@ -214,14 +214,18 @@ const CREDENTIAL_RULES: readonly ContentRule[] = [ const PERSONAL_DATA_RULES: readonly ContentRule[] = [ { rule: "email-address", - pattern: /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g, + // 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("@"); - // `git@host` is the SSH service account in clone URLs, not a person. - return !/^(?:no-?reply|git)$/.test(local!) && !/(?:^|\.)no-?reply(?:\.|$)/.test(domain!); + return !/^(?:no-?reply)$/.test(local!) && !/(?:^|\.)no-?reply(?:\.|$)/.test(domain!); }, }, { diff --git a/test/gate.test.ts b/test/gate.test.ts index d1f3c51d..0d6f74b0 100644 --- a/test/gate.test.ts +++ b/test/gate.test.ts @@ -126,6 +126,10 @@ test("escaped tokens and Windows paths retain redacted findings", () => { 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")); }); From d7041ad12b103b949d2b1e8b910e783a1577ef25 Mon Sep 17 00:00:00 2001 From: SteveBot <1153461+unbraind@users.noreply.github.com> Date: Wed, 7 Oct 2026 01:10:50 +0200 Subject: [PATCH 42/60] Fail closed on added diff lines without a target file CodeRabbit full review of 194cf12: parseUnifiedDiff skipped every line while no target file was set, so an operator-supplied --diff that put a hunk after a `diff --git` header with no ---/+++ pair, or after `+++ /dev/null`, passed with an added credential. Hunks are now parsed even without a target, and an added line without one fails the gate. Genuine deletions (only `-` lines after `+++ /dev/null`) still parse. Both shapes are in the malformed-diff test, which fails on revert. The two privacy-gate tests that write POSIX sh git wrappers now skip visibly on Windows (CI runs on Linux). --- .agents/pm/history/pm-github-hptv.jsonl | 1 + .agents/pm/issues/pm-github-hptv.toon | 5 +++-- gate.ts | 8 +++++++- test/gate.test.ts | 7 +++++++ test/privacy-gate.test.ts | 4 ++-- 5 files changed, 20 insertions(+), 5 deletions(-) diff --git a/.agents/pm/history/pm-github-hptv.jsonl b/.agents/pm/history/pm-github-hptv.jsonl index 667de00b..30a9379d 100644 --- a/.agents/pm/history/pm-github-hptv.jsonl +++ b/.agents/pm/history/pm-github-hptv.jsonl @@ -78,3 +78,4 @@ {"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"} diff --git a/.agents/pm/issues/pm-github-hptv.toon b/.agents/pm/issues/pm-github-hptv.toon index 7e8eb524..3ce50a0b 100644 --- a/.agents/pm/issues/pm-github-hptv.toon +++ b/.agents/pm/issues/pm-github-hptv.toon @@ -6,9 +6,9 @@ status: in_progress priority: 1 tags: [] created_at: "2026-10-05T08:28:54.625Z" -updated_at: "2026-10-06T22:03:02.088Z" +updated_at: "2026-10-06T22:59:21.029Z" author: codex-sol -comments[38]{created_at,author,text}: +comments[39]{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." @@ -47,6 +47,7 @@ comments[38]{created_at,author,text}: "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." files[15]{path,scope}: .github/workflows/pm-github-sync.yml,project docs/coverage-gap-inventory.md,project diff --git a/gate.ts b/gate.ts index 8fe74d2e..182c92c6 100644 --- a/gate.ts +++ b/gate.ts @@ -549,7 +549,9 @@ export function parseUnifiedDiff(diffText: string): ChangeFile[] { itemId = ""; continue; } - if (filePath === "") continue; // headers, index lines, and text before any +++ + // 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); @@ -566,6 +568,10 @@ export function parseUnifiedDiff(diffText: string): ChangeFile[] { 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); diff --git a/test/gate.test.ts b/test/gate.test.ts index 0d6f74b0..ffb280cf 100644 --- a/test/gate.test.ts +++ b/test/gate.test.ts @@ -114,6 +114,13 @@ test("malformed, truncated, and binary diffs fail closed", () => { 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", () => { diff --git a/test/privacy-gate.test.ts b/test/privacy-gate.test.ts index cbbc6376..f540d6c6 100644 --- a/test/privacy-gate.test.ts +++ b/test/privacy-gate.test.ts @@ -423,7 +423,7 @@ test("privacy gate fails closed when git cannot be spawned", () => { } finally { rmSync(root, { recursive: true, force: true }); } }); -test("privacy gate fails closed when object inventory or blob reads are unusable", () => { +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 { @@ -479,7 +479,7 @@ test("privacy gate negative control: a fresh violation introduced after a clean } }); -test("historical fixture provenance reports Git query failures", () => { +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"]); From 0f0c8c9697925be367b53cdd69d39798b4f688ad Mon Sep 17 00:00:00 2001 From: SteveBot <1153461+unbraind@users.noreply.github.com> Date: Wed, 7 Oct 2026 02:34:56 +0200 Subject: [PATCH 43/60] Keep extension-install setup state out of the sync commit CodeRabbit (review of d7041ad): when a caller's settings.json has an explicit extensions.enabled list without pm-github, installing the pinned extension adds it there and pm records that change in the workspace history. The repeat-import step staged both. A sync with no item changes then failed with "no item link could be generated", and a real sync would have put the setup change into the review PR. After the final gate the commit step restores settings.json and history/_workspace.jsonl from HEAD, or removes them when HEAD has neither. The new workflow test starts from main with an explicit enabled list, asserts that the install really edits settings.json, and requires a clean exit with no PR and no pushed branch; it fails on revert. The workflow also declares `permissions: {}` at the top level (zizmor excessive-permissions); the job keeps its least-privilege block. --- .agents/pm/history/pm-github-hptv.jsonl | 1 + .agents/pm/issues/pm-github-hptv.toon | 5 +++-- .github/workflows/pm-github-sync.yml | 15 +++++++++++++ test/import-gate.test.ts | 28 +++++++++++++++++++++++++ 4 files changed, 47 insertions(+), 2 deletions(-) diff --git a/.agents/pm/history/pm-github-hptv.jsonl b/.agents/pm/history/pm-github-hptv.jsonl index 30a9379d..7328db24 100644 --- a/.agents/pm/history/pm-github-hptv.jsonl +++ b/.agents/pm/history/pm-github-hptv.jsonl @@ -79,3 +79,4 @@ {"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"} diff --git a/.agents/pm/issues/pm-github-hptv.toon b/.agents/pm/issues/pm-github-hptv.toon index 3ce50a0b..bbe6aed7 100644 --- a/.agents/pm/issues/pm-github-hptv.toon +++ b/.agents/pm/issues/pm-github-hptv.toon @@ -6,9 +6,9 @@ status: in_progress priority: 1 tags: [] created_at: "2026-10-05T08:28:54.625Z" -updated_at: "2026-10-06T22:59:21.029Z" +updated_at: "2026-10-07T00:23:51.656Z" author: codex-sol -comments[39]{created_at,author,text}: +comments[40]{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." @@ -48,6 +48,7 @@ comments[39]{created_at,author,text}: "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." files[15]{path,scope}: .github/workflows/pm-github-sync.yml,project docs/coverage-gap-inventory.md,project diff --git a/.github/workflows/pm-github-sync.yml b/.github/workflows/pm-github-sync.yml index 72e3ac9e..47e88eb8 100644 --- a/.github/workflows/pm-github-sync.yml +++ b/.github/workflows/pm-github-sync.yml @@ -41,6 +41,9 @@ 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. @@ -156,6 +159,18 @@ jobs: run: | set -euo pipefail ./node_modules/.bin/pm github gate --json + # 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." exit 0 diff --git a/test/import-gate.test.ts b/test/import-gate.test.ts index 6d5d71e5..782f348f 100644 --- a/test/import-gate.test.ts +++ b/test/import-gate.test.ts @@ -845,6 +845,34 @@ test("the executable reusable workflow pushes only a clean import and creates or }); +test("an install-only settings change never reaches the sync commit", async () => { + 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); + await withMockGithub((req, res, url, server) => { + if (req.url === "/repos/acme/widgets") jsonResponse(res, 200, { private: false }); + else githubHandler([])(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.match(result.stdout, /no PR is needed/); + assert.equal(fs.existsSync(env.REVIEW_ACTIONS!), false, "no review PR was created or edited"); + assert.deepEqual(remoteRefs(bare), ["refs/heads/main"]); + }); + } finally { fs.rmSync(base, { recursive: true, force: true }); } +}); + 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 { From 15d4dc3e6e754d7a8de0f8cf53fd3e4896f03501 Mon Sep 17 00:00:00 2001 From: SteveBot <1153461+unbraind@users.noreply.github.com> Date: Wed, 7 Oct 2026 03:25:58 +0200 Subject: [PATCH 44/60] Document the review-PR repository settings and decode CLI output streams whole MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CodeRabbit full review of 0f0c8c9: - The README caller prerequisites now name the repository settings the final step needs: read and write workflow permissions and "Allow GitHub Actions to create and approve pull requests" (otherwise the sync branch is pushed but no review PR opens). They also note that a PR opened by GITHUB_TOKEN triggers no pull_request CI. - The bounded CLI test helper decodes child stdout and stderr with setEncoding("utf8") instead of converting each chunk, so a multibyte character split across two chunks is no longer corrupted. The new test writes the two bytes of "é" separately to both streams and fails on revert. --- .agents/pm/history/pm-github-hptv.jsonl | 1 + .agents/pm/issues/pm-github-hptv.toon | 5 +++-- README.md | 2 +- test/bounded-cli.test.ts | 12 ++++++++++++ test/helpers/bounded-cli.ts | 8 ++++++-- 5 files changed, 23 insertions(+), 5 deletions(-) diff --git a/.agents/pm/history/pm-github-hptv.jsonl b/.agents/pm/history/pm-github-hptv.jsonl index 7328db24..d5f9eabd 100644 --- a/.agents/pm/history/pm-github-hptv.jsonl +++ b/.agents/pm/history/pm-github-hptv.jsonl @@ -80,3 +80,4 @@ {"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"} diff --git a/.agents/pm/issues/pm-github-hptv.toon b/.agents/pm/issues/pm-github-hptv.toon index bbe6aed7..ef60ab56 100644 --- a/.agents/pm/issues/pm-github-hptv.toon +++ b/.agents/pm/issues/pm-github-hptv.toon @@ -6,9 +6,9 @@ status: in_progress priority: 1 tags: [] created_at: "2026-10-05T08:28:54.625Z" -updated_at: "2026-10-07T00:23:51.656Z" +updated_at: "2026-10-07T01:12:40.286Z" author: codex-sol -comments[40]{created_at,author,text}: +comments[41]{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." @@ -49,6 +49,7 @@ comments[40]{created_at,author,text}: "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." files[15]{path,scope}: .github/workflows/pm-github-sync.yml,project docs/coverage-gap-inventory.md,project diff --git a/README.md b/README.md index ba21fce3..8cb56f3e 100644 --- a/README.md +++ b/README.md @@ -317,7 +317,7 @@ jobs: 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`. +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 (or the caller supplies a GitHub App or fine-grained token for the PR step). ### Installed CLI timeout acceptance diff --git a/test/bounded-cli.test.ts b/test/bounded-cli.test.ts index ef6a0f0b..9c96ae9c 100644 --- a/test/bounded-cli.test.ts +++ b/test/bounded-cli.test.ts @@ -23,3 +23,15 @@ test("the acceptance watchdog passes normal exits and force-kills a CPU spin ign 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/helpers/bounded-cli.ts b/test/helpers/bounded-cli.ts index 905b3a36..794e92fd 100644 --- a/test/helpers/bounded-cli.ts +++ b/test/helpers/bounded-cli.ts @@ -29,8 +29,12 @@ export function boundedCli(command: string, args: string[], cwd: string, env: No kill("SIGTERM"); cleanup = setTimeout(() => kill("SIGKILL"), cleanupMs); }, deadlineMs); - child.stdout.on("data", chunk => { stdout += String(chunk); }); - child.stderr.on("data", chunk => { stderr += String(chunk); }); + // 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); From d57335e46e16e1dda70b8950bed634fc9345a122 Mon Sep 17 00:00:00 2001 From: SteveBot <1153461+unbraind@users.noreply.github.com> Date: Wed, 7 Oct 2026 03:31:57 +0200 Subject: [PATCH 45/60] Drop README advice the reusable workflow cannot honour Greptile on 15d4dc3: the reusable workflow exposes no token input and always opens the review PR with the job's GITHUB_TOKEN, so callers cannot supply a GitHub App or fine-grained token. The note now says to re-run CI on the sync PR, for example by closing and reopening it. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8cb56f3e..c942d6c8 100644 --- a/README.md +++ b/README.md @@ -317,7 +317,7 @@ jobs: 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 (or the caller supplies a GitHub App or fine-grained token for the PR step). +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 From 61b7ed6c0f488753c5295a64693ae7b6da6e5c30 Mon Sep 17 00:00:00 2001 From: SteveBot <1153461+unbraind@users.noreply.github.com> Date: Wed, 7 Oct 2026 04:31:49 +0200 Subject: [PATCH 46/60] Scan JSON object keys in history and planned records CodeRabbit (review of d57335e): for .jsonl changes only string values reached the rule scan; object keys became field labels and were never scanned, so a credential used as a key passed while the same text in a .toon line failed. Every non-structural key is now scanned as text. The new test puts a token in a history patch value's key and requires a fail verdict; it fails on revert. --- .agents/pm/history/pm-github-hptv.jsonl | 1 + .agents/pm/issues/pm-github-hptv.toon | 5 +++-- gate.ts | 3 +++ test/gate-inputs.test.ts | 13 +++++++++++++ 4 files changed, 20 insertions(+), 2 deletions(-) diff --git a/.agents/pm/history/pm-github-hptv.jsonl b/.agents/pm/history/pm-github-hptv.jsonl index d5f9eabd..6068d407 100644 --- a/.agents/pm/history/pm-github-hptv.jsonl +++ b/.agents/pm/history/pm-github-hptv.jsonl @@ -81,3 +81,4 @@ {"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"} diff --git a/.agents/pm/issues/pm-github-hptv.toon b/.agents/pm/issues/pm-github-hptv.toon index ef60ab56..daaea438 100644 --- a/.agents/pm/issues/pm-github-hptv.toon +++ b/.agents/pm/issues/pm-github-hptv.toon @@ -6,9 +6,9 @@ status: in_progress priority: 1 tags: [] created_at: "2026-10-05T08:28:54.625Z" -updated_at: "2026-10-07T01:12:40.286Z" +updated_at: "2026-10-07T02:20:59.122Z" author: codex-sol -comments[41]{created_at,author,text}: +comments[42]{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." @@ -50,6 +50,7 @@ comments[41]{created_at,author,text}: "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." files[15]{path,scope}: .github/workflows/pm-github-sync.yml,project docs/coverage-gap-inventory.md,project diff --git a/gate.ts b/gate.ts index 182c92c6..33d4d42e 100644 --- a/gate.ts +++ b/gate.ts @@ -885,6 +885,9 @@ function scanChangeFile(change: ChangeFile): Array 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"); From 08ca152a610255159d65b7a068541d66dc80c850 Mon Sep 17 00:00:00 2001 From: SteveBot <1153461+unbraind@users.noreply.github.com> Date: Wed, 7 Oct 2026 05:21:33 +0200 Subject: [PATCH 47/60] Attach the mock-handler doc to its function and assert the observable refusal order CodeRabbit (review of 61b7ed6): the githubHandler doc comment sat above planEntry's and documented nothing; it now precedes githubHandler. The idempotency-refusal test asserted a regex that always matched 'Fetching issues'; it now asserts what it can observe, that no 'plan verified' line is printed before the refusal. --- .agents/pm/history/pm-github-hptv.jsonl | 1 + .agents/pm/issues/pm-github-hptv.toon | 5 +++-- test/import-gate.test.ts | 24 ++++++++++++------------ 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/.agents/pm/history/pm-github-hptv.jsonl b/.agents/pm/history/pm-github-hptv.jsonl index 6068d407..078768c2 100644 --- a/.agents/pm/history/pm-github-hptv.jsonl +++ b/.agents/pm/history/pm-github-hptv.jsonl @@ -82,3 +82,4 @@ {"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"} diff --git a/.agents/pm/issues/pm-github-hptv.toon b/.agents/pm/issues/pm-github-hptv.toon index daaea438..3f9e3a9a 100644 --- a/.agents/pm/issues/pm-github-hptv.toon +++ b/.agents/pm/issues/pm-github-hptv.toon @@ -6,9 +6,9 @@ status: in_progress priority: 1 tags: [] created_at: "2026-10-05T08:28:54.625Z" -updated_at: "2026-10-07T02:20:59.122Z" +updated_at: "2026-10-07T03:21:18.330Z" author: codex-sol -comments[42]{created_at,author,text}: +comments[43]{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." @@ -51,6 +51,7 @@ comments[42]{created_at,author,text}: "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'." files[15]{path,scope}: .github/workflows/pm-github-sync.yml,project docs/coverage-gap-inventory.md,project diff --git a/test/import-gate.test.ts b/test/import-gate.test.ts index 782f348f..86d1c767 100644 --- a/test/import-gate.test.ts +++ b/test/import-gate.test.ts @@ -75,16 +75,6 @@ function issue(number: number, title: string, body: string | null): GhIssue { }; } -/** - * 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. - */ /** * Build one plan entry for the pure verification functions. * @@ -154,6 +144,16 @@ test("a real completed SDK journal with reset item files cannot authorize a push } 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(), @@ -386,8 +386,8 @@ test("a gated import refuses to write when the corpus cannot yield an idempotent }, ); assert.ok( - messages.some((message) => /pm github gate: plan verified|Fetching issues/.test(message)), - "the gate runs its plan phase before failing", + !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; From 66a3a43150c88a11566bf5bf8c089047491bca83 Mon Sep 17 00:00:00 2001 From: SteveBot <1153461+unbraind@users.noreply.github.com> Date: Wed, 7 Oct 2026 06:27:57 +0200 Subject: [PATCH 48/60] Resolve historical fixture provenance only for blobs the repository holds CodeRabbit (review of 08ca152): the privacy gate resolved provenance for every historical_test manifest entry with git ls-tree on the pinned commit. That commit (434df42) exists only on this branch, so after a squash merge the gate would fail on main before checking whether the blob is even present. Provenance is now resolved only for blobs in the audited object inventory: a historical blob that the squash removed needs no exemption, and a present blob whose provenance is missing or mismatched still fails closed. The provenance test covers both cases and fails on revert. --- .agents/pm/history/pm-github-hptv.jsonl | 1 + .agents/pm/issues/pm-github-hptv.toon | 5 +++-- scripts/privacy-gate.ts | 12 +++++++++--- test/privacy-gate.test.ts | 11 ++++++++++- 4 files changed, 23 insertions(+), 6 deletions(-) diff --git a/.agents/pm/history/pm-github-hptv.jsonl b/.agents/pm/history/pm-github-hptv.jsonl index 078768c2..48913fd1 100644 --- a/.agents/pm/history/pm-github-hptv.jsonl +++ b/.agents/pm/history/pm-github-hptv.jsonl @@ -83,3 +83,4 @@ {"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"} diff --git a/.agents/pm/issues/pm-github-hptv.toon b/.agents/pm/issues/pm-github-hptv.toon index 3f9e3a9a..673c6c5b 100644 --- a/.agents/pm/issues/pm-github-hptv.toon +++ b/.agents/pm/issues/pm-github-hptv.toon @@ -6,9 +6,9 @@ status: in_progress priority: 1 tags: [] created_at: "2026-10-05T08:28:54.625Z" -updated_at: "2026-10-07T03:21:18.330Z" +updated_at: "2026-10-07T04:27:56.381Z" author: codex-sol -comments[43]{created_at,author,text}: +comments[44]{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." @@ -52,6 +52,7 @@ comments[43]{created_at,author,text}: "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." files[15]{path,scope}: .github/workflows/pm-github-sync.yml,project docs/coverage-gap-inventory.md,project diff --git a/scripts/privacy-gate.ts b/scripts/privacy-gate.ts index a9ec4351..483aa2b7 100644 --- a/scripts/privacy-gate.ts +++ b/scripts/privacy-gate.ts @@ -168,15 +168,21 @@ 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); for (const [oid, entry] of manifest) { const source = entry.historical_test; - if (!source) continue; + 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."); } @@ -354,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, diff --git a/test/privacy-gate.test.ts b/test/privacy-gate.test.ts index f540d6c6..348b9210 100644 --- a/test/privacy-gate.test.ts +++ b/test/privacy-gate.test.ts @@ -491,12 +491,21 @@ test("historical fixture provenance reports Git query failures", { skip: process const commit = "a".repeat(40); const manifestDir = join(root, "test/fixtures/privacy-gate"); mkdirSync(manifestDir, { recursive: true }); - writeFileSync(join(manifestDir, "manifest.json"), JSON.stringify({ ["b".repeat(40)]: { justification: "Synthetic", historical_test: { commit, path: "test/gate.test.ts" } } })); + // 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 }); } }); From 81b6c6aadbadff365f5206ff5b3fb37624190706 Mon Sep 17 00:00:00 2001 From: SteveBot <1153461+unbraind@users.noreply.github.com> Date: Wed, 7 Oct 2026 07:38:24 +0200 Subject: [PATCH 49/60] Fail a gated non-atomic import when a planned write fails CodeRabbit (review of 66a3a43): in the gated non-atomic path, a failed update, close or reopen reconciliation only counted as skipped, and the provenance check still passed because the item kept its tag. A gated import, and so the reusable sync workflow, therefore reported success while the tracker stayed different from GitHub, and the repeat import hid it. The gated path now fails closed when its write count differs from the verified plan. The new test refuses one close while a second issue updates cleanly: without the fix the import succeeds, with it the import is refused. --- .agents/pm/history/pm-github-hptv.jsonl | 1 + .agents/pm/issues/pm-github-hptv.toon | 5 +++-- index.ts | 9 ++++++++ test/coverage-remainder.test.ts | 30 +++++++++++++++++++++++++ 4 files changed, 43 insertions(+), 2 deletions(-) diff --git a/.agents/pm/history/pm-github-hptv.jsonl b/.agents/pm/history/pm-github-hptv.jsonl index 48913fd1..aa0233b0 100644 --- a/.agents/pm/history/pm-github-hptv.jsonl +++ b/.agents/pm/history/pm-github-hptv.jsonl @@ -84,3 +84,4 @@ {"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"} diff --git a/.agents/pm/issues/pm-github-hptv.toon b/.agents/pm/issues/pm-github-hptv.toon index 673c6c5b..9f72ca74 100644 --- a/.agents/pm/issues/pm-github-hptv.toon +++ b/.agents/pm/issues/pm-github-hptv.toon @@ -6,9 +6,9 @@ status: in_progress priority: 1 tags: [] created_at: "2026-10-05T08:28:54.625Z" -updated_at: "2026-10-07T04:27:56.381Z" +updated_at: "2026-10-07T05:27:52.940Z" author: codex-sol -comments[44]{created_at,author,text}: +comments[45]{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." @@ -53,6 +53,7 @@ comments[44]{created_at,author,text}: "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." files[15]{path,scope}: .github/workflows/pm-github-sync.yml,project docs/coverage-gap-inventory.md,project diff --git a/index.ts b/index.ts index b77307ef..c8b21431 100644 --- a/index.ts +++ b/index.ts @@ -3387,6 +3387,15 @@ 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) 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. + if (opts.gate && skipped !== gateCompleteness!.skipped) { + throw new CommandError( + `pm github gate: ${skipped - gateCompleteness!.skipped} planned write(s) failed; the tracker does not match the verified plan. Nothing may be pushed.`, + ); + } const depLink = opts.linkDeps ? await linkImportedDependencies(repo, filtered, pmRoot, dependencies) : undefined; diff --git a/test/coverage-remainder.test.ts b/test/coverage-remainder.test.ts index 6cbb8b3f..2438ab67 100644 --- a/test/coverage-remainder.test.ts +++ b/test/coverage-remainder.test.ts @@ -259,6 +259,36 @@ test("gated atomic import reports a recovered journal and fails closed after a p } }); +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 }); + } +}); + 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" }); From 0ad387f1ccbc0abf5db88ad7eb444f8e85d2d6c0 Mon Sep 17 00:00:00 2001 From: SteveBot <1153461+unbraind@users.noreply.github.com> Date: Wed, 7 Oct 2026 09:11:51 +0200 Subject: [PATCH 50/60] Count unwritten native comments as gated plan divergence syncGithubCommentsToAnnotations now reports a failed count covering lock contention, unreadable existing comments, and refused per-comment adds. runImport accumulates those failures (plus comments left unreachable by an unparsable created id) on both the non-atomic and the atomic path and fails a gated run closed through one shared refusal when the tracker no longer matches the verified plan; ungated imports keep the existing warn-and-continue behaviour. --- .agents/pm/history/pm-github-hptv.jsonl | 7 ++ .agents/pm/issues/pm-github-hptv.toon | 26 ++++- index.ts | 95 +++++++++++---- test/coverage-remainder.test.ts | 148 ++++++++++++++++++++++++ test/import-errors.test.ts | 2 +- test/import-lock.test.ts | 4 +- 6 files changed, 255 insertions(+), 27 deletions(-) diff --git a/.agents/pm/history/pm-github-hptv.jsonl b/.agents/pm/history/pm-github-hptv.jsonl index aa0233b0..00512cfb 100644 --- a/.agents/pm/history/pm-github-hptv.jsonl +++ b/.agents/pm/history/pm-github-hptv.jsonl @@ -85,3 +85,10 @@ {"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"} diff --git a/.agents/pm/issues/pm-github-hptv.toon b/.agents/pm/issues/pm-github-hptv.toon index 9f72ca74..a2769290 100644 --- a/.agents/pm/issues/pm-github-hptv.toon +++ b/.agents/pm/issues/pm-github-hptv.toon @@ -6,9 +6,11 @@ status: in_progress priority: 1 tags: [] created_at: "2026-10-05T08:28:54.625Z" -updated_at: "2026-10-07T05:27:52.940Z" +updated_at: "2026-10-07T07:11:43.075Z" +assignee: pi-glm +claim_principal: pi-glm author: codex-sol -comments[45]{created_at,author,text}: +comments[49]{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." @@ -54,6 +56,10 @@ comments[45]{created_at,author,text}: "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." files[15]{path,scope}: .github/workflows/pm-github-sync.yml,project docs/coverage-gap-inventory.md,project @@ -70,7 +76,7 @@ files[15]{path,scope}: test/import-gate.test.ts,project test/import-lock.test.ts,project test/review-round1.test.ts,project -tests[14]: +tests[16]: - command: node --test test/bounded-cli.test.ts scope: project timeout_seconds: 60 @@ -173,6 +179,20 @@ tests[14]: 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 docs[3]{path,scope}: CHANGELOG.md,project docs/coverage-gap-inventory.md,project diff --git a/index.ts b/index.ts index c8b21431..22fd735c 100644 --- a/index.ts +++ b/index.ts @@ -2184,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 { @@ -2218,11 +2220,12 @@ export async function syncGithubCommentsToAnnotations( } catch (err: unknown) { 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++; @@ -2235,9 +2238,10 @@ export async function syncGithubCommentsToAnnotations( } catch (err: unknown) { const msg = errorMessage(err); console.error(`#${issueNumber}: comment ${c.id} sync failed — ${msg}`); + failed++; } } - return { added, skipped }; + return { added, skipped, failed }; } finally { release(); } @@ -3050,6 +3054,10 @@ export async function runImport( let imported = 0; let updated = 0; let skipped = 0; + // Planned native comments the write phase could not land (contended lock, + // refused add, unparsable created id). Ungated runs warn and continue; gated + // runs add this to the plan-divergence refusal at the end. + let commentWriteFailures = 0; // Gated pipeline, phase 1 — plan BEFORE any mutation. The same prepare step // the write paths use builds every plan entry up front, so completeness @@ -3160,18 +3168,26 @@ export async function runImport( 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).`, @@ -3303,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; @@ -3357,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++; @@ -3388,14 +3408,16 @@ export async function runImport( 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) 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. - if (opts.gate && skipped !== gateCompleteness!.skipped) { - throw new CommandError( - `pm github gate: ${skipped - gateCompleteness!.skipped} planned write(s) failed; the tracker does not match the verified plan. Nothing may be pushed.`, - ); - } + // (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; @@ -3441,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) // --------------------------------------------------------------------------- diff --git a/test/coverage-remainder.test.ts b/test/coverage-remainder.test.ts index 2438ab67..4dc2a61e 100644 --- a/test/coverage-remainder.test.ts +++ b/test/coverage-remainder.test.ts @@ -289,6 +289,154 @@ test("a gated non-atomic import fails closed when a planned write fails", async } }); +// 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", 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", 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" }); diff --git a/test/import-errors.test.ts b/test/import-errors.test.ts index 0eba3ad3..e03be06a 100644 --- a/test/import-errors.test.ts +++ b/test/import-errors.test.ts @@ -118,7 +118,7 @@ test("comment sync degrades on an unavailable lock directory and handles a missi 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 }); + 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/); }); diff --git a/test/import-lock.test.ts b/test/import-lock.test.ts index 960e25d6..84d25df2 100644 --- a/test/import-lock.test.ts +++ b/test/import-lock.test.ts @@ -525,12 +525,12 @@ 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 }); } From dc87f02a38ad389cfafbdd1628815b4a0b34c2e0 Mon Sep 17 00:00:00 2001 From: SteveBot <1153461+unbraind@users.noreply.github.com> Date: Wed, 7 Oct 2026 09:11:56 +0200 Subject: [PATCH 51/60] Describe the 505 test count as a later-head measurement The 504/504 receipt at the closing head 7a41660 stays the authoritative closing count; the 505/505 figure is now explicitly a later-head measurement taken after later review rounds added tests. --- .agents/pm/history/pm-github-hptv.jsonl | 1 + .agents/pm/issues/pm-github-hptv.toon | 5 +++-- docs/coverage-gap-inventory.md | 5 +++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/.agents/pm/history/pm-github-hptv.jsonl b/.agents/pm/history/pm-github-hptv.jsonl index 00512cfb..b1becce8 100644 --- a/.agents/pm/history/pm-github-hptv.jsonl +++ b/.agents/pm/history/pm-github-hptv.jsonl @@ -92,3 +92,4 @@ {"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"} diff --git a/.agents/pm/issues/pm-github-hptv.toon b/.agents/pm/issues/pm-github-hptv.toon index a2769290..00885885 100644 --- a/.agents/pm/issues/pm-github-hptv.toon +++ b/.agents/pm/issues/pm-github-hptv.toon @@ -6,11 +6,11 @@ status: in_progress priority: 1 tags: [] created_at: "2026-10-05T08:28:54.625Z" -updated_at: "2026-10-07T07:11:43.075Z" +updated_at: "2026-10-07T07:11:56.715Z" assignee: pi-glm claim_principal: pi-glm author: codex-sol -comments[49]{created_at,author,text}: +comments[50]{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." @@ -60,6 +60,7 @@ comments[49]{created_at,author,text}: "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." files[15]{path,scope}: .github/workflows/pm-github-sync.yml,project docs/coverage-gap-inventory.md,project diff --git a/docs/coverage-gap-inventory.md b/docs/coverage-gap-inventory.md index d2d869bd..c868ea86 100644 --- a/docs/coverage-gap-inventory.md +++ b/docs/coverage-gap-inventory.md @@ -118,5 +118,6 @@ 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 closing head passes 504/504 tests; the current head -passes 505/505 with zero failures or skips. +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. From 9321e8a1e399041140cbc69fa9acf89644f234e1 Mon Sep 17 00:00:00 2001 From: SteveBot <1153461+unbraind@users.noreply.github.com> Date: Wed, 7 Oct 2026 09:12:06 +0200 Subject: [PATCH 52/60] Record the gated comment-write fix and coverage-count fix on the tracker --- .agents/pm/history/pm-github-hptv.jsonl | 1 + .agents/pm/issues/pm-github-hptv.toon | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.agents/pm/history/pm-github-hptv.jsonl b/.agents/pm/history/pm-github-hptv.jsonl index b1becce8..f4d8ebb4 100644 --- a/.agents/pm/history/pm-github-hptv.jsonl +++ b/.agents/pm/history/pm-github-hptv.jsonl @@ -93,3 +93,4 @@ {"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"} diff --git a/.agents/pm/issues/pm-github-hptv.toon b/.agents/pm/issues/pm-github-hptv.toon index 00885885..5fe263f0 100644 --- a/.agents/pm/issues/pm-github-hptv.toon +++ b/.agents/pm/issues/pm-github-hptv.toon @@ -6,11 +6,11 @@ status: in_progress priority: 1 tags: [] created_at: "2026-10-05T08:28:54.625Z" -updated_at: "2026-10-07T07:11:56.715Z" +updated_at: "2026-10-07T07:12:00.358Z" assignee: pi-glm claim_principal: pi-glm author: codex-sol -comments[50]{created_at,author,text}: +comments[51]{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." @@ -61,6 +61,7 @@ comments[50]{created_at,author,text}: "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." files[15]{path,scope}: .github/workflows/pm-github-sync.yml,project docs/coverage-gap-inventory.md,project From 011f850e0399a7e114c56502f927b0536319f77c Mon Sep 17 00:00:00 2001 From: SteveBot <1153461+unbraind@users.noreply.github.com> Date: Wed, 7 Oct 2026 09:12:18 +0200 Subject: [PATCH 53/60] Release the tracker claim for this review round --- .agents/pm/history/pm-github-hptv.jsonl | 1 + .agents/pm/issues/pm-github-hptv.toon | 4 +--- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.agents/pm/history/pm-github-hptv.jsonl b/.agents/pm/history/pm-github-hptv.jsonl index f4d8ebb4..3c21efd8 100644 --- a/.agents/pm/history/pm-github-hptv.jsonl +++ b/.agents/pm/history/pm-github-hptv.jsonl @@ -94,3 +94,4 @@ {"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"} diff --git a/.agents/pm/issues/pm-github-hptv.toon b/.agents/pm/issues/pm-github-hptv.toon index 5fe263f0..ca0e1b3a 100644 --- a/.agents/pm/issues/pm-github-hptv.toon +++ b/.agents/pm/issues/pm-github-hptv.toon @@ -6,9 +6,7 @@ status: in_progress priority: 1 tags: [] created_at: "2026-10-05T08:28:54.625Z" -updated_at: "2026-10-07T07:12:00.358Z" -assignee: pi-glm -claim_principal: pi-glm +updated_at: "2026-10-07T07:12:11.960Z" author: codex-sol comments[51]{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." From 7e981cba6f4f536226502f712aace44f7db86d70 Mon Sep 17 00:00:00 2001 From: SteveBot <1153461+unbraind@users.noreply.github.com> Date: Wed, 7 Oct 2026 09:44:20 +0200 Subject: [PATCH 54/60] Link proposed items on the sync branch and state the caller's PR setting CodeRabbit (review of 011f850): - The review PR body linked each changed item only on main, which does not resolve for a new item, or shows the old version of an updated one, until the PR merges. Each item now also links its proposed version on the sync branch; the executed workflow test asserts the link and fails on revert. - docs/sync-workflow-caller.yml now states that the caller repository needs read and write workflow permissions and "Allow GitHub Actions to create and approve pull requests", or the sync branch is pushed without a review PR. --- .agents/pm/history/pm-github-hptv.jsonl | 1 + .agents/pm/issues/pm-github-hptv.toon | 5 +++-- .github/workflows/pm-github-sync.yml | 4 +++- docs/sync-workflow-caller.yml | 4 ++++ test/import-gate.test.ts | 6 +++++- 5 files changed, 16 insertions(+), 4 deletions(-) diff --git a/.agents/pm/history/pm-github-hptv.jsonl b/.agents/pm/history/pm-github-hptv.jsonl index 3c21efd8..e6f05393 100644 --- a/.agents/pm/history/pm-github-hptv.jsonl +++ b/.agents/pm/history/pm-github-hptv.jsonl @@ -95,3 +95,4 @@ {"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"} diff --git a/.agents/pm/issues/pm-github-hptv.toon b/.agents/pm/issues/pm-github-hptv.toon index ca0e1b3a..f32e9f80 100644 --- a/.agents/pm/issues/pm-github-hptv.toon +++ b/.agents/pm/issues/pm-github-hptv.toon @@ -6,9 +6,9 @@ status: in_progress priority: 1 tags: [] created_at: "2026-10-05T08:28:54.625Z" -updated_at: "2026-10-07T07:12:11.960Z" +updated_at: "2026-10-07T07:31:19.560Z" author: codex-sol -comments[51]{created_at,author,text}: +comments[52]{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." @@ -60,6 +60,7 @@ comments[51]{created_at,author,text}: "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." files[15]{path,scope}: .github/workflows/pm-github-sync.yml,project docs/coverage-gap-inventory.md,project diff --git a/.github/workflows/pm-github-sync.yml b/.github/workflows/pm-github-sync.yml index 47e88eb8..e92e6504 100644 --- a/.github/workflows/pm-github-sync.yml +++ b/.github/workflows/pm-github-sync.yml @@ -183,7 +183,9 @@ jobs: .agents/pm/*/*.toon) item_id="${changed##*/}" item_id="${item_id%.toon}" - printf -- '- [%s](https://github.com/%s/blob/main/%s)\n' "${item_id}" "${REPOSITORY}" "${changed}" >> "${links_file}" + # 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) diff --git a/docs/sync-workflow-caller.yml b/docs/sync-workflow-caller.yml index 528ff480..cfa8cd59 100644 --- a/docs/sync-workflow-caller.yml +++ b/docs/sync-workflow-caller.yml @@ -1,4 +1,8 @@ # 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: diff --git a/test/import-gate.test.ts b/test/import-gate.test.ts index 86d1c767..ecdeae3e 100644 --- a/test/import-gate.test.ts +++ b/test/import-gate.test.ts @@ -827,7 +827,11 @@ test("the executable reusable workflow pushes only a clean import and creates or 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()); - assert.match(fs.readFileSync(env.REVIEW_RECEIPT!, "utf8"), /https:\/\/github\.com\/acme\/widgets\/blob\/main\/\.agents\/pm\/issues\/[^)]+\.toon/); + 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"); From 4449fd4c54a7750457d20302dcdea32aa3bd145c Mon Sep 17 00:00:00 2001 From: SteveBot <1153461+unbraind@users.noreply.github.com> Date: Wed, 7 Oct 2026 10:44:33 +0200 Subject: [PATCH 55/60] Skip read-only-directory fixtures under root and decode workflow output whole CodeRabbit (review of 7e981cb): - Three comment-sync tests rely on a read-only tasks directory, which root ignores. They now skip visibly when the suite runs as root (the nonRootOnly pattern from import-lock), and still run on the non-root CI runners. This also resolves pm-github-rap0, which tracked the oldest of the three; it is closed. - executeWorkflow decodes the child's stdout and stderr with setEncoding("utf8"), like bounded-cli, so a multibyte character split across chunks is no longer corrupted. --- .agents/pm/history/pm-github-hptv.jsonl | 1 + .agents/pm/history/pm-github-rap0.jsonl | 1 + .agents/pm/issues/pm-github-hptv.toon | 5 +++-- .agents/pm/issues/pm-github-rap0.toon | 7 +++++-- CHANGELOG.md | 4 ++++ test/coverage-remainder.test.ts | 11 ++++++++--- test/import-gate.test.ts | 8 ++++++-- 7 files changed, 28 insertions(+), 9 deletions(-) diff --git a/.agents/pm/history/pm-github-hptv.jsonl b/.agents/pm/history/pm-github-hptv.jsonl index e6f05393..b036f7d7 100644 --- a/.agents/pm/history/pm-github-hptv.jsonl +++ b/.agents/pm/history/pm-github-hptv.jsonl @@ -96,3 +96,4 @@ {"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"} diff --git a/.agents/pm/history/pm-github-rap0.jsonl b/.agents/pm/history/pm-github-rap0.jsonl index b9a8331a..e12f703b 100644 --- a/.agents/pm/history/pm-github-rap0.jsonl +++ b/.agents/pm/history/pm-github-rap0.jsonl @@ -1,3 +1,4 @@ {"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"} diff --git a/.agents/pm/issues/pm-github-hptv.toon b/.agents/pm/issues/pm-github-hptv.toon index f32e9f80..d96713af 100644 --- a/.agents/pm/issues/pm-github-hptv.toon +++ b/.agents/pm/issues/pm-github-hptv.toon @@ -6,9 +6,9 @@ status: in_progress priority: 1 tags: [] created_at: "2026-10-05T08:28:54.625Z" -updated_at: "2026-10-07T07:31:19.560Z" +updated_at: "2026-10-07T08:33:52.212Z" author: codex-sol -comments[52]{created_at,author,text}: +comments[53]{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." @@ -61,6 +61,7 @@ comments[52]{created_at,author,text}: "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." files[15]{path,scope}: .github/workflows/pm-github-sync.yml,project docs/coverage-gap-inventory.md,project diff --git a/.agents/pm/issues/pm-github-rap0.toon b/.agents/pm/issues/pm-github-rap0.toon index ac8aef3e..0e3b49ff 100644 --- a/.agents/pm/issues/pm-github-rap0.toon +++ b/.agents/pm/issues/pm-github-rap0.toon @@ -2,12 +2,15 @@ id: pm-github-rap0 title: "The comment-sync per-comment write-failure test is root-sensitive: withReadOnlyDirectory cannot make the tasks directory unwritable for root" 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: open +status: closed priority: 2 tags[3]: ci,root,tests created_at: "2026-10-06T18:29:11.654Z" -updated_at: "2026-10-06T18:54:50.637Z" +updated_at: "2026-10-07T08:33:51.539Z" +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/CHANGELOG.md b/CHANGELOG.md index 80a23919..22c8f245 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ - 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 + +- The comment-sync per-comment write-failure test is root-sensitive: withReadOnlyDirectory cannot make the tasks directory unwritable for root ([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)) diff --git a/test/coverage-remainder.test.ts b/test/coverage-remainder.test.ts index 4dc2a61e..a4535c1a 100644 --- a/test/coverage-remainder.test.ts +++ b/test/coverage-remainder.test.ts @@ -32,6 +32,11 @@ import extension, { } 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"; @@ -319,7 +324,7 @@ test("a gated import fails closed when the comment-sync lock is contended", asyn } }); -test("a gated import fails closed when a per-comment add fails mid-sync", async () => { +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" }); @@ -392,7 +397,7 @@ test("a gated atomic import fails closed when committed items lack their planned } }); -test("the same comment-sync failures without --gate still import with a warning", async () => { +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" }); @@ -506,7 +511,7 @@ test("non-atomic import reports update, close, reopen, and unparsed-id failures" } }); -test("comment sync skips a contended lock and reports a per-comment write failure", async () => { +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 { diff --git a/test/import-gate.test.ts b/test/import-gate.test.ts index ecdeae3e..446e7032 100644 --- a/test/import-gate.test.ts +++ b/test/import-gate.test.ts @@ -670,8 +670,12 @@ function executeWorkflow(shell: string, root: string, env: NodeJS.ProcessEnv): P const child = spawn("bash", ["-c", "set -euo pipefail\n" + shell], { cwd: root, env }); let stdout = ""; let stderr = ""; - child.stdout.on("data", chunk => { stdout += String(chunk); }); - child.stderr.on("data", chunk => { stderr += String(chunk); }); + // 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 })); }); From 48c5885c265a1b64a15f25bb7bfed0a3f64986d3 Mon Sep 17 00:00:00 2001 From: SteveBot <1153461+unbraind@users.noreply.github.com> Date: Wed, 7 Oct 2026 12:47:16 +0200 Subject: [PATCH 56/60] Select the sync review PR by head repository and close it on a no-change run The review-PR lookup now asks gh pr list for the head-repository fields and selects, with jq, only an open PR whose head repository is the synced repository, so a fork PR sharing the sync branch name is never edited and a new own PR is created instead. A no-change run now closes a stale own review PR with a short comment instead of leaving it mergeable, and the public acceptance helper's npm pack isolates the npm 10 prepare lifecycle exactly like packWorkflowCandidate (package-local PM_PATH, --foreground-scripts=false). --- .agents/pm/history/pm-github-hptv.jsonl | 6 ++ .agents/pm/issues/pm-github-hptv.toon | 25 ++++++- .github/workflows/pm-github-sync.yml | 21 +++++- test/helpers/public-acceptance.ts | 19 ++++- test/import-gate.test.ts | 93 ++++++++++++++++++++++--- 5 files changed, 148 insertions(+), 16 deletions(-) diff --git a/.agents/pm/history/pm-github-hptv.jsonl b/.agents/pm/history/pm-github-hptv.jsonl index b036f7d7..24d0fdd5 100644 --- a/.agents/pm/history/pm-github-hptv.jsonl +++ b/.agents/pm/history/pm-github-hptv.jsonl @@ -97,3 +97,9 @@ {"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"} diff --git a/.agents/pm/issues/pm-github-hptv.toon b/.agents/pm/issues/pm-github-hptv.toon index d96713af..e08e727a 100644 --- a/.agents/pm/issues/pm-github-hptv.toon +++ b/.agents/pm/issues/pm-github-hptv.toon @@ -6,9 +6,11 @@ status: in_progress priority: 1 tags: [] created_at: "2026-10-05T08:28:54.625Z" -updated_at: "2026-10-07T08:33:52.212Z" +updated_at: "2026-10-07T10:02:42.448Z" +assignee: pi-glm +claim_principal: pi-glm author: codex-sol -comments[53]{created_at,author,text}: +comments[56]{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." @@ -62,6 +64,9 @@ comments[53]{created_at,author,text}: "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." files[15]{path,scope}: .github/workflows/pm-github-sync.yml,project docs/coverage-gap-inventory.md,project @@ -78,7 +83,7 @@ files[15]{path,scope}: test/import-gate.test.ts,project test/import-lock.test.ts,project test/review-round1.test.ts,project -tests[16]: +tests[18]: - command: node --test test/bounded-cli.test.ts scope: project timeout_seconds: 60 @@ -195,6 +200,20 @@ tests[16]: 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 diff --git a/.github/workflows/pm-github-sync.yml b/.github/workflows/pm-github-sync.yml index e92e6504..a61545d5 100644 --- a/.github/workflows/pm-github-sync.yml +++ b/.github/workflows/pm-github-sync.yml @@ -159,6 +159,22 @@ jobs: 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 @@ -173,6 +189,9 @@ jobs: 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 @@ -203,7 +222,7 @@ jobs: printf '%s\n\n' "Tracker items changed:" cat "${links_file}" } > "${body_file}" - pr_number="$(gh pr list -R "${REPOSITORY}" --head "${SYNC_BRANCH}" --base main --state open --json number --jq '.[0].number // empty')" + 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 diff --git a/test/helpers/public-acceptance.ts b/test/helpers/public-acceptance.ts index affa37b8..8196c84d 100644 --- a/test/helpers/public-acceptance.ts +++ b/test/helpers/public-acceptance.ts @@ -19,8 +19,17 @@ const env: NodeJS.ProcessEnv & { PM_PATH: string } = { ...process.env, PM_AUTHOR delete env.PM_GITHUB_API_BASE; delete env.NODE_TEST_CONTEXT; delete env.NODE_V8_COVERAGE; -function setup(bin: string, args: string[], cwd = root): string { - const result = spawnSync(bin, args, { cwd, env, encoding: "utf8", timeout: 45000 }); +/** + * 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; } @@ -55,7 +64,11 @@ try { setup("git", ["config", "user.name", "Fixture"]); setup("git", ["config", "user.email", "fixture@example.invalid"]); setup("node", [cli, "--path", env.PM_PATH, "init", "fixture"]); - const packed = JSON.parse(setup("npm", ["pack", "--ignore-scripts", "--json", "--pack-destination", base], packageRoot)) as [{ filename: string }]; + // 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"); diff --git a/test/import-gate.test.ts b/test/import-gate.test.ts index 446e7032..1fe35d03 100644 --- a/test/import-gate.test.ts +++ b/test/import-gate.test.ts @@ -702,7 +702,17 @@ 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") printf '%s' "\${EXISTING_PR:-}" ;; + "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 @@ -711,6 +721,13 @@ case "$1 $2" in 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 }); @@ -845,15 +862,48 @@ test("the executable reusable workflow pushes only a clean import and creates or 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, EXISTING_PR: "1" }); + 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 }); } +}); + -test("an install-only settings change never reaches the sync commit", async () => { +/** + * 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 @@ -867,18 +917,43 @@ test("an install-only settings change never reaches the sync commit", async () = 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 () => { - 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.match(result.stdout, /no PR is needed/); - assert.equal(fs.existsSync(env.REVIEW_ACTIONS!), false, "no review PR was created or edited"); - assert.deepEqual(remoteRefs(bare), ["refs/heads/main"]); + 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 () => { From 660107dde14bee73a4355c4d4f76912ce76c8c80 Mon Sep 17 00:00:00 2001 From: SteveBot <1153461+unbraind@users.noreply.github.com> Date: Wed, 7 Oct 2026 12:59:43 +0200 Subject: [PATCH 57/60] Record the verified sync review-PR round on pm-github-hptv --- .agents/pm/history/pm-github-hptv.jsonl | 3 +++ .agents/pm/issues/pm-github-hptv.toon | 8 ++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/.agents/pm/history/pm-github-hptv.jsonl b/.agents/pm/history/pm-github-hptv.jsonl index 24d0fdd5..4419c513 100644 --- a/.agents/pm/history/pm-github-hptv.jsonl +++ b/.agents/pm/history/pm-github-hptv.jsonl @@ -103,3 +103,6 @@ {"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"} diff --git a/.agents/pm/issues/pm-github-hptv.toon b/.agents/pm/issues/pm-github-hptv.toon index e08e727a..6f7c3214 100644 --- a/.agents/pm/issues/pm-github-hptv.toon +++ b/.agents/pm/issues/pm-github-hptv.toon @@ -6,11 +6,9 @@ status: in_progress priority: 1 tags: [] created_at: "2026-10-05T08:28:54.625Z" -updated_at: "2026-10-07T10:02:42.448Z" -assignee: pi-glm -claim_principal: pi-glm +updated_at: "2026-10-07T10:59:42.057Z" author: codex-sol -comments[56]{created_at,author,text}: +comments[58]{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." @@ -67,6 +65,8 @@ comments[56]{created_at,author,text}: "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." files[15]{path,scope}: .github/workflows/pm-github-sync.yml,project docs/coverage-gap-inventory.md,project From 448b022bd699d791c6aabd9c427c803317dae636 Mon Sep 17 00:00:00 2001 From: SteveBot <1153461+unbraind@users.noreply.github.com> Date: Wed, 7 Oct 2026 13:49:09 +0200 Subject: [PATCH 58/60] Detect host paths behind file:// URLs and label colons without double-reporting drive paths --- .agents/pm/history/pm-github-hptv.jsonl | 1 + .agents/pm/issues/pm-github-hptv.toon | 5 +++-- gate.ts | 12 +++++++----- test/gate.test.ts | 17 +++++++++++++++++ 4 files changed, 28 insertions(+), 7 deletions(-) diff --git a/.agents/pm/history/pm-github-hptv.jsonl b/.agents/pm/history/pm-github-hptv.jsonl index 4419c513..d229e700 100644 --- a/.agents/pm/history/pm-github-hptv.jsonl +++ b/.agents/pm/history/pm-github-hptv.jsonl @@ -106,3 +106,4 @@ {"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"} diff --git a/.agents/pm/issues/pm-github-hptv.toon b/.agents/pm/issues/pm-github-hptv.toon index 6f7c3214..4fb0fc69 100644 --- a/.agents/pm/issues/pm-github-hptv.toon +++ b/.agents/pm/issues/pm-github-hptv.toon @@ -6,9 +6,9 @@ status: in_progress priority: 1 tags: [] created_at: "2026-10-05T08:28:54.625Z" -updated_at: "2026-10-07T10:59:42.057Z" +updated_at: "2026-10-07T11:33:39.558Z" author: codex-sol -comments[58]{created_at,author,text}: +comments[59]{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." @@ -67,6 +67,7 @@ comments[58]{created_at,author,text}: "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." files[15]{path,scope}: .github/workflows/pm-github-sync.yml,project docs/coverage-gap-inventory.md,project diff --git a/gate.ts b/gate.ts index 33d4d42e..7cc32bb5 100644 --- a/gate.ts +++ b/gate.ts @@ -266,12 +266,15 @@ const HOST_PATH_RULES: readonly ContentRule[] = [ // 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`). - pattern: /(?:^|[\s"'`([=,{])\/(?: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, + // 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", - pattern: /(?),;\]}]+/g, + // A drive letter at a token start, or as a `file:///C:/…` URL path. + pattern: /(?:(?),;\]}]+/g, }, { rule: "home-username", @@ -404,8 +407,7 @@ export function scanLineForRuleHits(line: string): RuleHit[] { 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 === "absolute-host-path" ? match[0].slice(match[0].indexOf("/")) - : rule.rule === "home-username" ? match[0].slice(match[0].indexOf("~")) : 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]! }); } } diff --git a/test/gate.test.ts b/test/gate.test.ts index ffb280cf..8dde28c3 100644 --- a/test/gate.test.ts +++ b/test/gate.test.ts @@ -307,6 +307,23 @@ test("host-path rule flags host-identifying roots, not slash commands, links or } }); +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), From 344fca459bf243a46fab9def3afbc6039ae8bd61 Mon Sep 17 00:00:00 2001 From: SteveBot <1153461+unbraind@users.noreply.github.com> Date: Wed, 7 Oct 2026 14:45:30 +0200 Subject: [PATCH 59/60] List the repeat-import no-op step in the documented workflow order and retitle pm-github-rap0 for release notes --- .agents/pm/history/pm-github-hptv.jsonl | 1 + .agents/pm/history/pm-github-rap0.jsonl | 1 + .agents/pm/issues/pm-github-hptv.toon | 5 +++-- .agents/pm/issues/pm-github-rap0.toon | 4 ++-- CHANGELOG.md | 2 +- README.md | 2 +- 6 files changed, 9 insertions(+), 6 deletions(-) diff --git a/.agents/pm/history/pm-github-hptv.jsonl b/.agents/pm/history/pm-github-hptv.jsonl index d229e700..f1340678 100644 --- a/.agents/pm/history/pm-github-hptv.jsonl +++ b/.agents/pm/history/pm-github-hptv.jsonl @@ -107,3 +107,4 @@ {"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"} diff --git a/.agents/pm/history/pm-github-rap0.jsonl b/.agents/pm/history/pm-github-rap0.jsonl index e12f703b..17355017 100644 --- a/.agents/pm/history/pm-github-rap0.jsonl +++ b/.agents/pm/history/pm-github-rap0.jsonl @@ -2,3 +2,4 @@ {"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-hptv.toon b/.agents/pm/issues/pm-github-hptv.toon index 4fb0fc69..f1b05074 100644 --- a/.agents/pm/issues/pm-github-hptv.toon +++ b/.agents/pm/issues/pm-github-hptv.toon @@ -6,9 +6,9 @@ status: in_progress priority: 1 tags: [] created_at: "2026-10-05T08:28:54.625Z" -updated_at: "2026-10-07T11:33:39.558Z" +updated_at: "2026-10-07T12:39:25.378Z" author: codex-sol -comments[59]{created_at,author,text}: +comments[60]{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." @@ -68,6 +68,7 @@ comments[59]{created_at,author,text}: "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." files[15]{path,scope}: .github/workflows/pm-github-sync.yml,project docs/coverage-gap-inventory.md,project diff --git a/.agents/pm/issues/pm-github-rap0.toon b/.agents/pm/issues/pm-github-rap0.toon index 0e3b49ff..17253070 100644 --- a/.agents/pm/issues/pm-github-rap0.toon +++ b/.agents/pm/issues/pm-github-rap0.toon @@ -1,12 +1,12 @@ id: pm-github-rap0 -title: "The comment-sync per-comment write-failure test is root-sensitive: withReadOnlyDirectory cannot make the tasks directory unwritable for root" +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-07T08:33:51.539Z" +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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 22c8f245..0fb08323 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ ### Fixed -- The comment-sync per-comment write-failure test is root-sensitive: withReadOnlyDirectory cannot make the tasks directory unwritable for root ([pm-github-rap0](https://github.com/unbraind/pm-github/blob/main/.agents/pm/issues/pm-github-rap0.toon)) +- 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 diff --git a/README.md b/README.md index c942d6c8..d989cce4 100644 --- a/README.md +++ b/README.md @@ -294,7 +294,7 @@ The `gate` receipt reports the verdict, scanned file count, findings, and allowl ## 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** → `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`). +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)): From bf13c08732c81245d5c554f10aa53b5524efcdb6 Mon Sep 17 00:00:00 2001 From: SteveBot <1153461+unbraind@users.noreply.github.com> Date: Wed, 7 Oct 2026 15:53:49 +0200 Subject: [PATCH 60/60] Give the gate's size-cap, allowlist-JSON and unreadable-change refusals clean diagnostics --- .agents/pm/history/pm-github-hptv.jsonl | 1 + .agents/pm/issues/pm-github-hptv.toon | 5 +++-- gate.ts | 6 +++--- test/gate.test.ts | 7 ++++++- 4 files changed, 13 insertions(+), 6 deletions(-) diff --git a/.agents/pm/history/pm-github-hptv.jsonl b/.agents/pm/history/pm-github-hptv.jsonl index f1340678..e9695fa5 100644 --- a/.agents/pm/history/pm-github-hptv.jsonl +++ b/.agents/pm/history/pm-github-hptv.jsonl @@ -108,3 +108,4 @@ {"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/issues/pm-github-hptv.toon b/.agents/pm/issues/pm-github-hptv.toon index f1b05074..f4ba6a3c 100644 --- a/.agents/pm/issues/pm-github-hptv.toon +++ b/.agents/pm/issues/pm-github-hptv.toon @@ -6,9 +6,9 @@ status: in_progress priority: 1 tags: [] created_at: "2026-10-05T08:28:54.625Z" -updated_at: "2026-10-07T12:39:25.378Z" +updated_at: "2026-10-07T13:42:15.444Z" author: codex-sol -comments[60]{created_at,author,text}: +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." @@ -69,6 +69,7 @@ comments[60]{created_at,author,text}: "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 diff --git a/gate.ts b/gate.ts index 7cc32bb5..30da93a6 100644 --- a/gate.ts +++ b/gate.ts @@ -747,7 +747,7 @@ export function collectTrackerChange( } if (size > UNTRACKED_FILE_BYTE_CAP) { throw new GateInputError( - `pm github gate: untracked tracker file is too large to scan ; refusing to guess.`, + `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)); @@ -807,7 +807,7 @@ export function readGateAllowlist( try { parsed = JSON.parse(raw); } catch { - throw new GateInputError(`pm github gate: allowlist file is not valid JSON: (allowlist)`); + 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`); @@ -956,7 +956,7 @@ export function runTrackerGate(input: TrackerGateInput): GateReport { // 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.`, + "pm github gate: could not read the proposed tracker change; input is unavailable.", ); } diff --git a/test/gate.test.ts b/test/gate.test.ts index 8dde28c3..3b2b30ed 100644 --- a/test/gate.test.ts +++ b/test/gate.test.ts @@ -570,7 +570,8 @@ test("an oversized untracked tracker file fails closed instead of being skipped" 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 }), GateInputError); + 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 }); } @@ -900,6 +901,10 @@ test("pm github gate scans an explicit --diff file and honors --allowlist", asyn 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 }); }