@deksden-com/dd-flow-cli 0.9.0-beta.87 → 0.9.0-beta.89

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.
@@ -1,8 +1,9 @@
1
1
  import { managedLifecycleCommand } from "./lifecycle-invocations.js";
2
+ import { bindStageWork, currentStageWorkIds, stageWorkIds } from "./stage-work-graph.js";
3
+ import { runtimeCommand } from "./runtime-command.js";
2
4
  import crypto from "node:crypto";
3
5
  import fs from "node:fs";
4
6
  import path from "node:path";
5
- import { quote } from "shell-quote";
6
7
  import { AppError } from "../shared/errors.js";
7
8
  import { entityReferenceVariants, shortEntityReference } from "../shared/entity-references.js";
8
9
  import { resolveWorkReference } from "../storage/work-references.js";
@@ -104,6 +105,8 @@ export function createChildWork(context, input) {
104
105
  const id = nextWorkId(context, parent.project_id, input.slug);
105
106
  const now = context.now();
106
107
  context.db.run(`INSERT INTO works (${workColumns}) VALUES (?, ?, ?, ?, ?, ?, ?, ?, '[]', 'created', NULL, ?, NULL, ?, NULL)`, [id, parent.project_id, parent.run_id, parent.work_id, input.task, input.launchPolicy, input.resultSchema ?? null, input.payload ? JSON.stringify(input.payload) : null, now, now]);
108
+ if (input.stage)
109
+ bindStageWork(context, { projectId: parent.project_id, runId: parent.run_id, workId: id, stage: input.stage });
107
110
  refreshRunWorkProjection(context, parent.project_id, parent.run_id);
108
111
  return requireWork(context, id);
109
112
  });
@@ -181,8 +184,14 @@ export function addWorkBatch(context, input) {
181
184
  for (const item of proposed)
182
185
  context.db.run(`INSERT INTO works (work_id, project_id, run_id, parent_work_id, task, launch_policy, result_schema, payload_json, depends_on_json, status, result, started_at, created_at, updated_at, completed_at)
183
186
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'created', NULL, NULL, ?, ?, NULL)`, [item.id, parent.project_id, parent.run_id, item.parentId, item.task, item.launch_policy ?? "reuse_allowed", item.result_schema ?? null, item.payload ? JSON.stringify(item.payload) : null, JSON.stringify(item.dependencies), now, now]);
184
- for (const item of proposed)
187
+ const run = context.db.get("SELECT index_json FROM runs WHERE project_id = ? AND id = ?", [parent.project_id, parent.run_id]);
188
+ const stages = run ? (JSON.parse(run.index_json).stage_runs ?? []) : [];
189
+ const ownerStage = input.stage ?? [...stages].reverse().find(stage => stage.status === "running")?.stage;
190
+ for (const item of proposed) {
191
+ if (ownerStage)
192
+ bindStageWork(context, { projectId: parent.project_id, runId: parent.run_id, workId: item.id, stage: ownerStage, ...(input.cycle ? { cycle: input.cycle } : {}) });
185
193
  appendFlowRunTimelineEvent(context, parent.project_id, parent.run_id, { type: "work_materialized", work_id: item.id, parent_work_id: item.parentId, depends_on: item.dependencies, launch_policy: item.launch_policy ?? "reuse_allowed" });
194
+ }
186
195
  refreshRunWorkProjection(context, parent.project_id, parent.run_id);
187
196
  return { ok: true, work_ids: Object.fromEntries(ids) };
188
197
  });
@@ -205,8 +214,8 @@ export function listWorks(context, input) {
205
214
  where.push("status = ?");
206
215
  params.push(input.status);
207
216
  }
208
- const rows = context.db.all(`SELECT ${workColumns} FROM works WHERE ${where.join(" AND ")} ORDER BY created_at, work_id`, params);
209
- const ready = (work) => isReady(context, work);
217
+ const rows = context.db.all(`SELECT ${workColumns} FROM works WHERE ${where.join(" AND ")} ORDER BY created_at, work_id`, params).filter(work => !input.workIds || input.workIds.has(work.work_id));
218
+ const ready = (work) => (input.workIds || currentStageWorkIds(context, work.project_id, work.run_id))?.has(work.work_id) !== false && isReady(context, work);
210
219
  const filtered = input.ready ? rows.filter(ready) : rows;
