qualia-framework-v2 2.0.0 → 2.1.1

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/bin/install.js CHANGED
@@ -1,248 +1,393 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const { execSync } = require("child_process");
3
+ const { createInterface } = require("readline");
4
4
  const path = require("path");
5
5
  const fs = require("fs");
6
6
 
7
+ // ─── Colors ──────────────────────────────────────────────
7
8
  const TEAL = "\x1b[38;2;0;206;209m";
8
9
  const DIM = "\x1b[38;2;80;90;100m";
9
10
  const GREEN = "\x1b[38;2;52;211;153m";
10
11
  const WHITE = "\x1b[38;2;220;225;230m";
12
+ const YELLOW = "\x1b[38;2;234;179;8m";
13
+ const RED = "\x1b[38;2;239;68;68m";
11
14
  const RESET = "\x1b[0m";
12
15
 
16
+ // ─── Team codes ──────────────────────────────────────────
17
+ const TEAM = {
18
+ "QS-FAWZI-01": {
19
+ name: "Fawzi Goussous",
20
+ role: "OWNER",
21
+ description: "Company owner. Full access. Can push to main, approve deploys, edit secrets.",
22
+ },
23
+ "QS-HASAN-02": {
24
+ name: "Hasan",
25
+ role: "EMPLOYEE",
26
+ description: "Developer. Feature branches only. Cannot push to main or edit .env files.",
27
+ },
28
+ "QS-MOAYAD-03": {
29
+ name: "Moayad",
30
+ role: "EMPLOYEE",
31
+ description: "Developer. Feature branches only. Cannot push to main or edit .env files.",
32
+ },
33
+ };
34
+
13
35
  const CLAUDE_DIR = path.join(require("os").homedir(), ".claude");
14
36
  const FRAMEWORK_DIR = path.resolve(__dirname, "..");
15
37
 
38
+ let installed = 0;
39
+ let errors = 0;
40
+
16
41
  function log(msg) {
17
42
  console.log(` ${msg}`);
18
43
  }
19
-
44
+ function ok(label) {
45
+ installed++;
46
+ log(`${GREEN}✓${RESET} ${label}`);
47
+ }
48
+ function warn(label) {
49
+ errors++;
50
+ log(`${YELLOW}✗${RESET} ${label}`);
51
+ }
20
52
  function copy(src, dest) {
21
53
  const destDir = path.dirname(dest);
22
54
  if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true });
23
55
  fs.copyFileSync(src, dest);
24
56
  }
25
57
 
26
- function copyDir(src, dest) {
27
- if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
28
- for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
29
- const srcPath = path.join(src, entry.name);
30
- const destPath = path.join(dest, entry.name);
31
- if (entry.isDirectory()) copyDir(srcPath, destPath);
32
- else fs.copyFileSync(srcPath, destPath);
33
- }
58
+ // ─── Prompt for code ─────────────────────────────────────
59
+ function askCode() {
60
+ return new Promise((resolve) => {
61
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
62
+ console.log("");
63
+ console.log(`${TEAL} ◆ Qualia Framework v2${RESET}`);
64
+ console.log(`${DIM} ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}`);
65
+ console.log("");
66
+ rl.question(` ${WHITE}Enter install code:${RESET} `, (answer) => {
67
+ rl.close();
68
+ resolve(answer.trim());
69
+ });
70
+ });
34
71
  }
35
72
 
