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 +28 -3
- package/lib/settings-merge.js +41 -6
- package/package.json +1 -1
- package/templates/cabinet/pib-db-access.md +56 -0
- package/templates/hooks/compaction-recover.sh +46 -0
- package/templates/hooks/compaction-save.md +46 -0
- package/templates/mcp/pib-db.json +11 -0
- package/templates/scripts/pib-db-lib.mjs +281 -0
- package/templates/scripts/pib-db-mcp-server.mjs +292 -0
- package/templates/scripts/pib-db.mjs +38 -249
- package/templates/skills/audit/SKILL.md +21 -0
- package/templates/skills/cabinet/SKILL.md +10 -0
- package/templates/skills/cabinet-anthropic-insider/SKILL.md +326 -0
- package/templates/skills/debrief/SKILL.md +9 -0
- package/templates/skills/execute/SKILL.md +11 -0
- package/templates/skills/investigate/SKILL.md +7 -0
- package/templates/skills/orient/SKILL.md +9 -0
- package/templates/skills/plan/SKILL.md +17 -0
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-
|
|
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 {
|
package/lib/settings-merge.js
CHANGED
|
@@ -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
|
|
118
|
-
|
|
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
|
|
121
|
-
const alreadyExists =
|
|
122
|
-
|
|
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
|
@@ -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,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
|
+
}
|