qualia-framework 7.0.1 → 7.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.
@@ -76,9 +76,11 @@ Verify the {L} concerns of this phase. Every finding needs file:line evidence an
76
76
  Write your findings as a JSON array to .planning/phase-{N}-panel-{L}.json:
77
77
  [{\"file\":\"path\",\"line\":N,\"severity\":\"CRITICAL|HIGH|MEDIUM|LOW\",\"title\":\"one-line claim\"}]
78
78
  Empty array [] if the {L} lens is clean. Also append a human '## {L} lens' section to .planning/phase-{N}-verification.md.
79
- ", subagent_type="qualia-verifier", description="Verify phase {N} — {L} lens")
79
+ ", subagent_type="qualia-verifier", model="sonnet", description="Verify phase {N} — {L} lens")
80
80
  ```
81
81
 
82
+ **Model routing.** Panel verifiers do *finding* work — grep code against acceptance criteria and the shared machine evidence — which is high-recall but low-judgment, so they run on **`sonnet`**. Spend the frontier model where the verdict is actually decided: the skeptic vote (§3c step 2). This is the intelligent-model-routing OpEx lever applied to verification — cheaper on the wide finding pass, frontier on the narrow adjudication.
83
+
82
84
  ### 3b. Browser QA (if phase touched frontend)
83
85
 
84
86
  If plan Files include `.tsx`/`.jsx`/`.css`/`.scss` or `app/`/`pages/`/`components/` paths, spawn browser QA parallel:
@@ -129,6 +131,8 @@ Return exactly one line: REAL — {file:line reason} OR NOT_REAL — {file:l
129
131
  ", subagent_type="qualia-verifier", description="Skeptic {i}/3 — {title}")
130
132
  ```
131
133
 
134
+ Skeptics deliberately **omit `model=`** so they inherit the session's frontier model: their REAL/NOT_REAL judgment is what flips a CRITICAL/HIGH verdict, and that is the one step in the pipeline where model strength most changes the outcome. Route cheap on the finding pass, never on the adjudication.
135
+
132
136
  Tally each finding's votes into `.planning/phase-{N}-panel.json` (`votes.real` / `votes.notReal`).
133
137
 
134
138
  **3. Aggregate** deterministically:
@@ -196,13 +200,16 @@ Write the deterministic eval artifact before changing state:
196
200
  node ${QUALIA_BIN}/harness-eval.js --phase {N} --run --write
197
201
  ```
198
202
 
199
- Run the zero-token anti-slop scan as a deterministic gate (same role as `migration-guard`/`branch-guard` — the scanner exits non-zero on CRITICAL design tells). A CRITICAL finding is a verification FAIL, not a soft note:
203
+ Run the zero-token deterministic gates (same role as `migration-guard`/`branch-guard` — each exits non-zero on a hard fault). A non-zero exit is a verification FAIL, not a soft note:
200
204
 
201
205
  ```bash
202
- node ${QUALIA_BIN}/slop-detect.mjs --severity=critical
206
+ node ${QUALIA_BIN}/slop-detect.mjs --severity=critical # CRITICAL design tells (the slop half)
207
+ node ${QUALIA_BIN}/dep-verify.mjs --severity=critical # hallucinated/slopsquatted imports (the correctness half)
203
208
  ```
204
209
 
205
- The phase is PASS only if ALL of these agree: the panel verdict (§3c `verify-panel.js` exit 0), the harness-eval status, and the anti-slop scan. If any is FAIL/non-zero, mark the phase FAIL. The state machine also refuses PASS when a contract exists but `.planning/evidence/phase-{N}-contract-run.json` is missing/failing, or when the verification report contains `INSUFFICIENT EVIDENCE`.
210
+ `dep-verify` flags any import whose package is BOTH undeclared in `package.json` AND absent from `node_modules` the exact signature of an AI-invented or typosquatted dependency (the #1 named AI-generated-code security failure mode). It is the correctness/security companion to the design-focused `slop-detect`.
211
+
212
+ The phase is PASS only if ALL of these agree: the panel verdict (§3c `verify-panel.js` exit 0), the harness-eval status, the anti-slop scan, and the dependency scan. If any is FAIL/non-zero, mark the phase FAIL. The state machine also refuses PASS when a contract exists but `.planning/evidence/phase-{N}-contract-run.json` is missing/failing, or when the verification report contains `INSUFFICIENT EVIDENCE`.
206
213
 
207
214
  ```bash
208
215
  node ${QUALIA_BIN}/state.js transition --to verified --phase {N} --verification {pass|fail} --evidence .planning/evals/harness-eval-*.json
package/tests/bin.test.sh CHANGED
@@ -495,8 +495,8 @@ fi
495
495
  # usage-capture added in v6.9.1 — UserPromptSubmit telemetry capture;
496
496
  # task-write-guard added in v6.13 — R1 runtime plan-contract file-scope guard)
497
497
  HOOK_COUNT=$(ls "$TMP/.claude/hooks/"*.js 2>/dev/null | wc -l)
498
- if [ "$HOOK_COUNT" -eq 15 ]; then
499
- pass "15 hooks installed in hooks/ (incl. task-write-guard v6.13)"
498
+ if [ "$HOOK_COUNT" -eq 16 ]; then
499
+ pass "16 hooks installed in hooks/ (incl. task-write-guard + secret-guard)"
500
500
  else
501
501
  fail_case "hook count" "got $HOOK_COUNT"
502
502
  fi
@@ -0,0 +1,247 @@
1
+ #!/bin/bash
2
+ # Qualia Framework — bin/dep-verify.mjs behavior tests
3
+ # Verifies the hallucinated/slopsquatted-dependency gate catches what it claims:
4
+ # imports that are neither declared in package.json nor installed in node_modules.
5
+ #
6
+ # Run: bash tests/dep-verify.test.sh
7
+
8
+ PASS=0
9
+ FAIL=0
10
+ DEP_VERIFY="$(cd "$(dirname "$0")/../bin" && pwd)/dep-verify.mjs"
11
+ NODE="${NODE:-node}"
12
+
13
+ TMP_DIRS=()
14
+ cleanup() {
15
+ for d in "${TMP_DIRS[@]}"; do
16
+ [ -d "$d" ] && rm -rf "$d"
17
+ done
18
+ }
19
+ trap cleanup EXIT
20
+
21
+ mktmp() {
22
+ local TMP
23
+ TMP=$(mktemp -d)
24
+ TMP_DIRS+=("$TMP")
25
+ echo "$TMP"
26
+ }
27
+
28
+ pass() { echo " ✓ $1"; PASS=$((PASS + 1)); }
29
+ fail_case() { echo " ✗ $1"; echo " $2"; FAIL=$((FAIL + 1)); }
30
+
31
+ echo "dep-verify.test.sh — bin/dep-verify.mjs behavioral tests"
32
+ echo ""
33
+
34
+ # ── Sanity: file exists and parses ────────────────────────────────────
35
+ if [ ! -f "$DEP_VERIFY" ]; then
36
+ fail_case "dep-verify exists" "$DEP_VERIFY not found"
37
+ echo "=== Results: $PASS passed, $FAIL failed ==="
38
+ exit 1
39
+ fi
40
+ pass "dep-verify.mjs exists at expected path"
41
+
42
+ if $NODE --check "$DEP_VERIFY" 2>/dev/null; then
43
+ pass "dep-verify.mjs syntax is valid"
44
+ else
45
+ fail_case "syntax check" "node --check failed on $DEP_VERIFY"
46
+ fi
47
+
48
+ # ── Declared dependency: should pass (exit 0) ─────────────────────────
49
+ TMP=$(mktmp)
50
+ cat > "$TMP/package.json" <<'EOF'
51
+ { "name": "fixture", "dependencies": { "react": "^19.0.0" } }
52
+ EOF
53
+ cat > "$TMP/declared.tsx" <<'EOF'
54
+ import { useState } from 'react';
55
+ import { thing } from './local-module';
56
+ import fs from 'node:fs';
57
+ export const x = useState;
58
+ EOF
59
+ EXIT_CODE=0
60
+ $NODE "$DEP_VERIFY" "$TMP/declared.tsx" >/dev/null 2>&1 || EXIT_CODE=$?
61
+ if [ "$EXIT_CODE" = "0" ]; then
62
+ pass "exits 0 when imports are declared / relative / builtin"
63
+ else
64
+ fail_case "declared deps" "expected exit 0, got $EXIT_CODE"
65
+ fi
66
+
67
+ # ── Hallucinated package: undeclared AND not installed → exit 1 ────────
68
+ TMP2=$(mktmp)
69
+ cat > "$TMP2/package.json" <<'EOF'
70
+ { "name": "fixture", "dependencies": { "react": "^19.0.0" } }
71
+ EOF
72
+ cat > "$TMP2/bad.tsx" <<'EOF'
73
+ import { toast } from 'react-use-magic-toast-9000';
74
+ export const t = toast;
75
+ EOF
76
+ EXIT_CODE=0
77
+ OUT=$($NODE "$DEP_VERIFY" "$TMP2/bad.tsx" 2>&1) || EXIT_CODE=$?
78
+ if [ "$EXIT_CODE" = "1" ] && echo "$OUT" | grep -q "react-use-magic-toast-9000"; then
79
+ pass "exits 1 and names the hallucinated package"
80
+ else
81
+ fail_case "hallucinated dep" "exit=$EXIT_CODE out=$(echo "$OUT" | head -c 120)"
82
+ fi
83
+
84
+ # ── Installed-but-undeclared (phantom): should NOT flag ───────────────
85
+ TMP3=$(mktmp)
86
+ cat > "$TMP3/package.json" <<'EOF'
87
+ { "name": "fixture", "dependencies": {} }
88
+ EOF
89
+ mkdir -p "$TMP3/node_modules/left-pad"
90
+ cat > "$TMP3/uses-installed.js" <<'EOF'
91
+ const lp = require('left-pad');
92
+ module.exports = lp;
93
+ EOF
94
+ EXIT_CODE=0
95
+ $NODE "$DEP_VERIFY" "$TMP3/uses-installed.js" >/dev/null 2>&1 || EXIT_CODE=$?
96
+ if [ "$EXIT_CODE" = "0" ]; then
97
+ pass "does NOT flag a package that is installed in node_modules (low false positives)"
98
+ else
99
+ fail_case "installed phantom" "expected exit 0, got $EXIT_CODE"
100
+ fi
101
+
102
+ # ── tsconfig path alias should be ignored ─────────────────────────────
103
+ TMP4=$(mktmp)
104
+ cat > "$TMP4/package.json" <<'EOF'
105
+ { "name": "fixture", "dependencies": {} }
106
+ EOF
107
+ cat > "$TMP4/tsconfig.json" <<'EOF'
108
+ { "compilerOptions": { "paths": { "@app/*": ["src/*"] } } }
109
+ EOF
110
+ cat > "$TMP4/alias.tsx" <<'EOF'
111
+ import { Button } from '@app/components/button';
112
+ export const B = Button;
113
+ EOF
114
+ EXIT_CODE=0
115
+ $NODE "$DEP_VERIFY" "$TMP4/alias.tsx" >/dev/null 2>&1 || EXIT_CODE=$?
116
+ if [ "$EXIT_CODE" = "0" ]; then
117
+ pass "ignores tsconfig path aliases (@app/*)"
118
+ else
119
+ fail_case "path alias" "expected exit 0, got $EXIT_CODE"
120
+ fi
121
+
122
+ # ── Commented-out import must not trigger ─────────────────────────────
123
+ TMP5=$(mktmp)
124
+ cat > "$TMP5/package.json" <<'EOF'
125
+ { "name": "fixture", "dependencies": {} }
126
+ EOF
127
+ cat > "$TMP5/commented.ts" <<'EOF'
128
+ // import { x } from 'definitely-not-real-pkg';
129
+ export const y = 1;
130
+ EOF
131
+ EXIT_CODE=0
132
+ $NODE "$DEP_VERIFY" "$TMP5/commented.ts" >/dev/null 2>&1 || EXIT_CODE=$?
133
+ if [ "$EXIT_CODE" = "0" ]; then
134
+ pass "ignores commented-out imports"
135
+ else
136
+ fail_case "commented import" "expected exit 0, got $EXIT_CODE"
137
+ fi
138
+
139
+ # ── String/regex awareness: import sharing a line with a // -bearing
140
+ # string or regex literal must STILL be caught (no silent false negative) ──
141
+ TMP6=$(mktmp)
142
+ cat > "$TMP6/package.json" <<'EOF'
143
+ { "name": "fixture", "dependencies": {} }
144
+ EOF
145
+ cat > "$TMP6/sameline.ts" <<'EOF'
146
+ const re = /url:\/\//; import { x } from 'hallucinated-sameline';
147
+ EOF
148
+ EXIT_CODE=0
149
+ OUT=$($NODE "$DEP_VERIFY" "$TMP6/sameline.ts" 2>&1) || EXIT_CODE=$?
150
+ if [ "$EXIT_CODE" = "1" ] && echo "$OUT" | grep -q "hallucinated-sameline"; then
151
+ pass "catches an import on the same line as a regex literal (no false negative)"
152
+ else
153
+ fail_case "regex-sameline FN" "exit=$EXIT_CODE out=$(echo "$OUT" | head -c 120)"
154
+ fi
155
+
156
+ # ── Import syntax INSIDE a string literal must NOT be flagged (no FP) ──
157
+ TMP7=$(mktmp)
158
+ cat > "$TMP7/package.json" <<'EOF'
159
+ { "name": "fixture", "dependencies": {} }
160
+ EOF
161
+ cat > "$TMP7/strlit.ts" <<'EOF'
162
+ export const example = "import { fake } from 'made-up-package'";
163
+ EOF
164
+ EXIT_CODE=0
165
+ $NODE "$DEP_VERIFY" "$TMP7/strlit.ts" >/dev/null 2>&1 || EXIT_CODE=$?
166
+ if [ "$EXIT_CODE" = "0" ]; then
167
+ pass "does NOT flag import syntax inside a string literal (no false positive)"
168
+ else
169
+ fail_case "string-literal FP" "expected exit 0, got $EXIT_CODE"
170
+ fi
171
+
172
+ # ── Scoped hallucinated package → flagged and named ───────────────────
173
+ TMP8=$(mktmp)
174
+ cat > "$TMP8/package.json" <<'EOF'
175
+ { "name": "fixture", "dependencies": {} }
176
+ EOF
177
+ cat > "$TMP8/scoped.tsx" <<'EOF'
178
+ import { thing } from '@hallucinated-scope/nonexistent-pkg';
179
+ export const t = thing;
180
+ EOF
181
+ EXIT_CODE=0
182
+ OUT=$($NODE "$DEP_VERIFY" "$TMP8/scoped.tsx" 2>&1) || EXIT_CODE=$?
183
+ if [ "$EXIT_CODE" = "1" ] && echo "$OUT" | grep -q "@hallucinated-scope/nonexistent-pkg"; then
184
+ pass "flags a scoped hallucinated package (@scope/name)"
185
+ else
186
+ fail_case "scoped dep" "exit=$EXIT_CODE out=$(echo "$OUT" | head -c 120)"
187
+ fi
188
+
189
+ # ── Protocol specifier (virtual:/https:) must NOT be flagged ──────────
190
+ TMP9=$(mktmp)
191
+ cat > "$TMP9/package.json" <<'EOF'
192
+ { "name": "fixture", "dependencies": {} }
193
+ EOF
194
+ cat > "$TMP9/proto.ts" <<'EOF'
195
+ import config from 'virtual:generated-config';
196
+ export const c = config;
197
+ EOF
198
+ EXIT_CODE=0
199
+ $NODE "$DEP_VERIFY" "$TMP9/proto.ts" >/dev/null 2>&1 || EXIT_CODE=$?
200
+ if [ "$EXIT_CODE" = "0" ]; then
201
+ pass "ignores protocol specifiers (virtual:, https:)"
202
+ else
203
+ fail_case "protocol specifier" "expected exit 0, got $EXIT_CODE"
204
+ fi
205
+
206
+ # ── Directory scan: flags src/, skips node_modules/ (production invocation) ──
207
+ TMPD=$(mktmp)
208
+ cat > "$TMPD/package.json" <<'EOF'
209
+ { "name": "fixture", "dependencies": { "react": "^19.0.0" } }
210
+ EOF
211
+ mkdir -p "$TMPD/src" "$TMPD/node_modules/some-pkg"
212
+ echo "import { useState } from 'react';" > "$TMPD/src/good.ts"
213
+ echo "import { z } from 'ghost-pkg-in-src';" > "$TMPD/src/bad.ts"
214
+ echo "import { q } from 'ghost-inside-node-modules';" > "$TMPD/node_modules/some-pkg/index.ts"
215
+ EXIT_CODE=0
216
+ OUT=$($NODE "$DEP_VERIFY" "$TMPD" 2>&1) || EXIT_CODE=$?
217
+ if [ "$EXIT_CODE" = "1" ] && echo "$OUT" | grep -q "ghost-pkg-in-src" && ! echo "$OUT" | grep -q "ghost-inside-node-modules"; then
218
+ pass "directory scan flags src/ and skips node_modules/"
219
+ else
220
+ fail_case "directory scan" "exit=$EXIT_CODE out=$(echo "$OUT" | head -c 160)"
221
+ fi
222
+
223
+ # ── --json flag produces JSON output (exit 1 + ok:false on findings) ──
224
+ EXIT_CODE=0
225
+ JSON_OUT=$($NODE "$DEP_VERIFY" --json "$TMP2/bad.tsx" 2>/dev/null) || EXIT_CODE=$?
226
+ if [ "$EXIT_CODE" = "1" ] \
227
+ && echo "$JSON_OUT" | head -1 | grep -qE "^[\{\[]" \
228
+ && echo "$JSON_OUT" | grep -q '"findings"' \
229
+ && echo "$JSON_OUT" | grep -q '"ok": false'; then
230
+ pass "--json produces JSON with ok:false and exits 1 on findings"
231
+ else
232
+ fail_case "--json output" "exit=$EXIT_CODE first line: '$(echo "$JSON_OUT" | head -c 80)'"
233
+ fi
234
+
235
+ # ── Missing path → invocation error (exit 2) ──────────────────────────
236
+ EXIT_CODE=0
237
+ $NODE "$DEP_VERIFY" /nonexistent/path/xyz >/dev/null 2>&1 || EXIT_CODE=$?
238
+ if [ "$EXIT_CODE" = "2" ]; then
239
+ pass "exits 2 on a missing path (invocation error)"
240
+ else
241
+ fail_case "missing path" "expected exit 2, got $EXIT_CODE"
242
+ fi
243
+
244
+ echo ""
245
+ echo "=== Results: $PASS passed, $FAIL failed ==="
246
+
247
+ [ "$FAIL" = "0" ]
@@ -494,6 +494,103 @@ else
494
494
  fi
495
495
  rm -rf "$TMP" "$QH"
496
496
 
497
+ # --- pre-deploy-gate: dependency-verification gate (dep-verify.mjs wired into ship) ---
498
+ # Build a tmp QUALIA_HOME carrying bin/dep-verify.mjs so the gate can find it.
499
+ DEP_SRC="$(cd "$(dirname "$0")/../bin" && pwd)/dep-verify.mjs"
500
+
501
+ # Hallucinated import (undeclared + not installed) → blocked with diagnostic
502
+ TMP=$(mktemp -d)
503
+ QH=$(mktemp -d)
504
+ mkdir -p "$QH/bin" "$TMP/app"
505
+ cp "$DEP_SRC" "$QH/bin/dep-verify.mjs"
506
+ echo '{"name":"x","dependencies":{}}' > "$TMP/package.json"
507
+ echo "import { z } from 'totally-made-up-pkg-xyz';" > "$TMP/app/page.tsx"
508
+ OUT=$( (cd "$TMP" && QUALIA_HOME="$QH" $NODE "$HOOKS_DIR/pre-deploy-gate.js") 2>&1 )
509
+ RC=$?
510
+ if [ "$RC" -eq 2 ] && echo "$OUT" | grep -qi "hallucinated"; then
511
+ echo " ✓ hallucinated import → blocked by dependency gate (exit 2)"
512
+ PASS=$((PASS + 1))
513
+ else
514
+ echo " ✗ hallucinated import → dependency block (exit=$RC out=$OUT)"
515
+ FAIL=$((FAIL + 1))
516
+ fi
517
+ rm -rf "$TMP" "$QH"
518
+
519
+ # Clean file (no external imports) → dependency gate passes → exit 0
520
+ TMP=$(mktemp -d)
521
+ QH=$(mktemp -d)
522
+ mkdir -p "$QH/bin" "$TMP/app"
523
+ cp "$DEP_SRC" "$QH/bin/dep-verify.mjs"
524
+ echo '{"name":"x","dependencies":{}}' > "$TMP/package.json"
525
+ echo 'export default function P(){return null;}' > "$TMP/app/page.tsx"
526
+ OUT=$( (cd "$TMP" && QUALIA_HOME="$QH" $NODE "$HOOKS_DIR/pre-deploy-gate.js") 2>&1 )
527
+ RC=$?
528
+ if [ "$RC" -eq 0 ] && echo "$OUT" | grep -q "Dependencies"; then
529
+ echo " ✓ clean imports → dependency gate passes (exit 0)"
530
+ PASS=$((PASS + 1))
531
+ else
532
+ echo " ✗ clean imports → dependency gate passes (exit=$RC out=$OUT)"
533
+ FAIL=$((FAIL + 1))
534
+ fi
535
+ rm -rf "$TMP" "$QH"
536
+
537
+ # QUALIA_SKIP_DEPCHECK=1 by non-OWNER → blocked (OWNER-only escape)
538
+ TMP=$(mktemp -d)
539
+ QH=$(mktemp -d)
540
+ mkdir -p "$QH/bin" "$TMP/app"
541
+ cp "$DEP_SRC" "$QH/bin/dep-verify.mjs"
542
+ echo '{"role":"EMPLOYEE"}' > "$QH/.qualia-config.json"
543
+ echo '{"name":"x","dependencies":{}}' > "$TMP/package.json"
544
+ echo "import { z } from 'totally-made-up-pkg-xyz';" > "$TMP/app/page.tsx"
545
+ OUT=$( (cd "$TMP" && QUALIA_SKIP_DEPCHECK=1 QUALIA_HOME="$QH" $NODE "$HOOKS_DIR/pre-deploy-gate.js") 2>&1 )
546
+ RC=$?
547
+ if [ "$RC" -eq 2 ] && echo "$OUT" | grep -q "OWNER-only"; then
548
+ echo " ✓ QUALIA_SKIP_DEPCHECK by non-OWNER → blocked (OWNER-only)"
549
+ PASS=$((PASS + 1))
550
+ else
551
+ echo " ✗ QUALIA_SKIP_DEPCHECK non-OWNER block (exit=$RC out=$OUT)"
552
+ FAIL=$((FAIL + 1))
553
+ fi
554
+ rm -rf "$TMP" "$QH"
555
+
556
+ # QUALIA_SKIP_DEPCHECK=1 by OWNER → allowed through (the escape valve works)
557
+ TMP=$(mktemp -d)
558
+ QH=$(mktemp -d)
559
+ mkdir -p "$QH/bin" "$TMP/app"
560
+ cp "$DEP_SRC" "$QH/bin/dep-verify.mjs"
561
+ echo '{"role":"OWNER"}' > "$QH/.qualia-config.json"
562
+ echo '{"name":"x","dependencies":{}}' > "$TMP/package.json"
563
+ echo "import { z } from 'totally-made-up-pkg-xyz';" > "$TMP/app/page.tsx"
564
+ OUT=$( (cd "$TMP" && QUALIA_SKIP_DEPCHECK=1 QUALIA_HOME="$QH" $NODE "$HOOKS_DIR/pre-deploy-gate.js") 2>&1 )
565
+ RC=$?
566
+ if [ "$RC" -eq 0 ] && echo "$OUT" | grep -qi "Dependency check skipped"; then
567
+ echo " ✓ QUALIA_SKIP_DEPCHECK by OWNER → allowed (escape valve works)"
568
+ PASS=$((PASS + 1))
569
+ else
570
+ echo " ✗ QUALIA_SKIP_DEPCHECK OWNER allow (exit=$RC out=$OUT)"
571
+ FAIL=$((FAIL + 1))
572
+ fi
573
+ rm -rf "$TMP" "$QH"
574
+
575
+ # Fail-closed: a crashing/non-completing dep-verify must BLOCK, not pass
576
+ TMP=$(mktemp -d)
577
+ QH=$(mktemp -d)
578
+ mkdir -p "$QH/bin" "$TMP/app"
579
+ # A stub scanner that exits 2 (invocation error) — must be treated as FAIL.
580
+ printf '#!/usr/bin/env node\nprocess.exit(2);\n' > "$QH/bin/dep-verify.mjs"
581
+ echo '{"name":"x","dependencies":{}}' > "$TMP/package.json"
582
+ echo 'export default function P(){return null;}' > "$TMP/app/page.tsx"
583
+ OUT=$( (cd "$TMP" && QUALIA_HOME="$QH" $NODE "$HOOKS_DIR/pre-deploy-gate.js") 2>&1 )
584
+ RC=$?
585
+ if [ "$RC" -eq 2 ] && echo "$OUT" | grep -qi "did not complete"; then
586
+ echo " ✓ dep-verify non-completion → blocked, not silently passed (fail-closed)"
587
+ PASS=$((PASS + 1))
588
+ else
589
+ echo " ✗ dep-verify fail-closed (exit=$RC out=$OUT)"
590
+ FAIL=$((FAIL + 1))
591
+ fi
592
+ rm -rf "$TMP" "$QH"
593
+
497
594
  # Scanner absent (older install) → gate skips silently → exit 0
498
595
  TMP=$(mktemp -d)
499
596
  QH=$(mktemp -d)
@@ -124,12 +124,13 @@ 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 ' ')" = "15" ] \
127
+ && [ "$(find "$HOME_DIR/.claude/hooks" -maxdepth 1 -name '*.js' | wc -l | tr -d ' ')" = "16" ] \
128
128
  && [ -f "$HOME_DIR/.claude/hooks/fawzi-approval-guard.js" ] \
129
129
  && [ -f "$HOME_DIR/.claude/hooks/pre-compact.js" ] \
130
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
+ && [ -f "$HOME_DIR/.claude/hooks/task-write-guard.js" ] \
132
+ && [ -f "$HOME_DIR/.claude/hooks/secret-guard.js" ]; then
133
+ pass "packaged install has 16 hooks including task-write-guard + secret-guard"
133
134
  else
134
135
  fail_case "packaged hook set mismatch"
135
136
  fi
@@ -0,0 +1,171 @@
1
+ #!/bin/bash
2
+ # journey-spine.test.sh — lifecycle cosmetic layer (bin/qualia-ui.js spine/onboard/
3
+ # phase-complete/clockout + barTicks + statusline progress bar).
4
+ # Run: bash tests/journey-spine.test.sh
5
+
6
+ PASS=0
7
+ FAIL=0
8
+ BIN_DIR="$(cd "$(dirname "$0")/../bin" && pwd)"
9
+ NODE="${NODE:-node}"
10
+ UI="$BIN_DIR/qualia-ui.js"
11
+ STATE="$BIN_DIR/state.js"
12
+ STATUSLINE="$BIN_DIR/statusline.js"
13
+
14
+ # strip ANSI for column-accurate assertions
15
+ strip() { sed 's/\x1b\[[0-9;]*m//g'; }
16
+
17
+ assert_contains() { if echo "$2" | strip | grep -qF -- "$3"; then echo " ✓ $1"; PASS=$((PASS+1)); else echo " ✗ $1 (missing '$3')"; FAIL=$((FAIL+1)); fi; }
18
+ assert_not_contains() { if echo "$2" | strip | grep -qF -- "$3"; then echo " ✗ $1 (should not contain '$3')"; FAIL=$((FAIL+1)); else echo " ✓ $1"; PASS=$((PASS+1)); fi; }
19
+
20
+ echo "journey-spine.test.sh — lifecycle cosmetic layer"
21
+ echo ""
22
+
23
+ $NODE -c "$UI" 2>/dev/null && { echo " ✓ qualia-ui syntax valid"; PASS=$((PASS+1)); } || { echo " ✗ qualia-ui syntax invalid"; FAIL=$((FAIL+1)); }
24
+
25
+ # ── barTicks: scaling + clamping ──
26
+ TICKS=$($NODE -e 'const u=require(process.argv[1]); console.log(["a"+u.barTicks(4,5),"b"+u.barTicks(0,5),"c"+u.barTicks(5,5),"d"+u.barTicks(9,5),"e"+u.barTicks(2,0)].join("\n"))' "$UI" | strip)
27
+ assert_contains "barTicks 4/5 → 4 filled" "$TICKS" "a▰▰▰▰▱"
28
+ assert_contains "barTicks 0/5 → all empty" "$TICKS" "b▱▱▱▱▱"
29
+ assert_contains "barTicks 5/5 → all filled" "$TICKS" "c▰▰▰▰▰"
30
+ assert_contains "barTicks clamps over-fill to 5" "$TICKS" "d▰▰▰▰▰"
31
+ assert_contains "barTicks total<1 → empty string" "$TICKS" "e"
32
+
33
+ # ── onboard card ──
34
+ ONB=$($NODE "$UI" onboard "Maria" 2>&1)
35
+ assert_contains "onboard greets by name" "$ONB" "welcome aboard, Maria"
36
+ assert_contains "onboard shows milestone flow" "$ONB" "Plan"
37
+ assert_contains "onboard shows the first move" "$ONB" "/qualia"
38
+
39
+ # ── phase-complete card ──
40
+ PC=$($NODE "$UI" phase-complete 3 "Checkout flow" tasks=6 mdone=4 mtotal=5 streak=3 next=/qualia-build nextname="Order confirmation" 2>&1)
41
+ assert_contains "phase-complete shows phase + name" "$PC" "PHASE 3 COMPLETE"
42
+ assert_contains "phase-complete shows task count" "$PC" "6 tasks"
43
+ assert_contains "phase-complete shows milestone bar" "$PC" "4/5 phases"
44
+ assert_contains "phase-complete 'one more' at 4/5" "$PC" "one more to close the milestone"
45
+ assert_contains "phase-complete shows streak" "$PC" "3 phases shipped today"
46
+ assert_contains "phase-complete shows next command" "$PC" "/qualia-build"
47
+ # streak of 1 must NOT render (not a streak)
48
+ PC1=$($NODE "$UI" phase-complete 1 "Cart" streak=1 2>&1)
49
+ assert_not_contains "phase-complete hides streak=1" "$PC1" "shipped today"
50
+ # milestone complete wording at full
51
+ PCFULL=$($NODE "$UI" phase-complete 5 "Polish" mdone=5 mtotal=5 2>&1)
52
+ assert_contains "phase-complete 'ready to close' at 5/5" "$PCFULL" "milestone ready to close"
53
+
54
+ # ── clockout card ──
55
+ CO=$($NODE "$UI" clockout "Maria" date="Thu Jun 25" phases=3 tasks=14 commits=9 mdone=4 mtotal=5 streak=4 erp=ok 2>&1)
56
+ assert_contains "clockout names the employee" "$CO" "CLOCK OUT"
57
+ assert_contains "clockout shows shipped phases" "$CO" "3 phases"
58
+ assert_contains "clockout shows commits" "$CO" "9 commits"
59
+ assert_contains "clockout shows milestone %" "$CO" "80%"
60
+ assert_contains "clockout shows streak" "$CO" "4 days"
61
+ assert_contains "clockout confirms ERP upload" "$CO" "uploaded to ERP"
62
+ CO_Q=$($NODE "$UI" clockout "Maria" erp=queued 2>&1)
63
+ assert_contains "clockout queued state" "$CO_Q" "queued"
64
+
65
+ # ── spine: needs a multi-milestone project fixture ──
66
+ TMP=$(mktemp -d)
67
+ (
68
+ cd "$TMP"
69
+ $NODE "$STATE" init --project "acme-portal" \
70
+ --phases '[{"name":"Cart","goal":"x"},{"name":"Checkout","goal":"y"},{"name":"Receipts","goal":"z"}]' >/dev/null 2>&1
71
+ mkdir -p .planning
72
+ cat > .planning/JOURNEY.md <<'JEOF'
73
+ ---
74
+ project: "acme-portal"
75
+ ---
76
+ ## Milestone 1 · Foundations
77
+ 1. **Auth**
78
+ ## Milestone 2 · Commerce
79
+ 1. **Cart**
80
+ ## Milestone 3 · Growth
81
+ 1. **Referrals**
82
+ ## Milestone 4 · Handoff
83
+ 1. **Docs**
84
+ JEOF
85
+ )
86
+ SPINE=$(cd "$TMP" && $NODE "$UI" spine 2>&1)
87
+ assert_contains "spine renders the Journey label" "$SPINE" "Journey"
88
+ assert_contains "spine renders all 4 milestones" "$SPINE" "M4"
89
+ assert_contains "spine shows current marker ◆" "$SPINE" "◆"
90
+ assert_contains "spine shows 'you are here'" "$SPINE" "you are here"
91
+
92
+ # caret column == current-marker column (exact alignment)
93
+ ALIGN=$(cd "$TMP" && $NODE "$UI" spine 2>&1 | strip | $NODE -e '
94
+ let s=""; process.stdin.on("data",d=>s+=d).on("end",()=>{
95
+ const lines=s.split("\n").filter(l=>l.length);
96
+ const spine=lines.find(l=>l.includes("Journey"));
97
+ const caret=lines.find(l=>l.includes("you are here"));
98
+ if(!spine||!caret){console.log("NOLINES");return;}
99
+ const markerCol=spine.indexOf("◆");
100
+ const caretCol=caret.indexOf("↑");
101
+ console.log(markerCol===caretCol?"ALIGNED":("MISALIGNED "+markerCol+" vs "+caretCol));
102
+ });
103
+ ')
104
+ assert_contains "spine caret aligns under current marker" "$ALIGN" "ALIGNED"
105
+
106
+ # spine self-skips with <2 milestones (single-milestone project)
107
+ TMP2=$(mktemp -d)
108
+ ( cd "$TMP2" && $NODE "$STATE" init --project "solo" --phases '[{"name":"A","goal":"x"}]' >/dev/null 2>&1 )
109
+ SPINE2=$(cd "$TMP2" && $NODE "$UI" spine 2>&1)
110
+ assert_not_contains "spine self-skips single-milestone project" "$SPINE2" "you are here"
111
+
112
+ # spine outside any project → no crash, no output
113
+ SPINE3=$(cd / && $NODE "$UI" spine 2>&1)
114
+ assert_not_contains "spine no-ops outside a project" "$SPINE3" "Journey"
115
+
116
+ # ── statusline: the always-visible progress bar ──
117
+ TMP3=$(mktemp -d); mkdir -p "$TMP3/.planning"
118
+ cat > "$TMP3/.planning/tracking.json" <<'TEOF'
119
+ {"phase":3,"total_phases":5,"status":"building","milestone":2,"milestone_name":"Commerce","tasks_done":2,"tasks_total":6,"blockers":[]}
120
+ TEOF
121
+ SL=$(printf '{"workspace":{"current_dir":"%s"},"cost":{"total_cost_usd":0},"duration_ms":0}' "$TMP3" | $NODE "$STATUSLINE" 2>&1)
122
+ assert_contains "statusline shows P3/5" "$SL" "P3/5"
123
+ assert_contains "statusline shows the tick bar" "$SL" "▰▰▰▱▱"
124
+
125
+ # ── skill-wiring smoke: run the ACTUAL ship/report closing bash ──
126
+ # These mirror the exact variable plumbing in skills/qualia-ship/SKILL.md and
127
+ # skills/qualia-report/SKILL.md (state.js parsing + ${VAR:+…} expansions), so a
128
+ # regression in the wiring — not just the renderer — is caught.
129
+ TMP4=$(mktemp -d)
130
+ ( cd "$TMP4" && $NODE "$STATE" init --project "wired" \
131
+ --phases '[{"name":"Cart","goal":"x"},{"name":"Checkout","goal":"y"},{"name":"Receipts","goal":"z"}]' >/dev/null 2>&1 )
132
+
133
+ # ---- /qualia-ship closing block (verbatim plumbing) ----
134
+ SHIP_OUT=$(cd "$TMP4" && {
135
+ PHASE_NUM=$($NODE "$STATE" check 2>/dev/null | $NODE -pe 'try{JSON.parse(require("fs").readFileSync(0)).phase||""}catch{""}')
136
+ PHASE_NAME=$($NODE "$STATE" check 2>/dev/null | $NODE -pe 'try{JSON.parse(require("fs").readFileSync(0)).phase_name||""}catch{""}')
137
+ TOTAL_PHASES=$($NODE "$STATE" check 2>/dev/null | $NODE -pe 'try{JSON.parse(require("fs").readFileSync(0)).total_phases||""}catch{""}')
138
+ TASKS_DONE=4
139
+ $NODE "$UI" phase-complete "${PHASE_NUM:-?}" "$PHASE_NAME" \
140
+ ${TASKS_DONE:+tasks=$TASKS_DONE} \
141
+ ${PHASE_NUM:+mdone=$PHASE_NUM} ${TOTAL_PHASES:+mtotal=$TOTAL_PHASES} \
142
+ next=/qualia-handoff
143
+ } 2>&1)
144
+ assert_contains "ship wiring → resolves phase number into card" "$SHIP_OUT" "PHASE 1 COMPLETE"
145
+ assert_contains "ship wiring → tasks plumbed through" "$SHIP_OUT" "4 tasks"
146
+ assert_contains "ship wiring → milestone bar plumbed" "$SHIP_OUT" "phases"
147
+ assert_contains "ship wiring → next command set" "$SHIP_OUT" "/qualia-handoff"
148
+ assert_not_contains "ship wiring → no literal \${VAR" "$SHIP_OUT" '${'
149
+
150
+ # ---- /qualia-report closing block (verbatim plumbing) ----
151
+ REPORT_OUT=$(cd "$TMP4" && {
152
+ EMP_NAME=$($NODE -pe 'try{JSON.parse(require("fs").readFileSync(process.env.HOME+"/.claude/.qualia-config.json","utf8")).installed_by||""}catch{""}' 2>/dev/null)
153
+ MDONE=$($NODE "$STATE" check 2>/dev/null | $NODE -pe 'try{JSON.parse(require("fs").readFileSync(0)).phase||""}catch{""}')
154
+ MTOTAL=$($NODE "$STATE" check 2>/dev/null | $NODE -pe 'try{JSON.parse(require("fs").readFileSync(0)).total_phases||""}catch{""}')
155
+ COUNT=9
156
+ ERP_RESULT=ok
157
+ $NODE "$UI" clockout "$EMP_NAME" date="2026-06-25" \
158
+ ${COUNT:+commits=$COUNT} \
159
+ ${MDONE:+mdone=$MDONE} ${MTOTAL:+mtotal=$MTOTAL} \
160
+ ${ERP_RESULT:+erp=$ERP_RESULT}
161
+ } 2>&1)
162
+ assert_contains "report wiring → renders CLOCK OUT" "$REPORT_OUT" "CLOCK OUT"
163
+ assert_contains "report wiring → commit count plumbed" "$REPORT_OUT" "9 commits"
164
+ assert_contains "report wiring → milestone % plumbed" "$REPORT_OUT" "%"
165
+ assert_contains "report wiring → erp result plumbed" "$REPORT_OUT" "uploaded to ERP"
166
+ assert_not_contains "report wiring → no literal \${VAR" "$REPORT_OUT" '${'
167
+
168
+ rm -rf "$TMP" "$TMP2" "$TMP3" "$TMP4"
169
+ echo ""
170
+ echo "=== Results: $PASS passed, $FAIL failed ==="
171
+ [ "$FAIL" -eq 0 ]
package/tests/lib.test.sh CHANGED
@@ -532,10 +532,10 @@ TMP=$(mktmp)
532
532
  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"
533
533
  echo '{"installed_by":"Test","role":"OWNER","version":"6.3.0","erp":{"enabled":false}}' > "$TMP/home/.claude/.qualia-config.json"
534
534
  touch "$TMP/home/.claude/CLAUDE.md" "$TMP/home/.claude/settings.json"
535
- for f in runtime-manifest.js command-surface.js host-adapters.js state.js qualia-ui.js statusline.js knowledge.js knowledge-flush.js recall.js vault-access.js repo-map.js design-tokens.js batch-plan.js state-ledger.js plan-contract.js contract-runner.js agent-status.js analyze-gate.js verify-panel.js wave-plan.js eval-runner.js branch-hygiene.js last-report.js harness-eval.js trust-score.js agent-runs.js slop-detect.mjs erp-retry.js erp-event.js work-packet.js report-payload.js project-snapshot.js project-sync.js codex-goal.js planning-hygiene.js prune-deprecated.js learning-candidates.js status-snapshot.js security-scan.js auto-report.js; do
535
+ for f in runtime-manifest.js command-surface.js host-adapters.js state.js qualia-ui.js statusline.js knowledge.js knowledge-flush.js recall.js vault-access.js repo-map.js design-tokens.js batch-plan.js state-ledger.js plan-contract.js contract-runner.js agent-status.js analyze-gate.js verify-panel.js wave-plan.js eval-runner.js branch-hygiene.js last-report.js harness-eval.js trust-score.js agent-runs.js slop-detect.mjs dep-verify.mjs erp-retry.js erp-event.js work-packet.js report-payload.js project-snapshot.js project-sync.js codex-goal.js planning-hygiene.js prune-deprecated.js learning-candidates.js status-snapshot.js security-scan.js auto-report.js; do
536
536
  touch "$TMP/home/.claude/bin/$f"
537
537
  done
538
- 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
538
+ 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 secret-guard.js; do
539
539
  touch "$TMP/home/.claude/hooks/$h"
540
540
  done
541
541
  touch "$TMP/home/.claude/knowledge/index.md" "$TMP/home/.claude/knowledge/agents.md" "$TMP/home/.claude/agents/visual-evaluator.md" "$TMP/home/.claude/qualia-guide.md" "$TMP/home/.claude/qualia-templates/help.html"
@@ -647,10 +647,10 @@ TMP=$(mktmp)
647
647
  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"
648
648
  echo '{"installed_by":"Test","role":"OWNER","erp":{"enabled":false}}' > "$TMP/.claude/.qualia-config.json"
649
649
  touch "$TMP/.claude/CLAUDE.md" "$TMP/.claude/settings.json"
650
- for f in runtime-manifest.js command-surface.js host-adapters.js state.js qualia-ui.js statusline.js knowledge.js knowledge-flush.js recall.js vault-access.js repo-map.js design-tokens.js batch-plan.js state-ledger.js plan-contract.js contract-runner.js agent-status.js analyze-gate.js verify-panel.js wave-plan.js eval-runner.js branch-hygiene.js last-report.js harness-eval.js trust-score.js agent-runs.js slop-detect.mjs erp-retry.js erp-event.js work-packet.js report-payload.js project-snapshot.js project-sync.js codex-goal.js planning-hygiene.js prune-deprecated.js learning-candidates.js status-snapshot.js security-scan.js auto-report.js; do
650
+ for f in runtime-manifest.js command-surface.js host-adapters.js state.js qualia-ui.js statusline.js knowledge.js knowledge-flush.js recall.js vault-access.js repo-map.js design-tokens.js batch-plan.js state-ledger.js plan-contract.js contract-runner.js agent-status.js analyze-gate.js verify-panel.js wave-plan.js eval-runner.js branch-hygiene.js last-report.js harness-eval.js trust-score.js agent-runs.js slop-detect.mjs dep-verify.mjs erp-retry.js erp-event.js work-packet.js report-payload.js project-snapshot.js project-sync.js codex-goal.js planning-hygiene.js prune-deprecated.js learning-candidates.js status-snapshot.js security-scan.js auto-report.js; do
651
651
  touch "$TMP/.claude/bin/$f"
652
652
  done
653
- 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
653
+ 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 secret-guard.js; do
654
654
  touch "$TMP/.claude/hooks/$h"
655
655
  done
656
656
  touch "$TMP/.claude/knowledge/index.md" "$TMP/.claude/knowledge/agents.md"
package/tests/run-all.sh CHANGED
@@ -18,6 +18,9 @@ SUITES=(
18
18
  "refs"
19
19
  "install-smoke"
20
20
  "slop-detect"
21
+ "dep-verify"
22
+ "runtime-parity"
23
+ "secret-guard"
21
24
  "agent-status"
22
25
  "analyze-gate"
23
26
  "instructions"
@@ -32,6 +35,7 @@ SUITES=(
32
35
  "repo-map"
33
36
  "design-tokens"
34
37
  "batch-plan"
38
+ "journey-spine"
35
39
  )
36
40
 
37
41
  FAILED=()