From d67814d3fc444539063eba72400cf36a589f63cc Mon Sep 17 00:00:00 2001 From: SteveBot <1153461+unbraind@users.noreply.github.com> Date: Sat, 15 Aug 2026 12:15:21 +0200 Subject: [PATCH 1/2] Bind the preflight scope to pm-github's declared command set MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #36 scoped registerPreflight to a fixed six-entry commands array, but nothing binds that array to isMutatingGithubCommand or to the commands the package declares: if a command is added or reclassified as mutating on one side only, the override silently stops running for it (the runtime matches by exact normalized path) and it executes with no early credential warning. Extend the scoping smoke test into a drift guard: every scoped path must be one isMutatingGithubCommand treats as mutating — the apply-gated github export and github project sync are checked under their apply configuration rather than skipped — and every declared-but-omitted path (github validate, github project list, github project fields) must be read-only, proving the omission is justified rather than an accidental gap. Manifest-vs-scope audit: every scoped path is a declared command; the three omitted commands are read-only diagnostics that were never gated under the old global registration either. No gate lost; the legacy gh-issues import alias was already in scope. Verified: npm test 251/251; coverage 92.04 lines / 81.34 branches / 91.49 functions (thresholds 88/79/89); release:check exit 0; npx pm health --strict-exit ok: true. --- .agents/pm/history/pm-github-4ga9.jsonl | 8 ++++++ .agents/pm/issues/pm-github-4ga9.toon | 20 ++++++++++++++ .gitattributes | 2 -- CHANGELOG.md | 6 +++++ test/smoke.test.ts | 36 +++++++++++++++++++++++++ 5 files changed, 70 insertions(+), 2 deletions(-) create mode 100644 .agents/pm/history/pm-github-4ga9.jsonl create mode 100644 .agents/pm/issues/pm-github-4ga9.toon diff --git a/.agents/pm/history/pm-github-4ga9.jsonl b/.agents/pm/history/pm-github-4ga9.jsonl new file mode 100644 index 0000000..7dd994e --- /dev/null +++ b/.agents/pm/history/pm-github-4ga9.jsonl @@ -0,0 +1,8 @@ +{"ts":"2026-08-15T09:59:58.137Z","author":"pi-agent","author_source":"asserted","agent_harness":"pi","agent_model":"glm-5.3","agent_model_source":"environment","agent_instance":"160291269a4da19f0e3cdf5b","agent_provenance":{"model":{"value":"glm-5.3","source":"environment"},"effort":null,"role":null,"topic":null},"op":"create","patch":[{"op":"add","path":"/metadata/id","value":"pm-github-4ga9"},{"op":"add","path":"/metadata/title","value":"A github command can silently lose its preflight credential gate when the override scope drifts from the mutating command set"},{"op":"add","path":"/metadata/description","value":"registerPreflight is scoped to a fixed six-entry commands array (PR #36). That array and isMutatingGithubCommand are maintained by hand with nothing binding them: the legacy gh-issues alias already showed how easily a hand-maintained command list drifts from the real mutating set. If a command is added or reclassified as mutating on one side only, the override stops running for it (the runtime matches by exact normalized path) and the command executes with no early credential warning — silently, because the runtime swallows preflight throws and the authoritative handler gate only fires once the command actually runs and fails. A test must bind the scope to the classifier: every scoped path has to be one isMutatingGithubCommand treats as mutating (the apply-gated github export and github project sync under their apply configuration), and every declared-but-omitted path has to be read-only, so the two lists cannot drift apart again."},{"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":[]},{"op":"add","path":"/metadata/created_at","value":"2026-08-15T09:59:58.137Z"},{"op":"add","path":"/metadata/updated_at","value":"2026-08-15T09:59:58.137Z"},{"op":"add","path":"/metadata/author","value":"pi-agent"}],"before_hash":"3cc22dff72be7b14824654a7a64ea62b04799939b2fee54c1b5f52ca60bf6df0","after_hash":"7f5185e842ae8ae19e58b3fed488319676e1aa0aceafe3f5c8ad83bd3e0d85dc","message":""} +{"ts":"2026-08-15T10:01:20.613Z","author":"pi-agent","author_source":"asserted","agent_harness":"pi","agent_model":"glm-5.3","agent_model_source":"environment","agent_instance":"160291269a4da19f0e3cdf5b","agent_provenance":{"model":{"value":"glm-5.3","source":"environment"},"effort":null,"role":null,"topic":null},"op":"update","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-08-15T10:01:20.613Z"},{"op":"replace","path":"/metadata/status","value":"in_progress"}],"before_hash":"7f5185e842ae8ae19e58b3fed488319676e1aa0aceafe3f5c8ad83bd3e0d85dc","after_hash":"f76e0a8eba61f45d0d31d0b3eea94c172deec5ae3aef5ba8d36c03ba3ba31b7d"} +{"ts":"2026-08-15T10:01:21.491Z","author":"pi-agent","author_source":"asserted","agent_harness":"pi","agent_model":"glm-5.3","agent_model_source":"environment","agent_instance":"160291269a4da19f0e3cdf5b","agent_provenance":{"model":{"value":"glm-5.3","source":"environment"},"effort":null,"role":null,"topic":null},"op":"comment_add","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-08-15T10:01:21.491Z"},{"op":"add","path":"/metadata/comments","value":[{"created_at":"2026-08-15T10:01:21.491Z","author":"pi-agent","text":"Manifest-vs-scope audit (the check CodeRabbit asked for on pm-linear #67). Commands pm-github declares: github sync, gh-issues import (legacy command alias), github validate, github project list, github project fields, github project import, github project sync (registerCommand), plus github import (registerImporter) and github export (registerExporter). Preflight scope after PR #36: github sync, github export, github import, gh-issues import, github project import, github project sync. Diff: every scoped path is a declared command; the three omitted commands (github validate, github project list, github project fields) are read-only diagnostics and were never gated — isMutatingGithubCommand returned false for them under the old global registration too, so no gate was lost. Notably the legacy alias gh-issues import was already in scope (the gap CodeRabbit caught in pm-linear/pm-jira does not exist here). Remaining defect: nothing binds the two lists, which this item fixes with a drift test."}]}],"before_hash":"f76e0a8eba61f45d0d31d0b3eea94c172deec5ae3aef5ba8d36c03ba3ba31b7d","after_hash":"9e96ef853cc07c8b776d96316fa2d26ac778b41b77f9936a64463fc1b23ad406"} +{"ts":"2026-08-15T10:02:02.393Z","author":"pi-agent","author_source":"asserted","agent_harness":"pi","agent_model":"glm-5.3","agent_model_source":"environment","agent_instance":"160291269a4da19f0e3cdf5b","agent_provenance":{"model":{"value":"glm-5.3","source":"environment"},"effort":null,"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/1","value":{"created_at":"2026-08-15T10:02:02.393Z","author":"pi-agent","text":"Manifest-vs-scope audit (the check CodeRabbit asked for on pm-linear #67). Commands pm-github declares: github sync, gh-issues import (legacy command alias), github validate, github project list, github project fields, github project import, github project sync (registerCommand), plus github import (registerImporter) and github export (registerExporter). Preflight scope after PR #36: github sync, github export, github import, gh-issues import, github project import, github project sync. Diff: every scoped path is a declared command; the three omitted commands (github validate, github project list, github project fields) are read-only diagnostics and were never gated — isMutatingGithubCommand returned false for them under the old global registration too, so no gate was lost. Notably the legacy alias gh-issues import was already in scope (the gap CodeRabbit caught in pm-linear/pm-jira does not exist here). Remaining defect: nothing binds the two lists, which this item fixes with a drift test."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-08-15T10:02:02.393Z"}],"before_hash":"9e96ef853cc07c8b776d96316fa2d26ac778b41b77f9936a64463fc1b23ad406","after_hash":"60bbfde97381940e7963a985a88516c21892836b3361e5b881e4aa0e94a39b1b"} +{"ts":"2026-08-15T10:02:14.940Z","author":"pi-agent","author_source":"asserted","agent_harness":"pi","agent_model":"glm-5.3","agent_model_source":"environment","agent_instance":"160291269a4da19f0e3cdf5b","agent_provenance":{"model":{"value":"glm-5.3","source":"environment"},"effort":null,"role":null,"topic":null},"op":"comment_delete","patch":[{"op":"remove","path":"/metadata/comments/1"},{"op":"replace","path":"/metadata/updated_at","value":"2026-08-15T10:02:14.940Z"}],"before_hash":"60bbfde97381940e7963a985a88516c21892836b3361e5b881e4aa0e94a39b1b","after_hash":"a7ee8e6f4afaa7197e4b4502fc3de44eefdbe5fed286b97efec001f8d51b2766"} +{"ts":"2026-08-15T10:02:15.603Z","author":"pi-agent","author_source":"asserted","agent_harness":"pi","agent_model":"glm-5.3","agent_model_source":"environment","agent_instance":"160291269a4da19f0e3cdf5b","agent_provenance":{"model":{"value":"glm-5.3","source":"environment"},"effort":null,"role":null,"topic":null},"op":"files_add","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-08-15T10:02:15.603Z"},{"op":"add","path":"/metadata/files","value":[{"path":"test/smoke.test.ts","scope":"project"}]}],"before_hash":"a7ee8e6f4afaa7197e4b4502fc3de44eefdbe5fed286b97efec001f8d51b2766","after_hash":"1670cf19621454a56749e0459440bffec71bc595ac9947fcd384493fd62cef8c"} +{"ts":"2026-08-15T10:03:11.022Z","author":"pi-agent","author_source":"asserted","agent_harness":"pi","agent_model":"glm-5.3","agent_model_source":"environment","agent_instance":"160291269a4da19f0e3cdf5b","agent_provenance":{"model":{"value":"glm-5.3","source":"environment"},"effort":null,"role":null,"topic":null},"op":"tests_add","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-08-15T10:03:11.022Z"},{"op":"add","path":"/metadata/tests","value":[{"command":"npm test","path":"test/smoke.test.ts","scope":"project","note":"Asserts the preflight override stays scoped to exactly pm-github's owned mutating command paths and binds the scope to isMutatingGithubCommand: every scoped path must be classified mutating (github export and github project sync under their apply configuration) and every declared-but-omitted path must be read-only. Test title: preflight override is scoped to pm-github's owned command paths"}]}],"before_hash":"1670cf19621454a56749e0459440bffec71bc595ac9947fcd384493fd62cef8c","after_hash":"55d2b03b36fa212c3ad633676ff87047e8b3df14b4c4983cc912c5ca9803bc2d"} +{"ts":"2026-08-15T10:15:01.046Z","author":"pi-agent","author_source":"asserted","agent_harness":"pi","agent_model":"glm-5.3","agent_model_source":"environment","agent_instance":"160291269a4da19f0e3cdf5b","agent_provenance":{"model":{"value":"glm-5.3","source":"environment"},"effort":null,"role":null,"topic":null},"op":"close","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-08-15T10:15:01.046Z"},{"op":"replace","path":"/metadata/status","value":"closed"},{"op":"add","path":"/metadata/closed_at","value":"2026-08-15T10:15:00.998Z"},{"op":"add","path":"/metadata/completed_at","value":"2026-08-15T10:15:00.998Z"},{"op":"add","path":"/metadata/close_reason","value":"Scope bound to the classifier in test/smoke.test.ts: every scoped preflight path must be one isMutatingGithubCommand treats as mutating (github export and github project sync under their apply configuration), and every declared-but-omitted path (github validate, github project list, github project fields) must be read-only, so the scope list and pm-github's declared command set cannot drift apart again. Manifest-vs-scope audit found no lost gate; gh-issues import was already in scope. Verified: npm test 251/251 pass, coverage 92.04/81.34/91.49 (thresholds 88/79/89), release:check exit 0, npx pm health --strict-exit ok:true."}],"before_hash":"55d2b03b36fa212c3ad633676ff87047e8b3df14b4c4983cc912c5ca9803bc2d","after_hash":"c0838d42424bf04a19c658a6de6a5303f0b058964f7c458a18621f14e9e0dfa6"} diff --git a/.agents/pm/issues/pm-github-4ga9.toon b/.agents/pm/issues/pm-github-4ga9.toon new file mode 100644 index 0000000..456d1a6 --- /dev/null +++ b/.agents/pm/issues/pm-github-4ga9.toon @@ -0,0 +1,20 @@ +id: pm-github-4ga9 +title: A github command can silently lose its preflight credential gate when the override scope drifts from the mutating command set +description: "registerPreflight is scoped to a fixed six-entry commands array (PR #36). That array and isMutatingGithubCommand are maintained by hand with nothing binding them: the legacy gh-issues alias already showed how easily a hand-maintained command list drifts from the real mutating set. If a command is added or reclassified as mutating on one side only, the override stops running for it (the runtime matches by exact normalized path) and the command executes with no early credential warning — silently, because the runtime swallows preflight throws and the authoritative handler gate only fires once the command actually runs and fails. A test must bind the scope to the classifier: every scoped path has to be one isMutatingGithubCommand treats as mutating (the apply-gated github export and github project sync under their apply configuration), and every declared-but-omitted path has to be read-only, so the two lists cannot drift apart again." +type: Issue +status: closed +priority: 2 +tags: [] +created_at: "2026-08-15T09:59:58.137Z" +updated_at: "2026-08-15T10:15:01.046Z" +closed_at: "2026-08-15T10:15:00.998Z" +completed_at: "2026-08-15T10:15:00.998Z" +author: pi-agent +comments[1]{created_at,author,text}: + "2026-08-15T10:01:21.491Z",pi-agent,"Manifest-vs-scope audit (the check CodeRabbit asked for on pm-linear #67). Commands pm-github declares: github sync, gh-issues import (legacy command alias), github validate, github project list, github project fields, github project import, github project sync (registerCommand), plus github import (registerImporter) and github export (registerExporter). Preflight scope after PR #36: github sync, github export, github import, gh-issues import, github project import, github project sync. Diff: every scoped path is a declared command; the three omitted commands (github validate, github project list, github project fields) are read-only diagnostics and were never gated — isMutatingGithubCommand returned false for them under the old global registration too, so no gate was lost. Notably the legacy alias gh-issues import was already in scope (the gap CodeRabbit caught in pm-linear/pm-jira does not exist here). Remaining defect: nothing binds the two lists, which this item fixes with a drift test." +files[1]{path,scope}: + test/smoke.test.ts,project +tests[1]{command,path,scope,note}: + npm test,test/smoke.test.ts,project,"Asserts the preflight override stays scoped to exactly pm-github's owned mutating command paths and binds the scope to isMutatingGithubCommand: every scoped path must be classified mutating (github export and github project sync under their apply configuration) and every declared-but-omitted path must be read-only. Test title: preflight override is scoped to pm-github's owned command paths" +close_reason: "Scope bound to the classifier in test/smoke.test.ts: every scoped preflight path must be one isMutatingGithubCommand treats as mutating (github export and github project sync under their apply configuration), and every declared-but-omitted path (github validate, github project list, github project fields) must be read-only, so the scope list and pm-github's declared command set cannot drift apart again. Manifest-vs-scope audit found no lost gate; gh-issues import was already in scope. Verified: npm test 251/251 pass, coverage 92.04/81.34/91.49 (thresholds 88/79/89), release:check exit 0, npx pm health --strict-exit ok:true." +body: "" diff --git a/.gitattributes b/.gitattributes index 4755b62..fe696ab 100644 --- a/.gitattributes +++ b/.gitattributes @@ -27,6 +27,4 @@ ".agents/pm/history/*.jsonl" merge=pm-history ".agents/pm/settings.json" merge=pm-json ".agents/pm/**/*.json" merge=pm-json -".agents/pm/extensions/**" -merge -".agents/pm/extensions/.managed-extensions.json" merge=pm-json # pm-cli:merge-drivers:end diff --git a/CHANGELOG.md b/CHANGELOG.md index d8a13a7..03b86ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### 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 ### Fixed diff --git a/test/smoke.test.ts b/test/smoke.test.ts index 6b9d1d5..5e495bf 100644 --- a/test/smoke.test.ts +++ b/test/smoke.test.ts @@ -23,6 +23,7 @@ import extension, { exportWillApply, formatRateLimit, isDraftPr, + isMutatingGithubCommand, listOwnerProjectsV2Nodes, indexByProvenance, searchDocumentToItem, @@ -170,6 +171,41 @@ test("preflight override is scoped to pm-github's owned command paths", async () ], "preflight override must be scoped to exactly pm-github's owned mutating command paths", ); + // Bind the scope to the classifier. Narrowing the override to a command list + // means an entry missing on either side is a command that silently loses + // its credential gate, so every scoped path must be one isMutatingGithubCommand + // treats as mutating. The two apply-gated paths (`github export`, + // `github project sync`) only mutate with an explicit apply, so they are + // checked under that configuration rather than skipped. + for (const command of override.commands ?? []) { + const mutatingOptions = + command === "github export" || command === "github project sync" ? { apply: true } : {}; + assert.strictEqual( + isMutatingGithubCommand(command, mutatingOptions), + true, + `${command} is in the preflight scope but the classifier does not treat it as mutating`, + ); + } + // Conversely, every command path pm-github declares that the scope omits + // must be read-only — a justified exclusion, not an accidental gap that + // silently dropped a gate. (Commands are declared via registerCommand plus + // the `github` importer/exporter aliases.) + const DECLARED_READ_ONLY_COMMANDS = [ + "github validate", + "github project list", + "github project fields", + ] as const; + for (const command of DECLARED_READ_ONLY_COMMANDS) { + assert.ok( + !(override.commands ?? []).includes(command), + `${command} is read-only and must not claim a preflight scope entry`, + ); + assert.strictEqual( + isMutatingGithubCommand(command, { apply: true, push: true }), + false, + `${command} is outside the preflight scope but the classifier treats it as mutating`, + ); + } }); test("parseNextLink extracts the rel=\"next\" page URL", () => { From d85872ab924fbffc45fbca3c0d3a061c62ea16ee Mon Sep 17 00:00:00 2001 From: SteveBot <1153461+unbraind@users.noreply.github.com> Date: Sat, 15 Aug 2026 13:38:07 +0200 Subject: [PATCH 2/2] Derive the preflight drift guard from real activation instead of a hand-maintained list Greptile (P2) and CodeRabbit (Major) independently raised the same objection to the guard added in the previous commit, and both were right. The test compared the override scope against DECLARED_READ_ONLY_COMMANDS, a second hand-maintained literal, and only checked that each CURRENT scope entry was mutating. So declaring a new mutating command while omitting it from both the scope and that literal left every assertion green - the guard could not detect the drift it was written for, which is worse than no guard because it reads as coverage. This matters more than a typical test-quality note. The whole point of the change is to NARROW a preflight scope, and narrowing is precisely the operation that can drop a command's credential gate silently: the runtime matches by exact normalized path, swallows preflight throws, and the authoritative handler gate only fires once the command has already run and failed. The test now derives the declared set from the real activation registrations - registerCommand, registerImporter and registerExporter - asserts the dispatch handler paths equal that derived set, partitions it with the same isMutatingGithubCommand predicate production uses rather than a second copy of the knowledge, and asserts the mutating and read-only classes reconstruct the declared set EXACTLY. That last assertion is the one that closes the finding: without it the hand-maintained list has merely moved. Verified non-vacuous rather than assumed. A scratch mutating command 'github scratch drift' was declared through registerCommand and added to isMutatingGithubCommand without touching the override scope; the suite failed 1 of 251 with 'preflight override scope must equal the mutating class of the declared command set exactly' and the diff named the command. The scratch command has been removed; 251 of 251 green. --- .agents/pm/history/pm-github-4ga9.jsonl | 3 + .agents/pm/issues/pm-github-4ga9.toon | 12 +- test/smoke.test.ts | 147 +++++++++++++++++------- 3 files changed, 118 insertions(+), 44 deletions(-) diff --git a/.agents/pm/history/pm-github-4ga9.jsonl b/.agents/pm/history/pm-github-4ga9.jsonl index 7dd994e..3baf4e3 100644 --- a/.agents/pm/history/pm-github-4ga9.jsonl +++ b/.agents/pm/history/pm-github-4ga9.jsonl @@ -6,3 +6,6 @@ {"ts":"2026-08-15T10:02:15.603Z","author":"pi-agent","author_source":"asserted","agent_harness":"pi","agent_model":"glm-5.3","agent_model_source":"environment","agent_instance":"160291269a4da19f0e3cdf5b","agent_provenance":{"model":{"value":"glm-5.3","source":"environment"},"effort":null,"role":null,"topic":null},"op":"files_add","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-08-15T10:02:15.603Z"},{"op":"add","path":"/metadata/files","value":[{"path":"test/smoke.test.ts","scope":"project"}]}],"before_hash":"a7ee8e6f4afaa7197e4b4502fc3de44eefdbe5fed286b97efec001f8d51b2766","after_hash":"1670cf19621454a56749e0459440bffec71bc595ac9947fcd384493fd62cef8c"} {"ts":"2026-08-15T10:03:11.022Z","author":"pi-agent","author_source":"asserted","agent_harness":"pi","agent_model":"glm-5.3","agent_model_source":"environment","agent_instance":"160291269a4da19f0e3cdf5b","agent_provenance":{"model":{"value":"glm-5.3","source":"environment"},"effort":null,"role":null,"topic":null},"op":"tests_add","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-08-15T10:03:11.022Z"},{"op":"add","path":"/metadata/tests","value":[{"command":"npm test","path":"test/smoke.test.ts","scope":"project","note":"Asserts the preflight override stays scoped to exactly pm-github's owned mutating command paths and binds the scope to isMutatingGithubCommand: every scoped path must be classified mutating (github export and github project sync under their apply configuration) and every declared-but-omitted path must be read-only. Test title: preflight override is scoped to pm-github's owned command paths"}]}],"before_hash":"1670cf19621454a56749e0459440bffec71bc595ac9947fcd384493fd62cef8c","after_hash":"55d2b03b36fa212c3ad633676ff87047e8b3df14b4c4983cc912c5ca9803bc2d"} {"ts":"2026-08-15T10:15:01.046Z","author":"pi-agent","author_source":"asserted","agent_harness":"pi","agent_model":"glm-5.3","agent_model_source":"environment","agent_instance":"160291269a4da19f0e3cdf5b","agent_provenance":{"model":{"value":"glm-5.3","source":"environment"},"effort":null,"role":null,"topic":null},"op":"close","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-08-15T10:15:01.046Z"},{"op":"replace","path":"/metadata/status","value":"closed"},{"op":"add","path":"/metadata/closed_at","value":"2026-08-15T10:15:00.998Z"},{"op":"add","path":"/metadata/completed_at","value":"2026-08-15T10:15:00.998Z"},{"op":"add","path":"/metadata/close_reason","value":"Scope bound to the classifier in test/smoke.test.ts: every scoped preflight path must be one isMutatingGithubCommand treats as mutating (github export and github project sync under their apply configuration), and every declared-but-omitted path (github validate, github project list, github project fields) must be read-only, so the scope list and pm-github's declared command set cannot drift apart again. Manifest-vs-scope audit found no lost gate; gh-issues import was already in scope. Verified: npm test 251/251 pass, coverage 92.04/81.34/91.49 (thresholds 88/79/89), release:check exit 0, npx pm health --strict-exit ok:true."}],"before_hash":"55d2b03b36fa212c3ad633676ff87047e8b3df14b4c4983cc912c5ca9803bc2d","after_hash":"c0838d42424bf04a19c658a6de6a5303f0b058964f7c458a18621f14e9e0dfa6"} +{"ts":"2026-08-15T11:12:30.498Z","author":"pi-agent","author_source":"asserted","agent_harness":"pi","agent_model":"glm-5.3","agent_model_source":"environment","agent_instance":"faf49faa4cb656dc3626f46b","agent_provenance":{"model":{"value":"glm-5.3","source":"environment"},"effort":null,"role":{"value":"implementer","source":"argv"},"topic":null},"op":"update","patch":[{"op":"remove","path":"/metadata/close_reason"},{"op":"remove","path":"/metadata/completed_at"},{"op":"remove","path":"/metadata/closed_at"},{"op":"replace","path":"/metadata/updated_at","value":"2026-08-15T11:12:30.498Z"},{"op":"replace","path":"/metadata/status","value":"in_progress"}],"before_hash":"c0838d42424bf04a19c658a6de6a5303f0b058964f7c458a18621f14e9e0dfa6","after_hash":"718f3dfba735d7db7bf147f44de32820fc9f7db426e2fee3012efa2c1cba53e5","item_hash_version":2,"message":"Reopened: PR #42 review (Greptile 3789172153 P2, CodeRabbit 3789173850 Major) correctly flags that DECLARED_READ_ONLY_COMMANDS is another hand-maintained list — the drift guard cannot detect the drift it was written for. Reworking the test to derive the declared command set from the real activation registry."} +{"ts":"2026-08-15T11:12:55.204Z","author":"pi-agent","author_source":"asserted","agent_harness":"pi","agent_model":"glm-5.3","agent_model_source":"environment","agent_instance":"faf49faa4cb656dc3626f46b","agent_provenance":{"model":{"value":"glm-5.3","source":"environment"},"effort":null,"role":{"value":"implementer","source":"argv"},"topic":null},"op":"claim","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-08-15T11:12:55.204Z"},{"op":"add","path":"/metadata/assignee","value":"pi-agent"},{"op":"add","path":"/metadata/claim_principal","value":"pi-agent"}],"before_hash":"718f3dfba735d7db7bf147f44de32820fc9f7db426e2fee3012efa2c1cba53e5","after_hash":"d990301af05c9f559285be757d43072dcb333dee426166e8954209fbb05ca67d","item_hash_version":2} +{"ts":"2026-08-15T11:37:53.946Z","author":"pi-agent","author_source":"asserted","agent_harness":"claude-code","agent_instance":"2a535af1e474c92d3fda7e13","agent_provenance":{"model":null,"effort":{"value":"xhigh","source":"environment"},"role":{"value":"implementer","source":"argv"},"topic":null},"op":"close","patch":[{"op":"remove","path":"/metadata/assignee"},{"op":"replace","path":"/metadata/updated_at","value":"2026-08-15T11:37:53.946Z"},{"op":"replace","path":"/metadata/status","value":"closed"},{"op":"add","path":"/metadata/closed_at","value":"2026-08-15T11:37:53.931Z"},{"op":"add","path":"/metadata/completed_at","value":"2026-08-15T11:37:53.931Z"},{"op":"add","path":"/metadata/resolution","value":"The smoke test now derives the declared command set from real activation registrations (registerCommand, registerImporter, registerExporter) instead of a hand-maintained literal, asserts the dispatch handler paths equal that derived set, partitions it with the same isMutatingGithubCommand predicate production uses, and asserts the mutating and read-only classes reconstruct the declared set exactly. A declared path that escapes both classes therefore fails the test."},{"op":"add","path":"/metadata/expected_result","value":"Declaring a new mutating github command without adding it to the preflight override scope fails the test, naming that command, rather than leaving every assertion green."},{"op":"add","path":"/metadata/actual_result","value":"Verified by declaring a scratch mutating command github scratch drift through registerCommand and adding it to isMutatingGithubCommand without touching the override scope: the suite failed 1 of 251 with preflight override scope must equal the mutating class of the declared command set exactly, and the diff named github scratch drift. The scratch command was then removed and the suite is 251 of 251 green."},{"op":"add","path":"/metadata/close_reason","value":"The smoke test now derives the declared command set from real activation registrations (registerCommand, registerImporter, registerExporter) instead of a hand-maintained literal, asserts the dispatch handler paths equal that derived set, partitions it with the same isMutatingGithubCommand predicate production uses, and asserts the mutating and read-only classes reconstruct the declared set exactly. A declared path that escapes both classes therefore fails the test."}],"before_hash":"d990301af05c9f559285be757d43072dcb333dee426166e8954209fbb05ca67d","after_hash":"f24768279b7db8c10eaf74463125f0852378128aad087826b4bb35086a5a5621","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"}}}} diff --git a/.agents/pm/issues/pm-github-4ga9.toon b/.agents/pm/issues/pm-github-4ga9.toon index 456d1a6..4ff3b22 100644 --- a/.agents/pm/issues/pm-github-4ga9.toon +++ b/.agents/pm/issues/pm-github-4ga9.toon @@ -6,15 +6,19 @@ status: closed priority: 2 tags: [] created_at: "2026-08-15T09:59:58.137Z" -updated_at: "2026-08-15T10:15:01.046Z" -closed_at: "2026-08-15T10:15:00.998Z" -completed_at: "2026-08-15T10:15:00.998Z" +updated_at: "2026-08-15T11:37:53.946Z" +closed_at: "2026-08-15T11:37:53.931Z" +completed_at: "2026-08-15T11:37:53.931Z" +claim_principal: pi-agent author: pi-agent +resolution: "The smoke test now derives the declared command set from real activation registrations (registerCommand, registerImporter, registerExporter) instead of a hand-maintained literal, asserts the dispatch handler paths equal that derived set, partitions it with the same isMutatingGithubCommand predicate production uses, and asserts the mutating and read-only classes reconstruct the declared set exactly. A declared path that escapes both classes therefore fails the test." +expected_result: "Declaring a new mutating github command without adding it to the preflight override scope fails the test, naming that command, rather than leaving every assertion green." +actual_result: "Verified by declaring a scratch mutating command github scratch drift through registerCommand and adding it to isMutatingGithubCommand without touching the override scope: the suite failed 1 of 251 with preflight override scope must equal the mutating class of the declared command set exactly, and the diff named github scratch drift. The scratch command was then removed and the suite is 251 of 251 green." comments[1]{created_at,author,text}: "2026-08-15T10:01:21.491Z",pi-agent,"Manifest-vs-scope audit (the check CodeRabbit asked for on pm-linear #67). Commands pm-github declares: github sync, gh-issues import (legacy command alias), github validate, github project list, github project fields, github project import, github project sync (registerCommand), plus github import (registerImporter) and github export (registerExporter). Preflight scope after PR #36: github sync, github export, github import, gh-issues import, github project import, github project sync. Diff: every scoped path is a declared command; the three omitted commands (github validate, github project list, github project fields) are read-only diagnostics and were never gated — isMutatingGithubCommand returned false for them under the old global registration too, so no gate was lost. Notably the legacy alias gh-issues import was already in scope (the gap CodeRabbit caught in pm-linear/pm-jira does not exist here). Remaining defect: nothing binds the two lists, which this item fixes with a drift test." files[1]{path,scope}: test/smoke.test.ts,project tests[1]{command,path,scope,note}: npm test,test/smoke.test.ts,project,"Asserts the preflight override stays scoped to exactly pm-github's owned mutating command paths and binds the scope to isMutatingGithubCommand: every scoped path must be classified mutating (github export and github project sync under their apply configuration) and every declared-but-omitted path must be read-only. Test title: preflight override is scoped to pm-github's owned command paths" -close_reason: "Scope bound to the classifier in test/smoke.test.ts: every scoped preflight path must be one isMutatingGithubCommand treats as mutating (github export and github project sync under their apply configuration), and every declared-but-omitted path (github validate, github project list, github project fields) must be read-only, so the scope list and pm-github's declared command set cannot drift apart again. Manifest-vs-scope audit found no lost gate; gh-issues import was already in scope. Verified: npm test 251/251 pass, coverage 92.04/81.34/91.49 (thresholds 88/79/89), release:check exit 0, npx pm health --strict-exit ok:true." +close_reason: "The smoke test now derives the declared command set from real activation registrations (registerCommand, registerImporter, registerExporter) instead of a hand-maintained literal, asserts the dispatch handler paths equal that derived set, partitions it with the same isMutatingGithubCommand predicate production uses, and asserts the mutating and read-only classes reconstruct the declared set exactly. A declared path that escapes both classes therefore fails the test." body: "" diff --git a/test/smoke.test.ts b/test/smoke.test.ts index 5e495bf..9399670 100644 --- a/test/smoke.test.ts +++ b/test/smoke.test.ts @@ -140,7 +140,7 @@ test("extension registers at least one capability", async () => { ); }); -test("preflight override is scoped to pm-github's owned command paths", async () => { +test("preflight override scope equals the mutating class of the declared command set", async () => { // The override MUST register as a scoped object (commands + run), not a bare // function: a global (unscoped) override collides pairwise with every other // installed package's preflight override (pm health reports @@ -159,51 +159,118 @@ test("preflight override is scoped to pm-github's owned command paths", async () "function", "scoped preflight override must expose a run function", ); + + // --------------------------------------------------------------------- + // Derive the declared command set from the REAL registration above — never + // from a hand-maintained literal. Every path declared through + // registerCommand, registerImporter and registerExporter is collected: + // registerImporter/registerExporter wrap their handlers into " + // import"/" export" command paths, exactly how the runtime names + // them. This derived set is the single source of truth for the rest of the + // test, so a newly registered command shows up here whether or not anyone + // remembered to classify it (PR #42 review: Greptile 3789172153, CodeRabbit + // 3789173850 — a guard that enumerates a literal cannot detect the drift it + // was written for). + // --------------------------------------------------------------------- + const registrations = ext.activation.registrations; + const declaredPaths = [...new Set([ + ...registrations.commands.map((c) => c.command), + ...registrations.importers.map((i) => `${i.importer} import`), + ...registrations.exporters.map((e) => `${e.exporter} export`), + ])].sort(); + assert.ok(declaredPaths.length > 0, "activation should declare command paths"); + + // Cross-check the registration metadata against the dispatch registry: the + // paths that actually execute (commands.handlers — which is where importer + // and exporter handlers land too) must equal the declared set. If a future + // registration surface stopped landing in one of the two, the derived set + // would be silently incomplete and every assertion below would narrow with + // it — so the two derivations are required to agree. + const dispatchedPaths = [...new Set(ext.activation.commands.handlers.map((h) => h.command))].sort(); assert.deepEqual( - override.commands, - [ - "github sync", - "github export", - "github import", - "gh-issues import", - "github project import", - "github project sync", - ], - "preflight override must be scoped to exactly pm-github's owned mutating command paths", + dispatchedPaths, + declaredPaths, + "dispatch handler paths must equal the registration-derived declared command set", ); - // Bind the scope to the classifier. Narrowing the override to a command list - // means an entry missing on either side is a command that silently loses - // its credential gate, so every scoped path must be one isMutatingGithubCommand - // treats as mutating. The two apply-gated paths (`github export`, - // `github project sync`) only mutate with an explicit apply, so they are - // checked under that configuration rather than skipped. - for (const command of override.commands ?? []) { - const mutatingOptions = - command === "github export" || command === "github project sync" ? { apply: true } : {}; - assert.strictEqual( - isMutatingGithubCommand(command, mutatingOptions), - true, - `${command} is in the preflight scope but the classifier does not treat it as mutating`, + + // --------------------------------------------------------------------- + // Partition the declared set with the SAME predicate production uses — the + // preflight run() itself consults isMutatingGithubCommand — never a second + // copy of the knowledge. The predicate is monotone in its option flags: + // dry-run only ever disables a mutation branch, and apply/no-dry-run/push + // only ever enable one, so a single maximally-mutating probe is exhaustive: + // if the predicate can return true for a command under ANY option object, + // it returns true under this one. "Can mutate GitHub at all" is therefore + // derived from the classifier, not listed by hand. + // --------------------------------------------------------------------- + const MAXIMALLY_MUTATING_OPTIONS: Record = { + apply: true, + "no-dry-run": true, + push: true, + }; + const canMutate = (command: string) => + isMutatingGithubCommand(command, MAXIMALLY_MUTATING_OPTIONS); + const mutatingPaths = declaredPaths.filter(canMutate); + const readOnlyPaths = declaredPaths.filter((command) => !canMutate(command)); + + // Total partition: every declared path lands in exactly one class and the + // two classes reconstruct the declared set EXACTLY. This is the assertion + // that keeps the guard honest — a declared path that escapes both classes + // (an unclassified new command, a normalization mismatch between the + // registration and classification surfaces) fails here BY NAME instead of + // passing unnoticed while it silently loses its credential gate. + assert.deepEqual( + [...mutatingPaths, ...readOnlyPaths].sort(), + declaredPaths, + "the mutating and read-only classes must reconstruct the declared command set exactly", + ); + for (const command of mutatingPaths) { + assert.ok( + !readOnlyPaths.includes(command), + `${command} landed in both the mutating and read-only classes`, + ); + } + assert.ok( + mutatingPaths.length > 0 && readOnlyPaths.length > 0, + "partition should classify commands on both sides (all-one-sided means the probe is broken)", + ); + + // The scope must EQUAL the derived mutating class — set equality in both + // directions, each failing with a command-naming message: + // 1. a declared path the classifier treats as mutating but the scope + // omits loses its early credential gate (the runtime matches by exact + // normalized path and simply never runs the override for it); + // 2. a scope entry that is not a declared pm-github path, or that the + // classifier treats as read-only, is dead weight or a misclassification. + const scopedPaths = [...(override.commands ?? [])].sort(); + assert.deepEqual( + scopedPaths, + mutatingPaths, + "preflight override scope must equal the mutating class of the declared command set exactly", + ); + for (const command of mutatingPaths) { + assert.ok( + scopedPaths.includes(command), + `${command} is declared and isMutatingGithubCommand treats it as mutating, ` + + "but it is missing from the preflight override's commands — it would execute " + + "with no early credential warning", ); } - // Conversely, every command path pm-github declares that the scope omits - // must be read-only — a justified exclusion, not an accidental gap that - // silently dropped a gate. (Commands are declared via registerCommand plus - // the `github` importer/exporter aliases.) - const DECLARED_READ_ONLY_COMMANDS = [ - "github validate", - "github project list", - "github project fields", - ] as const; - for (const command of DECLARED_READ_ONLY_COMMANDS) { + for (const command of scopedPaths) { assert.ok( - !(override.commands ?? []).includes(command), - `${command} is read-only and must not claim a preflight scope entry`, + declaredPaths.includes(command), + `${command} is in the preflight override scope but is not a command path pm-github ` + + "declares (registerCommand/registerImporter/registerExporter)", ); - assert.strictEqual( - isMutatingGithubCommand(command, { apply: true, push: true }), - false, - `${command} is outside the preflight scope but the classifier treats it as mutating`, + assert.ok( + canMutate(command), + `${command} is in the preflight override scope but isMutatingGithubCommand treats it as read-only`, + ); + } + for (const command of readOnlyPaths) { + assert.ok( + !scopedPaths.includes(command), + `${command} is read-only (isMutatingGithubCommand) and must not claim a preflight scope entry`, ); } });