karajan-code 2.18.1 → 2.19.1

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.
Files changed (57) hide show
  1. package/README.md +53 -34
  2. package/docs/README.es.md +13 -2
  3. package/package.json +9 -1
  4. package/packages/hu-board/README.md +69 -0
  5. package/packages/hu-board/package.json +23 -0
  6. package/packages/hu-board/public/app.js +4038 -0
  7. package/packages/hu-board/public/index.html +55 -0
  8. package/packages/hu-board/public/pipeline.css +132 -0
  9. package/packages/hu-board/public/pipeline.html +48 -0
  10. package/packages/hu-board/public/pipeline.js +200 -0
  11. package/packages/hu-board/public/styles.css +918 -0
  12. package/packages/hu-board/src/auth.js +92 -0
  13. package/packages/hu-board/src/boot-info.js +16 -0
  14. package/packages/hu-board/src/cleanup-zombies.js +142 -0
  15. package/packages/hu-board/src/command-runner.js +134 -0
  16. package/packages/hu-board/src/config-yaml.js +285 -0
  17. package/packages/hu-board/src/db.js +739 -0
  18. package/packages/hu-board/src/ephemeral-cleaner.js +198 -0
  19. package/packages/hu-board/src/event-bus.js +58 -0
  20. package/packages/hu-board/src/format.js +83 -0
  21. package/packages/hu-board/src/hu-zombie-reaper.js +103 -0
  22. package/packages/hu-board/src/journal-parser.js +180 -0
  23. package/packages/hu-board/src/plan-mutations.js +422 -0
  24. package/packages/hu-board/src/preflight.js +285 -0
  25. package/packages/hu-board/src/routes/api.js +1271 -0
  26. package/packages/hu-board/src/routes/pipeline.js +45 -0
  27. package/packages/hu-board/src/run-tracker.js +121 -0
  28. package/packages/hu-board/src/server.js +435 -0
  29. package/packages/hu-board/src/sync.js +626 -0
  30. package/packages/hu-board/src/token-store.js +57 -0
  31. package/packages/hu-board/src/zombie-reaper.js +207 -0
  32. package/src/audit/webperf-input.js +4 -7
  33. package/src/brain/standby-store.js +6 -4
  34. package/src/checks/dir-setup.js +39 -3
  35. package/src/cli/register-pipeline.js +1 -0
  36. package/src/cli/register-plan.js +1 -0
  37. package/src/cli.js +13 -0
  38. package/src/commands/plan/generate.js +15 -0
  39. package/src/commands/run.js +19 -1
  40. package/src/commands/webperf.js +4 -6
  41. package/src/config/role-resolver.js +3 -1
  42. package/src/mcp/handlers/direct-handlers.js +3 -0
  43. package/src/mcp/handlers/run-handler.js +5 -0
  44. package/src/mcp/tools.js +4 -2
  45. package/src/plan/plan-store.js +7 -27
  46. package/src/prompts/spec-reviewer.js +66 -0
  47. package/src/roles/perf-role.js +4 -4
  48. package/src/roles/spec-reviewer-role.js +106 -0
  49. package/src/spec-review/refine-loop.js +58 -0
  50. package/src/spec-review/run-spec-review.js +70 -0
  51. package/src/utils/board-prompt-bridge.js +4 -5
  52. package/src/utils/display/spec-findings.js +44 -0
  53. package/src/utils/garbage-collector.js +8 -6
  54. package/src/utils/home-migration.js +87 -0
  55. package/src/utils/paths.js +85 -17
  56. package/src/utils/run-registry.js +4 -4
  57. 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
+ }