create-claude-cabinet 0.25.3 → 0.26.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/README.md +46 -5
- 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/debrief/SKILL.md +22 -1
- package/templates/skills/orient/phases/verify-backfill.md +3 -2
- package/templates/skills/verify/SKILL.md +32 -7
- package/templates/skills/verify/install.sh +160 -19
- package/templates/skills/verify/phases/calibrate.md +79 -6
- package/templates/skills/verify/phases/discover.md +29 -0
- package/templates/skills/verify/phases/generate.md +31 -0
- package/templates/skills/verify/phases/recipes.md +113 -0
- package/templates/skills/verify/phases/scenario-template.md +49 -17
- package/templates/verify-runtime/package.json +1 -1
- package/templates/verify-runtime/src/baseline-steps.ts +135 -0
- package/templates/verify-runtime/src/index.ts +14 -0
package/README.md
CHANGED
|
@@ -131,6 +131,32 @@ hooks — things that keep going wrong become things that can't go wrong.
|
|
|
131
131
|
- **`/cc-upgrade`** — when Claude Cabinet publishes updates, this skill
|
|
132
132
|
runs the installer for the mechanical parts and walks you through
|
|
133
133
|
what changed conversationally. Intelligence is the merge strategy.
|
|
134
|
+
- **`/cc-feedback`** — file friction with CC itself mid-session
|
|
135
|
+
without waiting for debrief. When a skill, phase, or convention
|
|
136
|
+
causes pain, this captures the detail and queues it for upstream
|
|
137
|
+
delivery to the Claude Cabinet repo.
|
|
138
|
+
|
|
139
|
+
### Verify (opt-in, off by default)
|
|
140
|
+
|
|
141
|
+
Walkthrough verification harness — Cucumber `.feature` files describing
|
|
142
|
+
user journeys, Playwright running them, and human-in-the-loop verdict
|
|
143
|
+
pauses (Pass / Issue / Skip / Needs-info) at checks that need subjective
|
|
144
|
+
judgment. Replaces flat AC checklists with re-runnable scenarios you can
|
|
145
|
+
read months later.
|
|
146
|
+
|
|
147
|
+
- **`/verify`** — run the suite
|
|
148
|
+
- **`/verify learn`** — bootstrap from a cold start. Claude scans
|
|
149
|
+
routes, memory, git, and the live UI; proposes scenarios; calibrates
|
|
150
|
+
with you; then generates `.feature` files and step stubs
|
|
151
|
+
- **`/verify update "I changed X"`** — keep scenarios in sync as the
|
|
152
|
+
product evolves
|
|
153
|
+
- **`/verify backfill <fid>`** — attach a Verify Plan to a pending
|
|
154
|
+
action's notes
|
|
155
|
+
|
|
156
|
+
Enable with `--modules verify` (existing installs merge, nothing else
|
|
157
|
+
disturbed). Runtime lives at `~/.claude-cabinet/verify/<version>/` and
|
|
158
|
+
ships an opinionated `cabinet-verify` npm package built from de[sic]ify's
|
|
159
|
+
e2e harness.
|
|
134
160
|
|
|
135
161
|
## Your Workflow
|
|
136
162
|
|
|
@@ -158,14 +184,29 @@ that override default behavior for any skill. Write content in a phase
|
|
|
158
184
|
file to customize it, write `skip: true` to disable it, or leave it
|
|
159
185
|
absent to use the default. No config files, no YAML, no DSL.
|
|
160
186
|
|
|
187
|
+
## Adding Modules to an Existing Install
|
|
188
|
+
|
|
189
|
+
Some modules (like `verify` and `memory`) are opt-in. To add one
|
|
190
|
+
without touching anything else in your install:
|
|
191
|
+
|
|
192
|
+
```
|
|
193
|
+
npx create-claude-cabinet --modules verify --yes
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
The `--modules` flag **merges** with your existing install — it adds
|
|
197
|
+
the listed modules to what's already there, it doesn't replace your
|
|
198
|
+
module set. Safe to run on a mature project without losing
|
|
199
|
+
customization. You can pass multiple modules: `--modules verify,memory`.
|
|
200
|
+
|
|
161
201
|
## CLI Options
|
|
162
202
|
|
|
163
203
|
```
|
|
164
|
-
npx create-claude-cabinet
|
|
165
|
-
npx create-claude-cabinet my-project
|
|
166
|
-
npx create-claude-cabinet --yes
|
|
167
|
-
npx create-claude-cabinet --yes --no-db
|
|
168
|
-
npx create-claude-cabinet --dry-run
|
|
204
|
+
npx create-claude-cabinet # Interactive walkthrough
|
|
205
|
+
npx create-claude-cabinet my-project # Install in ./my-project/
|
|
206
|
+
npx create-claude-cabinet --yes # Accept all defaults
|
|
207
|
+
npx create-claude-cabinet --yes --no-db # All defaults, skip database
|
|
208
|
+
npx create-claude-cabinet --dry-run # Preview without writing files
|
|
209
|
+
npx create-claude-cabinet --modules verify --yes # Add an opt-in module (merges, doesn't replace)
|
|
169
210
|
```
|
|
170
211
|
|
|
171
212
|
## What Gets Installed
|
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;
|
|
@@ -493,7 +493,8 @@ Read `phases/report.md` for how to present the debrief summary.
|
|
|
493
493
|
|
|
494
494
|
Phases are either **core** (maintain system state) or **presentation**
|
|
495
495
|
(surface information for the user). For lightweight session closes,
|
|
496
|
-
skip presentation phases. Core phases always run
|
|
496
|
+
skip presentation phases. **Core phases always run — Quick Mode is not
|
|
497
|
+
a license to skip them.**
|
|
497
498
|
|
|
498
499
|
- **Core phases** (always run): inventory, close-work,
|
|
499
500
|
cabinet-consultations, audit-pattern-capture, auto-maintenance,
|
|
@@ -505,6 +506,26 @@ skip presentation phases. Core phases always run.
|
|
|
505
506
|
A project that wants a quick debrief variant skips the report and
|
|
506
507
|
outputs a minimal summary instead.
|
|
507
508
|
|
|
509
|
+
### What Quick Mode does NOT skip
|
|
510
|
+
|
|
511
|
+
**Cabinet-consultations (step 3) is core and MUST run — do NOT skip,
|
|
512
|
+
do NOT paraphrase, do NOT defer.** This is where record-keeper (and
|
|
513
|
+
any other debrief-mandated members) verifies documentation against
|
|
514
|
+
reality. Skipping it is the single most common Quick Mode failure
|
|
515
|
+
mode: the consultations *feel* like overhead because they spawn
|
|
516
|
+
agents, but they are the only mechanism that catches doc drift,
|
|
517
|
+
methodology gaps, and stale state. A debrief without
|
|
518
|
+
cabinet-consultations leaves the next orient reading stale docs.
|
|
519
|
+
|
|
520
|
+
**Audit-pattern-capture, methodology-capture, and upstream-feedback
|
|
521
|
+
are instruction phases — they ship with CC and are always required**,
|
|
522
|
+
in Quick Mode as much as in full debrief. Their per-session cost is
|
|
523
|
+
near zero when nothing fires.
|
|
524
|
+
|
|
525
|
+
If a session genuinely has no audit findings, no methodology work,
|
|
526
|
+
and no CC friction, those phases self-skip in seconds. That is not
|
|
527
|
+
the same as Claude choosing to skip them.
|
|
528
|
+
|
|
508
529
|
## Extending and Calibration
|
|
509
530
|
|
|
510
531
|
See [calibration.md](calibration.md) for the phase-extension pattern
|
|
@@ -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.
|
|
@@ -30,6 +30,9 @@ related:
|
|
|
30
30
|
- type: file
|
|
31
31
|
path: .claude/skills/verify/phases/backfill.md
|
|
32
32
|
role: "How to draft a ## Verify Plan section for a pending action"
|
|
33
|
+
- type: file
|
|
34
|
+
path: .claude/skills/verify/phases/recipes.md
|
|
35
|
+
role: "Testability gotchas (dnd-kit drag, dynamic file inputs, hash routing) and their workarounds"
|
|
33
36
|
- type: file
|
|
34
37
|
path: cabinet/_briefing.md
|
|
35
38
|
role: "Project identity and configuration"
|
|
@@ -125,8 +128,15 @@ scenario change.
|
|
|
125
128
|
|
|
126
129
|
1. Check if `e2e/` exists in the project root. If not, recommend
|
|
127
130
|
`/verify learn` and exit.
|
|
128
|
-
2.
|
|
129
|
-
|
|
131
|
+
2. **Test-isolation nudge.** If `e2e/` exists but `e2e/start-test-stack.sh`
|
|
132
|
+
does not — and the project's dev stack is suspected to write to a
|
|
133
|
+
real DB (signal: `package.json` references a single `data/`,
|
|
134
|
+
`db/`, or similar shared persistence path) — surface a one-line
|
|
135
|
+
note before running: *"No isolated test stack detected. Scenarios
|
|
136
|
+
will run against your dev stack. Run `/verify learn` to generate
|
|
137
|
+
an isolation scaffold if your dev DB matters."* Do not block.
|
|
138
|
+
3. Run `npm run verify` from the project's `e2e/` dir.
|
|
139
|
+
4. Surface the output. If failures or I-verdicts landed, suggest
|
|
130
140
|
`npm run report:last` to triage.
|
|
131
141
|
|
|
132
142
|
This mode is intentionally thin — the harness is the value, not
|
|
@@ -155,8 +165,12 @@ The "learn" flow runs four phases:
|
|
|
155
165
|
question at a time. Examples: "I see admin routes but only one admin
|
|
156
166
|
user — real persona or fold into main?", "Should the fresh-user
|
|
157
167
|
flow be its own scenario or part of admin?", "What's the dev stack
|
|
158
|
-
URL for preflight?".
|
|
159
|
-
project
|
|
168
|
+
URL for preflight?". Calibrate also includes a **test-isolation
|
|
169
|
+
probe** — if the project's dev stack writes to a real DB, the
|
|
170
|
+
skill captures the DB path, dev-server proxy config, and test
|
|
171
|
+
stack ports so install.sh can emit an `e2e/start-test-stack.sh`
|
|
172
|
+
scaffold. Do NOT batch questions — one at a time per project
|
|
173
|
+
convention.
|
|
160
174
|
|
|
161
175
|
4. **Generate** (read `phases/generate.md`): write the `.feature`
|
|
162
176
|
files using the template in `phases/scenario-template.md` plus
|
|
@@ -221,14 +235,25 @@ once the action runs. Backfill only adds the planning artifact so
|
|
|
221
235
|
| `update.md` | Default: action fid / diff / free-text dispatch | How change descriptions map to edits |
|
|
222
236
|
| `scenario-template.md` | Default: Gherkin with cost+role tags, NN.NN checkIds | Project-specific scenario shape |
|
|
223
237
|
| `backfill.md` | Default: interview-driven Verify Plan section drafting | Project-specific backfill questions |
|
|
238
|
+
| `recipes.md` | Default: dnd-kit, dynamic file input, hash routing gotchas | Project-specific testability recipes |
|
|
224
239
|
|
|
225
240
|
## Principles
|
|
226
241
|
|
|
227
242
|
- **One question at a time.** Calibrate phase NEVER batches questions
|
|
228
243
|
(per CLAUDE.md global convention). Each answer shapes the next.
|
|
229
|
-
- **≤5 scenarios on initial draft.** Force calibration
|
|
230
|
-
expansion. Adding scenarios later is cheap; removing scenarios
|
|
231
|
-
the user
|
|
244
|
+
- **≤5 scenarios on initial draft (cabinet-qa cap).** Force calibration
|
|
245
|
+
before expansion. Adding scenarios later is cheap; removing scenarios
|
|
246
|
+
the user did not ask for is expensive (per process-therapist). The
|
|
247
|
+
cap is load-bearing; do not loosen it without an audit-grade reason.
|
|
248
|
+
- **Depth-first, not shallow-first.** A scenario that touches a
|
|
249
|
+
surface but verifies nothing is worse than no scenario at all —
|
|
250
|
+
it occupies a slot in the catalogue and creates a false sense of
|
|
251
|
+
coverage. The first lap through a scenario should produce real
|
|
252
|
+
assertions and human-verdict pauses for the parts that genuinely
|
|
253
|
+
need subjective judgment. If a step can not be exercised (a
|
|
254
|
+
testability gotcha — see `phases/recipes.md`), file a finding
|
|
255
|
+
against the consuming project and mark the step "skip until
|
|
256
|
+
testable" rather than emitting a no-op stub.
|
|
232
257
|
- **cabinet-qa owns "what's worth a scenario".** /verify learn
|
|
233
258
|
delegates that judgment via subagent; it doesn't re-derive it.
|
|
234
259
|
- **The .feature file is the spec.** Anyone (user, future Claude,
|