@codyswann/lisa 4.36.2 → 4.37.1

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 (82) hide show
  1. package/all/copy-overwrite/scripts/lisa-enforcement-fallback.sh +2 -2
  2. package/all/copy-overwrite/scripts/lisa-hooks/block-blind-automerge.sh +626 -0
  3. package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
  4. package/dist/core/lisa-owned-hash-ledger.js +12 -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 +25 -7
  9. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  10. package/dist/strategies/package-lisa.js +10 -0
  11. package/dist/strategies/package-lisa.js.map +1 -1
  12. package/package.json +5 -4
  13. package/plugins/lisa/.claude-plugin/plugin.json +5 -1
  14. package/plugins/lisa/.codex-plugin/hooks.json +4 -0
  15. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  16. package/plugins/lisa/hooks/block-blind-automerge.agy.sh +54 -0
  17. package/plugins/lisa/hooks/block-blind-automerge.sh +623 -0
  18. package/plugins/lisa-agy/hooks/block-blind-automerge.agy.sh +54 -0
  19. package/plugins/lisa-agy/hooks/block-blind-automerge.sh +623 -0
  20. package/plugins/lisa-agy/hooks.json +13 -0
  21. package/plugins/lisa-agy/plugin.json +1 -1
  22. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  23. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  24. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  25. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  26. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  27. package/plugins/lisa-copilot/.claude-plugin/plugin.json +5 -1
  28. package/plugins/lisa-copilot/hooks/block-blind-automerge.sh +623 -0
  29. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  30. package/plugins/lisa-cursor/hooks/block-blind-automerge.sh +623 -0
  31. package/plugins/lisa-cursor/hooks/hooks.json +4 -0
  32. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  33. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  34. package/plugins/lisa-expo-agy/plugin.json +1 -1
  35. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  36. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  37. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  38. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  39. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  40. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  41. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  42. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  43. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  44. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  45. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  46. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  47. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  48. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  49. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  50. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  51. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  52. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  53. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  54. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  55. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  56. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  57. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  58. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  59. package/plugins/lisa-rails-agy/plugin.json +1 -1
  60. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  61. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  62. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  63. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  64. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  65. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  66. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  67. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  68. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  69. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  70. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  71. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  72. package/plugins/materialized-artifacts.json +1 -0
  73. package/plugins/src/base/.claude-plugin/plugin.json +4 -0
  74. package/plugins/src/base/hooks/block-blind-automerge.agy.sh +54 -0
  75. package/plugins/src/base/hooks/block-blind-automerge.sh +623 -0
  76. package/scripts/build-plugins.sh +1 -1
  77. package/scripts/check-self-dependency-pin.mjs +401 -0
  78. package/scripts/generate-agy-plugin-artifacts.mjs +11 -0
  79. package/scripts/lib/per-agent-hook-filter.mjs +10 -0
  80. package/scripts/lisa-enforcement-fallback.sh +2 -2
  81. package/scripts/two-channel-couplings.json +1 -1
  82. package/typescript/copy-overwrite/scripts/check-skipped-required-checks.mjs +750 -365
@@ -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")
@@ -7,7 +7,7 @@
7
7
  "workflows": 22,
8
8
  "steps": 716,
9
9
  "couplings": 34,
10
- "inventory": 72
10
+ "inventory": 73
11
11
  },
12
12
  "counts": {
13
13
  "package-backed": 10,