qualia-framework-v2 2.5.0 → 2.7.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.
Files changed (41) hide show
  1. package/README.md +14 -10
  2. package/agents/planner.md +8 -2
  3. package/agents/qa-browser.md +186 -0
  4. package/bin/install.js +52 -27
  5. package/bin/qualia-ui.js +278 -0
  6. package/hooks/auto-update.js +92 -0
  7. package/hooks/block-env-edit.js +30 -0
  8. package/hooks/branch-guard.js +47 -0
  9. package/hooks/migration-guard.js +60 -0
  10. package/hooks/pre-compact.js +32 -0
  11. package/hooks/pre-deploy-gate.js +110 -0
  12. package/hooks/pre-push.js +33 -0
  13. package/hooks/session-start.js +84 -0
  14. package/package.json +1 -1
  15. package/skills/qualia/SKILL.md +15 -11
  16. package/skills/qualia-build/SKILL.md +17 -16
  17. package/skills/qualia-debug/SKILL.md +14 -0
  18. package/skills/qualia-design/SKILL.md +4 -0
  19. package/skills/qualia-handoff/SKILL.md +5 -9
  20. package/skills/qualia-learn/SKILL.md +4 -0
  21. package/skills/qualia-new/SKILL.md +13 -14
  22. package/skills/qualia-pause/SKILL.md +4 -0
  23. package/skills/qualia-plan/SKILL.md +21 -20
  24. package/skills/qualia-polish/SKILL.md +15 -19
  25. package/skills/qualia-quick/SKILL.md +9 -0
  26. package/skills/qualia-report/SKILL.md +4 -0
  27. package/skills/qualia-resume/SKILL.md +11 -6
  28. package/skills/qualia-review/SKILL.md +4 -0
  29. package/skills/qualia-ship/SKILL.md +10 -13
  30. package/skills/qualia-skill-new/SKILL.md +148 -0
  31. package/skills/qualia-task/SKILL.md +11 -15
  32. package/skills/qualia-verify/SKILL.md +49 -20
  33. package/tests/hooks.test.sh +108 -44
  34. package/hooks/auto-update.sh +0 -56
  35. package/hooks/block-env-edit.sh +0 -11
  36. package/hooks/branch-guard.sh +0 -18
  37. package/hooks/migration-guard.sh +0 -43
  38. package/hooks/pre-compact.sh +0 -11
  39. package/hooks/pre-deploy-gate.sh +0 -50
  40. package/hooks/pre-push.sh +0 -28
  41. package/hooks/session-start.sh +0 -17
