@codyswann/lisa 4.4.5 → 4.4.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 (145) hide show
  1. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  2. package/dist/core/upstream-evidence-manifest.js +28 -15
  3. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  4. package/package.json +1 -1
  5. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  6. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  7. package/plugins/lisa/.codex-plugin/skills/lisa-remote-dispatch/scripts/dispatch.mjs +4 -3
  8. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/providers.mjs +11 -5
  9. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/sync-secret-to-ci.mjs +3 -2
  10. package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +36 -23
  11. package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/scripts/verify-remote-env.mjs +7 -3
  12. package/plugins/lisa/.codex-plugin/skills/lisa-setup-workstation/scripts/bounded-child.mjs +159 -0
  13. package/plugins/lisa/.codex-plugin/skills/lisa-setup-workstation/scripts/workstation.mjs +5 -4
  14. package/plugins/lisa/scripts/design-source-gate.mjs +19 -1
  15. package/plugins/lisa/scripts/plugin-sync-explain.mjs +23 -1
  16. package/plugins/lisa/scripts/project-ideation-idempotency-harness.mjs +20 -1
  17. package/plugins/lisa/skills/lisa-remote-dispatch/scripts/dispatch.mjs +4 -3
  18. package/plugins/lisa/skills/lisa-secrets-access/scripts/providers.mjs +11 -5
  19. package/plugins/lisa/skills/lisa-secrets-access/scripts/sync-secret-to-ci.mjs +3 -2
  20. package/plugins/lisa/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +36 -23
  21. package/plugins/lisa/skills/lisa-setup-remote-env/scripts/verify-remote-env.mjs +7 -3
  22. package/plugins/lisa/skills/lisa-setup-workstation/scripts/bounded-child.mjs +159 -0
  23. package/plugins/lisa/skills/lisa-setup-workstation/scripts/workstation.mjs +5 -4
  24. package/plugins/lisa-agy/plugin.json +1 -1
  25. package/plugins/lisa-agy/scripts/design-source-gate.mjs +19 -1
  26. package/plugins/lisa-agy/scripts/plugin-sync-explain.mjs +23 -1
  27. package/plugins/lisa-agy/scripts/project-ideation-idempotency-harness.mjs +20 -1
  28. package/plugins/lisa-agy/skills/lisa-remote-dispatch/scripts/dispatch.mjs +4 -3
  29. package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/providers.mjs +11 -5
  30. package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/sync-secret-to-ci.mjs +3 -2
  31. package/plugins/lisa-agy/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +36 -23
  32. package/plugins/lisa-agy/skills/lisa-setup-remote-env/scripts/verify-remote-env.mjs +7 -3
  33. package/plugins/lisa-agy/skills/lisa-setup-workstation/scripts/bounded-child.mjs +159 -0
  34. package/plugins/lisa-agy/skills/lisa-setup-workstation/scripts/workstation.mjs +5 -4
  35. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  36. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  37. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  38. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  39. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  40. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  41. package/plugins/lisa-copilot/scripts/design-source-gate.mjs +19 -1
  42. package/plugins/lisa-copilot/scripts/plugin-sync-explain.mjs +23 -1
  43. package/plugins/lisa-copilot/scripts/project-ideation-idempotency-harness.mjs +20 -1
  44. package/plugins/lisa-copilot/skills/lisa-remote-dispatch/scripts/dispatch.mjs +4 -3
  45. package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/providers.mjs +11 -5
  46. package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/sync-secret-to-ci.mjs +3 -2
  47. package/plugins/lisa-copilot/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +36 -23
  48. package/plugins/lisa-copilot/skills/lisa-setup-remote-env/scripts/verify-remote-env.mjs +7 -3
  49. package/plugins/lisa-copilot/skills/lisa-setup-workstation/scripts/bounded-child.mjs +159 -0
  50. package/plugins/lisa-copilot/skills/lisa-setup-workstation/scripts/workstation.mjs +5 -4
  51. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  52. package/plugins/lisa-cursor/scripts/design-source-gate.mjs +19 -1
  53. package/plugins/lisa-cursor/scripts/plugin-sync-explain.mjs +23 -1
  54. package/plugins/lisa-cursor/scripts/project-ideation-idempotency-harness.mjs +20 -1
  55. package/plugins/lisa-cursor/skills/lisa-remote-dispatch/scripts/dispatch.mjs +4 -3
  56. package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/providers.mjs +11 -5
  57. package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/sync-secret-to-ci.mjs +3 -2
  58. package/plugins/lisa-cursor/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +36 -23
  59. package/plugins/lisa-cursor/skills/lisa-setup-remote-env/scripts/verify-remote-env.mjs +7 -3
  60. package/plugins/lisa-cursor/skills/lisa-setup-workstation/scripts/bounded-child.mjs +159 -0
  61. package/plugins/lisa-cursor/skills/lisa-setup-workstation/scripts/workstation.mjs +5 -4
  62. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  63. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  64. package/plugins/lisa-expo-agy/plugin.json +1 -1
  65. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  66. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  67. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  68. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  69. package/plugins/lisa-harper-fabric/hooks/enforce-config-extensions.mjs +55 -0
  70. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  71. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  72. package/plugins/lisa-harper-fabric-copilot/hooks/enforce-config-extensions.mjs +55 -0
  73. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  74. package/plugins/lisa-harper-fabric-cursor/hooks/enforce-config-extensions.mjs +55 -0
  75. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  76. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  77. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  78. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  79. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  80. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  81. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  82. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  83. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  84. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  85. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  86. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  87. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  88. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  89. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  90. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  91. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  92. package/plugins/lisa-rails-agy/plugin.json +1 -1
  93. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  94. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  95. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  96. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  97. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  98. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  99. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  100. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  101. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  102. package/plugins/lisa-wiki/scripts/diff-guard.mjs +3 -2
  103. package/plugins/lisa-wiki/scripts/ensure-wiki.mjs +3 -2
  104. package/plugins/lisa-wiki/scripts/ingest-git.mjs +12 -6
  105. package/plugins/lisa-wiki/scripts/lib/bounded-child.mjs +159 -0
  106. package/plugins/lisa-wiki/scripts/verify-migration.mjs +3 -2
  107. package/plugins/lisa-wiki/scripts/verify-wiki-safety.mjs +15 -5
  108. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  109. package/plugins/lisa-wiki-agy/scripts/diff-guard.mjs +3 -2
  110. package/plugins/lisa-wiki-agy/scripts/ensure-wiki.mjs +3 -2
  111. package/plugins/lisa-wiki-agy/scripts/ingest-git.mjs +12 -6
  112. package/plugins/lisa-wiki-agy/scripts/lib/bounded-child.mjs +159 -0
  113. package/plugins/lisa-wiki-agy/scripts/verify-migration.mjs +3 -2
  114. package/plugins/lisa-wiki-agy/scripts/verify-wiki-safety.mjs +15 -5
  115. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  116. package/plugins/lisa-wiki-copilot/scripts/diff-guard.mjs +3 -2
  117. package/plugins/lisa-wiki-copilot/scripts/ensure-wiki.mjs +3 -2
  118. package/plugins/lisa-wiki-copilot/scripts/ingest-git.mjs +12 -6
  119. package/plugins/lisa-wiki-copilot/scripts/lib/bounded-child.mjs +159 -0
  120. package/plugins/lisa-wiki-copilot/scripts/verify-migration.mjs +3 -2
  121. package/plugins/lisa-wiki-copilot/scripts/verify-wiki-safety.mjs +15 -5
  122. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  123. package/plugins/lisa-wiki-cursor/scripts/diff-guard.mjs +3 -2
  124. package/plugins/lisa-wiki-cursor/scripts/ensure-wiki.mjs +3 -2
  125. package/plugins/lisa-wiki-cursor/scripts/ingest-git.mjs +12 -6
  126. package/plugins/lisa-wiki-cursor/scripts/lib/bounded-child.mjs +159 -0
  127. package/plugins/lisa-wiki-cursor/scripts/verify-migration.mjs +3 -2
  128. package/plugins/lisa-wiki-cursor/scripts/verify-wiki-safety.mjs +15 -5
  129. package/plugins/src/base/scripts/design-source-gate.mjs +19 -1
  130. package/plugins/src/base/scripts/plugin-sync-explain.mjs +23 -1
  131. package/plugins/src/base/scripts/project-ideation-idempotency-harness.mjs +20 -1
  132. package/plugins/src/base/skills/lisa-remote-dispatch/scripts/dispatch.mjs +4 -3
  133. package/plugins/src/base/skills/lisa-secrets-access/scripts/providers.mjs +11 -5
  134. package/plugins/src/base/skills/lisa-secrets-access/scripts/sync-secret-to-ci.mjs +3 -2
  135. package/plugins/src/base/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +36 -23
  136. package/plugins/src/base/skills/lisa-setup-remote-env/scripts/verify-remote-env.mjs +7 -3
  137. package/plugins/src/base/skills/lisa-setup-workstation/scripts/bounded-child.mjs +159 -0
  138. package/plugins/src/base/skills/lisa-setup-workstation/scripts/workstation.mjs +5 -4
  139. package/plugins/src/harper-fabric/hooks/enforce-config-extensions.mjs +55 -0
  140. package/plugins/src/wiki/scripts/diff-guard.mjs +3 -2
  141. package/plugins/src/wiki/scripts/ensure-wiki.mjs +3 -2
  142. package/plugins/src/wiki/scripts/ingest-git.mjs +12 -6
  143. package/plugins/src/wiki/scripts/lib/bounded-child.mjs +159 -0
  144. package/plugins/src/wiki/scripts/verify-migration.mjs +3 -2
  145. package/plugins/src/wiki/scripts/verify-wiki-safety.mjs +15 -5
