karajan-code 2.14.3 → 2.16.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/agents/claude-agent.js +2 -2
- package/src/agents/codex-agent.js +1 -1
- package/src/audit/agent-readiness.js +1 -1
- package/src/audit/basal-cost.js +4 -4
- package/src/audit/webperf-input.js +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/canvas/parse-md.js +1 -1
- 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/audit.js +1 -1
- 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 -5
- package/src/commands/plan/migrate-result.js +41 -0
- package/src/commands/plan.js +2 -1
- 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/commands/webperf.js +1 -1
- package/src/config/schema.js +40 -0
- package/src/domains/domain-synthesizer.js +1 -1
- package/src/git/hu-automation.js +2 -2
- package/src/git/hu-snapshot.js +105 -0
- package/src/guards/policy-resolver.js +38 -1
- package/src/hu/auto-generator.js +1 -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/repair/repair-detector.js +3 -3
- package/src/plan/adr-generator.js +5 -5
- 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 +21 -1
- 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 +64 -9
- 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/perf-role.js +1 -1
- package/src/roles/planner-role.js +10 -3
- package/src/roles/solomon-role.js +10 -4
- package/src/session/journal/decisions-writer.js +1 -1
- package/src/session/journal/iteration-logger.js +1 -1
- package/src/session/journal/summary-writer.js +1 -1
- package/src/skills/addyosmani-catalog.js +1 -1
- package/src/skills/semantic-detector.js +17 -2
- package/src/skills/skills-cache.js +1 -1
- package/src/sonar/api.js +22 -4
- package/src/sonar/config-resolver.js +1 -1
- package/src/sonar/issue-filter.js +110 -0
- package/src/sonar/scanner.js +1 -1
- package/src/utils/cli-progress.js +58 -1
- package/src/utils/derive-project-name-from-cwd.js +3 -3
- package/src/utils/garbage-collector.js +85 -2
- package/src/utils/injection-guard.js +1 -1
- package/src/utils/messages.js +1 -1
- package/src/utils/run-registry.js +115 -0
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) {
|
package/src/commands/webperf.js
CHANGED
|
@@ -26,7 +26,7 @@ function defaultReportDir() {
|
|
|
26
26
|
* with audit history under ~/.karajan/.
|
|
27
27
|
*/
|
|
28
28
|
function projectSlug(projectDir) {
|
|
29
|
-
return path.basename(projectDir || process.cwd()).
|
|
29
|
+
return path.basename(projectDir || process.cwd()).replaceAll(/[^a-zA-Z0-9_-]/g, "_");
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
function formatHumanReport(result) {
|
package/src/config/schema.js
CHANGED
|
@@ -53,12 +53,27 @@ const Methodology = v.picklist(
|
|
|
53
53
|
"development.methodology must be one of: tdd | standard"
|
|
54
54
|
);
|
|
55
55
|
|
|
56
|
+
/**
|
|
57
|
+
* KJC-TSK-0415: fallback chain. Cuando provider primario hits
|
|
58
|
+
* QUOTA_EXHAUSTED_* con cooldown > max_wait_hours, switch a fallback.
|
|
59
|
+
* Recursivo — el fallback puede tener su propio fallback.
|
|
60
|
+
*/
|
|
61
|
+
const RoleFallback = v.looseObject({
|
|
62
|
+
provider: v.string("fallback.provider required (claude|codex|gemini|opencode|aider)"),
|
|
63
|
+
model: v.optional(v.nullable(v.string())),
|
|
64
|
+
max_wait_hours: v.optional(v.pipe(v.number(), v.minValue(0)), 12),
|
|
65
|
+
// Recursivo. Schema permite cualquier shape compatible con RoleFallback.
|
|
66
|
+
fallback: v.optional(v.lazy(() => RoleFallback)),
|
|
67
|
+
});
|
|
68
|
+
|
|
56
69
|
/**
|
|
57
70
|
* Role entry — provider + model, both nullable. Extra keys allowed.
|
|
71
|
+
* KJC-TSK-0415: campo fallback opcional para Plan B en quota exhausted.
|
|
58
72
|
*/
|
|
59
73
|
const RoleEntry = v.looseObject({
|
|
60
74
|
provider: v.optional(v.nullable(v.string())),
|
|
61
75
|
model: v.optional(v.nullable(v.string())),
|
|
76
|
+
fallback: v.optional(RoleFallback),
|
|
62
77
|
});
|
|
63
78
|
|
|
64
79
|
const RolesMap = v.optional(v.looseObject({
|
|
@@ -176,6 +191,30 @@ const Skills = v.optional(v.looseObject({
|
|
|
176
191
|
addyosmani: v.optional(SkillsAddyosmani),
|
|
177
192
|
}));
|
|
178
193
|
|
|
194
|
+
/**
|
|
195
|
+
* KJC-TSK-0407: routing automático de modelos por HU según complexity.
|
|
196
|
+
* - by_provider: override de tiers per provider (claude/codex/gemini).
|
|
197
|
+
* Cada provider mapea level → modelo. Sobreescribe defaults del router.
|
|
198
|
+
* - fixed.coder / fixed.reviewer: si presentes, override TOTAL — todas
|
|
199
|
+
* las HUs usan esos modelos independientemente del score. Útil para
|
|
200
|
+
* forzar Sonnet en todo el proyecto, por ejemplo.
|
|
201
|
+
*/
|
|
202
|
+
const ModelRouting = v.optional(v.looseObject({
|
|
203
|
+
by_provider: v.optional(v.record(
|
|
204
|
+
v.string(),
|
|
205
|
+
v.looseObject({
|
|
206
|
+
trivial: v.optional(v.string()),
|
|
207
|
+
simple: v.optional(v.string()),
|
|
208
|
+
medium: v.optional(v.string()),
|
|
209
|
+
complex: v.optional(v.string()),
|
|
210
|
+
})
|
|
211
|
+
)),
|
|
212
|
+
fixed: v.optional(v.looseObject({
|
|
213
|
+
coder: v.optional(v.nullable(v.string())),
|
|
214
|
+
reviewer: v.optional(v.nullable(v.string())),
|
|
215
|
+
})),
|
|
216
|
+
}));
|
|
217
|
+
|
|
179
218
|
/**
|
|
180
219
|
* Main config schema. `looseObject` lets every consumer that reads an
|
|
181
220
|
* undeclared key keep working while the schema catches the bugs we care
|
|
@@ -207,6 +246,7 @@ export const ConfigSchema = v.looseObject({
|
|
|
207
246
|
policies: v.optional(v.record(v.string(), v.unknown())),
|
|
208
247
|
telemetry: v.optional(v.boolean()),
|
|
209
248
|
skills: Skills,
|
|
249
|
+
model_routing: ModelRouting,
|
|
210
250
|
});
|
|
211
251
|
|
|
212
252
|
/**
|
package/src/git/hu-automation.js
CHANGED
|
@@ -14,8 +14,8 @@ import { runCommand } from "../utils/process.js";
|
|
|
14
14
|
export function buildHuBranchName(prefix, story) {
|
|
15
15
|
const baseSlug = String(story.title || story.id || "hu")
|
|
16
16
|
.toLowerCase()
|
|
17
|
-
.
|
|
18
|
-
.
|
|
17
|
+
.replaceAll(/[^a-z0-9]+/g, "-")
|
|
18
|
+
.replaceAll(/^-+|-+$/g, "")
|
|
19
19
|
.slice(0, 40);
|
|
20
20
|
return `${prefix}${story.id}-${baseSlug}`;
|
|
21
21
|
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// KJC-TSK-0408: snapshots de ficheros por HU para soportar Undo.
|
|
2
|
+
//
|
|
3
|
+
// Antes de ejecutar una HU, guardamos un ref git apuntando al HEAD
|
|
4
|
+
// actual (`refs/kj-snapshots/<huId>`). Si el usuario hace Undo después,
|
|
5
|
+
// hacemos `git reset --hard <ref>` para volver al estado pre-run.
|
|
6
|
+
//
|
|
7
|
+
// Por qué refs en lugar de stash/tarball:
|
|
8
|
+
// - Refs son objetos git nativos. No mueven blobs (todos compartidos
|
|
9
|
+
// con HEAD), así que crear el snapshot es O(1).
|
|
10
|
+
// - El snapshot persiste a través de checkouts y rebases.
|
|
11
|
+
// - Si el ref ya apunta al SHA correcto, restore es no-op.
|
|
12
|
+
//
|
|
13
|
+
// El runner es inyectable para que los tests no necesiten un repo git real.
|
|
14
|
+
|
|
15
|
+
import { runCommand } from "../utils/process.js";
|
|
16
|
+
|
|
17
|
+
const REF_PREFIX = "refs/kj-snapshots/";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Sanitiza un huId para uso como nombre de ref. Git rechaza:
|
|
21
|
+
* - `..`, `:`, `~`, `^`, `?`, `*`, `[`, `\`, espacios, control chars
|
|
22
|
+
* - segmentos que empiecen por `.` o terminen en `.lock`
|
|
23
|
+
* Reemplazamos cualquier no [a-zA-Z0-9_-] por `_`.
|
|
24
|
+
*/
|
|
25
|
+
export function snapshotRefForHu(huId) {
|
|
26
|
+
if (!huId) throw new Error("huId requerido");
|
|
27
|
+
const safe = String(huId).replaceAll(/[^a-zA-Z0-9_-]/g, "_").slice(0, 80);
|
|
28
|
+
return REF_PREFIX + safe;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function defaultRunner(args, opts = {}) {
|
|
32
|
+
return runCommand("git", args, opts);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Crea (o actualiza) el ref del snapshot apuntando al HEAD actual.
|
|
37
|
+
*
|
|
38
|
+
* @param {object} args
|
|
39
|
+
* @param {string} args.projectDir
|
|
40
|
+
* @param {string} args.huId
|
|
41
|
+
* @param {Function} [args.runner] - sustituye git CLI en tests
|
|
42
|
+
* @returns {Promise<{ ok: boolean, ref?: string, sha?: string, error?: string }>}
|
|
43
|
+
*/
|
|
44
|
+
export async function createHuSnapshot({ projectDir, huId, runner }) {
|
|
45
|
+
const run = runner || defaultRunner;
|
|
46
|
+
const ref = snapshotRefForHu(huId);
|
|
47
|
+
const head = await run(["rev-parse", "HEAD"], { cwd: projectDir });
|
|
48
|
+
if (head.exitCode !== 0) {
|
|
49
|
+
return { ok: false, error: `rev-parse HEAD failed: ${(head.stderr || "").trim()}` };
|
|
50
|
+
}
|
|
51
|
+
const sha = (head.stdout || "").trim();
|
|
52
|
+
if (!sha) return { ok: false, error: "HEAD vacío" };
|
|
53
|
+
const update = await run(["update-ref", ref, sha], { cwd: projectDir });
|
|
54
|
+
if (update.exitCode !== 0) {
|
|
55
|
+
return { ok: false, error: `update-ref falló: ${(update.stderr || "").trim()}` };
|
|
56
|
+
}
|
|
57
|
+
return { ok: true, ref, sha };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* @param {object} args
|
|
62
|
+
* @param {string} args.projectDir
|
|
63
|
+
* @param {string} args.huId
|
|
64
|
+
* @param {Function} [args.runner]
|
|
65
|
+
* @returns {Promise<{ ok: boolean, sha?: string }>}
|
|
66
|
+
*/
|
|
67
|
+
export async function hasHuSnapshot({ projectDir, huId, runner }) {
|
|
68
|
+
const run = runner || defaultRunner;
|
|
69
|
+
const ref = snapshotRefForHu(huId);
|
|
70
|
+
const r = await run(["rev-parse", "--verify", "--quiet", ref], { cwd: projectDir });
|
|
71
|
+
if (r.exitCode !== 0) return { ok: false };
|
|
72
|
+
return { ok: true, sha: (r.stdout || "").trim() };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Restaura el workspace al snapshot. DESTRUCTIVO: hace `git reset --hard`,
|
|
77
|
+
* lo que descarta cambios no committeados. Caller debe avisar al usuario.
|
|
78
|
+
*
|
|
79
|
+
* @returns {Promise<{ ok: boolean, sha?: string, error?: string }>}
|
|
80
|
+
*/
|
|
81
|
+
export async function restoreHuSnapshot({ projectDir, huId, runner }) {
|
|
82
|
+
const run = runner || defaultRunner;
|
|
83
|
+
const ref = snapshotRefForHu(huId);
|
|
84
|
+
const has = await hasHuSnapshot({ projectDir, huId, runner: run });
|
|
85
|
+
if (!has.ok) return { ok: false, error: `snapshot no encontrado para HU ${huId}` };
|
|
86
|
+
const r = await run(["reset", "--hard", ref], { cwd: projectDir });
|
|
87
|
+
if (r.exitCode !== 0) {
|
|
88
|
+
return { ok: false, error: `reset --hard falló: ${(r.stderr || "").trim()}` };
|
|
89
|
+
}
|
|
90
|
+
return { ok: true, sha: has.sha };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Borra el ref del snapshot. Idempotente: si no existe, devuelve ok:true.
|
|
95
|
+
*/
|
|
96
|
+
export async function removeHuSnapshot({ projectDir, huId, runner }) {
|
|
97
|
+
const run = runner || defaultRunner;
|
|
98
|
+
const ref = snapshotRefForHu(huId);
|
|
99
|
+
const r = await run(["update-ref", "-d", ref], { cwd: projectDir });
|
|
100
|
+
// git update-ref -d devuelve 0 incluso si el ref no existía.
|
|
101
|
+
if (r.exitCode !== 0) {
|
|
102
|
+
return { ok: false, error: `update-ref -d falló: ${(r.stderr || "").trim()}` };
|
|
103
|
+
}
|
|
104
|
+
return { ok: true };
|
|
105
|
+
}
|
|
@@ -6,7 +6,14 @@
|
|
|
6
6
|
* @typedef {import("../types/config.js").KarajanConfig} KarajanConfig
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
// KJC-TSK-0400: spike y research añadidos como tipos no-code. Comparten
|
|
10
|
+
// las defaults restrictivas (tdd/sonar/testsRequired off) porque NO
|
|
11
|
+
// producen código que medir: un SPIKE es un .md de decisión, un research
|
|
12
|
+
// es una nota de investigación. Forzar Sonar les hace fallar siempre.
|
|
13
|
+
export const VALID_TASK_TYPES = new Set([
|
|
14
|
+
"sw", "infra", "doc", "add-tests", "refactor",
|
|
15
|
+
"audit", "analysis", "no-code", "spike", "research",
|
|
16
|
+
]);
|
|
10
17
|
|
|
11
18
|
export const DEFAULT_POLICIES = {
|
|
12
19
|
sw: { tdd: true, sonar: true, reviewer: true, testsRequired: true },
|
|
@@ -17,8 +24,38 @@ export const DEFAULT_POLICIES = {
|
|
|
17
24
|
audit: { tdd: false, sonar: false, reviewer: false, testsRequired: false, coderRequired: false },
|
|
18
25
|
analysis: { tdd: false, sonar: false, reviewer: false, testsRequired: false, coderRequired: false },
|
|
19
26
|
"no-code": { tdd: false, sonar: false, reviewer: true, testsRequired: false, coderRequired: true },
|
|
27
|
+
spike: { tdd: false, sonar: false, reviewer: true, testsRequired: false, coderRequired: true },
|
|
28
|
+
research: { tdd: false, sonar: false, reviewer: true, testsRequired: false, coderRequired: true },
|
|
20
29
|
};
|
|
21
30
|
|
|
31
|
+
// KJC-TSK-0400: si el title arranca con [SPIKE] / [DOC] / [RESEARCH] /
|
|
32
|
+
// [NOCODE], inferimos el task_type del prefijo. Pensado para casos donde
|
|
33
|
+
// el planner emite task_type='sw' por defecto pero el title declara
|
|
34
|
+
// claramente que la HU no produce código. Devuelve null si no hay match.
|
|
35
|
+
const TITLE_PREFIX_TO_TYPE = {
|
|
36
|
+
SPIKE: "spike",
|
|
37
|
+
RESEARCH: "research",
|
|
38
|
+
DOC: "doc",
|
|
39
|
+
NOCODE: "no-code",
|
|
40
|
+
"NO-CODE": "no-code",
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export function inferTaskTypeFromTitle(title) {
|
|
44
|
+
if (typeof title !== "string") return null;
|
|
45
|
+
const m = title.match(/^\s*\[([A-Z][A-Z-]+)\]/);
|
|
46
|
+
if (!m) return null;
|
|
47
|
+
return TITLE_PREFIX_TO_TYPE[m[1].toUpperCase()] || null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// KJC-TSK-0400: dado un HU del plan, devuelve el task_type efectivo:
|
|
51
|
+
// explícito en story.task_type si es válido, si no inferido del title.
|
|
52
|
+
// Fallback 'sw' (conservador, mantiene gates activos cuando hay duda).
|
|
53
|
+
export function effectiveTaskType(story) {
|
|
54
|
+
if (!story || typeof story !== "object") return "sw";
|
|
55
|
+
if (VALID_TASK_TYPES.has(story.task_type)) return story.task_type;
|
|
56
|
+
return inferTaskTypeFromTitle(story.title) || "sw";
|
|
57
|
+
}
|
|
58
|
+
|
|
22
59
|
const FALLBACK_TYPE = "sw";
|
|
23
60
|
|
|
24
61
|
/**
|
package/src/hu/auto-generator.js
CHANGED
|
@@ -24,7 +24,7 @@ export function deriveProjectName(originalTask) {
|
|
|
24
24
|
]);
|
|
25
25
|
const words = originalTask
|
|
26
26
|
.toLowerCase()
|
|
27
|
-
.
|
|
27
|
+
.replaceAll(/[^\p{L}\p{N}\s-]/gu, " ")
|
|
28
28
|
.split(/\s+/)
|
|
29
29
|
.filter(w => w && !STOPWORDS.has(w));
|
|
30
30
|
const meaningful = words.slice(0, 6);
|
package/src/hu/lazy-planner.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { createAgent } from "../agents/index.js";
|
|
9
9
|
import { extractFirstJson } from "../utils/json-extract.js";
|
|
10
|
+
import { withBrainRecovery } from "../brain/with-brain-recovery.js";
|
|
10
11
|
|
|
11
12
|
const SUBAGENT_PREAMBLE = [
|
|
12
13
|
"IMPORTANT: You are running as a Karajan sub-agent.",
|
|
@@ -72,7 +73,11 @@ export async function refineHuWithContext(hu, completedHus, config, logger) {
|
|
|
72
73
|
const agent = createAgent(provider, config, logger);
|
|
73
74
|
let result;
|
|
74
75
|
try {
|
|
75
|
-
|
|
76
|
+
// KJC-TSK-0413 step C
|
|
77
|
+
result = await withBrainRecovery({
|
|
78
|
+
agent, taskArgs: { prompt, role: "hu-reviewer" },
|
|
79
|
+
role: "lazy-planner", provider, logger,
|
|
80
|
+
});
|
|
76
81
|
} catch (err) {
|
|
77
82
|
logger.warn(`Lazy planner: refinement failed for HU ${hu.id}: ${err.message}`);
|
|
78
83
|
return hu;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// KJC-TSK-0405: dado un HU con complexity score, recomienda coder_model y
|
|
2
|
+
// reviewer_model. El reviewer por defecto usa el provider alternativo al
|
|
3
|
+
// coder (cross-provider review) para que dos cabezas distintas miren el
|
|
4
|
+
// código y se detecten más patrones.
|
|
5
|
+
|
|
6
|
+
const LEVELS = ["trivial", "simple", "medium", "complex"];
|
|
7
|
+
|
|
8
|
+
// Mapping complexity score numérico → nivel categórico.
|
|
9
|
+
// 1 → trivial, 2 → simple, 3/4 → medium, 5+ → complex.
|
|
10
|
+
const SCORE_TO_LEVEL = {
|
|
11
|
+
1: "trivial",
|
|
12
|
+
2: "simple",
|
|
13
|
+
3: "medium",
|
|
14
|
+
4: "medium",
|
|
15
|
+
5: "complex",
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// Defaults por provider — alineados con utils/model-selector.js DEFAULTS.
|
|
19
|
+
// opencode: usa "coder" como alias razonable; el usuario suele definir
|
|
20
|
+
// gemma-local/coder, ollama/coder o similar en opencode.json. Override
|
|
21
|
+
// per-tier vía config.model_routing.by_provider.opencode si quieres
|
|
22
|
+
// rutear distinto por complexity (ej. fast para trivial, coder para
|
|
23
|
+
// complex). aider: null porque aider envuelve a otros providers — no
|
|
24
|
+
// tiene "modelos propios". Si el usuario lo elige, debe configurarlo
|
|
25
|
+
// explícito en config.model_routing.by_provider.aider o vía override
|
|
26
|
+
// del modal por HU.
|
|
27
|
+
const DEFAULT_TIERS = {
|
|
28
|
+
claude: { trivial: "haiku", simple: "haiku", medium: "sonnet", complex: "opus" },
|
|
29
|
+
codex: { trivial: "o4-mini", simple: "o4-mini", medium: "o4-mini", complex: "o3" },
|
|
30
|
+
gemini: { trivial: "gemini-2.0-flash", simple: "gemini-2.0-flash", medium: "gemini-2.5-pro", complex: "gemini-2.5-pro" },
|
|
31
|
+
opencode: { trivial: "coder", simple: "coder", medium: "coder", complex: "coder" },
|
|
32
|
+
aider: { trivial: null, simple: null, medium: null, complex: null },
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// Cross-provider reviewer default: para cada coder provider, qué provider
|
|
36
|
+
// usa el reviewer por defecto. Dos cabezas distintas miran el código.
|
|
37
|
+
// opencode → claude: pattern "local barato escribe + cloud revisa".
|
|
38
|
+
// aider → claude: aider suele correr en un model pequeño, claude verifica.
|
|
39
|
+
const CROSS_PROVIDER_REVIEWER = {
|
|
40
|
+
claude: "codex",
|
|
41
|
+
codex: "claude",
|
|
42
|
+
gemini: "claude",
|
|
43
|
+
opencode: "claude",
|
|
44
|
+
aider: "claude",
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// KJC-TSK-0405 step 2: heurística para inferir complexity desde
|
|
48
|
+
// task_type cuando el planner no emite un score explícito. Permite
|
|
49
|
+
// que el model-router asigne modelos razonables sin un triage explícito.
|
|
50
|
+
const TASK_TYPE_COMPLEXITY = {
|
|
51
|
+
sw: "medium",
|
|
52
|
+
refactor: "medium",
|
|
53
|
+
"add-tests": "simple",
|
|
54
|
+
infra: "trivial",
|
|
55
|
+
doc: "trivial",
|
|
56
|
+
spike: "complex",
|
|
57
|
+
research: "complex",
|
|
58
|
+
audit: "complex",
|
|
59
|
+
analysis: "medium",
|
|
60
|
+
"no-code": "trivial",
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export function complexityFromTaskType(taskType) {
|
|
64
|
+
return TASK_TYPE_COMPLEXITY[taskType] || "medium";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function complexityToLevel(complexity) {
|
|
68
|
+
if (typeof complexity === "string" && LEVELS.includes(complexity)) return complexity;
|
|
69
|
+
if (typeof complexity === "number" && SCORE_TO_LEVEL[complexity]) return SCORE_TO_LEVEL[complexity];
|
|
70
|
+
return "medium";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* @param {object} args
|
|
75
|
+
* @param {string|number} args.complexity - "trivial" | "simple" | "medium" | "complex" | score 1-5
|
|
76
|
+
* @param {string} args.coderProvider - "claude" | "codex" | "gemini"
|
|
77
|
+
* @param {object} args.config - karajan config (lee model_routing si existe)
|
|
78
|
+
* @returns {{ coder_model: string|null, coder_provider: string, reviewer_model: string|null, reviewer_provider: string }}
|
|
79
|
+
*/
|
|
80
|
+
export function recommendModelsForHu({ complexity, coderProvider, config = {} }) {
|
|
81
|
+
const level = complexityToLevel(complexity);
|
|
82
|
+
const routing = config.model_routing || {};
|
|
83
|
+
const fixed = routing.fixed || {};
|
|
84
|
+
|
|
85
|
+
// Merge tiers: defaults + user overrides per provider.
|
|
86
|
+
const tiers = { ...DEFAULT_TIERS };
|
|
87
|
+
if (routing.by_provider) {
|
|
88
|
+
for (const [prov, levels] of Object.entries(routing.by_provider)) {
|
|
89
|
+
tiers[prov] = { ...(tiers[prov] || {}), ...levels };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const coderTier = tiers[coderProvider];
|
|
94
|
+
const coderAuto = coderTier ? (coderTier[level] || null) : null;
|
|
95
|
+
const coder_model = fixed.coder || coderAuto;
|
|
96
|
+
|
|
97
|
+
const reviewer_provider = CROSS_PROVIDER_REVIEWER[coderProvider] || "claude";
|
|
98
|
+
const reviewerTier = tiers[reviewer_provider];
|
|
99
|
+
const reviewerAuto = reviewerTier ? (reviewerTier[level] || null) : null;
|
|
100
|
+
const reviewer_model = fixed.reviewer || reviewerAuto;
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
coder_model,
|
|
104
|
+
coder_provider: coderProvider,
|
|
105
|
+
reviewer_model,
|
|
106
|
+
reviewer_provider,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { createAgent } from "../agents/index.js";
|
|
8
8
|
import { extractFirstJson } from "../utils/json-extract.js";
|
|
9
9
|
import { HEURISTIC_DESCRIPTIONS } from "./splitting-detector.js";
|
|
10
|
+
import { withBrainRecovery } from "../brain/with-brain-recovery.js";
|
|
10
11
|
|
|
11
12
|
const SUBAGENT_PREAMBLE = [
|
|
12
13
|
"IMPORTANT: You are running as a Karajan sub-agent.",
|
|
@@ -97,7 +98,11 @@ export async function generateSplitProposal(hu, heuristic, config, logger) {
|
|
|
97
98
|
|
|
98
99
|
let result;
|
|
99
100
|
try {
|
|
100
|
-
|
|
101
|
+
// KJC-TSK-0413 step C
|
|
102
|
+
result = await withBrainRecovery({
|
|
103
|
+
agent, taskArgs: { prompt, role: "hu-splitter" },
|
|
104
|
+
role: "hu-splitter", provider, logger,
|
|
105
|
+
});
|
|
101
106
|
} catch (err) {
|
|
102
107
|
logger.warn(`HU split generation threw: ${err.message}`);
|
|
103
108
|
return null;
|