qualia-framework-v2 2.6.0 → 2.8.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 +14 -6
- package/bin/cli.js +14 -6
- package/bin/install.js +34 -31
- package/bin/qualia-ui.js +6 -3
- package/bin/statusline.js +202 -0
- package/hooks/auto-update.js +92 -0
- package/hooks/block-env-edit.js +30 -0
- package/hooks/branch-guard.js +47 -0
- package/hooks/migration-guard.js +60 -0
- package/hooks/pre-compact.js +32 -0
- package/hooks/pre-deploy-gate.js +110 -0
- package/hooks/pre-push.js +33 -0
- package/hooks/session-start.js +84 -0
- package/package.json +3 -4
- package/skills/qualia-skill-new/SKILL.md +24 -5
- package/tests/hooks.test.sh +107 -55
- package/tests/state.test.sh +398 -0
- package/hooks/auto-update.sh +0 -56
- package/hooks/block-env-edit.sh +0 -11
- package/hooks/branch-guard.sh +0 -27
- package/hooks/migration-guard.sh +0 -43
- package/hooks/pre-compact.sh +0 -11
- package/hooks/pre-deploy-gate.sh +0 -50
- package/hooks/pre-push.sh +0 -28
- package/hooks/session-start.sh +0 -35
- package/statusline.sh +0 -93
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Qualia Framework v2 — state.js behavioral tests
|
|
3
|
+
# Run: bash tests/state.test.sh
|
|
4
|
+
|
|
5
|
+
PASS=0
|
|
6
|
+
FAIL=0
|
|
7
|
+
# Resolve STATE_JS to an ABSOLUTE path so `cd` inside subshells doesn't break it.
|
|
8
|
+
STATE_JS="$(cd "$(dirname "$0")/../bin" && pwd)/state.js"
|
|
9
|
+
NODE="${NODE:-node}"
|
|
10
|
+
|
|
11
|
+
# Track tmp dirs we create so we can clean them up on exit
|
|
12
|
+
TMP_DIRS=()
|
|
13
|
+
cleanup() {
|
|
14
|
+
for d in "${TMP_DIRS[@]}"; do
|
|
15
|
+
[ -d "$d" ] && rm -rf "$d"
|
|
16
|
+
done
|
|
17
|
+
}
|
|
18
|
+
trap cleanup EXIT
|
|
19
|
+
|
|
20
|
+
# Make a fresh temp project with 2 phases, already initialized.
|
|
21
|
+
# Prints the absolute path to the new tmp dir (does NOT cd).
|
|
22
|
+
make_project() {
|
|
23
|
+
local TMP
|
|
24
|
+
TMP=$(mktemp -d)
|
|
25
|
+
TMP_DIRS+=("$TMP")
|
|
26
|
+
(
|
|
27
|
+
cd "$TMP" || exit 1
|
|
28
|
+
$NODE "$STATE_JS" init \
|
|
29
|
+
--project "TestProject" \
|
|
30
|
+
--phases '[{"name":"Foundation","goal":"Auth"},{"name":"Core","goal":"Features"}]' \
|
|
31
|
+
>/dev/null 2>&1
|
|
32
|
+
)
|
|
33
|
+
echo "$TMP"
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
# pass "name" — record a passing assertion
|
|
37
|
+
pass() {
|
|
38
|
+
echo " ✓ $1"
|
|
39
|
+
PASS=$((PASS + 1))
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
# fail "name" "detail"
|
|
43
|
+
fail_case() {
|
|
44
|
+
echo " ✗ $1${2:+ — $2}"
|
|
45
|
+
FAIL=$((FAIL + 1))
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
echo "=== state.js Behavioral Tests ==="
|
|
49
|
+
echo ""
|
|
50
|
+
|
|
51
|
+
# Sanity check
|
|
52
|
+
if [ ! -f "$STATE_JS" ]; then
|
|
53
|
+
echo "FATAL: state.js not found at $STATE_JS"
|
|
54
|
+
exit 1
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
# ─── Basic I/O ───────────────────────────────────────────
|
|
58
|
+
echo "basic I/O:"
|
|
59
|
+
|
|
60
|
+
# 1. cmdInit produces valid tracking.json + STATE.md
|
|
61
|
+
TMP=$(mktemp -d); TMP_DIRS+=("$TMP")
|
|
62
|
+
(
|
|
63
|
+
cd "$TMP" || exit 1
|
|
64
|
+
$NODE "$STATE_JS" init \
|
|
65
|
+
--project "TestProject" \
|
|
66
|
+
--phases '[{"name":"Foundation","goal":"Auth"},{"name":"Core","goal":"Features"}]' \
|
|
67
|
+
>/tmp/qualia-state-test.out 2>&1
|
|
68
|
+
)
|
|
69
|
+
INIT_EXIT=$?
|
|
70
|
+
if [ "$INIT_EXIT" -eq 0 ] \
|
|
71
|
+
&& [ -f "$TMP/.planning/tracking.json" ] \
|
|
72
|
+
&& [ -f "$TMP/.planning/STATE.md" ] \
|
|
73
|
+
&& grep -q '"ok": true' /tmp/qualia-state-test.out \
|
|
74
|
+
&& grep -q '"action": "init"' /tmp/qualia-state-test.out; then
|
|
75
|
+
pass "cmdInit creates tracking.json + STATE.md"
|
|
76
|
+
else
|
|
77
|
+
fail_case "cmdInit creates tracking.json + STATE.md" "exit=$INIT_EXIT"
|
|
78
|
+
fi
|
|
79
|
+
|
|
80
|
+
# tracking.json content sanity
|
|
81
|
+
if grep -q '"project": "TestProject"' "$TMP/.planning/tracking.json" \
|
|
82
|
+
&& grep -q '"total_phases": 2' "$TMP/.planning/tracking.json" \
|
|
83
|
+
&& grep -q '"phase": 1' "$TMP/.planning/tracking.json" \
|
|
84
|
+
&& grep -q '"status": "setup"' "$TMP/.planning/tracking.json"; then
|
|
85
|
+
pass "cmdInit tracking.json has correct fields"
|
|
86
|
+
else
|
|
87
|
+
fail_case "cmdInit tracking.json fields"
|
|
88
|
+
fi
|
|
89
|
+
|
|
90
|
+
# STATE.md content sanity
|
|
91
|
+
if grep -q 'Phase: 1 of 2 — Foundation' "$TMP/.planning/STATE.md" \
|
|
92
|
+
&& grep -q 'Status: setup' "$TMP/.planning/STATE.md"; then
|
|
93
|
+
pass "cmdInit STATE.md has correct header"
|
|
94
|
+
else
|
|
95
|
+
fail_case "cmdInit STATE.md header"
|
|
96
|
+
fi
|
|
97
|
+
|
|
98
|
+
# 2. cmdCheck reads back init state
|
|
99
|
+
OUT=$(cd "$TMP" && $NODE "$STATE_JS" check 2>&1)
|
|
100
|
+
CHECK_EXIT=$?
|
|
101
|
+
if [ "$CHECK_EXIT" -eq 0 ] \
|
|
102
|
+
&& echo "$OUT" | grep -q '"ok": true' \
|
|
103
|
+
&& echo "$OUT" | grep -q '"phase": 1' \
|
|
104
|
+
&& echo "$OUT" | grep -q '"status": "setup"' \
|
|
105
|
+
&& echo "$OUT" | grep -q '"total_phases": 2'; then
|
|
106
|
+
pass "cmdCheck returns phase=1 status=setup total_phases=2"
|
|
107
|
+
else
|
|
108
|
+
fail_case "cmdCheck returns init state" "exit=$CHECK_EXIT"
|
|
109
|
+
fi
|
|
110
|
+
|
|
111
|
+
# 3. cmdCheck with no project → ok:false NO_PROJECT, exit 1
|
|
112
|
+
TMP2=$(mktemp -d); TMP_DIRS+=("$TMP2")
|
|
113
|
+
OUT=$(cd "$TMP2" && $NODE "$STATE_JS" check 2>&1)
|
|
114
|
+
CHECK_EXIT=$?
|
|
115
|
+
if [ "$CHECK_EXIT" -eq 1 ] \
|
|
116
|
+
&& echo "$OUT" | grep -q '"ok": false' \
|
|
117
|
+
&& echo "$OUT" | grep -q '"error": "NO_PROJECT"'; then
|
|
118
|
+
pass "cmdCheck without .planning → NO_PROJECT, exit 1"
|
|
119
|
+
else
|
|
120
|
+
fail_case "cmdCheck NO_PROJECT" "exit=$CHECK_EXIT"
|
|
121
|
+
fi
|
|
122
|
+
|
|
123
|
+
# ─── Happy path transitions ──────────────────────────────
|
|
124
|
+
echo ""
|
|
125
|
+
echo "happy path transitions:"
|
|
126
|
+
|
|
127
|
+
# 4. setup → planned (with plan file)
|
|
128
|
+
TMP=$(make_project)
|
|
129
|
+
touch "$TMP/.planning/phase-1-plan.md"
|
|
130
|
+
OUT=$(cd "$TMP" && $NODE "$STATE_JS" transition --to planned 2>&1)
|
|
131
|
+
EXIT=$?
|
|
132
|
+
if [ "$EXIT" -eq 0 ] \
|
|
133
|
+
&& echo "$OUT" | grep -q '"ok": true' \
|
|
134
|
+
&& echo "$OUT" | grep -q '"status": "planned"' \
|
|
135
|
+
&& echo "$OUT" | grep -q '"previous_status": "setup"'; then
|
|
136
|
+
pass "setup → planned succeeds with plan file"
|
|
137
|
+
else
|
|
138
|
+
fail_case "setup → planned" "exit=$EXIT out=$OUT"
|
|
139
|
+
fi
|
|
140
|
+
|
|
141
|
+
# 5. planned → built (records tasks_done/tasks_total)
|
|
142
|
+
OUT=$(cd "$TMP" && $NODE "$STATE_JS" transition --to built --tasks-done 5 --tasks-total 5 2>&1)
|
|
143
|
+
EXIT=$?
|
|
144
|
+
if [ "$EXIT" -eq 0 ] \
|
|
145
|
+
&& echo "$OUT" | grep -q '"ok": true' \
|
|
146
|
+
&& echo "$OUT" | grep -q '"status": "built"' \
|
|
147
|
+
&& grep -q '"tasks_done": 5' "$TMP/.planning/tracking.json" \
|
|
148
|
+
&& grep -q '"tasks_total": 5' "$TMP/.planning/tracking.json"; then
|
|
149
|
+
pass "planned → built records tasks_done/tasks_total"
|
|
150
|
+
else
|
|
151
|
+
fail_case "planned → built" "exit=$EXIT"
|
|
152
|
+
fi
|
|
153
|
+
|
|
154
|
+
# 6. built → verified(pass) auto-advances to phase 2, resets status to setup
|
|
155
|
+
touch "$TMP/.planning/phase-1-verification.md"
|
|
156
|
+
OUT=$(cd "$TMP" && $NODE "$STATE_JS" transition --to verified --verification pass 2>&1)
|
|
157
|
+
EXIT=$?
|
|
158
|
+
if [ "$EXIT" -eq 0 ] \
|
|
159
|
+
&& echo "$OUT" | grep -q '"ok": true' \
|
|
160
|
+
&& echo "$OUT" | grep -q '"phase": 2' \
|
|
161
|
+
&& echo "$OUT" | grep -q '"status": "setup"'; then
|
|
162
|
+
pass "built → verified(pass) auto-advances phase and resets to setup"
|
|
163
|
+
else
|
|
164
|
+
fail_case "built → verified(pass) auto-advance" "exit=$EXIT out=$OUT"
|
|
165
|
+
fi
|
|
166
|
+
|
|
167
|
+
# 7. built → verified(fail) stays on phase 1, records verification=fail
|
|
168
|
+
TMP=$(make_project)
|
|
169
|
+
touch "$TMP/.planning/phase-1-plan.md"
|
|
170
|
+
(cd "$TMP" && $NODE "$STATE_JS" transition --to planned >/dev/null 2>&1)
|
|
171
|
+
(cd "$TMP" && $NODE "$STATE_JS" transition --to built --tasks-done 3 --tasks-total 5 >/dev/null 2>&1)
|
|
172
|
+
touch "$TMP/.planning/phase-1-verification.md"
|
|
173
|
+
OUT=$(cd "$TMP" && $NODE "$STATE_JS" transition --to verified --verification fail 2>&1)
|
|
174
|
+
EXIT=$?
|
|
175
|
+
if [ "$EXIT" -eq 0 ] \
|
|
176
|
+
&& echo "$OUT" | grep -q '"ok": true' \
|
|
177
|
+
&& echo "$OUT" | grep -q '"phase": 1' \
|
|
178
|
+
&& echo "$OUT" | grep -q '"status": "verified"' \
|
|
179
|
+
&& echo "$OUT" | grep -q '"verification": "fail"'; then
|
|
180
|
+
pass "built → verified(fail) stays on phase 1"
|
|
181
|
+
else
|
|
182
|
+
fail_case "built → verified(fail)" "exit=$EXIT out=$OUT"
|
|
183
|
+
fi
|
|
184
|
+
|
|
185
|
+
# ─── Precondition failures ───────────────────────────────
|
|
186
|
+
echo ""
|
|
187
|
+
echo "precondition failures:"
|
|
188
|
+
|
|
189
|
+
# 8. setup → built fails with PRECONDITION_FAILED
|
|
190
|
+
TMP=$(make_project)
|
|
191
|
+
OUT=$(cd "$TMP" && $NODE "$STATE_JS" transition --to built 2>&1)
|
|
192
|
+
EXIT=$?
|
|
193
|
+
if [ "$EXIT" -eq 1 ] \
|
|
194
|
+
&& echo "$OUT" | grep -q '"ok": false' \
|
|
195
|
+
&& echo "$OUT" | grep -q '"error": "PRECONDITION_FAILED"' \
|
|
196
|
+
&& echo "$OUT" | grep -q "Cannot go from 'setup' to 'built'"; then
|
|
197
|
+
pass "setup → built fails with PRECONDITION_FAILED"
|
|
198
|
+
else
|
|
199
|
+
fail_case "setup → built precondition" "exit=$EXIT out=$OUT"
|
|
200
|
+
fi
|
|
201
|
+
|
|
202
|
+
# 9. planned → verified fails (requires status=built)
|
|
203
|
+
TMP=$(make_project)
|
|
204
|
+
touch "$TMP/.planning/phase-1-plan.md"
|
|
205
|
+
(cd "$TMP" && $NODE "$STATE_JS" transition --to planned >/dev/null 2>&1)
|
|
206
|
+
touch "$TMP/.planning/phase-1-verification.md"
|
|
207
|
+
OUT=$(cd "$TMP" && $NODE "$STATE_JS" transition --to verified --verification pass 2>&1)
|
|
208
|
+
EXIT=$?
|
|
209
|
+
if [ "$EXIT" -eq 1 ] \
|
|
210
|
+
&& echo "$OUT" | grep -q '"error": "PRECONDITION_FAILED"' \
|
|
211
|
+
&& echo "$OUT" | grep -q "Cannot go from 'planned' to 'verified'"; then
|
|
212
|
+
pass "planned → verified fails (requires built)"
|
|
213
|
+
else
|
|
214
|
+
fail_case "planned → verified precondition" "exit=$EXIT out=$OUT"
|
|
215
|
+
fi
|
|
216
|
+
|
|
217
|
+
# 10. planned with missing plan file → MISSING_FILE
|
|
218
|
+
TMP=$(make_project)
|
|
219
|
+
# no phase-1-plan.md created
|
|
220
|
+
OUT=$(cd "$TMP" && $NODE "$STATE_JS" transition --to planned 2>&1)
|
|
221
|
+
EXIT=$?
|
|
222
|
+
if [ "$EXIT" -eq 1 ] \
|
|
223
|
+
&& echo "$OUT" | grep -q '"error": "MISSING_FILE"' \
|
|
224
|
+
&& echo "$OUT" | grep -q "phase-1-plan.md"; then
|
|
225
|
+
pass "setup → planned fails without plan file (MISSING_FILE)"
|
|
226
|
+
else
|
|
227
|
+
fail_case "setup → planned MISSING_FILE" "exit=$EXIT out=$OUT"
|
|
228
|
+
fi
|
|
229
|
+
|
|
230
|
+
# 11. built → verified with missing verification file → MISSING_FILE
|
|
231
|
+
TMP=$(make_project)
|
|
232
|
+
touch "$TMP/.planning/phase-1-plan.md"
|
|
233
|
+
(cd "$TMP" && $NODE "$STATE_JS" transition --to planned >/dev/null 2>&1)
|
|
234
|
+
(cd "$TMP" && $NODE "$STATE_JS" transition --to built --tasks-done 1 --tasks-total 1 >/dev/null 2>&1)
|
|
235
|
+
# NO verification file
|
|
236
|
+
OUT=$(cd "$TMP" && $NODE "$STATE_JS" transition --to verified --verification pass 2>&1)
|
|
237
|
+
EXIT=$?
|
|
238
|
+
if [ "$EXIT" -eq 1 ] \
|
|
239
|
+
&& echo "$OUT" | grep -q '"error": "MISSING_FILE"' \
|
|
240
|
+
&& echo "$OUT" | grep -q "phase-1-verification.md"; then
|
|
241
|
+
pass "built → verified fails without verification file (MISSING_FILE)"
|
|
242
|
+
else
|
|
243
|
+
fail_case "built → verified MISSING_FILE" "exit=$EXIT out=$OUT"
|
|
244
|
+
fi
|
|
245
|
+
|
|
246
|
+
# 12. built → verified without --verification → MISSING_ARG
|
|
247
|
+
TMP=$(make_project)
|
|
248
|
+
touch "$TMP/.planning/phase-1-plan.md"
|
|
249
|
+
(cd "$TMP" && $NODE "$STATE_JS" transition --to planned >/dev/null 2>&1)
|
|
250
|
+
(cd "$TMP" && $NODE "$STATE_JS" transition --to built --tasks-done 1 --tasks-total 1 >/dev/null 2>&1)
|
|
251
|
+
touch "$TMP/.planning/phase-1-verification.md"
|
|
252
|
+
OUT=$(cd "$TMP" && $NODE "$STATE_JS" transition --to verified 2>&1)
|
|
253
|
+
EXIT=$?
|
|
254
|
+
if [ "$EXIT" -eq 1 ] \
|
|
255
|
+
&& echo "$OUT" | grep -q '"error": "MISSING_ARG"' \
|
|
256
|
+
&& echo "$OUT" | grep -q "verification"; then
|
|
257
|
+
pass "built → verified without --verification → MISSING_ARG"
|
|
258
|
+
else
|
|
259
|
+
fail_case "built → verified MISSING_ARG" "exit=$EXIT out=$OUT"
|
|
260
|
+
fi
|
|
261
|
+
|
|
262
|
+
# 13. → shipped without --deployed-url → MISSING_ARG
|
|
263
|
+
# Must go through polished first, so fabricate state by transitioning through the full path.
|
|
264
|
+
TMP=$(make_project)
|
|
265
|
+
touch "$TMP/.planning/phase-1-plan.md"
|
|
266
|
+
(cd "$TMP" && $NODE "$STATE_JS" transition --to planned >/dev/null 2>&1)
|
|
267
|
+
(cd "$TMP" && $NODE "$STATE_JS" transition --to built --tasks-done 1 --tasks-total 1 >/dev/null 2>&1)
|
|
268
|
+
touch "$TMP/.planning/phase-1-verification.md"
|
|
269
|
+
(cd "$TMP" && $NODE "$STATE_JS" transition --to verified --verification pass >/dev/null 2>&1)
|
|
270
|
+
# Now on phase 2, status=setup. Run phase 2 to completion.
|
|
271
|
+
touch "$TMP/.planning/phase-2-plan.md"
|
|
272
|
+
(cd "$TMP" && $NODE "$STATE_JS" transition --to planned >/dev/null 2>&1)
|
|
273
|
+
(cd "$TMP" && $NODE "$STATE_JS" transition --to built --tasks-done 1 --tasks-total 1 >/dev/null 2>&1)
|
|
274
|
+
touch "$TMP/.planning/phase-2-verification.md"
|
|
275
|
+
(cd "$TMP" && $NODE "$STATE_JS" transition --to verified --verification pass >/dev/null 2>&1)
|
|
276
|
+
# Status should now be "verified" on last phase (no auto-advance past last phase)
|
|
277
|
+
(cd "$TMP" && $NODE "$STATE_JS" transition --to polished >/dev/null 2>&1)
|
|
278
|
+
# Now try ship without deployed-url
|
|
279
|
+
OUT=$(cd "$TMP" && $NODE "$STATE_JS" transition --to shipped 2>&1)
|
|
280
|
+
EXIT=$?
|
|
281
|
+
if [ "$EXIT" -eq 1 ] \
|
|
282
|
+
&& echo "$OUT" | grep -q '"error": "MISSING_ARG"' \
|
|
283
|
+
&& echo "$OUT" | grep -q "deployed-url"; then
|
|
284
|
+
pass "→ shipped without --deployed-url → MISSING_ARG"
|
|
285
|
+
else
|
|
286
|
+
fail_case "→ shipped MISSING_ARG" "exit=$EXIT out=$OUT"
|
|
287
|
+
fi
|
|
288
|
+
|
|
289
|
+
# 14. Unknown target --to frobnicate → INVALID_STATUS
|
|
290
|
+
TMP=$(make_project)
|
|
291
|
+
OUT=$(cd "$TMP" && $NODE "$STATE_JS" transition --to frobnicate 2>&1)
|
|
292
|
+
EXIT=$?
|
|
293
|
+
if [ "$EXIT" -eq 1 ] \
|
|
294
|
+
&& echo "$OUT" | grep -q '"error": "INVALID_STATUS"'; then
|
|
295
|
+
pass "--to frobnicate → INVALID_STATUS"
|
|
296
|
+
else
|
|
297
|
+
fail_case "invalid target" "exit=$EXIT out=$OUT"
|
|
298
|
+
fi
|
|
299
|
+
|
|
300
|
+
# ─── Gap cycle circuit breaker ───────────────────────────
|
|
301
|
+
echo ""
|
|
302
|
+
echo "gap cycle circuit breaker:"
|
|
303
|
+
|
|
304
|
+
# 15. First gap closure: verified(fail) → planned, gap_cycles[1]=1
|
|
305
|
+
TMP=$(make_project)
|
|
306
|
+
touch "$TMP/.planning/phase-1-plan.md"
|
|
307
|
+
touch "$TMP/.planning/phase-1-verification.md"
|
|
308
|
+
(cd "$TMP" && $NODE "$STATE_JS" transition --to planned >/dev/null 2>&1)
|
|
309
|
+
(cd "$TMP" && $NODE "$STATE_JS" transition --to built --tasks-done 1 --tasks-total 1 >/dev/null 2>&1)
|
|
310
|
+
(cd "$TMP" && $NODE "$STATE_JS" transition --to verified --verification fail >/dev/null 2>&1)
|
|
311
|
+
OUT=$(cd "$TMP" && $NODE "$STATE_JS" transition --to planned 2>&1)
|
|
312
|
+
EXIT=$?
|
|
313
|
+
if [ "$EXIT" -eq 0 ] \
|
|
314
|
+
&& echo "$OUT" | grep -q '"ok": true' \
|
|
315
|
+
&& echo "$OUT" | grep -q '"gap_cycles": 1'; then
|
|
316
|
+
pass "first gap closure: verified(fail) → planned, gap_cycles=1"
|
|
317
|
+
else
|
|
318
|
+
fail_case "first gap closure" "exit=$EXIT out=$OUT"
|
|
319
|
+
fi
|
|
320
|
+
|
|
321
|
+
# 16. Second gap closure: gap_cycles[1]=2
|
|
322
|
+
(cd "$TMP" && $NODE "$STATE_JS" transition --to built --tasks-done 1 --tasks-total 1 >/dev/null 2>&1)
|
|
323
|
+
(cd "$TMP" && $NODE "$STATE_JS" transition --to verified --verification fail >/dev/null 2>&1)
|
|
324
|
+
OUT=$(cd "$TMP" && $NODE "$STATE_JS" transition --to planned 2>&1)
|
|
325
|
+
EXIT=$?
|
|
326
|
+
if [ "$EXIT" -eq 0 ] \
|
|
327
|
+
&& echo "$OUT" | grep -q '"ok": true' \
|
|
328
|
+
&& echo "$OUT" | grep -q '"gap_cycles": 2'; then
|
|
329
|
+
pass "second gap closure: gap_cycles=2"
|
|
330
|
+
else
|
|
331
|
+
fail_case "second gap closure" "exit=$EXIT out=$OUT"
|
|
332
|
+
fi
|
|
333
|
+
|
|
334
|
+
# 17. Third gap closure attempt → GAP_CYCLE_LIMIT
|
|
335
|
+
(cd "$TMP" && $NODE "$STATE_JS" transition --to built --tasks-done 1 --tasks-total 1 >/dev/null 2>&1)
|
|
336
|
+
(cd "$TMP" && $NODE "$STATE_JS" transition --to verified --verification fail >/dev/null 2>&1)
|
|
337
|
+
OUT=$(cd "$TMP" && $NODE "$STATE_JS" transition --to planned 2>&1)
|
|
338
|
+
EXIT=$?
|
|
339
|
+
if [ "$EXIT" -eq 1 ] \
|
|
340
|
+
&& echo "$OUT" | grep -q '"error": "GAP_CYCLE_LIMIT"'; then
|
|
341
|
+
pass "third gap closure attempt blocked (GAP_CYCLE_LIMIT)"
|
|
342
|
+
else
|
|
343
|
+
fail_case "gap cycle limit" "exit=$EXIT out=$OUT"
|
|
344
|
+
fi
|
|
345
|
+
|
|
346
|
+
# 18. verified(pass) resets gap_cycles[1] to 0
|
|
347
|
+
# Set up a fresh project, do ONE failed cycle, then pass on the next attempt.
|
|
348
|
+
TMP=$(make_project)
|
|
349
|
+
touch "$TMP/.planning/phase-1-plan.md"
|
|
350
|
+
touch "$TMP/.planning/phase-1-verification.md"
|
|
351
|
+
(cd "$TMP" && $NODE "$STATE_JS" transition --to planned >/dev/null 2>&1)
|
|
352
|
+
(cd "$TMP" && $NODE "$STATE_JS" transition --to built --tasks-done 1 --tasks-total 1 >/dev/null 2>&1)
|
|
353
|
+
(cd "$TMP" && $NODE "$STATE_JS" transition --to verified --verification fail >/dev/null 2>&1)
|
|
354
|
+
# gap_cycles[1] is now 0 before the gap closure; becomes 1 after
|
|
355
|
+
(cd "$TMP" && $NODE "$STATE_JS" transition --to planned >/dev/null 2>&1)
|
|
356
|
+
(cd "$TMP" && $NODE "$STATE_JS" transition --to built --tasks-done 1 --tasks-total 1 >/dev/null 2>&1)
|
|
357
|
+
(cd "$TMP" && $NODE "$STATE_JS" transition --to verified --verification pass >/dev/null 2>&1)
|
|
358
|
+
# After pass, gap_cycles[1] should be reset to 0 in tracking.json
|
|
359
|
+
if grep -q '"1": 0' "$TMP/.planning/tracking.json"; then
|
|
360
|
+
pass "verified(pass) resets gap_cycles[1] to 0"
|
|
361
|
+
else
|
|
362
|
+
fail_case "gap cycle reset on pass"
|
|
363
|
+
fi
|
|
364
|
+
|
|
365
|
+
# ─── Special transitions ─────────────────────────────────
|
|
366
|
+
echo ""
|
|
367
|
+
echo "special transitions:"
|
|
368
|
+
|
|
369
|
+
# 19. --to note --notes "foo" succeeds, records notes
|
|
370
|
+
TMP=$(make_project)
|
|
371
|
+
OUT=$(cd "$TMP" && $NODE "$STATE_JS" transition --to note --notes "hello world" 2>&1)
|
|
372
|
+
EXIT=$?
|
|
373
|
+
if [ "$EXIT" -eq 0 ] \
|
|
374
|
+
&& echo "$OUT" | grep -q '"ok": true' \
|
|
375
|
+
&& echo "$OUT" | grep -q '"action": "note"' \
|
|
376
|
+
&& echo "$OUT" | grep -q '"status": "setup"' \
|
|
377
|
+
&& grep -q '"notes": "hello world"' "$TMP/.planning/tracking.json"; then
|
|
378
|
+
pass "--to note records notes, status unchanged"
|
|
379
|
+
else
|
|
380
|
+
fail_case "--to note" "exit=$EXIT out=$OUT"
|
|
381
|
+
fi
|
|
382
|
+
|
|
383
|
+
# 20. --to activity succeeds without status change
|
|
384
|
+
OUT=$(cd "$TMP" && $NODE "$STATE_JS" transition --to activity 2>&1)
|
|
385
|
+
EXIT=$?
|
|
386
|
+
if [ "$EXIT" -eq 0 ] \
|
|
387
|
+
&& echo "$OUT" | grep -q '"ok": true' \
|
|
388
|
+
&& echo "$OUT" | grep -q '"action": "activity"' \
|
|
389
|
+
&& echo "$OUT" | grep -q '"status": "setup"'; then
|
|
390
|
+
pass "--to activity succeeds without status change"
|
|
391
|
+
else
|
|
392
|
+
fail_case "--to activity" "exit=$EXIT out=$OUT"
|
|
393
|
+
fi
|
|
394
|
+
|
|
395
|
+
# ─── Summary ─────────────────────────────────────────────
|
|
396
|
+
echo ""
|
|
397
|
+
echo "=== Results: $PASS passed, $FAIL failed ==="
|
|
398
|
+
[ "$FAIL" -eq 0 ] && exit 0 || exit 1
|
package/hooks/auto-update.sh
DELETED
|
@@ -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
|
package/hooks/block-env-edit.sh
DELETED
|
@@ -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
|
package/hooks/branch-guard.sh
DELETED
|
@@ -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
|
package/hooks/migration-guard.sh
DELETED
|
@@ -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
|
package/hooks/pre-compact.sh
DELETED
|
@@ -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
|
package/hooks/pre-deploy-gate.sh
DELETED
|
@@ -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
|
package/hooks/session-start.sh
DELETED
|
@@ -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
|