qualia-framework 6.14.0 → 7.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/AGENTS.md +8 -5
  2. package/CHANGELOG.md +316 -0
  3. package/CLAUDE.md +3 -1
  4. package/agents/roadmapper.md +16 -14
  5. package/bin/agent-status.js +24 -11
  6. package/bin/batch-plan.js +111 -0
  7. package/bin/branch-hygiene.js +135 -0
  8. package/bin/command-surface.js +2 -0
  9. package/bin/compile-instructions.js +82 -0
  10. package/bin/design-tokens.js +131 -0
  11. package/bin/erp-event.js +177 -0
  12. package/bin/erp-retry.js +12 -1
  13. package/bin/eval-runner.js +218 -0
  14. package/bin/host-adapters.js +84 -12
  15. package/bin/install.js +44 -13
  16. package/bin/knowledge-flush.js +6 -3
  17. package/bin/last-report.js +207 -0
  18. package/bin/project-sync.js +315 -0
  19. package/bin/recall.js +172 -0
  20. package/bin/repo-map.js +188 -0
  21. package/bin/runtime-manifest.js +12 -0
  22. package/bin/state.js +112 -1
  23. package/bin/vault-access.js +82 -0
  24. package/bin/verify-panel.js +294 -0
  25. package/bin/wave-plan.js +211 -0
  26. package/docs/erp-contract.md +180 -0
  27. package/mcp/memory-mcp/server.js +257 -0
  28. package/package.json +6 -3
  29. package/qualia-design/design-dials.md +72 -0
  30. package/qualia-design/design-reference.md +24 -0
  31. package/rules/access.md +42 -0
  32. package/rules/codex-goal.md +28 -26
  33. package/rules/infrastructure.md +1 -1
  34. package/skills/qualia/SKILL.md +6 -0
  35. package/skills/qualia-build/SKILL.md +43 -9
  36. package/skills/qualia-eval/SKILL.md +83 -0
  37. package/skills/qualia-feature/SKILL.md +20 -4
  38. package/skills/qualia-fix/SKILL.md +13 -1
  39. package/skills/qualia-map/SKILL.md +15 -0
  40. package/skills/qualia-milestone/SKILL.md +12 -6
  41. package/skills/qualia-new/REFERENCE.md +6 -4
  42. package/skills/qualia-new/SKILL.md +41 -15
  43. package/skills/qualia-plan/SKILL.md +2 -2
  44. package/skills/qualia-polish/SKILL.md +3 -2
  45. package/skills/qualia-recall/SKILL.md +76 -0
  46. package/skills/qualia-report/SKILL.md +10 -0
  47. package/skills/qualia-scope/SKILL.md +3 -3
  48. package/skills/qualia-ship/SKILL.md +34 -4
  49. package/skills/qualia-update/SKILL.md +4 -0
  50. package/skills/qualia-verify/SKILL.md +53 -24
  51. package/templates/DESIGN.md +15 -0
  52. package/templates/instructions.md +32 -0
  53. package/templates/journey.md +1 -1
  54. package/templates/project-discovery.md +30 -23
  55. package/templates/requirements.md +7 -7
  56. package/tests/agent-status.test.sh +15 -0
  57. package/tests/batch-plan.test.sh +56 -0
  58. package/tests/branch-hygiene.test.sh +93 -0
  59. package/tests/design-tokens.test.sh +53 -0
  60. package/tests/erp-event.test.sh +78 -0
  61. package/tests/eval-runner.test.sh +147 -0
  62. package/tests/instructions.test.sh +109 -0
  63. package/tests/last-report.test.sh +156 -0
  64. package/tests/lib.test.sh +29 -4
  65. package/tests/project-sync.test.sh +175 -0
  66. package/tests/recall.test.sh +91 -0
  67. package/tests/repo-map.test.sh +70 -0
  68. package/tests/run-all.sh +12 -0
  69. package/tests/runner.js +363 -33
  70. package/tests/state.test.sh +92 -0
  71. package/tests/verify-panel.test.sh +162 -0
  72. package/tests/wave-plan.test.sh +153 -0
