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/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 (tableMatch) {
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}◆${RESET} ${WHITE}${dirBase}${RESET}`;
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}◆${RESET} ${WHITE}qualia${RESET}`;
236
+ LINE1 = `${TEAL}⬢${RESET} ${WHITE}qualia${RESET}`;
187
237
  }
188
238
 
189
239
  // ─── Line 2: Context bar + Cost + Duration + Model ───────
@@ -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(" Migration guard — dangerous patterns found:");
51
+ console.log(" Migration guard — dangerous patterns found:");
52
52
  for (const e of errors) {
53
53
  console.log(` ✗ ${e}`);
54
54
  }
@@ -73,7 +73,7 @@ function scanServiceRoleLeaks() {
73
73
  return leaks;
74
74
  }
75
75
 
76
- console.log(" Pre-deploy gate...");
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(" All gates passed.");
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.8.1",
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/",
@@ -73,7 +73,7 @@ git commit -m "style: design transformation"
73
73
  ```
74
74
 
75
75
  ```
76
- Design Transformation Complete
76
+ Design Transformation Complete
77
77
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
78
78
  Files: {N}
79
79
  Changes:
@@ -70,7 +70,7 @@ echo "{formatted entry}" >> ~/.claude/knowledge/{type}.md
70
70
  ### 4. Confirm
71
71
 
72
72
  ```
73
- Saved to {file}
73
+ Saved to {file}
74
74
  "{title}"
75
75
  ```
76
76
 
@@ -100,7 +100,7 @@ options:
100
100
  description: "Gradients, rounded shapes, vibrant palette"
101
101
  preview: |
102
102
  ┌──────────────────────────────┐
103
- ● ▲ ■ COLORFUL │
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
- QUALIA PROJECT SUMMARY
176
+ QUALIA PROJECT SUMMARY
177
177
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
178
178
 
179
179
  Project {name}
@@ -64,7 +64,7 @@ Every finding:
64
64
  Write to `.planning/REVIEW.md`. CRITICAL or HIGH findings are deploy blockers — `/qualia-ship` checks for them.
65
65
 
66
66
  ```
67
- Review Complete
67
+ Review Complete
68
68
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
69
69
  Critical: {N}
70
70
  High: {N}