qualia-framework-v2 2.6.0 → 2.7.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 CHANGED
@@ -49,10 +49,14 @@ See `guide.md` for the full developer guide.
49
49
 
50
50
  - **19 skills** — slash commands from setup to handoff, plus debugging, design, review, knowledge, session management, and skill authoring
51
51
  - **4 agents** — planner, builder, verifier, qa-browser (each in fresh context)
52
- - **8 hooks** — session start, branch guard, pre-push tracking sync, env protection, migration guard, deploy gate, pre-compact state save, auto-update
52
+ - **8 hooks** — session start, branch guard, pre-push tracking sync, env protection, migration guard, deploy gate, pre-compact state save, auto-update (all Node.js — cross-platform)
53
53
  - **3 rules** — security, frontend, deployment
54
54
  - **5 templates** — tracking.json, state.md, project.md, plan.md, DESIGN.md
55
55
 
56
+ ## Supported Platforms
57
+
58
+ Works on **Windows 10/11, macOS, and Linux** (all distros). The only requirement is Node.js 18+. No Git Bash, no WSL, no bash dependency — every hook is pure Node.js.
59
+
56
60
  ## Why It Works
57
61
 
58
62
  ### Goal-Backward Verification
@@ -94,8 +98,8 @@ npx qualia-framework-v2 install
94
98
  ~/.claude/
95
99
  ├── skills/ 19 slash commands
96
100
  ├── agents/ planner.md, builder.md, verifier.md, qa-browser.md
97
- ├── hooks/ 8 shell scripts (session-start, branch, env, migration, deploy, push, compact, auto-update)
98
- ├── bin/ state.js (state machine with precondition enforcement)
101
+ ├── hooks/ 8 Node.js hooks — cross-platform (no bash dependency)
102
+ ├── bin/ state.js (state machine) + qualia-ui.js (cosmetics library)
99
103
  ├── knowledge/ learned-patterns.md, common-fixes.md, client-prefs.md (auto-loaded by skills)
100
104
  ├── rules/ security.md, frontend.md, deployment.md
101
105
  ├── qualia-templates/ tracking.json, state.md, project.md, plan.md, DESIGN.md
