karajan-code 1.16.0 → 1.18.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 (72) hide show
  1. package/package.json +1 -1
  2. package/src/activity-log.js +13 -13
  3. package/src/agents/availability.js +2 -3
  4. package/src/agents/claude-agent.js +42 -21
  5. package/src/agents/model-registry.js +1 -1
  6. package/src/becaria/dispatch.js +1 -1
  7. package/src/becaria/repo.js +3 -3
  8. package/src/cli.js +5 -2
  9. package/src/commands/doctor.js +154 -108
  10. package/src/commands/init.js +101 -90
  11. package/src/commands/plan.js +1 -1
  12. package/src/commands/report.js +77 -71
  13. package/src/commands/roles.js +0 -1
  14. package/src/commands/run.js +2 -3
  15. package/src/config.js +174 -93
  16. package/src/git/automation.js +3 -4
  17. package/src/guards/intent-guard.js +123 -0
  18. package/src/guards/output-guard.js +158 -0
  19. package/src/guards/perf-guard.js +126 -0
  20. package/src/guards/policy-resolver.js +3 -3
  21. package/src/mcp/orphan-guard.js +1 -2
  22. package/src/mcp/progress.js +4 -3
  23. package/src/mcp/run-kj.js +1 -0
  24. package/src/mcp/server-handlers.js +242 -253
  25. package/src/mcp/server.js +4 -3
  26. package/src/mcp/tools.js +2 -0
  27. package/src/orchestrator/agent-fallback.js +1 -3
  28. package/src/orchestrator/iteration-stages.js +206 -170
  29. package/src/orchestrator/pre-loop-stages.js +200 -34
  30. package/src/orchestrator/solomon-rules.js +2 -2
  31. package/src/orchestrator.js +902 -746
  32. package/src/planning-game/adapter.js +23 -20
  33. package/src/planning-game/architect-adrs.js +45 -0
  34. package/src/planning-game/client.js +15 -1
  35. package/src/planning-game/decomposition.js +7 -5
  36. package/src/prompts/architect.js +88 -0
  37. package/src/prompts/discover.js +54 -53
  38. package/src/prompts/planner.js +53 -33
  39. package/src/prompts/triage.js +8 -16
  40. package/src/review/parser.js +18 -19
  41. package/src/review/profiles.js +2 -2
  42. package/src/review/schema.js +3 -3
  43. package/src/review/scope-filter.js +3 -4
  44. package/src/roles/architect-role.js +122 -0
  45. package/src/roles/commiter-role.js +2 -2
  46. package/src/roles/discover-role.js +59 -67
  47. package/src/roles/index.js +1 -0
  48. package/src/roles/planner-role.js +54 -38
  49. package/src/roles/refactorer-role.js +8 -7
  50. package/src/roles/researcher-role.js +6 -7
  51. package/src/roles/reviewer-role.js +4 -5
  52. package/src/roles/security-role.js +3 -4
  53. package/src/roles/solomon-role.js +6 -18
  54. package/src/roles/sonar-role.js +5 -1
  55. package/src/roles/tester-role.js +8 -5
  56. package/src/roles/triage-role.js +2 -2
  57. package/src/session-cleanup.js +29 -24
  58. package/src/session-store.js +1 -1
  59. package/src/sonar/api.js +1 -1
  60. package/src/sonar/manager.js +1 -1
  61. package/src/sonar/project-key.js +5 -5
  62. package/src/sonar/scanner.js +34 -65
  63. package/src/utils/display.js +312 -272
  64. package/src/utils/git.js +3 -3
  65. package/src/utils/logger.js +6 -1
  66. package/src/utils/model-selector.js +5 -5
  67. package/src/utils/process.js +80 -102
  68. package/src/utils/rate-limit-detector.js +13 -13
  69. package/src/utils/run-log.js +55 -52
  70. package/templates/kj.config.yml +33 -0
  71. package/templates/roles/architect.md +62 -0
  72. package/templates/roles/planner.md +1 -0
@@ -87,308 +87,348 @@ export function printHeader({ task, config }) {
87
87
  if (pipeline.security?.enabled) activeRoles.push("Security");
88
88
  if (pipeline.solomon?.enabled) activeRoles.push(`Solomon (${config.roles?.solomon?.provider || "?"})`);
89
89
  if (activeRoles.length > 0) {
90
- console.log(`${ANSI.bold}Pipeline:${ANSI.reset} ${activeRoles.join(` ${ANSI.dim}|${ANSI.reset} `)}`);
90
+ const separator = ` ${ANSI.dim}|${ANSI.reset} `;
91
+ console.log(`${ANSI.bold}Pipeline:${ANSI.reset} ${activeRoles.join(separator)}`);
91
92
  }
92
93
 
93
94
  console.log(BAR);
94
95
  console.log();
95
96
  }
96
97
 