@@ -59,6 +59,44 @@ const gitEnv = () =>
59
59
  Object.entries(process.env).filter(([key]) => !key.startsWith("GIT_"))
60
60
  );
61
61
 
62
+ /**
63
+ * Milliseconds before the baseline read is presumed hung.
64
+ *
65
+ * A hang detector, not a budget. `git` reached through PATH on macOS goes via
66
+ * Apple's `xcrun` shim, measured over 20 seconds under load against 11ms for a
67
+ * real binary (CodySwannGT/lisa#2887), so a tighter deadline would make this
68
+ * hook's own timeout the ordinary outcome on a busy machine.
69
+ */
70
+ const GIT_BUDGET_MS = 30_000;
71
+
72
+ /** The baseline could not be read at all, as distinct from "there is none". */
73
+ const UNREADABLE = Symbol("unreadable");
74
+
75
+ /**
76
+ * The config as HEAD holds it: its text, `null` when HEAD has no such file, or
77
+ * `UNREADABLE` when git could not be asked.
78
+ *
79
+ * THE THREE ANSWERS ARE DELIBERATELY THREE. This used to be
80
+ * `result.status === 0 ? result.stdout : null`, and `status === 0` is false for
81
+ * a file that does not exist at HEAD **and** for a child killed at its
82
+ * deadline — so both became `null`, and the caller reads `null` as "no baseline
83
+ * to compare against, allow the edit". A busy machine therefore let through
84
+ * exactly the extension removal this hook exists to block, and nothing anywhere
85
+ * said the word "time".
86
+ *
87
+ * A deadline alone does not fix that. `spawnSync` does not throw when it kills
88
+ * a child; it returns `{ status: null, stdout: "" }`, which takes the same
89
+ * branch it always did. The discrimination has to be written, not scheduled —
90
+ * which is why this call site needed more than the `timeout:` its siblings did.
91
+ *
92
+ * `ETIMEDOUT` is set by Node itself, so this is a platform fact rather than a
93
+ * convention invented here; the shared `bounded-child.mjs` reads the same field,
94
+ * and this hook writes it out inline because a plugin payload materialized as a
95
+ * hook has no module to import from.
96
+ * @param {string} repoRoot Repository root.
97
+ * @returns {string|null|symbol} The text, `null` when absent, `UNREADABLE` when
98
+ * the child was killed.
99
+ */
62
100
  const readGitBlob = repoRoot => {
63
101
  const result = spawnSync(
64
102
  "git",
@@ -66,8 +104,11 @@ const readGitBlob = repoRoot => {
66
104
  {
67
105
  encoding: "utf8",
68
106
  env: gitEnv(),
107
+ killSignal: "SIGKILL",
108
+ timeout: GIT_BUDGET_MS,
69
109
  }
70
110
  );
111
+ if (result.error?.code === "ETIMEDOUT") return UNREADABLE;
71
112
  return result.status === 0 ? result.stdout : null;
72
113
  };
73
114
 
@@ -104,6 +145,20 @@ const main = () => {
104
145
  }
105
146
 
106
147
  const previousText = readGitBlob(repoRoot);
148
+ // FAIL CLOSED on "could not ask", ALLOW on "there is nothing to compare".
149
+ // Those are different findings and only the second is safe to permit: a
150
+ // config with no baseline at HEAD is a new file, while a baseline nobody
151
+ // could read is an unanswered question, and a guard that permits an edit
152
+ // because it was too busy to check has not checked.
153
+ if (previousText === UNREADABLE) {
154
+ process.stderr.write(
155
+ `Blocked: could not read HEAD:${CONFIG_PATH} — git was killed after ` +
156
+ `${String(GIT_BUDGET_MS)}ms without finishing, so the previous ` +
157
+ `extensions are unknown. This is a timeout, not a finding: nothing ` +
158
+ `about the edit was measured. Re-run when the machine is quieter.\n`
159
+ );
160
+ return BLOCKED;
161
+ }
107
162
  if (previousText === null) return ALLOWED;
108
163
 
109
164
  const previousExtensions = topLevelExtensionKeys(previousText);
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-harper-fabric",
3
- "version": "4.4.5",
3
+ "version": "4.4.7",
4
4
  "description": "Harper/Fabric-specific rules for TypeScript component apps",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-harper-fabric",
3
- "version": "4.4.5",
3
+ "version": "4.4.7",
4
4
  "description": "Harper/Fabric-specific rules for TypeScript component apps",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -59,6 +59,44 @@ const gitEnv = () =>
59
59
  Object.entries(process.env).filter(([key]) => !key.startsWith("GIT_"))
60
60
  );
