qualia-framework 4.5.0 → 5.3.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.
Files changed (66) hide show
  1. package/AGENTS.md +24 -0
  2. package/CLAUDE.md +12 -75
  3. package/README.md +23 -16
  4. package/agents/builder.md +9 -21
  5. package/agents/planner.md +8 -0
  6. package/agents/verifier.md +8 -0
  7. package/agents/visual-evaluator.md +132 -0
  8. package/bin/cli.js +54 -18
  9. package/bin/install.js +369 -29
  10. package/bin/qualia-ui.js +208 -1
  11. package/bin/slop-detect.mjs +5 -0
  12. package/bin/state.js +34 -1
  13. package/docs/install-redesign-builder-prompt.md +290 -0
  14. package/docs/install-redesign-pilot.md +234 -0
  15. package/docs/playwright-loop-builder-prompt.md +185 -0
  16. package/docs/playwright-loop-design-notes.md +108 -0
  17. package/docs/playwright-loop-pilot-results.md +170 -0
  18. package/docs/playwright-loop-tester-prompt.md +213 -0
  19. package/docs/polish-loop-supervised-run.md +111 -0
  20. package/docs/reviews/matt-pocock-skills-analysis.md +300 -0
  21. package/guide.md +9 -5
  22. package/hooks/env-empty-guard.js +74 -0
  23. package/hooks/pre-compact.js +19 -9
  24. package/hooks/pre-deploy-gate.js +8 -2
  25. package/hooks/pre-push.js +26 -12
  26. package/hooks/supabase-destructive-guard.js +62 -0
  27. package/hooks/vercel-account-guard.js +91 -0
  28. package/package.json +2 -1
  29. package/rules/design-brand.md +4 -0
  30. package/rules/design-laws.md +4 -0
  31. package/rules/design-product.md +4 -0
  32. package/rules/design-rubric.md +4 -0
  33. package/rules/grounding.md +4 -0
  34. package/skills/qualia-build/SKILL.md +40 -46
  35. package/skills/qualia-discuss/SKILL.md +51 -68
  36. package/skills/qualia-handoff/SKILL.md +1 -0
  37. package/skills/qualia-hook-gen/SKILL.md +206 -0
  38. package/skills/qualia-issues/SKILL.md +151 -0
  39. package/skills/qualia-map/SKILL.md +78 -35
  40. package/skills/qualia-new/REFERENCE.md +139 -0
  41. package/skills/qualia-new/SKILL.md +45 -121
  42. package/skills/qualia-optimize/REFERENCE.md +265 -0
  43. package/skills/qualia-optimize/SKILL.md +92 -232
  44. package/skills/qualia-plan/SKILL.md +58 -65
  45. package/skills/qualia-polish-loop/REFERENCE.md +265 -0
  46. package/skills/qualia-polish-loop/SKILL.md +201 -0
  47. package/skills/qualia-polish-loop/fixtures/broken.html +117 -0
  48. package/skills/qualia-polish-loop/fixtures/clean.html +196 -0
  49. package/skills/qualia-polish-loop/scripts/loop.mjs +323 -0
  50. package/skills/qualia-polish-loop/scripts/playwright-capture.mjs +206 -0
  51. package/skills/qualia-polish-loop/scripts/score.mjs +176 -0
  52. package/skills/qualia-prd/SKILL.md +199 -0
  53. package/skills/qualia-report/SKILL.md +141 -200
  54. package/skills/qualia-research/SKILL.md +28 -33
  55. package/skills/qualia-road/SKILL.md +103 -0
  56. package/skills/qualia-ship/SKILL.md +1 -0
  57. package/skills/qualia-task/SKILL.md +1 -1
  58. package/skills/qualia-test/SKILL.md +50 -2
  59. package/skills/qualia-triage/SKILL.md +152 -0
  60. package/skills/qualia-verify/SKILL.md +63 -104
  61. package/skills/qualia-zoom/SKILL.md +51 -0
  62. package/skills/zoho-workflow/SKILL.md +1 -1
  63. package/templates/CONTEXT.md +36 -0
  64. package/templates/decisions/ADR-template.md +30 -0
  65. package/tests/bin.test.sh +598 -7
  66. package/tests/state.test.sh +58 -0
package/tests/bin.test.sh CHANGED
@@ -9,6 +9,7 @@ NODE="${NODE:-node}"
9
9
  CLI_JS="$FRAMEWORK_DIR/bin/cli.js"
10
10
  UI_JS="$FRAMEWORK_DIR/bin/qualia-ui.js"
11
11
  INSTALL_JS="$FRAMEWORK_DIR/bin/install.js"
12
+ STATE_JS="$FRAMEWORK_DIR/bin/state.js"
12
13
  PKG_VERSION=$($NODE -e 'console.log(require("'"$FRAMEWORK_DIR"'/package.json").version)')
13
14
 
14
15
  # Track tmp dirs we create so we can clean them up on exit
@@ -476,11 +477,12 @@ else
476
477
  fail_case "CLAUDE.md role substitution"
477
478
  fi
478
479
 
479
- # 31. All 9 hooks installed (block-env-edit removed in v3.2.0;
480
- # git-guardrails + stop-session-log added in v4.2.0)
480
+ # 31. All 12 hooks installed (block-env-edit removed in v3.2.0;
481
+ # git-guardrails + stop-session-log added in v4.2.0;
482
+ # vercel-account-guard + env-empty-guard + supabase-destructive-guard added in v5.0.0)
481
483
  HOOK_COUNT=$(ls "$TMP/.claude/hooks/"*.js 2>/dev/null | wc -l)
482
- if [ "$HOOK_COUNT" -eq 9 ]; then
483
- pass "9 hooks installed in hooks/"
484
+ if [ "$HOOK_COUNT" -eq 12 ]; then
485
+ pass "12 hooks installed in hooks/"
484
486
  else
485
487
  fail_case "hook count" "got $HOOK_COUNT"
