@deksden-com/dd-flow-cli 0.6.0 → 0.7.0
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 +6 -0
- package/README.md +8 -1
- package/dist/build-info.json +5 -5
- package/dist/cli/help.js +20 -8
- package/dist/cli/run-cli.js +104 -24
- package/dist/domain/flow-contract.js +11 -0
- package/dist/runtime/context.js +8 -2
- package/dist/schemas/code-stage-report.schema.json +7 -2
- package/dist/schemas/engine-manifest.schema.json +22 -0
- package/dist/schemas/flow-run.schema.json +1 -0
- 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/run-engine-binding.schema.json +37 -0
- package/dist/schemas/stage-prompt.schema.json +4 -4
- package/dist/services/canon.js +15 -1
- package/dist/services/cli-operation-classifier.js +52 -8
- package/dist/services/compatibility-preflight.js +1 -1
- package/dist/services/dashboard.js +2 -2
- package/dist/services/engines.js +408 -30
- package/dist/services/hooks.js +1 -5
- package/dist/services/lanes.js +0 -4
- package/dist/services/merge-queue.js +48 -0
- package/dist/services/merge-worker.js +3 -4
- package/dist/services/migrations.js +307 -44
- package/dist/services/plan-runtime.js +4 -4
- package/dist/services/plans.js +5 -3
- package/dist/services/protocols.js +23 -2
- package/dist/services/run-engine-bindings.js +157 -0
- package/dist/services/runs.js +21 -7
- package/dist/services/schema-validation.js +96 -0
- package/dist/services/stage-lifecycle.js +98 -10
- package/dist/services/status.js +8 -3
- package/dist/storage/database.js +32 -11
- package/package.json +1 -1
|
@@ -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
|
+
}
|
package/dist/services/runs.js
CHANGED
|
@@ -3,7 +3,7 @@ import fs from "node:fs";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { spawnSync } from "node:child_process";
|
|
5
5
|
import { formatFullId, isFullEntityId, isShortEntityId, parseFullEntityId } from "../domain/entity-ids.js";
|
|
6
|
-
import { checksumForFlowFlagValues, isFlowFlagDowngrade, loadProjectFlowContract, resolveFlowFlags } from "../domain/flow-contract.js";
|
|
6
|
+
import { checksumForFlowFlagValues, isFlowFlagDowngrade, loadCanonicalFlowContract, loadProjectFlowContract, resolveFlowFlags } from "../domain/flow-contract.js";
|
|
7
7
|
import { AppError } from "../shared/errors.js";
|
|
8
8
|
import { ensureDir, projectRunHome, projectRunJsonPath, resolveProjectRoot } from "../storage/paths.js";
|
|
9
9
|
import { appendAudit } from "./audit.js";
|
|
@@ -12,6 +12,8 @@ import { buildRunFlowGuidance } from "./flow-guidance.js";
|
|
|
12
12
|
import { readProtocolRuntimeState, requireProtocol } from "./protocols.js";
|
|
13
13
|
import { checkpointRunUsage, usageForRun } from "./usage.js";
|
|
14
14
|
import { refreshRunSessionProjection } from "./run-projection.js";
|
|
15
|
+
import { resolveCanonRoot } from "./canon.js";
|
|
16
|
+
import { bindCurrentEngineToRun } from "./engines.js";
|
|
15
17
|
const runSchemaId = "dd-flow/flow-run@2";
|
|
16
18
|
const runtimeSchemaId = "dd-flow/flow-run@2";
|
|
17
19
|
const runIdType = "RUN";
|
|
@@ -30,7 +32,9 @@ const allowedRunFlowKinds = [
|
|
|
30
32
|
];
|
|
31
33
|
export function startFlowRun(context, input) {
|
|
32
34
|
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
33
|
-
const flowContract =
|
|
35
|
+
const flowContract = input.flowKind === "mb-upgrade"
|
|
36
|
+
? loadUpgradeFlowContract(context, projectRoot)
|
|
37
|
+
: loadProjectFlowContract(projectRoot);
|
|
34
38
|
registerProject(context, { root: projectRoot });
|
|
35
39
|
const project = requireProjectByRoot(context, projectRoot);
|
|
36
40
|
const workspaceRoot = resolveWorkspaceRoot(input.workspaceRoot ?? projectRoot);
|
|
@@ -107,6 +111,7 @@ export function startFlowRun(context, input) {
|
|
|
107
111
|
};
|
|
108
112
|
ensureDir(path.dirname(runtimePath));
|
|
109
113
|
ensureDir(runHome);
|
|
114
|
+
bindCurrentEngineToRun(context, { projectRoot, runId, runHome });
|
|
110
115
|
writeJsonFile(runtimePath, runtimeSnapshotForIndex(index, 1));
|
|
111
116
|
context.db.run(`INSERT INTO flow_runs
|
|
112
117
|
(id, short_id, slug, project_id, project_root, workspace_root, flow_kind, subject_type, subject_id,
|
|
@@ -151,14 +156,23 @@ export function startFlowRun(context, input) {
|
|
|
151
156
|
});
|
|
152
157
|
return { ok: true, run: flowRunSummary(requireRunById(context, project.id, runId)), index };
|
|
153
158
|
}
|
|
159
|
+
function loadUpgradeFlowContract(context, projectRoot) {
|
|
160
|
+
const canon = resolveCanonRoot(context, context.env.DD_MEMORYBANK ? { explicitRoot: context.env.DD_MEMORYBANK } : {});
|
|
161
|
+
if (!canon.ok || !canon.canon) {
|
|
162
|
+
throw new AppError("canon_unavailable", "mb-upgrade RUN requires the pinned canonical Memory Bank", 1, {
|
|
163
|
+
project_root: projectRoot,
|
|
164
|
+
blockers: canon.blockers,
|
|
165
|
+
bootstrap: canon.bootstrap
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
return loadCanonicalFlowContract(canon.canon.flow_root);
|
|
169
|
+
}
|
|
154
170
|
export function getFlowRunStatus(context, input) {
|
|
155
171
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
156
172
|
const run = resolveRun(context, project.id, input.runId);
|
|
157
|
-
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
const runtime = readRuntimeSnapshot(refreshedRun.runtime_path);
|
|
161
|
-
return { ok: true, run: flowRunSummary(refreshedRun), index, ...(runtime ? { runtime } : {}), flow_guidance: guidanceForRun(context, refreshedRun, index) };
|
|
173
|
+
const index = authoritativeIndex(run);
|
|
174
|
+
const runtime = readRuntimeSnapshot(run.runtime_path);
|
|
175
|
+
return { ok: true, run: flowRunSummary(run), index, ...(runtime ? { runtime } : {}), flow_guidance: guidanceForRun(context, run, index) };
|
|
162
176
|
}
|
|
163
177
|
export function getFlowRunFlagsStatus(context, input) {
|
|
164
178
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
@@ -5,6 +5,7 @@ import { fileURLToPath } from "node:url";
|
|
|
5
5
|
import { Ajv } from "ajv/dist/ajv.js";
|
|
6
6
|
import { normalizeFlowContract } from "../domain/flow-contract.js";
|
|
7
7
|
import { AppError } from "../shared/errors.js";
|
|
8
|
+
import { findRunHome, readRunEngineBinding } from "./run-engine-bindings.js";
|
|
8
9
|
export function captureMemoryBankBaseline(input) {
|
|
9
10
|
const projectRoot = path.resolve(input.projectRoot);
|
|
10
11
|
const paths = normalizeMemoryBankPaths(projectRoot, input.paths);
|
|
@@ -116,6 +117,9 @@ export function validateSchema(options) {
|
|
|
116
117
|
};
|
|
117
118
|
}
|
|
118
119
|
function resolveSchema(options) {
|
|
120
|
+
const bound = resolveRunBoundSchema(options);
|
|
121
|
+
if (bound)
|
|
122
|
+
return bound;
|
|
119
123
|
const fileNames = [`${options.schemaName}.schema.json`];
|
|
120
124
|
const projectRoot = path.resolve(options.projectRoot ?? process.cwd());
|
|
121
125
|
const roots = [
|
|
@@ -145,6 +149,98 @@ function resolveSchema(options) {
|
|
|
145
149
|
source: found.source
|
|
146
150
|
};
|
|
147
151
|
}
|
|
152
|
+
const historicalSchemaRegistry = {
|
|
153
|
+
"294fd5f24ec6a4f938f15dfd98a4b303c2e026753062c3c6c8b15e09029a1128": [{
|
|
154
|
+
name: "merge-stage-report",
|
|
155
|
+
id: "dd-flow/merge-stage-report@2/engine-0.4.2",
|
|
156
|
+
path: "dist/schemas/merge-stage-report-legacy-0.4.2.schema.json",
|
|
157
|
+
checksum: "d861630cafd51e71e5440dadac2628413501e8b063ff19bef7d16966dc7077e3"
|
|
158
|
+
}],
|
|
159
|
+
"0fc717e17c359242662b76d8c2518bf4c4237366221e69bb979d2959d5ae8b60": [{
|
|
160
|
+
name: "merge-stage-report",
|
|
161
|
+
id: "dd-flow/stage-report@1",
|
|
162
|
+
path: "dist/schemas/stage-report.schema.json",
|
|
163
|
+
checksum: "d9889af7f2c0afb47e2abab330aea3e1750879d0738d87d38d2d20e3fe0403f3"
|
|
164
|
+
}]
|
|
165
|
+
};
|
|
166
|
+
function resolveRunBoundSchema(options) {
|
|
167
|
+
if (!options.projectRoot || options.schemaDir)
|
|
168
|
+
return null;
|
|
169
|
+
const data = readJson(path.resolve(options.file), "input file");
|
|
170
|
+
const root = asRecord(data);
|
|
171
|
+
const run = root ? asRecord(objectValue(root, "run")) : undefined;
|
|
172
|
+
const runId = run ? objectValue(run, "run_id") : objectValue(root ?? {}, "run_id");
|
|
173
|
+
if (typeof runId !== "string")
|
|
174
|
+
return null;
|
|
175
|
+
const projectRoot = path.resolve(options.projectRoot);
|
|
176
|
+
const located = findRunHome(options.ddFlowHome ?? process.env.DD_FLOW_HOME ?? path.join(path.dirname(projectRoot), ".dd-flow"), projectRoot, runId);
|
|
177
|
+
if (!located || !isInside(located.run_home, path.resolve(options.file)))
|
|
178
|
+
return null;
|
|
179
|
+
const binding = readRunEngineBinding(located.binding_path, { allowMissing: true });
|
|
180
|
+
if (!binding)
|
|
181
|
+
return null;
|
|
182
|
+
const manifestPath = path.join(binding.engine.snapshot_root, "engine.json");
|
|
183
|
+
const manifest = readJson(manifestPath, "bound engine manifest");
|
|
184
|
+
const integrity = asRecord(objectValue(manifest, "integrity"));
|
|
185
|
+
if (manifest.package_version !== binding.engine.package_version || manifest.engine_version !== binding.engine.engine_version || integrity?.checksum !== binding.engine.integrity_checksum) {
|
|
186
|
+
throw new AppError("run_engine_binding_invalid", "RUN engine binding does not match its engine manifest", 1, { binding: binding.engine, manifest: manifestPath });
|
|
187
|
+
}
|
|
188
|
+
const registry = asRecord(objectValue(manifest, "schema_registry"));
|
|
189
|
+
const entries = Array.isArray(registry?.entries) ? registry.entries.filter((entry) => Boolean(asRecord(entry))) : [];
|
|
190
|
+
const registered = entries.map(asSchemaRegistryEntry).find((entry) => entry?.name === options.schemaName);
|
|
191
|
+
const historical = !registered ? historicalSchemaRegistry[binding.engine.integrity_checksum]?.find((entry) => entry.name === options.schemaName) : undefined;
|
|
192
|
+
const profile = registered ?? historical;
|
|
193
|
+
if (registry && !registered) {
|
|
194
|
+
throw new AppError("schema_not_found", `Schema not registered for RUN-bound engine: ${options.schemaName}`, 2, { run_id: runId, engine: binding.engine });
|
|
195
|
+
}
|
|
196
|
+
if (!profile) {
|
|
197
|
+
throw new AppError("schema_not_found", `Schema not found for RUN-bound engine: ${options.schemaName}`, 2, { run_id: runId, engine: binding.engine });
|
|
198
|
+
}
|
|
199
|
+
const relativePath = profile.path;
|
|
200
|
+
const schemaPath = historical
|
|
201
|
+
? path.join(bundledSchemaDir(), path.basename(relativePath))
|
|
202
|
+
: resolveInside(binding.engine.snapshot_root, relativePath);
|
|
203
|
+
if (!schemaPath || !fs.existsSync(schemaPath)) {
|
|
204
|
+
throw new AppError("schema_not_found", `Schema not found for RUN-bound engine: ${options.schemaName}`, 2, { run_id: runId, engine: binding.engine, path: relativePath });
|
|
205
|
+
}
|
|
206
|
+
if (checksum(schemaPath) !== profile.checksum) {
|
|
207
|
+
throw new AppError("schema_registry_invalid", "RUN-bound engine schema does not match its registry checksum", 1, { run_id: runId, schema: options.schemaName, path: schemaPath });
|
|
208
|
+
}
|
|
209
|
+
const schema = readJson(schemaPath, "schema");
|
|
210
|
+
const id = objectValue(asRecord(schema) ?? {}, "$id");
|
|
211
|
+
if (id !== profile.id) {
|
|
212
|
+
throw new AppError("schema_registry_invalid", "RUN-bound engine schema does not match its registry id", 1, { run_id: runId, schema: options.schemaName, path: schemaPath });
|
|
213
|
+
}
|
|
214
|
+
return {
|
|
215
|
+
name: options.schemaName,
|
|
216
|
+
id: typeof id === "string" ? id : options.schemaName,
|
|
217
|
+
path: schemaPath,
|
|
218
|
+
source: historical ? "engine_legacy_registry" : "run_bound_engine",
|
|
219
|
+
engine: { package_version: binding.engine.package_version, engine_version: binding.engine.engine_version, integrity_checksum: binding.engine.integrity_checksum, snapshot_root: binding.engine.snapshot_root }
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
function asSchemaRegistryEntry(value) {
|
|
223
|
+
const name = objectValue(value, "name");
|
|
224
|
+
const id = objectValue(value, "id");
|
|
225
|
+
const schemaPath = objectValue(value, "path");
|
|
226
|
+
const entryChecksum = objectValue(value, "checksum");
|
|
227
|
+
return typeof name === "string" && typeof id === "string" && typeof schemaPath === "string" && typeof entryChecksum === "string"
|
|
228
|
+
? { name, id, path: schemaPath, checksum: entryChecksum }
|
|
229
|
+
: null;
|
|
230
|
+
}
|
|
231
|
+
function resolveInside(root, relativePath) {
|
|
232
|
+
if (path.isAbsolute(relativePath))
|
|
233
|
+
return null;
|
|
234
|
+
const candidate = path.resolve(root, relativePath);
|
|
235
|
+
return isInside(root, candidate) ? candidate : null;
|
|
236
|
+
}
|
|
237
|
+
function isInside(root, candidate) {
|
|
238
|
+
const relative = path.relative(path.resolve(root), path.resolve(candidate));
|
|
239
|
+
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
|
240
|
+
}
|
|
241
|
+
function checksum(file) {
|
|
242
|
+
return crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex");
|
|
243
|
+
}
|
|
148
244
|
function bundledSchemaDir() {
|
|
149
245
|
return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "schemas");
|
|
150
246
|
}
|
|
@@ -92,6 +92,11 @@ function runtimeFacts(view, projectRoot) {
|
|
|
92
92
|
workspace_root: view.run.workspace_root,
|
|
93
93
|
run_id: view.run.id,
|
|
94
94
|
protocol_id: view.run.subject.id,
|
|
95
|
+
runtime: {
|
|
96
|
+
source: "dd-flow",
|
|
97
|
+
state: "trusted",
|
|
98
|
+
flow_kind: view.run.flow_kind
|
|
99
|
+
},
|
|
95
100
|
git: view.index.execution?.git ?? { branch: null, head: null, status: "unavailable" }
|
|
96
101
|
};
|
|
97
102
|
}
|
|
@@ -117,7 +122,9 @@ export function startStage(context, input) {
|
|
|
117
122
|
const preflight = stagePreflight(projectRoot, stageRoot);
|
|
118
123
|
if (preflight.ok !== true)
|
|
119
124
|
throw new AppError("permission_preflight_failed", "Stage workspace is not writable", 1, { preflight });
|
|
120
|
-
|
|
125
|
+
if (attached.run.subject.type === "protocol") {
|
|
126
|
+
syncProtocolLifecycle(context, attached.run.project_id, attached.run.subject.id, input.stage, "running");
|
|
127
|
+
}
|
|
121
128
|
if (input.sessionId) {
|
|
122
129
|
registerFlowSession(context, {
|
|
123
130
|
sessionId: input.sessionId,
|
|
@@ -143,6 +150,7 @@ export function startStage(context, input) {
|
|
|
143
150
|
const aspectMapPath = path.join(stageRoot, "aspect-map.json");
|
|
144
151
|
const attempt = attached.index.stage_runs?.find((stage) => stage.stage === input.stage)?.attempt ?? "try-001";
|
|
145
152
|
const attemptNumber = Number(attempt.replace("try-", "")) || 1;
|
|
153
|
+
const sources = stageInstructionSources(context, projectRoot, attached.run.flow_kind, input.stage);
|
|
146
154
|
atomicWrite(promptDataPath, {
|
|
147
155
|
schema_id: "dd-flow/stage-prompt@2",
|
|
148
156
|
run_id: attached.run.id,
|
|
@@ -160,10 +168,25 @@ export function startStage(context, input) {
|
|
|
160
168
|
...(input.stage === "plan" ? { plan: planPath, "aspect-map": aspectMapPath } : {})
|
|
161
169
|
},
|
|
162
170
|
write_boundary: { current: "@stage", archive: attempt, archive_writable: false },
|
|
163
|
-
source_fragments:
|
|
171
|
+
source_fragments: sources.map((source) => source.label),
|
|
164
172
|
authoritative_facts: runtimeFacts(attached, projectRoot),
|
|
165
|
-
preflight
|
|
166
|
-
|
|
173
|
+
preflight: {
|
|
174
|
+
compatibility: {
|
|
175
|
+
status: "checked",
|
|
176
|
+
operation: `stage.${input.stage}`,
|
|
177
|
+
project_root: projectRoot
|
|
178
|
+
},
|
|
179
|
+
permissions: preflight,
|
|
180
|
+
session_binding: {
|
|
181
|
+
status: input.sessionId ? "bound" : "not_bound",
|
|
182
|
+
session_id: input.sessionId ?? null
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
required_context: sources.map((source) => ({
|
|
186
|
+
path: source.label,
|
|
187
|
+
reason: "Stage-specific canonical instruction source",
|
|
188
|
+
stop_condition: "Stop when the source no longer contains unresolved requirements for this stage"
|
|
189
|
+
})),
|
|
167
190
|
worker_prompt_markdown: prompt
|
|
168
191
|
});
|
|
169
192
|
validateSchema({ schemaName: "stage-prompt", file: promptDataPath, projectRoot });
|
|
@@ -196,7 +219,7 @@ export function startStage(context, input) {
|
|
|
196
219
|
},
|
|
197
220
|
...(input.stage === "plan" ? { plan_ref: planPath, aspect_map_ref: aspectMapPath } : {}),
|
|
198
221
|
resolved_context: { run: attached.run, stage: { name: input.stage, dir, status: "running" } },
|
|
199
|
-
next_command:
|
|
222
|
+
next_command: stageFinishCommand(attached.run, projectRoot, input.stage),
|
|
200
223
|
permission_probe: preflight,
|
|
201
224
|
run: attached.run,
|
|
202
225
|
prompt: { path: promptPath, data_path: promptDataPath },
|
|
@@ -239,10 +262,12 @@ export function finishStage(context, input) {
|
|
|
239
262
|
const reportPath = path.join(stageRoot, "stage-report.md");
|
|
240
263
|
const htmlPath = path.join(stageRoot, "stage-report.html");
|
|
241
264
|
atomicWrite(dataPath, report);
|
|
242
|
-
validateSchema({ schemaName:
|
|
265
|
+
validateSchema({ schemaName: stageReportSchemaName(input.stage, planFinish), file: dataPath, projectRoot });
|
|
243
266
|
atomicWrite(reportPath, renderMarkdown(report));
|
|
244
267
|
atomicWrite(htmlPath, renderHtml(projectRoot, report));
|
|
245
|
-
|
|
268
|
+
if (view.run.subject.type === "protocol") {
|
|
269
|
+
syncProtocolLifecycle(context, view.run.project_id, view.run.subject.id, input.stage, status, stringValue(semantic.next_action));
|
|
270
|
+
}
|
|
246
271
|
const summaryPath = updateProtocolSummary(context, projectRoot, view, report, dataPath, htmlPath, reportPath);
|
|
247
272
|
const completed = completeFlowRunStage(context, {
|
|
248
273
|
projectRoot,
|
|
@@ -360,6 +385,19 @@ function dataSchemaForStage(stage) {
|
|
|
360
385
|
return "dd-flow/merge-stage-report@2";
|
|
361
386
|
return "dd-flow/code-stage-report@2";
|
|
362
387
|
}
|
|
388
|
+
function stageReportSchemaName(stage, planFinish) {
|
|
389
|
+
if (planFinish)
|
|
390
|
+
return "plan-stage-report";
|
|
391
|
+
if (stage === "code" || stage === "implementation")
|
|
392
|
+
return "code-stage-report";
|
|
393
|
+
if (stage === "merge")
|
|
394
|
+
return "merge-stage-report";
|
|
395
|
+
return "stage-report";
|
|
396
|
+
}
|
|
397
|
+
function stageFinishCommand(run, projectRoot, stage) {
|
|
398
|
+
const compatibilityMode = run.flow_kind === "mb-upgrade" ? " --compatibility-mode mb-upgrade" : "";
|
|
399
|
+
return `dd-flow stage finish ${run.id} --stage ${stage} --outcome done --project-root ${JSON.stringify(projectRoot)}${compatibilityMode} --json`;
|
|
400
|
+
}
|
|
363
401
|
function composeStagePrompt(context, projectRoot, before, current, stage, dir, preflight) {
|
|
364
402
|
const project = requireProjectByRoot(context, projectRoot);
|
|
365
403
|
const runHome = runHomePath(current.run);
|
|
@@ -414,14 +452,14 @@ function composeStagePrompt(context, projectRoot, before, current, stage, dir, p
|
|
|
414
452
|
"</work_contract>",
|
|
415
453
|
"",
|
|
416
454
|
"<completion_contract>",
|
|
417
|
-
`Write semantic output to \`@stage/stage-input.json\`, then run:
|
|
455
|
+
`Write semantic output to \`@stage/stage-input.json\`, then run: \`${stageFinishCommand(current.run, projectRoot, stage)}\`.`,
|
|
418
456
|
"The CLI must generate validated JSON, Markdown, HTML and protocol-summary evidence.",
|
|
419
457
|
"</completion_contract>",
|
|
420
458
|
].join("\n");
|
|
421
459
|
}
|
|
422
460
|
function stageInstructionSources(context, projectRoot, flowKind, stage) {
|
|
423
461
|
if (flowKind === "mb-upgrade") {
|
|
424
|
-
const canon = resolveCanonRoot(context);
|
|
462
|
+
const canon = resolveCanonRoot(context, context.env.DD_MEMORYBANK ? { explicitRoot: context.env.DD_MEMORYBANK } : {});
|
|
425
463
|
if (!canon.ok || !canon.canon) {
|
|
426
464
|
throw new AppError("canon_unavailable", "mb-upgrade stage prompt requires the canonical Memory Bank", 1, {
|
|
427
465
|
blockers: canon.blockers,
|
|
@@ -541,6 +579,34 @@ function buildStageReport(context, view, stage, status, semantic, stageRoot, lin
|
|
|
541
579
|
}
|
|
542
580
|
};
|
|
543
581
|
}
|
|
582
|
+
if (stage === "code" || stage === "implementation") {
|
|
583
|
+
return {
|
|
584
|
+
schema_id: "dd-flow/code-stage-report@2",
|
|
585
|
+
run: { run_id: view.run.id, run_state: view.run.run_index_path },
|
|
586
|
+
stage: { name: stage, dir: path.basename(stageRoot), status },
|
|
587
|
+
project: { id: view.run.project_id, title: path.basename(view.run.project_root) },
|
|
588
|
+
subject: { id: view.run.subject.id, title: view.run.subject.id },
|
|
589
|
+
flow_flags: flowFlagsReportProjection(view.index.flow_flags),
|
|
590
|
+
overall: {
|
|
591
|
+
verdict: status === "done" ? "accepted" : status,
|
|
592
|
+
summary: result,
|
|
593
|
+
next_action: stringValue(semantic.next_action) ?? "Proceed to readiness."
|
|
594
|
+
},
|
|
595
|
+
breadcrumbs: [{ label: "RUN", href: view.run.run_index_path, status: "available" }],
|
|
596
|
+
implemented_goals: [{ title: `Stage ${stage}`, summary: result }],
|
|
597
|
+
acceptance_scenarios: acceptance.map((summary, index) => ({
|
|
598
|
+
id: `SCN-${stage}-${index + 1}`,
|
|
599
|
+
title: `Acceptance ${index + 1}`,
|
|
600
|
+
verdict: "accepted",
|
|
601
|
+
steps: [{ title: "Stage finish", summary }],
|
|
602
|
+
evidence: evidence.map((item) => ({ path: item, label: item }))
|
|
603
|
+
})),
|
|
604
|
+
changed_files: changedFiles.map((file) => ({ path: file, label: file })),
|
|
605
|
+
checks: checks.map((name) => ({ name, status: "passed" })),
|
|
606
|
+
review: { verdict: status === "done" ? "accepted" : status, findings: [] },
|
|
607
|
+
defs: Array.isArray(semantic.def_outcomes) ? semantic.def_outcomes : []
|
|
608
|
+
};
|
|
609
|
+
}
|
|
544
610
|
return {
|
|
545
611
|
schema_id: "dd-flow/stage-report@1",
|
|
546
612
|
run_id: view.run.id,
|
|
@@ -611,6 +677,28 @@ function stringValue(value) {
|
|
|
611
677
|
function stringArray(value, fallback) {
|
|
612
678
|
return Array.isArray(value) && value.every((entry) => typeof entry === "string") ? value : fallback;
|
|
613
679
|
}
|
|
680
|
+
function flowFlagsReportProjection(value) {
|
|
681
|
+
if (!value) {
|
|
682
|
+
return {
|
|
683
|
+
flow_kind: "unknown",
|
|
684
|
+
snapshot_revision: 1,
|
|
685
|
+
resolution_status: "legacy_incomplete",
|
|
686
|
+
values: {},
|
|
687
|
+
snapshot_checksum: "0".repeat(64)
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
return {
|
|
691
|
+
...(value.contract ? { contract: value.contract } : {}),
|
|
692
|
+
flow_kind: value.flow_kind ?? "unknown",
|
|
693
|
+
...(value.preset ? { preset: value.preset } : {}),
|
|
694
|
+
snapshot_revision: value.snapshot_revision ?? 1,
|
|
695
|
+
resolution_status: value.resolution_status ?? "legacy_incomplete",
|
|
696
|
+
values: value.values ?? {},
|
|
697
|
+
...(value.snapshot_checksum ? { snapshot_checksum: value.snapshot_checksum } : { snapshot_checksum: "0".repeat(64) }),
|
|
698
|
+
...(Array.isArray(value.floors_applied) ? { floors_applied: value.floors_applied } : {}),
|
|
699
|
+
...(value.resolved_at ? { resolved_at: value.resolved_at } : {})
|
|
700
|
+
};
|
|
701
|
+
}
|
|
614
702
|
function gitChangedFiles(workspaceRoot) {
|
|
615
703
|
const result = spawnSync("git", ["-C", workspaceRoot, "status", "--short"], { encoding: "utf8" });
|
|
616
704
|
if (result.status !== 0)
|
|
@@ -634,7 +722,7 @@ function workerCoverage(context, projectId, runId) {
|
|
|
634
722
|
}
|
|
635
723
|
function runTargetedMemoryBankLint(projectRoot, semantic) {
|
|
636
724
|
const declared = stringArray(semantic.changed_files, []);
|
|
637
|
-
const files = [...new Set(declared.filter((file) => file.startsWith(".memory-bank/") && !file.split(/[\\/]/u).some((part) => part === ".." || part.startsWith(".env"))))];
|
|
725
|
+
const files = [...new Set(declared.filter((file) => file.startsWith(".memory-bank/") && file.toLowerCase().endsWith(".md") && !file.split(/[\\/]/u).some((part) => part === ".." || part.startsWith(".env"))))];
|
|
638
726
|
for (const file of files)
|
|
639
727
|
ensureWithin(projectRoot, path.resolve(projectRoot, file), "lint target");
|
|
640
728
|
if (files.length === 0) {
|
package/dist/services/status.js
CHANGED
|
@@ -9,7 +9,7 @@ import { getProjectVersionStatus, resolveStatusProjectRoot } from "./version-sta
|
|
|
9
9
|
import { findProjectByRoot } from "./projects.js";
|
|
10
10
|
import { classifyCliOperation } from "./cli-operation-classifier.js";
|
|
11
11
|
import { compatibilityReport } from "./compatibility-preflight.js";
|
|
12
|
-
import { selectEngine } from "./engines.js";
|
|
12
|
+
import { compatibilityContexts, selectEngine } from "./engines.js";
|
|
13
13
|
export function getRuntimeStatus(context, input = {}) {
|
|
14
14
|
const cwd = process.cwd();
|
|
15
15
|
const projectRootResolution = resolveStatusProjectRoot({ requestedRoot: input.projectRoot, cwd });
|
|
@@ -26,9 +26,13 @@ export function getRuntimeStatus(context, input = {}) {
|
|
|
26
26
|
const cli = getCliBuildInfo();
|
|
27
27
|
const compatibilityManifest = resolvedCanonForProject ? readCompatibilityManifest(canon) : null;
|
|
28
28
|
const cliCompatibility = cliCompatibilityVerdict(cli, compatibilityManifest);
|
|
29
|
-
const
|
|
29
|
+
const classification = classifyCliOperation(["status"], context.env);
|
|
30
|
+
const engineSelection = projectRoot
|
|
31
|
+
? selectEngine(context, { projectRoot, env: context.env }, { allowCurrentInProcess: true }, classification)
|
|
32
|
+
: null;
|
|
30
33
|
const flowContract = projectRoot ? projectFlowContractStatus(projectRoot) : null;
|
|
31
34
|
const registry = input.checkRegistry ? checkNpmRegistry(context, cli.package_name) : undefined;
|
|
35
|
+
const contexts = projectRoot ? compatibilityContexts(context, { projectRoot, env: context.env, classification }) : null;
|
|
32
36
|
return {
|
|
33
37
|
ok: true,
|
|
34
38
|
schema_id: "dd-flow/status-report@1",
|
|
@@ -54,7 +58,8 @@ export function getRuntimeStatus(context, input = {}) {
|
|
|
54
58
|
engine: engineSelection
|
|
55
59
|
? {
|
|
56
60
|
selection: engineSelection,
|
|
57
|
-
compatibility: compatibilityReport(engineSelection,
|
|
61
|
+
compatibility: compatibilityReport(engineSelection, classification),
|
|
62
|
+
contexts
|
|
58
63
|
}
|
|
59
64
|
: null,
|
|
60
65
|
canon: {
|
package/dist/storage/database.js
CHANGED
|
@@ -5,27 +5,48 @@ import { ensureDir } from "./paths.js";
|
|
|
5
5
|
import { AppError } from "../shared/errors.js";
|
|
6
6
|
const require = createRequire(import.meta.url);
|
|
7
7
|
const { DatabaseSync } = require("node:sqlite");
|
|
8
|
-
export function getDatabase(ddFlowHome) {
|
|
9
|
-
ensureDir(ddFlowHome);
|
|
8
|
+
export function getDatabase(ddFlowHome, mode = "initialize") {
|
|
10
9
|
const dbPath = path.join(ddFlowHome, "db.sqlite");
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
const exists = fs.existsSync(dbPath);
|
|
11
|
+
if (mode === "read_existing" && !exists) {
|
|
12
|
+
return emptyReadOnlyDatabase(dbPath);
|
|
13
|
+
}
|
|
14
|
+
if (!exists || mode === "migrate")
|
|
15
|
+
ensureDir(ddFlowHome);
|
|
16
|
+
const db = mode === "read_existing" ? new DatabaseSync(dbPath, { readOnly: true }) : new DatabaseSync(dbPath);
|
|
17
|
+
configureDatabase(db, mode);
|
|
18
|
+
// Existing storage is never schema-migrated by an ordinary initialize open.
|
|
19
|
+
// Only the explicit migrate handle is allowed to execute DDL on it.
|
|
20
|
+
if (!exists || mode === "migrate")
|
|
21
|
+
migrate(db);
|
|
22
|
+
if (mode === "read_existing")
|
|
23
|
+
db.exec("PRAGMA query_only = ON");
|
|
14
24
|
db.exec("PRAGMA foreign_keys = ON");
|
|
15
25
|
return {
|
|
16
26
|
path: dbPath,
|
|
27
|
+
writable: mode !== "read_existing",
|
|
17
28
|
exec: (sql) => db.exec(sql),
|
|
18
29
|
run: (sql, params = []) => db.prepare(sql).run(...params),
|
|
19
30
|
get: (sql, params = []) => db.prepare(sql).get(...params),
|
|
20
31
|
all: (sql, params = []) => db.prepare(sql).all(...params)
|
|
21
32
|
};
|
|
22
33
|
}
|
|
23
|
-
function
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
34
|
+
function emptyReadOnlyDatabase(dbPath) {
|
|
35
|
+
return {
|
|
36
|
+
path: dbPath,
|
|
37
|
+
writable: false,
|
|
38
|
+
exec: () => { throw new AppError("read_only_storage", "Read-only access cannot write absent dd-flow storage", 1, { path: dbPath }); },
|
|
39
|
+
run: () => { throw new AppError("read_only_storage", "Read-only access cannot write absent dd-flow storage", 1, { path: dbPath }); },
|
|
40
|
+
get: () => undefined,
|
|
41
|
+
all: () => []
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function configureDatabase(db, mode) {
|
|
45
|
+
db.exec("PRAGMA busy_timeout = 4000");
|
|
46
|
+
if (mode !== "read_existing") {
|
|
47
|
+
db.exec("PRAGMA journal_mode = WAL");
|
|
48
|
+
db.exec("PRAGMA synchronous = NORMAL");
|
|
49
|
+
}
|
|
29
50
|
}
|
|
30
51
|
function migrate(db) {
|
|
31
52
|
db.exec(`
|