36
- // Banner
37
- console.log("");
38
- console.log(`${TEAL} Qualia Framework v2${RESET}`);
39
- console.log(`${DIM} ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}`);
40
- console.log(` Installing to ${WHITE}${CLAUDE_DIR}${RESET}`);
41
- console.log("");
42
-
43
- // Skills
44
- const skillsDir = path.join(FRAMEWORK_DIR, "skills");
45
- const skills = fs.readdirSync(skillsDir).filter((d) =>
46
- fs.statSync(path.join(skillsDir, d)).isDirectory()
47
- );
48
- log(`${WHITE}Skills${RESET}`);
49
- for (const skill of skills) {
50
- const src = path.join(skillsDir, skill, "SKILL.md");
51
- const dest = path.join(CLAUDE_DIR, "skills", skill, "SKILL.md");
52
- copy(src, dest);
53
- log(` ${GREEN}✓${RESET} ${skill}`);
54
- }
73
+ // ─── Main ────────────────────────────────────────────────
74
+ async function main() {
75
+ const code = await askCode();
76
+ const member = TEAM[code];
55
77
 
56
- // Agents
57
- log(`${WHITE}Agents${RESET}`);
58
- const agentsDir = path.join(FRAMEWORK_DIR, "agents");
59
- for (const file of fs.readdirSync(agentsDir)) {
60
- copy(path.join(agentsDir, file), path.join(CLAUDE_DIR, "agents", file));
61
- log(` ${GREEN}✓${RESET} ${file}`);
62
- }
78
+ if (!member) {
79
+ console.log("");
80
+ log(`${RED}✗${RESET} Invalid code. Get your install code from Fawzi.`);
81
+ console.log("");
82
+ process.exit(1);
83
+ }
63
84
 
64
- // Rules
65
- log(`${WHITE}Rules${RESET}`);
66
- const rulesDir = path.join(FRAMEWORK_DIR, "rules");
67
- for (const file of fs.readdirSync(rulesDir)) {
68
- copy(path.join(rulesDir, file), path.join(CLAUDE_DIR, "rules", file));
69
- log(` ${GREEN}✓${RESET} ${file}`);
70
- }
85
+ console.log("");
86
+ log(`${GREEN}✓${RESET} ${WHITE}${member.name}${RESET} ${DIM}(${member.role})${RESET}`);
87
+ console.log("");
88
+ log(`Installing to ${WHITE}${CLAUDE_DIR}${RESET}`);
89
+ console.log("");
71
90
 
72
- // Hooks
73
- log(`${WHITE}Hooks${RESET}`);
74
- const hooksDir = path.join(FRAMEWORK_DIR, "hooks");
75
- const hooksDest = path.join(CLAUDE_DIR, "hooks");
76
- if (!fs.existsSync(hooksDest)) fs.mkdirSync(hooksDest, { recursive: true });
77
- for (const file of fs.readdirSync(hooksDir)) {
78
- const dest = path.join(hooksDest, file);
79
- copy(path.join(hooksDir, file), dest);
80
- fs.chmodSync(dest, 0o755);
81
- log(` ${GREEN}✓${RESET} ${file}`);
82
- }
91
+ // ─── Skills ──────────────────────────────────────────
92
+ const skillsDir = path.join(FRAMEWORK_DIR, "skills");
93
+ const skills = fs
94
+ .readdirSync(skillsDir)
95
+ .filter((d) => fs.statSync(path.join(skillsDir, d)).isDirectory());
83
96
 
84
- // Status line
85
- log(`${WHITE}Status line${RESET}`);
86
- const slDest = path.join(CLAUDE_DIR, "statusline.sh");
87
- copy(path.join(FRAMEWORK_DIR, "statusline.sh"), slDest);
88
- fs.chmodSync(slDest, 0o755);
89
- log(` ${GREEN}✓${RESET} statusline.sh`);
90
-
91
- // Templates
92
- log(`${WHITE}Templates${RESET}`);
93
- const tmplDir = path.join(FRAMEWORK_DIR, "templates");
94
- const tmplDest = path.join(CLAUDE_DIR, "qualia-templates");
95
- if (!fs.existsSync(tmplDest)) fs.mkdirSync(tmplDest, { recursive: true });
96
- for (const file of fs.readdirSync(tmplDir)) {
97
- copy(path.join(tmplDir, file), path.join(tmplDest, file));
98
- log(` ${GREEN}✓${RESET} ${file}`);
99
- }
97
+ log(`${WHITE}Skills${RESET}`);
98
+ for (const skill of skills) {
99
+ try {
100
+ copy(
101
+ path.join(skillsDir, skill, "SKILL.md"),
102
+ path.join(CLAUDE_DIR, "skills", skill, "SKILL.md")
103
+ );
104
+ ok(skill);
105
+ } catch (e) {
106
+ warn(`${skill} ${e.message}`);
107
+ }
108
+ }
100
109
 
101
- // CLAUDE.md
102
- log(`${WHITE}CLAUDE.md${RESET}`);
103
- copy(path.join(FRAMEWORK_DIR, "CLAUDE.md"), path.join(CLAUDE_DIR, "CLAUDE.md"));
104
- log(` ${GREEN}✓${RESET} user-level instructions`);
110
+ // ─── Agents ────────────────────────────────────────────
111
+ log(`${WHITE}Agents${RESET}`);
112
+ const agentsDir = path.join(FRAMEWORK_DIR, "agents");
113
+ for (const file of fs.readdirSync(agentsDir)) {
114
+ try {
115
+ copy(path.join(agentsDir, file), path.join(CLAUDE_DIR, "agents", file));
116
+ ok(file);
117
+ } catch (e) {
118
+ warn(`${file} — ${e.message}`);
119
+ }
120
+ }
105
121
 
106
- // Guide
107
- copy(path.join(FRAMEWORK_DIR, "guide.md"), path.join(CLAUDE_DIR, "qualia-guide.md"));
108
- log(` ${GREEN}✓${RESET} guide.md`);
122
+ // ─── Rules ─────────────────────────────────────────────
123
+ log(`${WHITE}Rules${RESET}`);
124
+ const rulesDir = path.join(FRAMEWORK_DIR, "rules");
125
+ for (const file of fs.readdirSync(rulesDir)) {
126
+ try {
127
+ copy(path.join(rulesDir, file), path.join(CLAUDE_DIR, "rules", file));
128
+ ok(file);
129
+ } catch (e) {
130
+ warn(`${file} — ${e.message}`);
131
+ }
132
+ }
109
133
 
110
- // Configure settings.json
111
- console.log("");
112
- log(`${WHITE}Configuring settings.json...${RESET}`);
134
+ // ─── Hooks ─────────────────────────────────────────────
135
+ log(`${WHITE}Hooks${RESET}`);
136
+ const hooksSource = path.join(FRAMEWORK_DIR, "hooks");
137
+ const hooksDest = path.join(CLAUDE_DIR, "hooks");
138
+ if (!fs.existsSync(hooksDest)) fs.mkdirSync(hooksDest, { recursive: true });
139
+ for (const file of fs.readdirSync(hooksSource)) {
140
+ try {
141
+ const dest = path.join(hooksDest, file);
142
+ copy(path.join(hooksSource, file), dest);
143
+ fs.chmodSync(dest, 0o755);
144
+ ok(file);
145
+ } catch (e) {
146
+ warn(`${file} — ${e.message}`);
147
+ }
148
+ }
113
149
 
114
- const settingsPath = path.join(CLAUDE_DIR, "settings.json");
115
- let settings = {};
116
- if (fs.existsSync(settingsPath)) {
150
+ // ─── Status line ───────────────────────────────────────
151
+ log(`${WHITE}Status line${RESET}`);
117
152
  try {
118
- settings = JSON.parse(fs.readFileSync(settingsPath, "utf8"));
119
- } catch {}
120
- }
153
+ const slDest = path.join(CLAUDE_DIR, "statusline.sh");
154
+ copy(path.join(FRAMEWORK_DIR, "statusline.sh"), slDest);
155
+ fs.chmodSync(slDest, 0o755);
156
+ ok("statusline.sh");
157
+ } catch (e) {
158
+ warn(`statusline.sh — ${e.message}`);
159
+ }
121
160
 
