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/erp-retry.js
CHANGED
|
@@ -40,9 +40,17 @@ const http = require("http");
|
|
|
40
40
|
const urlLib = require("url");
|
|
41
41
|
|
|
42
42
|
const HOME = os.homedir();
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const
|
|
43
|
+
function qualiaHome() {
|
|
44
|
+
if (process.env.QUALIA_HOME) return process.env.QUALIA_HOME;
|
|
45
|
+
const parent = path.basename(path.dirname(__dirname));
|
|
46
|
+
if (parent === ".codex" || parent === ".claude") return path.dirname(__dirname);
|
|
47
|
+
return path.join(HOME, ".claude");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const QUALIA_HOME = qualiaHome();
|
|
51
|
+
const QUEUE_FILE = path.join(QUALIA_HOME, ".erp-retry-queue.json");
|
|
52
|
+
const API_KEY_FILE = path.join(QUALIA_HOME, ".erp-api-key");
|
|
53
|
+
const CONFIG_FILE = path.join(QUALIA_HOME, ".qualia-config.json");
|
|
46
54
|
|
|
47
55
|
const MAX_GIVE_UP_ATTEMPTS = 10;
|
|
48
56
|
const DEFAULT_TIMEOUT_MS = 5000;
|
package/bin/install.js
CHANGED
|
@@ -58,15 +58,17 @@ const DEFAULT_TEAM = {
|
|
|
58
58
|
|
|
59
59
|
// Load team from external file, fall back to embedded defaults.
|
|
60
60
|
function loadTeam() {
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
61
|
+
for (const home of [CLAUDE_DIR, CODEX_DIR]) {
|
|
62
|
+
const teamFile = path.join(home, ".qualia-team.json");
|
|
63
|
+
try {
|
|
64
|
+
if (fs.existsSync(teamFile)) {
|
|
65
|
+
const external = JSON.parse(fs.readFileSync(teamFile, "utf8"));
|
|
66
|
+
if (external && typeof external === "object" && Object.keys(external).length > 0) {
|
|
67
|
+
return external;
|
|
68
|
+
}
|
|
67
69
|
}
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
+
} catch {}
|
|
71
|
+
}
|
|
70
72
|
return DEFAULT_TEAM;
|
|
71
73
|
}
|
|
72
74
|
|
|
@@ -123,6 +125,99 @@ function copyTree(src, dest) {
|
|
|
123
125
|
}
|
|
124
126
|
}
|
|
125
127
|
|
|
128
|
+
function codexText(content) {
|
|
129
|
+
return String(content)
|
|
130
|
+
.replaceAll("~/.claude/", "~/.codex/")
|
|
131
|
+
.replaceAll("$HOME/.claude/", "$HOME/.codex/")
|
|
132
|
+
.replaceAll("${HOME}/.claude/", "${HOME}/.codex/")
|
|
133
|
+
.replaceAll("@~/.claude/", "@~/.codex/")
|
|
134
|
+
.replaceAll(".claude/", ".codex/");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function copyTextTransform(src, dest, transform) {
|
|
138
|
+
const destDir = path.dirname(dest);
|
|
139
|
+
if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true });
|
|
140
|
+
const content = fs.readFileSync(src, "utf8");
|
|
141
|
+
fs.writeFileSync(dest, transform(content), "utf8");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function copyTreeTransform(src, dest, transform) {
|
|
145
|
+
if (!fs.existsSync(src)) return;
|
|
146
|
+
if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
|
|
147
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
148
|
+
if (entry.name.startsWith(".")) continue;
|
|
149
|
+
const srcPath = path.join(src, entry.name);
|
|
150
|
+
const destPath = path.join(dest, entry.name);
|
|
151
|
+
if (entry.isDirectory()) {
|
|
152
|
+
copyTreeTransform(srcPath, destPath, transform);
|
|
153
|
+
} else if (entry.isFile()) {
|
|
154
|
+
copyTextTransform(srcPath, destPath, transform);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function backupIfDifferent(dest, nextContent, label) {
|
|
160
|
+
if (!fs.existsSync(dest)) return false;
|
|
161
|
+
try {
|
|
162
|
+
const existing = fs.readFileSync(dest, "utf8");
|
|
163
|
+
if (existing === nextContent) return false;
|
|
164
|
+
const ts = new Date().toISOString().replace(/[:.]/g, "-");
|
|
165
|
+
const bak = `${dest}.bak.${ts}`;
|
|
166
|
+
fs.copyFileSync(dest, bak);
|
|
167
|
+
ok(`Backed up existing ${label} -> ${path.basename(bak)}`);
|
|
168
|
+
return true;
|
|
169
|
+
} catch {
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function atomicWrite(dest, content, mode) {
|
|
175
|
+
const destDir = path.dirname(dest);
|
|
176
|
+
if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true });
|
|
177
|
+
const tmp = `${dest}.tmp.${process.pid}`;
|
|
178
|
+
if (mode) fs.writeFileSync(tmp, content, { mode });
|
|
179
|
+
else fs.writeFileSync(tmp, content, "utf8");
|
|
180
|
+
fs.renameSync(tmp, dest);
|
|
181
|
+
if (mode) {
|
|
182
|
+
try { fs.chmodSync(dest, mode); } catch {}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function tomlString(value) {
|
|
187
|
+
return JSON.stringify(String(value == null ? "" : value));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function parseAgentMarkdown(content) {
|
|
191
|
+
const result = { name: "", description: "", body: content };
|
|
192
|
+
if (!content.startsWith("---\n")) return result;
|
|
193
|
+
const end = content.indexOf("\n---", 4);
|
|
194
|
+
if (end === -1) return result;
|
|
195
|
+
const fm = content.slice(4, end).trim().split(/\r?\n/);
|
|
196
|
+
for (const line of fm) {
|
|
197
|
+
const match = line.match(/^([a-zA-Z0-9_-]+):\s*(.*)$/);
|
|
198
|
+
if (!match) continue;
|
|
199
|
+
const key = match[1];
|
|
200
|
+
const value = match[2].replace(/^["']|["']$/g, "").trim();
|
|
201
|
+
if (key === "name") result.name = value;
|
|
202
|
+
if (key === "description") result.description = value;
|
|
203
|
+
}
|
|
204
|
+
result.body = content.slice(end + "\n---".length).replace(/^\s+/, "");
|
|
205
|
+
return result;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function renderCodexAgentToml(markdown) {
|
|
209
|
+
const parsed = parseAgentMarkdown(markdown);
|
|
210
|
+
const body = parsed.body
|
|
211
|
+
.replaceAll("~/.claude/", "~/.codex/")
|
|
212
|
+
.replaceAll("@~/.claude/", "@~/.codex/");
|
|
213
|
+
const description = parsed.description || "Qualia Framework specialist agent.";
|
|
214
|
+
return [
|
|
215
|
+
`description = ${tomlString(description)}`,
|
|
216
|
+
`developer_instructions = ${tomlString(body)}`,
|
|
217
|
+
"",
|
|
218
|
+
].join("\n");
|
|
219
|
+
}
|
|
220
|
+
|
|
126
221
|
// Surgically remove orphaned v2.6 install cruft from ~/.claude/ on upgrade.
|
|
127
222
|
// v2.6 installed a separate ~/.claude/qualia-framework/ directory with workflows/,
|
|
128
223
|
// references/, assets/, bin/qualia-tools.js. v3 doesn't use any of that — it was
|
|
@@ -286,7 +381,7 @@ function askTarget() {
|
|
|
286
381
|
console.log(` ${WHITE}Where would you like to install Qualia?${RESET}`);
|
|
287
382
|
console.log("");
|
|
288
383
|
console.log(` ${TEAL}[1]${RESET} ${WHITE}Claude Code only${RESET} ${DIM}— recommended, full feature set${RESET}`);
|
|
289
|
-
console.log(` ${TEAL}[2]${RESET} ${WHITE}OpenAI Codex only${RESET} ${DIM}— AGENTS.md
|
|
384
|
+
console.log(` ${TEAL}[2]${RESET} ${WHITE}OpenAI Codex only${RESET} ${DIM}— AGENTS.md + hooks + agents + runtime${RESET}`);
|
|
290
385
|
console.log(` ${TEAL}[3]${RESET} ${WHITE}Both${RESET} ${DIM}— max compatibility${RESET}`);
|
|
291
386
|
console.log("");
|
|
292
387
|
|
|
@@ -486,9 +581,14 @@ async function main() {
|
|
|
486
581
|
}
|
|
487
582
|
}
|
|
488
583
|
} catch {}
|
|
489
|
-
//
|
|
490
|
-
// block-env-edit.js
|
|
491
|
-
|
|
584
|
+
// Purge deprecated hooks from existing installs on upgrade.
|
|
585
|
+
// - block-env-edit.js (v3.2.0): team now has full read/write on .env*.
|
|
586
|
+
// - pre-compact.js (v6.2.0): bot-committed STATE.md + tracking.json on
|
|
587
|
+
// context compaction for ERP visibility. ERP never read tracking.json
|
|
588
|
+
// from git, and state.js already provides crash-safe atomic writes with
|
|
589
|
+
// a write-ahead journal (state.js:36-64) — the bot commit added no
|
|
590
|
+
// durability, just history noise.
|
|
591
|
+
const DEPRECATED_HOOKS = ["block-env-edit.js", "pre-compact.js"];
|
|
492
592
|
for (const f of DEPRECATED_HOOKS) {
|
|
493
593
|
const p = path.join(hooksDest, f);
|
|
494
594
|
try { if (fs.existsSync(p)) fs.unlinkSync(p); } catch {}
|
|
@@ -671,6 +771,18 @@ async function main() {
|
|
|
671
771
|
);
|
|
672
772
|
fs.chmodSync(path.join(binDest, "erp-retry.js"), 0o755);
|
|
673
773
|
ok("erp-retry.js (ERP report retry queue — drained by session-start hook and erp-flush CLI)");
|
|
774
|
+
copy(
|
|
775
|
+
path.join(FRAMEWORK_DIR, "bin", "report-payload.js"),
|
|
776
|
+
path.join(binDest, "report-payload.js")
|
|
777
|
+
);
|
|
778
|
+
fs.chmodSync(path.join(binDest, "report-payload.js"), 0o755);
|
|
779
|
+
ok("report-payload.js (Framework -> ERP report payload builder)");
|
|
780
|
+
copy(
|
|
781
|
+
path.join(FRAMEWORK_DIR, "bin", "project-snapshot.js"),
|
|
782
|
+
path.join(binDest, "project-snapshot.js")
|
|
783
|
+
);
|
|
784
|
+
fs.chmodSync(path.join(binDest, "project-snapshot.js"), 0o755);
|
|
785
|
+
ok("project-snapshot.js (ERP/admin project progress snapshot)");
|
|
674
786
|
} catch (e) {
|
|
675
787
|
warn(`scripts — ${e.message}`);
|
|
676
788
|
}
|
|
@@ -835,7 +947,7 @@ Client-specific preferences, design choices, and requirements. Loaded by \`/qual
|
|
|
835
947
|
try { fs.chmodSync(configFile, 0o600); } catch {}
|
|
836
948
|
} catch {}
|
|
837
949
|
log(`${YELLOW}!${RESET} ERP key not configured — reports won't upload until set.`);
|
|
838
|
-
log(`${DIM} Set with:${RESET} ${TEAL}qualia-framework set-erp-key
|
|
950
|
+
log(`${DIM} Set with:${RESET} ${TEAL}printf '%s' "$QUALIA_ERP_KEY" | qualia-framework set-erp-key${RESET}`);
|
|
839
951
|
log(`${DIM} or:${RESET} ${TEAL}export QUALIA_ERP_KEY=...${RESET} ${DIM}then re-install.${RESET}`);
|
|
840
952
|
log(`${DIM} Get a key from Fawzi.${RESET}`);
|
|
841
953
|
}
|
|
@@ -907,7 +1019,7 @@ Client-specific preferences, design choices, and requirements. Loaded by \`/qual
|
|
|
907
1019
|
"⬢ Feature branches only — never push to main",
|
|
908
1020
|
"⬢ Read before write — no exceptions",
|
|
909
1021
|
"⬢ MVP first — build what's asked, nothing extra",
|
|
910
|
-
"⬢ tracking.json
|
|
1022
|
+
"⬢ tracking.json is local telemetry — no git pollution",
|
|
911
1023
|
],
|
|
912
1024
|
};
|
|
913
1025
|
|
|
@@ -961,14 +1073,11 @@ Client-specific preferences, design choices, and requirements. Loaded by \`/qual
|
|
|
961
1073
|
],
|
|
962
1074
|
},
|
|
963
1075
|
],
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
],
|
|
970
|
-
},
|
|
971
|
-
],
|
|
1076
|
+
// v6.2.0: PreCompact intentionally empty. The qualiaHooks loop in the
|
|
1077
|
+
// settings.json merge below still iterates over this key, so legacy
|
|
1078
|
+
// Qualia-owned pre-compact.js entries get stripped from existing user
|
|
1079
|
+
// settings on upgrade. Nothing new is wired in.
|
|
1080
|
+
PreCompact: [],
|
|
972
1081
|
Stop: [
|
|
973
1082
|
{
|
|
974
1083
|
matcher: ".*",
|
|
@@ -991,7 +1100,15 @@ Client-specific preferences, design choices, and requirements. Loaded by \`/qual
|
|
|
991
1100
|
if (kept.length > 0) cleaned.push({ ...block, hooks: kept });
|
|
992
1101
|
}
|
|
993
1102
|
// Append our canonical blocks after the preserved user ones.
|
|
994
|
-
|
|
1103
|
+
const merged = [...cleaned, ...qualiaHooks[event]];
|
|
1104
|
+
if (merged.length > 0) {
|
|
1105
|
+
settings.hooks[event] = merged;
|
|
1106
|
+
} else {
|
|
1107
|
+
// No hooks left for this event (e.g. PreCompact after v6.2.0 removal) —
|
|
1108
|
+
// drop the key entirely rather than leaving an empty array sitting in
|
|
1109
|
+
// settings.json.
|
|
1110
|
+
delete settings.hooks[event];
|
|
1111
|
+
}
|
|
995
1112
|
}
|
|
996
1113
|
|
|
997
1114
|
// Permissions stay permissive; Qualia policy enforcement happens in hooks so
|
|
@@ -1025,7 +1142,7 @@ Client-specific preferences, design choices, and requirements. Loaded by \`/qual
|
|
|
1025
1142
|
fs.writeFileSync(settingsTmp, JSON.stringify(settings, null, 2));
|
|
1026
1143
|
fs.renameSync(settingsTmp, settingsPath);
|
|
1027
1144
|
|
|
1028
|
-
ok("Hooks: session-start, auto-update, branch-guard, pre-push, migration-guard, deploy-gate,
|
|
1145
|
+
ok("Hooks: session-start, auto-update, branch-guard, pre-push, migration-guard, deploy-gate, git-guardrails, stop-session-log, vercel-account-guard, env-empty-guard, supabase-destructive-guard");
|
|
1029
1146
|
ok("Status line + spinner configured");
|
|
1030
1147
|
ok("Environment variables + permissions");
|
|
1031
1148
|
|
|
@@ -1104,9 +1221,9 @@ function printSummary({ member, target, claudeInstalled }) {
|
|
|
1104
1221
|
console.log(` ${TEAL}3.${RESET} ${WHITE}Try${RESET} ${TEAL}${BOLD}${firstCmd}${RESET} ${DIM}— ${firstCmdHint}${RESET}`);
|
|
1105
1222
|
} else {
|
|
1106
1223
|
// Codex-only path
|
|
1107
|
-
console.log(` ${TEAL}1.${RESET} ${WHITE}
|
|
1108
|
-
console.log(` ${TEAL}2.${RESET} ${WHITE}Codex
|
|
1109
|
-
console.log(` ${TEAL}3.${RESET} ${WHITE}
|
|
1224
|
+
console.log(` ${TEAL}1.${RESET} ${WHITE}Restart Codex${RESET} ${DIM}(loads hooks + agents)${RESET}`);
|
|
1225
|
+
console.log(` ${TEAL}2.${RESET} ${WHITE}Open Codex in any project${RESET}`);
|
|
1226
|
+
console.log(` ${TEAL}3.${RESET} ${WHITE}Use Qualia commands${RESET} ${DIM}from AGENTS.md with ~/.codex/bin + hooks wired${RESET}`);
|
|
1110
1227
|
}
|
|
1111
1228
|
console.log("");
|
|
1112
1229
|
console.log(` ${DIM}New project?${RESET} ${TEAL}/qualia-new${RESET}`);
|
|
@@ -1120,12 +1237,13 @@ function printSummary({ member, target, claudeInstalled }) {
|
|
|
1120
1237
|
console.log("");
|
|
1121
1238
|
}
|
|
1122
1239
|
|
|
1123
|
-
// ─── Codex install
|
|
1124
|
-
//
|
|
1125
|
-
//
|
|
1126
|
-
//
|
|
1127
|
-
//
|
|
1128
|
-
//
|
|
1240
|
+
// ─── Codex install ───────────────────────────────────────
|
|
1241
|
+
// Codex now has native support for AGENTS.md, ~/.codex/agents/*.toml, and
|
|
1242
|
+
// ~/.codex/hooks.json. Install the framework into those Codex-native surfaces,
|
|
1243
|
+
// plus the same local bin/rules/templates/knowledge substrate used by the
|
|
1244
|
+
// Claude install. The only thing intentionally not claimed is a Claude-style
|
|
1245
|
+
// persistent statusLine setting; Codex exposes hook status messages today, not
|
|
1246
|
+
// an equivalent global status-line command.
|
|
1129
1247
|
async function installCodex(member, target) {
|
|
1130
1248
|
console.log("");
|
|
1131
1249
|
console.log(` ${TEAL}▸${RESET} ${WHITE}${BOLD}Codex${RESET}`);
|
|
@@ -1145,7 +1263,7 @@ async function installCodex(member, target) {
|
|
|
1145
1263
|
|
|
1146
1264
|
if (!codexDetected) {
|
|
1147
1265
|
console.log(` ${YELLOW}!${RESET} ${WHITE}Codex CLI not detected on this system${RESET}`);
|
|
1148
|
-
console.log(` ${DIM} Installing
|
|
1266
|
+
console.log(` ${DIM} Installing Codex files to ~/.codex/ anyway — Codex will pick them up${RESET}`);
|
|
1149
1267
|
console.log(` ${DIM} when you install via:${RESET} ${TEAL}npm install -g @openai/codex${RESET}`);
|
|
1150
1268
|
}
|
|
1151
1269
|
|
|
@@ -1175,24 +1293,9 @@ async function installCodex(member, target) {
|
|
|
1175
1293
|
|
|
1176
1294
|
const dest = path.join(CODEX_DIR, "AGENTS.md");
|
|
1177
1295
|
|
|
1178
|
-
// Backup if existing differs (matches v5.0 CLAUDE.md / settings.json
|
|
1179
|
-
// discipline — never silently destroy a hand-edited file).
|
|
1180
|
-
if (fs.existsSync(dest)) {
|
|
1181
|
-
try {
|
|
1182
|
-
const existing = fs.readFileSync(dest, "utf8");
|
|
1183
|
-
if (existing !== agentsContent) {
|
|
1184
|
-
const ts = new Date().toISOString().replace(/[:.]/g, "-");
|
|
1185
|
-
const bak = `${dest}.bak.${ts}`;
|
|
1186
|
-
try { fs.copyFileSync(dest, bak); ok(`Backed up existing AGENTS.md → ${path.basename(bak)}`); } catch {}
|
|
1187
|
-
}
|
|
1188
|
-
} catch {}
|
|
1189
|
-
}
|
|
1190
|
-
|
|
1191
|
-
// Atomic write: tmp + rename. Same pattern as settings.json above.
|
|
1192
1296
|
try {
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
fs.renameSync(tmp, dest);
|
|
1297
|
+
backupIfDifferent(dest, agentsContent, "AGENTS.md");
|
|
1298
|
+
atomicWrite(dest, agentsContent);
|
|
1196
1299
|
sectionCount++;
|
|
1197
1300
|
ok(`AGENTS.md (configured as ${member.role})`);
|
|
1198
1301
|
} catch (e) {
|
|
@@ -1200,10 +1303,235 @@ async function installCodex(member, target) {
|
|
|
1200
1303
|
return;
|
|
1201
1304
|
}
|
|
1202
1305
|
|
|
1203
|
-
//
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1306
|
+
// Codex treats config.toml as optional, but doctor reports a warning when it
|
|
1307
|
+
// is absent. Create a minimal, parseable file on fresh Codex-only homes and
|
|
1308
|
+
// leave existing user config untouched.
|
|
1309
|
+
try {
|
|
1310
|
+
const configToml = path.join(CODEX_DIR, "config.toml");
|
|
1311
|
+
if (!fs.existsSync(configToml)) {
|
|
1312
|
+
atomicWrite(configToml, [
|
|
1313
|
+
"# Created by qualia-framework install.",
|
|
1314
|
+
"# User settings can be added normally; Qualia runtime lives in AGENTS.md, hooks.json, agents/, and bin/.",
|
|
1315
|
+
"",
|
|
1316
|
+
"[features]",
|
|
1317
|
+
"hooks = true",
|
|
1318
|
+
"plugin_hooks = true",
|
|
1319
|
+
"",
|
|
1320
|
+
].join("\n"));
|
|
1321
|
+
ok("config.toml (minimal Codex config)");
|
|
1322
|
+
} else {
|
|
1323
|
+
log(`${DIM}config.toml (kept — user has customized)${RESET}`);
|
|
1324
|
+
}
|
|
1325
|
+
} catch (e) {
|
|
1326
|
+
warn(`Codex config.toml — ${e.message}`);
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
// Save Codex-local role config. Hooks/scripts resolve their install home at
|
|
1330
|
+
// runtime, so Codex-only installs must not depend on ~/.claude existing.
|
|
1331
|
+
try {
|
|
1332
|
+
const codexConfig = {
|
|
1333
|
+
code: Object.entries(TEAM).find(([, m]) => m.name === member.name)?.[0] || "",
|
|
1334
|
+
installed_by: member.name,
|
|
1335
|
+
role: member.role,
|
|
1336
|
+
version: require("../package.json").version,
|
|
1337
|
+
installed_at: new Date().toISOString().split("T")[0],
|
|
1338
|
+
erp: {
|
|
1339
|
+
enabled: true,
|
|
1340
|
+
url: "https://portal.qualiasolutions.net",
|
|
1341
|
+
api_key_file: ".erp-api-key",
|
|
1342
|
+
},
|
|
1343
|
+
};
|
|
1344
|
+
atomicWrite(path.join(CODEX_DIR, ".qualia-config.json"), JSON.stringify(codexConfig, null, 2) + "\n", 0o600);
|
|
1345
|
+
ok(".qualia-config.json");
|
|
1346
|
+
} catch (e) {
|
|
1347
|
+
warn(`Codex config — ${e.message}`);
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
// Scripts
|
|
1351
|
+
try {
|
|
1352
|
+
const binDest = path.join(CODEX_DIR, "bin");
|
|
1353
|
+
if (!fs.existsSync(binDest)) fs.mkdirSync(binDest, { recursive: true });
|
|
1354
|
+
const scripts = [
|
|
1355
|
+
"state.js",
|
|
1356
|
+
"qualia-ui.js",
|
|
1357
|
+
"statusline.js",
|
|
1358
|
+
"knowledge.js",
|
|
1359
|
+
"knowledge-flush.js",
|
|
1360
|
+
"plan-contract.js",
|
|
1361
|
+
"agent-runs.js",
|
|
1362
|
+
"slop-detect.mjs",
|
|
1363
|
+
"erp-retry.js",
|
|
1364
|
+
"report-payload.js",
|
|
1365
|
+
"project-snapshot.js",
|
|
1366
|
+
];
|
|
1367
|
+
for (const script of scripts) {
|
|
1368
|
+
const src = path.join(FRAMEWORK_DIR, "bin", script);
|
|
1369
|
+
const out = path.join(binDest, script);
|
|
1370
|
+
copy(src, out);
|
|
1371
|
+
try { fs.chmodSync(out, 0o755); } catch {}
|
|
1372
|
+
}
|
|
1373
|
+
ok(`bin/ (${scripts.length} scripts)`);
|
|
1374
|
+
} catch (e) {
|
|
1375
|
+
warn(`Codex scripts — ${e.message}`);
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
// Agents: convert Claude markdown agents into Codex TOML agents.
|
|
1379
|
+
try {
|
|
1380
|
+
const agentsDir = path.join(FRAMEWORK_DIR, "agents");
|
|
1381
|
+
const agentsDest = path.join(CODEX_DIR, "agents");
|
|
1382
|
+
if (!fs.existsSync(agentsDest)) fs.mkdirSync(agentsDest, { recursive: true });
|
|
1383
|
+
for (const file of fs.readdirSync(agentsDir)) {
|
|
1384
|
+
if (!file.endsWith(".md")) continue;
|
|
1385
|
+
const source = fs.readFileSync(path.join(agentsDir, file), "utf8");
|
|
1386
|
+
const parsed = parseAgentMarkdown(source);
|
|
1387
|
+
const base = parsed.name ? parsed.name.replace(/^qualia-/, "") : path.basename(file, ".md");
|
|
1388
|
+
const out = path.join(agentsDest, `${base}.toml`);
|
|
1389
|
+
const toml = renderCodexAgentToml(source);
|
|
1390
|
+
backupIfDifferent(out, toml, `agents/${base}.toml`);
|
|
1391
|
+
atomicWrite(out, toml);
|
|
1392
|
+
}
|
|
1393
|
+
ok("agents/*.toml");
|
|
1394
|
+
} catch (e) {
|
|
1395
|
+
warn(`Codex agents — ${e.message}`);
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
// Rules + lazy-loaded design substrate.
|
|
1399
|
+
try {
|
|
1400
|
+
const rulesDir = path.join(FRAMEWORK_DIR, "rules");
|
|
1401
|
+
const rulesDest = path.join(CODEX_DIR, "rules");
|
|
1402
|
+
if (!fs.existsSync(rulesDest)) fs.mkdirSync(rulesDest, { recursive: true });
|
|
1403
|
+
for (const file of fs.readdirSync(rulesDir)) {
|
|
1404
|
+
copy(path.join(rulesDir, file), path.join(rulesDest, file));
|
|
1405
|
+
}
|
|
1406
|
+
copyTree(path.join(FRAMEWORK_DIR, "qualia-design"), path.join(CODEX_DIR, "qualia-design"));
|
|
1407
|
+
ok("rules/ + qualia-design/");
|
|
1408
|
+
} catch (e) {
|
|
1409
|
+
warn(`Codex rules/design — ${e.message}`);
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
// Skills are copied for reference and path parity, with ~/.claude command
|
|
1413
|
+
// paths rewritten to ~/.codex so Codex-only installs do not depend on a
|
|
1414
|
+
// Claude install existing.
|
|
1415
|
+
try {
|
|
1416
|
+
const skillsSrc = path.join(FRAMEWORK_DIR, "skills");
|
|
1417
|
+
const skillsDest = path.join(CODEX_DIR, "skills");
|
|
1418
|
+
for (const skill of fs.readdirSync(skillsSrc)) {
|
|
1419
|
+
const src = path.join(skillsSrc, skill);
|
|
1420
|
+
if (!fs.statSync(src).isDirectory()) continue;
|
|
1421
|
+
copyTreeTransform(src, path.join(skillsDest, skill), codexText);
|
|
1422
|
+
}
|
|
1423
|
+
ok("skills/");
|
|
1424
|
+
} catch (e) {
|
|
1425
|
+
warn(`Codex skills — ${e.message}`);
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
// Templates + knowledge layer.
|
|
1429
|
+
try {
|
|
1430
|
+
const tmplDir = path.join(FRAMEWORK_DIR, "templates");
|
|
1431
|
+
const tmplDest = path.join(CODEX_DIR, "qualia-templates");
|
|
1432
|
+
if (!fs.existsSync(tmplDest)) fs.mkdirSync(tmplDest, { recursive: true });
|
|
1433
|
+
for (const entry of fs.readdirSync(tmplDir, { withFileTypes: true })) {
|
|
1434
|
+
if (entry.name.startsWith(".") || entry.name === "knowledge") continue;
|
|
1435
|
+
const srcPath = path.join(tmplDir, entry.name);
|
|
1436
|
+
const destPath = path.join(tmplDest, entry.name);
|
|
1437
|
+
if (entry.isDirectory()) copyTreeTransform(srcPath, destPath, codexText);
|
|
1438
|
+
else copyTextTransform(srcPath, destPath, codexText);
|
|
1439
|
+
}
|
|
1440
|
+
const referencesSrc = path.join(FRAMEWORK_DIR, "references");
|
|
1441
|
+
if (fs.existsSync(referencesSrc)) {
|
|
1442
|
+
copyTreeTransform(referencesSrc, path.join(CODEX_DIR, "qualia-references"), codexText);
|
|
1443
|
+
}
|
|
1444
|
+
const knowledgeSrc = path.join(FRAMEWORK_DIR, "templates", "knowledge");
|
|
1445
|
+
const knowledgeDest = path.join(CODEX_DIR, "knowledge");
|
|
1446
|
+
if (!fs.existsSync(path.join(knowledgeDest, "daily-log"))) {
|
|
1447
|
+
fs.mkdirSync(path.join(knowledgeDest, "daily-log"), { recursive: true });
|
|
1448
|
+
}
|
|
1449
|
+
if (fs.existsSync(knowledgeSrc)) {
|
|
1450
|
+
for (const file of fs.readdirSync(knowledgeSrc)) {
|
|
1451
|
+
const out = path.join(knowledgeDest, file);
|
|
1452
|
+
if (!fs.existsSync(out)) copyTextTransform(path.join(knowledgeSrc, file), out, codexText);
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
copyTextTransform(path.join(FRAMEWORK_DIR, "guide.md"), path.join(CODEX_DIR, "qualia-guide.md"), codexText);
|
|
1456
|
+
ok("templates/ + knowledge/ + references/ + guide");
|
|
1457
|
+
} catch (e) {
|
|
1458
|
+
warn(`Codex templates/knowledge — ${e.message}`);
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
// Hooks: Codex reads ~/.codex/hooks.json. Use Codex-local hook script paths
|
|
1462
|
+
// and statusMessage strings so the TUI surfaces Qualia activity inline.
|
|
1463
|
+
try {
|
|
1464
|
+
const hooksSource = path.join(FRAMEWORK_DIR, "hooks");
|
|
1465
|
+
const hooksDest = path.join(CODEX_DIR, "hooks");
|
|
1466
|
+
if (!fs.existsSync(hooksDest)) fs.mkdirSync(hooksDest, { recursive: true });
|
|
1467
|
+
const hookFiles = fs.readdirSync(hooksSource).filter((f) => f.endsWith(".js"));
|
|
1468
|
+
for (const file of hookFiles) {
|
|
1469
|
+
const out = path.join(hooksDest, file);
|
|
1470
|
+
copy(path.join(hooksSource, file), out);
|
|
1471
|
+
try { fs.chmodSync(out, 0o755); } catch {}
|
|
1472
|
+
}
|
|
1473
|
+
const nodeCmd = (hookFile) => `node "${path.join(hooksDest, hookFile)}"`;
|
|
1474
|
+
const qualiaHooks = {
|
|
1475
|
+
hooks: {
|
|
1476
|
+
SessionStart: [
|
|
1477
|
+
{ matcher: ".*", hooks: [{ type: "command", command: nodeCmd("session-start.js"), timeout: 5 }] },
|
|
1478
|
+
],
|
|
1479
|
+
PreToolUse: [
|
|
1480
|
+
{
|
|
1481
|
+
matcher: "Bash",
|
|
1482
|
+
hooks: [
|
|
1483
|
+
{ type: "command", command: nodeCmd("auto-update.js"), timeout: 5, statusMessage: "Qualia update check..." },
|
|
1484
|
+
{ type: "command", command: nodeCmd("git-guardrails.js"), timeout: 5, statusMessage: "Qualia git safety..." },
|
|
1485
|
+
{ type: "command", if: "Bash(git push*)", command: nodeCmd("branch-guard.js"), timeout: 5, statusMessage: "Qualia branch guard..." },
|
|
1486
|
+
{ type: "command", if: "Bash(git push*)", command: nodeCmd("pre-push.js"), timeout: 15, statusMessage: "Qualia tracking stamp..." },
|
|
1487
|
+
{ type: "command", if: "Bash(vercel --prod*)", command: nodeCmd("pre-deploy-gate.js"), timeout: 180, statusMessage: "Qualia deploy gate..." },
|
|
1488
|
+
{ type: "command", if: "Bash(vercel --prod*)|Bash(vercel deploy*)", command: nodeCmd("vercel-account-guard.js"), timeout: 8, statusMessage: "Qualia Vercel account..." },
|
|
1489
|
+
{ type: "command", if: "Bash(vercel env*)", command: nodeCmd("env-empty-guard.js"), timeout: 5, statusMessage: "Qualia env guard..." },
|
|
1490
|
+
{ type: "command", if: "Bash(supabase*)|Bash(npx supabase*)", command: nodeCmd("supabase-destructive-guard.js"), timeout: 5, statusMessage: "Qualia Supabase guard..." },
|
|
1491
|
+
],
|
|
1492
|
+
},
|
|
1493
|
+
{
|
|
1494
|
+
matcher: "Edit|Write",
|
|
1495
|
+
hooks: [
|
|
1496
|
+
{ type: "command", if: "Edit(*migration*)|Write(*migration*)|Edit(*.sql)|Write(*.sql)", command: nodeCmd("migration-guard.js"), timeout: 10, statusMessage: "Qualia migration guard..." },
|
|
1497
|
+
],
|
|
1498
|
+
},
|
|
1499
|
+
],
|
|
1500
|
+
Stop: [
|
|
1501
|
+
{ matcher: ".*", hooks: [{ type: "command", command: nodeCmd("stop-session-log.js"), timeout: 5 }] },
|
|
1502
|
+
],
|
|
1503
|
+
},
|
|
1504
|
+
};
|
|
1505
|
+
const hooksPath = path.join(CODEX_DIR, "hooks.json");
|
|
1506
|
+
let hooksJson = { hooks: {} };
|
|
1507
|
+
if (fs.existsSync(hooksPath)) {
|
|
1508
|
+
try {
|
|
1509
|
+
const parsed = JSON.parse(fs.readFileSync(hooksPath, "utf8"));
|
|
1510
|
+
if (parsed && typeof parsed === "object") hooksJson = parsed;
|
|
1511
|
+
} catch {}
|
|
1512
|
+
}
|
|
1513
|
+
if (!hooksJson.hooks || typeof hooksJson.hooks !== "object") hooksJson.hooks = {};
|
|
1514
|
+
const isQualiaHookCmd = (cmd) => {
|
|
1515
|
+
if (typeof cmd !== "string") return false;
|
|
1516
|
+
return hookFiles.some((file) => cmd.includes(file));
|
|
1517
|
+
};
|
|
1518
|
+
for (const event of Object.keys(qualiaHooks.hooks)) {
|
|
1519
|
+
const existing = Array.isArray(hooksJson.hooks[event]) ? hooksJson.hooks[event] : [];
|
|
1520
|
+
const cleaned = [];
|
|
1521
|
+
for (const block of existing) {
|
|
1522
|
+
if (!block || !Array.isArray(block.hooks)) continue;
|
|
1523
|
+
const kept = block.hooks.filter((h) => !isQualiaHookCmd(h && h.command));
|
|
1524
|
+
if (kept.length > 0) cleaned.push({ ...block, hooks: kept });
|
|
1525
|
+
}
|
|
1526
|
+
hooksJson.hooks[event] = [...cleaned, ...qualiaHooks.hooks[event]];
|
|
1527
|
+
}
|
|
1528
|
+
const content = JSON.stringify(hooksJson, null, 2) + "\n";
|
|
1529
|
+
backupIfDifferent(hooksPath, content, "hooks.json");
|
|
1530
|
+
atomicWrite(hooksPath, content);
|
|
1531
|
+
ok(`hooks.json + hooks/ (${hookFiles.length} hooks)`);
|
|
1532
|
+
} catch (e) {
|
|
1533
|
+
warn(`Codex hooks — ${e.message}`);
|
|
1534
|
+
}
|
|
1207
1535
|
|
|
1208
1536
|
// Codex-only path: still need to write the role config and print summary.
|
|
1209
1537
|
if (target === TARGET_CODEX_ONLY) {
|
package/bin/knowledge-flush.js
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
// CI/scheduled job) without an interactive Claude Code session. Closes the
|
|
6
6
|
// memory loop end-to-end:
|
|
7
7
|
//
|
|
8
|
-
// Stop hook (auto, every turn) →
|
|
9
|
-
// THIS SCRIPT (weekly cron) → spawns
|
|
8
|
+
// Stop hook (auto, every turn) → <install-home>/knowledge/daily-log/{date}.md
|
|
9
|
+
// THIS SCRIPT (weekly cron) → spawns the installed agent CLI
|
|
10
10
|
// /qualia-flush → promotes raw → curated tier
|
|
11
11
|
// bin/knowledge.js (every spawn) → reads index.md → reaches the right file
|
|
12
12
|
//
|
|
@@ -20,11 +20,11 @@
|
|
|
20
20
|
// 0 3 * * 0 node ~/.claude/bin/knowledge-flush.js >> ~/.claude/.qualia-flush.log 2>&1
|
|
21
21
|
//
|
|
22
22
|
// Behavior:
|
|
23
|
-
// • If
|
|
23
|
+
// • If the required agent CLI isn't on PATH, exits 0 with a logged warning. Cron
|
|
24
24
|
// spam is worse than a missed flush — a real failure surfaces in the
|
|
25
25
|
// log file the user is presumably watching.
|
|
26
26
|
// • If the daily-log dir is empty (nothing to flush), exits 0 silently.
|
|
27
|
-
// • If
|
|
27
|
+
// • If the agent CLI returns non-zero, exits 1 with the error captured in
|
|
28
28
|
// the log so cron can be configured to alert on it.
|
|
29
29
|
// • Writes one structured JSONL line per run to ~/.claude/.qualia-flush.log
|
|
30
30
|
// so the user can audit "when did the last 5 flushes run, what did they
|
|
@@ -38,9 +38,17 @@ const os = require("os");
|
|
|
38
38
|
const { spawnSync } = require("child_process");
|
|
39
39
|
|
|
40
40
|
const HOME = os.homedir();
|
|
41
|
-
|
|
41
|
+
function qualiaHome() {
|
|
42
|
+
if (process.env.QUALIA_HOME) return process.env.QUALIA_HOME;
|
|
43
|
+
const parent = path.basename(path.dirname(__dirname));
|
|
44
|
+
if (parent === ".codex" || parent === ".claude") return path.dirname(__dirname);
|
|
45
|
+
return path.join(HOME, ".claude");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const QUALIA_HOME = qualiaHome();
|
|
49
|
+
const KNOWLEDGE_DIR = path.join(QUALIA_HOME, "knowledge");
|
|
42
50
|
const DAILY_DIR = path.join(KNOWLEDGE_DIR, "daily-log");
|
|
43
|
-
const LOG_FILE = path.join(
|
|
51
|
+
const LOG_FILE = path.join(QUALIA_HOME, ".qualia-flush.log");
|
|
44
52
|
|
|
45
53
|
const _start = Date.now();
|
|
46
54
|
|
|
@@ -103,9 +111,11 @@ function dailyLogHasRecentEntries(windowDays) {
|
|
|
103
111
|
}
|
|
104
112
|
|
|
105
113
|
// ── Preflight ────────────────────────────────────────────
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
114
|
+
const IS_CODEX_INSTALL = path.basename(QUALIA_HOME) === ".codex";
|
|
115
|
+
const agentCli = IS_CODEX_INSTALL ? "codex" : "claude";
|
|
116
|
+
const agentBin = which(agentCli);
|
|
117
|
+
if (!agentBin) {
|
|
118
|
+
logEvent({ event: "skipped", reason: `${agentCli}-cli-not-on-path` });
|
|
109
119
|
// Exit 0 — a missing CLI on the host running cron is a config issue, not
|
|
110
120
|
// a flush failure. Don't spam alerts.
|
|
111
121
|
process.exit(0);
|
|
@@ -117,11 +127,13 @@ if (!dailyLogHasRecentEntries(days)) {
|
|
|
117
127
|
}
|
|
118
128
|
|
|
119
129
|
// ── Run ──────────────────────────────────────────────────
|
|
120
|
-
// `claude -p "<prompt>"`
|
|
121
|
-
// invocation matches what the user would
|
|
130
|
+
// `claude -p "<prompt>"` and `codex exec "<prompt>"` run a single
|
|
131
|
+
// non-interactive turn. The skill body invocation matches what the user would
|
|
132
|
+
// type at the prompt.
|
|
122
133
|
const prompt = `/qualia-flush ${argv.join(" ")}`.trim();
|
|
123
134
|
|
|
124
|
-
const
|
|
135
|
+
const cliArgs = IS_CODEX_INSTALL ? ["exec", prompt] : ["-p", prompt];
|
|
136
|
+
const result = spawnSync(agentBin, cliArgs, {
|
|
125
137
|
encoding: "utf8",
|
|
126
138
|
timeout: 5 * 60 * 1000, // 5 min hard cap — flush should never take this long
|
|
127
139
|
shell: process.platform === "win32",
|
|
@@ -140,7 +152,7 @@ if (status !== 0) {
|
|
|
140
152
|
stderr_tail: stderr.slice(-1000),
|
|
141
153
|
});
|
|
142
154
|
// Surface to stderr so cron's MAILTO sends an alert.
|
|
143
|
-
console.error(`knowledge-flush:
|
|
155
|
+
console.error(`knowledge-flush: ${agentCli} exited ${status}`);
|
|
144
156
|
if (stderr) console.error(stderr.slice(-2000));
|
|
145
157
|
process.exit(1);
|
|
146
158
|
}
|