qualia-framework 6.2.9 → 6.2.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/README.md +14 -11
  2. package/agents/builder.md +7 -7
  3. package/agents/planner.md +39 -3
  4. package/agents/research-synthesizer.md +1 -1
  5. package/agents/researcher.md +3 -3
  6. package/agents/roadmapper.md +7 -7
  7. package/agents/verifier.md +18 -6
  8. package/agents/visual-evaluator.md +8 -7
  9. package/bin/cli.js +111 -14
  10. package/bin/contract-runner.js +219 -0
  11. package/bin/host-adapters.js +66 -0
  12. package/bin/install.js +99 -152
  13. package/bin/plan-contract.js +99 -2
  14. package/bin/planning-hygiene.js +262 -0
  15. package/bin/runtime-manifest.js +32 -0
  16. package/bin/state-ledger.js +184 -0
  17. package/bin/state.js +299 -20
  18. package/bin/trust-score.js +276 -0
  19. package/docs/onboarding.html +5 -4
  20. package/guide.md +3 -2
  21. package/package.json +1 -1
  22. package/qualia-design/design-rubric.md +17 -5
  23. package/qualia-design/frontend.md +5 -1
  24. package/qualia-design/graphics.md +47 -0
  25. package/rules/command-output.md +35 -0
  26. package/skills/qualia/SKILL.md +10 -10
  27. package/skills/qualia-build/SKILL.md +20 -14
  28. package/skills/qualia-debug/SKILL.md +16 -8
  29. package/skills/qualia-discuss/SKILL.md +10 -10
  30. package/skills/qualia-doctor/SKILL.md +140 -0
  31. package/skills/qualia-feature/SKILL.md +23 -21
  32. package/skills/qualia-fix/SKILL.md +216 -0
  33. package/skills/qualia-flush/SKILL.md +9 -9
  34. package/skills/qualia-handoff/SKILL.md +9 -9
  35. package/skills/qualia-help/SKILL.md +3 -3
  36. package/skills/qualia-hook-gen/SKILL.md +1 -1
  37. package/skills/qualia-idk/SKILL.md +4 -4
  38. package/skills/qualia-issues/SKILL.md +2 -2
  39. package/skills/qualia-learn/SKILL.md +10 -10
  40. package/skills/qualia-map/SKILL.md +2 -2
  41. package/skills/qualia-milestone/SKILL.md +15 -15
  42. package/skills/qualia-new/REFERENCE.md +9 -9
  43. package/skills/qualia-new/SKILL.md +14 -14
  44. package/skills/qualia-optimize/REFERENCE.md +1 -1
  45. package/skills/qualia-optimize/SKILL.md +23 -16
  46. package/skills/qualia-pause/SKILL.md +2 -2
  47. package/skills/qualia-plan/SKILL.md +23 -13
  48. package/skills/qualia-polish/REFERENCE.md +14 -14
  49. package/skills/qualia-polish/SKILL.md +64 -19
  50. package/skills/qualia-polish/scripts/loop.mjs +3 -3
  51. package/skills/qualia-polish/scripts/score.mjs +9 -3
  52. package/skills/qualia-postmortem/SKILL.md +9 -9
  53. package/skills/qualia-report/SKILL.md +23 -23
  54. package/skills/qualia-research/SKILL.md +5 -5
  55. package/skills/qualia-resume/SKILL.md +4 -4
  56. package/skills/qualia-review/SKILL.md +28 -12
  57. package/skills/qualia-road/SKILL.md +18 -5
  58. package/skills/qualia-ship/SKILL.md +22 -22
  59. package/skills/qualia-skill-new/SKILL.md +13 -13
  60. package/skills/qualia-test/SKILL.md +5 -5
  61. package/skills/qualia-triage/SKILL.md +1 -1
  62. package/skills/qualia-verify/SKILL.md +37 -23
  63. package/skills/qualia-vibe/SKILL.md +13 -10
  64. package/skills/qualia-vibe/scripts/extract.mjs +1 -1
  65. package/skills/zoho-workflow/SKILL.md +1 -1
  66. package/templates/help.html +12 -10
  67. package/tests/bin.test.sh +34 -4
  68. package/tests/install-smoke.test.sh +22 -2
  69. package/tests/lib.test.sh +290 -0
  70. package/tests/runner.js +3 -0
  71. package/tests/skills.test.sh +4 -4
  72. package/tests/state.test.sh +65 -3
