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.
- package/README.md +18 -11
- package/agents/builder.md +7 -7
- package/agents/planner.md +39 -3
- package/agents/research-synthesizer.md +1 -1
- package/agents/researcher.md +3 -3
- package/agents/roadmapper.md +7 -7
- package/agents/verifier.md +18 -6
- package/agents/visual-evaluator.md +8 -7
- package/bin/cli.js +111 -14
- package/bin/codex-goal.js +92 -0
- package/bin/contract-runner.js +219 -0
- package/bin/host-adapters.js +66 -0
- package/bin/install.js +171 -124
- package/bin/plan-contract.js +99 -2
- package/bin/planning-hygiene.js +262 -0
- package/bin/runtime-manifest.js +32 -0
- package/bin/state-ledger.js +184 -0
- package/bin/state.js +299 -20
- package/bin/trust-score.js +276 -0
- package/docs/onboarding.html +5 -4
- package/guide.md +3 -2
- package/hooks/pre-deploy-gate.js +27 -0
- package/hooks/pre-push.js +19 -0
- package/package.json +1 -1
- package/qualia-design/design-rubric.md +17 -5
- package/qualia-design/frontend.md +5 -1
- package/qualia-design/graphics.md +47 -0
- package/rules/codex-goal.md +46 -0
- package/rules/command-output.md +35 -0
- package/skills/qualia/SKILL.md +10 -10
- package/skills/qualia-build/SKILL.md +24 -14
- package/skills/qualia-debug/SKILL.md +16 -8
- package/skills/qualia-discuss/SKILL.md +10 -10
- package/skills/qualia-doctor/SKILL.md +140 -0
- package/skills/qualia-feature/SKILL.md +27 -21
- package/skills/qualia-fix/SKILL.md +216 -0
- package/skills/qualia-flush/SKILL.md +9 -9
- package/skills/qualia-handoff/SKILL.md +9 -9
- package/skills/qualia-help/SKILL.md +3 -3
- package/skills/qualia-hook-gen/SKILL.md +1 -1
- package/skills/qualia-idk/SKILL.md +4 -4
- package/skills/qualia-issues/SKILL.md +2 -2
- package/skills/qualia-learn/SKILL.md +10 -10
- package/skills/qualia-map/SKILL.md +2 -2
- package/skills/qualia-milestone/SKILL.md +15 -15
- package/skills/qualia-new/REFERENCE.md +9 -9
- package/skills/qualia-new/SKILL.md +14 -14
- package/skills/qualia-optimize/REFERENCE.md +1 -1
- package/skills/qualia-optimize/SKILL.md +23 -16
- package/skills/qualia-pause/SKILL.md +2 -2
- package/skills/qualia-plan/SKILL.md +27 -13
- package/skills/qualia-polish/REFERENCE.md +14 -14
- package/skills/qualia-polish/SKILL.md +64 -19
- package/skills/qualia-polish/scripts/loop.mjs +3 -3
- package/skills/qualia-polish/scripts/score.mjs +9 -3
- package/skills/qualia-postmortem/SKILL.md +9 -9
- package/skills/qualia-report/SKILL.md +23 -23
- package/skills/qualia-research/SKILL.md +5 -5
- package/skills/qualia-resume/SKILL.md +4 -4
- package/skills/qualia-review/SKILL.md +28 -12
- package/skills/qualia-road/SKILL.md +18 -5
- package/skills/qualia-ship/SKILL.md +22 -22
- package/skills/qualia-skill-new/SKILL.md +13 -13
- package/skills/qualia-test/SKILL.md +5 -5
- package/skills/qualia-triage/SKILL.md +1 -1
- package/skills/qualia-verify/SKILL.md +37 -23
- package/skills/qualia-vibe/SKILL.md +13 -10
- package/skills/qualia-vibe/scripts/extract.mjs +1 -1
- package/skills/zoho-workflow/SKILL.md +1 -1
- package/templates/help.html +12 -10
- package/tests/bin.test.sh +35 -5
- package/tests/install-smoke.test.sh +23 -3
- package/tests/lib.test.sh +290 -0
- package/tests/runner.js +3 -0
- package/tests/skills.test.sh +4 -4
- 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
|
-
|
|
129
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
706
|
+
copyTreeTransform(srcPath, destPath, claudeText);
|
|
624
707
|
ok(`${entry.name}/ (directory)`);
|
|
625
708
|
} else {
|
|
626
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
725
|
-
path.join(
|
|
726
|
-
path.join(
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
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
|
-
|
|
793
|
-
|
|
794
|
-
|
|
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.
|
|
1308
|
-
//
|
|
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 —
|
|
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
|
-
|
|
1437
|
+
copyTextTransform(path.join(rulesDir, file), path.join(rulesDest, file), codexText);
|
|
1405
1438
|
}
|
|
1406
|
-
|
|
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",
|
|
1486
|
-
{ type: "command",
|
|
1487
|
-
{ type: "command",
|
|
1488
|
-
{ type: "command",
|
|
1489
|
-
{ type: "command",
|
|
1490
|
-
{ type: "command",
|
|
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",
|
|
1543
|
+
{ type: "command", command: nodeCmd("migration-guard.js"), timeout: 10 },
|
|
1497
1544
|
],
|
|
1498
1545
|
},
|
|
1499
1546
|
],
|
package/bin/plan-contract.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// Plan contract validator + helpers. See docs/plan-contract.md.
|
|
3
3
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
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
|
+
}
|