@deksden-com/dd-flow-cli 0.9.0-beta.1 → 0.9.0-beta.100

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 (153) hide show
  1. package/CHANGELOG.md +561 -0
  2. package/README.md +68 -0
  3. package/dist/build-info.json +8 -8
  4. package/dist/cli/command-inputs.js +305 -0
  5. package/dist/cli/help.js +91 -15
  6. package/dist/cli/hook-ingress.js +82 -0
  7. package/dist/cli/input-preparation.js +116 -0
  8. package/dist/cli/run-cli.js +1506 -334
  9. package/dist/cli.js +6 -2
  10. package/dist/harness-runtime/DD-ZCODE.md +99 -0
  11. package/dist/harness-runtime/bin/dd-agy.mjs +39 -0
  12. package/dist/harness-runtime/bin/dd-codex.mjs +25 -0
  13. package/dist/harness-runtime/bin/dd-droid.mjs +33 -0
  14. package/dist/harness-runtime/bin/dd-grok.mjs +30 -0
  15. package/dist/harness-runtime/bin/dd-opencode.mjs +21 -0
  16. package/dist/harness-runtime/bin/dd-zcode.mjs +83 -0
  17. package/dist/harness-runtime/lib/daemon-operations.mjs +215 -0
  18. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +449 -0
  19. package/dist/harness-runtime/lib/dd-agy.mjs +59 -0
  20. package/dist/harness-runtime/lib/dd-codex-daemon.d.mts +1 -0
  21. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +146 -0
  22. package/dist/harness-runtime/lib/dd-codex.mjs +562 -0
  23. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +125 -0
  24. package/dist/harness-runtime/lib/dd-droid.mjs +468 -0
  25. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +300 -0
  26. package/dist/harness-runtime/lib/dd-grok.mjs +174 -0
  27. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +217 -0
  28. package/dist/harness-runtime/lib/dd-opencode.mjs +98 -0
  29. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +644 -0
  30. package/dist/harness-runtime/lib/dd-zcode.mjs +941 -0
  31. package/dist/harness-runtime/lib/delegation-instructions.d.mts +10 -0
  32. package/dist/harness-runtime/lib/delegation-instructions.mjs +130 -0
  33. package/dist/harness-runtime/lib/dispatch-fence.mjs +14 -0
  34. package/dist/harness-runtime/lib/driver-recovery.mjs +129 -0
  35. package/dist/harness-runtime/lib/droid-observation.mjs +66 -0
  36. package/dist/harness-runtime/lib/managed-daemon.mjs +267 -0
  37. package/dist/harness-runtime/lib/model-observations.mjs +77 -0
  38. package/dist/harness-runtime/lib/native-hook-command.d.mts +1 -0
  39. package/dist/harness-runtime/lib/native-hook-command.mjs +34 -0
  40. package/dist/harness-runtime/lib/observation-clock.mjs +36 -0
  41. package/dist/harness-runtime/lib/operation-context.mjs +5 -0
  42. package/dist/harness-runtime/lib/operation-errors.mjs +19 -0
  43. package/dist/harness-runtime/lib/process-json.mjs +70 -0
  44. package/dist/harness-runtime/lib/process-snapshot.mjs +10 -0
  45. package/dist/harness-runtime/lib/runner-events.mjs +200 -0
  46. package/dist/harness-runtime/lib/runner-lock.mjs +42 -0
  47. package/dist/harness-runtime/lib/session-settlement.mjs +30 -0
  48. package/dist/harness-runtime/lib/tool-observations.d.mts +17 -0
  49. package/dist/harness-runtime/lib/tool-observations.mjs +153 -0
  50. package/dist/harness-runtime/lib/zcode-usage.mjs +54 -0
  51. package/dist/runtime/context.js +3 -1
  52. package/dist/schemas/agent-profile.schema.json +1 -1
  53. package/dist/schemas/code-review-result.schema.json +2 -2
  54. package/dist/schemas/code-work-batch.schema.json +4 -3
  55. package/dist/schemas/code-work-result.schema.json +4 -4
  56. package/dist/schemas/harness-config.schema.json +23 -0
  57. package/dist/schemas/plan-review-decision.schema.json +1 -1
  58. package/dist/schemas/plan-review-result.schema.json +2 -2
  59. package/dist/schemas/run-control-receipt.schema.json +99 -0
  60. package/dist/schemas/run-control-request.schema.json +36 -0
  61. package/dist/schemas/vnext-protocol-plan.schema.json +2 -2
  62. package/dist/services/canon.js +11 -8
  63. package/dist/services/cleanup.js +75 -21
  64. package/dist/services/cli-operation-classifier.js +22 -24
  65. package/dist/services/code-checks.js +676 -79
  66. package/dist/services/codex-hook-delivery.js +28 -0
  67. package/dist/services/command-context.js +80 -0
  68. package/dist/services/config.js +11 -8
  69. package/dist/services/continuation-outcome.js +20 -0
  70. package/dist/services/controller-fanout.js +134 -0
  71. package/dist/services/dashboard.js +103 -31
  72. package/dist/services/engines.js +125 -80
  73. package/dist/services/eval-snapshots.js +838 -74
  74. package/dist/services/execution-policy.js +170 -0
  75. package/dist/services/external-work-launch.js +110 -0
  76. package/dist/services/harness-adapter.js +147 -24
  77. package/dist/services/harness-config.js +68 -0
  78. package/dist/services/hooks.js +609 -310
  79. package/dist/services/lanes.js +62 -53
  80. package/dist/services/lifecycle-command.js +103 -17
  81. package/dist/services/lifecycle-contract.js +30 -0
  82. package/dist/services/lifecycle-invocations.js +1128 -0
  83. package/dist/services/managed-daemon-binding.js +38 -0
  84. package/dist/services/managed-processes.js +425 -0
  85. package/dist/services/merge-queue.js +179 -104
  86. package/dist/services/merge-server.js +78 -27
  87. package/dist/services/migrations.js +14 -9
  88. package/dist/services/native-daemon-history.js +49 -0
  89. package/dist/services/native-session-control.js +39 -0
  90. package/dist/services/plan-runtime.js +19 -8
  91. package/dist/services/plans.js +28 -25
  92. package/dist/services/portable-refs.js +57 -0
  93. package/dist/services/projects.js +18 -5
  94. package/dist/services/prompts.js +27 -18
  95. package/dist/services/protocols.js +64 -20
  96. package/dist/services/recovery-observation-budget.js +61 -0
  97. package/dist/services/recovery-snapshot-database.js +107 -0
  98. package/dist/services/repair-intents.js +40 -0
  99. package/dist/services/run-control-receipt.js +56 -0
  100. package/dist/services/run-control-worker.js +363 -0
  101. package/dist/services/run-control.js +876 -0
  102. package/dist/services/run-controller-adapter.js +222 -0
  103. package/dist/services/run-controller-capture.js +136 -0
  104. package/dist/services/run-controller-process.js +189 -0
  105. package/dist/services/run-controller-recovery.js +222 -0
  106. package/dist/services/run-controller-state.js +36 -0
  107. package/dist/services/run-controller.js +803 -0
  108. package/dist/services/run-engine-bindings.js +20 -62
  109. package/dist/services/run-fork.js +138 -0
  110. package/dist/services/run-observations.js +112 -0
  111. package/dist/services/run-projection.js +10 -8
  112. package/dist/services/run-recovery-runtime.js +69 -0
  113. package/dist/services/run-recovery.js +343 -0
  114. package/dist/services/runs.js +437 -81
  115. package/dist/services/runtime-budget.js +439 -0
  116. package/dist/services/runtime-command.js +43 -0
  117. package/dist/services/runtime-scope-capture.js +50 -0
  118. package/dist/services/runtime-scope-control.js +450 -0
  119. package/dist/services/runtime-scope-resume.js +565 -0
  120. package/dist/services/runtime-scope-stop.js +99 -0
  121. package/dist/services/runtime-scope-worker.js +243 -0
  122. package/dist/services/runtime-service.js +97 -0
  123. package/dist/services/schema-validation.js +21 -19
  124. package/dist/services/session-identity.js +19 -0
  125. package/dist/services/sessions.js +61 -80
  126. package/dist/services/stage-blocker.js +17 -7
  127. package/dist/services/stage-context.js +44 -18
  128. package/dist/services/stage-lifecycle.js +112 -108
  129. package/dist/services/stage-pause.js +117 -73
  130. package/dist/services/stage-work-graph.js +35 -0
  131. package/dist/services/usage.js +108 -132
  132. package/dist/services/vnext-code-review.js +245 -98
  133. package/dist/services/vnext-code.js +355 -257
  134. package/dist/services/vnext-execution-profile.js +5 -3
  135. package/dist/services/vnext-fanout.js +166 -26
  136. package/dist/services/vnext-merge.js +413 -147
  137. package/dist/services/vnext-plan-review.js +182 -109
  138. package/dist/services/vnext-plan.js +199 -82
  139. package/dist/services/vnext-protocolize.js +133 -60
  140. package/dist/services/vnext-specify.js +94 -71
  141. package/dist/services/work-registry.js +813 -160
  142. package/dist/services/workspace-bootstrap.js +76 -0
  143. package/dist/services/worktrees.js +85 -35
  144. package/dist/shared/entity-references.js +8 -0
  145. package/dist/shared/errors.js +12 -0
  146. package/dist/storage/database.js +569 -16
  147. package/dist/storage/paths.js +17 -4
  148. package/dist/storage/work-references.js +23 -0
  149. package/dist/storage/writer-contract.js +77 -0
  150. package/dist/storage/writer-migration.js +102 -0
  151. package/package.json +24 -13
  152. package/tools/audit-runtime-fix-boundaries.mjs +96 -0
  153. package/tools/repair-paused-run-status.mjs +59 -0
