@deksden-com/dd-flow-cli 0.9.0-beta.9 → 0.9.0-beta.90

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.
Files changed (143) hide show
  1. package/CHANGELOG.md +460 -0
  2. package/README.md +65 -0
  3. package/dist/build-info.json +8 -8
  4. package/dist/cli/command-inputs.js +304 -0
  5. package/dist/cli/help.js +89 -13
  6. package/dist/cli/hook-ingress.js +82 -0
  7. package/dist/cli/input-preparation.js +115 -0
  8. package/dist/cli/run-cli.js +1409 -382
  9. package/dist/cli.js +6 -2
  10. package/dist/harness-runtime/bin/dd-agy.mjs +37 -0
  11. package/dist/harness-runtime/bin/dd-codex.mjs +25 -0
  12. package/dist/harness-runtime/bin/dd-droid.mjs +33 -0
  13. package/dist/harness-runtime/bin/dd-grok.mjs +30 -0
  14. package/dist/harness-runtime/bin/dd-opencode.mjs +21 -0
  15. package/dist/harness-runtime/bin/dd-zcode.mjs +83 -0
  16. package/dist/harness-runtime/lib/daemon-operations.mjs +215 -0
  17. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +423 -0
  18. package/dist/harness-runtime/lib/dd-agy.mjs +59 -0
  19. package/dist/harness-runtime/lib/dd-codex-daemon.d.mts +1 -0
  20. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +146 -0
  21. package/dist/harness-runtime/lib/dd-codex.mjs +562 -0
  22. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +125 -0
  23. package/dist/harness-runtime/lib/dd-droid.mjs +471 -0
  24. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +274 -0
  25. package/dist/harness-runtime/lib/dd-grok.mjs +174 -0
  26. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +198 -0
  27. package/dist/harness-runtime/lib/dd-opencode.mjs +98 -0
  28. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +644 -0
  29. package/dist/harness-runtime/lib/dd-zcode.mjs +910 -0
  30. package/dist/harness-runtime/lib/delegation-instructions.d.mts +10 -0
  31. package/dist/harness-runtime/lib/delegation-instructions.mjs +130 -0
  32. package/dist/harness-runtime/lib/dispatch-fence.mjs +14 -0
  33. package/dist/harness-runtime/lib/driver-recovery.mjs +129 -0
  34. package/dist/harness-runtime/lib/droid-observation.mjs +66 -0
  35. package/dist/harness-runtime/lib/managed-daemon.mjs +260 -0
  36. package/dist/harness-runtime/lib/model-observations.mjs +77 -0
  37. package/dist/harness-runtime/lib/native-hook-command.d.mts +1 -0
  38. package/dist/harness-runtime/lib/native-hook-command.mjs +34 -0
  39. package/dist/harness-runtime/lib/observation-clock.mjs +36 -0
  40. package/dist/harness-runtime/lib/operation-context.mjs +5 -0
  41. package/dist/harness-runtime/lib/operation-errors.mjs +19 -0
  42. package/dist/harness-runtime/lib/process-json.mjs +70 -0
  43. package/dist/harness-runtime/lib/process-snapshot.mjs +10 -0
  44. package/dist/harness-runtime/lib/runner-events.mjs +200 -0
  45. package/dist/harness-runtime/lib/runner-lock.mjs +42 -0
  46. package/dist/harness-runtime/lib/session-settlement.mjs +30 -0
  47. package/dist/harness-runtime/lib/tool-observations.d.mts +17 -0
  48. package/dist/harness-runtime/lib/tool-observations.mjs +153 -0
  49. package/dist/schemas/agent-profile.schema.json +1 -1
  50. package/dist/schemas/code-review-result.schema.json +2 -2
  51. package/dist/schemas/code-work-batch.schema.json +4 -3
  52. package/dist/schemas/code-work-result.schema.json +4 -4
  53. package/dist/schemas/plan-review-result.schema.json +2 -2
  54. package/dist/schemas/run-control-receipt.schema.json +99 -0
  55. package/dist/schemas/run-control-request.schema.json +36 -0
  56. package/dist/schemas/vnext-protocol-plan.schema.json +2 -2
  57. package/dist/services/canon.js +11 -8
  58. package/dist/services/cleanup.js +57 -13
  59. package/dist/services/cli-operation-classifier.js +22 -24
  60. package/dist/services/code-checks.js +543 -96
  61. package/dist/services/codex-hook-delivery.js +28 -0
  62. package/dist/services/command-context.js +80 -0
  63. package/dist/services/config.js +11 -8
  64. package/dist/services/continuation-outcome.js +20 -0
  65. package/dist/services/controller-fanout.js +132 -0
  66. package/dist/services/dashboard.js +103 -31
  67. package/dist/services/engines.js +121 -76
  68. package/dist/services/eval-snapshots.js +750 -73
  69. package/dist/services/execution-policy.js +170 -0
  70. package/dist/services/external-work-launch.js +110 -0
  71. package/dist/services/harness-adapter.js +147 -24
  72. package/dist/services/harness-config.js +2 -0
  73. package/dist/services/hooks.js +587 -299
  74. package/dist/services/lanes.js +61 -53
  75. package/dist/services/lifecycle-command.js +118 -7
  76. package/dist/services/lifecycle-invocations.js +1055 -0
  77. package/dist/services/managed-daemon-binding.js +38 -0
  78. package/dist/services/managed-processes.js +283 -27
  79. package/dist/services/merge-queue.js +179 -104
  80. package/dist/services/merge-server.js +73 -28
  81. package/dist/services/migrations.js +14 -9
  82. package/dist/services/native-daemon-history.js +49 -0
  83. package/dist/services/native-session-control.js +39 -0
  84. package/dist/services/plan-runtime.js +19 -8
  85. package/dist/services/plans.js +28 -25
  86. package/dist/services/projects.js +18 -5
  87. package/dist/services/prompts.js +23 -16
  88. package/dist/services/protocols.js +64 -20
  89. package/dist/services/recovery-observation-budget.js +61 -0
  90. package/dist/services/recovery-snapshot-database.js +107 -0
  91. package/dist/services/repair-intents.js +40 -0
  92. package/dist/services/run-control-receipt.js +56 -0
  93. package/dist/services/run-control-worker.js +363 -0
  94. package/dist/services/run-control.js +876 -0
  95. package/dist/services/run-controller-adapter.js +222 -0
  96. package/dist/services/run-controller-capture.js +134 -0
  97. package/dist/services/run-controller-process.js +189 -0
  98. package/dist/services/run-controller-recovery.js +222 -0
  99. package/dist/services/run-controller-state.js +36 -0
  100. package/dist/services/run-controller.js +789 -0
  101. package/dist/services/run-engine-bindings.js +1 -1
  102. package/dist/services/run-fork.js +138 -0
  103. package/dist/services/run-observations.js +112 -0
  104. package/dist/services/run-recovery-runtime.js +69 -0
  105. package/dist/services/run-recovery.js +343 -0
  106. package/dist/services/runs.js +368 -74
  107. package/dist/services/runtime-budget.js +434 -0
  108. package/dist/services/runtime-command.js +43 -0
  109. package/dist/services/runtime-scope-capture.js +50 -0
  110. package/dist/services/runtime-scope-control.js +450 -0
  111. package/dist/services/runtime-scope-resume.js +565 -0
  112. package/dist/services/runtime-scope-stop.js +99 -0
  113. package/dist/services/runtime-scope-worker.js +243 -0
  114. package/dist/services/runtime-service.js +97 -0
  115. package/dist/services/schema-validation.js +10 -8
  116. package/dist/services/sessions.js +35 -69
  117. package/dist/services/stage-blocker.js +17 -7
  118. package/dist/services/stage-context.js +44 -18
  119. package/dist/services/stage-lifecycle.js +99 -102
  120. package/dist/services/stage-pause.js +84 -55
  121. package/dist/services/stage-work-graph.js +35 -0
  122. package/dist/services/usage.js +37 -93
  123. package/dist/services/vnext-code-review.js +195 -88
  124. package/dist/services/vnext-code.js +285 -252
  125. package/dist/services/vnext-execution-profile.js +5 -3
  126. package/dist/services/vnext-fanout.js +160 -17
  127. package/dist/services/vnext-merge.js +291 -104
  128. package/dist/services/vnext-plan-review.js +139 -81
  129. package/dist/services/vnext-plan.js +138 -69
  130. package/dist/services/vnext-protocolize.js +129 -56
  131. package/dist/services/vnext-specify.js +88 -65
  132. package/dist/services/work-registry.js +716 -168
  133. package/dist/services/workspace-bootstrap.js +76 -0
  134. package/dist/services/worktrees.js +85 -35
  135. package/dist/shared/entity-references.js +8 -0
  136. package/dist/shared/errors.js +12 -0
  137. package/dist/storage/database.js +459 -32
  138. package/dist/storage/paths.js +17 -4
  139. package/dist/storage/work-references.js +23 -0
  140. package/dist/storage/writer-contract.js +77 -0
  141. package/dist/storage/writer-migration.js +102 -0
  142. package/package.json +24 -13
  143. package/tools/repair-paused-run-status.mjs +59 -0
