@deksden-com/dd-flow-cli 0.9.0-beta.8 → 0.9.0-beta.81

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 (138) hide show
  1. package/CHANGELOG.md +402 -0
  2. package/README.md +65 -0
  3. package/dist/build-info.json +8 -8
  4. package/dist/cli/command-inputs.js +300 -0
  5. package/dist/cli/help.js +81 -9
  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 +1344 -376
  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/config.js +11 -8
  63. package/dist/services/controller-fanout.js +116 -0
  64. package/dist/services/dashboard.js +103 -31
  65. package/dist/services/engines.js +121 -76
  66. package/dist/services/eval-snapshots.js +750 -73
  67. package/dist/services/execution-policy.js +170 -0
  68. package/dist/services/external-work-launch.js +110 -0
  69. package/dist/services/harness-adapter.js +147 -24
  70. package/dist/services/harness-config.js +2 -0
  71. package/dist/services/hooks.js +575 -293
  72. package/dist/services/lanes.js +61 -53
  73. package/dist/services/lifecycle-command.js +87 -7
  74. package/dist/services/lifecycle-invocations.js +835 -0
  75. package/dist/services/managed-daemon-binding.js +38 -0
  76. package/dist/services/managed-processes.js +283 -27
  77. package/dist/services/merge-queue.js +179 -104
  78. package/dist/services/merge-server.js +73 -28
  79. package/dist/services/migrations.js +14 -9
  80. package/dist/services/native-daemon-history.js +49 -0
  81. package/dist/services/native-session-control.js +39 -0
  82. package/dist/services/plan-runtime.js +19 -8
  83. package/dist/services/plans.js +28 -25
  84. package/dist/services/projects.js +18 -5
  85. package/dist/services/prompts.js +23 -16
  86. package/dist/services/protocols.js +64 -20
  87. package/dist/services/recovery-observation-budget.js +61 -0
  88. package/dist/services/recovery-snapshot-database.js +107 -0
  89. package/dist/services/run-control-receipt.js +56 -0
  90. package/dist/services/run-control-worker.js +363 -0
  91. package/dist/services/run-control.js +876 -0
  92. package/dist/services/run-controller-adapter.js +222 -0
  93. package/dist/services/run-controller-capture.js +134 -0
  94. package/dist/services/run-controller-process.js +189 -0
  95. package/dist/services/run-controller-recovery.js +222 -0
  96. package/dist/services/run-controller-state.js +36 -0
  97. package/dist/services/run-controller.js +779 -0
  98. package/dist/services/run-engine-bindings.js +1 -1
  99. package/dist/services/run-fork.js +138 -0
  100. package/dist/services/run-observations.js +112 -0
  101. package/dist/services/run-recovery-runtime.js +69 -0
  102. package/dist/services/run-recovery.js +343 -0
  103. package/dist/services/runs.js +368 -74
  104. package/dist/services/runtime-budget.js +434 -0
  105. package/dist/services/runtime-scope-capture.js +50 -0
  106. package/dist/services/runtime-scope-control.js +450 -0
  107. package/dist/services/runtime-scope-resume.js +565 -0
  108. package/dist/services/runtime-scope-stop.js +99 -0
  109. package/dist/services/runtime-scope-worker.js +243 -0
  110. package/dist/services/runtime-service.js +97 -0
  111. package/dist/services/schema-validation.js +10 -8
  112. package/dist/services/sessions.js +35 -69
  113. package/dist/services/stage-blocker.js +17 -7
  114. package/dist/services/stage-context.js +44 -18
  115. package/dist/services/stage-lifecycle.js +99 -102
  116. package/dist/services/stage-pause.js +69 -28
  117. package/dist/services/usage.js +37 -93
  118. package/dist/services/vnext-code-review.js +189 -85
  119. package/dist/services/vnext-code.js +257 -243
  120. package/dist/services/vnext-execution-profile.js +5 -3
  121. package/dist/services/vnext-fanout.js +134 -19
  122. package/dist/services/vnext-merge.js +285 -102
  123. package/dist/services/vnext-plan-review.js +148 -94
  124. package/dist/services/vnext-plan.js +141 -72
  125. package/dist/services/vnext-protocolize.js +129 -56
  126. package/dist/services/vnext-specify.js +87 -64
  127. package/dist/services/work-registry.js +701 -164
  128. package/dist/services/workspace-bootstrap.js +76 -0
  129. package/dist/services/worktrees.js +85 -35
  130. package/dist/shared/entity-references.js +8 -0
  131. package/dist/shared/errors.js +12 -0
  132. package/dist/storage/database.js +444 -32
  133. package/dist/storage/paths.js +17 -4
  134. package/dist/storage/work-references.js +23 -0
  135. package/dist/storage/writer-contract.js +77 -0
  136. package/dist/storage/writer-migration.js +102 -0
  137. package/package.json +11 -4
  138. 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,16 +3,32 @@ 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";
