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.
- package/AGENTS.md +8 -5
- package/CHANGELOG.md +316 -0
- package/CLAUDE.md +3 -1
- package/agents/roadmapper.md +16 -14
- package/bin/agent-status.js +24 -11
- package/bin/batch-plan.js +111 -0
- package/bin/branch-hygiene.js +135 -0
- package/bin/command-surface.js +2 -0
- package/bin/compile-instructions.js +82 -0
- package/bin/design-tokens.js +131 -0
- package/bin/erp-event.js +177 -0
- package/bin/erp-retry.js +12 -1
- package/bin/eval-runner.js +218 -0
- package/bin/host-adapters.js +84 -12
- package/bin/install.js +44 -13
- package/bin/knowledge-flush.js +6 -3
- package/bin/last-report.js +207 -0
- package/bin/project-sync.js +315 -0
- package/bin/recall.js +172 -0
- package/bin/repo-map.js +188 -0
- package/bin/runtime-manifest.js +12 -0
- package/bin/state.js +112 -1
- package/bin/vault-access.js +82 -0
- package/bin/verify-panel.js +294 -0
- package/bin/wave-plan.js +211 -0
- package/docs/erp-contract.md +180 -0
- package/mcp/memory-mcp/server.js +257 -0
- package/package.json +6 -3
- package/qualia-design/design-dials.md +72 -0
- package/qualia-design/design-reference.md +24 -0
- package/rules/access.md +42 -0
- package/rules/codex-goal.md +28 -26
- package/rules/infrastructure.md +1 -1
- package/skills/qualia/SKILL.md +6 -0
- package/skills/qualia-build/SKILL.md +43 -9
- package/skills/qualia-eval/SKILL.md +83 -0
- package/skills/qualia-feature/SKILL.md +20 -4
- package/skills/qualia-fix/SKILL.md +13 -1
- package/skills/qualia-map/SKILL.md +15 -0
- package/skills/qualia-milestone/SKILL.md +12 -6
- package/skills/qualia-new/REFERENCE.md +6 -4
- package/skills/qualia-new/SKILL.md +41 -15
- package/skills/qualia-plan/SKILL.md +2 -2
- package/skills/qualia-polish/SKILL.md +3 -2
- package/skills/qualia-recall/SKILL.md +76 -0
- package/skills/qualia-report/SKILL.md +10 -0
- package/skills/qualia-scope/SKILL.md +3 -3
- package/skills/qualia-ship/SKILL.md +34 -4
- package/skills/qualia-update/SKILL.md +4 -0
- package/skills/qualia-verify/SKILL.md +53 -24
- package/templates/DESIGN.md +15 -0
- package/templates/instructions.md +32 -0
- package/templates/journey.md +1 -1
- package/templates/project-discovery.md +30 -23
- package/templates/requirements.md +7 -7
- package/tests/agent-status.test.sh +15 -0
- package/tests/batch-plan.test.sh +56 -0
- package/tests/branch-hygiene.test.sh +93 -0
- package/tests/design-tokens.test.sh +53 -0
- package/tests/erp-event.test.sh +78 -0
- package/tests/eval-runner.test.sh +147 -0
- package/tests/instructions.test.sh +109 -0
- package/tests/last-report.test.sh +156 -0
- package/tests/lib.test.sh +29 -4
- package/tests/project-sync.test.sh +175 -0
- package/tests/recall.test.sh +91 -0
- package/tests/repo-map.test.sh +70 -0
- package/tests/run-all.sh +12 -0
- package/tests/runner.js +363 -33
- package/tests/state.test.sh +92 -0
- package/tests/verify-panel.test.sh +162 -0
- 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
|
+
}
|
package/bin/host-adapters.js
CHANGED
|
@@ -1,26 +1,66 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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,
|
|
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,
|
|
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
|
-
//
|
|
1594
|
-
//
|
|
1595
|
-
//
|
|
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,
|
|
1599
|
-
agentsContent =
|
|
1600
|
-
|
|
1601
|
-
|
|
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
|
|
1634
|
+
warn(`Codex — could not read framework ${codexFile}: ${e.message}`);
|
|
1604
1635
|
return;
|
|
1605
1636
|
}
|
|
1606
1637
|
|
|
1607
|
-
const dest = path.join(CODEX_DIR,
|
|
1638
|
+
const dest = path.join(CODEX_DIR, codexFile);
|
|
1608
1639
|
|
|
1609
1640
|
try {
|
|
1610
|
-
backupIfDifferent(dest, agentsContent,
|
|
1641
|
+
backupIfDifferent(dest, agentsContent, codexFile);
|
|
1611
1642
|
atomicWrite(dest, agentsContent);
|
|
1612
1643
|
sectionCount++;
|
|
1613
1644
|
ok(`AGENTS.md (configured as ${member.role})`);
|
package/bin/knowledge-flush.js
CHANGED
|
@@ -110,8 +110,11 @@ function dailyLogHasRecentEntries(windowDays) {
|
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
// ── Preflight ────────────────────────────────────────────
|
|
113
|
-
|
|
114
|
-
|
|
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 =
|
|
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
|