qualia-framework 3.1.0 → 3.2.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/CLAUDE.md +4 -3
- package/README.md +5 -10
- package/agents/planner.md +0 -52
- package/agents/verifier.md +32 -180
- package/bin/cli.js +9 -403
- package/bin/install.js +61 -113
- package/bin/qualia-ui.js +15 -0
- package/bin/state.js +6 -200
- package/bin/statusline.js +4 -4
- package/hooks/auto-update.js +30 -8
- package/hooks/branch-guard.js +2 -23
- package/hooks/migration-guard.js +0 -23
- package/hooks/pre-compact.js +0 -20
- package/hooks/pre-deploy-gate.js +0 -39
- package/hooks/pre-push.js +0 -20
- package/hooks/session-start.js +44 -0
- package/package.json +4 -5
- package/skills/qualia/SKILL.md +0 -1
- package/skills/qualia-build/SKILL.md +0 -18
- package/skills/qualia-design/SKILL.md +8 -14
- package/skills/qualia-learn/SKILL.md +4 -27
- package/skills/qualia-optimize/SKILL.md +417 -0
- package/skills/qualia-polish/SKILL.md +117 -167
- package/skills/qualia-report/SKILL.md +8 -17
- package/skills/qualia-review/SKILL.md +41 -126
- package/skills/qualia-verify/SKILL.md +1 -1
- package/templates/DESIGN.md +102 -440
- package/templates/plan.md +0 -14
- package/tests/bin.test.sh +6 -20
- package/tests/hooks.test.sh +7 -76
- package/tests/state.test.sh +11 -189
- package/docs/erp-contract.md +0 -161
- package/hooks/block-env-edit.js +0 -52
- package/rules/infrastructure.md +0 -87
- package/skills/qualia-help/SKILL.md +0 -60
- package/skills/qualia-test/SKILL.md +0 -134
- package/templates/help.html +0 -476
- package/tests/runner.js +0 -1956
package/tests/hooks.test.sh
CHANGED
|
@@ -34,29 +34,6 @@ for f in "$HOOKS_DIR"/*.js; do
|
|
|
34
34
|
fi
|
|
35
35
|
done
|
|
36
36
|
|
|
37
|
-
# --- block-env-edit.js ---
|
|
38
|
-
echo ""
|
|
39
|
-
echo "block-env-edit:"
|
|
40
|
-
|
|
41
|
-
echo '{"tool_input":{"file_path":".env.local"}}' | $NODE "$HOOKS_DIR/block-env-edit.js" > /dev/null 2>&1
|
|
42
|
-
assert_exit "blocks .env.local" 2 $?
|
|
43
|
-
|
|
44
|
-
echo '{"tool_input":{"file_path":".env.production"}}' | $NODE "$HOOKS_DIR/block-env-edit.js" > /dev/null 2>&1
|
|
45
|
-
assert_exit "blocks .env.production" 2 $?
|
|
46
|
-
|
|
47
|
-
echo '{"tool_input":{"file_path":".env"}}' | $NODE "$HOOKS_DIR/block-env-edit.js" > /dev/null 2>&1
|
|
48
|
-
assert_exit "blocks .env" 2 $?
|
|
49
|
-
|
|
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
|
|
55
|
-
assert_exit "allows src/app.tsx" 0 $?
|
|
56
|
-
|
|
57
|
-
echo '{"tool_input":{"file_path":"components/Footer.tsx"}}' | $NODE "$HOOKS_DIR/block-env-edit.js" > /dev/null 2>&1
|
|
58
|
-
assert_exit "allows components/Footer.tsx" 0 $?
|
|
59
|
-
|
|
60
37
|
# --- migration-guard.js ---
|
|
61
38
|
echo ""
|
|
62
39
|
echo "migration-guard:"
|
|
@@ -107,11 +84,11 @@ TMP=$(setup_guard_repo main OWNER)
|
|
|
107
84
|
assert_exit "OWNER on main → allowed" 0 $?
|
|
108
85
|
rm -rf "$TMP"
|
|
109
86
|
|
|
110
|
-
# EMPLOYEE on main → blocked (exit
|
|
87
|
+
# EMPLOYEE on main → blocked (exit 1)
|
|
111
88
|
TMP=$(setup_guard_repo main EMPLOYEE)
|
|
112
89
|
OUT=$(cd "$TMP/proj" && HOME="$TMP" $NODE "$HOOKS_DIR/branch-guard.js" 2>&1)
|
|
113
90
|
RC=$?
|
|
114
|
-
if [ "$RC" -eq
|
|
91
|
+
if [ "$RC" -eq 1 ] && echo "$OUT" | grep -q "BLOCKED" && echo "$OUT" | grep -q "main"; then
|
|
115
92
|
echo " ✓ EMPLOYEE on main → blocked (BLOCKED in stdout)"
|
|
116
93
|
PASS=$((PASS + 1))
|
|
117
94
|
else
|
|
@@ -123,7 +100,7 @@ rm -rf "$TMP"
|
|
|
123
100
|
# EMPLOYEE on master → blocked
|
|
124
101
|
TMP=$(setup_guard_repo master EMPLOYEE)
|
|
125
102
|
(cd "$TMP/proj" && HOME="$TMP" $NODE "$HOOKS_DIR/branch-guard.js" >/dev/null 2>&1)
|
|
126
|
-
assert_exit "EMPLOYEE on master → blocked"
|
|
103
|
+
assert_exit "EMPLOYEE on master → blocked" 1 $?
|
|
127
104
|
rm -rf "$TMP"
|
|
128
105
|
|
|
129
106
|
# EMPLOYEE on feature branch → allowed
|
|
@@ -138,13 +115,13 @@ TMP=$(setup_guard_repo feature/xyz OWNER)
|
|
|
138
115
|
assert_exit "OWNER on feature/xyz → allowed" 0 $?
|
|
139
116
|
rm -rf "$TMP"
|
|
140
117
|
|
|
141
|
-
# Missing config → fails closed (block, exit
|
|
118
|
+
# Missing config → fails closed (block, exit 1)
|
|
142
119
|
TMP=$(mktemp -d)
|
|
143
120
|
mkdir -p "$TMP/proj"
|
|
144
121
|
(cd "$TMP/proj" && git init -q && git checkout -b feature/x -q 2>/dev/null)
|
|
145
122
|
# NO .claude/.qualia-config.json
|
|
146
123
|
(cd "$TMP/proj" && HOME="$TMP" $NODE "$HOOKS_DIR/branch-guard.js" >/dev/null 2>&1)
|
|
147
|
-
assert_exit "missing config → blocked (fails closed)"
|
|
124
|
+
assert_exit "missing config → blocked (fails closed)" 1 $?
|
|
148
125
|
rm -rf "$TMP"
|
|
149
126
|
|
|
150
127
|
# Malformed config JSON → fails closed
|
|
@@ -153,7 +130,7 @@ mkdir -p "$TMP/proj" "$TMP/.claude"
|
|
|
153
130
|
(cd "$TMP/proj" && git init -q && git checkout -b feature/x -q 2>/dev/null)
|
|
154
131
|
echo 'not json{' > "$TMP/.claude/.qualia-config.json"
|
|
155
132
|
(cd "$TMP/proj" && HOME="$TMP" $NODE "$HOOKS_DIR/branch-guard.js" >/dev/null 2>&1)
|
|
156
|
-
assert_exit "malformed config JSON → blocked"
|
|
133
|
+
assert_exit "malformed config JSON → blocked" 1 $?
|
|
157
134
|
rm -rf "$TMP"
|
|
158
135
|
|
|
159
136
|
# Empty role field → fails closed
|
|
@@ -162,7 +139,7 @@ mkdir -p "$TMP/proj" "$TMP/.claude"
|
|
|
162
139
|
(cd "$TMP/proj" && git init -q && git checkout -b feature/x -q 2>/dev/null)
|
|
163
140
|
echo '{"role":""}' > "$TMP/.claude/.qualia-config.json"
|
|
164
141
|
(cd "$TMP/proj" && HOME="$TMP" $NODE "$HOOKS_DIR/branch-guard.js" >/dev/null 2>&1)
|
|
165
|
-
assert_exit "empty role field → blocked"
|
|
142
|
+
assert_exit "empty role field → blocked" 1 $?
|
|
166
143
|
rm -rf "$TMP"
|
|
167
144
|
|
|
168
145
|
# --- pre-push.js ---
|
|
@@ -286,52 +263,6 @@ else
|
|
|
286
263
|
fi
|
|
287
264
|
rm -rf "$TMP"
|
|
288
265
|
|
|
289
|
-
# --- pre-deploy-gate: Server Component / route handler exemptions ---
|
|
290
|
-
|
|
291
|
-
# route.ts with service_role → exempt (always server-side)
|
|
292
|
-
TMP=$(mktemp -d)
|
|
293
|
-
mkdir -p "$TMP/app/api/auth"
|
|
294
|
-
echo 'const key = process.env.SUPABASE_SERVICE_ROLE_KEY; export async function POST() {}' > "$TMP/app/api/auth/route.ts"
|
|
295
|
-
OUT=$( (cd "$TMP" && $NODE "$HOOKS_DIR/pre-deploy-gate.js") 2>&1 )
|
|
296
|
-
RC=$?
|
|
297
|
-
assert_exit "route.ts with service_role → exempt (exit 0)" 0 $RC
|
|
298
|
-
rm -rf "$TMP"
|
|
299
|
-
|
|
300
|
-
# middleware.ts with service_role → exempt (always server-side)
|
|
301
|
-
TMP=$(mktemp -d)
|
|
302
|
-
echo 'import { service_role } from "./config"; export function middleware() {}' > "$TMP/middleware.ts"
|
|
303
|
-
OUT=$( (cd "$TMP" && $NODE "$HOOKS_DIR/pre-deploy-gate.js") 2>&1 )
|
|
304
|
-
RC=$?
|
|
305
|
-
assert_exit "middleware.ts with service_role → exempt (exit 0)" 0 $RC
|
|
306
|
-
rm -rf "$TMP"
|
|
307
|
-
|
|
308
|
-
# File in app/api/ with service_role → exempt
|
|
309
|
-
TMP=$(mktemp -d)
|
|
310
|
-
mkdir -p "$TMP/app/api/webhook"
|
|
311
|
-
echo 'const sr = "service_role"; export async function GET() { return new Response(sr); }' > "$TMP/app/api/webhook/route.js"
|
|
312
|
-
OUT=$( (cd "$TMP" && $NODE "$HOOKS_DIR/pre-deploy-gate.js") 2>&1 )
|
|
313
|
-
RC=$?
|
|
314
|
-
assert_exit "app/api/ file with service_role → exempt (exit 0)" 0 $RC
|
|
315
|
-
rm -rf "$TMP"
|
|
316
|
-
|
|
317
|
-
# File with "use server" directive + service_role → exempt
|
|
318
|
-
TMP=$(mktemp -d)
|
|
319
|
-
mkdir -p "$TMP/app/admin"
|
|
320
|
-
printf '"use server"\nconst key = process.env.SUPABASE_SERVICE_ROLE_KEY;\nexport async function deleteUser() {}\n' > "$TMP/app/admin/actions.ts"
|
|
321
|
-
OUT=$( (cd "$TMP" && $NODE "$HOOKS_DIR/pre-deploy-gate.js") 2>&1 )
|
|
322
|
-
RC=$?
|
|
323
|
-
assert_exit "\"use server\" file with service_role → exempt (exit 0)" 0 $RC
|
|
324
|
-
rm -rf "$TMP"
|
|
325
|
-
|
|
326
|
-
# Regular app/page.tsx WITHOUT directive + service_role → still blocks
|
|
327
|
-
TMP=$(mktemp -d)
|
|
328
|
-
mkdir -p "$TMP/app/admin"
|
|
329
|
-
echo 'const key = "service_role"; export default function Page() { return <div>{key}</div>; }' > "$TMP/app/admin/page.tsx"
|
|
330
|
-
OUT=$( (cd "$TMP" && $NODE "$HOOKS_DIR/pre-deploy-gate.js") 2>&1 )
|
|
331
|
-
RC=$?
|
|
332
|
-
assert_exit "regular page.tsx with service_role → blocked (exit 1)" 1 $RC
|
|
333
|
-
rm -rf "$TMP"
|
|
334
|
-
|
|
335
266
|
# --- session-start.js — must exit 0 always ---
|
|
336
267
|
echo ""
|
|
337
268
|
echo "session-start:"
|
package/tests/state.test.sh
CHANGED
|
@@ -45,34 +45,6 @@ fail_case() {
|
|
|
45
45
|
FAIL=$((FAIL + 1))
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
# Write a minimal valid plan file (passes content validation).
|
|
49
|
-
# Usage: make_valid_plan "$TMP" 1
|
|
50
|
-
make_valid_plan() {
|
|
51
|
-
local dir="$1"
|
|
52
|
-
local phase="${2:-1}"
|
|
53
|
-
cat > "$dir/.planning/phase-${phase}-plan.md" <<'PLAN'
|
|
54
|
-
---
|
|
55
|
-
phase: 1
|
|
56
|
-
goal: "Test goal"
|
|
57
|
-
tasks: 1
|
|
58
|
-
waves: 1
|
|
59
|
-
---
|
|
60
|
-
|
|
61
|
-
# Phase 1: Test
|
|
62
|
-
|
|
63
|
-
Goal: Test goal
|
|
64
|
-
|
|
65
|
-
## Task 1 — Test task
|
|
66
|
-
**Wave:** 1
|
|
67
|
-
**Files:** src/test.ts
|
|
68
|
-
**Action:** Create test file
|
|
69
|
-
**Done when:** File exists
|
|
70
|
-
|
|
71
|
-
## Success Criteria
|
|
72
|
-
- [ ] Test passes
|
|
73
|
-
PLAN
|
|
74
|
-
}
|
|
75
|
-
|
|
76
48
|
echo "=== state.js Behavioral Tests ==="
|
|
77
49
|
echo ""
|
|
78
50
|
|
|
@@ -154,7 +126,7 @@ echo "happy path transitions:"
|
|
|
154
126
|
|
|
155
127
|
# 4. setup → planned (with plan file)
|
|
156
128
|
TMP=$(make_project)
|
|
157
|
-
|
|
129
|
+
touch "$TMP/.planning/phase-1-plan.md"
|
|
158
130
|
OUT=$(cd "$TMP" && $NODE "$STATE_JS" transition --to planned 2>&1)
|
|
159
131
|
EXIT=$?
|
|
160
132
|
if [ "$EXIT" -eq 0 ] \
|
|
@@ -194,7 +166,7 @@ fi
|
|
|
194
166
|
|
|
195
167
|
# 7. built → verified(fail) stays on phase 1, records verification=fail
|
|
196
168
|
TMP=$(make_project)
|
|
197
|
-
|
|
169
|
+
touch "$TMP/.planning/phase-1-plan.md"
|
|
198
170
|
(cd "$TMP" && $NODE "$STATE_JS" transition --to planned >/dev/null 2>&1)
|
|
199
171
|
(cd "$TMP" && $NODE "$STATE_JS" transition --to built --tasks-done 3 --tasks-total 5 >/dev/null 2>&1)
|
|
200
172
|
touch "$TMP/.planning/phase-1-verification.md"
|
|
@@ -229,7 +201,7 @@ fi
|
|
|
229
201
|
|
|
230
202
|
# 9. planned → verified fails (requires status=built)
|
|
231
203
|
TMP=$(make_project)
|
|
232
|
-
|
|
204
|
+
touch "$TMP/.planning/phase-1-plan.md"
|
|
233
205
|
(cd "$TMP" && $NODE "$STATE_JS" transition --to planned >/dev/null 2>&1)
|
|
234
206
|
touch "$TMP/.planning/phase-1-verification.md"
|
|
235
207
|
OUT=$(cd "$TMP" && $NODE "$STATE_JS" transition --to verified --verification pass 2>&1)
|
|
@@ -257,7 +229,7 @@ fi
|
|
|
257
229
|
|
|
258
230
|
# 11. built → verified with missing verification file → MISSING_FILE
|
|
259
231
|
TMP=$(make_project)
|
|
260
|
-
|
|
232
|
+
touch "$TMP/.planning/phase-1-plan.md"
|
|
261
233
|
(cd "$TMP" && $NODE "$STATE_JS" transition --to planned >/dev/null 2>&1)
|
|
262
234
|
(cd "$TMP" && $NODE "$STATE_JS" transition --to built --tasks-done 1 --tasks-total 1 >/dev/null 2>&1)
|
|
263
235
|
# NO verification file
|
|
@@ -273,7 +245,7 @@ fi
|
|
|
273
245
|
|
|
274
246
|
# 12. built → verified without --verification → MISSING_ARG
|
|
275
247
|
TMP=$(make_project)
|
|
276
|
-
|
|
248
|
+
touch "$TMP/.planning/phase-1-plan.md"
|
|
277
249
|
(cd "$TMP" && $NODE "$STATE_JS" transition --to planned >/dev/null 2>&1)
|
|
278
250
|
(cd "$TMP" && $NODE "$STATE_JS" transition --to built --tasks-done 1 --tasks-total 1 >/dev/null 2>&1)
|
|
279
251
|
touch "$TMP/.planning/phase-1-verification.md"
|
|
@@ -290,13 +262,13 @@ fi
|
|
|
290
262
|
# 13. → shipped without --deployed-url → MISSING_ARG
|
|
291
263
|
# Must go through polished first, so fabricate state by transitioning through the full path.
|
|
292
264
|
TMP=$(make_project)
|
|
293
|
-
|
|
265
|
+
touch "$TMP/.planning/phase-1-plan.md"
|
|
294
266
|
(cd "$TMP" && $NODE "$STATE_JS" transition --to planned >/dev/null 2>&1)
|
|
295
267
|
(cd "$TMP" && $NODE "$STATE_JS" transition --to built --tasks-done 1 --tasks-total 1 >/dev/null 2>&1)
|
|
296
268
|
touch "$TMP/.planning/phase-1-verification.md"
|
|
297
269
|
(cd "$TMP" && $NODE "$STATE_JS" transition --to verified --verification pass >/dev/null 2>&1)
|
|
298
270
|
# Now on phase 2, status=setup. Run phase 2 to completion.
|
|
299
|
-
|
|
271
|
+
touch "$TMP/.planning/phase-2-plan.md"
|
|
300
272
|
(cd "$TMP" && $NODE "$STATE_JS" transition --to planned >/dev/null 2>&1)
|
|
301
273
|
(cd "$TMP" && $NODE "$STATE_JS" transition --to built --tasks-done 1 --tasks-total 1 >/dev/null 2>&1)
|
|
302
274
|
touch "$TMP/.planning/phase-2-verification.md"
|
|
@@ -331,7 +303,7 @@ echo "gap cycle circuit breaker:"
|
|
|
331
303
|
|
|
332
304
|
# 15. First gap closure: verified(fail) → planned, gap_cycles[1]=1
|
|
333
305
|
TMP=$(make_project)
|
|
334
|
-
|
|
306
|
+
touch "$TMP/.planning/phase-1-plan.md"
|
|
335
307
|
touch "$TMP/.planning/phase-1-verification.md"
|
|
336
308
|
(cd "$TMP" && $NODE "$STATE_JS" transition --to planned >/dev/null 2>&1)
|
|
337
309
|
(cd "$TMP" && $NODE "$STATE_JS" transition --to built --tasks-done 1 --tasks-total 1 >/dev/null 2>&1)
|
|
@@ -374,7 +346,7 @@ fi
|
|
|
374
346
|
# 18. verified(pass) resets gap_cycles[1] to 0
|
|
375
347
|
# Set up a fresh project, do ONE failed cycle, then pass on the next attempt.
|
|
376
348
|
TMP=$(make_project)
|
|
377
|
-
|
|
349
|
+
touch "$TMP/.planning/phase-1-plan.md"
|
|
378
350
|
touch "$TMP/.planning/phase-1-verification.md"
|
|
379
351
|
(cd "$TMP" && $NODE "$STATE_JS" transition --to planned >/dev/null 2>&1)
|
|
380
352
|
(cd "$TMP" && $NODE "$STATE_JS" transition --to built --tasks-done 1 --tasks-total 1 >/dev/null 2>&1)
|
|
@@ -493,7 +465,7 @@ fi
|
|
|
493
465
|
|
|
494
466
|
# 26. Transition refuses on severity=error (missing Phase: header)
|
|
495
467
|
TMP=$(make_project)
|
|
496
|
-
|
|
468
|
+
touch "$TMP/.planning/phase-1-plan.md"
|
|
497
469
|
sed -i.bak '/^Phase:/d' "$TMP/.planning/STATE.md"
|
|
498
470
|
OUT=$(cd "$TMP" && $NODE "$STATE_JS" transition --to planned 2>&1)
|
|
499
471
|
EXIT=$?
|
|
@@ -541,7 +513,7 @@ fi
|
|
|
541
513
|
|
|
542
514
|
# 29. After fix, transition that was previously blocked now works
|
|
543
515
|
TMP=$(make_project)
|
|
544
|
-
|
|
516
|
+
touch "$TMP/.planning/phase-1-plan.md"
|
|
545
517
|
sed -i.bak '/^Phase:/d' "$TMP/.planning/STATE.md"
|
|
546
518
|
# Blocked before fix
|
|
547
519
|
(cd "$TMP" && $NODE "$STATE_JS" transition --to planned 2>&1 | grep -q STATE_SCHEMA_ERROR) || \
|
|
@@ -557,156 +529,6 @@ else
|
|
|
557
529
|
fail_case "after fix transition" "exit=$EXIT out=$OUT"
|
|
558
530
|
fi
|
|
559
531
|
|
|
560
|
-
# ─── Configurable gap cycle limit ────────────────────────
|
|
561
|
-
echo ""
|
|
562
|
-
echo "configurable gap cycle limit:"
|
|
563
|
-
|
|
564
|
-
# 30. gap_cycle_limit=5 allows 3rd gap closure (would fail at default 2)
|
|
565
|
-
TMP=$(make_project)
|
|
566
|
-
make_valid_plan "$TMP" 1
|
|
567
|
-
touch "$TMP/.planning/phase-1-verification.md"
|
|
568
|
-
# Set custom limit in tracking.json
|
|
569
|
-
TRACKING=$(cat "$TMP/.planning/tracking.json")
|
|
570
|
-
echo "$TRACKING" | $NODE -e "
|
|
571
|
-
const t = JSON.parse(require('fs').readFileSync(0,'utf8'));
|
|
572
|
-
t.gap_cycle_limit = 5;
|
|
573
|
-
process.stdout.write(JSON.stringify(t, null, 2));
|
|
574
|
-
" > "$TMP/.planning/tracking.json"
|
|
575
|
-
# Do 3 gap closure cycles
|
|
576
|
-
(cd "$TMP" && $NODE "$STATE_JS" transition --to planned >/dev/null 2>&1)
|
|
577
|
-
(cd "$TMP" && $NODE "$STATE_JS" transition --to built --tasks-done 1 --tasks-total 1 >/dev/null 2>&1)
|
|
578
|
-
(cd "$TMP" && $NODE "$STATE_JS" transition --to verified --verification fail >/dev/null 2>&1)
|
|
579
|
-
(cd "$TMP" && $NODE "$STATE_JS" transition --to planned >/dev/null 2>&1)
|
|
580
|
-
(cd "$TMP" && $NODE "$STATE_JS" transition --to built --tasks-done 1 --tasks-total 1 >/dev/null 2>&1)
|
|
581
|
-
(cd "$TMP" && $NODE "$STATE_JS" transition --to verified --verification fail >/dev/null 2>&1)
|
|
582
|
-
# 3rd closure should succeed (limit is 5, we're at 2)
|
|
583
|
-
OUT=$(cd "$TMP" && $NODE "$STATE_JS" transition --to planned 2>&1)
|
|
584
|
-
EXIT=$?
|
|
585
|
-
if [ "$EXIT" -eq 0 ] \
|
|
586
|
-
&& echo "$OUT" | grep -q '"ok": true'; then
|
|
587
|
-
pass "gap_cycle_limit=5 allows 3rd closure (default would block)"
|
|
588
|
-
else
|
|
589
|
-
fail_case "custom gap limit" "exit=$EXIT out=$OUT"
|
|
590
|
-
fi
|
|
591
|
-
|
|
592
|
-
# 31. cmdCheck includes gap_cycle_limit in output
|
|
593
|
-
TMP=$(make_project)
|
|
594
|
-
OUT=$(cd "$TMP" && $NODE "$STATE_JS" check 2>&1)
|
|
595
|
-
if echo "$OUT" | grep -q '"gap_cycle_limit":'; then
|
|
596
|
-
pass "cmdCheck includes gap_cycle_limit in output"
|
|
597
|
-
else
|
|
598
|
-
fail_case "gap_cycle_limit in check" "out=$OUT"
|
|
599
|
-
fi
|
|
600
|
-
|
|
601
|
-
# ─── Plan content validation ────────────────────────────
|
|
602
|
-
echo ""
|
|
603
|
-
echo "plan content validation:"
|
|
604
|
-
|
|
605
|
-
# 32. validate-plan accepts well-formed plan
|
|
606
|
-
TMP=$(make_project)
|
|
607
|
-
make_valid_plan "$TMP" 1
|
|
608
|
-
OUT=$(cd "$TMP" && $NODE "$STATE_JS" validate-plan --phase 1 2>&1)
|
|
609
|
-
EXIT=$?
|
|
610
|
-
if [ "$EXIT" -eq 0 ] \
|
|
611
|
-
&& echo "$OUT" | grep -q '"action": "validate-plan"' \
|
|
612
|
-
&& echo "$OUT" | grep -q '"task_count": 1'; then
|
|
613
|
-
pass "validate-plan accepts well-formed plan"
|
|
614
|
-
else
|
|
615
|
-
fail_case "validate well-formed plan" "exit=$EXIT out=$OUT"
|
|
616
|
-
fi
|
|
617
|
-
|
|
618
|
-
# 33. validate-plan rejects empty plan
|
|
619
|
-
TMP=$(make_project)
|
|
620
|
-
echo "" > "$TMP/.planning/phase-1-plan.md"
|
|
621
|
-
OUT=$(cd "$TMP" && $NODE "$STATE_JS" validate-plan --phase 1 2>&1)
|
|
622
|
-
EXIT=$?
|
|
623
|
-
if [ "$EXIT" -eq 1 ] \
|
|
624
|
-
&& echo "$OUT" | grep -q '"error": "PLAN_VALIDATION_FAILED"'; then
|
|
625
|
-
pass "validate-plan rejects empty plan"
|
|
626
|
-
else
|
|
627
|
-
fail_case "validate empty plan" "exit=$EXIT out=$OUT"
|
|
628
|
-
fi
|
|
629
|
-
|
|
630
|
-
# 34. validate-plan rejects plan missing Done when
|
|
631
|
-
TMP=$(make_project)
|
|
632
|
-
cat > "$TMP/.planning/phase-1-plan.md" <<'EOF'
|
|
633
|
-
---
|
|
634
|
-
phase: 1
|
|
635
|
-
goal: "Test"
|
|
636
|
-
tasks: 1
|
|
637
|
-
waves: 1
|
|
638
|
-
---
|
|
639
|
-
## Task 1 — Incomplete
|
|
640
|
-
**Wave:** 1
|
|
641
|
-
**Files:** test.ts
|
|
642
|
-
**Action:** Do something
|
|
643
|
-
|
|
644
|
-
## Success Criteria
|
|
645
|
-
- [ ] Works
|
|
646
|
-
EOF
|
|
647
|
-
OUT=$(cd "$TMP" && $NODE "$STATE_JS" validate-plan --phase 1 2>&1)
|
|
648
|
-
EXIT=$?
|
|
649
|
-
if [ "$EXIT" -eq 1 ] \
|
|
650
|
-
&& echo "$OUT" | grep -q "PLAN_VALIDATION_FAILED" \
|
|
651
|
-
&& echo "$OUT" | grep -q "Done when"; then
|
|
652
|
-
pass "validate-plan rejects plan missing 'Done when'"
|
|
653
|
-
else
|
|
654
|
-
fail_case "validate missing done-when" "exit=$EXIT out=$OUT"
|
|
655
|
-
fi
|
|
656
|
-
|
|
657
|
-
# 35. Transition to planned with invalid plan content → INVALID_PLAN
|
|
658
|
-
TMP=$(make_project)
|
|
659
|
-
echo "# Empty plan with no tasks" > "$TMP/.planning/phase-1-plan.md"
|
|
660
|
-
OUT=$(cd "$TMP" && $NODE "$STATE_JS" transition --to planned 2>&1)
|
|
661
|
-
EXIT=$?
|
|
662
|
-
if [ "$EXIT" -eq 1 ] \
|
|
663
|
-
&& echo "$OUT" | grep -q '"error": "INVALID_PLAN"'; then
|
|
664
|
-
pass "transition → planned with invalid plan → INVALID_PLAN"
|
|
665
|
-
else
|
|
666
|
-
fail_case "transition invalid plan" "exit=$EXIT out=$OUT"
|
|
667
|
-
fi
|
|
668
|
-
|
|
669
|
-
# ─── Force flag ──────────────────────────────────────────
|
|
670
|
-
echo ""
|
|
671
|
-
echo "force flag:"
|
|
672
|
-
|
|
673
|
-
# 36. --force bypasses precondition failure
|
|
674
|
-
TMP=$(make_project)
|
|
675
|
-
# setup → built should fail (requires planned first)
|
|
676
|
-
OUT=$(cd "$TMP" && $NODE "$STATE_JS" transition --to built --force 2>&1)
|
|
677
|
-
EXIT=$?
|
|
678
|
-
if [ "$EXIT" -eq 0 ] \
|
|
679
|
-
&& echo "$OUT" | grep -q '"ok": true' \
|
|
680
|
-
&& echo "$OUT" | grep -q '"status": "built"'; then
|
|
681
|
-
pass "--force bypasses precondition (setup → built)"
|
|
682
|
-
else
|
|
683
|
-
fail_case "force flag" "exit=$EXIT out=$OUT"
|
|
684
|
-
fi
|
|
685
|
-
|
|
686
|
-
# 37. --force does NOT bypass MISSING_FILE (planned without plan file)
|
|
687
|
-
TMP=$(make_project)
|
|
688
|
-
# No plan file exists — force should NOT help
|
|
689
|
-
OUT=$(cd "$TMP" && $NODE "$STATE_JS" transition --to planned --force 2>&1)
|
|
690
|
-
EXIT=$?
|
|
691
|
-
if [ "$EXIT" -eq 1 ] \
|
|
692
|
-
&& echo "$OUT" | grep -q '"error": "MISSING_FILE"'; then
|
|
693
|
-
pass "--force does NOT bypass MISSING_FILE"
|
|
694
|
-
else
|
|
695
|
-
fail_case "force vs MISSING_FILE" "exit=$EXIT out=$OUT"
|
|
696
|
-
fi
|
|
697
|
-
|
|
698
|
-
# 38. --force does NOT bypass INVALID_PLAN
|
|
699
|
-
TMP=$(make_project)
|
|
700
|
-
echo "# No tasks here" > "$TMP/.planning/phase-1-plan.md"
|
|
701
|
-
OUT=$(cd "$TMP" && $NODE "$STATE_JS" transition --to planned --force 2>&1)
|
|
702
|
-
EXIT=$?
|
|
703
|
-
if [ "$EXIT" -eq 1 ] \
|
|
704
|
-
&& echo "$OUT" | grep -q '"error": "INVALID_PLAN"'; then
|
|
705
|
-
pass "--force does NOT bypass INVALID_PLAN"
|
|
706
|
-
else
|
|
707
|
-
fail_case "force vs INVALID_PLAN" "exit=$EXIT out=$OUT"
|
|
708
|
-
fi
|
|
709
|
-
|
|
710
532
|
# ─── Summary ─────────────────────────────────────────────
|
|
711
533
|
echo ""
|
|
712
534
|
echo "=== Results: $PASS passed, $FAIL failed ==="
|
package/docs/erp-contract.md
DELETED
|
@@ -1,161 +0,0 @@
|
|
|
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/block-env-edit.js
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
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
|
-
const _traceStart = Date.now();
|
|
10
|
-
|
|
11
|
-
function readInput() {
|
|
12
|
-
try {
|
|
13
|
-
const raw = fs.readFileSync(0, "utf8");
|
|
14
|
-
return JSON.parse(raw);
|
|
15
|
-
} catch {
|
|
16
|
-
return {};
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const input = readInput();
|
|
21
|
-
const file = (input.tool_input && (input.tool_input.file_path || input.tool_input.command)) || "";
|
|
22
|
-
|
|
23
|
-
// Match .env, .env.local, .env.production, .env.*, etc.
|
|
24
|
-
// Normalize separators so Windows paths (C:\project\.env.local) also match.
|
|
25
|
-
const normalized = String(file).replace(/\\/g, "/");
|
|
26
|
-
|
|
27
|
-
function _trace(hookName, result, extra) {
|
|
28
|
-
try {
|
|
29
|
-
const os = require("os");
|
|
30
|
-
const path = require("path");
|
|
31
|
-
const traceDir = path.join(os.homedir(), ".claude", ".qualia-traces");
|
|
32
|
-
if (!fs.existsSync(traceDir)) fs.mkdirSync(traceDir, { recursive: true });
|
|
33
|
-
const entry = {
|
|
34
|
-
hook: hookName,
|
|
35
|
-
result,
|
|
36
|
-
timestamp: new Date().toISOString(),
|
|
37
|
-
duration_ms: Date.now() - _traceStart,
|
|
38
|
-
...extra,
|
|
39
|
-
};
|
|
40
|
-
const file = path.join(traceDir, `${new Date().toISOString().split("T")[0]}.jsonl`);
|
|
41
|
-
fs.appendFileSync(file, JSON.stringify(entry) + "\n");
|
|
42
|
-
} catch {}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
if (/\.env(\.|$)/.test(normalized)) {
|
|
46
|
-
console.log("BLOCKED: Cannot edit environment files. Ask Fawzi to update secrets.");
|
|
47
|
-
_trace("block-env-edit", "block", { file: normalized });
|
|
48
|
-
process.exit(2);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
_trace("block-env-edit", "allow");
|
|
52
|
-
process.exit(0);
|