@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.
Files changed (127) hide show
  1. package/all/copy-overwrite/scripts/lisa-hooks/block-direct-issue-create.sh +507 -11
  2. package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
  3. package/dist/core/lisa-owned-hash-ledger.js +102 -0
  4. package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
  5. package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
  6. package/dist/core/project-config-nightly-e2e.d.ts +21 -0
  7. package/dist/core/project-config-nightly-e2e.d.ts.map +1 -0
  8. package/dist/core/project-config-nightly-e2e.js +43 -0
  9. package/dist/core/project-config-nightly-e2e.js.map +1 -0
  10. package/dist/core/project-config.d.ts +4 -0
  11. package/dist/core/project-config.d.ts.map +1 -1
  12. package/dist/core/project-config.js +3 -0
  13. package/dist/core/project-config.js.map +1 -1
  14. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  15. package/dist/core/upstream-evidence-manifest.js +56 -12
  16. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  17. package/dist/opencode/plugin-templates/lisa-block-direct-issue-create.ts +180 -19
  18. package/expo/create-only/.github/nightly-e2e-policy.json +16 -0
  19. package/expo/create-only/.github/workflows/nightly-e2e-health.yml +33 -8
  20. package/expo/create-only/.github/workflows/nightly-e2e-tracking.yml +30 -0
  21. package/package.json +3 -3
  22. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  23. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  24. package/plugins/lisa/.codex-plugin/skills/lisa-exploratory-qa/SKILL.md +10 -6
  25. package/plugins/lisa/.codex-plugin/skills/lisa-intake/SKILL.md +18 -13
  26. package/plugins/lisa/.codex-plugin/skills/lisa-learnings-audit/SKILL.md +13 -7
  27. package/plugins/lisa/.codex-plugin/skills/lisa-linear-build-intake/SKILL.md +3 -1
  28. package/plugins/lisa/.codex-plugin/skills/lisa-monitor/SKILL.md +10 -6
  29. package/plugins/lisa/.codex-plugin/skills/lisa-project-ideation/SKILL.md +10 -6
  30. package/plugins/lisa/.codex-plugin/skills/lisa-repair-intake/SKILL.md +10 -6
  31. package/plugins/lisa/hooks/block-direct-issue-create.sh +507 -11
  32. package/plugins/lisa/skills/lisa-exploratory-qa/SKILL.md +10 -6
  33. package/plugins/lisa/skills/lisa-intake/SKILL.md +18 -13
  34. package/plugins/lisa/skills/lisa-learnings-audit/SKILL.md +13 -7
  35. package/plugins/lisa/skills/lisa-linear-build-intake/SKILL.md +3 -1
  36. package/plugins/lisa/skills/lisa-monitor/SKILL.md +10 -6
  37. package/plugins/lisa/skills/lisa-project-ideation/SKILL.md +10 -6
  38. package/plugins/lisa/skills/lisa-repair-intake/SKILL.md +10 -6
  39. package/plugins/lisa-agy/hooks/block-direct-issue-create.sh +507 -11
  40. package/plugins/lisa-agy/plugin.json +1 -1
  41. package/plugins/lisa-agy/skills/lisa-exploratory-qa/SKILL.md +10 -6
  42. package/plugins/lisa-agy/skills/lisa-intake/SKILL.md +18 -13
  43. package/plugins/lisa-agy/skills/lisa-learnings-audit/SKILL.md +13 -7
  44. package/plugins/lisa-agy/skills/lisa-linear-build-intake/SKILL.md +3 -1
  45. package/plugins/lisa-agy/skills/lisa-monitor/SKILL.md +10 -6
  46. package/plugins/lisa-agy/skills/lisa-project-ideation/SKILL.md +10 -6
  47. package/plugins/lisa-agy/skills/lisa-repair-intake/SKILL.md +10 -6
  48. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  49. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  50. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  51. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  52. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  53. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  54. package/plugins/lisa-copilot/hooks/block-direct-issue-create.sh +507 -11
  55. package/plugins/lisa-copilot/skills/lisa-exploratory-qa/SKILL.md +10 -6
  56. package/plugins/lisa-copilot/skills/lisa-intake/SKILL.md +18 -13
  57. package/plugins/lisa-copilot/skills/lisa-learnings-audit/SKILL.md +13 -7
  58. package/plugins/lisa-copilot/skills/lisa-linear-build-intake/SKILL.md +3 -1
  59. package/plugins/lisa-copilot/skills/lisa-monitor/SKILL.md +10 -6
  60. package/plugins/lisa-copilot/skills/lisa-project-ideation/SKILL.md +10 -6
  61. package/plugins/lisa-copilot/skills/lisa-repair-intake/SKILL.md +10 -6
  62. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  63. package/plugins/lisa-cursor/hooks/block-direct-issue-create.sh +507 -11
  64. package/plugins/lisa-cursor/skills/lisa-exploratory-qa/SKILL.md +10 -6
  65. package/plugins/lisa-cursor/skills/lisa-intake/SKILL.md +18 -13
  66. package/plugins/lisa-cursor/skills/lisa-learnings-audit/SKILL.md +13 -7
  67. package/plugins/lisa-cursor/skills/lisa-linear-build-intake/SKILL.md +3 -1
  68. package/plugins/lisa-cursor/skills/lisa-monitor/SKILL.md +10 -6
  69. package/plugins/lisa-cursor/skills/lisa-project-ideation/SKILL.md +10 -6
  70. package/plugins/lisa-cursor/skills/lisa-repair-intake/SKILL.md +10 -6
  71. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  72. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  73. package/plugins/lisa-expo-agy/plugin.json +1 -1
  74. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  75. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  76. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  77. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  78. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  79. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  80. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  81. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  82. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  83. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  84. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  85. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  86. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  87. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  88. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  89. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  90. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  91. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  92. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  93. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  94. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  95. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  96. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  97. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  98. package/plugins/lisa-rails-agy/plugin.json +1 -1
  99. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  100. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  101. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  102. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  103. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  104. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  105. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  106. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  107. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  108. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  109. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  110. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  111. package/plugins/src/base/hooks/block-direct-issue-create.sh +507 -11
  112. package/plugins/src/base/skills/lisa-exploratory-qa/SKILL.md +10 -6
  113. package/plugins/src/base/skills/lisa-intake/SKILL.md +18 -13
  114. package/plugins/src/base/skills/lisa-learnings-audit/SKILL.md +13 -7
  115. package/plugins/src/base/skills/lisa-linear-build-intake/SKILL.md +3 -1
  116. package/plugins/src/base/skills/lisa-monitor/SKILL.md +10 -6
  117. package/plugins/src/base/skills/lisa-project-ideation/SKILL.md +10 -6
  118. package/plugins/src/base/skills/lisa-repair-intake/SKILL.md +10 -6
  119. package/scripts/two-channel-couplings.json +70 -5
  120. package/typescript/copy-overwrite/.prettierignore +7 -0
  121. package/typescript/copy-overwrite/scripts/nightly-e2e-provider-action.mjs +70 -0
  122. package/typescript/copy-overwrite/scripts/nightly-e2e-provider-github-jira.mjs +296 -0
  123. package/typescript/copy-overwrite/scripts/nightly-e2e-provider-linear-sentry.mjs +283 -0
  124. package/typescript/copy-overwrite/scripts/nightly-e2e-provider-support.mjs +55 -0
  125. package/typescript/copy-overwrite/scripts/nightly-e2e-tracking-config.mjs +149 -0
  126. package/typescript/copy-overwrite/scripts/reconcile-nightly-e2e-tracking-cli.mjs +56 -0
  127. 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. Two deliberate differences remain, both in the
