@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 +21 -0
- package/dist/build-info.json +3 -3
- package/dist/cli/run-cli.js +26 -5
- package/dist/services/controller-fanout.js +2 -2
- package/dist/services/hooks.js +2 -2
- package/dist/services/lifecycle-command.js +4 -0
- package/dist/services/lifecycle-invocations.js +97 -33
- package/dist/services/run-controller.js +1 -1
- package/dist/services/vnext-merge.js +4 -3
- package/package.json +1 -1
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
|
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.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",
|
package/dist/cli/run-cli.js
CHANGED
|
@@ -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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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."
|
package/dist/services/hooks.js
CHANGED
|
@@ -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(
|
|
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(
|
|
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
|
|
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,
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
|
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:
|
|
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 =
|
|
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}`); }
|