@codyswann/lisa 4.1.6 → 4.3.1
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.
- package/all/copy-overwrite/scripts/lib/bounded-spawn.mjs +188 -0
- package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.js +4 -0
- package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
- package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +12 -1
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/expo/copy-overwrite/scripts/lib/bounded-spawn.mjs +188 -0
- package/package.json +2 -1
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-agy/plugin.json +1 -1
- package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-agy/plugin.json +1 -1
- package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-agy/plugin.json +1 -1
- package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-agy/plugin.json +1 -1
- package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-agy/plugin.json +1 -1
- package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-agy/plugin.json +1 -1
- package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-agy/plugin.json +1 -1
- package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-agy/plugin.json +1 -1
- package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
- package/scripts/build-plugins.sh +28 -0
- package/scripts/lib/bounded-spawn.mjs +185 -0
- package/typescript/copy-overwrite/scripts/lib/bounded-spawn.mjs +188 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
// This file is managed by Lisa and IS replaced on each `lisa` run.
|
|
2
|
+
// Do not edit directly — durable changes belong upstream in Lisa.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* bounded-spawn — the one implementation of "start a child, and refuse to
|
|
6
|
+
* return a verdict from one that never finished", shared by every guarded
|
|
7
|
+
* `.mjs` entry point Lisa ships that starts a synchronous child.
|
|
8
|
+
*
|
|
9
|
+
* @remarks
|
|
10
|
+
* ## The defect this exists to remove
|
|
11
|
+
*
|
|
12
|
+
* A test with a hung child fails a gate. **A guard with a hung child returns a
|
|
13
|
+
* verdict.** That is the whole reason this module is separate from the test
|
|
14
|
+
* tree's equivalent: out here the consequence of a child that never answered
|
|
15
|
+
* is a check that says "allow".
|
|
16
|
+
*
|
|
17
|
+
* The mechanism is one line, and it is measured rather than reasoned. These are
|
|
18
|
+
* the three outcomes of a synchronous child start, on this platform:
|
|
19
|
+
*
|
|
20
|
+
* | outcome | `.code` | `.status` | `.signal` | streams |
|
|
21
|
+
* | --- | --- | --- | --- | --- |
|
|
22
|
+
* | killed at its deadline | `"ETIMEDOUT"` | `null` | `"SIGKILL"` | **empty** |
|
|
23
|
+
* | ran and exited non-zero | *(absent)* | `3` | `null` | real |
|
|
24
|
+
* | binary not found | `"ENOENT"` | `null` | `null` | empty |
|
|
25
|
+
*
|
|
26
|
+
* Now read the shape that appears all over this repository:
|
|
27
|
+
*
|
|
28
|
+
* ```js
|
|
29
|
+
* return result.status === 0 ? result.stdout : null;
|
|
30
|
+
* ```
|
|
31
|
+
*
|
|
32
|
+
* `status === 0` is false for a killed child **and** for a program that ran and
|
|
33
|
+
* said no, so the two are indistinguishable and both become `null`. The caller
|
|
34
|
+
* treats `null` as "the answer is no" and proceeds. **A busy machine therefore
|
|
35
|
+
* produces the same value a clean answer does**, and nothing anywhere says the
|
|
36
|
+
* word "time".
|
|
37
|
+
*
|
|
38
|
+
* That is why the fail-open spelling was never a careless choice. It is
|
|
39
|
+
* indistinguishable from correct handling unless you already know the kill case
|
|
40
|
+
* exists — which is precisely the argument for putting the discrimination in
|
|
41
|
+
* one shared place instead of asking 80 call sites to remember it.
|
|
42
|
+
*
|
|
43
|
+
* ## Why this THROWS
|
|
44
|
+
*
|
|
45
|
+
* Returning a distinguishable value would leave every call site free to ignore
|
|
46
|
+
* it, and the call sites that most need to check are the ones that already
|
|
47
|
+
* decided a falsy result means "no". Throwing inverts the default: a call site
|
|
48
|
+
* that does nothing gets fail-closed behaviour, and only a call site that
|
|
49
|
+
* *deliberately* catches can proceed.
|
|
50
|
+
*
|
|
51
|
+
* Measured on the tree this was written for: of 80 synchronous child starts in
|
|
52
|
+
* shipped scripts, **51 propagate** — a throw escapes and fails the process, so
|
|
53
|
+
* those are fixed by this module alone — and **29 sit inside a `try` with a
|
|
54
|
+
* `catch`**, where a throw would be swallowed. Those 29 are not a conversion
|
|
55
|
+
* job; each needs a human answer to "what should happen when this child is
|
|
56
|
+
* killed?", and the answer is written at the call site with
|
|
57
|
+
* {@link isChildTimeout}.
|
|
58
|
+
*
|
|
59
|
+
* ## Why the discriminator is `error.code`, not a custom error class
|
|
60
|
+
*
|
|
61
|
+
* A class cannot cross the boundary. Plugin payloads have no `./lib/` to import
|
|
62
|
+
* from — the accommodation `threshold-ratchet.mjs` and `preflight-secrets.mjs`
|
|
63
|
+
* already make — so a shared class would be unavailable in exactly the tree
|
|
64
|
+
* with the most call sites. `ETIMEDOUT` is set by Node itself on all three
|
|
65
|
+
* forms (`spawnSync` puts it on `result.error`; `execFileSync` and `execSync`
|
|
66
|
+
* throw an error carrying it), so a payload that cannot import this module
|
|
67
|
+
* writes the identical check inline and is stating the same **platform fact**
|
|
68
|
+
* rather than duplicating a Lisa convention.
|
|
69
|
+
*
|
|
70
|
+
* `ENOENT` is deliberately NOT treated as a timeout. A missing binary is a real
|
|
71
|
+
* answer about the environment, it is reported the same way on every run, and
|
|
72
|
+
* conflating it with a kill would hide a broken install behind a retryable-
|
|
73
|
+
* looking error.
|
|
74
|
+
* @module scripts/lib/bounded-spawn
|
|
75
|
+
*/
|
|
76
|
+
|
|
77
|
+
import { execFileSync, spawnSync } from "node:child_process";
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Deadline a child gets when the caller states none, in milliseconds.
|
|
81
|
+
*
|
|
82
|
+
* Deliberately generous. This is not a performance budget — it is the point
|
|
83
|
+
* past which a child is presumed hung, and the cost of setting it too low is a
|
|
84
|
+
* guard that fails on a slow machine while the cost of setting it too high is
|
|
85
|
+
* only a slower failure. `git` resolved through `PATH` on macOS goes through
|
|
86
|
+
* Apple's `xcrun` shim, which has been measured at over 20 seconds under load
|
|
87
|
+
* against 11ms for a real binary (CodySwannGT/lisa#2887).
|
|
88
|
+
*
|
|
89
|
+
* **30s is not padding, and tightening it is not a safe optimisation.** A
|
|
90
|
+
* deadline below the shim's measured worst case would make this module's own
|
|
91
|
+
* timeout the dominant failure mode — the tool would start manufacturing the
|
|
92
|
+
* failures it exists to detect, and every one of them would look exactly like
|
|
93
|
+
* the defect it was built to catch. If a number here ever needs changing, the
|
|
94
|
+
* thing to change first is `git` being resolved through `PATH` at all.
|
|
95
|
+
*/
|
|
96
|
+
export const DEFAULT_CHILD_BUDGET_MS = 30_000;
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Whether this error is a child that was killed at its deadline.
|
|
100
|
+
*
|
|
101
|
+
* The one question a `catch` around a bounded child start has to ask. A `catch`
|
|
102
|
+
* that does not ask it treats "the box was busy" as "the command said no",
|
|
103
|
+
* which is the fail-open this module exists to remove — so a catch block
|
|
104
|
+
* enclosing a bounded start must either re-raise on this or say in a comment
|
|
105
|
+
* why continuing is correct.
|
|
106
|
+
*
|
|
107
|
+
* Accepts `unknown` because that is what a `catch` binding is, and reads the
|
|
108
|
+
* property defensively for the same reason: a thrown non-object is legal
|
|
109
|
+
* JavaScript and must answer `false` rather than crash the guard that was
|
|
110
|
+
* trying to be careful.
|
|
111
|
+
* @param {unknown} error A caught value, or `result.error` from `spawnSync`.
|
|
112
|
+
* @returns {boolean} True when the child was killed at its deadline.
|
|
113
|
+
*/
|
|
114
|
+
export function isChildTimeout(error) {
|
|
115
|
+
return (
|
|
116
|
+
typeof error === "object" &&
|
|
117
|
+
error !== null &&
|
|
118
|
+
/** @type {{ code?: unknown }} */ (error).code === "ETIMEDOUT"
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Re-raise a caught value when it is a killed child, otherwise do nothing.
|
|
124
|
+
*
|
|
125
|
+
* The one-line form for the 29 call sites whose `catch` predates this module.
|
|
126
|
+
* Written as a helper rather than left to each site because the correct spelling
|
|
127
|
+
* is `throw error` — NOT wrapping, NOT `throw new Error(...)` — and a wrapped
|
|
128
|
+
* error loses the `code` the next frame up needs to make the same decision.
|
|
129
|
+
* @param {unknown} error The caught value.
|
|
130
|
+
* @returns {void}
|
|
131
|
+
* @throws {unknown} The original value, when it is a killed child.
|
|
132
|
+
*/
|
|
133
|
+
export function rethrowIfChildTimeout(error) {
|
|
134
|
+
if (isChildTimeout(error)) throw error;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Options a bounded start always applies, whatever the caller asked for.
|
|
139
|
+
*
|
|
140
|
+
* `killSignal` is `SIGKILL` rather than the default `SIGTERM` deliberately: the
|
|
141
|
+
* hang this module exists for is a process that is not servicing signals, and a
|
|
142
|
+
* `SIGTERM` a child ignores turns the deadline into a suggestion.
|
|
143
|
+
* @param {object} options The caller's options.
|
|
144
|
+
* @returns {object} The caller's options with the deadline enforced.
|
|
145
|
+
*/
|
|
146
|
+
function bounded(options) {
|
|
147
|
+
return {
|
|
148
|
+
...options,
|
|
149
|
+
killSignal: "SIGKILL",
|
|
150
|
+
timeout: options.timeout ?? DEFAULT_CHILD_BUDGET_MS,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* `spawnSync` with a deadline, refusing to return a killed child's result.
|
|
156
|
+
*
|
|
157
|
+
* `spawnSync` does not throw on a timeout — it returns a result whose streams
|
|
158
|
+
* are EMPTY and whose `status` is `null`, which is why the fail-open shape in
|
|
159
|
+
* the module docblock cannot see it. This throws instead, so a caller that does
|
|
160
|
+
* nothing inherits fail-closed behaviour.
|
|
161
|
+
* @param {string} command The executable.
|
|
162
|
+
* @param {readonly string[]} args Arguments.
|
|
163
|
+
* @param {object} [options] `spawnSync` options; `timeout` overrides the default.
|
|
164
|
+
* @returns {import("node:child_process").SpawnSyncReturns<string>} The result.
|
|
165
|
+
* @throws {Error} When the child was killed at its deadline.
|
|
166
|
+
*/
|
|
167
|
+
export function boundedSpawnSync(command, args = [], options = {}) {
|
|
168
|
+
const result = spawnSync(command, [...args], bounded(options));
|
|
169
|
+
if (isChildTimeout(result.error)) throw result.error;
|
|
170
|
+
return result;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* `execFileSync` with a deadline.
|
|
175
|
+
*
|
|
176
|
+
* `execFileSync` already throws on a timeout, so this changes the FAILURE MODE
|
|
177
|
+
* rather than adding one: without a `timeout` there is no deadline to reach and
|
|
178
|
+
* the call blocks for as long as the child lives. The throw it produces carries
|
|
179
|
+
* `code: "ETIMEDOUT"`, which is what {@link isChildTimeout} reads.
|
|
180
|
+
* @param {string} command The executable.
|
|
181
|
+
* @param {readonly string[]} args Arguments.
|
|
182
|
+
* @param {object} [options] `execFileSync` options; `timeout` overrides the default.
|
|
183
|
+
* @returns {string|Buffer} Whatever the child wrote to stdout.
|
|
184
|
+
* @throws {Error} When the child was killed, or exited non-zero.
|
|
185
|
+
*/
|
|
186
|
+
export function boundedExecFileSync(command, args = [], options = {}) {
|
|
187
|
+
return execFileSync(command, [...args], bounded(options));
|
|
188
|
+
}
|
package/package.json
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
"scripts": {
|
|
3
3
|
"build": "bun run build:dist && bun run build:plugins",
|
|
4
4
|
"build:dist": "node scripts/clean-dist.mjs && tsc && node scripts/copy-codex-scripts.mjs && node scripts/copy-opencode-plugin-templates.mjs",
|
|
5
|
+
"build:dist:in-place": "tsc && node scripts/copy-codex-scripts.mjs && node scripts/copy-opencode-plugin-templates.mjs",
|
|
5
6
|
"test:integration": "vitest run tests/integration",
|
|
6
7
|
"test:integration:push": "vitest run tests/integration --exclude='**/mutation-gate-bite.test.ts' --exclude='**/mutation-gate-diff-bite.test.ts' --exclude='**/mutation-sigterm-control.test.ts'",
|
|
7
8
|
"postinstall": "bash ./scripts/install-claude-plugins.sh || true; [ -d dist/configs ] || tsc || true",
|
|
@@ -144,7 +145,7 @@
|
|
|
144
145
|
"zod-validation-error": "^4.0.0"
|
|
145
146
|
},
|
|
146
147
|
"name": "@codyswann/lisa",
|
|
147
|
-
"version": "4.1
|
|
148
|
+
"version": "4.3.1",
|
|
148
149
|
"description": "Claude Code governance framework that applies guardrails, guidance, and automated enforcement to projects",
|
|
149
150
|
"main": "dist/index.js",
|
|
150
151
|
"exports": {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lisa-openclaw",
|
|
3
|
-
"version": "4.1
|
|
3
|
+
"version": "4.3.1",
|
|
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.1
|
|
3
|
+
"version": "4.3.1",
|
|
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.1
|
|
3
|
+
"version": "4.3.1",
|
|
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.1
|
|
3
|
+
"version": "4.3.1",
|
|
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.1
|
|
3
|
+
"version": "4.3.1",
|
|
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"
|