21
- * permissive direction so this port can never refuse something the canonical
22
- * guard would allow:
23
- * - `--body-file` contents are not read, so an OpenCode caller declaring a
24
- * human gate puts the `[lisa-human-gate]` marker on the command line;
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 signature = CREATION_SIGNATURES.find(entry =>
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
- const declaresRole = [...declarable.matchAll(LABEL_FLAG)].some(match =>
304
- (match[2] ?? "")
305
- .split(",")
306
- .map(part => part.trim())
307
- .some(candidate => roles.includes(candidate))
308
- );
309
- if (declaresRole || declarable.includes(HUMAN_GATE_MARKER)) return;
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 ${signature.name} — this filing declares no readiness.`,
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 ${signature.name} — this filing declares no readiness.`,
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
- "label": "Maestro native e2e",
196
- "workflow": "maestro-e2e.yml",
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.6",
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": "ca4a03afc00b14c943a2b4eda506f4c9483ca4ec",
310
- "gitHead": "ca4a03afc00b14c943a2b4eda506f4c9483ca4ec"
309
+ "lisaReleaseCommit": "6a87f9561279e498af1099f327f4c0c321a4e387",
310
+ "gitHead": "6a87f9561279e498af1099f327f4c0c321a4e387"
311
311
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "4.27.6",
3
+ "version": "4.27.8",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "4.27.6",
3
+ "version": "4.27.8",
4
4
  "description": "Universal governance: agents, skills, commands, hooks, and rules for all projects.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -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
- If `${CLAUDE_PLUGIN_ROOT}` is unset, resolve the plugin scripts directory directly — the built copy
132
- `plugins/lisa/scripts/automation-run-record.mjs` or the source
133
- `plugins/src/base/scripts/automation-run-record.mjs`. If recording still fails, **degrade, never
134
- abort** (per `automation-runbook-contract`): note the recording failure in the run output and finish
135
- the cycle — a recording failure is a degradation to report, never a reason to block the loop.
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
- `scripts/intake-prework-denominator.mjs` — and the recorder **refuses the row without it**. This is
157
- not decoration: 31 consecutive cycles reported a dry lane against a queue holding 61 unswept
158
- pre-work rows, and every one of those records was honest. A missing row is noisy on inspection; a
159
- wrong denominator is silent forever and looks exactly like a healthy dry queue. Stating the swept
160
- set is what turns the silent wrong answer into an inspectable one (#2657).
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
- If `${CLAUDE_PLUGIN_ROOT}` is unset, resolve the plugin scripts directory directly — the built copy
171
- `plugins/lisa/scripts/automation-run-record.mjs` or the source
172
- `plugins/src/base/scripts/automation-run-record.mjs`. If recording still fails, **degrade, never
173
- abort** (per `automation-runbook-contract`): note the recording failure in the run output and finish
174
- the cycle — a recording failure is a degradation to report, never a reason to block the loop.
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
- If `${CLAUDE_PLUGIN_ROOT}` is unset, resolve the plugin scripts directory
378
- directly — the built copy `plugins/lisa/scripts/automation-run-record.mjs` or
379
- the source `plugins/src/base/scripts/automation-run-record.mjs`. If recording
380
- still fails, **degrade, never abort** (per `automation-runbook-contract`): note
381
- the recording failure in the run output and finish the run — a recording
382
- failure is a degradation to report, never a reason to block the loop.
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:-plugins/src/base}"'/scripts/intake-prework-denominator.mjs").then(m => {
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
- If `${CLAUDE_PLUGIN_ROOT}` is unset, resolve the plugin scripts directory directly — the built copy
181
- `plugins/lisa/scripts/automation-run-record.mjs` or the source
182
- `plugins/src/base/scripts/automation-run-record.mjs`. If recording still fails, **degrade, never
183
- abort** (per `automation-runbook-contract`): note the recording failure in the run output and finish
184
- the cycle — a recording failure is a degradation to report, never a reason to block the loop.
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
- If `${CLAUDE_PLUGIN_ROOT}` is unset, resolve the plugin scripts directory directly — the built copy
331
- `plugins/lisa/scripts/automation-run-record.mjs` or the source
332
- `plugins/src/base/scripts/automation-run-record.mjs`. If recording still fails, **degrade, never
333
- abort** (per `automation-runbook-contract`): note the recording failure in the run output and finish
334
- the run — a recording failure is a degradation to report, never a reason to block the loop.
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