create-claude-cabinet 0.14.2 → 0.15.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/lib/cli.js CHANGED
@@ -252,6 +252,9 @@ function generateSkillIndex(projectDir) {
252
252
  type: isCabinet ? 'cabinet' : 'workflow',
253
253
  };
254
254
 
255
+ // Argument hint (for skill index consumers)
256
+ if (fm['argument-hint']) entry.argumentHint = fm['argument-hint'];
257
+
255
258
  // Invocability flags
256
259
  if (fm['disable-model-invocation'] === 'true') entry.manual = true;
257
260
  if (fm['user-invocable'] === 'false') entry.userInvocable = false;
@@ -353,7 +356,7 @@ const MODULES = {
353
356
  mandatory: false,
354
357
  default: true,
355
358
  lean: true,
356
- templates: ['hooks/git-guardrails.sh', 'hooks/cc-upstream-guard.sh', 'hooks/skill-telemetry.sh', 'hooks/skill-tool-telemetry.sh', 'scripts/cc-drift-check.cjs'],
359
+ templates: ['hooks/git-guardrails.sh', 'hooks/cc-upstream-guard.sh', 'hooks/skill-telemetry.sh', 'hooks/skill-tool-telemetry.sh', 'hooks/compaction-save.md', 'hooks/compaction-recover.sh', 'scripts/cc-drift-check.cjs'],
357
360
  },
