create-claude-cabinet 0.6.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 (135) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +196 -0
  3. package/bin/create-claude-cabinet.js +8 -0
  4. package/lib/cli.js +624 -0
  5. package/lib/copy.js +152 -0
  6. package/lib/db-setup.js +51 -0
  7. package/lib/metadata.js +42 -0
  8. package/lib/reset.js +193 -0
  9. package/lib/settings-merge.js +93 -0
  10. package/package.json +29 -0
  11. package/templates/EXTENSIONS.md +311 -0
  12. package/templates/README.md +485 -0
  13. package/templates/briefing/_briefing-api-template.md +21 -0
  14. package/templates/briefing/_briefing-architecture-template.md +16 -0
  15. package/templates/briefing/_briefing-cabinet-template.md +20 -0
  16. package/templates/briefing/_briefing-identity-template.md +18 -0
  17. package/templates/briefing/_briefing-scopes-template.md +39 -0
  18. package/templates/briefing/_briefing-template.md +148 -0
  19. package/templates/briefing/_briefing-work-tracking-template.md +18 -0
  20. package/templates/cabinet/committees-template.yaml +49 -0
  21. package/templates/cabinet/composition-patterns.md +240 -0
  22. package/templates/cabinet/eval-protocol.md +208 -0
  23. package/templates/cabinet/lifecycle.md +93 -0
  24. package/templates/cabinet/output-contract.md +148 -0
  25. package/templates/cabinet/prompt-guide.md +266 -0
  26. package/templates/hooks/cor-upstream-guard.sh +79 -0
  27. package/templates/hooks/git-guardrails.sh +67 -0
  28. package/templates/hooks/skill-telemetry.sh +66 -0
  29. package/templates/hooks/skill-tool-telemetry.sh +54 -0
  30. package/templates/hooks/stop-hook.md +56 -0
  31. package/templates/memory/patterns/_pattern-template.md +119 -0
  32. package/templates/memory/patterns/pattern-intelligence-first.md +41 -0
  33. package/templates/rules/enforcement-pipeline.md +151 -0
  34. package/templates/scripts/cor-drift-check.cjs +84 -0
  35. package/templates/scripts/finding-schema.json +94 -0
  36. package/templates/scripts/load-triage-history.js +151 -0
  37. package/templates/scripts/merge-findings.js +126 -0
  38. package/templates/scripts/pib-db-schema.sql +68 -0
  39. package/templates/scripts/pib-db.js +365 -0
  40. package/templates/scripts/triage-server.mjs +98 -0
  41. package/templates/scripts/triage-ui.html +536 -0
  42. package/templates/skills/audit/SKILL.md +273 -0
  43. package/templates/skills/audit/phases/finding-output.md +56 -0
  44. package/templates/skills/audit/phases/member-execution.md +83 -0
  45. package/templates/skills/audit/phases/member-selection.md +44 -0
  46. package/templates/skills/audit/phases/structural-checks.md +54 -0
  47. package/templates/skills/audit/phases/triage-history.md +45 -0
  48. package/templates/skills/cabinet-accessibility/SKILL.md +180 -0
  49. package/templates/skills/cabinet-anti-confirmation/SKILL.md +172 -0
  50. package/templates/skills/cabinet-architecture/SKILL.md +279 -0
  51. package/templates/skills/cabinet-boundary-man/SKILL.md +265 -0
  52. package/templates/skills/cabinet-cor-health/SKILL.md +342 -0
  53. package/templates/skills/cabinet-data-integrity/SKILL.md +157 -0
  54. package/templates/skills/cabinet-debugger/SKILL.md +221 -0
  55. package/templates/skills/cabinet-historian/SKILL.md +253 -0
  56. package/templates/skills/cabinet-organized-mind/SKILL.md +338 -0
  57. package/templates/skills/cabinet-process-therapist/SKILL.md +261 -0
  58. package/templates/skills/cabinet-qa/SKILL.md +205 -0
  59. package/templates/skills/cabinet-record-keeper/SKILL.md +168 -0
  60. package/templates/skills/cabinet-roster-check/SKILL.md +297 -0
  61. package/templates/skills/cabinet-security/SKILL.md +181 -0
  62. package/templates/skills/cabinet-small-screen/SKILL.md +154 -0
  63. package/templates/skills/cabinet-speed-freak/SKILL.md +169 -0
  64. package/templates/skills/cabinet-system-advocate/SKILL.md +194 -0
  65. package/templates/skills/cabinet-technical-debt/SKILL.md +115 -0
  66. package/templates/skills/cabinet-usability/SKILL.md +189 -0
  67. package/templates/skills/cabinet-workflow-cop/SKILL.md +238 -0
  68. package/templates/skills/cor-upgrade/SKILL.md +302 -0
  69. package/templates/skills/debrief/SKILL.md +409 -0
  70. package/templates/skills/debrief/phases/auto-maintenance.md +48 -0
  71. package/templates/skills/debrief/phases/close-work.md +88 -0
  72. package/templates/skills/debrief/phases/health-checks.md +54 -0
  73. package/templates/skills/debrief/phases/inventory.md +40 -0
  74. package/templates/skills/debrief/phases/loose-ends.md +52 -0
  75. package/templates/skills/debrief/phases/record-lessons.md +67 -0
  76. package/templates/skills/debrief/phases/report.md +59 -0
  77. package/templates/skills/debrief/phases/update-state.md +48 -0
  78. package/templates/skills/debrief/phases/upstream-feedback.md +129 -0
  79. package/templates/skills/debrief-quick/SKILL.md +12 -0
  80. package/templates/skills/execute/SKILL.md +293 -0
  81. package/templates/skills/execute/phases/cabinet.md +49 -0
  82. package/templates/skills/execute/phases/commit-and-deploy.md +66 -0
  83. package/templates/skills/execute/phases/load-plan.md +49 -0
  84. package/templates/skills/execute/phases/validators.md +50 -0
  85. package/templates/skills/execute/phases/verification-tools.md +67 -0
  86. package/templates/skills/extract/SKILL.md +168 -0
  87. package/templates/skills/investigate/SKILL.md +160 -0
  88. package/templates/skills/link/SKILL.md +52 -0
  89. package/templates/skills/menu/SKILL.md +61 -0
  90. package/templates/skills/onboard/SKILL.md +356 -0
  91. package/templates/skills/onboard/phases/detect-state.md +79 -0
  92. package/templates/skills/onboard/phases/generate-briefing.md +127 -0
  93. package/templates/skills/onboard/phases/generate-session-loop.md +87 -0
  94. package/templates/skills/onboard/phases/interview.md +233 -0
  95. package/templates/skills/onboard/phases/modularity-menu.md +162 -0
  96. package/templates/skills/onboard/phases/options.md +98 -0
  97. package/templates/skills/onboard/phases/post-onboard-audit.md +121 -0
  98. package/templates/skills/onboard/phases/summary.md +122 -0
  99. package/templates/skills/onboard/phases/work-tracking.md +231 -0
  100. package/templates/skills/orient/SKILL.md +251 -0
  101. package/templates/skills/orient/phases/auto-maintenance.md +48 -0
  102. package/templates/skills/orient/phases/briefing.md +53 -0
  103. package/templates/skills/orient/phases/cabinet.md +46 -0
  104. package/templates/skills/orient/phases/context.md +63 -0
  105. package/templates/skills/orient/phases/data-sync.md +35 -0
  106. package/templates/skills/orient/phases/health-checks.md +50 -0
  107. package/templates/skills/orient/phases/work-scan.md +69 -0
  108. package/templates/skills/orient-quick/SKILL.md +12 -0
  109. package/templates/skills/plan/SKILL.md +358 -0
  110. package/templates/skills/plan/phases/cabinet-critique.md +47 -0
  111. package/templates/skills/plan/phases/calibration-examples.md +75 -0
  112. package/templates/skills/plan/phases/completeness-check.md +44 -0
  113. package/templates/skills/plan/phases/composition-check.md +36 -0
  114. package/templates/skills/plan/phases/overlap-check.md +62 -0
  115. package/templates/skills/plan/phases/plan-template.md +69 -0
  116. package/templates/skills/plan/phases/present.md +60 -0
  117. package/templates/skills/plan/phases/research.md +43 -0
  118. package/templates/skills/plan/phases/work-tracker.md +95 -0
  119. package/templates/skills/publish/SKILL.md +74 -0
  120. package/templates/skills/pulse/SKILL.md +242 -0
  121. package/templates/skills/pulse/phases/auto-fix-scope.md +40 -0
  122. package/templates/skills/pulse/phases/checks.md +58 -0
  123. package/templates/skills/pulse/phases/output.md +54 -0
  124. package/templates/skills/seed/SKILL.md +257 -0
  125. package/templates/skills/seed/phases/build-member.md +93 -0
  126. package/templates/skills/seed/phases/evaluate-existing.md +61 -0
  127. package/templates/skills/seed/phases/maintain.md +92 -0
  128. package/templates/skills/seed/phases/scan-signals.md +86 -0
  129. package/templates/skills/triage-audit/SKILL.md +251 -0
  130. package/templates/skills/triage-audit/phases/apply-verdicts.md +90 -0
  131. package/templates/skills/triage-audit/phases/load-findings.md +38 -0
  132. package/templates/skills/triage-audit/phases/triage-ui.md +66 -0
  133. package/templates/skills/unlink/SKILL.md +35 -0
  134. package/templates/skills/validate/SKILL.md +116 -0
  135. package/templates/skills/validate/phases/validators.md +53 -0
