@codyswann/lisa 4.42.6 → 4.42.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 (150) hide show
  1. package/README.md +4 -0
  2. package/all/copy-overwrite/scripts/check-orphaned-branches.mjs +7 -0
  3. package/all/copy-overwrite/scripts/lib/process-tree-runner.mjs +12 -2
  4. package/all/copy-overwrite/scripts/lisa-cross-worktree-guard.mjs +4 -0
  5. package/all/copy-overwrite/scripts/lisa-environment-prepare.mjs +2 -0
  6. package/all/copy-overwrite/scripts/lisa-hooks/worktree-binding-guard.mjs +31 -3
  7. package/all/copy-overwrite/scripts/lisa-run-gates.mjs +4 -0
  8. package/all/copy-overwrite/scripts/lisa-work-item.mjs +13 -0
  9. package/dist/configs/vitest/base.d.ts +1 -1
  10. package/dist/configs/vitest/base.d.ts.map +1 -1
  11. package/dist/configs/vitest/base.js +1 -0
  12. package/dist/configs/vitest/base.js.map +1 -1
  13. package/dist/configs/vitest/cdk.d.ts +2 -2
  14. package/dist/configs/vitest/cdk.d.ts.map +1 -1
  15. package/dist/configs/vitest/cdk.js +5 -2
  16. package/dist/configs/vitest/cdk.js.map +1 -1
  17. package/dist/configs/vitest/coverage-reports-directory.d.ts +47 -0
  18. package/dist/configs/vitest/coverage-reports-directory.d.ts.map +1 -0
  19. package/dist/configs/vitest/coverage-reports-directory.js +131 -0
  20. package/dist/configs/vitest/coverage-reports-directory.js.map +1 -0
  21. package/dist/configs/vitest/harper-fabric.d.ts +2 -2
  22. package/dist/configs/vitest/harper-fabric.d.ts.map +1 -1
  23. package/dist/configs/vitest/harper-fabric.js +5 -2
  24. package/dist/configs/vitest/harper-fabric.js.map +1 -1
  25. package/dist/configs/vitest/nestjs.d.ts +2 -2
  26. package/dist/configs/vitest/nestjs.d.ts.map +1 -1
  27. package/dist/configs/vitest/nestjs.js +5 -2
  28. package/dist/configs/vitest/nestjs.js.map +1 -1
  29. package/dist/configs/vitest/phaser.d.ts +2 -2
  30. package/dist/configs/vitest/phaser.d.ts.map +1 -1
  31. package/dist/configs/vitest/phaser.js +5 -2
  32. package/dist/configs/vitest/phaser.js.map +1 -1
  33. package/dist/configs/vitest/typescript.d.ts +2 -2
  34. package/dist/configs/vitest/typescript.d.ts.map +1 -1
  35. package/dist/configs/vitest/typescript.js +5 -2
  36. package/dist/configs/vitest/typescript.js.map +1 -1
  37. package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
  38. package/dist/core/lisa-owned-hash-ledger.js +24 -0
  39. package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
  40. package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
  41. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  42. package/dist/core/upstream-evidence-manifest.js +30 -23
  43. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  44. package/expo/copy-overwrite/scripts/bdd/baseline.mjs +26 -7
  45. package/package.json +5 -4
  46. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  47. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  48. package/plugins/lisa/.codex-plugin/skills/lisa-drive-pr-to-merge/SKILL.md +11 -0
  49. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/bootstrap-store.mjs +3 -0
  50. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/preflight-secrets.mjs +3 -0
  51. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/providers.mjs +4 -0
  52. package/plugins/lisa/.codex-plugin/skills/lisa-setup-workstation/scripts/workstation.mjs +3 -0
  53. package/plugins/lisa/hooks/threshold-ratchet.mjs +6 -0
  54. package/plugins/lisa/hooks/worktree-binding-guard.mjs +31 -3
  55. package/plugins/lisa/scripts/design-variable-ids.mjs +2 -0
  56. package/plugins/lisa/skills/lisa-drive-pr-to-merge/SKILL.md +11 -0
  57. package/plugins/lisa/skills/lisa-secrets-access/scripts/bootstrap-store.mjs +3 -0
  58. package/plugins/lisa/skills/lisa-secrets-access/scripts/preflight-secrets.mjs +3 -0
  59. package/plugins/lisa/skills/lisa-secrets-access/scripts/providers.mjs +4 -0
  60. package/plugins/lisa/skills/lisa-setup-workstation/scripts/workstation.mjs +3 -0
  61. package/plugins/lisa-agy/plugin.json +1 -1
  62. package/plugins/lisa-agy/scripts/design-variable-ids.mjs +2 -0
  63. package/plugins/lisa-agy/skills/lisa-drive-pr-to-merge/SKILL.md +11 -0
  64. package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/bootstrap-store.mjs +3 -0
  65. package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/preflight-secrets.mjs +3 -0
  66. package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/providers.mjs +4 -0
  67. package/plugins/lisa-agy/skills/lisa-setup-workstation/scripts/workstation.mjs +3 -0
  68. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  69. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  70. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  71. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  72. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  73. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  74. package/plugins/lisa-copilot/hooks/threshold-ratchet.mjs +6 -0
  75. package/plugins/lisa-copilot/hooks/worktree-binding-guard.mjs +31 -3
  76. package/plugins/lisa-copilot/scripts/design-variable-ids.mjs +2 -0
  77. package/plugins/lisa-copilot/skills/lisa-drive-pr-to-merge/SKILL.md +11 -0
  78. package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/bootstrap-store.mjs +3 -0
  79. package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/preflight-secrets.mjs +3 -0
  80. package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/providers.mjs +4 -0
  81. package/plugins/lisa-copilot/skills/lisa-setup-workstation/scripts/workstation.mjs +3 -0
  82. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  83. package/plugins/lisa-cursor/hooks/threshold-ratchet.mjs +6 -0
  84. package/plugins/lisa-cursor/hooks/worktree-binding-guard.mjs +31 -3
  85. package/plugins/lisa-cursor/scripts/design-variable-ids.mjs +2 -0
  86. package/plugins/lisa-cursor/skills/lisa-drive-pr-to-merge/SKILL.md +11 -0
  87. package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/bootstrap-store.mjs +3 -0
  88. package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/preflight-secrets.mjs +3 -0
  89. package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/providers.mjs +4 -0
  90. package/plugins/lisa-cursor/skills/lisa-setup-workstation/scripts/workstation.mjs +3 -0
  91. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  92. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  93. package/plugins/lisa-expo-agy/plugin.json +1 -1
  94. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  95. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  96. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  97. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  98. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  99. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  100. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  101. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  102. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  103. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  104. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  105. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  106. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  107. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  108. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  109. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  110. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  111. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  112. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  113. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  114. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  115. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  116. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  117. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  118. package/plugins/lisa-rails-agy/plugin.json +1 -1
  119. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  120. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  121. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  122. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  123. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  124. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  125. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  126. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  127. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  128. package/plugins/lisa-wiki/scripts/ingest-git.mjs +2 -0
  129. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  130. package/plugins/lisa-wiki-agy/scripts/ingest-git.mjs +2 -0
  131. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  132. package/plugins/lisa-wiki-copilot/scripts/ingest-git.mjs +2 -0
  133. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  134. package/plugins/lisa-wiki-cursor/scripts/ingest-git.mjs +2 -0
  135. package/plugins/src/base/hooks/threshold-ratchet.mjs +6 -0
  136. package/plugins/src/base/hooks/worktree-binding-guard.mjs +31 -3
  137. package/plugins/src/base/scripts/design-variable-ids.mjs +2 -0
  138. package/plugins/src/base/skills/lisa-drive-pr-to-merge/SKILL.md +11 -0
  139. package/plugins/src/base/skills/lisa-secrets-access/scripts/bootstrap-store.mjs +3 -0
  140. package/plugins/src/base/skills/lisa-secrets-access/scripts/preflight-secrets.mjs +3 -0
  141. package/plugins/src/base/skills/lisa-secrets-access/scripts/providers.mjs +4 -0
  142. package/plugins/src/base/skills/lisa-setup-workstation/scripts/workstation.mjs +3 -0
  143. package/plugins/src/wiki/scripts/ingest-git.mjs +2 -0
  144. package/rails/copy-overwrite/scripts/check-threshold-ratchet.mjs +6 -0
  145. package/scripts/check-merge-driver-registration.mjs +2 -0
  146. package/scripts/check-probe-absence-direction.mjs +611 -0
  147. package/scripts/install-generated-artifact-merge-driver.mjs +7 -1
  148. package/scripts/lib/shell-guard-refusal-coverage.mjs +18 -19
  149. package/typescript/copy-overwrite/scripts/check-threshold-ratchet.mjs +6 -0
  150. package/typescript/copy-overwrite/scripts/lisa-mutation.mjs +4 -0
