Conversation
… benchmark findings Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM
openspec archive applied unit-rung-and-fixture-snapshots' deltas to openspec/specs, but the archive commit (17a1225) staged only openspec/changes and a post-merge git reset --hard discarded the spec edits. Restored: engine-invocation +2 requirements (per-call record store; CLI store parity); suite-certification +2 (fixture snapshots; rung membership by observable) and 3 replaced with the 0.48.0 text. 0.47.0's deltas verified complete. openspec validate --specs --strict: 14/14. Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM
Support the oldest non-EOL Node major (floor 22); CI matrix from nodejs/Release schedule.json with a committed fallback; per-file test mode (measured ~2x faster than single-process on 22/24/26); forced spec reporter; retire the isolation probe and [ -f ] loop; EOL warning in brief; actions v7; correct every Node 18+ claim; bound child waits (#220). Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM
…review 4 reviewers (2 Gate 1 lenses, 2 cross-spec lenses) all failed: 7 BLOCKS incl. CI green on a zero-test bucket, an unfalsifiable version-default scenario, 'supported' contradicting between R1/R2 (23/25/27), an over-wide bounded-wait requirement, an unfalsifiable schedule filter (now scripts/test/node-majors.mjs + canned-schedule test); 10 Important; polish. All fixed except one declined with reason. Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM
End-clause canned entry + mutation (B5); twin-coupling landing order per certification.mjs:173 (1.3→1.2, 1.5→1.4, 2.4c→2.4b); misplaced task lines. Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM
The pre-commit hook's runner now forces `--test-reporter=spec` with
`FORCE_COLOR=0`, parses `^ℹ` only (the `#` TAP branch is deleted), and
checks in design D5's order: an unreadable summary ABORTs ("the count
could not be read"); `total < declared` is the shortfall ABORT, zero
included; `declared = 0` is the empty-rung ABORT. Previously a
colour-forcing environment made the hook print "tests passing" with the
floor skipped.
- 1.1 RED: functional/conductor-09 runs the hook under FORCE_COLOR=1 and
asserts `2/2 passing`. Failing run: red-1.1.txt (Node v26.9.0).
- 1.2 GREEN: the runner line and the ordered checks; a durable stub-node
fixture for the unreadable-count refusal; assert/conductor-09's
RUNNER_LINE re-pointed, its node --test count widened to see env
prefixes, and a text guard for the refusal and the ℹ-only parse.
Mutations: mutation-1.2.txt.
- 1.3 REGRESSION GUARD: both rungs holding only `.keep` refuse, naming
both rungs, and default discovery is never reached.
- 0.3 baseline-before.md; 0.1/0.2 ticked (verdicts recorded, not stale).
- runHookAgainstFixture gains env / pathPrepend / withFixture / setup.
Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM
- 2.1 RED: assert-half-has-no-spawn gains a walk over BOTH rungs that refuses, by name, a file installing the run-time git counter neither directly nor through assert-harness/unit-harness, plus a discrimination test (side-effect, named, each harness, a two-line import; a comment- only mention and an unrelated fixture are refused). Failing run, naming exactly the 13 file-rung files of design D3 row 1: red-2.1.txt. - 2.2 GREEN: each of the 13 gains `import "../fixtures/assert-git-shim.mjs";` as its first import. The half is 1271/1271 both per-file and single-process (1269 at 0.3(b) plus the two new tests); none of the 13 went red, so none was reaching the real git undetected. Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM
- 2.3 RED: new assert/git-shim.test.mjs calls the shim module's exported
removeTempDir() on a scratch tree and asserts it is gone. Failing run:
red-2.3.txt (the module exported no such function).
- 2.4 GREEN: assert-git-shim.mjs exports removeTempDir(); its exit
listener reads the log, then removes shimDir. G-I4 is re-scoped per
design D3 row 2: the per-process exit listener is the mechanism, and
the direct check is that the shim is first on PATH in THIS process.
The shim's prose is rewritten, and the old requirement name ("The
assertion half spawns no process and runs no git") is re-pointed at
every live citation: the shim's printed text, and
assert-half-has-no-spawn's header and unit-rung section (6.2).
verify-2.4.txt: pm-assert-no-git-* count 4705 -> 4705 across one full
run of the half, per-file and single-process.
Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM
…all in any file's process fails the run - 2.4b RED + GREEN: assert/git-shim gains a case asserting the harness's EMPTY_CACHE is scheduled for removal at process exit through the shim's own removal function (failing run: red-2.4b.txt). New fixtures/temp-dir.mjs owns removeTempDir/removeAtExit/ scheduledForRemoval — a separate module because harness.mjs is also imported by the functional half, which must never load the shim; the shim re-exports them. verify-2.4b.txt: pm-empty-cache-* 7804 -> 7804 across one full run, both modes. - 2.4c REGRESSION GUARD: new functional/git-shim.test.mjs runs a fixture file that installs the shim and reaches git through a tolerated library call, under `FORCE_COLOR=0 node --test --test-reporter=spec`, and asserts exit 1, `ℹ fail 1` and the listener naming `git --version`. Twin: assert/git-shim (edited here). mutation-2.4c.txt: the listener's `process.exitCode = 1` removed -> the functional test fails. Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM
… rationale
2.5 (design D3 rows 4-8, 10, 11): every comment and message that gave
"the assertion half is one process" as its reason now gives the reason
that survives per-file isolation — an in-process caller serving many
invocations, or one file's process:
- scripts/lib/invocation.mjs (three), constants.mjs, command-exit.mjs,
git-gateway.mjs;
- fixtures/helpers.mjs, fixtures/fs-work-counter.mjs, certification.mjs;
- assert/no-inline-exit (rationale + message), assert/per-call-roots,
assert/delivered-obligations;
- assert-half-has-no-spawn's header, walk message and the unit-rung test
TITLE ("...and is a rung of the half" — a renamed test, not a new one).
- functional/conformance's sync-main() rationale, which the 2.5 rg also
caught; its twin assert/conformance gains the one-line rationale so the
coupling check sees a real edit.
verify-2.5.txt classifies every surviving rg line. certify sweeps and
functional ran (constants.mjs is certified).
Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM
…eleted - 1.4 GREEN (design D3, D5): the isolation probe, ISOFLAGFILE, $ISOFLAG and the `[ -f ]` loop are gone. The runner is `FORCE_COLOR=0 node --test --test-reporter=spec scripts/test/unit/*.test.mjs scripts/test/assert/*.test.mjs` under the runner's default per-file isolation. `rm -f` of the old pm-isolation-flag is the inverse of the probe's write. The empty-rung abort is check 3 (`declared = 0`), after the floor. Hook prose and assert/conductor-09's narrative rewritten; its guard re-pointed: the runner line by equality, exactly ONE node --test line, the two rung globs unit first, the zero-count abort after the floor, no --test-isolation, the rm -f line. functional/conductor-09's Node-18 narrative (the one-unmatched-rung test) is history now, assertions kept; a new fixture proves a collapsed run is a shortfall, not an empty rung. verify-1.4.txt: the file on 22.23.3, 24.21.0, 26.10.0, 33/33 each, G-I1 firing on each. mutation-1.4.txt: (i)-(iv) refused. - 1.5 REGRESSION GUARD: a fixture whose git dir holds a stale pm-isolation-flag has none after one hook run. mutation-1.5.txt. - 2.6: verify-2.6.txt records the one-time cleanup (4705 and 7804 leaked directories -> 0); a machine action, no repository change. Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM
…f's one warning line - 3.1: new unit/runtime-support.test.mjs over memoryEngine. RED (no seam, no line): the below-floor line under v20.20.2 names 20.20.2 and 22 and leaves the rest of the brief and the status unchanged; two different below-floor versions in one process each name their own; with no version supplied, a store read observed mid-call sees process.version. Failing run: red-3.1.txt. REGRESSION GUARDS (at/above incl. v25, an unparseable version, uninitialized, render/snapshot), each seen to fail on its mutation: mutation-3.1.txt. - 3.2 GREEN: new scripts/lib/runtime-support.mjs (NODE_FLOOR_MAJOR = 22, supportFloorLine(), the version through escapeControls); invocation.mjs gains PROCESS_CONTEXT.nodeVersion (live) and runtimeVersion(ctx) = ctx.nodeVersion ?? process.version; runInvocation sets nodeVersion: io.nodeVersion ?? process.version; brief() prepends the line (never buildBrief()); fixtures/harness.mjs invokeEngine and fixtures/unit-harness.mjs memoryEngine pass nodeVersion through. New functional/runtime-support.test.mjs spawns `node --import <preload>` with process.version redefined to v20.20.2 and asserts the line, and none without the preload. conductor.mjs's "Node 18+" header corrected. The output-interpolation sweep is green (version escaped, the constant literal). Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM
… and HTTP/2 3.3 REGRESSION GUARD, decided: widen. conductor-35's forbidden set named only fetch( and node:http(s); it now also refuses node:net, node:tls, node:dgram and node:http2 (which `node:https?\b` never matched), and their require() forms. mutation-3.3.txt: `import net from "node:net"` added to a scratch runtime-support.mjs is refused, and the pre-3.3 set is shown passing it. Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM
…n clause by clause
- 4.2b RED + GREEN (design D2, Gate 1 B5): new scripts/test/node-majors.mjs
exports supportedMajors(schedule, today) — an lts date present,
start <= today, end > today — and decide({ fetched, body, today,
fallback }) owning every row of D2's table (failed fetch -> fallback +
warning; unreadable body -> error, never the fallback; computed !=
committed -> error naming both; empty -> error), plus the CLI tail CI's
node-majors job runs. New assert/support-floor.test.mjs drives both
against a CANNED schedule where each clause alone decides one entry.
Failing run: red-4.2b.txt (module absent). mutation-4.2b.txt: start,
lts and end clauses removed, and a malformed body routed to the
fallback — each refused. The README/CLAUDE.md support-floor-copy cases
land with 7.2/7.3, which rewrite those lines (per the task).
- 4.1: actions-notes-4.1.md — checkout/setup-node v5, v6, v7 release notes
read; latest tags re-confirmed v7.0.1 / v7.0.0, both node24; no default
this workflow relies on changes, and fetch-depth: 0 keeps its meaning.
Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM
…e, behind one aggregate `test` - 4.2 RED: assert/ci-workflow gains pure functions over ci.yml's text, each with a test: (a)(b) an aggregate `test` job with `if: always()` needing both jobs and failing unless both results are success; (c) the matrix job's fail-fast: false, timeout-minutes and matrix.node from the compute output; (d) curl of the pinned schedule URL and node-majors.mjs given the file or --fetch-failed, `date -u +%F` and $PM_NODE_FALLBACK; (e) min(PM_NODE_FALLBACK) === NODE_FLOOR_MAJOR, naming both on a mismatch; (f)(f2) every bucket step forces the reporter and FORCE_COLOR=0, parses ℹ only and refuses a zero count; (g) v7 actions, no Node 18 pin. runnerLine() allows flags before the globs; the pipeline pattern accepts NAME=value prefixes (Gate 1 I5); the G-C1 fixtures are re-pointed. Failing run against the old ci.yml: red-4.2.txt. - 4.3 GREEN: ci.yml rewritten per design D2 — node-majors (the support floor's Node, curl + node-majors.mjs), test-node (matrix, reported as `test (node N)`), and the `test` aggregate; PM_NODE_FALLBACK "[22,24,26]"; actions @v7; every bucket step one reporter, ℹ parse, an unreadable/short/zero count refused in that order; the stale comment block and its dangling path replaced. security.yml untouched. mutation-4.3.txt: all twelve mutations refused. Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM
…er an unreadable or zero count 4.4 RED + GREEN: certify.mjs exports summaryCount(output, label); new assert/certify-count.test.mjs asserts it reads the spec summary and returns null for a TAP summary (`# tests 3`) and for a coloured one (failing run: red-4.4.txt — no export, and the old regex accepted `#`). runBucket() starts the runner with --test-reporter=spec and FORCE_COLOR=0; a pass whose tests/pass count is null, or whose tests count is 0, records nothing and exits 1. The header comment naming the CI runner's Node 18 is corrected. `certify sweeps` recorded 25/25 through the new path. Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM
…k path (task 4.5, never merged) Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM
…match path (task 4.5, never merged) Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM
… a CI runner (task 8.3, never merged) Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM
Owner
Author
|
Throwaway CI verification for node-support-policy (tasks 4.5 and 8.3) — closed unmerged; branch deleted. |
cfdude
added a commit
that referenced
this pull request
Sep 25, 2026
…fallback and mismatch runs 4.5: ci-verify-4.5.txt. Through throwaway draft PR #221 (closed unmerged, branch deleted remote and local): the committed workflow computed [22,24,26], ran `test (node 22|24|26)` with `ran 1294, declared 1294` on every leg, and reported the aggregate under `test`; a 404 schedule URL fell back to [22,24,26] with a visible warning; a fallback of [22,24] failed node-majors naming both sets, and the `if: always()` aggregate failed on the skipped legs. Runs 36089089608 / 36089519332 / 36089888678. Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM
cfdude
added a commit
that referenced
this pull request
Sep 25, 2026
…g faster, per-file slower on 4 cores 8.1: the hook end to end at cd093fe, hermetic clone, same method as 0.3: median 24.0 s (runner 15.7 s, 1298 tests) against 33.55 s before. Design D10's outcome: NOT under 15 s, so store-owns-claude-md-managed- block's premise stands (not touched). Leaked temp dirs: 0 / 0 before and after, against 4,043 / 6,879 at 0.3(d). 8.2: from this change's PR run 36089089608, every leg is faster than the Node 18 job in the job and in each bucket step (4m18s / 4m18s / 3m38s against 4m46s). 8.3: on a CI runner (nproc 4, Node 24) per-file is SLOWER — 14.49 s against 11.33 s single-process, median of three interleaved runs each (run 36089977671, throwaway draft PR #221, closed unmerged, branch deleted). Recorded so the trade is known; the decision stands. Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM
cfdude
added a commit
that referenced
this pull request
Sep 25, 2026
…ed LTS line (#225) * chore(pm): upgrade conductor to 0.48.0 * chore(conductor): register node-support-policy (0.49.0) with the Node benchmark findings Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM * fix(openspec): restore the 0.48.0 main-spec sync lost to a reset openspec archive applied unit-rung-and-fixture-snapshots' deltas to openspec/specs, but the archive commit (17a1225) staged only openspec/changes and a post-merge git reset --hard discarded the spec edits. Restored: engine-invocation +2 requirements (per-call record store; CLI store parity); suite-certification +2 (fixture snapshots; rung membership by observable) and 3 replaced with the 0.48.0 text. 0.47.0's deltas verified complete. openspec validate --specs --strict: 14/14. Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM * docs(lessons): an openspec archive writes the main specs too — stage both halves Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM * docs(openspec): propose node-support-policy (0.49.0) Support the oldest non-EOL Node major (floor 22); CI matrix from nodejs/Release schedule.json with a committed fallback; per-file test mode (measured ~2x faster than single-process on 22/24/26); forced spec reporter; retire the isolation probe and [ -f ] loop; EOL warning in brief; actions v7; correct every Node 18+ claim; bound child waits (#220). Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM * docs(openspec): revise node-support-policy for Gate 1 and cross-spec review 4 reviewers (2 Gate 1 lenses, 2 cross-spec lenses) all failed: 7 BLOCKS incl. CI green on a zero-test bucket, an unfalsifiable version-default scenario, 'supported' contradicting between R1/R2 (23/25/27), an over-wide bounded-wait requirement, an unfalsifiable schedule filter (now scripts/test/node-majors.mjs + canned-schedule test); 10 Important; polish. All fixed except one declined with reason. Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM * docs(openspec): close node-support-policy confirmation residue End-clause canned entry + mutation (B5); twin-coupling landing order per certification.mjs:173 (1.3→1.2, 1.5→1.4, 2.4c→2.4b); misplaced task lines. Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM * fix(hook): one reporter, no colour, and an unreadable count refuses The pre-commit hook's runner now forces `--test-reporter=spec` with `FORCE_COLOR=0`, parses `^ℹ` only (the `#` TAP branch is deleted), and checks in design D5's order: an unreadable summary ABORTs ("the count could not be read"); `total < declared` is the shortfall ABORT, zero included; `declared = 0` is the empty-rung ABORT. Previously a colour-forcing environment made the hook print "tests passing" with the floor skipped. - 1.1 RED: functional/conductor-09 runs the hook under FORCE_COLOR=1 and asserts `2/2 passing`. Failing run: red-1.1.txt (Node v26.9.0). - 1.2 GREEN: the runner line and the ordered checks; a durable stub-node fixture for the unreadable-count refusal; assert/conductor-09's RUNNER_LINE re-pointed, its node --test count widened to see env prefixes, and a text guard for the refusal and the ℹ-only parse. Mutations: mutation-1.2.txt. - 1.3 REGRESSION GUARD: both rungs holding only `.keep` refuse, naming both rungs, and default discovery is never reached. - 0.3 baseline-before.md; 0.1/0.2 ticked (verdicts recorded, not stale). - runHookAgainstFixture gains env / pathPrepend / withFixture / setup. Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM * test(assert): every rung file installs the git shim in its own process - 2.1 RED: assert-half-has-no-spawn gains a walk over BOTH rungs that refuses, by name, a file installing the run-time git counter neither directly nor through assert-harness/unit-harness, plus a discrimination test (side-effect, named, each harness, a two-line import; a comment- only mention and an unrelated fixture are refused). Failing run, naming exactly the 13 file-rung files of design D3 row 1: red-2.1.txt. - 2.2 GREEN: each of the 13 gains `import "../fixtures/assert-git-shim.mjs";` as its first import. The half is 1271/1271 both per-file and single-process (1269 at 0.3(b) plus the two new tests); none of the 13 went red, so none was reaching the real git undetected. Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM * fix(test): the git shim removes its temp directory at process exit - 2.3 RED: new assert/git-shim.test.mjs calls the shim module's exported removeTempDir() on a scratch tree and asserts it is gone. Failing run: red-2.3.txt (the module exported no such function). - 2.4 GREEN: assert-git-shim.mjs exports removeTempDir(); its exit listener reads the log, then removes shimDir. G-I4 is re-scoped per design D3 row 2: the per-process exit listener is the mechanism, and the direct check is that the shim is first on PATH in THIS process. The shim's prose is rewritten, and the old requirement name ("The assertion half spawns no process and runs no git") is re-pointed at every live citation: the shim's printed text, and assert-half-has-no-spawn's header and unit-rung section (6.2). verify-2.4.txt: pm-assert-no-git-* count 4705 -> 4705 across one full run of the half, per-file and single-process. Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM * fix(test): the harness's empty cache is removed at exit; a real git call in any file's process fails the run - 2.4b RED + GREEN: assert/git-shim gains a case asserting the harness's EMPTY_CACHE is scheduled for removal at process exit through the shim's own removal function (failing run: red-2.4b.txt). New fixtures/temp-dir.mjs owns removeTempDir/removeAtExit/ scheduledForRemoval — a separate module because harness.mjs is also imported by the functional half, which must never load the shim; the shim re-exports them. verify-2.4b.txt: pm-empty-cache-* 7804 -> 7804 across one full run, both modes. - 2.4c REGRESSION GUARD: new functional/git-shim.test.mjs runs a fixture file that installs the shim and reaches git through a tolerated library call, under `FORCE_COLOR=0 node --test --test-reporter=spec`, and asserts exit 1, `ℹ fail 1` and the listener naming `git --version`. Twin: assert/git-shim (edited here). mutation-2.4c.txt: the listener's `process.exitCode = 1` removed -> the functional test fails. Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM * docs(test): comments no longer state the single-process half as their rationale 2.5 (design D3 rows 4-8, 10, 11): every comment and message that gave "the assertion half is one process" as its reason now gives the reason that survives per-file isolation — an in-process caller serving many invocations, or one file's process: - scripts/lib/invocation.mjs (three), constants.mjs, command-exit.mjs, git-gateway.mjs; - fixtures/helpers.mjs, fixtures/fs-work-counter.mjs, certification.mjs; - assert/no-inline-exit (rationale + message), assert/per-call-roots, assert/delivered-obligations; - assert-half-has-no-spawn's header, walk message and the unit-rung test TITLE ("...and is a rung of the half" — a renamed test, not a new one). - functional/conformance's sync-main() rationale, which the 2.5 rg also caught; its twin assert/conformance gains the one-line rationale so the coupling check sees a real edit. verify-2.5.txt classifies every surviving rg line. certify sweeps and functional ran (constants.mjs is certified). Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM * perf(hook): per-file isolation — the probe and the Node-18 loop are deleted - 1.4 GREEN (design D3, D5): the isolation probe, ISOFLAGFILE, $ISOFLAG and the `[ -f ]` loop are gone. The runner is `FORCE_COLOR=0 node --test --test-reporter=spec scripts/test/unit/*.test.mjs scripts/test/assert/*.test.mjs` under the runner's default per-file isolation. `rm -f` of the old pm-isolation-flag is the inverse of the probe's write. The empty-rung abort is check 3 (`declared = 0`), after the floor. Hook prose and assert/conductor-09's narrative rewritten; its guard re-pointed: the runner line by equality, exactly ONE node --test line, the two rung globs unit first, the zero-count abort after the floor, no --test-isolation, the rm -f line. functional/conductor-09's Node-18 narrative (the one-unmatched-rung test) is history now, assertions kept; a new fixture proves a collapsed run is a shortfall, not an empty rung. verify-1.4.txt: the file on 22.23.3, 24.21.0, 26.10.0, 33/33 each, G-I1 firing on each. mutation-1.4.txt: (i)-(iv) refused. - 1.5 REGRESSION GUARD: a fixture whose git dir holds a stale pm-isolation-flag has none after one hook run. mutation-1.5.txt. - 2.6: verify-2.6.txt records the one-time cleanup (4705 and 7804 leaked directories -> 0); a machine action, no repository change. Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM * feat(engine): the support floor, a per-call runtime version, and brief's one warning line - 3.1: new unit/runtime-support.test.mjs over memoryEngine. RED (no seam, no line): the below-floor line under v20.20.2 names 20.20.2 and 22 and leaves the rest of the brief and the status unchanged; two different below-floor versions in one process each name their own; with no version supplied, a store read observed mid-call sees process.version. Failing run: red-3.1.txt. REGRESSION GUARDS (at/above incl. v25, an unparseable version, uninitialized, render/snapshot), each seen to fail on its mutation: mutation-3.1.txt. - 3.2 GREEN: new scripts/lib/runtime-support.mjs (NODE_FLOOR_MAJOR = 22, supportFloorLine(), the version through escapeControls); invocation.mjs gains PROCESS_CONTEXT.nodeVersion (live) and runtimeVersion(ctx) = ctx.nodeVersion ?? process.version; runInvocation sets nodeVersion: io.nodeVersion ?? process.version; brief() prepends the line (never buildBrief()); fixtures/harness.mjs invokeEngine and fixtures/unit-harness.mjs memoryEngine pass nodeVersion through. New functional/runtime-support.test.mjs spawns `node --import <preload>` with process.version redefined to v20.20.2 and asserts the line, and none without the preload. conductor.mjs's "Node 18+" header corrected. The output-interpolation sweep is green (version escaped, the constant literal). Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM * test(assert): the no-network walk refuses raw sockets, TLS, datagrams and HTTP/2 3.3 REGRESSION GUARD, decided: widen. conductor-35's forbidden set named only fetch( and node:http(s); it now also refuses node:net, node:tls, node:dgram and node:http2 (which `node:https?\b` never matched), and their require() forms. mutation-3.3.txt: `import net from "node:net"` added to a scratch runtime-support.mjs is refused, and the pre-3.3 set is shown passing it. Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM * feat(ci): the supported-major filter as a committed dev script, proven clause by clause - 4.2b RED + GREEN (design D2, Gate 1 B5): new scripts/test/node-majors.mjs exports supportedMajors(schedule, today) — an lts date present, start <= today, end > today — and decide({ fetched, body, today, fallback }) owning every row of D2's table (failed fetch -> fallback + warning; unreadable body -> error, never the fallback; computed != committed -> error naming both; empty -> error), plus the CLI tail CI's node-majors job runs. New assert/support-floor.test.mjs drives both against a CANNED schedule where each clause alone decides one entry. Failing run: red-4.2b.txt (module absent). mutation-4.2b.txt: start, lts and end clauses removed, and a malformed body routed to the fallback — each refused. The README/CLAUDE.md support-floor-copy cases land with 7.2/7.3, which rewrite those lines (per the task). - 4.1: actions-notes-4.1.md — checkout/setup-node v5, v6, v7 release notes read; latest tags re-confirmed v7.0.1 / v7.0.0, both node24; no default this workflow relies on changes, and fetch-depth: 0 keeps its meaning. Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM * feat(ci): a matrix of every supported major computed from the schedule, behind one aggregate `test` - 4.2 RED: assert/ci-workflow gains pure functions over ci.yml's text, each with a test: (a)(b) an aggregate `test` job with `if: always()` needing both jobs and failing unless both results are success; (c) the matrix job's fail-fast: false, timeout-minutes and matrix.node from the compute output; (d) curl of the pinned schedule URL and node-majors.mjs given the file or --fetch-failed, `date -u +%F` and $PM_NODE_FALLBACK; (e) min(PM_NODE_FALLBACK) === NODE_FLOOR_MAJOR, naming both on a mismatch; (f)(f2) every bucket step forces the reporter and FORCE_COLOR=0, parses ℹ only and refuses a zero count; (g) v7 actions, no Node 18 pin. runnerLine() allows flags before the globs; the pipeline pattern accepts NAME=value prefixes (Gate 1 I5); the G-C1 fixtures are re-pointed. Failing run against the old ci.yml: red-4.2.txt. - 4.3 GREEN: ci.yml rewritten per design D2 — node-majors (the support floor's Node, curl + node-majors.mjs), test-node (matrix, reported as `test (node N)`), and the `test` aggregate; PM_NODE_FALLBACK "[22,24,26]"; actions @v7; every bucket step one reporter, ℹ parse, an unreadable/short/zero count refused in that order; the stale comment block and its dangling path replaced. security.yml untouched. mutation-4.3.txt: all twelve mutations refused. Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM * fix(certify): one reporter, the ℹ parse only, and nothing recorded over an unreadable or zero count 4.4 RED + GREEN: certify.mjs exports summaryCount(output, label); new assert/certify-count.test.mjs asserts it reads the spec summary and returns null for a TAP summary (`# tests 3`) and for a coloured one (failing run: red-4.4.txt — no export, and the old regex accepted `#`). runBucket() starts the runner with --test-reporter=spec and FORCE_COLOR=0; a pass whose tests/pass count is null, or whose tests count is 0, records nothing and exits 1. The header comment naming the CI runner's Node 18 is corrected. `certify sweeps` recorded 25/25 through the new path. Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM * docs(openspec): record the CI matrix verified on Actions — computed, fallback and mismatch runs 4.5: ci-verify-4.5.txt. Through throwaway draft PR #221 (closed unmerged, branch deleted remote and local): the committed workflow computed [22,24,26], ran `test (node 22|24|26)` with `ran 1294, declared 1294` on every leg, and reported the aggregate under `test`; a 404 schedule URL fell back to [22,24,26] with a visible warning; a fallback of [22,24] failed node-majors naming both sets, and the `if: always()` aggregate failed on the skipped legs. Runs 36089089608 / 36089519332 / 36089888678. Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM * fix(test): every awaited child is bounded — a hung child fails its test, not the run (#220) - 5.1 RED: assert/state-file-refuses-to-guess gains a source scan over scripts/test/**/*.mjs: every wait on a child's close/exit event, in all three forms (<child>.on, <child>.once, events once(<child>, …)), must sit in a function that arms setTimeout( and .kill( — process.on("exit") excluded, comments blanked, tokens and samples built from parts. A discrimination test refuses an unbounded sample of each form and accepts a bounded one. Failing run naming functional/state-file-refuses-to-guess.test.mjs:299: red-5.1.txt. - 5.2 GREEN: spawnAll arms a 30 s timer per child (verb-surface's bound) that SIGKILLs it and resolves { timedOut: true }; both callers assert timedOut is false naming the argv and the bound. verify-5.2.txt: the functional file 46/46 on 22.23.3, 24.21.0, 26.10.0. - 5.3 MUTATION: one never-exiting child handed to spawnAll fails 3.1 in 30.2 s naming `node conductor.mjs __hang__ never exits`, and the run continues (45/46): mutation-5.3.txt. Fixes #220. Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM * docs(contributing): per-file runner, the aggregate `test` check over the matrix, re-measured quickstart 7.1: the required-check description names the `test` aggregate over the `test (node N)` matrix and the computed supported majors; the hook's command is the forced-reporter, no-colour runner line; the quickstart and watch commands drop --test-isolation=none; "not optional" becomes a note on --test-concurrency for throttling; "one process" wording becomes one runner invocation; the guard section names the per-file shim-install rule. The quickstart was re-run in a fresh clone of a848988: Node v22.23.3 (the support floor) 1296/1296 in 14.0 s, Node v26.9.0 (Homebrew) 1296/1296 in 18.0 s. Every runnable command in the file was run as written in that clone (git config, both node --test forms, --watch for 8 s, --test-concurrency=2, the engine banner); the merge/rebase/tag procedures were not executed. Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM * docs(readme): Node 22+ — pm supports the oldest Node LTS line that is not end-of-life 7.2: README.md's two "Node 18+" claims (the Real Numbers line and Installation's requirements) carry the policy wording. In the same commit 4.2b's documented-support-floor assertion lands in assert/support-floor.test.mjs: every `Node N+` in README.md (at least 2) and CONTRIBUTING.md (at least 1, the copy 7.1 added) must equal NODE_FLOOR_MAJOR, naming the file and both values otherwise, with a discrimination test. The README's "1,116 tests" is left to the release cut's Real Numbers recompute, by design. Documentation currency: YES, user-facing; the pm-plugin.dev pages go to the release cut (7.6). Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM * docs(claude-md): the support-floor wording, the derived built-in list, and no single-process mandate 7.3: CLAUDE.md's hard constraint states Node 22+ with the policy wording and the engine's seven built-ins, re-derived (node:child_process, node:crypto, node:fs, node:os, node:path, node:tty, node:url — it named five). The Tests bullet loses --test-isolation=none and "in ONE process": both rungs run in one runner invocation whose TESTS spawn nothing and run no git. 4.2b's CLAUDE.md support-floor assertion lands here (CLAUDE.md added to assert/support-floor's copies). `rg -n 'isolation|Node 18' CLAUDE.md` returns nothing. Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM * docs(skills): per-file test commands, the aggregate check, a stop line on the Real Numbers count, and the support-floor step 7.4: - pr-workflow: the test commands drop --test-isolation=none (one runner invocation); the description and intro say the required `test` check is an aggregate over the `test (node N)` matrix legs. - release-checklist: step 1's command and "one process" wording; the Real Numbers recipe runs `FORCE_COLOR=0 node --test --test-reporter=spec …`, greps `-m1 '^ℹ tests '`, and STOPS without publishing when the line is missing or reads 0 (Gate 1 B1); a new support-floor step at "Engine + tests" computes the supported set via scripts/test/node-majors.mjs against the release date and names NODE_FLOOR_MAJOR, PM_NODE_FALLBACK, the README/CONTRIBUTING/CLAUDE.md copies and the docs-site pages as one unit. Its command was run as written: [22,24,26], exit 0. `rg -n 'isolation' .claude/skills` returns nothing. Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM * docs(release-checklist): the archive commit stages openspec/ whole — closes the archive lesson 7.8: the release-checklist skill had no archive step at all; step 5 now archives each change after its Gate 2 and says, in these words, "stage openspec/ whole (or everything `git status --short openspec/` lists) — the archive rewrites openspec/specs too". In the same commit the lesson docs/lessons/an-archive-writes-outside-the-change-dir.md's enforced_in and docs/lessons/README.md's enforced-in row name that step. `rg -n 'stage openspec/ whole' .claude/skills/release-checklist/SKILL.md` returns one hit; `rg -n 'until 0.49.0' docs/lessons/` returns nothing. Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM * docs(changelog): an [Unreleased] entry for the Node support policy 7.5: CHANGELOG.md gains `## [Unreleased]` — Changed (the support policy and floor 22; the CI matrix computed from the schedule behind the `test` aggregate; per-file isolation with 0.3's and 8.1's numbers — hook 33.55 s -> 24.0 s here, still not under 15 s, and 14.5 s vs 11.3 s against it on a 4-core runner; one reporter; actions v7), Added (the brief's below-floor line), Fixed (#220; the hook's skipped floor on an unreadable count; the shim and harness temp-dir leaks), Removed (the isolation probe and the Node-18 loop). The release cut folds it into 0.49.0. 7.7: `git diff --stat aa48c88..HEAD -- commands/ agents/ skills/ hooks/ .claude-plugin/` is empty and assert/parity is 9/9 — nothing shipped changed, so the ledger needs no entry; the version bump is the cut's. Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM * docs(openspec): the AFTER measurement — hook 24.0 s here, every CI leg faster, per-file slower on 4 cores 8.1: the hook end to end at cd093fe, hermetic clone, same method as 0.3: median 24.0 s (runner 15.7 s, 1298 tests) against 33.55 s before. Design D10's outcome: NOT under 15 s, so store-owns-claude-md-managed- block's premise stands (not touched). Leaked temp dirs: 0 / 0 before and after, against 4,043 / 6,879 at 0.3(d). 8.2: from this change's PR run 36089089608, every leg is faster than the Node 18 job in the job and in each bucket step (4m18s / 4m18s / 3m38s against 4m46s). 8.3: on a CI runner (nproc 4, Node 24) per-file is SLOWER — 14.49 s against 11.33 s single-process, median of three interleaved runs each (run 36089977671, throwaway draft PR #221, closed unmerged, branch deleted). Recorded so the trade is known; the decision stands. Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM * docs(openspec): the required task items — call sites, data references, inverses, commit verification, routed findings - 6.1 call-site-sweep-6.1.txt: every `node --test` count site (hook, ci.yml x3, certify.mjs, the Real Numbers recipe, functional/git-shim) forces the reporter + FORCE_COLOR=0 and refuses an unreadable or zero count; every runtime-version reader/writer (process.version read at three context defaults — one more than design D6 counts, per 3.2's own text); every NODE_FLOOR_MAJOR reader; 141 rung files, 0 without the shim; both awaited child waits bounded, the synchronous class named; D8's sweep line-based (27) and multiline (13), every hit classified. - 6.2/6.3 data-refs-and-inverses-6.2-6.3.txt: PM_NODE_FALLBACK <-> NODE_FLOOR_MAJOR and the documented copies, pm-isolation-flag's writer deleted and removal shipped, the renamed requirement's citations (one left: the main spec header the archive rewrites); every inverse named. - 6.4 commit-verification-6.4.txt: 20 commits, every claimed file in its own commit (git show --name-only), 0 missing. - 6.5 verified: 9.2 carries <!-- pm:lifecycle -->. - 6.9 routed: FRICTION filed as #222 (archive gate vs unsynced main specs) and #223 (attribution nudge on throwaway-branch commits); PROCESS lesson a-guard-installed-by-import-covers-only-its-process; PRACTICE decided not a pm epic; the openspec-validate friction left to the maintainer. 6.6/6.7/6.8 stay open: they bind through Gate 2 and the archive. Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM * chore(conductor): record 0.49.0 apply attributions Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM * test(ci-workflow): CI's unreadable-count and floor refusals, their order, and nothing that skips a leg (Gate 2 I1, M3, M6) - I1: reporterRefusals() requires, in every bucket step, the `[ -z "$total" ]` refusal, the `-lt "$declared"` floor and the `-eq 0` refusal — each ending in `exit 1` — in that order. Deleting the -z block used to stay green (`[ "" -lt N ]` errors, `if` reads false, the step exits 0). - M3: actionRefusals() refuses --test-isolation anywhere in ci.yml. - M6: matrixRefusals() refuses `if:` or `continue-on-error:` on the test-node job or any of its steps, and `exclude:`/`include:` under its matrix. mutation-4.3.txt gains (13)-(22), each refused; the pre-fix guard passes (13) 8/8 (control). Also stages the uncommitted attribution of f84fe9d. Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM * test(certify): the runner's argv/env and the null/zero refusal are exercised (Gate 2 I2) certify.mjs exports runnerInvocation(files, env) — the argv (`--test --test-reporter=spec …`) and env (FORCE_COLOR forced "0") the bucket runner is started with — and countRefusal(). A function test, not a conductor-09-style text guard: the property is what the runner is handed and what is refused, which a spelling pin would miss after a refactor. assert/certify-count gains two tests: argv/env (colour forced off even under FORCE_COLOR=1), and countRefusal over null, a missing pass, 0, a TAP summary and a coloured one (refused) and a real count (recorded). RED: red-g2-i2.txt (no exports). mutation-g2-i2.txt: the reporter flag, FORCE_COLOR, the null refusal and the zero refusal each removed — each now fails a test. Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM * test(assert): a trusted harness must itself install the git shim; the shim's own exit removal is checked (Gate 2 I3, M1) - I3: assert-half-has-no-spawn gains fixtureInstallsShim(name, read), a walk through fixture imports (a harness installs when it imports ./assert-git-shim.mjs or another fixture that does — unit-harness through assert-harness), a test that both trusted harnesses do, and a discrimination test (direct, transitive, neither, comment). Removing the shim import and re-export from fixtures/assert-harness.mjs now fails it, naming both harnesses; the pre-fix walk alone passed that mutation (control). - M1: the shim's exit work is exported — drainAndRemove(dir) reads the log then removes the directory, onExit is the registered listener. assert/git-shim asserts onExit is in process.listeners("exit") and drives drainAndRemove on a scratch directory. RED: red-g2-m1.txt. mutation-g2-i3-m1.txt: I3 and both M1 mutations refused. Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM * test(assert): bare and dynamic network specifiers refused; the engine header is a checked support-floor copy (Gate 2 M4, M5) - M5: conductor-35's no-network walk also refuses `from "(node:)?(net| tls|dgram|http2|https?|dns)"` and dynamic `import("…")` of the same, and `dns` joins the set. The reviewer's mutation (`import net from "net"` in runtime-support.mjs) and a dynamic `import("dns")` are both refused; the pre-fix set matched neither (control). - M4: support-floor's FLOOR_COPIES adds ["scripts/conductor.mjs", 1] (a header stating Node 20+ is refused), and the release-checklist's move-together list names the engine header. mutation-g2-m4-m5.txt. Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM * fix(engine): one authoritative runtime-version default; no single-process prose left (Gate 2 M2, M7) - M2: runtimeVersion()'s `ctx.nodeVersion ?? process.version` (lib/invocation.mjs) is the ONE default. runInvocation passes io.nodeVersion through unchanged and PROCESS_CONTEXT drops its getter. New assert/conformance "M2" test pins process.version to that single engine code site (comments stripped). RED: red-g2-m2.txt (three sites); mutation-g2-m2.txt: re-adding runInvocation's default is refused. tasks.md 3.2 records the amendment. - M7: conductor.mjs's resetGitCaches note and exit-handler rationale, and functional/conformance's twin rationale, reworded from "the assertion half's ONE shared process" to an in-process caller / one test file's process under per-file isolation. D3's rg sweep widened to scripts/conductor.mjs; the result is appended to verify-2.5.txt. certify sweeps (25/25) and functional (1189/1189) recorded. Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM * fix(test): spawnAll's bound resolves from its own timer, so a grandchild holding a pipe cannot hang it (Gate 2 M8) spawnAll's timer now SIGKILLs the child AND resolves { timedOut: true } itself; `close` (which waits for every stdio pipe) only settles it when the child finishes first. The assert twin gains an M8 test that the CHILD_BOUND_MS timer kills and resolves timedOut: true — RED against the previous spawnAll. mutation-g2-m8.txt: with the bound shortened to 3 s and a child whose backgrounded grandchild holds stdout, the old helper's test took 20.2 s, the new one fails at 3.05 s. Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM * docs(hook): the pm-isolation-flag cleanup is permanent, not sunset at 0.50.0 Coordinator's decision at Gate 2 (2026-09-24): a contributor clone can skip releases, and a one-line idempotent `rm -f` costs nothing. The hook comment, design D3, task 6.8's candidate list and the 6.2 data-reference record now say so; there is no deferral to register for it at archive. Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM * chore(conductor): record the Gate 2 fix-round attributions Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM * chore(conductor): record 0.49.0 Gate 2 pass; tick 6.6-6.8, 9.1 Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM * chore(openspec): archive node-support-policy; apply its spec deltas to the main specs Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM * chore(release): 0.49.0 Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Throwaway draft PR from a throwaway branch to exercise the node-support-policy CI matrix (task 4.5): the computed set, the fetch-failure fallback and the mismatch path. It will be closed unmerged and the branch deleted.
https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM