@codyswann/lisa 4.36.2 → 4.37.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.
- package/all/copy-overwrite/scripts/lisa-enforcement-fallback.sh +2 -2
- package/all/copy-overwrite/scripts/lisa-hooks/block-blind-automerge.sh +626 -0
- package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.js +10 -0
- package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
- package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
- package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +24 -6
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/dist/strategies/package-lisa.js +10 -0
- package/dist/strategies/package-lisa.js.map +1 -1
- package/package.json +5 -4
- package/plugins/lisa/.claude-plugin/plugin.json +5 -1
- package/plugins/lisa/.codex-plugin/hooks.json +4 -0
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa/hooks/block-blind-automerge.agy.sh +54 -0
- package/plugins/lisa/hooks/block-blind-automerge.sh +623 -0
- package/plugins/lisa-agy/hooks/block-blind-automerge.agy.sh +54 -0
- package/plugins/lisa-agy/hooks/block-blind-automerge.sh +623 -0
- package/plugins/lisa-agy/hooks.json +13 -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 +5 -1
- package/plugins/lisa-copilot/hooks/block-blind-automerge.sh +623 -0
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/hooks/block-blind-automerge.sh +623 -0
- package/plugins/lisa-cursor/hooks/hooks.json +4 -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/materialized-artifacts.json +1 -0
- package/plugins/src/base/.claude-plugin/plugin.json +4 -0
- package/plugins/src/base/hooks/block-blind-automerge.agy.sh +54 -0
- package/plugins/src/base/hooks/block-blind-automerge.sh +623 -0
- package/scripts/build-plugins.sh +1 -1
- package/scripts/check-self-dependency-pin.mjs +401 -0
- package/scripts/generate-agy-plugin-artifacts.mjs +11 -0
- package/scripts/lib/per-agent-hook-filter.mjs +10 -0
- package/scripts/lisa-enforcement-fallback.sh +2 -2
- package/scripts/two-channel-couplings.json +1 -1
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Report when Lisa's own `@codyswann/lisa` pin has stopped tracking the
|
|
4
|
+
* package it pins (CodySwannGT/lisa#3768).
|
|
5
|
+
*
|
|
6
|
+
* ## The hole this fills
|
|
7
|
+
*
|
|
8
|
+
* `lisa apply` rewrites a stale `@codyswann/lisa` pin in every host
|
|
9
|
+
* repository. It deliberately does not do so here: applying to a manifest whose
|
|
10
|
+
* own `name` is `@codyswann/lisa` is restricted to security pins, so the
|
|
11
|
+
* self-pin phase never runs. That exemption is right — a package should not
|
|
12
|
+
* acquire a dependency on a published copy of itself as a side effect of
|
|
13
|
+
* applying to itself — and it has a consequence nobody wrote down: **the one
|
|
14
|
+
* repository whose pin no automation will ever correct is the one that ships
|
|
15
|
+
* the automation.**
|
|
16
|
+
*
|
|
17
|
+
* The result was measured twice. #2279 (2026-08-04) moved the floor to
|
|
18
|
+
* `^2.328.0` because it was 130 releases behind. #3662 (2026-09-03) moved it
|
|
19
|
+
* off that same `^2.328.0`, by then two majors behind, after drifting for about
|
|
20
|
+
* a month with nothing reporting it. Both fixes were bumps by a human who
|
|
21
|
+
* happened to read a version string. This is the thing that reads it instead.
|
|
22
|
+
*
|
|
23
|
+
* It is not cosmetic. A stale self-pin held `push-collects-integration-tree-once`
|
|
24
|
+
* green for a month: the 2.328.0 work-item gate tolerated running outside a git
|
|
25
|
+
* repository and 4.33+ does not, so the old fixture was the only reason that
|
|
26
|
+
* test passed (#3662).
|
|
27
|
+
*
|
|
28
|
+
* ## The threshold, and why it needs no tuning
|
|
29
|
+
*
|
|
30
|
+
* **The declared range must admit the newest installable release.** A caret
|
|
31
|
+
* floor that can no longer resolve the current release has, by construction,
|
|
32
|
+
* stopped tracking — no distance to agree on, no knob to set loose. That is
|
|
33
|
+
* exactly the state both #2279 and #3662 found, and it is the first moment the
|
|
34
|
+
* pin becomes incapable of following on its own.
|
|
35
|
+
*
|
|
36
|
+
* ## Which surface, stated rather than assumed
|
|
37
|
+
*
|
|
38
|
+
* Three surfaces disagree about which version of Lisa is in force and only one
|
|
39
|
+
* of them answers "what can a consumer install": a running session executes a
|
|
40
|
+
* version-pinned plugin cache (observed on `4.32.2` while npm was on
|
|
41
|
+
* `4.33.14`), the marketplace clone is a third answer again, and the registry
|
|
42
|
+
* is the fourth. This check compares against **the npm registry, and says so in
|
|
43
|
+
* its verdict line** — the plugin cache and the marketplace clone are never
|
|
44
|
+
* consulted, because neither is what `bun install` resolves.
|
|
45
|
+
*
|
|
46
|
+
* ## A version existing is not a version being installable
|
|
47
|
+
*
|
|
48
|
+
* `v4.33.7` was tagged, GitHub-released and provenance-attested and never
|
|
49
|
+
* reached npm (#3684) — the only gap in eleven consecutive tags. A pin can
|
|
50
|
+
* therefore be "behind" something no consumer can resolve. The packument's
|
|
51
|
+
* `versions` map contains only what was published, so such a version is absent
|
|
52
|
+
* by construction; on top of that the newest candidate is confirmed through the
|
|
53
|
+
* shipped exact-version prover before it is used as the yardstick, and a
|
|
54
|
+
* candidate the registry 404s is dropped in favour of the next one rather than
|
|
55
|
+
* treated as a gap in the check.
|
|
56
|
+
*
|
|
57
|
+
* ## Which tree, stated because two now exist
|
|
58
|
+
*
|
|
59
|
+
* This reads the **declared spec in the root `package.json`** and nothing else.
|
|
60
|
+
* It is deliberately not a lockfile audit: `bun.lock` carries a NESTED
|
|
61
|
+
* `@codyswann/lisa` subtree — its own `fs-extra` and `semver` resolve at
|
|
62
|
+
* versions the root does not — so "what is installed" has more than one answer
|
|
63
|
+
* here while "what is declared" has exactly one. The declared floor is what
|
|
64
|
+
* rotted twice and what a human had to read, so that is the thing watched. A
|
|
65
|
+
* nested tree diverging from the root is a different property with a different
|
|
66
|
+
* prover (`check-duplicate-versions.mjs`, `check-security-floors.mjs`), and
|
|
67
|
+
* conflating them would produce a check that answers neither question cleanly.
|
|
68
|
+
* @module scripts/check-self-dependency-pin
|
|
69
|
+
*/
|
|
70
|
+
import { readFileSync } from "node:fs";
|
|
71
|
+
import path from "node:path";
|
|
72
|
+
import process from "node:process";
|
|
73
|
+
import { fileURLToPath } from "node:url";
|
|
74
|
+
|
|
75
|
+
import semver from "semver";
|
|
76
|
+
|
|
77
|
+
import { verifyPublish } from "../all/copy-overwrite/scripts/check-npm-publish-landed.mjs";
|
|
78
|
+
import { invokedAsScript } from "./lib/invoked-as-script.mjs";
|
|
79
|
+
|
|
80
|
+
/** The package whose pin this checks — this repository's own name. */
|
|
81
|
+
export const SELF_PACKAGE = "@codyswann/lisa";
|
|
82
|
+
|
|
83
|
+
/** Public npm registry: the surface a consumer's install actually reads. */
|
|
84
|
+
const DEFAULT_REGISTRY = "https://registry.npmjs.org";
|
|
85
|
+
|
|
86
|
+
/** Sections a self-pin may legitimately live in, most specific first. */
|
|
87
|
+
const PIN_SECTIONS = ["dependencies", "devDependencies"];
|
|
88
|
+
|
|
89
|
+
/** The line a caller greps for. Never printed without a real verdict behind it. */
|
|
90
|
+
const VERDICT_PREFIX = "self-dependency-pin:";
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* How many published versions to confirm before giving up.
|
|
94
|
+
*
|
|
95
|
+
* Bounded on purpose: #3684 was one skipped version, not a run of them, and a
|
|
96
|
+
* check that walked backwards indefinitely would quietly convert a broken
|
|
97
|
+
* registry into a green pass against an ancient release.
|
|
98
|
+
*/
|
|
99
|
+
const MAX_CANDIDATES = 5;
|
|
100
|
+
|
|
101
|
+
/** Specs naming a LOCATION rather than a registry version. */
|
|
102
|
+
const NON_REGISTRY_SPEC =
|
|
103
|
+
/^(?:file|link|portal|workspace|git|git\+[a-z]+|github|https?|npm):/i;
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Where this manifest declares its self-pin, and what it says.
|
|
107
|
+
* @param {Record<string, unknown>} manifest - Parsed package.json.
|
|
108
|
+
* @returns {{section: string, spec: string} | null} The declaration, or null.
|
|
109
|
+
*/
|
|
110
|
+
export function locateSelfPin(manifest) {
|
|
111
|
+
const found = PIN_SECTIONS.map(section => ({
|
|
112
|
+
section,
|
|
113
|
+
spec: manifest?.[section]?.[SELF_PACKAGE],
|
|
114
|
+
})).find(entry => typeof entry.spec === "string");
|
|
115
|
+
return found === undefined ? null : found;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Published, non-prerelease versions, newest first.
|
|
120
|
+
*
|
|
121
|
+
* Read off the packument's `versions` map, which is the set the registry will
|
|
122
|
+
* actually serve — a tagged-but-unpublished version never appears in it, which
|
|
123
|
+
* is why the #3684 shape cannot reach the comparison at all.
|
|
124
|
+
* @param {Record<string, unknown>} packument - Parsed packument document.
|
|
125
|
+
* @returns {string[]} Versions in descending semver order.
|
|
126
|
+
*/
|
|
127
|
+
export function publishedVersionsDescending(packument) {
|
|
128
|
+
return Object.keys(packument?.versions ?? {})
|
|
129
|
+
.filter(version => semver.valid(version) !== null)
|
|
130
|
+
.filter(version => semver.prerelease(version) === null)
|
|
131
|
+
.sort(semver.rcompare);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Compare one declared spec against one installable version.
|
|
136
|
+
* @param {string} spec - The spec the manifest declares.
|
|
137
|
+
* @param {string} newest - Newest installable version.
|
|
138
|
+
* @returns {{verdict: string, detail: string}} The comparison's outcome.
|
|
139
|
+
*/
|
|
140
|
+
export function compareSpec(spec, newest) {
|
|
141
|
+
if (NON_REGISTRY_SPEC.test(spec)) {
|
|
142
|
+
return {
|
|
143
|
+
verdict: "local-checkout",
|
|
144
|
+
detail: `the spec "${spec}" names a location, not a release, so there is no registry version for it to fall behind`,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
if (semver.validRange(spec) === null) {
|
|
148
|
+
return { verdict: "unprovable", detail: `"${spec}" is not a valid range` };
|
|
149
|
+
}
|
|
150
|
+
if (semver.satisfies(newest, spec)) {
|
|
151
|
+
return { verdict: "current", detail: `${spec} still admits ${newest}` };
|
|
152
|
+
}
|
|
153
|
+
const floor = semver.minVersion(spec);
|
|
154
|
+
const majors = floor === null ? "?" : semver.major(newest) - floor.major;
|
|
155
|
+
return {
|
|
156
|
+
verdict: "stale",
|
|
157
|
+
detail: `${spec} cannot resolve ${newest} — ${majors} major(s) behind`,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* One packument read.
|
|
163
|
+
* @param {string} url - The packument URL.
|
|
164
|
+
* @param {typeof fetch} fetchImpl - Injected for tests.
|
|
165
|
+
* @returns {Promise<{packument: object | null, detail: string}>} The outcome.
|
|
166
|
+
*/
|
|
167
|
+
async function readPackumentOnce(url, fetchImpl) {
|
|
168
|
+
try {
|
|
169
|
+
const response = await fetchImpl(url, {
|
|
170
|
+
headers: { accept: "application/json" },
|
|
171
|
+
});
|
|
172
|
+
if (!response.ok) {
|
|
173
|
+
return { packument: null, detail: `HTTP ${response.status} from ${url}` };
|
|
174
|
+
}
|
|
175
|
+
return { packument: await response.json(), detail: "ok" };
|
|
176
|
+
} catch (error) {
|
|
177
|
+
return { packument: null, detail: `network: ${error.message} (${url})` };
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Ask the registry for the packument, retrying transport failures.
|
|
183
|
+
* @param {object} options - Registry, package, fetch seam and retry budget.
|
|
184
|
+
* @returns {Promise<{packument: object | null, detail: string}>} The document.
|
|
185
|
+
*/
|
|
186
|
+
async function fetchPackument({
|
|
187
|
+
registry,
|
|
188
|
+
packageName,
|
|
189
|
+
fetchImpl,
|
|
190
|
+
attempts,
|
|
191
|
+
sleep,
|
|
192
|
+
}) {
|
|
193
|
+
const url = `${registry}/${packageName}`;
|
|
194
|
+
const tries = Array.from({ length: attempts }, (_unused, index) => index);
|
|
195
|
+
return tries.reduce(
|
|
196
|
+
async (previous, index) => {
|
|
197
|
+
const settled = await previous;
|
|
198
|
+
if (settled.packument !== null) return settled;
|
|
199
|
+
if (index > 0) await sleep();
|
|
200
|
+
return readPackumentOnce(url, fetchImpl);
|
|
201
|
+
},
|
|
202
|
+
Promise.resolve({ packument: null, detail: "no attempt was made" })
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Confirm one candidate through the shipped exact-version prover.
|
|
208
|
+
* @param {object} options - Candidate, registry and the prover's seams.
|
|
209
|
+
* @returns {Promise<{version: string | null, detail: string, halt: boolean}>} Outcome.
|
|
210
|
+
*/
|
|
211
|
+
async function confirmCandidate({ version, registry, injected }) {
|
|
212
|
+
const proof = await verifyPublish({
|
|
213
|
+
packageName: SELF_PACKAGE,
|
|
214
|
+
version,
|
|
215
|
+
registry,
|
|
216
|
+
attempts: 2,
|
|
217
|
+
delayMs: 0,
|
|
218
|
+
...injected,
|
|
219
|
+
});
|
|
220
|
+
if (proof.verdict === "published") {
|
|
221
|
+
return { version, detail: proof.detail, halt: true };
|
|
222
|
+
}
|
|
223
|
+
// `missing` is the #3684 shape — a version that exists everywhere except the
|
|
224
|
+
// registry. It is dropped in favour of the next candidate. `unprovable` is a
|
|
225
|
+
// registry that could not answer, which stops the walk rather than sliding
|
|
226
|
+
// the yardstick backwards to a version chosen by a network fault.
|
|
227
|
+
return {
|
|
228
|
+
version: null,
|
|
229
|
+
detail:
|
|
230
|
+
proof.verdict === "missing"
|
|
231
|
+
? `${version} is listed in the packument but the registry 404s it`
|
|
232
|
+
: `${version}: ${proof.detail}`,
|
|
233
|
+
halt: proof.verdict === "unprovable",
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* The newest version the registry will actually serve.
|
|
239
|
+
* @param {object} options - Candidates, registry and the prover's seams.
|
|
240
|
+
* @returns {Promise<{version: string | null, detail: string}>} The yardstick.
|
|
241
|
+
*/
|
|
242
|
+
async function newestInstallable({ candidates, registry, injected }) {
|
|
243
|
+
if (candidates.length === 0) {
|
|
244
|
+
return {
|
|
245
|
+
version: null,
|
|
246
|
+
detail: "the packument listed no published version",
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
return candidates.slice(0, MAX_CANDIDATES).reduce(
|
|
250
|
+
async (previous, version) => {
|
|
251
|
+
const settled = await previous;
|
|
252
|
+
if (settled.halt) return settled;
|
|
253
|
+
return confirmCandidate({ version, registry, injected });
|
|
254
|
+
},
|
|
255
|
+
Promise.resolve({
|
|
256
|
+
version: null,
|
|
257
|
+
detail: "no candidate was confirmed",
|
|
258
|
+
halt: false,
|
|
259
|
+
})
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Decide the verdict for one manifest.
|
|
265
|
+
* @param {object} options - Manifest, registry origin and injected seams.
|
|
266
|
+
* @returns {Promise<{verdict: string, detail: string, newest: string | null, spec: string | null}>} Outcome.
|
|
267
|
+
*/
|
|
268
|
+
export async function checkSelfPin({
|
|
269
|
+
manifest,
|
|
270
|
+
registry = DEFAULT_REGISTRY,
|
|
271
|
+
fetchImpl = fetch,
|
|
272
|
+
attempts = 3,
|
|
273
|
+
sleep = () => new Promise(resolve => setTimeout(resolve, 2000)),
|
|
274
|
+
injected = {},
|
|
275
|
+
}) {
|
|
276
|
+
const declared = locateSelfPin(manifest);
|
|
277
|
+
if (declared === null) {
|
|
278
|
+
return {
|
|
279
|
+
verdict: "no-pin",
|
|
280
|
+
detail: `this manifest declares no ${SELF_PACKAGE} dependency, so there is no pin to keep fresh`,
|
|
281
|
+
newest: null,
|
|
282
|
+
spec: null,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
const { packument, detail } = await fetchPackument({
|
|
286
|
+
registry,
|
|
287
|
+
packageName: SELF_PACKAGE,
|
|
288
|
+
fetchImpl,
|
|
289
|
+
attempts,
|
|
290
|
+
sleep,
|
|
291
|
+
});
|
|
292
|
+
if (packument === null) {
|
|
293
|
+
return { verdict: "unprovable", detail, newest: null, spec: declared.spec };
|
|
294
|
+
}
|
|
295
|
+
const found = await newestInstallable({
|
|
296
|
+
candidates: publishedVersionsDescending(packument),
|
|
297
|
+
registry,
|
|
298
|
+
injected: { fetchImpl, ...injected },
|
|
299
|
+
});
|
|
300
|
+
if (found.version === null) {
|
|
301
|
+
return {
|
|
302
|
+
verdict: "unprovable",
|
|
303
|
+
detail: found.detail,
|
|
304
|
+
newest: null,
|
|
305
|
+
spec: declared.spec,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
return {
|
|
309
|
+
...compareSpec(declared.spec, found.version),
|
|
310
|
+
newest: found.version,
|
|
311
|
+
spec: declared.spec,
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/** Exit code for each verdict. `unprovable` blocks: nothing was proved. */
|
|
316
|
+
const FAILING_VERDICTS = new Set(["stale", "unprovable"]);
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* The default manifest: this repository's own package.json.
|
|
320
|
+
* @returns {string} Absolute path.
|
|
321
|
+
*/
|
|
322
|
+
function defaultManifestPath() {
|
|
323
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
324
|
+
return path.join(here, "..", "package.json");
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Return an optional CLI option.
|
|
329
|
+
* @param {string[]} argv - Arguments after the script path.
|
|
330
|
+
* @param {string} name - Option name, `--` included.
|
|
331
|
+
* @param {string} fallback - Value when the option is absent.
|
|
332
|
+
* @returns {string} The option's value.
|
|
333
|
+
*/
|
|
334
|
+
function option(argv, name, fallback) {
|
|
335
|
+
const index = argv.indexOf(name);
|
|
336
|
+
const value = index === -1 ? undefined : argv[index + 1];
|
|
337
|
+
return value === undefined || value.startsWith("--") ? fallback : value;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Render the failing verdicts, which is where the reader needs the detail.
|
|
342
|
+
* @param {{verdict: string, detail: string, spec: string | null}} outcome - Result.
|
|
343
|
+
* @param {string} registry - The surface that was compared against.
|
|
344
|
+
* @returns {string} Text for stderr.
|
|
345
|
+
*/
|
|
346
|
+
function failureReport(outcome, registry) {
|
|
347
|
+
if (outcome.verdict === "stale") {
|
|
348
|
+
return (
|
|
349
|
+
`❌ ${SELF_PACKAGE} is pinned at "${outcome.spec}" and ${outcome.detail}.\n` +
|
|
350
|
+
` Compared against ${registry} — NOT the session plugin cache and NOT\n` +
|
|
351
|
+
` the marketplace clone, both of which can name a different version.\n` +
|
|
352
|
+
` Nothing corrects this pin automatically: apply skips the self-pin\n` +
|
|
353
|
+
` phase on Lisa's own manifest (#3768). Raise the floor by hand.\n`
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
return (
|
|
357
|
+
`❌ Could not prove ${SELF_PACKAGE}'s pin is current: ${outcome.detail}.\n` +
|
|
358
|
+
` This is not a pass — nothing was measured, so it blocks rather than\n` +
|
|
359
|
+
` reporting a verdict it does not have.\n`
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Run the check for a caller that is a workflow step.
|
|
365
|
+
* @param {string[]} argv - Arguments after the script path.
|
|
366
|
+
* @param {object} [injected] - Test seams forwarded to {@link checkSelfPin}.
|
|
367
|
+
* @returns {Promise<number>} Process exit code.
|
|
368
|
+
*/
|
|
369
|
+
export async function main(argv, injected = {}) {
|
|
370
|
+
const registry = option(argv, "--registry", DEFAULT_REGISTRY);
|
|
371
|
+
const manifestPath = option(argv, "--manifest", defaultManifestPath());
|
|
372
|
+
const outcome = await checkSelfPin({
|
|
373
|
+
manifest: JSON.parse(readFileSync(manifestPath, "utf8")),
|
|
374
|
+
registry,
|
|
375
|
+
...injected,
|
|
376
|
+
});
|
|
377
|
+
process.stdout.write(
|
|
378
|
+
`${VERDICT_PREFIX} ${outcome.verdict} package=${SELF_PACKAGE} ` +
|
|
379
|
+
`declared=${outcome.spec ?? "none"} newest-installable=${outcome.newest ?? "unknown"} ` +
|
|
380
|
+
`surface=npm-registry:${registry}\n`
|
|
381
|
+
);
|
|
382
|
+
if (!FAILING_VERDICTS.has(outcome.verdict)) {
|
|
383
|
+
process.stdout.write(`✅ ${outcome.detail}.\n`);
|
|
384
|
+
return 0;
|
|
385
|
+
}
|
|
386
|
+
process.stderr.write(failureReport(outcome, registry));
|
|
387
|
+
return 1;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/* c8 ignore start -- CLI wiring, exercised through main() in tests */
|
|
391
|
+
if (invokedAsScript(import.meta.url)) {
|
|
392
|
+
main(process.argv.slice(2))
|
|
393
|
+
.then(code => {
|
|
394
|
+
process.exitCode = code;
|
|
395
|
+
})
|
|
396
|
+
.catch(error => {
|
|
397
|
+
process.stderr.write(`❌ ${error.message}\n`);
|
|
398
|
+
process.exitCode = 1;
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
/* c8 ignore stop */
|
|
@@ -261,6 +261,17 @@ const AGY_PLUGIN_HOOKS = [
|
|
|
261
261
|
agyScript: "block-direct-issue-create.agy.sh",
|
|
262
262
|
supportScripts: ["block-direct-issue-create.sh"],
|
|
263
263
|
},
|
|
264
|
+
{
|
|
265
|
+
// Bash-only by construction, like the guard above: `gh pr merge --auto` is
|
|
266
|
+
// a shell command whatever the harness, so `run_command` is the whole of
|
|
267
|
+
// its surface and agy loses nothing.
|
|
268
|
+
sourceScript: "block-blind-automerge.sh",
|
|
269
|
+
hookName: "lisa-block-blind-automerge",
|
|
270
|
+
event: "PreToolUse",
|
|
271
|
+
matcher: "run_command",
|
|
272
|
+
agyScript: "block-blind-automerge.agy.sh",
|
|
273
|
+
supportScripts: ["block-blind-automerge.sh"],
|
|
274
|
+
},
|
|
264
275
|
{
|
|
265
276
|
// Same shape as the entry above, plus one support file the others do not
|
|
266
277
|
// need: the classifier is Node, because the denylist it consults is
|
|
@@ -70,6 +70,16 @@ const SCRIPT_RULES = {
|
|
|
70
70
|
agy: true,
|
|
71
71
|
copilot: true,
|
|
72
72
|
},
|
|
73
|
+
// Bash-only by construction, like the guard above, and about a command every
|
|
74
|
+
// agent runs on every PR it opens. Nothing in it reads a Claude-only
|
|
75
|
+
// capability: it tokenizes a shell command and asks `gh` one question.
|
|
76
|
+
"block-blind-automerge.sh": {
|
|
77
|
+
claude: true,
|
|
78
|
+
codex: true,
|
|
79
|
+
cursor: true,
|
|
80
|
+
agy: true,
|
|
81
|
+
copilot: true,
|
|
82
|
+
},
|
|
73
83
|
// Portable everywhere the agent has a session-start event, because the
|
|
74
84
|
// question it answers — can this agent reach the credentials its work needs
|
|
75
85
|
// — is not vendor-specific. It only reads config and asks the provider for
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
#
|
|
6
6
|
# Every PreToolUse guard — block-no-verify, parity-safety-net,
|
|
7
7
|
# block-shell-json-parsing, block-instruction-file-edits,
|
|
8
|
-
# block-direct-issue-create — is declared in the
|
|
8
|
+
# block-direct-issue-create, block-blind-automerge — is declared in the
|
|
9
9
|
# Lisa plugin. A cloud session
|
|
10
10
|
# installs plugins at session start from the marketplace the repository
|
|
11
11
|
# declares, and when that does not happen the container runs with
|
|
@@ -313,7 +313,7 @@ plugin_tree_used=0
|
|
|
313
313
|
|
|
314
314
|
for guard in block-no-verify parity-safety-net block-shell-json-parsing \
|
|
315
315
|
block-instruction-file-edits block-direct-issue-create \
|
|
316
|
-
block-managed-file-edits; do
|
|
316
|
+
block-managed-file-edits block-blind-automerge; do
|
|
317
317
|
if [ -f "$host_tree/$guard.sh" ]; then
|
|
318
318
|
guard_names+=("$guard")
|
|
319
319
|
guard_scripts+=("$host_tree/$guard.sh")
|