@codyswann/lisa 4.57.2 → 4.57.4

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 (66) hide show
  1. package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
  2. package/dist/core/lisa-owned-hash-ledger.js +4 -0
  3. package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
  4. package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
  5. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  6. package/dist/core/upstream-evidence-manifest.js +5 -3
  7. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  8. package/expo/copy-overwrite/scripts/classify-maestro-failures.mjs +66 -14
  9. package/package.json +4 -4
  10. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  11. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  12. package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/scripts/preflight-tools.mjs +44 -19
  13. package/plugins/lisa/skills/lisa-setup-remote-env/scripts/preflight-tools.mjs +44 -19
  14. package/plugins/lisa-agy/plugin.json +1 -1
  15. package/plugins/lisa-agy/skills/lisa-setup-remote-env/scripts/preflight-tools.mjs +44 -19
  16. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  17. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  18. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  19. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  20. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  21. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  22. package/plugins/lisa-copilot/skills/lisa-setup-remote-env/scripts/preflight-tools.mjs +44 -19
  23. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  24. package/plugins/lisa-cursor/skills/lisa-setup-remote-env/scripts/preflight-tools.mjs +44 -19
  25. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  26. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  27. package/plugins/lisa-expo-agy/plugin.json +1 -1
  28. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  29. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  30. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  31. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  32. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  33. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  34. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  35. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  36. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  37. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  38. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  39. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  40. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  41. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  42. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  43. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  44. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  45. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  46. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  47. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  48. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  49. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  50. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  51. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  52. package/plugins/lisa-rails-agy/plugin.json +1 -1
  53. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  54. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  55. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  56. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  57. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  58. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  59. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  60. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  61. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  62. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  63. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  64. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  65. package/plugins/src/base/skills/lisa-setup-remote-env/scripts/preflight-tools.mjs +44 -19
  66. package/scripts/two-channel-couplings.json +7 -5