211
220
  return { works: filtered.slice(0, input.limit ?? filtered.length).map((work) => {
212
221
  const isReadyNow = ready(work);
@@ -382,7 +391,7 @@ export function shortWorkId(id) { return shortEntityReference(id); }
382
391
  * PATH is not inherited; using the shared lifecycle command keeps them on the
383
392
  * same captured runtime as their parent stage.
384
393
  */
385
- export function workStartCommand(context, work) { const run = requireRun(context, work.project_id, work.run_id); const recovery = recoveryGuard(context, work.project_id, work.run_id); return managedLifecycleCommand(context, `${flowCommand(context)} work start ${shortWorkId(work.work_id)} --project-root ${JSON.stringify(run.project_root)}${recovery ? ` --recovery-id ${JSON.stringify(recovery.recovery_id)}` : ""} --json`); }
394
+ export function workStartCommand(context, work) { const run = requireRun(context, work.project_id, work.run_id); const recovery = recoveryGuard(context, work.project_id, work.run_id); return managedLifecycleCommand(context, runtimeCommand(context, ["work", "start", shortWorkId(work.work_id), "--project-root", run.project_root, ...(recovery ? ["--recovery-id", recovery.recovery_id] : []), "--json"])); }
386
395
  export function assertWorkLaunchReady(context, id) {
387
396
  ensureWorkRegistry(context);
388
397
  const work = requireWork(context, id);
@@ -391,7 +400,7 @@ export function assertWorkLaunchReady(context, id) {
391
400
  throw new AppError("invalid_work_state", "Work is not created", 2, { work_id: id, status: work.status });
392
401
  if (!isReady(context, work)) {
393
402
  const blockers = readinessBlockers(context, work);
394
- const ready = context.db.all(`SELECT ${workColumns} FROM works WHERE project_id = ? AND run_id = ? AND status = 'created' ORDER BY created_at, work_id`, [work.project_id, work.run_id]).filter((candidate) => isReady(context, candidate)).map((candidate) => ({ work_id: candidate.work_id, start_command: workStartCommand(context, candidate) }));
403
+ const ready = context.db.all(`SELECT ${workColumns} FROM works WHERE project_id = ? AND run_id = ? AND status = 'created' ORDER BY created_at, work_id`, [work.project_id, work.run_id]).filter((candidate) => currentStageWorkIds(context, candidate.project_id, candidate.run_id)?.has(candidate.work_id) !== false && isReady(context, candidate)).map((candidate) => ({ work_id: candidate.work_id, start_command: workStartCommand(context, candidate) }));
395
404
  throw new AppError("work_not_ready", "Work cannot start until dependencies complete and overlapping planned coordination areas are free", 2, { work_id: id, blockers, ready });
396
405
  }
397
406
  return work;
@@ -976,8 +985,8 @@ function renderWorkerPrompt(context, work, run, dependencies) {
976
985
  const command = flowCommand(context);
977
986
  const packet = codePacket(work);
978
987
  const resultInputPath = path.join(requireRunHome(run), "works", work.work_id, "result-input.json");
979
- const finishWorkCommand = managedLifecycleCommand(context, `${command} work finish ${work.work_id} --result-file ${quote([resultInputPath])} --project-root ${quote([run.project_root])} --json --progress-jsonl`);
980
- const failWorkCommand = managedLifecycleCommand(context, `${command} work fail ${shortWorkId(work.work_id)} --reason "receipt path + exact external or semantic blocker" --project-root ${JSON.stringify(run.project_root)} --json`);
988
+ const finishWorkCommand = managedLifecycleCommand(context, runtimeCommand(context, ["work", "finish", work.work_id, "--result-file", resultInputPath, "--project-root", run.project_root, "--json", "--progress-jsonl"]));
989
+ const failWorkCommand = managedLifecycleCommand(context, runtimeCommand(context, ["work", "fail", shortWorkId(work.work_id), "--reason", "receipt path + exact external or semantic blocker", "--project-root", run.project_root, "--json"]));
981
990
  const mergeWork = parsePayload(work)?.kind === "merge";
982
991
  const writeBoundary = mergeWork
983
992
  ? ["MERGE does not own product-code repair. Use only the stage packet's merge apply command; resolve only paths that Git reports as unmerged. Do not edit integration code, tests, documentation or configuration to make a gate pass.", "RUN artifacts are read-only evidence: refer to them with run:// URIs and let dd-flow persist your submitted result."]
@@ -1163,7 +1172,7 @@ export function refreshRunWorkProjection(context, projectId, runId) { refreshRun
1163
1172
  if (fs.existsSync(obsolete))
1164
1173
  fs.rmSync(obsolete);
1165
1174
  } }
1166
- export function codeWorkGraph(context, projectId, runId) { const works = context.db.all(`SELECT ${workColumns} FROM works WHERE project_id = ? AND run_id = ? ORDER BY created_at, work_id`, [projectId, runId]).filter((work) => Boolean(codePacket(work))); const ready = works.filter((work) => isReady(context, work)); return { total: works.length, created: works.filter((work) => work.status === "created").length, running: works.filter((work) => work.status === "running").length, completed: works.filter((work) => work.status === "completed").length, failed: works.filter((work) => work.status === "failed").length, ready: ready.map((work) => ({ work_id: work.work_id, task: work.task, launch_policy: work.launch_policy, start_command: workStartCommand(context, work) })), blocked: works.filter((work) => work.status === "created" && !ready.includes(work)).map((work) => ({ work_id: work.work_id, depends_on: parseDependencies(work) })) }; }
1175
+ export function codeWorkGraph(context, projectId, runId) { const index = JSON.parse(context.db.get("SELECT index_json FROM runs WHERE project_id = ? AND id = ?", [projectId, runId])?.index_json ?? "{}"); const active = index.stage_runs?.some(item => item.stage === "code-review" && item.status === "running") ? "code-review" : "code"; const members = index.stage_runs?.length ? stageWorkIds(context, projectId, runId, active) : null; const works = context.db.all(`SELECT ${workColumns} FROM works WHERE project_id = ? AND run_id = ? ORDER BY created_at, work_id`, [projectId, runId]).filter((work) => Boolean(codePacket(work)) && (!members || members.has(work.work_id))); const ready = works.filter((work) => isReady(context, work)); return { total: works.length, created: works.filter((work) => work.status === "created").length, running: works.filter((work) => work.status === "running").length, completed: works.filter((work) => work.status === "completed").length, failed: works.filter((work) => work.status === "failed").length, ready: ready.map((work) => ({ work_id: work.work_id, task: work.task, launch_policy: work.launch_policy, start_command: workStartCommand(context, work) })), blocked: works.filter((work) => work.status === "created" && !ready.includes(work)).map((work) => ({ work_id: work.work_id, depends_on: parseDependencies(work) })) }; }
1167
1176
  function parsePayload(work) { if (!work.payload_json)
1168
1177
  return null; try {
1169
1178
  const value = JSON.parse(work.payload_json);
@@ -5,6 +5,18 @@ import { createRequire } from "node:module";
5
5
  import { ensureDir } from "./paths.js";
6
6
  import { AppError } from "../shared/errors.js";
7
7
  import { executeStoreSchema, readStoreWriterContract, registerStoreWriter } from "./writer-contract.js";
8
+ const workContinuationStorageSql = `
9
+ CREATE TABLE IF NOT EXISTS stage_work_memberships (
10
+ project_id TEXT NOT NULL, run_id TEXT NOT NULL, work_id TEXT NOT NULL REFERENCES works(work_id),
11
+ stage TEXT NOT NULL, attempt TEXT NOT NULL, cycle TEXT NOT NULL DEFAULT '',
12
+ PRIMARY KEY(project_id, run_id, work_id, stage, attempt, cycle)
13
+ );
14
+ CREATE TABLE IF NOT EXISTS repair_intents (
15
+ identity TEXT PRIMARY KEY, project_id TEXT NOT NULL, run_id TEXT NOT NULL,
16
+ stage TEXT NOT NULL, attempt TEXT NOT NULL, payload_hash TEXT NOT NULL,
17
+ payload_json TEXT NOT NULL, work_id TEXT NOT NULL REFERENCES works(work_id), created_at TEXT NOT NULL
18
+ );
19
+ `;
8
20
  const require = createRequire(import.meta.url);
9
21
  const { DatabaseSync } = require("node:sqlite");
10
22
  const resourceDatabases = new Map();
@@ -210,6 +222,7 @@ export function ensureVnextWorkStorage(db) {
210
222
  FOREIGN KEY(work_id) REFERENCES works(work_id)
211
223
  );
212
224
  CREATE INDEX IF NOT EXISTS idx_check_receipts_run ON check_receipts(project_id, run_id, started_at);
225
+ ${workContinuationStorageSql}
213
226
  `);
214
227
  // A committed Work start is authoritative even if publishing its prompt
215
228
  // files is interrupted after commit. Keep the exact bytes for repair.
@@ -949,6 +962,8 @@ function migrate(db) {
949
962
  CREATE INDEX IF NOT EXISTS idx_check_receipts_run
950
963
  ON check_receipts(project_id, run_id, started_at);
951
964
 
965
+ ${workContinuationStorageSql}
966
+
952
967
  CREATE TABLE IF NOT EXISTS merge_requests (
953
968
  merge_request_id TEXT PRIMARY KEY,
954
969
  project_id TEXT NOT NULL,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deksden-com/dd-flow-cli",
3
- "version": "0.9.0-beta.87",
3
+ "version": "0.9.0-beta.89",
4
4
  "description": "Mechanical runtime CLI for dd-flow workflows.",
5
5
  "type": "module",
6
6
  "bin": {