@@ -0,0 +1,148 @@
1
+ ---
2
+ name: qualia-skill-new
3
+ description: "Author a new Qualia skill or agent. Use when the user says 'create a new skill', 'add a skill', 'I want to build a skill', 'make this a reusable command', 'turn this into a skill'. Generates the SKILL.md, registers it in the right location, and optionally ships to the framework repo."
4
+ ---
5
+
6
+ # /qualia-skill-new — Author a New Skill
7
+
8
+ You are about to create a reusable slash command. Skills are the leverage of the Qualia framework — if the team does something twice, it probably belongs here.
9
+
10
+ ## Process
11
+
12
+ ```bash
13
+ node ~/.claude/bin/qualia-ui.js banner skill-new
14
+ ```
15
+
16
+ ### 1. Scope Decision
17
+
18
+ Ask the user with AskUserQuestion:
19
+
20
+ ```
21
+ question: "Where should this skill live?"
22
+ header: "Scope"
23
+ options:
24
+ - label: "Framework skill (ships to the team)"
25
+ description: "Edit qualia-framework-v2 repo. Everyone gets it on next update."
26
+ - label: "Local skill (just me)"
27
+ description: "Lives only in ~/.claude/skills/. Not shared."
28
+ - label: "Agent instead of a skill"
29
+ description: "This is a subagent role, not a slash command. Creates agents/{name}.md."
30
+ ```
31
+
32
+ **Framework** → target: `/home/qualia/Projects/qualia/qualia-framework-v2/skills/{name}/SKILL.md`
33
+ **Local** → target: `~/.claude/skills/{name}/SKILL.md`
34
+ **Agent** → target: `/home/qualia/Projects/qualia/qualia-framework-v2/agents/{name}.md` (framework) or `~/.claude/agents/{name}.md` (local)
35
+
36
+ ### 2. Gather Requirements
37
+
38
+ Ask the user — one question at a time, natural conversation:
39
+
40
+ 1. **"What's the name?"** — kebab-case, prefix with `qualia-` for framework skills. E.g., `qualia-seed-db`.
41
+ 2. **"What does it do?"** — one sentence, used as the description.
42
+ 3. **"How does the user invoke it?"** — trigger phrases they'd naturally say. E.g., "seed the database", "load test data", "populate dev db".
43
+ 4. **"Does it need planning / building / verification?"** — if yes, it probably should spawn an agent. If no, it's a direct-action skill.
44
+ 5. **"What files does it read or write?"** — tells you what tools to restrict to.
45
+
46
+ ### 3. Read Reference Skills
47
+
48
+ Before writing, read two existing skills that are structurally similar:
49
+
50
+ ```bash
51
+ # Short direct-action skill reference:
52
+ cat ~/.claude/skills/qualia-learn/SKILL.md
53
+
54
+ # Skill-that-spawns-an-agent reference:
55
+ cat ~/.claude/skills/qualia-plan/SKILL.md
56
+
57
+ # Interactive wizard reference:
58
+ cat ~/.claude/skills/qualia-new/SKILL.md
59
+ ```
60
+
61
+ Pick the closest pattern and copy its structure.
62
+
63
+ ### 4. Write the SKILL.md
64
+
65
+ Every SKILL.md MUST have:
66
+
67
+ ```markdown
68
+ ---
69
+ name: {kebab-case-name}
70
+ description: "{one sentence}. {trigger phrases}"
71
+ ---
72
+
73
+ # /{name} — {Human Title}
74
+
75
+ {one-paragraph explanation}
76
+
77
+ ## Usage
78
+
79
+ `/{name}` — {default behavior}
80
+ `/{name} {arg}` — {with argument}
81
+
82
+ ## Process
83
+
84
+ ### 1. {First Step}
85
+ {specifics}
86
+
87
+ ### 2. {Second Step}
88
+ {specifics}
89
+
90
+ ### N. Update State (only if this skill changes project state)
91
+
92
+ ```bash
93
+ node ~/.claude/bin/state.js transition --to {status} ...
94
+ ```
95
+ Do NOT manually edit STATE.md or tracking.json.
96
+ ```
97
+
98
+ **Description field rules:**
99
+ - MUST include trigger phrases the user would naturally say
100
+ - The Claude Code router matches user messages against descriptions — if you don't list triggers, the skill never fires
101
+ - Bad: `"Manages database seeding."` (no triggers)
102
+ - Good: `"Seed the database with test data. Trigger on 'seed db', 'load test data', 'populate dev'."`
103
+
104
+ ### 5. Test the Skill
105
+
106
+ Spawn a fresh subagent to simulate running the skill — does it make sense without the context you have right now?
107
+
108
+ ```
109
+ Agent(prompt="
110
+ Read this skill: @~/.claude/skills/{name}/SKILL.md
111
+
112
+ Pretend the user just said '{one of the trigger phrases}'. Walk through what you would do, step by step. Flag anything ambiguous or missing.
113
+ ", subagent_type="general-purpose", description="Test skill {name}")
114
+ ```
115
+
116
+ Fix any ambiguity the test agent found.
117
+
118
+ ### 6. Install (if framework skill)
119
+
120
+ ```bash
121
+ # Framework skill — copy to local .claude for immediate testing
122
+ cp /home/qualia/Projects/qualia/qualia-framework-v2/skills/{name}/SKILL.md ~/.claude/skills/{name}/SKILL.md
123
+
124
+ # Verify it parses
125
+ node -e "const fs=require('fs');const c=fs.readFileSync('/home/qualia/.claude/skills/{name}/SKILL.md','utf8');if(!c.includes('---'))throw new Error('missing frontmatter');if(!c.match(/^name:\s*\S/m))throw new Error('missing name');if(!c.match(/^description:\s*\S/m))throw new Error('missing description');console.log('OK')"
126
+ ```
127
+
128
+ ### 7. Commit (framework skills only)
129
+
130
+ Do NOT commit unless the user explicitly says "commit" or "ship it".
131
+
132
+ When they do:
133
+ ```bash
134
+ cd /home/qualia/Projects/qualia/qualia-framework-v2
135
+ git add skills/{name}/
136
+ git commit -m "feat: add /{name} skill"
137
+ ```
138
+
139
+ Remind the user to run `npx qualia-framework-v2 update` on their other machines, or bump the version and `npm publish`.
140
+
141
+ ## Anti-Patterns
142
+
143
+ - ❌ **Description without triggers** — the skill won't fire
144
+ - ❌ **Multiple commands in one skill** — split into two skills
145
+ - ❌ **Direct file writes instead of state.js** — always use state.js for STATE.md/tracking.json
146
+ - ❌ **Hardcoded project paths** — use `.planning/` relative or `~/.claude/` absolute, never `/home/specific-user/`
147
+ - ❌ **Skills that spawn agents without passing PROJECT.md and STATE.md context** — agents are blind by default
148
+ - ❌ **Skills longer than ~150 lines** — split or move logic to an agent
@@ -43,13 +43,11 @@ If "Large" — suggest `/qualia-plan` instead. Ask if they want to proceed anywa
43
43
 
