karajan-code 2.14.3 → 2.15.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 +1 -1
- package/package.json +1 -1
- package/src/brain/agent-error-classifier.js +101 -0
- package/src/brain/standby-scheduler.js +92 -0
- package/src/brain/standby-store.js +129 -0
- package/src/brain/with-brain-recovery.js +178 -0
- package/src/cli/register-plan.js +29 -0
- package/src/cli/register-standby.js +26 -0
- package/src/cli.js +2 -0
- package/src/commands/architect.js +6 -2
- package/src/commands/discover.js +6 -2
- package/src/commands/init.js +47 -0
- package/src/commands/plan/_shared.js +19 -4
- package/src/commands/plan/fix.js +99 -0
- package/src/commands/plan/generate.js +112 -27
- package/src/commands/plan/index.js +2 -0
- package/src/commands/plan/migrate-result.js +41 -0
- package/src/commands/plan.js +2 -0
- package/src/commands/researcher.js +6 -2
- package/src/commands/run.js +33 -1
- package/src/commands/standby.js +114 -0
- package/src/commands/triage.js +6 -2
- package/src/config/schema.js +40 -0
- package/src/git/hu-snapshot.js +105 -0
- package/src/guards/policy-resolver.js +38 -1
- package/src/hu/lazy-planner.js +6 -1
- package/src/hu/model-router.js +108 -0
- package/src/hu/splitting-generator.js +6 -1
- package/src/orchestrator/drivers/pre-loop-phases/inject-loaded-plan.js +47 -8
- package/src/orchestrator/drivers/run-hu-batch.js +30 -4
- package/src/plan/plan-executor.js +13 -4
- package/src/plan/plan-fixer.js +36 -4
- package/src/plan/plan-hu-ops.js +24 -1
- package/src/plan/plan-id.js +20 -0
- package/src/plan/plan-migrate-result.js +24 -0
- package/src/plan/plan-reviewer.js +40 -3
- package/src/plan/plan-schema.js +128 -1
- package/src/plan/plan-store.js +63 -8
- package/src/plan/plan-structural-pass.js +122 -0
- package/src/plan/plan-validation.js +82 -0
- package/src/plan/tests-synthesizer.js +8 -3
- package/src/prompts/planner.js +27 -1
- package/src/roles/agent-role.js +18 -3
- package/src/roles/planner-role.js +10 -3
- package/src/roles/solomon-role.js +10 -4
- package/src/utils/cli-progress.js +58 -1
- package/src/utils/garbage-collector.js +85 -2
- package/src/utils/run-registry.js +115 -0
package/src/commands/init.js
CHANGED
|
@@ -127,6 +127,52 @@ async function askPerRoleProviders(wizard, available, config, { coder, reviewer
|
|
|
127
127
|
}
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
+
/**
|
|
131
|
+
* KJC-TSK-0415: Plan B — fallback chain. Cuando el provider primario se
|
|
132
|
+
* queda sin créditos (QUOTA_EXHAUSTED_DAILY/MONTHLY) y el cooldown excede
|
|
133
|
+
* max_wait_hours, Brain switchea al fallback en vez de hibernar.
|
|
134
|
+
*
|
|
135
|
+
* Solo pregunta si hay >= 2 providers disponibles (sin alternativas reales
|
|
136
|
+
* no tiene sentido el fallback).
|
|
137
|
+
*/
|
|
138
|
+
async function askFallbackChain(wizard, available, config, { coder, reviewer }, logger) {
|
|
139
|
+
if (available.length < 2) return;
|
|
140
|
+
|
|
141
|
+
logger.info("");
|
|
142
|
+
logger.info("Plan B — fallback automático cuando un provider se queda sin créditos:");
|
|
143
|
+
const enable = await wizard.confirm(
|
|
144
|
+
" ¿Configurar fallback? (default: 12h; si el cooldown supera ese tiempo, switch al provider alternativo)",
|
|
145
|
+
true
|
|
146
|
+
);
|
|
147
|
+
if (!enable) return;
|
|
148
|
+
|
|
149
|
+
const maxHoursRaw = await wizard.input(" Máximo tiempo de espera antes de fallback (horas):", "12");
|
|
150
|
+
const maxHours = Number.parseFloat(maxHoursRaw) || 12;
|
|
151
|
+
|
|
152
|
+
config.roles = config.roles || {};
|
|
153
|
+
const KEY_ROLES = [
|
|
154
|
+
{ key: "coder", label: "coder", primary: coder },
|
|
155
|
+
{ key: "reviewer", label: "reviewer", primary: reviewer },
|
|
156
|
+
{ key: "planner", label: "planner", primary: coder },
|
|
157
|
+
];
|
|
158
|
+
for (const role of KEY_ROLES) {
|
|
159
|
+
config.roles[role.key] = config.roles[role.key] || {};
|
|
160
|
+
const opts = [{ label: "Sin fallback (hibernar)", value: "__none__", available: true }];
|
|
161
|
+
for (const a of available) {
|
|
162
|
+
if (a.name === role.primary) continue;
|
|
163
|
+
opts.push({ label: a.name, value: a.name, available: true });
|
|
164
|
+
}
|
|
165
|
+
const choice = await wizard.select(` Fallback de ${role.label} cuando ${role.primary} se quede sin créditos:`, opts);
|
|
166
|
+
if (choice === "__none__") {
|
|
167
|
+
delete config.roles[role.key].fallback;
|
|
168
|
+
logger.info(` -> ${role.key}: sin fallback (hibernará)`);
|
|
169
|
+
} else {
|
|
170
|
+
config.roles[role.key].fallback = { provider: choice, max_wait_hours: maxHours };
|
|
171
|
+
logger.info(` -> ${role.key}: ${role.primary} → ${choice} (tras ${maxHours}h)`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
130
176
|
async function askMethodology(wizard, config, logger) {
|
|
131
177
|
const methodology = await wizard.select("Development methodology:", [
|
|
132
178
|
{ label: "TDD (test-driven development)", value: "tdd", available: true },
|
|
@@ -256,6 +302,7 @@ async function runWizard(config, logger) {
|
|
|
256
302
|
logger.info(` -> HU Board: ${enableHuBoard ? "enabled (auto-start on kj run)" : "disabled"}`);
|
|
257
303
|
|
|
258
304
|
await askPerRoleProviders(wizard, available, config, { coder, reviewer }, logger);
|
|
305
|
+
await askFallbackChain(wizard, available, config, { coder, reviewer }, logger);
|
|
259
306
|
await askMethodology(wizard, config, logger);
|
|
260
307
|
await askLanguages(wizard, config, logger);
|
|
261
308
|
|
|
@@ -25,16 +25,31 @@ export function formatPlan(plan) {
|
|
|
25
25
|
return lines.join("\n");
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Convierte el id largo de una dep `hu_<planId>_<NNN>` al short_id de
|
|
30
|
+
* la HU correspondiente cuando lo conoce. Mantiene el id largo si la
|
|
31
|
+
* dep no tiene short_id (HU legacy pre-humanización).
|
|
32
|
+
*/
|
|
33
|
+
function displayDep(depId, hus) {
|
|
34
|
+
const hu = hus.find((h) => h.id === depId);
|
|
35
|
+
if (hu?.short_id) return hu.short_id;
|
|
36
|
+
return depId;
|
|
37
|
+
}
|
|
38
|
+
|
|
28
39
|
export function formatHuTable(hus) {
|
|
29
40
|
if (!hus?.length) return " (no HUs)";
|
|
41
|
+
// Humanización IDs: si la HU tiene short_id ("INFRA-001"), úsalo
|
|
42
|
+
// como columna ID; si no, fallback al id largo (HU legacy).
|
|
43
|
+
const idOf = (h) => h.short_id || h.id;
|
|
30
44
|
const lines = [];
|
|
45
|
+
const maxId = Math.min(20, Math.max(...hus.map((h) => idOf(h).length || 0)));
|
|
31
46
|
const maxTitle = Math.min(50, Math.max(...hus.map(h => h.title?.length || 0)));
|
|
32
|
-
lines.push(` ${"ID".padEnd(
|
|
33
|
-
lines.push(` ${"─".repeat(
|
|
47
|
+
lines.push(` ${"ID".padEnd(maxId)} ${"Title".padEnd(maxTitle)} ${"Type".padEnd(10)} ${"Status".padEnd(10)} Deps`);
|
|
48
|
+
lines.push(` ${"─".repeat(maxId)} ${"─".repeat(maxTitle)} ${"─".repeat(10)} ${"─".repeat(10)} ${"─".repeat(15)}`);
|
|
34
49
|
for (const hu of hus) {
|
|
35
50
|
const title = (hu.title || "").slice(0, maxTitle).padEnd(maxTitle);
|
|
36
|
-
const deps = (hu.blocked_by || []).join(", ") || "—";
|
|
37
|
-
lines.push(` ${hu.
|
|
51
|
+
const deps = (hu.blocked_by || []).map((d) => displayDep(d, hus)).join(", ") || "—";
|
|
52
|
+
lines.push(` ${idOf(hu).padEnd(maxId)} ${title} ${(hu.task_type || "sw").padEnd(10)} ${(hu.status || "?").padEnd(10)} ${deps}`);
|
|
38
53
|
}
|
|
39
54
|
return lines.join("\n");
|
|
40
55
|
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// KJC-TSK-0402: relanzar el self-fix loop sobre un plan ya generado,
|
|
2
|
+
// opcionalmente con feedback humano que guía al reviewer hacia los
|
|
3
|
+
// puntos débiles que la primera pasada no detectó. NO regenera desde
|
|
4
|
+
// cero — preserva el trabajo previo.
|
|
5
|
+
|
|
6
|
+
import { createAgent } from "../../agents/index.js";
|
|
7
|
+
import { assertAgentsAvailable } from "../../agents/availability.js";
|
|
8
|
+
import { resolveRole } from "../../config.js";
|
|
9
|
+
import { createCliProgressReporter } from "../../utils/cli-progress.js";
|
|
10
|
+
import { reviewPlan, findingsCount, findingsCountByType } from "../../plan/plan-reviewer.js";
|
|
11
|
+
import { applyReviewerFeedback, applyFixerPatch } from "../../plan/plan-fixer.js";
|
|
12
|
+
import { runStructuralPass } from "../../plan/plan-structural-pass.js";
|
|
13
|
+
import { loadPlan, savePlan } from "../../plan/plan-store.js";
|
|
14
|
+
|
|
15
|
+
export async function planFixCommand({ config, planId, prompt, logger, json }) {
|
|
16
|
+
const projectDir = config.projectDir || process.cwd();
|
|
17
|
+
const plan = await loadPlan(projectDir, planId);
|
|
18
|
+
if (!plan) throw new Error(`Plan no encontrado: ${planId}`);
|
|
19
|
+
if (!Array.isArray(plan.hus) || plan.hus.length === 0) {
|
|
20
|
+
throw new Error(`Plan ${planId} no tiene HUs.`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const before = { hus: plan.hus.length, deps: plan.hus.reduce((a, h) => a + (h.blocked_by?.length || 0), 0) };
|
|
24
|
+
|
|
25
|
+
const plannerRole = resolveRole(config, "planner");
|
|
26
|
+
await assertAgentsAvailable([plannerRole.provider]);
|
|
27
|
+
const planner = createAgent(plannerRole.provider, config, logger);
|
|
28
|
+
const silenceTimeoutMs = Number(config?.session?.max_agent_silence_minutes) > 0
|
|
29
|
+
? Math.round(Number(config.session.max_agent_silence_minutes) * 60 * 1000) : undefined;
|
|
30
|
+
const timeoutMs = Number(config?.session?.max_planner_minutes) > 0
|
|
31
|
+
? Math.round(Number(config.session.max_planner_minutes) * 60 * 1000) : undefined;
|
|
32
|
+
|
|
33
|
+
// Reviewer con prompt opcional del usuario inyectado como contexto extra.
|
|
34
|
+
// El prompt se prepende al task original para que el reviewer LLM lo lea
|
|
35
|
+
// antes de la HU list.
|
|
36
|
+
const enrichedTask = prompt
|
|
37
|
+
? `${plan.task || ""}\n\n## USER FEEDBACK (priorizar estos puntos)\n${prompt}\n`
|
|
38
|
+
: plan.task || "";
|
|
39
|
+
|
|
40
|
+
const reviewProgress = createCliProgressReporter({ role: "plan-fix:reviewer", quiet: Boolean(json) });
|
|
41
|
+
const review = await reviewPlan({
|
|
42
|
+
agent: planner, task: enrichedTask, hus: plan.hus,
|
|
43
|
+
onOutput: reviewProgress.onOutput, silenceTimeoutMs, timeoutMs,
|
|
44
|
+
});
|
|
45
|
+
reviewProgress.finish(review.ok ? "done" : "failed");
|
|
46
|
+
if (!review.ok) throw new Error(`Reviewer falló: ${review.error}`);
|
|
47
|
+
plan.review = review.findings;
|
|
48
|
+
let count = findingsCount(review.findings);
|
|
49
|
+
if (!json) process.stderr.write(`Reviewer encontró ${count} item(s).\n`);
|
|
50
|
+
|
|
51
|
+
// Self-fix loop con smart convergence guard (mismo que generate.js).
|
|
52
|
+
const maxIter = 4;
|
|
53
|
+
for (let i = 1; i <= maxIter && count > 0; i++) {
|
|
54
|
+
const husSnapshot = JSON.parse(JSON.stringify(plan.hus));
|
|
55
|
+
const reviewSnapshot = JSON.parse(JSON.stringify(plan.review));
|
|
56
|
+
const fixProgress = createCliProgressReporter({ role: `plan-fix:iter ${i}`, quiet: Boolean(json) });
|
|
57
|
+
const fix = await applyReviewerFeedback({
|
|
58
|
+
agent: planner, task: enrichedTask, hus: plan.hus, findings: plan.review,
|
|
59
|
+
onOutput: fixProgress.onOutput, silenceTimeoutMs, timeoutMs,
|
|
60
|
+
});
|
|
61
|
+
if (!fix.ok) { fixProgress.finish("failed"); break; }
|
|
62
|
+
const ops = applyFixerPatch(plan, fix.patch);
|
|
63
|
+
if (ops.added + ops.depsAdded + ops.deleted === 0) { fixProgress.finish("done"); break; }
|
|
64
|
+
const review2 = await reviewPlan({
|
|
65
|
+
agent: planner, task: enrichedTask, hus: plan.hus,
|
|
66
|
+
onOutput: fixProgress.onOutput, silenceTimeoutMs, timeoutMs,
|
|
67
|
+
});
|
|
68
|
+
if (!review2.ok) { plan.hus = husSnapshot; plan.review = reviewSnapshot; fixProgress.finish("failed"); break; }
|
|
69
|
+
const b = findingsCountByType(reviewSnapshot);
|
|
70
|
+
const a = findingsCountByType(review2.findings);
|
|
71
|
+
const accept = a.priority < b.priority || (a.priority === b.priority && a.total <= b.total);
|
|
72
|
+
if (!accept) { plan.hus = husSnapshot; plan.review = reviewSnapshot; fixProgress.finish("reverted"); break; }
|
|
73
|
+
plan.review = review2.findings;
|
|
74
|
+
count = a.total;
|
|
75
|
+
fixProgress.finish("done");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Structural integrity pass determinístico al final.
|
|
79
|
+
const { fixes: structuralFixes } = runStructuralPass(plan);
|
|
80
|
+
if (structuralFixes.length > 0) {
|
|
81
|
+
if (!plan.review) plan.review = {};
|
|
82
|
+
plan.review.structural_fixes = structuralFixes;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
await savePlan(projectDir, plan);
|
|
86
|
+
const after = { hus: plan.hus.length, deps: plan.hus.reduce((a, h) => a + (h.blocked_by?.length || 0), 0) };
|
|
87
|
+
|
|
88
|
+
if (json) {
|
|
89
|
+
console.log(JSON.stringify({ planId, before, after, structuralFixes }, null, 2));
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const diffHus = after.hus - before.hus;
|
|
93
|
+
const diffDeps = after.deps - before.deps;
|
|
94
|
+
const structuralSummary = structuralFixes.length > 0
|
|
95
|
+
? `, ${structuralFixes.length} structural fix(es)` : "";
|
|
96
|
+
console.log(
|
|
97
|
+
`kj plan fix ${planId}: ${diffHus >= 0 ? "+" : ""}${diffHus} HUs, ${diffDeps >= 0 ? "+" : ""}${diffDeps} deps${structuralSummary}. Findings: ${count} restantes.`
|
|
98
|
+
);
|
|
99
|
+
}
|
|
@@ -8,6 +8,9 @@ import { runAutoGC, summarizeGC } from "../../utils/garbage-collector.js";
|
|
|
8
8
|
import { promptProjectName } from "../../utils/prompt-project-name.js";
|
|
9
9
|
import { deriveProjectNameFromCwd } from "../../utils/derive-project-name-from-cwd.js";
|
|
10
10
|
import { applyReviewerFeedback, applyFixerPatch } from "../../plan/plan-fixer.js";
|
|
11
|
+
import { runStructuralPass } from "../../plan/plan-structural-pass.js";
|
|
12
|
+
import { recommendModelsForHu, complexityFromTaskType } from "../../hu/model-router.js";
|
|
13
|
+
import { withBrainRecovery } from "../../brain/with-brain-recovery.js";
|
|
11
14
|
import { formatPlan, formatHuTable } from "./_shared.js";
|
|
12
15
|
|
|
13
16
|
/**
|
|
@@ -51,14 +54,23 @@ async function planGenerateImpl({ task, config, logger, json, context, runLog, f
|
|
|
51
54
|
// Live progress to stderr so the user sees what the planner is doing
|
|
52
55
|
// instead of staring at a silent shell for 30 s – 3 min. `--json` mode
|
|
53
56
|
// stays quiet so stdout remains a pure JSON document.
|
|
54
|
-
|
|
57
|
+
// Sub-progress por fase (KJC outputs limpios): cada llamada al
|
|
58
|
+
// agente arranca su propio "[planner:fase] starting → done" para
|
|
59
|
+
// que el usuario no piense que "[planner] done" significa "el
|
|
60
|
+
// comando entero terminó" — sigue habiendo tests-synth, reviewer,
|
|
61
|
+
// self-fix iter N por delante.
|
|
62
|
+
const progress = createCliProgressReporter({ role: "planner:initial", quiet: Boolean(json) });
|
|
55
63
|
let result;
|
|
56
64
|
try {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
65
|
+
// KJC-TSK-0413: planner inicial pasa por Brain Recovery — clasifica
|
|
66
|
+
// 401/429/5xx/SILENCED/etc y decide retry/standby/hibernate/abort.
|
|
67
|
+
result = await withBrainRecovery({
|
|
68
|
+
agent: planner,
|
|
69
|
+
taskArgs: { prompt, role: "planner", silenceTimeoutMs, timeoutMs, onOutput: progress.onOutput },
|
|
70
|
+
role: "planner",
|
|
71
|
+
logger,
|
|
60
72
|
});
|
|
61
|
-
progress.finish(result.ok ? "done" : "failed");
|
|
73
|
+
progress.finish(result.ok ? "done" : (result.action || "failed"));
|
|
62
74
|
} catch (err) {
|
|
63
75
|
progress.finish("failed");
|
|
64
76
|
throw err;
|
|
@@ -124,19 +136,38 @@ async function planGenerateImpl({ task, config, logger, json, context, runLog, f
|
|
|
124
136
|
const rawTests = typeof step === "object" ? step.acceptance_tests : null;
|
|
125
137
|
const tests = normaliseAcceptanceTests(rawTests);
|
|
126
138
|
if (tests.length === 0) stepsMissingTests += 1;
|
|
139
|
+
// Humanización IDs: preservar el id simbólico del planner ("INFRA-001",
|
|
140
|
+
// "AUTH-SIGNUP", etc.) como short_id del HU. La clave canónica
|
|
141
|
+
// sigue siendo el `hu.id` largo (`hu_<planId>_<NNN>`); el short_id
|
|
142
|
+
// solo da una etiqueta amigable que la CLI muestra y acepta como
|
|
143
|
+
// referencia en `kj run --hu INFRA-001`.
|
|
144
|
+
const symbolicId = typeof step === "object" && typeof step.id === "string" && step.id.trim()
|
|
145
|
+
? step.id.trim() : null;
|
|
146
|
+
const taskType = classifyTaskType(desc);
|
|
147
|
+
// KJC-TSK-0405 step 2: asigna coder_model + reviewer_model
|
|
148
|
+
// automáticamente según complexity inferido del task_type. El
|
|
149
|
+
// planner puede emitir `step.complexity` (trivial/simple/medium/complex
|
|
150
|
+
// o score 1-5) para override más fino. Cross-provider reviewer
|
|
151
|
+
// (claude↔codex) por defecto.
|
|
152
|
+
const coderProvider = config?.roles?.coder?.provider || config?.coder || "claude";
|
|
153
|
+
const complexity = (typeof step === "object" && step.complexity)
|
|
154
|
+
? step.complexity
|
|
155
|
+
: complexityFromTaskType(taskType);
|
|
156
|
+
const models = recommendModelsForHu({ complexity, coderProvider, config });
|
|
127
157
|
const hu = addHu(plan, {
|
|
128
158
|
title: desc.slice(0, 80),
|
|
129
|
-
task_type:
|
|
159
|
+
task_type: taskType,
|
|
130
160
|
scope: desc,
|
|
131
161
|
blocked_by: [],
|
|
132
162
|
acceptance_tests: tests,
|
|
133
|
-
|
|
134
|
-
// so the coder/reviewer / board can show "implements §5.3".
|
|
163
|
+
short_id: symbolicId,
|
|
135
164
|
spec_section: typeof step === "object" && typeof step.spec_section === "string" ? step.spec_section.trim() || null : null,
|
|
165
|
+
coder_model: models.coder_model,
|
|
166
|
+
coder_provider: models.coder_provider,
|
|
167
|
+
reviewer_model: models.reviewer_model,
|
|
168
|
+
reviewer_provider: models.reviewer_provider,
|
|
136
169
|
});
|
|
137
|
-
if (
|
|
138
|
-
symbolicToHuId.set(step.id.trim(), hu.id);
|
|
139
|
-
}
|
|
170
|
+
if (symbolicId) symbolicToHuId.set(symbolicId, hu.id);
|
|
140
171
|
const deps = typeof step === "object" && Array.isArray(step.dependencies) ? step.dependencies : [];
|
|
141
172
|
stepDeps.push(deps);
|
|
142
173
|
const reuse = typeof step === "object" && Array.isArray(step.reuse) ? step.reuse : [];
|
|
@@ -169,7 +200,10 @@ async function planGenerateImpl({ task, config, logger, json, context, runLog, f
|
|
|
169
200
|
runLog.logText(
|
|
170
201
|
`[planner] ${stepsMissingTests}/${steps.length} HUs missing acceptance_tests — running tests-synthesizer pass`
|
|
171
202
|
);
|
|
172
|
-
progress
|
|
203
|
+
// Sub-progress reporter por fase: el usuario ve cada fase como su
|
|
204
|
+
// propio "[planner:X] starting → done" en vez de un solo
|
|
205
|
+
// "[planner] done" que oculta que aún hay trabajo en cola.
|
|
206
|
+
const synthProgress = createCliProgressReporter({ role: "planner:tests-synth", quiet: Boolean(json) });
|
|
173
207
|
const { synthesizeMissingTests } = await import("../../plan/tests-synthesizer.js");
|
|
174
208
|
const husNeedingTests = plan.hus
|
|
175
209
|
.filter((h) => !Array.isArray(h.acceptance_tests) || h.acceptance_tests.length === 0)
|
|
@@ -178,10 +212,11 @@ async function planGenerateImpl({ task, config, logger, json, context, runLog, f
|
|
|
178
212
|
agent: planner,
|
|
179
213
|
task,
|
|
180
214
|
husNeedingTests,
|
|
181
|
-
onOutput:
|
|
215
|
+
onOutput: synthProgress.onOutput,
|
|
182
216
|
silenceTimeoutMs,
|
|
183
217
|
timeoutMs,
|
|
184
218
|
});
|
|
219
|
+
synthProgress.finish(synthResult.ok ? "done" : "failed");
|
|
185
220
|
if (synthResult.ok) {
|
|
186
221
|
let patched = 0;
|
|
187
222
|
for (const hu of plan.hus) {
|
|
@@ -216,16 +251,17 @@ async function planGenerateImpl({ task, config, logger, json, context, runLog, f
|
|
|
216
251
|
const skipReview = Boolean(flags?.noPlanReview || flags?.quick);
|
|
217
252
|
if (plan.hus.length > 0 && !skipReview) {
|
|
218
253
|
runLog.logText("[planner] running plan reviewer pass");
|
|
219
|
-
|
|
220
|
-
const { reviewPlan, findingsCount } = await import("../../plan/plan-reviewer.js");
|
|
254
|
+
const reviewProgress = createCliProgressReporter({ role: "planner:reviewer", quiet: Boolean(json) });
|
|
255
|
+
const { reviewPlan, findingsCount, findingsCountByType } = await import("../../plan/plan-reviewer.js");
|
|
221
256
|
const review = await reviewPlan({
|
|
222
257
|
agent: planner,
|
|
223
258
|
task,
|
|
224
259
|
hus: plan.hus,
|
|
225
|
-
onOutput:
|
|
260
|
+
onOutput: reviewProgress.onOutput,
|
|
226
261
|
silenceTimeoutMs,
|
|
227
262
|
timeoutMs,
|
|
228
263
|
});
|
|
264
|
+
reviewProgress.finish(review.ok ? "done" : "failed");
|
|
229
265
|
if (review.ok) {
|
|
230
266
|
plan.review = review.findings;
|
|
231
267
|
const count = findingsCount(review.findings);
|
|
@@ -239,13 +275,18 @@ async function planGenerateImpl({ task, config, logger, json, context, runLog, f
|
|
|
239
275
|
// PATCH the plan, apply in-process, and re-review. Loop until 0
|
|
240
276
|
// findings or maxIterations. Skippable with --no-plan-fixer/--quick.
|
|
241
277
|
const skipFixer = Boolean(flags?.noPlanFixer || flags?.quick);
|
|
242
|
-
|
|
278
|
+
// KJC-BUG-0054: bumped 2 → 4 — el guard antiguo cortaba el loop
|
|
279
|
+
// demasiado pronto cuando había muchos hallazgos. Con el guard
|
|
280
|
+
// inteligente (priority vs secondary) podemos permitir más
|
|
281
|
+
// iteraciones sin riesgo de divergir indefinidamente, porque
|
|
282
|
+
// cada iter que NO reduce priority ni total se revierte.
|
|
283
|
+
const maxFixerIterations = 4;
|
|
243
284
|
if (count > 0 && !skipFixer) {
|
|
244
285
|
for (let i = 1; i <= maxFixerIterations; i++) {
|
|
245
286
|
const currentCount = findingsCount(plan.review);
|
|
246
287
|
if (currentCount === 0) break;
|
|
247
288
|
runLog.logText(`[planner] self-fix iter ${i}/${maxFixerIterations} — patching ${currentCount} issue(s)`);
|
|
248
|
-
|
|
289
|
+
const fixProgress = createCliProgressReporter({ role: `planner:self-fix iter ${i}`, quiet: Boolean(json) });
|
|
249
290
|
// KJC-BUG-0046 / P5: snapshot plan + review BEFORE applying the patch
|
|
250
291
|
// so we can revert if the iteration regresses (newCount > currentCount).
|
|
251
292
|
// Dogfooding 2026-05-12: GRETA Plan 2 iter 2 went 10 → 17 findings
|
|
@@ -256,38 +297,66 @@ async function planGenerateImpl({ task, config, logger, json, context, runLog, f
|
|
|
256
297
|
const reviewSnapshot = JSON.parse(JSON.stringify(plan.review));
|
|
257
298
|
const fix = await applyReviewerFeedback({
|
|
258
299
|
agent: planner, task, hus: plan.hus, findings: plan.review,
|
|
259
|
-
onOutput:
|
|
300
|
+
onOutput: fixProgress.onOutput, silenceTimeoutMs, timeoutMs,
|
|
260
301
|
});
|
|
261
302
|
if (!fix.ok) {
|
|
303
|
+
fixProgress.finish("failed");
|
|
262
304
|
runLog.logText(`[planner] self-fix iter ${i} failed (non-blocking): ${fix.error}`);
|
|
263
305
|
break;
|
|
264
306
|
}
|
|
265
307
|
const ops = applyFixerPatch(plan, fix.patch);
|
|
266
308
|
runLog.logText(`[planner] self-fix iter ${i} applied: +${ops.added} HU, +${ops.depsAdded} dep, -${ops.deleted} HU`);
|
|
267
309
|
if (ops.added + ops.depsAdded + ops.deleted === 0) {
|
|
310
|
+
fixProgress.finish("done");
|
|
268
311
|
runLog.logText(`[planner] self-fix iter ${i} empty patch — stopping`);
|
|
269
312
|
break;
|
|
270
313
|
}
|
|
271
314
|
const review2 = await reviewPlan({
|
|
272
315
|
agent: planner, task, hus: plan.hus,
|
|
273
|
-
onOutput:
|
|
316
|
+
onOutput: fixProgress.onOutput, silenceTimeoutMs, timeoutMs,
|
|
274
317
|
});
|
|
275
318
|
if (!review2.ok) {
|
|
319
|
+
fixProgress.finish("failed");
|
|
276
320
|
runLog.logText(`[planner] re-review after iter ${i} failed: ${review2.error}`);
|
|
277
321
|
plan.hus = husSnapshot;
|
|
278
322
|
plan.review = reviewSnapshot;
|
|
279
323
|
break;
|
|
280
324
|
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
325
|
+
// KJC-BUG-0054: smart convergence guard. El guard antiguo
|
|
326
|
+
// (newCount > currentCount → revert) tiraba iteraciones
|
|
327
|
+
// VÁLIDAS que arreglaban un ciclo o una HU faltante pero
|
|
328
|
+
// a cambio creaban deps menores. Ahora distinguimos:
|
|
329
|
+
// - priority = cycles + missing_hus (CRÍTICO, no ignorable)
|
|
330
|
+
// - secondary = missing_deps + overlaps + order non-circular
|
|
331
|
+
//
|
|
332
|
+
// Reglas (en orden):
|
|
333
|
+
// 1. priority bajó → ACEPTAR (aunque secondary suba — el plan es ejecutable)
|
|
334
|
+
// 2. priority igual + total bajó → ACEPTAR
|
|
335
|
+
// 3. priority igual + total igual → ACEPTAR (lateral move, da chance)
|
|
336
|
+
// 4. priority subió → REVERT
|
|
337
|
+
// 5. priority igual + total subió → REVERT (regresión genuina)
|
|
338
|
+
const before = findingsCountByType(reviewSnapshot);
|
|
339
|
+
const after = findingsCountByType(review2.findings);
|
|
340
|
+
const newCount = after.total;
|
|
341
|
+
const accept =
|
|
342
|
+
after.priority < before.priority
|
|
343
|
+
|| (after.priority === before.priority && after.total <= before.total);
|
|
344
|
+
if (!accept) {
|
|
345
|
+
const reason = after.priority > before.priority
|
|
346
|
+
? `priority ${before.priority}→${after.priority}`
|
|
347
|
+
: `total ${before.total}→${after.total} (priority unchanged at ${after.priority})`;
|
|
284
348
|
plan.hus = husSnapshot;
|
|
285
349
|
plan.review = reviewSnapshot;
|
|
286
|
-
|
|
350
|
+
fixProgress.finish("reverted");
|
|
351
|
+
runLog.logText(`[planner] self-fix iter ${i} regressed (${reason}) — reverted, stopping`);
|
|
287
352
|
break;
|
|
288
353
|
}
|
|
289
354
|
plan.review = review2.findings;
|
|
290
|
-
|
|
355
|
+
fixProgress.finish("done");
|
|
356
|
+
const deltaSummary = after.priority < before.priority
|
|
357
|
+
? `priority ${before.priority}→${after.priority}, total ${before.total}→${after.total}`
|
|
358
|
+
: `total ${before.total}→${after.total}`;
|
|
359
|
+
runLog.logText(`[planner] after self-fix iter ${i}: ${newCount} issue(s) (${deltaSummary})`);
|
|
291
360
|
}
|
|
292
361
|
}
|
|
293
362
|
} else {
|
|
@@ -295,6 +364,19 @@ async function planGenerateImpl({ task, config, logger, json, context, runLog, f
|
|
|
295
364
|
}
|
|
296
365
|
}
|
|
297
366
|
|
|
367
|
+
// KJC-TSK-0399: deterministic structural integrity pass — cycles,
|
|
368
|
+
// orphan refs, missing short_id. Runs after self-fix loop because
|
|
369
|
+
// the LLM is bad at graph problems. Always runs (cheap, no network).
|
|
370
|
+
if (plan.hus.length > 0) {
|
|
371
|
+
const { fixes } = runStructuralPass(plan);
|
|
372
|
+
if (fixes.length > 0) {
|
|
373
|
+
if (!plan.review) plan.review = {};
|
|
374
|
+
plan.review.structural_fixes = fixes;
|
|
375
|
+
const byKind = fixes.reduce((a, f) => ({ ...a, [f.kind]: (a[f.kind] || 0) + 1 }), {});
|
|
376
|
+
runLog.logText(`[planner] structural pass: ${fixes.length} fix(es) — ${JSON.stringify(byKind)}`);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
298
380
|
const planId = await savePlan(projectDir, plan);
|
|
299
381
|
const { plansDir } = await import("../../plan/plan-store.js");
|
|
300
382
|
const path = await import("node:path");
|
|
@@ -341,9 +423,12 @@ async function planGenerateImpl({ task, config, logger, json, context, runLog, f
|
|
|
341
423
|
console.log(`Reviewer flagged: ${counts.join(", ")} — see \`kj plan show ${planId}\``);
|
|
342
424
|
}
|
|
343
425
|
}
|
|
426
|
+
// Humanización IDs: si el plan tiene alias, lo preferimos como
|
|
427
|
+
// referencia visible (más legible). Fallback al planId.
|
|
428
|
+
const ref = plan.alias || planId;
|
|
344
429
|
console.log("");
|
|
345
|
-
console.log(`Inspect: kj plan show ${
|
|
346
|
-
console.log(`Run: kj run --plan ${
|
|
430
|
+
console.log(`Inspect: kj plan show ${ref}`);
|
|
431
|
+
console.log(`Run: kj run --plan ${ref}`);
|
|
347
432
|
console.log(` (or click ▶ Run plan on the board, if running)`);
|
|
348
433
|
|
|
349
434
|
// UX boost for HU-bearing plans: auto-start the HU Board (same pattern as
|
|
@@ -9,6 +9,8 @@ export { planValidateCommand } from "./validate.js";
|
|
|
9
9
|
export { planDeleteCommand } from "./delete.js";
|
|
10
10
|
export { planAddHuCommand } from "./add-hu.js";
|
|
11
11
|
export { planRemoveHuCommand } from "./remove-hu.js";
|
|
12
|
+
export { planMigrateResultCommand } from "./migrate-result.js";
|
|
13
|
+
export { planFixCommand } from "./fix.js";
|
|
12
14
|
|
|
13
15
|
// Keep backward compat — old callers import planCommand
|
|
14
16
|
export const planCommand = planGenerateCommand;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* kj plan migrate-result [--dry-run] — KJC-TSK-0394 step 4.
|
|
3
|
+
* Siembra `result` en las HUs de los plan files existentes,
|
|
4
|
+
* deducido del status legacy. Idempotente.
|
|
5
|
+
*/
|
|
6
|
+
import { listPlans, loadPlan, savePlan } from "../../plan/plan-store.js";
|
|
7
|
+
import { migratePlanResult } from "../../plan/plan-migrate-result.js";
|
|
8
|
+
|
|
9
|
+
export async function planMigrateResultCommand({ config, flags = {} }) {
|
|
10
|
+
const projectDir = config.projectDir || process.cwd();
|
|
11
|
+
const dryRun = !!flags.dryRun;
|
|
12
|
+
|
|
13
|
+
const plans = await listPlans(projectDir);
|
|
14
|
+
if (plans.length === 0) {
|
|
15
|
+
console.log("No hay plans que migrar en este proyecto.");
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let totalPlans = 0;
|
|
20
|
+
let totalHus = 0;
|
|
21
|
+
for (const meta of plans) {
|
|
22
|
+
const plan = await loadPlan(projectDir, meta.planId);
|
|
23
|
+
if (!plan) continue;
|
|
24
|
+
const { plan: migrated, changes } = migratePlanResult(plan);
|
|
25
|
+
if (changes.length === 0) continue;
|
|
26
|
+
totalPlans += 1;
|
|
27
|
+
totalHus += changes.length;
|
|
28
|
+
const summary = changes
|
|
29
|
+
.map((c) => `${c.huId}→${c.result ?? "null"}`)
|
|
30
|
+
.join(", ");
|
|
31
|
+
console.log(`${dryRun ? "[dry-run] " : ""}${meta.planId}: ${changes.length} HU(s) — ${summary}`);
|
|
32
|
+
if (!dryRun) await savePlan(projectDir, migrated);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (totalPlans === 0) {
|
|
36
|
+
console.log("Todos los plans ya tienen `result` sembrado. Nada que hacer.");
|
|
37
|
+
} else {
|
|
38
|
+
const prefix = dryRun ? "[dry-run] " : "";
|
|
39
|
+
console.log(`${prefix}Migrados ${totalHus} HU(s) en ${totalPlans} plan(s).`);
|
|
40
|
+
}
|
|
41
|
+
}
|
package/src/commands/plan.js
CHANGED
|
@@ -2,6 +2,7 @@ import { createAgent } from "../agents/index.js";
|
|
|
2
2
|
import { assertAgentsAvailable } from "../agents/availability.js";
|
|
3
3
|
import { resolveRole } from "../config.js";
|
|
4
4
|
import { createCliProgressReporter } from "../utils/cli-progress.js";
|
|
5
|
+
import { withBrainRecovery } from "../brain/with-brain-recovery.js";
|
|
5
6
|
|
|
6
7
|
const SUBAGENT_PREAMBLE = [
|
|
7
8
|
"IMPORTANT: You are running as a Karajan sub-agent.",
|
|
@@ -33,8 +34,11 @@ export async function researcherCommand({ task, config, logger }) {
|
|
|
33
34
|
const progress = createCliProgressReporter({ role: "researcher" });
|
|
34
35
|
let result;
|
|
35
36
|
try {
|
|
36
|
-
result = await
|
|
37
|
-
|
|
37
|
+
result = await withBrainRecovery({
|
|
38
|
+
agent, taskArgs: { prompt, onOutput: progress.onOutput, role: "researcher" },
|
|
39
|
+
role: "researcher", provider: researcherRole.provider, logger,
|
|
40
|
+
});
|
|
41
|
+
progress.finish(result.ok ? "done" : (result.action || "failed"));
|
|
38
42
|
} catch (err) { progress.finish("failed"); throw err; }
|
|
39
43
|
|
|
40
44
|
if (!result.ok) {
|
package/src/commands/run.js
CHANGED
|
@@ -4,6 +4,7 @@ import { runFlow } from "../orchestrator.js";
|
|
|
4
4
|
import { assertAgentsAvailable } from "../agents/availability.js";
|
|
5
5
|
import { createActivityLog } from "../activity-log.js";
|
|
6
6
|
import { withCliRunLog } from "../utils/cli-run-log.js";
|
|
7
|
+
import { registerRun, unregisterRun } from "../utils/run-registry.js";
|
|
7
8
|
import { printHeader } from "../utils/display/header.js";
|
|
8
9
|
import { printEvent } from "../utils/display/event-handlers.js";
|
|
9
10
|
import { resolveRole } from "../config.js";
|
|
@@ -112,6 +113,28 @@ export async function runCommandHandler({ task, config, logger, flags }) {
|
|
|
112
113
|
return withCliRunLog("run", { projectDir: config?.projectDir, logger }, async ({ runLog, forwardProgress }) => {
|
|
113
114
|
runLog.logText(`[kj_run] task="${String(task).slice(0, 80)}..."`);
|
|
114
115
|
|
|
116
|
+
// KJC-TSK-0396 extensión: registrar el PID + planId + huIds en
|
|
117
|
+
// ~/.karajan/runs/<runId>.json para que el HU Board pueda mostrar
|
|
118
|
+
// el botón ⏹ Stop incluso cuando el run se lanza desde terminal
|
|
119
|
+
// externa (no solo desde el botón ▶ Run del board). Best-effort:
|
|
120
|
+
// si la escritura falla, el run sigue su curso normal. Limpieza
|
|
121
|
+
// garantizada en el finally del withCliRunLog vía registry.unregisterRun.
|
|
122
|
+
const huIdsFromFlag = typeof flags?.hu === "string"
|
|
123
|
+
? flags.hu.split(",").map((s) => s.trim()).filter(Boolean)
|
|
124
|
+
: null;
|
|
125
|
+
const runId = registerRun({
|
|
126
|
+
pid: process.pid,
|
|
127
|
+
planId: flags?.plan || null,
|
|
128
|
+
huIds: huIdsFromFlag,
|
|
129
|
+
projectDir: config?.projectDir || process.cwd(),
|
|
130
|
+
source: "cli",
|
|
131
|
+
});
|
|
132
|
+
const cleanupRegistry = () => { if (runId) unregisterRun(runId); };
|
|
133
|
+
// Cubrir todos los caminos de salida del proceso.
|
|
134
|
+
process.once("exit", cleanupRegistry);
|
|
135
|
+
process.once("SIGTERM", () => { cleanupRegistry(); process.exit(143); });
|
|
136
|
+
process.once("SIGINT", () => { cleanupRegistry(); process.exit(130); });
|
|
137
|
+
|
|
115
138
|
const emitter = new EventEmitter();
|
|
116
139
|
let activityLog = null;
|
|
117
140
|
|
|
@@ -139,7 +162,16 @@ export async function runCommandHandler({ task, config, logger, flags }) {
|
|
|
139
162
|
}
|
|
140
163
|
|
|
141
164
|
const askQuestion = createCliAskQuestion();
|
|
142
|
-
|
|
165
|
+
let result;
|
|
166
|
+
try {
|
|
167
|
+
result = await runFlow({ task: task, config, logger, flags, emitter, askQuestion, pgTaskId: pgCardId || null, pgProject: pgProject || null });
|
|
168
|
+
} finally {
|
|
169
|
+
// KJC-TSK-0396: limpia el registro pase lo que pase (éxito,
|
|
170
|
+
// throw, o paused). Los handlers de SIGINT/SIGTERM/exit son
|
|
171
|
+
// backstop para casos donde el throw no llega aquí (segfault,
|
|
172
|
+
// kill -9, etc.).
|
|
173
|
+
cleanupRegistry();
|
|
174
|
+
}
|
|
143
175
|
|
|
144
176
|
if (jsonMode) {
|
|
145
177
|
console.log(JSON.stringify(result, null, 2));
|