create-claude-cabinet 0.25.3 → 0.25.4
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/package.json +1 -1
- package/templates/hooks/action-completion-gate.sh +1 -2
- package/templates/hooks/action-quality-gate.sh +1 -1
- package/templates/hooks/cc-upstream-guard.sh +4 -6
- package/templates/hooks/git-guardrails.sh +5 -4
- package/templates/hooks/omega-memory-guard.sh +3 -7
- package/templates/hooks/work-tracker-guard.sh +2 -5
- package/templates/scripts/triage-server.mjs +79 -9
- package/templates/scripts/triage-ui.html +37 -3
- package/templates/skills/orient/phases/verify-backfill.md +3 -2
package/package.json
CHANGED
|
@@ -14,7 +14,6 @@ INPUT="$CLAUDE_TOOL_INPUT"
|
|
|
14
14
|
FID=$(echo "$INPUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('fid',''))" 2>/dev/null)
|
|
15
15
|
|
|
16
16
|
if [ -z "$FID" ]; then
|
|
17
|
-
echo '{"decision":"allow"}'
|
|
18
17
|
exit 0
|
|
19
18
|
fi
|
|
20
19
|
|
|
@@ -43,4 +42,4 @@ if [ "$AC_VERIFIED" != "True" ]; then
|
|
|
43
42
|
exit 0
|
|
44
43
|
fi
|
|
45
44
|
|
|
46
|
-
|
|
45
|
+
exit 0
|
|
@@ -14,13 +14,14 @@
|
|
|
14
14
|
#
|
|
15
15
|
# Hook contract:
|
|
16
16
|
# Input: $CLAUDE_TOOL_INPUT has the tool use JSON with "file_path" field
|
|
17
|
-
# Output: JSON on stdout with { "decision": "block"
|
|
17
|
+
# Output: JSON on stdout with { "decision": "block", "reason": "..." }
|
|
18
|
+
# when blocking. Otherwise empty stdout + exit 0 (allow is the
|
|
19
|
+
# default; emitting "allow" violates the hook output schema).
|
|
18
20
|
|
|
19
21
|
# Extract file_path from tool input
|
|
20
22
|
FILE_PATH=$(echo "$CLAUDE_TOOL_INPUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('file_path',''))" 2>/dev/null)
|
|
21
23
|
|
|
22
24
|
if [ -z "$FILE_PATH" ]; then
|
|
23
|
-
echo '{"decision":"allow"}'
|
|
24
25
|
exit 0
|
|
25
26
|
fi
|
|
26
27
|
|
|
@@ -42,7 +43,6 @@ PROJECT_ROOT=$(find_project_root)
|
|
|
42
43
|
|
|
43
44
|
if [ -z "$PROJECT_ROOT" ]; then
|
|
44
45
|
# No .ccrc.json found — not a CC project, allow everything
|
|
45
|
-
echo '{"decision":"allow"}'
|
|
46
46
|
exit 0
|
|
47
47
|
fi
|
|
48
48
|
|
|
@@ -53,7 +53,6 @@ if [[ "$FILE_PATH" = /* ]]; then
|
|
|
53
53
|
REL_PATH="${FILE_PATH#$PROJECT_ROOT/}"
|
|
54
54
|
# If the path didn't change, the file is outside the project
|
|
55
55
|
if [ "$REL_PATH" = "$FILE_PATH" ]; then
|
|
56
|
-
echo '{"decision":"allow"}'
|
|
57
56
|
exit 0
|
|
58
57
|
fi
|
|
59
58
|
else
|
|
@@ -74,6 +73,5 @@ except:
|
|
|
74
73
|
|
|
75
74
|
if [ "$IN_MANIFEST" = "yes" ]; then
|
|
76
75
|
echo "{\"decision\":\"block\",\"reason\":\"Blocked: $REL_PATH is managed by Claude Cabinet. CC-managed files are upstream-owned — edits come through /cc-upgrade, not direct modification. Put project-specific content in briefing files or phase files instead.\"}"
|
|
77
|
-
else
|
|
78
|
-
echo '{"decision":"allow"}'
|
|
79
76
|
fi
|
|
77
|
+
exit 0
|
|
@@ -9,13 +9,15 @@
|
|
|
9
9
|
#
|
|
10
10
|
# Hook contract:
|
|
11
11
|
# Input: $CLAUDE_TOOL_INPUT has the tool use JSON with "command" field
|
|
12
|
-
# Output: JSON on stdout with { "decision": "block"
|
|
12
|
+
# Output: JSON on stdout with { "decision": "block", "reason": "..." }
|
|
13
|
+
# when blocking. Otherwise empty stdout + exit 0 (Claude Code
|
|
14
|
+
# treats no-output as allow; emitting "allow" violates the
|
|
15
|
+
# hook output schema).
|
|
13
16
|
|
|
14
17
|
# Read the command from the tool input
|
|
15
18
|
COMMAND=$(echo "$CLAUDE_TOOL_INPUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('command',''))" 2>/dev/null)
|
|
16
19
|
|
|
17
20
|
if [ -z "$COMMAND" ]; then
|
|
18
|
-
echo '{"decision":"allow"}'
|
|
19
21
|
exit 0
|
|
20
22
|
fi
|
|
21
23
|
|
|
@@ -62,6 +64,5 @@ RESULT=$(check_command "$COMMAND")
|
|
|
62
64
|
|
|
63
65
|
if [ -n "$RESULT" ]; then
|
|
64
66
|
echo "$RESULT"
|
|
65
|
-
else
|
|
66
|
-
echo '{"decision":"allow"}'
|
|
67
67
|
fi
|
|
68
|
+
exit 0
|
|
@@ -16,13 +16,14 @@
|
|
|
16
16
|
#
|
|
17
17
|
# Hook contract:
|
|
18
18
|
# Input: $CLAUDE_TOOL_INPUT has the tool use JSON with "file_path" field
|
|
19
|
-
# Output: JSON on stdout with { "decision": "block"
|
|
19
|
+
# Output: JSON on stdout with { "decision": "block", "reason": "..." }
|
|
20
|
+
# when blocking. Otherwise empty stdout + exit 0 (allow is the
|
|
21
|
+
# default; emitting "allow" violates the hook output schema).
|
|
20
22
|
|
|
21
23
|
# Extract file_path from tool input
|
|
22
24
|
FILE_PATH=$(echo "$CLAUDE_TOOL_INPUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('file_path',''))" 2>/dev/null)
|
|
23
25
|
|
|
24
26
|
if [ -z "$FILE_PATH" ]; then
|
|
25
|
-
echo '{"decision":"allow"}'
|
|
26
27
|
exit 0
|
|
27
28
|
fi
|
|
28
29
|
|
|
@@ -30,21 +31,18 @@ fi
|
|
|
30
31
|
# Note: case patterns with * don't cross / boundaries in some shells,
|
|
31
32
|
# so we use [[ ]] substring matching for absolute path compatibility.
|
|
32
33
|
if [[ "$FILE_PATH" != *"/.claude/memory/"* ]] && [[ "$FILE_PATH" != *"/.claude/projects/"*"/memory/"* ]]; then
|
|
33
|
-
echo '{"decision":"allow"}'
|
|
34
34
|
exit 0
|
|
35
35
|
fi
|
|
36
36
|
|
|
37
37
|
# Allow MEMORY.md index files (structural, not memory content)
|
|
38
38
|
BASENAME=$(basename "$FILE_PATH")
|
|
39
39
|
if [ "$BASENAME" = "MEMORY.md" ]; then
|
|
40
|
-
echo '{"decision":"allow"}'
|
|
41
40
|
exit 0
|
|
42
41
|
fi
|
|
43
42
|
|
|
44
43
|
# Allow pattern files (enforcement pipeline artifacts, not semantic memories)
|
|
45
44
|
case "$FILE_PATH" in
|
|
46
45
|
*/memory/patterns/*)
|
|
47
|
-
echo '{"decision":"allow"}'
|
|
48
46
|
exit 0
|
|
49
47
|
;;
|
|
50
48
|
esac
|
|
@@ -53,7 +51,6 @@ esac
|
|
|
53
51
|
OMEGA_PYTHON="$HOME/.claude-cabinet/omega-venv/bin/python3"
|
|
54
52
|
if [ ! -x "$OMEGA_PYTHON" ]; then
|
|
55
53
|
# Omega not available — flat markdown IS the correct fallback
|
|
56
|
-
echo '{"decision":"allow"}'
|
|
57
54
|
exit 0
|
|
58
55
|
fi
|
|
59
56
|
|
|
@@ -73,7 +70,6 @@ find_adapter() {
|
|
|
73
70
|
ADAPTER=$(find_adapter)
|
|
74
71
|
if [ -z "$ADAPTER" ]; then
|
|
75
72
|
# No adapter found — flat markdown fallback is correct
|
|
76
|
-
echo '{"decision":"allow"}'
|
|
77
73
|
exit 0
|
|
78
74
|
fi
|
|
79
75
|
|
|
@@ -10,7 +10,6 @@ INPUT="$CLAUDE_TOOL_INPUT"
|
|
|
10
10
|
COMMAND=$(echo "$INPUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('command',''))" 2>/dev/null)
|
|
11
11
|
|
|
12
12
|
if [ -z "$COMMAND" ]; then
|
|
13
|
-
echo '{"decision":"allow"}'
|
|
14
13
|
exit 0
|
|
15
14
|
fi
|
|
16
15
|
|
|
@@ -19,20 +18,18 @@ PHASE_FILE=".claude/skills/hooks/phases/work-tracker-guard.md"
|
|
|
19
18
|
if [ -f "$PHASE_FILE" ]; then
|
|
20
19
|
FIRST_LINE=$(head -1 "$PHASE_FILE")
|
|
21
20
|
if [ "$FIRST_LINE" = "skip: true" ]; then
|
|
22
|
-
echo '{"decision":"allow"}'
|
|
23
21
|
exit 0
|
|
24
22
|
fi
|
|
25
23
|
fi
|
|
26
24
|
|
|
27
25
|
# Check for SQL operations against actions table
|
|
28
26
|
if echo "$COMMAND" | grep -qiE '(INSERT\s+INTO|UPDATE|DELETE\s+FROM)\s+actions'; then
|
|
29
|
-
# Override escape hatch
|
|
27
|
+
# Override escape hatch — allow is the default, just exit 0
|
|
30
28
|
if echo "$COMMAND" | grep -q '\-\-force-raw-sql'; then
|
|
31
|
-
echo '{"decision":"allow","reason":"Raw SQL override acknowledged. Quality gates bypassed."}'
|
|
32
29
|
exit 0
|
|
33
30
|
fi
|
|
34
31
|
echo '{"decision":"block","reason":"Raw SQL against actions table detected. Use MCP tools instead: pib_create_action, pib_update_action, pib_complete_action, pib_get_action. These enforce quality gates. To override (almost certainly wrong): add --force-raw-sql to your command."}'
|
|
35
32
|
exit 0
|
|
36
33
|
fi
|
|
37
34
|
|
|
38
|
-
|
|
35
|
+
exit 0
|
|
@@ -1,21 +1,53 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Minimal triage server — serves the triage UI and holds findings/verdicts
|
|
2
|
+
* Minimal triage server — serves the triage UI and holds findings/verdicts.
|
|
3
3
|
*
|
|
4
4
|
* Claude POSTs findings, user triages in browser, Claude GETs verdicts.
|
|
5
|
+
* Each verdict is written through to disk as the user makes it, so page
|
|
6
|
+
* reloads or server restarts don't lose in-progress triage decisions.
|
|
5
7
|
*
|
|
6
8
|
* Usage: node triage-server.mjs [--port 3457]
|
|
7
9
|
*/
|
|
8
10
|
|
|
9
11
|
import { createServer } from 'node:http';
|
|
10
|
-
import { readFile } from 'node:fs/promises';
|
|
12
|
+
import { readFile, writeFile, mkdir, unlink } from 'node:fs/promises';
|
|
11
13
|
import { fileURLToPath } from 'node:url';
|
|
12
14
|
import { dirname, join } from 'node:path';
|
|
13
15
|
|
|
14
16
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
15
17
|
const PORT = parseInt(process.argv.find((_, i, a) => a[i - 1] === '--port') || '3457');
|
|
18
|
+
const DRAFT_PATH = join(process.cwd(), '.claude', 'triage-draft.json');
|
|
16
19
|
|
|
17
20
|
let currentFindings = [];
|
|
18
21
|
let currentVerdicts = null;
|
|
22
|
+
let drafts = {}; // { findingId: { verdict, feedback } }
|
|
23
|
+
|
|
24
|
+
async function loadDrafts() {
|
|
25
|
+
try {
|
|
26
|
+
drafts = JSON.parse(await readFile(DRAFT_PATH, 'utf-8'));
|
|
27
|
+
} catch {
|
|
28
|
+
drafts = {};
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function saveDrafts() {
|
|
33
|
+
await mkdir(dirname(DRAFT_PATH), { recursive: true });
|
|
34
|
+
await writeFile(DRAFT_PATH, JSON.stringify(drafts, null, 2));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function dedupById(items) {
|
|
38
|
+
const seen = new Set();
|
|
39
|
+
const out = [];
|
|
40
|
+
for (const f of items) {
|
|
41
|
+
if (!f || !f.id || seen.has(f.id)) continue;
|
|
42
|
+
seen.add(f.id);
|
|
43
|
+
out.push(f);
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function withDrafts(findings) {
|
|
49
|
+
return findings.map((f) => ({ ...f, draft: drafts[f.id] || null }));
|
|
50
|
+
}
|
|
19
51
|
|
|
20
52
|
function json(res, data, status = 200) {
|
|
21
53
|
res.writeHead(status, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
@@ -53,21 +85,47 @@ const server = createServer(async (req, res) => {
|
|
|
53
85
|
for await (const chunk of req) body += chunk;
|
|
54
86
|
try {
|
|
55
87
|
const data = JSON.parse(body);
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
88
|
+
const raw = data.findings || data;
|
|
89
|
+
currentFindings = dedupById(Array.isArray(raw) ? raw : []);
|
|
90
|
+
currentVerdicts = null;
|
|
91
|
+
// Trim drafts to the new finding id set so a fresh batch doesn't
|
|
92
|
+
// surface stale entries, but a re-POST of the same batch preserves
|
|
93
|
+
// in-progress work.
|
|
94
|
+
const idSet = new Set(currentFindings.map((f) => f.id));
|
|
95
|
+
drafts = Object.fromEntries(Object.entries(drafts).filter(([id]) => idSet.has(id)));
|
|
96
|
+
await saveDrafts();
|
|
97
|
+
console.log(`Loaded ${currentFindings.length} findings (${Object.keys(drafts).length} drafts retained)`);
|
|
59
98
|
return json(res, { ok: true, count: currentFindings.length });
|
|
60
99
|
} catch (err) {
|
|
61
100
|
return json(res, { error: 'Invalid JSON' }, 400);
|
|
62
101
|
}
|
|
63
102
|
}
|
|
64
103
|
|
|
65
|
-
// GET /api/findings — UI fetches findings
|
|
104
|
+
// GET /api/findings — UI fetches findings (with any saved drafts attached)
|
|
66
105
|
if (req.method === 'GET' && url.pathname === '/api/findings') {
|
|
67
|
-
return json(res, { findings: currentFindings });
|
|
106
|
+
return json(res, { findings: withDrafts(currentFindings) });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// POST /api/verdict — UI writes a single verdict as the user makes it
|
|
110
|
+
if (req.method === 'POST' && url.pathname === '/api/verdict') {
|
|
111
|
+
let body = '';
|
|
112
|
+
for await (const chunk of req) body += chunk;
|
|
113
|
+
try {
|
|
114
|
+
const { id, verdict, feedback } = JSON.parse(body);
|
|
115
|
+
if (!id) return json(res, { error: 'Missing id' }, 400);
|
|
116
|
+
const existing = drafts[id] || { verdict: '', feedback: '' };
|
|
117
|
+
drafts[id] = {
|
|
118
|
+
verdict: verdict !== undefined ? verdict : existing.verdict,
|
|
119
|
+
feedback: feedback !== undefined ? feedback : existing.feedback,
|
|
120
|
+
};
|
|
121
|
+
await saveDrafts();
|
|
122
|
+
return json(res, { ok: true });
|
|
123
|
+
} catch (err) {
|
|
124
|
+
return json(res, { error: 'Invalid JSON' }, 400);
|
|
125
|
+
}
|
|
68
126
|
}
|
|
69
127
|
|
|
70
|
-
// POST /api/verdicts — UI submits verdicts
|
|
128
|
+
// POST /api/verdicts — UI submits final verdicts batch
|
|
71
129
|
if (req.method === 'POST' && url.pathname === '/api/verdicts') {
|
|
72
130
|
let body = '';
|
|
73
131
|
for await (const chunk of req) body += chunk;
|
|
@@ -80,7 +138,7 @@ const server = createServer(async (req, res) => {
|
|
|
80
138
|
}
|
|
81
139
|
}
|
|
82
140
|
|
|
83
|
-
// GET /api/verdicts — Claude reads verdicts
|
|
141
|
+
// GET /api/verdicts — Claude reads submitted verdicts
|
|
84
142
|
if (req.method === 'GET' && url.pathname === '/api/verdicts') {
|
|
85
143
|
if (!currentVerdicts) {
|
|
86
144
|
return json(res, { submitted: false, message: 'No verdicts submitted yet' });
|
|
@@ -88,11 +146,23 @@ const server = createServer(async (req, res) => {
|
|
|
88
146
|
return json(res, currentVerdicts);
|
|
89
147
|
}
|
|
90
148
|
|
|
149
|
+
// DELETE /api/draft — clear in-progress draft state
|
|
150
|
+
if (req.method === 'DELETE' && url.pathname === '/api/draft') {
|
|
151
|
+
drafts = {};
|
|
152
|
+
try {
|
|
153
|
+
await unlink(DRAFT_PATH);
|
|
154
|
+
} catch {}
|
|
155
|
+
return json(res, { ok: true });
|
|
156
|
+
}
|
|
157
|
+
|
|
91
158
|
// 404
|
|
92
159
|
res.writeHead(404);
|
|
93
160
|
res.end('Not found');
|
|
94
161
|
});
|
|
95
162
|
|
|
163
|
+
await loadDrafts();
|
|
96
164
|
server.listen(PORT, () => {
|
|
97
165
|
console.log(`Triage server running at http://localhost:${PORT}`);
|
|
166
|
+
const draftCount = Object.keys(drafts).length;
|
|
167
|
+
if (draftCount) console.log(`Restored ${draftCount} in-progress draft verdict(s) from ${DRAFT_PATH}`);
|
|
98
168
|
});
|
|
@@ -285,13 +285,42 @@
|
|
|
285
285
|
let findings = [];
|
|
286
286
|
const verdicts = {}; // { findingId: { verdict, feedback } }
|
|
287
287
|
const memberNotes = {}; // { member: { comment, question } }
|
|
288
|
+
const feedbackDebounce = {}; // { findingId: timeoutId }
|
|
289
|
+
|
|
290
|
+
function dedupById(items) {
|
|
291
|
+
const seen = new Set();
|
|
292
|
+
const out = [];
|
|
293
|
+
for (const f of items) {
|
|
294
|
+
if (!f || !f.id || seen.has(f.id)) continue;
|
|
295
|
+
seen.add(f.id);
|
|
296
|
+
out.push(f);
|
|
297
|
+
}
|
|
298
|
+
return out;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// POST a verdict write-through to the server (silent on failure
|
|
302
|
+
// — write-through is best-effort; submit remains the source of truth)
|
|
303
|
+
function persistVerdict(id) {
|
|
304
|
+
const v = verdicts[id] || { verdict: '', feedback: '' };
|
|
305
|
+
fetch('/api/verdict', {
|
|
306
|
+
method: 'POST',
|
|
307
|
+
headers: { 'Content-Type': 'application/json' },
|
|
308
|
+
body: JSON.stringify({ id, verdict: v.verdict, feedback: v.feedback }),
|
|
309
|
+
}).catch(() => {});
|
|
310
|
+
}
|
|
288
311
|
|
|
289
312
|
// ── Public API for Claude (javascript_tool) ──
|
|
290
313
|
window.loadFindings = function(data) {
|
|
291
|
-
findings = Array.isArray(data) ? data : [];
|
|
292
|
-
//
|
|
314
|
+
findings = dedupById(Array.isArray(data) ? data : []);
|
|
315
|
+
// Hydrate from server-persisted drafts first; recommendations only
|
|
316
|
+
// fill in when there's no prior draft.
|
|
293
317
|
findings.forEach(f => {
|
|
294
|
-
if (f.
|
|
318
|
+
if (f.draft && f.draft.verdict !== undefined) {
|
|
319
|
+
verdicts[f.id] = {
|
|
320
|
+
verdict: f.draft.verdict || '',
|
|
321
|
+
feedback: f.draft.feedback || '',
|
|
322
|
+
};
|
|
323
|
+
} else if (f.recommendation && !verdicts[f.id]) {
|
|
295
324
|
verdicts[f.id] = { verdict: f.recommendation, feedback: '' };
|
|
296
325
|
}
|
|
297
326
|
});
|
|
@@ -476,6 +505,7 @@
|
|
|
476
505
|
const v = e.target.dataset.v;
|
|
477
506
|
if (!verdicts[id]) verdicts[id] = { verdict: '', feedback: '' };
|
|
478
507
|
verdicts[id].verdict = v;
|
|
508
|
+
persistVerdict(id);
|
|
479
509
|
render();
|
|
480
510
|
}
|
|
481
511
|
// Bulk buttons
|
|
@@ -485,6 +515,7 @@
|
|
|
485
515
|
findings.filter(f => f['cabinet-member'] === member).forEach(f => {
|
|
486
516
|
if (!verdicts[f.id]) verdicts[f.id] = { verdict: '', feedback: '' };
|
|
487
517
|
verdicts[f.id].verdict = v;
|
|
518
|
+
persistVerdict(f.id);
|
|
488
519
|
});
|
|
489
520
|
render();
|
|
490
521
|
}
|
|
@@ -504,6 +535,9 @@
|
|
|
504
535
|
const id = e.target.dataset.id;
|
|
505
536
|
if (!verdicts[id]) verdicts[id] = { verdict: '', feedback: '' };
|
|
506
537
|
verdicts[id].feedback = e.target.value;
|
|
538
|
+
// Debounce write-through so we don't POST on every keystroke
|
|
539
|
+
clearTimeout(feedbackDebounce[id]);
|
|
540
|
+
feedbackDebounce[id] = setTimeout(() => persistVerdict(id), 400);
|
|
507
541
|
}
|
|
508
542
|
if (e.target.classList.contains('member-input')) {
|
|
509
543
|
const p = e.target.dataset.member;
|
|
@@ -83,8 +83,9 @@ alongside any items surfaced by deferred-check, health-checks, etc.
|
|
|
83
83
|
|
|
84
84
|
## What this phase does NOT do
|
|
85
85
|
|
|
86
|
-
- It does not modify action notes. The operator runs
|
|
87
|
-
to backfill, or chooses to accept the
|
|
86
|
+
- It does not modify action notes. The operator runs
|
|
87
|
+
`/verify backfill <fid>` to backfill, or chooses to accept the
|
|
88
|
+
drift.
|
|
88
89
|
- It does not file new actions or projects.
|
|
89
90
|
- It does not block orient. Even with 5 backfill candidates, orient
|
|
90
91
|
completes; the Attention Items accumulate.
|