44
44
  Write a quick task spec (don't save to file, just confirm with user):
45
45
 
46
- ```
47
- QUALIA TASK
48
- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
49
-
50
- What: {what to build}
51
- Files: {files to create/modify}
52
- Done: {what "done" looks like}
46
+ ```bash
47
+ node ~/.claude/bin/qualia-ui.js banner task
48
+ node ~/.claude/bin/qualia-ui.js info "What: {what to build}"
49
+ node ~/.claude/bin/qualia-ui.js info "Files: {files to create/modify}"
50
+ node ~/.claude/bin/qualia-ui.js info "Done: {what done looks like}"
53
51
  ```
54
52
 
55
53
  Ask: **"Good to build?"**
@@ -79,14 +77,12 @@ After the builder finishes:
79
77
 
80
78
  ### 5. Report
81
79
 
82
- ```
83
- QUALIA ► TASK COMPLETE
84
- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
85
-
86
- Task {description}
87
- Files {files changed}
88
- Commit {commit hash}
89
- Status ✓ Done
80
+ ```bash
81
+ node ~/.claude/bin/qualia-ui.js divider
82
+ node ~/.claude/bin/qualia-ui.js ok "Task: {description}"
83
+ node ~/.claude/bin/qualia-ui.js ok "Files: {files changed}"
84
+ node ~/.claude/bin/qualia-ui.js ok "Commit: {commit hash}"
85
+ node ~/.claude/bin/qualia-ui.js end "TASK COMPLETE"
90
86
  ```
91
87
 
92
88
  ```bash
@@ -24,10 +24,9 @@ cat .planning/phase-{N}-verification.md 2>/dev/null || echo "NONE"
24
24
 
25
25
  ### 2. Spawn Verifier (Fresh Context)
26
26
 
27
- ```
28
- QUALIA VERIFYING Phase {N}
29
- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
30
- Spawning verifier...
27
+ ```bash
28
+ node ~/.claude/bin/qualia-ui.js banner verify {N} "{phase name}"
29
+ node ~/.claude/bin/qualia-ui.js spawn verifier "Goal-backward check..."
31
30
  ```
32
31
 
33
32
  ```
@@ -44,33 +43,55 @@ Verify this phase. Write report to .planning/phase-{N}-verification.md
44
43
  ", subagent_type="qualia-verifier", description="Verify phase {N}")
45
44
  ```
46
45
 
47
- ### 3. Present Results
46
+ ### 2b. Browser QA (if phase touched frontend)
48
47
 
49
- Read the verification report. Present:
48
+ If the phase plan's Files section includes any `.tsx`, `.jsx`, `.css`, `.scss`, or `app/`/`pages/`/`components/` paths, ALSO spawn the browser QA agent in parallel:
49
+
50
+ ```bash
51
+ # Detect frontend touch
52
+ grep -l "\.tsx\|\.jsx\|\.css\|app/\|components/\|pages/" .planning/phase-{N}-plan.md && FRONTEND=true
53
+ ```
54
+
55
+ If frontend:
50
56
 
51
- **If PASS:**
52
57
  ```
53
- ◆ QUALIA ► Phase {N} VERIFIED ✓
54
- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
58
+ Agent(prompt="
59
+ Read your role: @agents/qa-browser.md
55
60
 
56
- All {count} criteria passed.
61
+ Phase plan: @.planning/phase-{N}-plan.md
62
+ Existing verification: @.planning/phase-{N}-verification.md
57
63
 
58
- Run: /qualia-plan {N+1}
64
+ Drive the running dev server and test the routes this phase touched. Append a '## Browser QA' section to the verification file.
65
+ ", subagent_type="qualia-qa-browser", description="Browser QA phase {N}")
59
66
  ```
60
67
 
61
- **If FAIL:**
68
+ Wait for both the main verifier and the QA browser agent before moving to step 3. If Playwright MCP is unavailable, the QA browser agent returns BLOCKED — that's not a phase failure, just a note in the report.
69
+
70
+ ### 3. Present Results
71
+
72
+ Read the verification report. Present:
73
+
74
+ **If PASS:**
75
+ ```bash
76
+ node ~/.claude/bin/qualia-ui.js ok "All {count} criteria passed"
77
+ node ~/.claude/bin/qualia-ui.js end "PHASE {N} VERIFIED" "/qualia-plan {N+1}"
62
78
  ```
63
- QUALIA Phase {N} GAPS FOUND
64
- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
79
+ (If phase == total phases, use `/qualia-polish` as the next command.)
65
80
 
66
- Passed: {pass_count}
67
- Failed: {fail_count}
81
+ **If FAIL:**
82
+ ```bash
83
+ node ~/.claude/bin/qualia-ui.js ok "Passed: {pass_count}"
84
+ node ~/.claude/bin/qualia-ui.js fail "Failed: {fail_count}"
85
+ ```
68
86
 
69
- Gaps:
70
- ✗ {gap 1 — specific description}
71
- {gap 2 — specific description}
87
+ Then for each gap:
88
+ ```bash
89
+ node ~/.claude/bin/qualia-ui.js fail "{gap description}"
90
+ ```
72
91
 
73
- → Run: /qualia-plan {N} --gaps
92
+ End:
93
+ ```bash
94
+ node ~/.claude/bin/qualia-ui.js end "PHASE {N} GAPS FOUND" "/qualia-plan {N} --gaps"
74
95
  ```
75
96
 
76
97
  ### 4. Update State
@@ -82,3 +103,11 @@ If PASS and more phases: state.js auto-advances to the next phase.
82
103
  If FAIL and gap_cycles >= 2: state.js returns GAP_CYCLE_LIMIT — tell the employee to escalate.
83
104
  If FAIL and gap_cycles < 2: proceed to `/qualia-plan {N} --gaps`.
84
105
  Do NOT manually edit STATE.md or tracking.json — state.js handles both.
106
+
107
+ ### 5. Passive Knowledge Capture (on FAIL)
108
+
109
+ When verification fails, after showing the gaps, ask the user:
110
+
111
+ > *"Was any of this a recurring issue worth saving to common-fixes.md? (yes / no / which ones)"*
112
+
113
+ If yes, for each flagged gap spawn a brief `/qualia-learn` flow with type=`fix` — the gap title and fix direction from the verification report become the entry. Do NOT save every failure automatically — only the ones the user flags. The point is to build a real knowledge base, not a log of every mistake.
@@ -1,10 +1,12 @@
1
1
  #!/bin/bash
2
- # Qualia Framework v2 — Hook Tests
2
+ # Qualia Framework v2 — Hook Tests (cross-platform Node.js hooks)
3
3
  # Run: bash tests/hooks.test.sh
4
4
 
5
5
  PASS=0
6
6
  FAIL=0
7
- HOOKS_DIR="$(dirname "$0")/../hooks"
7
+ # Resolve HOOKS_DIR to an ABSOLUTE path so `cd` inside subshells doesn't break it.
8
+ HOOKS_DIR="$(cd "$(dirname "$0")/../hooks" && pwd)"
9
+ NODE="${NODE:-node}"
8
10
 
9
11
  assert_exit() {
10
12
  local name="$1" expected="$2" actual="$3"
@@ -17,127 +19,189 @@ assert_exit() {
17
19
  fi
18
20
  }
19
21
 
20
- echo "=== Hook Tests ==="
22
+ echo "=== Hook Tests (Node.js) ==="
21
23
  echo ""
22
24
 
23
- # --- block-env-edit.sh ---
25
+ # --- All hooks are syntactically valid Node.js ---
26
+ echo "syntax:"
27
+ for f in "$HOOKS_DIR"/*.js; do
28
+ if $NODE -c "$f" 2>/dev/null; then
29
+ echo " ✓ $(basename "$f")"
30
+ PASS=$((PASS + 1))
31
+ else
32
+ echo " ✗ $(basename "$f")"
33
+ FAIL=$((FAIL + 1))
34
+ fi
35
+ done
36
+
37
+ # --- block-env-edit.js ---
38
+ echo ""
24
39
  echo "block-env-edit:"
25
40
 
26
- echo '{"tool_input":{"file_path":".env.local"}}' | bash "$HOOKS_DIR/block-env-edit.sh" > /dev/null 2>&1
41
+ echo '{"tool_input":{"file_path":".env.local"}}' | $NODE "$HOOKS_DIR/block-env-edit.js" > /dev/null 2>&1
27
42
  assert_exit "blocks .env.local" 2 $?
28
43
 
29
- echo '{"tool_input":{"file_path":".env.production"}}' | bash "$HOOKS_DIR/block-env-edit.sh" > /dev/null 2>&1
44
+ echo '{"tool_input":{"file_path":".env.production"}}' | $NODE "$HOOKS_DIR/block-env-edit.js" > /dev/null 2>&1
30
45
  assert_exit "blocks .env.production" 2 $?
31
46
 
32
- echo '{"tool_input":{"file_path":".env"}}' | bash "$HOOKS_DIR/block-env-edit.sh" > /dev/null 2>&1
47
+ echo '{"tool_input":{"file_path":".env"}}' | $NODE "$HOOKS_DIR/block-env-edit.js" > /dev/null 2>&1
33
48
  assert_exit "blocks .env" 2 $?
34
49
 
35
- echo '{"tool_input":{"file_path":"src/app.tsx"}}' | bash "$HOOKS_DIR/block-env-edit.sh" > /dev/null 2>&1
50
+ # Windows-style path with backslashes (normalized by the hook)
51
+ echo '{"tool_input":{"file_path":"C:\\project\\.env.local"}}' | $NODE "$HOOKS_DIR/block-env-edit.js" > /dev/null 2>&1
52
+ assert_exit "blocks windows .env.local" 2 $?
53
+
54
+ echo '{"tool_input":{"file_path":"src/app.tsx"}}' | $NODE "$HOOKS_DIR/block-env-edit.js" > /dev/null 2>&1
36
55
  assert_exit "allows src/app.tsx" 0 $?
37
56
 
38
- echo '{"tool_input":{"file_path":"components/Footer.tsx"}}' | bash "$HOOKS_DIR/block-env-edit.sh" > /dev/null 2>&1
57
+ echo '{"tool_input":{"file_path":"components/Footer.tsx"}}' | $NODE "$HOOKS_DIR/block-env-edit.js" > /dev/null 2>&1
39
58
  assert_exit "allows components/Footer.tsx" 0 $?
40
59
 
41
- # --- migration-guard.sh ---
60
+ # --- migration-guard.js ---
42
61
  echo ""
43
62
  echo "migration-guard:"
44
63
 
45
- echo '{"tool_input":{"file_path":"migrations/001.sql","content":"DROP TABLE users;"}}' | bash "$HOOKS_DIR/migration-guard.sh" > /dev/null 2>&1
64
+ echo '{"tool_input":{"file_path":"migrations/001.sql","content":"DROP TABLE users;"}}' | $NODE "$HOOKS_DIR/migration-guard.js" > /dev/null 2>&1
46
65
  assert_exit "blocks DROP TABLE without IF EXISTS" 2 $?
47
66
 
48
- echo '{"tool_input":{"file_path":"migrations/001.sql","content":"DROP TABLE IF EXISTS old_users;"}}' | bash "$HOOKS_DIR/migration-guard.sh" > /dev/null 2>&1
67
+ echo '{"tool_input":{"file_path":"migrations/001.sql","content":"DROP TABLE IF EXISTS old_users;"}}' | $NODE "$HOOKS_DIR/migration-guard.js" > /dev/null 2>&1
49
68
  assert_exit "allows DROP TABLE IF EXISTS" 0 $?
50
69
 
51
- echo '{"tool_input":{"file_path":"migrations/002.sql","content":"DELETE FROM users;"}}' | bash "$HOOKS_DIR/migration-guard.sh" > /dev/null 2>&1
70
+ echo '{"tool_input":{"file_path":"migrations/002.sql","content":"DELETE FROM users;"}}' | $NODE "$HOOKS_DIR/migration-guard.js" > /dev/null 2>&1
52
71
  assert_exit "blocks DELETE without WHERE" 2 $?
53
72
 
54
- echo '{"tool_input":{"file_path":"migrations/003.sql","content":"TRUNCATE TABLE sessions;"}}' | bash "$HOOKS_DIR/migration-guard.sh" > /dev/null 2>&1
73
+ echo '{"tool_input":{"file_path":"migrations/003.sql","content":"TRUNCATE TABLE sessions;"}}' | $NODE "$HOOKS_DIR/migration-guard.js" > /dev/null 2>&1
55
74
  assert_exit "blocks TRUNCATE" 2 $?
56
75
 
57
- echo '{"tool_input":{"file_path":"migrations/004.sql","content":"CREATE TABLE users (id uuid);"}}' | bash "$HOOKS_DIR/migration-guard.sh" > /dev/null 2>&1
76
+ echo '{"tool_input":{"file_path":"migrations/004.sql","content":"CREATE TABLE users (id uuid);"}}' | $NODE "$HOOKS_DIR/migration-guard.js" > /dev/null 2>&1
58
77
  assert_exit "blocks CREATE TABLE without RLS" 2 $?
59
78
 
60
- echo '{"tool_input":{"file_path":"migrations/005.sql","content":"ALTER TABLE users ADD COLUMN email text;"}}' | bash "$HOOKS_DIR/migration-guard.sh" > /dev/null 2>&1
79
+ echo '{"tool_input":{"file_path":"migrations/005.sql","content":"ALTER TABLE users ADD COLUMN email text;"}}' | $NODE "$HOOKS_DIR/migration-guard.js" > /dev/null 2>&1
61
80
  assert_exit "allows safe ALTER TABLE" 0 $?
62
81
 
63
- echo '{"tool_input":{"file_path":"src/app.tsx","content":"DROP TABLE users;"}}' | bash "$HOOKS_DIR/migration-guard.sh" > /dev/null 2>&1
82
+ echo '{"tool_input":{"file_path":"src/app.tsx","content":"DROP TABLE users;"}}' | $NODE "$HOOKS_DIR/migration-guard.js" > /dev/null 2>&1
64
83
  assert_exit "skips non-migration files" 0 $?
65
84
 
66
- # --- branch-guard.sh ---
85
+ # --- branch-guard.js (grep-based — full run needs git + real config) ---
67
86
  echo ""
68
87
  echo "branch-guard:"
69
88
 
70
- if [ -f "$HOOKS_DIR/branch-guard.sh" ]; then
71
- echo " ✓ branch-guard.sh exists"
89
+ if grep -q '.qualia-config.json' "$HOOKS_DIR/branch-guard.js"; then
90
+ echo " ✓ reads role from .qualia-config.json"
72
91
  PASS=$((PASS + 1))
73
92
  else
74
- echo " ✗ branch-guard.sh missing"
93
+ echo " ✗ not reading from .qualia-config.json"
75
94
  FAIL=$((FAIL + 1))
76
95
  fi
77
96
 
78
- if grep -q 'ROLE' "$HOOKS_DIR/branch-guard.sh"; then
79
- echo " ✓ checks ROLE variable"
97
+ if grep -q 'branch --show-current' "$HOOKS_DIR/branch-guard.js"; then
98
+ echo " ✓ checks current git branch"
80
99
  PASS=$((PASS + 1))
81
100
  else
82
- echo " ✗ missing ROLE check"
101
+ echo " ✗ missing branch check"
83
102
  FAIL=$((FAIL + 1))
84
103
  fi
85
104
 
86
- if grep -q 'z "$ROLE"' "$HOOKS_DIR/branch-guard.sh"; then
87
- echo " ✓ defaults to deny on missing ROLE"
105
+ if grep -q 'OWNER' "$HOOKS_DIR/branch-guard.js"; then
106
+ echo " ✓ enforces OWNER role"
88
107
  PASS=$((PASS + 1))
89
108
  else
90
- echo " ✗ missing empty-ROLE deny"
109
+ echo " ✗ missing OWNER check"
91
110
  FAIL=$((FAIL + 1))
92
111
  fi
93
112
 
94
- # --- pre-push.sh ---
113
+ # --- pre-push.js ---
95
114
  echo ""
96
115
  echo "pre-push:"
97
116
 
98
- if grep -q 'command -v python3' "$HOOKS_DIR/pre-push.sh"; then
99
- echo " ✓ checks for python3 availability"
117
+ if grep -q 'tracking.json' "$HOOKS_DIR/pre-push.js"; then
118
+ echo " ✓ updates tracking.json"
100
119
  PASS=$((PASS + 1))
101
120
  else
102
- echo " ✗ missing python3 check"
121
+ echo " ✗ missing tracking.json update"
103
122
  FAIL=$((FAIL + 1))
104
123
  fi
105
124
 
106
- if grep -q 'qualia-push-err' "$HOOKS_DIR/pre-push.sh"; then
107
- echo " ✓ captures python3 errors"
125
+ if grep -q 'last_commit' "$HOOKS_DIR/pre-push.js"; then
126
+ echo " ✓ stamps last_commit"
108
127
  PASS=$((PASS + 1))
109
128
  else
110
- echo " ✗ missing python3 error capture"
129
+ echo " ✗ missing last_commit stamp"
111
130
  FAIL=$((FAIL + 1))
112
131
  fi
113
132
 
114
- # --- pre-deploy-gate.sh ---
133
+ # Run pre-push.js in a dir with no tracking.json — must exit 0 cleanly
134
+ TMP=$(mktemp -d)
135
+ (cd "$TMP" && $NODE "$HOOKS_DIR/pre-push.js" >/dev/null 2>&1)
136
+ assert_exit "exits 0 with no tracking.json" 0 $?
137
+ rm -rf "$TMP"
138
+
139
+ # --- pre-deploy-gate.js ---
115
140
  echo ""
116
141
  echo "pre-deploy-gate:"
117
142
 
118
- if [ -f "$HOOKS_DIR/pre-deploy-gate.sh" ]; then
119
- echo " ✓ pre-deploy-gate.sh exists"
143
+ if grep -q 'tsc' "$HOOKS_DIR/pre-deploy-gate.js"; then
144
+ echo " ✓ runs TypeScript check"
120
145
  PASS=$((PASS + 1))
121
146
  else
122
- echo " ✗ pre-deploy-gate.sh missing"
147
+ echo " ✗ missing TypeScript check"
123
148
  FAIL=$((FAIL + 1))
124
149
  fi
125
150
 
126
- if grep -q 'tsc --noEmit' "$HOOKS_DIR/pre-deploy-gate.sh"; then
127
- echo " ✓ runs TypeScript check"
151
+ if grep -q 'service_role' "$HOOKS_DIR/pre-deploy-gate.js"; then
152
+ echo " ✓ checks for service_role leaks"
128
153
  PASS=$((PASS + 1))
129
154
  else
130
- echo " ✗ missing TypeScript check"
155
+ echo " ✗ missing service_role check"
131
156
  FAIL=$((FAIL + 1))
132
157
  fi
133
158
 
134
- if grep -q 'service_role' "$HOOKS_DIR/pre-deploy-gate.sh"; then
135
- echo " ✓ checks for service_role leaks"
159
+ # --- session-start.js must exit 0 always ---
160
+ echo ""
161
+ echo "session-start:"
162
+
163
+ TMP=$(mktemp -d)
164
+ (cd "$TMP" && $NODE "$HOOKS_DIR/session-start.js" >/dev/null 2>&1)
165
+ assert_exit "exits 0 with no project" 0 $?
166
+
167
+ # Simulate a project with STATE.md
168
+ mkdir -p "$TMP/.planning"
169
+ cat > "$TMP/.planning/STATE.md" <<'EOF'
170
+ # Project State
171
+ Phase: 1 of 3 — Foundation
172
+ Status: setup
173
+ EOF
174
+ (cd "$TMP" && $NODE "$HOOKS_DIR/session-start.js" >/dev/null 2>&1)
175
+ assert_exit "exits 0 with STATE.md" 0 $?
176
+ rm -rf "$TMP"
177
+
178
+ # --- pre-compact.js ---
179
+ echo ""
180
+ echo "pre-compact:"
181
+
182
+ TMP=$(mktemp -d)
183
+ (cd "$TMP" && $NODE "$HOOKS_DIR/pre-compact.js" >/dev/null 2>&1)
184
+ assert_exit "exits 0 with no STATE.md" 0 $?
185
+ rm -rf "$TMP"
186
+
187
+ # --- auto-update.js ---
188
+ echo ""
189
+ echo "auto-update:"
190
+
191
+ TMP=$(mktemp -d)
192
+ mkdir -p "$TMP/.claude"
193
+ echo '{"code":"QS-FAWZI-01","version":"99.99.99"}' > "$TMP/.claude/.qualia-config.json"
194
+ HOME="$TMP" $NODE "$HOOKS_DIR/auto-update.js" >/dev/null 2>&1
195
+ assert_exit "exits 0 (fast path)" 0 $?
196
+ # Should now have cache file
197
+ if [ -f "$TMP/.claude/.qualia-last-update-check" ]; then
198
+ echo " ✓ writes cache timestamp"
136
199
  PASS=$((PASS + 1))
137
200
  else
138
- echo " ✗ missing service_role check"
201
+ echo " ✗ missing cache timestamp"
139
202
  FAIL=$((FAIL + 1))
140
203
  fi
204
+ rm -rf "$TMP"
141
205
 
142
206
  echo ""
143
207
  echo "=== Results: $PASS passed, $FAIL failed ==="
@@ -1,56 +0,0 @@
1
- #!/bin/bash
2
- # Qualia auto-update — checks once per day, updates silently in background
3
- # Runs as PreToolUse hook. Cached so it's a no-op most of the time.
4
-
5
- CLAUDE_DIR="$HOME/.claude"
6
- CACHE_FILE="$CLAUDE_DIR/.qualia-last-update-check"
7
- CONFIG_FILE="$CLAUDE_DIR/.qualia-config.json"
8
- LOCK_FILE="$CLAUDE_DIR/.qualia-updating"
9
- MAX_AGE=86400 # 24 hours in seconds
10
-
11
- # Exit fast if recently checked (most common path — single stat call)
12
- if [ -f "$CACHE_FILE" ]; then
13
- LAST_CHECK=$(cat "$CACHE_FILE" 2>/dev/null || echo 0)
14
- NOW=$(date +%s)
15
- AGE=$((NOW - LAST_CHECK))
16
- if [ "$AGE" -lt "$MAX_AGE" ]; then
17
- exit 0
18
- fi
19
- fi
20
-
21
- # Exit if already updating
22
- [ -f "$LOCK_FILE" ] && exit 0
23
-
24
- # Update cache timestamp immediately (prevents concurrent checks)
25
- date +%s > "$CACHE_FILE"
26
-
27
- # Run the actual check + update in background so we don't block the user
28
- (
29
- trap 'rm -f "$LOCK_FILE"' EXIT
30
- touch "$LOCK_FILE"
31
-
32
- # Get installed version
33
- INSTALLED=$(node -e "try{console.log(JSON.parse(require('fs').readFileSync('$CONFIG_FILE','utf8')).version)}catch{console.log('0.0.0')}" 2>/dev/null)
34
- [ -z "$INSTALLED" ] && INSTALLED="0.0.0"
35
-
36
- # Get latest from npm (5s timeout)
37
- LATEST=$(npm view qualia-framework-v2 version 2>/dev/null)
38
- [ -z "$LATEST" ] && exit 0
39
-
40
- # Compare versions
41
- NEEDS_UPDATE=$(node -e "
42
- const a='$LATEST'.split('.').map(Number), b='$INSTALLED'.split('.').map(Number);
43
- for(let i=0;i<3;i++){if(a[i]>b[i]){console.log('yes');process.exit()}if(a[i]<b[i]){process.exit()}}
44
- " 2>/dev/null)
45
-
46
- if [ "$NEEDS_UPDATE" = "yes" ]; then
47
- # Get saved install code
48
- CODE=$(node -e "try{console.log(JSON.parse(require('fs').readFileSync('$CONFIG_FILE','utf8')).code)}catch{}" 2>/dev/null)
49
- [ -z "$CODE" ] && exit 0
50
-
51
- # Run silent update
52
- npx qualia-framework-v2@latest install <<< "$CODE" > /dev/null 2>&1
53
- fi
54
- ) &
55
-
56
- exit 0
@@ -1,11 +0,0 @@
1
- #!/bin/bash
2
- # Prevent Claude from editing .env files
3
- # Claude Code hooks receive JSON on stdin with tool_input.file_path
4
-
5
- INPUT=$(cat)
6
- FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // .tool_input.command // ""' 2>/dev/null)
7
-
8
- if [[ "$FILE" == *.env* ]] || [[ "$FILE" == *".env.local"* ]] || [[ "$FILE" == *".env.production"* ]]; then
9
- echo "BLOCKED: Cannot edit environment files. Ask Fawzi to update secrets."
10
- exit 2
11
- fi
@@ -1,18 +0,0 @@
1
- #!/bin/bash
2
- # Block non-OWNER push to main/master
3
-
4
- BRANCH=$(git branch --show-current 2>/dev/null)
5
- ROLE=$(grep -m1 "^## Role:" ~/.claude/CLAUDE.md 2>/dev/null | sed 's/^## Role: *//')
6
-
7
- if [ -z "$ROLE" ]; then
8
- echo "BLOCKED: Cannot determine role — ~/.claude/CLAUDE.md missing or malformed. Defaulting to deny."
9
- exit 1
10
- fi
11
-
12
- if [[ "$BRANCH" == "main" || "$BRANCH" == "master" ]]; then
13
- if [[ "$ROLE" != "OWNER" ]]; then
14
- echo "BLOCKED: Employees cannot push to $BRANCH. Create a feature branch first."
15
- echo "Run: git checkout -b feature/your-feature-name"
16
- exit 1
17
- fi
18
- fi