package/bin/install.js CHANGED
@@ -146,10 +146,20 @@ async function main() {
146
146
  const hooksSource = path.join(FRAMEWORK_DIR, "hooks");
147
147
  const hooksDest = path.join(CLAUDE_DIR, "hooks");
148
148
  if (!fs.existsSync(hooksDest)) fs.mkdirSync(hooksDest, { recursive: true });
149
+ // Clean up legacy .sh hooks from previous v2.5/v2.6 installs so no orphans
150
+ // remain on disk after upgrading to the pure-Node v2.7+ hooks.
151
+ try {
152
+ for (const f of fs.readdirSync(hooksDest)) {
153
+ if (f.endsWith(".sh")) {
154
+ try { fs.unlinkSync(path.join(hooksDest, f)); } catch {}
155
+ }
156
+ }
157
+ } catch {}
149
158
  for (const file of fs.readdirSync(hooksSource)) {
150
159
  try {
151
160
  const dest = path.join(hooksDest, file);
152
161
  copy(path.join(hooksSource, file), dest);
162
+ // chmod is a no-op on Windows but harmless
153
163
  fs.chmodSync(dest, 0o755);
154
164
  ok(file);
155
165
  } catch (e) {
@@ -331,8 +341,11 @@ async function main() {
331
341
  ],
332
342
  };
333
343
 
334
- // Hooks — full system
344
+ // Hooks — pure Node.js, cross-platform (Windows/macOS/Linux).
345
+ // Every hook command is `node <absolute-path-to-hook.js>` which avoids the
346
+ // bash/Git Bash requirement on Windows.
335
347
  const hd = path.join(CLAUDE_DIR, "hooks");
348
+ const nodeCmd = (hookFile) => `node "${path.join(hd, hookFile)}"`;
336
349
  settings.hooks = {
337
350
  SessionStart: [
338
351
  {
@@ -340,7 +353,7 @@ async function main() {
340
353
  hooks: [
341
354
  {
342
355
  type: "command",
343
- command: `${hd}/session-start.sh`,
356
+ command: nodeCmd("session-start.js"),
344
357
  timeout: 5,
345
358
  },
346
359
  ],
@@ -352,28 +365,28 @@ async function main() {
352
365
  hooks: [
353
366
  {
354
367
  type: "command",
355
- command: `${hd}/auto-update.sh`,
368
+ command: nodeCmd("auto-update.js"),
356
369
  timeout: 5,
357
370
  },
358
371
  {
359
372
  type: "command",
360
373
  if: "Bash(git push*)",
361
- command: `${hd}/branch-guard.sh`,
374
+ command: nodeCmd("branch-guard.js"),
362
375
  timeout: 10,
363
376
  statusMessage: "◆ Checking branch permissions...",
364
377
  },
365
378
  {
366
379
  type: "command",
367
380
  if: "Bash(git push*)",
368
- command: `${hd}/pre-push.sh`,
381
+ command: nodeCmd("pre-push.js"),
369
382
  timeout: 15,
370
383
  statusMessage: "◆ Syncing tracking...",
371
384
  },
372
385
  {
373
386
  type: "command",
374
387
  if: "Bash(vercel --prod*)",
375
- command: `${hd}/pre-deploy-gate.sh`,
376
- timeout: 120,
388
+ command: nodeCmd("pre-deploy-gate.js"),
389
+ timeout: 180,
377
390
  statusMessage: "◆ Running quality gates...",
378
391
  },
379
392
  ],
@@ -384,14 +397,14 @@ async function main() {
384
397
  {
385
398
  type: "command",
386
399
  if: "Edit(*.env*)|Write(*.env*)",
387
- command: `${hd}/block-env-edit.sh`,
400
+ command: nodeCmd("block-env-edit.js"),
388
401
  timeout: 5,
389
402
  statusMessage: "◆ Checking file permissions...",
390
403
  },
391
404
  {
392
405
  type: "command",
393
406
  if: "Edit(*migration*)|Write(*migration*)|Edit(*.sql)|Write(*.sql)",
394
- command: `${hd}/migration-guard.sh`,
407
+ command: nodeCmd("migration-guard.js"),
395
408
  timeout: 10,
396
409
  statusMessage: "◆ Checking migration safety...",
397
410
  },
@@ -404,7 +417,7 @@ async function main() {
404
417
  hooks: [
405
418
  {
406
419
  type: "command",
407
- command: `${hd}/pre-compact.sh`,
420
+ command: nodeCmd("pre-compact.js"),
408
421
  timeout: 15,
409
422
  statusMessage: "◆ Saving state...",
410
423
  },
@@ -0,0 +1,92 @@
1
+ #!/usr/bin/env node
2
+ // ~/.claude/hooks/auto-update.js — daily silent update check in the background.
3
+ // PreToolUse hook on every Bash tool call. Fast path: single stat() call that
4
+ // returns immediately if last check was <24h ago. Cross-platform.
5
+
6
+ const fs = require("fs");
7
+ const path = require("path");
8
+ const os = require("os");
9
+ const { spawn, spawnSync } = require("child_process");
10
+
11
+ const CLAUDE_DIR = path.join(os.homedir(), ".claude");
12
+ const CACHE_FILE = path.join(CLAUDE_DIR, ".qualia-last-update-check");
13
+ const CONFIG_FILE = path.join(CLAUDE_DIR, ".qualia-config.json");
14
+ const LOCK_FILE = path.join(CLAUDE_DIR, ".qualia-updating");
15
+ const MAX_AGE_MS = 24 * 60 * 60 * 1000;
16
+
17
+ try {
18
+ // Fast path: recently checked
19
+ if (fs.existsSync(CACHE_FILE)) {
20
+ const last = Number(fs.readFileSync(CACHE_FILE, "utf8")) || 0;
21
+ if (Date.now() - last * 1000 < MAX_AGE_MS) {
22
+ process.exit(0);
23
+ }
24
+ }
25
+
26
+ // Already updating
27
+ if (fs.existsSync(LOCK_FILE)) {
28
+ process.exit(0);
29
+ }
30
+
31
+ // Update cache timestamp immediately to debounce concurrent checks
32
+ fs.writeFileSync(CACHE_FILE, String(Math.floor(Date.now() / 1000)));
33
+
34
+ // Read current config
35
+ let cfg = {};
36
+ try {
37
+ cfg = JSON.parse(fs.readFileSync(CONFIG_FILE, "utf8"));
38
+ } catch {
39
+ process.exit(0);
40
+ }
41
+ if (!cfg.code || !cfg.version) process.exit(0);
42
+
43
+ // Fork the check-and-update into a detached background process so the hook
44
+ // returns immediately and Claude Code is never blocked.
45
+ const script = `
46
+ const fs = require("fs");
47
+ const path = require("path");
48
+ const { spawnSync } = require("child_process");
49
+ const CLAUDE_DIR = ${JSON.stringify(CLAUDE_DIR)};
50
+ const LOCK_FILE = ${JSON.stringify(LOCK_FILE)};
51
+ const CONFIG_FILE = ${JSON.stringify(CONFIG_FILE)};
52
+ const cfg = ${JSON.stringify(cfg)};
53
+ try {
54
+ fs.writeFileSync(LOCK_FILE, String(process.pid));
55
+ const r = spawnSync("npm", ["view", "qualia-framework-v2", "version"], {
56
+ encoding: "utf8",
57
+ timeout: 15000,
58
+ shell: process.platform === "win32",
59
+ });
60
+ const latest = ((r.stdout || "").trim());
61
+ if (!latest) { fs.unlinkSync(LOCK_FILE); return; }
62
+ const cmp = (a, b) => {
63
+ const pa = a.split(".").map(Number), pb = b.split(".").map(Number);
64
+ for (let i = 0; i < 3; i++) {
65
+ if ((pa[i]||0) > (pb[i]||0)) return 1;
66
+ if ((pa[i]||0) < (pb[i]||0)) return -1;
67
+ }
68
+ return 0;
69
+ };
70
+ if (cmp(latest, cfg.version) > 0) {
71
+ // Silent update — pipe the install code via stdin
72
+ const child = spawnSync("npx", ["qualia-framework-v2@latest", "install"], {
73
+ input: cfg.code + "\\n",
74
+ timeout: 120000,
75
+ stdio: ["pipe", "ignore", "ignore"],
76
+ shell: process.platform === "win32",
77
+ });
78
+ }
79
+ } catch {}
80
+ try { fs.unlinkSync(LOCK_FILE); } catch {}
81
+ `;
82
+
83
+ const child = spawn(process.execPath, ["-e", script], {
84
+ detached: true,
85
+ stdio: "ignore",
86
+ });
87
+ child.unref();
88
+ } catch {
89
+ // Silent — never block the tool call
90
+ }
91
+
92
+ process.exit(0);
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env node
2
+ // ~/.claude/hooks/block-env-edit.js — prevent editing .env files.
3
+ // PreToolUse hook on Edit/Write tool calls. Reads tool input as JSON on stdin.
4
+ // Exits 2 to BLOCK the tool call. Exits 0 to allow it.
5
+ // Cross-platform (Windows/macOS/Linux).
6
+
7
+ const fs = require("fs");
8
+
9
+ function readInput() {
10
+ try {
11
+ const raw = fs.readFileSync(0, "utf8");
12
+ return JSON.parse(raw);
13
+ } catch {
14
+ return {};
15
+ }
16
+ }
17
+
18
+ const input = readInput();
19
+ const file = (input.tool_input && (input.tool_input.file_path || input.tool_input.command)) || "";
20
+
21
+ // Match .env, .env.local, .env.production, .env.*, etc.
22
+ // Normalize separators so Windows paths (C:\project\.env.local) also match.
23
+ const normalized = String(file).replace(/\\/g, "/");
24
+
25
+ if (/\.env(\.|$)/.test(normalized)) {
26
+ console.log("BLOCKED: Cannot edit environment files. Ask Fawzi to update secrets.");
27
+ process.exit(2);
28
+ }
29
+
30
+ process.exit(0);
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env node
2
+ // ~/.claude/hooks/branch-guard.js — block non-OWNER push to main/master.
3
+ // PreToolUse hook on `git push*` commands. Reads role from
4
+ // ~/.claude/.qualia-config.json (single source of truth).
5
+ // Exits 1 to BLOCK. Exits 0 to allow.
6
+ // Cross-platform (Windows/macOS/Linux).
7
+
8
+ const fs = require("fs");
9
+ const path = require("path");
10
+ const os = require("os");
11
+ const { spawnSync } = require("child_process");
12
+
13
+ const CONFIG = path.join(os.homedir(), ".claude", ".qualia-config.json");
14
+
15
+ function fail(msg) {
16
+ console.log(msg);
17
+ process.exit(1);
18
+ }
19
+
20
+ let role = "";
21
+ try {
22
+ const cfg = JSON.parse(fs.readFileSync(CONFIG, "utf8"));
23
+ role = cfg.role || "";
24
+ } catch {
25
+ fail(`BLOCKED: ${CONFIG} missing or unreadable. Run: npx qualia-framework-v2 install`);
26
+ }
27
+
28
+ if (!role) {
29
+ fail(`BLOCKED: Cannot determine role from ${CONFIG}. Defaulting to deny.`);
30
+ }
31
+
32
+ // Ask git for the current branch --show-current. Works identically on Windows/macOS/Linux.
33
+ const r = spawnSync("git", ["branch", "--show-current"], {
34
+ encoding: "utf8",
35
+ timeout: 3000,
36
+ });
37
+ const branch = ((r.stdout || "").trim());
38
+
39
+ if (branch === "main" || branch === "master") {
40
+ if (role !== "OWNER") {
41
+ console.log(`BLOCKED: Employees cannot push to ${branch}. Create a feature branch first.`);
42
+ console.log("Run: git checkout -b feature/your-feature-name");
43
+ process.exit(1);
44
+ }
45
+ }
46
+
47
+ process.exit(0);
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env node
2
+ // ~/.claude/hooks/migration-guard.js — catch dangerous SQL patterns in migrations.
3
+ // PreToolUse hook on Edit/Write tool calls. Reads tool input as JSON on stdin.
4
+ // Exits 2 to BLOCK. Exits 0 to allow.
5
+ // Cross-platform (Windows/macOS/Linux).
6
+
7
+ const fs = require("fs");
8
+
9
+ function readInput() {
10
+ try {
11
+ const raw = fs.readFileSync(0, "utf8");
12
+ return JSON.parse(raw);
13
+ } catch {
14
+ return {};
15
+ }
16
+ }
17
+
18
+ const input = readInput();
19
+ const ti = input.tool_input || {};
20
+ const file = String(ti.file_path || "").replace(/\\/g, "/");
21
+ const content = String(ti.content || ti.new_string || "");
22
+
23
+ // Only inspect migration/SQL files
24
+ if (!/migration|migrate|\.sql$/i.test(file)) {
25
+ process.exit(0);
26
+ }
27
+
28
+ const errors = [];
29
+
30
+ // DROP TABLE without IF EXISTS
31
+ if (/DROP\s+TABLE/i.test(content) && !/IF\s+EXISTS/i.test(content)) {
32
+ errors.push("DROP TABLE without IF EXISTS");
33
+ }
34
+
35
+ // DELETE without WHERE
36
+ if (/DELETE\s+FROM/i.test(content) && !/WHERE/i.test(content)) {
37
+ errors.push("DELETE FROM without WHERE clause");
38
+ }
39
+
40
+ // TRUNCATE (almost always wrong in migrations)
41
+ if (/TRUNCATE/i.test(content)) {
42
+ errors.push("TRUNCATE detected — are you sure?");
43
+ }
44
+
45
+ // CREATE TABLE without RLS
46
+ if (/CREATE\s+TABLE/i.test(content) && !/ENABLE\s+ROW\s+LEVEL\s+SECURITY/i.test(content)) {
47
+ errors.push("CREATE TABLE without ENABLE ROW LEVEL SECURITY");
48
+ }
49
+
50
+ if (errors.length > 0) {
51
+ console.log("◆ Migration guard — dangerous patterns found:");
52
+ for (const e of errors) {
53
+ console.log(` ✗ ${e}`);
54
+ }
55
+ console.log("");
56
+ console.log("Fix these before proceeding. If intentional, ask Fawzi to approve.");
57
+ process.exit(2);
58
+ }
59
+
60
+ process.exit(0);
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env node
2
+ // ~/.claude/hooks/pre-compact.js — commit STATE.md before context compaction.
3
+ // PreCompact hook. Silent on failure — context compaction must never be blocked.
4
+ // Cross-platform (Windows/macOS/Linux).
5
+
6
+ const fs = require("fs");
7
+ const path = require("path");
8
+ const { spawnSync } = require("child_process");
9
+
10
+ const STATE_FILE = path.join(".planning", "STATE.md");
11
+
12
+ try {
13
+ if (fs.existsSync(STATE_FILE)) {
14
+ console.log("QUALIA: Saving state before compaction...");
15
+ // Check if STATE.md has uncommitted changes
16
+ const diff = spawnSync("git", ["diff", "--name-only", STATE_FILE], {
17
+ encoding: "utf8",
18
+ timeout: 3000,
19
+ });
20
+ if ((diff.stdout || "").includes("STATE.md")) {
21
+ spawnSync("git", ["add", STATE_FILE], { timeout: 3000 });
22
+ spawnSync("git", ["commit", "-m", "state: pre-compaction save"], {
23
+ timeout: 5000,
24
+ stdio: "ignore",
25
+ });
26
+ }
27
+ }
28
+ } catch {
29
+ // Silent — never block compaction
30
+ }
31
+
32
+ process.exit(0);
@@ -0,0 +1,110 @@
1
+ #!/usr/bin/env node
2
+ // ~/.claude/hooks/pre-deploy-gate.js — quality gates before production deploy.
3
+ // PreToolUse hook on `vercel --prod*` commands. Runs tsc, lint, tests, build,
4
+ // then scans for service_role leaks in client code.
5
+ // Exits 1 to BLOCK deploy. Exits 0 to allow.
6
+ // Cross-platform (Windows/macOS/Linux). No `grep` or `find` — pure Node.
7
+
8
+ const fs = require("fs");
9
+ const path = require("path");
10
+ const { spawnSync } = require("child_process");
11
+
12
+ function runGate(label, cmd, args, { required = true } = {}) {
13
+ const r = spawnSync(cmd, args, {
14
+ stdio: "ignore",
15
+ timeout: 180000,
16
+ shell: process.platform === "win32",
17
+ });
18
+ if (r.status === 0) {
19
+ console.log(` ✓ ${label}`);
20
+ return true;
21
+ }
22
+ if (required) {
23
+ console.log(`BLOCKED: ${label} errors. Fix before deploying.`);
24
+ process.exit(1);
25
+ }
26
+ return false;
27
+ }
28
+
29
+ function hasScript(name) {
30
+ try {
31
+ const pkg = JSON.parse(fs.readFileSync("package.json", "utf8"));
32
+ return pkg.scripts && typeof pkg.scripts[name] === "string";
33
+ } catch {
34
+ return false;
35
+ }
36
+ }
37
+
38
+ function walk(dir, out = []) {
39
+ if (!fs.existsSync(dir)) return out;
40
+ let entries;
41
+ try {
42
+ entries = fs.readdirSync(dir, { withFileTypes: true });
43
+ } catch {
44
+ return out;
45
+ }
46
+ for (const e of entries) {
47
+ if (e.name === "node_modules" || e.name.startsWith(".")) continue;
48
+ const full = path.join(dir, e.name);
49
+ if (e.isDirectory()) {
50
+ walk(full, out);
51
+ } else if (/\.(ts|tsx|js|jsx|mjs|cjs)$/.test(e.name)) {
52
+ out.push(full);
53
+ }
54
+ }
55
+ return out;
56
+ }
57
+
58
+ function scanServiceRoleLeaks() {
59
+ const roots = ["app", "components", "src", "pages", "lib"];
60
+ const leaks = [];
61
+ for (const root of roots) {
62
+ for (const file of walk(root)) {
63
+ // Skip server-only files (convention: *.server.ts, server/ dirs)
64
+ if (/\.server\.|[\\/]server[\\/]/.test(file)) continue;
65
+ try {
66
+ const content = fs.readFileSync(file, "utf8");
67
+ if (/service_role/.test(content)) {
68
+ leaks.push(file);
69
+ }
70
+ } catch {}
71
+ }
72
+ }
73
+ return leaks;
74
+ }
75
+
76
+ console.log("◆ Pre-deploy gate...");
77
+
78
+ // TypeScript
79
+ if (fs.existsSync("tsconfig.json")) {
80
+ runGate("TypeScript", "npx", ["tsc", "--noEmit"]);
81
+ }
82
+
83
+ // Lint
84
+ if (hasScript("lint")) {
85
+ runGate("Lint", "npm", ["run", "lint"]);
86
+ }
87
+
88
+ // Tests
89
+ if (hasScript("test")) {
90
+ runGate("Tests", "npm", ["test"]);
91
+ }
92
+
93
+ // Build
94
+ if (hasScript("build")) {
95
+ runGate("Build", "npm", ["run", "build"]);
96
+ }
97
+
98
+ // Security: no service_role in client code
99
+ const leaks = scanServiceRoleLeaks();
100
+ if (leaks.length > 0) {
101
+ console.log("BLOCKED: service_role found in client code. Remove before deploying.");
102
+ for (const f of leaks.slice(0, 10)) {
103
+ console.log(` ✗ ${f}`);
104
+ }
105
+ process.exit(1);
106
+ }
107
+ console.log(" ✓ Security");
108
+ console.log("◆ All gates passed.");
109
+
110
+ process.exit(0);
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env node
2
+ // ~/.claude/hooks/pre-push.js — update tracking.json with last commit + timestamp.
3
+ // PreToolUse hook on `git push*` commands. state.js handles phase/status sync;
4
+ // this just stamps the file so the ERP sees fresh commit info on every push.
5
+ // Cross-platform (Windows/macOS/Linux).
6
+
7
+ const fs = require("fs");
8
+ const path = require("path");
9
+ const { spawnSync } = require("child_process");
10
+
11
+ const TRACKING = path.join(".planning", "tracking.json");
12
+
13
+ try {
14
+ if (fs.existsSync(TRACKING)) {
15
+ const r = spawnSync("git", ["log", "--oneline", "-1", "--format=%h"], {
16
+ encoding: "utf8",
17
+ timeout: 3000,
18
+ });
19
+ const lastCommit = ((r.stdout || "").trim());
20
+ const now = new Date().toISOString().replace(/\.\d+Z$/, "Z");
21
+
22
+ const t = JSON.parse(fs.readFileSync(TRACKING, "utf8"));
23
+ if (lastCommit) t.last_commit = lastCommit;
24
+ t.last_updated = now;
25
+ fs.writeFileSync(TRACKING, JSON.stringify(t, null, 2) + "\n");
26
+
27
+ spawnSync("git", ["add", TRACKING], { timeout: 3000 });
28
+ }
29
+ } catch (err) {
30
+ process.stderr.write(`WARNING: tracking sync failed: ${err.message}\n`);
31
+ }
32
+
33
+ process.exit(0);
@@ -0,0 +1,84 @@
1
+ #!/usr/bin/env node
2
+ // ~/.claude/hooks/session-start.js — branded context panel on session start.
3
+ // Cross-platform (Windows/macOS/Linux). Zero shell dependencies.
4
+ //
5
+ // CRITICAL: this hook must NEVER exit non-zero. Claude Code treats any non-zero
6
+ // exit from a SessionStart hook as a "hook error" and shows a red banner. The
7
+ // banner is purely informational — we'd rather render nothing than block a
8
+ // session. Every call is wrapped in try/catch and we always exit 0 at the end.
9
+
10
+ const fs = require("fs");
11
+ const path = require("path");
12
+ const os = require("os");
13
+ const { spawnSync } = require("child_process");
14
+
15
+ const HOME = os.homedir();
16
+ const UI = path.join(HOME, ".claude", "bin", "qualia-ui.js");
17
+ const STATE_FILE = path.join(".planning", "STATE.md");
18
+ const CONTINUE_HERE = ".continue-here.md";
19
+
20
+ function runUi(...args) {
21
+ if (!fs.existsSync(UI)) return;
22
+ try {
23
+ spawnSync(process.execPath, [UI, ...args], {
24
+ stdio: "inherit",
25
+ timeout: 3000,
26
+ });
27
+ } catch {}
28
+ }
29
+
30
+ function getNextCommand() {
31
+ const stateJs = path.join(HOME, ".claude", "bin", "state.js");
32
+ if (!fs.existsSync(stateJs)) return "";
33
+ try {
34
+ const r = spawnSync(process.execPath, [stateJs, "check"], {
35
+ encoding: "utf8",
36
+ timeout: 3000,
37
+ });
38
+ if (!r.stdout) return "";
39
+ const j = JSON.parse(r.stdout);
40
+ return j.next_command || "";
41
+ } catch {
42
+ return "";
43
+ }
44
+ }
45
+
46
+ function fallbackText() {
47
+ // If qualia-ui.js is missing, emit plain text. Keeps the session informative
48
+ // even on a broken install.
49
+ if (fs.existsSync(STATE_FILE)) {
50
+ try {
51
+ const content = fs.readFileSync(STATE_FILE, "utf8");
52
+ const phase = (content.match(/^Phase:\s*(.+)$/m) || [])[1] || "—";
53
+ const status = (content.match(/^Status:\s*(.+)$/m) || [])[1] || "—";
54
+ console.log(`QUALIA: Project loaded. Phase: ${phase.trim()} | Status: ${status.trim()}`);
55
+ console.log("QUALIA: Run /qualia for next step.");
56
+ } catch {
57
+ console.log("QUALIA: Project detected but STATE.md could not be read.");
58
+ }
59
+ } else if (fs.existsSync(CONTINUE_HERE)) {
60
+ console.log("QUALIA: Handoff file found. Read .continue-here.md to resume.");
61
+ } else {
62
+ console.log("QUALIA: No project detected. Run /qualia-new to start.");
63
+ }
64
+ }
65
+
66
+ try {
67
+ if (!fs.existsSync(UI)) {
68
+ fallbackText();
69
+ } else if (fs.existsSync(STATE_FILE)) {
70
+ runUi("banner", "router");
71
+ const next = getNextCommand();
72
+ if (next) runUi("info", `Run ${next} to continue`);
73
+ } else if (fs.existsSync(CONTINUE_HERE)) {
74
+ runUi("banner", "router");
75
+ runUi("warn", "Handoff found — read .continue-here.md to resume");
76
+ } else {
77
+ runUi("banner", "router");
78
+ runUi("info", "No project detected. Run /qualia-new to start.");
79
+ }
80
+ } catch {
81
+ // Deliberately silent — hook must never fail
82
+ }
83
+
84
+ process.exit(0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qualia-framework-v2",
3
- "version": "2.6.0",
3
+ "version": "2.7.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"
@@ -1,10 +1,12 @@
1
1
  #!/bin/bash
2
- # Qualia Framework v2 — Hook Tests
2
+ # Qualia Framework v2 — Hook Tests (cross-platform Node.js hooks)
3
3
  # Run: bash tests/hooks.test.sh
4
4
 
5
5
  PASS=0
6
6
  FAIL=0
7
- HOOKS_DIR="$(dirname "$0")/../hooks"
7
+ # Resolve HOOKS_DIR to an ABSOLUTE path so `cd` inside subshells doesn't break it.
8
+ HOOKS_DIR="$(cd "$(dirname "$0")/../hooks" && pwd)"
9
+ NODE="${NODE:-node}"
8
10
 
9
11
  assert_exit() {
10
12
  local name="$1" expected="$2" actual="$3"
@@ -17,139 +19,189 @@ assert_exit() {
17
19
  fi
18
20
  }
19
21
 
20
- echo "=== Hook Tests ==="
22
+ echo "=== Hook Tests (Node.js) ==="
21
23
  echo ""
22
24
 
23
- # --- block-env-edit.sh ---
25
+ # --- All hooks are syntactically valid Node.js ---
26
+ echo "syntax:"
27
+ for f in "$HOOKS_DIR"/*.js; do
28
+ if $NODE -c "$f" 2>/dev/null; then
29
+ echo " ✓ $(basename "$f")"
30
+ PASS=$((PASS + 1))
31
+ else
32
+ echo " ✗ $(basename "$f")"
33
+ FAIL=$((FAIL + 1))
34
+ fi
35
+ done
36
+
37
+ # --- block-env-edit.js ---
38
+ echo ""
24
39
  echo "block-env-edit:"
25
40
 
26
- echo '{"tool_input":{"file_path":".env.local"}}' | bash "$HOOKS_DIR/block-env-edit.sh" > /dev/null 2>&1
41
+ echo '{"tool_input":{"file_path":".env.local"}}' | $NODE "$HOOKS_DIR/block-env-edit.js" > /dev/null 2>&1
27
42
  assert_exit "blocks .env.local" 2 $?
28
43
 
29
- echo '{"tool_input":{"file_path":".env.production"}}' | bash "$HOOKS_DIR/block-env-edit.sh" > /dev/null 2>&1
44
+ echo '{"tool_input":{"file_path":".env.production"}}' | $NODE "$HOOKS_DIR/block-env-edit.js" > /dev/null 2>&1
30
45
  assert_exit "blocks .env.production" 2 $?
31
46
 
32
- echo '{"tool_input":{"file_path":".env"}}' | bash "$HOOKS_DIR/block-env-edit.sh" > /dev/null 2>&1
47
+ echo '{"tool_input":{"file_path":".env"}}' | $NODE "$HOOKS_DIR/block-env-edit.js" > /dev/null 2>&1
33
48
  assert_exit "blocks .env" 2 $?
34
49
 
35
- echo '{"tool_input":{"file_path":"src/app.tsx"}}' | bash "$HOOKS_DIR/block-env-edit.sh" > /dev/null 2>&1
50
+ # Windows-style path with backslashes (normalized by the hook)
51
+ echo '{"tool_input":{"file_path":"C:\\project\\.env.local"}}' | $NODE "$HOOKS_DIR/block-env-edit.js" > /dev/null 2>&1
52
+ assert_exit "blocks windows .env.local" 2 $?
53
+
54
+ echo '{"tool_input":{"file_path":"src/app.tsx"}}' | $NODE "$HOOKS_DIR/block-env-edit.js" > /dev/null 2>&1
36
55
  assert_exit "allows src/app.tsx" 0 $?
37
56
 
38
- echo '{"tool_input":{"file_path":"components/Footer.tsx"}}' | bash "$HOOKS_DIR/block-env-edit.sh" > /dev/null 2>&1
57
+ echo '{"tool_input":{"file_path":"components/Footer.tsx"}}' | $NODE "$HOOKS_DIR/block-env-edit.js" > /dev/null 2>&1
39
58
  assert_exit "allows components/Footer.tsx" 0 $?
40
59
 
41
- # --- migration-guard.sh ---
60
+ # --- migration-guard.js ---
42
61
  echo ""
43
62
  echo "migration-guard:"
44
63
 
45
- echo '{"tool_input":{"file_path":"migrations/001.sql","content":"DROP TABLE users;"}}' | bash "$HOOKS_DIR/migration-guard.sh" > /dev/null 2>&1
64
+ echo '{"tool_input":{"file_path":"migrations/001.sql","content":"DROP TABLE users;"}}' | $NODE "$HOOKS_DIR/migration-guard.js" > /dev/null 2>&1
46
65
  assert_exit "blocks DROP TABLE without IF EXISTS" 2 $?
47
66
 
48
- echo '{"tool_input":{"file_path":"migrations/001.sql","content":"DROP TABLE IF EXISTS old_users;"}}' | bash "$HOOKS_DIR/migration-guard.sh" > /dev/null 2>&1
67
+ echo '{"tool_input":{"file_path":"migrations/001.sql","content":"DROP TABLE IF EXISTS old_users;"}}' | $NODE "$HOOKS_DIR/migration-guard.js" > /dev/null 2>&1
49
68
  assert_exit "allows DROP TABLE IF EXISTS" 0 $?
50
69
 
51
- echo '{"tool_input":{"file_path":"migrations/002.sql","content":"DELETE FROM users;"}}' | bash "$HOOKS_DIR/migration-guard.sh" > /dev/null 2>&1
70
+ echo '{"tool_input":{"file_path":"migrations/002.sql","content":"DELETE FROM users;"}}' | $NODE "$HOOKS_DIR/migration-guard.js" > /dev/null 2>&1
52
71
  assert_exit "blocks DELETE without WHERE" 2 $?
53
72
 
54
- echo '{"tool_input":{"file_path":"migrations/003.sql","content":"TRUNCATE TABLE sessions;"}}' | bash "$HOOKS_DIR/migration-guard.sh" > /dev/null 2>&1
73
+ echo '{"tool_input":{"file_path":"migrations/003.sql","content":"TRUNCATE TABLE sessions;"}}' | $NODE "$HOOKS_DIR/migration-guard.js" > /dev/null 2>&1
55
74
  assert_exit "blocks TRUNCATE" 2 $?
56
75
 
57
- echo '{"tool_input":{"file_path":"migrations/004.sql","content":"CREATE TABLE users (id uuid);"}}' | bash "$HOOKS_DIR/migration-guard.sh" > /dev/null 2>&1
76
+ echo '{"tool_input":{"file_path":"migrations/004.sql","content":"CREATE TABLE users (id uuid);"}}' | $NODE "$HOOKS_DIR/migration-guard.js" > /dev/null 2>&1
58
77
  assert_exit "blocks CREATE TABLE without RLS" 2 $?
59
78
 
60
- echo '{"tool_input":{"file_path":"migrations/005.sql","content":"ALTER TABLE users ADD COLUMN email text;"}}' | bash "$HOOKS_DIR/migration-guard.sh" > /dev/null 2>&1
79
+ echo '{"tool_input":{"file_path":"migrations/005.sql","content":"ALTER TABLE users ADD COLUMN email text;"}}' | $NODE "$HOOKS_DIR/migration-guard.js" > /dev/null 2>&1
61
80
  assert_exit "allows safe ALTER TABLE" 0 $?
62
81
 
63
- echo '{"tool_input":{"file_path":"src/app.tsx","content":"DROP TABLE users;"}}' | bash "$HOOKS_DIR/migration-guard.sh" > /dev/null 2>&1
82
+ echo '{"tool_input":{"file_path":"src/app.tsx","content":"DROP TABLE users;"}}' | $NODE "$HOOKS_DIR/migration-guard.js" > /dev/null 2>&1
64
83
  assert_exit "skips non-migration files" 0 $?
65
84
 
66
- # --- branch-guard.sh ---
85
+ # --- branch-guard.js (grep-based — full run needs git + real config) ---
67
86
  echo ""
68
87
  echo "branch-guard:"
69
88
 
70
- if [ -f "$HOOKS_DIR/branch-guard.sh" ]; then
71
- echo " ✓ branch-guard.sh exists"
89
+ if grep -q '.qualia-config.json' "$HOOKS_DIR/branch-guard.js"; then
90
+ echo " ✓ reads role from .qualia-config.json"
72
91
  PASS=$((PASS + 1))
73
92
  else
74
- echo " ✗ branch-guard.sh missing"
93
+ echo " ✗ not reading from .qualia-config.json"
75
94
  FAIL=$((FAIL + 1))
76
95
  fi
77
96
 
78
- if grep -q 'ROLE' "$HOOKS_DIR/branch-guard.sh"; then
79
- echo " ✓ checks ROLE variable"
97
+ if grep -q 'branch --show-current' "$HOOKS_DIR/branch-guard.js"; then
98
+ echo " ✓ checks current git branch"
80
99
  PASS=$((PASS + 1))
81
100
  else
82
- echo " ✗ missing ROLE check"
101
+ echo " ✗ missing branch check"
83
102
  FAIL=$((FAIL + 1))
84
103
  fi
85
104
 
86
- if grep -q 'z "$ROLE"' "$HOOKS_DIR/branch-guard.sh"; then
87
- echo " ✓ defaults to deny on missing ROLE"
105
+ if grep -q 'OWNER' "$HOOKS_DIR/branch-guard.js"; then
106
+ echo " ✓ enforces OWNER role"
88
107
  PASS=$((PASS + 1))
89
108
  else
90
- echo " ✗ missing empty-ROLE deny"
109
+ echo " ✗ missing OWNER check"
91
110
  FAIL=$((FAIL + 1))
92
111
  fi
93
112
 
94
- # --- pre-push.sh ---
113
+ # --- pre-push.js ---
95
114
  echo ""
96
115
  echo "pre-push:"
97
116
 
98
- if grep -q 'command -v node' "$HOOKS_DIR/pre-push.sh"; then
99
- echo " ✓ checks for node availability"
117
+ if grep -q 'tracking.json' "$HOOKS_DIR/pre-push.js"; then
118
+ echo " ✓ updates tracking.json"
100
119
  PASS=$((PASS + 1))
101
120
  else
102
- echo " ✗ missing node check"
121
+ echo " ✗ missing tracking.json update"
103
122
  FAIL=$((FAIL + 1))
104
123
  fi
105
124
 
106
- if grep -q 'tracking sync failed' "$HOOKS_DIR/pre-push.sh"; then
107
- echo " ✓ captures tracking sync errors"
125
+ if grep -q 'last_commit' "$HOOKS_DIR/pre-push.js"; then
126
+ echo " ✓ stamps last_commit"
108
127
  PASS=$((PASS + 1))
109
128
  else
110
- echo " ✗ missing tracking sync error capture"
129
+ echo " ✗ missing last_commit stamp"
111
130
  FAIL=$((FAIL + 1))
112
131
  fi
113
132
 
114
- # --- branch-guard reads from .qualia-config.json ---
115
- echo ""
116
- echo "branch-guard config source:"
117
-
118
- if grep -q 'qualia-config.json' "$HOOKS_DIR/branch-guard.sh"; then
119
- echo " ✓ reads role from .qualia-config.json"
120
- PASS=$((PASS + 1))
121
- else
122
- echo " ✗ not reading from .qualia-config.json"
123
- FAIL=$((FAIL + 1))
124
- fi
133
+ # Run pre-push.js in a dir with no tracking.json — must exit 0 cleanly
134
+ TMP=$(mktemp -d)
135
+ (cd "$TMP" && $NODE "$HOOKS_DIR/pre-push.js" >/dev/null 2>&1)
136
+ assert_exit "exits 0 with no tracking.json" 0 $?
137
+ rm -rf "$TMP"
125
138
 
126
- # --- pre-deploy-gate.sh ---
139
+ # --- pre-deploy-gate.js ---
127
140
  echo ""
128
141
  echo "pre-deploy-gate:"
129
142
 
130
- if [ -f "$HOOKS_DIR/pre-deploy-gate.sh" ]; then
131
- echo " ✓ pre-deploy-gate.sh exists"
143
+ if grep -q 'tsc' "$HOOKS_DIR/pre-deploy-gate.js"; then
144
+ echo " ✓ runs TypeScript check"
132
145
  PASS=$((PASS + 1))
133
146
  else
134
- echo " ✗ pre-deploy-gate.sh missing"
147
+ echo " ✗ missing TypeScript check"
135
148
  FAIL=$((FAIL + 1))
136
149
  fi
137
150
 
138
- if grep -q 'tsc --noEmit' "$HOOKS_DIR/pre-deploy-gate.sh"; then
139
- echo " ✓ runs TypeScript check"
151
+ if grep -q 'service_role' "$HOOKS_DIR/pre-deploy-gate.js"; then
152
+ echo " ✓ checks for service_role leaks"
140
153
  PASS=$((PASS + 1))
141
154
  else
142
- echo " ✗ missing TypeScript check"
155
+ echo " ✗ missing service_role check"
143
156
  FAIL=$((FAIL + 1))
144
157
  fi
145
158
 
146
- if grep -q 'service_role' "$HOOKS_DIR/pre-deploy-gate.sh"; then
147
- echo " ✓ checks for service_role leaks"
159
+ # --- session-start.js must exit 0 always ---
160
+ echo ""
161
+ echo "session-start:"
162
+
163
+ TMP=$(mktemp -d)
164
+ (cd "$TMP" && $NODE "$HOOKS_DIR/session-start.js" >/dev/null 2>&1)
165
+ assert_exit "exits 0 with no project" 0 $?
166
+
167
+ # Simulate a project with STATE.md
168
+ mkdir -p "$TMP/.planning"
169
+ cat > "$TMP/.planning/STATE.md" <<'EOF'
170
+ # Project State
171
+ Phase: 1 of 3 — Foundation
172
+ Status: setup
173
+ EOF
174
+ (cd "$TMP" && $NODE "$HOOKS_DIR/session-start.js" >/dev/null 2>&1)
175
+ assert_exit "exits 0 with STATE.md" 0 $?
176
+ rm -rf "$TMP"
177
+
178
+ # --- pre-compact.js ---
179
+ echo ""
180
+ echo "pre-compact:"
181
+
182
+ TMP=$(mktemp -d)
183
+ (cd "$TMP" && $NODE "$HOOKS_DIR/pre-compact.js" >/dev/null 2>&1)
184
+ assert_exit "exits 0 with no STATE.md" 0 $?
185
+ rm -rf "$TMP"
186
+
187
+ # --- auto-update.js ---
188
+ echo ""
189
+ echo "auto-update:"
190
+
191
+ TMP=$(mktemp -d)
192
+ mkdir -p "$TMP/.claude"
193
+ echo '{"code":"QS-FAWZI-01","version":"99.99.99"}' > "$TMP/.claude/.qualia-config.json"
194
+ HOME="$TMP" $NODE "$HOOKS_DIR/auto-update.js" >/dev/null 2>&1
195
+ assert_exit "exits 0 (fast path)" 0 $?
196
+ # Should now have cache file
197
+ if [ -f "$TMP/.claude/.qualia-last-update-check" ]; then
198
+ echo " ✓ writes cache timestamp"
148
199
  PASS=$((PASS + 1))
149
200
  else
150
- echo " ✗ missing service_role check"
201
+ echo " ✗ missing cache timestamp"
151
202
  FAIL=$((FAIL + 1))
152
203
  fi
204
+ rm -rf "$TMP"
153
205
 
154
206
  echo ""
155
207
  echo "=== Results: $PASS passed, $FAIL failed ==="
@@ -1,56 +0,0 @@
1
- #!/bin/bash
2
- # Qualia auto-update — checks once per day, updates silently in background
3
- # Runs as PreToolUse hook. Cached so it's a no-op most of the time.
4
-
5
- CLAUDE_DIR="$HOME/.claude"
6
- CACHE_FILE="$CLAUDE_DIR/.qualia-last-update-check"
7
- CONFIG_FILE="$CLAUDE_DIR/.qualia-config.json"
8
- LOCK_FILE="$CLAUDE_DIR/.qualia-updating"
9
- MAX_AGE=86400 # 24 hours in seconds
10
-
11
- # Exit fast if recently checked (most common path — single stat call)
12
- if [ -f "$CACHE_FILE" ]; then
13
- LAST_CHECK=$(cat "$CACHE_FILE" 2>/dev/null || echo 0)
14
- NOW=$(date +%s)
15
- AGE=$((NOW - LAST_CHECK))
16
- if [ "$AGE" -lt "$MAX_AGE" ]; then
17
- exit 0
18
- fi
19
- fi
20
-
21
- # Exit if already updating
22
- [ -f "$LOCK_FILE" ] && exit 0
23
-
24
- # Update cache timestamp immediately (prevents concurrent checks)
25
- date +%s > "$CACHE_FILE"
26
-
27
- # Run the actual check + update in background so we don't block the user
28
- (
29
- trap 'rm -f "$LOCK_FILE"' EXIT
30
- touch "$LOCK_FILE"
31
-
32
- # Get installed version
33
- INSTALLED=$(node -e "try{console.log(JSON.parse(require('fs').readFileSync('$CONFIG_FILE','utf8')).version)}catch{console.log('0.0.0')}" 2>/dev/null)
34
- [ -z "$INSTALLED" ] && INSTALLED="0.0.0"
35
-
36
- # Get latest from npm (5s timeout)
37
- LATEST=$(npm view qualia-framework-v2 version 2>/dev/null)
38
- [ -z "$LATEST" ] && exit 0
39
-
40
- # Compare versions
41
- NEEDS_UPDATE=$(node -e "
42
- const a='$LATEST'.split('.').map(Number), b='$INSTALLED'.split('.').map(Number);
43
- for(let i=0;i<3;i++){if(a[i]>b[i]){console.log('yes');process.exit()}if(a[i]<b[i]){process.exit()}}
44
- " 2>/dev/null)
45
-
46
- if [ "$NEEDS_UPDATE" = "yes" ]; then
47
- # Get saved install code
48
- CODE=$(node -e "try{console.log(JSON.parse(require('fs').readFileSync('$CONFIG_FILE','utf8')).code)}catch{}" 2>/dev/null)
49
- [ -z "$CODE" ] && exit 0
50
-
51
- # Run silent update
52
- npx qualia-framework-v2@latest install <<< "$CODE" > /dev/null 2>&1
53
- fi
54
- ) &
55
-
56
- exit 0
@@ -1,11 +0,0 @@
1
- #!/bin/bash
2
- # Prevent Claude from editing .env files
3
- # Claude Code hooks receive JSON on stdin with tool_input.file_path
4
-
5
- INPUT=$(cat)
6
- FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // .tool_input.command // ""' 2>/dev/null)
7
-
8
- if [[ "$FILE" == *.env* ]] || [[ "$FILE" == *".env.local"* ]] || [[ "$FILE" == *".env.production"* ]]; then
9
- echo "BLOCKED: Cannot edit environment files. Ask Fawzi to update secrets."
10
- exit 2
11
- fi
@@ -1,27 +0,0 @@
1
- #!/bin/bash
2
- # Block non-OWNER push to main/master
3
- # Reads role from ~/.claude/.qualia-config.json (machine-readable source of truth)
4
-
5
- BRANCH=$(git branch --show-current 2>/dev/null)
6
- CONFIG="$HOME/.claude/.qualia-config.json"
7
-
8
- if [ ! -f "$CONFIG" ]; then
9
- echo "BLOCKED: ~/.claude/.qualia-config.json missing. Run: npx qualia-framework-v2 install"
10
- exit 1
11
- fi
12
-
13
- # Extract role without jq dependency (installers may not have jq)
14
- ROLE=$(node -e "try{console.log(JSON.parse(require('fs').readFileSync('$CONFIG','utf8')).role||'')}catch{}" 2>/dev/null)
15
-
16
- if [ -z "$ROLE" ]; then
17
- echo "BLOCKED: Cannot determine role from $CONFIG. Defaulting to deny."
18
- exit 1
19
- fi
20
-
21
- if [[ "$BRANCH" == "main" || "$BRANCH" == "master" ]]; then
22
- if [[ "$ROLE" != "OWNER" ]]; then
23
- echo "BLOCKED: Employees cannot push to $BRANCH. Create a feature branch first."
24
- echo "Run: git checkout -b feature/your-feature-name"
25
- exit 1
26
- fi
27
- fi
@@ -1,43 +0,0 @@
1
- #!/bin/bash
2
- # Catch dangerous SQL patterns in migration files
3
- # Runs as PreToolUse hook on Write/Edit of migration files
4
-
5
- INPUT=$(cat)
6
- FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // ""' 2>/dev/null)
7
- CONTENT=$(echo "$INPUT" | jq -r '.tool_input.content // .tool_input.new_string // ""' 2>/dev/null)
8
-
9
- # Only check migration/SQL files
10
- case "$FILE" in
11
- *migration*|*migrate*|*.sql) ;;
12
- *) exit 0 ;;
13
- esac
14
-
15
- ERRORS=""
16
-
17
- # DROP TABLE without safeguards
18
- if echo "$CONTENT" | grep -qi "DROP TABLE" && ! echo "$CONTENT" | grep -qi "IF EXISTS"; then
19
- ERRORS="${ERRORS}\n ✗ DROP TABLE without IF EXISTS"
20
- fi
21
-
22
- # DELETE without WHERE
23
- if echo "$CONTENT" | grep -qi "DELETE FROM" && ! echo "$CONTENT" | grep -qi "WHERE"; then
24
- ERRORS="${ERRORS}\n ✗ DELETE FROM without WHERE clause"
25
- fi
26
-
27
- # TRUNCATE (almost always wrong in migrations)
28
- if echo "$CONTENT" | grep -qi "TRUNCATE"; then
29
- ERRORS="${ERRORS}\n ✗ TRUNCATE detected — are you sure?"
30
- fi
31
-
32
- # CREATE TABLE without RLS
33
- if echo "$CONTENT" | grep -qi "CREATE TABLE" && ! echo "$CONTENT" | grep -qi "ENABLE ROW LEVEL SECURITY"; then
34
- ERRORS="${ERRORS}\n ✗ CREATE TABLE without ENABLE ROW LEVEL SECURITY"
35
- fi
36
-
37
- if [ -n "$ERRORS" ]; then
38
- echo "◆ Migration guard — dangerous patterns found:"
39
- echo -e "$ERRORS"
40
- echo ""
41
- echo "Fix these before proceeding. If intentional, ask Fawzi to approve."
42
- exit 2
43
- fi
@@ -1,11 +0,0 @@
1
- #!/bin/bash
2
- # Save state before context compression
3
-
4
- if [ -f ".planning/STATE.md" ]; then
5
- echo "QUALIA: Saving state before compaction..."
6
- # State is in git — just ensure it's committed
7
- if git diff --name-only .planning/STATE.md 2>/dev/null | grep -q STATE; then
8
- git add .planning/STATE.md
9
- git commit -m "state: pre-compaction save" 2>/dev/null
10
- fi
11
- fi
@@ -1,50 +0,0 @@
1
- #!/bin/bash
2
- # Quality gates before production deploy
3
-
4
- echo "◆ Pre-deploy gate..."
5
-
6
- # TypeScript check
7
- if [ -f "tsconfig.json" ]; then
8
- if ! npx tsc --noEmit 2>/dev/null; then
9
- echo "BLOCKED: TypeScript errors. Fix before deploying."
10
- exit 1
11
- fi
12
- echo " ✓ TypeScript"
13
- fi
14
-
15
- # Lint check
16
- if [ -f "package.json" ] && grep -q '"lint"' package.json; then
17
- if ! npm run lint 2>/dev/null; then
18
- echo "BLOCKED: Lint errors. Fix before deploying."
19
- exit 1
20
- fi
21
- echo " ✓ Lint"
22
- fi
23
-
24
- # Test check
25
- if [ -f "package.json" ] && grep -q '"test"' package.json; then
26
- if ! npm test 2>/dev/null; then
27
- echo "BLOCKED: Tests failed. Fix before deploying."
28
- exit 1
29
- fi
30
- echo " ✓ Tests"
31
- fi
32
-
33
- # Build check
34
- if [ -f "package.json" ] && grep -q '"build"' package.json; then
35
- if ! npm run build 2>/dev/null; then
36
- echo "BLOCKED: Build failed. Fix before deploying."
37
- exit 1
38
- fi
39
- echo " ✓ Build"
40
- fi
41
-
42
- # Security: no service_role in client code
43
- LEAKS=$(grep -r "service_role" app/ components/ src/ 2>/dev/null | grep -v node_modules | grep -v ".server." | wc -l)
44
- if [ "$LEAKS" -gt 0 ]; then
45
- echo "BLOCKED: service_role found in client code. Remove before deploying."
46
- exit 1
47
- fi
48
- echo " ✓ Security"
49
-
50
- echo "◆ All gates passed."
package/hooks/pre-push.sh DELETED
@@ -1,28 +0,0 @@
1
- #!/bin/bash
2
- # Update tracking.json timestamps before push
3
- # State.js handles phase/status sync — this just updates commit hash and timestamp
4
-
5
- TRACKING=".planning/tracking.json"
6
-
7
- if [ -f "$TRACKING" ]; then
8
- if ! command -v node &>/dev/null; then
9
- echo "WARNING: node not found, skipping tracking sync" >&2
10
- exit 0
11
- fi
12
-
13
- LAST_COMMIT=$(git log --oneline -1 --format="%h" 2>/dev/null)
14
- NOW=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
15
-
16
- node -e "
17
- const fs = require('fs');
18
- try {
19
- const t = JSON.parse(fs.readFileSync('$TRACKING', 'utf8'));
20
- t.last_commit = '${LAST_COMMIT}';
21
- t.last_updated = '${NOW}';
22
- fs.writeFileSync('$TRACKING', JSON.stringify(t, null, 2) + '\n');
23
- } catch (e) {
24
- process.stderr.write('WARNING: tracking sync failed: ' + e.message + '\n');
25
- }
26
- "
27
- git add "$TRACKING" 2>/dev/null
28
- fi
@@ -1,35 +0,0 @@
1
- #!/bin/bash
2
- # Qualia session start — show branded context panel on every new session.
3
- # Delegates to qualia-ui.js so formatting matches the rest of the framework.
4
-
5
- UI="$HOME/.claude/bin/qualia-ui.js"
6
- STATE=".planning/STATE.md"
7
-
8
- # Fallback if qualia-ui.js is missing (first install before mirror)
9
- if [ ! -f "$UI" ]; then
10
- if [ -f "$STATE" ]; then
11
- PHASE=$(grep "^Phase:" "$STATE" 2>/dev/null | head -1)
12
- STATUS=$(grep "^Status:" "$STATE" 2>/dev/null | head -1)
13
- echo "QUALIA: Project loaded. $PHASE | $STATUS"
14
- echo "QUALIA: Run /qualia for next step."
15
- elif [ -f ".continue-here.md" ]; then
16
- echo "QUALIA: Handoff file found. Read .continue-here.md to resume."
17
- else
18
- echo "QUALIA: No project detected. Run /qualia-new to start."
19
- fi
20
- exit 0
21
- fi
22
-
23
- # Branded banner for every session start
24
- if [ -f "$STATE" ]; then
25
- node "$UI" banner router
26
- # Read next command from state.js and suggest it
27
- NEXT=$(node "$HOME/.claude/bin/state.js" check 2>/dev/null | node -e "try{let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{const j=JSON.parse(d);process.stdout.write(j.next_command||'')})}" 2>/dev/null)
28
- [ -n "$NEXT" ] && node "$UI" info "Run $NEXT to continue"
29
- elif [ -f ".continue-here.md" ]; then
30
- node "$UI" banner router
31
- node "$UI" warn "Handoff found — read .continue-here.md to resume"
32
- else
33
- node "$UI" banner router
34
- node "$UI" info "No project detected. Run /qualia-new to start."
35
- fi