cool-workflow 0.2.5 → 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 (80) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/.gemini-plugin/plugin.json +1 -1
  4. package/.opencode-plugin/plugin.json +1 -1
  5. package/README.md +5 -3
  6. package/apps/architecture-review/app.json +1 -1
  7. package/apps/architecture-review-fast/app.json +1 -1
  8. package/apps/end-to-end-golden-path/app.json +1 -1
  9. package/apps/pr-review-fix-ci/app.json +1 -1
  10. package/apps/release-cut/app.json +1 -1
  11. package/apps/research-synthesis/app.json +1 -1
  12. package/dist/core/state/run-paths.js +3 -30
  13. package/dist/core/version.js +1 -1
  14. package/dist/mcp/server.js +95 -27
  15. package/dist/mcp/tool-process.js +181 -0
  16. package/dist/mcp-server.js +8 -1
  17. package/dist/shell/agent-config.js +11 -2
  18. package/dist/shell/audit-cli.js +1 -1
  19. package/dist/shell/drive.js +99 -89
  20. package/dist/shell/execution-backend/agent.js +18 -15
  21. package/dist/shell/execution-backend/local.js +1 -0
  22. package/dist/shell/fs-atomic.js +3 -0
  23. package/dist/shell/metrics-cli.js +1 -1
  24. package/dist/shell/multi-agent-cli.js +4 -1
  25. package/dist/shell/multi-agent-operator-ux.js +1 -1
  26. package/dist/shell/observability.js +7 -4
  27. package/dist/shell/operator-ux.js +1 -1
  28. package/dist/shell/perf-trace.js +136 -0
  29. package/dist/shell/pipeline.js +1 -1
  30. package/dist/shell/report-view-cli.js +2 -0
  31. package/dist/shell/run-export-cli.js +5 -2
  32. package/dist/shell/run-export.js +241 -27
  33. package/dist/shell/run-registry-io.js +8 -0
  34. package/dist/shell/run-store.js +27 -3
  35. package/dist/shell/state-explosion-cli.js +4 -1
  36. package/dist/shell/telemetry-demo.js +1 -1
  37. package/dist/shell/trust-audit.js +91 -43
  38. package/dist/shell/workbench.js +4 -1
  39. package/dist/wiring/capability-table/parity.js +10 -5
  40. package/docs/agent-delegation-drive.7.md +2 -0
  41. package/docs/cli-mcp-parity.7.md +21 -5
  42. package/docs/contract-migration-tooling.7.md +2 -0
  43. package/docs/control-plane-scheduling.7.md +2 -0
  44. package/docs/durable-state-and-locking.7.md +2 -0
  45. package/docs/evidence-adoption-reasoning-chain.7.md +2 -0
  46. package/docs/execution-backends.7.md +2 -0
  47. package/docs/mcp-app-surface.7.md +21 -0
  48. package/docs/multi-agent-cli-mcp-surface.7.md +2 -0
  49. package/docs/multi-agent-eval-replay-harness.7.md +2 -0
  50. package/docs/multi-agent-operator-ux.7.md +2 -0
  51. package/docs/node-snapshot-diff-replay.7.md +2 -0
  52. package/docs/observability-cost-accounting.7.md +2 -0
  53. package/docs/project-index.md +18 -4
  54. package/docs/real-execution-backends.7.md +2 -0
  55. package/docs/release-and-migration.7.md +2 -0
  56. package/docs/release-tooling.7.md +10 -0
  57. package/docs/run-registry-control-plane.7.md +35 -9
  58. package/docs/run-retention-reclamation.7.md +2 -0
  59. package/docs/state-explosion-management.7.md +2 -0
  60. package/docs/team-collaboration.7.md +2 -0
  61. package/docs/trust-audit-anchor.7.md +2 -0
  62. package/docs/web-desktop-workbench.7.md +48 -0
  63. package/manifest/plugin.manifest.json +1 -1
  64. package/package.json +3 -2
  65. package/scripts/canonical-apps.js +4 -4
  66. package/scripts/dogfood-release.js +1 -1
  67. package/scripts/gen-manifests.js +32 -61
  68. package/scripts/golden-path.js +4 -4
  69. package/scripts/parity-check.js +42 -23
  70. package/scripts/purity-baseline.json +0 -3
  71. package/scripts/release-flow.js +43 -13
  72. package/scripts/release-gate.js +30 -6
  73. package/scripts/schema-version-inventory.json +31 -0
  74. package/scripts/validate-run-state-schema.js +95 -4
  75. package/scripts/verify-release-verdict.js +139 -0
  76. package/ui/workbench/app.css +15 -7
  77. package/ui/workbench/app.js +118 -16
  78. package/ui/workbench/index.html +2 -0
  79. package/ui/workbench/inspection.js +51 -0
  80. package/ui/workbench/navigation.js +44 -0
