@codyswann/lisa 3.26.0 → 3.26.2

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 (161) hide show
  1. package/all/copy-overwrite/scripts/check-state-classification.mjs +31 -0
  2. package/all/copy-overwrite/scripts/lisa-gates.mjs +10 -1
  3. package/all/copy-overwrite/scripts/lisa-hooks/block-managed-file-edits.sh +136 -15
  4. package/dist/cli/doctor-lisa-owned-artifacts.d.ts.map +1 -1
  5. package/dist/cli/doctor-lisa-owned-artifacts.js +13 -2
  6. package/dist/cli/doctor-lisa-owned-artifacts.js.map +1 -1
  7. package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
  8. package/dist/core/lisa-owned-hash-ledger.js +6 -0
  9. package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
  10. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  11. package/dist/core/upstream-evidence-manifest.js +24 -23
  12. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  13. package/package.json +1 -1
  14. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  15. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  16. package/plugins/lisa/.codex-plugin/skills/lisa-atlassian-access/scripts/markdown-to-adf.mjs +23 -1
  17. package/plugins/lisa/.codex-plugin/skills/lisa-detect-tooling/scripts/detect-tooling.mjs +31 -2
  18. package/plugins/lisa/.codex-plugin/skills/lisa-remote-dispatch/scripts/dispatch.mjs +30 -2
  19. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/doctor-secrets.mjs +25 -1
  20. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/inspect-vault.mjs +23 -1
  21. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/materialize-secrets.mjs +24 -1
  22. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/read-secret-note.mjs +25 -2
  23. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/resolve-secret.mjs +25 -2
  24. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/rotate-secret.mjs +25 -2
  25. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/sync-secret-to-ci.mjs +25 -1
  26. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/validate-config.mjs +25 -2
  27. package/plugins/lisa/.codex-plugin/skills/lisa-setup-automations/scripts/generate-workflow.mjs +31 -2
  28. package/plugins/lisa/.codex-plugin/skills/lisa-setup-local-env/scripts/local-env.mjs +24 -2
  29. package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +23 -1
  30. package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/scripts/verify-remote-env.mjs +25 -2
  31. package/plugins/lisa/hooks/block-managed-file-edits.sh +136 -15
  32. package/plugins/lisa/skills/lisa-atlassian-access/scripts/markdown-to-adf.mjs +23 -1
  33. package/plugins/lisa/skills/lisa-detect-tooling/scripts/detect-tooling.mjs +31 -2
  34. package/plugins/lisa/skills/lisa-remote-dispatch/scripts/dispatch.mjs +30 -2
  35. package/plugins/lisa/skills/lisa-secrets-access/scripts/doctor-secrets.mjs +25 -1
  36. package/plugins/lisa/skills/lisa-secrets-access/scripts/inspect-vault.mjs +23 -1
  37. package/plugins/lisa/skills/lisa-secrets-access/scripts/materialize-secrets.mjs +24 -1
  38. package/plugins/lisa/skills/lisa-secrets-access/scripts/read-secret-note.mjs +25 -2
  39. package/plugins/lisa/skills/lisa-secrets-access/scripts/resolve-secret.mjs +25 -2
  40. package/plugins/lisa/skills/lisa-secrets-access/scripts/rotate-secret.mjs +25 -2
  41. package/plugins/lisa/skills/lisa-secrets-access/scripts/sync-secret-to-ci.mjs +25 -1
  42. package/plugins/lisa/skills/lisa-secrets-access/scripts/validate-config.mjs +25 -2
  43. package/plugins/lisa/skills/lisa-setup-automations/scripts/generate-workflow.mjs +31 -2
  44. package/plugins/lisa/skills/lisa-setup-local-env/scripts/local-env.mjs +24 -2
  45. package/plugins/lisa/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +23 -1
  46. package/plugins/lisa/skills/lisa-setup-remote-env/scripts/verify-remote-env.mjs +25 -2
  47. package/plugins/lisa-agy/plugin.json +1 -1
  48. package/plugins/lisa-agy/skills/lisa-atlassian-access/scripts/markdown-to-adf.mjs +23 -1
  49. package/plugins/lisa-agy/skills/lisa-detect-tooling/scripts/detect-tooling.mjs +31 -2
  50. package/plugins/lisa-agy/skills/lisa-remote-dispatch/scripts/dispatch.mjs +30 -2
  51. package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/doctor-secrets.mjs +25 -1
  52. package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/inspect-vault.mjs +23 -1
  53. package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/materialize-secrets.mjs +24 -1
  54. package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/read-secret-note.mjs +25 -2
  55. package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/resolve-secret.mjs +25 -2
  56. package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/rotate-secret.mjs +25 -2
  57. package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/sync-secret-to-ci.mjs +25 -1
  58. package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/validate-config.mjs +25 -2
  59. package/plugins/lisa-agy/skills/lisa-setup-automations/scripts/generate-workflow.mjs +31 -2
  60. package/plugins/lisa-agy/skills/lisa-setup-local-env/scripts/local-env.mjs +24 -2
  61. package/plugins/lisa-agy/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +23 -1
  62. package/plugins/lisa-agy/skills/lisa-setup-remote-env/scripts/verify-remote-env.mjs +25 -2
  63. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  64. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  65. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  66. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  67. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  68. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  69. package/plugins/lisa-copilot/hooks/block-managed-file-edits.sh +136 -15
  70. package/plugins/lisa-copilot/skills/lisa-atlassian-access/scripts/markdown-to-adf.mjs +23 -1
  71. package/plugins/lisa-copilot/skills/lisa-detect-tooling/scripts/detect-tooling.mjs +31 -2
  72. package/plugins/lisa-copilot/skills/lisa-remote-dispatch/scripts/dispatch.mjs +30 -2
  73. package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/doctor-secrets.mjs +25 -1
  74. package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/inspect-vault.mjs +23 -1
  75. package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/materialize-secrets.mjs +24 -1
  76. package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/read-secret-note.mjs +25 -2
  77. package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/resolve-secret.mjs +25 -2
  78. package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/rotate-secret.mjs +25 -2
  79. package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/sync-secret-to-ci.mjs +25 -1
  80. package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/validate-config.mjs +25 -2
  81. package/plugins/lisa-copilot/skills/lisa-setup-automations/scripts/generate-workflow.mjs +31 -2
  82. package/plugins/lisa-copilot/skills/lisa-setup-local-env/scripts/local-env.mjs +24 -2
  83. package/plugins/lisa-copilot/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +23 -1
  84. package/plugins/lisa-copilot/skills/lisa-setup-remote-env/scripts/verify-remote-env.mjs +25 -2
  85. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  86. package/plugins/lisa-cursor/hooks/block-managed-file-edits.sh +136 -15
  87. package/plugins/lisa-cursor/skills/lisa-atlassian-access/scripts/markdown-to-adf.mjs +23 -1
  88. package/plugins/lisa-cursor/skills/lisa-detect-tooling/scripts/detect-tooling.mjs +31 -2
  89. package/plugins/lisa-cursor/skills/lisa-remote-dispatch/scripts/dispatch.mjs +30 -2
  90. package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/doctor-secrets.mjs +25 -1
  91. package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/inspect-vault.mjs +23 -1
  92. package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/materialize-secrets.mjs +24 -1
  93. package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/read-secret-note.mjs +25 -2
  94. package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/resolve-secret.mjs +25 -2
  95. package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/rotate-secret.mjs +25 -2
  96. package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/sync-secret-to-ci.mjs +25 -1
  97. package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/validate-config.mjs +25 -2
  98. package/plugins/lisa-cursor/skills/lisa-setup-automations/scripts/generate-workflow.mjs +31 -2
  99. package/plugins/lisa-cursor/skills/lisa-setup-local-env/scripts/local-env.mjs +24 -2
  100. package/plugins/lisa-cursor/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +23 -1
  101. package/plugins/lisa-cursor/skills/lisa-setup-remote-env/scripts/verify-remote-env.mjs +25 -2
  102. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  103. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  104. package/plugins/lisa-expo-agy/plugin.json +1 -1
  105. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  106. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  107. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  108. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  109. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  110. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  111. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  112. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  113. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  114. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  115. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  116. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  117. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  118. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  119. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  120. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  121. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  122. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  123. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  124. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  125. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  126. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  127. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  128. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  129. package/plugins/lisa-rails-agy/plugin.json +1 -1
  130. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  131. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  132. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  133. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  134. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  135. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  136. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  137. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  138. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  139. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  140. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  141. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  142. package/plugins/src/base/hooks/block-managed-file-edits.sh +136 -15
  143. package/plugins/src/base/skills/lisa-atlassian-access/scripts/markdown-to-adf.mjs +23 -1
  144. package/plugins/src/base/skills/lisa-detect-tooling/scripts/detect-tooling.mjs +31 -2
  145. package/plugins/src/base/skills/lisa-remote-dispatch/scripts/dispatch.mjs +30 -2
  146. package/plugins/src/base/skills/lisa-secrets-access/scripts/doctor-secrets.mjs +25 -1
  147. package/plugins/src/base/skills/lisa-secrets-access/scripts/inspect-vault.mjs +23 -1
  148. package/plugins/src/base/skills/lisa-secrets-access/scripts/materialize-secrets.mjs +24 -1
  149. package/plugins/src/base/skills/lisa-secrets-access/scripts/read-secret-note.mjs +25 -2
  150. package/plugins/src/base/skills/lisa-secrets-access/scripts/resolve-secret.mjs +25 -2
  151. package/plugins/src/base/skills/lisa-secrets-access/scripts/rotate-secret.mjs +25 -2
  152. package/plugins/src/base/skills/lisa-secrets-access/scripts/sync-secret-to-ci.mjs +25 -1
  153. package/plugins/src/base/skills/lisa-secrets-access/scripts/validate-config.mjs +25 -2
  154. package/plugins/src/base/skills/lisa-setup-automations/scripts/generate-workflow.mjs +31 -2
  155. package/plugins/src/base/skills/lisa-setup-local-env/scripts/local-env.mjs +24 -2
  156. package/plugins/src/base/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +23 -1
  157. package/plugins/src/base/skills/lisa-setup-remote-env/scripts/verify-remote-env.mjs +25 -2
  158. package/scripts/generate-agy-plugin-artifacts.mjs +2 -1
  159. package/scripts/generate-copilot-plugin-artifacts.mjs +2 -1
  160. package/scripts/generate-cursor-plugin-artifacts.mjs +2 -1
  161. package/typescript/copy-overwrite/scripts/check-skipped-required-checks.mjs +14 -3
