karajan-code 4.22.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 +9 -0
- package/package.json +3 -2
- 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/packages/hu-board/src/auth.js +29 -1
- package/scripts/install-binary.ps1 +12 -3
- package/scripts/install-binary.sh +13 -3
- package/src/audit/basal-cost.js +4 -0
- package/src/audit/dead-exports.js +52 -1
- package/src/audit/deterministic-summary.js +18 -3
- package/src/audit/member-reachability.js +158 -0
- package/src/audit/osv-findings.js +1 -0
- package/src/claims/cross-check.js +10 -5
- package/src/cli/advanced-commands.js +2 -1
- package/src/cli/register-meta.js +36 -3
- package/src/commands/board.js +19 -4
- package/src/commands/claims.js +33 -1
- package/src/commands/go.js +110 -0
- package/src/commands/hu.js +3 -1
- package/src/commands/privacy.js +6 -2
- package/src/commands/resume.js +4 -0
- package/src/commands/review-gate.js +30 -7
- package/src/commands/steward.js +146 -0
- package/src/config/defaults.js +6 -1
- package/src/harden/sentinel-hooks.js +98 -6
- package/src/harden/workflow-engine.js +8 -2
- package/src/harden/workflow-templates.js +38 -1
- package/src/privacy/scan.js +23 -1
- package/src/review/sonar-pregate.js +38 -3
- package/src/review/tests-with-code.js +12 -1
- package/src/roles/audit-role.js +19 -1
- package/src/sonar/scanner.js +34 -8
- package/src/steward/invariants.js +190 -0
- package/src/steward/phantom-coverage.js +174 -0
- package/src/steward/proposed-work.js +68 -0
package/README.md
CHANGED
|
@@ -34,6 +34,7 @@ Your AI agent (Claude Code, Codex, Copilot, Antigravity, Cursor…) writes the c
|
|
|
34
34
|
- **Nothing personal ships** — every outbound boundary audits before it leaves the machine: the pre-commit rejects a staged diff carrying your denylisted personal data, hardcoded platform tokens (`ghp_`, `sk-`, `AKIA`…) block outright, `verify-pack`-style tarball scans guard the publish, and `kj privacy scan <dir>` audits any build output. Your denylist lives in `~/.karajan/privacy.yml` — the install asks and writes it for you.
|
|
35
35
|
- **Installing IS activating** — `kj env install` performs the enforcement itself (git hooks, verdict gate, tool gate) instead of trusting the agent to run setup steps, and ends by printing the method into the very conversation that installed it. A commit outside the method is rejected, not narrated.
|
|
36
36
|
- **The turn cannot end red — the Sentinel** — a deterministic supervisor (zero LLM) wired into the harness's synchronous hooks records the method state of the session as tools run, and a Stop hook blocks the agent from ending its turn while method violations are open. The program rules, the agent thinks. See [guarantee levels](#guarantee-levels-governed-vs-supervised).
|
|
37
|
+
- **Trust expires — the Steward** — `kj steward sweep` checks the project's declared guarantees against their freshness and answers one of FOUR verdicts per invariant: ok, broken, *unknown* (evidence expired → refresh it) or *not observable* (nowhere to look → instrument it) — because confusing the last two with green is how a project decays for weeks behind a passing facade. The verdict is versioned in the repo, sealed in the decision chain, and every break lands on the board as PROPOSED work that nothing executes unreviewed. Runs on demand, on `kj resume` when stale, or as an opt-in scheduled Action.
|
|
37
38
|
- **The review panel never runs dry** — nine built-in agents (Claude Code, Codex, GitHub Copilot, Antigravity `agy` — the gemini successor —, Kimi Code, Qwen, OpenCode, Aider, Gemini legacy), and when the configured reviewer exhausts its quota, `kj review` switches to an authenticated candidate with a LOUD notice — or hands you the menu of candidates with their tier (free / subscription / local) and the exact login command. Never a silent failure, never the brain reviewing itself.
|
|
38
39
|
|
|
39
40
|
This repo runs under its own environment: every commit to karajan-code carries a cross-AI verdict.
|
|
@@ -58,6 +59,14 @@ kj init && kj env install && kj harden && kj review --install-gate
|
|
|
58
59
|
git config core.hooksPath .karajan/hooks
|
|
59
60
|
```
|
|
60
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
|
+
|
|
61
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/).
|
|
62
71
|
|
|
63
72
|
## The daily loop
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "karajan-code",
|
|
3
|
-
"version": "4.
|
|
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",
|
|
@@ -123,17 +123,18 @@
|
|
|
123
123
|
},
|
|
124
124
|
"devDependencies": {
|
|
125
125
|
"@eslint/js": "^10.0.1",
|
|
126
|
-
"smol-toml": "^1.7.0",
|
|
127
126
|
"@vitest/coverage-v8": "^4.1.8",
|
|
128
127
|
"esbuild": "^0.28.0",
|
|
129
128
|
"eslint": "^10.4.1",
|
|
130
129
|
"eslint-plugin-import-x": "^4.16.2",
|
|
130
|
+
"eslint-plugin-node-security": "5.2.2",
|
|
131
131
|
"eslint-plugin-security": "^4.0.0",
|
|
132
132
|
"globals": "^17.6.0",
|
|
133
133
|
"lint-staged": "^17.0.7",
|
|
134
134
|
"postject": "^1.0.0-alpha.6",
|
|
135
135
|
"prettier": "^3.4.2",
|
|
136
136
|
"simple-git-hooks": "^2.13.1",
|
|
137
|
+
"smol-toml": "^1.7.0",
|
|
137
138
|
"vitest": "^4.1.8"
|
|
138
139
|
}
|
|
139
140
|
}
|
|
@@ -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) => {
|
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
* legacy callers keep working.
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
+
import { timingSafeEqual } from "node:crypto";
|
|
24
|
+
|
|
23
25
|
/** Loopback addresses we trust without auth, regardless of token state. */
|
|
24
26
|
const LOOPBACK_ADDRESSES = new Set([
|
|
25
27
|
"127.0.0.1",
|
|
@@ -41,7 +43,7 @@ export function authMiddleware() {
|
|
|
41
43
|
if (isLoopback(req)) return next();
|
|
42
44
|
|
|
43
45
|
const token = extractToken(req);
|
|
44
|
-
if (token
|
|
46
|
+
if (tokensMatch(token, expected)) return next();
|
|
45
47
|
|
|
46
48
|
return res.status(401).json({
|
|
47
49
|
error: "Unauthorized",
|
|
@@ -53,6 +55,32 @@ export function authMiddleware() {
|
|
|
53
55
|
};
|
|
54
56
|
}
|
|
55
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Constant-time comparison of a presented token against the expected one.
|
|
60
|
+
*
|
|
61
|
+
* `===` on strings stops at the first differing byte, so the time it takes to
|
|
62
|
+
* reject a token leaks how much of a correct prefix was supplied. Over the
|
|
63
|
+
* loopback interface that is noise — but this middleware exists precisely for
|
|
64
|
+
* the case the header above describes, where the board is bound beyond
|
|
65
|
+
* loopback and the peer is on the LAN. There the difference is measurable, and
|
|
66
|
+
* the token can be recovered a byte at a time.
|
|
67
|
+
*
|
|
68
|
+
* `timingSafeEqual` throws when the buffers differ in length, so the length is
|
|
69
|
+
* checked first. That is not a leak worth closing: the length of the expected
|
|
70
|
+
* token is fixed by whatever wrote `~/.karajan/hu-board/token`, and a token of
|
|
71
|
+
* the wrong length is wrong regardless of its contents.
|
|
72
|
+
*
|
|
73
|
+
* @param {string|undefined} presented
|
|
74
|
+
* @param {string} expected
|
|
75
|
+
* @returns {boolean}
|
|
76
|
+
*/
|
|
77
|
+
function tokensMatch(presented, expected) {
|
|
78
|
+
if (typeof presented !== "string") return false;
|
|
79
|
+
const left = Buffer.from(presented, "utf8");
|
|
80
|
+
const right = Buffer.from(expected, "utf8");
|
|
81
|
+
return left.length === right.length && timingSafeEqual(left, right);
|
|
82
|
+
}
|
|
83
|
+
|
|
56
84
|
/**
|
|
57
85
|
* @param {import('express').Request} req
|
|
58
86
|
* @returns {boolean}
|
|
@@ -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/audit/basal-cost.js
CHANGED
|
@@ -272,10 +272,14 @@ export async function saveAuditSnapshot(projectDir, metrics) {
|
|
|
272
272
|
|
|
273
273
|
export function computeGrowthDelta(current, previous) {
|
|
274
274
|
if (!previous) return null;
|
|
275
|
+
// AC8 (KJC-TSK-0794): "not measured before" must never read as "unchanged" —
|
|
276
|
+
// the derivative only exists when BOTH snapshots actually measured it.
|
|
277
|
+
const bothMeasured = Array.isArray(current.deadExports) && Array.isArray(previous.deadExports);
|
|
275
278
|
return {
|
|
276
279
|
lines: current.totalLines - previous.totalLines,
|
|
277
280
|
files: current.totalFiles - previous.totalFiles,
|
|
278
281
|
deps: current.dependencies.total - (previous.dependencies?.total ?? 0),
|
|
282
|
+
deadExports: bothMeasured ? current.deadExports.length - previous.deadExports.length : null,
|
|
279
283
|
since: previous.timestamp || null
|
|
280
284
|
};
|
|
281
285
|
}
|