@deksden-com/dd-flow-cli 0.9.0-beta.90 → 0.9.0-beta.92
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 +13 -0
- package/dist/build-info.json +3 -3
- package/dist/cli/command-inputs.js +5 -4
- package/dist/cli/input-preparation.js +4 -3
- package/dist/cli/run-cli.js +13 -3
- package/dist/harness-runtime/lib/dd-zcode.mjs +6 -2
- package/dist/services/lifecycle-command.js +2 -27
- package/dist/services/lifecycle-contract.js +30 -0
- package/dist/services/lifecycle-invocations.js +70 -3
- package/dist/services/run-controller.js +6 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# @deksden-com/dd-flow-cli
|
|
2
2
|
|
|
3
|
+
## 0.9.0-beta.92
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 03fea2a: Accept the behaviorally qualified ZCode 0.16.9 and zcode-acp 0.43.2 harness overlay tuple in the managed runtime gate.
|
|
8
|
+
|
|
9
|
+
## 0.9.0-beta.91
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 8650f0f: Unify managed lifecycle command syntax, preserve terminal preparation errors,
|
|
14
|
+
bound unchanged retries, and retain causal controller failures through MERGE.
|
|
15
|
+
|
|
3
16
|
## 0.9.0-beta.90
|
|
4
17
|
|
|
5
18
|
### 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.92",
|
|
4
|
+
"cli_commit": "c1017bf64843d6979b9dee222dc0831baff6ad0a",
|
|
5
|
+
"built_at": "2026-09-22T04:33:57.178Z",
|
|
6
6
|
"built_with_canon": {
|
|
7
7
|
"version": "4.1.1",
|
|
8
8
|
"commit": "d1a6081ab15ab92ac917ff5d037121a40c709db1",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { AppError } from "../shared/errors.js";
|
|
2
2
|
import { parseCommandArgs } from "../services/lifecycle-command.js";
|
|
3
|
+
import { lifecycleInternalOptions, lifecycleRouteForOperation } from "../services/lifecycle-contract.js";
|
|
3
4
|
import { isUpgradeAllowlisted } from "../services/cli-operation-classifier.js";
|
|
4
5
|
const words = (value) => value.split(" ").filter(Boolean);
|
|
5
6
|
const route = (positions, required = "", values = "", switches = "", repeatable = "", enums = {}) => ({
|
|
@@ -8,7 +9,7 @@ const route = (positions, required = "", values = "", switches = "", repeatable
|
|
|
8
9
|
});
|
|
9
10
|
const project = "project-root";
|
|
10
11
|
const scoped = "project-root"; // Optional when the router resolves a unique retained scope.
|
|
11
|
-
const lifecycle = "
|
|
12
|
+
const lifecycle = lifecycleInternalOptions.join(" ");
|
|
12
13
|
const worker = "worker-id";
|
|
13
14
|
const control = "project-root run";
|
|
14
15
|
/** Path-shaped arguments default to physical paths unless the route explicitly
|
|
@@ -154,8 +155,8 @@ export const commandInputs = {
|
|
|
154
155
|
"merge serve": route(0, "agent-profile", "poll-seconds max-parallel-projects", "once"),
|
|
155
156
|
"merge server status": route(0),
|
|
156
157
|
"merge server stop": route(1),
|
|
157
|
-
"merge apply": route(1, `${project} work
|
|
158
|
-
"merge repair": route(1, project),
|
|
158
|
+
"merge apply": route(1, `${project} work`, lifecycle),
|
|
159
|
+
"merge repair": route(1, project, lifecycle),
|
|
159
160
|
"merge request status": route(1, project),
|
|
160
161
|
"merge request route": route(1, `${project} mode reason`, "", "", "", { mode: ["same_session", "server"] }),
|
|
161
162
|
"merge status": route(0, project, "path"),
|
|
@@ -225,7 +226,7 @@ export const commandInputs = {
|
|
|
225
226
|
"memory permissions preflight": route(0, "root memory-bank flow mode", "tasks")
|
|
226
227
|
};
|
|
227
228
|
export function contextualPathOptionsForLifecycle(operation) {
|
|
228
|
-
const key = operation
|
|
229
|
+
const key = lifecycleRouteForOperation(operation);
|
|
229
230
|
return new Set(commandInputs[key]?.contextualPaths ?? []);
|
|
230
231
|
}
|
|
231
232
|
const flag = "__dd_flow_flag__";
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import crypto from "node:crypto";
|
|
3
4
|
import { AppError, isAppError } from "../shared/errors.js";
|
|
4
5
|
import { validateSchema } from "../services/schema-validation.js";
|
|
5
6
|
export function prepareError(error, input = {}) {
|
|
@@ -19,8 +20,8 @@ export function prepareError(error, input = {}) {
|
|
|
19
20
|
throw new AppError(error.code, error.message, 2, {
|
|
20
21
|
...error.details,
|
|
21
22
|
phase: "prepare",
|
|
22
|
-
effect: "no_effect",
|
|
23
|
-
recoverable: true,
|
|
23
|
+
effect: error.details.effect ?? "no_effect",
|
|
24
|
+
recoverable: error.details.recoverable ?? true,
|
|
24
25
|
...input
|
|
25
26
|
});
|
|
26
27
|
}
|
|
@@ -68,7 +69,7 @@ export function prepareJsonText(text, input) {
|
|
|
68
69
|
// safe correction-and-retry condition.
|
|
69
70
|
throw new AppError("invalid_json", `Invalid JSON in ${input.label ?? "input"}: ${input.file}`, 2, {
|
|
70
71
|
phase: "prepare", effect: "no_effect", recoverable: true, file: input.file, label: input.label ?? "input",
|
|
71
|
-
cause: "Invalid JSON syntax"
|
|
72
|
+
cause: "Invalid JSON syntax", input_sha256: crypto.createHash("sha256").update(text).digest("hex")
|
|
72
73
|
});
|
|
73
74
|
}
|
|
74
75
|
}
|
package/dist/cli/run-cli.js
CHANGED
|
@@ -217,7 +217,17 @@ export async function runCli(args, io = defaultIo, env = process.env) {
|
|
|
217
217
|
if (invocationId) {
|
|
218
218
|
const command = quote(["dd-flow", ...invocationArgs]);
|
|
219
219
|
const admission = await awaitLifecycleInvocation(dispatchContext, { id: invocationId, command,
|
|
220
|
-
validateArgs: () => {
|
|
220
|
+
validateArgs: () => {
|
|
221
|
+
try {
|
|
222
|
+
preparedRoute = prepareCommandRoute(invocationArgs);
|
|
223
|
+
}
|
|
224
|
+
catch (error) {
|
|
225
|
+
if (error instanceof AppError && error.code === "usage" && ["invocation-id", "hook-event-id"].includes(String(error.details.parameter ?? ""))) {
|
|
226
|
+
throw new AppError("lifecycle_contract_invalid", `The runtime generated arguments rejected by its own CLI contract: ${error.message}`, 1, { ...error.details, recoverable: false, operation: observedOperation });
|
|
227
|
+
}
|
|
228
|
+
throw error;
|
|
229
|
+
}
|
|
230
|
+
},
|
|
221
231
|
prepare: async () => { preparedInput = await prepareCliInput(invocationArgs, dispatchContext, io, scopeProjectRoot, output.responseFile, preparedRoute); },
|
|
222
232
|
assertCurrent: scope => assertLifecycleInvocationCurrent(dispatchContext, scope, command) });
|
|
223
233
|
if (admission.replay) {
|
|
@@ -3330,7 +3340,7 @@ function projectRootForMutation(context, args, result, resolvedScope) {
|
|
|
3330
3340
|
return projectRootForMergeQueueMutation(context, command, parsed);
|
|
3331
3341
|
}
|
|
3332
3342
|
if (family === "merge" && ["one-shot", "apply", "repair", "request"].includes(command ?? "")) {
|
|
3333
|
-
return requiredOption(parsed, "project-root");
|
|
3343
|
+
return resolvedScope ?? requiredOption(parsed, "project-root");
|
|
3334
3344
|
}
|
|
3335
3345
|
if (family === "merge-worker" && ["start", "stop"].includes(command ?? "")) {
|
|
3336
3346
|
return requiredOption(parsed, "project-root");
|
|
@@ -3348,7 +3358,7 @@ function projectRootForMutation(context, args, result, resolvedScope) {
|
|
|
3348
3358
|
return requiredOption(parsed, "project-root");
|
|
3349
3359
|
}
|
|
3350
3360
|
if (family === "stage" && ["start", "pause", "resume", "finish"].includes(command ?? "")) {
|
|
3351
|
-
return requiredOption(parsed, "project-root");
|
|
3361
|
+
return resolvedScope ?? requiredOption(parsed, "project-root");
|
|
3352
3362
|
}
|
|
3353
3363
|
if (family === "worktree" && ["create", "bootstrap", "close"].includes(command ?? "")) {
|
|
3354
3364
|
return protocolProjectRoot(context, requiredOption(parsed, "protocol-id"));
|
|
@@ -753,8 +753,12 @@ export function zcodeLifecycleQualification(sha256, bridgeCommit, harnessContrac
|
|
|
753
753
|
// Behavioural evidence: dd-eval production-invocations probe, 2026-09-13.
|
|
754
754
|
// Pin both providers of native event identity, not a string in the bundle.
|
|
755
755
|
// 60af0d3 adds retainedSubagents without changing native event forwarding.
|
|
756
|
-
const
|
|
757
|
-
|
|
756
|
+
const qualifiedTuples = new Map([
|
|
757
|
+
["e9f1868c0fdb863537ed910ee3828b9be96b8c2fd805473f63b439e1113266b8", new Set(["e0600fe3c46e215695f257e65aa6f674ced998d8", "60af0d31e13076a313d9770f10aa70f7c94742cf"])],
|
|
758
|
+
["500ae84fa2cb8dd74c1264e5f3d3709c004c59b58899f1b0c3e205137a0ef466", new Set(["8c0a893f3c26a0b96cb138acf762db84c800298b"])],
|
|
759
|
+
]);
|
|
760
|
+
const qualified = qualifiedTuples.get(sha256)?.has(bridgeCommit) === true
|
|
761
|
+
&& harnessContract === ZCODE_HARNESS_CONTRACT;
|
|
758
762
|
return { status: qualified ? "qualified" : "unqualified", mechanism: "cli-invocation@1", sha256,
|
|
759
763
|
capabilities: qualified ? ["root", "concurrent_children", "child_continuation"] : [],
|
|
760
764
|
native_nested_children: false, reason: qualified ? null : "native_event_contract_unqualified" };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { parse, quote } from "shell-quote";
|
|
3
|
+
import { lifecycleOperationForArgv } from "./lifecycle-contract.js";
|
|
3
4
|
/** Native hooks and the lightweight ingress must recognize the same shell. */
|
|
4
5
|
export function lifecycleCommandFromPayload(payload) {
|
|
5
6
|
const input = payload.tool_input && typeof payload.tool_input === "object" ? payload.tool_input : {};
|
|
@@ -203,33 +204,7 @@ function wrappedInvocationFromSegment(segment) {
|
|
|
203
204
|
return parseLifecycleCommandInner(payload, true);
|
|
204
205
|
}
|
|
205
206
|
function lifecycleOperation(argv) {
|
|
206
|
-
|
|
207
|
-
if (argv.some(arg => arg === "--help" || arg === "-h"))
|
|
208
|
-
return null;
|
|
209
|
-
if (argv[0] === "run" && argv[1] === "recovery" && argv[2] === "accept")
|
|
210
|
-
return "recovery_accept";
|
|
211
|
-
const key = `${argv[0] ?? ""} ${argv[1] ?? ""}`;
|
|
212
|
-
if (key === "session register")
|
|
213
|
-
return "session_register";
|
|
214
|
-
if (key === "stage start")
|
|
215
|
-
return "stage_start";
|
|
216
|
-
if (key === "stage finish")
|
|
217
|
-
return "stage_finish";
|
|
218
|
-
if (key === "stage pause")
|
|
219
|
-
return "stage_pause";
|
|
220
|
-
if (key === "stage resume")
|
|
221
|
-
return "stage_resume";
|
|
222
|
-
if (key === "work start")
|
|
223
|
-
return "work_start";
|
|
224
|
-
if (key === "work finish")
|
|
225
|
-
return "work_finish";
|
|
226
|
-
if (key === "work fail")
|
|
227
|
-
return "work_fail";
|
|
228
|
-
if (key === "merge apply")
|
|
229
|
-
return "merge_apply";
|
|
230
|
-
if (key === "merge repair")
|
|
231
|
-
return "merge_repair";
|
|
232
|
-
return null;
|
|
207
|
+
return lifecycleOperationForArgv(argv);
|
|
233
208
|
}
|
|
234
209
|
export function parseCommandArgs(argv) {
|
|
235
210
|
const positional = [];
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export const lifecycleRoutes = {
|
|
2
|
+
"session register": "session_register",
|
|
3
|
+
"stage start": "stage_start",
|
|
4
|
+
"stage finish": "stage_finish",
|
|
5
|
+
"stage pause": "stage_pause",
|
|
6
|
+
"stage resume": "stage_resume",
|
|
7
|
+
"work start": "work_start",
|
|
8
|
+
"work finish": "work_finish",
|
|
9
|
+
"work fail": "work_fail",
|
|
10
|
+
"merge apply": "merge_apply",
|
|
11
|
+
"merge repair": "merge_repair",
|
|
12
|
+
"run recovery accept": "recovery_accept"
|
|
13
|
+
};
|
|
14
|
+
export const lifecycleInternalOptions = ["invocation-id", "hook-event-id"];
|
|
15
|
+
export function lifecycleOperationForArgv(argv) {
|
|
16
|
+
if (argv.some(arg => arg === "--help" || arg === "-h"))
|
|
17
|
+
return null;
|
|
18
|
+
for (const [route, operation] of Object.entries(lifecycleRoutes)) {
|
|
19
|
+
const parts = route.split(" ");
|
|
20
|
+
if (parts.every((part, index) => argv[index] === part))
|
|
21
|
+
return operation;
|
|
22
|
+
}
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
export function lifecycleRouteForOperation(operation) {
|
|
26
|
+
const entry = Object.entries(lifecycleRoutes).find(([, value]) => value === operation);
|
|
27
|
+
if (!entry)
|
|
28
|
+
throw new Error(`Unknown lifecycle operation: ${operation}`);
|
|
29
|
+
return entry[0];
|
|
30
|
+
}
|
|
@@ -67,6 +67,8 @@ export function isRetryableLifecycleRejection(code, details = {}) {
|
|
|
67
67
|
return false;
|
|
68
68
|
if (code === "validation" || code === "schema_validation")
|
|
69
69
|
return details.phase === "prepare" && details.effect === "no_effect" && details.recoverable === true;
|
|
70
|
+
if (["invocation_command_mismatch", "invocation_argument_mismatch"].includes(code))
|
|
71
|
+
return details.recoverable !== false;
|
|
70
72
|
return details.recoverable === true || ["work_checks_failed", "code_gate_failed", "code_review_gate_failed", "merge_gate_failed",
|
|
71
73
|
"review_repair_incomplete", "plan_review_incomplete", "semantic_result_invalid",
|
|
72
74
|
"review_evidence_invalid", "evidence_obligation_unknown", "invalid_evidence_ref",
|
|
@@ -966,13 +968,78 @@ export function settleLifecyclePreparationRejection(context, id, error) {
|
|
|
966
968
|
const scope = JSON.parse(row.scope_json);
|
|
967
969
|
if (scope.runId)
|
|
968
970
|
assertLifecycleInvocationCurrent(context, scope, row.command);
|
|
969
|
-
const
|
|
970
|
-
|
|
971
|
-
|
|
971
|
+
const signature = preparationRejectionSignature(error);
|
|
972
|
+
const repeated = context.db.all("SELECT outcome_json FROM lifecycle_invocations WHERE scope_json = ? AND fingerprint = ? AND id <> ? AND status = 'settled'", [row.scope_json, row.fingerprint, row.id])
|
|
973
|
+
.some(prior => { try {
|
|
974
|
+
return JSON.parse(prior.outcome_json ?? "null")?.error?.details?.rejection_signature === signature;
|
|
975
|
+
}
|
|
976
|
+
catch {
|
|
977
|
+
return false;
|
|
978
|
+
} });
|
|
979
|
+
const retryable = !repeated && isRetryableLifecycleRejection(error.code, error.details);
|
|
980
|
+
const details = { ...error.details, effect: "no_effect", recoverable: retryable, rejection_signature: signature,
|
|
981
|
+
...(repeated ? { repeated_without_progress: true } : {}),
|
|
982
|
+
...(retryable ? {
|
|
983
|
+
retry_command: successorLifecycleInvocationCommand(context, row),
|
|
984
|
+
retry_instruction: "Correct the reported input, then execute retry_command verbatim in this same Session. The rejected call changed no lifecycle state."
|
|
985
|
+
} : {}) };
|
|
972
986
|
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]);
|
|
973
987
|
return details;
|
|
974
988
|
});
|
|
975
989
|
}
|
|
990
|
+
function preparationRejectionSignature(error) {
|
|
991
|
+
const omitted = new Set(["retry_command", "retry_instruction", "invocation_id", "hook_event_id", "rejection_signature"]);
|
|
992
|
+
const normalize = (value) => {
|
|
993
|
+
if (Array.isArray(value))
|
|
994
|
+
return value.map(normalize);
|
|
995
|
+
if (!value || typeof value !== "object")
|
|
996
|
+
return value;
|
|
997
|
+
return Object.fromEntries(Object.entries(value).filter(([key]) => !omitted.has(key)).sort(([a], [b]) => a.localeCompare(b)).map(([key, item]) => [key, normalize(item)]));
|
|
998
|
+
};
|
|
999
|
+
return crypto.createHash("sha256").update(JSON.stringify([error.code, normalize(error.details)])).digest("hex");
|
|
1000
|
+
}
|
|
1001
|
+
export function currentLifecycleFailure(context, input) {
|
|
1002
|
+
const attempts = context.db.all("SELECT * FROM lifecycle_invocations ORDER BY updated_at DESC, rowid DESC");
|
|
1003
|
+
const scoped = attempts.filter(row => {
|
|
1004
|
+
try {
|
|
1005
|
+
const scope = JSON.parse(row.scope_json);
|
|
1006
|
+
return scope.runId === input.runId && scope.generation === input.generation;
|
|
1007
|
+
}
|
|
1008
|
+
catch {
|
|
1009
|
+
return false;
|
|
1010
|
+
}
|
|
1011
|
+
});
|
|
1012
|
+
const fingerprints = [...new Set(scoped.map(row => row.fingerprint))];
|
|
1013
|
+
for (const fingerprint of fingerprints) {
|
|
1014
|
+
const rows = scoped.filter(row => row.fingerprint === fingerprint);
|
|
1015
|
+
if (rows.some(row => ["issued", "observed", "executing"].includes(row.status)))
|
|
1016
|
+
continue;
|
|
1017
|
+
const row = rows.find(candidate => candidate.status === "settled");
|
|
1018
|
+
if (!row)
|
|
1019
|
+
continue;
|
|
1020
|
+
let outcome;
|
|
1021
|
+
try {
|
|
1022
|
+
outcome = JSON.parse(row.outcome_json ?? "null") ?? {};
|
|
1023
|
+
}
|
|
1024
|
+
catch {
|
|
1025
|
+
continue;
|
|
1026
|
+
}
|
|
1027
|
+
if (!outcome.error?.code || !outcome.error.message)
|
|
1028
|
+
continue;
|
|
1029
|
+
let parsed;
|
|
1030
|
+
try {
|
|
1031
|
+
parsed = invocation(row.command);
|
|
1032
|
+
}
|
|
1033
|
+
catch {
|
|
1034
|
+
continue;
|
|
1035
|
+
}
|
|
1036
|
+
const stage = parsed.operation.startsWith("merge_") ? "merge" : commandOption(parsed, "stage");
|
|
1037
|
+
if (stage !== input.stage)
|
|
1038
|
+
continue;
|
|
1039
|
+
return { code: outcome.error.code, message: outcome.error.message, details: outcome.error.details ?? {}, invocation_id: row.id };
|
|
1040
|
+
}
|
|
1041
|
+
return null;
|
|
1042
|
+
}
|
|
976
1043
|
/** One caller owns execution; a lost process leaves an explicit unknown
|
|
977
1044
|
* outcome, never an automatic replay. Existing recovery decides the next step. */
|
|
978
1045
|
export async function awaitLifecycleInvocation(context, input) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { assertLifecycleOutcomes, managedInvocationContext, managedLifecycleCommand } from "./lifecycle-invocations.js";
|
|
1
|
+
import { assertLifecycleOutcomes, currentLifecycleFailure, managedInvocationContext, managedLifecycleCommand } from "./lifecycle-invocations.js";
|
|
2
2
|
import crypto from "node:crypto";
|
|
3
3
|
import fs from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
@@ -711,8 +711,12 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
711
711
|
const children = controllerNativeChildren(state.last_receipt ?? {}, session.id).filter(child => !baseline.has(child.session_id));
|
|
712
712
|
const fanout = await nextControllerFanout(executionContext, { projectRoot: run.project_root, runId: run.id, stage: next.stage, harness: runtimeHarness(session.profile), sessionId: session.id, children, policy, profiles });
|
|
713
713
|
assertOwner();
|
|
714
|
-
if (fanout.kind === "none")
|
|
714
|
+
if (fanout.kind === "none") {
|
|
715
|
+
const cause = currentLifecycleFailure(executionContext, { runId: run.id, generation: row.generation, stage: next.stage });
|
|
716
|
+
if (cause)
|
|
717
|
+
throw new AppError(cause.code, cause.message, 1, { ...cause.details, invocation_id: cause.invocation_id, controller_wrapper: "incomplete_subject_turn", stage: next.stage });
|
|
715
718
|
throw new AppError("incomplete_subject_turn", "Managed Turn ended without a completed Stage or declared continuation", 1, { stage: next.stage });
|
|
719
|
+
}
|
|
716
720
|
if (fanout.kind === "wait") {
|
|
717
721
|
setStatus(fanout.reason === "qualified_capacity_required" ? "waiting_for_capacity" : "waiting_for_children");
|
|
718
722
|
await delay(1000);
|