create-claude-cabinet 0.18.0 → 0.19.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  # Claude Cabinet
2
2
 
3
3
  A cabinet of expert advisors for your Claude Code project. One command
4
- gives Claude a memory, 27 domain experts, a planning process, and the
4
+ gives Claude a memory, 30 domain experts, a planning process, and the
5
5
  habit of starting sessions informed and ending them properly.
6
6
 
7
7
  Built by a guy who'd rather talk to Claude than write code. Most of it
@@ -12,7 +12,7 @@ was built by Claude. I just complained until it worked.
12
12
  Your project gets a cabinet — specialist advisors who each own a domain
13
13
  and weigh in when their expertise matters:
14
14
 
15
- - **Cabinet members** — 27 domain experts (security, accessibility,
15
+ - **Cabinet members** — 30 domain experts (security, accessibility,
16
16
  architecture, QA, etc.) who review your project and surface what
17
17
  you'd miss alone
18
18
  - **Briefings** — project context members read before weighing in
@@ -78,7 +78,7 @@ left off.
78
78
 
79
79
  ### The Cabinet (included in lean)
80
80
 
81
- 27 expert cabinet members who each own a domain and stay in their lane.
81
+ 30 expert cabinet members who each own a domain and stay in their lane.
82
82
  **Speed-freak** watches performance. **Boundary-man** catches edge cases.
83
83
  **Record-keeper** flags when docs drift from code. **Workflow-cop**
84
84
  evaluates whether your process actually works. Each member has a
@@ -176,7 +176,7 @@ source code.
176
176
  ```
177
177
  .claude/
178
178
  ├── skills/ # orient, debrief, plan, execute, audit, etc.
179
- │ └── cabinet-*/ # 27 cabinet member definitions
179
+ │ └── cabinet-*/ # 30 cabinet member definitions
180
180
  ├── cabinet/ # committees, lifecycle, composition patterns
181
181
  ├── briefing/ # project briefing templates
182
182
  ├── hooks/ # git guardrails, telemetry
package/lib/cli.js CHANGED
@@ -356,7 +356,7 @@ const MODULES = {
356
356
  mandatory: false,
357
357
  default: true,
358
358
  lean: true,
359
- 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/work-tracker-guard.sh', 'hooks/action-quality-gate.sh', 'hooks/action-completion-gate.sh', 'hooks/domain-memories.sh', 'scripts/cc-drift-check.cjs'],
360
360
  },
361
361
  'work-tracking': {
362
362
  name: 'Work Tracking (pib-db or markdown)',
@@ -412,6 +412,7 @@ const MODULES = {
412
412
  'scripts/merge-findings.js', 'scripts/load-triage-history.js',
413
413
  'scripts/triage-server.mjs', 'scripts/triage-ui.html',
414
414
  'scripts/finding-schema.json', 'scripts/resolve-committees.cjs',
415
+ 'scripts/review-server.mjs', 'scripts/review-ui.html',
415
416
  ],
416
417
  },
417
418
  'lifecycle': {
@@ -437,7 +438,7 @@ const MODULES = {
437
438
  default: true,
438
439
  lean: false,
439
440
  needsOmega: true,
440
- templates: ['skills/memory', 'scripts/cabinet-memory-adapter.py', 'rules/memory-capture.md', 'hooks/omega-memory-guard.sh'],
441
+ templates: ['skills/memory', 'scripts/cabinet-memory-adapter.py', 'scripts/migrate-memory-to-omega.py', 'rules/memory-capture.md', 'hooks/omega-memory-guard.sh'],
441
442
  },
442
443
  };
443
444
 
@@ -26,6 +26,15 @@ const DEFAULT_HOOKS = {
26
26
  },
27
27
  ],
28
28
  },
29
+ {
30
+ matcher: 'Bash',
31
+ hooks: [
32
+ {
33
+ type: 'command',
34
+ command: '.claude/hooks/work-tracker-guard.sh',
35
+ },
36
+ ],
37
+ },
29
38
  {
30
39
  matcher: 'Edit|Write',
31
40
  hooks: [
@@ -35,6 +44,33 @@ const DEFAULT_HOOKS = {
35
44
  },
36
45
  ],
37
46
  },
47
+ {
48
+ matcher: 'Edit|Write',
49
+ hooks: [
50
+ {
51
+ type: 'prompt',
52
+ command: '.claude/hooks/domain-memories.sh',
53
+ },
54
+ ],
55
+ },
56
+ {
57
+ matcher: 'pib_create_action',
58
+ hooks: [
59
+ {
60
+ type: 'command',
61
+ command: '.claude/hooks/action-quality-gate.sh',
62
+ },
63
+ ],
64
+ },
65
+ {
66
+ matcher: 'pib_complete_action',
67
+ hooks: [
68
+ {
69
+ type: 'command',
70
+ command: '.claude/hooks/action-completion-gate.sh',
71
+ },
72
+ ],
73
+ },
38
74
  ],
39
75
  UserPromptSubmit: [
40
76
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-claude-cabinet",
3
- "version": "0.18.0",
3
+ "version": "0.19.1",
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"
@@ -27,7 +27,7 @@ templates, see [EXTENSIONS.md](EXTENSIONS.md).
27
27
  | `rules/enforcement-pipeline.md` | Generic enforcement pipeline: capture, classify, promote, encode, monitor. Describes the compliance stack and promotion criteria. |
28
28
  | `rules/memory-capture.md` | When and how to capture memories to omega. What to capture, what not to, cadence guidance. |
29
29
 
30
- ### Skills (22 workflow + 27 cabinet members)
30
+ ### Skills (22 workflow + 30 cabinet members)
31
31
 
32
32
  **Workflow Skills:**
33
33
 
@@ -0,0 +1,46 @@
1
+ #!/bin/bash
2
+ # PreToolUse hook on pib_complete_action
3
+ # Blocks completion unless verification breadcrumb exists.
4
+ #
5
+ # The execute skill writes breadcrumbs to .claude/verification/<fid>.json
6
+ # at two points:
7
+ # Step 1 (spec loaded): spec_read = true
8
+ # Step 7 (AC verified): ac_verified = true
9
+ #
10
+ # This hook checks both phases are recorded.
11
+ # Works whether you used /execute or worked ad-hoc.
12
+
13
+ INPUT="$CLAUDE_TOOL_INPUT"
14
+ FID=$(echo "$INPUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('fid',''))" 2>/dev/null)
15
+
16
+ if [ -z "$FID" ]; then
17
+ echo '{"decision":"allow"}'
18
+ exit 0
19
+ fi
20
+
21
+ VERIFY_DIR=".claude/verification"
22
+ BREADCRUMB="$VERIFY_DIR/$FID.json"
23
+
24
+ if [ ! -f "$BREADCRUMB" ]; then
25
+ NOW=$(date -u +%Y-%m-%dT%H:%M:%SZ)
26
+ cat <<EOF
27
+ {"decision":"block","reason":"No verification record for action $FID. Before completing:\n1. Read the full spec: use pib_get_action with fid $FID\n2. Create breadcrumb: mkdir -p $VERIFY_DIR && echo '{\"fid\":\"$FID\",\"spec_read\":true,\"spec_read_at\":\"$NOW\",\"ac_verified\":false}' > $BREADCRUMB\n3. Verify each AC against implementation\n4. Update breadcrumb with ac_verified:true and verification_summary\n5. Retry completion."}
28
+ EOF
29
+ exit 0
30
+ fi
31
+
32
+ # Check breadcrumb has both phases
33
+ SPEC_READ=$(python3 -c "import json; d=json.load(open('$BREADCRUMB')); print(d.get('spec_read',False))" 2>/dev/null)
34
+ AC_VERIFIED=$(python3 -c "import json; d=json.load(open('$BREADCRUMB')); print(d.get('ac_verified',False))" 2>/dev/null)
35
+
36
+ if [ "$SPEC_READ" != "True" ]; then
37
+ echo "{\"decision\":\"block\",\"reason\":\"Verification record exists but spec not read. Use pib_get_action to read full notes for $FID, then update $BREADCRUMB with spec_read: true.\"}"
38
+ exit 0
39
+ fi
40
+
41
+ if [ "$AC_VERIFIED" != "True" ]; then
42
+ echo "{\"decision\":\"block\",\"reason\":\"Spec was read but ACs not verified. Compare implementation against each acceptance criterion in the spec, then update $BREADCRUMB with ac_verified: true and a verification_summary.\"}"
43
+ exit 0
44
+ fi
45
+
46
+ echo '{"decision":"allow"}'
@@ -0,0 +1,50 @@
1
+ #!/bin/bash
2
+ # PreToolUse hook on pib_create_action
3
+ # Blocks action creation when notes fail quality checks.
4
+ #
5
+ # Quality criteria (from field feedback analysis):
6
+ # 1. Notes field is present and non-empty
7
+ # 2. Notes are not just a copy of the title (text field)
8
+ # 3. Notes are at least 100 characters (a meaningful paragraph)
9
+ # 4. Notes contain an acceptance criteria section
10
+ # 5. Notes contain a surface area section
11
+ #
12
+ # These fire on ALL pib_create_action calls — whether from /plan,
13
+ # /execute, or ad-hoc. The hook doesn't care how you got here.
14
+
15
+ INPUT="$CLAUDE_TOOL_INPUT"
16
+
17
+ NOTES=$(echo "$INPUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('notes',''))" 2>/dev/null)
18
+ TEXT=$(echo "$INPUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('text',''))" 2>/dev/null)
19
+
20
+ if [ -z "$NOTES" ]; then
21
+ echo '{"decision":"block","reason":"Action notes are empty. Every action needs notes with: implementation details, acceptance criteria (## AC or ## Acceptance Criteria), and surface area (## Surface Area with - files: entries). The bar: a cold-start developer reads ONLY these notes and can implement correctly."}'
22
+ exit 0
23
+ fi
24
+
25
+ # Check: notes are not just the title repeated
26
+ if [ "$NOTES" = "$TEXT" ]; then
27
+ echo '{"decision":"block","reason":"Action notes are identical to the title. Notes must contain implementation details, acceptance criteria, and surface area — not just a restated title."}'
28
+ exit 0
29
+ fi
30
+
31
+ # Check: minimum length (100 chars)
32
+ NOTE_LEN=${#NOTES}
33
+ if [ "$NOTE_LEN" -lt 100 ]; then
34
+ echo "{\"decision\":\"block\",\"reason\":\"Action notes are only ${NOTE_LEN} characters. Minimum is 100. Include: implementation approach, acceptance criteria (## Acceptance Criteria), and surface area (## Surface Area).\"}"
35
+ exit 0
36
+ fi
37
+
38
+ # Check: has acceptance criteria section
39
+ if ! echo "$NOTES" | grep -qiE '(## (AC|Acceptance|Criteria)|(\*\*AC|\*\*Acceptance)|- \[[ x]\])'; then
40
+ echo '{"decision":"block","reason":"Action notes have no acceptance criteria section. Add ## Acceptance Criteria containing testable pass/fail criteria."}'
41
+ exit 0
42
+ fi
43
+
44
+ # Check: has surface area section
45
+ if ! echo "$NOTES" | grep -qiE '(## Surface|files:|dirs:)'; then
46
+ echo '{"decision":"block","reason":"Action notes have no surface area section. Add ## Surface Area with - files: path/to/file entries listing files this action changes."}'
47
+ exit 0
48
+ fi
49
+
50
+ echo '{"decision":"allow"}'
@@ -0,0 +1,65 @@
1
+ #!/bin/bash
2
+ # PreToolUse prompt hook on Edit|Write
3
+ # Surfaces prevent-type memories BEFORE editing files in risky domains.
4
+ #
5
+ # Two-tier domain identification:
6
+ # 1. Static map: known high-risk file patterns → behavioral search terms
7
+ # 2. Dynamic fallback: omega query with file path context
8
+ #
9
+ # Projects extend the static map via phases/domain-memories.md
10
+
11
+ INPUT="$CLAUDE_TOOL_INPUT"
12
+ FILE_PATH=$(echo "$INPUT" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('file_path', d.get('path','')))" 2>/dev/null)
13
+
14
+ if [ -z "$FILE_PATH" ]; then
15
+ exit 0
16
+ fi
17
+
18
+ OMEGA_BIN="$HOME/.claude-cabinet/omega-venv/bin/omega"
19
+ if [ ! -x "$OMEGA_BIN" ]; then
20
+ exit 0
21
+ fi
22
+
23
+ # Tier 1: Static domain map — known high-risk patterns
24
+ SEARCH_TERM=""
25
+ case "$FILE_PATH" in
26
+ *playwright*|*puppeteer*|*selenium*|*cypress*|*webdriver*)
27
+ SEARCH_TERM="browser automation testing prevent mistakes constraints" ;;
28
+ *deploy*|*railway*|*docker*|*fly.toml*|*vercel*|*Dockerfile*)
29
+ SEARCH_TERM="deployment prevent mistakes constraints gotchas" ;;
30
+ *migration*|*schema*|*.sql*|*prisma*|*drizzle*)
31
+ SEARCH_TERM="database migration prevent mistakes constraints" ;;
32
+ *auth*|*session*|*token*|*credential*|*login*|*oauth*)
33
+ SEARCH_TERM="authentication security prevent mistakes constraints" ;;
34
+ *webhook*|*stripe*|*payment*|*billing*)
35
+ SEARCH_TERM="payment webhook prevent mistakes constraints" ;;
36
+ esac
37
+
38
+ # Check for project-specific domain extensions
39
+ PHASE_FILE=".claude/skills/hooks/phases/domain-memories.md"
40
+ if [ -f "$PHASE_FILE" ] && [ -z "$SEARCH_TERM" ]; then
41
+ # Phase file can define additional pattern|search terms (one per line)
42
+ while IFS='|' read -r pattern terms; do
43
+ [ -z "$pattern" ] && continue
44
+ if echo "$FILE_PATH" | grep -qE "$pattern"; then
45
+ SEARCH_TERM="$terms"
46
+ break
47
+ fi
48
+ done < "$PHASE_FILE"
49
+ fi
50
+
51
+ # Tier 2: Dynamic fallback — query omega with file context
52
+ if [ -z "$SEARCH_TERM" ]; then
53
+ BASENAME=$(basename "$FILE_PATH")
54
+ DIRNAME=$(basename "$(dirname "$FILE_PATH")")
55
+ SEARCH_TERM="prevent mistakes constraints when editing $DIRNAME $BASENAME"
56
+ fi
57
+
58
+ # Query omega for prevent-type memories
59
+ MEMORIES=$("$OMEGA_BIN" query "$SEARCH_TERM" --type constraint --type error_pattern --limit 3 2>/dev/null)
60
+
61
+ if [ -n "$MEMORIES" ] && [ "$MEMORIES" != "No results" ] && [ "$MEMORIES" != "[]" ]; then
62
+ echo "⚠ RELEVANT MEMORIES for $(basename "$FILE_PATH"):"
63
+ echo "$MEMORIES"
64
+ echo "---"
65
+ fi
@@ -0,0 +1,38 @@
1
+ #!/bin/bash
2
+ # PreToolUse hook on Bash tool
3
+ # Blocks raw SQL operations against work tracker tables.
4
+ # Consuming projects customize via phases/work-tracker-guard.md
5
+ #
6
+ # Default: guards pib-db actions table
7
+ # Disable: phase file with "skip: true"
8
+
9
+ INPUT="$CLAUDE_TOOL_INPUT"
10
+ COMMAND=$(echo "$INPUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('command',''))" 2>/dev/null)
11
+
12
+ if [ -z "$COMMAND" ]; then
13
+ echo '{"decision":"allow"}'
14
+ exit 0
15
+ fi
16
+
17
+ # Check for phase file override
18
+ PHASE_FILE=".claude/skills/hooks/phases/work-tracker-guard.md"
19
+ if [ -f "$PHASE_FILE" ]; then
20
+ FIRST_LINE=$(head -1 "$PHASE_FILE")
21
+ if [ "$FIRST_LINE" = "skip: true" ]; then
22
+ echo '{"decision":"allow"}'
23
+ exit 0
24
+ fi
25
+ fi
26
+
27
+ # Check for SQL operations against actions table
28
+ if echo "$COMMAND" | grep -qiE '(INSERT\s+INTO|UPDATE|DELETE\s+FROM)\s+actions'; then
29
+ # Override escape hatch
30
+ if echo "$COMMAND" | grep -q '\-\-force-raw-sql'; then
31
+ echo '{"decision":"allow","reason":"Raw SQL override acknowledged. Quality gates bypassed."}'
32
+ exit 0
33
+ fi
34
+ 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
+ exit 0
36
+ fi
37
+
38
+ echo '{"decision":"allow"}'
@@ -77,7 +77,11 @@ Implement the promoted rule at its target layer:
77
77
  `.claude/rules/` file
78
78
  - **Hook promotion:** Add to `.claude/settings.json` hooks section.
79
79
  Command hooks for deterministic checks, prompt hooks for semantic
80
- evaluation.
80
+ evaluation. **Tooling:** The `hookify` plugin (`claude plugins install
81
+ hookify`) can generate hooks from natural language — run `/hookify`
82
+ with a description or let it auto-generate rules from corrected
83
+ behaviors in the current session. Useful for rapid promotion without
84
+ hand-writing shell scripts.
81
85
  - **Structural encoding:** Modify API, schema, or validation to reject
82
86
  invalid states directly
83
87
 
@@ -0,0 +1,124 @@
1
+ #!/usr/bin/env python3
2
+ """Migrate flat .claude/memory/*.md files to omega semantic memory.
3
+
4
+ Usage: python3 migrate-memory-to-omega.py [--dry-run] [--memory-dir PATH]
5
+
6
+ Features:
7
+ - Detects memory type from filename/content heuristics
8
+ - Tags memories with source project name
9
+ - Checks for near-duplicates before storing (skips if similar exists)
10
+ - Renames migrated files to .md.migrated (idempotent)
11
+ """
12
+
13
+ import argparse, json, os, subprocess, sys
14
+ from pathlib import Path
15
+
16
+ OMEGA_BIN = os.path.expanduser("~/.claude-cabinet/omega-venv/bin/omega")
17
+
18
+ TYPE_HEURISTICS = {
19
+ "decision": ["decision", "chose", "decided", "went with", "picked"],
20
+ "lesson_learned": ["lesson", "learned", "mistake", "gotcha", "never again"],
21
+ "user_preference": ["prefer", "preference", "always", "never", "style"],
22
+ "constraint": ["constraint", "limitation", "can't", "must not", "blocked"],
23
+ "error_pattern": ["error", "bug", "broke", "failed", "crash"],
24
+ }
25
+
26
+ def detect_project():
27
+ """Get project name from package.json or directory name."""
28
+ try:
29
+ with open("package.json") as f:
30
+ pkg = json.load(f)
31
+ return pkg.get("name", Path.cwd().name)
32
+ except Exception:
33
+ return Path.cwd().name
34
+
35
+ def detect_type(filename, content):
36
+ text = (filename + " " + content).lower()
37
+ scores = {t: sum(1 for kw in kws if kw in text) for t, kws in TYPE_HEURISTICS.items()}
38
+ best = max(scores, key=scores.get)
39
+ return best if scores[best] > 0 else "decision"
40
+
41
+ def check_duplicate(content):
42
+ """Query omega for similar memories. Returns True if near-dup found."""
43
+ try:
44
+ query = content[:200].replace('"', '\\"').replace("\n", " ")
45
+ result = subprocess.run(
46
+ [OMEGA_BIN, "query", query, "--limit", "3"],
47
+ capture_output=True, text=True, timeout=15
48
+ )
49
+ if result.returncode != 0 or not result.stdout.strip():
50
+ return False
51
+ content_words = set(content.lower().split())
52
+ for line in result.stdout.strip().split("\n"):
53
+ line_words = set(line.lower().split())
54
+ if len(content_words & line_words) > 0.6 * min(len(content_words), len(line_words)):
55
+ return True
56
+ return False
57
+ except Exception:
58
+ return False
59
+
60
+ def migrate_file(filepath, project_name, dry_run=False):
61
+ content = filepath.read_text(encoding="utf-8").strip()
62
+ if not content:
63
+ return "empty", "Skipped — empty file"
64
+
65
+ mtype = detect_type(filepath.name, content)
66
+ tagged_content = f"[source: {project_name}] {content}"
67
+
68
+ if dry_run:
69
+ return "dry_run", f"Would store as {mtype} from {project_name}: {filepath.name}"
70
+
71
+ if check_duplicate(content):
72
+ return "duplicate", f"Near-duplicate found in omega — skipping: {filepath.name}"
73
+
74
+ try:
75
+ result = subprocess.run(
76
+ [OMEGA_BIN, "store", "--type", mtype, "--text", tagged_content],
77
+ capture_output=True, text=True, timeout=30
78
+ )
79
+ if result.returncode == 0:
80
+ migrated = filepath.with_suffix(".md.migrated")
81
+ filepath.rename(migrated)
82
+ return "migrated", f"Stored as {mtype}, renamed to {migrated.name}"
83
+ else:
84
+ return "error", f"omega store failed: {result.stderr.strip()}"
85
+ except Exception as e:
86
+ return "error", f"Exception: {e}"
87
+
88
+ def main():
89
+ parser = argparse.ArgumentParser(description="Migrate flat memory files to omega")
90
+ parser.add_argument("--dry-run", action="store_true", help="Show what would be migrated without doing it")
91
+ parser.add_argument("--memory-dir", default=".claude/memory", help="Path to memory directory")
92
+ args = parser.parse_args()
93
+
94
+ if not os.path.exists(OMEGA_BIN):
95
+ print(f"ERROR: Omega not found at {OMEGA_BIN}")
96
+ sys.exit(1)
97
+
98
+ memory_dir = Path(args.memory_dir)
99
+ if not memory_dir.exists():
100
+ print(f"No memory directory at {memory_dir}")
101
+ sys.exit(0)
102
+
103
+ # Skip MEMORY.md, patterns/, and already-migrated files
104
+ md_files = [f for f in sorted(memory_dir.glob("**/*.md"))
105
+ if f.name != "MEMORY.md" and "/patterns/" not in str(f)]
106
+ if not md_files:
107
+ print("No .md files to migrate.")
108
+ sys.exit(0)
109
+
110
+ project = detect_project()
111
+ print(f"Migrating {len(md_files)} files from project '{project}'")
112
+ if args.dry_run:
113
+ print("DRY RUN\n")
114
+
115
+ counts = {}
116
+ for f in md_files:
117
+ status, msg = migrate_file(f, project, args.dry_run)
118
+ counts[status] = counts.get(status, 0) + 1
119
+ print(f" [{status}] {f.name}: {msg}")
120
+
121
+ print(f"\nDone. {counts}")
122
+
123
+ if __name__ == "__main__":
124
+ main()
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Generic review server — serves the review UI and holds items/verdicts in memory.
3
+ *
4
+ * Claude POSTs items to review, user reviews in browser, Claude GETs verdicts.
5
+ * Works for audit findings, plan critique, plan actions, or any itemized list.
6
+ *
7
+ * Usage: node review-server.mjs [--port 3459]
8
+ *
9
+ * API:
10
+ * POST /api/session — Claude sends { title, items, verdictLabels?, groups? }
11
+ * GET /api/session — UI fetches current session
12
+ * POST /api/verdicts — UI submits verdicts
13
+ * GET /api/verdicts — Claude reads verdicts
14
+ */
15
+
16
+ import { createServer } from 'node:http';
17
+ import { readFile } from 'node:fs/promises';
18
+ import { fileURLToPath } from 'node:url';
19
+ import { dirname, join } from 'node:path';
20
+
21
+ const __dirname = dirname(fileURLToPath(import.meta.url));
22
+ const PORT = parseInt(process.argv.find((_, i, a) => a[i - 1] === '--port') || '3459');
23
+
24
+ let currentSession = null;
25
+ let currentVerdicts = null;
26
+
27
+ function json(res, data, status = 200) {
28
+ res.writeHead(status, {
29
+ 'Content-Type': 'application/json',
30
+ 'Access-Control-Allow-Origin': '*',
31
+ 'Access-Control-Allow-Headers': 'Content-Type',
32
+ 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
33
+ });
34
+ res.end(JSON.stringify(data));
35
+ }
36
+
37
+ const server = createServer(async (req, res) => {
38
+ const url = new URL(req.url, `http://localhost:${PORT}`);
39
+
40
+ if (req.method === 'OPTIONS') {
41
+ res.writeHead(204, {
42
+ 'Access-Control-Allow-Origin': '*',
43
+ 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
44
+ 'Access-Control-Allow-Headers': 'Content-Type',
45
+ });
46
+ return res.end();
47
+ }
48
+
49
+ // Serve HTML
50
+ if (req.method === 'GET' && url.pathname === '/') {
51
+ try {
52
+ const html = await readFile(join(__dirname, 'review-ui.html'), 'utf-8');
53
+ res.writeHead(200, { 'Content-Type': 'text/html' });
54
+ return res.end(html);
55
+ } catch (err) {
56
+ res.writeHead(500);
57
+ return res.end('Failed to read review-ui.html');
58
+ }
59
+ }
60
+
61
+ // POST /api/session — Claude sends items to review
62
+ if (req.method === 'POST' && url.pathname === '/api/session') {
63
+ let body = '';
64
+ for await (const chunk of req) body += chunk;
65
+ try {
66
+ currentSession = JSON.parse(body);
67
+ currentVerdicts = null;
68
+ console.log(`Review session: "${currentSession.title}" — ${currentSession.items?.length || 0} items`);
69
+ return json(res, { ok: true, count: currentSession.items?.length || 0 });
70
+ } catch (err) {
71
+ return json(res, { error: 'Invalid JSON' }, 400);
72
+ }
73
+ }
74
+
75
+ // GET /api/session — UI fetches current session
76
+ if (req.method === 'GET' && url.pathname === '/api/session') {
77
+ if (!currentSession) return json(res, { active: false });
78
+ return json(res, { active: true, ...currentSession });
79
+ }
80
+
81
+ // POST /api/verdicts — UI submits verdicts
82
+ if (req.method === 'POST' && url.pathname === '/api/verdicts') {
83
+ let body = '';
84
+ for await (const chunk of req) body += chunk;
85
+ try {
86
+ currentVerdicts = JSON.parse(body);
87
+ console.log(`Received ${currentVerdicts.reviewed}/${currentVerdicts.total} verdicts`);
88
+ return json(res, { ok: true });
89
+ } catch (err) {
90
+ return json(res, { error: 'Invalid JSON' }, 400);
91
+ }
92
+ }
93
+
94
+ // GET /api/verdicts — Claude reads verdicts
95
+ if (req.method === 'GET' && url.pathname === '/api/verdicts') {
96
+ if (!currentVerdicts) {
97
+ return json(res, { submitted: false, message: 'No verdicts submitted yet' });
98
+ }
99
+ return json(res, { submitted: true, ...currentVerdicts });
100
+ }
101
+
102
+ res.writeHead(404);
103
+ res.end('Not found');
104
+ });
105
+
106
+ server.listen(PORT, () => {
107
+ console.log(`Review server running at http://localhost:${PORT}`);
108
+ });