qualia-framework 3.2.0 → 3.2.1

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/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 with labels
228
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}`);
229
+ if (MEMORY_COUNT > 0) contextParts.push(`${DIM}mem${RESET} ${TEAL}${MEMORY_COUNT}${RESET}`);
230
+ if (HOOKS_COUNT > 0) contextParts.push(`${DIM}hooks${RESET} ${TEAL_GLOW}${HOOKS_COUNT}${RESET}`);
231
+ if (SKILLS_COUNT > 0) contextParts.push(`${DIM}skills${RESET} ${TEAL_DIM}${SKILLS_COUNT}${RESET}`);
232
232
  if (contextParts.length > 0) {
233
233
  LINE1 += ` ${DIM}│${RESET} ${contextParts.join(` ${DIM}·${RESET} `)}`;
234
234
  }
@@ -0,0 +1,161 @@
1
+ # ERP API Contract
2
+
3
+ The Qualia Framework optionally uploads session reports to the company ERP at `https://portal.qualiasolutions.net`. This document specifies the API shape.
4
+
5
+ ## Configuration
6
+
7
+ Stored in `~/.claude/.qualia-config.json`:
8
+
9
+ ```json
10
+ {
11
+ "erp": {
12
+ "enabled": true,
13
+ "url": "https://portal.qualiasolutions.net",
14
+ "api_key_file": ".erp-api-key"
15
+ }
16
+ }
17
+ ```
18
+
19
+ The API key is read from `~/.claude/.erp-api-key` (file mode 0600).
20
+
21
+ ## Endpoints
22
+
23
+ ### POST /api/v1/reports
24
+
25
+ Upload a session report.
26
+
27
+ **Headers:**
28
+ ```
29
+ Authorization: Bearer <api-key>
30
+ Content-Type: application/json
31
+ ```
32
+
33
+ **Request Body:**
34
+ ```json
35
+ {
36
+ "project": "client-project-name",
37
+ "client": "Client Name",
38
+ "phase": 2,
39
+ "phase_name": "Authentication & Dashboard",
40
+ "total_phases": 4,
41
+ "status": "built",
42
+ "tasks_done": 5,
43
+ "tasks_total": 5,
44
+ "verification": "pass",
45
+ "gap_cycles": 0,
46
+ "deployed_url": "https://client.vercel.app",
47
+ "session_duration_minutes": 45,
48
+ "commits": ["abc1234", "def5678"],
49
+ "notes": "Completed auth flow, dashboard layout, and API routes.",
50
+ "submitted_by": "Fawzi Goussous",
51
+ "submitted_at": "2026-04-12T14:30:00Z"
52
+ }
53
+ ```
54
+
55
+ **Response (200 OK):**
56
+ ```json
57
+ {
58
+ "ok": true,
59
+ "report_id": "rpt_abc123def456",
60
+ "message": "Report received"
61
+ }
62
+ ```
63
+
64
+ **Response (401 Unauthorized):**
65
+ ```json
66
+ {
67
+ "ok": false,
68
+ "error": "INVALID_API_KEY",
69
+ "message": "API key is invalid or expired"
70
+ }
71
+ ```
72
+
73
+ **Response (422 Unprocessable Entity):**
74
+ ```json
75
+ {
76
+ "ok": false,
77
+ "error": "VALIDATION_FAILED",
78
+ "message": "Missing required field: project"
79
+ }
80
+ ```
81
+
82
+ ### GET /api/v1/reports/:project
83
+
84
+ Retrieve reports for a project.
85
+
86
+ **Headers:**
87
+ ```
88
+ Authorization: Bearer <api-key>
89
+ ```
90
+
91
+ **Response (200 OK):**
92
+ ```json
93
+ {
94
+ "ok": true,
95
+ "reports": [
96
+ {
97
+ "report_id": "rpt_abc123def456",
98
+ "phase": 2,
99
+ "status": "built",
100
+ "submitted_at": "2026-04-12T14:30:00Z",
101
+ "submitted_by": "Fawzi Goussous"
102
+ }
103
+ ]
104
+ }
105
+ ```
106
+
107
+ ### GET /api/v1/tracking/:project
108
+
109
+ Retrieve current tracking state (same shape as tracking.json).
110
+
111
+ **Headers:**
112
+ ```
113
+ Authorization: Bearer <api-key>
114
+ ```
115
+
116
+ **Response (200 OK):**
117
+ ```json
118
+ {
119
+ "ok": true,
120
+ "tracking": {
121
+ "project": "client-project-name",
122
+ "phase": 2,
123
+ "total_phases": 4,
124
+ "status": "built",
125
+ "last_updated": "2026-04-12T14:30:00Z"
126
+ }
127
+ }
128
+ ```
129
+
130
+ ## Behavior
131
+
132
+ - When `erp.enabled` is `false`, `/qualia-report` skips the upload silently.
133
+ - When the API key file is missing or empty, the upload is skipped with a warning.
134
+ - Network failures are non-blocking — the report is saved locally regardless.
135
+ - The ERP reads `tracking.json` directly from git for real-time status (no API call needed for passive monitoring).
136
+ - Reports are append-only — no update or delete endpoints exist.
137
+
138
+ ## Required Fields
139
+
140
+ | Field | Type | Required | Description |
141
+ |-------|------|----------|-------------|
142
+ | project | string | yes | Project slug from tracking.json |
143
+ | phase | number | yes | Current phase number |
144
+ | status | string | yes | Current status (setup, planned, built, verified, etc.) |
145
+ | submitted_by | string | yes | Team member name |
146
+ | submitted_at | string | yes | ISO 8601 timestamp |
147
+
148
+ All other fields are optional but recommended for complete reporting.
149
+
150
+ ## Rate Limits
151
+
152
+ - 60 requests per minute per API key
153
+ - Report body max size: 64KB
154
+ - No batch endpoint — one report per request
155
+
156
+ ## Security
157
+
158
+ - API keys are per-user, not per-project
159
+ - Keys expire after 90 days (re-issue via Fawzi)
160
+ - All traffic is HTTPS-only
161
+ - No PII beyond team member names is transmitted
@@ -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
 
