@deksden-com/dd-flow-cli 0.4.0 → 0.4.2

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 +22 -0
  2. package/README.md +25 -9
  3. package/dist/build-info.json +5 -5
  4. package/dist/cli/help.js +45 -29
  5. package/dist/cli/run-cli.js +229 -49
  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 +17 -5
  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 +3 -75
  24. package/dist/services/dashboard.js +48 -11
  25. package/dist/services/engines.js +124 -13
  26. package/dist/services/hooks.js +6 -6
  27. package/dist/services/ids.js +40 -49
  28. package/dist/services/merge-queue.js +33 -26
  29. package/dist/services/merge-worker.js +2 -1
  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 +77 -46
  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";
@@ -17,18 +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 { getFlowSessionStatus, registerFlowSession, stopFlowSession, stopMergeWorker, syncFlowSessionUsage } from "../services/sessions.js";
21
22
  import { getProtocolBranchContext } from "../services/branch-context.js";
22
- import { attachFlowRunStage, completeFlowRun, completeFlowRunStage, getFlowRunStatus, listFlowRuns, startFlowRun } from "../services/runs.js";
23
+ import { attachFlowRunStage, completeFlowRun, completeFlowRunStage, getFlowRunTimeline, getFlowRunUsage, getFlowRunFlagsStatus, getFlowRunStatus, listFlowRuns, reviseFlowRunFlags, startFlowRun } from "../services/runs.js";
23
24
  import { previewNextEntityId } from "../services/ids.js";
24
25
  import { validateSchema } from "../services/schema-validation.js";
25
26
  import { preflightMemoryPermissions } from "../services/memory-permissions.js";
26
- import { planMigration, verifyMigrationReport } from "../services/migrations.js";
27
+ import { assessMigrationImpact, planMigration, verifyMigrationReport } from "../services/migrations.js";
27
28
  import { publishProjectSummary } from "../services/project-summary.js";
28
29
  import { requireProjectByRoot } from "../services/projects.js";
29
30
  import { resolveProjectRoot } from "../storage/paths.js";
30
- import { doctorEngines, engineInfo, installCurrentEngine, listEngines, resolveEngine, routeArgsThroughEngine } from "../services/engines.js";
31
+ import { doctorEngines, engineInfo, installCurrentEngine, listEngines, resolveEngine, resolveOperationProjectRoot, routeArgsThroughEngine } from "../services/engines.js";
31
32
  import { preflightCliCompatibility } from "../services/compatibility-preflight.js";
