qualia-framework 6.8.1 → 6.9.2

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 (85) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/bin/install.js +184 -9
  3. package/bin/report-payload.js +12 -0
  4. package/bin/state.js +70 -5
  5. package/docs/EMPLOYEE-QUICKSTART.md +164 -0
  6. package/docs/erp-contract.md +10 -1
  7. package/docs/qualia-manual.html +396 -0
  8. package/hooks/session-start.js +24 -4
  9. package/hooks/usage-capture.js +108 -0
  10. package/package.json +3 -1
  11. package/skills/qualia-doctor/SKILL.md +62 -0
  12. package/skills/qualia-new/REFERENCE.md +7 -0
  13. package/skills/qualia-new/SKILL.md +42 -0
  14. package/skills/qualia-report/SKILL.md +16 -0
  15. package/templates/planning.gitignore +3 -0
  16. package/templates/stacks/README.md +110 -0
  17. package/templates/stacks/ai-agent/env.required.json +44 -0
  18. package/templates/stacks/ai-agent/phases.md +53 -0
  19. package/templates/stacks/ai-agent/scaffold/.env.example +12 -0
  20. package/templates/stacks/ai-agent/scaffold/README.md +31 -0
  21. package/templates/stacks/ai-agent/scaffold/app/api/chat/route.ts +33 -0
  22. package/templates/stacks/ai-agent/scaffold/app/globals.css +13 -0
  23. package/templates/stacks/ai-agent/scaffold/app/layout.tsx +19 -0
  24. package/templates/stacks/ai-agent/scaffold/app/page.tsx +12 -0
  25. package/templates/stacks/ai-agent/scaffold/evals/cases.json +23 -0
  26. package/templates/stacks/ai-agent/scaffold/lib/openrouter/client.ts +51 -0
  27. package/templates/stacks/ai-agent/scaffold/lib/prompts/system.ts +6 -0
  28. package/templates/stacks/ai-agent/scaffold/lib/supabase/client.ts +10 -0
  29. package/templates/stacks/ai-agent/scaffold/lib/supabase/server.ts +28 -0
  30. package/templates/stacks/ai-agent/scaffold/next.config.mjs +7 -0
  31. package/templates/stacks/ai-agent/scaffold/package.json +29 -0
  32. package/templates/stacks/ai-agent/scaffold/postcss.config.mjs +7 -0
  33. package/templates/stacks/ai-agent/scaffold/supabase/migrations/0001_init.sql +41 -0
  34. package/templates/stacks/ai-agent/scaffold/tsconfig.json +21 -0
  35. package/templates/stacks/ai-agent/stack.json +9 -0
  36. package/templates/stacks/ai-agent/verify-checklist.md +31 -0
  37. package/templates/stacks/full-app/env.required.json +20 -0
  38. package/templates/stacks/full-app/phases.md +45 -0
  39. package/templates/stacks/full-app/scaffold/.env.example +7 -0
  40. package/templates/stacks/full-app/scaffold/README.md +28 -0
  41. package/templates/stacks/full-app/scaffold/app/globals.css +14 -0
  42. package/templates/stacks/full-app/scaffold/app/layout.tsx +19 -0
  43. package/templates/stacks/full-app/scaffold/app/page.tsx +20 -0
  44. package/templates/stacks/full-app/scaffold/lib/supabase/client.ts +10 -0
  45. package/templates/stacks/full-app/scaffold/lib/supabase/server.ts +31 -0
  46. package/templates/stacks/full-app/scaffold/next.config.mjs +7 -0
  47. package/templates/stacks/full-app/scaffold/package.json +29 -0
  48. package/templates/stacks/full-app/scaffold/postcss.config.mjs +7 -0
  49. package/templates/stacks/full-app/scaffold/supabase/migrations/0001_init.sql +27 -0
  50. package/templates/stacks/full-app/scaffold/tsconfig.json +21 -0
  51. package/templates/stacks/full-app/stack.json +9 -0
  52. package/templates/stacks/full-app/verify-checklist.md +32 -0
  53. package/templates/stacks/internal-tool/env.required.json +20 -0
  54. package/templates/stacks/internal-tool/phases.md +45 -0
  55. package/templates/stacks/internal-tool/scaffold/.env.example +7 -0
  56. package/templates/stacks/internal-tool/scaffold/README.md +29 -0
  57. package/templates/stacks/internal-tool/scaffold/app/globals.css +13 -0
  58. package/templates/stacks/internal-tool/scaffold/app/layout.tsx +20 -0
  59. package/templates/stacks/internal-tool/scaffold/app/page.tsx +22 -0
  60. package/templates/stacks/internal-tool/scaffold/lib/supabase/client.ts +9 -0
  61. package/templates/stacks/internal-tool/scaffold/lib/supabase/server.ts +28 -0
  62. package/templates/stacks/internal-tool/scaffold/next.config.mjs +6 -0
  63. package/templates/stacks/internal-tool/scaffold/package.json +29 -0
  64. package/templates/stacks/internal-tool/scaffold/postcss.config.mjs +7 -0
  65. package/templates/stacks/internal-tool/scaffold/supabase/migrations/0001_init.sql +28 -0
  66. package/templates/stacks/internal-tool/scaffold/tsconfig.json +21 -0
  67. package/templates/stacks/internal-tool/stack.json +9 -0
  68. package/templates/stacks/internal-tool/verify-checklist.md +31 -0
  69. package/templates/stacks/landing-page/env.required.json +8 -0
  70. package/templates/stacks/landing-page/phases.md +42 -0
  71. package/templates/stacks/landing-page/scaffold/.env.example +3 -0
  72. package/templates/stacks/landing-page/scaffold/README.md +25 -0
  73. package/templates/stacks/landing-page/scaffold/app/globals.css +14 -0
  74. package/templates/stacks/landing-page/scaffold/app/layout.tsx +19 -0
  75. package/templates/stacks/landing-page/scaffold/app/page.tsx +21 -0
  76. package/templates/stacks/landing-page/scaffold/next.config.mjs +7 -0
  77. package/templates/stacks/landing-page/scaffold/package.json +26 -0
  78. package/templates/stacks/landing-page/scaffold/postcss.config.mjs +7 -0
  79. package/templates/stacks/landing-page/scaffold/tsconfig.json +21 -0
  80. package/templates/stacks/landing-page/stack.json +9 -0
  81. package/templates/stacks/landing-page/verify-checklist.md +28 -0
  82. package/tests/bin.test.sh +8 -7
  83. package/tests/hooks.test.sh +32 -0
  84. package/tests/install-smoke.test.sh +4 -3
  85. package/tests/state.test.sh +83 -0
