@deksden-com/dd-flow-cli 0.9.0-beta.13 → 0.9.0-beta.134
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 +687 -0
- package/README.md +101 -1
- package/dist/build-info.json +10 -10
- package/dist/cli/argument-values.js +38 -0
- package/dist/cli/command-inputs.js +306 -0
- package/dist/cli/help.js +88 -12
- package/dist/cli/hook-ingress.js +78 -0
- package/dist/cli/input-preparation.js +116 -0
- package/dist/cli/native-hook-ingress.js +80 -0
- package/dist/cli/run-cli.js +1465 -450
- package/dist/cli/runtime-maintenance-diagnostics.js +46 -0
- package/dist/cli/runtime-maintenance-ingress.js +52 -0
- package/dist/cli/runtime-process.js +153 -0
- package/dist/cli.js +17 -2
- package/dist/domain/stage-catalog.js +1 -1
- package/dist/harness-runtime/DD-ZCODE.md +99 -0
- package/dist/harness-runtime/bin/dd-agy.mjs +56 -0
- package/dist/harness-runtime/bin/dd-codex.mjs +27 -0
- package/dist/harness-runtime/bin/dd-droid.mjs +34 -0
- package/dist/harness-runtime/bin/dd-grok.mjs +40 -0
- package/dist/harness-runtime/bin/dd-opencode.mjs +22 -0
- package/dist/harness-runtime/bin/dd-zcode.mjs +87 -0
- package/dist/harness-runtime/lib/adapter-timeouts.d.mts +5 -0
- package/dist/harness-runtime/lib/adapter-timeouts.mjs +34 -0
- package/dist/harness-runtime/lib/agy-hooks.d.mts +1 -0
- package/dist/harness-runtime/lib/agy-hooks.mjs +9 -0
- package/dist/harness-runtime/lib/codex-capacity-policy.d.mts +10 -0
- package/dist/harness-runtime/lib/codex-capacity-policy.mjs +89 -0
- package/dist/harness-runtime/lib/daemon-observation.d.mts +2 -0
- package/dist/harness-runtime/lib/daemon-observation.mjs +163 -0
- package/dist/harness-runtime/lib/daemon-operations.mjs +370 -0
- package/dist/harness-runtime/lib/dd-agy-daemon.d.mts +6 -0
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +693 -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 +190 -0
- package/dist/harness-runtime/lib/dd-codex.d.mts +1 -0
- package/dist/harness-runtime/lib/dd-codex.mjs +805 -0
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +128 -0
- package/dist/harness-runtime/lib/dd-droid.mjs +561 -0
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +490 -0
- package/dist/harness-runtime/lib/dd-grok.mjs +177 -0
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +255 -0
- package/dist/harness-runtime/lib/dd-opencode.mjs +128 -0
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +703 -0
- package/dist/harness-runtime/lib/dd-zcode.mjs +1128 -0
- package/dist/harness-runtime/lib/delegation-instructions.d.mts +13 -0
- package/dist/harness-runtime/lib/delegation-instructions.mjs +144 -0
- package/dist/harness-runtime/lib/dispatch-fence.mjs +14 -0
- package/dist/harness-runtime/lib/driver-recovery.mjs +139 -0
- package/dist/harness-runtime/lib/droid-observation.mjs +66 -0
- package/dist/harness-runtime/lib/lease-renewal.d.mts +8 -0
- package/dist/harness-runtime/lib/lease-renewal.mjs +36 -0
- package/dist/harness-runtime/lib/lifecycle-command.d.mts +20 -0
- package/dist/harness-runtime/lib/lifecycle-command.mjs +272 -0
- package/dist/harness-runtime/lib/lifecycle-contract.d.mts +10 -0
- package/dist/harness-runtime/lib/lifecycle-contract.mjs +19 -0
- package/dist/harness-runtime/lib/managed-daemon.d.mts +12 -0
- package/dist/harness-runtime/lib/managed-daemon.mjs +619 -0
- package/dist/harness-runtime/lib/model-observations.mjs +101 -0
- package/dist/harness-runtime/lib/native-children.d.mts +14 -0
- package/dist/harness-runtime/lib/native-children.mjs +94 -0
- package/dist/harness-runtime/lib/native-hook-command.d.mts +7 -0
- package/dist/harness-runtime/lib/native-hook-command.mjs +251 -0
- package/dist/harness-runtime/lib/native-streams.mjs +45 -0
- package/dist/harness-runtime/lib/observation-clock.d.mts +18 -0
- package/dist/harness-runtime/lib/observation-clock.mjs +92 -0
- package/dist/harness-runtime/lib/operation-context.mjs +5 -0
- package/dist/harness-runtime/lib/operation-errors.d.mts +14 -0
- package/dist/harness-runtime/lib/operation-errors.mjs +116 -0
- package/dist/harness-runtime/lib/operation-progress.d.mts +7 -0
- package/dist/harness-runtime/lib/operation-progress.mjs +112 -0
- package/dist/harness-runtime/lib/process-json.d.mts +5 -0
- package/dist/harness-runtime/lib/process-json.mjs +201 -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/setup-progress.d.mts +9 -0
- package/dist/harness-runtime/lib/setup-progress.mjs +51 -0
- package/dist/harness-runtime/lib/tool-observations.d.mts +21 -0
- package/dist/harness-runtime/lib/tool-observations.mjs +228 -0
- package/dist/harness-runtime/lib/zcode-usage.mjs +55 -0
- package/dist/protocol/local-files.js +18 -9
- package/dist/runtime/context.js +8 -4
- package/dist/schemas/agent-profile.schema.json +1 -1
- package/dist/schemas/code-review-result.schema.json +1 -1
- package/dist/schemas/code-verification.schema.json +7 -5
- package/dist/schemas/code-work-batch.schema.json +2 -2
- package/dist/schemas/flow-run.schema.json +2 -0
- package/dist/schemas/plan-review-decision.schema.json +3 -3
- package/dist/schemas/plan-review-result.schema.json +2 -2
- package/dist/schemas/run-control-receipt.schema.json +118 -0
- package/dist/schemas/run-control-request.schema.json +36 -0
- package/dist/schemas/vnext-protocol-plan.schema.json +2 -2
- package/dist/services/branch-context.js +3 -3
- package/dist/services/canon.js +11 -8
- package/dist/services/check-runner-evidence.js +121 -0
- package/dist/services/cleanup.js +133 -40
- package/dist/services/cli-operation-classifier.js +22 -24
- package/dist/services/code-checks.js +851 -109
- package/dist/services/codex-capacity.js +367 -0
- package/dist/services/codex-hook-delivery.js +32 -0
- package/dist/services/command-context.js +80 -0
- package/dist/services/compatibility-preflight.js +13 -3
- package/dist/services/config.js +11 -8
- package/dist/services/continuation-outcome.js +20 -0
- package/dist/services/controller-fanout.js +136 -0
- package/dist/services/dashboard.js +135 -56
- package/dist/services/engines.js +245 -119
- package/dist/services/eval-snapshots.js +1146 -105
- package/dist/services/execution-policy.js +181 -0
- package/dist/services/external-work-launch.js +230 -0
- package/dist/services/harness-adapter.js +329 -36
- package/dist/services/harness-config.js +2 -0
- package/dist/services/hooks.js +753 -325
- package/dist/services/lanes.js +78 -65
- package/dist/services/lifecycle-command.js +1 -288
- package/dist/services/lifecycle-contract.js +1 -0
- package/dist/services/lifecycle-invocations.js +1828 -0
- package/dist/services/managed-daemon-binding.js +38 -0
- package/dist/services/managed-lease-monitor.js +133 -0
- package/dist/services/managed-processes.js +409 -51
- package/dist/services/merge-queue.js +228 -149
- package/dist/services/merge-server.js +163 -35
- package/dist/services/migrations.js +37 -16
- package/dist/services/native-daemon-history.js +49 -0
- package/dist/services/native-session-control.js +39 -0
- package/dist/services/plan-runtime.js +26 -11
- package/dist/services/plans.js +28 -25
- package/dist/services/projects.js +20 -15
- package/dist/services/prompts.js +28 -17
- package/dist/services/protocols.js +163 -87
- package/dist/services/recovery-observation-budget.js +121 -0
- package/dist/services/recovery-snapshot-database.js +145 -0
- package/dist/services/repair-intents.js +42 -0
- package/dist/services/review-copy.js +157 -0
- package/dist/services/review-deferral.js +23 -0
- package/dist/services/run-control-receipt.js +56 -0
- package/dist/services/run-control-worker.js +411 -0
- package/dist/services/run-control.js +985 -0
- package/dist/services/run-controller-adapter.js +399 -0
- package/dist/services/run-controller-capture.js +179 -0
- package/dist/services/run-controller-process.js +187 -0
- package/dist/services/run-controller-recovery.js +289 -0
- package/dist/services/run-controller-state.js +97 -0
- package/dist/services/run-controller.js +1035 -0
- package/dist/services/run-engine-bindings.js +1 -1
- package/dist/services/run-fork.js +147 -0
- package/dist/services/run-observations.js +152 -0
- package/dist/services/run-projection.js +22 -64
- package/dist/services/run-recovery-guard.js +75 -0
- package/dist/services/run-recovery-runtime.js +112 -0
- package/dist/services/run-recovery.js +349 -0
- package/dist/services/runs.js +558 -217
- package/dist/services/runtime-budget.js +430 -0
- package/dist/services/runtime-command.js +43 -0
- package/dist/services/runtime-scope-capture.js +52 -0
- package/dist/services/runtime-scope-control.js +468 -0
- package/dist/services/runtime-scope-resume.js +599 -0
- package/dist/services/runtime-scope-stop.js +180 -0
- package/dist/services/runtime-scope-worker.js +287 -0
- package/dist/services/runtime-service.js +96 -0
- package/dist/services/schema-validation.js +10 -8
- package/dist/services/scope-writer-cleanup.js +27 -0
- package/dist/services/sessions.js +35 -69
- package/dist/services/stage-blocker.js +17 -7
- package/dist/services/stage-context.js +185 -19
- package/dist/services/stage-lifecycle.js +98 -107
- package/dist/services/stage-pause.js +89 -75
- package/dist/services/stage-report-renderer.js +158 -6
- package/dist/services/stage-work-graph.js +35 -0
- package/dist/services/tooling.js +32 -23
- package/dist/services/usage.js +49 -101
- package/dist/services/verification-matrix.js +374 -0
- package/dist/services/vnext-code-review.js +222 -87
- package/dist/services/vnext-code.js +324 -251
- package/dist/services/vnext-contracts.js +10 -1
- package/dist/services/vnext-execution-profile.js +79 -4
- package/dist/services/vnext-fanout.js +182 -17
- package/dist/services/vnext-merge.js +335 -113
- package/dist/services/vnext-plan-review.js +276 -126
- package/dist/services/vnext-plan.js +227 -102
- package/dist/services/vnext-protocolize.js +335 -82
- package/dist/services/vnext-specify.js +119 -81
- package/dist/services/work-registry.js +1083 -194
- package/dist/services/workspace-bootstrap.js +87 -0
- package/dist/services/workspace-files.js +85 -0
- package/dist/services/worktrees.js +98 -44
- package/dist/shared/entity-references.js +8 -0
- package/dist/shared/errors.js +17 -0
- package/dist/storage/agent-output.js +26 -0
- package/dist/storage/database.js +785 -37
- package/dist/storage/foreign-key-transaction.js +39 -0
- package/dist/storage/immutable-input.js +45 -0
- package/dist/storage/import-guard.js +102 -0
- package/dist/storage/native-cleanup.js +52 -0
- package/dist/storage/paths.js +17 -4
- package/dist/storage/projection-lock.js +73 -0
- package/dist/storage/work-references.js +23 -0
- package/dist/storage/writer-contract.js +77 -0
- package/dist/storage/writer-migration.js +85 -0
- package/package.json +30 -13
- package/tools/repair-paused-run-status.mjs +84 -0
|
@@ -0,0 +1,181 @@
|
|
|
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
|
+
/** The RUN identity root and a native Session's fixed launch directory are distinct. */
|
|
136
|
+
export function stageExecutionCwd(input) {
|
|
137
|
+
return path.resolve(input.stage === "specify" || input.stage === "protocolize" ? input.projectRoot
|
|
138
|
+
: input.stage === "merge" ? input.mergeTargetWorkspace ?? input.projectRoot
|
|
139
|
+
: input.workspaceRoot);
|
|
140
|
+
}
|
|
141
|
+
export function resolveStageTransition(input) {
|
|
142
|
+
const target = resolveExecutionPolicy(input);
|
|
143
|
+
const previous = input.fromStage ? resolveExecutionPolicy({ ...input, stage: input.fromStage }).coordinator : null;
|
|
144
|
+
const compatible = !previous || sameSessionProfile(previous, target.coordinator);
|
|
145
|
+
const previousCwd = input.fromStage ? stageExecutionCwd({ ...input, stage: input.fromStage }) : null;
|
|
146
|
+
const targetCwd = stageExecutionCwd(input);
|
|
147
|
+
const sameWorkspace = previousCwd === null || previousCwd === targetCwd;
|
|
148
|
+
if (input.stage === "merge" && input.mergeMode === "same_session") {
|
|
149
|
+
if (!compatible)
|
|
150
|
+
throw new AppError("merge_profile_session_conflict", "Same-session MERGE cannot replace its current coordinator", 2);
|
|
151
|
+
if (!sameWorkspace)
|
|
152
|
+
throw new AppError("merge_session_workspace_conflict", "Same-session MERGE cannot change the native coordinator workspace", 2, { source_workspace: previousCwd, target_workspace: targetCwd });
|
|
153
|
+
return { ...target, session_mode: "same_session" };
|
|
154
|
+
}
|
|
155
|
+
return { ...target, session_mode: !compatible || !sameWorkspace || (input.stage === "merge" && input.mergeMode === "server") ? "new_session" : input.stageSessionMode };
|
|
156
|
+
}
|
|
157
|
+
function validateDelegation(value) {
|
|
158
|
+
if (!value || typeof value !== "object" || Array.isArray(value) || Object.keys(value).some(key => !["mode", "agent_profile_id", "max_parallel"].includes(key)))
|
|
159
|
+
throw new AppError("execution_profile_invalid", "Delegation has unsupported fields", 2);
|
|
160
|
+
if (value.mode !== "native" && value.mode !== "external")
|
|
161
|
+
throw new AppError("execution_profile_invalid", "Delegation mode must be native or external", 2);
|
|
162
|
+
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)))
|
|
163
|
+
throw new AppError("execution_profile_invalid", "Delegation profile id is invalid", 2);
|
|
164
|
+
if (value.max_parallel !== undefined && (!Number.isInteger(value.max_parallel) || value.max_parallel < 1))
|
|
165
|
+
throw new AppError("execution_profile_invalid", "External max_parallel must be a positive integer", 2);
|
|
166
|
+
if (value.mode === "native" && value.max_parallel !== undefined)
|
|
167
|
+
throw new AppError("execution_profile_invalid", "Native delegation does not accept max_parallel", 2);
|
|
168
|
+
}
|
|
169
|
+
export function validateExecutionOverride(value) {
|
|
170
|
+
if (!value || typeof value !== "object" || Array.isArray(value) || Object.keys(value).some(key => !["agent_profile_id", "delegation"].includes(key)))
|
|
171
|
+
throw new AppError("execution_profile_invalid", "Work execution override has unsupported fields", 2);
|
|
172
|
+
const override = value;
|
|
173
|
+
if (override.agent_profile_id === undefined && override.delegation === undefined)
|
|
174
|
+
throw new AppError("execution_profile_invalid", "Work execution override is empty", 2);
|
|
175
|
+
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)))
|
|
176
|
+
throw new AppError("execution_profile_invalid", "Work profile id is invalid", 2);
|
|
177
|
+
if (override.delegation !== undefined)
|
|
178
|
+
validateDelegation(override.delegation);
|
|
179
|
+
}
|
|
180
|
+
function nonempty(value) { return typeof value === "string" && Boolean(value.trim()); }
|
|
181
|
+
function digest(value) { return crypto.createHash("sha256").update(JSON.stringify(value)).digest("hex"); }
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
5
|
+
import { ensureWorkLaunchStorage } from "../storage/database.js";
|
|
6
|
+
import { AppError } from "../shared/errors.js";
|
|
7
|
+
import { runtimeBudgetForRun } from "./runtime-budget.js";
|
|
8
|
+
import { adapterSessionId, prepareHarnessFlowExecutable, runHarnessAdapter } from "./harness-adapter.js";
|
|
9
|
+
import { harnessRuntimeArguments, resolveHarnessCommand } from "./harness-config.js";
|
|
10
|
+
import { resolveExecutionPolicy, runtimeHarness, validateExecutionOverride } from "./execution-policy.js";
|
|
11
|
+
import { assertRunMutationAllowed } from "./run-recovery.js";
|
|
12
|
+
import { assertWorkLaunchReady, reviewSourceInputs, workStartCommand } from "./work-registry.js";
|
|
13
|
+
import { initCodexHome } from "./hooks.js";
|
|
14
|
+
import { managedInvocationContext } from "./lifecycle-invocations.js";
|
|
15
|
+
import { reviewExecutionCopyHash, reviewSourceFilter, reviewSourceHash, reviewSourcePaths } from "./review-copy.js";
|
|
16
|
+
import { workspaceGitFiles } from "./workspace-files.js";
|
|
17
|
+
import { qualifyAgyHooks } from "../harness-runtime/lib/dd-agy-daemon.mjs";
|
|
18
|
+
import { lifecycleRetryInstruction } from "../harness-runtime/lib/delegation-instructions.mjs";
|
|
19
|
+
import { continueCodexCapacity } from "./codex-capacity.js";
|
|
20
|
+
import { errorRecord } from "./run-controller-state.js";
|
|
21
|
+
/**
|
|
22
|
+
* Creates an external provider root for one already-created Work. The launch
|
|
23
|
+
* is not a Work claim: only the worker's trusted `work start` hook binds it.
|
|
24
|
+
*/
|
|
25
|
+
export async function launchExternalWork(context, input) {
|
|
26
|
+
ensureWorkLaunchStorage(context.db);
|
|
27
|
+
const work = context.db.get("SELECT work_id, project_id, run_id, status, payload_json FROM works WHERE work_id = ?", [input.workId]);
|
|
28
|
+
if (!work)
|
|
29
|
+
throw new AppError("not_found", "Work was not found", 1, { work_id: input.workId });
|
|
30
|
+
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]);
|
|
31
|
+
if (!run?.run_root)
|
|
32
|
+
throw new AppError("runtime_missing", "Work has no materialized RUN root", 1, { work_id: work.work_id });
|
|
33
|
+
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]);
|
|
34
|
+
if (existing)
|
|
35
|
+
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 };
|
|
36
|
+
assertWorkLaunchReady(context, work.work_id);
|
|
37
|
+
if (run.status !== "running")
|
|
38
|
+
throw new AppError("invalid_run_state", "External Work dispatch requires a running RUN", 2, { run_id: work.run_id, status: run.status });
|
|
39
|
+
const generation = assertRunMutationAllowed(context, work.project_id, work.run_id);
|
|
40
|
+
const snapshot = JSON.parse(run.index_json);
|
|
41
|
+
if (!snapshot.execution_profile?.agent_profiles)
|
|
42
|
+
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 });
|
|
43
|
+
const override = workOverride(work.payload_json);
|
|
44
|
+
const policy = snapshot.execution_profile?.settings?.execution ?? {};
|
|
45
|
+
const stage = snapshot.current_stage ?? [...(snapshot.stage_runs ?? [])].reverse().find((entry) => entry.status === "running")?.stage;
|
|
46
|
+
if (input.stage !== undefined && input.stage !== stage)
|
|
47
|
+
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 });
|
|
48
|
+
if (!stage && Object.keys(policy.stage_overrides ?? {}).length)
|
|
49
|
+
throw new AppError("execution_stage_missing", "Stage overrides require an authoritative RUN stage", 1, { run_id: work.run_id });
|
|
50
|
+
const resolved = resolveExecutionPolicy({ ddFlowHome: context.ddFlowHome, policy, profiles: snapshot.execution_profile.agent_profiles, ...(stage ? { stage } : {}), ...(override ? { work: override } : {}) });
|
|
51
|
+
if (resolved.delegation.mode !== "external")
|
|
52
|
+
throw new AppError("external_launch_not_selected", "This Work's frozen policy selects native delegation", 2, { work_id: work.work_id });
|
|
53
|
+
const agyWorker = runtimeHarness(resolved.worker) === "antigravity-cli";
|
|
54
|
+
if (agyWorker)
|
|
55
|
+
await qualifyAgyHooks(run.workspace_root);
|
|
56
|
+
const operationId = `OPR-${crypto.randomUUID()}`;
|
|
57
|
+
const root = path.join(run.run_root, "external-launches", operationId);
|
|
58
|
+
const journal = path.join(root, "adapter.events.jsonl");
|
|
59
|
+
fs.mkdirSync(root, { recursive: true });
|
|
60
|
+
const isolatedWorkspace = isReadOnlyWork(work.payload_json) ? path.join(root, "read-only-workspace") : null;
|
|
61
|
+
const reviewInputs = isolatedWorkspace ? reviewSourceInputs(context, work.work_id) : [];
|
|
62
|
+
const reviewBase = snapshot.workspace?.base && snapshot.workspace.base !== "unknown" ? snapshot.workspace.base : "HEAD";
|
|
63
|
+
let reviewCopy = null;
|
|
64
|
+
if (isolatedWorkspace) {
|
|
65
|
+
const allowed = reviewSourceFilter(run.workspace_root, reviewInputs, reviewBase);
|
|
66
|
+
const before = reviewSourceHash(run.workspace_root, reviewInputs, reviewBase);
|
|
67
|
+
fs.cpSync(run.workspace_root, isolatedWorkspace, { recursive: true, verbatimSymlinks: true, filter: allowed });
|
|
68
|
+
if (agyWorker)
|
|
69
|
+
await qualifyAgyHooks(isolatedWorkspace);
|
|
70
|
+
const after = reviewSourceHash(run.workspace_root, reviewInputs, reviewBase), copy = reviewExecutionCopyHash(isolatedWorkspace, run.workspace_root, false, reviewInputs, reviewBase);
|
|
71
|
+
if (before !== after || after !== copy)
|
|
72
|
+
throw new AppError("read_only_input_changed", "Read-only execution copy differs from its authoritative source", 2, { work_id: work.work_id, before, after, copy });
|
|
73
|
+
writeReviewGitEvidence(run.workspace_root, isolatedWorkspace, snapshot.workspace?.base, reviewInputs);
|
|
74
|
+
if (reviewSourceHash(run.workspace_root, reviewInputs, reviewBase) !== after)
|
|
75
|
+
throw new AppError("read_only_input_changed", "Authoritative review workspace changed while collecting Git evidence", 2, { work_id: work.work_id });
|
|
76
|
+
reviewCopy = { root: path.relative(run.run_root, isolatedWorkspace).split(path.sep).join("/"), sha256: reviewExecutionCopyHash(isolatedWorkspace, run.workspace_root, true, reviewInputs, reviewBase), source_sha256: after };
|
|
77
|
+
}
|
|
78
|
+
const now = context.now();
|
|
79
|
+
const flowExecutable = prepareHarnessFlowExecutable(context, root);
|
|
80
|
+
const env = { ...context.env, DD_FLOW_BIN: flowExecutable, DD_FLOW_INVOCATION_SCOPE: undefined, DD_FLOW_CURRENT_INVOCATION: undefined, ...(isolatedWorkspace ? { DD_FLOW_READ_ONLY_WORKSPACE: isolatedWorkspace } : {}) };
|
|
81
|
+
// An external Codex root is a fresh thread, so it does not inherit the
|
|
82
|
+
// coordinator's managed PreToolUse hook. Its first `work start` must emit
|
|
83
|
+
// that hook event to bind the Work to the physical session.
|
|
84
|
+
if (runtimeHarness(resolved.worker) === "codex-desktop") {
|
|
85
|
+
const codexHome = path.join(root, "codex-home");
|
|
86
|
+
initCodexHome(context, { projectRoot: run.project_root, profile: `external-${operationId}`, targetHome: codexHome, ...(context.env.CODEX_HOME ? { sourceHome: context.env.CODEX_HOME } : {}) });
|
|
87
|
+
env.CODEX_HOME = codexHome;
|
|
88
|
+
}
|
|
89
|
+
let runtimeContext = { ...context, env };
|
|
90
|
+
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) };
|
|
91
|
+
const executable = resolveHarnessCommand(context.ddFlowHome, runtimeHarness(resolved.worker), "adapter");
|
|
92
|
+
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 }, ...(reviewCopy ? { review_copy: reviewCopy } : {}) }), resolved.profile_hash, root, journal, now, now]);
|
|
93
|
+
const common = ["--dd-flow-bin", flowExecutable, "--state-dir", root, "--journal", journal, "--cwd", isolatedWorkspace ?? 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"];
|
|
94
|
+
const evidence = (value) => fs.appendFileSync(journal, `${JSON.stringify({ type: "adapter_call", at: context.now(), ...value })}\n`);
|
|
95
|
+
try {
|
|
96
|
+
const call = (args, timeoutMs, id = `${operationId}:${args[0]}.${args[1]}`) => runHarnessAdapter({ executable, args, harness: runtimeHarness(resolved.worker), env: runtimeContext.env, operationId: id, runtimeOwner, ...(timeoutMs !== undefined ? { timeoutMs } : {}), ...(input.signal ? { signal: input.signal } : {}), ...(input.progress ? { progress: input.progress } : {}), onEvidence: evidence, onNativeOutcome: value => {
|
|
97
|
+
if (args[0] !== "session" || args[1] !== "create")
|
|
98
|
+
return;
|
|
99
|
+
const nativeId = adapterSessionId(value);
|
|
100
|
+
if (nativeId)
|
|
101
|
+
context.db.run("UPDATE work_launches SET provider_session_id = ?, adapter_receipt_json = ?, updated_at = ? WHERE operation_id = ?", [nativeId, JSON.stringify({ create: value, settlement_pending: true }), context.now(), operationId]);
|
|
102
|
+
} });
|
|
103
|
+
await call(["daemon", "start", ...common]);
|
|
104
|
+
assertRunMutationAllowed(context, work.project_id, work.run_id, generation);
|
|
105
|
+
const created = await call(["session", "create", ...common]);
|
|
106
|
+
const sessionId = adapterSessionId(created);
|
|
107
|
+
if (!sessionId)
|
|
108
|
+
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) });
|
|
109
|
+
// Retain the physical root before any subsequent guard or prompt can fail.
|
|
110
|
+
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]);
|
|
111
|
+
runtimeContext = managedInvocationContext(runtimeContext, { harness: runtimeHarness(resolved.worker), stateDir: root, sessionId,
|
|
112
|
+
projectRoot: run.project_root, runId: work.run_id, generation });
|
|
113
|
+
assertRunMutationAllowed(context, work.project_id, work.run_id, generation);
|
|
114
|
+
assertWorkLaunchReady(context, work.work_id);
|
|
115
|
+
if (reviewCopy && reviewSourceHash(run.workspace_root, reviewInputs, reviewBase) !== reviewCopy.source_sha256)
|
|
116
|
+
throw new AppError("read_only_input_changed", "Authoritative review workspace changed after the execution copy was made", 2, { work_id: work.work_id });
|
|
117
|
+
const promptFile = path.join(root, "launch.md");
|
|
118
|
+
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\n${lifecycleRetryInstruction}${isolatedWorkspace ? `\n\nThe review copy has no .git. Read the captured status/diff or gitless note in ${path.join(isolatedWorkspace, ".dd-flow", "review-evidence")}; do not run Git inside the copy.\n` : "\n"}`);
|
|
119
|
+
const promptOperationId = `${operationId}:session.prompt`;
|
|
120
|
+
context.db.run("UPDATE work_launches SET adapter_receipt_json = ?, updated_at = ? WHERE operation_id = ? AND status = 'launching'", [JSON.stringify({ create: created, progress_policy: "native-inactivity@1" }), context.now(), operationId]);
|
|
121
|
+
let prompted;
|
|
122
|
+
try {
|
|
123
|
+
prompted = await call(["session", "prompt", "--session-id", sessionId, "--prompt-file", promptFile, ...common], null);
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
if (runtimeHarness(resolved.worker) !== "codex-desktop")
|
|
127
|
+
throw error;
|
|
128
|
+
const retained = context.db.get("SELECT adapter_receipt_json FROM work_launches WHERE operation_id = ?", [operationId]);
|
|
129
|
+
const capacityIntents = retained?.adapter_receipt_json ? JSON.parse(retained.adapter_receipt_json).capacity_intents ?? [] : [];
|
|
130
|
+
const capacityFailures = retained?.adapter_receipt_json ? JSON.parse(retained.adapter_receipt_json).capacity_failures ?? [] : [];
|
|
131
|
+
const retainCapacity = () => {
|
|
132
|
+
const changed = context.db.run("UPDATE work_launches SET adapter_receipt_json = ?, updated_at = ? WHERE operation_id = ? AND status = 'launching'", [JSON.stringify({ create: created, progress_policy: "native-inactivity@1", capacity_intents: capacityIntents, capacity_failures: capacityFailures }), context.now(), operationId]);
|
|
133
|
+
if (changed.changes !== 1)
|
|
134
|
+
throw new AppError("capacity_continuation_blocked", "External Work launch owner changed", 1);
|
|
135
|
+
};
|
|
136
|
+
const resumed = await continueCodexCapacity({ error, sessionId, operationId: promptOperationId, allowSettledDescendants: true,
|
|
137
|
+
...(input.signal ? { signal: input.signal } : {}),
|
|
138
|
+
continuationText: `Continue only Work ${work.work_id} in RUN ${work.run_id}, Stage ${stage ?? "current"}, in the same Session. Inspect its current durable state and the retained Work packet. Never repeat Work start or a completed tool, accepted answer, check or lifecycle command. Use only the current exact issued successor and finish at the original Work boundary.`,
|
|
139
|
+
retainedIntents: () => capacityIntents,
|
|
140
|
+
retainedFailures: () => capacityFailures,
|
|
141
|
+
retainFailure: (_id, refusal) => { capacityFailures.push(refusal); retainCapacity(); },
|
|
142
|
+
inspectOperation: id => call(["daemon", "operation", "--state-dir", root, "--operation-id", id, "--json"], 30_000, `${id}:capacity-observe`),
|
|
143
|
+
inspectSession: turnId => call(["session", "inspect", "--session-id", sessionId, ...(turnId ? ["--turn-id", turnId] : []), ...common], 30_000, `${operationId}:capacity-inspect`),
|
|
144
|
+
assertCurrent: () => {
|
|
145
|
+
assertRunMutationAllowed(context, work.project_id, work.run_id, generation);
|
|
146
|
+
const current = context.db.get("SELECT w.status, l.status AS launch_status FROM works w JOIN work_launches l ON l.work_id = w.work_id WHERE w.work_id = ? AND l.operation_id = ?", [work.work_id, operationId]);
|
|
147
|
+
if (!current || !["created", "running", "paused", "completed"].includes(current.status) || current.launch_status !== "launching")
|
|
148
|
+
throw new AppError("capacity_continuation_blocked", "External Work changed before its native continuation", 1, { work_id: work.work_id, state: current });
|
|
149
|
+
if (reviewCopy && reviewSourceHash(run.workspace_root, reviewInputs, reviewBase) !== reviewCopy.source_sha256)
|
|
150
|
+
throw new AppError("read_only_input_changed", "Authoritative review workspace changed during capacity wait", 2);
|
|
151
|
+
},
|
|
152
|
+
decision: () => {
|
|
153
|
+
const status = context.db.get("SELECT status FROM works WHERE work_id = ?", [work.work_id])?.status;
|
|
154
|
+
return status === "completed" ? "completed" : status === "paused" ? "paused" : "continue";
|
|
155
|
+
},
|
|
156
|
+
retainIntent: intent => {
|
|
157
|
+
capacityIntents.push(intent);
|
|
158
|
+
retainCapacity();
|
|
159
|
+
},
|
|
160
|
+
prompt: (id, text, ordinal) => {
|
|
161
|
+
const file = path.join(root, `capacity-${ordinal}.md`);
|
|
162
|
+
if (fs.existsSync(file)) {
|
|
163
|
+
if (fs.readFileSync(file, "utf8") !== text)
|
|
164
|
+
throw new AppError("capacity_continuation_conflict", "Retained Work continuation prompt differs", 1);
|
|
165
|
+
}
|
|
166
|
+
else
|
|
167
|
+
fs.writeFileSync(file, text, { flag: "wx", mode: 0o600 });
|
|
168
|
+
return call(["session", "prompt", "--session-id", sessionId, "--prompt-file", file, ...common], null, id);
|
|
169
|
+
} });
|
|
170
|
+
if (!resumed)
|
|
171
|
+
throw error;
|
|
172
|
+
prompted = resumed;
|
|
173
|
+
}
|
|
174
|
+
assertRunMutationAllowed(context, work.project_id, work.run_id, generation);
|
|
175
|
+
const saved = context.db.get("SELECT adapter_receipt_json FROM work_launches WHERE operation_id = ?", [operationId]);
|
|
176
|
+
context.db.run("UPDATE work_launches SET status = 'start_requested', provider_session_id = ?, adapter_receipt_json = ?, updated_at = ? WHERE operation_id = ?", [sessionId, JSON.stringify({ ...(saved?.adapter_receipt_json ? JSON.parse(saved.adapter_receipt_json) : {}), create: created, prompt: prompted }), context.now(), operationId]);
|
|
177
|
+
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 };
|
|
178
|
+
}
|
|
179
|
+
catch (error) {
|
|
180
|
+
const saved = context.db.get("SELECT adapter_receipt_json FROM work_launches WHERE operation_id = ?", [operationId]);
|
|
181
|
+
context.db.run("UPDATE work_launches SET status = 'failed', adapter_receipt_json = ?, updated_at = ? WHERE operation_id = ?", [JSON.stringify({ ...(saved?.adapter_receipt_json ? JSON.parse(saved.adapter_receipt_json) : {}), error: errorRecord(error) }), context.now(), operationId]);
|
|
182
|
+
throw error;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
export function writeReviewGitEvidence(source, copy, base, explicitInputs = []) {
|
|
186
|
+
const directory = path.join(copy, ".dd-flow", "review-evidence");
|
|
187
|
+
fs.mkdirSync(directory, { recursive: true });
|
|
188
|
+
if (workspaceGitFiles(source, ["--cached"]) === null) {
|
|
189
|
+
fs.writeFileSync(path.join(directory, "gitless.txt"), "Source workspace is not a Git repository. Review the copied files and accepted RUN evidence.\n");
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
if (!base || base === "unknown")
|
|
193
|
+
throw new AppError("review_git_evidence_missing", "Git review source has no frozen base commit", 1, { workspace_root: source });
|
|
194
|
+
const paths = (reviewSourcePaths(source, explicitInputs, base) ?? []).map(file => `:(literal)${file}`);
|
|
195
|
+
for (const [name, args] of [["status.txt", ["status", "--short", "--untracked-files=all"]], ["diff.patch", ["diff", "--no-ext-diff", "--no-renames", "--binary", base]]]) {
|
|
196
|
+
const result = paths.length ? spawnSync("git", ["-C", source, ...args, "--", ...paths], { encoding: "utf8", maxBuffer: 64 * 1024 * 1024 }) : { status: 0, stdout: "", stderr: "", error: undefined };
|
|
197
|
+
if (result.status !== 0 || result.error)
|
|
198
|
+
throw new AppError("review_git_evidence_failed", "Could not collect read-only Git evidence for reviewer", 1, { workspace_root: source, command: name, cause: result.error?.message ?? result.stderr });
|
|
199
|
+
fs.writeFileSync(path.join(directory, name), result.stdout);
|
|
200
|
+
}
|
|
201
|
+
fs.writeFileSync(path.join(directory, "base.txt"), `${base}\n`);
|
|
202
|
+
}
|
|
203
|
+
function workOverride(payload) {
|
|
204
|
+
if (!payload)
|
|
205
|
+
return undefined;
|
|
206
|
+
let value;
|
|
207
|
+
try {
|
|
208
|
+
value = JSON.parse(payload);
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
return undefined;
|
|
212
|
+
}
|
|
213
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
214
|
+
return undefined;
|
|
215
|
+
const execution = value.execution_override;
|
|
216
|
+
if (execution === undefined)
|
|
217
|
+
return undefined;
|
|
218
|
+
validateExecutionOverride(execution);
|
|
219
|
+
return execution;
|
|
220
|
+
}
|
|
221
|
+
function isReadOnlyWork(payload) {
|
|
222
|
+
if (!payload)
|
|
223
|
+
return false;
|
|
224
|
+
try {
|
|
225
|
+
return JSON.parse(payload).read_only === true;
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
}
|