@codyswann/lisa 4.45.17 → 4.47.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 (91) hide show
  1. package/all/copy-overwrite/scripts/lib/gate-failure-diagnosis.mjs +89 -2
  2. package/all/copy-overwrite/scripts/lisa-gates.mjs +132 -1
  3. package/all/copy-overwrite/scripts/lisa-run-gates.mjs +68 -4
  4. package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
  5. package/dist/core/lisa-owned-hash-ledger.js +10 -0
  6. package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
  7. package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
  8. package/dist/core/nightly-e2e-guard-behavior-certificate.js.map +1 -1
  9. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  10. package/dist/core/upstream-evidence-manifest.js +28 -7
  11. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  12. package/package.json +4 -4
  13. package/plugins/lisa/.claude-plugin/plugin.json +19 -1
  14. package/plugins/lisa/.codex-plugin/hooks.json +18 -0
  15. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  16. package/plugins/lisa/.codex-plugin/skills/lisa-drive-pr-to-merge/SKILL.md +24 -0
  17. package/plugins/lisa/.codex-plugin/skills/lisa-queue-status/SKILL.md +24 -0
  18. package/plugins/lisa/hooks/enforcement-vintage.mjs +453 -0
  19. package/plugins/lisa/hooks/enforcement-vintage.sh +48 -0
  20. package/plugins/lisa/scripts/pr-arming-sweep.mjs +561 -0
  21. package/plugins/lisa/skills/lisa-drive-pr-to-merge/SKILL.md +24 -0
  22. package/plugins/lisa/skills/lisa-queue-status/SKILL.md +24 -0
  23. package/plugins/lisa-agy/plugin.json +1 -1
  24. package/plugins/lisa-agy/scripts/pr-arming-sweep.mjs +561 -0
  25. package/plugins/lisa-agy/skills/lisa-drive-pr-to-merge/SKILL.md +24 -0
  26. package/plugins/lisa-agy/skills/lisa-queue-status/SKILL.md +24 -0
  27. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  28. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  29. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  30. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  31. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  32. package/plugins/lisa-copilot/.claude-plugin/plugin.json +10 -1
  33. package/plugins/lisa-copilot/hooks/enforcement-vintage.mjs +453 -0
  34. package/plugins/lisa-copilot/hooks/enforcement-vintage.sh +48 -0
  35. package/plugins/lisa-copilot/scripts/pr-arming-sweep.mjs +561 -0
  36. package/plugins/lisa-copilot/skills/lisa-drive-pr-to-merge/SKILL.md +24 -0
  37. package/plugins/lisa-copilot/skills/lisa-queue-status/SKILL.md +24 -0
  38. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  39. package/plugins/lisa-cursor/hooks/enforcement-vintage.mjs +453 -0
  40. package/plugins/lisa-cursor/hooks/enforcement-vintage.sh +48 -0
  41. package/plugins/lisa-cursor/hooks/hooks.json +6 -0
  42. package/plugins/lisa-cursor/scripts/pr-arming-sweep.mjs +561 -0
  43. package/plugins/lisa-cursor/skills/lisa-drive-pr-to-merge/SKILL.md +24 -0
  44. package/plugins/lisa-cursor/skills/lisa-queue-status/SKILL.md +24 -0
  45. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  46. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  47. package/plugins/lisa-expo-agy/plugin.json +1 -1
  48. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  49. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  50. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  51. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  52. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  53. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  54. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  55. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  56. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  57. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  58. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  59. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  60. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  61. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  62. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  63. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  64. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  65. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  66. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  67. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  68. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  69. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  70. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  71. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  72. package/plugins/lisa-rails-agy/plugin.json +1 -1
  73. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  74. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  75. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  76. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  77. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  78. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  79. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  80. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  81. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  82. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  83. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  84. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  85. package/plugins/src/base/.claude-plugin/plugin.json +18 -0
  86. package/plugins/src/base/hooks/enforcement-vintage.mjs +453 -0
  87. package/plugins/src/base/hooks/enforcement-vintage.sh +48 -0
  88. package/plugins/src/base/scripts/pr-arming-sweep.mjs +561 -0
  89. package/plugins/src/base/skills/lisa-drive-pr-to-merge/SKILL.md +24 -0
  90. package/plugins/src/base/skills/lisa-queue-status/SKILL.md +24 -0
  91. package/scripts/lib/per-agent-hook-filter.mjs +19 -0
