qualia-framework-v2 2.9.0 → 3.0.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.
@@ -10,11 +10,30 @@ const path = require("path");
10
10
  const os = require("os");
11
11
  const { spawnSync } = require("child_process");
12
12
 
13
+ const _traceStart = Date.now();
14
+
13
15
  const CONFIG = path.join(os.homedir(), ".claude", ".qualia-config.json");
14
16
 
17
+ function _trace(hookName, result, extra) {
18
+ try {
19
+ const traceDir = path.join(os.homedir(), ".claude", ".qualia-traces");
20
+ if (!fs.existsSync(traceDir)) fs.mkdirSync(traceDir, { recursive: true });
21
+ const entry = {
22
+ hook: hookName,
23
+ result,
24
+ timestamp: new Date().toISOString(),
25
+ duration_ms: Date.now() - _traceStart,
26
+ ...extra,
27
+ };
28
+ const file = path.join(traceDir, `${new Date().toISOString().split("T")[0]}.jsonl`);
29
+ fs.appendFileSync(file, JSON.stringify(entry) + "\n");
30
+ } catch {}
31
+ }
32
+
15
33
  function fail(msg) {
16
34
  console.log(msg);
17
- process.exit(1);
35
+ _trace("branch-guard", "block", { reason: msg });
36
+ process.exit(2);
18
37
  }
19
38
 
20
39
  let role = "";
@@ -40,8 +59,10 @@ if (branch === "main" || branch === "master") {
40
59
  if (role !== "OWNER") {
41
60
  console.log(`BLOCKED: Employees cannot push to ${branch}. Create a feature branch first.`);
42
61
  console.log("Run: git checkout -b feature/your-feature-name");
43
- process.exit(1);
62
+ _trace("branch-guard", "block", { reason: `non-owner push to ${branch}` });
63
+ process.exit(2);
44
64
  }
45
65
  }
46
66
 
67
+ _trace("branch-guard", "allow");
47
68
  process.exit(0);
@@ -6,6 +6,8 @@
6
6
 
7
7
  const fs = require("fs");
8
8
 
