qualia-framework-v2 2.6.0 → 2.8.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 -6
- package/bin/cli.js +14 -6
- package/bin/install.js +34 -31
- package/bin/qualia-ui.js +6 -3
- package/bin/statusline.js +202 -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 +3 -4
- package/skills/qualia-skill-new/SKILL.md +24 -5
- package/tests/hooks.test.sh +107 -55
- package/tests/state.test.sh +398 -0
- package/hooks/auto-update.sh +0 -56
- package/hooks/block-env-edit.sh +0 -11
- package/hooks/branch-guard.sh +0 -27
- 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 -35
- package/statusline.sh +0 -93
package/README.md
CHANGED
|
@@ -49,10 +49,18 @@ See `guide.md` for the full developer guide.
|
|
|
49
49
|
|
|
50
50
|
- **19 skills** — slash commands from setup to handoff, plus debugging, design, review, knowledge, session management, and skill authoring
|
|
51
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
|
-
- **
|
|
52
|
+
- **8 hooks** — session start, branch guard, pre-push tracking sync, env protection, migration guard, deploy gate, pre-compact state save, auto-update (all Node.js — cross-platform)
|
|
53
|
+
- **4 rules** — security, frontend, design-reference, deployment
|
|
54
54
|
- **5 templates** — tracking.json, state.md, project.md, plan.md, DESIGN.md
|
|
55
55
|
|
|
56
|
+
## Supported Platforms
|
|
57
|
+
|
|
58
|
+
Works on **Windows 10/11, macOS, and Linux**. Requires Node.js 18+ and Claude Code.
|
|
59
|
+
|
|
60
|
+
- Every hook and the status line are pure Node.js — no external bash, jq, or GNU coreutils required.
|
|
61
|
+
- Skills are executed by Claude Code's own Bash tool (which Claude Code provides on all platforms, including Windows).
|
|
62
|
+
- Tested on Fedora, EndeavourOS, macOS, and Windows 10/11.
|
|
63
|
+
|
|
56
64
|
## Why It Works
|
|
57
65
|
|
|
58
66
|
### Goal-Backward Verification
|
|
@@ -65,7 +73,7 @@ Splitting planner, builder, and verifier into separate agents with separate cont
|
|
|
65
73
|
|
|
66
74
|
### Production-Grade Hooks
|
|
67
75
|
|
|
68
|
-
|
|
76
|
+
All 8 hooks are real ops engineering, not theoretical. Highlights:
|
|
69
77
|
|
|
70
78
|
- **Pre-deploy gate** — TypeScript, lint, tests, build, and `service_role` leak scan before `vercel --prod`
|
|
71
79
|
- **Branch guard** — Role-aware: owner can push to main, employees can't
|
|
@@ -94,9 +102,9 @@ npx qualia-framework-v2 install
|
|
|
94
102
|
~/.claude/
|
|
95
103
|
├── skills/ 19 slash commands
|
|
96
104
|
├── agents/ planner.md, builder.md, verifier.md, qa-browser.md
|
|
97
|
-
├── hooks/ 8
|
|
98
|
-
├── bin/ state.js (state machine
|
|
99
|
-
├── knowledge/ learned-patterns.md, common-fixes.md, client-prefs.md (
|
|
105
|
+
├── hooks/ 8 Node.js hooks — cross-platform (no bash dependency)
|
|
106
|
+
├── bin/ state.js (state machine) + qualia-ui.js (cosmetics library)
|
|
107
|
+
├── knowledge/ learned-patterns.md, common-fixes.md, client-prefs.md (loaded by plan/debug/new)
|
|
100
108
|
├── rules/ security.md, frontend.md, deployment.md
|
|
101
109
|
├── qualia-templates/ tracking.json, state.md, project.md, plan.md, DESIGN.md
|
|
102
110
|
├── CLAUDE.md global instructions (role-configured per team member)
|
package/bin/cli.js
CHANGED
|
@@ -94,13 +94,21 @@ function cmdUpdate() {
|
|
|
94
94
|
console.log("");
|
|
95
95
|
|
|
96
96
|
try {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
97
|
+
const { spawnSync } = require("child_process");
|
|
98
|
+
const r = spawnSync("npx", ["qualia-framework-v2@latest", "install"], {
|
|
99
|
+
input: cfg.code + "\n",
|
|
100
|
+
stdio: ["pipe", "inherit", "inherit"],
|
|
101
|
+
shell: process.platform === "win32", // npx is a .cmd shim on Windows — must go through shell
|
|
102
|
+
timeout: 120000,
|
|
103
|
+
encoding: "utf8",
|
|
104
|
+
});
|
|
105
|
+
if (r.status !== 0) {
|
|
106
|
+
console.log(` ${RED}✗${RESET} Update failed. Run manually: npx qualia-framework-v2@latest install`);
|
|
107
|
+
process.exit(1);
|
|
108
|
+
}
|
|
102
109
|
} catch (e) {
|
|
103
|
-
console.log(` ${RED}✗${RESET} Update failed
|
|
110
|
+
console.log(` ${RED}✗${RESET} Update failed: ${e.message}`);
|
|
111
|
+
console.log(` ${DIM}Run manually:${RESET} npx qualia-framework-v2@latest install`);
|
|
104
112
|
process.exit(1);
|
|
105
113
|
}
|
|
106
114
|
}
|
package/bin/install.js
CHANGED
|
@@ -146,10 +146,20 @@ async function main() {
|
|
|
146
146
|
const hooksSource = path.join(FRAMEWORK_DIR, "hooks");
|
|
147
147
|
const hooksDest = path.join(CLAUDE_DIR, "hooks");
|
|
148
148
|
if (!fs.existsSync(hooksDest)) fs.mkdirSync(hooksDest, { recursive: true });
|
|
149
|
+
// Clean up legacy .sh hooks from previous v2.5/v2.6 installs so no orphans
|
|
150
|
+
// remain on disk after upgrading to the pure-Node v2.7+ hooks.
|
|
151
|
+
try {
|
|
152
|
+
for (const f of fs.readdirSync(hooksDest)) {
|
|
153
|
+
if (f.endsWith(".sh")) {
|
|
154
|
+
try { fs.unlinkSync(path.join(hooksDest, f)); } catch {}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
} catch {}
|
|
149
158
|
for (const file of fs.readdirSync(hooksSource)) {
|
|
150
159
|
try {
|
|
151
160
|
const dest = path.join(hooksDest, file);
|
|
152
161
|
copy(path.join(hooksSource, file), dest);
|
|
162
|
+
// chmod is a no-op on Windows but harmless
|
|
153
163
|
fs.chmodSync(dest, 0o755);
|
|
154
164
|
ok(file);
|
|
155
165
|
} catch (e) {
|
|
@@ -160,12 +170,11 @@ async function main() {
|
|
|
160
170
|
// ─── Status line ───────────────────────────────────────
|
|
161
171
|
log(`${WHITE}Status line${RESET}`);
|
|
162
172
|
try {
|
|
163
|
-
const slDest = path.join(CLAUDE_DIR, "statusline.
|
|
164
|
-
copy(path.join(FRAMEWORK_DIR, "statusline.
|
|
165
|
-
|
|
166
|
-
ok("statusline.sh");
|
|
173
|
+
const slDest = path.join(CLAUDE_DIR, "bin", "statusline.js");
|
|
174
|
+
copy(path.join(FRAMEWORK_DIR, "bin", "statusline.js"), slDest);
|
|
175
|
+
ok("statusline.js");
|
|
167
176
|
} catch (e) {
|
|
168
|
-
warn(`statusline.
|
|
177
|
+
warn(`statusline.js — ${e.message}`);
|
|
169
178
|
}
|
|
170
179
|
|
|
171
180
|
// ─── Templates ─────────────────────────────────────────
|
|
@@ -214,6 +223,11 @@ async function main() {
|
|
|
214
223
|
);
|
|
215
224
|
fs.chmodSync(path.join(binDest, "qualia-ui.js"), 0o755);
|
|
216
225
|
ok("qualia-ui.js (cosmetics library)");
|
|
226
|
+
copy(
|
|
227
|
+
path.join(FRAMEWORK_DIR, "bin", "statusline.js"),
|
|
228
|
+
path.join(binDest, "statusline.js")
|
|
229
|
+
);
|
|
230
|
+
ok("statusline.js (status bar renderer)");
|
|
217
231
|
} catch (e) {
|
|
218
232
|
warn(`scripts — ${e.message}`);
|
|
219
233
|
}
|
|
@@ -293,7 +307,7 @@ async function main() {
|
|
|
293
307
|
// Status line
|
|
294
308
|
settings.statusLine = {
|
|
295
309
|
type: "command",
|
|
296
|
-
command: "
|
|
310
|
+
command: `node "${path.join(CLAUDE_DIR, "bin", "statusline.js")}"`,
|
|
297
311
|
};
|
|
298
312
|
|
|
299
313
|
// Spinner
|
|
@@ -331,8 +345,11 @@ async function main() {
|
|
|
331
345
|
],
|
|
332
346
|
};
|
|
333
347
|
|
|
334
|
-
// Hooks —
|
|
348
|
+
// Hooks — pure Node.js, cross-platform (Windows/macOS/Linux).
|
|
349
|
+
// Every hook command is `node <absolute-path-to-hook.js>` which avoids the
|
|
350
|
+
// bash/Git Bash requirement on Windows.
|
|
335
351
|
const hd = path.join(CLAUDE_DIR, "hooks");
|
|
352
|
+
const nodeCmd = (hookFile) => `node "${path.join(hd, hookFile)}"`;
|
|
336
353
|
settings.hooks = {
|
|
337
354
|
SessionStart: [
|
|
338
355
|
{
|
|
@@ -340,7 +357,7 @@ async function main() {
|
|
|
340
357
|
hooks: [
|
|
341
358
|
{
|
|
342
359
|
type: "command",
|
|
343
|
-
command:
|
|
360
|
+
command: nodeCmd("session-start.js"),
|
|
344
361
|
timeout: 5,
|
|
345
362
|
},
|
|
346
363
|
],
|
|
@@ -352,28 +369,28 @@ async function main() {
|
|
|
352
369
|
hooks: [
|
|
353
370
|
{
|
|
354
371
|
type: "command",
|
|
355
|
-
command:
|
|
372
|
+
command: nodeCmd("auto-update.js"),
|
|
356
373
|
timeout: 5,
|
|
357
374
|
},
|
|
358
375
|
{
|
|
359
376
|
type: "command",
|
|
360
377
|
if: "Bash(git push*)",
|
|
361
|
-
command:
|
|
378
|
+
command: nodeCmd("branch-guard.js"),
|
|
362
379
|
timeout: 10,
|
|
363
380
|
statusMessage: "◆ Checking branch permissions...",
|
|
364
381
|
},
|
|
365
382
|
{
|
|
366
383
|
type: "command",
|
|
367
384
|
if: "Bash(git push*)",
|
|
368
|
-
command:
|
|
385
|
+
command: nodeCmd("pre-push.js"),
|
|
369
386
|
timeout: 15,
|
|
370
387
|
statusMessage: "◆ Syncing tracking...",
|
|
371
388
|
},
|
|
372
389
|
{
|
|
373
390
|
type: "command",
|
|
374
391
|
if: "Bash(vercel --prod*)",
|
|
375
|
-
command:
|
|
376
|
-
timeout:
|
|
392
|
+
command: nodeCmd("pre-deploy-gate.js"),
|
|
393
|
+
timeout: 180,
|
|
377
394
|
statusMessage: "◆ Running quality gates...",
|
|
378
395
|
},
|
|
379
396
|
],
|
|
@@ -384,14 +401,14 @@ async function main() {
|
|
|
384
401
|
{
|
|
385
402
|
type: "command",
|
|
386
403
|
if: "Edit(*.env*)|Write(*.env*)",
|
|
387
|
-
command:
|
|
404
|
+
command: nodeCmd("block-env-edit.js"),
|
|
388
405
|
timeout: 5,
|
|
389
406
|
statusMessage: "◆ Checking file permissions...",
|
|
390
407
|
},
|
|
391
408
|
{
|
|
392
409
|
type: "command",
|
|
393
410
|
if: "Edit(*migration*)|Write(*migration*)|Edit(*.sql)|Write(*.sql)",
|
|
394
|
-
command:
|
|
411
|
+
command: nodeCmd("migration-guard.js"),
|
|
395
412
|
timeout: 10,
|
|
396
413
|
statusMessage: "◆ Checking migration safety...",
|
|
397
414
|
},
|
|
@@ -404,25 +421,13 @@ async function main() {
|
|
|
404
421
|
hooks: [
|
|
405
422
|
{
|
|
406
423
|
type: "command",
|
|
407
|
-
command:
|
|
424
|
+
command: nodeCmd("pre-compact.js"),
|
|
408
425
|
timeout: 15,
|
|
409
426
|
statusMessage: "◆ Saving state...",
|
|
410
427
|
},
|
|
411
428
|
],
|
|
412
429
|
},
|
|
413
430
|
],
|
|
414
|
-
SubagentStart: [
|
|
415
|
-
{
|
|
416
|
-
matcher: ".*",
|
|
417
|
-
hooks: [
|
|
418
|
-
{
|
|
419
|
-
type: "command",
|
|
420
|
-
command:
|
|
421
|
-
'echo \'{"additionalContext": "◆ Qualia agent spawned"}\'',
|
|
422
|
-
},
|
|
423
|
-
],
|
|
424
|
-
},
|
|
425
|
-
],
|
|
426
431
|
};
|
|
427
432
|
|
|
428
433
|
// Permissions
|
|
@@ -436,8 +441,6 @@ async function main() {
|
|
|
436
441
|
];
|
|
437
442
|
}
|
|
438
443
|
|
|
439
|
-
settings.effortLevel = "high";
|
|
440
|
-
|
|
441
444
|
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
442
445
|
|
|
443
446
|
ok("Hooks: session-start, auto-update, branch-guard, pre-push, env-block, migration-guard, deploy-gate, pre-compact");
|
|
@@ -454,7 +457,7 @@ async function main() {
|
|
|
454
457
|
console.log(` Agents: ${WHITE}${agentCount}${RESET} ${DIM}(planner, builder, verifier, qa-browser)${RESET}`);
|
|
455
458
|
console.log(` Hooks: ${WHITE}8${RESET} ${DIM}(session-start, auto-update, branch-guard, pre-push, env-block, migration-guard, deploy-gate, pre-compact)${RESET}`);
|
|
456
459
|
console.log(` Rules: ${WHITE}${fs.readdirSync(rulesDir).length}${RESET} ${DIM}(security, frontend, design-reference, deployment)${RESET}`);
|
|
457
|
-
console.log(` Scripts: ${WHITE}
|
|
460
|
+
console.log(` Scripts: ${WHITE}3${RESET} ${DIM}(state.js, qualia-ui.js, statusline.js)${RESET}`);
|
|
458
461
|
console.log(` Knowledge: ${WHITE}3${RESET} ${DIM}(patterns, fixes, client prefs)${RESET}`);
|
|
459
462
|
console.log(` Templates: ${WHITE}${fs.readdirSync(tmplDir).length}${RESET}`);
|
|
460
463
|
console.log(` Status line: ${GREEN}✓${RESET}`);
|
package/bin/qualia-ui.js
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
const fs = require("fs");
|
|
21
21
|
const path = require("path");
|
|
22
22
|
const os = require("os");
|
|
23
|
-
const {
|
|
23
|
+
const { spawnSync } = require("child_process");
|
|
24
24
|
|
|
25
25
|
// ─── Colors ──────────────────────────────────────────────
|
|
26
26
|
const TEAL = "\x1b[38;2;0;206;209m";
|
|
@@ -64,11 +64,14 @@ const ACTIONS = {
|
|
|
64
64
|
// ─── State Reading ───────────────────────────────────────
|
|
65
65
|
function readState() {
|
|
66
66
|
try {
|
|
67
|
-
const
|
|
67
|
+
const statePath = path.join(os.homedir(), ".claude", "bin", "state.js");
|
|
68
|
+
const r = spawnSync(process.execPath, [statePath, "check"], {
|
|
68
69
|
encoding: "utf8",
|
|
69
70
|
timeout: 3000,
|
|
71
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
70
72
|
});
|
|
71
|
-
|
|
73
|
+
if (r.status !== 0 || !r.stdout) return null;
|
|
74
|
+
return JSON.parse(r.stdout);
|
|
72
75
|
} catch {
|
|
73
76
|
return null;
|
|
74
77
|
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Qualia status line — teal branded, shows phase + context + git.
|
|
3
|
+
// Pure Node.js port of the original statusline.sh. Cross-platform
|
|
4
|
+
// (Windows/macOS/Linux). No jq, no GNU stat, no /tmp hardcoding.
|
|
5
|
+
//
|
|
6
|
+
// Reads JSON from stdin (Claude Code status line schema), prints
|
|
7
|
+
// two ANSI-formatted lines to stdout. Never throws — every section
|
|
8
|
+
// is wrapped so missing data degrades gracefully.
|
|
9
|
+
|
|
10
|
+
const fs = require("fs");
|
|
11
|
+
const os = require("os");
|
|
12
|
+
const path = require("path");
|
|
13
|
+
const { spawnSync } = require("child_process");
|
|
14
|
+
|
|
15
|
+
// ─── Colors (matches bin/qualia-ui.js palette) ───────────
|
|
16
|
+
const TEAL = "\x1b[38;2;0;206;209m";
|
|
17
|
+
const TEAL_GLOW = "\x1b[38;2;0;170;175m";
|
|
18
|
+
const TEAL_DIM = "\x1b[38;2;0;130;135m";
|
|
19
|
+
const WHITE = "\x1b[38;2;220;225;230m";
|
|
20
|
+
const DIM = "\x1b[38;2;80;90;100m";
|
|
21
|
+
const GREEN = "\x1b[38;2;52;211;153m";
|
|
22
|
+
const YELLOW = "\x1b[38;2;234;179;8m";
|
|
23
|
+
const RED = "\x1b[38;2;239;68;68m";
|
|
24
|
+
const RESET = "\x1b[0m";
|
|
25
|
+
|
|
26
|
+
// ─── Read input ──────────────────────────────────────────
|
|
27
|
+
function readInput() {
|
|
28
|
+
try {
|
|
29
|
+
const raw = fs.readFileSync(0, "utf8");
|
|
30
|
+
return JSON.parse(raw);
|
|
31
|
+
} catch {
|
|
32
|
+
return {};
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const input = readInput();
|
|
37
|
+
|
|
38
|
+
function pick(obj, keypath, fallback) {
|
|
39
|
+
try {
|
|
40
|
+
let cur = obj;
|
|
41
|
+
for (const k of keypath.split(".")) {
|
|
42
|
+
if (cur == null) return fallback;
|
|
43
|
+
cur = cur[k];
|
|
44
|
+
}
|
|
45
|
+
return cur == null ? fallback : cur;
|
|
46
|
+
} catch {
|
|
47
|
+
return fallback;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const MODEL = String(pick(input, "model.display_name", ""));
|
|
52
|
+
const DIR = String(pick(input, "workspace.current_dir", process.cwd()));
|
|
53
|
+
const PCT_RAW = Number(pick(input, "context_window.used_percentage", 0)) || 0;
|
|
54
|
+
const PCT = Math.floor(PCT_RAW);
|
|
55
|
+
const COST = Number(pick(input, "cost.total_cost_usd", 0)) || 0;
|
|
56
|
+
const DURATION_MS = Number(pick(input, "cost.total_duration_ms", 0)) || 0;
|
|
57
|
+
const AGENT = String(pick(input, "agent.name", "") || "");
|
|
58
|
+
const WORKTREE = String(pick(input, "worktree.name", "") || "");
|
|
59
|
+
|
|
60
|
+
// ─── Context bar ─────────────────────────────────────────
|
|
61
|
+
let BAR = "";
|
|
62
|
+
let BAR_COLOR = TEAL;
|
|
63
|
+
try {
|
|
64
|
+
if (PCT >= 80) BAR_COLOR = RED;
|
|
65
|
+
else if (PCT >= 50) BAR_COLOR = YELLOW;
|
|
66
|
+
else BAR_COLOR = TEAL;
|
|
67
|
+
|
|
68
|
+
const BAR_WIDTH = 10;
|
|
69
|
+
const filled = Math.max(0, Math.min(BAR_WIDTH, Math.floor((PCT * BAR_WIDTH) / 100)));
|
|
70
|
+
const empty = BAR_WIDTH - filled;
|
|
71
|
+
BAR = "━".repeat(filled) + "╌".repeat(empty);
|
|
72
|
+
} catch {
|
|
73
|
+
BAR = "╌".repeat(10);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ─── Git branch (cached, cross-platform) ─────────────────
|
|
77
|
+
let BRANCH = "";
|
|
78
|
+
let CHANGES = 0;
|
|
79
|
+
try {
|
|
80
|
+
const username = (os.userInfo().username || "anon").replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
81
|
+
const cacheFile = path.join(os.tmpdir(), `qualia-git-cache-${username}`);
|
|
82
|
+
|
|
83
|
+
let fresh = false;
|
|
84
|
+
try {
|
|
85
|
+
const st = fs.statSync(cacheFile);
|
|
86
|
+
if (Date.now() - st.mtimeMs <= 3000) fresh = true;
|
|
87
|
+
} catch {
|
|
88
|
+
fresh = false;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (!fresh) {
|
|
92
|
+
let branch = "";
|
|
93
|
+
let changes = 0;
|
|
94
|
+
try {
|
|
95
|
+
const dirCheck = spawnSync("git", ["rev-parse", "--git-dir"], {
|
|
96
|
+
cwd: DIR,
|
|
97
|
+
encoding: "utf8",
|
|
98
|
+
timeout: 1000,
|
|
99
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
100
|
+
});
|
|
101
|
+
if (dirCheck.status === 0) {
|
|
102
|
+
const br = spawnSync("git", ["branch", "--show-current"], {
|
|
103
|
+
cwd: DIR,
|
|
104
|
+
encoding: "utf8",
|
|
105
|
+
timeout: 1000,
|
|
106
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
107
|
+
});
|
|
108
|
+
if (br.status === 0) branch = (br.stdout || "").trim();
|
|
109
|
+
|
|
110
|
+
const st = spawnSync("git", ["status", "--porcelain"], {
|
|
111
|
+
cwd: DIR,
|
|
112
|
+
encoding: "utf8",
|
|
113
|
+
timeout: 1000,
|
|
114
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
115
|
+
});
|
|
116
|
+
if (st.status === 0) {
|
|
117
|
+
const out = (st.stdout || "").trim();
|
|
118
|
+
changes = out ? out.split("\n").length : 0;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
} catch {}
|
|
122
|
+
try {
|
|
123
|
+
fs.writeFileSync(cacheFile, `${branch}|${changes}`);
|
|
124
|
+
} catch {}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
const cached = fs.readFileSync(cacheFile, "utf8");
|
|
129
|
+
const [b, c] = cached.split("|");
|
|
130
|
+
BRANCH = b || "";
|
|
131
|
+
CHANGES = parseInt(c, 10) || 0;
|
|
132
|
+
} catch {}
|
|
133
|
+
} catch {}
|
|
134
|
+
|
|
135
|
+
// ─── Phase info from .planning/tracking.json ─────────────
|
|
136
|
+
let PHASE_INFO = "";
|
|
137
|
+
try {
|
|
138
|
+
const trackingPath = path.join(DIR, ".planning", "tracking.json");
|
|
139
|
+
if (fs.existsSync(trackingPath)) {
|
|
140
|
+
const tracking = JSON.parse(fs.readFileSync(trackingPath, "utf8"));
|
|
141
|
+
const phase = Number(tracking.phase || 0) || 0;
|
|
142
|
+
const total = Number(tracking.total_phases || 0) || 0;
|
|
143
|
+
const status = String(tracking.status || "");
|
|
144
|
+
if (total > 0) {
|
|
145
|
+
const pdone = Math.floor((phase * 100) / total);
|
|
146
|
+
const pfill = Math.max(0, Math.min(4, Math.floor(pdone / 25)));
|
|
147
|
+
const pempt = 4 - pfill;
|
|
148
|
+
const pbar = "●".repeat(pfill) + "○".repeat(pempt);
|
|
149
|
+
PHASE_INFO = `${TEAL}${pbar}${RESET} ${WHITE}P${phase}/${total}${RESET} ${TEAL_GLOW}${status}${RESET}`;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
} catch {}
|
|
153
|
+
|
|
154
|
+
// ─── Duration ────────────────────────────────────────────
|
|
155
|
+
let DUR = "0s";
|
|
156
|
+
try {
|
|
157
|
+
if (DURATION_MS >= 60000) {
|
|
158
|
+
DUR = `${Math.floor(DURATION_MS / 60000)}m`;
|
|
159
|
+
} else {
|
|
160
|
+
DUR = `${Math.floor(DURATION_MS / 1000)}s`;
|
|
161
|
+
}
|
|
162
|
+
} catch {}
|
|
163
|
+
|
|
164
|
+
// ─── Cost ────────────────────────────────────────────────
|
|
165
|
+
let COST_FMT = "$0.00";
|
|
166
|
+
try {
|
|
167
|
+
COST_FMT = `$${COST.toFixed(2)}`;
|
|
168
|
+
} catch {}
|
|
169
|
+
|
|
170
|
+
// ─── Line 1: Project + Git + Agent + Worktree + Phase ────
|
|
171
|
+
let LINE1 = "";
|
|
172
|
+
try {
|
|
173
|
+
const dirBase = path.basename(DIR) || DIR;
|
|
174
|
+
LINE1 = `${TEAL}◆${RESET} ${WHITE}${dirBase}${RESET}`;
|
|
175
|
+
if (BRANCH) {
|
|
176
|
+
if (CHANGES > 0) {
|
|
177
|
+
LINE1 += ` ${DIM}on${RESET} ${TEAL_GLOW}${BRANCH}${RESET} ${YELLOW}~${CHANGES}${RESET}`;
|
|
178
|
+
} else {
|
|
179
|
+
LINE1 += ` ${DIM}on${RESET} ${TEAL_GLOW}${BRANCH}${RESET}`;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (AGENT) LINE1 += ` ${DIM}│${RESET} ${TEAL}⚡${AGENT}${RESET}`;
|
|
183
|
+
if (WORKTREE) LINE1 += ` ${DIM}│${RESET} ${TEAL_DIM}⎇ ${WORKTREE}${RESET}`;
|
|
184
|
+
if (PHASE_INFO) LINE1 += ` ${DIM}│${RESET} ${PHASE_INFO}`;
|
|
185
|
+
} catch {
|
|
186
|
+
LINE1 = `${TEAL}◆${RESET} ${WHITE}qualia${RESET}`;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ─── Line 2: Context bar + Cost + Duration + Model ───────
|
|
190
|
+
let LINE2 = "";
|
|
191
|
+
try {
|
|
192
|
+
LINE2 =
|
|
193
|
+
`${BAR_COLOR}${BAR}${RESET} ${DIM}${PCT}%${RESET} ` +
|
|
194
|
+
`${DIM}│${RESET} ${DIM}${COST_FMT}${RESET} ` +
|
|
195
|
+
`${DIM}│${RESET} ${DIM}${DUR}${RESET} ` +
|
|
196
|
+
`${DIM}│${RESET} ${TEAL_DIM}${MODEL}${RESET}`;
|
|
197
|
+
} catch {
|
|
198
|
+
LINE2 = `${DIM}${PCT}%${RESET}`;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
process.stdout.write(LINE1 + "\n");
|
|
202
|
+
process.stdout.write(LINE2 + "\n");
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// ~/.claude/hooks/auto-update.js — daily silent update check in the background.
|
|
3
|
+
// PreToolUse hook on every Bash tool call. Fast path: single stat() call that
|
|
4
|
+
// returns immediately if last check was <24h ago. Cross-platform.
|
|
5
|
+
|
|
6
|
+
const fs = require("fs");
|
|
7
|
+
const path = require("path");
|
|
8
|
+
const os = require("os");
|
|
9
|
+
const { spawn, spawnSync } = require("child_process");
|
|
10
|
+
|
|
11
|
+
const CLAUDE_DIR = path.join(os.homedir(), ".claude");
|
|
12
|
+
const CACHE_FILE = path.join(CLAUDE_DIR, ".qualia-last-update-check");
|
|
13
|
+
const CONFIG_FILE = path.join(CLAUDE_DIR, ".qualia-config.json");
|
|
14
|
+
const LOCK_FILE = path.join(CLAUDE_DIR, ".qualia-updating");
|
|
15
|
+
const MAX_AGE_MS = 24 * 60 * 60 * 1000;
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
// Fast path: recently checked
|
|
19
|
+
if (fs.existsSync(CACHE_FILE)) {
|
|
20
|
+
const last = Number(fs.readFileSync(CACHE_FILE, "utf8")) || 0;
|
|
21
|
+
if (Date.now() - last * 1000 < MAX_AGE_MS) {
|
|
22
|
+
process.exit(0);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Already updating
|
|
27
|
+
if (fs.existsSync(LOCK_FILE)) {
|
|
28
|
+
process.exit(0);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Update cache timestamp immediately to debounce concurrent checks
|
|
32
|
+
fs.writeFileSync(CACHE_FILE, String(Math.floor(Date.now() / 1000)));
|
|
33
|
+
|
|
34
|
+
// Read current config
|
|
35
|
+
let cfg = {};
|
|
36
|
+
try {
|
|
37
|
+
cfg = JSON.parse(fs.readFileSync(CONFIG_FILE, "utf8"));
|
|
38
|
+
} catch {
|
|
39
|
+
process.exit(0);
|
|
40
|
+
}
|
|
41
|
+
if (!cfg.code || !cfg.version) process.exit(0);
|
|
42
|
+
|
|
43
|
+
// Fork the check-and-update into a detached background process so the hook
|
|
44
|
+
// returns immediately and Claude Code is never blocked.
|
|
45
|
+
const script = `
|
|
46
|
+
const fs = require("fs");
|
|
47
|
+
const path = require("path");
|
|
48
|
+
const { spawnSync } = require("child_process");
|
|
49
|
+
const CLAUDE_DIR = ${JSON.stringify(CLAUDE_DIR)};
|
|
50
|
+
const LOCK_FILE = ${JSON.stringify(LOCK_FILE)};
|
|
51
|
+
const CONFIG_FILE = ${JSON.stringify(CONFIG_FILE)};
|
|
52
|
+
const cfg = ${JSON.stringify(cfg)};
|
|
53
|
+
try {
|
|
54
|
+
fs.writeFileSync(LOCK_FILE, String(process.pid));
|
|
55
|
+
const r = spawnSync("npm", ["view", "qualia-framework-v2", "version"], {
|
|
56
|
+
encoding: "utf8",
|
|
57
|
+
timeout: 15000,
|
|
58
|
+
shell: process.platform === "win32",
|
|
59
|
+
});
|
|
60
|
+
const latest = ((r.stdout || "").trim());
|
|
61
|
+
if (!latest) { fs.unlinkSync(LOCK_FILE); return; }
|
|
62
|
+
const cmp = (a, b) => {
|
|
63
|
+
const pa = a.split(".").map(Number), pb = b.split(".").map(Number);
|
|
64
|
+
for (let i = 0; i < 3; i++) {
|
|
65
|
+
if ((pa[i]||0) > (pb[i]||0)) return 1;
|
|
66
|
+
if ((pa[i]||0) < (pb[i]||0)) return -1;
|
|
67
|
+
}
|
|
68
|
+
return 0;
|
|
69
|
+
};
|
|
70
|
+
if (cmp(latest, cfg.version) > 0) {
|
|
71
|
+
// Silent update — pipe the install code via stdin
|
|
72
|
+
const child = spawnSync("npx", ["qualia-framework-v2@latest", "install"], {
|
|
73
|
+
input: cfg.code + "\\n",
|
|
74
|
+
timeout: 120000,
|
|
75
|
+
stdio: ["pipe", "ignore", "ignore"],
|
|
76
|
+
shell: process.platform === "win32",
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
} catch {}
|
|
80
|
+
try { fs.unlinkSync(LOCK_FILE); } catch {}
|
|
81
|
+
`;
|
|
82
|
+
|
|
83
|
+
const child = spawn(process.execPath, ["-e", script], {
|
|
84
|
+
detached: true,
|
|
85
|
+
stdio: "ignore",
|
|
86
|
+
});
|
|
87
|
+
child.unref();
|
|
88
|
+
} catch {
|
|
89
|
+
// Silent — never block the tool call
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
process.exit(0);
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// ~/.claude/hooks/block-env-edit.js — prevent editing .env files.
|
|
3
|
+
// PreToolUse hook on Edit/Write tool calls. Reads tool input as JSON on stdin.
|
|
4
|
+
// Exits 2 to BLOCK the tool call. Exits 0 to allow it.
|
|
5
|
+
// Cross-platform (Windows/macOS/Linux).
|
|
6
|
+
|
|
7
|
+
const fs = require("fs");
|
|
8
|
+
|
|
9
|
+
function readInput() {
|
|
10
|
+
try {
|
|
11
|
+
const raw = fs.readFileSync(0, "utf8");
|
|
12
|
+
return JSON.parse(raw);
|
|
13
|
+
} catch {
|
|
14
|
+
return {};
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const input = readInput();
|
|
19
|
+
const file = (input.tool_input && (input.tool_input.file_path || input.tool_input.command)) || "";
|
|
20
|
+
|
|
21
|
+
// Match .env, .env.local, .env.production, .env.*, etc.
|
|
22
|
+
// Normalize separators so Windows paths (C:\project\.env.local) also match.
|
|
23
|
+
const normalized = String(file).replace(/\\/g, "/");
|
|
24
|
+
|
|
25
|
+
if (/\.env(\.|$)/.test(normalized)) {
|
|
26
|
+
console.log("BLOCKED: Cannot edit environment files. Ask Fawzi to update secrets.");
|
|
27
|
+
process.exit(2);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
process.exit(0);
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// ~/.claude/hooks/branch-guard.js — block non-OWNER push to main/master.
|
|
3
|
+
// PreToolUse hook on `git push*` commands. Reads role from
|
|
4
|
+
// ~/.claude/.qualia-config.json (single source of truth).
|
|
5
|
+
// Exits 1 to BLOCK. Exits 0 to allow.
|
|
6
|
+
// Cross-platform (Windows/macOS/Linux).
|
|
7
|
+
|
|
8
|
+
const fs = require("fs");
|
|
9
|
+
const path = require("path");
|
|
10
|
+
const os = require("os");
|
|
11
|
+
const { spawnSync } = require("child_process");
|
|
12
|
+
|
|
13
|
+
const CONFIG = path.join(os.homedir(), ".claude", ".qualia-config.json");
|
|
14
|
+
|
|
15
|
+
function fail(msg) {
|
|
16
|
+
console.log(msg);
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let role = "";
|
|
21
|
+
try {
|
|
22
|
+
const cfg = JSON.parse(fs.readFileSync(CONFIG, "utf8"));
|
|
23
|
+
role = cfg.role || "";
|
|
24
|
+
} catch {
|
|
25
|
+
fail(`BLOCKED: ${CONFIG} missing or unreadable. Run: npx qualia-framework-v2 install`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!role) {
|
|
29
|
+
fail(`BLOCKED: Cannot determine role from ${CONFIG}. Defaulting to deny.`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Ask git for the current branch --show-current. Works identically on Windows/macOS/Linux.
|
|
33
|
+
const r = spawnSync("git", ["branch", "--show-current"], {
|
|
34
|
+
encoding: "utf8",
|
|
35
|
+
timeout: 3000,
|
|
36
|
+
});
|
|
37
|
+
const branch = ((r.stdout || "").trim());
|
|
38
|
+
|
|
39
|
+
if (branch === "main" || branch === "master") {
|
|
40
|
+
if (role !== "OWNER") {
|
|
41
|
+
console.log(`BLOCKED: Employees cannot push to ${branch}. Create a feature branch first.`);
|
|
42
|
+
console.log("Run: git checkout -b feature/your-feature-name");
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
process.exit(0);
|