@deksden-com/dd-flow-cli 0.3.1 → 0.4.1

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 (43) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +25 -9
  3. package/dist/build-info.json +5 -5
  4. package/dist/cli/help.js +58 -32
  5. package/dist/cli/run-cli.js +282 -50
  6. package/dist/domain/entity-ids.js +4 -4
  7. package/dist/domain/flow-contract.js +502 -36
  8. package/dist/domain/validation.js +34 -0
  9. package/dist/protocol/local-files.js +8 -6
  10. package/dist/schemas/code-stage-report.schema.json +197 -2
  11. package/dist/schemas/flow-contract.schema.json +126 -0
  12. package/dist/schemas/flow-run-index-v3.schema.json +203 -0
  13. package/dist/schemas/flow-run-index.schema.json +22 -2
  14. package/dist/schemas/flow-run.schema.json +36 -0
  15. package/dist/schemas/merge-stage-report.schema.json +213 -2
  16. package/dist/schemas/plan-stage-report.schema.json +156 -2
  17. package/dist/schemas/release-impact.schema.json +16 -0
  18. package/dist/services/audit.js +3 -3
  19. package/dist/services/branch-context.js +266 -0
  20. package/dist/services/canon.js +0 -1
  21. package/dist/services/cleanup.js +6 -6
  22. package/dist/services/cli-operation-classifier.js +1 -1
  23. package/dist/services/compatibility-preflight.js +8 -77
  24. package/dist/services/dashboard.js +48 -11
  25. package/dist/services/engines.js +123 -13
  26. package/dist/services/hooks.js +6 -6
  27. package/dist/services/ids.js +40 -49
  28. package/dist/services/merge-queue.js +240 -20
  29. package/dist/services/merge-worker.js +12 -4
  30. package/dist/services/migrations.js +64 -0
  31. package/dist/services/plans.js +23 -16
  32. package/dist/services/projects.js +2 -2
  33. package/dist/services/prompts.js +322 -0
  34. package/dist/services/protocols.js +78 -42
  35. package/dist/services/run-projection.js +80 -0
  36. package/dist/services/runs.js +360 -22
  37. package/dist/services/schema-validation.js +35 -12
  38. package/dist/services/sessions.js +81 -3
  39. package/dist/services/status.js +32 -1
  40. package/dist/services/usage.js +233 -0
  41. package/dist/services/worktrees.js +24 -19
  42. package/dist/storage/database.js +223 -9
  43. package/package.json +1 -1
@@ -1,3 +1,4 @@
1
+ import fs from "node:fs";
1
2
  import { createContext } from "../runtime/context.js";
2
3
  import { helpForArgs } from "./help.js";
3
4
  import { AppError, isAppError } from "../shared/errors.js";
@@ -5,7 +6,7 @@ import { writeJson } from "../shared/json.js";
5
6
  import { archiveProject, getProjectStatus, migrateProjectIds, registerProject, resolveProject } from "../services/projects.js";
6
7
  import { cancelProtocol, getProtocolBlockers, getProtocolStatus, getReadyProtocols, implementProtocol, readyForMerge, registerProtocol, requireProtocol, syncProtocolFromRun, transitionProtocol } from "../services/protocols.js";
7
8
  import { blockPlanItem, completePlanItem, getPlanStatus, setProtocolPlan, skipPlanItem, startPlanItem } from "../services/plans.js";
8
- import { cancelMergeQueueJob, claimNextMergeJob, completeMergeJob, failMergeJob, getMergeQueueStatus, noteMergeJob, waitNextMergeJob } from "../services/merge-queue.js";
9
+ import { cancelMergeQueueJob, claimNextMergeJob, claimMergeBundle, completeMergeBundle, completeMergeJob, failMergeBundle, failMergeJob, getMergeBundleStatus, getMergeQueueStatus, noteMergeJob, waitNextMergeJob } from "../services/merge-queue.js";
9
10
  import { getMergeWorkerStatus, oneShotMergeClaim, startMergeWorker, stopProjectMergeWorker } from "../services/merge-worker.js";
10
11
  import { cleanupApply, cleanupScan } from "../services/cleanup.js";
11
12
  import { getCodexHomeStatus, getCodexHooksStatus, handleCodexHook, initCodexHome, installCodexHooks, planCodexHome, printCodexHomeEnv, printCodexHooks, removeCodexHome, removeCodexHooks } from "../services/hooks.js";
@@ -17,17 +18,19 @@ import { getRuntimeStatus } from "../services/status.js";
17
18
  import { getCliVersionReport } from "../services/build-info.js";
18
19
  import { autoRefreshDashboards, dashboardData, getCmuxStatus, openDashboard, openGlobalDashboard, openProtocolDashboard, refreshAllDashboards, refreshDashboard, refreshGlobalDashboard, renderDashboard, renderGlobalDashboard } from "../services/dashboard.js";
19
20
  import { dashboardTargetSummary, preferredDashboardCommand, relatedDashboardCommands, resolveDashboardTarget } from "../services/dashboard-targets.js";
