karajan-code 2.4.1 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/README.md +20 -4
  2. package/package.json +5 -2
  3. package/src/agents/aider-agent.js +1 -2
  4. package/src/agents/base-agent.js +63 -1
  5. package/src/agents/claude-agent.js +3 -4
  6. package/src/agents/codex-agent.js +1 -2
  7. package/src/agents/gemini-agent.js +1 -2
  8. package/src/agents/index.js +11 -2
  9. package/src/agents/opencode-agent.js +1 -2
  10. package/src/brain/decision-tracker.js +123 -0
  11. package/src/brain/decisor.js +243 -0
  12. package/src/brain/solomon-consult.js +163 -0
  13. package/src/budget/comparison.js +96 -0
  14. package/src/checks/binaries.js +123 -0
  15. package/src/checks/ci.js +105 -0
  16. package/src/checks/config-files.js +217 -0
  17. package/src/checks/dir-setup.js +63 -0
  18. package/src/checks/mcp-health.js +157 -0
  19. package/src/checks/node.js +61 -0
  20. package/src/checks/ports.js +110 -0
  21. package/src/checks/rtk.js +36 -0
  22. package/src/checks/runner.js +267 -0
  23. package/src/checks/skills.js +49 -0
  24. package/src/checks/sonar.js +67 -0
  25. package/src/checks/system.js +53 -0
  26. package/src/checks/tokens.js +96 -0
  27. package/src/checks/types.js +105 -0
  28. package/src/cli.js +90 -7
  29. package/src/commands/board.js +23 -0
  30. package/src/commands/doctor.js +130 -362
  31. package/src/commands/plan.js +221 -37
  32. package/src/commands/resume.js +1 -1
  33. package/src/commands/run.js +2 -1
  34. package/src/commands/skills.js +49 -0
  35. package/src/config/schema.js +222 -0
  36. package/src/config.js +32 -3
  37. package/src/guards/policy-resolver.js +8 -0
  38. package/src/hu/store.js +9 -0
  39. package/src/infrastructure/command-runner.js +44 -0
  40. package/src/infrastructure/environment.js +38 -0
  41. package/src/infrastructure/file-system-service.js +63 -0
  42. package/src/infrastructure/mocks.js +182 -0
  43. package/src/mcp/handlers/management-handlers.js +7 -2
  44. package/src/mcp/tools.js +10 -3
  45. package/src/orchestrator/config-init.js +16 -1
  46. package/src/orchestrator/flow-runner.js +2085 -0
  47. package/src/orchestrator/pipeline-context.js +17 -0
  48. package/src/orchestrator/preflight-checks.js +78 -4
  49. package/src/orchestrator/solomon-escalation.js +21 -0
  50. package/src/orchestrator/stages/stage-executor.js +155 -0
  51. package/src/orchestrator.js +19 -1884
  52. package/src/plan/plan-executor.js +69 -0
  53. package/src/plan/plan-hu-ops.js +134 -0
  54. package/src/plan/plan-id.js +25 -0
  55. package/src/plan/plan-schema.js +139 -0
  56. package/src/plan/plan-store.js +58 -25
  57. package/src/prompts/architect.js +2 -2
  58. package/src/prompts/coder.js +2 -2
  59. package/src/prompts/prompt-resolver.js +127 -0
  60. package/src/prompts/reviewer.js +2 -2
  61. package/src/roles/architect-role.js +3 -1
  62. package/src/roles/base-role.js +23 -2
  63. package/src/roles/coder-role.js +3 -1
  64. package/src/roles/security-role.js +12 -0
  65. package/src/roles/tester-role.js +11 -0
  66. package/src/session/journal/decisions-writer.js +200 -0
  67. package/src/session/journal/iteration-logger.js +194 -0
  68. package/src/session/journal/summary-writer.js +176 -0
  69. package/src/session/journal/tree-writer.js +164 -0
  70. package/src/skills/semantic-detector.js +147 -0
  71. package/src/skills/skill-detector.js +186 -8
  72. package/src/skills/skill-loader.js +131 -1
  73. package/src/skills/skills-cache.js +128 -0
  74. package/src/types/agent.js +55 -0
  75. package/src/types/config.js +114 -0
  76. package/src/types/hu.js +47 -0
  77. package/src/types/index.js +34 -0
  78. package/src/types/policy.js +43 -0
  79. package/src/types/session.js +74 -0
  80. package/src/types/stage.js +68 -0
  81. package/src/utils/display/event-handlers.js +389 -0
  82. package/src/utils/display/formatters.js +132 -0
  83. package/src/utils/display/header.js +48 -0
  84. package/src/utils/display/session-summary.js +108 -0
  85. package/src/utils/display/solomon.js +42 -0
  86. package/src/utils/port-check.js +45 -0
  87. package/src/utils/port-occupant.js +95 -0
  88. package/src/utils/provider-env.js +116 -0
  89. package/src/utils/with-timeout.js +42 -0
  90. package/templates/roles/architect/anthropic.md +65 -0
  91. package/templates/roles/architect/default.md +63 -0
  92. package/templates/roles/architect/google.md +52 -0
  93. package/templates/roles/architect/openai.md +51 -0
  94. package/templates/roles/architect/opencode.md +50 -0
  95. package/templates/roles/coder/anthropic.md +72 -0
  96. package/templates/roles/coder/default.md +69 -0
  97. package/templates/roles/coder/google.md +78 -0
  98. package/templates/roles/coder/openai.md +59 -0
  99. package/templates/roles/coder/opencode.md +49 -0
  100. package/templates/roles/planner/anthropic.md +50 -0
  101. package/templates/roles/planner/default.md +48 -0
  102. package/templates/roles/planner/google.md +66 -0
  103. package/templates/roles/planner/openai.md +46 -0
  104. package/templates/roles/planner/opencode.md +53 -0
  105. package/templates/roles/reviewer/anthropic.md +61 -0
  106. package/templates/roles/reviewer/default.md +62 -0
  107. package/templates/roles/reviewer/google.md +63 -0
  108. package/templates/roles/reviewer/openai.md +57 -0
  109. package/templates/roles/reviewer/opencode.md +42 -0
  110. package/templates/roles/solomon/anthropic.md +71 -0
  111. package/templates/roles/solomon/default.md +83 -0
  112. package/templates/roles/solomon/google.md +61 -0
  113. package/templates/roles/solomon/openai.md +53 -0
  114. package/templates/roles/solomon/opencode.md +55 -0
  115. package/src/utils/display.js +0 -669
