@codyswann/lisa 4.4.3 → 4.4.5

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 (67) hide show
  1. package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
  2. package/dist/core/lisa-owned-hash-ledger.js +5 -0
  3. package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
  4. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  5. package/dist/core/upstream-evidence-manifest.js +8 -7
  6. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  7. package/expo/copy-overwrite/scripts/bdd/baseline.mjs +2 -2
  8. package/package.json +1 -1
  9. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  10. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  11. package/plugins/lisa/hooks/threshold-ratchet.mjs +17 -0
  12. package/plugins/lisa-agy/plugin.json +1 -1
  13. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  14. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  15. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  16. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  17. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  18. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  19. package/plugins/lisa-copilot/hooks/threshold-ratchet.mjs +17 -0
  20. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  21. package/plugins/lisa-cursor/hooks/threshold-ratchet.mjs +17 -0
  22. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  23. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  24. package/plugins/lisa-expo-agy/plugin.json +1 -1
  25. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  26. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  27. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  28. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  29. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  30. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  31. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  32. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  33. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  34. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  35. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  36. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  37. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  38. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  39. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  40. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  41. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  42. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  43. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  44. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  45. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  46. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  47. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  48. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  49. package/plugins/lisa-rails-agy/plugin.json +1 -1
  50. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  51. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  52. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  53. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  54. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  55. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  56. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  57. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  58. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  59. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  60. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  61. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  62. package/plugins/src/base/hooks/threshold-ratchet.mjs +17 -0
  63. package/rails/copy-overwrite/scripts/check-threshold-ratchet.mjs +17 -0
  64. package/scripts/plugin-parity-drift.mjs +66 -11
  65. package/typescript/copy-overwrite/scripts/check-skipped-required-checks.mjs +3 -3
  66. package/typescript/copy-overwrite/scripts/check-threshold-ratchet.mjs +17 -0
  67. package/typescript/copy-overwrite/scripts/check-verification-coverage.mjs +24 -2
@@ -95,6 +95,20 @@ const NO_RENAMES = "--no-renames";
95
95
 
