qualia-framework 6.9.0 → 6.14.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/CHANGELOG.md +88 -0
- package/agents/verifier.md +1 -1
- package/bin/agent-status.js +251 -0
- package/bin/analyze-gate.js +318 -0
- package/bin/command-surface.js +1 -0
- package/bin/install.js +31 -4
- package/bin/report-payload.js +19 -0
- package/bin/runtime-manifest.js +2 -0
- package/bin/state.js +145 -11
- package/docs/EMPLOYEE-QUICKSTART.md +5 -3
- package/docs/erp-contract.md +33 -1
- package/docs/qualia-manual.html +396 -0
- package/hooks/branch-guard.js +133 -63
- package/hooks/pre-deploy-gate.js +38 -0
- package/hooks/session-start.js +24 -4
- package/hooks/task-write-guard.js +165 -0
- package/hooks/usage-capture.js +108 -0
- package/package.json +2 -1
- package/skills/qualia-build/SKILL.md +30 -1
- package/skills/qualia-report/SKILL.md +3 -0
- package/skills/qualia-ship/SKILL.md +3 -0
- package/skills/qualia-update/SKILL.md +96 -0
- package/skills/qualia-verify/SKILL.md +7 -1
- package/templates/journey.md +1 -1
- package/templates/planning.gitignore +3 -0
- package/tests/agent-status.test.sh +138 -0
- package/tests/analyze-gate.test.sh +170 -0
- package/tests/bin.test.sh +6 -4
- package/tests/hooks.test.sh +250 -17
- package/tests/install-smoke.test.sh +5 -3
- package/tests/lib.test.sh +2 -2
- package/tests/run-all.sh +2 -0
- package/tests/runner.js +3 -2
- package/tests/state.test.sh +95 -0
- package/skills/qualia-discuss/SKILL.md +0 -222
package/tests/hooks.test.sh
CHANGED
|
@@ -96,29 +96,78 @@ TMP=$(setup_guard_repo main OWNER)
|
|
|
96
96
|
assert_exit "OWNER on main → allowed" 0 $?
|
|
97
97
|
rm -rf "$TMP"
|
|
98
98
|
|
|
99
|
-
# EMPLOYEE on main →
|
|
99
|
+
# EMPLOYEE on main → ALLOWED + counted + notice (v6.10 policy: accountability, not block)
|
|
100
100
|
TMP=$(setup_guard_repo main EMPLOYEE)
|
|
101
101
|
OUT=$(cd "$TMP/proj" && HOME="$TMP" $NODE "$HOOKS_DIR/branch-guard.js" 2>&1)
|
|
102
102
|
RC=$?
|
|
103
|
-
if [ "$RC" -eq
|
|
104
|
-
|
|
103
|
+
if [ "$RC" -eq 0 ] \
|
|
104
|
+
&& echo "$OUT" | grep -q "NOTICE" \
|
|
105
|
+
&& grep -q "employee_main_push" "$TMP/.claude/.main-push-events.json" 2>/dev/null \
|
|
106
|
+
&& grep -q '"total": 1' "$TMP/.claude/.main-push-events.json" 2>/dev/null; then
|
|
107
|
+
echo " ✓ EMPLOYEE on main → allowed + recorded + notice"
|
|
108
|
+
PASS=$((PASS + 1))
|
|
109
|
+
else
|
|
110
|
+
echo " ✗ EMPLOYEE on main → allowed+recorded (exit=$RC out=$OUT)"
|
|
111
|
+
FAIL=$((FAIL + 1))
|
|
112
|
+
fi
|
|
113
|
+
rm -rf "$TMP"
|
|
114
|
+
|
|
115
|
+
# EMPLOYEE repeated main push → per-employee count increments (framework counts)
|
|
116
|
+
TMP=$(setup_guard_repo main EMPLOYEE)
|
|
117
|
+
(cd "$TMP/proj" && HOME="$TMP" $NODE "$HOOKS_DIR/branch-guard.js" >/dev/null 2>&1)
|
|
118
|
+
(cd "$TMP/proj" && HOME="$TMP" $NODE "$HOOKS_DIR/branch-guard.js" >/dev/null 2>&1)
|
|
119
|
+
if grep -q '"total": 2' "$TMP/.claude/.main-push-events.json" 2>/dev/null; then
|
|
120
|
+
echo " ✓ EMPLOYEE repeated main push → count increments to 2"
|
|
121
|
+
PASS=$((PASS + 1))
|
|
122
|
+
else
|
|
123
|
+
echo " ✗ EMPLOYEE repeated main push → count increments"
|
|
124
|
+
FAIL=$((FAIL + 1))
|
|
125
|
+
fi
|
|
126
|
+
rm -rf "$TMP"
|
|
127
|
+
|
|
128
|
+
# EMPLOYEE refspec push to main from a feature branch → recorded
|
|
129
|
+
TMP=$(setup_guard_repo feature/xyz EMPLOYEE)
|
|
130
|
+
OUT=$(cd "$TMP/proj" && printf '%s' '{"tool_input":{"command":"git push origin feature/xyz:main"}}' | HOME="$TMP" $NODE "$HOOKS_DIR/branch-guard.js" 2>&1)
|
|
131
|
+
RC=$?
|
|
132
|
+
if [ "$RC" -eq 0 ] && grep -q "employee_main_push" "$TMP/.claude/.main-push-events.json" 2>/dev/null; then
|
|
133
|
+
echo " ✓ EMPLOYEE refspec push to :main → allowed + recorded"
|
|
105
134
|
PASS=$((PASS + 1))
|
|
106
135
|
else
|
|
107
|
-
echo " ✗ EMPLOYEE
|
|
136
|
+
echo " ✗ EMPLOYEE refspec push to :main → recorded (exit=$RC)"
|
|
108
137
|
FAIL=$((FAIL + 1))
|
|
109
138
|
fi
|
|
110
139
|
rm -rf "$TMP"
|
|
111
140
|
|
|
112
|
-
# EMPLOYEE on master →
|
|
141
|
+
# EMPLOYEE on master → allowed + recorded
|
|
113
142
|
TMP=$(setup_guard_repo master EMPLOYEE)
|
|
114
143
|
(cd "$TMP/proj" && HOME="$TMP" $NODE "$HOOKS_DIR/branch-guard.js" >/dev/null 2>&1)
|
|
115
|
-
assert_exit "EMPLOYEE on master →
|
|
144
|
+
assert_exit "EMPLOYEE on master → allowed (recorded)" 0 $?
|
|
116
145
|
rm -rf "$TMP"
|
|
117
146
|
|
|
118
|
-
# EMPLOYEE on feature branch → allowed
|
|
147
|
+
# EMPLOYEE on feature branch → allowed, NOT recorded
|
|
119
148
|
TMP=$(setup_guard_repo feature/xyz EMPLOYEE)
|
|
120
149
|
(cd "$TMP/proj" && HOME="$TMP" $NODE "$HOOKS_DIR/branch-guard.js" >/dev/null 2>&1)
|
|
121
|
-
|
|
150
|
+
RC=$?
|
|
151
|
+
if [ "$RC" -eq 0 ] && [ ! -f "$TMP/.claude/.main-push-events.json" ]; then
|
|
152
|
+
echo " ✓ EMPLOYEE on feature/xyz → allowed, not recorded"
|
|
153
|
+
PASS=$((PASS + 1))
|
|
154
|
+
else
|
|
155
|
+
echo " ✗ EMPLOYEE on feature/xyz → allowed/not-recorded (exit=$RC)"
|
|
156
|
+
FAIL=$((FAIL + 1))
|
|
157
|
+
fi
|
|
158
|
+
rm -rf "$TMP"
|
|
159
|
+
|
|
160
|
+
# OWNER on main → allowed, NOT recorded (OWNER pushes are unremarkable)
|
|
161
|
+
TMP=$(setup_guard_repo main OWNER)
|
|
162
|
+
(cd "$TMP/proj" && HOME="$TMP" $NODE "$HOOKS_DIR/branch-guard.js" >/dev/null 2>&1)
|
|
163
|
+
RC=$?
|
|
164
|
+
if [ "$RC" -eq 0 ] && [ ! -f "$TMP/.claude/.main-push-events.json" ]; then
|
|
165
|
+
echo " ✓ OWNER on main → allowed, not recorded"
|
|
166
|
+
PASS=$((PASS + 1))
|
|
167
|
+
else
|
|
168
|
+
echo " ✗ OWNER on main → allowed/not-recorded (exit=$RC)"
|
|
169
|
+
FAIL=$((FAIL + 1))
|
|
170
|
+
fi
|
|
122
171
|
rm -rf "$TMP"
|
|
123
172
|
|
|
124
173
|
# OWNER on feature branch → allowed
|
|
@@ -127,31 +176,38 @@ TMP=$(setup_guard_repo feature/xyz OWNER)
|
|
|
127
176
|
assert_exit "OWNER on feature/xyz → allowed" 0 $?
|
|
128
177
|
rm -rf "$TMP"
|
|
129
178
|
|
|
130
|
-
# Missing config →
|
|
179
|
+
# Missing config → allowed (the hook never blocks now)
|
|
131
180
|
TMP=$(mktemp -d)
|
|
132
181
|
mkdir -p "$TMP/proj"
|
|
133
|
-
(cd "$TMP/proj" && git init -q && git checkout -b
|
|
182
|
+
(cd "$TMP/proj" && git init -q && git checkout -b main -q 2>/dev/null)
|
|
134
183
|
# NO .claude/.qualia-config.json
|
|
135
184
|
(cd "$TMP/proj" && HOME="$TMP" $NODE "$HOOKS_DIR/branch-guard.js" >/dev/null 2>&1)
|
|
136
|
-
assert_exit "missing config →
|
|
185
|
+
assert_exit "missing config → allowed (never blocks)" 0 $?
|
|
137
186
|
rm -rf "$TMP"
|
|
138
187
|
|
|
139
|
-
# Malformed config JSON →
|
|
188
|
+
# Malformed config JSON → allowed
|
|
140
189
|
TMP=$(mktemp -d)
|
|
141
190
|
mkdir -p "$TMP/proj" "$TMP/.claude"
|
|
142
|
-
(cd "$TMP/proj" && git init -q && git checkout -b
|
|
191
|
+
(cd "$TMP/proj" && git init -q && git checkout -b main -q 2>/dev/null)
|
|
143
192
|
echo 'not json{' > "$TMP/.claude/.qualia-config.json"
|
|
144
193
|
(cd "$TMP/proj" && HOME="$TMP" $NODE "$HOOKS_DIR/branch-guard.js" >/dev/null 2>&1)
|
|
145
|
-
assert_exit "malformed config JSON →
|
|
194
|
+
assert_exit "malformed config JSON → allowed" 0 $?
|
|
146
195
|
rm -rf "$TMP"
|
|
147
196
|
|
|
148
|
-
# Empty role field →
|
|
197
|
+
# Empty role field → allowed, not recorded (not a known EMPLOYEE)
|
|
149
198
|
TMP=$(mktemp -d)
|
|
150
199
|
mkdir -p "$TMP/proj" "$TMP/.claude"
|
|
151
|
-
(cd "$TMP/proj" && git init -q && git checkout -b
|
|
200
|
+
(cd "$TMP/proj" && git init -q && git checkout -b main -q 2>/dev/null)
|
|
152
201
|
echo '{"role":""}' > "$TMP/.claude/.qualia-config.json"
|
|
153
202
|
(cd "$TMP/proj" && HOME="$TMP" $NODE "$HOOKS_DIR/branch-guard.js" >/dev/null 2>&1)
|
|
154
|
-
|
|
203
|
+
RC=$?
|
|
204
|
+
if [ "$RC" -eq 0 ] && [ ! -f "$TMP/.claude/.main-push-events.json" ]; then
|
|
205
|
+
echo " ✓ empty role → allowed, not recorded"
|
|
206
|
+
PASS=$((PASS + 1))
|
|
207
|
+
else
|
|
208
|
+
echo " ✗ empty role → allowed/not-recorded (exit=$RC)"
|
|
209
|
+
FAIL=$((FAIL + 1))
|
|
210
|
+
fi
|
|
155
211
|
rm -rf "$TMP"
|
|
156
212
|
|
|
157
213
|
# --- fawzi-approval-guard.js ---
|
|
@@ -382,6 +438,72 @@ RC=$?
|
|
|
382
438
|
assert_exit "regular page.tsx with service_role → blocked (exit 2)" 2 $RC
|
|
383
439
|
rm -rf "$TMP"
|
|
384
440
|
|
|
441
|
+
# --- pre-deploy-gate: anti-slop gate (slop-detect.mjs wired into ship) ---
|
|
442
|
+
# Build a tmp QUALIA_HOME carrying bin/slop-detect.mjs so the gate can find it.
|
|
443
|
+
SLOP_SRC="$(cd "$(dirname "$0")/../bin" && pwd)/slop-detect.mjs"
|
|
444
|
+
|
|
445
|
+
# Purple-blue gradient (CRITICAL design tell) → blocked with anti-slop diagnostic
|
|
446
|
+
TMP=$(mktemp -d)
|
|
447
|
+
QH=$(mktemp -d)
|
|
448
|
+
mkdir -p "$QH/bin" "$TMP/app"
|
|
449
|
+
cp "$SLOP_SRC" "$QH/bin/slop-detect.mjs"
|
|
450
|
+
echo 'export default function P(){return <div className="bg-gradient-to-r from-blue-500 to-purple-600">hi</div>;}' > "$TMP/app/page.tsx"
|
|
451
|
+
OUT=$( (cd "$TMP" && QUALIA_HOME="$QH" $NODE "$HOOKS_DIR/pre-deploy-gate.js") 2>&1 )
|
|
452
|
+
RC=$?
|
|
453
|
+
if [ "$RC" -eq 2 ] && echo "$OUT" | grep -qi "anti-slop"; then
|
|
454
|
+
echo " ✓ purple-blue gradient → blocked by anti-slop gate (exit 2)"
|
|
455
|
+
PASS=$((PASS + 1))
|
|
456
|
+
else
|
|
457
|
+
echo " ✗ purple-blue gradient → anti-slop block (exit=$RC out=$OUT)"
|
|
458
|
+
FAIL=$((FAIL + 1))
|
|
459
|
+
fi
|
|
460
|
+
rm -rf "$TMP" "$QH"
|
|
461
|
+
|
|
462
|
+
# Clean frontend file → anti-slop passes → exit 0 with "Anti-slop" check shown
|
|
463
|
+
TMP=$(mktemp -d)
|
|
464
|
+
QH=$(mktemp -d)
|
|
465
|
+
mkdir -p "$QH/bin" "$TMP/app"
|
|
466
|
+
cp "$SLOP_SRC" "$QH/bin/slop-detect.mjs"
|
|
467
|
+
echo 'export default function P(){return <div className="bg-stone-50 text-ink">hi</div>;}' > "$TMP/app/page.tsx"
|
|
468
|
+
OUT=$( (cd "$TMP" && QUALIA_HOME="$QH" $NODE "$HOOKS_DIR/pre-deploy-gate.js") 2>&1 )
|
|
469
|
+
RC=$?
|
|
470
|
+
if [ "$RC" -eq 0 ] && echo "$OUT" | grep -q "Anti-slop"; then
|
|
471
|
+
echo " ✓ clean frontend → anti-slop passes (exit 0)"
|
|
472
|
+
PASS=$((PASS + 1))
|
|
473
|
+
else
|
|
474
|
+
echo " ✗ clean frontend → anti-slop passes (exit=$RC out=$OUT)"
|
|
475
|
+
FAIL=$((FAIL + 1))
|
|
476
|
+
fi
|
|
477
|
+
rm -rf "$TMP" "$QH"
|
|
478
|
+
|
|
479
|
+
# QUALIA_SKIP_SLOP=1 by non-OWNER → blocked (OWNER-only escape)
|
|
480
|
+
TMP=$(mktemp -d)
|
|
481
|
+
QH=$(mktemp -d)
|
|
482
|
+
mkdir -p "$QH/bin" "$TMP/app"
|
|
483
|
+
cp "$SLOP_SRC" "$QH/bin/slop-detect.mjs"
|
|
484
|
+
echo '{"role":"EMPLOYEE"}' > "$QH/.qualia-config.json"
|
|
485
|
+
echo 'export default function P(){return <div className="bg-gradient-to-r from-blue-500 to-purple-600">x</div>;}' > "$TMP/app/page.tsx"
|
|
486
|
+
OUT=$( (cd "$TMP" && QUALIA_SKIP_SLOP=1 QUALIA_HOME="$QH" $NODE "$HOOKS_DIR/pre-deploy-gate.js") 2>&1 )
|
|
487
|
+
RC=$?
|
|
488
|
+
if [ "$RC" -eq 2 ] && echo "$OUT" | grep -q "OWNER-only"; then
|
|
489
|
+
echo " ✓ QUALIA_SKIP_SLOP by non-OWNER → blocked (OWNER-only)"
|
|
490
|
+
PASS=$((PASS + 1))
|
|
491
|
+
else
|
|
492
|
+
echo " ✗ QUALIA_SKIP_SLOP non-OWNER block (exit=$RC out=$OUT)"
|
|
493
|
+
FAIL=$((FAIL + 1))
|
|
494
|
+
fi
|
|
495
|
+
rm -rf "$TMP" "$QH"
|
|
496
|
+
|
|
497
|
+
# Scanner absent (older install) → gate skips silently → exit 0
|
|
498
|
+
TMP=$(mktemp -d)
|
|
499
|
+
QH=$(mktemp -d)
|
|
500
|
+
mkdir -p "$TMP/app"
|
|
501
|
+
echo 'export default function P(){return <div className="bg-gradient-to-r from-blue-500 to-purple-600">x</div>;}' > "$TMP/app/page.tsx"
|
|
502
|
+
OUT=$( (cd "$TMP" && QUALIA_HOME="$QH" $NODE "$HOOKS_DIR/pre-deploy-gate.js") 2>&1 )
|
|
503
|
+
RC=$?
|
|
504
|
+
assert_exit "anti-slop gate skips when scanner absent (exit 0)" 0 $RC
|
|
505
|
+
rm -rf "$TMP" "$QH"
|
|
506
|
+
|
|
385
507
|
# --- session-start.js — must exit 0 always ---
|
|
386
508
|
echo ""
|
|
387
509
|
echo "session-start:"
|
|
@@ -420,6 +542,38 @@ else
|
|
|
420
542
|
fi
|
|
421
543
|
rm -rf "$TMP"
|
|
422
544
|
|
|
545
|
+
# --- session-start.js — stale update banner self-clears (regression: v6.9.1) ---
|
|
546
|
+
# A notif file whose advertised `latest` is already installed must NOT render a
|
|
547
|
+
# false "update available" banner; it must be deleted. A genuinely-newer notif
|
|
548
|
+
# must be preserved.
|
|
549
|
+
QH=$(mktemp -d)
|
|
550
|
+
mkdir -p "$QH/bin"
|
|
551
|
+
echo 'process.exit(0)' > "$QH/bin/qualia-ui.js" # dummy UI so render path is reachable
|
|
552
|
+
echo '{"code":"QS-FAWZI-11","version":"6.9.0"}' > "$QH/.qualia-config.json"
|
|
553
|
+
|
|
554
|
+
# Case 1: installed (6.9.0) >= notif.latest (6.9.0) → stale, must self-clear
|
|
555
|
+
echo '{"current":"6.8.1","latest":"6.9.0","detected_at":"t"}' > "$QH/.qualia-update-available.json"
|
|
556
|
+
(cd "$QH" && QUALIA_HOME="$QH" $NODE "$HOOKS_DIR/session-start.js" >/dev/null 2>&1)
|
|
557
|
+
if [ ! -f "$QH/.qualia-update-available.json" ]; then
|
|
558
|
+
echo " ✓ stale update notice self-clears when version caught up"
|
|
559
|
+
PASS=$((PASS + 1))
|
|
560
|
+
else
|
|
561
|
+
echo " ✗ stale update notice was NOT cleared"
|
|
562
|
+
FAIL=$((FAIL + 1))
|
|
563
|
+
fi
|
|
564
|
+
|
|
565
|
+
# Case 2: installed (6.9.0) < notif.latest (9.9.9) → genuine, must persist
|
|
566
|
+
echo '{"current":"6.9.0","latest":"9.9.9","detected_at":"t"}' > "$QH/.qualia-update-available.json"
|
|
567
|
+
(cd "$QH" && QUALIA_HOME="$QH" $NODE "$HOOKS_DIR/session-start.js" >/dev/null 2>&1)
|
|
568
|
+
if [ -f "$QH/.qualia-update-available.json" ]; then
|
|
569
|
+
echo " ✓ genuine update notice is preserved"
|
|
570
|
+
PASS=$((PASS + 1))
|
|
571
|
+
else
|
|
572
|
+
echo " ✗ genuine update notice was wrongly cleared"
|
|
573
|
+
FAIL=$((FAIL + 1))
|
|
574
|
+
fi
|
|
575
|
+
rm -rf "$QH"
|
|
576
|
+
|
|
423
577
|
# pre-compact.js removed in v6.2.0 — state.js journal provides crash safety.
|
|
424
578
|
|
|
425
579
|
# --- auto-update.js ---
|
|
@@ -563,6 +717,85 @@ else
|
|
|
563
717
|
fi
|
|
564
718
|
rm -rf "$TMP"
|
|
565
719
|
|
|
720
|
+
# --- task-write-guard.js (R1 — runtime plan-contract file-scope guard) ---
|
|
721
|
+
echo ""
|
|
722
|
+
echo "task-write-guard:"
|
|
723
|
+
|
|
724
|
+
AS_BIN="$(cd "$(dirname "$0")/../bin" && pwd)/agent-status.js"
|
|
725
|
+
|
|
726
|
+
# setup_twg_project → tmp dir with a phase-1 contract (T1 declares src/a.ts +
|
|
727
|
+
# src/b.ts) and an active RUNNING status for T1. Prints the dir; caller rm -rf.
|
|
728
|
+
setup_twg_project() {
|
|
729
|
+
local tmp
|
|
730
|
+
tmp=$(mktemp -d)
|
|
731
|
+
mkdir -p "$tmp/.planning"
|
|
732
|
+
cat > "$tmp/.planning/phase-1-contract.json" <<'EOF'
|
|
733
|
+
{
|
|
734
|
+
"version": 1,
|
|
735
|
+
"phase": 1,
|
|
736
|
+
"tasks": [
|
|
737
|
+
{ "id": "T1", "wave": 1, "files_modify": ["src/a.ts"], "files_create": ["src/b.ts"] }
|
|
738
|
+
]
|
|
739
|
+
}
|
|
740
|
+
EOF
|
|
741
|
+
echo "$tmp"
|
|
742
|
+
}
|
|
743
|
+
twg() { echo "{\"tool_input\":{\"file_path\":\"$2\"}}" | (cd "$1" && $NODE "$HOOKS_DIR/task-write-guard.js" >/dev/null 2>&1); }
|
|
744
|
+
|
|
745
|
+
# No active build → guard is a no-op even on an undeclared file
|
|
746
|
+
TMP=$(setup_twg_project)
|
|
747
|
+
twg "$TMP" "src/evil.ts"
|
|
748
|
+
assert_exit "no active build → allowed (no-op)" 0 $?
|
|
749
|
+
rm -rf "$TMP"
|
|
750
|
+
|
|
751
|
+
# Active build (T1 RUNNING) + write to a DECLARED file → allowed
|
|
752
|
+
TMP=$(setup_twg_project)
|
|
753
|
+
$NODE "$AS_BIN" write T1 RUNNING --phase 1 --wave 1 --cwd "$TMP" >/dev/null 2>&1
|
|
754
|
+
twg "$TMP" "src/a.ts"
|
|
755
|
+
assert_exit "active build + declared file → allowed" 0 $?
|
|
756
|
+
|
|
757
|
+
# Active build + write to an UNDECLARED file → blocked (exit 2)
|
|
758
|
+
OUT=$(echo '{"tool_input":{"file_path":"src/evil.ts"}}' | (cd "$TMP" && $NODE "$HOOKS_DIR/task-write-guard.js" 2>&1))
|
|
759
|
+
RC=$?
|
|
760
|
+
if [ "$RC" -eq 2 ] && echo "$OUT" | grep -q "task-write-guard" && echo "$OUT" | grep -q "evil.ts"; then
|
|
761
|
+
echo " ✓ active build + undeclared file → blocked with diagnostic"
|
|
762
|
+
PASS=$((PASS + 1))
|
|
763
|
+
else
|
|
764
|
+
echo " ✗ active build + undeclared file → blocked (exit=$RC out=$OUT)"
|
|
765
|
+
FAIL=$((FAIL + 1))
|
|
766
|
+
fi
|
|
767
|
+
|
|
768
|
+
# files_create entries count as declared
|
|
769
|
+
twg "$TMP" "src/b.ts"
|
|
770
|
+
assert_exit "active build + declared files_create → allowed" 0 $?
|
|
771
|
+
|
|
772
|
+
# .planning/ and .agent-status/ paths are always writable during a build
|
|
773
|
+
twg "$TMP" ".planning/phase-1-verification.md"
|
|
774
|
+
assert_exit "active build + .planning path → allowed" 0 $?
|
|
775
|
+
twg "$TMP" ".agent-status/T1.json"
|
|
776
|
+
assert_exit "active build + .agent-status path → allowed" 0 $?
|
|
777
|
+
|
|
778
|
+
# OWNER escape hatch overrides the block
|
|
779
|
+
echo '{"tool_input":{"file_path":"src/evil.ts"}}' | (cd "$TMP" && QUALIA_ALLOW_OUTSIDE_CONTRACT=1 $NODE "$HOOKS_DIR/task-write-guard.js" >/dev/null 2>&1)
|
|
780
|
+
assert_exit "QUALIA_ALLOW_OUTSIDE_CONTRACT=1 → allowed despite undeclared" 0 $?
|
|
781
|
+
|
|
782
|
+
# Absolute path to an undeclared file still blocks
|
|
783
|
+
echo "{\"tool_input\":{\"file_path\":\"$TMP/src/evil.ts\"}}" | (cd "$TMP" && $NODE "$HOOKS_DIR/task-write-guard.js" >/dev/null 2>&1)
|
|
784
|
+
assert_exit "active build + absolute undeclared path → blocked" 2 $?
|
|
785
|
+
|
|
786
|
+
# Once T1 flips to DONE (no RUNNING), the guard goes quiet again
|
|
787
|
+
$NODE "$AS_BIN" write T1 DONE --commit abc --cwd "$TMP" >/dev/null 2>&1
|
|
788
|
+
twg "$TMP" "src/evil.ts"
|
|
789
|
+
assert_exit "build finished (no RUNNING) → allowed" 0 $?
|
|
790
|
+
rm -rf "$TMP"
|
|
791
|
+
|
|
792
|
+
# Active build but no contract present → fail open (allowed)
|
|
793
|
+
TMP=$(mktemp -d)
|
|
794
|
+
$NODE "$AS_BIN" write T1 RUNNING --phase 1 --cwd "$TMP" >/dev/null 2>&1
|
|
795
|
+
twg "$TMP" "src/anything.ts"
|
|
796
|
+
assert_exit "active build + no contract → allowed (fail open)" 0 $?
|
|
797
|
+
rm -rf "$TMP"
|
|
798
|
+
|
|
566
799
|
echo ""
|
|
567
800
|
echo "=== Results: $PASS passed, $FAIL failed ==="
|
|
568
801
|
[ "$FAIL" -eq 0 ] && exit 0 || exit 1
|
|
@@ -124,10 +124,12 @@ else
|
|
|
124
124
|
fi
|
|
125
125
|
|
|
126
126
|
if [ -d "$HOME_DIR/.claude/hooks" ] \
|
|
127
|
-
&& [ "$(find "$HOME_DIR/.claude/hooks" -maxdepth 1 -name '*.js' | wc -l | tr -d ' ')" = "
|
|
127
|
+
&& [ "$(find "$HOME_DIR/.claude/hooks" -maxdepth 1 -name '*.js' | wc -l | tr -d ' ')" = "15" ] \
|
|
128
128
|
&& [ -f "$HOME_DIR/.claude/hooks/fawzi-approval-guard.js" ] \
|
|
129
|
-
&& [ -f "$HOME_DIR/.claude/hooks/pre-compact.js" ]
|
|
130
|
-
|
|
129
|
+
&& [ -f "$HOME_DIR/.claude/hooks/pre-compact.js" ] \
|
|
130
|
+
&& [ -f "$HOME_DIR/.claude/hooks/usage-capture.js" ] \
|
|
131
|
+
&& [ -f "$HOME_DIR/.claude/hooks/task-write-guard.js" ]; then
|
|
132
|
+
pass "packaged install has 15 hooks including task-write-guard (v6.13)"
|
|
131
133
|
else
|
|
132
134
|
fail_case "packaged hook set mismatch"
|
|
133
135
|
fi
|
package/tests/lib.test.sh
CHANGED
|
@@ -507,7 +507,7 @@ TMP=$(mktmp)
|
|
|
507
507
|
mkdir -p "$TMP/home/.claude/bin" "$TMP/home/.claude/hooks" "$TMP/home/.claude/knowledge/daily-log" "$TMP/home/.claude/qualia-design" "$TMP/home/.claude/agents" "$TMP/home/.claude/qualia-templates" "$TMP/project"
|
|
508
508
|
echo '{"installed_by":"Test","role":"OWNER","version":"6.3.0","erp":{"enabled":false}}' > "$TMP/home/.claude/.qualia-config.json"
|
|
509
509
|
touch "$TMP/home/.claude/CLAUDE.md" "$TMP/home/.claude/settings.json"
|
|
510
|
-
for f in runtime-manifest.js command-surface.js host-adapters.js state.js qualia-ui.js statusline.js knowledge.js knowledge-flush.js state-ledger.js plan-contract.js contract-runner.js harness-eval.js trust-score.js agent-runs.js slop-detect.mjs erp-retry.js work-packet.js report-payload.js project-snapshot.js codex-goal.js planning-hygiene.js prune-deprecated.js learning-candidates.js status-snapshot.js security-scan.js auto-report.js; do
|
|
510
|
+
for f in runtime-manifest.js command-surface.js host-adapters.js state.js qualia-ui.js statusline.js knowledge.js knowledge-flush.js state-ledger.js plan-contract.js contract-runner.js agent-status.js analyze-gate.js harness-eval.js trust-score.js agent-runs.js slop-detect.mjs erp-retry.js work-packet.js report-payload.js project-snapshot.js codex-goal.js planning-hygiene.js prune-deprecated.js learning-candidates.js status-snapshot.js security-scan.js auto-report.js; do
|
|
511
511
|
touch "$TMP/home/.claude/bin/$f"
|
|
512
512
|
done
|
|
513
513
|
for h in session-start.js auto-update.js branch-guard.js pre-push.js pre-deploy-gate.js migration-guard.js git-guardrails.js stop-session-log.js fawzi-approval-guard.js vercel-account-guard.js env-empty-guard.js supabase-destructive-guard.js; do
|
|
@@ -622,7 +622,7 @@ TMP=$(mktmp)
|
|
|
622
622
|
mkdir -p "$TMP/.claude/bin" "$TMP/.claude/hooks" "$TMP/.claude/knowledge/daily-log" "$TMP/.claude/qualia-design" "$TMP/.claude/agents" "$TMP/.claude/qualia-templates" "$TMP/project/.planning"
|
|
623
623
|
echo '{"installed_by":"Test","role":"OWNER","erp":{"enabled":false}}' > "$TMP/.claude/.qualia-config.json"
|
|
624
624
|
touch "$TMP/.claude/CLAUDE.md" "$TMP/.claude/settings.json"
|
|
625
|
-
for f in runtime-manifest.js command-surface.js host-adapters.js state.js qualia-ui.js statusline.js knowledge.js knowledge-flush.js state-ledger.js plan-contract.js contract-runner.js harness-eval.js trust-score.js agent-runs.js slop-detect.mjs erp-retry.js work-packet.js report-payload.js project-snapshot.js codex-goal.js planning-hygiene.js prune-deprecated.js learning-candidates.js status-snapshot.js security-scan.js auto-report.js; do
|
|
625
|
+
for f in runtime-manifest.js command-surface.js host-adapters.js state.js qualia-ui.js statusline.js knowledge.js knowledge-flush.js state-ledger.js plan-contract.js contract-runner.js agent-status.js analyze-gate.js harness-eval.js trust-score.js agent-runs.js slop-detect.mjs erp-retry.js work-packet.js report-payload.js project-snapshot.js codex-goal.js planning-hygiene.js prune-deprecated.js learning-candidates.js status-snapshot.js security-scan.js auto-report.js; do
|
|
626
626
|
touch "$TMP/.claude/bin/$f"
|
|
627
627
|
done
|
|
628
628
|
for h in session-start.js auto-update.js branch-guard.js pre-push.js pre-deploy-gate.js migration-guard.js git-guardrails.js stop-session-log.js fawzi-approval-guard.js vercel-account-guard.js env-empty-guard.js supabase-destructive-guard.js; do
|
package/tests/run-all.sh
CHANGED
package/tests/runner.js
CHANGED
|
@@ -2578,14 +2578,15 @@ describe("install.js", () => {
|
|
|
2578
2578
|
}
|
|
2579
2579
|
});
|
|
2580
2580
|
|
|
2581
|
-
it("
|
|
2581
|
+
it("15 hooks installed (v6.13: task-write-guard added — R1 plan-contract file-scope guard)", () => {
|
|
2582
2582
|
const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "qualia-install-"));
|
|
2583
2583
|
try {
|
|
2584
2584
|
runInstall("QS-FAWZI-11", tmpHome);
|
|
2585
2585
|
const hooks = fs.readdirSync(path.join(tmpHome, ".claude", "hooks")).filter(f => f.endsWith(".js"));
|
|
2586
|
-
assert.equal(hooks.length,
|
|
2586
|
+
assert.equal(hooks.length, 15, `expected 15 hooks, got ${hooks.length}: ${hooks.join(", ")}`);
|
|
2587
2587
|
assert.ok(hooks.includes("fawzi-approval-guard.js"), "fawzi-approval-guard.js must be installed");
|
|
2588
2588
|
assert.ok(hooks.includes("pre-compact.js"), "pre-compact.js must be installed (v6.3.2 sidecar-snapshot mechanism)");
|
|
2589
|
+
assert.ok(hooks.includes("task-write-guard.js"), "task-write-guard.js must be installed (v6.13 R1 guard)");
|
|
2589
2590
|
} finally {
|
|
2590
2591
|
fs.rmSync(tmpHome, { recursive: true, force: true });
|
|
2591
2592
|
}
|
package/tests/state.test.sh
CHANGED
|
@@ -104,6 +104,12 @@ Goal: Test goal
|
|
|
104
104
|
## Success Criteria
|
|
105
105
|
- [ ] Test passes
|
|
106
106
|
PLAN
|
|
107
|
+
# v7 kernel: `planned` now requires a machine contract and `verified(pass)`
|
|
108
|
+
# requires passing machine evidence. Emit both so happy-path setups satisfy the
|
|
109
|
+
# preconditions; the few cases that exercise their ABSENCE delete them explicitly.
|
|
110
|
+
make_valid_contract "$dir" "$phase"
|
|
111
|
+
mkdir -p "$dir/.planning/evidence"
|
|
112
|
+
printf '{"ok":true,"checks":[]}\n' > "$dir/.planning/evidence/phase-${phase}-contract-run.json"
|
|
107
113
|
}
|
|
108
114
|
|
|
109
115
|
make_valid_contract() {
|
|
@@ -289,6 +295,9 @@ fi
|
|
|
289
295
|
TMP=$(make_project)
|
|
290
296
|
make_valid_plan "$TMP" 1
|
|
291
297
|
make_valid_contract "$TMP" 1
|
|
298
|
+
# This case asserts the missing-evidence guard, so strip the evidence that
|
|
299
|
+
# make_valid_plan now writes by default.
|
|
300
|
+
rm -f "$TMP/.planning/evidence/phase-1-contract-run.json"
|
|
292
301
|
(cd "$TMP" && $NODE "$STATE_JS" transition --to planned >/dev/null 2>&1)
|
|
293
302
|
(cd "$TMP" && $NODE "$STATE_JS" transition --to built --tasks-done 1 --tasks-total 1 >/dev/null 2>&1)
|
|
294
303
|
echo "result: PASS" > "$TMP/.planning/phase-1-verification.md"
|
|
@@ -735,6 +744,9 @@ else
|
|
|
735
744
|
fail_case "validate well-formed plan" "exit=$EXIT out=$OUT"
|
|
736
745
|
fi
|
|
737
746
|
|
|
747
|
+
# This case asserts the missing-contract path, so strip the contract that
|
|
748
|
+
# make_valid_plan now writes by default.
|
|
749
|
+
rm -f "$TMP/.planning/phase-1-contract.json"
|
|
738
750
|
OUT=$(cd "$TMP" && $NODE "$STATE_JS" validate-plan --phase 1 --require-contract 2>&1)
|
|
739
751
|
EXIT=$?
|
|
740
752
|
if [ "$EXIT" -eq 1 ] \
|
|
@@ -1417,6 +1429,89 @@ else
|
|
|
1417
1429
|
fail_case "id traversal guard" "out=$TRAV"
|
|
1418
1430
|
fi
|
|
1419
1431
|
|
|
1432
|
+
# ─── v7 lifecycle (build → operate) ──────────────────────
|
|
1433
|
+
echo ""
|
|
1434
|
+
echo "lifecycle (build/operate):"
|
|
1435
|
+
|
|
1436
|
+
# Helper: drive a fresh single-phase project to verified(pass).
|
|
1437
|
+
make_verified() {
|
|
1438
|
+
local dir="$1"
|
|
1439
|
+
make_valid_plan "$dir" 1
|
|
1440
|
+
(cd "$dir" && $NODE "$STATE_JS" transition --to planned >/dev/null 2>&1)
|
|
1441
|
+
(cd "$dir" && $NODE "$STATE_JS" transition --to built --tasks-done 1 --tasks-total 1 >/dev/null 2>&1)
|
|
1442
|
+
echo "result: PASS" > "$dir/.planning/phase-1-verification.md"
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
# 1. A new project defaults to lifecycle=build
|
|
1446
|
+
TMP=$(_mktemp_native); TMP_DIRS+=("$TMP")
|
|
1447
|
+
(cd "$TMP" && git init -q 2>/dev/null; $NODE "$STATE_JS" init --project acme --total-phases 1 --phases '[{"name":"Core","goal":"x"}]' --force >/dev/null 2>&1)
|
|
1448
|
+
OUT=$(cd "$TMP" && $NODE "$STATE_JS" check 2>&1)
|
|
1449
|
+
if echo "$OUT" | grep -q '"lifecycle": "build"'; then
|
|
1450
|
+
pass "init defaults to lifecycle=build"
|
|
1451
|
+
else
|
|
1452
|
+
fail_case "init lifecycle default" "out=$OUT"
|
|
1453
|
+
fi
|
|
1454
|
+
|
|
1455
|
+
# 2. launch flips to operate, stamps launched_at + source, routes to /qualia-update
|
|
1456
|
+
OUT=$(cd "$TMP" && $NODE "$STATE_JS" launch --deployed-url https://acme.app --source erp 2>&1)
|
|
1457
|
+
if echo "$OUT" | grep -q '"lifecycle": "operate"' \
|
|
1458
|
+
&& echo "$OUT" | grep -q '"launch_source": "erp"' \
|
|
1459
|
+
&& echo "$OUT" | grep -q '"next_command": "/qualia-update"' \
|
|
1460
|
+
&& echo "$OUT" | grep -q '"launched_at"'; then
|
|
1461
|
+
pass "launch → operate (stamped, routes to /qualia-update)"
|
|
1462
|
+
else
|
|
1463
|
+
fail_case "launch to operate" "out=$OUT"
|
|
1464
|
+
fi
|
|
1465
|
+
|
|
1466
|
+
# 3. launch is idempotent
|
|
1467
|
+
OUT=$(cd "$TMP" && $NODE "$STATE_JS" launch 2>&1)
|
|
1468
|
+
if echo "$OUT" | grep -q '"already_launched": true'; then
|
|
1469
|
+
pass "launch is idempotent (already_launched)"
|
|
1470
|
+
else
|
|
1471
|
+
fail_case "launch idempotent" "out=$OUT"
|
|
1472
|
+
fi
|
|
1473
|
+
|
|
1474
|
+
# 4. operate: verified(pass) on the last phase routes to /qualia-update + bumps updates_completed
|
|
1475
|
+
TMP=$(_mktemp_native); TMP_DIRS+=("$TMP")
|
|
1476
|
+
(cd "$TMP" && git init -q 2>/dev/null; $NODE "$STATE_JS" init --project beta --total-phases 1 --phases '[{"name":"Core","goal":"x"}]' --force >/dev/null 2>&1)
|
|
1477
|
+
(cd "$TMP" && $NODE "$STATE_JS" launch >/dev/null 2>&1)
|
|
1478
|
+
make_verified "$TMP"
|
|
1479
|
+
OUT=$(cd "$TMP" && $NODE "$STATE_JS" transition --to verified --verification pass 2>&1)
|
|
1480
|
+
if echo "$OUT" | grep -q '"next_command": "/qualia-update"'; then
|
|
1481
|
+
pass "operate verified(pass) → /qualia-update"
|
|
1482
|
+
else
|
|
1483
|
+
fail_case "operate verified routing" "out=$OUT"
|
|
1484
|
+
fi
|
|
1485
|
+
OUT=$(cd "$TMP" && $NODE "$STATE_JS" check 2>&1)
|
|
1486
|
+
if echo "$OUT" | grep -q '"updates_completed": 1'; then
|
|
1487
|
+
pass "operate verified(pass) bumps lifetime.updates_completed"
|
|
1488
|
+
else
|
|
1489
|
+
fail_case "updates_completed bump" "out=$OUT"
|
|
1490
|
+
fi
|
|
1491
|
+
|
|
1492
|
+
# 5. BUILD mode still REQUIRES HANDOFF.md (regression: forced handoff intact in build)
|
|
1493
|
+
TMP=$(_mktemp_native); TMP_DIRS+=("$TMP")
|
|
1494
|
+
(cd "$TMP" && git init -q 2>/dev/null; $NODE "$STATE_JS" init --project gamma --total-phases 1 --phases '[{"name":"Core","goal":"x"}]' --force >/dev/null 2>&1)
|
|
1495
|
+
make_verified "$TMP"
|
|
1496
|
+
(cd "$TMP" && $NODE "$STATE_JS" transition --to verified --verification pass >/dev/null 2>&1)
|
|
1497
|
+
(cd "$TMP" && $NODE "$STATE_JS" transition --to polished >/dev/null 2>&1)
|
|
1498
|
+
(cd "$TMP" && $NODE "$STATE_JS" transition --to shipped --deployed-url https://x.app >/dev/null 2>&1)
|
|
1499
|
+
OUT=$(cd "$TMP" && $NODE "$STATE_JS" transition --to handed_off 2>&1)
|
|
1500
|
+
if echo "$OUT" | grep -q '"error": "MISSING_FILE"'; then
|
|
1501
|
+
pass "build mode → handed_off still requires HANDOFF.md"
|
|
1502
|
+
else
|
|
1503
|
+
fail_case "build handoff still gated" "out=$OUT"
|
|
1504
|
+
fi
|
|
1505
|
+
|
|
1506
|
+
# 6. OPERATE mode: handed_off allowed WITHOUT HANDOFF.md (forced handoff removed)
|
|
1507
|
+
(cd "$TMP" && $NODE "$STATE_JS" launch >/dev/null 2>&1) # same project → flip to operate
|
|
1508
|
+
OUT=$(cd "$TMP" && $NODE "$STATE_JS" transition --to handed_off 2>&1)
|
|
1509
|
+
if echo "$OUT" | grep -q '"status": "handed_off"' && echo "$OUT" | grep -q '"ok": true'; then
|
|
1510
|
+
pass "operate mode → handed_off allowed without HANDOFF.md"
|
|
1511
|
+
else
|
|
1512
|
+
fail_case "operate handoff ungated" "out=$OUT"
|
|
1513
|
+
fi
|
|
1514
|
+
|
|
1420
1515
|
# ─── Summary ─────────────────────────────────────────────
|
|
1421
1516
|
echo ""
|
|
1422
1517
|
echo "=== Results: $PASS passed, $FAIL failed ==="
|