karajan-code 2.14.2 → 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 +3 -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/checks/project-checks.js +245 -0
- package/src/checks/runner.js +29 -0
- package/src/checks/tokens.js +24 -1
- package/src/checks/types.js +10 -0
- package/src/cli/register-pipeline.js +2 -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/doctor.js +18 -5
- 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/orchestrator/preflight-checks.js +5 -0
- 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
|
@@ -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));
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// KJC-TSK-0414 PR2: comandos kj standby list / kj resume.
|
|
2
|
+
//
|
|
3
|
+
// `kj resume <sessionId>` valida cooldownUntil y, si llegó la hora, re-spawn
|
|
4
|
+
// el comando original capturado en argv. Si cooldown todavía está en el
|
|
5
|
+
// futuro, muestra countdown y aborta. `kj standby list` solo enumera.
|
|
6
|
+
//
|
|
7
|
+
// Estrategia de resume: cada sessionState persiste su argv original (los
|
|
8
|
+
// argumentos con los que se lanzó el `kj` que hibernó). El resume re-spawn
|
|
9
|
+
// EXACTAMENTE ese mismo comando con env preservado. El run reanuda como si
|
|
10
|
+
// fuera un retry — el state interno (HU pendiente, snapshot_sha, etc.) está
|
|
11
|
+
// en el JSON persistido en disco por TSK-0408 + TSK-0414 PR1.
|
|
12
|
+
|
|
13
|
+
import { spawn } from "node:child_process";
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
import { fileURLToPath } from "node:url";
|
|
16
|
+
import { loadStandby, listPendingStandby, markStandbyDone, acquireStandbyLock } from "../brain/standby-store.js";
|
|
17
|
+
|
|
18
|
+
const REPO_KJ_CLI = (() => {
|
|
19
|
+
try {
|
|
20
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
return path.join(here, "..", "cli.js");
|
|
22
|
+
} catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
})();
|
|
26
|
+
|
|
27
|
+
function formatCountdown(ms) {
|
|
28
|
+
if (ms <= 0) return "ahora";
|
|
29
|
+
const s = Math.round(ms / 1000);
|
|
30
|
+
const h = Math.floor(s / 3600);
|
|
31
|
+
const m = Math.floor((s % 3600) / 60);
|
|
32
|
+
const sec = s % 60;
|
|
33
|
+
return `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}:${String(sec).padStart(2, "0")}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function standbyListCommand({ json } = {}) {
|
|
37
|
+
const sessions = listPendingStandby();
|
|
38
|
+
if (json) {
|
|
39
|
+
console.log(JSON.stringify(sessions, null, 2));
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (sessions.length === 0) {
|
|
43
|
+
console.log("No hay sesiones hibernadas.");
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
console.log(`Sesiones hibernadas (${sessions.length}):`);
|
|
47
|
+
const now = Date.now();
|
|
48
|
+
for (const s of sessions) {
|
|
49
|
+
const ms = new Date(s.cooldownUntil).getTime() - now;
|
|
50
|
+
const status = ms <= 0 ? "✓ lista para resume" : `⏳ resume en ${formatCountdown(ms)}`;
|
|
51
|
+
console.log(` ${s.sessionId} · ${s.reason || "?"} · ${status}`);
|
|
52
|
+
if (s.planId) console.log(` plan: ${s.planId}${s.huId ? `, HU: ${s.huId}` : ""}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Re-spawn el comando original capturado en sessionState.argv.
|
|
58
|
+
* Si cooldownUntil > now y force=false → aborta con mensaje.
|
|
59
|
+
*/
|
|
60
|
+
export async function resumeCommand({ sessionId, force } = {}) {
|
|
61
|
+
if (!sessionId) {
|
|
62
|
+
console.error("kj resume requiere <sessionId>. Usa `kj standby list` para ver pendientes.");
|
|
63
|
+
process.exitCode = 1;
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const state = loadStandby(sessionId);
|
|
67
|
+
if (!state) {
|
|
68
|
+
console.error(`Sesión no encontrada: ${sessionId}`);
|
|
69
|
+
process.exitCode = 1;
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const cooldownMs = new Date(state.cooldownUntil).getTime() - Date.now();
|
|
73
|
+
if (cooldownMs > 0 && !force) {
|
|
74
|
+
console.log(`Sesión ${sessionId} todavía en cooldown ${formatCountdown(cooldownMs)} (hasta ${state.cooldownUntil}).`);
|
|
75
|
+
console.log("Espera o usa `kj resume <sessionId> --force` (no recomendado — la API seguramente seguirá rechazando).");
|
|
76
|
+
process.exitCode = 0;
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const lock = acquireStandbyLock(sessionId);
|
|
81
|
+
if (!lock.acquired) {
|
|
82
|
+
console.log(`Sesión ${sessionId} ya está siendo reanudada por otro proceso.`);
|
|
83
|
+
process.exitCode = 0;
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
if (!Array.isArray(state.argv) || state.argv.length === 0) {
|
|
89
|
+
console.error(`Sesión ${sessionId} sin argv — no se puede re-spawn. Borra manual y relanza.`);
|
|
90
|
+
process.exitCode = 1;
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (!REPO_KJ_CLI) {
|
|
94
|
+
console.error("No se pudo localizar cli.js. ¿Karajan instalado correctamente?");
|
|
95
|
+
process.exitCode = 1;
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
console.log(`Reanudando ${sessionId}: kj ${state.argv.join(" ")}`);
|
|
99
|
+
const child = spawn(process.execPath, [REPO_KJ_CLI, ...state.argv], {
|
|
100
|
+
cwd: state.cwd || process.cwd(),
|
|
101
|
+
env: { ...process.env, ...(state.env || {}), KJ_RESUME_FROM: sessionId },
|
|
102
|
+
stdio: "inherit",
|
|
103
|
+
});
|
|
104
|
+
await new Promise((resolve) => {
|
|
105
|
+
child.on("exit", (code) => {
|
|
106
|
+
if (code === 0) markStandbyDone(sessionId);
|
|
107
|
+
process.exitCode = code ?? 0;
|
|
108
|
+
resolve();
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
} finally {
|
|
112
|
+
lock.release?.();
|
|
113
|
+
}
|
|
114
|
+
}
|
package/src/commands/triage.js
CHANGED
|
@@ -4,6 +4,7 @@ import { resolveRole } from "../config.js";
|
|
|
4
4
|
import { createCliProgressReporter } from "../utils/cli-progress.js";
|
|
5
5
|
import { buildTriagePrompt } from "../prompts/triage.js";
|
|
6
6
|
import { parseMaybeJsonString } from "../review/parser.js";
|
|
7
|
+
import { withBrainRecovery } from "../brain/with-brain-recovery.js";
|
|
7
8
|
|
|
8
9
|
function formatTriage(result) {
|
|
9
10
|
const lines = [];
|
|
@@ -47,8 +48,11 @@ export async function triageCommand({ task, config, logger, json }) {
|
|
|
47
48
|
const progress = createCliProgressReporter({ role: "triage" });
|
|
48
49
|
let result;
|
|
49
50
|
try {
|
|
50
|
-
result = await
|
|
51
|
-
|
|
51
|
+
result = await withBrainRecovery({
|
|
52
|
+
agent, taskArgs: { prompt, onOutput: progress.onOutput, role: "triage" },
|
|
53
|
+
role: "triage", provider: triageRole.provider, logger,
|
|
54
|
+
});
|
|
55
|
+
progress.finish(result.ok ? "done" : (result.action || "failed"));
|
|
52
56
|
} catch (err) { progress.finish("failed"); throw err; }
|
|
53
57
|
|
|
54
58
|
if (!result.ok) {
|