qualia-framework 4.5.0 → 5.1.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 (64) hide show
  1. package/AGENTS.md +24 -0
  2. package/CLAUDE.md +12 -75
  3. package/README.md +23 -16
  4. package/agents/builder.md +9 -21
  5. package/agents/planner.md +8 -0
  6. package/agents/verifier.md +8 -0
  7. package/agents/visual-evaluator.md +132 -0
  8. package/bin/cli.js +54 -18
  9. package/bin/install.js +369 -29
  10. package/bin/qualia-ui.js +208 -1
  11. package/bin/slop-detect.mjs +5 -0
  12. package/bin/state.js +34 -1
  13. package/docs/install-redesign-builder-prompt.md +290 -0
  14. package/docs/install-redesign-pilot.md +234 -0
  15. package/docs/playwright-loop-builder-prompt.md +185 -0
  16. package/docs/playwright-loop-design-notes.md +108 -0
  17. package/docs/playwright-loop-pilot-results.md +170 -0
  18. package/docs/playwright-loop-review-2026-05-03.md +65 -0
  19. package/docs/playwright-loop-tester-prompt.md +213 -0
  20. package/docs/reviews/matt-pocock-skills-analysis.md +300 -0
  21. package/guide.md +9 -5
  22. package/hooks/env-empty-guard.js +74 -0
  23. package/hooks/pre-compact.js +19 -9
  24. package/hooks/pre-deploy-gate.js +8 -2
  25. package/hooks/pre-push.js +26 -12
  26. package/hooks/supabase-destructive-guard.js +62 -0
  27. package/hooks/vercel-account-guard.js +91 -0
  28. package/package.json +2 -1
  29. package/rules/design-brand.md +4 -0
  30. package/rules/design-laws.md +4 -0
  31. package/rules/design-product.md +4 -0
  32. package/rules/design-rubric.md +4 -0
  33. package/rules/grounding.md +4 -0
  34. package/skills/qualia-build/SKILL.md +40 -46
  35. package/skills/qualia-discuss/SKILL.md +51 -68
  36. package/skills/qualia-handoff/SKILL.md +1 -0
  37. package/skills/qualia-issues/SKILL.md +151 -0
  38. package/skills/qualia-map/SKILL.md +78 -35
  39. package/skills/qualia-new/REFERENCE.md +139 -0
  40. package/skills/qualia-new/SKILL.md +45 -121
  41. package/skills/qualia-optimize/REFERENCE.md +202 -0
  42. package/skills/qualia-optimize/SKILL.md +72 -237
  43. package/skills/qualia-plan/SKILL.md +58 -65
  44. package/skills/qualia-polish-loop/REFERENCE.md +265 -0
  45. package/skills/qualia-polish-loop/SKILL.md +201 -0
  46. package/skills/qualia-polish-loop/fixtures/broken.html +117 -0
  47. package/skills/qualia-polish-loop/fixtures/clean.html +196 -0
  48. package/skills/qualia-polish-loop/scripts/loop.mjs +302 -0
  49. package/skills/qualia-polish-loop/scripts/playwright-capture.mjs +197 -0
  50. package/skills/qualia-polish-loop/scripts/score.mjs +176 -0
  51. package/skills/qualia-report/SKILL.md +141 -200
  52. package/skills/qualia-research/SKILL.md +28 -33
  53. package/skills/qualia-road/SKILL.md +103 -0
  54. package/skills/qualia-ship/SKILL.md +1 -0
  55. package/skills/qualia-task/SKILL.md +1 -1
  56. package/skills/qualia-test/SKILL.md +50 -2
  57. package/skills/qualia-triage/SKILL.md +152 -0
  58. package/skills/qualia-verify/SKILL.md +63 -104
  59. package/skills/qualia-zoom/SKILL.md +51 -0
  60. package/skills/zoho-workflow/SKILL.md +1 -1
  61. package/templates/CONTEXT.md +36 -0
  62. package/templates/decisions/ADR-template.md +30 -0
  63. package/tests/bin.test.sh +451 -7
  64. package/tests/state.test.sh +58 -0
package/bin/install.js CHANGED
@@ -3,10 +3,12 @@
3
3
  const { createInterface } = require("readline");
4
4
  const path = require("path");
5
5
  const fs = require("fs");
6
+ const ui = require("./qualia-ui.js");
6
7
 
7
- // ─── Colors ──────────────────────────────────────────────
8
+ // ─── Colors (kept for legacy log lines; new sections route through qualia-ui) ─
8
9
  const TEAL = "\x1b[38;2;0;206;209m";
