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.
@@ -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": "6.22.0",
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
+ ```
@@ -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, &lt; 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
@@ -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
  ```