@deksden-com/dd-flow-cli 0.9.0-beta.75 → 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.
Files changed (68) hide show
  1. package/dist/build-info.json +6 -6
  2. package/dist/cli/command-inputs.js +300 -0
  3. package/dist/cli/help.js +1 -1
  4. package/dist/cli/hook-ingress.js +82 -0
  5. package/dist/cli/input-preparation.js +115 -0
  6. package/dist/cli/run-cli.js +1015 -423
  7. package/dist/cli.js +6 -2
  8. package/dist/harness-runtime/lib/dd-codex-daemon.d.mts +1 -0
  9. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +7 -2
  10. package/dist/harness-runtime/lib/dd-codex.mjs +137 -13
  11. package/dist/harness-runtime/lib/dd-droid.mjs +2 -2
  12. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +1 -1
  13. package/dist/harness-runtime/lib/delegation-instructions.mjs +13 -4
  14. package/dist/harness-runtime/lib/native-hook-command.d.mts +1 -0
  15. package/dist/services/canon.js +11 -8
  16. package/dist/services/cleanup.js +49 -8
  17. package/dist/services/cli-operation-classifier.js +12 -24
  18. package/dist/services/codex-hook-delivery.js +28 -0
  19. package/dist/services/config.js +11 -8
  20. package/dist/services/controller-fanout.js +2 -2
  21. package/dist/services/dashboard.js +94 -31
  22. package/dist/services/engines.js +75 -61
  23. package/dist/services/eval-snapshots.js +138 -38
  24. package/dist/services/execution-policy.js +5 -12
  25. package/dist/services/hooks.js +226 -203
  26. package/dist/services/lanes.js +60 -52
  27. package/dist/services/lifecycle-command.js +13 -1
  28. package/dist/services/lifecycle-invocations.js +285 -94
  29. package/dist/services/managed-processes.js +158 -18
  30. package/dist/services/merge-queue.js +173 -98
  31. package/dist/services/migrations.js +13 -8
  32. package/dist/services/plan-runtime.js +19 -8
  33. package/dist/services/plans.js +28 -25
  34. package/dist/services/projects.js +11 -2
  35. package/dist/services/prompts.js +23 -16
  36. package/dist/services/protocols.js +54 -19
  37. package/dist/services/recovery-observation-budget.js +1 -1
  38. package/dist/services/run-controller-adapter.js +31 -20
  39. package/dist/services/run-controller-state.js +5 -0
  40. package/dist/services/run-controller.js +102 -56
  41. package/dist/services/run-fork.js +36 -22
  42. package/dist/services/run-recovery.js +32 -17
  43. package/dist/services/runs.js +110 -38
  44. package/dist/services/runtime-budget.js +120 -48
  45. package/dist/services/runtime-scope-capture.js +2 -2
  46. package/dist/services/runtime-scope-control.js +2 -2
  47. package/dist/services/runtime-scope-resume.js +34 -12
  48. package/dist/services/runtime-scope-worker.js +49 -16
  49. package/dist/services/runtime-service.js +8 -7
  50. package/dist/services/schema-validation.js +9 -7
  51. package/dist/services/sessions.js +35 -69
  52. package/dist/services/stage-blocker.js +17 -7
  53. package/dist/services/stage-context.js +42 -16
  54. package/dist/services/stage-lifecycle.js +82 -100
  55. package/dist/services/stage-pause.js +54 -24
  56. package/dist/services/vnext-code-review.js +82 -32
  57. package/dist/services/vnext-code.js +78 -45
  58. package/dist/services/vnext-fanout.js +13 -12
  59. package/dist/services/vnext-merge.js +68 -35
  60. package/dist/services/vnext-plan-review.js +84 -41
  61. package/dist/services/vnext-plan.js +91 -70
  62. package/dist/services/vnext-protocolize.js +116 -50
  63. package/dist/services/vnext-specify.js +70 -57
  64. package/dist/services/work-registry.js +195 -92
  65. package/dist/services/worktrees.js +85 -35
  66. package/dist/storage/database.js +104 -10
  67. package/dist/storage/paths.js +17 -4
  68. 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 registerManagedProcess(context, input) {
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, JSON.stringify({ ...input.metadata, dd_flow_home: context.ddFlowHome, ...(input.ownerPid ? { owner_pid: input.ownerPid, owner_pid_started_at: processStartedAt(input.ownerPid) } : {}) })]);
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 db = registry(context);
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
- return registry(context).all("SELECT * FROM managed_processes ORDER BY updated_at DESC, id");
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
- if (record.lease_token !== input.leaseToken)
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(input.graceMs ?? 1_000);
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 Error(`Managed process is missing: ${id}`); return row; }
262
- function leaseExpiry(now, leaseMs = defaultLeaseMs) { return new Date(Date.parse(now) + leaseMs).toISOString(); }
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]); }