@@ -0,0 +1,219 @@
1
+ #!/usr/bin/env node
2
+ // Execute Qualia phase contract checks and write evidence.
3
+ // No shell interpolation: command checks run through spawnSync(argv).
4
+
5
+ const fs = require("fs");
6
+ const path = require("path");
7
+ const { spawnSync } = require("child_process");
8
+ const pc = require("./plan-contract.js");
9
+
10
+ function parseArgs(argv) {
11
+ const args = { _: [] };
12
+ for (let i = 2; i < argv.length; i++) {
13
+ const a = argv[i];
14
+ if (a === "--json") args.json = true;
15
+ else if (a === "--no-write") args.no_write = true;
16
+ else if (a === "--cwd") args.cwd = argv[++i];
17
+ else if (a.startsWith("--cwd=")) args.cwd = a.slice("--cwd=".length);
18
+ else args._.push(a);
19
+ }
20
+ return args;
21
+ }
22
+
23
+ function usage() {
24
+ console.error([
25
+ "Usage:",
26
+ " contract-runner.js <contract.json> [--cwd DIR] [--json] [--no-write]",
27
+ "",
28
+ "Runs file-exists, grep-match, command-exit, and behavioral evidence checks.",
29
+ ].join("\n"));
30
+ }
31
+
32
+ function rel(root, p) {
33
+ return path.resolve(root, p);
34
+ }
35
+
36
+ function checkFileExists(root, check) {
37
+ const file = rel(root, check.path);
38
+ if (!fs.existsSync(file)) return { ok: false, detail: `missing file: ${check.path}` };
39
+ if (check.must_contain != null) {
40
+ const content = fs.readFileSync(file, "utf8");
41
+ if (!content.includes(check.must_contain)) {
42
+ return { ok: false, detail: `file does not contain required text: ${check.path}` };
43
+ }
44
+ }
45
+ return { ok: true };
46
+ }
47
+
48
+ function checkGrepMatch(root, check) {
49
+ const file = rel(root, check.path);
50
+ if (!fs.existsSync(file)) return { ok: false, detail: `missing file: ${check.path}` };
51
+ const content = fs.readFileSync(file, "utf8");
52
+ const re = new RegExp(check.pattern);
53
+ const present = re.test(content);
54
+ if (check.expect === "present" && !present) return { ok: false, detail: `pattern absent: ${check.pattern}` };
55
+ if (check.expect === "absent" && present) return { ok: false, detail: `pattern present: ${check.pattern}` };
56
+ return { ok: true };
57
+ }
58
+
59
+ function checkCommandExit(root, check) {
60
+ const started = Date.now();
61
+ const r = spawnSync(check.command, check.args || [], {
62
+ cwd: root,
63
+ encoding: "utf8",
64
+ timeout: check.timeout_ms || 30_000,
65
+ stdio: ["ignore", "pipe", "pipe"],
66
+ shell: false,
67
+ });
68
+ const status = typeof r.status === "number" ? r.status : 1;
69
+ if (status !== check.expected_exit) {
70
+ return {
71
+ ok: false,
72
+ detail: `exit ${status}, expected ${check.expected_exit}`,
73
+ duration_ms: Date.now() - started,
74
+ stdout: (r.stdout || "").slice(-1000),
75
+ stderr: (r.stderr || r.error?.message || "").slice(-1000),
76
+ };
77
+ }
78
+ if (check.expect_stdout_match != null) {
79
+ const re = new RegExp(check.expect_stdout_match);
80
+ if (!re.test(r.stdout || "")) {
81
+ return {
82
+ ok: false,
83
+ detail: `stdout did not match: ${check.expect_stdout_match}`,
84
+ duration_ms: Date.now() - started,
85
+ stdout: (r.stdout || "").slice(-1000),
86
+ stderr: (r.stderr || "").slice(-1000),
87
+ };
88
+ }
89
+ }
90
+ return {
91
+ ok: true,
92
+ duration_ms: Date.now() - started,
93
+ stdout: (r.stdout || "").slice(-1000),
94
+ stderr: (r.stderr || "").slice(-1000),
95
+ };
96
+ }
97
+
98
+ function checkBehavioral(root, check) {
99
+ for (const ev of check.evidence_required || []) {
100
+ const file = rel(root, ev.path);
101
+ if (!fs.existsSync(file)) {
102
+ return { ok: false, detail: `missing evidence file: ${ev.path}` };
103
+ }
104
+ if (ev.matcher != null) {
105
+ const content = fs.readFileSync(file, "utf8");
106
+ const re = new RegExp(ev.matcher);
107
+ if (!re.test(content)) {
108
+ return { ok: false, detail: `evidence matcher failed for ${ev.path}: ${ev.matcher}` };
109
+ }
110
+ }
111
+ }
112
+ return { ok: true };
113
+ }
114
+
115
+ function runCheck(root, check) {
116
+ try {
117
+ if (check.type === "file-exists") return checkFileExists(root, check);
118
+ if (check.type === "grep-match") return checkGrepMatch(root, check);
119
+ if (check.type === "command-exit") return checkCommandExit(root, check);
120
+ if (check.type === "behavioral") return checkBehavioral(root, check);
121
+ return { ok: false, detail: `unknown check type: ${check.type}` };
122
+ } catch (e) {
123
+ return { ok: false, detail: e.message };
124
+ }
125
+ }
126
+
127
+ function writeEvidence(root, contract, result) {
128
+ const dir = path.join(root, ".planning", "evidence");
129
+ fs.mkdirSync(dir, { recursive: true });
130
+ const phase = Number(contract.phase || 0) || "unknown";
131
+ const file = path.join(dir, `phase-${phase}-contract-run.json`);
132
+ fs.writeFileSync(file, JSON.stringify(result, null, 2) + "\n");
133
+ return path.relative(root, file);
134
+ }
135
+
136
+ function runContract(contract, opts = {}) {
137
+ const root = path.resolve(opts.cwd || process.cwd());
138
+ const errors = pc.validate(contract);
139
+ if (errors.length > 0) {
140
+ return {
141
+ ok: false,
142
+ error: "CONTRACT_INVALID",
143
+ errors,
144
+ checked: 0,
145
+ failed: errors.length,
146
+ results: [],
147
+ };
148
+ }
149
+
150
+ const results = [];
151
+ for (const task of contract.tasks || []) {
152
+ for (let i = 0; i < (task.verification || []).length; i++) {
153
+ const check = task.verification[i];
154
+ const r = runCheck(root, check);
155
+ results.push({
156
+ task_id: task.id,
157
+ task_title: task.title,
158
+ index: i,
159
+ type: check.type,
160
+ ok: !!r.ok,
161
+ detail: r.detail || "",
162
+ duration_ms: r.duration_ms,
163
+ stdout: r.stdout,
164
+ stderr: r.stderr,
165
+ });
166
+ }
167
+ }
168
+ const failed = results.filter((r) => !r.ok).length;
169
+ const payload = {
170
+ ok: failed === 0,
171
+ phase: contract.phase,
172
+ goal: contract.goal,
173
+ checked: results.length,
174
+ failed,
175
+ generated_at: new Date().toISOString(),
176
+ results,
177
+ };
178
+ if (!opts.no_write) payload.evidence_file = writeEvidence(root, contract, payload);
179
+ return payload;
180
+ }
181
+
182
+ function main(argv) {
183
+ const args = parseArgs(argv);
184
+ const contractPath = args._[0];
185
+ if (!contractPath || contractPath === "--help" || contractPath === "-h") {
186
+ usage();
187
+ return 2;
188
+ }
189
+ const loaded = pc.readContractFile(contractPath);
190
+ if (!loaded.ok) {
191
+ const payload = { ok: false, ...loaded, path: contractPath };
192
+ if (args.json) console.log(JSON.stringify(payload, null, 2));
193
+ else console.error(`${payload.error}: ${payload.message}`);
194
+ return 2;
195
+ }
196
+ const result = runContract(loaded.contract, {
197
+ cwd: args.cwd,
198
+ no_write: args.no_write,
199
+ });
200
+ if (args.json) {
201
+ console.log(JSON.stringify(result, null, 2));
202
+ } else if (result.ok) {
203
+ console.log(`PASS phase ${result.phase}: ${result.checked} check(s)`);
204
+ if (result.evidence_file) console.log(`Evidence: ${result.evidence_file}`);
205
+ } else {
206
+ console.error(`FAIL phase ${result.phase || "?"}: ${result.failed} of ${result.checked || result.failed} check(s) failed`);
207
+ for (const r of result.results || []) {
208
+ if (!r.ok) console.error(`- ${r.task_id} ${r.type}: ${r.detail}`);
209
+ }
210
+ if (result.error) for (const e of result.errors || []) console.error(`- ${e}`);
211
+ }
212
+ return result.ok ? 0 : 1;
213
+ }
214
+
215
+ module.exports = { runContract, runCheck };
216
+
217
+ if (require.main === module) {
218
+ process.exit(main(process.argv));
219
+ }
@@ -0,0 +1,66 @@
1
+ #!/usr/bin/env node
2
+ // Host adapter rendering for installed Qualia text surfaces.
3
+
4
+ const path = require("path");
5
+ const os = require("os");
6
+
7
+ const HOSTS = {
8
+ claude: {
9
+ name: "Claude Code",
10
+ home: path.join(os.homedir(), ".claude"),
11
+ },
12
+ codex: {
13
+ name: "OpenAI Codex",
14
+ home: path.join(os.homedir(), ".codex"),
15
+ },
16
+ };
17
+
18
+ function adapter(name) {
19
+ const host = HOSTS[name];
20
+ if (!host) throw new Error(`Unknown Qualia host adapter: ${name}`);
21
+ const home = host.home;
22
+ return {
23
+ ...host,
24
+ tokens: {
25
+ QUALIA_HOME: home,
26
+ QUALIA_BIN: `${home}/bin`,
27
+ QUALIA_AGENTS: `${home}/agents`,
28
+ QUALIA_SKILLS: `${home}/skills`,
29
+ QUALIA_RULES: `${home}/rules`,
30
+ QUALIA_TEMPLATES: `${home}/qualia-templates`,
31
+ QUALIA_KNOWLEDGE: `${home}/knowledge`,
32
+ QUALIA_REFERENCES: `${home}/qualia-references`,
33
+ QUALIA_DESIGN: `${home}/qualia-design`,
34
+ },
35
+ };
36
+ }
37
+
38
+ function renderText(content, hostName) {
39
+ const host = adapter(hostName);
40
+ let out = String(content);
41
+ for (const [token, value] of Object.entries(host.tokens)) {
42
+ out = out.replaceAll(`\${${token}}`, value);
43
+ }
44
+
45
+ // Backward-compatible rendering while source files migrate from hardcoded
46
+ // Claude paths to explicit ${QUALIA_*} tokens.
47
+ out = out
48
+ .replaceAll("~/.claude/", `${host.home}/`)
49
+ .replaceAll("$HOME/.claude/", `${host.home}/`)
50
+ .replaceAll("${HOME}/.claude/", `${host.home}/`)
51
+ .replaceAll("@~/.claude/", `@${host.home}/`)
52
+ .replaceAll(".claude/", `${path.basename(host.home)}/`);
53
+
54
+ if (hostName === "codex") {
55
+ out = out
56
+ .replaceAll("Claude Code", "Codex")
57
+ .replaceAll("Claude's", "Codex's");
58
+ }
59
+ return out;
60
+ }
61
+
62
+ module.exports = {
63
+ HOSTS,
64
+ adapter,
65
+ renderText,
66
+ };
package/bin/install.js CHANGED
@@ -4,6 +4,8 @@ const { createInterface } = require("readline");
4
4
  const path = require("path");
