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.
- package/README.md +14 -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/contract-runner.js +219 -0
- package/bin/host-adapters.js +66 -0
- package/bin/install.js +99 -152
- 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/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/command-output.md +35 -0
- package/skills/qualia/SKILL.md +10 -10
- package/skills/qualia-build/SKILL.md +20 -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 +23 -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 +23 -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 +34 -4
- package/tests/install-smoke.test.sh +22 -2
- 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
|
@@ -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
|
-
|
|
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 {
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
706
|
+
copyTreeTransform(srcPath, destPath, claudeText);
|
|
655
707
|
ok(`${entry.name}/ (directory)`);
|
|
656
708
|
} else {
|
|
657
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
756
|
-
path.join(
|
|
757
|
-
path.join(
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
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
|
-
|
|
830
|
-
|
|
831
|
-
|
|
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.
|
|
1345
|
-
//
|
|
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
|
-
|
|
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 (kept — Codex 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
|
-
|
|
1437
|
+
copyTextTransform(path.join(rulesDir, file), path.join(rulesDest, file), codexText);
|
|
1491
1438
|
}
|
|
1492
|
-
|
|
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}`);
|