qualia-framework 6.14.0 → 7.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/AGENTS.md +8 -5
  2. package/CHANGELOG.md +316 -0
  3. package/CLAUDE.md +3 -1
  4. package/agents/roadmapper.md +16 -14
  5. package/bin/agent-status.js +24 -11
  6. package/bin/batch-plan.js +111 -0
  7. package/bin/branch-hygiene.js +135 -0
  8. package/bin/command-surface.js +2 -0
  9. package/bin/compile-instructions.js +82 -0
  10. package/bin/design-tokens.js +131 -0
  11. package/bin/erp-event.js +177 -0
  12. package/bin/erp-retry.js +12 -1
  13. package/bin/eval-runner.js +218 -0
  14. package/bin/host-adapters.js +84 -12
  15. package/bin/install.js +44 -13
  16. package/bin/knowledge-flush.js +6 -3
  17. package/bin/last-report.js +207 -0
  18. package/bin/project-sync.js +315 -0
  19. package/bin/recall.js +172 -0
  20. package/bin/repo-map.js +188 -0
  21. package/bin/runtime-manifest.js +12 -0
  22. package/bin/state.js +112 -1
  23. package/bin/vault-access.js +82 -0
  24. package/bin/verify-panel.js +294 -0
  25. package/bin/wave-plan.js +211 -0
  26. package/docs/erp-contract.md +180 -0
  27. package/mcp/memory-mcp/server.js +257 -0
  28. package/package.json +6 -3
  29. package/qualia-design/design-dials.md +72 -0
  30. package/qualia-design/design-reference.md +24 -0
  31. package/rules/access.md +42 -0
  32. package/rules/codex-goal.md +28 -26
  33. package/rules/infrastructure.md +1 -1
  34. package/skills/qualia/SKILL.md +6 -0
  35. package/skills/qualia-build/SKILL.md +43 -9
  36. package/skills/qualia-eval/SKILL.md +83 -0
  37. package/skills/qualia-feature/SKILL.md +20 -4
  38. package/skills/qualia-fix/SKILL.md +13 -1
  39. package/skills/qualia-map/SKILL.md +15 -0
  40. package/skills/qualia-milestone/SKILL.md +12 -6
  41. package/skills/qualia-new/REFERENCE.md +6 -4
  42. package/skills/qualia-new/SKILL.md +41 -15
  43. package/skills/qualia-plan/SKILL.md +2 -2
  44. package/skills/qualia-polish/SKILL.md +3 -2
  45. package/skills/qualia-recall/SKILL.md +76 -0
  46. package/skills/qualia-report/SKILL.md +10 -0
  47. package/skills/qualia-scope/SKILL.md +3 -3
  48. package/skills/qualia-ship/SKILL.md +34 -4
  49. package/skills/qualia-update/SKILL.md +4 -0
  50. package/skills/qualia-verify/SKILL.md +53 -24
  51. package/templates/DESIGN.md +15 -0
  52. package/templates/instructions.md +32 -0
  53. package/templates/journey.md +1 -1
  54. package/templates/project-discovery.md +30 -23
  55. package/templates/requirements.md +7 -7
  56. package/tests/agent-status.test.sh +15 -0
  57. package/tests/batch-plan.test.sh +56 -0
  58. package/tests/branch-hygiene.test.sh +93 -0
  59. package/tests/design-tokens.test.sh +53 -0
  60. package/tests/erp-event.test.sh +78 -0
  61. package/tests/eval-runner.test.sh +147 -0
  62. package/tests/instructions.test.sh +109 -0
  63. package/tests/last-report.test.sh +156 -0
  64. package/tests/lib.test.sh +29 -4
  65. package/tests/project-sync.test.sh +175 -0
  66. package/tests/recall.test.sh +91 -0
  67. package/tests/repo-map.test.sh +70 -0
  68. package/tests/run-all.sh +12 -0
  69. package/tests/runner.js +363 -33
  70. package/tests/state.test.sh +92 -0
  71. package/tests/verify-panel.test.sh +162 -0
  72. package/tests/wave-plan.test.sh +153 -0
