karajan-code 1.5.0 → 1.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.
- package/README.md +5 -1
- package/docs/README.es.md +2 -0
- package/package.json +1 -1
- package/src/cli.js +3 -1
- package/src/commands/report.js +76 -5
- package/src/commands/run.js +1 -1
- package/src/config.js +3 -1
- package/src/git/automation.js +33 -2
- package/src/mcp/run-kj.js +16 -2
- package/src/mcp/server-handlers.js +5 -2
- package/src/mcp/tools.js +2 -0
- package/src/orchestrator/iteration-stages.js +9 -5
- package/src/orchestrator/post-loop-stages.js +14 -2
- package/src/orchestrator/pre-loop-stages.js +39 -5
- package/src/orchestrator.js +119 -7
- package/src/planning-game/client.js +28 -0
- package/src/planning-game/decomposition.js +83 -0
- package/src/roles/planner-role.js +14 -2
- package/src/roles/triage-role.js +29 -9
- package/src/utils/process.js +44 -6
- package/templates/roles/triage.md +17 -2
package/README.md
CHANGED
|
@@ -39,6 +39,8 @@ Instead of running one AI agent and manually reviewing its output, `kj` chains a
|
|
|
39
39
|
- **Session management** — pause/resume with fail-fast detection and automatic cleanup of expired sessions
|
|
40
40
|
- **Plugin system** — extend with custom agents via `.karajan/plugins/`
|
|
41
41
|
- **Smart model selection** — auto-selects optimal model per role based on triage complexity (lighter models for trivial tasks, powerful models for complex ones)
|
|
42
|
+
- **Interactive checkpoints** — instead of killing long-running tasks, pauses every 5 minutes with a progress report and lets you decide: continue, stop, or adjust the time
|
|
43
|
+
- **Task decomposition** — triage detects when tasks should be split and recommends subtasks; with Planning Game integration, creates linked cards with sequential blocking
|
|
42
44
|
- **Retry with backoff** — automatic recovery from transient API errors (429, 5xx) with exponential backoff and jitter
|
|
43
45
|
- **Planning Game integration** — optionally pair with [Planning Game](https://github.com/AgenteIA-Geniova/planning-game) for agile project management (tasks, sprints, estimation) — like Jira, but open-source and XP-native
|
|
44
46
|
|
|
@@ -199,6 +201,7 @@ kj run "Fix the login bug" [options]
|
|
|
199
201
|
| `--smart-models` | Enable smart model selection based on triage complexity |
|
|
200
202
|
| `--no-smart-models` | Disable smart model selection |
|
|
201
203
|
| `--no-sonar` | Skip SonarQube analysis |
|
|
204
|
+
| `--checkpoint-interval <n>` | Minutes between interactive checkpoints (default: 5) |
|
|
202
205
|
| `--pg-task <cardId>` | Planning Game card ID for task context |
|
|
203
206
|
| `--pg-project <projectId>` | Planning Game project ID |
|
|
204
207
|
| `--dry-run` | Show what would run without executing |
|
|
@@ -349,6 +352,7 @@ git:
|
|
|
349
352
|
session:
|
|
350
353
|
max_iteration_minutes: 15
|
|
351
354
|
max_total_minutes: 120
|
|
355
|
+
checkpoint_interval_minutes: 5 # Interactive checkpoint every N minutes
|
|
352
356
|
max_budget_usd: null # null = unlimited
|
|
353
357
|
fail_fast_repeats: 2
|
|
354
358
|
|
|
@@ -443,7 +447,7 @@ Use `kj roles show <role>` to inspect any template. Create a project override to
|
|
|
443
447
|
git clone https://github.com/manufosela/karajan-code.git
|
|
444
448
|
cd karajan-code
|
|
445
449
|
npm install
|
|
446
|
-
npm test # Run
|
|
450
|
+
npm test # Run 1025+ tests with Vitest
|
|
447
451
|
npm run test:watch # Watch mode
|
|
448
452
|
npm run validate # Lint + test
|
|
449
453
|
```
|
package/docs/README.es.md
CHANGED
|
@@ -38,6 +38,8 @@ En lugar de ejecutar un agente de IA y revisar manualmente su output, `kj` encad
|
|
|
38
38
|
- **Automatizacion Git** — auto-commit, auto-push, auto-PR tras aprobacion
|
|
39
39
|
- **Gestion de sesiones** — pausa/reanudacion con deteccion fail-fast y limpieza automatica de sesiones expiradas
|
|
40
40
|
- **Sistema de plugins** — extiende con agentes custom via `.karajan/plugins/`
|
|
41
|
+
- **Checkpoints interactivos** — en lugar de matar tareas largas, pausa cada 5 minutos con un informe de progreso y te deja decidir: continuar, parar o ajustar el tiempo
|
|
42
|
+
- **Descomposicion de tareas** — triage detecta cuando una tarea debe dividirse y recomienda subtareas; con integracion Planning Game, crea cards vinculadas con bloqueo secuencial
|
|
41
43
|
- **Retry con backoff** — recuperacion automatica ante errores transitorios de API (429, 5xx) con backoff exponencial y jitter
|
|
42
44
|
- **Integracion con Planning Game** — combina opcionalmente con [Planning Game](https://github.com/AgenteIA-Geniova/planning-game) para gestion agil de proyectos (tareas, sprints, estimacion) — como Jira, pero open-source y nativo XP
|
|
43
45
|
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -24,7 +24,7 @@ async function withConfig(commandName, flags, fn) {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
const program = new Command();
|
|
27
|
-
program.name("kj").description("Karajan Code CLI").version("1.
|
|
27
|
+
program.name("kj").description("Karajan Code CLI").version("1.6.0");
|
|
28
28
|
|
|
29
29
|
program
|
|
30
30
|
.command("init")
|
|
@@ -81,6 +81,7 @@ program
|
|
|
81
81
|
.option("--methodology <name>")
|
|
82
82
|
.option("--no-auto-rebase")
|
|
83
83
|
.option("--no-sonar")
|
|
84
|
+
.option("--checkpoint-interval <n>", "Minutes between interactive checkpoints (default: 5)")
|
|
84
85
|
.option("--pg-task <cardId>", "Planning Game card ID (e.g., KJC-TSK-0042)")
|
|
85
86
|
.option("--pg-project <projectId>", "Planning Game project ID")
|
|
86
87
|
.option("--smart-models", "Enable smart model selection based on triage complexity")
|
|
@@ -140,6 +141,7 @@ program
|
|
|
140
141
|
.option("--format <type>", "Output format: text|json", "text")
|
|
141
142
|
.option("--trace", "Show chronological trace of all pipeline stages")
|
|
142
143
|
.option("--currency <code>", "Display costs in currency: usd|eur", "usd")
|
|
144
|
+
.option("--pg-task <cardId>", "Filter reports by Planning Game card ID")
|
|
143
145
|
.action(async (flags) => {
|
|
144
146
|
await reportCommand(flags);
|
|
145
147
|
});
|
package/src/commands/report.js
CHANGED
|
@@ -135,7 +135,7 @@ async function buildReport(dir, sessionId) {
|
|
|
135
135
|
|
|
136
136
|
const budgetTrace = Array.isArray(session.budget?.trace) ? session.budget.trace : [];
|
|
137
137
|
|
|
138
|
-
|
|
138
|
+
const report = {
|
|
139
139
|
session_id: session.id,
|
|
140
140
|
task_description: session.task || "",
|
|
141
141
|
plan_executed: summarizePlan(checkpoints),
|
|
@@ -150,6 +150,9 @@ async function buildReport(dir, sessionId) {
|
|
|
150
150
|
commits_generated: commits,
|
|
151
151
|
status: session.status || "unknown"
|
|
152
152
|
};
|
|
153
|
+
if (session.pg_task_id) report.pg_task_id = session.pg_task_id;
|
|
154
|
+
if (session.pg_project_id) report.pg_project_id = session.pg_project_id;
|
|
155
|
+
return report;
|
|
153
156
|
}
|
|
154
157
|
|
|
155
158
|
function printTextReport(report) {
|
|
@@ -178,6 +181,10 @@ function printTextReport(report) {
|
|
|
178
181
|
: String(report.commits_generated.count);
|
|
179
182
|
|
|
180
183
|
console.log(`Session: ${report.session_id}`);
|
|
184
|
+
if (report.pg_task_id) {
|
|
185
|
+
const projectLabel = report.pg_project_id ? ` (${report.pg_project_id})` : "";
|
|
186
|
+
console.log(`Planning Game Card: ${report.pg_task_id}${projectLabel}`);
|
|
187
|
+
}
|
|
181
188
|
console.log(`Status: ${report.status}`);
|
|
182
189
|
console.log("Task Description:");
|
|
183
190
|
console.log(report.task_description || "N/A");
|
|
@@ -233,13 +240,14 @@ function printTraceTable(trace, { currency = "usd", exchangeRate = 0.92 } = {})
|
|
|
233
240
|
|
|
234
241
|
const currencyLabel = currency.toUpperCase();
|
|
235
242
|
|
|
236
|
-
const headers = ["#", "Stage", "Provider", "Duration", "Tokens In", "Tokens Out", `Cost ${currencyLabel}`];
|
|
243
|
+
const headers = ["#", "Stage", "Provider", "Model", "Duration", "Tokens In", "Tokens Out", `Cost ${currencyLabel}`];
|
|
237
244
|
const rows = trace.map((entry) => {
|
|
238
245
|
const cost = convertCost(entry.cost_usd, currency, exchangeRate);
|
|
239
246
|
return [
|
|
240
247
|
String(entry.index ?? "-"),
|
|
241
248
|
entry.role,
|
|
242
249
|
entry.provider || "-",
|
|
250
|
+
entry.model || "-",
|
|
243
251
|
formatDuration(entry.duration_ms),
|
|
244
252
|
String(entry.tokens_in),
|
|
245
253
|
String(entry.tokens_out),
|
|
@@ -262,6 +270,7 @@ function printTraceTable(trace, { currency = "usd", exchangeRate = 0.92 } = {})
|
|
|
262
270
|
"",
|
|
263
271
|
"TOTAL",
|
|
264
272
|
"",
|
|
273
|
+
"",
|
|
265
274
|
formatDuration(totals.duration),
|
|
266
275
|
String(totals.tokens_in),
|
|
267
276
|
String(totals.tokens_out),
|
|
@@ -273,7 +282,7 @@ function printTraceTable(trace, { currency = "usd", exchangeRate = 0.92 } = {})
|
|
|
273
282
|
Math.max(...allRows.map((row) => String(row[colIdx]).length))
|
|
274
283
|
);
|
|
275
284
|
|
|
276
|
-
const rightAligned = new Set([0,
|
|
285
|
+
const rightAligned = new Set([0, 4, 5, 6, 7]);
|
|
277
286
|
function formatRow(row) {
|
|
278
287
|
return row
|
|
279
288
|
.map((cell, idx) =>
|
|
@@ -293,7 +302,26 @@ function printTraceTable(trace, { currency = "usd", exchangeRate = 0.92 } = {})
|
|
|
293
302
|
console.log(formatRow(totalRow));
|
|
294
303
|
}
|
|
295
304
|
|
|
296
|
-
|
|
305
|
+
async function findSessionsByPgTask(dir, pgTask) {
|
|
306
|
+
const entries = await fs.readdir(dir);
|
|
307
|
+
const matches = [];
|
|
308
|
+
for (const entry of entries) {
|
|
309
|
+
const sessionFile = path.join(dir, entry, "session.json");
|
|
310
|
+
if (!(await exists(sessionFile))) continue;
|
|
311
|
+
try {
|
|
312
|
+
const raw = await fs.readFile(sessionFile, "utf8");
|
|
313
|
+
const session = JSON.parse(raw);
|
|
314
|
+
if (session.pg_task_id === pgTask) {
|
|
315
|
+
matches.push(entry);
|
|
316
|
+
}
|
|
317
|
+
} catch {
|
|
318
|
+
// skip malformed sessions
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return matches.sort();
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
export async function reportCommand({ list = false, sessionId = null, format = "text", trace = false, currency = "usd", pgTask = null }) {
|
|
297
325
|
const dir = getSessionRoot();
|
|
298
326
|
if (!(await exists(dir))) {
|
|
299
327
|
console.log("No reports yet");
|
|
@@ -301,6 +329,45 @@ export async function reportCommand({ list = false, sessionId = null, format = "
|
|
|
301
329
|
}
|
|
302
330
|
|
|
303
331
|
const entries = await fs.readdir(dir);
|
|
332
|
+
|
|
333
|
+
if (pgTask) {
|
|
334
|
+
const matches = await findSessionsByPgTask(dir, pgTask);
|
|
335
|
+
if (matches.length === 0) {
|
|
336
|
+
console.log(`No sessions found for card: ${pgTask}`);
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
if (list) {
|
|
340
|
+
for (const item of matches) console.log(item);
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
// Show all reports for this card, or the latest if no list flag
|
|
344
|
+
const targetId = sessionId || matches.at(-1);
|
|
345
|
+
const report = await buildReport(dir, targetId);
|
|
346
|
+
if (format === "json") {
|
|
347
|
+
console.log(JSON.stringify({ card: pgTask, sessions: matches, report }, null, 2));
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
console.log(`Card ${pgTask}: ${matches.length} session${matches.length === 1 ? "" : "s"}`);
|
|
351
|
+
for (const m of matches) {
|
|
352
|
+
const marker = m === targetId ? " <--" : "";
|
|
353
|
+
console.log(` ${m}${marker}`);
|
|
354
|
+
}
|
|
355
|
+
console.log("");
|
|
356
|
+
if (trace) {
|
|
357
|
+
const { config } = await loadConfig();
|
|
358
|
+
const cur = currency?.toLowerCase() || config?.budget?.currency || "usd";
|
|
359
|
+
const rate = config?.budget?.exchange_rate_eur ?? 0.92;
|
|
360
|
+
console.log(`Session: ${report.session_id}`);
|
|
361
|
+
console.log(`Status: ${report.status}`);
|
|
362
|
+
console.log(`Task: ${report.task_description || "N/A"}`);
|
|
363
|
+
console.log("");
|
|
364
|
+
printTraceTable(report.budget_trace, { currency: cur, exchangeRate: rate });
|
|
365
|
+
} else {
|
|
366
|
+
printTextReport(report);
|
|
367
|
+
}
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
|
|
304
371
|
if (list) {
|
|
305
372
|
for (const item of entries) console.log(item);
|
|
306
373
|
return;
|
|
@@ -327,6 +394,10 @@ export async function reportCommand({ list = false, sessionId = null, format = "
|
|
|
327
394
|
const cur = currency?.toLowerCase() || config?.budget?.currency || "usd";
|
|
328
395
|
const rate = config?.budget?.exchange_rate_eur ?? 0.92;
|
|
329
396
|
console.log(`Session: ${report.session_id}`);
|
|
397
|
+
if (report.pg_task_id) {
|
|
398
|
+
const projectLabel = report.pg_project_id ? ` (${report.pg_project_id})` : "";
|
|
399
|
+
console.log(`Planning Game Card: ${report.pg_task_id}${projectLabel}`);
|
|
400
|
+
}
|
|
330
401
|
console.log(`Status: ${report.status}`);
|
|
331
402
|
console.log(`Task: ${report.task_description || "N/A"}`);
|
|
332
403
|
console.log("");
|
|
@@ -337,4 +408,4 @@ export async function reportCommand({ list = false, sessionId = null, format = "
|
|
|
337
408
|
printTextReport(report);
|
|
338
409
|
}
|
|
339
410
|
|
|
340
|
-
export { formatDuration, convertCost, formatCost, printTraceTable, buildReport };
|
|
411
|
+
export { formatDuration, convertCost, formatCost, printTraceTable, buildReport, findSessionsByPgTask };
|
package/src/commands/run.js
CHANGED
|
@@ -66,7 +66,7 @@ export async function runCommandHandler({ task, config, logger, flags }) {
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
const startDate = new Date().toISOString();
|
|
69
|
-
const result = await runFlow({ task: enrichedTask, config, logger, flags, emitter });
|
|
69
|
+
const result = await runFlow({ task: enrichedTask, config, logger, flags, emitter, pgTaskId: pgCardId || null, pgProject: pgProject || null });
|
|
70
70
|
|
|
71
71
|
// --- Planning Game: update card on completion ---
|
|
72
72
|
if (pgCard && pgProject && result?.approved) {
|
package/src/config.js
CHANGED
|
@@ -112,8 +112,9 @@ const DEFAULTS = {
|
|
|
112
112
|
role_overrides: {}
|
|
113
113
|
},
|
|
114
114
|
session: {
|
|
115
|
-
max_iteration_minutes:
|
|
115
|
+
max_iteration_minutes: 30,
|
|
116
116
|
max_total_minutes: 120,
|
|
117
|
+
checkpoint_interval_minutes: 5,
|
|
117
118
|
fail_fast_repeats: 2,
|
|
118
119
|
repeat_detection_threshold: 2,
|
|
119
120
|
max_sonar_retries: 3,
|
|
@@ -244,6 +245,7 @@ export function applyRunOverrides(config, flags) {
|
|
|
244
245
|
if (flags.maxIterations) out.max_iterations = Number(flags.maxIterations);
|
|
245
246
|
if (flags.maxIterationMinutes) out.session.max_iteration_minutes = Number(flags.maxIterationMinutes);
|
|
246
247
|
if (flags.maxTotalMinutes) out.session.max_total_minutes = Number(flags.maxTotalMinutes);
|
|
248
|
+
if (flags.checkpointInterval) out.session.checkpoint_interval_minutes = Number(flags.checkpointInterval);
|
|
247
249
|
if (flags.baseBranch) out.base_branch = flags.baseBranch;
|
|
248
250
|
if (flags.coderFallback) out.coder_options.fallback_coder = flags.coderFallback;
|
|
249
251
|
if (flags.reviewerFallback) out.reviewer_options.fallback_reviewer = flags.reviewerFallback;
|
package/src/git/automation.js
CHANGED
|
@@ -52,7 +52,37 @@ export async function prepareGitAutomation({ config, task, logger, session }) {
|
|
|
52
52
|
return { enabled: true, branch, baseBranch, autoRebase };
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
export
|
|
55
|
+
export function buildPrBody({ task, stageResults }) {
|
|
56
|
+
const sections = ["Created by Karajan Code."];
|
|
57
|
+
|
|
58
|
+
const approach = stageResults?.planner?.approach;
|
|
59
|
+
if (approach) {
|
|
60
|
+
sections.push("", "## Approach", approach);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const steps = stageResults?.planner?.steps;
|
|
64
|
+
if (steps?.length) {
|
|
65
|
+
sections.push("", "## Steps");
|
|
66
|
+
for (let i = 0; i < steps.length; i++) {
|
|
67
|
+
sections.push(`${i + 1}. ${steps[i]}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const triageSubtasks = stageResults?.triage?.subtasks;
|
|
72
|
+
const shouldDecompose = stageResults?.triage?.shouldDecompose;
|
|
73
|
+
const pendingSubtasks = shouldDecompose && triageSubtasks?.length > 1 ? triageSubtasks.slice(1) : [];
|
|
74
|
+
if (pendingSubtasks.length > 0) {
|
|
75
|
+
sections.push("", "## Pending subtasks");
|
|
76
|
+
sections.push("This PR addresses part of a larger task. The following subtasks were identified but not included:");
|
|
77
|
+
for (const subtask of pendingSubtasks) {
|
|
78
|
+
sections.push(`- [ ] ${subtask}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return sections.join("\n");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function finalizeGitAutomation({ config, gitCtx, task, logger, session, stageResults = null }) {
|
|
56
86
|
if (!gitCtx?.enabled) return { git: "disabled", commits: [] };
|
|
57
87
|
|
|
58
88
|
const commitMsg = config.git.commit_message || commitMessageFromTask(task);
|
|
@@ -86,11 +116,12 @@ export async function finalizeGitAutomation({ config, gitCtx, task, logger, sess
|
|
|
86
116
|
|
|
87
117
|
let prUrl = null;
|
|
88
118
|
if (config.git.auto_pr) {
|
|
119
|
+
const body = buildPrBody({ task, stageResults });
|
|
89
120
|
prUrl = await createPullRequest({
|
|
90
121
|
baseBranch: gitCtx.baseBranch,
|
|
91
122
|
branch: gitCtx.branch,
|
|
92
123
|
title: commitMessageFromTask(task),
|
|
93
|
-
body
|
|
124
|
+
body
|
|
94
125
|
});
|
|
95
126
|
await addCheckpoint(session, { stage: "git-pr", branch: gitCtx.branch, pr: prUrl });
|
|
96
127
|
logger.info("Pull request created");
|
package/src/mcp/run-kj.js
CHANGED
|
@@ -51,6 +51,7 @@ export async function runKjCommand({ command, commandArgs = [], options = {}, en
|
|
|
51
51
|
normalizeBoolFlag(options.noSonar, "--no-sonar", args);
|
|
52
52
|
if (options.smartModels === true) args.push("--smart-models");
|
|
53
53
|
if (options.smartModels === false) args.push("--no-smart-models");
|
|
54
|
+
addOptionalValue(args, "--checkpoint-interval", options.checkpointInterval);
|
|
54
55
|
addOptionalValue(args, "--pg-task", options.pgTask);
|
|
55
56
|
addOptionalValue(args, "--pg-project", options.pgProject);
|
|
56
57
|
|
|
@@ -67,10 +68,12 @@ export async function runKjCommand({ command, commandArgs = [], options = {}, en
|
|
|
67
68
|
runEnv.KJ_SONAR_TOKEN = options.sonarToken;
|
|
68
69
|
}
|
|
69
70
|
|
|
71
|
+
const timeout = options.timeoutMs ? Number(options.timeoutMs) : undefined;
|
|
72
|
+
|
|
70
73
|
const result = await execa("node", args, {
|
|
71
74
|
env: runEnv,
|
|
72
75
|
reject: false,
|
|
73
|
-
timeout
|
|
76
|
+
timeout
|
|
74
77
|
});
|
|
75
78
|
|
|
76
79
|
const ok = result.exitCode === 0;
|
|
@@ -81,7 +84,18 @@ export async function runKjCommand({ command, commandArgs = [], options = {}, en
|
|
|
81
84
|
stderr: result.stderr
|
|
82
85
|
};
|
|
83
86
|
|
|
84
|
-
if (
|
|
87
|
+
if (result.timedOut) {
|
|
88
|
+
payload.ok = false;
|
|
89
|
+
payload.timedOut = true;
|
|
90
|
+
payload.stderr = `Process timed out after ${Math.round((timeout || 0) / 1000)}s (explicit timeout). Consider using kj_run for interactive checkpoint support instead of subprocess commands.`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (result.killed && !payload.timedOut) {
|
|
94
|
+
payload.ok = false;
|
|
95
|
+
payload.stderr = `Process was killed by signal ${result.signal || "unknown"}. ${result.stderr || ""}`.trim();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (!ok && result.stderr && !payload.timedOut) {
|
|
85
99
|
payload.errorSummary = result.stderr.split("\n").filter(Boolean).slice(-3).join(" | ");
|
|
86
100
|
}
|
|
87
101
|
|
|
@@ -66,7 +66,7 @@ export function classifyError(error) {
|
|
|
66
66
|
if (lower.includes("timed out") || lower.includes("timeout")) {
|
|
67
67
|
return {
|
|
68
68
|
category: "timeout",
|
|
69
|
-
suggestion: "The
|
|
69
|
+
suggestion: "The agent did not complete in time. Try: (1) increase --max-iteration-minutes (default: 5), (2) split the task into smaller pieces, (3) use kj_code for single-agent tasks. If a SonarQube scan timed out, check Docker health."
|
|
70
70
|
};
|
|
71
71
|
}
|
|
72
72
|
|
|
@@ -145,7 +145,9 @@ export async function handleRunDirect(a, server, extra) {
|
|
|
145
145
|
if (progressNotifier) emitter.on("progress", progressNotifier);
|
|
146
146
|
|
|
147
147
|
const askQuestion = buildAskQuestion(server);
|
|
148
|
-
const
|
|
148
|
+
const pgTaskId = a.pgTask || null;
|
|
149
|
+
const pgProject = a.pgProject || config.planning_game?.project_id || null;
|
|
150
|
+
const result = await runFlow({ task: a.task, config, logger, flags: a, emitter, askQuestion, pgTaskId, pgProject });
|
|
149
151
|
return { ok: !result.paused && (result.approved !== false), ...result };
|
|
150
152
|
}
|
|
151
153
|
|
|
@@ -212,6 +214,7 @@ export async function handleToolCall(name, args, server, extra) {
|
|
|
212
214
|
if (a.format) commandArgs.push("--format", String(a.format));
|
|
213
215
|
if (a.trace) commandArgs.push("--trace");
|
|
214
216
|
if (a.currency) commandArgs.push("--currency", String(a.currency));
|
|
217
|
+
if (a.pgTask) commandArgs.push("--pg-task", String(a.pgTask));
|
|
215
218
|
return runKjCommand({
|
|
216
219
|
command: "report",
|
|
217
220
|
commandArgs,
|
package/src/mcp/tools.js
CHANGED
|
@@ -86,6 +86,7 @@ export const tools = [
|
|
|
86
86
|
autoRebase: { type: "boolean" },
|
|
87
87
|
branchPrefix: { type: "string" },
|
|
88
88
|
smartModels: { type: "boolean", description: "Enable/disable smart model selection based on triage complexity" },
|
|
89
|
+
checkpointInterval: { type: "number", description: "Minutes between interactive checkpoints (default: 5). Set 0 to disable." },
|
|
89
90
|
noSonar: { type: "boolean" },
|
|
90
91
|
kjHome: { type: "string" },
|
|
91
92
|
sonarToken: { type: "string" },
|
|
@@ -118,6 +119,7 @@ export const tools = [
|
|
|
118
119
|
format: { type: "string", enum: ["text", "json"] },
|
|
119
120
|
trace: { type: "boolean", description: "Show chronological trace of all pipeline stages" },
|
|
120
121
|
currency: { type: "string", enum: ["usd", "eur"], description: "Display costs in this currency" },
|
|
122
|
+
pgTask: { type: "string", description: "Filter reports by Planning Game card ID (e.g., KJC-TSK-0042)" },
|
|
121
123
|
kjHome: { type: "string" }
|
|
122
124
|
}
|
|
123
125
|
}
|
|
@@ -73,7 +73,7 @@ export async function runCoderStage({ coderRoleInstance, coderRole, config, logg
|
|
|
73
73
|
});
|
|
74
74
|
|
|
75
75
|
if (fallbackResult.execResult?.ok) {
|
|
76
|
-
await addCheckpoint(session, { stage: "coder", iteration, note: `Coder completed via fallback (${fallbackCoder})
|
|
76
|
+
await addCheckpoint(session, { stage: "coder", iteration, note: `Coder completed via fallback (${fallbackCoder})`, provider: fallbackCoder, model: null });
|
|
77
77
|
emitProgress(
|
|
78
78
|
emitter,
|
|
79
79
|
makeEvent("coder:end", { ...eventBase, stage: "coder" }, {
|
|
@@ -112,7 +112,7 @@ export async function runCoderStage({ coderRoleInstance, coderRole, config, logg
|
|
|
112
112
|
throw new Error(`Coder failed: ${details}`);
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
-
await addCheckpoint(session, { stage: "coder", iteration, note: "Coder applied changes" });
|
|
115
|
+
await addCheckpoint(session, { stage: "coder", iteration, note: "Coder applied changes", provider: coderRole.provider, model: coderRole.model || null });
|
|
116
116
|
emitProgress(
|
|
117
117
|
emitter,
|
|
118
118
|
makeEvent("coder:end", { ...eventBase, stage: "coder" }, {
|
|
@@ -169,7 +169,7 @@ export async function runRefactorerStage({ refactorerRole, config, logger, emitt
|
|
|
169
169
|
);
|
|
170
170
|
throw new Error(`Refactorer failed: ${details}`);
|
|
171
171
|
}
|
|
172
|
-
await addCheckpoint(session, { stage: "refactorer", iteration, note: "Refactorer applied cleanups" });
|
|
172
|
+
await addCheckpoint(session, { stage: "refactorer", iteration, note: "Refactorer applied cleanups", provider: refactorerRole.provider, model: refactorerRole.model || null });
|
|
173
173
|
emitProgress(
|
|
174
174
|
emitter,
|
|
175
175
|
makeEvent("refactorer:end", { ...eventBase, stage: "refactorer" }, {
|
|
@@ -285,7 +285,9 @@ export async function runSonarStage({ config, logger, emitter, eventBase, sessio
|
|
|
285
285
|
iteration,
|
|
286
286
|
project_key: sonarResult.projectKey,
|
|
287
287
|
quality_gate: sonarResult.gateStatus,
|
|
288
|
-
open_issues: sonarResult.openIssuesTotal
|
|
288
|
+
open_issues: sonarResult.openIssuesTotal,
|
|
289
|
+
provider: "sonar",
|
|
290
|
+
model: null
|
|
289
291
|
});
|
|
290
292
|
|
|
291
293
|
emitProgress(
|
|
@@ -472,7 +474,9 @@ export async function runReviewerStage({ reviewerRole, config, logger, emitter,
|
|
|
472
474
|
stage: "reviewer",
|
|
473
475
|
iteration,
|
|
474
476
|
approved: review.approved,
|
|
475
|
-
blocking_issues: review.blocking_issues.length
|
|
477
|
+
blocking_issues: review.blocking_issues.length,
|
|
478
|
+
provider: reviewerRole.provider,
|
|
479
|
+
model: reviewerRole.model || null
|
|
476
480
|
});
|
|
477
481
|
|
|
478
482
|
emitProgress(
|
|
@@ -25,7 +25,13 @@ export async function runTesterStage({ config, logger, emitter, eventBase, sessi
|
|
|
25
25
|
duration_ms: Date.now() - testerStart
|
|
26
26
|
});
|
|
27
27
|
|
|
28
|
-
await addCheckpoint(session, {
|
|
28
|
+
await addCheckpoint(session, {
|
|
29
|
+
stage: "tester",
|
|
30
|
+
iteration,
|
|
31
|
+
ok: testerOutput.ok,
|
|
32
|
+
provider: config?.roles?.tester?.provider || coderRole.provider,
|
|
33
|
+
model: config?.roles?.tester?.model || coderRole.model || null
|
|
34
|
+
});
|
|
29
35
|
|
|
30
36
|
emitProgress(
|
|
31
37
|
emitter,
|
|
@@ -93,7 +99,13 @@ export async function runSecurityStage({ config, logger, emitter, eventBase, ses
|
|
|
93
99
|
duration_ms: Date.now() - securityStart
|
|
94
100
|
});
|
|
95
101
|
|
|
96
|
-
await addCheckpoint(session, {
|
|
102
|
+
await addCheckpoint(session, {
|
|
103
|
+
stage: "security",
|
|
104
|
+
iteration,
|
|
105
|
+
ok: securityOutput.ok,
|
|
106
|
+
provider: config?.roles?.security?.provider || coderRole.provider,
|
|
107
|
+
model: config?.roles?.security?.model || coderRole.model || null
|
|
108
|
+
});
|
|
97
109
|
|
|
98
110
|
emitProgress(
|
|
99
111
|
emitter,
|
|
@@ -28,7 +28,13 @@ export async function runTriageStage({ config, logger, emitter, eventBase, sessi
|
|
|
28
28
|
duration_ms: Date.now() - triageStart
|
|
29
29
|
});
|
|
30
30
|
|
|
31
|
-
await addCheckpoint(session, {
|
|
31
|
+
await addCheckpoint(session, {
|
|
32
|
+
stage: "triage",
|
|
33
|
+
iteration: 0,
|
|
34
|
+
ok: triageOutput.ok,
|
|
35
|
+
provider: config?.roles?.triage?.provider || coderRole.provider,
|
|
36
|
+
model: config?.roles?.triage?.model || coderRole.model || null
|
|
37
|
+
});
|
|
32
38
|
|
|
33
39
|
const recommendedRoles = new Set(triageOutput.result?.roles || []);
|
|
34
40
|
const roleOverrides = {};
|
|
@@ -41,11 +47,16 @@ export async function runTriageStage({ config, logger, emitter, eventBase, sessi
|
|
|
41
47
|
roleOverrides.securityEnabled = recommendedRoles.has("security");
|
|
42
48
|
}
|
|
43
49
|
|
|
50
|
+
const shouldDecompose = triageOutput.result?.shouldDecompose || false;
|
|
51
|
+
const subtasks = triageOutput.result?.subtasks || [];
|
|
52
|
+
|
|
44
53
|
const stageResult = {
|
|
45
54
|
ok: triageOutput.ok,
|
|
46
55
|
level: triageOutput.result?.level || null,
|
|
47
56
|
roles: Array.from(recommendedRoles),
|
|
48
|
-
reasoning: triageOutput.result?.reasoning || null
|
|
57
|
+
reasoning: triageOutput.result?.reasoning || null,
|
|
58
|
+
shouldDecompose,
|
|
59
|
+
subtasks
|
|
49
60
|
};
|
|
50
61
|
|
|
51
62
|
let modelSelection = null;
|
|
@@ -73,6 +84,16 @@ export async function runTriageStage({ config, logger, emitter, eventBase, sessi
|
|
|
73
84
|
stageResult.modelSelection = modelSelection;
|
|
74
85
|
}
|
|
75
86
|
|
|
87
|
+
if (shouldDecompose && subtasks.length > 0) {
|
|
88
|
+
emitProgress(
|
|
89
|
+
emitter,
|
|
90
|
+
makeEvent("triage:decompose", { ...eventBase, stage: "triage" }, {
|
|
91
|
+
message: `Task decomposition recommended: ${subtasks.length} subtask${subtasks.length === 1 ? "" : "s"}`,
|
|
92
|
+
detail: { shouldDecompose, subtasks }
|
|
93
|
+
})
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
76
97
|
emitProgress(
|
|
77
98
|
emitter,
|
|
78
99
|
makeEvent("triage:end", { ...eventBase, stage: "triage" }, {
|
|
@@ -106,7 +127,13 @@ export async function runResearcherStage({ config, logger, emitter, eventBase, s
|
|
|
106
127
|
duration_ms: Date.now() - researchStart
|
|
107
128
|
});
|
|
108
129
|
|
|
109
|
-
await addCheckpoint(session, {
|
|
130
|
+
await addCheckpoint(session, {
|
|
131
|
+
stage: "researcher",
|
|
132
|
+
iteration: 0,
|
|
133
|
+
ok: researchOutput.ok,
|
|
134
|
+
provider: config?.roles?.researcher?.provider || coderRole.provider,
|
|
135
|
+
model: config?.roles?.researcher?.model || coderRole.model || null
|
|
136
|
+
});
|
|
110
137
|
|
|
111
138
|
emitProgress(
|
|
112
139
|
emitter,
|
|
@@ -122,7 +149,7 @@ export async function runResearcherStage({ config, logger, emitter, eventBase, s
|
|
|
122
149
|
return { researchContext, stageResult };
|
|
123
150
|
}
|
|
124
151
|
|
|
125
|
-
export async function runPlannerStage({ config, logger, emitter, eventBase, session, plannerRole, researchContext, trackBudget }) {
|
|
152
|
+
export async function runPlannerStage({ config, logger, emitter, eventBase, session, plannerRole, researchContext, triageDecomposition = null, trackBudget }) {
|
|
126
153
|
const task = session.task;
|
|
127
154
|
logger.setContext({ iteration: 0, stage: "planner" });
|
|
128
155
|
emitProgress(
|
|
@@ -134,11 +161,18 @@ export async function runPlannerStage({ config, logger, emitter, eventBase, sess
|
|
|
134
161
|
);
|
|
135
162
|
|
|
136
163
|
const planRole = new PlannerRole({ config, logger, emitter, createAgentFn: createAgent });
|
|
137
|
-
planRole.context = { task, research: researchContext };
|
|
164
|
+
planRole.context = { task, research: researchContext, triageDecomposition };
|
|
138
165
|
await planRole.init();
|
|
139
166
|
const plannerStart = Date.now();
|
|
140
167
|
const planResult = await planRole.execute(task);
|
|
141
168
|
trackBudget({ role: "planner", provider: plannerRole.provider, model: plannerRole.model, result: planResult.result, duration_ms: Date.now() - plannerStart });
|
|
169
|
+
await addCheckpoint(session, {
|
|
170
|
+
stage: "planner",
|
|
171
|
+
iteration: 0,
|
|
172
|
+
ok: planResult.ok,
|
|
173
|
+
provider: plannerRole.provider,
|
|
174
|
+
model: plannerRole.model || null
|
|
175
|
+
});
|
|
142
176
|
|
|
143
177
|
if (!planResult.ok) {
|
|
144
178
|
await markSessionStatus(session, "failed");
|
package/src/orchestrator.js
CHANGED
|
@@ -4,7 +4,8 @@ import {
|
|
|
4
4
|
loadSession,
|
|
5
5
|
markSessionStatus,
|
|
6
6
|
resumeSessionWithAnswer,
|
|
7
|
-
saveSession
|
|
7
|
+
saveSession,
|
|
8
|
+
addCheckpoint
|
|
8
9
|
} from "./session-store.js";
|
|
9
10
|
import { computeBaseRef, generateDiff } from "./review/diff-generator.js";
|
|
10
11
|
import { buildCoderPrompt } from "./prompts/coder.js";
|
|
@@ -27,7 +28,7 @@ import { runTesterStage, runSecurityStage } from "./orchestrator/post-loop-stage
|
|
|
27
28
|
|
|
28
29
|
|
|
29
30
|
|
|
30
|
-
export async function runFlow({ task, config, logger, flags = {}, emitter = null, askQuestion = null }) {
|
|
31
|
+
export async function runFlow({ task, config, logger, flags = {}, emitter = null, askQuestion = null, pgTaskId = null, pgProject = null }) {
|
|
31
32
|
const plannerRole = resolveRole(config, "planner");
|
|
32
33
|
const coderRole = resolveRole(config, "coder");
|
|
33
34
|
const reviewerRole = resolveRole(config, "reviewer");
|
|
@@ -136,7 +137,7 @@ export async function runFlow({ task, config, logger, flags = {}, emitter = null
|
|
|
136
137
|
}
|
|
137
138
|
|
|
138
139
|
const baseRef = await computeBaseRef({ baseBranch: config.base_branch, baseRef: flags.baseRef || null });
|
|
139
|
-
const
|
|
140
|
+
const sessionInit = {
|
|
140
141
|
task,
|
|
141
142
|
config_snapshot: config,
|
|
142
143
|
base_ref: baseRef,
|
|
@@ -149,7 +150,10 @@ export async function runFlow({ task, config, logger, flags = {}, emitter = null
|
|
|
149
150
|
sonar_repeat_count: 0,
|
|
150
151
|
last_reviewer_issue_signature: null,
|
|
151
152
|
reviewer_repeat_count: 0
|
|
152
|
-
}
|
|
153
|
+
};
|
|
154
|
+
if (pgTaskId) sessionInit.pg_task_id = pgTaskId;
|
|
155
|
+
if (pgProject) sessionInit.pg_project_id = pgProject;
|
|
156
|
+
const session = await createSession(sessionInit);
|
|
153
157
|
|
|
154
158
|
eventBase.sessionId = session.id;
|
|
155
159
|
|
|
@@ -179,6 +183,57 @@ export async function runFlow({ task, config, logger, flags = {}, emitter = null
|
|
|
179
183
|
if (triageResult.roleOverrides.testerEnabled !== undefined) testerEnabled = triageResult.roleOverrides.testerEnabled;
|
|
180
184
|
if (triageResult.roleOverrides.securityEnabled !== undefined) securityEnabled = triageResult.roleOverrides.securityEnabled;
|
|
181
185
|
stageResults.triage = triageResult.stageResult;
|
|
186
|
+
|
|
187
|
+
// --- PG decomposition: offer to create subtasks in Planning Game ---
|
|
188
|
+
const pgDecompose = triageResult.stageResult?.shouldDecompose
|
|
189
|
+
&& triageResult.stageResult.subtasks?.length > 1
|
|
190
|
+
&& pgTaskId
|
|
191
|
+
&& pgProject
|
|
192
|
+
&& config.planning_game?.enabled !== false
|
|
193
|
+
&& askQuestion;
|
|
194
|
+
|
|
195
|
+
if (pgDecompose) {
|
|
196
|
+
try {
|
|
197
|
+
const { buildDecompositionQuestion, createDecompositionSubtasks } = await import("./planning-game/decomposition.js");
|
|
198
|
+
const { createCard, relateCards, fetchCard } = await import("./planning-game/client.js");
|
|
199
|
+
|
|
200
|
+
const question = buildDecompositionQuestion(triageResult.stageResult.subtasks, pgTaskId);
|
|
201
|
+
const answer = await askQuestion(question);
|
|
202
|
+
|
|
203
|
+
if (answer && (answer.trim().toLowerCase() === "yes" || answer.trim().toLowerCase() === "sí" || answer.trim().toLowerCase() === "si")) {
|
|
204
|
+
const parentCard = await fetchCard({ projectId: pgProject, cardId: pgTaskId }).catch(() => null);
|
|
205
|
+
const createdSubtasks = await createDecompositionSubtasks({
|
|
206
|
+
client: { createCard, relateCards },
|
|
207
|
+
projectId: pgProject,
|
|
208
|
+
parentCardId: pgTaskId,
|
|
209
|
+
parentFirebaseId: parentCard?.firebaseId || null,
|
|
210
|
+
subtasks: triageResult.stageResult.subtasks,
|
|
211
|
+
epic: parentCard?.epic || null,
|
|
212
|
+
sprint: parentCard?.sprint || null,
|
|
213
|
+
codeveloper: config.planning_game?.codeveloper || null
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
stageResults.triage.pgSubtasks = createdSubtasks;
|
|
217
|
+
logger.info(`Planning Game: created ${createdSubtasks.length} subtasks from decomposition`);
|
|
218
|
+
|
|
219
|
+
emitProgress(
|
|
220
|
+
emitter,
|
|
221
|
+
makeEvent("pg:decompose", { ...eventBase, stage: "triage" }, {
|
|
222
|
+
message: `Created ${createdSubtasks.length} subtasks in Planning Game`,
|
|
223
|
+
detail: { subtasks: createdSubtasks.map((s) => ({ cardId: s.cardId, title: s.title })) }
|
|
224
|
+
})
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
await addCheckpoint(session, {
|
|
228
|
+
stage: "pg-decompose",
|
|
229
|
+
subtasksCreated: createdSubtasks.length,
|
|
230
|
+
cardIds: createdSubtasks.map((s) => s.cardId)
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
} catch (err) {
|
|
234
|
+
logger.warn(`Planning Game decomposition failed: ${err.message}`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
182
237
|
}
|
|
183
238
|
|
|
184
239
|
if (flags.enablePlanner !== undefined) plannerEnabled = Boolean(flags.enablePlanner);
|
|
@@ -198,8 +253,9 @@ export async function runFlow({ task, config, logger, flags = {}, emitter = null
|
|
|
198
253
|
|
|
199
254
|
// --- Planner ---
|
|
200
255
|
let plannedTask = task;
|
|
256
|
+
const triageDecomposition = stageResults.triage?.shouldDecompose ? stageResults.triage.subtasks : null;
|
|
201
257
|
if (plannerEnabled) {
|
|
202
|
-
const plannerResult = await runPlannerStage({ config, logger, emitter, eventBase, session, plannerRole, researchContext, trackBudget });
|
|
258
|
+
const plannerResult = await runPlannerStage({ config, logger, emitter, eventBase, session, plannerRole, researchContext, triageDecomposition, trackBudget });
|
|
203
259
|
plannedTask = plannerResult.plannedTask;
|
|
204
260
|
stageResults.planner = plannerResult.stageResult;
|
|
205
261
|
}
|
|
@@ -210,9 +266,65 @@ export async function runFlow({ task, config, logger, flags = {}, emitter = null
|
|
|
210
266
|
const { rules: reviewRules } = await resolveReviewProfile({ mode: config.review_mode, projectDir });
|
|
211
267
|
await coderRoleInstance.init();
|
|
212
268
|
|
|
269
|
+
const checkpointIntervalMs = (config.session.checkpoint_interval_minutes ?? 5) * 60 * 1000;
|
|
270
|
+
let lastCheckpointAt = Date.now();
|
|
271
|
+
let checkpointDisabled = false;
|
|
272
|
+
|
|
213
273
|
for (let i = 1; i <= config.max_iterations; i += 1) {
|
|
214
274
|
const elapsedMinutes = (Date.now() - startedAt) / 60000;
|
|
215
|
-
|
|
275
|
+
|
|
276
|
+
// --- Interactive checkpoint: pause and ask every N minutes ---
|
|
277
|
+
if (!checkpointDisabled && askQuestion && (Date.now() - lastCheckpointAt) >= checkpointIntervalMs) {
|
|
278
|
+
const elapsedStr = elapsedMinutes.toFixed(1);
|
|
279
|
+
const iterInfo = `${i - 1}/${config.max_iterations} iterations completed`;
|
|
280
|
+
const budgetInfo = budgetTracker.total().cost_usd > 0 ? ` | Budget: $${budgetTracker.total().cost_usd.toFixed(2)}` : "";
|
|
281
|
+
const stagesCompleted = Object.keys(stageResults).join(", ") || "none";
|
|
282
|
+
const checkpointMsg = `Checkpoint — ${elapsedStr} min elapsed | ${iterInfo}${budgetInfo} | Stages completed: ${stagesCompleted}. What would you like to do?`;
|
|
283
|
+
|
|
284
|
+
emitProgress(
|
|
285
|
+
emitter,
|
|
286
|
+
makeEvent("session:checkpoint", { ...eventBase, iteration: i, stage: "checkpoint" }, {
|
|
287
|
+
message: `Interactive checkpoint at ${elapsedStr} min`,
|
|
288
|
+
detail: { elapsed_minutes: Number(elapsedStr), iterations_done: i - 1, stages: stagesCompleted }
|
|
289
|
+
})
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
const answer = await askQuestion(
|
|
293
|
+
`${checkpointMsg}\n\nOptions:\n1. Continue 5 more minutes\n2. Continue until done (no more checkpoints)\n3. Continue for N minutes (reply with the number)\n4. Stop now`
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
await addCheckpoint(session, { stage: "interactive-checkpoint", elapsed_minutes: Number(elapsedStr), answer });
|
|
297
|
+
|
|
298
|
+
if (!answer || answer.trim() === "4" || answer.trim().toLowerCase().startsWith("stop")) {
|
|
299
|
+
await markSessionStatus(session, "stopped");
|
|
300
|
+
emitProgress(
|
|
301
|
+
emitter,
|
|
302
|
+
makeEvent("session:end", { ...eventBase, iteration: i, stage: "user-stop" }, {
|
|
303
|
+
status: "stopped",
|
|
304
|
+
message: "Session stopped by user at checkpoint",
|
|
305
|
+
detail: { approved: false, reason: "user_stopped", elapsed_minutes: Number(elapsedStr), budget: budgetSummary() }
|
|
306
|
+
})
|
|
307
|
+
);
|
|
308
|
+
return { approved: false, sessionId: session.id, reason: "user_stopped", elapsed_minutes: Number(elapsedStr) };
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (answer.trim() === "2" || answer.trim().toLowerCase().startsWith("continue until")) {
|
|
312
|
+
checkpointDisabled = true;
|
|
313
|
+
} else if (answer.trim() === "1" || answer.trim().toLowerCase().includes("5 m")) {
|
|
314
|
+
lastCheckpointAt = Date.now();
|
|
315
|
+
} else {
|
|
316
|
+
const customMinutes = parseInt(answer.trim().replace(/\D/g, ""), 10);
|
|
317
|
+
if (customMinutes > 0) {
|
|
318
|
+
lastCheckpointAt = Date.now();
|
|
319
|
+
config.session.checkpoint_interval_minutes = customMinutes;
|
|
320
|
+
} else {
|
|
321
|
+
lastCheckpointAt = Date.now();
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// --- Hard timeout: only when no askQuestion available ---
|
|
327
|
+
if (!askQuestion && elapsedMinutes > config.session.max_total_minutes) {
|
|
216
328
|
await markSessionStatus(session, "failed");
|
|
217
329
|
emitProgress(
|
|
218
330
|
emitter,
|
|
@@ -366,7 +478,7 @@ export async function runFlow({ task, config, logger, flags = {}, emitter = null
|
|
|
366
478
|
}
|
|
367
479
|
|
|
368
480
|
// --- All post-loop checks passed → finalize ---
|
|
369
|
-
const gitResult = await finalizeGitAutomation({ config, gitCtx, task, logger, session });
|
|
481
|
+
const gitResult = await finalizeGitAutomation({ config, gitCtx, task, logger, session, stageResults });
|
|
370
482
|
if (stageResults.planner?.ok) {
|
|
371
483
|
stageResults.planner.completedSteps = [...(stageResults.planner.steps || [])];
|
|
372
484
|
}
|
|
@@ -89,3 +89,31 @@ export async function updateCard({ projectId, cardId, firebaseId, updates, timeo
|
|
|
89
89
|
);
|
|
90
90
|
return parseJsonResponse(response);
|
|
91
91
|
}
|
|
92
|
+
|
|
93
|
+
export async function createCard({ projectId, card, timeoutMs = DEFAULT_TIMEOUT_MS }) {
|
|
94
|
+
const url = `${getApiUrl()}/projects/${encodeURIComponent(projectId)}/cards`;
|
|
95
|
+
const response = await fetchWithRetry(
|
|
96
|
+
url,
|
|
97
|
+
{
|
|
98
|
+
method: "POST",
|
|
99
|
+
headers: { "Content-Type": "application/json" },
|
|
100
|
+
body: JSON.stringify(card)
|
|
101
|
+
},
|
|
102
|
+
timeoutMs
|
|
103
|
+
);
|
|
104
|
+
return parseJsonResponse(response);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export async function relateCards({ projectId, sourceCardId, targetCardId, relationType, timeoutMs = DEFAULT_TIMEOUT_MS }) {
|
|
108
|
+
const url = `${getApiUrl()}/projects/${encodeURIComponent(projectId)}/cards/relate`;
|
|
109
|
+
const response = await fetchWithRetry(
|
|
110
|
+
url,
|
|
111
|
+
{
|
|
112
|
+
method: "POST",
|
|
113
|
+
headers: { "Content-Type": "application/json" },
|
|
114
|
+
body: JSON.stringify({ sourceCardId, targetCardId, relationType })
|
|
115
|
+
},
|
|
116
|
+
timeoutMs
|
|
117
|
+
);
|
|
118
|
+
return parseJsonResponse(response);
|
|
119
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Planning Game decomposition.
|
|
3
|
+
* Creates subtask cards in PG with blocks/blockedBy chain when triage
|
|
4
|
+
* recommends decomposition and user accepts.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export async function createDecompositionSubtasks({
|
|
8
|
+
client,
|
|
9
|
+
projectId,
|
|
10
|
+
parentCardId,
|
|
11
|
+
parentFirebaseId,
|
|
12
|
+
subtasks,
|
|
13
|
+
epic,
|
|
14
|
+
sprint,
|
|
15
|
+
codeveloper
|
|
16
|
+
}) {
|
|
17
|
+
if (!subtasks?.length || subtasks.length < 2) return [];
|
|
18
|
+
|
|
19
|
+
const created = [];
|
|
20
|
+
|
|
21
|
+
for (let i = 0; i < subtasks.length; i++) {
|
|
22
|
+
const card = {
|
|
23
|
+
type: "task",
|
|
24
|
+
title: subtasks[i],
|
|
25
|
+
descriptionStructured: [{
|
|
26
|
+
role: "developer",
|
|
27
|
+
goal: subtasks[i],
|
|
28
|
+
benefit: `Part ${i + 1}/${subtasks.length} of decomposed task ${parentCardId}`
|
|
29
|
+
}],
|
|
30
|
+
acceptanceCriteria: `Subtask ${i + 1} of ${parentCardId}: ${subtasks[i]}`
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
if (epic) card.epic = epic;
|
|
34
|
+
if (sprint) card.sprint = sprint;
|
|
35
|
+
if (codeveloper) card.codeveloper = codeveloper;
|
|
36
|
+
|
|
37
|
+
const result = await client.createCard({ projectId, card });
|
|
38
|
+
created.push({
|
|
39
|
+
cardId: result.cardId,
|
|
40
|
+
firebaseId: result.firebaseId,
|
|
41
|
+
title: subtasks[i],
|
|
42
|
+
index: i
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Chain blocks/blockedBy relationships: card[0] blocks card[1], card[1] blocks card[2], etc.
|
|
47
|
+
for (let i = 0; i < created.length - 1; i++) {
|
|
48
|
+
await client.relateCards({
|
|
49
|
+
projectId,
|
|
50
|
+
sourceCardId: created[i].cardId,
|
|
51
|
+
targetCardId: created[i + 1].cardId,
|
|
52
|
+
relationType: "blocks"
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Relate all subtasks to parent
|
|
57
|
+
for (const sub of created) {
|
|
58
|
+
await client.relateCards({
|
|
59
|
+
projectId,
|
|
60
|
+
sourceCardId: parentCardId,
|
|
61
|
+
targetCardId: sub.cardId,
|
|
62
|
+
relationType: "related"
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return created;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function buildDecompositionQuestion(subtasks, parentCardId) {
|
|
70
|
+
const lines = [
|
|
71
|
+
`Triage recommends decomposing this task into ${subtasks.length} subtasks:`,
|
|
72
|
+
""
|
|
73
|
+
];
|
|
74
|
+
for (let i = 0; i < subtasks.length; i++) {
|
|
75
|
+
lines.push(`${i + 1}. ${subtasks[i]}`);
|
|
76
|
+
}
|
|
77
|
+
lines.push("");
|
|
78
|
+
lines.push(`Create these as linked cards in Planning Game (parent: ${parentCardId})?`);
|
|
79
|
+
lines.push("Each subtask will block the next one (sequential chain).");
|
|
80
|
+
lines.push("");
|
|
81
|
+
lines.push("Reply: yes / no");
|
|
82
|
+
return lines.join("\n");
|
|
83
|
+
}
|
|
@@ -9,7 +9,7 @@ function resolveProvider(config) {
|
|
|
9
9
|
);
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
function buildPrompt({ task, instructions, research }) {
|
|
12
|
+
function buildPrompt({ task, instructions, research, triageDecomposition }) {
|
|
13
13
|
const sections = [];
|
|
14
14
|
|
|
15
15
|
if (instructions) {
|
|
@@ -21,6 +21,17 @@ function buildPrompt({ task, instructions, research }) {
|
|
|
21
21
|
sections.push("Return concise numbered steps focused on execution order and risk.");
|
|
22
22
|
sections.push("");
|
|
23
23
|
|
|
24
|
+
if (triageDecomposition?.length) {
|
|
25
|
+
sections.push("## Triage decomposition recommendation");
|
|
26
|
+
sections.push("The triage stage determined this task should be decomposed. Suggested subtasks:");
|
|
27
|
+
for (let i = 0; i < triageDecomposition.length; i++) {
|
|
28
|
+
sections.push(`${i + 1}. ${triageDecomposition[i]}`);
|
|
29
|
+
}
|
|
30
|
+
sections.push("");
|
|
31
|
+
sections.push("Focus your plan on the FIRST subtask only. List the remaining subtasks as 'pending_subtasks' in your output for documentation.");
|
|
32
|
+
sections.push("");
|
|
33
|
+
}
|
|
34
|
+
|
|
24
35
|
if (research) {
|
|
25
36
|
sections.push("## Research findings");
|
|
26
37
|
if (research.affected_files?.length) {
|
|
@@ -56,10 +67,11 @@ export class PlannerRole extends BaseRole {
|
|
|
56
67
|
async execute(input) {
|
|
57
68
|
const task = input || this.context?.task || "";
|
|
58
69
|
const research = this.context?.research || null;
|
|
70
|
+
const triageDecomposition = this.context?.triageDecomposition || null;
|
|
59
71
|
const provider = resolveProvider(this.config);
|
|
60
72
|
|
|
61
73
|
const agent = this._createAgent(provider, this.config, this.logger);
|
|
62
|
-
const prompt = buildPrompt({ task, instructions: this.instructions, research });
|
|
74
|
+
const prompt = buildPrompt({ task, instructions: this.instructions, research, triageDecomposition });
|
|
63
75
|
|
|
64
76
|
const result = await agent.runTask({ prompt, role: "planner" });
|
|
65
77
|
|
package/src/roles/triage-role.js
CHANGED
|
@@ -26,10 +26,10 @@ function buildPrompt({ task, instructions }) {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
sections.push(
|
|
29
|
-
"Classify the task complexity
|
|
29
|
+
"Classify the task complexity, recommend only the necessary pipeline roles, and assess whether the task should be decomposed into smaller subtasks.",
|
|
30
30
|
"Keep the reasoning short and practical.",
|
|
31
31
|
"Return a single valid JSON object and nothing else.",
|
|
32
|
-
'JSON schema: {"level":"trivial|simple|medium|complex","roles":["planner|researcher|refactorer|reviewer|tester|security"],"reasoning":string}'
|
|
32
|
+
'JSON schema: {"level":"trivial|simple|medium|complex","roles":["planner|researcher|refactorer|reviewer|tester|security"],"reasoning":string,"shouldDecompose":boolean,"subtasks":string[]}'
|
|
33
33
|
);
|
|
34
34
|
|
|
35
35
|
sections.push(`## Task\n${task}`);
|
|
@@ -49,6 +49,14 @@ function normalizeRoles(roles) {
|
|
|
49
49
|
return Array.from(new Set(roles.filter((role) => VALID_ROLES.has(role))));
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
function normalizeSubtasks(subtasks) {
|
|
53
|
+
if (!Array.isArray(subtasks)) return [];
|
|
54
|
+
return subtasks
|
|
55
|
+
.map((s) => (typeof s === "string" ? s.trim() : ""))
|
|
56
|
+
.filter(Boolean)
|
|
57
|
+
.slice(0, 5);
|
|
58
|
+
}
|
|
59
|
+
|
|
52
60
|
export class TriageRole extends BaseRole {
|
|
53
61
|
constructor({ config, logger, emitter = null, createAgentFn = null }) {
|
|
54
62
|
super({ name: "triage", config, logger, emitter });
|
|
@@ -98,16 +106,28 @@ export class TriageRole extends BaseRole {
|
|
|
98
106
|
const level = VALID_LEVELS.has(parsed.level) ? parsed.level : "medium";
|
|
99
107
|
const roles = normalizeRoles(parsed.roles);
|
|
100
108
|
const reasoning = String(parsed.reasoning || "").trim() || "No reasoning provided.";
|
|
109
|
+
const shouldDecompose = Boolean(parsed.shouldDecompose);
|
|
110
|
+
const subtasks = normalizeSubtasks(parsed.subtasks);
|
|
111
|
+
|
|
112
|
+
const triageResult = {
|
|
113
|
+
level,
|
|
114
|
+
roles,
|
|
115
|
+
reasoning,
|
|
116
|
+
provider
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
if (shouldDecompose && subtasks.length > 0) {
|
|
120
|
+
triageResult.shouldDecompose = true;
|
|
121
|
+
triageResult.subtasks = subtasks;
|
|
122
|
+
} else {
|
|
123
|
+
triageResult.shouldDecompose = false;
|
|
124
|
+
}
|
|
101
125
|
|
|
126
|
+
const decomposeNote = shouldDecompose ? " — decomposition recommended" : "";
|
|
102
127
|
return {
|
|
103
128
|
ok: true,
|
|
104
|
-
result:
|
|
105
|
-
|
|
106
|
-
roles,
|
|
107
|
-
reasoning,
|
|
108
|
-
provider
|
|
109
|
-
},
|
|
110
|
-
summary: `Triage: ${level} (${roles.length} role${roles.length === 1 ? "" : "s"})`,
|
|
129
|
+
result: triageResult,
|
|
130
|
+
summary: `Triage: ${level} (${roles.length} role${roles.length === 1 ? "" : "s"})${decomposeNote}`,
|
|
111
131
|
usage: result.usage
|
|
112
132
|
};
|
|
113
133
|
} catch {
|
package/src/utils/process.js
CHANGED
|
@@ -7,6 +7,20 @@ export async function runCommand(command, args = [], options = {}) {
|
|
|
7
7
|
...rest
|
|
8
8
|
});
|
|
9
9
|
|
|
10
|
+
let stdoutAccum = "";
|
|
11
|
+
let stderrAccum = "";
|
|
12
|
+
|
|
13
|
+
if (subprocess.stdout) {
|
|
14
|
+
subprocess.stdout.on("data", (chunk) => {
|
|
15
|
+
stdoutAccum += chunk.toString();
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
if (subprocess.stderr) {
|
|
19
|
+
subprocess.stderr.on("data", (chunk) => {
|
|
20
|
+
stderrAccum += chunk.toString();
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
10
24
|
if (onOutput) {
|
|
11
25
|
const handler = (stream) => {
|
|
12
26
|
let partial = "";
|
|
@@ -25,7 +39,8 @@ export async function runCommand(command, args = [], options = {}) {
|
|
|
25
39
|
|
|
26
40
|
try {
|
|
27
41
|
if (!timeout) {
|
|
28
|
-
|
|
42
|
+
const result = await subprocess;
|
|
43
|
+
return enrichResult(result, stdoutAccum, stderrAccum);
|
|
29
44
|
}
|
|
30
45
|
|
|
31
46
|
let timer = null;
|
|
@@ -38,15 +53,17 @@ export async function runCommand(command, args = [], options = {}) {
|
|
|
38
53
|
}
|
|
39
54
|
resolve({
|
|
40
55
|
exitCode: 143,
|
|
41
|
-
stdout:
|
|
42
|
-
stderr: `Command timed out after ${timeout}ms
|
|
56
|
+
stdout: stdoutAccum,
|
|
57
|
+
stderr: `Command timed out after ${timeout}ms`,
|
|
58
|
+
timedOut: true,
|
|
59
|
+
signal: "SIGKILL"
|
|
43
60
|
});
|
|
44
61
|
}, timeout);
|
|
45
62
|
});
|
|
46
63
|
|
|
47
64
|
const result = await Promise.race([subprocess, timeoutResult]);
|
|
48
65
|
if (timer) clearTimeout(timer);
|
|
49
|
-
return result;
|
|
66
|
+
return enrichResult(result, stdoutAccum, stderrAccum);
|
|
50
67
|
} catch (error) {
|
|
51
68
|
const details = [
|
|
52
69
|
error?.shortMessage,
|
|
@@ -60,8 +77,29 @@ export async function runCommand(command, args = [], options = {}) {
|
|
|
60
77
|
|
|
61
78
|
return {
|
|
62
79
|
exitCode: 1,
|
|
63
|
-
stdout: error?.stdout ||
|
|
64
|
-
stderr: details || String(error)
|
|
80
|
+
stdout: error?.stdout || stdoutAccum,
|
|
81
|
+
stderr: details || String(error),
|
|
82
|
+
signal: error?.signal || null
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function enrichResult(result, stdoutAccum, stderrAccum) {
|
|
88
|
+
if (result.timedOut) return result;
|
|
89
|
+
|
|
90
|
+
const killed = result.killed || !!result.signal;
|
|
91
|
+
const signal = result.signal || null;
|
|
92
|
+
|
|
93
|
+
if (killed && !result.stderr) {
|
|
94
|
+
return {
|
|
95
|
+
...result,
|
|
96
|
+
stdout: result.stdout || stdoutAccum,
|
|
97
|
+
stderr: signal
|
|
98
|
+
? `Process killed by signal ${signal}`
|
|
99
|
+
: "Process was killed externally",
|
|
100
|
+
signal
|
|
65
101
|
};
|
|
66
102
|
}
|
|
103
|
+
|
|
104
|
+
return result;
|
|
67
105
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
You are the **Triage** role in a multi-role AI pipeline.
|
|
2
2
|
|
|
3
|
-
Your job is to quickly classify task complexity
|
|
3
|
+
Your job is to quickly classify task complexity, activate only the necessary roles, and assess whether the task should be decomposed into smaller subtasks before execution.
|
|
4
4
|
|
|
5
5
|
## Output format
|
|
6
6
|
Return a single valid JSON object and nothing else:
|
|
@@ -9,7 +9,9 @@ Return a single valid JSON object and nothing else:
|
|
|
9
9
|
{
|
|
10
10
|
"level": "trivial|simple|medium|complex",
|
|
11
11
|
"roles": ["planner", "researcher", "refactorer", "reviewer", "tester", "security"],
|
|
12
|
-
"reasoning": "brief practical justification"
|
|
12
|
+
"reasoning": "brief practical justification",
|
|
13
|
+
"shouldDecompose": false,
|
|
14
|
+
"subtasks": []
|
|
13
15
|
}
|
|
14
16
|
```
|
|
15
17
|
|
|
@@ -19,7 +21,20 @@ Return a single valid JSON object and nothing else:
|
|
|
19
21
|
- `medium`: moderate scope/risk. Reviewer required; optional planner/researcher.
|
|
20
22
|
- `complex`: high scope/risk, architecture or security/testing impact. Full pipeline.
|
|
21
23
|
|
|
24
|
+
## Decomposition guidance
|
|
25
|
+
Analyze whether the task is too large for a single agent iteration. Set `shouldDecompose: true` when ANY of these apply:
|
|
26
|
+
- The task touches more than 3 unrelated areas of the codebase.
|
|
27
|
+
- It requires both architectural changes AND feature implementation.
|
|
28
|
+
- It combines multiple independent features or fixes in one request.
|
|
29
|
+
- It would likely require more than ~200 lines of changes across many files.
|
|
30
|
+
- It mixes refactoring with new functionality.
|
|
31
|
+
|
|
32
|
+
When `shouldDecompose` is true, provide `subtasks`: an array of 2-5 short strings, each describing one focused, independently deliverable piece of work. Order them by dependency (do first → do last).
|
|
33
|
+
|
|
34
|
+
When `shouldDecompose` is false, `subtasks` must be an empty array.
|
|
35
|
+
|
|
22
36
|
## Rules
|
|
23
37
|
- Keep `reasoning` short.
|
|
24
38
|
- Recommend only roles that add clear value.
|
|
25
39
|
- Do not include `coder` or `sonar` in `roles` (they are always active).
|
|
40
|
+
- Subtask descriptions should be actionable and specific, not vague.
|