@codyswann/lisa 4.4.6 → 4.4.8
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/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +29 -16
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/package.json +1 -1
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/skills/lisa-drive-pr-to-merge/SKILL.md +30 -1
- package/plugins/lisa/.codex-plugin/skills/lisa-remote-dispatch/scripts/dispatch.mjs +4 -3
- package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/providers.mjs +11 -5
- package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/sync-secret-to-ci.mjs +3 -2
- package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +36 -23
- package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/scripts/verify-remote-env.mjs +7 -3
- package/plugins/lisa/.codex-plugin/skills/lisa-setup-workstation/scripts/bounded-child.mjs +159 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-setup-workstation/scripts/workstation.mjs +5 -4
- package/plugins/lisa/scripts/design-source-gate.mjs +19 -1
- package/plugins/lisa/scripts/plugin-sync-explain.mjs +23 -1
- package/plugins/lisa/scripts/project-ideation-idempotency-harness.mjs +20 -1
- package/plugins/lisa/skills/lisa-drive-pr-to-merge/SKILL.md +30 -1
- package/plugins/lisa/skills/lisa-remote-dispatch/scripts/dispatch.mjs +4 -3
- package/plugins/lisa/skills/lisa-secrets-access/scripts/providers.mjs +11 -5
- package/plugins/lisa/skills/lisa-secrets-access/scripts/sync-secret-to-ci.mjs +3 -2
- package/plugins/lisa/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +36 -23
- package/plugins/lisa/skills/lisa-setup-remote-env/scripts/verify-remote-env.mjs +7 -3
- package/plugins/lisa/skills/lisa-setup-workstation/scripts/bounded-child.mjs +159 -0
- package/plugins/lisa/skills/lisa-setup-workstation/scripts/workstation.mjs +5 -4
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-agy/scripts/design-source-gate.mjs +19 -1
- package/plugins/lisa-agy/scripts/plugin-sync-explain.mjs +23 -1
- package/plugins/lisa-agy/scripts/project-ideation-idempotency-harness.mjs +20 -1
- package/plugins/lisa-agy/skills/lisa-drive-pr-to-merge/SKILL.md +30 -1
- package/plugins/lisa-agy/skills/lisa-remote-dispatch/scripts/dispatch.mjs +4 -3
- package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/providers.mjs +11 -5
- package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/sync-secret-to-ci.mjs +3 -2
- package/plugins/lisa-agy/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +36 -23
- package/plugins/lisa-agy/skills/lisa-setup-remote-env/scripts/verify-remote-env.mjs +7 -3
- package/plugins/lisa-agy/skills/lisa-setup-workstation/scripts/bounded-child.mjs +159 -0
- package/plugins/lisa-agy/skills/lisa-setup-workstation/scripts/workstation.mjs +5 -4
- 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-copilot/scripts/design-source-gate.mjs +19 -1
- package/plugins/lisa-copilot/scripts/plugin-sync-explain.mjs +23 -1
- package/plugins/lisa-copilot/scripts/project-ideation-idempotency-harness.mjs +20 -1
- package/plugins/lisa-copilot/skills/lisa-drive-pr-to-merge/SKILL.md +30 -1
- package/plugins/lisa-copilot/skills/lisa-remote-dispatch/scripts/dispatch.mjs +4 -3
- package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/providers.mjs +11 -5
- package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/sync-secret-to-ci.mjs +3 -2
- package/plugins/lisa-copilot/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +36 -23
- package/plugins/lisa-copilot/skills/lisa-setup-remote-env/scripts/verify-remote-env.mjs +7 -3
- package/plugins/lisa-copilot/skills/lisa-setup-workstation/scripts/bounded-child.mjs +159 -0
- package/plugins/lisa-copilot/skills/lisa-setup-workstation/scripts/workstation.mjs +5 -4
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/scripts/design-source-gate.mjs +19 -1
- package/plugins/lisa-cursor/scripts/plugin-sync-explain.mjs +23 -1
- package/plugins/lisa-cursor/scripts/project-ideation-idempotency-harness.mjs +20 -1
- package/plugins/lisa-cursor/skills/lisa-drive-pr-to-merge/SKILL.md +30 -1
- package/plugins/lisa-cursor/skills/lisa-remote-dispatch/scripts/dispatch.mjs +4 -3
- package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/providers.mjs +11 -5
- package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/sync-secret-to-ci.mjs +3 -2
- package/plugins/lisa-cursor/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +36 -23
- package/plugins/lisa-cursor/skills/lisa-setup-remote-env/scripts/verify-remote-env.mjs +7 -3
- package/plugins/lisa-cursor/skills/lisa-setup-workstation/scripts/bounded-child.mjs +159 -0
- package/plugins/lisa-cursor/skills/lisa-setup-workstation/scripts/workstation.mjs +5 -4
- 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/hooks/enforce-config-extensions.mjs +55 -0
- 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-copilot/hooks/enforce-config-extensions.mjs +55 -0
- package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-cursor/hooks/enforce-config-extensions.mjs +55 -0
- 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/scripts/diff-guard.mjs +3 -2
- package/plugins/lisa-wiki/scripts/ensure-wiki.mjs +3 -2
- package/plugins/lisa-wiki/scripts/ingest-git.mjs +12 -6
- package/plugins/lisa-wiki/scripts/lib/bounded-child.mjs +159 -0
- package/plugins/lisa-wiki/scripts/verify-migration.mjs +3 -2
- package/plugins/lisa-wiki/scripts/verify-wiki-safety.mjs +15 -5
- package/plugins/lisa-wiki-agy/plugin.json +1 -1
- package/plugins/lisa-wiki-agy/scripts/diff-guard.mjs +3 -2
- package/plugins/lisa-wiki-agy/scripts/ensure-wiki.mjs +3 -2
- package/plugins/lisa-wiki-agy/scripts/ingest-git.mjs +12 -6
- package/plugins/lisa-wiki-agy/scripts/lib/bounded-child.mjs +159 -0
- package/plugins/lisa-wiki-agy/scripts/verify-migration.mjs +3 -2
- package/plugins/lisa-wiki-agy/scripts/verify-wiki-safety.mjs +15 -5
- package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-copilot/scripts/diff-guard.mjs +3 -2
- package/plugins/lisa-wiki-copilot/scripts/ensure-wiki.mjs +3 -2
- package/plugins/lisa-wiki-copilot/scripts/ingest-git.mjs +12 -6
- package/plugins/lisa-wiki-copilot/scripts/lib/bounded-child.mjs +159 -0
- package/plugins/lisa-wiki-copilot/scripts/verify-migration.mjs +3 -2
- package/plugins/lisa-wiki-copilot/scripts/verify-wiki-safety.mjs +15 -5
- package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-cursor/scripts/diff-guard.mjs +3 -2
- package/plugins/lisa-wiki-cursor/scripts/ensure-wiki.mjs +3 -2
- package/plugins/lisa-wiki-cursor/scripts/ingest-git.mjs +12 -6
- package/plugins/lisa-wiki-cursor/scripts/lib/bounded-child.mjs +159 -0
- package/plugins/lisa-wiki-cursor/scripts/verify-migration.mjs +3 -2
- package/plugins/lisa-wiki-cursor/scripts/verify-wiki-safety.mjs +15 -5
- package/plugins/src/base/scripts/design-source-gate.mjs +19 -1
- package/plugins/src/base/scripts/plugin-sync-explain.mjs +23 -1
- package/plugins/src/base/scripts/project-ideation-idempotency-harness.mjs +20 -1
- package/plugins/src/base/skills/lisa-drive-pr-to-merge/SKILL.md +30 -1
- package/plugins/src/base/skills/lisa-remote-dispatch/scripts/dispatch.mjs +4 -3
- package/plugins/src/base/skills/lisa-secrets-access/scripts/providers.mjs +11 -5
- package/plugins/src/base/skills/lisa-secrets-access/scripts/sync-secret-to-ci.mjs +3 -2
- package/plugins/src/base/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +36 -23
- package/plugins/src/base/skills/lisa-setup-remote-env/scripts/verify-remote-env.mjs +7 -3
- package/plugins/src/base/skills/lisa-setup-workstation/scripts/bounded-child.mjs +159 -0
- package/plugins/src/base/skills/lisa-setup-workstation/scripts/workstation.mjs +5 -4
- package/plugins/src/harper-fabric/hooks/enforce-config-extensions.mjs +55 -0
- package/plugins/src/wiki/scripts/diff-guard.mjs +3 -2
- package/plugins/src/wiki/scripts/ensure-wiki.mjs +3 -2
- package/plugins/src/wiki/scripts/ingest-git.mjs +12 -6
- package/plugins/src/wiki/scripts/lib/bounded-child.mjs +159 -0
- package/plugins/src/wiki/scripts/verify-migration.mjs +3 -2
- package/plugins/src/wiki/scripts/verify-wiki-safety.mjs +15 -5
|
@@ -21,7 +21,6 @@
|
|
|
21
21
|
* @module verify-remote-env
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
-
import { execFileSync } from "node:child_process";
|
|
25
24
|
import { fileURLToPath } from "node:url";
|
|
26
25
|
import { existsSync, readFileSync, statSync, realpathSync } from "node:fs";
|
|
27
26
|
import { join } from "node:path";
|
|
@@ -29,6 +28,8 @@ import { join } from "node:path";
|
|
|
29
28
|
import { probe, readRemoteEnvConfig } from "./setup-remote-env.mjs";
|
|
30
29
|
import { planToolchain } from "./toolchain.mjs";
|
|
31
30
|
|
|
31
|
+
import { boundedChildOutput } from "../../lisa-setup-workstation/scripts/bounded-child.mjs";
|
|
32
|
+
|
|
32
33
|
/** Collected results, so every check runs before anything reports failure. */
|
|
33
34
|
const results = [];
|
|
34
35
|
|
|
@@ -73,12 +74,15 @@ function checkMode(path, mode, label) {
|
|
|
73
74
|
*/
|
|
74
75
|
function node(args) {
|
|
75
76
|
try {
|
|
76
|
-
const out =
|
|
77
|
+
const out = boundedChildOutput("node", args, {
|
|
77
78
|
encoding: "utf8",
|
|
78
79
|
stdio: ["ignore", "pipe", "pipe"],
|
|
79
80
|
});
|
|
80
81
|
return { ok: true, out: out.trim() };
|
|
81
82
|
} catch (err) {
|
|
83
|
+
// KEPT: `ok: false` fails the check either way, and a killed child's
|
|
84
|
+
// `err.message` already reads "... ETIMEDOUT", so the timeout names
|
|
85
|
+
// itself. This is the shape the others are being moved toward.
|
|
82
86
|
return { ok: false, out: String(err.stderr ?? err.message).trim() };
|
|
83
87
|
}
|
|
84
88
|
}
|
|
@@ -149,7 +153,7 @@ export function verifyNotProxied(required, env, report) {
|
|
|
149
153
|
*/
|
|
150
154
|
function verifyCleanCheckout() {
|
|
151
155
|
try {
|
|
152
|
-
const out =
|
|
156
|
+
const out = boundedChildOutput("git", ["status", "--porcelain"], {
|
|
153
157
|
encoding: "utf8",
|
|
154
158
|
stdio: ["ignore", "pipe", "ignore"],
|
|
155
159
|
});
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bounded-child — the plugin payload's child-start deadline.
|
|
3
|
+
*
|
|
4
|
+
* @remarks
|
|
5
|
+
* ## Why this exists separately from `scripts/lib/bounded-spawn.mjs`
|
|
6
|
+
*
|
|
7
|
+
* Same property, different reach. A plugin payload cannot import from
|
|
8
|
+
* `scripts/lib/`: the built plugin is a self-contained copy of
|
|
9
|
+
* `plugins/src/<name>/`, and nothing outside that tree exists at runtime. So the
|
|
10
|
+
* rule is written out here rather than pointed at — the accommodation
|
|
11
|
+
* `threshold-ratchet.mjs` and `preflight-secrets.mjs` already make for their
|
|
12
|
+
* entry guards.
|
|
13
|
+
*
|
|
14
|
+
* It is a MODULE rather than 28 inline copies because the fanout preserves
|
|
15
|
+
* relative imports. Measured in the built output, not assumed:
|
|
16
|
+
* `plugins/lisa/skills/lisa-setup-workstation/scripts/catalogue.mjs` exists and
|
|
17
|
+
* is imported as `../../lisa-setup-workstation/scripts/catalogue.mjs` from a
|
|
18
|
+
* sibling skill. The build copies the plugin tree wholesale, so a path that
|
|
19
|
+
* resolves in `plugins/src/base/` resolves in all five copies.
|
|
20
|
+
*
|
|
21
|
+
* That distinction matters because the accommodation above is narrower than its
|
|
22
|
+
* wording suggests. `threshold-ratchet.mjs` cannot import a sibling because it
|
|
23
|
+
* is **materialized into other lanes** — `typescript/`, `rails/` — where the
|
|
24
|
+
* sibling does not exist at the destination. A file that lives only in this
|
|
25
|
+
* plugin has no such problem.
|
|
26
|
+
*
|
|
27
|
+
* ## The one thing a deadline alone does not fix
|
|
28
|
+
*
|
|
29
|
+
* `spawnSync` does not throw when it kills a child. It returns:
|
|
30
|
+
*
|
|
31
|
+
* | outcome | `.code` | `.status` | `.signal` | streams |
|
|
32
|
+
* | --- | --- | --- | --- | --- |
|
|
33
|
+
* | killed at deadline | `"ETIMEDOUT"` | `null` | `"SIGKILL"` | **empty** |
|
|
34
|
+
* | ran, exited non-zero | *(absent)* | `3` | `null` | real |
|
|
35
|
+
* | binary not found | `"ENOENT"` | `null` | `null` | empty |
|
|
36
|
+
*
|
|
37
|
+
* So the shape that appears all over this tree —
|
|
38
|
+
*
|
|
39
|
+
* ```js
|
|
40
|
+
* return result.status === 0 ? result.stdout : null;
|
|
41
|
+
* ```
|
|
42
|
+
*
|
|
43
|
+
* — takes the benign branch for a killed child exactly as it does for a program
|
|
44
|
+
* that ran and said no. **Adding `timeout:` gives such a call site a deadline
|
|
45
|
+
* and changes nothing about its verdict.** There is no `catch` to fix, because
|
|
46
|
+
* the fail-open is in the conditional.
|
|
47
|
+
*
|
|
48
|
+
* That is why `boundedChild` THROWS rather than returning a flag: a caller that
|
|
49
|
+
* does nothing inherits fail-closed behaviour, and only one that deliberately
|
|
50
|
+
* catches can proceed.
|
|
51
|
+
* ## Why it lives in THIS skill's directory
|
|
52
|
+
*
|
|
53
|
+
* It is not about workstation setup, and it is here anyway, for a measured
|
|
54
|
+
* reason. The built Codex artifact — `plugins/lisa/.codex-plugin/` — copies
|
|
55
|
+
* `skills/` and nothing else: no `scripts/`, `agents/`, `commands/` or
|
|
56
|
+
* `rules/`. So a skill script importing `../../../scripts/lib/…` resolves to a
|
|
57
|
+
* path that does not exist in that copy, while `../../<skill>/scripts/…`
|
|
58
|
+
* resolves everywhere, which is exactly why `catalogue.mjs` beside this file is
|
|
59
|
+
* already imported that way from a sibling skill.
|
|
60
|
+
*
|
|
61
|
+
* A directory of its own under `skills/` was the tidier home and was rejected
|
|
62
|
+
* on evidence: every `skills/` subdirectory in this tree is a real skill with a
|
|
63
|
+
* `SKILL.md`, the generators parse that file to build their artifacts, and a
|
|
64
|
+
* directory without one is a shape nothing here has ever produced. Choosing an
|
|
65
|
+
* unproven layout is what put the import in the wrong place to begin with.
|
|
66
|
+
*
|
|
67
|
+
* **Do not delete this as unused from its host skill.** Its importers are in
|
|
68
|
+
* other skills and in `../../../scripts/`; `lisa-setup-workstation` hosts it,
|
|
69
|
+
* it does not own it.
|
|
70
|
+
* @module bounded-child
|
|
71
|
+
*/
|
|
72
|
+
|
|
73
|
+
import { execFileSync, spawnSync } from "node:child_process";
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Deadline a child gets when the caller states none, in milliseconds.
|
|
77
|
+
*
|
|
78
|
+
* A hang detector, not a performance budget. `git` resolved through `PATH` on
|
|
79
|
+
* macOS goes via Apple's `xcrun` shim, measured at over 20 seconds under load
|
|
80
|
+
* against 11ms for a real binary (CodySwannGT/lisa#2887) — so a tighter
|
|
81
|
+
* deadline would make this module's own timeout the dominant failure mode, and
|
|
82
|
+
* the tool would start manufacturing the failures it exists to detect.
|
|
83
|
+
*/
|
|
84
|
+
export const CHILD_BUDGET_MS = 30_000;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Whether this value is a child that was killed at its deadline.
|
|
88
|
+
*
|
|
89
|
+
* Reads `code` rather than an error class, because `ETIMEDOUT` is set by Node
|
|
90
|
+
* itself on all three call forms — it is a platform fact, not a convention this
|
|
91
|
+
* module invented, so the same test is correct anywhere including inline.
|
|
92
|
+
*
|
|
93
|
+
* Defensive about the shape: a `catch` binding is `unknown` and a thrown
|
|
94
|
+
* non-object is legal JavaScript, so this must answer `false` rather than crash
|
|
95
|
+
* the guard that was being careful.
|
|
96
|
+
* @param {unknown} error A caught value, or `result.error` from `spawnSync`.
|
|
97
|
+
* @returns {boolean} True when the child was killed at its deadline.
|
|
98
|
+
*/
|
|
99
|
+
export function isChildTimeout(error) {
|
|
100
|
+
return (
|
|
101
|
+
typeof error === "object" &&
|
|
102
|
+
error !== null &&
|
|
103
|
+
/** @type {{ code?: unknown }} */ (error).code === "ETIMEDOUT"
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Re-raise a caught value when it is a killed child, otherwise do nothing.
|
|
109
|
+
*
|
|
110
|
+
* The correct spelling is `throw error` — not wrapping — because a wrapped
|
|
111
|
+
* error loses the `code` the next frame up needs to make the same decision.
|
|
112
|
+
* @param {unknown} error The caught value.
|
|
113
|
+
* @returns {void}
|
|
114
|
+
* @throws {unknown} The original value, when it is a killed child.
|
|
115
|
+
*/
|
|
116
|
+
export function rethrowIfChildTimeout(error) {
|
|
117
|
+
if (isChildTimeout(error)) throw error;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* `spawnSync` with a deadline, refusing to return a killed child's result.
|
|
122
|
+
* @param {string} command The executable.
|
|
123
|
+
* @param {readonly string[]} args Arguments.
|
|
124
|
+
* @param {object} [options] `spawnSync` options; `timeout` overrides the default.
|
|
125
|
+
* @returns {import("node:child_process").SpawnSyncReturns<string>} The result.
|
|
126
|
+
* @throws {Error} When the child was killed at its deadline.
|
|
127
|
+
*/
|
|
128
|
+
export function boundedChild(command, args = [], options = {}) {
|
|
129
|
+
const result = spawnSync(command, [...args], {
|
|
130
|
+
...options,
|
|
131
|
+
// `SIGKILL` rather than the default `SIGTERM`: the hang this exists for is
|
|
132
|
+
// a process not servicing signals, and a `SIGTERM` a child ignores turns
|
|
133
|
+
// the deadline into a suggestion.
|
|
134
|
+
killSignal: "SIGKILL",
|
|
135
|
+
timeout: options.timeout ?? CHILD_BUDGET_MS,
|
|
136
|
+
});
|
|
137
|
+
if (isChildTimeout(result.error)) throw result.error;
|
|
138
|
+
return result;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* `execFileSync` with a deadline.
|
|
143
|
+
*
|
|
144
|
+
* `execFileSync` already throws on a timeout, so this changes the FAILURE MODE
|
|
145
|
+
* rather than adding one: without a `timeout` there is no deadline to reach and
|
|
146
|
+
* the call blocks for as long as the child lives.
|
|
147
|
+
* @param {string} command The executable.
|
|
148
|
+
* @param {readonly string[]} args Arguments.
|
|
149
|
+
* @param {object} [options] `execFileSync` options; `timeout` overrides the default.
|
|
150
|
+
* @returns {string|Buffer} Whatever the child wrote to stdout.
|
|
151
|
+
* @throws {Error} When the child was killed, or exited non-zero.
|
|
152
|
+
*/
|
|
153
|
+
export function boundedChildOutput(command, args = [], options = {}) {
|
|
154
|
+
return execFileSync(command, [...args], {
|
|
155
|
+
...options,
|
|
156
|
+
killSignal: "SIGKILL",
|
|
157
|
+
timeout: options.timeout ?? CHILD_BUDGET_MS,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
@@ -33,10 +33,11 @@
|
|
|
33
33
|
* @module workstation
|
|
34
34
|
*/
|
|
35
35
|
|
|
36
|
-
import { execFileSync } from "node:child_process";
|
|
37
36
|
import { existsSync, lstatSync, mkdirSync, rmSync, symlinkSync } from "node:fs";
|
|
38
37
|
import { join } from "node:path";
|
|
39
38
|
|
|
39
|
+
import { boundedChildOutput } from "./bounded-child.mjs";
|
|
40
|
+
|
|
40
41
|
import {
|
|
41
42
|
AGENTS,
|
|
42
43
|
CHECKSUMMED,
|
|
@@ -56,7 +57,7 @@ import {
|
|
|
56
57
|
* @param {Function} [exec] Runner, injectable for tests.
|
|
57
58
|
* @returns {string|null} Absolute path, or null when absent.
|
|
58
59
|
*/
|
|
59
|
-
export function locate(name, exec =
|
|
60
|
+
export function locate(name, exec = boundedChildOutput) {
|
|
60
61
|
try {
|
|
61
62
|
return String(
|
|
62
63
|
exec("command", ["-v", name], { encoding: "utf8", shell: true })
|
|
@@ -76,7 +77,7 @@ export function locate(name, exec = execFileSync) {
|
|
|
76
77
|
* @param {Function} [exec] Runner, injectable for tests.
|
|
77
78
|
* @returns {string|null} A dotted version, or null.
|
|
78
79
|
*/
|
|
79
|
-
export function version(name, exec =
|
|
80
|
+
export function version(name, exec = boundedChildOutput) {
|
|
80
81
|
const read = out => {
|
|
81
82
|
const match = /(\d+(?:\.\d+)+)/.exec(String(out ?? ""));
|
|
82
83
|
return match ? match[1] : null;
|
|
@@ -381,7 +382,7 @@ export function installEntry(entry, row, deps = {}) {
|
|
|
381
382
|
const run =
|
|
382
383
|
deps.run ??
|
|
383
384
|
((cmd, args, opts) =>
|
|
384
|
-
|
|
385
|
+
boundedChildOutput(cmd, args, { stdio: "inherit", ...opts }));
|
|
385
386
|
const find = deps.locate ?? locate;
|
|
386
387
|
|
|
387
388
|
try {
|
|
@@ -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);
|
|
@@ -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);
|
|
@@ -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-openclaw",
|
|
3
|
-
"version": "4.4.
|
|
3
|
+
"version": "4.4.8",
|
|
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.
|
|
3
|
+
"version": "4.4.8",
|
|
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.
|
|
3
|
+
"version": "4.4.8",
|
|
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.
|
|
3
|
+
"version": "4.4.8",
|
|
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.
|
|
3
|
+
"version": "4.4.8",
|
|
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"
|