@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.
- package/CHANGELOG.md +636 -0
- package/README.md +13 -1
- package/dist/build-info.json +10 -10
- package/dist/cli/help.js +108 -18
- package/dist/cli/run-cli.js +626 -49
- package/dist/domain/flow-contract.js +11 -0
- package/dist/domain/stage-catalog.js +22 -0
- package/dist/runtime/context.js +8 -2
- package/dist/schemas/code-review-decision.schema.json +26 -0
- package/dist/schemas/code-review-result.schema.json +14 -0
- package/dist/schemas/code-stage-report.schema.json +7 -2
- package/dist/schemas/code-verification.schema.json +14 -0
- package/dist/schemas/code-work-batch.schema.json +24 -0
- package/dist/schemas/code-work-result.schema.json +16 -0
- package/dist/schemas/engine-manifest.schema.json +22 -0
- package/dist/schemas/flow-contract.schema.json +6 -3
- package/dist/schemas/flow-run.schema.json +16 -122
- package/dist/schemas/mb-upgrade-migration-report.schema.json +3 -1
- package/dist/schemas/merge-stage-report-legacy-0.4.2.schema.json +24 -0
- package/dist/schemas/plan-aspect-map.schema.json +22 -0
- package/dist/schemas/plan-review-decision.schema.json +14 -0
- package/dist/schemas/plan-review-result.schema.json +42 -0
- package/dist/schemas/protocol-plan.schema.json +15 -182
- package/dist/schemas/run-engine-binding.schema.json +37 -0
- package/dist/schemas/stage-finish-input.schema.json +16 -2
- package/dist/schemas/stage-prompt.schema.json +4 -4
- package/dist/schemas/stage-report.schema.json +8 -7
- package/dist/schemas/vnext-protocol-plan.schema.json +37 -0
- package/dist/schemas/vnext-protocolize-result.schema.json +29 -0
- package/dist/schemas/vnext-specify.schema.json +45 -0
- package/dist/services/branch-context.js +1 -1
- package/dist/services/canon.js +15 -1
- package/dist/services/cleanup.js +8 -8
- package/dist/services/cli-operation-classifier.js +60 -8
- package/dist/services/code-checks.js +244 -0
- package/dist/services/compatibility-preflight.js +1 -1
- package/dist/services/config.js +7 -1
- package/dist/services/dashboard.js +14 -14
- package/dist/services/engines.js +408 -30
- package/dist/services/eval-snapshots.js +404 -0
- package/dist/services/hooks.js +775 -23
- package/dist/services/ids.js +16 -6
- package/dist/services/lanes.js +1 -5
- package/dist/services/merge-queue.js +53 -5
- package/dist/services/merge-worker.js +5 -6
- package/dist/services/migrations.js +307 -44
- package/dist/services/plan-runtime.js +5 -5
- package/dist/services/plans.js +5 -3
- package/dist/services/projects.js +4 -4
- package/dist/services/prompts.js +1 -1
- package/dist/services/protocols.js +31 -10
- package/dist/services/run-engine-bindings.js +157 -0
- package/dist/services/run-projection.js +49 -13
- package/dist/services/runs.js +525 -58
- package/dist/services/schema-validation.js +116 -2
- package/dist/services/sessions.js +51 -12
- package/dist/services/stage-blocker.js +57 -0
- package/dist/services/stage-context.js +90 -0
- package/dist/services/stage-lifecycle.js +288 -77
- package/dist/services/stage-pause.js +175 -0
- package/dist/services/stage-report-renderer.js +65 -0
- package/dist/services/status.js +8 -3
- package/dist/services/usage.js +526 -18
- package/dist/services/vnext-code-review.js +308 -0
- package/dist/services/vnext-code.js +616 -0
- package/dist/services/vnext-contracts.js +1 -0
- package/dist/services/vnext-execution-profile.js +27 -0
- package/dist/services/vnext-fanout.js +79 -0
- package/dist/services/vnext-plan-review.js +499 -0
- package/dist/services/vnext-plan.js +576 -0
- package/dist/services/vnext-protocolize.js +542 -0
- package/dist/services/vnext-specify.js +595 -0
- package/dist/services/vnext-workspace-policy.js +87 -0
- package/dist/services/work-registry.js +499 -0
- package/dist/services/worktrees.js +58 -37
- package/dist/storage/database.js +292 -42
- package/dist/storage/paths.js +47 -1
- 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
|
|
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
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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@
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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,
|