122
- // Env
123
- if (!settings.env) settings.env = {};
124
- Object.assign(settings.env, {
125
- CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: "1",
126
- CLAUDE_CODE_DISABLE_AUTO_MEMORY: "0",
127
- MAX_MCP_OUTPUT_TOKENS: "25000",
128
- CLAUDE_CODE_NO_FLICKER: "1",
129
- });
161
+ // ─── Templates ─────────────────────────────────────────
162
+ log(`${WHITE}Templates${RESET}`);
163
+ const tmplDir = path.join(FRAMEWORK_DIR, "templates");
164
+ const tmplDest = path.join(CLAUDE_DIR, "qualia-templates");
165
+ if (!fs.existsSync(tmplDest)) fs.mkdirSync(tmplDest, { recursive: true });
166
+ for (const file of fs.readdirSync(tmplDir)) {
167
+ try {
168
+ copy(path.join(tmplDir, file), path.join(tmplDest, file));
169
+ ok(file);
170
+ } catch (e) {
171
+ warn(`${file} — ${e.message}`);
172
+ }
173
+ }
130
174
 
131
- // Status line
132
- settings.statusLine = { type: "command", command: "~/.claude/statusline.sh" };
133
-
134
- // Spinner
135
- settings.spinnerVerbs = {
136
- mode: "replace",
137
- verbs: [
138
- "Qualia-fying", "Solution-ing", "Teal-crafting", "Vibe-forging",
139
- "Shipping", "Wiring", "Polishing", "Verifying",
140
- "Orchestrating", "Architecting", "Deploying", "Hardening",
141
- ],
142
- };
175
+ // ─── CLAUDE.md with role ───────────────────────────────
176
+ log(`${WHITE}CLAUDE.md${RESET}`);
177
+ try {
178
+ let claudeMd = fs.readFileSync(
179
+ path.join(FRAMEWORK_DIR, "CLAUDE.md"),
180
+ "utf8"
181
+ );
182
+ claudeMd = claudeMd.replace("{{ROLE}}", member.role);
183
+ claudeMd = claudeMd.replace("{{ROLE_DESCRIPTION}}", member.description);
184
+ const claudeDest = path.join(CLAUDE_DIR, "CLAUDE.md");
185
+ fs.writeFileSync(claudeDest, claudeMd, "utf8");
186
+ ok(`Configured as ${member.role}`);
187
+ } catch (e) {
188
+ warn(`CLAUDE.md — ${e.message}`);
189
+ }
143
190
 
