qualia-framework-v2 2.8.1 → 2.10.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 +5 -0
- package/bin/cli.js +268 -6
- package/bin/install.js +96 -21
- package/bin/qualia-ui.js +26 -26
- package/bin/state.js +136 -2
- package/bin/statusline.js +53 -3
- package/hooks/migration-guard.js +1 -1
- package/hooks/pre-deploy-gate.js +2 -2
- package/package.json +2 -2
- package/skills/qualia-design/SKILL.md +1 -1
- package/skills/qualia-learn/SKILL.md +1 -1
- package/skills/qualia-new/SKILL.md +2 -2
- package/skills/qualia-review/SKILL.md +1 -1
- package/tests/bin.test.sh +673 -0
- package/tests/hooks.test.sh +155 -25
- package/tests/state.test.sh +137 -0
- package/tests/statusline.test.sh +243 -0
package/README.md
CHANGED
|
@@ -16,6 +16,7 @@ Enter your team code when prompted. Get your code from Fawzi.
|
|
|
16
16
|
```bash
|
|
17
17
|
npx qualia-framework-v2 version # Check installed version + updates
|
|
18
18
|
npx qualia-framework-v2 update # Update to latest (remembers your code)
|
|
19
|
+
npx qualia-framework-v2 uninstall # Clean removal from ~/.claude/
|
|
19
20
|
```
|
|
20
21
|
|
|
21
22
|
## Usage
|
|
@@ -115,4 +116,8 @@ npx qualia-framework-v2 install
|
|
|
115
116
|
|
|
116
117
|
Stack: Next.js 16+, React 19, TypeScript, Supabase, Vercel.
|
|
117
118
|
|
|
119
|
+
## Changelog
|
|
120
|
+
|
|
121
|
+
See [CHANGELOG.md](./CHANGELOG.md) for the full version history.
|
|
122
|
+
|
|
118
123
|
Built by [Qualia Solutions](https://qualiasolutions.net) — Nicosia, Cyprus.
|
package/bin/cli.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
const {
|
|
3
|
+
const { spawnSync } = require("child_process");
|
|
4
4
|
const path = require("path");
|
|
5
5
|
const fs = require("fs");
|
|
6
|
+
const readline = require("readline");
|
|
6
7
|
|
|
7
8
|
const TEAL = "\x1b[38;2;0;206;209m";
|
|
8
9
|
const TG = "\x1b[38;2;0;170;175m";
|
|
@@ -33,7 +34,7 @@ function writeConfig(cfg) {
|
|
|
33
34
|
|
|
34
35
|
function banner() {
|
|
35
36
|
console.log("");
|
|
36
|
-
console.log(` ${TEAL}${BOLD}
|
|
37
|
+
console.log(` ${TEAL}${BOLD}⬢${RESET} ${WHITE}${BOLD}Qualia Framework${RESET} ${DIM}v${PKG.version}${RESET}`);
|
|
37
38
|
console.log(` ${DIM}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}`);
|
|
38
39
|
}
|
|
39
40
|
|
|
@@ -57,10 +58,17 @@ function cmdVersion() {
|
|
|
57
58
|
|
|
58
59
|
// Check for updates
|
|
59
60
|
try {
|
|
60
|
-
|
|
61
|
-
|
|
61
|
+
// spawnSync with argv — no bash-only `2>/dev/null` redirect, no shell
|
|
62
|
+
// interpolation. stdio: "ignore" on stderr silences any npm warnings
|
|
63
|
+
// (offline, proxy, etc.) without a shell redirect. shell: true on
|
|
64
|
+
// Windows because `npm` is a .cmd shim that only resolves through cmd.
|
|
65
|
+
const r = spawnSync("npm", ["view", "qualia-framework-v2", "version"], {
|
|
66
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
67
|
+
shell: process.platform === "win32",
|
|
62
68
|
timeout: 5000,
|
|
63
|
-
|
|
69
|
+
encoding: "utf8",
|
|
70
|
+
});
|
|
71
|
+
const latest = (r.stdout || "").trim();
|
|
64
72
|
const semverGt = (a, b) => {
|
|
65
73
|
const pa = a.split(".").map(Number), pb = b.split(".").map(Number);
|
|
66
74
|
for (let i = 0; i < 3; i++) { if (pa[i] > pb[i]) return true; if (pa[i] < pb[i]) return false; }
|
|
@@ -94,7 +102,6 @@ function cmdUpdate() {
|
|
|
94
102
|
console.log("");
|
|
95
103
|
|
|
96
104
|
try {
|
|
97
|
-
const { spawnSync } = require("child_process");
|
|
98
105
|
const r = spawnSync("npx", ["qualia-framework-v2@latest", "install"], {
|
|
99
106
|
input: cfg.code + "\n",
|
|
100
107
|
stdio: ["pipe", "inherit", "inherit"],
|
|
@@ -113,6 +120,253 @@ function cmdUpdate() {
|
|
|
113
120
|
}
|
|
114
121
|
}
|
|
115
122
|
|
|
123
|
+
// ─── Uninstall ───────────────────────────────────────────
|
|
124
|
+
// Surgical removal of the Qualia Framework from ~/.claude/.
|
|
125
|
+
// Preserves CLAUDE.md (user may have customized it) and preserves any
|
|
126
|
+
// non-Qualia entries in settings.json (other hooks, user env vars, etc.).
|
|
127
|
+
// --yes / -y skips the confirmation prompt for scripted use.
|
|
128
|
+
|
|
129
|
+
// 8 Qualia hook filenames — only these are removed from ~/.claude/hooks/,
|
|
130
|
+
// any other hooks the user dropped in there are left alone.
|
|
131
|
+
const QUALIA_HOOK_FILES = [
|
|
132
|
+
"session-start.js",
|
|
133
|
+
"auto-update.js",
|
|
134
|
+
"branch-guard.js",
|
|
135
|
+
"pre-push.js",
|
|
136
|
+
"block-env-edit.js",
|
|
137
|
+
"migration-guard.js",
|
|
138
|
+
"pre-deploy-gate.js",
|
|
139
|
+
"pre-compact.js",
|
|
140
|
+
];
|
|
141
|
+
|
|
142
|
+
// 4 Qualia agents — only these are removed.
|
|
143
|
+
const QUALIA_AGENT_FILES = ["planner.md", "builder.md", "verifier.md", "qa-browser.md"];
|
|
144
|
+
|
|
145
|
+
// 3 Qualia bin scripts.
|
|
146
|
+
const QUALIA_BIN_FILES = ["state.js", "qualia-ui.js", "statusline.js"];
|
|
147
|
+
|
|
148
|
+
// 4 Qualia rules.
|
|
149
|
+
const QUALIA_RULE_FILES = ["security.md", "frontend.md", "design-reference.md", "deployment.md"];
|
|
150
|
+
|
|
151
|
+
function promptYesNo(question, defaultYes) {
|
|
152
|
+
return new Promise((resolve) => {
|
|
153
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
154
|
+
const suffix = defaultYes ? " (Y/n)" : " (y/N)";
|
|
155
|
+
rl.question(` ${WHITE}${question}${RESET}${suffix} `, (answer) => {
|
|
156
|
+
rl.close();
|
|
157
|
+
const a = String(answer || "").trim().toLowerCase();
|
|
158
|
+
if (!a) return resolve(defaultYes);
|
|
159
|
+
resolve(a === "y" || a === "yes");
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function safeUnlink(p, counters) {
|
|
165
|
+
try {
|
|
166
|
+
if (fs.existsSync(p)) {
|
|
167
|
+
fs.unlinkSync(p);
|
|
168
|
+
counters.filesRemoved++;
|
|
169
|
+
}
|
|
170
|
+
} catch (e) {
|
|
171
|
+
counters.errors.push(`${p}: ${e.message}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function safeRmDir(p, counters) {
|
|
176
|
+
try {
|
|
177
|
+
if (fs.existsSync(p)) {
|
|
178
|
+
fs.rmSync(p, { recursive: true, force: true });
|
|
179
|
+
counters.dirsRemoved++;
|
|
180
|
+
}
|
|
181
|
+
} catch (e) {
|
|
182
|
+
counters.errors.push(`${p}: ${e.message}`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function cleanSettingsJson(counters) {
|
|
187
|
+
const settingsPath = path.join(CLAUDE_DIR, "settings.json");
|
|
188
|
+
if (!fs.existsSync(settingsPath)) return;
|
|
189
|
+
let settings;
|
|
190
|
+
try {
|
|
191
|
+
settings = JSON.parse(fs.readFileSync(settingsPath, "utf8"));
|
|
192
|
+
} catch (e) {
|
|
193
|
+
counters.errors.push(`settings.json: ${e.message}`);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Only remove entries that point at qualia paths. Leave everything else.
|
|
198
|
+
const isQualiaCommand = (cmd) =>
|
|
199
|
+
typeof cmd === "string" && (cmd.includes("qualia") || cmd.includes(".claude/hooks/") || cmd.includes(".claude/bin/"));
|
|
200
|
+
|
|
201
|
+
const filterHookArray = (arr) => {
|
|
202
|
+
if (!Array.isArray(arr)) return arr;
|
|
203
|
+
return arr
|
|
204
|
+
.map((entry) => {
|
|
205
|
+
if (!entry || !Array.isArray(entry.hooks)) return entry;
|
|
206
|
+
const hooks = entry.hooks.filter((h) => !isQualiaCommand(h && h.command));
|
|
207
|
+
return { ...entry, hooks };
|
|
208
|
+
})
|
|
209
|
+
.filter((entry) => Array.isArray(entry.hooks) && entry.hooks.length > 0);
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
if (settings.hooks && typeof settings.hooks === "object") {
|
|
213
|
+
for (const key of ["SessionStart", "PreToolUse", "PreCompact"]) {
|
|
214
|
+
if (settings.hooks[key]) {
|
|
215
|
+
const cleaned = filterHookArray(settings.hooks[key]);
|
|
216
|
+
if (cleaned && cleaned.length > 0) {
|
|
217
|
+
settings.hooks[key] = cleaned;
|
|
218
|
+
} else {
|
|
219
|
+
delete settings.hooks[key];
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
// If hooks is now empty, remove it entirely.
|
|
224
|
+
if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Status line — only drop it if it points at our renderer.
|
|
228
|
+
if (settings.statusLine && typeof settings.statusLine === "object") {
|
|
229
|
+
const cmd = settings.statusLine.command || "";
|
|
230
|
+
if (isQualiaCommand(cmd) || cmd.includes("statusline.js") || cmd.includes("qualia-ui")) {
|
|
231
|
+
delete settings.statusLine;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Qualia-specific spinner overrides.
|
|
236
|
+
if (settings.spinnerVerbs) delete settings.spinnerVerbs;
|
|
237
|
+
if (settings.spinnerTipsOverride) delete settings.spinnerTipsOverride;
|
|
238
|
+
|
|
239
|
+
// Leave settings.env alone — the user may have other env vars in there.
|
|
240
|
+
|
|
241
|
+
try {
|
|
242
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
243
|
+
counters.settingsCleaned = true;
|
|
244
|
+
} catch (e) {
|
|
245
|
+
counters.errors.push(`settings.json write: ${e.message}`);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async function cmdUninstall() {
|
|
250
|
+
banner();
|
|
251
|
+
|
|
252
|
+
const args = process.argv.slice(3);
|
|
253
|
+
const skipConfirm = args.includes("-y") || args.includes("--yes");
|
|
254
|
+
|
|
255
|
+
const cfg = readConfig();
|
|
256
|
+
console.log("");
|
|
257
|
+
if (cfg.installed_by) {
|
|
258
|
+
console.log(` ${DIM}User:${RESET} ${WHITE}${cfg.installed_by}${RESET} ${DIM}(${cfg.role || "?"})${RESET}`);
|
|
259
|
+
} else {
|
|
260
|
+
console.log(` ${DIM}No Qualia config found at${RESET} ${WHITE}${CONFIG_FILE}${RESET}`);
|
|
261
|
+
}
|
|
262
|
+
console.log("");
|
|
263
|
+
|
|
264
|
+
if (!skipConfirm) {
|
|
265
|
+
const confirm = await promptYesNo("Are you sure you want to uninstall the Qualia Framework?", false);
|
|
266
|
+
if (!confirm) {
|
|
267
|
+
console.log("");
|
|
268
|
+
console.log(` ${DIM}Aborted.${RESET}`);
|
|
269
|
+
console.log("");
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Preserve knowledge base by default.
|
|
275
|
+
let preserveKnowledge = true;
|
|
276
|
+
if (!skipConfirm) {
|
|
277
|
+
preserveKnowledge = await promptYesNo(
|
|
278
|
+
"Preserve knowledge base? (your learned patterns, fixes, client prefs)",
|
|
279
|
+
true
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
console.log("");
|
|
284
|
+
console.log(` ${DIM}Removing framework files...${RESET}`);
|
|
285
|
+
console.log("");
|
|
286
|
+
|
|
287
|
+
const counters = { filesRemoved: 0, dirsRemoved: 0, settingsCleaned: false, errors: [] };
|
|
288
|
+
|
|
289
|
+
// Skills — any directory starting with "qualia" under ~/.claude/skills/.
|
|
290
|
+
const skillsDir = path.join(CLAUDE_DIR, "skills");
|
|
291
|
+
try {
|
|
292
|
+
if (fs.existsSync(skillsDir)) {
|
|
293
|
+
for (const name of fs.readdirSync(skillsDir)) {
|
|
294
|
+
if (name === "qualia" || name.startsWith("qualia-")) {
|
|
295
|
+
safeRmDir(path.join(skillsDir, name), counters);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
} catch (e) {
|
|
300
|
+
counters.errors.push(`skills scan: ${e.message}`);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Agents — only the 4 Qualia ones.
|
|
304
|
+
for (const f of QUALIA_AGENT_FILES) {
|
|
305
|
+
safeUnlink(path.join(CLAUDE_DIR, "agents", f), counters);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Hooks — only the 8 Qualia ones.
|
|
309
|
+
for (const f of QUALIA_HOOK_FILES) {
|
|
310
|
+
safeUnlink(path.join(CLAUDE_DIR, "hooks", f), counters);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Bin scripts — only the 3 Qualia ones.
|
|
314
|
+
for (const f of QUALIA_BIN_FILES) {
|
|
315
|
+
safeUnlink(path.join(CLAUDE_DIR, "bin", f), counters);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Rules — all 4.
|
|
319
|
+
for (const f of QUALIA_RULE_FILES) {
|
|
320
|
+
safeUnlink(path.join(CLAUDE_DIR, "rules", f), counters);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Templates directory (entire).
|
|
324
|
+
safeRmDir(path.join(CLAUDE_DIR, "qualia-templates"), counters);
|
|
325
|
+
|
|
326
|
+
// Knowledge directory (optional preservation).
|
|
327
|
+
if (!preserveKnowledge) {
|
|
328
|
+
safeRmDir(path.join(CLAUDE_DIR, "knowledge"), counters);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Config + state files.
|
|
332
|
+
safeUnlink(path.join(CLAUDE_DIR, ".qualia-config.json"), counters);
|
|
333
|
+
safeUnlink(path.join(CLAUDE_DIR, ".qualia-last-update-check"), counters);
|
|
334
|
+
safeUnlink(path.join(CLAUDE_DIR, ".erp-api-key"), counters);
|
|
335
|
+
safeUnlink(path.join(CLAUDE_DIR, "qualia-guide.md"), counters);
|
|
336
|
+
|
|
337
|
+
// Clean settings.json surgically.
|
|
338
|
+
cleanSettingsJson(counters);
|
|
339
|
+
|
|
340
|
+
// Summary.
|
|
341
|
+
console.log("");
|
|
342
|
+
console.log(`${TEAL} ⬢ Uninstall complete${RESET}`);
|
|
343
|
+
console.log(`${DIM} ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}`);
|
|
344
|
+
console.log(` ${DIM}Files removed:${RESET} ${WHITE}${counters.filesRemoved}${RESET}`);
|
|
345
|
+
console.log(` ${DIM}Directories removed:${RESET} ${WHITE}${counters.dirsRemoved}${RESET}`);
|
|
346
|
+
console.log(
|
|
347
|
+
` ${DIM}settings.json:${RESET} ${counters.settingsCleaned ? `${GREEN}cleaned ✓${RESET}` : `${DIM}not present${RESET}`}`
|
|
348
|
+
);
|
|
349
|
+
if (preserveKnowledge) {
|
|
350
|
+
console.log(` ${DIM}Knowledge base:${RESET} ${GREEN}preserved ✓${RESET}`);
|
|
351
|
+
} else {
|
|
352
|
+
console.log(` ${DIM}Knowledge base:${RESET} ${YELLOW}removed${RESET}`);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (counters.errors.length > 0) {
|
|
356
|
+
console.log("");
|
|
357
|
+
console.log(` ${YELLOW}${counters.errors.length} warning(s):${RESET}`);
|
|
358
|
+
for (const err of counters.errors.slice(0, 5)) {
|
|
359
|
+
console.log(` ${DIM}${err}${RESET}`);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
console.log("");
|
|
364
|
+
console.log(
|
|
365
|
+
` ${YELLOW}Manual step:${RESET} edit ${WHITE}~/.claude/CLAUDE.md${RESET} to remove the Qualia Framework section if desired.`
|
|
366
|
+
);
|
|
367
|
+
console.log("");
|
|
368
|
+
}
|
|
369
|
+
|
|
116
370
|
function cmdHelp() {
|
|
117
371
|
banner();
|
|
118
372
|
console.log("");
|
|
@@ -120,6 +374,7 @@ function cmdHelp() {
|
|
|
120
374
|
console.log(` npx qualia-framework-v2 ${TEAL}install${RESET} Install or reinstall the framework`);
|
|
121
375
|
console.log(` npx qualia-framework-v2 ${TEAL}update${RESET} Update to the latest version`);
|
|
122
376
|
console.log(` npx qualia-framework-v2 ${TEAL}version${RESET} Show installed version + check for updates`);
|
|
377
|
+
console.log(` npx qualia-framework-v2 ${TEAL}uninstall${RESET} Clean removal from ~/.claude/ (${DIM}-y to skip prompts${RESET})`);
|
|
123
378
|
console.log("");
|
|
124
379
|
console.log(` ${WHITE}After install:${RESET}`);
|
|
125
380
|
console.log(` ${TG}/qualia${RESET} What should I do next?`);
|
|
@@ -151,6 +406,13 @@ switch (cmd) {
|
|
|
151
406
|
case "upgrade":
|
|
152
407
|
cmdUpdate();
|
|
153
408
|
break;
|
|
409
|
+
case "uninstall":
|
|
410
|
+
case "remove":
|
|
411
|
+
cmdUninstall().catch((e) => {
|
|
412
|
+
console.error(`${RED} ✗ Uninstall failed: ${e.message}${RESET}`);
|
|
413
|
+
process.exit(1);
|
|
414
|
+
});
|
|
415
|
+
break;
|
|
154
416
|
default:
|
|
155
417
|
cmdHelp();
|
|
156
418
|
}
|
package/bin/install.js
CHANGED
|
@@ -70,7 +70,7 @@ function askCode() {
|
|
|
70
70
|
return new Promise((resolve) => {
|
|
71
71
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
72
72
|
console.log("");
|
|
73
|
-
console.log(`${TEAL}
|
|
73
|
+
console.log(`${TEAL} ⬢ Qualia Framework v2${RESET}`);
|
|
74
74
|
console.log(`${DIM} ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}`);
|
|
75
75
|
console.log("");
|
|
76
76
|
rl.question(` ${WHITE}Enter install code:${RESET} `, (answer) => {
|
|
@@ -266,9 +266,84 @@ async function main() {
|
|
|
266
266
|
const knowledgeDir = path.join(CLAUDE_DIR, "knowledge");
|
|
267
267
|
if (!fs.existsSync(knowledgeDir)) fs.mkdirSync(knowledgeDir, { recursive: true });
|
|
268
268
|
const knowledgeFiles = {
|
|
269
|
-
"learned-patterns.md":
|
|
270
|
-
|
|
271
|
-
|
|
269
|
+
"learned-patterns.md": `# Learned Patterns
|
|
270
|
+
|
|
271
|
+
Patterns discovered across projects. Updated by \`/qualia-learn\` and manual notes.
|
|
272
|
+
|
|
273
|
+
---
|
|
274
|
+
|
|
275
|
+
## Cross-platform Node: always spawnSync with argv, never execSync with shell strings
|
|
276
|
+
**Why:** \`execSync(\\\`node \${path}/state.js check 2>/dev/null\\\`)\` breaks on Windows when the path contains spaces (common: \`C:\\\\Users\\\\John Doe\`) and the \`2>/dev/null\` redirect is bash-only. Windows cmd.exe tries to create \`\\\\dev\\\\null\` at drive root.
|
|
277
|
+
**How:** Use \`spawnSync(process.execPath, [path, "check"], { stdio: ["ignore","pipe","ignore"] })\`. Argv array is immune to path splitting; \`stdio: "ignore"\` silences stderr without shell redirection.
|
|
278
|
+
|
|
279
|
+
---
|
|
280
|
+
|
|
281
|
+
## Cross-platform stdin piping: spawnSync with input:, not bash <<< here-strings
|
|
282
|
+
**Why:** The \`<<<\` bash here-string works on bash + zsh but fails silently on Windows cmd.exe AND on Debian/Ubuntu where \`/bin/sh\` is dash (no \`<<<\` support).
|
|
283
|
+
**How:** \`spawnSync("npx", ["cmd"], { input: "data\\\\n", stdio: ["pipe","inherit","inherit"], shell: process.platform === "win32" })\`. The \`input:\` option pipes stdin directly. \`shell: process.platform === "win32"\` is required because npm/npx are \`.cmd\` shims on Windows that only resolve through a shell.
|
|
284
|
+
|
|
285
|
+
---
|
|
286
|
+
|
|
287
|
+
## Fresh-context isolation beats shared-context compression
|
|
288
|
+
**Why:** Claude's output quality degrades as context fills. A single massive context doing plan + build + verify hits the degradation curve on the later tasks.
|
|
289
|
+
**How:** Spawn separate subagents for planner / builder (per task) / verifier. Each gets fresh context. Task 50 gets the same quality as task 1. Cost: PROJECT.md + STATE.md get re-loaded into each subagent context, but the quality win dominates.
|
|
290
|
+
|
|
291
|
+
---
|
|
292
|
+
|
|
293
|
+
## Goal-backward verification beats task-completion tracking
|
|
294
|
+
**Why:** A task "create chat component" can be marked complete with a placeholder file. The task ran; the goal didn't.
|
|
295
|
+
**How:** For each phase success criterion, do a 3-level check: (1) what must be TRUE, (2) what files/functions must EXIST and be substantive (not stubs), (3) what must be CONNECTED (imported and called). Grep the codebase. Never trust summaries.
|
|
296
|
+
`,
|
|
297
|
+
"common-fixes.md": `# Common Fixes
|
|
298
|
+
|
|
299
|
+
Recurring issues and their solutions.
|
|
300
|
+
|
|
301
|
+
---
|
|
302
|
+
|
|
303
|
+
## Install code "Invalid" — user typed letter O instead of digit 0
|
|
304
|
+
**Symptom:** \`npx qualia-framework-v2 install\` rejects \`QS-NAME-O1\` (letter O in suffix).
|
|
305
|
+
**Cause:** Team codes use digit zero (\`-01\`, \`-02\`, etc.), not letter O.
|
|
306
|
+
**Fix:** Since v2.8.1, install.js auto-normalizes: \`QS-FAWZI-O1\` → \`QS-FAWZI-01\`. The normalization only touches the segment after the last dash, so \`QS-MOAYAD-03\` (real O in name) is preserved.
|
|
307
|
+
**Framework version:** Fixed in v2.8.1.
|
|
308
|
+
|
|
309
|
+
---
|
|
310
|
+
|
|
311
|
+
## Windows banner shows "No project detected" inside a real project
|
|
312
|
+
**Symptom:** The session-start banner from qualia-ui.js displays the router panel but without phase/status, even in a project with \`.planning/\`.
|
|
313
|
+
**Cause:** Before v2.8.0, \`qualia-ui.js\` called state.js via \`execSync(\\\`node \${path} check 2>/dev/null\\\`)\`. Windows cmd.exe couldn't parse the \`2>/dev/null\` redirect and/or split the path on spaces in the username.
|
|
314
|
+
**Fix:** v2.8.0 switched to \`spawnSync(process.execPath, [statePath, "check"], { stdio: ["ignore","pipe","ignore"] })\`. Argv array + silent stdio = cross-platform safe.
|
|
315
|
+
**Framework version:** Fixed in v2.8.0.
|
|
316
|
+
|
|
317
|
+
---
|
|
318
|
+
|
|
319
|
+
## \`npx qualia-framework-v2 update\` fails on Windows or Ubuntu
|
|
320
|
+
**Symptom:** Manual update command fails silently or with a shell parse error on Windows and Debian/Ubuntu.
|
|
321
|
+
**Cause:** Before v2.8.0, cli.js cmdUpdate used \`execSync(\\\`npx ... install <<< "\${code}"\\\`, { shell: true })\`. The \`<<<\` here-string is bash-only; cmd.exe doesn't understand it, and \`/bin/sh\` on Debian/Ubuntu is \`dash\` which also lacks it.
|
|
322
|
+
**Fix:** v2.8.0 replaced with \`spawnSync("npx", [...], { input: code + "\\\\n", shell: process.platform === "win32" })\`. Uses stdin pipe instead of here-string.
|
|
323
|
+
**Framework version:** Fixed in v2.8.0.
|
|
324
|
+
|
|
325
|
+
---
|
|
326
|
+
|
|
327
|
+
## Pre-deploy gate false-positive on Next.js Server Components using service_role
|
|
328
|
+
**Symptom:** \`/qualia-ship\` is blocked with "service_role found in client code" for a file that's actually a Server Component (runs server-side only).
|
|
329
|
+
**Cause:** pre-deploy-gate.js skips files matching \`.server.\` filename pattern OR \`server/\` directory path. If the Server Component is at \`app/admin/page.tsx\` (no .server. marker, not in a server/ dir), the scan flags it.
|
|
330
|
+
**Workaround:** Rename to \`.server.tsx\` OR move to a \`server/\` subdirectory OR extract the service_role usage into a helper in \`lib/server/\`.
|
|
331
|
+
**Framework version:** Known issue as of v2.8.1; better heuristic planned for v3.0.
|
|
332
|
+
`,
|
|
333
|
+
"client-prefs.md": `# Client Preferences
|
|
334
|
+
|
|
335
|
+
Client-specific preferences, design choices, and requirements. Loaded by \`/qualia-new\` when starting a project for a known client.
|
|
336
|
+
|
|
337
|
+
---
|
|
338
|
+
|
|
339
|
+
## Example Client (template)
|
|
340
|
+
**Industry:** {e.g., fintech, healthcare, SaaS}
|
|
341
|
+
**Contact:** {email}
|
|
342
|
+
**Design:** {dark-bold | clean-minimal | colorful-playful | corporate-professional}
|
|
343
|
+
**Stack preferences:** {anything non-default}
|
|
344
|
+
**Hard constraints:** {things they've explicitly said no to}
|
|
345
|
+
**Source of notes:** {date or conversation reference}
|
|
346
|
+
`,
|
|
272
347
|
};
|
|
273
348
|
for (const [name, defaultContent] of Object.entries(knowledgeFiles)) {
|
|
274
349
|
const dest = path.join(knowledgeDir, name);
|
|
@@ -350,16 +425,16 @@ async function main() {
|
|
|
350
425
|
settings.spinnerTipsOverride = {
|
|
351
426
|
excludeDefault: true,
|
|
352
427
|
tips: [
|
|
353
|
-
"
|
|
354
|
-
"
|
|
355
|
-
"
|
|
356
|
-
"
|
|
357
|
-
"
|
|
358
|
-
"
|
|
359
|
-
"
|
|
360
|
-
"
|
|
361
|
-
"
|
|
362
|
-
"
|
|
428
|
+
"⬢ Lost? Type /qualia for the next step",
|
|
429
|
+
"⬢ Small fix? Use /qualia-quick to skip planning",
|
|
430
|
+
"⬢ End of day? /qualia-report before you clock out",
|
|
431
|
+
"⬢ Context isolation: every task gets a fresh AI brain",
|
|
432
|
+
"⬢ The verifier doesn't trust claims — it greps the code",
|
|
433
|
+
"⬢ Plans are prompts — the plan IS what the builder reads",
|
|
434
|
+
"⬢ Feature branches only — never push to main",
|
|
435
|
+
"⬢ Read before write — no exceptions",
|
|
436
|
+
"⬢ MVP first — build what's asked, nothing extra",
|
|
437
|
+
"⬢ tracking.json syncs to ERP on every push",
|
|
363
438
|
],
|
|
364
439
|
};
|
|
365
440
|
|
|
@@ -395,21 +470,21 @@ async function main() {
|
|
|
395
470
|
if: "Bash(git push*)",
|
|
396
471
|
command: nodeCmd("branch-guard.js"),
|
|
397
472
|
timeout: 10,
|
|
398
|
-
statusMessage: "
|
|
473
|
+
statusMessage: "⬢ Checking branch permissions...",
|
|
399
474
|
},
|
|
400
475
|
{
|
|
401
476
|
type: "command",
|
|
402
477
|
if: "Bash(git push*)",
|
|
403
478
|
command: nodeCmd("pre-push.js"),
|
|
404
479
|
timeout: 15,
|
|
405
|
-
statusMessage: "
|
|
480
|
+
statusMessage: "⬢ Syncing tracking...",
|
|
406
481
|
},
|
|
407
482
|
{
|
|
408
483
|
type: "command",
|
|
409
484
|
if: "Bash(vercel --prod*)",
|
|
410
485
|
command: nodeCmd("pre-deploy-gate.js"),
|
|
411
486
|
timeout: 180,
|
|
412
|
-
statusMessage: "
|
|
487
|
+
statusMessage: "⬢ Running quality gates...",
|
|
413
488
|
},
|
|
414
489
|
],
|
|
415
490
|
},
|
|
@@ -421,14 +496,14 @@ async function main() {
|
|
|
421
496
|
if: "Edit(*.env*)|Write(*.env*)",
|
|
422
497
|
command: nodeCmd("block-env-edit.js"),
|
|
423
498
|
timeout: 5,
|
|
424
|
-
statusMessage: "
|
|
499
|
+
statusMessage: "⬢ Checking file permissions...",
|
|
425
500
|
},
|
|
426
501
|
{
|
|
427
502
|
type: "command",
|
|
428
503
|
if: "Edit(*migration*)|Write(*migration*)|Edit(*.sql)|Write(*.sql)",
|
|
429
504
|
command: nodeCmd("migration-guard.js"),
|
|
430
505
|
timeout: 10,
|
|
431
|
-
statusMessage: "
|
|
506
|
+
statusMessage: "⬢ Checking migration safety...",
|
|
432
507
|
},
|
|
433
508
|
],
|
|
434
509
|
},
|
|
@@ -441,7 +516,7 @@ async function main() {
|
|
|
441
516
|
type: "command",
|
|
442
517
|
command: nodeCmd("pre-compact.js"),
|
|
443
518
|
timeout: 15,
|
|
444
|
-
statusMessage: "
|
|
519
|
+
statusMessage: "⬢ Saving state...",
|
|
445
520
|
},
|
|
446
521
|
],
|
|
447
522
|
},
|
|
@@ -467,7 +542,7 @@ async function main() {
|
|
|
467
542
|
|
|
468
543
|
// ─── Summary ───────────────────────────────────────────
|
|
469
544
|
console.log("");
|
|
470
|
-
console.log(`${TEAL}
|
|
545
|
+
console.log(`${TEAL} ⬢ Installed ✓${RESET}`);
|
|
471
546
|
console.log(`${DIM} ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}`);
|
|
472
547
|
console.log(` ${WHITE}${member.name}${RESET} ${DIM}(${member.role})${RESET}`);
|
|
473
548
|
console.log(` Skills: ${WHITE}${skills.length}${RESET}`);
|
package/bin/qualia-ui.js
CHANGED
|
@@ -40,25 +40,25 @@ const RULE_DIM = `${DIM2}${RULE}${RESET}`;
|
|
|
40
40
|
|
|
41
41
|
// ─── Action Labels ───────────────────────────────────────
|
|
42
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: "
|
|
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
62
|
};
|
|
63
63
|
|
|
64
64
|
// ─── State Reading ───────────────────────────────────────
|
|
@@ -126,7 +126,7 @@ function pad(str, width) {
|
|
|
126
126
|
|
|
127
127
|
// ─── Commands ────────────────────────────────────────────
|
|
128
128
|
function cmdBanner(action, phase, subtitle) {
|
|
129
|
-
const spec = ACTIONS[action] || { label: (action || "qualia").toUpperCase(), glyph: "
|
|
129
|
+
const spec = ACTIONS[action] || { label: (action || "qualia").toUpperCase(), glyph: "⬢" };
|
|
130
130
|
const state = readState();
|
|
131
131
|
const config = readConfig();
|
|
132
132
|
const project = projectName();
|
|
@@ -136,7 +136,7 @@ function cmdBanner(action, phase, subtitle) {
|
|
|
136
136
|
: spec.label;
|
|
137
137
|
|
|
138
138
|
console.log("");
|
|
139
|
-
console.log(` ${TEAL}${BOLD}${spec.glyph}${RESET} ${WHITE}${BOLD}QUALIA${RESET} ${DIM}
|
|
139
|
+
console.log(` ${TEAL}${BOLD}${spec.glyph}${RESET} ${WHITE}${BOLD}QUALIA${RESET} ${DIM}▸${RESET} ${WHITE}${title}${RESET}`);
|
|
140
140
|
console.log(` ${RULE_DIM}`);
|
|
141
141
|
|
|
142
142
|
// Context panel
|
|
@@ -218,7 +218,7 @@ function cmdInfo(msg) {
|
|
|
218
218
|
function cmdSpawn(agent, desc) {
|
|
219
219
|
const name = agent || "agent";
|
|
220
220
|
const d = desc ? ` ${DIM}— ${desc}${RESET}` : "";
|
|
221
|
-
console.log(` ${TEAL}
|
|
221
|
+
console.log(` ${TEAL}⬡${RESET} ${WHITE}Spawning${RESET} ${TEAL}${name}${RESET}${d}`);
|
|
222
222
|
}
|
|
223
223
|
|
|
224
224
|
function cmdWave(num, total, taskCount) {
|
|
@@ -226,7 +226,7 @@ function cmdWave(num, total, taskCount) {
|
|
|
226
226
|
const n = parseInt(num) || 0;
|
|
227
227
|
const t = parseInt(total) || 0;
|
|
228
228
|
const c = parseInt(taskCount) || 0;
|
|
229
|
-
console.log(` ${TEAL}
|
|
229
|
+
console.log(` ${TEAL}»${RESET} ${WHITE}${BOLD}Wave ${n}/${t}${RESET} ${DIM}(${c} ${c === 1 ? "task" : "tasks"}, parallel)${RESET}`);
|
|
230
230
|
}
|
|
231
231
|
|
|
232
232
|
function cmdTask(num, title) {
|
|
@@ -241,16 +241,16 @@ function cmdDone(num, title, commit) {
|
|
|
241
241
|
function cmdNext(cmd) {
|
|
242
242
|
if (!cmd) return;
|
|
243
243
|
console.log("");
|
|
244
|
-
console.log(` ${TEAL}
|
|
244
|
+
console.log(` ${TEAL}⟶${RESET} ${WHITE}Next:${RESET} ${TEAL}${BOLD}${cmd}${RESET}`);
|
|
245
245
|
console.log("");
|
|
246
246
|
}
|
|
247
247
|
|
|
248
248
|
function cmdEnd(status, nextCmd) {
|
|
249
249
|
console.log("");
|
|
250
|
-
console.log(` ${TEAL}${BOLD}
|
|
250
|
+
console.log(` ${TEAL}${BOLD}⬢${RESET} ${WHITE}${BOLD}${status || "DONE"}${RESET}`);
|
|
251
251
|
console.log(` ${RULE_DIM}`);
|
|
252
252
|
if (nextCmd) {
|
|
253
|
-
console.log(` ${TEAL}
|
|
253
|
+
console.log(` ${TEAL}⟶${RESET} ${WHITE}Next:${RESET} ${TEAL}${BOLD}${nextCmd}${RESET}`);
|
|
254
254
|
}
|
|
255
255
|
console.log("");
|
|
256
256
|
}
|