@deksden-com/dd-flow-cli 0.9.0-beta.1 → 0.9.0-beta.7

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,54 @@
1
1
  # @deksden-com/dd-flow-cli
2
2
 
3
+ ## 0.9.0-beta.7
4
+
5
+ ### Patch Changes
6
+
7
+ - 4621387: Expose public Session receipts and cleanup actions as native
8
+ harness/Session pairs without SQLite storage keys. Keep a freshly reserved
9
+ check receipt owned while its process is initializing, so a concurrent caller
10
+ cannot incorrectly abort it.
11
+
12
+ ## 0.9.0-beta.6
13
+
14
+ ### Patch Changes
15
+
16
+ - 781274c: Materialize a causal CODE repair Work when the coordinator's semantic
17
+ verification lists unresolved accepted obligations, rather than leaving CODE
18
+ running after a rejected finish.
19
+
20
+ ## 0.9.0-beta.5
21
+
22
+ ### Patch Changes
23
+
24
+ - Keep every accepted original CODE check eligible for a later causal repair, so
25
+ a prior repair cannot make another failed aggregate check impossible to fix.
26
+
27
+ ## 0.9.0-beta.4
28
+
29
+ ### Patch Changes
30
+
31
+ - Persist an atomic check completion marker so a later CLI invocation can recover
32
+ the actual terminal result after its original caller exits.
33
+
34
+ ## 0.9.0-beta.3
35
+
36
+ ### Patch Changes
37
+
38
+ - 0bd32f6: Expose a durable host resource registry for managed processes, preserve public
39
+ native Session identity separately from internal storage keys, and safely
40
+ reconcile interrupted check receipts.
41
+
42
+ ## 0.9.0-beta.2
43
+
44
+ ### Patch Changes
45
+
46
+ - Make CODE and CODE-REVIEW terminal transitions create their MERGE request
47
+ before persisting a completed stage, and validate MERGE prerequisites before
48
+ freezing the source commit. Upgrade project check profiles to
49
+ `dd-flow/code-check-profile@6`: aliases may declare named local ports, which
50
+ the CLI allocates per check invocation and records in its receipt.
51
+
3
52
  ## 0.9.0-beta.1
4
53
 
5
54
  ### Patch Changes
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "cli_package": "@deksden-com/dd-flow-cli",
3
- "cli_version": "0.9.0-beta.1",
4
- "cli_commit": "5e493443b9f97ad3c83e0edccc29dfbee584b92e",
5
- "built_at": "2026-09-01T21:02:46.536Z",
3
+ "cli_version": "0.9.0-beta.7",
4
+ "cli_commit": "a4d857a7e4c5d9b5e9379a951073c499c21982ad",
5
+ "built_at": "2026-09-03T14:01:18.896Z",
6
6
  "built_with_canon": {
7
- "version": "4.0.0",
8
- "commit": "bd6494ad11bca9544d9b483bd7fe37049e6d00fe",
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"
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
14
14
  }
15
15
  }
package/dist/cli/help.js CHANGED
@@ -204,9 +204,9 @@ Usage:
204
204
  dd-flow work show <WORK-ID> --json
205
205
  dd-flow work start <WORK-ID> --project-root <root> --json
206
206
  dd-flow work finish <WORK-ID> (--result-file <path>|--result-stdin) --project-root <root> --json
207
- dd-flow work repair add --run <RUN-ID> --from-check <CHECK-ID> --origin-work <WORK-ID>... --task-stdin --project-root <root> --json
207
+ dd-flow work repair add --run <RUN-ID> (--from-check <CHECK-ID>|--from-unresolved <gap> --verification-file <file>) --origin-work <WORK-ID>... --task-stdin --project-root <root> --json
208
208
 
209
- CODE workers receive their full accepted packet from work start. work finish validates the result and executes the packet's focused checks before completion. A failed aggregate CODE gate returns the exact failed receipt and repair command; repair add creates a new Work from the selected completed origin context plus that failure evidence. Its planned write areas are coordination hints only; the repair may change any necessary project file inside its RUN workspace.`
209
+ CODE workers receive their full accepted packet from work start. work finish validates the result and executes the packet's focused checks before completion. A failed aggregate CODE gate or unresolved semantic verification creates a repair Work from the selected completed origin context and retained evidence. Its planned write areas are coordination hints only; the repair may change any necessary project file inside its RUN workspace.`
210
210
  ],
