diff --git a/.agents/pm/history/pm-github-9cjx.jsonl b/.agents/pm/history/pm-github-9cjx.jsonl new file mode 100644 index 0000000..89bb328 --- /dev/null +++ b/.agents/pm/history/pm-github-9cjx.jsonl @@ -0,0 +1,4 @@ +{"ts":"2026-08-17T07:32:29.666Z","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":"create","patch":[{"op":"add","path":"/metadata/id","value":"pm-github-9cjx"},{"op":"add","path":"/metadata/title","value":"Reach exact all-source coverage and include every operational script"},{"op":"add","path":"/metadata/description","value":"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."},{"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":["all-source","coverage","quality-gate","scripts","tdd"]},{"op":"add","path":"/metadata/created_at","value":"2026-08-17T07:32:29.666Z"},{"op":"add","path":"/metadata/updated_at","value":"2026-08-17T07:32:29.666Z"},{"op":"add","path":"/metadata/author","value":"codex"},{"op":"add","path":"/metadata/estimated_minutes","value":960},{"op":"add","path":"/metadata/acceptance_criteria","value":"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."},{"op":"add","path":"/metadata/parent","value":"pm-github-n3z3"},{"op":"add","path":"/metadata/expected_result","value":"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."},{"op":"add","path":"/metadata/actual_result","value":"The source walker skips scripts and aggregate coverage remains 91.83/82.14/91.75 with thresholds fixed at 88/79/89."},{"op":"add","path":"/metadata/dependencies","value":[{"id":"pm-github-n3z3","kind":"related","created_at":"2026-08-17T07:32:29.666Z","author":"codex","source_kind":"cli:create:dep","author_source":"detected"}]},{"op":"add","path":"/metadata/docs","value":[{"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"}]}],"before_hash":"3cc22dff72be7b14824654a7a64ea62b04799939b2fee54c1b5f52ca60bf6df0","after_hash":"a93c9c1d275d8774059e9db2b614ff2d34af31ae22b5ad9cda3ef09a05043dbb","item_hash_version":2,"message":"Track the separately measured exact all-source quality gap without conflating it with the completeness fix"} +{"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"} diff --git a/.agents/pm/history/pm-github-ep0u.jsonl b/.agents/pm/history/pm-github-ep0u.jsonl new file mode 100644 index 0000000..bcb826d --- /dev/null +++ b/.agents/pm/history/pm-github-ep0u.jsonl @@ -0,0 +1,18 @@ +{"ts":"2026-08-17T07:22:41.554Z","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":"create","patch":[{"op":"add","path":"/metadata/id","value":"pm-github-ep0u"},{"op":"add","path":"/metadata/title","value":"Refuse incomplete pm item corpora before GitHub imports, exports, and syncs"},{"op":"add","path":"/metadata/description","value":"readPmItems requests an arbitrary 10,000-row ceiling and trusts permissive JSON shapes without validating the CLI completeness, omission, pagination, count, or row contracts. A large, unreadable, budget-compacted, or malformed tracker can therefore be consumed as complete, risking duplicate imports, skipped exports, and incorrect GitHub state synchronization."},{"op":"add","path":"/metadata/type","value":"Issue"},{"op":"add","path":"/metadata/status","value":"in_progress"},{"op":"add","path":"/metadata/priority","value":1},{"op":"add","path":"/metadata/tags","value":["agent-ux","correctness","data-loss-prevention","list-all","sdk"]},{"op":"add","path":"/metadata/created_at","value":"2026-08-17T07:22:41.554Z"},{"op":"add","path":"/metadata/updated_at","value":"2026-08-17T07:22:41.554Z"},{"op":"add","path":"/metadata/author","value":"codex"},{"op":"add","path":"/metadata/estimated_minutes","value":240},{"op":"add","path":"/metadata/acceptance_criteria","value":"Remove the row ceiling and request full item fields and bodies; fail closed on missing or contradictory completeness, omission, pagination, budget, and count receipts; runtime-validate every field consumed by import, export, sync, project, and search fallback paths; prove behavior using the installed pm CLI against a fresh multi-item tracker and behavioral malformed-envelope tests; keep package and linked PM tests green; retain the 64 MiB output safety cap; use the current pm CLI/SDK toolchain without inline or dynamic imports."},{"op":"add","path":"/metadata/severity","value":"high"},{"op":"add","path":"/metadata/repro_steps","value":"Inspect index.ts readPmItems; provide a successful list-all envelope with has_more true, truncated true, partial completeness, omissions, mismatched counts, duplicate ids, or malformed consumed fields; the current parser accepts its items."},{"op":"add","path":"/metadata/expected_result","value":"Every whole-tracker consumer requests an unbounded full corpus and refuses any response that does not prove complete, omission-free, unpaginated, count-consistent, uniquely identified, runtime-valid rows before planning or performing GitHub operations."},{"op":"add","path":"/metadata/actual_result","value":"readPmItems invokes pm list-all with --limit 10000, accepts arrays or loosely shaped envelopes, defaults missing rows to an empty array, casts them to PmItem[], and ignores all completeness receipts."},{"op":"add","path":"/metadata/affected_version","value":"2026.8.17"},{"op":"add","path":"/metadata/component","value":"workspace reads"},{"op":"add","path":"/metadata/regression","value":false}],"before_hash":"3cc22dff72be7b14824654a7a64ea62b04799939b2fee54c1b5f52ca60bf6df0","after_hash":"78c0a51bd40368707cb6475ec413deb141c329786e19119c1940861147a34db6","item_hash_version":2,"message":"Track the final fleet-wide whole-corpus correctness gap before implementation"} +{"ts":"2026-08-17T07:22:56.135Z","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":"claim","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-08-17T07:22:56.135Z"},{"op":"add","path":"/metadata/assignee","value":"codex"},{"op":"add","path":"/metadata/claim_principal","value":"codex"}],"before_hash":"78c0a51bd40368707cb6475ec413deb141c329786e19119c1940861147a34db6","after_hash":"813c048a27680cec2f80dfb9406338b5d79cf58f42709eb95eb56c76faade7f3","item_hash_version":2,"message":"Claim the completeness tranche in an isolated package worktree"} +{"ts":"2026-08-17T07:22:56.690Z","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:22:56.690Z"},{"op":"add","path":"/metadata/files","value":[{"path":"index.ts","scope":"project"},{"path":"test/complete-corpus.test.ts","scope":"project"}]}],"before_hash":"813c048a27680cec2f80dfb9406338b5d79cf58f42709eb95eb56c76faade7f3","after_hash":"5dac0335418e24f07c2f2e5c122083b6861e3a90c7c896e969fcea50d9d556dd","item_hash_version":2,"message":"Link the whole-corpus reader and its behavioral acceptance suite"} +{"ts":"2026-08-17T07:22:57.137Z","author":"codex","author_source":"configured","agent_harness":"codex","agent_instance":"839e601757a3957be5ebc227","agent_provenance":{"model":null,"effort":null,"role":null,"topic":null},"op":"docs_add","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-08-17T07:22:57.137Z"},{"op":"add","path":"/metadata/docs","value":[{"path":"https://github.com/unbraind/pm-cli-companion/blob/main/.agents/pm/tasks/pm-cli-website-tv05.toon","scope":"global","note":"Companion fleet completeness task"},{"path":"README.md","scope":"project","note":"Document fail-closed whole-tracker reads"}]}],"before_hash":"5dac0335418e24f07c2f2e5c122083b6861e3a90c7c896e969fcea50d9d556dd","after_hash":"b2d4b529544f1ead628697e1ceb2bd93dbd557677d235028fed3e89e375b27cf","item_hash_version":2,"message":"Link user guidance and companion ownership"} +{"ts":"2026-08-17T07:22:57.631Z","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:22:57.631Z"},{"op":"add","path":"/metadata/tests","value":[{"command":"npm run coverage","path":"test/complete-corpus.test.ts","scope":"project","timeout_seconds":600,"note":"Runs the complete package gate including installed-CLI and malformed-envelope behaviors."}]}],"before_hash":"b2d4b529544f1ead628697e1ceb2bd93dbd557677d235028fed3e89e375b27cf","after_hash":"8cc1f2ed5d6732914442f77cd6f6a991ec0651e6943e0ae94c4e67f1f088e5c4","item_hash_version":2,"message":"Bind the package quality gate before implementation"} +{"ts":"2026-08-17T07:32:29.201Z","author":"codex","author_source":"configured","agent_harness":"codex","agent_instance":"839e601757a3957be5ebc227","agent_provenance":{"model":null,"effort":null,"role":null,"topic":null},"op":"note_add","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-08-17T07:32:29.201Z"},{"op":"add","path":"/metadata/notes","value":[{"created_at":"2026-08-17T07:32:29.200Z","author":"codex","text":"TDD evidence: the new focused suite first failed before implementation because index.ts exported none of completePmListArgs, decodeCompletePmItems, or readPmItems. After implementation, direct node --test used the host-installed pm CLI 2026.8.17 and passed 5/5, including a fresh tracker with one closed GitHub-linked issue and one open task. npm run coverage then passed the full package suite against the pinned 2026.8.15 development CLI. The generated LCOV currently measures the two production files at 91.83% lines, 82.14% branches, and 91.75% functions; this tranche did not overstate the separate exact-coverage mandate."}]}],"before_hash":"8cc1f2ed5d6732914442f77cd6f6a991ec0651e6943e0ae94c4e67f1f088e5c4","after_hash":"ef5bdfcb65017c677f232efaf0250ad3dd199279a0dc2fd910a1ef81835ef987","item_hash_version":2,"message":"Record red-green and honest all-source coverage evidence"} +{"ts":"2026-08-17T07:33:14.984Z","author":"codex","author_source":"configured","agent_harness":"codex","agent_instance":"839e601757a3957be5ebc227","agent_provenance":{"model":null,"effort":null,"role":null,"topic":null},"op":"docs_add","patch":[{"op":"replace","path":"/metadata/docs/1/note","value":"Upstream typed whole-list SDK primitive request discovered during implementation"},{"op":"replace","path":"/metadata/docs/1/scope","value":"global"},{"op":"replace","path":"/metadata/docs/1/path","value":"https://github.com/unbraind/pm-cli/issues/1052"},{"op":"add","path":"/metadata/docs/2","value":{"path":"README.md","scope":"project","note":"Document fail-closed whole-tracker reads"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-08-17T07:33:14.984Z"}],"before_hash":"ef5bdfcb65017c677f232efaf0250ad3dd199279a0dc2fd910a1ef81835ef987","after_hash":"bcfe9c88d3d2a157a6f1a45c341ea7bba189513d45efe88df916f04cae113b2b","item_hash_version":2,"message":"Link the reusable SDK follow-up rather than hiding duplicated package plumbing"} +{"ts":"2026-08-17T07:33:42.981Z","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":"replace","path":"/metadata/actual_result","value":"readPmItems now invokes list-all without a row limit and requests strict, full, unbounded output. decodeCompletePmItems rejects missing or contradictory pagination, source completeness, omission, projection, output-budget, count, identity, and consumed-field contracts. The real installed-CLI acceptance returns both a closed GitHub-linked issue and an open task from a fresh tracker."},{"op":"replace","path":"/metadata/updated_at","value":"2026-08-17T07:33:42.981Z"},{"op":"add","path":"/metadata/resolution","value":"Replaced the permissive cast and 10,000-row ceiling with an explicit whole-corpus argv contract and fail-closed runtime decoder; added adversarial receipt/row tests, fresh-tracker installed-CLI acceptance, README safety guidance, and upstream SDK issue 1052 for a reusable primitive."}],"before_hash":"bcfe9c88d3d2a157a6f1a45c341ea7bba189513d45efe88df916f04cae113b2b","after_hash":"13ada3756be5496524c1a826fbaa75e8520bb83efb351cdc48368640bdc76380","item_hash_version":2,"message":"Record the implemented complete-read contract before closure"} +{"ts":"2026-08-17T07:33:43.378Z","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":"close","patch":[{"op":"remove","path":"/metadata/assignee"},{"op":"replace","path":"/metadata/updated_at","value":"2026-08-17T07:33:43.378Z"},{"op":"replace","path":"/metadata/status","value":"closed"},{"op":"add","path":"/metadata/closed_at","value":"2026-08-17T07:33:43.363Z"},{"op":"add","path":"/metadata/completed_at","value":"2026-08-17T07:33:43.363Z"},{"op":"add","path":"/metadata/close_reason","value":"Shipped the fail-closed whole-corpus read: 5 focused tests pass with host pm 2026.8.17, the full coverage gate passes with the pinned 2026.8.15 CLI, and incomplete or malformed list-all answers cannot reach import, export, sync, project, or search planning."}],"before_hash":"13ada3756be5496524c1a826fbaa75e8520bb83efb351cdc48368640bdc76380","after_hash":"bfb96495a00331d9f6ca5a3e41963eb8cbe0ee095e61fd5ddf349d5e79a7bb4f","item_hash_version":2} +{"ts":"2026-08-17T07:36:19.273Z","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":"add","path":"/metadata/files/2","value":{"path":"package.json","scope":"project"}},{"op":"add","path":"/metadata/files/3","value":{"path":"package-lock.json","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-08-17T07:36:19.273Z"}],"before_hash":"bfb96495a00331d9f6ca5a3e41963eb8cbe0ee095e61fd5ddf349d5e79a7bb4f","after_hash":"2ab80f5342fcc86fcae796baedaed0ab2d6e166c8d75bf6fae9ff42eb4bf732d","item_hash_version":2,"message":"Link the latest pm CLI development pin used by the completed acceptance"} +{"ts":"2026-08-17T07:36:19.650Z","author":"codex","author_source":"configured","agent_harness":"codex","agent_instance":"839e601757a3957be5ebc227","agent_provenance":{"model":null,"effort":null,"role":null,"topic":null},"op":"note_add","patch":[{"op":"add","path":"/metadata/notes/1","value":{"created_at":"2026-08-17T07:36:19.650Z","author":"codex","text":"Toolchain follow-up: updated the exact development pin from @unbrained/pm-cli 2026.8.15 to the installed latest 2026.8.17 using the cached registry artifact. The public peer and enforced manifest remain the compatible >=2026.8.3 floor because the strict/output contracts used here shipped by that floor; development and CI now verify the latest implementation without narrowing consumer compatibility."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-08-17T07:36:19.650Z"}],"before_hash":"2ab80f5342fcc86fcae796baedaed0ab2d6e166c8d75bf6fae9ff42eb4bf732d","after_hash":"06762969f8a442ebb6a5ec41416e6d71173ee8d514a8a3a4a7f1c621e8f85f5f","item_hash_version":2,"message":"Record the latest CLI pin and compatibility-floor decision"} +{"ts":"2026-08-17T07:36:41.342Z","author":"codex","author_source":"configured","agent_harness":"codex","agent_instance":"839e601757a3957be5ebc227","agent_provenance":{"model":null,"effort":null,"role":null,"topic":null},"op":"note_add","patch":[{"op":"add","path":"/metadata/notes/2","value":{"created_at":"2026-08-17T07:36:41.342Z","author":"codex","text":"Correction to the preceding toolchain note: the first npm install command omitted --save-exact and wrote ^2026.8.17 even though the note called it exact. The compatibility test immediately failed on that caret range. Re-running npm install with --save-exact restored the intended 2026.8.17 pin; the combined compatibility and complete-corpus suite then passed 9/9. This correction is appended rather than rewriting the earlier record."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-08-17T07:36:41.342Z"}],"before_hash":"06762969f8a442ebb6a5ec41416e6d71173ee8d514a8a3a4a7f1c621e8f85f5f","after_hash":"f44a02b47991ca282d528ea566c017934c942e83f29a75a9f0342eb0d9931243","item_hash_version":2,"message":"Correct the toolchain evidence from the test-observed manifest state"} +{"ts":"2026-08-17T07:38:19.029Z","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":"replace","path":"/metadata/close_reason","value":"Shipped the fail-closed whole-corpus read: adversarial and fresh-tracker acceptance pass with @unbraind/pm-cli 2026.8.17, the full release gate and Bun dependency install pass, and incomplete or malformed list-all answers cannot reach import, export, sync, project, or search planning."},{"op":"replace","path":"/metadata/updated_at","value":"2026-08-17T07:38:19.029Z"}],"before_hash":"f44a02b47991ca282d528ea566c017934c942e83f29a75a9f0342eb0d9931243","after_hash":"c0349b2d741d800f9dd7c19e0b8d60cbbf0335463b58a42660fbdd03857aae73","item_hash_version":2,"message":"Align the closure summary with the final latest-CLI and Bun verification"} +{"ts":"2026-08-17T07:38:19.589Z","author":"codex","author_source":"configured","agent_harness":"codex","agent_instance":"839e601757a3957be5ebc227","agent_provenance":{"model":null,"effort":null,"role":null,"topic":null},"op":"note_add","patch":[{"op":"add","path":"/metadata/notes/3","value":{"created_at":"2026-08-17T07:38:19.589Z","author":"codex","text":"Final local delivery gates on the current tree: bun 1.3.5 installed the complete dependency graph with no changes; npm ci installed 73 packages with zero vulnerabilities; npm run release:check exited 0 after typecheck, build, 85-of-85 docstrings, the full behavioral coverage suite, production audit, package dry-run, and changelog verification. The generated Bun lock was intentionally not tracked because CI uses bun install --no-save from package-lock.json."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-08-17T07:38:19.589Z"}],"before_hash":"c0349b2d741d800f9dd7c19e0b8d60cbbf0335463b58a42660fbdd03857aae73","after_hash":"57db89e9c3b1196515c69dd503426d7966d8ad804db4b0d562f534b339a1b9ea","item_hash_version":2,"message":"Record package-manager and release-gate acceptance"} +{"ts":"2026-08-17T07:40:47.295Z","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":"replace","path":"/metadata/updated_at","value":"2026-08-17T07:40:47.295Z"},{"op":"add","path":"/metadata/dependencies","value":[{"id":"pm-github-9cjx","kind":"related","created_at":"2026-08-17T07:40:47.251Z","author":"codex","source_kind":"cli:update:dep","author_source":"detected"},{"id":"pm-github-zqad","kind":"related","created_at":"2026-08-17T07:40:47.251Z","author":"codex","source_kind":"cli:update:dep","author_source":"detected"}]}],"before_hash":"57db89e9c3b1196515c69dd503426d7966d8ad804db4b0d562f534b339a1b9ea","after_hash":"3b059ae0900c5adbfa1db0506a413a7bcbbdfeed62fb57a7d07a14476e2562ad","item_hash_version":2,"message":"Link the separately measured exact-coverage and full-history privacy gates"} +{"ts":"2026-08-17T07:49:21.044Z","author":"codex","author_source":"configured","agent_harness":"codex","agent_instance":"839e601757a3957be5ebc227","agent_provenance":{"model":null,"effort":null,"role":null,"topic":null},"op":"note_add","patch":[{"op":"add","path":"/metadata/notes/4","value":{"created_at":"2026-08-17T07:49:21.044Z","author":"codex","text":"Exact-head CodeRabbit review identified a Windows regression in the production whole-corpus subprocess: npm exposes pm through pm.cmd, which spawnSync cannot execute directly without a Windows shell. Added a platform-scoped spawn option contract (shell only on win32), a regression test proving Windows and non-Windows behavior, and retained direct argument execution elsewhere. Greptile reported no corpus correctness, duplication, data-loss, or security defect. Sourcery's projection-extension concern is already explicit in the PmItem and decoder documentation; its suggested extra envelope wrapper decomposition was not applied because each check is intentionally adjacent and auditable, and one-use wrappers would violate the repo-wide helper rule."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-08-17T07:49:21.044Z"}],"before_hash":"3b059ae0900c5adbfa1db0506a413a7bcbbdfeed62fb57a7d07a14476e2562ad","after_hash":"8106b9eb7ed44056207a244e8edbe9482aff0a99575bf7e688744782c82309b0","item_hash_version":2,"message":"Record exact-head bot review response"} +{"ts":"2026-08-17T07:56:04.206Z","author":"codex","author_source":"configured","agent_harness":"codex","agent_instance":"839e601757a3957be5ebc227","agent_provenance":{"model":null,"effort":null,"role":null,"topic":null},"op":"note_add","patch":[{"op":"add","path":"/metadata/notes/5","value":{"created_at":"2026-08-17T07:56:04.206Z","author":"codex","text":"Superseding the initial Windows-shell correction: CodeRabbit's second exact-head review correctly identified that shell:true would expose user-selected pmRoot paths to cmd.exe metacharacter interpretation. The Windows strategy now reads the installed @unbrained/pm-cli package metadata, resolves its declared pm JavaScript entry point, and executes it directly with process.execPath and an argument vector. A real installed-CLI regression creates and reads a tracker whose path contains ampersand and parentheses using the Windows strategy; no shell is involved on any platform."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-08-17T07:56:04.206Z"}],"before_hash":"8106b9eb7ed44056207a244e8edbe9482aff0a99575bf7e688744782c82309b0","after_hash":"d08424f1466734e09e587452ae9ed7620b82454f27d00fa821c7a5ccd68f0920","item_hash_version":2,"message":"Record secure Windows launcher review correction"} +{"ts":"2026-08-17T08:04:59.744Z","author":"codex","author_source":"configured","agent_harness":"codex","agent_instance":"839e601757a3957be5ebc227","agent_provenance":{"model":null,"effort":null,"role":null,"topic":null},"op":"note_add","patch":[{"op":"add","path":"/metadata/notes/6","value":{"created_at":"2026-08-17T08:04:59.744Z","author":"codex","text":"Third exact-head CodeRabbit review identified that resolving the peer CLI relative to a packed standalone extension could fail because the published extension contains only dist artifacts. Windows relaunch now consumes the host-owned PM_CLI_PACKAGE_ROOT contract set by both CLI and MCP hosts, reads the host package's declared bin.pm entry, rejects entries escaping that root, and never resolves the peer from the extension. The metacharacter-path acceptance injects the host package root exactly as an installed host does. A separate real npm-pack -> fresh pm install -> github export dry-run passed with an extension containing only published artifacts. Sourcery's follow-up hardening was applied: primitive-only expected receipt values, centralized version/command contract constants, and adversarial future-version, null/array receipt, and non-array omission-group cases."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-08-17T08:04:59.744Z"}],"before_hash":"d08424f1466734e09e587452ae9ed7620b82454f27d00fa821c7a5ccd68f0920","after_hash":"9f162a67f531865fc15944b0da49461b8ab7c53dc61c496db99c8d17d6f894ca","item_hash_version":2,"message":"Record installed-layout and receipt-drift review fixes"} diff --git a/.agents/pm/history/pm-github-zqad.jsonl b/.agents/pm/history/pm-github-zqad.jsonl new file mode 100644 index 0000000..a88086d --- /dev/null +++ b/.agents/pm/history/pm-github-zqad.jsonl @@ -0,0 +1 @@ +{"ts":"2026-08-17T07:40:36.004Z","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":"create","patch":[{"op":"add","path":"/metadata/id","value":"pm-github-zqad"},{"op":"add","path":"/metadata/title","value":"Gate historical identities and host-path privacy across every Git object"},{"op":"add","path":"/metadata/description","value":"A full-object privacy read found zero high-confidence credential signatures and zero current tracked absolute host paths, but historical objects still contain 18 host-path lines and the commit graph includes one machine-local identity already tracked by the companion fleet issue. The repository is public and those commits are ancestors of releases, so destructive history rewriting requires an explicit maintainer decision; a forward gate is independently required."},{"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":["git-history","identity-gate","privacy","release","security"]},{"op":"add","path":"/metadata/created_at","value":"2026-08-17T07:40:36.004Z"},{"op":"add","path":"/metadata/updated_at","value":"2026-08-17T07:40:36.004Z"},{"op":"add","path":"/metadata/author","value":"codex"},{"op":"add","path":"/metadata/estimated_minutes","value":480},{"op":"add","path":"/metadata/acceptance_criteria","value":"Add a checked-in justified identity allowlist and fail-closed audit covering reachable and unreachable commits and annotated tags; add a current-tree and all-object secret/private-path gate with false-positive fixtures; classify every existing historical match without printing sensitive values into logs; obtain an explicit human decision before rewriting published history; if approved, preserve release metadata and verify old sensitive objects are no longer remotely reachable; if declined, record the residual risk and enforce the forward gate; include the audits in release:check and CI with behavioral negative controls."},{"op":"add","path":"/metadata/severity","value":"high"},{"op":"add","path":"/metadata/repro_steps","value":"Run a high-confidence credential scan and local-host-path scan over git cat-file --batch-all-objects --batch, then enumerate unique author and committer identities across all reachable refs. Compare the result with current tracked files."},{"op":"add","path":"/metadata/expected_result","value":"Every reachable and unreachable commit/tag/blob is scanned; only explicitly justified identities are allowed; new credential or local-host-path material fails CI and release:check; every historical match is classified; any rewrite of published commits/tags occurs only after explicit maintainer approval and restoration planning."},{"op":"add","path":"/metadata/actual_result","value":"Current tracked files contain zero high-confidence secret or absolute-host-path matches. Full Git object scanning reports zero high-confidence secrets and 18 historical host-path lines. The repository lacks a checked-in identity allowlist and all-object identity/privacy gate."},{"op":"add","path":"/metadata/component","value":"release governance"},{"op":"add","path":"/metadata/regression","value":false},{"op":"add","path":"/metadata/dependencies","value":[{"id":"pm-github-1jez","kind":"related","created_at":"2026-08-17T07:40:36.004Z","author":"codex","source_kind":"cli:create:dep","author_source":"detected"}]},{"op":"add","path":"/metadata/docs","value":[{"path":"https://github.com/unbraind/pm-cli-companion/blob/main/.agents/pm/issues/pm-cli-website-kxtl.toon","scope":"global","note":"Fleet commit identity gate and human rewrite decision"}]}],"before_hash":"3cc22dff72be7b14824654a7a64ea62b04799939b2fee54c1b5f52ca60bf6df0","after_hash":"c62c98a28cc4cce6ab75bde695ceef37a18d49f48c3f0384997463b001198fa6","item_hash_version":2,"message":"Track the full-history privacy and identity gate discovered by the release audit"} diff --git a/.agents/pm/issues/pm-github-ep0u.toon b/.agents/pm/issues/pm-github-ep0u.toon new file mode 100644 index 0000000..3b937b2 --- /dev/null +++ b/.agents/pm/issues/pm-github-ep0u.toon @@ -0,0 +1,47 @@ +id: pm-github-ep0u +title: "Refuse incomplete pm item corpora before GitHub imports, exports, and syncs" +description: "readPmItems requests an arbitrary 10,000-row ceiling and trusts permissive JSON shapes without validating the CLI completeness, omission, pagination, count, or row contracts. A large, unreadable, budget-compacted, or malformed tracker can therefore be consumed as complete, risking duplicate imports, skipped exports, and incorrect GitHub state synchronization." +type: Issue +status: closed +priority: 1 +tags[5]: agent-ux,correctness,data-loss-prevention,list-all,sdk +created_at: "2026-08-17T07:22:41.554Z" +updated_at: "2026-08-17T08:04:59.744Z" +closed_at: "2026-08-17T07:33:43.363Z" +completed_at: "2026-08-17T07:33:43.363Z" +claim_principal: codex +author: codex +estimated_minutes: 240 +acceptance_criteria: "Remove the row ceiling and request full item fields and bodies; fail closed on missing or contradictory completeness, omission, pagination, budget, and count receipts; runtime-validate every field consumed by import, export, sync, project, and search fallback paths; prove behavior using the installed pm CLI against a fresh multi-item tracker and behavioral malformed-envelope tests; keep package and linked PM tests green; retain the 64 MiB output safety cap; use the current pm CLI/SDK toolchain without inline or dynamic imports." +severity: high +repro_steps: "Inspect index.ts readPmItems; provide a successful list-all envelope with has_more true, truncated true, partial completeness, omissions, mismatched counts, duplicate ids, or malformed consumed fields; the current parser accepts its items." +resolution: "Replaced the permissive cast and 10,000-row ceiling with an explicit whole-corpus argv contract and fail-closed runtime decoder; added adversarial receipt/row tests, fresh-tracker installed-CLI acceptance, README safety guidance, and upstream SDK issue 1052 for a reusable primitive." +expected_result: "Every whole-tracker consumer requests an unbounded full corpus and refuses any response that does not prove complete, omission-free, unpaginated, count-consistent, uniquely identified, runtime-valid rows before planning or performing GitHub operations." +actual_result: "readPmItems now invokes list-all without a row limit and requests strict, full, unbounded output. decodeCompletePmItems rejects missing or contradictory pagination, source completeness, omission, projection, output-budget, count, identity, and consumed-field contracts. The real installed-CLI acceptance returns both a closed GitHub-linked issue and an open task from a fresh tracker." +affected_version: 2026.8.17 +component: workspace reads +regression: false +dependencies[2]{id,kind,created_at,author,source_kind,author_source}: + pm-github-9cjx,related,"2026-08-17T07:40:47.251Z",codex,"cli:update:dep",detected + pm-github-zqad,related,"2026-08-17T07:40:47.251Z",codex,"cli:update:dep",detected +notes[7]{created_at,author,text}: + "2026-08-17T07:32:29.200Z",codex,"TDD evidence: the new focused suite first failed before implementation because index.ts exported none of completePmListArgs, decodeCompletePmItems, or readPmItems. After implementation, direct node --test used the host-installed pm CLI 2026.8.17 and passed 5/5, including a fresh tracker with one closed GitHub-linked issue and one open task. npm run coverage then passed the full package suite against the pinned 2026.8.15 development CLI. The generated LCOV currently measures the two production files at 91.83% lines, 82.14% branches, and 91.75% functions; this tranche did not overstate the separate exact-coverage mandate." + "2026-08-17T07:36:19.650Z",codex,"Toolchain follow-up: updated the exact development pin from @unbrained/pm-cli 2026.8.15 to the installed latest 2026.8.17 using the cached registry artifact. The public peer and enforced manifest remain the compatible >=2026.8.3 floor because the strict/output contracts used here shipped by that floor; development and CI now verify the latest implementation without narrowing consumer compatibility." + "2026-08-17T07:36:41.342Z",codex,"Correction to the preceding toolchain note: the first npm install command omitted --save-exact and wrote ^2026.8.17 even though the note called it exact. The compatibility test immediately failed on that caret range. Re-running npm install with --save-exact restored the intended 2026.8.17 pin; the combined compatibility and complete-corpus suite then passed 9/9. This correction is appended rather than rewriting the earlier record." + "2026-08-17T07:38:19.589Z",codex,"Final local delivery gates on the current tree: bun 1.3.5 installed the complete dependency graph with no changes; npm ci installed 73 packages with zero vulnerabilities; npm run release:check exited 0 after typecheck, build, 85-of-85 docstrings, the full behavioral coverage suite, production audit, package dry-run, and changelog verification. The generated Bun lock was intentionally not tracked because CI uses bun install --no-save from package-lock.json." + "2026-08-17T07:49:21.044Z",codex,"Exact-head CodeRabbit review identified a Windows regression in the production whole-corpus subprocess: npm exposes pm through pm.cmd, which spawnSync cannot execute directly without a Windows shell. Added a platform-scoped spawn option contract (shell only on win32), a regression test proving Windows and non-Windows behavior, and retained direct argument execution elsewhere. Greptile reported no corpus correctness, duplication, data-loss, or security defect. Sourcery's projection-extension concern is already explicit in the PmItem and decoder documentation; its suggested extra envelope wrapper decomposition was not applied because each check is intentionally adjacent and auditable, and one-use wrappers would violate the repo-wide helper rule." + "2026-08-17T07:56:04.206Z",codex,"Superseding the initial Windows-shell correction: CodeRabbit's second exact-head review correctly identified that shell:true would expose user-selected pmRoot paths to cmd.exe metacharacter interpretation. The Windows strategy now reads the installed @unbrained/pm-cli package metadata, resolves its declared pm JavaScript entry point, and executes it directly with process.execPath and an argument vector. A real installed-CLI regression creates and reads a tracker whose path contains ampersand and parentheses using the Windows strategy; no shell is involved on any platform." + "2026-08-17T08:04:59.744Z",codex,"Third exact-head CodeRabbit review identified that resolving the peer CLI relative to a packed standalone extension could fail because the published extension contains only dist artifacts. Windows relaunch now consumes the host-owned PM_CLI_PACKAGE_ROOT contract set by both CLI and MCP hosts, reads the host package's declared bin.pm entry, rejects entries escaping that root, and never resolves the peer from the extension. The metacharacter-path acceptance injects the host package root exactly as an installed host does. A separate real npm-pack -> fresh pm install -> github export dry-run passed with an extension containing only published artifacts. Sourcery's follow-up hardening was applied: primitive-only expected receipt values, centralized version/command contract constants, and adversarial future-version, null/array receipt, and non-array omission-group cases." +files[4]{path,scope}: + index.ts,project + test/complete-corpus.test.ts,project + package.json,project + package-lock.json,project +tests[1]{command,path,scope,timeout_seconds,note}: + npm run coverage,test/complete-corpus.test.ts,project,600,Runs the complete package gate including installed-CLI and malformed-envelope behaviors. +docs[3]{path,scope,note}: + "https://github.com/unbraind/pm-cli-companion/blob/main/.agents/pm/tasks/pm-cli-website-tv05.toon",global,Companion fleet completeness task + "https://github.com/unbraind/pm-cli/issues/1052",global,Upstream typed whole-list SDK primitive request discovered during implementation + README.md,project,Document fail-closed whole-tracker reads +close_reason: "Shipped the fail-closed whole-corpus read: adversarial and fresh-tracker acceptance pass with @unbraind/pm-cli 2026.8.17, the full release gate and Bun dependency install pass, and incomplete or malformed list-all answers cannot reach import, export, sync, project, or search planning." +body: "" diff --git a/.agents/pm/issues/pm-github-zqad.toon b/.agents/pm/issues/pm-github-zqad.toon new file mode 100644 index 0000000..43fc311 --- /dev/null +++ b/.agents/pm/issues/pm-github-zqad.toon @@ -0,0 +1,23 @@ +id: pm-github-zqad +title: Gate historical identities and host-path privacy across every Git object +description: "A full-object privacy read found zero high-confidence credential signatures and zero current tracked absolute host paths, but historical objects still contain 18 host-path lines and the commit graph includes one machine-local identity already tracked by the companion fleet issue. The repository is public and those commits are ancestors of releases, so destructive history rewriting requires an explicit maintainer decision; a forward gate is independently required." +type: Issue +status: open +priority: 1 +tags[5]: git-history,identity-gate,privacy,release,security +created_at: "2026-08-17T07:40:36.004Z" +updated_at: "2026-08-17T07:40:36.004Z" +author: codex +estimated_minutes: 480 +acceptance_criteria: "Add a checked-in justified identity allowlist and fail-closed audit covering reachable and unreachable commits and annotated tags; add a current-tree and all-object secret/private-path gate with false-positive fixtures; classify every existing historical match without printing sensitive values into logs; obtain an explicit human decision before rewriting published history; if approved, preserve release metadata and verify old sensitive objects are no longer remotely reachable; if declined, record the residual risk and enforce the forward gate; include the audits in release:check and CI with behavioral negative controls." +severity: high +repro_steps: "Run a high-confidence credential scan and local-host-path scan over git cat-file --batch-all-objects --batch, then enumerate unique author and committer identities across all reachable refs. Compare the result with current tracked files." +expected_result: "Every reachable and unreachable commit/tag/blob is scanned; only explicitly justified identities are allowed; new credential or local-host-path material fails CI and release:check; every historical match is classified; any rewrite of published commits/tags occurs only after explicit maintainer approval and restoration planning." +actual_result: Current tracked files contain zero high-confidence secret or absolute-host-path matches. Full Git object scanning reports zero high-confidence secrets and 18 historical host-path lines. The repository lacks a checked-in identity allowlist and all-object identity/privacy gate. +component: release governance +regression: false +dependencies[1]{id,kind,created_at,author,source_kind,author_source}: + pm-github-1jez,related,"2026-08-17T07:40:36.004Z",codex,"cli:create:dep",detected +docs[1]{path,scope,note}: + "https://github.com/unbraind/pm-cli-companion/blob/main/.agents/pm/issues/pm-cli-website-kxtl.toon",global,Fleet commit identity gate and human rewrite decision +body: "" diff --git a/.agents/pm/tasks/pm-github-9cjx.toon b/.agents/pm/tasks/pm-github-9cjx.toon new file mode 100644 index 0000000..fbfbf58 --- /dev/null +++ b/.agents/pm/tasks/pm-github-9cjx.toon @@ -0,0 +1,26 @@ +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 +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" +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." +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. +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 + package.json,project + test,project +tests[1]{command,path,scope,timeout_seconds,note}: + npm run coverage,scripts/coverage-gate.ts,project,900,Current ratcheted gate; this item remains open until it measures all executable sources at exact 100/100/100/100. +docs[2]{path,scope,note}: + "https://github.com/unbraind/pm-cli-companion/blob/main/.agents/pm/epics/pm-cli-website-ktip.toon",global,Fleet exact coverage mandate + "https://github.com/unbraind/pm-cli-companion/blob/main/.agents/pm/issues/pm-cli-website-odki.toon",global,Fleet operational script coverage gap +body: "" diff --git a/CHANGELOG.md b/CHANGELOG.md index 5589380..af5c85a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Fixed + +- 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 ### Fixed diff --git a/README.md b/README.md index d324b42..719d237 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,21 @@ pm install npm:pm-github --global | `preflight` | early warning when a mutating github command lacks a token | | `search` | `github` search provider — `pm search` reaches GitHub for imported items | +### Whole-workspace read safety + +Import idempotency, export, status sync, Projects v2 sync, and search fallback +all depend on seeing every pm item, including closed and canceled work. Before +any of those paths plans or performs a GitHub operation, pm-github requests a +strict, full, unbounded `pm list-all` response and verifies its completeness, +omission, pagination, output-budget, count, identity, and consumed-field +contracts. An incomplete or unverifiable response fails closed; it is never +treated as an empty or partial workspace. + +The subprocess still has a 64 MiB byte safety cap. Set +`PM_JSON_MAX_BUFFER=` to a larger positive safe integer for an unusually +large tracker. Raising that transport cap does not weaken the completeness +checks. + ## Import ### `pm github import ` (or `pm gh-issues import`) diff --git a/index.ts b/index.ts index 73e3c8d..0d5225c 100644 --- a/index.ts +++ b/index.ts @@ -848,17 +848,280 @@ export function scopeItemsByIds( return { selected, missing }; } -// Read every pm item (active + closed) via `pm list-all --full --include-body` -// so the idempotency index never misses closed issues and re-creates them. -function readPmItems(pmRoot: string): PmItem[] { +/** True only for a JSON object, excluding arrays and `null`. */ +function isJsonRecord(value: unknown): value is Record { + return value !== null && typeof value === "object" && !Array.isArray(value); +} + +/** Render an untrusted JSON value compactly inside a read-contract error. */ +function describeJsonValue(value: unknown): string { + const rendered = JSON.stringify(value); + return rendered === undefined ? String(value) : rendered; +} + +/** Require one exact field value from the `pm list-all` truthfulness envelope. */ +function requireCompletePmField( + actual: unknown, + expected: string | number | boolean | null, + field: string, +): void { + if (actual !== expected) { + throw new CommandError( + `Refusing unverifiable pm list-all output: ${field} must be ${describeJsonValue(expected)}; received ${describeJsonValue(actual)}.`, + ); + } +} + +/** Installed CLI receipt version and command accepted by the complete reader. */ +const COMPLETE_PM_READ_CONTRACT = { + version: 1, + command: "list", +} as const; + +/** + * Build the canonical installed-CLI invocation for a whole-workspace read. + * + * The output controls are deliberately explicit. `list-all` includes terminal + * items, `--output-include full` plus `--include-body` retains every field this + * integration consumes, `--strict-read` rejects unreadable records, and both + * amount and cost are unbounded. An arbitrary `--limit` must never be added: + * callers use this corpus to prevent duplicate imports and missing syncs. + * + * @param pmRoot - Workspace or tracker root accepted by the pm CLI `--path` flag. + * @returns Argument vector passed to the installed `pm` executable. + * @internal Exported so the acceptance test can bind the safety contract to the + * exact production invocation; it is removed from the published declaration. + */ +export function completePmListArgs(pmRoot: string): string[] { + return [ + "--path", + pmRoot, + "list-all", + "--json", + "--include-body", + "--output-include", + "full", + "--strict-read", + "--output-limit", + "unbounded", + "--output-budget", + "unbounded", + ]; +} + +/** + * Decode only a complete, unbounded `pm list-all` response. + * + * The subprocess JSON is untrusted. This gate independently verifies every + * completeness signal emitted by the current CLI, reconciles envelope counts, + * rejects duplicate identities, and validates each field consumed by GitHub + * import, export, state sync, Projects v2 sync, and search fallback paths. + * Missing receipts fail closed because an unverifiable read is not a whole + * workspace read. + * + * @param parsed - JSON decoded from the installed pm CLI. + * @returns Fresh runtime-validated item objects. + * @throws {@link CommandError} When a receipt, count, identity, or consumed row + * field is absent, incomplete, or contradictory. + * @internal Exported for direct adversarial contract tests and stripped from + * the published declaration surface. + */ +export function decodeCompletePmItems(parsed: unknown): PmItem[] { + if (!isJsonRecord(parsed)) { + throw new CommandError( + "Refusing unverifiable pm list-all output: the response must be a top-level object with completeness receipts.", + ); + } + if (!Array.isArray(parsed.items)) { + throw new CommandError("Refusing unverifiable pm list-all output: items must be an array."); + } + + requireCompletePmField(parsed.truncated, false, "truncated"); + requireCompletePmField(parsed.has_more, false, "has_more"); + requireCompletePmField(parsed.next_cursor, null, "next_cursor"); + + const completeness = isJsonRecord(parsed.completeness) ? parsed.completeness : {}; + requireCompletePmField(completeness.status, "complete", "completeness.status"); + requireCompletePmField(completeness.unreadable_item_count, 0, "completeness.unreadable_item_count"); + requireCompletePmField( + completeness.unreadable_directory_count, + 0, + "completeness.unreadable_directory_count", + ); + + const omission = isJsonRecord(parsed.omission_receipt) ? parsed.omission_receipt : {}; + requireCompletePmField(omission.has_omissions, false, "omission_receipt.has_omissions"); + requireCompletePmField( + omission.omitted_field_group_count, + 0, + "omission_receipt.omitted_field_group_count", + ); + if (!Array.isArray(omission.omitted_field_groups) || omission.omitted_field_groups.length !== 0) { + throw new CommandError( + "Refusing unverifiable pm list-all output: omission_receipt.omitted_field_groups must be empty.", + ); + } + + const projection = isJsonRecord(parsed.projection) ? parsed.projection : {}; + requireCompletePmField(projection.mode, "full", "projection.mode"); + + const readOutput = isJsonRecord(parsed.read_output) ? parsed.read_output : {}; + requireCompletePmField( + readOutput.contract_version, + COMPLETE_PM_READ_CONTRACT.version, + "read_output.contract_version", + ); + requireCompletePmField( + readOutput.command, + COMPLETE_PM_READ_CONTRACT.command, + "read_output.command", + ); + requireCompletePmField(readOutput.within_budget, true, "read_output.within_budget"); + requireCompletePmField(readOutput.strings_compacted, false, "read_output.strings_compacted"); + requireCompletePmField(readOutput.rows_compacted, false, "read_output.rows_compacted"); + requireCompletePmField(readOutput.result_omitted, false, "read_output.result_omitted"); + if ( + !Array.isArray(readOutput.requested_dimensions) + || !readOutput.requested_dimensions.includes("include") + || !readOutput.requested_dimensions.includes("amount") + || !readOutput.requested_dimensions.includes("cost") + ) { + throw new CommandError( + "Refusing unverifiable pm list-all output: read_output.requested_dimensions must include include, amount, and cost.", + ); + } + if ("output_budget_truncation" in parsed || "output_budget_exceeded" in parsed) { + throw new CommandError( + "Refusing unverifiable pm list-all output: a budget truncation or omission disclosure was present.", + ); + } + + if (!Number.isSafeInteger(parsed.count) || (parsed.count as number) < 0) { + throw new CommandError( + `Refusing unverifiable pm list-all output: count must be a non-negative safe integer; received ${describeJsonValue(parsed.count)}.`, + ); + } + if (!Number.isSafeInteger(parsed.total) || (parsed.total as number) < 0) { + throw new CommandError( + `Refusing unverifiable pm list-all output: total must be a non-negative safe integer; received ${describeJsonValue(parsed.total)}.`, + ); + } + if (parsed.items.length !== parsed.count) { + throw new CommandError( + `Refusing unverifiable pm list-all output: items.length ${parsed.items.length} must equal count ${String(parsed.count)}.`, + ); + } + if (parsed.count !== parsed.total) { + throw new CommandError( + `Refusing incomplete pm list-all output: count ${String(parsed.count)} must equal total ${String(parsed.total)}.`, + ); + } + + const ids = new Set(); + const items: PmItem[] = []; + for (const [index, item] of parsed.items.entries()) { + if (!isJsonRecord(item)) { + throw new CommandError(`Refusing unverifiable pm list-all output: item ${index} must be an object.`); + } + if (typeof item.id !== "string" || item.id.trim().length === 0) { + throw new CommandError(`Refusing unverifiable pm list-all output: item ${index} must have a non-empty id.`); + } + if (ids.has(item.id)) { + throw new CommandError(`Refusing unverifiable pm list-all output: duplicate item id ${item.id}.`); + } + const title = item.title; + const status = item.status; + const body = item.body; + const description = item.description; + if (typeof title !== "string") { + throw new CommandError(`Refusing unverifiable pm list-all output: item ${item.id} title must be a string.`); + } + if (typeof status !== "string") { + throw new CommandError(`Refusing unverifiable pm list-all output: item ${item.id} status must be a string.`); + } + if (typeof body !== "string") { + throw new CommandError(`Refusing unverifiable pm list-all output: item ${item.id} body must be a string.`); + } + if (typeof description !== "string") { + throw new CommandError( + `Refusing unverifiable pm list-all output: item ${item.id} description must be a string.`, + ); + } + if (!Array.isArray(item.tags) || item.tags.some((tag) => typeof tag !== "string")) { + throw new CommandError( + `Refusing unverifiable pm list-all output: item ${item.id} tags must be an array of strings.`, + ); + } + ids.add(item.id); + items.push({ + id: item.id, + title, + status, + body, + description, + tags: [...item.tags], + }); + } + return items; +} + +/** + * Read every pm item, including terminal items, through a proven-complete CLI response. + * + * The enlarged byte buffer prevents Node's default 1 MiB cap from killing a + * mature tracker, while {@link decodeCompletePmItems} refuses any successful + * process response that does not prove the entire item corpus and every + * consumed field were returned intact. + * + * @param pmRoot - Workspace or tracker root accepted by the pm CLI. + * @param platform - Runtime platform; injectable only to exercise the secure + * Windows launcher strategy on non-Windows CI. + * @param pmPackageRoot - Host pm CLI package root from `PM_CLI_PACKAGE_ROOT`; + * injectable only to reproduce installed-extension layout in tests. + * @returns The complete runtime-validated item corpus. + * @throws {@link CommandError} On process, buffer, JSON, or completeness failure. + * @internal Exported for installed-CLI acceptance and stripped from the public declaration. + */ +export function readPmItems( + pmRoot: string, + platform: NodeJS.Platform = process.platform, + pmPackageRoot: string | undefined = process.env.PM_CLI_PACKAGE_ROOT, +): PmItem[] { const maxBuffer = pmJsonMaxBuffer(); - // `list-all` (NOT `list`) so CLOSED items are included: `pm list` returns only - // active items, which would make the idempotency index miss every closed - // issue and re-create it as a DUPLICATE on re-import. `--full --include-body` - // so tags and body survive the read instead of the brief projection. + const listArgs = completePmListArgs(pmRoot); + let command = "pm"; + let args = listArgs; + if (platform === "win32") { + if (typeof pmPackageRoot !== "string" || pmPackageRoot.trim().length === 0) { + throw new CommandError( + "The pm host did not publish PM_CLI_PACKAGE_ROOT for a secure Windows CLI relaunch.", + ); + } + const hostRoot = path.resolve(pmPackageRoot.trim()); + const packageJsonPath = path.join(hostRoot, "package.json"); + let packageMetadata: unknown; + try { + packageMetadata = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")) as unknown; + } catch { + throw new CommandError("Could not read the installed pm CLI package metadata."); + } + const bin = isJsonRecord(packageMetadata) && isJsonRecord(packageMetadata.bin) + ? packageMetadata.bin.pm + : undefined; + if (typeof bin !== "string" || bin.trim().length === 0) { + throw new CommandError("The installed pm CLI package does not declare its pm executable."); + } + const cliEntry = path.resolve(hostRoot, bin); + const relativeEntry = path.relative(hostRoot, cliEntry); + if (relativeEntry.startsWith("..") || path.isAbsolute(relativeEntry)) { + throw new CommandError("The installed pm CLI package declares an executable outside its package root."); + } + command = process.execPath; + args = [cliEntry, ...listArgs]; + } const result = spawnSync( - "pm", - ["--path", pmRoot, "--json", "list-all", "--full", "--include-body", "--limit", "10000"], + command, + args, { encoding: "utf-8", maxBuffer }, ); // A buffer overrun kills the child with status null and no stderr, so name the @@ -877,13 +1140,13 @@ function readPmItems(pmRoot: string): PmItem[] { if (result.status !== 0) { throw new CommandError(result.stderr || "pm list-all failed"); } + let parsed: unknown; try { - const parsed = JSON.parse(result.stdout); - const items = Array.isArray(parsed) ? parsed : parsed.items ?? parsed.results ?? []; - return items as PmItem[]; + parsed = JSON.parse(result.stdout) as unknown; } catch { throw new CommandError("Could not parse `pm list-all --json` output."); } + return decodeCompletePmItems(parsed); } // Index existing pm items by their GitHub provenance tag for O(1) idempotent @@ -925,6 +1188,16 @@ export function resolveSearchCorpus(documents: unknown, pmRootValue: unknown): P .filter((item): item is PmItem => item !== undefined); } +/** + * Index complete pm items by their normalized GitHub issue provenance. + * + * Items without a stable local id or a valid `gh:owner/repo#N` tag are skipped. + * When legacy data contains duplicate provenance, the last corpus row wins; + * the complete reader prevents duplicate local item ids separately. + * + * @param items - Complete runtime-validated pm item corpus. + * @returns Map keyed by lowercase `owner/repo#N` provenance. + */ export function indexByProvenance(items: PmItem[]): Map { const index = new Map(); for (const item of items) { diff --git a/package-lock.json b/package-lock.json index b551b52..23af4fa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "MIT", "devDependencies": { "@types/node": "^26.1.1", - "@unbrained/pm-cli": "2026.8.15", + "@unbrained/pm-cli": "2026.8.17", "pm-changelog": "^2026.8.6", "pm-ops": "^2026.8.8", "typescript": "^7.0.2" @@ -732,9 +732,9 @@ } }, "node_modules/@unbrained/pm-cli": { - "version": "2026.8.15", - "resolved": "https://registry.npmjs.org/@unbrained/pm-cli/-/pm-cli-2026.8.15.tgz", - "integrity": "sha512-tQxSeOVLOjmbcwQp7GZ55NvtbMw6KJyVmPMpfCwSTnQiEq9GofGFwKqP8dzgoN5etrvyKA13XZZypM0Kja4x1A==", + "version": "2026.8.17", + "resolved": "https://registry.npmjs.org/@unbrained/pm-cli/-/pm-cli-2026.8.17.tgz", + "integrity": "sha512-G0eh1wVNyhJvIKXAx3epbuDamHBfl7AaU6zNbUuIHEpWU1yia7gfx6d0TSwLuhjgTb2lTChCWApp+et1E53r5Q==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index adf33b8..e6bbbc4 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ }, "devDependencies": { "@types/node": "^26.1.1", - "@unbrained/pm-cli": "2026.8.15", + "@unbrained/pm-cli": "2026.8.17", "pm-changelog": "^2026.8.6", "pm-ops": "^2026.8.8", "typescript": "^7.0.2" diff --git a/test/complete-corpus.test.ts b/test/complete-corpus.test.ts new file mode 100644 index 0000000..d8d44b5 --- /dev/null +++ b/test/complete-corpus.test.ts @@ -0,0 +1,258 @@ +import assert from "node:assert/strict"; +import { execFileSync } from "node:child_process"; +import { mkdtempSync, rmSync } from "node:fs"; +import { createRequire } from "node:module"; +import { tmpdir } from "node:os"; +import { dirname, join } from "node:path"; +import test from "node:test"; + +import { + completePmListArgs, + decodeCompletePmItems, + readPmItems, +} from "../index.ts"; + +/** Current complete `pm list-all --json` envelope, with caller overrides. */ +function completeEnvelope(overrides: Record = {}): Record { + return { + items: [ + { + id: "fixture-1", + title: "Fixture", + status: "open", + body: "Body", + description: "Description", + tags: ["gh:acme/widgets#1", "bug"], + }, + ], + count: 1, + total: 1, + has_more: false, + truncated: false, + next_cursor: null, + completeness: { + status: "complete", + unreadable_item_count: 0, + unreadable_directory_count: 0, + }, + projection: { mode: "full", fields: null }, + omission_receipt: { + has_omissions: false, + omitted_field_group_count: 0, + omitted_field_groups: [], + }, + read_output: { + contract_version: 1, + command: "list", + requested_dimensions: ["include", "amount", "cost"], + within_budget: true, + strings_compacted: false, + rows_compacted: false, + result_omitted: false, + }, + ...overrides, + }; +} + +/** Clone an envelope and remove one top-level truthfulness field. */ +function withoutField(field: string): Record { + const envelope = completeEnvelope(); + delete envelope[field]; + return envelope; +} + +/** Execute the installed pm CLI or fail with its captured diagnostic. */ +function runPm(args: string[], cwd?: string): string { + return execFileSync("pm", args, { + cwd, + encoding: "utf8", + env: { ...process.env, PM_AUTHOR: "pm-github-acceptance" }, + shell: process.platform === "win32", + }); +} + +test("whole-corpus argv requests strict full unbounded output without an arbitrary row ceiling", () => { + const args = completePmListArgs("/workspace/.agents/pm"); + assert.deepEqual(args, [ + "--path", + "/workspace/.agents/pm", + "list-all", + "--json", + "--include-body", + "--output-include", + "full", + "--strict-read", + "--output-limit", + "unbounded", + "--output-budget", + "unbounded", + ]); + assert.ok(!args.includes("--limit"), "a list-all row ceiling makes the corpus incomplete by construction"); +}); + +test("Windows strategy keeps shell metacharacters inside the pm workspace argument", () => { + const workspace = mkdtempSync(join(tmpdir(), "pm-github-&()-")); + try { + runPm(["init", "-y", "--force", "--workspace", workspace]); + runPm([ + "--path", + workspace, + "create", + "Task", + "Metacharacter path", + "--description", + "Argument-vector acceptance", + ]); + + assert.deepEqual( + readPmItems( + workspace, + "win32", + dirname(createRequire(import.meta.url).resolve("@unbrained/pm-cli/package.json")), + ).map((item) => item.title), + ["Metacharacter path"], + ); + } finally { + rmSync(workspace, { recursive: true, force: true }); + } +}); + +test("decoder preserves every pm field consumed by GitHub import, export, sync, project, and search paths", () => { + assert.deepEqual(decodeCompletePmItems(completeEnvelope()), [ + { + id: "fixture-1", + title: "Fixture", + status: "open", + body: "Body", + description: "Description", + tags: ["gh:acme/widgets#1", "bug"], + }, + ]); + assert.deepEqual( + decodeCompletePmItems(completeEnvelope({ items: [], count: 0, total: 0 })), + [], + ); +}); + +test("decoder refuses every independent incomplete, omitted, paginated, compacted, or contradictory receipt", () => { + const completeReadOutput = completeEnvelope().read_output as Record; + const cases: Array<[string, unknown, RegExp]> = [ + ["bare array", [], /top-level object/], + ["missing items", withoutField("items"), /items must be an array/], + ["truncated", completeEnvelope({ truncated: true }), /truncated must be false/], + ["has more", completeEnvelope({ has_more: true }), /has_more must be false/], + ["cursor", completeEnvelope({ next_cursor: "next" }), /next_cursor must be null/], + ["missing completeness", withoutField("completeness"), /completeness.status must be "complete"/], + ["null completeness", completeEnvelope({ completeness: null }), /completeness.status must be "complete"/], + ["partial completeness", completeEnvelope({ completeness: { status: "partial", unreadable_item_count: 1, unreadable_directory_count: 0 } }), /completeness.status must be "complete"/], + ["unreadable item", completeEnvelope({ completeness: { status: "complete", unreadable_item_count: 1, unreadable_directory_count: 0 } }), /unreadable_item_count must be 0/], + ["unreadable directory", completeEnvelope({ completeness: { status: "complete", unreadable_item_count: 0, unreadable_directory_count: 1 } }), /unreadable_directory_count must be 0/], + ["missing omission receipt", withoutField("omission_receipt"), /omission_receipt.has_omissions must be false/], + ["array omission receipt", completeEnvelope({ omission_receipt: [] }), /omission_receipt.has_omissions must be false/], + ["omitted fields", completeEnvelope({ omission_receipt: { has_omissions: true, omitted_field_group_count: 1, omitted_field_groups: ["body"] } }), /has_omissions must be false/], + ["contradictory omission count", completeEnvelope({ omission_receipt: { has_omissions: false, omitted_field_group_count: 1, omitted_field_groups: [] } }), /omitted_field_group_count must be 0/], + ["contradictory omission groups", completeEnvelope({ omission_receipt: { has_omissions: false, omitted_field_group_count: 0, omitted_field_groups: ["body"] } }), /omitted_field_groups must be empty/], + ["non-array omission groups", completeEnvelope({ omission_receipt: { has_omissions: false, omitted_field_group_count: 0, omitted_field_groups: "body" } }), /omitted_field_groups must be empty/], + ["missing projection", withoutField("projection"), /projection.mode must be "full"/], + ["brief projection", completeEnvelope({ projection: { mode: "brief" } }), /projection.mode must be "full"/], + ["missing read receipt", withoutField("read_output"), /read_output.contract_version must be 1/], + ["future contract version", completeEnvelope({ read_output: { ...completeReadOutput, contract_version: 2 } }), /read_output.contract_version must be 1/], + ["wrong command", completeEnvelope({ read_output: { ...completeReadOutput, command: "context" } }), /read_output.command must be "list"/], + ["over budget", completeEnvelope({ read_output: { ...completeReadOutput, within_budget: false } }), /within_budget must be true/], + ["strings compacted", completeEnvelope({ read_output: { ...completeReadOutput, strings_compacted: true } }), /strings_compacted must be false/], + ["rows compacted", completeEnvelope({ read_output: { ...completeReadOutput, rows_compacted: true } }), /rows_compacted must be false/], + ["result omitted", completeEnvelope({ read_output: { ...completeReadOutput, result_omitted: true } }), /result_omitted must be false/], + ["missing include proof", completeEnvelope({ read_output: { ...completeReadOutput, requested_dimensions: ["amount", "cost"] } }), /requested_dimensions must include include, amount, and cost/], + ["missing amount proof", completeEnvelope({ read_output: { ...completeReadOutput, requested_dimensions: ["include", "cost"] } }), /requested_dimensions must include include, amount, and cost/], + ["missing cost proof", completeEnvelope({ read_output: { ...completeReadOutput, requested_dimensions: ["include", "amount"] } }), /requested_dimensions must include include, amount, and cost/], + ["budget truncation disclosure", completeEnvelope({ output_budget_truncation: { reason: "output_budget_reached" } }), /budget truncation or omission disclosure/], + ["budget omission disclosure", completeEnvelope({ output_budget_exceeded: { omitted_result: true } }), /budget truncation or omission disclosure/], + ["non-integer count", completeEnvelope({ count: "1" }), /count must be a non-negative safe integer/], + ["negative total", completeEnvelope({ total: -1 }), /total must be a non-negative safe integer/], + ["row count mismatch", completeEnvelope({ count: 2, total: 2 }), /items.length 1 must equal count 2/], + ["total mismatch", completeEnvelope({ total: 2 }), /count 1 must equal total 2/], + ]; + + for (const [name, envelope, expected] of cases) { + assert.throws(() => decodeCompletePmItems(envelope), expected, name); + } +}); + +test("decoder refuses malformed and duplicate rows instead of trusting a TypeScript cast", () => { + const cases: Array<[string, unknown, RegExp]> = [ + ["non-object", null, /item 0 must be an object/], + ["missing id", { title: "Fixture", status: "open", body: "", description: "", tags: [] }, /non-empty id/], + ["empty id", { id: " ", title: "Fixture", status: "open", body: "", description: "", tags: [] }, /non-empty id/], + ["non-string title", { id: "fixture-1", title: 1, status: "open", body: "", description: "", tags: [] }, /title must be a string/], + ["non-string status", { id: "fixture-1", title: "Fixture", status: 1, body: "", description: "", tags: [] }, /status must be a string/], + ["non-string body", { id: "fixture-1", title: "Fixture", status: "open", body: null, description: "", tags: [] }, /body must be a string/], + ["non-string description", { id: "fixture-1", title: "Fixture", status: "open", body: "", description: null, tags: [] }, /description must be a string/], + ["tags not an array", { id: "fixture-1", title: "Fixture", status: "open", body: "", description: "", tags: "bug" }, /tags must be an array of strings/], + ["non-string tag", { id: "fixture-1", title: "Fixture", status: "open", body: "", description: "", tags: [1] }, /tags must be an array of strings/], + ]; + + for (const [name, row, expected] of cases) { + assert.throws(() => decodeCompletePmItems(completeEnvelope({ items: [row] })), expected, name); + } + + assert.throws( + () => decodeCompletePmItems(completeEnvelope({ + items: [ + { id: "same", title: "First", status: "open", body: "", description: "", tags: [] }, + { id: "same", title: "Second", status: "closed", body: "", description: "", tags: [] }, + ], + count: 2, + total: 2, + })), + /duplicate item id same/, + ); +}); + +test("real installed CLI returns a complete open-and-closed corpus from a fresh tracker", () => { + const workspace = mkdtempSync(join(tmpdir(), "pm-github-complete-corpus-")); + try { + runPm(["init", "-y", "--force", "--workspace", workspace]); + const created = JSON.parse(runPm([ + "--path", + workspace, + "--json", + "create", + "Issue", + "Imported issue", + "--description", + "Real imported issue", + "--body", + "Acceptance body", + "--tags", + "gh:acme/widgets#7,bug", + ])) as { id: string }; + runPm(["--path", workspace, "close", created.id, "--reason", "acceptance complete"]); + runPm([ + "--path", + workspace, + "create", + "Task", + "Still open", + "--description", + "Open acceptance task", + ]); + + const items = readPmItems(workspace); + assert.equal(items.length, 2); + assert.deepEqual( + items + .map((item) => ({ title: item.title, status: item.status })) + .sort((left, right) => (left.title ?? "").localeCompare(right.title ?? "")), + [ + { title: "Imported issue", status: "closed" }, + { title: "Still open", status: "open" }, + ], + ); + const imported = items.find((item) => item.id === created.id); + assert.ok(imported); + assert.equal(imported.body, "Acceptance body"); + assert.deepEqual(imported.tags, ["bug", "gh:acme/widgets#7"]); + } finally { + rmSync(workspace, { recursive: true, force: true }); + } +});