karajan-code 2.19.0 → 2.19.2
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 +4 -0
- package/package.json +9 -1
- package/packages/hu-board/README.md +69 -0
- package/packages/hu-board/package.json +23 -0
- package/packages/hu-board/public/app.js +4038 -0
- package/packages/hu-board/public/index.html +55 -0
- package/packages/hu-board/public/pipeline.css +132 -0
- package/packages/hu-board/public/pipeline.html +48 -0
- package/packages/hu-board/public/pipeline.js +200 -0
- package/packages/hu-board/public/styles.css +918 -0
- package/packages/hu-board/src/auth.js +92 -0
- package/packages/hu-board/src/boot-info.js +16 -0
- package/packages/hu-board/src/cleanup-zombies.js +142 -0
- package/packages/hu-board/src/command-runner.js +134 -0
- package/packages/hu-board/src/config-yaml.js +285 -0
- package/packages/hu-board/src/db.js +739 -0
- package/packages/hu-board/src/ephemeral-cleaner.js +198 -0
- package/packages/hu-board/src/event-bus.js +58 -0
- package/packages/hu-board/src/format.js +83 -0
- package/packages/hu-board/src/hu-zombie-reaper.js +103 -0
- package/packages/hu-board/src/journal-parser.js +180 -0
- package/packages/hu-board/src/plan-mutations.js +422 -0
- package/packages/hu-board/src/preflight.js +285 -0
- package/packages/hu-board/src/routes/api.js +1271 -0
- package/packages/hu-board/src/routes/pipeline.js +45 -0
- package/packages/hu-board/src/run-tracker.js +121 -0
- package/packages/hu-board/src/server.js +435 -0
- package/packages/hu-board/src/sync.js +626 -0
- package/packages/hu-board/src/token-store.js +57 -0
- package/packages/hu-board/src/zombie-reaper.js +207 -0
- package/src/audit/webperf-input.js +4 -7
- package/src/checks/dir-setup.js +5 -3
- package/src/cli/register-pipeline.js +1 -0
- package/src/cli/register-plan.js +1 -0
- package/src/commands/plan/generate.js +15 -0
- package/src/commands/run.js +19 -1
- package/src/commands/webperf.js +4 -6
- package/src/config/role-resolver.js +3 -1
- package/src/mcp/handlers/direct-handlers.js +3 -0
- package/src/mcp/handlers/run-handler.js +5 -0
- package/src/mcp/tools.js +4 -2
- package/src/prompts/spec-reviewer.js +66 -0
- package/src/roles/perf-role.js +4 -4
- package/src/roles/spec-reviewer-role.js +106 -0
- package/src/sonar/api.js +17 -3
- package/src/sonar/token-recovery.js +67 -0
- package/src/spec-review/refine-loop.js +58 -0
- package/src/spec-review/run-spec-review.js +70 -0
- package/src/utils/board-prompt-bridge.js +4 -5
- package/src/utils/display/spec-findings.js +44 -0
- package/src/utils/paths.js +15 -0
- package/src/utils/run-registry.js +4 -4
- package/templates/roles/spec-reviewer.md +38 -0
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Preflight checks for a project shown in the board.
|
|
3
|
+
*
|
|
4
|
+
* Goal: when a non-technical user opens a project on the board, they
|
|
5
|
+
* should see at a glance "is this ready to run, or am I missing
|
|
6
|
+
* something risky?" — every requirement listed with ✓ / ⚠ / ✗ and
|
|
7
|
+
* the consequence of each missing item in plain language.
|
|
8
|
+
*
|
|
9
|
+
* The same data backs the pre-run safety modal: if the user clicks
|
|
10
|
+
* ▶ Run plan / ▶ Run HU and any check returns `fail` (blocker) or
|
|
11
|
+
* `warn` (risky but not fatal), we show a confirm dialog listing
|
|
12
|
+
* what they're choosing to ignore. No silent surprises.
|
|
13
|
+
*
|
|
14
|
+
* The output is intentionally JSON-only so the front can render it
|
|
15
|
+
* without parsing CLI output. Each check has:
|
|
16
|
+
* id stable identifier for tests + UI hooks
|
|
17
|
+
* label short Spanish label rendered next to the icon
|
|
18
|
+
* status "ok" | "warn" | "fail" | "info"
|
|
19
|
+
* detail short string describing what was found
|
|
20
|
+
* consequence what the user loses if status is warn/fail (plain
|
|
21
|
+
* Spanish, written for someone who is NOT a developer)
|
|
22
|
+
* blocking true → run buttons require explicit confirm
|
|
23
|
+
*
|
|
24
|
+
* Lookups are non-blocking: if a CLI binary is not on PATH the
|
|
25
|
+
* version check returns `warn` with a hint, never throws. The whole
|
|
26
|
+
* endpoint must respond fast (< 500 ms) so the panel never blocks
|
|
27
|
+
* the board UI.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { execSync } from 'node:child_process';
|
|
31
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
32
|
+
import { join } from 'node:path';
|
|
33
|
+
|
|
34
|
+
function runQuiet(cmd, opts = {}) {
|
|
35
|
+
try {
|
|
36
|
+
const out = execSync(cmd, { stdio: ['ignore', 'pipe', 'ignore'], encoding: 'utf8', timeout: 3000, ...opts });
|
|
37
|
+
return { ok: true, out: out.trim() };
|
|
38
|
+
} catch (err) {
|
|
39
|
+
return { ok: false, err };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function checkGitInit(projectDir) {
|
|
44
|
+
const ok = existsSync(join(projectDir, '.git'));
|
|
45
|
+
return ok
|
|
46
|
+
? { id: 'git_init', label: 'Git inicializado', status: 'ok', detail: '.git/ presente', blocking: false }
|
|
47
|
+
: {
|
|
48
|
+
id: 'git_init', label: 'Git inicializado', status: 'fail',
|
|
49
|
+
detail: 'no se encontró .git/',
|
|
50
|
+
consequence: 'Karajan no podrá crear ramas, commits ni PRs. La ejecución fallará en el primer paso.',
|
|
51
|
+
blocking: true
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function checkGitRemote(projectDir) {
|
|
56
|
+
const r = runQuiet(`git -C "${projectDir}" remote -v`);
|
|
57
|
+
if (!r.ok) {
|
|
58
|
+
return {
|
|
59
|
+
id: 'git_remote', label: 'Repositorio remoto', status: 'fail',
|
|
60
|
+
detail: 'git remote falló — ¿no es un repo git?',
|
|
61
|
+
consequence: 'Sin remoto, no se podrán abrir Pull Requests entre roles ni colaborar con el reviewer remoto.',
|
|
62
|
+
blocking: false
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
if (!r.out) {
|
|
66
|
+
return {
|
|
67
|
+
id: 'git_remote', label: 'Repositorio remoto', status: 'warn',
|
|
68
|
+
detail: 'no hay remote configurado',
|
|
69
|
+
consequence: 'Karajan funcionará en local: hará commits pero no podrá abrir Pull Requests automáticos. El reviewer trabajará sobre tu copia local en lugar de un PR remoto. Para uso real, configura un remote (ej. `git remote add origin git@github.com:tu-user/tu-repo.git`).',
|
|
70
|
+
blocking: false
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
// Capture first remote URL for display.
|
|
74
|
+
const firstLine = r.out.split('\n')[0] || '';
|
|
75
|
+
const remoteUrl = firstLine.split(/\s+/)[1] || '(unknown)';
|
|
76
|
+
return { id: 'git_remote', label: 'Repositorio remoto', status: 'ok', detail: remoteUrl, blocking: false };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function checkGitBranch(projectDir) {
|
|
80
|
+
const r = runQuiet(`git -C "${projectDir}" rev-parse --abbrev-ref HEAD`);
|
|
81
|
+
if (!r.ok) return { id: 'git_branch', label: 'Rama actual', status: 'info', detail: '(no disponible)', blocking: false };
|
|
82
|
+
return { id: 'git_branch', label: 'Rama actual', status: 'info', detail: r.out, blocking: false };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function checkGitClean(projectDir) {
|
|
86
|
+
const r = runQuiet(`git -C "${projectDir}" status --porcelain`);
|
|
87
|
+
if (!r.ok) return { id: 'git_clean', label: 'Estado del working tree', status: 'info', detail: '(no disponible)', blocking: false };
|
|
88
|
+
if (!r.out) return { id: 'git_clean', label: 'Working tree limpio', status: 'ok', detail: 'sin cambios pendientes', blocking: false };
|
|
89
|
+
const count = r.out.split('\n').length;
|
|
90
|
+
return {
|
|
91
|
+
id: 'git_clean', label: 'Working tree con cambios', status: 'warn',
|
|
92
|
+
detail: `${count} archivo(s) modificado(s)/sin trackear`,
|
|
93
|
+
consequence: 'Karajan trabaja sobre tu copia actual. Si tienes cambios sin commitear, podrían mezclarse con los del coder y dificultar la revisión. Recomendado: commitea o stashéa antes de lanzar.',
|
|
94
|
+
blocking: false
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function checkNodeVersion() {
|
|
99
|
+
const v = process.version; // e.g. "v22.21.1"
|
|
100
|
+
const major = Number(v.replace(/^v/, '').split('.')[0]);
|
|
101
|
+
if (major >= 20) return { id: 'node_version', label: 'Node.js', status: 'ok', detail: v, blocking: false };
|
|
102
|
+
return {
|
|
103
|
+
id: 'node_version', label: 'Node.js', status: 'fail',
|
|
104
|
+
detail: `${v} (se requiere ≥ 20)`,
|
|
105
|
+
consequence: 'Las dependencias y los agentes asumen Node 20+. En versiones anteriores puede crashear sin mensaje claro.',
|
|
106
|
+
blocking: true
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function checkKarajanVersion() {
|
|
111
|
+
// Read package.json from the karajan-code repo this board ships in.
|
|
112
|
+
// The board package lives at packages/hu-board/, so the parent
|
|
113
|
+
// package.json is two levels up.
|
|
114
|
+
const candidatePaths = [
|
|
115
|
+
join(import.meta.dirname || '', '..', '..', '..', 'package.json'),
|
|
116
|
+
join(process.cwd(), 'package.json'),
|
|
117
|
+
];
|
|
118
|
+
for (const p of candidatePaths) {
|
|
119
|
+
try {
|
|
120
|
+
const pkg = JSON.parse(readFileSync(p, 'utf8'));
|
|
121
|
+
if (pkg?.name === 'karajan-code' && pkg?.version) {
|
|
122
|
+
return { id: 'kj_version', label: 'Karajan Code', status: 'ok', detail: `v${pkg.version}`, blocking: false };
|
|
123
|
+
}
|
|
124
|
+
} catch { /* keep trying */ }
|
|
125
|
+
}
|
|
126
|
+
return { id: 'kj_version', label: 'Karajan Code', status: 'info', detail: '(versión no detectada)', blocking: false };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const AGENT_BINS = [
|
|
130
|
+
{ id: 'agent_claude', bin: 'claude', label: 'Claude CLI', versionFlag: '--version' },
|
|
131
|
+
{ id: 'agent_codex', bin: 'codex', label: 'Codex CLI', versionFlag: '--version' },
|
|
132
|
+
{ id: 'agent_gemini', bin: 'gemini', label: 'Gemini CLI', versionFlag: '--version' },
|
|
133
|
+
{ id: 'agent_aider', bin: 'aider', label: 'Aider', versionFlag: '--version' },
|
|
134
|
+
];
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Returns the set of agent ids the user has explicitly declared in
|
|
138
|
+
* their kj.config.yml. An agent that is missing from $PATH is only a
|
|
139
|
+
* problem when it's declared (coder/reviewer/etc.); otherwise it's
|
|
140
|
+
* just "not installed" — informational, not a warning.
|
|
141
|
+
*
|
|
142
|
+
* Reads the same yml the editor modal writes (~/.karajan/kj.config.yml).
|
|
143
|
+
* Returns a Set of bin names ("claude", "codex", "gemini", "aider").
|
|
144
|
+
*/
|
|
145
|
+
function detectDeclaredAgents() {
|
|
146
|
+
const declared = new Set();
|
|
147
|
+
try {
|
|
148
|
+
const home = process.env.KJ_HOME || process.env.HOME || '';
|
|
149
|
+
// The yml lives in two possible locations depending on how the
|
|
150
|
+
// user installed Karajan; check both for forwards-compat.
|
|
151
|
+
const candidates = [join(home, '.karajan', 'kj.config.yml'), join(home, 'kj.config.yml')];
|
|
152
|
+
for (const p of candidates) {
|
|
153
|
+
if (!existsSync(p)) continue;
|
|
154
|
+
const raw = readFileSync(p, 'utf8');
|
|
155
|
+
// Don't pull in js-yaml just to read a few top-level keys —
|
|
156
|
+
// a tolerant regex scan over `coder:` / `reviewer:` / `roles.X.provider:`
|
|
157
|
+
// is enough and keeps preflight zero-dep.
|
|
158
|
+
const topLevel = raw.match(/^(?:coder|reviewer|planner|architect|triage|researcher|tester|security|refactorer|impeccable)\s*:\s*([\w-]+)\s*$/gm) || [];
|
|
159
|
+
for (const line of topLevel) {
|
|
160
|
+
const m = /:\s*([\w-]+)\s*$/.exec(line);
|
|
161
|
+
if (m) declared.add(m[1]);
|
|
162
|
+
}
|
|
163
|
+
const rolesProviders = raw.match(/^\s+provider\s*:\s*([\w-]+)\s*$/gm) || [];
|
|
164
|
+
for (const line of rolesProviders) {
|
|
165
|
+
const m = /:\s*([\w-]+)\s*$/.exec(line);
|
|
166
|
+
if (m) declared.add(m[1]);
|
|
167
|
+
}
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
} catch { /* config unreadable → declare nothing, every absence is just info */ }
|
|
171
|
+
return declared;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Check one agent CLI. Status semantics changed in PR-C:
|
|
176
|
+
*
|
|
177
|
+
* - found on PATH → "ok" (✓, with version)
|
|
178
|
+
* - not on PATH, not used → "info" (gray, "(no instalado)")
|
|
179
|
+
* - not on PATH, declared → "warn" (yellow, with consequence)
|
|
180
|
+
*
|
|
181
|
+
* The user explicitly asked for this: missing Aider should NOT be a
|
|
182
|
+
* warning when they don't use Aider. Only flag absence as a problem
|
|
183
|
+
* when the kj.config.yml expects it.
|
|
184
|
+
*/
|
|
185
|
+
function checkAgent({ id, bin, label, versionFlag }, declaredAgents) {
|
|
186
|
+
const which = runQuiet(`command -v ${bin}`);
|
|
187
|
+
if (!which.ok) {
|
|
188
|
+
if (declaredAgents.has(bin)) {
|
|
189
|
+
return {
|
|
190
|
+
id, label, status: 'warn',
|
|
191
|
+
detail: 'declarado en kj.config.yml pero no encontrado en PATH',
|
|
192
|
+
consequence: `Tu pipeline declara usar ${label.replace(' CLI', '')} pero el binario no está instalado. Las ejecuciones que lo invoquen fallarán. Instálalo o cambia el agente del rol en la configuración.`,
|
|
193
|
+
blocking: false
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
return {
|
|
197
|
+
id, label, status: 'info',
|
|
198
|
+
detail: 'no instalado',
|
|
199
|
+
blocking: false
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
const ver = runQuiet(`${bin} ${versionFlag}`);
|
|
203
|
+
const vStr = ver.ok ? ver.out.split('\n')[0].trim() : '(versión desconocida)';
|
|
204
|
+
return { id, label, status: 'ok', detail: vStr, blocking: false };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function checkConfigYml() {
|
|
208
|
+
const home = process.env.KJ_HOME || process.env.HOME || '';
|
|
209
|
+
const candidates = [join(home, '.karajan', 'kj.config.yml'), join(home, 'kj.config.yml')];
|
|
210
|
+
for (const p of candidates) {
|
|
211
|
+
if (existsSync(p)) {
|
|
212
|
+
return { id: 'config_yml', label: 'Configuración global', status: 'ok', detail: p, blocking: false };
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return {
|
|
216
|
+
id: 'config_yml', label: 'Configuración global', status: 'warn',
|
|
217
|
+
detail: 'no se encontró ~/.karajan/kj.config.yml',
|
|
218
|
+
consequence: 'Karajan usará valores por defecto. Funcionará, pero no podrás personalizar coder/reviewer/modelos.',
|
|
219
|
+
blocking: false
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function checkPlans(projectId) {
|
|
224
|
+
// Plans live at ~/.kj/plans/<projectId>/*.json (or
|
|
225
|
+
// $KJ_HOME/plans/<projectId>/*.json under VITEST or custom config).
|
|
226
|
+
// Mirror plan-mutations.js::plansRoot() so the path matches reality.
|
|
227
|
+
const dir = process.env.KJ_HOME
|
|
228
|
+
? join(process.env.KJ_HOME, 'plans', projectId)
|
|
229
|
+
: join(process.env.HOME || '', '.kj', 'plans', projectId);
|
|
230
|
+
if (!existsSync(dir)) {
|
|
231
|
+
return {
|
|
232
|
+
id: 'plans', label: 'Plans en disco', status: 'info',
|
|
233
|
+
detail: 'sin plans aún',
|
|
234
|
+
blocking: false
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
try {
|
|
238
|
+
const files = readdirSync(dir).filter(f => f.endsWith('.json'));
|
|
239
|
+
return { id: 'plans', label: 'Plans en disco', status: 'ok', detail: `${files.length} plan(s)`, blocking: false };
|
|
240
|
+
} catch {
|
|
241
|
+
return { id: 'plans', label: 'Plans en disco', status: 'info', detail: '(no se pudo leer)', blocking: false };
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Run all preflight checks for a project.
|
|
247
|
+
*
|
|
248
|
+
* @param {object} args
|
|
249
|
+
* @param {string} args.projectId - The board's project slug.
|
|
250
|
+
* @param {string|null} args.projectDir - Absolute project path. When
|
|
251
|
+
* the project has no plan yet (or no projectDir stamped) we skip
|
|
252
|
+
* git-related checks instead of failing them — git state is only
|
|
253
|
+
* meaningful when we know which working copy to inspect.
|
|
254
|
+
* @returns {Promise<{checks: object[], blockers: object[], warnings: object[]}>}
|
|
255
|
+
*/
|
|
256
|
+
export async function runPreflight({ projectId, projectDir }) {
|
|
257
|
+
const checks = [];
|
|
258
|
+
if (projectDir && existsSync(projectDir)) {
|
|
259
|
+
checks.push(checkGitInit(projectDir));
|
|
260
|
+
checks.push(checkGitRemote(projectDir));
|
|
261
|
+
checks.push(checkGitBranch(projectDir));
|
|
262
|
+
checks.push(checkGitClean(projectDir));
|
|
263
|
+
} else {
|
|
264
|
+
checks.push({
|
|
265
|
+
id: 'project_dir', label: 'Directorio del proyecto', status: 'warn',
|
|
266
|
+
detail: projectDir ? `no existe: ${projectDir}` : '(no detectado, abre un plan para que se registre)',
|
|
267
|
+
consequence: 'Sin un directorio de proyecto válido, Karajan no sabe sobre qué trabajar.',
|
|
268
|
+
blocking: !projectDir
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
checks.push(checkNodeVersion());
|
|
272
|
+
checks.push(checkKarajanVersion());
|
|
273
|
+
// PR-C: agents are checked against the declared set in kj.config.yml.
|
|
274
|
+
// Absent + undeclared → "info" (just informational, no warning sign).
|
|
275
|
+
// Absent + declared → "warn" (real problem: the pipeline can't run).
|
|
276
|
+
const declaredAgents = detectDeclaredAgents();
|
|
277
|
+
for (const a of AGENT_BINS) checks.push(checkAgent(a, declaredAgents));
|
|
278
|
+
checks.push(checkConfigYml());
|
|
279
|
+
checks.push(checkPlans(projectId));
|
|
280
|
+
|
|
281
|
+
const blockers = checks.filter(c => c.status === 'fail' && c.blocking);
|
|
282
|
+
const warnings = checks.filter(c => c.status === 'warn' || (c.status === 'fail' && !c.blocking));
|
|
283
|
+
|
|
284
|
+
return { projectId, projectDir: projectDir || null, checks, blockers, warnings };
|
|
285
|
+
}
|