@@ -77,6 +77,41 @@ many times each employee attempted to use proxy approval.
77
77
  ```
78
78
  Store by `(type, actor_code)` the same way so Fawzi sees a per-employee main-push tally. `client_report_id` is `QS-MAINPUSH-<actor_code>-<count>` and each post carries an idempotency key.
79
79
 
80
+ ### POST /api/v1/events (R14 — unified signed event log)
81
+
82
+ The single endpoint for ALL CLI→ERP events: lifecycle/run events
83
+ (`session_started`, `phase_planned`, `build_wave_started`, `verify_pass`,
84
+ `verify_fail`, …) and, going forward, the policy events above. Emitted by
85
+ `bin/erp-event.js` and queued through `erp-retry.js`.
86
+
87
+ **Headers (Standard-Webhooks style):**
88
+ ```
89
+ Authorization: Bearer <api-key>
90
+ Content-Type: application/json
91
+ Qualia-Event-Id: <unique id — also the idempotency key>
92
+ Qualia-Event-Timestamp: <unix seconds or ISO 8601>
93
+ Qualia-Signature: <base64 HMAC-SHA256 of `${id}.${timestamp}.${rawBody}`, keyed by the api-key; omit for unsigned>
94
+ ```
95
+
96
+ **Body (the `FrameworkEvent`):**
97
+ ```json
98
+ {
99
+ "action": "verify_pass",
100
+ "actor": { "code": "QS-FAWZI-11", "name": "Fawzi", "role": "OWNER" },
101
+ "targets": [{ "type": "project", "ref": "acme-portal" }],
102
+ "context": {},
103
+ "metadata": {},
104
+ "occurred_at": "2026-06-22T10:00:00.000Z",
105
+ "erp_project_id": "7b5d3b4e-…"
106
+ }
107
+ ```
108
+
109
+ The HMAC is keyed by the caller's `qlt_` token (the ERP holds only its hash but
110
+ sees the plaintext Bearer per request), so body integrity + replay are verified
111
+ with no extra secret. Server stores to an append-only `framework_events` table,
112
+ idempotent on `Qualia-Event-Id`; unsigned posts are accepted but recorded
113
+ `signature_valid=false`. Accepts `reports:write` or `events:write` scope.
114
+
80
115
  ### POST /api/v1/reports
81
116
 
82
117
  Upload a session report.
@@ -346,6 +381,151 @@ Snapshot shape:
346
381
  }
347
382
  ```
348
383
 