9
12
  export const subagentCapacityKey = "runtime.subagent.available_slots";
10
- export const capacityProbe = {
11
- fanout_size: 15,
12
- probe_hold_seconds: 60,
13
- cleanup_deadline_seconds: 180,
14
- completion_token: "AGENT-NN"
15
- };
13
+ /** External concurrency is policy, never evidence of native child qualification. */
14
+ export function getStageFanoutCapacity(context, input) {
15
+ const projectRoot = resolveProjectRoot(input.projectRoot);
16
+ const project = requireProjectByRoot(context, projectRoot);
17
+ const run = context.db.get("SELECT index_json FROM runs WHERE project_id = ? AND id = ?", [project.id, input.runId]);
18
+ if (!run)
19
+ throw new AppError("not_found", "RUN is unavailable", 1);
20
+ const frozen = JSON.parse(run.index_json).execution_profile;
21
+ if (frozen?.settings.execution) {
22
+ if (!frozen.agent_profiles)
23
+ throw new AppError("execution_profile_not_frozen", "Fan-out needs the RUN's frozen profiles", 1);
24
+ const resolved = resolveExecutionPolicy({ ddFlowHome: context.ddFlowHome, policy: frozen.settings.execution, profiles: frozen.agent_profiles, stage: input.stage });
25
+ if (resolved.delegation.mode === "external")
26
+ return { run_key: null, available_slots: resolved.delegation.max_parallel, source: "external_policy" };
27
+ }
28
+ const variables = getFlowRunVariables(context, { projectRoot, runId: input.runId });
29
+ const available = variables.variables[subagentCapacityKey];
30
+ return { run_key: subagentCapacityKey, available_slots: typeof available === "number" && Number.isInteger(available) && available >= 0 ? available : null, source: "harness_qualification" };
31
+ }
16
32
  export function fanoutState(input) {
17
33
  if (!input.hasWork)
18
34
  return input.dispatch === "none" ? "coordinator_required" : "dispatch_required";
@@ -43,21 +59,22 @@ export function getVnextFanoutStatus(context, input) {
43
59
  const projectRoot = resolveProjectRoot(input.projectRoot);
44
60
  const project = requireProjectByRoot(context, projectRoot);
45
61
  const run = context.db.get("SELECT run_root FROM runs WHERE project_id = ? AND id = ?", [project.id, input.runId]);
46
- if (!run?.run_root)
47
- throw new AppError("not_found", "RUN artifact root is unavailable", 1, { run_id: input.runId });
62
+ if (!run)
63
+ throw new AppError("not_found", "RUN is not registered", 1, { run_id: input.runId });
64
+ if (!run.run_root)
65
+ throw new AppError("runtime_artifact_missing", "RUN artifact root is unavailable", 1, { run_id: input.runId });
48
66
  const stageRoot = path.join(run.run_root, vnextStageDirectory(input.stage));
49
67
  const descriptor = readFanoutDescriptor(stageRoot);
50
68
  if (!descriptor)
51
69
  return { ok: true, run_id: input.runId, stage: input.stage, orchestration: null };
52
70
  if (descriptor.stage !== input.stage)
53
71
  throw new AppError("runtime_missing", "Stage fan-out descriptor belongs to another stage", 1, { requested_stage: input.stage, descriptor_stage: descriptor.stage });
54
- const variables = getFlowRunVariables(context, { projectRoot, runId: input.runId });
55
- const available = variables.variables[subagentCapacityKey];
72
+ const capacity = getStageFanoutCapacity(context, { projectRoot, runId: input.runId, stage: input.stage });
56
73
  const all = listWorks(context, { parentWorkId: descriptor.parent_work_id, includeResults: false });
57
74
  const ready = listWorks(context, { parentWorkId: descriptor.parent_work_id, ready: true, includeResults: false });
58
75
  const counts = Object.fromEntries(["created", "running", "completed", "failed", "cancelled"].map((status) => [status, all.works.filter((work) => work.status === status).length]));
59
76
  const hasWork = all.works.length > 0;
60
- const capacityKnown = typeof available === "number" && Number.isInteger(available) && available >= 0;
77
+ const capacityKnown = capacity.available_slots !== null;
61
78
  return {
62
79
  ok: true,
63
80
  run_id: input.runId,
@@ -68,12 +85,110 @@ export function getVnextFanoutStatus(context, input) {
68
85
  // not: their coordinator first materializes the Work graph from its
69
86
  // authoritative prompt. An empty graph is therefore not a success.
70
87
  state: fanoutState({ dispatch: descriptor.dispatch, hasWork, capacityRequired: descriptor.capacity_required, capacityKnown, created: counts.created ?? 0, running: counts.running ?? 0 }),
71
- capacity: {
72
- run_key: subagentCapacityKey,
73
- available_slots: capacityKnown ? available : null,
74
- ...(descriptor.capacity_required && !capacityKnown ? { probe: capacityProbe } : {})
75
- },
76
- works: { ...counts, ready: ready.works.map((work) => ({ work_id: work.work_id, task: work.task, start_command: work.start_command })) }
88
+ capacity,
89
+ 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 })) }
77
90
  }
