karajan-code 1.28.1 → 1.29.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "karajan-code",
3
- "version": "1.28.1",
3
+ "version": "1.29.0",
4
4
  "description": "Local multi-agent coding orchestrator with TDD, SonarQube, and code review pipeline",
5
5
  "type": "module",
6
6
  "license": "AGPL-3.0",
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Shared runner for direct role execution (discover, triage, researcher, architect, audit).
3
+ * Extracts the repeated boilerplate from server-handlers.js handleXxxDirect functions.
4
+ */
5
+
6
+ import { createStallDetector } from "../utils/stall-detector.js";
7
+ import { resolveRole } from "../config.js";
8
+ import { createLogger } from "../utils/logger.js";
9
+ import { assertAgentsAvailable } from "../agents/availability.js";
10
+ import { createRunLog } from "../utils/run-log.js";
11
+ import { sendTrackerLog } from "./progress.js";
12
+
13
+ /**
14
+ * Run a role-based tool directly (not through the orchestrator pipeline).
15
+ *
16
+ * @param {object} opts
17
+ * @param {string} opts.roleName - Config role key (e.g. "discover", "triage")
18
+ * @param {string} [opts.stage] - Stage label for events/logs (defaults to roleName)
19
+ * @param {Function} opts.importRole - Async fn returning { RoleClass } (dynamic import wrapper)
20
+ * @param {object} opts.initContext - Object passed to role.init()
21
+ * @param {object} opts.runInput - Object passed to role.run() (onOutput is injected automatically)
22
+ * @param {string} [opts.logStartMsg] - Custom "[kj_xxx] started" suffix (optional)
23
+ * @param {object} opts.args - Raw tool arguments (for buildConfig overrides)
24
+ * @param {string} [opts.commandName] - Config command name (defaults to roleName)
25
+ * @param {object} opts.server - MCP server instance
26
+ * @param {object} opts.extra - MCP extra context (for progress notifier)
27
+ * @param {Function} opts.resolveProjectDir - Async fn(server, explicitDir) => projectDir
28
+ * @param {Function} opts.buildConfig - Async fn(args, commandName) => config
29
+ * @param {Function} opts.buildDirectEmitter - fn(server, runLog, extra) => emitter
30
+ */
31
+ export async function runDirectRole({
32
+ roleName,
33
+ stage,
34
+ importRole,
35
+ initContext,
36
+ runInput,
37
+ logStartMsg,
38
+ args,
39
+ commandName,
40
+ server,
41
+ extra,
42
+ resolveProjectDir,
43
+ buildConfig,
44
+ buildDirectEmitter
45
+ }) {
46
+ const effectiveStage = stage || roleName;
47
+ const effectiveCommand = commandName || roleName;
48
+
49
+ const config = await buildConfig(args, effectiveCommand);
50
+ const logger = createLogger(config.output.log_level, "mcp");
51
+
52
+ const role = resolveRole(config, roleName);
53
+ await assertAgentsAvailable([role.provider]);
54
+
55
+ const projectDir = await resolveProjectDir(server, args.projectDir);
56
+ const runLog = createRunLog(projectDir);
57
+ const startMsg = logStartMsg || `[kj_${effectiveStage}] started`;
58
+ runLog.logText(startMsg);
59
+
60
+ const emitter = buildDirectEmitter(server, runLog, extra);
61
+ const eventBase = { sessionId: null, iteration: 0, startedAt: Date.now() };
62
+ const onOutput = ({ stream, line }) => {
63
+ emitter.emit("progress", {
64
+ type: "agent:output",
65
+ stage: effectiveStage,
66
+ message: line,
67
+ detail: { stream, agent: role.provider }
68
+ });
69
+ };
70
+ const stallDetector = createStallDetector({
71
+ onOutput, emitter, eventBase, stage: effectiveStage, provider: role.provider
72
+ });
73
+
74
+ const { RoleClass } = await importRole();
75
+ const roleInstance = new RoleClass({ config, logger, emitter });
76
+ await roleInstance.init(initContext);
77
+
78
+ sendTrackerLog(server, effectiveStage, "running", role.provider);
79
+ runLog.logText(`[${effectiveStage}] agent launched, waiting for response...`);
80
+
81
+ let result;
82
+ try {
83
+ result = await roleInstance.run({ ...runInput, onOutput: stallDetector.onOutput });
84
+ } finally {
85
+ stallDetector.stop();
86
+ const stats = stallDetector.stats();
87
+ runLog.logText(
88
+ `[${effectiveStage}] finished — lines=${stats.lineCount}, bytes=${stats.bytesReceived}, elapsed=${Math.round(stats.elapsedMs / 1000)}s`
89
+ );
90
+ runLog.close();
91
+ }
92
+
93
+ if (!result.ok) {
94
+ sendTrackerLog(server, effectiveStage, "failed");
95
+ throw new Error(result.result?.error || result.summary || `${effectiveStage} failed`);
96
+ }
97
+
98
+ sendTrackerLog(server, effectiveStage, "done");
99
+ return { ok: true, ...result.result, summary: result.summary };
100
+ }
@@ -9,6 +9,7 @@ import { runKjCommand } from "./run-kj.js";
9
9
  import { normalizePlanArgs } from "./tool-arg-normalizers.js";
10
10
  import { buildProgressHandler, buildProgressNotifier, buildPipelineTracker, sendTrackerLog } from "./progress.js";
11
11
  import { createStallDetector } from "../utils/stall-detector.js";
12
+ import { runDirectRole } from "./direct-role-runner.js";
12
13
  import { runFlow, resumeFlow } from "../orchestrator.js";
13
14
  import { loadConfig, applyRunOverrides, validateConfig, resolveRole } from "../config.js";
14
15
  import { createLogger } from "../utils/logger.js";
@@ -523,28 +524,6 @@ export async function handleReviewDirect(a, server, extra) {
523
524
  }
524
525
 