@@ -2,12 +2,12 @@ import fs from "node:fs";
2
2
  import { spawnSync } from "node:child_process";
3
3
  import path from "node:path";
4
4
  import { AppError } from "../shared/errors.js";
5
- import { parseJsonObject } from "../shared/json.js";
6
- import { ensureReadableFile } from "../storage/database.js";
7
- import { projectCheckoutRoot, resolveProjectRoot } from "../storage/paths.js";
5
+ import { prepareJsonFile } from "../cli/input-preparation.js";
6
+ import { projectCheckoutRoot, resolveProjectRoot, runtimeStateJsonPath } from "../storage/paths.js";
8
7
  import { appendAudit } from "./audit.js";
9
8
  import { requireProjectByRoot } from "./projects.js";
10
- import { protocolRunDiagnostics, readProtocolRuntimeState, requireProtocol } from "./protocols.js";
9
+ import { protocolRunDiagnostics, readProtocolRuntimeState, repairProtocolRuntimeState, requireProtocol } from "./protocols.js";
10
+ import { publicSessionIdentity } from "./session-identity.js";
11
11
  const actionKinds = new Set([
12
12
  "repair_protocol_state",
13
13
  "cancel_queue_job",
@@ -24,7 +24,8 @@ export function cleanupScan(context, input) {
24
24
  const protocols = context.db.all("SELECT id, status, stage, state_path FROM protocols WHERE project_id = ? ORDER BY id ASC", [project.id]);
25
25
  for (const protocol of protocols) {
26
26
  const terminal = ["closed", "cancelled"].includes(protocol.status) || ["closed", "cancelled"].includes(protocol.stage);
27
- if (!fs.existsSync(protocol.state_path)) {
27
+ const requiresMigration = protocol.state_path !== runtimeStateJsonPath(context.ddFlowHome, project.id, protocol.id);
28
+ if (!fs.existsSync(protocol.state_path) || requiresMigration) {
28
29
  pushFinding(findings, actions, "missing_protocol_runtime_state", "warning", {
29
30
  protocol_id: protocol.id,
30
31
  path: protocol.state_path,
@@ -42,6 +43,8 @@ export function cleanupScan(context, input) {
42
43
  }
43
44
  }
44
45
  const fullProtocol = requireProtocol(context, protocol.id, project.id);
46
+ if (requiresMigration)
47
+ continue;
45
48
  const state = readProtocolRuntimeState(context, fullProtocol).state;
46
49
  const diagnostics = protocolRunDiagnostics(context, fullProtocol, state).diagnostics;
47
50
  for (const diagnostic of diagnostics.filter((entry) => entry.code === "protocol_run_stage_mismatch")) {
@@ -68,7 +71,7 @@ export function cleanupScan(context, input) {
68
71
  action: { kind: "cancel_queue_job", project_id: project.id, protocol_id: job.protocol_id }
69
72
  });
70
73
  }
71
- const staleSessions = context.db.all(`SELECT session_id, protocol_id, workspace_path FROM sessions
74
+ const staleSessions = context.db.all(`SELECT session_id, harness, provider_session_id, protocol_id, workspace_path FROM sessions
72
75
  WHERE project_id = ? AND status IN ('pending', 'active', 'waiting_user', 'blocked', 'stopping')
73
76
  ORDER BY updated_at ASC, session_id ASC`, [project.id]);
74
77
  for (const session of staleSessions) {
@@ -79,11 +82,11 @@ export function cleanupScan(context, input) {
79
82
  const terminalProtocol = Boolean(protocol && (["closed", "cancelled"].includes(protocol.status) || ["closed", "cancelled"].includes(protocol.stage)));
80
83
  if (missingWorkspace || terminalProtocol) {
81
84
  pushFinding(findings, actions, "stale_flow_session", "warning", {
82
- session_id: session.session_id,
85
+ session: publicSessionIdentity(session),
83
86
  protocol_id: session.protocol_id,
84
87
  missing_workspace: missingWorkspace,
85
88
  terminal_protocol: terminalProtocol,
86
- action: { kind: "close_flow_session", project_id: project.id, session_id: session.session_id }
89
+ action: { kind: "close_flow_session", project_id: project.id, session: publicSessionIdentity(session) }
87
90
  });
88
91
  }
89
92
  }
@@ -169,20 +172,29 @@ export function cleanupScan(context, input) {
169
172
  plan: { project_root: project.root, project_id: project.id, actions }
170
173
  };
171
174
  }
172
- export function cleanupApply(context, input) {
173
- const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
175
+ export function prepareCleanupApply(context, input) {
174
176
  const reason = input.reason.trim();
175
177
  if (!reason) {
176
178
  throw new AppError("validation", "cleanup apply requires --reason", 2);
177
179
  }
178
- ensureReadableFile(input.planFile);
179
- const plan = validateCleanupPlan(parseJsonObject(fs.readFileSync(input.planFile, "utf8"), input.planFile), project);
180
+ const value = prepareJsonFile(input.planFile, "cleanup plan").value;
181
+ if (!value || typeof value !== "object" || Array.isArray(value))
182
+ throw new AppError("validation", "cleanup plan must be an object", 2);
183
+ const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
184
+ const plan = validateCleanupPlan(value, project);
180
185
  validateNoDestructiveSurprises(context, plan, input.force);
186
+ return { project, reason, plan };
187
+ }
188
+ export function cleanupApply(context, input) {
189
+ const { project, reason, plan } = input.prepared ?? prepareCleanupApply(context, input);
181
190
  const now = context.now();
182
191
  const results = [];
183
- context.db.exec("BEGIN IMMEDIATE");
192
+ context.db.beginWriteTransaction();
193
+ let executing = false;
184
194
  try {
195
+ validateNoDestructiveSurprises(context, plan, input.force);
185
196
  for (const action of plan.actions) {
197
+ executing = true;
186
198
  results.push(applyAction(context, action, reason, input.force, now));
187
199
  }
188
200
  const changed = results.filter((result) => result.changed).length;
@@ -199,6 +211,8 @@ export function cleanupApply(context, input) {
199
211
  }
200
212
  catch (error) {
201
213
  context.db.exec("ROLLBACK");
214
+ if (executing)
215
+ throw new AppError("cleanup_apply_failed", "Cleanup failed after execution began; inspect retained artifacts before retrying", 1, { effect: "unknown", database_rolled_back: true, attempted_results: results, cause: error instanceof Error ? error.message : String(error), ...(error instanceof AppError ? { cause_code: error.code } : {}) });
202
216
  throw error;
203
217
  }
204
218
  return {
@@ -241,11 +255,35 @@ function validateCleanupPlan(value, project) {
241
255
  actual_project_id: action.project_id
242
256
  });
243
257
  }
258
+ if (action.kind === "close_flow_session") {
259
+ const session = action.session;
260
+ if (!session || typeof session.harness_id !== "string" || !session.harness_id || typeof session.session_id !== "string" || !session.session_id) {
261
+ throw new AppError("validation", "close_flow_session requires a native session identity", 2);
262
+ }
263
+ }
264
+ const requiredField = action.kind === "discard_stale_run" ? "run_id"
265
+ : ["repair_protocol_state", "cancel_queue_job", "close_worktree_record"].includes(String(action.kind)) ? "protocol_id" : undefined;
266
+ if (requiredField && (typeof action[requiredField] !== "string" || !action[requiredField].trim()))
267
+ throw new AppError("validation", `cleanup ${String(action.kind)} requires ${requiredField}`, 2);
268
+ const numericField = action.kind === "expire_lane_lock" ? "lock_id" : action.kind === "expire_lane_waiter" ? "waiter_id" : undefined;
269
+ if (numericField && (!Number.isSafeInteger(action[numericField]) || action[numericField] < 1))
270
+ throw new AppError("validation", `cleanup ${String(action.kind)} requires a positive integer ${numericField}`, 2);
271
+ if (action.worktree_path !== undefined && (typeof action.worktree_path !== "string" || !path.isAbsolute(action.worktree_path)))
272
+ throw new AppError("validation", "cleanup worktree_path must be an absolute path", 2);
244
273
  return action;
245
274
  });
246
275
  return { project_root: project.root, project_id: project.id, actions };
247
276
  }
248
277
  function validateNoDestructiveSurprises(context, plan, force) {
278
+ for (const action of plan.actions) {
279
+ if (action.kind === "repair_protocol_state")
280
+ requireProtocol(context, action.protocol_id, action.project_id);
281
+ if (action.kind === "discard_stale_run") {
282
+ const run = context.db.get("SELECT index_json, status FROM runs WHERE project_id = ? AND id = ?", [action.project_id, action.run_id]);
283
+ if (run && ["running", "discarded"].includes(run.status))
284
+ cleanupRunIndex(run.index_json);
285
+ }
286
+ }
249
287
  const destructive = plan.actions.find((action) => action.kind === "close_worktree_record" && action.worktree_path && dirtyGit(action.worktree_path));
250
288
  if (destructive && !force) {
251
289
  throw new AppError("worktree_dirty", "cleanup apply refuses dirty worktree actions without --force --reason", 1, {
@@ -276,7 +314,7 @@ function applyAction(context, action, reason, force, now) {
276
314
  const protocol = requireProtocol(context, action.protocol_id, action.project_id);
277
315
  const beforePath = protocol.state_path;
278
316
  const existed = fs.existsSync(beforePath);
279
- readProtocolRuntimeState(context, protocol);
317
+ repairProtocolRuntimeState(context, protocol);
280
318
  return {
281
319
  kind: action.kind,
282
320
  changed: !existed || beforePath !== protocol.state_path,
@@ -305,20 +343,23 @@ function applyAction(context, action, reason, force, now) {
305
343
  });
306
344
  return { kind: action.kind, changed: true, protocol_id: action.protocol_id };
307
345
  }
308
- if (action.kind === "close_flow_session" && action.session_id) {
346
+ if (action.kind === "close_flow_session" && action.session) {
347
+ const stored = context.db.get("SELECT session_id FROM sessions WHERE project_id = ? AND harness = ? AND COALESCE(provider_session_id, session_id) = ?", [action.project_id, action.session.harness_id, action.session.session_id]);
348
+ if (!stored)
349
+ return { kind: action.kind, changed: false, skipped: true, reason: "session_not_found", session: action.session };
309
350
  const result = context.db.run(`UPDATE sessions SET status = 'stopped', stop_reason = ?, updated_at = ?, stopped_at = ?
310
- WHERE project_id = ? AND session_id = ? AND status IN ('pending', 'active', 'waiting_user', 'blocked', 'stopping')`, [reason, now, now, action.project_id, action.session_id]);
351
+ WHERE project_id = ? AND session_id = ? AND status IN ('pending', 'active', 'waiting_user', 'blocked', 'stopping')`, [reason, now, now, action.project_id, stored.session_id]);
311
352
  if (result.changes !== 1) {
312
- return { kind: action.kind, changed: false, skipped: true, reason: "already_stopped", session_id: action.session_id };
353
+ return { kind: action.kind, changed: false, skipped: true, reason: "already_stopped", session: action.session };
313
354
  }
314
355
  appendAudit(context, {
315
356
  projectId: action.project_id,
316
357
  eventType: "flow_session.stopped",
317
358
  reason,
318
359
  forced: force,
319
- payload: { session_id: action.session_id, source: "cleanup.apply" }
360
+ payload: { session_id: stored.session_id, source: "cleanup.apply" }
320
361
  });
321
- return { kind: action.kind, changed: true, session_id: action.session_id };
362
+ return { kind: action.kind, changed: true, session: action.session };
322
363
  }
323
364
  if (action.kind === "discard_stale_run" && action.run_id) {
324
365
  const run = context.db.get("SELECT status, runtime_path, run_dir, workspace_root, index_json FROM runs WHERE project_id = ? AND id = ?", [action.project_id, action.run_id]);
@@ -397,7 +438,7 @@ function applyAction(context, action, reason, force, now) {
397
438
  return { kind: action.kind, changed: false, skipped: true, reason: "invalid_action" };
398
439
  }
399
440
  function discardedRunIndex(indexJson, reason, now) {
400
- const index = JSON.parse(indexJson);
441
+ const index = cleanupRunIndex(indexJson);
401
442
  index.schema_id = "dd-flow/flow-run@3";
402
443
  index.status = "discarded";
403
444
  index.verdict = "discarded";
@@ -407,12 +448,25 @@ function discardedRunIndex(indexJson, reason, now) {
407
448
  index.finished_at ??= now;
408
449
  return index;
409
450
  }
451
+ function cleanupRunIndex(indexJson) {
452
+ try {
453
+ const index = JSON.parse(indexJson);
454
+ if (!index || typeof index !== "object" || Array.isArray(index))
455
+ throw new Error("RUN index must be an object");
456
+ return index;
457
+ }
458
+ catch (error) {
459
+ throw new AppError("runtime_state_invalid", "Cleanup cannot read retained RUN state", 1, { cause: String(error) });
460
+ }
461
+ }
410
462
  function dirtyGit(worktreePath) {
411
463
  if (!fs.existsSync(worktreePath)) {
412
464
  return false;
413
465
  }
414
466
  const result = spawnSync("git", ["-C", worktreePath, "status", "--porcelain"], { encoding: "utf8" });
415
- return result.status === 0 && result.stdout.trim().length > 0;
467
+ if (result.error || result.status !== 0)
468
+ throw new AppError("workspace_status_failed", "Cannot establish worktree status before cleanup", 1, { path: worktreePath, cause: result.error?.message ?? result.stderr });
469
+ return result.stdout.trim().length > 0;
416
470
  }
417
471
  function safeCleanupWorktreePath(context, projectId, worktreePath) {
418
472
  if (!fs.existsSync(worktreePath)) {
@@ -1,3 +1,4 @@
1
+ import { parseCommandArgs } from "./lifecycle-command.js";
1
2
  export function classifyCliOperation(args, env = process.env) {
2
3
  const positional = positionalArgs(args);
3
4
  const [family = null, command = null, action = null] = positional;
@@ -14,19 +15,7 @@ export function classifyCliOperation(args, env = process.env) {
14
15
  return { mode: "normal_write", operation, family, command, action };
15
16
  }
16
17
  function positionalArgs(args) {
17
- const positional = [];
18
- for (let index = 0; index < args.length; index += 1) {
19
- const value = args[index];
20
- if (!value)
21
- continue;
22
- if (value.startsWith("--")) {
23
- if (!value.includes("=") && args[index + 1] && !args[index + 1]?.startsWith("--"))
24
- index += 1;
25
- continue;
26
- }
27
- positional.push(value);
28
- }
29
- return positional;
18
+ return parseCommandArgs(args).positional;
30
19
  }
31
20
  export function isReadOnlyCliOperation(args, env = process.env) {
32
21
  return classifyCliOperation(args, env).mode === "read_only_diagnostics";
@@ -57,7 +46,7 @@ function isMbUpgradeMode(args, env) {
57
46
  }
58
47
  return marked && isUpgradeAllowlisted(args);
59
48
  }
60
- function isUpgradeAllowlisted(args) {
49
+ export function isUpgradeAllowlisted(args) {
61
50
  const positional = positionalArgs(args);
62
51
  const [family, command] = positional;
63
52
  if (family === "status")
@@ -82,14 +71,9 @@ function hasRunBinding(args) {
82
71
  return Boolean(optionValue(args, "run") || positionalArgs(args).some((value) => value.startsWith("RUN-")));
83
72
  }
84
73
  function optionValue(args, name) {
85
- for (let index = args.length - 1; index >= 0; index -= 1) {
86
- const arg = args[index];
87
- if (arg === `--${name}`)
88
- return args[index + 1] ?? null;
89
- if (arg?.startsWith(`--${name}=`))
90
- return arg.slice(name.length + 3);
91
- }
92
- return null;
74
+ const values = parseCommandArgs(args).options.get(name);
75
+ const value = values?.[values.length - 1];
76
+ return value && value !== "__dd_flow_flag__" ? value : null;
93
77
  }
94
78
  function isRouterNative(args) {
95
79
  const first = args[0];
@@ -101,11 +85,17 @@ function isRouterNative(args) {
101
85
  return true;
102
86
  if (["engine", "version", "schema"].includes(first))
103
87
  return true;
88
+ if (first === "storage" && args[1] === "migrate-writer")
89
+ return true;
104
90
  return (first === "codex" && args[1] === "hook" && args[2] === "handle")
91
+ || (first === "zcode" && args[1] === "hook" && args[2] === "handle")
92
+ || (first === "zcode" && args[1] === "invocation" && args[2] === "prepare")
105
93
  || (first === "zcode" && args[1] === "event" && args[2] === "handle")
106
94
  || (first === "zcode" && args[1] === "usage" && args[2] === "ingest")
107
95
  || (first === "grok" && args[1] === "event" && args[2] === "handle")
108
96
  || (first === "grok" && args[1] === "usage" && args[2] === "ingest")
97
+ || (first === "droid" && args[1] === "event" && args[2] === "handle")
98
+ || (first === "droid" && args[1] === "usage" && args[2] === "ingest")
109
99
  || (first === "opencode" && args[1] === "event" && args[2] === "handle")
110
100
  || (first === "opencode" && args[1] === "usage" && args[2] === "ingest")
111
101
  || (first === "agy" && args[1] === "event" && args[2] === "handle")
@@ -119,13 +109,21 @@ function isReadOnlyDiagnostic(family, command, action) {
119
109
  if (family === "canon")
120
110
  return command === "status" || command === "resolve";
121
111
  if (family === "project")
122
- return command === "status" || command === "resolve";
112
+ return command === "status" || command === "resolve" || (command === "config" && action === "status");
123
113
  if (family === "protocol")
124
114
  return ["status", "branch-status", "ready", "blockers", "implement"].includes(command ?? "");
125
115
  if (family === "plan")
126
116
  return command === "status";
127
117
  if (family === "run")
128
- return command === "status" || command === "list";
118
+ return ["status", "list", "timeline"].includes(command ?? "") || (["config", "flags", "drive", "control"].includes(command ?? "") && action === "status") || (command === "vars" && ["ls", "get"].includes(action ?? "")) || (command === "recovery" && action === "inspect");
119
+ if (family === "runtime" && command === "process" && ["status", "check-admission"].includes(action ?? ""))
120
+ return true;
121
+ if (family === "runtime" && command === "scope" && action === "status")
122
+ return true;
123
+ if (family === "work")
124
+ return ["status", "ls", "show"].includes(command ?? "");
125
+ if (family === "stage")
126
+ return command === "fanout" && action === "status";
129
127
  if (family === "lane") {
130
128
  return (command === "status" ||
131
129
  command === "waiters" ||