karajan-code 1.36.1 → 1.38.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.
@@ -9,6 +9,7 @@ import { createArchitectADRs } from "../planning-game/architect-adrs.js";
9
9
  import { addCheckpoint, markSessionStatus } from "../session-store.js";
10
10
  import { emitProgress, makeEvent } from "../utils/events.js";
11
11
  import { parsePlannerOutput } from "../prompts/planner.js";
12
+ import { buildDecompositionPrompt, parseDecompositionOutput } from "../prompts/hu-reviewer.js";
12
13
  import { selectModelsForRoles } from "../utils/model-selector.js";
13
14
  import { createStallDetector } from "../utils/stall-detector.js";
14
15
  import { createHuBatch, loadHuBatch, saveHuBatch, updateStoryStatus, updateStoryQuality, updateStoryCertified, addContextRequest, answerContextRequest } from "../hu/store.js";
@@ -48,14 +49,16 @@ function applyModelSelection(triageOutput, config, emitter, eventBase) {
48
49
 
49
50
  export async function runTriageStage({ config, logger, emitter, eventBase, session, coderRole, trackBudget }) {
50
51
  logger.setContext({ iteration: 0, stage: "triage" });
52
+
53
+ const triageProvider = config?.roles?.triage?.provider || coderRole.provider;
51
54
  emitProgress(
52
55
  emitter,
53
56
  makeEvent("triage:start", { ...eventBase, stage: "triage" }, {
54
- message: "Triage classifying task complexity"
57
+ message: "Triage classifying task complexity",
58
+ detail: { provider: triageProvider, executorType: "agent" }
55
59
  })
56
60
  );
57
61
 
58
- const triageProvider = config?.roles?.triage?.provider || coderRole.provider;
59
62
  const triageOnOutput = ({ stream, line }) => {
60
63
  emitProgress(emitter, makeEvent("agent:output", { ...eventBase, stage: "triage" }, {
61
64
  message: line,
@@ -132,7 +135,7 @@ export async function runTriageStage({ config, logger, emitter, eventBase, sessi
132
135
  makeEvent("triage:end", { ...eventBase, stage: "triage" }, {
133
136
  status: triageOutput.ok ? "ok" : "fail",
134
137
  message: triageOutput.ok ? "Triage completed" : `Triage failed: ${triageOutput.summary}`,
135
- detail: stageResult
138
+ detail: { ...stageResult, provider: triageProvider, executorType: "agent" }
136
139
  })
137
140
  );
138
141
 
@@ -141,14 +144,16 @@ export async function runTriageStage({ config, logger, emitter, eventBase, sessi
141
144
 
142
145
  export async function runResearcherStage({ config, logger, emitter, eventBase, session, coderRole, trackBudget }) {
143
146
  logger.setContext({ iteration: 0, stage: "researcher" });
147
+
148
+ const researcherProvider = config?.roles?.researcher?.provider || coderRole.provider;
144
149
  emitProgress(
145
150
  emitter,
146
151
  makeEvent("researcher:start", { ...eventBase, stage: "researcher" }, {
147
- message: "Researcher investigating codebase"
152
+ message: "Researcher investigating codebase",
153
+ detail: { researcher: researcherProvider, provider: researcherProvider, executorType: "agent" }
148
154
  })
149
155
  );
150
156
 
151
- const researcherProvider = config?.roles?.researcher?.provider || coderRole.provider;
152
157
  const researcherOnOutput = ({ stream, line }) => {
153
158
  emitProgress(emitter, makeEvent("agent:output", { ...eventBase, stage: "researcher" }, {
154
159
  message: line,
@@ -191,7 +196,8 @@ export async function runResearcherStage({ config, logger, emitter, eventBase, s
191
196
  emitter,
192
197
  makeEvent("researcher:end", { ...eventBase, stage: "researcher" }, {
193
198
  status: researchOutput.ok ? "ok" : "fail",
194
- message: researchOutput.ok ? "Research completed" : `Research failed: ${researchOutput.summary}`
199
+ message: researchOutput.ok ? "Research completed" : `Research failed: ${researchOutput.summary}`,
200
+ detail: { provider: researcherProvider, executorType: "agent" }
195
201
  })
196
202
  );
197
203
 
@@ -260,14 +266,14 @@ async function handleArchitectClarification({ architectOutput, askQuestion, conf
260
266
 
261
267
  export async function runArchitectStage({ config, logger, emitter, eventBase, session, coderRole, trackBudget, researchContext = null, discoverResult = null, triageLevel = null, askQuestion = null }) {
262
268
  logger.setContext({ iteration: 0, stage: "architect" });
269
+ const architectProvider = config?.roles?.architect?.provider || coderRole.provider;
263
270
  emitProgress(
264
271
  emitter,
265
272
  makeEvent("architect:start", { ...eventBase, stage: "architect" }, {
266
- message: "Architect designing solution architecture"
273
+ message: "Architect designing solution architecture",
274
+ detail: { architect: architectProvider, provider: architectProvider, executorType: "agent" }
267
275
  })
268
276
  );
269
-
270
- const architectProvider = config?.roles?.architect?.provider || coderRole.provider;
271
277
  const architectOnOutput = ({ stream, line }) => {
272
278
  emitProgress(emitter, makeEvent("agent:output", { ...eventBase, stage: "architect" }, {
273
279
  message: line,
@@ -330,7 +336,7 @@ export async function runArchitectStage({ config, logger, emitter, eventBase, se
330
336
  makeEvent("architect:end", { ...eventBase, stage: "architect" }, {
331
337
  status: architectOutput.ok ? "ok" : "fail",
332
338
  message: architectOutput.ok ? "Architecture completed" : `Architecture failed: ${architectOutput.summary}`,
333
- detail: stageResult
339
+ detail: { ...stageResult, provider: architectProvider, executorType: "agent" }
334
340
  })
335
341
  );
336
342
 
@@ -373,7 +379,7 @@ export async function runPlannerStage({ config, logger, emitter, eventBase, sess
373
379
  emitter,
374
380
  makeEvent("planner:start", { ...eventBase, stage: "planner" }, {
375
381
  message: `Planner (${plannerRole.provider}) running`,
376
- detail: { planner: plannerRole.provider }
382
+ detail: { planner: plannerRole.provider, provider: plannerRole.provider, executorType: "agent" }
377
383
  })
378
384
  );
379
385
 
@@ -416,7 +422,8 @@ export async function runPlannerStage({ config, logger, emitter, eventBase, sess
416
422
  emitter,
417
423
  makeEvent("planner:end", { ...eventBase, stage: "planner" }, {
418
424
  status: "fail",
419
- message: `Planner failed: ${details}`
425
+ message: `Planner failed: ${details}`,
426
+ detail: { provider: plannerRole.provider, executorType: "agent" }
420
427
  })
421
428
  );
422
429
  throw new Error(`Planner failed: ${details}`);
@@ -436,7 +443,8 @@ export async function runPlannerStage({ config, logger, emitter, eventBase, sess
436
443
  emitProgress(
437
444
  emitter,
438
445
  makeEvent("planner:end", { ...eventBase, stage: "planner" }, {
439
- message: "Planner completed"
446
+ message: "Planner completed",
447
+ detail: { provider: plannerRole.provider, executorType: "agent" }
440
448
  })
441
449
  );
442
450
 
@@ -445,14 +453,14 @@ export async function runPlannerStage({ config, logger, emitter, eventBase, sess
445
453
 
446
454
  export async function runDiscoverStage({ config, logger, emitter, eventBase, session, coderRole, trackBudget }) {
447
455
  logger.setContext({ iteration: 0, stage: "discover" });
456
+ const discoverProvider = config?.roles?.discover?.provider || coderRole.provider;
448
457
  emitProgress(
449
458
  emitter,
450
459
  makeEvent("discover:start", { ...eventBase, stage: "discover" }, {
451
- message: "Discover analyzing task for gaps"
460
+ message: "Discover analyzing task for gaps",
461
+ detail: { discover: discoverProvider, provider: discoverProvider, executorType: "agent" }
452
462
  })
453
463
  );
454
-
455
- const discoverProvider = config?.roles?.discover?.provider || coderRole.provider;
456
464
  const discoverOnOutput = ({ stream, line }) => {
457
465
  emitProgress(emitter, makeEvent("agent:output", { ...eventBase, stage: "discover" }, {
458
466
  message: line,
@@ -504,7 +512,7 @@ export async function runDiscoverStage({ config, logger, emitter, eventBase, ses
504
512
  makeEvent("discover:end", { ...eventBase, stage: "discover" }, {
505
513
  status: discoverOutput.ok ? "ok" : "fail",
506
514
  message: discoverOutput.ok ? "Discovery completed" : `Discovery failed: ${discoverOutput.summary}`,
507
- detail: stageResult
515
+ detail: { ...stageResult, provider: discoverProvider, executorType: "agent" }
508
516
  })
509
517
  );
510
518
 
@@ -512,43 +520,140 @@ export async function runDiscoverStage({ config, logger, emitter, eventBase, ses
512
520
  }
513
521
 
514
522
  /**
515
- * Run the HU Reviewer stage: load stories from YAML, evaluate, certify, and return in topological order.
523
+ * Use an AI agent to decompose a complex task into multiple formal HUs.
524
+ * Returns null if decomposition fails or the task is too simple.
516
525
  * @param {object} params
517
- * @returns {Promise<{stageResult: object}>}
526
+ * @returns {Promise<Array|null>} Array of decomposed HUs or null.
518
527
  */
519
- export async function runHuReviewerStage({ config, logger, emitter, eventBase, session, coderRole, trackBudget, huFile, askQuestion }) {
520
- logger.setContext({ iteration: 0, stage: "hu-reviewer" });
528
+ async function decomposeTaskIntoHUs({ config, logger, emitter, eventBase, session, coderRole, trackBudget }) {
529
+ const provider = config?.roles?.hu_reviewer?.provider || coderRole.provider;
530
+ const prompt = buildDecompositionPrompt(session.task);
531
+
521
532
  emitProgress(
522
533
  emitter,
523
- makeEvent("hu-reviewer:start", { ...eventBase, stage: "hu-reviewer" }, {
524
- message: "HU Reviewer certifying user stories"
534
+ makeEvent("hu-reviewer:decompose-start", { ...eventBase, stage: "hu-reviewer" }, {
535
+ message: "Decomposing complex task into formal HUs",
536
+ detail: { provider }
525
537
  })
526
538
  );
527
539
 
528
- // --- Load YAML file ---
529
- const yaml = await import("js-yaml");
530
- const fs = await import("node:fs/promises");
531
- let rawYaml;
540
+ const onOutput = ({ stream, line }) => {
541
+ emitProgress(emitter, makeEvent("agent:output", { ...eventBase, stage: "hu-reviewer" }, {
542
+ message: line,
543
+ detail: { stream, agent: provider }
544
+ }));
545
+ };
546
+ const stall = createStallDetector({
547
+ onOutput, emitter, eventBase, stage: "hu-reviewer", provider
548
+ });
549
+
550
+ const agent = createAgent(provider, config, logger);
551
+ const decompStart = Date.now();
552
+ let result;
532
553
  try {
533
- rawYaml = await fs.readFile(huFile, "utf8");
554
+ result = await agent.runTask({ prompt, role: "hu-reviewer", onOutput: stall.onOutput });
534
555
  } catch (err) {
535
- const stageResult = { ok: false, error: `Could not read HU file: ${err.message}` };
536
- emitProgress(emitter, makeEvent("hu-reviewer:end", { ...eventBase, stage: "hu-reviewer" }, {
537
- status: "fail", message: stageResult.error
538
- }));
539
- return { stageResult };
556
+ logger.warn(`HU decomposition threw: ${err.message}`);
557
+ return null;
558
+ } finally {
559
+ stall.stop();
540
560
  }
541
561
 
562
+ trackBudget({
563
+ role: "hu-reviewer-decompose",
564
+ provider,
565
+ model: config?.roles?.hu_reviewer?.model || coderRole.model,
566
+ result,
567
+ duration_ms: Date.now() - decompStart
568
+ });
569
+
570
+ if (!result.ok) {
571
+ logger.warn(`HU decomposition failed: ${result.error || "unknown"}`);
572
+ return null;
573
+ }
574
+
575
+ const stories = parseDecompositionOutput(result.output);
576
+ if (!stories || stories.length < 2) {
577
+ logger.info("HU decomposition returned < 2 stories, falling back to single auto-story");
578
+ return null;
579
+ }
580
+
581
+ emitProgress(
582
+ emitter,
583
+ makeEvent("hu-reviewer:decompose-end", { ...eventBase, stage: "hu-reviewer" }, {
584
+ message: `Task decomposed into ${stories.length} HUs`,
585
+ detail: { count: stories.length, ids: stories.map(s => s.id) }
586
+ })
587
+ );
588
+
589
+ return stories;
590
+ }
591
+
592
+ /**
593
+ * Run the HU Reviewer stage: load stories from YAML or generate from task, evaluate, certify, and return in topological order.
594
+ * @param {object} params
595
+ * @returns {Promise<{stageResult: object}>}
596
+ */
597
+ export async function runHuReviewerStage({ config, logger, emitter, eventBase, session, coderRole, trackBudget, huFile, askQuestion, pgStories = null }) {
598
+ logger.setContext({ iteration: 0, stage: "hu-reviewer" });
599
+ const huReviewerProvider = config?.roles?.hu_reviewer?.provider || coderRole.provider;
600
+ emitProgress(
601
+ emitter,
602
+ makeEvent("hu-reviewer:start", { ...eventBase, stage: "hu-reviewer" }, {
603
+ message: "HU Reviewer certifying user stories",
604
+ detail: { provider: huReviewerProvider, executorType: "agent" }
605
+ })
606
+ );
607
+
542
608
  let stories;
543
- try {
544
- const parsed = yaml.load(rawYaml);
545
- stories = Array.isArray(parsed) ? parsed : (parsed?.stories || []);
546
- } catch (err) {
547
- const stageResult = { ok: false, error: `Invalid YAML in HU file: ${err.message}` };
548
- emitProgress(emitter, makeEvent("hu-reviewer:end", { ...eventBase, stage: "hu-reviewer" }, {
549
- status: "fail", message: stageResult.error
609
+
610
+ if (huFile) {
611
+ // --- Load YAML file ---
612
+ const yaml = await import("js-yaml");
613
+ const fs = await import("node:fs/promises");
614
+ let rawYaml;
615
+ try {
616
+ rawYaml = await fs.readFile(huFile, "utf8");
617
+ } catch (err) {
618
+ const stageResult = { ok: false, error: `Could not read HU file: ${err.message}` };
619
+ emitProgress(emitter, makeEvent("hu-reviewer:end", { ...eventBase, stage: "hu-reviewer" }, {
620
+ status: "fail", message: stageResult.error
621
+ }));
622
+ return { stageResult };
623
+ }
624
+
625
+ try {
626
+ const parsed = yaml.load(rawYaml);
627
+ stories = Array.isArray(parsed) ? parsed : (parsed?.stories || []);
628
+ } catch (err) {
629
+ const stageResult = { ok: false, error: `Invalid YAML in HU file: ${err.message}` };
630
+ emitProgress(emitter, makeEvent("hu-reviewer:end", { ...eventBase, stage: "hu-reviewer" }, {
631
+ status: "fail", message: stageResult.error
632
+ }));
633
+ return { stageResult };
634
+ }
635
+ } else if (pgStories && pgStories.length > 0) {
636
+ // --- Use pre-built stories from Planning Game card structured data ---
637
+ stories = pgStories;
638
+ logger.info(`HU Reviewer: using ${pgStories.length} story(ies) from PG card`);
639
+ emitProgress(emitter, makeEvent("hu-reviewer:pg-feed", { ...eventBase, stage: "hu-reviewer" }, {
640
+ message: `Using ${pgStories.length} story(ies) from Planning Game card`,
641
+ detail: { source: "pg-card", storyIds: pgStories.map(s => s.id) }
550
642
  }));
551
- return { stageResult };
643
+ } else {
644
+ // --- Decompose task into formal HUs via AI, or fall back to single auto-story ---
645
+ const decomposed = await decomposeTaskIntoHUs({
646
+ config, logger, emitter, eventBase, session, coderRole, trackBudget
647
+ });
648
+ if (decomposed && decomposed.length > 1) {
649
+ stories = decomposed.map(hu => ({
650
+ id: hu.id,
651
+ text: `As a ${hu.role}, I want to ${hu.goal}, so that ${hu.benefit}\n\nTitle: ${hu.title}\nAcceptance Criteria:\n${hu.acceptanceCriteria.map(ac => `- ${ac}`).join("\n")}`,
652
+ blocked_by: hu.dependsOn || []
653
+ }));
654
+ } else {
655
+ stories = [{ id: "HU-AUTO-001", text: session.task }];
656
+ }
552
657
  }
553
658
 
554
659
  if (stories.length === 0) {
@@ -569,7 +674,6 @@ export async function runHuReviewerStage({ config, logger, emitter, eventBase, s
569
674
  }
570
675
 
571
676
  // --- Evaluate loop (re-evaluate entire batch until all certified or needs_context with no askQuestion) ---
572
- const huReviewerProvider = config?.roles?.hu_reviewer?.provider || coderRole.provider;
573
677
  const huReviewerOnOutput = ({ stream, line }) => {
574
678
  emitProgress(emitter, makeEvent("agent:output", { ...eventBase, stage: "hu-reviewer" }, {
575
679
  message: line,
@@ -711,7 +815,7 @@ export async function runHuReviewerStage({ config, logger, emitter, eventBase, s
711
815
  makeEvent("hu-reviewer:end", { ...eventBase, stage: "hu-reviewer" }, {
712
816
  status: "ok",
713
817
  message: `HU Review complete: ${certifiedStories.length}/${batch.stories.length} certified`,
714
- detail: stageResult
818
+ detail: { ...stageResult, provider: huReviewerProvider, executorType: "agent" }
715
819
  })
716
820
  );
717
821
 
@@ -167,14 +167,14 @@ export async function runPreflightChecks({ config, logger, emitter, eventBase, r
167
167
  logger.info("Preflight: skipped (no sonar, no security)");
168
168
  emitProgress(emitter, makeEvent("preflight:end", { ...eventBase, stage: "preflight" }, {
169
169
  message: "Preflight skipped (no checks needed)",
170
- detail: result
170
+ detail: { ...result, executorType: "local" }
171
171
  }));
172
172
  return result;
173
173
  }
174
174
 
175
175
  emitProgress(emitter, makeEvent("preflight:start", { ...eventBase, stage: "preflight" }, {
176
176
  message: "Running preflight environment checks",
177
- detail: { sonarEnabled, securityEnabled }
177
+ detail: { sonarEnabled, securityEnabled, executorType: "local" }
178
178
  }));
179
179
 
180
180
  // --- 1. Docker (only if sonar enabled and not external) ---
@@ -286,7 +286,7 @@ export async function runPreflightChecks({ config, logger, emitter, eventBase, r
286
286
  : hasWarnings
287
287
  ? `Preflight completed with ${result.warnings.length} warning(s)`
288
288
  : "Preflight passed — all checks OK",
289
- detail: result
289
+ detail: { ...result, executorType: "local" }
290
290
  }));
291
291
 
292
292
  return result;
@@ -9,11 +9,12 @@ export async function invokeSolomon({ config, logger, emitter, eventBase, stage,
9
9
  return escalateToHuman({ askQuestion, session, emitter, eventBase, stage, conflict, iteration });
10
10
  }
11
11
 
12
+ const solomonProvider = config?.roles?.solomon?.provider || "gemini";
12
13
  emitProgress(
13
14
  emitter,
14
15
  makeEvent("solomon:start", { ...eventBase, stage: "solomon" }, {
15
16
  message: `Solomon arbitrating ${stage} conflict`,
16
- detail: { conflictStage: stage }
17
+ detail: { conflictStage: stage, provider: solomonProvider, executorType: "agent" }
17
18
  })
18
19
  );
19
20
 
@@ -41,7 +42,7 @@ export async function invokeSolomon({ config, logger, emitter, eventBase, stage,
41
42
  message: ruling.ok
42
43
  ? `Solomon ruling: ${ruling.result?.ruling || "unknown"}`
43
44
  : `Solomon failed: ${(solomonError || ruling.summary || "unknown error").slice(0, 200)}`,
44
- detail: ruling.result
45
+ detail: { ...ruling.result, provider: solomonProvider, executorType: "agent" }
45
46
  })
46
47
  );
47
48
 
@@ -35,6 +35,7 @@ import { PipelineContext } from "./orchestrator/pipeline-context.js";
35
35
  import { runTriageStage, runResearcherStage, runArchitectStage, runPlannerStage, runDiscoverStage, runHuReviewerStage } from "./orchestrator/pre-loop-stages.js";
36
36
  import { runCoderStage, runRefactorerStage, runTddCheckStage, runSonarStage, runSonarCloudStage, runReviewerStage } from "./orchestrator/iteration-stages.js";
37
37
  import { runTesterStage, runSecurityStage, runImpeccableStage, runFinalAuditStage } from "./orchestrator/post-loop-stages.js";
38
+ import { needsSubPipeline, runHuSubPipeline } from "./orchestrator/hu-sub-pipeline.js";
38
39
  import { waitForCooldown, MAX_STANDBY_RETRIES } from "./orchestrator/standby.js";
39
40
  import { detectTestFramework } from "./utils/project-detect.js";
40
41
  import { runPreflightChecks } from "./orchestrator/preflight-checks.js";
@@ -177,7 +178,8 @@ function createBudgetManager({ config, emitter, eventBase }) {
177
178
  max_budget_usd: budgetLimit,
178
179
  warn_threshold_pct: warnThresholdPct,
179
180
  pct_used: Number(pctUsed.toFixed(2)),
180
- remaining_usd: budgetTracker.remaining(budgetLimit)
181
+ remaining_usd: budgetTracker.remaining(budgetLimit),
182
+ executorType: "system"
181
183
  }
182
184
  })
183
185
  );
@@ -379,7 +381,7 @@ async function handleCheckpoint({ checkpointDisabled, askQuestion, lastCheckpoin
379
381
  emitter,
380
382
  makeEvent("session:checkpoint", { ...eventBase, iteration: i, stage: "checkpoint" }, {
381
383
  message: `Checkpoint: progress detected, continuing (${elapsedStr} min elapsed)`,
382
- detail: { elapsed_minutes: Number(elapsedStr), iterations_done: i - 1, stages: stagesCompleted, auto_continued: true }
384
+ detail: { elapsed_minutes: Number(elapsedStr), iterations_done: i - 1, stages: stagesCompleted, auto_continued: true, executorType: "system" }
383
385
  })
384
386
  );
385
387
  return { action: "continue_loop", checkpointDisabled, lastCheckpointAt: Date.now(), lastCheckpointSnapshot: newSnapshot };
@@ -815,7 +817,7 @@ async function runPreLoopStages({ config, logger, emitter, eventBase, session, f
815
817
  const huFile = flags.huFile || null;
816
818
  if (flags.enableHuReviewer !== undefined) pipelineFlags.huReviewerEnabled = Boolean(flags.enableHuReviewer);
817
819
  if (pipelineFlags.huReviewerEnabled && huFile) {
818
- const huResult = await runHuReviewerStage({ config, logger, emitter, eventBase, session, coderRole, trackBudget, huFile, askQuestion });
820
+ const huResult = await runHuReviewerStage({ config, logger, emitter, eventBase, session, coderRole, trackBudget, huFile, askQuestion, pgStories: null });
819
821
  stageResults.huReviewer = huResult.stageResult;
820
822
  }
821
823
 
@@ -843,6 +845,20 @@ async function runPreLoopStages({ config, logger, emitter, eventBase, session, f
843
845
  applyTriageOverrides(pipelineFlags, triageResult.roleOverrides);
844
846
  stageResults.triage = triageResult.stageResult;
845
847
 
848
+ // --- HU Reviewer auto-activation from triage (post-triage, no huFile needed) ---
849
+ const triageRoles = new Set(triageResult.stageResult?.roles || []);
850
+ if (triageRoles.has("hu-reviewer") && !stageResults.huReviewer) {
851
+ pipelineFlags.huReviewerEnabled = true;
852
+ // Feed PG card structured data to hu-reviewer when available
853
+ let pgStories = null;
854
+ if (pgTaskId && pgProject && session.pg_card) {
855
+ const { buildHuStoriesFromPgCard } = await import("./planning-game/pipeline-adapter.js");
856
+ pgStories = buildHuStoriesFromPgCard(session.pg_card);
857
+ }
858
+ const huResult = await runHuReviewerStage({ config, logger, emitter, eventBase, session, coderRole, trackBudget, huFile: null, askQuestion, pgStories });
859
+ stageResults.huReviewer = huResult.stageResult;
860
+ }
861
+
846
862
  // --- Auto-simplify pipeline for simple tasks (before explicit flag overrides) ---
847
863
  const simplified = applyAutoSimplify({
848
864
  pipelineFlags,
@@ -956,7 +972,7 @@ async function runGuardStages({ config, logger, emitter, eventBase, session, ite
956
972
  const warnings = outputResult.violations.filter(v => v.severity === "warning");
957
973
  emitProgress(emitter, makeEvent("guard:output", { ...eventBase, stage: "guard" }, {
958
974
  message: `Output guard: ${critical.length} critical, ${warnings.length} warnings`,
959
- detail: { violations: outputResult.violations }
975
+ detail: { violations: outputResult.violations, executorType: "local" }
960
976
  }));
961
977
  logger.info(`Output guard: ${outputResult.violations.length} violation(s) found`);
962
978
  for (const v of outputResult.violations) {
@@ -983,7 +999,7 @@ async function runGuardStages({ config, logger, emitter, eventBase, session, ite
983
999
  if (!perfResult.skipped && perfResult.violations.length > 0) {
984
1000
  emitProgress(emitter, makeEvent("guard:perf", { ...eventBase, stage: "guard" }, {
985
1001
  message: `Perf guard: ${perfResult.violations.length} issue(s)`,
986
- detail: { violations: perfResult.violations }
1002
+ detail: { violations: perfResult.violations, executorType: "local" }
987
1003
  }));
988
1004
  logger.info(`Perf guard: ${perfResult.violations.length} issue(s) found`);
989
1005
  for (const v of perfResult.violations) {
@@ -1165,7 +1181,7 @@ async function initFlowContext({ task, config, logger, emitter, askQuestion, pgT
1165
1181
  logger.info(`RTK detected (${rtkResult.version}) — instructing agents to prefix Bash commands with rtk`);
1166
1182
  emitProgress(emitter, makeEvent("rtk:detected", ctx.eventBase, {
1167
1183
  message: "RTK detected — agent commands will use token optimization",
1168
- detail: { version: rtkResult.version }
1184
+ detail: { version: rtkResult.version, executorType: "local" }
1169
1185
  }));
1170
1186
  }
1171
1187
 
@@ -1300,14 +1316,29 @@ async function runSingleIteration(ctx) {
1300
1316
  return { action: "next" };
1301
1317
  }
1302
1318
 
1303
- export async function runFlow({ task, config, logger, flags = {}, emitter = null, askQuestion = null, pgTaskId = null, pgProject = null }) {
1304
- const pipelineFlags = resolvePipelineFlags(config);
1305
-
1306
- if (flags.dryRun) {
1307
- return handleDryRun({ task, config, flags, emitter, pipelineFlags });
1319
+ async function writeHistoryRecord({ sessionId, task, result, logger }) {
1320
+ try {
1321
+ const { createHistoryRecord } = await import("./hu/store.js");
1322
+ const approved = Boolean(result?.approved);
1323
+ const summary = result?.review?.summary || result?.reason || null;
1324
+ await createHistoryRecord(sessionId, {
1325
+ task,
1326
+ result: JSON.stringify(result),
1327
+ approved,
1328
+ summary
1329
+ });
1330
+ } catch (err) {
1331
+ logger.warn(`HU history record failed (non-blocking): ${err.message}`);
1308
1332
  }
1333
+ }
1309
1334
 
1310
- const ctx = await initFlowContext({ task, config, logger, emitter, askQuestion, pgTaskId, pgProject, flags });
1335
+ /**
1336
+ * Run the standard iteration loop for a given task using the pipeline context.
1337
+ * Returns a result object with at least { approved: boolean }.
1338
+ * Used both as the main loop and as the per-HU callback in sub-pipeline mode.
1339
+ */
1340
+ async function runIterationLoop(ctx, { task: loopTask, askQuestion, emitter, logger }) {
1341
+ ctx.plannedTask = loopTask;
1311
1342
 
1312
1343
  const checkpointIntervalMs = (ctx.config.session.checkpoint_interval_minutes ?? 5) * 60 * 1000;
1313
1344
  let lastCheckpointAt = Date.now();
@@ -1323,7 +1354,9 @@ export async function runFlow({ task, config, logger, flags = {}, emitter = null
1323
1354
  checkpointDisabled, askQuestion, lastCheckpointAt, checkpointIntervalMs, elapsedMinutes,
1324
1355
  i, config: ctx.config, budgetTracker: ctx.budgetTracker, stageResults: ctx.stageResults, emitter, eventBase: ctx.eventBase, session: ctx.session, budgetSummary: ctx.budgetSummary, lastCheckpointSnapshot
1325
1356
  });
1326
- if (cpResult.action === "stop") return cpResult.result;
1357
+ if (cpResult.action === "stop") {
1358
+ return cpResult.result;
1359
+ }
1327
1360
  checkpointDisabled = cpResult.checkpointDisabled;
1328
1361
  lastCheckpointAt = cpResult.lastCheckpointAt;
1329
1362
  if (cpResult.lastCheckpointSnapshot !== undefined) lastCheckpointSnapshot = cpResult.lastCheckpointSnapshot;
@@ -1335,11 +1368,63 @@ export async function runFlow({ task, config, logger, flags = {}, emitter = null
1335
1368
  ctx.iteration = i;
1336
1369
 
1337
1370
  const iterResult = await runSingleIteration(ctx);
1338
- if (iterResult.action === "return") return iterResult.result;
1371
+ if (iterResult.action === "return") {
1372
+ return iterResult.result;
1373
+ }
1339
1374
  if (iterResult.action === "retry") { i -= 1; }
1340
1375
  }
1341
1376
 
1342
- return handleMaxIterationsReached({ session: ctx.session, budgetSummary: ctx.budgetSummary, emitter, eventBase: ctx.eventBase, config: ctx.config, stageResults: ctx.stageResults, logger, askQuestion, task });
1377
+ const maxIterResult = await handleMaxIterationsReached({ session: ctx.session, budgetSummary: ctx.budgetSummary, emitter, eventBase: ctx.eventBase, config: ctx.config, stageResults: ctx.stageResults, logger, askQuestion, task: loopTask });
1378
+ return maxIterResult;
1379
+ }
1380
+
1381
+ export async function runFlow({ task, config, logger, flags = {}, emitter = null, askQuestion = null, pgTaskId = null, pgProject = null }) {
1382
+ const pipelineFlags = resolvePipelineFlags(config);
1383
+
1384
+ if (flags.dryRun) {
1385
+ return handleDryRun({ task, config, flags, emitter, pipelineFlags });
1386
+ }
1387
+
1388
+ const ctx = await initFlowContext({ task, config, logger, emitter, askQuestion, pgTaskId, pgProject, flags });
1389
+
1390
+ // --- HU Sub-Pipeline: run each certified HU as an independent iteration loop ---
1391
+ if (needsSubPipeline(ctx.stageResults.huReviewer)) {
1392
+ logger.info(`HU sub-pipeline: ${ctx.stageResults.huReviewer.certified} certified stories — running each as a sub-pipeline`);
1393
+ emitProgress(emitter, makeEvent("hu:sub-pipeline:start", { ...ctx.eventBase, stage: "hu-sub-pipeline" }, {
1394
+ message: `Running ${ctx.stageResults.huReviewer.certified} HUs as sub-pipelines`,
1395
+ detail: { total: ctx.stageResults.huReviewer.total, certified: ctx.stageResults.huReviewer.certified }
1396
+ }));
1397
+
1398
+ const subPipelineResult = await runHuSubPipeline({
1399
+ huReviewerResult: ctx.stageResults.huReviewer,
1400
+ runIterationFn: async (huTask) => runIterationLoop(ctx, { task: huTask, askQuestion, emitter, logger }),
1401
+ emitter,
1402
+ eventBase: ctx.eventBase,
1403
+ logger
1404
+ });
1405
+
1406
+ emitProgress(emitter, makeEvent("hu:sub-pipeline:end", { ...ctx.eventBase, stage: "hu-sub-pipeline" }, {
1407
+ status: subPipelineResult.approved ? "ok" : "fail",
1408
+ message: subPipelineResult.approved
1409
+ ? "All HUs completed successfully"
1410
+ : `Sub-pipeline finished with failures (${subPipelineResult.blockedIds.length} blocked)`,
1411
+ detail: { results: subPipelineResult.results, blockedIds: subPipelineResult.blockedIds }
1412
+ }));
1413
+
1414
+ const finalResult = {
1415
+ approved: subPipelineResult.approved,
1416
+ sessionId: ctx.session.id,
1417
+ huResults: subPipelineResult.results,
1418
+ blockedIds: subPipelineResult.blockedIds
1419
+ };
1420
+ await writeHistoryRecord({ sessionId: ctx.session.id, task, result: finalResult, logger });
1421
+ return finalResult;
1422
+ }
1423
+
1424
+ // --- Standard single-task pipeline (1 HU or no HU reviewer) ---
1425
+ const result = await runIterationLoop(ctx, { task: ctx.plannedTask, askQuestion, emitter, logger });
1426
+ await writeHistoryRecord({ sessionId: ctx.session.id, task, result, logger });
1427
+ return result;
1343
1428
  }
1344
1429
 
1345
1430
  export async function resumeFlow({ sessionId, answer, config, logger, flags = {}, emitter = null, askQuestion = null }) {
@@ -119,6 +119,43 @@ export async function markPgCardToValidate({ pgCard, pgProject, config, session,
119
119
  }
120
120
  }
121
121
 
122
+ /**
123
+ * Convert a PG card's structured data into HU stories for the hu-reviewer stage.
124
+ * Returns an array of story objects ({id, text}) or null if the card lacks structured data.
125
+ *
126
+ * @param {object} pgCard - The PG card object (from fetchCard)
127
+ * @returns {Array<{id: string, text: string}>|null}
128
+ */
129
+ export function buildHuStoriesFromPgCard(pgCard) {
130
+ if (!pgCard?.descriptionStructured?.length) return null;
131
+
132
+ const parts = [];
133
+
134
+ // User stories (Como/Quiero/Para → As/I want/So that)
135
+ for (const s of pgCard.descriptionStructured) {
136
+ parts.push(`As ${s.role}, I want ${s.goal}, so that ${s.benefit}.`);
137
+ }
138
+
139
+ // Acceptance criteria
140
+ if (pgCard.acceptanceCriteriaStructured?.length) {
141
+ parts.push("", "Acceptance Criteria:");
142
+ for (const ac of pgCard.acceptanceCriteriaStructured) {
143
+ if (ac.given && ac.when && ac.then) {
144
+ parts.push(`- Given ${ac.given}, When ${ac.when}, Then ${ac.then}`);
145
+ } else if (ac.raw) {
146
+ parts.push(`- ${ac.raw}`);
147
+ }
148
+ }
149
+ } else if (pgCard.acceptanceCriteria) {
150
+ parts.push("", "Acceptance Criteria:", pgCard.acceptanceCriteria);
151
+ }
152
+
153
+ const storyId = pgCard.cardId || "HU-PG-001";
154
+ const storyText = parts.join("\n");
155
+
156
+ return [{ id: storyId, text: storyText }];
157
+ }
158
+
122
159
  // --- Internal helpers ---
123
160
 
124
161
  async function markPgCardInProgress({ pgTaskId, pgProject, config, logger }) {