61
61
 
62
+ /**
63
+ * Milliseconds before the baseline read is presumed hung.
64
+ *
65
+ * A hang detector, not a budget. `git` reached through PATH on macOS goes via
66
+ * Apple's `xcrun` shim, measured over 20 seconds under load against 11ms for a
67
+ * real binary (CodySwannGT/lisa#2887), so a tighter deadline would make this
68
+ * hook's own timeout the ordinary outcome on a busy machine.
69
+ */
70
+ const GIT_BUDGET_MS = 30_000;
71
+
72
+ /** The baseline could not be read at all, as distinct from "there is none". */
73
+ const UNREADABLE = Symbol("unreadable");
74
+
75
+ /**
76
+ * The config as HEAD holds it: its text, `null` when HEAD has no such file, or
77
+ * `UNREADABLE` when git could not be asked.
78
+ *
79
+ * THE THREE ANSWERS ARE DELIBERATELY THREE. This used to be
80
+ * `result.status === 0 ? result.stdout : null`, and `status === 0` is false for
81
+ * a file that does not exist at HEAD **and** for a child killed at its
82
+ * deadline — so both became `null`, and the caller reads `null` as "no baseline
83
+ * to compare against, allow the edit". A busy machine therefore let through
84
+ * exactly the extension removal this hook exists to block, and nothing anywhere
85
+ * said the word "time".
86
+ *
87
+ * A deadline alone does not fix that. `spawnSync` does not throw when it kills
88
+ * a child; it returns `{ status: null, stdout: "" }`, which takes the same
89
+ * branch it always did. The discrimination has to be written, not scheduled —
90
+ * which is why this call site needed more than the `timeout:` its siblings did.
91
+ *
92
+ * `ETIMEDOUT` is set by Node itself, so this is a platform fact rather than a
93
+ * convention invented here; the shared `bounded-child.mjs` reads the same field,
94
+ * and this hook writes it out inline because a plugin payload materialized as a
95
+ * hook has no module to import from.
96
+ * @param {string} repoRoot Repository root.
97
+ * @returns {string|null|symbol} The text, `null` when absent, `UNREADABLE` when
98
+ * the child was killed.
99
+ */
62
100
  const readGitBlob = repoRoot => {
63
101
  const result = spawnSync(
64
102
  "git",
@@ -66,8 +104,11 @@ const readGitBlob = repoRoot => {
66
104
  {
67
105
  encoding: "utf8",
68
106
  env: gitEnv(),
107
+ killSignal: "SIGKILL",
108
+ timeout: GIT_BUDGET_MS,
69
109
  }
70
110
  );
111
+ if (result.error?.code === "ETIMEDOUT") return UNREADABLE;
71
112
  return result.status === 0 ? result.stdout : null;
72
113
  };
73
114
 
@@ -104,6 +145,20 @@ const main = () => {
104
145
  }
105
146
 
106
147
  const previousText = readGitBlob(repoRoot);
148
+ // FAIL CLOSED on "could not ask", ALLOW on "there is nothing to compare".
149
+ // Those are different findings and only the second is safe to permit: a
150
+ // config with no baseline at HEAD is a new file, while a baseline nobody
151
+ // could read is an unanswered question, and a guard that permits an edit
152
+ // because it was too busy to check has not checked.
153
+ if (previousText === UNREADABLE) {
154
+ process.stderr.write(
155
+ `Blocked: could not read HEAD:${CONFIG_PATH} — git was killed after ` +
156
+ `${String(GIT_BUDGET_MS)}ms without finishing, so the previous ` +
157
+ `extensions are unknown. This is a timeout, not a finding: nothing ` +
158
+ `about the edit was measured. Re-run when the machine is quieter.\n`
159
+ );
160
+ return BLOCKED;
161
+ }
107
162
  if (previousText === null) return ALLOWED;
108
163
 
109
164
  const previousExtensions = topLevelExtensionKeys(previousText);
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-harper-fabric",
3
- "version": "4.4.5",
3
+ "version": "4.4.7",
4
4
  "description": "Harper/Fabric-specific rules for TypeScript component apps",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -59,6 +59,44 @@ const gitEnv = () =>
59
59
  Object.entries(process.env).filter(([key]) => !key.startsWith("GIT_"))
60
60
  );