144
- settings.spinnerTipsOverride = {
145
- excludeDefault: true,
146
- tips: [
147
- "◆ Lost? Type /qualia for the next step",
148
- "◆ Small fix? Use /qualia-quick to skip planning",
149
- "◆ End of day? /qualia-report before you clock out",
150
- "◆ Context isolation: every task gets a fresh AI brain",
151
- "◆ The verifier doesn't trust claims — it greps the code",
152
- "◆ Plans are prompts the plan IS what the builder reads",
153
- "◆ Feature branches only — never push to main",
154
- "◆ Read before write — no exceptions",
155
- "◆ MVP first — build what's asked, nothing extra",
156
- "◆ tracking.json syncs to ERP on every push",
157
- ],
158
- };
191
+ // ─── Guide ─────────────────────────────────────────────
192
+ try {
193
+ copy(
194
+ path.join(FRAMEWORK_DIR, "guide.md"),
195
+ path.join(CLAUDE_DIR, "qualia-guide.md")
196
+ );
197
+ ok("guide.md");
198
+ } catch (e) {
199
+ warn(`guide.md${e.message}`);
200
+ }
159
201
 
160
- // Hooks
161
- const hd = path.join(CLAUDE_DIR, "hooks");
162
- settings.hooks = {
163
- PreToolUse: [
164
- {
165
- matcher: "Bash",
166
- hooks: [{
167
- type: "command",
168
- if: "Bash(git push*)",
169
- command: `${hd}/branch-guard.sh`,
170
- timeout: 10,
171
- statusMessage: "◆ Checking branch permissions...",
172
- }],
173
- },
174
- {
175
- matcher: "Edit|Write",
176
- hooks: [{
177
- type: "command",
178
- if: "Edit(*.env*)",
179
- command: `${hd}/block-env-edit.sh`,
180
- timeout: 5,
181
- statusMessage: "◆ Checking file permissions...",
182
- }],
183
- },
184
- ],
185
- PostToolUse: [
186
- {
187
- matcher: "Bash",
188
- hooks: [{
189
- type: "command",
190
- if: "Bash(vercel --prod*)",
191
- command: `${hd}/pre-deploy-gate.sh`,
192
- timeout: 120,
193
- statusMessage: "◆ Running quality gates...",
194
- }],
195
- },
196
- ],
197
- PreCompact: [
198
- {
199
- matcher: "compact",
200
- hooks: [{
201
- type: "command",
202
- command: `${hd}/pre-compact.sh`,
203
- timeout: 15,
204
- statusMessage: "◆ Saving state...",
205
- }],
206
- },
207
- ],
208
- SubagentStart: [
209
- {
210
- matcher: ".*",
211
- hooks: [{
212
- type: "command",
213
- command: 'echo \'{"additionalContext": "◆ Qualia agent spawned"}\'',
214
- }],
215
- },
216
- ],
217
- };
202
+ // ─── Configure settings.json ───────────────────────────
203
+ console.log("");
204
+ log(`${WHITE}Configuring settings.json...${RESET}`);
205
+
206
+ const settingsPath = path.join(CLAUDE_DIR, "settings.json");
207
+ let settings = {};
208
+ if (fs.existsSync(settingsPath)) {
209
+ try {
210
+ settings = JSON.parse(fs.readFileSync(settingsPath, "utf8"));
211
+ } catch {}
212
+ }
213
+
214
+ // Env
215
+ if (!settings.env) settings.env = {};
216
+ Object.assign(settings.env, {
217
+ CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: "1",
218
+ CLAUDE_CODE_DISABLE_AUTO_MEMORY: "0",
219
+ MAX_MCP_OUTPUT_TOKENS: "25000",
220
+ CLAUDE_CODE_NO_FLICKER: "1",
221
+ });
222
+
223
+ // Status line
224
+ settings.statusLine = {
225
+ type: "command",
226
+ command: "~/.claude/statusline.sh",
227
+ };
228
+
229
+ // Spinner
230
+ settings.spinnerVerbs = {
231
+ mode: "replace",
232
+ verbs: [
233
+ "Qualia-fying",
234
+ "Solution-ing",
235
+ "Teal-crafting",
236
+ "Vibe-forging",
237
+ "Shipping",
238
+ "Wiring",
239
+ "Polishing",
240
+ "Verifying",
241
+ "Orchestrating",
242
+ "Architecting",
243
+ "Deploying",
244
+ "Hardening",
245
+ ],
246
+ };
218
247
 