5
5
  const fs = require("fs");
6
6
  const ui = require("./qualia-ui.js");
7
+ const { RUNTIME_BIN_SCRIPTS, binFiles } = require("./runtime-manifest.js");
8
+ const { renderText } = require("./host-adapters.js");
7
9
 
8
10
  // ─── Colors (kept for legacy log lines; new sections route through qualia-ui) ─
9
11
  const TEAL = "\x1b[38;2;0;206;209m";
@@ -24,6 +26,24 @@ const TARGET_CLAUDE_ONLY = "1";
24
26
  const TARGET_CODEX_ONLY = "2";
25
27
  const TARGET_BOTH = "3";
26
28
 
29
+ const CODEX_STATUS_LINE = [
30
+ "model-with-reasoning",
31
+ "task-progress",
32
+ "current-dir",
33
+ "git-branch",
34
+ "context-used",
35
+ "five-hour-limit",
36
+ "weekly-limit",
37
+ ];
38
+
39
+ const CODEX_STATUS_LINE_BLOCK = [
40
+ "# Added by qualia-framework — Codex native bottom status line.",
41
+ "[tui]",
42
+ `status_line = ${JSON.stringify(CODEX_STATUS_LINE)}`,
43
+ "status_line_use_colors = true",
44
+ "",
45
+ ].join("\n");
46
+
27
47
  // Total install timer — set in main(), read by the final summary card.
