@codyswann/lisa 4.73.8 → 4.74.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/README.md +52 -3
  2. package/all/copy-overwrite/scripts/lib/github-attestation-recovery.mjs +48 -35
  3. package/all/copy-overwrite/scripts/lib/npm-update-hosted-gate.mjs +15 -1
  4. package/all/copy-overwrite/scripts/lib/npm-update-hosted-hook.mjs +56 -27
  5. package/all/copy-overwrite/scripts/lib/npm-update-invariants.mjs +78 -1
  6. package/all/copy-overwrite/scripts/lib/npm-update-native-process.mjs +29 -10
  7. package/all/copy-overwrite/scripts/lisa-automation-provenance.mjs +86 -37
  8. package/all/copy-overwrite/scripts/npm-updater-helper-graph.json +11 -8
  9. package/all/create-only/.github/workflows/continuous-gates.yml +2 -2
  10. package/all/create-only/.github/workflows/workflow-load-failure-sweep.yml +1 -1
  11. package/cdk/create-only/.github/workflows/ci.yml +2 -2
  12. package/cdk/create-only/.github/workflows/deploy.yml +3 -3
  13. package/cdk/package-lisa/package.lisa.json +1 -1
  14. package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
  15. package/dist/core/lisa-owned-hash-ledger.js +26 -0
  16. package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
  17. package/dist/core/lisa-owned-templates.d.ts +9 -2
  18. package/dist/core/lisa-owned-templates.d.ts.map +1 -1
  19. package/dist/core/lisa-owned-templates.js +13 -3
  20. package/dist/core/lisa-owned-templates.js.map +1 -1
  21. package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
  22. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  23. package/dist/core/upstream-evidence-manifest.js +59 -37
  24. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  25. package/expo/copy-overwrite/jest.setup.pre.js +15 -1
  26. package/expo/create-only/.github/workflows/ci.yml +3 -3
  27. package/expo/create-only/.github/workflows/deploy.yml +4 -4
  28. package/expo/create-only/.github/workflows/nightly-e2e-bypass-reaper.yml +1 -1
  29. package/expo/create-only/.github/workflows/playwright-e2e.yml +1 -1
  30. package/expo/create-only/eas.json +1 -1
  31. package/harper-fabric/copy-overwrite/.github/workflows/ci.yml +1 -1
  32. package/harper-fabric/create-only/.github/workflows/deploy.yml +3 -3
  33. package/harper-fabric/create-only/.github/workflows/zap-baseline.yml +1 -1
  34. package/harper-fabric/package-lisa/package.lisa.json +2 -2
  35. package/nestjs/create-only/.github/workflows/ci.yml +2 -2
  36. package/nestjs/create-only/.github/workflows/deploy.yml +5 -5
  37. package/package.json +6 -6
  38. package/phaser/copy-overwrite/.github/workflows/ci.yml +1 -1
  39. package/phaser/package-lisa/package.lisa.json +2 -2
  40. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  41. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  42. package/plugins/lisa/.codex-plugin/skills/lisa-parity-sentry-sdk-setup/SKILL.md +104 -0
  43. package/plugins/lisa/.codex-plugin/skills/lisa-setup-workstation/scripts/workstation.mjs +3 -1
  44. package/plugins/lisa/skills/lisa-parity-sentry-sdk-setup/SKILL.md +104 -0
  45. package/plugins/lisa/skills/lisa-setup-workstation/scripts/workstation.mjs +3 -1
  46. package/plugins/lisa-agy/plugin.json +1 -1
  47. package/plugins/lisa-agy/skills/lisa-parity-sentry-sdk-setup/SKILL.md +104 -0
  48. package/plugins/lisa-agy/skills/lisa-setup-workstation/scripts/workstation.mjs +3 -1
  49. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  50. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  51. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  52. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  53. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  54. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  55. package/plugins/lisa-copilot/skills/lisa-parity-sentry-sdk-setup/SKILL.md +104 -0
  56. package/plugins/lisa-copilot/skills/lisa-setup-workstation/scripts/workstation.mjs +3 -1
  57. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  58. package/plugins/lisa-cursor/skills/lisa-parity-sentry-sdk-setup/SKILL.md +104 -0
  59. package/plugins/lisa-cursor/skills/lisa-setup-workstation/scripts/workstation.mjs +3 -1
  60. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  61. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  62. package/plugins/lisa-expo-agy/plugin.json +1 -1
  63. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  64. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  65. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  66. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  67. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  68. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  69. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  70. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  71. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  72. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  73. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  74. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  75. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  76. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  77. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  78. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  79. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  80. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  81. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  82. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  83. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  84. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  85. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  86. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  87. package/plugins/lisa-rails-agy/plugin.json +1 -1
  88. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  89. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  90. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  91. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  92. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  93. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  94. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  95. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  96. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  97. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  98. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  99. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  100. package/plugins/src/base/skills/lisa-parity-sentry-sdk-setup/SKILL.md +104 -0
  101. package/plugins/src/base/skills/lisa-setup-workstation/scripts/workstation.mjs +3 -1
  102. package/scripts/check-shell-guard-refusal-coverage.mjs +65 -7
  103. package/scripts/generate-lisa-owned-hash-ledger.mjs +3 -0
  104. package/scripts/lib/shell-guard-shard-runner.mjs +381 -0
  105. package/scripts/lib/shell-guard-shards.mjs +383 -0
  106. package/scripts/lib/shell-guard-terminal-reporter.mjs +89 -0
  107. package/scripts/two-channel-couplings.json +1 -1
  108. package/scripts/update-node-version.ts +52 -6
  109. package/typescript/copy-overwrite/.nvmrc +1 -1
  110. package/typescript/create-only/.github/workflows/ci.yml +1 -1
  111. package/typescript/create-only/.github/workflows/review-evidence.yml +1 -1
  112. package/typescript/create-only/.github/workflows/third-party-review-evidence.yml +1 -1
  113. package/typescript/package-lisa/package.lisa.json +1 -1
