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/CLAUDE.md +3 -4
- package/README.md +10 -5
- package/agents/planner.md +52 -0
- package/agents/verifier.md +180 -32
- package/bin/cli.js +403 -9
- package/bin/install.js +118 -65
- package/bin/qualia-ui.js +11 -11
- package/bin/state.js +200 -6
- package/bin/statusline.js +4 -4
- package/docs/erp-contract.md +161 -0
- package/hooks/branch-guard.js +23 -2
- package/hooks/migration-guard.js +23 -0
- package/hooks/pre-compact.js +20 -0
- package/hooks/pre-deploy-gate.js +39 -0
- package/hooks/pre-push.js +20 -0
- package/hooks/session-start.js +16 -43
- package/package.json +5 -4
- package/rules/infrastructure.md +87 -0
- package/skills/qualia/SKILL.md +1 -0
- package/skills/qualia-build/SKILL.md +18 -0
- package/skills/qualia-design/SKILL.md +14 -8
- package/skills/qualia-help/SKILL.md +60 -0
- package/skills/qualia-learn/SKILL.md +27 -4
- package/skills/qualia-polish/SKILL.md +167 -117
- package/skills/qualia-report/SKILL.md +17 -8
- package/skills/qualia-review/SKILL.md +126 -41
- package/skills/qualia-test/SKILL.md +134 -0
- package/skills/qualia-verify/SKILL.md +1 -1
- package/templates/DESIGN.md +440 -102
- package/templates/help.html +476 -0
- package/templates/plan.md +14 -0
- package/tests/bin.test.sh +20 -6
- package/tests/hooks.test.sh +76 -7
- package/tests/runner.js +1915 -0
- package/tests/state.test.sh +189 -11
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(`${
|
|
230
|
-
if (HOOKS_COUNT > 0) contextParts.push(`${
|
|
231
|
-
if (SKILLS_COUNT > 0) contextParts.push(`${
|
|
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
|
package/hooks/branch-guard.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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);
|
package/hooks/migration-guard.js
CHANGED
|
@@ -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);
|
package/hooks/pre-compact.js
CHANGED
|
@@ -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);
|
package/hooks/pre-deploy-gate.js
CHANGED
|
@@ -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);
|
package/hooks/session-start.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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/
|
|
22
|
+
"url": "git+https://github.com/Qualiasolutions/qualia-framework.git"
|
|
23
23
|
},
|
|
24
|
-
"homepage": "https://github.com/
|
|
24
|
+
"homepage": "https://github.com/Qualiasolutions/qualia-framework#readme",
|
|
25
25
|
"scripts": {
|
|
26
|
-
"test": "
|
|
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.
|
package/skills/qualia/SKILL.md
CHANGED
|
@@ -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
|