cool-workflow 0.2.4 → 0.2.6

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 (155) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/.gemini-plugin/mcp.json +10 -0
  4. package/.gemini-plugin/plugin.json +40 -0
  5. package/.opencode-plugin/mcp.json +10 -0
  6. package/.opencode-plugin/plugin.json +40 -0
  7. package/README.md +204 -48
  8. package/apps/architecture-review/app.json +1 -1
  9. package/apps/architecture-review-fast/app.json +1 -1
  10. package/apps/end-to-end-golden-path/app.json +1 -1
  11. package/apps/pr-review-fix-ci/app.json +1 -1
  12. package/apps/release-cut/app.json +1 -1
  13. package/apps/research-synthesis/app.json +1 -1
  14. package/dist/cli/dispatch.js +32 -4
  15. package/dist/cli/entry.js +11 -19
  16. package/dist/cli/global-flags.js +113 -0
  17. package/dist/cli/parseargv.js +7 -1
  18. package/dist/core/capability-data.js +337 -8
  19. package/dist/core/format/help.js +73 -3
  20. package/dist/core/format/recovery-hint.js +32 -0
  21. package/dist/core/multi-agent/collaboration.js +35 -6
  22. package/dist/core/multi-agent/runtime.js +7 -0
  23. package/dist/core/multi-agent/trust-policy.js +7 -1
  24. package/dist/core/pipeline/contract.js +7 -0
  25. package/dist/core/pipeline/error-feedback.js +2 -2
  26. package/dist/core/state/run-paths.js +3 -30
  27. package/dist/core/trust/evidence-grounding.js +13 -1
  28. package/dist/core/util/cli-args.js +22 -0
  29. package/dist/core/version.js +1 -1
  30. package/dist/mcp/dispatch.js +22 -2
  31. package/dist/mcp/server.js +215 -36
  32. package/dist/mcp/tool-process.js +181 -0
  33. package/dist/mcp-server.js +28 -1
  34. package/dist/shell/agent-config.js +11 -2
  35. package/dist/shell/audit-cli.js +1 -1
  36. package/dist/shell/commit.js +8 -2
  37. package/dist/shell/coordinator-io.js +73 -1
  38. package/dist/shell/drive.js +195 -128
  39. package/dist/shell/error-feedback-io.js +6 -0
  40. package/dist/shell/execution-backend/agent.js +201 -26
  41. package/dist/shell/execution-backend/container.js +44 -10
  42. package/dist/shell/execution-backend/local.js +33 -0
  43. package/dist/shell/fs-atomic.js +96 -12
  44. package/dist/shell/ledger-cli.js +9 -2
  45. package/dist/shell/metrics-cli.js +1 -1
  46. package/dist/shell/multi-agent-cli.js +9 -2
  47. package/dist/shell/multi-agent-operator-ux.js +1 -1
  48. package/dist/shell/observability.js +7 -4
  49. package/dist/shell/onramp.js +48 -5
  50. package/dist/shell/operator-ux.js +1 -1
  51. package/dist/shell/perf-trace.js +136 -0
  52. package/dist/shell/pipeline.js +3 -2
  53. package/dist/shell/reclamation-io.js +10 -9
  54. package/dist/shell/report-view-cli.js +2 -0
  55. package/dist/shell/run-export-cli.js +5 -2
  56. package/dist/shell/run-export.js +293 -31
  57. package/dist/shell/run-registry-io.js +8 -0
  58. package/dist/shell/run-store.js +183 -3
  59. package/dist/shell/scheduler-io.js +101 -10
  60. package/dist/shell/state-explosion-cli.js +4 -1
  61. package/dist/shell/telemetry-demo.js +1 -1
  62. package/dist/shell/telemetry-ledger-io.js +36 -24
  63. package/dist/shell/trust-audit.js +178 -36
  64. package/dist/shell/workbench-host.js +121 -10
  65. package/dist/shell/workbench.js +83 -6
  66. package/dist/shell/worker-isolation.js +1 -1
  67. package/dist/wiring/capability-table/basics.js +5 -0
  68. package/dist/wiring/capability-table/exec-backend.js +40 -22
  69. package/dist/wiring/capability-table/parity.js +10 -5
  70. package/dist/wiring/capability-table/pipeline.js +32 -0
  71. package/dist/wiring/capability-table/registry-core.js +26 -3
  72. package/dist/wiring/capability-table/reporting.js +7 -1
  73. package/dist/wiring/capability-table/scheduling-registry.js +8 -2
  74. package/dist/wiring/capability-table/trust-ledger.js +54 -12
  75. package/docs/agent-delegation-drive.7.md +17 -0
  76. package/docs/cli-mcp-parity.7.md +46 -5
  77. package/docs/contract-migration-tooling.7.md +4 -0
  78. package/docs/control-plane-scheduling.7.md +38 -0
  79. package/docs/cross-agent-ledger.7.md +20 -8
  80. package/docs/durable-state-and-locking.7.md +4 -0
  81. package/docs/evidence-adoption-reasoning-chain.7.md +4 -0
  82. package/docs/execution-backends.7.md +4 -0
  83. package/docs/fix.7.md +4 -4
  84. package/docs/getting-started.md +40 -32
  85. package/docs/index.md +17 -0
  86. package/docs/launch/demo.tape +4 -3
  87. package/docs/mcp-app-surface.7.md +27 -0
  88. package/docs/multi-agent-cli-mcp-surface.7.md +4 -0
  89. package/docs/multi-agent-eval-replay-harness.7.md +4 -0
  90. package/docs/multi-agent-operator-ux.7.md +4 -0
  91. package/docs/node-snapshot-diff-replay.7.md +4 -0
  92. package/docs/observability-cost-accounting.7.md +4 -0
  93. package/docs/project-index.md +53 -9
  94. package/docs/real-execution-backends.7.md +4 -0
  95. package/docs/release-and-migration.7.md +4 -0
  96. package/docs/release-tooling.7.md +12 -0
  97. package/docs/routine.7.md +22 -0
  98. package/docs/run-registry-control-plane.7.md +37 -9
  99. package/docs/run-retention-reclamation.7.md +12 -4
  100. package/docs/state-explosion-management.7.md +4 -0
  101. package/docs/team-collaboration.7.md +4 -0
  102. package/docs/trust-audit-anchor.7.md +4 -0
  103. package/docs/web-desktop-workbench.7.md +72 -2
  104. package/docs/workflow-app-framework.7.md +31 -0
  105. package/manifest/plugin.manifest.json +1 -1
  106. package/manifest/source-context-profiles.json +4 -3
  107. package/package.json +6 -3
  108. package/scripts/agents/agent-adapter-core.js +26 -0
  109. package/scripts/agents/claude-p-agent.js +4 -1
  110. package/scripts/agents/codex-agent.js +4 -0
  111. package/scripts/agents/cw-attest-wrap.js +1 -1
  112. package/scripts/agents/gemini-agent.js +4 -0
  113. package/scripts/agents/opencode-agent.js +5 -0
  114. package/scripts/block-unapproved-tag.js +160 -0
  115. package/scripts/canonical-apps.js +4 -4
  116. package/scripts/children/batch-delegate-child.js +72 -4
  117. package/scripts/children/http-batch-delegate-child.js +132 -0
  118. package/scripts/children/http-delegate-child.js +8 -0
  119. package/scripts/dogfood-release.js +2 -2
  120. package/scripts/fake-date-for-reproduction.js +1 -1
  121. package/scripts/gen-manifests.js +32 -61
  122. package/scripts/golden-path.js +4 -4
  123. package/scripts/lang-policy-check.js +82 -0
  124. package/scripts/parity-check.js +42 -23
  125. package/scripts/purity-baseline.json +0 -3
  126. package/scripts/purity-gate.js +16 -3
  127. package/scripts/release-check.js +2 -1
  128. package/scripts/release-flow.js +169 -41
  129. package/scripts/release-gate.js +208 -0
  130. package/scripts/release-oneclick.js +38 -7
  131. package/scripts/release-tags.js +47 -0
  132. package/scripts/schema-version-inventory.json +31 -0
  133. package/scripts/validate-run-state-schema.js +95 -4
  134. package/scripts/verdict-keygen.js +2 -2
  135. package/scripts/verify-bump-reproduction.js +193 -0
  136. package/scripts/verify-release-verdict.js +139 -0
  137. package/scripts/verify-verdict-signature.js +1 -1
  138. package/ui/workbench/app.css +50 -10
  139. package/ui/workbench/app.js +269 -34
  140. package/ui/workbench/index.html +4 -2
  141. package/ui/workbench/inspection.js +51 -0
  142. package/ui/workbench/navigation.js +44 -0
  143. package/docs/agent-framework.md +0 -177
  144. package/docs/designs/handoff-ledger.md +0 -145
  145. package/docs/dogfood/resume-drive-real-agent-2026-06-14.md +0 -40
  146. package/docs/launch/launch-kit.md +0 -195
  147. package/docs/launch/pre-launch-checklist.md +0 -53
  148. package/docs/readme-v0.1.87-full.md +0 -301
  149. package/docs/routines.md +0 -101
  150. package/docs/scheduled-tasks.md +0 -80
  151. package/scripts/agents/claude-p-agent.sh +0 -9
  152. package/scripts/block-unapproved-tag.sh +0 -75
  153. package/scripts/release-gate.sh +0 -94
  154. package/scripts/verify-bump-reproduction.sh +0 -148
  155. package/scripts/verify-container-selfref.js +0 -64
