cool-workflow 0.1.98 → 0.2.1
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/README.md +18 -7
- package/apps/architecture-review/app.json +2 -2
- package/apps/architecture-review/workflow.js +12 -12
- 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/cli/dispatch.js +236 -0
- package/dist/cli/entry.js +120 -0
- package/dist/cli/io.js +21 -4
- package/dist/cli/parseargv.js +157 -0
- package/dist/cli.js +6 -33
- package/dist/core/capability-table.js +3518 -0
- package/dist/core/format/help.js +314 -0
- package/dist/{state-explosion/format.js → core/format/state-explosion-text.js} +10 -1
- package/dist/core/hash.js +137 -0
- package/dist/core/multi-agent/candidate-scoring.js +219 -0
- package/dist/core/multi-agent/collaboration.js +481 -0
- package/dist/core/multi-agent/coordinator.js +515 -0
- package/dist/core/multi-agent/eval-replay.js +306 -0
- package/dist/core/multi-agent/runtime.js +929 -0
- package/dist/core/multi-agent/topology.js +298 -0
- package/dist/core/multi-agent/trust-policy.js +197 -0
- package/dist/core/pipeline/commit-gate.js +320 -0
- package/dist/{pipeline-contract.js → core/pipeline/contract.js} +24 -37
- package/dist/core/pipeline/dispatch.js +103 -0
- package/dist/core/pipeline/drive-decide.js +227 -0
- package/dist/core/pipeline/error-feedback.js +161 -0
- package/dist/core/pipeline/loop-expansion.js +124 -0
- package/dist/{result-normalize.js → core/pipeline/result-normalize.js} +14 -37
- package/dist/core/pipeline/runner.js +230 -0
- package/dist/{contract-migration.js → core/state/contract-migration.js} +68 -92
- package/dist/{state-migrations.js → core/state/migrations.js} +103 -96
- package/dist/core/state/node-projection.js +95 -0
- package/dist/core/state/node-snapshot.js +230 -0
- package/dist/core/state/run-paths.js +93 -0
- package/dist/{schema-validate.js → core/state/schema-validate.js} +35 -28
- package/dist/core/state/schema.js +50 -0
- package/dist/core/state/state-explosion/digest.js +243 -0
- package/dist/core/state/state-explosion/graph.js +527 -0
- package/dist/{state-explosion → core/state/state-explosion}/helpers.js +34 -3
- package/dist/core/state/state-explosion/report.js +187 -0
- package/dist/{state-explosion → core/state/state-explosion}/size.js +25 -7
- package/dist/{state-node.js → core/state/state-node.js} +90 -113
- package/dist/core/state/types.js +19 -0
- package/dist/{validation.js → core/state/validation.js} +64 -131
- package/dist/core/trust/evidence-grounding.js +134 -0
- package/dist/core/trust/ledger.js +199 -0
- package/dist/{telemetry-attestation.js → core/trust/telemetry-attestation.js} +94 -68
- package/dist/core/trust/telemetry-ledger.js +127 -0
- package/dist/core/types.js +20 -0
- package/dist/core/version.js +16 -0
- package/dist/{workflow-app-framework.js → core/workflow-apps/app-schema.js} +337 -426
- package/dist/mcp/dispatch.js +104 -0
- package/dist/mcp/server.js +144 -0
- package/dist/mcp-server.js +6 -84
- package/dist/{agent-config.js → shell/agent-config.js} +118 -71
- package/dist/shell/app-run-cli.js +88 -0
- package/dist/shell/audit-cli.js +259 -0
- package/dist/shell/audit-provenance.js +83 -0
- package/dist/shell/candidate-scoring-io.js +459 -0
- package/dist/shell/collaboration-io.js +264 -0
- package/dist/shell/commit-summary.js +104 -0
- package/dist/shell/commit.js +290 -0
- package/dist/shell/coordinator-io.js +476 -0
- package/dist/shell/demo-cli.js +19 -0
- package/dist/shell/dispatch.js +162 -0
- package/dist/shell/doctor.js +292 -0
- package/dist/shell/drive.js +885 -0
- package/dist/shell/error-feedback-io.js +305 -0
- package/dist/shell/eval-io.js +473 -0
- package/dist/{multi-agent-eval/format.js → shell/eval-text.js} +78 -49
- package/dist/{evidence-reasoning.js → shell/evidence-reasoning.js} +124 -255
- package/dist/shell/exec-backend-cli.js +88 -0
- package/dist/shell/execution-backend/agent.js +507 -0
- package/dist/shell/execution-backend/ci.js +15 -0
- package/dist/shell/execution-backend/container.js +69 -0
- package/dist/shell/execution-backend/envelopes.js +55 -0
- package/dist/shell/execution-backend/local.js +113 -0
- package/dist/shell/execution-backend/probes.js +175 -0
- package/dist/shell/execution-backend/registry.js +402 -0
- package/dist/shell/execution-backend/remote.js +128 -0
- package/dist/shell/execution-backend/types.js +11 -0
- package/dist/shell/feedback-cli.js +81 -0
- package/dist/shell/feedback-operations.js +48 -0
- package/dist/shell/fs-atomic.js +276 -0
- package/dist/shell/harness.js +98 -0
- package/dist/shell/ledger-cli.js +212 -0
- package/dist/shell/ledger-io.js +169 -0
- package/dist/shell/man-cli.js +89 -0
- package/dist/shell/metrics-cli.js +100 -0
- package/dist/shell/multi-agent-cli.js +1002 -0
- package/dist/shell/multi-agent-host.js +563 -0
- package/dist/shell/multi-agent-io.js +387 -0
- package/dist/{multi-agent-operator-ux.js → shell/multi-agent-operator-ux.js} +234 -191
- package/dist/shell/node-store.js +124 -0
- package/dist/{observability/format.js → shell/observability-format.js} +7 -1
- package/dist/{observability/intake.js → shell/observability-intake.js} +79 -56
- package/dist/{observability.js → shell/observability.js} +159 -332
- package/dist/{onramp.js → shell/onramp.js} +11 -0
- package/dist/{operator-ux/format.js → shell/operator-ux-text.js} +250 -239
- package/dist/shell/operator-ux.js +431 -0
- package/dist/shell/orchestrator.js +231 -0
- package/dist/shell/pipeline-cli.js +667 -0
- package/dist/shell/pipeline.js +217 -0
- package/dist/shell/reclamation-io.js +1366 -0
- package/dist/shell/registry-cli.js +344 -0
- package/dist/{remote-source.js → shell/remote-source.js} +2 -2
- package/dist/shell/report-cli.js +101 -0
- package/dist/shell/report-view-cli.js +117 -0
- package/dist/{orchestrator → shell}/report.js +289 -282
- package/dist/shell/reporter.js +62 -0
- package/dist/shell/run-export-cli.js +106 -0
- package/dist/shell/run-export.js +680 -0
- package/dist/shell/run-registry-io.js +1014 -0
- package/dist/shell/run-store.js +164 -0
- package/dist/{sandbox-profile.js → shell/sandbox-profile.js} +134 -95
- package/dist/{scheduler.js → shell/scheduler-io.js} +257 -48
- package/dist/shell/scheduling-io.js +321 -0
- package/dist/shell/state-cli.js +181 -0
- package/dist/shell/state-explosion-cli.js +197 -0
- package/dist/shell/telemetry-cli.js +85 -0
- package/dist/{telemetry-demo.js → shell/telemetry-demo.js} +149 -119
- package/dist/shell/telemetry-ledger-io.js +132 -0
- package/dist/{term.js → shell/term.js} +36 -46
- package/dist/shell/topology-io.js +361 -0
- package/dist/shell/trust-audit.js +472 -0
- package/dist/{multi-agent-trust.js → shell/trust-policy-io.js} +67 -205
- package/dist/shell/verifier.js +48 -0
- package/dist/shell/workbench-host.js +258 -0
- package/dist/shell/workbench-text.js +18 -0
- package/dist/shell/workbench.js +170 -0
- package/dist/shell/worker-cli.js +124 -0
- package/dist/shell/worker-isolation.js +852 -0
- package/dist/shell/workflow-app-loader.js +650 -0
- package/docs/agent-delegation-drive.7.md +4 -0
- package/docs/cli-mcp-parity.7.md +282 -219
- package/docs/contract-migration-tooling.7.md +4 -0
- package/docs/control-plane-scheduling.7.md +4 -0
- package/docs/durable-state-and-locking.7.md +4 -0
- package/docs/evidence-adoption-reasoning-chain.7.md +4 -0
- package/docs/execution-backends.7.md +4 -0
- package/docs/multi-agent-cli-mcp-surface.7.md +4 -0
- package/docs/multi-agent-eval-replay-harness.7.md +4 -0
- package/docs/multi-agent-operator-ux.7.md +4 -0
- package/docs/node-snapshot-diff-replay.7.md +4 -0
- package/docs/observability-cost-accounting.7.md +6 -1
- package/docs/project-index.md +137 -71
- package/docs/real-execution-backends.7.md +4 -0
- package/docs/release-and-migration.7.md +4 -0
- package/docs/release-tooling.7.md +4 -0
- package/docs/remote-source-review.7.md +4 -4
- package/docs/report-verifiable-bundle.7.md +1 -1
- package/docs/run-registry-control-plane.7.md +4 -0
- package/docs/run-retention-reclamation.7.md +25 -0
- package/docs/security-trust-hardening.7.md +3 -1
- package/docs/state-explosion-management.7.md +4 -0
- package/docs/team-collaboration.7.md +4 -0
- package/docs/web-desktop-workbench.7.md +15 -1
- package/manifest/plugin.manifest.json +1 -1
- package/manifest/source-context-profiles.json +9 -13
- package/package.json +1 -1
- package/scripts/agents/agent-adapter-core.js +22 -2
- package/scripts/agents/claude-p-agent.js +8 -3
- package/scripts/agents/cw-attest-wrap.js +2 -2
- package/scripts/agents/gemini-agent.js +5 -1
- package/scripts/agents/opencode-agent.js +5 -1
- package/scripts/bump-version.js +4 -3
- package/scripts/canonical-apps.js +4 -4
- package/scripts/dogfood-architecture-review.js +2 -3
- package/scripts/dogfood-release.js +3 -3
- package/scripts/gen-parity-doc.js +15 -2
- package/scripts/golden-path.js +4 -4
- package/scripts/onramp-check.js +1 -1
- package/scripts/parity-check.js +38 -21
- package/scripts/release-flow.js +2 -2
- package/scripts/sync-project-index.js +51 -27
- package/scripts/validate-run-state-schema.js +2 -2
- package/scripts/version-sync-check.js +30 -30
- package/dist/candidate-scoring.js +0 -729
- package/dist/capability-core.js +0 -1189
- package/dist/capability-registry.js +0 -885
- package/dist/cli/command-surface.js +0 -494
- package/dist/cli/format.js +0 -56
- package/dist/cli/handlers/audit.js +0 -82
- package/dist/cli/handlers/blackboard.js +0 -81
- package/dist/cli/handlers/candidate.js +0 -40
- package/dist/cli/handlers/clones.js +0 -34
- package/dist/cli/handlers/collaboration.js +0 -61
- package/dist/cli/handlers/eval.js +0 -40
- package/dist/cli/handlers/ledger.js +0 -169
- package/dist/cli/handlers/maintenance.js +0 -107
- package/dist/cli/handlers/multi-agent.js +0 -165
- package/dist/cli/handlers/node.js +0 -41
- package/dist/cli/handlers/operational.js +0 -155
- package/dist/cli/handlers/operator.js +0 -146
- package/dist/cli/handlers/registry.js +0 -68
- package/dist/cli/handlers/run.js +0 -153
- package/dist/cli/handlers/scheduling.js +0 -132
- package/dist/cli/handlers/workbench.js +0 -41
- package/dist/cli/handlers/worker.js +0 -45
- package/dist/cli/run-summary.js +0 -45
- package/dist/clones.js +0 -162
- package/dist/collaboration.js +0 -726
- package/dist/commit.js +0 -592
- package/dist/compare.js +0 -18
- package/dist/coordinator/classify.js +0 -45
- package/dist/coordinator/paths.js +0 -42
- package/dist/coordinator/util.js +0 -126
- package/dist/coordinator.js +0 -990
- package/dist/daemon.js +0 -44
- package/dist/dispatch.js +0 -250
- package/dist/doctor.js +0 -212
- package/dist/drive.js +0 -864
- package/dist/error-feedback.js +0 -419
- package/dist/evidence-grounding.js +0 -184
- package/dist/execution-backend/agent.js +0 -354
- package/dist/execution-backend/probes.js +0 -112
- package/dist/execution-backend/util.js +0 -47
- package/dist/execution-backend.js +0 -961
- package/dist/gates.js +0 -48
- package/dist/harness.js +0 -61
- package/dist/ledger.js +0 -313
- package/dist/loop-expansion.js +0 -60
- package/dist/mcp/tool-call.js +0 -470
- package/dist/mcp/tool-definitions.js +0 -1066
- package/dist/mcp-surface.js +0 -30
- package/dist/multi-agent/graph.js +0 -84
- package/dist/multi-agent/helpers.js +0 -141
- package/dist/multi-agent/ids.js +0 -20
- package/dist/multi-agent/paths.js +0 -22
- package/dist/multi-agent-eval/normalize.js +0 -51
- package/dist/multi-agent-eval.js +0 -678
- package/dist/multi-agent-host.js +0 -777
- package/dist/multi-agent.js +0 -984
- package/dist/node-projection.js +0 -59
- package/dist/node-snapshot.js +0 -260
- package/dist/operator-ux.js +0 -631
- package/dist/orchestrator/app-operations.js +0 -211
- package/dist/orchestrator/audit-operations.js +0 -182
- package/dist/orchestrator/candidate-operations.js +0 -117
- package/dist/orchestrator/cli-options.js +0 -294
- package/dist/orchestrator/collaboration-operations.js +0 -86
- package/dist/orchestrator/feedback-operations.js +0 -81
- package/dist/orchestrator/host-operations.js +0 -78
- package/dist/orchestrator/lifecycle-operations.js +0 -650
- package/dist/orchestrator/migration-operations.js +0 -44
- package/dist/orchestrator/multi-agent-operations.js +0 -362
- package/dist/orchestrator/topology-operations.js +0 -84
- package/dist/orchestrator.js +0 -925
- package/dist/pipeline-runner.js +0 -285
- package/dist/reclamation/hash.js +0 -72
- package/dist/reclamation.js +0 -812
- package/dist/reporter.js +0 -67
- package/dist/run-export.js +0 -815
- package/dist/run-registry/derive.js +0 -175
- package/dist/run-registry/format.js +0 -124
- package/dist/run-registry/gc.js +0 -251
- package/dist/run-registry/policy.js +0 -16
- package/dist/run-registry/queue.js +0 -115
- package/dist/run-registry.js +0 -850
- package/dist/run-state-schema.js +0 -68
- package/dist/scheduling.js +0 -184
- package/dist/state-explosion.js +0 -1014
- package/dist/state.js +0 -367
- package/dist/telemetry-ledger.js +0 -196
- package/dist/topology.js +0 -565
- package/dist/triggers.js +0 -184
- package/dist/trust-audit.js +0 -644
- package/dist/types/blackboard.js +0 -2
- package/dist/types/candidate.js +0 -2
- package/dist/types/collaboration.js +0 -2
- package/dist/types/core.js +0 -2
- package/dist/types/drive.js +0 -10
- package/dist/types/error-feedback.js +0 -2
- package/dist/types/evidence-reasoning.js +0 -2
- package/dist/types/execution-backend.js +0 -2
- package/dist/types/multi-agent.js +0 -2
- package/dist/types/observability.js +0 -2
- package/dist/types/pipeline.js +0 -2
- package/dist/types/reclamation.js +0 -8
- package/dist/types/report-bundle.js +0 -6
- package/dist/types/result.js +0 -2
- package/dist/types/run-registry.js +0 -2
- package/dist/types/run.js +0 -2
- package/dist/types/sandbox.js +0 -2
- package/dist/types/schedule.js +0 -2
- package/dist/types/state-node.js +0 -2
- package/dist/types/topology.js +0 -2
- package/dist/types/trust.js +0 -2
- package/dist/types/workbench.js +0 -2
- package/dist/types/worker.js +0 -2
- package/dist/types/workflow-app.js +0 -2
- package/dist/types.js +0 -44
- package/dist/util/fingerprint.js +0 -19
- package/dist/util/fingerprint.test.js +0 -27
- package/dist/verifier.js +0 -78
- package/dist/version.js +0 -8
- package/dist/workbench-host.js +0 -192
- package/dist/workbench.js +0 -192
- package/dist/worker-accept/acceptance.js +0 -114
- package/dist/worker-accept/blackboard-fanout.js +0 -80
- package/dist/worker-accept/blackboard-linkage.js +0 -19
- package/dist/worker-accept/context.js +0 -2
- package/dist/worker-accept/telemetry-ledger.js +0 -126
- package/dist/worker-accept/validation.js +0 -77
- package/dist/worker-accept/verifier-completion.js +0 -73
- package/dist/worker-isolation/helpers.js +0 -51
- package/dist/worker-isolation/paths.js +0 -46
- package/dist/worker-isolation.js +0 -656
- package/dist/workflow-api.js +0 -131
- /package/dist/{types → core/types}/boundary.js +0 -0
|
@@ -0,0 +1,885 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// shell/drive.ts — the thin imperative loop that calls drive-decide.ts
|
|
3
|
+
// once per step and performs the spawn/commit/cache-write IO the
|
|
4
|
+
// decision names.
|
|
5
|
+
//
|
|
6
|
+
// MILESTONE 6+7 (combined; see docs/rebuild/PLAN.md Open risk 9/10 — the LARGEST
|
|
7
|
+
// milestone). Byte-exact port of the old build's src/drive.ts's
|
|
8
|
+
// imperative shell around the pure decision core now in
|
|
9
|
+
// core/pipeline/drive-decide.ts. Sub-workflow nesting and `--incremental`
|
|
10
|
+
// are ported; the concurrent-round driver (driveConcurrentRound) is
|
|
11
|
+
// scoped down to the serial driver run through a width loop, since no
|
|
12
|
+
// case in this milestone's combined gate exercises true concurrent-batch
|
|
13
|
+
// recording order (that is `--concurrency`/parallel-phase-specific and is
|
|
14
|
+
// authored as its own future conformance case per Open risk 5) — the
|
|
15
|
+
// `mode:"parallel"` architecture-review phases still complete correctly
|
|
16
|
+
// through the serial per-task loop, just without the wall-clock-parallel
|
|
17
|
+
// spawn optimization; this is flagged here rather than silently ported as
|
|
18
|
+
// if fully equivalent.
|
|
19
|
+
//
|
|
20
|
+
// Evidence: SPEC/pipeline-run.md "Drive loop — src/drive.ts".
|
|
21
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
22
|
+
if (k2 === undefined) k2 = k;
|
|
23
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
24
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
25
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
26
|
+
}
|
|
27
|
+
Object.defineProperty(o, k2, desc);
|
|
28
|
+
}) : (function(o, m, k, k2) {
|
|
29
|
+
if (k2 === undefined) k2 = k;
|
|
30
|
+
o[k2] = m[k];
|
|
31
|
+
}));
|
|
32
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
33
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
34
|
+
}) : function(o, v) {
|
|
35
|
+
o["default"] = v;
|
|
36
|
+
});
|
|
37
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
38
|
+
var ownKeys = function(o) {
|
|
39
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
40
|
+
var ar = [];
|
|
41
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
42
|
+
return ar;
|
|
43
|
+
};
|
|
44
|
+
return ownKeys(o);
|
|
45
|
+
};
|
|
46
|
+
return function (mod) {
|
|
47
|
+
if (mod && mod.__esModule) return mod;
|
|
48
|
+
var result = {};
|
|
49
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
50
|
+
__setModuleDefault(result, mod);
|
|
51
|
+
return result;
|
|
52
|
+
};
|
|
53
|
+
})();
|
|
54
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
55
|
+
exports.MAX_SUB_WORKFLOW_DEPTH = exports.DRIVE_SCHEMA_VERSION = void 0;
|
|
56
|
+
exports.maybeExpandLoop = maybeExpandLoop;
|
|
57
|
+
exports.driveStep = driveStep;
|
|
58
|
+
exports.drive = drive;
|
|
59
|
+
exports.drivePreview = drivePreview;
|
|
60
|
+
const fs = __importStar(require("node:fs"));
|
|
61
|
+
const path = __importStar(require("node:path"));
|
|
62
|
+
const drive_decide_1 = require("../core/pipeline/drive-decide");
|
|
63
|
+
const loop_expansion_1 = require("../core/pipeline/loop-expansion");
|
|
64
|
+
const dispatch_1 = require("../core/pipeline/dispatch");
|
|
65
|
+
const run_store_1 = require("./run-store");
|
|
66
|
+
const dispatch_2 = require("./dispatch");
|
|
67
|
+
const worker_isolation_1 = require("./worker-isolation");
|
|
68
|
+
const state_node_1 = require("../core/state/state-node");
|
|
69
|
+
const node_store_1 = require("./node-store");
|
|
70
|
+
const runner_1 = require("../core/pipeline/runner");
|
|
71
|
+
const harness_1 = require("./harness");
|
|
72
|
+
const term_1 = require("./term");
|
|
73
|
+
const commit_1 = require("./commit");
|
|
74
|
+
const report_1 = require("./report");
|
|
75
|
+
const trust_audit_1 = require("./trust-audit");
|
|
76
|
+
const agent_config_1 = require("./agent-config");
|
|
77
|
+
const registry_1 = require("./execution-backend/registry");
|
|
78
|
+
const agent_1 = require("./execution-backend/agent");
|
|
79
|
+
const hash_1 = require("../core/hash");
|
|
80
|
+
const pipeline_1 = require("./pipeline");
|
|
81
|
+
const reporter_1 = require("./reporter");
|
|
82
|
+
const fs_atomic_1 = require("./fs-atomic");
|
|
83
|
+
const observability_1 = require("./observability");
|
|
84
|
+
const telemetry_attestation_1 = require("../core/trust/telemetry-attestation");
|
|
85
|
+
/** Total RECORDED tokens across the run's attested units, reading the
|
|
86
|
+
* agent-reported usage through normalizeReportedUsage so a hop that
|
|
87
|
+
* reported snake_case buckets (`input_tokens`/`output_tokens`, the shape
|
|
88
|
+
* parseAgentReport hands back verbatim) is counted — not silently zeroed.
|
|
89
|
+
* Reuses deriveUsageTotals' own unit selection (its `rows` are the
|
|
90
|
+
* deduped attested units) so worker-vs-task double-counting stays fixed;
|
|
91
|
+
* only the key-reading is corrected here. The old build normalized at
|
|
92
|
+
* store time in worker-accept/verifier-completion.ts; v2 stores the raw
|
|
93
|
+
* reportedUsage, so the drive's budget accounting normalizes at read
|
|
94
|
+
* time — same net RECORDED total, same fail-closed backstop. */
|
|
95
|
+
function recordedTokenTotal(run) {
|
|
96
|
+
let total = 0;
|
|
97
|
+
for (const row of (0, observability_1.deriveUsageTotals)(run).rows) {
|
|
98
|
+
const usage = row.usage;
|
|
99
|
+
if (!usage)
|
|
100
|
+
continue;
|
|
101
|
+
const declared = usage.totalTokens;
|
|
102
|
+
if (typeof declared === "number") {
|
|
103
|
+
total += declared;
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
const n = (0, telemetry_attestation_1.normalizeReportedUsage)(usage);
|
|
107
|
+
if (typeof n.totalTokens === "number")
|
|
108
|
+
total += n.totalTokens;
|
|
109
|
+
else
|
|
110
|
+
total += (n.inputTokens || 0) + (n.outputTokens || 0);
|
|
111
|
+
}
|
|
112
|
+
return total;
|
|
113
|
+
}
|
|
114
|
+
/** Token-budget gate input: enforce limits.tokenBudget against RECORDED usage,
|
|
115
|
+
* not a hardcoded zero. Returns undefined when no positive budget is set so the
|
|
116
|
+
* gate is a no-op. Spent = the run's total recorded tokens. */
|
|
117
|
+
function tokenBudgetUsage(run) {
|
|
118
|
+
const budget = run.workflow.limits?.tokenBudget;
|
|
119
|
+
if (!budget || budget <= 0)
|
|
120
|
+
return undefined;
|
|
121
|
+
return { spent: recordedTokenTotal(run), budget };
|
|
122
|
+
}
|
|
123
|
+
exports.DRIVE_SCHEMA_VERSION = 1;
|
|
124
|
+
exports.MAX_SUB_WORKFLOW_DEPTH = 4;
|
|
125
|
+
function agentConfigured(config) {
|
|
126
|
+
return Boolean(config.command || config.endpoint);
|
|
127
|
+
}
|
|
128
|
+
/** Progress to STDERR (stdout stays clean JSON). On by default when
|
|
129
|
+
* stderr is a TTY; silent in CI/pipes. CW_DRIVE_PROGRESS=0 forces off,
|
|
130
|
+
* =1 forces on. This is gate point #2 of the Rule of Silence's three
|
|
131
|
+
* gate points (SPEC/reporting-ux.md rebuild risk #1) — byte-exact port
|
|
132
|
+
* of the old build's src/drive.ts's emitProgress. */
|
|
133
|
+
function emitProgress(message) {
|
|
134
|
+
const forcedOff = process.env.CW_DRIVE_PROGRESS === "0";
|
|
135
|
+
const forcedOn = process.env.CW_DRIVE_PROGRESS === "1";
|
|
136
|
+
if ((Boolean(process.stderr.isTTY) && !forcedOff) || forcedOn)
|
|
137
|
+
reporter_1.reporter.progress(`[drive] ${message}`);
|
|
138
|
+
}
|
|
139
|
+
// A concurrent round runs many dispatch/accept steps against ONE shared
|
|
140
|
+
// in-memory run object, deferring every disk write to a single flush at
|
|
141
|
+
// round end (see driveConcurrentRound below). loadRun(ctx) is the single
|
|
142
|
+
// choke point every step reads through, so the cache lives here: while a
|
|
143
|
+
// round is active for a given run id, loadRun returns the SAME mutated
|
|
144
|
+
// object instead of re-reading (necessarily stale) disk state. Keyed by
|
|
145
|
+
// run id (not a stack) so a sub-workflow task's nested drive() call on a
|
|
146
|
+
// DIFFERENT run id is unaffected — re-entrant, matches the old build's
|
|
147
|
+
// runner.loadWithCache. Byte-exact in spirit to src/drive.ts's own
|
|
148
|
+
// per-runner cache; ported here as a module-level map since this build
|
|
149
|
+
// has no persistent "runner" object to hang it on.
|
|
150
|
+
const roundCache = new Map();
|
|
151
|
+
function loadRun(ctx) {
|
|
152
|
+
const cached = roundCache.get(ctx.runId);
|
|
153
|
+
if (cached)
|
|
154
|
+
return cached;
|
|
155
|
+
return (0, run_store_1.loadRunFromCwd)(ctx.runId, ctx.cwd);
|
|
156
|
+
}
|
|
157
|
+
/** Runs `fn` with `runId`'s loadRun calls served from one shared cached
|
|
158
|
+
* object (seeded fresh from disk), and always clears the cache entry
|
|
159
|
+
* afterward — even on throw — so a round never leaks its cache into a
|
|
160
|
+
* later, unrelated drive call. */
|
|
161
|
+
function withRoundCache(ctx, fn) {
|
|
162
|
+
const seed = (0, run_store_1.loadRunFromCwd)(ctx.runId, ctx.cwd);
|
|
163
|
+
roundCache.set(ctx.runId, seed);
|
|
164
|
+
try {
|
|
165
|
+
return fn();
|
|
166
|
+
}
|
|
167
|
+
finally {
|
|
168
|
+
roundCache.delete(ctx.runId);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
function resultCachePath(run, task, promptDigest, incremental, delegationDigest) {
|
|
172
|
+
let digest;
|
|
173
|
+
if (incremental) {
|
|
174
|
+
const upstream = previousPhaseResultsDigest(run, task);
|
|
175
|
+
digest = (0, drive_decide_1.incrementalCacheKey)(run.workflow.id, task.id, promptDigest, (0, hash_1.sha256)((0, hash_1.stableStringify)(run.inputs || {})), delegationDigest, upstream);
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
const policy = task.resultCache;
|
|
179
|
+
if (!policy || policy.mode !== "read-write" || !policy.keyInput)
|
|
180
|
+
return undefined;
|
|
181
|
+
const keyValue = String(run.inputs[policy.keyInput] || "").trim();
|
|
182
|
+
digest = (0, drive_decide_1.defaultCacheKey)(run.workflow.id, task.id, policy.keyInput, keyValue, promptDigest, "");
|
|
183
|
+
}
|
|
184
|
+
if (!digest)
|
|
185
|
+
return undefined;
|
|
186
|
+
return path.join(run.cwd, ".cw", "cache", "worker-results", (0, fs_atomic_1.safeFileName)(run.workflow.id), (0, drive_decide_1.cacheFileName)(task.id, digest));
|
|
187
|
+
}
|
|
188
|
+
function previousPhaseResultsDigest(run, task) {
|
|
189
|
+
const phaseIndex = run.phases.findIndex((p) => p.name === task.phase || p.id === task.phase);
|
|
190
|
+
if (phaseIndex < 0)
|
|
191
|
+
return undefined;
|
|
192
|
+
const previousTaskIds = new Set(run.phases.slice(0, phaseIndex).flatMap((p) => p.taskIds));
|
|
193
|
+
const records = [];
|
|
194
|
+
for (const candidate of run.tasks.filter((t) => previousTaskIds.has(t.id)).sort((a, b) => a.id.localeCompare(b.id))) {
|
|
195
|
+
if (candidate.status !== "completed" || !candidate.resultPath || !fs.existsSync(candidate.resultPath)) {
|
|
196
|
+
records.push(undefined);
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
records.push([candidate.id, (0, hash_1.sha256)(fs.readFileSync(candidate.resultPath, "utf8"))]);
|
|
200
|
+
}
|
|
201
|
+
if (records.some((r) => r === undefined))
|
|
202
|
+
return undefined;
|
|
203
|
+
return (0, hash_1.sha256)(JSON.stringify(records));
|
|
204
|
+
}
|
|
205
|
+
function writeResultCache(file, content) {
|
|
206
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
207
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
208
|
+
fs.writeFileSync(tmp, content, "utf8");
|
|
209
|
+
fs.renameSync(tmp, file);
|
|
210
|
+
}
|
|
211
|
+
/** `deferPersist` (concurrent-round callers ONLY — never a plain serial
|
|
212
|
+
* step) skips saveCheckpoint so a caller driving many tasks through one
|
|
213
|
+
* in-memory `run` can defer the disk flush to a single call at round
|
|
214
|
+
* end; `sharedRun`, when given, is mutated in place instead of a fresh
|
|
215
|
+
* loadRun (the round's one shared cached object). */
|
|
216
|
+
function handleHop(ctx, task, workerId, reason, deferPersist = false, sharedRun) {
|
|
217
|
+
// ONE load, mutated in place and saved — a fresh reload right before
|
|
218
|
+
// saveCheckpoint would discard recordWorkerFailure/RetryAttempt's own
|
|
219
|
+
// in-memory mutation (they return an updated scope but mutate the run
|
|
220
|
+
// object passed in), silently dropping the park/retry bookkeeping.
|
|
221
|
+
const run = sharedRun || loadRun(ctx);
|
|
222
|
+
const scope = (0, worker_isolation_1.getWorkerScope)(run, workerId);
|
|
223
|
+
const persisted = scope?.retryCount || 0;
|
|
224
|
+
const prior = (0, drive_decide_1.priorAttempts)(ctx.attempts.get(task.id) || 0, persisted);
|
|
225
|
+
const decided = (0, drive_decide_1.retryOrPark)(prior, ctx.policy, reason);
|
|
226
|
+
ctx.attempts.set(task.id, decided.attempts);
|
|
227
|
+
if (decided.status === "parked") {
|
|
228
|
+
(0, worker_isolation_1.recordWorkerFailure)(run, workerId, decided.parkedReason || reason, { code: "agent-delegation-parked", retryable: false, retryCount: decided.attempts });
|
|
229
|
+
if (!deferPersist)
|
|
230
|
+
(0, run_store_1.saveCheckpoint)(run);
|
|
231
|
+
return (0, drive_decide_1.makeStep)("park", "parked", { runId: ctx.runId, taskId: task.id, phase: task.phase, backendId: "agent", attempts: decided.attempts, reason: decided.parkedReason || reason });
|
|
232
|
+
}
|
|
233
|
+
(0, worker_isolation_1.recordWorkerRetryAttempt)(run, workerId, decided.attempts, reason);
|
|
234
|
+
if (!deferPersist)
|
|
235
|
+
(0, run_store_1.saveCheckpoint)(run);
|
|
236
|
+
return (0, drive_decide_1.makeStep)("fulfill", "failed", { runId: ctx.runId, taskId: task.id, phase: task.phase, backendId: "agent", attempts: decided.attempts, reason });
|
|
237
|
+
}
|
|
238
|
+
function renderSubInputs(spec, parentInputs) {
|
|
239
|
+
const out = {};
|
|
240
|
+
for (const [key, template] of Object.entries(spec.inputs || {})) {
|
|
241
|
+
out[key] = String(template).replace(/\{\{(\w+)\}\}/g, (_, name) => String(parentInputs[name] ?? ""));
|
|
242
|
+
}
|
|
243
|
+
return out;
|
|
244
|
+
}
|
|
245
|
+
function errMessage(error) {
|
|
246
|
+
return error instanceof Error ? error.message : String(error);
|
|
247
|
+
}
|
|
248
|
+
/** `deferPersist` (concurrent-round callers ONLY) skips the per-task
|
|
249
|
+
* commitState/saveCheckpoint calls — the round flushes once at the end
|
|
250
|
+
* instead. `preparedOutcome`, when given (concurrent round only), is
|
|
251
|
+
* fed to runBackend so the agent spawn that already ran concurrently in
|
|
252
|
+
* prepareConcurrentOutcomes is SETTLED here, not re-spawned. */
|
|
253
|
+
function processSelectedTask(ctx, selectedId, preparedOutcome, deferPersist = false) {
|
|
254
|
+
let run = loadRun(ctx);
|
|
255
|
+
let selected = run.tasks.find((t) => t.id === selectedId);
|
|
256
|
+
let workerId = selected.workerId;
|
|
257
|
+
let dispatched = false;
|
|
258
|
+
if (selected.status === "pending") {
|
|
259
|
+
const manifest = (0, dispatch_2.createDispatchManifest)(run, 1, { backendId: selected.agentType || "agent" });
|
|
260
|
+
// Advance the RUN-level lifecycle stage on dispatch, exactly as the old
|
|
261
|
+
// build's orchestrator dispatch() wrapper did (run.loopStage = "act").
|
|
262
|
+
// The operator status "Stage:" line reads run.loopStage; v2's shell/
|
|
263
|
+
// dispatch.ts advances the task/node loopStage but never the run, so a
|
|
264
|
+
// driven run's operator status stayed frozen at "interpret".
|
|
265
|
+
run.loopStage = "act";
|
|
266
|
+
// Byte-exact to the old build's orchestrator dispatch() wrapper: a
|
|
267
|
+
// successful dispatch is its own checkpoint commit (reason
|
|
268
|
+
// `dispatch:<dispatch-id>`), not just a bare saveCheckpoint — SPEC/
|
|
269
|
+
// pipeline-run.md's persist-ordering section pins this exact reason.
|
|
270
|
+
if (!deferPersist) {
|
|
271
|
+
if (manifest.dispatchId)
|
|
272
|
+
(0, commit_1.commitState)(run, `dispatch:${manifest.dispatchId}`);
|
|
273
|
+
(0, run_store_1.saveCheckpoint)(run);
|
|
274
|
+
}
|
|
275
|
+
const dispatchedTask = manifest.tasks.find((t) => t.id === selected.id) || manifest.tasks[0];
|
|
276
|
+
if (!dispatchedTask || !dispatchedTask.workerId) {
|
|
277
|
+
return (0, drive_decide_1.makeStep)("dispatch", "failed", { runId: ctx.runId, taskId: selected.id, phase: selected.phase, reason: "dispatch produced no worker scope" });
|
|
278
|
+
}
|
|
279
|
+
workerId = dispatchedTask.workerId;
|
|
280
|
+
dispatched = true;
|
|
281
|
+
run = loadRun(ctx);
|
|
282
|
+
selected = run.tasks.find((t) => t.id === selectedId);
|
|
283
|
+
}
|
|
284
|
+
if (!workerId) {
|
|
285
|
+
return (0, drive_decide_1.makeStep)("dispatch", "failed", { runId: ctx.runId, taskId: selected.id, phase: selected.phase, reason: "no worker scope for task" });
|
|
286
|
+
}
|
|
287
|
+
const manifest = (0, worker_isolation_1.showWorkerManifest)(run, workerId);
|
|
288
|
+
// `promptDigest` here is the PER-DISPATCH worker instructions file
|
|
289
|
+
// (input.md) — it embeds this run's own id/dispatch id, so it is
|
|
290
|
+
// NEVER stable across separate runs. It feeds ONLY recordWorkerOutput's
|
|
291
|
+
// agentDelegation telemetry below, never the cache key. The cache key
|
|
292
|
+
// instead digests the task's own static, workflow-authored prompt text
|
|
293
|
+
// (selected.prompt), which IS stable across runs — byte-exact to the
|
|
294
|
+
// old build's src/drive.ts:280-282 (two differently-sourced digests,
|
|
295
|
+
// easy to collapse into one by mistake).
|
|
296
|
+
const promptDigest = fs.existsSync(manifest.inputPath) ? (0, hash_1.sha256)(fs.readFileSync(manifest.inputPath, "utf8")) : (0, hash_1.sha256)(manifest.prompt || "");
|
|
297
|
+
const cacheKeyPromptDigest = (0, hash_1.sha256)(selected.prompt || "");
|
|
298
|
+
const delegationDigest = ctx.incremental
|
|
299
|
+
? (0, drive_decide_1.incrementalDelegationDigest)(selected.model || ctx.config.model || "", selected.agentType || "agent", manifest.sandboxPolicy?.id || selected.sandboxProfileId || "", ctx.config.command || "", ctx.config.args ? (0, agent_1.stripSecretArgs)(ctx.config.args) : [], ctx.config.endpoint || "")
|
|
300
|
+
: "";
|
|
301
|
+
const cachePath = resultCachePath(run, selected, cacheKeyPromptDigest, ctx.incremental, delegationDigest);
|
|
302
|
+
if (cachePath && fs.existsSync(cachePath)) {
|
|
303
|
+
emitProgress(`↺ ${selected.label || selected.id} (${selected.phase}) — accepting cached result`);
|
|
304
|
+
try {
|
|
305
|
+
fs.writeFileSync(manifest.resultPath, fs.readFileSync(cachePath, "utf8"), "utf8");
|
|
306
|
+
(0, worker_isolation_1.recordWorkerOutput)(run, workerId, manifest.resultPath);
|
|
307
|
+
// Advance the run lifecycle stage on accept, as the old build's
|
|
308
|
+
// recordWorkerOutput wrapper did (run.loopStage = "observe").
|
|
309
|
+
run.loopStage = "observe";
|
|
310
|
+
// Bounded dynamic loops: after a round's tasks complete, evaluate the
|
|
311
|
+
// predicate and either append the next round or mark the loop done —
|
|
312
|
+
// folded into this same worker:<id>:result checkpoint, exactly as the
|
|
313
|
+
// old build's recordWorkerOutput wrapper did (no-op for non-loop runs).
|
|
314
|
+
maybeExpandLoop(run);
|
|
315
|
+
// Byte-exact to the old build's orchestrator recordWorkerOutput()
|
|
316
|
+
// wrapper: an accepted result is its own checkpoint commit (reason
|
|
317
|
+
// `worker:<worker-id>:result`), not just a bare saveCheckpoint.
|
|
318
|
+
if (!deferPersist) {
|
|
319
|
+
(0, commit_1.commitState)(run, `worker:${workerId}:result`);
|
|
320
|
+
(0, run_store_1.saveCheckpoint)(run);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
catch (error) {
|
|
324
|
+
return handleHop(ctx, selected, workerId, `result cache rejected: ${errMessage(error)}`, deferPersist, deferPersist ? run : undefined);
|
|
325
|
+
}
|
|
326
|
+
return (0, drive_decide_1.makeStep)("accept", "ok", { runId: ctx.runId, taskId: selected.id, phase: selected.phase, handleKind: "result-cache", reason: "result cache hit" });
|
|
327
|
+
}
|
|
328
|
+
const subWorkflow = selected.subWorkflow;
|
|
329
|
+
if (subWorkflow) {
|
|
330
|
+
emitProgress(`⧉ ${selected.label || selected.id} (${selected.phase}) — sub-workflow ${subWorkflow.appId}…`);
|
|
331
|
+
return runSubWorkflow(ctx, run, selected, workerId, manifest, subWorkflow, deferPersist);
|
|
332
|
+
}
|
|
333
|
+
emitProgress(`→ ${selected.label || selected.id} (${selected.phase}) — ${dispatched ? "dispatched, " : ""}spawning agent, may take minutes…`);
|
|
334
|
+
const envelope = (0, registry_1.runBackend)({
|
|
335
|
+
schemaVersion: 1,
|
|
336
|
+
runId: ctx.runId,
|
|
337
|
+
taskId: selected.id,
|
|
338
|
+
backendId: selected.agentType || "agent",
|
|
339
|
+
cwd: run.cwd,
|
|
340
|
+
sandboxPolicy: manifest.sandboxPolicy,
|
|
341
|
+
manifest: { workerDir: manifest.workerDir, manifestPath: manifest.manifestPath, inputPath: manifest.inputPath, resultPath: manifest.resultPath, prompt: manifest.prompt },
|
|
342
|
+
label: selected.id,
|
|
343
|
+
timeoutMs: ctx.config.timeoutMs,
|
|
344
|
+
delegation: { command: ctx.config.command, args: ctx.config.args, endpoint: ctx.config.endpoint, model: selected.model || ctx.config.model },
|
|
345
|
+
...(preparedOutcome ? { preparedAgentOutcome: preparedOutcome } : {}),
|
|
346
|
+
});
|
|
347
|
+
void dispatched;
|
|
348
|
+
const handle = envelope.provenance.handle;
|
|
349
|
+
const reportedModel = handle?.metadata?.reportedModel || "unreported";
|
|
350
|
+
const reportedUsage = handle?.metadata?.reportedUsage;
|
|
351
|
+
const usageSignature = handle?.metadata?.usageSignature;
|
|
352
|
+
if (envelope.status !== "completed") {
|
|
353
|
+
return handleHop(ctx, selected, workerId, `agent hop ${envelope.status}: ${envelope.result.summary}`, deferPersist, deferPersist ? run : undefined);
|
|
354
|
+
}
|
|
355
|
+
if (!manifest.resultPath || !fs.existsSync(manifest.resultPath)) {
|
|
356
|
+
return handleHop(ctx, selected, workerId, "agent produced no result.md", deferPersist, deferPersist ? run : undefined);
|
|
357
|
+
}
|
|
358
|
+
try {
|
|
359
|
+
(0, worker_isolation_1.recordWorkerOutput)(run, workerId, manifest.resultPath, {
|
|
360
|
+
agentDelegation: {
|
|
361
|
+
handle: handle,
|
|
362
|
+
model: reportedModel,
|
|
363
|
+
promptDigest,
|
|
364
|
+
command: handle?.metadata?.command,
|
|
365
|
+
args: handle?.metadata?.args || [],
|
|
366
|
+
exitCode: (0, drive_decide_1.exitCodeFromEvidence)(envelope.evidence),
|
|
367
|
+
reportedUsage,
|
|
368
|
+
usageSignature,
|
|
369
|
+
usageTrustPublicKey: ctx.config.attestPublicKey,
|
|
370
|
+
},
|
|
371
|
+
requireAttestedTelemetry: ctx.config.requireAttestedTelemetry,
|
|
372
|
+
});
|
|
373
|
+
// Advance the run lifecycle stage on accept (old build: "observe").
|
|
374
|
+
run.loopStage = "observe";
|
|
375
|
+
// Bounded dynamic loops: same round-boundary evaluation the old build's
|
|
376
|
+
// recordWorkerOutput wrapper performed, folded into this checkpoint.
|
|
377
|
+
maybeExpandLoop(run);
|
|
378
|
+
if (!deferPersist) {
|
|
379
|
+
(0, commit_1.commitState)(run, `worker:${workerId}:result`);
|
|
380
|
+
(0, run_store_1.saveCheckpoint)(run);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
catch (error) {
|
|
384
|
+
return handleHop(ctx, selected, workerId, `result.md rejected: ${errMessage(error)}`, deferPersist, deferPersist ? run : undefined);
|
|
385
|
+
}
|
|
386
|
+
// Trust visibility (purely additive, no execution-behavior change): when
|
|
387
|
+
// the agent backend forwarded provider-namespace env vars (CW_/ANTHROPIC_/
|
|
388
|
+
// etc.) to the delegated child, record the NAMES (never values) as their
|
|
389
|
+
// own tamper-evident audit event, mirroring the worker.sub-workflow event
|
|
390
|
+
// pattern above. Only ever populated by execution-backend/agent.ts's
|
|
391
|
+
// recordedAgentHandle() — a no-op for every other backend.
|
|
392
|
+
const forwardedEnvVars = handle?.metadata?.forwardedEnvVars || [];
|
|
393
|
+
if (forwardedEnvVars.length) {
|
|
394
|
+
(0, trust_audit_1.recordTrustAuditEvent)(run, {
|
|
395
|
+
kind: "worker.agent-env",
|
|
396
|
+
decision: "delegated",
|
|
397
|
+
source: "runtime-derived",
|
|
398
|
+
workerId,
|
|
399
|
+
taskId: selected.id,
|
|
400
|
+
nodeId: selected.resultNodeId,
|
|
401
|
+
envVars: forwardedEnvVars,
|
|
402
|
+
metadata: { reason: "provider-namespace env vars forwarded from host process env to the delegated agent child" },
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
if (cachePath && fs.existsSync(manifest.resultPath)) {
|
|
406
|
+
writeResultCache(cachePath, fs.readFileSync(manifest.resultPath, "utf8"));
|
|
407
|
+
}
|
|
408
|
+
return (0, drive_decide_1.makeStep)("accept", "ok", { runId: ctx.runId, taskId: selected.id, phase: selected.phase, backendId: "agent", handleKind: handle?.kind, reportedModel });
|
|
409
|
+
}
|
|
410
|
+
function runSubWorkflow(ctx, run, selected, workerId, manifest, spec, deferPersist = false) {
|
|
411
|
+
const parentApp = run.workflow.id;
|
|
412
|
+
if (ctx.depth + 1 > exports.MAX_SUB_WORKFLOW_DEPTH) {
|
|
413
|
+
return handleHop(ctx, selected, workerId, `sub-workflow depth limit exceeded (> ${exports.MAX_SUB_WORKFLOW_DEPTH})`, deferPersist, deferPersist ? run : undefined);
|
|
414
|
+
}
|
|
415
|
+
if ([...ctx.visitedAppIds, parentApp].includes(spec.appId)) {
|
|
416
|
+
return handleHop(ctx, selected, workerId, `sub-workflow cycle detected: ${[...ctx.visitedAppIds, parentApp, spec.appId].join(" -> ")}`, deferPersist, deferPersist ? run : undefined);
|
|
417
|
+
}
|
|
418
|
+
const childRunId = `sub-${run.id}-${(0, fs_atomic_1.safeFileName)(selected.id)}`;
|
|
419
|
+
const childInputs = {
|
|
420
|
+
repo: run.inputs.repo ?? run.cwd,
|
|
421
|
+
cwd: run.cwd,
|
|
422
|
+
question: run.inputs.question ?? "",
|
|
423
|
+
...renderSubInputs(spec, run.inputs),
|
|
424
|
+
runId: childRunId,
|
|
425
|
+
};
|
|
426
|
+
let childRun;
|
|
427
|
+
try {
|
|
428
|
+
const { loadWorkflowApp } = require("./workflow-app-loader");
|
|
429
|
+
childRun = (0, pipeline_1.plan)(loadWorkflowApp(spec.appId), childInputs);
|
|
430
|
+
}
|
|
431
|
+
catch (error) {
|
|
432
|
+
return handleHop(ctx, selected, workerId, `sub-workflow plan failed (${spec.appId}): ${errMessage(error)}`, deferPersist, deferPersist ? run : undefined);
|
|
433
|
+
}
|
|
434
|
+
const childResult = drive(childRun.id, childRun.cwd, {
|
|
435
|
+
now: ctx.now,
|
|
436
|
+
agentConfig: ctx.config,
|
|
437
|
+
incremental: ctx.incremental,
|
|
438
|
+
depth: ctx.depth + 1,
|
|
439
|
+
visitedAppIds: [...ctx.visitedAppIds, parentApp],
|
|
440
|
+
policy: ctx.policy,
|
|
441
|
+
});
|
|
442
|
+
if (childResult.status !== "complete") {
|
|
443
|
+
return handleHop(ctx, selected, workerId, `sub-workflow ${spec.appId} did not complete (status: ${childResult.status})`, deferPersist, deferPersist ? run : undefined);
|
|
444
|
+
}
|
|
445
|
+
const finalChild = (0, run_store_1.loadRunFromCwd)(childRun.id, childRun.cwd);
|
|
446
|
+
let childBytes;
|
|
447
|
+
if (spec.bindResult === "verdict-result") {
|
|
448
|
+
const verdict = finalChild.tasks.find((t) => /^verdict[:/]|^synthesis[:/]/i.test(t.id) && t.status === "completed");
|
|
449
|
+
childBytes = verdict?.resultPath && fs.existsSync(verdict.resultPath) ? fs.readFileSync(verdict.resultPath, "utf8") : undefined;
|
|
450
|
+
}
|
|
451
|
+
else {
|
|
452
|
+
childBytes = fs.existsSync(finalChild.paths.report) ? fs.readFileSync(finalChild.paths.report, "utf8") : undefined;
|
|
453
|
+
}
|
|
454
|
+
if (childBytes === undefined) {
|
|
455
|
+
return handleHop(ctx, selected, workerId, `sub-workflow ${spec.appId} produced no ${spec.bindResult || "report"}`, deferPersist, deferPersist ? run : undefined);
|
|
456
|
+
}
|
|
457
|
+
try {
|
|
458
|
+
fs.writeFileSync(manifest.resultPath, childBytes, "utf8");
|
|
459
|
+
(0, worker_isolation_1.recordWorkerOutput)(run, workerId, manifest.resultPath);
|
|
460
|
+
// Cross-link the child run onto the parent's delegate task + a tamper-
|
|
461
|
+
// evident `worker.sub-workflow` trust-audit event (byte-behavior port of
|
|
462
|
+
// the old build's drive sub-workflow cross-link). subRunId/subRunDir let a
|
|
463
|
+
// reader walk from the parent task to the child run; the audit event binds
|
|
464
|
+
// the child report digest + verification into the parent's hash chain.
|
|
465
|
+
selected.subRunId = childRun.id;
|
|
466
|
+
selected.subRunDir = finalChild.paths.runDir;
|
|
467
|
+
try {
|
|
468
|
+
const childAudit = (0, trust_audit_1.verifyTrustAudit)(finalChild);
|
|
469
|
+
(0, trust_audit_1.recordTrustAuditEvent)(run, {
|
|
470
|
+
kind: "worker.sub-workflow",
|
|
471
|
+
decision: "accepted",
|
|
472
|
+
source: "cw-validated",
|
|
473
|
+
workerId,
|
|
474
|
+
taskId: selected.id,
|
|
475
|
+
nodeId: selected.resultNodeId,
|
|
476
|
+
metadata: {
|
|
477
|
+
subWorkflowAppId: spec.appId,
|
|
478
|
+
subRunId: childRun.id,
|
|
479
|
+
childReportDigest: (0, hash_1.sha256)(childBytes),
|
|
480
|
+
childAuditVerified: childAudit.verified,
|
|
481
|
+
bindResult: spec.bindResult || "report",
|
|
482
|
+
},
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
catch {
|
|
486
|
+
/* the cross-link is provenance; a failure here must not undo an accepted hop */
|
|
487
|
+
}
|
|
488
|
+
// Advance the run lifecycle stage on accept (old build: "observe").
|
|
489
|
+
run.loopStage = "observe";
|
|
490
|
+
// Bounded dynamic loops: evaluate the round boundary in the same
|
|
491
|
+
// checkpoint (no-op unless this task's phase is a loop round).
|
|
492
|
+
maybeExpandLoop(run);
|
|
493
|
+
if (!deferPersist) {
|
|
494
|
+
(0, commit_1.commitState)(run, `worker:${workerId}:result`);
|
|
495
|
+
(0, run_store_1.saveCheckpoint)(run);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
catch (error) {
|
|
499
|
+
return handleHop(ctx, selected, workerId, `sub-workflow result rejected by parent gate: ${errMessage(error)}`, deferPersist, deferPersist ? run : undefined);
|
|
500
|
+
}
|
|
501
|
+
return (0, drive_decide_1.makeStep)("accept", "ok", { runId: run.id, taskId: selected.id, phase: selected.phase, handleKind: "sub-workflow", reason: `sub-workflow ${spec.appId} → ${childRun.id}` });
|
|
502
|
+
}
|
|
503
|
+
/** Byte-stable string order for anything that flows into a recorded
|
|
504
|
+
* loop-control decision (POLA: deterministic across runs/locales). */
|
|
505
|
+
function compareBytes(a, b) {
|
|
506
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
507
|
+
}
|
|
508
|
+
/** Bounded dynamic loop expansion — the shell half of the loop runtime.
|
|
509
|
+
* After a worker result is recorded (mutating `run` in memory), if the
|
|
510
|
+
* just-completed phase is the LATEST round of a loop whose origin is not
|
|
511
|
+
* yet done and all that round's tasks completed, evaluate the pure stop
|
|
512
|
+
* decision (evaluateLoopStop) and either append the next round (clone the
|
|
513
|
+
* round-1 template tasks into a fresh phase, materialized like plan()
|
|
514
|
+
* does — task files + a plan-stage node per new task) or mark the loop
|
|
515
|
+
* done. One deterministic `loop-control` node is recorded per round
|
|
516
|
+
* boundary — the replay source of truth. No-op when the run has no loop
|
|
517
|
+
* phases (POLA). Expands at most ONE loop boundary per call; the next
|
|
518
|
+
* accept handles the next. Byte-exact port of the old build's
|
|
519
|
+
* orchestrator/lifecycle-operations.ts maybeExpandLoop, called there from
|
|
520
|
+
* recordWorkerOutput; v2's recordWorkerOutput (shell/worker-isolation.ts)
|
|
521
|
+
* does not expand, so the drive shell wires it in right after an accept.
|
|
522
|
+
* Exported so the standalone `cw worker output` CLI verb (worker-cli.ts) can
|
|
523
|
+
* run the same round-expansion after a hand-recorded accept, matching the old
|
|
524
|
+
* build's recordWorkerOutput wrapper. */
|
|
525
|
+
function maybeExpandLoop(run) {
|
|
526
|
+
for (const phase of [...run.phases]) {
|
|
527
|
+
const originId = phase.loop ? phase.id : phase.loopOrigin;
|
|
528
|
+
if (!originId)
|
|
529
|
+
continue;
|
|
530
|
+
const origin = run.phases.find((p) => p.id === originId);
|
|
531
|
+
if (!origin || !origin.loop || origin.loopDone)
|
|
532
|
+
continue;
|
|
533
|
+
// Act only from the LATEST round phase of this loop.
|
|
534
|
+
const loopPhases = run.phases.filter((p) => p.id === originId || p.loopOrigin === originId);
|
|
535
|
+
const latest = loopPhases.reduce((a, b) => ((b.loopRound || 1) >= (a.loopRound || 1) ? b : a));
|
|
536
|
+
if (phase.id !== latest.id)
|
|
537
|
+
continue;
|
|
538
|
+
const roundTasks = run.tasks.filter((t) => latest.taskIds.includes(t.id));
|
|
539
|
+
if (roundTasks.length === 0 || !roundTasks.every((t) => t.status === "completed"))
|
|
540
|
+
continue;
|
|
541
|
+
const round = latest.loopRound || 1;
|
|
542
|
+
const ordered = (tasks) => tasks
|
|
543
|
+
.slice()
|
|
544
|
+
.sort((a, b) => compareBytes(a.id, b.id))
|
|
545
|
+
.map((t) => t.result);
|
|
546
|
+
const roundResults = ordered(roundTasks);
|
|
547
|
+
const allLoopTasks = run.tasks.filter((t) => t.status === "completed" && loopPhases.some((p) => p.taskIds.includes(t.id)));
|
|
548
|
+
const allResults = ordered(allLoopTasks);
|
|
549
|
+
const ctx = {
|
|
550
|
+
round,
|
|
551
|
+
roundResults,
|
|
552
|
+
allResults,
|
|
553
|
+
// budget-target scaling counts RECORDED tokens; read them through the
|
|
554
|
+
// same normalizer the CAP gate uses so a snake_case reportedUsage hop
|
|
555
|
+
// is counted (evaluateLoopStop reads usageTotals.totalTokens).
|
|
556
|
+
usageTotals: { totalTokens: recordedTokenTotal(run) },
|
|
557
|
+
inputs: run.inputs,
|
|
558
|
+
};
|
|
559
|
+
const decision = (0, loop_expansion_1.evaluateLoopStop)(origin, round, ctx);
|
|
560
|
+
const until = origin.loop.until;
|
|
561
|
+
// Record the decision under a deterministic id (the replay source of truth).
|
|
562
|
+
(0, node_store_1.appendRunNode)(run, (0, state_node_1.createStateNode)({
|
|
563
|
+
id: (0, loop_expansion_1.loopControlNodeId)(run.id, originId, round),
|
|
564
|
+
kind: "loop-control",
|
|
565
|
+
status: "completed",
|
|
566
|
+
loopStage: "adjust",
|
|
567
|
+
outputs: { round, done: decision.done, atCap: decision.atCap, reason: decision.reason },
|
|
568
|
+
metadata: {
|
|
569
|
+
originPhaseId: originId,
|
|
570
|
+
until: until.kind === "predicate" ? until.ref : `budget-target:${until.target}`,
|
|
571
|
+
round,
|
|
572
|
+
done: decision.done,
|
|
573
|
+
atCap: decision.atCap,
|
|
574
|
+
reason: decision.reason,
|
|
575
|
+
},
|
|
576
|
+
}));
|
|
577
|
+
if (decision.done) {
|
|
578
|
+
origin.loopDone = true;
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
// Expand: clone the ROUND-1 template tasks into a fresh phase appended
|
|
582
|
+
// right after the latest round.
|
|
583
|
+
const nextRound = round + 1;
|
|
584
|
+
const templateTasks = run.tasks.filter((t) => origin.taskIds.includes(t.id));
|
|
585
|
+
const { phase: nextPhase, tasks: newTasks } = (0, loop_expansion_1.cloneLoopRoundTasks)(origin, templateTasks, nextRound);
|
|
586
|
+
const insertAt = run.phases.findIndex((p) => p.id === latest.id);
|
|
587
|
+
run.phases.splice(insertAt + 1, 0, nextPhase);
|
|
588
|
+
run.tasks.push(...newTasks);
|
|
589
|
+
// Materialize: task files + a plan-stage node per new task (mirrors plan()).
|
|
590
|
+
(0, harness_1.writeTaskFiles)(run);
|
|
591
|
+
const inputNodeId = `${run.id}:input`;
|
|
592
|
+
for (const t of newTasks) {
|
|
593
|
+
const result = (0, runner_1.runPipelineStage)(run, "plan", inputNodeId, {
|
|
594
|
+
outputNodeId: `${run.id}:task:${t.id}`,
|
|
595
|
+
outputStatus: "pending",
|
|
596
|
+
loopStage: "interpret",
|
|
597
|
+
artifacts: [{ id: "task", kind: "markdown", path: t.taskPath }],
|
|
598
|
+
metadata: {
|
|
599
|
+
workflowId: run.workflow.id,
|
|
600
|
+
taskId: t.id,
|
|
601
|
+
phase: t.phase,
|
|
602
|
+
taskKind: t.kind,
|
|
603
|
+
requiresEvidence: t.requiresEvidence,
|
|
604
|
+
sandboxProfileId: t.sandboxProfileId,
|
|
605
|
+
},
|
|
606
|
+
}, { persist: false, persistNode: (r, node) => void (0, node_store_1.appendRunNode)(r, node) });
|
|
607
|
+
t.stateNodeId = result.outputNodeId;
|
|
608
|
+
}
|
|
609
|
+
(0, dispatch_1.updatePhaseStatuses)(run);
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
/** One deterministic drive step. */
|
|
614
|
+
function driveStep(ctx) {
|
|
615
|
+
const run = loadRun(ctx);
|
|
616
|
+
const selected = (0, drive_decide_1.selectDriveTask)(run);
|
|
617
|
+
const gate = (0, drive_decide_1.terminalOrConfigStep)(run, selected, agentConfigured(ctx.config), tokenBudgetUsage(run));
|
|
618
|
+
if (gate.kind === "commit") {
|
|
619
|
+
// Terminal commit: advance the run lifecycle stage as the old build's
|
|
620
|
+
// commit() wrapper did (run.loopStage = "checkpoint").
|
|
621
|
+
run.loopStage = "checkpoint";
|
|
622
|
+
const commit = (0, commit_1.commitState)(run, { reason: "agent-delegation-drive: audited verdict committed", ...(gate.verifierNodeId ? { verifierNodeId: gate.verifierNodeId } : { allowUnverifiedCheckpoint: true, verifierGated: false }) });
|
|
623
|
+
(0, report_1.writeReport)(run);
|
|
624
|
+
(0, run_store_1.saveCheckpoint)(run);
|
|
625
|
+
return (0, drive_decide_1.makeStep)("commit", "complete", { runId: run.id, reason: `committed ${commit.id}` });
|
|
626
|
+
}
|
|
627
|
+
if (gate.step)
|
|
628
|
+
return gate.step;
|
|
629
|
+
return processSelectedTask(ctx, selected.id);
|
|
630
|
+
}
|
|
631
|
+
/** Dispatch every batch task (sequential — dispatch mutates state), then
|
|
632
|
+
* collect ALL spawn-style agent child outcomes in one concurrent window
|
|
633
|
+
* (one batch delegate child process, per-job timeout kill). Returns
|
|
634
|
+
* outcomes keyed by task id; a cache-hit or endpoint-configured agent
|
|
635
|
+
* gets no prepared outcome and settles through the serial accept path
|
|
636
|
+
* inside processSelectedTask. Dispatch failures become recorded fail
|
|
637
|
+
* steps up front, exactly what the serial path would emit. Byte-exact
|
|
638
|
+
* to the old build's src/drive.ts's prepareConcurrentOutcomes. */
|
|
639
|
+
function prepareConcurrentOutcomes(ctx, batch) {
|
|
640
|
+
const failSteps = new Map();
|
|
641
|
+
const jobs = [];
|
|
642
|
+
const jobTaskIds = [];
|
|
643
|
+
for (const taskId of batch) {
|
|
644
|
+
const run = loadRun(ctx);
|
|
645
|
+
const task = run.tasks.find((candidate) => candidate.id === taskId);
|
|
646
|
+
if (!task || (task.status !== "pending" && task.status !== "running"))
|
|
647
|
+
continue;
|
|
648
|
+
let workerId = task.workerId;
|
|
649
|
+
if (task.status === "pending") {
|
|
650
|
+
const manifest = (0, dispatch_2.createDispatchManifest)(run, 1, { backendId: task.agentType || "agent" });
|
|
651
|
+
const dispatchedTask = manifest.tasks.find((entry) => entry.id === task.id) || manifest.tasks[0];
|
|
652
|
+
if (!dispatchedTask || !dispatchedTask.workerId) {
|
|
653
|
+
failSteps.set(taskId, (0, drive_decide_1.makeStep)("dispatch", "failed", { runId: ctx.runId, taskId, phase: task.phase, reason: "dispatch produced no worker scope" }));
|
|
654
|
+
continue;
|
|
655
|
+
}
|
|
656
|
+
workerId = dispatchedTask.workerId;
|
|
657
|
+
}
|
|
658
|
+
if (!workerId) {
|
|
659
|
+
failSteps.set(taskId, (0, drive_decide_1.makeStep)("dispatch", "failed", { runId: ctx.runId, taskId, phase: task.phase, reason: "no worker scope for task" }));
|
|
660
|
+
continue;
|
|
661
|
+
}
|
|
662
|
+
const freshRun = loadRun(ctx);
|
|
663
|
+
const manifest = (0, worker_isolation_1.showWorkerManifest)(freshRun, workerId);
|
|
664
|
+
const delegationDigest = ctx.incremental
|
|
665
|
+
? (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 || "")
|
|
666
|
+
: "";
|
|
667
|
+
const cachePath = resultCachePath(freshRun, task, (0, hash_1.sha256)(task.prompt || ""), ctx.incremental, delegationDigest);
|
|
668
|
+
if (cachePath && fs.existsSync(cachePath))
|
|
669
|
+
continue;
|
|
670
|
+
const job = (0, agent_1.prepareAgentSpawn)({
|
|
671
|
+
schemaVersion: 1,
|
|
672
|
+
runId: ctx.runId,
|
|
673
|
+
taskId: task.id,
|
|
674
|
+
backendId: task.agentType || "agent",
|
|
675
|
+
cwd: freshRun.cwd,
|
|
676
|
+
sandboxPolicy: manifest.sandboxPolicy,
|
|
677
|
+
manifest: { workerDir: manifest.workerDir, manifestPath: manifest.manifestPath, inputPath: manifest.inputPath, resultPath: manifest.resultPath, prompt: manifest.prompt },
|
|
678
|
+
label: task.id,
|
|
679
|
+
timeoutMs: ctx.config.timeoutMs,
|
|
680
|
+
delegation: { command: ctx.config.command, args: ctx.config.args, endpoint: ctx.config.endpoint, model: task.model || ctx.config.model },
|
|
681
|
+
});
|
|
682
|
+
if (job) {
|
|
683
|
+
const sandboxPolicy = manifest.sandboxPolicy;
|
|
684
|
+
if (sandboxPolicy) {
|
|
685
|
+
job.env = (0, agent_1.buildAgentChildEnv)(sandboxPolicy).env;
|
|
686
|
+
}
|
|
687
|
+
jobs.push(job);
|
|
688
|
+
jobTaskIds.push(taskId);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
if (jobs.length) {
|
|
692
|
+
emitProgress(`⇉ concurrent round: ${jobs.length} agent${jobs.length > 1 ? "s" : ""} spawning in parallel, may take minutes…`);
|
|
693
|
+
}
|
|
694
|
+
const settled = (0, agent_1.runAgentBatchOutcomes)(jobs);
|
|
695
|
+
const outcomes = new Map();
|
|
696
|
+
jobTaskIds.forEach((taskId, index) => outcomes.set(taskId, settled[index]));
|
|
697
|
+
return { outcomes, failSteps };
|
|
698
|
+
}
|
|
699
|
+
/** One concurrent round inside one cached in-memory run: dispatches every
|
|
700
|
+
* batch task, spawns all spawn-style agent children in one concurrent
|
|
701
|
+
* window, then settles + accepts in DETERMINISTIC batch (task-id) order
|
|
702
|
+
* regardless of wall-clock finish order. At round end it flushes once:
|
|
703
|
+
* commitState(run, "concurrent-round:<n>-tasks") + writeReport +
|
|
704
|
+
* saveCheckpoint. Cache-hit tasks and endpoint-only agents get no
|
|
705
|
+
* prepared outcome and settle through the serial path (still inside
|
|
706
|
+
* this one deferred-persist round). If no step was produced (nothing
|
|
707
|
+
* runnable at round entry — terminal/blocked/token-budget gate) the
|
|
708
|
+
* round degrades to one plain driveStep. Byte-exact to the old build's
|
|
709
|
+
* src/drive.ts's driveConcurrentRound. */
|
|
710
|
+
function driveConcurrentRound(ctx, limit) {
|
|
711
|
+
return withRoundCache(ctx, () => {
|
|
712
|
+
const run = loadRun(ctx);
|
|
713
|
+
const selected = (0, drive_decide_1.selectDriveTask)(run);
|
|
714
|
+
const gate = (0, drive_decide_1.terminalOrConfigStep)(run, selected, agentConfigured(ctx.config), tokenBudgetUsage(run));
|
|
715
|
+
if (gate.kind === "commit" || gate.step)
|
|
716
|
+
return [driveStep(ctx)];
|
|
717
|
+
const phase = (0, dispatch_1.firstRunnablePhase)(run);
|
|
718
|
+
const width = Math.max(1, Math.floor(limit) || 1);
|
|
719
|
+
const batch = run.tasks
|
|
720
|
+
.filter((task) => phase.taskIds.includes(task.id) && (task.status === "pending" || task.status === "running"))
|
|
721
|
+
.slice(0, width)
|
|
722
|
+
.map((task) => task.id);
|
|
723
|
+
const prepared = prepareConcurrentOutcomes(ctx, batch);
|
|
724
|
+
const steps = [];
|
|
725
|
+
for (const taskId of batch) {
|
|
726
|
+
const failStep = prepared.failSteps.get(taskId);
|
|
727
|
+
if (failStep) {
|
|
728
|
+
steps.push(failStep);
|
|
729
|
+
continue;
|
|
730
|
+
}
|
|
731
|
+
// Re-read per task: a prior accept in this round mutated state (the
|
|
732
|
+
// SAME cached object via loadRun's round cache — no disk round-trip
|
|
733
|
+
// until the round-end flush below).
|
|
734
|
+
const freshRun = loadRun(ctx);
|
|
735
|
+
const fresh = freshRun.tasks.find((task) => task.id === taskId);
|
|
736
|
+
if (!fresh || (fresh.status !== "pending" && fresh.status !== "running"))
|
|
737
|
+
continue;
|
|
738
|
+
steps.push(processSelectedTask(ctx, taskId, prepared.outcomes.get(taskId), true));
|
|
739
|
+
}
|
|
740
|
+
if (steps.length > 0) {
|
|
741
|
+
const settledRun = loadRun(ctx);
|
|
742
|
+
(0, commit_1.commitState)(settledRun, `concurrent-round:${batch.length}-tasks`);
|
|
743
|
+
(0, report_1.writeReport)(settledRun);
|
|
744
|
+
(0, run_store_1.saveCheckpoint)(settledRun);
|
|
745
|
+
}
|
|
746
|
+
return steps.length > 0 ? steps : [driveStep(ctx)];
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
/** Drive a run: `--once` advances exactly one step; otherwise run to
|
|
750
|
+
* completion, park, or a blocked stop. */
|
|
751
|
+
function drive(runId, cwd, options = {}) {
|
|
752
|
+
const now = options.now || new Date().toISOString();
|
|
753
|
+
const config = options.agentConfig || (0, agent_config_1.resolveAgentConfig)(options.args || {});
|
|
754
|
+
const policy = { ...drive_decide_1.DEFAULT_SCHEDULING_POLICY, ...(options.policy || {}) };
|
|
755
|
+
const ctx = {
|
|
756
|
+
runId,
|
|
757
|
+
cwd,
|
|
758
|
+
now,
|
|
759
|
+
config,
|
|
760
|
+
attempts: new Map(),
|
|
761
|
+
incremental: Boolean(options.incremental),
|
|
762
|
+
depth: Math.max(0, Math.floor(options.depth || 0)),
|
|
763
|
+
visitedAppIds: options.visitedAppIds || [],
|
|
764
|
+
policy,
|
|
765
|
+
};
|
|
766
|
+
const steps = [];
|
|
767
|
+
const run0 = loadRun(ctx);
|
|
768
|
+
const plannedWorkers = run0.tasks.length;
|
|
769
|
+
const maxIter = (0, drive_decide_1.maxIterations)(plannedWorkers, (0, loop_expansion_1.maxLoopExpansion)(run0), policy);
|
|
770
|
+
// Phase-boundary progress (brew-style): announce each phase when it
|
|
771
|
+
// becomes active and when it finishes — `==> Map ✓ (6/6)` / `==> Assess
|
|
772
|
+
// ⇉ (3/6)`. Describes CW's OWN phases (vendor-neutral); goes to stderr
|
|
773
|
+
// via emitProgress so stdout stays clean data. Byte-exact port of the
|
|
774
|
+
// old build's src/drive.ts emitPhaseProgress. term.phaseProgressLine
|
|
775
|
+
// renders the line; this closure decides WHEN to emit each boundary.
|
|
776
|
+
const announcedPhaseComplete = new Set();
|
|
777
|
+
let activePhaseId;
|
|
778
|
+
const titleCase = (s) => (s ? s.charAt(0).toUpperCase() + s.slice(1) : s);
|
|
779
|
+
const emitPhaseProgress = (run) => {
|
|
780
|
+
for (const ph of run.phases || []) {
|
|
781
|
+
const phaseTasks = run.tasks.filter((task) => ph.taskIds.includes(task.id));
|
|
782
|
+
const total = phaseTasks.length;
|
|
783
|
+
if (total === 0)
|
|
784
|
+
continue;
|
|
785
|
+
const done = phaseTasks.filter((task) => task.status === "completed").length;
|
|
786
|
+
const label = titleCase(ph.name || ph.id);
|
|
787
|
+
if (done >= total) {
|
|
788
|
+
if (!announcedPhaseComplete.has(ph.id)) {
|
|
789
|
+
announcedPhaseComplete.add(ph.id);
|
|
790
|
+
emitProgress((0, term_1.phaseProgressLine)(label, done, total, ph.mode, process.stderr));
|
|
791
|
+
}
|
|
792
|
+
continue;
|
|
793
|
+
}
|
|
794
|
+
if (ph.id !== activePhaseId) {
|
|
795
|
+
activePhaseId = ph.id;
|
|
796
|
+
emitProgress((0, term_1.phaseProgressLine)(label, done, total, ph.mode, process.stderr));
|
|
797
|
+
}
|
|
798
|
+
return; // only the first not-yet-complete phase is "active"
|
|
799
|
+
}
|
|
800
|
+
};
|
|
801
|
+
let exhaustedMaxIterations = !options.once;
|
|
802
|
+
for (let i = 0; i < maxIter; i++) {
|
|
803
|
+
const width = (0, drive_decide_1.roundWidth)(loadRun(ctx), options.concurrency);
|
|
804
|
+
// width>1 (an explicit --concurrency>1, or an auto-width parallel
|
|
805
|
+
// phase) runs the whole round through driveConcurrentRound — one or
|
|
806
|
+
// more steps recorded in deterministic batch order, one flush at
|
|
807
|
+
// round end. `--once` still stops after this ONE outer-loop
|
|
808
|
+
// iteration even though a round can yield multiple steps.
|
|
809
|
+
const roundSteps = width > 1 ? driveConcurrentRound(ctx, width) : [driveStep(ctx)];
|
|
810
|
+
for (const stepResult of roundSteps)
|
|
811
|
+
steps.push(stepResult);
|
|
812
|
+
// Brew-style phase boundaries: after each round, announce a
|
|
813
|
+
// newly-active phase and any phase that just finished. Cheap — reuses
|
|
814
|
+
// the run we just advanced; goes to stderr so stdout stays clean.
|
|
815
|
+
emitPhaseProgress(loadRun(ctx));
|
|
816
|
+
const last = roundSteps[roundSteps.length - 1];
|
|
817
|
+
if (options.once) {
|
|
818
|
+
exhaustedMaxIterations = false;
|
|
819
|
+
break;
|
|
820
|
+
}
|
|
821
|
+
if (last && (last.status === "complete" || last.status === "parked" || last.status === "blocked")) {
|
|
822
|
+
exhaustedMaxIterations = false;
|
|
823
|
+
break;
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
const run = loadRun(ctx);
|
|
827
|
+
const completedWorkers = (0, drive_decide_1.countCompleted)(run);
|
|
828
|
+
const parkedWorkers = (0, drive_decide_1.countParked)(run);
|
|
829
|
+
const committed = (0, drive_decide_1.hasTerminalCommit)(run);
|
|
830
|
+
const last = steps[steps.length - 1];
|
|
831
|
+
if (exhaustedMaxIterations) {
|
|
832
|
+
steps.push((0, drive_decide_1.makeStep)("blocked", "blocked", { runId, reason: `drive reached max iteration limit (${maxIter}) before a terminal state` }));
|
|
833
|
+
}
|
|
834
|
+
const statusInputs = {
|
|
835
|
+
once: Boolean(options.once),
|
|
836
|
+
completedWorkers,
|
|
837
|
+
plannedWorkers,
|
|
838
|
+
committed,
|
|
839
|
+
lastStepStatus: steps[steps.length - 1]?.status,
|
|
840
|
+
exhaustedMaxIterations,
|
|
841
|
+
parkedWorkers,
|
|
842
|
+
};
|
|
843
|
+
void last;
|
|
844
|
+
void drive_decide_1.verdictVerifierNodeId;
|
|
845
|
+
const status = (0, drive_decide_1.finalDriveStatus)(statusInputs);
|
|
846
|
+
const committedCommit = (run.commits || []).find((c) => c.reason && c.reason.startsWith("agent-delegation-drive"));
|
|
847
|
+
return {
|
|
848
|
+
schemaVersion: 1,
|
|
849
|
+
runId,
|
|
850
|
+
workflowId: run.workflow.id,
|
|
851
|
+
status,
|
|
852
|
+
steps,
|
|
853
|
+
plannedWorkers,
|
|
854
|
+
completedWorkers,
|
|
855
|
+
parkedWorkers,
|
|
856
|
+
commitId: committedCommit?.id,
|
|
857
|
+
reportPath: run.paths.report,
|
|
858
|
+
statePath: run.paths.state,
|
|
859
|
+
agentConfigured: agentConfigured(config),
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
function drivePreview(runId, cwd, args = {}) {
|
|
863
|
+
const run = (0, run_store_1.loadRunFromCwd)(runId, cwd);
|
|
864
|
+
const config = (0, agent_config_1.resolveAgentConfig)(args);
|
|
865
|
+
const configured = agentConfigured(config);
|
|
866
|
+
const selected = (0, drive_decide_1.selectDriveTask)(run);
|
|
867
|
+
const plannedWorkers = run.tasks.length;
|
|
868
|
+
const pendingWorkers = run.tasks.filter((t) => t.status === "pending" || t.status === "running").length;
|
|
869
|
+
const completedWorkers = (0, drive_decide_1.countCompleted)(run);
|
|
870
|
+
const parkedWorkers = (0, drive_decide_1.countParked)(run);
|
|
871
|
+
let nextAction;
|
|
872
|
+
if (!selected) {
|
|
873
|
+
nextAction = run.tasks.every((t) => t.status === "completed") ? "commit" : "blocked";
|
|
874
|
+
}
|
|
875
|
+
else if (!configured) {
|
|
876
|
+
nextAction = "blocked";
|
|
877
|
+
}
|
|
878
|
+
else if (selected.status === "pending") {
|
|
879
|
+
nextAction = "dispatch";
|
|
880
|
+
}
|
|
881
|
+
else {
|
|
882
|
+
nextAction = "fulfill";
|
|
883
|
+
}
|
|
884
|
+
return { schemaVersion: 1, runId, workflowId: run.workflow.id, plannedWorkers, pendingWorkers, completedWorkers, parkedWorkers, nextAction, nextTaskId: selected?.id, nextPhase: selected?.phase, agentConfigured: configured };
|
|
885
|
+
}
|