@@ -0,0 +1,218 @@
1
+ #!/usr/bin/env node
2
+ // eval-runner.js — layered assertion engine for AI features (chat / RAG / voice).
3
+ //
4
+ // WHY: Qualia gates UI and code (contract-runner, slop-detect, verify-panel) but
5
+ // has no equivalent for the AI artifacts it BUILDS. "The chatbot answers refund
6
+ // questions" is not checkable by a grep. This runs a suite of cases against
7
+ // captured AI outputs with LAYERED assertions: cheap deterministic checks first
8
+ // (contains / regex / json_path / length / latency / cost), then llm_rubric for
9
+ // the judgments only a model can make — same shape as contract-runner evidence.
10
+ //
11
+ // Deterministic-first by design: every assertion this module can settle WITHOUT
12
+ // a model, it settles here. `llm_rubric` assertions carry a `verdict`
13
+ // (pass|fail) that the /qualia-eval skill fills by spawning a judge BEFORE
14
+ // calling this runner — exactly how verify-panel consumes skeptic votes. An
15
+ // llm_rubric with no verdict is PENDING and fails the suite (never silently
16
+ // passes an unjudged rubric).
17
+ //
18
+ // Suite (JSON — zero-dependency; no YAML parser pulled in):
19
+ // {
20
+ // "feature": "support-chat",
21
+ // "cases": [
22
+ // { "name": "refund window",
23
+ // "input": "what's your refund policy?",
24
+ // "output": "We refund within 30 days...", // or "output_file": "path"
25
+ // "latency_ms": 1200, "cost_usd": 0.008,
26
+ // "assert": [
27
+ // { "type": "contains", "value": "30 days" },
28
+ // { "type": "not_contains", "value": "I cannot help" },
29
+ // { "type": "max_latency_ms", "value": 2000 },
30
+ // { "type": "llm_rubric", "rubric": "grounded in policy, no hallucination", "verdict": "pass" }
31
+ // ] } ] }
32
+ //
33
+ // Exit 0 = all cases pass, 1 = a failure or an unjudged rubric, 2 = bad input.
34
+ // Zero npm dependencies. Library + CLI.
35
+
36
+ const fs = require("fs");
37
+ const path = require("path");
38
+
39
+ function getPath(obj, dotted) {
40
+ const parts = String(dotted).split(".").filter(Boolean);
41
+ let cur = obj;
42
+ for (const p of parts) {
43
+ if (cur == null) return undefined;
44
+ const idx = /^\d+$/.test(p) ? Number(p) : p;
45
+ cur = cur[idx];
46
+ }
47
+ return cur;
48
+ }
49
+
50
+ function safeRegex(src) {
51
+ try { return new RegExp(src); } catch { return null; }
52
+ }
53
+
54
+ // Run one assertion against a case's captured output + metrics.
55
+ // Returns { ok, detail }.
56
+ function runAssertion(a, ctx) {
57
+ const out = ctx.output != null ? String(ctx.output) : "";
58
+ switch (a.type) {
59
+ case "contains":
60
+ return { ok: out.includes(String(a.value)), detail: `contains "${a.value}"` };
61
+ case "not_contains":
62
+ return { ok: !out.includes(String(a.value)), detail: `not_contains "${a.value}"` };
63
+ case "equals":
64
+ return { ok: out === String(a.value), detail: `equals "${a.value}"` };
65
+ case "regex": {
66
+ const re = safeRegex(a.value);
67
+ if (!re) return { ok: false, detail: `invalid regex: ${a.value}` };
68
+ return { ok: re.test(out), detail: `regex /${a.value}/` };
69
+ }
70
+ case "not_regex": {
71
+ const re = safeRegex(a.value);
72
+ if (!re) return { ok: false, detail: `invalid regex: ${a.value}` };
73
+ return { ok: !re.test(out), detail: `not_regex /${a.value}/` };
74
+ }
75
+ case "min_length":
76
+ return { ok: out.length >= Number(a.value), detail: `min_length ${a.value} (got ${out.length})` };
77
+ case "max_length":
78
+ return { ok: out.length <= Number(a.value), detail: `max_length ${a.value} (got ${out.length})` };
79
+ case "json_valid":
80
+ try { JSON.parse(out); return { ok: true, detail: "json_valid" }; }
81
+ catch { return { ok: false, detail: "output is not valid JSON" }; }
82
+ case "json_path": {
83
+ let parsed;
84
+ try { parsed = JSON.parse(out); } catch { return { ok: false, detail: "json_path: output not JSON" }; }
85
+ const val = getPath(parsed, a.path);
86
+ if (val === undefined) return { ok: false, detail: `json_path ${a.path}: missing` };
87
+ if (a.equals !== undefined) return { ok: String(val) === String(a.equals), detail: `json_path ${a.path} equals "${a.equals}" (got "${val}")` };
88
+ if (a.contains !== undefined) return { ok: String(val).includes(String(a.contains)), detail: `json_path ${a.path} contains "${a.contains}"` };
89
+ return { ok: true, detail: `json_path ${a.path} present` };
90
+ }
91
+ case "max_latency_ms":
92
+ if (ctx.latency_ms == null) return { ok: false, detail: "max_latency_ms: no latency_ms recorded on case" };
93
+ return { ok: Number(ctx.latency_ms) <= Number(a.value), detail: `max_latency_ms ${a.value} (got ${ctx.latency_ms})` };
94
+ case "max_cost_usd":
95
+ if (ctx.cost_usd == null) return { ok: false, detail: "max_cost_usd: no cost_usd recorded on case" };
96
+ return { ok: Number(ctx.cost_usd) <= Number(a.value), detail: `max_cost_usd ${a.value} (got ${ctx.cost_usd})` };
97
+ case "llm_rubric": {
98
+ const v = a.verdict != null ? String(a.verdict).toLowerCase() : null;
99
+ if (v === null) return { ok: false, pending: true, detail: `llm_rubric PENDING (no judge verdict): "${a.rubric}"` };
100
+ return { ok: v === "pass", detail: `llm_rubric "${a.rubric}" → ${v}` };
101
+ }
102
+ default:
103
+ return { ok: false, detail: `unknown assertion type: ${a.type}` };
104
+ }
105
+ }
106
+
107
+ function resolveOutput(root, c) {
108
+ if (c.output != null) return c.output;
109
+ if (c.output_file) {
110
+ try { return fs.readFileSync(path.resolve(root, c.output_file), "utf8"); }
111
+ catch (e) { return { __error: `output_file unreadable: ${e.message}` }; }
112
+ }
113
+ return "";
114
+ }
115
+
116
+ function run(suite, opts = {}) {
117
+ const root = path.resolve(opts.cwd || process.cwd());
118
+ const cases = [];
119
+ let pending = 0;
120
+ for (const c of suite.cases || []) {
121
+ const output = resolveOutput(root, c);
122
+ const ctx = { output: (output && output.__error) ? "" : output, latency_ms: c.latency_ms, cost_usd: c.cost_usd };
123
+ const assertions = [];
124
+ if (output && output.__error) {
125
+ assertions.push({ type: "output", ok: false, detail: output.__error });
126
+ }
127
+ for (const a of c.assert || []) {
128
+ const r = runAssertion(a, ctx);
129
+ if (r.pending) pending++;
130
+ assertions.push({ type: a.type, ok: !!r.ok, pending: !!r.pending, detail: r.detail });
131
+ }
132
+ const ok = assertions.length > 0 && assertions.every((x) => x.ok);
133
+ cases.push({ name: c.name || "(unnamed)", ok, assertions });
134
+ }
135
+ const passed = cases.filter((c) => c.ok).length;
136
+ return {
137
+ ok: cases.length > 0 && passed === cases.length,
138
+ feature: suite.feature || "(unnamed feature)",
139
+ total_cases: cases.length,
140
+ passed_cases: passed,
141
+ failed_cases: cases.length - passed,
142
+ pending,
143
+ cases,
144
+ };
145
+ }
146
+
147
+ // ── CLI ───────────────────────────────────────────────────────────────────
148
+ function parseArgs(argv) {
149
+ const args = { _: [] };
150
+ for (let i = 2; i < argv.length; i++) {
151
+ const a = argv[i];
152
+ if (a === "--json") args.json = true;
153
+ else if (a === "--write") args.write = true;
154
+ else if (a === "--cwd") args.cwd = argv[++i];
155
+ else if (a.startsWith("--cwd=")) args.cwd = a.slice(6);
156
+ else args._.push(a);
157
+ }
158
+ return args;
159
+ }
160
+
161
+ function usage() {
162
+ console.error([
163
+ "Usage:",
164
+ " eval-runner.js <suite.json> [--json] [--write] [--cwd DIR]",
165
+ "",
166
+ "Layered assertion runner for AI-feature eval suites.",
167
+ "Exit 0 = all cases pass, 1 = failure/unjudged rubric, 2 = invocation error.",
168
+ ].join("\n"));
169
+ }
170
+
171
+ function main(argv) {
172
+ const args = parseArgs(argv);
173
+ const suitePath = args._[0];
174
+ if (!suitePath || suitePath === "-h" || suitePath === "--help") { usage(); return 2; }
175
+ const root = path.resolve(args.cwd || process.cwd());
176
+
177
+ let suite;
178
+ try {
179
+ suite = JSON.parse(fs.readFileSync(path.resolve(root, suitePath), "utf8"));
180
+ } catch (e) {
181
+ console.error(`ERROR: cannot read eval suite: ${e.message}`);
182
+ return 2;
183
+ }
184
+
185
+ const result = run(suite, { cwd: root });
186
+
187
+ if (args.write) {
188
+ const dir = path.join(root, ".planning", "evals");
189
+ try {
190
+ fs.mkdirSync(dir, { recursive: true });
191
+ const slug = String(result.feature).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
192
+ fs.writeFileSync(path.join(dir, `eval-${slug || "feature"}.json`), JSON.stringify(result, null, 2) + "\n");
193
+ } catch (e) {
194
+ console.error(`WARN: could not write eval artifact: ${e.message}`);
195
+ }
196
+ }
197
+
198
+ if (args.json) {
199
+ console.log(JSON.stringify(result, null, 2));
200
+ return result.ok ? 0 : 1;
201
+ }
202
+
203
+ console.log(`EVAL ${result.ok ? "PASS" : "FAIL"} [${result.feature}]: ` +
204
+ `${result.passed_cases}/${result.total_cases} cases` +
205
+ (result.pending ? ` (${result.pending} rubric(s) unjudged)` : ""));
206
+ for (const c of result.cases) {
207
+ if (c.ok) continue;
208
+ console.log(` ✗ ${c.name}`);
209
+ for (const a of c.assertions) if (!a.ok) console.log(` - ${a.detail}`);
210
+ }
211
+ return result.ok ? 0 : 1;
212
+ }
213
+
214
+ module.exports = { run, runAssertion, getPath };
215
+
216
+ if (require.main === module) {
217
+ process.exit(main(process.argv));
218
+ }
@@ -1,26 +1,66 @@
1
1
  #!/usr/bin/env node
