qualia-framework 6.2.7 → 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 (76) hide show
  1. package/README.md +18 -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/codex-goal.js +92 -0
  11. package/bin/contract-runner.js +219 -0
  12. package/bin/host-adapters.js +66 -0
  13. package/bin/install.js +171 -124
  14. package/bin/plan-contract.js +99 -2
  15. package/bin/planning-hygiene.js +262 -0
  16. package/bin/runtime-manifest.js +32 -0
  17. package/bin/state-ledger.js +184 -0
  18. package/bin/state.js +299 -20
  19. package/bin/trust-score.js +276 -0
  20. package/docs/onboarding.html +5 -4
  21. package/guide.md +3 -2
  22. package/hooks/pre-deploy-gate.js +27 -0
  23. package/hooks/pre-push.js +19 -0
  24. package/package.json +1 -1
  25. package/qualia-design/design-rubric.md +17 -5
  26. package/qualia-design/frontend.md +5 -1
  27. package/qualia-design/graphics.md +47 -0
  28. package/rules/codex-goal.md +46 -0
  29. package/rules/command-output.md +35 -0
  30. package/skills/qualia/SKILL.md +10 -10
  31. package/skills/qualia-build/SKILL.md +24 -14
  32. package/skills/qualia-debug/SKILL.md +16 -8
  33. package/skills/qualia-discuss/SKILL.md +10 -10
  34. package/skills/qualia-doctor/SKILL.md +140 -0
  35. package/skills/qualia-feature/SKILL.md +27 -21
  36. package/skills/qualia-fix/SKILL.md +216 -0
  37. package/skills/qualia-flush/SKILL.md +9 -9
  38. package/skills/qualia-handoff/SKILL.md +9 -9
  39. package/skills/qualia-help/SKILL.md +3 -3
  40. package/skills/qualia-hook-gen/SKILL.md +1 -1
  41. package/skills/qualia-idk/SKILL.md +4 -4
  42. package/skills/qualia-issues/SKILL.md +2 -2
  43. package/skills/qualia-learn/SKILL.md +10 -10
  44. package/skills/qualia-map/SKILL.md +2 -2
  45. package/skills/qualia-milestone/SKILL.md +15 -15
  46. package/skills/qualia-new/REFERENCE.md +9 -9
  47. package/skills/qualia-new/SKILL.md +14 -14
  48. package/skills/qualia-optimize/REFERENCE.md +1 -1
  49. package/skills/qualia-optimize/SKILL.md +23 -16
  50. package/skills/qualia-pause/SKILL.md +2 -2
  51. package/skills/qualia-plan/SKILL.md +27 -13
  52. package/skills/qualia-polish/REFERENCE.md +14 -14
  53. package/skills/qualia-polish/SKILL.md +64 -19
  54. package/skills/qualia-polish/scripts/loop.mjs +3 -3
  55. package/skills/qualia-polish/scripts/score.mjs +9 -3
  56. package/skills/qualia-postmortem/SKILL.md +9 -9
  57. package/skills/qualia-report/SKILL.md +23 -23
  58. package/skills/qualia-research/SKILL.md +5 -5
  59. package/skills/qualia-resume/SKILL.md +4 -4
  60. package/skills/qualia-review/SKILL.md +28 -12
  61. package/skills/qualia-road/SKILL.md +18 -5
  62. package/skills/qualia-ship/SKILL.md +22 -22
  63. package/skills/qualia-skill-new/SKILL.md +13 -13
  64. package/skills/qualia-test/SKILL.md +5 -5
  65. package/skills/qualia-triage/SKILL.md +1 -1
  66. package/skills/qualia-verify/SKILL.md +37 -23
  67. package/skills/qualia-vibe/SKILL.md +13 -10
  68. package/skills/qualia-vibe/scripts/extract.mjs +1 -1
  69. package/skills/zoho-workflow/SKILL.md +1 -1
  70. package/templates/help.html +12 -10
  71. package/tests/bin.test.sh +35 -5
  72. package/tests/install-smoke.test.sh +23 -3
  73. package/tests/lib.test.sh +290 -0
  74. package/tests/runner.js +3 -0
  75. package/tests/skills.test.sh +4 -4
  76. package/tests/state.test.sh +65 -3
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 {
@@ -205,19 +258,46 @@ function parseAgentMarkdown(content) {
205
258
  return result;
206
259
  }
207
260
 
208
- function renderCodexAgentToml(markdown) {
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);
264
+ const name = (parsed.name || filenameFallback || "").replace(/^qualia-/, "");
213
265
  const description = parsed.description || "Qualia Framework specialist agent.";
214
266
  return [
267
+ `name = ${tomlString(name)}`,
215
268
  `description = ${tomlString(description)}`,
216
269
  `developer_instructions = ${tomlString(body)}`,
217
270
  "",
218
271
  ].join("\n");
219
272
  }