61
61
 
62
+ /**
63
+ * Milliseconds before the baseline read is presumed hung.
64
+ *
65
+ * A hang detector, not a budget. `git` reached through PATH on macOS goes via
66
+ * Apple's `xcrun` shim, measured over 20 seconds under load against 11ms for a
67
+ * real binary (CodySwannGT/lisa#2887), so a tighter deadline would make this
68
+ * hook's own timeout the ordinary outcome on a busy machine.
69
+ */
70
+ const GIT_BUDGET_MS = 30_000;
71
+
72
+ /** The baseline could not be read at all, as distinct from "there is none". */
73
+ const UNREADABLE = Symbol("unreadable");
74
+
75
+ /**
76
+ * The config as HEAD holds it: its text, `null` when HEAD has no such file, or
77
+ * `UNREADABLE` when git could not be asked.
78
+ *
79
+ * THE THREE ANSWERS ARE DELIBERATELY THREE. This used to be
80
+ * `result.status === 0 ? result.stdout : null`, and `status === 0` is false for
81
+ * a file that does not exist at HEAD **and** for a child killed at its
82
+ * deadline — so both became `null`, and the caller reads `null` as "no baseline
83
+ * to compare against, allow the edit". A busy machine therefore let through
84
+ * exactly the extension removal this hook exists to block, and nothing anywhere
85
+ * said the word "time".
86
+ *
87
+ * A deadline alone does not fix that. `spawnSync` does not throw when it kills
88
+ * a child; it returns `{ status: null, stdout: "" }`, which takes the same
89
+ * branch it always did. The discrimination has to be written, not scheduled —
90
+ * which is why this call site needed more than the `timeout:` its siblings did.
91
+ *
92
+ * `ETIMEDOUT` is set by Node itself, so this is a platform fact rather than a
93
+ * convention invented here; the shared `bounded-child.mjs` reads the same field,
94
+ * and this hook writes it out inline because a plugin payload materialized as a
95
+ * hook has no module to import from.
96
+ * @param {string} repoRoot Repository root.
97
+ * @returns {string|null|symbol} The text, `null` when absent, `UNREADABLE` when
98
+ * the child was killed.
99
+ */
62
100
  const readGitBlob = repoRoot => {
63
101
  const result = spawnSync(
64
102
  "git",
@@ -66,8 +104,11 @@ const readGitBlob = repoRoot => {
66
104
  {
67
105
  encoding: "utf8",
68
106
  env: gitEnv(),
107
+ killSignal: "SIGKILL",
108
+ timeout: GIT_BUDGET_MS,
69
109
  }
70
110
  );
111
+ if (result.error?.code === "ETIMEDOUT") return UNREADABLE;
71
112
  return result.status === 0 ? result.stdout : null;
72
113
  };
73
114
 
@@ -104,6 +145,20 @@ const main = () => {
104
145
  }
105
146
 
106
147
  const previousText = readGitBlob(repoRoot);
148
+ // FAIL CLOSED on "could not ask", ALLOW on "there is nothing to compare".
149
+ // Those are different findings and only the second is safe to permit: a
150
+ // config with no baseline at HEAD is a new file, while a baseline nobody
151
+ // could read is an unanswered question, and a guard that permits an edit
152
+ // because it was too busy to check has not checked.
153
+ if (previousText === UNREADABLE) {
154
+ process.stderr.write(
155
+ `Blocked: could not read HEAD:${CONFIG_PATH} — git was killed after ` +
156
+ `${String(GIT_BUDGET_MS)}ms without finishing, so the previous ` +
157
+ `extensions are unknown. This is a timeout, not a finding: nothing ` +
158
+ `about the edit was measured. Re-run when the machine is quieter.\n`
159
+ );
160
+ return BLOCKED;
161
+ }
107
162
  if (previousText === null) return ALLOWED;
108
163
 
109
164
  const previousExtensions = topLevelExtensionKeys(previousText);
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-nestjs",
3
- "version": "4.4.5",
3
+ "version": "4.4.7",
4
4
  "description": "NestJS-specific skills (GraphQL, TypeORM) and hooks (migration write-protection)",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-nestjs",
3
- "version": "4.4.5",
3
+ "version": "4.4.7",
4
4
  "description": "NestJS-specific skills and migration write-protection hooks.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-nestjs",
3
- "version": "4.4.5",
3
+ "version": "4.4.7",
4
4
  "description": "NestJS-specific skills (GraphQL, TypeORM) and hooks (migration write-protection)",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-nestjs",
3
- "version": "4.4.5",
3
+ "version": "4.4.7",
4
4
  "description": "NestJS-specific skills (GraphQL, TypeORM) and hooks (migration write-protection)",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-nestjs",
3
- "version": "4.4.5",
3
+ "version": "4.4.7",
4
4
  "description": "NestJS-specific skills (GraphQL, TypeORM) and hooks (migration write-protection)",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-openclaw",
3
- "version": "4.4.5",
3
+ "version": "4.4.7",
4
4
  "description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-openclaw",
3
- "version": "4.4.5",
3
+ "version": "4.4.7",
4
4
  "description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, across Claude and Codex.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-openclaw",
3
- "version": "4.4.5",
3
+ "version": "4.4.7",
4
4
  "description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-openclaw",
3
- "version": "4.4.5",
3
+ "version": "4.4.7",
4
4
  "description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-openclaw",
3
- "version": "4.4.5",
3
+ "version": "4.4.7",
4
4
  "description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-phaser",
3
- "version": "4.4.5",
3
+ "version": "4.4.7",
4
4
  "description": "Phaser 4 game-development rules for TypeScript projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-phaser",
3
- "version": "4.4.5",
3
+ "version": "4.4.7",
4
4
  "description": "Phaser 4 game-development rules for TypeScript projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-phaser",
3
- "version": "4.4.5",
3
+ "version": "4.4.7",
4
4
  "description": "Phaser 4 game-development rules for TypeScript projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-phaser",
3
- "version": "4.4.5",
3
+ "version": "4.4.7",
4
4
  "description": "Phaser 4 game-development rules for TypeScript projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-phaser",
3
- "version": "4.4.5",
3
+ "version": "4.4.7",
4
4
  "description": "Phaser 4 game-development rules for TypeScript projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-rails",
3
- "version": "4.4.5",
3
+ "version": "4.4.7",
4
4
  "description": "Ruby on Rails-specific hooks — RuboCop linting/formatting and ast-grep scanning on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-rails",
3
- "version": "4.4.5",
3
+ "version": "4.4.7",
4
4
  "description": "Ruby on Rails-specific skills and hooks for RuboCop and ast-grep scanning on edit.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-rails",
3
- "version": "4.4.5",
3
+ "version": "4.4.7",
4
4
  "description": "Ruby on Rails-specific hooks — RuboCop linting/formatting and ast-grep scanning on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-rails",
3
- "version": "4.4.5",
3
+ "version": "4.4.7",
4
4
  "description": "Ruby on Rails-specific hooks — RuboCop linting/formatting and ast-grep scanning on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-rails",
3
- "version": "4.4.5",
3
+ "version": "4.4.7",
4
4
  "description": "Ruby on Rails-specific hooks — RuboCop linting/formatting and ast-grep scanning on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-typescript",
3
- "version": "4.4.5",
3
+ "version": "4.4.7",
4
4
  "description": "TypeScript-specific hooks — Prettier formatting, ESLint linting, ast-grep scanning, and error-suppression blocking on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-typescript",
3
- "version": "4.4.5",
3
+ "version": "4.4.7",
4
4
  "description": "TypeScript-specific hooks for formatting, linting, and ast-grep scanning on edit.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-typescript",
3
- "version": "4.4.5",
3
+ "version": "4.4.7",
4
4
  "description": "TypeScript-specific hooks — Prettier formatting, ESLint linting, ast-grep scanning, and error-suppression blocking on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-typescript",
3
- "version": "4.4.5",
3
+ "version": "4.4.7",
4
4
  "description": "TypeScript-specific hooks — Prettier formatting, ESLint linting, ast-grep scanning, and error-suppression blocking on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-typescript",
3
- "version": "4.4.5",
3
+ "version": "4.4.7",
4
4
  "description": "TypeScript-specific hooks — Prettier formatting, ESLint linting, ast-grep scanning, and error-suppression blocking on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-wiki",
3
- "version": "4.4.5",
3
+ "version": "4.4.7",
4
4
  "description": "LLM Wiki — a distributable, git-native markdown knowledge base for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-wiki",
3
- "version": "4.4.5",
3
+ "version": "4.4.7",
4
4
  "description": "Distributable LLM Wiki kernel — ingest, query, lint, and maintain a git-native markdown knowledge base across Claude and Codex.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -16,9 +16,10 @@
16
16
  * Usage: node diff-guard.mjs --allow <glob> [--allow <glob>...] [--base <ref>]
17
17
  * Exit 0 = all changes within allowed globs, 1 = out-of-bounds change (or git error).
18
18
  */
19
- import { execFileSync } from "node:child_process";
20
19
  import { globToRegExp } from "./_wiki-lib.mjs";
21
20
 
21
+ import { boundedChildOutput } from "./lib/bounded-child.mjs";
22
+
22
23
  function fail(msg) {
23
24
  console.error(`✗ ${msg}`);
24
25
  process.exit(1);
@@ -51,7 +52,7 @@ if (allows.length === 0) {
51
52
  }
52
53
 
53
54
  function git(args) {
54
- return execFileSync("git", args, { encoding: "utf8" });
55
+ return boundedChildOutput("git", args, { encoding: "utf8" });
55
56
  }
56
57
 
57
58
  let repoRoot;
@@ -49,7 +49,8 @@
49
49
  */
50
50
  import fs from "node:fs";
51
51
  import path from "node:path";
52
- import { execFileSync } from "node:child_process";
52
+
53
+ import { boundedChildOutput } from "./lib/bounded-child.mjs";
53
54
 
54
55
  const GITIGNORE_BEGIN = "# BEGIN: AI GUARDRAILS WIKI MIRROR";
55
56
  const GITIGNORE_END = "# END: AI GUARDRAILS WIKI MIRROR";
@@ -71,7 +72,7 @@ function readJsonSafe(file) {
71
72
  }
72
73
  }
73
74
  function git(args, cwd) {
74
- return execFileSync("git", args, {
75
+ return boundedChildOutput("git", args, {
75
76
  cwd,
76
77
  stdio: ["ignore", "pipe", "pipe"],
77
78
  encoding: "utf8",
@@ -14,9 +14,13 @@
14
14
  */
15
15
  import fs from "node:fs";
16
16
  import path from "node:path";
17
- import { execFileSync } from "node:child_process";
18
17
  import { readJsonSafe, writeSanitizedSourceNote } from "./_wiki-lib.mjs";
19
18
 
19
+ import {
20
+ boundedChildOutput,
21
+ rethrowIfChildTimeout,
22
+ } from "./lib/bounded-child.mjs";
23
+
20
24
  const argv = process.argv.slice(2);
21
25
  const opt = (n, d) => {
22
26
  const i = argv.indexOf(n);
@@ -35,7 +39,7 @@ const fail = m => {
35
39
  process.exit(1);
36
40
  };
37
41
  const git = args =>
38
- execFileSync("git", ["-C", repo, ...args], { encoding: "utf8" }).trim();
42
+ boundedChildOutput("git", ["-C", repo, ...args], { encoding: "utf8" }).trim();
39
43
  const tryGit = args => {
40
44
  try {
41
45
  return git(args);
@@ -45,11 +49,13 @@ const tryGit = args => {
45
49
  };
46
50
  const commitExists = c => {
47
51
  try {
48
- execFileSync("git", ["-C", repo, "cat-file", "-e", `${c}^{commit}`], {
52
+ boundedChildOutput("git", ["-C", repo, "cat-file", "-e", `${c}^{commit}`], {
49
53
  stdio: "ignore",
50
54
  });
51
55
  return true;
52
- } catch {
56
+ } catch (error) {
57
+ // `false` is a verdict of "absent", not an admission of ignorance.
58
+ rethrowIfChildTimeout(error);
53
59
  return false;
54
60
  }
55
61
  };
@@ -87,7 +93,7 @@ let prSummary = "(no GitHub repo resolved — PR history skipped)";
87
93
  let ghRepo = githubRepo;
88
94
  if (!ghRepo) {
89
95
  try {
90
- ghRepo = execFileSync(
96
+ ghRepo = boundedChildOutput(
91
97
  "gh",
92
98
  ["repo", "view", "--json", "nameWithOwner", "-q", ".nameWithOwner"],
93
99
  { cwd: repo, encoding: "utf8" }
@@ -99,7 +105,7 @@ if (!ghRepo) {
99
105
  if (ghRepo) {
100
106
  try {
101
107
  const prs = JSON.parse(
102
- execFileSync(
108
+ boundedChildOutput(
103
109
  "gh",
104
110
  [
105
111
  "pr",