2
- // Host adapter rendering for installed Qualia text surfaces.
2
+ // host-adapters.js the single internal contract for everything that differs
3
+ // between the runtimes Qualia targets (Claude Code, OpenAI Codex).
4
+ //
5
+ // The rule (ACP shape): NOTHING else branches on runtime. Skills, install, and
6
+ // hooks ask the adapter for a fact ("what's the instruction filename?", "how do
7
+ // I render this text for this host?") instead of hardcoding an `if (codex)`.
8
+ // When a third runtime arrives, it's one entry in HOSTS — not a grep-and-patch
9
+ // across the tree.
3
10
 
4
11
  const path = require("path");
5
12
  const os = require("os");
6
13
 
14
+ // Per-host facts. Each host declares — declaratively, in ONE place:
15
+ // home install root
16
+ // name display name (used in the naming map + UX copy)
17
+ // instructionFile the top-level agent-instructions file this runtime reads
18
+ // configFile the runtime's settings file
19
+ // agentDir where subagent definitions live (relative to home)
20
+ // agentExt the subagent file extension the runtime consumes
21
+ // naming source→host display-string swaps applied to rendered text
7
22
  const HOSTS = {
8
23
  claude: {
9
24
  name: "Claude Code",
10
25
  home: path.join(os.homedir(), ".claude"),
26
+ instructionFile: "CLAUDE.md",
27
+ configFile: "settings.json",
28
+ agentDir: "agents",
29
+ agentExt: ".md",
30
+ agentCli: "claude", // the CLI that runs a non-interactive turn for this host
31
+ agentExecPrefix: ["-p"], // `claude -p "<prompt>"`
32
+ naming: {}, // Claude is the canonical voice — no swaps.
11
33
  },
12
34
  codex: {
13
35
  name: "OpenAI Codex",
14
36
  home: path.join(os.homedir(), ".codex"),
37
+ instructionFile: "AGENTS.md",
38
+ configFile: "config.toml",
39
+ agentDir: "agents",
40
+ agentExt: ".toml",
41
+ agentCli: "codex",
42
+ agentExecPrefix: ["exec"], // `codex exec "<prompt>"`
43
+ naming: { "Claude Code": "Codex", "Claude's": "Codex's" },
15
44
  },
16
45
  };