220
273
 
274
+ // Skills removed in past versions but still present in older installs.
275
+ // Pruned from BOTH ~/.claude/skills/ and ~/.codex/skills/ on every install run
276
+ // so the active surface matches what the framework currently ships.
277
+ const DEPRECATED_SKILLS = [
278
+ "qualia-task", // v5.7.0 — folded into qualia-feature
279
+ "qualia-quick", // v5.7.0 — folded into qualia-feature
280
+ "qualia-polish-loop", // v5.8.0 — folded into qualia-polish --loop
281
+ "qualia-design", // v4 wave 2 — folded into scope-adaptive qualia-polish
282
+ "qualia-prd", // v5.8.0 — surface cleanup
283
+ ];
284
+
285
+ function pruneDeprecatedSkills(baseDir) {
286
+ const skillsDir = path.join(baseDir, "skills");
287
+ if (!fs.existsSync(skillsDir)) return [];
288
+ const removed = [];
289
+ for (const name of DEPRECATED_SKILLS) {
290
+ const target = path.join(skillsDir, name);
291
+ try {
292
+ if (fs.existsSync(target)) {
293
+ fs.rmSync(target, { recursive: true, force: true });
294
+ removed.push(name);
295
+ }
296
+ } catch {}
297
+ }
298
+ return removed;
299
+ }
300
+
221
301
  // Surgically remove orphaned v2.6 install cruft from ~/.claude/ on upgrade.
222
302
  // v2.6 installed a separate ~/.claude/qualia-framework/ directory with workflows/,
223
303
  // references/, assets/, bin/qualia-tools.js. v3 doesn't use any of that — it was
