@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
|
@@ -34,7 +34,22 @@ export function managedProcessRuntimeHome(record, authority) {
|
|
|
34
34
|
return authority.ddFlowHome;
|
|
35
35
|
return null;
|
|
36
36
|
}
|
|
37
|
-
export function
|
|
37
|
+
export function prepareManagedProcessRegistration(context, input) {
|
|
38
|
+
requireProcessText(input.kind, "kind");
|
|
39
|
+
requireProcessText(input.ownerId, "owner");
|
|
40
|
+
for (const field of ["id", "projectId", "runId", "workId", "checkId", "operationId", "stdoutPath", "stderrPath"]) {
|
|
41
|
+
if (input[field] !== undefined && input[field] !== null)
|
|
42
|
+
requireProcessText(input[field], field);
|
|
43
|
+
}
|
|
44
|
+
for (const field of ["pid", "ownerPid", "processGroupId"]) {
|
|
45
|
+
if (input[field] !== undefined)
|
|
46
|
+
requireProcessPid(input[field], field);
|
|
47
|
+
}
|
|
48
|
+
if (input.uniqueActiveOperation !== undefined && typeof input.uniqueActiveOperation !== "boolean")
|
|
49
|
+
throw new AppError("validation", "uniqueActiveOperation must be boolean", 2);
|
|
50
|
+
if (input.metadata !== undefined && (!input.metadata || typeof input.metadata !== "object" || Array.isArray(input.metadata)))
|
|
51
|
+
throw new AppError("validation", "metadata must be a JSON object", 2);
|
|
52
|
+
prepareManagedProcessLease(context, input.leaseMs);
|
|
38
53
|
const observer = input.kind === "eval-observer";
|
|
39
54
|
const observerBirth = observer && input.pid ? processStartedAt(input.pid) : null;
|
|
40
55
|
if (observer) {
|
|
@@ -42,17 +57,41 @@ export function registerManagedProcess(context, input) {
|
|
|
42
57
|
throw new AppError("process_ownership_unknown", "EVAL observer registration requires its live self-owned process identity", 1);
|
|
43
58
|
input = { ...input, ownerPid: input.pid, uniqueActiveOperation: true };
|
|
44
59
|
}
|
|
60
|
+
if (input.uniqueActiveOperation && !input.operationId)
|
|
61
|
+
throw new AppError("validation", "Unique managed operation requires operationId", 2);
|
|
62
|
+
let metadataJson;
|
|
63
|
+
try {
|
|
64
|
+
metadataJson = JSON.stringify({ ...input.metadata, dd_flow_home: context.ddFlowHome, ...(input.ownerPid ? { owner_pid: input.ownerPid, owner_pid_started_at: processStartedAt(input.ownerPid) } : {}) });
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
throw new AppError("validation", "metadata must be JSON-serializable", 2);
|
|
68
|
+
}
|
|
69
|
+
const reader = getResourceDatabase(resourceHome(context), "read_existing");
|
|
70
|
+
try {
|
|
71
|
+
if (input.id && reader.get("SELECT 1 FROM managed_processes WHERE id = ?", [input.id]))
|
|
72
|
+
throw new AppError("validation", "Managed process ID already exists; use its retained registration", 2, { process_id: input.id });
|
|
73
|
+
if (input.uniqueActiveOperation) {
|
|
74
|
+
const existing = reader.get("SELECT * FROM managed_processes WHERE operation_id = ? AND state IN ('starting','running','stopping','orphaned')", [input.operationId]);
|
|
75
|
+
if (existing)
|
|
76
|
+
throw new AppError("managed_operation_in_progress", "The operation already owns a managed process; observe it instead of spawning a duplicate", 1, { process_id: existing.id, operation_id: input.operationId });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
finally {
|
|
80
|
+
reader.close?.();
|
|
81
|
+
}
|
|
82
|
+
return { input, observer, observerBirth, metadataJson };
|
|
83
|
+
}
|
|
84
|
+
export function registerManagedProcess(context, rawInput) {
|
|
85
|
+
const { input, observer, observerBirth, metadataJson } = prepareManagedProcessRegistration(context, rawInput);
|
|
45
86
|
if (!input.uniqueActiveOperation)
|
|
46
|
-
return insertManagedProcess(context, input);
|
|
47
|
-
if (!input.operationId)
|
|
48
|
-
throw new Error("Unique managed operation requires operationId");
|
|
87
|
+
return insertManagedProcess(context, input, metadataJson);
|
|
49
88
|
const db = registry(context);
|
|
50
89
|
db.beginWriteTransaction();
|
|
51
90
|
try {
|
|
52
91
|
const existing = db.get("SELECT * FROM managed_processes WHERE operation_id = ? AND state IN ('starting','running','stopping','orphaned')", [input.operationId]);
|
|
53
92
|
if (existing)
|
|
54
93
|
throw new AppError("managed_operation_in_progress", "The operation already owns a managed process; observe it instead of spawning a duplicate", 1, { process_id: existing.id, operation_id: input.operationId });
|
|
55
|
-
let result = insertManagedProcess(context, input);
|
|
94
|
+
let result = insertManagedProcess(context, input, metadataJson);
|
|
56
95
|
if (observer) {
|
|
57
96
|
result = confirmManagedProcess(context, { id: result.id, leaseToken: result.lease_token, pid: input.pid, ...(input.processGroupId ? { processGroupId: input.processGroupId } : {}) });
|
|
58
97
|
if (result.pid_started_at !== observerBirth || !processIsAlive(result))
|
|
@@ -66,20 +105,36 @@ export function registerManagedProcess(context, input) {
|
|
|
66
105
|
throw error;
|
|
67
106
|
}
|
|
68
107
|
}
|
|
69
|
-
function insertManagedProcess(context, input) {
|
|
108
|
+
function insertManagedProcess(context, input, metadataJson) {
|
|
70
109
|
const db = registry(context);
|
|
71
110
|
const now = context.now();
|
|
72
111
|
const id = input.id ?? `PROC-${crypto.randomUUID()}`;
|
|
73
112
|
const token = crypto.randomUUID();
|
|
74
113
|
db.run(`INSERT INTO managed_processes
|
|
75
114
|
(id, kind, pid, pid_started_at, owner_id, lease_token, lease_expires_at, project_id, run_id, work_id, check_id, operation_id, stdout_path, stderr_path, state, started_at, updated_at, finished_at, termination_reason, metadata_json)
|
|
76
|
-
VALUES (?, ?, NULL, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'starting', ?, ?, NULL, NULL, ?)`, [id, input.kind, input.ownerId, token, leaseExpiry(now, input.leaseMs), input.projectId ?? null, input.runId ?? null, input.workId ?? null, input.checkId ?? null, input.operationId ?? null, input.stdoutPath ?? null, input.stderrPath ?? null, now, now,
|
|
115
|
+
VALUES (?, ?, NULL, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'starting', ?, ?, NULL, NULL, ?)`, [id, input.kind, input.ownerId, token, leaseExpiry(now, input.leaseMs), input.projectId ?? null, input.runId ?? null, input.workId ?? null, input.checkId ?? null, input.operationId ?? null, input.stdoutPath ?? null, input.stderrPath ?? null, now, now, metadataJson]);
|
|
77
116
|
return requireProcess(db, id);
|
|
78
117
|
}
|
|
118
|
+
export function prepareManagedProcessConfirmation(context, input) {
|
|
119
|
+
requireProcessText(input.id, "id");
|
|
120
|
+
requireProcessText(input.leaseToken, "lease-token");
|
|
121
|
+
requireProcessPid(input.pid, "pid");
|
|
122
|
+
if (input.ownerPid !== undefined)
|
|
123
|
+
requireProcessPid(input.ownerPid, "owner-pid");
|
|
124
|
+
if (input.processGroupId !== undefined && input.processGroupId !== null)
|
|
125
|
+
requireProcessPid(input.processGroupId, "process-group-id");
|
|
126
|
+
prepareManagedProcessLease(context, input.leaseMs);
|
|
127
|
+
const current = readManagedProcess(context, input.id);
|
|
128
|
+
if (current.lease_token !== input.leaseToken)
|
|
129
|
+
throw new AppError("process_lease_lost", "Managed process lease does not match", 1);
|
|
130
|
+
if (current.state !== "starting")
|
|
131
|
+
throw new AppError("process_confirmation_rejected", "Only a starting managed process can be confirmed", 1, { process_id: current.id, state: current.state });
|
|
132
|
+
return current;
|
|
133
|
+
}
|
|
79
134
|
export function confirmManagedProcess(context, input) {
|
|
135
|
+
const current = prepareManagedProcessConfirmation(context, input);
|
|
80
136
|
const db = registry(context);
|
|
81
137
|
const now = context.now();
|
|
82
|
-
const current = requireProcess(db, input.id);
|
|
83
138
|
const metadata = parseMetadata(current.metadata_json);
|
|
84
139
|
if (input.processGroupId)
|
|
85
140
|
metadata.process_group_id = input.processGroupId;
|
|
@@ -94,15 +149,40 @@ export function confirmManagedProcess(context, input) {
|
|
|
94
149
|
throw new Error(`Managed process cannot be confirmed: ${input.id}`);
|
|
95
150
|
return requireProcess(db, input.id);
|
|
96
151
|
}
|
|
152
|
+
export function prepareManagedProcessHeartbeat(context, input) {
|
|
153
|
+
requireProcessText(input.id, "id");
|
|
154
|
+
requireProcessText(input.leaseToken, "lease-token");
|
|
155
|
+
prepareManagedProcessLease(context, input.leaseMs);
|
|
156
|
+
}
|
|
97
157
|
export function heartbeatManagedProcess(context, input) {
|
|
158
|
+
prepareManagedProcessHeartbeat(context, input);
|
|
159
|
+
const reader = getResourceDatabase(resourceHome(context), "read_existing");
|
|
160
|
+
try {
|
|
161
|
+
if (!reader.get("SELECT 1 FROM managed_processes WHERE id = ? AND lease_token = ? AND state IN ('starting','running','stopping')", [input.id, input.leaseToken]))
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
finally {
|
|
165
|
+
reader.close?.();
|
|
166
|
+
}
|
|
98
167
|
const db = registry(context);
|
|
99
168
|
return db.run("UPDATE managed_processes SET lease_expires_at = ?, updated_at = ? WHERE id = ? AND lease_token = ? AND state IN ('starting','running','stopping')", [leaseExpiry(context.now(), input.leaseMs), context.now(), input.id, input.leaseToken]).changes === 1;
|
|
100
169
|
}
|
|
170
|
+
export function prepareManagedProcessFinish(context, input) {
|
|
171
|
+
requireProcessText(input.id, "id");
|
|
172
|
+
requireProcessText(input.leaseToken, "lease-token");
|
|
173
|
+
if (input.state !== "stopped" && input.state !== "failed")
|
|
174
|
+
throw new AppError("validation", "--state must be stopped or failed", 2);
|
|
175
|
+
if (input.reason !== undefined && input.reason !== null && typeof input.reason !== "string")
|
|
176
|
+
throw new AppError("validation", "reason must be a string or null", 2);
|
|
177
|
+
return readManagedProcess(context, input.id);
|
|
178
|
+
}
|
|
101
179
|
export function finishManagedProcess(context, input) {
|
|
102
|
-
const
|
|
103
|
-
const existing = requireProcess(db, input.id);
|
|
180
|
+
const existing = prepareManagedProcessFinish(context, input);
|
|
104
181
|
if (existing.lease_token === input.leaseToken && existing.state === input.state)
|
|
105
182
|
return true;
|
|
183
|
+
if (existing.lease_token !== input.leaseToken || !["starting", "running", "stopping", "orphaned"].includes(existing.state))
|
|
184
|
+
return false;
|
|
185
|
+
const db = registry(context);
|
|
106
186
|
const now = context.now();
|
|
107
187
|
const updated = db.run("UPDATE managed_processes SET state = ?, termination_reason = ?, finished_at = ?, updated_at = ?, lease_expires_at = ? WHERE id = ? AND lease_token = ? AND state IN ('starting','running','stopping','orphaned')", [input.state, input.reason ?? null, now, now, now, input.id, input.leaseToken]).changes === 1;
|
|
108
188
|
if (updated)
|
|
@@ -183,20 +263,47 @@ export function managedLeaseReleased(record) {
|
|
|
183
263
|
&& ["stopped", "failed"].includes(record.state) && Boolean(record.finished_at);
|
|
184
264
|
}
|
|
185
265
|
export function managedProcessStatus(context) {
|
|
186
|
-
|
|
266
|
+
const db = getResourceDatabase(resourceHome(context), "read_existing");
|
|
267
|
+
try {
|
|
268
|
+
return db.all("SELECT * FROM managed_processes ORDER BY updated_at DESC, id");
|
|
269
|
+
}
|
|
270
|
+
finally {
|
|
271
|
+
db.close?.();
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
export function prepareManagedProcessStopOperation(context, input) {
|
|
275
|
+
const graceMs = prepareManagedProcessStop(input);
|
|
276
|
+
const record = readManagedProcess(context, input.id);
|
|
277
|
+
assertStopOwnership(record, input.leaseToken);
|
|
278
|
+
return { record, graceMs };
|
|
279
|
+
}
|
|
280
|
+
function readManagedProcess(context, id) {
|
|
281
|
+
const db = getResourceDatabase(resourceHome(context), "read_existing");
|
|
282
|
+
try {
|
|
283
|
+
return requireProcess(db, id);
|
|
284
|
+
}
|
|
285
|
+
finally {
|
|
286
|
+
db.close?.();
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
function assertStopOwnership(record, leaseToken) {
|
|
290
|
+
if (record.lease_token !== leaseToken)
|
|
291
|
+
throw new AppError("process_lease_lost", "Managed process lease does not match", 1);
|
|
292
|
+
if (["stopped", "failed"].includes(record.state) && !processTreeIsAlive(record))
|
|
293
|
+
return;
|
|
294
|
+
if (!record.pid || !record.pid_started_at)
|
|
295
|
+
throw new AppError("process_identity_unknown", "Managed process has no confirmed physical identity; retain its pending launch", 1);
|
|
187
296
|
}
|
|
188
297
|
export async function stopManagedProcess(context, input) {
|
|
298
|
+
const { graceMs } = prepareManagedProcessStopOperation(context, input);
|
|
189
299
|
const db = registry(context), record = requireProcess(db, input.id);
|
|
190
|
-
|
|
191
|
-
throw new Error("Managed process lease does not match");
|
|
300
|
+
assertStopOwnership(record, input.leaseToken);
|
|
192
301
|
if (["stopped", "failed"].includes(record.state) && !processTreeIsAlive(record))
|
|
193
302
|
return record;
|
|
194
|
-
if (!record.pid || !record.pid_started_at)
|
|
195
|
-
throw Object.assign(new Error("Managed process has no confirmed physical identity; retain its pending launch"), { code: "process_identity_unknown" });
|
|
196
303
|
db.run("UPDATE managed_processes SET state = 'stopping', updated_at = ? WHERE id = ? AND lease_token = ? AND state IN ('starting','running','orphaned')", [context.now(), record.id, record.lease_token]);
|
|
197
304
|
if (processTreeIsAlive(record)) {
|
|
198
305
|
terminateOwnedProcess(record, "SIGTERM");
|
|
199
|
-
await delay(
|
|
306
|
+
await delay(graceMs);
|
|
200
307
|
if (input.force !== false && processTreeIsAlive(record)) {
|
|
201
308
|
terminateOwnedProcess(record, "SIGKILL");
|
|
202
309
|
await delay(250);
|
|
@@ -210,6 +317,7 @@ export async function stopManagedProcess(context, input) {
|
|
|
210
317
|
}
|
|
211
318
|
/** Reconcile only records the system owns and has atomically claimed. */
|
|
212
319
|
export async function reconcileExpiredManagedProcesses(context, ownerId, graceMs = 1_000) {
|
|
320
|
+
prepareManagedProcessReconcile(ownerId, graceMs);
|
|
213
321
|
const claimed = claimExpiredManagedProcesses(context, ownerId);
|
|
214
322
|
const outcomes = [];
|
|
215
323
|
for (const processRecord of claimed) {
|
|
@@ -230,7 +338,34 @@ export async function reconcileExpiredManagedProcesses(context, ownerId, graceMs
|
|
|
230
338
|
}
|
|
231
339
|
return outcomes;
|
|
232
340
|
}
|
|
341
|
+
export function prepareManagedProcessStop(input) {
|
|
342
|
+
requireProcessText(input.id, "id");
|
|
343
|
+
requireProcessText(input.leaseToken, "lease-token");
|
|
344
|
+
if (input.reason !== undefined)
|
|
345
|
+
requireProcessText(input.reason, "reason");
|
|
346
|
+
if (input.force !== undefined && typeof input.force !== "boolean")
|
|
347
|
+
throw new AppError("validation", "force must be boolean", 2);
|
|
348
|
+
return prepareProcessGrace(input.graceMs);
|
|
349
|
+
}
|
|
350
|
+
export function prepareManagedProcessReconcile(ownerId, graceMs) {
|
|
351
|
+
requireProcessText(ownerId, "owner");
|
|
352
|
+
return prepareProcessGrace(graceMs);
|
|
353
|
+
}
|
|
354
|
+
function requireProcessText(value, parameter) {
|
|
355
|
+
if (typeof value !== "string" || !value.trim())
|
|
356
|
+
throw new AppError("validation", `--${parameter} must be nonempty text`, 2);
|
|
357
|
+
}
|
|
358
|
+
function requireProcessPid(value, parameter) {
|
|
359
|
+
if (!Number.isSafeInteger(value) || Number(value) <= 0)
|
|
360
|
+
throw new AppError("validation", `--${parameter} must be a positive integer`, 2);
|
|
361
|
+
}
|
|
362
|
+
function prepareProcessGrace(value = 1_000) {
|
|
363
|
+
if (!Number.isSafeInteger(value) || value < 0 || value > 2 ** 31 - 1)
|
|
364
|
+
throw new AppError("validation", "--grace-ms must be a nonnegative integer within the supported timer range", 2);
|
|
365
|
+
return value;
|
|
366
|
+
}
|
|
233
367
|
export async function reservePorts(context, input) {
|
|
368
|
+
prepareManagedProcessLease(context, input.leaseMs);
|
|
234
369
|
const db = registry(context);
|
|
235
370
|
const claims = [];
|
|
236
371
|
const ports = {};
|
|
@@ -258,8 +393,13 @@ export async function reservePorts(context, input) {
|
|
|
258
393
|
}
|
|
259
394
|
function registry(context) { return getResourceDatabase(resourceHome(context)); }
|
|
260
395
|
function requireProcess(db, id) { const row = db.get("SELECT * FROM managed_processes WHERE id = ?", [id]); if (!row)
|
|
261
|
-
throw new
|
|
262
|
-
function
|
|
396
|
+
throw new AppError("not_found", `Managed process is missing: ${id}`, 1); return row; }
|
|
397
|
+
export function prepareManagedProcessLease(context, leaseMs) { return leaseExpiry(context.now(), leaseMs); }
|
|
398
|
+
function leaseExpiry(now, leaseMs = defaultLeaseMs) {
|
|
399
|
+
if (!Number.isSafeInteger(leaseMs) || leaseMs <= 0 || !Number.isFinite(new Date(Date.parse(now) + leaseMs).getTime()))
|
|
400
|
+
throw new AppError("validation", "--lease-ms must be a positive integer within the supported date range", 2);
|
|
401
|
+
return new Date(Date.parse(now) + leaseMs).toISOString();
|
|
402
|
+
}
|
|
263
403
|
export function processStartedAt(pid) { const result = spawnSync("ps", ["-o", "lstart=", "-p", String(pid)], { encoding: "utf8" }); const value = result.status === 0 ? result.stdout.trim() : ""; return value || null; }
|
|
264
404
|
function releasePortClaims(db, claims) { for (const claim of claims)
|
|
265
405
|
db.run("DELETE FROM managed_resources WHERE resource_kind = 'port' AND resource_key = ? AND lease_token = ?", [claim.key, claim.token]); }
|