97
- export function printEvent(event) {
98
- const icon = ICONS[event.type] || "\u2022";
99
- const elapsed = event.elapsed !== undefined ? `${ANSI.dim}[${formatElapsed(event.elapsed)}]${ANSI.reset}` : "";
100
- const status = event.status ? STATUS_ICON[event.status] || "" : "";
101
-
102
- switch (event.type) {
103
- case "session:start":
104
- break;
105
-
106
- case "iteration:start":
107
- console.log(
108
- `\n${ANSI.bold}${icon} Iteration ${event.detail?.iteration}/${event.detail?.maxIterations}${ANSI.reset} ${elapsed}`
109
- );
110
- break;
111
-
112
- case "planner:start":
113
- console.log(` \u251c\u2500 ${icon} Planner (${event.detail?.planner || "?"}) running...`);
114
- break;
115
-
116
- case "planner:end":
117
- console.log(` \u251c\u2500 ${status} Planner completed ${elapsed}`);
118
- break;
119
-
120
- case "coder:start":
121
- console.log(` \u251c\u2500 ${icon} Coder (${event.detail?.coder || "?"}) running...`);
122
- break;
123
-
124
- case "coder:end":
125
- console.log(` \u251c\u2500 ${status} Coder completed ${elapsed}`);
126
- break;
127
-
128
- case "refactorer:start":
129
- console.log(` \u251c\u2500 ${icon} Refactorer (${event.detail?.refactorer || "?"}) running...`);
130
- break;
131
-
132
- case "refactorer:end":
133
- console.log(` \u251c\u2500 ${status} Refactorer completed ${elapsed}`);
134
- break;
135
-
136
- case "tdd:result": {
137
- const tdd = event.detail || {};
138
- const label = tdd.ok ? `${ANSI.green}PASS${ANSI.reset}` : `${ANSI.red}FAIL${ANSI.reset}`;
139
- const files = tdd.sourceFiles !== undefined ? ` (${tdd.sourceFiles} src, ${tdd.testFiles} test)` : "";
140
- console.log(` \u251c\u2500 ${icon} TDD policy: ${label}${files}`);
141
- break;
142
- }
98
+ /* ── Helper: role start/end one-liners ───────────────────────── */
143
99
 
144
- case "researcher:start":
145
- console.log(` \u251c\u2500 ${icon} Researcher (${event.detail?.researcher || "?"}) investigating...`);
146
- break;
100
+ function roleStart(icon, label, provider) {
101
+ console.log(` \u251c\u2500 ${icon} ${label} (${provider || "?"}) running...`);
102
+ }
147
103
 
148
- case "researcher:end":
149
- console.log(` \u251c\u2500 ${status} Researcher completed ${elapsed}`);
150
- break;
104
+ function roleEnd(status, label, elapsed) {
105
+ console.log(` \u251c\u2500 ${status} ${label} completed ${elapsed}`);
106
+ }
151
107
 
152
- case "sonar:start":
153
- console.log(` \u251c\u2500 ${icon} SonarQube scanning...`);
154
- break;
108
+ /* ── Helper: pass/fail stage result ─────────────────────────── */
155
109
 
156
- case "sonar:end": {
157
- const gate = event.detail?.gateStatus || "?";
158
- const gateColor = gate === "OK" ? ANSI.green : ANSI.red;
159
- console.log(` \u251c\u2500 ${status} Quality gate: ${gateColor}${gate}${ANSI.reset} ${elapsed}`);
160
- break;
161
- }
110
+ function passFailStage(detail, label, failDefault, elapsed) {
111
+ if (detail?.ok === false) {
112
+ const summary = detail?.summary || failDefault;
113
+ console.log(` \u251c\u2500 ${ANSI.red}\u274c ${label}: ${summary}${ANSI.reset} ${elapsed}`);
114
+ } else {
115
+ console.log(` \u251c\u2500 ${ANSI.green}\u2705 ${label}: passed${ANSI.reset} ${elapsed}`);
116
+ }
117
+ }
162
118
 
163
- case "reviewer:start":
164
- console.log(` \u251c\u2500 ${icon} Reviewer (${event.detail?.reviewer || "?"}) running...`);
165
- break;
166
-
167
- case "reviewer:end": {
168
- const review = event.detail || {};
169
- if (review.approved) {
170
- console.log(` \u251c\u2500 ${ANSI.green}\u2705 Review: APPROVED${ANSI.reset} ${elapsed}`);
171
- } else {
172
- const count = review.blockingCount || 0;
173
- console.log(` \u251c\u2500 ${ANSI.red}\u274c Review: REJECTED (${count} blocking)${ANSI.reset}`);
174
- if (review.issues) {
175
- for (const issue of review.issues) {
176
- console.log(` \u2502 ${ANSI.dim}${issue}${ANSI.reset}`);
177
- }
178
- }
119
+ /* ── Helper: solomon ruling display ─────────────────────────── */
120
+
121
+ const SOLOMON_RULING_HANDLERS = {
122
+ approve(detail, elapsed) {
123
+ const dismissedCount = detail?.dismissed?.length || 0;
124
+ const dismissedSuffix = dismissedCount > 0 ? ` (${dismissedCount} dismissed)` : "";
125
+ console.log(` \u251c\u2500 ${ANSI.green}\u2696\ufe0f Solomon: APPROVE${dismissedSuffix}${ANSI.reset} ${elapsed}`);
126
+ },
127
+ approve_with_conditions(detail, elapsed) {
128
+ const condCount = detail?.conditions?.length || 0;
129
+ console.log(` \u251c\u2500 ${ANSI.yellow}\u2696\ufe0f Solomon: ${condCount} condition${condCount === 1 ? "" : "s"}${ANSI.reset} ${elapsed}`);
130
+ if (detail?.conditions) {
131
+ for (const cond of detail.conditions) {
132
+ console.log(` \u2502 ${ANSI.dim}${cond}${ANSI.reset}`);
179
133
  }
180
- break;
181
134
  }
135
+ },
136
+ escalate_human(detail, elapsed) {
137
+ const reason = detail?.escalate_reason || "unknown reason";
138
+ console.log(` \u251c\u2500 ${ANSI.red}\u2696\ufe0f Solomon: ESCALATE \u2014 ${reason}${ANSI.reset} ${elapsed}`);
139
+ },
140
+ create_subtask(detail, elapsed) {
141
+ const subtaskTitle = detail?.subtask?.title || "untitled";
142
+ console.log(` \u251c\u2500 ${ANSI.magenta}\u2696\ufe0f Solomon: SUBTASK \u2014 ${subtaskTitle}${ANSI.reset} ${elapsed}`);
143
+ }
144
+ };
182
145
 
183
- case "tester:start":
184
- console.log(` \u251c\u2500 ${icon} Tester evaluating...`);
185
- break;
186
-
187
- case "tester:end": {
188
- const testerOk = event.detail?.ok !== false;
189
- if (testerOk) {
190
- console.log(` \u251c\u2500 ${ANSI.green}\u2705 Tester: passed${ANSI.reset} ${elapsed}`);
191
- } else {
192
- const testerSummary = event.detail?.summary || "issues found";
193
- console.log(` \u251c\u2500 ${ANSI.red}\u274c Tester: ${testerSummary}${ANSI.reset} ${elapsed}`);
194
- }
195
- break;
196
- }
146
+ function printSolomonRuling(detail, elapsed) {
147
+ const ruling = detail?.ruling || "unknown";
148
+ const handler = SOLOMON_RULING_HANDLERS[ruling];
149
+ if (handler) {
150
+ handler(detail, elapsed);
151
+ } else {
152
+ const rulingUpper = ruling.toUpperCase().replaceAll("_", " ");
153
+ console.log(` \u251c\u2500 \u2696\ufe0f Solomon: ${rulingUpper} ${elapsed}`);
154
+ }
155
+ }
197
156
 
198
- case "security:start":
199
- console.log(` \u251c\u2500 ${icon} Security auditing...`);
200
- break;
201
-
202
- case "security:end": {
203
- const secOk = event.detail?.ok !== false;
204
- if (secOk) {
205
- console.log(` \u251c\u2500 ${ANSI.green}\u2705 Security: passed${ANSI.reset} ${elapsed}`);
206
- } else {
207
- const secSummary = event.detail?.summary || "vulnerabilities found";
208
- console.log(` \u251c\u2500 ${ANSI.red}\u274c Security: ${secSummary}${ANSI.reset} ${elapsed}`);
209
- }
210
- break;
211
- }
157
+ /* ── Helper: budget color selection ─────────────────────────── */
212
158
 
213
- case "solomon:start":
214
- console.log(` \u251c\u2500 ${icon} Solomon arbitrating ${event.detail?.conflictStage || "?"} conflict...`);
215
- break;
216
-
217
- case "solomon:end": {
218
- const ruling = event.detail?.ruling || "unknown";
219
- const rulingUpper = ruling.toUpperCase().replace(/_/g, " ");
220
- if (ruling === "approve") {
221
- const dismissedCount = event.detail?.dismissed?.length || 0;
222
- console.log(` \u251c\u2500 ${ANSI.green}\u2696\ufe0f Solomon: APPROVE${dismissedCount > 0 ? ` (${dismissedCount} dismissed)` : ""}${ANSI.reset} ${elapsed}`);
223
- } else if (ruling === "approve_with_conditions") {
224
- const condCount = event.detail?.conditions?.length || 0;
225
- console.log(` \u251c\u2500 ${ANSI.yellow}\u2696\ufe0f Solomon: ${condCount} condition${condCount !== 1 ? "s" : ""}${ANSI.reset} ${elapsed}`);
226
- if (event.detail?.conditions) {
227
- for (const cond of event.detail.conditions) {
228
- console.log(` \u2502 ${ANSI.dim}${cond}${ANSI.reset}`);
229
- }
230
- }
231
- } else if (ruling === "escalate_human") {
232
- const reason = event.detail?.escalate_reason || "unknown reason";
233
- console.log(` \u251c\u2500 ${ANSI.red}\u2696\ufe0f Solomon: ESCALATE \u2014 ${reason}${ANSI.reset} ${elapsed}`);
234
- } else if (ruling === "create_subtask") {
235
- const subtaskTitle = event.detail?.subtask?.title || "untitled";
236
- console.log(` \u251c\u2500 ${ANSI.magenta}\u2696\ufe0f Solomon: SUBTASK \u2014 ${subtaskTitle}${ANSI.reset} ${elapsed}`);
237
- } else {
238
- console.log(` \u251c\u2500 \u2696\ufe0f Solomon: ${rulingUpper} ${elapsed}`);
239
- }
240
- break;
241
- }
159
+ function budgetColor(max, pct, warn) {
160
+ if (max > 0 && pct >= 100) return ANSI.red;
161
+ if (max > 0 && pct >= warn) return ANSI.yellow;
162
+ return ANSI.green;
163
+ }
242
164
 
243
- case "solomon:escalate": {
244
- const subloop = event.detail?.subloop || "?";
245
- const retryCount = event.detail?.retryCount || 0;
246
- const limit = event.detail?.limit || "?";
247
- console.log(` \u251c\u2500 ${icon} ${subloop} sub-loop limit reached (${retryCount}/${limit}), invoking Solomon...`);
248
- break;
249
- }
165
+ /* ── Helpers: session:end sub-sections ──────────────────────── */
250
166
 
251
- case "coder:standby": {
252
- const until = event.detail?.cooldownUntil || "?";
253
- const attempt = event.detail?.retryCount || "?";
254
- const maxRetries = event.detail?.maxRetries || "?";
255
- console.log(` \u251c\u2500 ${ANSI.yellow}${icon} Rate limited \u2014 standby until ${until} (attempt ${attempt}/${maxRetries})${ANSI.reset}`);
256
- break;
257
- }
167
+ function printSessionStages(stages) {
168
+ if (!stages) return;
169
+ if (stages.researcher?.summary) {
170
+ console.log(` ${ANSI.dim}\ud83d\udd2c Research: ${stages.researcher.summary}${ANSI.reset}`);
171
+ }
172
+ printSessionPlanner(stages.planner);
173
+ if (stages.tester?.summary) {
174
+ console.log(` ${ANSI.dim}\ud83e\uddea Tester: ${stages.tester.summary}${ANSI.reset}`);
175
+ }
176
+ if (stages.security?.summary) {
177
+ console.log(` ${ANSI.dim}\ud83d\udd12 Security: ${stages.security.summary}${ANSI.reset}`);
178
+ }
179
+ printSessionSonar(stages.sonar);
180
+ }
258
181
 
259
- case "coder:standby_heartbeat": {
260
- const remaining = event.detail?.remainingMs !== undefined ? Math.round(event.detail.remainingMs / 1000) : "?";
261
- console.log(` \u251c\u2500 ${ANSI.yellow}${icon} Standby: ${remaining}s remaining${ANSI.reset}`);
262
- break;
263
- }
182
+ function printSessionPlanner(planner) {
183
+ if (!planner?.title && !planner?.approach && !planner?.completedSteps?.length) return;
184
+ const planParts = [];
185
+ if (planner.title) planParts.push(planner.title);
186
+ if (planner.approach) planParts.push(`approach: ${planner.approach}`);
187
+ console.log(` ${ANSI.dim}\ud83d\uddfa Plan: ${planParts.join(" | ")}${ANSI.reset}`);
188
+ for (const step of planner.completedSteps || []) {
189
+ console.log(` ${ANSI.dim} \u2713 ${step}${ANSI.reset}`);
190
+ }
191
+ }
264
192
 
265
- case "coder:standby_resume":
266
- console.log(` \u251c\u2500 ${ANSI.green}${icon} Cooldown expired \u2014 resuming with ${event.detail?.coder || event.detail?.provider || "?"}${ANSI.reset}`);
267
- break;
268
-
269
- case "iteration:end":
270
- console.log(` \u2514\u2500 ${icon} Duration: ${formatElapsed(event.detail?.duration)} ${elapsed}`);
271
- break;
272
-
273
- case "budget:update": {
274
- const total = Number(event.detail?.total_cost_usd || 0);
275
- const max = Number(event.detail?.max_budget_usd);
276
- const pct = Number(event.detail?.pct_used ?? 0);
277
- const warn = Number(event.detail?.warn_threshold_pct ?? 80);
278
- const color = max > 0 && pct >= 100 ? ANSI.red : max > 0 && pct >= warn ? ANSI.yellow : ANSI.green;
279
- if (Number.isFinite(max) && max >= 0) {
280
- console.log(` \u251c\u2500 ${icon} Budget: ${color}$${total.toFixed(2)} / $${max.toFixed(2)} (${pct.toFixed(1)}%)${ANSI.reset}`);
281
- }
282
- break;
193
+ function printSessionSonar(sonar) {
194
+ if (!sonar) return;
195
+ const gateLabel = sonar.gateStatus === "OK" ? ANSI.green : ANSI.red;
196
+ console.log(` ${ANSI.dim}\ud83d\udd0d Sonar: ${gateLabel}${sonar.gateStatus}${ANSI.reset}${ANSI.dim} (${sonar.openIssues ?? 0} issues)${ANSI.reset}`);
197
+ if (typeof sonar.issuesInitial === "number" || typeof sonar.issuesResolved === "number") {
198
+ const issuesInitial = sonar.issuesInitial ?? sonar.openIssues ?? 0;
199
+ const issuesFinal = sonar.issuesFinal ?? sonar.openIssues ?? 0;
200
+ const issuesResolved = sonar.issuesResolved ?? Math.max(issuesInitial - issuesFinal, 0);
201
+ console.log(` ${ANSI.dim}\ud83d\udee0 Issues: ${issuesInitial} detected, ${issuesFinal} open, ${issuesResolved} resolved${ANSI.reset}`);
202
+ }
203
+ }
204
+
205
+ function printSessionGit(git) {
206
+ if (!git?.branch) return;
207
+ const parts = [`branch: ${git.branch}`];
208
+ if (git.committed) parts.push("committed");
209
+ if (git.pushed) parts.push("pushed");
210
+ if (git.pr || git.prUrl) parts.push(`PR: ${git.pr || git.prUrl}`);
211
+ console.log(` ${ANSI.dim}\ud83d\udcce Git: ${parts.join(", ")}${ANSI.reset}`);
212
+ if (Array.isArray(git.commits) && git.commits.length > 0) {
213
+ console.log(` ${ANSI.dim}\ud83e\uddfe Commits:${ANSI.reset}`);
214
+ for (const commit of git.commits) {
215
+ const shortHash = (commit.hash || "").slice(0, 7) || "unknown";
216
+ const message = commit.message || "";
217
+ console.log(` ${ANSI.dim} - ${shortHash} ${message}${ANSI.reset}`);
283
218
  }
219
+ }
220
+ }
284
221
 
285
- case "session:end": {
286
- console.log();
287
- const resultLabel = event.detail?.approved
288
- ? `${ANSI.bold}${ANSI.green}APPROVED${ANSI.reset}`
289
- : `${ANSI.bold}${ANSI.red}${event.detail?.reason || "FAILED"}${ANSI.reset}`;
290
- console.log(`${icon} Result: ${resultLabel} ${elapsed}`);
291
-
292
- const stages = event.detail?.stages;
293
- if (stages) {
294
- if (stages.researcher?.summary) {
295
- console.log(` ${ANSI.dim}\ud83d\udd2c Research: ${stages.researcher.summary}${ANSI.reset}`);
296
- }
297
- if (stages.planner?.title || stages.planner?.approach || stages.planner?.completedSteps?.length) {
298
- const planParts = [];
299
- if (stages.planner.title) planParts.push(stages.planner.title);
300
- if (stages.planner.approach) planParts.push(`approach: ${stages.planner.approach}`);
301
- console.log(` ${ANSI.dim}\ud83d\uddfa Plan: ${planParts.join(" | ")}${ANSI.reset}`);
302
- for (const step of stages.planner.completedSteps || []) {
303
- console.log(` ${ANSI.dim} \u2713 ${step}${ANSI.reset}`);
304
- }
305
- }
306
- if (stages.tester?.summary) {
307
- console.log(` ${ANSI.dim}\ud83e\uddea Tester: ${stages.tester.summary}${ANSI.reset}`);
308
- }
309
- if (stages.security?.summary) {
310
- console.log(` ${ANSI.dim}\ud83d\udd12 Security: ${stages.security.summary}${ANSI.reset}`);
311
- }
312
- if (stages.sonar) {
313
- const gateLabel = stages.sonar.gateStatus === "OK" ? ANSI.green : ANSI.red;
314
- console.log(` ${ANSI.dim}\ud83d\udd0d Sonar: ${gateLabel}${stages.sonar.gateStatus}${ANSI.reset}${ANSI.dim} (${stages.sonar.openIssues ?? 0} issues)${ANSI.reset}`);
315
- if (typeof stages.sonar.issuesInitial === "number" || typeof stages.sonar.issuesResolved === "number") {
316
- const issuesInitial = stages.sonar.issuesInitial ?? stages.sonar.openIssues ?? 0;
317
- const issuesFinal = stages.sonar.issuesFinal ?? stages.sonar.openIssues ?? 0;
318
- const issuesResolved = stages.sonar.issuesResolved ?? Math.max(issuesInitial - issuesFinal, 0);
319
- console.log(` ${ANSI.dim}\ud83d\udee0 Issues: ${issuesInitial} detected, ${issuesFinal} open, ${issuesResolved} resolved${ANSI.reset}`);
320
- }
321
- }
322
- }
222
+ function printSessionBudget(budget) {
223
+ if (!budget) return;
224
+ console.log(` ${ANSI.dim}\ud83d\udcb0 Total tokens: ${budget.total_tokens ?? 0}${ANSI.reset}`);
225
+ console.log(` ${ANSI.dim}\ud83d\udcb0 Total cost: $${Number(budget.total_cost_usd || 0).toFixed(2)}${ANSI.reset}`);
226
+ for (const [role, metrics] of Object.entries(budget.breakdown_by_role || {})) {
227
+ console.log(
228
+ ` ${ANSI.dim} - ${role}: ${metrics.total_tokens ?? 0} tokens, $${Number(metrics.total_cost_usd || 0).toFixed(2)}${ANSI.reset}`
229
+ );
230
+ }
231
+ }
323
232
 
324
- const git = event.detail?.git;
325
- if (git?.branch) {
326
- const parts = [`branch: ${git.branch}`];
327
- if (git.committed) parts.push("committed");
328
- if (git.pushed) parts.push("pushed");
329
- if (git.pr || git.prUrl) parts.push(`PR: ${git.pr || git.prUrl}`);
330
- console.log(` ${ANSI.dim}\ud83d\udcce Git: ${parts.join(", ")}${ANSI.reset}`);
331
- if (Array.isArray(git.commits) && git.commits.length > 0) {
332
- console.log(` ${ANSI.dim}\ud83e\uddfe Commits:${ANSI.reset}`);
333
- for (const commit of git.commits) {
334
- const shortHash = (commit.hash || "").slice(0, 7) || "unknown";
335
- const message = commit.message || "";
336
- console.log(` ${ANSI.dim} - ${shortHash} ${message}${ANSI.reset}`);
337
- }
338
- }
339
- }
233
+ /* ── Helper: pipeline tracker stage icon/color ──────────────── */
340
234
 
341
- const budget = event.detail?.budget;
342
- if (budget) {
343
- console.log(` ${ANSI.dim}\ud83d\udcb0 Total tokens: ${budget.total_tokens ?? 0}${ANSI.reset}`);
344
- console.log(` ${ANSI.dim}\ud83d\udcb0 Total cost: $${Number(budget.total_cost_usd || 0).toFixed(2)}${ANSI.reset}`);
345
- const byRole = budget.breakdown_by_role || {};
346
- const roles = Object.entries(byRole);
347
- if (roles.length > 0) {
348
- for (const [role, metrics] of roles) {
349
- console.log(
350
- ` ${ANSI.dim} - ${role}: ${metrics.total_tokens ?? 0} tokens, $${Number(metrics.total_cost_usd || 0).toFixed(2)}${ANSI.reset}`
351
- );
352
- }
235
+ const TRACKER_STATUS = {
236
+ done: { icon: "\u2713", color: ANSI.green },
237
+ running: { icon: "\u25b6", color: ANSI.cyan },
238
+ failed: { icon: "\u2717", color: ANSI.red }
239
+ };
240
+ const TRACKER_DEFAULT = { icon: "\u00b7", color: ANSI.dim };
241
+
242
+ /* ── Event handler map ──────────────────────────────────────── */
243
+
244
+ const EVENT_HANDLERS = {
245
+ "session:start": () => {},
246
+
247
+ "iteration:start": (event, icon, elapsed) => {
248
+ console.log(
249
+ `\n${ANSI.bold}${icon} Iteration ${event.detail?.iteration}/${event.detail?.maxIterations}${ANSI.reset} ${elapsed}`
250
+ );
251
+ },
252
+
253
+ "planner:start": (event, icon) => {
254
+ roleStart(icon, "Planner", event.detail?.planner);
255
+ },
256
+
257
+ "planner:end": (_event, _icon, elapsed, status) => {
258
+ roleEnd(status, "Planner", elapsed);
259
+ },
260
+
261
+ "coder:start": (event, icon) => {
262
+ roleStart(icon, "Coder", event.detail?.coder);
263
+ },
264
+
265
+ "coder:end": (_event, _icon, elapsed, status) => {
266
+ roleEnd(status, "Coder", elapsed);
267
+ },
268
+
269
+ "refactorer:start": (event, icon) => {
270
+ roleStart(icon, "Refactorer", event.detail?.refactorer);
271
+ },
272
+
273
+ "refactorer:end": (_event, _icon, elapsed, status) => {
274
+ roleEnd(status, "Refactorer", elapsed);
275
+ },
276
+
277
+ "tdd:result": (event, icon) => {
278
+ const tdd = event.detail || {};
279
+ const label = tdd.ok ? `${ANSI.green}PASS${ANSI.reset}` : `${ANSI.red}FAIL${ANSI.reset}`;
280
+ const files = tdd.sourceFiles === undefined ? "" : ` (${tdd.sourceFiles} src, ${tdd.testFiles} test)`;
281
+ console.log(` \u251c\u2500 ${icon} TDD policy: ${label}${files}`);
282
+ },
283
+
284
+ "researcher:start": (event, icon) => {
285
+ console.log(` \u251c\u2500 ${icon} Researcher (${event.detail?.researcher || "?"}) investigating...`);
286
+ },
287
+
288
+ "researcher:end": (_event, _icon, elapsed, status) => {
289
+ roleEnd(status, "Researcher", elapsed);
290
+ },
291
+
292
+ "sonar:start": (_event, icon) => {
293
+ console.log(` \u251c\u2500 ${icon} SonarQube scanning...`);
294
+ },
295
+
296
+ "sonar:end": (event, _icon, elapsed, status) => {
297
+ const gate = event.detail?.gateStatus || "?";
298
+ const gateColor = gate === "OK" ? ANSI.green : ANSI.red;
299
+ console.log(` \u251c\u2500 ${status} Quality gate: ${gateColor}${gate}${ANSI.reset} ${elapsed}`);
300
+ },
301
+
302
+ "reviewer:start": (event, icon) => {
303
+ console.log(` \u251c\u2500 ${icon} Reviewer (${event.detail?.reviewer || "?"}) running...`);
304
+ },
305
+
306
+ "reviewer:end": (event, _icon, elapsed) => {
307
+ const review = event.detail || {};
308
+ if (review.approved) {
309
+ console.log(` \u251c\u2500 ${ANSI.green}\u2705 Review: APPROVED${ANSI.reset} ${elapsed}`);
310
+ } else {
311
+ const count = review.blockingCount || 0;
312
+ console.log(` \u251c\u2500 ${ANSI.red}\u274c Review: REJECTED (${count} blocking)${ANSI.reset}`);
313
+ if (review.issues) {
314
+ for (const issue of review.issues) {
315
+ console.log(` \u2502 ${ANSI.dim}${issue}${ANSI.reset}`);
353
316
  }
354
317
  }
355
-
356
- console.log(`${ANSI.dim}Session: ${event.sessionId}${ANSI.reset}`);
357
- break;
358
318
  }
359
-
360
- case "question":
361
- console.log();
362
- console.log(`${ANSI.bold}${ANSI.yellow}${icon} Paused - question:${ANSI.reset}`);
363
- console.log(` ${event.detail?.question || event.message}`);
364
- console.log(`${ANSI.dim}Resume with: kj resume ${event.sessionId} --answer "<response>"${ANSI.reset}`);
365
- break;
366
-
367
- case "pipeline:tracker": {
368
- const trackerStages = event.detail?.stages || [];
369
- console.log(` ${ANSI.dim}\u250c Pipeline${ANSI.reset}`);
370
- for (const stage of trackerStages) {
371
- let stIcon, stColor;
372
- switch (stage.status) {
373
- case "done": stIcon = "\u2713"; stColor = ANSI.green; break;
374
- case "running": stIcon = "\u25b6"; stColor = ANSI.cyan; break;
375
- case "failed": stIcon = "\u2717"; stColor = ANSI.red; break;
376
- default: stIcon = "\u00b7"; stColor = ANSI.dim; break;
377
- }
378
- const suffix = stage.summary
379
- ? stage.status === "running" ? ` (${stage.summary})` : ` \u2192 ${stage.summary}`
380
- : "";
381
- console.log(` ${ANSI.dim}\u2502${ANSI.reset} ${stColor}${stIcon} ${stage.name}${suffix}${ANSI.reset}`);
319
+ },
320
+
321
+ "tester:start": (_event, icon) => {
322
+ console.log(` \u251c\u2500 ${icon} Tester evaluating...`);
323
+ },
324
+
325
+ "tester:end": (event, _icon, elapsed) => {
326
+ passFailStage(event.detail, "Tester", "issues found", elapsed);
327
+ },
328
+
329
+ "security:start": (_event, icon) => {
330
+ console.log(` \u251c\u2500 ${icon} Security auditing...`);
331
+ },
332
+
333
+ "security:end": (event, _icon, elapsed) => {
334
+ passFailStage(event.detail, "Security", "vulnerabilities found", elapsed);
335
+ },
336
+
337
+ "solomon:start": (event, icon) => {
338
+ console.log(` \u251c\u2500 ${icon} Solomon arbitrating ${event.detail?.conflictStage || "?"} conflict...`);
339
+ },
340
+
341
+ "solomon:end": (event, _icon, elapsed) => {
342
+ printSolomonRuling(event.detail, elapsed);
343
+ },
344
+
345
+ "solomon:escalate": (event, icon) => {
346
+ const subloop = event.detail?.subloop || "?";
347
+ const retryCount = event.detail?.retryCount || 0;
348
+ const limit = event.detail?.limit || "?";
349
+ console.log(` \u251c\u2500 ${icon} ${subloop} sub-loop limit reached (${retryCount}/${limit}), invoking Solomon...`);
350
+ },
351
+
352
+ "coder:standby": (event, icon) => {
353
+ const until = event.detail?.cooldownUntil || "?";
354
+ const attempt = event.detail?.retryCount || "?";
355
+ const maxRetries = event.detail?.maxRetries || "?";
356
+ console.log(` \u251c\u2500 ${ANSI.yellow}${icon} Rate limited \u2014 standby until ${until} (attempt ${attempt}/${maxRetries})${ANSI.reset}`);
357
+ },
358
+
359
+ "coder:standby_heartbeat": (event, icon) => {
360
+ const remaining = event.detail?.remainingMs === undefined ? "?" : Math.round(event.detail.remainingMs / 1000);
361
+ console.log(` \u251c\u2500 ${ANSI.yellow}${icon} Standby: ${remaining}s remaining${ANSI.reset}`);
362
+ },
363
+
364
+ "coder:standby_resume": (event, icon) => {
365
+ console.log(` \u251c\u2500 ${ANSI.green}${icon} Cooldown expired \u2014 resuming with ${event.detail?.coder || event.detail?.provider || "?"}${ANSI.reset}`);
366
+ },
367
+
368
+ "iteration:end": (event, icon, elapsed) => {
369
+ console.log(` \u2514\u2500 ${icon} Duration: ${formatElapsed(event.detail?.duration)} ${elapsed}`);
370
+ },
371
+
372
+ "budget:update": (event, icon) => {
373
+ const total = Number(event.detail?.total_cost_usd || 0);
374
+ const max = Number(event.detail?.max_budget_usd);
375
+ const pct = Number(event.detail?.pct_used ?? 0);
376
+ const warn = Number(event.detail?.warn_threshold_pct ?? 80);
377
+ const color = budgetColor(max, pct, warn);
378
+ if (Number.isFinite(max) && max >= 0) {
379
+ console.log(` \u251c\u2500 ${icon} Budget: ${color}$${total.toFixed(2)} / $${max.toFixed(2)} (${pct.toFixed(1)}%)${ANSI.reset}`);
380
+ }
381
+ },
382
+
383
+ "session:end": (event, icon, elapsed) => {
384
+ console.log();
385
+ const resultLabel = event.detail?.approved
386
+ ? `${ANSI.bold}${ANSI.green}APPROVED${ANSI.reset}`
387
+ : `${ANSI.bold}${ANSI.red}${event.detail?.reason || "FAILED"}${ANSI.reset}`;
388
+ console.log(`${icon} Result: ${resultLabel} ${elapsed}`);
389
+ printSessionStages(event.detail?.stages);
390
+ printSessionGit(event.detail?.git);
391
+ printSessionBudget(event.detail?.budget);
392
+ console.log(`${ANSI.dim}Session: ${event.sessionId}${ANSI.reset}`);
393
+ },
394
+
395
+ question: (event, icon) => {
396
+ console.log();
397
+ console.log(`${ANSI.bold}${ANSI.yellow}${icon} Paused - question:${ANSI.reset}`);
398
+ console.log(` ${event.detail?.question || event.message}`);
399
+ console.log(`${ANSI.dim}Resume with: kj resume ${event.sessionId} --answer "<response>"${ANSI.reset}`);
400
+ },
401
+
402
+ "pipeline:tracker": (event) => {
403
+ const trackerStages = event.detail?.stages || [];
404
+ console.log(` ${ANSI.dim}\u250c Pipeline${ANSI.reset}`);
405
+ for (const stage of trackerStages) {
406
+ const { icon: stIcon, color: stColor } = TRACKER_STATUS[stage.status] || TRACKER_DEFAULT;
407
+ let suffix = "";
408
+ if (stage.summary) {
409
+ suffix = stage.status === "running" ? ` (${stage.summary})` : ` \u2192 ${stage.summary}`;
382
410
  }
383
- console.log(` ${ANSI.dim}\u2514${ANSI.reset}`);
384
- break;
411
+ console.log(` ${ANSI.dim}\u2502${ANSI.reset} ${stColor}${stIcon} ${stage.name}${suffix}${ANSI.reset}`);
385
412
  }
413
+ console.log(` ${ANSI.dim}\u2514${ANSI.reset}`);
414
+ },
386
415
 
387
- case "agent:output":
388
- console.log(` \u2502 ${ANSI.dim}${event.message}${ANSI.reset}`);
389
- break;
416
+ "agent:output": (event) => {
417
+ console.log(` \u2502 ${ANSI.dim}${event.message}${ANSI.reset}`);
418
+ }
419
+ };
420
+
421
+ /* ── Main entry point ───────────────────────────────────────── */
422
+
423
+ export function printEvent(event) {
424
+ const icon = ICONS[event.type] || "\u2022";
425
+ const elapsed = event.elapsed === undefined ? "" : `${ANSI.dim}[${formatElapsed(event.elapsed)}]${ANSI.reset}`;
426
+ const status = event.status ? STATUS_ICON[event.status] || "" : "";
390
427
 
391
- default:
392
- console.log(` \u251c\u2500 ${icon} ${event.message || event.type} ${elapsed}`);
428
+ const handler = EVENT_HANDLERS[event.type];
429
+ if (handler) {
430
+ handler(event, icon, elapsed, status);
431
+ } else {
432
+ console.log(` \u251c\u2500 ${icon} ${event.message || event.type} ${elapsed}`);
393
433
  }
394
434
  }
package/src/utils/git.js CHANGED
@@ -3,8 +3,8 @@ import { runCommand } from "./process.js";
3
3
  function slugifyTask(task) {
4
4
  return String(task)
5
5
  .toLowerCase()
6
- .replace(/[^a-z0-9]+/g, "-")
7
- .replace(/^-+|-+$/g, "")
6
+ .replaceAll(/[^a-z0-9]+/g, "-")
7
+ .replaceAll(/(^-+)|(-+$)/g, "")
8
8
  .slice(0, 40);
9
9
  }
10
10
 
@@ -68,7 +68,7 @@ export async function createBranch(branchName) {
68
68
  }
69
69
 
70
70
  export function buildBranchName(prefix, task) {
71
- const stamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 16);
71
+ const stamp = new Date().toISOString().replaceAll(/[:.]/g, "-").slice(0, 16);
72
72
  return `${prefix}${slugifyTask(task) || "task"}-${stamp}`;
73
73
  }
74
74
 
@@ -58,7 +58,12 @@ export function createLogger(level = "info", mode = "cli") {
58
58
  const ts = `${ANSI.dim}${timestamp()}${ANSI.reset}`;
59
59
  const prefix = `${color}[${lvl}]${ANSI.reset}`;
60
60
  const ctx = formatContext(context);
61
- const stream = lvl === "error" ? console.error : lvl === "warn" ? console.warn : console.log;
61
+ let stream = console.log;
62
+ if (lvl === "error") {
63
+ stream = console.error;
64
+ } else if (lvl === "warn") {
65
+ stream = console.warn;
66
+ }
62
67
  stream(`${ts} ${prefix} ${ctx}${args.map((a) => (typeof a === "string" ? a : JSON.stringify(a))).join(" ")}`);
63
68
  }
64
69