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/README.md +52 -13
- package/agents/builder.md +22 -5
- package/agents/planner.md +13 -1
- package/agents/verifier.md +40 -0
- package/bin/cli.js +20 -0
- package/bin/install.js +352 -207
- package/hooks/block-env-edit.sh +5 -2
- package/hooks/branch-guard.sh +5 -0
- package/hooks/migration-guard.sh +43 -0
- package/hooks/pre-deploy-gate.sh +18 -0
- package/hooks/pre-push.sh +10 -4
- package/package.json +7 -4
- package/skills/qualia/SKILL.md +7 -1
- package/skills/qualia-build/SKILL.md +1 -1
- package/skills/qualia-plan/SKILL.md +14 -1
- package/skills/qualia-quick/SKILL.md +1 -1
- package/skills/qualia-task/SKILL.md +92 -0
- package/skills/qualia-verify/SKILL.md +1 -1
- package/tests/hooks.test.sh +144 -0
- package/install.sh +0 -223
package/bin/install.js
CHANGED
|
@@ -1,248 +1,393 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
const {
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
//
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
57
|
-
log(
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
65
|
-
log(`${WHITE}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
//
|
|
73
|
-
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
copy(
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
//
|
|
102
|
-
log(`${WHITE}
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
//
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
//
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
if (fs.existsSync(settingsPath)) {
|
|
150
|
+
// ─── Status line ───────────────────────────────────────
|
|
151
|
+
log(`${WHITE}Status line${RESET}`);
|
|
117
152
|
try {
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
//
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
//
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
"
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
//
|
|
161
|
-
|
|
162
|
-
settings.
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
+
});
|
package/hooks/block-env-edit.sh
CHANGED
|
@@ -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
|
|
10
|
+
exit 2
|
|
8
11
|
fi
|
package/hooks/branch-guard.sh
CHANGED
|
@@ -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."
|