@codyswann/lisa 4.45.1 → 4.45.3

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 (70) hide show
  1. package/all/copy-overwrite/scripts/lisa-enforcement-fallback.sh +49 -0
  2. package/all/copy-overwrite/scripts/lisa-hooks/parity-safety-net.sh +115 -12
  3. package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
  4. package/dist/core/lisa-owned-hash-ledger.js +6 -0
  5. package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
  6. package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
  7. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  8. package/dist/core/upstream-evidence-manifest.js +11 -6
  9. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  10. package/dist/opencode/plugin-templates/parity-safety-net.sh +115 -12
  11. package/package.json +4 -4
  12. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  13. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  14. package/plugins/lisa/hooks/parity-safety-net.sh +115 -12
  15. package/plugins/lisa-agy/hooks/parity-safety-net.sh +115 -12
  16. package/plugins/lisa-agy/plugin.json +1 -1
  17. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  18. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  19. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  20. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  21. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  22. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  23. package/plugins/lisa-copilot/hooks/parity-safety-net.sh +115 -12
  24. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  25. package/plugins/lisa-cursor/hooks/parity-safety-net.sh +115 -12
  26. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  27. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  28. package/plugins/lisa-expo-agy/plugin.json +1 -1
  29. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  30. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  31. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  32. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  33. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  34. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  35. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  36. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  37. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  38. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  39. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  40. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  41. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  42. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  43. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  44. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  45. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  46. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  47. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  48. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  49. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  50. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  51. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  52. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  53. package/plugins/lisa-rails-agy/plugin.json +1 -1
  54. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  55. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  56. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  57. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  58. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  59. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  60. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  61. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  62. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  63. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  64. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  65. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  66. package/plugins/src/base/hooks/parity-safety-net.sh +115 -12
  67. package/scripts/check-typecheck-tests.mjs +229 -72
  68. package/scripts/lib/dependency-tree.mjs +136 -0
  69. package/scripts/lisa-enforcement-fallback.sh +49 -0
  70. package/typescript/copy-contents/.husky/pre-push +45 -0