358
361
  'work-tracking': {
359
362
  name: 'Work Tracking (pib-db or markdown)',
@@ -361,7 +364,7 @@ const MODULES = {
361
364
  mandatory: false,
362
365
  default: true,
363
366
  lean: false,
364
- templates: ['scripts/pib-db.mjs', 'scripts/pib-db-schema.sql', 'scripts/work-tracker-server.mjs', 'scripts/work-tracker-ui.html', 'skills/work-tracker'],
367
+ templates: ['scripts/pib-db.mjs', 'scripts/pib-db-lib.mjs', 'scripts/pib-db-mcp-server.mjs', 'scripts/pib-db-schema.sql', 'scripts/work-tracker-server.mjs', 'scripts/work-tracker-ui.html', 'skills/work-tracker'],
365
368
  needsDb: true,
366
369
  },
367
370
  'planning': {
@@ -391,7 +394,8 @@ const MODULES = {
391
394
  'cabinet', 'briefing',
392
395
  'skills/cabinet-accessibility', 'skills/cabinet-anti-confirmation',
393
396
  'skills/cabinet-architecture', 'skills/cabinet-boundary-man',
394
- 'skills/cabinet-cc-health', 'skills/cabinet-data-integrity',
397
+ 'skills/cabinet-anthropic-insider', 'skills/cabinet-cc-health',
398
+ 'skills/cabinet-data-integrity',
395
399
  'skills/cabinet-debugger', 'skills/cabinet-historian',
396
400
  'skills/cabinet-organized-mind', 'skills/cabinet-process-therapist',
397
401
  'skills/cabinet-qa', 'skills/cabinet-record-keeper',
@@ -891,6 +895,27 @@ async function run() {
891
895
  console.log(` ⚙️ Merged hooks into ${path.relative(projectDir, settingsPath)}`);
892
896
  }
893
897
 
898
+ // --- Merge pib-db MCP server into .mcp.json ---
899
+ if (selectedModules.includes('work-tracking') && !flags.dryRun) {
900
+ try {
901
+ const mcpTemplatePath = path.join(templateRoot, 'mcp', 'pib-db.json');
902
+ if (fs.existsSync(mcpTemplatePath)) {
903
+ const mcpConfig = JSON.parse(fs.readFileSync(mcpTemplatePath, 'utf8'));
904
+ const mcpJsonPath = path.join(projectDir, '.mcp.json');
905
+ let existing = {};
906
+ if (fs.existsSync(mcpJsonPath)) {
907
+ existing = JSON.parse(fs.readFileSync(mcpJsonPath, 'utf8'));
908
+ }
909
+ if (!existing.mcpServers) existing.mcpServers = {};
910
+ Object.assign(existing.mcpServers, mcpConfig.mcpServers);
911
+ fs.writeFileSync(mcpJsonPath, JSON.stringify(existing, null, 2) + '\n');
912
+ console.log(` 🔌 Merged pib-db MCP server into .mcp.json`);
913
+ }
914
+ } catch (err) {
915
+ console.log(` ⚠ MCP config merge failed: ${err.message}`);
916
+ }
917
+ }
918
+
894
919
  // --- Set up database ---
895
920
  if (includeDb && selectedModules.includes('work-tracking') && !flags.dryRun) {
896
921
  try {
@@ -15,6 +15,19 @@ const MEMORY_HOOKS = {
15
15
  ],
16
16
  };
17
17
 
18
+ // Prompt text for the PreCompact hook. Source of truth: templates/hooks/compaction-save.md
19
+ const COMPACTION_SAVE_PROMPT = `Before compaction destroys your current context, you MUST save state so the next session can recover.
20
+
21
+ REQUIRED — Always write .claude/compaction-state.md with these sections:
22
+ - Current Task: what you were actively working on (file paths, function names, exact step)
23
+ - Decisions Made: key decisions with reasoning
24
+ - Next Steps: ordered list, most urgent first
25
+ - References: files, URLs, error messages needed by next context
26
+
27
+ CONDITIONAL — If mid-workflow with intermediate results, ALSO write .claude/<workflow-name>-partial.md (e.g. .claude/audit-partial.md for a mid-audit). Include completed items, partial results, progress tracking.
28
+
29
+ Keep total output under 200 lines. Use concrete details, not vague summaries. Write the files now.`;
30
+
18
31
  const DEFAULT_HOOKS = {
19
32
  PreToolUse: [
20
33
  {
@@ -58,6 +71,27 @@ const DEFAULT_HOOKS = {
58
71
  ],
59
72
  },
60
73
  ],
74
+ PreCompact: [
75
+ {
76
+ hooks: [
77
+ {
78
+ type: 'prompt',
79
+ prompt: COMPACTION_SAVE_PROMPT,
80
+ },
81
+ ],
82
+ },
83
+ ],
84
+ SessionStart: [
85
+ {
86
+ matcher: 'compact',
87
+ hooks: [
88
+ {
89
+ type: 'command',
90
+ command: '.claude/hooks/compaction-recover.sh',
91
+ },
92
+ ],
93
+ },
94
+ ],
61
95
  };
62
96
 
63
97
  /**
@@ -112,14 +146,15 @@ function mergeSettings(projectDir, { includeDb = true, includeMemory = false } =
112
146
  if (!settings.hooks[event]) {
113
147
  settings.hooks[event] = newHooks;
114
148
  } else {
115
- // Add hooks that don't already exist (check by command path)
149
+ // Add hooks that don't already exist (check by command path or prompt text)
116
150
  for (const newHook of newHooks) {
117
- const existingCommands = settings.hooks[event].flatMap(h =>
118
- h.hooks.map(hh => hh.command)
151
+ const hookKey = h => h.command || h.prompt || '';
152
+ const existingKeys = settings.hooks[event].flatMap(h =>
153
+ h.hooks.map(hh => hookKey(hh))
119
154
  );
120
- const newCommands = newHook.hooks.map(h => h.command);
121
- const alreadyExists = newCommands.every(cmd =>
122
- existingCommands.includes(cmd)
155
+ const newKeys = newHook.hooks.map(h => hookKey(h));
156
+ const alreadyExists = newKeys.every(k =>
157
+ existingKeys.includes(k)
123
158
  );
124
159
  if (!alreadyExists) {
125
160
  settings.hooks[event].push(newHook);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-claude-cabinet",
3
- "version": "0.14.2",
3
+ "version": "0.15.0",
4
4
  "description": "Claude Cabinet — opinionated process scaffolding for Claude Code projects",
5
5
  "bin": {
6
6
  "create-claude-cabinet": "bin/create-claude-cabinet.js"
@@ -0,0 +1,56 @@
1
+ # pib-db Access Protocol
2
+
3
+ How to interact with the process infrastructure database (pib-db).
4
+
5
+ ## Preference Order
6
+
7
+ 1. **MCP tools (preferred):** If `pib_*` MCP tools are available (check
8
+ by attempting to use them), use them directly. They return structured
9
+ JSON — no parsing needed.
10
+
11
+ 2. **CLI fallback:** If MCP tools are not available, use the CLI:
12
+ ```bash
13
+ node scripts/pib-db.mjs <command> [args]
14
+ ```
15
+
16
+ Skills should reference this document instead of embedding their own
17
+ fallback logic. The access method is determined once at the start of the
18
+ skill execution:
19
+
20
+ ```
21
+ Check: are pib_* MCP tools available?
22
+ YES → use pib_list_projects, pib_create_action, etc.
23
+ NO → use node scripts/pib-db.mjs list-projects, etc.
24
+ ```
25
+
26
+ ## Available Operations
27
+
28
+ | MCP Tool | CLI Equivalent | Description |
29
+ | ---------------------- | --------------------------------------- | ------------------------------ |
30
+ | pib_create_project | create-project "name" --area X | Create a project |
31
+ | pib_list_projects | list-projects | List active projects |
32
+ | pib_create_action | create-action "text" --notes X | Create an action (work item) |
33
+ | pib_list_actions | list-actions [--status X] | List actions |
34
+ | pib_update_action | update-action fid --status X | Update action fields |
35
+ | pib_complete_action | complete-action fid | Mark action done |
36
+ | pib_ingest_findings | ingest-findings run-dir | Ingest audit findings |
37
+ | pib_triage | triage finding-id status [notes] | Triage a finding |
38
+ | pib_triage_history | triage-history | Get suppression list |
39
+ | pib_query | query "SQL" | Run arbitrary SQL |
40
+
41
+ ## Surface Area Validation
42
+
43
+ `pib_create_action` (and the CLI `create-action`) require that notes
44
+ contain a `## Surface Area` section with at least one `- files:` or
45
+ `- dirs:` line. This ensures every action clearly defines what it
46
+ touches.
47
+
48
+ Example notes format:
49
+ ```
50
+ Implement the new feature.
51
+
52
+ ## Surface Area
53
+ - files: src/components/Widget.js
54
+ - files: src/utils/helpers.js
55
+ - dirs: tests/components/
56
+ ```
@@ -0,0 +1,46 @@
1
+ #!/bin/bash
2
+ # Compaction Recovery — SessionStart command hook (compact matcher)
3
+ #
4
+ # Reads compaction state files written by the PreCompact prompt hook
5
+ # and outputs them to stdout for injection as additionalContext.
6
+ #
7
+ # Files checked:
8
+ # .claude/compaction-state.md — always (main state)
9
+ # .claude/*-partial.md — any workflow partial state files
10
+
11
+ PROJECT_DIR="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
12
+ STATE_FILE="$PROJECT_DIR/.claude/compaction-state.md"
13
+
14
+ # Collect all partial state files
15
+ PARTIAL_FILES=()
16
+ for f in "$PROJECT_DIR"/.claude/*-partial.md; do
17
+ [ -f "$f" ] && PARTIAL_FILES+=("$f")
18
+ done
19
+
20
+ # If no state files exist, output fallback message
21
+ if [ ! -f "$STATE_FILE" ] && [ ${#PARTIAL_FILES[@]} -eq 0 ]; then
22
+ echo "No compaction state found. This session started fresh (no prior compaction state to recover)."
23
+ exit 0
24
+ fi
25
+
26
+ echo "=== COMPACTION RECOVERY ==="
27
+ echo ""
28
+ echo "State was saved before compaction. Use this to resume where you left off."
29
+ echo ""
30
+
31
+ # Output main state file
32
+ if [ -f "$STATE_FILE" ]; then
33
+ echo "--- compaction-state.md ---"
34
+ cat "$STATE_FILE"
35
+ echo ""
36
+ fi
37
+
38
+ # Output any partial state files
39
+ for f in "${PARTIAL_FILES[@]}"; do
40
+ BASENAME=$(basename "$f")
41
+ echo "--- $BASENAME ---"
42
+ cat "$f"
43
+ echo ""
44
+ done
45
+
46
+ echo "=== END COMPACTION RECOVERY ==="
@@ -0,0 +1,46 @@
1
+ # Compaction State Save — PreCompact Prompt Hook
2
+
3
+ Before compaction destroys your current context, you MUST save state so the
4
+ next session can recover. This is not optional — compaction erases everything
5
+ you're holding in working memory.
6
+
7
+ ## Required: Always write .claude/compaction-state.md
8
+
9
+ Write this file with these structured sections:
10
+
11
+ ```
12
+ # Compaction State
13
+
14
+ ## Current Task
15
+ What you were actively working on. Be specific — include file paths, function
16
+ names, the exact step you were on.
17
+
18
+ ## Decisions Made
19
+ Key decisions from this session that a fresh context needs to know.
20
+ Include the reasoning, not just the conclusion.
21
+
22
+ ## Next Steps
23
+ What should happen immediately after recovery. Ordered list, most urgent first.
24
+
25
+ ## References
26
+ Files, URLs, error messages, or other artifacts that the next context will need.
27
+ ```
28
+
29
+ ## Conditional: Write workflow-specific partial state
30
+
31
+ If you are in the middle of a multi-step workflow with intermediate results,
32
+ ALSO write a partial state file to `.claude/<workflow-name>-partial.md`.
33
+
34
+ Use the workflow you're currently executing as the filename. Examples:
35
+ - Mid-audit with findings collected so far → `.claude/audit-partial.md`
36
+ - Mid-migration with some files moved → `.claude/migration-partial.md`
37
+ - Mid-refactor tracking what's done → `.claude/refactor-partial.md`
38
+
39
+ Include whatever intermediate work products would be lost to compaction:
40
+ completed items, partial results, progress tracking, error logs.
41
+
42
+ ## Constraints
43
+
44
+ - Keep total output under 200 lines across all files written.
45
+ - Use concrete details (file paths, line numbers, variable names), not vague summaries.
46
+ - Write the files using the Edit/Write tools — do not just describe what you would write.
@@ -0,0 +1,11 @@
1
+ {
2
+ "mcpServers": {
3
+ "pib-db": {
4
+ "command": "node",
5
+ "args": ["scripts/pib-db-mcp-server.mjs"],
6
+ "env": {
7
+ "PIB_DB_PATH": "./pib.db"
8
+ }
9
+ }
10
+ }
11
+ }
@@ -0,0 +1,281 @@
1
+ // Process-in-a-Box shared library
2
+ //
3
+ // All database operations as importable functions.
4
+ // Both the CLI (pib-db.mjs) and MCP server (pib-db-mcp-server.mjs)
5
+ // import from here. Schema changes update one place.
6
+ //
7
+ // Every function takes (db, params) and returns a result object.
8
+ // None of them do console.log — callers decide how to present output.
9
+
10
+ import { existsSync, readFileSync } from 'node:fs';
11
+ import { join } from 'node:path';
12
+ import { randomUUID } from 'node:crypto';
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // Helpers
16
+ // ---------------------------------------------------------------------------
17
+ function generateFid(prefix) {
18
+ return `${prefix}:${randomUUID().replace(/-/g, '').slice(0, 8)}`;
19
+ }
20
+
21
+ function today() {
22
+ return new Date().toISOString().slice(0, 10);
23
+ }
24
+
25
+ // ---------------------------------------------------------------------------
26
+ // Init — create tables from schema
27
+ // ---------------------------------------------------------------------------
28
+ export function init(db, { schemaPath }) {
29
+ const schema = readFileSync(schemaPath, 'utf-8');
30
+ db.exec(schema);
31
+
32
+ // Migrate existing DBs — add columns that may not exist yet
33
+ const migrations = [
34
+ { table: 'actions', column: 'status', sql: "ALTER TABLE actions ADD COLUMN status TEXT NOT NULL DEFAULT 'open' CHECK(status IN ('open','in-progress','blocked','deferred','done'))" },
35
+ { table: 'actions', column: 'tags', sql: "ALTER TABLE actions ADD COLUMN tags TEXT NOT NULL DEFAULT ''" },
36
+ ];
37
+ for (const m of migrations) {
38
+ const cols = db.prepare(`PRAGMA table_info(${m.table})`).all();
39
+ if (!cols.some(c => c.name === m.column)) {
40
+ try { db.exec(m.sql); } catch { /* column may already exist */ }
41
+ }
42
+ }
43
+
44
+ return { message: `Database initialized` };
45
+ }
46
+
47
+ // ---------------------------------------------------------------------------
48
+ // Query — run arbitrary SQL
49
+ // ---------------------------------------------------------------------------
50
+ export function query(db, { sql }) {
51
+ if (sql.trim().toUpperCase().startsWith('SELECT')) {
52
+ const rows = db.prepare(sql).all();
53
+ return { rows };
54
+ } else {
55
+ db.exec(sql);
56
+ return { message: 'Done.' };
57
+ }
58
+ }
59
+
60
+ // ---------------------------------------------------------------------------
61
+ // Actions
62
+ // ---------------------------------------------------------------------------
63
+
64
+ /**
65
+ * Validate that notes contain a ## Surface Area section with at least one
66
+ * - files: or - dirs: line. Returns null if valid, or an error object if not.
67
+ */
68
+ function validateSurfaceArea(notes) {
69
+ if (!notes) {
70
+ return {
71
+ error: 'missing_surface_area',
72
+ message: 'Action notes must contain a ## Surface Area section.',
73
+ suggestedFormat: [
74
+ '## Surface Area',
75
+ '- files: path/to/file.js',
76
+ '- files: path/to/other.js',
77
+ '- dirs: src/components/',
78
+ ].join('\n'),
79
+ };
80
+ }
81
+
82
+ const hasSection = /^## Surface Area/m.test(notes);
83
+ if (!hasSection) {
84
+ return {
85
+ error: 'missing_surface_area',
86
+ message: 'Action notes must contain a ## Surface Area section.',
87
+ suggestedFormat: [
88
+ '## Surface Area',
89
+ '- files: path/to/file.js',
90
+ '- files: path/to/other.js',
91
+ '- dirs: src/components/',
92
+ ].join('\n'),
93
+ };
94
+ }
95
+
96
+ // Extract everything after ## Surface Area until the next ## or end
97
+ const sectionMatch = notes.match(/^## Surface Area\s*\n([\s\S]*?)(?=\n## |\n*$)/m);
98
+ const sectionBody = sectionMatch ? sectionMatch[1] : '';
99
+ const hasEntry = /^- (?:files|dirs):/m.test(sectionBody);
100
+ if (!hasEntry) {
101
+ return {
102
+ error: 'empty_surface_area',
103
+ message: '## Surface Area section must contain at least one "- files:" or "- dirs:" line.',
104
+ suggestedFormat: [
105
+ '## Surface Area',
106
+ '- files: path/to/file.js',
107
+ '- dirs: src/components/',
108
+ ].join('\n'),
109
+ };
110
+ }
111
+
112
+ return null; // valid
113
+ }
114
+
115
+ export function createAction(db, { text, area, projectFid, due, notes }) {
116
+ const validationError = validateSurfaceArea(notes);
117
+ if (validationError) {
118
+ return { error: validationError };
119
+ }
120
+
121
+ const fid = generateFid('act');
122
+ db.prepare(`
123
+ INSERT INTO actions (fid, text, area, project_fid, due, notes, created)
124
+ VALUES (?, ?, ?, ?, ?, ?, ?)
125
+ `).run(fid, text, area || null, projectFid || null, due || null, notes || '', today());
126
+ return { fid, text, message: `Created action ${fid}: ${text}` };
127
+ }
128
+
129
+ export function listActions(db, { status, project } = {}) {
130
+ const conditions = ['a.deleted_at IS NULL'];
131
+ const params = [];
132
+
133
+ if (status) {
134
+ conditions.push('a.status = ?');
135
+ params.push(status);
136
+ } else {
137
+ conditions.push('a.completed = 0');
138
+ }
139
+ if (project) {
140
+ conditions.push('a.project_fid = ?');
141
+ params.push(project);
142
+ }
143
+
144
+ const rows = db.prepare(`
145
+ SELECT a.fid, a.text, a.area, a.due, a.flagged, a.status, a.tags, p.name as project
146
+ FROM actions a
147
+ LEFT JOIN projects p ON a.project_fid = p.fid
148
+ WHERE ${conditions.join(' AND ')}
149
+ ORDER BY
150
+ CASE WHEN a.due IS NOT NULL AND a.due <= date('now') THEN 0 ELSE 1 END,
151
+ a.due,
152
+ a.flagged DESC,
153
+ a.created DESC
154
+ `).all(...params);
155
+ return { rows };
156
+ }
157
+
158
+ export function updateAction(db, { fid, status, text, tags, notes, due, flagged }) {
159
+ const sets = [];
160
+ const params = [];
161
+
162
+ if (status !== undefined) { sets.push('status = ?'); params.push(status); }
163
+ if (text !== undefined) { sets.push('text = ?'); params.push(text); }
164
+ if (tags !== undefined) { sets.push('tags = ?'); params.push(tags); }
165
+ if (notes !== undefined) { sets.push('notes = ?'); params.push(notes); }
166
+ if (due !== undefined) { sets.push('due = ?'); params.push(due); }
167
+ if (flagged !== undefined) { sets.push('flagged = ?'); params.push(flagged === 'true' || flagged === '1' || flagged === true ? 1 : 0); }
168
+
169
+ // If marking done, also set completed fields
170
+ if (status === 'done') {
171
+ sets.push('completed = 1', 'completed_at = ?');
172
+ params.push(new Date().toISOString());
173
+ }
174
+
175
+ if (sets.length === 0) {
176
+ return { error: { message: 'No fields to update. Use status, text, tags, notes, due, or flagged.' } };
177
+ }
178
+
179
+ params.push(fid);
180
+ db.prepare(`UPDATE actions SET ${sets.join(', ')} WHERE fid = ?`).run(...params);
181
+ return { fid, message: `Updated ${fid}` };
182
+ }
183
+
184
+ export function completeAction(db, { fid }) {
185
+ db.prepare(`
186
+ UPDATE actions SET completed = 1, completed_at = ?, status = 'done' WHERE fid = ?
187
+ `).run(new Date().toISOString(), fid);
188
+ return { fid, message: `Completed ${fid}` };
189
+ }
190
+
191
+ // ---------------------------------------------------------------------------
192
+ // Projects
193
+ // ---------------------------------------------------------------------------
194
+ export function createProject(db, { name, area, notes, due }) {
195
+ const fid = generateFid('prj');
196
+ db.prepare(`
197
+ INSERT INTO projects (fid, name, area, notes, due, created)
198
+ VALUES (?, ?, ?, ?, ?, ?)
199
+ `).run(fid, name, area || null, notes || '', due || null, today());
200
+ return { fid, name, message: `Created project ${fid}: ${name}` };
201
+ }
202
+
203
+ export function listProjects(db) {
204
+ const rows = db.prepare(`
205
+ SELECT p.fid, p.name, p.area, p.status, p.due,
206
+ (SELECT COUNT(*) FROM actions a WHERE a.project_fid = p.fid AND a.completed = 0 AND a.deleted_at IS NULL) as open_actions
207
+ FROM projects p
208
+ WHERE p.status = 'active' AND p.deleted_at IS NULL
209
+ ORDER BY p.created DESC
210
+ `).all();
211
+ return { rows };
212
+ }
213
+
214
+ // ---------------------------------------------------------------------------
215
+ // Audit — ingest findings from a run directory
216
+ // ---------------------------------------------------------------------------
217
+ export function ingestFindings(db, { runDir }) {
218
+ const summaryPath = join(runDir, 'run-summary.json');
219
+ if (!existsSync(summaryPath)) {
220
+ return { error: { message: `No run-summary.json found in ${runDir}` } };
221
+ }
222
+ const data = JSON.parse(readFileSync(summaryPath, 'utf-8'));
223
+ const runId = data.meta?.runId || `run-${Date.now()}`;
224
+ const timestamp = data.meta?.timestamp || new Date().toISOString();
225
+ const dateStr = timestamp.slice(0, 10);
226
+
227
+ db.prepare(`
228
+ INSERT OR REPLACE INTO audit_runs (id, date, timestamp, trigger, finding_count)
229
+ VALUES (?, ?, ?, ?, ?)
230
+ `).run(runId, dateStr, timestamp, data.meta?.trigger || 'manual', data.findings?.length || 0);
231
+
232
+ const insert = db.prepare(`
233
+ INSERT OR REPLACE INTO audit_findings
234
+ (id, run_id, cabinet_member, severity, title, description, assumption,
235
+ evidence, question, file, line, suggested_fix, auto_fixable, type)
236
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
237
+ `);
238
+
239
+ let count = 0;
240
+ for (const f of (data.findings || [])) {
241
+ insert.run(
242
+ f.id, runId, f['cabinet-member'], f.severity, f.title,
243
+ f.description || null, f.assumption || null, f.evidence || null,
244
+ f.question || null, f.file || null, f.line || null,
245
+ f.suggestedFix || null, f.autoFixable ? 1 : 0, f.type || 'finding'
246
+ );
247
+ count++;
248
+ }
249
+ return { count, runId, message: `Ingested ${count} findings from ${runDir} (run: ${runId})` };
250
+ }
251
+
252
+ // ---------------------------------------------------------------------------
253
+ // Triage
254
+ // ---------------------------------------------------------------------------
255
+ export function triage(db, { findingId, status, notes }) {
256
+ db.prepare(`
257
+ UPDATE audit_findings
258
+ SET triage_status = ?, triage_notes = ?, triaged_at = ?
259
+ WHERE id = ?
260
+ `).run(status, notes || null, new Date().toISOString(), findingId);
261
+ return { findingId, status, message: `Triaged ${findingId} → ${status}` };
262
+ }
263
+
264
+ export function triageHistory(db) {
265
+ const rejected = db.prepare(`
266
+ SELECT id, cabinet_member, title FROM audit_findings
267
+ WHERE triage_status = 'rejected'
268
+ `).all();
269
+
270
+ const deferred = db.prepare(`
271
+ SELECT id, cabinet_member, title FROM audit_findings
272
+ WHERE triage_status = 'deferred'
273
+ `).all();
274
+
275
+ return {
276
+ rejectedIds: rejected.map(r => r.id),
277
+ rejectedFingerprints: rejected.map(r => ({ 'cabinet-member': r.cabinet_member, title: r.title })),
278
+ deferredIds: deferred.map(r => r.id),
279
+ deferredFingerprints: deferred.map(r => ({ 'cabinet-member': r.cabinet_member, title: r.title })),
280
+ };
281
+ }