28
48
  const installStart = Date.now();
29
49
 
@@ -125,14 +145,8 @@ function copyTree(src, dest) {
125
145
  }
126
146
  }
127
147
 
128
- function codexText(content) {
129
- return String(content)
130
- .replaceAll("~/.claude/", "~/.codex/")
131
- .replaceAll("$HOME/.claude/", "$HOME/.codex/")
132
- .replaceAll("${HOME}/.claude/", "${HOME}/.codex/")
133
- .replaceAll("@~/.claude/", "@~/.codex/")
134
- .replaceAll(".claude/", ".codex/");
135
- }
148
+ const claudeText = (content) => renderText(content, "claude");
149
+ const codexText = (content) => renderText(content, "codex");
136
150
 
137
151
  function copyTextTransform(src, dest, transform) {
138
152
  const destDir = path.dirname(dest);
@@ -156,6 +170,45 @@ function copyTreeTransform(src, dest, transform) {
156
170
  }
157
171
  }
158
172
 
173
+ function ensureCodexStatusLineConfig(existing) {
174
+ const statusLine = `status_line = ${JSON.stringify(CODEX_STATUS_LINE)}`;
175
+ const colors = "status_line_use_colors = true";
176
+ if (!existing || !existing.trim()) {
177
+ return [
178
+ "# Created by qualia-framework install.",
179
+ "# User settings can be added normally; Qualia runtime lives in AGENTS.md, hooks.json, agents/, and bin/.",
180
+ "",
181
+ "[features]",
182
+ "hooks = true",
183
+ "plugin_hooks = true",
184
+ "",
185
+ "# Codex's native status line is rendered at the bottom of the TUI.",
186
+ "# It supports a fixed list of built-in segment names. Custom command-backed",
187
+ "# status lines are not supported in Codex 0.133, so Qualia phase/project",
188
+ "# context is rendered by the SessionStart banner while the native bottom",
189
+ "# line keeps model, task, directory, git, context, and limit state visible.",
190
+ CODEX_STATUS_LINE_BLOCK,
191
+ ].join("\n");
192
+ }
193
+
194
+ let next = existing;
195
+ if (!/^\[tui\]\s*$/m.test(next)) {
196
+ return `${next.replace(/\s*$/, "\n\n")}${CODEX_STATUS_LINE_BLOCK}`;
197
+ }
198
+
199
+ const tuiMatch = next.match(/^\[tui\]\s*$(?:\n(?!\[)[^\n]*)*/m);
200
+ if (!tuiMatch) return `${next.replace(/\s*$/, "\n\n")}${CODEX_STATUS_LINE_BLOCK}`;
201
+
202
+ let tuiBlock = tuiMatch[0];
203
+ if (!/^\s*status_line\s*=/m.test(tuiBlock)) {
204
+ tuiBlock = tuiBlock.replace(/^\[tui\]\s*$/m, `[tui]\n${statusLine}`);
205
+ }
206
+ if (!/^\s*status_line_use_colors\s*=/m.test(tuiBlock)) {
207
+ tuiBlock = `${tuiBlock.replace(/\s*$/, "")}\n${colors}`;
208
+ }
209
+ return `${next.slice(0, tuiMatch.index)}${tuiBlock}${next.slice(tuiMatch.index + tuiMatch[0].length)}`;
210
+ }
211
+
159
212
  function backupIfDifferent(dest, nextContent, label) {
160
213
  if (!fs.existsSync(dest)) return false;
161
214
  try {
@@ -207,9 +260,7 @@ function parseAgentMarkdown(content) {
207
260
 
208
261
  function renderCodexAgentToml(markdown, filenameFallback) {
209
262
  const parsed = parseAgentMarkdown(markdown);
210
- const body = parsed.body
211
- .replaceAll("~/.claude/", "~/.codex/")
212
- .replaceAll("@~/.claude/", "@~/.codex/");
263
+ const body = codexText(parsed.body);
213
264
  const name = (parsed.name || filenameFallback || "").replace(/^qualia-/, "");
214
265
  const description = parsed.description || "Qualia Framework specialist agent.";
215
266
  return [
@@ -507,14 +558,15 @@ async function main() {
507
558
  for (const name of claudePruned) ok(`pruned deprecated: ${name}`);
508
559
  for (const skill of skills) {
509
560
  try {
510
- copy(
561
+ copyTextTransform(
511
562
  path.join(skillsDir, skill, "SKILL.md"),
512
- path.join(CLAUDE_DIR, "skills", skill, "SKILL.md")
563
+ path.join(CLAUDE_DIR, "skills", skill, "SKILL.md"),
564
+ claudeText
513
565
  );
514
566
  // Copy REFERENCE.md if the skill has one (progressive-disclosure pattern)
515
567
  const refSrc = path.join(skillsDir, skill, "REFERENCE.md");
516
568
  if (fs.existsSync(refSrc)) {
517
- copy(refSrc, path.join(CLAUDE_DIR, "skills", skill, "REFERENCE.md"));
569
+ copyTextTransform(refSrc, path.join(CLAUDE_DIR, "skills", skill, "REFERENCE.md"), claudeText);
518
570
  }
519
571
  // v5.1: Copy scripts/ subfolder if present (e.g. qualia-polish ships
520
572
  // playwright-capture.mjs, loop.mjs, score.mjs that the --loop mode
@@ -537,7 +589,7 @@ async function main() {
537
589
  const agentsDir = path.join(FRAMEWORK_DIR, "agents");
538
590
  for (const file of fs.readdirSync(agentsDir)) {
539
591
  try {
540
- copy(path.join(agentsDir, file), path.join(CLAUDE_DIR, "agents", file));
592
+ copyTextTransform(path.join(agentsDir, file), path.join(CLAUDE_DIR, "agents", file), claudeText);
541
593
  ok(file);
542
594
  } catch (e) {
543
595
  warn(`${file} — ${e.message}`);
@@ -549,7 +601,7 @@ async function main() {
549
601
  const rulesDir = path.join(FRAMEWORK_DIR, "rules");
550
602
  for (const file of fs.readdirSync(rulesDir)) {
551
603
  try {
552
- copy(path.join(rulesDir, file), path.join(CLAUDE_DIR, "rules", file));
604
+ copyTextTransform(path.join(rulesDir, file), path.join(CLAUDE_DIR, "rules", file), claudeText);
553
605
  ok(file);
554
606
  } catch (e) {
555
607
  warn(`${file} — ${e.message}`);
@@ -590,7 +642,7 @@ async function main() {
590
642
  if (fs.existsSync(designDir)) {
591
643
  for (const file of fs.readdirSync(designDir)) {
592
644
  try {
593
- copy(path.join(designDir, file), path.join(designDest, file));
645
+ copyTextTransform(path.join(designDir, file), path.join(designDest, file), claudeText);
594
646
  ok(file);
595
647
  } catch (e) {
596
648
  warn(`${file} — ${e.message}`);
@@ -651,10 +703,10 @@ async function main() {
651
703
  const destPath = path.join(tmplDest, entry.name);
652
704
  try {
653
705
  if (entry.isDirectory()) {
654
- copyTree(srcPath, destPath);
706
+ copyTreeTransform(srcPath, destPath, claudeText);
655
707
  ok(`${entry.name}/ (directory)`);
656
708
  } else {
657
- copy(srcPath, destPath);
709
+ copyTextTransform(srcPath, destPath, claudeText);
658
710
  ok(entry.name);
659
711
  }
660
712
  } catch (e) {
@@ -685,7 +737,7 @@ async function main() {
685
737
  if (fs.existsSync(dest)) {
686
738
  log(`${DIM}${file} (kept — user has customized)${RESET}`);
687
739
  } else {
688
- copy(src, dest);
740
+ copyTextTransform(src, dest, claudeText);
689
741
  ok(`${file} (initialized)`);
690
742
  }
691
743
  } catch (e) {
@@ -702,7 +754,7 @@ async function main() {
702
754
  if (!fs.existsSync(refDest)) fs.mkdirSync(refDest, { recursive: true });
703
755
  for (const file of fs.readdirSync(refDir)) {
704
756
  try {
705
- copy(path.join(refDir, file), path.join(refDest, file));
757
+ copyTextTransform(path.join(refDir, file), path.join(refDest, file), claudeText);
706
758
  ok(file);
707
759
  } catch (e) {
708
760
  warn(`${file} — ${e.message}`);
@@ -741,7 +793,7 @@ async function main() {
741
793
  try { fs.copyFileSync(claudeDest, bak); ok(`Backed up existing CLAUDE.md → ${path.basename(bak)}`); } catch {}
742
794
  }
743
795
  }
744
- fs.writeFileSync(claudeDest, claudeMd, "utf8");
796
+ fs.writeFileSync(claudeDest, claudeText(claudeMd), "utf8");
745
797
  ok(`Configured as ${member.role}`);
746
798
  } catch (e) {
747
799
  warn(`CLAUDE.md — ${e.message}`);
@@ -752,84 +804,23 @@ async function main() {
752
804
  try {
753
805
  const binDest = path.join(CLAUDE_DIR, "bin");
754
806
  if (!fs.existsSync(binDest)) fs.mkdirSync(binDest, { recursive: true });
755
- copy(
756
- path.join(FRAMEWORK_DIR, "bin", "state.js"),
757
- path.join(binDest, "state.js")
758
- );
759
- ok("state.js (state machine)");
760
- copy(
761
- path.join(FRAMEWORK_DIR, "bin", "qualia-ui.js"),
762
- path.join(binDest, "qualia-ui.js")
763
- );
764
- fs.chmodSync(path.join(binDest, "qualia-ui.js"), 0o755);
765
- ok("qualia-ui.js (cosmetics library)");
766
- copy(
767
- path.join(FRAMEWORK_DIR, "bin", "statusline.js"),
768
- path.join(binDest, "statusline.js")
769
- );
770
- ok("statusline.js (status bar renderer)");
771
- copy(
772
- path.join(FRAMEWORK_DIR, "bin", "knowledge.js"),
773
- path.join(binDest, "knowledge.js")
774
- );
775
- fs.chmodSync(path.join(binDest, "knowledge.js"), 0o755);
776
- ok("knowledge.js (memory-layer loader)");
777
- copy(
778
- path.join(FRAMEWORK_DIR, "bin", "knowledge-flush.js"),
779
- path.join(binDest, "knowledge-flush.js")
780
- );
781
- fs.chmodSync(path.join(binDest, "knowledge-flush.js"), 0o755);
782
- ok("knowledge-flush.js (cron-runnable flush)");
783
- copy(
784
- path.join(FRAMEWORK_DIR, "bin", "plan-contract.js"),
785
- path.join(binDest, "plan-contract.js")
786
- );
787
- ok("plan-contract.js (plan JSON validator)");
788
- copy(
789
- path.join(FRAMEWORK_DIR, "bin", "agent-runs.js"),
790
- path.join(binDest, "agent-runs.js")
791
- );
792
- ok("agent-runs.js (agent telemetry writer)");
793
- copy(
794
- path.join(FRAMEWORK_DIR, "bin", "slop-detect.mjs"),
795
- path.join(binDest, "slop-detect.mjs")
796
- );
797
- fs.chmodSync(path.join(binDest, "slop-detect.mjs"), 0o755);
798
- ok("slop-detect.mjs (anti-pattern scanner — runs pre-commit on frontend builds)");
799
- copy(
800
- path.join(FRAMEWORK_DIR, "bin", "erp-retry.js"),
801
- path.join(binDest, "erp-retry.js")
802
- );
803
- fs.chmodSync(path.join(binDest, "erp-retry.js"), 0o755);
804
- ok("erp-retry.js (ERP report retry queue — drained by session-start hook and erp-flush CLI)");
805
- copy(
806
- path.join(FRAMEWORK_DIR, "bin", "report-payload.js"),
807
- path.join(binDest, "report-payload.js")
808
- );
809
- fs.chmodSync(path.join(binDest, "report-payload.js"), 0o755);
810
- ok("report-payload.js (Framework -> ERP report payload builder)");
811
- copy(
812
- path.join(FRAMEWORK_DIR, "bin", "project-snapshot.js"),
813
- path.join(binDest, "project-snapshot.js")
814
- );
815
- fs.chmodSync(path.join(binDest, "project-snapshot.js"), 0o755);
816
- ok("project-snapshot.js (ERP/admin project progress snapshot)");
817
- copy(
818
- path.join(FRAMEWORK_DIR, "bin", "codex-goal.js"),
819
- path.join(binDest, "codex-goal.js")
820
- );
821
- fs.chmodSync(path.join(binDest, "codex-goal.js"), 0o755);
822
- ok("codex-goal.js (Codex /goal objective + token-budget suggester)");
807
+ for (const script of RUNTIME_BIN_SCRIPTS) {
808
+ const out = path.join(binDest, script.file);
809
+ copy(path.join(FRAMEWORK_DIR, "bin", script.file), out);
810
+ try { fs.chmodSync(out, 0o755); } catch {}
811
+ ok(script.label);
812
+ }
823
813
  } catch (e) {
824
814
  warn(`scripts — ${e.message}`);
825
815
  }
826
816
 
827
817
  // ─── Guide ─────────────────────────────────────────────
828
818
  try {
829
- copy(
830
- path.join(FRAMEWORK_DIR, "guide.md"),
831
- path.join(CLAUDE_DIR, "qualia-guide.md")
832
- );
819
+ copyTextTransform(
820
+ path.join(FRAMEWORK_DIR, "guide.md"),
821
+ path.join(CLAUDE_DIR, "qualia-guide.md"),
822
+ claudeText
823
+ );
833
824
  ok("guide.md");
834
825
  } catch (e) {
835
826
  warn(`guide.md — ${e.message}`);
@@ -1048,6 +1039,7 @@ Client-specific preferences, design choices, and requirements. Loaded by \`/qual
1048
1039
  excludeDefault: true,
1049
1040
  tips: [
1050
1041
  "⬢ Lost? Type /qualia for the next step",
1042
+ "⬢ Broken behavior? Use /qualia-fix for root cause, patch, verify",
1051
1043
  "⬢ Single feature? Use /qualia-feature, it auto-scopes",
1052
1044
  "⬢ End of day? /qualia-report submits your shift before clock-out",
1053
1045
  "⬢ Context isolation: every task gets a fresh AI brain",
@@ -1264,6 +1256,7 @@ function printSummary({ member, target, claudeInstalled }) {
1264
1256
  }
1265
1257
  console.log("");
1266
1258
  console.log(` ${DIM}New project?${RESET} ${TEAL}/qualia-new${RESET}`);
1259
+ console.log(` ${DIM}Broken thing?${RESET} ${TEAL}/qualia-fix${RESET}`);
1267
1260
  console.log(` ${DIM}Single feature?${RESET} ${TEAL}/qualia-feature${RESET}`);
1268
1261
  console.log(` ${DIM}End of day?${RESET} ${TEAL}/qualia-report${RESET} ${DIM}(shift submission)${RESET}`);
1269
1262
  console.log(` ${DIM}Stuck?${RESET} ${TEAL}/qualia${RESET}`);
@@ -1341,53 +1334,20 @@ async function installCodex(member, target) {
1341
1334
  }
1342
1335
 
1343
1336
  // Codex treats config.toml as optional, but doctor reports a warning when it
1344
- // is absent. Create a minimal, parseable file on fresh Codex-only homes and
1345
- // leave existing user config untouched.
1337
+ // is absent. Keep user settings intact while guaranteeing the native bottom
1338
+ // status line is present.
1346
1339
  try {
1347
1340
  const configToml = path.join(CODEX_DIR, "config.toml");
1341
+ const existing = fs.existsSync(configToml) ? fs.readFileSync(configToml, "utf8") : "";
1342
+ const next = ensureCodexStatusLineConfig(existing);
1348
1343
  if (!fs.existsSync(configToml)) {
1349
- atomicWrite(configToml, [
1350
- "# Created by qualia-framework install.",
1351
- "# User settings can be added normally; Qualia runtime lives in AGENTS.md, hooks.json, agents/, and bin/.",
1352
- "",
1353
- "[features]",
1354
- "hooks = true",
1355
- "plugin_hooks = true",
1356
- "",
1357
- "# Codex's built-in status line is rendered at the bottom of the TUI.",
1358
- "# It takes an ARRAY of pre-defined segment names; Codex does NOT support",
1359
- "# custom-command status lines (unlike Claude's settings.json statusLine),",
1360
- "# so the Qualia phase/state info is rendered via the SessionStart banner",
1361
- "# at the top of the session instead. The segment list below mirrors the",
1362
- "# Codex default rich layout.",
1363
- "[tui]",
1364
- 'status_line = ["model-with-reasoning", "task-progress", "current-dir", "git-branch", "context-used", "five-hour-limit", "weekly-limit"]',
1365
- "status_line_use_colors = true",
1366
- "",
1367
- ].join("\n"));
1344
+ atomicWrite(configToml, next);
1368
1345
  ok("config.toml (minimal Codex config)");
1346
+ } else if (next !== existing) {
1347
+ atomicWrite(configToml, next);
1348
+ ok("config.toml (Codex bottom status line)");
1369
1349
  } else {
1370
- // Existing user config append [tui] block only if absent. Leaves
1371
- // every other user setting untouched.
1372
- try {
1373
- const existing = fs.readFileSync(configToml, "utf8");
1374
- if (!/^\[tui\]/m.test(existing) && !/^status_line\s*=/m.test(existing)) {
1375
- const append = [
1376
- "",
1377
- "# Added by qualia-framework — Codex bottom status line.",
1378
- "[tui]",
1379
- 'status_line = ["model-with-reasoning", "task-progress", "current-dir", "git-branch", "context-used", "five-hour-limit", "weekly-limit"]',
1380
- "status_line_use_colors = true",
1381
- "",
1382
- ].join("\n");
1383
- fs.appendFileSync(configToml, append);
1384
- ok("config.toml (appended [tui] status line block)");
1385
- } else {
1386
- log(`${DIM}config.toml (kept — user has customized)${RESET}`);
1387
- }
1388
- } catch {
1389
- log(`${DIM}config.toml (kept — user has customized)${RESET}`);
1390
- }
1350
+ log(`${DIM}config.toml (keptCodex status line already wired)${RESET}`);
1391
1351
  }
1392
1352
  } catch (e) {
1393
1353
  warn(`Codex config.toml — ${e.message}`);
@@ -1436,20 +1396,7 @@ async function installCodex(member, target) {
1436
1396
  try {
1437
1397
  const binDest = path.join(CODEX_DIR, "bin");
1438
1398
  if (!fs.existsSync(binDest)) fs.mkdirSync(binDest, { recursive: true });
1439
- const scripts = [
1440
- "state.js",
1441
- "qualia-ui.js",
1442
- "statusline.js",
1443
- "knowledge.js",
1444
- "knowledge-flush.js",
1445
- "plan-contract.js",
1446
- "agent-runs.js",
1447
- "slop-detect.mjs",
1448
- "erp-retry.js",
1449
- "report-payload.js",
1450
- "project-snapshot.js",
1451
- "codex-goal.js",
1452
- ];
1399
+ const scripts = binFiles();
1453
1400
  for (const script of scripts) {
1454
1401
  const src = path.join(FRAMEWORK_DIR, "bin", script);
1455
1402
  const out = path.join(binDest, script);
@@ -1487,9 +1434,9 @@ async function installCodex(member, target) {
1487
1434
  const rulesDest = path.join(CODEX_DIR, "rules");
1488
1435
  if (!fs.existsSync(rulesDest)) fs.mkdirSync(rulesDest, { recursive: true });
1489
1436
  for (const file of fs.readdirSync(rulesDir)) {
1490
- copy(path.join(rulesDir, file), path.join(rulesDest, file));
1437
+ copyTextTransform(path.join(rulesDir, file), path.join(rulesDest, file), codexText);
1491
1438
  }
1492
- copyTree(path.join(FRAMEWORK_DIR, "qualia-design"), path.join(CODEX_DIR, "qualia-design"));
1439
+ copyTreeTransform(path.join(FRAMEWORK_DIR, "qualia-design"), path.join(CODEX_DIR, "qualia-design"), codexText);
1493
1440
  ok("rules/ + qualia-design/");
1494
1441
  } catch (e) {
1495
1442
  warn(`Codex rules/design — ${e.message}`);