96
96
  /**
97
97
  * Run git, returning stdout or null on any failure.
98
+ *
99
+ * The swallow is CORRECT here and is kept, because #2777 already made the
100
+ * consumer fail closed: a null change set reaches `threshold-ratchet: failing
101
+ * closed — an enforcement gate that cannot read the change set has not verified
102
+ * it`. That comment names the original defect exactly — "git() swallows every
103
+ * failure, so the ratchet passed exactly when it had no evidence" — so a killed
104
+ * child now joins the failures that already refuse. This call site needed the
105
+ * DEADLINE and nothing else.
106
+ *
107
+ * The deadline is written inline rather than imported. This file is
108
+ * materialized from `plugins/src/base/hooks/` into two stack lanes AND runs as
109
+ * an agent-time hook inside a plugin payload, which has no `./lib/` to import
110
+ * from — the same accommodation `preflight-secrets.mjs` makes, and the same one
111
+ * the entry guard below is written out for.
98
112
  * @param {string[]} args Git arguments
99
113
  * @param {string} [cwd] Working directory
100
114
  * @returns {string | null} Captured stdout, or null when git failed
@@ -105,6 +119,9 @@ function git(args, cwd) {
105
119
  cwd,
106
120
  encoding: "utf-8",
107
121
  stdio: ["ignore", "pipe", "ignore"],
122
+ // A hang detector, not a budget. `git` reached through PATH on macOS goes
123
+ // via Apple's xcrun shim, measured over 20s under load in #2887.
124
+ timeout: 30_000,
108
125
  });
109
126
  } catch {
110
127
  return null;
@@ -278,12 +278,51 @@ function readManifestVersion(manifestPath) {
278
278
  }
279
279
  }
280
280
 
281
+ /**
282
+ * Whether a cached version directory has been ORPHANED — the plugin manager's
283
+ * marker for a version that is no longer installed or served.
284
+ *
285
+ * The cache is append-mostly: uninstalling a plugin does not delete its version
286
+ * directories, it stamps each one with `.orphaned_at`. So the directories on
287
+ * disk are a record of every version ever fetched, not of what is installed
288
+ * now, and reading them as the latter is how this script came to compare a pin
289
+ * against ten-day-old leftovers.
290
+ *
291
+ * @param {string} versionDir - absolute path to one cached version directory.
292
+ * @returns {boolean} true when the directory carries an orphan marker.
293
+ */
294
+ function isOrphanedVersion(versionDir) {
295
+ return fs.existsSync(path.join(versionDir, ".orphaned_at"));
296
+ }
297
+
281
298
  /**
282
299
  * Resolve the current upstream version of `name@marketplace` purely from the
283
- * cache tree: the MAX valid semver across the immediate version subdirs, read
284
- * from each subdir's `.claude-plugin/plugin.json` `version` field. Non-semver
285
- * dirs (`unknown`, git hashes) are skipped because the manifest version is what
286
- * counts.
300
+ * cache tree: the MAX valid semver across the immediate version subdirs that
301
+ * are still LIVE, read from each subdir's `.claude-plugin/plugin.json`
302
+ * `version` field. Non-semver dirs (`unknown`, git hashes) are skipped because
303
+ * the manifest version is what counts, and orphaned dirs are skipped because
304
+ * they are not installed.
305
+ *
306
+ * Orphans are filtered BEFORE the max, and the order is load-bearing.
307
+ * "Filter, then take the max" and "take the max, then check whether it is
308
+ * orphaned" are different functions, and they disagree exactly when the newest
309
+ * live version is older than an orphan:
310
+ *
311
+ * live 1.0.6 + orphaned 2.0.4
312
+ * filter-then-max -> 1.0.6 (correct: 1.0.6 IS installed)
313
+ * max-then-check -> not-installed (wrong)
314
+ *
315
+ * When nothing live remains, this reports `not-installed` — a status this
316
+ * script already has and already handles — rather than manufacturing a current
317
+ * version out of orphans. That is `core/apply-receipt`'s principle: an
318
+ * unresolvable state reports unresolvable, not half-understood.
319
+ *
320
+ * The failure this closes was not theoretical. Every one of the ten cached
321
+ * `safety-net` versions was orphaned in a single sweep, and the resulting
322
+ * manufactured comparison blocked every push from the checkout — in one
323
+ * direction, and then, after a pin was moved to satisfy it, in the other.
324
+ * A defect that produces two opposite plausible remedies is one where the
325
+ * remedy is in neither direction.
287
326
  *
288
327
  * @param {string} cacheRoot - the installed-plugin cache root.
289
328
  * @param {string} name - plugin name.
@@ -304,23 +343,39 @@ export function resolveCurrentVersion(cacheRoot, name, marketplace) {
304
343
  return { status: NOT_INSTALLED, version: null };
305
344
  }
306
345
  const versions = [];
346
+ // Counted separately from `versions`, because "nothing is installed" and
347
+ // "something is installed but I cannot read its version" are different
348
+ // answers and must not collapse into one. A live directory with an
349
+ // unparseable manifest is `unresolved`; no live directory at all is
350
+ // `not-installed`.
351
+ let liveDirs = 0;
307
352
  for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
308
353
  if (!entry.isDirectory()) {
309
354
  continue;
310
355
  }
311
- const manifest = path.join(
312
- dir,
313
- entry.name,
314
- ".claude-plugin",
315
- "plugin.json"
316
- );
356
+ const versionDir = path.join(dir, entry.name);
357
+ // Before the manifest is even read: an orphaned directory is not an
358
+ // installed version, whatever its manifest claims.
359
+ if (isOrphanedVersion(versionDir)) {
360
+ continue;
361
+ }
362
+ liveDirs += 1;
363
+ const manifest = path.join(versionDir, ".claude-plugin", "plugin.json");
317
364
  const version = readManifestVersion(manifest);
318
365
  if (version !== null && isValidSemver(version)) {
319
366
  versions.push(version);
320
367
  }
321
368
  }
322
369
  if (versions.length === 0) {
323
- return { status: "unresolved", version: null };
370
+ // No live directory at all means the plugin is not installed — a known,
371
+ // answerable state, and the one the whole orphan filter exists to reach.
372
+ // But a live directory whose manifest would not parse is still INSTALLED
373
+ // and merely unreadable, which is what `unresolved` has always meant.
374
+ // Collapsing the two would answer "I cannot read this version" with "this
375
+ // is not here", and an operator would go looking for the wrong thing.
376
+ return liveDirs === 0
377
+ ? { status: NOT_INSTALLED, version: null }
378
+ : { status: "unresolved", version: null };
324
379
  }
325
380
  // `versions` is non-empty (guarded above), so seeding with the first element
326
381
  // is exactly what the no-seed form did — and it cannot throw if that guard is
@@ -195,10 +195,10 @@
195
195
  * @module scripts/check-skipped-required-checks
196
196
  */
197
197
 
198
- import { execFileSync } from "node:child_process";
199
198
  import { appendFileSync, existsSync, readFileSync } from "node:fs";
200
199
  import { resolve } from "node:path";
201
200
 
201
+ import { boundedExecFileSync } from "./lib/bounded-spawn.mjs";
202
202
  import { invokedAsScript } from "./lib/invoked-as-script.mjs";
203
203
 
204
204
  /** Repo-relative path of the per-repo declaration file. */