525
526
  export async function handleDiscoverDirect(a, server, extra) {
526
- const config = await buildConfig(a, "discover");
527
- const logger = createLogger(config.output.log_level, "mcp");
528
-
529
- const discoverRole = resolveRole(config, "discover");
530
- await assertAgentsAvailable([discoverRole.provider]);
531
-
532
- const projectDir = await resolveProjectDir(server, a.projectDir);
533
- const runLog = createRunLog(projectDir);
534
- runLog.logText(`[kj_discover] started — mode=${a.mode || "gaps"}`);
535
- const emitter = buildDirectEmitter(server, runLog, extra);
536
- const eventBase = { sessionId: null, iteration: 0, startedAt: Date.now() };
537
- const onOutput = ({ stream, line }) => {
538
- emitter.emit("progress", { type: "agent:output", stage: "discover", message: line, detail: { stream, agent: discoverRole.provider } });
539
- };
540
- const stallDetector = createStallDetector({
541
- onOutput, emitter, eventBase, stage: "discover", provider: discoverRole.provider
542
- });
543
-
544
- const { DiscoverRole } = await import("../roles/discover-role.js");
545
- const discover = new DiscoverRole({ config, logger, emitter });
546
- await discover.init({ task: a.task });
547
-
548
527
  // Build context from pgTask if provided
549
528
  let context = a.context || null;
550
529
  if (a.pgTask && a.pgProject) {
@@ -554,205 +533,76 @@ export async function handleDiscoverDirect(a, server, extra) {
554
533
  } catch { /* PG not available — proceed without */ }
555
534
  }
556
535
 
557
- sendTrackerLog(server, "discover", "running", discoverRole.provider);
558
- runLog.logText(`[discover] agent launched, waiting for response...`);
559
- let result;
560
- try {
561
- result = await discover.run({ task: a.task, mode: a.mode || "gaps", context, onOutput: stallDetector.onOutput });
562
- } finally {
563
- stallDetector.stop();
564
- const stats = stallDetector.stats();
565
- runLog.logText(`[discover] finishedlines=${stats.lineCount}, bytes=${stats.bytesReceived}, elapsed=${Math.round(stats.elapsedMs / 1000)}s`);
566
- runLog.close();
567
- }
568
-
569
- if (!result.ok) {
570
- sendTrackerLog(server, "discover", "failed");
571
- throw new Error(result.result?.error || result.summary || "Discovery failed");
572
- }
573
-
574
- sendTrackerLog(server, "discover", "done");
575
- return { ok: true, ...result.result, summary: result.summary };
536
+ return runDirectRole({
537
+ roleName: "discover",
538
+ importRole: async () => {
539
+ const { DiscoverRole } = await import("../roles/discover-role.js");
540
+ return { RoleClass: DiscoverRole };
541
+ },
542
+ initContext: { task: a.task },
543
+ runInput: { task: a.task, mode: a.mode || "gaps", context },
544
+ logStartMsg: `[kj_discover] startedmode=${a.mode || "gaps"}`,
545
+ args: a, server, extra,
546
+ resolveProjectDir, buildConfig, buildDirectEmitter
547
+ });
576
548
  }
577
549
 