486
488
  fi
@@ -496,7 +498,7 @@ else
496
498
  fail_case "settings.json contents"
497
499
  fi
498
500
 
499
- # 33. settings.json contains all 9 hooks wired correctly
501
+ # 33. settings.json contains all 12 hooks wired correctly
500
502
  if grep -q 'branch-guard.js' "$TMP/.claude/settings.json" \
501
503
  && grep -q 'migration-guard.js' "$TMP/.claude/settings.json" \
502
504
  && grep -q 'pre-push.js' "$TMP/.claude/settings.json" \
@@ -505,8 +507,11 @@ if grep -q 'branch-guard.js' "$TMP/.claude/settings.json" \
505
507
  && grep -q 'session-start.js' "$TMP/.claude/settings.json" \
506
508
  && grep -q 'pre-compact.js' "$TMP/.claude/settings.json" \
507
509
  && grep -q 'git-guardrails.js' "$TMP/.claude/settings.json" \
508
- && grep -q 'stop-session-log.js' "$TMP/.claude/settings.json"; then
509
- pass "settings.json has all 9 hooks wired"
510
+ && grep -q 'stop-session-log.js' "$TMP/.claude/settings.json" \
511
+ && grep -q 'vercel-account-guard.js' "$TMP/.claude/settings.json" \
512
+ && grep -q 'env-empty-guard.js' "$TMP/.claude/settings.json" \
513
+ && grep -q 'supabase-destructive-guard.js' "$TMP/.claude/settings.json"; then
514
+ pass "settings.json has all 12 hooks wired"
510
515
  else
511
516
  fail_case "settings.json missing hooks"
512
517
  fi
@@ -992,6 +997,592 @@ else
992
997
  fail_case "help missing flush" "exit=$EXIT"
993
998
  fi
994
999
 