20
- import { getFlowSessionStatus, registerFlowSession, stopFlowSession, stopMergeWorker } from "../services/sessions.js";
21
- import { attachFlowRunStage, completeFlowRun, completeFlowRunStage, getFlowRunStatus, listFlowRuns, startFlowRun } from "../services/runs.js";
21
+ import { getFlowSessionStatus, registerFlowSession, stopFlowSession, stopMergeWorker, syncFlowSessionUsage } from "../services/sessions.js";
22
+ import { getProtocolBranchContext } from "../services/branch-context.js";
23
+ import { attachFlowRunStage, completeFlowRun, completeFlowRunStage, getFlowRunTimeline, getFlowRunUsage, getFlowRunFlagsStatus, getFlowRunStatus, listFlowRuns, reviseFlowRunFlags, startFlowRun } from "../services/runs.js";
22
24
  import { previewNextEntityId } from "../services/ids.js";
23
25
  import { validateSchema } from "../services/schema-validation.js";
24
26
  import { preflightMemoryPermissions } from "../services/memory-permissions.js";
25
- import { planMigration, verifyMigrationReport } from "../services/migrations.js";
27
+ import { assessMigrationImpact, planMigration, verifyMigrationReport } from "../services/migrations.js";
26
28
  import { publishProjectSummary } from "../services/project-summary.js";
27
29
  import { requireProjectByRoot } from "../services/projects.js";
28
30
  import { resolveProjectRoot } from "../storage/paths.js";
29
- import { doctorEngines, engineInfo, installCurrentEngine, listEngines, resolveEngine, routeArgsThroughEngine } from "../services/engines.js";
31
+ import { doctorEngines, engineInfo, installCurrentEngine, listEngines, resolveEngine, resolveOperationProjectRoot, routeArgsThroughEngine } from "../services/engines.js";
30
32
  import { preflightCliCompatibility } from "../services/compatibility-preflight.js";