@@ -0,0 +1,383 @@
1
+ /** Validate exhaustive native-shard receipts before one original guard judge. */
2
+ import assert from "node:assert/strict";
3
+ import { createHash } from "node:crypto";
4
+ import path from "node:path";
5
+ import { tracerIndex } from "./shell-guard-refusal-coverage.mjs";
6
+
7
+ export const sha256 = bytes => createHash("sha256").update(bytes).digest("hex");
8
+ const encoded = value => JSON.stringify(value);
9
+ const nonnegativeInteger = value => Number.isSafeInteger(value) && value >= 0;
10
+ export const identityHash = value => sha256(encoded(value));
11
+ const hex = (value, count) =>
12
+ typeof value === "string" &&
13
+ new RegExp(`^[a-f0-9]{${count}}$`, "u").test(value);
14
+ const relative = file =>
15
+ typeof file === "string" &&
16
+ file.length > 0 &&
17
+ !file.includes("\\") &&
18
+ !path.posix.isAbsolute(file) &&
19
+ path.posix.normalize(file) === file &&
20
+ !file.split("/").some(part => part === ".." || part === "." || part === "");
21
+
22
+ export function parseShard(value) {
23
+ const match = /^(\d+)\/(\d+)$/u.exec(value ?? "");
24
+ assert(match, "shard must be I/3");
25
+ const index = Number(match[1]),
26
+ total = Number(match[2]);
27
+ assert(
28
+ Number.isSafeInteger(index) && total === 3 && index >= 1 && index <= total,
29
+ "invalid shard index/total"
30
+ );
31
+ return { index, total };
32
+ }
33
+
34
+ export function validateSnapshot(snapshot) {
35
+ assert(hex(snapshot.head, 40) && hex(snapshot.tree, 40));
36
+ assert(
37
+ snapshot.provider &&
38
+ /^\d+$/u.test(snapshot.provider.runId) &&
39
+ /^\d+$/u.test(snapshot.provider.attempt)
40
+ );
41
+ assert(
42
+ hex(snapshot.provider.sourceSha, 40) &&
43
+ snapshot.provider.sourceSha === snapshot.head
44
+ );
45
+ assert(
46
+ hex(snapshot.provider.prHead, 40) &&
47
+ typeof snapshot.provider.workflowRef === "string" &&
48
+ snapshot.provider.workflowRef.length > 0
49
+ );
50
+ assert(Array.isArray(snapshot.candidates) && snapshot.candidates.length > 0);
51
+ const paths = snapshot.candidates.map(row => row.path);
52
+ assert(new Set(paths).size === paths.length && paths.every(relative));
53
+ assert(
54
+ paths.every((file, at) => at === 0 || paths[at - 1].localeCompare(file) < 0)
55
+ );
56
+ assert(snapshot.candidates.every(row => hex(row.sha256, 64)));
57
+ assert(Array.isArray(snapshot.population) && snapshot.population.length > 0);
58
+ assert(
59
+ new Set(snapshot.population.map(row => row.path)).size ===
60
+ snapshot.population.length
61
+ );
62
+ assert(
63
+ snapshot.population.every(
64
+ row =>
65
+ relative(row.path) &&
66
+ hex(row.sha256, 64) &&
67
+ nonnegativeInteger(row.size) &&
68
+ typeof row.canRefuse === "boolean" &&
69
+ typeof row.toolBoundary === "boolean"
70
+ )
71
+ );
72
+ assert(snapshot.candidateHash === identityHash(snapshot.candidates));
73
+ assert(snapshot.populationHash === identityHash(snapshot.population));
74
+ assert(
75
+ Object.keys(snapshot.scriptHashes).length > 0 &&
76
+ Object.entries(snapshot.scriptHashes).every(
77
+ ([file, hash]) => relative(file) && hex(hash, 64)
78
+ )
79
+ );
80
+ assert(snapshot.runtime && Object.keys(snapshot.runtime).length > 0);
81
+ return snapshot;
82
+ }
83
+
84
+ function reportedPath(root, file) {
85
+ assert(
86
+ typeof file === "string" && path.isAbsolute(file),
87
+ "native reporter path must be absolute"
88
+ );
89
+ const rel = path.relative(root, file).split(path.sep).join("/");
90
+ assert(relative(rel), "reported file outside checkout");
91
+ return rel;
92
+ }
93
+
94
+ /** JSON suite counters include nested suites. File identity is testResults[].name. */
95
+ export function completeReportedFiles(root, report, terminal) {
96
+ assert(
97
+ report &&
98
+ report.success === true &&
99
+ report.numFailedTests === 0 &&
100
+ report.numFailedTestSuites === 0,
101
+ "native JSON report unsuccessful/incomplete"
102
+ );
103
+ assert(
104
+ Array.isArray(report.testResults) && report.testResults.length > 0,
105
+ "missing native file report"
106
+ );
107
+ assert(
108
+ terminal &&
109
+ terminal.schema === 1 &&
110
+ terminal.complete === true &&
111
+ terminal.reason === "passed" &&
112
+ terminal.unhandledErrorCount === 0 &&
113
+ Array.isArray(terminal.files),
114
+ "missing/incomplete lifecycle receipt"
115
+ );
116
+ const files = report.testResults.map(file => {
117
+ assert(
118
+ file.status === "passed" && Array.isArray(file.assertionResults),
119
+ "native file failed/malformed"
120
+ );
121
+ assert(
122
+ file.assertionResults.every(
123
+ test =>
124
+ ["passed", "skipped", "todo"].includes(test.status) &&
125
+ Array.isArray(test.failureMessages) &&
126
+ test.failureMessages.length === 0
127
+ ),
128
+ "native assertion failed/pending/malformed"
129
+ );
130
+ return {
131
+ path: reportedPath(root, file.name),
132
+ assertions: file.assertionResults.length,
133
+ };
134
+ });
135
+ assert(
136
+ new Set(files.map(file => file.path)).size === files.length,
137
+ "duplicate native file"
138
+ );
139
+ const lifecycle = terminal.files.map(file => {
140
+ assert(
141
+ file.complete === true &&
142
+ Number.isSafeInteger(file.tests) &&
143
+ file.tests >= 0,
144
+ "unresolved native task"
145
+ );
146
+ assert(
147
+ Array.isArray(file.tasks) &&
148
+ file.tasks.length > 0 &&
149
+ file.tasks.every(terminalTask),
150
+ "unresolved native task state"
151
+ );
152
+ assert(
153
+ file.tasks.filter(task => task.type === "test").length === file.tests
154
+ );
155
+ assert(["passed", "skipped"].includes(file.moduleState));
156
+ assert(
157
+ Array.isArray(file.cases) &&
158
+ file.cases.length === file.tests &&
159
+ file.cases.every(test => ["passed", "skipped"].includes(test.state)),
160
+ "pending/failed native case"
161
+ );
162
+ return { path: reportedPath(root, file.name), assertions: file.tests };
163
+ });
164
+ const sort = rows =>
165
+ rows.slice().sort((a, b) => a.path.localeCompare(b.path));
166
+ assert.deepEqual(
167
+ sort(files),
168
+ sort(lifecycle),
169
+ "JSON/lifecycle file roster mismatch"
170
+ );
171
+ for (const roster of [terminal.queuedFiles, terminal.collectedFiles]) {
172
+ assert(Array.isArray(roster) && new Set(roster).size === roster.length);
173
+ assert.deepEqual(
174
+ roster.map(file => reportedPath(root, file)).sort(),
175
+ lifecycle.map(file => file.path).sort(),
176
+ "native queued/collected/ended roster mismatch"
177
+ );
178
+ }
179
+ assert(
180
+ report.numTotalTests ===
181
+ files.reduce((count, file) => count + file.assertions, 0)
182
+ );
183
+ return files.map(file => file.path).sort((a, b) => a.localeCompare(b));
184
+ }
185
+
186
+ export function terminalTask(task) {
187
+ return (
188
+ task &&
189
+ ["test", "suite"].includes(task.type) &&
190
+ (["pass", "skip"].includes(task.state) ||
191
+ (task.state === "todo" && task.mode === "todo") ||
192
+ (task.state === null && ["skip", "todo"].includes(task.mode)))
193
+ );
194
+ }
195
+
196
+ /** Preserve null status and repeated records, but neither counts as extra proof. */
197
+ export function strictTrace(raw, population) {
198
+ assert(typeof raw === "string");
199
+ const current = new Map(population.map(guard => [guard.path, guard.sha256]));
200
+ for (const line of raw.split("\n")) {
201
+ if (line.trim() === "") continue;
202
+ const row = JSON.parse(line);
203
+ assert(
204
+ row &&
205
+ typeof row === "object" &&
206
+ current.has(row.script) &&
207
+ current.get(row.script) === row.sha256,
208
+ "unknown/stale guard observation"
209
+ );
210
+ assert(
211
+ row.status === null ||
212
+ (Number.isInteger(row.status) && row.status >= 0 && row.status <= 255),
213
+ "invalid child status"
214
+ );
215
+ assert(typeof row.origin === "string", "missing origin");
216
+ }
217
+ return raw;
218
+ }
219
+
220
+ export function nativeArgv(root, artifactRoot, shard, candidates) {
221
+ return [
222
+ "run",
223
+ `--shard=${shard.index}/${shard.total}`,
224
+ "--reporter=dot",
225
+ "--reporter=json",
226
+ `--reporter=${path.join(root, "scripts/lib/shell-guard-terminal-reporter.mjs")}`,
227
+ `--outputFile.json=${path.join(artifactRoot, "results.json")}`,
228
+ ...candidates.map(row => row.path),
229
+ ];
230
+ }
231
+
232
+ export function validateBundle({
233
+ snapshot,
234
+ manifest,
235
+ reportRaw,
236
+ terminalRaw,
237
+ traceRaw,
238
+ indexRaw,
239
+ executionRaw,
240
+ stdoutRaw,
241
+ stderrRaw,
242
+ }) {
243
+ validateSnapshot(snapshot);
244
+ assert(manifest && manifest.schema === 1 && manifest.complete === true);
245
+ assert.deepEqual(
246
+ manifest.snapshot,
247
+ snapshot,
248
+ "stale source/run/attempt/runtime/roster identity"
249
+ );
250
+ parseShard(`${manifest.shard.index}/${manifest.shard.total}`);
251
+ assert(
252
+ manifest.child.status === 0 &&
253
+ manifest.child.signal === null &&
254
+ manifest.child.error === null,
255
+ "child failed/killed/not started"
256
+ );
257
+ assert(manifest.producerJob === "guard-trace", "wrong native producer job");
258
+ assert(
259
+ typeof manifest.artifactRoot === "string" &&
260
+ path.isAbsolute(manifest.artifactRoot)
261
+ );
262
+ assert(
263
+ manifest.child.command ===
264
+ path.join(manifest.checkoutRoot, "node_modules/.bin/vitest"),
265
+ "wrong child executable"
266
+ );
267
+ assert.deepEqual(
268
+ manifest.child.argv,
269
+ nativeArgv(
270
+ manifest.checkoutRoot,
271
+ manifest.artifactRoot,
272
+ manifest.shard,
273
+ snapshot.candidates
274
+ ),
275
+ "incomplete/altered native argv"
276
+ );
277
+ assert(
278
+ Number.isFinite(Date.parse(manifest.child.startedAt)) &&
279
+ Number.isFinite(Date.parse(manifest.child.endedAt)) &&
280
+ Date.parse(manifest.child.endedAt) >=
281
+ Date.parse(manifest.child.startedAt),
282
+ "missing/reversed execution times"
283
+ );
284
+ assert(
285
+ manifest.traceSha256 === sha256(traceRaw) &&
286
+ manifest.reportSha256 === sha256(reportRaw) &&
287
+ manifest.terminalSha256 === sha256(terminalRaw),
288
+ "artifact digest mismatch"
289
+ );
290
+ assert(
291
+ typeof manifest.checkoutRoot === "string" &&
292
+ path.isAbsolute(manifest.checkoutRoot)
293
+ );
294
+ for (const [name, raw] of Object.entries({
295
+ "guard-index.json": indexRaw,
296
+ "execution.json": executionRaw,
297
+ "stdout.log": stdoutRaw,
298
+ "stderr.log": stderrRaw,
299
+ })) {
300
+ assert(
301
+ (typeof raw === "string" || Buffer.isBuffer(raw)) &&
302
+ manifest.rawHashes[name] === sha256(raw),
303
+ `raw receipt digest mismatch: ${name}`
304
+ );
305
+ }
306
+ assert.deepEqual(
307
+ JSON.parse(indexRaw),
308
+ tracerIndex(snapshot.population),
309
+ "narrowed/stale tracer index"
310
+ );
311
+ assert(
312
+ sha256(indexRaw) ===
313
+ sha256(JSON.stringify(tracerIndex(snapshot.population))),
314
+ "canonical tracer index bytes changed"
315
+ );
316
+ assert.deepEqual(
317
+ JSON.parse(executionRaw),
318
+ manifest.child,
319
+ "execution record/manifest mismatch"
320
+ );
321
+ // Report paths belong to that producer's checkout; runners need not share a path.
322
+ const terminal = JSON.parse(terminalRaw);
323
+ const files = completeReportedFiles(
324
+ manifest.checkoutRoot,
325
+ JSON.parse(reportRaw),
326
+ terminal
327
+ );
328
+ assert(
329
+ Array.isArray(terminal.startedFiles) &&
330
+ new Set(terminal.startedFiles).size === terminal.startedFiles.length
331
+ );
332
+ // Vitest4.1.11 starts with ALL filtered specs BEFORE native sharding.
333
+ assert.deepEqual(
334
+ terminal.startedFiles
335
+ .map(file => reportedPath(manifest.checkoutRoot, file))
336
+ .sort((a, b) => a.localeCompare(b)),
337
+ snapshot.candidates.map(row => row.path),
338
+ "complete pre-shard native collection differs from canonical candidates"
339
+ );
340
+ assert.deepEqual(manifest.reportedFiles, files);
341
+ assert(
342
+ files.every(file =>
343
+ snapshot.candidates.some(candidate => candidate.path === file)
344
+ ),
345
+ "native filter collected a noncandidate file"
346
+ );
347
+ assert(manifest.reportedFileCount === files.length && files.length > 0);
348
+ strictTrace(traceRaw, snapshot.population);
349
+ return { files, trace: traceRaw, shard: manifest.shard };
350
+ }
351
+
352
+ export function mergeBundles(snapshot, bundles) {
353
+ validateSnapshot(snapshot);
354
+ assert(
355
+ Array.isArray(bundles) && bundles.length === 3,
356
+ "all three complete shards required"
357
+ );
358
+ const seen = new Set(),
359
+ files = new Set(),
360
+ validated = [];
361
+ for (const bundle of bundles) {
362
+ const value = validateBundle({ ...bundle, snapshot });
363
+ assert(!seen.has(value.shard.index), "duplicate shard identity");
364
+ seen.add(value.shard.index);
365
+ for (const file of value.files) {
366
+ assert(!files.has(file), "duplicate reported file across shards");
367
+ files.add(file);
368
+ }
369
+ validated.push(value);
370
+ }
371
+ assert.deepEqual([...seen].sort(), [1, 2, 3]);
372
+ assert.deepEqual(
373
+ [...files].sort((a, b) => a.localeCompare(b)),
374
+ snapshot.candidates.map(row => row.path),
375
+ "missing/extra candidate files"
376
+ );
377
+ // ONE existing judge receives this trace. Empty individual traces are valid.
378
+ return `${validated
379
+ .sort((a, b) => a.shard.index - b.shard.index)
380
+ .map(value => value.trace.trimEnd())
381
+ .filter(Boolean)
382
+ .join("\n")}\n`;
383
+ }
@@ -0,0 +1,89 @@
1
+ /** Companion to the unchanged native JSON reporter: refuses unresolved tasks. */
2
+ import { writeFileSync } from "node:fs";
3
+ import { terminalTask } from "./shell-guard-shards.mjs";
4
+
5
+ export default class TerminalReporter {
6
+ queuedFiles = [];
7
+ collectedFiles = [];
8
+ onTestModuleQueued(module) {
9
+ this.queuedFiles.push(module.moduleId);
10
+ }
11
+ onTestModuleCollected(module) {
12
+ this.collectedFiles.push(module.moduleId);
13
+ }
14
+ onTestRunStart(specifications) {
15
+ this.startedFiles = specifications.map(
16
+ specification => specification.moduleId
17
+ );
18
+ writeFileSync(
19
+ `${process.env.LISA_GUARD_TERMINAL_REPORT}.started.json`,
20
+ JSON.stringify({
21
+ schema: 1,
22
+ startedFiles: this.startedFiles,
23
+ startedAt: new Date().toISOString(),
24
+ })
25
+ );
26
+ }
27
+ onTestRunEnd(modules, unhandledErrors, reason) {
28
+ const files = modules.map(module => {
29
+ let tests = 0;
30
+ const tasks = [];
31
+ const inspect = task => {
32
+ if (task.type === "test") tests += 1;
33
+ tasks.push({
34
+ type: task.type,
35
+ mode: task.mode,
36
+ state: task.result?.state ?? null,
37
+ });
38
+ for (const child of task.tasks ?? []) inspect(child);
39
+ };
40
+ inspect(module.task);
41
+ const moduleState = module.state();
42
+ const cases = [...module.children.allTests()].map(test => ({
43
+ name: test.fullName,
44
+ mode: test.options.mode,
45
+ state: test.result().state,
46
+ }));
47
+ const complete =
48
+ ["passed", "skipped"].includes(moduleState) &&
49
+ cases.every(test => ["passed", "skipped"].includes(test.state)) &&
50
+ tasks.every(terminalTask);
51
+ return {
52
+ name: module.moduleId,
53
+ moduleState,
54
+ tests,
55
+ complete,
56
+ tasks,
57
+ cases,
58
+ };
59
+ });
60
+ const complete =
61
+ reason === "passed" &&
62
+ files.length > 0 &&
63
+ files.every(file => file.complete) &&
64
+ Array.isArray(unhandledErrors) &&
65
+ unhandledErrors.length === 0 &&
66
+ Array.isArray(this.startedFiles) &&
67
+ JSON.stringify(this.queuedFiles.slice().sort()) ===
68
+ JSON.stringify(files.map(file => file.name).sort()) &&
69
+ JSON.stringify(this.collectedFiles.slice().sort()) ===
70
+ JSON.stringify(files.map(file => file.name).sort());
71
+ // Collector precreates an owned directory; the reporter path is explicit.
72
+ writeFileSync(
73
+ process.env.LISA_GUARD_TERMINAL_REPORT,
74
+ JSON.stringify({
75
+ schema: 1,
76
+ complete,
77
+ reason,
78
+ unhandledErrorCount: Array.isArray(unhandledErrors)
79
+ ? unhandledErrors.length
80
+ : null,
81
+ startedFiles: this.startedFiles,
82
+ queuedFiles: this.queuedFiles,
83
+ collectedFiles: this.collectedFiles,
84
+ files,
85
+ endedAt: new Date().toISOString(),
86
+ })
87
+ );
88
+ }
89
+ }
@@ -103,7 +103,7 @@
103
103
  },
