@deksden-com/dd-flow-cli 0.6.0 → 0.8.0-beta.135

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 (78) hide show
  1. package/CHANGELOG.md +636 -0
  2. package/README.md +13 -1
  3. package/dist/build-info.json +10 -10
  4. package/dist/cli/help.js +108 -18
  5. package/dist/cli/run-cli.js +626 -49
  6. package/dist/domain/flow-contract.js +11 -0
  7. package/dist/domain/stage-catalog.js +22 -0
  8. package/dist/runtime/context.js +8 -2
  9. package/dist/schemas/code-review-decision.schema.json +26 -0
  10. package/dist/schemas/code-review-result.schema.json +14 -0
  11. package/dist/schemas/code-stage-report.schema.json +7 -2
  12. package/dist/schemas/code-verification.schema.json +14 -0
  13. package/dist/schemas/code-work-batch.schema.json +24 -0
  14. package/dist/schemas/code-work-result.schema.json +16 -0
  15. package/dist/schemas/engine-manifest.schema.json +22 -0
  16. package/dist/schemas/flow-contract.schema.json +6 -3
  17. package/dist/schemas/flow-run.schema.json +16 -122
  18. package/dist/schemas/mb-upgrade-migration-report.schema.json +3 -1
  19. package/dist/schemas/merge-stage-report-legacy-0.4.2.schema.json +24 -0
  20. package/dist/schemas/plan-aspect-map.schema.json +22 -0
  21. package/dist/schemas/plan-review-decision.schema.json +14 -0
  22. package/dist/schemas/plan-review-result.schema.json +42 -0
  23. package/dist/schemas/protocol-plan.schema.json +15 -182
  24. package/dist/schemas/run-engine-binding.schema.json +37 -0
  25. package/dist/schemas/stage-finish-input.schema.json +16 -2
  26. package/dist/schemas/stage-prompt.schema.json +4 -4
  27. package/dist/schemas/stage-report.schema.json +8 -7
  28. package/dist/schemas/vnext-protocol-plan.schema.json +37 -0
  29. package/dist/schemas/vnext-protocolize-result.schema.json +29 -0
  30. package/dist/schemas/vnext-specify.schema.json +45 -0
  31. package/dist/services/branch-context.js +1 -1
  32. package/dist/services/canon.js +15 -1
  33. package/dist/services/cleanup.js +8 -8
  34. package/dist/services/cli-operation-classifier.js +60 -8
  35. package/dist/services/code-checks.js +244 -0
  36. package/dist/services/compatibility-preflight.js +1 -1
  37. package/dist/services/config.js +7 -1
  38. package/dist/services/dashboard.js +14 -14
  39. package/dist/services/engines.js +408 -30
  40. package/dist/services/eval-snapshots.js +404 -0
  41. package/dist/services/hooks.js +775 -23
  42. package/dist/services/ids.js +16 -6
  43. package/dist/services/lanes.js +1 -5
  44. package/dist/services/merge-queue.js +53 -5
  45. package/dist/services/merge-worker.js +5 -6
  46. package/dist/services/migrations.js +307 -44
  47. package/dist/services/plan-runtime.js +5 -5
  48. package/dist/services/plans.js +5 -3
  49. package/dist/services/projects.js +4 -4
  50. package/dist/services/prompts.js +1 -1
  51. package/dist/services/protocols.js +31 -10
  52. package/dist/services/run-engine-bindings.js +157 -0
  53. package/dist/services/run-projection.js +49 -13
  54. package/dist/services/runs.js +525 -58
  55. package/dist/services/schema-validation.js +116 -2
  56. package/dist/services/sessions.js +51 -12
  57. package/dist/services/stage-blocker.js +57 -0
  58. package/dist/services/stage-context.js +90 -0
  59. package/dist/services/stage-lifecycle.js +288 -77
  60. package/dist/services/stage-pause.js +175 -0
  61. package/dist/services/stage-report-renderer.js +65 -0
  62. package/dist/services/status.js +8 -3
  63. package/dist/services/usage.js +526 -18
  64. package/dist/services/vnext-code-review.js +308 -0
  65. package/dist/services/vnext-code.js +616 -0
  66. package/dist/services/vnext-contracts.js +1 -0
  67. package/dist/services/vnext-execution-profile.js +27 -0
  68. package/dist/services/vnext-fanout.js +79 -0
  69. package/dist/services/vnext-plan-review.js +499 -0
  70. package/dist/services/vnext-plan.js +576 -0
  71. package/dist/services/vnext-protocolize.js +542 -0
  72. package/dist/services/vnext-specify.js +595 -0
  73. package/dist/services/vnext-workspace-policy.js +87 -0
  74. package/dist/services/work-registry.js +499 -0
  75. package/dist/services/worktrees.js +58 -37
  76. package/dist/storage/database.js +292 -42
  77. package/dist/storage/paths.js +47 -1
  78. package/package.json +12 -12
