qualia-framework 6.1.0 → 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/README.md +35 -26
- package/agents/roadmapper.md +1 -1
- package/bin/cli.js +339 -200
- 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/state.js +8 -1
- package/bin/statusline.js +14 -2
- 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 +1 -1
- 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 +16 -4
- package/hooks/auto-update.js +14 -7
- package/hooks/branch-guard.js +10 -2
- package/hooks/env-empty-guard.js +10 -1
- 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 +42 -162
- 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 +3 -2
- package/skills/qualia-map/SKILL.md +1 -1
- package/skills/qualia-milestone/SKILL.md +1 -1
- package/skills/qualia-optimize/SKILL.md +1 -1
- package/skills/qualia-polish/SKILL.md +2 -2
- package/skills/qualia-report/SKILL.md +6 -43
- package/skills/qualia-road/SKILL.md +1 -1
- package/skills/qualia-verify/SKILL.md +1 -1
- package/templates/help.html +1 -1
- 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 +411 -13
- 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 +42 -0
- package/tests/run-all.sh +1 -0
- package/tests/runner.js +19 -33
- package/tests/state.test.sh +4 -1
- package/hooks/pre-compact.js +0 -127
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;
|
|
23
42
|
try {
|
|
24
|
-
return JSON.parse(fs.readFileSync(
|
|
43
|
+
return JSON.parse(fs.readFileSync(configFile, "utf8"));
|
|
25
44
|
} catch {
|
|
26
45
|
return {};
|
|
27
46
|
}
|
|
28
47
|
}
|
|
29
48
|
|
|
30
|
-
function
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
49
|
+
function readConfigAt(home) {
|
|
50
|
+
try {
|
|
51
|
+
return JSON.parse(fs.readFileSync(configFileForHome(home), "utf8"));
|
|
52
|
+
} catch {
|
|
53
|
+
return {};
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
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,7 +202,6 @@ 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",
|
|
145
207
|
"env-empty-guard.js",
|
|
@@ -148,6 +210,7 @@ const QUALIA_HOOK_FILES = [
|
|
|
148
210
|
];
|
|
149
211
|
const QUALIA_LEGACY_HOOK_FILES = [
|
|
150
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
|
|
151
214
|
];
|
|
152
215
|
|
|
153
216
|
// Qualia agents — only these are removed.
|
|
@@ -162,6 +225,7 @@ const QUALIA_AGENT_FILES = [
|
|
|
162
225
|
"roadmapper.md",
|
|
163
226
|
"visual-evaluator.md",
|
|
164
227
|
];
|
|
228
|
+
const QUALIA_CODEX_AGENT_FILES = QUALIA_AGENT_FILES.map((f) => f.replace(/\.md$/, ".toml"));
|
|
165
229
|
|
|
166
230
|
// Qualia bin scripts.
|
|
167
231
|
const QUALIA_BIN_FILES = [
|
|
@@ -174,12 +238,15 @@ const QUALIA_BIN_FILES = [
|
|
|
174
238
|
"agent-runs.js",
|
|
175
239
|
"slop-detect.mjs",
|
|
176
240
|
"erp-retry.js",
|
|
241
|
+
"report-payload.js",
|
|
242
|
+
"project-snapshot.js",
|
|
177
243
|
];
|
|
178
244
|
|
|
179
245
|
// Qualia rules — security, deployment, infra, grounding, plus the v4.5.0 design substrate.
|
|
180
246
|
// frontend.md and design-reference.md are kept for backward compat; new projects use design-laws/brand/product/rubric.
|
|
181
247
|
const QUALIA_RULE_FILES = [
|
|
182
248
|
"security.md", "deployment.md", "infrastructure.md", "grounding.md",
|
|
249
|
+
"speed.md", "architecture.md", "trust-boundary.md", "one-opinion.md",
|
|
183
250
|
"frontend.md", "design-reference.md",
|
|
184
251
|
"design-laws.md", "design-brand.md", "design-product.md", "design-rubric.md",
|
|
185
252
|
];
|
|
@@ -219,46 +286,55 @@ function safeRmDir(p, counters) {
|
|
|
219
286
|
}
|
|
220
287
|
}
|
|
221
288
|
|
|
222
|
-
function
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
return;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// Only remove entries that point at qualia paths. Leave everything else.
|
|
234
|
-
const isQualiaCommand = (cmd) =>
|
|
235
|
-
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
|
+
}
|
|
236
297
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
|
240
305
|
.map((entry) => {
|
|
241
306
|
if (!entry || !Array.isArray(entry.hooks)) return entry;
|
|
242
307
|
const hooks = entry.hooks.filter((h) => !isQualiaCommand(h && h.command));
|
|
308
|
+
if (hooks.length !== entry.hooks.length) changed = true;
|
|
243
309
|
return { ...entry, hooks };
|
|
244
310
|
})
|
|
245
311
|
.filter((entry) => Array.isArray(entry.hooks) && entry.hooks.length > 0);
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
for (const key of Object.keys(settings.hooks)) {
|
|
252
|
-
const cleaned = filterHookArray(settings.hooks[key]);
|
|
253
|
-
if (cleaned && cleaned.length > 0) {
|
|
254
|
-
settings.hooks[key] = cleaned;
|
|
255
|
-
} else {
|
|
256
|
-
delete settings.hooks[key];
|
|
257
|
-
}
|
|
312
|
+
if (cleaned.length > 0) {
|
|
313
|
+
root[hookKey][key] = cleaned;
|
|
314
|
+
} else {
|
|
315
|
+
delete root[hookKey][key];
|
|
316
|
+
changed = true;
|
|
258
317
|
}
|
|
259
|
-
// If hooks is now empty, remove it entirely.
|
|
260
|
-
if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
|
|
261
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");
|
|
262
338
|
|
|
263
339
|
// Status line — only drop it if it points at our renderer.
|
|
264
340
|
if (settings.statusLine && typeof settings.statusLine === "object") {
|
|
@@ -282,19 +358,40 @@ function cleanSettingsJson(counters) {
|
|
|
282
358
|
}
|
|
283
359
|
}
|
|
284
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
|
+
|
|
285
380
|
async function cmdUninstall() {
|
|
286
381
|
banner();
|
|
287
382
|
|
|
288
383
|
const args = process.argv.slice(3);
|
|
289
384
|
const skipConfirm = args.includes("-y") || args.includes("--yes");
|
|
290
385
|
|
|
386
|
+
const homes = frameworkHomes();
|
|
291
387
|
const cfg = readConfig();
|
|
292
388
|
console.log("");
|
|
293
389
|
if (cfg.installed_by) {
|
|
294
390
|
console.log(` ${DIM}User:${RESET} ${WHITE}${cfg.installed_by}${RESET} ${DIM}(${cfg.role || "?"})${RESET}`);
|
|
295
391
|
} else {
|
|
296
|
-
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}`);
|
|
297
393
|
}
|
|
394
|
+
console.log(` ${DIM}Targets:${RESET} ${WHITE}${homes.map(installLabel).join(" · ")}${RESET}`);
|
|
298
395
|
console.log("");
|
|
299
396
|
|
|
300
397
|
if (!skipConfirm) {
|
|
@@ -320,62 +417,60 @@ async function cmdUninstall() {
|
|
|
320
417
|
console.log(` ${DIM}Removing framework files...${RESET}`);
|
|
321
418
|
console.log("");
|
|
322
419
|
|
|
323
|
-
const counters = { filesRemoved: 0, dirsRemoved: 0, settingsCleaned: false, errors: [] };
|
|
420
|
+
const counters = { filesRemoved: 0, dirsRemoved: 0, settingsCleaned: false, hooksJsonCleaned: false, errors: [] };
|
|
324
421
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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
|
+
}
|
|
332
430
|
}
|
|
333
431
|
}
|
|
432
|
+
} catch (e) {
|
|
433
|
+
counters.errors.push(`${installLabel(home)} skills scan: ${e.message}`);
|
|
334
434
|
}
|
|
335
|
-
} catch (e) {
|
|
336
|
-
counters.errors.push(`skills scan: ${e.message}`);
|
|
337
|
-
}
|
|
338
435
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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
|
+
}
|
|
343
442
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
}
|
|
443
|
+
for (const f of [...QUALIA_HOOK_FILES, ...QUALIA_LEGACY_HOOK_FILES]) {
|
|
444
|
+
safeUnlink(path.join(home, "hooks", f), counters);
|
|
445
|
+
}
|
|
348
446
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
}
|
|
447
|
+
for (const f of QUALIA_BIN_FILES) {
|
|
448
|
+
safeUnlink(path.join(home, "bin", f), counters);
|
|
449
|
+
}
|
|
353
450
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
}
|
|
451
|
+
for (const f of QUALIA_RULE_FILES) {
|
|
452
|
+
safeUnlink(path.join(home, "rules", f), counters);
|
|
453
|
+
}
|
|
358
454
|
|
|
359
|
-
|
|
360
|
-
|
|
455
|
+
safeRmDir(path.join(home, "qualia-templates"), counters);
|
|
456
|
+
safeRmDir(path.join(home, "qualia-design"), counters);
|
|
457
|
+
safeRmDir(path.join(home, "qualia-references"), counters);
|
|
361
458
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
}
|
|
459
|
+
if (!preserveKnowledge) {
|
|
460
|
+
safeRmDir(path.join(home, "knowledge"), counters);
|
|
461
|
+
}
|
|
366
462
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
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);
|
|
373
468
|
|
|
374
|
-
|
|
375
|
-
safeRmDir(path.join(CLAUDE_DIR, ".qualia-traces"), counters);
|
|
469
|
+
safeRmDir(path.join(home, ".qualia-traces"), counters);
|
|
376
470
|
|
|
377
|
-
|
|
378
|
-
|
|
471
|
+
if (isCodexHome(home)) cleanCodexHooksJson(home, counters);
|
|
472
|
+
else cleanSettingsJson(home, counters);
|
|
473
|
+
}
|
|
379
474
|
|
|
380
475
|
// Summary.
|
|
381
476
|
console.log("");
|
|
@@ -386,6 +481,9 @@ async function cmdUninstall() {
|
|
|
386
481
|
console.log(
|
|
387
482
|
` ${DIM}settings.json:${RESET} ${counters.settingsCleaned ? `${GREEN}cleaned ✓${RESET}` : `${DIM}not present${RESET}`}`
|
|
388
483
|
);
|
|
484
|
+
console.log(
|
|
485
|
+
` ${DIM}hooks.json:${RESET} ${counters.hooksJsonCleaned ? `${GREEN}cleaned ✓${RESET}` : `${DIM}not present${RESET}`}`
|
|
486
|
+
);
|
|
389
487
|
if (preserveKnowledge) {
|
|
390
488
|
console.log(` ${DIM}Knowledge base:${RESET} ${GREEN}preserved ✓${RESET}`);
|
|
391
489
|
} else {
|
|
@@ -401,14 +499,13 @@ async function cmdUninstall() {
|
|
|
401
499
|
}
|
|
402
500
|
|
|
403
501
|
console.log("");
|
|
404
|
-
console.log(
|
|
405
|
-
` ${YELLOW}Manual step:${RESET} edit ${WHITE}~/.claude/CLAUDE.md${RESET} to remove the Qualia Framework section if desired.`
|
|
406
|
-
);
|
|
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.`);
|
|
407
503
|
console.log("");
|
|
408
504
|
}
|
|
409
505
|
|
|
410
506
|
// ─── Team Management ────────────────────────────────────
|
|
411
|
-
// External team file at ~/.claude
|
|
507
|
+
// External team file at the active install home. Falls back to ~/.claude before
|
|
508
|
+
// install for backward compatibility.
|
|
412
509
|
// Falls back to embedded defaults in install.js.
|
|
413
510
|
|
|
414
511
|
function getDefaultTeam() {
|
|
@@ -422,19 +519,23 @@ function getDefaultTeam() {
|
|
|
422
519
|
}
|
|
423
520
|
|
|
424
521
|
function readTeamFile() {
|
|
425
|
-
const
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
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
|
+
}
|
|
432
531
|
return null;
|
|
433
532
|
}
|
|
434
533
|
|
|
435
534
|
function writeTeamFile(team) {
|
|
436
|
-
|
|
437
|
-
|
|
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
|
+
}
|
|
438
539
|
}
|
|
439
540
|
|
|
440
541
|
function parseTeamArgs(argv) {
|
|
@@ -456,7 +557,7 @@ function cmdTeam() {
|
|
|
456
557
|
console.log("");
|
|
457
558
|
const team = readTeamFile();
|
|
458
559
|
const source = team || getDefaultTeam();
|
|
459
|
-
const label = team ? "team file" : "embedded defaults";
|
|
560
|
+
const label = team ? "installed team file" : "embedded defaults";
|
|
460
561
|
console.log(` ${DIM}Source: ${label}${RESET}`);
|
|
461
562
|
console.log("");
|
|
462
563
|
for (const [code, member] of Object.entries(source)) {
|
|
@@ -516,7 +617,7 @@ function cmdTeam() {
|
|
|
516
617
|
function cmdTraces() {
|
|
517
618
|
banner();
|
|
518
619
|
console.log("");
|
|
519
|
-
const tracesDir = path.join(
|
|
620
|
+
const tracesDir = path.join(primaryInstallHome(), ".qualia-traces");
|
|
520
621
|
if (!fs.existsSync(tracesDir)) {
|
|
521
622
|
console.log(` ${DIM}No traces found. Traces are written by hooks during normal operation.${RESET}`);
|
|
522
623
|
console.log("");
|
|
@@ -552,6 +653,15 @@ function cmdMigrate() {
|
|
|
552
653
|
|
|
553
654
|
const settingsPath = path.join(CLAUDE_DIR, "settings.json");
|
|
554
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
|
+
}
|
|
555
665
|
console.log(` ${RED}✗${RESET} No settings.json found. Run ${TEAL}qualia-framework install${RESET} first.`);
|
|
556
666
|
console.log("");
|
|
557
667
|
process.exit(1);
|
|
@@ -667,23 +777,22 @@ function cmdMigrate() {
|
|
|
667
777
|
}
|
|
668
778
|
}
|
|
669
779
|
|
|
670
|
-
//
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
if (
|
|
684
|
-
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) {
|
|
685
794
|
changes++;
|
|
686
|
-
console.log(` ${GREEN}
|
|
795
|
+
console.log(` ${GREEN}-${RESET} Removed legacy pre-compact.js from PreCompact`);
|
|
687
796
|
}
|
|
688
797
|
}
|
|
689
798
|
|
|
@@ -764,7 +873,7 @@ function cmdAnalytics() {
|
|
|
764
873
|
banner();
|
|
765
874
|
console.log("");
|
|
766
875
|
|
|
767
|
-
const tracesDir = path.join(
|
|
876
|
+
const tracesDir = path.join(primaryInstallHome(), ".qualia-traces");
|
|
768
877
|
if (!fs.existsSync(tracesDir)) {
|
|
769
878
|
console.log(` ${DIM}No traces found. Analytics require hook telemetry data.${RESET}`);
|
|
770
879
|
console.log(` ${DIM}Traces are collected automatically during normal framework use.${RESET}`);
|
|
@@ -854,11 +963,12 @@ async function cmdErpPing() {
|
|
|
854
963
|
banner();
|
|
855
964
|
console.log("");
|
|
856
965
|
|
|
966
|
+
const installHome = primaryInstallHome();
|
|
857
967
|
const cfg = readConfig();
|
|
858
968
|
const args = new Set(process.argv.slice(3));
|
|
859
969
|
const erpUrl = (cfg.erp && cfg.erp.url) || "https://portal.qualiasolutions.net";
|
|
860
970
|
let erpEnabled = !(cfg.erp && cfg.erp.enabled === false);
|
|
861
|
-
const keyFile = path.join(
|
|
971
|
+
const keyFile = path.join(installHome, ".erp-api-key");
|
|
862
972
|
|
|
863
973
|
console.log(` ${DIM}URL:${RESET} ${WHITE}${erpUrl}${RESET}`);
|
|
864
974
|
console.log(` ${DIM}Enabled:${RESET} ${erpEnabled ? `${GREEN}yes${RESET}` : `${YELLOW}no (erp.enabled=false)${RESET}`}`);
|
|
@@ -989,17 +1099,21 @@ function cmdSetErpKey() {
|
|
|
989
1099
|
banner();
|
|
990
1100
|
console.log("");
|
|
991
1101
|
|
|
992
|
-
const
|
|
1102
|
+
const homes = installedHomes();
|
|
993
1103
|
const rawArgs = process.argv.slice(3);
|
|
994
1104
|
const clear = rawArgs.includes("--clear");
|
|
995
1105
|
|
|
996
|
-
|
|
1106
|
+
for (const home of homes) {
|
|
1107
|
+
if (!fs.existsSync(home)) fs.mkdirSync(home, { recursive: true });
|
|
1108
|
+
}
|
|
997
1109
|
|
|
998
1110
|
if (clear) {
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
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
|
+
}
|
|
1003
1117
|
console.log(` ${GREEN}✓${RESET} ERP key removed and ERP disabled.`);
|
|
1004
1118
|
console.log("");
|
|
1005
1119
|
return;
|
|
@@ -1036,19 +1150,21 @@ function cmdSetErpKey() {
|
|
|
1036
1150
|
console.log(` ${YELLOW}!${RESET} Key looks short (${key.length} bytes). Saving anyway.`);
|
|
1037
1151
|
}
|
|
1038
1152
|
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
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
|
+
}
|
|
1050
1166
|
|
|
1051
|
-
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}`);
|
|
1052
1168
|
console.log(` ${DIM}Verify with:${RESET} ${TEAL}qualia-framework erp-ping${RESET}`);
|
|
1053
1169
|
console.log("");
|
|
1054
1170
|
}
|
|
@@ -1066,7 +1182,7 @@ function cmdSetErpKey() {
|
|
|
1066
1182
|
// retry stranded reports on demand (e.g., after the ERP came back online,
|
|
1067
1183
|
// or after rotating the API key). All args pass through.
|
|
1068
1184
|
function cmdErpFlush() {
|
|
1069
|
-
const retryScript = path.join(
|
|
1185
|
+
const retryScript = path.join(primaryInstallHome(), "bin", "erp-retry.js");
|
|
1070
1186
|
if (!fs.existsSync(retryScript)) {
|
|
1071
1187
|
console.log(` ${RED}✗${RESET} erp-retry.js not installed at ${retryScript}`);
|
|
1072
1188
|
console.log(` ${DIM}Run: npx qualia-framework@latest install${RESET}`);
|
|
@@ -1084,8 +1200,25 @@ function cmdErpFlush() {
|
|
|
1084
1200
|
process.exit(r.status || 0);
|
|
1085
1201
|
}
|
|
1086
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
|
+
|
|
1087
1220
|
function cmdFlush() {
|
|
1088
|
-
const flushScript = path.join(
|
|
1221
|
+
const flushScript = path.join(primaryInstallHome(), "bin", "knowledge-flush.js");
|
|
1089
1222
|
if (!fs.existsSync(flushScript)) {
|
|
1090
1223
|
console.log(` ${RED}✗${RESET} knowledge-flush.js not installed at ${flushScript}`);
|
|
1091
1224
|
console.log(` ${DIM}Run: npx qualia-framework@latest install${RESET}`);
|
|
@@ -1111,70 +1244,71 @@ function cmdDoctor() {
|
|
|
1111
1244
|
if (!ok) issues.push({ label, hint });
|
|
1112
1245
|
}
|
|
1113
1246
|
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
path.
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
"
|
|
1134
|
-
|
|
1135
|
-
|
|
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
|
+
}
|
|
1136
1277
|
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
`hooks/${h}`,
|
|
1141
|
-
fs.existsSync(path.join(CLAUDE_DIR, "hooks", h)),
|
|
1142
|
-
"reinstall: npx qualia-framework@latest install",
|
|
1143
|
-
);
|
|
1144
|
-
}
|
|
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
|
+
}
|
|
1145
1281
|
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
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
|
+
}
|
|
1159
1297
|
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
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);
|
|
1172
1310
|
}
|
|
1173
|
-
} catch (e) {
|
|
1174
|
-
check("settings.json parseable", false, e.message);
|
|
1175
1311
|
}
|
|
1176
|
-
} else {
|
|
1177
|
-
check("settings.json", false, "Claude Code never ran here? Open Claude once first");
|
|
1178
1312
|
}
|
|
1179
1313
|
|
|
1180
1314
|
// ── Version vs. installed ──────────────────────────────
|
|
@@ -1289,7 +1423,7 @@ function cmdHelp() {
|
|
|
1289
1423
|
console.log(` qualia-framework ${TEAL}install${RESET} Install or reinstall the framework`);
|
|
1290
1424
|
console.log(` qualia-framework ${TEAL}update${RESET} Update to the latest version`);
|
|
1291
1425
|
console.log(` qualia-framework ${TEAL}version${RESET} Show installed version + check for updates`);
|
|
1292
|
-
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})`);
|
|
1293
1427
|
console.log(` qualia-framework ${TEAL}migrate${RESET} Wire current hook + env layout into ~/.claude/settings.json`);
|
|
1294
1428
|
console.log(` qualia-framework ${TEAL}team${RESET} Manage team members (${DIM}list|add|remove${RESET})`);
|
|
1295
1429
|
console.log(` qualia-framework ${TEAL}traces${RESET} View recent hook telemetry`);
|
|
@@ -1298,6 +1432,7 @@ function cmdHelp() {
|
|
|
1298
1432
|
console.log(` qualia-framework ${TEAL}set-erp-key${RESET} Save/enable the ERP API key`);
|
|
1299
1433
|
console.log(` qualia-framework ${TEAL}erp-ping${RESET} Verify ERP connectivity + API key`);
|
|
1300
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})`);
|
|
1301
1436
|
console.log(` qualia-framework ${TEAL}doctor${RESET} Health-check the install (files, hooks, settings)`);
|
|
1302
1437
|
console.log(` qualia-framework ${TEAL}flush${RESET} Promote daily-log → curated knowledge (memory layer)`);
|
|
1303
1438
|
console.log("");
|
|
@@ -1367,6 +1502,10 @@ switch (cmd) {
|
|
|
1367
1502
|
case "erp-retry":
|
|
1368
1503
|
cmdErpFlush();
|
|
1369
1504
|
break;
|
|
1505
|
+
case "project-snapshot":
|
|
1506
|
+
case "snapshot":
|
|
1507
|
+
cmdProjectSnapshot();
|
|
1508
|
+
break;
|
|
1370
1509
|
case "doctor":
|
|
1371
1510
|
case "health":
|
|
1372
1511
|
case "health-check":
|