karajan-code 2.7.4 → 2.9.0

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 (192) hide show
  1. package/README.md +1 -1
  2. package/package.json +24 -4
  3. package/scripts/diet-baseline.mjs +170 -0
  4. package/scripts/regen-arch-stats.sh +71 -0
  5. package/src/agents/availability.js +15 -9
  6. package/src/agents/claude-agent.js +143 -45
  7. package/src/agents/model-registry.js +1 -1
  8. package/src/audit/agent-readiness.js +240 -0
  9. package/src/audit/basal-cost.js +92 -9
  10. package/src/audit/deterministic-summary.js +200 -0
  11. package/src/audit/osv-findings.js +162 -0
  12. package/src/audit/semgrep-findings.js +117 -0
  13. package/src/audit/sonar-findings.js +81 -0
  14. package/src/audit/webperf-input.js +90 -0
  15. package/src/bootstrap.js +26 -1
  16. package/src/brain/decision-tracker.js +3 -1
  17. package/src/brain/decisor.js +1 -1
  18. package/src/canvas/parse-md.js +238 -0
  19. package/src/canvas/schema.js +101 -0
  20. package/src/canvas/to-flat.js +85 -0
  21. package/src/canvas/validate.js +163 -0
  22. package/src/checks/binaries.js +3 -3
  23. package/src/checks/ci.js +2 -2
  24. package/src/checks/config-files.js +6 -6
  25. package/src/checks/dir-setup.js +4 -0
  26. package/src/checks/mcp-health.js +6 -0
  27. package/src/checks/ports.js +91 -29
  28. package/src/checks/rtk.js +1 -1
  29. package/src/checks/skills.js +5 -1
  30. package/src/checks/sonar.js +2 -2
  31. package/src/checks/system.js +3 -3
  32. package/src/checks/tokens.js +1 -1
  33. package/src/checks/types.js +0 -12
  34. package/src/cli/_shared.js +30 -0
  35. package/src/cli/register-meta.js +218 -0
  36. package/src/cli/register-pipeline.js +212 -0
  37. package/src/cli/register-plan.js +152 -0
  38. package/src/cli/register-roles-skills.js +38 -0
  39. package/src/cli/register-sonar.js +28 -0
  40. package/src/cli.js +83 -468
  41. package/src/commands/agents.js +18 -6
  42. package/src/commands/architect.js +29 -2
  43. package/src/commands/audit.js +265 -30
  44. package/src/commands/board.js +160 -30
  45. package/src/commands/clean.js +83 -0
  46. package/src/commands/code.js +7 -2
  47. package/src/commands/discover.js +7 -2
  48. package/src/commands/doctor.js +1 -1
  49. package/src/commands/init.js +2 -2
  50. package/src/commands/plan/_shared.js +40 -0
  51. package/src/commands/plan/add-hu.js +21 -0
  52. package/src/commands/plan/delete.js +9 -0
  53. package/src/commands/plan/generate.js +284 -0
  54. package/src/commands/plan/index.js +14 -0
  55. package/src/commands/plan/list.js +21 -0
  56. package/src/commands/plan/ready.js +46 -0
  57. package/src/commands/plan/remove-hu.js +17 -0
  58. package/src/commands/plan/show.js +81 -0
  59. package/src/commands/plan/validate.js +20 -0
  60. package/src/commands/plan.js +14 -291
  61. package/src/commands/researcher.js +7 -2
  62. package/src/commands/resume.js +24 -1
  63. package/src/commands/review.js +7 -2
  64. package/src/commands/run.js +52 -1
  65. package/src/commands/status.js +1 -1
  66. package/src/commands/sync.js +75 -0
  67. package/src/commands/triage.js +7 -2
  68. package/src/commands/undo.js +1 -1
  69. package/src/config/defaults.js +213 -0
  70. package/src/config/loader.js +137 -0
  71. package/src/config/overrides.js +199 -0
  72. package/src/config/role-resolver.js +159 -0
  73. package/src/config/test-harness.js +78 -0
  74. package/src/config.js +30 -632
  75. package/src/git/automation.js +2 -2
  76. package/src/guards/output-guard.js +0 -2
  77. package/src/hu/acceptance-runner.js +52 -10
  78. package/src/hu/store.js +11 -0
  79. package/src/mcp/handlers/direct-handlers.js +2 -5
  80. package/src/mcp/handlers/management-handlers.js +48 -4
  81. package/src/mcp/handlers/run-handler.js +9 -8
  82. package/src/mcp/preflight.js +32 -13
  83. package/src/mcp/server-handlers.js +4 -3
  84. package/src/mcp/suggest-handler.js +3 -2
  85. package/src/mcp/tools.js +15 -0
  86. package/src/orchestrator/agent-fallback.js +1 -1
  87. package/src/orchestrator/ci-integration.js +11 -11
  88. package/src/orchestrator/config-init.js +25 -7
  89. package/src/orchestrator/direct-actions.js +7 -3
  90. package/src/orchestrator/drivers/error-recovery.js +287 -0
  91. package/src/orchestrator/drivers/init-context.js +222 -0
  92. package/src/orchestrator/drivers/iteration-loop.js +301 -0
  93. package/src/orchestrator/drivers/iteration-phases/coder-and-refactorer.js +48 -0
  94. package/src/orchestrator/drivers/iteration-phases/guards.js +84 -0
  95. package/src/orchestrator/drivers/iteration-phases/handle-approved.js +29 -0
  96. package/src/orchestrator/drivers/iteration-phases/quality-gates.js +76 -0
  97. package/src/orchestrator/drivers/iteration-phases/reviewer-gate.js +60 -0
  98. package/src/orchestrator/drivers/post-loop.js +370 -0
  99. package/src/orchestrator/drivers/pre-loop-phases/auto-hu-batch.js +116 -0
  100. package/src/orchestrator/drivers/pre-loop-phases/config-deprecations.js +47 -0
  101. package/src/orchestrator/drivers/pre-loop-phases/ensure-addyosmani-skills.js +84 -0
  102. package/src/orchestrator/drivers/pre-loop-phases/inject-loaded-plan.js +268 -0
  103. package/src/orchestrator/drivers/pre-loop.js +431 -0
  104. package/src/orchestrator/drivers/run-hu-batch.js +367 -0
  105. package/src/orchestrator/feedback-queue.js +0 -1
  106. package/src/orchestrator/flow-control.js +4 -3
  107. package/src/orchestrator/flow-runner.js +121 -2077
  108. package/src/orchestrator/fs-leak-detector.js +156 -0
  109. package/src/orchestrator/hu-sub-pipeline.js +197 -9
  110. package/src/orchestrator/integrations.js +81 -0
  111. package/src/orchestrator/post-loop-stages.js +35 -8
  112. package/src/orchestrator/preflight-checks.js +20 -7
  113. package/src/orchestrator/repair/repair-detector.js +117 -0
  114. package/src/orchestrator/repair/repair-runner.js +142 -0
  115. package/src/orchestrator/reviewer-fallback.js +1 -1
  116. package/src/orchestrator/role-output-compressor.js +0 -1
  117. package/src/orchestrator/session-journal.js +4 -41
  118. package/src/orchestrator/solomon-escalation.js +3 -2
  119. package/src/orchestrator/solomon-rules.js +7 -3
  120. package/src/orchestrator/stages/architect-stage.js +12 -11
  121. package/src/orchestrator/stages/coder-stage.js +56 -7
  122. package/src/orchestrator/stages/hu-reviewer-phases/detect-and-apply-splits.js +127 -0
  123. package/src/orchestrator/stages/hu-reviewer-stage.js +6 -79
  124. package/src/orchestrator/stages/planner-stage.js +1 -1
  125. package/src/orchestrator/stages/research-stage.js +1 -1
  126. package/src/orchestrator/stages/reviewer-stage.js +7 -7
  127. package/src/orchestrator/stages/sonar-stage.js +94 -10
  128. package/src/orchestrator/stages/stage-classes.js +84 -0
  129. package/src/orchestrator/stages/stage-executor.js +1 -1
  130. package/src/orchestrator/stages/triage-stage.js +1 -1
  131. package/src/orchestrator/standby.js +3 -2
  132. package/src/orchestrator/verification-gate.js +20 -12
  133. package/src/plan/adr-generator.js +170 -0
  134. package/src/plan/adr-loader.js +60 -0
  135. package/src/plan/plan-executor.js +19 -1
  136. package/src/plan/plan-hu-ops.js +170 -1
  137. package/src/plan/plan-reviewer.js +199 -0
  138. package/src/plan/plan-schema.js +69 -1
  139. package/src/plan/plan-store.js +40 -17
  140. package/src/plan/spec-detector.js +61 -0
  141. package/src/plan/tests-synthesizer.js +116 -0
  142. package/src/planning-game/client.js +3 -28
  143. package/src/planning-game/decomposition.js +1 -1
  144. package/src/planning-game/pipeline-adapter.js +37 -6
  145. package/src/prompts/audit.js +206 -6
  146. package/src/prompts/coder.js +172 -1
  147. package/src/prompts/hu-reviewer.js +0 -1
  148. package/src/prompts/planner.js +81 -4
  149. package/src/prompts/rtk-snippet.js +1 -1
  150. package/src/review/diff-generator.js +35 -22
  151. package/src/review/snapshot-diff.js +1 -1
  152. package/src/roles/audit-role.js +125 -13
  153. package/src/roles/coder-role.js +25 -3
  154. package/src/roles/planner-role.js +42 -1
  155. package/src/roles/repairer-role.js +181 -0
  156. package/src/roles/tester-role.js +70 -4
  157. package/src/session/mutators.js +304 -0
  158. package/src/session/runtime-overrides.js +59 -0
  159. package/src/{session-store.js → session/store.js} +2 -2
  160. package/src/skills/semantic-detector.js +10 -5
  161. package/src/skills/skill-detector.js +1 -1
  162. package/src/sonar/api.js +1 -1
  163. package/src/sonar/cloud-scanner.js +0 -1
  164. package/src/sonar/credentials.js +14 -6
  165. package/src/sonar/discovery.js +123 -0
  166. package/src/sonar/manager.js +18 -0
  167. package/src/sync/detect-drift.js +113 -0
  168. package/src/tracker-bootstrap.js +44 -0
  169. package/src/utils/agent-detect.js +15 -11
  170. package/src/utils/board-prompt-bridge.js +109 -0
  171. package/src/utils/cli-progress.js +161 -0
  172. package/src/utils/cwd-confirm.js +39 -0
  173. package/src/utils/derive-project-name-from-cwd.js +119 -0
  174. package/src/utils/display/event-handlers.js +1 -1
  175. package/src/utils/display/header.js +1 -1
  176. package/src/utils/display/session-summary.js +3 -3
  177. package/src/utils/display/solomon.js +1 -1
  178. package/src/utils/garbage-collector.js +396 -0
  179. package/src/utils/git.js +16 -4
  180. package/src/utils/locale.js +1 -1
  181. package/src/utils/paths.js +21 -1
  182. package/src/utils/process.js +1 -1
  183. package/src/utils/prompt-project-name.js +35 -0
  184. package/src/utils/role-assigner.js +1 -1
  185. package/src/utils/run-context.js +75 -0
  186. package/src/utils/run-log.js +12 -0
  187. package/src/utils/task-file.js +1 -1
  188. package/src/utils/telemetry.js +13 -1
  189. package/src/webperf/devtools-detect.js +1 -1
  190. package/templates/coder-rules.md +13 -0
  191. package/templates/roles/coder.md +34 -0
  192. package/templates/roles/repairer.md +94 -0
