diff --git a/.agents/pm/history/pm-linear-lonp.jsonl b/.agents/pm/history/pm-linear-lonp.jsonl new file mode 100644 index 00000000..82efd8c8 --- /dev/null +++ b/.agents/pm/history/pm-linear-lonp.jsonl @@ -0,0 +1,5 @@ +{"ts":"2026-08-10T06:06:04.494Z","author":"claude","author_source":"asserted","agent_harness":"claude-code","agent_instance":"507208966ab0dc1f88c5707f","agent_provenance":{"model":null,"effort":{"value":"xhigh","source":"environment"},"role":{"value":"1","source":"environment"},"topic":null},"op":"create","patch":[{"op":"add","path":"/metadata/id","value":"pm-linear-lonp"},{"op":"add","path":"/metadata/title","value":"The shared script launcher skipped every gate it guarded when a path could not be resolved"},{"op":"add","path":"/metadata/description","value":"isMainInvocation caught a realpathSync failure and returned false, which makes runIfMain treat a direct invocation as a library import and skip main, so a required release check exits zero having done nothing."},{"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":["docstrings","gate","release"]},{"op":"add","path":"/metadata/created_at","value":"2026-08-10T06:06:04.494Z"},{"op":"add","path":"/metadata/updated_at","value":"2026-08-10T06:06:04.494Z"},{"op":"add","path":"/metadata/author","value":"claude"}],"before_hash":"3cc22dff72be7b14824654a7a64ea62b04799939b2fee54c1b5f52ca60bf6df0","after_hash":"d83717695cfc3e5beee8945caa3001eba99fd9b60321e3405be40bed8bf0ee79","message":""} +{"ts":"2026-08-10T06:06:30.695Z","author":"claude","author_source":"asserted","agent_harness":"claude-code","agent_instance":"507208966ab0dc1f88c5707f","agent_provenance":{"model":null,"effort":{"value":"xhigh","source":"environment"},"role":{"value":"1","source":"environment"},"topic":null},"op":"note_add","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-08-10T06:06:30.695Z"},{"op":"add","path":"/metadata/notes","value":[{"created_at":"2026-08-10T06:06:30.695Z","author":"claude","text":"The refactor into script-launcher.ts is why the fleet-wide sweep missed this repo. A survey that grepped scripts/docstring-gate.ts for realpathSync found nothing here, because the guard had moved into the shared launcher and the gate merely re-exports it. Factoring a defect out of the file people search makes it invisible to the search."}]}],"before_hash":"d83717695cfc3e5beee8945caa3001eba99fd9b60321e3405be40bed8bf0ee79","after_hash":"69e2367d836822c37b6d425dca48518d104dc428db5b088e2fe9be69291699d6"} +{"ts":"2026-08-10T06:06:31.150Z","author":"claude","author_source":"asserted","agent_harness":"claude-code","agent_instance":"507208966ab0dc1f88c5707f","agent_provenance":{"model":null,"effort":{"value":"xhigh","source":"environment"},"role":{"value":"1","source":"environment"},"topic":null},"op":"note_add","patch":[{"op":"add","path":"/metadata/notes/1","value":{"created_at":"2026-08-10T06:06:31.150Z","author":"claude","text":"Unlike the copies in other repositories this variant resolves both sides through realpathSync, deliberately, to normalise Windows drive-letter casing. That reasoning is sound and was kept. Only the catch was removed, so the fix does not reintroduce the casing bug the both-sides comparison exists to prevent."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-08-10T06:06:31.150Z"}],"before_hash":"69e2367d836822c37b6d425dca48518d104dc428db5b088e2fe9be69291699d6","after_hash":"a62c0754a976597f2880898e12b929880832dc3e8247a7cf2cb1421f85a48181"} +{"ts":"2026-08-10T06:07:08.993Z","author":"claude","author_source":"asserted","agent_harness":"claude-code","agent_instance":"507208966ab0dc1f88c5707f","agent_provenance":{"model":null,"effort":{"value":"xhigh","source":"environment"},"role":{"value":"1","source":"environment"},"topic":null},"op":"note_add","patch":[{"op":"add","path":"/metadata/notes/2","value":{"created_at":"2026-08-10T06:07:08.993Z","author":"claude","text":"The refactor into script-launcher.ts is why the fleet-wide sweep missed this repo. A survey that grepped scripts/docstring-gate.ts for realpathSync found nothing here, because the guard had moved into the shared launcher and the gate merely re-exports it."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-08-10T06:07:08.993Z"}],"before_hash":"a62c0754a976597f2880898e12b929880832dc3e8247a7cf2cb1421f85a48181","after_hash":"f1f7cbf733fa48cd8a34ebf3a2635acf19818a5ea19b53a7de198b12cdecdd1e"} +{"ts":"2026-08-10T06:07:28.390Z","author":"claude","author_source":"asserted","agent_harness":"claude-code","agent_instance":"507208966ab0dc1f88c5707f","agent_provenance":{"model":null,"effort":{"value":"xhigh","source":"environment"},"role":{"value":"1","source":"environment"},"topic":null},"op":"note_delete","patch":[{"op":"remove","path":"/metadata/notes/2"},{"op":"replace","path":"/metadata/updated_at","value":"2026-08-10T06:07:28.390Z"}],"before_hash":"f1f7cbf733fa48cd8a34ebf3a2635acf19818a5ea19b53a7de198b12cdecdd1e","after_hash":"0aa88d05f28c9081abb025c0d8a9a2b5cdc69e92fb7ac4388b325cc71c3fd18e"} diff --git a/.agents/pm/issues/pm-linear-lonp.toon b/.agents/pm/issues/pm-linear-lonp.toon new file mode 100644 index 00000000..5574a571 --- /dev/null +++ b/.agents/pm/issues/pm-linear-lonp.toon @@ -0,0 +1,14 @@ +id: pm-linear-lonp +title: The shared script launcher skipped every gate it guarded when a path could not be resolved +description: "isMainInvocation caught a realpathSync failure and returned false, which makes runIfMain treat a direct invocation as a library import and skip main, so a required release check exits zero having done nothing." +type: Issue +status: open +priority: 1 +tags[3]: docstrings,gate,release +created_at: "2026-08-10T06:06:04.494Z" +updated_at: "2026-08-10T06:07:28.390Z" +author: claude +notes[2]{created_at,author,text}: + "2026-08-10T06:06:30.695Z",claude,"The refactor into script-launcher.ts is why the fleet-wide sweep missed this repo. A survey that grepped scripts/docstring-gate.ts for realpathSync found nothing here, because the guard had moved into the shared launcher and the gate merely re-exports it. Factoring a defect out of the file people search makes it invisible to the search." + "2026-08-10T06:06:31.150Z",claude,"Unlike the copies in other repositories this variant resolves both sides through realpathSync, deliberately, to normalise Windows drive-letter casing. That reasoning is sound and was kept. Only the catch was removed, so the fix does not reintroduce the casing bug the both-sides comparison exists to prevent." +body: "" diff --git a/scripts/script-launcher.ts b/scripts/script-launcher.ts index 1932a144..aa580c5c 100644 --- a/scripts/script-launcher.ts +++ b/scripts/script-launcher.ts @@ -22,22 +22,30 @@ import { fileURLToPath } from "node:url"; * return a different drive-letter casing than the URL form, so an exact href * comparison would treat a direct invocation as a library import and silently * skip `main`. Resolving both sides through `realpathSync` after converting the - * URL back to a path removes that casing and symlink ambiguity. A release - * script that no-ops without erroring is worse than one that throws, so fail - * closed if either path cannot be resolved. + * URL back to a path removes that casing and symlink ambiguity. + * + * An unresolvable path **propagates** rather than returning false. The two + * outcomes are not equally safe: returning false makes the caller treat the + * script as a library import and skip `main`, so a required release check + * exits 0 having done nothing — the one failure these gates exist to prevent. + * Letting `realpathSync` throw turns that into a loud non-zero exit. Either + * path failing to resolve after Node has already loaded the module means the + * environment is broken, and a broken environment must not silently satisfy a + * gate. + * + * A genuinely different entry path still returns false, which is how a test + * importing a script declines to run it. * * @param argv - The process argv slice to inspect. * @param moduleUrl - The `import.meta.url` of the module that might be main. - * @returns True when `argv[1]` resolves to this module's own real path. + * @returns True when `argv[1]` resolves to this module's own real path, false + * when it resolves to a different one. + * @throws Whatever `realpathSync` throws when either path cannot be resolved. */ export function isMainInvocation(argv: readonly string[], moduleUrl: string): boolean { const entry = argv[1]; if (entry === undefined) return false; - try { - return realpathSync(entry) === realpathSync(fileURLToPath(moduleUrl)); - } catch { - return false; - } + return realpathSync(entry) === realpathSync(fileURLToPath(moduleUrl)); } /** diff --git a/test/docstring-gate.test.ts b/test/docstring-gate.test.ts index af91752d..da81ada5 100644 --- a/test/docstring-gate.test.ts +++ b/test/docstring-gate.test.ts @@ -138,3 +138,22 @@ test("docstring gate main writes a success line to stdout and exits 0", () => { assert.equal(observedExitCode, 0); assert.match(stdout, /docstring-gate:.*documented\.\n$/); }); + +test("docstring gate isMainInvocation throws rather than skipping the gate when a path cannot be resolved", () => { + const root = mkdtempSync(join(tmpdir(), "pm-linear-docstring-unresolvable-")); + try { + const script = join(root, "docstring-gate.ts"); + writeFileSync(script, ""); + const url = pathToFileURL(script).href; + // Returning false here would make the caller treat the script as a library + // import and skip main, so a required release check exits 0 having done + // nothing. Crashing is the safe outcome, so assert it is what happens. + assert.throws( + () => isMainInvocation([process.execPath, join(root, "does-not-exist.ts")], url), + /ENOENT/, + "an unresolvable entry must propagate, not silently decline to run the gate", + ); + } finally { + rmSync(root, { recursive: true, force: true }); + } +});