@@ -0,0 +1,389 @@
1
+ import {
2
+ ANSI,
3
+ formatElapsed,
4
+ formatExecutor,
5
+ budgetColor,
6
+ roleStart,
7
+ roleEnd,
8
+ passFailStage,
9
+ TRACKER_STATUS,
10
+ TRACKER_DEFAULT,
11
+ ICONS,
12
+ STATUS_ICON
13
+ } from "./formatters.js";
14
+ import { printSolomonRuling } from "./solomon.js";
15
+ import {
16
+ printSessionStages,
17
+ printSessionGit,
18
+ printSessionBudget,
19
+ printSessionRtkSavings
20
+ } from "./session-summary.js";
21
+
22
+ export const EVENT_HANDLERS = {
23
+ "session:start": () => {},
24
+
25
+ "iteration:start": (event, icon, elapsed) => {
26
+ const retryCount = event.detail?.reviewerRetryCount || 0;
27
+ const maxRetries = event.detail?.maxReviewerRetries;
28
+ const retrySuffix = retryCount > 0 && maxRetries
29
+ ? ` ${ANSI.dim}\u2014 reviewer retry ${retryCount}/${maxRetries}${ANSI.reset}`
30
+ : "";
31
+ console.log(
32
+ `\n${ANSI.bold}${icon} Iteration ${event.detail?.iteration}/${event.detail?.maxIterations}${ANSI.reset}${retrySuffix} ${elapsed}`
33
+ );
34
+ },
35
+
36
+ "planner:start": (event, icon) => {
37
+ roleStart(icon, "Planner", event.detail?.planner);
38
+ },
39
+
40
+ "planner:end": (event, _icon, elapsed, status) => {
41
+ roleEnd(status, "Planner", elapsed, formatExecutor(event.detail));
42
+ },
43
+
44
+ "coder:start": (event, icon) => {
45
+ roleStart(icon, "Coder", event.detail?.coder);
46
+ },
47
+
48
+ "coder:end": (event, _icon, elapsed, status) => {
49
+ roleEnd(status, "Coder", elapsed, formatExecutor(event.detail));
50
+ },
51
+
52
+ "refactorer:start": (event, icon) => {
53
+ roleStart(icon, "Refactorer", event.detail?.refactorer);
54
+ },
55
+
56
+ "refactorer:end": (event, _icon, elapsed, status) => {
57
+ roleEnd(status, "Refactorer", elapsed, formatExecutor(event.detail));
58
+ },
59
+
60
+ "tdd:result": (event, icon) => {
61
+ const tdd = event.detail || {};
62
+ if (tdd.reason === "no_diff_available") {
63
+ console.log(` \u251c\u2500 ${icon} TDD policy: ${ANSI.yellow}SKIP${ANSI.reset} ${ANSI.dim}(no diff available)${ANSI.reset}`);
64
+ return;
65
+ }
66
+ const label = tdd.ok ? `${ANSI.green}PASS${ANSI.reset}` : `${ANSI.red}FAIL${ANSI.reset}`;
67
+ const files = tdd.sourceFiles === undefined ? "" : ` (${tdd.sourceFiles} src, ${tdd.testFiles} test)`;
68
+ const executor = formatExecutor(event.detail);
69
+ console.log(` \u251c\u2500 ${icon} TDD policy: ${label}${files}${executor}`);
70
+ },
71
+
72
+ "researcher:start": (event, icon) => {
73
+ console.log(` \u251c\u2500 ${icon} Researcher (${event.detail?.researcher || "?"}) investigating...`);
74
+ },
75
+
76
+ "researcher:end": (event, _icon, elapsed, status) => {
77
+ roleEnd(status, "Researcher", elapsed, formatExecutor(event.detail));
78
+ },
79
+
80
+ "sonar:start": (event, icon) => {
81
+ const executor = formatExecutor(event.detail);
82
+ console.log(` \u251c\u2500 ${icon} SonarQube scanning...${executor}`);
83
+ },
84
+
85
+ "sonar:end": (event, _icon, elapsed, status) => {
86
+ const gate = event.detail?.gateStatus || "?";
87
+ const gateColor = gate === "OK" ? ANSI.green : ANSI.red;
88
+ const executor = formatExecutor(event.detail);
89
+ console.log(` \u251c\u2500 ${status} Quality gate: ${gateColor}${gate}${ANSI.reset}${executor} ${elapsed}`);
90
+ },
91
+
92
+ "reviewer:start": (event, icon) => {
93
+ console.log(` \u251c\u2500 ${icon} Reviewer (${event.detail?.reviewer || "?"}) running...`);
94
+ },
95
+
96
+ "reviewer:end": (event, _icon, elapsed) => {
97
+ const review = event.detail || {};
98
+ const executor = formatExecutor(event.detail);
99
+ if (review.approved) {
100
+ console.log(` \u251c\u2500 ${ANSI.green}\u2705 Review: APPROVED${ANSI.reset}${executor} ${elapsed}`);
101
+ } else {
102
+ const count = review.blockingCount || 0;
103
+ console.log(` \u251c\u2500 ${ANSI.red}\u274c Review: REJECTED (${count} blocking)${ANSI.reset}${executor}`);
104
+ if (review.issues) {
105
+ for (const issue of review.issues) {
106
+ console.log(` \u2502 ${ANSI.dim}${issue}${ANSI.reset}`);
107
+ }
108
+ }
109
+ }
110
+ },
111
+
112
+ "tester:start": (event, icon) => {
113
+ const executor = formatExecutor(event.detail);
114
+ console.log(` \u251c\u2500 ${icon} Tester evaluating...${executor}`);
115
+ },
116
+
117
+ "tester:end": (event, _icon, elapsed) => {
118
+ passFailStage(event.detail, "Tester", "issues found", elapsed);
119
+ },
120
+
121
+ "security:start": (event, icon) => {
122
+ const executor = formatExecutor(event.detail);
123
+ console.log(` \u251c\u2500 ${icon} Security auditing...${executor}`);
124
+ },
125
+
126
+ "security:end": (event, _icon, elapsed) => {
127
+ passFailStage(event.detail, "Security", "vulnerabilities found", elapsed);
128
+ },
129
+
130
+ "solomon:start": (event, icon) => {
131
+ const executor = formatExecutor(event.detail);
132
+ console.log(` \u251c\u2500 ${icon} Solomon arbitrating ${event.detail?.conflictStage || "?"} conflict...${executor}`);
133
+ },
134
+
135
+ "solomon:end": (event, _icon, elapsed) => {
136
+ printSolomonRuling(event.detail, elapsed);
137
+ },
138
+
139
+ "solomon:escalate": (event, icon) => {
140
+ const subloop = event.detail?.subloop || "?";
141
+ const retryCount = event.detail?.retryCount || 0;
142
+ const limit = event.detail?.limit || "?";
143
+ console.log(` \u251c\u2500 ${icon} ${subloop} sub-loop limit reached (${retryCount}/${limit}), invoking Solomon...`);
144
+ },
145
+
146
+ "coder:standby": (event, icon) => {
147
+ const until = event.detail?.cooldownUntil || "?";
148
+ const attempt = event.detail?.retryCount || "?";
149
+ const maxRetries = event.detail?.maxRetries || "?";
150
+ console.log(` \u251c\u2500 ${ANSI.yellow}${icon} Rate limited \u2014 standby until ${until} (attempt ${attempt}/${maxRetries})${ANSI.reset}`);
151
+ },
152
+
153
+ "coder:standby_heartbeat": (event, icon) => {
154
+ const remaining = event.detail?.remainingMs === undefined ? "?" : Math.round(event.detail.remainingMs / 1000);
155
+ console.log(` \u251c\u2500 ${ANSI.yellow}${icon} Standby: ${remaining}s remaining${ANSI.reset}`);
156
+ },
157
+
158
+ "agent:heartbeat": (event, icon) => {
159
+ const d = event.detail || {};
160
+ const provider = d.provider || "?";
161
+ const elapsed = d.elapsedMs ? Math.round(d.elapsedMs / 1000) : 0;
162
+ const silent = d.silenceMs ? Math.round(d.silenceMs / 1000) : 0;
163
+ const status = d.status === "waiting" ? `${ANSI.yellow}waiting (silent ${silent}s)` : `${ANSI.green}working`;
164
+ console.log(` \u251c\u2500 ${ANSI.dim}${icon} ${provider} ${status}${ANSI.dim} — ${elapsed}s elapsed${ANSI.reset}`);
165
+ },
166
+
167
+ "coder:standby_resume": (event, icon) => {
168
+ console.log(` \u251c\u2500 ${ANSI.green}${icon} Cooldown expired \u2014 resuming with ${event.detail?.coder || event.detail?.provider || "?"}${ANSI.reset}`);
169
+ },
170
+
171
+ "iteration:end": (event, icon, elapsed) => {
172
+ console.log(` \u2514\u2500 ${icon} Duration: ${formatElapsed(event.detail?.duration)} ${elapsed}`);
173
+ },
174
+
175
+ "budget:update": (event, icon) => {
176
+ const d = event.detail || {};
177
+ const total = Number(d.total_cost_usd || 0);
178
+ const totalTokens = Number(d.total_tokens || 0);
179
+ const max = Number(d.max_budget_usd);
180
+ const pct = Number(d.pct_used ?? 0);
181
+ const warn = Number(d.warn_threshold_pct ?? 80);
182
+ // Don't show N/A — just skip budget display when there's nothing to report
183
+ if (total === 0 && totalTokens === 0) return;
184
+ const fmtTokens = (n) => n.toLocaleString("en-US");
185
+ const tokenStr = totalTokens > 0 ? ` / ${fmtTokens(totalTokens)} tokens` : "";
186
+ const costStr = `$${total.toFixed(2)}`;
187
+ const color = budgetColor(max, pct, warn);
188
+ if (Number.isFinite(max) && max > 0) {
189
+ console.log(` \u251c\u2500 ${icon} Budget: ${color}${costStr}${tokenStr} / $${max.toFixed(2)} (${pct.toFixed(1)}%)${ANSI.reset}`);
190
+ } else {
191
+ console.log(` \u251c\u2500 ${icon} Budget: ${color}${costStr}${tokenStr}${ANSI.reset}`);
192
+ }
193
+ // KJC-TSK-0274: show "With KJ vs Without KJ" comparison when the
194
+ // pipeline has compression data. Silently skipped when absent (AC).
195
+ const cmp = d.kj_comparison;
196
+ if (cmp?.hasCompression) {
197
+ const withoutCost = `$${Number(cmp.withoutKj?.cost || 0).toFixed(2)}`;
198
+ const withoutTok = fmtTokens(Number(cmp.withoutKj?.tokens || 0));
199
+ console.log(` \u2502 ${ANSI.dim}\u2937 Without KJ: ~${withoutCost} / ~${withoutTok} tokens (-${Math.round(cmp.savedPct)}%)${ANSI.reset}`);
200
+ }
201
+ },
202
+
203
+ "session:end": (event, icon, elapsed) => {
204
+ console.log();
205
+ const resultLabel = event.detail?.approved
206
+ ? `${ANSI.bold}${ANSI.green}APPROVED${ANSI.reset}`
207
+ : `${ANSI.bold}${ANSI.red}${event.detail?.reason || "FAILED"}${ANSI.reset}`;
208
+ console.log(`${icon} Result: ${resultLabel} ${elapsed}`);
209
+ printSessionStages(event.detail?.stages);
210
+ printSessionGit(event.detail?.git);
211
+ printSessionBudget(event.detail?.budget);
212
+ printSessionRtkSavings(event.detail?.rtk_savings);
213
+ console.log(`${ANSI.dim}Session: ${event.sessionId}${ANSI.reset}`);
214
+ },
215
+
216
+ question: (event, icon) => {
217
+ console.log();
218
+ console.log(`${ANSI.bold}${ANSI.yellow}${icon} Paused - question:${ANSI.reset}`);
219
+ const questionText = event.detail?.question || event.message || "";
220
+ const questionLines = questionText.split("\n");
221
+ for (const line of questionLines) {
222
+ console.log(` ${line}`);
223
+ }
224
+ console.log(`${ANSI.dim}Resume with: kj resume ${event.sessionId} --answer "<response>"${ANSI.reset}`);
225
+ },
226
+
227
+ "preflight:start": (event) => {
228
+ const executor = formatExecutor(event.detail);
229
+ console.log(` \u251c\u2500 \ud83d\udee1\ufe0f Preflight checks running...${executor}`);
230
+ },
231
+
232
+ "preflight:end": (event, _icon, elapsed) => {
233
+ const executor = formatExecutor(event.detail);
234
+ const ok = event.status === "ok";
235
+ const statusIcon = ok ? `${ANSI.green}\u2705` : `${ANSI.red}\u274c`;
236
+ console.log(` \u251c\u2500 ${statusIcon} Preflight: ${event.message}${ANSI.reset}${executor} ${elapsed}`);
237
+ },
238
+
239
+ "discover:start": (event, icon) => {
240
+ const provider = event.detail?.provider || event.detail?.discover || "?";
241
+ console.log(` \u251c\u2500 ${icon || "\ud83d\udd0e"} Discover (${provider}) analyzing...`);
242
+ },
243
+
244
+ "discover:end": (event, _icon, elapsed, status) => {
245
+ roleEnd(status, "Discover", elapsed, formatExecutor(event.detail));
246
+ },
247
+
248
+ "architect:start": (event) => {
249
+ const provider = event.detail?.provider || event.detail?.architect || "?";
250
+ console.log(` \u251c\u2500 \ud83c\udfdb\ufe0f Architect (${provider}) designing...`);
251
+ },
252
+
253
+ "architect:end": (event, _icon, elapsed, status) => {
254
+ roleEnd(status, "Architect", elapsed, formatExecutor(event.detail));
255
+ },
256
+
257
+ "hu-reviewer:start": (event) => {
258
+ const executor = formatExecutor(event.detail);
259
+ console.log(` \u251c\u2500 \ud83d\udcdd HU Reviewer certifying...${executor}`);
260
+ },
261
+
262
+ "hu-reviewer:end": (event, _icon, elapsed, status) => {
263
+ roleEnd(status, "HU Reviewer", elapsed, formatExecutor(event.detail));
264
+ },
265
+
266
+ "impeccable:start": (event) => {
267
+ const executor = formatExecutor(event.detail);
268
+ console.log(` \u251c\u2500 \ud83c\udfa8 Impeccable auditing design...${executor}`);
269
+ },
270
+
271
+ "impeccable:end": (event, _icon, elapsed, status) => {
272
+ roleEnd(status, "Impeccable", elapsed, formatExecutor(event.detail));
273
+ },
274
+
275
+ "audit:start": (event) => {
276
+ const executor = formatExecutor(event.detail);
277
+ console.log(` \u251c\u2500 \ud83d\udccb Final audit running...${executor}`);
278
+ },
279
+
280
+ "audit:end": (event, _icon, elapsed, status) => {
281
+ roleEnd(status, "Audit", elapsed, formatExecutor(event.detail));
282
+ },
283
+
284
+ "sonarcloud:start": (event) => {
285
+ const executor = formatExecutor(event.detail);
286
+ console.log(` \u251c\u2500 \u2601\ufe0f SonarCloud scanning...${executor}`);
287
+ },
288
+
289
+ "sonarcloud:end": (event, _icon, elapsed, status) => {
290
+ roleEnd(status, "SonarCloud", elapsed, formatExecutor(event.detail));
291
+ },
292
+
293
+ "guard:output": (event) => {
294
+ const executor = formatExecutor(event.detail);
295
+ console.log(` \u251c\u2500 \ud83d\udee1\ufe0f ${event.message}${executor}`);
296
+ },
297
+
298
+ "guard:perf": (event) => {
299
+ const executor = formatExecutor(event.detail);
300
+ console.log(` \u251c\u2500 \u26a1 ${event.message}${executor}`);
301
+ },
302
+
303
+ "pipeline:tracker": (event) => {
304
+ const trackerStages = event.detail?.stages || [];
305
+ console.log(` ${ANSI.dim}\u250c Pipeline${ANSI.reset}`);
306
+ for (const stage of trackerStages) {
307
+ const { icon: stIcon, color: stColor } = TRACKER_STATUS[stage.status] || TRACKER_DEFAULT;
308
+ let suffix = "";
309
+ if (stage.summary) {
310
+ suffix = stage.status === "running" ? ` (${stage.summary})` : ` \u2192 ${stage.summary}`;
311
+ }
312
+ console.log(` ${ANSI.dim}\u2502${ANSI.reset} ${stColor}${stIcon} ${stage.name}${suffix}${ANSI.reset}`);
313
+ }
314
+ console.log(` ${ANSI.dim}\u2514${ANSI.reset}`);
315
+ },
316
+
317
+ "session:checkpoint": (event) => {
318
+ const d = event.detail || {};
319
+ if (d.auto_continued) {
320
+ console.log(` \u251c\u2500 ${ANSI.dim}\u2714 Checkpoint: auto-continuing (${d.reason || "progress"})${ANSI.reset}`);
321
+ } else {
322
+ console.log(` \u251c\u2500 ${ANSI.yellow}\u23f8\ufe0f Interactive checkpoint at ${d.elapsed_minutes?.toFixed(1) || "?"}min${ANSI.reset}`);
323
+ }
324
+ },
325
+
326
+ "ci:pr-created": (event) => {
327
+ const url = event.detail?.prUrl || "";
328
+ console.log(` \u251c\u2500 ${ANSI.green}\ud83d\ude80 CI PR created: ${url}${ANSI.reset}`);
329
+ },
330
+
331
+ "brain:rules-alert": (event) => {
332
+ console.log(` \u251c\u2500 ${ANSI.yellow}\u26a0\ufe0f Rules alert: ${event.message || "anomaly detected"}${ANSI.reset}`);
333
+ },
334
+
335
+ "agent:output": (event) => {
336
+ console.log(` \u2502 ${ANSI.dim}${event.message}${ANSI.reset}`);
337
+ },
338
+
339
+ "agent:action": (event) => {
340
+ console.log(` \u2502 ${ANSI.dim}\u2937 ${event.message}${ANSI.reset}`);
341
+ }
342
+ };
343
+
344
+ /** Event types suppressed in quiet mode (internal/noise events). */
345
+ export const QUIET_SUPPRESSED = new Set([
346
+ "agent:output",
347
+ "agent:stall",
348
+ "pipeline:simplify",
349
+ "pipeline:analysis-only",
350
+ "policies:resolved",
351
+ "intent:classified",
352
+ "skills:auto-install",
353
+ "context:loaded",
354
+ "rtk:detected",
355
+ "board:started",
356
+ "plan:loaded",
357
+ "tdd:auto-detect",
358
+ "dry-run:summary",
359
+ "pg:decompose",
360
+ "tester:auto-continue",
361
+ "security:auto-continue",
362
+ "hu:sub-pipeline:start",
363
+ "hu:sub-pipeline:end",
364
+ "resilient:auto_resume",
365
+ "guard:blocked",
366
+ ]);
367
+
368
+ /**
369
+ * Handle and print pipeline events
370
+ * @param {Object} event
371
+ * @param {Object} [opts]
372
+ * @param {boolean} [opts.quiet]
373
+ */
374
+ export function printEvent(event, opts = {}) {
375
+ if (opts.quiet && QUIET_SUPPRESSED.has(event.type)) {
376
+ return;
377
+ }
378
+
379
+ const icon = ICONS[event.type] || "\u2022";
380
+ const elapsed = event.elapsed === undefined ? "" : `${ANSI.dim}[${formatElapsed(event.elapsed)}]${ANSI.reset}`;
381
+ const status = event.status ? STATUS_ICON[event.status] || "" : "";
382
+
383
+ const handler = EVENT_HANDLERS[event.type];
384
+ if (handler) {
385
+ handler(event, icon, elapsed, status);
386
+ } else if (event.message) {
387
+ console.log(` \u251c\u2500 ${icon} ${event.message} ${elapsed}`);
388
+ }
389
+ }
@@ -0,0 +1,132 @@
1
+ export const ANSI = {
2
+ reset: "\x1b[0m",
3
+ bold: "\x1b[1m",
4
+ dim: "\x1b[2m",
5
+ cyan: "\x1b[36m",
6
+ green: "\x1b[32m",
7
+ yellow: "\x1b[33m",
8
+ red: "\x1b[31m",
9
+ gray: "\x1b[90m",
10
+ white: "\x1b[37m",
11
+ magenta: "\x1b[35m"
12
+ };
13
+
14
+ export const ICONS = {
15
+ "session:start": "\u25b6",
16
+ "planner:start": "\ud83e\udde0",
17
+ "planner:end": "\ud83e\udde0",
18
+ "researcher:start": "\ud83d\udd2c",
19
+ "researcher:end": "\ud83d\udd2c",
20
+ "iteration:start": "\u25b6",
21
+ "coder:start": "\ud83d\udd28",
22
+ "coder:end": "\ud83d\udd28",
23
+ "refactorer:start": "\u267b\ufe0f",
24
+ "refactorer:end": "\u267b\ufe0f",
25
+ "tdd:result": "\ud83d\udccb",
26
+ "sonar:start": "\ud83d\udd0d",
27
+ "sonar:end": "\ud83d\udd0d",
28
+ "reviewer:start": "\ud83d\udc41\ufe0f",
29
+ "reviewer:end": "\ud83d\udc41\ufe0f",
30
+ "tester:start": "\ud83e\uddea",
31
+ "tester:end": "\ud83e\uddea",
32
+ "security:start": "\ud83d\udd12",
33
+ "security:end": "\ud83d\udd12",
34
+ "solomon:start": "\u2696\ufe0f",
35
+ "solomon:end": "\u2696\ufe0f",
36
+ "solomon:escalate": "\u26a0\ufe0f",
37
+ "agent:heartbeat": "\u23f3",
38
+ "coder:standby": "\u23f3",
39
+ "coder:standby_heartbeat": "\u23f3",
40
+ "coder:standby_resume": "\u25b6\ufe0f",
41
+ "budget:update": "\ud83d\udcb8",
42
+ "iteration:end": "\u23f1\ufe0f",
43
+ "session:end": "\ud83c\udfc1",
44
+ "discover:start": "\ud83d\udd0e",
45
+ "discover:end": "\ud83d\udd0e",
46
+ "architect:start": "\ud83c\udfdb\ufe0f",
47
+ "architect:end": "\ud83c\udfdb\ufe0f",
48
+ "hu-reviewer:start": "\ud83d\udcdd",
49
+ "hu-reviewer:end": "\ud83d\udcdd",
50
+ "impeccable:start": "\ud83c\udfa8",
51
+ "impeccable:end": "\ud83c\udfa8",
52
+ "audit:start": "\ud83d\udccb",
53
+ "audit:end": "\ud83d\udccb",
54
+ "sonarcloud:start": "\u2601\ufe0f",
55
+ "sonarcloud:end": "\u2601\ufe0f",
56
+ "preflight:start": "\ud83d\udee1\ufe0f",
57
+ "preflight:end": "\ud83d\udee1\ufe0f",
58
+ question: "\u2753"
59
+ };
60
+
61
+ export const STATUS_ICON = {
62
+ ok: `${ANSI.green}\u2705${ANSI.reset}`,
63
+ fail: `${ANSI.red}\u274c${ANSI.reset}`,
64
+ paused: `${ANSI.yellow}\u23f8\ufe0f${ANSI.reset}`,
65
+ running: `${ANSI.cyan}\u25b6${ANSI.reset}`
66
+ };
67
+
68
+ export const TRACKER_STATUS = {
69
+ done: { icon: "\u2713", color: ANSI.green },
70
+ running: { icon: "\u25b6", color: ANSI.cyan },
71
+ failed: { icon: "\u2717", color: ANSI.red }
72
+ };
73
+
74
+ export const TRACKER_DEFAULT = { icon: "\u00b7", color: ANSI.dim };
75
+
76
+ /**
77
+ * Format milliseconds into MM:SS string
78
+ * @param {number} ms
79
+ * @returns {string}
80
+ */
81
+ export function formatElapsed(ms) {
82
+ const totalSec = Math.floor((ms || 0) / 1000);
83
+ const min = String(Math.floor(totalSec / 60)).padStart(2, "0");
84
+ const sec = String(totalSec % 60).padStart(2, "0");
85
+ return `${min}:${sec}`;
86
+ }
87
+
88
+ /**
89
+ * Format executor details into a display string
90
+ * @param {Object} detail
91
+ * @returns {string}
92
+ */
93
+ export function formatExecutor(detail) {
94
+ if (!detail) return '';
95
+ const provider = detail.provider || '';
96
+ const type = detail.executorType || '';
97
+ if (provider) return ` ${ANSI.dim}${provider}${ANSI.reset}`;
98
+ if (type === 'local') return ` ${ANSI.dim}local${ANSI.reset}`;
99
+ if (type === 'system') return ` ${ANSI.dim}system${ANSI.reset}`;
100
+ return '';
101
+ }
102
+
103
+ /**
104
+ * Calculate budget ANSI color based on usage
105
+ * @param {number} max
106
+ * @param {number} pct
107
+ * @param {number} warn
108
+ * @returns {string}
109
+ */
110
+ export function budgetColor(max, pct, warn) {
111
+ if (max > 0 && pct >= 100) return ANSI.red;
112
+ if (max > 0 && pct >= warn) return ANSI.yellow;
113
+ return ANSI.green;
114
+ }
115
+
116
+ export function roleStart(icon, label, provider) {
117
+ console.log(` \u251c\u2500 ${icon} ${label} (${provider || "?"}) running...`);
118
+ }
119
+
120
+ export function roleEnd(status, label, elapsed, executor) {
121
+ console.log(` \u251c\u2500 ${status} ${label} completed${executor || ''} ${elapsed}`);
122
+ }
123
+
124
+ export function passFailStage(detail, label, failDefault, elapsed) {
125
+ const executor = formatExecutor(detail);
126
+ if (detail?.ok === false) {
127
+ const summary = detail?.summary || failDefault;
128
+ console.log(` \u251c\u2500 ${ANSI.red}\u274c ${label}: ${summary}${ANSI.reset}${executor} ${elapsed}`);
129
+ } else {
130
+ console.log(` \u251c\u2500 ${ANSI.green}\u2705 ${label}: passed${ANSI.reset}${executor} ${elapsed}`);
131
+ }
132
+ }
@@ -0,0 +1,48 @@
1
+ import path from "node:path";
2
+ import { readFileSync } from "node:fs";
3
+ import { fileURLToPath } from "node:url";
4
+ import { printBanner } from "../banner.js";
5
+ import { ANSI } from "./formatters.js";
6
+
7
+ // TODO: i18n display messages
8
+ const DISPLAY_PKG_PATH = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../../package.json");
9
+ const DISPLAY_VERSION = JSON.parse(readFileSync(DISPLAY_PKG_PATH, "utf8")).version;
10
+
11
+ export const BAR = `${ANSI.dim}\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500${ANSI.reset}`;
12
+
13
+ /**
14
+ * Print task, configuration and pipeline header
15
+ * @param {Object} params
16
+ * @param {string} params.task
17
+ * @param {Object} params.config
18
+ */
19
+ export function printHeader({ task, config }) {
20
+ const version = DISPLAY_VERSION;
21
+ // Force banner: caller already gates on !jsonMode, so if we're here it's a human-readable run
22
+ printBanner(version, { force: true });
23
+ console.log(`${ANSI.bold}Task:${ANSI.reset} ${task}`);
24
+ console.log(
25
+ `${ANSI.bold}Coder:${ANSI.reset} ${config.roles?.coder?.provider || config.coder} ${ANSI.dim}|${ANSI.reset} ${ANSI.bold}Reviewer:${ANSI.reset} ${config.roles?.reviewer?.provider || config.reviewer}`
26
+ );
27
+ console.log(
28
+ `${ANSI.bold}Max iterations:${ANSI.reset} ${config.max_iterations} ${ANSI.dim}|${ANSI.reset} ${ANSI.bold}Timeout:${ANSI.reset} ${config.session.max_total_minutes}min`
29
+ );
30
+
31
+ const pipeline = config.pipeline || {};
32
+ const activeRoles = [];
33
+ if (pipeline.planner?.enabled) activeRoles.push(`Planner (${config.roles?.planner?.provider || "?"})`);
34
+ if (pipeline.researcher?.enabled) activeRoles.push(`Researcher (${config.roles?.researcher?.provider || "?"})`);
35
+ if (pipeline.tester?.enabled) activeRoles.push("Tester");
36
+ if (pipeline.security?.enabled) activeRoles.push("Security");
37
+ if (pipeline.solomon?.enabled) {
38
+ const solomonProvider = config.roles?.solomon?.provider;
39
+ activeRoles.push(solomonProvider ? `Solomon (${solomonProvider})` : "Solomon");
40
+ }
41
+ if (activeRoles.length > 0) {
42
+ const separator = ` ${ANSI.dim}|${ANSI.reset} `;
43
+ console.log(`${ANSI.bold}Pipeline:${ANSI.reset} ${activeRoles.join(separator)}`);
44
+ }
45
+
46
+ console.log(BAR);
47
+ console.log();
48
+ }
@@ -0,0 +1,108 @@
1
+ import { ANSI } from "./formatters.js";
2
+
3
+ export function printSessionStages(stages) {
4
+ if (!stages) return;
5
+ if (stages.researcher?.summary) {
6
+ console.log(` ${ANSI.dim}\ud83d\udd2c Research: ${stages.researcher.summary}${ANSI.reset}`);
7
+ }
8
+ printSessionPlanner(stages.planner);
9
+ if (stages.tester?.summary) {
10
+ console.log(` ${ANSI.dim}\ud83e\uddea Tester: ${stages.tester.summary}${ANSI.reset}`);
11
+ }
12
+ if (stages.security?.summary) {
13
+ console.log(` ${ANSI.dim}\ud83d\udd12 Security: ${stages.security.summary}${ANSI.reset}`);
14
+ }
15
+ printSessionSonar(stages.sonar);
16
+ }
17
+
18
+ export function printSessionPlanner(planner) {
19
+ if (!planner?.title && !planner?.approach && !planner?.completedSteps?.length) return;
20
+ const planParts = [];
21
+ if (planner.title) planParts.push(planner.title);
22
+ if (planner.approach) planParts.push(`approach: ${planner.approach}`);
23
+ console.log(` ${ANSI.dim}\ud83d\uddfa Plan: ${planParts.join(" | ")}${ANSI.reset}`);
24
+ for (const step of planner.completedSteps || []) {
25
+ console.log(` ${ANSI.dim} \u2713 ${step}${ANSI.reset}`);
26
+ }
27
+ }
28
+
29
+ export function printSessionSonar(sonar) {
30
+ if (!sonar) return;
31
+ const gateLabel = sonar.gateStatus === "OK" ? ANSI.green : ANSI.red;
32
+ console.log(` ${ANSI.dim}\ud83d\udd0d Sonar: ${gateLabel}${sonar.gateStatus}${ANSI.reset}${ANSI.dim} (${sonar.openIssues ?? 0} issues)${ANSI.reset}`);
33
+ if (typeof sonar.issuesInitial === "number" || typeof sonar.issuesResolved === "number") {
34
+ const issuesInitial = sonar.issuesInitial ?? sonar.openIssues ?? 0;
35
+ const issuesFinal = sonar.issuesFinal ?? sonar.openIssues ?? 0;
36
+ const issuesResolved = sonar.issuesResolved ?? Math.max(issuesInitial - issuesFinal, 0);
37
+ console.log(` ${ANSI.dim}\ud83d\udee0 Issues: ${issuesInitial} detected, ${issuesFinal} open, ${issuesResolved} resolved${ANSI.reset}`);
38
+ }
39
+ }
40
+
41
+ export function printSessionGit(git) {
42
+ if (!git?.branch) return;
43
+ const parts = [`branch: ${git.branch}`];
44
+ if (git.committed) parts.push("committed");
45
+ if (git.pushed) parts.push("pushed");
46
+ if (git.pr || git.prUrl) parts.push(`PR: ${git.pr || git.prUrl}`);
47
+ console.log(` ${ANSI.dim}\ud83d\udcce Git: ${parts.join(", ")}${ANSI.reset}`);
48
+ if (Array.isArray(git.commits) && git.commits.length > 0) {
49
+ console.log(` ${ANSI.dim}\ud83e\uddfe Commits:${ANSI.reset}`);
50
+ for (const commit of git.commits) {
51
+ const shortHash = (commit.hash || "").slice(0, 7) || "unknown";
52
+ const message = commit.message || "";
53
+ console.log(` ${ANSI.dim} - ${shortHash} ${message}${ANSI.reset}`);
54
+ }
55
+ }
56
+ }
57
+
58
+ export function isBudgetUnavailable(budget) {
59
+ return budget.usage_available === false ||
60
+ (budget.total_tokens === 0 && budget.total_cost_usd === 0 && Object.keys(budget.breakdown_by_role || {}).length > 0);
61
+ }
62
+
63
+ export function printSessionRtkSavings(rtkSavings) {
64
+ if (!rtkSavings || !rtkSavings.callCount) return;
65
+ const tokens = rtkSavings.estimatedTokensSaved ?? 0;
66
+ const ratio = rtkSavings.savedPct ?? 0;
67
+ const commands = rtkSavings.callCount ?? 0;
68
+ const original = rtkSavings.originalBytes ?? 0;
69
+ const compressed = rtkSavings.rtkBytes ?? 0;
70
+
71
+ if (tokens === 0 || ratio === 0) {
72
+ console.log(` ${ANSI.dim}⚡ RTK: ${commands} commands wrapped, 0% savings — output was already minimal (${original} bytes in, ${compressed} bytes out)${ANSI.reset}`);
73
+ } else {
74
+ console.log(` ${ANSI.dim}⚡ RTK: saved ~${tokens.toLocaleString()} tokens (${ratio}% compression, ${commands} commands, ${original.toLocaleString()} → ${compressed.toLocaleString()} bytes)${ANSI.reset}`);
75
+ }
76
+ }
77
+
78
+ export function printSessionBudget(budget) {
79
+ if (!budget) return;
80
+ if (isBudgetUnavailable(budget)) {
81
+ console.log(` ${ANSI.dim}\ud83d\udcb0 Budget: N/A (provider does not report usage)${ANSI.reset}`);
82
+ return;
83
+ }
84
+ const estPrefix = budget.includes_estimates ? "~" : "";
85
+ const estNote = budget.includes_estimates ? " (includes estimates)" : "";
86
+ const fmtTokens = (n) => Number(n || 0).toLocaleString("en-US");
87
+ console.log(` ${ANSI.dim}\ud83d\udcb0 Total tokens: ${estPrefix}${fmtTokens(budget.total_tokens)}${estNote}${ANSI.reset}`);
88
+ console.log(` ${ANSI.dim}\ud83d\udcb0 Total cost: ${estPrefix}$${Number(budget.total_cost_usd || 0).toFixed(2)}${ANSI.reset}`);
89
+ // KJC-TSK-0274: KJ-vs-non-KJ comparison line when compression data exists.
90
+ const cmp = budget.kj_comparison;
91
+ if (cmp?.hasCompression) {
92
+ const wkCost = `$${Number(cmp.withKj?.cost || 0).toFixed(2)}`;
93
+ const wkTok = fmtTokens(Number(cmp.withKj?.tokens || 0));
94
+ const wnCost = `$${Number(cmp.withoutKj?.cost || 0).toFixed(2)}`;
95
+ const wnTok = fmtTokens(Number(cmp.withoutKj?.tokens || 0));
96
+ const pct = Math.round(Number(cmp.savedPct || 0));
97
+ console.log(` ${ANSI.dim} \u2937 With KJ: ${wkCost} / ${wkTok} tokens${ANSI.reset}`);
98
+ console.log(` ${ANSI.dim} \u2937 Without KJ: ~${wnCost} / ~${wnTok} tokens (-${pct}%)${ANSI.reset}`);
99
+ }
100
+ for (const [role, metrics] of Object.entries(budget.breakdown_by_role || {})) {
101
+ const tokens = Number(metrics.total_tokens || 0);
102
+ const cost = Number(metrics.total_cost_usd || 0);
103
+ if (tokens === 0 && cost === 0) continue; // skip roles with no usage
104
+ console.log(
105
+ ` ${ANSI.dim} - ${role}: ${estPrefix}${fmtTokens(tokens)} tokens, ${estPrefix}$${cost.toFixed(2)}${ANSI.reset}`
106
+ );
107
+ }
108
+ }