qualia-framework 5.9.1 → 6.2.7
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/AGENTS.md +2 -1
- package/CLAUDE.md +2 -1
- package/README.md +45 -29
- package/agents/builder.md +1 -5
- package/agents/plan-checker.md +1 -1
- package/agents/planner.md +2 -6
- package/agents/qa-browser.md +3 -3
- package/agents/roadmapper.md +2 -2
- package/agents/verifier.md +7 -9
- package/agents/visual-evaluator.md +1 -3
- package/bin/cli.js +370 -205
- package/bin/erp-retry.js +11 -3
- package/bin/install.js +383 -55
- package/bin/knowledge-flush.js +25 -13
- package/bin/knowledge.js +11 -1
- package/bin/project-snapshot.js +293 -0
- package/bin/qualia-ui.js +13 -2
- package/bin/report-payload.js +137 -0
- package/bin/slop-detect.mjs +81 -9
- package/bin/state.js +8 -1
- package/bin/statusline.js +14 -2
- package/docs/archive/CHANGELOG-pre-v4.md +855 -0
- package/docs/changelog-v6.html +864 -0
- package/docs/ecosystem-operating-model.md +121 -0
- package/docs/erp-contract.md +74 -21
- package/docs/onboarding.html +2 -2
- package/docs/release.md +44 -0
- package/docs/reviews/v6.2.1-revival-audit.md +53 -0
- package/docs/reviews/v6.2.2-memory-erp-audit.md +41 -0
- package/docs/reviews/v6.2.3-erp-id-guard.md +15 -0
- package/guide.md +28 -3
- package/hooks/auto-update.js +20 -10
- package/hooks/branch-guard.js +10 -2
- package/hooks/env-empty-guard.js +15 -5
- package/hooks/git-guardrails.js +10 -1
- package/hooks/migration-guard.js +4 -1
- package/hooks/pre-deploy-gate.js +11 -1
- package/hooks/pre-push.js +43 -106
- package/hooks/session-start.js +22 -14
- package/hooks/stop-session-log.js +11 -3
- package/hooks/supabase-destructive-guard.js +11 -1
- package/hooks/vercel-account-guard.js +12 -3
- package/package.json +4 -3
- package/qualia-design/design-reference.md +2 -1
- package/qualia-design/frontend.md +4 -4
- package/rules/one-opinion.md +59 -0
- package/rules/trust-boundary.md +35 -0
- package/skills/qualia-feature/SKILL.md +5 -5
- package/skills/qualia-flush/SKILL.md +5 -7
- package/skills/qualia-hook-gen/SKILL.md +1 -1
- package/skills/qualia-learn/SKILL.md +1 -0
- package/skills/qualia-map/SKILL.md +2 -1
- package/skills/qualia-milestone/SKILL.md +2 -2
- package/skills/qualia-new/SKILL.md +6 -6
- package/skills/qualia-optimize/SKILL.md +1 -1
- package/skills/qualia-plan/SKILL.md +1 -1
- package/skills/qualia-polish/REFERENCE.md +8 -6
- package/skills/qualia-polish/SKILL.md +11 -9
- package/skills/qualia-polish/scripts/loop.mjs +18 -6
- package/skills/qualia-postmortem/SKILL.md +1 -1
- package/skills/qualia-report/SKILL.md +6 -42
- package/skills/qualia-road/SKILL.md +17 -5
- package/skills/qualia-verify/SKILL.md +3 -3
- package/skills/qualia-vibe/SKILL.md +226 -0
- package/skills/qualia-vibe/scripts/extract.mjs +141 -0
- package/skills/qualia-vibe/scripts/tokens.mjs +342 -0
- package/templates/help.html +10 -3
- package/templates/knowledge/agents.md +3 -3
- package/templates/knowledge/index.md +1 -1
- package/templates/tracking.json +3 -0
- package/templates/work-packet.md +46 -0
- package/tests/bin.test.sh +423 -25
- package/tests/hooks.test.sh +1 -8
- package/tests/install-smoke.test.sh +137 -0
- package/tests/published-install-smoke.test.sh +126 -0
- package/tests/refs.test.sh +43 -1
- package/tests/run-all.sh +49 -0
- package/tests/runner.js +19 -33
- package/tests/slop-detect.test.sh +11 -5
- package/tests/state.test.sh +4 -1
- package/hooks/pre-compact.js +0 -125
package/bin/cli.js
CHANGED
|
@@ -4,6 +4,7 @@ const { spawnSync } = require("child_process");
|
|
|
4
4
|
const path = require("path");
|
|
5
5
|
const fs = require("fs");
|
|
6
6
|
const readline = require("readline");
|
|
7
|
+
const os = require("os");
|
|
7
8
|
|
|
8
9
|
const TEAL = "\x1b[38;2;0;206;209m";
|
|
9
10
|
const TG = "\x1b[38;2;0;170;175m";
|
|
@@ -15,22 +16,77 @@ const RED = "\x1b[38;2;239;68;68m";
|
|
|
15
16
|
const RESET = "\x1b[0m";
|
|
16
17
|
const BOLD = "\x1b[1m";
|
|
17
18
|
|
|
18
|
-
const CLAUDE_DIR = path.join(
|
|
19
|
+
const CLAUDE_DIR = path.join(os.homedir(), ".claude");
|
|
20
|
+
const CODEX_DIR = path.join(os.homedir(), ".codex");
|
|
19
21
|
const PKG = require("../package.json");
|
|
20
22
|
const CONFIG_FILE = path.join(CLAUDE_DIR, ".qualia-config.json");
|
|
23
|
+
const CODEX_CONFIG_FILE = path.join(CODEX_DIR, ".qualia-config.json");
|
|
24
|
+
|
|
25
|
+
function installedHomes() {
|
|
26
|
+
const homes = [];
|
|
27
|
+
if (fs.existsSync(CONFIG_FILE)) homes.push(CLAUDE_DIR);
|
|
28
|
+
if (fs.existsSync(CODEX_CONFIG_FILE)) homes.push(CODEX_DIR);
|
|
29
|
+
return homes.length > 0 ? homes : [CLAUDE_DIR];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function primaryInstallHome() {
|
|
33
|
+
return installedHomes()[0];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function configFileForHome(home) {
|
|
37
|
+
return path.join(home, ".qualia-config.json");
|
|
38
|
+
}
|
|
21
39
|
|
|
22
40
|
function readConfig() {
|
|
41
|
+
const configFile = fs.existsSync(CONFIG_FILE) ? CONFIG_FILE : CODEX_CONFIG_FILE;
|
|
42
|
+
try {
|
|
43
|
+
return JSON.parse(fs.readFileSync(configFile, "utf8"));
|
|
44
|
+
} catch {
|
|
45
|
+
return {};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function readConfigAt(home) {
|
|
23
50
|
try {
|
|
24
|
-
return JSON.parse(fs.readFileSync(
|
|
51
|
+
return JSON.parse(fs.readFileSync(configFileForHome(home), "utf8"));
|
|
25
52
|
} catch {
|
|
26
53
|
return {};
|
|
27
54
|
}
|
|
28
55
|
}
|
|
29
56
|
|
|
30
|
-
function writeConfig(cfg) {
|
|
31
|
-
if (!fs.existsSync(
|
|
32
|
-
|
|
33
|
-
|
|
57
|
+
function writeConfig(cfg, home = primaryInstallHome()) {
|
|
58
|
+
if (!fs.existsSync(home)) fs.mkdirSync(home, { recursive: true });
|
|
59
|
+
const configFile = configFileForHome(home);
|
|
60
|
+
fs.writeFileSync(configFile, JSON.stringify(cfg, null, 2) + "\n", { mode: 0o600 });
|
|
61
|
+
try { fs.chmodSync(configFile, 0o600); } catch {}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function installTargetForUpdate() {
|
|
65
|
+
const hasClaude = fs.existsSync(CONFIG_FILE);
|
|
66
|
+
const hasCodex = fs.existsSync(CODEX_CONFIG_FILE);
|
|
67
|
+
if (hasClaude && hasCodex) return "3";
|
|
68
|
+
if (hasCodex) return "2";
|
|
69
|
+
return "1";
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function isCodexHome(home) {
|
|
73
|
+
return path.basename(home) === ".codex";
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function installLabel(home) {
|
|
77
|
+
return isCodexHome(home) ? "Codex" : "Claude Code";
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function hasQualiaArtifacts(home) {
|
|
81
|
+
return fs.existsSync(configFileForHome(home))
|
|
82
|
+
|| fs.existsSync(path.join(home, "bin", "state.js"))
|
|
83
|
+
|| fs.existsSync(path.join(home, "hooks", "session-start.js"))
|
|
84
|
+
|| fs.existsSync(path.join(home, "skills", "qualia-new"));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function frameworkHomes() {
|
|
88
|
+
const homes = [CLAUDE_DIR, CODEX_DIR].filter(hasQualiaArtifacts);
|
|
89
|
+
return homes.length > 0 ? homes : [CLAUDE_DIR];
|
|
34
90
|
}
|
|
35
91
|
|
|
36
92
|
function banner() {
|
|
@@ -50,6 +106,12 @@ function cmdVersion() {
|
|
|
50
106
|
const cfg = readConfig();
|
|
51
107
|
|
|
52
108
|
console.log(` ${DIM}Installed:${RESET} ${WHITE}${PKG.version}${RESET}`);
|
|
109
|
+
const surfaces = installedHomes()
|
|
110
|
+
.filter((home) => fs.existsSync(configFileForHome(home)))
|
|
111
|
+
.map((home) => path.basename(home) === ".codex" ? "Codex" : "Claude Code");
|
|
112
|
+
if (surfaces.length > 0) {
|
|
113
|
+
console.log(` ${DIM}Targets:${RESET} ${WHITE}${surfaces.join(" · ")}${RESET}`);
|
|
114
|
+
}
|
|
53
115
|
if (cfg.installed_by) {
|
|
54
116
|
console.log(` ${DIM}User:${RESET} ${WHITE}${cfg.installed_by}${RESET} ${DIM}(${cfg.role})${RESET}`);
|
|
55
117
|
}
|
|
@@ -103,8 +165,9 @@ function cmdUpdate() {
|
|
|
103
165
|
console.log("");
|
|
104
166
|
|
|
105
167
|
try {
|
|
168
|
+
const target = installTargetForUpdate();
|
|
106
169
|
const r = spawnSync("npx", ["qualia-framework@latest", "install"], {
|
|
107
|
-
input: cfg.code
|
|
170
|
+
input: `${cfg.code}\n${target}\n`,
|
|
108
171
|
stdio: ["pipe", "inherit", "inherit"],
|
|
109
172
|
shell: process.platform === "win32", // npx is a .cmd shim on Windows — must go through shell
|
|
110
173
|
timeout: 120000,
|
|
@@ -122,12 +185,12 @@ function cmdUpdate() {
|
|
|
122
185
|
}
|
|
123
186
|
|
|
124
187
|
// ─── Uninstall ───────────────────────────────────────────
|
|
125
|
-
// Surgical removal of the Qualia Framework from
|
|
126
|
-
// Preserves CLAUDE.md (user may have customized
|
|
127
|
-
// non-Qualia entries in settings.json
|
|
188
|
+
// Surgical removal of the Qualia Framework from installed homes.
|
|
189
|
+
// Preserves CLAUDE.md / AGENTS.md (user may have customized them), Codex
|
|
190
|
+
// config.toml, and non-Qualia hook entries in settings.json / hooks.json.
|
|
128
191
|
// --yes / -y skips the confirmation prompt for scripted use.
|
|
129
192
|
|
|
130
|
-
// Current Qualia hook filenames — only these are removed from
|
|
193
|
+
// Current Qualia hook filenames — only these are removed from hooks/,
|
|
131
194
|
// any other hooks the user dropped in there are left alone. The LEGACY set
|
|
132
195
|
// lists hooks that were shipped by older framework versions but have since
|
|
133
196
|
// been removed; uninstall still tries to clean them so old installs get a
|
|
@@ -139,15 +202,18 @@ const QUALIA_HOOK_FILES = [
|
|
|
139
202
|
"pre-push.js",
|
|
140
203
|
"migration-guard.js",
|
|
141
204
|
"pre-deploy-gate.js",
|
|
142
|
-
"pre-compact.js",
|
|
143
205
|
"git-guardrails.js",
|
|
144
206
|
"stop-session-log.js",
|
|
207
|
+
"env-empty-guard.js",
|
|
208
|
+
"supabase-destructive-guard.js",
|
|
209
|
+
"vercel-account-guard.js",
|
|
145
210
|
];
|
|
146
211
|
const QUALIA_LEGACY_HOOK_FILES = [
|
|
147
212
|
"block-env-edit.js", // removed in v3.2.0
|
|
213
|
+
"pre-compact.js", // removed in v6.2.0 — state.js journal makes bot-commits redundant
|
|
148
214
|
];
|
|
149
215
|
|
|
150
|
-
//
|
|
216
|
+
// Qualia agents — only these are removed.
|
|
151
217
|
const QUALIA_AGENT_FILES = [
|
|
152
218
|
"planner.md",
|
|
153
219
|
"builder.md",
|
|
@@ -157,15 +223,30 @@ const QUALIA_AGENT_FILES = [
|
|
|
157
223
|
"researcher.md",
|
|
158
224
|
"research-synthesizer.md",
|
|
159
225
|
"roadmapper.md",
|
|
226
|
+
"visual-evaluator.md",
|
|
227
|
+
];
|
|
228
|
+
const QUALIA_CODEX_AGENT_FILES = QUALIA_AGENT_FILES.map((f) => f.replace(/\.md$/, ".toml"));
|
|
229
|
+
|
|
230
|
+
// Qualia bin scripts.
|
|
231
|
+
const QUALIA_BIN_FILES = [
|
|
232
|
+
"state.js",
|
|
233
|
+
"qualia-ui.js",
|
|
234
|
+
"statusline.js",
|
|
235
|
+
"knowledge.js",
|
|
236
|
+
"knowledge-flush.js",
|
|
237
|
+
"plan-contract.js",
|
|
238
|
+
"agent-runs.js",
|
|
239
|
+
"slop-detect.mjs",
|
|
240
|
+
"erp-retry.js",
|
|
241
|
+
"report-payload.js",
|
|
242
|
+
"project-snapshot.js",
|
|
160
243
|
];
|
|
161
|
-
|
|
162
|
-
// 3 Qualia bin scripts.
|
|
163
|
-
const QUALIA_BIN_FILES = ["state.js", "qualia-ui.js", "statusline.js", "knowledge.js", "knowledge-flush.js", "plan-contract.js", "agent-runs.js", "slop-detect.mjs", "erp-retry.js"];
|
|
164
244
|
|
|
165
245
|
// Qualia rules — security, deployment, infra, grounding, plus the v4.5.0 design substrate.
|
|
166
246
|
// frontend.md and design-reference.md are kept for backward compat; new projects use design-laws/brand/product/rubric.
|
|
167
247
|
const QUALIA_RULE_FILES = [
|
|
168
248
|
"security.md", "deployment.md", "infrastructure.md", "grounding.md",
|
|
249
|
+
"speed.md", "architecture.md", "trust-boundary.md", "one-opinion.md",
|
|
169
250
|
"frontend.md", "design-reference.md",
|
|
170
251
|
"design-laws.md", "design-brand.md", "design-product.md", "design-rubric.md",
|
|
171
252
|
];
|
|
@@ -205,46 +286,55 @@ function safeRmDir(p, counters) {
|
|
|
205
286
|
}
|
|
206
287
|
}
|
|
207
288
|
|
|
208
|
-
function
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
return;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// Only remove entries that point at qualia paths. Leave everything else.
|
|
220
|
-
const isQualiaCommand = (cmd) =>
|
|
221
|
-
typeof cmd === "string" && (cmd.includes("qualia") || cmd.includes(".claude/hooks/") || cmd.includes(".claude/bin/"));
|
|
289
|
+
function isQualiaCommand(cmd) {
|
|
290
|
+
return typeof cmd === "string"
|
|
291
|
+
&& (cmd.includes("qualia")
|
|
292
|
+
|| cmd.includes(".claude/hooks/")
|
|
293
|
+
|| cmd.includes(".claude/bin/")
|
|
294
|
+
|| cmd.includes(".codex/hooks/")
|
|
295
|
+
|| cmd.includes(".codex/bin/"));
|
|
296
|
+
}
|
|
222
297
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
298
|
+
function cleanHookBlocks(root, hookKey) {
|
|
299
|
+
if (!root || typeof root !== "object" || !root[hookKey] || typeof root[hookKey] !== "object") return false;
|
|
300
|
+
let changed = false;
|
|
301
|
+
for (const key of Object.keys(root[hookKey])) {
|
|
302
|
+
const arr = root[hookKey][key];
|
|
303
|
+
if (!Array.isArray(arr)) continue;
|
|
304
|
+
const cleaned = arr
|
|
226
305
|
.map((entry) => {
|
|
227
306
|
if (!entry || !Array.isArray(entry.hooks)) return entry;
|
|
228
307
|
const hooks = entry.hooks.filter((h) => !isQualiaCommand(h && h.command));
|
|
308
|
+
if (hooks.length !== entry.hooks.length) changed = true;
|
|
229
309
|
return { ...entry, hooks };
|
|
230
310
|
})
|
|
231
311
|
.filter((entry) => Array.isArray(entry.hooks) && entry.hooks.length > 0);
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
for (const key of Object.keys(settings.hooks)) {
|
|
238
|
-
const cleaned = filterHookArray(settings.hooks[key]);
|
|
239
|
-
if (cleaned && cleaned.length > 0) {
|
|
240
|
-
settings.hooks[key] = cleaned;
|
|
241
|
-
} else {
|
|
242
|
-
delete settings.hooks[key];
|
|
243
|
-
}
|
|
312
|
+
if (cleaned.length > 0) {
|
|
313
|
+
root[hookKey][key] = cleaned;
|
|
314
|
+
} else {
|
|
315
|
+
delete root[hookKey][key];
|
|
316
|
+
changed = true;
|
|
244
317
|
}
|
|
245
|
-
// If hooks is now empty, remove it entirely.
|
|
246
|
-
if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
|
|
247
318
|
}
|
|
319
|
+
if (Object.keys(root[hookKey]).length === 0) {
|
|
320
|
+
delete root[hookKey];
|
|
321
|
+
changed = true;
|
|
322
|
+
}
|
|
323
|
+
return changed;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function cleanSettingsJson(home, counters) {
|
|
327
|
+
const settingsPath = path.join(home, "settings.json");
|
|
328
|
+
if (!fs.existsSync(settingsPath)) return;
|
|
329
|
+
let settings;
|
|
330
|
+
try {
|
|
331
|
+
settings = JSON.parse(fs.readFileSync(settingsPath, "utf8"));
|
|
332
|
+
} catch (e) {
|
|
333
|
+
counters.errors.push(`settings.json: ${e.message}`);
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
cleanHookBlocks(settings, "hooks");
|
|
248
338
|
|
|
249
339
|
// Status line — only drop it if it points at our renderer.
|
|
250
340
|
if (settings.statusLine && typeof settings.statusLine === "object") {
|
|
@@ -268,19 +358,40 @@ function cleanSettingsJson(counters) {
|
|
|
268
358
|
}
|
|
269
359
|
}
|
|
270
360
|
|
|
361
|
+
function cleanCodexHooksJson(home, counters) {
|
|
362
|
+
const hooksPath = path.join(home, "hooks.json");
|
|
363
|
+
if (!fs.existsSync(hooksPath)) return;
|
|
364
|
+
let hooksJson;
|
|
365
|
+
try {
|
|
366
|
+
hooksJson = JSON.parse(fs.readFileSync(hooksPath, "utf8"));
|
|
367
|
+
} catch (e) {
|
|
368
|
+
counters.errors.push(`hooks.json: ${e.message}`);
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
cleanHookBlocks(hooksJson, "hooks");
|
|
372
|
+
try {
|
|
373
|
+
fs.writeFileSync(hooksPath, JSON.stringify(hooksJson, null, 2) + "\n");
|
|
374
|
+
counters.hooksJsonCleaned = true;
|
|
375
|
+
} catch (e) {
|
|
376
|
+
counters.errors.push(`hooks.json write: ${e.message}`);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
271
380
|
async function cmdUninstall() {
|
|
272
381
|
banner();
|
|
273
382
|
|
|
274
383
|
const args = process.argv.slice(3);
|
|
275
384
|
const skipConfirm = args.includes("-y") || args.includes("--yes");
|
|
276
385
|
|
|
386
|
+
const homes = frameworkHomes();
|
|
277
387
|
const cfg = readConfig();
|
|
278
388
|
console.log("");
|
|
279
389
|
if (cfg.installed_by) {
|
|
280
390
|
console.log(` ${DIM}User:${RESET} ${WHITE}${cfg.installed_by}${RESET} ${DIM}(${cfg.role || "?"})${RESET}`);
|
|
281
391
|
} else {
|
|
282
|
-
console.log(` ${DIM}No Qualia config found at${RESET} ${WHITE}${CONFIG_FILE}${RESET}`);
|
|
392
|
+
console.log(` ${DIM}No Qualia config found at${RESET} ${WHITE}${CONFIG_FILE}${RESET} ${DIM}or${RESET} ${WHITE}${CODEX_CONFIG_FILE}${RESET}`);
|
|
283
393
|
}
|
|
394
|
+
console.log(` ${DIM}Targets:${RESET} ${WHITE}${homes.map(installLabel).join(" · ")}${RESET}`);
|
|
284
395
|
console.log("");
|
|
285
396
|
|
|
286
397
|
if (!skipConfirm) {
|
|
@@ -306,62 +417,60 @@ async function cmdUninstall() {
|
|
|
306
417
|
console.log(` ${DIM}Removing framework files...${RESET}`);
|
|
307
418
|
console.log("");
|
|
308
419
|
|
|
309
|
-
const counters = { filesRemoved: 0, dirsRemoved: 0, settingsCleaned: false, errors: [] };
|
|
420
|
+
const counters = { filesRemoved: 0, dirsRemoved: 0, settingsCleaned: false, hooksJsonCleaned: false, errors: [] };
|
|
310
421
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
422
|
+
for (const home of homes) {
|
|
423
|
+
const skillsDir = path.join(home, "skills");
|
|
424
|
+
try {
|
|
425
|
+
if (fs.existsSync(skillsDir)) {
|
|
426
|
+
for (const name of fs.readdirSync(skillsDir)) {
|
|
427
|
+
if (name === "qualia" || name.startsWith("qualia-")) {
|
|
428
|
+
safeRmDir(path.join(skillsDir, name), counters);
|
|
429
|
+
}
|
|
318
430
|
}
|
|
319
431
|
}
|
|
432
|
+
} catch (e) {
|
|
433
|
+
counters.errors.push(`${installLabel(home)} skills scan: ${e.message}`);
|
|
320
434
|
}
|
|
321
|
-
} catch (e) {
|
|
322
|
-
counters.errors.push(`skills scan: ${e.message}`);
|
|
323
|
-
}
|
|
324
435
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
436
|
+
const agentFiles = isCodexHome(home)
|
|
437
|
+
? [...QUALIA_CODEX_AGENT_FILES, ...QUALIA_AGENT_FILES]
|
|
438
|
+
: QUALIA_AGENT_FILES;
|
|
439
|
+
for (const f of agentFiles) {
|
|
440
|
+
safeUnlink(path.join(home, "agents", f), counters);
|
|
441
|
+
}
|
|
329
442
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
}
|
|
443
|
+
for (const f of [...QUALIA_HOOK_FILES, ...QUALIA_LEGACY_HOOK_FILES]) {
|
|
444
|
+
safeUnlink(path.join(home, "hooks", f), counters);
|
|
445
|
+
}
|
|
334
446
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
}
|
|
447
|
+
for (const f of QUALIA_BIN_FILES) {
|
|
448
|
+
safeUnlink(path.join(home, "bin", f), counters);
|
|
449
|
+
}
|
|
339
450
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
}
|
|
451
|
+
for (const f of QUALIA_RULE_FILES) {
|
|
452
|
+
safeUnlink(path.join(home, "rules", f), counters);
|
|
453
|
+
}
|
|
344
454
|
|
|
345
|
-
|
|
346
|
-
|
|
455
|
+
safeRmDir(path.join(home, "qualia-templates"), counters);
|
|
456
|
+
safeRmDir(path.join(home, "qualia-design"), counters);
|
|
457
|
+
safeRmDir(path.join(home, "qualia-references"), counters);
|
|
347
458
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
}
|
|
459
|
+
if (!preserveKnowledge) {
|
|
460
|
+
safeRmDir(path.join(home, "knowledge"), counters);
|
|
461
|
+
}
|
|
352
462
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
safeUnlink(path.join(CLAUDE_DIR, "qualia-guide.md"), counters);
|
|
463
|
+
safeUnlink(path.join(home, ".qualia-config.json"), counters);
|
|
464
|
+
safeUnlink(path.join(home, ".qualia-last-update-check"), counters);
|
|
465
|
+
safeUnlink(path.join(home, ".erp-api-key"), counters);
|
|
466
|
+
safeUnlink(path.join(home, ".qualia-team.json"), counters);
|
|
467
|
+
safeUnlink(path.join(home, "qualia-guide.md"), counters);
|
|
359
468
|
|
|
360
|
-
|
|
361
|
-
safeRmDir(path.join(CLAUDE_DIR, ".qualia-traces"), counters);
|
|
469
|
+
safeRmDir(path.join(home, ".qualia-traces"), counters);
|
|
362
470
|
|
|
363
|
-
|
|
364
|
-
|
|
471
|
+
if (isCodexHome(home)) cleanCodexHooksJson(home, counters);
|
|
472
|
+
else cleanSettingsJson(home, counters);
|
|
473
|
+
}
|
|
365
474
|
|
|
366
475
|
// Summary.
|
|
367
476
|
console.log("");
|
|
@@ -372,6 +481,9 @@ async function cmdUninstall() {
|
|
|
372
481
|
console.log(
|
|
373
482
|
` ${DIM}settings.json:${RESET} ${counters.settingsCleaned ? `${GREEN}cleaned ✓${RESET}` : `${DIM}not present${RESET}`}`
|
|
374
483
|
);
|
|
484
|
+
console.log(
|
|
485
|
+
` ${DIM}hooks.json:${RESET} ${counters.hooksJsonCleaned ? `${GREEN}cleaned ✓${RESET}` : `${DIM}not present${RESET}`}`
|
|
486
|
+
);
|
|
375
487
|
if (preserveKnowledge) {
|
|
376
488
|
console.log(` ${DIM}Knowledge base:${RESET} ${GREEN}preserved ✓${RESET}`);
|
|
377
489
|
} else {
|
|
@@ -387,14 +499,13 @@ async function cmdUninstall() {
|
|
|
387
499
|
}
|
|
388
500
|
|
|
389
501
|
console.log("");
|
|
390
|
-
console.log(
|
|
391
|
-
` ${YELLOW}Manual step:${RESET} edit ${WHITE}~/.claude/CLAUDE.md${RESET} to remove the Qualia Framework section if desired.`
|
|
392
|
-
);
|
|
502
|
+
console.log(` ${YELLOW}Manual step:${RESET} edit ${WHITE}~/.claude/CLAUDE.md${RESET} or ${WHITE}~/.codex/AGENTS.md${RESET} if you want to remove the global instruction text.`);
|
|
393
503
|
console.log("");
|
|
394
504
|
}
|
|
395
505
|
|
|
396
506
|
// ─── Team Management ────────────────────────────────────
|
|
397
|
-
// External team file at ~/.claude
|
|
507
|
+
// External team file at the active install home. Falls back to ~/.claude before
|
|
508
|
+
// install for backward compatibility.
|
|
398
509
|
// Falls back to embedded defaults in install.js.
|
|
399
510
|
|
|
400
511
|
function getDefaultTeam() {
|
|
@@ -408,19 +519,23 @@ function getDefaultTeam() {
|
|
|
408
519
|
}
|
|
409
520
|
|
|
410
521
|
function readTeamFile() {
|
|
411
|
-
const
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
522
|
+
for (const home of installedHomes()) {
|
|
523
|
+
const teamFile = path.join(home, ".qualia-team.json");
|
|
524
|
+
try {
|
|
525
|
+
if (fs.existsSync(teamFile)) {
|
|
526
|
+
const data = JSON.parse(fs.readFileSync(teamFile, "utf8"));
|
|
527
|
+
if (data && typeof data === "object" && Object.keys(data).length > 0) return data;
|
|
528
|
+
}
|
|
529
|
+
} catch {}
|
|
530
|
+
}
|
|
418
531
|
return null;
|
|
419
532
|
}
|
|
420
533
|
|
|
421
534
|
function writeTeamFile(team) {
|
|
422
|
-
|
|
423
|
-
|
|
535
|
+
for (const home of installedHomes()) {
|
|
536
|
+
if (!fs.existsSync(home)) fs.mkdirSync(home, { recursive: true });
|
|
537
|
+
fs.writeFileSync(path.join(home, ".qualia-team.json"), JSON.stringify(team, null, 2) + "\n");
|
|
538
|
+
}
|
|
424
539
|
}
|
|
425
540
|
|
|
426
541
|
function parseTeamArgs(argv) {
|
|
@@ -442,7 +557,7 @@ function cmdTeam() {
|
|
|
442
557
|
console.log("");
|
|
443
558
|
const team = readTeamFile();
|
|
444
559
|
const source = team || getDefaultTeam();
|
|
445
|
-
const label = team ? "team file" : "embedded defaults";
|
|
560
|
+
const label = team ? "installed team file" : "embedded defaults";
|
|
446
561
|
console.log(` ${DIM}Source: ${label}${RESET}`);
|
|
447
562
|
console.log("");
|
|
448
563
|
for (const [code, member] of Object.entries(source)) {
|
|
@@ -502,7 +617,7 @@ function cmdTeam() {
|
|
|
502
617
|
function cmdTraces() {
|
|
503
618
|
banner();
|
|
504
619
|
console.log("");
|
|
505
|
-
const tracesDir = path.join(
|
|
620
|
+
const tracesDir = path.join(primaryInstallHome(), ".qualia-traces");
|
|
506
621
|
if (!fs.existsSync(tracesDir)) {
|
|
507
622
|
console.log(` ${DIM}No traces found. Traces are written by hooks during normal operation.${RESET}`);
|
|
508
623
|
console.log("");
|
|
@@ -538,6 +653,15 @@ function cmdMigrate() {
|
|
|
538
653
|
|
|
539
654
|
const settingsPath = path.join(CLAUDE_DIR, "settings.json");
|
|
540
655
|
if (!fs.existsSync(settingsPath)) {
|
|
656
|
+
if (fs.existsSync(CODEX_CONFIG_FILE)) {
|
|
657
|
+
const hooksPath = path.join(CODEX_DIR, "hooks.json");
|
|
658
|
+
if (fs.existsSync(hooksPath)) {
|
|
659
|
+
console.log(` ${GREEN}✓${RESET} Codex install uses ${WHITE}~/.codex/hooks.json${RESET}; no Claude settings migration is needed.`);
|
|
660
|
+
console.log(` ${DIM}Refresh Codex wiring with:${RESET} ${TEAL}npx qualia-framework@latest install${RESET}`);
|
|
661
|
+
console.log("");
|
|
662
|
+
process.exit(0);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
541
665
|
console.log(` ${RED}✗${RESET} No settings.json found. Run ${TEAL}qualia-framework install${RESET} first.`);
|
|
542
666
|
console.log("");
|
|
543
667
|
process.exit(1);
|
|
@@ -582,7 +706,16 @@ function cmdMigrate() {
|
|
|
582
706
|
}
|
|
583
707
|
|
|
584
708
|
// Check PreToolUse hooks — ensure all critical hooks are present
|
|
585
|
-
const requiredBashHooks = [
|
|
709
|
+
const requiredBashHooks = [
|
|
710
|
+
"auto-update.js",
|
|
711
|
+
"git-guardrails.js",
|
|
712
|
+
"branch-guard.js",
|
|
713
|
+
"pre-push.js",
|
|
714
|
+
"pre-deploy-gate.js",
|
|
715
|
+
"vercel-account-guard.js",
|
|
716
|
+
"env-empty-guard.js",
|
|
717
|
+
"supabase-destructive-guard.js",
|
|
718
|
+
];
|
|
586
719
|
const requiredEditHooks = ["migration-guard.js"];
|
|
587
720
|
|
|
588
721
|
if (!settings.hooks.PreToolUse) settings.hooks.PreToolUse = [];
|
|
@@ -614,6 +747,9 @@ function cmdMigrate() {
|
|
|
614
747
|
if (hookFile === "branch-guard.js") { hookDef.if = "Bash(git push*)"; hookDef.statusMessage = "⬢ Checking branch permissions..."; }
|
|
615
748
|
if (hookFile === "pre-push.js") { hookDef.if = "Bash(git push*)"; hookDef.timeout = 15; }
|
|
616
749
|
if (hookFile === "pre-deploy-gate.js") { hookDef.if = "Bash(vercel --prod*)"; hookDef.timeout = 180; hookDef.statusMessage = "⬢ Running quality gates..."; }
|
|
750
|
+
if (hookFile === "vercel-account-guard.js") { hookDef.if = "Bash(vercel --prod*)|Bash(vercel deploy*)"; hookDef.timeout = 8; hookDef.statusMessage = "⬢ Verifying Vercel account..."; }
|
|
751
|
+
if (hookFile === "env-empty-guard.js") { hookDef.if = "Bash(vercel env*)"; hookDef.statusMessage = "⬢ Checking env value..."; }
|
|
752
|
+
if (hookFile === "supabase-destructive-guard.js") { hookDef.if = "Bash(supabase*)|Bash(npx supabase*)"; hookDef.statusMessage = "⬢ Checking Supabase safety..."; }
|
|
617
753
|
bashEntry.hooks.push(hookDef);
|
|
618
754
|
changes++;
|
|
619
755
|
console.log(` ${GREEN}+${RESET} Wired ${hookFile} into PreToolUse/Bash`);
|
|
@@ -641,23 +777,22 @@ function cmdMigrate() {
|
|
|
641
777
|
}
|
|
642
778
|
}
|
|
643
779
|
|
|
644
|
-
//
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
if (
|
|
658
|
-
compactEntry.hooks.push({ type: "command", command: nodeCmd("pre-compact.js"), timeout: 15, statusMessage: "⬢ Saving state..." });
|
|
780
|
+
// PreCompact: pre-compact.js was removed in v6.2.0 (state.js already provides
|
|
781
|
+
// crash-safe atomic writes with a write-ahead journal — the bot commit added
|
|
782
|
+
// no durability). Strip any legacy entry; drop the event key if it's empty.
|
|
783
|
+
if (Array.isArray(settings.hooks.PreCompact)) {
|
|
784
|
+
const beforeLen = settings.hooks.PreCompact.length;
|
|
785
|
+
settings.hooks.PreCompact = settings.hooks.PreCompact
|
|
786
|
+
.map(block => {
|
|
787
|
+
if (!block || !Array.isArray(block.hooks)) return block;
|
|
788
|
+
return { ...block, hooks: block.hooks.filter(h => extractScriptName(h && h.command) !== "pre-compact.js") };
|
|
789
|
+
})
|
|
790
|
+
.filter(block => Array.isArray(block.hooks) && block.hooks.length > 0);
|
|
791
|
+
const removed = beforeLen !== settings.hooks.PreCompact.length || settings.hooks.PreCompact.length === 0;
|
|
792
|
+
if (settings.hooks.PreCompact.length === 0) delete settings.hooks.PreCompact;
|
|
793
|
+
if (removed) {
|
|
659
794
|
changes++;
|
|
660
|
-
console.log(` ${GREEN}
|
|
795
|
+
console.log(` ${GREEN}-${RESET} Removed legacy pre-compact.js from PreCompact`);
|
|
661
796
|
}
|
|
662
797
|
}
|
|
663
798
|
|
|
@@ -738,7 +873,7 @@ function cmdAnalytics() {
|
|
|
738
873
|
banner();
|
|
739
874
|
console.log("");
|
|
740
875
|
|
|
741
|
-
const tracesDir = path.join(
|
|
876
|
+
const tracesDir = path.join(primaryInstallHome(), ".qualia-traces");
|
|
742
877
|
if (!fs.existsSync(tracesDir)) {
|
|
743
878
|
console.log(` ${DIM}No traces found. Analytics require hook telemetry data.${RESET}`);
|
|
744
879
|
console.log(` ${DIM}Traces are collected automatically during normal framework use.${RESET}`);
|
|
@@ -828,11 +963,12 @@ async function cmdErpPing() {
|
|
|
828
963
|
banner();
|
|
829
964
|
console.log("");
|
|
830
965
|
|
|
966
|
+
const installHome = primaryInstallHome();
|
|
831
967
|
const cfg = readConfig();
|
|
832
968
|
const args = new Set(process.argv.slice(3));
|
|
833
969
|
const erpUrl = (cfg.erp && cfg.erp.url) || "https://portal.qualiasolutions.net";
|
|
834
970
|
let erpEnabled = !(cfg.erp && cfg.erp.enabled === false);
|
|
835
|
-
const keyFile = path.join(
|
|
971
|
+
const keyFile = path.join(installHome, ".erp-api-key");
|
|
836
972
|
|
|
837
973
|
console.log(` ${DIM}URL:${RESET} ${WHITE}${erpUrl}${RESET}`);
|
|
838
974
|
console.log(` ${DIM}Enabled:${RESET} ${erpEnabled ? `${GREEN}yes${RESET}` : `${YELLOW}no (erp.enabled=false)${RESET}`}`);
|
|
@@ -963,17 +1099,21 @@ function cmdSetErpKey() {
|
|
|
963
1099
|
banner();
|
|
964
1100
|
console.log("");
|
|
965
1101
|
|
|
966
|
-
const
|
|
1102
|
+
const homes = installedHomes();
|
|
967
1103
|
const rawArgs = process.argv.slice(3);
|
|
968
1104
|
const clear = rawArgs.includes("--clear");
|
|
969
1105
|
|
|
970
|
-
|
|
1106
|
+
for (const home of homes) {
|
|
1107
|
+
if (!fs.existsSync(home)) fs.mkdirSync(home, { recursive: true });
|
|
1108
|
+
}
|
|
971
1109
|
|
|
972
1110
|
if (clear) {
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
1111
|
+
for (const home of homes) {
|
|
1112
|
+
try { fs.unlinkSync(path.join(home, ".erp-api-key")); } catch {}
|
|
1113
|
+
const cfg = readConfigAt(home);
|
|
1114
|
+
cfg.erp = { ...(cfg.erp || {}), enabled: false, url: (cfg.erp && cfg.erp.url) || "https://portal.qualiasolutions.net", api_key_file: ".erp-api-key" };
|
|
1115
|
+
writeConfig(cfg, home);
|
|
1116
|
+
}
|
|
977
1117
|
console.log(` ${GREEN}✓${RESET} ERP key removed and ERP disabled.`);
|
|
978
1118
|
console.log("");
|
|
979
1119
|
return;
|
|
@@ -1010,19 +1150,21 @@ function cmdSetErpKey() {
|
|
|
1010
1150
|
console.log(` ${YELLOW}!${RESET} Key looks short (${key.length} bytes). Saving anyway.`);
|
|
1011
1151
|
}
|
|
1012
1152
|
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1153
|
+
for (const home of homes) {
|
|
1154
|
+
const keyFile = path.join(home, ".erp-api-key");
|
|
1155
|
+
fs.writeFileSync(keyFile, key, { mode: 0o600 });
|
|
1156
|
+
try { fs.chmodSync(keyFile, 0o600); } catch {}
|
|
1157
|
+
const cfg = readConfigAt(home);
|
|
1158
|
+
cfg.erp = {
|
|
1159
|
+
...(cfg.erp || {}),
|
|
1160
|
+
enabled: true,
|
|
1161
|
+
url: (cfg.erp && cfg.erp.url) || "https://portal.qualiasolutions.net",
|
|
1162
|
+
api_key_file: ".erp-api-key",
|
|
1163
|
+
};
|
|
1164
|
+
writeConfig(cfg, home);
|
|
1165
|
+
}
|
|
1024
1166
|
|
|
1025
|
-
console.log(` ${GREEN}✓${RESET} ERP key saved to ${WHITE}${
|
|
1167
|
+
console.log(` ${GREEN}✓${RESET} ERP key saved to ${WHITE}${homes.map((h) => path.join(h, ".erp-api-key")).join(", ")}${RESET}`);
|
|
1026
1168
|
console.log(` ${DIM}Verify with:${RESET} ${TEAL}qualia-framework erp-ping${RESET}`);
|
|
1027
1169
|
console.log("");
|
|
1028
1170
|
}
|
|
@@ -1040,7 +1182,7 @@ function cmdSetErpKey() {
|
|
|
1040
1182
|
// retry stranded reports on demand (e.g., after the ERP came back online,
|
|
1041
1183
|
// or after rotating the API key). All args pass through.
|
|
1042
1184
|
function cmdErpFlush() {
|
|
1043
|
-
const retryScript = path.join(
|
|
1185
|
+
const retryScript = path.join(primaryInstallHome(), "bin", "erp-retry.js");
|
|
1044
1186
|
if (!fs.existsSync(retryScript)) {
|
|
1045
1187
|
console.log(` ${RED}✗${RESET} erp-retry.js not installed at ${retryScript}`);
|
|
1046
1188
|
console.log(` ${DIM}Run: npx qualia-framework@latest install${RESET}`);
|
|
@@ -1058,8 +1200,25 @@ function cmdErpFlush() {
|
|
|
1058
1200
|
process.exit(r.status || 0);
|
|
1059
1201
|
}
|
|
1060
1202
|
|
|
1203
|
+
function cmdProjectSnapshot() {
|
|
1204
|
+
const snapshotScript = path.join(primaryInstallHome(), "bin", "project-snapshot.js");
|
|
1205
|
+
const localSnapshotScript = path.join(__dirname, "project-snapshot.js");
|
|
1206
|
+
const script = fs.existsSync(snapshotScript) ? snapshotScript : localSnapshotScript;
|
|
1207
|
+
if (!fs.existsSync(script)) {
|
|
1208
|
+
console.log(` ${RED}✗${RESET} project-snapshot.js not available`);
|
|
1209
|
+
console.log(` ${DIM}Run: npx qualia-framework@latest install${RESET}`);
|
|
1210
|
+
process.exit(1);
|
|
1211
|
+
}
|
|
1212
|
+
const args = process.argv.slice(3);
|
|
1213
|
+
const r = spawnSync(process.execPath, [script, ...args], {
|
|
1214
|
+
stdio: "inherit",
|
|
1215
|
+
shell: false,
|
|
1216
|
+
});
|
|
1217
|
+
process.exit(typeof r.status === "number" ? r.status : 1);
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1061
1220
|
function cmdFlush() {
|
|
1062
|
-
const flushScript = path.join(
|
|
1221
|
+
const flushScript = path.join(primaryInstallHome(), "bin", "knowledge-flush.js");
|
|
1063
1222
|
if (!fs.existsSync(flushScript)) {
|
|
1064
1223
|
console.log(` ${RED}✗${RESET} knowledge-flush.js not installed at ${flushScript}`);
|
|
1065
1224
|
console.log(` ${DIM}Run: npx qualia-framework@latest install${RESET}`);
|
|
@@ -1085,70 +1244,71 @@ function cmdDoctor() {
|
|
|
1085
1244
|
if (!ok) issues.push({ label, hint });
|
|
1086
1245
|
}
|
|
1087
1246
|
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
path.
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
"
|
|
1108
|
-
|
|
1109
|
-
|
|
1247
|
+
const homes = installedHomes().filter((home) => fs.existsSync(configFileForHome(home)));
|
|
1248
|
+
if (homes.length === 0) {
|
|
1249
|
+
check("Qualia install", false, "run: npx qualia-framework@latest install");
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
for (const home of homes) {
|
|
1253
|
+
const label = path.basename(home) === ".codex" ? "Codex" : "Claude";
|
|
1254
|
+
const coreFiles = [
|
|
1255
|
+
"rules/grounding.md",
|
|
1256
|
+
"rules/security.md",
|
|
1257
|
+
"rules/deployment.md",
|
|
1258
|
+
"bin/state.js",
|
|
1259
|
+
"bin/qualia-ui.js",
|
|
1260
|
+
"bin/statusline.js",
|
|
1261
|
+
"bin/knowledge.js",
|
|
1262
|
+
"bin/knowledge-flush.js",
|
|
1263
|
+
"bin/erp-retry.js",
|
|
1264
|
+
"bin/report-payload.js",
|
|
1265
|
+
"bin/project-snapshot.js",
|
|
1266
|
+
"knowledge/agents.md",
|
|
1267
|
+
"knowledge/index.md",
|
|
1268
|
+
"knowledge/daily-log",
|
|
1269
|
+
".qualia-config.json",
|
|
1270
|
+
];
|
|
1271
|
+
if (label === "Claude") coreFiles.push("CLAUDE.md", "settings.json");
|
|
1272
|
+
else coreFiles.push("AGENTS.md", "config.toml", "hooks.json", "agents/planner.toml", "skills/qualia-new/SKILL.md");
|
|
1273
|
+
|
|
1274
|
+
for (const rel of coreFiles) {
|
|
1275
|
+
check(`${label} ${rel}`, fs.existsSync(path.join(home, rel)), "run: npx qualia-framework@latest install");
|
|
1276
|
+
}
|
|
1110
1277
|
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
`hooks/${h}`,
|
|
1115
|
-
fs.existsSync(path.join(CLAUDE_DIR, "hooks", h)),
|
|
1116
|
-
"reinstall: npx qualia-framework@latest install",
|
|
1117
|
-
);
|
|
1118
|
-
}
|
|
1278
|
+
for (const h of QUALIA_HOOK_FILES) {
|
|
1279
|
+
check(`${label} hooks/${h}`, fs.existsSync(path.join(home, "hooks", h)), "reinstall: npx qualia-framework@latest install");
|
|
1280
|
+
}
|
|
1119
1281
|
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1282
|
+
if (label === "Claude" && fs.existsSync(path.join(home, "settings.json"))) {
|
|
1283
|
+
try {
|
|
1284
|
+
const settings = JSON.parse(fs.readFileSync(path.join(home, "settings.json"), "utf8"));
|
|
1285
|
+
for (const ev of ["SessionStart", "PreToolUse", "Stop"]) {
|
|
1286
|
+
const blocks = (settings.hooks || {})[ev] || [];
|
|
1287
|
+
const hasQualia = blocks.some((b) =>
|
|
1288
|
+
(b.hooks || []).some((h) => typeof h.command === "string" && h.command.includes(".claude")),
|
|
1289
|
+
);
|
|
1290
|
+
check(`Claude settings.json hooks.${ev}`, hasQualia, "reinstall to wire hooks");
|
|
1291
|
+
}
|
|
1292
|
+
check("Claude settings.json statusLine", !!(settings.statusLine && settings.statusLine.command && settings.statusLine.command.includes("statusline.js")), "reinstall to wire statusline");
|
|
1293
|
+
} catch (e) {
|
|
1294
|
+
check("Claude settings.json parseable", false, e.message);
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1133
1297
|
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
check(
|
|
1298
|
+
if (label === "Codex" && fs.existsSync(path.join(home, "hooks.json"))) {
|
|
1299
|
+
try {
|
|
1300
|
+
const hooksJson = JSON.parse(fs.readFileSync(path.join(home, "hooks.json"), "utf8"));
|
|
1301
|
+
for (const ev of ["SessionStart", "PreToolUse", "Stop"]) {
|
|
1302
|
+
const blocks = (hooksJson.hooks || {})[ev] || [];
|
|
1303
|
+
const hasQualia = blocks.some((b) =>
|
|
1304
|
+
(b.hooks || []).some((h) => typeof h.command === "string" && h.command.includes(".codex")),
|
|
1305
|
+
);
|
|
1306
|
+
check(`Codex hooks.json ${ev}`, hasQualia, "reinstall to wire Codex hooks");
|
|
1307
|
+
}
|
|
1308
|
+
} catch (e) {
|
|
1309
|
+
check("Codex hooks.json parseable", false, e.message);
|
|
1146
1310
|
}
|
|
1147
|
-
} catch (e) {
|
|
1148
|
-
check("settings.json parseable", false, e.message);
|
|
1149
1311
|
}
|
|
1150
|
-
} else {
|
|
1151
|
-
check("settings.json", false, "Claude Code never ran here? Open Claude once first");
|
|
1152
1312
|
}
|
|
1153
1313
|
|
|
1154
1314
|
// ── Version vs. installed ──────────────────────────────
|
|
@@ -1263,7 +1423,7 @@ function cmdHelp() {
|
|
|
1263
1423
|
console.log(` qualia-framework ${TEAL}install${RESET} Install or reinstall the framework`);
|
|
1264
1424
|
console.log(` qualia-framework ${TEAL}update${RESET} Update to the latest version`);
|
|
1265
1425
|
console.log(` qualia-framework ${TEAL}version${RESET} Show installed version + check for updates`);
|
|
1266
|
-
console.log(` qualia-framework ${TEAL}uninstall${RESET} Clean removal from
|
|
1426
|
+
console.log(` qualia-framework ${TEAL}uninstall${RESET} Clean removal from installed Claude/Codex homes (${DIM}-y to skip prompts${RESET})`);
|
|
1267
1427
|
console.log(` qualia-framework ${TEAL}migrate${RESET} Wire current hook + env layout into ~/.claude/settings.json`);
|
|
1268
1428
|
console.log(` qualia-framework ${TEAL}team${RESET} Manage team members (${DIM}list|add|remove${RESET})`);
|
|
1269
1429
|
console.log(` qualia-framework ${TEAL}traces${RESET} View recent hook telemetry`);
|
|
@@ -1272,6 +1432,7 @@ function cmdHelp() {
|
|
|
1272
1432
|
console.log(` qualia-framework ${TEAL}set-erp-key${RESET} Save/enable the ERP API key`);
|
|
1273
1433
|
console.log(` qualia-framework ${TEAL}erp-ping${RESET} Verify ERP connectivity + API key`);
|
|
1274
1434
|
console.log(` qualia-framework ${TEAL}erp-flush${RESET} Retry queued ERP report uploads (${DIM}show|clear${RESET})`);
|
|
1435
|
+
console.log(` qualia-framework ${TEAL}project-snapshot${RESET} Export/upload ERP admin project progress snapshot (${DIM}--write|--upload${RESET})`);
|
|
1275
1436
|
console.log(` qualia-framework ${TEAL}doctor${RESET} Health-check the install (files, hooks, settings)`);
|
|
1276
1437
|
console.log(` qualia-framework ${TEAL}flush${RESET} Promote daily-log → curated knowledge (memory layer)`);
|
|
1277
1438
|
console.log("");
|
|
@@ -1341,6 +1502,10 @@ switch (cmd) {
|
|
|
1341
1502
|
case "erp-retry":
|
|
1342
1503
|
cmdErpFlush();
|
|
1343
1504
|
break;
|
|
1505
|
+
case "project-snapshot":
|
|
1506
|
+
case "snapshot":
|
|
1507
|
+
cmdProjectSnapshot();
|
|
1508
|
+
break;
|
|
1344
1509
|
case "doctor":
|
|
1345
1510
|
case "health":
|
|
1346
1511
|
case "health-check":
|