@@ -474,16 +554,19 @@ async function main() {
474
554
  .filter((d) => fs.statSync(path.join(skillsDir, d)).isDirectory());
475
555
 
476
556
  printSection("Skills");
557
+ const claudePruned = pruneDeprecatedSkills(CLAUDE_DIR);
558
+ for (const name of claudePruned) ok(`pruned deprecated: ${name}`);
477
559
  for (const skill of skills) {
478
560
  try {
479
- copy(
561
+ copyTextTransform(
480
562
  path.join(skillsDir, skill, "SKILL.md"),
481
- path.join(CLAUDE_DIR, "skills", skill, "SKILL.md")
563
+ path.join(CLAUDE_DIR, "skills", skill, "SKILL.md"),
564
+ claudeText
482
565
  );
483
566
  // Copy REFERENCE.md if the skill has one (progressive-disclosure pattern)
484
567
  const refSrc = path.join(skillsDir, skill, "REFERENCE.md");
485
568
  if (fs.existsSync(refSrc)) {
486
- copy(refSrc, path.join(CLAUDE_DIR, "skills", skill, "REFERENCE.md"));
569
+ copyTextTransform(refSrc, path.join(CLAUDE_DIR, "skills", skill, "REFERENCE.md"), claudeText);
487
570
  }
488
571
  // v5.1: Copy scripts/ subfolder if present (e.g. qualia-polish ships
489
572
  // playwright-capture.mjs, loop.mjs, score.mjs that the --loop mode
@@ -506,7 +589,7 @@ async function main() {
506
589
  const agentsDir = path.join(FRAMEWORK_DIR, "agents");
507
590
  for (const file of fs.readdirSync(agentsDir)) {
508
591
  try {
509
- copy(path.join(agentsDir, file), path.join(CLAUDE_DIR, "agents", file));
592
+ copyTextTransform(path.join(agentsDir, file), path.join(CLAUDE_DIR, "agents", file), claudeText);
510
593
  ok(file);
511
594
  } catch (e) {
512
595
  warn(`${file} — ${e.message}`);
@@ -518,7 +601,7 @@ async function main() {
518
601
  const rulesDir = path.join(FRAMEWORK_DIR, "rules");
519
602
  for (const file of fs.readdirSync(rulesDir)) {
520
603
  try {
521
- copy(path.join(rulesDir, file), path.join(CLAUDE_DIR, "rules", file));
604
+ copyTextTransform(path.join(rulesDir, file), path.join(CLAUDE_DIR, "rules", file), claudeText);
522
605
  ok(file);
523
606
  } catch (e) {
524
607
  warn(`${file} — ${e.message}`);
@@ -559,7 +642,7 @@ async function main() {
559
642
  if (fs.existsSync(designDir)) {
560
643
  for (const file of fs.readdirSync(designDir)) {
561
644
  try {
562
- copy(path.join(designDir, file), path.join(designDest, file));
645
+ copyTextTransform(path.join(designDir, file), path.join(designDest, file), claudeText);
563
646
  ok(file);
564
647
  } catch (e) {
565
648
  warn(`${file} — ${e.message}`);
@@ -620,10 +703,10 @@ async function main() {
620
703
  const destPath = path.join(tmplDest, entry.name);
621
704
  try {
622
705
  if (entry.isDirectory()) {
623
- copyTree(srcPath, destPath);
706
+ copyTreeTransform(srcPath, destPath, claudeText);
624
707
  ok(`${entry.name}/ (directory)`);
625
708
  } else {
626
- copy(srcPath, destPath);
709
+ copyTextTransform(srcPath, destPath, claudeText);
627
710
  ok(entry.name);
628
711
  }
629
712
  } catch (e) {
@@ -654,7 +737,7 @@ async function main() {
654
737
  if (fs.existsSync(dest)) {
655
738
  log(`${DIM}${file} (kept — user has customized)${RESET}`);
656
739
  } else {
657
- copy(src, dest);
740
+ copyTextTransform(src, dest, claudeText);
658
741
  ok(`${file} (initialized)`);
659
742
  }
660
743
  } catch (e) {
@@ -671,7 +754,7 @@ async function main() {
671
754
  if (!fs.existsSync(refDest)) fs.mkdirSync(refDest, { recursive: true });
672
755
  for (const file of fs.readdirSync(refDir)) {
673
756
  try {
674
- copy(path.join(refDir, file), path.join(refDest, file));
757
+ copyTextTransform(path.join(refDir, file), path.join(refDest, file), claudeText);
675
758
  ok(file);
676
759
  } catch (e) {
677
760
  warn(`${file} — ${e.message}`);
@@ -710,7 +793,7 @@ async function main() {
710
793
  try { fs.copyFileSync(claudeDest, bak); ok(`Backed up existing CLAUDE.md → ${path.basename(bak)}`); } catch {}
711
794
  }
712
795
  }
713
- fs.writeFileSync(claudeDest, claudeMd, "utf8");
796
+ fs.writeFileSync(claudeDest, claudeText(claudeMd), "utf8");
714
797
  ok(`Configured as ${member.role}`);
715
798
  } catch (e) {
716
799
  warn(`CLAUDE.md — ${e.message}`);
@@ -721,78 +804,23 @@ async function main() {
721
804
  try {
722
805
  const binDest = path.join(CLAUDE_DIR, "bin");
723
806
  if (!fs.existsSync(binDest)) fs.mkdirSync(binDest, { recursive: true });
724
- copy(
725
- path.join(FRAMEWORK_DIR, "bin", "state.js"),
726
- path.join(binDest, "state.js")
727
- );
728
- ok("state.js (state machine)");
729
- copy(
730
- path.join(FRAMEWORK_DIR, "bin", "qualia-ui.js"),
731
- path.join(binDest, "qualia-ui.js")
732
- );
733
- fs.chmodSync(path.join(binDest, "qualia-ui.js"), 0o755);
734
- ok("qualia-ui.js (cosmetics library)");
735
- copy(
736
- path.join(FRAMEWORK_DIR, "bin", "statusline.js"),
737
- path.join(binDest, "statusline.js")
738
- );
739
- ok("statusline.js (status bar renderer)");
740
- copy(
741
- path.join(FRAMEWORK_DIR, "bin", "knowledge.js"),
742
- path.join(binDest, "knowledge.js")
743
- );
744
- fs.chmodSync(path.join(binDest, "knowledge.js"), 0o755);
745
- ok("knowledge.js (memory-layer loader)");
746
- copy(
747
- path.join(FRAMEWORK_DIR, "bin", "knowledge-flush.js"),
748
- path.join(binDest, "knowledge-flush.js")
749
- );
750
- fs.chmodSync(path.join(binDest, "knowledge-flush.js"), 0o755);
751
- ok("knowledge-flush.js (cron-runnable flush)");
752
- copy(
753
- path.join(FRAMEWORK_DIR, "bin", "plan-contract.js"),
754
- path.join(binDest, "plan-contract.js")
755
- );
756
- ok("plan-contract.js (plan JSON validator)");
757
- copy(
758
- path.join(FRAMEWORK_DIR, "bin", "agent-runs.js"),
759
- path.join(binDest, "agent-runs.js")
760
- );
761
- ok("agent-runs.js (agent telemetry writer)");
762
- copy(
763
- path.join(FRAMEWORK_DIR, "bin", "slop-detect.mjs"),
764
- path.join(binDest, "slop-detect.mjs")
765
- );
766
- fs.chmodSync(path.join(binDest, "slop-detect.mjs"), 0o755);
767
- ok("slop-detect.mjs (anti-pattern scanner — runs pre-commit on frontend builds)");
768
- copy(
769
- path.join(FRAMEWORK_DIR, "bin", "erp-retry.js"),
770
- path.join(binDest, "erp-retry.js")
771
- );
772
- fs.chmodSync(path.join(binDest, "erp-retry.js"), 0o755);
773
- ok("erp-retry.js (ERP report retry queue — drained by session-start hook and erp-flush CLI)");
774
- copy(
775
- path.join(FRAMEWORK_DIR, "bin", "report-payload.js"),
776
- path.join(binDest, "report-payload.js")
777
- );
778
- fs.chmodSync(path.join(binDest, "report-payload.js"), 0o755);
779
- ok("report-payload.js (Framework -> ERP report payload builder)");
780
- copy(
781
- path.join(FRAMEWORK_DIR, "bin", "project-snapshot.js"),
782
- path.join(binDest, "project-snapshot.js")
783
- );
784
- fs.chmodSync(path.join(binDest, "project-snapshot.js"), 0o755);
785
- ok("project-snapshot.js (ERP/admin project progress snapshot)");
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
+ }
786
813
  } catch (e) {
787
814
  warn(`scripts — ${e.message}`);
788
815
  }
789
816
 
790
817
  // ─── Guide ─────────────────────────────────────────────
791
818
  try {
792
- copy(
793
- path.join(FRAMEWORK_DIR, "guide.md"),
794
- path.join(CLAUDE_DIR, "qualia-guide.md")
795
- );
819
+ copyTextTransform(
820
+ path.join(FRAMEWORK_DIR, "guide.md"),
821
+ path.join(CLAUDE_DIR, "qualia-guide.md"),
822
+ claudeText
823
+ );
796
824
  ok("guide.md");
797
825
  } catch (e) {
798
826
  warn(`guide.md — ${e.message}`);
@@ -1011,6 +1039,7 @@ Client-specific preferences, design choices, and requirements. Loaded by \`/qual
1011
1039
  excludeDefault: true,
1012
1040
  tips: [
1013
1041
  "⬢ Lost? Type /qualia for the next step",
1042
+ "⬢ Broken behavior? Use /qualia-fix for root cause, patch, verify",
1014
1043
  "⬢ Single feature? Use /qualia-feature, it auto-scopes",
1015
1044
  "⬢ End of day? /qualia-report submits your shift before clock-out",
1016
1045
  "⬢ Context isolation: every task gets a fresh AI brain",
@@ -1227,6 +1256,7 @@ function printSummary({ member, target, claudeInstalled }) {
1227
1256
  }
1228
1257
  console.log("");
1229
1258
  console.log(` ${DIM}New project?${RESET} ${TEAL}/qualia-new${RESET}`);
1259
+ console.log(` ${DIM}Broken thing?${RESET} ${TEAL}/qualia-fix${RESET}`);
1230
1260
  console.log(` ${DIM}Single feature?${RESET} ${TEAL}/qualia-feature${RESET}`);
1231
1261
  console.log(` ${DIM}End of day?${RESET} ${TEAL}/qualia-report${RESET} ${DIM}(shift submission)${RESET}`);
1232
1262
  console.log(` ${DIM}Stuck?${RESET} ${TEAL}/qualia${RESET}`);
@@ -1304,23 +1334,20 @@ async function installCodex(member, target) {
1304
1334
  }
1305
1335
 
1306
1336
  // Codex treats config.toml as optional, but doctor reports a warning when it
1307
- // is absent. Create a minimal, parseable file on fresh Codex-only homes and
1308
- // leave existing user config untouched.
1337
+ // is absent. Keep user settings intact while guaranteeing the native bottom
1338
+ // status line is present.
1309
1339
  try {
1310
1340
  const configToml = path.join(CODEX_DIR, "config.toml");
1341
+ const existing = fs.existsSync(configToml) ? fs.readFileSync(configToml, "utf8") : "";
1342
+ const next = ensureCodexStatusLineConfig(existing);
1311
1343
  if (!fs.existsSync(configToml)) {
1312
- atomicWrite(configToml, [
1313
- "# Created by qualia-framework install.",
1314
- "# User settings can be added normally; Qualia runtime lives in AGENTS.md, hooks.json, agents/, and bin/.",
1315
- "",
1316
- "[features]",
1317
- "hooks = true",
1318
- "plugin_hooks = true",
1319
- "",
1320
- ].join("\n"));
1344
+ atomicWrite(configToml, next);
1321
1345
  ok("config.toml (minimal Codex config)");
1346
+ } else if (next !== existing) {
1347
+ atomicWrite(configToml, next);
1348
+ ok("config.toml (Codex bottom status line)");
1322
1349
  } else {
1323
- log(`${DIM}config.toml (kept — user has customized)${RESET}`);
1350
+ log(`${DIM}config.toml (kept — Codex status line already wired)${RESET}`);
1324
1351
  }
1325
1352
  } catch (e) {
1326
1353
  warn(`Codex config.toml — ${e.message}`);
@@ -1347,23 +1374,29 @@ async function installCodex(member, target) {
1347
1374
  warn(`Codex config — ${e.message}`);
1348
1375
  }
1349
1376
 
1377
+ // Mirror the ERP API key from Claude → Codex so erp-retry/report-payload can
1378
+ // post from Codex sessions without a separate provisioning step. The key
1379
+ // resolver at runtime looks in $CODEX_DIR/.erp-api-key only; without this
1380
+ // copy, every Codex ERP write 401s and the queue grows silently.
1381
+ try {
1382
+ const claudeKey = path.join(CLAUDE_DIR, ".erp-api-key");
1383
+ const codexKey = path.join(CODEX_DIR, ".erp-api-key");
1384
+ if (fs.existsSync(claudeKey) && !fs.existsSync(codexKey)) {
1385
+ const key = fs.readFileSync(claudeKey, "utf8");
1386
+ atomicWrite(codexKey, key, 0o600);
1387
+ ok(".erp-api-key (mirrored from ~/.claude/)");
1388
+ } else if (fs.existsSync(codexKey)) {
1389
+ log(`${DIM}.erp-api-key (existing — preserved)${RESET}`);
1390
+ }
1391
+ } catch (e) {
1392
+ warn(`Codex .erp-api-key — ${e.message}`);
1393
+ }
1394
+
1350
1395
  // Scripts
1351
1396
  try {
1352
1397
  const binDest = path.join(CODEX_DIR, "bin");
1353
1398
  if (!fs.existsSync(binDest)) fs.mkdirSync(binDest, { recursive: true });
1354
- const scripts = [
1355
- "state.js",
1356
- "qualia-ui.js",
1357
- "statusline.js",
1358
- "knowledge.js",
1359
- "knowledge-flush.js",
1360
- "plan-contract.js",
1361
- "agent-runs.js",
1362
- "slop-detect.mjs",
1363
- "erp-retry.js",
1364
- "report-payload.js",
1365
- "project-snapshot.js",
1366
- ];
1399
+ const scripts = binFiles();
1367
1400
  for (const script of scripts) {
1368
1401
  const src = path.join(FRAMEWORK_DIR, "bin", script);
1369
1402
  const out = path.join(binDest, script);
@@ -1386,7 +1419,7 @@ async function installCodex(member, target) {
1386
1419
  const parsed = parseAgentMarkdown(source);
1387
1420
  const base = parsed.name ? parsed.name.replace(/^qualia-/, "") : path.basename(file, ".md");
1388
1421
  const out = path.join(agentsDest, `${base}.toml`);
1389
- const toml = renderCodexAgentToml(source);
1422
+ const toml = renderCodexAgentToml(source, base);
1390
1423
  backupIfDifferent(out, toml, `agents/${base}.toml`);
1391
1424
  atomicWrite(out, toml);
1392
1425
  }
@@ -1401,9 +1434,9 @@ async function installCodex(member, target) {
1401
1434
  const rulesDest = path.join(CODEX_DIR, "rules");
1402
1435
  if (!fs.existsSync(rulesDest)) fs.mkdirSync(rulesDest, { recursive: true });
1403
1436
  for (const file of fs.readdirSync(rulesDir)) {
1404
- copy(path.join(rulesDir, file), path.join(rulesDest, file));
1437
+ copyTextTransform(path.join(rulesDir, file), path.join(rulesDest, file), codexText);
1405
1438
  }
1406
- 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);
1407
1440
  ok("rules/ + qualia-design/");
1408
1441
  } catch (e) {
1409
1442
  warn(`Codex rules/design — ${e.message}`);
@@ -1415,6 +1448,8 @@ async function installCodex(member, target) {
1415
1448
  try {
1416
1449
  const skillsSrc = path.join(FRAMEWORK_DIR, "skills");
1417
1450
  const skillsDest = path.join(CODEX_DIR, "skills");
1451
+ const codexPruned = pruneDeprecatedSkills(CODEX_DIR);
1452
+ for (const name of codexPruned) ok(`pruned deprecated: ${name}`);
1418
1453
  for (const skill of fs.readdirSync(skillsSrc)) {
1419
1454
  const src = path.join(skillsSrc, skill);
1420
1455
  if (!fs.statSync(src).isDirectory()) continue;
@@ -1471,6 +1506,18 @@ async function installCodex(member, target) {
1471
1506
  try { fs.chmodSync(out, 0o755); } catch {}
1472
1507
  }
1473
1508
  const nodeCmd = (hookFile) => `node "${path.join(hooksDest, hookFile)}"`;
1509
+ // Codex's hook schema does NOT include an `if` field — only `command`,
1510
+ // `commandWindows`, `timeout`, `async`, `statusMessage`. Filtering on
1511
+ // tool_input.command happens inside each hook script (they read stdin
1512
+ // JSON and `process.exit(0)` fast when the command doesn't match).
1513
+ //
1514
+ // Codex prints `statusMessage` for every entry in the matched group BEFORE
1515
+ // running the hook. Including statusMessage on conditional hooks produced
1516
+ // 8 lines of "Running PreToolUse hook: Qualia X..." on every Bash call
1517
+ // even when 6 of the 8 immediately exited 0. We only set statusMessage on
1518
+ // hooks that always do real work (auto-update + git-guardrails). The
1519
+ // conditional hooks stay registered (so they still fire when applicable)
1520
+ // but render silently.
1474
1521
  const qualiaHooks = {
1475
1522
  hooks: {
1476
1523
  SessionStart: [
@@ -1482,18 +1529,18 @@ async function installCodex(member, target) {
1482
1529
  hooks: [
1483
1530
  { type: "command", command: nodeCmd("auto-update.js"), timeout: 5, statusMessage: "Qualia update check..." },
1484
1531
  { type: "command", command: nodeCmd("git-guardrails.js"), timeout: 5, statusMessage: "Qualia git safety..." },
1485
- { type: "command", if: "Bash(git push*)", command: nodeCmd("branch-guard.js"), timeout: 5, statusMessage: "Qualia branch guard..." },
1486
- { type: "command", if: "Bash(git push*)", command: nodeCmd("pre-push.js"), timeout: 15, statusMessage: "Qualia tracking stamp..." },
1487
- { type: "command", if: "Bash(vercel --prod*)", command: nodeCmd("pre-deploy-gate.js"), timeout: 180, statusMessage: "Qualia deploy gate..." },
1488
- { type: "command", if: "Bash(vercel --prod*)|Bash(vercel deploy*)", command: nodeCmd("vercel-account-guard.js"), timeout: 8, statusMessage: "Qualia Vercel account..." },
1489
- { type: "command", if: "Bash(vercel env*)", command: nodeCmd("env-empty-guard.js"), timeout: 5, statusMessage: "Qualia env guard..." },
1490
- { type: "command", if: "Bash(supabase*)|Bash(npx supabase*)", command: nodeCmd("supabase-destructive-guard.js"), timeout: 5, statusMessage: "Qualia Supabase guard..." },
1532
+ { type: "command", command: nodeCmd("branch-guard.js"), timeout: 5 },
1533
+ { type: "command", command: nodeCmd("pre-push.js"), timeout: 15 },
1534
+ { type: "command", command: nodeCmd("pre-deploy-gate.js"), timeout: 180 },
1535
+ { type: "command", command: nodeCmd("vercel-account-guard.js"), timeout: 8 },
1536
+ { type: "command", command: nodeCmd("env-empty-guard.js"), timeout: 5 },
1537
+ { type: "command", command: nodeCmd("supabase-destructive-guard.js"), timeout: 5 },
1491
1538
  ],
1492
1539
  },
1493
1540
  {
1494
1541
  matcher: "Edit|Write",
1495
1542
  hooks: [
1496
- { type: "command", if: "Edit(*migration*)|Write(*migration*)|Edit(*.sql)|Write(*.sql)", command: nodeCmd("migration-guard.js"), timeout: 10, statusMessage: "Qualia migration guard..." },
1543
+ { type: "command", command: nodeCmd("migration-guard.js"), timeout: 10 },
1497
1544
  ],
1498
1545
  },
1499
1546
  ],
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  // Plan contract validator + helpers. See docs/plan-contract.md.
3
3
  //
4
- // Pure library no CLI dispatch. Required by state.js and by skills that
5
- // emit/consume `.planning/phase-{N}-contract.json`.
4
+ // Library + tiny CLI. Required by state.js and by skills that emit/consume
5
+ // `.planning/phase-{N}-contract.json`.
6
6
  //
7
7
  // Zero npm dependencies. Hand-rolled validator, ~100 LOC.
8
8
 
@@ -241,11 +241,108 @@ function checkDrift(contractPath, planMdPath) {
241
241
  return { ok: true, drift: stored !== current, stored, current };
242
242
  }
243
243
 
244
+ function readContractFile(contractPath) {
245
+ if (!fs.existsSync(contractPath)) {
246
+ return { ok: false, error: "CONTRACT_MISSING", message: `Contract file not found: ${contractPath}` };
247
+ }
248
+ const parsed = parseSafely(fs.readFileSync(contractPath, "utf8"));
249
+ if (!parsed.ok) {
250
+ return { ok: false, error: "CONTRACT_UNPARSEABLE", message: parsed.error };
251
+ }
252
+ return { ok: true, contract: parsed.value };
253
+ }
254
+
255
+ function cliUsage() {
256
+ console.error([
257
+ "Usage:",
258
+ " plan-contract.js validate <contract.json> [--json]",
259
+ " plan-contract.js drift <contract.json> <plan.md> [--json]",
260
+ " plan-contract.js hash <plan.md>",
261
+ ].join("\n"));
262
+ }
263
+
264
+ function printResult(payload, asJson) {
265
+ if (asJson) {
266
+ console.log(JSON.stringify(payload, null, 2));
267
+ return;
268
+ }
269
+ if (payload.ok) {
270
+ if (payload.action === "validate") console.log(`VALID ${payload.path}`);
271
+ else if (payload.action === "drift") console.log(payload.drift ? "DRIFT" : "NO_DRIFT");
272
+ else if (payload.action === "hash") console.log(payload.hash);
273
+ else console.log("OK");
274
+ return;
275
+ }
276
+ console.error(`${payload.error || "ERROR"}: ${payload.message || (payload.errors || []).join("; ")}`);
277
+ }
278
+
279
+ function main(argv) {
280
+ const cmd = argv[2];
281
+ const asJson = argv.includes("--json");
282
+ if (!cmd || cmd === "--help" || cmd === "-h") {
283
+ cliUsage();
284
+ return 2;
285
+ }
286
+
287
+ if (cmd === "validate") {
288
+ const contractPath = argv[3];
289
+ if (!contractPath || contractPath.startsWith("--")) {
290
+ cliUsage();
291
+ return 2;
292
+ }
293
+ const loaded = readContractFile(contractPath);
294
+ if (!loaded.ok) {
295
+ printResult({ ok: false, action: "validate", path: contractPath, ...loaded }, asJson);
296
+ return 2;
297
+ }
298
+ const errors = validate(loaded.contract);
299
+ const payload = { ok: errors.length === 0, action: "validate", path: contractPath, errors };
300
+ printResult(payload, asJson);
301
+ return payload.ok ? 0 : 1;
302
+ }
303
+
304
+ if (cmd === "drift") {
305
+ const contractPath = argv[3];
306
+ const planPath = argv[4];
307
+ if (!contractPath || !planPath || contractPath.startsWith("--") || planPath.startsWith("--")) {
308
+ cliUsage();
309
+ return 2;
310
+ }
311
+ const result = checkDrift(contractPath, planPath);
312
+ const payload = { ok: !!result.ok && !result.drift, action: "drift", path: contractPath, plan: planPath, ...result };
313
+ printResult(payload, asJson);
314
+ return payload.ok ? 0 : 1;
315
+ }
316
+
317
+ if (cmd === "hash") {
318
+ const planPath = argv[3];
319
+ if (!planPath || planPath.startsWith("--")) {
320
+ cliUsage();
321
+ return 2;
322
+ }
323
+ if (!fs.existsSync(planPath)) {
324
+ printResult({ ok: false, action: "hash", error: "PLAN_MISSING", message: `Plan file not found: ${planPath}` }, asJson);
325
+ return 2;
326
+ }
327
+ const hash = hashPlan(fs.readFileSync(planPath, "utf8"));
328
+ printResult({ ok: true, action: "hash", path: planPath, hash }, asJson);
329
+ return 0;
330
+ }
331
+
332
+ cliUsage();
333
+ return 2;
334
+ }
335
+
244
336
  module.exports = {
245
337
  SCHEMA_VERSION,
246
338
  validate,
247
339
  parseSafely,
248
340
  hashPlan,
249
341
  checkDrift,
342
+ readContractFile,
250
343
  findScopeReductionPhrases,
251
344
  };
345
+
346
+ if (require.main === module) {
347
+ process.exit(main(process.argv));
348
+ }