@@ -143,9 +143,18 @@ export function showAtRevision(root, revision, relative) {
143
143
 
144
144
  /**
145
145
  * List the feature files that existed at a revision.
146
+ *
147
+ * Three-state on purpose. `git ls-tree` exits 0 with EMPTY output for a
148
+ * revision that genuinely holds no features, so a non-zero exit is always
149
+ * "could not ask" and never "there were none". Handing both back as `[]` let
150
+ * one failed listing read as an empty baseline, and an empty `before` set
151
+ * produces no `scenario-deleted` and no `coverage-regression` finding at all —
152
+ * so the gate reported that nothing regressed having compared against nothing.
153
+ * That is the same hole `loadBaseline` already closes for the coverage map,
154
+ * left open on the feature files beside it.
146
155
  * @param {string} root - Repo root.
147
156
  * @param {string} revision - Commit-ish.
148
- * @returns {string[]} Repo-relative feature paths.
157
+ * @returns {{ok: boolean, files: string[]}} Listing, and whether it was taken.
149
158
  */
150
159
  function featureFilesAt(root, revision) {
151
160
  const result = git(root, [
@@ -156,11 +165,16 @@ function featureFilesAt(root, revision) {
156
165
  "--",
157
166
  "bdd/features",
158
167
  ]);
159
- if (!result.ok) return [];
160
- return result.stdout
161
- .split("\n")
162
- .map(line => line.trim())
163
- .filter(line => line.endsWith(".feature"));
168
+ // probe-direction: fail-closed — `ok: false` reaches `loadBaseline`, which
169
+ // records the baseline UNAVAILABLE instead of empty.
170
+ if (!result.ok) return { ok: false, files: [] };
171
+ return {
172
+ ok: true,
173
+ files: result.stdout
174
+ .split("\n")
175
+ .map(line => line.trim())
176
+ .filter(line => line.endsWith(".feature")),
177
+ };
164
178
  }
165
179
 
166
180
  /**
@@ -198,12 +212,17 @@ export function loadBaseline(root, revision, headPlatforms = new Set()) {
198
212
  error = `bdd/coverage-map.json at that revision is not valid JSON: ${parseError.message}`;
199
213
  }
200
214
  }
215
+ const listing = featureFilesAt(root, revision);
201
216
  if (binary === null) {
202
217
  error = "git was not found, so no base revision could be read";
203
218
  } else if (!revisionExists) {
204
219
  error = "the requested base does not name a readable commit";
220
+ } else if (!listing.ok) {
221
+ error =
222
+ "the base revision's feature files could not be listed, so no scenario " +
223
+ "from it could be compared";
205
224
  }
206
- const documents = featureFilesAt(root, revision)
225
+ const documents = listing.files
207
226
  .map(file => ({ file, source: showAtRevision(root, revision, file) }))
208
227
  .filter(document => document.source !== null);
209
228
  const platforms = new Set([
package/package.json CHANGED
@@ -63,6 +63,7 @@
63
63
  "check:pipeline-status-reads": "node scripts/check-pipeline-status-reads.mjs",
64
64
  "check:whole-output-guards": "node scripts/check-whole-output-guards.mjs",
65
65
  "check:empty-subject-guards": "node scripts/check-empty-subject-guards.mjs",
66
+ "check:probe-absence-direction": "node scripts/check-probe-absence-direction.mjs",
66
67
  "check:shell-guard-refusals": "bun run build && $npm_execpath run lisa-test-run -- --adapter direct -- node scripts/check-shell-guard-refusal-coverage.mjs",
67
68
  "check:workflow-inputs": "node scripts/detect-stale-workflow-inputs.mjs --project . --project typescript/create-only --project rails/create-only --json",
68
69
  "check:conflict-markers": "node all/copy-overwrite/scripts/check-conflict-markers.mjs",
@@ -173,7 +174,7 @@
173
174
  "zod-validation-error": "^4.0.0"
174
175
  },
175
176
  "name": "@codyswann/lisa",
176
- "version": "4.42.6",
177
+ "version": "4.42.7",
177
178
  "description": "Claude Code governance framework that applies guardrails, guidance, and automated enforcement to projects",
178
179
  "main": "dist/index.js",
179
180
  "exports": {
@@ -321,7 +322,7 @@
321
322
  "test": "tests"
322
323
  },
323
324
  "types": "./dist/index.d.ts",
324
- "lisaReleaseCommit": "1148474ed60b8770215ec537dd4c949d259cda45",
325
- "gitHead": "1148474ed60b8770215ec537dd4c949d259cda45",
326
- "lisaReleaseTag": "v4.42.6"
325
+ "lisaReleaseCommit": "d968e062e6a11798f0d1f7c138d611a0531d975e",
326
+ "gitHead": "d968e062e6a11798f0d1f7c138d611a0531d975e",
327
+ "lisaReleaseTag": "v4.42.7"
327
328
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "4.42.6",
3
+ "version": "4.42.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.42.6",
3
+ "version": "4.42.7",
4
4
  "description": "Universal governance: agents, skills, commands, hooks, and rules for all projects.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -617,6 +617,17 @@ gh api "repos/<owner>/<repo>/actions/runs?head_sha=<head>" --jq .total_count
617
617
  problem. If CI were merely slow you would see runs QUEUED, not absent. Resolve
618
618
  the conflict and the runs appear; nothing else will make them appear.
619
619
 
620
+ **`<head>` must be the FULL 40-character SHA.** `head_sha=` does not match a
621
+ prefix and does not reject one: an abbreviated SHA returns a clean
622
+ `total_count: 0` — not a 404, not a validation error, an absence shaped exactly
623
+ like "no workflow ran". Measured on one pull request's own head: `77f223b9d` →
624
+ `total_count 0`, `77f223b9d8ffe7b172e9cac48b068b3bcac304c3` → `total_count 4`.
625
+ Take the SHA from `gh pr view <pr> --json headRefOid --jq .headRefOid`, never
626
+ from `git log --oneline` or a truncated line in a log or a UI. Copying a short
627
+ SHA here makes EVERY pull request read as having no CI, and the natural next
628
+ action — rebase and force-push — throws away the live run you could not see
629
+ (#3848).
630
+
620
631
  **`mergeable` is computed asynchronously.** GitHub returns `null` while it is
621
632
  still working it out, so a single read on a freshly-opened PR can say `null` on
622
633
  a perfectly clean branch. Treat `null` as "cannot tell yet" and re-read — never
@@ -218,6 +218,9 @@ export function hasBootstrap(key, deps = {}) {
218
218
  );
219
219
  return true;
220
220
  } catch {
221
+ // probe-direction: fail-closed — false means "no bootstrap value proven
222
+ // present", which makes the setup flow ASK for one. A keychain that cannot
223
+ // be queried costs a redundant prompt, never a skipped bootstrap.
221
224
  return false;
222
225
  }
223
226
  }
@@ -270,6 +270,9 @@ export function runProbe(substrate) {
270
270
  });
271
271
  return true;
272
272
  } catch {
273
+ // probe-direction: fail-closed — false makes the caller behave as if no
274
+ // substrate existed and fall through to the provider, which is the stricter
275
+ // path, exactly as the paragraph above states.
273
276
  return false;
274
277
  }
275
278
  }
@@ -178,6 +178,8 @@ function keychainServiceNames() {
178
178
  ([, name]) => name
179
179
  );
180
180
  } catch {
181
+ // probe-direction: neutral — the names only enrich a diagnostic message on a
182
+ // path that has already failed; no verdict reads them.
181
183
  // A locked, absent, or oversized keychain leaves the list empty.
182
184
  return [];
183
185
  }
@@ -253,6 +255,8 @@ export function providerCliPresent(prefix) {
253
255
  ).trim()
254
256
  );
255
257
  } catch {
258
+ // probe-direction: neutral — PATH presence is reported in the explanation of
259
+ // an already-failing lookup; it gates nothing on its own.
256
260
  return false;
257
261
  }
258
262
  }
@@ -66,6 +66,9 @@ export function locate(name, exec = boundedChildOutput) {
66
66
  exec("command", ["-v", name], { encoding: "utf8", shell: true })
67
67
  ).trim();
68
68
  } catch {
69
+ // probe-direction: fail-closed — null means "not proven present", and the
70
+ // setup flow responds by installing. A probe that cannot answer costs a
71
+ // redundant install, never a missing tool reported as present.
69
72
  return null;
70
73
  }
71
74
  }
@@ -121,6 +121,10 @@ function git(args, cwd) {
121
121
  timeout: 30_000,
122
122
  });
123
123
  } catch {
124
+ // probe-direction: fail-closed — null never reaches a verdict as "no
125
+ // weakening found". `changePlan` turns it into `undeterminable`, which
126
+ // REFUSES, and `isPromotion` falls to the strict reading that keeps the
127
+ // allow list pinned to the baseline.
124
128
  return null;
125
129
  }
126
130
  }
@@ -166,6 +170,8 @@ function resolvePlan(mode, root, baseRef, onlyFiles) {
166
170
  try {
167
171
  return fs.readFileSync(path.join(root, f), "utf-8");
168
172
  } catch {
173
+ // probe-direction: fail-closed — an unreadable current file compares as
174
+ // absent against the baseline, which is the strict side of the ratchet.
169
175
  return null;
170
176
  }
171
177
  },
@@ -96,14 +96,35 @@ function git(args, cwd) {
96
96
  stdio: ["ignore", "pipe", "ignore"],
97
97
  }).trim();
98
98
  } catch {
99
+ // probe-direction: neutral — this helper REPORTS, it does not decide. Both
100
+ // callers below read null and each one says out loud what it could not do
101
+ // before returning, so no gate outcome follows from this null silently
102
+ // (#3848). Keep that true: a caller added here that spends null as an
103
+ // answer has to say so at its own line.
99
104
  return null;
100
105
  }
101
106
  }
102
107
 
103
- /** The worktree root containing `cwd`, or null when `cwd` is not in a repo. */
108
+ /**
109
+ * The worktree root containing `cwd`, or null when `cwd` is not in a repo.
110
+ *
111
+ * `--show-toplevel` exits non-zero for two OPPOSITE reasons and hands back the
112
+ * same nothing for both: `cwd` is outside any repository — a real absence, and
113
+ * a call there is genuinely none of this guard's business — or git could not
114
+ * answer at all, where the binding is unproven rather than inapplicable. Only
115
+ * the second is a withdrawal of enforcement, and it is the one that must not
116
+ * pass for the first. `git --version` succeeds wherever git can run, so its
117
+ * failure is what separates them.
118
+ * @param {string} cwd Directory the tool call runs in.
119
+ * @returns {string|null} Absolute worktree root, or null.
120
+ */
104
121
  function worktreeRoot(cwd) {
105
122
  const top = git(["rev-parse", "--show-toplevel"], cwd);
106
- return top ? resolve(top) : null;
123
+ if (top) return resolve(top);
124
+ if (git(["--version"], cwd) === null) {
125
+ say("git could not be run here, so the worktree binding is NOT enforced");
126
+ }
127
+ return null;
107
128
  }
108
129
 
109
130
  /**
@@ -117,7 +138,14 @@ function mainCheckout(cwd) {
117
138
  ["rev-parse", "--path-format=absolute", "--git-common-dir"],
118
139
  cwd
119
140
  );
120
- return common ? dirname(resolve(common)) : null;
141
+ if (common) return dirname(resolve(common));
142
+ // Reached only for an `EnterWorktree` naming a worktree, which means a
143
+ // repository was already in hand — so failing here is "could not ask", and
144
+ // the consequence is that the claim goes unrecorded and the NEXT call's
145
+ // displacement check has nothing to compare against. Said out loud rather
146
+ // than returned as a quiet null (#3848).
147
+ say("git could not resolve the main checkout, so this claim is NOT recorded");
148
+ return null;
121
149
  }
122
150
 
123
151
  function stateFile(sessionId) {
@@ -298,6 +298,8 @@ async function callMcp(token, tool, args) {
298
298
  .map(part => part.text ?? "")
299
299
  .join("");
300
300
  } catch {
301
+ // probe-direction: neutral — this is a design-token lookup for a generator;
302
+ // no gate reads the result, and null renders as a missing token.
301
303
  return null;
302
304
  }
303
305
  }
@@ -617,6 +617,17 @@ gh api "repos/<owner>/<repo>/actions/runs?head_sha=<head>" --jq .total_count
617
617
  problem. If CI were merely slow you would see runs QUEUED, not absent. Resolve
618
618
  the conflict and the runs appear; nothing else will make them appear.
619
619
 
620
+ **`<head>` must be the FULL 40-character SHA.** `head_sha=` does not match a
621
+ prefix and does not reject one: an abbreviated SHA returns a clean
622
+ `total_count: 0` — not a 404, not a validation error, an absence shaped exactly
623
+ like "no workflow ran". Measured on one pull request's own head: `77f223b9d` →
624
+ `total_count 0`, `77f223b9d8ffe7b172e9cac48b068b3bcac304c3` → `total_count 4`.
625
+ Take the SHA from `gh pr view <pr> --json headRefOid --jq .headRefOid`, never
626
+ from `git log --oneline` or a truncated line in a log or a UI. Copying a short
627
+ SHA here makes EVERY pull request read as having no CI, and the natural next
628
+ action — rebase and force-push — throws away the live run you could not see
629
+ (#3848).
630
+
620
631
  **`mergeable` is computed asynchronously.** GitHub returns `null` while it is
621
632
  still working it out, so a single read on a freshly-opened PR can say `null` on
622
633
  a perfectly clean branch. Treat `null` as "cannot tell yet" and re-read — never
@@ -218,6 +218,9 @@ export function hasBootstrap(key, deps = {}) {
218
218
  );
219
219
  return true;
220
220
  } catch {
221
+ // probe-direction: fail-closed — false means "no bootstrap value proven
222
+ // present", which makes the setup flow ASK for one. A keychain that cannot
223
+ // be queried costs a redundant prompt, never a skipped bootstrap.
221
224
  return false;
222
225
  }
223
226
  }
@@ -270,6 +270,9 @@ export function runProbe(substrate) {
270
270
  });
271
271
  return true;
272
272
  } catch {
273
+ // probe-direction: fail-closed — false makes the caller behave as if no
274
+ // substrate existed and fall through to the provider, which is the stricter
275
+ // path, exactly as the paragraph above states.
273
276
  return false;
274
277
  }
275
278
  }
@@ -178,6 +178,8 @@ function keychainServiceNames() {
178
178
  ([, name]) => name
179
179
  );
180
180
  } catch {
181
+ // probe-direction: neutral — the names only enrich a diagnostic message on a
182
+ // path that has already failed; no verdict reads them.
181
183
  // A locked, absent, or oversized keychain leaves the list empty.
182
184
  return [];
183
185
  }
@@ -253,6 +255,8 @@ export function providerCliPresent(prefix) {
253
255
  ).trim()
254
256
  );
255
257
  } catch {
258
+ // probe-direction: neutral — PATH presence is reported in the explanation of
259
+ // an already-failing lookup; it gates nothing on its own.
256
260
  return false;
257
261
  }
258
262
  }
@@ -66,6 +66,9 @@ export function locate(name, exec = boundedChildOutput) {
66
66
  exec("command", ["-v", name], { encoding: "utf8", shell: true })
67
67
  ).trim();
68
68
  } catch {
69
+ // probe-direction: fail-closed — null means "not proven present", and the
70
+ // setup flow responds by installing. A probe that cannot answer costs a
71
+ // redundant install, never a missing tool reported as present.
69
72
  return null;
70
73
  }
71
74
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "4.42.6",
3
+ "version": "4.42.7",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -298,6 +298,8 @@ async function callMcp(token, tool, args) {
298
298
  .map(part => part.text ?? "")
299
299
  .join("");
300
300
  } catch {
301
+ // probe-direction: neutral — this is a design-token lookup for a generator;
302
+ // no gate reads the result, and null renders as a missing token.
301
303
  return null;
302
304
  }
303
305
  }
@@ -617,6 +617,17 @@ gh api "repos/<owner>/<repo>/actions/runs?head_sha=<head>" --jq .total_count
617
617
  problem. If CI were merely slow you would see runs QUEUED, not absent. Resolve
618
618
  the conflict and the runs appear; nothing else will make them appear.
619
619
 
620
+ **`<head>` must be the FULL 40-character SHA.** `head_sha=` does not match a
621
+ prefix and does not reject one: an abbreviated SHA returns a clean
622
+ `total_count: 0` — not a 404, not a validation error, an absence shaped exactly
623
+ like "no workflow ran". Measured on one pull request's own head: `77f223b9d` →
624
+ `total_count 0`, `77f223b9d8ffe7b172e9cac48b068b3bcac304c3` → `total_count 4`.
625
+ Take the SHA from `gh pr view <pr> --json headRefOid --jq .headRefOid`, never
626
+ from `git log --oneline` or a truncated line in a log or a UI. Copying a short
627
+ SHA here makes EVERY pull request read as having no CI, and the natural next
628
+ action — rebase and force-push — throws away the live run you could not see
629
+ (#3848).
630
+
620
631
  **`mergeable` is computed asynchronously.** GitHub returns `null` while it is
621
632
  still working it out, so a single read on a freshly-opened PR can say `null` on
622
633
  a perfectly clean branch. Treat `null` as "cannot tell yet" and re-read — never
@@ -218,6 +218,9 @@ export function hasBootstrap(key, deps = {}) {
218
218
  );
219
219
  return true;
220
220
  } catch {
221
+ // probe-direction: fail-closed — false means "no bootstrap value proven
222
+ // present", which makes the setup flow ASK for one. A keychain that cannot
223
+ // be queried costs a redundant prompt, never a skipped bootstrap.
221
224
  return false;
222
225
  }
223
226
  }
@@ -270,6 +270,9 @@ export function runProbe(substrate) {
270
270
  });
271
271
  return true;
272
272
  } catch {
273
+ // probe-direction: fail-closed — false makes the caller behave as if no
274
+ // substrate existed and fall through to the provider, which is the stricter
275
+ // path, exactly as the paragraph above states.
273
276
  return false;
274
277
  }
275
278
  }
@@ -178,6 +178,8 @@ function keychainServiceNames() {
178
178
  ([, name]) => name
179
179
  );
180
180
  } catch {
181
+ // probe-direction: neutral — the names only enrich a diagnostic message on a
182
+ // path that has already failed; no verdict reads them.
181
183
  // A locked, absent, or oversized keychain leaves the list empty.
182
184
  return [];
183
185
  }
@@ -253,6 +255,8 @@ export function providerCliPresent(prefix) {
253
255
  ).trim()
254
256
  );
255
257
  } catch {
258
+ // probe-direction: neutral — PATH presence is reported in the explanation of
259
+ // an already-failing lookup; it gates nothing on its own.
256
260
  return false;
257
261
  }
258
262
  }
@@ -66,6 +66,9 @@ export function locate(name, exec = boundedChildOutput) {
66
66
  exec("command", ["-v", name], { encoding: "utf8", shell: true })
67
67
  ).trim();
68
68
  } catch {
69
+ // probe-direction: fail-closed — null means "not proven present", and the
70
+ // setup flow responds by installing. A probe that cannot answer costs a
71
+ // redundant install, never a missing tool reported as present.
69
72
  return null;
70
73
  }
71
74
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "4.42.6",
3
+ "version": "4.42.7",
4
4
  "description": "AWS CDK-specific plugin",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "4.42.6",
3
+ "version": "4.42.7",
4
4
  "description": "AWS CDK-specific Lisa plugin.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "4.42.6",
3
+ "version": "4.42.7",
4
4
  "description": "AWS CDK-specific plugin",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "4.42.6",
3
+ "version": "4.42.7",
4
4
  "description": "AWS CDK-specific plugin",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "4.42.6",
3
+ "version": "4.42.7",
4
4
  "description": "AWS CDK-specific plugin",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "4.42.6",
3
+ "version": "4.42.7",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -121,6 +121,10 @@ function git(args, cwd) {
121
121
  timeout: 30_000,
122
122
  });
123
123
  } catch {
124
+ // probe-direction: fail-closed — null never reaches a verdict as "no
125
+ // weakening found". `changePlan` turns it into `undeterminable`, which
126
+ // REFUSES, and `isPromotion` falls to the strict reading that keeps the
127
+ // allow list pinned to the baseline.
124
128
  return null;
125
129
  }
126
130
  }
@@ -166,6 +170,8 @@ function resolvePlan(mode, root, baseRef, onlyFiles) {
166
170
  try {
167
171
  return fs.readFileSync(path.join(root, f), "utf-8");
168
172
  } catch {
173
+ // probe-direction: fail-closed — an unreadable current file compares as
174
+ // absent against the baseline, which is the strict side of the ratchet.
169
175
  return null;
170
176
  }
171
177
  },
@@ -96,14 +96,35 @@ function git(args, cwd) {
96
96
  stdio: ["ignore", "pipe", "ignore"],
97
97
  }).trim();
98
98
  } catch {
99
+ // probe-direction: neutral — this helper REPORTS, it does not decide. Both
100
+ // callers below read null and each one says out loud what it could not do
101
+ // before returning, so no gate outcome follows from this null silently
102
+ // (#3848). Keep that true: a caller added here that spends null as an
103
+ // answer has to say so at its own line.
99
104
  return null;
100
105
  }
101
106
  }
102
107
 
103
- /** The worktree root containing `cwd`, or null when `cwd` is not in a repo. */
108
+ /**
109
+ * The worktree root containing `cwd`, or null when `cwd` is not in a repo.
110
+ *
111
+ * `--show-toplevel` exits non-zero for two OPPOSITE reasons and hands back the
112
+ * same nothing for both: `cwd` is outside any repository — a real absence, and
113
+ * a call there is genuinely none of this guard's business — or git could not
114
+ * answer at all, where the binding is unproven rather than inapplicable. Only
115
+ * the second is a withdrawal of enforcement, and it is the one that must not
116
+ * pass for the first. `git --version` succeeds wherever git can run, so its
117
+ * failure is what separates them.
118
+ * @param {string} cwd Directory the tool call runs in.
119
+ * @returns {string|null} Absolute worktree root, or null.
120
+ */
104
121
  function worktreeRoot(cwd) {
105
122
  const top = git(["rev-parse", "--show-toplevel"], cwd);
106
- return top ? resolve(top) : null;
123
+ if (top) return resolve(top);
124
+ if (git(["--version"], cwd) === null) {
125
+ say("git could not be run here, so the worktree binding is NOT enforced");
126
+ }
127
+ return null;
107
128
  }
108
129
 
109
130
  /**
@@ -117,7 +138,14 @@ function mainCheckout(cwd) {
117
138
  ["rev-parse", "--path-format=absolute", "--git-common-dir"],
118
139
  cwd
119
140
  );
120
- return common ? dirname(resolve(common)) : null;
141
+ if (common) return dirname(resolve(common));
142
+ // Reached only for an `EnterWorktree` naming a worktree, which means a
143
+ // repository was already in hand — so failing here is "could not ask", and
144
+ // the consequence is that the claim goes unrecorded and the NEXT call's
145
+ // displacement check has nothing to compare against. Said out loud rather
146
+ // than returned as a quiet null (#3848).
147
+ say("git could not resolve the main checkout, so this claim is NOT recorded");
148
+ return null;
121
149
  }
122
150
 
123
151
  function stateFile(sessionId) {
@@ -298,6 +298,8 @@ async function callMcp(token, tool, args) {
298
298
  .map(part => part.text ?? "")
299
299
  .join("");
300
300
  } catch {
301
+ // probe-direction: neutral — this is a design-token lookup for a generator;
302
+ // no gate reads the result, and null renders as a missing token.
301
303
  return null;
302
304
  }
303
305
  }
@@ -617,6 +617,17 @@ gh api "repos/<owner>/<repo>/actions/runs?head_sha=<head>" --jq .total_count
617
617
  problem. If CI were merely slow you would see runs QUEUED, not absent. Resolve
618
618
  the conflict and the runs appear; nothing else will make them appear.
619
619
 
620
+ **`<head>` must be the FULL 40-character SHA.** `head_sha=` does not match a
621
+ prefix and does not reject one: an abbreviated SHA returns a clean
622
+ `total_count: 0` — not a 404, not a validation error, an absence shaped exactly
623
+ like "no workflow ran". Measured on one pull request's own head: `77f223b9d` →
624
+ `total_count 0`, `77f223b9d8ffe7b172e9cac48b068b3bcac304c3` → `total_count 4`.
625
+ Take the SHA from `gh pr view <pr> --json headRefOid --jq .headRefOid`, never
626
+ from `git log --oneline` or a truncated line in a log or a UI. Copying a short
627
+ SHA here makes EVERY pull request read as having no CI, and the natural next
628
+ action — rebase and force-push — throws away the live run you could not see
629
+ (#3848).
630
+
620
631
  **`mergeable` is computed asynchronously.** GitHub returns `null` while it is
621
632
  still working it out, so a single read on a freshly-opened PR can say `null` on
622
633
  a perfectly clean branch. Treat `null` as "cannot tell yet" and re-read — never
@@ -218,6 +218,9 @@ export function hasBootstrap(key, deps = {}) {
218
218
  );
219
219
  return true;
220
220
  } catch {
221
+ // probe-direction: fail-closed — false means "no bootstrap value proven
222
+ // present", which makes the setup flow ASK for one. A keychain that cannot
223
+ // be queried costs a redundant prompt, never a skipped bootstrap.
221
224
  return false;
222
225
  }
223
226
  }
@@ -270,6 +270,9 @@ export function runProbe(substrate) {
270
270
  });
271
271
  return true;
272
272
  } catch {
273
+ // probe-direction: fail-closed — false makes the caller behave as if no
274
+ // substrate existed and fall through to the provider, which is the stricter
275
+ // path, exactly as the paragraph above states.
273
276
  return false;
274
277
  }
275
278
  }
@@ -178,6 +178,8 @@ function keychainServiceNames() {
178
178
  ([, name]) => name
179
179
  );
180
180
  } catch {
181
+ // probe-direction: neutral — the names only enrich a diagnostic message on a
182
+ // path that has already failed; no verdict reads them.
181
183
  // A locked, absent, or oversized keychain leaves the list empty.
182
184
  return [];
183
185
  }
@@ -253,6 +255,8 @@ export function providerCliPresent(prefix) {
253
255
  ).trim()
254
256
  );
255
257
  } catch {
258
+ // probe-direction: neutral — PATH presence is reported in the explanation of
259
+ // an already-failing lookup; it gates nothing on its own.
256
260
  return false;
257
261
  }
258
262
  }
@@ -66,6 +66,9 @@ export function locate(name, exec = boundedChildOutput) {
66
66
  exec("command", ["-v", name], { encoding: "utf8", shell: true })
67
67
  ).trim();
68
68
  } catch {
69
+ // probe-direction: fail-closed — null means "not proven present", and the
70
+ // setup flow responds by installing. A probe that cannot answer costs a
71
+ // redundant install, never a missing tool reported as present.
69
72
  return null;
70
73
  }
71
74
  }