9
10
  const DIM = "\x1b[38;2;80;90;100m";
11
+ const DIM2 = "\x1b[38;2;70;80;90m";
10
12
  const GREEN = "\x1b[38;2;52;211;153m";
11
13
  const WHITE = "\x1b[38;2;220;225;230m";
12
14
  const YELLOW = "\x1b[38;2;234;179;8m";
@@ -14,8 +16,17 @@ const RED = "\x1b[38;2;239;68;68m";
14
16
  const RESET = "\x1b[0m";
15
17
 
16
18
  const CLAUDE_DIR = path.join(require("os").homedir(), ".claude");
19
+ const CODEX_DIR = path.join(require("os").homedir(), ".codex");
17
20
  const FRAMEWORK_DIR = path.resolve(__dirname, "..");
18
21
 
22
+ // Target IDs match the menu numbers shown to the user.
23
+ const TARGET_CLAUDE_ONLY = "1";
24
+ const TARGET_CODEX_ONLY = "2";
25
+ const TARGET_BOTH = "3";
26
+
27
+ // Total install timer — set in main(), read by the final summary card.
28
+ const installStart = Date.now();
29
+
19
30
  // ─── Team codes ──────────────────────────────────────────
20
31
  const DEFAULT_TEAM = {
21
32
  "QS-FAWZI-01": {
@@ -64,16 +75,30 @@ const TEAM = loadTeam();
64
75
  let installed = 0;
65
76
  let errors = 0;
66
77
 
78
+ // Per-section timer state. Started by printSection(), read by closeSection().
79
+ let sectionStart = 0;
80
+ let sectionLabel = "";
81
+ let sectionCount = 0;
82
+
67
83
  function log(msg) {
68
84
  console.log(` ${msg}`);
69
85
  }
70
86
  function ok(label) {
71
87
  installed++;
88
+ sectionCount++;
72
89
  log(`${GREEN}✓${RESET} ${label}`);
73
90
  }
74
91
  function warn(label) {
75
92
  errors++;
76
- log(`${YELLOW}✗${RESET} ${label}`);
93
+ log(`${YELLOW}!${RESET} ${label}`);
94
+ }
95
+ // step(text) → handle returned by qualia-ui. Use for slow ops where the
96
+ // "doing → done" lifecycle helps the user trust the install isn't hung.
97
+ function step(text) {
98
+ return ui.step(text);
99
+ }
100
+ function spin(text) {
101
+ return ui.spinner(text);
77
102
  }
78
103
  function copy(src, dest) {
79
104
  const destDir = path.dirname(dest);
@@ -161,23 +186,135 @@ function printHeader() {
161
186
  }
162
187
 
163
188
  function printSection(title) {
189
+ // Close prior section if one was open (shows count + elapsed).
190
+ closeSection();
191
+ sectionStart = Date.now();
192
+ sectionLabel = title;
193
+ sectionCount = 0;
164
194
  console.log("");
165
195
  console.log(` ${TEAL}▸${RESET} ${WHITE}${BOLD}${title}${RESET}`);
166
- console.log(` ${DIM}${"─".repeat(40)}${RESET}`);
196
+ console.log(` ${DIM2}${"─".repeat(40)}${RESET}`);
197
+ }
198
+
199
+ function closeSection() {
200
+ if (!sectionLabel) return;
201
+ const elapsedMs = Date.now() - sectionStart;
202
+ const elapsed = elapsedMs >= 1000
203
+ ? `${(elapsedMs / 1000).toFixed(1)}s`
204
+ : `${elapsedMs}ms`;
205
+ if (sectionCount > 0) {
206
+ console.log(` ${DIM2}└─${RESET} ${DIM}${sectionCount} ${sectionLabel.toLowerCase()} · ${elapsed}${RESET}`);
207
+ }
208
+ sectionLabel = "";
209
+ sectionCount = 0;
210
+ }
211
+
212
+ // ─── Prompt helpers ──────────────────────────────────────
213
+ // Two prompts (team code, install target) need to coexist with two stdin
214
+ // modes: interactive TTY and piped. The earlier two-readline approach
215
+ // raced 'close' against the second question on piped installs (`echo CODE
216
+ // | install.js` would EOF before the second question could attach a line
217
+ // listener) — the target prompt always saw "" and defaulted to "1" even
218
+ // when the user piped "CODE\n2\n".
219
+ //
220
+ // Fix: in piped mode, pre-buffer all stdin lines synchronously up front and
221
+ // hand them out one by one. In TTY mode, use a shared readline.
222
+ let SHARED_RL = null;
223
+ let PIPED_LINES = null;
224
+ let PIPED_INDEX = 0;
225
+ const IS_INTERACTIVE = !!(process.stdin && process.stdin.isTTY);
226
+
227
+ function getRl() {
228
+ if (!SHARED_RL) {
229
+ SHARED_RL = createInterface({ input: process.stdin, output: process.stdout });
230
+ }
231
+ return SHARED_RL;
232
+ }
233
+ function closeRl() {
234
+ if (SHARED_RL) { try { SHARED_RL.close(); } catch {} SHARED_RL = null; }
235
+ }
236
+
237
+ // Read every available stdin line into an array. Resolves immediately on
238
+ // 'end'. Used only when stdin is piped (legacy `echo ... | install`).
239
+ function bufferStdin() {
240
+ return new Promise((resolve) => {
241
+ let buf = "";
242
+ process.stdin.setEncoding("utf8");
243
+ process.stdin.on("data", (chunk) => { buf += chunk; });
244
+ process.stdin.on("end", () => {
245
+ const lines = buf.split(/\r?\n/);
246
+ // Trim a trailing empty entry from the final newline, but preserve
247
+ // intentional empties (so an empty target line still defaults to "1"
248
+ // rather than swallowing the team-code line).
249
+ if (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
250
+ resolve(lines);
251
+ });
252
+ });
253
+ }
254
+
255
+ function nextPipedLine() {
256
+ if (!PIPED_LINES) return "";
257
+ if (PIPED_INDEX >= PIPED_LINES.length) return "";
258
+ return PIPED_LINES[PIPED_INDEX++];
167
259
  }
168
260
 
169
- // ─── Prompt for code ─────────────────────────────────────
170
261
  function askCode() {
171
262
  return new Promise((resolve) => {
172
- const rl = createInterface({ input: process.stdin, output: process.stdout });
263
+ if (!IS_INTERACTIVE) {
264
+ printHeader();
265
+ const line = nextPipedLine();
266
+ // Echo the prompt + answer for log readability.
267
+ process.stdout.write(` ${WHITE}Enter install code:${RESET} ${line}\n`);
268
+ resolve(String(line || "").trim());
269
+ return;
270
+ }
271
+ const rl = getRl();
173
272
  printHeader();
174
273
  rl.question(` ${WHITE}Enter install code:${RESET} `, (answer) => {
175
- rl.close();
176
- resolve(answer.trim());
274
+ resolve(String(answer || "").trim());
177
275
  });
178
276
  });
179
277
  }
180
278
 
279
+ // ─── Prompt for install target (Claude / Codex / Both) ──
280
+ // Backward-compat: a piped install with only the team code (single stdin
281
+ // line) closes stdin before this prompt; we silently default to "1"
282
+ // (Claude only) so existing scripts keep working untouched.
283
+ function askTarget() {
284
+ return new Promise((resolve) => {
285
+ console.log("");
286
+ console.log(` ${WHITE}Where would you like to install Qualia?${RESET}`);
287
+ console.log("");
288
+ console.log(` ${TEAL}[1]${RESET} ${WHITE}Claude Code only${RESET} ${DIM}— recommended, full feature set${RESET}`);
289
+ console.log(` ${TEAL}[2]${RESET} ${WHITE}OpenAI Codex only${RESET} ${DIM}— AGENTS.md (Codex's open standard)${RESET}`);
290
+ console.log(` ${TEAL}[3]${RESET} ${WHITE}Both${RESET} ${DIM}— max compatibility${RESET}`);
291
+ console.log("");
292
+
293
+ const normalize = (val) => {
294
+ const trimmed = String(val || "").trim();
295
+ // Empty or unrecognized → default to Claude only (preserves legacy
296
+ // single-line piped install: `echo CODE | npx qualia-framework install`).
297
+ if (trimmed === TARGET_CODEX_ONLY || trimmed === TARGET_BOTH) return trimmed;
298
+ return TARGET_CLAUDE_ONLY;
299
+ };
300
+
301
+ if (!IS_INTERACTIVE) {
302
+ const line = nextPipedLine();
303
+ process.stdout.write(` ${WHITE}Choice [1]:${RESET} ${line}\n`);
304
+ resolve(normalize(line));
305
+ return;
306
+ }
307
+ const rl = getRl();
308
+ rl.question(` ${WHITE}Choice [1]:${RESET} `, (answer) => resolve(normalize(answer)));
309
+ });
310
+ }
311
+
312
+ function targetLabel(t) {
313
+ if (t === TARGET_CODEX_ONLY) return "Codex";
314
+ if (t === TARGET_BOTH) return "Claude Code · Codex";
315
+ return "Claude Code";
316
+ }
317
+
181
318
  // ─── Resolve team code (tolerates case + O/0 typo in suffix) ─
182
319
  // Accepts "qs-fawzi-01", "QS-FAWZI-01", "QS-FAWZI-O1" (letter O in the
183
320
  // numeric suffix), and returns the canonical key if found, else null.
@@ -196,6 +333,12 @@ function resolveTeamCode(input) {
196
333
 
197
334
  // ─── Main ────────────────────────────────────────────────
198
335
  async function main() {
336
+ // Piped install: drain stdin once up front. Avoids EOF/'close' racing
337
+ // ahead of the second prompt's 'line' listener (the bug v5.0 didn't have
338
+ // because v5.0 only had one prompt).
339
+ if (!IS_INTERACTIVE) {
340
+ PIPED_LINES = await bufferStdin();
341
+ }
199
342
  const rawCode = await askCode();
200
343
  const code = resolveTeamCode(rawCode);
201
344
  const member = code ? TEAM[code] : null;
@@ -211,7 +354,23 @@ async function main() {
211
354
  console.log("");
212
355
  const roleColor = member.role === "OWNER" ? TEAL : GREEN;
213
356
  console.log(` ${GREEN}✓${RESET} ${WHITE}${BOLD}Welcome, ${member.name}${RESET}`);
214
- console.log(` ${DIM} Role:${RESET} ${roleColor}${member.role}${RESET} ${DIM}·${RESET} ${DIM}Target:${RESET} ${WHITE}${CLAUDE_DIR}${RESET}`);
357
+ console.log(` ${DIM} Role:${RESET} ${roleColor}${member.role}${RESET}`);
358
+
359
+ // ─── Ask install target (Claude / Codex / Both) ────────
360
+ const target = await askTarget();
361
+ closeRl();
362
+ const installClaude = target === TARGET_CLAUDE_ONLY || target === TARGET_BOTH;
363
+ const installCodexTarget = target === TARGET_CODEX_ONLY || target === TARGET_BOTH;
364
+
365
+ console.log("");
366
+ console.log(` ${DIM} Target:${RESET} ${WHITE}${targetLabel(target)}${RESET}`);
367
+
368
+ if (!installClaude) {
369
+ // Codex-only path: skip the entire Claude install block. Jump straight
370
+ // to the Codex installer + final summary.
371
+ await installCodex(member, target);
372
+ return;
373
+ }
215
374
 
216
375
  // ─── Skills ──────────────────────────────────────────
217
376
  const skillsDir = path.join(FRAMEWORK_DIR, "skills");
@@ -226,6 +385,21 @@ async function main() {
226
385
  path.join(skillsDir, skill, "SKILL.md"),
227
386
  path.join(CLAUDE_DIR, "skills", skill, "SKILL.md")
228
387
  );
388
+ // Copy REFERENCE.md if the skill has one (progressive-disclosure pattern)
389
+ const refSrc = path.join(skillsDir, skill, "REFERENCE.md");
390
+ if (fs.existsSync(refSrc)) {
391
+ copy(refSrc, path.join(CLAUDE_DIR, "skills", skill, "REFERENCE.md"));
392
+ }
393
+ // v5.1: Copy scripts/ subfolder if present (e.g. qualia-polish-loop ships
394
+ // playwright-capture.mjs, loop.mjs, score.mjs that the skill invokes at
395
+ // runtime). Recursive — preserves nested files. fixtures/ also copied
396
+ // for self-test scenarios.
397
+ for (const sub of ["scripts", "fixtures"]) {
398
+ const subSrc = path.join(skillsDir, skill, sub);
399
+ if (fs.existsSync(subSrc) && fs.statSync(subSrc).isDirectory()) {
400
+ copyTree(subSrc, path.join(CLAUDE_DIR, "skills", skill, sub));
401
+ }
402
+ }
229
403
  ok(skill);
230
404
  } catch (e) {
231
405
  warn(`${skill} — ${e.message}`);
@@ -383,6 +557,17 @@ async function main() {
383
557
  claudeMd = claudeMd.replace("{{ROLE}}", member.role);
384
558
  claudeMd = claudeMd.replace("{{ROLE_DESCRIPTION}}", member.description);
385
559
  const claudeDest = path.join(CLAUDE_DIR, "CLAUDE.md");
560
+ // v5.0: backup existing CLAUDE.md before overwrite. Users may have added
561
+ // personal instructions; without a backup, re-install silently destroys
562
+ // them. .bak files are harmless and easy to clean up.
563
+ if (fs.existsSync(claudeDest)) {
564
+ const existing = fs.readFileSync(claudeDest, "utf8");
565
+ if (existing !== claudeMd) {
566
+ const ts = new Date().toISOString().replace(/[:.]/g, "-");
567
+ const bak = `${claudeDest}.bak.${ts}`;
568
+ try { fs.copyFileSync(claudeDest, bak); ok(`Backed up existing CLAUDE.md → ${path.basename(bak)}`); } catch {}
569
+ }
570
+ }
386
571
  fs.writeFileSync(claudeDest, claudeMd, "utf8");
387
572
  ok(`Configured as ${member.role}`);
388
573
  } catch (e) {
@@ -687,6 +872,8 @@ Client-specific preferences, design choices, and requirements. Loaded by \`/qual
687
872
  "session-start.js", "auto-update.js", "branch-guard.js", "pre-push.js",
688
873
  "pre-deploy-gate.js", "migration-guard.js", "pre-compact.js",
689
874
  "git-guardrails.js", "stop-session-log.js",
875
+ // v5.0 — insights-driven destructive-op + wrong-account guards
876
+ "vercel-account-guard.js", "env-empty-guard.js", "supabase-destructive-guard.js",
690
877
  ]);
691
878
  const isQualiaHookCmd = (cmd) => {
692
879
  if (typeof cmd !== "string") return false;
@@ -713,6 +900,10 @@ Client-specific preferences, design choices, and requirements. Loaded by \`/qual
713
900
  { type: "command", if: "Bash(git push*)", command: nodeCmd("branch-guard.js"), timeout: 5, statusMessage: "⬢ Checking branch permissions..." },
714
901
  { type: "command", if: "Bash(git push*)", command: nodeCmd("pre-push.js"), timeout: 15, statusMessage: "⬢ Syncing tracking..." },
715
902
  { type: "command", if: "Bash(vercel --prod*)", command: nodeCmd("pre-deploy-gate.js"), timeout: 180, statusMessage: "⬢ Running quality gates..." },
903
+ // v5.0 hooks — insights-driven friction prevention
904
+ { type: "command", if: "Bash(vercel --prod*)|Bash(vercel deploy*)", command: nodeCmd("vercel-account-guard.js"), timeout: 8, statusMessage: "⬢ Verifying Vercel account..." },
905
+ { type: "command", if: "Bash(vercel env*)", command: nodeCmd("env-empty-guard.js"), timeout: 5, statusMessage: "⬢ Checking env value..." },
906
+ { type: "command", if: "Bash(supabase*)|Bash(npx supabase*)", command: nodeCmd("supabase-destructive-guard.js"), timeout: 5, statusMessage: "⬢ Checking Supabase safety..." },
716
907
  ],
717
908
  },
718
909
  {
@@ -773,56 +964,205 @@ Client-specific preferences, design choices, and requirements. Loaded by \`/qual
773
964
  ok("MCP: next-devtools (runtime error visibility for Next.js projects)");
774
965
  }
775
966
 
776
- fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
967
+ // v5.0: backup existing settings.json before overwrite. The merge logic above
968
+ // preserves user fields, but a partial-write or merger bug could destroy MCP
969
+ // configs / custom permissions. Atomic write (tmp + rename) avoids partial
970
+ // writes; the .bak file is the recovery point if the merger ever misbehaves.
971
+ if (fs.existsSync(settingsPath)) {
972
+ const ts = new Date().toISOString().replace(/[:.]/g, "-");
973
+ const bak = `${settingsPath}.bak.${ts}`;
974
+ try { fs.copyFileSync(settingsPath, bak); } catch {}
975
+ }
976
+ const settingsTmp = `${settingsPath}.tmp.${process.pid}`;
977
+ fs.writeFileSync(settingsTmp, JSON.stringify(settings, null, 2));
978
+ fs.renameSync(settingsTmp, settingsPath);
777
979
 
778
- ok("Hooks: session-start, auto-update, branch-guard, pre-push, migration-guard, deploy-gate, pre-compact, git-guardrails, stop-session-log");
980
+ ok("Hooks: session-start, auto-update, branch-guard, pre-push, migration-guard, deploy-gate, pre-compact, git-guardrails, stop-session-log, vercel-account-guard, env-empty-guard, supabase-destructive-guard");
779
981
  ok("Status line + spinner configured");
780
982
  ok("Environment variables + permissions");
781
983
 
984
+ // ─── Codex (optional second target) ──────────────────────
985
+ if (installCodexTarget) {
986
+ await installCodex(member, target);
987
+ }
988
+
782
989
  // ─── Summary ───────────────────────────────────────────
990
+ closeSection();
991
+ printSummary({ member, target, claudeInstalled: true });
992
+ }
993
+
994
+ // ─── Final summary card (shared by Claude / Codex / Both paths) ──
995
+ function printSummary({ member, target, claudeInstalled }) {
996
+ const roleColor = member.role === "OWNER" ? TEAL : GREEN;
997
+ const totalMs = Date.now() - installStart;
998
+ const totalSec = totalMs >= 1000
999
+ ? `${(totalMs / 1000).toFixed(1)}s`
1000
+ : `${totalMs}ms`;
1001
+
783
1002
  console.log("");
784
- console.log(` ${DIM}${RULE}${RESET}`);
1003
+ console.log(` ${DIM2}${RULE}${RESET}`);
785
1004
  console.log(` ${TEAL}${BOLD}⬢ INSTALLED${RESET}`);
786
- console.log(` ${DIM}${RULE}${RESET}`);
1005
+ console.log(` ${DIM2}${RULE}${RESET}`);
787
1006
  console.log("");
788
1007
  console.log(` ${WHITE}${BOLD}${member.name}${RESET} ${DIM}·${RESET} ${roleColor}${member.role}${RESET} ${DIM}·${RESET} ${DIM}v${PKG_VERSION}${RESET}`);
789
1008
  console.log("");
790
- const agentCount = fs.readdirSync(agentsDir).filter(f => f.endsWith('.md')).length;
791
- const hookCount = fs.readdirSync(hooksSource).length;
792
- const ruleCount = fs.readdirSync(rulesDir).length;
793
- const tmplCount = fs.readdirSync(tmplDir).length;
794
- console.log(` ${DIM}Skills${RESET} ${TEAL}${skills.length}${RESET} ${DIM}Agents${RESET} ${TEAL}${agentCount}${RESET} ${DIM}Hooks${RESET} ${TEAL}${hookCount}${RESET}`);
795
- const installedBinDir = path.join(CLAUDE_DIR, "bin");
796
- const scriptCount = fs.existsSync(installedBinDir)
797
- ? fs.readdirSync(installedBinDir).filter(f => f.endsWith('.js')).length
798
- : 0;
799
- console.log(` ${DIM}Rules${RESET} ${TEAL}${ruleCount}${RESET} ${DIM}Scripts${RESET} ${TEAL}${scriptCount}${RESET} ${DIM}Templates${RESET} ${TEAL}${tmplCount}${RESET}`);
1009
+ console.log(` ${DIM}Targets${RESET} ${TEAL}${targetLabel(target)}${RESET}`);
1010
+ console.log(` ${DIM}Time${RESET} ${TEAL}${totalSec}${RESET}`);
1011
+
1012
+ if (claudeInstalled) {
1013
+ const agentsDir = path.join(FRAMEWORK_DIR, "agents");
1014
+ const hooksSource = path.join(FRAMEWORK_DIR, "hooks");
1015
+ const rulesDir = path.join(FRAMEWORK_DIR, "rules");
1016
+ const tmplDir = path.join(FRAMEWORK_DIR, "templates");
1017
+ const skillsDir = path.join(FRAMEWORK_DIR, "skills");
1018
+ const skillCount = fs
1019
+ .readdirSync(skillsDir)
1020
+ .filter((d) => fs.statSync(path.join(skillsDir, d)).isDirectory()).length;
1021
+ const agentCount = fs.readdirSync(agentsDir).filter((f) => f.endsWith(".md")).length;
1022
+ const hookCount = fs.readdirSync(hooksSource).length;
1023
+ const ruleCount = fs.readdirSync(rulesDir).length;
1024
+ const tmplCount = fs.readdirSync(tmplDir).length;
1025
+ const installedBinDir = path.join(CLAUDE_DIR, "bin");
1026
+ const scriptCount = fs.existsSync(installedBinDir)
1027
+ ? fs.readdirSync(installedBinDir).filter((f) => f.endsWith(".js")).length
1028
+ : 0;
1029
+ console.log("");
1030
+ console.log(` ${DIM}Skills${RESET} ${TEAL}${skillCount}${RESET} ${DIM}Agents${RESET} ${TEAL}${agentCount}${RESET} ${DIM}Hooks${RESET} ${TEAL}${hookCount}${RESET}`);
1031
+ console.log(` ${DIM}Rules${RESET} ${TEAL}${ruleCount}${RESET} ${DIM}Scripts${RESET} ${TEAL}${scriptCount}${RESET} ${DIM}Templates${RESET} ${TEAL}${tmplCount}${RESET}`);
1032
+ }
800
1033
 
801
1034
  if (errors > 0) {
802
1035
  console.log("");
803
1036
  console.log(` ${YELLOW}${errors} warning(s)${RESET} — check output above`);
804
1037
  }
805
1038
 
1039
+ // Contextual first-command suggestion: if we're in a Qualia project,
1040
+ // recommend /qualia (router); otherwise /qualia-new (kickoff).
1041
+ const inProject = (() => {
1042
+ try { return fs.existsSync(path.join(process.cwd(), ".planning")); }
1043
+ catch { return false; }
1044
+ })();
1045
+ const firstCmd = inProject ? "/qualia" : "/qualia-new";
1046
+ const firstCmdHint = inProject ? "router — tells you the next command" : "kickoff a new project";
1047
+
806
1048
  console.log("");
807
- console.log(` ${DIM}${RULE}${RESET}`);
1049
+ console.log(` ${DIM2}${RULE}${RESET}`);
808
1050
  console.log(` ${WHITE}${BOLD}Quick Start${RESET}`);
809
- console.log(` ${DIM}${RULE}${RESET}`);
1051
+ console.log(` ${DIM2}${RULE}${RESET}`);
810
1052
  console.log("");
811
- console.log(` ${TEAL}1.${RESET} ${WHITE}Restart Claude Code${RESET} ${DIM}(loads new settings)${RESET}`);
812
- console.log(` ${TEAL}2.${RESET} ${WHITE}cd into any project${RESET} ${DIM}and run${RESET} ${TEAL}claude${RESET}`);
813
- console.log(` ${TEAL}3.${RESET} ${WHITE}Type${RESET} ${TEAL}${BOLD}/qualia${RESET} ${DIM}— it tells you what to do next${RESET}`);
1053
+ if (claudeInstalled) {
1054
+ console.log(` ${TEAL}1.${RESET} ${WHITE}Restart Claude Code${RESET} ${DIM}(loads new settings)${RESET}`);
1055
+ console.log(` ${TEAL}2.${RESET} ${WHITE}cd into any project${RESET} ${DIM}and run${RESET} ${TEAL}claude${RESET}`);
1056
+ console.log(` ${TEAL}3.${RESET} ${WHITE}Try${RESET} ${TEAL}${BOLD}${firstCmd}${RESET} ${DIM}— ${firstCmdHint}${RESET}`);
1057
+ } else {
1058
+ // Codex-only path
1059
+ console.log(` ${TEAL}1.${RESET} ${WHITE}Open Codex in any project${RESET}`);
1060
+ console.log(` ${TEAL}2.${RESET} ${WHITE}Codex picks up${RESET} ${TEAL}~/.codex/AGENTS.md${RESET} ${DIM}automatically${RESET}`);
1061
+ console.log(` ${TEAL}3.${RESET} ${WHITE}Ask Codex${RESET} ${DIM}about Qualia rules — they're in AGENTS.md${RESET}`);
1062
+ }
814
1063
  console.log("");
815
1064
  console.log(` ${DIM}New project?${RESET} ${TEAL}/qualia-new${RESET}`);
816
1065
  console.log(` ${DIM}Quick fix?${RESET} ${TEAL}/qualia-quick${RESET}`);
817
1066
  console.log(` ${DIM}End of day?${RESET} ${TEAL}/qualia-report${RESET} ${DIM}(mandatory)${RESET}`);
818
1067
  console.log(` ${DIM}Stuck?${RESET} ${TEAL}/qualia${RESET}`);
819
1068
  console.log("");
820
- console.log(` ${DIM}${RULE}${RESET}`);
1069
+ console.log(` ${DIM2}${RULE}${RESET}`);
821
1070
  console.log(` ${TEAL}${BOLD}Welcome to the future with Qualia.${RESET}`);
822
- console.log(` ${DIM}${RULE}${RESET}`);
1071
+ console.log(` ${DIM2}${RULE}${RESET}`);
823
1072
  console.log("");
824
1073
  }
825
1074
 
1075
+ // ─── Codex install (writes AGENTS.md to ~/.codex/) ───────
1076
+ // Scope is intentionally minimal: AGENTS.md is the open standard adopted
1077
+ // by Codex / Cursor / Continue / Aider / Devin. Codex's runtime does not
1078
+ // today consume Claude-style skills/agents/hooks on disk in a way the
1079
+ // framework can map 1:1, so we write the convention file and document the
1080
+ // scope honestly. If Codex grows skill/hook support, we extend this here.
1081
+ async function installCodex(member, target) {
1082
+ console.log("");
1083
+ console.log(` ${TEAL}▸${RESET} ${WHITE}${BOLD}Codex${RESET}`);
1084
+ console.log(` ${DIM2}${"─".repeat(40)}${RESET}`);
1085
+
1086
+ // Detect Codex CLI; soft-warn if missing (file write still proceeds).
1087
+ const { spawnSync } = require("child_process");
1088
+ let codexDetected = false;
1089
+ try {
1090
+ const r = spawnSync("codex", ["--version"], {
1091
+ encoding: "utf8",
1092
+ timeout: 3000,
1093
+ stdio: ["ignore", "pipe", "ignore"],
1094
+ });
1095
+ codexDetected = r.status === 0;
1096
+ } catch { codexDetected = false; }
1097
+
1098
+ if (!codexDetected) {
1099
+ console.log(` ${YELLOW}!${RESET} ${WHITE}Codex CLI not detected on this system${RESET}`);
1100
+ console.log(` ${DIM} Installing AGENTS.md to ~/.codex/AGENTS.md anyway — Codex will pick it up${RESET}`);
1101
+ console.log(` ${DIM} when you install via:${RESET} ${TEAL}npm install -g @openai/codex${RESET}`);
1102
+ }
1103
+
1104
+ // Make sure the dir exists.
1105
+ if (!fs.existsSync(CODEX_DIR)) {
1106
+ try {
1107
+ fs.mkdirSync(CODEX_DIR, { recursive: true });
1108
+ } catch (e) {
1109
+ warn(`Codex — could not create ${CODEX_DIR}: ${e.message}`);
1110
+ return;
1111
+ }
1112
+ }
1113
+
1114
+ // Render AGENTS.md with role substitution. Source is the same AGENTS.md
1115
+ // already shipped in the framework root (it's the cross-vendor mirror of
1116
+ // CLAUDE.md, kept under 25 lines per Pocock's instruction-budget rule).
1117
+ let agentsContent;
1118
+ try {
1119
+ agentsContent = fs.readFileSync(path.join(FRAMEWORK_DIR, "AGENTS.md"), "utf8");
1120
+ agentsContent = agentsContent
1121
+ .replace("{{ROLE}}", member.role)
1122
+ .replace("{{ROLE_DESCRIPTION}}", member.description);
1123
+ } catch (e) {
1124
+ warn(`Codex — could not read framework AGENTS.md: ${e.message}`);
1125
+ return;
1126
+ }
1127
+
1128
+ const dest = path.join(CODEX_DIR, "AGENTS.md");
1129
+
1130
+ // Backup if existing differs (matches v5.0 CLAUDE.md / settings.json
1131
+ // discipline — never silently destroy a hand-edited file).
1132
+ if (fs.existsSync(dest)) {
1133
+ try {
1134
+ const existing = fs.readFileSync(dest, "utf8");
1135
+ if (existing !== agentsContent) {
1136
+ const ts = new Date().toISOString().replace(/[:.]/g, "-");
1137
+ const bak = `${dest}.bak.${ts}`;
1138
+ try { fs.copyFileSync(dest, bak); ok(`Backed up existing AGENTS.md → ${path.basename(bak)}`); } catch {}
1139
+ }
1140
+ } catch {}
1141
+ }
1142
+
1143
+ // Atomic write: tmp + rename. Same pattern as settings.json above.
1144
+ try {
1145
+ const tmp = `${dest}.tmp.${process.pid}`;
1146
+ fs.writeFileSync(tmp, agentsContent, "utf8");
1147
+ fs.renameSync(tmp, dest);
1148
+ sectionCount++;
1149
+ ok(`AGENTS.md (configured as ${member.role})`);
1150
+ } catch (e) {
1151
+ warn(`Codex AGENTS.md — ${e.message}`);
1152
+ return;
1153
+ }
1154
+
1155
+ // Honest scope note.
1156
+ console.log(` ${DIM}└─${RESET} ${DIM}Codex install scope: AGENTS.md only — Codex's runtime does not currently${RESET}`);
1157
+ console.log(` ${DIM}consume the framework's skills/hooks/agents on disk. AGENTS.md carries${RESET}`);
1158
+ console.log(` ${DIM}the rules; commands route through Claude Code.${RESET}`);
1159
+
1160
+ // Codex-only path: still need to write the role config and print summary.
1161
+ if (target === TARGET_CODEX_ONLY) {
1162
+ printSummary({ member, target, claudeInstalled: false });
1163
+ }
1164
+ }
1165
+
826
1166
  main().catch((e) => {
827
1167
  console.error(`${RED} ✗ Installation failed: ${e.message}${RESET}`);
828
1168
  process.exit(1);