33
+ import { renderWorkerPrompt } from "../services/prompts.js";
32
34
  const defaultIo = {
33
35
  stdout: process.stdout,
34
36
  stderr: process.stderr,
@@ -57,7 +59,8 @@ export async function runCli(args, io = defaultIo, env = process.env) {
57
59
  }
58
60
  const context = createContext(env);
59
61
  emitHumanProgress(io, output, progress, "start");
60
- 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);
61
64
  if (routed?.routed && !routed.inProcess) {
62
65
  emitHumanProgress(io, output, progress, "done");
63
66
  return routed.exitCode;
@@ -65,11 +68,12 @@ export async function runCli(args, io = defaultIo, env = process.env) {
65
68
  const dispatchContext = routed?.inProcess
66
69
  ? createContext({ ...env, DD_FLOW_ENGINE_MODE: "1", DD_FLOW_ENGINE_HOME: "in-process", DD_FLOW_ROUTED_FROM: getCliVersionReport().cli.version })
67
70
  : context;
68
- preflightCliCompatibility(dispatchContext, output.args, env);
69
- const result = await dispatch(output.args, dispatchContext, io);
70
- 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);
71
75
  emitHumanProgress(io, output, progress, "done");
72
- const payload = attachProgress(result, progress, output);
76
+ const payload = attachProgress(resultWithWarnings, progress, output);
73
77
  if (output.json)
74
78
  writeJson(io.stdout, payload);
75
79
  else
@@ -374,7 +378,7 @@ function stringValue(value) {
374
378
  function shortSha(value) {
375
379
  return value ? value.slice(0, 12) : "unknown";
376
380
  }
377
- async function dispatch(args, context, io) {
381
+ async function dispatch(args, context, io, scopeProjectRoot = null) {
378
382
  const [family, command, ...rest] = args;
379
383
  const parsed = parseArgs(rest);
380
384
  if (family === "engine") {
@@ -433,13 +437,14 @@ async function dispatch(args, context, io) {
433
437
  });
434
438
  }
435
439
  if (family === "protocol" && command === "status") {
436
- 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") });
437
442
  }
438
443
  if (family === "protocol" && command === "branch-status") {
439
444
  const protocolId = parsed.positional[0];
440
445
  return getProtocolBranchContext(context, {
441
446
  ...(protocolId ? { protocolId } : {}),
442
- projectRoot: optionalOption(parsed, "project-root"),
447
+ projectRoot: scopeProjectRoot ?? optionalOption(parsed, "project-root"),
443
448
  workspacePath: optionalOption(parsed, "path")
444
449
  });
445
450
  }
@@ -453,21 +458,25 @@ async function dispatch(args, context, io) {
453
458
  });
454
459
  }
455
460
  if (family === "protocol" && command === "implement") {
461
+ const protocolId = requiredPosition(parsed, 0, "protocol-id");
456
462
  const reason = optionalOption(parsed, "reason");
457
463
  return implementProtocol(context, {
458
- protocolId: requiredPosition(parsed, 0, "protocol-id"),
459
- projectRoot: requiredOption(parsed, "project-root"),
464
+ protocolId,
465
+ projectRoot: requiredScopeProjectRoot(scopeProjectRoot, "protocol implement"),
460
466
  force: hasOption(parsed, "force"),
461
467
  ...(reason ? { reason } : {})
462
468
  });
463
469
  }
464
470
  if (family === "protocol" && command === "ready-for-merge") {
465
- 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") });
466
473
  }
467
474
  if (family === "protocol" && command === "transition") {
475
+ const protocolId = requiredPosition(parsed, 0, "protocol-id");
468
476
  const reason = optionalOption(parsed, "reason");
469
477
  return transitionProtocol(context, {
470
- protocolId: requiredPosition(parsed, 0, "protocol-id"),
478
+ protocolId,
479
+ projectRoot: requiredScopeProjectRoot(scopeProjectRoot, "protocol transition"),
471
480
  to: requiredOption(parsed, "to"),
472
481
  jsonFile: requiredOptionAlias(parsed, ["payload-file", "json-file"]),
473
482
  force: hasOption(parsed, "force"),
@@ -475,15 +484,19 @@ async function dispatch(args, context, io) {
475
484
  });
476
485
  }
477
486
  if (family === "protocol" && command === "sync-from-run") {
487
+ const protocolId = requiredPosition(parsed, 0, "protocol-id");
478
488
  return syncProtocolFromRun(context, {
479
- protocolId: requiredPosition(parsed, 0, "protocol-id"),
489
+ protocolId,
490
+ projectRoot: requiredScopeProjectRoot(scopeProjectRoot, "protocol sync-from-run"),
480
491
  runId: requiredOption(parsed, "run"),
481
492
  target: optionalOption(parsed, "target") ?? "auto"
482
493
  });
483
494
  }
484
495
  if (family === "protocol" && command === "cancel") {
496
+ const protocolId = requiredPosition(parsed, 0, "protocol-id");
485
497
  return cancelProtocol(context, {
486
- protocolId: requiredPosition(parsed, 0, "protocol-id"),
498
+ protocolId,
499
+ projectRoot: requiredScopeProjectRoot(scopeProjectRoot, "protocol cancel"),
487
500
  reason: requiredOption(parsed, "reason"),
488
501
  closeSessions: parseOptionalBoolean(optionalOption(parsed, "close-sessions"), "close-sessions") ?? false,
489
502
  cancelQueue: parseOptionalBoolean(optionalOption(parsed, "cancel-queue"), "cancel-queue") ?? false,
@@ -494,9 +507,11 @@ async function dispatch(args, context, io) {
494
507
  }
495
508
  if (family === "transition") {
496
509
  const transitionParsed = parseArgs([command ?? "", ...rest]);
510
+ const protocolId = requiredPosition(transitionParsed, 0, "protocol-id");
497
511
  const reason = optionalOption(transitionParsed, "reason");
498
512
  return transitionProtocol(context, {
499
- protocolId: requiredPosition(transitionParsed, 0, "protocol-id"),
513
+ protocolId,
514
+ projectRoot: requiredScopeProjectRoot(scopeProjectRoot, "transition"),
500
515
  to: requiredOption(transitionParsed, "to"),
501
516
  jsonFile: requiredOptionAlias(transitionParsed, ["payload-file", "json-file"]),
502
517
  force: hasOption(transitionParsed, "force"),
@@ -504,22 +519,37 @@ async function dispatch(args, context, io) {
504
519
  });
505
520
  }
506
521
  if (family === "plan" && command === "set") {
522
+ const protocolId = requiredPosition(parsed, 0, "protocol-id");
507
523
  return setProtocolPlan(context, {
508
- protocolId: requiredPosition(parsed, 0, "protocol-id"),
524
+ protocolId,
525
+ projectRoot: requiredScopeProjectRoot(scopeProjectRoot, "plan set"),
509
526
  file: requiredOption(parsed, "file")
510
527
  });
511
528
  }
512
529
  if (family === "plan" && command === "status") {
513
- 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") });
514
532
  }
515
533
  if (family === "plan" && command === "item") {
516
- 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
+ });
517
547
  }
518
548
  if (family === "lane") {
519
549
  return dispatchLane(context, command, parsed);
520
550
  }
521
551
  if (family === "merge-queue") {
522
- return dispatchMergeQueue(context, command, parsed);
552
+ return dispatchMergeQueue(context, command, parsed, requiredScopeProjectRootForOptional(scopeProjectRoot, command, parsed));
523
553
  }
524
554
  if (family === "merge") {
525
555
  return dispatchMerge(context, command, parsed);
@@ -564,7 +594,7 @@ async function dispatch(args, context, io) {
564
594
  return dispatchCodexHook(context, parsed, await readStdin(io.stdin));
565
595
  }
566
596
  if (family === "worktree") {
567
- return dispatchWorktree(context, command, parsed);
597
+ return dispatchWorktree(context, command, parsed, requiredScopeProjectRoot(scopeProjectRoot, `worktree ${command ?? ""}`));
568
598
  }
569
599
  throw new AppError("usage", `Unknown command: ${args.join(" ") || "<empty>"}`, 2);
570
600
  }
@@ -587,6 +617,8 @@ function dispatchEngine(context, command, parsed) {
587
617
  throw new AppError("usage", `Unknown engine command: ${command ?? "<empty>"}`, 2);
588
618
  }
589
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") } : {}) });
590
622
  if (command === "plan" || command === "report") {
591
623
  return planMigration(context, {
592
624
  projectRoot: requiredOption(parsed, "project-root"),
@@ -615,23 +647,23 @@ function dispatchCanon(context, command, parsed) {
615
647
  }
616
648
  throw new AppError("usage", `Unknown canon command: ${command ?? "<empty>"}`, 2);
617
649
  }
618
- function dispatchMergeQueue(context, command, parsed) {
650
+ function dispatchMergeQueue(context, command, parsed, scopeProjectRoot) {
619
651
  if (hasOption(parsed, "session-id")) {
620
652
  throw new AppError("usage", "--session-id is no longer accepted on merge-queue commands; use --worker-id", 2);
621
653
  }
622
654
  if (command === "status") {
623
- return getMergeQueueStatus(context, { projectRoot: requiredOption(parsed, "project-root") });
655
+ return getMergeQueueStatus(context, { projectRoot: requiredOptionOrScope(parsed, scopeProjectRoot, "project-root") });
624
656
  }
625
657
  if (command === "next") {
626
658
  return claimNextMergeJob(context, {
627
- projectRoot: requiredOption(parsed, "project-root"),
659
+ projectRoot: requiredOptionOrScope(parsed, scopeProjectRoot, "project-root"),
628
660
  workerId: requiredWorkerId(parsed),
629
661
  workspacePath: workspacePathOption(parsed)
630
662
  });
631
663
  }
632
664
  if (command === "wait-next") {
633
665
  return waitNextMergeJob(context, {
634
- projectRoot: requiredOption(parsed, "project-root"),
666
+ projectRoot: requiredOptionOrScope(parsed, scopeProjectRoot, "project-root"),
635
667
  workerId: requiredWorkerId(parsed),
636
668
  timeoutSeconds: optionalNumber(parsed, "timeout"),
637
669
  pollIntervalSeconds: optionalNumber(parsed, "poll-interval"),
@@ -640,16 +672,20 @@ function dispatchMergeQueue(context, command, parsed) {
640
672
  });
641
673
  }
642
674
  if (command === "complete") {
675
+ const protocolId = requiredPosition(parsed, 0, "protocol-id");
643
676
  return completeMergeJob(context, {
644
- protocolId: requiredPosition(parsed, 0, "protocol-id"),
677
+ protocolId,
678
+ projectRoot: requiredScopeProjectRoot(scopeProjectRoot, "merge-queue complete"),
645
679
  workerId: requiredWorkerId(parsed),
646
680
  workspacePath: workspacePathOption(parsed),
647
681
  summary: requiredOption(parsed, "summary")
648
682
  });
649
683
  }
650
684
  if (command === "fail") {
685
+ const protocolId = requiredPosition(parsed, 0, "protocol-id");
651
686
  return failMergeJob(context, {
652
- protocolId: requiredPosition(parsed, 0, "protocol-id"),
687
+ protocolId,
688
+ projectRoot: requiredScopeProjectRoot(scopeProjectRoot, "merge-queue fail"),
653
689
  workerId: requiredWorkerId(parsed),
654
690
  workspacePath: workspacePathOption(parsed),
655
691
  reason: requiredOption(parsed, "reason"),
@@ -657,15 +693,19 @@ function dispatchMergeQueue(context, command, parsed) {
657
693
  });
658
694
  }
659
695
  if (command === "note") {
696
+ const protocolId = requiredPosition(parsed, 0, "protocol-id");
660
697
  return noteMergeJob(context, {
661
- protocolId: requiredPosition(parsed, 0, "protocol-id"),
698
+ protocolId,
699
+ projectRoot: requiredScopeProjectRoot(scopeProjectRoot, "merge-queue note"),
662
700
  workerId: requiredWorkerId(parsed),
663
701
  summary: requiredOption(parsed, "summary")
664
702
  });
665
703
  }
666
704
  if (command === "cancel") {
705
+ const protocolId = requiredPosition(parsed, 0, "protocol-id");
667
706
  return cancelMergeQueueJob(context, {
668
- protocolId: requiredPosition(parsed, 0, "protocol-id"),
707
+ protocolId,
708
+ projectRoot: requiredScopeProjectRoot(scopeProjectRoot, "merge-queue cancel"),
669
709
  reason: requiredOption(parsed, "reason"),
670
710
  workerId: optionalOption(parsed, "worker-id"),
671
711
  workspacePath: optionalOption(parsed, "path"),
@@ -816,10 +856,17 @@ function dispatchSession(context, command, parsed) {
816
856
  reason: requiredOption(parsed, "reason")
817
857
  });
818
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
+ }
819
865
  throw new AppError("usage", `Unknown session command: ${command ?? "<empty>"}`, 2);
820
866
  }
821
867
  function dispatchRun(context, command, parsed) {
822
868
  if (command === "start") {
869
+ const protocolOverrides = readOptionalJsonFile(optionalOption(parsed, "protocol-override-file"));
823
870
  return startFlowRun(context, {
824
871
  projectRoot: requiredOption(parsed, "project-root"),
825
872
  workspaceRoot: optionalOption(parsed, "workspace-root"),
@@ -827,7 +874,11 @@ function dispatchRun(context, command, parsed) {
827
874
  subjectType: requiredOption(parsed, "subject-type"),
828
875
  subjectId: requiredOption(parsed, "subject-id"),
829
876
  slug: requiredOption(parsed, "slug"),
830
- 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"))
831
882
  });
832
883
  }
833
884
  if (command === "status") {
@@ -839,6 +890,43 @@ function dispatchRun(context, command, parsed) {
839
890
  if (command === "list") {
840
891
  return listFlowRuns(context, { projectRoot: requiredOption(parsed, "project-root") });
841
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
+ }
842
930
  if (command === "attach-stage") {
843
931
  return attachFlowRunStage(context, {
844
932
  projectRoot: requiredOption(parsed, "project-root"),
@@ -1231,42 +1319,45 @@ function dispatchCodexHook(context, parsed, stdin) {
1231
1319
  }
1232
1320
  throw new AppError("usage", `Unknown codex hook action: ${action}`, 2);
1233
1321
  }
1234
- function dispatchWorktree(context, command, parsed) {
1322
+ function dispatchWorktree(context, command, parsed, scopeProjectRoot) {
1235
1323
  if (command === "plan") {
1236
- return planWorktree(context, { protocolId: requiredOption(parsed, "protocol-id") });
1324
+ return planWorktree(context, { protocolId: requiredOption(parsed, "protocol-id"), projectRoot: scopeProjectRoot });
1237
1325
  }
1238
1326
  if (command === "create") {
1239
1327
  return createWorktreeRecord(context, {
1240
1328
  protocolId: requiredOption(parsed, "protocol-id"),
1329
+ projectRoot: scopeProjectRoot,
1241
1330
  branch: requiredOption(parsed, "branch"),
1242
1331
  base: requiredOption(parsed, "base"),
1243
1332
  path: requiredOption(parsed, "path")
1244
1333
  });
1245
1334
  }
1246
1335
  if (command === "status") {
1247
- return getWorktreeStatus(context, { protocolId: requiredOption(parsed, "protocol-id") });
1336
+ return getWorktreeStatus(context, { protocolId: requiredOption(parsed, "protocol-id"), projectRoot: scopeProjectRoot });
1248
1337
  }
1249
1338
  if (command === "bootstrap") {
1250
- return bootstrapWorktree(context, { protocolId: requiredOption(parsed, "protocol-id") });
1339
+ return bootstrapWorktree(context, { protocolId: requiredOption(parsed, "protocol-id"), projectRoot: scopeProjectRoot });
1251
1340
  }
1252
1341
  if (command === "close") {
1253
1342
  return closeWorktree(context, {
1254
1343
  protocolId: requiredOption(parsed, "protocol-id"),
1344
+ projectRoot: scopeProjectRoot,
1255
1345
  mode: requiredOption(parsed, "mode")
1256
1346
  });
1257
1347
  }
1258
1348
  throw new AppError("usage", `Unknown worktree command: ${command ?? "<empty>"}`, 2);
1259
1349
  }
1260
- function dispatchPlanItem(context, parsed) {
1350
+ function dispatchPlanItem(context, parsed, projectRoot) {
1261
1351
  const action = requiredPosition(parsed, 0, "plan item action");
1262
1352
  const protocolId = requiredPosition(parsed, 1, "protocol-id");
1263
1353
  const itemId = requiredPosition(parsed, 2, "item-id");
1264
1354
  if (action === "start") {
1265
- return startPlanItem(context, { protocolId, itemId });
1355
+ return startPlanItem(context, { protocolId, projectRoot, itemId });
1266
1356
  }
1267
1357
  if (action === "done") {
1268
1358
  return completePlanItem(context, {
1269
1359
  protocolId,
1360
+ projectRoot,
1270
1361
  itemId,
1271
1362
  summary: requiredOption(parsed, "summary"),
1272
1363
  evidence: parsed.options.get("evidence") ?? []
@@ -1275,6 +1366,7 @@ function dispatchPlanItem(context, parsed) {
1275
1366
  if (action === "block") {
1276
1367
  return blockPlanItem(context, {
1277
1368
  protocolId,
1369
+ projectRoot,
1278
1370
  itemId,
1279
1371
  reason: requiredOption(parsed, "reason"),
1280
1372
  userRequired: parseBoolean(requiredOption(parsed, "user-required"), "user-required")
@@ -1283,12 +1375,46 @@ function dispatchPlanItem(context, parsed) {
1283
1375
  if (action === "skip") {
1284
1376
  return skipPlanItem(context, {
1285
1377
  protocolId,
1378
+ projectRoot,
1286
1379
  itemId,
1287
1380
  reason: requiredOption(parsed, "reason")
1288
1381
  });
1289
1382
  }
1290
1383
  throw new AppError("usage", `Unknown plan item action: ${action}`, 2);
1291
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
+ }
1292
1418
  function parseArgs(args) {
1293
1419
  const positional = [];
1294
1420
  const options = new Map();
@@ -1333,6 +1459,22 @@ function requiredOption(parsed, key) {
1333
1459
  }
1334
1460
  return value;
1335
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
+ }
1336
1478
  function requiredOptionAlias(parsed, keys) {
1337
1479
  for (const key of keys) {
1338
1480
  const value = optionalOption(parsed, key);
@@ -1391,20 +1533,42 @@ async function readStdin(stream) {
1391
1533
  }
1392
1534
  return data;
1393
1535
  }
1394
- function refreshDashboardsAfterMutation(context, args, result) {
1395
- if (args[0] === "project" && args[1] === "archive") {
1396
- refreshGlobalDashboard(context, {});
1397
- 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 [];
1398
1547
  }
1399
- const projectRoot = projectRootForMutation(context, args, result);
1400
- if (!projectRoot) {
1401
- 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
+ }];
1402
1554
  }
1403
- autoRefreshDashboards(context, { projectRoot });
1404
1555
  }
1405
- function projectRootForMutation(context, args, result) {
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] };
1563
+ }
1564
+ return { ok: true, result, warnings };
1565
+ }
1566
+ function projectRootForMutation(context, args, result, resolvedScope) {
1406
1567
  const [family, command, ...rest] = args;
1407
1568
  const parsed = parseArgs(rest);
1569
+ if (isScopedProtocolMutation(family, command)) {
1570
+ return resolvedScope ?? resolveOperationProjectRoot(context, args) ?? undefined;
1571
+ }
1408
1572
  if (family === "dashboard" || family === "integration") {
1409
1573
  return undefined;
1410
1574
  }
@@ -1433,6 +1597,9 @@ function projectRootForMutation(context, args, result) {
1433
1597
  if (family === "plan" && command === "item") {
1434
1598
  return protocolProjectRoot(context, requiredPosition(parsed, 1, "protocol-id"));
1435
1599
  }
1600
+ if (family === "prompt" && command === "render") {
1601
+ return requiredOption(parsed, "project-root");
1602
+ }
1436
1603
  if (family === "lane") {
1437
1604
  return projectRootForLaneMutation(command, parsed);
1438
1605
  }
@@ -1454,7 +1621,7 @@ function projectRootForMutation(context, args, result) {
1454
1621
  }
1455
1622
  return ["stop", "stop-worker"].includes(command ?? "") ? requiredOption(parsed, "project-root") : undefined;
1456
1623
  }
1457
- if (family === "run" && ["start", "attach-stage", "complete-stage", "complete"].includes(command ?? "")) {
1624
+ if (family === "run" && ["start", "attach-stage", "complete-stage", "complete", "flags"].includes(command ?? "")) {
1458
1625
  return requiredOption(parsed, "project-root");
1459
1626
  }
1460
1627
  if (family === "worktree" && ["create", "bootstrap", "close"].includes(command ?? "")) {
@@ -1498,6 +1665,19 @@ function projectRootForMergeQueueMutation(context, command, parsed) {
1498
1665
  function protocolProjectRoot(context, protocolId) {
1499
1666
  return requireProtocol(context, protocolId).project_root;
1500
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
+ }
1501
1681
  function projectRootFromResult(result) {
1502
1682
  if (!result || typeof result !== "object") {
1503
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}`;