@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 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
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "cli_package": "@deksden-com/dd-flow-cli",
3
- "cli_version": "0.9.0-beta.7",
4
- "cli_commit": "a4d857a7e4c5d9b5e9379a951073c499c21982ad",
5
- "built_at": "2026-09-03T14:01:18.896Z",
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": null,
8
- "commit": null,
9
- "flow_contract": null,
10
- "repo_root": null,
11
- "memorybank_root": null,
12
- "flow_root": null,
13
- "layout": null
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
  ],
@@ -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: requiredOption(parsed, "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") });
@@ -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: resolveDdFlowHome(env),
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
+ }
@@ -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, findRunHome, readRunEngineBinding, runEngineBindingSchemaId, writeRunEngineBinding } from "./run-engine-bindings.js";
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 = findRunHome(context.ddFlowHome, projectRoot, input.runId);
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.runHome, "engine-binding.json"), binding).binding;
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 = findRunHome(context.ddFlowHome, projectRoot, runId);
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
- const project = requireProjectByRoot(restored, projectRoot);
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
- const runHome = status.run.run_home_path ?? path.join(context.ddFlowHome, "projects", project.id, "runs", manifest.run_id);
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, run_home: started.run.run_home_path, target_stage: "specify" };
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.env[`DD_FLOW_${input.profile.harness.toUpperCase()}_ADAPTER`] ?? `dd-${input.profile.harness}`;
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]);
@@ -136,7 +136,9 @@ export function renderWorkerPrompt(context, input) {
136
136
  };
137
137
  }
138
138
  function runHomePath(run) {
139
- return run.run_home_path ?? path.dirname(run.run_index_path);
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, run_index_path, run_home_path, index_json FROM runs WHERE project_id = ? AND (id = ? OR short_id = ?)", [projectId, runId, runId]);
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
- export function findRunHome(ddFlowHome, projectRoot, runIdOrAlias) {
6
- const projectsRoot = path.join(ddFlowHome, "projects");
7
- if (!fs.existsSync(projectsRoot))
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 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;
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
- }
@@ -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
- run_home_path: run.run_home_path ?? null,
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
- return run.run_home_path ?? path.dirname(run.runtime_path);
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 { findRunHome, readRunEngineBinding } from "./run-engine-bindings.js";
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 semanticErrors = valid ? validateSemanticSchema(options.schemaName, data) : [];
121
- const allErrors = [...errors, ...semanticErrors];
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.projectRoot || options.schemaDir)
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 projectRoot = path.resolve(options.projectRoot);
194
- const located = findRunHome(options.ddFlowHome ?? process.env.DD_FLOW_HOME ?? path.join(path.dirname(projectRoot), ".dd-flow"), projectRoot, runId);
195
- if (!located || !isInside(located.run_home, path.resolve(options.file)))
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
- return null;
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 validateSemanticSchema(schemaName, data) {
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
- const intakeDir = path.join(started.run.run_home_path ?? path.dirname(started.run.id), "intake");
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
- return run.run_home_path ?? path.dirname(run.run_index_path);
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.run_index_path },
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.run_index_path, status: "available" }],
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.run_index_path, status: "available" }]
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, run_home_path, index_json FROM runs WHERE project_id = ? AND (id = ? OR short_id = ?)", [projectId, id, id]);
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.run_home_path)
190
- throw new AppError("runtime_missing", "RUN has no artifact home", 1, { run_id: run.id });
191
- return run.run_home_path;
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");