@@ -58,6 +58,8 @@ export const DIAGNOSIS = Object.freeze({
58
58
  COMMENT_TERMINATED: "comment-terminated",
59
59
  /** A type checker ran and reported errors in named files. */
60
60
  TYPE_ERRORS: "type-errors",
61
+ /** The prover printed a failure shape the project declared for its gate. */
62
+ DECLARED_FAILURE: "declared-failure",
61
63
  /** Output was read and matched nothing this module knows. */
62
64
  UNDIAGNOSED: "undiagnosed",
63
65
  /** No output was available to read, so nothing can be said. */
@@ -1107,7 +1109,11 @@ function classify(output, code, load, read, tempRoot) {
1107
1109
 
1108
1110
  return {
1109
1111
  kind: DIAGNOSIS.UNDIAGNOSED,
1110
- summary: "no recognised failure signature; the command's last lines follow",
1112
+ summary:
1113
+ "no recognised failure signature: Lisa could not read this tool's " +
1114
+ "output, so nothing here is a verdict about the code. Re-run this one " +
1115
+ "check on its own — the tool names what the classifier could not. The " +
1116
+ "command's last lines follow",
1111
1117
  evidence: capped(tailLines(output)),
1112
1118
  };
1113
1119
  }
@@ -1381,6 +1387,81 @@ function findTerminatedComments(output, read) {
1381
1387
  return found;
1382
1388
  }
1383
1389
 
1390
+ /**
1391
+ * The verdict for a transcript carrying a failure shape the project declared.
1392
+ *
1393
+ * ## Why the shapes come from OUTSIDE this module
1394
+ *
1395
+ * Everything above this function recognises a fixed set of transcript forms,
1396
+ * and every content signature among them is a vitest or `tsc` form. A prover
1397
+ * that prints anything else falls into the residual bucket wearing
1398
+ * `UNPROVABLE` — the word this fleet reads as *"the box, re-run it somewhere
1399
+ * quieter"* — so a run that genuinely measured a property and found it wanting
1400
+ * is routed into the re-run path.
1401
+ *
1402
+ * The tempting repair is one more entry in the table. It does not converge:
1403
+ * every project points its gates at provers this module has never seen, so
1404
+ * each new one re-introduces the defect once and the cost is paid by whoever
1405
+ * hits it first. Measured on one repository (CodySwannGT/lisa#3974), four
1406
+ * distinct provers hit it — and a Lisa-repo-specific pattern in a module every
1407
+ * consumer installs is the same objection that keeps a Lisa-repo-specific gate
1408
+ * out of the shipped registry.
1409
+ *
1410
+ * So the project that owns the prover says how its output reads as measured,
1411
+ * and this module carries the MECHANISM and none of the vocabulary. Nothing
1412
+ * below names a tool, a script, or an artifact.
1413
+ *
1414
+ * ## Why the shapes are literal substrings and not patterns
1415
+ *
1416
+ * A declaration is untrusted input compiled from configuration, and this
1417
+ * function parses a multi-megabyte transcript inside a git hook. A regular
1418
+ * expression from config would be an operator-authored ReDoS with a
1419
+ * transcript-sized haystack — see `SCRATCH_REMOVAL_RACE` and `PATH_DELIMITER`
1420
+ * for how carefully this module already avoids that shape in patterns it wrote
1421
+ * itself. A literal substring cannot backtrack, needs no escaping, and is what
1422
+ * an operator copying a line out of a failed transcript would write anyway.
1423
+ *
1424
+ * ## Why this is consulted separately rather than folded into `classify`
1425
+ *
1426
+ * The runner calls it ONLY when the shipped classifier has already returned
1427
+ * `undiagnosed`. That placement is the whole safety argument: a killed,
1428
+ * refused, interfered-with or zero-test run never reaches a content signature
1429
+ * at all, so a declaration cannot promote non-measurement into a failure. The
1430
+ * guarantee is structural rather than a matter of where a line was inserted.
1431
+ * @param {string|null|undefined} output The command's combined output.
1432
+ * @param {Array<{gate: string, shape: string[]}>|null|undefined} declarations
1433
+ * What each gate sharing this prover declared its failure looks like.
1434
+ * @returns {Diagnosis|null} The verdict, or null when nothing declared matched.
1435
+ */
1436
+ export function declaredFailure(output, declarations) {
1437
+ if (typeof output !== "string" || output.length === 0) return null;
1438
+ if (!Array.isArray(declarations)) return null;
1439
+
1440
+ const lines = output.split("\n");
1441
+ for (const declaration of declarations) {
1442
+ const shapes = declaration?.shape;
1443
+ if (!Array.isArray(shapes)) continue;
1444
+ const matched = lines.filter(line =>
1445
+ shapes.some(
1446
+ shape =>
1447
+ typeof shape === "string" && shape !== "" && line.includes(shape)
1448
+ )
1449
+ );
1450
+ if (matched.length === 0) continue;
1451
+ const gate = declaration.gate ?? null;
1452
+ return {
1453
+ kind: DIAGNOSIS.DECLARED_FAILURE,
1454
+ summary:
1455
+ `the prover printed a failure shape ${gate ?? "this project"} ` +
1456
+ `declared, so it ran and found this property wanting. That is a ` +
1457
+ `verdict about the code, not about the machine`,
1458
+ evidence: capped(matched.map(line => line.trim())),
1459
+ proves: gate,
1460
+ };
1461
+ }
1462
+ return null;
1463
+ }
1464
+
1384
1465
  /**
1385
1466
  * Classify a failure and say whose property it belongs to.
1386
1467
  *
@@ -1429,5 +1510,11 @@ export function diagnoseFailure(
1429
1510
  tempRoot
1430
1511
  ) {
1431
1512
  const verdict = classify(output, code, load, read, tempRoot);
1432
- return { ...verdict, proves: ATTRIBUTION[verdict.kind] ?? null };
1513
+ // A verdict that already knows whose property it is keeps that answer. The
1514
+ // static table maps a KIND to a gate, which cannot express a failure whose
1515
+ // owner is whichever gate declared the shape that matched.
1516
+ return {
1517
+ ...verdict,
1518
+ proves: verdict.proves ?? ATTRIBUTION[verdict.kind] ?? null,
1519
+ };
1433
1520
  }
@@ -193,6 +193,57 @@ export const STATE_FAMILIES = [CONTINUOUS];
193
193
  /** Keys on a gate entry that are settings rather than moments. */
194
194
  export const GATE_FIELDS = new Set(["run", "needs", "task", "reuse"]);
195
195
 
196
+ /**
197
+ * The per-moment field saying how this gate's prover prints a MEASURED failure.
198
+ *
199
+ * `gate-failure-diagnosis.mjs` recognises a fixed set of transcript forms, and
200
+ * every content signature among them is a vitest or `tsc` form. A prover that
201
+ * prints anything else lands in the residual bucket wearing `UNPROVABLE` — the
202
+ * word this fleet reads as "the box, re-run it" — so a run that measured a
203
+ * property and found it wanting is routed into the re-run path
204
+ * (CodySwannGT/lisa#3974).
205
+ *
206
+ * Enumerating other tools' output in the shipped module cannot converge: every
207
+ * project points its gates at provers Lisa has never seen. So the project that
208
+ * OWNS the prover declares how its failure reads, beside the `run:` that names
209
+ * the prover.
210
+ *
211
+ * ## Why per MOMENT and not per gate
212
+ *
213
+ * The first draft put it on the gate, reasoning that output shape is a
214
+ * property of the prover. Two facts overruled that, and the second is the one
215
+ * with consequences.
216
+ *
217
+ * **A gate's prover already varies by moment.** `code-style` runs `lint:staged`
218
+ * at commit and `lint` at pull-request here; the first prints lint-staged's
219
+ * `✖ <task>` banner and the second does not. A gate-level shape would assert
220
+ * one prover's vocabulary for both.
221
+ *
222
+ * **The gate level is a CLOSED allowlist and the moment level is open.**
223
+ * `GATE_FIELDS` is exhaustive, so an older Lisa refuses any gate-level key it
224
+ * does not know — and refuses the WHOLE gates block with it. Measured against
225
+ * the packaged resolver this repository's own quality facade resolves first:
226
+ *
227
+ * ```
228
+ * Invalid gates configuration:
229
+ * gates."code-style"."failure_shape" is not a moment Lisa knows.
230
+ * ```
231
+ *
232
+ * Every facade then reads `configured=false` and falls through to built-in
233
+ * behaviour, so declaring a shape would silently un-configure every job in the
234
+ * repository until the pin caught up. A moment entry's extra keys are ignored
235
+ * by an older validator instead, which makes the declaration inert there
236
+ * rather than destructive. Inert-until-upgraded is the safe direction; every
237
+ * other per-moment field (`await`, `posted_by`, `evidence`, `caller_chain`)
238
+ * already lives with the same property.
239
+ *
240
+ * The value is a list of literal substrings, not patterns. A regular
241
+ * expression from configuration would be an operator-authored ReDoS run
242
+ * against a multi-megabyte transcript inside a git hook, and a literal is what
243
+ * somebody copying a line out of a failed transcript writes anyway.
244
+ */
245
+ export const FAILURE_SHAPE_FIELD = "failure_shape";
246
+
196
247
  /**
197
248
  * The per-declaration field naming the job chain this gate's prover sits under.
198
249
  *
@@ -4489,6 +4540,22 @@ function validateGate(id, gate) {
4489
4540
  // as "this is not a moment" and sends the author looking for a moment
4490
4541
  // spelling. The mistake is real but different: the field exists, and it
4491
4542
  // belongs one level down.
4543
+ if (moment === FAILURE_SHAPE_FIELD) {
4544
+ // Named before the generic typo message for the reason the caller
4545
+ // chain is: "this is not a moment" sends the author looking for a
4546
+ // moment spelling, and the mistake is a level, not a spelling.
4547
+ problems.push(
4548
+ `gates."${id}"."${FAILURE_SHAPE_FIELD}" is declared for the whole ` +
4549
+ `gate, but a gate's prover can differ by moment — this ` +
4550
+ `repository runs code-style through "lint:staged" at commit and ` +
4551
+ `"lint" at pull-request, and only the first prints lint-staged's ` +
4552
+ `own banner. Declare it inside the moment whose prover prints ` +
4553
+ `it, e.g. gates."${id}"."${COMMIT}".${FAILURE_SHAPE_FIELD}. ` +
4554
+ `A gate-level key is also refused outright by any older Lisa, ` +
4555
+ `which refuses the whole gates block with it.`
4556
+ );
4557
+ continue;
4558
+ }
4492
4559
  if (moment === CALLER_CHAIN_FIELD) {
4493
4560
  problems.push(
4494
4561
  `gates."${id}"."${CALLER_CHAIN_FIELD}" is declared for the whole ` +
@@ -4517,6 +4584,61 @@ function validateGate(id, gate) {
4517
4584
  return problems;
4518
4585
  }
4519
4586
 
4587
+ /**
4588
+ * Validate a gate's declared failure shape.
4589
+ *
4590
+ * Three refusals, and all three are the same defect wearing different clothes:
4591
+ * a declaration that looks configured and governs nothing.
4592
+ *
4593
+ * - **Not an array.** A bare string is the natural typo, and it would iterate
4594
+ * as characters — every transcript containing the letter `c` would match.
4595
+ * - **Empty array.** Reads as "declared" at a glance and matches nothing ever.
4596
+ * - **Empty string.** The declaration that cannot fail: an empty needle is
4597
+ * contained in every line, so every unrecognised failure would report FAILED
4598
+ * against whichever gate declared it. That is this mechanism's own inverse
4599
+ * defect, and shipping it by accident is one keystroke away.
4600
+ * @param {string} id Gate id.
4601
+ * @param {string} moment Moment key.
4602
+ * @param {*} shape The declared value, if any.
4603
+ * @returns {string[]} Problems.
4604
+ */
4605
+ function validateFailureShape(id, moment, shape) {
4606
+ if (shape === undefined) return [];
4607
+ const where = `gates."${id}"."${moment}".${FAILURE_SHAPE_FIELD}`;
4608
+ if (!Array.isArray(shape)) {
4609
+ return [
4610
+ `${where} is ${JSON.stringify(shape)}; expected a list of literal ` +
4611
+ `strings this gate's prover prints when it measures the property and ` +
4612
+ `finds it wanting. A bare string would be read one character at a ` +
4613
+ `time, and every transcript contains most characters.`,
4614
+ ];
4615
+ }
4616
+ if (shape.length === 0) {
4617
+ return [
4618
+ `${where} is empty, so it declares nothing while looking configured. ` +
4619
+ `Remove it, or name the line the prover actually prints.`,
4620
+ ];
4621
+ }
4622
+ const problems = [];
4623
+ for (const [index, entry] of shape.entries()) {
4624
+ if (typeof entry !== "string") {
4625
+ problems.push(
4626
+ `${where}[${index}] is ${JSON.stringify(entry)}; expected a literal ` +
4627
+ `string from the prover's own output.`
4628
+ );
4629
+ continue;
4630
+ }
4631
+ if (entry.trim() === "") {
4632
+ problems.push(
4633
+ `${where}[${index}] is blank. An empty needle is contained in every ` +
4634
+ `line, so this would report a measured failure for any output at ` +
4635
+ `all — the inverse of the defect the field exists to fix.`
4636
+ );
4637
+ }
4638
+ }
4639
+ return problems;
4640
+ }
4641
+
4520
4642
  /**
4521
4643
  * Validate a gate's reuse declaration (#3013).
4522
4644
  *
@@ -4620,6 +4742,9 @@ function validateMoment(id, moment, value, known, interceptor, gateRun) {
4620
4742
  `alongside it, or drop the evidence block — it governs nothing here.`
4621
4743
  );
4622
4744
  }
4745
+ problems.push(
4746
+ ...validateFailureShape(id, moment, entry[FAILURE_SHAPE_FIELD])
4747
+ );
4623
4748
  problems.push(...validatePostedBy(id, moment, entry));
4624
4749
  problems.push(...validateCallerChain(id, moment, entry));
4625
4750
  if (!known && !interceptor && !entry.await && !entry.run && !gateRun) {
@@ -5107,7 +5232,7 @@ export function auditConfigKeys(config) {
5107
5232
  * UNKNOWN, and an unknown manifest resolves exactly as it did before this
5108
5233
  * option existed — a caller that has not been taught to read the manifest
5109
5234
  * must not have its answers changed by silence.
5110
- * @returns {Array<{id: string, level: string, mode: string, awaits: string|null, postedBy: number|null, declared: string|null, task: string|null, command: string|null, label: string, work: string|null, alias: {from: string, to: string}|null, evidence: {proof: string[], no_work: string[], on_hollow: string, wait_minutes: number|null, on_timeout: string}|null}>} Resolved provers, sorted by gate id. `declared` is the project's own `run:` — the PROVENANCE of the command, which `task` alone cannot carry because a project may spell its `run:` exactly as the registry default.
5235
+ * @returns {Array<{id: string, level: string, mode: string, awaits: string|null, postedBy: number|null, declared: string|null, task: string|null, command: string|null, label: string, work: string|null, alias: {from: string, to: string}|null, evidence: {proof: string[], no_work: string[], on_hollow: string, wait_minutes: number|null, on_timeout: string}|null, callerChain: string[]|null, failureShape: string[]|null, mayRewrite: boolean, costly: boolean}>} Resolved provers, sorted by gate id. `declared` is the project's own `run:` — the PROVENANCE of the command, which `task` alone cannot carry because a project may spell its `run:` exactly as the registry default.
5111
5236
  */
5112
5237
  export function resolveMoment({
5113
5238
  gates,
@@ -5174,6 +5299,7 @@ export function resolveMoment({
5174
5299
  alias: null,
5175
5300
  evidence: null,
5176
5301
  callerChain: null,
5302
+ failureShape: entry[FAILURE_SHAPE_FIELD] ?? null,
5177
5303
  mayRewrite: false,
5178
5304
  costly: false,
5179
5305
  });
@@ -5265,6 +5391,11 @@ export function resolveMoment({
5265
5391
  // malformed chain at declaration time; `contextsFor` refuses it again at
5266
5392
  // derivation time.
5267
5393
  callerChain: entry.await ? null : (entry[CALLER_CHAIN_FIELD] ?? null),
5394
+ // Carried on every resolved entry, including an awaited one. It describes
5395
+ // what the project SAID about this moment's prover, and the project said
5396
+ // it whatever mode the moment resolves to — the same argument `declared`
5397
+ // makes one field up.
5398
+ failureShape: entry[FAILURE_SHAPE_FIELD] ?? null,
5268
5399
  mayRewrite: definition?.mayRewrite === true,
5269
5400
  costly: definition?.costly === true,
5270
5401
  });
@@ -54,6 +54,7 @@ import { dirname, join, relative } from "node:path";
54
54
  import { fileURLToPath } from "node:url";
55
55
 
56
56
  import {
57
+ declaredFailure,
57
58
  DIAGNOSIS,
58
59
  diagnoseFailure,
59
60
  ranNoTests,
@@ -423,7 +424,7 @@ function stateFor(kind) {
423
424
  return kind === DIAGNOSIS.KILLED ? STATE.KILLED : STATE.UNPROVABLE;
424
425
  }
425
426
 
426
- function execute(gate, exec) {
427
+ function execute(gate, exec, declarations) {
427
428
  const { code, output } = normaliseExec(exec(gate.command, gate));
428
429
  if (code === 0) {
429
430
  // A zero exit is not by itself a measurement. A runner invoked with
@@ -466,7 +467,11 @@ function execute(gate, exec) {
466
467
  // reads exactly like a real gate failure — which is how one `exit 1` came to
467
468
  // have three distinct causes with re-running a rational response to all of
468
469
  // them (CodySwannGT/lisa#2897).
469
- const diagnosis = diagnoseFailure(output, code);
470
+ const diagnosis = withDeclarations(
471
+ diagnoseFailure(output, code),
472
+ output,
473
+ declarations
474
+ );
470
475
  return {
471
476
  state: stateFor(diagnosis.kind),
472
477
  detail: `${gate.command} (exit ${shown}) — ${diagnosis.summary}`,
@@ -477,6 +482,53 @@ function execute(gate, exec) {
477
482
  };
478
483
  }
479
484
 
485
+ /**
486
+ * Give a shape the project declared its say, but only where nothing else did.
487
+ *
488
+ * The shipped classifier knows a fixed set of transcript forms, and every
489
+ * prover outside that set lands in the residual bucket wearing `UNPROVABLE` —
490
+ * the word this fleet reads as "the box, re-run it". Enumerating other tools'
491
+ * output in a module every consumer installs cannot converge, so a project
492
+ * declares its own prover's measured-failure shape instead
493
+ * (CodySwannGT/lisa#3974).
494
+ *
495
+ * **Consulted ONLY on `undiagnosed`, and that is the entire safety argument.**
496
+ * A killed, refused, interfered-with or zero-test run is classified before any
497
+ * content signature is read, so it can never reach this function — a
498
+ * declaration cannot promote non-measurement into a failure. The guarantee is
499
+ * structural rather than a matter of where a line was inserted, which is what
500
+ * the constraint carried in from CodySwannGT/lisa#3946 asks for.
501
+ *
502
+ * **A declaration that matched nothing is REPORTED rather than dropped.** A
503
+ * mechanism wired to a field nothing reads passes exactly the way the current
504
+ * absence passes: silently. A typo'd shape and no shape at all must not look
505
+ * the same to the operator who wrote one.
506
+ * @param {object} diagnosis What the shipped classifier decided.
507
+ * @param {string|null|undefined} output The command's combined output.
508
+ * @param {Array<{gate: string, shape: string[]}>} declarations What the gates
509
+ * sharing this prover declared.
510
+ * @returns {object} The diagnosis as it should read.
511
+ */
512
+ function withDeclarations(diagnosis, output, declarations) {
513
+ if (diagnosis.kind !== DIAGNOSIS.UNDIAGNOSED) return diagnosis;
514
+ if (!Array.isArray(declarations) || declarations.length === 0) {
515
+ return diagnosis;
516
+ }
517
+ const declared = declaredFailure(output, declarations);
518
+ if (declared) return declared;
519
+ const count = declarations.reduce(
520
+ (total, entry) => total + (entry.shape?.length ?? 0),
521
+ 0
522
+ );
523
+ return {
524
+ ...diagnosis,
525
+ summary:
526
+ `${diagnosis.summary}. ${count} declared failure shape(s) from ` +
527
+ `${declarations.map(entry => entry.gate).join(", ")} were consulted ` +
528
+ `and none matched, so the declaration did not help here`,
529
+ };
530
+ }
531
+
480
532
  /**
481
533
  * Re-attribute one command's failure to the property it actually belongs to.
482
534
  *
@@ -658,7 +710,7 @@ function summarise(result, priorKills = []) {
658
710
  * @param {{proved: Map<string, object>, blockedBy: string|null, exec: Function}} ctx Run state.
659
711
  * @returns {{outcome: object, shared: object|undefined, ran: boolean}} The verdict.
660
712
  */
661
- function verdictFor(gate, { proved, blockedBy, exec, siblings }) {
713
+ function verdictFor(gate, { proved, blockedBy, exec, siblings, declarations }) {
662
714
  const own = classifyStatic(gate);
663
715
  if (own) return { outcome: own, shared: undefined, ran: false };
664
716
 
@@ -704,7 +756,7 @@ function verdictFor(gate, { proved, blockedBy, exec, siblings }) {
704
756
  ran: false,
705
757
  };
706
758
  }
707
- const raw = execute(gate, exec);
759
+ const raw = execute(gate, exec, declarations);
708
760
  return {
709
761
  outcome: attributed(gate, raw, siblings),
710
762
  raw,
@@ -768,12 +820,23 @@ export function runGates({
768
820
  // the failure belongs to — `coverage-adequacy` runs `test:cov` and
769
821
  // `test-correctness` is the one the timeout indicts.
770
822
  const sharers = new Map();
823
+ // Declared failure shapes, keyed the same way and for the same reason: one
824
+ // command runs once, and the shape that explains its output may have been
825
+ // declared by a gate other than the one that happened to run it.
826
+ const declaredShapes = new Map();
771
827
  for (const gate of resolved) {
772
828
  if (!gate.command) continue;
773
829
  sharers.set(
774
830
  gate.command,
775
831
  (sharers.get(gate.command) ?? new Set()).add(gate.id)
776
832
  );
833
+ if (!Array.isArray(gate.failureShape) || gate.failureShape.length === 0) {
834
+ continue;
835
+ }
836
+ declaredShapes.set(gate.command, [
837
+ ...(declaredShapes.get(gate.command) ?? []),
838
+ { gate: gate.id, shape: gate.failureShape },
839
+ ]);
777
840
  }
778
841
 
779
842
  // One command proves every gate that names it, so it runs once.
@@ -796,6 +859,7 @@ export function runGates({
796
859
  blockedBy,
797
860
  exec,
798
861
  siblings: sharers.get(gate.command) ?? new Set(),
862
+ declarations: declaredShapes.get(gate.command) ?? [],
799
863
  });
800
864
 
801
865
  // Only what this iteration actually executed is shareable, and `ran` says
@@ -1 +1 @@
1
- {"version":3,"file":"lisa-owned-hash-ledger.d.ts","sourceRoot":"","sources":["../../src/core/lisa-owned-hash-ledger.ts"],"names":[],"mappings":"AACA,6EAA6E;AAE7E;;;;;;;GAOG;AACH,eAAO,MAAM,sBAAsB,EAAE,QAAQ,CAC3C,MAAM,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC,CAwmCjC,CAAC;AAEH;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,+BAA+B,EAAE,QAAQ,CACpD,MAAM,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC,CA2gCjC,CAAC"}
1
+ {"version":3,"file":"lisa-owned-hash-ledger.d.ts","sourceRoot":"","sources":["../../src/core/lisa-owned-hash-ledger.ts"],"names":[],"mappings":"AACA,6EAA6E;AAE7E;;;;;;;GAOG;AACH,eAAO,MAAM,sBAAsB,EAAE,QAAQ,CAC3C,MAAM,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC,CA6mCjC,CAAC;AAEH;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,+BAA+B,EAAE,QAAQ,CACpD,MAAM,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC,CAghCjC,CAAC"}
@@ -365,9 +365,11 @@ export const LISA_OWNED_HASH_LEDGER = Object.freeze({
365
365
  "4503ddbe7bd54cd553db2b5952972d5319d6b6eb39a258b7c58577b61c7e7ce7",
366
366
  "45bff9913d5a9e75b712a67cd42fe69bd4620cf4d7807f153ea83ab24319f425",
367
367
  "503b51e9239411d07ce6b51dbe2b3079bb12e94bf7c0310e76247af19ff667e6",
368
+ "59cdf9163880189a18e1b9ff6e587048101812f424d219f429189f0978df8a0f",
368
369
  "5d0aee00b95dba43cae499f338358ad4b0286f869c993508c1146448e92bd777",
369
370
  "6742cc12ea14fc99b7bef31ff03df68d5c4610b56fad43107790372e6d777eb1",
370
371
  "6b72a6a71078c838fdb0531e74898a5e36d942cad424b20480e2cd370b5264eb",
372
+ "7f5f40819835d8f7cfc2fd36ce4c377ea9d52cd0d3d69fd8049996e0050e6d38",
371
373
  "7ffcd3f6a0595ae26c697bb0f77044b8a100200985774ad7059ebc3dc2e6bedd",
372
374
  "9bce79d2cdcf709be40fee1bec469796bee2bb06a4cd38b11905677b97c12bc3",
373
375
  "a36a7bd7845878e5f1f991e896739ba4cc468e50f562b71a251ebe193ca151b6",
@@ -537,6 +539,7 @@ export const LISA_OWNED_HASH_LEDGER = Object.freeze({
537
539
  "49f37c7e896df0c43c07827b02bbecc570cccf3c27b2f7741ed53dc139a7824c",
538
540
  "4a1dd584267006e0538590ec183d40f90a87a5eea40bf041027920145f4f39d3",
539
541
  "4b8236c11146725b2f0ae297c1741193bba2808cfc408336e9741dfa4eb424ad",
542
+ "4bbebb7516227532a13136e34f4ea86e7df3d5bb2524924871fdaab82c7562c8",
540
543
  "4e663692d9ae7d4631b6493674aa75e19b74fd35687d4b403a2d57effb877a97",
541
544
  "4fe7003d048086a366b9bea1711275c2186e095f4bf976279d1ff825fd4eca1d",
542
545
  "50cb619a56ed4c81c078ef4b680f781a6e0d89a606d3c68acab129f25e9aa893",
@@ -608,6 +611,7 @@ export const LISA_OWNED_HASH_LEDGER = Object.freeze({
608
611
  "d93cd07b576ab630a29cb381b9e845042684b6003fde290965bf9c2619d57314",
609
612
  "dab7ae5948e26b49eb4caf0ef6be5f3a29b6ed013edef73904a20ba5e06a1b22",
610
613
  "dbca450e5a17133841ab015e71808feb028996f8297fda362dbeca4f07af2352",
614
+ "de1bb66dd26e504a98485e090691df43dd22e961929abfc1bf70a32a4fb84959",
611
615
  "e13d9c5ee11648cebae22d71890c0b81415c72e5e5b977472bffa810dfe52dd4",
612
616
  "e1c5edde3e8ca79f079fe2f350a41c984ecbae152c09db8dde11957c26f9d2ba",
613
617
  "e35a175f8f97f841b2b21de7b6e62bc31eb6de2ba2fa502742325a5e95bf05eb",
@@ -900,6 +904,7 @@ export const LISA_OWNED_HASH_LEDGER = Object.freeze({
900
904
  "23574bf396cadfaa1e8f8c59b70bca4ecebd145e23917a90d79ee2ded74f2b69",
901
905
  "235c541534fc0eca510135ac39b9fbd196965fbeb54d42a09fbf052da1c925cd",
902
906
  "242ac60aa8d77842e3704c5568b426084d9f2643ec9bce2a0601f66497835093",
907
+ "2a961b0ea1a8cbd0311dbb3643b427afdf46157c9372165b1a757ad8efb7062b",
903
908
  "2d166a07c9809800771c7f41e515bff8d66c23aeeae2838dd52d5f2547037422",
904
909
  "305b0567b309eba1176d3ff003fe32e87731497130cddd1281bdbcd0dd08f733",
905
910
  "36f96d5be341004417d005746baeef902b610c5708b75c4a3fbbf84cf05b478c",
@@ -1488,8 +1493,10 @@ export const LISA_OWNED_HASH_HISTORY_DERIVED = Object.freeze({
1488
1493
  "4503ddbe7bd54cd553db2b5952972d5319d6b6eb39a258b7c58577b61c7e7ce7",
1489
1494
  "45bff9913d5a9e75b712a67cd42fe69bd4620cf4d7807f153ea83ab24319f425",
1490
1495
  "503b51e9239411d07ce6b51dbe2b3079bb12e94bf7c0310e76247af19ff667e6",
1496
+ "59cdf9163880189a18e1b9ff6e587048101812f424d219f429189f0978df8a0f",
1491
1497
  "5d0aee00b95dba43cae499f338358ad4b0286f869c993508c1146448e92bd777",
1492
1498
  "6742cc12ea14fc99b7bef31ff03df68d5c4610b56fad43107790372e6d777eb1",
1499
+ "7f5f40819835d8f7cfc2fd36ce4c377ea9d52cd0d3d69fd8049996e0050e6d38",
1493
1500
  "7ffcd3f6a0595ae26c697bb0f77044b8a100200985774ad7059ebc3dc2e6bedd",
1494
1501
  "9bce79d2cdcf709be40fee1bec469796bee2bb06a4cd38b11905677b97c12bc3",
1495
1502
  "a36a7bd7845878e5f1f991e896739ba4cc468e50f562b71a251ebe193ca151b6",
@@ -1640,6 +1647,7 @@ export const LISA_OWNED_HASH_HISTORY_DERIVED = Object.freeze({
1640
1647
  "49f37c7e896df0c43c07827b02bbecc570cccf3c27b2f7741ed53dc139a7824c",
1641
1648
  "4a1dd584267006e0538590ec183d40f90a87a5eea40bf041027920145f4f39d3",
1642
1649
  "4b8236c11146725b2f0ae297c1741193bba2808cfc408336e9741dfa4eb424ad",
1650
+ "4bbebb7516227532a13136e34f4ea86e7df3d5bb2524924871fdaab82c7562c8",
1643
1651
  "4e663692d9ae7d4631b6493674aa75e19b74fd35687d4b403a2d57effb877a97",
1644
1652
  "4fe7003d048086a366b9bea1711275c2186e095f4bf976279d1ff825fd4eca1d",
1645
1653
  "50cb619a56ed4c81c078ef4b680f781a6e0d89a606d3c68acab129f25e9aa893",
@@ -1691,6 +1699,7 @@ export const LISA_OWNED_HASH_HISTORY_DERIVED = Object.freeze({
1691
1699
  "d6ce40929ead45c4df1fa79a607bc17b967a88df00833807d1e3b0b727964b1b",
1692
1700
  "d93cd07b576ab630a29cb381b9e845042684b6003fde290965bf9c2619d57314",
1693
1701
  "dab7ae5948e26b49eb4caf0ef6be5f3a29b6ed013edef73904a20ba5e06a1b22",
1702
+ "de1bb66dd26e504a98485e090691df43dd22e961929abfc1bf70a32a4fb84959",
1694
1703
  "e1c5edde3e8ca79f079fe2f350a41c984ecbae152c09db8dde11957c26f9d2ba",
1695
1704
  "e35a175f8f97f841b2b21de7b6e62bc31eb6de2ba2fa502742325a5e95bf05eb",
1696
1705
  "e3bf3269817b857d20747b2c73c8088d95c281d4cae5e08d47086b7ac0fb23cf",
@@ -1959,6 +1968,7 @@ export const LISA_OWNED_HASH_HISTORY_DERIVED = Object.freeze({
1959
1968
  "23574bf396cadfaa1e8f8c59b70bca4ecebd145e23917a90d79ee2ded74f2b69",
1960
1969
  "235c541534fc0eca510135ac39b9fbd196965fbeb54d42a09fbf052da1c925cd",
1961
1970
  "242ac60aa8d77842e3704c5568b426084d9f2643ec9bce2a0601f66497835093",
1971
+ "2a961b0ea1a8cbd0311dbb3643b427afdf46157c9372165b1a757ad8efb7062b",
1962
1972
  "2d166a07c9809800771c7f41e515bff8d66c23aeeae2838dd52d5f2547037422",
1963
1973
  "305b0567b309eba1176d3ff003fe32e87731497130cddd1281bdbcd0dd08f733",
1964
1974
  "36f96d5be341004417d005746baeef902b610c5708b75c4a3fbbf84cf05b478c",