219
- // Permissions
220
- if (!settings.permissions) settings.permissions = {};
221
- if (!settings.permissions.allow) settings.permissions.allow = [];
222
- if (!settings.permissions.deny) {
223
- settings.permissions.deny = [
224
- "Read(./.env)",
225
- "Read(./.env.*)",
226
- "Read(./secrets/**)",
227
- ];
248
+ settings.spinnerTipsOverride = {
249
+ excludeDefault: true,
250
+ tips: [
251
+ "◆ Lost? Type /qualia for the next step",
252
+ "◆ Small fix? Use /qualia-quick to skip planning",
253
+ "◆ End of day? /qualia-report before you clock out",
254
+ "◆ Context isolation: every task gets a fresh AI brain",
255
+ "◆ The verifier doesn't trust claims — it greps the code",
256
+ "◆ Plans are prompts — the plan IS what the builder reads",
257
+ "◆ Feature branches only — never push to main",
258
+ "◆ Read before write — no exceptions",
259
+ "◆ MVP first — build what's asked, nothing extra",
260
+ "◆ tracking.json syncs to ERP on every push",
261
+ ],
262
+ };
263
+
264
+ // Hooks — full system
265
+ const hd = path.join(CLAUDE_DIR, "hooks");
266
+ settings.hooks = {
267
+ PreToolUse: [
268
+ {
269
+ matcher: "Bash",
270
+ hooks: [
271
+ {
272
+ type: "command",
273
+ if: "Bash(git push*)",
274
+ command: `${hd}/branch-guard.sh`,
275
+ timeout: 10,
276
+ statusMessage: "◆ Checking branch permissions...",
277
+ },
278
+ {
279
+ type: "command",
280
+ if: "Bash(git push*)",
281
+ command: `${hd}/pre-push.sh`,
282
+ timeout: 15,
283
+ statusMessage: "◆ Syncing tracking...",
284
+ },
285
+ ],
286
+ },
287
+ {
288
+ matcher: "Edit|Write",
289
+ hooks: [
290
+ {
291
+ type: "command",
292
+ if: "Edit(*.env*)|Write(*.env*)",
293
+ command: `${hd}/block-env-edit.sh`,
294
+ timeout: 5,
295
+ statusMessage: "◆ Checking file permissions...",
296
+ },
297
+ {
298
+ type: "command",
299
+ if: "Edit(*migration*)|Write(*migration*)|Edit(*.sql)|Write(*.sql)",
300
+ command: `${hd}/migration-guard.sh`,
301
+ timeout: 10,
302
+ statusMessage: "◆ Checking migration safety...",
303
+ },
304
+ ],
305
+ },
306
+ ],
307
+ PostToolUse: [
308
+ {
309
+ matcher: "Bash",
310
+ hooks: [
311
+ {
312
+ type: "command",
313
+ if: "Bash(vercel --prod*)",
314
+ command: `${hd}/pre-deploy-gate.sh`,
315
+ timeout: 120,
316
+ statusMessage: "◆ Running quality gates...",
317
+ },
318
+ ],
319
+ },
320
+ ],
321
+ PreCompact: [
322
+ {
323
+ matcher: "compact",
324
+ hooks: [
325
+ {
326
+ type: "command",
327
+ command: `${hd}/pre-compact.sh`,
328
+ timeout: 15,
329
+ statusMessage: "◆ Saving state...",
330
+ },
331
+ ],
332
+ },
333
+ ],
334
+ SubagentStart: [
335
+ {
336
+ matcher: ".*",
337
+ hooks: [
338
+ {
339
+ type: "command",
340
+ command:
341
+ 'echo \'{"additionalContext": "◆ Qualia agent spawned"}\'',
342
+ },
343
+ ],
344
+ },
345
+ ],
346
+ };
347
+
348
+ // Permissions
349
+ if (!settings.permissions) settings.permissions = {};
350
+ if (!settings.permissions.allow) settings.permissions.allow = [];
351
+ if (!settings.permissions.deny) {
352
+ settings.permissions.deny = [
353
+ "Read(./.env)",
354
+ "Read(./.env.*)",
355
+ "Read(./secrets/**)",
356
+ ];
357
+ }
358
+
359
+ settings.effortLevel = "high";
360
+
361
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
362
+
363
+ ok("Hooks: branch-guard, pre-push, env-block, migration-guard, deploy-gate, pre-compact");
364
+ ok("Status line + spinner configured");
365
+ ok("Environment variables + permissions");
366
+
367
+ // ─── Summary ───────────────────────────────────────────
368
+ console.log("");
369
+ console.log(`${TEAL} ◆ Installed ✓${RESET}`);
370
+ console.log(`${DIM} ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}`);
371
+ console.log(` ${WHITE}${member.name}${RESET} ${DIM}(${member.role})${RESET}`);
372
+ console.log(` Skills: ${WHITE}${skills.length}${RESET}`);
373
+ console.log(` Agents: ${WHITE}3${RESET} ${DIM}(planner, builder, verifier)${RESET}`);
374
+ console.log(` Hooks: ${WHITE}6${RESET} ${DIM}(branch-guard, pre-push, env-block, migration-guard, deploy-gate, pre-compact)${RESET}`);
375
+ console.log(` Rules: ${WHITE}3${RESET} ${DIM}(security, frontend, deployment)${RESET}`);
376
+ console.log(` Templates: ${WHITE}4${RESET}`);
377
+ console.log(` Status line: ${GREEN}✓${RESET}`);
378
+ console.log(` CLAUDE.md: ${GREEN}✓${RESET} ${DIM}(${member.role})${RESET}`);
379
+
380
+ if (errors > 0) {
381
+ console.log("");
382
+ console.log(` ${YELLOW}${errors} warning(s)${RESET} — check output above`);
383
+ }
384
+
385
+ console.log("");
386
+ console.log(` Restart Claude Code, then type ${TEAL}/qualia${RESET} in any project.`);
387
+ console.log("");
228
388
  }