@@ -0,0 +1,536 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Audit Triage</title>
7
+ <meta name="color-scheme" content="light dark">
8
+ <style>
9
+ * { box-sizing: border-box; margin: 0; padding: 0; }
10
+ body {
11
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
12
+ padding: 20px 24px;
13
+ color: #c1c2c5;
14
+ background: #1a1b1e;
15
+ font-size: 13px;
16
+ line-height: 1.5;
17
+ max-width: 960px;
18
+ margin: 0 auto;
19
+ }
20
+
21
+ header {
22
+ display: flex;
23
+ justify-content: space-between;
24
+ align-items: center;
25
+ margin-bottom: 16px;
26
+ padding-bottom: 12px;
27
+ border-bottom: 1px solid #373a40;
28
+ }
29
+ h1 { font-size: 18px; color: #fff; font-weight: 600; }
30
+ .run-info { color: #909296; font-size: 12px; }
31
+
32
+ /* Perspective groups */
33
+ .perspective-group {
34
+ margin-bottom: 16px;
35
+ border: 1px solid #373a40;
36
+ border-radius: 8px;
37
+ overflow: hidden;
38
+ }
39
+ .perspective-header {
40
+ display: flex;
41
+ align-items: center;
42
+ justify-content: space-between;
43
+ padding: 10px 14px;
44
+ background: #25262b;
45
+ cursor: pointer;
46
+ user-select: none;
47
+ }
48
+ .perspective-header:hover { background: #2c2e33; }
49
+ .perspective-name {
50
+ font-weight: 600;
51
+ color: #e9ecef;
52
+ font-size: 14px;
53
+ }
54
+ .perspective-stats {
55
+ display: flex;
56
+ gap: 8px;
57
+ align-items: center;
58
+ font-size: 12px;
59
+ color: #909296;
60
+ }
61
+ .perspective-body { padding: 0; }
62
+ .perspective-body.collapsed { display: none; }
63
+
64
+ /* Finding rows */
65
+ .finding {
66
+ padding: 10px 14px;
67
+ border-top: 1px solid #2c2e33;
68
+ transition: background 0.1s;
69
+ }
70
+ .finding:hover { background: #25262b; }
71
+ .finding-top {
72
+ display: flex;
73
+ align-items: flex-start;
74
+ gap: 8px;
75
+ margin-bottom: 4px;
76
+ }
77
+ .badge {
78
+ display: inline-block;
79
+ padding: 1px 7px;
80
+ border-radius: 3px;
81
+ font-size: 10px;
82
+ font-weight: 700;
83
+ text-transform: uppercase;
84
+ letter-spacing: 0.3px;
85
+ flex-shrink: 0;
86
+ margin-top: 2px;
87
+ }
88
+ .badge-critical { background: #e03131; color: #fff; }
89
+ .badge-warn { background: #f08c00; color: #fff; }
90
+ .badge-info { background: #1c7ed6; color: #fff; }
91
+ .badge-idea { background: #7048e8; color: #fff; }
92
+ .finding-title { color: #e9ecef; font-weight: 500; flex: 1; }
93
+ .finding-id { color: #5c5f66; font-size: 11px; font-family: monospace; flex-shrink: 0; }
94
+
95
+ .commentary {
96
+ color: #909296;
97
+ font-size: 12px;
98
+ margin: 4px 0 8px 0;
99
+ padding-left: 2px;
100
+ line-height: 1.6;
101
+ }
102
+
103
+ .controls {
104
+ display: flex;
105
+ gap: 5px;
106
+ align-items: center;
107
+ flex-wrap: wrap;
108
+ }
109
+ .verdict-btn {
110
+ padding: 3px 10px;
111
+ border: 1px solid #373a40;
112
+ border-radius: 4px;
113
+ background: #2c2e33;
114
+ color: #909296;
115
+ cursor: pointer;
116
+ font-size: 11px;
117
+ font-weight: 500;
118
+ transition: all 0.12s;
119
+ }
120
+ .verdict-btn:hover { background: #373a40; color: #c1c2c5; }
121
+ .verdict-btn.sel-fix { background: #2b8a3e; color: #fff; border-color: #2b8a3e; }
122
+ .verdict-btn.sel-defer { background: #5c5f66; color: #fff; border-color: #5c5f66; }
123
+ .verdict-btn.sel-reject { background: #e03131; color: #fff; border-color: #e03131; }
124
+ .verdict-btn.sel-question { background: #1c7ed6; color: #fff; border-color: #1c7ed6; }
125
+
126
+ .feedback-input {
127
+ flex: 1;
128
+ min-width: 140px;
129
+ padding: 3px 8px;
130
+ border: 1px solid #373a40;
131
+ border-radius: 4px;
132
+ background: #1a1b1e;
133
+ color: #c1c2c5;
134
+ font-size: 11px;
135
+ }
136
+ .feedback-input::placeholder { color: #5c5f66; }
137
+
138
+ /* Perspective-level controls */
139
+ .perspective-controls {
140
+ padding: 8px 14px;
141
+ background: #2c2e33;
142
+ border-top: 1px solid #373a40;
143
+ display: flex;
144
+ flex-direction: column;
145
+ gap: 6px;
146
+ }
147
+ .perspective-controls-row {
148
+ display: flex;
149
+ gap: 8px;
150
+ align-items: center;
151
+ font-size: 12px;
152
+ }
153
+ .perspective-controls-row label { color: #909296; flex-shrink: 0; }
154
+ .perspective-input {
155
+ flex: 1;
156
+ padding: 4px 8px;
157
+ border: 1px solid #373a40;
158
+ border-radius: 4px;
159
+ background: #1a1b1e;
160
+ color: #c1c2c5;
161
+ font-size: 12px;
162
+ }
163
+ .perspective-input::placeholder { color: #5c5f66; }
164
+ .perspective-q-btn {
165
+ padding: 3px 10px;
166
+ border: 1px solid #1c7ed6;
167
+ border-radius: 4px;
168
+ background: #25262b;
169
+ color: #1c7ed6;
170
+ cursor: pointer;
171
+ font-size: 11px;
172
+ font-weight: 500;
173
+ flex-shrink: 0;
174
+ }
175
+ .perspective-q-btn:hover { background: #1c7ed622; }
176
+ .perspective-q-btn.active { background: #1c7ed6; color: #fff; }
177
+
178
+ /* Bulk actions */
179
+ .bulk-bar {
180
+ display: flex;
181
+ gap: 8px;
182
+ padding: 8px 14px;
183
+ background: #2c2e33;
184
+ border-top: 1px solid #373a40;
185
+ align-items: center;
186
+ font-size: 12px;
187
+ }
188
+ .bulk-bar label { color: #909296; }
189
+ .bulk-btn {
190
+ padding: 3px 10px;
191
+ border: 1px solid #373a40;
192
+ border-radius: 4px;
193
+ background: #25262b;
194
+ color: #c1c2c5;
195
+ cursor: pointer;
196
+ font-size: 11px;
197
+ }
198
+ .bulk-btn:hover { background: #373a40; }
199
+
200
+ /* Bottom bar */
201
+ .bottom-bar {
202
+ position: sticky;
203
+ bottom: 0;
204
+ background: #1a1b1e;
205
+ border-top: 1px solid #373a40;
206
+ padding: 14px 0;
207
+ display: flex;
208
+ justify-content: space-between;
209
+ align-items: center;
210
+ margin-top: 16px;
211
+ }
212
+ .progress { color: #909296; font-size: 13px; }
213
+ .progress strong { color: #e9ecef; }
214
+ .submit-btn {
215
+ padding: 8px 28px;
216
+ background: #7048e8;
217
+ color: #fff;
218
+ border: none;
219
+ border-radius: 6px;
220
+ font-size: 14px;
221
+ font-weight: 600;
222
+ cursor: pointer;
223
+ transition: background 0.15s;
224
+ }
225
+ .submit-btn:hover { background: #6741d9; }
226
+ .submit-btn:disabled { opacity: 0.4; cursor: not-allowed; }
227
+
228
+ #status {
229
+ margin-top: 10px;
230
+ padding: 10px 14px;
231
+ background: #2b8a3e22;
232
+ border: 1px solid #2b8a3e;
233
+ border-radius: 6px;
234
+ color: #51cf66;
235
+ font-weight: 600;
236
+ display: none;
237
+ }
238
+
239
+ #loading {
240
+ text-align: center;
241
+ padding: 60px 20px;
242
+ color: #909296;
243
+ }
244
+ #loading .spinner {
245
+ display: inline-block;
246
+ width: 24px;
247
+ height: 24px;
248
+ border: 2px solid #373a40;
249
+ border-top-color: #7048e8;
250
+ border-radius: 50%;
251
+ animation: spin 0.8s linear infinite;
252
+ margin-bottom: 12px;
253
+ }
254
+ @keyframes spin { to { transform: rotate(360deg); } }
255
+
256
+ .empty-state {
257
+ text-align: center;
258
+ padding: 60px 20px;
259
+ color: #5c5f66;
260
+ }
261
+ </style>
262
+ </head>
263
+ <body>
264
+ <header>
265
+ <h1>Audit Triage</h1>
266
+ <span class="run-info" id="run-info"></span>
267
+ </header>
268
+
269
+ <div id="loading">
270
+ <div class="spinner"></div>
271
+ <div>Loading findings...</div>
272
+ </div>
273
+
274
+ <div id="findings-root"></div>
275
+
276
+ <div class="bottom-bar" id="bottom-bar" style="display:none">
277
+ <span class="progress" id="progress"></span>
278
+ <button class="submit-btn" id="submit-btn" disabled>Submit Verdicts</button>
279
+ </div>
280
+
281
+ <div id="status"></div>
282
+
283
+ <script>
284
+ // ── State ──
285
+ let findings = [];
286
+ const verdicts = {}; // { findingId: { verdict, feedback } }
287
+ const perspectiveNotes = {}; // { perspective: { comment, question } }
288
+
289
+ // ── Public API for Claude (javascript_tool) ──
290
+ window.loadFindings = function(data) {
291
+ findings = Array.isArray(data) ? data : [];
292
+ // Pre-populate from recommendations
293
+ findings.forEach(f => {
294
+ if (f.recommendation && !verdicts[f.id]) {
295
+ verdicts[f.id] = { verdict: f.recommendation, feedback: '' };
296
+ }
297
+ });
298
+ render();
299
+ return `Loaded ${findings.length} findings`;
300
+ };
301
+
302
+ window.getVerdicts = function() {
303
+ return JSON.stringify({
304
+ total: findings.length,
305
+ triaged: Object.keys(verdicts).length,
306
+ submitted: !!window._submitted,
307
+ perspectiveNotes: Object.fromEntries(
308
+ Object.entries(perspectiveNotes).filter(([_, v]) => v.comment || v.question)
309
+ ),
310
+ verdicts: findings.map(f => ({
311
+ id: f.id,
312
+ perspective: f.perspective,
313
+ severity: f.severity,
314
+ title: f.title,
315
+ verdict: verdicts[f.id]?.verdict || null,
316
+ feedback: verdicts[f.id]?.feedback || '',
317
+ }))
318
+ });
319
+ };
320
+
321
+ window.getVerdictSummary = function() {
322
+ const counts = { fix: 0, defer: 0, reject: 0, question: 0, untriaged: 0 };
323
+ findings.forEach(f => {
324
+ const v = verdicts[f.id]?.verdict;
325
+ if (v) counts[v]++;
326
+ else counts.untriaged++;
327
+ });
328
+ return JSON.stringify(counts);
329
+ };
330
+
331
+ // ── Try fetching from server ──
332
+ fetch('/api/findings')
333
+ .then(r => r.ok ? r.json() : Promise.reject())
334
+ .then(data => {
335
+ if (data.findings?.length) window.loadFindings(data.findings);
336
+ else showEmpty();
337
+ })
338
+ .catch(() => showEmpty());
339
+
340
+ function showEmpty() {
341
+ document.getElementById('loading').innerHTML =
342
+ '<div class="empty-state">No findings loaded.<br>Claude will inject findings via javascript_tool.</div>';
343
+ }
344
+
345
+ // ── Render ──
346
+ function render() {
347
+ document.getElementById('loading').style.display = 'none';
348
+ document.getElementById('bottom-bar').style.display = 'flex';
349
+
350
+ // Group by perspective
351
+ const groups = {};
352
+ findings.forEach(f => {
353
+ if (!groups[f.perspective]) groups[f.perspective] = [];
354
+ groups[f.perspective].push(f);
355
+ });
356
+
357
+ const root = document.getElementById('findings-root');
358
+ root.innerHTML = '';
359
+
360
+ // Sort perspectives alphabetically
361
+ const perspectives = Object.keys(groups).sort();
362
+
363
+ perspectives.forEach(perspective => {
364
+ const items = groups[perspective];
365
+ const triaged = items.filter(f => verdicts[f.id]?.verdict).length;
366
+
367
+ const group = document.createElement('div');
368
+ group.className = 'perspective-group';
369
+
370
+ // Header
371
+ const header = document.createElement('div');
372
+ header.className = 'perspective-header';
373
+ header.innerHTML = `
374
+ <span class="perspective-name">${perspective} (${items.length})</span>
375
+ <span class="perspective-stats">
376
+ ${triaged}/${items.length} triaged
377
+ </span>
378
+ `;
379
+ header.addEventListener('click', () => {
380
+ body.classList.toggle('collapsed');
381
+ });
382
+ group.appendChild(header);
383
+
384
+ // Body
385
+ const body = document.createElement('div');
386
+ body.className = 'perspective-body';
387
+
388
+ items.forEach(f => {
389
+ const row = document.createElement('div');
390
+ row.className = 'finding';
391
+ const v = verdicts[f.id]?.verdict || '';
392
+
393
+ row.innerHTML = `
394
+ <div class="finding-top">
395
+ <span class="badge badge-${f.severity}">${f.severity}</span>
396
+ <span class="finding-title">${esc(f.title)}</span>
397
+ <span class="finding-id">${esc(f.id)}</span>
398
+ </div>
399
+ ${f.commentary ? `<div class="commentary">${esc(f.commentary)}</div>` : ''}
400
+ <div class="controls">
401
+ <button class="verdict-btn ${v === 'fix' ? 'sel-fix' : ''}" data-id="${f.id}" data-v="fix">Fix</button>
402
+ <button class="verdict-btn ${v === 'defer' ? 'sel-defer' : ''}" data-id="${f.id}" data-v="defer">Defer</button>
403
+ <button class="verdict-btn ${v === 'reject' ? 'sel-reject' : ''}" data-id="${f.id}" data-v="reject">Reject</button>
404
+ <button class="verdict-btn ${v === 'question' ? 'sel-question' : ''}" data-id="${f.id}" data-v="question">?</button>
405
+ <input class="feedback-input" data-id="${f.id}" placeholder="notes..." value="${esc(verdicts[f.id]?.feedback || '')}">
406
+ </div>
407
+ `;
408
+ body.appendChild(row);
409
+ });
410
+
411
+ // Bulk action bar
412
+ const bulk = document.createElement('div');
413
+ bulk.className = 'bulk-bar';
414
+ bulk.innerHTML = `
415
+ <label>Bulk:</label>
416
+ <button class="bulk-btn" data-p="${perspective}" data-v="fix">All Fix</button>
417
+ <button class="bulk-btn" data-p="${perspective}" data-v="defer">All Defer</button>
418
+ <button class="bulk-btn" data-p="${perspective}" data-v="reject">All Reject</button>
419
+ `;
420
+ body.appendChild(bulk);
421
+
422
+ // Perspective-level controls
423
+ const pn = perspectiveNotes[perspective] || { comment: '', question: false };
424
+ const pControls = document.createElement('div');
425
+ pControls.className = 'perspective-controls';
426
+ pControls.innerHTML = `
427
+ <div class="perspective-controls-row">
428
+ <button class="perspective-q-btn ${pn.question ? 'active' : ''}" data-perspective="${perspective}">?</button>
429
+ <input class="perspective-input" data-perspective="${perspective}"
430
+ placeholder="${pn.question ? 'your question about this perspective...' : 'comment on this perspective...'}"
431
+ value="${esc(pn.comment || '')}">
432
+ </div>
433
+ `;
434
+ body.appendChild(pControls);
435
+
436
+ group.appendChild(body);
437
+ root.appendChild(group);
438
+ });
439
+
440
+ updateProgress();
441
+ if (document.getElementById('run-info'))
442
+ document.getElementById('run-info').textContent = `${findings.length} findings`;
443
+ }
444
+
445
+ function updateProgress() {
446
+ const triaged = Object.values(verdicts).filter(v => v.verdict).length;
447
+ const el = document.getElementById('progress');
448
+ el.innerHTML = `<strong>${triaged}</strong> / ${findings.length} triaged`;
449
+ document.getElementById('submit-btn').disabled = triaged < findings.length;
450
+ // Highlight feedback inputs for question verdicts
451
+ document.querySelectorAll('.feedback-input').forEach(input => {
452
+ const id = input.dataset.id;
453
+ if (verdicts[id]?.verdict === 'question') {
454
+ input.placeholder = 'your question...';
455
+ input.style.borderColor = '#1c7ed6';
456
+ } else {
457
+ input.placeholder = 'notes...';
458
+ input.style.borderColor = '#373a40';
459
+ }
460
+ });
461
+ }
462
+
463
+ function esc(s) {
464
+ if (!s) return '';
465
+ const d = document.createElement('div');
466
+ d.textContent = s;
467
+ return d.innerHTML;
468
+ }
469
+
470
+ // ── Event delegation ──
471
+ document.addEventListener('click', e => {
472
+ // Verdict buttons
473
+ if (e.target.classList.contains('verdict-btn')) {
474
+ const id = e.target.dataset.id;
475
+ const v = e.target.dataset.v;
476
+ if (!verdicts[id]) verdicts[id] = { verdict: '', feedback: '' };
477
+ verdicts[id].verdict = v;
478
+ render();
479
+ }
480
+ // Bulk buttons
481
+ if (e.target.classList.contains('bulk-btn')) {
482
+ const perspective = e.target.dataset.p;
483
+ const v = e.target.dataset.v;
484
+ findings.filter(f => f.perspective === perspective).forEach(f => {
485
+ if (!verdicts[f.id]) verdicts[f.id] = { verdict: '', feedback: '' };
486
+ verdicts[f.id].verdict = v;
487
+ });
488
+ render();
489
+ }
490
+ });
491
+
492
+ document.addEventListener('click', e => {
493
+ if (e.target.classList.contains('perspective-q-btn')) {
494
+ const p = e.target.dataset.perspective;
495
+ if (!perspectiveNotes[p]) perspectiveNotes[p] = { comment: '', question: false };
496
+ perspectiveNotes[p].question = !perspectiveNotes[p].question;
497
+ render();
498
+ }
499
+ });
500
+
501
+ document.addEventListener('input', e => {
502
+ if (e.target.classList.contains('feedback-input')) {
503
+ const id = e.target.dataset.id;
504
+ if (!verdicts[id]) verdicts[id] = { verdict: '', feedback: '' };
505
+ verdicts[id].feedback = e.target.value;
506
+ }
507
+ if (e.target.classList.contains('perspective-input')) {
508
+ const p = e.target.dataset.perspective;
509
+ if (!perspectiveNotes[p]) perspectiveNotes[p] = { comment: '', question: false };
510
+ perspectiveNotes[p].comment = e.target.value;
511
+ }
512
+ });
513
+
514
+ // Submit
515
+ document.getElementById('submit-btn').addEventListener('click', async () => {
516
+ window._submitted = true;
517
+ const results = window.getVerdicts();
518
+
519
+ // Try posting to server
520
+ try {
521
+ await fetch('/api/verdicts', {
522
+ method: 'POST',
523
+ headers: { 'Content-Type': 'application/json' },
524
+ body: results,
525
+ });
526
+ } catch {}
527
+
528
+ document.getElementById('status').style.display = 'block';
529
+ document.getElementById('status').textContent =
530
+ 'Verdicts submitted. Tell Claude to read results.';
531
+ document.getElementById('submit-btn').disabled = true;
532
+ document.getElementById('submit-btn').textContent = 'Submitted';
533
+ });
534
+ </script>
535
+ </body>
536
+ </html>