@@ -188,10 +188,17 @@ function ledgerApplyCli(options) {
188
188
  return (0, ledger_1.applyLedgerProposal)(parsed);
189
189
  }
190
190
  function ledgerListCli(options) {
191
- const dirs = Array.isArray(options.dir) ? options.dir.map(String).filter(Boolean) : [];
191
+ // `--ledger-dir` is the preferred flag: the global CLI front door
192
+ // (cli/entry.ts) treats `--dir` as an alias of `--repo` for EVERY
193
+ // command, so `cw ledger list --dir X` made one flag mean two things.
194
+ // `--dir` keeps working unchanged as the legacy spelling; when both are
195
+ // given, `--ledger-dir` wins. Repeated flags become an array via
196
+ // parseArgv's append behavior, same as `--dir` always has.
197
+ const input = options["ledger-dir"] ?? options.dir;
198
+ const dirs = Array.isArray(input) ? input.map(String).filter(Boolean) : [];
192
199
  if (dirs.length > 1)
193
200
  return (0, ledger_io_1.unionLedgerEntries)(dirs);
194
- const dir = required(dirs[0] || stringOption(options.dir), "--dir <ledger-directory>");
201
+ const dir = required(dirs[0] || stringOption(input), "--ledger-dir <ledger-directory>");
195
202
  return (0, ledger_io_1.listLedgerEntries)(dir);
196
203
  }
