qualia-framework-v2 2.8.1 → 2.10.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/README.md +5 -0
- package/bin/cli.js +268 -6
- package/bin/install.js +96 -21
- package/bin/qualia-ui.js +26 -26
- package/bin/state.js +136 -2
- package/bin/statusline.js +53 -3
- package/hooks/migration-guard.js +1 -1
- package/hooks/pre-deploy-gate.js +2 -2
- package/package.json +2 -2
- package/skills/qualia-design/SKILL.md +1 -1
- package/skills/qualia-learn/SKILL.md +1 -1
- package/skills/qualia-new/SKILL.md +2 -2
- package/skills/qualia-review/SKILL.md +1 -1
- package/tests/bin.test.sh +673 -0
- package/tests/hooks.test.sh +155 -25
- package/tests/state.test.sh +137 -0
- package/tests/statusline.test.sh +243 -0
package/bin/state.js
CHANGED
|
@@ -51,19 +51,54 @@ function readState() {
|
|
|
51
51
|
// ─── STATE.md Parser ─────────────────────────────────────
|
|
52
52
|
function parseStateMd(content) {
|
|
53
53
|
if (!content) return null;
|
|
54
|
+
const schema_errors = [];
|
|
54
55
|
const get = (prefix) => {
|
|
55
56
|
const m = content.match(new RegExp(`^${prefix}:\\s*(.+)$`, "m"));
|
|
56
57
|
return m ? m[1].trim() : "";
|
|
57
58
|
};
|
|
59
|
+
const hasField = (prefix) =>
|
|
60
|
+
new RegExp(`^${prefix}:\\s*`, "m").test(content);
|
|
61
|
+
|
|
58
62
|
const phaseMatch = content.match(
|
|
59
63
|
/^Phase:\s*(\d+)\s+of\s+(\d+)\s*[—-]\s*(.+)$/m
|
|
60
64
|
);
|
|
65
|
+
if (!phaseMatch) {
|
|
66
|
+
schema_errors.push({
|
|
67
|
+
field: "phase_header",
|
|
68
|
+
message: 'Missing or malformed "Phase: N of M — Name" header',
|
|
69
|
+
severity: "error",
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Status field presence (independent of value)
|
|
74
|
+
if (!hasField("Status")) {
|
|
75
|
+
schema_errors.push({
|
|
76
|
+
field: "status_field",
|
|
77
|
+
message: "Missing Status: field",
|
|
78
|
+
severity: "warning",
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
61
82
|
// Parse roadmap table
|
|
62
83
|
const phases = [];
|
|
84
|
+
const tableHeaderRe = /\| # \| Phase \| Goal \| Status \|/;
|
|
63
85
|
const tableMatch = content.match(
|
|
64
86
|
/\| # \| Phase \| Goal \| Status \|\n\|[-|]+\|\n([\s\S]*?)(?=\n##|\n$|$)/
|
|
65
87
|
);
|
|
66
|
-
if (
|
|
88
|
+
if (!tableHeaderRe.test(content)) {
|
|
89
|
+
schema_errors.push({
|
|
90
|
+
field: "roadmap_table",
|
|
91
|
+
message: "Roadmap table header not found",
|
|
92
|
+
severity: "error",
|
|
93
|
+
});
|
|
94
|
+
} else if (!tableMatch) {
|
|
95
|
+
// Header is there but the separator row or body is malformed
|
|
96
|
+
schema_errors.push({
|
|
97
|
+
field: "roadmap_table",
|
|
98
|
+
message: "Roadmap table is malformed (missing separator row or body)",
|
|
99
|
+
severity: "error",
|
|
100
|
+
});
|
|
101
|
+
} else {
|
|
67
102
|
for (const row of tableMatch[1].trim().split("\n")) {
|
|
68
103
|
const cols = row.split("|").map((c) => c.trim()).filter(Boolean);
|
|
69
104
|
if (cols.length >= 4) {
|
|
@@ -76,6 +111,19 @@ function parseStateMd(content) {
|
|
|
76
111
|
}
|
|
77
112
|
}
|
|
78
113
|
}
|
|
114
|
+
|
|
115
|
+
// Row count vs header "of M"
|
|
116
|
+
if (phaseMatch) {
|
|
117
|
+
const declaredTotal = parseInt(phaseMatch[2]);
|
|
118
|
+
if (phases.length && phases.length !== declaredTotal) {
|
|
119
|
+
schema_errors.push({
|
|
120
|
+
field: "roadmap_rows",
|
|
121
|
+
message: `Expected ${declaredTotal} phases in roadmap, found ${phases.length}`,
|
|
122
|
+
severity: "warning",
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
79
127
|
return {
|
|
80
128
|
phase: phaseMatch ? parseInt(phaseMatch[1]) : 1,
|
|
81
129
|
total_phases: phaseMatch ? parseInt(phaseMatch[2]) : phases.length || 1,
|
|
@@ -83,6 +131,7 @@ function parseStateMd(content) {
|
|
|
83
131
|
status: get("Status").toLowerCase().replace(/\s+/g, "_") || "setup",
|
|
84
132
|
assigned_to: get("Assigned to") || "",
|
|
85
133
|
phases,
|
|
134
|
+
schema_errors,
|
|
86
135
|
};
|
|
87
136
|
}
|
|
88
137
|
|
|
@@ -254,6 +303,7 @@ function cmdCheck(opts) {
|
|
|
254
303
|
s.total_phases,
|
|
255
304
|
t.verification
|
|
256
305
|
),
|
|
306
|
+
schema_errors: s.schema_errors && s.schema_errors.length ? s.schema_errors : undefined,
|
|
257
307
|
});
|
|
258
308
|
}
|
|
259
309
|
|
|
@@ -269,6 +319,16 @@ function cmdTransition(opts) {
|
|
|
269
319
|
);
|
|
270
320
|
}
|
|
271
321
|
|
|
322
|
+
// Refuse transitions if STATE.md has schema errors (severity=error)
|
|
323
|
+
if (s.schema_errors && s.schema_errors.some((e) => e.severity === "error")) {
|
|
324
|
+
return output(
|
|
325
|
+
fail(
|
|
326
|
+
"STATE_SCHEMA_ERROR",
|
|
327
|
+
"STATE.md is malformed. Run `node state.js check` to see errors. Consider `state.js fix` to rewrite canonically."
|
|
328
|
+
)
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
|
|
272
332
|
// Special: note/activity (no status change)
|
|
273
333
|
if (target === "note" || target === "activity") {
|
|
274
334
|
const now = new Date().toISOString().split("T")[0];
|
|
@@ -466,6 +526,77 @@ function cmdInit(opts) {
|
|
|
466
526
|
});
|
|
467
527
|
}
|
|
468
528
|
|
|
529
|
+
function cmdFix(opts) {
|
|
530
|
+
const raw = readState();
|
|
531
|
+
const t = readTracking();
|
|
532
|
+
if (!raw && !t) {
|
|
533
|
+
return output(
|
|
534
|
+
fail("NO_PROJECT", "No .planning/ found. Run /qualia-new.")
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
const parsed = parseStateMd(raw) || {
|
|
538
|
+
phase: 1,
|
|
539
|
+
total_phases: 1,
|
|
540
|
+
phase_name: "",
|
|
541
|
+
status: "setup",
|
|
542
|
+
assigned_to: "",
|
|
543
|
+
phases: [],
|
|
544
|
+
schema_errors: [
|
|
545
|
+
{ field: "content", message: "STATE.md missing or empty", severity: "error" },
|
|
546
|
+
],
|
|
547
|
+
};
|
|
548
|
+
const previousErrors = (parsed.schema_errors || []).length;
|
|
549
|
+
|
|
550
|
+
// Prefer tracking.json values when parsed fields are defaulted/missing
|
|
551
|
+
const tr = t || {};
|
|
552
|
+
const totalPhases =
|
|
553
|
+
parseInt(tr.total_phases) || parsed.total_phases || parsed.phases.length || 1;
|
|
554
|
+
const phaseNum = parseInt(tr.phase) || parsed.phase || 1;
|
|
555
|
+
const phaseName =
|
|
556
|
+
(parsed.phase_name && parsed.phase_name.trim()) ||
|
|
557
|
+
tr.phase_name ||
|
|
558
|
+
`Phase ${phaseNum}`;
|
|
559
|
+
const status = parsed.status || tr.status || "setup";
|
|
560
|
+
const assignedTo = parsed.assigned_to || tr.assigned_to || "";
|
|
561
|
+
|
|
562
|
+
// Build a phases array of the right length
|
|
563
|
+
const phases = [];
|
|
564
|
+
for (let i = 0; i < totalPhases; i++) {
|
|
565
|
+
const existing = parsed.phases[i];
|
|
566
|
+
phases.push({
|
|
567
|
+
num: i + 1,
|
|
568
|
+
name: existing?.name || `Phase ${i + 1}`,
|
|
569
|
+
goal: existing?.goal || "TBD",
|
|
570
|
+
status: existing?.status || (i === 0 ? "ready" : "—"),
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
const s = {
|
|
575
|
+
phase: phaseNum,
|
|
576
|
+
total_phases: totalPhases,
|
|
577
|
+
phase_name: phaseName,
|
|
578
|
+
status,
|
|
579
|
+
assigned_to: assignedTo,
|
|
580
|
+
last_activity: "STATE.md repaired by state.js fix",
|
|
581
|
+
phases,
|
|
582
|
+
blockers: "None.",
|
|
583
|
+
resume: "—",
|
|
584
|
+
};
|
|
585
|
+
|
|
586
|
+
try {
|
|
587
|
+
writeStateMd(s);
|
|
588
|
+
} catch (e) {
|
|
589
|
+
return output(fail("WRITE_ERROR", e.message));
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
output({
|
|
593
|
+
ok: true,
|
|
594
|
+
action: "fix",
|
|
595
|
+
previous_errors: previousErrors,
|
|
596
|
+
fixed: true,
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
|
|
469
600
|
// ─── Output ──────────────────────────────────────────────
|
|
470
601
|
function output(obj) {
|
|
471
602
|
console.log(JSON.stringify(obj, null, 2));
|
|
@@ -486,11 +617,14 @@ switch (cmd) {
|
|
|
486
617
|
case "init":
|
|
487
618
|
cmdInit(opts);
|
|
488
619
|
break;
|
|
620
|
+
case "fix":
|
|
621
|
+
cmdFix(opts);
|
|
622
|
+
break;
|
|
489
623
|
default:
|
|
490
624
|
output(
|
|
491
625
|
fail(
|
|
492
626
|
"UNKNOWN_COMMAND",
|
|
493
|
-
`Usage: state.js <check|transition|init> [--options]`
|
|
627
|
+
`Usage: state.js <check|transition|init|fix> [--options]`
|
|
494
628
|
)
|
|
495
629
|
);
|
|
496
630
|
}
|
package/bin/statusline.js
CHANGED
|
@@ -11,6 +11,7 @@ const fs = require("fs");
|
|
|
11
11
|
const os = require("os");
|
|
12
12
|
const path = require("path");
|
|
13
13
|
const { spawnSync } = require("child_process");
|
|
14
|
+
const HOME = os.homedir();
|
|
14
15
|
|
|
15
16
|
// ─── Colors (matches bin/qualia-ui.js palette) ───────────
|
|
16
17
|
const TEAL = "\x1b[38;2;0;206;209m";
|
|
@@ -151,6 +152,47 @@ try {
|
|
|
151
152
|
}
|
|
152
153
|
} catch {}
|
|
153
154
|
|
|
155
|
+
// ─── Memory count ────────────────────────────────────────
|
|
156
|
+
let MEMORY_COUNT = 0;
|
|
157
|
+
try {
|
|
158
|
+
const dirKey = DIR.replace(/\//g, "-");
|
|
159
|
+
const memDir = path.join(HOME, ".claude", "projects", dirKey, "memory");
|
|
160
|
+
if (fs.existsSync(memDir)) {
|
|
161
|
+
const files = fs.readdirSync(memDir).filter(f => f.endsWith(".md") && f !== "MEMORY.md");
|
|
162
|
+
MEMORY_COUNT = files.length;
|
|
163
|
+
}
|
|
164
|
+
} catch {}
|
|
165
|
+
|
|
166
|
+
// ─── Hooks count ─────────────────────────────────────────
|
|
167
|
+
let HOOKS_COUNT = 0;
|
|
168
|
+
try {
|
|
169
|
+
const settingsPath = path.join(HOME, ".claude", "settings.json");
|
|
170
|
+
if (fs.existsSync(settingsPath)) {
|
|
171
|
+
const settings = JSON.parse(fs.readFileSync(settingsPath, "utf8"));
|
|
172
|
+
if (settings.hooks) {
|
|
173
|
+
for (const event of Object.values(settings.hooks)) {
|
|
174
|
+
if (Array.isArray(event)) {
|
|
175
|
+
for (const matcher of event) {
|
|
176
|
+
if (matcher.hooks && Array.isArray(matcher.hooks)) {
|
|
177
|
+
HOOKS_COUNT += matcher.hooks.length;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
} catch {}
|
|
185
|
+
|
|
186
|
+
// ─── Skills count ────────────────────────────────────────
|
|
187
|
+
let SKILLS_COUNT = 0;
|
|
188
|
+
try {
|
|
189
|
+
const skillsDir = path.join(HOME, ".claude", "skills");
|
|
190
|
+
if (fs.existsSync(skillsDir)) {
|
|
191
|
+
const entries = fs.readdirSync(skillsDir, { withFileTypes: true });
|
|
192
|
+
SKILLS_COUNT = entries.filter(e => e.isDirectory() || e.name.endsWith(".md")).length;
|
|
193
|
+
}
|
|
194
|
+
} catch {}
|
|
195
|
+
|
|
154
196
|
// ─── Duration ────────────────────────────────────────────
|
|
155
197
|
let DUR = "0s";
|
|
156
198
|
try {
|
|
@@ -167,11 +209,11 @@ try {
|
|
|
167
209
|
COST_FMT = `$${COST.toFixed(2)}`;
|
|
168
210
|
} catch {}
|
|
169
211
|
|
|
170
|
-
// ─── Line 1: Project + Git + Agent + Worktree + Phase
|
|
212
|
+
// ─── Line 1: Project + Git + Agent + Worktree + Phase + Memory + Hooks ──
|
|
171
213
|
let LINE1 = "";
|
|
172
214
|
try {
|
|
173
215
|
const dirBase = path.basename(DIR) || DIR;
|
|
174
|
-
LINE1 = `${TEAL}
|
|
216
|
+
LINE1 = `${TEAL}⬢${RESET} ${WHITE}${dirBase}${RESET}`;
|
|
175
217
|
if (BRANCH) {
|
|
176
218
|
if (CHANGES > 0) {
|
|
177
219
|
LINE1 += ` ${DIM}on${RESET} ${TEAL_GLOW}${BRANCH}${RESET} ${YELLOW}~${CHANGES}${RESET}`;
|
|
@@ -182,8 +224,16 @@ try {
|
|
|
182
224
|
if (AGENT) LINE1 += ` ${DIM}│${RESET} ${TEAL}⚡${AGENT}${RESET}`;
|
|
183
225
|
if (WORKTREE) LINE1 += ` ${DIM}│${RESET} ${TEAL_DIM}⎇ ${WORKTREE}${RESET}`;
|
|
184
226
|
if (PHASE_INFO) LINE1 += ` ${DIM}│${RESET} ${PHASE_INFO}`;
|
|
227
|
+
// Memory, hooks, skills — context indicators
|
|
228
|
+
const contextParts = [];
|
|
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
|
+
if (contextParts.length > 0) {
|
|
233
|
+
LINE1 += ` ${DIM}│${RESET} ${contextParts.join(` ${DIM}·${RESET} `)}`;
|
|
234
|
+
}
|
|
185
235
|
} catch {
|
|
186
|
-
LINE1 = `${TEAL}
|
|
236
|
+
LINE1 = `${TEAL}⬢${RESET} ${WHITE}qualia${RESET}`;
|
|
187
237
|
}
|
|
188
238
|
|
|
189
239
|
// ─── Line 2: Context bar + Cost + Duration + Model ───────
|
package/hooks/migration-guard.js
CHANGED
|
@@ -48,7 +48,7 @@ if (/CREATE\s+TABLE/i.test(content) && !/ENABLE\s+ROW\s+LEVEL\s+SECURITY/i.test(
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
if (errors.length > 0) {
|
|
51
|
-
console.log("
|
|
51
|
+
console.log("⬢ Migration guard — dangerous patterns found:");
|
|
52
52
|
for (const e of errors) {
|
|
53
53
|
console.log(` ✗ ${e}`);
|
|
54
54
|
}
|
package/hooks/pre-deploy-gate.js
CHANGED
|
@@ -73,7 +73,7 @@ function scanServiceRoleLeaks() {
|
|
|
73
73
|
return leaks;
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
console.log("
|
|
76
|
+
console.log("⬢ Pre-deploy gate...");
|
|
77
77
|
|
|
78
78
|
// TypeScript
|
|
79
79
|
if (fs.existsSync("tsconfig.json")) {
|
|
@@ -105,6 +105,6 @@ if (leaks.length > 0) {
|
|
|
105
105
|
process.exit(1);
|
|
106
106
|
}
|
|
107
107
|
console.log(" ✓ Security");
|
|
108
|
-
console.log("
|
|
108
|
+
console.log("⬢ All gates passed.");
|
|
109
109
|
|
|
110
110
|
process.exit(0);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "qualia-framework-v2",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.10.0",
|
|
4
4
|
"description": "Claude Code workflow framework by Qualia Solutions. Plan, build, verify, ship.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"qualia-framework-v2": "./bin/cli.js"
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
},
|
|
24
24
|
"homepage": "https://github.com/Qualiasolutions/qualia-framework-v2#readme",
|
|
25
25
|
"scripts": {
|
|
26
|
-
"test": "bash tests/hooks.test.sh && bash tests/state.test.sh"
|
|
26
|
+
"test": "bash tests/hooks.test.sh && bash tests/state.test.sh && bash tests/bin.test.sh && bash tests/statusline.test.sh"
|
|
27
27
|
},
|
|
28
28
|
"files": [
|
|
29
29
|
"bin/",
|
|
@@ -100,7 +100,7 @@ options:
|
|
|
100
100
|
description: "Gradients, rounded shapes, vibrant palette"
|
|
101
101
|
preview: |
|
|
102
102
|
┌──────────────────────────────┐
|
|
103
|
-
│
|
|
103
|
+
│ ⬢ ● ▲ ■ COLORFUL │
|
|
104
104
|
│ │
|
|
105
105
|
│ ╭──────╮ ╭──────╮ │
|
|
106
106
|
│ │ Card │ │ Card │ │
|
|
@@ -173,7 +173,7 @@ If there's an entry for this client, show it to the user: *"I have notes on {cli
|
|
|
173
173
|
Present a summary:
|
|
174
174
|
|
|
175
175
|
```
|
|
176
|
-
|
|
176
|
+
⬢ QUALIA ▸ PROJECT SUMMARY
|
|
177
177
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
178
178
|
|
|
179
179
|
Project {name}
|