104
104
  "inspected": {
105
105
  "workflows": 26,
106
- "steps": 810,
106
+ "steps": 819,
107
107
  "couplings": 36,
108
108
  "inventory": 178
109
109
  },
@@ -22,6 +22,15 @@ import * as path from "node:path";
22
22
  // Literals named once — each was repeated enough times that a typo in one
23
23
  // copy would diverge silently.
24
24
  const VERSION_REPLACEMENT = "$1{{version}}$2";
25
+ const PRESERVE_RUNTIME_MARKER = "lisa-preserve-node-version:";
26
+
27
+ // These workflows execute a separately qualified, hash-bound updater runtime.
28
+ // Their Node22 identities cannot advance with host defaults without a complete
29
+ // updater requalification (CodySwannGT/lisa#4367).
30
+ const QUALIFIED_UPDATER_WORKFLOWS = new Set([
31
+ ".github/workflows/npm-updater.yml",
32
+ ".github/workflows/npm-updater-runtime-qualification.yml",
33
+ ]);
25
34
 
26
35
  const PROJECT_ROOT = path.resolve(import.meta.dirname, "..");
27
36
 
@@ -74,6 +83,7 @@ function filesToUpdate(): readonly string[] {
74
83
  encoding: "utf8",
75
84
  })
76
85
  .split("\0")
