@deksden-com/dd-flow-cli 0.9.0-beta.74 → 0.9.0-beta.79
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 +8 -0
- package/dist/build-info.json +6 -6
- package/dist/cli/command-inputs.js +300 -0
- package/dist/cli/help.js +1 -1
- package/dist/cli/hook-ingress.js +82 -0
- package/dist/cli/input-preparation.js +115 -0
- package/dist/cli/run-cli.js +1015 -423
- package/dist/cli.js +6 -2
- package/dist/harness-runtime/lib/dd-codex-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +7 -2
- package/dist/harness-runtime/lib/dd-codex.mjs +137 -13
- package/dist/harness-runtime/lib/dd-droid.mjs +3 -2
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +1 -1
- package/dist/harness-runtime/lib/delegation-instructions.d.mts +10 -0
- package/dist/harness-runtime/lib/delegation-instructions.mjs +130 -0
- package/dist/harness-runtime/lib/native-hook-command.d.mts +1 -0
- package/dist/services/canon.js +11 -8
- package/dist/services/cleanup.js +49 -8
- package/dist/services/cli-operation-classifier.js +12 -24
- package/dist/services/codex-hook-delivery.js +28 -0
- package/dist/services/config.js +11 -8
- package/dist/services/controller-fanout.js +7 -11
- package/dist/services/dashboard.js +94 -31
- package/dist/services/engines.js +75 -61
- package/dist/services/eval-snapshots.js +138 -38
- package/dist/services/execution-policy.js +5 -12
- package/dist/services/hooks.js +226 -203
- package/dist/services/lanes.js +60 -52
- package/dist/services/lifecycle-command.js +13 -1
- package/dist/services/lifecycle-invocations.js +285 -94
- package/dist/services/managed-processes.js +158 -18
- package/dist/services/merge-queue.js +173 -98
- package/dist/services/migrations.js +13 -8
- package/dist/services/plan-runtime.js +19 -8
- package/dist/services/plans.js +28 -25
- package/dist/services/projects.js +11 -2
- package/dist/services/prompts.js +23 -16
- package/dist/services/protocols.js +54 -19
- package/dist/services/recovery-observation-budget.js +1 -1
- package/dist/services/run-controller-adapter.js +31 -20
- package/dist/services/run-controller-state.js +5 -0
- package/dist/services/run-controller.js +102 -56
- package/dist/services/run-fork.js +36 -22
- package/dist/services/run-recovery.js +32 -17
- package/dist/services/runs.js +110 -38
- package/dist/services/runtime-budget.js +120 -48
- package/dist/services/runtime-scope-capture.js +2 -2
- package/dist/services/runtime-scope-control.js +2 -2
- package/dist/services/runtime-scope-resume.js +34 -12
- package/dist/services/runtime-scope-worker.js +49 -16
- package/dist/services/runtime-service.js +8 -7
- package/dist/services/schema-validation.js +9 -7
- package/dist/services/sessions.js +35 -69
- package/dist/services/stage-blocker.js +17 -7
- package/dist/services/stage-context.js +42 -16
- package/dist/services/stage-lifecycle.js +82 -100
- package/dist/services/stage-pause.js +54 -24
- package/dist/services/vnext-code-review.js +82 -32
- package/dist/services/vnext-code.js +78 -45
- package/dist/services/vnext-fanout.js +13 -12
- package/dist/services/vnext-merge.js +68 -35
- package/dist/services/vnext-plan-review.js +85 -42
- package/dist/services/vnext-plan.js +91 -70
- package/dist/services/vnext-protocolize.js +116 -50
- package/dist/services/vnext-specify.js +70 -57
- package/dist/services/work-registry.js +207 -94
- package/dist/services/worktrees.js +85 -35
- package/dist/storage/database.js +104 -10
- package/dist/storage/paths.js +17 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# @deksden-com/dd-flow-cli
|
|
2
2
|
|
|
3
|
+
## 0.9.0-beta.75
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Use adapter-owned native delegation instructions for Codex and the other
|
|
8
|
+
supported harnesses, preserving independent child Work ownership and clearer
|
|
9
|
+
native-session diagnostics across fan-out and recovery.
|
|
10
|
+
|
|
3
11
|
## 0.9.0-beta.64
|
|
4
12
|
|
|
5
13
|
### Patch Changes
|
package/dist/build-info.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
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.79",
|
|
4
|
+
"cli_commit": "f8f5436ce8399616d7da953991c13e9c8b7e1004",
|
|
5
|
+
"built_at": "2026-09-19T21:50:05.682Z",
|
|
6
6
|
"built_with_canon": {
|
|
7
7
|
"version": "4.1.1",
|
|
8
8
|
"commit": "97f811d33c212ae3497020178b1ed825c7c3ebac",
|
|
9
9
|
"flow_contract": "dd-flow-canonical-2026-08",
|
|
10
|
-
"repo_root": "/
|
|
11
|
-
"memorybank_root": "/
|
|
12
|
-
"flow_root": "/
|
|
10
|
+
"repo_root": "/Users/deksden/Documents/_Projects/dd-memorybank",
|
|
11
|
+
"memorybank_root": "/Users/deksden/Documents/_Projects/dd-memorybank/.memory-bank",
|
|
12
|
+
"flow_root": "/Users/deksden/Documents/_Projects/dd-memorybank/.memory-bank/dd-flow",
|
|
13
13
|
"layout": "dot_memory_bank"
|
|
14
14
|
}
|
|
15
15
|
}
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import { AppError } from "../shared/errors.js";
|
|
2
|
+
import { parseCommandArgs } from "../services/lifecycle-command.js";
|
|
3
|
+
import { isUpgradeAllowlisted } from "../services/cli-operation-classifier.js";
|
|
4
|
+
const words = (value) => value.split(" ").filter(Boolean);
|
|
5
|
+
const route = (positions, required = "", values = "", switches = "", repeatable = "", enums = {}) => ({
|
|
6
|
+
positions: typeof positions === "number" ? [positions, positions] : positions,
|
|
7
|
+
required: words(required), values: words(`${required} ${values}`), switches: words(switches), repeatable: words(repeatable), enums
|
|
8
|
+
});
|
|
9
|
+
const project = "project-root";
|
|
10
|
+
const scoped = "project-root"; // Optional when the router resolves a unique retained scope.
|
|
11
|
+
const lifecycle = "invocation-id hook-event-id";
|
|
12
|
+
const worker = "worker-id";
|
|
13
|
+
const control = "project-root run";
|
|
14
|
+
export const commandInputs = {
|
|
15
|
+
"version": route(0),
|
|
16
|
+
"status": route(0, "", "root project-root", "check-registry"),
|
|
17
|
+
"project register": route(0, "root"),
|
|
18
|
+
"project status": route(0, "root"),
|
|
19
|
+
"project summary": route(0, project, "write"),
|
|
20
|
+
"project resolve": route(1),
|
|
21
|
+
"project archive": route([0, 1], "reason", "root"),
|
|
22
|
+
"project migrate-ids": route(0, "root", "", "apply"),
|
|
23
|
+
"project config status": route(0, project),
|
|
24
|
+
"project config set": route(0, `${project} key value`),
|
|
25
|
+
"protocol register": route(1, project, "workspace-path"),
|
|
26
|
+
"protocol status": route(1, "", scoped),
|
|
27
|
+
"protocol branch-status": route([0, 1], "", `${scoped} path`),
|
|
28
|
+
"protocol ready": route(0, project),
|
|
29
|
+
"protocol blockers": route(1, project),
|
|
30
|
+
"protocol implement": route(1, "", `${scoped} reason`, "force"),
|
|
31
|
+
"protocol ready-for-merge": route(1, "", scoped),
|
|
32
|
+
"protocol transition": route(1, "to", `${scoped} payload-file json-file reason`, "force"),
|
|
33
|
+
"transition": route(1, "to", `${scoped} payload-file json-file reason`, "force"),
|
|
34
|
+
"protocol sync-from-run": route(1, "run", `${scoped} target`),
|
|
35
|
+
"protocol cancel": route(1, "reason", `${scoped} close-sessions cancel-queue release-locks worktree`, "force"),
|
|
36
|
+
"plan status": route(1, "", scoped),
|
|
37
|
+
"plan reviews": { ...route(0), retired: "PLAN review dispatch moved to: dd-flow plan-review dispatch <RUN>" },
|
|
38
|
+
"plan item start": route(2, "", scoped),
|
|
39
|
+
"plan item done": route(2, "summary", `${scoped} evidence`, "", "evidence"),
|
|
40
|
+
"plan item block": route(2, "reason user-required", scoped),
|
|
41
|
+
"plan item skip": route(2, "reason", scoped),
|
|
42
|
+
"plan-review dispatch": route(1, "", scoped),
|
|
43
|
+
"prompt render": route(0, `${project} run stage profile`, "workspace-root task-file plan-item"),
|
|
44
|
+
"stage start": { ...route([0, 1], `${project} stage`, `${lifecycle} dir subject intake-file context-file context-sha256`, "bootstrap intake-stdin require-session-binding"), contextualPaths: words("project-root intake-file context-file") },
|
|
45
|
+
"stage finish": { ...route(1, `${project} stage`, `${lifecycle} dir semantic-file data result-file outcome decision-file verification-file retry-check reason request work`, "result-stdin"), contextualPaths: words("project-root semantic-file result-file decision-file verification-file") },
|
|
46
|
+
"stage pause": { ...route(1, `${project} stage work`, lifecycle, "question-stdin"), contextualPaths: ["project-root"] },
|
|
47
|
+
"stage resume": { ...route(1, `${project} stage work`, lifecycle, "answer-stdin"), contextualPaths: ["project-root"] },
|
|
48
|
+
"stage block": route(1, `${project} stage work kind code`, "", "summary-stdin retryable", "", { kind: ["engine", "harness", "environment"] }),
|
|
49
|
+
"stage unblock": route(1, `${project} stage work`),
|
|
50
|
+
"stage fanout status": route(1, "stage", scoped),
|
|
51
|
+
"stage fanout dispatch": route(1, "stage", scoped),
|
|
52
|
+
"stage fanout reconcile": route(1, "stage observations-file", scoped),
|
|
53
|
+
"stage native observe": route(1, "stage observations-file", scoped),
|
|
54
|
+
"work status": route(1, project),
|
|
55
|
+
"work add-batch": route(0, "parent file", scoped),
|
|
56
|
+
"work ls": route(0, "", `${scoped} run parent status limit`, "ready include-results", "", { status: ["created", "running", "paused", "completed", "failed", "cancelled"] }),
|
|
57
|
+
"work show": route(1, "", scoped),
|
|
58
|
+
"work repair add": route(0, "run", `${scoped} origin-work from-check from-finding from-unresolved check-ref verification-file`, "task-stdin", "origin-work from-finding from-unresolved check-ref"),
|
|
59
|
+
"work deps add": route(1, "on", scoped, "", "on"),
|
|
60
|
+
"work deps remove": route(1, "on", scoped, "", "on"),
|
|
61
|
+
"work deps list": route(1, "", scoped),
|
|
62
|
+
"work deps clear": route(1, "", scoped),
|
|
63
|
+
"work delete": route(1, "", scoped),
|
|
64
|
+
"work launch": route(1, "", `${scoped} stage`),
|
|
65
|
+
"work start": { ...route(1, "", `${scoped} ${lifecycle} recovery-id`), contextualPaths: ["project-root"] },
|
|
66
|
+
"work finish": { ...route(1, "", `${scoped} ${lifecycle} result-file retry-check reason`, "result-stdin"), contextualPaths: words("project-root result-file") },
|
|
67
|
+
"work fail": { ...route(1, "reason", `${scoped} ${lifecycle}`), contextualPaths: ["project-root"] },
|
|
68
|
+
"work cancel": route(1, "reason", scoped),
|
|
69
|
+
"work retry": route(1, "reason", scoped),
|
|
70
|
+
"session register": { ...route(0, "", `${scoped} ${lifecycle} payload-base64 payload-json payload-file session-id`), contextualPaths: words("project-root payload-file") },
|
|
71
|
+
"session status": route(0, project, "session-id worker-id"),
|
|
72
|
+
"session stop": route(0, `${project} session-id reason`),
|
|
73
|
+
"session stop-worker": route(0, `${project} worker-id reason`),
|
|
74
|
+
"session usage sync": route(0, project, "session-id"),
|
|
75
|
+
"run start": route(0, `${project} flow-kind subject-type subject-id slug`, "workspace-root next-action preset task-profile-file execution-routing-file protocol-override-file flag", "", "flag"),
|
|
76
|
+
"run prepare-vnext-specify": route(0, `${project} slug`, "execution-routing-file"),
|
|
77
|
+
"run status": route(1, project),
|
|
78
|
+
"run list": route(0, project),
|
|
79
|
+
"run timeline": route(1, project, "hide", "", "hide"),
|
|
80
|
+
"run config status": route(1, project),
|
|
81
|
+
"run config set": route(1, `${project} key value reason`),
|
|
82
|
+
"run vars ls": route(1, project),
|
|
83
|
+
"run vars get": route(1, `${project} key`),
|
|
84
|
+
"run vars set": route(1, `${project} key value`),
|
|
85
|
+
"run flags status": route(1, project),
|
|
86
|
+
"run flags revise": route(1, `${project} expected-revision idempotency-key`, "flag preset reason", "allow-downgrade", "flag"),
|
|
87
|
+
"run attach-stage": route(1, `${project} stage dir status`, "data-schema-id"),
|
|
88
|
+
"run complete-stage": route(1, `${project} stage status`, "stage-report dashboard data data-schema-id report alias", "", "alias"),
|
|
89
|
+
"run complete": { ...route(0), retired: "run complete is retired; only an operator may use run override" },
|
|
90
|
+
"run override": route(1, `${project} status reason`, "verdict next-action", "", "", { status: ["cancelled", "failed"] }),
|
|
91
|
+
"run capacity record": route(1, `${project} available-slots`),
|
|
92
|
+
"run recovery inspect": route(1, project),
|
|
93
|
+
"run recovery begin": route(1, `${project} interruption-json`),
|
|
94
|
+
"run recovery seal": route(1, `${project} recovery-id settlement-json`),
|
|
95
|
+
"run recovery capture": route(1, `${project} recovery-id capture-path`),
|
|
96
|
+
"run recovery resume": route(1, `${project} recovery-id daemon-id harness native-session-id`),
|
|
97
|
+
"run recovery accept": { ...route(1, `${project} recovery-id`, lifecycle), contextualPaths: ["project-root"] },
|
|
98
|
+
"run snapshot bootstrap create": route(0, `${project} output`),
|
|
99
|
+
"run snapshot bootstrap restore": route(0, `${project} snapshot`),
|
|
100
|
+
"run snapshot create": route(1, `${project} output`, "recovery-id stage-entry", "candidate incomplete"),
|
|
101
|
+
"run snapshot restore": route(0, `${project} snapshot`, "execution-routing-file recovery-id"),
|
|
102
|
+
"run fork": route(0, "from output engine-version request-id", "integrity-checksum"),
|
|
103
|
+
"run drive status": route(0, control, "after"),
|
|
104
|
+
"run drive launch": route(0, `${control} request-id context-file context-sha256`, "stop-after capture-root"),
|
|
105
|
+
"run drive serve": route(0, `${control} controller-id`),
|
|
106
|
+
"run drive answer": route(0, `${control} request-id pause-id answer-file`),
|
|
107
|
+
"run drive context": route(0, `${control} controller-id request-id stage context-file context-sha256`),
|
|
108
|
+
"run control status": route(0, control),
|
|
109
|
+
"run control reconcile": route(0, `${control} control-id request-id`),
|
|
110
|
+
"run control resume": route(0, `${control} request-id from`, "wait-ms"),
|
|
111
|
+
"run control serve": route(0, `${control} control-id`),
|
|
112
|
+
"run control pause": route(0, `${control} request-id`, "grace-ms wait-ms escalate", "force"),
|
|
113
|
+
"run control stop": route(0, `${control} request-id`, "grace-ms wait-ms escalate", "force"),
|
|
114
|
+
"runtime scope fence": route(0, "scope-id request-id"),
|
|
115
|
+
"runtime scope status": route(0, "scope-id"),
|
|
116
|
+
"runtime scope stop": route(0, "scope-id request-id"),
|
|
117
|
+
"runtime scope control": route(0, "scope-id request-id mode", "manifest-json", "", "", { mode: ["pause", "stop"] }),
|
|
118
|
+
"runtime scope serve": route(0, "scope-id generation"),
|
|
119
|
+
"runtime scope resume": route(0, "scope-id request-id generation capture-key"),
|
|
120
|
+
"runtime scope reconcile": route(0, "scope-id generation request-id"),
|
|
121
|
+
"runtime process check-admission": route(0, "id lease-token"),
|
|
122
|
+
"runtime process start": route(0, `${control} command ports ready-port`, "ready-path ready-timeout-ms"),
|
|
123
|
+
"runtime process stop": route(0, "id lease-token", "grace-ms"),
|
|
124
|
+
"runtime process register": route(0, "kind owner", "lease-ms budget-json project-id run pid process-group-id owner-pid work check operation stdout stderr harness dispatch-barrier role state-dir adapter-executable"),
|
|
125
|
+
"runtime process confirm": route(0, "id lease-token pid", "owner-pid lease-ms process-group-id"),
|
|
126
|
+
"runtime process heartbeat": route(0, "id lease-token", "lease-ms"),
|
|
127
|
+
"runtime process admission": route(0, "id lease-token owner state-dir", "daemon-id operation-id operation native-session-id prompt-sha256"),
|
|
128
|
+
"runtime process release-turn": route(0, "id lease-token owner daemon-id", "operation-id observation-file"),
|
|
129
|
+
"runtime process finish": route(0, "id lease-token state", "reason", "", "", { state: ["stopped", "failed"] }),
|
|
130
|
+
"runtime process status": route(0),
|
|
131
|
+
"runtime process reconcile": route(0, "owner", "grace-ms"),
|
|
132
|
+
"lane status": route(0, project, "lane"),
|
|
133
|
+
"lane waiters": route(0, project, "lane"),
|
|
134
|
+
"lane waiter cancel": route(0, `${project} lane ${worker} reason`),
|
|
135
|
+
"lane workspace set": route(0, `${project} lane path`, "branch"),
|
|
136
|
+
"lane workspace check": route(0, `${project} lane path`),
|
|
137
|
+
"lane lock acquire": route(0, `${project} lane ${worker} reason`, "path ttl"),
|
|
138
|
+
"lane lock heartbeat": route(0, `${project} lane ${worker}`, "path lease-token ttl"),
|
|
139
|
+
"lane lock release": route(0, `${project} lane ${worker} reason`, "path lease-token"),
|
|
140
|
+
"lane lock status": route(0, `${project} lane`),
|
|
141
|
+
"lane lock wait": route(0, `${project} lane ${worker}`, "path timeout poll-interval"),
|
|
142
|
+
"lane lock wait-acquire": route(0, `${project} lane ${worker} reason`, "path timeout poll-interval ttl"),
|
|
143
|
+
"merge-queue status": route(0, "", scoped),
|
|
144
|
+
"merge-queue next": route(0, worker, `${scoped} path`),
|
|
145
|
+
"merge-queue claim": route(1, worker, `${scoped} path`),
|
|
146
|
+
"merge-queue wait-next": route(0, worker, `${scoped} path timeout poll-interval acquire-lock`),
|
|
147
|
+
"merge-queue complete": route(1, `${worker} summary`, `${scoped} path`),
|
|
148
|
+
"merge-queue fail": route(1, `${worker} reason requeue`, `${scoped} path`),
|
|
149
|
+
"merge-queue note": route(1, `${worker} summary`, scoped),
|
|
150
|
+
"merge-queue cancel": route(1, "reason", `${scoped} worker-id path`, "force"),
|
|
151
|
+
"merge serve": route(0, "agent-profile", "poll-seconds max-parallel-projects", "once"),
|
|
152
|
+
"merge server status": route(0),
|
|
153
|
+
"merge server stop": route(1),
|
|
154
|
+
"merge apply": route(1, `${project} work`),
|
|
155
|
+
"merge repair": route(1, project),
|
|
156
|
+
"merge request status": route(1, project),
|
|
157
|
+
"merge request route": route(1, `${project} mode reason`, "", "", "", { mode: ["same_session", "server"] }),
|
|
158
|
+
"merge status": route(0, project, "path"),
|
|
159
|
+
"merge bundle status": route(0, project, "path"),
|
|
160
|
+
"merge bundle claim": route(0, `${project} ${worker}`, "path"),
|
|
161
|
+
"merge bundle complete": route(0, `${project} ${worker} summary`, "path"),
|
|
162
|
+
"merge bundle fail": route(0, `${project} ${worker} reason requeue`, "path"),
|
|
163
|
+
"merge one-shot": route(0, `${project} ${worker}`, "path"),
|
|
164
|
+
"merge-worker status": route(0, project),
|
|
165
|
+
"merge-worker start": route(0, `${project} ${worker}`, "path branch"),
|
|
166
|
+
"merge-worker stop": route(0, `${project} reason`, worker),
|
|
167
|
+
"engine install": route(0, "", "", "force"),
|
|
168
|
+
"engine list": route(0),
|
|
169
|
+
"engine info": route(0, "", "package version"),
|
|
170
|
+
"engine resolve": route(0, "", "root project-root"),
|
|
171
|
+
"engine doctor": route(0, "", "root project-root"),
|
|
172
|
+
"engine bind-run": route(1, `${project} engine-version reason`, "package integrity-checksum"),
|
|
173
|
+
"canon register": route(0, "root"),
|
|
174
|
+
"canon status": route(0, "", "root"),
|
|
175
|
+
"canon resolve": route(0, "", "root"),
|
|
176
|
+
"migration impact": route(0, project, "target-version mode"),
|
|
177
|
+
"migration plan": route(0, project, "source-version target-version run backup-path backup-created-at"),
|
|
178
|
+
"migration report": route(0, project, "source-version target-version run backup-path backup-created-at"),
|
|
179
|
+
"migration verify": route(0, "file"),
|
|
180
|
+
"migration apply": route(0, project, "source-version target-version run backup-path backup-created-at file"),
|
|
181
|
+
"storage migrate-writer": route(0, "database backup", "", "offline"),
|
|
182
|
+
"cleanup scan": route(0, project),
|
|
183
|
+
"cleanup apply": route(0, `${project} plan-file reason`, "", "force"),
|
|
184
|
+
"worktree plan": route(0, "protocol-id", scoped),
|
|
185
|
+
"worktree create": route(0, "protocol-id branch base path", scoped),
|
|
186
|
+
"worktree status": route(0, "protocol-id", scoped),
|
|
187
|
+
"worktree bootstrap": route(0, "protocol-id", scoped),
|
|
188
|
+
"worktree close": route(0, "protocol-id mode", scoped, "", "", { mode: ["keep", "remove"] }),
|
|
189
|
+
"codex home plan": route(0, project, "profile source-home target-home"),
|
|
190
|
+
"codex home init": route(0, project, "profile source-home target-home"),
|
|
191
|
+
"codex home status": route(0, project, "profile"),
|
|
192
|
+
"codex home print-env": route(0, project, "profile"),
|
|
193
|
+
"codex home remove": route(0, `${project} mode`, "profile"),
|
|
194
|
+
"codex hooks print": route(0, project, "target profile"),
|
|
195
|
+
"codex hooks status": route(0, project, "target profile"),
|
|
196
|
+
"codex hooks install": route(0, project, "target profile", "yes"),
|
|
197
|
+
"codex hooks remove": route(0, project, "target profile", "yes"),
|
|
198
|
+
"codex hook handle": route(0, "event", scoped),
|
|
199
|
+
"zcode hook handle": route(0, project),
|
|
200
|
+
"zcode invocation prepare": route(0),
|
|
201
|
+
"zcode event handle": route(0, project),
|
|
202
|
+
"zcode usage ingest": route(0, project),
|
|
203
|
+
"grok event handle": route(0, project),
|
|
204
|
+
"grok usage ingest": route(0, project),
|
|
205
|
+
"opencode event handle": route(0, project),
|
|
206
|
+
"opencode usage ingest": route(0, project),
|
|
207
|
+
"droid event handle": route(0, project),
|
|
208
|
+
"droid usage ingest": route(0, project),
|
|
209
|
+
"agy event handle": route(0, project),
|
|
210
|
+
"agy usage ingest": route(0, project),
|
|
211
|
+
"stat usage": route(0, `${project} run`, "group-by session stage"),
|
|
212
|
+
"stat run sessions ls": route(0, `${project} run`),
|
|
213
|
+
"id next": route(0, `${project} type slug`, "", "", "", { type: ["protocol", "prt", "run", "work", "wrk"] }),
|
|
214
|
+
"integration cmux status": route(0, project),
|
|
215
|
+
"dashboard data": route(0, "", "project project-root protocol", "global"),
|
|
216
|
+
"dashboard render": route(0, "", "project project-root protocol output format"),
|
|
217
|
+
"dashboard render-global": route(0, "", "output format"),
|
|
218
|
+
"dashboard open": route(0, "", "project project-root protocol viewer format"),
|
|
219
|
+
"dashboard refresh": route(0, "", "project project-root protocol open format output", "all"),
|
|
220
|
+
"dashboard refresh-global": route(0, "", "output format"),
|
|
221
|
+
"schema validate": route(0, "schema file", "schema-dir project-root run"),
|
|
222
|
+
"memory permissions preflight": route(0, "root memory-bank flow mode", "tasks")
|
|
223
|
+
};
|
|
224
|
+
export function contextualPathOptionsForLifecycle(operation) {
|
|
225
|
+
const key = operation === "recovery_accept" ? "run recovery accept" : operation.replaceAll("_", " ");
|
|
226
|
+
return new Set(commandInputs[key]?.contextualPaths ?? []);
|
|
227
|
+
}
|
|
228
|
+
const flag = "__dd_flow_flag__";
|
|
229
|
+
const positiveInteger = new Set(words("limit lease-ms pid owner-pid process-group-id ready-timeout-ms max-parallel-projects expected-revision"));
|
|
230
|
+
const nonnegativeInteger = new Set(words("after generation wait-ms grace-ms available-slots"));
|
|
231
|
+
const positiveNumber = new Set(words("ttl poll-interval poll-seconds"));
|
|
232
|
+
const booleans = new Set(words("write close-sessions cancel-queue release-locks user-required requeue acquire-lock"));
|
|
233
|
+
export function prepareCommandRoute(args) {
|
|
234
|
+
const parsed = parseCommandArgs(args);
|
|
235
|
+
const key = Object.keys(commandInputs).filter(key => key.split(" ").every((part, index) => parsed.positional[index] === part))
|
|
236
|
+
.sort((a, b) => b.split(" ").length - a.split(" ").length)[0];
|
|
237
|
+
const reject = (message, parameter) => { throw new AppError("usage", message, 2, { phase: "prepare", effect: "no_effect", recoverable: true, ...(parameter ? { parameter } : {}) }); };
|
|
238
|
+
if (!key)
|
|
239
|
+
return reject("Unknown command; use dd-flow --help");
|
|
240
|
+
const contract = commandInputs[key];
|
|
241
|
+
if (contract.retired)
|
|
242
|
+
reject(contract.retired);
|
|
243
|
+
const prefixLength = key.split(" ").length;
|
|
244
|
+
const positionCount = parsed.positional.length - prefixLength;
|
|
245
|
+
if (positionCount < contract.positions[0] || positionCount > contract.positions[1])
|
|
246
|
+
reject(`${key} expects ${contract.positions[0] === contract.positions[1] ? contract.positions[0] : `${contract.positions[0]}..${contract.positions[1]}`} positional argument(s)`);
|
|
247
|
+
for (const [name, values] of parsed.options) {
|
|
248
|
+
if (key.startsWith("merge-queue ") && name === "session-id")
|
|
249
|
+
reject("--session-id is no longer accepted on merge-queue commands; use --worker-id", name);
|
|
250
|
+
const upgrade = ["compatibility-mode", "mb-upgrade-mode"].includes(name);
|
|
251
|
+
if (upgrade && !isUpgradeAllowlisted(args))
|
|
252
|
+
reject(`${key} does not support explicit upgrade mode`, name);
|
|
253
|
+
if (!upgrade && !contract.values.includes(name) && !contract.switches.includes(name))
|
|
254
|
+
reject(`${key} does not accept --${name}`, name);
|
|
255
|
+
if (values.length > 1 && !contract.repeatable.includes(name))
|
|
256
|
+
reject(`--${name} must occur once`, name);
|
|
257
|
+
for (const value of values) {
|
|
258
|
+
if (contract.switches.includes(name) || name === "mb-upgrade-mode") {
|
|
259
|
+
if (value !== flag)
|
|
260
|
+
reject(`--${name} is a switch and does not take a value`, name);
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
if (value === flag || !value.trim())
|
|
264
|
+
reject(`--${name} requires a non-empty value`, name);
|
|
265
|
+
if (name === "compatibility-mode" && value !== "mb-upgrade")
|
|
266
|
+
reject("--compatibility-mode must be mb-upgrade", name);
|
|
267
|
+
const choices = contract.enums[name];
|
|
268
|
+
if (choices && !choices.includes(key === "id next" && name === "type" ? value.toLowerCase() : value))
|
|
269
|
+
reject(`--${name} must be ${choices.join(" or ")}`, name);
|
|
270
|
+
if (booleans.has(name) && !["true", "false"].includes(value))
|
|
271
|
+
reject(`--${name} must be true or false`, name);
|
|
272
|
+
const number = Number(value);
|
|
273
|
+
if (positiveInteger.has(name) && (!/^[0-9]+$/.test(value) || !Number.isSafeInteger(number) || number <= 0)
|
|
274
|
+
|| nonnegativeInteger.has(name) && (!/^[0-9]+$/.test(value) || !Number.isSafeInteger(number) || number < 0)
|
|
275
|
+
|| positiveNumber.has(name) && (!Number.isFinite(number) || number <= 0)
|
|
276
|
+
|| name === "timeout" && (!Number.isFinite(number) || number < 0)
|
|
277
|
+
|| name === "available-slots" && number > 15)
|
|
278
|
+
reject(`Invalid numeric value for --${name}`, name);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
for (const name of contract.required)
|
|
282
|
+
if (!parsed.options.has(name))
|
|
283
|
+
reject(`Missing required option: --${name}`, name);
|
|
284
|
+
for (const aliases of [
|
|
285
|
+
["root", "project-root"], ["payload-file", "json-file"],
|
|
286
|
+
["payload-file", "payload-json", "payload-base64"], ["semantic-file", "data", "result-file", "result-stdin"],
|
|
287
|
+
["stage-report", "dashboard"], ["task-file", "plan-item"], ["project", "project-root"],
|
|
288
|
+
["intake-file", "intake-stdin", "context-file"]
|
|
289
|
+
]) {
|
|
290
|
+
const present = aliases.filter(name => parsed.options.has(name));
|
|
291
|
+
if (present.length > 1)
|
|
292
|
+
reject(`Use only one of ${present.map(name => `--${name}`).join(", ")}`);
|
|
293
|
+
}
|
|
294
|
+
if (contract.values.includes("retry-check") && parsed.options.has("retry-check") !== parsed.options.has("reason"))
|
|
295
|
+
reject("--retry-check and --reason must be supplied together");
|
|
296
|
+
// Retain the dispatcher's historic offsets for nested action positions.
|
|
297
|
+
const family = parsed.positional[0];
|
|
298
|
+
const singleWord = ["status", "version", "transition"].includes(family);
|
|
299
|
+
return { key, family, command: singleWord ? undefined : parsed.positional[1], parsed: { options: parsed.options, positional: parsed.positional.slice(singleWord ? 1 : 2) } };
|
|
300
|
+
}
|
package/dist/cli/help.js
CHANGED
|
@@ -242,7 +242,7 @@ Usage:
|
|
|
242
242
|
dd-flow stage finish <RUN-ID|RUN-short-id> --project-root <root> --stage <name> [--semantic-file <RUN-local-json>] --json
|
|
243
243
|
dd-flow stage finish <RUN-ID|RUN-short-id> --project-root <root> --stage specify (--result-file <RUN-local-specify.json>|--result-stdin) --outcome <specified|failed|cancelled> --json
|
|
244
244
|
|
|
245
|
-
stage start is the worker's first practical flow command. It performs deterministic setup and returns a ready-to-use, stage-specific work packet with trusted runtime facts, compatibility and permission preflight, aliases, write boundary and exact lifecycle commands. A material user question uses stage pause, not stage finish: pause leaves the same stage, Work, Session and attempt open and returns the exact resume command. Engine, harness and environment failures use stage block and never create HITL. On the next Turn, pass a complete user answer to stage resume, or repair an external blocker and call its returned stage unblock command. stage fanout is read-only orchestration state plus the stage's deterministic Work materialization action for an external runner; it never makes product decisions. stage finish is reserved for actual stage completion and generates the final deterministic reports.
|
|
245
|
+
stage start is the worker's first practical flow command. It performs deterministic setup and returns a ready-to-use, stage-specific work packet with trusted runtime facts, compatibility and permission preflight, aliases, write boundary and exact lifecycle commands. Managed commands use scoped RUN/Work aliases and @project; their internal invocation UUID is bound by the native hook and is intentionally not model-facing. @ aliases apply only to declared dd-flow path arguments, not to shell tools. A material user question uses stage pause, not stage finish: pause leaves the same stage, Work, Session and attempt open and returns the exact resume command. Engine, harness and environment failures use stage block and never create HITL. On the next Turn, pass a complete user answer to stage resume, or repair an external blocker and call its returned stage unblock command. stage fanout is read-only orchestration state plus the stage's deterministic Work materialization action for an external runner; it never makes product decisions. stage finish is reserved for actual stage completion and generates the final deterministic reports. Unmanaged operator calls retain explicit project and runtime selection.`
|
|
246
246
|
],
|
|
247
247
|
[
|
|
248
248
|
"work",
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { fileURLToPath } from "node:url";
|
|
2
|
+
import { performance } from "node:perf_hooks";
|
|
3
|
+
import { parseCommandArgs, parseLifecycleCommand, lifecycleCommandFromPayload } from "../services/lifecycle-command.js";
|
|
4
|
+
import { errorRecord, AppError } from "../shared/errors.js";
|
|
5
|
+
import { parseJsonObject } from "../shared/json.js";
|
|
6
|
+
const workerFile = fileURLToPath(import.meta.url);
|
|
7
|
+
const ingressBudgetMs = 20_000; // Native host timeout is 30s; this includes cold module/DB open.
|
|
8
|
+
/** Lightweight entry: unrelated hooks never load the router or open storage. */
|
|
9
|
+
export async function runHookIngress(args) {
|
|
10
|
+
try {
|
|
11
|
+
const parsed = parseCommandArgs(args.slice(3));
|
|
12
|
+
for (const [key, values] of parsed.options)
|
|
13
|
+
if (!["event", "project-root", "json"].includes(key) || values.length !== 1)
|
|
14
|
+
throw new AppError("usage", `Invalid hook option: ${key}`, 2);
|
|
15
|
+
const event = parsed.options.get("event")?.[0];
|
|
16
|
+
if (!event?.trim() || event === "__dd_flow_flag__" || parsed.positional.length)
|
|
17
|
+
throw new AppError("usage", "Hook ingress requires --event with a value", 2);
|
|
18
|
+
const root = parsed.options.get("project-root")?.[0];
|
|
19
|
+
if (root !== undefined && (!root.trim() || root === "__dd_flow_flag__"))
|
|
20
|
+
throw new AppError("usage", "--project-root requires a value", 2);
|
|
21
|
+
if (parsed.options.has("json") && parsed.options.get("json")?.[0] !== "__dd_flow_flag__")
|
|
22
|
+
throw new AppError("usage", "--json does not take a value", 2);
|
|
23
|
+
let stdin = "";
|
|
24
|
+
for await (const chunk of process.stdin) {
|
|
25
|
+
stdin += String(chunk);
|
|
26
|
+
if (Buffer.byteLength(stdin) > 1_048_576)
|
|
27
|
+
throw new AppError("native_hook_response_invalid", "Hook input exceeds 1 MiB", 1);
|
|
28
|
+
}
|
|
29
|
+
const payload = parseJsonObject(stdin || "{}", "native hook stdin");
|
|
30
|
+
const command = lifecycleCommandFromPayload(payload);
|
|
31
|
+
if (event !== "PreToolUse" || !command || parseLifecycleCommand(command).kind === "none") {
|
|
32
|
+
process.stdout.write("{}\n");
|
|
33
|
+
return 0;
|
|
34
|
+
}
|
|
35
|
+
const started = performance.now();
|
|
36
|
+
const { invokeNativeHook } = await import("../harness-runtime/lib/native-hook-command.mjs");
|
|
37
|
+
const result = await invokeNativeHook({ bin: workerFile, args, payload, home: process.env.DD_FLOW_HOME, timeoutMs: ingressBudgetMs });
|
|
38
|
+
const { timing, ...output } = result;
|
|
39
|
+
process.stderr.write(`${JSON.stringify({ kind: "hook_ingress", total_ms: Math.round(performance.now() - started), ...timing })}\n`);
|
|
40
|
+
process.stdout.write(`${JSON.stringify(output)}\n`);
|
|
41
|
+
return 0;
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
const record = errorRecord(error);
|
|
45
|
+
process.stderr.write(`${JSON.stringify({ ok: false, error: record })}\n`);
|
|
46
|
+
return record.exitCode;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
// A bounded CLI subprocess, not a detached writer: its successful reply follows COMMIT.
|
|
50
|
+
if (process.argv[1] === workerFile) {
|
|
51
|
+
try {
|
|
52
|
+
let stdin = "";
|
|
53
|
+
for await (const chunk of process.stdin)
|
|
54
|
+
stdin += String(chunk);
|
|
55
|
+
const payload = parseJsonObject(stdin, "native hook stdin");
|
|
56
|
+
const started = performance.now();
|
|
57
|
+
const { verifyCodexHookDelivery } = await import("../services/codex-hook-delivery.js");
|
|
58
|
+
const proof = await verifyCodexHookDelivery(process.env, payload);
|
|
59
|
+
const verified = performance.now();
|
|
60
|
+
const [{ createContext }, { handleCodexHook }] = await Promise.all([import("../runtime/context.js"), import("../services/hooks.js")]);
|
|
61
|
+
const loaded = performance.now();
|
|
62
|
+
const context = createContext(process.env, "hook");
|
|
63
|
+
const opened = performance.now();
|
|
64
|
+
try {
|
|
65
|
+
if (proof)
|
|
66
|
+
context.codexHookProof = proof;
|
|
67
|
+
const parsed = parseCommandArgs(process.argv.slice(5));
|
|
68
|
+
const projectRoot = parsed.options.get("project-root")?.[0];
|
|
69
|
+
const result = handleCodexHook(context, { event: "PreToolUse", ...(projectRoot ? { projectRoot } : {}), stdin });
|
|
70
|
+
process.stdout.write(`${JSON.stringify({ ...(result.hookSpecificOutput ? { hookSpecificOutput: result.hookSpecificOutput } : {}), timing: {
|
|
71
|
+
verify_ms: Math.round(verified - started), load_ms: Math.round(loaded - verified), open_ms: Math.round(opened - loaded), receipt_ms: Math.round(performance.now() - opened)
|
|
72
|
+
} })}\n`);
|
|
73
|
+
}
|
|
74
|
+
finally {
|
|
75
|
+
context.db.close?.();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
process.stderr.write(`${JSON.stringify({ ok: false, error: errorRecord(error) })}\n`);
|
|
80
|
+
process.exitCode = 1;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { AppError, isAppError } from "../shared/errors.js";
|
|
4
|
+
import { validateSchema } from "../services/schema-validation.js";
|
|
5
|
+
export function prepareError(error, input = {}) {
|
|
6
|
+
if (isAppError(error)) {
|
|
7
|
+
// A malformed user input is safely correctable. A broken schema registry,
|
|
8
|
+
// binding, or storage is not; do not disguise infrastructure damage as a
|
|
9
|
+
// retryable command typo.
|
|
10
|
+
const inputErrorCodes = new Set([
|
|
11
|
+
"usage", "validation", "schema_validation", "invalid_json", "not_found",
|
|
12
|
+
"path_escape", "input_file_missing", "input_file_not_regular", "input_file_unreadable",
|
|
13
|
+
"response_file_parent_invalid", "response_file_invalid", "context_checksum_mismatch", "invalid_run_config_state", "invalid_work_state", "invalid_stage_state", "runtime_budget_invalid", "runtime_scope_manifest_invalid", "runtime_scope_resume_invalid",
|
|
14
|
+
"fork_path_conflict", "fork_output_not_empty", "fork_checkpoint_invalid", "fork_engine_unavailable", "snapshot_missing",
|
|
15
|
+
"repair_check_declaration_missing", "review_repair_check_required", "review_check_reference_unknown", "semantic_status_inconsistent", "migration_report_invalid", "controller_context_missing", "snapshot_output_inside_runtime"
|
|
16
|
+
]);
|
|
17
|
+
if (!inputErrorCodes.has(error.code) && error.details.phase !== "prepare")
|
|
18
|
+
throw error;
|
|
19
|
+
throw new AppError(error.code, error.message, 2, {
|
|
20
|
+
...error.details,
|
|
21
|
+
phase: "prepare",
|
|
22
|
+
effect: "no_effect",
|
|
23
|
+
recoverable: true,
|
|
24
|
+
...input
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
const code = error?.code;
|
|
28
|
+
if (typeof input.file !== "string" || !["ENOENT", "EACCES", "EPERM", "EISDIR", "ENOTDIR", "ELOOP", "ENAMETOOLONG"].includes(code ?? ""))
|
|
29
|
+
throw error;
|
|
30
|
+
throw new AppError("input_file_unreadable", `Cannot read input file${typeof input.file === "string" ? `: ${input.file}` : ""}`, 2, {
|
|
31
|
+
phase: "prepare", effect: "no_effect", recoverable: true, ...input, cause: code ?? String(error)
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
export function prepareTextFile(file, label = "input file") {
|
|
35
|
+
const prepared = prepareFileBytes(file, label);
|
|
36
|
+
return { file: prepared.file, text: prepared.bytes.toString("utf8") };
|
|
37
|
+
}
|
|
38
|
+
export function prepareFileBytes(file, label = "input file") {
|
|
39
|
+
const absolute = path.resolve(file);
|
|
40
|
+
let descriptor;
|
|
41
|
+
try {
|
|
42
|
+
descriptor = fs.openSync(absolute, fs.constants.O_RDONLY | fs.constants.O_NONBLOCK);
|
|
43
|
+
const stat = fs.fstatSync(descriptor);
|
|
44
|
+
if (!stat.isFile()) {
|
|
45
|
+
throw new AppError("input_file_not_regular", `${label} must be a readable regular file: ${absolute}`, 2, { file: absolute, label });
|
|
46
|
+
}
|
|
47
|
+
return { file: absolute, bytes: fs.readFileSync(descriptor) };
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
if (error?.code === "ENOENT") {
|
|
51
|
+
throw new AppError("input_file_missing", `${label} does not exist: ${absolute}`, 2, {
|
|
52
|
+
phase: "prepare", effect: "no_effect", recoverable: true, file: absolute, label
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
return prepareError(error, { file: absolute, label });
|
|
56
|
+
}
|
|
57
|
+
finally {
|
|
58
|
+
if (descriptor !== undefined)
|
|
59
|
+
fs.closeSync(descriptor);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
export function prepareJsonText(text, input) {
|
|
63
|
+
try {
|
|
64
|
+
return { file: input.file, text, value: JSON.parse(text) };
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
// Keep the established public code so agents can recognise this as a
|
|
68
|
+
// safe correction-and-retry condition.
|
|
69
|
+
throw new AppError("invalid_json", `Invalid JSON in ${input.label ?? "input"}: ${input.file}`, 2, {
|
|
70
|
+
phase: "prepare", effect: "no_effect", recoverable: true, file: input.file, label: input.label ?? "input",
|
|
71
|
+
cause: "Invalid JSON syntax"
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
export function prepareJsonFile(file, label = "input JSON") {
|
|
76
|
+
const prepared = prepareTextFile(file, label);
|
|
77
|
+
return prepareJsonText(prepared.text, { file: prepared.file, label });
|
|
78
|
+
}
|
|
79
|
+
export function prepareJsonSchema(file, label, schema) {
|
|
80
|
+
const prepared = prepareJsonFile(file, label);
|
|
81
|
+
try {
|
|
82
|
+
validateSchema({ ...schema, file: prepared.file, data: prepared.value });
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
return prepareError(error, { file: prepared.file, label });
|
|
86
|
+
}
|
|
87
|
+
return prepared;
|
|
88
|
+
}
|
|
89
|
+
/** Read-only destination check; execution still handles filesystem races. */
|
|
90
|
+
export function prepareOutputFile(value) { return prepareOutput(value, false); }
|
|
91
|
+
export function prepareOutputDirectory(value) { return prepareOutput(value, true); }
|
|
92
|
+
function prepareOutput(value, directory) {
|
|
93
|
+
const file = path.resolve(value);
|
|
94
|
+
try {
|
|
95
|
+
let ancestor = directory ? file : path.dirname(file);
|
|
96
|
+
while (!fs.existsSync(ancestor) && path.dirname(ancestor) !== ancestor)
|
|
97
|
+
ancestor = path.dirname(ancestor);
|
|
98
|
+
if (!fs.statSync(ancestor).isDirectory())
|
|
99
|
+
throw new AppError("validation", "Output parent must be a directory", 2);
|
|
100
|
+
fs.accessSync(ancestor, fs.constants.W_OK | fs.constants.X_OK);
|
|
101
|
+
if (fs.existsSync(file)) {
|
|
102
|
+
const stat = fs.statSync(file);
|
|
103
|
+
if (directory ? !stat.isDirectory() : !stat.isFile())
|
|
104
|
+
throw new AppError("validation", directory ? "Output must name a directory" : "Output must name a regular file", 2);
|
|
105
|
+
fs.accessSync(file, fs.constants.W_OK | (directory ? fs.constants.X_OK : 0));
|
|
106
|
+
}
|
|
107
|
+
return file;
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
const code = error?.code;
|
|
111
|
+
if (["ENOENT", "EACCES", "EPERM", "ENOTDIR", "ELOOP", "ENAMETOOLONG"].includes(code ?? ""))
|
|
112
|
+
return prepareError(new AppError("validation", "Output destination is not writable or has an invalid parent", 2, { cause: code }), { file, parameter: "output" });
|
|
113
|
+
return prepareError(error, { file, parameter: "output" });
|
|
114
|
+
}
|
|
115
|
+
}
|