@deksden-com/dd-flow-cli 0.9.0-beta.1 → 0.9.0-beta.100
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 +561 -0
- package/README.md +68 -0
- package/dist/build-info.json +8 -8
- package/dist/cli/command-inputs.js +305 -0
- package/dist/cli/help.js +91 -15
- package/dist/cli/hook-ingress.js +82 -0
- package/dist/cli/input-preparation.js +116 -0
- package/dist/cli/run-cli.js +1506 -334
- package/dist/cli.js +6 -2
- package/dist/harness-runtime/DD-ZCODE.md +99 -0
- package/dist/harness-runtime/bin/dd-agy.mjs +39 -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 +449 -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 +468 -0
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +300 -0
- package/dist/harness-runtime/lib/dd-grok.mjs +174 -0
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +217 -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 +941 -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 +267 -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/harness-runtime/lib/zcode-usage.mjs +54 -0
- package/dist/runtime/context.js +3 -1
- 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/harness-config.schema.json +23 -0
- package/dist/schemas/plan-review-decision.schema.json +1 -1
- 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 +75 -21
- package/dist/services/cli-operation-classifier.js +22 -24
- package/dist/services/code-checks.js +676 -79
- package/dist/services/codex-hook-delivery.js +28 -0
- package/dist/services/command-context.js +80 -0
- package/dist/services/config.js +11 -8
- package/dist/services/continuation-outcome.js +20 -0
- package/dist/services/controller-fanout.js +134 -0
- package/dist/services/dashboard.js +103 -31
- package/dist/services/engines.js +125 -80
- package/dist/services/eval-snapshots.js +838 -74
- 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 +68 -0
- package/dist/services/hooks.js +609 -310
- package/dist/services/lanes.js +62 -53
- package/dist/services/lifecycle-command.js +103 -17
- package/dist/services/lifecycle-contract.js +30 -0
- package/dist/services/lifecycle-invocations.js +1128 -0
- package/dist/services/managed-daemon-binding.js +38 -0
- package/dist/services/managed-processes.js +425 -0
- package/dist/services/merge-queue.js +179 -104
- package/dist/services/merge-server.js +78 -27
- 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/portable-refs.js +57 -0
- package/dist/services/projects.js +18 -5
- package/dist/services/prompts.js +27 -18
- 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/repair-intents.js +40 -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 +136 -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 +803 -0
- package/dist/services/run-engine-bindings.js +20 -62
- package/dist/services/run-fork.js +138 -0
- package/dist/services/run-observations.js +112 -0
- package/dist/services/run-projection.js +10 -8
- package/dist/services/run-recovery-runtime.js +69 -0
- package/dist/services/run-recovery.js +343 -0
- package/dist/services/runs.js +437 -81
- package/dist/services/runtime-budget.js +439 -0
- package/dist/services/runtime-command.js +43 -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 +21 -19
- package/dist/services/session-identity.js +19 -0
- package/dist/services/sessions.js +61 -80
- package/dist/services/stage-blocker.js +17 -7
- package/dist/services/stage-context.js +44 -18
- package/dist/services/stage-lifecycle.js +112 -108
- package/dist/services/stage-pause.js +117 -73
- package/dist/services/stage-work-graph.js +35 -0
- package/dist/services/usage.js +108 -132
- package/dist/services/vnext-code-review.js +245 -98
- package/dist/services/vnext-code.js +355 -257
- package/dist/services/vnext-execution-profile.js +5 -3
- package/dist/services/vnext-fanout.js +166 -26
- package/dist/services/vnext-merge.js +413 -147
- package/dist/services/vnext-plan-review.js +182 -109
- package/dist/services/vnext-plan.js +199 -82
- package/dist/services/vnext-protocolize.js +133 -60
- package/dist/services/vnext-specify.js +94 -71
- package/dist/services/work-registry.js +813 -160
- package/dist/services/workspace-bootstrap.js +76 -0
- package/dist/services/worktrees.js +85 -35
- package/dist/shared/entity-references.js +8 -0
- package/dist/shared/errors.js +12 -0
- package/dist/storage/database.js +569 -16
- package/dist/storage/paths.js +17 -4
- package/dist/storage/work-references.js +23 -0
- package/dist/storage/writer-contract.js +77 -0
- package/dist/storage/writer-migration.js +102 -0
- package/package.json +24 -13
- package/tools/audit-runtime-fix-boundaries.mjs +96 -0
- package/tools/repair-paused-run-status.mjs +59 -0
|
@@ -1,35 +1,59 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
2
|
import fs from "node:fs";
|
|
3
|
-
import os from "node:os";
|
|
4
3
|
import path from "node:path";
|
|
4
|
+
import { quote } from "shell-quote";
|
|
5
|
+
import { lifecycleRetryCommands, managedLifecycleCommand } from "./lifecycle-invocations.js";
|
|
5
6
|
import { AppError } from "../shared/errors.js";
|
|
7
|
+
import { entityReferenceVariants, shortEntityReference } from "../shared/entity-references.js";
|
|
6
8
|
import { resolveProjectRoot } from "../storage/paths.js";
|
|
9
|
+
import { resolveWorkReference } from "../storage/work-references.js";
|
|
7
10
|
import { findRecentMatchingHookEvent, claimStageResumeHookEvent, stageResumeMatchKey } from "./hooks.js";
|
|
8
11
|
import { requireProjectByRoot } from "./projects.js";
|
|
9
12
|
import { appendFlowRunTimelineEvent, pauseFlowRunStage, resumeFlowRunStage } from "./runs.js";
|
|
10
|
-
import { refreshRunWorkProjection } from "./work-registry.js";
|
|
11
|
-
|
|
13
|
+
import { bindSessionForResume, refreshRunWorkProjection } from "./work-registry.js";
|
|
14
|
+
import { assertRunMutationAllowed } from "./run-recovery.js";
|
|
15
|
+
import { runtimeCommand } from "./runtime-command.js";
|
|
16
|
+
export { flowCommand } from "./runtime-command.js";
|
|
17
|
+
export function prepareStagePause(context, input) {
|
|
18
|
+
if (typeof input.question !== "string" || !input.question.trim())
|
|
19
|
+
throw new AppError("validation", "stage pause requires a non-empty user question", 2);
|
|
20
|
+
safeStage(input.stage);
|
|
12
21
|
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
13
22
|
const project = requireProjectByRoot(context, projectRoot);
|
|
14
23
|
const run = requireRun(context, project.id, input.runId);
|
|
24
|
+
assertRunMutationAllowed(context, project.id, run.id);
|
|
15
25
|
const work = requireWork(context, project.id, run.id, input.workId);
|
|
16
26
|
if (work.status !== "running")
|
|
17
27
|
throw new AppError("invalid_work_state", "Only a running Work can pause", 1, { work_id: work.work_id, status: work.status });
|
|
18
28
|
const question = input.question;
|
|
19
|
-
if (!question.trim())
|
|
20
|
-
throw new AppError("validation", "stage pause requires a non-empty user question", 2);
|
|
21
29
|
const stage = requireStage(run, input.stage, "running");
|
|
30
|
+
requireRunHome(run);
|
|
31
|
+
return { projectRoot, project, run, work, question, stage };
|
|
32
|
+
}
|
|
33
|
+
export function pauseStageForUser(context, input) {
|
|
34
|
+
const { projectRoot, project, run, work, question, stage } = prepareStagePause(context, input);
|
|
22
35
|
const pauseId = nextPauseId(requireRunHome(run));
|
|
23
36
|
const pauseRoot = path.join(requireRunHome(run), "intake", "hitl", `${pauseId}-${safeStage(input.stage)}`);
|
|
24
37
|
const questionPath = path.join(pauseRoot, "question.md");
|
|
25
38
|
fs.mkdirSync(pauseRoot, { recursive: true });
|
|
26
39
|
fs.writeFileSync(questionPath, question);
|
|
27
40
|
const now = context.now();
|
|
28
|
-
context.db.
|
|
29
|
-
|
|
41
|
+
context.db.beginWriteTransaction();
|
|
42
|
+
try {
|
|
43
|
+
const paused = context.db.run("UPDATE works SET status = 'paused', updated_at = ? WHERE work_id = ? AND status = 'running'", [now, work.work_id]);
|
|
44
|
+
if (paused.changes !== 1)
|
|
45
|
+
throw new AppError("invalid_work_state", "Work changed before pause could be accepted", 1, { work_id: work.work_id });
|
|
46
|
+
pauseFlowRunStage(context, { projectRoot, runId: run.id, stage: stage.stage, workId: work.work_id, pauseId, questionPath });
|
|
47
|
+
context.db.exec("COMMIT");
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
context.db.exec("ROLLBACK");
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
30
53
|
refreshRunWorkProjection(context, project.id, run.id);
|
|
31
|
-
const
|
|
32
|
-
const
|
|
54
|
+
const answerInputPath = path.join(pauseRoot, "answer-input.md");
|
|
55
|
+
const resume = stageResumeCommand(context, { runId: run.id, stage: stage.stage, workId: work.work_id, projectRoot, answerInputPath });
|
|
56
|
+
const resumeTemplate = `Write the complete user answer exactly to ${quote([answerInputPath])}, then run:\n${resume}`;
|
|
33
57
|
return {
|
|
34
58
|
ok: true,
|
|
35
59
|
outcome: "paused",
|
|
@@ -38,63 +62,93 @@ export function pauseStageForUser(context, input) {
|
|
|
38
62
|
stage: stage.stage,
|
|
39
63
|
pause: { id: pauseId, reason: "waiting_for_user", question_path: questionPath, user_message: question },
|
|
40
64
|
next_action: "ask_user_then_resume_same_stage",
|
|
41
|
-
agent_instruction:
|
|
65
|
+
agent_instruction: `Send user_message to the user and stop this Turn. When the user answers, write the complete raw answer without interpretation to ${answerInputPath}, then make resume_command the first flow command and follow the returned continuation prompt. Do not create a RUN, Work, attempt, or stage start.`,
|
|
42
66
|
resume_command: resume,
|
|
43
67
|
resume_command_template: resumeTemplate
|
|
44
68
|
};
|
|
45
69
|
}
|
|
46
|
-
export function
|
|
70
|
+
export function prepareStageResume(context, input) {
|
|
71
|
+
if (typeof input.answer !== "string" || !input.answer.trim())
|
|
72
|
+
throw new AppError("validation", "stage resume requires a non-empty user answer", 2);
|
|
73
|
+
safeStage(input.stage);
|
|
47
74
|
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
48
75
|
const project = requireProjectByRoot(context, projectRoot);
|
|
49
76
|
const run = requireRun(context, project.id, input.runId);
|
|
77
|
+
assertRunMutationAllowed(context, project.id, run.id);
|
|
50
78
|
const work = requireWork(context, project.id, run.id, input.workId);
|
|
51
79
|
if (work.status !== "paused")
|
|
52
80
|
throw new AppError("invalid_work_state", "stage resume requires its paused Work", 1, { work_id: work.work_id, status: work.status });
|
|
53
81
|
const stage = requireStage(run, input.stage, "paused");
|
|
54
82
|
if (!stage.pause || stage.pause.work_id !== work.work_id || stage.pause.answer_path)
|
|
55
83
|
throw new AppError("invalid_stage_state", "Stage has no unanswered pause for this Work", 1, { run_id: run.id, stage: stage.stage, work_id: work.work_id });
|
|
56
|
-
|
|
57
|
-
|
|
84
|
+
const workSession = context.db.get("SELECT id, session_id, prompt_path, result_path, status FROM work_sessions WHERE work_id = ? AND status = 'running' ORDER BY created_at DESC LIMIT 1", [work.work_id]);
|
|
85
|
+
if (!workSession)
|
|
86
|
+
throw new AppError("runtime_missing", "Paused Work has no open Session link", 1, { work_id: work.work_id });
|
|
87
|
+
let evidence = input.evidence;
|
|
88
|
+
if (evidence && (evidence.pauseId !== stage.pause.id || evidence.promptPath !== workSession.prompt_path || evidence.questionPath !== stage.pause.question_path))
|
|
89
|
+
throw new AppError("runtime_state_changed", "Paused Work changed after resume preparation", 1, { work_id: work.work_id });
|
|
90
|
+
if (!evidence) {
|
|
91
|
+
try {
|
|
92
|
+
evidence = { pauseId: stage.pause.id, promptPath: workSession.prompt_path, questionPath: stage.pause.question_path, prompt: fs.readFileSync(workSession.prompt_path, "utf8"), question: fs.readFileSync(stage.pause.question_path, "utf8") };
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
throw new AppError("runtime_missing", "Paused Work continuation artifacts cannot be read", 1, { work_id: work.work_id, cause: String(error) });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return { projectRoot, project, run, work, stage, pause: stage.pause, workSession, evidence };
|
|
99
|
+
}
|
|
100
|
+
export function resumeStageAfterUser(context, input) {
|
|
101
|
+
const { projectRoot, project, run, work, stage, pause, workSession, evidence } = prepareStageResume(context, input);
|
|
58
102
|
const hookEventId = input.hookEventId ?? findRecentMatchingHookEvent(context, {
|
|
59
103
|
projectId: project.id,
|
|
60
|
-
matchKey:
|
|
104
|
+
matchKey: entityReferenceVariants(run.id).flatMap((runReference) => entityReferenceVariants(work.work_id).map((workReference) => stageResumeMatchKey(runReference, stage.stage, workReference, projectRoot))),
|
|
61
105
|
errorCode: "trusted_stage_resume_required",
|
|
62
106
|
operation: "stage resume"
|
|
63
107
|
}).eventKey;
|
|
64
|
-
const identity = claimStageResumeHookEvent(context, {
|
|
65
|
-
projectId: project.id,
|
|
66
|
-
runId: run.id,
|
|
67
|
-
stage: stage.stage,
|
|
68
|
-
workId: work.work_id,
|
|
69
|
-
projectRoot,
|
|
70
|
-
eventKey: hookEventId
|
|
71
|
-
});
|
|
72
|
-
const answerPath = path.join(path.dirname(stage.pause.question_path), "answer.md");
|
|
73
|
-
fs.writeFileSync(answerPath, input.answer);
|
|
74
108
|
const now = context.now();
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
109
|
+
const answerPath = path.join(path.dirname(pause.question_path), `answer-${crypto.createHash("sha256").update(`${pause.id}\0${input.answer}`).digest("hex")}.md`);
|
|
110
|
+
context.db.beginWriteTransaction();
|
|
111
|
+
let identity;
|
|
112
|
+
try {
|
|
113
|
+
identity = claimStageResumeHookEvent(context, { projectId: project.id, runId: run.id, stage: stage.stage, workId: work.work_id, projectRoot, eventKey: hookEventId });
|
|
114
|
+
try {
|
|
115
|
+
fs.writeFileSync(answerPath, input.answer, { flag: "wx", mode: 0o600 });
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
if (error.code !== "EEXIST" || fs.readFileSync(answerPath, "utf8") !== input.answer)
|
|
119
|
+
throw error;
|
|
120
|
+
}
|
|
121
|
+
if (workSession.session_id !== identity.sessionId) {
|
|
122
|
+
bindSessionForResume(context, work.work_id, identity, now);
|
|
123
|
+
context.db.run("UPDATE work_sessions SET status = 'completed', updated_at = ?, completed_at = ? WHERE id = ?", [now, now, workSession.id]);
|
|
124
|
+
context.db.run("INSERT INTO work_sessions (id, work_id, session_id, hook_event_id, status, prompt_path, result_path, created_at, updated_at, completed_at) VALUES (?, ?, ?, ?, 'running', ?, ?, ?, ?, NULL)", [`WSES-${crypto.randomUUID()}`, work.work_id, identity.sessionId, identity.hookEventId, workSession.prompt_path, workSession.result_path, now, now]);
|
|
125
|
+
}
|
|
126
|
+
const resumed = context.db.run("UPDATE works SET status = 'running', updated_at = ? WHERE work_id = ? AND status = 'paused'", [now, work.work_id]);
|
|
127
|
+
if (resumed.changes !== 1)
|
|
128
|
+
throw new AppError("invalid_work_state", "Work changed before resume could be accepted", 1, { work_id: work.work_id });
|
|
129
|
+
resumeFlowRunStage(context, { projectRoot, runId: run.id, stage: stage.stage, workId: work.work_id, answerPath });
|
|
130
|
+
context.db.exec("COMMIT");
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
context.db.exec("ROLLBACK");
|
|
134
|
+
throw error;
|
|
81
135
|
}
|
|
82
|
-
context.db.run("UPDATE works SET status = 'running', updated_at = ? WHERE work_id = ? AND status = 'paused'", [now, work.work_id]);
|
|
83
|
-
resumeFlowRunStage(context, { projectRoot, runId: run.id, stage: stage.stage, workId: work.work_id, answerPath });
|
|
84
136
|
refreshRunWorkProjection(context, project.id, run.id);
|
|
85
137
|
appendFlowRunTimelineEvent(context, project.id, run.id, { type: "stage_resume_session_bound", stage: stage.stage, work_id: work.work_id, session_id: identity.sessionId });
|
|
86
|
-
const prompt =
|
|
138
|
+
const prompt = evidence.prompt;
|
|
139
|
+
const retryCommands = lifecycleRetryCommands(context, stage.stage);
|
|
87
140
|
const continuation = [
|
|
88
141
|
prompt.trimEnd(),
|
|
89
142
|
"",
|
|
90
143
|
"<hitl_resume>",
|
|
91
|
-
`- Pause: ${
|
|
92
|
-
`- Question source: ${
|
|
144
|
+
`- Pause: ${pause.id}`,
|
|
145
|
+
`- Question source: ${pause.question_path}`,
|
|
93
146
|
`- Answer source: ${answerPath}`,
|
|
94
147
|
"- Continue the same stage, Work and attempt. Do not call stage start or repeat completed preparation.",
|
|
148
|
+
...(retryCommands.length ? ["- A prior no-effect lifecycle rejection now has these authoritative replacement commands; correct the rejected input and use only these replacements:", ...retryCommands.map(command => ` ${command}`)] : []),
|
|
95
149
|
"",
|
|
96
150
|
"<user_question>",
|
|
97
|
-
|
|
151
|
+
evidence.question,
|
|
98
152
|
"</user_question>",
|
|
99
153
|
"",
|
|
100
154
|
"<user_answer>",
|
|
@@ -103,18 +157,18 @@ export function resumeStageAfterUser(context, input) {
|
|
|
103
157
|
"</hitl_resume>",
|
|
104
158
|
""
|
|
105
159
|
].join("\n");
|
|
106
|
-
return { ok: true, outcome: "resumed", run_id: run.id, work_id: work.work_id, stage: stage.stage, pause_id:
|
|
160
|
+
return { ok: true, outcome: "resumed", run_id: run.id, work_id: work.work_id, stage: stage.stage, pause_id: pause.id, question_path: pause.question_path, answer_path: answerPath, prompt_path: workSession.prompt_path, worker_prompt_markdown: continuation, next_action: `continue_${stage.stage}` };
|
|
107
161
|
}
|
|
108
162
|
export function stagePauseCommand(context, input) {
|
|
109
|
-
|
|
163
|
+
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
164
|
+
const run = requireRun(context, project.id, input.runId);
|
|
165
|
+
const questionInputPath = path.join(requireRunHome(run), "works", input.workId, "question-input.md");
|
|
166
|
+
return managedLifecycleCommand(context, runtimeCommand(context, ["stage", "pause", shortEntityReference(input.runId), "--stage", input.stage, "--work", shortEntityReference(input.workId), "--question-file", questionInputPath, "--project-root", input.projectRoot, "--json"]));
|
|
110
167
|
}
|
|
111
|
-
/**
|
|
112
|
-
* The only shell form allowed for an agent-owned HITL pause. Supplying a
|
|
113
|
-
* complete heredoc makes stdin explicit without asking an agent to invent a
|
|
114
|
-
* pipe, a temporary file or a second command.
|
|
115
|
-
*/
|
|
168
|
+
/** Render separate file-write and lifecycle-call instructions for HITL. */
|
|
116
169
|
export function stagePauseCommandTemplate(command) {
|
|
117
|
-
|
|
170
|
+
const questionFile = command.match(/--question-file\s+(?:'([^']*)'|"([^"]*)"|(\S+))/)?.slice(1).find(Boolean) ?? "<question-file-from-command>";
|
|
171
|
+
return `Write this text to ${questionFile}:
|
|
118
172
|
## Q-001 — <short decision title>
|
|
119
173
|
|
|
120
174
|
Why this decision is required:
|
|
@@ -129,51 +183,41 @@ Recommendation:
|
|
|
129
183
|
|
|
130
184
|
Effect on scope or acceptance:
|
|
131
185
|
- <one sentence>
|
|
132
|
-
|
|
186
|
+
|
|
187
|
+
Then run this standalone command:
|
|
188
|
+
${command}`;
|
|
133
189
|
}
|
|
134
190
|
function stageResumeCommand(context, input) {
|
|
135
|
-
return
|
|
136
|
-
}
|
|
137
|
-
export function flowCommand(context) {
|
|
138
|
-
const defaultHome = path.resolve(path.join(os.homedir(), ".dd-flow"));
|
|
139
|
-
// Desktop/ACP tool processes are not required to inherit the environment of
|
|
140
|
-
// the adapter that created their Session. In an isolated runtime, a bare
|
|
141
|
-
// `dd-flow` can therefore select the host-global router despite a correct
|
|
142
|
-
// DD_FLOW_HOME. The runner supplies an absolute executable for that case;
|
|
143
|
-
// make every generated lifecycle command self-contained.
|
|
144
|
-
const executable = context.env.DD_FLOW_BIN;
|
|
145
|
-
if (executable && path.isAbsolute(executable)) {
|
|
146
|
-
return `DD_FLOW_HOME=${JSON.stringify(context.ddFlowHome)} ${JSON.stringify(executable)}`;
|
|
147
|
-
}
|
|
148
|
-
const isolatedExecutable = path.join(context.ddFlowHome, "bin", "dd-flow");
|
|
149
|
-
if (context.ddFlowHome !== defaultHome && fs.existsSync(isolatedExecutable)) {
|
|
150
|
-
return `DD_FLOW_HOME=${JSON.stringify(context.ddFlowHome)} ${JSON.stringify(isolatedExecutable)}`;
|
|
151
|
-
}
|
|
152
|
-
return context.ddFlowHome === defaultHome ? "dd-flow" : `DD_FLOW_HOME=${JSON.stringify(context.ddFlowHome)} dd-flow`;
|
|
191
|
+
return managedLifecycleCommand(context, runtimeCommand(context, ["stage", "resume", shortEntityReference(input.runId), "--stage", input.stage, "--work", shortEntityReference(input.workId), "--answer-file", input.answerInputPath, "--project-root", input.projectRoot, "--json"]));
|
|
153
192
|
}
|
|
154
193
|
function requireRun(context, projectId, id) {
|
|
155
|
-
const rows = context.db.all("SELECT id, short_id, project_id, project_root,
|
|
194
|
+
const rows = context.db.all("SELECT id, short_id, project_id, project_root, run_root, index_json FROM runs WHERE project_id = ? AND (id = ? OR short_id = ?)", [projectId, id, id]);
|
|
156
195
|
if (rows.length !== 1)
|
|
157
196
|
throw new AppError(rows.length ? "ambiguous_alias" : "not_found", rows.length ? "RUN alias is ambiguous" : "RUN is not registered", 1, { run_id: id });
|
|
158
197
|
return rows[0];
|
|
159
198
|
}
|
|
160
199
|
function requireWork(context, projectId, runId, workId) {
|
|
161
|
-
|
|
162
|
-
if (!work)
|
|
163
|
-
throw new AppError("not_found", "Work does not belong to this RUN", 1, { run_id: runId, work_id: workId });
|
|
164
|
-
return work;
|
|
200
|
+
return resolveWorkReference(context, workId, { projectId, runId });
|
|
165
201
|
}
|
|
166
202
|
function requireStage(run, stage, status) {
|
|
167
|
-
|
|
168
|
-
|
|
203
|
+
let index;
|
|
204
|
+
try {
|
|
205
|
+
index = JSON.parse(run.index_json);
|
|
206
|
+
if (!index || !Array.isArray(index.stage_runs))
|
|
207
|
+
throw new Error("RUN index must contain stage_runs");
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
throw new AppError("runtime_state_invalid", "RUN mechanical state in SQLite is missing or invalid", 1, { run_id: run.id, cause: String(error) });
|
|
211
|
+
}
|
|
212
|
+
const found = index.stage_runs.find((item) => item.stage === stage);
|
|
169
213
|
if (!found || found.status !== status)
|
|
170
214
|
throw new AppError("invalid_stage_state", `Stage must be ${status}`, 1, { run_id: run.id, stage, status: found?.status ?? "missing" });
|
|
171
215
|
return found;
|
|
172
216
|
}
|
|
173
217
|
function requireRunHome(run) {
|
|
174
|
-
if (!run.
|
|
175
|
-
throw new AppError("runtime_missing", "RUN has no artifact
|
|
176
|
-
return run.
|
|
218
|
+
if (!run.run_root)
|
|
219
|
+
throw new AppError("runtime_missing", "RUN has no artifact root", 1, { run_id: run.id });
|
|
220
|
+
return run.run_root;
|
|
177
221
|
}
|
|
178
222
|
function nextPauseId(runHome) {
|
|
179
223
|
const root = path.join(runHome, "intake", "hitl");
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { AppError } from "../shared/errors.js";
|
|
2
|
+
export function stageAttempt(context, projectId, runId, stage) {
|
|
3
|
+
const run = context.db.get("SELECT index_json FROM runs WHERE project_id = ? AND id = ?", [projectId, runId]);
|
|
4
|
+
if (!run)
|
|
5
|
+
throw new AppError("not_found", "Stage graph RUN is unavailable", 1);
|
|
6
|
+
const stages = JSON.parse(run.index_json).stage_runs ?? [];
|
|
7
|
+
return [...stages].reverse().find(item => item.stage === stage)?.attempt ?? "try-001";
|
|
8
|
+
}
|
|
9
|
+
export function bindStageWork(context, input) {
|
|
10
|
+
context.db.run("INSERT OR IGNORE INTO stage_work_memberships (project_id, run_id, work_id, stage, attempt, cycle) VALUES (?, ?, ?, ?, ?, ?)", [input.projectId, input.runId, input.workId, input.stage, input.attempt ?? stageAttempt(context, input.projectId, input.runId, input.stage), input.cycle ?? stageCycle(context, input.projectId, input.runId, input.stage)]);
|
|
11
|
+
}
|
|
12
|
+
export function stageCycle(context, projectId, runId, stage) {
|
|
13
|
+
if (stage !== "code" && stage !== "code-review")
|
|
14
|
+
return "";
|
|
15
|
+
const row = context.db.get("SELECT index_json FROM runs WHERE project_id = ? AND id = ?", [projectId, runId]);
|
|
16
|
+
const cycle = row ? JSON.parse(row.index_json).variables?.["merge.source_repair"]?.cycle : undefined;
|
|
17
|
+
return typeof cycle === "string" ? cycle : stage === "code-review" ? "initial" : "";
|
|
18
|
+
}
|
|
19
|
+
export function stageWorkIds(context, projectId, runId, stage) {
|
|
20
|
+
if (!context.db.get("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'stage_work_memberships'"))
|
|
21
|
+
throw new AppError("stage_graph_compatibility_required", "Stage Work membership is unavailable; use the RUN's pinned engine", 1);
|
|
22
|
+
return new Set(context.db.all("SELECT work_id FROM stage_work_memberships WHERE project_id = ? AND run_id = ? AND stage = ? AND attempt = ? AND cycle = ?", [projectId, runId, stage, stageAttempt(context, projectId, runId, stage), stageCycle(context, projectId, runId, stage)]).map(row => row.work_id));
|
|
23
|
+
}
|
|
24
|
+
/** Generic Work APIs also serve non-stage runs; null preserves that contract. */
|
|
25
|
+
export function currentStageWorkIds(context, projectId, runId) {
|
|
26
|
+
const row = context.db.get("SELECT index_json FROM runs WHERE project_id = ? AND id = ?", [projectId, runId]);
|
|
27
|
+
if (!row)
|
|
28
|
+
return null;
|
|
29
|
+
const index = JSON.parse(row.index_json);
|
|
30
|
+
const stage = [...(index.stage_runs ?? [])].reverse().find(item => item.status === "running")?.stage;
|
|
31
|
+
return stage ? stageWorkIds(context, projectId, runId, stage) : null;
|
|
32
|
+
}
|
|
33
|
+
export function stageGraphScope(context, projectId, runId, stage) {
|
|
34
|
+
return { project_id: projectId, run_id: runId, attempt: stageAttempt(context, projectId, runId, stage), cycle: stageCycle(context, projectId, runId, stage) };
|
|
35
|
+
}
|