@codyswann/lisa 4.27.6 → 4.27.7

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 (65) 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 +4 -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/upstream-evidence-manifest.d.ts.map +1 -1
  7. package/dist/core/upstream-evidence-manifest.js +4 -2
  8. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  9. package/dist/opencode/plugin-templates/lisa-block-direct-issue-create.ts +180 -19
  10. package/package.json +3 -3
  11. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  12. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  13. package/plugins/lisa/hooks/block-direct-issue-create.sh +507 -11
  14. package/plugins/lisa-agy/hooks/block-direct-issue-create.sh +507 -11
  15. package/plugins/lisa-agy/plugin.json +1 -1
  16. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  17. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  18. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  19. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  20. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  21. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  22. package/plugins/lisa-copilot/hooks/block-direct-issue-create.sh +507 -11
  23. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  24. package/plugins/lisa-cursor/hooks/block-direct-issue-create.sh +507 -11
  25. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  26. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  27. package/plugins/lisa-expo-agy/plugin.json +1 -1
  28. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  29. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  30. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  31. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  32. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  33. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  34. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  35. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  36. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  37. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  38. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  39. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  40. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  41. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  42. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  43. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  44. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  45. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  46. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  47. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  48. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  49. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  50. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  51. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  52. package/plugins/lisa-rails-agy/plugin.json +1 -1
  53. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  54. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  55. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  56. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  57. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  58. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  59. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  60. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  61. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  62. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  63. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  64. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  65. package/plugins/src/base/hooks/block-direct-issue-create.sh +507 -11
@@ -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",
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.7",
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": "96e367f6712d61b7951346f042d32dc066ccbfc7",
310
+ "gitHead": "96e367f6712d61b7951346f042d32dc066ccbfc7"
311
311
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "4.27.6",
3
+ "version": "4.27.7",
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.7",
4
4
  "description": "Universal governance: agents, skills, commands, hooks, and rules for all projects.",
5
5
  "author": {
6
6
  "name": "Cody Swann"