qualia-framework 3.1.0 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +4 -3
- package/README.md +5 -10
- package/agents/planner.md +0 -52
- package/agents/verifier.md +32 -180
- package/bin/cli.js +9 -403
- package/bin/install.js +61 -113
- package/bin/qualia-ui.js +15 -0
- package/bin/state.js +6 -200
- package/bin/statusline.js +4 -4
- package/hooks/auto-update.js +30 -8
- package/hooks/branch-guard.js +2 -23
- package/hooks/migration-guard.js +0 -23
- package/hooks/pre-compact.js +0 -20
- package/hooks/pre-deploy-gate.js +0 -39
- package/hooks/pre-push.js +0 -20
- package/hooks/session-start.js +44 -0
- package/package.json +4 -5
- package/skills/qualia/SKILL.md +0 -1
- package/skills/qualia-build/SKILL.md +0 -18
- package/skills/qualia-design/SKILL.md +8 -14
- package/skills/qualia-learn/SKILL.md +4 -27
- package/skills/qualia-optimize/SKILL.md +417 -0
- package/skills/qualia-polish/SKILL.md +117 -167
- package/skills/qualia-report/SKILL.md +8 -17
- package/skills/qualia-review/SKILL.md +41 -126
- package/skills/qualia-verify/SKILL.md +1 -1
- package/templates/DESIGN.md +102 -440
- package/templates/plan.md +0 -14
- package/tests/bin.test.sh +6 -20
- package/tests/hooks.test.sh +7 -76
- package/tests/state.test.sh +11 -189
- package/docs/erp-contract.md +0 -161
- package/hooks/block-env-edit.js +0 -52
- package/rules/infrastructure.md +0 -87
- package/skills/qualia-help/SKILL.md +0 -60
- package/skills/qualia-test/SKILL.md +0 -134
- package/templates/help.html +0 -476
- package/tests/runner.js +0 -1956
package/bin/install.js
CHANGED
|
@@ -13,11 +13,8 @@ const YELLOW = "\x1b[38;2;234;179;8m";
|
|
|
13
13
|
const RED = "\x1b[38;2;239;68;68m";
|
|
14
14
|
const RESET = "\x1b[0m";
|
|
15
15
|
|
|
16
|
-
const CLAUDE_DIR = path.join(require("os").homedir(), ".claude");
|
|
17
|
-
const FRAMEWORK_DIR = path.resolve(__dirname, "..");
|
|
18
|
-
|
|
19
16
|
// ─── Team codes ──────────────────────────────────────────
|
|
20
|
-
const
|
|
17
|
+
const TEAM = {
|
|
21
18
|
"QS-FAWZI-01": {
|
|
22
19
|
name: "Fawzi Goussous",
|
|
23
20
|
role: "OWNER",
|
|
@@ -45,21 +42,8 @@ const DEFAULT_TEAM = {
|
|
|
45
42
|
},
|
|
46
43
|
};
|
|
47
44
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const teamFile = path.join(CLAUDE_DIR, ".qualia-team.json");
|
|
51
|
-
try {
|
|
52
|
-
if (fs.existsSync(teamFile)) {
|
|
53
|
-
const external = JSON.parse(fs.readFileSync(teamFile, "utf8"));
|
|
54
|
-
if (external && typeof external === "object" && Object.keys(external).length > 0) {
|
|
55
|
-
return external;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
} catch {}
|
|
59
|
-
return DEFAULT_TEAM;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const TEAM = loadTeam();
|
|
45
|
+
const CLAUDE_DIR = path.join(require("os").homedir(), ".claude");
|
|
46
|
+
const FRAMEWORK_DIR = path.resolve(__dirname, "..");
|
|
63
47
|
|
|
64
48
|
let installed = 0;
|
|
65
49
|
let errors = 0;
|
|
@@ -81,34 +65,14 @@ function copy(src, dest) {
|
|
|
81
65
|
fs.copyFileSync(src, dest);
|
|
82
66
|
}
|
|
83
67
|
|
|
84
|
-
// ─── Branded Header ─────────────────────────────────────
|
|
85
|
-
const BOLD = "\x1b[1m";
|
|
86
|
-
const TEAL_GLOW = "\x1b[38;2;0;170;175m";
|
|
87
|
-
const PKG_VERSION = require("../package.json").version;
|
|
88
|
-
const RULE = "━".repeat(48);
|
|
89
|
-
|
|
90
|
-
function printHeader() {
|
|
91
|
-
console.log("");
|
|
92
|
-
console.log("");
|
|
93
|
-
console.log(` ${TEAL}${BOLD}⬢ Q U A L I A${RESET}`);
|
|
94
|
-
console.log(` ${DIM}${RULE}${RESET}`);
|
|
95
|
-
console.log(` ${WHITE}Framework v${PKG_VERSION}${RESET} ${DIM}·${RESET} ${TEAL_GLOW}Qualia Solutions${RESET}`);
|
|
96
|
-
console.log(` ${DIM}Plan → Build → Verify → Ship${RESET}`);
|
|
97
|
-
console.log(` ${DIM}${RULE}${RESET}`);
|
|
98
|
-
console.log("");
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function printSection(title) {
|
|
102
|
-
console.log("");
|
|
103
|
-
console.log(` ${TEAL}▸${RESET} ${WHITE}${BOLD}${title}${RESET}`);
|
|
104
|
-
console.log(` ${DIM}${"─".repeat(40)}${RESET}`);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
68
|
// ─── Prompt for code ─────────────────────────────────────
|
|
108
69
|
function askCode() {
|
|
109
70
|
return new Promise((resolve) => {
|
|
110
71
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
111
|
-
|
|
72
|
+
console.log("");
|
|
73
|
+
console.log(`${TEAL} ⬢ Qualia Framework v2${RESET}`);
|
|
74
|
+
console.log(`${DIM} ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}`);
|
|
75
|
+
console.log("");
|
|
112
76
|
rl.question(` ${WHITE}Enter install code:${RESET} `, (answer) => {
|
|
113
77
|
rl.close();
|
|
114
78
|
resolve(answer.trim());
|
|
@@ -147,9 +111,10 @@ async function main() {
|
|
|
147
111
|
}
|
|
148
112
|
|
|
149
113
|
console.log("");
|
|
150
|
-
|
|
151
|
-
console.log(
|
|
152
|
-
|
|
114
|
+
log(`${GREEN}✓${RESET} ${WHITE}${member.name}${RESET} ${DIM}(${member.role})${RESET}`);
|
|
115
|
+
console.log("");
|
|
116
|
+
log(`Installing to ${WHITE}${CLAUDE_DIR}${RESET}`);
|
|
117
|
+
console.log("");
|
|
153
118
|
|
|
154
119
|
// ─── Skills ──────────────────────────────────────────
|
|
155
120
|
const skillsDir = path.join(FRAMEWORK_DIR, "skills");
|
|
@@ -157,7 +122,7 @@ async function main() {
|
|
|
157
122
|
.readdirSync(skillsDir)
|
|
158
123
|
.filter((d) => fs.statSync(path.join(skillsDir, d)).isDirectory());
|
|
159
124
|
|
|
160
|
-
|
|
125
|
+
log(`${WHITE}Skills${RESET}`);
|
|
161
126
|
for (const skill of skills) {
|
|
162
127
|
try {
|
|
163
128
|
copy(
|
|
@@ -171,7 +136,7 @@ async function main() {
|
|
|
171
136
|
}
|
|
172
137
|
|
|
173
138
|
// ─── Agents ────────────────────────────────────────────
|
|
174
|
-
|
|
139
|
+
log(`${WHITE}Agents${RESET}`);
|
|
175
140
|
const agentsDir = path.join(FRAMEWORK_DIR, "agents");
|
|
176
141
|
for (const file of fs.readdirSync(agentsDir)) {
|
|
177
142
|
try {
|
|
@@ -183,7 +148,7 @@ async function main() {
|
|
|
183
148
|
}
|
|
184
149
|
|
|
185
150
|
// ─── Rules ─────────────────────────────────────────────
|
|
186
|
-
|
|
151
|
+
log(`${WHITE}Rules${RESET}`);
|
|
187
152
|
const rulesDir = path.join(FRAMEWORK_DIR, "rules");
|
|
188
153
|
for (const file of fs.readdirSync(rulesDir)) {
|
|
189
154
|
try {
|
|
@@ -195,15 +160,20 @@ async function main() {
|
|
|
195
160
|
}
|
|
196
161
|
|
|
197
162
|
// ─── Hooks ─────────────────────────────────────────────
|
|
198
|
-
|
|
163
|
+
log(`${WHITE}Hooks${RESET}`);
|
|
199
164
|
const hooksSource = path.join(FRAMEWORK_DIR, "hooks");
|
|
200
165
|
const hooksDest = path.join(CLAUDE_DIR, "hooks");
|
|
201
166
|
if (!fs.existsSync(hooksDest)) fs.mkdirSync(hooksDest, { recursive: true });
|
|
202
|
-
// Clean up legacy .sh hooks from previous
|
|
203
|
-
// remain on disk after upgrading to the pure-Node
|
|
167
|
+
// Clean up legacy .sh hooks from previous installs so no orphans
|
|
168
|
+
// remain on disk after upgrading to the pure-Node hooks.
|
|
169
|
+
// Also purge explicitly-deprecated hooks by name (these are no longer
|
|
170
|
+
// shipped in framework/hooks/ but may linger on existing installs).
|
|
171
|
+
const DEPRECATED_HOOKS = [
|
|
172
|
+
"block-env-edit.js", // v3.2.0: team decision to unblock .env read/write
|
|
173
|
+
];
|
|
204
174
|
try {
|
|
205
175
|
for (const f of fs.readdirSync(hooksDest)) {
|
|
206
|
-
if (f.endsWith(".sh")) {
|
|
176
|
+
if (f.endsWith(".sh") || DEPRECATED_HOOKS.includes(f)) {
|
|
207
177
|
try { fs.unlinkSync(path.join(hooksDest, f)); } catch {}
|
|
208
178
|
}
|
|
209
179
|
}
|
|
@@ -220,8 +190,18 @@ async function main() {
|
|
|
220
190
|
}
|
|
221
191
|
}
|
|
222
192
|
|
|
193
|
+
// ─── Status line ───────────────────────────────────────
|
|
194
|
+
log(`${WHITE}Status line${RESET}`);
|
|
195
|
+
try {
|
|
196
|
+
const slDest = path.join(CLAUDE_DIR, "bin", "statusline.js");
|
|
197
|
+
copy(path.join(FRAMEWORK_DIR, "bin", "statusline.js"), slDest);
|
|
198
|
+
ok("statusline.js");
|
|
199
|
+
} catch (e) {
|
|
200
|
+
warn(`statusline.js — ${e.message}`);
|
|
201
|
+
}
|
|
202
|
+
|
|
223
203
|
// ─── Templates ─────────────────────────────────────────
|
|
224
|
-
|
|
204
|
+
log(`${WHITE}Templates${RESET}`);
|
|
225
205
|
const tmplDir = path.join(FRAMEWORK_DIR, "templates");
|
|
226
206
|
const tmplDest = path.join(CLAUDE_DIR, "qualia-templates");
|
|
227
207
|
if (!fs.existsSync(tmplDest)) fs.mkdirSync(tmplDest, { recursive: true });
|
|
@@ -235,7 +215,7 @@ async function main() {
|
|
|
235
215
|
}
|
|
236
216
|
|
|
237
217
|
// ─── CLAUDE.md with role ───────────────────────────────
|
|
238
|
-
|
|
218
|
+
log(`${WHITE}CLAUDE.md${RESET}`);
|
|
239
219
|
try {
|
|
240
220
|
let claudeMd = fs.readFileSync(
|
|
241
221
|
path.join(FRAMEWORK_DIR, "CLAUDE.md"),
|
|
@@ -251,7 +231,7 @@ async function main() {
|
|
|
251
231
|
}
|
|
252
232
|
|
|
253
233
|
// ─── Scripts ─────────────────────────────────────────────
|
|
254
|
-
|
|
234
|
+
log(`${WHITE}Scripts${RESET}`);
|
|
255
235
|
try {
|
|
256
236
|
const binDest = path.join(CLAUDE_DIR, "bin");
|
|
257
237
|
if (!fs.existsSync(binDest)) fs.mkdirSync(binDest, { recursive: true });
|
|
@@ -287,7 +267,7 @@ async function main() {
|
|
|
287
267
|
}
|
|
288
268
|
|
|
289
269
|
// ─── Knowledge directory ─────────────────────────────────
|
|
290
|
-
|
|
270
|
+
log(`${WHITE}Knowledge${RESET}`);
|
|
291
271
|
const knowledgeDir = path.join(CLAUDE_DIR, "knowledge");
|
|
292
272
|
if (!fs.existsSync(knowledgeDir)) fs.mkdirSync(knowledgeDir, { recursive: true });
|
|
293
273
|
const knowledgeFiles = {
|
|
@@ -353,7 +333,7 @@ Recurring issues and their solutions.
|
|
|
353
333
|
**Symptom:** \`/qualia-ship\` is blocked with "service_role found in client code" for a file that's actually a Server Component (runs server-side only).
|
|
354
334
|
**Cause:** pre-deploy-gate.js skips files matching \`.server.\` filename pattern OR \`server/\` directory path. If the Server Component is at \`app/admin/page.tsx\` (no .server. marker, not in a server/ dir), the scan flags it.
|
|
355
335
|
**Workaround:** Rename to \`.server.tsx\` OR move to a \`server/\` subdirectory OR extract the service_role usage into a helper in \`lib/server/\`.
|
|
356
|
-
**Framework version:** Known issue
|
|
336
|
+
**Framework version:** Known issue; better heuristic planned for a future release.
|
|
357
337
|
`,
|
|
358
338
|
"client-prefs.md": `# Client Preferences
|
|
359
339
|
|
|
@@ -388,16 +368,11 @@ Client-specific preferences, design choices, and requirements. Loaded by \`/qual
|
|
|
388
368
|
role: member.role,
|
|
389
369
|
version: require("../package.json").version,
|
|
390
370
|
installed_at: new Date().toISOString().split("T")[0],
|
|
391
|
-
erp: {
|
|
392
|
-
enabled: true,
|
|
393
|
-
url: "https://portal.qualiasolutions.net",
|
|
394
|
-
api_key_file: ".erp-api-key",
|
|
395
|
-
},
|
|
396
371
|
};
|
|
397
372
|
fs.writeFileSync(configFile, JSON.stringify(config, null, 2) + "\n");
|
|
398
373
|
|
|
399
374
|
// ─── ERP API key (for report uploads) ──────────────────
|
|
400
|
-
|
|
375
|
+
log(`${WHITE}ERP integration${RESET}`);
|
|
401
376
|
const erpKeyFile = path.join(CLAUDE_DIR, ".erp-api-key");
|
|
402
377
|
if (!fs.existsSync(erpKeyFile)) {
|
|
403
378
|
fs.writeFileSync(erpKeyFile, "qualia-claude-2026", { mode: 0o600 });
|
|
@@ -499,7 +474,7 @@ Client-specific preferences, design choices, and requirements. Loaded by \`/qual
|
|
|
499
474
|
type: "command",
|
|
500
475
|
if: "Bash(git push*)",
|
|
501
476
|
command: nodeCmd("branch-guard.js"),
|
|
502
|
-
timeout:
|
|
477
|
+
timeout: 10,
|
|
503
478
|
statusMessage: "⬢ Checking branch permissions...",
|
|
504
479
|
},
|
|
505
480
|
{
|
|
@@ -521,12 +496,6 @@ Client-specific preferences, design choices, and requirements. Loaded by \`/qual
|
|
|
521
496
|
{
|
|
522
497
|
matcher: "Edit|Write",
|
|
523
498
|
hooks: [
|
|
524
|
-
{
|
|
525
|
-
type: "command",
|
|
526
|
-
command: nodeCmd("block-env-edit.js"),
|
|
527
|
-
timeout: 5,
|
|
528
|
-
statusMessage: "⬢ Checking file permissions...",
|
|
529
|
-
},
|
|
530
499
|
{
|
|
531
500
|
type: "command",
|
|
532
501
|
if: "Edit(*migration*)|Write(*migration*)|Edit(*.sql)|Write(*.sql)",
|
|
@@ -552,44 +521,38 @@ Client-specific preferences, design choices, and requirements. Loaded by \`/qual
|
|
|
552
521
|
],
|
|
553
522
|
};
|
|
554
523
|
|
|
555
|
-
// Permissions
|
|
556
|
-
// Everyone can read/write .env, push to main.
|
|
524
|
+
// Permissions
|
|
557
525
|
if (!settings.permissions) settings.permissions = {};
|
|
558
526
|
if (!settings.permissions.allow) settings.permissions.allow = [];
|
|
559
|
-
if (!settings.permissions.deny)
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
settings.mcpServers["next-devtools"] = {
|
|
566
|
-
command: "npx",
|
|
567
|
-
args: ["next-devtools-mcp@0.3.10"],
|
|
568
|
-
disabled: false,
|
|
569
|
-
};
|
|
570
|
-
ok("MCP: next-devtools (runtime error visibility for Next.js projects)");
|
|
527
|
+
if (!settings.permissions.deny) {
|
|
528
|
+
settings.permissions.deny = [
|
|
529
|
+
"Read(./.env)",
|
|
530
|
+
"Read(./.env.*)",
|
|
531
|
+
"Read(./secrets/**)",
|
|
532
|
+
];
|
|
571
533
|
}
|
|
572
534
|
|
|
573
535
|
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
574
536
|
|
|
575
|
-
ok("Hooks: session-start, auto-update, branch-guard, pre-push,
|
|
537
|
+
ok("Hooks: session-start, auto-update, branch-guard, pre-push, env-block, migration-guard, deploy-gate, pre-compact");
|
|
576
538
|
ok("Status line + spinner configured");
|
|
577
539
|
ok("Environment variables + permissions");
|
|
578
540
|
|
|
579
541
|
// ─── Summary ───────────────────────────────────────────
|
|
580
542
|
console.log("");
|
|
581
|
-
console.log(
|
|
582
|
-
console.log(
|
|
583
|
-
console.log(` ${DIM}${
|
|
584
|
-
console.log(
|
|
585
|
-
console.log(` ${WHITE}${BOLD}${member.name}${RESET} ${DIM}·${RESET} ${roleColor}${member.role}${RESET} ${DIM}·${RESET} ${DIM}v${PKG_VERSION}${RESET}`);
|
|
586
|
-
console.log("");
|
|
543
|
+
console.log(`${TEAL} ⬢ Installed ✓${RESET}`);
|
|
544
|
+
console.log(`${DIM} ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}`);
|
|
545
|
+
console.log(` ${WHITE}${member.name}${RESET} ${DIM}(${member.role})${RESET}`);
|
|
546
|
+
console.log(` Skills: ${WHITE}${skills.length}${RESET}`);
|
|
587
547
|
const agentCount = fs.readdirSync(agentsDir).filter(f => f.endsWith('.md')).length;
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
console.log(` ${
|
|
592
|
-
console.log(` ${
|
|
548
|
+
console.log(` Agents: ${WHITE}${agentCount}${RESET} ${DIM}(planner, builder, verifier, qa-browser)${RESET}`);
|
|
549
|
+
console.log(` Hooks: ${WHITE}8${RESET} ${DIM}(session-start, auto-update, branch-guard, pre-push, env-block, migration-guard, deploy-gate, pre-compact)${RESET}`);
|
|
550
|
+
console.log(` Rules: ${WHITE}${fs.readdirSync(rulesDir).length}${RESET} ${DIM}(security, frontend, design-reference, deployment)${RESET}`);
|
|
551
|
+
console.log(` Scripts: ${WHITE}3${RESET} ${DIM}(state.js, qualia-ui.js, statusline.js)${RESET}`);
|
|
552
|
+
console.log(` Knowledge: ${WHITE}3${RESET} ${DIM}(patterns, fixes, client prefs)${RESET}`);
|
|
553
|
+
console.log(` Templates: ${WHITE}${fs.readdirSync(tmplDir).length}${RESET}`);
|
|
554
|
+
console.log(` Status line: ${GREEN}✓${RESET}`);
|
|
555
|
+
console.log(` CLAUDE.md: ${GREEN}✓${RESET} ${DIM}(${member.role})${RESET}`);
|
|
593
556
|
|
|
594
557
|
if (errors > 0) {
|
|
595
558
|
console.log("");
|
|
@@ -597,22 +560,7 @@ Client-specific preferences, design choices, and requirements. Loaded by \`/qual
|
|
|
597
560
|
}
|
|
598
561
|
|
|
599
562
|
console.log("");
|
|
600
|
-
console.log(` ${
|
|
601
|
-
console.log(` ${WHITE}${BOLD}Quick Start${RESET}`);
|
|
602
|
-
console.log(` ${DIM}${RULE}${RESET}`);
|
|
603
|
-
console.log("");
|
|
604
|
-
console.log(` ${TEAL}1.${RESET} ${WHITE}Restart Claude Code${RESET} ${DIM}(loads new settings)${RESET}`);
|
|
605
|
-
console.log(` ${TEAL}2.${RESET} ${WHITE}cd into any project${RESET} ${DIM}and run${RESET} ${TEAL}claude${RESET}`);
|
|
606
|
-
console.log(` ${TEAL}3.${RESET} ${WHITE}Type${RESET} ${TEAL}${BOLD}/qualia${RESET} ${DIM}— it tells you what to do next${RESET}`);
|
|
607
|
-
console.log("");
|
|
608
|
-
console.log(` ${DIM}New project?${RESET} ${TEAL}/qualia-new${RESET}`);
|
|
609
|
-
console.log(` ${DIM}Quick fix?${RESET} ${TEAL}/qualia-quick${RESET}`);
|
|
610
|
-
console.log(` ${DIM}End of day?${RESET} ${TEAL}/qualia-report${RESET} ${DIM}(mandatory)${RESET}`);
|
|
611
|
-
console.log(` ${DIM}Stuck?${RESET} ${TEAL}/qualia${RESET}`);
|
|
612
|
-
console.log("");
|
|
613
|
-
console.log(` ${DIM}${RULE}${RESET}`);
|
|
614
|
-
console.log(` ${TEAL}${BOLD}Welcome to the future with Qualia.${RESET}`);
|
|
615
|
-
console.log(` ${DIM}${RULE}${RESET}`);
|
|
563
|
+
console.log(` Restart Claude Code, then type ${TEAL}/qualia${RESET} in any project.`);
|
|
616
564
|
console.log("");
|
|
617
565
|
}
|
|
618
566
|
|
package/bin/qualia-ui.js
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
// done <N> <title> [commit] — task line (completed)
|
|
17
17
|
// next <command> — "Run: /qualia-X" footer
|
|
18
18
|
// end <status> [next-command] — closing banner with optional next
|
|
19
|
+
// update <current> <latest> — sticky framework update banner
|
|
19
20
|
|
|
20
21
|
const fs = require("fs");
|
|
21
22
|
const path = require("path");
|
|
@@ -248,6 +249,19 @@ function cmdNext(cmd) {
|
|
|
248
249
|
console.log("");
|
|
249
250
|
}
|
|
250
251
|
|
|
252
|
+
function cmdUpdate(current, latest) {
|
|
253
|
+
if (!current || !latest) return;
|
|
254
|
+
console.log("");
|
|
255
|
+
console.log(` ${YELLOW}${BOLD}▲${RESET} ${WHITE}${BOLD}QUALIA FRAMEWORK UPDATE AVAILABLE${RESET}`);
|
|
256
|
+
console.log(` ${DIM2}${RULE}${RESET}`);
|
|
257
|
+
console.log(` ${pad(DIM + "Current" + RESET, 20)}${DIM}${current}${RESET}`);
|
|
258
|
+
console.log(` ${pad(DIM + "Latest" + RESET, 20)}${GREEN}${BOLD}${latest}${RESET}`);
|
|
259
|
+
console.log(` ${pad(DIM + "Update" + RESET, 20)}${TEAL}npx qualia-framework@latest install${RESET}`);
|
|
260
|
+
console.log(` ${DIM2}${RULE}${RESET}`);
|
|
261
|
+
console.log(` ${DIM}This notice shows every session until you update.${RESET}`);
|
|
262
|
+
console.log("");
|
|
263
|
+
}
|
|
264
|
+
|
|
251
265
|
function cmdEnd(status, nextCmd) {
|
|
252
266
|
console.log("");
|
|
253
267
|
console.log(` ${TEAL}${BOLD}⬢${RESET} ${WHITE}${BOLD}${status || "DONE"}${RESET}`);
|
|
@@ -276,6 +290,7 @@ switch (cmd) {
|
|
|
276
290
|
case "done": cmdDone(rest[0], rest[1], rest[2]); break;
|
|
277
291
|
case "next": cmdNext(rest.join(" ")); break;
|
|
278
292
|
case "end": cmdEnd(rest[0], rest.slice(1).join(" ")); break;
|
|
293
|
+
case "update": cmdUpdate(rest[0], rest[1]); break;
|
|
279
294
|
default:
|
|
280
295
|
console.error(
|
|
281
296
|
`Usage: qualia-ui.js <banner|context|divider|ok|fail|warn|info|spawn|wave|task|done|next|end> [args]`
|
package/bin/state.js
CHANGED
|
@@ -9,17 +9,6 @@ const PLANNING = ".planning";
|
|
|
9
9
|
const STATE_FILE = path.join(PLANNING, "STATE.md");
|
|
10
10
|
const TRACKING_FILE = path.join(PLANNING, "tracking.json");
|
|
11
11
|
|
|
12
|
-
// ─── Trace ──────────────────────────────────────────────
|
|
13
|
-
function _trace(event, data) {
|
|
14
|
-
try {
|
|
15
|
-
const traceDir = path.join(require("os").homedir(), ".claude", ".qualia-traces");
|
|
16
|
-
if (!fs.existsSync(traceDir)) fs.mkdirSync(traceDir, { recursive: true });
|
|
17
|
-
const entry = { hook: event, timestamp: new Date().toISOString(), ...data };
|
|
18
|
-
const file = path.join(traceDir, `${new Date().toISOString().split("T")[0]}.jsonl`);
|
|
19
|
-
fs.appendFileSync(file, JSON.stringify(entry) + "\n");
|
|
20
|
-
} catch { /* trace failures must not disrupt state machine */ }
|
|
21
|
-
}
|
|
22
|
-
|
|
23
12
|
// ─── Arg Parsing ─────────────────────────────────────────
|
|
24
13
|
function parseArgs(argv) {
|
|
25
14
|
const args = {};
|
|
@@ -197,25 +186,6 @@ const VALID_FROM = {
|
|
|
197
186
|
done: ["handed_off"],
|
|
198
187
|
};
|
|
199
188
|
|
|
200
|
-
// ─── Configurable Gap Cycle Limit ────────────────────────
|
|
201
|
-
function getGapCycleLimit() {
|
|
202
|
-
// Priority: tracking.json.gap_cycle_limit > PROJECT.md > default (2)
|
|
203
|
-
try {
|
|
204
|
-
const t = readTracking();
|
|
205
|
-
if (t && typeof t.gap_cycle_limit === "number" && t.gap_cycle_limit > 0) {
|
|
206
|
-
return t.gap_cycle_limit;
|
|
207
|
-
}
|
|
208
|
-
} catch {}
|
|
209
|
-
|
|
210
|
-
try {
|
|
211
|
-
const projectMd = fs.readFileSync(path.join(PLANNING, "PROJECT.md"), "utf8");
|
|
212
|
-
const match = projectMd.match(/^gap_cycle_limit:\s*(\d+)/m);
|
|
213
|
-
if (match) return parseInt(match[1]);
|
|
214
|
-
} catch {}
|
|
215
|
-
|
|
216
|
-
return 2; // default
|
|
217
|
-
}
|
|
218
|
-
|
|
219
189
|
function checkPreconditions(current, target, opts) {
|
|
220
190
|
const phase = parseInt(opts.phase) || current.phase;
|
|
221
191
|
|
|
@@ -237,14 +207,6 @@ function checkPreconditions(current, target, opts) {
|
|
|
237
207
|
const planFile = path.join(PLANNING, `phase-${phase}-plan.md`);
|
|
238
208
|
if (!fs.existsSync(planFile))
|
|
239
209
|
return fail("MISSING_FILE", `Plan file not found: ${planFile}`);
|
|
240
|
-
// Validate plan content (not just existence)
|
|
241
|
-
const planContent = fs.readFileSync(planFile, "utf8");
|
|
242
|
-
const taskHeaders = planContent.match(/^## Task \d+/gm);
|
|
243
|
-
if (!taskHeaders || taskHeaders.length === 0)
|
|
244
|
-
return fail("INVALID_PLAN", "Plan file has no task headers (expected '## Task N')");
|
|
245
|
-
const doneWhenCount = (planContent.match(/\*\*Done when:\*\*/g) || []).length;
|
|
246
|
-
if (doneWhenCount < taskHeaders.length)
|
|
247
|
-
return fail("INVALID_PLAN", `${taskHeaders.length} tasks but only ${doneWhenCount} 'Done when:' entries`);
|
|
248
210
|
}
|
|
249
211
|
|
|
250
212
|
if (target === "verified") {
|
|
@@ -266,15 +228,14 @@ function checkPreconditions(current, target, opts) {
|
|
|
266
228
|
return fail("MISSING_FILE", `Handoff file not found: ${hFile}`);
|
|
267
229
|
}
|
|
268
230
|
|
|
269
|
-
// Gap-closure circuit breaker
|
|
231
|
+
// Gap-closure circuit breaker
|
|
270
232
|
if (target === "planned" && current.status === "verified") {
|
|
271
233
|
const t = readTracking() || {};
|
|
272
234
|
const cycles = (t.gap_cycles || {})[String(phase)] || 0;
|
|
273
|
-
|
|
274
|
-
if (cycles >= limit) {
|
|
235
|
+
if (cycles >= 2) {
|
|
275
236
|
return fail(
|
|
276
237
|
"GAP_CYCLE_LIMIT",
|
|
277
|
-
`Phase ${phase} has failed verification ${cycles} times
|
|
238
|
+
`Phase ${phase} has failed verification ${cycles} times. Escalate to Fawzi or re-plan from scratch.`
|
|
278
239
|
);
|
|
279
240
|
}
|
|
280
241
|
}
|
|
@@ -333,7 +294,6 @@ function cmdCheck(opts) {
|
|
|
333
294
|
assigned_to: s.assigned_to,
|
|
334
295
|
verification: t.verification || "pending",
|
|
335
296
|
gap_cycles: (t.gap_cycles || {})[String(s.phase)] || 0,
|
|
336
|
-
gap_cycle_limit: getGapCycleLimit(),
|
|
337
297
|
tasks_done: t.tasks_done || 0,
|
|
338
298
|
tasks_total: t.tasks_total || 0,
|
|
339
299
|
deployed_url: t.deployed_url || "",
|
|
@@ -371,6 +331,7 @@ function cmdTransition(opts) {
|
|
|
371
331
|
|
|
372
332
|
// Special: note/activity (no status change)
|
|
373
333
|
if (target === "note" || target === "activity") {
|
|
334
|
+
const now = new Date().toISOString().split("T")[0];
|
|
374
335
|
if (opts.notes) t.notes = opts.notes;
|
|
375
336
|
t.last_updated = new Date().toISOString();
|
|
376
337
|
writeTracking(t);
|
|
@@ -392,16 +353,7 @@ function cmdTransition(opts) {
|
|
|
392
353
|
target,
|
|
393
354
|
{ ...opts, phase }
|
|
394
355
|
);
|
|
395
|
-
if (!check.ok)
|
|
396
|
-
// Force only bypasses status-ordering errors (PRECONDITION_FAILED, GAP_CYCLE_LIMIT).
|
|
397
|
-
// Never bypass MISSING_FILE, MISSING_ARG, INVALID_PLAN — those cause broken state.
|
|
398
|
-
const forceableErrors = ["PRECONDITION_FAILED", "GAP_CYCLE_LIMIT"];
|
|
399
|
-
if (opts.force && forceableErrors.includes(check.error)) {
|
|
400
|
-
console.error(`WARNING: Forcing transition despite: ${check.message}`);
|
|
401
|
-
} else {
|
|
402
|
-
return output(check);
|
|
403
|
-
}
|
|
404
|
-
}
|
|
356
|
+
if (!check.ok) return output(check);
|
|
405
357
|
|
|
406
358
|
const prevStatus = s.status;
|
|
407
359
|
|
|
@@ -480,18 +432,6 @@ function cmdTransition(opts) {
|
|
|
480
432
|
return output(fail("WRITE_ERROR", e.message));
|
|
481
433
|
}
|
|
482
434
|
|
|
483
|
-
// Skill outcome scoring — log transition for analytics
|
|
484
|
-
_trace("state-transition", {
|
|
485
|
-
result: "allow",
|
|
486
|
-
phase: s.phase,
|
|
487
|
-
status: s.status,
|
|
488
|
-
previous_status: prevStatus,
|
|
489
|
-
verification: t.verification,
|
|
490
|
-
gap_closure: prevStatus === "verified" && target === "planned",
|
|
491
|
-
duration_ms: 0,
|
|
492
|
-
extra: { verification: t.verification, gap_closure: prevStatus === "verified" && target === "planned" }
|
|
493
|
-
});
|
|
494
|
-
|
|
495
435
|
output({
|
|
496
436
|
ok: true,
|
|
497
437
|
phase: s.phase,
|
|
@@ -657,137 +597,6 @@ function cmdFix(opts) {
|
|
|
657
597
|
});
|
|
658
598
|
}
|
|
659
599
|
|
|
660
|
-
function cmdValidatePlan(opts) {
|
|
661
|
-
const phase = parseInt(opts.phase) || 1;
|
|
662
|
-
const planFile = path.join(PLANNING, `phase-${phase}-plan.md`);
|
|
663
|
-
|
|
664
|
-
if (!fs.existsSync(planFile)) {
|
|
665
|
-
return output(fail("MISSING_FILE", `Plan file not found: ${planFile}`));
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
const content = fs.readFileSync(planFile, "utf8");
|
|
669
|
-
const errors = [];
|
|
670
|
-
|
|
671
|
-
// Check frontmatter exists
|
|
672
|
-
if (!/^---\n/.test(content)) {
|
|
673
|
-
errors.push("Missing frontmatter (---) at start of file");
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
// Check task count > 0
|
|
677
|
-
const taskHeaders = content.match(/^## Task \d+/gm);
|
|
678
|
-
if (!taskHeaders || taskHeaders.length === 0) {
|
|
679
|
-
errors.push("No task headers found (expected '## Task N — title')");
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
// Check "Done when" exists for each task
|
|
683
|
-
const taskCount = taskHeaders ? taskHeaders.length : 0;
|
|
684
|
-
const doneWhenCount = (content.match(/\*\*Done when:\*\*/g) || []).length;
|
|
685
|
-
if (doneWhenCount < taskCount) {
|
|
686
|
-
errors.push(
|
|
687
|
-
`${taskCount} tasks but only ${doneWhenCount} 'Done when:' entries`
|
|
688
|
-
);
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
// Check Success Criteria section exists
|
|
692
|
-
if (!/## Success Criteria/m.test(content)) {
|
|
693
|
-
errors.push("Missing '## Success Criteria' section");
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
// Check goal in frontmatter
|
|
697
|
-
if (!/^goal:/m.test(content)) {
|
|
698
|
-
errors.push("Missing 'goal:' in frontmatter");
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
// ─── Verification Contract Validation (non-blocking) ────
|
|
702
|
-
const warnings = [];
|
|
703
|
-
const VALID_CHECK_TYPES = ["file-exists", "grep-match", "command-exit", "behavioral"];
|
|
704
|
-
let contractCount = 0;
|
|
705
|
-
|
|
706
|
-
if (/^## Verification Contract/m.test(content)) {
|
|
707
|
-
// Extract the contract section (from header to next ## or end of file)
|
|
708
|
-
const contractSectionMatch = content.match(
|
|
709
|
-
/^## Verification Contract\s*\n([\s\S]+)/m
|
|
710
|
-
);
|
|
711
|
-
if (contractSectionMatch) {
|
|
712
|
-
// Trim at the next ## heading that isn't ### (i.e., a new top-level section)
|
|
713
|
-
let contractSection = contractSectionMatch[1];
|
|
714
|
-
const nextH2 = contractSection.search(/\n## (?!#)/);
|
|
715
|
-
if (nextH2 !== -1) contractSection = contractSection.substring(0, nextH2);
|
|
716
|
-
// Each contract starts with ### Contract for Task N
|
|
717
|
-
const contractBlocks = contractSection.match(/^### Contract for Task \d+/gm);
|
|
718
|
-
contractCount = contractBlocks ? contractBlocks.length : 0;
|
|
719
|
-
|
|
720
|
-
if (contractCount === 0) {
|
|
721
|
-
warnings.push("Verification Contract section exists but contains no contract blocks (expected '### Contract for Task N')");
|
|
722
|
-
} else {
|
|
723
|
-
// Split into individual contract blocks for validation
|
|
724
|
-
const blockSplits = contractSection.split(/^(?=### Contract for Task \d+)/m).filter(Boolean);
|
|
725
|
-
for (const block of blockSplits) {
|
|
726
|
-
const taskNumMatch = block.match(/^### Contract for Task (\d+)/);
|
|
727
|
-
if (!taskNumMatch) continue;
|
|
728
|
-
const taskNum = taskNumMatch[1];
|
|
729
|
-
|
|
730
|
-
const checkTypeMatch = block.match(/\*\*Check type:\*\*\s*(.+)/);
|
|
731
|
-
const hasCommand = /\*\*Command:\*\*/.test(block);
|
|
732
|
-
const hasExpected = /\*\*Expected:\*\*/.test(block);
|
|
733
|
-
const hasFailIf = /\*\*Fail if:\*\*/.test(block);
|
|
734
|
-
|
|
735
|
-
if (!checkTypeMatch) {
|
|
736
|
-
warnings.push(`Contract for Task ${taskNum}: missing 'Check type'`);
|
|
737
|
-
} else {
|
|
738
|
-
const checkType = checkTypeMatch[1].trim().toLowerCase();
|
|
739
|
-
if (!VALID_CHECK_TYPES.includes(checkType)) {
|
|
740
|
-
warnings.push(
|
|
741
|
-
`Contract for Task ${taskNum}: invalid check type '${checkType}' (valid: ${VALID_CHECK_TYPES.join(", ")})`
|
|
742
|
-
);
|
|
743
|
-
}
|
|
744
|
-
// behavioral type doesn't require Command or Expected
|
|
745
|
-
const isBehavioral = checkType === "behavioral";
|
|
746
|
-
if (!isBehavioral && !hasCommand) {
|
|
747
|
-
warnings.push(`Contract for Task ${taskNum}: missing 'Command' (required for ${checkType})`);
|
|
748
|
-
}
|
|
749
|
-
if (!isBehavioral && !hasExpected) {
|
|
750
|
-
warnings.push(`Contract for Task ${taskNum}: missing 'Expected' (required for ${checkType})`);
|
|
751
|
-
}
|
|
752
|
-
}
|
|
753
|
-
|
|
754
|
-
if (!hasFailIf) {
|
|
755
|
-
warnings.push(`Contract for Task ${taskNum}: missing 'Fail if'`);
|
|
756
|
-
}
|
|
757
|
-
}
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
// Warn if contract count < task count
|
|
761
|
-
if (taskCount > 0 && contractCount > 0 && contractCount < taskCount) {
|
|
762
|
-
warnings.push(
|
|
763
|
-
`Only ${contractCount} contract(s) for ${taskCount} task(s) — not all tasks have verification contracts`
|
|
764
|
-
);
|
|
765
|
-
}
|
|
766
|
-
}
|
|
767
|
-
}
|
|
768
|
-
|
|
769
|
-
if (errors.length > 0) {
|
|
770
|
-
return output({
|
|
771
|
-
ok: false,
|
|
772
|
-
error: "PLAN_VALIDATION_FAILED",
|
|
773
|
-
phase,
|
|
774
|
-
errors,
|
|
775
|
-
warnings: warnings.length > 0 ? warnings : undefined,
|
|
776
|
-
message: `Plan file has ${errors.length} issue(s)`,
|
|
777
|
-
});
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
output({
|
|
781
|
-
ok: true,
|
|
782
|
-
action: "validate-plan",
|
|
783
|
-
phase,
|
|
784
|
-
task_count: taskCount,
|
|
785
|
-
done_when_count: doneWhenCount,
|
|
786
|
-
contract_count: contractCount,
|
|
787
|
-
warnings: warnings.length > 0 ? warnings : undefined,
|
|
788
|
-
});
|
|
789
|
-
}
|
|
790
|
-
|
|
791
600
|
// ─── Output ──────────────────────────────────────────────
|
|
792
601
|
function output(obj) {
|
|
793
602
|
console.log(JSON.stringify(obj, null, 2));
|
|
@@ -811,14 +620,11 @@ switch (cmd) {
|
|
|
811
620
|
case "fix":
|
|
812
621
|
cmdFix(opts);
|
|
813
622
|
break;
|
|
814
|
-
case "validate-plan":
|
|
815
|
-
cmdValidatePlan(opts);
|
|
816
|
-
break;
|
|
817
623
|
default:
|
|
818
624
|
output(
|
|
819
625
|
fail(
|
|
820
626
|
"UNKNOWN_COMMAND",
|
|
821
|
-
`Usage: state.js <check|transition|init|fix
|
|
627
|
+
`Usage: state.js <check|transition|init|fix> [--options]`
|
|
822
628
|
)
|
|
823
629
|
);
|
|
824
630
|
}
|
package/bin/statusline.js
CHANGED
|
@@ -224,11 +224,11 @@ try {
|
|
|
224
224
|
if (AGENT) LINE1 += ` ${DIM}│${RESET} ${TEAL}⚡${AGENT}${RESET}`;
|
|
225
225
|
if (WORKTREE) LINE1 += ` ${DIM}│${RESET} ${TEAL_DIM}⎇ ${WORKTREE}${RESET}`;
|
|
226
226
|
if (PHASE_INFO) LINE1 += ` ${DIM}│${RESET} ${PHASE_INFO}`;
|
|
227
|
-
// Memory, hooks, skills — context indicators
|
|
227
|
+
// Memory, hooks, skills — context indicators
|
|
228
228
|
const contextParts = [];
|
|
229
|
-
if (MEMORY_COUNT > 0) contextParts.push(`${
|
|
230
|
-
if (HOOKS_COUNT > 0) contextParts.push(`${
|
|
231
|
-
if (SKILLS_COUNT > 0) contextParts.push(`${
|
|
229
|
+
if (MEMORY_COUNT > 0) contextParts.push(`${TEAL}⊙${RESET}${DIM}${MEMORY_COUNT}${RESET}`);
|
|
230
|
+
if (HOOKS_COUNT > 0) contextParts.push(`${TEAL_GLOW}⚙${RESET}${DIM}${HOOKS_COUNT}${RESET}`);
|
|
231
|
+
if (SKILLS_COUNT > 0) contextParts.push(`${TEAL_DIM}✦${RESET}${DIM}${SKILLS_COUNT}${RESET}`);
|
|
232
232
|
if (contextParts.length > 0) {
|
|
233
233
|
LINE1 += ` ${DIM}│${RESET} ${contextParts.join(` ${DIM}·${RESET} `)}`;
|
|
234
234
|
}
|