@codyswann/lisa 4.27.6 → 4.27.8
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-hooks/block-direct-issue-create.sh +507 -11
- package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.js +102 -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/project-config-nightly-e2e.d.ts +21 -0
- package/dist/core/project-config-nightly-e2e.d.ts.map +1 -0
- package/dist/core/project-config-nightly-e2e.js +43 -0
- package/dist/core/project-config-nightly-e2e.js.map +1 -0
- package/dist/core/project-config.d.ts +4 -0
- package/dist/core/project-config.d.ts.map +1 -1
- package/dist/core/project-config.js +3 -0
- package/dist/core/project-config.js.map +1 -1
- package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +56 -12
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/dist/opencode/plugin-templates/lisa-block-direct-issue-create.ts +180 -19
- package/expo/create-only/.github/nightly-e2e-policy.json +16 -0
- package/expo/create-only/.github/workflows/nightly-e2e-health.yml +33 -8
- package/expo/create-only/.github/workflows/nightly-e2e-tracking.yml +30 -0
- package/package.json +3 -3
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/skills/lisa-exploratory-qa/SKILL.md +10 -6
- package/plugins/lisa/.codex-plugin/skills/lisa-intake/SKILL.md +18 -13
- package/plugins/lisa/.codex-plugin/skills/lisa-learnings-audit/SKILL.md +13 -7
- package/plugins/lisa/.codex-plugin/skills/lisa-linear-build-intake/SKILL.md +3 -1
- package/plugins/lisa/.codex-plugin/skills/lisa-monitor/SKILL.md +10 -6
- package/plugins/lisa/.codex-plugin/skills/lisa-project-ideation/SKILL.md +10 -6
- package/plugins/lisa/.codex-plugin/skills/lisa-repair-intake/SKILL.md +10 -6
- package/plugins/lisa/hooks/block-direct-issue-create.sh +507 -11
- package/plugins/lisa/skills/lisa-exploratory-qa/SKILL.md +10 -6
- package/plugins/lisa/skills/lisa-intake/SKILL.md +18 -13
- package/plugins/lisa/skills/lisa-learnings-audit/SKILL.md +13 -7
- package/plugins/lisa/skills/lisa-linear-build-intake/SKILL.md +3 -1
- package/plugins/lisa/skills/lisa-monitor/SKILL.md +10 -6
- package/plugins/lisa/skills/lisa-project-ideation/SKILL.md +10 -6
- package/plugins/lisa/skills/lisa-repair-intake/SKILL.md +10 -6
- package/plugins/lisa-agy/hooks/block-direct-issue-create.sh +507 -11
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-agy/skills/lisa-exploratory-qa/SKILL.md +10 -6
- package/plugins/lisa-agy/skills/lisa-intake/SKILL.md +18 -13
- package/plugins/lisa-agy/skills/lisa-learnings-audit/SKILL.md +13 -7
- package/plugins/lisa-agy/skills/lisa-linear-build-intake/SKILL.md +3 -1
- package/plugins/lisa-agy/skills/lisa-monitor/SKILL.md +10 -6
- package/plugins/lisa-agy/skills/lisa-project-ideation/SKILL.md +10 -6
- package/plugins/lisa-agy/skills/lisa-repair-intake/SKILL.md +10 -6
- 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/block-direct-issue-create.sh +507 -11
- package/plugins/lisa-copilot/skills/lisa-exploratory-qa/SKILL.md +10 -6
- package/plugins/lisa-copilot/skills/lisa-intake/SKILL.md +18 -13
- package/plugins/lisa-copilot/skills/lisa-learnings-audit/SKILL.md +13 -7
- package/plugins/lisa-copilot/skills/lisa-linear-build-intake/SKILL.md +3 -1
- package/plugins/lisa-copilot/skills/lisa-monitor/SKILL.md +10 -6
- package/plugins/lisa-copilot/skills/lisa-project-ideation/SKILL.md +10 -6
- package/plugins/lisa-copilot/skills/lisa-repair-intake/SKILL.md +10 -6
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/hooks/block-direct-issue-create.sh +507 -11
- package/plugins/lisa-cursor/skills/lisa-exploratory-qa/SKILL.md +10 -6
- package/plugins/lisa-cursor/skills/lisa-intake/SKILL.md +18 -13
- package/plugins/lisa-cursor/skills/lisa-learnings-audit/SKILL.md +13 -7
- package/plugins/lisa-cursor/skills/lisa-linear-build-intake/SKILL.md +3 -1
- package/plugins/lisa-cursor/skills/lisa-monitor/SKILL.md +10 -6
- package/plugins/lisa-cursor/skills/lisa-project-ideation/SKILL.md +10 -6
- package/plugins/lisa-cursor/skills/lisa-repair-intake/SKILL.md +10 -6
- 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/block-direct-issue-create.sh +507 -11
- package/plugins/src/base/skills/lisa-exploratory-qa/SKILL.md +10 -6
- package/plugins/src/base/skills/lisa-intake/SKILL.md +18 -13
- package/plugins/src/base/skills/lisa-learnings-audit/SKILL.md +13 -7
- package/plugins/src/base/skills/lisa-linear-build-intake/SKILL.md +3 -1
- package/plugins/src/base/skills/lisa-monitor/SKILL.md +10 -6
- package/plugins/src/base/skills/lisa-project-ideation/SKILL.md +10 -6
- package/plugins/src/base/skills/lisa-repair-intake/SKILL.md +10 -6
- package/scripts/two-channel-couplings.json +70 -5
- package/typescript/copy-overwrite/.prettierignore +7 -0
- package/typescript/copy-overwrite/scripts/nightly-e2e-provider-action.mjs +70 -0
- package/typescript/copy-overwrite/scripts/nightly-e2e-provider-github-jira.mjs +296 -0
- package/typescript/copy-overwrite/scripts/nightly-e2e-provider-linear-sentry.mjs +283 -0
- package/typescript/copy-overwrite/scripts/nightly-e2e-provider-support.mjs +55 -0
- package/typescript/copy-overwrite/scripts/nightly-e2e-tracking-config.mjs +149 -0
- package/typescript/copy-overwrite/scripts/reconcile-nightly-e2e-tracking-cli.mjs +56 -0
- package/typescript/copy-overwrite/scripts/reconcile-nightly-e2e-tracking.mjs +208 -0
|
@@ -17,13 +17,22 @@
|
|
|
17
17
|
* The port matches on the raw command text rather than tokenising it, which
|
|
18
18
|
* makes it naturally immune to the prefix and tokenisation bypass classes the
|
|
19
19
|
* shell guard had to be restructured to close — an unrecognised wrapper is just
|
|
20
|
-
* more text before the CLI name.
|
|
21
|
-
*
|
|
22
|
-
* guard
|
|
23
|
-
*
|
|
24
|
-
*
|
|
20
|
+
* more text before the CLI name.
|
|
21
|
+
*
|
|
22
|
+
* It reaches past the command text in the two places the canonical guard does:
|
|
23
|
+
* into the contents of any file the command names — which is what closes
|
|
24
|
+
* `bash create.sh`, `node wrapper.mjs` and `curl --data-binary @payload.json`
|
|
25
|
+
* — and into the lifecycle-role declaration a state-based tracker has to use
|
|
26
|
+
* because no flag on `curl` can carry a workflow state.
|
|
27
|
+
*
|
|
28
|
+
* Two deliberate differences remain, both in the permissive direction so this
|
|
29
|
+
* port can never refuse something the canonical guard would allow:
|
|
25
30
|
* - remote execution (`ssh host '…'`) is not intercepted, matching the shell
|
|
26
|
-
* guard's documented limit
|
|
31
|
+
* guard's documented limit;
|
|
32
|
+
* - a declaration found inside a FILE answers for every creation in that
|
|
33
|
+
* file. Matching raw text cannot attribute a label to one create among
|
|
34
|
+
* several, and the alternative — accepting only whole-scope markers there
|
|
35
|
+
* — would refuse an honest single-create script that carries its label.
|
|
27
36
|
*
|
|
28
37
|
* NOTE: This file is a template Lisa copies verbatim into a host project's
|
|
29
38
|
* `.opencode/plugin/`. It is intentionally excluded from this repo's tsconfig
|
|
@@ -53,6 +62,37 @@ const LisaBlockDirectIssueCreate = async () => {
|
|
|
53
62
|
const ISSUES_ENDPOINT = /repos\/([^/\s]+)\/([^/\s]+)\/issues\b/;
|
|
54
63
|
const DEFAULT_READY_ROLE = "status:ready";
|
|
55
64
|
const DEFAULT_UPSTREAM_REPO = "CodySwannGT/lisa";
|
|
65
|
+
/**
|
|
66
|
+
* The build-ready declaration for a tracker whose ready role is a workflow
|
|
67
|
+
* STATE rather than a label.
|
|
68
|
+
*
|
|
69
|
+
* GitHub's role is a label and labels are argv-native, so `--label` has
|
|
70
|
+
* always been writable there. JIRA's and Linear's are states living in the
|
|
71
|
+
* request payload, and the mandated client is `curl`, which has no flag that
|
|
72
|
+
* carries one — so the only declaration those trackers could satisfy was
|
|
73
|
+
* `[lisa-human-gate]`, a false statement about a build-ready item. A guard
|
|
74
|
+
* that leaves an honest operator no compliant command and one dishonest one
|
|
75
|
+
* fails in the harmful direction.
|
|
76
|
+
*
|
|
77
|
+
* The role is what the access layer already consumes: it refuses a
|
|
78
|
+
* caller-supplied state id, takes `lifecycle_role`, resolves it against the
|
|
79
|
+
* tracker's own catalog and fails closed. So the token decides the lane
|
|
80
|
+
* rather than decorating the command.
|
|
81
|
+
*
|
|
82
|
+
* The optional quote AFTER the key name is load-bearing: the refusal tells
|
|
83
|
+
* the operator to put the declaration in the request payload, and a JSON
|
|
84
|
+
* payload quotes its keys, so demanding `[:=]` immediately after a bare key
|
|
85
|
+
* name refuses the exact spelling it just asked for.
|
|
86
|
+
*/
|
|
87
|
+
const LIFECYCLE_ROLE_READY =
|
|
88
|
+
/(?:^|[^\w.-])(?:lifecycle_role|LIFECYCLE_ROLE)["']?\s*[:=]\s*["']?ready\b|(?:^|[^\w.-])--role[=\s]+["']?ready\b/;
|
|
89
|
+
/** A creation verb, and a tracker endpoint to send it to. */
|
|
90
|
+
const GRAPHQL_CREATE = /createIssue|issueCreate/;
|
|
91
|
+
const TRACKER_ENDPOINT =
|
|
92
|
+
/api\.linear\.app\/graphql|api\.github\.com|atlassian\.net\/rest\/api|repos\/[^/\s?#'"]+\/[^/\s?#'"]+\/issues/i;
|
|
93
|
+
/** Bounds on reading files a command names, so a hook stays a hook. */
|
|
94
|
+
const MAX_FILE_BYTES = 262_144;
|
|
95
|
+
const MAX_FILES = 8;
|
|
56
96
|
const CREATION_SIGNATURES: readonly {
|
|
57
97
|
readonly re: RegExp;
|
|
58
98
|
readonly name: string;
|
|
@@ -117,6 +157,8 @@ const LisaBlockDirectIssueCreate = async () => {
|
|
|
117
157
|
readonly upstreamReadyRole: string;
|
|
118
158
|
/** Whether the caller's ready role is a GitHub label at all. */
|
|
119
159
|
readonly callerIsGithub: boolean;
|
|
160
|
+
/** Whether the caller's ready role is a workflow state, not a label. */
|
|
161
|
+
readonly stateRoleTracker: boolean;
|
|
120
162
|
}
|
|
121
163
|
|
|
122
164
|
/** Which roles satisfy a filing, and the target a refusal should name. */
|
|
@@ -179,11 +221,62 @@ const LisaBlockDirectIssueCreate = async () => {
|
|
|
179
221
|
).toLowerCase(),
|
|
180
222
|
upstreamReadyRole: hardening.upstreamReadyRole ?? DEFAULT_READY_ROLE,
|
|
181
223
|
callerIsGithub: tracker === "github",
|
|
224
|
+
stateRoleTracker: tracker === "jira" || tracker === "linear",
|
|
182
225
|
};
|
|
183
226
|
};
|
|
184
227
|
|
|
185
228
|
const policy = await resolvePolicy();
|
|
186
229
|
|
|
230
|
+
/**
|
|
231
|
+
* The readable files a command names, with their contents.
|
|
232
|
+
*
|
|
233
|
+
* The guard used to match the command text and nothing else, so a creation
|
|
234
|
+
* one file away was invisible: `bash create.sh` is two words, and the
|
|
235
|
+
* conjunction it looks for — an endpoint and a creation verb together — never
|
|
236
|
+
* formed. Lisa's own guards push agents into exactly that shape, telling them
|
|
237
|
+
* to write payloads to a file and execute the file, so complying with the
|
|
238
|
+
* guidance produced the bypass.
|
|
239
|
+
*
|
|
240
|
+
* Which programs execute their operands is unbounded and every gap in such a
|
|
241
|
+
* list fails open, so the question is inverted the same way the wrapper
|
|
242
|
+
* question was: which tokens name a file? That is bounded by the command.
|
|
243
|
+
* @param props Helper inputs.
|
|
244
|
+
* @param props.text The command being inspected.
|
|
245
|
+
* @returns Each named file's path and contents, bounded in count and size.
|
|
246
|
+
*/
|
|
247
|
+
const namedFiles = async ({
|
|
248
|
+
text,
|
|
249
|
+
}: Readonly<{ text: string }>): Promise<
|
|
250
|
+
readonly { readonly path: string; readonly text: string }[]
|
|
251
|
+
> => {
|
|
252
|
+
const found: { path: string; text: string }[] = [];
|
|
253
|
+
const seen = new Set<string>();
|
|
254
|
+
const candidates = text
|
|
255
|
+
.split(/[\s'"]+/)
|
|
256
|
+
// `curl -d@payload.json` is one word, and the path is the half after the
|
|
257
|
+
// `@`. Both halves are offered rather than guessing which flag it was.
|
|
258
|
+
.flatMap(raw =>
|
|
259
|
+
raw.includes("@") ? [raw, raw.split("@").pop() ?? ""] : [raw]
|
|
260
|
+
);
|
|
261
|
+
for (const raw of candidates) {
|
|
262
|
+
if (found.length >= MAX_FILES) break;
|
|
263
|
+
const token = raw.replace(/^@/, "");
|
|
264
|
+
if (!token || token === "-" || seen.has(token)) continue;
|
|
265
|
+
seen.add(token);
|
|
266
|
+
try {
|
|
267
|
+
const handle = Bun.file(token);
|
|
268
|
+
// A file too large to inspect is skipped rather than half-read: a
|
|
269
|
+
// truncated scan reports a confident allow about text it never saw.
|
|
270
|
+
if (handle.size === 0 || handle.size > MAX_FILE_BYTES) continue;
|
|
271
|
+
if (!(await handle.exists())) continue;
|
|
272
|
+
found.push({ path: token, text: await handle.text() });
|
|
273
|
+
} catch {
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return found;
|
|
278
|
+
};
|
|
279
|
+
|
|
187
280
|
/**
|
|
188
281
|
* A repository token reduced to a comparable `owner/name`.
|
|
189
282
|
*
|
|
@@ -274,10 +367,7 @@ const LisaBlockDirectIssueCreate = async () => {
|
|
|
274
367
|
const inlineOverride = /LISA_ALLOW_DIRECT_ISSUE_CREATE=/.test(command);
|
|
275
368
|
if (process.env["LISA_ALLOW_DIRECT_ISSUE_CREATE"] && !inlineOverride)
|
|
276
369
|
return;
|
|
277
|
-
const
|
|
278
|
-
entry.re.test(command)
|
|
279
|
-
);
|
|
280
|
-
if (!signature) return;
|
|
370
|
+
const inline = CREATION_SIGNATURES.find(entry => entry.re.test(command));
|
|
281
371
|
// The build-ready role counts ONLY as the value of a label / state flag.
|
|
282
372
|
// A free-text scan of the command let a bug report's own title declare
|
|
283
373
|
// readiness — `gh issue create --title "status:ready is broken"` — which
|
|
@@ -300,17 +390,71 @@ const LisaBlockDirectIssueCreate = async () => {
|
|
|
300
390
|
policy,
|
|
301
391
|
targetRepository({ declarable })
|
|
302
392
|
);
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
393
|
+
// The role escape is scoped: accepted only where no argv flag on the
|
|
394
|
+
// mandated client can carry a state, and never on a GitHub target where
|
|
395
|
+
// the label IS writable and a second weaker spelling would be a hole.
|
|
396
|
+
const stateRoleOk = policy.stateRoleTracker && named === undefined;
|
|
397
|
+
const declaresFor = (
|
|
398
|
+
text: string,
|
|
399
|
+
against: readonly string[],
|
|
400
|
+
roleOk: boolean
|
|
401
|
+
): boolean =>
|
|
402
|
+
[...text.matchAll(LABEL_FLAG)].some(match =>
|
|
403
|
+
(match[2] ?? "")
|
|
404
|
+
.split(",")
|
|
405
|
+
.map(part => part.trim())
|
|
406
|
+
.some(candidate => against.includes(candidate))
|
|
407
|
+
) ||
|
|
408
|
+
text.includes(HUMAN_GATE_MARKER) ||
|
|
409
|
+
(roleOk && LIFECYCLE_ROLE_READY.test(text));
|
|
410
|
+
const declares = (text: string): boolean =>
|
|
411
|
+
declaresFor(text, roles, stateRoleOk);
|
|
412
|
+
// A creation the command does not spell out itself, because it lives in
|
|
413
|
+
// a file the command runs or submits. The conjunction may straddle the
|
|
414
|
+
// two: `curl <endpoint> --data-binary @payload.json` keeps the endpoint
|
|
415
|
+
// in argv and the mutation in the file.
|
|
416
|
+
const fromFile = inline
|
|
417
|
+
? undefined
|
|
418
|
+
: (await namedFiles({ text: command })).flatMap(file => {
|
|
419
|
+
const signature =
|
|
420
|
+
CREATION_SIGNATURES.find(entry => entry.re.test(file.text))
|
|
421
|
+
?.name ??
|
|
422
|
+
(GRAPHQL_CREATE.test(file.text) &&
|
|
423
|
+
(TRACKER_ENDPOINT.test(file.text) ||
|
|
424
|
+
TRACKER_ENDPOINT.test(command))
|
|
425
|
+
? "a tracker creation"
|
|
426
|
+
: undefined);
|
|
427
|
+
if (signature === undefined) return [];
|
|
428
|
+
// WHOSE vocabulary answers is decided by where the create inside
|
|
429
|
+
// the FILE is addressed, not by the command that ran it. `bash
|
|
430
|
+
// create.sh` names no repository, so reading the target from the
|
|
431
|
+
// command let a script doing `gh issue create --repo <other>` be
|
|
432
|
+
// waved through by this project's own lifecycle role — a role the
|
|
433
|
+
// other repository's build queue never reads.
|
|
434
|
+
const fileTarget = targetRepository({ declarable: file.text });
|
|
435
|
+
const scoped = rolesFor(policy, fileTarget);
|
|
436
|
+
const fileRoleOk =
|
|
437
|
+
policy.stateRoleTracker && scoped.named === undefined;
|
|
438
|
+
// A label rather than a marker, and the looseness is forced by
|
|
439
|
+
// this port's stated invariant. It matches raw text rather than
|
|
440
|
+
// tokenising, so it cannot attribute a label to ONE create inside
|
|
441
|
+
// a file. Accepting only whole-scope markers would refuse an
|
|
442
|
+
// honest single-create script that carries its label — a false
|
|
443
|
+
// refusal on the honest path, the failure mode this guard exists
|
|
444
|
+
// to remove. The cost is a decoy: a labelled create alongside an
|
|
445
|
+
// undeclared one in the same file passes here. The canonical guard
|
|
446
|
+
// refuses that, and this port is documented as never being
|
|
447
|
+
// STRICTER than the canonical guard, only ever looser.
|
|
448
|
+
if (declaresFor(file.text, scoped.roles, fileRoleOk)) return [];
|
|
449
|
+
return [`${signature} inside ${file.path}`];
|
|
450
|
+
})[0];
|
|
451
|
+
const signatureName = inline?.name ?? fromFile;
|
|
452
|
+
if (signatureName === undefined) return;
|
|
453
|
+
if (declares(declarable)) return;
|
|
310
454
|
if (named !== undefined)
|
|
311
455
|
throw new Error(
|
|
312
456
|
[
|
|
313
|
-
`block-direct-issue-create: refusing ${
|
|
457
|
+
`block-direct-issue-create: refusing ${signatureName} — this filing declares no readiness.`,
|
|
314
458
|
"",
|
|
315
459
|
"WHY: a work item filed without the build-ready role is an incomplete",
|
|
316
460
|
"handoff. Build-intake scans the ready lane and nothing else, so nothing",
|
|
@@ -347,7 +491,7 @@ const LisaBlockDirectIssueCreate = async () => {
|
|
|
347
491
|
);
|
|
348
492
|
throw new Error(
|
|
349
493
|
[
|
|
350
|
-
`block-direct-issue-create: refusing ${
|
|
494
|
+
`block-direct-issue-create: refusing ${signatureName} — this filing declares no readiness.`,
|
|
351
495
|
"",
|
|
352
496
|
"WHY: a work item filed without the build-ready role is an incomplete",
|
|
353
497
|
"handoff. Build-intake scans the ready lane and nothing else, so nothing",
|
|
@@ -369,6 +513,23 @@ const LisaBlockDirectIssueCreate = async () => {
|
|
|
369
513
|
"If you must run the CLI directly, the command has to carry one of the",
|
|
370
514
|
`two declarations itself: the configured build-ready role "${policy.readyRole}",`,
|
|
371
515
|
`or a ${HUMAN_GATE_MARKER} marker in the body it submits.`,
|
|
516
|
+
...(policy.stateRoleTracker
|
|
517
|
+
? [
|
|
518
|
+
"",
|
|
519
|
+
`Your role "${policy.readyRole}" is a workflow STATE, not a label, and the`,
|
|
520
|
+
"mandated client is curl, which has no flag that carries a state. So",
|
|
521
|
+
"declare the lifecycle role the access layer resolves the state from:",
|
|
522
|
+
"",
|
|
523
|
+
" LIFECYCLE_ROLE=ready curl -sS -X POST <the tracker endpoint> …",
|
|
524
|
+
"",
|
|
525
|
+
'or lifecycle_role:"ready" in the request payload, or a --state /',
|
|
526
|
+
"--status flag where the CLI has one.",
|
|
527
|
+
]
|
|
528
|
+
: []),
|
|
529
|
+
"",
|
|
530
|
+
"WHERE THE DECLARATION IS READ FROM: the command, and the contents of any",
|
|
531
|
+
"file it runs or submits. Moving the create into a script no longer moves",
|
|
532
|
+
"it out of sight, so the declaration can live wherever the create does.",
|
|
372
533
|
"",
|
|
373
534
|
"OPERATOR ESCAPE: a human can export LISA_ALLOW_DIRECT_ISSUE_CREATE=1 in",
|
|
374
535
|
"the environment before starting the session. Setting it inline on this",
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"freshness_hours": 36,
|
|
3
|
+
"bypass_max_hours": 24,
|
|
4
|
+
"suites": [
|
|
5
|
+
{
|
|
6
|
+
"label": "🎭 Playwright Web E2E",
|
|
7
|
+
"workflow": "playwright-e2e.yml",
|
|
8
|
+
"match": { "mode": "run" }
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"label": "📱 Maestro Native E2E",
|
|
12
|
+
"workflow": "maestro-e2e.yml",
|
|
13
|
+
"match": { "mode": "run" }
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
}
|
|
@@ -85,6 +85,33 @@ concurrency:
|
|
|
85
85
|
cancel-in-progress: true
|
|
86
86
|
|
|
87
87
|
jobs:
|
|
88
|
+
policy:
|
|
89
|
+
name: Read nightly E2E policy
|
|
90
|
+
runs-on: ubuntu-latest
|
|
91
|
+
timeout-minutes: 2
|
|
92
|
+
outputs:
|
|
93
|
+
suites: ${{ steps.policy.outputs.suites }}
|
|
94
|
+
freshness_hours: ${{ steps.policy.outputs.freshness_hours }}
|
|
95
|
+
bypass_max_hours: ${{ steps.policy.outputs.bypass_max_hours }}
|
|
96
|
+
steps:
|
|
97
|
+
- name: Check out the policy
|
|
98
|
+
uses: actions/checkout@v6
|
|
99
|
+
with:
|
|
100
|
+
persist-credentials: false
|
|
101
|
+
- name: Read the health suite table
|
|
102
|
+
id: policy
|
|
103
|
+
run: |
|
|
104
|
+
set -euo pipefail
|
|
105
|
+
file='.github/nightly-e2e-policy.json'
|
|
106
|
+
test -f "$file"
|
|
107
|
+
suites="$(jq -c '.suites' "$file")"
|
|
108
|
+
test "$(printf '%s' "$suites" | jq 'length')" -gt 0
|
|
109
|
+
printf 'suites=%s\n' "$suites" >> "$GITHUB_OUTPUT"
|
|
110
|
+
jq -er '.freshness_hours | numbers' "$file" \
|
|
111
|
+
| sed 's/^/freshness_hours=/' >> "$GITHUB_OUTPUT"
|
|
112
|
+
jq -er '.bypass_max_hours | numbers' "$file" \
|
|
113
|
+
| sed 's/^/bypass_max_hours=/' >> "$GITHUB_OUTPUT"
|
|
114
|
+
|
|
88
115
|
# This job's `name:` is HALF of the required status-check context; the other
|
|
89
116
|
# half is the reusable's job name (`🌙 Gate`). GitHub composes them as
|
|
90
117
|
# `🌙 Nightly E2E Health / 🌙 Gate`. Pin both strings in a test.
|
|
@@ -102,6 +129,7 @@ jobs:
|
|
|
102
129
|
# describing this context as required anywhere.
|
|
103
130
|
health:
|
|
104
131
|
name: 🌙 Nightly E2E Health
|
|
132
|
+
needs: policy
|
|
105
133
|
# PIN AN IMMUTABLE REF. `@main` is not a supported pin for a merge gate: the
|
|
106
134
|
# thing that decides whether code may merge must not change under you
|
|
107
135
|
# between two runs of the same PR. Better still, pin the tag's commit SHA
|
|
@@ -189,14 +217,11 @@ jobs:
|
|
|
189
217
|
# `⚠️ scope unverified: this run published no executed-flow count`.
|
|
190
218
|
# Declaring `min_flows` is what turns that notice into a blocking question
|
|
191
219
|
# — and from then on an UNREADABLE count blocks too, which is the point.
|
|
192
|
-
suites:
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
"match": { "mode": "run" }
|
|
198
|
-
}
|
|
199
|
-
]
|
|
220
|
+
suites: ${{ needs.policy.outputs.suites }}
|
|
221
|
+
freshness_hours: >-
|
|
222
|
+
${{ fromJSON(needs.policy.outputs.freshness_hours) }}
|
|
223
|
+
bypass_max_hours: >-
|
|
224
|
+
${{ fromJSON(needs.policy.outputs.bypass_max_hours) }}
|
|
200
225
|
# A TIME-BOXED grace period. While this is in the future, MISSING evidence
|
|
201
226
|
# reports but does not block, and every PR shows the expiry. A red suite
|
|
202
227
|
# blocks inside the window too. Set it, do not remove it — and do not push
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Seeded by Lisa on first setup — this file is YOURS.
|
|
2
|
+
# Lisa will not overwrite it. (copy-overwrite assets ARE replaced each run.)
|
|
3
|
+
name: 📌 Nightly E2E Condition Tracking
|
|
4
|
+
|
|
5
|
+
on:
|
|
6
|
+
workflow_run:
|
|
7
|
+
workflows:
|
|
8
|
+
- 🎭 Playwright Web E2E
|
|
9
|
+
- 📱 Maestro Native E2E
|
|
10
|
+
types: [completed]
|
|
11
|
+
|
|
12
|
+
permissions:
|
|
13
|
+
contents: read
|
|
14
|
+
actions: read
|
|
15
|
+
issues: write
|
|
16
|
+
|
|
17
|
+
concurrency:
|
|
18
|
+
group: nightly-e2e-tracking-${{ github.repository }}
|
|
19
|
+
cancel-in-progress: false
|
|
20
|
+
|
|
21
|
+
jobs:
|
|
22
|
+
track:
|
|
23
|
+
if: >-
|
|
24
|
+
github.event.workflow_run.event == 'schedule' &&
|
|
25
|
+
github.event.workflow_run.head_branch ==
|
|
26
|
+
github.event.repository.default_branch &&
|
|
27
|
+
github.event.workflow_run.head_repository.full_name == github.repository
|
|
28
|
+
uses: >-
|
|
29
|
+
CodySwannGT/lisa/.github/workflows/nightly-e2e-tracking.yml@08628ca0d2db045d3b0d87fcad8e444565836ff9
|
|
30
|
+
secrets: inherit
|
package/package.json
CHANGED
|
@@ -162,7 +162,7 @@
|
|
|
162
162
|
"zod-validation-error": "^4.0.0"
|
|
163
163
|
},
|
|
164
164
|
"name": "@codyswann/lisa",
|
|
165
|
-
"version": "4.27.
|
|
165
|
+
"version": "4.27.8",
|
|
166
166
|
"description": "Claude Code governance framework that applies guardrails, guidance, and automated enforcement to projects",
|
|
167
167
|
"main": "dist/index.js",
|
|
168
168
|
"exports": {
|
|
@@ -306,6 +306,6 @@
|
|
|
306
306
|
"test": "tests"
|
|
307
307
|
},
|
|
308
308
|
"types": "./dist/index.d.ts",
|
|
309
|
-
"lisaReleaseCommit": "
|
|
310
|
-
"gitHead": "
|
|
309
|
+
"lisaReleaseCommit": "6a87f9561279e498af1099f327f4c0c321a4e387",
|
|
310
|
+
"gitHead": "6a87f9561279e498af1099f327f4c0c321a4e387"
|
|
311
311
|
}
|
|
@@ -122,17 +122,21 @@ Record **exactly one** outcome per invocation through the run-record CLI, naming
|
|
|
122
122
|
specific, actionable, e.g. `Explored 4 personas; nothing confusing to file.` — or, when a decline suppressed the only candidates, `Explored 4 personas; 2 candidates suppressed by a prior decline — nothing new to propose.` — for `nothing-needed`; and for a `recovery-required` from an unreadable decline check, `Tracker unreachable during the decline check — restore credentials; nothing was filed this run.`):
|
|
123
123
|
|
|
124
124
|
```bash
|
|
125
|
-
node "${CLAUDE_PLUGIN_ROOT}/scripts/automation-run-record.mjs" \
|
|
125
|
+
node "${CLAUDE_PLUGIN_ROOT:-node_modules/@codyswann/lisa/plugins/lisa}/scripts/automation-run-record.mjs" \
|
|
126
126
|
--loop-id exploratory-bugs --outcome candidate-proposed \
|
|
127
127
|
--summary "Explored 4 personas; filed 3 findings from the checkout flow — awaiting your flip to ready." \
|
|
128
128
|
--runbook .lisa/automations/exploratory-bugs.runbook.md [--ref <ticket-url>]...
|
|
129
129
|
```
|
|
130
130
|
|
|
131
|
-
|
|
132
|
-
`
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
131
|
+
The `:-` default in that command is load-bearing, not decoration. `CLAUDE_PLUGIN_ROOT` is **not
|
|
132
|
+
exported into an agent's Bash tool environment**, so the bare `"${CLAUDE_PLUGIN_ROOT}/scripts/…"`
|
|
133
|
+
form expands to an absolute `/scripts/…` and exits 1. The default names the installed package copy,
|
|
134
|
+
which resolves from the **consumer repository root** — which is where the recorder is actually
|
|
135
|
+
invoked. Never substitute a bare `plugins/lisa/scripts/…` or `plugins/src/base/scripts/…` path:
|
|
136
|
+
those are relative to the **Lisa package root**, not the repository you are standing in, and exit 1
|
|
137
|
+
from there. If recording still fails, **degrade, never abort** (per `automation-runbook-contract`):
|
|
138
|
+
note the recording failure in the run output and finish the cycle — a recording failure is a
|
|
139
|
+
degradation to report, never a reason to block the loop.
|
|
136
140
|
|
|
137
141
|
**Retirement evaluation (every run).** Evaluate this loop's runbook **Retirement condition** on
|
|
138
142
|
every run, exactly as the `automation-runbook-contract` rule's Retirement section defines it — this
|
|
@@ -145,33 +145,38 @@ Record **exactly one** outcome per invocation through the run-record CLI, naming
|
|
|
145
145
|
specific, actionable, e.g. `Scanned 12 ready items; nothing to propose.` for `nothing-needed`):
|
|
146
146
|
|
|
147
147
|
```bash
|
|
148
|
-
node "${CLAUDE_PLUGIN_ROOT}/scripts/automation-run-record.mjs" \
|
|
148
|
+
node "${CLAUDE_PLUGIN_ROOT:-node_modules/@codyswann/lisa/plugins/lisa}/scripts/automation-run-record.mjs" \
|
|
149
149
|
--loop-id intake-tickets --outcome candidate-proposed \
|
|
150
150
|
--summary "Routed PRD #1810 to Blocked with clarifying questions; the run succeeded." \
|
|
151
151
|
--runbook .lisa/automations/intake-tickets.runbook.md [--ref <item-url>]...
|
|
152
152
|
```
|
|
153
153
|
|
|
154
154
|
**A dry build lane must say what it looked in.** A `nothing-needed` run on `intake-tickets`
|
|
155
|
-
additionally passes `--denominator` — the JSON from `buildIntakeDenominator()` in
|
|
156
|
-
`
|
|
157
|
-
|
|
158
|
-
pre-work rows, and every one of those
|
|
159
|
-
|
|
160
|
-
set is what turns the silent wrong
|
|
155
|
+
additionally passes `--denominator` — the JSON from `buildIntakeDenominator()` in the plugin
|
|
156
|
+
scripts directory's `intake-prework-denominator.mjs`, resolved exactly like the recorder below —
|
|
157
|
+
and the recorder **refuses the row without it**. This is not decoration: 31 consecutive cycles
|
|
158
|
+
reported a dry lane against a queue holding 61 unswept pre-work rows, and every one of those
|
|
159
|
+
records was honest. A missing row is noisy on inspection; a wrong denominator is silent forever
|
|
160
|
+
and looks exactly like a healthy dry queue. Stating the swept set is what turns the silent wrong
|
|
161
|
+
answer into an inspectable one (#2657).
|
|
161
162
|
|
|
162
163
|
```bash
|
|
163
|
-
node "${CLAUDE_PLUGIN_ROOT}/scripts/automation-run-record.mjs" \
|
|
164
|
+
node "${CLAUDE_PLUGIN_ROOT:-node_modules/@codyswann/lisa/plugins/lisa}/scripts/automation-run-record.mjs" \
|
|
164
165
|
--loop-id intake-tickets --outcome nothing-needed \
|
|
165
166
|
--summary "Nothing ready to build on team ENG. Looked in every lane holding work that has not started — Backlog (20), Todo (17), Ready (2), Blocked (61) — 100 items checked out of 343 still open." \
|
|
166
167
|
--denominator "$DENOMINATOR_JSON" \
|
|
167
168
|
--runbook .lisa/automations/intake-tickets.runbook.md
|
|
168
169
|
```
|
|
169
170
|
|
|
170
|
-
|
|
171
|
-
`
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
171
|
+
The `:-` default in that command is load-bearing, not decoration. `CLAUDE_PLUGIN_ROOT` is **not
|
|
172
|
+
exported into an agent's Bash tool environment**, so the bare `"${CLAUDE_PLUGIN_ROOT}/scripts/…"`
|
|
173
|
+
form expands to an absolute `/scripts/…` and exits 1. The default names the installed package copy,
|
|
174
|
+
which resolves from the **consumer repository root** — which is where the recorder is actually
|
|
175
|
+
invoked. Never substitute a bare `plugins/lisa/scripts/…` or `plugins/src/base/scripts/…` path:
|
|
176
|
+
those are relative to the **Lisa package root**, not the repository you are standing in, and exit 1
|
|
177
|
+
from there. If recording still fails, **degrade, never abort** (per `automation-runbook-contract`):
|
|
178
|
+
note the recording failure in the run output and finish the cycle — a recording failure is a
|
|
179
|
+
degradation to report, never a reason to block the loop.
|
|
175
180
|
|
|
176
181
|
**Retirement evaluation (every run).** Both loop-ids this skill backs are **structural to the
|
|
177
182
|
factory — they do not retire.** Their runbooks say so plainly instead of leaving the Retirement
|
|
@@ -368,18 +368,24 @@ loop's runbook (the `--summary` is the operator-readable one-liner in the
|
|
|
368
368
|
contract's exemplar voice — plain, specific, actionable):
|
|
369
369
|
|
|
370
370
|
```bash
|
|
371
|
-
node "${CLAUDE_PLUGIN_ROOT}/scripts/automation-run-record.mjs" \
|
|
371
|
+
node "${CLAUDE_PLUGIN_ROOT:-node_modules/@codyswann/lisa/plugins/lisa}/scripts/automation-run-record.mjs" \
|
|
372
372
|
--loop-id learnings-audit --outcome candidate-proposed \
|
|
373
373
|
--summary "Proposed 2 promotions and 1 batch confirm; awaiting your flip to ready." \
|
|
374
374
|
--runbook .lisa/automations/learnings-audit.runbook.md [--ref <ticket-url>]...
|
|
375
375
|
```
|
|
376
376
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
the
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
377
|
+
The `:-` default in that command is load-bearing, not decoration.
|
|
378
|
+
`CLAUDE_PLUGIN_ROOT` is **not exported into an agent's Bash tool
|
|
379
|
+
environment**, so the bare `"${CLAUDE_PLUGIN_ROOT}/scripts/…"` form expands
|
|
380
|
+
to an absolute `/scripts/…` and exits 1. The default names the installed
|
|
381
|
+
package copy, which resolves from the **consumer repository root** — which
|
|
382
|
+
is where the recorder is actually invoked. Never substitute a bare
|
|
383
|
+
`plugins/lisa/scripts/…` or `plugins/src/base/scripts/…` path: those are
|
|
384
|
+
relative to the **Lisa package root**, not the repository you are standing
|
|
385
|
+
in, and exit 1 from there. If recording still fails, **degrade, never
|
|
386
|
+
abort** (per `automation-runbook-contract`): note the recording failure in
|
|
387
|
+
the run output and finish the run — a recording failure is a degradation to
|
|
388
|
+
report, never a reason to block the loop.
|
|
383
389
|
|
|
384
390
|
## Retirement condition
|
|
385
391
|
|
|
@@ -140,13 +140,15 @@ Build the denominator with the shared helper, which owns the type vocabulary so
|
|
|
140
140
|
|
|
141
141
|
```bash
|
|
142
142
|
node -e '
|
|
143
|
-
import("'"${CLAUDE_PLUGIN_ROOT
|
|
143
|
+
import("'"${CLAUDE_PLUGIN_ROOT:-./node_modules/@codyswann/lisa/plugins/lisa}"'/scripts/intake-prework-denominator.mjs").then(m => {
|
|
144
144
|
const d = m.buildIntakeDenominator({ lanes: JSON.parse(process.argv[1]), totalOpen: Number(process.argv[2]) });
|
|
145
145
|
console.log(JSON.stringify(d));
|
|
146
146
|
console.log(m.summarizeDryLane(d, { queue: process.argv[3] }));
|
|
147
147
|
});' "$LANES_JSON" "$TOTAL_OPEN" "team $TEAM_KEY"
|
|
148
148
|
```
|
|
149
149
|
|
|
150
|
+
The `./` on that default is load-bearing and is not the same as the recorder's. `import()` reads a bare `node_modules/…` as a **package specifier** named `node_modules` and fails with `ERR_MODULE_NOT_FOUND: Cannot find package 'node_modules'`; only `./`, `../`, `/` or a `file:` URL is a path. A `node <path>` command line has no such rule, which is why the run-recorder default alongside this one carries no `./`.
|
|
151
|
+
|
|
150
152
|
`$LANES_JSON` is `[{"name":"<state>","type":"<state.type>","position":<state.position>,"count":<open rows>}, …]` for **every** state on the team, pre-work and not — the helper does the selecting.
|
|
151
153
|
|
|
152
154
|
**Candidate order.** Work `$READY` first (it is the human-flipped signal), then the remaining pre-work lanes oldest-first. Every candidate outside `$READY` must clear Phase 2.5 before it is treated as a candidate at all.
|
|
@@ -171,17 +171,21 @@ plain, specific, actionable, e.g. `Health green; audit clean — nothing to prop
|
|
|
171
171
|
unreachable during the decline check — restore credentials; nothing was filed this run.`):
|
|
172
172
|
|
|
173
173
|
```bash
|
|
174
|
-
node "${CLAUDE_PLUGIN_ROOT}/scripts/automation-run-record.mjs" \
|
|
174
|
+
node "${CLAUDE_PLUGIN_ROOT:-node_modules/@codyswann/lisa/plugins/lisa}/scripts/automation-run-record.mjs" \
|
|
175
175
|
--loop-id monitor --outcome candidate-proposed \
|
|
176
176
|
--summary "Filed #1810 for the p99 latency spike; awaiting your flip to ready." \
|
|
177
177
|
--runbook .lisa/automations/monitor.runbook.md [--ref <ticket-url>]...
|
|
178
178
|
```
|
|
179
179
|
|
|
180
|
-
|
|
181
|
-
`
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
180
|
+
The `:-` default in that command is load-bearing, not decoration. `CLAUDE_PLUGIN_ROOT` is **not
|
|
181
|
+
exported into an agent's Bash tool environment**, so the bare `"${CLAUDE_PLUGIN_ROOT}/scripts/…"`
|
|
182
|
+
form expands to an absolute `/scripts/…` and exits 1. The default names the installed package copy,
|
|
183
|
+
which resolves from the **consumer repository root** — which is where the recorder is actually
|
|
184
|
+
invoked. Never substitute a bare `plugins/lisa/scripts/…` or `plugins/src/base/scripts/…` path:
|
|
185
|
+
those are relative to the **Lisa package root**, not the repository you are standing in, and exit 1
|
|
186
|
+
from there. If recording still fails, **degrade, never abort** (per `automation-runbook-contract`):
|
|
187
|
+
note the recording failure in the run output and finish the cycle — a recording failure is a
|
|
188
|
+
degradation to report, never a reason to block the loop.
|
|
185
189
|
|
|
186
190
|
**Retirement evaluation (every run).** Evaluate this loop's runbook **Retirement condition** on
|
|
187
191
|
every standalone run, exactly as the `automation-runbook-contract` rule's Retirement section defines
|
|
@@ -321,17 +321,21 @@ propose.` for `nothing-needed`; and for a `recovery-required` from an unreadable
|
|
|
321
321
|
`Tracker unreachable during the decline check — restore credentials; nothing was filed this run.`):
|
|
322
322
|
|
|
323
323
|
```bash
|
|
324
|
-
node "${CLAUDE_PLUGIN_ROOT}/scripts/automation-run-record.mjs" \
|
|
324
|
+
node "${CLAUDE_PLUGIN_ROOT:-node_modules/@codyswann/lisa/plugins/lisa}/scripts/automation-run-record.mjs" \
|
|
325
325
|
--loop-id exploratory-prds --outcome candidate-proposed \
|
|
326
326
|
--summary "Created PRD #1810 for offline export; awaiting your flip to ready." \
|
|
327
327
|
--runbook .lisa/automations/exploratory-prds.runbook.md [--ref <prd-url>]...
|
|
328
328
|
```
|
|
329
329
|
|
|
330
|
-
|
|
331
|
-
`
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
330
|
+
The `:-` default in that command is load-bearing, not decoration. `CLAUDE_PLUGIN_ROOT` is **not
|
|
331
|
+
exported into an agent's Bash tool environment**, so the bare `"${CLAUDE_PLUGIN_ROOT}/scripts/…"`
|
|
332
|
+
form expands to an absolute `/scripts/…` and exits 1. The default names the installed package copy,
|
|
333
|
+
which resolves from the **consumer repository root** — which is where the recorder is actually
|
|
334
|
+
invoked. Never substitute a bare `plugins/lisa/scripts/…` or `plugins/src/base/scripts/…` path:
|
|
335
|
+
those are relative to the **Lisa package root**, not the repository you are standing in, and exit 1
|
|
336
|
+
from there. If recording still fails, **degrade, never abort** (per `automation-runbook-contract`):
|
|
337
|
+
note the recording failure in the run output and finish the run — a recording failure is a
|
|
338
|
+
degradation to report, never a reason to block the loop.
|
|
335
339
|
|
|
336
340
|
**Retirement evaluation (every run).** Evaluate this loop's runbook **Retirement condition** on
|
|
337
341
|
every run, exactly as the `automation-runbook-contract` rule's Retirement section defines it — this
|