package/CHANGELOG.md CHANGED
@@ -8,6 +8,38 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
8
8
  > Note: git tags for historical versions were not retained; commit references are approximate
9
9
  > and dates reflect commit history rather than npm publish timestamps.
10
10
 
11
+ ## [6.9.2] - 2026-06-20 (docs — visual field manual)
12
+
13
+ ### Added — `docs/qualia-manual.html`
14
+ - A self-contained, single-file introductory **Field Manual** in the Qualia house style (dark + teal `#00FFD1`), shipped in the npm package (`files`). It is the human-friendly visual companion to `docs/EMPLOYEE-QUICKSTART.md`: what the framework is, the plan→build→verify→ship→report loop, employee-mode install, the full command map grouped by purpose, a first-project walkthrough, the five hard rules, the credentials-and-who-issues-them table, and the `/qualia` "when you're lost" escape hatch. Content is grounded in the existing quickstart + skill set — nothing invented. Interactive but dependency-free: sticky scroll-spy nav, click-to-copy command chips, reveal-on-scroll, mobile-responsive, skip-link for a11y. `docs/EMPLOYEE-QUICKSTART.md` now links to it.
15
+
16
+ ## [6.9.1] - 2026-06-16 (fix — stale update banner)
17
+
18
+ ### Fixed — session-start showed a false "update available" banner after catching up
19
+ - `hooks/session-start.js` rendered the cached `.qualia-update-available.json` notice on every session without checking it against the installed version. Because `auto-update.js` only clears that file on its next (throttled) run, the window between an install and that run showed a stale "Current 6.8.1 → Latest 6.9.0" banner even though 6.9.0 was already installed. `maybeRenderUpdateBanner` now validates the notice against `.qualia-config.json` and self-clears (skips render) when the installed version is at or past the advertised `latest`. Regression covered by two new cases in `tests/hooks.test.sh` (stale self-clears; genuine notice preserved).
20
+
21
+ ## [6.9.0] - 2026-06-13 (employee on-ramps — stack presets, employee-mode install, shared knowledge pull)
22
+
23
+ Implements Part A of the 2026-06-13 restructure plan: turn the framework from an owner-only tool into something a new employee can take idea→deployed without tribal knowledge. **Additive only** — no skill, agent, hook, or rule was rewritten; these are on-ramps built *around* the existing loop.
24
+
25
+ ### Added — project-type stack presets (`templates/stacks/`)
26
+ - **Four presets matching real Qualia work:** `landing-page` (Next.js SSG, no DB), `full-app` (Next.js + Supabase auth/RLS + Tailwind), `ai-agent` (adds OpenRouter + optional Retell/ElevenLabs/Telnyx voice), `internal-tool` (Supabase portal-style auth). Each preset ships `stack.json`, `env.required.json` (`{name, purpose, howToObtain, ownerIssued}`), `phases.md`, `verify-checklist.md`, and a minimal-but-runnable `scaffold/` (full-app/internal-tool include `lib/supabase` server+client adapters and an RLS-from-first-migration `0001_init.sql`; ai-agent adds a `lib/openrouter` adapter + `app/api/chat` route + `evals/`). `templates/stacks/README.md` documents the contract. This kills the "every project gets identical generic phases / no working skeleton" gaps.
27
+
28
+ ### Changed — `/qualia-new` instantiates a preset
29
+ - Adds one project-type question, then lays down the chosen preset's scaffold, folds `phases.md` into the plan, and copies `env.required.json` to the new project's `.planning/env.required.json`. `--quick` skips it. Existing flow otherwise unchanged.
30
+
31
+ ### Changed — `/qualia-doctor` validates the preset's env + CLI auth
32
+ - Reads `.planning/env.required.json` (fallback `./env.required.json`), checks each var (env or `.env.local`) and CLI logins (`vercel`, `supabase`, `gh`), and prints PASS/FAIL with the exact fix command. For `ownerIssued: true` vars it routes to "ask the OWNER (Fawzi)" instead of a self-serve command.
33
+
34
+ ### Added — two-mode install (OWNER / EMPLOYEE)
35
+ - `npx qualia-framework install` accepts the explicit keyword `EMPLOYEE` at the install-code prompt: installs the full framework with **no team code**, `erp.enabled=false`. OWNER/team-code installs are byte-for-byte unchanged, and a genuinely bogus or empty code is still rejected (no silent self-elevation). `/qualia-report` degrades gracefully when no team code is set — it writes a **local** report file and says so, instead of failing. New `docs/EMPLOYEE-QUICKSTART.md` walks install→doctor→new→build/verify→ship→report with the credential each step needs.
36
+
37
+ ### Added — shared knowledge pull (Part C bridge)
38
+ - On install/update the framework pulls a **read-only** copy of the `qualia-memory` vault's sanitized `wiki/_export/` (configurable via `QUALIA_KNOWLEDGE_SOURCE` / `QUALIA_KNOWLEDGE_SUBPATH`, git URL or local path) into the knowledge dir, skipping gracefully when unavailable. Freshness comes from the vault's nightly ERP-report ingest, not sync infra. The export is financial-data-sanitized at the source (see qualia-memory `scripts/export-team-wiki.py`).
39
+
40
+ ### Fixed — test drift
41
+ - `tests/bin.test.sh` (Codex backup discipline #122/#123) now greps the `backups/` subdir, matching the v6.8.1 `bakPath()` relocation. The behavior was correct; the test path was stale.
42
+
11
43
  ## [6.8.1] - 2026-06-10 (installer hygiene — global hook sweep, bin purge, bak routing)
12
44
 
13
45
  Found via a live audit of an installed `~/.claude`: the retired brain experiment's `brain-inject.js` was still wired under `UserPromptSubmit`, firing a failing node process on every user prompt, and had survived four releases because the settings merge never looked at that event.
package/bin/install.js CHANGED
@@ -171,6 +171,95 @@ function copyTreeTransform(src, dest, transform) {
171
171
  }
172
172
  }
173
173
 
174
+ // ─── Shared knowledge pull (read-only mirror of qualia-memory) ───
175
+ // Copy-on-install/update only — NOT a sync engine. Pulls a read-only snapshot
176
+ // of the team's shared knowledge wiki into <home>/knowledge/shared/ so every
177
+ // install ships the same accumulated patterns. Source is configurable:
178
+ // QUALIA_KNOWLEDGE_SOURCE — a local path OR a git URL (https/ssh/file)
179
+ // QUALIA_KNOWLEDGE_SUBPATH — subdir inside the source to copy (default wiki/_export)
180
+ // Degrades GRACEFULLY: any failure (no git, no network, missing path, private-
181
+ // repo auth fail) warns and continues. Never throws, never exits the installer.
182
+ //
183
+ // Returns { status, detail } for the caller to log. status is one of:
184
+ // "copied" | "skipped" | "error" (all non-fatal).
185
+ const DEFAULT_KNOWLEDGE_SUBPATH = "wiki/_export";
186
+
187
+ function copyTreeReadOnly(src, dest) {
188
+ // Copy a tree and lock every FILE 0o444 — this mirror is read-only; local
189
+ // edits belong in the user's own knowledge files, not here. Directories stay
190
+ // writable (0o755) so the next install/update can replace the mirror cleanly
191
+ // (deleting an entry needs write on its parent dir, not on the entry).
192
+ if (!fs.existsSync(src)) return 0;
193
+ if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
194
+ let count = 0;
195
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
196
+ if (entry.name.startsWith(".")) continue;
197
+ const s = path.join(src, entry.name);
198
+ const d = path.join(dest, entry.name);
199
+ if (entry.isDirectory()) {
200
+ count += copyTreeReadOnly(s, d);
201
+ } else if (entry.isFile()) {
202
+ fs.copyFileSync(s, d);
203
+ try { fs.chmodSync(d, 0o444); } catch {}
204
+ count++;
205
+ }
206
+ }
207
+ return count;
208
+ }
209
+
210
+ function pullSharedKnowledge(knowledgeDest) {
211
+ const source = (process.env.QUALIA_KNOWLEDGE_SOURCE || "").trim();
212
+ if (!source) {
213
+ return { status: "skipped", detail: "QUALIA_KNOWLEDGE_SOURCE not set — shared knowledge pull disabled" };
214
+ }
215
+ const subpath = (process.env.QUALIA_KNOWLEDGE_SUBPATH || DEFAULT_KNOWLEDGE_SUBPATH).trim();
216
+ const sharedDest = path.join(knowledgeDest, "shared");
217
+ const os = require("os");
218
+ let tmpClone = null;
219
+ try {
220
+ let sourceRoot;
221
+ const looksGit = /^(https?:\/\/|git@|ssh:\/\/|git:\/\/)/.test(source) || source.endsWith(".git");
222
+ if (looksGit) {
223
+ // Shallow clone into a temp dir. spawnSync with argv (no shell) so the
224
+ // URL can't be misinterpreted. Short timeout — never hang an install.
225
+ const { spawnSync } = require("child_process");
226
+ tmpClone = fs.mkdtempSync(path.join(os.tmpdir(), "qualia-knowledge-"));
227
+ const r = spawnSync("git", ["clone", "--depth", "1", "--quiet", source, tmpClone], {
228
+ stdio: ["ignore", "ignore", "pipe"],
229
+ timeout: 30000,
230
+ encoding: "utf8",
231
+ });
232
+ if (r.status !== 0) {
233
+ const why = (r.stderr || "").trim().split("\n").pop() || `git exited ${r.status}`;
234
+ return { status: "skipped", detail: `clone failed (${why}) — skipped, continuing` };
235
+ }
236
+ sourceRoot = tmpClone;
237
+ } else {
238
+ // Local path source.
239
+ if (!fs.existsSync(source)) {
240
+ return { status: "skipped", detail: `source path not found: ${source} — skipped, continuing` };
241
+ }
242
+ sourceRoot = source;
243
+ }
244
+
245
+ const exportDir = path.join(sourceRoot, subpath);
246
+ if (!fs.existsSync(exportDir)) {
247
+ return { status: "skipped", detail: `subpath '${subpath}' absent in source — skipped, continuing` };
248
+ }
249
+
250
+ // Replace the prior mirror wholesale (it's read-only and fully derived).
251
+ if (fs.existsSync(sharedDest)) {
252
+ try { fs.rmSync(sharedDest, { recursive: true, force: true }); } catch {}
253
+ }
254
+ const n = copyTreeReadOnly(exportDir, sharedDest);
255
+ return { status: "copied", detail: `${n} file(s) → knowledge/shared/ (read-only mirror)` };
256
+ } catch (e) {
257
+ return { status: "error", detail: `${e.message} — skipped, continuing` };
258
+ } finally {
259
+ if (tmpClone) { try { fs.rmSync(tmpClone, { recursive: true, force: true }); } catch {} }
260
+ }
261
+ }
262
+
174
263
  function ensureCodexStatusLineConfig(existing) {
175
264
  const statusLine = `status_line = ${JSON.stringify(CODEX_STATUS_LINE)}`;
176
265
  const colors = "status_line_use_colors = true";
@@ -448,18 +537,42 @@ function askCode() {
448
537
  printHeader();
449
538
  const line = nextPipedLine();
450
539
  // Echo the prompt + answer for log readability.
451
- process.stdout.write(` ${WHITE}Enter install code:${RESET} ${line}\n`);
540
+ process.stdout.write(` ${WHITE}Enter install code (or "EMPLOYEE" for no-code install):${RESET} ${line}\n`);
452
541
  resolve(String(line || "").trim());
453
542
  return;
454
543
  }
455
544
  const rl = getRl();
456
545
  printHeader();
457
- rl.question(` ${WHITE}Enter install code:${RESET} `, (answer) => {
546
+ console.log(` ${DIM}OWNER / team member? Enter your install code (QS-NAME-##).${RESET}`);
547
+ console.log(` ${DIM}New employee without a code? Type ${RESET}${TEAL}EMPLOYEE${RESET}${DIM} to install in employee mode.${RESET}`);
548
+ console.log("");
549
+ rl.question(` ${WHITE}Install code or "EMPLOYEE":${RESET} `, (answer) => {
458
550
  resolve(String(answer || "").trim());
459
551
  });
460
552
  });
461
553
  }
462
554
 
555
+ // ─── Employee mode (no team code) ────────────────────────
556
+ // A new employee may not have a QS-NAME-## code yet. Typing "EMPLOYEE" at the
557
+ // install-code prompt installs the full framework at the least-privilege role:
558
+ // feature branches only, no main pushes (enforced by branch-guard, which trusts
559
+ // the role bit in the 0o600 config). ERP reporting stays OFF until a real team
560
+ // code is set, so /qualia-report degrades to a local-only report.
561
+ //
562
+ // Defaulting to EMPLOYEE (not OWNER) on a missing/keyword code is the secure
563
+ // failure mode: a bogus code that is NOT the EMPLOYEE keyword is still rejected
564
+ // (see main()), so this can't be used to silently self-elevate.
565
+ const EMPLOYEE_KEYWORDS = new Set(["EMPLOYEE", "EMPLOYEE-MODE", "NO-CODE", "NOCODE"]);
566
+ const EMPLOYEE_MEMBER = {
567
+ name: "Qualia Employee",
568
+ role: "EMPLOYEE",
569
+ description: "Developer. Feature branches only. Cannot push to main or edit .env files.",
570
+ };
571
+
572
+ function isEmployeeKeyword(raw) {
573
+ return EMPLOYEE_KEYWORDS.has(String(raw || "").trim().toUpperCase());
574
+ }
575
+
463
576
  // ─── Prompt for install target (Claude / Codex / Both) ──
464
577
  // Backward-compat: a piped install with only the team code (single stdin
465
578
  // line) closes stdin before this prompt; we silently default to "1"
@@ -525,12 +638,24 @@ async function main() {
525
638
  }
526
639
  const rawCode = await askCode();
527
640
  const code = resolveTeamCode(rawCode);
528
- const member = code ? TEAM[code] : null;
641
+ let member = code ? TEAM[code] : null;
642
+
643
+ // Employee mode: the user typed the EMPLOYEE keyword instead of a team code.
644
+ // Synthesize a least-privilege EMPLOYEE member. The team code stays empty,
645
+ // which keeps ERP reporting off (set later via a real code or set-erp-key).
646
+ const employeeMode = !member && isEmployeeKeyword(rawCode);
647
+ if (employeeMode) {
648
+ member = EMPLOYEE_MEMBER;
649
+ }
529
650
 
530
651
  if (!member) {
652
+ // A genuine bad code (not the EMPLOYEE keyword) is still rejected — typing
653
+ // garbage must never silently install. This preserves the install-code gate
654
+ // for team members while only the explicit EMPLOYEE keyword bypasses it.
531
655
  console.log("");
532
656
  log(`${RED}✗${RESET} Invalid code: "${rawCode}". Get your install code from Fawzi.`);
533
657
  log(`${DIM} Tip: codes use digit zero, not letter O. Format: QS-NAME-##${RESET}`);
658
+ log(`${DIM} No code yet? Type ${RESET}${TEAL}EMPLOYEE${RESET}${DIM} to install in employee mode.${RESET}`);
534
659
  console.log("");
535
660
  process.exit(1);
536
661
  }
@@ -539,6 +664,9 @@ async function main() {
539
664
  const roleColor = member.role === "OWNER" ? TEAL : GREEN;
540
665
  console.log(` ${GREEN}✓${RESET} ${WHITE}${BOLD}Welcome, ${member.name}${RESET}`);
541
666
  console.log(` ${DIM} Role:${RESET} ${roleColor}${member.role}${RESET}`);
667
+ if (employeeMode) {
668
+ console.log(` ${DIM} Mode:${RESET} ${YELLOW}employee (no team code) — ERP reporting off until a code is set${RESET}`);
669
+ }
542
670
 
543
671
  // ─── Ask install target (Claude / Codex / Both) ────────
544
672
  const target = await askTarget();
@@ -552,7 +680,7 @@ async function main() {
552
680
  if (!installClaude) {
553
681
  // Codex-only path: skip the entire Claude install block. Jump straight
554
682
  // to the Codex installer + final summary.
555
- await installCodex(member, target);
683
+ await installCodex(member, target, employeeMode);
556
684
  return;
557
685
  }
558
686
 
@@ -779,6 +907,15 @@ async function main() {
779
907
  }
780
908
  }
781
909
 
910
+ // ─── Shared knowledge pull (read-only mirror of qualia-memory wiki) ──
911
+ // Copy-on-install only. Configure with QUALIA_KNOWLEDGE_SOURCE (git URL or
912
+ // local path). Skips gracefully when unset/unreachable — never blocks install.
913
+ {
914
+ const pull = pullSharedKnowledge(knowledgeDest);
915
+ if (pull.status === "copied") ok(`shared knowledge — ${pull.detail}`);
916
+ else log(`${DIM}shared knowledge — ${pull.detail}${RESET}`);
917
+ }
918
+
782
919
  // ─── References (methodology docs loaded by skills at runtime) ────
783
920
  printSection("References");
784
921
  const refDir = path.join(FRAMEWORK_DIR, "references");
@@ -988,17 +1125,25 @@ Client-specific preferences, design choices, and requirements. Loaded by \`/qual
988
1125
  }
989
1126
 
990
1127
  // ─── Save config (for update command) ──────────────────
1128
+ // Employee mode (no team code): code is the empty string, and ERP reporting
1129
+ // is disabled by default — there's no team code to attribute reports to, so
1130
+ // /qualia-report degrades to a local-only report. A real team code (or
1131
+ // `qualia-framework set-erp-key`) flips ERP back on later.
991
1132
  const configFile = path.join(CLAUDE_DIR, ".qualia-config.json");
992
1133
  const config = {
993
- code,
1134
+ code: code || "",
994
1135
  installed_by: member.name,
995
1136
  role: member.role,
996
1137
  version: require("../package.json").version,
997
1138
  installed_at: new Date().toISOString().split("T")[0],
998
1139
  erp: {
999
- enabled: true,
1140
+ enabled: !employeeMode,
1000
1141
  url: "https://portal.qualiasolutions.net",
1001
1142
  api_key_file: ".erp-api-key",
1143
+ // Performance-audit telemetry. command_usage (counts) is always on.
1144
+ // capturePrompts records real prompt text for the prompt-quality judge —
1145
+ // written explicitly (not implied) so engineers can see and flip it.
1146
+ capturePrompts: true,
1002
1147
  },
1003
1148
  };
1004
1149
  // mode 0o600: this file holds the role bit (OWNER vs EMPLOYEE) which the
@@ -1134,6 +1279,8 @@ Client-specific preferences, design choices, and requirements. Loaded by \`/qual
1134
1279
  "fawzi-approval-guard.js",
1135
1280
  // v5.0 — insights-driven destructive-op + wrong-account guards
1136
1281
  "vercel-account-guard.js", "env-empty-guard.js", "supabase-destructive-guard.js",
1282
+ // performance-audit telemetry capture (UserPromptSubmit)
1283
+ "usage-capture.js",
1137
1284
  ]);
1138
1285
  const isQualiaHookCmd = (cmd) => {
1139
1286
  if (typeof cmd !== "string") return false;
@@ -1197,6 +1344,16 @@ Client-specific preferences, design choices, and requirements. Loaded by \`/qual
1197
1344
  ],
1198
1345
  },
1199
1346
  ],
1347
+ // Performance-audit telemetry — record qualia-command usage + (opt-in)
1348
+ // prompt samples per session for the ERP clock-out payload.
1349
+ UserPromptSubmit: [
1350
+ {
1351
+ matcher: ".*",
1352
+ hooks: [
1353
+ { type: "command", command: nodeCmd("usage-capture.js"), timeout: 5 },
1354
+ ],
1355
+ },
1356
+ ],
1200
1357
  };
1201
1358
 
1202
1359
  // Merge user hooks: strip Qualia-owned commands, preserve everything else.
@@ -1297,7 +1454,7 @@ Client-specific preferences, design choices, and requirements. Loaded by \`/qual
1297
1454
 
1298
1455
  // ─── Codex (optional second target) ──────────────────────
1299
1456
  if (installCodexTarget) {
1300
- await installCodex(member, target);
1457
+ await installCodex(member, target, employeeMode);
1301
1458
  }
1302
1459
 
1303
1460
  // ─── Summary ───────────────────────────────────────────
@@ -1378,6 +1535,13 @@ function printSummary({ member, target, claudeInstalled }) {
1378
1535
  console.log(` ${DIM}End of day?${RESET} ${TEAL}/qualia-report${RESET} ${DIM}(shift submission)${RESET}`);
1379
1536
  console.log(` ${DIM}Stuck?${RESET} ${TEAL}/qualia${RESET}`);
1380
1537
  console.log("");
1538
+ console.log(
1539
+ ` ${DIM}Telemetry${RESET} ${DIM}your /qualia command usage + prompt samples feed the team${RESET}`
1540
+ );
1541
+ console.log(
1542
+ ` ${DIM} performance audit. Opt out:${RESET} ${TEAL}erp.capturePrompts=false${RESET} ${DIM}in ~/.claude/.qualia-config.json${RESET}`
1543
+ );
1544
+ console.log("");
1381
1545
  console.log(` ${DIM2}${RULE}${RESET}`);
1382
1546
  console.log(` ${TEAL}${BOLD}Welcome to the future with Qualia.${RESET}`);
1383
1547
  console.log(` ${DIM2}${RULE}${RESET}`);
@@ -1391,7 +1555,7 @@ function printSummary({ member, target, claudeInstalled }) {
1391
1555
  // Claude install. The only thing intentionally not claimed is a Claude-style
1392
1556
  // persistent statusLine setting; Codex exposes hook status messages today, not
1393
1557
  // an equivalent global status-line command.
1394
- async function installCodex(member, target) {
1558
+ async function installCodex(member, target, employeeMode = false) {
1395
1559
  console.log("");
1396
1560
  console.log(` ${TEAL}▸${RESET} ${WHITE}${BOLD}Codex${RESET}`);
1397
1561
  console.log(` ${DIM2}${"─".repeat(40)}${RESET}`);
@@ -1480,9 +1644,13 @@ async function installCodex(member, target) {
1480
1644
  version: require("../package.json").version,
1481
1645
  installed_at: new Date().toISOString().split("T")[0],
1482
1646
  erp: {
1483
- enabled: true,
1647
+ // Employee mode (no team code) leaves ERP off so /qualia-report
1648
+ // degrades to a local-only report instead of failing.
1649
+ enabled: !employeeMode,
1484
1650
  url: "https://portal.qualiasolutions.net",
1485
1651
  api_key_file: ".erp-api-key",
1652
+ // See ~/.claude config above — explicit so engineers can see/flip it.
1653
+ capturePrompts: true,
1486
1654
  },
1487
1655
  };
1488
1656
  atomicWrite(path.join(CODEX_DIR, ".qualia-config.json"), JSON.stringify(codexConfig, null, 2) + "\n", 0o600);
@@ -1604,6 +1772,13 @@ async function installCodex(member, target) {
1604
1772
  if (!fs.existsSync(out)) copyTextTransform(path.join(knowledgeSrc, file), out, codexText);
1605
1773
  }
1606
1774
  }
1775
+ // Shared knowledge mirror (read-only) — same copy-on-install semantics as
1776
+ // the Claude path. Skips gracefully when QUALIA_KNOWLEDGE_SOURCE is unset.
1777
+ {
1778
+ const pull = pullSharedKnowledge(knowledgeDest);
1779
+ if (pull.status === "copied") ok(`shared knowledge — ${pull.detail}`);
1780
+ else log(`${DIM}shared knowledge — ${pull.detail}${RESET}`);
1781
+ }
1607
1782
  copyTextTransform(path.join(FRAMEWORK_DIR, "guide.md"), path.join(CODEX_DIR, "qualia-guide.md"), codexText);
1608
1783
  ok("templates/ + knowledge/ + references/ + guide");
1609
1784
  } catch (e) {
@@ -89,6 +89,16 @@ function buildPayload(options = {}) {
89
89
  const latestHarnessEval = harnessEval.latestEval(cwd);
90
90
  const workPacket = readLocalWorkPacket(cwd);
91
91
 
92
+ // Performance-audit telemetry captured during the session by the
93
+ // usage-capture UserPromptSubmit hook. Cleared by /qualia-report after upload.
94
+ const usagePath = options.usagePath || path.join(cwd, ".planning", ".session-usage.json");
95
+ const usage = readJson(usagePath, {});
96
+ const commandUsage =
97
+ usage && typeof usage.command_usage === "object" && usage.command_usage
98
+ ? usage.command_usage
99
+ : {};
100
+ const promptSamples = Array.isArray(usage.prompt_samples) ? usage.prompt_samples : [];
101
+
92
102
  return {
93
103
  project: tracking.project || path.basename(cwd),
94
104
  project_id: projectKey,
@@ -118,6 +128,8 @@ function buildPayload(options = {}) {
118
128
  gap_cycles: (tracking.gap_cycles || {})[String(phase)] || 0,
119
129
  build_count: tracking.build_count || 0,
120
130
  deploy_count: tracking.deploy_count || 0,
131
+ command_usage: commandUsage,
132
+ prompt_samples: promptSamples,
121
133
  deployed_url: tracking.deployed_url || "",
122
134
  ...(tracking.session_started_at ? { session_started_at: tracking.session_started_at } : {}),
123
135
  ...(tracking.last_pushed_at ? { last_pushed_at: tracking.last_pushed_at } : {}),
package/bin/state.js CHANGED
@@ -729,6 +729,33 @@ function readNextMilestoneNameFromJourney(milestoneNum) {
729
729
  }
730
730
  }
731
731
 
732
+ // A milestone name is a "placeholder" when it's blank or the literal
733
+ // "Milestone N" fallback that close-milestone historically emitted when
734
+ // tracking.json carried no real name. Placeholders must never beat a real
735
+ // name from JOURNEY.md (observed in ERP reports as {"num":"9","name":"Milestone 9"}).
736
+ function isPlaceholderMilestoneName(name, num) {
737
+ const trimmed = String(name == null ? "" : name).trim();
738
+ if (!trimmed) return true;
739
+ return new RegExp(`^Milestone\\s+${num}$`, "i").test(trimmed);
740
+ }
741
+
742
+ // Resolve the human name for milestone `num`. Preference order:
743
+ // 1. `candidate` (e.g. tracking.json milestone_name) when it's a real name
744
+ // 2. the `## Milestone N · Name` header in JOURNEY.md
745
+ // 3. the "Milestone N" placeholder — last resort only
746
+ function resolveMilestoneName(num, candidate) {
747
+ const trimmed = String(candidate == null ? "" : candidate).trim();
748
+ if (!isPlaceholderMilestoneName(trimmed, num)) return trimmed;
749
+ const fromJourney = readNextMilestoneNameFromJourney(num);
750
+ if (fromJourney) return fromJourney;
751
+ return `Milestone ${num}`;
752
+ }
753
+
754
+ // Normalize a milestone name for dedupe comparisons.
755
+ function normalizeMilestoneName(name) {
756
+ return String(name == null ? "" : name).trim().toLowerCase();
757
+ }
758
+
732
759
  function readState() {
733
760
  try {
734
761
  return fs.readFileSync(STATE_FILE, "utf8");
@@ -2166,7 +2193,9 @@ function cmdCloseMilestone(opts) {
2166
2193
  }
2167
2194
  ensureLifetime(t);
2168
2195
 
2169
- const closedMilestone = t.milestone || 1;
2196
+ // parseInt legacy tracking.json files carry milestone as a string ("9"),
2197
+ // which would corrupt `closedMilestone + 1` ("91") and break num dedupe.
2198
+ const closedMilestone = parseInt(t.milestone, 10) || 1;
2170
2199
  if (
2171
2200
  !opts.force &&
2172
2201
  typeof t.lifetime.last_closed_milestone === "number" &&
@@ -2227,9 +2256,13 @@ function cmdCloseMilestone(opts) {
2227
2256
  0,
2228
2257
  (parseInt(t.lifetime && t.lifetime.tasks_completed) || 0) - priorMilestoneTasks
2229
2258
  );
2259
+ // Resolve the REAL milestone name: non-placeholder tracking.json
2260
+ // milestone_name first, then the JOURNEY.md `## Milestone N · Name` header,
2261
+ // and only as a last resort the "Milestone N" placeholder.
2262
+ const milestoneName = resolveMilestoneName(closedMilestone, t.milestone_name);
2230
2263
  const summary = {
2231
2264
  num: closedMilestone,
2232
- name: t.milestone_name || `Milestone ${closedMilestone}`,
2265
+ name: milestoneName,
2233
2266
  total_phases: parseInt(t.total_phases) || s.phases.length || 0,
2234
2267
  phases_completed: phasesCompleted,
2235
2268
  tasks_completed: tasksCompletedThisMilestone,
@@ -2237,8 +2270,18 @@ function cmdCloseMilestone(opts) {
2237
2270
  closed_at: new Date().toISOString(),
2238
2271
  };
2239
2272
  t.milestones = Array.isArray(t.milestones) ? t.milestones : [];
2240
- // Idempotency: don't duplicate if the same milestone number is already logged.
2241
- const existing = t.milestones.findIndex((m) => m && m.num === closedMilestone);
2273
+ // Idempotency + dedupe: update in place when the same milestone number is
2274
+ // already logged (num may be a string in legacy entries), OR when the same
2275
+ // real name is already logged under a different num (renumbering drift —
2276
+ // observed as duplicate names in ERP session_reports.milestones).
2277
+ let existing = t.milestones.findIndex(
2278
+ (m) => m && parseInt(m.num, 10) === closedMilestone
2279
+ );
2280
+ if (existing < 0 && !isPlaceholderMilestoneName(milestoneName, closedMilestone)) {
2281
+ existing = t.milestones.findIndex(
2282
+ (m) => m && normalizeMilestoneName(m.name) === normalizeMilestoneName(milestoneName)
2283
+ );
2284
+ }
2242
2285
  if (existing >= 0) {
2243
2286
  t.milestones[existing] = summary;
2244
2287
  } else {
@@ -2533,13 +2576,35 @@ function cmdBackfillMilestones(opts) {
2533
2576
  };
2534
2577
  closedSummaries.push({ num: row.num, name: row.name, phases: phaseCount });
2535
2578
 
2536
- const existing = t.milestones.findIndex((mm) => mm && mm.num === row.num);
2579
+ // Match by num first (parseInt legacy entries store num as a string),
2580
+ // then by normalized real name under a different num (renumbering drift).
2581
+ let existing = t.milestones.findIndex(
2582
+ (mm) => mm && parseInt(mm.num, 10) === row.num
2583
+ );
2584
+ if (existing < 0 && !isPlaceholderMilestoneName(row.name, row.num)) {
2585
+ existing = t.milestones.findIndex(
2586
+ (mm) => mm && normalizeMilestoneName(mm.name) === normalizeMilestoneName(row.name)
2587
+ );
2588
+ }
2537
2589
  if (existing >= 0) {
2538
2590
  // Don't override entries that came from real /qualia-milestone close
2539
2591
  // (they have richer data). Only overwrite previously-backfilled entries.
2540
2592
  if (t.milestones[existing].backfilled) {
2541
2593
  t.milestones[existing] = summary;
2542
2594
  updated++;
2595
+ } else if (
2596
+ isPlaceholderMilestoneName(
2597
+ t.milestones[existing].name,
2598
+ parseInt(t.milestones[existing].num, 10)
2599
+ ) ||
2600
+ parseInt(t.milestones[existing].num, 10) !== row.num
2601
+ ) {
2602
+ // Real close entry but with a placeholder name ("Milestone 9") or a
2603
+ // stale num — repair the identity fields in place, keep the richer
2604
+ // close data (tasks_completed, shipped_url, closed_at).
2605
+ t.milestones[existing].num = row.num;
2606
+ t.milestones[existing].name = row.name;
2607
+ updated++;
2543
2608
  }
2544
2609
  } else {
2545
2610
  t.milestones.push(summary);
@@ -0,0 +1,164 @@
1
+ # Qualia Framework — Employee Quickstart
2
+
3
+ A five-minute path from a fresh machine to a shipped, reported day of work. This is the route for a **new employee who does not have a team install code yet**. You can install and do real work in employee mode today; the OWNER (Fawzi) issues the keys that unlock ERP reporting and any direct provider integrations.
4
+
5
+ > Prefer a visual tour? Open **[`docs/qualia-manual.html`](./qualia-manual.html)** — the same path as an interactive Field Manual (command map, first-project walkthrough, copy-to-clipboard commands).
6
+
7
+ Who issues credentials: **the OWNER (Fawzi) issues every key** — team install codes, the ERP API key, OpenRouter / Supabase / Vercel / Retell / ElevenLabs / Telnyx credentials. If a step says "ask Fawzi", that is who to ask. Never share or reuse another person's key.
8
+
9
+ ---
10
+
11
+ ## The path
12
+
13
+ ```
14
+ install (employee mode) → /qualia-doctor → /qualia-new (pick preset) → build/verify loop → /qualia-ship → report
15
+ ```
16
+
17
+ ---
18
+
19
+ ## 1. Install — employee mode (no team code needed)
20
+
21
+ ```bash
22
+ npx qualia-framework@latest install
23
+ ```
24
+
25
+ At the prompt:
26
+
27
+ ```
28
+ Install code or "EMPLOYEE":
29
+ ```
30
+
31
+ - Have a team code (`QS-NAME-##`)? Enter it — you install as that team member.
32
+ - **No code yet?** Type **`EMPLOYEE`**. You install at the least-privilege role: feature branches only, no pushes to `main` (enforced by the `branch-guard` hook). The full framework — skills, agents, hooks, knowledge — is installed exactly the same.
33
+
34
+ What employee mode changes vs. a coded install:
35
+
36
+ | | Coded install (team member) | Employee mode (no code) |
37
+ |---|---|---|
38
+ | Role | OWNER or EMPLOYEE per code | EMPLOYEE |
39
+ | Skills / agents / hooks | full | full |
40
+ | Push to `main` | OWNER only | blocked |
41
+ | ERP reporting | on (with API key) | **off** until a code/key is set |
42
+ | `/qualia-report` | uploads to ERP | saves a **local** report file |
43
+
44
+ **Credentials this step needs:** none. That's the point — you can start immediately.
45
+
46
+ To upgrade to a real team identity later: ask Fawzi for your `QS-NAME-##` code, then re-run `npx qualia-framework install` and enter it. That flips ERP reporting back on (you'll also need the ERP API key — see step 6).
47
+
48
+ ### Optional: shared team knowledge mirror
49
+
50
+ The installer can pull a **read-only** copy of the team's shared knowledge wiki (`qualia-memory`) into `~/.claude/knowledge/shared/` so you start with the team's accumulated patterns and fixes. It is **opt-in and copy-on-install only** — not a live sync.
51
+
52
+ ```bash
53
+ # Local checkout of the knowledge repo:
54
+ QUALIA_KNOWLEDGE_SOURCE=/path/to/qualia-memory npx qualia-framework@latest install
55
+
56
+ # Or a git URL (ask Fawzi for access if the repo is private):
57
+ QUALIA_KNOWLEDGE_SOURCE=https://github.com/Qualiasolutions/qualia-memory.git \
58
+ npx qualia-framework@latest install
59
+ ```
60
+
61
+ - `QUALIA_KNOWLEDGE_SOURCE` — a local path **or** a git URL. Unset → the pull is skipped silently.
62
+ - `QUALIA_KNOWLEDGE_SUBPATH` — subdirectory inside the source to copy (default `wiki/_export`).
63
+ - The mirror lands at `~/.claude/knowledge/shared/` with every file marked read-only. Your own learnings still go through `/qualia-learn` into your personal knowledge files — the shared mirror is reference material, refreshed on each install/update.
64
+ - If the source is unreachable (no git, offline, missing path, private-repo auth fail) the installer **warns and continues** — it never blocks the install.
65
+
66
+ **Credentials this step needs:** read access to the `qualia-memory` repo if you use a private git URL — ask Fawzi.
67
+
68
+ ---
69
+
70
+ ## 2. `/qualia-doctor` — confirm the install is healthy
71
+
72
+ ```
73
+ /qualia-doctor
74
+ ```
75
+
76
+ Checks install integrity, hooks, project state, contracts, memory, and the ERP queue. In employee mode it will report ERP as disabled — that is expected and not an error. Fix anything it flags before starting real work.
77
+
78
+ **Credentials this step needs:** none.
79
+
80
+ ---
81
+
82
+ ## 3. `/qualia-new` — start a project (pick a preset)
83
+
84
+ ```
85
+ /qualia-new
86
+ ```
87
+
88
+ Runs the kickoff interview, researches the domain, and writes the planning substrate (`JOURNEY.md`, `REQUIREMENTS.md`, `ROADMAP.md`, `CONTEXT.md`). When prompted, pick the project **preset / type** that matches what you're building (e.g. landing page, full app, AI agent, internal tool). The preset seeds the milestone arc so you're not planning from a blank page.
89
+
90
+ **Credentials this step may need:**
91
+ - `OPENROUTER_API_KEY` — for any AI-assisted research/generation. Ask Fawzi for one.
92
+ - Provider keys (Supabase, Vercel, etc.) are not needed yet — they come in when you wire those services.
93
+
94
+ ---
95
+
96
+ ## 4. Build / verify loop
97
+
98
+ Plan, build, and verify each phase:
99
+
100
+ ```
101
+ /qualia-plan # break the current phase into wave-grouped tasks
102
+ /qualia-build # execute the plan — builders + atomic commits
103
+ /qualia-verify # goal-backward check against acceptance criteria
104
+ ```
105
+
106
+ Iterate until the phase passes verification. Use `/qualia` at any point to ask "what's my next step?".
107
+
108
+ **Credentials this step needs:** whatever the feature touches —
109
+ - `NEXT_PUBLIC_SUPABASE_URL`, `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY`, `SUPABASE_SERVICE_ROLE_KEY` for database work (service role key is **server-only**, never in client code).
110
+ - `OPENROUTER_API_KEY` for AI calls.
111
+ - Voice keys (`RETELL_API_KEY`, `ELEVENLABS_API_KEY`, `TELNYX_API_KEY`) for voice work.
112
+
113
+ Pull these into a project with `vercel env pull` once the project is linked. **Ask Fawzi for any key you don't have** — do not invent or hardcode keys, and never commit a `.env` file.
114
+
115
+ ---
116
+
117
+ ## 5. `/qualia-ship` — deploy
118
+
119
+ ```
120
+ /qualia-ship
121
+ ```
122
+
123
+ Runs the quality gates, commits, deploys (Vercel via CLI), and verifies. As an employee you ship through a **feature branch and review** — `branch-guard` blocks direct pushes to `main`. Deploys happen only through the CLI; GitHub auto-deploy is intentionally disabled.
124
+
125
+ **Credentials this step needs:**
126
+ - A Vercel login on the correct team (`vercel whoami`; `vercel link` if the project isn't linked). Ask Fawzi which Vercel team the project belongs to.
127
+ - Push access to the GitHub repo (org `QualiasolutionsCY` or `SakaniQualia`). Ask Fawzi to be added.
128
+
129
+ ---
130
+
131
+ ## 6. Report — clock out
132
+
133
+ ```
134
+ /qualia-report
135
+ ```
136
+
137
+ Generates your shift report, commits it to `.planning/reports/report-{date}.md`, and (when configured) uploads it to the ERP.
138
+
139
+ **In employee mode (no team code), the report degrades gracefully:** it is generated and committed **locally**, and you'll see a clear message that it was saved locally because no team code is set. Nothing fails. To start uploading reports to the ERP:
140
+
141
+ 1. Get your team code (`QS-NAME-##`) from Fawzi and re-run `npx qualia-framework install` with it, **and**
142
+ 2. Set the ERP API key (ask Fawzi for it):
143
+ ```bash
144
+ printf '%s' "$QUALIA_ERP_KEY" | qualia-framework set-erp-key
145
+ ```
146
+ Verify with `qualia-framework erp-ping`.
147
+
148
+ After that, `/qualia-report` uploads automatically and retries on transient ERP outages.
149
+
150
+ **Credentials this step needs:** a team code + the ERP API key — both issued by Fawzi. Until then, local reports are the expected behavior.
151
+
152
+ ---
153
+
154
+ ## Quick reference
155
+
156
+ | Need | Command | Issued by |
157
+ |---|---|---|
158
+ | Install with no code | type `EMPLOYEE` at the prompt | — |
159
+ | Team code | re-run install, enter `QS-NAME-##` | Fawzi |
160
+ | ERP API key | `qualia-framework set-erp-key` (piped) | Fawzi |
161
+ | AI model access | `OPENROUTER_API_KEY` | Fawzi |
162
+ | Supabase / Vercel / Voice keys | `vercel env pull` once linked | Fawzi |
163
+ | Shared knowledge mirror | `QUALIA_KNOWLEDGE_SOURCE=... install` | repo access from Fawzi |
164
+ | "What's my next step?" | `/qualia` | — |