@deksden-com/dd-flow-cli 0.9.0-beta.8 → 0.9.0-beta.80
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/CHANGELOG.md +396 -0
- package/README.md +65 -0
- package/dist/build-info.json +8 -8
- package/dist/cli/command-inputs.js +300 -0
- package/dist/cli/help.js +81 -9
- package/dist/cli/hook-ingress.js +82 -0
- package/dist/cli/input-preparation.js +115 -0
- package/dist/cli/run-cli.js +1342 -375
- package/dist/cli.js +6 -2
- package/dist/harness-runtime/bin/dd-agy.mjs +37 -0
- package/dist/harness-runtime/bin/dd-codex.mjs +25 -0
- package/dist/harness-runtime/bin/dd-droid.mjs +33 -0
- package/dist/harness-runtime/bin/dd-grok.mjs +30 -0
- package/dist/harness-runtime/bin/dd-opencode.mjs +21 -0
- package/dist/harness-runtime/bin/dd-zcode.mjs +83 -0
- package/dist/harness-runtime/lib/daemon-operations.mjs +215 -0
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +423 -0
- package/dist/harness-runtime/lib/dd-agy.mjs +59 -0
- package/dist/harness-runtime/lib/dd-codex-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +146 -0
- package/dist/harness-runtime/lib/dd-codex.mjs +562 -0
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +125 -0
- package/dist/harness-runtime/lib/dd-droid.mjs +471 -0
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +274 -0
- package/dist/harness-runtime/lib/dd-grok.mjs +174 -0
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +198 -0
- package/dist/harness-runtime/lib/dd-opencode.mjs +98 -0
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +644 -0
- package/dist/harness-runtime/lib/dd-zcode.mjs +910 -0
- package/dist/harness-runtime/lib/delegation-instructions.d.mts +10 -0
- package/dist/harness-runtime/lib/delegation-instructions.mjs +130 -0
- package/dist/harness-runtime/lib/dispatch-fence.mjs +14 -0
- package/dist/harness-runtime/lib/driver-recovery.mjs +129 -0
- package/dist/harness-runtime/lib/droid-observation.mjs +66 -0
- package/dist/harness-runtime/lib/managed-daemon.mjs +260 -0
- package/dist/harness-runtime/lib/model-observations.mjs +77 -0
- package/dist/harness-runtime/lib/native-hook-command.d.mts +1 -0
- package/dist/harness-runtime/lib/native-hook-command.mjs +34 -0
- package/dist/harness-runtime/lib/observation-clock.mjs +36 -0
- package/dist/harness-runtime/lib/operation-context.mjs +5 -0
- package/dist/harness-runtime/lib/operation-errors.mjs +19 -0
- package/dist/harness-runtime/lib/process-json.mjs +70 -0
- package/dist/harness-runtime/lib/process-snapshot.mjs +10 -0
- package/dist/harness-runtime/lib/runner-events.mjs +200 -0
- package/dist/harness-runtime/lib/runner-lock.mjs +42 -0
- package/dist/harness-runtime/lib/session-settlement.mjs +30 -0
- package/dist/harness-runtime/lib/tool-observations.d.mts +17 -0
- package/dist/harness-runtime/lib/tool-observations.mjs +153 -0
- package/dist/schemas/agent-profile.schema.json +1 -1
- package/dist/schemas/code-review-result.schema.json +2 -2
- package/dist/schemas/code-work-batch.schema.json +4 -3
- package/dist/schemas/code-work-result.schema.json +4 -4
- package/dist/schemas/plan-review-result.schema.json +2 -2
- package/dist/schemas/run-control-receipt.schema.json +99 -0
- package/dist/schemas/run-control-request.schema.json +36 -0
- package/dist/schemas/vnext-protocol-plan.schema.json +2 -2
- package/dist/services/canon.js +11 -8
- package/dist/services/cleanup.js +57 -13
- package/dist/services/cli-operation-classifier.js +22 -24
- package/dist/services/code-checks.js +543 -96
- package/dist/services/codex-hook-delivery.js +28 -0
- package/dist/services/config.js +11 -8
- package/dist/services/controller-fanout.js +116 -0
- package/dist/services/dashboard.js +103 -31
- package/dist/services/engines.js +121 -76
- package/dist/services/eval-snapshots.js +750 -73
- package/dist/services/execution-policy.js +170 -0
- package/dist/services/external-work-launch.js +110 -0
- package/dist/services/harness-adapter.js +147 -24
- package/dist/services/harness-config.js +2 -0
- package/dist/services/hooks.js +553 -285
- package/dist/services/lanes.js +61 -53
- package/dist/services/lifecycle-command.js +87 -7
- package/dist/services/lifecycle-invocations.js +828 -0
- package/dist/services/managed-daemon-binding.js +38 -0
- package/dist/services/managed-processes.js +283 -27
- package/dist/services/merge-queue.js +179 -104
- package/dist/services/merge-server.js +73 -28
- package/dist/services/migrations.js +14 -9
- package/dist/services/native-daemon-history.js +49 -0
- package/dist/services/native-session-control.js +39 -0
- package/dist/services/plan-runtime.js +19 -8
- package/dist/services/plans.js +28 -25
- package/dist/services/projects.js +18 -5
- package/dist/services/prompts.js +23 -16
- package/dist/services/protocols.js +64 -20
- package/dist/services/recovery-observation-budget.js +61 -0
- package/dist/services/recovery-snapshot-database.js +107 -0
- package/dist/services/run-control-receipt.js +56 -0
- package/dist/services/run-control-worker.js +363 -0
- package/dist/services/run-control.js +876 -0
- package/dist/services/run-controller-adapter.js +222 -0
- package/dist/services/run-controller-capture.js +134 -0
- package/dist/services/run-controller-process.js +189 -0
- package/dist/services/run-controller-recovery.js +222 -0
- package/dist/services/run-controller-state.js +36 -0
- package/dist/services/run-controller.js +779 -0
- package/dist/services/run-engine-bindings.js +1 -1
- package/dist/services/run-fork.js +138 -0
- package/dist/services/run-observations.js +112 -0
- package/dist/services/run-recovery-runtime.js +69 -0
- package/dist/services/run-recovery.js +342 -0
- package/dist/services/runs.js +368 -74
- package/dist/services/runtime-budget.js +434 -0
- package/dist/services/runtime-scope-capture.js +50 -0
- package/dist/services/runtime-scope-control.js +450 -0
- package/dist/services/runtime-scope-resume.js +565 -0
- package/dist/services/runtime-scope-stop.js +99 -0
- package/dist/services/runtime-scope-worker.js +243 -0
- package/dist/services/runtime-service.js +97 -0
- package/dist/services/schema-validation.js +10 -8
- package/dist/services/sessions.js +35 -69
- package/dist/services/stage-blocker.js +17 -7
- package/dist/services/stage-context.js +44 -18
- package/dist/services/stage-lifecycle.js +99 -102
- package/dist/services/stage-pause.js +65 -23
- package/dist/services/usage.js +37 -93
- package/dist/services/vnext-code-review.js +189 -85
- package/dist/services/vnext-code.js +257 -243
- package/dist/services/vnext-execution-profile.js +5 -3
- package/dist/services/vnext-fanout.js +134 -19
- package/dist/services/vnext-merge.js +285 -102
- package/dist/services/vnext-plan-review.js +148 -94
- package/dist/services/vnext-plan.js +141 -72
- package/dist/services/vnext-protocolize.js +129 -56
- package/dist/services/vnext-specify.js +87 -64
- package/dist/services/work-registry.js +707 -156
- package/dist/services/workspace-bootstrap.js +76 -0
- package/dist/services/worktrees.js +85 -35
- package/dist/shared/errors.js +12 -0
- package/dist/storage/database.js +444 -32
- package/dist/storage/paths.js +17 -4
- package/dist/storage/writer-contract.js +77 -0
- package/dist/storage/writer-migration.js +102 -0
- package/package.json +11 -4
- package/tools/repair-paused-run-status.mjs +59 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { AppError } from "../shared/errors.js";
|
|
5
|
+
import { harnessKey } from "./harness-config.js";
|
|
6
|
+
import { vnextStages } from "../domain/stage-catalog.js";
|
|
7
|
+
import { prepareFileBytes, prepareJsonText } from "../cli/input-preparation.js";
|
|
8
|
+
/** Read once so the frozen policy and its provenance describe the same bytes. */
|
|
9
|
+
export function loadExecutionRoutingInput(file) {
|
|
10
|
+
const { file: source_path, bytes } = prepareFileBytes(file, "execution routing");
|
|
11
|
+
const value = prepareJsonText(bytes.toString("utf8"), { file: source_path, label: "execution routing" }).value;
|
|
12
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
13
|
+
throw new AppError("execution_profile_invalid", "Execution routing input must be an object", 2, { source_path, phase: "prepare", effect: "no_effect", recoverable: true });
|
|
14
|
+
}
|
|
15
|
+
const input = value;
|
|
16
|
+
if (input.schema_id !== "dd-flow/execution-routing@1"
|
|
17
|
+
|| Object.keys(input).some(key => key !== "schema_id" && key !== "execution")
|
|
18
|
+
|| !isExecutionRouting(input.execution)) {
|
|
19
|
+
throw new AppError("execution_profile_invalid", "Execution routing input does not match dd-flow/execution-routing@1", 2, { source_path, phase: "prepare", effect: "no_effect", recoverable: true });
|
|
20
|
+
}
|
|
21
|
+
return { execution: input.execution, bytes, source_path, source_checksum: crypto.createHash("sha256").update(bytes).digest("hex") };
|
|
22
|
+
}
|
|
23
|
+
/** One routing validator for project defaults and explicit RUN inputs. */
|
|
24
|
+
export function isExecutionRouting(value) {
|
|
25
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
26
|
+
return false;
|
|
27
|
+
const { stage_overrides: stages, ...root } = value;
|
|
28
|
+
if (typeof root.agent_profile_id !== "string")
|
|
29
|
+
return false;
|
|
30
|
+
try {
|
|
31
|
+
validateExecutionOverride(root);
|
|
32
|
+
if (stages !== undefined) {
|
|
33
|
+
if (!stages || typeof stages !== "object" || Array.isArray(stages))
|
|
34
|
+
return false;
|
|
35
|
+
for (const [stage, override] of Object.entries(stages)) {
|
|
36
|
+
if (!vnextStages.some(item => item.id === stage))
|
|
37
|
+
return false;
|
|
38
|
+
validateExecutionOverride(override);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/** Reads one complete profile. A policy selects profiles; it never field-merges them. */
|
|
48
|
+
export function loadAgentProfile(ddFlowHome, id) {
|
|
49
|
+
if (typeof id !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(id))
|
|
50
|
+
throw new AppError("agent_profile_invalid", "Agent profile id is invalid", 2, { id });
|
|
51
|
+
const file = path.join(ddFlowHome, "agent-profiles", `${id}.json`);
|
|
52
|
+
let value;
|
|
53
|
+
try {
|
|
54
|
+
value = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
55
|
+
}
|
|
56
|
+
catch (cause) {
|
|
57
|
+
throw new AppError("agent_profile_missing", "Agent profile is unavailable", 1, { id, file, cause: String(cause) });
|
|
58
|
+
}
|
|
59
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
60
|
+
throw new AppError("agent_profile_invalid", "Agent profile must be an object", 2, { id, file });
|
|
61
|
+
const profile = value;
|
|
62
|
+
if (profile.schema_id !== "dd-flow/agent-profile@1" || profile.id !== id
|
|
63
|
+
|| Object.keys(profile).some(key => !["schema_id", "id", "harness", "provider", "model", "reasoning", "mode", "permission"].includes(key))
|
|
64
|
+
|| !["codex", "zcode", "agy", "opencode", "grok", "droid"].includes(String(profile.harness))
|
|
65
|
+
|| !nonempty(profile.provider) || !nonempty(profile.model) || !nonempty(profile.reasoning) || !nonempty(profile.mode)
|
|
66
|
+
|| (profile.permission !== "allow" && profile.permission !== "deny")) {
|
|
67
|
+
throw new AppError("agent_profile_invalid", "Agent profile does not match dd-flow/agent-profile@1", 2, { id, file });
|
|
68
|
+
}
|
|
69
|
+
// Only execution settings enter RUN evidence; never copy arbitrary profile fields.
|
|
70
|
+
return { schema_id: profile.schema_id, id, harness: profile.harness, provider: profile.provider, model: profile.model, reasoning: profile.reasoning, mode: profile.mode, permission: profile.permission };
|
|
71
|
+
}
|
|
72
|
+
export function snapshotAgentProfiles(ddFlowHome, policy) {
|
|
73
|
+
if (!isExecutionRouting(policy))
|
|
74
|
+
throw new AppError("execution_profile_invalid", "Execution routing contains an invalid stage, profile or delegation policy", 2);
|
|
75
|
+
const profiles = Object.create(null);
|
|
76
|
+
for (const override of [policy, ...Object.values(policy.stage_overrides ?? {})]) {
|
|
77
|
+
for (const id of [override.agent_profile_id, override.delegation?.agent_profile_id]) {
|
|
78
|
+
if (id !== undefined && !Object.hasOwn(profiles, id))
|
|
79
|
+
profiles[id] = loadAgentProfile(ddFlowHome, id);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// Validate every declared route now, including stages not reached by this run.
|
|
83
|
+
for (const stage of [undefined, ...Object.keys(policy.stage_overrides ?? {})]) {
|
|
84
|
+
resolveExecutionPolicy({ ddFlowHome, policy, profiles, ...(stage ? { stage } : {}) });
|
|
85
|
+
}
|
|
86
|
+
return profiles;
|
|
87
|
+
}
|
|
88
|
+
/** Resolves a frozen policy with explicit topology. Same profiles do not turn external work into native work. */
|
|
89
|
+
export function resolveExecutionPolicy(input) {
|
|
90
|
+
if (input.work !== undefined)
|
|
91
|
+
validateExecutionOverride(input.work);
|
|
92
|
+
const profile = (id) => {
|
|
93
|
+
if (!input.profiles)
|
|
94
|
+
return loadAgentProfile(input.ddFlowHome, id);
|
|
95
|
+
if (!Object.hasOwn(input.profiles, id))
|
|
96
|
+
throw new AppError("execution_profile_not_frozen", "Profile is not in this RUN's frozen execution snapshot", 1, { id });
|
|
97
|
+
return input.profiles[id];
|
|
98
|
+
};
|
|
99
|
+
const stage = input.stage ? input.policy.stage_overrides?.[input.stage] : undefined;
|
|
100
|
+
const coordinatorId = stage?.agent_profile_id ?? input.policy.agent_profile_id;
|
|
101
|
+
if (!coordinatorId)
|
|
102
|
+
throw new AppError("execution_profile_missing", "Execution policy needs agent_profile_id before external dispatch", 1, { stage: input.stage ?? null });
|
|
103
|
+
const coordinator = profile(coordinatorId);
|
|
104
|
+
const delegation = input.work?.delegation ?? stage?.delegation ?? input.policy.delegation ?? { mode: "native" };
|
|
105
|
+
validateDelegation(delegation);
|
|
106
|
+
if (input.work?.agent_profile_id && input.work.delegation?.agent_profile_id && input.work.agent_profile_id !== input.work.delegation.agent_profile_id)
|
|
107
|
+
throw new AppError("execution_profile_invalid", "Work selects conflicting worker profiles", 2);
|
|
108
|
+
const worker = profile(input.work?.agent_profile_id ?? delegation.agent_profile_id ?? coordinator.id);
|
|
109
|
+
if (delegation.mode === "native" && harnessKey(worker.harness) !== harnessKey(coordinator.harness)) {
|
|
110
|
+
throw new AppError("native_harness_mismatch", "Native delegation cannot change harness", 2, { coordinator_harness: coordinator.harness, worker_harness: worker.harness });
|
|
111
|
+
}
|
|
112
|
+
if (delegation.mode === "native" && !sameSessionProfile(coordinator, worker))
|
|
113
|
+
throw new AppError("native_profile_override_unqualified", "Native worker profile overrides require qualified adapter support; select external delegation", 2, { coordinator_profile_id: coordinator.id, worker_profile_id: worker.id });
|
|
114
|
+
const resolved = {
|
|
115
|
+
coordinator,
|
|
116
|
+
worker,
|
|
117
|
+
delegation: { mode: delegation.mode, max_parallel: delegation.mode === "external" ? delegation.max_parallel ?? 1 : null }
|
|
118
|
+
};
|
|
119
|
+
return { ...resolved, profile_hash: digest(resolved) };
|
|
120
|
+
}
|
|
121
|
+
export function runtimeHarness(profile) { return harnessKey(profile.harness); }
|
|
122
|
+
/** Profile labels are not execution identity; every session-affecting value is. */
|
|
123
|
+
export function sameSessionProfile(left, right) {
|
|
124
|
+
const settings = (profile) => ({ harness: runtimeHarness(profile), provider: profile.provider, model: profile.model, reasoning: profile.reasoning, mode: profile.mode, permission: profile.permission });
|
|
125
|
+
return digest(settings(left)) === digest(settings(right));
|
|
126
|
+
}
|
|
127
|
+
export function assertSameSessionMerge(input) {
|
|
128
|
+
const merge = resolveExecutionPolicy({ ...input, stage: "merge" }).coordinator;
|
|
129
|
+
for (const stage of input.previousStages) {
|
|
130
|
+
const previous = resolveExecutionPolicy({ ...input, stage }).coordinator;
|
|
131
|
+
if (!sameSessionProfile(previous, merge))
|
|
132
|
+
throw new AppError("merge_profile_session_conflict", "Same-session MERGE cannot switch execution profiles; select a compatible profile or server MERGE", 2, { previous_stage: stage, previous_profile_id: previous.id, merge_profile_id: merge.id });
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
export function resolveStageTransition(input) {
|
|
136
|
+
const target = resolveExecutionPolicy(input);
|
|
137
|
+
const previous = input.fromStage ? resolveExecutionPolicy({ ...input, stage: input.fromStage }).coordinator : null;
|
|
138
|
+
const compatible = !previous || sameSessionProfile(previous, target.coordinator);
|
|
139
|
+
if (input.stage === "merge" && input.mergeMode === "same_session") {
|
|
140
|
+
if (!compatible)
|
|
141
|
+
throw new AppError("merge_profile_session_conflict", "Same-session MERGE cannot replace its current coordinator", 2);
|
|
142
|
+
return { ...target, session_mode: "same_session" };
|
|
143
|
+
}
|
|
144
|
+
return { ...target, session_mode: !compatible || (input.stage === "merge" && input.mergeMode === "server") ? "new_session" : input.stageSessionMode };
|
|
145
|
+
}
|
|
146
|
+
function validateDelegation(value) {
|
|
147
|
+
if (!value || typeof value !== "object" || Array.isArray(value) || Object.keys(value).some(key => !["mode", "agent_profile_id", "max_parallel"].includes(key)))
|
|
148
|
+
throw new AppError("execution_profile_invalid", "Delegation has unsupported fields", 2);
|
|
149
|
+
if (value.mode !== "native" && value.mode !== "external")
|
|
150
|
+
throw new AppError("execution_profile_invalid", "Delegation mode must be native or external", 2);
|
|
151
|
+
if (value.agent_profile_id !== undefined && (typeof value.agent_profile_id !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(value.agent_profile_id)))
|
|
152
|
+
throw new AppError("execution_profile_invalid", "Delegation profile id is invalid", 2);
|
|
153
|
+
if (value.max_parallel !== undefined && (!Number.isInteger(value.max_parallel) || value.max_parallel < 1))
|
|
154
|
+
throw new AppError("execution_profile_invalid", "External max_parallel must be a positive integer", 2);
|
|
155
|
+
if (value.mode === "native" && value.max_parallel !== undefined)
|
|
156
|
+
throw new AppError("execution_profile_invalid", "Native delegation does not accept max_parallel", 2);
|
|
157
|
+
}
|
|
158
|
+
export function validateExecutionOverride(value) {
|
|
159
|
+
if (!value || typeof value !== "object" || Array.isArray(value) || Object.keys(value).some(key => !["agent_profile_id", "delegation"].includes(key)))
|
|
160
|
+
throw new AppError("execution_profile_invalid", "Work execution override has unsupported fields", 2);
|
|
161
|
+
const override = value;
|
|
162
|
+
if (override.agent_profile_id === undefined && override.delegation === undefined)
|
|
163
|
+
throw new AppError("execution_profile_invalid", "Work execution override is empty", 2);
|
|
164
|
+
if (override.agent_profile_id !== undefined && (typeof override.agent_profile_id !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(override.agent_profile_id)))
|
|
165
|
+
throw new AppError("execution_profile_invalid", "Work profile id is invalid", 2);
|
|
166
|
+
if (override.delegation !== undefined)
|
|
167
|
+
validateDelegation(override.delegation);
|
|
168
|
+
}
|
|
169
|
+
function nonempty(value) { return typeof value === "string" && Boolean(value.trim()); }
|
|
170
|
+
function digest(value) { return crypto.createHash("sha256").update(JSON.stringify(value)).digest("hex"); }
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { ensureWorkLaunchStorage } from "../storage/database.js";
|
|
5
|
+
import { AppError } from "../shared/errors.js";
|
|
6
|
+
import { runtimeBudgetForRun } from "./runtime-budget.js";
|
|
7
|
+
import { adapterSessionId, prepareHarnessFlowExecutable, runHarnessAdapter } from "./harness-adapter.js";
|
|
8
|
+
import { harnessRuntimeArguments, resolveHarnessCommand } from "./harness-config.js";
|
|
9
|
+
import { resolveExecutionPolicy, runtimeHarness, validateExecutionOverride } from "./execution-policy.js";
|
|
10
|
+
import { assertRunMutationAllowed } from "./run-recovery.js";
|
|
11
|
+
import { assertWorkLaunchReady, workStartCommand } from "./work-registry.js";
|
|
12
|
+
import { initCodexHome } from "./hooks.js";
|
|
13
|
+
import { managedInvocationContext } from "./lifecycle-invocations.js";
|
|
14
|
+
/**
|
|
15
|
+
* Creates an external provider root for one already-created Work. The launch
|
|
16
|
+
* is not a Work claim: only the worker's trusted `work start` hook binds it.
|
|
17
|
+
*/
|
|
18
|
+
export async function launchExternalWork(context, input) {
|
|
19
|
+
ensureWorkLaunchStorage(context.db);
|
|
20
|
+
const work = context.db.get("SELECT work_id, project_id, run_id, status, payload_json FROM works WHERE work_id = ?", [input.workId]);
|
|
21
|
+
if (!work)
|
|
22
|
+
throw new AppError("not_found", "Work was not found", 1, { work_id: input.workId });
|
|
23
|
+
const run = context.db.get("SELECT run_root, project_root, workspace_root, index_json, status FROM runs WHERE project_id = ? AND id = ?", [work.project_id, work.run_id]);
|
|
24
|
+
if (!run?.run_root)
|
|
25
|
+
throw new AppError("runtime_missing", "Work has no materialized RUN root", 1, { work_id: work.work_id });
|
|
26
|
+
const existing = context.db.get("SELECT operation_id, status, provider_session_id, profile_hash, journal_path FROM work_launches WHERE work_id = ?", [work.work_id]);
|
|
27
|
+
if (existing)
|
|
28
|
+
return { ok: true, work_id: work.work_id, operation_id: existing.operation_id, status: existing.status, provider_session_id: existing.provider_session_id, profile_hash: existing.profile_hash, journal_path: existing.journal_path, idempotent: true };
|
|
29
|
+
assertWorkLaunchReady(context, work.work_id);
|
|
30
|
+
if (run.status !== "running")
|
|
31
|
+
throw new AppError("invalid_run_state", "External Work dispatch requires a running RUN", 2, { run_id: work.run_id, status: run.status });
|
|
32
|
+
const generation = assertRunMutationAllowed(context, work.project_id, work.run_id);
|
|
33
|
+
const snapshot = JSON.parse(run.index_json);
|
|
34
|
+
if (!snapshot.execution_profile?.agent_profiles)
|
|
35
|
+
throw new AppError("execution_profile_not_frozen", "External dispatch requires a RUN with frozen agent profiles; recreate the RUN with the current engine", 1, { work_id: work.work_id });
|
|
36
|
+
const override = workOverride(work.payload_json);
|
|
37
|
+
const policy = snapshot.execution_profile?.settings?.execution ?? {};
|
|
38
|
+
const stage = snapshot.current_stage ?? [...(snapshot.stage_runs ?? [])].reverse().find((entry) => entry.status === "running")?.stage;
|
|
39
|
+
if (input.stage !== undefined && input.stage !== stage)
|
|
40
|
+
throw new AppError("execution_stage_mismatch", "Requested stage does not match the RUN's current stage", 2, { requested_stage: input.stage, current_stage: stage ?? null });
|
|
41
|
+
if (!stage && Object.keys(policy.stage_overrides ?? {}).length)
|
|
42
|
+
throw new AppError("execution_stage_missing", "Stage overrides require an authoritative RUN stage", 1, { run_id: work.run_id });
|
|
43
|
+
const resolved = resolveExecutionPolicy({ ddFlowHome: context.ddFlowHome, policy, profiles: snapshot.execution_profile.agent_profiles, ...(stage ? { stage } : {}), ...(override ? { work: override } : {}) });
|
|
44
|
+
if (resolved.delegation.mode !== "external")
|
|
45
|
+
throw new AppError("external_launch_not_selected", "This Work's frozen policy selects native delegation", 2, { work_id: work.work_id });
|
|
46
|
+
const operationId = `OPR-${crypto.randomUUID()}`;
|
|
47
|
+
const root = path.join(run.run_root, "external-launches", operationId);
|
|
48
|
+
const journal = path.join(root, "adapter.events.jsonl");
|
|
49
|
+
fs.mkdirSync(root, { recursive: true });
|
|
50
|
+
const now = context.now();
|
|
51
|
+
const flowExecutable = prepareHarnessFlowExecutable(context, root);
|
|
52
|
+
const env = { ...context.env, DD_FLOW_BIN: flowExecutable, DD_FLOW_INVOCATION_SCOPE: undefined, DD_FLOW_CURRENT_INVOCATION: undefined };
|
|
53
|
+
// An external Codex root is a fresh thread, so it does not inherit the
|
|
54
|
+
// coordinator's managed PreToolUse hook. Its first `work start` must emit
|
|
55
|
+
// that hook event to bind the Work to the physical session.
|
|
56
|
+
if (runtimeHarness(resolved.worker) === "codex-desktop") {
|
|
57
|
+
const codexHome = path.join(root, "codex-home");
|
|
58
|
+
initCodexHome(context, { projectRoot: run.project_root, profile: `external-${operationId}`, targetHome: codexHome, ...(context.env.CODEX_HOME ? { sourceHome: context.env.CODEX_HOME } : {}) });
|
|
59
|
+
env.CODEX_HOME = codexHome;
|
|
60
|
+
}
|
|
61
|
+
let runtimeContext = { ...context, env };
|
|
62
|
+
const runtimeOwner = { schema_id: "dd-flow/runtime-owner@1", owner_id: `run:${work.run_id}:work:${work.work_id}`, role: "worker", project_id: work.project_id, run_id: work.run_id, work_id: work.work_id, operation_id: operationId, state_dir: root, dd_flow_home: context.ddFlowHome, dd_flow_bin: flowExecutable, resource_home: context.env.DD_FLOW_RESOURCE_HOME ?? context.ddFlowHome, budget: runtimeBudgetForRun(context, work.project_id, work.run_id) };
|
|
63
|
+
const executable = resolveHarnessCommand(context.ddFlowHome, runtimeHarness(resolved.worker), "adapter");
|
|
64
|
+
context.db.run("INSERT INTO work_launches (operation_id, work_id, profile_json, profile_hash, status, state_dir, journal_path, provider_session_id, adapter_receipt_json, created_at, updated_at) VALUES (?, ?, ?, ?, 'launching', ?, ?, NULL, NULL, ?, ?)", [operationId, work.work_id, JSON.stringify({ ...resolved, runtime_adapter: { executable, owner: runtimeOwner } }), resolved.profile_hash, root, journal, now, now]);
|
|
65
|
+
const common = ["--dd-flow-bin", flowExecutable, "--state-dir", root, "--journal", journal, "--cwd", run.workspace_root, "--provider", resolved.worker.provider, "--model", resolved.worker.model, "--reasoning", resolved.worker.reasoning, "--mode", resolved.worker.mode, "--permission", resolved.worker.permission, "--project-root", run.project_root, "--dd-flow-home", context.ddFlowHome, ...harnessRuntimeArguments(context.ddFlowHome, runtimeHarness(resolved.worker)), "--json"];
|
|
66
|
+
const evidence = (value) => fs.appendFileSync(journal, `${JSON.stringify({ type: "adapter_call", at: context.now(), ...value })}\n`);
|
|
67
|
+
try {
|
|
68
|
+
const call = (args, timeoutMs) => runHarnessAdapter({ executable, args, env: runtimeContext.env, operationId: `${operationId}:${args[0]}.${args[1]}`, runtimeOwner, ...(timeoutMs ? { timeoutMs } : {}), ...(input.progress ? { progress: input.progress } : {}), onEvidence: evidence });
|
|
69
|
+
await call(["daemon", "start", ...common]);
|
|
70
|
+
assertRunMutationAllowed(context, work.project_id, work.run_id, generation);
|
|
71
|
+
const created = await call(["session", "create", ...common]);
|
|
72
|
+
const sessionId = adapterSessionId(created);
|
|
73
|
+
if (!sessionId)
|
|
74
|
+
throw new AppError("harness_adapter_invalid", "Harness adapter did not return a typed Session id", 1, { work_id: work.work_id, harness: runtimeHarness(resolved.worker) });
|
|
75
|
+
// Retain the physical root before any subsequent guard or prompt can fail.
|
|
76
|
+
context.db.run("UPDATE work_launches SET provider_session_id = ?, adapter_receipt_json = ?, updated_at = ? WHERE operation_id = ?", [sessionId, JSON.stringify({ create: created }), context.now(), operationId]);
|
|
77
|
+
runtimeContext = managedInvocationContext(runtimeContext, { harness: runtimeHarness(resolved.worker), stateDir: root, sessionId,
|
|
78
|
+
projectRoot: run.project_root, runId: work.run_id, generation });
|
|
79
|
+
assertRunMutationAllowed(context, work.project_id, work.run_id, generation);
|
|
80
|
+
assertWorkLaunchReady(context, work.work_id);
|
|
81
|
+
const promptFile = path.join(root, "launch.md");
|
|
82
|
+
fs.writeFileSync(promptFile, `Start the assigned Work now. Your first tool call must be this exact standalone command:\n\n${workStartCommand(runtimeContext, work)}\n\nUse the packet returned by that command. Do not claim another Work or run this command more than once.\n`);
|
|
83
|
+
const prompted = await call(["session", "prompt", "--session-id", sessionId, "--prompt-file", promptFile, ...common], 45 * 60_000);
|
|
84
|
+
assertRunMutationAllowed(context, work.project_id, work.run_id, generation);
|
|
85
|
+
context.db.run("UPDATE work_launches SET status = 'start_requested', provider_session_id = ?, adapter_receipt_json = ?, updated_at = ? WHERE operation_id = ?", [sessionId, JSON.stringify({ create: created, prompt: prompted }), context.now(), operationId]);
|
|
86
|
+
return { ok: true, work_id: work.work_id, operation_id: operationId, status: "start_requested", session: { harness_id: runtimeHarness(resolved.worker), session_id: sessionId }, profile_hash: resolved.profile_hash, journal_path: journal };
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
context.db.run("UPDATE work_launches SET status = 'failed', adapter_receipt_json = ?, updated_at = ? WHERE operation_id = ?", [JSON.stringify({ error: String(error) }), context.now(), operationId]);
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function workOverride(payload) {
|
|
94
|
+
if (!payload)
|
|
95
|
+
return undefined;
|
|
96
|
+
let value;
|
|
97
|
+
try {
|
|
98
|
+
value = JSON.parse(payload);
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
104
|
+
return undefined;
|
|
105
|
+
const execution = value.execution_override;
|
|
106
|
+
if (execution === undefined)
|
|
107
|
+
return undefined;
|
|
108
|
+
validateExecutionOverride(execution);
|
|
109
|
+
return execution;
|
|
110
|
+
}
|
|
@@ -1,15 +1,73 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
2
5
|
import { AppError } from "../shared/errors.js";
|
|
6
|
+
import { recoveryRpcTimeout } from "./recovery-observation-budget.js";
|
|
7
|
+
const maxAdapterOutputBytes = 1_000_000;
|
|
8
|
+
/** Native hooks need a real executable, not a PATH-dependent shell expression. */
|
|
9
|
+
export function prepareHarnessFlowExecutable(context, stateDir) {
|
|
10
|
+
const configured = context.env.DD_FLOW_BIN;
|
|
11
|
+
if (configured !== undefined) {
|
|
12
|
+
if (!path.isAbsolute(configured) || !fs.existsSync(configured))
|
|
13
|
+
throw new AppError("harness_runtime_missing", "DD_FLOW_BIN must identify an existing absolute executable", 2);
|
|
14
|
+
if (path.basename(configured) === "dd-flow")
|
|
15
|
+
return configured;
|
|
16
|
+
}
|
|
17
|
+
const isolated = path.join(context.ddFlowHome, "bin", "dd-flow");
|
|
18
|
+
if (!configured && fs.existsSync(isolated))
|
|
19
|
+
return isolated;
|
|
20
|
+
const entry = fileURLToPath(new URL("../cli.js", import.meta.url));
|
|
21
|
+
if (!configured && !fs.existsSync(entry))
|
|
22
|
+
throw new AppError("harness_runtime_unbuilt", "Build the CLI or supply its pinned DD_FLOW_BIN before launching a harness", 1);
|
|
23
|
+
// The trusted lifecycle parser recognizes this basename, including for a
|
|
24
|
+
// custom pinned executable. Do not broaden that parser to arbitrary scripts.
|
|
25
|
+
const executable = path.join(stateDir, "dd-flow");
|
|
26
|
+
const dispatch = configured
|
|
27
|
+
? `const { spawnSync } = await import("node:child_process");\nconst child = spawnSync(${JSON.stringify(configured)}, process.argv.slice(2), { stdio: "inherit", env: process.env });\nif (child.error) throw child.error;\nif (child.signal) process.kill(process.pid, child.signal);\nprocess.exit(child.status ?? 1);\n`
|
|
28
|
+
: `await import(${JSON.stringify(pathToFileURL(entry).href)});\n`;
|
|
29
|
+
const contents = `#!${process.execPath}\nprocess.env.DD_FLOW_HOME = ${JSON.stringify(context.ddFlowHome)};\n${dispatch}`;
|
|
30
|
+
try {
|
|
31
|
+
fs.writeFileSync(executable, contents, { flag: "wx", mode: 0o700 });
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
if (error.code !== "EEXIST" || fs.readFileSync(executable, "utf8") !== contents)
|
|
35
|
+
throw new AppError("harness_runtime_binding_conflict", "Launch runtime executable already has different content", 1, { path: executable });
|
|
36
|
+
}
|
|
37
|
+
return executable;
|
|
38
|
+
}
|
|
3
39
|
/** Runs one harness command with the lifecycle guarantees shared by agent services. */
|
|
4
40
|
export function runHarnessAdapter(input) {
|
|
5
|
-
|
|
41
|
+
// `null` is deliberate: a productive Turn has no adapter-wall-clock limit.
|
|
42
|
+
// Preserve it instead of treating it like an omitted short control request.
|
|
43
|
+
const timeoutMs = recoveryRpcTimeout(input.timeoutMs === undefined ? 120_000 : input.timeoutMs);
|
|
44
|
+
if (!input.operationId.trim())
|
|
45
|
+
throw new AppError("operation_id_required", "Harness calls require a scoped operation id", 2);
|
|
46
|
+
// The current adapter transport uses this name; its identity is runtime-owned,
|
|
47
|
+
// never the operation or trusted daemon identity of the calling coordinator.
|
|
48
|
+
const env = { ...input.env, DD_EVAL_OPERATION_ID: input.operationId };
|
|
49
|
+
delete env.DD_FLOW_DAEMON_ID;
|
|
50
|
+
delete env.DD_FLOW_SCOPE_CONTROL_TOKEN;
|
|
51
|
+
delete env.DD_FLOW_RUNTIME_OWNER;
|
|
52
|
+
if (input.runtimeOwner) {
|
|
53
|
+
if (!path.isAbsolute(input.executable) || input.runtimeOwner.adapter_executable !== undefined && input.runtimeOwner.adapter_executable !== input.executable)
|
|
54
|
+
throw new AppError("harness_runtime_binding_conflict", "Owned harness call must use its frozen absolute adapter executable", 1);
|
|
55
|
+
env.DD_FLOW_RUNTIME_OWNER = JSON.stringify({ ...input.runtimeOwner, adapter_executable: input.executable });
|
|
56
|
+
}
|
|
6
57
|
return new Promise((resolve, reject) => {
|
|
58
|
+
if (input.signal?.aborted)
|
|
59
|
+
return reject(new AppError("harness_adapter_aborted", "Harness adapter call was cancelled before dispatch", 1));
|
|
7
60
|
const detached = process.platform !== "win32";
|
|
8
|
-
const child = spawn(input.executable, input.args, { env: input.
|
|
61
|
+
const child = spawn(input.executable, input.args, { env, ...(input.cwd ? { cwd: input.cwd } : {}), detached, stdio: ["ignore", "pipe", "pipe"] });
|
|
9
62
|
let stdout = "";
|
|
10
63
|
let stderr = "";
|
|
64
|
+
let outputTooLarge = false;
|
|
11
65
|
let timedOut = false;
|
|
66
|
+
let aborted = false;
|
|
12
67
|
let killTimer;
|
|
68
|
+
let settled = false;
|
|
69
|
+
let callbackError;
|
|
70
|
+
let spawnError;
|
|
13
71
|
const terminate = (signal) => { try {
|
|
14
72
|
if (detached && child.pid)
|
|
15
73
|
process.kill(-child.pid, signal);
|
|
@@ -17,43 +75,108 @@ export function runHarnessAdapter(input) {
|
|
|
17
75
|
child.kill(signal);
|
|
18
76
|
}
|
|
19
77
|
catch { /* The process already settled. */ } };
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
78
|
+
const stopTransport = () => { terminate("SIGTERM"); killTimer ??= setTimeout(() => terminate("SIGKILL"), 5_000); };
|
|
79
|
+
const deliver = (callback) => {
|
|
80
|
+
try {
|
|
81
|
+
const result = callback();
|
|
82
|
+
if (result && typeof result.then === "function") {
|
|
83
|
+
void Promise.resolve(result).catch(() => { });
|
|
84
|
+
throw new AppError("progress_callback_async", "Harness callbacks must be synchronous", 1);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
callbackError ??= error;
|
|
89
|
+
if (!settled)
|
|
90
|
+
stopTransport();
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
const heartbeat = input.progress && input.progressMessage ? setInterval(() => deliver(() => input.progress?.(`${input.progressMessage}; waiting for adapter completion`)), 15_000) : undefined;
|
|
94
|
+
const timer = timeoutMs === null ? undefined : setTimeout(() => { timedOut = true; stopTransport(); }, timeoutMs);
|
|
95
|
+
const abort = () => { aborted = true; stopTransport(); };
|
|
96
|
+
if (input.signal?.aborted)
|
|
97
|
+
abort();
|
|
98
|
+
else
|
|
99
|
+
input.signal?.addEventListener("abort", abort, { once: true });
|
|
100
|
+
const evidence = (value) => input.onEvidence?.({ operation_id: input.operationId, executable: input.executable, args: input.args, ...value, at: new Date().toISOString() });
|
|
101
|
+
const append = (current, chunk) => {
|
|
102
|
+
if (Buffer.byteLength(current) + Buffer.byteLength(chunk) > maxAdapterOutputBytes) {
|
|
103
|
+
outputTooLarge = true;
|
|
104
|
+
stopTransport();
|
|
105
|
+
return current;
|
|
106
|
+
}
|
|
107
|
+
return current + chunk;
|
|
108
|
+
};
|
|
109
|
+
child.stdout.setEncoding("utf8").on("data", (chunk) => { stdout = append(stdout, chunk); });
|
|
110
|
+
child.stderr.setEncoding("utf8").on("data", (chunk) => { stderr = append(stderr, chunk); });
|
|
111
|
+
child.once("error", (error) => { spawnError = error; });
|
|
28
112
|
child.once("close", (status, signal) => {
|
|
29
|
-
|
|
113
|
+
if (settled)
|
|
114
|
+
return;
|
|
115
|
+
settled = true;
|
|
116
|
+
if (timer)
|
|
117
|
+
clearTimeout(timer);
|
|
118
|
+
input.signal?.removeEventListener("abort", abort);
|
|
30
119
|
if (heartbeat)
|
|
31
120
|
clearInterval(heartbeat);
|
|
32
|
-
if (killTimer)
|
|
121
|
+
if (killTimer) {
|
|
33
122
|
clearTimeout(killTimer);
|
|
34
|
-
|
|
123
|
+
terminate("SIGKILL");
|
|
124
|
+
}
|
|
125
|
+
const progressFailure = callbackError;
|
|
126
|
+
deliver(() => evidence({ status, signal, timed_out: timedOut, aborted, stdout, stderr, ...(spawnError ? { error: spawnError.message } : {}) }));
|
|
127
|
+
if (spawnError)
|
|
128
|
+
return reject(new AppError("harness_adapter_failed", "Harness adapter call failed", 1, { cause: adapterCause(spawnError.message), ...(callbackError ? { evidence_error: String(callbackError) } : {}) }));
|
|
129
|
+
if (outputTooLarge)
|
|
130
|
+
return reject(new AppError("harness_adapter_output_limit", "Harness adapter exceeded the bounded reply limit", 1, { executable: input.executable, args: input.args.slice(0, 3), max_output_bytes: maxAdapterOutputBytes }));
|
|
35
131
|
if (timedOut)
|
|
36
|
-
return reject(new AppError("
|
|
132
|
+
return reject(new AppError("harness_adapter_timeout", "Harness adapter call timed out", 1, { executable: input.executable, args: input.args.slice(0, 3), timeout_ms: timeoutMs }));
|
|
133
|
+
if (aborted)
|
|
134
|
+
return reject(new AppError("harness_adapter_aborted", "Harness adapter call was cancelled", 1, { executable: input.executable, args: input.args.slice(0, 3) }));
|
|
37
135
|
if (status !== 0)
|
|
38
|
-
return reject(new AppError("
|
|
136
|
+
return reject(progressFailure ?? new AppError("harness_adapter_failed", "Harness adapter call failed", 1, { executable: input.executable, args: input.args.slice(0, 3), status, signal, stderr, cause: adapterCause(stdout, stderr), ...(callbackError ? { evidence_error: String(callbackError) } : {}) }));
|
|
137
|
+
if (callbackError)
|
|
138
|
+
return reject(callbackError);
|
|
39
139
|
try {
|
|
40
|
-
|
|
140
|
+
const reply = JSON.parse(stdout);
|
|
141
|
+
if (!reply || typeof reply !== "object" || Array.isArray(reply))
|
|
142
|
+
throw new Error("not object");
|
|
143
|
+
const value = reply;
|
|
144
|
+
if (value.ok === false)
|
|
145
|
+
return reject(new AppError("harness_adapter_failed", "Harness adapter reported a runtime failure", 1, { cause: adapterCause(stdout) }));
|
|
146
|
+
resolve(reply);
|
|
41
147
|
}
|
|
42
148
|
catch {
|
|
43
|
-
reject(new AppError("
|
|
149
|
+
reject(new AppError("harness_adapter_invalid", "Harness adapter returned one invalid JSON object", 1, { executable: input.executable }));
|
|
44
150
|
}
|
|
45
151
|
});
|
|
46
152
|
});
|
|
47
153
|
}
|
|
154
|
+
function adapterCause(...outputs) {
|
|
155
|
+
for (const output of outputs)
|
|
156
|
+
for (const line of [output.trim(), ...output.trim().split("\n").reverse()]) {
|
|
157
|
+
try {
|
|
158
|
+
const value = JSON.parse(line);
|
|
159
|
+
const nested = value.error && typeof value.error === "object" && !Array.isArray(value.error) ? value.error : null;
|
|
160
|
+
const code = typeof nested?.code === "string" ? nested.code : typeof value.code === "string" ? value.code : null;
|
|
161
|
+
const message = typeof nested?.message === "string" ? nested.message : typeof value.error === "string" ? value.error : code;
|
|
162
|
+
if (code)
|
|
163
|
+
return { code, message, ...(nested?.details ?? value.details ? { details: nested?.details ?? value.details } : {}), ...((nested?.retryable ?? value.retryable) === true ? { retryable: true } : {}) };
|
|
164
|
+
}
|
|
165
|
+
catch { /* Adapter diagnostics need not be JSON. */ }
|
|
166
|
+
}
|
|
167
|
+
return { message: outputs.map(output => output.trim()).filter(Boolean).join("\n") || "adapter process failed" };
|
|
168
|
+
}
|
|
48
169
|
export function adapterSessionId(value) {
|
|
49
170
|
if (!value || typeof value !== "object")
|
|
50
171
|
return null;
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
172
|
+
const reply = value;
|
|
173
|
+
for (const key of ["provider_session_id", "adapter_session_id", "session_id"])
|
|
174
|
+
if (typeof reply[key] === "string" && reply[key].trim())
|
|
175
|
+
return reply[key];
|
|
176
|
+
const session = reply.session;
|
|
177
|
+
if (session && typeof session === "object" && !Array.isArray(session))
|
|
178
|
+
for (const key of ["provider_session_id", "session_id"])
|
|
179
|
+
if (typeof session[key] === "string" && String(session[key]).trim())
|
|
180
|
+
return session[key];
|
|
58
181
|
return null;
|
|
59
182
|
}
|
|
@@ -10,6 +10,7 @@ export function harnessKey(value) {
|
|
|
10
10
|
zcode: "zcode-acp", "zcode-acp": "zcode-acp",
|
|
11
11
|
grok: "grok-acp", "grok-acp": "grok-acp",
|
|
12
12
|
agy: "antigravity-cli", "antigravity-cli": "antigravity-cli",
|
|
13
|
+
droid: "droid-cli", "droid-cli": "droid-cli",
|
|
13
14
|
opencode: "opencode-server", "opencode-server": "opencode-server"
|
|
14
15
|
};
|
|
15
16
|
const resolved = keys[value];
|
|
@@ -60,6 +61,7 @@ export function harnessRuntimeArguments(ddFlowHome, harness) {
|
|
|
60
61
|
"zcode-acp": "--zcode-acp-bin",
|
|
61
62
|
"grok-acp": "--grok-bin",
|
|
62
63
|
"antigravity-cli": "--agy-bin",
|
|
64
|
+
"droid-cli": "--droid-bin",
|
|
63
65
|
"opencode-server": "--opencode-bin"
|
|
64
66
|
};
|
|
65
67
|
return [option[key], runtime];
|