kantban-cli 0.1.15 → 0.1.17

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.
@@ -0,0 +1,1162 @@
1
+ import {
2
+ VALID_BRANCH_RE
3
+ } from "./chunk-MN4H5NSU.js";
4
+
5
+ // src/lib/stuck-detector.ts
6
+ import { z } from "zod";
7
+
8
+ // src/lib/parse-utils.ts
9
+ function parseJsonFromLlmOutput(raw) {
10
+ try {
11
+ return JSON.parse(raw.trim());
12
+ } catch {
13
+ }
14
+ const fenceMatch = raw.match(/```(?:json)?\s*([\s\S]*?)```/i);
15
+ if (fenceMatch?.[1]) {
16
+ const fenced = fenceMatch[1].trim();
17
+ try {
18
+ return JSON.parse(fenced);
19
+ } catch {
20
+ }
21
+ }
22
+ for (let i = 0; i < raw.length; i++) {
23
+ if (raw[i] !== "{") continue;
24
+ let depth = 0;
25
+ let inString = false;
26
+ let escape = false;
27
+ for (let j = i; j < raw.length; j++) {
28
+ const ch = raw[j];
29
+ if (escape) {
30
+ escape = false;
31
+ continue;
32
+ }
33
+ if (ch === "\\" && inString) {
34
+ escape = true;
35
+ continue;
36
+ }
37
+ if (ch === '"') {
38
+ inString = !inString;
39
+ continue;
40
+ }
41
+ if (inString) continue;
42
+ if (ch === "{") depth++;
43
+ if (ch === "}") depth--;
44
+ if (depth === 0) {
45
+ const candidate = raw.slice(i, j + 1);
46
+ try {
47
+ return JSON.parse(candidate);
48
+ } catch {
49
+ break;
50
+ }
51
+ }
52
+ }
53
+ }
54
+ throw new Error(`Invalid JSON in LLM output: ${raw.slice(0, 120)}`);
55
+ }
56
+
57
+ // src/lib/stuck-detector.ts
58
+ function shouldCheckStuckDetection(config, iteration) {
59
+ if (!config.enabled) return false;
60
+ const firstCheck = config.firstCheck ?? 3;
61
+ const interval = config.interval ?? 2;
62
+ if (interval <= 0) return iteration === firstCheck;
63
+ if (iteration < firstCheck) return false;
64
+ if (iteration === firstCheck) return true;
65
+ return (iteration - firstCheck) % interval === 0;
66
+ }
67
+ function composeStuckDetectionPrompt(input) {
68
+ const commentLines = input.recentComments.length > 0 ? input.recentComments.map((c) => ` [${c.author}] ${c.body.slice(0, 200)}`).join("\n") : " (no comments)";
69
+ return `You are a pipeline trajectory classifier. Analyze recent agent activity and classify the trajectory.
70
+
71
+ Ticket: #${String(input.ticketNumber)} "${input.ticketTitle}"
72
+ Column: ${input.columnName}
73
+ Iteration: ${String(input.iteration)} of ${String(input.maxIterations)}
74
+
75
+ Recent iteration comments:
76
+ ${commentLines}
77
+
78
+ Classify as one of:
79
+ - "progressing" \u2014 each iteration makes distinct forward progress (new code, new tests, new fields set)
80
+ - "spinning" \u2014 agent is active but repeating similar actions without advancing (same error, same approach retried, output duplicated)
81
+ - "blocked" \u2014 agent cannot make progress due to external dependency, missing information, or fundamental task issue
82
+
83
+ Respond with ONLY a JSON object:
84
+ {"status": "progressing|spinning|blocked", "confidence": 0.0-1.0, "evidence": "one sentence"}`;
85
+ }
86
+ var StuckDetectionResponseSchema = z.object({
87
+ status: z.enum(["progressing", "spinning", "blocked"]),
88
+ confidence: z.number().min(0).max(1),
89
+ evidence: z.string()
90
+ });
91
+ function parseStuckDetectionResponse(raw) {
92
+ let parsed;
93
+ try {
94
+ parsed = parseJsonFromLlmOutput(raw);
95
+ } catch (err) {
96
+ throw new Error(`StuckDetectionResponse: ${err instanceof Error ? err.message : String(err)}`);
97
+ }
98
+ if (parsed && typeof parsed === "object" && "confidence" in parsed) {
99
+ const p = parsed;
100
+ if (typeof p.confidence === "number") {
101
+ p.confidence = Math.min(1, Math.max(0, p.confidence));
102
+ }
103
+ }
104
+ const result = StuckDetectionResponseSchema.safeParse(parsed);
105
+ if (!result.success) {
106
+ throw new Error(`StuckDetectionResponse: validation failed \u2014 ${result.error.message}`);
107
+ }
108
+ return result.data;
109
+ }
110
+ function classifyTrajectory(snapshots) {
111
+ if (snapshots.length === 0) {
112
+ return { status: "progressing", evidence: "no data", confidence: 0.5 };
113
+ }
114
+ const recent = snapshots.slice(-3);
115
+ const deltas = recent.map((s) => s.delta_from_previous);
116
+ const meaningful = deltas.filter((d) => d !== "first_check");
117
+ if (meaningful.length === 0) {
118
+ return { status: "progressing", evidence: "only initial checks", confidence: 0.5 };
119
+ }
120
+ if (meaningful.length < 2) {
121
+ return { status: "progressing", evidence: "insufficient data for trajectory", confidence: 0.3 };
122
+ }
123
+ if (meaningful.every((d) => d === "same")) {
124
+ return { status: "spinning", evidence: "identical gate results", confidence: 1 };
125
+ }
126
+ if (meaningful.every((d) => d === "regressed" || d === "same")) {
127
+ return { status: "regressing", evidence: "gate results degrading", confidence: 1 };
128
+ }
129
+ if (meaningful.some((d) => d === "improved")) {
130
+ const lastMeaningful = meaningful[meaningful.length - 1];
131
+ if (lastMeaningful === "improved") {
132
+ return { status: "progressing", evidence: "gate results improving", confidence: 1 };
133
+ }
134
+ }
135
+ if (meaningful.some((d) => d === "improved") && meaningful.some((d) => d === "regressed")) {
136
+ return { status: "spinning", evidence: "oscillating gate results", confidence: 1 };
137
+ }
138
+ if (meaningful.some((d) => d === "improved")) {
139
+ return { status: "spinning", evidence: "stale improvement \u2014 no recent progress", confidence: 0.8 };
140
+ }
141
+ return { status: "spinning", evidence: "no improvement detected", confidence: 1 };
142
+ }
143
+
144
+ // src/lib/prompt-composer.ts
145
+ var PROMPT_BUDGETS = {
146
+ system_preamble: 800,
147
+ gate_results: 500,
148
+ column_prompt: Infinity,
149
+ // NEVER truncated
150
+ lookahead: 1e3,
151
+ run_memory: 1e3,
152
+ rejection: 500,
153
+ ticket_details: 1500,
154
+ comments: 2e3,
155
+ transition_rules: 500,
156
+ transition_history: 1e3,
157
+ dependency_requirements: 500,
158
+ linked_documents: 2e3,
159
+ metadata: 200
160
+ };
161
+ function estimateTokens(text) {
162
+ return Math.ceil(text.length / 4);
163
+ }
164
+ function truncateToTokens(text, maxTokens) {
165
+ if (maxTokens === Infinity) return text;
166
+ const maxChars = maxTokens * 4;
167
+ if (text.length <= maxChars) return text;
168
+ return text.slice(0, maxChars) + "\n[...truncated]";
169
+ }
170
+ function windowComments(comments, maxTokens) {
171
+ const pinned = comments.filter((c) => c.pinned);
172
+ const unpinned = comments.filter((c) => !c.pinned);
173
+ const recentFull = unpinned.slice(-3);
174
+ const older = unpinned.slice(0, -3);
175
+ const parts = [];
176
+ for (const c of pinned) {
177
+ parts.push(`[pinned] **${c.author}** (${c.created_at}):
178
+ ${c.body}
179
+ `);
180
+ }
181
+ for (const c of recentFull) {
182
+ parts.push(`**${c.author}** (${c.created_at}):
183
+ ${c.body}
184
+ `);
185
+ }
186
+ for (const c of older) {
187
+ const firstLine = c.body.split("\n")[0]?.slice(0, 100) ?? "";
188
+ parts.push(`- ${c.author}: ${firstLine}`);
189
+ }
190
+ const joined = parts.join("\n");
191
+ return truncateToTokens(joined, maxTokens);
192
+ }
193
+ function composePrompt(columnContext, ticketContext, meta) {
194
+ const parts = [];
195
+ if (!columnContext.prompt_document?.content) {
196
+ throw new Error(
197
+ `Column "${columnContext.column.name}" has no prompt document. Configure a prompt document for this column before running the pipeline.`
198
+ );
199
+ }
200
+ const hasUnresolvedBlockers = ticketContext.ticket_links.some(
201
+ (l) => l.direction === "inward" && l.link_type === "blocks" && !l.resolved
202
+ );
203
+ parts.push(`# Pipeline Agent Instructions
204
+
205
+ You are a pipeline automation agent processing ticket #${String(ticketContext.ticket.ticket_number)}: "${ticketContext.ticket.title}".
206
+
207
+ ## Your Goal
208
+ Advance this ticket through the pipeline. Your success criteria:
209
+ 1. Complete the work described in the ticket and column prompt below
210
+ 2. Move the ticket to the next column using \`${ticketContext.tool_prefix}move_ticket\`
211
+ 3. Or mark it complete using \`${ticketContext.tool_prefix}complete_task\`
212
+
213
+ ## Iteration ${meta.iteration} of ${meta.maxIterations}
214
+ ${meta.iteration >= meta.maxIterations - 1 ? "**FINAL ITERATIONS** \u2014 prioritize moving the ticket NOW or it will be marked stalled." : `You have ${meta.maxIterations - meta.iteration} iterations remaining. Make meaningful progress each iteration.`}
215
+ ${meta.gutterCount > 0 ? `
216
+ ## Progress Warning
217
+ No meaningful progress detected for ${meta.gutterCount} consecutive iteration(s).
218
+ ${meta.gutterThreshold - meta.gutterCount} iteration(s) remain before this loop is terminated as stalled.
219
+
220
+ You MUST change approach. What you've been doing is not working. Consider:
221
+ - Breaking the problem into smaller steps
222
+ - Setting a field value to record partial progress
223
+ - Creating a comment explaining what's blocking you
224
+ - Asking for help via a signal
225
+ ` : ""}
226
+ ## Available Tools (prefix: ${ticketContext.tool_prefix})
227
+ - **${ticketContext.tool_prefix}check_transition** \u2014 ALWAYS call this before moving. Returns allowed/blocked with recovery steps.
228
+ Params: \`{ projectId, boardId, ticketId, targetColumnId }\`
229
+ - **${ticketContext.tool_prefix}move_ticket** \u2014 Move ticket to a new column. Include handoff data for the next agent.
230
+ Params: \`{ projectId, ticketId, column_id, handoff: { branch?, commit_sha?, build_status?, notes? } }\`
231
+ - **${ticketContext.tool_prefix}complete_task** \u2014 Move to a done column with handoff data.
232
+ Params: \`{ projectId, ticketId, handoff: { ... } }\`
233
+ - **${ticketContext.tool_prefix}set_field_value** \u2014 Set a required field before moving (check transition rules).
234
+ Params: \`{ projectId, ticketId, fieldId, value }\`
235
+ - **${ticketContext.tool_prefix}create_signal** \u2014 Leave knowledge for future agents at any scope.
236
+ - **${ticketContext.tool_prefix}create_comment** \u2014 Add progress notes to the ticket.
237
+ - **${ticketContext.tool_prefix}append_run_memory** \u2014 Share discoveries with future agents.
238
+ Params: \`{ section: 'conventions'|'interfaces'|'failures'|'decisions', content: string }\`
239
+
240
+ ## Knowledge Sharing
241
+ Before moving the ticket to the next column, you MUST:
242
+ 1. **Write a signal** via \`${ticketContext.tool_prefix}create_signal\` summarizing the key outcome of your work:
243
+ - What you discovered, built, validated, or decided
244
+ - Scope it to the ticket (\`ticketId\`) so future agents on this ticket see it
245
+ - One concise signal is better than none \u2014 don't skip this step
246
+ 2. **Contribute to run memory** via \`${ticketContext.tool_prefix}append_run_memory\` if you discovered:
247
+ - Conventions or patterns others should follow (\`section: 'conventions'\`)
248
+ - Interfaces or APIs consumed or created (\`section: 'interfaces'\`)
249
+ - Failures or dead-ends to avoid (\`section: 'failures'\`)
250
+ - Design decisions and their rationale (\`section: 'decisions'\`)
251
+
252
+ Signals and run memory are how you communicate with future agents. Without them, the next agent starts blind.
253
+
254
+ ## Iteration Summary
255
+ If you made meaningful progress this iteration, create a comment using \`${ticketContext.tool_prefix}create_comment\` summarizing:
256
+ - What you accomplished
257
+ - What remains to be done
258
+ - Any blockers or issues encountered
259
+
260
+ ## Critical Rules
261
+ 1. **Always call ${ticketContext.tool_prefix}check_transition before ${ticketContext.tool_prefix}move_ticket** \u2014 moves that violate workflow rules will fail.
262
+ 2. **If the ticket has UNRESOLVED blockers, do not attempt to move it.** Create a comment explaining you are waiting, then stop.
263
+ 3. **Include handoff data** when moving \u2014 the next agent needs context (branch name, build status, etc.).
264
+ 4. **If you cannot make progress**, create a comment explaining why and stop. Do not burn iterations.
265
+ ${hasUnresolvedBlockers ? "\n**WARNING: This ticket has UNRESOLVED blockers. Do NOT attempt to move it. Document your status and stop.**\n" : ""}
266
+ ---
267
+ `);
268
+ const worktreeConfig = columnContext.agent_config?.worktree;
269
+ if (worktreeConfig?.enabled) {
270
+ const integrationBranch = worktreeConfig.integration_branch ?? "main";
271
+ if (!VALID_BRANCH_RE.test(integrationBranch)) {
272
+ parts.push(`## Git Worktree
273
+
274
+ You are working in an isolated git worktree. Integration branch name is invalid \u2014 contact the pipeline operator.`);
275
+ } else {
276
+ parts.push(`## Git Worktree
277
+
278
+ You are working in an isolated git worktree. Before starting any code changes:
279
+ 1. Merge \`${integrationBranch}\` into your current branch: \`git merge '${integrationBranch}'\`
280
+ 2. Resolve any merge conflicts before proceeding with ticket work
281
+ 3. Never rebase \u2014 always merge (rebase destroys traceability across the pipeline)
282
+
283
+ After completing your work, commit all changes to your worktree branch.
284
+ `);
285
+ }
286
+ }
287
+ if (ticketContext.signals.length > 0) {
288
+ parts.push(`
289
+ ## Signals (Guardrails)
290
+ `);
291
+ for (const s of ticketContext.signals) {
292
+ parts.push(`- ${s}`);
293
+ }
294
+ }
295
+ if (meta.gateResults && meta.gateResults.length > 0) {
296
+ parts.push(`
297
+ ## Previous Gate Results
298
+ `);
299
+ for (const r of meta.gateResults) {
300
+ const status = r.passed ? "PASS" : "FAIL";
301
+ const req = r.required ? "(required)" : "(advisory)";
302
+ parts.push(`- **${r.name}** ${status} ${req} [${String(r.duration_ms)}ms]`);
303
+ if (!r.passed && r.output) {
304
+ parts.push(` \`\`\`
305
+ ${truncateToTokens(r.output, 200)}
306
+ \`\`\``);
307
+ }
308
+ }
309
+ }
310
+ if (meta.rejectionFindings) {
311
+ parts.push(`
312
+ ## Previous Rejection (fix these before resubmitting)
313
+ `);
314
+ parts.push(truncateToTokens(meta.rejectionFindings, PROMPT_BUDGETS.rejection));
315
+ } else {
316
+ const rejectionComment = ticketContext.comments.slice().reverse().find((c) => c.body.startsWith("QA REJECTION:") || c.body.startsWith("REJECTION:"));
317
+ if (rejectionComment) {
318
+ parts.push(`
319
+ ## Previous Rejection (fix these before resubmitting)
320
+ `);
321
+ parts.push(truncateToTokens(rejectionComment.body, PROMPT_BUDGETS.rejection));
322
+ }
323
+ }
324
+ if (columnContext.prompt_document?.content) {
325
+ parts.push(columnContext.prompt_document.content);
326
+ }
327
+ if (meta.lookaheadDocument?.content) {
328
+ parts.push(`
329
+ ## Downstream Criteria (build to pass these)
330
+ `);
331
+ parts.push(`*From: ${meta.lookaheadDocument.title}*
332
+ `);
333
+ parts.push(truncateToTokens(meta.lookaheadDocument.content, PROMPT_BUDGETS.lookahead));
334
+ }
335
+ if (meta.runMemoryContent) {
336
+ parts.push(`
337
+ ## Run Memory
338
+ `);
339
+ parts.push(truncateToTokens(meta.runMemoryContent, PROMPT_BUDGETS.run_memory));
340
+ }
341
+ const ticketParts = [];
342
+ ticketParts.push(`## Current Ticket
343
+ `);
344
+ ticketParts.push(`**Title:** ${ticketContext.ticket.title}`);
345
+ ticketParts.push(`**Ticket ID:** ${ticketContext.ticket.id}`);
346
+ ticketParts.push(`**Ticket Number:** ${String(ticketContext.ticket.ticket_number)}`);
347
+ if (ticketContext.ticket.description) {
348
+ ticketParts.push(`
349
+ ${ticketContext.ticket.description}`);
350
+ }
351
+ if (ticketContext.ticket.assignee) {
352
+ ticketParts.push(`**Assignee:** ${ticketContext.ticket.assignee.name}`);
353
+ }
354
+ if (ticketContext.ticket.column) {
355
+ ticketParts.push(`**Current Column:** ${ticketContext.ticket.column.name} (${ticketContext.ticket.column.type})`);
356
+ }
357
+ if (ticketContext.ticket.backward_transitions > 0) {
358
+ ticketParts.push(`**Backward Transitions:** ${String(ticketContext.ticket.backward_transitions)}`);
359
+ }
360
+ if (ticketContext.field_values.length > 0) {
361
+ ticketParts.push(`
362
+ ## Field Values
363
+ `);
364
+ for (const fv of ticketContext.field_values) {
365
+ ticketParts.push(`- **${fv.field_name}:** ${fv.value !== null ? JSON.stringify(fv.value) : "(not set)"}`);
366
+ }
367
+ }
368
+ if (ticketContext.parent) {
369
+ ticketParts.push(`
370
+ ## Parent Ticket
371
+ `);
372
+ ticketParts.push(`- #${String(ticketContext.parent.ticket_number)}: ${ticketContext.parent.title}`);
373
+ }
374
+ if (ticketContext.children.length > 0) {
375
+ ticketParts.push(`
376
+ ## Child Tickets
377
+ `);
378
+ for (const child of ticketContext.children) {
379
+ ticketParts.push(`- #${String(child.ticket_number)}: ${child.title}${child.column_name ? ` (${child.column_name})` : ""}`);
380
+ }
381
+ }
382
+ if (ticketContext.transitions.length > 0) {
383
+ ticketParts.push(`
384
+ ## Transition History
385
+ `);
386
+ let transitionTokens = 0;
387
+ for (const t of ticketContext.transitions) {
388
+ let line = `- ${t.from ?? "Backlog"} \u2192 ${t.to}`;
389
+ if (t.handoff) line += ` (handoff: ${JSON.stringify(t.handoff)})`;
390
+ const lineTokens = estimateTokens(line);
391
+ if (transitionTokens + lineTokens > PROMPT_BUDGETS.transition_history) {
392
+ ticketParts.push(`- [...truncated \u2014 ${String(ticketContext.transitions.length)} total transitions]`);
393
+ break;
394
+ }
395
+ ticketParts.push(line);
396
+ transitionTokens += lineTokens;
397
+ }
398
+ }
399
+ if (ticketContext.ticket_links.length > 0) {
400
+ ticketParts.push(`
401
+ ## Ticket Links
402
+ `);
403
+ const blockers = ticketContext.ticket_links.filter((l) => l.direction === "inward" && l.link_type === "blocks");
404
+ const blocking = ticketContext.ticket_links.filter((l) => l.direction === "outward" && l.link_type === "blocks");
405
+ const related = ticketContext.ticket_links.filter((l) => l.link_type === "relates_to");
406
+ if (blockers.length > 0) {
407
+ ticketParts.push(`**Blocked by:**`);
408
+ for (const l of blockers) {
409
+ const status = l.resolved ? "(resolved)" : "UNRESOLVED";
410
+ ticketParts.push(`- #${String(l.ticket_number)}: ${l.title} [${l.column_name ?? "backlog"}] ${status}`);
411
+ }
412
+ }
413
+ if (blocking.length > 0) {
414
+ ticketParts.push(`**Blocks:**`);
415
+ for (const l of blocking) {
416
+ ticketParts.push(`- #${String(l.ticket_number)}: ${l.title} [${l.column_name ?? "backlog"}]`);
417
+ }
418
+ }
419
+ if (related.length > 0) {
420
+ ticketParts.push(`**Related:**`);
421
+ for (const l of related) {
422
+ ticketParts.push(`- #${String(l.ticket_number)}: ${l.title}`);
423
+ }
424
+ }
425
+ }
426
+ parts.push(truncateToTokens(ticketParts.join("\n"), PROMPT_BUDGETS.ticket_details));
427
+ if (ticketContext.comments.length > 0) {
428
+ parts.push(`
429
+ ## Comments
430
+ `);
431
+ parts.push(windowComments(ticketContext.comments, PROMPT_BUDGETS.comments));
432
+ }
433
+ if (ticketContext.transition_rules) {
434
+ parts.push(`
435
+ ## Transition Rules
436
+ `);
437
+ parts.push(truncateToTokens(ticketContext.transition_rules, PROMPT_BUDGETS.transition_rules));
438
+ }
439
+ if (ticketContext.dependency_requirements && ticketContext.dependency_requirements !== "No dependency or field requirements configured.") {
440
+ parts.push(`
441
+ ## Dependency & Field Requirements
442
+ `);
443
+ parts.push(truncateToTokens(ticketContext.dependency_requirements, PROMPT_BUDGETS.dependency_requirements));
444
+ }
445
+ if (ticketContext.linked_documents.length > 0) {
446
+ parts.push(`
447
+ ## Linked Documents
448
+ `);
449
+ let remainingTokens = PROMPT_BUDGETS.linked_documents;
450
+ for (const doc of ticketContext.linked_documents) {
451
+ if (doc.content) {
452
+ const docHeader = `### ${doc.title}
453
+ `;
454
+ const headerTokens = estimateTokens(docHeader);
455
+ const contentTokens = estimateTokens(doc.content);
456
+ if (headerTokens + contentTokens <= remainingTokens) {
457
+ parts.push(docHeader);
458
+ parts.push(doc.content);
459
+ remainingTokens -= headerTokens + contentTokens;
460
+ } else if (remainingTokens > headerTokens + 50) {
461
+ parts.push(docHeader);
462
+ parts.push(truncateToTokens(doc.content, remainingTokens - headerTokens));
463
+ remainingTokens = 0;
464
+ } else {
465
+ parts.push(`- ${doc.title} (truncated \u2014 token budget exceeded)`);
466
+ remainingTokens = 0;
467
+ }
468
+ } else if (doc.truncated) {
469
+ parts.push(`- ${doc.title} (document too large \u2014 read via kantban_get_document)`);
470
+ }
471
+ if (remainingTokens <= 0) break;
472
+ }
473
+ }
474
+ parts.push(`
475
+ ## Pipeline Metadata
476
+ `);
477
+ parts.push(`- Iteration: ${meta.iteration} / ${meta.maxIterations}`);
478
+ parts.push(`- Project ID: ${meta.projectId}`);
479
+ parts.push(`- Tool Prefix: ${ticketContext.tool_prefix}`);
480
+ parts.push(`- Column: ${columnContext.column.name}`);
481
+ parts.push(`- Goal: ${columnContext.column.goal ?? "No goal set"}`);
482
+ return parts.join("\n");
483
+ }
484
+
485
+ // src/lib/ralph-loop.ts
486
+ var API_TIMEOUT_MS = 3e4;
487
+ function withTimeout(promise, ms, label) {
488
+ let timer;
489
+ return Promise.race([
490
+ promise.finally(() => clearTimeout(timer)),
491
+ new Promise((_, reject) => {
492
+ timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
493
+ })
494
+ ]);
495
+ }
496
+ var RalphLoop = class {
497
+ ticketId;
498
+ columnId;
499
+ config;
500
+ deps;
501
+ stopped = false;
502
+ currentIteration = 0;
503
+ constructor(ticketId, columnId, config, deps) {
504
+ this.ticketId = ticketId;
505
+ this.columnId = columnId;
506
+ this.config = config;
507
+ this.deps = deps;
508
+ }
509
+ stop() {
510
+ this.stopped = true;
511
+ }
512
+ get iteration() {
513
+ return this.currentIteration;
514
+ }
515
+ looksLike404(err) {
516
+ const statusCode = err?.statusCode ?? err?.status;
517
+ if (statusCode === 404) return true;
518
+ const message = err instanceof Error ? err.message : String(err);
519
+ return message.includes("API error 404");
520
+ }
521
+ async run() {
522
+ let gutterCount = this.config.startGutterCount ?? 0;
523
+ let lastFingerprint = this.config.startFingerprint ?? null;
524
+ const log = this.deps.log ?? (() => {
525
+ });
526
+ const resolvedModel = this.config.model ?? "default";
527
+ const gateSnapshots = [];
528
+ let cumulativeTokensIn = 0;
529
+ let cumulativeTokensOut = 0;
530
+ let cumulativeToolCalls = 0;
531
+ let cumulativeDurationMs = 0;
532
+ const withCosts = (r) => ({
533
+ ...r,
534
+ tokensIn: cumulativeTokensIn,
535
+ tokensOut: cumulativeTokensOut,
536
+ toolCallCount: cumulativeToolCalls,
537
+ durationMs: cumulativeDurationMs
538
+ });
539
+ let lastOutput = "";
540
+ const startIter = this.config.startIteration ?? 1;
541
+ for (let i = startIter; i <= this.config.maxIterations; i++) {
542
+ if (this.stopped) return withCosts({ reason: "stopped", iterations: i - 1, gutterCount, model: resolvedModel });
543
+ if (this.config.isBudgetExhausted?.()) {
544
+ log(`Budget exhausted \u2014 stopping after ${i - 1} iterations`);
545
+ return withCosts({ reason: "budget", iterations: i - 1, gutterCount, model: resolvedModel });
546
+ }
547
+ this.currentIteration = i;
548
+ log(`Iteration ${i}/${this.config.maxIterations} starting`);
549
+ if (!lastFingerprint) {
550
+ try {
551
+ lastFingerprint = await withTimeout(
552
+ this.deps.fetchFingerprint(this.ticketId),
553
+ API_TIMEOUT_MS,
554
+ "fetchFingerprint (baseline)"
555
+ );
556
+ } catch (err) {
557
+ const message = err instanceof Error ? err.message : String(err);
558
+ log(`Baseline fingerprint fetch failed: ${message}`);
559
+ if (this.looksLike404(err)) {
560
+ return withCosts({ reason: "deleted", iterations: i, gutterCount, model: resolvedModel });
561
+ }
562
+ return withCosts({ reason: "error", iterations: i, gutterCount, lastError: `Baseline fingerprint failed: ${message}`, model: resolvedModel });
563
+ }
564
+ }
565
+ let ticketCtx;
566
+ let columnCtx;
567
+ try {
568
+ [ticketCtx, columnCtx] = await Promise.all([
569
+ withTimeout(this.deps.fetchTicketContext(this.ticketId), API_TIMEOUT_MS, "fetchTicketContext"),
570
+ withTimeout(this.deps.fetchColumnContext(this.columnId), API_TIMEOUT_MS, "fetchColumnContext")
571
+ ]);
572
+ } catch (err) {
573
+ const message = err instanceof Error ? err.message : String(err);
574
+ log(`Context fetch failed: ${message}`);
575
+ if (this.looksLike404(err)) {
576
+ return withCosts({ reason: "deleted", iterations: i, gutterCount, model: resolvedModel });
577
+ }
578
+ return withCosts({ reason: "error", iterations: i, gutterCount, lastError: `Context fetch failed: ${message}`, model: resolvedModel });
579
+ }
580
+ if (ticketCtx.ticket.column && ticketCtx.ticket.column.id !== this.columnId) {
581
+ log(`Ticket already in column ${ticketCtx.ticket.column.name}, not ${this.columnId} \u2014 exiting as moved`);
582
+ return withCosts({ reason: "moved", iterations: i - 1, gutterCount, model: resolvedModel });
583
+ }
584
+ const runMemoryContent = this.deps.fetchRunMemoryContent ? await this.deps.fetchRunMemoryContent().catch(() => "") : void 0;
585
+ const lookaheadDocument = this.deps.fetchLookaheadDocument ? await this.deps.fetchLookaheadDocument().catch(() => void 0) : void 0;
586
+ let prompt;
587
+ try {
588
+ prompt = composePrompt(columnCtx, ticketCtx, {
589
+ iteration: i,
590
+ maxIterations: this.config.maxIterations,
591
+ projectId: this.deps.projectId,
592
+ gutterCount,
593
+ gutterThreshold: this.config.gutterThreshold,
594
+ runMemoryContent: runMemoryContent || void 0,
595
+ lookaheadDocument
596
+ });
597
+ } catch (err) {
598
+ const message = err instanceof Error ? err.message : String(err);
599
+ log(`Prompt composition failed: ${message}`);
600
+ return withCosts({ reason: "error", iterations: i, gutterCount, lastError: `Prompt composition failed: ${message}`, model: resolvedModel });
601
+ }
602
+ if (this.stopped) return withCosts({ reason: "stopped", iterations: i - 1, gutterCount, model: resolvedModel });
603
+ log(`Invoking ${this.deps.provider.displayName} (model=${this.config.model ?? "default"})`);
604
+ const iterationRunId = `${this.config.runId ?? this.ticketId}-iter-${String(i)}`;
605
+ this.deps.onSessionStart?.({
606
+ runId: iterationRunId,
607
+ model: this.config.model ?? "default",
608
+ iteration: i
609
+ });
610
+ const streamContext = { runId: iterationRunId, ticketId: this.ticketId, columnId: this.columnId };
611
+ const agentRequest = {
612
+ prompt,
613
+ ...this.config.model != null && { model: this.config.model },
614
+ ...this.config.maxBudgetUsd != null && { maxTurns: Math.max(1, Math.ceil(this.config.maxBudgetUsd * 10)) },
615
+ ...this.config.worktreeName != null && { workingDirectory: this.config.worktreeName },
616
+ ...this.deps.mcpConfig != null && { mcpConfig: this.deps.mcpConfig },
617
+ ...this.config.toolRestrictions != null && {
618
+ toolRestrictions: { ...this.config.toolRestrictions, includeMcpConfig: this.config.toolRestrictions.includeMcpConfig ?? true }
619
+ },
620
+ onStreamEvent: (event) => this.deps.onStreamEvent?.(event, streamContext)
621
+ };
622
+ const { exitCode, output, toolCallCount, usage, durationMs: iterationDurationMs, degradedCapabilities } = await this.deps.provider.invoke(agentRequest);
623
+ const tokensIn = usage.inputTokens;
624
+ const tokensOut = usage.outputTokens;
625
+ if (i === startIter && degradedCapabilities?.length) {
626
+ log(`Provider ${this.deps.provider.id} degraded: ${degradedCapabilities.join(", ")}`);
627
+ }
628
+ cumulativeTokensIn += tokensIn;
629
+ cumulativeTokensOut += tokensOut;
630
+ cumulativeToolCalls += toolCallCount;
631
+ cumulativeDurationMs += iterationDurationMs;
632
+ this.deps.onSessionEnd?.({
633
+ runId: iterationRunId,
634
+ exitCode,
635
+ tokensIn,
636
+ tokensOut,
637
+ toolCallCount,
638
+ durationMs: iterationDurationMs
639
+ });
640
+ if (exitCode !== 0) {
641
+ const snippet = output.slice(-200);
642
+ log(`${this.deps.provider.displayName} exited with code ${exitCode}: ${snippet}`);
643
+ return withCosts({ reason: "error", iterations: i, gutterCount, lastError: `non-zero exit code: ${exitCode}. Last output: ${snippet}`, model: resolvedModel });
644
+ }
645
+ log(`${this.deps.provider.displayName} exited successfully`);
646
+ lastOutput = output;
647
+ let afterFp;
648
+ try {
649
+ const retryDelayMs = this.config.postMoveRetryDelayMs ?? 1500;
650
+ afterFp = await withTimeout(
651
+ this.deps.fetchFingerprint(this.ticketId),
652
+ API_TIMEOUT_MS,
653
+ "fetchFingerprint (post-iteration)"
654
+ );
655
+ if (afterFp.column_id === this.columnId) {
656
+ for (let retry = 0; retry < 2; retry++) {
657
+ await new Promise((r) => setTimeout(r, retryDelayMs));
658
+ if (this.stopped) break;
659
+ try {
660
+ afterFp = await withTimeout(
661
+ this.deps.fetchFingerprint(this.ticketId),
662
+ API_TIMEOUT_MS,
663
+ "fetchFingerprint (retry)"
664
+ );
665
+ } catch (err) {
666
+ log(`Fingerprint retry ${retry + 1} failed: ${err instanceof Error ? err.message : String(err)} \u2014 using last good value`);
667
+ break;
668
+ }
669
+ if (afterFp.column_id !== this.columnId) break;
670
+ }
671
+ }
672
+ } catch (err) {
673
+ const message = err instanceof Error ? err.message : String(err);
674
+ log(`Post-iteration fingerprint failed: ${message}`);
675
+ if (this.looksLike404(err)) {
676
+ return withCosts({ reason: "deleted", iterations: i, gutterCount, model: resolvedModel });
677
+ }
678
+ return withCosts({ reason: "error", iterations: i, gutterCount, lastError: `Post-iteration fingerprint failed: ${message}`, model: resolvedModel });
679
+ }
680
+ if (afterFp.column_id !== this.columnId) {
681
+ log(`Ticket moved to column ${afterFp.column_id ?? "null"}`);
682
+ return withCosts({ reason: "moved", iterations: i, gutterCount, model: resolvedModel, output: lastOutput });
683
+ }
684
+ const gutterBeforeGates = gutterCount;
685
+ if (this.config.onPostIterationGates) {
686
+ try {
687
+ const snapshot = await this.config.onPostIterationGates(this.ticketId, i);
688
+ gateSnapshots.push(snapshot);
689
+ const fieldDelta = lastFingerprint ? afterFp.field_value_count !== lastFingerprint.field_value_count : false;
690
+ switch (snapshot.delta_from_previous) {
691
+ case "improved":
692
+ if (gutterCount > 0) log(`Gate improvement detected \u2014 gutter counter reset`);
693
+ gutterCount = 0;
694
+ break;
695
+ case "same":
696
+ if (!fieldDelta) {
697
+ gutterCount++;
698
+ log(`No gate progress (gutter ${gutterCount}/${this.config.gutterThreshold})`);
699
+ } else {
700
+ log(`Gates unchanged but fields changed \u2014 not incrementing gutter`);
701
+ }
702
+ break;
703
+ case "regressed":
704
+ gutterCount += 2;
705
+ log(`Gate regression detected (gutter ${gutterCount}/${this.config.gutterThreshold})`);
706
+ break;
707
+ case "first_check":
708
+ break;
709
+ }
710
+ } catch (err) {
711
+ const message = err instanceof Error ? err.message : String(err);
712
+ log(`Post-iteration gate check failed (non-blocking): ${message}`);
713
+ if (lastFingerprint && fingerprintsMatch(lastFingerprint, afterFp)) {
714
+ gutterCount++;
715
+ log(`No progress detected via fingerprint fallback (gutter ${gutterCount}/${this.config.gutterThreshold})`);
716
+ } else {
717
+ if (gutterCount > 0) log(`Progress detected \u2014 gutter counter reset`);
718
+ gutterCount = 0;
719
+ }
720
+ }
721
+ } else {
722
+ if (lastFingerprint && fingerprintsMatch(lastFingerprint, afterFp)) {
723
+ gutterCount++;
724
+ log(`No progress detected (gutter ${gutterCount}/${this.config.gutterThreshold})`);
725
+ } else {
726
+ if (gutterCount > 0) log(`Progress detected \u2014 gutter counter reset`);
727
+ gutterCount = 0;
728
+ }
729
+ }
730
+ lastFingerprint = afterFp;
731
+ if (this.config.stuckDetection && shouldCheckStuckDetection(this.config.stuckDetection, i)) {
732
+ try {
733
+ let sdStatus;
734
+ let sdEvidence;
735
+ let sdConfidence;
736
+ if (gateSnapshots.length > 0) {
737
+ const trajectory = classifyTrajectory(gateSnapshots);
738
+ sdStatus = trajectory.status === "regressing" ? "spinning" : trajectory.status;
739
+ sdEvidence = trajectory.evidence;
740
+ sdConfidence = trajectory.confidence;
741
+ log(`Gate-based stuck detection: ${sdStatus} (confidence=${String(sdConfidence)}) \u2014 ${sdEvidence}`);
742
+ } else if (this.config.invokeStuckDetection) {
743
+ const sdInput = {
744
+ ticketNumber: ticketCtx.ticket.ticket_number,
745
+ ticketTitle: ticketCtx.ticket.title,
746
+ columnName: columnCtx.column.name,
747
+ iteration: i,
748
+ maxIterations: this.config.maxIterations,
749
+ recentComments: ticketCtx.comments.slice(-3).map((c) => ({
750
+ author: c.author,
751
+ body: c.body
752
+ }))
753
+ };
754
+ const sdResult = await this.config.invokeStuckDetection(sdInput);
755
+ sdStatus = sdResult.status;
756
+ sdEvidence = sdResult.evidence;
757
+ sdConfidence = sdResult.confidence;
758
+ log(`Stuck detection: ${sdStatus} (confidence=${String(sdConfidence)}) \u2014 ${sdEvidence}`);
759
+ } else {
760
+ sdStatus = "progressing";
761
+ sdEvidence = "no detection method";
762
+ sdConfidence = 0;
763
+ }
764
+ switch (sdStatus) {
765
+ case "progressing":
766
+ if (gutterCount > 0) {
767
+ log(`Stuck detection override: resetting gutter counter (was ${String(gutterCount)})`);
768
+ gutterCount = 0;
769
+ }
770
+ break;
771
+ case "spinning":
772
+ gutterCount = gutterBeforeGates + 2;
773
+ log(`Stuck detection: spinning \u2014 gutter set to ${String(gutterCount)}/${String(this.config.gutterThreshold)}`);
774
+ break;
775
+ case "blocked":
776
+ log(`Stuck detection: blocked \u2014 exiting immediately`);
777
+ return withCosts({
778
+ reason: "stalled",
779
+ iterations: i,
780
+ gutterCount: this.config.gutterThreshold,
781
+ model: resolvedModel,
782
+ output: lastOutput,
783
+ ...gateSnapshots.length > 0 && { finalGateSnapshot: gateSnapshots[gateSnapshots.length - 1] }
784
+ });
785
+ }
786
+ } catch (err) {
787
+ const message = err instanceof Error ? err.message : String(err);
788
+ log(`Stuck detection failed (non-blocking): ${message}`);
789
+ }
790
+ }
791
+ if (gutterCount >= this.config.gutterThreshold) {
792
+ return withCosts({
793
+ reason: "stalled",
794
+ iterations: i,
795
+ gutterCount,
796
+ model: resolvedModel,
797
+ output: lastOutput,
798
+ ...gateSnapshots.length > 0 && { finalGateSnapshot: gateSnapshots[gateSnapshots.length - 1] }
799
+ });
800
+ }
801
+ if (this.config.onCheckpoint) {
802
+ try {
803
+ await this.config.onCheckpoint(this.ticketId, {
804
+ run_id: this.config.runId ?? "00000000-0000-0000-0000-000000000000",
805
+ column_id: this.columnId,
806
+ iteration: i,
807
+ gutter_count: gutterCount,
808
+ advisor_invocations: 0,
809
+ // tracked by orchestrator, not loop
810
+ model_tier: resolvedModel,
811
+ last_fingerprint: afterFp,
812
+ updated_at: (/* @__PURE__ */ new Date()).toISOString(),
813
+ worktree_name: this.config.worktreeName
814
+ });
815
+ } catch (err) {
816
+ log(`Checkpoint write failed (non-blocking): ${err instanceof Error ? err.message : String(err)}`);
817
+ }
818
+ }
819
+ }
820
+ return withCosts({
821
+ reason: "max_iterations",
822
+ iterations: this.config.maxIterations,
823
+ gutterCount,
824
+ model: resolvedModel,
825
+ output: lastOutput,
826
+ ...gateSnapshots.length > 0 && { finalGateSnapshot: gateSnapshots[gateSnapshots.length - 1] }
827
+ });
828
+ }
829
+ };
830
+ function fingerprintsMatch(a, b) {
831
+ return a.column_id === b.column_id && a.field_value_count === b.field_value_count && a.comment_count === b.comment_count;
832
+ }
833
+
834
+ // src/lib/mcp-config.ts
835
+ import { writeFileSync, unlinkSync, mkdirSync, existsSync, readdirSync } from "fs";
836
+ import { join, dirname } from "path";
837
+ import { fileURLToPath } from "url";
838
+ import { homedir } from "os";
839
+ var __filename = fileURLToPath(import.meta.url);
840
+ var __dirname = dirname(__filename);
841
+ function generateMcpConfig(apiUrl, apiToken, boardId) {
842
+ const localMcpPath = existsSync(join(__dirname, "..", "..", "mcp", "dist", "index.js")) ? join(__dirname, "..", "..", "mcp", "dist", "index.js") : join(__dirname, "..", "..", "..", "mcp", "dist", "index.js");
843
+ const useLocal = existsSync(localMcpPath);
844
+ const kantbanServer = useLocal ? {
845
+ command: "node",
846
+ args: [localMcpPath],
847
+ env: {
848
+ KANTBAN_API_TOKEN: apiToken,
849
+ KANTBAN_API_URL: apiUrl
850
+ }
851
+ } : {
852
+ command: "npx",
853
+ args: ["-y", "kantban-mcp@latest"],
854
+ env: {
855
+ KANTBAN_API_TOKEN: apiToken,
856
+ KANTBAN_API_URL: apiUrl
857
+ }
858
+ };
859
+ const config = {
860
+ mcpServers: {
861
+ kantban: kantbanServer
862
+ }
863
+ };
864
+ const dir = join(homedir(), ".kantban", "pipelines", boardId);
865
+ mkdirSync(dir, { recursive: true, mode: 448 });
866
+ const filePath = join(dir, "mcp-config.json");
867
+ writeFileSync(filePath, JSON.stringify(config, null, 2), { mode: 384 });
868
+ return filePath;
869
+ }
870
+ function cleanupMcpConfig(filePath) {
871
+ try {
872
+ if (existsSync(filePath)) {
873
+ unlinkSync(filePath);
874
+ }
875
+ } catch {
876
+ }
877
+ }
878
+ function generateGateProxyMcpConfig(apiUrl, apiToken, boardId, gateConfigPath, columnId, columnName, projectId, gateCwd, ticketId) {
879
+ const localMcpPath = existsSync(join(__dirname, "..", "..", "mcp", "dist", "index.js")) ? join(__dirname, "..", "..", "mcp", "dist", "index.js") : join(__dirname, "..", "..", "..", "mcp", "dist", "index.js");
880
+ const useLocal = existsSync(localMcpPath);
881
+ const kantbanServer = useLocal ? { command: "node", args: [localMcpPath], env: { KANTBAN_API_TOKEN: apiToken, KANTBAN_API_URL: apiUrl, KANTBAN_HIDDEN_TOOLS: "kantban_move_ticket,kantban_move_tickets,kantban_complete_task,kantban_move_to_board" } } : { command: "npx", args: ["-y", "kantban-mcp@latest"], env: { KANTBAN_API_TOKEN: apiToken, KANTBAN_API_URL: apiUrl, KANTBAN_HIDDEN_TOOLS: "kantban_move_ticket,kantban_move_tickets,kantban_complete_task,kantban_move_to_board" } };
882
+ const gateProxyPath = existsSync(join(__dirname, "lib", "gate-proxy-server.js")) ? join(__dirname, "lib", "gate-proxy-server.js") : join(__dirname, "gate-proxy-server.js");
883
+ const gateProxyEnv = {
884
+ GATE_CONFIG_PATH: gateConfigPath,
885
+ COLUMN_ID: columnId,
886
+ COLUMN_NAME: columnName,
887
+ PROJECT_ID: projectId,
888
+ KANTBAN_API_TOKEN: apiToken,
889
+ KANTBAN_API_URL: apiUrl
890
+ };
891
+ if (gateCwd) gateProxyEnv["GATE_CWD"] = gateCwd;
892
+ const gateProxyServer = {
893
+ command: "node",
894
+ args: [gateProxyPath],
895
+ env: gateProxyEnv
896
+ };
897
+ const config = {
898
+ mcpServers: {
899
+ kantban: kantbanServer,
900
+ "kantban-gates": gateProxyServer
901
+ }
902
+ };
903
+ const dir = join(homedir(), ".kantban", "pipelines", boardId);
904
+ mkdirSync(dir, { recursive: true, mode: 448 });
905
+ const suffix = ticketId ? `${columnId}-${ticketId}` : columnId;
906
+ const filePath = join(dir, `mcp-config-${suffix}.json`);
907
+ writeFileSync(filePath, JSON.stringify(config, null, 2), { mode: 384 });
908
+ return filePath;
909
+ }
910
+ function cleanupGateProxyConfigs(pipelineDir) {
911
+ try {
912
+ const files = readdirSync(pipelineDir);
913
+ for (const f of files) {
914
+ if (f.startsWith("mcp-config-") && f.endsWith(".json")) {
915
+ try {
916
+ unlinkSync(join(pipelineDir, f));
917
+ } catch {
918
+ }
919
+ }
920
+ }
921
+ } catch {
922
+ }
923
+ }
924
+
925
+ // src/providers/claude-provider.ts
926
+ import { spawn } from "child_process";
927
+ import { writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
928
+ import { join as join2 } from "path";
929
+ import { homedir as homedir2 } from "os";
930
+
931
+ // src/providers/claude-stream-parser.ts
932
+ var ClaudeStreamParser = class {
933
+ buffer = "";
934
+ toolCallCount = 0;
935
+ inputTokens = 0;
936
+ outputTokens = 0;
937
+ lastOutput = "";
938
+ onEvent = () => {
939
+ };
940
+ onError = () => {
941
+ };
942
+ feed(chunk) {
943
+ this.buffer += chunk;
944
+ const lines = this.buffer.split("\n");
945
+ this.buffer = lines.pop() ?? "";
946
+ for (const line of lines) {
947
+ this.parseLine(line.trim());
948
+ }
949
+ }
950
+ flush() {
951
+ const trimmed = this.buffer.trim();
952
+ this.buffer = "";
953
+ if (trimmed) this.parseLine(trimmed);
954
+ }
955
+ getToolCallCount() {
956
+ return this.toolCallCount;
957
+ }
958
+ getUsage() {
959
+ return { inputTokens: this.inputTokens, outputTokens: this.outputTokens };
960
+ }
961
+ getLastOutput() {
962
+ return this.lastOutput;
963
+ }
964
+ reset() {
965
+ this.buffer = "";
966
+ this.toolCallCount = 0;
967
+ this.inputTokens = 0;
968
+ this.outputTokens = 0;
969
+ this.lastOutput = "";
970
+ }
971
+ parseLine(line) {
972
+ if (!line) return;
973
+ try {
974
+ const raw = JSON.parse(line);
975
+ this.translateEvent(raw);
976
+ } catch {
977
+ this.onError(new Error(`Failed to parse stream-json line: ${line.slice(0, 100)}`));
978
+ }
979
+ }
980
+ translateEvent(raw) {
981
+ const message = raw.message;
982
+ const content = message?.content ?? raw.content;
983
+ if (raw.type === "assistant" && Array.isArray(content)) {
984
+ for (const block of content) {
985
+ if (block.type === "text" && typeof block.text === "string") {
986
+ this.onEvent({ type: "text", text: block.text });
987
+ } else if (block.type === "tool_use") {
988
+ this.toolCallCount++;
989
+ this.onEvent({
990
+ type: "tool_call",
991
+ tool: block.name ?? "unknown",
992
+ input: block.input
993
+ });
994
+ } else if (block.type === "tool_result") {
995
+ this.onEvent({
996
+ type: "tool_result",
997
+ tool: block.name ?? "unknown",
998
+ output: block.content ?? block.output
999
+ });
1000
+ }
1001
+ }
1002
+ } else if (raw.type === "result") {
1003
+ const usage = raw.usage;
1004
+ const inTok = usage?.input_tokens ?? 0;
1005
+ const outTok = usage?.output_tokens ?? 0;
1006
+ this.inputTokens += inTok;
1007
+ this.outputTokens += outTok;
1008
+ if (inTok || outTok) {
1009
+ this.onEvent({ type: "usage", inputTokens: inTok, outputTokens: outTok });
1010
+ }
1011
+ if (typeof raw.result === "string") {
1012
+ this.lastOutput = raw.result;
1013
+ this.onEvent({ type: "done", result: raw.result });
1014
+ }
1015
+ }
1016
+ }
1017
+ };
1018
+
1019
+ // src/providers/claude-provider.ts
1020
+ var CLAUDE_TIMEOUT_MS = 60 * 60 * 1e3;
1021
+ var ClaudeProvider = class {
1022
+ id = "claude";
1023
+ displayName = "Claude Code";
1024
+ capabilities() {
1025
+ return {
1026
+ supportsToolAllowlist: true,
1027
+ supportsToolDenylist: true,
1028
+ supportsBuiltinToolStripping: true,
1029
+ supportsMaxTurns: true,
1030
+ supportsMcpConfigInjection: true,
1031
+ supportsMcpConfigOverride: false,
1032
+ supportsWorktreeFlag: true,
1033
+ supportsSandboxModes: false,
1034
+ supportedModels: [
1035
+ { id: "claude-haiku-4-5-20251001", displayName: "Haiku 4.5", tier: "fast" },
1036
+ { id: "claude-sonnet-4-6", displayName: "Sonnet 4.6", tier: "default" },
1037
+ { id: "claude-opus-4-6", displayName: "Opus 4.6", tier: "thorough" }
1038
+ ],
1039
+ streamFormat: "stream-json"
1040
+ };
1041
+ }
1042
+ async invoke(request) {
1043
+ const args = this.buildArgs(request);
1044
+ const startTime = Date.now();
1045
+ return new Promise((resolve) => {
1046
+ const child = spawn("claude", args, {
1047
+ stdio: ["pipe", "pipe", "pipe"],
1048
+ ...request.workingDirectory && { cwd: request.workingDirectory }
1049
+ });
1050
+ const parser = new ClaudeStreamParser();
1051
+ parser.onEvent = (event) => request.onStreamEvent?.(event);
1052
+ parser.onError = (err) => process.stderr.write(`[claude-stream] ${err.message}
1053
+ `);
1054
+ let stderr = "";
1055
+ child.stdout?.on("data", (chunk) => parser.feed(chunk.toString()));
1056
+ child.stderr?.on("data", (chunk) => {
1057
+ stderr += chunk.toString();
1058
+ });
1059
+ child.stdin?.end();
1060
+ if (request.abortSignal) {
1061
+ request.abortSignal.addEventListener("abort", () => {
1062
+ try {
1063
+ child.kill("SIGTERM");
1064
+ } catch {
1065
+ }
1066
+ }, { once: true });
1067
+ }
1068
+ let killTimer;
1069
+ const timeoutHandle = setTimeout(() => {
1070
+ try {
1071
+ child.kill("SIGTERM");
1072
+ } catch {
1073
+ }
1074
+ killTimer = setTimeout(() => {
1075
+ try {
1076
+ child.kill("SIGKILL");
1077
+ } catch {
1078
+ }
1079
+ }, 5e3);
1080
+ }, CLAUDE_TIMEOUT_MS);
1081
+ let resolved = false;
1082
+ const finish = (code, errorMsg) => {
1083
+ if (resolved) return;
1084
+ resolved = true;
1085
+ clearTimeout(timeoutHandle);
1086
+ if (killTimer) clearTimeout(killTimer);
1087
+ parser.flush();
1088
+ const usage = parser.getUsage();
1089
+ resolve({
1090
+ exitCode: code ?? 1,
1091
+ output: errorMsg ?? (parser.getLastOutput() || stderr),
1092
+ toolCallCount: parser.getToolCallCount(),
1093
+ usage,
1094
+ durationMs: Date.now() - startTime
1095
+ });
1096
+ };
1097
+ child.on("close", (code) => finish(code));
1098
+ child.on("error", (err) => finish(1, err.message));
1099
+ });
1100
+ }
1101
+ async preflight() {
1102
+ try {
1103
+ const whichModule = await import("which");
1104
+ const syncFn = whichModule.default?.sync ?? whichModule.sync;
1105
+ syncFn("claude");
1106
+ return { available: true, authenticated: true };
1107
+ } catch {
1108
+ return { available: false, authenticated: false, error: "claude binary not found on PATH" };
1109
+ }
1110
+ }
1111
+ buildArgs(request) {
1112
+ const args = [
1113
+ "-p",
1114
+ request.prompt,
1115
+ "--dangerously-skip-permissions",
1116
+ "--output-format",
1117
+ "stream-json",
1118
+ "--verbose"
1119
+ ];
1120
+ if (request.mcpConfig && request.toolRestrictions?.includeMcpConfig !== false) {
1121
+ const configPath = this.writeMcpConfigJson(request.mcpConfig);
1122
+ args.push("--mcp-config", configPath);
1123
+ }
1124
+ if (request.model) args.push("--model", request.model);
1125
+ if (request.maxTurns) {
1126
+ args.push("--max-turns", String(request.maxTurns));
1127
+ }
1128
+ if (request.workingDirectory) {
1129
+ args.push("--worktree", request.workingDirectory);
1130
+ }
1131
+ if (request.toolRestrictions) {
1132
+ const tr = request.toolRestrictions;
1133
+ if (tr.tools !== void 0) args.push("--tools", tr.tools);
1134
+ if (tr.allowedTools?.length) args.push("--allowedTools", ...tr.allowedTools);
1135
+ if (tr.disallowedTools?.length) args.push("--disallowedTools", ...tr.disallowedTools);
1136
+ }
1137
+ return args;
1138
+ }
1139
+ writeMcpConfigJson(mcpConfig) {
1140
+ if (!mcpConfig) return "";
1141
+ const config = { mcpServers: mcpConfig.servers };
1142
+ const dir = join2(homedir2(), ".kantban", "tmp");
1143
+ mkdirSync2(dir, { recursive: true, mode: 448 });
1144
+ const filePath = join2(dir, `mcp-config-${Date.now()}.json`);
1145
+ writeFileSync2(filePath, JSON.stringify(config, null, 2), { mode: 384 });
1146
+ return filePath;
1147
+ }
1148
+ };
1149
+
1150
+ export {
1151
+ parseJsonFromLlmOutput,
1152
+ composeStuckDetectionPrompt,
1153
+ parseStuckDetectionResponse,
1154
+ classifyTrajectory,
1155
+ RalphLoop,
1156
+ generateMcpConfig,
1157
+ cleanupMcpConfig,
1158
+ generateGateProxyMcpConfig,
1159
+ cleanupGateProxyConfigs,
1160
+ ClaudeProvider
1161
+ };
1162
+ //# sourceMappingURL=chunk-4RQDDZLM.js.map