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.
- package/README.md +31 -32
- package/docs/README.es.md +131 -237
- package/package.json +1 -1
- package/src/agents/resolve-bin.js +26 -14
- package/src/hu/store.js +57 -0
- package/src/orchestrator/hu-sub-pipeline.js +166 -0
- package/src/orchestrator/iteration-stages.js +50 -15
- package/src/orchestrator/post-loop-stages.js +25 -11
- package/src/orchestrator/pre-loop-stages.js +147 -43
- package/src/orchestrator/preflight-checks.js +3 -3
- package/src/orchestrator/solomon-escalation.js +3 -2
- package/src/orchestrator.js +100 -15
- package/src/planning-game/pipeline-adapter.js +37 -0
- package/src/prompts/hu-reviewer.js +55 -0
- package/src/prompts/triage.js +4 -2
- package/src/utils/display.js +133 -23
- package/src/utils/injection-guard.js +171 -0
- package/src/utils/os-detect.js +17 -8
- package/src/utils/process.js +7 -4
|
@@ -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
|
-
*
|
|
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<
|
|
526
|
+
* @returns {Promise<Array|null>} Array of decomposed HUs or null.
|
|
518
527
|
*/
|
|
519
|
-
|
|
520
|
-
|
|
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: "
|
|
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
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
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
|
-
|
|
554
|
+
result = await agent.runTask({ prompt, role: "hu-reviewer", onOutput: stall.onOutput });
|
|
534
555
|
} catch (err) {
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
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
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
const
|
|
548
|
-
|
|
549
|
-
|
|
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
|
-
|
|
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
|
|
package/src/orchestrator.js
CHANGED
|
@@ -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
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
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
|
-
|
|
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")
|
|
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")
|
|
1371
|
+
if (iterResult.action === "return") {
|
|
1372
|
+
return iterResult.result;
|
|
1373
|
+
}
|
|
1339
1374
|
if (iterResult.action === "retry") { i -= 1; }
|
|
1340
1375
|
}
|
|
1341
1376
|
|
|
1342
|
-
|
|
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 }) {
|