86
+ .filter(file => !QUALIFIED_UPDATER_WORKFLOWS.has(file))
77
87
  .filter(
78
88
  file =>
79
89
  [
@@ -115,6 +125,40 @@ function getMajorVersion(version: string): string {
115
125
  return version.split(".")[0];
116
126
  }
117
127
 
128
+ /**
129
+ * Replace shared defaults while retaining explicitly qualified workflow pins.
130
+ * @param content - Original configuration bytes
131
+ * @param pattern - Runtime expression with two ownership-preserving captures
132
+ * @param replacement - Resolved replacement containing $1 and $2 captures
133
+ * @param workflow - Whether inline workflow qualification markers apply
134
+ * @returns Updated bytes and the number of changed, non-qualified values
135
+ */
136
+ function replaceRuntimeMatches(
137
+ content: string,
138
+ pattern: RegExp,
139
+ replacement: string,
140
+ workflow: boolean
141
+ ): { content: string; changes: number } {
142
+ let changes = 0;
143
+ const updated = content.replace(
144
+ pattern,
145
+ (match: string, first: string, second: string, offset: number) => {
146
+ const lineEnd = content.indexOf("\n", offset);
147
+ const line = content.slice(
148
+ offset,
149
+ lineEnd < 0 ? content.length : lineEnd
150
+ );
151
+ if (workflow && line.includes(PRESERVE_RUNTIME_MARKER)) return match;
152
+ const next = replacement.replace(/\$([12])/g, (_token, group: string) =>
153
+ group === "1" ? first : second
154
+ );
155
+ changes += Number(next !== match);
156
+ return next;
157
+ }
158
+ );
159
+ return { content: updated, changes };
160
+ }
161
+
118
162
  /**
119
163
  * Updates a file with the new Node version
120
164
  */
@@ -150,12 +194,14 @@ function updateFile(
150
194
  .replace("{{version}}", version)
151
195
  .replace("{{major}}", majorVersion);
152
196
 
153
- const matches = content.match(pattern);
154
- if (matches) {
155
- totalChanges += matches.length;
156
- }
157
-
158
- content = content.replace(pattern, resolvedReplacement);
197
+ const result = replaceRuntimeMatches(
198
+ content,
199
+ pattern,
200
+ resolvedReplacement,
201
+ /\.ya?ml$/.test(filePath)
202
+ );
203
+ totalChanges += result.changes;
204
+ content = result.content;
159
205
  });
160
206
 
161
207
  if (content !== originalContent) {
@@ -1 +1 @@
1
- 22.23.3
1
+ 24.21.0
@@ -31,7 +31,7 @@ jobs:
31
31
  # declared a break, and says NOT DETERMINED if this line goes missing
32
32
  # (CodySwannGT/lisa#3698).
33
33
  expected_workflow_contract_major: '1'
34
- node_version: '22.23.3'
34
+ node_version: '24.21.0'
35
35
  package_manager: 'bun'
36
36
  skip_jobs: ''
37
37
  secrets:
@@ -190,7 +190,7 @@ jobs:
190
190
  uses: actions/setup-node@v6
191
191
  with:
192
192
  package-manager-cache: false
193
- node-version: '22.23.3'
193
+ node-version: '24.21.0'
194
194
 
195
195
  - name: 🕵️ Refuse a required check that reviewed nothing
196
196
  id: review_evidence
@@ -90,7 +90,7 @@ jobs:
90
90
  uses: actions/setup-node@v6
91
91
  with:
92
92
  package-manager-cache: false
93
- node-version: ${{ vars.NODE_VERSION || '22.23.3' }}
93
+ node-version: ${{ vars.NODE_VERSION || '24.21.0' }}
94
94
 
95
95
  - name: 🕵️ Prove review evidence, or a posted substitute, at this head
96
96
  env:
@@ -101,7 +101,7 @@
101
101
  "yarn": "please-use-bun",
102
102
  "pnpm": "please-use-bun",
103
103
  "bun": "1.3.8",
104
- "node": "22.23.3"
104
+ "node": "24.21.0"
105
105
  }
106
106
  },
107
107
  "merge": {