229
389
 
230
- settings.effortLevel = "high";
231
-
232
- fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
233
- log(` ${GREEN}✓${RESET} hooks, status line, spinner, env vars`);
234
-
235
- // Done
236
- console.log("");
237
- console.log(`${TEAL} ◆ Installed ✓${RESET}`);
238
- console.log(`${DIM} ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}`);
239
- console.log(` Skills: ${WHITE}${skills.length}${RESET}`);
240
- console.log(` Agents: ${WHITE}3${RESET} ${DIM}(planner, builder, verifier)${RESET}`);
241
- console.log(` Hooks: ${WHITE}5${RESET} ${DIM}(branch-guard, env-block, deploy-gate, pre-compact, subagent-label)${RESET}`);
242
- console.log(` Rules: ${WHITE}3${RESET} ${DIM}(security, frontend, deployment)${RESET}`);
243
- console.log(` Templates: ${WHITE}4${RESET}`);
244
- console.log(` Status line: ${GREEN}✓${RESET}`);
245
- console.log(` CLAUDE.md: ${GREEN}✓${RESET} ${DIM}(user-level)${RESET}`);
246
- console.log("");
247
- console.log(` Restart Claude Code, then type ${TEAL}/qualia${RESET} in any project.`);
248
- console.log("");
390
+ main().catch((e) => {
391
+ console.error(`${RED} ✗ Installation failed: ${e.message}${RESET}`);
392
+ process.exit(1);
393
+ });
@@ -1,8 +1,11 @@
1
1
  #!/bin/bash
2
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)
3
7
 
4
- FILE="$1"
5
8
  if [[ "$FILE" == *.env* ]] || [[ "$FILE" == *".env.local"* ]] || [[ "$FILE" == *".env.production"* ]]; then
6
9
  echo "BLOCKED: Cannot edit environment files. Ask Fawzi to update secrets."
7
- exit 1
10
+ exit 2
8
11
  fi
@@ -4,6 +4,11 @@
4
4
  BRANCH=$(git branch --show-current 2>/dev/null)
5
5
  ROLE=$(grep -m1 "^## Role:" ~/.claude/CLAUDE.md 2>/dev/null | sed 's/^## Role: *//')
6
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
+
7
12
  if [[ "$BRANCH" == "main" || "$BRANCH" == "master" ]]; then
8
13
  if [[ "$ROLE" != "OWNER" ]]; then
9
14
  echo "BLOCKED: Employees cannot push to $BRANCH. Create a feature branch first."