create-claude-cabinet 0.18.0 → 0.19.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 +4 -4
- package/lib/cli.js +3 -2
- package/lib/settings-merge.js +36 -0
- package/package.json +1 -1
- package/templates/README.md +1 -1
- package/templates/hooks/action-completion-gate.sh +46 -0
- package/templates/hooks/action-quality-gate.sh +50 -0
- package/templates/hooks/domain-memories.sh +65 -0
- package/templates/hooks/work-tracker-guard.sh +38 -0
- package/templates/scripts/migrate-memory-to-omega.py +124 -0
- package/templates/scripts/review-server.mjs +108 -0
- package/templates/scripts/review-ui.html +654 -0
- package/templates/skills/cabinet-cc-health/SKILL.md +9 -0
- package/templates/skills/cc-feedback/SKILL.md +0 -1
- package/templates/skills/debrief/SKILL.md +26 -0
- package/templates/skills/execute/SKILL.md +56 -0
- package/templates/skills/orient/SKILL.md +78 -11
- package/templates/skills/plan/SKILL.md +88 -0
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,
|
|
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** —
|
|
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
|
-
|
|
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-*/ #
|
|
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
|
|
package/lib/settings-merge.js
CHANGED
|
@@ -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
package/templates/README.md
CHANGED
|
@@ -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 +
|
|
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"}'
|
|
@@ -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
|
+
});
|