@@ -988,7 +988,7 @@ export function fetchPullRequestChecks(pr, repo) {
988
988
  if (repo) args.push("--repo", repo);
989
989
  let raw;
990
990
  try {
991
- raw = execFileSync("gh", args, { encoding: "utf8" });
991
+ raw = boundedExecFileSync("gh", args, { encoding: "utf8" });
992
992
  } catch (error) {
993
993
  raw = typeof error?.stdout === "string" ? error.stdout : "";
994
994
  if (raw.trim() === "") {
@@ -1027,7 +1027,7 @@ export function fetchLiveRequiredContexts(ruleset) {
1027
1027
  }
1028
1028
  const contexts = [];
1029
1029
  for (const id of ruleset.ids) {
1030
- const raw = execFileSync(
1030
+ const raw = boundedExecFileSync(
1031
1031
  "gh",
1032
1032
  [
1033
1033
  "api",
@@ -95,6 +95,20 @@ const NO_RENAMES = "--no-renames";
95
95
 
96
96
  /**
97
97
  * Run git, returning stdout or null on any failure.
98
+ *
99
+ * The swallow is CORRECT here and is kept, because #2777 already made the
100
+ * consumer fail closed: a null change set reaches `threshold-ratchet: failing
101
+ * closed — an enforcement gate that cannot read the change set has not verified
102
+ * it`. That comment names the original defect exactly — "git() swallows every
103
+ * failure, so the ratchet passed exactly when it had no evidence" — so a killed
104
+ * child now joins the failures that already refuse. This call site needed the
105
+ * DEADLINE and nothing else.
106
+ *
107
+ * The deadline is written inline rather than imported. This file is
108
+ * materialized from `plugins/src/base/hooks/` into two stack lanes AND runs as
109
+ * an agent-time hook inside a plugin payload, which has no `./lib/` to import
110
+ * from — the same accommodation `preflight-secrets.mjs` makes, and the same one
111
+ * the entry guard below is written out for.
98
112
  * @param {string[]} args Git arguments
99
113
  * @param {string} [cwd] Working directory
100
114
  * @returns {string | null} Captured stdout, or null when git failed
@@ -105,6 +119,9 @@ function git(args, cwd) {
105
119
  cwd,
106
120
  encoding: "utf-8",
107
121
  stdio: ["ignore", "pipe", "ignore"],
122
+ // A hang detector, not a budget. `git` reached through PATH on macOS goes
123
+ // via Apple's xcrun shim, measured over 20s under load in #2887.
124
+ timeout: 30_000,
108
125
  });
109
126
  } catch {
110
127
  return null;
@@ -28,6 +28,7 @@
28
28
  */
29
29
  import { execSync } from "node:child_process";
30
30
 
31
+ import { isChildTimeout } from "./lib/bounded-spawn.mjs";
31
32
  import { invokedAsScript } from "./lib/invoked-as-script.mjs";
32
33
 
33
34
  const BEHAVIORAL_TYPES = new Set(["feat", "fix"]);
@@ -173,10 +174,31 @@ async function gatherContext() {
173
174
  const diffRange = `${base}...${head}`;
174
175
  const logRange = `${base}..${head}`;
175
176
 
177
+ // A KILLED child re-raises rather than taking the fallback. The first
178
+ // caller's fallback is `""`, which makes `changedFiles` empty — and an empty
179
+ // change set means this gate concludes the pull request requires no
180
+ // verification coverage and PASSES. So a busy machine would wave through
181
+ // exactly the change the gate exists to measure, having measured nothing.
182
+ //
183
+ // This is a degree worse than the sibling case in
184
+ // `generate-lisa-owned-hash-ledger.mjs`, and the difference is worth keeping
185
+ // straight. A ledger built from silence merely VOUCHES FOR NOTHING — it is
186
+ // empty, and empty is at least a shape someone might notice. This gate
187
+ // actively CERTIFIES the pull request as needing nothing, which is a positive
188
+ // claim about work nobody did, recorded as a pass.
189
+ //
190
+ // `timeout:` is stated literally rather than imported, because this file is
191
+ // shipped into a stack lane and read by the conformance scan, which looks for
192
+ // the option at the call site.
176
193
  const runGit = (cmd, fallback) => {
177
194
  try {
178
- return execSync(cmd, { encoding: "utf8" });
179
- } catch {
195
+ return execSync(cmd, {
196
+ encoding: "utf8",
197
+ // A hang detector, not a budget — see #2887 on the xcrun shim.
198
+ timeout: 30_000,
199
+ });
200
+ } catch (error) {
201
+ if (isChildTimeout(error)) throw error;
180
202
  return fallback;
181
203
  }
182
204
  };