karajan-code 3.0.0 → 3.2.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.
Files changed (104) hide show
  1. package/README.md +14 -6
  2. package/docs/README.es.md +1 -1
  3. package/package.json +8 -2
  4. package/packages/hu-board/package.json +3 -1
  5. package/packages/hu-board/public/app.js +81 -4025
  6. package/packages/hu-board/public/index.html +21 -0
  7. package/packages/hu-board/public/styles.css +32 -0
  8. package/packages/hu-board/public/utils/api.js +139 -0
  9. package/packages/hu-board/public/utils/board-view.js +462 -0
  10. package/packages/hu-board/public/utils/command-launcher.js +213 -0
  11. package/packages/hu-board/public/utils/config-editor.js +181 -0
  12. package/packages/hu-board/public/utils/dashboard-view.js +103 -0
  13. package/packages/hu-board/public/utils/formatters.js +273 -0
  14. package/packages/hu-board/public/utils/graph-view.js +216 -0
  15. package/packages/hu-board/public/utils/hu-actions.js +224 -0
  16. package/packages/hu-board/public/utils/init-listeners.js +180 -0
  17. package/packages/hu-board/public/utils/log-panel.js +222 -0
  18. package/packages/hu-board/public/utils/modals.js +290 -0
  19. package/packages/hu-board/public/utils/plan-rollup-view.js +53 -0
  20. package/packages/hu-board/public/utils/preflight-view.js +89 -0
  21. package/packages/hu-board/public/utils/project-actions.js +143 -0
  22. package/packages/hu-board/public/utils/project-picker-view.js +139 -0
  23. package/packages/hu-board/public/utils/run-launcher.js +169 -0
  24. package/packages/hu-board/public/utils/server-push.js +252 -0
  25. package/packages/hu-board/public/utils/sessions-view.js +199 -0
  26. package/packages/hu-board/public/utils/story-detail-view.js +324 -0
  27. package/packages/hu-board/public/utils/story-edit-form.js +271 -0
  28. package/packages/hu-board/public/wiki.html +32 -0
  29. package/packages/hu-board/public/wiki.js +53 -0
  30. package/packages/hu-board/src/config-yaml.js +14 -0
  31. package/packages/hu-board/src/db.js +93 -2
  32. package/packages/hu-board/src/format.js +79 -0
  33. package/packages/hu-board/src/plan-mutations.js +4 -4
  34. package/packages/hu-board/src/routes/api.js +43 -10
  35. package/packages/hu-board/src/routes/rag.js +1 -1
  36. package/packages/hu-board/src/routes/wiki.js +43 -0
  37. package/packages/hu-board/src/run-tracker.js +1 -1
  38. package/packages/hu-board/src/server.js +4 -2
  39. package/packages/hu-board/src/sync.js +1 -1
  40. package/scripts/audit-test-diet.mjs +151 -0
  41. package/scripts/git-hooks/post-merge +13 -2
  42. package/src/audit/ai-slop-findings.js +102 -0
  43. package/src/audit/deterministic-summary.js +18 -0
  44. package/src/audit/harness-scorecard.js +52 -3
  45. package/src/audit/harness-section.js +6 -2
  46. package/src/brain/standby-scheduler.js +10 -92
  47. package/src/brain/standby-store.js +12 -168
  48. package/src/checks/ai-trash.js +34 -0
  49. package/src/checks/qmd.js +41 -0
  50. package/src/checks/runner.js +10 -0
  51. package/src/checks/squeezr.js +36 -0
  52. package/src/cli/register-meta.js +36 -2
  53. package/src/cli/register-pipeline.js +3 -0
  54. package/src/cli/register-plan.js +10 -1
  55. package/src/commands/audit.js +6 -4
  56. package/src/commands/board.js +34 -0
  57. package/src/commands/clean.js +62 -0
  58. package/src/commands/doctor.js +6 -0
  59. package/src/commands/init.js +73 -8
  60. package/src/commands/plan/generate.js +9 -3
  61. package/src/commands/qmd.js +86 -0
  62. package/src/commands/resume.js +6 -45
  63. package/src/commands/run.js +6 -50
  64. package/src/commands/sync.js +30 -2
  65. package/src/config/defaults.js +5 -0
  66. package/src/config/schema.js +2 -0
  67. package/src/git/hu-snapshot.js +10 -105
  68. package/src/orchestrator/config-init.js +1 -0
  69. package/src/orchestrator/drivers/iteration-phases/quality-gates.js +23 -0
  70. package/src/orchestrator/drivers/pre-loop-phases/inject-loaded-plan.js +16 -0
  71. package/src/orchestrator/drivers/run-hu-batch.js +5 -1
  72. package/src/orchestrator/hu-sub-pipeline.js +52 -8
  73. package/src/orchestrator/stages/coder-stage.js +8 -2
  74. package/src/orchestrator/stages/tdd-discipline-stage.js +56 -0
  75. package/src/orchestrator/stages/tool-judge-stage.js +53 -0
  76. package/src/plan/plan-hu-ops.js +16 -338
  77. package/src/plan/plan-id.js +4 -45
  78. package/src/plan/plan-validation.js +4 -82
  79. package/src/prompts/tool-correctness-judge.js +31 -0
  80. package/src/rag/vec-store.js +17 -253
  81. package/src/review/git-stash-helper.js +104 -0
  82. package/src/review/tdd-discipline.js +90 -0
  83. package/src/roles/audit-role.js +11 -1
  84. package/src/roles/tool-correctness-judge-role.js +78 -0
  85. package/src/session/mutators.js +4 -0
  86. package/src/sync/apply-canvas-patch.js +55 -0
  87. package/src/utils/ai-trash.js +48 -0
  88. package/src/utils/atomic-write.js +6 -43
  89. package/src/utils/cli-ask-question.js +76 -0
  90. package/src/utils/doc-links.js +2 -0
  91. package/src/utils/os-detect.js +10 -0
  92. package/src/utils/paths.js +14 -111
  93. package/src/utils/port-check.js +6 -45
  94. package/src/utils/process.js +4 -188
  95. package/src/utils/qmd-collection.js +72 -0
  96. package/src/utils/qmd-detect.js +18 -0
  97. package/src/utils/qmd-install.js +37 -0
  98. package/src/utils/repo-cleaner.js +79 -0
  99. package/src/utils/run-registry.js +9 -116
  100. package/src/utils/shared-paths.js +12 -41
  101. package/src/utils/squeezr-detect.js +18 -0
  102. package/src/utils/squeezr-install.js +37 -0
  103. package/src/utils/tool-call-extractor.js +56 -0
  104. 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 &lt;id&gt;">${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 &lt;id&gt;">${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()">&times;</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()">&times;</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()">&times;</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()">&times;</button></div><p>${esc(err.message)}</p>`;
323
+ }
324
+ }