karajan-code 4.23.0 → 4.24.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 CHANGED
@@ -59,6 +59,14 @@ kj init && kj env install && kj harden && kj review --install-gate
59
59
  git config core.hooksPath .karajan/hooks
60
60
  ```
61
61
 
62
+ Already installed and just want to work — even with no computing background? One command:
63
+
64
+ ```sh
65
+ kj go
66
+ ```
67
+
68
+ It detects your agent (Claude Code or Codex; asks which one only if you have both), prepares the project silently the first time, opens the board in your browser, and drops you into a conversation that already follows the method. Your agent account and login stay yours — kj never touches credentials.
69
+
62
70
  Requires git and at least one AI agent CLI — two enables cross-AI review; three enables arbitration. The npm route is `npm install -g @karajan-family/code` (published as `karajan-code` before joining the scope; the legacy name still installs the same versions). All install routes (npm, binaries, brew, Python wrapper) in the [install docs](https://karajancode.com/docs/v4/install/).
63
71
 
64
72
  ## The daily loop
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "karajan-code",
3
- "version": "4.23.0",
3
+ "version": "4.24.0",
4
4
  "description": "Local multi-agent coding orchestrator with TDD, SonarQube, and code review pipeline",
5
5
  "type": "module",
6
6
  "license": "AGPL-3.0",
@@ -53,6 +53,7 @@
53
53
  <div class="modal" id="modal-content"></div>
54
54
  </div>
55
55
 
56
+ <script src="/utils/maggle-mode.js"></script>
56
57
  <script src="/utils/formatters.js"></script>
57
58
  <script src="/utils/modals.js"></script>
58
59
  <script src="/utils/api.js"></script>
@@ -87,48 +87,49 @@ async function renderBoard() {
87
87
 
88
88
  // KJC-TSK-0403: 3 canonical lanes. Failed eliminado — HUs con
89
89
  // result=fail aparecen en Pending con badge ✗.
90
+ // Maggle mode (KJC-TSK-0810): plain label first, jargon as tooltip.
90
91
  const visibleColumns = [
91
- { title: 'Pending', cls: 'pending', rows: columns.pending },
92
- { title: 'Running', cls: 'running', rows: columns.running },
93
- { title: 'Done', cls: 'done', rows: columns.done },
92
+ { title: maggleText('column.pending', 'Pending'), cls: 'pending', rows: columns.pending },
93
+ { title: maggleText('column.running', 'Running'), cls: 'running', rows: columns.running },
94
+ { title: maggleText('column.done', 'Done'), cls: 'done', rows: columns.done },
94
95
  ];
95
96
 
96
97
  app.innerHTML = `
97
98
  <div class="section-header">
98
- <span class="section-header__title" title="${selectedProject ? esc(selectedProject) : ''}">Story Board${selectedProject ? ` - ${esc(projectDisplayName)}` : ''}</span>
99
+ <span class="section-header__title" title="${selectedProject ? esc(selectedProject) : ''}">${maggleText('board.title', 'Story Board')}${selectedProject ? ` - ${esc(projectDisplayName)}` : ''}</span>
99
100
  ${selectedProject ? `
100
101
  <button class="control-btn project-rename-btn"
101
102
  style="background:transparent;border:none;color:var(--text-muted);cursor:pointer;font-size:0.9rem;padding:2px 6px;"
102
103
  title="Renombrar este proyecto"
103
104
  onclick="event.stopPropagation(); window.renameProjectModal('${esc(selectedProject)}', '${esc(projectDisplayName.replace(/'/g, '&#39;'))}');">✎</button>
104
105
  ` : ''}
105
- <span class="section-header__count">${stories.length} stories</span>
106
+ <span class="section-header__count" title="stories">${stories.length} ${maggleText('board.stories', 'stories')}</span>
106
107
  ${costSummary ? `<span class="section-header__cost" title="${esc(costSummary.tooltip)}">💵 ${esc(costSummary.label)}</span>` : ''}
107
108
  ${cacheSummary ? `<span class="section-header__cache" title="${esc(cacheSummary.tooltip)}">${esc(cacheSummary.label)}</span>` : ''}
108
109
  ${isRunning ? `
109
110
  <button id="running-badge-btn" class="section-header__badge"
110
111
  style="margin-left:auto;padding:4px 10px;font-size:0.8rem;background:var(--color-yellow,#eab308);color:#000;border-radius:var(--radius-sm);font-weight:600;border:none;cursor:pointer;"
111
112
  title="Abrir el log de la HU en marcha">
112
- ⚙ ${runningCount} running…
113
+ ⚙ ${runningCount} ${maggleText('board.running', 'running')}…
113
114
  </button>
114
115
  <button id="stop-run-btn" class="control-btn"
115
116
  style="padding:4px 10px;font-size:0.8rem;background:var(--color-red,#ef4444);color:#fff;border:none;border-radius:var(--radius-sm);cursor:pointer;font-weight:600;"
116
- title="Abortar todos los kj run en marcha de este proyecto (SIGTERM con escalado a SIGKILL tras 5s)">
117
- ⏹ Stop
117
+ title="${maggleText('board.stopTitle', 'Abortar todos los kj run en marcha de este proyecto (SIGTERM con escalado a SIGKILL tras 5s)')}">
118
+ ${maggleText('board.stop', '⏹ Stop')}
118
119
  </button>
119
120
  ` : ''}
120
121
  ${lastOpenedLog ? `
121
122
  <button class="control-btn" id="view-log-btn"
122
123
  style="${isRunning ? '' : 'margin-left:auto;'}padding:6px 12px;font-size:0.85rem;background:var(--bg-primary);border:1px solid var(--border);color:var(--text);border-radius:var(--radius-sm);cursor:pointer;"
123
124
  title="Re-open the log of the most recent run/command">
124
- 📜 View log
125
+ ${maggleText('board.viewLog', '📜 View log')}
125
126
  </button>
126
127
  ` : ''}