33
+ import { renderWorkerPrompt } from "../services/prompts.js";
31
34
  const defaultIo = {
32
35
  stdout: process.stdout,
33
36
  stderr: process.stderr,
@@ -56,7 +59,8 @@ export async function runCli(args, io = defaultIo, env = process.env) {
56
59
  }
57
60
  const context = createContext(env);
58
61
  emitHumanProgress(io, output, progress, "start");
59
- const routed = await routeArgsThroughEngine(context, output.args, io, io.stdin, env);
62
+ const scopeProjectRoot = resolveOperationProjectRoot(context, output.args);
63
+ const routed = await routeArgsThroughEngine(context, output.args, io, io.stdin, env, scopeProjectRoot);
60
64
  if (routed?.routed && !routed.inProcess) {
61
65
  emitHumanProgress(io, output, progress, "done");
62
66
  return routed.exitCode;
@@ -64,11 +68,12 @@ export async function runCli(args, io = defaultIo, env = process.env) {
64
68
  const dispatchContext = routed?.inProcess
65
69
  ? createContext({ ...env, DD_FLOW_ENGINE_MODE: "1", DD_FLOW_ENGINE_HOME: "in-process", DD_FLOW_ROUTED_FROM: getCliVersionReport().cli.version })
66
70
  : context;
67
- preflightCliCompatibility(dispatchContext, output.args, env);
68
- const result = await dispatch(output.args, dispatchContext, io);
69
- refreshDashboardsAfterMutation(context, output.args, result);
71
+ preflightCliCompatibility(dispatchContext, output.args, env, scopeProjectRoot);
72
+ const result = await dispatch(output.args, dispatchContext, io, scopeProjectRoot);
73
+ const refreshWarnings = refreshDashboardsAfterMutation(context, output.args, result, scopeProjectRoot);
74
+ const resultWithWarnings = addWarnings(result, refreshWarnings);
70
75
  emitHumanProgress(io, output, progress, "done");
71
- const payload = attachProgress(result, progress, output);
76
+ const payload = attachProgress(resultWithWarnings, progress, output);
72
77
  if (output.json)
73
78
  writeJson(io.stdout, payload);
74
79
  else
@@ -223,6 +228,18 @@ function renderHumanResult(args, result) {
223
228
  const job = record.job;
224
229
  lines.push(`job: ${String(job.protocol_id ?? "none")} ${String(job.status ?? "")}`.trim());
225
230
  }
231
+ const branchContext = recordObject(record.branch_context);
232
+ const mergeBundle = recordObject(branchContext?.merge_bundle);
233
+ if (branchContext) {
234
+ lines.push(`branch: ${String(branchContext.feature_branch ?? branchContext.grouping_key ?? "unknown")}`);
235
+ const protocols = Array.isArray(branchContext.protocols) ? branchContext.protocols.length : 0;
236
+ lines.push(`branch_protocols: ${protocols}`);
237
+ if (mergeBundle) {
238
+ const eligible = Array.isArray(mergeBundle.eligible_protocols) ? mergeBundle.eligible_protocols.length : 0;
239
+ const notReady = Array.isArray(mergeBundle.not_ready_protocols) ? mergeBundle.not_ready_protocols.length : 0;
240
+ lines.push(`bundle: ${String(mergeBundle.reason ?? "unknown")} ready=${eligible} not_ready=${notReady}`);
241
+ }
242
+ }
226
243
  if (record.stopped)
227
244
  lines.push(`stopped: ${String(record.reason ?? "true")}`);
228
245
  if (record.timed_out)
@@ -361,7 +378,7 @@ function stringValue(value) {
361
378
  function shortSha(value) {
362
379
  return value ? value.slice(0, 12) : "unknown";
363
380
  }
364
- async function dispatch(args, context, io) {
381
+ async function dispatch(args, context, io, scopeProjectRoot = null) {
365
382
  const [family, command, ...rest] = args;
366
383
  const parsed = parseArgs(rest);
367
384
  if (family === "engine") {
@@ -420,7 +437,16 @@ async function dispatch(args, context, io) {
420
437
  });
421
438
  }
422
439
  if (family === "protocol" && command === "status") {
423
- return getProtocolStatus(context, { protocolId: requiredPosition(parsed, 0, "protocol-id") });
440
+ const protocolId = requiredPosition(parsed, 0, "protocol-id");
441
+ return getProtocolStatus(context, { protocolId, projectRoot: requiredScopeProjectRoot(scopeProjectRoot, "protocol status") });
442
+ }
443
+ if (family === "protocol" && command === "branch-status") {
444
+ const protocolId = parsed.positional[0];
445
+ return getProtocolBranchContext(context, {
446
+ ...(protocolId ? { protocolId } : {}),
447
+ projectRoot: scopeProjectRoot ?? optionalOption(parsed, "project-root"),
448
+ workspacePath: optionalOption(parsed, "path")
449
+ });
424
450
  }
425
451
  if (family === "protocol" && command === "ready") {
426
452
  return getReadyProtocols(context, { projectRoot: requiredOption(parsed, "project-root") });
@@ -432,21 +458,25 @@ async function dispatch(args, context, io) {
432
458
  });
433
459
  }
434
460
  if (family === "protocol" && command === "implement") {
461
+ const protocolId = requiredPosition(parsed, 0, "protocol-id");
435
462
  const reason = optionalOption(parsed, "reason");
436
463
  return implementProtocol(context, {
437
- protocolId: requiredPosition(parsed, 0, "protocol-id"),
438
- projectRoot: requiredOption(parsed, "project-root"),
464
+ protocolId,
465
+ projectRoot: requiredScopeProjectRoot(scopeProjectRoot, "protocol implement"),
439
466
  force: hasOption(parsed, "force"),
440
467
  ...(reason ? { reason } : {})
441
468
  });
442
469
  }
443
470
  if (family === "protocol" && command === "ready-for-merge") {
444
- return readyForMerge(context, { protocolId: requiredPosition(parsed, 0, "protocol-id") });
471
+ const protocolId = requiredPosition(parsed, 0, "protocol-id");
472
+ return readyForMerge(context, { protocolId, projectRoot: requiredScopeProjectRoot(scopeProjectRoot, "protocol ready-for-merge") });
445
473
  }
446
474
  if (family === "protocol" && command === "transition") {
475
+ const protocolId = requiredPosition(parsed, 0, "protocol-id");
447
476
  const reason = optionalOption(parsed, "reason");
448
477
  return transitionProtocol(context, {
449
- protocolId: requiredPosition(parsed, 0, "protocol-id"),
478
+ protocolId,
479
+ projectRoot: requiredScopeProjectRoot(scopeProjectRoot, "protocol transition"),
450
480
  to: requiredOption(parsed, "to"),
451
481
  jsonFile: requiredOptionAlias(parsed, ["payload-file", "json-file"]),
452
482
  force: hasOption(parsed, "force"),
@@ -454,15 +484,19 @@ async function dispatch(args, context, io) {
454
484
  });
455
485
  }
456
486
  if (family === "protocol" && command === "sync-from-run") {
487
+ const protocolId = requiredPosition(parsed, 0, "protocol-id");
457
488
  return syncProtocolFromRun(context, {
458
- protocolId: requiredPosition(parsed, 0, "protocol-id"),
489
+ protocolId,
490
+ projectRoot: requiredScopeProjectRoot(scopeProjectRoot, "protocol sync-from-run"),
459
491
  runId: requiredOption(parsed, "run"),
460
492
  target: optionalOption(parsed, "target") ?? "auto"
461
493
  });
462
494
  }
463
495
  if (family === "protocol" && command === "cancel") {
496
+ const protocolId = requiredPosition(parsed, 0, "protocol-id");
464
497
  return cancelProtocol(context, {
465
- protocolId: requiredPosition(parsed, 0, "protocol-id"),
498
+ protocolId,
499
+ projectRoot: requiredScopeProjectRoot(scopeProjectRoot, "protocol cancel"),
466
500
  reason: requiredOption(parsed, "reason"),
467
501
  closeSessions: parseOptionalBoolean(optionalOption(parsed, "close-sessions"), "close-sessions") ?? false,
468
502
  cancelQueue: parseOptionalBoolean(optionalOption(parsed, "cancel-queue"), "cancel-queue") ?? false,
@@ -473,9 +507,11 @@ async function dispatch(args, context, io) {
473
507
  }
474
508
  if (family === "transition") {
475
509
  const transitionParsed = parseArgs([command ?? "", ...rest]);
510
+ const protocolId = requiredPosition(transitionParsed, 0, "protocol-id");
476
511
  const reason = optionalOption(transitionParsed, "reason");
477
512
  return transitionProtocol(context, {
478
- protocolId: requiredPosition(transitionParsed, 0, "protocol-id"),
513
+ protocolId,
514
+ projectRoot: requiredScopeProjectRoot(scopeProjectRoot, "transition"),
479
515
  to: requiredOption(transitionParsed, "to"),
480
516
  jsonFile: requiredOptionAlias(transitionParsed, ["payload-file", "json-file"]),
481
517
  force: hasOption(transitionParsed, "force"),
@@ -483,22 +519,37 @@ async function dispatch(args, context, io) {
483
519
  });
484
520
  }
485
521
  if (family === "plan" && command === "set") {
522
+ const protocolId = requiredPosition(parsed, 0, "protocol-id");
486
523
  return setProtocolPlan(context, {
487
- protocolId: requiredPosition(parsed, 0, "protocol-id"),
524
+ protocolId,
525
+ projectRoot: requiredScopeProjectRoot(scopeProjectRoot, "plan set"),
488
526
  file: requiredOption(parsed, "file")
489
527
  });
490
528
  }
491
529
  if (family === "plan" && command === "status") {
492
- return getPlanStatus(context, { protocolId: requiredPosition(parsed, 0, "protocol-id") });
530
+ const protocolId = requiredPosition(parsed, 0, "protocol-id");
531
+ return getPlanStatus(context, { protocolId, projectRoot: requiredScopeProjectRoot(scopeProjectRoot, "plan status") });
493
532
  }
494
533
  if (family === "plan" && command === "item") {
495
- return dispatchPlanItem(context, parsed);
534
+ return dispatchPlanItem(context, parsed, requiredScopeProjectRoot(scopeProjectRoot, "plan item"));
535
+ }
536
+ if (family === "prompt" && command === "render") {
537
+ const workspaceRoot = optionalOption(parsed, "workspace-root");
538
+ const taskFile = optionalOption(parsed, "task-file");
539
+ return renderWorkerPrompt(context, {
540
+ projectRoot: requiredOption(parsed, "project-root"),
541
+ runId: requiredOption(parsed, "run"),
542
+ stage: requiredOption(parsed, "stage"),
543
+ profile: requiredOption(parsed, "profile"),
544
+ ...(taskFile ? { taskFile } : { planItemId: requiredOption(parsed, "plan-item") }),
545
+ ...(workspaceRoot ? { workspaceRoot } : {})
546
+ });
496
547
  }
497
548
  if (family === "lane") {
498
549
  return dispatchLane(context, command, parsed);
499
550
  }
500
551
  if (family === "merge-queue") {
501
- return dispatchMergeQueue(context, command, parsed);
552
+ return dispatchMergeQueue(context, command, parsed, requiredScopeProjectRootForOptional(scopeProjectRoot, command, parsed));
502
553
  }
503
554
  if (family === "merge") {
504
555
  return dispatchMerge(context, command, parsed);
@@ -543,7 +594,7 @@ async function dispatch(args, context, io) {
543
594
  return dispatchCodexHook(context, parsed, await readStdin(io.stdin));
544
595
  }
545
596
  if (family === "worktree") {
546
- return dispatchWorktree(context, command, parsed);
597
+ return dispatchWorktree(context, command, parsed, requiredScopeProjectRoot(scopeProjectRoot, `worktree ${command ?? ""}`));
547
598
  }
548
599
  throw new AppError("usage", `Unknown command: ${args.join(" ") || "<empty>"}`, 2);
549
600
  }
@@ -566,6 +617,8 @@ function dispatchEngine(context, command, parsed) {
566
617
  throw new AppError("usage", `Unknown engine command: ${command ?? "<empty>"}`, 2);
567
618
  }
568
619
  function dispatchMigration(context, command, parsed) {
620
+ if (command === "impact")
621
+ return assessMigrationImpact(context, { projectRoot: requiredOption(parsed, "project-root"), ...(optionalOption(parsed, "target-version") ? { targetVersion: optionalOption(parsed, "target-version") } : {}), ...(optionalOption(parsed, "mode") ? { mode: optionalOption(parsed, "mode") } : {}) });
569
622
  if (command === "plan" || command === "report") {
570
623
  return planMigration(context, {
571
624
  projectRoot: requiredOption(parsed, "project-root"),
@@ -594,23 +647,23 @@ function dispatchCanon(context, command, parsed) {
594
647
  }
595
648
  throw new AppError("usage", `Unknown canon command: ${command ?? "<empty>"}`, 2);
596
649
  }
597
- function dispatchMergeQueue(context, command, parsed) {
650
+ function dispatchMergeQueue(context, command, parsed, scopeProjectRoot) {
598
651
  if (hasOption(parsed, "session-id")) {
599
652
  throw new AppError("usage", "--session-id is no longer accepted on merge-queue commands; use --worker-id", 2);
600
653
  }
601
654
  if (command === "status") {
602
- return getMergeQueueStatus(context, { projectRoot: requiredOption(parsed, "project-root") });
655
+ return getMergeQueueStatus(context, { projectRoot: requiredOptionOrScope(parsed, scopeProjectRoot, "project-root") });
603
656
  }
604
657
  if (command === "next") {
605
658
  return claimNextMergeJob(context, {
606
- projectRoot: requiredOption(parsed, "project-root"),
659
+ projectRoot: requiredOptionOrScope(parsed, scopeProjectRoot, "project-root"),
607
660
  workerId: requiredWorkerId(parsed),
608
661
  workspacePath: workspacePathOption(parsed)
609
662
  });
610
663
  }
611
664
  if (command === "wait-next") {
612
665
  return waitNextMergeJob(context, {
613
- projectRoot: requiredOption(parsed, "project-root"),
666
+ projectRoot: requiredOptionOrScope(parsed, scopeProjectRoot, "project-root"),
614
667
  workerId: requiredWorkerId(parsed),
615
668
  timeoutSeconds: optionalNumber(parsed, "timeout"),
616
669
  pollIntervalSeconds: optionalNumber(parsed, "poll-interval"),
@@ -619,16 +672,20 @@ function dispatchMergeQueue(context, command, parsed) {
619
672
  });
620
673
  }
621
674
  if (command === "complete") {
675
+ const protocolId = requiredPosition(parsed, 0, "protocol-id");
622
676
  return completeMergeJob(context, {
623
- protocolId: requiredPosition(parsed, 0, "protocol-id"),
677
+ protocolId,
678
+ projectRoot: requiredScopeProjectRoot(scopeProjectRoot, "merge-queue complete"),
624
679
  workerId: requiredWorkerId(parsed),
625
680
  workspacePath: workspacePathOption(parsed),
626
681
  summary: requiredOption(parsed, "summary")
627
682
  });
628
683
  }
629
684
  if (command === "fail") {
685
+ const protocolId = requiredPosition(parsed, 0, "protocol-id");
630
686
  return failMergeJob(context, {
631
- protocolId: requiredPosition(parsed, 0, "protocol-id"),
687
+ protocolId,
688
+ projectRoot: requiredScopeProjectRoot(scopeProjectRoot, "merge-queue fail"),
632
689
  workerId: requiredWorkerId(parsed),
633
690
  workspacePath: workspacePathOption(parsed),
634
691
  reason: requiredOption(parsed, "reason"),
@@ -636,15 +693,19 @@ function dispatchMergeQueue(context, command, parsed) {
636
693
  });
637
694
  }
638
695
  if (command === "note") {
696
+ const protocolId = requiredPosition(parsed, 0, "protocol-id");
639
697
  return noteMergeJob(context, {
640
- protocolId: requiredPosition(parsed, 0, "protocol-id"),
698
+ protocolId,
699
+ projectRoot: requiredScopeProjectRoot(scopeProjectRoot, "merge-queue note"),
641
700
  workerId: requiredWorkerId(parsed),
642
701
  summary: requiredOption(parsed, "summary")
643
702
  });
644
703
  }
645
704
  if (command === "cancel") {
705
+ const protocolId = requiredPosition(parsed, 0, "protocol-id");
646
706
  return cancelMergeQueueJob(context, {
647
- protocolId: requiredPosition(parsed, 0, "protocol-id"),
707
+ protocolId,
708
+ projectRoot: requiredScopeProjectRoot(scopeProjectRoot, "merge-queue cancel"),
648
709
  reason: requiredOption(parsed, "reason"),
649
710
  workerId: optionalOption(parsed, "worker-id"),
650
711
  workspacePath: optionalOption(parsed, "path"),
@@ -655,7 +716,38 @@ function dispatchMergeQueue(context, command, parsed) {
655
716
  }
656
717
  function dispatchMerge(context, command, parsed) {
657
718
  if (command === "status") {
658
- return getMergeWorkerStatus(context, { projectRoot: requiredOption(parsed, "project-root") });
719
+ return getMergeWorkerStatus(context, { projectRoot: requiredOption(parsed, "project-root"), workspacePath: optionalOption(parsed, "path") });
720
+ }
721
+ if (command === "bundle") {
722
+ const action = requiredPosition(parsed, 0, "bundle action");
723
+ if (action === "status") {
724
+ return getMergeBundleStatus(context, { projectRoot: requiredOption(parsed, "project-root"), workspacePath: workspacePathOption(parsed) });
725
+ }
726
+ if (action === "claim") {
727
+ return claimMergeBundle(context, {
728
+ projectRoot: requiredOption(parsed, "project-root"),
729
+ workerId: requiredWorkerId(parsed),
730
+ workspacePath: workspacePathOption(parsed)
731
+ });
732
+ }
733
+ if (action === "complete") {
734
+ return completeMergeBundle(context, {
735
+ projectRoot: requiredOption(parsed, "project-root"),
736
+ workerId: requiredWorkerId(parsed),
737
+ workspacePath: workspacePathOption(parsed),
738
+ summary: requiredOption(parsed, "summary")
739
+ });
740
+ }
741
+ if (action === "fail") {
742
+ return failMergeBundle(context, {
743
+ projectRoot: requiredOption(parsed, "project-root"),
744
+ workerId: requiredWorkerId(parsed),
745
+ workspacePath: workspacePathOption(parsed),
746
+ reason: requiredOption(parsed, "reason"),
747
+ requeue: parseBoolean(requiredOption(parsed, "requeue"), "requeue")
748
+ });
749
+ }
750
+ throw new AppError("usage", `Unknown merge bundle action: ${action}`, 2);
659
751
  }
660
752
  if (command === "one-shot") {
661
753
  return oneShotMergeClaim(context, {
@@ -764,10 +856,17 @@ function dispatchSession(context, command, parsed) {
764
856
  reason: requiredOption(parsed, "reason")
765
857
  });
766
858
  }
859
+ if (command === "usage" && requiredPosition(parsed, 0, "session usage action") === "sync") {
860
+ return syncFlowSessionUsage(context, {
861
+ projectRoot: requiredOption(parsed, "project-root"),
862
+ sessionId: requiredOption(parsed, "session-id")
863
+ });
864
+ }
767
865
  throw new AppError("usage", `Unknown session command: ${command ?? "<empty>"}`, 2);
768
866
  }
769
867
  function dispatchRun(context, command, parsed) {
770
868
  if (command === "start") {
869
+ const protocolOverrides = readOptionalJsonFile(optionalOption(parsed, "protocol-override-file"));
771
870
  return startFlowRun(context, {
772
871
  projectRoot: requiredOption(parsed, "project-root"),
773
872
  workspaceRoot: optionalOption(parsed, "workspace-root"),
@@ -775,7 +874,11 @@ function dispatchRun(context, command, parsed) {
775
874
  subjectType: requiredOption(parsed, "subject-type"),
776
875
  subjectId: requiredOption(parsed, "subject-id"),
777
876
  slug: requiredOption(parsed, "slug"),
778
- nextAction: optionalOption(parsed, "next-action")
877
+ nextAction: optionalOption(parsed, "next-action"),
878
+ preset: optionalOption(parsed, "preset"),
879
+ taskProfile: readOptionalJsonFile(optionalOption(parsed, "task-profile-file")),
880
+ ...(protocolOverrides ? { protocolOverrides } : {}),
881
+ runOverrides: parseFlowFlagOverrides(allOptions(parsed, "flag"))
779
882
  });
780
883
  }
781
884
  if (command === "status") {
@@ -787,6 +890,43 @@ function dispatchRun(context, command, parsed) {
787
890
  if (command === "list") {
788
891
  return listFlowRuns(context, { projectRoot: requiredOption(parsed, "project-root") });
789
892
  }
893
+ if (command === "timeline") {
894
+ return getFlowRunTimeline(context, {
895
+ projectRoot: requiredOption(parsed, "project-root"),
896
+ runId: requiredPosition(parsed, 0, "run-id"),
897
+ hiddenSections: allOptions(parsed, "hide")
898
+ });
899
+ }
900
+ if (command === "usage") {
901
+ return getFlowRunUsage(context, {
902
+ projectRoot: requiredOption(parsed, "project-root"),
903
+ runId: requiredPosition(parsed, 0, "run-id"),
904
+ groupBy: optionalOption(parsed, "group-by")
905
+ });
906
+ }
907
+ if (command === "flags") {
908
+ const action = requiredPosition(parsed, 0, "flow flags action");
909
+ const runId = requiredPosition(parsed, 1, "run-id");
910
+ if (action === "status") {
911
+ return getFlowRunFlagsStatus(context, {
912
+ projectRoot: requiredOption(parsed, "project-root"),
913
+ runId
914
+ });
915
+ }
916
+ if (action === "revise") {
917
+ return reviseFlowRunFlags(context, {
918
+ projectRoot: requiredOption(parsed, "project-root"),
919
+ runId,
920
+ expectedRevision: Number.parseInt(requiredOption(parsed, "expected-revision"), 10),
921
+ idempotencyKey: requiredOption(parsed, "idempotency-key"),
922
+ flags: parseFlowFlagOverrides(allOptions(parsed, "flag")),
923
+ preset: optionalOption(parsed, "preset"),
924
+ reason: optionalOption(parsed, "reason"),
925
+ allowDowngrade: hasOption(parsed, "allow-downgrade")
926
+ });
927
+ }
928
+ throw new AppError("usage", `Unknown run flags command: ${action}`, 2);
929
+ }
790
930
  if (command === "attach-stage") {
791
931
  return attachFlowRunStage(context, {
792
932
  projectRoot: requiredOption(parsed, "project-root"),
@@ -1179,42 +1319,45 @@ function dispatchCodexHook(context, parsed, stdin) {
1179
1319
  }
1180
1320
  throw new AppError("usage", `Unknown codex hook action: ${action}`, 2);
1181
1321
  }
1182
- function dispatchWorktree(context, command, parsed) {
1322
+ function dispatchWorktree(context, command, parsed, scopeProjectRoot) {
1183
1323
  if (command === "plan") {
1184
- return planWorktree(context, { protocolId: requiredOption(parsed, "protocol-id") });
1324
+ return planWorktree(context, { protocolId: requiredOption(parsed, "protocol-id"), projectRoot: scopeProjectRoot });
1185
1325
  }
1186
1326
  if (command === "create") {
1187
1327
  return createWorktreeRecord(context, {
1188
1328
  protocolId: requiredOption(parsed, "protocol-id"),
1329
+ projectRoot: scopeProjectRoot,
1189
1330
  branch: requiredOption(parsed, "branch"),
1190
1331
  base: requiredOption(parsed, "base"),
1191
1332
  path: requiredOption(parsed, "path")
1192
1333
  });
1193
1334
  }
1194
1335
  if (command === "status") {
1195
- return getWorktreeStatus(context, { protocolId: requiredOption(parsed, "protocol-id") });
1336
+ return getWorktreeStatus(context, { protocolId: requiredOption(parsed, "protocol-id"), projectRoot: scopeProjectRoot });
1196
1337
  }
1197
1338
  if (command === "bootstrap") {
1198
- return bootstrapWorktree(context, { protocolId: requiredOption(parsed, "protocol-id") });
1339
+ return bootstrapWorktree(context, { protocolId: requiredOption(parsed, "protocol-id"), projectRoot: scopeProjectRoot });
1199
1340
  }
1200
1341
  if (command === "close") {
1201
1342
  return closeWorktree(context, {
1202
1343
  protocolId: requiredOption(parsed, "protocol-id"),
1344
+ projectRoot: scopeProjectRoot,
1203
1345
  mode: requiredOption(parsed, "mode")
1204
1346
  });
1205
1347
  }
1206
1348
  throw new AppError("usage", `Unknown worktree command: ${command ?? "<empty>"}`, 2);
1207
1349
  }
1208
- function dispatchPlanItem(context, parsed) {
1350
+ function dispatchPlanItem(context, parsed, projectRoot) {
1209
1351
  const action = requiredPosition(parsed, 0, "plan item action");
1210
1352
  const protocolId = requiredPosition(parsed, 1, "protocol-id");
1211
1353
  const itemId = requiredPosition(parsed, 2, "item-id");
1212
1354
  if (action === "start") {
1213
- return startPlanItem(context, { protocolId, itemId });
1355
+ return startPlanItem(context, { protocolId, projectRoot, itemId });
1214
1356
  }
1215
1357
  if (action === "done") {
1216
1358
  return completePlanItem(context, {
1217
1359
  protocolId,
1360
+ projectRoot,
1218
1361
  itemId,
1219
1362
  summary: requiredOption(parsed, "summary"),
1220
1363
  evidence: parsed.options.get("evidence") ?? []
@@ -1223,6 +1366,7 @@ function dispatchPlanItem(context, parsed) {
1223
1366
  if (action === "block") {
1224
1367
  return blockPlanItem(context, {
1225
1368
  protocolId,
1369
+ projectRoot,
1226
1370
  itemId,
1227
1371
  reason: requiredOption(parsed, "reason"),
1228
1372
  userRequired: parseBoolean(requiredOption(parsed, "user-required"), "user-required")
@@ -1231,12 +1375,46 @@ function dispatchPlanItem(context, parsed) {
1231
1375
  if (action === "skip") {
1232
1376
  return skipPlanItem(context, {
1233
1377
  protocolId,
1378
+ projectRoot,
1234
1379
  itemId,
1235
1380
  reason: requiredOption(parsed, "reason")
1236
1381
  });
1237
1382
  }
1238
1383
  throw new AppError("usage", `Unknown plan item action: ${action}`, 2);
1239
1384
  }
1385
+ function readOptionalJsonFile(file) {
1386
+ if (!file)
1387
+ return undefined;
1388
+ if (!fs.existsSync(file)) {
1389
+ throw new AppError("not_found", `JSON file does not exist: ${file}`, 1);
1390
+ }
1391
+ try {
1392
+ return JSON.parse(fs.readFileSync(file, "utf8"));
1393
+ }
1394
+ catch {
1395
+ throw new AppError("validation", `JSON file is malformed: ${file}`, 2);
1396
+ }
1397
+ }
1398
+ function parseFlowFlagOverrides(values) {
1399
+ const result = {};
1400
+ for (const item of values) {
1401
+ const separator = item.indexOf("=");
1402
+ if (separator <= 0) {
1403
+ throw new AppError("usage", "--flag must use key=value", 2, { value: item });
1404
+ }
1405
+ const key = item.slice(0, separator).trim();
1406
+ const raw = item.slice(separator + 1).trim();
1407
+ if (!key || !raw)
1408
+ throw new AppError("usage", "--flag must use non-empty key and value", 2, { value: item });
1409
+ try {
1410
+ result[key] = JSON.parse(raw);
1411
+ }
1412
+ catch {
1413
+ result[key] = raw;
1414
+ }
1415
+ }
1416
+ return result;
1417
+ }
1240
1418
  function parseArgs(args) {
1241
1419
  const positional = [];
1242
1420
  const options = new Map();
@@ -1281,6 +1459,22 @@ function requiredOption(parsed, key) {
1281
1459
  }
1282
1460
  return value;
1283
1461
  }
1462
+ function requiredScopeProjectRoot(root, operation) {
1463
+ if (root)
1464
+ return root;
1465
+ throw new AppError("usage", `${operation} requires --project-root or a unique protocol context`, 2);
1466
+ }
1467
+ function requiredOptionOrScope(parsed, root, key) {
1468
+ return root ?? requiredOption(parsed, key);
1469
+ }
1470
+ function requiredScopeProjectRootForOptional(root, command, parsed) {
1471
+ if (root)
1472
+ return root;
1473
+ if (["complete", "note", "fail", "cancel"].includes(command ?? "")) {
1474
+ return requiredScopeProjectRoot(root, `merge-queue ${command ?? ""}`);
1475
+ }
1476
+ return optionalOption(parsed, "project-root") ?? optionalOption(parsed, "root") ?? null;
1477
+ }
1284
1478
  function requiredOptionAlias(parsed, keys) {
1285
1479
  for (const key of keys) {
1286
1480
  const value = optionalOption(parsed, key);
@@ -1339,20 +1533,42 @@ async function readStdin(stream) {
1339
1533
  }
1340
1534
  return data;
1341
1535
  }
1342
- function refreshDashboardsAfterMutation(context, args, result) {
1343
- if (args[0] === "project" && args[1] === "archive") {
1344
- refreshGlobalDashboard(context, {});
1345
- return;
1536
+ function refreshDashboardsAfterMutation(context, args, result, resolvedScope) {
1537
+ try {
1538
+ if (args[0] === "project" && args[1] === "archive") {
1539
+ refreshGlobalDashboard(context, {});
1540
+ return [];
1541
+ }
1542
+ const projectRoot = projectRootForMutation(context, args, result, resolvedScope);
1543
+ if (!projectRoot)
1544
+ return [];
1545
+ autoRefreshDashboards(context, { projectRoot });
1546
+ return [];
1346
1547
  }
1347
- const projectRoot = projectRootForMutation(context, args, result);
1348
- if (!projectRoot) {
1349
- return;
1548
+ catch (error) {
1549
+ return [{
1550
+ code: "dashboard_refresh_failed",
1551
+ message: error instanceof Error ? error.message : String(error),
1552
+ project_root: resolvedScope
1553
+ }];
1554
+ }
1555
+ }
1556
+ function addWarnings(result, warnings) {
1557
+ if (warnings.length === 0)
1558
+ return result;
1559
+ if (result && typeof result === "object" && !Array.isArray(result)) {
1560
+ const record = result;
1561
+ const existing = Array.isArray(record.warnings) ? record.warnings : [];
1562
+ return { ...record, ok: record.ok === false ? record.ok : true, warnings: [...existing, ...warnings] };
1350
1563
  }
1351
- autoRefreshDashboards(context, { projectRoot });
1564
+ return { ok: true, result, warnings };
1352
1565
  }
1353
- function projectRootForMutation(context, args, result) {
1566
+ function projectRootForMutation(context, args, result, resolvedScope) {
1354
1567
  const [family, command, ...rest] = args;
1355
1568
  const parsed = parseArgs(rest);
1569
+ if (isScopedProtocolMutation(family, command)) {
1570
+ return resolvedScope ?? resolveOperationProjectRoot(context, args) ?? undefined;
1571
+ }
1356
1572
  if (family === "dashboard" || family === "integration") {
1357
1573
  return undefined;
1358
1574
  }
@@ -1381,6 +1597,9 @@ function projectRootForMutation(context, args, result) {
1381
1597
  if (family === "plan" && command === "item") {
1382
1598
  return protocolProjectRoot(context, requiredPosition(parsed, 1, "protocol-id"));
1383
1599
  }
1600
+ if (family === "prompt" && command === "render") {
1601
+ return requiredOption(parsed, "project-root");
1602
+ }
1384
1603
  if (family === "lane") {
1385
1604
  return projectRootForLaneMutation(command, parsed);
1386
1605
  }
@@ -1402,7 +1621,7 @@ function projectRootForMutation(context, args, result) {
1402
1621
  }
1403
1622
  return ["stop", "stop-worker"].includes(command ?? "") ? requiredOption(parsed, "project-root") : undefined;
1404
1623
  }
1405
- if (family === "run" && ["start", "attach-stage", "complete-stage", "complete"].includes(command ?? "")) {
1624
+ if (family === "run" && ["start", "attach-stage", "complete-stage", "complete", "flags"].includes(command ?? "")) {
1406
1625
  return requiredOption(parsed, "project-root");
1407
1626
  }
1408
1627
  if (family === "worktree" && ["create", "bootstrap", "close"].includes(command ?? "")) {
@@ -1446,6 +1665,19 @@ function projectRootForMergeQueueMutation(context, command, parsed) {
1446
1665
  function protocolProjectRoot(context, protocolId) {
1447
1666
  return requireProtocol(context, protocolId).project_root;
1448
1667
  }
1668
+ function isScopedProtocolMutation(family, command) {
1669
+ if (family === "transition")
1670
+ return true;
1671
+ if (family === "protocol")
1672
+ return ["transition", "sync-from-run", "ready-for-merge", "cancel"].includes(command ?? "");
1673
+ if (family === "plan")
1674
+ return command === "set" || command === "item";
1675
+ if (family === "worktree")
1676
+ return ["create", "bootstrap", "close"].includes(command ?? "");
1677
+ if (family === "merge-queue")
1678
+ return ["complete", "note", "fail", "cancel"].includes(command ?? "");
1679
+ return false;
1680
+ }
1449
1681
  function projectRootFromResult(result) {
1450
1682
  if (!result || typeof result !== "object") {
1451
1683
  return undefined;
@@ -1,12 +1,12 @@
1
1
  import path from "node:path";
2
2
  import { AppError } from "../shared/errors.js";
3
- const fullIdPattern = /^([A-Z]{2,4})-(\d{3})-([a-z0-9]+(?:-[a-z0-9]+)*)$/;
4
- const shortIdPattern = /^([A-Z]{2,4})-\d{3}$/;
3
+ const fullIdPattern = /^([A-Z]{2,4})-(\d{3,})-([a-z0-9]+(?:-[a-z0-9]+)*)$/;
4
+ const shortIdPattern = /^([A-Z]{2,4})-\d{3,}$/;
5
5
  export function parseFullEntityId(id) {
6
6
  const match = fullIdPattern.exec(id);
7
7
  if (!match?.[1] || !match[2] || !match[3]) {
8
8
  throw new AppError("validation", `Invalid typed entity id: ${id}`, 2, {
9
- expected: "TYPE-NNN-slug"
9
+ expected: "TYPE-<decimal-sequence>-slug (at least three digits)"
10
10
  });
11
11
  }
12
12
  return {
@@ -37,7 +37,7 @@ export function formatFullId(type, sequence, slug) {
37
37
  if (!/^[A-Z]{2,4}$/.test(type)) {
38
38
  throw new AppError("validation", `Invalid entity type: ${type}`, 2);
39
39
  }
40
- if (sequence < 1 || sequence > 999) {
40
+ if (!Number.isSafeInteger(sequence) || sequence < 1) {
41
41
  throw new AppError("validation", `Entity sequence is out of range: ${sequence}`, 2);
42
42
  }
43
43
  return `${type}-${String(sequence).padStart(3, "0")}-${slug}`;