384
+ ## Project Sync Payload (reconciliation)
385
+
386
+ `qualia-framework project-sync` builds the **single complete project-sync snapshot** — the one payload the ERP reads to RECONCILE a project's milestones, phases, tasks, and reports in one pass, and to understand the PR/merge model that maps a feature branch to deployed `main`.
387
+
388
+ It is distinct from the two existing emitters:
389
+ - `POST /api/v1/reports` (report-payload.js) = ONE work session.
390
+ - `POST /api/v1/project-snapshots` (project-snapshot.js) = lean progress rollup for dashboards (`progress_percent`).
391
+ - **project-sync** = the FULL reconciliation surface: every milestone with REQ-ID completion, current position, task rollup, accountability counters, and the trunk/merge model. project-sync **composes** project-snapshot's identity/current/journey/lifetime/quality blocks (no duplication) and enriches them.
392
+
393
+ ```text
394
+ qualia-framework project-sync # print JSON (compact) to stdout
395
+ qualia-framework project-sync --json # print JSON (pretty)
396
+ qualia-framework project-sync --write # persist .planning/snapshots/project-sync-<ts>.json
397
+ ```
398
+
399
+ Exit 0 = built; exit 2 = no `.planning/` (run `/qualia-new`). Read-only, zero dependencies. Graceful when JOURNEY.md / REQUIREMENTS.md / fields are absent (milestones still emit, REQ blocks report `tracked: false`).
400
+
401
+ ### Payload shape
402
+
403
+ ```json
404
+ {
405
+ "schema_version": 1,
406
+ "generated_at": "2026-06-21T00:00:00.000Z",
407
+ "source": "qualia-framework",
408
+ "payload": "project-sync",
409
+ "framework_version": "6.14.0",
410
+ "identifiers": {
411
+ "project_id": "qs-acme-portal",
412
+ "team_id": "qualia-solutions",
413
+ "git_remote": "github.com/QualiasolutionsCY/acme-portal",
414
+ "erp_project_id": "7b5d3b4e-2b8a-4de4-91a1-9b2f3182f5ef"
415
+ },
416
+ "project": {
417
+ "name": "acme-portal",
418
+ "client": "Acme",
419
+ "status": "built",
420
+ "deployed_url": "https://client.vercel.app",
421
+ "progress_percent": 42,
422
+ "lifecycle": "build",
423
+ "launched_at": "2026-05-01T00:00:00Z"
424
+ },
425
+ "current": {
426
+ "milestone": 2, "milestone_name": "Product",
427
+ "phase": 2, "phase_name": "Dashboard", "total_phases": 4,
428
+ "tasks_done": 3, "tasks_total": 5,
429
+ "verification": "pending", "gap_cycles": 1
430
+ },
431
+ "quality": { "harness_eval": { "status": "PASS", "score": 92, "phase": 2, "generated_at": "…", "artifact": "…" } },
432
+ "total_milestones": 3,
433
+ "milestones": [
434
+ {
435
+ "num": 1, "name": "Foundation", "status": "closed",
436
+ "phases": 3, "tasks_completed": 12,
437
+ "requirements": { "tracked": true, "total": 4, "complete": 4, "incomplete": [] },
438
+ "closed_at": "2026-04-10T18:00:00Z",
439
+ "deployed_url": "https://m1.vercel.app"
440
+ },
441
+ {
442
+ "num": 2, "name": "Product", "status": "current",
443
+ "phases": 4, "tasks_completed": 0,
444
+ "requirements": { "tracked": true, "total": 5, "complete": 3,
445
+ "incomplete": [ { "id": "CORE-03", "status": "Incomplete" } ] }
446
+ },
447
+ {
448
+ "num": 3, "name": "Handoff", "status": "future",
449
+ "phases": 0, "tasks_completed": 0,
450
+ "requirements": { "tracked": false, "total": 0, "complete": 0, "incomplete": [] }
451
+ }
452
+ ],
453
+ "task_rollup": {
454
+ "tasks_completed": 15, "phases_completed": 4, "milestones_completed": 1,
455
+ "total_phases_all_milestones": 7, "build_count": 4, "deploy_count": 1,
456
+ "current_phase_gap_cycles": 1
457
+ },
458
+ "accountability": {
459
+ "offroad_count": 2,
460
+ "offroad": [ { "at": "2026-06-01T10:00:00Z", "milestone": 2, "ref": "BUG-7", "note": "hotfix login" } ]
461
+ },
462
+ "integration": {
463
+ "model": "trunk",
464
+ "integrates_at": "/qualia-ship",
465
+ "protected_branches": ["main", "master"],
466
+ "main_push_event_type": "employee_main_push",
467
+ "main_push_events_path": "~/.claude/.main-push-events.json",
468
+ "note": "Feature branches integrate to main at /qualia-ship (then deploy). Direct pushes to a protected branch are allowed but recorded as employee_main_push policy events for per-employee accountability."
469
+ },
470
+ "timestamps": { "session_started_at": "", "last_pushed_at": "", "last_updated": "" }
471
+ }
472
+ ```
473
+
474
+ ### Field meanings
475
+
476
+ | Field | Meaning |
477
+ |-------|---------|
478
+ | `schema_version` | project-sync contract version. The ERP branches on this to evolve the shape safely. **Distinct** from `snapshot_version` (project-snapshot). |
479
+ | `payload` | Always `"project-sync"` — discriminates this from the report / snapshot payloads when they share an endpoint or queue. |
480
+ | `identifiers` | Same resolution block as project-snapshot — `erp_project_id` is the strongest link, then `git_remote`, then `(team_id, project_id)`. |
481
+ | `project.lifecycle` | `build` (milestone journey) or `operate` (post-launch update stream). The ERP stops expecting a handoff for an `operate` project. |
482
+ | `project.launched_at` / `launch_source` | Present once launched; lets the ERP date the build→operate transition. |
483
+ | `current.*` | The active milestone/phase position + verification + flattened `gap_cycles` (number, current phase). |
484
+ | `total_milestones` | Denominator for milestone-completion (from JOURNEY.md, else max known). |
485
+ | `milestones[]` | The reconciliation spine — every known milestone (journey ∪ closed ∪ current), ordered by num. |
486
+ | `milestones[].status` | `closed` \| `current` \| `future`. The ERP marks done / in-progress / pending from this. |
487
+ | `milestones[].phases` | Phase count: `phases_completed` for closed milestones; the current roadmap phase count for the active one; 0 for future. |
488
+ | `milestones[].tasks_completed` | Tasks completed within that milestone (closed summaries carry this; 0 for current/future — use `task_rollup` for cumulative). |
489
+ | `milestones[].requirements` | REQ-ID completion from REQUIREMENTS.md: `tracked` (false ⇒ no REQ rows declared for this milestone, ERP skips the REQ gate), `total`, `complete`, `incomplete[]` (`{id, status}`). |
490
+ | `milestones[].closed_at` / `deployed_url` | Present when the closed-milestone summary carries them. |
491
+ | `task_rollup` | Cumulative counters so the ERP rolls tasks up without replaying every report: lifetime tasks/phases/milestones, `total_phases_all_milestones`, build/deploy counts, current-phase gap cycles. |
492
+ | `accountability.offroad_count` | Count of off-milestone work units (mirrors the branch-guard main-push tally — drift is counted, not hidden). |
493
+ | `accountability.offroad[]` | Last 10 off-road entries: `{at, milestone, ref, note}`. |
494
+ | `integration` | The PR/merge model (see below). |
495
+
496
+ ### How the ERP reconciles from this payload
497
+
498
+ 1. **Resolve the project** via `identifiers` (same precedence as reports/snapshots).
499
+ 2. **Milestones** — upsert each `milestones[]` entry by `num`. Set the ERP milestone state from `status` (`closed`→done, `current`→in-progress, `future`→pending). When `requirements.tracked` is true, the milestone is **complete only if** `complete === total`; surface `incomplete[]` REQ-IDs as the remaining work. When `tracked` is false, fall back to `status` alone (no REQ table was declared).
500
+ 3. **Phases** — `milestones[].phases` + `current.total_phases` give per-milestone phase counts; `task_rollup.total_phases_all_milestones` is the grand total across the whole journey.
501
+ 4. **Tasks** — use `task_rollup.tasks_completed` for the cumulative figure and `milestones[].tasks_completed` for per-closed-milestone attribution. Do **not** sum reports to get this — the rollup is authoritative.
502
+ 5. **Reports** — project-sync does not carry session notes; it reconciles *state*, while `/api/v1/reports` carries each *session*. Reconcile both against the same resolved project: reports for the activity feed, project-sync for the milestone/phase/task tree.
503
+ 6. **Accountability** — store `offroad_count` per project so off-milestone drift is visible alongside the per-employee `employee_main_push` tally from `/api/v1/policy-events`.
504
+
505
+ ### PR / merge model (what the ERP must understand)
506
+
507
+ The framework uses **trunk integration**, surfaced in `integration`:
508
+
509
+ - Work happens on a **feature branch** (`branch-guard` enforces feature-branch-only; `main`/`master` are protected).
510
+ - `/qualia-ship` **integrates the feature branch into `main`** and then deploys (`integrates_at: "/qualia-ship"`). There is no long-lived release branch; `main` is always the deployable trunk.
511
+ - A direct push to a protected branch is **allowed, not blocked** — but `branch-guard` records an `employee_main_push` policy event (`POST /api/v1/policy-events`, keyed by `(type, actor_code)`) for per-employee accountability. The local journal lives at `main_push_events_path` (`~/.claude/.main-push-events.json`) in the install home; project-sync **references** it, it does not read across the home boundary.
512
+
513
+ So the branch→main→deploy mapping the ERP should encode: **a milestone's `deployed_url` + `status: closed` means its feature work was integrated to `main` and shipped via `/qualia-ship`.** Main-push events are the exception trail, not the normal integration path.
514
+
515
+ ---
516
+
517
+ ## Framework emits (this repo) vs ERP ingests (backend — out of scope here)
518
+
519
+ **Framework emits (delivered in this repo):**
520
+ - `qualia-framework project-sync [--json|--write]` builds and (optionally) persists the payload above.
521
+ - Deterministic, read-only, zero-dependency; composes project-snapshot; graceful on missing fields.
522
+ - The payload SHAPE and field semantics documented in this section are the contract.
523
+
524
+ **ERP ingests (backend work — NOT in this repo):**
525
+ - A `POST /api/v1/project-sync` endpoint (or extend project-snapshots to accept `payload: "project-sync"`) that authenticates the Bearer key and validates `schema_version`.
526
+ - Project resolution + the reconciliation logic in "How the ERP reconciles" above — upserting milestones/phases/tasks, marking completion from REQ counts, and storing `offroad_count`.
527
+ - Wiring an upload path (the framework's `project-snapshot.js` already has `uploadSnapshot()`/retry plumbing the ERP team can mirror for project-sync once the endpoint exists). project-sync itself ships **emit + document** only; the HTTP upload + server-side ingest are the backend's to build.
528
+
349
529
  ## Behavior
350
530
 
351
531
  - When `erp.enabled` is `false`, `/qualia-report` skips the upload and prints an info line so the employee knows the report stayed local.
@@ -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.14.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",
@@ -33,12 +33,15 @@
33
33
  "test:statusline": "bash tests/statusline.test.sh",
34
34
  "test:refs": "bash tests/refs.test.sh",
35
35
  "test:published-install": "bash tests/published-install-smoke.test.sh",
36
- "test:shell": "bash tests/run-all.sh"
36
+ "test:shell": "bash tests/run-all.sh",
37
+ "test:node": "node --test tests/runner.js",
38
+ "compile:instructions": "node bin/compile-instructions.js"
37
39
  },
38
40
  "files": [
39
41
  "bin/",
40
42
  "agents/",
41
43
  "hooks/",
44
+ "mcp/",
42
45
  "rules/",
43
46
  "qualia-design/",
44
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).