127
128
  ${canRun ? `
128
129
  <button class="control-btn" id="run-plan-btn"
129
130
  style="margin-left:auto;padding:6px 14px;font-size:0.9rem;background:var(--color-green);color:#fff;border:none;border-radius:var(--radius-sm);cursor:pointer;font-weight:600;"
130
- title="Launch kj run --plan over every plan in this project">
131
- ▶ Run plan (${awaitingCount} HU${awaitingCount === 1 ? '' : 's'})
131
+ title="${maggleText('board.runTitle', 'Launch kj run --plan over every plan in this project')}">
132
+ ${maggleText('board.run', '▶ Run plan')} (${awaitingCount} ${maggleText('board.story', 'HU')}${awaitingCount === 1 ? '' : 's'})
132
133
  </button>
133
134
  ` : ''}
134
135
  </div>
@@ -138,7 +139,7 @@ async function renderBoard() {
138
139
  </div>
139
140
  </div>
140
141
  <div id="rag-panel" class="rag-panel" style="margin:8px 0;display:flex;gap:8px;align-items:flex-start;flex-wrap:wrap;">
141
- <input id="rag-query" type="text" placeholder="🔍 RAG search: ask anything about this project's plans / onboarding / code…"
142
+ <input id="rag-query" type="text" placeholder="${maggleText('board.ragPlaceholder', "🔍 RAG search: ask anything about this project's plans / onboarding / code…")}"
142
143
  style="flex:1;min-width:260px;padding:6px 10px;font-size:0.85rem;background:var(--bg-primary);border:1px solid var(--border);color:var(--text);border-radius:var(--radius-sm);" />
143
144
  <select id="rag-scope" style="padding:6px 10px;font-size:0.85rem;background:var(--bg-primary);border:1px solid var(--border);color:var(--text);border-radius:var(--radius-sm);">
144
145
  <option value="all">All</option><option value="plans">Plans</option><option value="onboarding">Onboarding</option><option value="code">Code</option>
@@ -305,7 +306,7 @@ function renderKanbanColumn(title, cssClass, stories) {
305
306
  return `
306
307
  <div class="kanban__column kanban__column--${cssClass}" data-column="${cssClass}"${stories.length === 0 ? ' style="opacity:0.55"' : ''}>
307
308
  <div class="kanban__column-header">
308
- <span class="kanban__column-title">${title}</span>
309
+ <span class="kanban__column-title" title="${cssClass}">${title}</span>
309
310
  <span class="kanban__column-count" data-column-count>${stories.length}</span>
310
311
  </div>
311
312
  ${stories.map(renderStoryCard).join('')}
@@ -60,10 +60,25 @@ async function showCommandLauncher() {
60
60
  },
61
61
  };
62
62
 
63
+ // Maggle mode (KJC-TSK-0810 AC2): the frequent action leads in plain
64
+ // language — one textarea, clear promise of what will happen. The full
65
+ // technical launcher (file paths, architect, clean) stays one click
66
+ // away behind "Más opciones…".
67
+ const maggle = isMaggleMode();
68
+ let advancedVisible = !maggle;
69
+ if (maggle) {
70
+ SCHEMAS.plan.label = maggleText('launcher.planLabel', SCHEMAS.plan.label);
71
+ SCHEMAS.plan.help = maggleText('launcher.planHelp', SCHEMAS.plan.help);
72
+ SCHEMAS.plan.simpleFields = [
73
+ { name: 'task', label: maggleText('launcher.taskLabel', 'Task'), type: 'textarea', rows: 6 },
74
+ ];
75
+ }
76
+
63
77
  let active = 'plan';
64
78
  const renderForm = () => {
65
79
  const s = SCHEMAS[active];
66
- const fields = s.fields.map((f) => {
80
+ const visibleFields = (!advancedVisible && s.simpleFields) ? s.simpleFields : s.fields;
81
+ const fields = visibleFields.map((f) => {
67
82
  const id = `cmd-field-${f.name}`;
68
83
  if (f.type === 'textarea') {
69
84
  return `
@@ -90,14 +105,14 @@ async function showCommandLauncher() {
90
105
 
91
106
  dlg.innerHTML = `
92
107
  <div style="padding:14px 18px;border-bottom:1px solid var(--border);font-weight:600;display:flex;align-items:center;justify-content:space-between;gap:10px">
93
- <span>⚡ Run a Karajan command</span>
108
+ <span>${esc(maggleText('launcher.title', '⚡ Run a Karajan command'))}</span>
94
109
  <button id="cmd-close" type="button" class="control-btn"
95
110
  style="padding:4px 10px;border:1px solid var(--border);background:var(--bg-primary);color:var(--text);border-radius:var(--radius-sm);cursor:pointer">
96
111
  ✕
97
112
  </button>
98
113
  </div>
99
114
  <div style="padding:12px 18px;display:flex;gap:8px;border-bottom:1px solid var(--border)">
100
- ${Object.entries(SCHEMAS).map(([key, s]) => `
115
+ ${Object.entries(SCHEMAS).filter(([key]) => advancedVisible || key === 'plan').map(([key, s]) => `
101
116
  <button type="button" data-cmd="${key}"
102
117
  style="padding:6px 12px;border:1px solid var(--border);
103
118
  background:${key === active ? 'var(--color-green)' : 'var(--bg-primary)'};
@@ -106,6 +121,13 @@ async function showCommandLauncher() {
106
121
  ${esc(s.label)}
107
122
  </button>
108
123
  `).join('')}
124
+ ${maggle && !advancedVisible ? `
125
+ <button type="button" id="cmd-more"
126
+ style="margin-left:auto;padding:6px 12px;border:1px solid var(--border);background:var(--bg-primary);color:var(--text-muted);border-radius:var(--radius-sm);cursor:pointer;font-size:0.85rem"
127
+ title="Comandos técnicos: kj plan con ficheros, kj architect, kj clean">
128
+ ${esc(maggleText('launcher.more', 'More…'))}
129
+ </button>
130
+ ` : ''}
109
131
  </div>
110
132
  <form id="cmd-form" onsubmit="return false"
111
133
  style="padding:14px 18px;display:flex;flex-direction:column;gap:12px;width:min(640px, 88vw);box-sizing:border-box">
@@ -114,11 +136,11 @@ async function showCommandLauncher() {
114
136
  <div style="display:flex;justify-content:flex-end;gap:8px;padding-top:8px;border-top:1px solid var(--border)">
115
137
  <button type="button" id="cmd-cancel" class="control-btn"
116
138
  style="padding:6px 14px;border:1px solid var(--border);background:var(--bg-primary);color:var(--text);border-radius:var(--radius-sm);cursor:pointer">
117
- Cancel
139
+ ${esc(maggleText('launcher.cancel', 'Cancel'))}
118
140
  </button>
119
141
  <button type="button" id="cmd-submit" class="control-btn"
120
142
  style="padding:6px 14px;border:none;background:var(--color-green);color:#fff;border-radius:var(--radius-sm);cursor:pointer;font-weight:600">
121
- Run
143
+ ${esc(maggleText('launcher.submit', 'Run'))}
122
144
  </button>
123
145
  </div>
124
146
  </form>
@@ -127,6 +149,8 @@ async function showCommandLauncher() {
127
149
  dlg.querySelectorAll('[data-cmd]').forEach((btn) => {
128
150
  btn.addEventListener('click', () => { active = btn.dataset.cmd; renderForm(); });
129
151
  });
152
+ const moreBtn = dlg.querySelector('#cmd-more');
153
+ if (moreBtn) moreBtn.addEventListener('click', () => { advancedVisible = true; renderForm(); });
130
154
  dlg.querySelector('#cmd-close').addEventListener('click', () => dlg.close());
131
155
  dlg.querySelector('#cmd-cancel').addEventListener('click', () => dlg.close());
132
156
  dlg.querySelector('#cmd-submit').addEventListener('click', () => submitCommand(active));
@@ -146,9 +170,21 @@ async function showCommandLauncher() {
146
170
  }
147
171
  // plan / architect: at least one of taskFile or task is required.
148
172
  if ((command === 'plan' || command === 'architect') && !body.taskFile && !body.task) {
149
- await showError('Provide either a SPEC file path or paste the task text.', { title: 'Missing input' });
173
+ await showError(
174
+ maggle ? 'Escribe primero qué quieres pedir — con tus palabras vale.' : 'Provide either a SPEC file path or paste the task text.',
175
+ { title: 'Missing input' }
176
+ );
150
177
  return;
151
178
  }
179
+ // AC2: before launching, the maggle confirms knowing exactly what
180
+ // will happen — in their language, never a command name alone.
181
+ if (maggle && command === 'plan') {
182
+ const ok = await showConfirm(maggleText('launcher.confirm', ''), {
183
+ title: SCHEMAS.plan.label,
184
+ okLabel: maggleText('launcher.submit', 'Run'),
185
+ });
186
+ if (!ok) return;
187
+ }
152
188
  if (command === 'clean' && body.nuke) {
153
189
  const ok = await showConfirm(
154
190
  '`kj clean --nuke` will delete EVERY plan / session / batch + wipe the HU Board DB. Cannot be undone.',
@@ -79,13 +79,13 @@ function openGenericLogPanel({ id, label, tailUrl }) {
79
79
  border-bottom:1px solid var(--border);
80
80
  user-select:none;flex-shrink:0">
81
81
  <div style="display:flex;align-items:baseline;gap:10px;min-width:0">
82
- <strong style="color:var(--text)">${esc(label)}</strong>
82
+ <strong style="color:var(--text)" title="${esc(label)}">${esc(maggleText('log.label', label))}</strong>
83
83
  <span style="font-family:var(--font-mono, monospace);font-size:0.78rem;
84
84
  color:var(--text-muted);overflow:hidden;text-overflow:ellipsis;
85
85
  white-space:nowrap">${esc(id)}</span>
86
86
  </div>
87
87
  <div style="display:flex;align-items:center;gap:6px;flex-shrink:0">
88
- <span id="log-status" style="font-size:0.75rem;color:var(--text-muted);margin-right:8px">connecting…</span>
88
+ <span id="log-status" style="font-size:0.75rem;color:var(--text-muted);margin-right:8px">${maggleText('log.connecting', 'connecting…')}</span>
89
89
  <button id="log-min" type="button" title="Minimize (run keeps going)"
90
90
  style="${winBtnStyle('#facc15')}">_</button>
91
91
  <button id="log-max" type="button" title="Maximize / Restore"
@@ -101,8 +101,8 @@ function openGenericLogPanel({ id, label, tailUrl }) {
101
101
  white-space:pre-wrap;word-break:break-word"></pre>
102
102
  <footer style="padding:6px 12px;border-top:1px solid var(--border);
103
103
  font-size:0.7rem;color:var(--text-muted);flex-shrink:0">
104
- Closing this panel does NOT stop the run. It keeps going in the background;
105
- reopen with the 📜 View log button on the board.
104
+ ${maggleText('log.footer', `Closing this panel does NOT stop the run. It keeps going in the background;
105
+ reopen with the 📜 View log button on the board.`)}
106
106
  </footer>
107
107
  `;
108
108
 
@@ -0,0 +1,124 @@
1
+ // KJC-TSK-0810 (MGL-C) — maggle mode: the board in plain language.
2
+ //
3
+ // Classic script (no exports), loaded FIRST among the utils so the other
4
+ // scripts can call maggleText() at render time. `kj go` opens the board at
5
+ // /?maggle=1; the choice persists in localStorage so reloads keep the
6
+ // plain-language UI, and ?maggle=0 (the "modo experto" link) clears it.
7
+ // A dev opening the board directly sees no change. If storage is
8
+ // unavailable the mode degrades to the technical UI — it never breaks.
9
+
10
+ const MAGGLE_STORE_KEY = 'kj-maggle-mode';
11
+
12
+ // Plain label first; the technical term survives as tooltip/secondary
13
+ // detail where each call site renders it (AC1: llano delante, jerga detrás).
14
+ const MAGGLE_LABELS = {
15
+ 'column.pending': 'Por hacer',
16
+ 'column.running': 'En marcha',
17
+ 'column.done': 'Hecho',
18
+ 'board.title': 'Tu trabajo',
19
+ 'board.stories': 'tareas',
20
+ 'board.story': 'tarea',
21
+ 'board.run': '▶ Continuar con lo pendiente',
22
+ 'board.runTitle': 'Karajan seguirá con las tareas que faltan, una a una. Podrás ver la actividad aquí.',
23
+ 'board.stop': '⏹ Parar',
24
+ 'board.stopTitle': 'Detiene el trabajo en marcha. Lo ya terminado se conserva.',
25
+ 'board.viewLog': '📜 Ver actividad',
26
+ 'board.running': 'en marcha',
27
+ 'board.ragPlaceholder': '🔍 Pregunta lo que quieras sobre este proyecto…',
28
+ 'header.subtitle': 'tu tablero de trabajo',
29
+ 'nav.board': 'Tablero',
30
+ 'nav.dashboard': 'Proyectos',
31
+ 'nav.more': 'Más',
32
+ 'picker.hint': 'Elige un proyecto para ver su tablero. Puedes cambiar de proyecto arriba a la derecha.',
33
+ 'picker.project': 'proyecto',
34
+ 'launcher.title': '📝 Pedir trabajo a Karajan',
35
+ 'launcher.planLabel': '📝 Pedir trabajo nuevo',
36
+ 'launcher.planHelp': 'Cuéntale a Karajan qué quieres construir o cambiar, con tus palabras. Lo convertirá en tareas que verás en el tablero — todavía no toca tu código.',
37
+ 'launcher.taskLabel': 'Qué quieres pedir',
38
+ 'launcher.submit': 'Pedir',
39
+ 'launcher.cancel': 'Cancelar',
40
+ 'launcher.more': 'Más opciones…',
41
+ 'launcher.confirm': 'Voy a convertir tu petición en tareas del tablero. Tardará unos minutos y podrás seguir la actividad en esta ventana. ¿Sigo?',
42
+ 'log.label': 'Actividad',
43
+ 'log.connecting': 'conectando…',
44
+ 'log.footer': 'Cerrar esta ventana NO detiene el trabajo: sigue en marcha. Vuelve a abrirla con el botón «📜 Ver actividad» del tablero.',
45
+ };
46
+
47
+ function isMaggleMode() {
48
+ try {
49
+ const flag = new URLSearchParams(location.search || '').get('maggle');
50
+ if (flag === '1') { localStorage.setItem(MAGGLE_STORE_KEY, '1'); return true; }
51
+ if (flag === '0') { localStorage.removeItem(MAGGLE_STORE_KEY); return false; }
52
+ return localStorage.getItem(MAGGLE_STORE_KEY) === '1';
53
+ } catch {
54
+ return false;
55
+ }
56
+ }
57
+
58
+ /** Plain label when maggle mode is on; the technical one otherwise. */
59
+ function maggleText(key, technical) {
60
+ if (!isMaggleMode()) return technical;
61
+ return MAGGLE_LABELS[key] || technical;
62
+ }
63
+
64
+ // AC4: an error shown to a maggle is never a stacktrace alone. Pure —
65
+ // the caller (showError) decides by mode and renders the HTML.
66
+ function maggleErrorParts(rawMessage) {
67
+ return {
68
+ headline: 'Algo no ha salido como se esperaba',
69
+ next: 'No has roto nada. Vuelve a intentarlo; si se repite, cuéntaselo a tu agente en la conversación — este detalle le sirve para arreglarlo.',
70
+ detail: rawMessage,
71
+ };
72
+ }
73
+
74
+ // AC2 (nav half): the header keeps Tablero + Proyectos in plain Spanish
75
+ // and folds every advanced view behind one "Más" toggle, which also
76
+ // reveals the way back to the expert UI.
77
+ function applyMaggleChrome() {
78
+ if (!isMaggleMode()) return;
79
+ document.body.classList.add('maggle-mode');
80
+ const subtitle = document.querySelector('.header__subtitle');
81
+ if (subtitle) subtitle.textContent = MAGGLE_LABELS['header.subtitle'];
82
+ const nav = document.querySelector('.header__nav');
83
+ if (!nav) return;
84
+ const KEEP = { board: MAGGLE_LABELS['nav.board'], dashboard: MAGGLE_LABELS['nav.dashboard'] };
85
+ const advanced = [];
86
+ for (const btn of nav.querySelectorAll('.nav-btn')) {
87
+ const view = btn.dataset.view;
88
+ if (view && KEEP[view]) {
89
+ btn.title = `${btn.textContent.trim()} — ${btn.title}`;
90
+ btn.textContent = KEEP[view];
91
+ } else {
92
+ btn.style.display = 'none';
93
+ advanced.push(btn);
94
+ }
95
+ }
96
+ if (advanced.length === 0) return;
97
+ const more = document.createElement('button');
98
+ more.className = 'nav-btn';
99
+ more.textContent = `${MAGGLE_LABELS['nav.more']} ▾`;
100
+ more.title = 'Vistas avanzadas y volver al modo experto';
101
+ more.addEventListener('click', () => {
102
+ const hidden = advanced[0].style.display === 'none';
103
+ for (const btn of advanced) btn.style.display = hidden ? '' : 'none';
104
+ more.textContent = `${MAGGLE_LABELS['nav.more']} ${hidden ? '▴' : '▾'}`;
105
+ let expert = document.getElementById('maggle-expert-link');
106
+ if (hidden && !expert) {
107
+ expert = document.createElement('a');
108
+ expert.id = 'maggle-expert-link';
109
+ expert.className = 'nav-btn';
110
+ expert.href = '/?maggle=0';
111
+ expert.textContent = 'Modo experto';
112
+ expert.title = 'Vuelve a la interfaz técnica (Pending/Running/Done, comandos kj)';
113
+ more.after(expert);
114
+ } else if (!hidden && expert) {
115
+ expert.remove();
116
+ }
117
+ });
118
+ // After the LAST nav button, so the plain row reads Tablero | Proyectos | Más.
119
+ advanced.at(-1).after(more);
120
+ }
121
+
122
+ if (typeof document !== 'undefined' && document.addEventListener) {
123
+ document.addEventListener('DOMContentLoaded', applyMaggleChrome);
124
+ }
@@ -74,14 +74,23 @@ function ensureDialog() {
74
74
  function showError(message, opts = {}) {
75
75
  return new Promise((resolve) => {
76
76
  const dlg = ensureDialog();
77
- const title = opts.title || 'Error';
77
+ // Maggle mode (KJC-TSK-0810 AC4): plain headline + next step, the raw
78
+ // message demoted to a collapsed technical detail — never alone.
79
+ const maggle = isMaggleMode() ? maggleErrorParts(message) : null;
80
+ const title = maggle ? maggle.headline : (opts.title || 'Error');
81
+ const body = maggle
82
+ ? `${esc(maggle.next)}
83
+ <details style="margin-top:10px"><summary style="cursor:pointer;color:var(--text-muted)">Detalle técnico${opts.title ? ` — ${esc(opts.title)}` : ''}</summary>
84
+ <pre style="white-space:pre-wrap;font-size:0.8rem;color:var(--text-muted);margin:8px 0 0">${esc(maggle.detail)}</pre>
85
+ </details>`
86
+ : esc(message);
78
87
  dlg.innerHTML = `
79
88
  <div style="padding:14px 18px;border-bottom:1px solid var(--border);
80
89
  font-weight:600;color:var(--color-red,#ef4444)">
81
90
  ${esc(title)}
82
91
  </div>
83
92
  <div style="padding:16px 18px;font-size:0.9rem;line-height:1.5;
84
- white-space:pre-wrap">${esc(message)}</div>
93
+ white-space:pre-wrap">${body}</div>
85
94
  <div style="padding:12px 18px;border-top:1px solid var(--border);
86
95
  text-align:right">
87
96
  <button id="app-dialog-ok" class="control-btn"
@@ -70,11 +70,11 @@ async function renderProjectPicker() {
70
70
 
71
71
  app.innerHTML = `
72
72
  <div class="section-header">
73
- <span class="section-header__title">Story Board</span>
74
- <span class="section-header__count">${sorted.length} project${sorted.length === 1 ? '' : 's'}</span>
73
+ <span class="section-header__title">${maggleText('board.title', 'Story Board')}</span>
74
+ <span class="section-header__count">${sorted.length} ${maggleText('picker.project', 'project')}${sorted.length === 1 ? '' : 's'}</span>
75
75
  </div>
76
76
  <p style="padding:8px 4px 16px;color:var(--text-muted);font-size:0.9rem">
77
- Pick a project to see its kanban. Use the project selector in the header to switch later.
77
+ ${maggleText('picker.hint', 'Pick a project to see its kanban. Use the project selector in the header to switch later.')}
78
78
  </p>
79
79
  <div style="display:grid;grid-template-columns:repeat(auto-fill, minmax(320px, 1fr));gap:14px">
80
80
  ${sorted.map((p) => {
@@ -62,7 +62,10 @@ try {
62
62
  Write-Host "kj-install: Node $(& node --version) found — installing via npm (full product)..."
63
63
  & npm install -g (Get-NpmPkg)
64
64
  if ($LASTEXITCODE -ne 0) { throw "npm install failed (exit $LASTEXITCODE)." }
65
- Write-Host "kj-install: installed. Run 'kj doctor' next, then 'kj install-tools' to complete the whole stack."
65
+ Write-Host ""
66
+ Write-Host " Listo. Ahora escribe: kj go"
67
+ Write-Host ""
68
+ Write-Host " (avanzado: 'kj doctor' revisa la instalacion, 'kj install-tools' completa el stack)"
66
69
  return
67
70
  }
68
71
 
@@ -137,7 +140,10 @@ try {
137
140
  Add-UserPath $installDir
138
141
  $installed = (& (Join-Path $installDir "kj.cmd") --version) 2>$null
139
142
  Write-Host "kj-install: installed kj $installed (full product) — kj at $(Join-Path $installDir 'kj.cmd')"
140
- Write-Host "kj-install: next — run 'kj doctor', then 'kj install-tools' to complete the whole stack."
143
+ Write-Host ""
144
+ Write-Host " Listo. Ahora escribe: kj go"
145
+ Write-Host ""
146
+ Write-Host " (avanzado: 'kj doctor' revisa la instalacion, 'kj install-tools' completa el stack)"
141
147
  return
142
148
  }
143
149
 
@@ -168,7 +174,10 @@ try {
168
174
  Add-UserPath $installDir
169
175
  $installed = (& $dest --version) 2>$null
170
176
  Write-Host "kj-install: installed standalone kj $installed to $dest"
171
- Write-Host "kj-install: next — run 'kj doctor'. For the full product later: re-run without KJ_STANDALONE."
177
+ Write-Host ""
178
+ Write-Host " Listo. Ahora escribe: kj go"
179
+ Write-Host ""
180
+ Write-Host " (avanzado: 'kj doctor' revisa la instalacion; para el producto completo, re-ejecuta sin KJ_STANDALONE)"
172
181
  } finally {
173
182
  Remove-Item -Path $tmp -Recurse -Force -ErrorAction SilentlyContinue
174
183
  }
@@ -71,7 +71,11 @@ if [ "$MODE" = "full" ]; then
71
71
  if node_ok; then
72
72
  echo "kj-install: Node $(node --version) found — installing via npm (full product)..."
73
73
  npm install -g "$(npm_pkg)" || die "npm install failed. If it was a permissions error, set a user prefix (npm config set prefix ~/.local) and re-run."
74
- echo "kj-install: installed $(kj --version 2>/dev/null || echo karajan-code). Run 'kj doctor' next."
74
+ echo "kj-install: installed $(kj --version 2>/dev/null || echo karajan-code)."
75
+ echo ""
76
+ echo " Listo. Ahora escribe: kj go"
77
+ echo ""
78
+ echo " (avanzado: 'kj doctor' revisa la instalacion, 'kj install-tools' completa el stack)"
75
79
  exit 0
76
80
  fi
77
81
 
@@ -125,7 +129,10 @@ if [ "$MODE" = "full" ]; then
125
129
  installed="$("${INSTALL_DIR}/kj" --version 2>/dev/null || echo '?')"
126
130
  echo "kj-install: installed kj ${installed} (full product) — kj at ${INSTALL_DIR}/kj"
127
131
  path_hint "$INSTALL_DIR"
128
- echo "kj-install: next — run 'kj doctor', then 'kj install-tools' to complete the whole stack."
132
+ echo ""
133
+ echo " Listo. Ahora escribe: kj go"
134
+ echo ""
135
+ echo " (avanzado: 'kj doctor' revisa la instalacion, 'kj install-tools' completa el stack)"
129
136
  exit 0
130
137
  fi
131
138
 
@@ -156,4 +163,7 @@ fi
156
163
  installed="$("${INSTALL_DIR}/kj" --version 2>/dev/null || echo '?')"
157
164
  echo "kj-install: installed standalone kj ${installed} to ${INSTALL_DIR}/kj"
158
165
  path_hint "$INSTALL_DIR"
159
- echo "kj-install: next — run 'kj doctor'. For the full product later: re-run this installer without --standalone."
166
+ echo ""
167
+ echo " Listo. Ahora escribe: kj go"
168
+ echo ""
169
+ echo " (avanzado: 'kj doctor' revisa la instalacion; para el producto completo, re-ejecuta sin --standalone)"
@@ -7,6 +7,7 @@
7
7
 
8
8
  /** The few commands a newcomer needs. Shown in `kj --help`. */
9
9
  export const CORE_COMMANDS = [
10
+ "go",
10
11
  "start",
11
12
  "init",
12
13
  "run",
@@ -4,6 +4,7 @@ import { researcherCommand } from "../commands/researcher.js";
4
4
  import { architectCommand } from "../commands/architect.js";
5
5
  import { onboardCommand } from "../commands/onboard.js";
6
6
  import { startCommand } from "../commands/start.js";
7
+ import { goCommand } from "../commands/go.js";
7
8
  import { identityCommand } from "../commands/identity.js";
8
9
  import { policyCommand } from "../commands/policy.js";
9
10
  import { claimsCommand, claimsGateCommand } from "../commands/claims.js";
@@ -148,6 +149,17 @@ export function registerMeta(program, { pkgVersion }) {
148
149
  });
149
150
  });
150
151
 
152
+ // MGL-A (KJC-TSK-0808): the muggle launcher — one command, at most one
153
+ // question, and the person lands inside a governed conversation.
154
+ program
155
+ .command("go")
156
+ .description("Arranca Karajan sin saber nada: detecta tu agente, prepara el proyecto, abre el tablero y te deja en la conversación")
157
+ .action(async (flags) => {
158
+ await withConfig(pkgVersion, "go", flags, async ({ config, logger }) => {
159
+ await goCommand({ config, logger, flags });
160
+ });
161
+ });
162
+
151
163
  program
152
164
  .command("start")
153
165
  .description("Single entry point: assess the project and recommend the next step")
@@ -87,8 +87,23 @@ async function findAvailablePort(desiredPort, maxTries = 10) {
87
87
  * @returns {string}
88
88
  */
89
89
  export function buildBoardUrl(port, projectSlug) {
90
- const base = `http://localhost:${port}`;
91
- return projectSlug ? `${base}/#board/${projectSlug}` : base;
90
+ return projectSlug ? boardUrl(port, `/#board/${projectSlug}`) : boardUrl(port);
91
+ }
92
+
93
+ /**
94
+ * Base board URL plus an optional absolute path (query and/or hash) —
95
+ * `kj go` opens `/?maggle=1` so the frontend renders in plain language
96
+ * (KJC-TSK-0810). A relative path is a caller bug, not something to fix
97
+ * up silently.
98
+ * @param {number} port
99
+ * @param {string} [path]
100
+ * @returns {string}
101
+ */
102
+ export function boardUrl(port, path = "") {
103
+ if (path && !path.startsWith("/")) {
104
+ throw new Error(`board path must start with "/": ${path}`);
105
+ }
106
+ return `http://localhost:${port}${path}`;
92
107
  }
93
108
 
94
109
  /**
@@ -368,7 +383,7 @@ export function renderBoardBanner({ url, status, projectName }) {
368
383
  return ["", rule, ...content, rule, ""].join("\n");
369
384
  }
370
385
 
371
- export async function boardCommand({ action = "start", port = 4000, bind = "127.0.0.1", logger }) {
386
+ export async function boardCommand({ action = "start", port = 4000, bind = "127.0.0.1", logger, path }) {
372
387
  switch (action) {
373
388
  case "start": {
374
389
  let result;
@@ -422,7 +437,7 @@ export async function boardCommand({ action = "start", port = 4000, bind = "127.
422
437
  logger.info("HU Board is not running. Starting it first...");
423
438
  await startBoard(port);
424
439
  }
425
- const url = `http://localhost:${port}`;
440
+ const url = boardUrl(port, path);
426
441
  // eslint-disable-next-line import-x/no-unresolved -- optional peer; the .catch handles its absence
427
442
  const { default: open } = await import("open").catch(() => ({ default: null }));
428
443
  if (open) {
@@ -0,0 +1,110 @@
1
+ /**
2
+ * `kj go` (MGL-A, KJC-TSK-0808, epic KJC-PCS-0084) — the muggle launcher.
3
+ * One command for a person with no computing background: detect their agents,
4
+ * ask AT MOST one question, prepare the project silently, open the board, and
5
+ * leave them inside a conversation that already knows the method. What cannot
6
+ * be hidden is said honestly: the agent's account and login are THEIRS — kj
7
+ * never touches credentials. Everything is injectable so tests spawn nothing.
8
+ */
9
+ import { existsSync } from "node:fs";
10
+ import os from "node:os";
11
+ import path from "node:path";
12
+ import { spawn } from "node:child_process";
13
+ import { checkBinary } from "../utils/agent-detect.js";
14
+ import { createCliAskQuestion } from "../utils/cli-ask-question.js";
15
+ import { envInstallCommand } from "./env.js";
16
+ import { boardCommand } from "./board.js";
17
+ // The interactive launchers a muggle can live inside (v1: the two the epic
18
+ // names). Auth heuristics are the same cheap file checks reviewer-fallback
19
+ // uses: menu signal, never a spawned process.
20
+ export const MAGGLE_AGENTS = [
21
+ { name: "claude", label: "Claude Code (Anthropic)", authPaths: [".claude.json", ".claude"], install: "npm install -g @anthropic-ai/claude-code", login: "claude (sigue el enlace de inicio de sesión que te muestre)" },
22
+ { name: "codex", label: "Codex (OpenAI)", authPaths: [".codex/auth.json"], install: "npm install -g @openai/codex", login: "codex login" },
23
+ ];
24
+ export async function detectMaggleAgents({ home = os.homedir(), checkBin = checkBinary } = {}) {
25
+ return Promise.all(
26
+ MAGGLE_AGENTS.map(async (a) => {
27
+ let installed = false;
28
+ try { installed = (await checkBin(a.name)).ok; } catch { /* not installed */ }
29
+ const authenticated = installed && a.authPaths.some((p) => existsSync(path.join(home, p)));
30
+ return { ...a, installed, authenticated };
31
+ }),
32
+ );
33
+ }
34
+ /** The session's opening prompt — SHORT and in plain language. The full
35
+ * playbook already lives in the agent files env-install wrote; duplicating it
36
+ * here would dilute it. This prompt sets the tone for a muggle. */
37
+ export function buildGoPrompt() {
38
+ return [
39
+ "Arrancas dentro de un proyecto gobernado por Karajan (las reglas del método ya están en los ficheros de agente de este proyecto: síguelas siempre).",
40
+ "La persona que te habla puede no saber programar. Habla en lenguaje llano: sin jerga, sin siglas sin explicar, y resume cada resultado en una o dos frases antes del detalle.",
41
+ "Antes de cambios importantes, di en llano qué vas a hacer y qué pasará. Si algo falla, explica qué pasó y cuál es el siguiente paso — nunca un volcado de error a secas.",
42
+ "El tablero del proyecto está abierto en su navegador: cuando termines algo, recuérdale que puede verlo ahí.",
43
+ "Empieza presentándote en dos frases y preguntando qué quiere construir o cambiar hoy.",
44
+ ].join("\n");
45
+ }
46
+ async function defaultPrepare({ config, logger }) {
47
+ await envInstallCommand({ config, logger, flags: { yes: true } });
48
+ }
49
+ export async function defaultBoard({ config, logger, runBoard = boardCommand }) {
50
+ const port = config.hu_board?.port || 4000;
51
+ await runBoard({ action: "start", port, bind: "127.0.0.1", logger });
52
+ // /?maggle=1 switches the frontend to plain language (KJC-TSK-0810) —
53
+ // the muggle's window opens already speaking their language.
54
+ await runBoard({ action: "open", port, bind: "127.0.0.1", path: "/?maggle=1", logger });
55
+ }
56
+ function defaultLaunch(agent, prompt) {
57
+ // Interactive session: the muggle LIVES here. CLAUDECODE is stripped so a
58
+ // nested Claude does not refuse to start (the known subprocess quirk).
59
+ const { CLAUDECODE: _omit, ...env } = process.env;
60
+ return new Promise((resolvePromise) => {
61
+ const child = spawn(agent.name, [prompt], { stdio: "inherit", env });
62
+ child.on("exit", (code) => resolvePromise(code ?? 0));
63
+ child.on("error", (err) => {
64
+ console.error(`No se pudo arrancar ${agent.label}: ${err.message}`);
65
+ resolvePromise(1);
66
+ });
67
+ });
68
+ }
69
+ export async function goCommand({ config = {}, logger = console, flags = {}, deps = {} } = {}) {
70
+ const projectDir = config.projectDir || process.cwd();
71
+ const agents = await (deps.detect ?? detectMaggleAgents)();
72
+ const ready = agents.filter((a) => a.installed && a.authenticated);
73
+ const needLogin = agents.filter((a) => a.installed && !a.authenticated);
74
+ if (ready.length === 0) {
75
+ if (needLogin.length > 0) {
76
+ logger.error?.("Tienes el agente instalado pero falta iniciar sesión — eso solo puedes hacerlo tú (la cuenta es tuya; Karajan nunca toca tus credenciales):");
77
+ for (const a of needLogin) logger.error?.(` ${a.label}: ${a.login}`);
78
+ logger.error?.("Cuando hayas iniciado sesión, vuelve a escribir: kj go");
79
+ } else {
80
+ logger.error?.("Karajan trabaja con un agente de IA que aún no tienes instalado. Elige uno, instálalo con su comando, e inicia sesión con TU cuenta (la cuenta es tuya):");
81
+ for (const a of agents) logger.error?.(` ${a.label}: ${a.install}`);
82
+ logger.error?.("Después, vuelve a escribir: kj go");
83
+ }
84
+ process.exitCode = 1;
85
+ return 1;
86
+ }
87
+ let chosen = ready[0];
88
+ if (ready.length > 1) {
89
+ const ask = deps.ask ?? createCliAskQuestion({ flags });
90
+ const answer = await ask("¿Con cuál de tus agentes quieres trabajar?", { options: ready.map((a) => a.name), default: ready[0].name });
91
+ chosen = ready.find((a) => a.name === String(answer).trim()) ?? ready[0];
92
+ }
93
+ logger.info?.(`Trabajarás con ${chosen.label}.`);
94
+ // Prepare ONCE: decisions already taken are never re-asked.
95
+ if (!existsSync(path.join(projectDir, ".karajan", "review-gate"))) {
96
+ logger.info?.("Preparando tu proyecto (solo la primera vez)…");
97
+ await (deps.prepare ?? defaultPrepare)({ config, logger });
98
+ }
99
+ // The board is the muggle's window — unless the project turned it off
100
+ // (hu_board.enabled false is respected: KJC-BUG-0152). A board failure is
101
+ // said and never stops the conversation from starting.
102
+ if (config.hu_board?.enabled !== false) {
103
+ try { await (deps.board ?? defaultBoard)({ config, logger }); } catch (err) { logger.warn?.(`El tablero no pudo abrirse (${err.message}) — la conversación arranca igual.`); }
104
+ }
105
+ const prompt = (deps.prompt ?? buildGoPrompt)();
106
+ logger.info?.("Abriendo tu conversación… (escribe ahí lo que necesites, en tu idioma)");
107
+ const code = await (deps.launch ?? defaultLaunch)(chosen, prompt);
108
+ process.exitCode = code;
109
+ return code;
110
+ }
@@ -75,12 +75,27 @@ export function detectPhantomUnit({ sourceAnalysis, testSource, file = "<test>"
75
75
  const literalsOf = (tree, minLength) => {
76
76
  const out = [];
77
77
  walk(tree, (n) => {
78
- if (n.type === "StringLiteral" && n.value.trim().length >= minLength) out.push({ value: n.value, line: n.loc.start.line });
79
- if (n.type === "TemplateElement" && n.value.cooked && n.value.cooked.trim().length >= minLength) out.push({ value: n.value.cooked, line: n.loc.start.line });
78
+ if (n.type === "StringLiteral" && n.value.trim().length >= minLength) out.push({ value: n.value, line: n.loc.start.line, template: false });
79
+ if (n.type === "TemplateElement" && n.value.cooked && n.value.cooked.trim().length >= minLength) out.push({ value: n.value.cooked, line: n.loc.start.line, template: true });
80
80
  });
81
81
  return out;
82
82
  };
83
83
 
84
+ // KJC-BUG-0158 (GREBLA's field validation): an interpolated label —
85
+ // aria-label="Head al que reporta ${name}" — never matches the RESOLVED
86
+ // string a test looks for, and in Lit that is the usual case, not the
87
+ // exception. The static chunk carries markup before the label, so the right
88
+ // crossing is: the longest SUFFIX of a template chunk that is a PREFIX of the
89
+ // test's literal. Threshold contains false positives; a LIVE chunk producing
90
+ // the same start absolves — covering interpolation must never accuse live UI.
91
+ const PREFIX_MIN = 8;
92
+ const overlap = (chunk, wanted) => {
93
+ for (let k = Math.min(chunk.length, wanted.length); k >= PREFIX_MIN; k--) {
94
+ if (chunk.endsWith(wanted.slice(0, k))) return k;
95
+ }
96
+ return 0;
97
+ };
98
+
84
99
  /**
85
100
  * E2E detector: literals the test looks for that ONLY exist inside
86
101
  * unreachable spans of the source. A literal also present in reachable code
@@ -99,15 +114,37 @@ export function detectPhantomE2E({ sourceText, sourceAnalysis, testSource, file
99
114
  }
100
115
  const deadSpans = observableClasses(sourceAnalysis).flatMap((c) => c.unreachable.map((u) => [u.line, u.endLine]));
101
116
  const inDead = (line) => deadSpans.some(([a, b]) => line >= a && line <= b);
117
+ const sourceLits = literalsOf(sourceTree, minLength);
102
118
  const onlyInDead = new Map(); // literal → line where it lives
103
- for (const lit of literalsOf(sourceTree, minLength)) {
119
+ for (const lit of sourceLits) {
104
120
  if (inDead(lit.line)) { if (!onlyInDead.has(lit.value)) onlyInDead.set(lit.value, lit.line); }
105
121
  else onlyInDead.set(lit.value, -1); // seen reachable — poisoned, never reportable
106
122
  }
107
- const wanted = new Set(literalsOf(testTree, minLength).map((l) => l.value));
108
- const phantoms = [...wanted]
109
- .filter((v) => onlyInDead.get(v) !== undefined && onlyInDead.get(v) !== -1)
110
- .map((v) => ({ literal: v, line: onlyInDead.get(v), file }));
123
+ // KJC-BUG-0159 (GREBLA's field catch): file+line point at the TEST — that is
124
+ // where the fix happens; the dead code's spot travels as sourceLine. A
125
+ // finding at spec.js:2183 on a 32-line spec sends the reader nowhere.
126
+ const wanted = new Map(); // literal → its line IN THE TEST (first sighting)
127
+ for (const l of literalsOf(testTree, minLength)) if (!wanted.has(l.value)) wanted.set(l.value, l.line);
128
+ const phantoms = [...wanted.entries()]
129
+ .filter(([v]) => onlyInDead.get(v) !== undefined && onlyInDead.get(v) !== -1)
130
+ .map(([v, testLine]) => ({ literal: v, file, line: testLine, sourceLine: onlyInDead.get(v) }));
131
+ // KJC-BUG-0158: interpolated labels match by static-chunk prefix. Dead wins
132
+ // only when NO live chunk produces at least the same start.
133
+ const chunks = sourceLits.filter((l) => l.template);
134
+ for (const [v, testLine] of wanted) {
135
+ if (phantoms.some((p) => p.literal === v)) continue;
136
+ // the exact literal living in REACHABLE code absolves here too (codex's
137
+ // catch): that test does point at something alive.
138
+ if (onlyInDead.get(v) === -1) continue;
139
+ let kDead = 0, kLive = 0, deadLine = 0;
140
+ for (const c of chunks) {
141
+ // a chunk CONTAINING the whole literal is the strongest match either way
142
+ const k = c.value.includes(v) ? v.length : overlap(c.value, v);
143
+ if (inDead(c.line)) { if (k > kDead) { kDead = k; deadLine = c.line; } }
144
+ else if (k > kLive) kLive = k;
145
+ }
146
+ if (kDead >= PREFIX_MIN && kDead > kLive) phantoms.push({ literal: v, file, line: testLine, sourceLine: deadLine });
147
+ }
111
148
  return { observable: true, phantoms };
112
149
  }
113
150