cool-workflow 0.1.96 → 0.1.98
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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/apps/architecture-review/app.json +1 -1
- package/apps/architecture-review-fast/app.json +1 -1
- package/apps/end-to-end-golden-path/app.json +1 -1
- package/apps/pr-review-fix-ci/app.json +1 -1
- package/apps/release-cut/app.json +1 -1
- package/apps/research-synthesis/app.json +1 -1
- package/dist/candidate-scoring.js +3 -3
- package/dist/capability-core.js +9 -1
- package/dist/capability-registry.js +7 -1
- package/dist/cli/command-surface.js +4 -0
- package/dist/cli/handlers/ledger.js +169 -0
- package/dist/cli/handlers/scheduling.js +7 -1
- package/dist/drive.js +108 -61
- package/dist/execution-backend/agent.js +84 -24
- package/dist/execution-backend.js +25 -5
- package/dist/ledger.js +313 -0
- package/dist/mcp/tool-call.js +36 -0
- package/dist/mcp/tool-definitions.js +26 -0
- package/dist/mcp-server.js +4 -0
- package/dist/onramp.js +2 -0
- package/dist/orchestrator/app-operations.js +6 -0
- package/dist/orchestrator/cli-options.js +8 -2
- package/dist/orchestrator/lifecycle-operations.js +40 -13
- package/dist/orchestrator/migration-operations.js +1 -1
- package/dist/orchestrator.js +11 -3
- package/dist/remote-source.js +10 -3
- package/dist/run-export.js +45 -5
- package/dist/sandbox-profile.js +6 -1
- package/dist/triggers.js +7 -1
- package/dist/version.js +1 -1
- package/dist/workbench-host.js +18 -2
- package/docs/agent-delegation-drive.7.md +4 -0
- package/docs/cli-mcp-parity.7.md +16 -2
- package/docs/contract-migration-tooling.7.md +4 -0
- package/docs/control-plane-scheduling.7.md +4 -0
- package/docs/cross-agent-ledger.7.md +217 -0
- package/docs/demo.7.md +80 -0
- package/docs/designs/handoff-ledger.md +145 -0
- package/docs/doctor.7.md +97 -0
- package/docs/durable-state-and-locking.7.md +4 -0
- package/docs/evidence-adoption-reasoning-chain.7.md +4 -0
- package/docs/execution-backends.7.md +4 -0
- package/docs/fix.7.md +44 -0
- package/docs/handoff-setup.md +120 -0
- package/docs/init.7.md +62 -0
- package/docs/multi-agent-cli-mcp-surface.7.md +4 -0
- package/docs/multi-agent-eval-replay-harness.7.md +4 -0
- package/docs/multi-agent-operator-ux.7.md +4 -0
- package/docs/node-snapshot-diff-replay.7.md +4 -0
- package/docs/observability-cost-accounting.7.md +4 -0
- package/docs/pipeline-verbs.7.md +93 -0
- package/docs/project-index.md +28 -5
- package/docs/real-execution-backends.7.md +4 -0
- package/docs/release-and-migration.7.md +4 -0
- package/docs/release-tooling.7.md +4 -0
- package/docs/routine.7.md +73 -0
- package/docs/run-registry-control-plane.7.md +4 -0
- package/docs/run-retention-reclamation.7.md +4 -0
- package/docs/state-explosion-management.7.md +4 -0
- package/docs/team-collaboration.7.md +4 -0
- package/docs/web-desktop-workbench.7.md +4 -0
- package/manifest/README.md +16 -10
- package/manifest/plugin.manifest.json +1 -1
- package/package.json +1 -1
- package/scripts/agents/agent-adapter-core.js +4 -1
- package/scripts/agents/codex-agent.js +34 -4
- package/scripts/canonical-apps.js +4 -4
- package/scripts/children/batch-delegate-child.js +40 -13
- package/scripts/children/http-delegate-child.js +2 -1
- package/scripts/dogfood-release.js +1 -1
- package/scripts/golden-path.js +4 -4
- package/scripts/release-flow.js +31 -17
package/dist/ledger.js
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Cross-agent handoff ledger — the core mechanism for two agents scoped to two
|
|
3
|
+
// separate repos to hand each other a CHANGE PROPOSAL or a REVIEW VERDICT as
|
|
4
|
+
// verifiable data, not chat. Design: docs/designs/handoff-ledger.md.
|
|
5
|
+
//
|
|
6
|
+
// Stage 1 (human-relay transport): a ledger entry is a self-contained JSON
|
|
7
|
+
// object carrying its own sha256 content digest. The producing side prints one;
|
|
8
|
+
// the operator carries it to the other session; the consuming side VERIFIES it
|
|
9
|
+
// fail-closed (a tampered or malformed entry is refused, never acted on) before
|
|
10
|
+
// turning a proposal into a real PR or recording a verdict.
|
|
11
|
+
//
|
|
12
|
+
// Zero-dependency (only node stdlib). `build*`/`verify*` are pure; the stage-2
|
|
13
|
+
// git transport adds `listLedgerEntries`, a READ-ONLY scan of a shared ledger
|
|
14
|
+
// directory (the working tree of a handoff repo) that verifies every entry
|
|
15
|
+
// fail-closed. No run state, no writes, no network.
|
|
16
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
17
|
+
if (k2 === undefined) k2 = k;
|
|
18
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
19
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
20
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
21
|
+
}
|
|
22
|
+
Object.defineProperty(o, k2, desc);
|
|
23
|
+
}) : (function(o, m, k, k2) {
|
|
24
|
+
if (k2 === undefined) k2 = k;
|
|
25
|
+
o[k2] = m[k];
|
|
26
|
+
}));
|
|
27
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
28
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
29
|
+
}) : function(o, v) {
|
|
30
|
+
o["default"] = v;
|
|
31
|
+
});
|
|
32
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
33
|
+
var ownKeys = function(o) {
|
|
34
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
35
|
+
var ar = [];
|
|
36
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
37
|
+
return ar;
|
|
38
|
+
};
|
|
39
|
+
return ownKeys(o);
|
|
40
|
+
};
|
|
41
|
+
return function (mod) {
|
|
42
|
+
if (mod && mod.__esModule) return mod;
|
|
43
|
+
var result = {};
|
|
44
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
45
|
+
__setModuleDefault(result, mod);
|
|
46
|
+
return result;
|
|
47
|
+
};
|
|
48
|
+
})();
|
|
49
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
50
|
+
exports.computeLedgerDigest = computeLedgerDigest;
|
|
51
|
+
exports.buildLedgerProposal = buildLedgerProposal;
|
|
52
|
+
exports.buildLedgerReview = buildLedgerReview;
|
|
53
|
+
exports.verifyLedgerEntry = verifyLedgerEntry;
|
|
54
|
+
exports.applyLedgerProposal = applyLedgerProposal;
|
|
55
|
+
exports.listLedgerEntries = listLedgerEntries;
|
|
56
|
+
exports.unionLedgerEntries = unionLedgerEntries;
|
|
57
|
+
exports.resolveLedgerInbox = resolveLedgerInbox;
|
|
58
|
+
const crypto = __importStar(require("crypto"));
|
|
59
|
+
const fs = __importStar(require("fs"));
|
|
60
|
+
const path = __importStar(require("path"));
|
|
61
|
+
/** Deterministic JSON with recursively sorted object keys, so the digest is a
|
|
62
|
+
* function of content only — never key insertion order. */
|
|
63
|
+
function stableStringify(value) {
|
|
64
|
+
if (value === null || typeof value !== "object")
|
|
65
|
+
return JSON.stringify(value);
|
|
66
|
+
if (Array.isArray(value))
|
|
67
|
+
return `[${value.map(stableStringify).join(",")}]`;
|
|
68
|
+
const keys = Object.keys(value).sort();
|
|
69
|
+
const body = keys
|
|
70
|
+
.map((k) => `${JSON.stringify(k)}:${stableStringify(value[k])}`)
|
|
71
|
+
.join(",");
|
|
72
|
+
return `{${body}}`;
|
|
73
|
+
}
|
|
74
|
+
/** sha256 over the canonical content (every field except `id` and `digest`,
|
|
75
|
+
* which are derived FROM it). Returns the full `sha256:<hex>` form. */
|
|
76
|
+
function computeLedgerDigest(entry) {
|
|
77
|
+
const hash = crypto.createHash("sha256");
|
|
78
|
+
hash.update(stableStringify(entry));
|
|
79
|
+
return `sha256:${hash.digest("hex")}`;
|
|
80
|
+
}
|
|
81
|
+
/** Content-addressed id: `ldg-` + the first 16 hex chars of the digest. Two
|
|
82
|
+
* entries with the same content (and createdAt) get the same id. */
|
|
83
|
+
function deriveId(digest) {
|
|
84
|
+
return `ldg-${digest.replace(/^sha256:/, "").slice(0, 16)}`;
|
|
85
|
+
}
|
|
86
|
+
function seal(content) {
|
|
87
|
+
const digest = computeLedgerDigest(content);
|
|
88
|
+
return { ...content, id: deriveId(digest), digest };
|
|
89
|
+
}
|
|
90
|
+
function buildLedgerProposal(input) {
|
|
91
|
+
const content = {
|
|
92
|
+
kind: "proposal",
|
|
93
|
+
schemaVersion: 1,
|
|
94
|
+
from: input.from,
|
|
95
|
+
to: input.to,
|
|
96
|
+
title: input.title,
|
|
97
|
+
rationale: input.rationale,
|
|
98
|
+
targetFiles: [...input.targetFiles],
|
|
99
|
+
suggestedDiff: input.suggestedDiff || "",
|
|
100
|
+
createdAt: input.createdAt
|
|
101
|
+
};
|
|
102
|
+
return seal(content);
|
|
103
|
+
}
|
|
104
|
+
function buildLedgerReview(input) {
|
|
105
|
+
const content = {
|
|
106
|
+
kind: "review",
|
|
107
|
+
schemaVersion: 1,
|
|
108
|
+
from: input.from,
|
|
109
|
+
to: input.to,
|
|
110
|
+
target: input.target,
|
|
111
|
+
verdict: input.verdict,
|
|
112
|
+
findings: [...input.findings],
|
|
113
|
+
createdAt: input.createdAt
|
|
114
|
+
};
|
|
115
|
+
return seal(content);
|
|
116
|
+
}
|
|
117
|
+
function isRecord(value) {
|
|
118
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
119
|
+
}
|
|
120
|
+
const PROPOSAL_FIELDS = ["from", "to", "title", "rationale", "targetFiles", "suggestedDiff", "createdAt"];
|
|
121
|
+
const REVIEW_FIELDS = ["from", "to", "target", "verdict", "findings", "createdAt"];
|
|
122
|
+
/** Fail-closed verification. Any structural defect, unknown kind, or digest
|
|
123
|
+
* mismatch yields `ok:false` — the caller refuses to act on it. */
|
|
124
|
+
function verifyLedgerEntry(raw) {
|
|
125
|
+
const checks = [];
|
|
126
|
+
const fail = (name, code, detail) => {
|
|
127
|
+
checks.push({ name, pass: false, code, detail });
|
|
128
|
+
return {
|
|
129
|
+
ok: false,
|
|
130
|
+
id: isRecord(raw) && typeof raw.id === "string" ? raw.id : null,
|
|
131
|
+
kind: isRecord(raw) && typeof raw.kind === "string" ? raw.kind : null,
|
|
132
|
+
checks,
|
|
133
|
+
failedChecks: checks.filter((c) => !c.pass).map((c) => ({ name: c.name, code: c.code, detail: c.detail }))
|
|
134
|
+
};
|
|
135
|
+
};
|
|
136
|
+
if (!isRecord(raw))
|
|
137
|
+
return fail("structure", "ledger-not-object", "entry is not a JSON object");
|
|
138
|
+
checks.push({ name: "structure", pass: true });
|
|
139
|
+
const kind = raw.kind;
|
|
140
|
+
if (kind !== "proposal" && kind !== "review")
|
|
141
|
+
return fail("kind", "ledger-unknown-kind", `kind must be proposal|review, got ${JSON.stringify(kind)}`);
|
|
142
|
+
checks.push({ name: "kind", pass: true });
|
|
143
|
+
if (raw.schemaVersion !== 1)
|
|
144
|
+
return fail("schema", "ledger-bad-schema", `schemaVersion must be 1, got ${JSON.stringify(raw.schemaVersion)}`);
|
|
145
|
+
checks.push({ name: "schema", pass: true });
|
|
146
|
+
if (typeof raw.digest !== "string" || !raw.digest)
|
|
147
|
+
return fail("digest-present", "ledger-missing-digest", "digest is absent or not a string");
|
|
148
|
+
checks.push({ name: "digest-present", pass: true });
|
|
149
|
+
const fields = kind === "proposal" ? PROPOSAL_FIELDS : REVIEW_FIELDS;
|
|
150
|
+
const content = { kind, schemaVersion: 1 };
|
|
151
|
+
for (const field of fields) {
|
|
152
|
+
if (!(field in raw))
|
|
153
|
+
return fail("fields", "ledger-missing-field", `required field ${field} is absent`);
|
|
154
|
+
content[field] = raw[field];
|
|
155
|
+
}
|
|
156
|
+
if (kind === "review" && raw.verdict !== "APPROVED" && raw.verdict !== "REJECTED") {
|
|
157
|
+
return fail("verdict", "ledger-bad-verdict", `verdict must be APPROVED|REJECTED, got ${JSON.stringify(raw.verdict)}`);
|
|
158
|
+
}
|
|
159
|
+
checks.push({ name: "fields", pass: true });
|
|
160
|
+
const recomputed = computeLedgerDigest(content);
|
|
161
|
+
if (recomputed !== raw.digest) {
|
|
162
|
+
return fail("digest", "ledger-digest-mismatch", `stored digest does not match content (recomputed ${recomputed})`);
|
|
163
|
+
}
|
|
164
|
+
checks.push({ name: "digest", pass: true });
|
|
165
|
+
// Bind the id to the content: it MUST be the content-addressed id derived from
|
|
166
|
+
// the digest. Without this, `id` is a free, unverified field (it is excluded
|
|
167
|
+
// from the digest) — a forged entry could set `id` to collide with a legit
|
|
168
|
+
// one, and any id-keyed de-duplication (`cw ledger list` union) would silently
|
|
169
|
+
// drop one of them. Fail closed so a spoofed or absent id is refused, not
|
|
170
|
+
// trusted.
|
|
171
|
+
const expectedId = deriveId(raw.digest);
|
|
172
|
+
if (raw.id !== expectedId) {
|
|
173
|
+
return fail("id", "ledger-id-mismatch", `id ${JSON.stringify(raw.id)} is not the content-addressed id for this digest (expected ${expectedId})`);
|
|
174
|
+
}
|
|
175
|
+
checks.push({ name: "id", pass: true });
|
|
176
|
+
return { ok: true, id: expectedId, kind, checks, failedChecks: [] };
|
|
177
|
+
}
|
|
178
|
+
/** Fail-closed extraction of a proposal's `suggestedDiff` for `git apply`. The
|
|
179
|
+
* diff can ONLY escape after the entry verifies: a tampered entry, a review
|
|
180
|
+
* (not a proposal), or a proposal with no diff all yield `ok:false` and
|
|
181
|
+
* `diff:null`, so `cw ledger apply <file> | git apply` can never feed an
|
|
182
|
+
* unverified patch to git. The kernel never shells out to git — turning the
|
|
183
|
+
* diff into a patch stays the operator's step (mechanism, not policy). */
|
|
184
|
+
function applyLedgerProposal(raw) {
|
|
185
|
+
const verified = verifyLedgerEntry(raw);
|
|
186
|
+
if (!verified.ok) {
|
|
187
|
+
return { ok: false, id: verified.id, kind: verified.kind, diff: null, failedChecks: verified.failedChecks };
|
|
188
|
+
}
|
|
189
|
+
if (verified.kind !== "proposal") {
|
|
190
|
+
return { ok: false, id: verified.id, kind: verified.kind, diff: null, failedChecks: [{ name: "kind", code: "ledger-not-a-proposal", detail: "apply expects a proposal entry, not a review" }] };
|
|
191
|
+
}
|
|
192
|
+
const rec = isRecord(raw) ? raw : {};
|
|
193
|
+
const diff = typeof rec.suggestedDiff === "string" ? rec.suggestedDiff : "";
|
|
194
|
+
if (!diff) {
|
|
195
|
+
return { ok: false, id: verified.id, kind: verified.kind, diff: null, failedChecks: [{ name: "diff", code: "ledger-empty-diff", detail: "proposal carries no suggestedDiff to apply" }] };
|
|
196
|
+
}
|
|
197
|
+
return { ok: true, id: verified.id, kind: verified.kind, diff, failedChecks: [] };
|
|
198
|
+
}
|
|
199
|
+
/** Read every `*.json` in `dir`, verify each entry fail-closed, and report.
|
|
200
|
+
* `allOk` is false if any entry is tampered, malformed, or unreadable — so the
|
|
201
|
+
* receiving side refuses the whole inbox rather than acting on a mixed batch. */
|
|
202
|
+
function listLedgerEntries(dir) {
|
|
203
|
+
let names;
|
|
204
|
+
try {
|
|
205
|
+
names = fs.readdirSync(dir).filter((n) => n.endsWith(".json")).sort();
|
|
206
|
+
}
|
|
207
|
+
catch (error) {
|
|
208
|
+
const entry = { file: dir, id: null, kind: null, from: null, to: null, title: null, target: null, verdict: null, ok: false, failedChecks: [{ name: "dir", code: "ledger-dir-unreadable", detail: error.message }] };
|
|
209
|
+
return { dir, count: 0, allOk: false, entries: [entry], resolution: resolveLedgerInbox([entry]) };
|
|
210
|
+
}
|
|
211
|
+
const entries = names.map((name) => {
|
|
212
|
+
const file = path.join(dir, name);
|
|
213
|
+
let raw;
|
|
214
|
+
try {
|
|
215
|
+
const stat = fs.lstatSync(file);
|
|
216
|
+
if (!stat.isFile()) {
|
|
217
|
+
return { file: name, id: null, kind: null, from: null, to: null, title: null, target: null, verdict: null, ok: false, failedChecks: [{ name: "file", code: "ledger-entry-not-regular" }] };
|
|
218
|
+
}
|
|
219
|
+
raw = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
return { file: name, id: null, kind: null, from: null, to: null, title: null, target: null, verdict: null, ok: false, failedChecks: [{ name: "parse", code: "ledger-bad-json" }] };
|
|
223
|
+
}
|
|
224
|
+
const result = verifyLedgerEntry(raw);
|
|
225
|
+
const rec = isRecord(raw) ? raw : {};
|
|
226
|
+
return {
|
|
227
|
+
file: name,
|
|
228
|
+
id: result.id,
|
|
229
|
+
kind: result.kind,
|
|
230
|
+
from: typeof rec.from === "string" ? rec.from : null,
|
|
231
|
+
to: typeof rec.to === "string" ? rec.to : null,
|
|
232
|
+
title: typeof rec.title === "string" ? rec.title : null,
|
|
233
|
+
target: typeof rec.target === "string" ? rec.target : null,
|
|
234
|
+
verdict: typeof rec.verdict === "string" ? rec.verdict : null,
|
|
235
|
+
ok: result.ok,
|
|
236
|
+
failedChecks: result.failedChecks
|
|
237
|
+
};
|
|
238
|
+
});
|
|
239
|
+
return { dir, count: entries.length, allOk: entries.every((e) => e.ok), entries, resolution: resolveLedgerInbox(entries) };
|
|
240
|
+
}
|
|
241
|
+
/** Union-verify several mirror directories into ONE fail-closed inbox. Verified
|
|
242
|
+
* entries are de-duplicated by their content-addressed id (the same entry
|
|
243
|
+
* mirrored to N hosts collapses to one, recording every mirror it came from);
|
|
244
|
+
* failing entries are kept per-occurrence so every problem in every mirror is
|
|
245
|
+
* visible. `allOk` is false if ANY entry in ANY mirror does not verify — a
|
|
246
|
+
* tampered mirror fails the whole batch. Safe because entries are immutable and
|
|
247
|
+
* content-addressed, so a union is a conflict-free set-union, not a merge. */
|
|
248
|
+
function unionLedgerEntries(dirs) {
|
|
249
|
+
const byId = new Map();
|
|
250
|
+
const failures = [];
|
|
251
|
+
let allOk = true;
|
|
252
|
+
for (const dir of dirs) {
|
|
253
|
+
const listed = listLedgerEntries(dir);
|
|
254
|
+
if (!listed.allOk)
|
|
255
|
+
allOk = false;
|
|
256
|
+
for (const entry of listed.entries) {
|
|
257
|
+
if (entry.ok && entry.id) {
|
|
258
|
+
const existing = byId.get(entry.id);
|
|
259
|
+
if (existing) {
|
|
260
|
+
if (!existing.dirs.includes(dir))
|
|
261
|
+
existing.dirs.push(dir);
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
byId.set(entry.id, { ...entry, dirs: [dir] });
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
failures.push({ ...entry, dirs: [dir] });
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
const entries = [...byId.values(), ...failures];
|
|
273
|
+
return { dirs, count: entries.length, allOk, entries, resolution: resolveLedgerInbox(entries) };
|
|
274
|
+
}
|
|
275
|
+
/** Derive a machine-actionable inbox summary: pair each proposal with the
|
|
276
|
+
* review(s) that target it and report whether it is pending, approved,
|
|
277
|
+
* rejected, or contested. Only VERIFIED entries take part — a tampered review
|
|
278
|
+
* must never resolve a proposal, so a proposal with only a failing review
|
|
279
|
+
* stays `pending` (fail-closed). Pure derivation over content-addressed
|
|
280
|
+
* entries: no git, no network, no policy (it reports the decision, it does not
|
|
281
|
+
* enforce one). */
|
|
282
|
+
function resolveLedgerInbox(entries) {
|
|
283
|
+
const verified = entries.filter((e) => e.ok);
|
|
284
|
+
const reviews = verified.filter((e) => e.kind === "review" && e.target);
|
|
285
|
+
const proposals = verified
|
|
286
|
+
.filter((e) => e.kind === "proposal" && e.id)
|
|
287
|
+
.map((p) => {
|
|
288
|
+
const answering = reviews.filter((r) => r.target === p.id);
|
|
289
|
+
const verdicts = new Set(answering.map((r) => r.verdict));
|
|
290
|
+
let resolution;
|
|
291
|
+
if (answering.length === 0)
|
|
292
|
+
resolution = "pending";
|
|
293
|
+
else if (verdicts.size > 1)
|
|
294
|
+
resolution = "contested";
|
|
295
|
+
else
|
|
296
|
+
resolution = verdicts.has("APPROVED") ? "approved" : "rejected";
|
|
297
|
+
return {
|
|
298
|
+
id: p.id,
|
|
299
|
+
title: p.title,
|
|
300
|
+
resolution,
|
|
301
|
+
reviews: answering.map((r) => r.id).sort()
|
|
302
|
+
};
|
|
303
|
+
})
|
|
304
|
+
.sort((a, b) => a.id.localeCompare(b.id));
|
|
305
|
+
const tally = (s) => proposals.filter((p) => p.resolution === s).length;
|
|
306
|
+
return {
|
|
307
|
+
proposals,
|
|
308
|
+
pending: tally("pending"),
|
|
309
|
+
approved: tally("approved"),
|
|
310
|
+
rejected: tally("rejected"),
|
|
311
|
+
contested: tally("contested")
|
|
312
|
+
};
|
|
313
|
+
}
|
package/dist/mcp/tool-call.js
CHANGED
|
@@ -7,6 +7,7 @@ exports.callTool = callTool;
|
|
|
7
7
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
8
|
const node_path_1 = __importDefault(require("node:path"));
|
|
9
9
|
const orchestrator_1 = require("../orchestrator");
|
|
10
|
+
const ledger_1 = require("../ledger");
|
|
10
11
|
const scheduler_1 = require("../scheduler");
|
|
11
12
|
const triggers_1 = require("../triggers");
|
|
12
13
|
const workbench_1 = require("../workbench");
|
|
@@ -294,6 +295,41 @@ function callTool(name, args) {
|
|
|
294
295
|
return runner.collaborationCommentList(String(args.runId || ""), args);
|
|
295
296
|
case "cw_handoff":
|
|
296
297
|
return runner.collaborationHandoff(String(args.runId || ""), String(args.targetKind || args.kind || ""), String(args.targetId || args.target || ""), args);
|
|
298
|
+
// ---- Cross-agent handoff ledger (stage 2 MCP surface) ----
|
|
299
|
+
case "cw_ledger_propose":
|
|
300
|
+
return (0, ledger_1.buildLedgerProposal)({
|
|
301
|
+
from: String(args.from || ""),
|
|
302
|
+
to: String(args.to || ""),
|
|
303
|
+
title: String(args.title || ""),
|
|
304
|
+
rationale: String(args.rationale || ""),
|
|
305
|
+
targetFiles: String(args.files || "").split(",").map((f) => f.trim()).filter(Boolean),
|
|
306
|
+
suggestedDiff: args.diff === undefined ? undefined : String(args.diff),
|
|
307
|
+
createdAt: new Date().toISOString()
|
|
308
|
+
});
|
|
309
|
+
case "cw_ledger_review": {
|
|
310
|
+
const verdict = String(args.verdict || "").toUpperCase();
|
|
311
|
+
if (verdict !== "APPROVED" && verdict !== "REJECTED")
|
|
312
|
+
throw new Error('verdict must be "approved" or "rejected".');
|
|
313
|
+
return (0, ledger_1.buildLedgerReview)({
|
|
314
|
+
from: String(args.from || ""),
|
|
315
|
+
to: String(args.to || ""),
|
|
316
|
+
target: String(args.target || ""),
|
|
317
|
+
verdict,
|
|
318
|
+
findings: String(args.findings || "").split(",").map((f) => f.trim()).filter(Boolean),
|
|
319
|
+
createdAt: new Date().toISOString()
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
case "cw_ledger_verify":
|
|
323
|
+
return (0, ledger_1.verifyLedgerEntry)(args.entry);
|
|
324
|
+
case "cw_ledger_apply":
|
|
325
|
+
return (0, ledger_1.applyLedgerProposal)(args.entry);
|
|
326
|
+
case "cw_ledger_list": {
|
|
327
|
+
// `dirs` (2+) union-verifies mirrors; a single `dir` keeps the original shape.
|
|
328
|
+
const dirs = Array.isArray(args.dirs) ? args.dirs.map(String).filter(Boolean) : [];
|
|
329
|
+
if (dirs.length > 1)
|
|
330
|
+
return (0, ledger_1.unionLedgerEntries)(dirs);
|
|
331
|
+
return (0, ledger_1.listLedgerEntries)(dirs[0] || String(args.dir || ""));
|
|
332
|
+
}
|
|
297
333
|
case "cw_review_status":
|
|
298
334
|
return runner.reviewStatus(String(args.runId || ""), args);
|
|
299
335
|
case "cw_review_policy":
|
|
@@ -641,6 +641,31 @@ function toolDefinitions() {
|
|
|
641
641
|
from: stringSchema("From-actor id (defaults to recorder)"),
|
|
642
642
|
reason: stringSchema("Handoff reason")
|
|
643
643
|
}),
|
|
644
|
+
capabilityTool("ledger.propose", "Build a verifiable cross-agent change proposal entry (digest-sealed JSON).", {
|
|
645
|
+
from: stringSchema("Proposing agent/repo"),
|
|
646
|
+
to: stringSchema("Target agent/repo"),
|
|
647
|
+
title: stringSchema("Short proposal title"),
|
|
648
|
+
rationale: stringSchema("Why this change"),
|
|
649
|
+
files: stringSchema("Comma-separated target files"),
|
|
650
|
+
diff: stringSchema("Suggested unified diff")
|
|
651
|
+
}),
|
|
652
|
+
capabilityTool("ledger.review", "Build a verifiable cross-agent review verdict entry (digest-sealed JSON).", {
|
|
653
|
+
from: stringSchema("Reviewing agent/repo"),
|
|
654
|
+
to: stringSchema("Target agent/repo"),
|
|
655
|
+
target: stringSchema("Proposal id or PR ref being judged"),
|
|
656
|
+
verdict: stringSchema("approved | rejected"),
|
|
657
|
+
findings: stringSchema("Comma-separated findings")
|
|
658
|
+
}),
|
|
659
|
+
capabilityTool("ledger.verify", "Verify a ledger entry against its content digest (fail-closed on tampering).", {
|
|
660
|
+
entry: objectSchema("The ledger entry object to verify")
|
|
661
|
+
}),
|
|
662
|
+
capabilityTool("ledger.apply", "Verify a proposal entry and return its suggestedDiff for `git apply` (fail-closed: no diff unless the entry verifies).", {
|
|
663
|
+
entry: objectSchema("The proposal entry object to verify and extract the diff from")
|
|
664
|
+
}),
|
|
665
|
+
capabilityTool("ledger.list", "Read + verify every entry in a shared ledger directory (fail-closed inbox).", {
|
|
666
|
+
dir: stringSchema("Path to the ledger directory (a handoff repo working tree)"),
|
|
667
|
+
dirs: arraySchema("Multiple ledger directories to union-verify as mirrors (2+)")
|
|
668
|
+
}),
|
|
644
669
|
capabilityTool("review.status", "Read the derived per-target review state + collaboration timeline for a run.", {
|
|
645
670
|
...runIdSchema(),
|
|
646
671
|
targetKind: stringSchema("Optional target kind filter"),
|
|
@@ -876,6 +901,7 @@ function toolDefinitions() {
|
|
|
876
901
|
question: stringSchema("The question the audited report answers"),
|
|
877
902
|
once: booleanSchema("Advance exactly one step then stop"),
|
|
878
903
|
now: stringSchema("Injected ISO timestamp for deterministic scheduling"),
|
|
904
|
+
concurrency: numberSchema("Override the round width for parallel() phases (default: the workflow's own limits.maxConcurrentAgents)"),
|
|
879
905
|
cwd: stringSchema("Run workspace")
|
|
880
906
|
}),
|
|
881
907
|
capabilityTool("queue.add", "Enqueue a pending/planned run with explicit ordering policy (lower priority drains first). Plain files; the host still executes workers.", {
|
package/dist/mcp-server.js
CHANGED
|
@@ -35,6 +35,10 @@ function handleLine(line) {
|
|
|
35
35
|
sendError(null, -32700, `Parse error: ${messageOf(error)}`);
|
|
36
36
|
return;
|
|
37
37
|
}
|
|
38
|
+
if (message === null || typeof message !== "object" || Array.isArray(message)) {
|
|
39
|
+
sendError(null, -32600, "Invalid Request: not a JSON-RPC object");
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
38
42
|
try {
|
|
39
43
|
if (message.method === "initialize") {
|
|
40
44
|
sendResult(message.id, {
|
package/dist/onramp.js
CHANGED
|
@@ -386,6 +386,8 @@ function resolveBaseRef(root, changedFrom, env) {
|
|
|
386
386
|
return verifyRef(root, "HEAD");
|
|
387
387
|
}
|
|
388
388
|
function verifyRef(root, ref) {
|
|
389
|
+
if (ref.startsWith("-"))
|
|
390
|
+
throw new Error(`Invalid onramp base ref (must not start with '-'): ${ref}`);
|
|
389
391
|
const resolved = gitOne(root, ["rev-parse", "--verify", `${ref}^{commit}`]);
|
|
390
392
|
if (!resolved)
|
|
391
393
|
throw new Error(`Unknown onramp base ref: ${ref}`);
|
|
@@ -169,6 +169,12 @@ function initApp(appsDir, appId, options, resolveFromBase, validateApp) {
|
|
|
169
169
|
throw new Error("App id must include at least one letter or digit");
|
|
170
170
|
const title = String(options.title || titleize(id));
|
|
171
171
|
const destinationDir = resolveFromBase(String(options.directory || options.output || node_path_1.default.join(appsDir, id)));
|
|
172
|
+
// Reject writes to system-owned directories. The operator may provide any
|
|
173
|
+
// output path, but writing to /etc, /bin, /usr etc. is never valid.
|
|
174
|
+
const sysDirs = /^\/(etc|bin|sbin|usr|Library|System|Applications|boot|dev|proc|sys|root|var\/log|var\/run)\//;
|
|
175
|
+
if (sysDirs.test(node_path_1.default.resolve(destinationDir))) {
|
|
176
|
+
throw new Error(`Refusing to create app in a system directory: ${destinationDir}`);
|
|
177
|
+
}
|
|
172
178
|
const manifestPath = node_path_1.default.join(destinationDir, "app.json");
|
|
173
179
|
const entrypointPath = node_path_1.default.join(destinationDir, "workflow.js");
|
|
174
180
|
if (!options.force && (node_fs_1.default.existsSync(manifestPath) || node_fs_1.default.existsSync(entrypointPath))) {
|
|
@@ -112,8 +112,14 @@ function metadataOption(options) {
|
|
|
112
112
|
const raw = options.metadata;
|
|
113
113
|
if (raw && typeof raw === "object" && !Array.isArray(raw))
|
|
114
114
|
return raw;
|
|
115
|
-
if (typeof raw === "string")
|
|
116
|
-
|
|
115
|
+
if (typeof raw === "string") {
|
|
116
|
+
try {
|
|
117
|
+
return JSON.parse(raw);
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
throw new Error(`Invalid JSON in --metadata: ${String(raw).slice(0, 80)}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
117
123
|
return undefined;
|
|
118
124
|
}
|
|
119
125
|
function withoutHostRunKeys(args) {
|
|
@@ -29,6 +29,7 @@ const workflow_api_1 = require("../workflow-api");
|
|
|
29
29
|
const observability_1 = require("../observability");
|
|
30
30
|
const compare_1 = require("../compare");
|
|
31
31
|
const loop_expansion_1 = require("../loop-expansion");
|
|
32
|
+
const evidence_grounding_1 = require("../evidence-grounding");
|
|
32
33
|
const dispatch_1 = require("../dispatch");
|
|
33
34
|
const verifier_1 = require("../verifier");
|
|
34
35
|
const trust_audit_1 = require("../trust-audit");
|
|
@@ -179,7 +180,14 @@ function plan(appRecord, options) {
|
|
|
179
180
|
(0, state_1.saveCheckpoint)(run);
|
|
180
181
|
return run;
|
|
181
182
|
}
|
|
183
|
+
/** `options.persistState === false` (concurrent-round callers ONLY — never from
|
|
184
|
+
* a CLI/MCP arg bag) skips commitState/saveCheckpoint/writeReport on every
|
|
185
|
+
* branch, success or error, so a caller driving many tasks through one
|
|
186
|
+
* in-memory `run` can defer the disk flush to a single call at round end
|
|
187
|
+
* instead of once per task. Default (absent) preserves today's exact
|
|
188
|
+
* per-call persistence. */
|
|
182
189
|
function dispatch(run, options) {
|
|
190
|
+
const persistState = options.persistState !== false;
|
|
183
191
|
try {
|
|
184
192
|
const manifest = (0, dispatch_1.createDispatchManifest)(run, (0, cli_options_1.numberOption)(options.limit), {
|
|
185
193
|
sandboxProfileId: (0, cli_options_1.stringOption)(options.sandbox) || (0, cli_options_1.stringOption)(options.sandboxProfile) || (0, cli_options_1.stringOption)(options.sandboxProfileId),
|
|
@@ -190,10 +198,12 @@ function dispatch(run, options) {
|
|
|
190
198
|
multiAgentFanoutId: (0, cli_options_1.stringOption)(options.multiAgentFanout || options.multiAgentFanoutId || options.fanout || options["multi-agent-fanout"])
|
|
191
199
|
});
|
|
192
200
|
run.loopStage = "act";
|
|
193
|
-
if (
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
201
|
+
if (persistState) {
|
|
202
|
+
if (manifest.dispatchId)
|
|
203
|
+
(0, commit_1.commitState)(run, `dispatch:${manifest.dispatchId}`);
|
|
204
|
+
(0, state_1.saveCheckpoint)(run);
|
|
205
|
+
(0, report_1.writeReport)(run);
|
|
206
|
+
}
|
|
197
207
|
return manifest;
|
|
198
208
|
}
|
|
199
209
|
catch (error) {
|
|
@@ -212,8 +222,10 @@ function dispatch(run, options) {
|
|
|
212
222
|
retryable: false,
|
|
213
223
|
metadata: { sandboxProfileId: (0, cli_options_1.stringOption)(options.sandbox) || (0, cli_options_1.stringOption)(options.sandboxProfile) || (0, cli_options_1.stringOption)(options.sandboxProfileId) }
|
|
214
224
|
}, { persist: false });
|
|
215
|
-
|
|
216
|
-
|
|
225
|
+
if (persistState) {
|
|
226
|
+
(0, report_1.writeReport)(run);
|
|
227
|
+
(0, state_1.saveCheckpoint)(run);
|
|
228
|
+
}
|
|
217
229
|
}
|
|
218
230
|
throw error;
|
|
219
231
|
}
|
|
@@ -228,6 +240,9 @@ function recordResult(run, taskId, resultPath, options = {}) {
|
|
|
228
240
|
try {
|
|
229
241
|
(0, verifier_1.assertTaskCanComplete)(run, task);
|
|
230
242
|
const absoluteResultPath = node_path_1.default.resolve(resultPath);
|
|
243
|
+
if (/^\/(etc|bin|sbin|usr|Library|System|Applications|boot|dev|proc|sys|root|var\/log|var\/run)\//.test(absoluteResultPath)) {
|
|
244
|
+
throw new Error(`Result path must not be a system directory: ${resultPath}`);
|
|
245
|
+
}
|
|
231
246
|
if (!node_fs_1.default.existsSync(absoluteResultPath)) {
|
|
232
247
|
throw new Error(`Result file does not exist: ${absoluteResultPath}`);
|
|
233
248
|
}
|
|
@@ -236,6 +251,10 @@ function recordResult(run, taskId, resultPath, options = {}) {
|
|
|
236
251
|
const parsedResult = (0, verifier_1.parseResultEnvelope)(rawResult);
|
|
237
252
|
run.loopStage = "adjust";
|
|
238
253
|
(0, verifier_1.validateResultEnvelope)(task, parsedResult);
|
|
254
|
+
const unresolved = (0, evidence_grounding_1.unresolvedFileEvidence)(parsedResult.evidence, [run.cwd, process.cwd(), run.paths.runDir, node_path_1.default.dirname(absoluteResultPath)]);
|
|
255
|
+
if (unresolved.length) {
|
|
256
|
+
throw new Error(`Result cites file evidence that does not resolve on disk: ${unresolved.join(", ")}`);
|
|
257
|
+
}
|
|
239
258
|
const destination = node_path_1.default.join(run.paths.resultsDir, `${(0, state_1.safeFileName)(taskId)}.md`);
|
|
240
259
|
node_fs_1.default.copyFileSync(absoluteResultPath, destination);
|
|
241
260
|
task.status = "completed";
|
|
@@ -324,6 +343,7 @@ function recordWorkerOutput(run, workerId, resultPath, options = {}) {
|
|
|
324
343
|
// Track 1 fail-closed (opt-in): forward the policy so recordWorkerOutput can
|
|
325
344
|
// park a hop whose telemetry isn't attested. Default (absent) ⇒ flag-and-surface.
|
|
326
345
|
const requireAttestedTelemetry = options.requireAttestedTelemetry === true;
|
|
346
|
+
const persistState = options.persistState !== false;
|
|
327
347
|
try {
|
|
328
348
|
(0, worker_isolation_1.recordWorkerOutput)(run, workerId, resultPath, { persist: false, agentDelegation, requireAttestedTelemetry });
|
|
329
349
|
if (usage) {
|
|
@@ -338,20 +358,25 @@ function recordWorkerOutput(run, workerId, resultPath, options = {}) {
|
|
|
338
358
|
// and either append the next round or mark the loop done (no-op for non-loop runs).
|
|
339
359
|
maybeExpandLoop(run);
|
|
340
360
|
(0, verifier_1.validateRunGates)(run);
|
|
341
|
-
(
|
|
342
|
-
|
|
343
|
-
|
|
361
|
+
if (persistState) {
|
|
362
|
+
(0, commit_1.commitState)(run, `worker:${workerId}:result`);
|
|
363
|
+
(0, report_1.writeReport)(run);
|
|
364
|
+
(0, state_1.saveCheckpoint)(run);
|
|
365
|
+
}
|
|
344
366
|
return (0, report_1.summarizeRun)(run);
|
|
345
367
|
}
|
|
346
368
|
catch (error) {
|
|
347
369
|
run.loopStage = "adjust";
|
|
348
370
|
(0, dispatch_1.updatePhaseStatuses)(run);
|
|
349
|
-
|
|
350
|
-
|
|
371
|
+
if (persistState) {
|
|
372
|
+
(0, report_1.writeReport)(run);
|
|
373
|
+
(0, state_1.saveCheckpoint)(run);
|
|
374
|
+
}
|
|
351
375
|
throw error;
|
|
352
376
|
}
|
|
353
377
|
}
|
|
354
378
|
function recordWorkerFailure(run, workerId, message, options = {}) {
|
|
379
|
+
const persistState = options.persistState !== false;
|
|
355
380
|
const failure = (0, worker_isolation_1.recordWorkerFailure)(run, workerId, {
|
|
356
381
|
code: String(options.code || "worker-runtime-error"),
|
|
357
382
|
message,
|
|
@@ -361,8 +386,10 @@ function recordWorkerFailure(run, workerId, message, options = {}) {
|
|
|
361
386
|
}, { persist: false, retryCount: typeof options.retryCount === "number" ? Number(options.retryCount) : undefined });
|
|
362
387
|
run.loopStage = "adjust";
|
|
363
388
|
(0, dispatch_1.updatePhaseStatuses)(run);
|
|
364
|
-
|
|
365
|
-
|
|
389
|
+
if (persistState) {
|
|
390
|
+
(0, report_1.writeReport)(run);
|
|
391
|
+
(0, state_1.saveCheckpoint)(run);
|
|
392
|
+
}
|
|
366
393
|
return failure;
|
|
367
394
|
}
|
|
368
395
|
function checkState(runId, options = {}) {
|
|
@@ -40,5 +40,5 @@ function loadMigrationSnapshot(target, options) {
|
|
|
40
40
|
: node_path_1.default.join(process.cwd(), ".cw", "runs", target, "state.json");
|
|
41
41
|
if (!node_fs_1.default.existsSync(file))
|
|
42
42
|
throw new Error(`Migration target not found: ${target}`);
|
|
43
|
-
return { snapshot:
|
|
43
|
+
return { snapshot: (0, state_1.readJson)(file), contract, dir: node_path_1.default.dirname(file) };
|
|
44
44
|
}
|
package/dist/orchestrator.js
CHANGED
|
@@ -114,15 +114,23 @@ class CoolWorkflowRunner {
|
|
|
114
114
|
}
|
|
115
115
|
/** Run fn with a per-request loadRun cache. All loadRun calls inside fn share
|
|
116
116
|
* the same cached run state, collapsing 18 reads into 1. Returns fn's result
|
|
117
|
-
* and clears the cache afterwards (never leaks between requests).
|
|
117
|
+
* and clears the cache afterwards (never leaks between requests).
|
|
118
|
+
*
|
|
119
|
+
* Reentrant: a nested call (e.g. a concurrent drive round whose sub-workflow
|
|
120
|
+
* task recursively drives a child run on the SAME runner instance) saves and
|
|
121
|
+
* restores the OUTER scope's cache rather than clobbering it to undefined —
|
|
122
|
+
* otherwise the inner call's `finally` would wipe the outer round's still-in-
|
|
123
|
+
* flight (not yet persisted) mutations, and the outer scope's next loadRun
|
|
124
|
+
* would silently fall back to a stale disk read. */
|
|
118
125
|
loadWithCache(fn) {
|
|
126
|
+
const previousCache = this._requestCache;
|
|
119
127
|
this._requestCache = new Map();
|
|
120
128
|
(0, trust_audit_1.setAuditEventCache)(new Map());
|
|
121
129
|
try {
|
|
122
130
|
return fn(this);
|
|
123
131
|
}
|
|
124
132
|
finally {
|
|
125
|
-
this._requestCache =
|
|
133
|
+
this._requestCache = previousCache;
|
|
126
134
|
(0, trust_audit_1.clearAuditEventCache)();
|
|
127
135
|
}
|
|
128
136
|
}
|
|
@@ -818,7 +826,7 @@ function formatHelp() {
|
|
|
818
826
|
"sandbox backend contract node feedback worker audit candidate review loop schedule " +
|
|
819
827
|
"routine registry run queue clones history quickstart audit-run multi-agent topology summary " +
|
|
820
828
|
"blackboard coordinator metrics operator sched gc telemetry migration demo workbench " +
|
|
821
|
-
"approve reject comment handoff graph eval man version update fix").split(" ");
|
|
829
|
+
"approve reject comment handoff ledger graph eval man version update fix").split(" ");
|
|
822
830
|
// Wrap the command list into clean, indented, pipe-joined lines (<=76 cols) instead of
|
|
823
831
|
// one 400-char line that wraps raggedly and merges with the next shell prompt. Pipe-joined
|
|
824
832
|
// (no internal spaces) keeps it parseable by the CLI/MCP parity help-token check.
|
package/dist/remote-source.js
CHANGED
|
@@ -287,9 +287,13 @@ function fetchArchiveBytes(rawUrl, sanitizedUrl, dest, opts) {
|
|
|
287
287
|
"if(!r.ok){process.stderr.write('HTTP '+r.status+' '+r.statusText);process.exit(2);}",
|
|
288
288
|
"const len=Number(r.headers.get('content-length')||0);",
|
|
289
289
|
"if(cap&&len>cap){process.stderr.write('archive too large');process.exit(3);}",
|
|
290
|
-
"
|
|
291
|
-
"
|
|
292
|
-
"
|
|
290
|
+
"if(!r.body||!r.body.getReader){process.stderr.write('response body is not streamable');process.exit(4);}",
|
|
291
|
+
"const fd=fs.openSync(out,'w');let total=0;",
|
|
292
|
+
"try{const reader=r.body.getReader();for(;;){const x=await reader.read();if(x.done)break;",
|
|
293
|
+
"const chunk=Buffer.from(x.value);total+=chunk.length;",
|
|
294
|
+
"if(cap&&total>cap){try{await reader.cancel();}catch{};fs.closeSync(fd);fs.rmSync(out,{force:true});process.stderr.write('archive too large');process.exit(3);}",
|
|
295
|
+
"fs.writeSync(fd,chunk,0,chunk.length);}fs.closeSync(fd);return;}",
|
|
296
|
+
"catch(e){try{fs.closeSync(fd);}catch{};fs.rmSync(out,{force:true});throw e;}}",
|
|
293
297
|
"process.stderr.write('too many redirects');process.exit(6);",
|
|
294
298
|
"})().catch(e=>{process.stderr.write(String((e&&e.message)||e));process.exit(4);});"
|
|
295
299
|
].join("");
|
|
@@ -315,6 +319,9 @@ function listArchive(file, isZip) {
|
|
|
315
319
|
if (isZip && result.error?.code === "ENOENT") {
|
|
316
320
|
throw new Error("unzip is required to review a .zip link but was not found on PATH (use a .tar.gz or a git URL)");
|
|
317
321
|
}
|
|
322
|
+
if (!isZip && result.error?.code === "ENOENT") {
|
|
323
|
+
throw new Error("tar is required to review a .tar/.tgz link but was not found on PATH");
|
|
324
|
+
}
|
|
318
325
|
throw new Error(`could not read archive: ${String(result.stderr || "").trim() || `exit ${result.status}`}`);
|
|
319
326
|
}
|
|
320
327
|
return String(result.stdout || "").split(/\r?\n/).filter(Boolean);
|