1000
+ echo ""
1001
+ echo "--- v5.0.0 (alignment substrate + new skills) ---"
1002
+
1003
+ # 79. CONTEXT.md template installs to qualia-templates/
1004
+ TMP=$(mktmp)
1005
+ echo "QS-FAWZI-01" | HOME="$TMP" $NODE "$INSTALL_JS" >/dev/null 2>&1
1006
+ if [ -f "$TMP/.claude/qualia-templates/CONTEXT.md" ]; then
1007
+ pass "CONTEXT.md template installs to qualia-templates/"
1008
+ else
1009
+ fail_case "CONTEXT.md template missing"
1010
+ fi
1011
+
1012
+ # 80. decisions/ADR-template.md installs (validates nested template dir copy)
1013
+ if [ -f "$TMP/.claude/qualia-templates/decisions/ADR-template.md" ]; then
1014
+ pass "decisions/ADR-template.md installs (nested template dir)"
1015
+ else
1016
+ fail_case "decisions/ADR-template.md missing"
1017
+ fi
1018
+
1019
+ # 81-84. The 4 new v5 skills install with their SKILL.md
1020
+ for SKILL in qualia-zoom qualia-road qualia-issues qualia-triage; do
1021
+ if [ -f "$TMP/.claude/skills/$SKILL/SKILL.md" ]; then
1022
+ pass "$SKILL skill installs"
1023
+ else
1024
+ fail_case "$SKILL skill missing after install"
1025
+ fi
1026
+ done
1027
+
1028
+ # 85. qualia-discuss SKILL.md mentions "grilling" (validates v5 fold of qualia-grill into discuss)
1029
+ if grep -qi "grilling" "$TMP/.claude/skills/qualia-discuss/SKILL.md"; then
1030
+ pass "qualia-discuss documents grilling pattern (folded from qualia-grill)"
1031
+ else
1032
+ fail_case "qualia-discuss missing grilling reference"
1033
+ fi
1034
+
1035
+ # 86. qualia-discuss SKILL.md references CONTEXT.md (validates v5 substrate wiring)
1036
+ if grep -q "CONTEXT.md" "$TMP/.claude/skills/qualia-discuss/SKILL.md"; then
1037
+ pass "qualia-discuss references CONTEXT.md"
1038
+ else
1039
+ fail_case "qualia-discuss missing CONTEXT.md reference"
1040
+ fi
1041
+
1042
+ # 87. qualia-road has disable-model-invocation: true (pure reference skill, user-only)
1043
+ if grep -q "disable-model-invocation: true" "$TMP/.claude/skills/qualia-road/SKILL.md"; then
1044
+ pass "qualia-road has disable-model-invocation: true"
1045
+ else
1046
+ fail_case "qualia-road missing disable-model-invocation"
1047
+ fi
1048
+
1049
+ # 88. qualia-road has NO dead skill refs (qualia-grill, qualia-deepen, qualia-tdd, qualia-onboard
1050
+ # were folded; if they appear as bare /qualia-* commands the road documentation is broken)
1051
+ if grep -qE '/qualia-(grill|deepen|tdd|onboard)\b' "$TMP/.claude/skills/qualia-road/SKILL.md"; then
1052
+ fail_case "qualia-road still references folded skills as commands"
1053
+ else
1054
+ pass "qualia-road has no dead-skill references"
1055
+ fi
1056
+
1057
+ # 89. CONTEXT.md template has NO /qualia-grill reference (was folded into /qualia-discuss)
1058
+ if grep -q '/qualia-grill\b' "$TMP/.claude/qualia-templates/CONTEXT.md"; then
1059
+ fail_case "CONTEXT.md template still references /qualia-grill"
1060
+ else
1061
+ pass "CONTEXT.md template has no /qualia-grill reference"
1062
+ fi
1063
+
1064
+ # 90. Builder agent has trust-boundary block (defends against CONTEXT.md prompt injection)
1065
+ if grep -q "Trust boundary" "$TMP/.claude/agents/builder.md"; then
1066
+ pass "builder.md has Trust boundary block (prompt-injection defense)"
1067
+ else
1068
+ fail_case "builder.md missing Trust boundary block"
1069
+ fi
1070
+
1071
+ # 91. qualia-issues uses --body-file (not vulnerable heredoc) for gh issue create
1072
+ if grep -q -- "--body-file" "$TMP/.claude/skills/qualia-issues/SKILL.md" \
1073
+ && ! grep -q "gh issue create.*--body \"\$(cat <<" "$TMP/.claude/skills/qualia-issues/SKILL.md"; then
1074
+ pass "qualia-issues uses --body-file (no heredoc shell injection)"
1075
+ else
1076
+ fail_case "qualia-issues still uses heredoc for gh issue body"
1077
+ fi
1078
+
1079
+ # 92-94. v5.0 insights-driven hooks parse as valid Node and install
1080
+ for HOOK in vercel-account-guard env-empty-guard supabase-destructive-guard; do
1081
+ HOOK_FILE="$TMP/.claude/hooks/${HOOK}.js"
1082
+ if [ -f "$HOOK_FILE" ]; then
1083
+ EXIT=0; $NODE -c "$HOOK_FILE" 2>/dev/null || EXIT=$?
1084
+ if [ "$EXIT" -eq 0 ]; then
1085
+ pass "${HOOK}.js installs and parses as valid Node"
1086
+ else
1087
+ fail_case "${HOOK}.js parse error"
1088
+ fi
1089
+ else
1090
+ fail_case "${HOOK}.js missing after install"
1091
+ fi
1092
+ done
1093
+
1094
+ # 95. cmdInit guards against suspicious project names (insights friction: throwaway projects)
1095
+ TMP_PROJ=$(mktemp -d)
1096
+ EXIT=0; OUT=$(cd "$TMP_PROJ" && $NODE "$STATE_JS" init --project "test" --phases '[{"name":"X","goal":"y"}]' 2>&1) || EXIT=$?
1097
+ if [ "$EXIT" -ne 0 ] && echo "$OUT" | grep -q "SUSPICIOUS_NAME"; then
1098
+ pass "state.js init blocks suspicious project name 'test' without --force"
1099
+ else
1100
+ fail_case "state.js init allowed 'test' as project name"
1101
+ fi
1102
+ rm -rf "$TMP_PROJ"
1103
+
1104
+ # 96. cmdInit allows real client name
1105
+ TMP_PROJ=$(mktemp -d)
1106
+ EXIT=0; OUT=$(cd "$TMP_PROJ" && $NODE "$STATE_JS" init --project "RealClient" --phases '[{"name":"X","goal":"y"}]' 2>&1) || EXIT=$?
1107
+ if [ "$EXIT" -eq 0 ] && echo "$OUT" | grep -q '"action": "init"'; then
1108
+ pass "state.js init allows real project name 'RealClient'"
1109
+ else
1110
+ fail_case "state.js init rejected real name" "exit=$EXIT"
1111
+ fi
1112
+ rm -rf "$TMP_PROJ"
1113
+
1114
+ # 97. cmdInit allows suspicious name with --force (escape hatch)
1115
+ TMP_PROJ=$(mktemp -d)
1116
+ EXIT=0; OUT=$(cd "$TMP_PROJ" && $NODE "$STATE_JS" init --project "test" --phases '[{"name":"X","goal":"y"}]' --force 2>&1) || EXIT=$?
1117
+ if [ "$EXIT" -eq 0 ]; then
1118
+ pass "state.js init --force bypasses suspicious-name guard"
1119
+ else
1120
+ fail_case "state.js init --force was blocked" "exit=$EXIT"
1121
+ fi
1122
+ rm -rf "$TMP_PROJ"
1123
+
1124
+ # 98. pre-deploy-gate honors QUALIA_SKIP_LINT escape (insights friction: lint blocks ship)
1125
+ if grep -q "QUALIA_SKIP_LINT" "$TMP/.claude/hooks/pre-deploy-gate.js"; then
1126
+ pass "pre-deploy-gate.js honors QUALIA_SKIP_LINT escape"
1127
+ else
1128
+ fail_case "pre-deploy-gate missing QUALIA_SKIP_LINT escape"
1129
+ fi
1130
+
1131
+ echo ""
1132
+ echo "--- v5.0.0 (visual-polish loop addendum) ---"
1133
+
1134
+ # 99. qualia-polish-loop SKILL.md installs
1135
+ TMP=$(mktmp)
1136
+ echo "QS-FAWZI-01" | HOME="$TMP" $NODE "$INSTALL_JS" >/dev/null 2>&1
1137
+ if [ -f "$TMP/.claude/skills/qualia-polish-loop/SKILL.md" ]; then
1138
+ pass "qualia-polish-loop SKILL.md installs"
1139
+ else
1140
+ fail_case "qualia-polish-loop SKILL.md missing after install"
1141
+ fi
1142
+
1143
+ # 100. qualia-polish-loop REFERENCE.md installs
1144
+ if [ -f "$TMP/.claude/skills/qualia-polish-loop/REFERENCE.md" ]; then
1145
+ pass "qualia-polish-loop REFERENCE.md installs"
1146
+ else
1147
+ fail_case "qualia-polish-loop REFERENCE.md missing after install"
1148
+ fi
1149
+
1150
+ # 101. qualia-polish-loop SKILL.md references the design rubric
1151
+ if grep -q "design-rubric.md" "$TMP/.claude/skills/qualia-polish-loop/SKILL.md"; then
1152
+ pass "qualia-polish-loop references design-rubric.md"
1153
+ else
1154
+ fail_case "qualia-polish-loop missing rubric reference"
1155
+ fi
1156
+
1157
+ # 102. qualia-polish-loop REFERENCE.md has the anchored vision eval prompt
1158
+ if grep -q "DEFAULT TO 3" "$TMP/.claude/skills/qualia-polish-loop/REFERENCE.md"; then
1159
+ pass "qualia-polish-loop REFERENCE.md has anchored rubric prompt"
1160
+ else
1161
+ fail_case "qualia-polish-loop REFERENCE.md missing anchored rubric"
1162
+ fi
1163
+
1164
+ # 103. qualia-polish-loop SKILL.md has regression detection (LOOP_REGRESSION_DETECTED)
1165
+ if grep -q "LOOP_REGRESSION_DETECTED" "$TMP/.claude/skills/qualia-polish-loop/SKILL.md"; then
1166
+ pass "qualia-polish-loop has regression kill-switch"
1167
+ else
1168
+ fail_case "qualia-polish-loop missing regression kill-switch"
1169
+ fi
1170
+
1171
+ # 104. score.mjs exists in the framework skill scripts
1172
+ if [ -f "$FRAMEWORK_DIR/skills/qualia-polish-loop/scripts/score.mjs" ]; then
1173
+ pass "score.mjs exists in skill scripts"
1174
+ else
1175
+ fail_case "score.mjs missing from skill scripts"
1176
+ fi
1177
+
1178
+ # 105. score.mjs computes pass correctly (all dims >= 3)
1179
+ SCORE_OUT=$(echo '{"typography":3,"color":3,"spatial":3,"layout":3,"shadow":3,"motion":3,"microcopy":3,"container":3}' | $NODE "$FRAMEWORK_DIR/skills/qualia-polish-loop/scripts/score.mjs" 2>&1)
1180
+ EXIT=$?
1181
+ if [ "$EXIT" -eq 0 ] && echo "$SCORE_OUT" | grep -q '"pass": true'; then
1182
+ pass "score.mjs pass on all-3 scores"
1183
+ else
1184
+ fail_case "score.mjs pass computation" "exit=$EXIT"
1185
+ fi
1186
+
1187
+ # 106. score.mjs computes fail correctly (one dim < 3)
1188
+ SCORE_OUT=$(echo '{"typography":2,"color":3,"spatial":3,"layout":3,"shadow":3,"motion":3,"microcopy":3,"container":3}' | $NODE "$FRAMEWORK_DIR/skills/qualia-polish-loop/scripts/score.mjs" 2>&1)
1189
+ EXIT=$?
1190
+ if [ "$EXIT" -eq 1 ] && echo "$SCORE_OUT" | grep -q '"pass": false'; then
1191
+ pass "score.mjs fail on dim < 3"
1192
+ else
1193
+ fail_case "score.mjs fail computation" "exit=$EXIT"
1194
+ fi
1195
+
1196
+ # 107. qualia-road references qualia-polish-loop
1197
+ if grep -q "qualia-polish-loop" "$TMP/.claude/skills/qualia-road/SKILL.md"; then
1198
+ pass "qualia-road references qualia-polish-loop"
1199
+ else
1200
+ fail_case "qualia-road missing qualia-polish-loop reference"
1201
+ fi
1202
+
1203
+ # 108. package.json version is 5.x (5.1+ accepted; v5.1 / v5.2 share the v5 line)
1204
+ if grep -qE '"5\.[123]\.' "$FRAMEWORK_DIR/package.json"; then
1205
+ pass "package.json version is 5.x"
1206
+ else
1207
+ fail_case "package.json version not 5.x"
1208
+ fi
1209
+
1210
+ # 109. loop.mjs installs (orchestrator)
1211
+ if [ -f "$TMP/.claude/skills/qualia-polish-loop/scripts/loop.mjs" ]; then
1212
+ pass "qualia-polish-loop scripts/loop.mjs installs"
1213
+ else
1214
+ fail_case "scripts/loop.mjs missing — install.js scripts/ subfolder copy broken"
1215
+ fi
1216
+
1217
+ # 110. playwright-capture.mjs installs (capture helper)
1218
+ if [ -f "$TMP/.claude/skills/qualia-polish-loop/scripts/playwright-capture.mjs" ]; then
1219
+ pass "qualia-polish-loop scripts/playwright-capture.mjs installs"
1220
+ else
1221
+ fail_case "scripts/playwright-capture.mjs missing"
1222
+ fi
1223
+
1224
+ # 111. fixtures/ subfolder installs (self-test pages)
1225
+ if [ -f "$TMP/.claude/skills/qualia-polish-loop/fixtures/clean.html" ] && \
1226
+ [ -f "$TMP/.claude/skills/qualia-polish-loop/fixtures/broken.html" ]; then
1227
+ pass "qualia-polish-loop fixtures/ installs (clean.html + broken.html)"
1228
+ else
1229
+ fail_case "fixtures/ subfolder not copied by install.js"
1230
+ fi
1231
+
1232
+ # 112. visual-evaluator agent installs
1233
+ if [ -f "$TMP/.claude/agents/visual-evaluator.md" ]; then
1234
+ pass "agents/visual-evaluator.md installs"
1235
+ else
1236
+ fail_case "visual-evaluator agent missing after install"
1237
+ fi
1238
+
1239
+ # 113. loop.mjs parses as valid Node ESM
1240
+ EXIT=0; $NODE --check "$FRAMEWORK_DIR/skills/qualia-polish-loop/scripts/loop.mjs" 2>/dev/null || EXIT=$?
1241
+ if [ "$EXIT" -eq 0 ]; then
1242
+ pass "loop.mjs parses as valid Node ESM"
1243
+ else
1244
+ fail_case "loop.mjs parse error"
1245
+ fi
1246
+
1247
+ # 114. playwright-capture.mjs parses as valid Node ESM
1248
+ EXIT=0; $NODE --check "$FRAMEWORK_DIR/skills/qualia-polish-loop/scripts/playwright-capture.mjs" 2>/dev/null || EXIT=$?
1249
+ if [ "$EXIT" -eq 0 ]; then
1250
+ pass "playwright-capture.mjs parses as valid Node ESM"
1251
+ else
1252
+ fail_case "playwright-capture.mjs parse error"
1253
+ fi
1254
+
1255
+ # 115. loop.mjs init creates a valid state file
1256
+ TMP_STATE=$(mktmp)/qpl-state.json
1257
+ mkdir -p "$(dirname "$TMP_STATE")"
1258
+ EXIT=0; $NODE "$FRAMEWORK_DIR/skills/qualia-polish-loop/scripts/loop.mjs" init \
1259
+ --state "$TMP_STATE" --url "http://localhost:3000" --max 4 --budget 50000 >/dev/null 2>&1 || EXIT=$?
1260
+ if [ "$EXIT" -eq 0 ] && [ -f "$TMP_STATE" ] && grep -q '"verdict": "pending"' "$TMP_STATE"; then
1261
+ pass "loop.mjs init creates valid state.json"
1262
+ else
1263
+ fail_case "loop.mjs init failed (exit=$EXIT, state present? $([ -f "$TMP_STATE" ] && echo yes || echo no))"
1264
+ fi
1265
+
1266
+ # 116. loop.mjs record kill-switch fires on 3 consecutive recurrences of same fingerprint
1267
+ TMP_STATE2=$(mktmp)/qpl-kill.json
1268
+ TMP_EVAL=$(mktmp)/qpl-eval.json
1269
+ mkdir -p "$(dirname "$TMP_STATE2")" "$(dirname "$TMP_EVAL")"
1270
+ $NODE "$FRAMEWORK_DIR/skills/qualia-polish-loop/scripts/loop.mjs" init \
1271
+ --state "$TMP_STATE2" --url "http://localhost:3000" --max 8 >/dev/null 2>&1
1272
+ # write 3 identical evals
1273
+ KILL_OK=true
1274
+ for ITER in 1 2 3; do
1275
+ cat > "$TMP_EVAL" <<EOF
1276
+ {
1277
+ "iteration": $ITER,
1278
+ "tokens_used": 100,
1279
+ "viewport_results": [],
1280
+ "aggregate_scores": {"typography":1,"color":3,"spatial":3,"layout":3,"shadow":3,"motion":3,"microcopy":3,"container":3},
1281
+ "top_issues": [{"dim":"typography","severity":"critical","description":"banned font Inter visible","likely_file":"src/styles.css","fix":"replace"}],
1282
+ "pass": false
1283
+ }
1284
+ EOF
1285
+ EXIT=0; $NODE "$FRAMEWORK_DIR/skills/qualia-polish-loop/scripts/loop.mjs" record \
1286
+ --state "$TMP_STATE2" --eval "$TMP_EVAL" >/dev/null 2>&1 || EXIT=$?
1287
+ if [ "$ITER" -lt 3 ] && [ "$EXIT" -ne 1 ]; then KILL_OK=false; fi
1288
+ if [ "$ITER" -eq 3 ] && [ "$EXIT" -ne 3 ]; then KILL_OK=false; fi
1289
+ done
1290
+ if [ "$KILL_OK" = true ] && grep -q '"verdict": "killed_regression"' "$TMP_STATE2"; then
1291
+ pass "loop.mjs kill-switch fires after 3 consecutive recurrences"
1292
+ else
1293
+ fail_case "loop.mjs kill-switch did not fire correctly"
1294
+ fi
1295
+
1296
+ # 117. visual-evaluator agent has trust-boundary block (security-critical)
1297
+ if grep -q "Trust boundary" "$TMP/.claude/agents/visual-evaluator.md"; then
1298
+ pass "visual-evaluator has trust-boundary block (refuses inlined-file injection)"
1299
+ else
1300
+ fail_case "visual-evaluator missing trust-boundary block"
1301
+ fi
1302
+
1303
+ echo ""
1304
+ echo "--- v5.1.0 (multi-target install: Claude / Codex / Both) ---"
1305
+
1306
+ # 118. Target=1 (Claude only) installs to ~/.claude/, not ~/.codex/
1307
+ TMP=$(mktmp)
1308
+ printf 'QS-FAWZI-01\n1\n' | HOME="$TMP" $NODE "$INSTALL_JS" > "$TMP/log.txt" 2>&1
1309
+ EXIT=$?
1310
+ if [ "$EXIT" -eq 0 ] \
1311
+ && [ -f "$TMP/.claude/.qualia-config.json" ] \
1312
+ && [ ! -d "$TMP/.codex" ]; then
1313
+ pass "target=1 → ~/.claude/ populated, ~/.codex/ untouched"
1314
+ else
1315
+ fail_case "target=1 Claude-only" "exit=$EXIT codex_exists=$(test -d "$TMP/.codex" && echo yes || echo no)"
1316
+ fi
1317
+
1318
+ # 119. Target=2 (Codex only) writes ~/.codex/AGENTS.md, skips ~/.claude/
1319
+ TMP=$(mktmp)
1320
+ printf 'QS-FAWZI-01\n2\n' | HOME="$TMP" $NODE "$INSTALL_JS" > "$TMP/log.txt" 2>&1
1321
+ EXIT=$?
1322
+ if [ "$EXIT" -eq 0 ] \
1323
+ && [ -f "$TMP/.codex/AGENTS.md" ] \
1324
+ && [ ! -d "$TMP/.claude" ] \
1325
+ && grep -q "Role: OWNER" "$TMP/.codex/AGENTS.md" \
1326
+ && ! grep -q "{{ROLE}}" "$TMP/.codex/AGENTS.md"; then
1327
+ pass "target=2 → ~/.codex/AGENTS.md with Role: OWNER, ~/.claude/ skipped"
1328
+ else
1329
+ fail_case "target=2 Codex-only" "exit=$EXIT claude_exists=$(test -d "$TMP/.claude" && echo yes || echo no)"
1330
+ fi
1331
+
1332
+ # 120. Target=3 (Both) populates both directories with the right artifacts
1333
+ TMP=$(mktmp)
1334
+ printf 'QS-FAWZI-01\n3\n' | HOME="$TMP" $NODE "$INSTALL_JS" > "$TMP/log.txt" 2>&1
1335
+ EXIT=$?
1336
+ if [ "$EXIT" -eq 0 ] \
1337
+ && [ -f "$TMP/.claude/.qualia-config.json" ] \
1338
+ && [ -f "$TMP/.codex/AGENTS.md" ] \
1339
+ && grep -q "Role: OWNER" "$TMP/.codex/AGENTS.md"; then
1340
+ pass "target=3 → both ~/.claude/ and ~/.codex/AGENTS.md populated"
1341
+ else
1342
+ fail_case "target=3 Both" "exit=$EXIT"
1343
+ fi
1344
+
1345
+ # 121. Backward compat: legacy single-line piped install (no target line)
1346
+ # defaults to Claude only. Same exact invocation as pre-v5.1.
1347
+ TMP=$(mktmp)
1348
+ echo "QS-FAWZI-01" | HOME="$TMP" $NODE "$INSTALL_JS" > "$TMP/log.txt" 2>&1
1349
+ EXIT=$?
1350
+ if [ "$EXIT" -eq 0 ] \
1351
+ && [ -f "$TMP/.claude/.qualia-config.json" ] \
1352
+ && [ ! -d "$TMP/.codex" ]; then
1353
+ pass "legacy single-line piped install → defaults to Claude only"
1354
+ else
1355
+ fail_case "backward compat" "exit=$EXIT"
1356
+ fi
1357
+
1358
+ # 122. Codex AGENTS.md is backed up before overwrite (matches v5.0 CLAUDE.md
1359
+ # / settings.json discipline — never silently destroy user-edited files).
1360
+ TMP=$(mktmp)
1361
+ mkdir -p "$TMP/.codex"
1362
+ echo "OLD USER CONTENT" > "$TMP/.codex/AGENTS.md"
1363
+ printf 'QS-FAWZI-01\n2\n' | HOME="$TMP" $NODE "$INSTALL_JS" > "$TMP/log.txt" 2>&1
1364
+ EXIT=$?
1365
+ BAK_COUNT=$(ls "$TMP/.codex/"AGENTS.md.bak.* 2>/dev/null | wc -l)
1366
+ if [ "$EXIT" -eq 0 ] \
1367
+ && [ "$BAK_COUNT" -ge 1 ] \
1368
+ && grep -q "OLD USER CONTENT" "$TMP/.codex/"AGENTS.md.bak.* \
1369
+ && ! grep -q "OLD USER CONTENT" "$TMP/.codex/AGENTS.md"; then
1370
+ pass "Codex AGENTS.md backup-before-overwrite preserves prior content"
1371
+ else
1372
+ fail_case "Codex backup discipline" "exit=$EXIT bak_count=$BAK_COUNT"
1373
+ fi
1374
+
1375
+ # 123. Codex install with same content does NOT create a redundant .bak
1376
+ # (the v5.0 CLAUDE.md backup discipline only backs up if content differs).
1377
+ TMP=$(mktmp)
1378
+ printf 'QS-FAWZI-01\n2\n' | HOME="$TMP" $NODE "$INSTALL_JS" > "$TMP/log.txt" 2>&1
1379
+ # Re-run with same input — content should be identical, no new backup.
1380
+ printf 'QS-FAWZI-01\n2\n' | HOME="$TMP" $NODE "$INSTALL_JS" > "$TMP/log2.txt" 2>&1
1381
+ BAK_COUNT=$(ls "$TMP/.codex/"AGENTS.md.bak.* 2>/dev/null | wc -l)
1382
+ if [ "$BAK_COUNT" -eq 0 ]; then
1383
+ pass "Codex re-install with identical content → no redundant .bak"
1384
+ else
1385
+ fail_case "Codex backup over-zealous" "bak_count=$BAK_COUNT"
1386
+ fi
1387
+
1388
+ # 124. Non-TTY install log is free of cursor-control / line-clear escape
1389
+ # sequences (\r, \x1b[?25l/h hide-cursor, \x1b[2K clear-line). Spinner +
1390
+ # overwrite primitives must degrade cleanly when output is piped.
1391
+ TMP=$(mktmp)
1392
+ printf 'QS-FAWZI-01\n3\n' | HOME="$TMP" $NODE "$INSTALL_JS" > "$TMP/log.txt" 2>&1
1393
+ # Look for any of: bare CR, hide-cursor (?25), clear-line (\x1b[2K).
1394
+ if [ -s "$TMP/log.txt" ] \
1395
+ && ! grep -q $'\r' "$TMP/log.txt" \
1396
+ && ! grep -q '?25' "$TMP/log.txt" \
1397
+ && ! grep -q '\[2K' "$TMP/log.txt"; then
1398
+ pass "non-TTY install log degrades cleanly (no cursor-control escapes)"
1399
+ else
1400
+ fail_case "non-TTY log has orphan escape sequences"
1401
+ fi
1402
+
1403
+ # 125. Final summary card includes the new "Targets" + "Time" rows
1404
+ TMP=$(mktmp)
1405
+ printf 'QS-FAWZI-01\n3\n' | HOME="$TMP" $NODE "$INSTALL_JS" > "$TMP/log.txt" 2>&1
1406
+ CLEAN=$(strip_ansi < "$TMP/log.txt")
1407
+ if echo "$CLEAN" | grep -q "Targets" \
1408
+ && echo "$CLEAN" | grep -q "Claude Code · Codex" \
1409
+ && echo "$CLEAN" | grep -qE "Time +[0-9]+(\.[0-9]+s|ms|s)"; then
1410
+ pass "summary card shows Targets + Time rows"
1411
+ else
1412
+ fail_case "summary card missing v5.1 rows"
1413
+ fi
1414
+
1415
+ # 126. qualia-ui.js exports the v5.1 live-progress primitives
1416
+ EXPORTED=$($NODE -e 'const u=require("'"$UI_JS"'"); console.log(["step","spinner","progress","box","kv","divider","section","sectionClose"].every(k=>typeof u[k]==="function") ? "ok" : "missing")')
1417
+ if [ "$EXPORTED" = "ok" ]; then
1418
+ pass "qualia-ui.js exports step/spinner/progress/box/kv/divider/section/sectionClose"
1419
+ else
1420
+ fail_case "qualia-ui exports missing" "got=$EXPORTED"
1421
+ fi
1422
+
1423
+ # 127. qualia-ui.js still works as CLI when invoked directly (require-vs-CLI gate)
1424
+ OUT=$($NODE "$UI_JS" ok "test" 2>&1)
1425
+ if echo "$OUT" | grep -q "test"; then
1426
+ pass "qualia-ui.js CLI dispatch still works after exports refactor"
1427
+ else
1428
+ fail_case "qualia-ui CLI broke"
1429
+ fi
1430
+
1431
+ # 128. package.json bumped to 5.x (5.1+ accepted; 5.2 is the v5.2 release)
1432
+ PKG_V=$($NODE -e 'console.log(require("'"$FRAMEWORK_DIR"'/package.json").version)')
1433
+ if echo "$PKG_V" | grep -qE "^5\.[123]\."; then
1434
+ pass "package.json version bumped to 5.x ($PKG_V)"
1435
+ else
1436
+ fail_case "package.json version not 5.x" "got=$PKG_V"
1437
+ fi
1438
+
1439
+ echo ""
1440
+ echo "--- v5.2.0 (polish-loop reliability) ---"
1441
+
1442
+ # 129. loop.mjs init accepts --routes and stores the URL list
1443
+ TMP_S=$(mktmp)/qpl-routes.json
1444
+ mkdir -p "$(dirname "$TMP_S")"
1445
+ EXIT=0; $NODE "$FRAMEWORK_DIR/skills/qualia-polish-loop/scripts/loop.mjs" init \
1446
+ --state "$TMP_S" \
1447
+ --routes "http://x.test/a,http://x.test/b,http://x.test/c" \
1448
+ --max 4 >/dev/null 2>&1 || EXIT=$?
1449
+ if [ "$EXIT" -eq 0 ] \
1450
+ && grep -q '"urls"' "$TMP_S" \
1451
+ && grep -q '"http://x.test/a"' "$TMP_S" \
1452
+ && grep -q '"http://x.test/b"' "$TMP_S" \
1453
+ && grep -q '"http://x.test/c"' "$TMP_S"; then
1454
+ pass "loop.mjs init --routes stores URL list (multi-route)"
1455
+ else
1456
+ fail_case "loop.mjs --routes failed (exit=$EXIT)"
1457
+ fi
1458
+
1459
+ # 130. state.url is the first --routes entry (backward compat with single-route SKILL.md)
1460
+ if grep -q '"url": "http://x.test/a"' "$TMP_S"; then
1461
+ pass "loop.mjs init --routes sets state.url = first URL (backward compat)"
1462
+ else
1463
+ fail_case "loop.mjs --routes did not set state.url to first entry"
1464
+ fi
1465
+
1466
+ # 131. loop.mjs init accepts --reduced-motion and records it in state
1467
+ TMP_S2=$(mktmp)/qpl-rm.json
1468
+ mkdir -p "$(dirname "$TMP_S2")"
1469
+ EXIT=0; $NODE "$FRAMEWORK_DIR/skills/qualia-polish-loop/scripts/loop.mjs" init \
1470
+ --state "$TMP_S2" --url "http://x.test/" --reduced-motion >/dev/null 2>&1 || EXIT=$?
1471
+ if [ "$EXIT" -eq 0 ] && grep -q '"reduced_motion": true' "$TMP_S2"; then
1472
+ pass "loop.mjs init --reduced-motion records state.reduced_motion=true"
1473
+ else
1474
+ fail_case "loop.mjs --reduced-motion not recorded (exit=$EXIT)"
1475
+ fi
1476
+
1477
+ # 132. playwright-capture.mjs accepts --reduced-motion (parses without error)
1478
+ EXIT=0; OUT=$($NODE "$FRAMEWORK_DIR/skills/qualia-polish-loop/scripts/playwright-capture.mjs" --help 2>&1) || EXIT=$?
1479
+ if [ "$EXIT" -eq 0 ] && echo "$OUT" | grep -q -- "--reduced-motion"; then
1480
+ pass "playwright-capture.mjs --help documents --reduced-motion"
1481
+ else
1482
+ fail_case "playwright-capture --reduced-motion not in --help"
1483
+ fi
1484
+
1485
+ # 133. loop.mjs init rejects when neither --url nor --routes given
1486
+ TMP_S3=$(mktmp)/qpl-nourl.json
1487
+ mkdir -p "$(dirname "$TMP_S3")"
1488
+ EXIT=0; $NODE "$FRAMEWORK_DIR/skills/qualia-polish-loop/scripts/loop.mjs" init \
1489
+ --state "$TMP_S3" --max 4 >/dev/null 2>&1 || EXIT=$?
1490
+ if [ "$EXIT" -eq 2 ]; then
1491
+ pass "loop.mjs init rejects missing --url/--routes (exit 2)"
1492
+ else
1493
+ fail_case "loop.mjs init did not reject missing URL (exit=$EXIT)"
1494
+ fi
1495
+
1496
+ # 134. loop.mjs report mentions multi-route when state.urls > 1
1497
+ TMP_S4=$(mktmp)/qpl-rep.json
1498
+ mkdir -p "$(dirname "$TMP_S4")"
1499
+ $NODE "$FRAMEWORK_DIR/skills/qualia-polish-loop/scripts/loop.mjs" init \
1500
+ --state "$TMP_S4" --routes "http://a/,http://b/" >/dev/null 2>&1
1501
+ REP=$($NODE "$FRAMEWORK_DIR/skills/qualia-polish-loop/scripts/loop.mjs" report --state "$TMP_S4" 2>&1)
1502
+ if echo "$REP" | grep -q "URLs (2)"; then
1503
+ pass "loop.mjs report renders multi-route header"
1504
+ else
1505
+ fail_case "loop.mjs report missing multi-route header"
1506
+ fi
1507
+
1508
+ echo ""
1509
+ echo "--- v5.3.0 (Matt Pocock gaps: prd, hook-gen, parallel-interface) ---"
1510
+
1511
+ # Re-install for v5.3 assertions (TMP from #99 may have v5.1 state)
1512
+ TMP=$(mktmp)
1513
+ echo "QS-FAWZI-01" | HOME="$TMP" $NODE "$INSTALL_JS" >/dev/null 2>&1
1514
+
1515
+ # 135. qualia-prd skill installs
1516
+ if [ -f "$TMP/.claude/skills/qualia-prd/SKILL.md" ]; then
1517
+ pass "qualia-prd skill installs"
1518
+ else
1519
+ fail_case "qualia-prd SKILL.md missing after install"
1520
+ fi
1521
+
1522
+ # 136. qualia-prd description mentions PRD synthesis from conversation
1523
+ if grep -q "synthesize\|Synthesize" "$TMP/.claude/skills/qualia-prd/SKILL.md" \
1524
+ && grep -q "/qualia-issues" "$TMP/.claude/skills/qualia-prd/SKILL.md"; then
1525
+ pass "qualia-prd describes synthesis flow + pairs with /qualia-issues"
1526
+ else
1527
+ fail_case "qualia-prd missing synthesis or /qualia-issues link"
1528
+ fi
1529
+
1530
+ # 137. qualia-prd documents fork-based token discipline
1531
+ if grep -qE "[Ff]orked subagent|fork.*subagent" "$TMP/.claude/skills/qualia-prd/SKILL.md" \
1532
+ && grep -q "Token discipline" "$TMP/.claude/skills/qualia-prd/SKILL.md"; then
1533
+ pass "qualia-prd documents fork-based synthesis (token discipline)"
1534
+ else
1535
+ fail_case "qualia-prd missing fork/token-discipline section"
1536
+ fi
1537
+
1538
+ # 138. qualia-hook-gen skill installs
1539
+ if [ -f "$TMP/.claude/skills/qualia-hook-gen/SKILL.md" ]; then
1540
+ pass "qualia-hook-gen skill installs"
1541
+ else
1542
+ fail_case "qualia-hook-gen SKILL.md missing after install"
1543
+ fi
1544
+
1545
+ # 139. qualia-hook-gen documents the three enforcement patterns (block/rewrite/warn)
1546
+ if grep -q "Block" "$TMP/.claude/skills/qualia-hook-gen/SKILL.md" \
1547
+ && grep -q "Rewrite" "$TMP/.claude/skills/qualia-hook-gen/SKILL.md" \
1548
+ && grep -q "Warn" "$TMP/.claude/skills/qualia-hook-gen/SKILL.md"; then
1549
+ pass "qualia-hook-gen documents block/rewrite/warn patterns"
1550
+ else
1551
+ fail_case "qualia-hook-gen missing one of block/rewrite/warn patterns"
1552
+ fi
1553
+
1554
+ # 140. qualia-hook-gen mandates Node hooks (cross-platform), not .sh scripts
1555
+ if grep -q "pure Node\|pure-node\|cross-platform" "$TMP/.claude/skills/qualia-hook-gen/SKILL.md" \
1556
+ && grep -q "No \`.sh\`\|No \\.sh\|exit 0/2\|exit 2 to" "$TMP/.claude/skills/qualia-hook-gen/SKILL.md"; then
1557
+ pass "qualia-hook-gen mandates pure-Node hooks (cross-platform discipline)"
1558
+ else
1559
+ fail_case "qualia-hook-gen missing pure-Node mandate"
1560
+ fi
1561
+
1562
+ # 141. qualia-optimize SKILL.md adds Step 5b (parallel-interface design)
1563
+ if grep -q "Step 5b\|Parallel Interface Design\|Wave 3" "$TMP/.claude/skills/qualia-optimize/SKILL.md" \
1564
+ && grep -q "radically different" "$TMP/.claude/skills/qualia-optimize/SKILL.md"; then
1565
+ pass "qualia-optimize Step 5b parallel-interface fan-out documented"
1566
+ else
1567
+ fail_case "qualia-optimize missing Step 5b parallel-interface stage"
1568
+ fi
1569
+
1570
+ # 142. qualia-optimize REFERENCE.md has the parallel-interface spawn template
1571
+ if grep -q "Parallel interface design prompt" "$TMP/.claude/skills/qualia-optimize/REFERENCE.md" \
1572
+ && grep -q "Variant 1\|variant 1" "$TMP/.claude/skills/qualia-optimize/REFERENCE.md"; then
1573
+ pass "qualia-optimize REFERENCE.md has parallel-interface spawn template"
1574
+ else
1575
+ fail_case "qualia-optimize REFERENCE.md missing parallel-interface template"
1576
+ fi
1577
+
1578
+ # 143. package.json version is 5.x (5.1+ accepted; v5.3 is the v5.3 release)
1579
+ PKG_V=$($NODE -e 'console.log(require("'"$FRAMEWORK_DIR"'/package.json").version)')
1580
+ if echo "$PKG_V" | grep -qE "^5\.[123]\."; then
1581
+ pass "package.json version is 5.x ($PKG_V) — v5.3 accepted"
1582
+ else
1583
+ fail_case "package.json version not 5.x" "got=$PKG_V"
1584
+ fi
1585
+
995
1586
  echo ""
996
1587
  echo "=== Results: $PASS passed, $FAIL failed ==="
997
1588
  [ "$FAIL" -eq 0 ] && exit 0 || exit 1