17
46
 
47
+ // Which host owns this install home? (".codex" → codex, else claude.) The one
48
+ // place that maps a home dir back to a host name — callers ask, never re-derive.
49
+ function hostForHome(home) {
50
+ return path.basename(home) === ".codex" ? "codex" : "claude";
51
+ }
52
+
18
53
  function adapter(name) {
19
54
  const host = HOSTS[name];
20
55
  if (!host) throw new Error(`Unknown Qualia host adapter: ${name}`);
21
56
  const home = host.home;
22
57
  return {
23
58
  ...host,
59
+ instructionPath: path.join(home, host.instructionFile),
60
+ configPath: path.join(home, host.configFile),
61
+ agentPath: path.join(home, host.agentDir),
62
+ // Full argv (minus the binary) to run one non-interactive turn on this host.
63
+ agentExec: (prompt) => [...host.agentExecPrefix, prompt],
24
64
  tokens: {
25
65
  QUALIA_HOME: home,
26
66
  QUALIA_BIN: `${home}/bin`,
@@ -35,32 +75,64 @@ function adapter(name) {
35
75
  };
36
76
  }
37
77
 
38
- function renderText(content, hostName) {
78
+ // Host-display naming swaps only (e.g. "Claude Code" → "Codex"). Split out from
79
+ // path rendering so the canonical→artifact compile can swap names WITHOUT baking
80
+ // in install-time paths (install resolves those per host afterwards).
81
+ function applyNaming(content, hostName) {
82
+ const host = adapter(hostName);
83
+ let out = String(content);
84
+ for (const [from, to] of Object.entries(host.naming)) {
85
+ out = out.replaceAll(from, to);
86
+ }
87
+ return out;
88
+ }
89
+
90
+ // Path/token rendering: ${QUALIA_*} tokens + legacy `.claude/` literals → this
91
+ // host's install paths. Idempotent (running twice is a no-op), so install can
92
+ // safely re-render an already-compiled artifact.
93
+ function applyPaths(content, hostName) {
39
94
  const host = adapter(hostName);
40
95
  let out = String(content);
41
96
  for (const [token, value] of Object.entries(host.tokens)) {
42
97
  out = out.replaceAll(`\${${token}}`, value);
43
98
  }
44
-
45
- // Backward-compatible rendering while source files migrate from hardcoded
46
- // Claude paths to explicit ${QUALIA_*} tokens.
47
- out = out
99
+ return out
48
100
  .replaceAll("~/.claude/", `${host.home}/`)
49
101
  .replaceAll("$HOME/.claude/", `${host.home}/`)
50
102
  .replaceAll("${HOME}/.claude/", `${host.home}/`)
51
103
  .replaceAll("@~/.claude/", `@${host.home}/`)
52
104
  .replaceAll(".claude/", `${path.basename(host.home)}/`);
105
+ }
53
106
 
54
- if (hostName === "codex") {
55
- out = out
56
- .replaceAll("Claude Code", "Codex")
57
- .replaceAll("Claude's", "Codex's");
58
- }
59
- return out;
107
+ // Full install-time render: paths + naming. Unchanged public behavior.
108
+ function renderText(content, hostName) {
109
+ return applyNaming(applyPaths(content, hostName), hostName);
110
+ }
111
+
112
+ // Host-conditional blocks in a canonical source:
113
+ // <!--QUALIA-HOST claude--> …kept only for claude… <!--/QUALIA-HOST-->
114
+ // Keep the matching host's block (unwrapped), drop every other host's block.
115
+ function stripHostBlocks(content, hostName) {
116
+ const re = /[ \t]*<!--QUALIA-HOST\s+(\w+)-->\n?([\s\S]*?)\n?[ \t]*<!--\/QUALIA-HOST-->\n?/g;
117
+ return String(content).replace(re, (_m, host, body) =>
118
+ host === hostName ? `${body}\n` : ""
119
+ );
120
+ }
121
+
122
+ // Compile a canonical instruction source into one host's artifact: resolve
123
+ // conditional blocks + apply naming, but DO NOT resolve paths or {{ROLE}} —
124
+ // those stay as tokens for install.js to render per concrete install.
125
+ function compileInstructions(canonical, hostName) {
126
+ return applyNaming(stripHostBlocks(canonical, hostName), hostName);
60
127
  }
61
128
 
62
129
  module.exports = {
63
130
  HOSTS,
64
131
  adapter,
132
+ hostForHome,
133
+ applyNaming,
134
+ applyPaths,
65
135
  renderText,
136
+ stripHostBlocks,
137
+ compileInstructions,
66
138
  };
package/bin/install.js CHANGED
@@ -6,7 +6,7 @@ const fs = require("fs");
6
6
  const ui = require("./qualia-ui.js");
7
7
  const { RUNTIME_BIN_SCRIPTS, binFiles } = require("./runtime-manifest.js");
8
8
  const { ACTIVE_SKILLS, RETIRED_SKILLS } = require("./command-surface.js");
9
- const { renderText } = require("./host-adapters.js");
9
+ const { renderText, adapter } = require("./host-adapters.js");
10
10
 
11
11
  // ─── Colors (kept for legacy log lines; new sections route through qualia-ui) ─
12
12
  const TEAL = "\x1b[38;2;0;206;209m";
@@ -956,13 +956,16 @@ async function main() {
956
956
  // ─── CLAUDE.md with role ───────────────────────────────
957
957
  printSection("Configuration");
958
958
  try {
959
+ // Source + dest filenames come from the host adapter (single per-host
960
+ // contract). CLAUDE.md is a generated artifact of templates/instructions.md.
961
+ const claudeFile = adapter("claude").instructionFile;
959
962
  let claudeMd = fs.readFileSync(
960
- path.join(FRAMEWORK_DIR, "CLAUDE.md"),
963
+ path.join(FRAMEWORK_DIR, claudeFile),
961
964
  "utf8"
962
965
  );
963
966
  claudeMd = claudeMd.replace("{{ROLE}}", member.role);
964
967
  claudeMd = claudeMd.replace("{{ROLE_DESCRIPTION}}", member.description);
965
- const claudeDest = path.join(CLAUDE_DIR, "CLAUDE.md");
968
+ const claudeDest = path.join(CLAUDE_DIR, claudeFile);
966
969
  // v5.0: backup existing CLAUDE.md before overwrite. Users may have added
967
970
  // personal instructions; without a backup, re-install silently destroys
968
971
  // them. .bak files are harmless and easy to clean up.
@@ -1439,6 +1442,29 @@ Client-specific preferences, design choices, and requirements. Loaded by \`/qual
1439
1442
  ok("MCP: next-devtools (runtime error visibility for Next.js projects)");
1440
1443
  }
1441
1444
 
1445
+ // ─── qualia-memory MCP ──────────────────────────────────
1446
+ // Read-only access to the Karpathy LLM Wiki at ~/qualia-memory/. Three tools:
1447
+ // memory.search, memory.read, memory.list. Zero deps — see docs/MEMORY-MCP.md.
1448
+ // Registered globally so every Claude Code session has it without per-project
1449
+ // .mcp.json. Users override the vault path via QUALIA_MEMORY_ROOT.
1450
+ const memoryServerPath = path.join(FRAMEWORK_DIR, "mcp", "memory-mcp", "server.js");
1451
+ if (!settings.mcpServers["qualia-memory"]) {
1452
+ settings.mcpServers["qualia-memory"] = {
1453
+ command: "node",
1454
+ args: [memoryServerPath],
1455
+ env: {
1456
+ QUALIA_MEMORY_ROOT: path.join(require("os").homedir(), "qualia-memory"),
1457
+ },
1458
+ disabled: false,
1459
+ };
1460
+ ok("MCP: qualia-memory (read-only wiki at ~/qualia-memory/)");
1461
+ } else {
1462
+ // Always refresh the args path — the framework may have moved (npm i -g
1463
+ // location, dev checkout vs published install). Env stays user-controlled.
1464
+ settings.mcpServers["qualia-memory"].command = "node";
1465
+ settings.mcpServers["qualia-memory"].args = [memoryServerPath];
1466
+ }
1467
+
1442
1468
  // v5.0: backup existing settings.json before overwrite. The merge logic above
1443
1469
  // preserves user fields, but a partial-write or merger bug could destroy MCP
1444
1470
  // configs / custom permissions. Atomic write (tmp + rename) avoids partial
@@ -1590,24 +1616,29 @@ async function installCodex(member, target, employeeMode = false) {
1590
1616
  }
1591
1617
  }
1592
1618
 
1593
- // Render AGENTS.md with role substitution. Source is the same AGENTS.md
1594
- // already shipped in the framework root (it's the cross-vendor mirror of
1595
- // CLAUDE.md, kept under 25 lines per Pocock's instruction-budget rule).
1619
+ // AGENTS.md is the Codex artifact of templates/instructions.md (compiled by
1620
+ // bin/compile-instructions.js). Source filename + render come from the host
1621
+ // adapter, mirroring the CLAUDE.md path exactly. codexText() resolves
1622
+ // ${QUALIA_*} tokens + .claude→.codex paths — CLAUDE.md got this rendering;
1623
+ // AGENTS.md previously did not (latent drift the single-contract path fixes).
1624
+ const codexFile = adapter("codex").instructionFile;
1596
1625
  let agentsContent;
1597
1626
  try {
1598
- agentsContent = fs.readFileSync(path.join(FRAMEWORK_DIR, "AGENTS.md"), "utf8");
1599
- agentsContent = agentsContent
1600
- .replace("{{ROLE}}", member.role)
1601
- .replace("{{ROLE_DESCRIPTION}}", member.description);
1627
+ agentsContent = fs.readFileSync(path.join(FRAMEWORK_DIR, codexFile), "utf8");
1628
+ agentsContent = codexText(
1629
+ agentsContent
1630
+ .replace("{{ROLE}}", member.role)
1631
+ .replace("{{ROLE_DESCRIPTION}}", member.description)
1632
+ );
1602
1633
  } catch (e) {
1603
- warn(`Codex — could not read framework AGENTS.md: ${e.message}`);
1634
+ warn(`Codex — could not read framework ${codexFile}: ${e.message}`);
1604
1635
  return;
1605
1636
  }
1606
1637
 
1607
- const dest = path.join(CODEX_DIR, "AGENTS.md");
1638
+ const dest = path.join(CODEX_DIR, codexFile);
1608
1639
 
1609
1640
  try {
1610
- backupIfDifferent(dest, agentsContent, "AGENTS.md");
1641
+ backupIfDifferent(dest, agentsContent, codexFile);
1611
1642
  atomicWrite(dest, agentsContent);
1612
1643
  sectionCount++;
1613
1644
  ok(`AGENTS.md (configured as ${member.role})`);
@@ -110,8 +110,11 @@ function dailyLogHasRecentEntries(windowDays) {
110
110
  }
111
111
 
112
112
  // ── Preflight ────────────────────────────────────────────
113
- const IS_CODEX_INSTALL = path.basename(QUALIA_HOME) === ".codex";
114
- const agentCli = IS_CODEX_INSTALL ? "codex" : "claude";
113
+ // Per-host facts (which CLI, how to invoke it) come from the adapter — the one
114
+ // place runtime differences live. See bin/host-adapters.js.
115
+ const { adapter, hostForHome } = require("./host-adapters.js");
116
+ const host = adapter(hostForHome(QUALIA_HOME));
117
+ const agentCli = host.agentCli;
115
118
  const agentBin = which(agentCli);
116
119
  if (!agentBin) {
117
120
  logEvent({ event: "skipped", reason: `${agentCli}-cli-not-on-path` });
@@ -143,7 +146,7 @@ const prompt = [
143
146
  "Finish with one line starting exactly: ⬢ Flushed daily-log",
144
147
  ].join("\n");
145
148
 
146
- const cliArgs = IS_CODEX_INSTALL ? ["exec", prompt] : ["-p", prompt];
149
+ const cliArgs = host.agentExec(prompt);
147
150
  const result = spawnSync(agentBin, cliArgs, {
148
151
  encoding: "utf8",
149
152
  timeout: 5 * 60 * 1000, // 5 min hard cap — flush should never take this long