@@ -27,7 +27,8 @@
27
27
  * @module resolve-secret
28
28
  */
29
29
 
30
- import { existsSync, readFileSync } from "node:fs";
30
+ import { existsSync, readFileSync, realpathSync } from "node:fs";
31
+ import { fileURLToPath } from "node:url";
31
32
 
32
33
  import { parseEnv } from "./envfile.mjs";
33
34
  import { validateNote } from "./note-format.mjs";
@@ -252,7 +253,29 @@ function main() {
252
253
  );
253
254
  }
254
255
 
255
- if (import.meta.url === `file://${process.argv[1]}`) {
256
+ /**
257
+ * Whether this module is the one node was asked to run.
258
+ *
259
+ * Both sides are realpath'd: a raw URL comparison answers "no" through a
260
+ * symlinked checkout, a git worktree, or a /tmp path on macOS, so the module
261
+ * loads, runs nothing and exits 0 — a silent no-op that reads as success.
262
+ *
263
+ * A local copy rather than an import: plugin payload scripts ship standalone,
264
+ * with no `lib/` sibling to import from once installed.
265
+ * @param {string} moduleUrl - The caller's own `import.meta.url`.
266
+ * @param {string | undefined} [argv1] - Entry path; defaults to `process.argv[1]`.
267
+ * @returns {boolean} Whether the caller should run its CLI body.
268
+ */
269
+ function invokedAsScript(moduleUrl, argv1 = process.argv[1]) {
270
+ if (!argv1) return false;
271
+ try {
272
+ return realpathSync(argv1) === realpathSync(fileURLToPath(moduleUrl));
273
+ } catch {
274
+ return false;
275
+ }
276
+ }
277
+
278
+ if (invokedAsScript(import.meta.url)) {
256
279
  try {
257
280
  main();
258
281
  } catch (err) {
@@ -27,7 +27,8 @@
27
27
  * @module rotate-secret
28
28
  */
29
29
 
30
- import { readFileSync } from "node:fs";
30
+ import { readFileSync, realpathSync } from "node:fs";
31
+ import { fileURLToPath } from "node:url";
31
32
  import { hostname } from "node:os";
32
33
 
33
34
  import {
@@ -264,7 +265,29 @@ function main() {
264
265
  );
265
266
  }
266
267
 
267
- if (import.meta.url === `file://${process.argv[1]}`) {
268
+ /**
269
+ * Whether this module is the one node was asked to run.
270
+ *
271
+ * Both sides are realpath'd: a raw URL comparison answers "no" through a
272
+ * symlinked checkout, a git worktree, or a /tmp path on macOS, so the module
273
+ * loads, runs nothing and exits 0 — a silent no-op that reads as success.
274
+ *
275
+ * A local copy rather than an import: plugin payload scripts ship standalone,
276
+ * with no `lib/` sibling to import from once installed.
277
+ * @param {string} moduleUrl - The caller's own `import.meta.url`.
278
+ * @param {string | undefined} [argv1] - Entry path; defaults to `process.argv[1]`.
279
+ * @returns {boolean} Whether the caller should run its CLI body.
280
+ */
281
+ function invokedAsScript(moduleUrl, argv1 = process.argv[1]) {
282
+ if (!argv1) return false;
283
+ try {
284
+ return realpathSync(argv1) === realpathSync(fileURLToPath(moduleUrl));
285
+ } catch {
286
+ return false;
287
+ }
288
+ }
289
+
290
+ if (invokedAsScript(import.meta.url)) {
268
291
  try {
269
292
  main();
270
293
  } catch (err) {
@@ -48,6 +48,8 @@
48
48
  */
49
49
 
50
50
  import { execFileSync } from "node:child_process";
51
+ import { fileURLToPath } from "node:url";
52
+ import { realpathSync } from "node:fs";
51
53
 
52
54
  import { ENV_KEY, fetchAll } from "./providers.mjs";
53
55
  import { readConfig } from "./surfaces.mjs";
@@ -471,7 +473,29 @@ function main() {
471
473
  );
472
474
  }
473
475
 
474
- if (import.meta.url === `file://${process.argv[1]}`) {
476
+ /**
477
+ * Whether this module is the one node was asked to run.
478
+ *
479
+ * Both sides are realpath'd: a raw URL comparison answers "no" through a
480
+ * symlinked checkout, a git worktree, or a /tmp path on macOS, so the module
481
+ * loads, runs nothing and exits 0 — a silent no-op that reads as success.
482
+ *
483
+ * A local copy rather than an import: plugin payload scripts ship standalone,
484
+ * with no `lib/` sibling to import from once installed.
485
+ * @param {string} moduleUrl - The caller's own `import.meta.url`.
486
+ * @param {string | undefined} [argv1] - Entry path; defaults to `process.argv[1]`.
487
+ * @returns {boolean} Whether the caller should run its CLI body.
488
+ */
489
+ function invokedAsScript(moduleUrl, argv1 = process.argv[1]) {
490
+ if (!argv1) return false;
491
+ try {
492
+ return realpathSync(argv1) === realpathSync(fileURLToPath(moduleUrl));
493
+ } catch {
494
+ return false;
495
+ }
496
+ }
497
+
498
+ if (invokedAsScript(import.meta.url)) {
475
499
  try {
476
500
  main();
477
501
  } catch (err) {
@@ -16,7 +16,8 @@
16
16
  * @module validate-config
17
17
  */
18
18
 
19
- import { existsSync, readFileSync } from "node:fs";
19
+ import { existsSync, readFileSync, realpathSync } from "node:fs";
20
+ import { fileURLToPath } from "node:url";
20
21
  import { join } from "node:path";
21
22
 
22
23
  import { SURFACES as SURFACE_CAPABILITIES } from "./surfaces.mjs";
@@ -517,7 +518,29 @@ function main() {
517
518
  throw new Error(`${problems.length} configuration problem(s)`);
518
519
  }
519
520
 
520
- if (import.meta.url === `file://${process.argv[1]}`) {
521
+ /**
522
+ * Whether this module is the one node was asked to run.
523
+ *
524
+ * Both sides are realpath'd: a raw URL comparison answers "no" through a
525
+ * symlinked checkout, a git worktree, or a /tmp path on macOS, so the module
526
+ * loads, runs nothing and exits 0 — a silent no-op that reads as success.
527
+ *
528
+ * A local copy rather than an import: plugin payload scripts ship standalone,
529
+ * with no `lib/` sibling to import from once installed.
530
+ * @param {string} moduleUrl - The caller's own `import.meta.url`.
531
+ * @param {string | undefined} [argv1] - Entry path; defaults to `process.argv[1]`.
532
+ * @returns {boolean} Whether the caller should run its CLI body.
533
+ */
534
+ function invokedAsScript(moduleUrl, argv1 = process.argv[1]) {
535
+ if (!argv1) return false;
536
+ try {
537
+ return realpathSync(argv1) === realpathSync(fileURLToPath(moduleUrl));
538
+ } catch {
539
+ return false;
540
+ }
541
+ }
542
+
543
+ if (invokedAsScript(import.meta.url)) {
521
544
  try {
522
545
  main();
523
546
  } catch (err) {
@@ -17,7 +17,14 @@
17
17
  * @module generate-workflow
18
18
  */
19
19
 
20
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
20
+ import {
21
+ existsSync,
22
+ mkdirSync,
23
+ readFileSync,
24
+ writeFileSync,
25
+ realpathSync,
26
+ } from "node:fs";
27
+ import { fileURLToPath } from "node:url";
21
28
  import { dirname, join } from "node:path";
22
29
 
23
30
  /** Where a generated loop workflow lands. */
@@ -249,7 +256,29 @@ function main() {
249
256
  }
250
257
  }
251
258
 
252
- if (import.meta.url === `file://${process.argv[1]}`) {
259
+ /**
260
+ * Whether this module is the one node was asked to run.
261
+ *
262
+ * Both sides are realpath'd: a raw URL comparison answers "no" through a
263
+ * symlinked checkout, a git worktree, or a /tmp path on macOS, so the module
264
+ * loads, runs nothing and exits 0 — a silent no-op that reads as success.
265
+ *
266
+ * A local copy rather than an import: plugin payload scripts ship standalone,
267
+ * with no `lib/` sibling to import from once installed.
268
+ * @param {string} moduleUrl - The caller's own `import.meta.url`.
269
+ * @param {string | undefined} [argv1] - Entry path; defaults to `process.argv[1]`.
270
+ * @returns {boolean} Whether the caller should run its CLI body.
271
+ */
272
+ function invokedAsScript(moduleUrl, argv1 = process.argv[1]) {
273
+ if (!argv1) return false;
274
+ try {
275
+ return realpathSync(argv1) === realpathSync(fileURLToPath(moduleUrl));
276
+ } catch {
277
+ return false;
278
+ }
279
+ }
280
+
281
+ if (invokedAsScript(import.meta.url)) {
253
282
  try {
254
283
  main();
255
284
  } catch (err) {
@@ -23,7 +23,7 @@
23
23
  * @module local-env
24
24
  */
25
25
 
26
- import { existsSync } from "node:fs";
26
+ import { existsSync, realpathSync } from "node:fs";
27
27
  import { dirname, resolve } from "node:path";
28
28
  import { fileURLToPath, pathToFileURL } from "node:url";
29
29
 
@@ -159,7 +159,29 @@ export async function run(argv) {
159
159
  return 0;
160
160
  }
161
161
 
162
- if (import.meta.url === `file://${process.argv[1]}`) {
162
+ /**
163
+ * Whether this module is the one node was asked to run.
164
+ *
165
+ * Both sides are realpath'd: a raw URL comparison answers "no" through a
166
+ * symlinked checkout, a git worktree, or a /tmp path on macOS, so the module
167
+ * loads, runs nothing and exits 0 — a silent no-op that reads as success.
168
+ *
169
+ * A local copy rather than an import: plugin payload scripts ship standalone,
170
+ * with no `lib/` sibling to import from once installed.
171
+ * @param {string} moduleUrl - The caller's own `import.meta.url`.
172
+ * @param {string | undefined} [argv1] - Entry path; defaults to `process.argv[1]`.
173
+ * @returns {boolean} Whether the caller should run its CLI body.
174
+ */
175
+ function invokedAsScript(moduleUrl, argv1 = process.argv[1]) {
176
+ if (!argv1) return false;
177
+ try {
178
+ return realpathSync(argv1) === realpathSync(fileURLToPath(moduleUrl));
179
+ } catch {
180
+ return false;
181
+ }
182
+ }
183
+
184
+ if (invokedAsScript(import.meta.url)) {
163
185
  const argv = {
164
186
  installTools: process.argv.includes("--install-tools"),
165
187
  dryRun: process.argv.includes("--dry-run"),
@@ -1519,7 +1519,29 @@ async function main() {
1519
1519
  console.log(`\nRemote environment ${dryRun ? "plan complete" : "ready"}.`);
1520
1520
  }
1521
1521
 
1522
- if (import.meta.url === `file://${process.argv[1]}`) {
1522
+ /**
1523
+ * Whether this module is the one node was asked to run.
1524
+ *
1525
+ * Both sides are realpath'd: a raw URL comparison answers "no" through a
1526
+ * symlinked checkout, a git worktree, or a /tmp path on macOS, so the module
1527
+ * loads, runs nothing and exits 0 — a silent no-op that reads as success.
1528
+ *
1529
+ * A local copy rather than an import: plugin payload scripts ship standalone,
1530
+ * with no `lib/` sibling to import from once installed.
1531
+ * @param {string} moduleUrl - The caller's own `import.meta.url`.
1532
+ * @param {string | undefined} [argv1] - Entry path; defaults to `process.argv[1]`.
1533
+ * @returns {boolean} Whether the caller should run its CLI body.
1534
+ */
1535
+ function invokedAsScript(moduleUrl, argv1 = process.argv[1]) {
1536
+ if (!argv1) return false;
1537
+ try {
1538
+ return realpathSync(argv1) === realpathSync(fileURLToPath(moduleUrl));
1539
+ } catch {
1540
+ return false;
1541
+ }
1542
+ }
1543
+
1544
+ if (invokedAsScript(import.meta.url)) {
1523
1545
  // Awaited rather than called bare: main is async, so a synchronous try/catch
1524
1546
  // would let a rejected promise escape as an unhandled rejection and exit 0 —
1525
1547
  // reporting a prepared environment that was never prepared.
@@ -22,7 +22,8 @@
22
22
  */
23
23
 
24
24
  import { execFileSync } from "node:child_process";
25
- import { existsSync, readFileSync, statSync } from "node:fs";
25
+ import { fileURLToPath } from "node:url";
26
+ import { existsSync, readFileSync, statSync, realpathSync } from "node:fs";
26
27
  import { join } from "node:path";
27
28
 
28
29
  import { probe, readRemoteEnvConfig } from "./setup-remote-env.mjs";
@@ -213,7 +214,29 @@ function main() {
213
214
  console.log("\nRemote environment verified without revealing any value.");
214
215
  }
215
216
 
216
- if (import.meta.url === `file://${process.argv[1]}`) {
217
+ /**
218
+ * Whether this module is the one node was asked to run.
219
+ *
220
+ * Both sides are realpath'd: a raw URL comparison answers "no" through a
221
+ * symlinked checkout, a git worktree, or a /tmp path on macOS, so the module
222
+ * loads, runs nothing and exits 0 — a silent no-op that reads as success.
223
+ *
224
+ * A local copy rather than an import: plugin payload scripts ship standalone,
225
+ * with no `lib/` sibling to import from once installed.
226
+ * @param {string} moduleUrl - The caller's own `import.meta.url`.
227
+ * @param {string | undefined} [argv1] - Entry path; defaults to `process.argv[1]`.
228
+ * @returns {boolean} Whether the caller should run its CLI body.
229
+ */
230
+ function invokedAsScript(moduleUrl, argv1 = process.argv[1]) {
231
+ if (!argv1) return false;
232
+ try {
233
+ return realpathSync(argv1) === realpathSync(fileURLToPath(moduleUrl));
234
+ } catch {
235
+ return false;
236
+ }
237
+ }
238
+
239
+ if (invokedAsScript(import.meta.url)) {
217
240
  try {
218
241
  main();
219
242
  } catch (err) {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "3.26.0",
3
+ "version": "3.26.2",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,10 +1,9 @@
1
1
  #!/usr/bin/env bash
2
2
  # PreToolUse hook: refuse agent writes to files Lisa overwrites on every apply.
3
3
  #
4
- # Lisa ships templates in three modes, and only one of them destroys host edits:
4
+ # Lisa ships templates in three modes, and only one of them is unsafe to edit:
5
5
  #
6
- # copy-overwrite — replaced wholesale on every `lisa apply`. An edit here is
7
- # gone at the next `bun install`, silently.
6
+ # copy-overwrite — see below; the harm depends on the file.
8
7
  # copy-contents — Lisa APPENDS its lines; host content survives.
9
8
  # create-only — skipped when the file exists; the host owns it outright.
10
9
  #
@@ -12,6 +11,21 @@
12
11
  # would stop agents editing files they are supposed to own, which is worse than
13
12
  # the problem being solved.
14
13
  #
14
+ # WITHIN copy-overwrite there are two populations with OPPOSITE consequences,
15
+ # and an earlier version of this guard described only one of them:
16
+ #
17
+ # hash-tracked guards (the `.mjs` scripts, listed in the Lisa-owned ledger)
18
+ # — an edit classifies as `host-modified` and `lisa apply` PRESERVES it.
19
+ # The file silently forks and stops receiving upstream fixes while looking
20
+ # current. Nothing is deleted; that is what makes it hard to notice.
21
+ # everything else (`.json` configs and friends)
22
+ # — replaced wholesale on the next apply, which runs on every install.
23
+ # The edit vanishes.
24
+ #
25
+ # The refusal branches on ledger membership so it states the consequence that
26
+ # actually applies, rather than describing both and leaving the reader to work
27
+ # out which they are in.
28
+ #
15
29
  # Measured, not hypothetical. Nothing enforced this, so downstream copies were
16
30
  # edited and then silently diverged: `classify-maestro-failures.mjs` reached
17
31
  # 36,061 bytes in one fleet repo against 29,586 shipped, and five gate files in
@@ -67,6 +81,71 @@ if [ -f "$project_root/package.json" ] &&
67
81
  exit 0
68
82
  fi
69
83
 
84
+ # Whether the project has claimed a path in `.lisaignore`.
85
+ #
86
+ # THIS IS THE OWNERSHIP QUESTION, and it decides whether blocking is right at
87
+ # all. `lisa apply` skips an ignored path — it logs `Kept (.lisaignore)` and
88
+ # counts it as ignored rather than overwritten — so the file is the project's,
89
+ # its edits survive, and refusing them would be refusing someone access to their
90
+ # own file. `doctor-lisa-owned-artifacts` already consults this list; this guard
91
+ # did not, which would have blocked deliberate forks.
92
+ #
93
+ # The real matcher is minimatch in `src/utils/ignore-patterns.ts` and cannot be
94
+ # reproduced faithfully in shell. This covers the common shapes — exact path,
95
+ # directory prefix, glob, and a bare pattern matching any segment — and where it
96
+ # is unsure it ALLOWS. Wrongly allowing costs an edit that apply may overwrite,
97
+ # which is the behaviour before this guard existed; wrongly blocking locks
98
+ # someone out of a file they own.
99
+ lisaignored() {
100
+ local rel="$1"
101
+ local list="$project_root/.lisaignore"
102
+ [ -f "$list" ] || return 1
103
+ local pattern
104
+ while IFS= read -r pattern || [ -n "$pattern" ]; do
105
+ pattern="${pattern#"${pattern%%[![:space:]]*}"}"
106
+ pattern="${pattern%"${pattern##*[![:space:]]}"}"
107
+ [ -n "$pattern" ] || continue
108
+ case "$pattern" in \#*) continue ;; esac
109
+ # A leading `!` is not gitignore negation here. The real matcher passes
110
+ # patterns to minimatch, which negates by default, and it combines them with
111
+ # `.some()` — so a single `!scripts/a.mjs` line reports EVERY OTHER PATH as
112
+ # ignored. Measured:
113
+ #
114
+ # patterns=["!scripts/a.mjs"] path=scripts/a.mjs -> ignored=false
115
+ # patterns=["!scripts/a.mjs"] path=scripts/b.mjs -> ignored=TRUE
116
+ #
117
+ # Reproducing that faithfully would mean disabling this guard on a typo.
118
+ # Reproducing the intuitive gitignore reading would mean blocking files the
119
+ # matcher considers ignored. Both are wrong, so the file is treated as
120
+ # claimed and the write is allowed — the same direction this function errs
121
+ # everywhere else. Filed upstream; when the matcher stops negating, delete
122
+ # this branch rather than teaching it a second wrong answer.
123
+ case "$pattern" in !*) return 0 ;; esac
124
+ case "$pattern" in
125
+ */)
126
+ case "$rel" in "${pattern%/}"/* | "${pattern%/}") return 0 ;; esac
127
+ ;;
128
+ esac
129
+ # shellcheck disable=SC2254 -- the pattern is a glob on purpose.
130
+ case "$rel" in $pattern) return 0 ;; esac
131
+ case "$pattern" in
132
+ */*) ;;
133
+ *)
134
+ # shellcheck disable=SC2254 -- ditto, matched against the basename.
135
+ case "${rel##*/}" in $pattern) return 0 ;; esac
136
+ ;;
137
+ esac
138
+ done <"$list"
139
+ return 1
140
+ }
141
+
142
+ # A candidate rewritten relative to the project, so an absolute target from a
143
+ # tool payload and a relative one from a shell command classify identically.
144
+ relative_path() {
145
+ local rel="${1#"$project_root"/}"
146
+ printf '%s' "${rel#./}"
147
+ }
148
+
70
149
  # Whether a host-relative path is shipped as a copy-overwrite template.
71
150
  #
72
151
  # A few stat calls rather than an enumeration: the same relative path is probed
@@ -77,10 +156,11 @@ managed_source() {
77
156
  case "$candidate" in
78
157
  */node_modules/* | node_modules/* | */dist/* | dist/*) return 1 ;;
79
158
  esac
80
- # Normalise to a project-relative path so an absolute target still matches.
81
- local rel="${candidate#"$project_root"/}"
82
- rel="${rel#./}"
159
+ local rel
160
+ rel="$(relative_path "$candidate")"
83
161
  [ -n "$rel" ] || return 1
162
+ # Claimed by the project, so not ours to refuse.
163
+ lisaignored "$rel" && return 1
84
164
  local stack
85
165
  for stack in "$package_root"/*/copy-overwrite; do
86
166
  [ -d "$stack" ] || continue
@@ -92,19 +172,44 @@ managed_source() {
92
172
  return 1
93
173
  }
94
174
 
175
+ # Whether a destination is a ledger-tracked Lisa-owned guard.
176
+ #
177
+ # The two populations behind this guard have OPPOSITE consequences, so the
178
+ # refusal has to know which one it is looking at rather than describing both and
179
+ # leaving the reader to guess. A guard in the content-hash ledger classifies as
180
+ # `host-modified` once edited and `lisa apply` PRESERVES it; anything else is
181
+ # replaced wholesale. Both are bad, for different reasons, and the right next
182
+ # step differs too.
183
+ ledger_tracked() {
184
+ local rel="$1"
185
+ local ledger="$package_root/dist/core/lisa-owned-hash-ledger.js"
186
+ [ -f "$ledger" ] || return 1
187
+ grep -q "\"$rel\"" "$ledger" 2>/dev/null
188
+ }
189
+
95
190
  refuse() {
96
191
  local target="$1"
97
192
  local source="$2"
193
+ local rel="$3"
194
+ local consequence
195
+ if ledger_tracked "$rel"; then
196
+ consequence="This is a Lisa-owned guard, tracked by content hash. \`lisa apply\`
197
+ will PRESERVE your edit rather than overwrite it — and that is the trap. The
198
+ file silently forks: it keeps looking current while every upstream fix stops
199
+ reaching it. One repository in this fleet is carrying 243 lines of divergence
200
+ nobody knew about, in a guard that had quietly stopped receiving fixes."
201
+ else
202
+ consequence="This template is not hash-tracked, so \`lisa apply\` REPLACES it
203
+ wholesale — and apply runs on every \`bun install\`. Your edit survives until the
204
+ next install and then vanishes, with nothing reporting that it had."
205
+ fi
98
206
  cat >&2 <<EOF
99
207
  BLOCKED: refusing to write \`$target\`.
100
208
 
101
- WHY: this file is Lisa-managed. It is shipped as a **copy-overwrite** template
102
- (\`$source\` in the installed package) and is replaced wholesale on every
103
- \`lisa apply\` — which runs on every \`bun install\`. Your edit would survive
104
- until the next install and then vanish, with nothing reporting that it had.
209
+ WHY: this file is Lisa-managed, shipped as a **copy-overwrite** template
210
+ (\`$source\` in the installed package).
105
211
 
106
- That is not hypothetical: downstream copies edited this way diverged silently
107
- until they stopped receiving upstream fixes entirely.
212
+ $consequence
108
213
 
109
214
  WHERE IT GOES INSTEAD — take the first one that fits:
110
215
 
@@ -122,7 +227,23 @@ WHERE IT GOES INSTEAD — take the first one that fits:
122
227
  proves each one, so a project can substitute its own task without touching a
123
228
  shipped file.
124
229
 
125
- 4. You believe this file should not be Lisa-managed at all. That is a real
230
+ 4. This project has deliberately FORKED this file and means to keep its own
231
+ version. What to do depends on which population it is, and \`.lisaignore\` is
232
+ the WRONG answer for a hash-tracked guard:
233
+
234
+ - **A Lisa-owned guard (hash-tracked).** Do NOT add it to \`.lisaignore\`. The
235
+ ledger already preserves your version, so ignoring it buys nothing — and it
236
+ silences the standoff \`lisa doctor\` reports on every run, replacing a true
237
+ warning with the line "Enforcement guards match the installed Lisa
238
+ version", which is then false. A visible, resolvable fork becomes a silent
239
+ permanent one. Keep the warning and resolve the fork: upstream what is
240
+ general, or accept the standoff knowingly. To make this one edit, use the
241
+ override below.
242
+ - **Any other template.** \`.lisaignore\` is the right answer. Nothing else
243
+ preserves it, and the entry declares the divergence where the next person
244
+ can see it.
245
+
246
+ 5. You believe this file should not be Lisa-managed at all. That is a real
126
247
  argument and it belongs upstream, not in a local edit that will be erased.
127
248
 
128
249
  If a human explicitly asked for this edit, they can re-run with
@@ -142,7 +263,7 @@ case "$tool_name" in
142
263
  while IFS= read -r candidate; do
143
264
  [ -n "$candidate" ] || continue
144
265
  if source_path="$(managed_source "$candidate")"; then
145
- refuse "$candidate" "$source_path"
266
+ refuse "$candidate" "$source_path" "$(relative_path "$candidate")"
146
267
  fi
147
268
  done <<EOF
148
269
  $paths
@@ -157,7 +278,7 @@ EOF
157
278
  while IFS= read -r token; do
158
279
  [ -n "$token" ] || continue
159
280
  if source_path="$(managed_source "$token")"; then
160
- refuse "$token" "$source_path"
281
+ refuse "$token" "$source_path" "$(relative_path "$token")"
161
282
  fi
162
283
  done <<EOF
163
284
  $(printf '%s' "$command_str" |
@@ -202,7 +202,29 @@ function parseInline(text) {
202
202
  return nodes.length > 0 ? nodes : [{ type: "text", text: "" }];
203
203
  }
204
204
 
205
- if (import.meta.url === `file://${process.argv[1]}`) {
205
+ /**
206
+ * Whether this module is the one node was asked to run.
207
+ *
208
+ * Both sides are realpath'd: a raw URL comparison answers "no" through a
209
+ * symlinked checkout, a git worktree, or a /tmp path on macOS, so the module
210
+ * loads, runs nothing and exits 0 — a silent no-op that reads as success.
211
+ *
212
+ * A local copy rather than an import: plugin payload scripts ship standalone,
213
+ * with no `lib/` sibling to import from once installed.
214
+ * @param {string} moduleUrl - The caller's own `import.meta.url`.
215
+ * @param {string | undefined} [argv1] - Entry path; defaults to `process.argv[1]`.
216
+ * @returns {boolean} Whether the caller should run its CLI body.
217
+ */
218
+ function invokedAsScript(moduleUrl, argv1 = process.argv[1]) {
219
+ if (!argv1) return false;
220
+ try {
221
+ return realpathSync(argv1) === realpathSync(fileURLToPath(moduleUrl));
222
+ } catch {
223
+ return false;
224
+ }
225
+ }
226
+
227
+ if (invokedAsScript(import.meta.url)) {
206
228
  let input = "";
207
229
  process.stdin.setEncoding("utf8");
208
230
  process.stdin.on("data", chunk => {
@@ -22,7 +22,14 @@
22
22
  * @module detect-tooling
23
23
  */
24
24
 
25
- import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
25
+ import {
26
+ existsSync,
27
+ readFileSync,
28
+ readdirSync,
29
+ statSync,
30
+ realpathSync,
31
+ } from "node:fs";
32
+ import { fileURLToPath } from "node:url";
26
33
  import { join } from "node:path";
27
34
 
28
35
  import { commandsIn } from "./commands.mjs";
@@ -583,7 +590,29 @@ export function proposedEntries(proposal) {
583
590
  };
584
591
  }
585
592
 
586
- if (process.argv[1] && import.meta.url === `file://${process.argv[1]}`) {
593
+ /**
594
+ * Whether this module is the one node was asked to run.
595
+ *
596
+ * Both sides are realpath'd: a raw URL comparison answers "no" through a
597
+ * symlinked checkout, a git worktree, or a /tmp path on macOS, so the module
598
+ * loads, runs nothing and exits 0 — a silent no-op that reads as success.
599
+ *
600
+ * A local copy rather than an import: plugin payload scripts ship standalone,
601
+ * with no `lib/` sibling to import from once installed.
602
+ * @param {string} moduleUrl - The caller's own `import.meta.url`.
603
+ * @param {string | undefined} [argv1] - Entry path; defaults to `process.argv[1]`.
604
+ * @returns {boolean} Whether the caller should run its CLI body.
605
+ */
606
+ function invokedAsScript(moduleUrl, argv1 = process.argv[1]) {
607
+ if (!argv1) return false;
608
+ try {
609
+ return realpathSync(argv1) === realpathSync(fileURLToPath(moduleUrl));
610
+ } catch {
611
+ return false;
612
+ }
613
+ }
614
+
615
+ if (invokedAsScript(import.meta.url)) {
587
616
  const proposals = detectTooling();
588
617
  if (process.argv.includes("--json")) {
589
618
  console.log(JSON.stringify(proposals, null, 2));
@@ -20,7 +20,13 @@
20
20
  */
21
21
 
22
22
  import { execFileSync } from "node:child_process";
23
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
23
+ import {
24
+ existsSync,
25
+ mkdirSync,
26
+ readFileSync,
27
+ writeFileSync,
28
+ realpathSync,
29
+ } from "node:fs";
24
30
  import { dirname, join, resolve } from "node:path";
25
31
  import { fileURLToPath } from "node:url";
26
32
 
@@ -495,7 +501,29 @@ async function main() {
495
501
  console.log(`recorded in ${LEDGER}; not polling — this process is done.`);
496
502
  }
497
503
 
498
- if (import.meta.url === `file://${process.argv[1]}`) {
504
+ /**
505
+ * Whether this module is the one node was asked to run.
506
+ *
507
+ * Both sides are realpath'd: a raw URL comparison answers "no" through a
508
+ * symlinked checkout, a git worktree, or a /tmp path on macOS, so the module
509
+ * loads, runs nothing and exits 0 — a silent no-op that reads as success.
510
+ *
511
+ * A local copy rather than an import: plugin payload scripts ship standalone,
512
+ * with no `lib/` sibling to import from once installed.
513
+ * @param {string} moduleUrl - The caller's own `import.meta.url`.
514
+ * @param {string | undefined} [argv1] - Entry path; defaults to `process.argv[1]`.
515
+ * @returns {boolean} Whether the caller should run its CLI body.
516
+ */
517
+ function invokedAsScript(moduleUrl, argv1 = process.argv[1]) {
518
+ if (!argv1) return false;
519
+ try {
520
+ return realpathSync(argv1) === realpathSync(fileURLToPath(moduleUrl));
521
+ } catch {
522
+ return false;
523
+ }
524
+ }
525
+
526
+ if (invokedAsScript(import.meta.url)) {
499
527
  // Awaited rather than called bare: one dispatch path is async, so a synchronous
500
528
  // try/catch would let a rejection escape as an unhandled rejection and exit 0 —
501
529
  // reporting dispatched work that was never accepted.