@deksden-com/dd-flow-cli 0.9.0-beta.7 → 0.9.0-beta.8
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 +8 -0
- package/dist/build-info.json +10 -10
- package/dist/cli/help.js +1 -1
- package/dist/cli/run-cli.js +22 -1
- package/dist/runtime/context.js +3 -1
- package/dist/schemas/harness-config.schema.json +23 -0
- package/dist/services/engines.js +4 -4
- package/dist/services/eval-snapshots.js +5 -3
- package/dist/services/harness-config.js +66 -0
- package/dist/services/merge-server.js +3 -2
- package/dist/services/prompts.js +4 -2
- package/dist/services/run-engine-bindings.js +19 -61
- package/dist/services/runs.js +21 -3
- package/dist/services/schema-validation.js +11 -11
- package/dist/services/stage-lifecycle.js +15 -8
- package/dist/services/stage-pause.js +4 -4
- package/dist/services/vnext-code-review.js +37 -31
- package/dist/services/vnext-code.js +40 -13
- package/dist/services/vnext-fanout.js +4 -4
- package/dist/services/vnext-merge.js +42 -23
- package/dist/services/vnext-plan-review.js +5 -5
- package/dist/services/vnext-plan.js +9 -9
- package/dist/services/vnext-protocolize.js +6 -6
- package/dist/services/vnext-specify.js +6 -6
- package/dist/services/work-registry.js +8 -8
- package/dist/storage/database.js +3 -0
- package/package.json +12 -12
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# @deksden-com/dd-flow-cli
|
|
2
2
|
|
|
3
|
+
## 0.9.0-beta.8
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 5f17e79: Keep CODE worker and reviewer prompts aligned on authorization/lifecycle write
|
|
8
|
+
boundaries, and project current check receipts without overwriting repair
|
|
9
|
+
history.
|
|
10
|
+
|
|
3
11
|
## 0.9.0-beta.7
|
|
4
12
|
|
|
5
13
|
### Patch Changes
|
package/dist/build-info.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"cli_package": "@deksden-com/dd-flow-cli",
|
|
3
|
-
"cli_version": "0.9.0-beta.
|
|
4
|
-
"cli_commit": "
|
|
5
|
-
"built_at": "2026-09-
|
|
3
|
+
"cli_version": "0.9.0-beta.8",
|
|
4
|
+
"cli_commit": "98af28f2e50816d1042ee2a75d2f8133b655ffcb",
|
|
5
|
+
"built_at": "2026-09-04T09:50:57.779Z",
|
|
6
6
|
"built_with_canon": {
|
|
7
|
-
"version":
|
|
8
|
-
"commit":
|
|
9
|
-
"flow_contract":
|
|
10
|
-
"repo_root":
|
|
11
|
-
"memorybank_root":
|
|
12
|
-
"flow_root":
|
|
13
|
-
"layout":
|
|
7
|
+
"version": "4.0.1",
|
|
8
|
+
"commit": "7bd6761e6bd4217ceec08e2f9e98387b60545661",
|
|
9
|
+
"flow_contract": "dd-flow-canonical-2026-08",
|
|
10
|
+
"repo_root": "/Users/deksden/Documents/_Projects/dd-memorybank",
|
|
11
|
+
"memorybank_root": "/Users/deksden/Documents/_Projects/dd-memorybank/.memory-bank",
|
|
12
|
+
"flow_root": "/Users/deksden/Documents/_Projects/dd-memorybank/.memory-bank/dd-flow",
|
|
13
|
+
"layout": "dot_memory_bank"
|
|
14
14
|
}
|
|
15
15
|
}
|
package/dist/cli/help.js
CHANGED
|
@@ -477,7 +477,7 @@ Dashboards are rendered views of dd-flow state. Markdown remains the compatibili
|
|
|
477
477
|
`dd-flow schema - validate canonical dd-flow data contracts
|
|
478
478
|
|
|
479
479
|
Usage:
|
|
480
|
-
dd-flow schema validate --schema <name> --file <json-file> [--project-root <root>] [--schema-dir <dir>] --json
|
|
480
|
+
dd-flow schema validate --schema <name> --file <json-file> [--project-root <root>] [--run <RUN>] [--schema-dir <dir>] --json
|
|
481
481
|
|
|
482
482
|
Schema names resolve to <name>.schema.json. Lookup order is --schema-dir, then <project-root>/.memory-bank/dd-flow/schemas/, then <project-root>/dd-flow/schemas/ for canonical checkouts, then bundled CLI schemas. Flow-contract validation runs structural checks first and the runtime's shared semantic normalizer second. If --project-root is omitted, the current working directory is used. JSON mode writes valid results to stdout and structured validation/usage errors to stderr.`
|
|
483
483
|
],
|
package/dist/cli/run-cli.js
CHANGED
|
@@ -1797,16 +1797,37 @@ function dispatchSchema(context, command, parsed) {
|
|
|
1797
1797
|
if (command === "validate") {
|
|
1798
1798
|
const schemaDir = optionalOption(parsed, "schema-dir");
|
|
1799
1799
|
const projectRoot = optionalOption(parsed, "project-root");
|
|
1800
|
+
const file = requiredOption(parsed, "file");
|
|
1801
|
+
// A generic validation command can still bind to the exact RUN without
|
|
1802
|
+
// searching project directories: run_id is a structural field in the
|
|
1803
|
+
// submitted artifact and the database owns its materialized run_root.
|
|
1804
|
+
const explicitRunId = optionalOption(parsed, "run");
|
|
1805
|
+
const declaredRunId = explicitRunId ?? readRunId(file);
|
|
1806
|
+
const fileRunRoot = path.basename(file) === "run.json" ? path.dirname(path.resolve(file)) : undefined;
|
|
1807
|
+
const bound = projectRoot
|
|
1808
|
+
? context.db.get("SELECT r.id, r.run_root FROM runs r JOIN projects p ON p.id = r.project_id WHERE p.root = ? AND (r.id = ? OR r.run_root = ?) LIMIT 1", [fs.realpathSync(projectRoot), declaredRunId ?? "", fileRunRoot ?? ""])
|
|
1809
|
+
: undefined;
|
|
1800
1810
|
return validateSchema({
|
|
1801
1811
|
schemaName: requiredOption(parsed, "schema"),
|
|
1802
|
-
file
|
|
1812
|
+
file,
|
|
1803
1813
|
...(schemaDir ? { schemaDir } : {}),
|
|
1804
1814
|
...(projectRoot ? { projectRoot } : {}),
|
|
1815
|
+
...(declaredRunId || bound?.id ? { runId: declaredRunId ?? bound.id } : {}),
|
|
1816
|
+
...(bound?.run_root ? { runRoot: bound.run_root } : {}),
|
|
1805
1817
|
ddFlowHome: context.ddFlowHome
|
|
1806
1818
|
});
|
|
1807
1819
|
}
|
|
1808
1820
|
throw new AppError("usage", `Unknown schema command: ${command ?? "<empty>"}`, 2);
|
|
1809
1821
|
}
|
|
1822
|
+
function readRunId(file) {
|
|
1823
|
+
try {
|
|
1824
|
+
const value = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
1825
|
+
return typeof value.run_id === "string" && value.run_id.trim() ? value.run_id : undefined;
|
|
1826
|
+
}
|
|
1827
|
+
catch {
|
|
1828
|
+
return undefined;
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1810
1831
|
async function dispatchLane(context, command, parsed) {
|
|
1811
1832
|
if (command === "status") {
|
|
1812
1833
|
return getLaneStatus(context, { projectRoot: requiredOption(parsed, "project-root"), lane: optionalOption(parsed, "lane") });
|
package/dist/runtime/context.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { getDatabase } from "../storage/database.js";
|
|
2
2
|
import { resolveDdFlowHome } from "../storage/paths.js";
|
|
3
3
|
export function createRouterContext(env) {
|
|
4
|
+
const ddFlowHome = resolveDdFlowHome(env);
|
|
4
5
|
return {
|
|
5
|
-
ddFlowHome
|
|
6
|
+
ddFlowHome,
|
|
7
|
+
db: getDatabase(ddFlowHome, "read_existing"),
|
|
6
8
|
now: () => new Date().toISOString()
|
|
7
9
|
};
|
|
8
10
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "dd-flow/harness-config@1",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"additionalProperties": false,
|
|
6
|
+
"required": ["schema_id", "harnesses"],
|
|
7
|
+
"properties": {
|
|
8
|
+
"schema_id": { "const": "dd-flow/harness-config@1" },
|
|
9
|
+
"harnesses": {
|
|
10
|
+
"type": "object",
|
|
11
|
+
"additionalProperties": {
|
|
12
|
+
"type": "object",
|
|
13
|
+
"additionalProperties": false,
|
|
14
|
+
"required": ["adapter_command", "runtime_command"],
|
|
15
|
+
"properties": {
|
|
16
|
+
"adapter_command": { "type": "string", "minLength": 1 },
|
|
17
|
+
"runtime_command": { "type": "string", "minLength": 1 },
|
|
18
|
+
"allow_path_discovery": { "type": "boolean" }
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
package/dist/services/engines.js
CHANGED
|
@@ -12,7 +12,7 @@ import { requireProtocol } from "./protocols.js";
|
|
|
12
12
|
import { resolveCanonRootBeforeContext } from "./canon.js";
|
|
13
13
|
import { appendAudit } from "./audit.js";
|
|
14
14
|
import { findProjectByRoot } from "./projects.js";
|
|
15
|
-
import { allRunEngineBindings,
|
|
15
|
+
import { allRunEngineBindings, locateRunRoot, readRunEngineBinding, runEngineBindingSchemaId, writeRunEngineBinding } from "./run-engine-bindings.js";
|
|
16
16
|
export const engineManifestSchemaId = "dd-flow/engine-manifest@1";
|
|
17
17
|
const routerNativeFamilies = new Set(["engine", "version", "schema"]);
|
|
18
18
|
const sourceFile = fileURLToPath(import.meta.url);
|
|
@@ -182,7 +182,7 @@ export function routeArgsThroughEngine(context, args, io, stdin, env, resolvedPr
|
|
|
182
182
|
}
|
|
183
183
|
export function bindRunEngine(context, input) {
|
|
184
184
|
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
185
|
-
const located =
|
|
185
|
+
const located = locateRunRoot(context.db, projectRoot, input.runId);
|
|
186
186
|
if (!located)
|
|
187
187
|
throw new AppError("not_found", `RUN is not registered in router storage: ${input.runId}`, 1, { run_id: input.runId, project_root: projectRoot });
|
|
188
188
|
const existing = readRunEngineBinding(located.binding_path, { allowMissing: true });
|
|
@@ -250,13 +250,13 @@ export function bindCurrentEngineToRun(context, input) {
|
|
|
250
250
|
engine: engineIdentity(manifest),
|
|
251
251
|
probe: { status: "not_required", command: null }
|
|
252
252
|
};
|
|
253
|
-
return writeRunEngineBinding(path.join(input.
|
|
253
|
+
return writeRunEngineBinding(path.join(input.runRoot, "engine-binding.json"), binding).binding;
|
|
254
254
|
}
|
|
255
255
|
function boundRunEngineForArgs(context, args, projectRoot) {
|
|
256
256
|
const runId = runIdForArgs(args);
|
|
257
257
|
if (!runId || !projectRoot)
|
|
258
258
|
return null;
|
|
259
|
-
const located =
|
|
259
|
+
const located = locateRunRoot(context.db, projectRoot, runId);
|
|
260
260
|
if (!located)
|
|
261
261
|
return null;
|
|
262
262
|
const binding = readRunEngineBinding(located.binding_path, { allowMissing: true });
|
|
@@ -148,10 +148,12 @@ export function restoreEvalRunSnapshot(context, input) {
|
|
|
148
148
|
try {
|
|
149
149
|
rebaseRuntime(restored, manifest.project_root, projectRoot, sourceWorkspaceRoot(manifest), workspaceRoot, manifest.dd_flow_home, context.ddFlowHome);
|
|
150
150
|
registerProject(restored, { root: projectRoot });
|
|
151
|
-
|
|
151
|
+
requireProjectByRoot(restored, projectRoot);
|
|
152
152
|
const status = getFlowRunStatus(restored, { projectRoot, runId: manifest.run_id });
|
|
153
153
|
assertStageEntry(status.index.stage_runs ?? [], manifest.stage_entry);
|
|
154
|
-
|
|
154
|
+
if (!status.run.run_root)
|
|
155
|
+
throw new AppError("runtime_missing", "Restored RUN has no artifact root", 1, { run_id: manifest.run_id });
|
|
156
|
+
const runHome = status.run.run_root;
|
|
155
157
|
return {
|
|
156
158
|
ok: true,
|
|
157
159
|
schema_id: "dd-flow/eval-run-restore@1",
|
|
@@ -182,7 +184,7 @@ export function prepareVnextSpecifyRun(context, input) {
|
|
|
182
184
|
slug: input.slug,
|
|
183
185
|
nextAction: "start_specify"
|
|
184
186
|
});
|
|
185
|
-
return { ok: true, run_id: started.run.id,
|
|
187
|
+
return { ok: true, run_id: started.run.id, run_root: started.run.run_root, target_stage: "specify" };
|
|
186
188
|
}
|
|
187
189
|
function assertDedicatedHome(context, projectId, runId) {
|
|
188
190
|
const projects = context.db.get("SELECT COUNT(*) AS count FROM projects")?.count ?? 0;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { AppError } from "../shared/errors.js";
|
|
4
|
+
export function harnessConfigPath(ddFlowHome) {
|
|
5
|
+
return path.join(ddFlowHome, "harnesses.json");
|
|
6
|
+
}
|
|
7
|
+
export function harnessKey(value) {
|
|
8
|
+
const keys = {
|
|
9
|
+
codex: "codex-desktop", "codex-desktop": "codex-desktop",
|
|
10
|
+
zcode: "zcode-acp", "zcode-acp": "zcode-acp",
|
|
11
|
+
grok: "grok-acp", "grok-acp": "grok-acp",
|
|
12
|
+
agy: "antigravity-cli", "antigravity-cli": "antigravity-cli",
|
|
13
|
+
opencode: "opencode-server", "opencode-server": "opencode-server"
|
|
14
|
+
};
|
|
15
|
+
const resolved = keys[value];
|
|
16
|
+
if (!resolved)
|
|
17
|
+
throw new AppError("harness_config_invalid", `Unsupported harness: ${value}`, 2, { harness: value });
|
|
18
|
+
return resolved;
|
|
19
|
+
}
|
|
20
|
+
export function loadHarnessConfig(ddFlowHome) {
|
|
21
|
+
const file = harnessConfigPath(ddFlowHome);
|
|
22
|
+
let value;
|
|
23
|
+
try {
|
|
24
|
+
value = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
25
|
+
}
|
|
26
|
+
catch (cause) {
|
|
27
|
+
throw new AppError("harness_config_missing", "Harness configuration is missing or invalid", 1, { path: file, cause: String(cause) });
|
|
28
|
+
}
|
|
29
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
30
|
+
throw new AppError("harness_config_invalid", "Harness configuration must be an object", 2, { path: file });
|
|
31
|
+
const config = value;
|
|
32
|
+
if (config.schema_id !== "dd-flow/harness-config@1" || !config.harnesses || typeof config.harnesses !== "object" || Array.isArray(config.harnesses))
|
|
33
|
+
throw new AppError("harness_config_invalid", "Harness configuration has an unsupported schema", 2, { path: file });
|
|
34
|
+
for (const [name, entry] of Object.entries(config.harnesses)) {
|
|
35
|
+
harnessKey(name);
|
|
36
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry) || typeof entry.adapter_command !== "string" || typeof entry.runtime_command !== "string")
|
|
37
|
+
throw new AppError("harness_config_invalid", "Each harness requires adapter_command and runtime_command", 2, { path: file, harness: name });
|
|
38
|
+
}
|
|
39
|
+
return config;
|
|
40
|
+
}
|
|
41
|
+
export function resolveHarnessCommand(ddFlowHome, harness, command) {
|
|
42
|
+
const key = harnessKey(harness);
|
|
43
|
+
const entry = loadHarnessConfig(ddFlowHome).harnesses[key];
|
|
44
|
+
if (!entry)
|
|
45
|
+
throw new AppError("harness_config_missing", "Selected harness is not configured", 1, { path: harnessConfigPath(ddFlowHome), harness: key });
|
|
46
|
+
const value = command === "adapter" ? entry.adapter_command : entry.runtime_command;
|
|
47
|
+
if (!path.isAbsolute(value))
|
|
48
|
+
throw new AppError("harness_config_invalid", "Harness commands must be absolute paths", 2, { harness: key, command, value });
|
|
49
|
+
if (!fs.existsSync(value))
|
|
50
|
+
throw new AppError("harness_command_missing", "Configured harness command does not exist", 1, { harness: key, command, value });
|
|
51
|
+
return value;
|
|
52
|
+
}
|
|
53
|
+
/** The adapter interface is intentionally small; native binary flag names stay here,
|
|
54
|
+
* rather than leaking through MERGE orchestration or agent prompts. */
|
|
55
|
+
export function harnessRuntimeArguments(ddFlowHome, harness) {
|
|
56
|
+
const key = harnessKey(harness);
|
|
57
|
+
const runtime = resolveHarnessCommand(ddFlowHome, key, "runtime");
|
|
58
|
+
const option = {
|
|
59
|
+
"codex-desktop": "--codex-bin",
|
|
60
|
+
"zcode-acp": "--zcode-acp-bin",
|
|
61
|
+
"grok-acp": "--grok-bin",
|
|
62
|
+
"antigravity-cli": "--agy-bin",
|
|
63
|
+
"opencode-server": "--opencode-bin"
|
|
64
|
+
};
|
|
65
|
+
return [option[key], runtime];
|
|
66
|
+
}
|
|
@@ -5,6 +5,7 @@ import { AppError } from "../shared/errors.js";
|
|
|
5
5
|
import { flowCommand } from "./stage-pause.js";
|
|
6
6
|
import { validateSchema } from "./schema-validation.js";
|
|
7
7
|
import { adapterSessionId, runHarnessAdapter } from "./harness-adapter.js";
|
|
8
|
+
import { harnessRuntimeArguments, resolveHarnessCommand } from "./harness-config.js";
|
|
8
9
|
/** A deterministic dispatcher; only the launched harness Session performs agent work. */
|
|
9
10
|
export async function serveMergeRequests(context, input) {
|
|
10
11
|
const running = context.db.get("SELECT server_id, pid FROM merge_servers WHERE status IN ('running','stopping') ORDER BY started_at LIMIT 1");
|
|
@@ -73,8 +74,8 @@ async function dispatch(context, input) {
|
|
|
73
74
|
fs.writeFileSync(promptFile, `Start the assigned MERGE stage now. Your first tool call must be this exact standalone command:\n\n${stageCommand}\n\nTrust and follow the complete stage packet it returns. Continue through merge apply and stage finish. Stop only when the stage reaches a terminal result or explicitly requests user/operator action.\n`);
|
|
74
75
|
event(input.journal, { type: "launch_intent", server_id: input.serverId, request_id: input.request.merge_request_id, token, profile: input.profile, prompt_file: promptFile, at: context.now() });
|
|
75
76
|
input.progress?.(`launching ${input.request.merge_request_id} with ${input.profile.id}`);
|
|
76
|
-
const executable = context.
|
|
77
|
-
const common = ["--state-dir", stateDir, "--journal", adapterJournal, "--cwd", input.request.target_workspace, "--provider", input.profile.provider, "--model", input.profile.model, "--reasoning", input.profile.reasoning, "--mode", input.profile.mode, "--permission", input.profile.permission, "--project-root", input.request.target_workspace, "--dd-flow-home", context.ddFlowHome, "--json"];
|
|
77
|
+
const executable = resolveHarnessCommand(context.ddFlowHome, input.profile.harness, "adapter");
|
|
78
|
+
const common = ["--state-dir", stateDir, "--journal", adapterJournal, "--cwd", input.request.target_workspace, "--provider", input.profile.provider, "--model", input.profile.model, "--reasoning", input.profile.reasoning, "--mode", input.profile.mode, "--permission", input.profile.permission, "--project-root", input.request.target_workspace, "--dd-flow-home", context.ddFlowHome, ...harnessRuntimeArguments(context.ddFlowHome, input.profile.harness), "--json"];
|
|
78
79
|
try {
|
|
79
80
|
const adapterCall = (args, timeoutMs, progressMessage) => runHarnessAdapter({ executable, args, env: context.env, ...(timeoutMs ? { timeoutMs } : {}), ...(input.progress ? { progress: input.progress } : {}), ...(progressMessage ? { progressMessage } : {}), onEvidence: (value) => event(input.journal, { type: "adapter_call", ...value }) });
|
|
80
81
|
await adapterCall(["daemon", "start", ...common]);
|
package/dist/services/prompts.js
CHANGED
|
@@ -136,7 +136,9 @@ export function renderWorkerPrompt(context, input) {
|
|
|
136
136
|
};
|
|
137
137
|
}
|
|
138
138
|
function runHomePath(run) {
|
|
139
|
-
|
|
139
|
+
if (!run.run_root)
|
|
140
|
+
throw new AppError("runtime_missing", "RUN has no artifact root", 1, { run_id: run.id });
|
|
141
|
+
return run.run_root;
|
|
140
142
|
}
|
|
141
143
|
function readWorkerTask(taskFile, runHome) {
|
|
142
144
|
const resolved = path.resolve(taskFile);
|
|
@@ -204,7 +206,7 @@ function protocolIdForRun(context, projectId, runId) {
|
|
|
204
206
|
return run.subject_id;
|
|
205
207
|
}
|
|
206
208
|
function requireRun(context, projectId, runId) {
|
|
207
|
-
const row = context.db.get("SELECT id, project_id, subject_type, subject_id, workspace_root,
|
|
209
|
+
const row = context.db.get("SELECT id, project_id, subject_type, subject_id, workspace_root, run_root, index_json FROM runs WHERE project_id = ? AND (id = ? OR short_id = ?)", [projectId, runId, runId]);
|
|
208
210
|
if (!row)
|
|
209
211
|
throw new AppError("not_found", `Run is not found: ${runId}`, 1);
|
|
210
212
|
return row;
|
|
@@ -2,36 +2,18 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { AppError } from "../shared/errors.js";
|
|
4
4
|
export const runEngineBindingSchemaId = "dd-flow/run-engine-binding@1";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
/** Resolve a RUN from SQLite authority; never discover one by scanning storage paths. */
|
|
6
|
+
export function locateRunRoot(db, projectRoot, runIdOrAlias) {
|
|
7
|
+
const rows = db.all(`SELECT r.project_id, r.id AS run_id, r.run_root
|
|
8
|
+
FROM runs r JOIN projects p ON p.id = r.project_id
|
|
9
|
+
WHERE p.root = ? AND (r.id = ? OR r.short_id = ?)`, [realpathOrResolve(projectRoot), runIdOrAlias, runIdOrAlias]).filter((row) => Boolean(row.run_root));
|
|
10
|
+
if (rows.length > 1)
|
|
11
|
+
throw new AppError("ambiguous_id", `RUN id is ambiguous for project: ${runIdOrAlias}`, 2, { project_root: realpathOrResolve(projectRoot), candidates: rows.map((row) => row.run_id) });
|
|
12
|
+
const row = rows[0];
|
|
13
|
+
if (!row?.run_root)
|
|
8
14
|
return null;
|
|
9
|
-
const
|
|
10
|
-
|
|
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;
|
|
15
|
+
const runRoot = path.resolve(row.run_root);
|
|
16
|
+
return { project_id: row.project_id, run_id: row.run_id, run_root: runRoot, authority_path: path.join(runRoot, "run.json"), binding_path: path.join(runRoot, "engine-binding.json") };
|
|
35
17
|
}
|
|
36
18
|
export function readRunEngineBinding(file, options = {}) {
|
|
37
19
|
if (!fs.existsSync(file)) {
|
|
@@ -86,6 +68,14 @@ export function allRunEngineBindings(ddFlowHome) {
|
|
|
86
68
|
}
|
|
87
69
|
return bindings;
|
|
88
70
|
}
|
|
71
|
+
function isDirectory(value) {
|
|
72
|
+
try {
|
|
73
|
+
return fs.statSync(value).isDirectory();
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
89
79
|
function isRunEngineBinding(value) {
|
|
90
80
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
91
81
|
return false;
|
|
@@ -115,32 +105,6 @@ function sameEngine(left, right) {
|
|
|
115
105
|
&& left.integrity_checksum === right.integrity_checksum
|
|
116
106
|
&& left.snapshot_root === right.snapshot_root;
|
|
117
107
|
}
|
|
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
108
|
function realpathOrResolve(value) {
|
|
145
109
|
try {
|
|
146
110
|
return fs.realpathSync(value);
|
|
@@ -149,9 +113,3 @@ function realpathOrResolve(value) {
|
|
|
149
113
|
return path.resolve(value);
|
|
150
114
|
}
|
|
151
115
|
}
|
|
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
|
@@ -126,7 +126,7 @@ export function startFlowRun(context, input) {
|
|
|
126
126
|
};
|
|
127
127
|
ensureDir(path.dirname(runtimePath));
|
|
128
128
|
ensureDir(runHome);
|
|
129
|
-
bindCurrentEngineToRun(context, { projectRoot, runId, runHome });
|
|
129
|
+
bindCurrentEngineToRun(context, { projectRoot, runId, runRoot: runHome });
|
|
130
130
|
const persistedIndex = persistedVnextIndex(index);
|
|
131
131
|
writeJsonFile(runtimePath, runtimeSnapshotForIndex(persistedIndex, 1));
|
|
132
132
|
context.db.run(`INSERT INTO runs
|
|
@@ -292,6 +292,22 @@ export function setFlowRunConfig(context, input) {
|
|
|
292
292
|
appendRunTimeline(runArtifactRoot(run), { at: now, type: "run_config_set", run_id: run.id, key: input.key, value: mode, source, reason });
|
|
293
293
|
return { ok: true, run_id: run.id, settings: runSettings(index), idempotent: false };
|
|
294
294
|
}
|
|
295
|
+
/** Freeze an automatic review request once from already accepted policy or PLAN facts. */
|
|
296
|
+
export function freezeFlowRunReviewMode(context, input) {
|
|
297
|
+
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
298
|
+
const run = resolveRun(context, project.id, input.runId);
|
|
299
|
+
const index = authoritativeIndex(run);
|
|
300
|
+
const settings = runSettings(index);
|
|
301
|
+
const key = input.review === "plan" ? "plan_review" : "code_review";
|
|
302
|
+
const current = settings[key];
|
|
303
|
+
if (current.mode !== "auto")
|
|
304
|
+
return;
|
|
305
|
+
const now = context.now();
|
|
306
|
+
index.settings = { ...settings, [key]: { mode: input.mode, source: input.source, reason: requiredPlain(input.reason, "reason"), updated_at: now } };
|
|
307
|
+
index.updated_at = now;
|
|
308
|
+
persistRunState(context, project, run, index);
|
|
309
|
+
appendAudit(context, { projectId: project.id, eventType: "flow_run.review_mode_frozen", payload: { run_id: run.id, review: input.review, mode: input.mode, source: input.source, reason: input.reason } });
|
|
310
|
+
}
|
|
295
311
|
export function getFlowRunFlagsStatus(context, input) {
|
|
296
312
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
297
313
|
const run = resolveRun(context, project.id, input.runId);
|
|
@@ -1245,7 +1261,7 @@ function flowRunSummary(run) {
|
|
|
1245
1261
|
runtime_path: run.runtime_path,
|
|
1246
1262
|
run_dir: run.run_dir,
|
|
1247
1263
|
run_index_path: run.run_index_path,
|
|
1248
|
-
|
|
1264
|
+
run_root: run.run_root,
|
|
1249
1265
|
layout_version: run.layout_version ?? null,
|
|
1250
1266
|
artifact_root_kind: run.artifact_root_kind ?? null,
|
|
1251
1267
|
created_at: run.created_at,
|
|
@@ -1340,7 +1356,9 @@ function writeJsonFile(file, value) {
|
|
|
1340
1356
|
fs.renameSync(tmpFile, file);
|
|
1341
1357
|
}
|
|
1342
1358
|
function runArtifactRoot(run) {
|
|
1343
|
-
|
|
1359
|
+
if (!run.run_root)
|
|
1360
|
+
throw new AppError("runtime_missing", "RUN artifact root is unavailable", 1, { run_id: run.id });
|
|
1361
|
+
return run.run_root;
|
|
1344
1362
|
}
|
|
1345
1363
|
export function gitFacts(workspaceRoot) {
|
|
1346
1364
|
const read = (args) => {
|
|
@@ -6,7 +6,7 @@ import { Ajv } from "ajv/dist/ajv.js";
|
|
|
6
6
|
import { Ajv2020 } from "ajv/dist/2020.js";
|
|
7
7
|
import { normalizeFlowContract } from "../domain/flow-contract.js";
|
|
8
8
|
import { AppError } from "../shared/errors.js";
|
|
9
|
-
import {
|
|
9
|
+
import { readRunEngineBinding } from "./run-engine-bindings.js";
|
|
10
10
|
export function captureMemoryBankBaseline(input) {
|
|
11
11
|
const projectRoot = path.resolve(input.projectRoot);
|
|
12
12
|
const paths = normalizeMemoryBankPaths(projectRoot, input.paths);
|
|
@@ -117,8 +117,8 @@ export function validateSchema(options) {
|
|
|
117
117
|
}
|
|
118
118
|
const valid = validate(data);
|
|
119
119
|
const errors = valid ? [] : (validate.errors ?? []).map(formatAjvError);
|
|
120
|
-
const
|
|
121
|
-
const allErrors = [...errors, ...
|
|
120
|
+
const contractErrors = valid ? validateContractInvariants(options.schemaName, data) : [];
|
|
121
|
+
const allErrors = [...errors, ...contractErrors];
|
|
122
122
|
if (allErrors.length > 0) {
|
|
123
123
|
throw new AppError("schema_validation", `${path.basename(filePath)} does not match schema`, 2, {
|
|
124
124
|
schema: schemaResolution,
|
|
@@ -181,7 +181,7 @@ const historicalSchemaRegistry = {
|
|
|
181
181
|
}]
|
|
182
182
|
};
|
|
183
183
|
function resolveRunBoundSchema(options) {
|
|
184
|
-
if (!options.
|
|
184
|
+
if (!options.runRoot || options.schemaDir)
|
|
185
185
|
return null;
|
|
186
186
|
const data = readJson(path.resolve(options.file), "input file");
|
|
187
187
|
const root = asRecord(data);
|
|
@@ -190,13 +190,13 @@ function resolveRunBoundSchema(options) {
|
|
|
190
190
|
const runId = options.runId ?? inferredRunId;
|
|
191
191
|
if (typeof runId !== "string")
|
|
192
192
|
return null;
|
|
193
|
-
const
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
return null;
|
|
197
|
-
const binding = readRunEngineBinding(located.binding_path, { allowMissing: true });
|
|
193
|
+
const runRoot = path.resolve(options.runRoot);
|
|
194
|
+
const bindingPath = path.join(runRoot, "engine-binding.json");
|
|
195
|
+
const binding = readRunEngineBinding(bindingPath);
|
|
198
196
|
if (!binding)
|
|
199
|
-
|
|
197
|
+
throw new AppError("run_engine_binding_missing", "RUN engine binding is missing", 1, { path: bindingPath });
|
|
198
|
+
if (binding.run_id !== runId)
|
|
199
|
+
throw new AppError("run_engine_binding_invalid", "RUN engine binding belongs to another RUN", 1, { expected_run_id: runId, actual_run_id: binding.run_id, path: bindingPath });
|
|
200
200
|
const manifestPath = path.join(binding.engine.snapshot_root, "engine.json");
|
|
201
201
|
const manifest = readJson(manifestPath, "bound engine manifest");
|
|
202
202
|
const integrity = asRecord(objectValue(manifest, "integrity"));
|
|
@@ -284,7 +284,7 @@ function formatAjvError(error) {
|
|
|
284
284
|
keyword: error.keyword
|
|
285
285
|
};
|
|
286
286
|
}
|
|
287
|
-
function
|
|
287
|
+
function validateContractInvariants(schemaName, data) {
|
|
288
288
|
const root = asRecord(data);
|
|
289
289
|
if (!root) {
|
|
290
290
|
return [];
|
|
@@ -53,7 +53,9 @@ function bootstrapStageRun(context, projectRoot, input) {
|
|
|
53
53
|
if (!fs.existsSync(source) || !fs.statSync(source).isFile()) {
|
|
54
54
|
throw new AppError("not_found", "--intake-file must point to an existing file", 1, { intake_file: intakeFile });
|
|
55
55
|
}
|
|
56
|
-
|
|
56
|
+
if (!started.run.run_root)
|
|
57
|
+
throw new AppError("runtime_missing", "New RUN has no artifact root", 1, { run_id: started.run.id });
|
|
58
|
+
const intakeDir = path.join(started.run.run_root, "intake");
|
|
57
59
|
fs.mkdirSync(intakeDir, { recursive: true });
|
|
58
60
|
fs.copyFileSync(source, path.join(intakeDir, "user-request.md"));
|
|
59
61
|
}
|
|
@@ -216,7 +218,7 @@ export function startStage(context, input) {
|
|
|
216
218
|
})),
|
|
217
219
|
worker_prompt_markdown: prompt
|
|
218
220
|
});
|
|
219
|
-
validateSchema({ schemaName: "stage-prompt", file: promptDataPath, projectRoot, runId: attached.run.id, ddFlowHome: context.ddFlowHome });
|
|
221
|
+
validateSchema({ schemaName: "stage-prompt", file: promptDataPath, projectRoot, runId: attached.run.id, runRoot: runHome, ddFlowHome: context.ddFlowHome });
|
|
220
222
|
atomicWrite(path.join(stageRoot, "stage-start.json"), {
|
|
221
223
|
schema_id: "dd-flow/stage-start@2",
|
|
222
224
|
run_id: attached.run.id,
|
|
@@ -273,7 +275,7 @@ export function finishStage(context, input) {
|
|
|
273
275
|
const semanticFile = resolveStageFile(input.semanticFile ?? "@stage/stage-input.json", stageRoot, runHome);
|
|
274
276
|
const receipt = beginFinishReceipt(context, stageRoot, semanticFile, view.run.id, input.stage);
|
|
275
277
|
try {
|
|
276
|
-
validateSchema({ schemaName: "stage-finish-input", file: semanticFile, projectRoot, runId, ddFlowHome: context.ddFlowHome });
|
|
278
|
+
validateSchema({ schemaName: "stage-finish-input", file: semanticFile, projectRoot, runId, runRoot: runHome, ddFlowHome: context.ddFlowHome });
|
|
277
279
|
const semantic = readSemanticFile(semanticFile, runHome);
|
|
278
280
|
const status = stringValue(semantic.status) ?? "done";
|
|
279
281
|
if (!["done", "waiting_for_user", "blocked", "failed"].includes(status)) {
|
|
@@ -293,7 +295,7 @@ export function finishStage(context, input) {
|
|
|
293
295
|
const reportPath = path.join(stageRoot, "stage-report.md");
|
|
294
296
|
const htmlPath = path.join(stageRoot, "stage-report.html");
|
|
295
297
|
atomicWrite(dataPath, report);
|
|
296
|
-
validateSchema({ schemaName: stageReportSchemaName(input.stage, planFinish), file: dataPath, projectRoot, runId, ddFlowHome: context.ddFlowHome });
|
|
298
|
+
validateSchema({ schemaName: stageReportSchemaName(input.stage, planFinish), file: dataPath, projectRoot, runId, runRoot: runHome, ddFlowHome: context.ddFlowHome });
|
|
297
299
|
atomicWrite(reportPath, renderMarkdown(report));
|
|
298
300
|
atomicWrite(htmlPath, renderHtml(projectRoot, report));
|
|
299
301
|
if (view.run.subject.type === "protocol") {
|
|
@@ -410,7 +412,12 @@ function nextActionForProtocolStage(stage) {
|
|
|
410
412
|
return `continue_${stage}`;
|
|
411
413
|
}
|
|
412
414
|
function runHomePath(run) {
|
|
413
|
-
|
|
415
|
+
if (!run.run_root)
|
|
416
|
+
throw new AppError("runtime_missing", "RUN has no artifact root", 1, { run_id: run.id });
|
|
417
|
+
return run.run_root;
|
|
418
|
+
}
|
|
419
|
+
function runStatePath(run) {
|
|
420
|
+
return path.join(runHomePath(run), "run.json");
|
|
414
421
|
}
|
|
415
422
|
export function defaultStageDir(stage, flowKind) {
|
|
416
423
|
const normalized = stage === "implementation" ? "code" : stage;
|
|
@@ -702,7 +709,7 @@ function buildStageReport(context, view, stage, status, semantic, stageRoot, lin
|
|
|
702
709
|
if (stage === "code" || stage === "implementation") {
|
|
703
710
|
return {
|
|
704
711
|
schema_id: "dd-flow/code-stage-report@2",
|
|
705
|
-
run: { run_id: view.run.id, run_state: view.run
|
|
712
|
+
run: { run_id: view.run.id, run_state: runStatePath(view.run) },
|
|
706
713
|
stage: { name: stage, dir: path.basename(stageRoot), status },
|
|
707
714
|
project: { id: view.run.project_id, title: path.basename(view.run.project_root) },
|
|
708
715
|
subject: { id: view.run.subject.id, title: view.run.subject.id },
|
|
@@ -712,7 +719,7 @@ function buildStageReport(context, view, stage, status, semantic, stageRoot, lin
|
|
|
712
719
|
summary: result,
|
|
713
720
|
next_action: stringValue(semantic.next_action) ?? "Proceed to readiness."
|
|
714
721
|
},
|
|
715
|
-
breadcrumbs: [{ label: "RUN", href: view.run
|
|
722
|
+
breadcrumbs: [{ label: "RUN", href: runStatePath(view.run), status: "available" }],
|
|
716
723
|
implemented_goals: [{ title: `Stage ${stage}`, summary: result }],
|
|
717
724
|
acceptance_scenarios: acceptance.map((summary, index) => ({
|
|
718
725
|
id: `SCN-${stage}-${index + 1}`,
|
|
@@ -763,7 +770,7 @@ function buildStageReport(context, view, stage, status, semantic, stageRoot, lin
|
|
|
763
770
|
memory_bank_scope: "changed_files_and_links_only",
|
|
764
771
|
status: "passed"
|
|
765
772
|
},
|
|
766
|
-
breadcrumbs: [{ label: "RUN", href: view.run
|
|
773
|
+
breadcrumbs: [{ label: "RUN", href: runStatePath(view.run), status: "available" }]
|
|
767
774
|
};
|
|
768
775
|
}
|
|
769
776
|
function preparePlanFinish(context, projectRoot, view, stageRoot) {
|
|
@@ -167,7 +167,7 @@ export function flowCommand(context) {
|
|
|
167
167
|
return context.ddFlowHome === defaultHome ? "dd-flow" : `DD_FLOW_HOME=${JSON.stringify(context.ddFlowHome)} dd-flow`;
|
|
168
168
|
}
|
|
169
169
|
function requireRun(context, projectId, id) {
|
|
170
|
-
const rows = context.db.all("SELECT id, short_id, project_id, project_root,
|
|
170
|
+
const rows = context.db.all("SELECT id, short_id, project_id, project_root, run_root, index_json FROM runs WHERE project_id = ? AND (id = ? OR short_id = ?)", [projectId, id, id]);
|
|
171
171
|
if (rows.length !== 1)
|
|
172
172
|
throw new AppError(rows.length ? "ambiguous_alias" : "not_found", rows.length ? "RUN alias is ambiguous" : "RUN is not registered", 1, { run_id: id });
|
|
173
173
|
return rows[0];
|
|
@@ -186,9 +186,9 @@ function requireStage(run, stage, status) {
|
|
|
186
186
|
return found;
|
|
187
187
|
}
|
|
188
188
|
function requireRunHome(run) {
|
|
189
|
-
if (!run.
|
|
190
|
-
throw new AppError("runtime_missing", "RUN has no artifact
|
|
191
|
-
return run.
|
|
189
|
+
if (!run.run_root)
|
|
190
|
+
throw new AppError("runtime_missing", "RUN has no artifact root", 1, { run_id: run.id });
|
|
191
|
+
return run.run_root;
|
|
192
192
|
}
|
|
193
193
|
function nextPauseId(runHome) {
|
|
194
194
|
const root = path.join(runHome, "intake", "hitl");
|