qualia-framework-v2 2.5.0 → 2.6.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 +9 -9
- package/agents/planner.md +8 -2
- package/agents/qa-browser.md +186 -0
- package/bin/install.js +32 -20
- package/bin/qualia-ui.js +278 -0
- package/hooks/branch-guard.sh +11 -2
- package/hooks/session-start.sh +27 -9
- 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 +18 -6
package/README.md
CHANGED
|
@@ -47,11 +47,11 @@ See `guide.md` for the full developer guide.
|
|
|
47
47
|
|
|
48
48
|
## What's Inside
|
|
49
49
|
|
|
50
|
-
- **
|
|
51
|
-
- **
|
|
52
|
-
- **
|
|
50
|
+
- **19 skills** — slash commands from setup to handoff, plus debugging, design, review, knowledge, session management, and skill authoring
|
|
51
|
+
- **4 agents** — planner, builder, verifier, qa-browser (each in fresh context)
|
|
52
|
+
- **8 hooks** — session start, branch guard, pre-push tracking sync, env protection, migration guard, deploy gate, pre-compact state save, auto-update
|
|
53
53
|
- **3 rules** — security, frontend, deployment
|
|
54
|
-
- **
|
|
54
|
+
- **5 templates** — tracking.json, state.md, project.md, plan.md, DESIGN.md
|
|
55
55
|
|
|
56
56
|
## Why It Works
|
|
57
57
|
|
|
@@ -92,13 +92,13 @@ npx qualia-framework-v2 install
|
|
|
92
92
|
|
|
|
93
93
|
v
|
|
94
94
|
~/.claude/
|
|
95
|
-
├── skills/
|
|
96
|
-
├── agents/ planner.md, builder.md, verifier.md
|
|
97
|
-
├── hooks/
|
|
95
|
+
├── skills/ 19 slash commands
|
|
96
|
+
├── agents/ planner.md, builder.md, verifier.md, qa-browser.md
|
|
97
|
+
├── hooks/ 8 shell scripts (session-start, branch, env, migration, deploy, push, compact, auto-update)
|
|
98
98
|
├── bin/ state.js (state machine with precondition enforcement)
|
|
99
|
-
├── knowledge/ learned-patterns.md, common-fixes.md, client-prefs.md
|
|
99
|
+
├── knowledge/ learned-patterns.md, common-fixes.md, client-prefs.md (auto-loaded by skills)
|
|
100
100
|
├── rules/ security.md, frontend.md, deployment.md
|
|
101
|
-
├── qualia-templates/ tracking.json, state.md, project.md, plan.md
|
|
101
|
+
├── qualia-templates/ tracking.json, state.md, project.md, plan.md, DESIGN.md
|
|
102
102
|
├── CLAUDE.md global instructions (role-configured per team member)
|
|
103
103
|
└── statusline.sh teal-branded 2-line status bar
|
|
104
104
|
```
|
package/agents/planner.md
CHANGED
|
@@ -81,7 +81,13 @@ Every task MUST have these three fields with concrete content:
|
|
|
81
81
|
- **Action:** At least one concrete instruction — not just "implement auth". Reference specific functions, components, or patterns. "Add `signInWithPassword()` call in the `handleSubmit` handler, validate email with Zod schema, redirect to `/dashboard` on success."
|
|
82
82
|
- **Done when:** Testable, not fuzzy. Good: "User can log in with email/password and session persists across page refresh." Bad: "Auth works." Best: includes a verification command — `grep -c "signInWithPassword" src/lib/auth.ts` returns non-zero.
|
|
83
83
|
|
|
84
|
-
If a task involves a library or API you're unsure about,
|
|
84
|
+
If a task involves a library, framework, or API you're unsure about, fetch the current documentation BEFORE specifying the approach. Don't guess at APIs.
|
|
85
|
+
|
|
86
|
+
Preferred order:
|
|
87
|
+
1. **Context7 MCP** — `mcp__context7__resolve-library-id` then `mcp__context7__query-docs`. Fast, current, structured. Use for: React, Next.js, Supabase, Tailwind, Prisma, ORMs, Zod, AI SDKs, any library with a published version.
|
|
88
|
+
2. **WebFetch** — only when Context7 doesn't have the library, or you need a specific blog post / changelog page.
|
|
89
|
+
|
|
90
|
+
Your training data is often stale. A two-second lookup is cheaper than a wrong task specification.
|
|
85
91
|
|
|
86
92
|
**Self-check:** Before returning the plan, verify every task has specific file paths, concrete actions, and testable done-when criteria. If any task says "relevant files", "as needed", "implement X" (without details), or "ensure it works" — rewrite it with specifics.
|
|
87
93
|
|
|
@@ -92,7 +98,7 @@ When a phase involves frontend work (pages, components, layouts, UI):
|
|
|
92
98
|
1. **Check for `.planning/DESIGN.md`** — if it exists, reference it in task Context fields: `@.planning/DESIGN.md`
|
|
93
99
|
2. **If no DESIGN.md and this is Phase 1** — add a Task 1 (Wave 1) to create it:
|
|
94
100
|
- Generate `.planning/DESIGN.md` from the design direction in PROJECT.md
|
|
95
|
-
- Use the template at
|
|
101
|
+
- Use the template at `~/.claude/qualia-templates/DESIGN.md` — fill in: palette, typography (distinctive fonts), spacing, motion approach, component patterns
|
|
96
102
|
- Done when: DESIGN.md exists with concrete CSS variable values (not placeholders)
|
|
97
103
|
3. **Include design criteria in "Done when"** for frontend tasks:
|
|
98
104
|
- Not just "page renders" but "page renders with design system typography, proper color palette, all interactive states (hover/focus/loading/error/empty), semantic HTML, keyboard accessible"
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: qualia-qa-browser
|
|
3
|
+
description: Real-browser QA. Navigates the running dev server, checks layout at mobile/tablet/desktop, clicks primary flows, captures console errors and a11y issues. Spawned by /qualia-verify on phases with frontend work.
|
|
4
|
+
tools: Read, Bash, Grep, Glob
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Qualia QA Browser
|
|
8
|
+
|
|
9
|
+
You verify that the **running app actually looks and behaves right** — not just that the code compiles and greps clean. Fresh context, no memory of what was built.
|
|
10
|
+
|
|
11
|
+
**Critical mindset:** You are the user. You don't trust the code — you drive the app and see what happens. If it breaks at 375px, it's broken. If the console screams, it's broken. If clicking the primary CTA does nothing, it's broken.
|
|
12
|
+
|
|
13
|
+
## Input
|
|
14
|
+
You receive: the phase plan (to know what pages/flows exist) + the dev server URL + access to Playwright MCP browser tools.
|
|
15
|
+
|
|
16
|
+
## Output
|
|
17
|
+
Append a `## Browser QA` section to `.planning/phase-{N}-verification.md` with PASS/FAIL per check.
|
|
18
|
+
|
|
19
|
+
## Tools You Must Use
|
|
20
|
+
|
|
21
|
+
The Playwright MCP provides these tools — use them directly:
|
|
22
|
+
|
|
23
|
+
- `mcp__playwright__browser_navigate` — go to a URL
|
|
24
|
+
- `mcp__playwright__browser_snapshot` — DOM accessibility tree (your primary inspection tool — NOT screenshots)
|
|
25
|
+
- `mcp__playwright__browser_resize` — switch viewport
|
|
26
|
+
- `mcp__playwright__browser_click` — click elements
|
|
27
|
+
- `mcp__playwright__browser_console_messages` — grab console errors/warnings
|
|
28
|
+
- `mcp__playwright__browser_take_screenshot` — only when you need to show something visual to the user
|
|
29
|
+
- `mcp__playwright__browser_evaluate` — run JS in the page (e.g., `document.querySelectorAll('img:not([alt])').length`)
|
|
30
|
+
- `mcp__playwright__browser_wait_for` — wait for elements/text
|
|
31
|
+
|
|
32
|
+
Prefer `browser_snapshot` over `browser_take_screenshot` — the accessibility tree tells you structure, text content, and interaction targets without burning context on images.
|
|
33
|
+
|
|
34
|
+
## Process
|
|
35
|
+
|
|
36
|
+
### 1. Find the Dev Server
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
# Check if already running
|
|
40
|
+
curl -s -o /dev/null -w "%{http_code}" http://localhost:3000 2>/dev/null
|
|
41
|
+
curl -s -o /dev/null -w "%{http_code}" http://localhost:3001 2>/dev/null
|
|
42
|
+
|
|
43
|
+
# If not running, start it in background
|
|
44
|
+
if ! curl -s http://localhost:3000 >/dev/null 2>&1; then
|
|
45
|
+
npm run dev > /tmp/dev-server.log 2>&1 &
|
|
46
|
+
sleep 5 # give it time to boot
|
|
47
|
+
fi
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
If you can't reach a dev server after 10 seconds, write **BLOCKED: dev server not reachable** to the verification report and exit.
|
|
51
|
+
|
|
52
|
+
### 2. Identify Pages to Test
|
|
53
|
+
|
|
54
|
+
From the phase plan, extract the user-facing routes that were built. If unclear, inspect the file tree:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
ls app/**/page.tsx 2>/dev/null || ls src/app/**/page.tsx 2>/dev/null || ls pages/*.tsx 2>/dev/null
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Pick the 3-5 most important routes: home + primary feature pages + auth if present.
|
|
61
|
+
|
|
62
|
+
### 3. Responsive Check (Critical)
|
|
63
|
+
|
|
64
|
+
For each route, visit at 3 viewports:
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
1. navigate http://localhost:{port}{route}
|
|
68
|
+
2. browser_resize 375 812 (iPhone 14)
|
|
69
|
+
3. browser_snapshot (capture mobile tree)
|
|
70
|
+
4. browser_resize 768 1024 (iPad)
|
|
71
|
+
5. browser_snapshot (capture tablet tree)
|
|
72
|
+
6. browser_resize 1440 900 (laptop)
|
|
73
|
+
7. browser_snapshot (capture desktop tree)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
**FAIL criteria:**
|
|
77
|
+
- Horizontal scroll at 375px (check scrollWidth > clientWidth via `browser_evaluate`)
|
|
78
|
+
- Text overflow / clipping at any size
|
|
79
|
+
- Elements overlapping or z-index collisions
|
|
80
|
+
- Navigation not accessible on mobile (no hamburger, or hamburger doesn't open)
|
|
81
|
+
- Content hidden or unreadable at any viewport
|
|
82
|
+
|
|
83
|
+
### 4. Console Errors Check
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
browser_console_messages
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
**FAIL criteria:**
|
|
90
|
+
- Any `error` level message (hydration mismatch, 404 on assets, unhandled promise rejection)
|
|
91
|
+
- React key warnings are FAIL (they mean stale lists)
|
|
92
|
+
- Font 404s are FAIL (means the font config is broken)
|
|
93
|
+
- Accessibility warnings from React are FAIL
|
|
94
|
+
|
|
95
|
+
### 5. Primary Flow Walkthrough
|
|
96
|
+
|
|
97
|
+
For each primary user flow (login, signup, main action), do it:
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
1. navigate to the flow's start
|
|
101
|
+
2. browser_snapshot to find the actual interactive elements
|
|
102
|
+
3. browser_click on the primary CTA
|
|
103
|
+
4. browser_wait_for the expected result
|
|
104
|
+
5. browser_snapshot to verify the result
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
**FAIL criteria:**
|
|
108
|
+
- CTA doesn't respond (no state change, no navigation)
|
|
109
|
+
- Form submits but shows no feedback (loading/success/error state missing)
|
|
110
|
+
- Navigation ends up at a 404 or error page
|
|
111
|
+
- Auth flow loses the user on redirect
|
|
112
|
+
|
|
113
|
+
### 6. Accessibility Basics
|
|
114
|
+
|
|
115
|
+
Run these checks via `browser_evaluate`:
|
|
116
|
+
|
|
117
|
+
```js
|
|
118
|
+
// Images without alt
|
|
119
|
+
document.querySelectorAll('img:not([alt])').length
|
|
120
|
+
|
|
121
|
+
// Form inputs without labels
|
|
122
|
+
Array.from(document.querySelectorAll('input, textarea, select')).filter(el => {
|
|
123
|
+
const id = el.id;
|
|
124
|
+
const hasLabel = id && document.querySelector(`label[for="${id}"]`);
|
|
125
|
+
return !hasLabel && !el.getAttribute('aria-label') && !el.getAttribute('aria-labelledby');
|
|
126
|
+
}).length
|
|
127
|
+
|
|
128
|
+
// Heading order
|
|
129
|
+
Array.from(document.querySelectorAll('h1,h2,h3,h4,h5,h6')).map(h => parseInt(h.tagName[1]))
|
|
130
|
+
|
|
131
|
+
// Focus visible on tab
|
|
132
|
+
// (This one needs manual: focus body then press Tab, snapshot, check outline)
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
**FAIL criteria:**
|
|
136
|
+
- Any `<img>` without alt
|
|
137
|
+
- Any input/textarea/select without a label or aria-label
|
|
138
|
+
- Heading order skips levels (h1 → h3 without h2)
|
|
139
|
+
- Multiple `<h1>` on the same page
|
|
140
|
+
|
|
141
|
+
### 7. Write the Report
|
|
142
|
+
|
|
143
|
+
Append to `.planning/phase-{N}-verification.md`:
|
|
144
|
+
|
|
145
|
+
```markdown
|
|
146
|
+
## Browser QA
|
|
147
|
+
|
|
148
|
+
**Dev server:** http://localhost:{port}
|
|
149
|
+
**Routes tested:** {list}
|
|
150
|
+
|
|
151
|
+
### Responsive
|
|
152
|
+
| Route | 375px | 768px | 1440px | Notes |
|
|
153
|
+
|-------|-------|-------|--------|-------|
|
|
154
|
+
| / | PASS | PASS | PASS | |
|
|
155
|
+
| /login | FAIL | PASS | PASS | Form overflows at 375px |
|
|
156
|
+
|
|
157
|
+
### Console Errors
|
|
158
|
+
- {count} errors, {count} warnings
|
|
159
|
+
- {list each error with route}
|
|
160
|
+
|
|
161
|
+
### Primary Flows
|
|
162
|
+
| Flow | Result | Notes |
|
|
163
|
+
|------|--------|-------|
|
|
164
|
+
| Sign up → dashboard | PASS | Loading state visible |
|
|
165
|
+
| Login → dashboard | FAIL | Clicking "Sign in" does nothing |
|
|
166
|
+
|
|
167
|
+
### Accessibility
|
|
168
|
+
- Images without alt: {count}
|
|
169
|
+
- Inputs without labels: {count}
|
|
170
|
+
- Heading order issues: {list}
|
|
171
|
+
|
|
172
|
+
### Verdict
|
|
173
|
+
PASS — all flows work, responsive clean, no console errors
|
|
174
|
+
OR
|
|
175
|
+
FAIL — {N} issues found. See above.
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Rules
|
|
179
|
+
|
|
180
|
+
1. **Never trust code that you haven't driven.** The compiler says "yes" all the time about things that don't work.
|
|
181
|
+
2. **Test at 375px first.** If it breaks on mobile, it's broken. Desktop-first thinking is a bug.
|
|
182
|
+
3. **Console errors are failures, not warnings.** A hydration mismatch today is a production bug tomorrow.
|
|
183
|
+
4. **Don't fix anything.** You have no Write/Edit tools. You report; the planner decides the fix.
|
|
184
|
+
5. **Don't start the dev server if it's already running.** You'd kill someone else's session.
|
|
185
|
+
6. **Cap snapshots.** Don't take 50 snapshots — aim for ~15 total across all pages and viewports. Budget your context.
|
|
186
|
+
7. **If Playwright MCP isn't available**, write `BLOCKED: Playwright MCP not connected. Run: claude mcp list` and exit. Don't fake it.
|
package/bin/install.js
CHANGED
|
@@ -208,8 +208,14 @@ async function main() {
|
|
|
208
208
|
path.join(binDest, "state.js")
|
|
209
209
|
);
|
|
210
210
|
ok("state.js (state machine)");
|
|
211
|
+
copy(
|
|
212
|
+
path.join(FRAMEWORK_DIR, "bin", "qualia-ui.js"),
|
|
213
|
+
path.join(binDest, "qualia-ui.js")
|
|
214
|
+
);
|
|
215
|
+
fs.chmodSync(path.join(binDest, "qualia-ui.js"), 0o755);
|
|
216
|
+
ok("qualia-ui.js (cosmetics library)");
|
|
211
217
|
} catch (e) {
|
|
212
|
-
warn(`
|
|
218
|
+
warn(`scripts — ${e.message}`);
|
|
213
219
|
}
|
|
214
220
|
|
|
215
221
|
// ─── Guide ─────────────────────────────────────────────
|
|
@@ -228,7 +234,7 @@ async function main() {
|
|
|
228
234
|
const knowledgeDir = path.join(CLAUDE_DIR, "knowledge");
|
|
229
235
|
if (!fs.existsSync(knowledgeDir)) fs.mkdirSync(knowledgeDir, { recursive: true });
|
|
230
236
|
const knowledgeFiles = {
|
|
231
|
-
"learned-patterns.md": "# Learned Patterns\n\nPatterns discovered across projects. Updated by `/qualia-
|
|
237
|
+
"learned-patterns.md": "# Learned Patterns\n\nPatterns discovered across projects. Updated by `/qualia-learn` and manual notes.\n",
|
|
232
238
|
"common-fixes.md": "# Common Fixes\n\nRecurring issues and their solutions.\n",
|
|
233
239
|
"client-prefs.md": "# Client Preferences\n\nClient-specific preferences, design choices, and requirements.\n",
|
|
234
240
|
};
|
|
@@ -328,6 +334,18 @@ async function main() {
|
|
|
328
334
|
// Hooks — full system
|
|
329
335
|
const hd = path.join(CLAUDE_DIR, "hooks");
|
|
330
336
|
settings.hooks = {
|
|
337
|
+
SessionStart: [
|
|
338
|
+
{
|
|
339
|
+
matcher: ".*",
|
|
340
|
+
hooks: [
|
|
341
|
+
{
|
|
342
|
+
type: "command",
|
|
343
|
+
command: `${hd}/session-start.sh`,
|
|
344
|
+
timeout: 5,
|
|
345
|
+
},
|
|
346
|
+
],
|
|
347
|
+
},
|
|
348
|
+
],
|
|
331
349
|
PreToolUse: [
|
|
332
350
|
{
|
|
333
351
|
matcher: "Bash",
|
|
@@ -351,6 +369,13 @@ async function main() {
|
|
|
351
369
|
timeout: 15,
|
|
352
370
|
statusMessage: "◆ Syncing tracking...",
|
|
353
371
|
},
|
|
372
|
+
{
|
|
373
|
+
type: "command",
|
|
374
|
+
if: "Bash(vercel --prod*)",
|
|
375
|
+
command: `${hd}/pre-deploy-gate.sh`,
|
|
376
|
+
timeout: 120,
|
|
377
|
+
statusMessage: "◆ Running quality gates...",
|
|
378
|
+
},
|
|
354
379
|
],
|
|
355
380
|
},
|
|
356
381
|
{
|
|
@@ -373,20 +398,6 @@ async function main() {
|
|
|
373
398
|
],
|
|
374
399
|
},
|
|
375
400
|
],
|
|
376
|
-
PostToolUse: [
|
|
377
|
-
{
|
|
378
|
-
matcher: "Bash",
|
|
379
|
-
hooks: [
|
|
380
|
-
{
|
|
381
|
-
type: "command",
|
|
382
|
-
if: "Bash(vercel --prod*)",
|
|
383
|
-
command: `${hd}/pre-deploy-gate.sh`,
|
|
384
|
-
timeout: 120,
|
|
385
|
-
statusMessage: "◆ Running quality gates...",
|
|
386
|
-
},
|
|
387
|
-
],
|
|
388
|
-
},
|
|
389
|
-
],
|
|
390
401
|
PreCompact: [
|
|
391
402
|
{
|
|
392
403
|
matcher: "compact",
|
|
@@ -429,7 +440,7 @@ async function main() {
|
|
|
429
440
|
|
|
430
441
|
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
431
442
|
|
|
432
|
-
ok("Hooks: auto-update, branch-guard, pre-push, env-block, migration-guard, deploy-gate, pre-compact");
|
|
443
|
+
ok("Hooks: session-start, auto-update, branch-guard, pre-push, env-block, migration-guard, deploy-gate, pre-compact");
|
|
433
444
|
ok("Status line + spinner configured");
|
|
434
445
|
ok("Environment variables + permissions");
|
|
435
446
|
|
|
@@ -439,10 +450,11 @@ async function main() {
|
|
|
439
450
|
console.log(`${DIM} ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}`);
|
|
440
451
|
console.log(` ${WHITE}${member.name}${RESET} ${DIM}(${member.role})${RESET}`);
|
|
441
452
|
console.log(` Skills: ${WHITE}${skills.length}${RESET}`);
|
|
442
|
-
|
|
443
|
-
console.log(`
|
|
453
|
+
const agentCount = fs.readdirSync(agentsDir).filter(f => f.endsWith('.md')).length;
|
|
454
|
+
console.log(` Agents: ${WHITE}${agentCount}${RESET} ${DIM}(planner, builder, verifier, qa-browser)${RESET}`);
|
|
455
|
+
console.log(` Hooks: ${WHITE}8${RESET} ${DIM}(session-start, auto-update, branch-guard, pre-push, env-block, migration-guard, deploy-gate, pre-compact)${RESET}`);
|
|
444
456
|
console.log(` Rules: ${WHITE}${fs.readdirSync(rulesDir).length}${RESET} ${DIM}(security, frontend, design-reference, deployment)${RESET}`);
|
|
445
|
-
console.log(` Scripts: ${WHITE}
|
|
457
|
+
console.log(` Scripts: ${WHITE}2${RESET} ${DIM}(state.js, qualia-ui.js)${RESET}`);
|
|
446
458
|
console.log(` Knowledge: ${WHITE}3${RESET} ${DIM}(patterns, fixes, client prefs)${RESET}`);
|
|
447
459
|
console.log(` Templates: ${WHITE}${fs.readdirSync(tmplDir).length}${RESET}`);
|
|
448
460
|
console.log(` Status line: ${GREEN}✓${RESET}`);
|
package/bin/qualia-ui.js
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Qualia UI — consistent banners, context panels, status for every skill.
|
|
3
|
+
// Zero dependencies. Reads state.js + .qualia-config.json for context.
|
|
4
|
+
//
|
|
5
|
+
// Commands:
|
|
6
|
+
// banner <action> [phase] [subtitle] — full header with context panel
|
|
7
|
+
// context — just the context panel
|
|
8
|
+
// divider — horizontal rule
|
|
9
|
+
// ok <message> — green check line
|
|
10
|
+
// fail <message> — red cross line
|
|
11
|
+
// warn <message> — yellow bang line
|
|
12
|
+
// info <message> — blue dot line
|
|
13
|
+
// spawn <agent> <description> — spawning a subagent
|
|
14
|
+
// wave <N> <total> <task-count> — wave header for /qualia-build
|
|
15
|
+
// task <N> <title> — task line (pending)
|
|
16
|
+
// done <N> <title> [commit] — task line (completed)
|
|
17
|
+
// next <command> — "Run: /qualia-X" footer
|
|
18
|
+
// end <status> [next-command] — closing banner with optional next
|
|
19
|
+
|
|
20
|
+
const fs = require("fs");
|
|
21
|
+
const path = require("path");
|
|
22
|
+
const os = require("os");
|
|
23
|
+
const { execSync } = require("child_process");
|
|
24
|
+
|
|
25
|
+
// ─── Colors ──────────────────────────────────────────────
|
|
26
|
+
const TEAL = "\x1b[38;2;0;206;209m";
|
|
27
|
+
const TEAL_DIM = "\x1b[38;2;0;140;145m";
|
|
28
|
+
const DIM = "\x1b[38;2;100;110;120m";
|
|
29
|
+
const DIM2 = "\x1b[38;2;70;80;90m";
|
|
30
|
+
const GREEN = "\x1b[38;2;52;211;153m";
|
|
31
|
+
const WHITE = "\x1b[38;2;220;225;230m";
|
|
32
|
+
const YELLOW = "\x1b[38;2;234;179;8m";
|
|
33
|
+
const RED = "\x1b[38;2;239;68;68m";
|
|
34
|
+
const BLUE = "\x1b[38;2;96;165;250m";
|
|
35
|
+
const RESET = "\x1b[0m";
|
|
36
|
+
const BOLD = "\x1b[1m";
|
|
37
|
+
|
|
38
|
+
const RULE = "━".repeat(42);
|
|
39
|
+
const RULE_DIM = `${DIM2}${RULE}${RESET}`;
|
|
40
|
+
|
|
41
|
+
// ─── Action Labels ───────────────────────────────────────
|
|
42
|
+
const ACTIONS = {
|
|
43
|
+
router: { label: "SMART ROUTER", glyph: "◆" },
|
|
44
|
+
new: { label: "NEW PROJECT", glyph: "◆" },
|
|
45
|
+
plan: { label: "PLANNING", glyph: "◇" },
|
|
46
|
+
build: { label: "BUILDING", glyph: "◈" },
|
|
47
|
+
verify: { label: "VERIFYING", glyph: "◉" },
|
|
48
|
+
polish: { label: "POLISHING", glyph: "◆" },
|
|
49
|
+
ship: { label: "SHIPPING", glyph: "▲" },
|
|
50
|
+
handoff: { label: "HANDING OFF", glyph: "▶" },
|
|
51
|
+
report: { label: "SESSION REPORT", glyph: "◆" },
|
|
52
|
+
debug: { label: "DEBUGGING", glyph: "◊" },
|
|
53
|
+
learn: { label: "LEARNING", glyph: "◆" },
|
|
54
|
+
pause: { label: "PAUSING", glyph: "◆" },
|
|
55
|
+
resume: { label: "RESUMING", glyph: "◆" },
|
|
56
|
+
review: { label: "REVIEW", glyph: "◆" },
|
|
57
|
+
design: { label: "DESIGN PASS", glyph: "◆" },
|
|
58
|
+
quick: { label: "QUICK FIX", glyph: "◆" },
|
|
59
|
+
task: { label: "TASK", glyph: "◆" },
|
|
60
|
+
"skill-new": { label: "NEW SKILL", glyph: "◆" },
|
|
61
|
+
gaps: { label: "GAP CLOSURE", glyph: "◇" },
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// ─── State Reading ───────────────────────────────────────
|
|
65
|
+
function readState() {
|
|
66
|
+
try {
|
|
67
|
+
const out = execSync(`node ${path.join(os.homedir(), ".claude", "bin", "state.js")} check 2>/dev/null`, {
|
|
68
|
+
encoding: "utf8",
|
|
69
|
+
timeout: 3000,
|
|
70
|
+
});
|
|
71
|
+
return JSON.parse(out);
|
|
72
|
+
} catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function readConfig() {
|
|
78
|
+
try {
|
|
79
|
+
const f = path.join(os.homedir(), ".claude", ".qualia-config.json");
|
|
80
|
+
return JSON.parse(fs.readFileSync(f, "utf8"));
|
|
81
|
+
} catch {
|
|
82
|
+
return {};
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function projectName() {
|
|
87
|
+
try {
|
|
88
|
+
return path.basename(process.cwd());
|
|
89
|
+
} catch {
|
|
90
|
+
return "—";
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ─── Rendering Helpers ───────────────────────────────────
|
|
95
|
+
function progressBar(phase, total) {
|
|
96
|
+
if (!total || total < 1) return "";
|
|
97
|
+
const pct = Math.min(100, Math.round(((phase - 1) / total) * 100));
|
|
98
|
+
const filled = Math.round(pct / 10);
|
|
99
|
+
const bar = `${TEAL}${"█".repeat(filled)}${DIM2}${"░".repeat(10 - filled)}${RESET}`;
|
|
100
|
+
return `${bar} ${DIM}${pct}%${RESET}`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function colorForStatus(s) {
|
|
104
|
+
const colors = {
|
|
105
|
+
setup: DIM,
|
|
106
|
+
planned: BLUE,
|
|
107
|
+
built: YELLOW,
|
|
108
|
+
verified: GREEN,
|
|
109
|
+
polished: GREEN,
|
|
110
|
+
shipped: TEAL,
|
|
111
|
+
handed_off: TEAL,
|
|
112
|
+
done: GREEN,
|
|
113
|
+
};
|
|
114
|
+
return colors[s] || WHITE;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function pad(str, width) {
|
|
118
|
+
// Width-aware padding ignoring ANSI codes
|
|
119
|
+
const visible = str.replace(/\x1b\[[0-9;]*m/g, "");
|
|
120
|
+
const need = Math.max(0, width - visible.length);
|
|
121
|
+
return str + " ".repeat(need);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ─── Commands ────────────────────────────────────────────
|
|
125
|
+
function cmdBanner(action, phase, subtitle) {
|
|
126
|
+
const spec = ACTIONS[action] || { label: (action || "qualia").toUpperCase(), glyph: "◆" };
|
|
127
|
+
const state = readState();
|
|
128
|
+
const config = readConfig();
|
|
129
|
+
const project = projectName();
|
|
130
|
+
|
|
131
|
+
const title = phase
|
|
132
|
+
? `${spec.label} ${DIM}·${WHITE} Phase ${phase}${subtitle ? ` ${DIM}— ${WHITE}${subtitle}` : ""}`
|
|
133
|
+
: spec.label;
|
|
134
|
+
|
|
135
|
+
console.log("");
|
|
136
|
+
console.log(` ${TEAL}${BOLD}${spec.glyph}${RESET} ${WHITE}${BOLD}QUALIA${RESET} ${DIM}►${RESET} ${WHITE}${title}${RESET}`);
|
|
137
|
+
console.log(` ${RULE_DIM}`);
|
|
138
|
+
|
|
139
|
+
// Context panel
|
|
140
|
+
const roleColor = config.role === "OWNER" ? TEAL : BLUE;
|
|
141
|
+
const roleLine = config.role
|
|
142
|
+
? `${roleColor}${config.role}${RESET} ${DIM}·${RESET} ${WHITE}${config.installed_by || ""}${RESET}`
|
|
143
|
+
: `${DIM}(not configured)${RESET}`;
|
|
144
|
+
|
|
145
|
+
console.log(` ${pad(DIM + "Project" + RESET, 20)}${WHITE}${project}${RESET}`);
|
|
146
|
+
|
|
147
|
+
if (state && state.ok) {
|
|
148
|
+
const phaseStr = state.phase_name
|
|
149
|
+
? `${state.phase} of ${state.total_phases} ${DIM}— ${WHITE}${state.phase_name}`
|
|
150
|
+
: `${state.phase} of ${state.total_phases}`;
|
|
151
|
+
console.log(` ${pad(DIM + "Phase" + RESET, 20)}${WHITE}${phaseStr}${RESET}`);
|
|
152
|
+
console.log(` ${pad(DIM + "Status" + RESET, 20)}${colorForStatus(state.status)}${state.status}${RESET}`);
|
|
153
|
+
if (state.tasks_total) {
|
|
154
|
+
console.log(` ${pad(DIM + "Tasks" + RESET, 20)}${WHITE}${state.tasks_done}/${state.tasks_total}${RESET}`);
|
|
155
|
+
}
|
|
156
|
+
const bar = progressBar(state.phase, state.total_phases);
|
|
157
|
+
if (bar) console.log(` ${pad(DIM + "Progress" + RESET, 20)}${bar}`);
|
|
158
|
+
if (state.gap_cycles > 0) {
|
|
159
|
+
console.log(` ${pad(DIM + "Gap cycles" + RESET, 20)}${YELLOW}${state.gap_cycles}/2${RESET}`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
console.log(` ${pad(DIM + "Role" + RESET, 20)}${roleLine}`);
|
|
164
|
+
console.log(` ${RULE_DIM}`);
|
|
165
|
+
console.log("");
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function cmdContext() {
|
|
169
|
+
const state = readState();
|
|
170
|
+
const config = readConfig();
|
|
171
|
+
const project = projectName();
|
|
172
|
+
|
|
173
|
+
console.log("");
|
|
174
|
+
console.log(` ${pad(DIM + "Project" + RESET, 20)}${WHITE}${project}${RESET}`);
|
|
175
|
+
|
|
176
|
+
if (state && state.ok) {
|
|
177
|
+
const phaseStr = state.phase_name
|
|
178
|
+
? `${state.phase} of ${state.total_phases} ${DIM}— ${WHITE}${state.phase_name}`
|
|
179
|
+
: `${state.phase} of ${state.total_phases}`;
|
|
180
|
+
console.log(` ${pad(DIM + "Phase" + RESET, 20)}${WHITE}${phaseStr}${RESET}`);
|
|
181
|
+
console.log(` ${pad(DIM + "Status" + RESET, 20)}${colorForStatus(state.status)}${state.status}${RESET}`);
|
|
182
|
+
const bar = progressBar(state.phase, state.total_phases);
|
|
183
|
+
if (bar) console.log(` ${pad(DIM + "Progress" + RESET, 20)}${bar}`);
|
|
184
|
+
} else {
|
|
185
|
+
console.log(` ${DIM}No project detected. Run${RESET} ${TEAL}/qualia-new${RESET}`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (config.role) {
|
|
189
|
+
const roleColor = config.role === "OWNER" ? TEAL : BLUE;
|
|
190
|
+
console.log(` ${pad(DIM + "Role" + RESET, 20)}${roleColor}${config.role}${RESET} ${DIM}·${RESET} ${WHITE}${config.installed_by || ""}${RESET}`);
|
|
191
|
+
}
|
|
192
|
+
console.log("");
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function cmdDivider() {
|
|
196
|
+
console.log(` ${RULE_DIM}`);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function cmdOk(msg) {
|
|
200
|
+
console.log(` ${GREEN}✓${RESET} ${WHITE}${msg}${RESET}`);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function cmdFail(msg) {
|
|
204
|
+
console.log(` ${RED}✗${RESET} ${WHITE}${msg}${RESET}`);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function cmdWarn(msg) {
|
|
208
|
+
console.log(` ${YELLOW}!${RESET} ${WHITE}${msg}${RESET}`);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function cmdInfo(msg) {
|
|
212
|
+
console.log(` ${BLUE}◦${RESET} ${DIM}${msg}${RESET}`);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function cmdSpawn(agent, desc) {
|
|
216
|
+
const name = agent || "agent";
|
|
217
|
+
const d = desc ? ` ${DIM}— ${desc}${RESET}` : "";
|
|
218
|
+
console.log(` ${TEAL}⟐${RESET} ${WHITE}Spawning${RESET} ${TEAL}${name}${RESET}${d}`);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function cmdWave(num, total, taskCount) {
|
|
222
|
+
console.log("");
|
|
223
|
+
const n = parseInt(num) || 0;
|
|
224
|
+
const t = parseInt(total) || 0;
|
|
225
|
+
const c = parseInt(taskCount) || 0;
|
|
226
|
+
console.log(` ${TEAL}▸${RESET} ${WHITE}${BOLD}Wave ${n}/${t}${RESET} ${DIM}(${c} ${c === 1 ? "task" : "tasks"}, parallel)${RESET}`);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function cmdTask(num, title) {
|
|
230
|
+
console.log(` ${DIM}${num}.${RESET} ${WHITE}${title}${RESET} ${DIM}…${RESET}`);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function cmdDone(num, title, commit) {
|
|
234
|
+
const c = commit ? ` ${DIM}(${commit})${RESET}` : "";
|
|
235
|
+
console.log(` ${GREEN}✓${RESET} ${DIM}${num}.${RESET} ${WHITE}${title}${RESET}${c}`);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function cmdNext(cmd) {
|
|
239
|
+
if (!cmd) return;
|
|
240
|
+
console.log("");
|
|
241
|
+
console.log(` ${TEAL}→${RESET} ${WHITE}Next:${RESET} ${TEAL}${BOLD}${cmd}${RESET}`);
|
|
242
|
+
console.log("");
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function cmdEnd(status, nextCmd) {
|
|
246
|
+
console.log("");
|
|
247
|
+
console.log(` ${TEAL}${BOLD}◆${RESET} ${WHITE}${BOLD}${status || "DONE"}${RESET}`);
|
|
248
|
+
console.log(` ${RULE_DIM}`);
|
|
249
|
+
if (nextCmd) {
|
|
250
|
+
console.log(` ${TEAL}→${RESET} ${WHITE}Next:${RESET} ${TEAL}${BOLD}${nextCmd}${RESET}`);
|
|
251
|
+
}
|
|
252
|
+
console.log("");
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// ─── Main ────────────────────────────────────────────────
|
|
256
|
+
const [cmd, ...rest] = process.argv.slice(2);
|
|
257
|
+
switch (cmd) {
|
|
258
|
+
case "banner":
|
|
259
|
+
cmdBanner(rest[0] || "router", rest[1] || "", rest.slice(2).join(" "));
|
|
260
|
+
break;
|
|
261
|
+
case "context": cmdContext(); break;
|
|
262
|
+
case "divider": cmdDivider(); break;
|
|
263
|
+
case "ok": cmdOk(rest.join(" ")); break;
|
|
264
|
+
case "fail": cmdFail(rest.join(" ")); break;
|
|
265
|
+
case "warn": cmdWarn(rest.join(" ")); break;
|
|
266
|
+
case "info": cmdInfo(rest.join(" ")); break;
|
|
267
|
+
case "spawn": cmdSpawn(rest[0], rest.slice(1).join(" ")); break;
|
|
268
|
+
case "wave": cmdWave(rest[0], rest[1], rest[2]); break;
|
|
269
|
+
case "task": cmdTask(rest[0], rest.slice(1).join(" ")); break;
|
|
270
|
+
case "done": cmdDone(rest[0], rest[1], rest[2]); break;
|
|
271
|
+
case "next": cmdNext(rest.join(" ")); break;
|
|
272
|
+
case "end": cmdEnd(rest[0], rest.slice(1).join(" ")); break;
|
|
273
|
+
default:
|
|
274
|
+
console.error(
|
|
275
|
+
`Usage: qualia-ui.js <banner|context|divider|ok|fail|warn|info|spawn|wave|task|done|next|end> [args]`
|
|
276
|
+
);
|
|
277
|
+
process.exit(1);
|
|
278
|
+
}
|
package/hooks/branch-guard.sh
CHANGED
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
# Block non-OWNER push to main/master
|
|
3
|
+
# Reads role from ~/.claude/.qualia-config.json (machine-readable source of truth)
|
|
3
4
|
|
|
4
5
|
BRANCH=$(git branch --show-current 2>/dev/null)
|
|
5
|
-
|
|
6
|
+
CONFIG="$HOME/.claude/.qualia-config.json"
|
|
7
|
+
|
|
8
|
+
if [ ! -f "$CONFIG" ]; then
|
|
9
|
+
echo "BLOCKED: ~/.claude/.qualia-config.json missing. Run: npx qualia-framework-v2 install"
|
|
10
|
+
exit 1
|
|
11
|
+
fi
|
|
12
|
+
|
|
13
|
+
# Extract role without jq dependency (installers may not have jq)
|
|
14
|
+
ROLE=$(node -e "try{console.log(JSON.parse(require('fs').readFileSync('$CONFIG','utf8')).role||'')}catch{}" 2>/dev/null)
|
|
6
15
|
|
|
7
16
|
if [ -z "$ROLE" ]; then
|
|
8
|
-
echo "BLOCKED: Cannot determine role
|
|
17
|
+
echo "BLOCKED: Cannot determine role from $CONFIG. Defaulting to deny."
|
|
9
18
|
exit 1
|
|
10
19
|
fi
|
|
11
20
|
|