qualia-framework 6.22.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/CHANGELOG.md +186 -0
- package/bin/batch-plan.js +111 -0
- package/bin/command-surface.js +1 -0
- package/bin/design-tokens.js +131 -0
- package/bin/erp-event.js +177 -0
- package/bin/erp-retry.js +12 -1
- package/bin/host-adapters.js +13 -1
- package/bin/install.js +23 -0
- package/bin/knowledge-flush.js +6 -3
- package/bin/recall.js +172 -0
- package/bin/repo-map.js +188 -0
- package/bin/runtime-manifest.js +6 -0
- package/bin/vault-access.js +82 -0
- package/docs/erp-contract.md +35 -0
- package/mcp/memory-mcp/server.js +257 -0
- package/package.json +4 -2
- package/qualia-design/design-dials.md +72 -0
- package/qualia-design/design-reference.md +24 -0
- package/rules/access.md +42 -0
- package/skills/qualia-build/SKILL.md +31 -0
- package/skills/qualia-map/SKILL.md +15 -0
- package/skills/qualia-new/SKILL.md +14 -0
- package/skills/qualia-polish/SKILL.md +3 -2
- package/skills/qualia-recall/SKILL.md +76 -0
- package/skills/qualia-verify/SKILL.md +8 -0
- package/templates/DESIGN.md +15 -0
- package/tests/batch-plan.test.sh +56 -0
- package/tests/design-tokens.test.sh +53 -0
- package/tests/erp-event.test.sh +78 -0
- package/tests/lib.test.sh +29 -4
- package/tests/recall.test.sh +91 -0
- package/tests/repo-map.test.sh +70 -0
- package/tests/run-all.sh +5 -0
- package/tests/runner.js +363 -33
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Qualia Memory MCP — read-only access to the Obsidian wiki at QUALIA_MEMORY_ROOT.
|
|
3
|
+
// Zero dependencies. Implements MCP over stdio (JSON-RPC 2.0, line-delimited).
|
|
4
|
+
//
|
|
5
|
+
// Tools:
|
|
6
|
+
// memory.search(query, scope?) — case-insensitive grep over wiki, returns hits with file:line
|
|
7
|
+
// memory.read(path) — read a single file under wiki/ as text
|
|
8
|
+
// memory.list(folder?) — list folder contents (one level), default = wiki root
|
|
9
|
+
//
|
|
10
|
+
// Safety:
|
|
11
|
+
// - All paths are resolved under QUALIA_MEMORY_ROOT/wiki and rejected if they escape it.
|
|
12
|
+
// - No write tools. Memory is curated by humans + qualia-framework SessionEnd hooks only.
|
|
13
|
+
|
|
14
|
+
const fs = require("fs");
|
|
15
|
+
const path = require("path");
|
|
16
|
+
const { spawnSync } = require("child_process");
|
|
17
|
+
|
|
18
|
+
const ROOT = process.env.QUALIA_MEMORY_ROOT || path.join(require("os").homedir(), "qualia-memory");
|
|
19
|
+
const WIKI = path.join(ROOT, "wiki");
|
|
20
|
+
|
|
21
|
+
// ─── Vault access control (shared with bin/recall.js) ─────
|
|
22
|
+
// Honor wiki/_meta/access.md so a non-OWNER MCP client can't read OWNER_ONLY
|
|
23
|
+
// paths. Same single implementation as recall.js; fail-closed if it can't load.
|
|
24
|
+
let accessGuard = null;
|
|
25
|
+
try {
|
|
26
|
+
accessGuard = require(path.join(__dirname, "..", "..", "bin", "vault-access.js"));
|
|
27
|
+
} catch {
|
|
28
|
+
/* fall back to OWNER-only access below */
|
|
29
|
+
}
|
|
30
|
+
const ROLE = accessGuard ? accessGuard.resolveRole() : "RESTRICTED";
|
|
31
|
+
function allowed(rel) {
|
|
32
|
+
return accessGuard ? accessGuard.isWikiPathAllowed(rel, ROLE, WIKI) : ROLE === "OWNER";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const PROTOCOL_VERSION = "2024-11-05";
|
|
36
|
+
const SERVER_INFO = { name: "qualia-memory", version: "0.1.0" };
|
|
37
|
+
|
|
38
|
+
const TOOLS = [
|
|
39
|
+
{
|
|
40
|
+
name: "memory.search",
|
|
41
|
+
description:
|
|
42
|
+
"Case-insensitive search across the Qualia memory wiki. Returns matches as " +
|
|
43
|
+
"{ path, line, snippet }. Use this before /qualia-plan or /qualia-build to pull " +
|
|
44
|
+
"prior decisions, client preferences, and reusable patterns.",
|
|
45
|
+
inputSchema: {
|
|
46
|
+
type: "object",
|
|
47
|
+
properties: {
|
|
48
|
+
query: { type: "string", description: "Search term — plain text, no regex." },
|
|
49
|
+
scope: {
|
|
50
|
+
type: "string",
|
|
51
|
+
description:
|
|
52
|
+
"Optional subfolder of wiki/ to limit the search (e.g. 'concepts', 'entities', " +
|
|
53
|
+
"'sessions'). Omit to search the whole wiki.",
|
|
54
|
+
},
|
|
55
|
+
max_results: { type: "integer", description: "Cap on hits (default 30).", default: 30 },
|
|
56
|
+
},
|
|
57
|
+
required: ["query"],
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: "memory.read",
|
|
62
|
+
description: "Read a single wiki page as text. Path is relative to wiki/ root.",
|
|
63
|
+
inputSchema: {
|
|
64
|
+
type: "object",
|
|
65
|
+
properties: {
|
|
66
|
+
path: { type: "string", description: "Relative path under wiki/, e.g. 'concepts/llm-wiki.md'." },
|
|
67
|
+
},
|
|
68
|
+
required: ["path"],
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
name: "memory.list",
|
|
73
|
+
description: "List one level of contents (files + subfolders) under a wiki folder.",
|
|
74
|
+
inputSchema: {
|
|
75
|
+
type: "object",
|
|
76
|
+
properties: {
|
|
77
|
+
folder: {
|
|
78
|
+
type: "string",
|
|
79
|
+
description: "Relative path under wiki/. Omit or '' for the wiki root.",
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
// ─── Path safety ──────────────────────────────────────────
|
|
87
|
+
// Resolve `rel` under WIKI and refuse anything that escapes (.. tricks, abs paths).
|
|
88
|
+
function safeResolve(rel) {
|
|
89
|
+
const resolved = path.resolve(WIKI, rel || ".");
|
|
90
|
+
if (resolved !== WIKI && !resolved.startsWith(WIKI + path.sep)) {
|
|
91
|
+
throw new Error(`Path escapes wiki root: ${rel}`);
|
|
92
|
+
}
|
|
93
|
+
return resolved;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ─── Tool implementations ────────────────────────────────
|
|
97
|
+
function toolSearch({ query, scope, max_results }) {
|
|
98
|
+
if (!query || typeof query !== "string") throw new Error("query is required");
|
|
99
|
+
const cap = Math.max(1, Math.min(200, Number(max_results) || 30));
|
|
100
|
+
const target = scope ? safeResolve(scope) : WIKI;
|
|
101
|
+
if (!fs.existsSync(target)) throw new Error(`Scope not found: ${scope}`);
|
|
102
|
+
|
|
103
|
+
// grep is universally available on POSIX; -r recursive, -n line numbers, -i case-insensitive.
|
|
104
|
+
// -F treats query as fixed string (no regex surprises). --include limits to text-ish files.
|
|
105
|
+
const r = spawnSync(
|
|
106
|
+
"grep",
|
|
107
|
+
[
|
|
108
|
+
"-rniF",
|
|
109
|
+
"--include=*.md",
|
|
110
|
+
"--include=*.txt",
|
|
111
|
+
"--include=*.canvas",
|
|
112
|
+
"--include=*.base",
|
|
113
|
+
"--",
|
|
114
|
+
query,
|
|
115
|
+
target,
|
|
116
|
+
],
|
|
117
|
+
{ encoding: "utf8", maxBuffer: 8 * 1024 * 1024, timeout: 10_000 },
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
if (r.status !== 0 && r.status !== 1) {
|
|
121
|
+
// status 1 = no matches (normal). Anything else is a real failure.
|
|
122
|
+
throw new Error(`grep failed: ${r.stderr || "exit " + r.status}`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const hits = (r.stdout || "")
|
|
126
|
+
.split("\n")
|
|
127
|
+
.filter(Boolean)
|
|
128
|
+
.map((line) => {
|
|
129
|
+
// grep output: <abs_path>:<lineno>:<text>
|
|
130
|
+
const firstColon = line.indexOf(":");
|
|
131
|
+
const secondColon = line.indexOf(":", firstColon + 1);
|
|
132
|
+
if (firstColon < 0 || secondColon < 0) return null;
|
|
133
|
+
const abs = line.slice(0, firstColon);
|
|
134
|
+
const lineno = Number(line.slice(firstColon + 1, secondColon));
|
|
135
|
+
const snippet = line.slice(secondColon + 1).trim();
|
|
136
|
+
return {
|
|
137
|
+
path: path.relative(WIKI, abs),
|
|
138
|
+
line: lineno,
|
|
139
|
+
snippet: snippet.length > 240 ? snippet.slice(0, 240) + "…" : snippet,
|
|
140
|
+
};
|
|
141
|
+
})
|
|
142
|
+
.filter(Boolean)
|
|
143
|
+
.filter((h) => allowed(h.path)) // honor access.md BEFORE capping
|
|
144
|
+
.slice(0, cap);
|
|
145
|
+
|
|
146
|
+
return { query, scope: scope || null, total: hits.length, hits };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function toolRead({ path: rel }) {
|
|
150
|
+
if (!rel || typeof rel !== "string") throw new Error("path is required");
|
|
151
|
+
const abs = safeResolve(rel);
|
|
152
|
+
if (!allowed(path.relative(WIKI, abs))) {
|
|
153
|
+
throw new Error("That path is OWNER-only. Ask Fawzi if you need this.");
|
|
154
|
+
}
|
|
155
|
+
const stat = fs.statSync(abs);
|
|
156
|
+
if (!stat.isFile()) throw new Error(`Not a file: ${rel}`);
|
|
157
|
+
// Cap at 256KB so a runaway page doesn't blow the JSON-RPC frame.
|
|
158
|
+
const MAX = 256 * 1024;
|
|
159
|
+
let content = fs.readFileSync(abs, "utf8");
|
|
160
|
+
let truncated = false;
|
|
161
|
+
if (content.length > MAX) {
|
|
162
|
+
content = content.slice(0, MAX);
|
|
163
|
+
truncated = true;
|
|
164
|
+
}
|
|
165
|
+
return { path: rel, bytes: stat.size, truncated, content };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function toolList({ folder }) {
|
|
169
|
+
const target = safeResolve(folder || ".");
|
|
170
|
+
if (!fs.existsSync(target)) throw new Error(`Folder not found: ${folder || "(root)"}`);
|
|
171
|
+
const entries = fs.readdirSync(target, { withFileTypes: true });
|
|
172
|
+
return {
|
|
173
|
+
folder: path.relative(WIKI, target) || ".",
|
|
174
|
+
entries: entries
|
|
175
|
+
// hide OWNER_ONLY / CONDITIONAL entries from non-OWNER listings
|
|
176
|
+
.filter((e) => allowed(path.relative(WIKI, path.join(target, e.name))))
|
|
177
|
+
.map((e) => ({ name: e.name, type: e.isDirectory() ? "dir" : "file" }))
|
|
178
|
+
.sort((a, b) => (a.type === b.type ? a.name.localeCompare(b.name) : a.type === "dir" ? -1 : 1)),
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const TOOL_HANDLERS = {
|
|
183
|
+
"memory.search": toolSearch,
|
|
184
|
+
"memory.read": toolRead,
|
|
185
|
+
"memory.list": toolList,
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
// ─── MCP / JSON-RPC plumbing ──────────────────────────────
|
|
189
|
+
function rpcResult(id, result) {
|
|
190
|
+
return JSON.stringify({ jsonrpc: "2.0", id, result });
|
|
191
|
+
}
|
|
192
|
+
function rpcError(id, code, message, data) {
|
|
193
|
+
return JSON.stringify({ jsonrpc: "2.0", id, error: { code, message, ...(data && { data }) } });
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function handleRequest(req) {
|
|
197
|
+
const { id, method, params } = req;
|
|
198
|
+
try {
|
|
199
|
+
if (method === "initialize") {
|
|
200
|
+
return rpcResult(id, {
|
|
201
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
202
|
+
serverInfo: SERVER_INFO,
|
|
203
|
+
capabilities: { tools: {} },
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
if (method === "tools/list") {
|
|
207
|
+
return rpcResult(id, { tools: TOOLS });
|
|
208
|
+
}
|
|
209
|
+
if (method === "tools/call") {
|
|
210
|
+
const { name, arguments: args } = params || {};
|
|
211
|
+
const handler = TOOL_HANDLERS[name];
|
|
212
|
+
if (!handler) return rpcError(id, -32601, `Unknown tool: ${name}`);
|
|
213
|
+
const out = handler(args || {});
|
|
214
|
+
return rpcResult(id, {
|
|
215
|
+
content: [{ type: "text", text: JSON.stringify(out, null, 2) }],
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
if (method === "notifications/initialized" || method === "notifications/cancelled") {
|
|
219
|
+
return null; // notifications get no response
|
|
220
|
+
}
|
|
221
|
+
return rpcError(id, -32601, `Unknown method: ${method}`);
|
|
222
|
+
} catch (err) {
|
|
223
|
+
return rpcError(id, -32000, err.message || String(err));
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ─── stdio loop ───────────────────────────────────────────
|
|
228
|
+
let buffer = "";
|
|
229
|
+
process.stdin.setEncoding("utf8");
|
|
230
|
+
process.stdin.on("data", (chunk) => {
|
|
231
|
+
buffer += chunk;
|
|
232
|
+
let nl;
|
|
233
|
+
while ((nl = buffer.indexOf("\n")) >= 0) {
|
|
234
|
+
const line = buffer.slice(0, nl).trim();
|
|
235
|
+
buffer = buffer.slice(nl + 1);
|
|
236
|
+
if (!line) continue;
|
|
237
|
+
let req;
|
|
238
|
+
try {
|
|
239
|
+
req = JSON.parse(line);
|
|
240
|
+
} catch (err) {
|
|
241
|
+
process.stdout.write(rpcError(null, -32700, "Parse error: " + err.message) + "\n");
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
const out = handleRequest(req);
|
|
245
|
+
if (out !== null) process.stdout.write(out + "\n");
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
process.stdin.on("end", () => process.exit(0));
|
|
250
|
+
|
|
251
|
+
// Surface root config on startup so misconfigured installs are visible in MCP logs.
|
|
252
|
+
process.stderr.write(`[qualia-memory] wiki root: ${WIKI}\n`);
|
|
253
|
+
if (!fs.existsSync(WIKI)) {
|
|
254
|
+
process.stderr.write(
|
|
255
|
+
`[qualia-memory] WARNING: wiki root does not exist. Set QUALIA_MEMORY_ROOT or create ${ROOT}/wiki/.\n`,
|
|
256
|
+
);
|
|
257
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "qualia-framework",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "7.0.0",
|
|
4
4
|
"description": "Claude Code and Codex workflow framework by Qualia Solutions. Plan, build, verify, ship.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"qualia-framework": "./bin/cli.js"
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
},
|
|
24
24
|
"homepage": "https://github.com/Qualiasolutions/qualia-framework#readme",
|
|
25
25
|
"scripts": {
|
|
26
|
-
"test": "npm run test:shell",
|
|
26
|
+
"test": "npm run test:shell && npm run test:node",
|
|
27
27
|
"test:state": "bash tests/state.test.sh",
|
|
28
28
|
"test:hooks": "bash tests/hooks.test.sh",
|
|
29
29
|
"test:bin": "bash tests/bin.test.sh",
|
|
@@ -34,12 +34,14 @@
|
|
|
34
34
|
"test:refs": "bash tests/refs.test.sh",
|
|
35
35
|
"test:published-install": "bash tests/published-install-smoke.test.sh",
|
|
36
36
|
"test:shell": "bash tests/run-all.sh",
|
|
37
|
+
"test:node": "node --test tests/runner.js",
|
|
37
38
|
"compile:instructions": "node bin/compile-instructions.js"
|
|
38
39
|
},
|
|
39
40
|
"files": [
|
|
40
41
|
"bin/",
|
|
41
42
|
"agents/",
|
|
42
43
|
"hooks/",
|
|
44
|
+
"mcp/",
|
|
43
45
|
"rules/",
|
|
44
46
|
"qualia-design/",
|
|
45
47
|
"skills/",
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Design Dials + Negative-Constraint Pre-Flight
|
|
2
|
+
|
|
3
|
+
Loaded by `/qualia-polish` (Stage 0) and read when generating any DESIGN.md.
|
|
4
|
+
Two jobs: (1) three tunable **dials** that set a project's aesthetic risk budget,
|
|
5
|
+
(2) a **pre-flight ban-list with positive alternatives** that breaks the model's
|
|
6
|
+
default "AI slop" trajectory *before* code is written — not after, where
|
|
7
|
+
`slop-detect.mjs` catches what slipped through.
|
|
8
|
+
|
|
9
|
+
> Why pre-flight beats post-hoc: a ban paired with a *positive* alternative
|
|
10
|
+
> ("don't do X — do Y instead") steers generation. A bare ban ("don't do X") is
|
|
11
|
+
> the pink-elephant backfire — the model fixates on X. Always pair.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## 1. The three dials
|
|
16
|
+
|
|
17
|
+
Each dial is `LOW | MEDIUM | HIGH`. Set them in DESIGN.md §0; gate them by
|
|
18
|
+
**register + project type** (see §3). The dials are intent, not enforcement —
|
|
19
|
+
they tell a builder/polish agent how much aesthetic risk the project wants.
|
|
20
|
+
|
|
21
|
+
| Dial | LOW | MEDIUM | HIGH |
|
|
22
|
+
|---|---|---|---|
|
|
23
|
+
| **DESIGN_VARIANCE** — how far from convention | Conventional, safe, familiar layouts. Respect existing/client tokens exactly. | One signature move per screen; otherwise conventional. | Editorial/experimental layouts, asymmetry, unexpected grids. Earns attention. |
|
|
24
|
+
| **MOTION_INTENSITY** — how much movement | Functional only (focus, state). No decorative motion. `prefers-reduced-motion` honored. | Purposeful entrance/transition on key elements. | Signature motion as identity (Brand register only) — orchestrated, still ≤ budget. |
|
|
25
|
+
| **VISUAL_DENSITY** — information per screen | Generous whitespace, few elements, one idea per screen. | Balanced; grouped sections. | Dense, dashboard-grade; every pixel earns its place. |
|
|
26
|
+
|
|
27
|
+
A builder reads the resolved values and makes choices *within* that budget. A
|
|
28
|
+
HIGH on every dial is not "better" — it's a different, riskier product.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## 2. Pre-flight ban-list (ban → do instead)
|
|
33
|
+
|
|
34
|
+
Read this BEFORE writing any component or token. Each ban mirrors a
|
|
35
|
+
`slop-detect.mjs` rule (the deterministic post-hoc gate) — this is the
|
|
36
|
+
generation-time half of the same contract, with the positive alternative the
|
|
37
|
+
scanner can't give you.
|
|
38
|
+
|
|
39
|
+
| Ban (the slop tell) | Do instead |
|
|
40
|
+
|---|---|
|
|
41
|
+
| `Inter` / `system-ui` / `Arial` as the brand face | The project's chosen typeface (DESIGN.md §3) with ≥2 weights; a real type pairing |
|
|
42
|
+
| Purple→blue/pink hero gradient | The brand palette from DESIGN.md §2 (OKLCH); a single committed accent |
|
|
43
|
+
| Gradient *text* | Solid ink at AA contrast; reserve color for one accent moment |
|
|
44
|
+
| Pure `#000` on pure `#fff` | Near-black on warm/cool off-white from the token scale (§2) |
|
|
45
|
+
| Hardcoded hex in JSX | A CSS variable / token (the per-client registry — see design-reference) |
|
|
46
|
+
| Glassmorphism (`backdrop-blur` everywhere) | Solid surfaces with real elevation tokens (DESIGN.md §6) |
|
|
47
|
+
| `card-grid` of identical rounded boxes | A layout with hierarchy — feature one item, vary span/scale |
|
|
48
|
+
| Spring/bounce easing as the default | Calm `ease-out` / project easing tokens (DESIGN.md §7); bounce only as a Brand signature |
|
|
49
|
+
| `outline: none` with no replacement | A visible `:focus-visible` ring (accessibility, non-negotiable) |
|
|
50
|
+
| Generic CTA copy ("Get Started", "Learn More") | Specific, product-true microcopy (rubric dim 7) |
|
|
51
|
+
|
|
52
|
+
When VISUAL_DENSITY=HIGH the whitespace ban relaxes; when MOTION_INTENSITY=LOW
|
|
53
|
+
the motion alternatives default to "none." The dials tune the list; they never
|
|
54
|
+
unlock a hard accessibility ban (focus ring, alt text, contrast).
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## 3. Gating by register + project type (don't fight client tokens)
|
|
59
|
+
|
|
60
|
+
The dials default by what the project IS — a corporate client's app must not get
|
|
61
|
+
HIGH variance that overrides their brand system:
|
|
62
|
+
|
|
63
|
+
| Project type | VARIANCE | MOTION | DENSITY | Note |
|
|
64
|
+
|---|---|---|---|---|
|
|
65
|
+
| Client app on an existing brand system | LOW | LOW–MED | per product | Respect their tokens; the per-client registry (design-reference) is law. |
|
|
66
|
+
| Internal tool / admin / dashboard | LOW | LOW | HIGH | Clarity over flair; dense is correct. |
|
|
67
|
+
| Greenfield product (Product register) | MEDIUM | MEDIUM | MEDIUM | One signature move; earn the rest. |
|
|
68
|
+
| Marketing / landing / portfolio (Brand register) | HIGH | HIGH | LOW–MED | Attention is the job; signature motion allowed. |
|
|
69
|
+
|
|
70
|
+
If a client ships a design-token registry, **VARIANCE is capped at LOW for
|
|
71
|
+
color/type** regardless of register — you may be expressive with layout/motion,
|
|
72
|
+
never with their palette or face. See `design-reference.md`.
|
|
@@ -178,3 +178,27 @@ When building reusable components:
|
|
|
178
178
|
- Explicit variants: `<Alert.Destructive>` instead of `<Alert isDestructive>`
|
|
179
179
|
- Children over render props: `children` for composition, not `renderHeader`/`renderFooter`
|
|
180
180
|
- React 19: use `use()` instead of `useContext()`, skip `forwardRef` (ref is a regular prop)
|
|
181
|
+
|
|
182
|
+
## Per-Client Token Registry (R10)
|
|
183
|
+
|
|
184
|
+
The proven escape from AI design monoculture: ground generation in a real design
|
|
185
|
+
system — first-party brand tokens as CSS custom properties — not each builder's
|
|
186
|
+
invented palette.
|
|
187
|
+
|
|
188
|
+
- **Source of truth:** `.planning/design-tokens.json` (the client's brand —
|
|
189
|
+
colors in OKLCH, type stack, radius, spacing). Generated by `/qualia-new` and
|
|
190
|
+
edited to the client.
|
|
191
|
+
- **Artifact:** `app/styles/tokens.css` (or the stack's equivalent), compiled by
|
|
192
|
+
`bin/design-tokens.js compile`. A `:root { --color-…; --font-…; … }` block.
|
|
193
|
+
- **The contract for builders:** reference `var(--color-accent)` /
|
|
194
|
+
`var(--font-sans)` — NEVER a literal hex (`ABS-HEX-IN-JSX` slop ban). The
|
|
195
|
+
registry is the only place brand values live; swap the JSON, recompile, the
|
|
196
|
+
whole app re-skins.
|
|
197
|
+
- **Dial gating:** a client brand registry caps `DESIGN_VARIANCE` at LOW for
|
|
198
|
+
color/type (`design-dials.md` §3) — be expressive with layout/motion, never
|
|
199
|
+
with their palette or face.
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
node bin/design-tokens.js init --out .planning/design-tokens.json
|
|
203
|
+
node bin/design-tokens.js compile .planning/design-tokens.json --out app/styles/tokens.css
|
|
204
|
+
```
|
package/rules/access.md
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Vault Access (security-critical)
|
|
2
|
+
|
|
3
|
+
Governs every read of the `~/qualia-memory/` vault — by `/qualia-recall`, by any
|
|
4
|
+
skill or agent that greps the vault, and by direct `Read`/`Grep`/`Bash` calls.
|
|
5
|
+
The single source of truth for *which* paths are sensitive is the vault's own
|
|
6
|
+
manifest at `~/qualia-memory/wiki/_meta/access.md`. This rule says how to honor it.
|
|
7
|
+
|
|
8
|
+
## The rule
|
|
9
|
+
|
|
10
|
+
Role is read from `~/.claude/.qualia-config.json` (`role` field). When
|
|
11
|
+
`role != OWNER`, paths the manifest lists as **OWNER_ONLY** or **CONDITIONAL**
|
|
12
|
+
MUST NOT be read, grepped, listed, or summarized:
|
|
13
|
+
|
|
14
|
+
- `Finances/` — live invoicing, VAT, outstanding balances
|
|
15
|
+
- `Clients/*.md` — rates, contacts, Zoho IDs, billing addresses
|
|
16
|
+
- `Team/*.md` — compensation / performance context
|
|
17
|
+
- `raw/sessions/` — verbatim session logs (commercial + internal decisions)
|
|
18
|
+
- `memory/` — personal AI context layer
|
|
19
|
+
- `wiki/_meta/access.md` — the manifest itself (prevents trivial bypass)
|
|
20
|
+
- CONDITIONAL (DEFAULT DENY for non-OWNER): `Projects/*.md`, `wiki/sessions/qa/`
|
|
21
|
+
|
|
22
|
+
**ALL_ROLES** paths (`wiki/concepts/`, `wiki/entities/`, `wiki/analysis/`,
|
|
23
|
+
`Research/`, navigational pages, etc.) are open to every authenticated user.
|
|
24
|
+
|
|
25
|
+
When following `[[wikilinks]]` out of ALL_ROLES content INTO an OWNER_ONLY path,
|
|
26
|
+
**stop at the boundary** — do not follow the link.
|
|
27
|
+
|
|
28
|
+
On refusal, say plainly: *"That path is OWNER-only. Ask Fawzi if you need this."*
|
|
29
|
+
|
|
30
|
+
## Deterministic enforcement (don't rely on this prose alone)
|
|
31
|
+
|
|
32
|
+
`${QUALIA_BIN}/recall.js` enforces the manifest in code: it parses
|
|
33
|
+
`wiki/_meta/access.md`, resolves the role, and filters OWNER_ONLY / CONDITIONAL
|
|
34
|
+
vault hits for non-OWNER callers (fail-closed on unknown role). **Prefer
|
|
35
|
+
`/qualia-recall` (or `recall.js`) over hand-rolled `grep ~/qualia-memory`** — the
|
|
36
|
+
tool can't forget the policy the way a prompt can. "A rule worth enforcing is
|
|
37
|
+
worth a gate" (constitution).
|
|
38
|
+
|
|
39
|
+
## When this rule applies
|
|
40
|
+
|
|
41
|
+
Always-on for any session that may touch `~/qualia-memory/`. If the vault does not
|
|
42
|
+
exist on the machine, this rule is inert. OWNER role ⇒ full access (no filtering).
|
|
@@ -22,9 +22,40 @@ Execute phase plan. Each task = fresh subagent. Independent tasks run parallel.
|
|
|
22
22
|
`/qualia-build {N}` — build specific phase
|
|
23
23
|
`/qualia-build {N} --auto` — build + chain into `/qualia-verify {N} --auto` (no human gate)
|
|
24
24
|
`/qualia-build {N} --parallel K` — cap concurrent builders at K (default auto: sequential under 3 tasks, else up to 5)
|
|
25
|
+
`/qualia-build --batch` — the migration lane (R20): map-reduce a large mechanical change over a flat file list, NOT the phase-wave model. See below.
|
|
26
|
+
|
|
27
|
+
## Batch lane (`--batch`) — large mechanical migrations
|
|
28
|
+
|
|
29
|
+
The wave model fits a phase (a few dependency-linked tasks). It does NOT fit a
|
|
30
|
+
50–500-file codemod (rename an API, swap a lib, change an import everywhere).
|
|
31
|
+
For that, `--batch` is a map-reduce over files: small, file-disjoint workers in
|
|
32
|
+
isolated worktrees, fanned in through ONE staging branch.
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
# 1. Resolve the target file list (the migration scope), one path per line:
|
|
36
|
+
grep -rl 'oldApi(' src --include='*.ts' > /tmp/targets.txt
|
|
37
|
+
# 2. Deterministic split into file-disjoint batches + concurrency-capped waves:
|
|
38
|
+
node ${QUALIA_BIN}/batch-plan.js --from /tmp/targets.txt --batch-size 20 --max-workers 5 --staging batch/migrate-oldapi --json
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
`batch-plan.js` emits `batches[]` (each = a branch + its files — disjoint by
|
|
42
|
+
construction) and `waves[]` (batch ids to run concurrently, capped at
|
|
43
|
+
`--max-workers`). Then orchestrate:
|
|
44
|
+
|
|
45
|
+
1. Create the staging branch from `main`: `git branch {staging_branch}`.
|
|
46
|
+
2. For each wave, spawn one worker per batch **in its own worktree**
|
|
47
|
+
(`isolation: "worktree"`), each handed ONLY its `files[]` + the single
|
|
48
|
+
transformation rule. Small context per worker = no drift.
|
|
49
|
+
3. Each worker applies the change to its files and commits to its `batch/…-NN`
|
|
50
|
+
branch. Workers in a wave are file-disjoint, so no conflicts.
|
|
51
|
+
4. Fan in: merge each batch branch into `{staging_branch}`. Run the full test
|
|
52
|
+
suite on staging ONCE. Then `/qualia-ship` merges staging → main.
|
|
53
|
+
5. A failed batch is isolated to its branch — re-run just that worker; the rest
|
|
54
|
+
proceed. Log any batch you drop (no silent partial migrations).
|
|
25
55
|
|
|
26
56
|
## Process
|
|
27
57
|
|
|
58
|
+
|
|
28
59
|
### 0. Set the work-unit goal
|
|
29
60
|
|
|
30
61
|
Per `rules/codex-goal.md` — set the work-unit goal at phase start with scope `phase` (Codex `/goal`; on Claude Code, a tracked task + budget in the banner). One named objective + budget for the whole build.
|
|
@@ -49,6 +49,21 @@ If `ALREADY_MAPPED`, ask:
|
|
|
49
49
|
node ${QUALIA_BIN}/qualia-ui.js banner map
|
|
50
50
|
```
|
|
51
51
|
|
|
52
|
+
### 2.5 Cheap symbol map first (ground the scan)
|
|
53
|
+
|
|
54
|
+
Before spawning the mapper agents, build a zero-dep symbol map so the agents
|
|
55
|
+
read STRUCTURE, not whole files — cheaper and better-grounded onboarding:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
node ${QUALIA_BIN}/repo-map.js . --json > .planning/codebase/repo-map.json
|
|
59
|
+
node ${QUALIA_BIN}/repo-map.js . # human view for the operator
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
It lists every source file's top-level symbols (exports, functions, classes,
|
|
63
|
+
types), busiest files first — the structural backbone. Pass the path of
|
|
64
|
+
`repo-map.json` to each mapper agent as a starting index so they target the
|
|
65
|
+
right files instead of reading the tree blind.
|
|
66
|
+
|
|
52
67
|
### 3. Spawn 5 Mapper Agents (parallel)
|
|
53
68
|
|
|
54
69
|
Each writes one file in `.planning/codebase/`:
|
|
@@ -285,6 +285,20 @@ Then fill the rest of DESIGN.md:
|
|
|
285
285
|
|
|
286
286
|
Cross-check the result against `qualia-design/design-laws.md` §8 absolute bans BEFORE writing — the design must not propose any banned pattern.
|
|
287
287
|
|
|
288
|
+
**Per-client token registry (R10).** Compile DESIGN.md §2/§3/§4 into a CSS-variable
|
|
289
|
+
registry so every builder inherits the real design system instead of inventing
|
|
290
|
+
one (and the `ABS-HEX-IN-JSX` ban has a target to point at):
|
|
291
|
+
|
|
292
|
+
```bash
|
|
293
|
+
node ${QUALIA_BIN}/design-tokens.js init --out .planning/design-tokens.json # starter; edit to the client's brand
|
|
294
|
+
node ${QUALIA_BIN}/design-tokens.js compile .planning/design-tokens.json --out app/styles/tokens.css
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
`design-tokens.json` is the registry source of truth; `tokens.css` is the
|
|
298
|
+
generated artifact the scaffold imports. Builders reference `var(--color-…)` /
|
|
299
|
+
`var(--font-…)`, never a literal hex. Per `design-dials.md`, a client brand
|
|
300
|
+
registry caps DESIGN_VARIANCE at LOW for color/type. See `design-reference.md`.
|
|
301
|
+
|
|
288
302
|
```bash
|
|
289
303
|
git add .planning/DESIGN.md .planning/config.json
|
|
290
304
|
git commit -m "docs: DESIGN.md — direction commit + OKLCH palette + tokens"
|
|
@@ -94,7 +94,8 @@ Before any work — design or otherwise — pass these gates. Skipping them prod
|
|
|
94
94
|
|
|
95
95
|
| Gate | Required check | If fail |
|
|
96
96
|
|---|---|---|
|
|
97
|
-
| Substrate | `qualia-design/design-laws.md`, `design-brand.md`, `design-product.md`, `design-rubric.md`, `design-reference.md`, `frontend.md`, `graphics.md` exist and have been read when scope requires them | Read them. |
|
|
97
|
+
| Substrate | `qualia-design/design-laws.md`, `design-brand.md`, `design-product.md`, `design-rubric.md`, `design-reference.md`, `design-dials.md`, `frontend.md`, `graphics.md` exist and have been read when scope requires them | Read them. |
|
|
98
|
+
| Dials + pre-flight | `qualia-design/design-dials.md` read; the 3 dials resolved from DESIGN.md §0 (or register defaults); the ban→do-instead list loaded BEFORE any token/component edit | Read it; resolve dials; cite the bans you'll avoid. |
|
|
98
99
|
| PRODUCT | `PRODUCT.md` exists at project root, has `register:` field, and is not placeholder (`[TODO]` markers, < 200 chars) | Run setup: ask 5 questions and generate it. Never synthesize from prompt alone. |
|
|
99
100
|
| DESIGN | `DESIGN.md` exists. If missing on App / Redesign scope, BLOCK and run setup. If missing on Component / Section / Critique / Quick scope, NUDGE and proceed. | Generate from PRODUCT.md + 3 questions. |
|
|
100
101
|
| Slop-detect | `bin/slop-detect.mjs` is callable | Install or pull from framework. |
|
|
@@ -103,7 +104,7 @@ Before any work — design or otherwise — pass these gates. Skipping them prod
|
|
|
103
104
|
State this preflight explicitly before editing:
|
|
104
105
|
|
|
105
106
|
```
|
|
106
|
-
POLISH_PREFLIGHT: substrate=pass product=pass design=pass|nudged graphics=loaded|skipped slop_detect=pass mutation=open
|
|
107
|
+
POLISH_PREFLIGHT: substrate=pass product=pass design=pass|nudged dials={VAR}/{MOT}/{DEN} graphics=loaded|skipped slop_detect=pass mutation=open
|
|
107
108
|
```
|
|
108
109
|
|
|
109
110
|
## Process
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: qualia-recall
|
|
3
|
+
description: "Recall curated prior lessons from the Qualia memory substrate — the knowledge layer + the qualia-memory vault, role-filtered. The read side of /qualia-learn. Triggers: 'recall', 'have we done this before', 'what did we learn about X', 'prior art', 'any notes on', 'check the vault'."
|
|
4
|
+
allowed-tools:
|
|
5
|
+
- Read
|
|
6
|
+
- Grep
|
|
7
|
+
- Bash
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# /qualia-recall — Recall Knowledge
|
|
11
|
+
|
|
12
|
+
The **read side** of memory. `/qualia-learn` writes; `/qualia-recall` reads. One
|
|
13
|
+
query, both stores:
|
|
14
|
+
|
|
15
|
+
1. **Knowledge layer** — `${QUALIA_KNOWLEDGE}/` (patterns, fixes, client prefs)
|
|
16
|
+
2. **qualia-memory vault** — `~/qualia-memory/wiki/` (the team's curated,
|
|
17
|
+
cross-project Obsidian LLM Wiki)
|
|
18
|
+
|
|
19
|
+
Use it BEFORE web search or before re-solving a problem — the team may already
|
|
20
|
+
have the answer, already filtered for relevance.
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
- `/qualia-recall {topic}` — recall across both stores
|
|
25
|
+
- `/qualia-recall {topic} --scope vault` — vault only (`knowledge` | `vault` | `all`)
|
|
26
|
+
- `/qualia-recall {topic} --json` — machine output
|
|
27
|
+
|
|
28
|
+
## Access is role-aware (non-negotiable)
|
|
29
|
+
|
|
30
|
+
The vault carries an access manifest at `wiki/_meta/access.md`. `recall.js`
|
|
31
|
+
**enforces it deterministically**: when your role (from
|
|
32
|
+
`~/.claude/.qualia-config.json`, or `$QUALIA_ROLE`) is not `OWNER`, OWNER_ONLY
|
|
33
|
+
and CONDITIONAL paths (Finances, Clients, Team, raw sessions, the access manifest
|
|
34
|
+
itself) are filtered out of results — you still get every ALL_ROLES lesson.
|
|
35
|
+
Unknown role ⇒ fail-closed (restricted). See `${QUALIA_RULES}/access.md`.
|
|
36
|
+
|
|
37
|
+
## Process
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
node ${QUALIA_BIN}/qualia-ui.js banner recall 2>/dev/null || true
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### 1. Run the recall
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
node ${QUALIA_BIN}/recall.js "{topic}" # human digest
|
|
47
|
+
# or, to consume programmatically:
|
|
48
|
+
node ${QUALIA_BIN}/recall.js "{topic}" --json
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
`recall.js` exits 0 even with zero hits, 2 on bad invocation. It delegates the
|
|
52
|
+
knowledge-layer search to `knowledge.js` (so newly-added knowledge files stay
|
|
53
|
+
discoverable) and greps `wiki/` for the vault.
|
|
54
|
+
|
|
55
|
+
### 2. Read the strongest hits
|
|
56
|
+
|
|
57
|
+
The digest ranks files by hit count. `Read` the top files (or the specific
|
|
58
|
+
`file:line` for the vault page) to pull the full lesson before acting. For vault
|
|
59
|
+
pages, paths are relative to `~/qualia-memory/wiki/`.
|
|
60
|
+
|
|
61
|
+
### 3. Apply, then consider learning
|
|
62
|
+
|
|
63
|
+
If the recall resolved your question, apply it. If you discovered something the
|
|
64
|
+
stores DON'T yet have, close the loop with `/qualia-learn`.
|
|
65
|
+
|
|
66
|
+
## Command Output Contract
|
|
67
|
+
|
|
68
|
+
```text
|
|
69
|
+
Command: /qualia-recall {topic}
|
|
70
|
+
Scope: knowledge layer + vault (role-filtered)
|
|
71
|
+
Intent: report
|
|
72
|
+
Mutation: none (read-only)
|
|
73
|
+
Evidence: recall.js across ${QUALIA_KNOWLEDGE}/ + ~/qualia-memory/wiki/
|
|
74
|
+
Output: ranked digest of prior lessons
|
|
75
|
+
Next: Read top hits → apply, or /qualia-learn if it's a new lesson
|
|
76
|
+
```
|
|
@@ -212,6 +212,14 @@ FAIL + gap_cycles >= limit → GAP_CYCLE_LIMIT, escalate.
|
|
|
212
212
|
FAIL + gap_cycles < limit → `/qualia-plan {N} --gaps`.
|
|
213
213
|
Do NOT edit STATE.md or tracking.json manually; state.js handles both.
|
|
214
214
|
|
|
215
|
+
**Emit the lifecycle event** (R14 — feeds the ERP live run-tree). Signed, non-blocking, a no-op when ERP is disabled:
|
|
216
|
+
|
|
217
|
+
```bash
|
|
218
|
+
node ${QUALIA_BIN}/erp-event.js emit {verify_pass|verify_fail} --target project:{project} ${ERP_PROJECT_ID:+--project $ERP_PROJECT_ID}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Use `verify_pass` on PASS, `verify_fail` on FAIL. The same emitter serves other lifecycle points — `session_started` (session start), `phase_planned` (`/qualia-plan`), `build_wave_started` (`/qualia-build`) — all optional and queued through `erp-retry.js`, so they never block the session.
|
|
222
|
+
|
|
215
223
|
Capture new state for auto-chain routing:
|
|
216
224
|
|
|
217
225
|
```bash
|
package/templates/DESIGN.md
CHANGED
|
@@ -4,6 +4,21 @@
|
|
|
4
4
|
> `/qualia-new` generates the skeleton. Update as visual decisions firm up.
|
|
5
5
|
> Required pair: `PRODUCT.md` (sets the brief). This file (`DESIGN.md`) realizes it visually.
|
|
6
6
|
|
|
7
|
+
## 0. Dials (aesthetic risk budget — set before §1)
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
DESIGN_VARIANCE: {LOW · MEDIUM · HIGH} # how far from convention
|
|
11
|
+
MOTION_INTENSITY: {LOW · MEDIUM · HIGH} # how much movement
|
|
12
|
+
VISUAL_DENSITY: {LOW · MEDIUM · HIGH} # information per screen
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Defaults flow from register + project type — see `qualia-design/design-dials.md`
|
|
16
|
+
(the dial table + the gating rules). A client app on an existing brand system
|
|
17
|
+
caps VARIANCE at LOW for color/type; a marketing site runs HIGH. Builders and
|
|
18
|
+
`/qualia-polish` read these and make choices *within* the budget. The same file
|
|
19
|
+
carries the **pre-flight ban→do-instead list** every agent reads before writing
|
|
20
|
+
components.
|
|
21
|
+
|
|
7
22
|
## 1. Direction (mandatory commit, before any color or font)
|
|
8
23
|
|
|
9
24
|
```
|