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.
- package/README.md +14 -10
- package/agents/planner.md +8 -2
- package/agents/qa-browser.md +186 -0
- package/bin/install.js +52 -27
- package/bin/qualia-ui.js +278 -0
- package/hooks/auto-update.js +92 -0
- package/hooks/block-env-edit.js +30 -0
- package/hooks/branch-guard.js +47 -0
- package/hooks/migration-guard.js +60 -0
- package/hooks/pre-compact.js +32 -0
- package/hooks/pre-deploy-gate.js +110 -0
- package/hooks/pre-push.js +33 -0
- package/hooks/session-start.js +84 -0
- package/package.json +1 -1
- package/skills/qualia/SKILL.md +15 -11
- package/skills/qualia-build/SKILL.md +17 -16
- package/skills/qualia-debug/SKILL.md +14 -0
- package/skills/qualia-design/SKILL.md +4 -0
- package/skills/qualia-handoff/SKILL.md +5 -9
- package/skills/qualia-learn/SKILL.md +4 -0
- package/skills/qualia-new/SKILL.md +13 -14
- package/skills/qualia-pause/SKILL.md +4 -0
- package/skills/qualia-plan/SKILL.md +21 -20
- package/skills/qualia-polish/SKILL.md +15 -19
- package/skills/qualia-quick/SKILL.md +9 -0
- package/skills/qualia-report/SKILL.md +4 -0
- package/skills/qualia-resume/SKILL.md +11 -6
- package/skills/qualia-review/SKILL.md +4 -0
- package/skills/qualia-ship/SKILL.md +10 -13
- package/skills/qualia-skill-new/SKILL.md +148 -0
- package/skills/qualia-task/SKILL.md +11 -15
- package/skills/qualia-verify/SKILL.md +49 -20
- package/tests/hooks.test.sh +108 -44
- package/hooks/auto-update.sh +0 -56
- package/hooks/block-env-edit.sh +0 -11
- package/hooks/branch-guard.sh +0 -18
- package/hooks/migration-guard.sh +0 -43
- package/hooks/pre-compact.sh +0 -11
- package/hooks/pre-deploy-gate.sh +0 -50
- package/hooks/pre-push.sh +0 -28
- 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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
46
|
+
### 2b. Browser QA (if phase touched frontend)
|
|
48
47
|
|
|
49
|
-
|
|
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
|
-
|
|
54
|
-
|
|
58
|
+
Agent(prompt="
|
|
59
|
+
Read your role: @agents/qa-browser.md
|
|
55
60
|
|
|
56
|
-
|
|
61
|
+
Phase plan: @.planning/phase-{N}-plan.md
|
|
62
|
+
Existing verification: @.planning/phase-{N}-verification.md
|
|
57
63
|
|
|
58
|
-
|
|
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
|
-
|
|
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
|
-
|
|
64
|
-
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
79
|
+
(If phase == total phases, use `/qualia-polish` as the next command.)
|
|
65
80
|
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
87
|
+
Then for each gap:
|
|
88
|
+
```bash
|
|
89
|
+
node ~/.claude/bin/qualia-ui.js fail "{gap description}"
|
|
90
|
+
```
|
|
72
91
|
|
|
73
|
-
|
|
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.
|
package/tests/hooks.test.sh
CHANGED
|
@@ -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
|
|
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
|
-
# ---
|
|
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"}}' |
|
|
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"}}' |
|
|
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"}}' |
|
|
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
|
-
|
|
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"}}' |
|
|
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.
|
|
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;"}}' |
|
|
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;"}}' |
|
|
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;"}}' |
|
|
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;"}}' |
|
|
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);"}}' |
|
|
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;"}}' |
|
|
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;"}}' |
|
|
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.
|
|
85
|
+
# --- branch-guard.js (grep-based — full run needs git + real config) ---
|
|
67
86
|
echo ""
|
|
68
87
|
echo "branch-guard:"
|
|
69
88
|
|
|
70
|
-
if
|
|
71
|
-
echo " ✓
|
|
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 " ✗
|
|
93
|
+
echo " ✗ not reading from .qualia-config.json"
|
|
75
94
|
FAIL=$((FAIL + 1))
|
|
76
95
|
fi
|
|
77
96
|
|
|
78
|
-
if grep -q '
|
|
79
|
-
echo " ✓ checks
|
|
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
|
|
101
|
+
echo " ✗ missing branch check"
|
|
83
102
|
FAIL=$((FAIL + 1))
|
|
84
103
|
fi
|
|
85
104
|
|
|
86
|
-
if grep -q '
|
|
87
|
-
echo " ✓
|
|
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
|
|
109
|
+
echo " ✗ missing OWNER check"
|
|
91
110
|
FAIL=$((FAIL + 1))
|
|
92
111
|
fi
|
|
93
112
|
|
|
94
|
-
# --- pre-push.
|
|
113
|
+
# --- pre-push.js ---
|
|
95
114
|
echo ""
|
|
96
115
|
echo "pre-push:"
|
|
97
116
|
|
|
98
|
-
if grep -q '
|
|
99
|
-
echo " ✓
|
|
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
|
|
121
|
+
echo " ✗ missing tracking.json update"
|
|
103
122
|
FAIL=$((FAIL + 1))
|
|
104
123
|
fi
|
|
105
124
|
|
|
106
|
-
if grep -q '
|
|
107
|
-
echo " ✓
|
|
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
|
|
129
|
+
echo " ✗ missing last_commit stamp"
|
|
111
130
|
FAIL=$((FAIL + 1))
|
|
112
131
|
fi
|
|
113
132
|
|
|
114
|
-
#
|
|
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
|
|
119
|
-
echo " ✓
|
|
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 " ✗
|
|
147
|
+
echo " ✗ missing TypeScript check"
|
|
123
148
|
FAIL=$((FAIL + 1))
|
|
124
149
|
fi
|
|
125
150
|
|
|
126
|
-
if grep -q '
|
|
127
|
-
echo " ✓
|
|
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
|
|
155
|
+
echo " ✗ missing service_role check"
|
|
131
156
|
FAIL=$((FAIL + 1))
|
|
132
157
|
fi
|
|
133
158
|
|
|
134
|
-
|
|
135
|
-
|
|
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
|
|
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 ==="
|
package/hooks/auto-update.sh
DELETED
|
@@ -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
|
package/hooks/block-env-edit.sh
DELETED
|
@@ -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
|
package/hooks/branch-guard.sh
DELETED
|
@@ -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
|