78
91
  };
79
92
  }
93
+ /** Explicit controller mutation, separate from read-only fanout status.
94
+ * A native child may be mapped only by its registered identity. Reused native
95
+ * Sessions need a provider execution identity: without one we report ambiguity
96
+ * instead of applying a late event to the newest Work attempt. */
97
+ export async function reconcileVnextFanout(context, input) {
98
+ const value = prepareNativeObservations(input.observations);
99
+ const status = getVnextFanoutStatus(context, input);
100
+ if (!status.orchestration)
101
+ return { ok: true, issues: [], reconciled: [] };
102
+ const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
103
+ const parent = storageSessionId({ harness_id: value.harness_id, session_id: value.parent_session_id });
104
+ const issues = [], reconciled = [];
105
+ for (const child of value.children) {
106
+ if (child.status === "running" || child.status === "unknown")
107
+ continue;
108
+ const sessionId = storageSessionId({ harness_id: value.harness_id, session_id: String(child.session_id) });
109
+ 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]);
110
+ const active = links.filter(link => link.status === "running" && link.work_status === "running" && link.parent_work_id === status.orchestration.parent_work_id);
111
+ if (!active.length) {
112
+ if (!links.length)
113
+ issues.push({ code: "child_launch_association_missing", session_id: child.session_id, outcome: child.status });
114
+ continue;
115
+ }
116
+ if (links.length !== 1 || active.length !== 1) {
117
+ issues.push({ code: "child_execution_ambiguous", session_id: child.session_id, work_ids: active.map(link => link.work_id) });
118
+ continue;
119
+ }
120
+ const link = active[0];
121
+ 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]);
122
+ if (pendingCheck()) {
123
+ const run = context.db.get("SELECT workspace_root, run_root FROM runs WHERE project_id = ? AND id = ?", [project.id, input.runId]);
124
+ try {
125
+ reconcileUnfinishedChecks(context, { projectId: project.id, runId: input.runId, workspaceRoot: run.workspace_root, runHome: run.run_root, workId: link.work_id });
126
+ }
127
+ catch (error) {
128
+ if (!(error instanceof AppError) || error.code !== "check_in_progress")
129
+ throw error;
130
+ }
131
+ if (pendingCheck()) {
132
+ issues.push({ code: "child_check_in_progress", work_id: link.work_id });
133
+ continue;
134
+ }
135
+ }
136
+ if (context.db.get("SELECT 1 FROM works WHERE parent_work_id = ? AND status IN ('created','running','paused') LIMIT 1", [link.work_id])) {
137
+ issues.push({ code: "child_descendants_unsettled", work_id: link.work_id });
138
+ continue;
139
+ }
140
+ if (child.status === "completed") {
141
+ issues.push({ code: "execution_ended_without_work_result", work_id: link.work_id, session_id: child.session_id });
142
+ continue;
143
+ }
144
+ 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 });
145
+ const result = await (child.status === "failed" ? failWork : cancelWork)(context, link.work_id, reason);
146
+ reconciled.push({ work_id: link.work_id, work_session_id: link.id, outcome: child.status, result });
147
+ }
148
+ appendFlowRunTimelineEvent(context, project.id, input.runId, { type: "native_child_reconciliation", stage: input.stage, observations: value, issues, reconciled });
149
+ return { ok: true, issues, reconciled };
150
+ }
151
+ /**
152
+ * Records every terminal native child at the Stage where it is first observed.
153
+ * This is separate from fan-out settlement: a historical child can be valid
154
+ * evidence for an earlier Work, while an unbound terminal child is retained
155
+ * as an inconsistency instead of being rediscovered in a later review wave.
156
+ */
157
+ export function prepareNativeObservations(observations) {
158
+ const value = observations;
159
+ 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)
160
+ || 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)))
161
+ throw new AppError("usage", "Expected native observations: harness_id, parent_session_id and children[{session_id,status}]", 2);
162
+ if (new Set(value.children.map(child => child.session_id)).size !== value.children.length)
163
+ throw new AppError("usage", "Native observations must contain each child Session exactly once", 2);
164
+ return { harness_id: value.harness_id, parent_session_id: value.parent_session_id, children: value.children };
165
+ }
166
+ export function observeVnextNativeChildren(context, input) {
167
+ const value = prepareNativeObservations(input.observations);
168
+ const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
169
+ const parent = storageSessionId({ harness_id: value.harness_id, session_id: value.parent_session_id });
170
+ const issues = [], history = [], pending = [];
171
+ for (const child of value.children) {
172
+ const sessionId = storageSessionId({ harness_id: value.harness_id, session_id: String(child.session_id) });
173
+ 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
174
+ FROM work_sessions ws JOIN works w ON w.work_id = ws.work_id
175
+ JOIN sessions s ON s.project_id = w.project_id AND s.session_id = ws.session_id
176
+ 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]);
177
+ if (child.status === "running" || child.status === "unknown") {
178
+ pending.push({ session_id: child.session_id, outcome: child.status, links: links.map(link => ({ work_id: link.work_id, work_session_id: link.id })) });
179
+ continue;
180
+ }
181
+ if (!links.length) {
182
+ issues.push({ code: "unbound_native_delegation_detected", session_id: child.session_id, outcome: child.status, detected_at_stage: input.stage, origin_stage: "unknown" });
183
+ continue;
184
+ }
185
+ if (links.length !== 1) {
186
+ 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) });
187
+ continue;
188
+ }
189
+ const link = links[0];
190
+ 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 });
191
+ }
192
+ appendFlowRunTimelineEvent(context, project.id, input.runId, { type: "native_child_observation", stage: input.stage, observations: value, issues, history, pending });
193
+ return { ok: true, issues, history, pending };
194
+ }