@@ -54,7 +75,9 @@ if (errors.length > 0) {
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
  }
@@ -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
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);
@@ -18,6 +18,7 @@ const HOME = os.homedir();
18
18
  const UI = path.join(HOME, ".claude", "bin", "qualia-ui.js");
19
19
  const STATE_FILE = path.join(".planning", "STATE.md");
20
20
  const CONTINUE_HERE = ".continue-here.md";
21
+ const NOTIF_FILE = path.join(HOME, ".claude", ".qualia-update-available.json");
21
22
 
22
23
  function runUi(...args) {
23
24
  if (!fs.existsSync(UI)) return;
@@ -45,46 +46,6 @@ function getNextCommand() {
45
46
  }
46
47
  }
47
48
 
48
- function maybeShowUpdateBanner() {
49
- // Sticky framework update notification for EMPLOYEEs. Populated by
50
- // auto-update.js when it detects a newer qualia-framework version on npm.
51
- // OWNER auto-updates silently, so this file is never created for Fawzi.
52
- try {
53
- const notifFile = path.join(HOME, ".claude", ".qualia-update-available.json");
54
- if (!fs.existsSync(notifFile)) return;
55
-
56
- const cfg = readConfig();
57
- // Belt-and-suspenders: even if a stale notification exists, OWNER never sees it.
58
- if (cfg.role === "OWNER") {
59
- try { fs.unlinkSync(notifFile); } catch {}
60
- return;
61
- }
62
-
63
- const notif = JSON.parse(fs.readFileSync(notifFile, "utf8"));
64
-
65
- // If the user has already updated (cfg.version >= notif.latest), clear the file.
66
- if (cfg.version && notif.latest) {
67
- const cmp = (a, b) => {
68
- const pa = String(a).split(".").map(Number);
69
- const pb = String(b).split(".").map(Number);
70
- for (let i = 0; i < 3; i++) {
71
- if ((pa[i] || 0) > (pb[i] || 0)) return 1;
72
- if ((pa[i] || 0) < (pb[i] || 0)) return -1;
73
- }
74
- return 0;
75
- };
76
- if (cmp(cfg.version, notif.latest) >= 0) {
77
- try { fs.unlinkSync(notifFile); } catch {}
78
- return;
79
- }
80
- }
81
-
82
- runUi("update", notif.current || cfg.version || "?", notif.latest || "?");
83
- } catch {
84
- // Never fail the session start.
85
- }
86
- }
87
-
88
49
  function fallbackText() {
89
50
  // If qualia-ui.js is missing, emit plain text. Keeps the session informative
90
51
  // even on a broken install.
@@ -105,10 +66,22 @@ function fallbackText() {
105
66
  }
106
67
  }
