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,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project-aware preflight checks (KJC-TSK-0393).
|
|
3
|
+
*
|
|
4
|
+
* `kj doctor` (global) y el preflight estándar verifican el ENTORNO de Karajan
|
|
5
|
+
* (CLIs instaladas, node, dirs ~/.karajan/, sonar). Pero **nada** validaba que
|
|
6
|
+
* el PROYECTO actual tuviera lo que necesita: tools detectables por signals
|
|
7
|
+
* (docker si hay Dockerfile, firebase si hay firebase.json, etc.), permisos
|
|
8
|
+
* write sobre projectDir, configuración inicializada, .env consistente con
|
|
9
|
+
* .env.example.
|
|
10
|
+
*
|
|
11
|
+
* Este módulo registra checks dinámicos basados en signals del proyecto. Se
|
|
12
|
+
* integra en preflight-checks.js como "fase project-aware" tras la fase global.
|
|
13
|
+
* También expone `kj doctor --project` para ejecutar solo esta fase manualmente.
|
|
14
|
+
*
|
|
15
|
+
* Filosofía: si un check falla, el mensaje incluye el comando EJECUTABLE para
|
|
16
|
+
* resolverlo (no "Run X or Y"), y siempre que sea posible se marca como
|
|
17
|
+
* `degradable` (KJC-BUG-0049) para no abortar runs por features opcionales.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import fs from "node:fs";
|
|
21
|
+
import path from "node:path";
|
|
22
|
+
import { runCommand } from "../utils/process.js";
|
|
23
|
+
import { checkBinary } from "../utils/agent-detect.js";
|
|
24
|
+
import { getInstallCommand } from "../utils/os-detect.js";
|
|
25
|
+
import { STRATEGY } from "./types.js";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Detect signals del proyecto. Devuelve un set de tags con qué hay en el
|
|
29
|
+
* projectDir. Pure-function, solo I/O de filesystem síncrono (rápido).
|
|
30
|
+
*
|
|
31
|
+
* @param {string} projectDir
|
|
32
|
+
* @returns {Set<string>}
|
|
33
|
+
*/
|
|
34
|
+
export function detectProjectSignals(projectDir) {
|
|
35
|
+
const signals = new Set();
|
|
36
|
+
if (!projectDir || !fs.existsSync(projectDir)) return signals;
|
|
37
|
+
const has = (rel) => fs.existsSync(path.join(projectDir, rel));
|
|
38
|
+
if (has("package.json")) signals.add("node");
|
|
39
|
+
if (has("Dockerfile") || has("docker-compose.yml") || has("compose.yml")) signals.add("docker");
|
|
40
|
+
if (has("firebase.json") || has(".firebaserc")) signals.add("firebase");
|
|
41
|
+
if (has("pyproject.toml") || has("requirements.txt") || has("setup.py")) signals.add("python");
|
|
42
|
+
if (has("Cargo.toml")) signals.add("rust");
|
|
43
|
+
if (has("go.mod")) signals.add("go");
|
|
44
|
+
if (has(".env.example") || has(".env.sample") || has(".env.template")) signals.add("env-example");
|
|
45
|
+
if (has(".env")) signals.add("env");
|
|
46
|
+
// Terraform / OpenTofu
|
|
47
|
+
try {
|
|
48
|
+
const entries = fs.readdirSync(projectDir);
|
|
49
|
+
if (entries.some((e) => e.endsWith(".tf") || e.endsWith(".tofu"))) signals.add("terraform");
|
|
50
|
+
} catch { /* unreadable dir */ }
|
|
51
|
+
return signals;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Check de tool específico del signal. Si el signal está presente pero la tool
|
|
56
|
+
* no, emite WARN con install command ejecutable.
|
|
57
|
+
*/
|
|
58
|
+
function createToolCheck({ signal, tool, label, signals }) {
|
|
59
|
+
return {
|
|
60
|
+
name: `project:tool:${tool}`,
|
|
61
|
+
label,
|
|
62
|
+
strategy: STRATEGY.MANUAL,
|
|
63
|
+
applies: () => signals.has(signal),
|
|
64
|
+
async detect() {
|
|
65
|
+
const res = await checkBinary(tool);
|
|
66
|
+
if (res.ok) {
|
|
67
|
+
return { ok: true, severity: "info", detail: `${tool} ${res.version} — required by ${signal} signal` };
|
|
68
|
+
}
|
|
69
|
+
const installCmd = getInstallCommand(tool) || `<install ${tool} for your OS>`;
|
|
70
|
+
return {
|
|
71
|
+
ok: false,
|
|
72
|
+
severity: "warn",
|
|
73
|
+
detail: `${tool} no instalada pero el proyecto tiene signal '${signal}' (puede no necesitarla para esta HU, pero la mayoría sí)`,
|
|
74
|
+
fix: installCmd,
|
|
75
|
+
};
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Permisos write check. Critical: si projectDir, .kj/ o .karajan/ no son
|
|
82
|
+
* escribibles, el pipeline va a fallar muy tarde con errores oscuros.
|
|
83
|
+
*/
|
|
84
|
+
function createWritePermsCheck({ projectDir }) {
|
|
85
|
+
return {
|
|
86
|
+
name: "project:write-perms",
|
|
87
|
+
label: "Write permissions (projectDir + .kj/ + .karajan/)",
|
|
88
|
+
strategy: STRATEGY.MANUAL,
|
|
89
|
+
async detect() {
|
|
90
|
+
const targets = [
|
|
91
|
+
projectDir,
|
|
92
|
+
path.join(projectDir, ".kj"),
|
|
93
|
+
path.join(projectDir, ".karajan"),
|
|
94
|
+
];
|
|
95
|
+
const failed = [];
|
|
96
|
+
for (const target of targets) {
|
|
97
|
+
try {
|
|
98
|
+
if (!fs.existsSync(target)) continue;
|
|
99
|
+
fs.accessSync(target, fs.constants.W_OK);
|
|
100
|
+
} catch {
|
|
101
|
+
failed.push(target);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (failed.length === 0) {
|
|
105
|
+
return { ok: true, severity: "info", detail: "Karajan puede escribir en projectDir, .kj/ y .karajan/" };
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
ok: false,
|
|
109
|
+
severity: "fail",
|
|
110
|
+
detail: `Sin permisos de escritura en: ${failed.join(", ")}`,
|
|
111
|
+
fix: `chmod -R u+w ${failed.join(" ")}`,
|
|
112
|
+
};
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* .env consistency check: si existe .env.example pero NO .env, o si las vars
|
|
119
|
+
* que .env.example declara faltan en .env, emite WARN con la lista.
|
|
120
|
+
*/
|
|
121
|
+
function createEnvConsistencyCheck({ projectDir, signals }) {
|
|
122
|
+
return {
|
|
123
|
+
name: "project:env-consistency",
|
|
124
|
+
label: ".env consistency (vs .env.example)",
|
|
125
|
+
strategy: STRATEGY.MANUAL,
|
|
126
|
+
applies: () => signals.has("env-example"),
|
|
127
|
+
async detect() {
|
|
128
|
+
const exampleFile = ["env.example", ".env.sample", ".env.template"]
|
|
129
|
+
.map((n) => path.join(projectDir, n.startsWith(".") ? n : `.${n}`))
|
|
130
|
+
.find((p) => fs.existsSync(p));
|
|
131
|
+
const realFile = path.join(projectDir, ".env");
|
|
132
|
+
const declaredVars = parseEnvKeys(exampleFile);
|
|
133
|
+
if (declaredVars.length === 0) {
|
|
134
|
+
return { ok: true, severity: "info", detail: ".env.example sin variables declaradas" };
|
|
135
|
+
}
|
|
136
|
+
const realVars = fs.existsSync(realFile) ? new Set(parseEnvKeys(realFile)) : new Set();
|
|
137
|
+
const missing = declaredVars.filter((k) => !realVars.has(k));
|
|
138
|
+
if (missing.length === 0) {
|
|
139
|
+
return { ok: true, severity: "info", detail: `.env tiene las ${declaredVars.length} variables declaradas en .env.example` };
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
ok: false,
|
|
143
|
+
severity: "warn",
|
|
144
|
+
detail: `.env no declara ${missing.length} variable(s) que sí están en .env.example: ${missing.slice(0, 5).join(", ")}${missing.length > 5 ? "..." : ""}`,
|
|
145
|
+
fix: `cp ${path.basename(exampleFile)} .env && edit it`,
|
|
146
|
+
};
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function parseEnvKeys(filePath) {
|
|
152
|
+
try {
|
|
153
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
154
|
+
return content
|
|
155
|
+
.split("\n")
|
|
156
|
+
.map((l) => l.trim())
|
|
157
|
+
.filter((l) => l && !l.startsWith("#") && l.includes("="))
|
|
158
|
+
.map((l) => l.split("=")[0].trim())
|
|
159
|
+
.filter(Boolean);
|
|
160
|
+
} catch { return []; }
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* gh remote-specific check: cuando auto_pr está activo, no basta con que `gh`
|
|
165
|
+
* esté autenticado en general (eso lo cubre token:gh ya). Hay que verificar
|
|
166
|
+
* que el usuario tiene push access AL REMOTE de este proyecto.
|
|
167
|
+
*/
|
|
168
|
+
function createGhRemoteCheck({ projectDir }) {
|
|
169
|
+
return {
|
|
170
|
+
name: "project:gh-remote-access",
|
|
171
|
+
label: "GitHub push access (current remote)",
|
|
172
|
+
strategy: STRATEGY.MANUAL,
|
|
173
|
+
applies: (config) => config?.git?.auto_pr === true,
|
|
174
|
+
degradable: {
|
|
175
|
+
disables: ["git.auto_pr"],
|
|
176
|
+
warn: "auto_pr DESACTIVADO — no se pueden crear PRs al remote actual. Verifica permisos con `gh repo view`",
|
|
177
|
+
},
|
|
178
|
+
async detect() {
|
|
179
|
+
// Verifica que el remote responde a ls-remote (read access mínimo,
|
|
180
|
+
// proxy para push access — el push real solo se valida al pushear).
|
|
181
|
+
try {
|
|
182
|
+
const res = await runCommand("git", ["-C", projectDir, "ls-remote", "--heads", "origin"], { reject: false });
|
|
183
|
+
if (res.exitCode === 0) {
|
|
184
|
+
return { ok: true, severity: "info", detail: "Remote origin accesible vía git ls-remote" };
|
|
185
|
+
}
|
|
186
|
+
return {
|
|
187
|
+
ok: false,
|
|
188
|
+
severity: "warn",
|
|
189
|
+
detail: `git ls-remote origin falló (exit ${res.exitCode}): ${(res.stderr || "").slice(0, 120)}`,
|
|
190
|
+
fix: "Verifica el remote con `git remote -v` y la auth con `gh repo view`",
|
|
191
|
+
};
|
|
192
|
+
} catch (err) {
|
|
193
|
+
return { ok: false, severity: "warn", detail: `git ls-remote error: ${err.message}`, fix: "Verifica git remote y auth gh" };
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* `kj init` ran check: si no existe ~/.karajan/kj.config.yml NI .karajan/coder-rules.md,
|
|
201
|
+
* el usuario probablemente no ejecutó `kj init`. WARN con instrucción ejecutable.
|
|
202
|
+
*/
|
|
203
|
+
function createKjInitCheck({ projectDir }) {
|
|
204
|
+
return {
|
|
205
|
+
name: "project:kj-init-ran",
|
|
206
|
+
label: "Karajan project initialized",
|
|
207
|
+
strategy: STRATEGY.MANUAL,
|
|
208
|
+
async detect() {
|
|
209
|
+
const projectKjFile = path.join(projectDir, ".karajan", "coder-rules.md");
|
|
210
|
+
const globalKjFile = path.join(process.env.HOME || "", ".karajan", "kj.config.yml");
|
|
211
|
+
if (fs.existsSync(projectKjFile) || fs.existsSync(globalKjFile)) {
|
|
212
|
+
return { ok: true, severity: "info", detail: "Karajan config presente (global o local del proyecto)" };
|
|
213
|
+
}
|
|
214
|
+
return {
|
|
215
|
+
ok: false,
|
|
216
|
+
severity: "warn",
|
|
217
|
+
detail: "No se encontró kj.config.yml global ni .karajan/coder-rules.md local — probablemente falta `kj init`",
|
|
218
|
+
fix: "kj init",
|
|
219
|
+
};
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Construye el set completo de project-aware checks basado en projectDir.
|
|
226
|
+
*
|
|
227
|
+
* @param {{ projectDir: string }} ctx
|
|
228
|
+
* @returns {Check[]}
|
|
229
|
+
*/
|
|
230
|
+
export function getProjectChecks({ projectDir }) {
|
|
231
|
+
if (!projectDir) return [];
|
|
232
|
+
const signals = detectProjectSignals(projectDir);
|
|
233
|
+
return [
|
|
234
|
+
createKjInitCheck({ projectDir }),
|
|
235
|
+
createWritePermsCheck({ projectDir }),
|
|
236
|
+
createToolCheck({ signal: "docker", tool: "docker", label: "Docker (project signal)", signals }),
|
|
237
|
+
createToolCheck({ signal: "firebase", tool: "firebase", label: "firebase-cli (project signal)", signals }),
|
|
238
|
+
createToolCheck({ signal: "python", tool: "python3", label: "Python 3 (project signal)", signals }),
|
|
239
|
+
createToolCheck({ signal: "rust", tool: "cargo", label: "Cargo / Rust (project signal)", signals }),
|
|
240
|
+
createToolCheck({ signal: "go", tool: "go", label: "Go (project signal)", signals }),
|
|
241
|
+
createToolCheck({ signal: "terraform", tool: "terraform", label: "Terraform (project signal)", signals }),
|
|
242
|
+
createEnvConsistencyCheck({ projectDir, signals }),
|
|
243
|
+
createGhRemoteCheck({ projectDir }),
|
|
244
|
+
];
|
|
245
|
+
}
|
package/src/checks/runner.js
CHANGED
|
@@ -98,6 +98,17 @@ export async function runChecks(checks, ctx, options = {}) {
|
|
|
98
98
|
entry.runMs = Date.now() - start;
|
|
99
99
|
entry.status = resolveStatusFromDetect(entry.check, result);
|
|
100
100
|
entry.detail = result.detail;
|
|
101
|
+
// KJC-BUG-0049: si el check es degradable y falló (status FAIL/WARN tras
|
|
102
|
+
// resolveStatusFromDetect), aplicar disables al overrides y degradar a WARN.
|
|
103
|
+
// Así el preflight no aborta — la feature opcional se desactiva con aviso.
|
|
104
|
+
if (!result.ok && entry.check.degradable && (entry.status === STATUS.FAIL || entry.status === STATUS.WARN)) {
|
|
105
|
+
for (const dotPath of entry.check.degradable.disables || []) {
|
|
106
|
+
setDotPath(overrides, dotPath, false);
|
|
107
|
+
}
|
|
108
|
+
entry.status = STATUS.WARN;
|
|
109
|
+
entry.detail = `${result.detail} → ${entry.check.degradable.warn}`;
|
|
110
|
+
entry.degraded = true;
|
|
111
|
+
}
|
|
101
112
|
} catch (err) {
|
|
102
113
|
entry.runMs = Date.now() - start;
|
|
103
114
|
if (err instanceof TimeoutError) {
|
|
@@ -227,6 +238,24 @@ function summarize(reports) {
|
|
|
227
238
|
return summary;
|
|
228
239
|
}
|
|
229
240
|
|
|
241
|
+
/**
|
|
242
|
+
* Set a dot-path on a target object, creating intermediate objects as needed.
|
|
243
|
+
* Used by the `degradable` check support (KJC-BUG-0049) to disable feature
|
|
244
|
+
* flags like `git.auto_pr` when their preflight check fails.
|
|
245
|
+
*
|
|
246
|
+
* @example setDotPath({}, "git.auto_pr", false) // → { git: { auto_pr: false } }
|
|
247
|
+
*/
|
|
248
|
+
function setDotPath(target, dotPath, value) {
|
|
249
|
+
const parts = String(dotPath).split(".");
|
|
250
|
+
let cursor = target;
|
|
251
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
252
|
+
const k = parts[i];
|
|
253
|
+
if (cursor[k] == null || typeof cursor[k] !== "object") cursor[k] = {};
|
|
254
|
+
cursor = cursor[k];
|
|
255
|
+
}
|
|
256
|
+
cursor[parts.at(-1)] = value;
|
|
257
|
+
}
|
|
258
|
+
|
|
230
259
|
/**
|
|
231
260
|
* Deep-merge (shallow for now, one level deep) remediation changes into overrides.
|
|
232
261
|
* Remediation outputs like { hu_board: { port: 4007 } } should not overwrite a
|
package/src/checks/tokens.js
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
|
|
22
22
|
import { checkBinary } from "../utils/agent-detect.js";
|
|
23
23
|
import { getRequiredProviderEnvs, normalizeProvider } from "../utils/provider-env.js";
|
|
24
|
+
import { runCommand } from "../utils/process.js";
|
|
24
25
|
import { STRATEGY } from "./types.js";
|
|
25
26
|
|
|
26
27
|
/**
|
|
@@ -95,6 +96,13 @@ function createGhTokenCheck() {
|
|
|
95
96
|
strategy: STRATEGY.PROMPT,
|
|
96
97
|
applies: (config) => config?.git?.auto_pr === true,
|
|
97
98
|
describe: "Run 'gh auth login' to authenticate with GitHub",
|
|
99
|
+
// KJC-BUG-0049: auto_pr es feature opcional. Si falta el token, no abortar
|
|
100
|
+
// el run — desactivar auto_pr y auto_push (sin push no hay PR posible) y
|
|
101
|
+
// continuar. El usuario verá WARN y el commit local sigue funcionando.
|
|
102
|
+
degradable: {
|
|
103
|
+
disables: ["git.auto_pr", "git.auto_push"],
|
|
104
|
+
warn: "auto_pr y auto_push DESACTIVADOS para este run (commit local sí funciona). Ejecuta `gh auth login` para reactivar.",
|
|
105
|
+
},
|
|
98
106
|
async detect() {
|
|
99
107
|
const envOk = !!process.env.GH_TOKEN || !!process.env.GITHUB_TOKEN;
|
|
100
108
|
if (envOk) {
|
|
@@ -111,10 +119,25 @@ function createGhTokenCheck() {
|
|
|
111
119
|
extra: { hasGh: false },
|
|
112
120
|
};
|
|
113
121
|
}
|
|
122
|
+
// KJC-BUG-0049 — el check anterior asumía que un usuario sin env var no
|
|
123
|
+
// tiene auth válida y abortaba el run. Pero `gh` puede estar autenticado
|
|
124
|
+
// por keyring/OAuth (caso default tras `gh auth login --web`) y operar
|
|
125
|
+
// sin env var. Si `gh auth status` devuelve exit 0 (logged in), el flujo
|
|
126
|
+
// de `auto_pr` funciona — Karajan ya no necesita `GH_TOKEN` en env.
|
|
127
|
+
try {
|
|
128
|
+
const auth = await runCommand("gh", ["auth", "status"], { reject: false });
|
|
129
|
+
if (auth.exitCode === 0) {
|
|
130
|
+
return {
|
|
131
|
+
ok: true,
|
|
132
|
+
severity: "info",
|
|
133
|
+
detail: "`gh` authenticated via keyring/OAuth (no env var needed)",
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
} catch { /* fall through to FAIL */ }
|
|
114
137
|
return {
|
|
115
138
|
ok: false,
|
|
116
139
|
severity: "fail",
|
|
117
|
-
detail: "No GH_TOKEN/GITHUB_TOKEN
|
|
140
|
+
detail: "No GH_TOKEN/GITHUB_TOKEN and `gh auth status` reports not logged in",
|
|
118
141
|
fix: "Run 'gh auth login' or export GH_TOKEN=...",
|
|
119
142
|
extra: { hasGh: true },
|
|
120
143
|
};
|
package/src/checks/types.js
CHANGED
|
@@ -41,6 +41,12 @@
|
|
|
41
41
|
* @property {Object} [changes] - runtime config overrides to merge into the active session
|
|
42
42
|
*/
|
|
43
43
|
|
|
44
|
+
/**
|
|
45
|
+
* @typedef {Object} DegradableSpec
|
|
46
|
+
* @property {string[]} disables - dot-paths del config a desactivar si el check falla (e.g. ["git.auto_pr", "git.auto_push"])
|
|
47
|
+
* @property {string} warn - mensaje WARN que se muestra al usuario explicando qué se desactivó y por qué
|
|
48
|
+
*/
|
|
49
|
+
|
|
44
50
|
/**
|
|
45
51
|
* @typedef {Object} Check
|
|
46
52
|
* @property {string} name - slug, unique across all checks (e.g. "node-version")
|
|
@@ -50,6 +56,10 @@
|
|
|
50
56
|
* @property {(ctx: RemediationContext) => Promise<RemediationResult>} [remediate]
|
|
51
57
|
* @property {string} [describe] - shown to user when strategy is "prompt"
|
|
52
58
|
* @property {(config: Object) => boolean} [applies] - if false, check is SKIPPED (lazy evaluation). Default: always applies.
|
|
59
|
+
* @property {DegradableSpec} [degradable] - KJC-BUG-0049: si está definido y el check falla, en lugar de abortar
|
|
60
|
+
* el preflight, se desactivan los flags listados en `disables` y se emite
|
|
61
|
+
* WARN con `warn`. Útil para features opcionales (auto_pr, auto_push,
|
|
62
|
+
* sonar advisory, etc.) que no son blockers absolutos.
|
|
53
63
|
*/
|
|
54
64
|
|
|
55
65
|
/**
|
|
@@ -177,6 +177,7 @@ export function registerPipeline(program, { pkgVersion }) {
|
|
|
177
177
|
.option("-y, --yes", "Auto-confirm prompts for invasive remediations (CI mode)")
|
|
178
178
|
.option("--json", "Emit machine-readable JSON instead of human output")
|
|
179
179
|
.option("--verbose", "Include fix hints and timing for every check")
|
|
180
|
+
.option("--project", "Run ONLY project-aware checks (signals, tools per signal, write perms, .env consistency, gh remote access) — useful to validate a specific project before kj run")
|
|
180
181
|
.action(async (flags) => {
|
|
181
182
|
const exitCode = await withConfig(pkgVersion, "doctor", {}, (ctx) =>
|
|
182
183
|
doctorCommand({
|
|
@@ -185,6 +186,7 @@ export function registerPipeline(program, { pkgVersion }) {
|
|
|
185
186
|
yes: !!flags.yes,
|
|
186
187
|
json: !!flags.json,
|
|
187
188
|
verbose: !!flags.verbose,
|
|
189
|
+
projectOnly: !!flags.project,
|
|
188
190
|
})
|
|
189
191
|
);
|
|
190
192
|
if (Number.isInteger(exitCode)) process.exit(exitCode);
|
package/src/cli/register-plan.js
CHANGED
|
@@ -7,6 +7,8 @@ import {
|
|
|
7
7
|
planDeleteCommand,
|
|
8
8
|
planAddHuCommand,
|
|
9
9
|
planRemoveHuCommand,
|
|
10
|
+
planMigrateResultCommand,
|
|
11
|
+
planFixCommand,
|
|
10
12
|
} from "../commands/plan.js";
|
|
11
13
|
import { parseCanvasMarkdown } from "../canvas/parse-md.js";
|
|
12
14
|
import { validateCanvas } from "../canvas/validate.js";
|
|
@@ -149,4 +151,31 @@ export function registerPlan(program, { pkgVersion }) {
|
|
|
149
151
|
await planRemoveHuCommand({ config, planId, huId });
|
|
150
152
|
});
|
|
151
153
|
});
|
|
154
|
+
|
|
155
|
+
// KJC-TSK-0402: re-ejecuta reviewer + self-fix + structural pass sobre
|
|
156
|
+
// un plan existente. Acepta --prompt opcional con feedback humano que
|
|
157
|
+
// el reviewer inyecta como contexto extra antes de revisar.
|
|
158
|
+
plan.command("fix")
|
|
159
|
+
.description("Relanzar self-fix loop sobre plan existente (con --prompt opcional)")
|
|
160
|
+
.argument("<planId>")
|
|
161
|
+
.option("--prompt <text>", "Feedback del usuario para guiar al reviewer")
|
|
162
|
+
.option("--json", "Output JSON con before/after counts")
|
|
163
|
+
.action(async (planId, flags) => {
|
|
164
|
+
await withConfig(pkgVersion, "plan", flags, async ({ config, logger }) => {
|
|
165
|
+
await planFixCommand({ config, planId, prompt: flags.prompt, logger, json: flags.json });
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// KJC-TSK-0394 step 4: migración one-shot que siembra `result` en las
|
|
170
|
+
// HUs de los plan files existentes (basado en su status legacy). Solo
|
|
171
|
+
// toca el campo result, NO el status — el cleanup del enum lo hará
|
|
172
|
+
// step 5. Idempotente.
|
|
173
|
+
plan.command("migrate-result")
|
|
174
|
+
.description("Backfill the `result` field on existing plan HUs (KJC-TSK-0394 step 4)")
|
|
175
|
+
.option("--dry-run", "Show what would change without writing")
|
|
176
|
+
.action(async (flags) => {
|
|
177
|
+
await withConfig(pkgVersion, "plan", flags, async ({ config }) => {
|
|
178
|
+
await planMigrateResultCommand({ config, flags: { dryRun: flags.dryRun } });
|
|
179
|
+
});
|
|
180
|
+
});
|
|
152
181
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// KJC-TSK-0414 PR2: registra `kj standby list` + `kj standby resume <id>`.
|
|
2
|
+
// `resume` es subcomando de standby para no colisionar con el `kj resume`
|
|
3
|
+
// pre-existente (Solomon pauses con --answer). Modelos mentales distintos:
|
|
4
|
+
// - `kj resume <id> --answer X` → Solomon pauseado (existente)
|
|
5
|
+
// - `kj standby resume <id>` → run hibernada por quota
|
|
6
|
+
import { resumeCommand, standbyListCommand } from "../commands/standby.js";
|
|
7
|
+
|
|
8
|
+
export function registerStandby(program) {
|
|
9
|
+
const standby = program.command("standby").description("Sesiones hibernadas (QUOTA_EXHAUSTED_DAILY etc.)");
|
|
10
|
+
standby
|
|
11
|
+
.command("list", { isDefault: true })
|
|
12
|
+
.description("Lista sesiones pendientes de reanudar")
|
|
13
|
+
.option("--json", "Output JSON")
|
|
14
|
+
.action(async (flags) => {
|
|
15
|
+
await standbyListCommand({ json: flags.json });
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
standby
|
|
19
|
+
.command("resume")
|
|
20
|
+
.description("Reanuda una sesión hibernada (re-spawn comando original)")
|
|
21
|
+
.argument("<sessionId>")
|
|
22
|
+
.option("--force", "Reanudar aunque cooldownUntil todavía esté en el futuro (no recomendado)")
|
|
23
|
+
.action(async (sessionId, flags) => {
|
|
24
|
+
await resumeCommand({ sessionId, force: Boolean(flags.force) });
|
|
25
|
+
});
|
|
26
|
+
}
|
package/src/cli.js
CHANGED
|
@@ -9,6 +9,7 @@ import { registerPlan } from "./cli/register-plan.js";
|
|
|
9
9
|
import { registerRolesSkills } from "./cli/register-roles-skills.js";
|
|
10
10
|
import { registerMeta } from "./cli/register-meta.js";
|
|
11
11
|
import { registerSonar } from "./cli/register-sonar.js";
|
|
12
|
+
import { registerStandby } from "./cli/register-standby.js";
|
|
12
13
|
import { printUpdateNotice } from "./utils/update-check.js";
|
|
13
14
|
import { printWelcomeScreen } from "./utils/welcome.js";
|
|
14
15
|
|
|
@@ -44,6 +45,7 @@ registerPlan(program, { pkgVersion: PKG_VERSION });
|
|
|
44
45
|
registerRolesSkills(program, { pkgVersion: PKG_VERSION });
|
|
45
46
|
registerMeta(program, { pkgVersion: PKG_VERSION });
|
|
46
47
|
registerSonar(program);
|
|
48
|
+
registerStandby(program);
|
|
47
49
|
|
|
48
50
|
/**
|
|
49
51
|
* Find subcommands whose name is "close enough" to a typo. Used by the
|
|
@@ -3,6 +3,7 @@ import { assertAgentsAvailable } from "../agents/availability.js";
|
|
|
3
3
|
import { resolveRole } from "../config.js";
|
|
4
4
|
import { buildArchitectPrompt, parseArchitectOutput } from "../prompts/architect.js";
|
|
5
5
|
import { createCliProgressReporter } from "../utils/cli-progress.js";
|
|
6
|
+
import { withBrainRecovery } from "../brain/with-brain-recovery.js";
|
|
6
7
|
|
|
7
8
|
function formatLayers(layers, lines) {
|
|
8
9
|
lines.push("### Layers");
|
|
@@ -71,8 +72,11 @@ export async function architectCommand({ task, config, logger, context, json })
|
|
|
71
72
|
const progress = createCliProgressReporter({ role: "architect" });
|
|
72
73
|
let result;
|
|
73
74
|
try {
|
|
74
|
-
result = await
|
|
75
|
-
|
|
75
|
+
result = await withBrainRecovery({
|
|
76
|
+
agent, taskArgs: { prompt, onOutput: progress.onOutput, role: "architect" },
|
|
77
|
+
role: "architect", provider: architectRole.provider, logger,
|
|
78
|
+
});
|
|
79
|
+
progress.finish(result.ok ? "done" : (result.action || "failed"));
|
|
76
80
|
} catch (err) { progress.finish("failed"); throw err; }
|
|
77
81
|
|
|
78
82
|
if (!result.ok) {
|
package/src/commands/discover.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createAgent } from "../agents/index.js";
|
|
2
2
|
import { assertAgentsAvailable } from "../agents/availability.js";
|
|
3
3
|
import { resolveRole } from "../config.js";
|
|
4
|
+
import { withBrainRecovery } from "../brain/with-brain-recovery.js";
|
|
4
5
|
import { createCliProgressReporter } from "../utils/cli-progress.js";
|
|
5
6
|
import { buildDiscoverPrompt, parseDiscoverOutput } from "../prompts/discover.js";
|
|
6
7
|
|
|
@@ -75,8 +76,11 @@ export async function discoverCommand({ task, config, logger, mode, json }) {
|
|
|
75
76
|
const progress = createCliProgressReporter({ role: "discover" });
|
|
76
77
|
let result;
|
|
77
78
|
try {
|
|
78
|
-
result = await
|
|
79
|
-
|
|
79
|
+
result = await withBrainRecovery({
|
|
80
|
+
agent, taskArgs: { prompt, onOutput: progress.onOutput, role: "discover" },
|
|
81
|
+
role: "discover", provider: discoverRole.provider, logger,
|
|
82
|
+
});
|
|
83
|
+
progress.finish(result.ok ? "done" : (result.action || "failed"));
|
|
80
84
|
} catch (err) { progress.finish("failed"); throw err; }
|
|
81
85
|
|
|
82
86
|
if (!result.ok) {
|
package/src/commands/doctor.js
CHANGED
|
@@ -26,13 +26,25 @@ import { getTokenChecks } from "../checks/tokens.js";
|
|
|
26
26
|
import { getMcpHealthChecks } from "../checks/mcp-health.js";
|
|
27
27
|
import { getSkillsChecks } from "../checks/skills.js";
|
|
28
28
|
import { getDirSetupChecks } from "../checks/dir-setup.js";
|
|
29
|
+
import { getProjectChecks } from "../checks/project-checks.js";
|
|
29
30
|
|
|
30
31
|
/**
|
|
31
32
|
* Build the list of Check objects applicable to the current config.
|
|
33
|
+
*
|
|
34
|
+
* KJC-TSK-0393: cuando `projectOnly === true`, devuelve SOLO los project-aware
|
|
35
|
+
* checks (signals, tools per signal, write perms, .env consistency, gh remote
|
|
36
|
+
* access). Útil para validar un proyecto específico sin re-correr todos los
|
|
37
|
+
* checks globales (CLIs, node, sonar, etc.).
|
|
38
|
+
*
|
|
32
39
|
* @param {Object} config
|
|
40
|
+
* @param {{ projectOnly?: boolean }} [opts]
|
|
33
41
|
* @returns {import("../checks/types.js").Check[]}
|
|
34
42
|
*/
|
|
35
|
-
function buildChecks(config) {
|
|
43
|
+
function buildChecks(config, { projectOnly = false } = {}) {
|
|
44
|
+
const projectDir = config?.projectDir || process.cwd();
|
|
45
|
+
if (projectOnly) {
|
|
46
|
+
return getProjectChecks({ projectDir });
|
|
47
|
+
}
|
|
36
48
|
return [
|
|
37
49
|
...getSystemChecks(),
|
|
38
50
|
...getNodeChecks(),
|
|
@@ -46,6 +58,7 @@ function buildChecks(config) {
|
|
|
46
58
|
...getSkillsChecks(),
|
|
47
59
|
...getCiChecks(),
|
|
48
60
|
...getRtkChecks(),
|
|
61
|
+
...getProjectChecks({ projectDir }),
|
|
49
62
|
];
|
|
50
63
|
}
|
|
51
64
|
|
|
@@ -77,8 +90,8 @@ function confirmViaTty(describe) {
|
|
|
77
90
|
* @param {{ config: Object, checkOnly?: boolean, yes?: boolean, onConfirm?: Function, logger?: Object }} args
|
|
78
91
|
* @returns {Promise<import("../checks/types.js").RunReport>}
|
|
79
92
|
*/
|
|
80
|
-
async function runDoctor({ config, checkOnly = false, yes = false, onConfirm, logger }) {
|
|
81
|
-
const checks = buildChecks(config);
|
|
93
|
+
async function runDoctor({ config, checkOnly = false, yes = false, onConfirm, logger, projectOnly = false }) {
|
|
94
|
+
const checks = buildChecks(config, { projectOnly });
|
|
82
95
|
return runCheckPipeline(checks, { config }, {
|
|
83
96
|
mode: checkOnly ? "check-only" : "fix",
|
|
84
97
|
yes,
|
|
@@ -105,8 +118,8 @@ export async function runChecks({ config }) {
|
|
|
105
118
|
* @param {{ config: Object, checkOnly?: boolean, yes?: boolean, json?: boolean, verbose?: boolean }} args
|
|
106
119
|
* @returns {Promise<number>}
|
|
107
120
|
*/
|
|
108
|
-
export async function doctorCommand({ config, checkOnly = false, yes = false, json = false, verbose = false }) {
|
|
109
|
-
const report = await runDoctor({ config, checkOnly, yes });
|
|
121
|
+
export async function doctorCommand({ config, checkOnly = false, yes = false, json = false, verbose = false, projectOnly = false }) {
|
|
122
|
+
const report = await runDoctor({ config, checkOnly, yes, projectOnly });
|
|
110
123
|
|
|
111
124
|
if (json) {
|
|
112
125
|
console.log(JSON.stringify(report, null, 2));
|
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
|
}
|