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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "cool-workflow",
3
3
  "description": "A workflow control plane and run-time you are able to check: it sends out jobs in TypeScript, makes certain of work against facts before it goes through, puts state into fixed records, orders jobs by time, runs jobs again and again, gets a group of agents to do their parts together, and talks MCP. It gives the doing of the work to outside agents — it never runs the models itself.",
4
- "version": "0.1.96",
4
+ "version": "0.1.98",
5
5
  "author": {
6
6
  "name": "COOLWHITE LLC"
7
7
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cool-workflow",
3
- "version": "0.1.96",
3
+ "version": "0.1.98",
4
4
  "description": "A workflow control plane and run-time you are able to check: it sends out jobs in TypeScript, makes certain of work against facts before it goes through, puts state into fixed records, orders jobs by time, runs jobs again and again, gets a group of agents to do their parts together, and talks MCP. It gives the doing of the work to outside agents — it never runs the models itself.",
5
5
  "author": {
6
6
  "name": "COOLWHITE LLC"
@@ -3,7 +3,7 @@
3
3
  "id": "architecture-review",
4
4
  "title": "Architecture Review",
5
5
  "summary": "Map a repository architecture, assess risks, verify important findings, and synthesize an evidence-backed verdict.",
6
- "version": "0.1.96",
6
+ "version": "0.1.98",
7
7
  "author": "COOLWHITE LLC",
8
8
  "inputs": [
9
9
  {
@@ -3,7 +3,7 @@
3
3
  "id": "architecture-review-fast",
4
4
  "title": "Architecture Review Fast",
5
5
  "summary": "Run a shorter architecture review with parallel map and assess phases for faster first results.",
6
- "version": "0.1.96",
6
+ "version": "0.1.98",
7
7
  "author": "COOLWHITE LLC",
8
8
  "inputs": [
9
9
  {
@@ -3,7 +3,7 @@
3
3
  "id": "end-to-end-golden-path",
4
4
  "title": "End-to-End Golden Path",
5
5
  "summary": "Deterministic one-worker workflow app for proving the CW integration chain.",
6
- "version": "0.1.96",
6
+ "version": "0.1.98",
7
7
  "author": "COOLWHITE LLC",
8
8
  "inputs": [
9
9
  {
@@ -3,7 +3,7 @@
3
3
  "id": "pr-review-fix-ci",
4
4
  "title": "PR Review Fix CI",
5
5
  "summary": "Review a pull request or branch, inspect CI failures, diagnose actionable issues, optionally patch, verify, and summarize with evidence.",
6
- "version": "0.1.96",
6
+ "version": "0.1.98",
7
7
  "author": "COOLWHITE LLC",
8
8
  "inputs": [
9
9
  {
@@ -3,7 +3,7 @@
3
3
  "id": "release-cut",
4
4
  "title": "Release Cut",
5
5
  "summary": "Prepare a release with checklist discipline: version checks, changelog, tests, packaging, release notes, and final verification.",
6
- "version": "0.1.96",
6
+ "version": "0.1.98",
7
7
  "author": "COOLWHITE LLC",
8
8
  "inputs": [
9
9
  {
@@ -3,7 +3,7 @@
3
3
  "id": "research-synthesis",
4
4
  "title": "Research Synthesis",
5
5
  "summary": "Split a research question into claims, investigate sources, cross-check evidence, verify claims, and synthesize a concise answer.",
6
- "version": "0.1.96",
6
+ "version": "0.1.98",
7
7
  "author": "COOLWHITE LLC",
8
8
  "inputs": [
9
9
  {
@@ -115,7 +115,7 @@ function getCandidate(run, candidateId) {
115
115
  // Fail-closed integrity boundary (F4/F5): validate the parsed record against
116
116
  // its type def BEFORE upserting it as a trusted CandidateRecord. A corrupt or
117
117
  // forged candidate.json must throw here rather than flow into the run.
118
- const candidate = (0, validation_1.validateCandidateRecord)(JSON.parse(node_fs_1.default.readFileSync(file, "utf8")));
118
+ const candidate = (0, validation_1.validateCandidateRecord)((0, state_1.readJson)(file));
119
119
  upsertCandidate(run, candidate);
120
120
  return candidate;
121
121
  }
@@ -537,7 +537,7 @@ function loadCandidatesFromDisk(run) {
537
537
  // Fail-closed integrity boundary (F4/F5): each candidate.json is validated
538
538
  // against CandidateRecord before it merges into the run; a corrupt record
539
539
  // throws rather than entering the candidate set as a trusted cast.
540
- .map((file) => (0, validation_1.validateCandidateRecord)(JSON.parse(node_fs_1.default.readFileSync(file, "utf8"))));
540
+ .map((file) => (0, validation_1.validateCandidateRecord)((0, state_1.readJson)(file)));
541
541
  }
542
542
  function readScores(run, candidateId) {
543
543
  const dir = node_path_1.default.join(candidateDir(run, candidateId), "scores");
@@ -550,7 +550,7 @@ function readScores(run, candidateId) {
550
550
  // Fail-closed integrity boundary (F4/F5): a score file is validated against
551
551
  // CandidateScore before it can feed ranking/selection. A corrupt score must
552
552
  // throw, not silently widen the normalized/verdict surface the gate reads.
553
- .map((file) => (0, validation_1.validateCandidateScore)(JSON.parse(node_fs_1.default.readFileSync(node_path_1.default.join(dir, file), "utf8"))));
553
+ .map((file) => (0, validation_1.validateCandidateScore)((0, state_1.readJson)(node_path_1.default.join(dir, file))));
554
554
  }
555
555
  function candidateArtifacts(run, candidate) {
556
556
  return [
@@ -86,6 +86,7 @@ const telemetry_demo_1 = require("./telemetry-demo");
86
86
  const state_1 = require("./state");
87
87
  const run_export_1 = require("./run-export");
88
88
  const result_normalize_1 = require("./result-normalize");
89
+ const cli_options_1 = require("./orchestrator/cli-options");
89
90
  const node_fs_1 = __importDefault(require("node:fs"));
90
91
  const node_path_1 = __importDefault(require("node:path"));
91
92
  const scheduling_1 = require("./scheduling");
@@ -288,7 +289,12 @@ function runExportArchive(runner, runId, args) {
288
289
  // offline. Default falls back to the same env the verify gate reads, so a single
289
290
  // configured key both attests at record-time and travels with the export.
290
291
  const trustPublicKey = optionalString(args["with-trust-key"] || args.withTrustKey || args.trustKey || args.pubkey) || process.env.CW_AGENT_ATTEST_PUBKEY;
291
- return (0, run_export_1.exportRun)(runner.withBaseDir(optionalString(args.cwd)).loadRun(runId), node_path_1.default.resolve(base, output), { trustPublicKey });
292
+ const resolvedOutput = node_path_1.default.resolve(base, output);
293
+ const sysDirs = /^\/(etc|bin|sbin|usr|Library|System|Applications|boot|dev|proc|sys|root|var\/log|var\/run)\//;
294
+ if (sysDirs.test(resolvedOutput)) {
295
+ throw new Error(`Refusing to write archive to a system directory: ${output}`);
296
+ }
297
+ return (0, run_export_1.exportRun)(runner.withBaseDir(optionalString(args.cwd)).loadRun(runId), resolvedOutput, { trustPublicKey });
292
298
  }
293
299
  function runImportArchive(runner, args) {
294
300
  const base = invocationCwd(args);
@@ -548,6 +554,7 @@ const DRIVE_RUNTIME_KEYS = [
548
554
  "agent-timeout-ms",
549
555
  "resume",
550
556
  "incremental",
557
+ "concurrency",
551
558
  // Remote-source flags (v0.1.91): materialized into a local checkout in the capability
552
559
  // layer, never passed to plan as inputs (the resolved sourceUrl/sourceCommit ARE inputs).
553
560
  "link",
@@ -587,6 +594,7 @@ function runDrive(runner, args) {
587
594
  once: isTrue(args.once),
588
595
  now: optionalString(args.now),
589
596
  incremental: isTrue(args.incremental),
597
+ concurrency: (0, cli_options_1.numberOption)(args.concurrency),
590
598
  args
591
599
  });
592
600
  }
@@ -467,7 +467,13 @@ const BUILTIN_CAPABILITIES = [
467
467
  { capability: "comment.list", summary: "List append-only comments for a run (optionally one target).", entry: "collaborationCommentList", surface: "both", cli: { path: ["comment", "list"], caseTokens: ["comment"], jsonMode: "flag" }, mcp: { tool: "cw_comment_list", requiredArgs: ["runId"] } },
468
468
  { capability: "handoff", summary: "Record an ownership transfer (from-actor → to-actor) of a run/task.", entry: "collaborationHandoff", surface: "both", cli: { path: ["handoff"], jsonMode: "default" }, mcp: { tool: "cw_handoff", requiredArgs: ["runId", "targetKind|kind", "targetId|target", "to|toActor"] } },
469
469
  { capability: "review.status", summary: "Read the derived per-target review state + collaboration timeline for a run.", entry: "reviewStatus", surface: "both", cli: { path: ["review", "status"], caseTokens: ["review"], jsonMode: "flag" }, mcp: { tool: "cw_review_status", requiredArgs: ["runId"] } },
470
- { capability: "review.policy", summary: "Set the run's review-gate policy (required approvals, authorized roles, self-approval rule).", entry: "reviewPolicy", surface: "both", cli: { path: ["review", "policy"], caseTokens: ["review"], jsonMode: "default" }, mcp: { tool: "cw_review_policy", requiredArgs: ["runId"] } }
470
+ { capability: "review.policy", summary: "Set the run's review-gate policy (required approvals, authorized roles, self-approval rule).", entry: "reviewPolicy", surface: "both", cli: { path: ["review", "policy"], caseTokens: ["review"], jsonMode: "default" }, mcp: { tool: "cw_review_policy", requiredArgs: ["runId"] } },
471
+ // ---- Cross-agent handoff ledger (stage 2, MCP surface + git transport) --
472
+ { capability: "ledger.propose", summary: "Build a verifiable cross-agent change proposal entry (printed as JSON).", entry: "buildLedgerProposal", surface: "both", cli: { path: ["ledger", "propose"], caseTokens: ["ledger", "propose"], jsonMode: "default" }, mcp: { tool: "cw_ledger_propose", requiredArgs: ["from", "to", "title", "rationale"] }, payloadIdentical: false, reason: "Mints a fresh entry each call: createdAt is the wall-clock instant and the id/digest are derived from it, so the output is inherently non-deterministic and a byte-identity probe does not apply. Both surfaces call the same buildLedgerProposal core; round-trip + fail-closed behavior is covered by ledger-verify-smoke." },
473
+ { capability: "ledger.review", summary: "Build a verifiable cross-agent review verdict entry (printed as JSON).", entry: "buildLedgerReview", surface: "both", cli: { path: ["ledger", "review"], caseTokens: ["ledger", "review"], jsonMode: "default" }, mcp: { tool: "cw_ledger_review", requiredArgs: ["from", "to", "target", "verdict"] }, payloadIdentical: false, reason: "Mints a fresh timestamped/digested verdict each call — non-deterministic output, same reasoning as ledger.propose. Both surfaces call the same buildLedgerReview core." },
474
+ { capability: "ledger.verify", summary: "Verify a ledger entry against its content digest (fail-closed on tampering).", entry: "verifyLedgerEntry", surface: "both", cli: { path: ["ledger", "verify"], caseTokens: ["ledger", "verify"], jsonMode: "default" }, mcp: { tool: "cw_ledger_verify", requiredArgs: ["entry"] }, payloadIdentical: false, reason: "The entry arrives by --file/stdin on the CLI and as an `entry` argument over MCP; there is no shared arg-bag the byte-identity probe can feed both. Both surfaces call the same verifyLedgerEntry core; ledger-verify-smoke proves the fail-closed contract." },
475
+ { capability: "ledger.apply", summary: "Verify a proposal entry and return its suggestedDiff for `git apply` (fail-closed: no diff unless the entry verifies as a proposal).", entry: "applyLedgerProposal", surface: "both", cli: { path: ["ledger", "apply"], caseTokens: ["ledger", "apply"], jsonMode: "default" }, mcp: { tool: "cw_ledger_apply", requiredArgs: ["entry"] }, payloadIdentical: false, reason: "The entry arrives by --file/stdin on the CLI and as an `entry` argument over MCP; there is no shared arg-bag the byte-identity probe can feed both. Both surfaces call the same applyLedgerProposal core (a fail-closed wrapper over verifyLedgerEntry); ledger-apply-smoke proves the diff only escapes a verified proposal." },
476
+ { capability: "ledger.list", summary: "Read + verify every entry in one or more shared ledger directories (fail-closed inbox; 2+ dirs union-verify mirrors).", entry: "listLedgerEntries", surface: "both", cli: { path: ["ledger", "list"], caseTokens: ["ledger", "list"], jsonMode: "default" }, mcp: { tool: "cw_ledger_list", requiredArgs: ["dir|dirs"] }, payloadIdentical: false, reason: "Output depends on the on-disk contents of the named ledger directory/directories, which the generic payload probe does not populate. Both surfaces call the same listLedgerEntries/unionLedgerEntries core; ledger-verify-smoke covers the fail-closed inbox and the multi-mirror union." }
471
477
  ];
472
478
  /** The capability registry — the single source of truth, deduplicated by
473
479
  * capability id (last declaration wins). Derived directly from the table above:
@@ -54,6 +54,7 @@ const registry_1 = require("./handlers/registry");
54
54
  const multi_agent_1 = require("./handlers/multi-agent");
55
55
  const run_1 = require("./handlers/run");
56
56
  const collaboration_1 = require("./handlers/collaboration");
57
+ const ledger_1 = require("./handlers/ledger");
57
58
  const blackboard_1 = require("./handlers/blackboard");
58
59
  const eval_1 = require("./handlers/eval");
59
60
  const node_1 = require("./handlers/node");
@@ -427,6 +428,9 @@ async function runCli(argv = process.argv.slice(2)) {
427
428
  case "review":
428
429
  (0, collaboration_1.handleReview)(args, runner);
429
430
  return;
431
+ case "ledger":
432
+ (0, ledger_1.handleLedger)(args, runner);
433
+ return;
430
434
  case "loop": {
431
435
  (0, io_1.printJson)(scheduler.create({ ...args.options, kind: "loop" }));
432
436
  return;
@@ -0,0 +1,169 @@
1
+ "use strict";
2
+ // `cw ledger propose|review|verify` — the cross-agent handoff ledger CLI surface.
3
+ // A proposing agent prints a proposal or a review verdict as a verifiable JSON
4
+ // entry; the receiving side verifies it fail-closed before acting. See
5
+ // docs/cross-agent-ledger.7.md and docs/designs/handoff-ledger.md.
6
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
7
+ if (k2 === undefined) k2 = k;
8
+ var desc = Object.getOwnPropertyDescriptor(m, k);
9
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
10
+ desc = { enumerable: true, get: function() { return m[k]; } };
11
+ }
12
+ Object.defineProperty(o, k2, desc);
13
+ }) : (function(o, m, k, k2) {
14
+ if (k2 === undefined) k2 = k;
15
+ o[k2] = m[k];
16
+ }));
17
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
18
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
19
+ }) : function(o, v) {
20
+ o["default"] = v;
21
+ });
22
+ var __importStar = (this && this.__importStar) || (function () {
23
+ var ownKeys = function(o) {
24
+ ownKeys = Object.getOwnPropertyNames || function (o) {
25
+ var ar = [];
26
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
27
+ return ar;
28
+ };
29
+ return ownKeys(o);
30
+ };
31
+ return function (mod) {
32
+ if (mod && mod.__esModule) return mod;
33
+ var result = {};
34
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
35
+ __setModuleDefault(result, mod);
36
+ return result;
37
+ };
38
+ })();
39
+ Object.defineProperty(exports, "__esModule", { value: true });
40
+ exports.handleLedger = handleLedger;
41
+ const fs = __importStar(require("fs"));
42
+ const ledger_1 = require("../../ledger");
43
+ const io_1 = require("../io");
44
+ /** Coerce a repeatable/comma-joined list option to a clean string[]. */
45
+ function listOption(value) {
46
+ const parts = Array.isArray(value) ? value : typeof value === "string" ? value.split(",") : [];
47
+ return parts.map((p) => String(p).trim()).filter(Boolean);
48
+ }
49
+ function stringOption(value) {
50
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
51
+ }
52
+ function nowIso() {
53
+ return new Date().toISOString();
54
+ }
55
+ function handleLedger(args, _runner) {
56
+ const [subcommand] = args.positionals;
57
+ const opts = args.options;
58
+ switch (subcommand) {
59
+ case "propose": {
60
+ const entry = (0, ledger_1.buildLedgerProposal)({
61
+ from: (0, io_1.required)(stringOption(opts.from), "--from <agent/repo>"),
62
+ to: (0, io_1.required)(stringOption(opts.to), "--to <agent/repo>"),
63
+ title: (0, io_1.required)(stringOption(opts.title), "--title <text>"),
64
+ rationale: (0, io_1.required)(stringOption(opts.rationale), "--rationale <text>"),
65
+ targetFiles: listOption(opts.files),
66
+ // Do NOT trim the diff: it is a unified patch (payload, not a label), and
67
+ // trimming strips the trailing newline `git apply` requires — a trimmed
68
+ // diff is a corrupt patch. Presence is detected with a trimmed test, but
69
+ // the bytes are passed through verbatim (matching the MCP propose path).
70
+ suggestedDiff: typeof opts.diff === "string" && opts.diff.trim() ? opts.diff : undefined,
71
+ createdAt: nowIso()
72
+ });
73
+ (0, io_1.printJson)(entry);
74
+ return;
75
+ }
76
+ case "review": {
77
+ const verdictRaw = (0, io_1.required)(stringOption(opts.verdict), "--verdict <approved|rejected>").toUpperCase();
78
+ if (verdictRaw !== "APPROVED" && verdictRaw !== "REJECTED") {
79
+ throw new Error('--verdict must be "approved" or "rejected".');
80
+ }
81
+ const entry = (0, ledger_1.buildLedgerReview)({
82
+ from: (0, io_1.required)(stringOption(opts.from), "--from <agent/repo>"),
83
+ to: (0, io_1.required)(stringOption(opts.to), "--to <agent/repo>"),
84
+ target: (0, io_1.required)(stringOption(opts.target), "--target <proposal-id|pr-ref>"),
85
+ verdict: verdictRaw,
86
+ findings: listOption(opts.findings),
87
+ createdAt: nowIso()
88
+ });
89
+ (0, io_1.printJson)(entry);
90
+ return;
91
+ }
92
+ case "verify": {
93
+ const file = stringOption(opts.file);
94
+ let text;
95
+ try {
96
+ // --file <path>, else read the entry from stdin (fd 0).
97
+ text = fs.readFileSync(file || 0, "utf8");
98
+ }
99
+ catch (error) {
100
+ throw new Error(`Cannot read ledger entry${file ? ` from ${file}` : " from stdin"}: ${error.message}`);
101
+ }
102
+ let parsed;
103
+ try {
104
+ parsed = JSON.parse(text);
105
+ }
106
+ catch {
107
+ // A non-JSON input is itself a fail-closed refusal, not a crash.
108
+ (0, io_1.printJson)({ ok: false, id: null, kind: null, checks: [{ name: "parse", pass: false, code: "ledger-bad-json" }], failedChecks: [{ name: "parse", code: "ledger-bad-json" }] });
109
+ process.exitCode = 1;
110
+ return;
111
+ }
112
+ const result = (0, ledger_1.verifyLedgerEntry)(parsed);
113
+ (0, io_1.printJson)(result);
114
+ // Fail-closed: a tampered/malformed entry exits non-zero so
115
+ // `cw ledger verify <file> && open-pr` cannot proceed on a lie.
116
+ if (!result.ok)
117
+ process.exitCode = 1;
118
+ return;
119
+ }
120
+ case "apply": {
121
+ const file = stringOption(opts.file);
122
+ let text;
123
+ try {
124
+ // --file <path>, else read the entry from stdin (fd 0), same as verify.
125
+ text = fs.readFileSync(file || 0, "utf8");
126
+ }
127
+ catch (error) {
128
+ throw new Error(`Cannot read ledger entry${file ? ` from ${file}` : " from stdin"}: ${error.message}`);
129
+ }
130
+ let parsed;
131
+ try {
132
+ parsed = JSON.parse(text);
133
+ }
134
+ catch {
135
+ (0, io_1.printJson)({ ok: false, id: null, kind: null, diff: null, failedChecks: [{ name: "parse", code: "ledger-bad-json" }] });
136
+ process.exitCode = 1;
137
+ return;
138
+ }
139
+ const result = (0, ledger_1.applyLedgerProposal)(parsed);
140
+ (0, io_1.printJson)(result);
141
+ // Fail-closed: the diff only comes out (ok:true) when the proposal verifies,
142
+ // so `cw ledger apply <file> | git apply` never feeds git an unverified patch.
143
+ if (!result.ok)
144
+ process.exitCode = 1;
145
+ return;
146
+ }
147
+ case "list": {
148
+ // `--dir` is repeatable: 2+ dirs union-verify multiple mirrors into one
149
+ // inbox; a single --dir keeps the original single-directory output (POLA).
150
+ const dirs = Array.isArray(opts.dir) ? opts.dir.map(String).filter(Boolean) : [];
151
+ if (dirs.length > 1) {
152
+ const union = (0, ledger_1.unionLedgerEntries)(dirs);
153
+ (0, io_1.printJson)(union);
154
+ if (!union.allOk)
155
+ process.exitCode = 1;
156
+ return;
157
+ }
158
+ const dir = (0, io_1.required)(dirs[0] || stringOption(opts.dir), "--dir <ledger-directory>");
159
+ const result = (0, ledger_1.listLedgerEntries)(dir);
160
+ (0, io_1.printJson)(result);
161
+ // Fail-closed inbox: refuse the whole batch if any entry does not verify.
162
+ if (!result.allOk)
163
+ process.exitCode = 1;
164
+ return;
165
+ }
166
+ default:
167
+ throw new Error("Usage: cw ledger propose|review|verify|apply|list [options]");
168
+ }
169
+ }
@@ -77,7 +77,13 @@ function handleRoutine(args, triggers) {
77
77
  return;
78
78
  case "fire": {
79
79
  const kind = (0, io_1.required)(idOrKind, "trigger kind");
80
- const payload = payloadPath ? JSON.parse(node_fs_1.default.readFileSync(payloadPath, "utf8")) : args.options;
80
+ let payload;
81
+ try {
82
+ payload = payloadPath ? JSON.parse(node_fs_1.default.readFileSync(payloadPath, "utf8")) : args.options;
83
+ }
84
+ catch (e) {
85
+ throw new Error(`Failed to parse payload${payloadPath ? ` file "${payloadPath}"` : ""}: ${String(e && e.message || e)}`);
86
+ }
81
87
  (0, io_1.printJson)(triggers.fire(kind, payload));
82
88
  return;
83
89
  }