@@ -4,8 +4,9 @@ import { formatFullId, parseFullEntityId } from "../domain/entity-ids.js";
4
4
  import { AppError } from "../shared/errors.js";
5
5
  import { resolveProjectRoot } from "../storage/paths.js";
6
6
  const kindConfig = {
7
- protocol: { type: "PRT", table: "protocols", fileRoot: ".memory-bank/protocol" },
8
- run: { type: "RUN", table: "flow_runs" }
7
+ protocol: { type: "PRT", table: "protocols", idField: "id", fileRoot: ".memory-bank/protocol" },
8
+ run: { type: "RUN", table: "runs", idField: "id" },
9
+ work: { type: "WRK", table: "works", idField: "work_id" }
9
10
  };
10
11
  export function previewNextEntityId(context, input) {
11
12
  const projectRoot = resolveProjectRoot(input.projectRoot);
@@ -14,7 +15,7 @@ export function previewNextEntityId(context, input) {
14
15
  const config = kindConfig[kind];
15
16
  const used = new Set();
16
17
  const project = context.db.get("SELECT id FROM projects WHERE root = ?", [projectRoot]);
17
- for (const id of databaseIds(context, config.table, config.type, project?.id)) {
18
+ for (const id of databaseIds(context, config.table, config.idField, config.type, project?.id)) {
18
19
  addSequence(used, id, config.type);
19
20
  }
20
21
  if (config.fileRoot) {
@@ -39,19 +40,28 @@ export function previewNextEntityId(context, input) {
39
40
  }
40
41
  };
41
42
  }
43
+ /** Work ids are globally allocated because work_id is the database primary key. */
44
+ export function nextWorkId(context, _projectId, slug) {
45
+ const used = new Set();
46
+ for (const row of context.db.all("SELECT work_id AS id FROM works WHERE work_id LIKE ?", ["WRK-%"]))
47
+ addSequence(used, row.id, "WRK");
48
+ return formatFullId("WRK", Math.max(0, ...used) + 1, normalizeSlug(slug));
49
+ }
42
50
  function parseEntityKind(value) {
43
51
  const normalized = value.toLowerCase();
44
52
  if (normalized === "protocol" || normalized === "prt")
45
53
  return "protocol";
46
54
  if (normalized === "run")
47
55
  return "run";
48
- throw new AppError("validation", "--type must be protocol or run", 2, { type: value });
56
+ if (normalized === "work" || normalized === "wrk")
57
+ return "work";
58
+ throw new AppError("validation", "--type must be protocol, run, or work", 2, { type: value });
49
59
  }
50
- function databaseIds(context, table, type, projectId) {
60
+ function databaseIds(context, table, idField, type, projectId) {
51
61
  if (!projectId)
52
62
  return [];
53
63
  return context.db
54
- .all(`SELECT id FROM ${table} WHERE project_id = ? AND id LIKE ?`, [projectId, `${type}-%`])
64
+ .all(`SELECT ${idField} AS id FROM ${table} WHERE project_id = ? AND ${idField} LIKE ?`, [projectId, `${type}-%`])
55
65
  .map((row) => row.id);
56
66
  }
57
67
  function filesystemIds(projectRoot, relativeRoot, type) {
@@ -10,8 +10,6 @@ const defaultTtlSeconds = 300;
10
10
  const defaultPollIntervalSeconds = 10;
11
11
  export function getLaneStatus(context, input) {
12
12
  const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
13
- expireStaleLocks(context, project.id);
14
- expireStaleWaiters(context, project.id);
15
13
  return {
16
14
  ok: true,
17
15
  lanes: lanesForProject(context, project.id, input.lane),
@@ -179,8 +177,6 @@ export async function waitForLaneLock(context, input) {
179
177
  }
180
178
  export function getLaneWaiters(context, input) {
181
179
  const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
182
- expireStaleLocks(context, project.id);
183
- expireStaleWaiters(context, project.id);
184
180
  return {
185
181
  ok: true,
186
182
  lane: input.lane ? normalizeLane(input.lane) : null,
@@ -463,7 +459,7 @@ function assertWorkerMayAcquireLaneLock(context, projectId, lane, workerId) {
463
459
  if (lane !== "merge") {
464
460
  return;
465
461
  }
466
- const latest = context.db.get(`SELECT status, stop_reason, flow_kind FROM flow_sessions
462
+ const latest = context.db.get(`SELECT status, stop_reason, flow_kind FROM sessions
467
463
  WHERE project_id = ? AND worker_id = ? AND flow_kind IN ('merge_worker', 'merge_job')
468
464
  ORDER BY updated_at DESC, rowid DESC LIMIT 1`, [projectId, workerId]);
469
465
  if (latest && ["stopped", "stopping"].includes(latest.status)) {
@@ -1,4 +1,4 @@
1
- import { loadProjectFlowContract } from "../domain/flow-contract.js";
1
+ import { flowContractForState } from "../domain/flow-contract.js";
2
2
  import { AppError } from "../shared/errors.js";
3
3
  import { appendAudit } from "./audit.js";
4
4
  import { requireProjectByRoot } from "./projects.js";
@@ -73,6 +73,54 @@ export function claimNextMergeJob(context, input) {
73
73
  claimed: Boolean(claimedProtocolId)
74
74
  });
75
75
  }
76
+ export function claimMergeJob(context, input) {
77
+ const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
78
+ const stopState = stoppedMergeWorkerState(context, project.id, input.workerId);
79
+ if (stopState.stopped) {
80
+ throw new AppError("merge_worker_stopped", "Stopped or stopping merge worker cannot claim another merge job", 1, {
81
+ worker_id: input.workerId,
82
+ status: stopState.status,
83
+ reason: stopState.reason
84
+ });
85
+ }
86
+ requireLaneLockOwner(context, {
87
+ projectRoot: project.root,
88
+ lane: "merge",
89
+ workerId: input.workerId,
90
+ workspacePath: input.workspacePath
91
+ });
92
+ const now = context.now();
93
+ context.db.exec("BEGIN IMMEDIATE");
94
+ try {
95
+ const job = queueJobByProtocol(context, project.id, input.protocolId);
96
+ if (!job || !["ready", "requeued"].includes(job.status)) {
97
+ throw new AppError("merge_job_not_claimable", "Requested merge job is not ready to claim", 1, {
98
+ protocol_id: input.protocolId,
99
+ status: job?.status ?? "missing",
100
+ allowed: ["ready", "requeued"]
101
+ });
102
+ }
103
+ const update = context.db.run(`UPDATE merge_queue
104
+ SET status = 'claimed', claimed_by_session_id = ?, claimed_at = ?, updated_at = ?
105
+ WHERE id = ? AND status IN ('ready', 'requeued')`, [input.workerId, now, now, job.id]);
106
+ if (update.changes !== 1) {
107
+ throw new AppError("merge_job_claim_race", "Requested merge job changed while claiming", 1, { protocol_id: input.protocolId });
108
+ }
109
+ appendAudit(context, {
110
+ protocolId: job.protocol_id,
111
+ projectId: project.id,
112
+ eventType: "merge_queue.claimed",
113
+ payload: { protocol_id: job.protocol_id, worker_id: input.workerId, targeted: true }
114
+ });
115
+ transitionClaimedProtocolToIntegration(context, project.id, job.protocol_id, input.workerId, now);
116
+ context.db.exec("COMMIT");
117
+ return mergeQueueResult(queueJobByProtocol(context, project.id, job.protocol_id), { ok: true, outcome: "claimed", claimed: true, targeted: true });
118
+ }
119
+ catch (error) {
120
+ context.db.exec("ROLLBACK");
121
+ throw error;
122
+ }
123
+ }
76
124
  export function claimMergeBundle(context, input) {
77
125
  const { project, protocolIds } = requireClaimableBranchBundle(context, {
78
126
  projectRoot: input.projectRoot,
@@ -292,7 +340,7 @@ export function completeMergeJob(context, input) {
292
340
  SET status = 'merged', last_reason = ?, completed_at = ?, updated_at = ?
293
341
  WHERE id = ?`, [input.summary, now, now, job.id]);
294
342
  const state = readProtocolRuntimeState(context, protocol).state;
295
- const flowContract = loadProjectFlowContract(protocol.project_root);
343
+ const flowContract = flowContractForState(state);
296
344
  const targetStage = flowContract.merge_queue.complete.target_stage;
297
345
  const targetStatus = flowContract.stages[targetStage]?.terminal ? targetStage : "running";
298
346
  persistProtocolState(context, protocol, {
@@ -342,7 +390,7 @@ export function completeMergeBundle(context, input) {
342
390
  SET status = 'merged', last_reason = ?, completed_at = ?, updated_at = ?
343
391
  WHERE id = ?`, [input.summary, now, now, job.id]);
344
392
  const state = readProtocolRuntimeState(context, protocol).state;
345
- const flowContract = loadProjectFlowContract(protocol.project_root);
393
+ const flowContract = flowContractForState(state);
346
394
  const targetStage = flowContract.merge_queue.complete.target_stage;
347
395
  const targetStatus = flowContract.stages[targetStage]?.terminal ? targetStage : "running";
348
396
  persistProtocolState(context, protocol, {
@@ -643,7 +691,7 @@ function requireClaimedJob(context, projectId, protocolId, sessionId) {
643
691
  function transitionClaimedProtocolToIntegration(context, projectId, protocolId, workerId, now) {
644
692
  const protocol = requireProtocol(context, protocolId, projectId);
645
693
  const state = readProtocolRuntimeState(context, protocol).state;
646
- const flowContract = loadProjectFlowContract(protocol.project_root);
694
+ const flowContract = flowContractForState(state);
647
695
  if (!["ready_for_merge", "queued_for_merge"].includes(state.stage)) {
648
696
  throw new AppError("merge_protocol_not_ready", "Cannot claim a protocol whose runtime state is not ready for merge", 1, {
649
697
  protocol_id: protocolId,
@@ -685,7 +733,7 @@ function releaseLaneLockIfOwned(context, input) {
685
733
  }
686
734
  }
687
735
  function stopAfterCurrentIfRequested(context, projectId, projectRoot, workerId, reason) {
688
- const requested = context.db.get(`SELECT session_id FROM flow_sessions
736
+ const requested = context.db.get(`SELECT session_id FROM sessions
689
737
  WHERE project_id = ? AND worker_id = ? AND flow_kind = 'merge_worker'
690
738
  AND status = 'stopping' AND current_stage = 'stop_after_current'
691
739
  ORDER BY updated_at DESC, rowid DESC LIMIT 1`, [projectId, workerId]);
@@ -1,7 +1,7 @@
1
1
  import { AppError } from "../shared/errors.js";
2
2
  import { resolveProjectRoot } from "../storage/paths.js";
3
3
  import { appendAudit } from "./audit.js";
4
- import { ensureLaneWorkspace, acquireLaneLock, releaseLaneLock, expireProjectLaneLocks } from "./lanes.js";
4
+ import { ensureLaneWorkspace, acquireLaneLock, releaseLaneLock } from "./lanes.js";
5
5
  import { claimNextMergeJob, queueForProject } from "./merge-queue.js";
6
6
  import { requireProjectByRoot } from "./projects.js";
7
7
  import { buildStaticFlowGuidance } from "./flow-guidance.js";
@@ -72,7 +72,7 @@ export function stopProjectMergeWorker(context, input) {
72
72
  if (claimed.length > 0) {
73
73
  const now = context.now();
74
74
  for (const session of targetSessions) {
75
- context.db.run(`UPDATE flow_sessions
75
+ context.db.run(`UPDATE sessions
76
76
  SET status = 'stopping', stop_reason = ?, current_stage = ?, next_action = ?, updated_at = ?
77
77
  WHERE project_id = ? AND session_id = ?`, [input.reason, "stop_after_current", "finish_current_merge_job_then_stop", now, project.id, session.session_id]);
78
78
  }
@@ -175,15 +175,14 @@ function withJobGuidance(context, job) {
175
175
  }
176
176
  }
177
177
  function detectMergeWorkerState(context, projectId) {
178
- expireProjectLaneLocks(context, projectId);
179
178
  const workers = activeMergeWorkerSessions(context, projectId);
180
179
  const workerIds = [...new Set(workers.map((worker) => worker.worker_id).filter((value) => Boolean(value)))];
181
180
  const claimed = context.db.all(`SELECT protocol_id, claimed_by_session_id, status, updated_at FROM merge_queue
182
181
  WHERE project_id = ? AND status = 'claimed'
183
182
  ORDER BY updated_at DESC, id DESC`, [projectId]);
184
183
  const lock = context.db.get(`SELECT worker_id, status, expires_at, reason FROM lane_locks
185
- WHERE project_id = ? AND lane = 'merge' AND status = 'active'
186
- ORDER BY updated_at DESC, id DESC LIMIT 1`, [projectId]);
184
+ WHERE project_id = ? AND lane = 'merge' AND status = 'active' AND expires_at > ?
185
+ ORDER BY updated_at DESC, id DESC LIMIT 1`, [projectId, context.now()]);
187
186
  if (workerIds.length > 1) {
188
187
  return { state: "blocked", reason: "multiple_active_merge_workers", workers };
189
188
  }
@@ -199,7 +198,7 @@ function detectMergeWorkerState(context, projectId) {
199
198
  return { state: "clear" };
200
199
  }
201
200
  function activeMergeWorkerSessions(context, projectId) {
202
- return context.db.all(`SELECT session_id, worker_id, status, current_stage, next_action, updated_at FROM flow_sessions
201
+ return context.db.all(`SELECT session_id, worker_id, status, current_stage, next_action, updated_at FROM sessions
203
202
  WHERE project_id = ? AND flow_kind = 'merge_worker' AND status IN ('pending', 'active', 'waiting_user', 'blocked', 'stopping')
204
203
  ORDER BY updated_at DESC, rowid DESC`, [projectId]);
205
204
  }