@@ -79,6 +79,7 @@ const reporter_1 = require("./reporter");
79
79
  const fs_atomic_1 = require("./fs-atomic");
80
80
  const observability_1 = require("./observability");
81
81
  const telemetry_attestation_1 = require("../core/trust/telemetry-attestation");
82
+ const perf_trace_1 = require("./perf-trace");
82
83
  /** Total RECORDED tokens across the run's attested units, reading the
83
84
  * agent-reported usage through normalizeReportedUsage so a hop that
84
85
  * reported snake_case buckets (`input_tokens`/`output_tokens`, the shape
@@ -117,8 +118,9 @@ function tokenBudgetUsage(run) {
117
118
  return undefined;
118
119
  return { spent: recordedTokenTotal(run), budget };
119
120
  }
120
- exports.DRIVE_SCHEMA_VERSION = 1;
121
- exports.MAX_SUB_WORKFLOW_DEPTH = 4;
121
+ var drive_decide_2 = require("../core/pipeline/drive-decide");
122
+ Object.defineProperty(exports, "DRIVE_SCHEMA_VERSION", { enumerable: true, get: function () { return drive_decide_2.DRIVE_SCHEMA_VERSION; } });
123
+ Object.defineProperty(exports, "MAX_SUB_WORKFLOW_DEPTH", { enumerable: true, get: function () { return drive_decide_2.MAX_SUB_WORKFLOW_DEPTH; } });
122
124
  function agentConfigured(config) {
123
125
  return Boolean(config.command || config.endpoint);
124
126
  }
@@ -433,8 +435,8 @@ function processSelectedTask(ctx, selectedId, preparedOutcome, deferPersist = fa
433
435
  }
434
436
  function runSubWorkflow(ctx, run, selected, workerId, manifest, spec, deferPersist = false) {
435
437
  const parentApp = run.workflow.id;
436
- if (ctx.depth + 1 > exports.MAX_SUB_WORKFLOW_DEPTH) {
437
- return handleHop(ctx, selected, workerId, `sub-workflow depth limit exceeded (> ${exports.MAX_SUB_WORKFLOW_DEPTH})`, deferPersist, deferPersist ? run : undefined);
438
+ if (ctx.depth + 1 > drive_decide_1.MAX_SUB_WORKFLOW_DEPTH) {
439
+ return handleHop(ctx, selected, workerId, `sub-workflow depth limit exceeded (> ${drive_decide_1.MAX_SUB_WORKFLOW_DEPTH})`, deferPersist, deferPersist ? run : undefined);
438
440
  }
439
441
  if ([...ctx.visitedAppIds, parentApp].includes(spec.appId)) {
440
442
  return handleHop(ctx, selected, workerId, `sub-workflow cycle detected: ${[...ctx.visitedAppIds, parentApp, spec.appId].join(" -> ")}`, deferPersist, deferPersist ? run : undefined);
@@ -682,75 +684,79 @@ function prepareConcurrentOutcomes(ctx, batch) {
682
684
  // all-sub-workflow batch), or a crash before the round-end flush loses
683
685
  // the dispatch.
684
686
  let dispatchedThisRound = false;
685
- for (const taskId of batch) {
686
- const run = loadRun(ctx);
687
- const task = run.tasks.find((candidate) => candidate.id === taskId);
688
- if (!task || (task.status !== "pending" && task.status !== "running"))
689
- continue;
690
- let workerId = task.workerId;
691
- if (task.status === "pending") {
692
- const manifest = (0, dispatch_2.createDispatchManifest)(run, 1, { backendId: task.agentType || "agent" });
693
- const dispatchedTask = manifest.tasks.find((entry) => entry.id === task.id) || manifest.tasks[0];
694
- if (!dispatchedTask || !dispatchedTask.workerId) {
695
- failSteps.set(taskId, (0, drive_decide_1.makeStep)("dispatch", "failed", { runId: ctx.runId, taskId, phase: task.phase, reason: "dispatch produced no worker scope" }));
687
+ // Dispatch is a short local mutation group. Keep its audit lock only until
688
+ // its events have one durable append; the agent work below stays outside it.
689
+ (0, trust_audit_1.withTrustAuditBatch)(loadRun(ctx), () => {
690
+ for (const taskId of batch) {
691
+ const run = loadRun(ctx);
692
+ const task = run.tasks.find((candidate) => candidate.id === taskId);
693
+ if (!task || (task.status !== "pending" && task.status !== "running"))
696
694
  continue;
695
+ let workerId = task.workerId;
696
+ if (task.status === "pending") {
697
+ const manifest = (0, dispatch_2.createDispatchManifest)(run, 1, { backendId: task.agentType || "agent" });
698
+ const dispatchedTask = manifest.tasks.find((entry) => entry.id === task.id) || manifest.tasks[0];
699
+ if (!dispatchedTask || !dispatchedTask.workerId) {
700
+ failSteps.set(taskId, (0, drive_decide_1.makeStep)("dispatch", "failed", { runId: ctx.runId, taskId, phase: task.phase, reason: "dispatch produced no worker scope" }));
701
+ continue;
702
+ }
703
+ workerId = dispatchedTask.workerId;
704
+ dispatchedThisRound = true;
697
705
  }
698
- workerId = dispatchedTask.workerId;
699
- dispatchedThisRound = true;
700
- }
701
- if (!workerId) {
702
- failSteps.set(taskId, (0, drive_decide_1.makeStep)("dispatch", "failed", { runId: ctx.runId, taskId, phase: task.phase, reason: "no worker scope for task" }));
703
- continue;
704
- }
705
- const freshRun = loadRun(ctx);
706
- const manifest = (0, worker_isolation_1.showWorkerManifest)(freshRun, workerId);
707
- const delegationDigest = ctx.incremental
708
- ? (0, drive_decide_1.incrementalDelegationDigest)(task.model || ctx.config.model || "", task.agentType || "agent", manifest.sandboxPolicy?.id || task.sandboxProfileId || "", ctx.config.command || "", ctx.config.args ? (0, agent_1.stripSecretArgs)(ctx.config.args) : [], ctx.config.endpoint || "")
709
- : "";
710
- const cachePath = resultCachePath(freshRun, task, (0, hash_1.sha256)(task.prompt || ""), ctx.incremental, delegationDigest);
711
- if (cachePath && fs.existsSync(cachePath))
712
- continue;
713
- // A sub-workflow task never goes through the spawn path processSelectedTask
714
- // checks task.subWorkflow before it ever looks at a prepared spawn outcome
715
- // and always takes its own runSubWorkflow branch instead (mirrors that
716
- // check byte-for-byte). Building a spawn OR endpoint job for it here would
717
- // just be a real agent child spawned/delegated and thrown away unused.
718
- const subWorkflow = task.subWorkflow;
719
- if (subWorkflow)
720
- continue;
721
- const request = {
722
- schemaVersion: 1,
723
- runId: ctx.runId,
724
- taskId: task.id,
725
- backendId: task.agentType || "agent",
726
- cwd: freshRun.cwd,
727
- sandboxPolicy: manifest.sandboxPolicy,
728
- manifest: { workerDir: manifest.workerDir, manifestPath: manifest.manifestPath, inputPath: manifest.inputPath, resultPath: manifest.resultPath, prompt: manifest.prompt },
729
- label: task.id,
730
- timeoutMs: ctx.config.timeoutMs,
731
- delegation: { command: ctx.config.command, args: ctx.config.args, endpoint: ctx.config.endpoint, model: task.model || ctx.config.model },
732
- };
733
- const job = (0, agent_1.prepareAgentSpawn)(request);
734
- if (job) {
735
- const sandboxPolicy = manifest.sandboxPolicy;
736
- if (sandboxPolicy) {
737
- job.env = (0, agent_1.buildAgentChildEnv)(sandboxPolicy).env;
706
+ if (!workerId) {
707
+ failSteps.set(taskId, (0, drive_decide_1.makeStep)("dispatch", "failed", { runId: ctx.runId, taskId, phase: task.phase, reason: "no worker scope for task" }));
708
+ continue;
709
+ }
710
+ const freshRun = loadRun(ctx);
711
+ const manifest = (0, worker_isolation_1.showWorkerManifest)(freshRun, workerId);
712
+ const delegationDigest = ctx.incremental
713
+ ? (0, drive_decide_1.incrementalDelegationDigest)(task.model || ctx.config.model || "", task.agentType || "agent", manifest.sandboxPolicy?.id || task.sandboxProfileId || "", ctx.config.command || "", ctx.config.args ? (0, agent_1.stripSecretArgs)(ctx.config.args) : [], ctx.config.endpoint || "")
714
+ : "";
715
+ const cachePath = resultCachePath(freshRun, task, (0, hash_1.sha256)(task.prompt || ""), ctx.incremental, delegationDigest);
716
+ if (cachePath && fs.existsSync(cachePath))
717
+ continue;
718
+ // A sub-workflow task never goes through the spawn path — processSelectedTask
719
+ // checks task.subWorkflow before it ever looks at a prepared spawn outcome
720
+ // and always takes its own runSubWorkflow branch instead (mirrors that
721
+ // check byte-for-byte). Building a spawn OR endpoint job for it here would
722
+ // just be a real agent child spawned/delegated and thrown away unused.
723
+ const subWorkflow = task.subWorkflow;
724
+ if (subWorkflow)
725
+ continue;
726
+ const request = {
727
+ schemaVersion: 1,
728
+ runId: ctx.runId,
729
+ taskId: task.id,
730
+ backendId: task.agentType || "agent",
731
+ cwd: freshRun.cwd,
732
+ sandboxPolicy: manifest.sandboxPolicy,
733
+ manifest: { workerDir: manifest.workerDir, manifestPath: manifest.manifestPath, inputPath: manifest.inputPath, resultPath: manifest.resultPath, prompt: manifest.prompt },
734
+ label: task.id,
735
+ timeoutMs: ctx.config.timeoutMs,
736
+ delegation: { command: ctx.config.command, args: ctx.config.args, endpoint: ctx.config.endpoint, model: task.model || ctx.config.model },
737
+ };
738
+ const job = (0, agent_1.prepareAgentSpawn)(request);
739
+ if (job) {
740
+ const sandboxPolicy = manifest.sandboxPolicy;
741
+ if (sandboxPolicy) {
742
+ job.env = (0, agent_1.buildAgentChildEnv)(sandboxPolicy).env;
743
+ }
744
+ jobs.push(job);
745
+ jobTaskIds.push(taskId);
746
+ continue;
747
+ }
748
+ // Endpoint-configured agent: no CLI binary to batch-spawn, but its HTTP
749
+ // delegation joins the concurrent window through the endpoint batch child
750
+ // (runEndpointBatchOutcomes) rather than settling one-at-a-time on the
751
+ // serial path. An unconfigured agent (neither command nor endpoint) returns
752
+ // undefined from both prepares here and falls to the serial refusal.
753
+ const endpointJob = (0, agent_1.prepareEndpointJob)(request);
754
+ if (endpointJob) {
755
+ endpointJobs.push(endpointJob);
756
+ endpointTaskIds.push(taskId);
738
757
  }
739
- jobs.push(job);
740
- jobTaskIds.push(taskId);
741
- continue;
742
- }
743
- // Endpoint-configured agent: no CLI binary to batch-spawn, but its HTTP
744
- // delegation joins the concurrent window through the endpoint batch child
745
- // (runEndpointBatchOutcomes) rather than settling one-at-a-time on the
746
- // serial path. An unconfigured agent (neither command nor endpoint) returns
747
- // undefined from both prepares here and falls to the serial refusal.
748
- const endpointJob = (0, agent_1.prepareEndpointJob)(request);
749
- if (endpointJob) {
750
- endpointJobs.push(endpointJob);
751
- endpointTaskIds.push(taskId);
752
758
  }
753
- }
759
+ });
754
760
  const totalJobs = jobs.length + endpointJobs.length;
755
761
  if (totalJobs) {
756
762
  emitProgress(`⇉ concurrent round: ${totalJobs} agent${totalJobs > 1 ? "s" : ""} spawning in parallel, may take minutes…`);
@@ -802,28 +808,32 @@ function driveConcurrentRound(ctx, limit) {
802
808
  .filter((task) => phaseTaskIds.has(task.id) && (task.status === "pending" || task.status === "running"))
803
809
  .slice(0, width)
804
810
  .map((task) => task.id);
805
- const prepared = prepareConcurrentOutcomes(ctx, batch);
811
+ const prepared = (0, perf_trace_1.withPerfTraceGroup)("dispatch", () => prepareConcurrentOutcomes(ctx, batch));
806
812
  const steps = [];
807
- for (const taskId of batch) {
808
- const failStep = prepared.failSteps.get(taskId);
809
- if (failStep) {
810
- steps.push(failStep);
811
- continue;
813
+ // Settlement is also local and synchronous. Flush the complete audit
814
+ // group before the existing round checkpoint, never across it.
815
+ (0, perf_trace_1.withPerfTraceGroup)("settlement", () => (0, trust_audit_1.withTrustAuditBatch)(loadRun(ctx), () => {
816
+ for (const taskId of batch) {
817
+ const failStep = prepared.failSteps.get(taskId);
818
+ if (failStep) {
819
+ steps.push(failStep);
820
+ continue;
821
+ }
822
+ // Re-read per task: a prior accept in this round mutated state (the
823
+ // SAME cached object via loadRun's round cache — no disk round-trip
824
+ // until the round-end flush below).
825
+ const freshRun = loadRun(ctx);
826
+ const fresh = freshRun.tasks.find((task) => task.id === taskId);
827
+ if (!fresh || (fresh.status !== "pending" && fresh.status !== "running"))
828
+ continue;
829
+ steps.push(processSelectedTask(ctx, taskId, prepared.outcomes.get(taskId), true));
812
830
  }
813
- // Re-read per task: a prior accept in this round mutated state (the
814
- // SAME cached object via loadRun's round cache — no disk round-trip
815
- // until the round-end flush below).
816
- const freshRun = loadRun(ctx);
817
- const fresh = freshRun.tasks.find((task) => task.id === taskId);
818
- if (!fresh || (fresh.status !== "pending" && fresh.status !== "running"))
819
- continue;
820
- steps.push(processSelectedTask(ctx, taskId, prepared.outcomes.get(taskId), true));
821
- }
831
+ }));
822
832
  if (steps.length > 0) {
823
833
  const settledRun = loadRun(ctx);
824
- (0, commit_1.commitState)(settledRun, `concurrent-round:${batch.length}-tasks`);
825
- (0, report_1.writeReport)(settledRun);
826
- (0, run_store_1.saveCheckpoint)(settledRun);
834
+ (0, perf_trace_1.withPerfTraceGroup)("checkpoint", () => (0, commit_1.commitState)(settledRun, `concurrent-round:${batch.length}-tasks`));
835
+ (0, perf_trace_1.withPerfTraceGroup)("report", () => (0, report_1.writeReport)(settledRun));
836
+ (0, perf_trace_1.withPerfTraceGroup)("checkpoint", () => (0, run_store_1.saveCheckpoint)(settledRun));
827
837
  }
828
838
  return steps.length > 0 ? steps : [driveStep(ctx)];
829
839
  });
@@ -918,7 +928,7 @@ function driveOneRound(ctx, options, steps, emitPhaseProgress) {
918
928
  // re-read+re-parse state.json even though nothing on disk changed
919
929
  // between them — reads that only reflect the mutations THIS round's
920
930
  // own steps make, which the shared in-memory object already carries.
921
- const roundSteps = withRoundCache(ctx, () => {
931
+ const roundSteps = (0, perf_trace_1.withPerfTraceGroup)("round", () => withRoundCache(ctx, () => {
922
932
  const width = (0, drive_decide_1.roundWidth)(loadRun(ctx), options.concurrency);
923
933
  // width>1 (an explicit --concurrency>1, or an auto-width parallel
924
934
  // phase) runs the whole round through driveConcurrentRound — one or
@@ -926,7 +936,7 @@ function driveOneRound(ctx, options, steps, emitPhaseProgress) {
926
936
  // end. `--once` still stops after this ONE round even though a round
927
937
  // can yield multiple steps.
928
938
  return width > 1 ? driveConcurrentRound(ctx, width) : [driveStep(ctx)];
929
- });
939
+ }));
930
940
  for (const stepResult of roundSteps)
931
941
  steps.push(stepResult);
932
942
  // Brew-style phase boundaries: after each round, announce a newly-active
@@ -75,6 +75,7 @@ const os = __importStar(require("node:os"));
75
75
  const node_child_process_1 = require("node:child_process");
76
76
  const local_1 = require("./local");
77
77
  const envelopes_1 = require("./envelopes");
78
+ const perf_trace_1 = require("../perf-trace");
78
79
  function messageOf(error) {
79
80
  return error instanceof Error ? error.message : String(error);
80
81
  }
@@ -438,19 +439,21 @@ function reconcileBatchOutcomes(jobs, child) {
438
439
  function runAgentBatchOutcomes(jobs) {
439
440
  if (!jobs.length)
440
441
  return [];
441
- const maxTimeout = Math.max(...jobs.map((job) => job.timeoutMs));
442
- let child;
443
- try {
444
- child = (0, node_child_process_1.spawnSync)(process.execPath, [BATCH_DELEGATE_CHILD_SCRIPT], {
445
- input: JSON.stringify(jobs),
446
- maxBuffer: 34 * 1024 * 1024 * jobs.length,
447
- timeout: maxTimeout + 30000,
448
- });
449
- }
450
- catch (error) {
451
- child = { error: error instanceof Error ? error : new Error(String(error)), status: null, stdout: null };
452
- }
453
- return reconcileBatchOutcomes(jobs, child);
442
+ return (0, perf_trace_1.withPerfTraceGroup)("agent-wait", () => {
443
+ const maxTimeout = Math.max(...jobs.map((job) => job.timeoutMs));
444
+ let child;
445
+ try {
446
+ child = (0, node_child_process_1.spawnSync)(process.execPath, [BATCH_DELEGATE_CHILD_SCRIPT], {
447
+ input: JSON.stringify(jobs),
448
+ maxBuffer: 34 * 1024 * 1024 * jobs.length,
449
+ timeout: maxTimeout + 30000,
450
+ });
451
+ }
452
+ catch (error) {
453
+ child = { error: error instanceof Error ? error : new Error(String(error)), status: null, stdout: null };
454
+ }
455
+ return reconcileBatchOutcomes(jobs, child);
456
+ });
454
457
  }
455
458
  // Same package-root depth fix as BATCH_DELEGATE_CHILD_SCRIPT above.
456
459
  const HTTP_BATCH_DELEGATE_CHILD_SCRIPT = path.resolve(__dirname, "..", "..", "..", "scripts", "children", "http-batch-delegate-child.js");
@@ -530,7 +533,7 @@ function runAgentProcess(descriptor, policy, request, label, attestation) {
530
533
  // of `built.forwarded` / the trust-audit forwarded list.
531
534
  const vendorPidFile = vendorPidFilePath();
532
535
  const childEnv = { ...built.env, CW_AGENT_VENDOR_PIDFILE: vendorPidFile };
533
- const child = (0, node_child_process_1.spawnSync)(resolved.binary, realArgs, {
536
+ const child = (0, perf_trace_1.withPerfTraceGroup)("agent-wait", () => (0, node_child_process_1.spawnSync)(resolved.binary, realArgs, {
534
537
  cwd: request.cwd,
535
538
  env: childEnv,
536
539
  encoding: "utf8",
@@ -544,7 +547,7 @@ function runAgentProcess(descriptor, policy, request, label, attestation) {
544
547
  // anyway — so a hard kill loses nothing and cannot wedge cw.
545
548
  killSignal: "SIGKILL",
546
549
  stdio: ["ignore", "pipe", streamStderr ? "inherit" : "pipe"],
547
- });
550
+ }));
548
551
  // A timeout surfaces as child.error with code ETIMEDOUT (status null,
549
552
  // signal set). Classify it FIRST: without this, the generic spawnError
550
553
  // branch below reported "agent process failed to spawn: ...ETIMEDOUT"
@@ -31,6 +31,7 @@ function messageOf(error) {
31
31
  exports.CW_NEVER_FORWARD_ENV = new Set([
32
32
  "CW_RELEASE_VERDICT_PRIVKEY", // release verdict signing key — release-flow.js only
33
33
  "CW_WORKBENCH_TOKEN", // workbench HTTP bearer token — workbench-host.ts only
34
+ "CW_BENCH_TRACE_FILE", // benchmark trace path — parent drive only
34
35
  ]);
35
36
  function buildChildEnv(policy, baseEnv = process.env) {
36
37
  // deny must win regardless of inherit: a custom profile combining
@@ -53,6 +53,7 @@ exports.nextBackoffMs = nextBackoffMs;
53
53
  exports.withFileLock = withFileLock;
54
54
  const fs = __importStar(require("node:fs"));
55
55
  const path = __importStar(require("node:path"));
56
+ const perf_trace_1 = require("./perf-trace");
56
57
  // ---------------------------------------------------------------------------
57
58
  // writeJson — the ONE JSON-on-disk byte format: JSON.stringify(value, null, 2)
58
59
  // + "\n", written via temp-file + optional fsync + rename (atomic).
@@ -104,6 +105,7 @@ function writeBytesAtomic(file, contents, durable) {
104
105
  catch {
105
106
  /* directory fsync is best-effort (not supported on every platform) */
