@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.
- package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.js +5 -0
- package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
- package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +8 -7
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/expo/copy-overwrite/scripts/bdd/baseline.mjs +2 -2
- package/package.json +1 -1
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa/hooks/threshold-ratchet.mjs +17 -0
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-agy/plugin.json +1 -1
- package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/hooks/threshold-ratchet.mjs +17 -0
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/hooks/threshold-ratchet.mjs +17 -0
- package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-agy/plugin.json +1 -1
- package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-agy/plugin.json +1 -1
- package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-agy/plugin.json +1 -1
- package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-agy/plugin.json +1 -1
- package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-agy/plugin.json +1 -1
- package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-agy/plugin.json +1 -1
- package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-agy/plugin.json +1 -1
- package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/src/base/hooks/threshold-ratchet.mjs +17 -0
- package/rails/copy-overwrite/scripts/check-threshold-ratchet.mjs +17 -0
- package/scripts/plugin-parity-drift.mjs +66 -11
- package/typescript/copy-overwrite/scripts/check-skipped-required-checks.mjs +3 -3
- package/typescript/copy-overwrite/scripts/check-threshold-ratchet.mjs +17 -0
- 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
|
|
284
|
-
* from each subdir's `.claude-plugin/plugin.json`
|
|
285
|
-
* dirs (`unknown`, git hashes) are skipped because
|
|
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
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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, {
|
|
179
|
-
|
|
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
|
};
|