197
204
  /** MCP-facing verify/apply take the entry OBJECT directly (not a file/
@@ -71,7 +71,7 @@ function nowOf(args) {
71
71
  function metricsShowCli(runId, args) {
72
72
  const run = (0, run_store_1.loadRunFromCwd)(runId, invocationCwd(args));
73
73
  const policy = (0, observability_1.loadCostPolicy)(args, pluginRoot());
74
- return (0, observability_1.showMetricsReport)(run, { now: nowOf(args), policy });
74
+ return (0, observability_1.showMetricsReport)(run, { now: nowOf(args), policy, persist: args.__cwWorkbenchReadOnlyProjection !== true });
75
75
  }
76
76
  /** `cw metrics summary [--scope repo|home] [--pricing ...] [--now ISO] [--limit N]`.
77
77
  * `--limit` defaults to RunRegistry.list()'s own 50-record floor when omitted. */
@@ -113,6 +113,7 @@ exports.evalScoreCli = evalScoreCli;
113
113
  exports.evalGateCli = evalGateCli;
114
114
  exports.evalReportCli = evalReportCli;
115
115
  const path = __importStar(require("node:path"));
116
+ const cli_args_1 = require("../core/util/cli-args");
116
117
  const numeric_flag_1 = require("../core/util/numeric-flag");
117
118
  const run_store_1 = require("./run-store");
118
119
  const report_1 = require("./report");
