Skip to content

DO NOT MERGE — 0.49.0 CI verification (throwaway, tasks 4.5 / 8.3) - #221

Closed
cfdude wants to merge 21 commits into
mainfrom
ci-verify-049
Closed

cfdude wants to merge 21 commits into
mainfrom
ci-verify-049

Conversation

@cfdude

@cfdude cfdude commented Sep 25, 2026

Copy link
Copy Markdown
Owner

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

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
@cfdude

cfdude commented Sep 25, 2026

Copy link
Copy Markdown
Owner Author

Throwaway CI verification for node-support-policy (tasks 4.5 and 8.3) — closed unmerged; branch deleted.

@cfdude cfdude closed this Sep 25, 2026
@cfdude
cfdude deleted the ci-verify-049 branch September 25, 2026 03:22
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant