qualia-framework 6.8.1 → 6.9.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 (77) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/bin/install.js +159 -9
  3. package/bin/state.js +70 -5
  4. package/docs/EMPLOYEE-QUICKSTART.md +162 -0
  5. package/package.json +2 -1
  6. package/skills/qualia-doctor/SKILL.md +62 -0
  7. package/skills/qualia-new/REFERENCE.md +7 -0
  8. package/skills/qualia-new/SKILL.md +42 -0
  9. package/skills/qualia-report/SKILL.md +13 -0
  10. package/templates/stacks/README.md +110 -0
  11. package/templates/stacks/ai-agent/env.required.json +44 -0
  12. package/templates/stacks/ai-agent/phases.md +53 -0
  13. package/templates/stacks/ai-agent/scaffold/.env.example +12 -0
  14. package/templates/stacks/ai-agent/scaffold/README.md +31 -0
  15. package/templates/stacks/ai-agent/scaffold/app/api/chat/route.ts +33 -0
  16. package/templates/stacks/ai-agent/scaffold/app/globals.css +13 -0
  17. package/templates/stacks/ai-agent/scaffold/app/layout.tsx +19 -0
  18. package/templates/stacks/ai-agent/scaffold/app/page.tsx +12 -0
  19. package/templates/stacks/ai-agent/scaffold/evals/cases.json +23 -0
  20. package/templates/stacks/ai-agent/scaffold/lib/openrouter/client.ts +51 -0
  21. package/templates/stacks/ai-agent/scaffold/lib/prompts/system.ts +6 -0
  22. package/templates/stacks/ai-agent/scaffold/lib/supabase/client.ts +10 -0
  23. package/templates/stacks/ai-agent/scaffold/lib/supabase/server.ts +28 -0
  24. package/templates/stacks/ai-agent/scaffold/next.config.mjs +7 -0
  25. package/templates/stacks/ai-agent/scaffold/package.json +29 -0
  26. package/templates/stacks/ai-agent/scaffold/postcss.config.mjs +7 -0
  27. package/templates/stacks/ai-agent/scaffold/supabase/migrations/0001_init.sql +41 -0
  28. package/templates/stacks/ai-agent/scaffold/tsconfig.json +21 -0
  29. package/templates/stacks/ai-agent/stack.json +9 -0
  30. package/templates/stacks/ai-agent/verify-checklist.md +31 -0
  31. package/templates/stacks/full-app/env.required.json +20 -0
  32. package/templates/stacks/full-app/phases.md +45 -0
  33. package/templates/stacks/full-app/scaffold/.env.example +7 -0
  34. package/templates/stacks/full-app/scaffold/README.md +28 -0
  35. package/templates/stacks/full-app/scaffold/app/globals.css +14 -0
  36. package/templates/stacks/full-app/scaffold/app/layout.tsx +19 -0
  37. package/templates/stacks/full-app/scaffold/app/page.tsx +20 -0
  38. package/templates/stacks/full-app/scaffold/lib/supabase/client.ts +10 -0
  39. package/templates/stacks/full-app/scaffold/lib/supabase/server.ts +31 -0
  40. package/templates/stacks/full-app/scaffold/next.config.mjs +7 -0
  41. package/templates/stacks/full-app/scaffold/package.json +29 -0
  42. package/templates/stacks/full-app/scaffold/postcss.config.mjs +7 -0
  43. package/templates/stacks/full-app/scaffold/supabase/migrations/0001_init.sql +27 -0
  44. package/templates/stacks/full-app/scaffold/tsconfig.json +21 -0
  45. package/templates/stacks/full-app/stack.json +9 -0
  46. package/templates/stacks/full-app/verify-checklist.md +32 -0
  47. package/templates/stacks/internal-tool/env.required.json +20 -0
  48. package/templates/stacks/internal-tool/phases.md +45 -0
  49. package/templates/stacks/internal-tool/scaffold/.env.example +7 -0
  50. package/templates/stacks/internal-tool/scaffold/README.md +29 -0
  51. package/templates/stacks/internal-tool/scaffold/app/globals.css +13 -0
  52. package/templates/stacks/internal-tool/scaffold/app/layout.tsx +20 -0
  53. package/templates/stacks/internal-tool/scaffold/app/page.tsx +22 -0
  54. package/templates/stacks/internal-tool/scaffold/lib/supabase/client.ts +9 -0
  55. package/templates/stacks/internal-tool/scaffold/lib/supabase/server.ts +28 -0
  56. package/templates/stacks/internal-tool/scaffold/next.config.mjs +6 -0
  57. package/templates/stacks/internal-tool/scaffold/package.json +29 -0
  58. package/templates/stacks/internal-tool/scaffold/postcss.config.mjs +7 -0
  59. package/templates/stacks/internal-tool/scaffold/supabase/migrations/0001_init.sql +28 -0
  60. package/templates/stacks/internal-tool/scaffold/tsconfig.json +21 -0
  61. package/templates/stacks/internal-tool/stack.json +9 -0
  62. package/templates/stacks/internal-tool/verify-checklist.md +31 -0
  63. package/templates/stacks/landing-page/env.required.json +8 -0
  64. package/templates/stacks/landing-page/phases.md +42 -0
  65. package/templates/stacks/landing-page/scaffold/.env.example +3 -0
  66. package/templates/stacks/landing-page/scaffold/README.md +25 -0
  67. package/templates/stacks/landing-page/scaffold/app/globals.css +14 -0
  68. package/templates/stacks/landing-page/scaffold/app/layout.tsx +19 -0
  69. package/templates/stacks/landing-page/scaffold/app/page.tsx +21 -0
  70. package/templates/stacks/landing-page/scaffold/next.config.mjs +7 -0
  71. package/templates/stacks/landing-page/scaffold/package.json +26 -0
  72. package/templates/stacks/landing-page/scaffold/postcss.config.mjs +7 -0
  73. package/templates/stacks/landing-page/scaffold/tsconfig.json +21 -0
  74. package/templates/stacks/landing-page/stack.json +9 -0
  75. package/templates/stacks/landing-page/verify-checklist.md +28 -0
  76. package/tests/bin.test.sh +3 -3
  77. package/tests/state.test.sh +83 -0
package/CHANGELOG.md CHANGED
@@ -8,6 +8,28 @@ 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.0] - 2026-06-13 (employee on-ramps — stack presets, employee-mode install, shared knowledge pull)
12
+
13
+ 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.
14
+
15
+ ### Added — project-type stack presets (`templates/stacks/`)
16
+ - **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.
17
+
18
+ ### Changed — `/qualia-new` instantiates a preset
19
+ - 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.
20
+
21
+ ### Changed — `/qualia-doctor` validates the preset's env + CLI auth
22
+ - 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.
23
+
24
+ ### Added — two-mode install (OWNER / EMPLOYEE)
25
+ - `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.
26
+
27
+ ### Added — shared knowledge pull (Part C bridge)
28
+ - 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`).
29
+
30
+ ### Fixed — test drift
31
+ - `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.
32
+
11
33
  ## [6.8.1] - 2026-06-10 (installer hygiene — global hook sweep, bin purge, bak routing)
12
34
 
13
35
  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,15 +1125,19 @@ 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",
1002
1143
  },
@@ -1297,7 +1438,7 @@ Client-specific preferences, design choices, and requirements. Loaded by \`/qual
1297
1438
 
1298
1439
  // ─── Codex (optional second target) ──────────────────────
1299
1440
  if (installCodexTarget) {
1300
- await installCodex(member, target);
1441
+ await installCodex(member, target, employeeMode);
1301
1442
  }
1302
1443
 
1303
1444
  // ─── Summary ───────────────────────────────────────────
@@ -1391,7 +1532,7 @@ function printSummary({ member, target, claudeInstalled }) {
1391
1532
  // Claude install. The only thing intentionally not claimed is a Claude-style
1392
1533
  // persistent statusLine setting; Codex exposes hook status messages today, not
1393
1534
  // an equivalent global status-line command.
1394
- async function installCodex(member, target) {
1535
+ async function installCodex(member, target, employeeMode = false) {
1395
1536
  console.log("");
1396
1537
  console.log(` ${TEAL}▸${RESET} ${WHITE}${BOLD}Codex${RESET}`);
1397
1538
  console.log(` ${DIM2}${"─".repeat(40)}${RESET}`);
@@ -1480,7 +1621,9 @@ async function installCodex(member, target) {
1480
1621
  version: require("../package.json").version,
1481
1622
  installed_at: new Date().toISOString().split("T")[0],
1482
1623
  erp: {
1483
- enabled: true,
1624
+ // Employee mode (no team code) leaves ERP off so /qualia-report
1625
+ // degrades to a local-only report instead of failing.
1626
+ enabled: !employeeMode,
1484
1627
  url: "https://portal.qualiasolutions.net",
1485
1628
  api_key_file: ".erp-api-key",
1486
1629
  },
@@ -1604,6 +1747,13 @@ async function installCodex(member, target) {
1604
1747
  if (!fs.existsSync(out)) copyTextTransform(path.join(knowledgeSrc, file), out, codexText);
1605
1748
  }
1606
1749
  }
1750
+ // Shared knowledge mirror (read-only) — same copy-on-install semantics as
1751
+ // the Claude path. Skips gracefully when QUALIA_KNOWLEDGE_SOURCE is unset.
1752
+ {
1753
+ const pull = pullSharedKnowledge(knowledgeDest);
1754
+ if (pull.status === "copied") ok(`shared knowledge — ${pull.detail}`);
1755
+ else log(`${DIM}shared knowledge — ${pull.detail}${RESET}`);
1756
+ }
1607
1757
  copyTextTransform(path.join(FRAMEWORK_DIR, "guide.md"), path.join(CODEX_DIR, "qualia-guide.md"), codexText);
1608
1758
  ok("templates/ + knowledge/ + references/ + guide");
1609
1759
  } catch (e) {
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,162 @@
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
+ 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.
6
+
7
+ ---
8
+
9
+ ## The path
10
+
11
+ ```
12
+ install (employee mode) → /qualia-doctor → /qualia-new (pick preset) → build/verify loop → /qualia-ship → report
13
+ ```
14
+
15
+ ---
16
+
17
+ ## 1. Install — employee mode (no team code needed)
18
+
19
+ ```bash
20
+ npx qualia-framework@latest install
21
+ ```
22
+
23
+ At the prompt:
24
+
25
+ ```
26
+ Install code or "EMPLOYEE":
27
+ ```
28
+
29
+ - Have a team code (`QS-NAME-##`)? Enter it — you install as that team member.
30
+ - **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.
31
+
32
+ What employee mode changes vs. a coded install:
33
+
34
+ | | Coded install (team member) | Employee mode (no code) |
35
+ |---|---|---|
36
+ | Role | OWNER or EMPLOYEE per code | EMPLOYEE |
37
+ | Skills / agents / hooks | full | full |
38
+ | Push to `main` | OWNER only | blocked |
39
+ | ERP reporting | on (with API key) | **off** until a code/key is set |
40
+ | `/qualia-report` | uploads to ERP | saves a **local** report file |
41
+
42
+ **Credentials this step needs:** none. That's the point — you can start immediately.
43
+
44
+ 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).
45
+
46
+ ### Optional: shared team knowledge mirror
47
+
48
+ 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.
49
+
50
+ ```bash
51
+ # Local checkout of the knowledge repo:
52
+ QUALIA_KNOWLEDGE_SOURCE=/path/to/qualia-memory npx qualia-framework@latest install
53
+
54
+ # Or a git URL (ask Fawzi for access if the repo is private):
55
+ QUALIA_KNOWLEDGE_SOURCE=https://github.com/Qualiasolutions/qualia-memory.git \
56
+ npx qualia-framework@latest install
57
+ ```
58
+
59
+ - `QUALIA_KNOWLEDGE_SOURCE` — a local path **or** a git URL. Unset → the pull is skipped silently.
60
+ - `QUALIA_KNOWLEDGE_SUBPATH` — subdirectory inside the source to copy (default `wiki/_export`).
61
+ - 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.
62
+ - If the source is unreachable (no git, offline, missing path, private-repo auth fail) the installer **warns and continues** — it never blocks the install.
63
+
64
+ **Credentials this step needs:** read access to the `qualia-memory` repo if you use a private git URL — ask Fawzi.
65
+
66
+ ---
67
+
68
+ ## 2. `/qualia-doctor` — confirm the install is healthy
69
+
70
+ ```
71
+ /qualia-doctor
72
+ ```
73
+
74
+ 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.
75
+
76
+ **Credentials this step needs:** none.
77
+
78
+ ---
79
+
80
+ ## 3. `/qualia-new` — start a project (pick a preset)
81
+
82
+ ```
83
+ /qualia-new
84
+ ```
85
+
86
+ 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.
87
+
88
+ **Credentials this step may need:**
89
+ - `OPENROUTER_API_KEY` — for any AI-assisted research/generation. Ask Fawzi for one.
90
+ - Provider keys (Supabase, Vercel, etc.) are not needed yet — they come in when you wire those services.
91
+
92
+ ---
93
+
94
+ ## 4. Build / verify loop
95
+
96
+ Plan, build, and verify each phase:
97
+
98
+ ```
99
+ /qualia-plan # break the current phase into wave-grouped tasks
100
+ /qualia-build # execute the plan — builders + atomic commits
101
+ /qualia-verify # goal-backward check against acceptance criteria
102
+ ```
103
+
104
+ Iterate until the phase passes verification. Use `/qualia` at any point to ask "what's my next step?".
105
+
106
+ **Credentials this step needs:** whatever the feature touches —
107
+ - `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).
108
+ - `OPENROUTER_API_KEY` for AI calls.
109
+ - Voice keys (`RETELL_API_KEY`, `ELEVENLABS_API_KEY`, `TELNYX_API_KEY`) for voice work.
110
+
111
+ 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.
112
+
113
+ ---
114
+
115
+ ## 5. `/qualia-ship` — deploy
116
+
117
+ ```
118
+ /qualia-ship
119
+ ```
120
+
121
+ 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.
122
+
123
+ **Credentials this step needs:**
124
+ - 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.
125
+ - Push access to the GitHub repo (org `QualiasolutionsCY` or `SakaniQualia`). Ask Fawzi to be added.
126
+
127
+ ---
128
+
129
+ ## 6. Report — clock out
130
+
131
+ ```
132
+ /qualia-report
133
+ ```
134
+
135
+ Generates your shift report, commits it to `.planning/reports/report-{date}.md`, and (when configured) uploads it to the ERP.
136
+
137
+ **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:
138
+
139
+ 1. Get your team code (`QS-NAME-##`) from Fawzi and re-run `npx qualia-framework install` with it, **and**
140
+ 2. Set the ERP API key (ask Fawzi for it):
141
+ ```bash
142
+ printf '%s' "$QUALIA_ERP_KEY" | qualia-framework set-erp-key
143
+ ```
144
+ Verify with `qualia-framework erp-ping`.
145
+
146
+ After that, `/qualia-report` uploads automatically and retries on transient ERP outages.
147
+
148
+ **Credentials this step needs:** a team code + the ERP API key — both issued by Fawzi. Until then, local reports are the expected behavior.
149
+
150
+ ---
151
+
152
+ ## Quick reference
153
+
154
+ | Need | Command | Issued by |
155
+ |---|---|---|
156
+ | Install with no code | type `EMPLOYEE` at the prompt | — |
157
+ | Team code | re-run install, enter `QS-NAME-##` | Fawzi |
158
+ | ERP API key | `qualia-framework set-erp-key` (piped) | Fawzi |
159
+ | AI model access | `OPENROUTER_API_KEY` | Fawzi |
160
+ | Supabase / Vercel / Voice keys | `vercel env pull` once linked | Fawzi |
161
+ | Shared knowledge mirror | `QUALIA_KNOWLEDGE_SOURCE=... install` | repo access from Fawzi |
162
+ | "What's my next step?" | `/qualia` | — |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qualia-framework",
3
- "version": "6.8.1",
3
+ "version": "6.9.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"
@@ -52,6 +52,7 @@
52
52
  "docs/release.md",
53
53
  "docs/changelog-v6.html",
54
54
  "docs/onboarding.html",
55
+ "docs/EMPLOYEE-QUICKSTART.md",
55
56
  "CLAUDE.md",
56
57
  "AGENTS.md",
57
58
  "guide.md",