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.
Files changed (74) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/apps/architecture-review/app.json +1 -1
  4. package/apps/architecture-review-fast/app.json +1 -1
  5. package/apps/end-to-end-golden-path/app.json +1 -1
  6. package/apps/pr-review-fix-ci/app.json +1 -1
  7. package/apps/release-cut/app.json +1 -1
  8. package/apps/research-synthesis/app.json +1 -1
  9. package/dist/candidate-scoring.js +3 -3
  10. package/dist/capability-core.js +9 -1
  11. package/dist/capability-registry.js +7 -1
  12. package/dist/cli/command-surface.js +4 -0
  13. package/dist/cli/handlers/ledger.js +169 -0
  14. package/dist/cli/handlers/scheduling.js +7 -1
  15. package/dist/drive.js +108 -61
  16. package/dist/execution-backend/agent.js +84 -24
  17. package/dist/execution-backend.js +25 -5
  18. package/dist/ledger.js +313 -0
  19. package/dist/mcp/tool-call.js +36 -0
  20. package/dist/mcp/tool-definitions.js +26 -0
  21. package/dist/mcp-server.js +4 -0
  22. package/dist/onramp.js +2 -0
  23. package/dist/orchestrator/app-operations.js +6 -0
  24. package/dist/orchestrator/cli-options.js +8 -2
  25. package/dist/orchestrator/lifecycle-operations.js +40 -13
  26. package/dist/orchestrator/migration-operations.js +1 -1
  27. package/dist/orchestrator.js +11 -3
  28. package/dist/remote-source.js +10 -3
  29. package/dist/run-export.js +45 -5
  30. package/dist/sandbox-profile.js +6 -1
  31. package/dist/triggers.js +7 -1
  32. package/dist/version.js +1 -1
  33. package/dist/workbench-host.js +18 -2
  34. package/docs/agent-delegation-drive.7.md +4 -0
  35. package/docs/cli-mcp-parity.7.md +16 -2
  36. package/docs/contract-migration-tooling.7.md +4 -0
  37. package/docs/control-plane-scheduling.7.md +4 -0
  38. package/docs/cross-agent-ledger.7.md +217 -0
  39. package/docs/demo.7.md +80 -0
  40. package/docs/designs/handoff-ledger.md +145 -0
  41. package/docs/doctor.7.md +97 -0
  42. package/docs/durable-state-and-locking.7.md +4 -0
  43. package/docs/evidence-adoption-reasoning-chain.7.md +4 -0
  44. package/docs/execution-backends.7.md +4 -0
  45. package/docs/fix.7.md +44 -0
  46. package/docs/handoff-setup.md +120 -0
  47. package/docs/init.7.md +62 -0
  48. package/docs/multi-agent-cli-mcp-surface.7.md +4 -0
  49. package/docs/multi-agent-eval-replay-harness.7.md +4 -0
  50. package/docs/multi-agent-operator-ux.7.md +4 -0
  51. package/docs/node-snapshot-diff-replay.7.md +4 -0
  52. package/docs/observability-cost-accounting.7.md +4 -0
  53. package/docs/pipeline-verbs.7.md +93 -0
  54. package/docs/project-index.md +28 -5
  55. package/docs/real-execution-backends.7.md +4 -0
  56. package/docs/release-and-migration.7.md +4 -0
  57. package/docs/release-tooling.7.md +4 -0
  58. package/docs/routine.7.md +73 -0
  59. package/docs/run-registry-control-plane.7.md +4 -0
  60. package/docs/run-retention-reclamation.7.md +4 -0
  61. package/docs/state-explosion-management.7.md +4 -0
  62. package/docs/team-collaboration.7.md +4 -0
  63. package/docs/web-desktop-workbench.7.md +4 -0
  64. package/manifest/README.md +16 -10
  65. package/manifest/plugin.manifest.json +1 -1
  66. package/package.json +1 -1
  67. package/scripts/agents/agent-adapter-core.js +4 -1
  68. package/scripts/agents/codex-agent.js +34 -4
  69. package/scripts/canonical-apps.js +4 -4
  70. package/scripts/children/batch-delegate-child.js +40 -13
  71. package/scripts/children/http-delegate-child.js +2 -1
  72. package/scripts/dogfood-release.js +1 -1
  73. package/scripts/golden-path.js +4 -4
  74. 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
+ }
@@ -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.", {
@@ -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
- return JSON.parse(raw);
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 (manifest.dispatchId)
194
- (0, commit_1.commitState)(run, `dispatch:${manifest.dispatchId}`);
195
- (0, state_1.saveCheckpoint)(run);
196
- (0, report_1.writeReport)(run);
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
- (0, report_1.writeReport)(run);
216
- (0, state_1.saveCheckpoint)(run);
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
- (0, commit_1.commitState)(run, `worker:${workerId}:result`);
342
- (0, report_1.writeReport)(run);
343
- (0, state_1.saveCheckpoint)(run);
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
- (0, report_1.writeReport)(run);
350
- (0, state_1.saveCheckpoint)(run);
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
- (0, report_1.writeReport)(run);
365
- (0, state_1.saveCheckpoint)(run);
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: JSON.parse(node_fs_1.default.readFileSync(file, "utf8")), contract, dir: node_path_1.default.dirname(file) };
43
+ return { snapshot: (0, state_1.readJson)(file), contract, dir: node_path_1.default.dirname(file) };
44
44
  }
@@ -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 = undefined;
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.
@@ -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
- "const buf=Buffer.from(await r.arrayBuffer());",
291
- "if(cap&&buf.length>cap){process.stderr.write('archive too large');process.exit(3);}",
292
- "fs.writeFileSync(out,buf);return;}",
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);