@@ -626,7 +626,7 @@ function countOccurrences(haystack, needle) {
626
626
 
627
627
  /**
628
628
  * Tally every marker across a run's debug artifacts, per flow.
629
- * @param {readonly {path: string, text: string}[]} artifacts - Debug artifacts
629
+ * @param {Iterable<{path: string, text: string}>} artifacts - Debug artifacts, iterated once
630
630
  * @param {readonly {flow: string, keys: readonly string[]}[]} flowKeys - Flows in the report
631
631
  * @param {readonly string[]} markers - Markers to count
632
632
  * @returns {{perFlow: Map<string, object>, unattributed: object}} Tallies
@@ -723,20 +723,72 @@ export function deviceVerdict(tallies, baselines, markers) {
723
723
  * Absence is normal and silent: a run that crashed before Maestro started, or a
724
724
  * caller that has not wired the flag, simply yields no evidence — and with no
725
725
  * evidence nothing is ever a device fault.
726
+ *
727
+ * LAZY, and that is load-bearing rather than a micro-optimisation. This used to
728
+ * build an array holding every file's text at once. A real iOS debug tree is
729
+ * 3.5 GB across 540 files — `device-simulator.log` alone reaches 235 MB for a
730
+ * single flow — so the array exhausted the V8 heap and the classifier died with
731
+ * `FATAL ERROR: Reached heap limit` inside `fs.readFileSync`. Measured on a
732
+ * 44-flow iOS leg, 2026-09-11.
733
+ *
734
+ * That crash was invisible: the workflow used `|| true` and `exit 0`
735
+ * so the classifier could never fail the job — deliberately, and the
736
+ * suite beside this file pins that — and the docblock above says absence of
737
+ * evidence is normal. So an OOM produces exactly the observable of a run with
738
+ * nothing to classify. On the measured run, the leg's ONE failure was a
739
+ * simulator launch failure — precisely the device fault this classifier exists
740
+ * to name — and it went unnamed because the classifier had died.
741
+ *
742
+ * Yielding one artifact at a time caps peak memory at the largest single file
743
+ * instead of their sum, and each text becomes garbage as soon as
744
+ * {@link tallyDeviceMarkers} has counted it.
745
+ *
746
+ * The return is a RE-ITERABLE iterable, not a one-shot generator: `run()`
747
+ * computes this once and passes it through `classifyRun` for every report, so a
748
+ * generator would be empty for the second report onward — a silent
749
+ * evidence-loss bug in place of a loud crash, which would be worse than what it
750
+ * replaced.
726
751
  * @param {string | null} debugRoot - Absolute path of the debug-output directory
727
752
  * @param {{listFiles: Function, readFile: Function}} io - Injected readers
728
- * @returns {{path: string, text: string}[]} Artifacts
753
+ * @returns {Iterable<{path: string, text: string}>} Artifacts, read on demand
729
754
  */
730
755
  export function readDebugArtifacts(debugRoot, { listFiles, readFile }) {
731
756
  if (!debugRoot) return [];
732
- const artifacts = [];
733
- for (const file of listFiles(debugRoot) ?? []) {
734
- if (isOpaque(file)) continue;
735
- const text = readFile(file);
736
- if (text === null) continue;
737
- artifacts.push({ path: file, text });
738
- }
739
- return artifacts;
757
+ return {
758
+ *[Symbol.iterator]() {
759
+ for (const file of listFiles(debugRoot) ?? []) {
760
+ if (isOpaque(file)) continue;
761
+ const text = readFile(file);
762
+ // `readFile` returns null for absent, unreadable AND too-large-to-
763
+ // string files alike. The last is the one that grows back into the bug
764
+ // above — Node refuses a string past its maximum length, and a
765
+ // device log that crosses it would drop out of the evidence silently.
766
+ if (text === null) {
767
+ reportUnreadableArtifact(file);
768
+ continue;
769
+ }
770
+ yield { path: file, text };
771
+ }
772
+ },
773
+ };
774
+ }
775
+
776
+ /**
777
+ * Say on stderr that an artifact could not be read.
778
+ *
779
+ * Not a throw: a missing or unreadable artifact must not fail the run, for the
780
+ * same reason the step absorbs a non-zero exit. But it must not be silent
781
+ * either — an artifact that drops out of the evidence takes its markers with
782
+ * it, and a device fault it alone witnessed then reads as a flow failure.
783
+ * stderr rather than the markdown, so it reaches the job log without becoming
784
+ * part of the classification that a consumer parses.
785
+ * @param {string} file - Path of the artifact that could not be read
786
+ */
787
+ function reportUnreadableArtifact(file) {
788
+ process.stderr.write(
789
+ `classify-maestro-failures: could not read ${path.basename(file)} — ` +
790
+ `its markers are NOT counted in this classification.\n`
791
+ );
740
792
  }
741
793
 
742
794
  /**
@@ -869,7 +921,7 @@ function resolveFlowPath(reported, { projectRoot, maestroRoot, readFile }) {
869
921
  * @param {readonly string[]} [options.signInMarkers] - Project sign-in markers
870
922
  * @param {readonly unknown[]} [options.knownIntermittent] - Raw registry entries
871
923
  * @param {string} [options.platform] - Arm the report came from
872
- * @param {readonly {path: string, text: string}[]} [options.debugArtifacts] - Run observation
924
+ * @param {Iterable<{path: string, text: string}>} [options.debugArtifacts] - Run observation
873
925
  * @param {readonly string[]} [options.deviceFaultMarkers] - Presence-decided device faults
874
926
  * @param {readonly string[]} [options.deviceInstabilityMarkers] - Count-decided device faults
875
927
  * @returns {{failures: object[], deviceRunEvidence: object[]}} The report's verdict
@@ -1115,9 +1167,9 @@ export function loadConfig(projectRoot) {
1115
1167
  /**
1116
1168
  * Classify every named report against one project checkout.
1117
1169
  *
1118
- * The debug-output tree is read ONCE and shared across the named reports: it is
1119
- * the observation of a single run, and re-walking it per report would only cost
1120
- * time. A retry arm writes into the same tree, which is correct — retries only
1170
+ * The debug-output iterable is shared across the named reports. Each report
1171
+ * reads it lazily so artifact text is not retained between reports.
1172
+ * A retry arm writes into the same tree, which is correct — retries only
1121
1173
  * re-run flows that already failed, so its evidence is about those same flows.
1122
1174
  * @param {readonly string[]} reportPaths - Report paths
1123
1175
  * @param {{projectRoot: string, platform: string | null, debugRoot?: string | null}} context - Run context
package/package.json CHANGED
@@ -187,7 +187,7 @@
187
187
  "zod-validation-error": "^4.0.0"
188
188
  },
189
189
  "name": "@codyswann/lisa",
190
- "version": "4.57.2",
190
+ "version": "4.57.4",
191
191
  "description": "Claude Code governance framework that applies guardrails, guidance, and automated enforcement to projects",
192
192
  "main": "dist/index.js",
193
193
  "exports": {
@@ -336,7 +336,7 @@
336
336
  "test": "tests"
337
337
  },
338
338
  "types": "./dist/index.d.ts",
339
- "lisaReleaseCommit": "0a3ca466098950c41a7e4a9f9edf65ae8831b830",
340
- "gitHead": "0a3ca466098950c41a7e4a9f9edf65ae8831b830",
341
- "lisaReleaseTag": "v4.57.2"
339
+ "lisaReleaseCommit": "f6f450438d19c364ff02140f32058a0b8c27f764",
340
+ "gitHead": "f6f450438d19c364ff02140f32058a0b8c27f764",
341
+ "lisaReleaseTag": "v4.57.4"
342
342
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "4.57.2",
3
+ "version": "4.57.4",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "4.57.2",
3
+ "version": "4.57.4",
4
4
  "description": "Universal governance: agents, skills, commands, hooks, and rules for all projects.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -232,8 +232,22 @@ export function preflightTools(
232
232
  };
233
233
  }
234
234
 
235
+ /**
236
+ * Format a count with the appropriate noun phrase.
237
+ * @param {number} n How many.
238
+ * @param {string} one Singular noun phrase.
239
+ * @param {string} many Plural noun phrase.
240
+ * @returns {string} A counted phrase such as "1 tool" or "3 tools".
241
+ */
242
+ function count(n, one, many) {
243
+ return `${n} ${n === 1 ? one : many}`;
244
+ }
245
+
235
246
  /**
236
247
  * Render a verdict for whoever has to act on it.
248
+ *
249
+ * Put blockers first and scope the stop instruction to that section. The
250
+ * separate installable section must not send usable work to the blocked lane.
237
251
  * @param {object} result A {@link preflightTools} result.
238
252
  * @returns {string} Operator-readable report, empty when nothing needs saying.
239
253
  */
@@ -243,42 +257,53 @@ export function reportTools(result) {
243
257
  // The header tracks the exit code. Only a blocked tool is a failure — one
244
258
  // Lisa can install is an action with a command attached, and calling that
245
259
  // "FAILED" while exiting zero teaches readers that the word means nothing.
260
+ const blocking = count(result.blocked.length, "tool blocks", "tools block");
261
+ const spare = result.installable.length
262
+ ? ` ${result.installable.length} more Lisa can install for you.`
263
+ : ``;
246
264
  const header = () => {
247
- if (result.blocked.length) return "Tooling preflight FAILED.";
265
+ if (result.blocked.length)
266
+ return `Tooling preflight FAILED — ${blocking}.${spare}`;
248
267
  if (result.installable.length)
249
- return "Tooling preflight — action available.";
268
+ return (
269
+ `Tooling preflight — action available. Nothing blocks;` +
270
+ ` ${count(result.installable.length, "tool", "tools")} Lisa can` +
271
+ ` install for you.`
272
+ );
250
273
  // Nothing to do and nothing to fix; the note below is the whole message.
251
- return "Tooling preflight passed, with a note.";
274
+ return `Tooling preflight passed, with a note.`;
252
275
  };
253
276
  const lines = [header()];
254
- if (result.installable.length) {
255
- lines.push(
256
- ``,
257
- `Lisa can install these itself — they are pinned and checksummed.`,
258
- `Run /lisa:setup:local-env to place them:`,
259
- ``
260
- );
261
- for (const step of result.installable) {
262
- lines.push(` ${step.name} — ${step.reason}`);
263
- }
264
- }
265
277
  if (result.blocked.length) {
266
278
  lines.push(
267
279
  ``,
268
- `These need you. Lisa has no pinned artifact it can place:`,
280
+ `STOP — these block the work. Lisa has no pinned artifact it can place,`,
281
+ `so it cannot fix them for you:`,
269
282
  ``
270
283
  );
271
284
  for (const step of result.blocked) {
272
285
  const why = result.reasons[step.name];
273
- lines.push(` ${step.name} — ${step.reason}`);
274
- if (why) lines.push(` required because ${why}`);
286
+ lines.push(` [BLOCKED] ${step.name} — ${step.reason}`);
287
+ if (why) lines.push(` required because ${why}`);
275
288
  }
276
289
  lines.push(
277
290
  ``,
278
- `Work needing one of these cannot be completed. Route the item to`,
279
- `blocked with this reason rather than claiming it and stopping partway.`
291
+ ` Work needing one of the tools above cannot be completed. Route that`,
292
+ ` item to blocked citing that tool, rather than claiming it and`,
293
+ ` stopping partway. Nothing else in this report is a reason to stop.`
280
294
  );
281
295
  }
296
+ if (result.installable.length) {
297
+ lines.push(
298
+ ``,
299
+ `NOT A BLOCKER — Lisa can install these itself; they are pinned and`,
300
+ `checksummed. Run /lisa:setup:local-env to place them:`,
301
+ ``
302
+ );
303
+ for (const step of result.installable) {
304
+ lines.push(` [INSTALLABLE] ${step.name} — ${step.reason}`);
305
+ }
306
+ }
282
307
  if (result.unverified.length) {
283
308
  lines.push(
284
309
  ``,
@@ -232,8 +232,22 @@ export function preflightTools(
232
232
  };
233
233
  }
234
234
 
235
+ /**
236
+ * Format a count with the appropriate noun phrase.
237
+ * @param {number} n How many.
238
+ * @param {string} one Singular noun phrase.
239
+ * @param {string} many Plural noun phrase.
240
+ * @returns {string} A counted phrase such as "1 tool" or "3 tools".
241
+ */
242
+ function count(n, one, many) {
243
+ return `${n} ${n === 1 ? one : many}`;
244
+ }
245
+
235
246
  /**
236
247
  * Render a verdict for whoever has to act on it.
248
+ *
249
+ * Put blockers first and scope the stop instruction to that section. The
250
+ * separate installable section must not send usable work to the blocked lane.
237
251
  * @param {object} result A {@link preflightTools} result.
238
252
  * @returns {string} Operator-readable report, empty when nothing needs saying.
239
253
  */
@@ -243,42 +257,53 @@ export function reportTools(result) {
243
257
  // The header tracks the exit code. Only a blocked tool is a failure — one
244
258
  // Lisa can install is an action with a command attached, and calling that
245
259
  // "FAILED" while exiting zero teaches readers that the word means nothing.
260
+ const blocking = count(result.blocked.length, "tool blocks", "tools block");
261
+ const spare = result.installable.length
262
+ ? ` ${result.installable.length} more Lisa can install for you.`
263
+ : ``;
246
264
  const header = () => {
247
- if (result.blocked.length) return "Tooling preflight FAILED.";
265
+ if (result.blocked.length)
266
+ return `Tooling preflight FAILED — ${blocking}.${spare}`;
248
267
  if (result.installable.length)
249
- return "Tooling preflight — action available.";
268
+ return (
269
+ `Tooling preflight — action available. Nothing blocks;` +
270
+ ` ${count(result.installable.length, "tool", "tools")} Lisa can` +
271
+ ` install for you.`
272
+ );
250
273
  // Nothing to do and nothing to fix; the note below is the whole message.
251
- return "Tooling preflight passed, with a note.";
274
+ return `Tooling preflight passed, with a note.`;
252
275
  };
253
276
  const lines = [header()];
254
- if (result.installable.length) {
255
- lines.push(
256
- ``,
257
- `Lisa can install these itself — they are pinned and checksummed.`,
258
- `Run /lisa:setup:local-env to place them:`,
259
- ``
260
- );
261
- for (const step of result.installable) {
262
- lines.push(` ${step.name} — ${step.reason}`);
263
- }
264
- }
265
277
  if (result.blocked.length) {
266
278
  lines.push(
267
279
  ``,
268
- `These need you. Lisa has no pinned artifact it can place:`,
280
+ `STOP — these block the work. Lisa has no pinned artifact it can place,`,
281
+ `so it cannot fix them for you:`,
269
282
  ``
270
283
  );
271
284
  for (const step of result.blocked) {
272
285
  const why = result.reasons[step.name];
273
- lines.push(` ${step.name} — ${step.reason}`);
274
- if (why) lines.push(` required because ${why}`);
286
+ lines.push(` [BLOCKED] ${step.name} — ${step.reason}`);
287
+ if (why) lines.push(` required because ${why}`);
275
288
  }
276
289
  lines.push(
277
290
  ``,
278
- `Work needing one of these cannot be completed. Route the item to`,
279
- `blocked with this reason rather than claiming it and stopping partway.`
291
+ ` Work needing one of the tools above cannot be completed. Route that`,
292
+ ` item to blocked citing that tool, rather than claiming it and`,
293
+ ` stopping partway. Nothing else in this report is a reason to stop.`
280
294
  );
281
295
  }
296
+ if (result.installable.length) {
297
+ lines.push(
298
+ ``,
299
+ `NOT A BLOCKER — Lisa can install these itself; they are pinned and`,
300
+ `checksummed. Run /lisa:setup:local-env to place them:`,
301
+ ``
302
+ );
303
+ for (const step of result.installable) {
304
+ lines.push(` [INSTALLABLE] ${step.name} — ${step.reason}`);
305
+ }
306
+ }
282
307
  if (result.unverified.length) {
283
308
  lines.push(
284
309
  ``,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "4.57.2",
3
+ "version": "4.57.4",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -232,8 +232,22 @@ export function preflightTools(
232
232
  };
233
233
  }
234
234
 
235
+ /**
236
+ * Format a count with the appropriate noun phrase.
237
+ * @param {number} n How many.
238
+ * @param {string} one Singular noun phrase.
239
+ * @param {string} many Plural noun phrase.
240
+ * @returns {string} A counted phrase such as "1 tool" or "3 tools".
241
+ */
242
+ function count(n, one, many) {
243
+ return `${n} ${n === 1 ? one : many}`;
244
+ }
245
+
235
246
  /**
236
247
  * Render a verdict for whoever has to act on it.
248
+ *
249
+ * Put blockers first and scope the stop instruction to that section. The
250
+ * separate installable section must not send usable work to the blocked lane.
237
251
  * @param {object} result A {@link preflightTools} result.
238
252
  * @returns {string} Operator-readable report, empty when nothing needs saying.
239
253
  */
@@ -243,42 +257,53 @@ export function reportTools(result) {
243
257
  // The header tracks the exit code. Only a blocked tool is a failure — one
244
258
  // Lisa can install is an action with a command attached, and calling that
245
259
  // "FAILED" while exiting zero teaches readers that the word means nothing.
260
+ const blocking = count(result.blocked.length, "tool blocks", "tools block");
261
+ const spare = result.installable.length
262
+ ? ` ${result.installable.length} more Lisa can install for you.`
263
+ : ``;
246
264
  const header = () => {
247
- if (result.blocked.length) return "Tooling preflight FAILED.";
265
+ if (result.blocked.length)
266
+ return `Tooling preflight FAILED — ${blocking}.${spare}`;
248
267
  if (result.installable.length)
249
- return "Tooling preflight — action available.";
268
+ return (
269
+ `Tooling preflight — action available. Nothing blocks;` +
270
+ ` ${count(result.installable.length, "tool", "tools")} Lisa can` +
271
+ ` install for you.`
272
+ );
250
273
  // Nothing to do and nothing to fix; the note below is the whole message.
251
- return "Tooling preflight passed, with a note.";
274
+ return `Tooling preflight passed, with a note.`;
252
275
  };
253
276
  const lines = [header()];
254
- if (result.installable.length) {
255
- lines.push(
256
- ``,
257
- `Lisa can install these itself — they are pinned and checksummed.`,
258
- `Run /lisa:setup:local-env to place them:`,
259
- ``
260
- );
261
- for (const step of result.installable) {
262
- lines.push(` ${step.name} — ${step.reason}`);
263
- }
264
- }
265
277
  if (result.blocked.length) {
266
278
  lines.push(
267
279
  ``,
268
- `These need you. Lisa has no pinned artifact it can place:`,
280
+ `STOP — these block the work. Lisa has no pinned artifact it can place,`,
281
+ `so it cannot fix them for you:`,
269
282
  ``
270
283
  );
271
284
  for (const step of result.blocked) {
272
285
  const why = result.reasons[step.name];
273
- lines.push(` ${step.name} — ${step.reason}`);
274
- if (why) lines.push(` required because ${why}`);
286
+ lines.push(` [BLOCKED] ${step.name} — ${step.reason}`);
287
+ if (why) lines.push(` required because ${why}`);
275
288
  }
276
289
  lines.push(
277
290
  ``,
278
- `Work needing one of these cannot be completed. Route the item to`,
279
- `blocked with this reason rather than claiming it and stopping partway.`
291
+ ` Work needing one of the tools above cannot be completed. Route that`,
292
+ ` item to blocked citing that tool, rather than claiming it and`,
293
+ ` stopping partway. Nothing else in this report is a reason to stop.`
280
294
  );
281
295
  }
296
+ if (result.installable.length) {
297
+ lines.push(
298
+ ``,
299
+ `NOT A BLOCKER — Lisa can install these itself; they are pinned and`,
300
+ `checksummed. Run /lisa:setup:local-env to place them:`,
301
+ ``
302
+ );
303
+ for (const step of result.installable) {
304
+ lines.push(` [INSTALLABLE] ${step.name} — ${step.reason}`);
305
+ }
306
+ }
282
307
  if (result.unverified.length) {
283
308
  lines.push(
284
309
  ``,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "4.57.2",
3
+ "version": "4.57.4",
4
4
  "description": "AWS CDK-specific plugin",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "4.57.2",
3
+ "version": "4.57.4",
4
4
  "description": "AWS CDK-specific Lisa plugin.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "4.57.2",
3
+ "version": "4.57.4",
4
4
  "description": "AWS CDK-specific plugin",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "4.57.2",
3
+ "version": "4.57.4",
4
4
  "description": "AWS CDK-specific plugin",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "4.57.2",
3
+ "version": "4.57.4",
4
4
  "description": "AWS CDK-specific plugin",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "4.57.2",
3
+ "version": "4.57.4",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -232,8 +232,22 @@ export function preflightTools(
232
232
  };
233
233
  }
234
234
 
235
+ /**
236
+ * Format a count with the appropriate noun phrase.
237
+ * @param {number} n How many.
238
+ * @param {string} one Singular noun phrase.
239
+ * @param {string} many Plural noun phrase.
240
+ * @returns {string} A counted phrase such as "1 tool" or "3 tools".
241
+ */
242
+ function count(n, one, many) {
243
+ return `${n} ${n === 1 ? one : many}`;
244
+ }
245
+
235
246
  /**
236
247
  * Render a verdict for whoever has to act on it.
248
+ *
249
+ * Put blockers first and scope the stop instruction to that section. The
250
+ * separate installable section must not send usable work to the blocked lane.
237
251
  * @param {object} result A {@link preflightTools} result.
238
252
  * @returns {string} Operator-readable report, empty when nothing needs saying.
239
253
  */
@@ -243,42 +257,53 @@ export function reportTools(result) {
243
257
  // The header tracks the exit code. Only a blocked tool is a failure — one
244
258
  // Lisa can install is an action with a command attached, and calling that
245
259
  // "FAILED" while exiting zero teaches readers that the word means nothing.
260
+ const blocking = count(result.blocked.length, "tool blocks", "tools block");
261
+ const spare = result.installable.length
262
+ ? ` ${result.installable.length} more Lisa can install for you.`
263
+ : ``;
246
264
  const header = () => {
247
- if (result.blocked.length) return "Tooling preflight FAILED.";
265
+ if (result.blocked.length)
266
+ return `Tooling preflight FAILED — ${blocking}.${spare}`;
248
267
  if (result.installable.length)
249
- return "Tooling preflight — action available.";
268
+ return (
269
+ `Tooling preflight — action available. Nothing blocks;` +
270
+ ` ${count(result.installable.length, "tool", "tools")} Lisa can` +
271
+ ` install for you.`
272
+ );
250
273
  // Nothing to do and nothing to fix; the note below is the whole message.
251
- return "Tooling preflight passed, with a note.";
274
+ return `Tooling preflight passed, with a note.`;
252
275
  };
253
276
  const lines = [header()];
254
- if (result.installable.length) {
255
- lines.push(
256
- ``,
257
- `Lisa can install these itself — they are pinned and checksummed.`,
258
- `Run /lisa:setup:local-env to place them:`,
259
- ``
260
- );
261
- for (const step of result.installable) {
262
- lines.push(` ${step.name} — ${step.reason}`);
263
- }
264
- }
265
277
  if (result.blocked.length) {
266
278
  lines.push(
267
279
  ``,
268
- `These need you. Lisa has no pinned artifact it can place:`,
280
+ `STOP — these block the work. Lisa has no pinned artifact it can place,`,
281
+ `so it cannot fix them for you:`,
269
282
  ``
270
283
  );
271
284
  for (const step of result.blocked) {
272
285
  const why = result.reasons[step.name];
273
- lines.push(` ${step.name} — ${step.reason}`);
274
- if (why) lines.push(` required because ${why}`);
286
+ lines.push(` [BLOCKED] ${step.name} — ${step.reason}`);
287
+ if (why) lines.push(` required because ${why}`);
275
288
  }
276
289
  lines.push(
277
290
  ``,
278
- `Work needing one of these cannot be completed. Route the item to`,
279
- `blocked with this reason rather than claiming it and stopping partway.`
291
+ ` Work needing one of the tools above cannot be completed. Route that`,
292
+ ` item to blocked citing that tool, rather than claiming it and`,
293
+ ` stopping partway. Nothing else in this report is a reason to stop.`
280
294
  );
281
295
  }
296
+ if (result.installable.length) {
297
+ lines.push(
298
+ ``,
299
+ `NOT A BLOCKER — Lisa can install these itself; they are pinned and`,
300
+ `checksummed. Run /lisa:setup:local-env to place them:`,
301
+ ``
302
+ );
303
+ for (const step of result.installable) {
304
+ lines.push(` [INSTALLABLE] ${step.name} — ${step.reason}`);
305
+ }
306
+ }
282
307
  if (result.unverified.length) {
283
308
  lines.push(
284
309
  ``,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "4.57.2",
3
+ "version": "4.57.4",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"