@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
@@ -0,0 +1,157 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { AppError } from "../shared/errors.js";
4
+ export const runEngineBindingSchemaId = "dd-flow/run-engine-binding@1";
5
+ export function findRunHome(ddFlowHome, projectRoot, runIdOrAlias) {
6
+ const projectsRoot = path.join(ddFlowHome, "projects");
7
+ if (!fs.existsSync(projectsRoot))
8
+ return null;
9
+ const expectedRoot = realpathOrResolve(projectRoot);
10
+ const matches = [];
11
+ for (const projectId of fs.readdirSync(projectsRoot)) {
12
+ const runsRoot = path.join(projectsRoot, projectId, "runs");
13
+ if (!isDirectory(runsRoot))
14
+ continue;
15
+ for (const runId of fs.readdirSync(runsRoot)) {
16
+ if (!runIdMatches(runId, runIdOrAlias))
17
+ continue;
18
+ const runHome = path.join(runsRoot, runId);
19
+ const authorityPath = path.join(runHome, "run.json");
20
+ const bindingPath = path.join(runHome, "engine-binding.json");
21
+ const binding = readRunEngineBinding(bindingPath, { allowMissing: true });
22
+ const authorityRoot = binding?.project_root ?? projectRootFromAuthority(authorityPath);
23
+ if (!authorityRoot || realpathOrResolve(authorityRoot) !== expectedRoot)
24
+ continue;
25
+ matches.push({ project_id: projectId, run_id: runId, run_home: runHome, authority_path: authorityPath, binding_path: bindingPath });
26
+ }
27
+ }
28
+ if (matches.length > 1) {
29
+ throw new AppError("ambiguous_id", `RUN id is ambiguous for project: ${runIdOrAlias}`, 2, {
30
+ project_root: expectedRoot,
31
+ candidates: matches.map((match) => match.run_id)
32
+ });
33
+ }
34
+ return matches[0] ?? null;
35
+ }
36
+ export function readRunEngineBinding(file, options = {}) {
37
+ if (!fs.existsSync(file)) {
38
+ if (options.allowMissing)
39
+ return null;
40
+ throw new AppError("run_engine_binding_missing", "RUN engine binding is missing", 1, { path: file });
41
+ }
42
+ let value;
43
+ try {
44
+ value = JSON.parse(fs.readFileSync(file, "utf8"));
45
+ }
46
+ catch (error) {
47
+ throw new AppError("run_engine_binding_invalid", "RUN engine binding is not valid JSON", 2, { path: file, cause: String(error) });
48
+ }
49
+ if (!isRunEngineBinding(value)) {
50
+ throw new AppError("run_engine_binding_invalid", "RUN engine binding is invalid", 2, { path: file });
51
+ }
52
+ return value;
53
+ }
54
+ export function writeRunEngineBinding(file, binding) {
55
+ const existing = readRunEngineBinding(file, { allowMissing: true });
56
+ if (existing) {
57
+ if (sameEngine(existing.engine, binding.engine) && existing.run_id === binding.run_id && realpathOrResolve(existing.project_root) === realpathOrResolve(binding.project_root)) {
58
+ return { changed: false, binding: existing };
59
+ }
60
+ throw new AppError("run_engine_binding_immutable", "RUN engine binding cannot be changed", 1, {
61
+ path: file,
62
+ current: existing.engine,
63
+ requested: binding.engine
64
+ });
65
+ }
66
+ fs.mkdirSync(path.dirname(file), { recursive: true });
67
+ const temporary = `${file}.tmp-${process.pid}-${Date.now()}`;
68
+ fs.writeFileSync(temporary, `${JSON.stringify(binding, null, 2)}\n`);
69
+ fs.renameSync(temporary, file);
70
+ return { changed: true, binding };
71
+ }
72
+ export function allRunEngineBindings(ddFlowHome) {
73
+ const projectsRoot = path.join(ddFlowHome, "projects");
74
+ if (!fs.existsSync(projectsRoot))
75
+ return [];
76
+ const bindings = [];
77
+ for (const projectId of fs.readdirSync(projectsRoot)) {
78
+ const runsRoot = path.join(projectsRoot, projectId, "runs");
79
+ if (!isDirectory(runsRoot))
80
+ continue;
81
+ for (const runId of fs.readdirSync(runsRoot)) {
82
+ const binding = readRunEngineBinding(path.join(runsRoot, runId, "engine-binding.json"), { allowMissing: true });
83
+ if (binding)
84
+ bindings.push(binding);
85
+ }
86
+ }
87
+ return bindings;
88
+ }
89
+ function isRunEngineBinding(value) {
90
+ if (!value || typeof value !== "object" || Array.isArray(value))
91
+ return false;
92
+ const record = value;
93
+ const engine = record.engine;
94
+ const probe = record.probe;
95
+ return record.schema_id === runEngineBindingSchemaId
96
+ && typeof record.run_id === "string"
97
+ && typeof record.project_root === "string"
98
+ && typeof record.bound_at === "string"
99
+ && ["run_creation", "legacy_recovery"].includes(String(record.source))
100
+ && typeof record.reason === "string"
101
+ && Boolean(engine && typeof engine === "object" && !Array.isArray(engine)
102
+ && typeof engine.package_name === "string"
103
+ && typeof engine.package_version === "string"
104
+ && typeof engine.engine_version === "string"
105
+ && /^[a-f0-9]{64}$/.test(String(engine.integrity_checksum))
106
+ && typeof engine.snapshot_root === "string")
107
+ && Boolean(probe && typeof probe === "object" && !Array.isArray(probe)
108
+ && ["not_required", "passed"].includes(String(probe.status))
109
+ && (typeof probe.command === "string" || probe.command === null));
110
+ }
111
+ function sameEngine(left, right) {
112
+ return left.package_name === right.package_name
113
+ && left.package_version === right.package_version
114
+ && left.engine_version === right.engine_version
115
+ && left.integrity_checksum === right.integrity_checksum
116
+ && left.snapshot_root === right.snapshot_root;
117
+ }
118
+ function projectRootFromAuthority(file) {
119
+ try {
120
+ const value = JSON.parse(fs.readFileSync(file, "utf8"));
121
+ const execution = recordValue(value.execution);
122
+ const workspace = recordValue(value.workspace);
123
+ const project = recordValue(value.project);
124
+ return stringValue(execution?.project_root)
125
+ ?? stringValue(workspace?.project_root)
126
+ ?? stringValue(project?.root)
127
+ ?? stringValue(value.project_root);
128
+ }
129
+ catch {
130
+ return null;
131
+ }
132
+ }
133
+ function runIdMatches(fullId, idOrAlias) {
134
+ return fullId === idOrAlias || /^RUN-[0-9]+$/.test(idOrAlias) && fullId.startsWith(`${idOrAlias}-`);
135
+ }
136
+ function isDirectory(value) {
137
+ try {
138
+ return fs.statSync(value).isDirectory();
139
+ }
140
+ catch {
141
+ return false;
142
+ }
143
+ }
144
+ function realpathOrResolve(value) {
145
+ try {
146
+ return fs.realpathSync(value);
147
+ }
148
+ catch {
149
+ return path.resolve(value);
150
+ }
151
+ }
152
+ function recordValue(value) {
153
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
154
+ }
155
+ function stringValue(value) {
156
+ return typeof value === "string" && value.length > 0 ? value : null;
157
+ }
@@ -2,16 +2,23 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { reconcileSessionCoverageRows } from "../domain/session-coverage.js";
4
4
  export function refreshRunSessionProjection(context, projectId, runId) {
5
- const run = context.db.get("SELECT id, status, project_root, subject_type, subject_id, runtime_path, run_index_path, index_json FROM flow_runs WHERE project_id = ? AND id = ?", [projectId, runId]);
5
+ const run = context.db.get("SELECT id, flow_kind, status, project_root, subject_type, subject_id, runtime_path, run_index_path, index_json FROM runs WHERE project_id = ? AND id = ?", [projectId, runId]);
6
6
  if (!run)
7
7
  return;
8
8
  const index = JSON.parse(run.index_json);
9
- const sessionRows = context.db.all(`SELECT session_id, parent_session_id, role, session_kind, worker_id, current_stage, status,
9
+ const sessionRows = context.db.all(`SELECT session_id, harness, provider_session_id, parent_session_id, role, session_kind, worker_id, current_stage, status,
10
10
  created_at, updated_at, stopped_at, coverage_units_json
11
- FROM flow_sessions WHERE project_id = ? AND run_id = ? ORDER BY created_at, session_id`, [projectId, runId]);
11
+ FROM sessions WHERE project_id = ? AND run_id = ? ORDER BY created_at, session_id`, [projectId, runId]);
12
12
  const sessions = sessionRows.map(sessionProjection);
13
+ const workRows = context.db.all("SELECT work_id, parent_work_id, status, depends_on_json, launch_policy, started_at, completed_at FROM works WHERE project_id = ? AND run_id = ? ORDER BY created_at, work_id", [projectId, runId]);
14
+ const works = workRows.map((work) => ({ work_id: work.work_id, parent_work_id: work.parent_work_id, status: work.status, depends_on: parseStringArray(work.depends_on_json), launch_policy: work.launch_policy, started_at: work.started_at, completed_at: work.completed_at }));
15
+ const rootWorkId = workRows.find((work) => work.parent_work_id === null)?.work_id ?? null;
13
16
  const coverage = reconcileSessionCoverageRows(sessionRows);
14
- const sessionCoverage = {
17
+ const sessionCoverage = run.flow_kind === "vnext_specify" && coverage.expected_unit_ids.length === 0 ? {
18
+ status: "not_applicable",
19
+ expected: [], observed: [], missing: [],
20
+ diagnostics: ["worker_coverage_not_required"]
21
+ } : {
15
22
  status: coverage.status,
16
23
  expected: coverage.expected_unit_ids,
17
24
  observed: coverage.observed_unit_ids,
@@ -20,19 +27,35 @@ export function refreshRunSessionProjection(context, projectId, runId) {
20
27
  };
21
28
  const plan = run.subject_type === "protocol" ? planProjection(context, projectId, run.subject_id, run.project_root) : {};
22
29
  const workers = workerProjection(context, projectId, runId);
30
+ const vnext = run.flow_kind === "vnext_specify" || run.flow_kind === "vnext_protocolize";
31
+ const hadLegacyCoverage = vnext && ("session_coverage" in index || "usage_coverage" in index);
32
+ if (vnext) {
33
+ delete index.session_coverage;
34
+ delete index.usage_coverage;
35
+ delete index.workers;
36
+ delete index.current_stage;
37
+ delete index.attempts;
38
+ }
23
39
  if (JSON.stringify(index.sessions ?? []) === JSON.stringify(sessions)
24
- && JSON.stringify(index.session_coverage) === JSON.stringify(sessionCoverage)
40
+ && JSON.stringify(index.works ?? []) === JSON.stringify(works)
41
+ && index.root_work_id === rootWorkId
42
+ && (vnext || JSON.stringify(index.session_coverage) === JSON.stringify(sessionCoverage))
25
43
  && JSON.stringify(index.plan_ref) === JSON.stringify(plan.plan_ref)
26
44
  && JSON.stringify(index.plan_progress) === JSON.stringify(plan.plan_progress)
27
- && JSON.stringify(index.workers ?? {}) === JSON.stringify(workers))
45
+ && (vnext || JSON.stringify(index.workers ?? {}) === JSON.stringify(workers))
46
+ && !hadLegacyCoverage)
28
47
  return;
29
48
  index.sessions = sessions;
30
- index.session_coverage = sessionCoverage;
49
+ index.works = works;
50
+ index.root_work_id = rootWorkId;
51
+ if (!vnext)
52
+ index.session_coverage = sessionCoverage;
31
53
  if (plan.plan_ref)
32
54
  index.plan_ref = plan.plan_ref;
33
55
  if (plan.plan_progress)
34
56
  index.plan_progress = plan.plan_progress;
35
- index.workers = workers;
57
+ if (!vnext)
58
+ index.workers = workers;
36
59
  index.updated_at = context.now();
37
60
  const runtime = readJson(run.runtime_path);
38
61
  const runtimeRevision = typeof runtime?.runtime_revision === "number"
@@ -41,19 +64,30 @@ export function refreshRunSessionProjection(context, projectId, runId) {
41
64
  ? index.runtime_revision + 1
42
65
  : 1;
43
66
  index.runtime_revision = runtimeRevision;
44
- const authoritative = runtime ?? { ...index, schema_id: "dd-flow/flow-run@2" };
67
+ const authoritative = { ...(runtime ?? {}), ...index, schema_id: String(index.schema_id ?? runtime?.schema_id ?? "dd-flow/flow-run@3") };
45
68
  authoritative.sessions = sessions;
46
- authoritative.session_coverage = sessionCoverage;
69
+ authoritative.works = works;
70
+ authoritative.root_work_id = rootWorkId;
71
+ if (vnext) {
72
+ delete authoritative.session_coverage;
73
+ delete authoritative.usage_coverage;
74
+ delete authoritative.workers;
75
+ delete authoritative.current_stage;
76
+ delete authoritative.attempts;
77
+ }
78
+ else
79
+ authoritative.session_coverage = sessionCoverage;
47
80
  if (plan.plan_ref)
48
81
  authoritative.plan_ref = plan.plan_ref;
49
82
  if (plan.plan_progress)
50
83
  authoritative.plan_progress = plan.plan_progress;
51
- authoritative.workers = workers;
84
+ if (!vnext)
85
+ authoritative.workers = workers;
52
86
  authoritative.updated_at = index.updated_at;
53
87
  authoritative.runtime_revision = runtimeRevision;
54
88
  if (run.status !== "discarded" || fs.existsSync(run.runtime_path))
55
89
  writeJson(run.runtime_path, authoritative);
56
- context.db.run("UPDATE flow_runs SET index_json = ?, updated_at = ? WHERE project_id = ? AND id = ?", [JSON.stringify(index), index.updated_at, projectId, runId]);
90
+ context.db.run("UPDATE runs SET index_json = ?, updated_at = ? WHERE project_id = ? AND id = ?", [JSON.stringify(index), index.updated_at, projectId, runId]);
57
91
  }
58
92
  function planProjection(context, projectId, protocolId, projectRoot) {
59
93
  const binding = context.db.get("SELECT plan_path, plan_revision, plan_sha256 FROM plan_bindings WHERE project_id = ? AND protocol_id = ?", [projectId, protocolId]);
@@ -107,10 +141,12 @@ function parseStringArray(value) {
107
141
  function sessionProjection(row) {
108
142
  return {
109
143
  session_id: row.session_id,
144
+ harness: row.harness,
145
+ provider_session_id: row.provider_session_id,
110
146
  parent_session_id: row.parent_session_id,
111
147
  role: row.role,
112
148
  session_kind: row.session_kind,
113
- worker_id: row.worker_id,
149
+ work_id: row.worker_id,
114
150
  current_stage: row.current_stage,
115
151
  status: row.status,
116
152
  created_at: row.created_at,