211
211
  [
212
212
  "id",
@@ -50,6 +50,7 @@ import { finishStage, startStage } from "../services/stage-lifecycle.js";
50
50
  import { addWorkBatch, cancelWork, deleteWork, failWork, finishWork, listWorks, mutateWorkDeps, retryWork, showWork, startWork } from "../services/work-registry.js";
51
51
  import { createEvalBootstrapSnapshot, createEvalRunSnapshot, prepareVnextSpecifyRun, restoreEvalBootstrapSnapshot, restoreEvalRunSnapshot } from "../services/eval-snapshots.js";
52
52
  import { loadExternalStageContext } from "../services/stage-context.js";
53
+ import { confirmManagedProcess, finishManagedProcess, heartbeatManagedProcess, managedProcessStatus, reconcileExpiredManagedProcesses, registerManagedProcess } from "../services/managed-processes.js";
53
54
  const defaultIo = {
54
55
  stdout: process.stdout,
55
56
  stderr: process.stderr,
@@ -88,7 +89,8 @@ export async function runCli(args, io = defaultIo, env = process.env) {
88
89
  heartbeat = setInterval(() => writeProgressJsonl(io, "running", "Command is still running"), 15_000);
89
90
  }
90
91
  const scopeProjectRoot = resolveProjectRootBeforeRouting(output.args, env);
91
- const routed = await routeArgsThroughEngine(routerContext, output.json ? [...output.args, "--json", ...(output.progressJsonl ? ["--progress-jsonl"] : [])] : output.args, io, io.stdin, env, scopeProjectRoot);
92
+ const routedArgs = output.responseFile ? [...output.args, "--response-file", output.responseFile] : output.args;
93
+ const routed = await routeArgsThroughEngine(routerContext, output.json ? [...routedArgs, "--json", ...(output.progressJsonl ? ["--progress-jsonl"] : [])] : routedArgs, io, io.stdin, env, scopeProjectRoot);
92
94
  if (routed?.routed && !routed.inProcess) {
93
95
  if (heartbeat)
94
96
  clearInterval(heartbeat);
@@ -108,8 +110,9 @@ export async function runCli(args, io = defaultIo, env = process.env) {
108
110
  writeProgressJsonl(io, "done", "Command finished");
109
111
  emitHumanProgress(io, output, progress, "done");
110
112
  const payload = attachProgress(resultWithWarnings, progress, output);
113
+ const visible = writeResponseFile(payload, output);
111
114
  if (output.json)
112
- writeJson(io.stdout, payload);
115
+ writeJson(io.stdout, visible);
113
116
  else
114
117
  io.stdout.write(renderHumanResult(output.args, payload));
115
118
  return completedCommandExitCode(payload);
@@ -124,10 +127,11 @@ export async function runCli(args, io = defaultIo, env = process.env) {
124
127
  if (output.progressJsonl)
125
128
  writeProgressJsonl(io, "failed", payload.error.message, { code: payload.error.code });
126
129
  const errorPayload = attachProgress(payload, progress, output);
130
+ const visible = writeResponseFile(errorPayload, output);
127
131
  if (output.json && output.progressJsonl)
128
132
  writeJson(io.stdout, errorPayload);
129
133
  else if (output.json)
130
- writeJson(io.stderr, errorPayload);
134
+ writeJson(io.stderr, visible);
131
135
  else
132
136
  io.stderr.write(renderHumanError(payload));
133
137
  return isAppError(error) ? error.exitCode : 1;
@@ -189,7 +193,11 @@ function parseOutputOptions(args) {
189
193
  const stripped = [];
190
194
  let json = false;
191
195
  let progressJsonl = false;
192
- for (const arg of args) {
196
+ let responseFile = null;
197
+ for (let index = 0; index < args.length; index += 1) {
198
+ const arg = args[index];
199
+ if (arg === undefined)
200
+ continue;
193
201
  if (arg === "--json") {
194
202
  json = true;
195
203
  continue;
@@ -198,10 +206,44 @@ function parseOutputOptions(args) {
198
206
  progressJsonl = true;
199
207
  continue;
200
208
  }
209
+ if (arg === "--response-file") {
210
+ const file = args[++index];
211
+ if (!file)
212
+ throw new AppError("usage", "--response-file requires a path", 2);
213
+ responseFile = file;
214
+ continue;
215
+ }
201
216
  stripped.push(arg);
202
217
  }
218
+ if (responseFile && !json)
219
+ throw new AppError("usage", "--response-file requires --json", 2);
203
220
  const hookJsonOnly = stripped[0] === "codex" && stripped[1] === "hook" && stripped[2] === "handle";
204
- return { json: json || hookJsonOnly, progressJsonl, args: stripped, hookJsonOnly };
221
+ return { json: json || hookJsonOnly, progressJsonl, args: stripped, hookJsonOnly, responseFile };
222
+ }
223
+ /** Persist the complete machine response for harnesses that cap tool output.
224
+ * The visible response keeps routing fields but omits the potentially large
225
+ * rendered prompt; an agent can safely read the named file without rerunning
226
+ * the lifecycle command. */
227
+ function writeResponseFile(payload, output) {
228
+ if (!output.responseFile)
229
+ return payload;
230
+ const file = path.resolve(output.responseFile);
231
+ try {
232
+ fs.mkdirSync(path.dirname(file), { recursive: true });
233
+ const temporary = `${file}.tmp-${process.pid}-${crypto.randomUUID()}`;
234
+ fs.writeFileSync(temporary, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
235
+ fs.renameSync(temporary, file);
236
+ }
237
+ catch (error) {
238
+ // The lifecycle mutation may already be accepted. Do not retry it just
239
+ // because its optional full-response file could not be written.
240
+ return { ...(payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { ok: true, result: payload }), response_file: file, response_file_error: String(error) };
241
+ }
242
+ if (!payload || typeof payload !== "object" || Array.isArray(payload))
243
+ return { ok: true, response_file: file };
244
+ const visible = { ...payload, response_file: file };
245
+ delete visible.worker_prompt_markdown;
246
+ return visible;
205
247
  }
206
248
  function progressForCommand(args) {
207
249
  const [family, command, subcommand] = args;
@@ -474,6 +516,9 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
474
516
  if (family === "version") {
475
517
  return getCliVersionReport();
476
518
  }
519
+ if (family === "runtime") {
520
+ return await dispatchRuntime(context, command, parsed);
521
+ }
477
522
  if (family === "canon") {
478
523
  return dispatchCanon(context, command, parsed);
479
524
  }
@@ -662,8 +707,9 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
662
707
  throw new AppError("usage", "work repair add requires --task-stdin", 2);
663
708
  const origins = parsed.options.get("origin-work") ?? [];
664
709
  const findings = parsed.options.get("from-finding") ?? [];
710
+ const unresolved = parsed.options.get("from-unresolved") ?? [];
665
711
  if (!origins.length && !findings.length)
666
- throw new AppError("usage", "work repair add requires --from-check with --origin-work or --from-finding", 2);
712
+ throw new AppError("usage", "work repair add requires --from-check with --origin-work, --from-finding, or --from-unresolved with --origin-work", 2);
667
713
  if (findings.length)
668
714
  return addVnextCodeReviewRepair(context, {
669
715
  projectRoot: requiredScopeProjectRoot(scopeProjectRoot, "work repair add"),
@@ -672,14 +718,30 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
672
718
  checkRefsByFinding: Object.fromEntries(findings.map((finding) => [finding, parsed.options.get("check-ref") ?? []])),
673
719
  objective: await readStdin(io.stdin)
674
720
  });
721
+ if (unresolved.length) {
722
+ if (!origins.length)
723
+ throw new AppError("usage", "semantic repair requires at least one --origin-work", 2);
724
+ const verificationPath = requiredOption(parsed, "verification-file");
725
+ return addVnextCodeRepair(context, {
726
+ projectRoot: requiredScopeProjectRoot(scopeProjectRoot, "work repair add"),
727
+ runId: requiredOption(parsed, "run"),
728
+ originWorkIds: origins,
729
+ semanticUnresolved: unresolved,
730
+ verificationPath,
731
+ objective: await readStdin(io.stdin)
732
+ });
733
+ }
675
734
  if (!hasOption(parsed, "from-check"))
676
735
  throw new AppError("usage", "check repair requires --from-check", 2);
677
736
  if (!origins.length)
678
737
  throw new AppError("usage", "check repair requires at least one --origin-work", 2);
738
+ const receipts = parsed.options.get("from-check") ?? [];
739
+ if (receipts.length !== 1)
740
+ throw new AppError("usage", "check repair accepts exactly one --from-check; create one causal repair Work per failed receipt", 2);
679
741
  return addVnextCodeRepair(context, {
680
742
  projectRoot: requiredScopeProjectRoot(scopeProjectRoot, "work repair add"),
681
743
  runId: requiredOption(parsed, "run"),
682
- checkReceiptId: requiredOption(parsed, "from-check"),
744
+ checkReceiptId: receipts[0],
683
745
  originWorkIds: origins,
684
746
  objective: await readStdin(io.stdin)
685
747
  });
@@ -1519,6 +1581,42 @@ function dispatchRun(context, command, parsed) {
1519
1581
  }
1520
1582
  throw new AppError("usage", `Unknown run command: ${command ?? "<empty>"}`, 2);
1521
1583
  }
1584
+ async function dispatchRuntime(context, command, parsed) {
1585
+ if (command !== "process")
1586
+ throw new AppError("usage", "Usage: dd-flow runtime process <register|confirm|heartbeat|finish|status|reconcile>", 2);
1587
+ const action = requiredPosition(parsed, 0, "runtime process action");
1588
+ if (action === "register") {
1589
+ const leaseMs = optionalPositiveNumber(parsed, "lease-ms", true);
1590
+ const process = registerManagedProcess(context, {
1591
+ kind: requiredOption(parsed, "kind"), ownerId: requiredOption(parsed, "owner"),
1592
+ projectId: optionalOption(parsed, "project-id") ?? null, runId: optionalOption(parsed, "run") ?? null,
1593
+ workId: optionalOption(parsed, "work") ?? null, checkId: optionalOption(parsed, "check") ?? null,
1594
+ operationId: optionalOption(parsed, "operation") ?? null, stdoutPath: optionalOption(parsed, "stdout") ?? null,
1595
+ stderrPath: optionalOption(parsed, "stderr") ?? null, ...(leaseMs ? { leaseMs } : {})
1596
+ });
1597
+ return { ok: true, process };
1598
+ }
1599
+ if (action === "confirm") {
1600
+ const leaseMs = optionalPositiveNumber(parsed, "lease-ms", true);
1601
+ const processGroupId = optionalPositiveNumber(parsed, "process-group-id", true);
1602
+ return { ok: true, process: confirmManagedProcess(context, { id: requiredOption(parsed, "id"), leaseToken: requiredOption(parsed, "lease-token"), pid: optionalPositiveNumber(parsed, "pid", true) ?? 0, ...(processGroupId ? { processGroupId } : {}), ...(leaseMs ? { leaseMs } : {}) }) };
1603
+ }
1604
+ if (action === "heartbeat") {
1605
+ const leaseMs = optionalPositiveNumber(parsed, "lease-ms", true);
1606
+ return { ok: heartbeatManagedProcess(context, { id: requiredOption(parsed, "id"), leaseToken: requiredOption(parsed, "lease-token"), ...(leaseMs ? { leaseMs } : {}) }) };
1607
+ }
1608
+ if (action === "finish") {
1609
+ const state = requiredOption(parsed, "state");
1610
+ if (state !== "stopped" && state !== "failed")
1611
+ throw new AppError("validation", "--state must be stopped or failed", 2);
1612
+ return { ok: finishManagedProcess(context, { id: requiredOption(parsed, "id"), leaseToken: requiredOption(parsed, "lease-token"), state: state, reason: optionalOption(parsed, "reason") ?? null }) };
1613
+ }
1614
+ if (action === "status")
1615
+ return { ok: true, processes: managedProcessStatus(context) };
1616
+ if (action === "reconcile")
1617
+ return { ok: true, processes: await reconcileExpiredManagedProcesses(context, requiredOption(parsed, "owner"), optionalPositiveNumber(parsed, "grace-ms", true)) };
1618
+ throw new AppError("usage", "Usage: dd-flow runtime process <register|confirm|heartbeat|finish|status|reconcile>", 2);
1619
+ }
1522
1620
  function dispatchStat(context, command, parsed) {
1523
1621
  if (command === "usage") {
1524
1622
  return getStoredFlowRunUsage(context, {
@@ -16,9 +16,9 @@
16
16
  "spine": {"type": "object", "additionalProperties": false, "required": ["user_outcome", "component_responsibility", "must_preserve", "non_goals", "acceptance_contribution"], "properties": {"user_outcome": {"type": "string", "minLength": 1}, "component_responsibility": {"type": "string", "minLength": 1}, "must_preserve": {"$ref": "#/$defs/strings"}, "non_goals": {"$ref": "#/$defs/stringList"}, "acceptance_contribution": {"type": "string", "minLength": 1}}},
17
17
  "obligation": {"type": "object", "additionalProperties": false, "required": ["id", "statement"], "properties": {"id": {"type": "string", "minLength": 1}, "statement": {"type": "string", "minLength": 1}}},
18
18
  "acceptance": {"type": "object", "additionalProperties": false, "required": ["criterion_id", "path", "environment", "fixtures", "cleanup", "check_refs", "expected_evidence", "proof_limits", "gate"], "properties": {"criterion_id": {"type": "string", "pattern": "^AC-[0-9]+$"}, "path": {"type": "string", "minLength": 1}, "environment": {"type": "string", "minLength": 1}, "fixtures": {"$ref": "#/$defs/stringList"}, "cleanup": {"type": "string", "minLength": 1}, "check_refs": {"type": "array", "uniqueItems": true, "items": {"type": "string", "minLength": 1}}, "expected_evidence": {"$ref": "#/$defs/strings"}, "proof_limits": {"$ref": "#/$defs/strings"}, "gate": {"type": "string", "minLength": 1}}},
19
- "check": {"type": "object", "additionalProperties": false, "required": ["id", "command", "purpose", "run_at", "availability"], "properties": {"id": {"type": "string", "pattern": "^CHK-[A-Za-z0-9-]+$"}, "command": {"type": "string", "minLength": 1}, "purpose": {"type": "string", "minLength": 1}, "run_at": {"enum": ["work", "code", "readiness", "merge", "release", "external"]}, "availability": {"enum": ["available", "planned"]}, "provided_by": {"type": "string", "minLength": 1}, "definition": {"type": "string", "minLength": 1}, "required_artifacts": {"type": "array", "uniqueItems": true, "items": {"type": "string", "minLength": 1}}}, "allOf": [{"if": {"properties": {"availability": {"const": "planned"}}, "required": ["availability"]}, "then": {"required": ["provided_by", "definition"], "properties": {"command": {"pattern": "^@check/"}}}}]},
19
+ "check": {"type": "object", "additionalProperties": false, "required": ["id", "command", "purpose", "run_at", "availability"], "properties": {"id": {"type": "string", "pattern": "^CHK-[A-Za-z0-9-]+$"}, "command": {"type": "string", "minLength": 1}, "purpose": {"type": "string", "minLength": 1}, "run_at": {"enum": ["work", "code", "readiness", "merge", "release", "external"]}, "availability": {"enum": ["available", "planned"]}, "provided_by": {"type": "string", "minLength": 1}, "definition": {"type": "string", "minLength": 1}, "required_artifacts": {"type": "array", "uniqueItems": true, "items": {"type": "string", "minLength": 1}}, "ports": {"type": "array", "uniqueItems": true, "items": {"type": "string", "pattern": "^[A-Za-z][A-Za-z0-9_]*$"}}}, "allOf": [{"if": {"properties": {"availability": {"const": "planned"}}, "required": ["availability"]}, "then": {"required": ["provided_by", "definition"], "properties": {"command": {"pattern": "^@check/"}}}}]},
20
20
  "documentUpdate": {"type": "object", "additionalProperties": false, "required": ["path", "action", "owner", "reason", "baseline_sha256"], "properties": {"path": {"type": "string", "minLength": 1}, "action": {"enum": ["create", "update"]}, "owner": {"type": "string", "minLength": 1}, "reason": {"type": "string", "minLength": 1}, "baseline_sha256": {"type": ["string", "null"], "pattern": "^[a-f0-9]{64}$"}}},
21
- "repair": {"type": "object", "additionalProperties": false, "required": ["origin_work_ids"], "anyOf": [{"required": ["check_receipt_id", "failure_receipt_path"]}, {"required": ["review_finding_ids", "review_evidence_refs"]}], "properties": {"origin_work_ids": {"$ref": "#/$defs/strings"}, "check_receipt_id": {"type": "string", "minLength": 1}, "failure_receipt_path": {"type": "string", "minLength": 1}, "review_finding_ids": {"$ref": "#/$defs/strings"}, "review_evidence_refs": {"$ref": "#/$defs/strings"}}},
22
- "work": {"type": "object", "additionalProperties": false, "required": ["schema_id", "key", "launch_policy", "source", "task", "semantic_spine", "requirements", "acceptance", "document_updates", "required_read", "discovery_boundary", "planned_write_areas", "checks", "provides_checks", "stop_conditions", "depends_on", "result_schema"], "properties": {"schema_id": {"const": "dd-flow/code-work-packet@5"}, "key": {"type": "string", "minLength": 1}, "launch_policy": {"const": "fresh_agent_required"}, "source": {"$ref": "#/$defs/source"}, "repair": {"$ref": "#/$defs/repair"}, "task": {"type": "string", "minLength": 1}, "semantic_spine": {"$ref": "#/$defs/spine"}, "requirements": {"type": "array", "minItems": 1, "items": {"$ref": "#/$defs/obligation"}}, "acceptance": {"type": "array", "items": {"$ref": "#/$defs/acceptance"}}, "document_updates": {"type": "array", "items": {"$ref": "#/$defs/documentUpdate"}}, "required_read": {"$ref": "#/$defs/strings", "description": "Mandatory starting sources, not a read allowlist."}, "discovery_boundary": {"$ref": "#/$defs/strings", "description": "Likely discovery areas; additional project-local reads are allowed."}, "planned_write_areas": {"type": "array", "items": {"type": "string", "minLength": 1}, "description": "Soft coordination hints only. A worker may change any necessary project file inside the RUN workspace."}, "checks": {"type": "array", "minItems": 1, "items": {"$ref": "#/$defs/check"}}, "provides_checks": {"type": "array", "items": {"$ref": "#/$defs/check"}}, "stop_conditions": {"$ref": "#/$defs/strings", "description": "Hard semantic or external stop conditions; path prediction drift is not a stop condition."}, "depends_on": {"type": "array", "uniqueItems": true, "items": {"type": "string", "minLength": 1}}, "result_schema": {"const": "dd-flow/code-work-result@2"}}}
21
+ "repair": {"type": "object", "additionalProperties": false, "required": ["origin_work_ids"], "anyOf": [{"required": ["check_receipt_id", "failure_receipt_path"]}, {"required": ["review_finding_ids", "review_evidence_refs"]}, {"required": ["semantic_unresolved", "verification_path"]}], "properties": {"origin_work_ids": {"$ref": "#/$defs/strings"}, "check_receipt_id": {"type": "string", "minLength": 1}, "failure_receipt_path": {"type": "string", "minLength": 1}, "review_finding_ids": {"$ref": "#/$defs/strings"}, "review_evidence_refs": {"$ref": "#/$defs/strings"}, "review_check_refs": {"$ref": "#/$defs/stringList"}, "semantic_unresolved": {"$ref": "#/$defs/strings"}, "verification_path": {"type": "string", "minLength": 1}}},
22
+ "work": {"type": "object", "additionalProperties": false, "required": ["schema_id", "key", "launch_policy", "source", "task", "semantic_spine", "requirements", "acceptance", "document_updates", "required_read", "discovery_boundary", "planned_write_areas", "checks", "provides_checks", "stop_conditions", "depends_on", "result_schema"], "properties": {"schema_id": {"const": "dd-flow/code-work-packet@5"}, "key": {"type": "string", "minLength": 1}, "launch_policy": {"const": "fresh_agent_required"}, "source": {"$ref": "#/$defs/source"}, "repair": {"$ref": "#/$defs/repair"}, "task": {"type": "string", "minLength": 1}, "semantic_spine": {"$ref": "#/$defs/spine"}, "requirements": {"type": "array", "minItems": 1, "items": {"$ref": "#/$defs/obligation"}}, "acceptance": {"type": "array", "items": {"$ref": "#/$defs/acceptance"}}, "document_updates": {"type": "array", "items": {"$ref": "#/$defs/documentUpdate"}}, "required_read": {"$ref": "#/$defs/strings", "description": "Mandatory starting sources, not a read allowlist."}, "discovery_boundary": {"$ref": "#/$defs/strings", "description": "Likely discovery areas; additional project-local reads are allowed."}, "planned_write_areas": {"type": "array", "items": {"type": "string", "minLength": 1}, "description": "Soft coordination hints only. A worker may change any necessary project file inside the RUN workspace."}, "checks": {"type": "array", "items": {"$ref": "#/$defs/check"}}, "provides_checks": {"type": "array", "items": {"$ref": "#/$defs/check"}}, "stop_conditions": {"$ref": "#/$defs/strings", "description": "Hard semantic or external stop conditions; path prediction drift is not a stop condition."}, "depends_on": {"type": "array", "uniqueItems": true, "items": {"type": "string", "minLength": 1}}, "result_schema": {"const": "dd-flow/code-work-result@2"}}}
23
23
  }
24
24
  }
@@ -6,7 +6,7 @@
6
6
  "required": ["schema_id", "outcome", "summary", "finding_decisions", "correction"],
7
7
  "properties": {
8
8
  "schema_id": {"const": "dd-flow/plan-review-decision@3"},
9
- "outcome": {"enum": ["accepted", "blocked", "failed", "cancelled"]},
9
+ "outcome": {"enum": ["accepted", "failed", "cancelled"]},
10
10
  "summary": {"type": "string", "minLength": 1},
11
11
  "finding_decisions": {"type": "array", "items": {"type": "object", "additionalProperties": false, "required": ["finding_ref", "decision", "reason"], "properties": {"finding_ref": {"type": "string", "pattern": "^WRK-[^/]+/FIND-[0-9]{3}$"}, "decision": {"enum": ["accepted_fix", "rejected", "deferred_as_DEF", "requires_user", "duplicate"]}, "reason": {"type": "string", "minLength": 1}}}},
12
12
  "correction": {"type": "object", "additionalProperties": false, "required": ["status", "previous_plan_revision", "changed_paths", "summary"], "properties": {"status": {"enum": ["applied", "not_required"]}, "previous_plan_revision": {"type": "integer", "minimum": 1}, "changed_paths": {"type": "array", "items": {"type": "string", "minLength": 1}}, "summary": {"type": "string", "minLength": 1}}}
@@ -29,7 +29,7 @@
29
29
  "documentUpdate": {"type": "object", "additionalProperties": false, "required": ["path", "action", "owner", "reason"], "properties": {"path": {"type": "string", "minLength": 1}, "action": {"enum": ["create", "update"]}, "owner": {"type": "string", "minLength": 1}, "reason": {"type": "string", "minLength": 1}}},
30
30
  "spine": {"type": "object", "additionalProperties": false, "required": ["user_outcome", "component_responsibility", "must_preserve", "non_goals", "acceptance_contribution"], "properties": {"user_outcome": {"type": "string", "minLength": 1}, "component_responsibility": {"type": "string", "minLength": 1}, "must_preserve": {"$ref": "#/$defs/strings"}, "non_goals": {"type": "array", "items": {"type": "string", "minLength": 1}}, "acceptance_contribution": {"type": "string", "minLength": 1}}},
31
31
  "execution": {"type": "object", "additionalProperties": false, "required": ["required_read", "discovery_boundary", "planned_write_areas", "stop_conditions"], "properties": {"required_read": {"$ref": "#/$defs/strings", "description": "Mandatory starting sources. This is not a read allowlist."}, "discovery_boundary": {"$ref": "#/$defs/strings", "description": "Likely discovery areas. A worker may inspect other project-local sources when necessary."}, "planned_write_areas": {"type": "array", "items": {"type": "string", "minLength": 1}, "description": "Optional file or directory hints used only to coordinate concurrent Works. They never grant or deny write permission."}, "stop_conditions": {"$ref": "#/$defs/strings", "description": "Semantic contradictions or external blockers that require the worker to stop."}}},
32
- "check": {"type": "object", "additionalProperties": false, "required": ["id", "command", "purpose", "run_at", "availability"], "properties": {"id": {"type": "string", "pattern": "^CHK-[A-Za-z0-9-]+$"}, "command": {"type": "string", "minLength": 1}, "purpose": {"type": "string", "minLength": 1}, "run_at": {"enum": ["work", "code", "readiness", "merge", "release", "external"]}, "availability": {"enum": ["available", "planned"]}, "provided_by": {"type": "string", "pattern": "^P[0-9]+$"}, "definition": {"type": "string", "minLength": 1}, "required_artifacts": {"type": "array", "uniqueItems": true, "items": {"type": "string", "minLength": 1}}}, "allOf": [{"if": {"properties": {"availability": {"const": "planned"}}, "required": ["availability"]}, "then": {"required": ["provided_by", "definition"], "properties": {"command": {"pattern": "^@check/"}}}}, {"if": {"properties": {"command": {"pattern": "^@check/"}}}, "then": {"required": ["definition"]}}]},
32
+ "check": {"type": "object", "additionalProperties": false, "required": ["id", "command", "purpose", "run_at", "availability"], "properties": {"id": {"type": "string", "pattern": "^CHK-[A-Za-z0-9-]+$"}, "command": {"type": "string", "minLength": 1}, "purpose": {"type": "string", "minLength": 1}, "run_at": {"enum": ["work", "code", "readiness", "merge", "release", "external"]}, "availability": {"enum": ["available", "planned"]}, "provided_by": {"type": "string", "pattern": "^P[0-9]+$"}, "definition": {"type": "string", "minLength": 1}, "required_artifacts": {"type": "array", "uniqueItems": true, "items": {"type": "string", "minLength": 1}}, "ports": {"type": "array", "uniqueItems": true, "items": {"type": "string", "pattern": "^[A-Za-z][A-Za-z0-9_]*$"}}}, "allOf": [{"if": {"properties": {"availability": {"const": "planned"}}, "required": ["availability"]}, "then": {"required": ["provided_by", "definition"], "properties": {"command": {"pattern": "^@check/"}}}}, {"if": {"properties": {"command": {"pattern": "^@check/"}}}, "then": {"required": ["definition"]}}]},
33
33
  "verification": {"type": "object", "additionalProperties": false, "required": ["check_refs"], "properties": {"check_refs": {"type": "array", "minItems": 1, "uniqueItems": true, "items": {"type": "string", "pattern": "^CHK-[A-Za-z0-9-]+$"}}}},
34
34
  "item": {"type": "object", "additionalProperties": false, "required": ["id", "title", "summary", "details", "depends_on", "requirement_refs", "semantic_spine", "execution_context", "verification"], "properties": {"id": {"type": "string", "pattern": "^P[0-9]+$"}, "title": {"type": "string", "minLength": 1}, "summary": {"type": "string", "minLength": 1}, "details": {"type": "string", "minLength": 1}, "depends_on": {"type": "array", "uniqueItems": true, "items": {"type": "string", "pattern": "^P[0-9]+$"}}, "requirement_refs": {"$ref": "#/$defs/strings"}, "semantic_spine": {"$ref": "#/$defs/spine"}, "execution_context": {"$ref": "#/$defs/execution"}, "verification": {"$ref": "#/$defs/verification"}}},
35
35
  "acceptance": {"type": "object", "additionalProperties": false, "required": ["criterion_id", "plan_item_ids", "changed_surfaces", "path", "environment", "fixtures", "cleanup", "check_refs", "expected_evidence", "proof_limits", "gate"], "properties": {"criterion_id": {"type": "string", "pattern": "^AC-[0-9]+$"}, "plan_item_ids": {"type": "array", "minItems": 1, "items": {"type": "string", "pattern": "^P[0-9]+$"}}, "changed_surfaces": {"$ref": "#/$defs/strings"}, "path": {"type": "string", "minLength": 1}, "environment": {"type": "string", "minLength": 1}, "fixtures": {"type": "array", "items": {"type": "string", "minLength": 1}}, "cleanup": {"type": "string", "minLength": 1}, "check_refs": {"type": "array", "minItems": 1, "uniqueItems": true, "items": {"type": "string", "pattern": "^CHK-[A-Za-z0-9-]+$"}}, "expected_evidence": {"$ref": "#/$defs/strings"}, "proof_limits": {"$ref": "#/$defs/strings"}, "gate": {"enum": ["work", "code", "readiness", "merge", "release", "external"]}}}
@@ -8,6 +8,7 @@ import { projectCheckoutRoot, resolveProjectRoot } from "../storage/paths.js";
8
8
  import { appendAudit } from "./audit.js";
9
9
  import { requireProjectByRoot } from "./projects.js";
10
10
  import { protocolRunDiagnostics, readProtocolRuntimeState, requireProtocol } from "./protocols.js";
11
+ import { publicSessionIdentity } from "./session-identity.js";
11
12
  const actionKinds = new Set([
12
13
  "repair_protocol_state",
13
14
  "cancel_queue_job",
@@ -68,7 +69,7 @@ export function cleanupScan(context, input) {
68
69
  action: { kind: "cancel_queue_job", project_id: project.id, protocol_id: job.protocol_id }
69
70
  });
70
71
  }
71
- const staleSessions = context.db.all(`SELECT session_id, protocol_id, workspace_path FROM sessions
72
+ const staleSessions = context.db.all(`SELECT session_id, harness, provider_session_id, protocol_id, workspace_path FROM sessions
72
73
  WHERE project_id = ? AND status IN ('pending', 'active', 'waiting_user', 'blocked', 'stopping')
73
74
  ORDER BY updated_at ASC, session_id ASC`, [project.id]);
74
75
  for (const session of staleSessions) {
@@ -79,11 +80,11 @@ export function cleanupScan(context, input) {
79
80
  const terminalProtocol = Boolean(protocol && (["closed", "cancelled"].includes(protocol.status) || ["closed", "cancelled"].includes(protocol.stage)));
80
81
  if (missingWorkspace || terminalProtocol) {
81
82
  pushFinding(findings, actions, "stale_flow_session", "warning", {
82
- session_id: session.session_id,
83
+ session: publicSessionIdentity(session),
83
84
  protocol_id: session.protocol_id,
84
85
  missing_workspace: missingWorkspace,
85
86
  terminal_protocol: terminalProtocol,
86
- action: { kind: "close_flow_session", project_id: project.id, session_id: session.session_id }
87
+ action: { kind: "close_flow_session", project_id: project.id, session: publicSessionIdentity(session) }
87
88
  });
88
89
  }
89
90
  }
@@ -241,6 +242,12 @@ function validateCleanupPlan(value, project) {
241
242
  actual_project_id: action.project_id
242
243
  });
243
244
  }
245
+ if (action.kind === "close_flow_session") {
246
+ const session = action.session;
247
+ if (!session || typeof session.harness_id !== "string" || !session.harness_id || typeof session.session_id !== "string" || !session.session_id) {
248
+ throw new AppError("validation", "close_flow_session requires a native session identity", 2);
249
+ }
250
+ }
244
251
  return action;
245
252
  });
246
253
  return { project_root: project.root, project_id: project.id, actions };
@@ -305,20 +312,23 @@ function applyAction(context, action, reason, force, now) {
305
312
  });
306
313
  return { kind: action.kind, changed: true, protocol_id: action.protocol_id };
307
314
  }
308
- if (action.kind === "close_flow_session" && action.session_id) {
315
+ if (action.kind === "close_flow_session" && action.session) {
316
+ const stored = context.db.get("SELECT session_id FROM sessions WHERE project_id = ? AND harness = ? AND COALESCE(provider_session_id, session_id) = ?", [action.project_id, action.session.harness_id, action.session.session_id]);
317
+ if (!stored)
318
+ return { kind: action.kind, changed: false, skipped: true, reason: "session_not_found", session: action.session };
309
319
  const result = context.db.run(`UPDATE sessions SET status = 'stopped', stop_reason = ?, updated_at = ?, stopped_at = ?
310
- WHERE project_id = ? AND session_id = ? AND status IN ('pending', 'active', 'waiting_user', 'blocked', 'stopping')`, [reason, now, now, action.project_id, action.session_id]);
320
+ WHERE project_id = ? AND session_id = ? AND status IN ('pending', 'active', 'waiting_user', 'blocked', 'stopping')`, [reason, now, now, action.project_id, stored.session_id]);
311
321
  if (result.changes !== 1) {
312
- return { kind: action.kind, changed: false, skipped: true, reason: "already_stopped", session_id: action.session_id };
322
+ return { kind: action.kind, changed: false, skipped: true, reason: "already_stopped", session: action.session };
313
323
  }
314
324
  appendAudit(context, {
315
325
  projectId: action.project_id,
316
326
  eventType: "flow_session.stopped",
317
327
  reason,
318
328
  forced: force,
319
- payload: { session_id: action.session_id, source: "cleanup.apply" }
329
+ payload: { session_id: stored.session_id, source: "cleanup.apply" }
320
330
  });
321
- return { kind: action.kind, changed: true, session_id: action.session_id };
331
+ return { kind: action.kind, changed: true, session: action.session };
322
332
  }
323
333
  if (action.kind === "discard_stale_run" && action.run_id) {
324
334
  const run = context.db.get("SELECT status, runtime_path, run_dir, workspace_root, index_json FROM runs WHERE project_id = ? AND id = ?", [action.project_id, action.run_id]);