@@ -139,7 +140,10 @@ function invocationCwd(args) {
139
140
  return typeof args.cwd === "string" && args.cwd.trim() ? path.resolve(args.cwd) : process.cwd();
140
141
  }
141
142
  function loadRun(args, runId) {
142
- return (0, run_store_1.loadRunFromCwd)(runId, invocationCwd(args));
143
+ const run = (0, run_store_1.loadRunFromCwd)(runId, invocationCwd(args));
144
+ if (args.__cwWorkbenchReadOnlyProjection === true)
145
+ run.__cwWorkbenchReadOnlyProjection = true;
146
+ return run;
143
147
  }
144
148
  /** Hold the run's state.json lock across the WHOLE load -> change ->
145
149
  * persist cycle. A bare loadRun + persist pair races: two processes both
@@ -188,7 +192,10 @@ function numberArg(value) {
188
192
  return Number.isFinite(parsed) ? parsed : undefined;
189
193
  }
190
194
  function boolArg(value) {
191
- return Boolean(value);
195
+ // parseBoolFlag reads "false"/"0"/"no"/"off" as false and throws on an
196
+ // unrecognized string — Boolean("false") is true, which silently
197
+ // ENABLED flags like `--allow-self-approval false` (fail-open).
198
+ return (0, cli_args_1.parseBoolFlag)(value, "flag") ?? false;
192
199
  }
193
200
  /** `--multi-agent-run <id>` — parseArgv keeps kebab-case option keys
194
201
  * verbatim (no camelCase folding), so this must check the literal
@@ -91,7 +91,7 @@ function summarizeMultiAgentOperator(run) {
91
91
  const topologies = (0, topology_io_1.summarizeTopologies)(run);
92
92
  const multiAgent = (0, multi_agent_io_1.summarizeMultiAgent)(run);
93
93
  const blackboard = (0, coordinator_io_1.summarizeBlackboard)(run);
94
- const trust = (0, trust_audit_1.summarizeTrustAudit)(run);
94
+ const trust = (0, trust_audit_1.summarizeTrustAudit)(run, { persist: run.__cwWorkbenchReadOnlyProjection !== true });
95
95
  const dependencies = deriveDependencies(run);
96
96
  const failures = deriveFailures(run, dependencies);
97
97
  const evidence = deriveEvidence(run);
@@ -453,13 +453,16 @@ function loadPersistedMetricsFingerprint(run) {
453
453
  }
454
454
  /** Derive + persist the per-run report. The RETURNED payload is pinned to
455
455
  * `freshness: "valid"` (persistedFingerprint === itself) so `cw metrics
456
- * show --json` and the MCP tool are byte-identical; NEVER touches
457
- * state.json (invariant 6). */
456
+ * show --json` and the MCP tool are byte-identical. The Workbench's private
457
+ * projection mode keeps that established payload but does not create a
458
+ * derived file; NEVER touches state.json (invariant 6). */
458
459
  function showMetricsReport(run, options) {
459
460
  const live = deriveMetricsReport(run, { now: options.now, policy: options.policy });
460
461
  const report = { ...live, freshness: { status: "valid", persistedFingerprint: live.sourceFingerprint, currentFingerprint: live.sourceFingerprint } };
461
- fs.mkdirSync(metricsDir(run), { recursive: true });
462
- (0, fs_atomic_1.writeJson)(metricsReportPath(run), report);
462
+ if (options.persist !== false) {
463
+ fs.mkdirSync(metricsDir(run), { recursive: true });
464
+ (0, fs_atomic_1.writeJson)(metricsReportPath(run), report);
465
+ }
463
466
  return report;
464
467
  }
465
468
  function poolRates(metric, rates) {
@@ -278,9 +278,9 @@ function resolveChangedFiles(options = {}) {
278
278
  const root = gitRoot(cwd);
279
279
  const baseRef = resolveBaseRef(root, options.changedFrom, options.env || process.env);
280
280
  const files = new Set();
281
- for (const file of gitLines(root, ["diff", "--name-only", baseRef, "--"]))
281
+ for (const file of gitLinesOrThrow(root, ["diff", "--name-only", baseRef, "--"]))
282
282
  files.add(normalizeChangedPath(file));
283
- for (const file of gitLines(root, ["ls-files", "--others", "--exclude-standard"]))
283
+ for (const file of gitLinesOrThrow(root, ["ls-files", "--others", "--exclude-standard"]))
284
284
  files.add(normalizeChangedPath(file));
285
285
  return { baseRef, files: [...files].filter(Boolean).sort() };
286
286
  }
@@ -406,8 +406,26 @@ function resolveBaseRef(root, changedFrom, env) {
406
406
  return verifyRef(root, changedFrom);
407
407
  if (env.CW_ONRAMP_BASE)
408
408
  return verifyRef(root, env.CW_ONRAMP_BASE);
409
- const baseBranch = env.GITHUB_BASE_REF ? `origin/${env.GITHUB_BASE_REF}` : "origin/main";
410
- const mergeBase = gitOne(root, ["merge-base", "HEAD", baseBranch]);
409
+ // A pull_request CI context sets GITHUB_BASE_REF, so a base ref is EXPECTED:
410
+ // the diff must run against the PR's own base branch. If merge-base cannot
411
+ // resolve there (a shallow clone that never fetched the base, a missing
412
+ // origin ref), we must NOT quietly degrade to HEAD -- a HEAD..HEAD diff on a
413
+ // clean, already-committed tree is empty, and an empty change set has no
414
+ // issues, so onramp:check would report ok:true on a real, unseen change (the
415
+ // same vacuous-pass class PR #446 closed one layer below). Fail closed.
416
+ if (env.GITHUB_BASE_REF) {
417
+ const baseBranch = `origin/${env.GITHUB_BASE_REF}`;
418
+ const mergeBase = gitOne(root, ["merge-base", "HEAD", baseBranch]);
419
+ if (mergeBase)
420
+ return mergeBase;
421
+ throw new Error(`onramp: cannot resolve a base ref -- git merge-base HEAD ${baseBranch} found no common commit (fail closed, not treated as zero changes). Fetch the base branch with a full (non-shallow) clone, or pass --changed-from / CW_ONRAMP_BASE.`);
422
+ }
423
+ // No base ref was requested (--changed-from / CW_ONRAMP_BASE) or expected
424
+ // (GITHUB_BASE_REF): this is the local "show my own changes" use. merge-base
425
+ // against origin/main narrows the diff to this branch's commits when the
426
+ // remote is present; with no remote we fall back to HEAD so `git diff HEAD`
427
+ // still surfaces the working-tree (uncommitted) changes.
428
+ const mergeBase = gitOne(root, ["merge-base", "HEAD", "origin/main"]);
411
429
  if (mergeBase)
412
430
  return mergeBase;
413
431
  return verifyRef(root, "HEAD");
@@ -423,12 +441,37 @@ function verifyRef(root, ref) {
423
441
  function gitRoot(cwd) {
424
442
  return gitOne(node_path_1.default.resolve(cwd), ["rev-parse", "--show-toplevel"]) || node_path_1.default.resolve(cwd);
425
443
  }
444
+ // Every onramp git call is a quick metadata read (rev-parse, merge-base, diff
445
+ // --name-only, ls-files). A finite timeout keeps a HUNG git -- a cold fsmonitor
446
+ // daemon, a credential prompt on a misconfigured remote -- from blocking the
447
+ // gate forever; 5s matches the git-metadata timeout already used in
448
+ // shell/commit.ts and shell/doctor.ts. On a timeout spawnSync sets
449
+ // `result.error` (and a null status), so gitLinesOrThrow fails closed while
450
+ // gitLines/gitOne fall back the same way they do for any other git failure.
451
+ const GIT_TIMEOUT_MS = 5000;
426
452
  function gitLines(cwd, args) {
427
- const result = (0, node_child_process_1.spawnSync)("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
453
+ const result = (0, node_child_process_1.spawnSync)("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: GIT_TIMEOUT_MS });
428
454
  if (result.status !== 0)
429
455
  return [];
430
456
  return String(result.stdout || "").split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
431
457
  }
458
+ /** Same as gitLines, but a failed git invocation THROWS instead of silently
459
+ * becoming []. resolveChangedFiles must never mistake "git could not run
460
+ * this diff" for "there is nothing to diff" -- that turns a transient git
461
+ * failure into a vacuous onramp-contract pass (a 2026-07-12 security audit
462
+ * finding: a broken base ref or a git error made the changed-file set
463
+ * empty, and an empty set has no issues, so the gate reported ok:true on a
464
+ * real, unseen change). Callers that WANT a soft fallback (merge-base
465
+ * probing, optional discovery) should keep using gitLines/gitOne. */
466
+ function gitLinesOrThrow(cwd, args) {
467
+ const result = (0, node_child_process_1.spawnSync)("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: GIT_TIMEOUT_MS });
468
+ if (result.error)
469
+ throw new Error(`onramp: git ${args.join(" ")} failed to run: ${result.error.message}`);
470
+ if (result.status !== 0) {
471
+ throw new Error(`onramp: git ${args.join(" ")} exited ${result.status} -- cannot resolve changed files (fail closed, not treated as zero changes): ${String(result.stderr || "").trim()}`);
472
+ }
473
+ return String(result.stdout || "").split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
474
+ }
432
475
  function gitOne(cwd, args) {
433
476
  return gitLines(cwd, args)[0] || "";
434
477
  }
@@ -290,7 +290,7 @@ function summarizeOperatorRun(run) {
290
290
  const multiAgentOperator = (0, multi_agent_operator_ux_1.summarizeMultiAgentOperator)(run);
291
291
  const multiAgentTrust = (0, trust_policy_io_1.summarizeMultiAgentTrust)(run);
292
292
  const blackboard = (0, coordinator_io_1.summarizeBlackboard)(run);
293
- const trust = (0, trust_audit_1.summarizeTrustAudit)(run);
293
+ const trust = (0, trust_audit_1.summarizeTrustAudit)(run, { persist: run.__cwWorkbenchReadOnlyProjection !== true });
294
294
  const activePhase = phases.find((p) => p.status === "running") || phases.find((p) => p.status === "pending");
295
295
  const blockedReasons = [];
296
296
  for (const worker of workers.workers) {
@@ -0,0 +1,136 @@
1
+ "use strict";
2
+ // shell/perf-trace.ts — opt-in cold-path timing evidence for the benchmark
3
+ // runner. This is not a product log: it is off unless CW_BENCH_TRACE_FILE is
4
+ // set by scripts/bench/run.js, and it writes one JSON report when the drive
5
+ // process ends. Normal CLI and MCP work do not see it.
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.withPerfTraceGroup = withPerfTraceGroup;
41
+ exports.recordPerfDurableWrite = recordPerfDurableWrite;
42
+ const fs = __importStar(require("node:fs"));
43
+ const TRACE_FILE = process.env.CW_BENCH_TRACE_FILE || "";
44
+ const groups = new Map();
45
+ const scopes = [];
46
+ function enabled() {
47
+ return TRACE_FILE.length > 0;
48
+ }
49
+ function groupFor(name) {
50
+ let group = groups.get(name);
51
+ if (!group) {
52
+ group = { durationMs: 0, selfMs: 0, durableWriteCount: 0, durableWriteBytes: 0, samples: [] };
53
+ groups.set(name, group);
54
+ }
55
+ return group;
56
+ }
57
+ function elapsedMs(startedAt) {
58
+ return Number(process.hrtime.bigint() - startedAt) / 1_000_000;
59
+ }
60
+ /** Run synchronous work as one named benchmark group. Nested work is kept in
61
+ * the parent duration, but removed from its self time. That lets the report
62
+ * show dispatch work apart from its agent wait without changing call order. */
63
+ function withPerfTraceGroup(name, fn) {
64
+ if (!enabled())
65
+ return fn();
66
+ const scope = { name, startedAt: process.hrtime.bigint(), childMs: 0, durableWriteCount: 0, durableWriteBytes: 0 };
67
+ scopes.push(scope);
68
+ try {
69
+ return fn();
70
+ }
71
+ finally {
72
+ const ended = scopes.pop();
73
+ if (ended) {
74
+ const durationMs = elapsedMs(ended.startedAt);
75
+ const group = groupFor(ended.name);
76
+ group.durationMs += durationMs;
77
+ group.selfMs += Math.max(0, durationMs - ended.childMs);
78
+ group.durableWriteCount += ended.durableWriteCount;
79
+ group.durableWriteBytes += ended.durableWriteBytes;
80
+ group.samples.push({
81
+ durationMs: rounded(durationMs),
82
+ selfMs: rounded(Math.max(0, durationMs - ended.childMs)),
83
+ durableWriteCount: ended.durableWriteCount,
84
+ durableWriteBytes: ended.durableWriteBytes,
85
+ });
86
+ const parent = scopes[scopes.length - 1];
87
+ if (parent)
88
+ parent.childMs += durationMs;
89
+ }
90
+ }
91
+ }
92
+ /** Count a completed durable file write in the active benchmark group. */
93
+ function recordPerfDurableWrite(bytes) {
94
+ if (!enabled())
95
+ return;
96
+ const active = scopes[scopes.length - 1];
97
+ if (!active) {
98
+ const group = groupFor("other");
99
+ group.durableWriteCount += 1;
100
+ group.durableWriteBytes += bytes;
101
+ return;
102
+ }
103
+ for (const scope of scopes) {
104
+ scope.durableWriteCount += 1;
105
+ scope.durableWriteBytes += bytes;
106
+ }
107
+ }
108
+ function rounded(value) {
109
+ return Math.round(value * 1000) / 1000;
110
+ }
111
+ function writeTrace() {
112
+ if (!enabled())
113
+ return;
114
+ const value = {
115
+ schemaVersion: 1,
116
+ benchmark: "cw-cold-path",
117
+ groups: [...groups.entries()]
118
+ .sort(([left], [right]) => left.localeCompare(right))
119
+ .map(([name, group]) => ({
120
+ name,
121
+ durationMs: rounded(group.durationMs),
122
+ selfMs: rounded(group.selfMs),
123
+ durableWriteCount: group.durableWriteCount,
124
+ durableWriteBytes: group.durableWriteBytes,
125
+ samples: group.samples,
126
+ })),
127
+ };
128
+ try {
129
+ fs.writeFileSync(TRACE_FILE, `${JSON.stringify(value, null, 2)}\n`, "utf8");
130
+ }
131
+ catch {
132
+ // A benchmark trace must never change the drive result or exit status.
133
+ }
134
+ }
135
+ if (enabled())
136
+ process.once("exit", writeTrace);
@@ -48,6 +48,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
48
48
  exports.pureAppendRunNode = exports.writeJson = void 0;
49
49
  exports.plan = plan;
50
50
  const crypto = __importStar(require("node:crypto"));
51
+ const fs = __importStar(require("node:fs"));
51
52
  const path = __importStar(require("node:path"));
52
53
  const run_paths_1 = require("../core/state/run-paths");
53
54
  const migrations_1 = require("../core/state/migrations");
@@ -147,7 +148,7 @@ function plan(app, options) {
147
148
  const runId = injectedRunId || createRunId(app.workflow.id);
148
149
  const runDir = path.join(cwd, ".cw", "runs", runId);
149
150
  const paths = (0, run_paths_1.createRunPaths)(runDir);
150
- (0, run_paths_1.ensureRunDirs)(paths);
151
+ (0, run_store_1.ensureRunDirs)(paths);
151
152
  const tasks = flattenTasks(app, inputs);
152
153
  // Stamp a real wall-clock createdAt/updatedAt at plan time (shell layer —
153
154
  // clock reads are allowed here). The pure migration defaults a run that
@@ -207,7 +208,7 @@ function plan(app, options) {
207
208
  loopStage: "interpret",
208
209
  artifacts: [{ id: "task", kind: "markdown", path: task.taskPath }],
209
210
  metadata: { workflowId: app.workflow.id, appId: app.id, appVersion: app.version, taskId: task.id, phase: task.phase, taskKind: task.kind, requiresEvidence: task.requiresEvidence, sandboxProfileId: task.sandboxProfileId },
210
- }, { persist: false, persistNode: node_store_1.writeRunNode });
211
+ }, { persist: false, persistNode: node_store_1.writeRunNode, pathExists: fs.existsSync });
211
212
  task.stateNodeId = taskResult.outputNodeId;
212
213
  }
213
214
  (0, report_1.writeReport)(run);
@@ -473,14 +473,15 @@ function planReclamation(run, policy = {}) {
473
473
  }
474
474
  }
475
475
  // (3) Superseded, non-verifier-gated commit snapshots. Each commitState()
476
- // call embeds the FULL run into commits/<id>.json, so these grow without
477
- // bound (both in count and per-file size) with no reclamation path today.
478
- // Only the run's LATEST commit and any verifier-gated commit (the actual
479
- // audit-significant milestones) are kept — an intermediate, non-gated
480
- // "checkpoint" commit's only value is as a point-in-time snapshot, and
481
- // state.json (not commits/) is the source of truth for resume. Not
482
- // reconstructable (no recipe): a commit snapshot is a genuine
483
- // point-in-time capture, not a projection derivable from retained data.
476
+ // call writes only the commit's own small record into commits/<id>.json
477
+ // (not the whole run), but these files still add up over a long run with
478
+ // no reclamation path today. Only the run's LATEST commit and any
479
+ // verifier-gated commit (the actual audit-significant milestones) are
480
+ // kept — an intermediate, non-gated "checkpoint" commit's only value is
481
+ // as a point-in-time snapshot, and state.json (not commits/) is the
482
+ // source of truth for resume. Treated as not reconstructable (no recipe)
483
+ // on purpose, kept conservative: a commit snapshot is not offered a
484
+ // projection path derivable from retained data.
484
485
  let reclaimedCommitSnapshot = false;
485
486
  if (!policy.keepCommits) {
486
487
  const commits = run.commits || [];
@@ -663,7 +664,7 @@ function prepareFree(run, tombstone) {
663
664
  for (const nodeId of repointed) {
664
665
  try {
665
666
  const fresh = (0, node_store_1.snapshotNode)(run, nodeId, { persist: false });
666
- const { freshness } = (0, node_snapshot_1.loadNodeSnapshot)(run, fresh);
667
+ const { freshness } = (0, node_snapshot_1.loadNodeSnapshot)(run, fresh, fs.existsSync);
667
668
  if (freshness === "absent") {
668
669
  throw new ReclamationError("repoint-incomplete", `re-pointed node ${nodeId} snapshot is absent (dangling artifact)`, { nodeId });
669
670
  }
@@ -109,6 +109,8 @@ function operatorReportText(runId, args) {
109
109
  /** `cw graph <id> [--json]` / `cw operator graph`. */
110
110
  function graphCli(runId, args) {
111
111
  const run = (0, run_store_1.loadRunFromCwd)(runId, invocationCwd(args));
112
+ if (args.__cwWorkbenchReadOnlyProjection === true)
113
+ run.__cwWorkbenchReadOnlyProjection = true;
112
114
  return (0, operator_ux_1.buildOperatorGraph)(run);
113
115
  }
114
116
  function graphText(runId, args) {
@@ -100,7 +100,10 @@ function runRestoreCli(archivePath, args) {
100
100
  if (!inspect.ok) {
101
101
  return { schemaVersion: 1, ok: false, target, inspect, imported: null, verify: null, registry: null };
102
102
  }
103
- const imported = (0, run_export_1.importRun)(resolvedArchive, target);
103
+ const restored = (0, run_export_1.restoreRunAtomically)(resolvedArchive, target);
104
+ if (!restored.imported) {
105
+ return { schemaVersion: 1, ok: false, target, inspect, imported: null, verify: restored.verification, registry: null };
106
+ }
104
107
  const registry = new run_registry_io_1.RunRegistry(target).refresh({ scope: "repo" });
105
- return { schemaVersion: 1, ok: imported.verification.ok, target, inspect, imported: imported.run, verify: imported.verification, registry };
108
+ return { schemaVersion: 1, ok: true, target, inspect, imported: restored.imported.run, verify: restored.verification, registry };
106
109
  }