106
107
  }
108
+ (0, perf_trace_1.recordPerfDurableWrite)(Buffer.byteLength(contents, "utf8"));
107
109
  }
108
110
  }
109
111
  /** Atomic, optionally-durable JSON write — see `writeBytesAtomic`. */
@@ -197,6 +199,7 @@ function durableAppendFileSync(file, data) {
197
199
  finally {
198
200
  fs.closeSync(fd);
199
201
  }
202
+ (0, perf_trace_1.recordPerfDurableWrite)(Buffer.byteLength(data, "utf8"));
200
203
  }
201
204
  /** True when `file`'s final byte is "\n", given its already-known `size`.
202
205
  * A COMPLETED `durableAppendFileSync` always leaves the file ending in
@@ -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. */
@@ -140,7 +140,10 @@ function invocationCwd(args) {
140
140
  return typeof args.cwd === "string" && args.cwd.trim() ? path.resolve(args.cwd) : process.cwd();
141
141
  }
142
142
  function loadRun(args, runId) {
143
- 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;
144
147
  }
145
148
  /** Hold the run's state.json lock across the WHOLE load -> change ->
146
149
  * persist cycle. A bare loadRun + persist pair races: two processes both
@@ -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) {
@@ -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);
@@ -148,7 +148,7 @@ function plan(app, options) {
148
148
  const runId = injectedRunId || createRunId(app.workflow.id);
149
149
  const runDir = path.join(cwd, ".cw", "runs", runId);
150
150
  const paths = (0, run_paths_1.createRunPaths)(runDir);
151
- (0, run_paths_1.ensureRunDirs)(paths);
151
+ (0, run_store_1.ensureRunDirs)(paths);
152
152
  const tasks = flattenTasks(app, inputs);
153
153
  // Stamp a real wall-clock createdAt/updatedAt at plan time (shell layer —
154
154
  // clock reads are allowed here). The pure migration defaults a run that
@@ -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
  }