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 +8 -0
- package/package.json +1 -1
- package/packages/hu-board/public/index.html +1 -0
- package/packages/hu-board/public/utils/board-view.js +14 -13
- package/packages/hu-board/public/utils/command-launcher.js +42 -6
- package/packages/hu-board/public/utils/log-panel.js +4 -4
- package/packages/hu-board/public/utils/maggle-mode.js +124 -0
- package/packages/hu-board/public/utils/modals.js +11 -2
- package/packages/hu-board/public/utils/project-picker-view.js +3 -3
- package/scripts/install-binary.ps1 +12 -3
- package/scripts/install-binary.sh +13 -3
- package/src/cli/advanced-commands.js +1 -0
- package/src/cli/register-meta.js +12 -0
- package/src/commands/board.js +19 -4
- package/src/commands/go.js +110 -0
- package/src/steward/phantom-coverage.js +44 -7
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
|
@@ -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) : ''}"
|
|
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, '''))}');">✎</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
|
|
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
|
|
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(
|
|
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"
|
|
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
|
-
|
|
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">${
|
|
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"
|
|
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 "
|
|
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 "
|
|
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 "
|
|
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).
|
|
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 "
|
|
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 "
|
|
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)"
|
package/src/cli/register-meta.js
CHANGED
|
@@ -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")
|
package/src/commands/board.js
CHANGED
|
@@ -87,8 +87,23 @@ async function findAvailablePort(desiredPort, maxTries = 10) {
|
|
|
87
87
|
* @returns {string}
|
|
88
88
|
*/
|
|
89
89
|
export function buildBoardUrl(port, projectSlug) {
|
|
90
|
-
|
|
91
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
|