9
+ const _traceStart = Date.now();
10
+
9
11
  function readInput() {
10
12
  try {
11
13
  const raw = fs.readFileSync(0, "utf8");
@@ -20,8 +22,27 @@ const ti = input.tool_input || {};
20
22
  const file = String(ti.file_path || "").replace(/\\/g, "/");
21
23
  const content = String(ti.content || ti.new_string || "");
22
24
 
25
+ function _trace(hookName, result, extra) {
26
+ try {
27
+ const os = require("os");
28
+ const path = require("path");
29
+ const traceDir = path.join(os.homedir(), ".claude", ".qualia-traces");
30
+ if (!fs.existsSync(traceDir)) fs.mkdirSync(traceDir, { recursive: true });
31
+ const entry = {
32
+ hook: hookName,
33
+ result,
34
+ timestamp: new Date().toISOString(),
35
+ duration_ms: Date.now() - _traceStart,
36
+ ...extra,
37
+ };
38
+ const filePath = path.join(traceDir, `${new Date().toISOString().split("T")[0]}.jsonl`);
39
+ fs.appendFileSync(filePath, JSON.stringify(entry) + "\n");
40
+ } catch {}
41
+ }
42
+
23
43
  // Only inspect migration/SQL files
24
44
  if (!/migration|migrate|\.sql$/i.test(file)) {
45
+ _trace("migration-guard", "allow", { reason: "non-migration file" });
25
46
  process.exit(0);
26
47
  }
27
48
 
@@ -48,13 +69,15 @@ if (/CREATE\s+TABLE/i.test(content) && !/ENABLE\s+ROW\s+LEVEL\s+SECURITY/i.test(
48
69
  }
49
70
 
50
71
  if (errors.length > 0) {
51
- console.log(" Migration guard — dangerous patterns found:");
72
+ console.log(" Migration guard — dangerous patterns found:");
52
73
  for (const e of errors) {
53
74
  console.log(` ✗ ${e}`);
54
75
  }
55
76
  console.log("");
56
77
  console.log("Fix these before proceeding. If intentional, ask Fawzi to approve.");
78
+ _trace("migration-guard", "block", { errors });
57
79
  process.exit(2);
58
80
  }
59
81
 
82
+ _trace("migration-guard", "allow");
60
83
  process.exit(0);
@@ -7,6 +7,8 @@ const fs = require("fs");
7
7
  const path = require("path");
8
8
  const { spawnSync } = require("child_process");
9
9
 
10
+ const _traceStart = Date.now();
11
+
10
12
  const STATE_FILE = path.join(".planning", "STATE.md");
11
13
 
12
14
  try {
@@ -29,4 +31,22 @@ try {
29
31
  // Silent — never block compaction
30
32
  }
31
33
 
34
+ function _trace(hookName, result, extra) {
35
+ try {
36
+ const os = require("os");
37
+ const traceDir = path.join(os.homedir(), ".claude", ".qualia-traces");
38
+ if (!fs.existsSync(traceDir)) fs.mkdirSync(traceDir, { recursive: true });
39
+ const entry = {
40
+ hook: hookName,
41
+ result,
42
+ timestamp: new Date().toISOString(),
43
+ duration_ms: Date.now() - _traceStart,
44
+ ...extra,
45
+ };
46
+ const file = path.join(traceDir, `${new Date().toISOString().split("T")[0]}.jsonl`);
47
+ fs.appendFileSync(file, JSON.stringify(entry) + "\n");
48
+ } catch {}
49
+ }
50
+
51
+ _trace("pre-compact", "allow");
32
52
  process.exit(0);
@@ -9,6 +9,25 @@ const fs = require("fs");
9
9
  const path = require("path");
10
10
  const { spawnSync } = require("child_process");
11
11
 
12
+ const _traceStart = Date.now();
13
+
14
+ function _trace(hookName, result, extra) {
15
+ try {
16
+ const os = require("os");
17
+ const traceDir = path.join(os.homedir(), ".claude", ".qualia-traces");
18
+ if (!fs.existsSync(traceDir)) fs.mkdirSync(traceDir, { recursive: true });
19
+ const entry = {
20
+ hook: hookName,
21
+ result,
22
+ timestamp: new Date().toISOString(),
23
+ duration_ms: Date.now() - _traceStart,
24
+ ...extra,
25
+ };
26
+ const file = path.join(traceDir, `${new Date().toISOString().split("T")[0]}.jsonl`);
27
+ fs.appendFileSync(file, JSON.stringify(entry) + "\n");
28
+ } catch {}
29
+ }
30
+
12
31
  function runGate(label, cmd, args, { required = true } = {}) {
13
32
  const r = spawnSync(cmd, args, {
14
33
  stdio: "ignore",
@@ -21,6 +40,7 @@ function runGate(label, cmd, args, { required = true } = {}) {
21
40
  }
22
41
  if (required) {
23
42
  console.log(`BLOCKED: ${label} errors. Fix before deploying.`);
43
+ _trace("pre-deploy-gate", "block", { gate: label });
24
44
  process.exit(1);
25
45
  }
26
46
  return false;
@@ -60,10 +80,27 @@ function scanServiceRoleLeaks() {
60
80
  const leaks = [];
61
81
  for (const root of roots) {
62
82
  for (const file of walk(root)) {
83
+ // --- Path-based skips (no I/O needed) ---
84
+
63
85
  // Skip server-only files (convention: *.server.ts, server/ dirs)
64
86
  if (/\.server\.|[\\/]server[\\/]/.test(file)) continue;
87
+
88
+ // Skip App Router route handlers (always server-side)
89
+ if (/[\\/]route\.(ts|tsx|js|jsx|mjs)$/.test(file)) continue;
90
+
91
+ // Skip middleware (always server-side)
92
+ if (/[\\/]middleware\.(ts|tsx|js|jsx|mjs)$/.test(file)) continue;
93
+
94
+ // Skip files in app/api/ directory (always server-side)
95
+ if (/[\\/]app[\\/]api[\\/]/.test(file)) continue;
96
+
97
+ // --- Content-based checks (requires reading file) ---
65
98
  try {
66
99
  const content = fs.readFileSync(file, "utf8");
100
+
101
+ // Skip files with "use server" directive (Server Actions / Server Components)
102
+ if (/^["']use server["']/m.test(content)) continue;
103
+
67
104
  if (/service_role/.test(content)) {
68
105
  leaks.push(file);
69
106
  }
@@ -73,7 +110,7 @@ function scanServiceRoleLeaks() {
73
110
  return leaks;
74
111
  }
75
112
 
76
- console.log(" Pre-deploy gate...");
113
+ console.log(" Pre-deploy gate...");
77
114
 
78
115
  // TypeScript
79
116
  if (fs.existsSync("tsconfig.json")) {
@@ -102,9 +139,11 @@ if (leaks.length > 0) {
102
139
  for (const f of leaks.slice(0, 10)) {
103
140
  console.log(` ✗ ${f}`);
104
141
  }
142
+ _trace("pre-deploy-gate", "block", { gate: "security", leaks: leaks.slice(0, 10) });
105
143
  process.exit(1);
106
144
  }
107
145
  console.log(" ✓ Security");
108
- console.log(" All gates passed.");
146
+ console.log(" All gates passed.");
109
147
 
148
+ _trace("pre-deploy-gate", "allow");
110
149
  process.exit(0);
package/hooks/pre-push.js CHANGED
@@ -8,6 +8,8 @@ const fs = require("fs");
8
8
  const path = require("path");
9
9
  const { spawnSync } = require("child_process");
10
10
 
11
+ const _traceStart = Date.now();
12
+
11
13
  const TRACKING = path.join(".planning", "tracking.json");
12
14
 
13
15
  try {
@@ -30,4 +32,22 @@ try {
30
32
  process.stderr.write(`WARNING: tracking sync failed: ${err.message}\n`);
31
33
  }
32
34
 
35
+ function _trace(hookName, result, extra) {
36
+ try {
37
+ const os = require("os");
38
+ const traceDir = path.join(os.homedir(), ".claude", ".qualia-traces");
39
+ if (!fs.existsSync(traceDir)) fs.mkdirSync(traceDir, { recursive: true });
40
+ const entry = {
41
+ hook: hookName,
42
+ result,
43
+ timestamp: new Date().toISOString(),
44
+ duration_ms: Date.now() - _traceStart,
45
+ ...extra,
46
+ };
47
+ const file = path.join(traceDir, `${new Date().toISOString().split("T")[0]}.jsonl`);
48
+ fs.appendFileSync(file, JSON.stringify(entry) + "\n");
49
+ } catch {}
50
+ }
51
+
52
+ _trace("pre-push", "allow");
33
53
  process.exit(0);
@@ -12,6 +12,8 @@ const path = require("path");
12
12
  const os = require("os");
13
13
  const { spawnSync } = require("child_process");
14
14
 
15
+ const _traceStart = Date.now();
16
+
15
17
  const HOME = os.homedir();
16
18
  const UI = path.join(HOME, ".claude", "bin", "qualia-ui.js");
17
19
  const STATE_FILE = path.join(".planning", "STATE.md");
@@ -81,4 +83,21 @@ try {
81
83
  // Deliberately silent — hook must never fail
82
84
  }
83
85
 
86
+ function _trace(hookName, result, extra) {
87
+ try {
88
+ const traceDir = path.join(os.homedir(), ".claude", ".qualia-traces");
89
+ if (!fs.existsSync(traceDir)) fs.mkdirSync(traceDir, { recursive: true });
90
+ const entry = {
91
+ hook: hookName,
92
+ result,
93
+ timestamp: new Date().toISOString(),
94
+ duration_ms: Date.now() - _traceStart,
95
+ ...extra,
96
+ };
97
+ const file = path.join(traceDir, `${new Date().toISOString().split("T")[0]}.jsonl`);
98
+ fs.appendFileSync(file, JSON.stringify(entry) + "\n");
99
+ } catch {}
100
+ }
101
+
102
+ _trace("session-start", "allow");
84
103
  process.exit(0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qualia-framework-v2",
3
- "version": "2.9.0",
3
+ "version": "3.0.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"
@@ -47,6 +47,7 @@ Use the state.js JSON output plus gathered context:
47
47
  | `handed-off` | status == "handed_off" | → `/qualia-report` then done |
48
48
  | `blocked` | STATE.md lists blockers or same error 3+ times | → Analyze, suggest `/qualia-debug` |
49
49
  | `bug-loop` | Same files edited 3+ times, user frustrated | → Different approach, `/qualia-debug` |
50
+ | `need-tests` | User mentions "tests", "coverage", "test this" | → `/qualia-test` |
50
51
 
51
52
  **Employee escalation:** If role is EMPLOYEE and situation is `gap-limit` or `bug-loop`, suggest: "Want to flag this for Fawzi?"
52
53
 
@@ -21,6 +21,24 @@ cat .planning/phase-{N}-plan.md
21
21
 
22
22
  Parse: tasks, waves, file references.
23
23
 
24
+ ### 1b. Create Recovery Point
25
+
26
+ Before executing any tasks, tag current HEAD for rollback:
27
+
28
+ ```bash
29
+ git tag -f "pre-build-phase-{N}" HEAD 2>/dev/null
30
+ ```
31
+
32
+ ```bash
33
+ node ~/.claude/bin/qualia-ui.js info "Recovery point: pre-build-phase-{N}"
34
+ ```
35
+
36
+ If a wave fails and the user needs to roll back:
37
+ ```bash
38
+ git reset --hard pre-build-phase-{N}
39
+ node ~/.claude/bin/state.js transition --to planned --force
40
+ ```
41
+
24
42
  ### 2. Execute Waves
25
43
 
26
44
  ```bash
@@ -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:
@@ -46,17 +46,40 @@ What did you learn?
46
46
  3. Client preference — client-specific requirement
47
47
  ```
48
48
 
49
- ### 2. Format Entry
49
+ ### 2. Check for Duplicates
50
+
51
+ Before saving, check if a similar entry already exists:
52
+
53
+ ```bash
54
+ # Search for the title (case-insensitive substring match)
55
+ grep -i "{title keywords}" ~/.claude/knowledge/{type}.md 2>/dev/null
56
+ ```
57
+
58
+ If a near-match exists (title is similar to an existing entry):
59
+ - Show the existing entry to the user
60
+ - Ask: "A similar entry exists. Update it, create a new one, or skip?"
61
+ - If update: replace the existing entry. If new: append. If skip: done.
62
+
63
+ ### 3. Format Entry
64
+
65
+ Each entry gets a unique ID and ISO timestamp for dedup and ordering:
50
66
 
51
67
  ```markdown
52
- ### {Title} ({date})
68
+
69
+ ---
70
+
71
+ ### {Title}
72
+ **ID:** {random 8-char hex, e.g. a3f7c1e9}
73
+ **Date:** {ISO 8601, e.g. 2026-04-11}
53
74
  **Project:** {current project name or "general"}
54
75
  **Context:** {brief context — what you were building when you learned this}
55
76
 
56
77
  {The learning — be specific enough that future-you understands without context}
57
78
  ```
58
79
 
59
- ### 3. Append to Knowledge File
80
+ ### 4. Append to Knowledge File
81
+
82
+ Append-only — never overwrite the file, always add at the end:
60
83
 
61
84
  ```bash
62
85
  # Append to the right file
@@ -67,10 +90,10 @@ echo "{formatted entry}" >> ~/.claude/knowledge/{type}.md
67
90
  - Fix → `~/.claude/knowledge/common-fixes.md`
68
91
  - Client pref → `~/.claude/knowledge/client-prefs.md`
69
92
 
70
- ### 4. Confirm
93
+ ### 5. Confirm
71
94
 
72
95
  ```
73
- Saved to {file}
96
+ Saved to {file}
74
97
  "{title}"
75
98
  ```
76
99
 
@@ -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}