@@ -0,0 +1,136 @@
1
+ /**
2
+ * dependency-tree — decide whether a gate's dependency tree is resolvable, and
3
+ * say "cannot measure" in one voice when it is not (CodySwannGT/lisa#3913).
4
+ *
5
+ * @remarks
6
+ * ## The defect this exists for
7
+ *
8
+ * The `type-correctness` gate spawns the compiler by PATH:
9
+ * `node node_modules/typescript/bin/tsc`. With an empty `node_modules` the
10
+ * SPAWN still succeeds — node starts, and it is node that then cannot find the
11
+ * module — so the gate's `if (tsc.error)` guard does not fire. node's
12
+ * "Cannot find module" goes to stderr and does not match the gate's
13
+ * `file(line,col): error TSxxxx:` pattern, so the parsed-diagnostics map comes
14
+ * back EMPTY. Every quarantined file then satisfies "has no errors", and the
15
+ * gate reports:
16
+ *
17
+ * ```
18
+ * ❌ 370 quarantined file(s) now type-check and must leave the list:
19
+ * Remove them from typecheck-quarantine.json.
20
+ * ```
21
+ *
22
+ * Following that deletes 370 live quarantine entries on the word of a checker
23
+ * that compiled nothing. The comparison was not wrong — it was correct about an
24
+ * input it was never entitled to accept.
25
+ *
26
+ * ## Why "cannot measure" is a THIRD outcome, not a failure message
27
+ *
28
+ * This is the #3888 family: a control that could not establish its subject must
29
+ * not render identically to one that established it and found nothing. The
30
+ * sibling exemplar is the review gate's `UNDETERMINED` verdict in
31
+ * `check-skipped-required-checks.mjs`, whose own comment states the rule this
32
+ * module follows — resolving an unestablished subject to `success` "would be a
33
+ * new instance of exactly that".
34
+ *
35
+ * A gate whose only surface is an exit code cannot emit a third code that
36
+ * anything reads, so the fallback is fixed: **fail closed, with the denial as
37
+ * the first clause.** Never pass. A reader who skims one line must come away
38
+ * knowing the finding was NOT made, because the natural inference from silence
39
+ * about errors is that there were none.
40
+ *
41
+ * ## The message contract
42
+ *
43
+ * {@link cannotMeasure} takes the clauses rather than a finished string so that
44
+ * no call site can ship three of the four. Every message names:
45
+ *
46
+ * 1. the outcome, in a distinct word (`CANNOT MEASURE`);
47
+ * 2. the inference a reader would otherwise draw, DENIED in words;
48
+ * 3. why the measurement could not happen;
49
+ * 4. what to do — including, where one exists, the destructive remedy that
50
+ * must NOT be followed.
51
+ *
52
+ * Clause 2 is the one that is easy to drop and the only one that stops a
53
+ * destructive edit, so it is a required parameter.
54
+ *
55
+ * @module scripts/lib/dependency-tree
56
+ */
57
+ import { existsSync } from "node:fs";
58
+ import path from "node:path";
59
+
60
+ /** The install command that repairs an unresolvable tree. */
61
+ export const INSTALL_COMMAND = "bun install --frozen-lockfile";
62
+
63
+ /**
64
+ * The distinct outcome word. Deliberately not "ERROR" or "FAILED": those are
65
+ * what the gate prints when it DID measure and found something, which is the
66
+ * confusion this whole module exists to prevent.
67
+ */
68
+ export const CANNOT_MEASURE = "CANNOT MEASURE";
69
+
70
+ /**
71
+ * Names the dependencies a gate needs that are not present under `root`.
72
+ *
73
+ * Presence is tested at the exact path the gate will spawn or import, not by
74
+ * asking whether `node_modules` exists. A partially installed tree — the case
75
+ * that produced the `knip` sibling of this defect, where `node_modules` was
76
+ * present but two binaries were not — has a `node_modules` directory and still
77
+ * cannot answer the question.
78
+ *
79
+ * @param {string} root - Directory the gate resolves dependencies against.
80
+ * @param {ReadonlyArray<string>} relativePaths - Paths under `root` to require,
81
+ * each already spelled the way the gate will use it.
82
+ * @returns {string[]} The missing paths, in the order given.
83
+ */
84
+ export function missingDependencies(root, relativePaths) {
85
+ return relativePaths.filter(
86
+ relative => !existsSync(path.join(root, relative))
87
+ );
88
+ }
89
+
90
+ /**
91
+ * True when a child process signalled failure without producing anything the
92
+ * caller could read as a finding.
93
+ *
94
+ * This is the general form of the defect, and it catches more than an empty
95
+ * `node_modules`: a broken `tsconfig`, an out-of-memory compiler, a crash. The
96
+ * three cases are distinguishable in principle and identical in what they
97
+ * license — nothing.
98
+ *
99
+ * The one ambiguous input resolves the safe way. If a compiler ever emitted
100
+ * real diagnostics in a format the caller's pattern did not match, this reports
101
+ * "could not measure" rather than "no errors" — and "I could not read the
102
+ * output" is the honest account of that too.
103
+ *
104
+ * A zero exit with no diagnostics is NOT this: that is a clean run, and the
105
+ * caller is entitled to report it.
106
+ *
107
+ * @param {number | null} status - The child's exit status.
108
+ * @param {number} findingCount - How many findings the caller parsed out.
109
+ * @returns {boolean} Whether the run was vacuous.
110
+ */
111
+ export function ranVacuously(status, findingCount) {
112
+ return status !== 0 && findingCount === 0;
113
+ }
114
+
115
+ /**
116
+ * Builds the operator-readable `CANNOT MEASURE` block.
117
+ *
118
+ * Returned rather than printed so callers can route it to stderr, a report, or
119
+ * a test assertion without this module deciding.
120
+ *
121
+ * @param {object} clauses - The four required clauses.
122
+ * @param {string} clauses.gate - The gate's own name, as it prints elsewhere.
123
+ * @param {string} clauses.denial - What this is NOT reporting, phrased as the
124
+ * inference a reader would otherwise draw. Required: it is the clause that
125
+ * stops a destructive edit.
126
+ * @param {string} clauses.because - Why the measurement could not happen.
127
+ * @param {string} clauses.remedy - What the reader should do instead.
128
+ * @returns {string} The block, newline-separated, no trailing newline.
129
+ */
130
+ export function cannotMeasure({ gate, denial, because, remedy }) {
131
+ return [
132
+ `${CANNOT_MEASURE} — ${gate} did not run, and is NOT reporting ${denial}.`,
133
+ `Why: ${because}`,
134
+ `Do this: ${remedy}`,
135
+ ].join("\n");
136
+ }
@@ -278,6 +278,12 @@ resolve_vintages() {
278
278
  # Description of the last describe_vintage call.
279
279
  vintage_label=""
280
280
 
281
+ # Whether that copy could NOT be shown current — stale, or undateable. Kept as a
282
+ # flag rather than re-read out of the label, because a refusal has to branch on
283
+ # it and matching on the word "STALE" inside prose is the kind of coupling that
284
+ # breaks the first time the wording is improved.
285
+ vintage_is_stale=0
286
+
281
287
  # A one-line description of a copy's age, used by both the notice and the
282
288
  # attribution line.
283
289
  #
@@ -288,10 +294,13 @@ vintage_label=""
288
294
  describe_vintage() {
289
295
  if [ -z "$1" ]; then
290
296
  vintage_label="vintage unknown"
297
+ vintage_is_stale=1
291
298
  elif [ -n "$newest_version" ] && version_older "$1" "$newest_version"; then
292
299
  vintage_label="lisa $1, STALE — $newest_version is on this machine"
300
+ vintage_is_stale=1
293
301
  else
294
302
  vintage_label="lisa $1"
303
+ vintage_is_stale=0
295
304
  fi
296
305
  }
297
306
 
@@ -665,6 +674,46 @@ while [ "$index" -lt "$guard_count" ]; do
665
674
  printf 'Guard %s exited %s — non-blocking error from %s (%s)\n' \
666
675
  "${guard_names[$index]}" "$guard_status" "$script" "$vintage_label" >&2
667
676
  fi
677
+ # A verdict from a copy that cannot be shown current states its own limit,
678
+ # here, attached to the verdict rather than to the session.
679
+ #
680
+ # WHY THIS IS NOT THE VINTAGE SUFFIX AGAIN. The suffix names a version; it
681
+ # does not say what follows from it, and a version number is not an
682
+ # instruction. The session-start notice DOES say what follows — and it is
683
+ # rate-limited per session, so a long-running session receives it once, at
684
+ # the beginning. One session measured 2026-09-04 had its notice written
685
+ # 34 hours before the refusals it existed to explain, and its vintage was
686
+ # computed at start, when there was nothing yet to report: a session that
687
+ # begins current and goes stale while running is told nothing, ever
688
+ # (CodySwannGT/lisa#3942).
689
+ #
690
+ # So the failure was never that the warning was ignored. It was delivered
691
+ # to a session, once, and the thing it warns about happens to a COMMAND.
692
+ # This block is keyed to the event instead: every refusal a
693
+ # not-provably-current copy emits carries what the reader has to know to
694
+ # avoid acting on it.
695
+ #
696
+ # The last line is the load-bearing one and is the reason this is not
697
+ # advisory prose. A refusal from a stale copy is indistinguishable from a
698
+ # refusal from current source, so an agent that re-runs it to check gets a
699
+ # SECOND confirmation of the same wrong thing — the observation is fresh
700
+ # and its subject is not. Two tickets were filed on 2026-09-04 against
701
+ # behaviour fixed the previous day, one of them re-verified live
702
+ # specifically to avoid citing a stale observation.
703
+ if [ "$vintage_is_stale" -eq 1 ]; then
704
+ {
705
+ printf '\nTHIS VERDICT MAY NOT REFLECT CURRENT SOURCE — the copy that produced it\n'
706
+ printf 'is not provably current (%s).\n' "$vintage_label"
707
+ if [ "${guard_trees[$index]}" = "host" ]; then
708
+ printf ' repair: %s\n' "$HOST_REPAIR"
709
+ else
710
+ printf ' repair: %s\n' "$PLUGIN_REPAIR"
711
+ fi
712
+ printf 'Before filing a defect on this behaviour, read the guard on your\n'
713
+ printf 'integration branch. Re-running the command confirms nothing: it asks\n'
714
+ printf 'the same stale copy again.\n'
715
+ } >&2
716
+ fi
668
717
  fi
669
718
  # 2 is the ONLY status Claude Code treats as a refusal. Every other non-zero
670
719
  # is a non-blocking error: it is surfaced, and the tool call proceeds anyway.
@@ -126,6 +126,51 @@ if ! command -v jq >/dev/null 2>&1; then
126
126
  exit 1
127
127
  fi
128
128
 
129
+ # The DEPENDENCY TREE is proved here too, for the same reason and one worse
130
+ # consequence (CodySwannGT/lisa#3913).
131
+ #
132
+ # Every gate below resolves its tool out of `node_modules` — knip, vitest, tsc.
133
+ # An empty tree does not stop those gates; it makes them answer from nothing,
134
+ # and an empty answer is not silent. Upstream, the `type-correctness` gate is
135
+ # what showed this belongs here rather than inside each gate: with no compiler
136
+ # to run it parsed zero diagnostics, concluded from that emptiness that every
137
+ # quarantined file now compiles, and instructed a reader to delete the
138
+ # quarantine entries protecting them — a destructive edit ordered by a control
139
+ # whose inputs never resolved. Its sibling `knip:check` reports unlisted
140
+ # binaries from the identical cause. That one merely LOOKS odd and nobody acts
141
+ # on it, which is exactly why it went unfixed while the dangerous one hid
142
+ # behind it.
143
+ #
144
+ # So this is not a convenience check for a missing install. Absence of the
145
+ # dependencies and absence of findings must not produce the same observable. A
146
+ # check that cannot RUN is not a skip and not a pass — it blocks.
147
+ if [ -f package.json ]; then
148
+ LISA_DECLARED_DEPS="$(jq -r '((.dependencies // {}) + (.devDependencies // {})) | length' package.json 2>/dev/null)"
149
+ # A non-numeric or absent answer counts as zero: this guard may not be the
150
+ # thing that blocks a push over an unreadable package.json.
151
+ case "$LISA_DECLARED_DEPS" in
152
+ "" | *[!0-9]*) LISA_DECLARED_DEPS=0 ;;
153
+ esac
154
+ # `ls -A` covers absent and present-but-empty in one test, which is the
155
+ # distinction the gates themselves cannot make.
156
+ if [ "$LISA_DECLARED_DEPS" -gt 0 ] && [ -z "$(ls -A node_modules 2>/dev/null)" ]; then
157
+ echo "❌ Push blocked: CANNOT MEASURE — dependencies are not installed."
158
+ echo ""
159
+ echo " package.json declares $LISA_DECLARED_DEPS dependencies and node_modules is"
160
+ echo " absent or empty, so the gates below cannot resolve the tools they"
161
+ echo " run: tsc, knip and vitest."
162
+ echo ""
163
+ echo " This is NOT a finding about your code, and nothing below it is"
164
+ echo " either."
165
+ echo ""
166
+ echo "To fix:"
167
+ echo " bun install --frozen-lockfile # or your package manager's equivalent"
168
+ echo ""
169
+ exit 1
170
+ fi
171
+ unset LISA_DECLARED_DEPS
172
+ fi
173
+
129
174
  # BEGIN: push destination guard
130
175
  # ---------------------------------------------------------------------------
131
176
  # Refuse a push whose RESOLVED destination is a deploy branch it was not aimed