@@ -1,6 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { AppError } from "../shared/errors.js";
4
+ import { isExecutionRouting } from "./execution-policy.js";
4
5
  const relativePath = path.join(".memory-bank", "dd-flow", "project-execution.json");
5
6
  /** The project owns defaults; a RUN snapshots them once and never re-infers them. */
6
7
  export function loadVnextExecutionProfile(projectRoot) {
@@ -13,7 +14,7 @@ export function loadVnextExecutionProfile(projectRoot) {
13
14
  throw new AppError("execution_profile_missing", "vNext flow requires .memory-bank/dd-flow/project-execution.json", 1, { file, cause: String(error) });
14
15
  }
15
16
  const profile = value;
16
- if (profile.schema_id !== "dd-flow/project-execution@2"
17
+ if ((profile.schema_id !== "dd-flow/project-execution@2" && profile.schema_id !== "dd-flow/project-execution@3")
17
18
  || (profile.stage_session_mode !== undefined && profile.stage_session_mode !== "same_session" && profile.stage_session_mode !== "new_session")
18
19
  || !["auto", "off", "standard", "deep"].includes(String(profile.plan_review_mode))
19
20
  || (profile.code_review_mode !== undefined && !["auto", "off", "standard", "deep"].includes(String(profile.code_review_mode)))
@@ -22,8 +23,9 @@ export function loadVnextExecutionProfile(projectRoot) {
22
23
  || !["retain", "delete_after_success"].includes(String(profile.merge_cleanup?.source))
23
24
  || !["code_completed", "code_review_completed", "merge_completed"].includes(String(profile.stop_target))
24
25
  || !profile.code_bootstrap || typeof profile.code_bootstrap.command !== "string" || !profile.code_bootstrap.command.trim()
25
- || typeof profile.code_bootstrap.policy_ref !== "string" || !profile.code_bootstrap.policy_ref.trim()) {
26
- throw new AppError("execution_profile_invalid", "Project execution profile does not match dd-flow/project-execution@2", 1, { file });
26
+ || typeof profile.code_bootstrap.policy_ref !== "string" || !profile.code_bootstrap.policy_ref.trim()
27
+ || (profile.execution === undefined ? profile.schema_id === "dd-flow/project-execution@3" : !isExecutionRouting(profile.execution))) {
28
+ throw new AppError("execution_profile_invalid", "Project execution profile does not match its supported dd-flow schema", 1, { file });
27
29
  }
28
30
  return { ...profile, stage_session_mode: profile.stage_session_mode ?? "same_session", code_review_mode: profile.code_review_mode ?? "off" };
29
31
  }
@@ -3,10 +3,33 @@ import path from "node:path";
3
3
  import { AppError } from "../shared/errors.js";
4
4
  import { resolveProjectRoot } from "../storage/paths.js";
5
5
  import { requireProjectByRoot } from "./projects.js";
6
- import { getFlowRunVariables } from "./runs.js";
7
- import { listWorks } from "./work-registry.js";
6
+ import { getFlowRunVariables, appendFlowRunTimelineEvent } from "./runs.js";
7
+ import { resolveExecutionPolicy } from "./execution-policy.js";
8
+ import { listWorks, failWork, cancelWork } from "./work-registry.js";
9
+ import { storageSessionId } from "./session-identity.js";
10
+ import { reconcileUnfinishedChecks } from "./code-checks.js";
8
11
  import { vnextStageDirectory } from "../domain/stage-catalog.js";
12
+ import { stageWorkIds, stageGraphScope } from "./stage-work-graph.js";
9
13
  export const subagentCapacityKey = "runtime.subagent.available_slots";
14
+ /** External concurrency is policy, never evidence of native child qualification. */
15
+ export function getStageFanoutCapacity(context, input) {
16
+ const projectRoot = resolveProjectRoot(input.projectRoot);
17
+ const project = requireProjectByRoot(context, projectRoot);
18
+ const run = context.db.get("SELECT index_json FROM runs WHERE project_id = ? AND id = ?", [project.id, input.runId]);
19
+ if (!run)
20
+ throw new AppError("not_found", "RUN is unavailable", 1);
21
+ const frozen = JSON.parse(run.index_json).execution_profile;
22
+ if (frozen?.settings.execution) {
23
+ if (!frozen.agent_profiles)
24
+ throw new AppError("execution_profile_not_frozen", "Fan-out needs the RUN's frozen profiles", 1);
25
+ const resolved = resolveExecutionPolicy({ ddFlowHome: context.ddFlowHome, policy: frozen.settings.execution, profiles: frozen.agent_profiles, stage: input.stage });
26
+ if (resolved.delegation.mode === "external")
27
+ return { run_key: null, available_slots: resolved.delegation.max_parallel, source: "external_policy" };
28
+ }
29
+ const variables = getFlowRunVariables(context, { projectRoot, runId: input.runId });
30
+ const available = variables.variables[subagentCapacityKey];
31
+ return { run_key: subagentCapacityKey, available_slots: typeof available === "number" && Number.isInteger(available) && available >= 0 ? available : null, source: "harness_qualification" };
32
+ }
10
33
  export function fanoutState(input) {
11
34
  if (!input.hasWork)
12
35
  return input.dispatch === "none" ? "coordinator_required" : "dispatch_required";
@@ -18,7 +41,7 @@ export function fanoutState(input) {
18
41
  }
19
42
  export function fanoutFile(stageRoot) { return path.join(stageRoot, "fanout.json"); }
20
43
  export function writeFanoutDescriptor(stageRoot, descriptor) {
21
- const value = { schema_id: "dd-flow/work-fanout@1", kind: "work_fanout", ...descriptor };
44
+ const value = { schema_id: descriptor.scope ? "dd-flow/work-fanout@2" : "dd-flow/work-fanout@1", kind: "work_fanout", ...descriptor };
22
45
  fs.writeFileSync(fanoutFile(stageRoot), `${JSON.stringify(value, null, 2)}\n`);
23
46
  return value;
24
47
  }
@@ -27,7 +50,7 @@ export function readFanoutDescriptor(stageRoot) {
27
50
  if (!fs.existsSync(file))
28
51
  return null;
29
52
  const value = JSON.parse(fs.readFileSync(file, "utf8"));
30
- if (value.schema_id !== "dd-flow/work-fanout@1" || value.kind !== "work_fanout" || typeof value.stage !== "string" || typeof value.parent_work_id !== "string" || !["none", "plan-review"].includes(String(value.dispatch)) || typeof value.capacity_required !== "boolean") {
53
+ if (!["dd-flow/work-fanout@1", "dd-flow/work-fanout@2"].includes(String(value.schema_id)) || value.kind !== "work_fanout" || typeof value.stage !== "string" || typeof value.parent_work_id !== "string" || !["none", "plan-review"].includes(String(value.dispatch)) || typeof value.capacity_required !== "boolean" || value.schema_id === "dd-flow/work-fanout@2" && (!value.scope || [value.scope.project_id, value.scope.run_id, value.scope.attempt, value.scope.cycle].some(item => typeof item !== "string"))) {
31
54
  throw new AppError("runtime_missing", "Stage fan-out descriptor is invalid", 1, { file });
32
55
  }
33
56
  return value;
@@ -37,21 +60,38 @@ export function getVnextFanoutStatus(context, input) {
37
60
  const projectRoot = resolveProjectRoot(input.projectRoot);
38
61
  const project = requireProjectByRoot(context, projectRoot);
39
62
  const run = context.db.get("SELECT run_root FROM runs WHERE project_id = ? AND id = ?", [project.id, input.runId]);
40
- if (!run?.run_root)
41
- throw new AppError("not_found", "RUN artifact root is unavailable", 1, { run_id: input.runId });
63
+ if (!run)
64
+ throw new AppError("not_found", "RUN is not registered", 1, { run_id: input.runId });
65
+ if (!run.run_root)
66
+ throw new AppError("runtime_artifact_missing", "RUN artifact root is unavailable", 1, { run_id: input.runId });
42
67
  const stageRoot = path.join(run.run_root, vnextStageDirectory(input.stage));
43
- const descriptor = readFanoutDescriptor(stageRoot);
68
+ let descriptor = readFanoutDescriptor(stageRoot);
69
+ // Registration is authoritative even if projection publication was interrupted,
70
+ // or an otherwise review-off stage acquired an aggregate repair graph.
71
+ if (!descriptor && context.db.get("SELECT 1 FROM sqlite_master WHERE name = 'repair_intents'")) {
72
+ const members = stageWorkIds(context, project.id, input.runId, input.stage);
73
+ const parents = context.db.all("SELECT w.work_id, w.parent_work_id FROM repair_intents i JOIN works w ON w.work_id = i.work_id WHERE i.project_id = ? AND i.run_id = ? AND i.stage = ?", [project.id, input.runId, input.stage]).filter(work => members.has(work.work_id));
74
+ const roots = [...new Set(parents.map(work => work.parent_work_id))];
75
+ if (roots.length > 1)
76
+ throw new AppError("stage_graph_conflict", "Registered repairs disagree on their stage coordinator", 1);
77
+ if (roots.length === 1)
78
+ descriptor = { schema_id: "dd-flow/work-fanout@2", scope: stageGraphScope(context, project.id, input.runId, input.stage), kind: "work_fanout", stage: input.stage, parent_work_id: roots[0], dispatch: "none", capacity_required: true };
79
+ }
44
80
  if (!descriptor)
45
81
  return { ok: true, run_id: input.runId, stage: input.stage, orchestration: null };
46
82
  if (descriptor.stage !== input.stage)
47
83
  throw new AppError("runtime_missing", "Stage fan-out descriptor belongs to another stage", 1, { requested_stage: input.stage, descriptor_stage: descriptor.stage });
48
- const variables = getFlowRunVariables(context, { projectRoot, runId: input.runId });
49
- const available = variables.variables[subagentCapacityKey];
50
- const all = listWorks(context, { parentWorkId: descriptor.parent_work_id, includeResults: false });
51
- const ready = listWorks(context, { parentWorkId: descriptor.parent_work_id, ready: true, includeResults: false });
84
+ const capacity = getStageFanoutCapacity(context, { projectRoot, runId: input.runId, stage: input.stage });
85
+ const members = stageWorkIds(context, project.id, input.runId, input.stage);
86
+ const currentScope = stageGraphScope(context, project.id, input.runId, input.stage);
87
+ const compatible = descriptor.scope ? Object.entries(currentScope).every(([key, value]) => descriptor.scope[key] === value) : members.size > 0;
88
+ if (!compatible)
89
+ return { ok: true, run_id: input.runId, stage: input.stage, orchestration: { ...descriptor, state: "compatibility_required", compatibility: { code: "stage_graph_compatibility_required", message: "Legacy or stale stage graph has no authoritative current membership; use the pinned engine", expected_scope: currentScope } } };
90
+ const all = listWorks(context, { parentWorkId: descriptor.parent_work_id, includeResults: false, workIds: members });
91
+ const ready = { works: all.works.filter(work => work.ready) };
52
92
  const counts = Object.fromEntries(["created", "running", "completed", "failed", "cancelled"].map((status) => [status, all.works.filter((work) => work.status === status).length]));
53
93
  const hasWork = all.works.length > 0;
54
- const capacityKnown = typeof available === "number" && Number.isInteger(available) && available >= 0;
94
+ const capacityKnown = capacity.available_slots !== null;
55
95
  return {
56
96
  ok: true,
57
97
  run_id: input.runId,
@@ -62,11 +102,114 @@ export function getVnextFanoutStatus(context, input) {
62
102
  // not: their coordinator first materializes the Work graph from its
63
103
  // authoritative prompt. An empty graph is therefore not a success.
64
104
  state: fanoutState({ dispatch: descriptor.dispatch, hasWork, capacityRequired: descriptor.capacity_required, capacityKnown, created: counts.created ?? 0, running: counts.running ?? 0 }),
65
- capacity: {
66
- run_key: subagentCapacityKey,
67
- available_slots: capacityKnown ? available : null
68
- },
69
- works: { ...counts, ready: ready.works.map((work) => ({ work_id: work.work_id, task: work.task, start_command: work.start_command })) }
105
+ capacity,
106
+ graph_revision: all.works.map(work => ({ work_id: work.work_id, status: work.status, depends_on: work.depends_on })),
107
+ works: { ...counts, ready: ready.works.map((work) => ({ work_id: work.work_id, task: work.task, launch_policy: work.launch_policy, start_command: work.start_command })) }
70
108
  }
71
109
  };
72
110
  }
111
+ /** Explicit controller mutation, separate from read-only fanout status.
112
+ * A native child may be mapped only by its registered identity. Reused native
113
+ * Sessions need a provider execution identity: without one we report ambiguity
114
+ * instead of applying a late event to the newest Work attempt. */
115
+ export async function reconcileVnextFanout(context, input) {
116
+ const value = prepareNativeObservations(input.observations);
117
+ const status = getVnextFanoutStatus(context, input);
118
+ if (!status.orchestration)
119
+ return { ok: true, issues: [], reconciled: [] };
120
+ if (status.orchestration.state === "compatibility_required")
121
+ throw new AppError("stage_graph_compatibility_required", "Cannot reconcile a legacy or stale graph", 1);
122
+ const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
123
+ const members = stageWorkIds(context, project.id, input.runId, input.stage);
124
+ const parent = storageSessionId({ harness_id: value.harness_id, session_id: value.parent_session_id });
125
+ const issues = [], reconciled = [];
126
+ for (const child of value.children) {
127
+ if (child.status === "running" || child.status === "unknown")
128
+ continue;
129
+ const sessionId = storageSessionId({ harness_id: value.harness_id, session_id: String(child.session_id) });
130
+ const links = context.db.all(`SELECT ws.id, ws.work_id, ws.status, w.status AS work_status, w.parent_work_id FROM work_sessions ws JOIN works w ON w.work_id = ws.work_id JOIN sessions s ON s.project_id = w.project_id AND s.session_id = ws.session_id WHERE w.project_id = ? AND w.run_id = ? AND ws.session_id = ? AND s.parent_session_id = ? ORDER BY ws.created_at`, [project.id, input.runId, sessionId, parent]);
131
+ const active = links.filter(link => link.status === "running" && link.work_status === "running" && link.parent_work_id === status.orchestration.parent_work_id && members.has(link.work_id));
132
+ if (!active.length) {
133
+ if (!links.length)
134
+ issues.push({ code: "child_launch_association_missing", session_id: child.session_id, outcome: child.status });
135
+ continue;
136
+ }
137
+ if (links.length !== 1 || active.length !== 1) {
138
+ issues.push({ code: "child_execution_ambiguous", session_id: child.session_id, work_ids: active.map(link => link.work_id) });
139
+ continue;
140
+ }
141
+ const link = active[0];
142
+ const pendingCheck = () => context.db.get("SELECT 1 FROM check_receipts WHERE project_id = ? AND run_id = ? AND work_id = ? AND status = 'running' LIMIT 1", [project.id, input.runId, link.work_id]);
143
+ if (pendingCheck()) {
144
+ const run = context.db.get("SELECT workspace_root, run_root FROM runs WHERE project_id = ? AND id = ?", [project.id, input.runId]);
145
+ try {
146
+ reconcileUnfinishedChecks(context, { projectId: project.id, runId: input.runId, workspaceRoot: run.workspace_root, runHome: run.run_root, workId: link.work_id });
147
+ }
148
+ catch (error) {
149
+ if (!(error instanceof AppError) || error.code !== "check_in_progress")
150
+ throw error;
151
+ }
152
+ if (pendingCheck()) {
153
+ issues.push({ code: "child_check_in_progress", work_id: link.work_id });
154
+ continue;
155
+ }
156
+ }
157
+ if (context.db.get("SELECT 1 FROM works WHERE parent_work_id = ? AND status IN ('created','running','paused') LIMIT 1", [link.work_id])) {
158
+ issues.push({ code: "child_descendants_unsettled", work_id: link.work_id });
159
+ continue;
160
+ }
161
+ if (child.status === "completed") {
162
+ issues.push({ code: "execution_ended_without_work_result", work_id: link.work_id, session_id: child.session_id });
163
+ continue;
164
+ }
165
+ const reason = JSON.stringify({ source: "native_child_observation", harness_id: value.harness_id, session_id: child.session_id, work_session_id: link.id, outcome: child.status });
166
+ const result = await (child.status === "failed" ? failWork : cancelWork)(context, link.work_id, reason);
167
+ reconciled.push({ work_id: link.work_id, work_session_id: link.id, outcome: child.status, result });
168
+ }
169
+ appendFlowRunTimelineEvent(context, project.id, input.runId, { type: "native_child_reconciliation", stage: input.stage, observations: value, issues, reconciled });
170
+ return { ok: true, issues, reconciled };
171
+ }
172
+ /**
173
+ * Records every terminal native child at the Stage where it is first observed.
174
+ * This is separate from fan-out settlement: a historical child can be valid
175
+ * evidence for an earlier Work, while an unbound terminal child is retained
176
+ * as an inconsistency instead of being rediscovered in a later review wave.
177
+ */
178
+ export function prepareNativeObservations(observations) {
179
+ const value = observations;
180
+ if (!value || typeof value.harness_id !== "string" || !value.harness_id.trim() || typeof value.parent_session_id !== "string" || !value.parent_session_id.trim() || !Array.isArray(value.children)
181
+ || value.children.some(child => !child || typeof child.session_id !== "string" || !child.session_id.trim() || typeof child.status !== "string" || !["running", "unknown", "completed", "failed", "cancelled"].includes(child.status)))
182
+ throw new AppError("usage", "Expected native observations: harness_id, parent_session_id and children[{session_id,status}]", 2);
183
+ if (new Set(value.children.map(child => child.session_id)).size !== value.children.length)
184
+ throw new AppError("usage", "Native observations must contain each child Session exactly once", 2);
185
+ return { harness_id: value.harness_id, parent_session_id: value.parent_session_id, children: value.children };
186
+ }
187
+ export function observeVnextNativeChildren(context, input) {
188
+ const value = prepareNativeObservations(input.observations);
189
+ const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
190
+ const parent = storageSessionId({ harness_id: value.harness_id, session_id: value.parent_session_id });
191
+ const issues = [], history = [], pending = [];
192
+ for (const child of value.children) {
193
+ const sessionId = storageSessionId({ harness_id: value.harness_id, session_id: String(child.session_id) });
194
+ const links = context.db.all(`SELECT ws.id, ws.work_id, ws.status, w.status AS work_status, w.parent_work_id, s.current_stage
195
+ FROM work_sessions ws JOIN works w ON w.work_id = ws.work_id
196
+ JOIN sessions s ON s.project_id = w.project_id AND s.session_id = ws.session_id
197
+ WHERE w.project_id = ? AND w.run_id = ? AND ws.session_id = ? AND s.parent_session_id = ? ORDER BY ws.created_at`, [project.id, input.runId, sessionId, parent]);
198
+ if (child.status === "running" || child.status === "unknown") {
199
+ pending.push({ session_id: child.session_id, outcome: child.status, links: links.map(link => ({ work_id: link.work_id, work_session_id: link.id })) });
200
+ continue;
201
+ }
202
+ if (!links.length) {
203
+ issues.push({ code: "unbound_native_delegation_detected", session_id: child.session_id, outcome: child.status, detected_at_stage: input.stage, origin_stage: "unknown" });
204
+ continue;
205
+ }
206
+ if (links.length !== 1) {
207
+ issues.push({ code: "child_execution_ambiguous", session_id: child.session_id, outcome: child.status, detected_at_stage: input.stage, work_ids: links.map(link => link.work_id) });
208
+ continue;
209
+ }
210
+ const link = links[0];
211
+ history.push({ session_id: child.session_id, outcome: child.status, work_id: link.work_id, work_session_id: link.id, work_status: link.work_status, origin_stage: link.current_stage ?? "unknown", detected_at_stage: input.stage });
212
+ }
213
+ appendFlowRunTimelineEvent(context, project.id, input.runId, { type: "native_child_observation", stage: input.stage, observations: value, issues, history, pending });
214
+ return { ok: true, issues, history, pending };
215
+ }