Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .agents/pm/history/pm-linear-lonp.jsonl
Original file line number Diff line number Diff line change
@@ -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"}
14 changes: 14 additions & 0 deletions .agents/pm/issues/pm-linear-lonp.toon
Original file line number Diff line number Diff line change
@@ -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: ""
26 changes: 17 additions & 9 deletions scripts/script-launcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}

/**
Expand Down
19 changes: 19 additions & 0 deletions test/docstring-gate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
}
});