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.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.gemini-plugin/plugin.json +1 -1
- package/.opencode-plugin/plugin.json +1 -1
- package/README.md +5 -3
- package/apps/architecture-review/app.json +1 -1
- package/apps/architecture-review-fast/app.json +1 -1
- package/apps/end-to-end-golden-path/app.json +1 -1
- package/apps/pr-review-fix-ci/app.json +1 -1
- package/apps/release-cut/app.json +1 -1
- package/apps/research-synthesis/app.json +1 -1
- package/dist/core/state/run-paths.js +3 -30
- package/dist/core/version.js +1 -1
- package/dist/mcp/server.js +95 -27
- package/dist/mcp/tool-process.js +181 -0
- package/dist/mcp-server.js +8 -1
- package/dist/shell/agent-config.js +11 -2
- package/dist/shell/audit-cli.js +1 -1
- package/dist/shell/drive.js +99 -89
- package/dist/shell/execution-backend/agent.js +18 -15
- package/dist/shell/execution-backend/local.js +1 -0
- package/dist/shell/fs-atomic.js +3 -0
- package/dist/shell/metrics-cli.js +1 -1
- package/dist/shell/multi-agent-cli.js +4 -1
- package/dist/shell/multi-agent-operator-ux.js +1 -1
- package/dist/shell/observability.js +7 -4
- package/dist/shell/operator-ux.js +1 -1
- package/dist/shell/perf-trace.js +136 -0
- package/dist/shell/pipeline.js +1 -1
- package/dist/shell/report-view-cli.js +2 -0
- package/dist/shell/run-export-cli.js +5 -2
- package/dist/shell/run-export.js +241 -27
- package/dist/shell/run-registry-io.js +8 -0
- package/dist/shell/run-store.js +27 -3
- package/dist/shell/state-explosion-cli.js +4 -1
- package/dist/shell/telemetry-demo.js +1 -1
- package/dist/shell/trust-audit.js +91 -43
- package/dist/shell/workbench.js +4 -1
- package/dist/wiring/capability-table/parity.js +10 -5
- package/docs/agent-delegation-drive.7.md +2 -0
- package/docs/cli-mcp-parity.7.md +21 -5
- package/docs/contract-migration-tooling.7.md +2 -0
- package/docs/control-plane-scheduling.7.md +2 -0
- package/docs/durable-state-and-locking.7.md +2 -0
- package/docs/evidence-adoption-reasoning-chain.7.md +2 -0
- package/docs/execution-backends.7.md +2 -0
- package/docs/mcp-app-surface.7.md +21 -0
- package/docs/multi-agent-cli-mcp-surface.7.md +2 -0
- package/docs/multi-agent-eval-replay-harness.7.md +2 -0
- package/docs/multi-agent-operator-ux.7.md +2 -0
- package/docs/node-snapshot-diff-replay.7.md +2 -0
- package/docs/observability-cost-accounting.7.md +2 -0
- package/docs/project-index.md +18 -4
- package/docs/real-execution-backends.7.md +2 -0
- package/docs/release-and-migration.7.md +2 -0
- package/docs/release-tooling.7.md +10 -0
- package/docs/run-registry-control-plane.7.md +35 -9
- package/docs/run-retention-reclamation.7.md +2 -0
- package/docs/state-explosion-management.7.md +2 -0
- package/docs/team-collaboration.7.md +2 -0
- package/docs/trust-audit-anchor.7.md +2 -0
- package/docs/web-desktop-workbench.7.md +48 -0
- package/manifest/plugin.manifest.json +1 -1
- package/package.json +3 -2
- package/scripts/canonical-apps.js +4 -4
- package/scripts/dogfood-release.js +1 -1
- package/scripts/gen-manifests.js +32 -61
- package/scripts/golden-path.js +4 -4
- package/scripts/parity-check.js +42 -23
- package/scripts/purity-baseline.json +0 -3
- package/scripts/release-flow.js +43 -13
- package/scripts/release-gate.js +30 -6
- package/scripts/schema-version-inventory.json +31 -0
- package/scripts/validate-run-state-schema.js +95 -4
- package/scripts/verify-release-verdict.js +139 -0
- package/ui/workbench/app.css +15 -7
- package/ui/workbench/app.js +118 -16
- package/ui/workbench/index.html +2 -0
- package/ui/workbench/inspection.js +51 -0
- package/ui/workbench/navigation.js +44 -0
package/dist/shell/drive.js
CHANGED
|
@@ -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
|
-
|
|
121
|
-
exports.
|
|
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 >
|
|
437
|
-
return handleHop(ctx, selected, workerId, `sub-workflow depth limit exceeded (> ${
|
|
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
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
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
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
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
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
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
|
-
|
|
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
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
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
|
package/dist/shell/fs-atomic.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
457
|
-
*
|
|
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
|
-
|
|
462
|
-
|
|
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);
|
package/dist/shell/pipeline.js
CHANGED
|
@@ -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,
|
|
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
|
|
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:
|
|
108
|
+
return { schemaVersion: 1, ok: true, target, inspect, imported: restored.imported.run, verify: restored.verification, registry };
|
|
106
109
|
}
|