karajan-code 2.34.0 → 3.1.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 +120 -38
- package/bin/kj-rag-mcp.js +75 -0
- package/docs/README.es.md +5 -5
- package/package.json +22 -17
- package/packages/hu-board/package.json +5 -2
- package/packages/hu-board/public/app.js +81 -4025
- package/packages/hu-board/public/index.html +20 -0
- package/packages/hu-board/public/utils/api.js +139 -0
- package/packages/hu-board/public/utils/board-view.js +453 -0
- package/packages/hu-board/public/utils/command-launcher.js +213 -0
- package/packages/hu-board/public/utils/config-editor.js +181 -0
- package/packages/hu-board/public/utils/dashboard-view.js +103 -0
- package/packages/hu-board/public/utils/formatters.js +232 -0
- package/packages/hu-board/public/utils/graph-view.js +216 -0
- package/packages/hu-board/public/utils/hu-actions.js +224 -0
- package/packages/hu-board/public/utils/init-listeners.js +180 -0
- package/packages/hu-board/public/utils/log-panel.js +222 -0
- package/packages/hu-board/public/utils/modals.js +290 -0
- package/packages/hu-board/public/utils/plan-rollup-view.js +53 -0
- package/packages/hu-board/public/utils/preflight-view.js +89 -0
- package/packages/hu-board/public/utils/project-actions.js +143 -0
- package/packages/hu-board/public/utils/project-picker-view.js +139 -0
- package/packages/hu-board/public/utils/run-launcher.js +169 -0
- package/packages/hu-board/public/utils/server-push.js +252 -0
- package/packages/hu-board/public/utils/sessions-view.js +199 -0
- package/packages/hu-board/public/utils/story-detail-view.js +324 -0
- package/packages/hu-board/public/utils/story-edit-form.js +271 -0
- package/packages/hu-board/src/config-yaml.js +14 -0
- package/packages/hu-board/src/db.js +15 -2
- package/packages/hu-board/src/routes/api.js +27 -9
- package/scripts/audit-test-diet.mjs +151 -0
- package/src/audit/deterministic-summary.js +20 -0
- package/src/audit/harness-scorecard.js +52 -3
- package/src/audit/harness-section.js +6 -2
- package/src/audit/injection-findings.js +105 -0
- package/src/checks/hardware.js +150 -0
- package/src/checks/node.js +9 -10
- package/src/checks/runner.js +10 -0
- package/src/cli/register-meta.js +48 -2
- package/src/cli/register-plan.js +10 -1
- package/src/commands/clean.js +62 -0
- package/src/commands/doctor.js +2 -0
- package/src/commands/init.js +41 -1
- package/src/commands/plan/generate.js +9 -3
- package/src/commands/rag-mcp.js +18 -0
- package/src/commands/sync.js +30 -2
- package/src/commands/telemetry.js +70 -0
- package/src/config/defaults.js +9 -1
- package/src/config/schema.js +2 -0
- package/src/orchestrator/config-init.js +1 -0
- package/src/orchestrator/drivers/iteration-phases/quality-gates.js +23 -0
- package/src/orchestrator/stages/coder-stage.js +11 -2
- package/src/orchestrator/stages/reviewer-stage.js +11 -0
- package/src/orchestrator/stages/tdd-discipline-stage.js +56 -0
- package/src/orchestrator/stages/tool-judge-stage.js +53 -0
- package/src/prompts/tool-correctness-judge.js +31 -0
- package/src/review/git-stash-helper.js +104 -0
- package/src/review/tdd-discipline.js +90 -0
- package/src/roles/audit-role.js +9 -1
- package/src/roles/tool-correctness-judge-role.js +78 -0
- package/src/session/mutators.js +4 -0
- package/src/sonar/scanner.js +51 -16
- package/src/sync/apply-canvas-patch.js +55 -0
- package/src/utils/paths.js +12 -0
- package/src/utils/quota-simulator.js +56 -0
- package/src/utils/repo-cleaner.js +79 -0
- package/src/utils/telemetry.js +34 -12
- package/src/utils/tool-call-extractor.js +56 -0
- package/src/utils/vector-store-orphans.js +89 -0
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
// KJC-TSK-0501 step 4/8 — Sessions view (list + card + detail modal).
|
|
2
|
+
//
|
|
3
|
+
// Classic script (no exports). Loaded by index.html after utils/formatters.js,
|
|
4
|
+
// utils/modals.js and utils/api.js, before app.js. Function declarations and
|
|
5
|
+
// top-level vars hoist to the window scope so app.js (and inline onclick
|
|
6
|
+
// handlers like `showSessionDetail(...)`) can call them transparently.
|
|
7
|
+
//
|
|
8
|
+
// Globals consumed from earlier scripts / app.js:
|
|
9
|
+
// - api() (utils/api.js)
|
|
10
|
+
// - formatSessionLabel() (utils/formatters.js)
|
|
11
|
+
// - formatDuration() (utils/formatters.js)
|
|
12
|
+
// - timeAgo() (utils/formatters.js)
|
|
13
|
+
// - esc() (app.js)
|
|
14
|
+
// - closeModal() (utils/modals.js)
|
|
15
|
+
// - renderEmptyState() (app.js)
|
|
16
|
+
// - selectedProject (mutable global) (app.js)
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Renders the sessions view.
|
|
20
|
+
*/
|
|
21
|
+
async function renderSessions() {
|
|
22
|
+
const app = document.getElementById('app');
|
|
23
|
+
app.innerHTML = '<div class="loading"><div class="loading__spinner"></div><p>Loading sessions...</p></div>';
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
let sessions;
|
|
27
|
+
if (selectedProject) {
|
|
28
|
+
sessions = await api(`/api/projects/${encodeURIComponent(selectedProject)}/sessions`);
|
|
29
|
+
} else {
|
|
30
|
+
sessions = await api('/api/sessions');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (sessions.length === 0) {
|
|
34
|
+
app.innerHTML = `
|
|
35
|
+
<div class="section-header">
|
|
36
|
+
<span class="section-header__title">Sessions</span>
|
|
37
|
+
<span class="section-header__count">0 sessions</span>
|
|
38
|
+
</div>
|
|
39
|
+
${renderEmptyState('No sessions found', 'KJ sessions will appear here when you run karajan.')}
|
|
40
|
+
`;
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
app.innerHTML = `
|
|
45
|
+
<div class="section-header">
|
|
46
|
+
<span class="section-header__title">Sessions${selectedProject ? ` - ${esc(selectedProject)}` : ''}</span>
|
|
47
|
+
<span class="section-header__count">${sessions.length} sessions</span>
|
|
48
|
+
</div>
|
|
49
|
+
<div class="sessions-list">
|
|
50
|
+
${sessions.map(renderSessionCard).join('')}
|
|
51
|
+
</div>
|
|
52
|
+
`;
|
|
53
|
+
} catch (err) {
|
|
54
|
+
app.innerHTML = `<div class="empty-state"><div class="empty-state__title">Error loading sessions</div><div class="empty-state__text">${esc(err.message)}</div></div>`;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Renders a session card.
|
|
60
|
+
* @param {object} session
|
|
61
|
+
* @returns {string}
|
|
62
|
+
*/
|
|
63
|
+
function renderSessionCard(session) {
|
|
64
|
+
const stages = session.stages_completed ? JSON.parse(session.stages_completed) : [];
|
|
65
|
+
const label = formatSessionLabel(session);
|
|
66
|
+
|
|
67
|
+
return `
|
|
68
|
+
<div class="session-card" onclick="showSessionDetail('${esc(session.id)}')">
|
|
69
|
+
<div class="session-card__header">
|
|
70
|
+
<span class="session-card__title" title="${esc(session.id)}">${esc(label.title)}</span>
|
|
71
|
+
<span class="session-card__status session-status--${session.status || 'unknown'}">${esc(session.status || 'unknown')}</span>
|
|
72
|
+
</div>
|
|
73
|
+
${label.subtitle ? `<div class="session-card__task">${esc(label.subtitle)}</div>` : ''}
|
|
74
|
+
<div class="session-card__meta">
|
|
75
|
+
<span>Iterations: ${session.iterations || 0}</span>
|
|
76
|
+
<span>Duration: ${formatDuration(session.duration_ms)}</span>
|
|
77
|
+
<span>Stages: ${stages.join(', ') || '--'}</span>
|
|
78
|
+
<span>${timeAgo(session.created_at)}</span>
|
|
79
|
+
</div>
|
|
80
|
+
<div class="session-card__id-chip" title="Session ID — use with kj resume <id>">${esc(label.idChip)}</div>
|
|
81
|
+
</div>
|
|
82
|
+
`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Shows the session detail modal.
|
|
87
|
+
* @param {string} sessionId
|
|
88
|
+
*/
|
|
89
|
+
async function showSessionDetail(sessionId) {
|
|
90
|
+
const backdrop = document.getElementById('modal-backdrop');
|
|
91
|
+
const content = document.getElementById('modal-content');
|
|
92
|
+
backdrop.classList.remove('hidden');
|
|
93
|
+
|
|
94
|
+
content.innerHTML = '<div class="loading"><div class="loading__spinner"></div></div>';
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
const session = await api(`/api/sessions/${encodeURIComponent(sessionId)}`);
|
|
98
|
+
const checkpoints = session.checkpoints || [];
|
|
99
|
+
const config = session.config_snapshot || {};
|
|
100
|
+
const budget = session.budget || {};
|
|
101
|
+
const startTime = session.created_at ? new Date(session.created_at).getTime() : 0;
|
|
102
|
+
|
|
103
|
+
const __label = formatSessionLabel(session);
|
|
104
|
+
content.innerHTML = `
|
|
105
|
+
<div class="modal__header">
|
|
106
|
+
<div>
|
|
107
|
+
<div class="modal__title">${esc(__label.title)}</div>
|
|
108
|
+
<div class="modal__subtitle session-card__id-chip" title="Use with kj resume <id>">${esc(session.id)}</div>
|
|
109
|
+
<span class="session-card__status session-status--${session.status}">${esc(session.status)}</span>
|
|
110
|
+
</div>
|
|
111
|
+
<button class="modal__close" onclick="closeModal()">×</button>
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
<div class="modal__section">
|
|
115
|
+
<div class="modal__section-title">Task</div>
|
|
116
|
+
<div class="modal__field-value" style="font-size:0.85rem">${esc(session.task || 'N/A')}</div>
|
|
117
|
+
</div>
|
|
118
|
+
|
|
119
|
+
<div class="modal__section">
|
|
120
|
+
<div class="modal__section-title">Overview</div>
|
|
121
|
+
<div class="stats-grid" style="margin-bottom:0">
|
|
122
|
+
<div class="stat-card">
|
|
123
|
+
<div class="stat-card__value">${session.iterations || 0}</div>
|
|
124
|
+
<div class="stat-card__label">Iterations</div>
|
|
125
|
+
</div>
|
|
126
|
+
<div class="stat-card">
|
|
127
|
+
<div class="stat-card__value">${formatDuration(session.duration_ms)}</div>
|
|
128
|
+
<div class="stat-card__label">Duration</div>
|
|
129
|
+
</div>
|
|
130
|
+
<div class="stat-card">
|
|
131
|
+
<div class="stat-card__value ${session.approved ? 'stat-card__value--green' : 'stat-card__value--yellow'}">${session.approved ? 'Yes' : 'No'}</div>
|
|
132
|
+
<div class="stat-card__label">Approved</div>
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
|
|
137
|
+
${config.coder || config.reviewer ? `
|
|
138
|
+
<div class="modal__section">
|
|
139
|
+
<div class="modal__section-title">Configuration</div>
|
|
140
|
+
<div style="font-family:var(--font-mono);font-size:0.8rem;color:var(--text-secondary)">
|
|
141
|
+
${config.coder ? `Coder: ${esc(config.coder)}` : ''}
|
|
142
|
+
${config.reviewer ? ` | Reviewer: ${esc(config.reviewer)}` : ''}
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
` : ''}
|
|
146
|
+
|
|
147
|
+
${checkpoints.length > 0 ? `
|
|
148
|
+
<div class="modal__section">
|
|
149
|
+
<div class="modal__section-title">Timeline (${checkpoints.length} checkpoints)</div>
|
|
150
|
+
<div class="timeline">
|
|
151
|
+
${checkpoints.map((cp) => {
|
|
152
|
+
const elapsed = cp.at && startTime ? formatDuration(new Date(cp.at).getTime() - startTime) : '';
|
|
153
|
+
const isOk = cp.ok === true || cp.approved === true;
|
|
154
|
+
const isFail = cp.ok === false || cp.approved === false;
|
|
155
|
+
const itemClass = isOk ? 'timeline__item--ok' : isFail ? 'timeline__item--fail' : 'timeline__item--info';
|
|
156
|
+
|
|
157
|
+
let detail = '';
|
|
158
|
+
if (cp.note) detail = cp.note;
|
|
159
|
+
else if (cp.approved !== undefined) detail = cp.approved ? 'APPROVED' : `REJECTED (${cp.blocking_issues || 0} issues)`;
|
|
160
|
+
else if (cp.reason) detail = cp.reason;
|
|
161
|
+
else if (cp.ok !== undefined) detail = cp.ok ? 'PASSED' : 'FAILED';
|
|
162
|
+
if (cp.provider) detail += ` [${cp.provider}]`;
|
|
163
|
+
|
|
164
|
+
return `
|
|
165
|
+
<div class="timeline__item ${itemClass}">
|
|
166
|
+
<span class="timeline__time">${elapsed}</span>
|
|
167
|
+
<div class="timeline__stage">[${esc(cp.stage)}] iter ${cp.iteration || 0}</div>
|
|
168
|
+
<div class="timeline__detail">${esc(detail)}</div>
|
|
169
|
+
</div>
|
|
170
|
+
`;
|
|
171
|
+
}).join('')}
|
|
172
|
+
</div>
|
|
173
|
+
</div>
|
|
174
|
+
` : ''}
|
|
175
|
+
|
|
176
|
+
${budget.total_cost_usd !== undefined ? `
|
|
177
|
+
<div class="modal__section">
|
|
178
|
+
<div class="modal__section-title">Budget</div>
|
|
179
|
+
<div style="font-family:var(--font-mono);font-size:0.8rem;color:var(--text-secondary)">
|
|
180
|
+
Tokens: ${budget.total_tokens || 0} | Cost: $${(budget.total_cost_usd || 0).toFixed(4)}
|
|
181
|
+
</div>
|
|
182
|
+
</div>
|
|
183
|
+
` : ''}
|
|
184
|
+
|
|
185
|
+
<details class="modal__section" style="margin-top:8px">
|
|
186
|
+
<summary style="cursor:pointer;font-weight:600;font-size:0.85rem;color:var(--text-muted);padding:6px 0">
|
|
187
|
+
Metadata
|
|
188
|
+
</summary>
|
|
189
|
+
<div style="padding-top:6px">
|
|
190
|
+
<div class="modal__field"><span class="modal__field-label">Project:</span> ${esc(session.project_id)}</div>
|
|
191
|
+
<div class="modal__field"><span class="modal__field-label">Created:</span> ${esc(session.created_at || '--')}</div>
|
|
192
|
+
<div class="modal__field"><span class="modal__field-label">Updated:</span> ${esc(session.updated_at || '--')}</div>
|
|
193
|
+
</div>
|
|
194
|
+
</details>
|
|
195
|
+
`;
|
|
196
|
+
} catch (err) {
|
|
197
|
+
content.innerHTML = `<div class="modal__header"><div class="modal__title">Error</div><button class="modal__close" onclick="closeModal()">×</button></div><p>${esc(err.message)}</p>`;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
// KJC-TSK-0501 step 7/8 — HU detail modal (read-only view).
|
|
2
|
+
//
|
|
3
|
+
// Classic script (no exports). Loaded by index.html before app.js so
|
|
4
|
+
// the function declaration hoists and inline onclick="showStoryDetail(...)"
|
|
5
|
+
// handlers emitted by board-view / graph-view resolve transparently.
|
|
6
|
+
//
|
|
7
|
+
// Globals consumed from earlier scripts / app.js:
|
|
8
|
+
// - api() (utils/api.js)
|
|
9
|
+
// - esc() (app.js)
|
|
10
|
+
// - resolveProjectInitials() (app.js)
|
|
11
|
+
// - shortStoryId() (app.js)
|
|
12
|
+
// - projectIsSharedCache (mutable, app.js)
|
|
13
|
+
// - renderStoryEditForm() (app.js)
|
|
14
|
+
// - closeModal(), changeHuStatusFromModal(), resetHuToPending(),
|
|
15
|
+
// undoHuChanges(), saveHuModels(), saveHuAssignee() — bound on
|
|
16
|
+
// window from app.js so the inline handlers in this template
|
|
17
|
+
// keep working unchanged.
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Show the HU detail modal in read-only mode. Fetches the full row
|
|
21
|
+
* (one round-trip — the kanban only carries a projection) and renders
|
|
22
|
+
* every field the planner / coder / reviewer may have populated:
|
|
23
|
+
* Original Text, Models (per-HU coder/reviewer overrides), Assignee
|
|
24
|
+
* (only for team-shared projects), Certified Story, Quality Score,
|
|
25
|
+
* Antipatterns, Acceptance Criteria, Acceptance Tests, Context
|
|
26
|
+
* Requests, Metadata. Header chrome surfaces Edit / Reset / Undo
|
|
27
|
+
* buttons gated on the HU's current status + outcome snapshot.
|
|
28
|
+
*/
|
|
29
|
+
async function showStoryDetail(storyId) {
|
|
30
|
+
const backdrop = document.getElementById('modal-backdrop');
|
|
31
|
+
const content = document.getElementById('modal-content');
|
|
32
|
+
backdrop.classList.remove('hidden');
|
|
33
|
+
|
|
34
|
+
content.innerHTML = '<div class="loading"><div class="loading__spinner"></div></div>';
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const story = await api(`/api/stories/${encodeURIComponent(storyId)}`);
|
|
38
|
+
const antipatterns = story.antipatterns ? JSON.parse(story.antipatterns) : [];
|
|
39
|
+
const ac = story.acceptance_criteria ? JSON.parse(story.acceptance_criteria) : [];
|
|
40
|
+
const tests = story.acceptance_tests ? JSON.parse(story.acceptance_tests) : [];
|
|
41
|
+
const ctxRequests = story.context_requests || [];
|
|
42
|
+
const initials = await resolveProjectInitials(story.project_id);
|
|
43
|
+
const shortId = shortStoryId(story, initials);
|
|
44
|
+
|
|
45
|
+
const dimLabels = ['Independent', 'Negotiable', 'Valuable', 'Estimable', 'Small', 'Testable'];
|
|
46
|
+
|
|
47
|
+
// Edit-in-place is gated on plan-backed stories: legacy rows
|
|
48
|
+
// (plan_id null) have no source-of-truth file to write to, so PATCH
|
|
49
|
+
// would 409 anyway.
|
|
50
|
+
const canEdit = ['pending', 'certified', 'needs_context', 'blocked'].includes(story.status);
|
|
51
|
+
// KJC-TSK-0394 step 2: "Reset to pending" para destrabar HUs zombi
|
|
52
|
+
// (coding/reviewing/blocked colgados) o relanzar limpio una HU
|
|
53
|
+
// done/failed. Solo plan-backed. Pending/certified ya están en el
|
|
54
|
+
// estado destino, no tiene sentido el botón.
|
|
55
|
+
const canResetToPending = story.plan_id
|
|
56
|
+
&& !['pending', 'certified'].includes(story.status);
|
|
57
|
+
// Dropdown libre de status. Solo plan-backed. La lista es la misma
|
|
58
|
+
// que ALLOWED_STORY_STATUSES en el backend — NO incluye coding/
|
|
59
|
+
// reviewing/running (esos los pone el orquestador; setearlos a
|
|
60
|
+
// mano genera zombies en el reaper).
|
|
61
|
+
const canChangeStatus = !!story.plan_id;
|
|
62
|
+
// KJC-TSK-0394 PR6 (AC 6): dropdown alineado con el modelo canónico
|
|
63
|
+
// que acepta el endpoint. `running` queda fuera porque settearlo a
|
|
64
|
+
// mano genera zombies (lifecycle del orquestador). Los status legacy
|
|
65
|
+
// (`certified`, `blocked`, `needs_context`) ya no se ofrecen — los
|
|
66
|
+
// plans en disco siguen vivos pero la UI sólo emite escrituras
|
|
67
|
+
// canónicas. Reset → pending y Run → running cubren el flujo.
|
|
68
|
+
const userSettableStatuses = ['pending', 'done'];
|
|
69
|
+
|
|
70
|
+
content.innerHTML = `
|
|
71
|
+
<div class="modal__header">
|
|
72
|
+
<div>
|
|
73
|
+
<div class="modal__title" title="${esc(story.id)}">${esc(shortId)}</div>
|
|
74
|
+
<div class="modal__subtitle" style="font-size:0.75rem;color:var(--text-muted);font-family:monospace;margin-top:2px">${esc(story.id)}</div>
|
|
75
|
+
<div style="display:flex;align-items:center;gap:8px;margin-top:6px">
|
|
76
|
+
<span class="story-card__status status--${story.status}">${esc(story.status)}</span>
|
|
77
|
+
${canChangeStatus ? `
|
|
78
|
+
<select id="hu-status-select"
|
|
79
|
+
title="Cambiar manualmente el status de esta HU"
|
|
80
|
+
style="padding:3px 6px;font-size:0.75rem;background:var(--bg-primary);color:var(--text);border:1px solid var(--border);border-radius:var(--radius-sm);cursor:pointer"
|
|
81
|
+
onchange="changeHuStatusFromModal('${esc(story.id)}', this.value, '${esc(story.status)}')">
|
|
82
|
+
<option value="">Cambiar a…</option>
|
|
83
|
+
${userSettableStatuses
|
|
84
|
+
.filter((s) => s !== story.status)
|
|
85
|
+
.map((s) => `<option value="${s}">${s}</option>`)
|
|
86
|
+
.join('')}
|
|
87
|
+
</select>
|
|
88
|
+
` : ''}
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
<div style="display:flex;align-items:center;gap:8px">
|
|
92
|
+
${canEdit ? `
|
|
93
|
+
<button id="edit-hu-btn" class="control-btn"
|
|
94
|
+
style="padding:6px 12px;border:1px solid var(--border);background:var(--bg-primary);color:var(--text);border-radius:var(--radius-sm);cursor:pointer;font-size:0.85rem"
|
|
95
|
+
title="Edit title, scope, task type, and acceptance criteria">
|
|
96
|
+
✎ Edit
|
|
97
|
+
</button>
|
|
98
|
+
` : ''}
|
|
99
|
+
${canResetToPending ? `
|
|
100
|
+
<button id="reset-hu-btn" class="control-btn"
|
|
101
|
+
style="padding:6px 12px;border:1px solid var(--border);background:var(--bg-primary);color:var(--text);border-radius:var(--radius-sm);cursor:pointer;font-size:0.85rem"
|
|
102
|
+
title="Devolver esta HU a pending (sin tocar el result anterior)"
|
|
103
|
+
onclick="resetHuToPending('${esc(story.id)}')">
|
|
104
|
+
↺ Reset
|
|
105
|
+
</button>
|
|
106
|
+
` : ''}
|
|
107
|
+
${(() => {
|
|
108
|
+
// KJC-TSK-0408: Undo solo si la HU tiene snapshot_sha en outcome.
|
|
109
|
+
let parsedOutcome = null;
|
|
110
|
+
try { parsedOutcome = typeof story.outcome === 'string' ? JSON.parse(story.outcome) : story.outcome; } catch { /* */ }
|
|
111
|
+
const canUndo = parsedOutcome?.snapshot_sha && !parsedOutcome?.reverted;
|
|
112
|
+
return canUndo ? `
|
|
113
|
+
<button id="undo-hu-btn" class="control-btn"
|
|
114
|
+
style="padding:6px 12px;border:1px solid var(--color-red,#ef4444);background:var(--bg-primary);color:var(--color-red,#ef4444);border-radius:var(--radius-sm);cursor:pointer;font-size:0.85rem"
|
|
115
|
+
title="Deshacer cambios de esta HU: restaura los ficheros al snapshot pre-run y marca pending"
|
|
116
|
+
onclick="undoHuChanges('${esc(story.id)}')">
|
|
117
|
+
⏪ Undo
|
|
118
|
+
</button>
|
|
119
|
+
` : '';
|
|
120
|
+
})()}
|
|
121
|
+
<button class="modal__close" onclick="closeModal()">×</button>
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
<div class="modal__section">
|
|
127
|
+
<div class="modal__section-title">Original Text</div>
|
|
128
|
+
<div class="modal__field-value">${esc(story.original_text || 'N/A')}</div>
|
|
129
|
+
</div>
|
|
130
|
+
|
|
131
|
+
${(story.coder_model || story.reviewer_model || canEdit) ? `
|
|
132
|
+
<!-- KJC-TSK-0406: model routing per HU. Cada modelo es independiente
|
|
133
|
+
y editable. Reviewer cross-provider del coder por defecto. -->
|
|
134
|
+
<div class="modal__section">
|
|
135
|
+
<div class="modal__section-title">Models</div>
|
|
136
|
+
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;font-size:0.85rem">
|
|
137
|
+
<div>
|
|
138
|
+
<div class="modal__field-label">Coder</div>
|
|
139
|
+
<div class="modal__field-value" style="font-family:monospace">
|
|
140
|
+
${esc(story.coder_model || 'auto')} ${story.coder_provider ? `<span style="color:var(--text-muted)">(${esc(story.coder_provider)})</span>` : ''}
|
|
141
|
+
</div>
|
|
142
|
+
${canEdit ? `
|
|
143
|
+
<input id="hu-coder-model" type="text" placeholder="modelo override"
|
|
144
|
+
value="${esc(story.coder_model || '')}"
|
|
145
|
+
style="margin-top:4px;width:100%;padding:4px;background:var(--bg-primary);color:var(--text);border:1px solid var(--border);border-radius:4px;font-family:monospace;font-size:0.8rem">
|
|
146
|
+
` : ''}
|
|
147
|
+
</div>
|
|
148
|
+
<div>
|
|
149
|
+
<div class="modal__field-label">Reviewer (cross-provider)</div>
|
|
150
|
+
<div class="modal__field-value" style="font-family:monospace">
|
|
151
|
+
${esc(story.reviewer_model || 'auto')} ${story.reviewer_provider ? `<span style="color:var(--text-muted)">(${esc(story.reviewer_provider)})</span>` : ''}
|
|
152
|
+
</div>
|
|
153
|
+
${canEdit ? `
|
|
154
|
+
<input id="hu-reviewer-model" type="text" placeholder="modelo override"
|
|
155
|
+
value="${esc(story.reviewer_model || '')}"
|
|
156
|
+
style="margin-top:4px;width:100%;padding:4px;background:var(--bg-primary);color:var(--text);border:1px solid var(--border);border-radius:4px;font-family:monospace;font-size:0.8rem">
|
|
157
|
+
` : ''}
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
${canEdit ? `
|
|
161
|
+
<div style="margin-top:8px;display:flex;gap:6px;align-items:center">
|
|
162
|
+
<button onclick="saveHuModels('${esc(story.id)}')" class="control-btn"
|
|
163
|
+
style="padding:4px 10px;background:var(--bg-primary);color:var(--text);border:1px solid var(--border);border-radius:4px;cursor:pointer;font-size:0.8rem">
|
|
164
|
+
💾 Save models
|
|
165
|
+
</button>
|
|
166
|
+
<span style="color:var(--text-muted);font-size:0.75rem">vacío → re-asignar automáticamente</span>
|
|
167
|
+
</div>
|
|
168
|
+
` : ''}
|
|
169
|
+
</div>
|
|
170
|
+
` : ''}
|
|
171
|
+
|
|
172
|
+
${projectIsSharedCache[story.project_id] === 1 ? `
|
|
173
|
+
<!-- KJC-PRP-0002 PR6: per-HU assignee — only surfaced when the
|
|
174
|
+
project is team-shared. Free-form string; no entity table. -->
|
|
175
|
+
<div class="modal__section">
|
|
176
|
+
<div class="modal__section-title">Asignado a</div>
|
|
177
|
+
<div class="modal__field-value" style="font-family:monospace">${esc(story.assignee || '—')}</div>
|
|
178
|
+
${canEdit ? `
|
|
179
|
+
<div style="margin-top:6px;display:flex;gap:6px;align-items:center">
|
|
180
|
+
<input id="hu-assignee" type="text" placeholder="@manu, dev_016, becaria…"
|
|
181
|
+
value="${esc(story.assignee || '')}"
|
|
182
|
+
style="flex:1;padding:4px;background:var(--bg-primary);color:var(--text);border:1px solid var(--border);border-radius:4px;font-family:monospace;font-size:0.8rem">
|
|
183
|
+
<button onclick="saveHuAssignee('${esc(story.id)}')" class="control-btn"
|
|
184
|
+
style="padding:4px 10px;background:var(--bg-primary);color:var(--text);border:1px solid var(--border);border-radius:4px;cursor:pointer;font-size:0.8rem">
|
|
185
|
+
💾 Save
|
|
186
|
+
</button>
|
|
187
|
+
</div>
|
|
188
|
+
<div style="margin-top:4px;color:var(--text-muted);font-size:0.75rem">vacío → sin asignar</div>
|
|
189
|
+
` : ''}
|
|
190
|
+
</div>
|
|
191
|
+
` : ''}
|
|
192
|
+
|
|
193
|
+
${story.certified_as ? `
|
|
194
|
+
<div class="modal__section">
|
|
195
|
+
<div class="modal__section-title">Certified Story</div>
|
|
196
|
+
<div class="modal__field">
|
|
197
|
+
<div class="modal__field-label">As a...</div>
|
|
198
|
+
<div class="modal__field-value">${esc(story.certified_as)}</div>
|
|
199
|
+
</div>
|
|
200
|
+
<div class="modal__field">
|
|
201
|
+
<div class="modal__field-label">I want to...</div>
|
|
202
|
+
<div class="modal__field-value">${esc(story.certified_want || '--')}</div>
|
|
203
|
+
</div>
|
|
204
|
+
<div class="modal__field">
|
|
205
|
+
<div class="modal__field-label">So that...</div>
|
|
206
|
+
<div class="modal__field-value">${esc(story.certified_so_that || '--')}</div>
|
|
207
|
+
</div>
|
|
208
|
+
</div>
|
|
209
|
+
` : story.certified_want ? `
|
|
210
|
+
<div class="modal__section">
|
|
211
|
+
<div class="modal__section-title">Scope</div>
|
|
212
|
+
<div class="modal__field-value" style="white-space: pre-wrap; font-size: 0.9rem; line-height: 1.5;">${esc(story.certified_want)}</div>
|
|
213
|
+
</div>
|
|
214
|
+
` : ''}
|
|
215
|
+
|
|
216
|
+
${story.quality_total !== null ? `
|
|
217
|
+
<div class="modal__section">
|
|
218
|
+
<div class="modal__section-title">Quality Score: ${story.quality_total}/60</div>
|
|
219
|
+
<div class="modal__quality-grid">
|
|
220
|
+
${[1, 2, 3, 4, 5, 6].map((d, i) => {
|
|
221
|
+
const val = story[`quality_d${d}`];
|
|
222
|
+
return `
|
|
223
|
+
<div class="modal__quality-dim">
|
|
224
|
+
<div class="modal__quality-dim-label">${dimLabels[i]}</div>
|
|
225
|
+
<div class="modal__quality-dim-value">${val !== null ? val + '/10' : '--'}</div>
|
|
226
|
+
</div>
|
|
227
|
+
`;
|
|
228
|
+
}).join('')}
|
|
229
|
+
</div>
|
|
230
|
+
</div>
|
|
231
|
+
` : ''}
|
|
232
|
+
|
|
233
|
+
${antipatterns.length > 0 ? `
|
|
234
|
+
<div class="modal__section">
|
|
235
|
+
<div class="modal__section-title">Antipatterns</div>
|
|
236
|
+
${antipatterns.map((a) => `<div class="story-card__antipattern" style="margin-bottom:4px">${esc(a)}</div>`).join('')}
|
|
237
|
+
</div>
|
|
238
|
+
` : ''}
|
|
239
|
+
|
|
240
|
+
${ac.length > 0 ? `
|
|
241
|
+
<div class="modal__section">
|
|
242
|
+
<div class="modal__section-title">Acceptance Criteria${story.ac_format ? ` (${esc(story.ac_format)})` : ''}</div>
|
|
243
|
+
<ul class="modal__ac-list">
|
|
244
|
+
${ac.map((c) => {
|
|
245
|
+
if (typeof c === 'string') return `<li class="modal__ac-item">${esc(c)}</li>`;
|
|
246
|
+
if (c.given) return `<li class="modal__ac-item"><code>Given</code> ${esc(c.given)}<br><code>When</code> ${esc(c.when)}<br><code>Then</code> ${esc(c.then)}</li>`;
|
|
247
|
+
return `<li class="modal__ac-item">${esc(JSON.stringify(c))}</li>`;
|
|
248
|
+
}).join('')}
|
|
249
|
+
</ul>
|
|
250
|
+
</div>
|
|
251
|
+
` : ''}
|
|
252
|
+
|
|
253
|
+
${tests.length === 0 ? `
|
|
254
|
+
<div class="modal__section" style="border:1px solid var(--color-yellow,#eab308);background:rgba(234,179,8,0.08);padding:10px 12px;border-radius:var(--radius-sm)">
|
|
255
|
+
<div class="modal__section-title" style="color:var(--color-yellow,#eab308)">⚠ Missing test contract</div>
|
|
256
|
+
<div style="font-size:0.85rem;line-height:1.5;margin-top:4px">
|
|
257
|
+
This HU has no acceptance_tests declared. The tests-first pipeline (v2.7.5)
|
|
258
|
+
refuses to run HUs without an executable contract. Click ✎ Edit above and
|
|
259
|
+
add at least one test — a <code>shell</code> command that exits 0 on pass,
|
|
260
|
+
or a <code>gherkin</code> Given/When/Then spec.
|
|
261
|
+
</div>
|
|
262
|
+
</div>
|
|
263
|
+
` : `
|
|
264
|
+
<div class="modal__section">
|
|
265
|
+
<div class="modal__section-title">Acceptance Tests (${tests.length})</div>
|
|
266
|
+
<ul class="modal__ac-list">
|
|
267
|
+
${tests.map((t) => {
|
|
268
|
+
// Legacy form: plain string — render as shell.
|
|
269
|
+
if (typeof t === 'string') {
|
|
270
|
+
return `<li class="modal__ac-item"><span style="font-size:0.7rem;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.5px;margin-right:8px">shell</span><code>${esc(t)}</code></li>`;
|
|
271
|
+
}
|
|
272
|
+
// v2.7.5 structured form: { type, content, file? }
|
|
273
|
+
if (t && typeof t === 'object' && typeof t.content === 'string') {
|
|
274
|
+
const type = t.type === 'gherkin' ? 'gherkin' : 'shell';
|
|
275
|
+
const badgeColor = type === 'gherkin' ? 'var(--color-blue,#3b82f6)' : 'var(--text-muted)';
|
|
276
|
+
const fileBit = t.file ? ` <span style="color:var(--text-muted);font-size:0.75rem;margin-left:8px">→ ${esc(t.file)}</span>` : '';
|
|
277
|
+
if (type === 'gherkin') {
|
|
278
|
+
return `<li class="modal__ac-item"><span style="font-size:0.7rem;color:${badgeColor};text-transform:uppercase;letter-spacing:0.5px;margin-right:8px">gherkin</span>${fileBit}<pre style="white-space:pre-wrap;margin:4px 0 0 0;font-family:inherit;font-size:0.9rem;line-height:1.5">${esc(t.content)}</pre></li>`;
|
|
279
|
+
}
|
|
280
|
+
return `<li class="modal__ac-item"><span style="font-size:0.7rem;color:${badgeColor};text-transform:uppercase;letter-spacing:0.5px;margin-right:8px">shell</span>${fileBit}<code>${esc(t.content)}</code></li>`;
|
|
281
|
+
}
|
|
282
|
+
// Fallback for anything else: show the raw JSON so the user
|
|
283
|
+
// can diagnose malformed entries.
|
|
284
|
+
return `<li class="modal__ac-item"><code>${esc(JSON.stringify(t))}</code></li>`;
|
|
285
|
+
}).join('')}
|
|
286
|
+
</ul>
|
|
287
|
+
</div>
|
|
288
|
+
`}
|
|
289
|
+
|
|
290
|
+
${ctxRequests.length > 0 ? `
|
|
291
|
+
<div class="modal__section">
|
|
292
|
+
<div class="modal__section-title">Context Requests (${ctxRequests.length})</div>
|
|
293
|
+
${ctxRequests.map((cr) => `
|
|
294
|
+
<div style="background:var(--bg-primary);border:1px solid var(--border);border-radius:var(--radius-sm);padding:8px;margin-bottom:6px;">
|
|
295
|
+
<div style="font-size:0.8rem;color:var(--color-yellow);">${esc(cr.question || 'Fields needed: ' + (cr.fields_needed || ''))}</div>
|
|
296
|
+
${cr.answer ? `<div style="font-size:0.8rem;color:var(--color-green);margin-top:4px;">${esc(cr.answer)}</div>` : ''}
|
|
297
|
+
</div>
|
|
298
|
+
`).join('')}
|
|
299
|
+
</div>
|
|
300
|
+
` : ''}
|
|
301
|
+
|
|
302
|
+
<details class="modal__section" style="margin-top:8px">
|
|
303
|
+
<summary style="cursor:pointer;font-weight:600;font-size:0.85rem;color:var(--text-muted);padding:6px 0">
|
|
304
|
+
Metadata
|
|
305
|
+
</summary>
|
|
306
|
+
<div style="padding-top:6px">
|
|
307
|
+
<div class="modal__field"><span class="modal__field-label">Project:</span> ${esc(story.project_id)}</div>
|
|
308
|
+
${story.spec_section ? `<div class="modal__field"><span class="modal__field-label">Implements SPEC:</span> §${esc(story.spec_section)}</div>` : ''}
|
|
309
|
+
<div class="modal__field"><span class="modal__field-label">Session:</span> ${esc(story.session_id || '--')}</div>
|
|
310
|
+
<div class="modal__field"><span class="modal__field-label">Created:</span> ${esc(story.created_at || '--')}</div>
|
|
311
|
+
<div class="modal__field"><span class="modal__field-label">Updated:</span> ${esc(story.updated_at || '--')}</div>
|
|
312
|
+
${story.certified_at ? `<div class="modal__field"><span class="modal__field-label">Certified at:</span> ${esc(story.certified_at)}</div>` : ''}
|
|
313
|
+
</div>
|
|
314
|
+
</details>
|
|
315
|
+
`;
|
|
316
|
+
|
|
317
|
+
// Wire the Edit button — stays within the modal so we don't flash
|
|
318
|
+
// a re-fetch; the edit form overwrites innerHTML in place.
|
|
319
|
+
const editBtn = document.getElementById('edit-hu-btn');
|
|
320
|
+
if (editBtn) editBtn.addEventListener('click', () => renderStoryEditForm(story));
|
|
321
|
+
} catch (err) {
|
|
322
|
+
content.innerHTML = `<div class="modal__header"><div class="modal__title">Error</div><button class="modal__close" onclick="closeModal()">×</button></div><p>${esc(err.message)}</p>`;
|
|
323
|
+
}
|
|
324
|
+
}
|