107
68
 
69
+ function maybeRenderUpdateBanner() {
70
+ // EMPLOYEE-only sticky banner. auto-update.js writes NOTIF_FILE when a new
71
+ // version is detected; we render it every session until the user actually
72
+ // runs `npx qualia-framework@latest install`. The file is cleared by
73
+ // auto-update.js once the install completes or the version catches up.
74
+ if (!fs.existsSync(NOTIF_FILE) || !fs.existsSync(UI)) return;
75
+ try {
76
+ const notif = JSON.parse(fs.readFileSync(NOTIF_FILE, "utf8"));
77
+ if (notif && notif.current && notif.latest) {
78
+ runUi("update", notif.current, notif.latest);
79
+ }
80
+ } catch {}
81
+ }
82
+
108
83
  try {
109
- // Sticky update notification — shown before anything else every session
110
- // until the employee runs `npx qualia-framework@latest install`.
111
- maybeShowUpdateBanner();
84
+ maybeRenderUpdateBanner();
112
85
 
113
86
  if (!fs.existsSync(UI)) {
114
87
  fallbackText();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qualia-framework",
3
- "version": "3.2.0",
3
+ "version": "3.2.1",
4
4
  "description": "Claude Code workflow framework by Qualia Solutions. Plan, build, verify, ship.",
5
5
  "bin": {
6
6
  "qualia-framework": "./bin/cli.js"
@@ -19,11 +19,11 @@
19
19
  "license": "MIT",
20
20
  "repository": {
21
21
  "type": "git",
22
- "url": "git+https://github.com/QualiasolutionsCY/qualia-framework.git"
22
+ "url": "git+https://github.com/Qualiasolutions/qualia-framework.git"
23
23
  },
24
- "homepage": "https://github.com/QualiasolutionsCY/qualia-framework#readme",
24
+ "homepage": "https://github.com/Qualiasolutions/qualia-framework#readme",
25
25
  "scripts": {
26
- "test": "bash tests/hooks.test.sh && bash tests/state.test.sh && bash tests/bin.test.sh && bash tests/statusline.test.sh"
26
+ "test": "node --test tests/runner.js"
27
27
  },
28
28
  "files": [
29
29
  "bin/",
@@ -33,6 +33,7 @@
33
33
  "skills/",
34
34
  "templates/",
35
35
  "tests/",
36
+ "docs/",
36
37
  "CLAUDE.md",
37
38
  "guide.md"
38
39
  ],
@@ -0,0 +1,87 @@
1
+ ---
2
+ globs: ["*.env*", "vercel.json", "next.config.*", "supabase/**", "railway.*"]
3
+ ---
4
+
5
+ # Infrastructure & Services
6
+
7
+ Standard services across all Qualia projects. Use these unless the project explicitly specifies otherwise.
8
+
9
+ ## Database: Supabase (every project)
10
+ - Every project uses Supabase for auth, database, and storage
11
+ - **CLI:** `npx supabase` — migrations, type generation, local dev
12
+ - **MCP:** Supabase MCP server is available in Claude Code for direct database operations
13
+ - Always enable RLS on every table (see `rules/security.md`)
14
+ - Use `lib/supabase/server.ts` for server-side, `lib/supabase/client.ts` for client-side
15
+ - Run `npx supabase gen types` after schema changes
16
+ - Migrations go in `supabase/migrations/` — never edit production directly
17
+
18
+ ## AI Models: OpenRouter (every project)
19
+ - Use OpenRouter API for all LLM calls — it routes to the best-suited model per task
20
+ - API key env var: `OPENROUTER_API_KEY`
21
+ - Don't have a key? Ask Fawzi for one
22
+ - Never hardcode a specific model provider (OpenAI, Anthropic, etc.) directly — always go through OpenRouter
23
+ - Exception: if a client specifically requires a direct provider integration
24
+
25
+ ## Voice AI: Retell AI + ElevenLabs
26
+ - **Retell AI** — primary voice agent platform. API key: `RETELL_API_KEY`
27
+ - **ElevenLabs** — voice synthesis, cloning, streaming. API key: `ELEVENLABS_API_KEY`
28
+ - **Telnyx** — telephony/SIP for voice agent phone numbers. API key: `TELNYX_API_KEY`
29
+ - For new voice projects, default to Retell AI + ElevenLabs unless client specifies otherwise
30
+
31
+ ## Compute: Vercel + Railway
32
+ - **Vercel** — primary hosting for all Next.js projects. Deploy via CLI only (see below)
33
+ - **Railway** — secondary compute for long-running agents, background jobs, and agentic workloads that exceed Vercel's function timeout
34
+ - **Railway CLI:** `railway` — deploy, logs, env management
35
+ - **Railway MCP:** Railway MCP server is available in Claude Code for project management
36
+ - Railway projects use Nixpacks (auto-detected) — check for `railway.json` or `railway.toml`
37
+
38
+ ## MCP Servers (available in Claude Code)
39
+ - **Supabase MCP** — database queries, table management, migrations from within Claude Code
40
+ - **Railway MCP** — project deployment, logs, environment variables from within Claude Code
41
+ - **next-devtools MCP** — runtime error visibility for Next.js 16+ dev servers (optional, added by framework install)
42
+
43
+ ## CLIs (must be installed)
44
+ - `npx supabase` — Supabase CLI (database, migrations, types)
45
+ - `railway` — Railway CLI (deploy, logs, env)
46
+ - `vercel` — Vercel CLI (deploy, env, link)
47
+ - `gh` — GitHub CLI (PRs, issues, repos)
48
+
49
+ ## GitHub Organizations
50
+ - **QualiasolutionsCY** — primary org for all Qualia Solutions projects
51
+ - **SakaniQualia** — org for Sakani-related projects (real estate platform)
52
+ - All repos are private by default
53
+ - Branch protection: main/master require PR reviews (enforced by framework guards)
54
+
55
+ ## Vercel Teams (admin knowledge)
56
+ - Qualia operates across **3 Vercel teams** — projects are distributed across them
57
+ - Check which team a project belongs to before deploying: `vercel whoami` and `vercel link`
58
+ - If a project isn't linked, link it first: `vercel link`
59
+
60
+ ## Deployment Rules
61
+ - **NO auto-deploys from GitHub pushes** — all Vercel projects have GitHub integration auto-deploy DISABLED
62
+ - Deploys happen ONLY via `vercel --prod` through the CLI (or `/qualia-ship`)
63
+ - This is intentional — we control when things go live, not git push triggers
64
+ - If you find a project with auto-deploy enabled, disable it: Vercel Dashboard → Project Settings → Git → Disable "Automatic Deployments"
65
+
66
+ ## Required Environment Variables (typical project)
67
+
68
+ ```bash
69
+ # Supabase (every project)
70
+ NEXT_PUBLIC_SUPABASE_URL=
71
+ NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=
72
+ SUPABASE_SERVICE_ROLE_KEY= # NEVER in client code
73
+
74
+ # AI (if applicable)
75
+ OPENROUTER_API_KEY= # ask Fawzi if you don't have one
76
+
77
+ # Voice (if applicable)
78
+ RETELL_API_KEY=
79
+ ELEVENLABS_API_KEY=
80
+ TELNYX_API_KEY=
81
+
82
+ # Deployment
83
+ VERCEL_ORG_ID= # from vercel link
84
+ VERCEL_PROJECT_ID= # from vercel link
85
+ ```
86
+
87
+ Always use `vercel env pull` to sync env vars locally. Never create `.env` manually from scratch.
@@ -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