@deksden-com/dd-flow-cli 0.9.0-beta.83 → 0.9.0-beta.86

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 CHANGED
@@ -1,5 +1,26 @@
1
1
  # @deksden-com/dd-flow-cli
2
2
 
3
+ ## 0.9.0-beta.86
4
+
5
+ ### Patch Changes
6
+
7
+ - 8abe471: Settle invalid managed lifecycle arguments atomically before dispatch and return one safe corrected command.
8
+
9
+ ## 0.9.0-beta.85
10
+
11
+ ### Patch Changes
12
+
13
+ - fe8f493: Restore runtime-owned response publication options before dispatching a managed lifecycle command, and update the controller fixture to validate the public command contract.
14
+
15
+ ## 0.9.0-beta.84
16
+
17
+ ### Patch Changes
18
+
19
+ - 644d181: Keep RUN identity, project paths, context files, response destinations and
20
+ integrity hashes inside retained lifecycle authority instead of asking agents
21
+ to copy them. Restore those inputs deterministically when the observed command
22
+ executes, including MERGE apply and repair commands.
23
+
3
24
  ## 0.9.0-beta.83
4
25
 
5
26
  ### Patch Changes
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "cli_package": "@deksden-com/dd-flow-cli",
3
- "cli_version": "0.9.0-beta.83",
4
- "cli_commit": "7ede03ea5da48b77ab59b46bed8028b2a885da61",
5
- "built_at": "2026-09-20T14:09:35.175Z",
3
+ "cli_version": "0.9.0-beta.86",
4
+ "cli_commit": "979a6151c910228ed3da8daa015bcf89098b04f1",
5
+ "built_at": "2026-09-20T19:12:05.808Z",
6
6
  "built_with_canon": {
7
7
  "version": "4.1.1",
8
8
  "commit": "97f811d33c212ae3497020178b1ed825c7c3ebac",
@@ -4,7 +4,7 @@ import { randomUUID } from "node:crypto";
4
4
  import { quote } from "shell-quote";
5
5
  import { observeLifecycleCommand } from "../services/lifecycle-invocations.js";
6
6
  import { verifyCodexHookDelivery } from "../services/codex-hook-delivery.js";
7
- import { assertLifecycleInvocationCurrent, awaitLifecycleInvocation, expandLifecycleInvocationArgs, issueLifecycleInvocation, prepareLifecycleIssuance, lifecycleInvocationScope, observedLifecycleInvocation, settleLifecycleInvocation, settleLifecycleRejection, validateLifecyclePathAliases } from "../services/lifecycle-invocations.js";
7
+ import { assertLifecycleInvocationCurrent, awaitLifecycleInvocation, expandLifecycleInvocationArgs, issueLifecycleInvocation, prepareLifecycleIssuance, lifecycleInvocationScope, observedLifecycleInvocation, resolveObservedLifecycleArgs, settleLifecycleInvocation, settleLifecyclePreparationRejection, settleLifecycleRejection, validateLifecyclePathAliases } from "../services/lifecycle-invocations.js";
8
8
  import { createContext, createRouterContext } from "../runtime/context.js";
9
9
  import { migrateStoreWriter } from "../storage/writer-migration.js";
10
10
  import { helpForArgs } from "./help.js";
@@ -137,16 +137,37 @@ export async function runCli(args, io = defaultIo, env = process.env) {
137
137
  throw new AppError("invocation_receipt_missing", "Managed lifecycle command has no committed native receipt", 1, { effect: "no_effect", recoverable: false });
138
138
  }
139
139
  const invocationId = observedInvocation?.id ?? suppliedInvocationId;
140
- const boundInvocationArgs = invocationId && invocationId !== suppliedInvocationId
141
- ? [...output.args.filter((_, index, values) => values[index - 1] !== "--invocation-id" && values[index] !== "--invocation-id"), "--invocation-id", invocationId]
142
- : output.args;
140
+ let boundInvocationArgs;
141
+ try {
142
+ boundInvocationArgs = observedInvocation
143
+ ? resolveObservedLifecycleArgs(routerContext, observedInvocation.id, output.args)
144
+ : invocationId && invocationId !== suppliedInvocationId
145
+ ? [...output.args.filter((_, index, values) => values[index - 1] !== "--invocation-id" && values[index] !== "--invocation-id"), "--invocation-id", invocationId]
146
+ : output.args;
147
+ }
148
+ catch (error) {
149
+ if (!(observedInvocation && error instanceof AppError))
150
+ throw error;
151
+ const rejectionContext = createContext({ ...env }, "hook");
152
+ try {
153
+ Object.assign(error.details, settleLifecyclePreparationRejection(rejectionContext, observedInvocation.id, error));
154
+ }
155
+ finally {
156
+ rejectionContext.db.close?.();
157
+ }
158
+ throw error;
159
+ }
143
160
  const observedOperation = lifecycleCommand.kind === "standalone" ? lifecycleCommand.invocation.operation : null;
144
161
  if (observedInvocation && !observedOperation)
145
162
  throw new AppError("invocation_command_invalid", "Observed lifecycle command could not be parsed", 1);
146
163
  const invocationScope = invocationId ? lifecycleInvocationScope({ ...routerContext, env }, invocationId) : undefined;
147
- const invocationArgs = invocationScope && observedOperation
164
+ const expandedInvocationArgs = invocationScope && observedOperation
148
165
  ? expandLifecycleInvocationArgs(routerContext, boundInvocationArgs, invocationScope, observedOperation)
149
166
  : boundInvocationArgs;
167
+ const retainedOutput = observedInvocation ? parseOutputOptions(expandedInvocationArgs) : null;
168
+ if (retainedOutput?.responseFile)
169
+ output.responseFile = retainedOutput.responseFile;
170
+ const invocationArgs = retainedOutput?.args ?? expandedInvocationArgs;
150
171
  emitHumanProgress(io, output, progress, "start");
151
172
  if (output.progressJsonl) {
152
173
  writeProgressJsonl(io, "start", `Starting ${output.args.slice(0, 3).join(" ")}`);
@@ -5,11 +5,11 @@ import { dispatchVnextPlanReview } from "./vnext-plan-review.js";
5
5
  import { resolveExecutionPolicy } from "./execution-policy.js";
6
6
  import { renderDelegationInstructions } from "../harness-runtime/lib/delegation-instructions.mjs";
7
7
  /** Stage entry acknowledges the packet before the controller issues child commands. */
8
- export function controllerStageEntryPrompt(stage, command, responseFile) {
8
+ export function controllerStageEntryPrompt(stage, command) {
9
9
  return [
10
10
  `Start the assigned ${stage} Stage now. Your first tool call must be this exact standalone command:`,
11
11
  command,
12
- `Read the complete authoritative packet saved to ${JSON.stringify(responseFile)}, including worker_prompt_markdown. If a read is truncated, continue until the entire packet has been read.`,
12
+ "Read the complete authoritative packet from the command's response_file result, including worker_prompt_markdown. If a read is truncated, continue until the entire packet has been read.",
13
13
  "If the returned packet has orchestration.kind = work_fanout, this Turn is only Stage entry: acknowledge the packet and end your Turn immediately. The packet describes the whole Stage, but its graph/dispatch/finish instructions belong to later controller continuations. Do not list, start, launch or finish Work, create children, change files, run checks, or finish the Stage in this entry Turn. The controller will inspect the graph and send the next assignment with exact issued commands (or dispatch external workers itself).",
14
14
  "Otherwise perform this Stage's semantic work and stop at its boundary or a declared HITL boundary. Do not start a successor Stage.",
15
15
  "Execute each runtime-issued lifecycle operation and target as one standalone call. Internal invocation IDs are intentionally absent; never add, copy or invent one. For a no-effect input rejection, correct the input and use its returned retry_command. If a required command is missing or no safe retry is returned, report the exact error and end the Turn so the controller can handle it."
@@ -352,7 +352,7 @@ export function handleCodexHook(context, input) {
352
352
  ?? (bindingOwnerId ? sessionBinding(context, project.id, bindingOwnerId)?.transcript_path ?? locateCodexTranscript(bindingOwnerId, context.env.CODEX_HOME) : null);
353
353
  const persist = (scope) => {
354
354
  if (scope)
355
- matchKey = lifecycleMatchKey(lifecycle, project.root, scope);
355
+ matchKey = lifecycleMatchKey(lifecycleFacts(scope.command), project.root, scope);
356
356
  const binding = bindingOwnerId ? upsertSessionBindingFromPayload(context, project, bindingOwnerId, payload, transcriptPath) : undefined;
357
357
  const effectiveSessionId = storageId;
358
358
  const protocolId = binding?.protocol_id ?? null;
@@ -549,7 +549,7 @@ function recordZcodeLifecycle(context, input, invocationId) {
549
549
  const eventNameForReceipt = invocationId ? "ToolCallObserved" : "PreToolUse";
550
550
  const persist = (scope) => {
551
551
  if (scope)
552
- matchKey = lifecycleMatchKey(lifecycle, expectedRoot, scope);
552
+ matchKey = lifecycleMatchKey(lifecycleFacts(scope.command), expectedRoot, scope);
553
553
  assertHookEventReplay(context, { projectId: project.id, eventKey, harness: "zcode-acp", providerSessionId, parentSessionId, daemonId, sessionId, turnId: null, eventName: eventNameForReceipt, toolName: toolName ?? "Bash", matchKey, cwd: expectedRoot });
554
554
  const payload = { ...hook, session_id: providerSessionId, cwd: expectedRoot, tool_name: toolName ?? "Bash", tool_input: rawInput,
555
555
  command, provider: stringValue(observedProfile.provider), model: stringValue(observedProfile.model), reasoning: stringValue(observedProfile.reasoning), mode: stringValue(observedProfile.mode) };
@@ -198,6 +198,10 @@ function lifecycleOperation(argv) {
198
198
  return "work_finish";
199
199
  if (key === "work fail")
200
200
  return "work_fail";
201
+ if (key === "merge apply")
202
+ return "merge_apply";
203
+ if (key === "merge repair")
204
+ return "merge_repair";
201
205
  return null;
202
206
  }
203
207
  export function parseCommandArgs(argv) {
@@ -58,10 +58,50 @@ function invocation(command) {
58
58
  // Input data may be supplied later (stdin/file contents); authority is bound to
59
59
  // the exact operation and literal argv. Existing lifecycle validation owns data.
60
60
  function fingerprint(parsed) {
61
- const options = [...parsed.args.options].filter(([key]) => !["invocation-id", "hook-event-id", "json", "progress-jsonl", "response-file"].includes(key))
61
+ const projected = publicInvocationArgs(parsed);
62
+ const publicParsed = invocation(`${quote([parsed.executable])} ${quoteModelFacingArgs(projected)}`);
63
+ const options = [...publicParsed.args.options].filter(([key]) => !["invocation-id", "hook-event-id", "json", "progress-jsonl"].includes(key))
62
64
  .map(([key, values]) => [key, key === "reason" ? ["<semantic-reason>"] : values.map(value => normalizedInvocationValue(key, value))])
63
65
  .sort(([a], [b]) => a.localeCompare(b));
64
- return crypto.createHash("sha256").update(JSON.stringify([parsed.operation, parsed.args.positional.map(value => normalizedInvocationValue("positional", value)), options])).digest("hex");
66
+ return crypto.createHash("sha256").update(JSON.stringify([parsed.operation, publicParsed.args.positional.map(value => normalizedInvocationValue("positional", value)), options])).digest("hex");
67
+ }
68
+ const runtimeOwnedOptions = {
69
+ stage_start: new Set(["project-root", "context-file", "context-sha256", "response-file", "require-session-binding"]),
70
+ stage_finish: new Set(["project-root", "result-file", "decision-file", "verification-file"]),
71
+ stage_pause: new Set(["project-root"]), stage_resume: new Set(["project-root"]),
72
+ work_start: new Set(["project-root", "recovery-id"]),
73
+ work_finish: new Set(["project-root"]), work_fail: new Set(["project-root"]),
74
+ merge_apply: new Set(["project-root"]), merge_repair: new Set(["project-root"]),
75
+ recovery_accept: new Set(["project-root"])
76
+ };
77
+ function publicInvocationArgs(parsed) {
78
+ const hidden = runtimeOwnedOptions[parsed.operation] ?? new Set();
79
+ const args = [];
80
+ for (let index = 0; index < parsed.argv.length; index += 1) {
81
+ const value = parsed.argv[index];
82
+ if (!value.startsWith("--")) {
83
+ args.push(value);
84
+ continue;
85
+ }
86
+ const key = value.slice(2).split("=", 1)[0];
87
+ const inline = value.includes("=");
88
+ const next = parsed.argv[index + 1];
89
+ if (hidden.has(key) || key === "invocation-id" || key === "hook-event-id") {
90
+ if (!inline && next && !next.startsWith("--"))
91
+ index += 1;
92
+ continue;
93
+ }
94
+ args.push(value);
95
+ if (!inline && next && !next.startsWith("--"))
96
+ args.push(parsed.argv[++index]);
97
+ }
98
+ // A managed coordinator Session is already bound to its RUN. Work IDs stay
99
+ // public because sibling Work assignments can coexist under one native root.
100
+ const operationOffset = parsed.operation === "recovery_accept" ? 3 : 2;
101
+ if (["stage_start", "stage_finish", "recovery_accept"].includes(parsed.operation)
102
+ && !parsed.args.options.has("bootstrap") && args[operationOffset] && !args[operationOffset].startsWith("--"))
103
+ args.splice(operationOffset, 1);
104
+ return args;
65
105
  }
66
106
  function normalizedInvocationValue(key, value) {
67
107
  value = normalizedPathAlias(value);
@@ -92,10 +132,7 @@ function quoteModelFacingArgs(argv) {
92
132
  }
93
133
  function withoutInvocationId(command, aliases) {
94
134
  const parsed = invocation(command);
95
- const argv = [...parsed.argv];
96
- const at = argv.indexOf("--invocation-id");
97
- if (at >= 0)
98
- argv.splice(at, 2);
135
+ const argv = publicInvocationArgs(parsed);
99
136
  for (let index = 0; index < argv.length; index += 1) {
100
137
  const option = argv[index - 1]?.replace(/^--/, "");
101
138
  if (option === "project-root")
@@ -123,11 +160,20 @@ function withoutInvocationId(command, aliases) {
123
160
  return `${prefix ? `${prefix} ` : ""}${executable}${argv.length ? ` ${quoteModelFacingArgs(argv)}` : ""}${suffix ? ` ${suffix}` : ""}`.trim();
124
161
  }
125
162
  function publicInvocationCommand(context, row, command) {
163
+ if (!commandOption(invocation(command), "project-root"))
164
+ command = retainedCommandWithoutInvocationId(row);
126
165
  const rendered = renderInvocationCommand(row, command);
127
166
  const scope = JSON.parse(row.scope_json);
128
167
  const run = scope.runId ? context.db.get("SELECT workspace_root, run_root FROM runs WHERE id = ?", [scope.runId]) : undefined;
129
168
  return withoutInvocationId(rendered, { project: scope.projectRoot, ...(run?.workspace_root ? { workspace: run.workspace_root } : {}), ...(run?.run_root ? { run: run.run_root } : {}) });
130
169
  }
170
+ function retainedCommandWithoutInvocationId(row) {
171
+ const marker = ` --invocation-id ${row.id}`;
172
+ const at = row.command.indexOf(marker);
173
+ if (at < 0 || row.command.indexOf(marker, at + marker.length) >= 0)
174
+ throw new AppError("invocation_command_invalid", "Retained issued command has an ambiguous invocation marker", 1);
175
+ return `${row.command.slice(0, at)}${row.command.slice(at + marker.length)}`;
176
+ }
131
177
  /** Resolve the small, declared alias vocabulary used by model-facing lifecycle commands.
132
178
  * Both native-hook admission and CLI execution call this function so they cannot
133
179
  * disagree about the command that an alias denotes. */
@@ -454,11 +500,7 @@ function successorLifecycleInvocationCommand(context, prior) {
454
500
  // The parser proves the marker belongs to argv. Remove that one rendered
455
501
  // token from the retained text so response-file/stdin/heredoc presentation
456
502
  // remains byte-for-byte stable across a retry.
457
- const marker = ` --invocation-id ${prior.id}`;
458
- const markerAt = prior.command.indexOf(marker);
459
- if (markerAt < 0 || prior.command.indexOf(marker, markerAt + marker.length) >= 0)
460
- throw new AppError("invocation_command_invalid", "Retained issued command has an ambiguous invocation marker", 1);
461
- const command = `${prior.command.slice(0, markerAt)}${prior.command.slice(markerAt + marker.length)}`;
503
+ const command = retainedCommandWithoutInvocationId(prior);
462
504
  return managedLifecycleCommand({ ...context, env: { ...context.env, DD_FLOW_INVOCATION_SCOPE: prior.scope_json, DD_FLOW_CURRENT_INVOCATION: prior.id } }, command);
463
505
  }
464
506
  // Caller holds the write transaction and has checked authority. Issued (or
@@ -568,6 +610,11 @@ export function assertLifecycleInvocationCurrent(context, scope, command) {
568
610
  throw new AppError("invocation_scope_mismatch", "Work does not belong unambiguously to the issued RUN scope", 1);
569
611
  }
570
612
  }
613
+ else if (parsed.operation.startsWith("merge_")) {
614
+ const request = context.db.get("SELECT project_id, run_id FROM merge_requests WHERE merge_request_id = ?", [parsed.args.positional[0] ?? ""]);
615
+ if (!request || request.project_id !== project.id || request.run_id !== scope.runId)
616
+ throw new AppError("invocation_scope_mismatch", "MERGE request does not belong to the issued RUN scope", 1);
617
+ }
571
618
  else if (parsed.operation !== "session_register" && !parsed.args.options.has("bootstrap")) {
572
619
  const target = parsed.args.positional[0];
573
620
  const run = context.db.get("SELECT id FROM runs WHERE project_id = ? AND (id = ? OR short_id = ?)", [project.id, target ?? null, target ?? null]);
@@ -713,7 +760,7 @@ export function observeLifecycleInvocation(context, input) {
713
760
  context.db.exec("COMMIT");
714
761
  return { eventKey: row.event_key, duplicate: true, ...resolved };
715
762
  }
716
- const eventKey = input.recordReceipt(scope);
763
+ const eventKey = input.recordReceipt({ projectRoot: scope.projectRoot, runId: scope.runId, command: initial.command });
717
764
  // A native hook may execute from a provider worktree while the issued
718
765
  // lifecycle scope is anchored to the stable project root. Project
719
766
  // identity is the canonical boundary; comparing raw cwd would reject a
@@ -763,6 +810,43 @@ export function observedLifecycleInvocation(context, command) {
763
810
  throw new AppError("invocation_ambiguous", "More than one observed lifecycle attempt matches this command", 1, { effect: "no_effect", matches: matches.map(row => row.id) });
764
811
  return { id: matches[0].id, scope: JSON.parse(matches[0].scope_json) };
765
812
  }
813
+ /** Replace presentation argv with the exact retained machine assignment.
814
+ * Public semantic arguments are already covered by the projected fingerprint;
815
+ * any explicitly repeated private value must agree instead of being ignored. */
816
+ export function resolveObservedLifecycleArgs(context, id, actualArgs) {
817
+ const row = context.db.get("SELECT * FROM lifecycle_invocations WHERE id = ?", [id]);
818
+ if (!row)
819
+ throw new AppError("invocation_unknown", "Lifecycle invocation is not registered", 1, { invocation_id: id });
820
+ const retained = invocation(row.command), actual = invocation(`${quote([retained.executable])} ${quoteModelFacingArgs(actualArgs)}`);
821
+ if (fingerprint(actual) !== row.fingerprint)
822
+ throw new AppError("invocation_command_mismatch", "Lifecycle command does not match its issued assignment", 2, { phase: "prepare", effect: "no_effect", recoverable: true });
823
+ for (const key of runtimeOwnedOptions[retained.operation] ?? []) {
824
+ const supplied = commandOption(actual, key), expected = commandOption(retained, key);
825
+ if (supplied !== undefined && normalizedInvocationValue(key, supplied) !== normalizedInvocationValue(key, expected ?? "")) {
826
+ throw new AppError("invocation_argument_mismatch", `--${key} is owned by the managed assignment and does not match it`, 2, { phase: "prepare", effect: "no_effect", recoverable: true, parameter: key });
827
+ }
828
+ }
829
+ return [...retained.argv];
830
+ }
831
+ export function settleLifecyclePreparationRejection(context, id, error) {
832
+ return context.db.writeTransaction(() => {
833
+ const row = load(context, id);
834
+ if (row.status === "settled") {
835
+ const details = row.outcome_json ? JSON.parse(row.outcome_json)?.error?.details : undefined;
836
+ return details?.effect === "no_effect" && details.retry_command ? details : { effect: "unknown", recoverable: false };
837
+ }
838
+ if (!["issued", "observed"].includes(row.status))
839
+ return { effect: "unknown", recoverable: false };
840
+ const scope = JSON.parse(row.scope_json);
841
+ if (scope.runId)
842
+ assertLifecycleInvocationCurrent(context, scope, row.command);
843
+ const details = { ...error.details, effect: "no_effect", recoverable: true,
844
+ retry_command: successorLifecycleInvocationCommand(context, row),
845
+ retry_instruction: "Execute retry_command verbatim in this same Session. The rejected call changed no lifecycle state." };
846
+ context.db.run("UPDATE lifecycle_invocations SET status = 'settled', outcome_json = ?, updated_at = ? WHERE id = ?", [JSON.stringify({ error: { ...errorRecord(error), details } }), context.now(), row.id]);
847
+ return details;
848
+ });
849
+ }
766
850
  /** One caller owns execution; a lost process leaves an explicit unknown
767
851
  * outcome, never an automatic replay. Existing recovery decides the next step. */
768
852
  export async function awaitLifecycleInvocation(context, input) {
@@ -782,27 +866,7 @@ export async function awaitLifecycleInvocation(context, input) {
782
866
  catch (error) {
783
867
  if (!(error instanceof AppError) || !(["usage", "invocation_command_mismatch", "invocation_command_invalid"].includes(error.code) || error.details.phase === "prepare"))
784
868
  throw error;
785
- const rejectionDetails = context.db.writeTransaction(() => {
786
- const row = load(context, input.id);
787
- if (row.status === "settled") {
788
- const details = row.outcome_json ? JSON.parse(row.outcome_json)?.error?.details : undefined;
789
- return details?.effect === "no_effect" && details.retry_command ? details : { effect: "unknown", recoverable: false };
790
- }
791
- if (!["issued", "observed"].includes(row.status)) {
792
- return { effect: "unknown", recoverable: false };
793
- }
794
- const scope = JSON.parse(row.scope_json);
795
- if (scope.runId)
796
- assertLifecycleInvocationCurrent(context, scope, row.command);
797
- // No executor has claimed this attempt. Atomically retain the rejected
798
- // call and issue one corrected command; old IDs only replay the outcome.
799
- const details = { ...error.details, effect: "no_effect", recoverable: true,
800
- retry_command: successorLifecycleInvocationCommand(context, row),
801
- retry_instruction: "Execute retry_command verbatim in this same Session. The rejected call changed no lifecycle state." };
802
- context.db.run("UPDATE lifecycle_invocations SET status = 'settled', outcome_json = ?, updated_at = ? WHERE id = ?", [JSON.stringify({ error: { ...errorRecord(error), details } }), context.now(), row.id]);
803
- return details;
804
- });
805
- Object.assign(error.details, rejectionDetails);
869
+ Object.assign(error.details, settleLifecyclePreparationRejection(context, input.id, error));
806
870
  throw error;
807
871
  }
808
872
  const timeoutMs = input.timeoutMs ?? 30000;
@@ -693,7 +693,7 @@ async function executeController(context, row, manifest, state, assertOwnership,
693
693
  const responseFile = path.join(row.state_dir, `${next.stage}-${next.attempt ?? 1}.stage-start-response.json`);
694
694
  const command = managedLifecycleCommand(executionContext, `${flowCommand(executionContext)} stage start ${run.id} --stage ${next.stage} --project-root ${JSON.stringify(run.project_root)} --context-file ${JSON.stringify(stageContext.file)} --context-sha256 ${stageContext.sha256} --require-session-binding --response-file ${JSON.stringify(responseFile)} --json --progress-jsonl`);
695
695
  appendEvent(context, row.controller_id, "stage_entered", { stage: next.stage, session_id: session.id, context_sha256: stageContext.sha256 });
696
- await prompt(session, controllerStageEntryPrompt(next.stage, command, responseFile));
696
+ await prompt(session, controllerStageEntryPrompt(next.stage, command));
697
697
  continue;
698
698
  }
699
699
  if (state.current_session === null)
@@ -290,7 +290,7 @@ export async function finishVnextMerge(context, input, prepared = prepareVnextMe
290
290
  const failed = receipts.filter((receipt) => receipt.status !== "passed");
291
291
  if (failed.length) {
292
292
  context.db.run("UPDATE merge_requests SET status = 'action_required', last_error_json = ?, updated_at = ? WHERE merge_request_id = ?", [JSON.stringify({ code: "merge_gate_failed", failures: failed }), context.now(), request.merge_request_id]);
293
- throw new AppError("merge_gate_failed", "Integrated target checks failed. Classify retained evidence: source defects use source repair; restored environment uses retry.", 2, { failures: failed, repair_command: `${flowCommand(context)} merge repair ${request.merge_request_id} --project-root ${JSON.stringify(projectRoot)} --json`, retry_command: finishCommand(context, run.id, request, projectRoot, failed[0].id) });
293
+ throw new AppError("merge_gate_failed", "Integrated target checks failed. Classify retained evidence: source defects use source repair; restored environment uses retry.", 2, { failures: failed, repair_command: repairCommand(context, request, projectRoot), retry_command: finishCommand(context, run.id, request, projectRoot, failed[0].id) });
294
294
  }
295
295
  const passedRefs = new Set(receipts.flatMap((receipt) => receipt.check_refs));
296
296
  const missingRefs = frozenGateRefs(gate).filter((ref) => !passedRefs.has(ref));
@@ -429,7 +429,7 @@ export async function repairVnextMerge(context, input) {
429
429
  appendFlowRunTimelineEvent(context, project.id, request.run_id, { type: "merge_source_repair_created", merge_request_id: request.merge_request_id, repair_work_id: repair.repair_work_id, cycle: prepared.cycle, failed_receipt_ids: failed });
430
430
  return { ok: true, run_id: request.run_id, merge_request_id: request.merge_request_id, status: "superseded", source_repair: repair, next: { kind: "start_stage", stage: "code", command: managedLifecycleCommand(context, `${flowCommand(context)} stage start ${request.run_id} --stage code --project-root ${JSON.stringify(projectRoot)} --json --progress-jsonl`) } };
431
431
  }
432
- function mergePrompt(context, input, checks, settings) { const pause = managedLifecycleCommand(context, `${flowCommand(context)} stage pause ${input.run.id} --stage merge --work ${input.request.executor_work_id} --project-root ${JSON.stringify(input.projectRoot)} --question-stdin --json`); const repair = `${flowCommand(context)} merge repair ${input.request.merge_request_id} --project-root ${JSON.stringify(input.projectRoot)} --json`; return ["<stage_identity>", `- RUN: ${input.run.id}`, `- MERGE request: ${input.request.merge_request_id}`, `- Work: ${input.request.executor_work_id}`, "- stage: merge", "</stage_identity>", "", "<trusted_runtime_context>", `- integration workspace: ${input.request.target_workspace}`, `- source workspace: ${input.request.source_workspace}`, `- frozen source commit: ${input.request.source_commit}`, `- target branch: ${input.request.target_branch}`, `- execution target baseline: ${input.request.execution_target_head}`, `- queue route: ${input.request.execution_route}`, `- delivery: ${JSON.stringify(settings.merge_delivery)}`, `- cleanup: ${JSON.stringify(settings.merge_cleanup)}`, "These facts and the acquired project integration lane were established by dd-flow. Do not repeat discovery and do not run git merge/rebase/squash yourself.", "</trusted_runtime_context>", "", "<effective_merge_gate>", ...checks.map((check) => `- ${check.canonical_ref ?? check.id}: ${check.command} — ${check.purpose}`), "</effective_merge_gate>", "", "<execution_contract>", `1. Run this exact standalone command first: ${applyCommand(context, input.request, input.projectRoot)}`, "2. If it reports conflicts, resolve only the actual unmerged paths in the integration workspace. Do not repeat merge apply and do not edit product code, tests, documentation or configuration merely to make a gate pass.", `3. Write the compact semantic result to ${input.resultPath}:`, "```json", JSON.stringify({ schema_id: "dd-flow/merge-result@1", outcome: "completed", summary: "What was integrated.", conflict_resolution: "How material conflicts were resolved, or empty when none.", verification_summary: "Why the integrated result is ready for deterministic checks.", residual_risks: [] }, null, 2), "```", `4. Finish with this exact standalone command and wait for all progress: ${finishCommand(context, input.run.id, input.request, input.projectRoot)}`, `If a gate fails, read its receipt and logs and determine the cause. For a product defect use ${repair}; it restores the target baseline and opens CODE → independent CODE-REVIEW → replacement MRG. For an environment failure restore the declared environment in this MERGE and use the returned finish command with --retry-check <receipt-id> --reason "<what was restored>". The CLI reruns the real gate and retains the old receipt. Never edit product code in the integration target to make a gate pass.`, "If a material conflict has no reasonable answer in accepted evidence, pause this same Work with the exact heredoc below, ask the returned user_message, then use the exact resume command returned by CLI:", "```sh", stagePauseCommandTemplate(pause), "```", "</execution_contract>", ""].join("\n"); }
432
+ function mergePrompt(context, input, checks, settings) { const pause = managedLifecycleCommand(context, `${flowCommand(context)} stage pause ${input.run.id} --stage merge --work ${input.request.executor_work_id} --project-root ${JSON.stringify(input.projectRoot)} --question-stdin --json`); const repair = repairCommand(context, input.request, input.projectRoot); return ["<stage_identity>", `- RUN: ${input.run.id}`, `- MERGE request: ${input.request.merge_request_id}`, `- Work: ${input.request.executor_work_id}`, "- stage: merge", "</stage_identity>", "", "<trusted_runtime_context>", `- integration workspace: ${input.request.target_workspace}`, `- source workspace: ${input.request.source_workspace}`, `- frozen source commit: ${input.request.source_commit}`, `- target branch: ${input.request.target_branch}`, `- execution target baseline: ${input.request.execution_target_head}`, `- queue route: ${input.request.execution_route}`, `- delivery: ${JSON.stringify(settings.merge_delivery)}`, `- cleanup: ${JSON.stringify(settings.merge_cleanup)}`, "These facts and the acquired project integration lane were established by dd-flow. Do not repeat discovery and do not run git merge/rebase/squash yourself.", "</trusted_runtime_context>", "", "<effective_merge_gate>", ...checks.map((check) => `- ${check.canonical_ref ?? check.id}: ${check.command} — ${check.purpose}`), "</effective_merge_gate>", "", "<execution_contract>", `1. Run this exact standalone command first: ${applyCommand(context, input.request, input.projectRoot)}`, "2. If it reports conflicts, resolve only the actual unmerged paths in the integration workspace. Do not repeat merge apply and do not edit product code, tests, documentation or configuration merely to make a gate pass.", `3. Write the compact semantic result to ${input.resultPath}:`, "```json", JSON.stringify({ schema_id: "dd-flow/merge-result@1", outcome: "completed", summary: "What was integrated.", conflict_resolution: "How material conflicts were resolved, or empty when none.", verification_summary: "Why the integrated result is ready for deterministic checks.", residual_risks: [] }, null, 2), "```", `4. Finish with this exact standalone command and wait for all progress: ${finishCommand(context, input.run.id, input.request, input.projectRoot)}`, `If a gate fails, read its receipt and logs and determine the cause. For a product defect use ${repair}; it restores the target baseline and opens CODE → independent CODE-REVIEW → replacement MRG. For an environment failure restore the declared environment in this MERGE and use the returned finish command with --retry-check <receipt-id> --reason "<what was restored>". The CLI reruns the real gate and retains the old receipt. Never edit product code in the integration target to make a gate pass.`, "If a material conflict has no reasonable answer in accepted evidence, pause this same Work with the exact heredoc below, ask the returned user_message, then use the exact resume command returned by CLI:", "```sh", stagePauseCommandTemplate(pause), "```", "</execution_contract>", ""].join("\n"); }
433
433
  function mergeReport(context, run, request, semantic, receipts) { const now = context.now(); const cleanup = cleanupReceiptPath(run); return { schema_id: "dd-flow/stage-report@2", run_id: run.id, stage, generated_at: now, verdict: "done", summary: semantic.summary, semantic: { result: semantic.summary, acceptance: ["source_commit_frozen", "integration_commit_created", "merge_gate_passed", "delivery_confirmed"], changed_files: [], checks: receipts.map((item) => item.command), evidence: [applyReceiptPath(context, request), ...receipts.map((item) => item.receipt_path), ...(fs.existsSync(cleanup) ? [cleanup] : [])], next_action: "merge_completed", merge: { merge_request_id: request.merge_request_id, work_id: request.executor_work_id, protocols: JSON.parse(request.protocol_ids_json), source_commit: request.source_commit, execution_target_head: request.execution_target_head, accepted_tree: request.accepted_tree, integration_commit: request.integration_commit, route: request.execution_route, delivery: executionSettings(run).merge_delivery, cleanup: executionSettings(run).merge_cleanup, verification_summary: semantic.verification_summary, residual_risks: semantic.residual_risks } }, mechanical: { started_at: request.lock_acquired_at, finished_at: now, git: gitFacts(request.target_workspace), queue: queueStatus(context, request) }, artifacts: { json: "stage-report.json", markdown: "stage-report.md", html: "stage-report.html" }, validation: { status: "passed" } }; }
434
434
  function effectiveMergeChecks(run, request) { return readFrozenMergeGate(path.join(requireHome(run), stageDir, "merge-gate.json"), request.merge_request_id).checks; }
435
435
  function planChecks(workspace, protocols) { return protocols.flatMap((protocol) => { const file = path.join(workspace, ".memory-bank", "protocol", protocol, "plan.json"); if (!fs.existsSync(file))
@@ -588,5 +588,6 @@ function findRootWork(context, projectId, runId) { const work = context.db.get("
588
588
  throw new AppError("runtime_missing", "vNext RUN has no root Work", 1); return work; }
589
589
  function requireRootWork(context, projectId, runId) { const work = findRootWork(context, projectId, runId); if (work.status !== "running")
590
590
  throw new AppError("runtime_missing", "vNext RUN has no running root Work", 1); return work; }
591
- function applyCommand(context, request, projectRoot) { return `${flowCommand(context)} merge apply ${request.merge_request_id} --work ${request.executor_work_id} --project-root ${JSON.stringify(projectRoot)} --json --progress-jsonl`; }
591
+ function applyCommand(context, request, projectRoot) { return managedLifecycleCommand(context, `${flowCommand(context)} merge apply ${request.merge_request_id} --work ${request.executor_work_id} --project-root ${JSON.stringify(projectRoot)} --json --progress-jsonl`); }
592
+ function repairCommand(context, request, projectRoot) { return managedLifecycleCommand(context, `${flowCommand(context)} merge repair ${request.merge_request_id} --project-root ${JSON.stringify(projectRoot)} --json`); }
592
593
  export function finishCommand(context, runId, request, projectRoot, retryCheckId) { const retry = retryCheckId ? ` --retry-check ${retryCheckId} --reason "<environment recovery evidence>"` : ""; return managedLifecycleCommand(context, `${flowCommand(context)} stage finish ${runId} --stage merge --request ${request.merge_request_id} --work ${request.executor_work_id} --project-root ${JSON.stringify(projectRoot)} --json --progress-jsonl${retry}`); }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deksden-com/dd-flow-cli",
3
- "version": "0.9.0-beta.83",
3
+ "version": "0.9.0-beta.86",
4
4
  "description": "Mechanical runtime CLI for dd-flow workflows.",
5
5
  "type": "module",
6
6
  "bin": {