@@ -0,0 +1,268 @@
1
+ /**
2
+ * Pre-loop phase: load a persisted plan and inject it into stageResults.
3
+ *
4
+ * Extracted from `src/orchestrator/drivers/pre-loop.js` in PR-K (audit
5
+ * follow-up) to drag runPreLoopStages out of god-function territory
6
+ * (was 496 LOC; extracting this 189-LOC block brings it to ~310).
7
+ *
8
+ * Behaviour is byte-for-byte identical to the inlined version. The
9
+ * extraction keeps the dynamic-import pattern (deferred load of
10
+ * plan-store / plan-schema / plan-executor / plan-hu-ops) so the
11
+ * dynamic-imports budget test stays unchanged.
12
+ *
13
+ * Returns { handled, plannedTask }:
14
+ * - handled=true → caller short-circuits (plan was loaded; skip
15
+ * researcher/architect/planner phases below).
16
+ * - handled=false → caller continues with the regular planning flow.
17
+ * Either flags.plan was unset OR loadPlan returned null OR the
18
+ * try/catch swallowed an error (logged as a warning).
19
+ *
20
+ * The function still mutates session via the existing mutators
21
+ * (setPlanRef, setPlanSyncCallback, setLiveStatusUpdater,
22
+ * setLiveOutcomeUpdater, setPreLoopContext) and stageResults
23
+ * (researcher / architect / planner / huReviewer slots). That
24
+ * mutation contract is unchanged from the inlined version — see
25
+ * tests/architecture/session-write-boundary.test.js for the
26
+ * mutator-routing invariant.
27
+ */
28
+
29
+ import { saveSession } from "#session/store.js";
30
+ import {
31
+ setPlanRef, setPlanSyncCallback, setLiveStatusUpdater,
32
+ setLiveOutcomeUpdater, setPreLoopContext,
33
+ } from "#session/mutators.js";
34
+ import { emitProgress, makeEvent } from "#utils/events.js";
35
+
36
+ /**
37
+ * @param {object} args
38
+ * @param {object} args.flags — runFlow flags (reads flags.plan, flags.hu)
39
+ * @param {object} args.updatedConfig — the running config (reads projectDir)
40
+ * @param {object} args.session
41
+ * @param {object} args.stageResults — mutated: researcher / architect / planner / huReviewer slots
42
+ * @param {object} args.eventBase
43
+ * @param {object} args.emitter
44
+ * @param {object} args.logger
45
+ * @param {string} args.task — original task description, used to build plannedTask
46
+ * @returns {Promise<{handled: boolean, plannedTask: string|null}>}
47
+ */
48
+ export async function injectLoadedPlan({ flags, updatedConfig, session, stageResults, eventBase, emitter, logger, task }) {
49
+ if (!flags.plan) return { handled: false, plannedTask: null };
50
+
51
+ try {
52
+ const { loadPlan, savePlan: savePlanToDisk } = await import("../../../plan/plan-store.js");
53
+ const { isPlanV2 } = await import("../../../plan/plan-schema.js");
54
+ const { planToHuBatch, syncResultsToPlan } = await import("../../../plan/plan-executor.js");
55
+ const projectDir = updatedConfig.projectDir || process.cwd();
56
+ const loadedPlan = await loadPlan(projectDir, flags.plan);
57
+ if (!loadedPlan) {
58
+ logger.warn(`Plan ${flags.plan} not found — falling back to normal pipeline`);
59
+ return { handled: false, plannedTask: null };
60
+ }
61
+
62
+ logger.info(`Loaded persisted plan: ${flags.plan} (v${loadedPlan.version || 1})`);
63
+ emitProgress(emitter, makeEvent("plan:loaded", { ...eventBase, stage: "plan" }, {
64
+ message: `Plan loaded: ${flags.plan}`,
65
+ detail: { planId: flags.plan, task: loadedPlan.task, version: loadedPlan.version, huCount: loadedPlan.hus?.length || 0 }
66
+ }));
67
+ stageResults.researcher = { ok: true, summary: "Loaded from persisted plan", fromPlan: flags.plan };
68
+ stageResults.architect = { ok: true, summary: "Loaded from persisted plan", fromPlan: flags.plan };
69
+ stageResults.planner = { ok: true, summary: "Loaded from persisted plan", fromPlan: flags.plan };
70
+
71
+ // Tests-first gate (v2.7.5): refuse to run a plan when any HU
72
+ // has no acceptance_tests. Mirrors the gate in `kj plan ready`
73
+ // so the CLI path and the board's "▶ Run plan" button enforce
74
+ // the same contract.
75
+ if (isPlanV2(loadedPlan) && Array.isArray(loadedPlan.hus)) {
76
+ assertEveryHuHasTests({ loadedPlan, planId: flags.plan, eventBase, emitter, logger });
77
+
78
+ // PR-E (auto-certify + anomaly guards): the user expects
79
+ // pending HUs to be runnable; "certified" was an invisible
80
+ // wall. Helpers in plan-hu-ops.js so they can be unit-tested
81
+ // without spinning up the orchestrator. Pulled from the same
82
+ // dynamic import we use a few lines below for updateHuStatus +
83
+ // setHuOutcome — no extra import budget.
84
+ const planHuOps = await import("../../../plan/plan-hu-ops.js");
85
+ await autoCertifyAndAssertRunnable({ planHuOps, loadedPlan, planId: flags.plan, projectDir, savePlanToDisk, eventBase, emitter, logger });
86
+
87
+ // v2 plan with HUs → inject as huReviewer so sub-pipeline
88
+ // picks them up. Sets every callback (sync, live status, live
89
+ // outcome) and stamps plan.status = "running".
90
+ if (loadedPlan.hus?.length > 0) {
91
+ const huBatch = planToHuBatch(loadedPlan);
92
+ applyHuFilterIfRequested({ huBatch, flags, eventBase, emitter, logger });
93
+ if (huBatch.ok) {
94
+ stageResults.huReviewer = huBatch;
95
+ wirePlanCallbacks({ session, loadedPlan, projectDir, syncResultsToPlan, savePlanToDisk, planHuOps, logger });
96
+ logger.info(`Plan ${flags.plan}: ${huBatch.certified} HUs ready for execution`);
97
+ emitProgress(emitter, makeEvent("plan:hus-loaded", { ...eventBase, stage: "plan" }, {
98
+ message: `${huBatch.certified} HUs loaded from plan`,
99
+ detail: { planId: flags.plan, total: huBatch.total, certified: huBatch.certified }
100
+ }));
101
+ loadedPlan.status = "running";
102
+ await savePlanToDisk(projectDir, loadedPlan);
103
+ }
104
+ }
105
+ }
106
+
107
+ // Inject contexts that the legacy planning phases would have
108
+ // produced. Then build a plannedTask string for the non-HU /
109
+ // fallback iteration path.
110
+ const ctx = loadedPlan.context || {};
111
+ setPreLoopContext(session, {
112
+ research: ctx.researchContext || loadedPlan.researchContext || null,
113
+ architect: ctx.architectContext || loadedPlan.architectContext || null,
114
+ plan: loadedPlan.approach || loadedPlan.plan || null,
115
+ });
116
+ await saveSession(session);
117
+
118
+ return { handled: true, plannedTask: buildPlannedTaskFromLoadedPlan({ task, loadedPlan }) };
119
+ } catch (err) {
120
+ logger.warn(`Plan loading failed: ${err.message} — falling back to normal pipeline`);
121
+ return { handled: false, plannedTask: null };
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Throws when any HU lacks an acceptance_tests array with at least
127
+ * one entry. Logs + emits the same `plan:tests-missing` progress
128
+ * event as the inlined version did.
129
+ */
130
+ function assertEveryHuHasTests({ loadedPlan, planId, eventBase, emitter, logger }) {
131
+ const missing = loadedPlan.hus.filter(
132
+ (h) => !Array.isArray(h.acceptance_tests) || h.acceptance_tests.length === 0
133
+ );
134
+ if (missing.length === 0) return;
135
+ const offenders = missing.map((h) => ` - ${h.id} ${(h.title || "").slice(0, 60)}`).join("\n");
136
+ const msg =
137
+ `Refusing to run plan ${planId}: ${missing.length}/${loadedPlan.hus.length} HU(s) have no acceptance_tests.\n`
138
+ + `${offenders}\n\n`
139
+ + "Tests-first flow requires a test contract per HU. Edit each HU on the board\n"
140
+ + "(http://localhost:4000) via the ✎ Edit button, or re-run `kj plan` to regenerate.";
141
+ logger.error(msg);
142
+ emitProgress(emitter, makeEvent("plan:tests-missing", { ...eventBase, stage: "plan" }, {
143
+ status: "fail",
144
+ message: `Plan ${planId}: ${missing.length} HU(s) missing acceptance_tests`,
145
+ detail: { planId, missingHuIds: missing.map((h) => h.id) }
146
+ }));
147
+ throw new Error(`Plan ${planId} has ${missing.length} HU(s) with no acceptance_tests — see the run log for details.`);
148
+ }
149
+
150
+ /**
151
+ * Promotes pending HUs with tests to certified, then asserts at
152
+ * least one HU is runnable. Throws (with `plan:no-runnable` event)
153
+ * when nothing is left to do, so the orchestrator never silently
154
+ * falls back to single-task mode and burns tokens.
155
+ */
156
+ async function autoCertifyAndAssertRunnable({ planHuOps, loadedPlan, planId, projectDir, savePlanToDisk, eventBase, emitter, logger }) {
157
+ const { autoCertifyPendingHus, assertPlanRunnable } = planHuOps;
158
+ const promoted = autoCertifyPendingHus(loadedPlan);
159
+ if (promoted > 0) {
160
+ await savePlanToDisk(projectDir, loadedPlan);
161
+ logger.info(`Plan ${planId}: auto-certified ${promoted} pending HU(s) — ready to run`);
162
+ emitProgress(emitter, makeEvent("plan:auto-certified", { ...eventBase, stage: "plan" }, {
163
+ message: `Auto-certificadas ${promoted} HU(s) pendientes`,
164
+ detail: { planId, count: promoted }
165
+ }));
166
+ }
167
+ try {
168
+ assertPlanRunnable(loadedPlan, planId);
169
+ } catch (err) {
170
+ logger.error(err.message);
171
+ emitProgress(emitter, makeEvent("plan:no-runnable", { ...eventBase, stage: "plan" }, {
172
+ status: "fail", message: err.message,
173
+ detail: { planId }
174
+ }));
175
+ throw err;
176
+ }
177
+ }
178
+
179
+ /**
180
+ * PR4: when --hu <ids> is set, narrow the batch to those HU ids by
181
+ * marking the rest as `done` in memory. The plan JSON on disk is
182
+ * NOT touched — this is a run-time filter only. Throws when none of
183
+ * the requested ids exist.
184
+ */
185
+ function applyHuFilterIfRequested({ huBatch, flags, eventBase, emitter, logger }) {
186
+ if (!huBatch.ok || !flags.hu) return;
187
+ const requested = String(flags.hu).split(",").map((s) => s.trim()).filter(Boolean);
188
+ if (requested.length === 0) {
189
+ logger.warn(`--hu was empty after parsing — falling back to running every certified HU`);
190
+ return;
191
+ }
192
+ const requestedSet = new Set(requested);
193
+ const found = huBatch.stories.filter((s) => requestedSet.has(s.id));
194
+ const missing = requested.filter((id) => !huBatch.stories.some((s) => s.id === id));
195
+ if (missing.length > 0) {
196
+ logger.warn(`--hu: ignored ${missing.length} unknown HU id(s): ${missing.join(", ")}`);
197
+ }
198
+ if (found.length === 0) {
199
+ throw new Error(`--hu: none of the requested HU id(s) exist in plan ${flags.plan}: ${requested.join(", ")}`);
200
+ }
201
+ for (const story of huBatch.stories) {
202
+ if (!requestedSet.has(story.id)) story.status = "done";
203
+ }
204
+ huBatch.certified = found.length;
205
+ logger.info(`Plan ${flags.plan}: --hu filter active — ${found.length}/${huBatch.total} HU(s) will run (${found.map((s) => s.id).join(", ")})`);
206
+ emitProgress(emitter, makeEvent("plan:hu-filter", { ...eventBase, stage: "plan" }, {
207
+ message: `--hu filter: ${found.length} HU(s) selected, ${huBatch.total - found.length} skipped`,
208
+ detail: { planId: flags.plan, requested, ignored: missing, willRun: found.map((s) => s.id) }
209
+ }));
210
+ }
211
+
212
+ /**
213
+ * Wire the four session-scoped callbacks the run will invoke as it
214
+ * progresses: PR1 live status (per HU), PR3 live outcome (per HU
215
+ * exit), and the syncResultsToPlan rollup (end-of-run).
216
+ */
217
+ function wirePlanCallbacks({ session, loadedPlan, projectDir, syncResultsToPlan, savePlanToDisk, planHuOps, logger }) {
218
+ setPlanRef(session, { planId: loadedPlan.planId, projectDir });
219
+ setPlanSyncCallback(session, async (subResult) => {
220
+ syncResultsToPlan(loadedPlan, subResult);
221
+ await savePlanToDisk(projectDir, loadedPlan);
222
+ });
223
+ // PR1: live HU status. Without this the plan only updates at
224
+ // end-of-run and the board's Kanban looks frozen during execution.
225
+ // (`planHuOps` was loaded above for PR-E auto-certify — reuse the
226
+ // reference to stay under the dynamic-import budget.)
227
+ const { updateHuStatus: updateHuStatusFn, setHuOutcome: setHuOutcomeFn } = planHuOps;
228
+ setLiveStatusUpdater(session, async (huId, status) => {
229
+ try {
230
+ if (!updateHuStatusFn(loadedPlan, huId, status)) return;
231
+ await savePlanToDisk(projectDir, loadedPlan);
232
+ } catch (err) {
233
+ logger?.warn?.(`Live status update failed for ${huId}: ${err.message}`);
234
+ }
235
+ });
236
+ // PR3: per-HU outcome. Stamped once per HU at runSingleHu's exit
237
+ // so the board can show the 📄 indicator as soon as each HU
238
+ // finishes (iterations / duration / commits / blockers / summary).
239
+ setLiveOutcomeUpdater(session, async (huId, outcome) => {
240
+ try {
241
+ if (!setHuOutcomeFn(loadedPlan, huId, outcome)) return;
242
+ await savePlanToDisk(projectDir, loadedPlan);
243
+ } catch (err) {
244
+ logger?.warn?.(`Live outcome update failed for ${huId}: ${err.message}`);
245
+ }
246
+ });
247
+ }
248
+
249
+ /**
250
+ * Build a single plannedTask string for the non-HU / fallback
251
+ * iteration path. The HU sub-pipeline doesn't read this; only the
252
+ * legacy single-iteration loop does. Identical to the inlined
253
+ * version's behaviour.
254
+ */
255
+ function buildPlannedTaskFromLoadedPlan({ task, loadedPlan }) {
256
+ const plan = loadedPlan.plan || (loadedPlan.approach ? { approach: loadedPlan.approach } : null);
257
+ if (plan && typeof plan === "object" && plan.steps) {
258
+ const stepList = plan.steps.map((s, idx) => `${idx + 1}. ${s.description || s}`).join("\n");
259
+ return `${task}\n\n## Implementation Plan\n${plan.approach || ""}\n\n## Steps\n${stepList}`;
260
+ }
261
+ if (typeof plan === "string") {
262
+ return `${task}\n\n## Implementation Plan\n${plan}`;
263
+ }
264
+ if (loadedPlan.approach) {
265
+ return `${task}\n\n## Approach\n${loadedPlan.approach}`;
266
+ }
267
+ return task;
268
+ }