578
550
  export async function handleTriageDirect(a, server, extra) {
579
- const config = await buildConfig(a, "triage");
580
- const logger = createLogger(config.output.log_level, "mcp");
581
-
582
- const triageRole = resolveRole(config, "triage");
583
- await assertAgentsAvailable([triageRole.provider]);
584
-
585
- const projectDir = await resolveProjectDir(server, a.projectDir);
586
- const runLog = createRunLog(projectDir);
587
- runLog.logText(`[kj_triage] started`);
588
- const emitter = buildDirectEmitter(server, runLog, extra);
589
- const eventBase = { sessionId: null, iteration: 0, startedAt: Date.now() };
590
- const onOutput = ({ stream, line }) => {
591
- emitter.emit("progress", { type: "agent:output", stage: "triage", message: line, detail: { stream, agent: triageRole.provider } });
592
- };
593
- const stallDetector = createStallDetector({
594
- onOutput, emitter, eventBase, stage: "triage", provider: triageRole.provider
551
+ return runDirectRole({
552
+ roleName: "triage",
553
+ importRole: async () => {
554
+ const { TriageRole } = await import("../roles/triage-role.js");
555
+ return { RoleClass: TriageRole };
556
+ },
557
+ initContext: { task: a.task },
558
+ runInput: { task: a.task },
559
+ args: a, server, extra,
560
+ resolveProjectDir, buildConfig, buildDirectEmitter
595
561
  });
596
-
597
- const { TriageRole } = await import("../roles/triage-role.js");
598
- const triage = new TriageRole({ config, logger, emitter });
599
- await triage.init({ task: a.task });
600
-
601
- sendTrackerLog(server, "triage", "running", triageRole.provider);
602
- runLog.logText(`[triage] agent launched, waiting for response...`);
603
- let result;
604
- try {
605
- result = await triage.run({ task: a.task, onOutput: stallDetector.onOutput });
606
- } finally {
607
- stallDetector.stop();
608
- const stats = stallDetector.stats();
609
- runLog.logText(`[triage] finished — lines=${stats.lineCount}, bytes=${stats.bytesReceived}, elapsed=${Math.round(stats.elapsedMs / 1000)}s`);
610
- runLog.close();
611
- }
612
-
613
- if (!result.ok) {
614
- sendTrackerLog(server, "triage", "failed");
615
- throw new Error(result.result?.error || result.summary || "Triage failed");
616
- }
617
-
618
- sendTrackerLog(server, "triage", "done");
619
- return { ok: true, ...result.result, summary: result.summary };
620
562
  }
621
563
 
622
564
  export async function handleResearcherDirect(a, server, extra) {
623
- const config = await buildConfig(a, "researcher");
624
- const logger = createLogger(config.output.log_level, "mcp");
625
-
626
- const researcherRole = resolveRole(config, "researcher");
627
- await assertAgentsAvailable([researcherRole.provider]);
628
-
629
- const projectDir = await resolveProjectDir(server, a.projectDir);
630
- const runLog = createRunLog(projectDir);
631
- runLog.logText(`[kj_researcher] started`);
632
- const emitter = buildDirectEmitter(server, runLog, extra);
633
- const eventBase = { sessionId: null, iteration: 0, startedAt: Date.now() };
634
- const onOutput = ({ stream, line }) => {
635
- emitter.emit("progress", { type: "agent:output", stage: "researcher", message: line, detail: { stream, agent: researcherRole.provider } });
636
- };
637
- const stallDetector = createStallDetector({
638
- onOutput, emitter, eventBase, stage: "researcher", provider: researcherRole.provider
565
+ return runDirectRole({
566
+ roleName: "researcher",
567
+ importRole: async () => {
568
+ const { ResearcherRole } = await import("../roles/researcher-role.js");
569
+ return { RoleClass: ResearcherRole };
570
+ },
571
+ initContext: { task: a.task },
572
+ runInput: { task: a.task },
573
+ args: a, server, extra,
574
+ resolveProjectDir, buildConfig, buildDirectEmitter
639
575
  });
640
-
641
- const { ResearcherRole } = await import("../roles/researcher-role.js");
642
- const researcher = new ResearcherRole({ config, logger, emitter });
643
- await researcher.init({ task: a.task });
644
-
645
- sendTrackerLog(server, "researcher", "running", researcherRole.provider);
646
- runLog.logText(`[researcher] agent launched, waiting for response...`);
647
- let result;
648
- try {
649
- result = await researcher.run({ task: a.task, onOutput: stallDetector.onOutput });
650
- } finally {
651
- stallDetector.stop();
652
- const stats = stallDetector.stats();
653
- runLog.logText(`[researcher] finished — lines=${stats.lineCount}, bytes=${stats.bytesReceived}, elapsed=${Math.round(stats.elapsedMs / 1000)}s`);
654
- runLog.close();
655
- }
656
-
657
- if (!result.ok) {
658
- sendTrackerLog(server, "researcher", "failed");
659
- throw new Error(result.result?.error || result.summary || "Researcher failed");
660
- }
661
-
662
- sendTrackerLog(server, "researcher", "done");
663
- return { ok: true, ...result.result, summary: result.summary };
664
576
  }
665
577
 
666
578
  export async function handleAuditDirect(a, server, extra) {
667
- const config = await buildConfig(a, "audit");
668
- const logger = createLogger(config.output.log_level, "mcp");
669
-
670
- const auditRole = resolveRole(config, "audit");
671
- await assertAgentsAvailable([auditRole.provider]);
672
-
673
- const projectDir = await resolveProjectDir(server, a.projectDir);
674
- const runLog = createRunLog(projectDir);
675
- runLog.logText(`[kj_audit] started dimensions=${a.dimensions || "all"}`);
676
- const emitter = buildDirectEmitter(server, runLog, extra);
677
- const eventBase = { sessionId: null, iteration: 0, startedAt: Date.now() };
678
- const onOutput = ({ stream, line }) => {
679
- emitter.emit("progress", { type: "agent:output", stage: "audit", message: line, detail: { stream, agent: auditRole.provider } });
680
- };
681
- const stallDetector = createStallDetector({
682
- onOutput, emitter, eventBase, stage: "audit", provider: auditRole.provider
579
+ const task = a.task || "Analyze the full codebase";
580
+ return runDirectRole({
581
+ roleName: "audit",
582
+ importRole: async () => {
583
+ const { AuditRole } = await import("../roles/audit-role.js");
584
+ return { RoleClass: AuditRole };
585
+ },
586
+ initContext: { task },
587
+ runInput: { task, dimensions: a.dimensions || null },
588
+ logStartMsg: `[kj_audit] started dimensions=${a.dimensions || "all"}`,
589
+ args: a, server, extra,
590
+ resolveProjectDir, buildConfig, buildDirectEmitter
683
591
  });
684
-
685
- const { AuditRole } = await import("../roles/audit-role.js");
686
- const audit = new AuditRole({ config, logger, emitter });
687
- await audit.init({ task: a.task || "Analyze the full codebase" });
688
-
689
- sendTrackerLog(server, "audit", "running", auditRole.provider);
690
- runLog.logText(`[audit] agent launched, waiting for response...`);
691
- let result;
692
- try {
693
- result = await audit.run({
694
- task: a.task || "Analyze the full codebase",
695
- dimensions: a.dimensions || null,
696
- onOutput: stallDetector.onOutput
697
- });
698
- } finally {
699
- stallDetector.stop();
700
- const stats = stallDetector.stats();
701
- runLog.logText(`[audit] finished — lines=${stats.lineCount}, bytes=${stats.bytesReceived}, elapsed=${Math.round(stats.elapsedMs / 1000)}s`);
702
- runLog.close();
703
- }
704
-
705
- if (!result.ok) {
706
- sendTrackerLog(server, "audit", "failed");
707
- throw new Error(result.result?.error || result.summary || "Audit failed");
708
- }
709
-
710
- sendTrackerLog(server, "audit", "done");
711
- return { ok: true, ...result.result, summary: result.summary };
712
592
  }
713
593
 
714
594
  export async function handleArchitectDirect(a, server, extra) {
715
- const config = await buildConfig(a, "architect");
716
- const logger = createLogger(config.output.log_level, "mcp");
717
-
718
- const architectRole = resolveRole(config, "architect");
719
- await assertAgentsAvailable([architectRole.provider]);
720
-
721
- const projectDir = await resolveProjectDir(server, a.projectDir);
722
- const runLog = createRunLog(projectDir);
723
- runLog.logText(`[kj_architect] started`);
724
- const emitter = buildDirectEmitter(server, runLog, extra);
725
- const eventBase = { sessionId: null, iteration: 0, startedAt: Date.now() };
726
- const onOutput = ({ stream, line }) => {
727
- emitter.emit("progress", { type: "agent:output", stage: "architect", message: line, detail: { stream, agent: architectRole.provider } });
728
- };
729
- const stallDetector = createStallDetector({
730
- onOutput, emitter, eventBase, stage: "architect", provider: architectRole.provider
595
+ return runDirectRole({
596
+ roleName: "architect",
597
+ importRole: async () => {
598
+ const { ArchitectRole } = await import("../roles/architect-role.js");
599
+ return { RoleClass: ArchitectRole };
600
+ },
601
+ initContext: { task: a.task },
602
+ runInput: { task: a.task, researchContext: a.context || null },
603
+ args: a, server, extra,
604
+ resolveProjectDir, buildConfig, buildDirectEmitter
731
605
  });
732
-
733
- const { ArchitectRole } = await import("../roles/architect-role.js");
734
- const architect = new ArchitectRole({ config, logger, emitter });
735
- await architect.init({ task: a.task });
736
-
737
- sendTrackerLog(server, "architect", "running", architectRole.provider);
738
- runLog.logText(`[architect] agent launched, waiting for response...`);
739
- let result;
740
- try {
741
- result = await architect.run({ task: a.task, researchContext: a.context || null, onOutput: stallDetector.onOutput });
742
- } finally {
743
- stallDetector.stop();
744
- const stats = stallDetector.stats();
745
- runLog.logText(`[architect] finished — lines=${stats.lineCount}, bytes=${stats.bytesReceived}, elapsed=${Math.round(stats.elapsedMs / 1000)}s`);
746
- runLog.close();
747
- }
748
-
749
- if (!result.ok) {
750
- sendTrackerLog(server, "architect", "failed");
751
- throw new Error(result.result?.error || result.summary || "Architect failed");
752
- }
753
-
754
- sendTrackerLog(server, "architect", "done");
755
- return { ok: true, ...result.result, summary: result.summary };
756
606
  }
757
607
 
758
608
  /* ── Preflight helpers ─────────────────────────────────────────────── */
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Shared context object for pipeline execution.
3
+ * Replaces destructured parameter objects across orchestrator functions.
4
+ */
5
+ export class PipelineContext {
6
+ constructor({ config, session, logger, emitter, task, flags = {} }) {
7
+ this.config = config;
8
+ this.session = session;
9
+ this.logger = logger;
10
+ this.emitter = emitter;
11
+ this.task = task;
12
+ this.flags = flags;
13
+
14
+ // Pipeline state (set during execution)
15
+ this.eventBase = {};
16
+ this.pipelineFlags = {};
17
+ this.trackBudget = null;
18
+ this.budgetTracker = null;
19
+ this.budgetLimit = null;
20
+ this.budgetSummary = null;
21
+ this.askQuestion = null;
22
+ this.reviewRules = null;
23
+ this.repeatDetector = null;
24
+ this.sonarState = { issuesInitial: null, issuesFinal: null };
25
+ this.stageResults = {};
26
+ this.gitCtx = {};
27
+ this.iteration = 0;
28
+
29
+ // Roles (initialized during setup)
30
+ this.coderRole = null;
31
+ this.reviewerRole = null;
32
+ this.refactorerRole = null;
33
+ this.coderRoleInstance = null;
34
+
35
+ // Planning Game context
36
+ this.pgTaskId = null;
37
+ this.pgProject = null;
38
+ this.pgCard = null;
39
+
40
+ // Planned task (may differ from original task after planner)
41
+ this.plannedTask = null;
42
+
43
+ // Timing
44
+ this.startedAt = null;
45
+ }
46
+ }
@@ -333,6 +333,8 @@ export async function runArchitectStage({ config, logger, emitter, eventBase, se
333
333
 
334
334
  const architectContext = architectOutput.ok ? architectOutput.result : null;
335
335
 
336
+ // TODO: Move ADR creation to planning-game/pipeline-adapter.js (PG coupling still here because
337
+ // stageResult.adrs is consumed synchronously within runArchitectStage's return value).
336
338
  // Generate ADRs from architect tradeoffs when PG is linked
337
339
  const tradeoffs = architectOutput.result?.architecture?.tradeoffs;
338
340
  if (architectOutput.ok
@@ -29,6 +29,7 @@ import { classifyIntent } from "./guards/intent-guard.js";
29
29
  import { resolveReviewProfile } from "./review/profiles.js";
30
30
  import { CoderRole } from "./roles/coder-role.js";
31
31
  import { invokeSolomon } from "./orchestrator/solomon-escalation.js";
32
+ import { PipelineContext } from "./orchestrator/pipeline-context.js";
32
33
  import { runTriageStage, runResearcherStage, runArchitectStage, runPlannerStage, runDiscoverStage } from "./orchestrator/pre-loop-stages.js";
33
34
  import { runCoderStage, runRefactorerStage, runTddCheckStage, runSonarStage, runSonarCloudStage, runReviewerStage } from "./orchestrator/iteration-stages.js";
34
35
  import { runTesterStage, runSecurityStage, runImpeccableStage } from "./orchestrator/post-loop-stages.js";
@@ -178,33 +179,7 @@ async function initializeSession({ task, config, flags, pgTaskId, pgProject }) {
178
179
  return createSession(sessionInit);
179
180
  }
180
181
 
181
- async function markPgCardInProgress({ pgTaskId, pgProject, config, logger }) {
182
- if (!pgTaskId || !pgProject || config.planning_game?.enabled === false) {
183
- return null;
184
- }
185
- try {
186
- const { fetchCard, updateCard } = await import("./planning-game/client.js");
187
- const pgCard = await fetchCard({ projectId: pgProject, cardId: pgTaskId });
188
- if (pgCard && pgCard.status !== "In Progress") {
189
- await updateCard({
190
- projectId: pgProject,
191
- cardId: pgTaskId,
192
- firebaseId: pgCard.firebaseId,
193
- updates: {
194
- status: "In Progress",
195
- startDate: new Date().toISOString(),
196
- developer: "dev_016",
197
- codeveloper: config.planning_game?.codeveloper || null
198
- }
199
- });
200
- logger.info(`Planning Game: ${pgTaskId} → In Progress`);
201
- }
202
- return pgCard;
203
- } catch (err) {
204
- logger.warn(`Planning Game: could not update ${pgTaskId}: ${err.message}`);
205
- return null;
206
- }
207
- }
182
+ // PG card "In Progress" logic moved to src/planning-game/pipeline-adapter.js → initPgAdapter()
208
183
 
209
184
  function applyTriageOverrides(pipelineFlags, roleOverrides) {
210
185
  const keys = ["plannerEnabled", "researcherEnabled", "architectEnabled", "refactorerEnabled", "reviewerEnabled", "testerEnabled", "securityEnabled", "impeccableEnabled"];
@@ -238,57 +213,7 @@ function applyAutoSimplify({ pipelineFlags, triageLevel, config, flags, logger,
238
213
  return true;
239
214
  }
240
215
 
241
- async function handlePgDecomposition({ triageResult, pgTaskId, pgProject, config, askQuestion, emitter, eventBase, session, stageResults, logger }) {
242
- const shouldDecompose = triageResult.stageResult?.shouldDecompose
243
- && triageResult.stageResult.subtasks?.length > 1
244
- && pgTaskId
245
- && pgProject
246
- && config.planning_game?.enabled !== false
247
- && askQuestion;
248
-
249
- if (!shouldDecompose) return;
250
-
251
- try {
252
- const { buildDecompositionQuestion, createDecompositionSubtasks } = await import("./planning-game/decomposition.js");
253
- const { createCard, relateCards, fetchCard } = await import("./planning-game/client.js");
254
-
255
- const question = buildDecompositionQuestion(triageResult.stageResult.subtasks, pgTaskId);
256
- const answer = await askQuestion(question);
257
-
258
- if (answer && (answer.trim().toLowerCase() === "yes" || answer.trim().toLowerCase() === "sí" || answer.trim().toLowerCase() === "si")) {
259
- const parentCard = await fetchCard({ projectId: pgProject, cardId: pgTaskId }).catch(() => null);
260
- const createdSubtasks = await createDecompositionSubtasks({
261
- client: { createCard, relateCards },
262
- projectId: pgProject,
263
- parentCardId: pgTaskId,
264
- parentFirebaseId: parentCard?.firebaseId || null,
265
- subtasks: triageResult.stageResult.subtasks,
266
- epic: parentCard?.epic || null,
267
- sprint: parentCard?.sprint || null,
268
- codeveloper: config.planning_game?.codeveloper || null
269
- });
270
-
271
- stageResults.triage.pgSubtasks = createdSubtasks;
272
- logger.info(`Planning Game: created ${createdSubtasks.length} subtasks from decomposition`);
273
-
274
- emitProgress(
275
- emitter,
276
- makeEvent("pg:decompose", { ...eventBase, stage: "triage" }, {
277
- message: `Created ${createdSubtasks.length} subtasks in Planning Game`,
278
- detail: { subtasks: createdSubtasks.map((s) => ({ cardId: s.cardId, title: s.title })) }
279
- })
280
- );
281
-
282
- await addCheckpoint(session, {
283
- stage: "pg-decompose",
284
- subtasksCreated: createdSubtasks.length,
285
- cardIds: createdSubtasks.map((s) => s.cardId)
286
- });
287
- }
288
- } catch (err) {
289
- logger.warn(`Planning Game decomposition failed: ${err.message}`);
290
- }
291
- }
216
+ // PG decomposition logic moved to src/planning-game/pipeline-adapter.js handlePgDecomposition()
292
217
 
293
218
  function applyFlagOverrides(pipelineFlags, flags) {
294
219
  if (flags.enablePlanner !== undefined) pipelineFlags.plannerEnabled = Boolean(flags.enablePlanner);
@@ -713,6 +638,7 @@ async function finalizeApprovedSession({ config, gitCtx, task, logger, session,
713
638
  session.budget = budgetSummary();
714
639
  await markSessionStatus(session, "approved");
715
640
 
641
+ const { markPgCardToValidate } = await import("./planning-game/pipeline-adapter.js");
716
642
  await markPgCardToValidate({ pgCard, pgProject, config, session, gitResult, logger });
717
643
 
718
644
  const deferredIssues = session.deferred_issues || [];
@@ -728,29 +654,7 @@ async function finalizeApprovedSession({ config, gitCtx, task, logger, session,
728
654
  return { approved: true, sessionId: session.id, review, git: gitResult, deferredIssues };
729
655
  }
730
656
 
731
- async function markPgCardToValidate({ pgCard, pgProject, config, session, gitResult, logger }) {
732
- if (!pgCard || !pgProject) return;
733
-
734
- try {
735
- const { updateCard } = await import("./planning-game/client.js");
736
- const { buildCompletionUpdates } = await import("./planning-game/adapter.js");
737
- const pgUpdates = buildCompletionUpdates({
738
- approved: true,
739
- commits: gitResult?.commits || [],
740
- startDate: session.pg_card?.startDate || session.created_at,
741
- codeveloper: config.planning_game?.codeveloper || null
742
- });
743
- await updateCard({
744
- projectId: pgProject,
745
- cardId: session.pg_task_id,
746
- firebaseId: pgCard.firebaseId,
747
- updates: pgUpdates
748
- });
749
- logger.info(`Planning Game: ${session.pg_task_id} → To Validate`);
750
- } catch (err) {
751
- logger.warn(`Planning Game: could not update ${session.pg_task_id} on completion: ${err.message}`);
752
- }
753
- }
657
+ // PG card "To Validate" logic moved to src/planning-game/pipeline-adapter.js → markPgCardToValidate()
754
658
 
755
659
  async function handleReviewerRetryAndSolomon({ config, session, emitter, eventBase, logger, review, task, i, askQuestion }) {
756
660
  session.last_reviewer_feedback = review.blocking_issues
@@ -835,6 +739,7 @@ async function runPreLoopStages({ config, logger, emitter, eventBase, session, f
835
739
  });
836
740
  if (simplified) stageResults.triage.autoSimplified = true;
837
741
 
742
+ const { handlePgDecomposition } = await import("./planning-game/pipeline-adapter.js");
838
743
  await handlePgDecomposition({ triageResult, pgTaskId, pgProject, config, askQuestion, emitter, eventBase, session, stageResults, logger });
839
744
 
840
745
  applyFlagOverrides(pipelineFlags, flags);
@@ -1097,59 +1002,61 @@ async function handleMaxIterationsReached({ session, budgetSummary, emitter, eve
1097
1002
  }
1098
1003
 
1099
1004
  async function initFlowContext({ task, config, logger, emitter, askQuestion, pgTaskId, pgProject, flags }) {
1100
- const coderRole = resolveRole(config, "coder");
1101
- const reviewerRole = resolveRole(config, "reviewer");
1102
- const refactorerRole = resolveRole(config, "refactorer");
1103
- const pipelineFlags = resolvePipelineFlags(config);
1104
- const repeatDetector = new RepeatDetector({ threshold: getRepeatThreshold(config) });
1105
- const coderRoleInstance = new CoderRole({ config, logger, emitter, createAgentFn: createAgent, askHost: askQuestion });
1106
- const startedAt = Date.now();
1107
- const eventBase = { sessionId: null, iteration: 0, stage: null, startedAt };
1108
- const { budgetTracker, budgetLimit, budgetSummary, trackBudget } = createBudgetManager({ config, emitter, eventBase });
1109
-
1110
- const session = await initializeSession({ task, config, flags, pgTaskId, pgProject });
1111
- eventBase.sessionId = session.id;
1112
-
1113
- const pgCard = await markPgCardInProgress({ pgTaskId, pgProject, config, logger });
1114
- session.pg_card = pgCard || null;
1005
+ const ctx = new PipelineContext({ config, session: null, logger, emitter, task, flags });
1006
+ ctx.askQuestion = askQuestion;
1007
+ ctx.pgTaskId = pgTaskId;
1008
+ ctx.pgProject = pgProject;
1009
+
1010
+ ctx.coderRole = resolveRole(config, "coder");
1011
+ ctx.reviewerRole = resolveRole(config, "reviewer");
1012
+ ctx.refactorerRole = resolveRole(config, "refactorer");
1013
+ ctx.pipelineFlags = resolvePipelineFlags(config);
1014
+ ctx.repeatDetector = new RepeatDetector({ threshold: getRepeatThreshold(config) });
1015
+ ctx.coderRoleInstance = new CoderRole({ config, logger, emitter, createAgentFn: createAgent, askHost: askQuestion });
1016
+ ctx.startedAt = Date.now();
1017
+ ctx.eventBase = { sessionId: null, iteration: 0, stage: null, startedAt: ctx.startedAt };
1018
+ const { budgetTracker, budgetLimit, budgetSummary, trackBudget } = createBudgetManager({ config, emitter, eventBase: ctx.eventBase });
1019
+ ctx.budgetTracker = budgetTracker;
1020
+ ctx.budgetLimit = budgetLimit;
1021
+ ctx.budgetSummary = budgetSummary;
1022
+ ctx.trackBudget = trackBudget;
1023
+
1024
+ ctx.session = await initializeSession({ task, config, flags, pgTaskId, pgProject });
1025
+ ctx.eventBase.sessionId = ctx.session.id;
1026
+
1027
+ const { initPgAdapter } = await import("./planning-game/pipeline-adapter.js");
1028
+ const pgAdapterResult = await initPgAdapter({ session: ctx.session, config, logger, pgTaskId, pgProject });
1029
+ ctx.pgCard = pgAdapterResult.pgCard;
1030
+ ctx.session.pg_card = ctx.pgCard || null;
1115
1031
 
1116
1032
  emitProgress(
1117
1033
  emitter,
1118
- makeEvent("session:start", eventBase, {
1034
+ makeEvent("session:start", ctx.eventBase, {
1119
1035
  message: "Session started",
1120
- detail: { task, coder: coderRole.provider, reviewer: reviewerRole.provider, maxIterations: config.max_iterations }
1036
+ detail: { task, coder: ctx.coderRole.provider, reviewer: ctx.reviewerRole.provider, maxIterations: config.max_iterations }
1121
1037
  })
1122
1038
  );
1123
1039
 
1124
- const stageResults = {};
1125
- const sonarState = { issuesInitial: null, issuesFinal: null };
1040
+ ctx.stageResults = {};
1041
+ ctx.sonarState = { issuesInitial: null, issuesFinal: null };
1126
1042
 
1127
- const preLoopResult = await runPreLoopStages({ config, logger, emitter, eventBase, session, flags, pipelineFlags, coderRole, trackBudget, task, askQuestion, pgTaskId, pgProject, stageResults });
1128
- const { plannedTask } = preLoopResult;
1129
- const updatedConfig = preLoopResult.updatedConfig;
1043
+ const preLoopResult = await runPreLoopStages({ config, logger, emitter, eventBase: ctx.eventBase, session: ctx.session, flags, pipelineFlags: ctx.pipelineFlags, coderRole: ctx.coderRole, trackBudget: ctx.trackBudget, task, askQuestion, pgTaskId, pgProject, stageResults: ctx.stageResults });
1044
+ ctx.plannedTask = preLoopResult.plannedTask;
1045
+ ctx.config = preLoopResult.updatedConfig;
1130
1046
 
1131
- const gitCtx = await prepareGitAutomation({ config: updatedConfig, task, logger, session });
1132
- const projectDir = updatedConfig.projectDir || process.cwd();
1133
- const { rules: reviewRules } = await resolveReviewProfile({ mode: updatedConfig.review_mode, projectDir });
1134
- await coderRoleInstance.init();
1047
+ ctx.gitCtx = await prepareGitAutomation({ config: ctx.config, task, logger, session: ctx.session });
1048
+ const projectDir = ctx.config.projectDir || process.cwd();
1049
+ ctx.reviewRules = (await resolveReviewProfile({ mode: ctx.config.review_mode, projectDir })).rules;
1050
+ await ctx.coderRoleInstance.init();
1135
1051
 
1136
- return {
1137
- coderRole, reviewerRole, refactorerRole, pipelineFlags, repeatDetector, coderRoleInstance,
1138
- startedAt, eventBase, budgetTracker, budgetLimit, budgetSummary, trackBudget,
1139
- session, pgCard, stageResults, sonarState, plannedTask, config: updatedConfig,
1140
- gitCtx, reviewRules
1141
- };
1052
+ return ctx;
1142
1053
  }
1143
1054
 
1144
1055
  async function runSingleIteration(ctx) {
1145
- const {
1146
- coderRoleInstance, coderRole, refactorerRole, pipelineFlags, config, logger, emitter, eventBase,
1147
- session, plannedTask, trackBudget, i, reviewerRole, reviewRules, task, repeatDetector,
1148
- budgetSummary, askQuestion, sonarState, stageResults, gitCtx, pgCard, pgProject
1149
- } = ctx;
1056
+ const { config, logger, emitter, eventBase, session, task, iteration: i } = ctx;
1150
1057
 
1151
1058
  const iterStart = Date.now();
1152
- const becariaEnabled = Boolean(config.becaria?.enabled) && gitCtx?.enabled;
1059
+ const becariaEnabled = Boolean(config.becaria?.enabled) && ctx.gitCtx?.enabled;
1153
1060
  logger.setContext({ iteration: i, stage: "iteration" });
1154
1061
 
1155
1062
  emitProgress(emitter, makeEvent("iteration:start", { ...eventBase, stage: "iteration" }, {
@@ -1158,18 +1065,34 @@ async function runSingleIteration(ctx) {
1158
1065
  }));
1159
1066
  logger.info(`Iteration ${i}/${config.max_iterations}`);
1160
1067
 
1161
- const crResult = await runCoderAndRefactorerStages({ coderRoleInstance, coderRole, refactorerRole, pipelineFlags, config, logger, emitter, eventBase, session, plannedTask, trackBudget, i });
1068
+ const crResult = await runCoderAndRefactorerStages({
1069
+ coderRoleInstance: ctx.coderRoleInstance, coderRole: ctx.coderRole, refactorerRole: ctx.refactorerRole,
1070
+ pipelineFlags: ctx.pipelineFlags, config, logger, emitter, eventBase, session,
1071
+ plannedTask: ctx.plannedTask, trackBudget: ctx.trackBudget, i
1072
+ });
1162
1073
  if (crResult.action === "return" || crResult.action === "retry") return crResult;
1163
1074
 
1164
1075
  const guardResult = await runGuardStages({ config, logger, emitter, eventBase, session, iteration: i });
1165
1076
  if (guardResult.action === "return") return guardResult;
1166
1077
 
1167
- const qgResult = await runQualityGateStages({ config, logger, emitter, eventBase, session, trackBudget, i, askQuestion, repeatDetector, budgetSummary, sonarState, task, stageResults, coderRole, pipelineFlags });
1078
+ const qgResult = await runQualityGateStages({
1079
+ config, logger, emitter, eventBase, session, trackBudget: ctx.trackBudget, i,
1080
+ askQuestion: ctx.askQuestion, repeatDetector: ctx.repeatDetector, budgetSummary: ctx.budgetSummary,
1081
+ sonarState: ctx.sonarState, task, stageResults: ctx.stageResults, coderRole: ctx.coderRole,
1082
+ pipelineFlags: ctx.pipelineFlags
1083
+ });
1168
1084
  if (qgResult.action === "return" || qgResult.action === "continue") return qgResult;
1169
1085
 
1170
- await handleBecariaEarlyPrOrPush({ becariaEnabled, config, session, emitter, eventBase, gitCtx, task, logger, stageResults, i });
1086
+ await handleBecariaEarlyPrOrPush({
1087
+ becariaEnabled, config, session, emitter, eventBase, gitCtx: ctx.gitCtx, task, logger,
1088
+ stageResults: ctx.stageResults, i
1089
+ });
1171
1090
 
1172
- const revResult = await runReviewerGateStage({ pipelineFlags, reviewerRole, config, logger, emitter, eventBase, session, trackBudget, i, reviewRules, task, repeatDetector, budgetSummary, askQuestion });
1091
+ const revResult = await runReviewerGateStage({
1092
+ pipelineFlags: ctx.pipelineFlags, reviewerRole: ctx.reviewerRole, config, logger, emitter, eventBase,
1093
+ session, trackBudget: ctx.trackBudget, i, reviewRules: ctx.reviewRules, task,
1094
+ repeatDetector: ctx.repeatDetector, budgetSummary: ctx.budgetSummary, askQuestion: ctx.askQuestion
1095
+ });
1173
1096
  if (revResult.action === "return" || revResult.action === "retry") return revResult;
1174
1097
  const review = revResult.review;
1175
1098
 
@@ -1179,20 +1102,27 @@ async function runSingleIteration(ctx) {
1179
1102
  }));
1180
1103
  session.standby_retry_count = 0;
1181
1104
 
1182
- const solomonResult = await handleSolomonCheck({ config, session, emitter, eventBase, logger, task, i, askQuestion, becariaEnabled, blockingIssues: review?.blocking_issues });
1105
+ const solomonResult = await handleSolomonCheck({
1106
+ config, session, emitter, eventBase, logger, task, i, askQuestion: ctx.askQuestion,
1107
+ becariaEnabled, blockingIssues: review?.blocking_issues
1108
+ });
1183
1109
  if (solomonResult.action === "pause") return { action: "return", result: solomonResult.result };
1184
1110
 
1185
1111
  await handleBecariaReviewDispatch({ becariaEnabled, config, session, review, i, logger });
1186
1112
 
1187
1113
  if (review.approved) {
1188
- const approvedResult = await handleApprovedReview({ config, session, emitter, eventBase, coderRole, trackBudget, i, task, stageResults, pipelineFlags, askQuestion, logger, gitCtx, budgetSummary, pgCard, pgProject, review });
1114
+ const approvedResult = await handleApprovedReview({
1115
+ config, session, emitter, eventBase, coderRole: ctx.coderRole, trackBudget: ctx.trackBudget, i, task,
1116
+ stageResults: ctx.stageResults, pipelineFlags: ctx.pipelineFlags, askQuestion: ctx.askQuestion, logger,
1117
+ gitCtx: ctx.gitCtx, budgetSummary: ctx.budgetSummary, pgCard: ctx.pgCard, pgProject: ctx.pgProject, review
1118
+ });
1189
1119
  if (approvedResult.action === "return" || approvedResult.action === "continue") return approvedResult;
1190
1120
  }
1191
1121
 
1192
- // Solomon already evaluated the rejection in runReviewerStage handleReviewerRejection
1122
+ // Solomon already evaluated the rejection in runReviewerStage -> handleReviewerRejection
1193
1123
  // Only use retry counter as fallback if Solomon is disabled
1194
1124
  if (!config.pipeline?.solomon?.enabled) {
1195
- const retryResult = await handleReviewerRetryAndSolomon({ config, session, emitter, eventBase, logger, review, task, i, askQuestion });
1125
+ const retryResult = await handleReviewerRetryAndSolomon({ config, session, emitter, eventBase, logger, review, task, i, askQuestion: ctx.askQuestion });
1196
1126
  if (retryResult.action === "return") return retryResult;
1197
1127
  } else {
1198
1128
  // Solomon is enabled — feed back the blocking issues for the next coder iteration
@@ -1213,37 +1143,36 @@ export async function runFlow({ task, config, logger, flags = {}, emitter = null
1213
1143
  }
1214
1144
 
1215
1145
  const ctx = await initFlowContext({ task, config, logger, emitter, askQuestion, pgTaskId, pgProject, flags });
1216
- config = ctx.config;
1217
1146
 
1218
- const checkpointIntervalMs = (config.session.checkpoint_interval_minutes ?? 5) * 60 * 1000;
1147
+ const checkpointIntervalMs = (ctx.config.session.checkpoint_interval_minutes ?? 5) * 60 * 1000;
1219
1148
  let lastCheckpointAt = Date.now();
1220
1149
  let checkpointDisabled = false;
1221
1150
 
1222
1151
  let i = 0;
1223
- while (i < config.max_iterations) {
1152
+ while (i < ctx.config.max_iterations) {
1224
1153
  i += 1;
1225
1154
  const elapsedMinutes = (Date.now() - ctx.startedAt) / 60000;
1226
1155
 
1227
1156
  const cpResult = await handleCheckpoint({
1228
1157
  checkpointDisabled, askQuestion, lastCheckpointAt, checkpointIntervalMs, elapsedMinutes,
1229
- i, config, budgetTracker: ctx.budgetTracker, stageResults: ctx.stageResults, emitter, eventBase: ctx.eventBase, session: ctx.session, budgetSummary: ctx.budgetSummary
1158
+ i, config: ctx.config, budgetTracker: ctx.budgetTracker, stageResults: ctx.stageResults, emitter, eventBase: ctx.eventBase, session: ctx.session, budgetSummary: ctx.budgetSummary
1230
1159
  });
1231
1160
  if (cpResult.action === "stop") return cpResult.result;
1232
1161
  checkpointDisabled = cpResult.checkpointDisabled;
1233
1162
  lastCheckpointAt = cpResult.lastCheckpointAt;
1234
1163
 
1235
- await checkSessionTimeout({ askQuestion, elapsedMinutes, config, session: ctx.session, emitter, eventBase: ctx.eventBase, i, budgetSummary: ctx.budgetSummary });
1236
- await checkBudgetExceeded({ budgetTracker: ctx.budgetTracker, config, session: ctx.session, emitter, eventBase: ctx.eventBase, i, budgetLimit: ctx.budgetLimit, budgetSummary: ctx.budgetSummary });
1164
+ await checkSessionTimeout({ askQuestion, elapsedMinutes, config: ctx.config, session: ctx.session, emitter, eventBase: ctx.eventBase, i, budgetSummary: ctx.budgetSummary });
1165
+ await checkBudgetExceeded({ budgetTracker: ctx.budgetTracker, config: ctx.config, session: ctx.session, emitter, eventBase: ctx.eventBase, i, budgetLimit: ctx.budgetLimit, budgetSummary: ctx.budgetSummary });
1237
1166
 
1238
1167
  ctx.eventBase.iteration = i;
1239
- ctx.i = i;
1168
+ ctx.iteration = i;
1240
1169
 
1241
- const iterResult = await runSingleIteration({ ...ctx, config, logger, emitter, askQuestion, task, pgProject, i });
1170
+ const iterResult = await runSingleIteration(ctx);
1242
1171
  if (iterResult.action === "return") return iterResult.result;
1243
1172
  if (iterResult.action === "retry") { i -= 1; }
1244
1173
  }
1245
1174
 
1246
- return handleMaxIterationsReached({ session: ctx.session, budgetSummary: ctx.budgetSummary, emitter, eventBase: ctx.eventBase, config, stageResults: ctx.stageResults, logger, askQuestion, task });
1175
+ return handleMaxIterationsReached({ session: ctx.session, budgetSummary: ctx.budgetSummary, emitter, eventBase: ctx.eventBase, config: ctx.config, stageResults: ctx.stageResults, logger, askQuestion, task });
1247
1176
  }
1248
1177
 
1249
1178
  export async function resumeFlow({ sessionId, answer, config, logger, flags = {}, emitter = null, askQuestion = null }) {
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Planning Game pipeline adapter.
3
+ * Subscribes to pipeline events and handles PG card lifecycle updates.
4
+ * Decouples PG logic from the orchestrator — works as an event-driven plugin.
5
+ *
6
+ * Handles:
7
+ * - Marking card as "In Progress" at session start
8
+ * - Decomposing tasks into subtasks after triage
9
+ * - Marking card as "To Validate" on approved completion
10
+ *
11
+ * If PG is disabled or no pgTaskId is present, the adapter does nothing.
12
+ */
13
+
14
+ import { emitProgress, makeEvent } from "../utils/events.js";
15
+ import { addCheckpoint, saveSession } from "../session-store.js";
16
+
17
+ /**
18
+ * Attach the PG adapter to a pipeline emitter.
19
+ * Call this once during runFlow() initialization.
20
+ *
21
+ * @param {object} opts
22
+ * @param {EventEmitter} opts.emitter - Pipeline event emitter
23
+ * @param {object} opts.session - Session object (mutated during pipeline)
24
+ * @param {object} opts.config - Resolved config
25
+ * @param {object} opts.logger - Logger instance
26
+ * @param {string|null} opts.pgTaskId - Planning Game card ID (e.g. "KJC-TSK-0099")
27
+ * @param {string|null} opts.pgProject - Planning Game project ID
28
+ * @returns {{ pgCard: object|null }} The fetched PG card (or null)
29
+ */
30
+ export async function initPgAdapter({ session, config, logger, pgTaskId, pgProject }) {
31
+ if (!pgTaskId || !pgProject || config.planning_game?.enabled === false) {
32
+ return { pgCard: null };
33
+ }
34
+
35
+ const pgCard = await markPgCardInProgress({ pgTaskId, pgProject, config, logger });
36
+ return { pgCard };
37
+ }
38
+
39
+ /**
40
+ * Handle PG task decomposition after triage.
41
+ * Creates subtask cards in PG when triage recommends decomposition.
42
+ */
43
+ export async function handlePgDecomposition({ triageResult, pgTaskId, pgProject, config, askQuestion, emitter, eventBase, session, stageResults, logger }) {
44
+ const shouldDecompose = triageResult.stageResult?.shouldDecompose
45
+ && triageResult.stageResult.subtasks?.length > 1
46
+ && pgTaskId
47
+ && pgProject
48
+ && config.planning_game?.enabled !== false
49
+ && askQuestion;
50
+
51
+ if (!shouldDecompose) return;
52
+
53
+ try {
54
+ const { buildDecompositionQuestion, createDecompositionSubtasks } = await import("./decomposition.js");
55
+ const { createCard, relateCards, fetchCard } = await import("./client.js");
56
+
57
+ const question = buildDecompositionQuestion(triageResult.stageResult.subtasks, pgTaskId);
58
+ const answer = await askQuestion(question);
59
+
60
+ if (answer && (answer.trim().toLowerCase() === "yes" || answer.trim().toLowerCase() === "sí" || answer.trim().toLowerCase() === "si")) {
61
+ const parentCard = await fetchCard({ projectId: pgProject, cardId: pgTaskId }).catch(() => null);
62
+ const createdSubtasks = await createDecompositionSubtasks({
63
+ client: { createCard, relateCards },
64
+ projectId: pgProject,
65
+ parentCardId: pgTaskId,
66
+ parentFirebaseId: parentCard?.firebaseId || null,
67
+ subtasks: triageResult.stageResult.subtasks,
68
+ epic: parentCard?.epic || null,
69
+ sprint: parentCard?.sprint || null,
70
+ codeveloper: config.planning_game?.codeveloper || null
71
+ });
72
+
73
+ stageResults.triage.pgSubtasks = createdSubtasks;
74
+ logger.info(`Planning Game: created ${createdSubtasks.length} subtasks from decomposition`);
75
+
76
+ emitProgress(
77
+ emitter,
78
+ makeEvent("pg:decompose", { ...eventBase, stage: "triage" }, {
79
+ message: `Created ${createdSubtasks.length} subtasks in Planning Game`,
80
+ detail: { subtasks: createdSubtasks.map((s) => ({ cardId: s.cardId, title: s.title })) }
81
+ })
82
+ );
83
+
84
+ await addCheckpoint(session, {
85
+ stage: "pg-decompose",
86
+ subtasksCreated: createdSubtasks.length,
87
+ cardIds: createdSubtasks.map((s) => s.cardId)
88
+ });
89
+ }
90
+ } catch (err) {
91
+ logger.warn(`Planning Game decomposition failed: ${err.message}`);
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Mark PG card as "To Validate" on approved session completion.
97
+ */
98
+ export async function markPgCardToValidate({ pgCard, pgProject, config, session, gitResult, logger }) {
99
+ if (!pgCard || !pgProject) return;
100
+
101
+ try {
102
+ const { updateCard } = await import("./client.js");
103
+ const { buildCompletionUpdates } = await import("./adapter.js");
104
+ const pgUpdates = buildCompletionUpdates({
105
+ approved: true,
106
+ commits: gitResult?.commits || [],
107
+ startDate: session.pg_card?.startDate || session.created_at,
108
+ codeveloper: config.planning_game?.codeveloper || null
109
+ });
110
+ await updateCard({
111
+ projectId: pgProject,
112
+ cardId: session.pg_task_id,
113
+ firebaseId: pgCard.firebaseId,
114
+ updates: pgUpdates
115
+ });
116
+ logger.info(`Planning Game: ${session.pg_task_id} → To Validate`);
117
+ } catch (err) {
118
+ logger.warn(`Planning Game: could not update ${session.pg_task_id} on completion: ${err.message}`);
119
+ }
120
+ }
121
+
122
+ // --- Internal helpers ---
123
+
124
+ async function markPgCardInProgress({ pgTaskId, pgProject, config, logger }) {
125
+ try {
126
+ const { fetchCard, updateCard } = await import("./client.js");
127
+ const pgCard = await fetchCard({ projectId: pgProject, cardId: pgTaskId });
128
+ if (pgCard && pgCard.status !== "In Progress") {
129
+ await updateCard({
130
+ projectId: pgProject,
131
+ cardId: pgTaskId,
132
+ firebaseId: pgCard.firebaseId,
133
+ updates: {
134
+ status: "In Progress",
135
+ startDate: new Date().toISOString(),
136
+ developer: "dev_016",
137
+ codeveloper: config.planning_game?.codeveloper || null
138
+ }
139
+ });
140
+ logger.info(`Planning Game: ${pgTaskId} → In Progress`);
141
+ }
142
+ return pgCard;
143
+ } catch (err) {
144
+ logger.warn(`Planning Game: could not update ${pgTaskId}: ${err.message}`);
145
+ return null;
146
+ }
147
+ }