@deksden-com/dd-flow-cli 0.9.0-beta.105 → 0.9.0-beta.106
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 +6 -0
- package/dist/build-info.json +3 -3
- package/dist/harness-runtime/lib/delegation-instructions.d.mts +1 -0
- package/dist/harness-runtime/lib/delegation-instructions.mjs +3 -1
- package/dist/services/controller-fanout.js +10 -2
- package/dist/services/external-work-launch.js +2 -1
- package/dist/services/hooks.js +6 -4
- package/dist/services/merge-server.js +2 -1
- package/dist/services/run-controller.js +2 -2
- package/dist/services/run-projection.js +9 -6
- package/dist/services/run-recovery-runtime.js +2 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @deksden-com/dd-flow-cli
|
|
2
2
|
|
|
3
|
+
## 0.9.0-beta.106
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Keep all Stage and Work lifecycle commands attached to their shell process until final output arrives, including HITL resume and standalone launch prompts. Resolve a model-facing command's hidden RUN from its issued authority when checking Antigravity workspace routing. Reserve the SQLite writer before reading and publishing the RUN projection so concurrent reviewer completions cannot invalidate it.
|
|
8
|
+
|
|
3
9
|
## 0.9.0-beta.105
|
|
4
10
|
|
|
5
11
|
### Patch Changes
|
package/dist/build-info.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"cli_package": "@deksden-com/dd-flow-cli",
|
|
3
|
-
"cli_version": "0.9.0-beta.
|
|
4
|
-
"cli_commit": "
|
|
5
|
-
"built_at": "2026-09-
|
|
3
|
+
"cli_version": "0.9.0-beta.106",
|
|
4
|
+
"cli_commit": "41377cb4915163be7220cc90928ea8a0f50683d0",
|
|
5
|
+
"built_at": "2026-09-27T00:48:48.042Z",
|
|
6
6
|
"built_with_canon": {
|
|
7
7
|
"version": "4.1.1",
|
|
8
8
|
"commit": "678daa038287c948ada5b2d785a6dcc925c7b891",
|
|
@@ -3,6 +3,7 @@ export type DelegationInput = { harness: string; stage: string; capacity: number
|
|
|
3
3
|
export function delegationAdapter(harness: string): string | null;
|
|
4
4
|
export function delegationContract(harness: string): string;
|
|
5
5
|
export const lifecycleRetryInstruction: string;
|
|
6
|
+
export const lifecycleCommandCompletionInstruction: string;
|
|
6
7
|
export const stageLifecycleInstruction: string;
|
|
7
8
|
export function workerDelegationTask(input: { workId: string; startCommand: string; launchPolicy?: string }): string;
|
|
8
9
|
export function renderDelegationInstructions(input: DelegationInput): string;
|
|
@@ -49,7 +49,9 @@ export function delegationContract(harness) {
|
|
|
49
49
|
return `dd-flow/delegation/${key}@${key === "codex" ? 2 : 1}`;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
export const
|
|
52
|
+
export const lifecycleCommandCompletionInstruction = "A shell tool may yield before the lifecycle process exits. Retain and poll its exact process/session handle to completion, including inside a tool wrapper; empty interim output is not a CLI result. Read final stdout or response_file before deciding continuation, and never reissue the command while it is pending.";
|
|
53
|
+
|
|
54
|
+
export const lifecycleRetryInstruction = `${lifecycleCommandCompletionInstruction} A failure is not permission to replay a lifecycle command. For proven effect=no_effect with recoverable=true and retry_command, correct the input and execute that exact issued successor. A check failure can retain receipts while leaving Work running: correct the evidenced cause first, then use the CLI's exact retry_command for an intentional fresh check attempt; never replay a check merely because Work is unsettled. If continuation.kind=repair_required, or a successful result has outcome=repair_required and a repair Work ID, end this Turn for the registered repair child; use any retry_command only after that repair settles and verification is refreshed. Unknown effects require reconciliation, never replay. A committed effect without an explicit continuation is not retryable. A native shell launch refusal before the CLI starts permits correcting only that shell-form error and retrying the same exact command. Otherwise report the primary error and stop; never invent IDs, call hooks directly, or run status as a substitute.`;
|
|
53
55
|
|
|
54
56
|
export const stageLifecycleInstruction = [
|
|
55
57
|
"The project root identifies this RUN; use the Stage packet's declared execution workspace and absolute artifact paths. Do not change a native Session's working directory to a workspace provisioned for a later Stage.",
|
|
@@ -3,7 +3,7 @@ import { getVnextFanoutStatus, observeVnextNativeChildren, reconcileVnextFanout,
|
|
|
3
3
|
import { getFlowRunVariables } from "./runs.js";
|
|
4
4
|
import { dispatchVnextPlanReview } from "./vnext-plan-review.js";
|
|
5
5
|
import { resolveExecutionPolicy } from "./execution-policy.js";
|
|
6
|
-
import { renderDelegationInstructions, stageLifecycleInstruction } from "../harness-runtime/lib/delegation-instructions.mjs";
|
|
6
|
+
import { lifecycleCommandCompletionInstruction, renderDelegationInstructions, stageLifecycleInstruction } from "../harness-runtime/lib/delegation-instructions.mjs";
|
|
7
7
|
import { assertWorkLaunchReady } from "./work-registry.js";
|
|
8
8
|
function semanticCompletionPrompt(stage, completed) {
|
|
9
9
|
const task = stage === "plan-review"
|
|
@@ -30,6 +30,14 @@ export function controllerStageEntryPrompt(stage, command) {
|
|
|
30
30
|
stageLifecycleInstruction
|
|
31
31
|
].join("\n\n");
|
|
32
32
|
}
|
|
33
|
+
export function controllerStageResumePrompt(pauseId, command) {
|
|
34
|
+
return [
|
|
35
|
+
`The user supplied the raw answer for pause ${pauseId}. Your first tool call must be this exact standalone command:`,
|
|
36
|
+
command,
|
|
37
|
+
"Do not alter the answer. Follow the returned packet, then stop at this Stage boundary.",
|
|
38
|
+
stageLifecycleInstruction
|
|
39
|
+
].join("\n\n");
|
|
40
|
+
}
|
|
33
41
|
/** The shared controller owns this former eval-only Work-graph decision. */
|
|
34
42
|
export async function nextControllerFanout(context, input) {
|
|
35
43
|
// Tree settlement is not a Work result. Keep these children visible to the
|
|
@@ -62,7 +70,7 @@ export async function nextControllerFanout(context, input) {
|
|
|
62
70
|
if (fanout.state === "dispatch_required" || fanout.state === "capacity_required")
|
|
63
71
|
return { kind: "wait", reason: "qualified_capacity_required" };
|
|
64
72
|
}
|
|
65
|
-
const prompt = (action, text) => ({ kind: "prompt", action, prompt: text
|
|
73
|
+
const prompt = (action, text) => ({ kind: "prompt", action, prompt: action === "dispatch_work" ? text : `${text}\n\n${lifecycleCommandCompletionInstruction}`, fingerprint: JSON.stringify({ stage: input.stage, parent: fanout.parent_work_id, state: fanout.state, graph: fanout.graph_revision }) });
|
|
66
74
|
if (fanout.state === "coordinator_required")
|
|
67
75
|
return prompt("materialize_graph", `Continue the current ${input.stage} Stage using its authoritative packet. Materialize only its required Work graph with real dependencies. Do not launch children, finish the Stage, or start a successor. Return after registering the graph; the runtime will dispatch its ready Work.`);
|
|
68
76
|
if (fanout.works.failed || fanout.works.cancelled)
|
|
@@ -15,6 +15,7 @@ import { managedInvocationContext } from "./lifecycle-invocations.js";
|
|
|
15
15
|
import { reviewExecutionCopyHash, reviewSourceFilter, reviewSourceHash, reviewSourcePaths } from "./review-copy.js";
|
|
16
16
|
import { workspaceGitFiles } from "./workspace-files.js";
|
|
17
17
|
import { qualifyAgyHooks } from "../harness-runtime/lib/dd-agy-daemon.mjs";
|
|
18
|
+
import { lifecycleRetryInstruction } from "../harness-runtime/lib/delegation-instructions.mjs";
|
|
18
19
|
/**
|
|
19
20
|
* Creates an external provider root for one already-created Work. The launch
|
|
20
21
|
* is not a Work claim: only the worker's trusted `work start` hook binds it.
|
|
@@ -106,7 +107,7 @@ export async function launchExternalWork(context, input) {
|
|
|
106
107
|
if (reviewCopy && reviewSourceHash(run.workspace_root, reviewInputs, reviewBase) !== reviewCopy.source_sha256)
|
|
107
108
|
throw new AppError("read_only_input_changed", "Authoritative review workspace changed after the execution copy was made", 2, { work_id: work.work_id });
|
|
108
109
|
const promptFile = path.join(root, "launch.md");
|
|
109
|
-
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
|
|
110
|
+
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"}`);
|
|
110
111
|
const prompted = await call(["session", "prompt", "--session-id", sessionId, "--prompt-file", promptFile, ...common], 45 * 60_000);
|
|
111
112
|
assertRunMutationAllowed(context, work.project_id, work.run_id, generation);
|
|
112
113
|
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]);
|
package/dist/services/hooks.js
CHANGED
|
@@ -12,7 +12,7 @@ import { appendAudit } from "./audit.js";
|
|
|
12
12
|
import { prepareTextFile, prepareJsonText, prepareError } from "../cli/input-preparation.js";
|
|
13
13
|
import { nativeSessionIdentity, storageSessionId } from "./session-identity.js";
|
|
14
14
|
import { assertRecoveryHookCurrent } from "./run-recovery.js";
|
|
15
|
-
import { observeLifecycleInvocation } from "./lifecycle-invocations.js";
|
|
15
|
+
import { observeLifecycleInvocation, observedLifecycleInvocation } from "./lifecycle-invocations.js";
|
|
16
16
|
import { commandHasOption, commandOption, commandPosition, shellSuffixOffset, parseLifecycleCommand, lifecycleCommandFromPayload, continuationCommand } from "./lifecycle-command.js";
|
|
17
17
|
import { registerProject, requireProjectByRoot } from "./projects.js";
|
|
18
18
|
import { activeFlowSessionsForProject, recordFlowSessionObservation } from "./sessions.js";
|
|
@@ -670,7 +670,7 @@ export function handleDroidEvent(context, input) {
|
|
|
670
670
|
return handleControlledToolEvent(context, input, event, "droid-cli");
|
|
671
671
|
}
|
|
672
672
|
/** Stable project identity is not the native Session's execution workspace. */
|
|
673
|
-
function controlledLifecycleCwd(context, projectRoot, facts, providerSessionId, auxiliary) {
|
|
673
|
+
function controlledLifecycleCwd(context, projectRoot, facts, providerSessionId, auxiliary, issuedRunId) {
|
|
674
674
|
if (facts?.bootstrapStageStart)
|
|
675
675
|
return projectRoot;
|
|
676
676
|
const project = context.db.get("SELECT id FROM projects WHERE root = ?", [projectRoot]);
|
|
@@ -686,7 +686,7 @@ function controlledLifecycleCwd(context, projectRoot, facts, providerSessionId,
|
|
|
686
686
|
throw error;
|
|
687
687
|
}
|
|
688
688
|
}
|
|
689
|
-
const reference = work?.run_id ?? facts?.runId ?? auxiliary?.args.options.get("run")?.[0] ?? auxiliary?.args.positional[2];
|
|
689
|
+
const reference = work?.run_id ?? facts?.runId ?? auxiliary?.args.options.get("run")?.[0] ?? auxiliary?.args.positional[2] ?? issuedRunId;
|
|
690
690
|
if (!reference)
|
|
691
691
|
return projectRoot;
|
|
692
692
|
const run = context.db.get("SELECT id, run_root, workspace_root FROM runs WHERE project_id = ? AND (id = ? OR short_id = ?)", [project.id, reference, reference]);
|
|
@@ -774,7 +774,9 @@ function handleControlledToolEvent(context, input, event, harness) {
|
|
|
774
774
|
const auxiliary = continuationCommand(command);
|
|
775
775
|
if (!lifecycle && !auxiliary)
|
|
776
776
|
return { ok: true, observed: false, reason: "event_not_participating" };
|
|
777
|
-
const
|
|
777
|
+
const issuedRunId = lifecycle && !lifecycle.runId && !lifecycle.workId && !lifecycle.bootstrapStageStart
|
|
778
|
+
? observedLifecycleInvocation(context, command, daemonId)?.scope.runId : null;
|
|
779
|
+
const expectedCwd = controlledLifecycleCwd(context, expectedRoot, lifecycle, providerSessionId, auxiliary, issuedRunId);
|
|
778
780
|
if (observedCwd !== expectedCwd) {
|
|
779
781
|
throw new AppError(`${errorPrefix}_directory_mismatch`, `${label} ${agy ? "launch route" : "Session directory"} does not match the assigned execution workspace`, 1, { expected: expectedCwd, actual: observedCwd, ...(agy ? { actual_provenance: stringValue(event.cwd_provenance) ?? "unknown" } : {}) });
|
|
780
782
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { managedInvocationContext, managedLifecycleCommand } from "./lifecycle-invocations.js";
|
|
2
|
+
import { stageLifecycleInstruction } from "../harness-runtime/lib/delegation-instructions.mjs";
|
|
2
3
|
import crypto from "node:crypto";
|
|
3
4
|
import fs from "node:fs";
|
|
4
5
|
import path from "node:path";
|
|
@@ -119,7 +120,7 @@ async function dispatch(context, input) {
|
|
|
119
120
|
retain();
|
|
120
121
|
runtimeContext = managedInvocationContext(runtimeContext, { harness: runtimeHarness(input.profile), stateDir, sessionId, projectRoot: run.project_root, runId: input.request.run_id, generation });
|
|
121
122
|
const stageCommand = managedLifecycleCommand(runtimeContext, `${flowCommand(runtimeContext)} stage start ${input.request.run_id} --stage merge --project-root ${JSON.stringify(run.project_root)} --json --progress-jsonl`);
|
|
122
|
-
fs.writeFileSync(promptFile, `Start the assigned MERGE stage now. Your first tool call must be this exact standalone command:\n\n${stageCommand}\n\nTrust and follow the complete stage packet it returns. Continue through merge apply and stage finish. Stop only when the stage reaches a terminal result or explicitly requests user/operator action.\n`);
|
|
123
|
+
fs.writeFileSync(promptFile, `Start the assigned MERGE stage now. Your first tool call must be this exact standalone command:\n\n${stageCommand}\n\nTrust and follow the complete stage packet it returns. Continue through merge apply and stage finish. Stop only when the stage reaches a terminal result or explicitly requests user/operator action.\n\n${stageLifecycleInstruction}\n`);
|
|
123
124
|
const prompted = await adapterCall(["session", "prompt", "--session-id", sessionId, "--prompt-file", promptFile, ...common], 45 * 60 * 1000, `MERGE ${input.request.merge_request_id} Session ${sessionId} is still running`);
|
|
124
125
|
Object.assign(retained, { response: prompted });
|
|
125
126
|
retain();
|
|
@@ -23,7 +23,7 @@ import { assertManagedProcessLease, confirmManagedProcess, heartbeatManagedProce
|
|
|
23
23
|
import { readNativeDaemonState, requireManagedDaemonBinding } from "./managed-daemon-binding.js";
|
|
24
24
|
import { initCodexHome } from "./hooks.js";
|
|
25
25
|
import { launchExternalWork } from "./external-work-launch.js";
|
|
26
|
-
import { controllerNativeChildren, controllerStageEntryPrompt, nextControllerFanout } from "./controller-fanout.js";
|
|
26
|
+
import { controllerNativeChildren, controllerStageEntryPrompt, controllerStageResumePrompt, nextControllerFanout } from "./controller-fanout.js";
|
|
27
27
|
import { claimQueuedMergeRequest } from "./merge-server.js";
|
|
28
28
|
import { prepareVnextCodeWorkspace } from "./vnext-code.js";
|
|
29
29
|
import { requireRun, requireController, controllerReceipt, appendEvent, saveState, retainControllerContinuation, digest, digestBytes, errorRecord } from "./run-controller-state.js";
|
|
@@ -613,7 +613,7 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
613
613
|
// The accepted answer is logical intent; only its generation-bound
|
|
614
614
|
// adapter operation represents a physical dispatch that stop must drain.
|
|
615
615
|
const command = managedLifecycleCommand(executionContext, `${flowCommand(executionContext)} stage resume ${run.id} --stage ${next.stage} --work ${input.work_id} --project-root ${quote([run.project_root])} --answer-file ${quote([input.file])} --json`, { replaceUnobservedAnswer: true });
|
|
616
|
-
const receipt = await prompt(state.sessions[state.current_session],
|
|
616
|
+
const receipt = await prompt(state.sessions[state.current_session], controllerStageResumePrompt(pause.id, command), `${answer.operation_id}:prompt:${row.generation}`);
|
|
617
617
|
const afterAnswer = getFlowRunStatus(executionContext, { projectRoot: run.project_root, runId: run.id });
|
|
618
618
|
if (afterAnswer.index.stage_runs.some(stage => stage.status === "paused" && stage.pause?.id === pause.id)) {
|
|
619
619
|
throw new AppError("controller_answer_not_applied", "The answer Turn completed without resuming the assigned HITL pause", 1, { pause_id: pause.id, operation_id: answer.operation_id, stage: next.stage, effect: "no_progress" });
|
|
@@ -3,6 +3,11 @@ import { reconcileSessionCoverageRows } from "../domain/session-coverage.js";
|
|
|
3
3
|
import { publicSessionIdentity } from "./session-identity.js";
|
|
4
4
|
import { publishRunProjections } from "./runs.js";
|
|
5
5
|
export function refreshRunSessionProjection(context, projectId, runId) {
|
|
6
|
+
// Review children can finish concurrently in separate processes. Reserve the
|
|
7
|
+
// writer before reading the RUN index so a sibling cannot invalidate it.
|
|
8
|
+
context.db.writeTransaction(() => refreshRunSessionProjectionLocked(context, projectId, runId), "run-projection.refresh");
|
|
9
|
+
}
|
|
10
|
+
function refreshRunSessionProjectionLocked(context, projectId, runId) {
|
|
6
11
|
const run = context.db.get("SELECT id, flow_kind, status, project_root, subject_type, subject_id, runtime_path, run_index_path, index_json FROM runs WHERE project_id = ? AND id = ?", [projectId, runId]);
|
|
7
12
|
if (!run)
|
|
8
13
|
return;
|
|
@@ -60,12 +65,10 @@ export function refreshRunSessionProjection(context, projectId, runId) {
|
|
|
60
65
|
index.workers = workers;
|
|
61
66
|
index.updated_at = context.now();
|
|
62
67
|
index.runtime_revision = typeof index.runtime_revision === "number" ? index.runtime_revision + 1 : 1;
|
|
63
|
-
context.db.
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
context.db.afterCommit(() => publishRunProjections(context, projectId, runId));
|
|
68
|
-
}, "run-projection.refresh");
|
|
68
|
+
const updated = context.db.run("UPDATE runs SET index_json = ?, updated_at = ? WHERE project_id = ? AND id = ? AND index_json = ?", [JSON.stringify(index), index.updated_at, projectId, runId, run.index_json]);
|
|
69
|
+
if (updated.changes !== 1)
|
|
70
|
+
throw new Error("RUN projection changed during preparation");
|
|
71
|
+
context.db.afterCommit(() => publishRunProjections(context, projectId, runId));
|
|
69
72
|
}
|
|
70
73
|
function planProjection(context, projectId, protocolId, projectRoot, index) {
|
|
71
74
|
const binding = context.db.get("SELECT plan_id, plan_path, plan_revision, plan_sha256 FROM plan_bindings WHERE project_id = ? AND protocol_id = ?", [projectId, protocolId]);
|
|
@@ -7,6 +7,7 @@ import { acceptanceCommand, recoveryGuard, recoveryPacketReceipt, requireRecover
|
|
|
7
7
|
import { managedProcessBinding, processIsAlive, resourceHome } from "./managed-processes.js";
|
|
8
8
|
import { getResourceDatabase } from "../storage/database.js";
|
|
9
9
|
import { requireManagedDaemonBinding } from "./managed-daemon-binding.js";
|
|
10
|
+
import { lifecycleRetryInstruction } from "../harness-runtime/lib/delegation-instructions.mjs";
|
|
10
11
|
/** Pin retained Session loading and its first acknowledgement Turn, never a general fence bypass. */
|
|
11
12
|
export function prepareRunRecoveryDispatch(context, input) {
|
|
12
13
|
if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$/.test(input.operationId) || !path.isAbsolute(input.stateDir))
|
|
@@ -37,7 +38,7 @@ export function prepareRunRecoveryDispatch(context, input) {
|
|
|
37
38
|
});
|
|
38
39
|
let dispatch = prepared.dispatch;
|
|
39
40
|
if (!dispatch) {
|
|
40
|
-
const prompt = `Resume this retained RUN in its existing native Session. Your first tool call must be this exact standalone command:\n\n${acceptanceCommand(context, run.id, prepared.guard.recovery_id, run.project_root)}\n\nFollow the verified recovery packet returned by that command. Preserve completed Work and existing HITL; do not start a successor Stage yourself. Stop at the Stage or declared Work-graph boundary.\n`;
|
|
41
|
+
const prompt = `Resume this retained RUN in its existing native Session. Your first tool call must be this exact standalone command:\n\n${acceptanceCommand(context, run.id, prepared.guard.recovery_id, run.project_root)}\n\nFollow the verified recovery packet returned by that command. Preserve completed Work and existing HITL; do not start a successor Stage yourself. Stop at the Stage or declared Work-graph boundary.\n\n${lifecycleRetryInstruction}\n`;
|
|
41
42
|
dispatch = { process_id: prepared.record.id, owner_id: prepared.record.owner_id, state_dir: input.stateDir, daemon_id: prepared.binding.daemon_id, operation_id: input.operationId, resume_operation_id: `${input.operationId}:load`, prompt_sha256: crypto.createHash("sha256").update(prompt).digest("hex"), prompt };
|
|
42
43
|
}
|
|
43
44
|
const registry = prepared.record ? getResourceDatabase(resourceHome(context)) : undefined;
|