qualia-framework 5.9.1 → 6.2.7

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 (81) hide show
  1. package/AGENTS.md +2 -1
  2. package/CLAUDE.md +2 -1
  3. package/README.md +45 -29
  4. package/agents/builder.md +1 -5
  5. package/agents/plan-checker.md +1 -1
  6. package/agents/planner.md +2 -6
  7. package/agents/qa-browser.md +3 -3
  8. package/agents/roadmapper.md +2 -2
  9. package/agents/verifier.md +7 -9
  10. package/agents/visual-evaluator.md +1 -3
  11. package/bin/cli.js +370 -205
  12. package/bin/erp-retry.js +11 -3
  13. package/bin/install.js +383 -55
  14. package/bin/knowledge-flush.js +25 -13
  15. package/bin/knowledge.js +11 -1
  16. package/bin/project-snapshot.js +293 -0
  17. package/bin/qualia-ui.js +13 -2
  18. package/bin/report-payload.js +137 -0
  19. package/bin/slop-detect.mjs +81 -9
  20. package/bin/state.js +8 -1
  21. package/bin/statusline.js +14 -2
  22. package/docs/archive/CHANGELOG-pre-v4.md +855 -0
  23. package/docs/changelog-v6.html +864 -0
  24. package/docs/ecosystem-operating-model.md +121 -0
  25. package/docs/erp-contract.md +74 -21
  26. package/docs/onboarding.html +2 -2
  27. package/docs/release.md +44 -0
  28. package/docs/reviews/v6.2.1-revival-audit.md +53 -0
  29. package/docs/reviews/v6.2.2-memory-erp-audit.md +41 -0
  30. package/docs/reviews/v6.2.3-erp-id-guard.md +15 -0
  31. package/guide.md +28 -3
  32. package/hooks/auto-update.js +20 -10
  33. package/hooks/branch-guard.js +10 -2
  34. package/hooks/env-empty-guard.js +15 -5
  35. package/hooks/git-guardrails.js +10 -1
  36. package/hooks/migration-guard.js +4 -1
  37. package/hooks/pre-deploy-gate.js +11 -1
  38. package/hooks/pre-push.js +43 -106
  39. package/hooks/session-start.js +22 -14
  40. package/hooks/stop-session-log.js +11 -3
  41. package/hooks/supabase-destructive-guard.js +11 -1
  42. package/hooks/vercel-account-guard.js +12 -3
  43. package/package.json +4 -3
  44. package/qualia-design/design-reference.md +2 -1
  45. package/qualia-design/frontend.md +4 -4
  46. package/rules/one-opinion.md +59 -0
  47. package/rules/trust-boundary.md +35 -0
  48. package/skills/qualia-feature/SKILL.md +5 -5
  49. package/skills/qualia-flush/SKILL.md +5 -7
  50. package/skills/qualia-hook-gen/SKILL.md +1 -1
  51. package/skills/qualia-learn/SKILL.md +1 -0
  52. package/skills/qualia-map/SKILL.md +2 -1
  53. package/skills/qualia-milestone/SKILL.md +2 -2
  54. package/skills/qualia-new/SKILL.md +6 -6
  55. package/skills/qualia-optimize/SKILL.md +1 -1
  56. package/skills/qualia-plan/SKILL.md +1 -1
  57. package/skills/qualia-polish/REFERENCE.md +8 -6
  58. package/skills/qualia-polish/SKILL.md +11 -9
  59. package/skills/qualia-polish/scripts/loop.mjs +18 -6
  60. package/skills/qualia-postmortem/SKILL.md +1 -1
  61. package/skills/qualia-report/SKILL.md +6 -42
  62. package/skills/qualia-road/SKILL.md +17 -5
  63. package/skills/qualia-verify/SKILL.md +3 -3
  64. package/skills/qualia-vibe/SKILL.md +226 -0
  65. package/skills/qualia-vibe/scripts/extract.mjs +141 -0
  66. package/skills/qualia-vibe/scripts/tokens.mjs +342 -0
  67. package/templates/help.html +10 -3
  68. package/templates/knowledge/agents.md +3 -3
  69. package/templates/knowledge/index.md +1 -1
  70. package/templates/tracking.json +3 -0
  71. package/templates/work-packet.md +46 -0
  72. package/tests/bin.test.sh +423 -25
  73. package/tests/hooks.test.sh +1 -8
  74. package/tests/install-smoke.test.sh +137 -0
  75. package/tests/published-install-smoke.test.sh +126 -0
  76. package/tests/refs.test.sh +43 -1
  77. package/tests/run-all.sh +49 -0
  78. package/tests/runner.js +19 -33
  79. package/tests/slop-detect.test.sh +11 -5
  80. package/tests/state.test.sh +4 -1
  81. package/hooks/pre-compact.js +0 -125
@@ -0,0 +1,137 @@
1
+ #!/usr/bin/env bash
2
+ # install-smoke.test.sh — package-level install smoke test.
3
+ # Builds the npm tarball, extracts it, and runs the packaged installer into an
4
+ # isolated HOME for the Claude + Codex target. This catches files that pass
5
+ # source-tree tests but are missing from the published package.
6
+
7
+ set -u
8
+
9
+ PASS=0
10
+ FAIL=0
11
+
12
+ pass() {
13
+ echo " ✓ $1"
14
+ PASS=$((PASS + 1))
15
+ }
16
+
17
+ fail_case() {
18
+ echo " ✗ $1"
19
+ [ -n "${2:-}" ] && echo " $2"
20
+ FAIL=$((FAIL + 1))
21
+ }
22
+
23
+ ROOT="$(cd "$(dirname "$0")/.." && pwd)"
24
+ NODE="${NODE:-node}"
25
+ TMP="$(mktemp -d 2>/dev/null || mktemp -d -t qualia-install-smoke)"
26
+ HOME_DIR="$TMP/home"
27
+ PACK_DIR="$TMP/pack"
28
+ CACHE_DIR="$TMP/npm-cache"
29
+ LOGS_DIR="$TMP/npm-logs"
30
+ USERCONFIG="$TMP/npmrc"
31
+ mkdir -p "$HOME_DIR" "$PACK_DIR" "$CACHE_DIR" "$LOGS_DIR"
32
+ : >"$USERCONFIG"
33
+
34
+ cleanup() {
35
+ rm -rf "$TMP"
36
+ }
37
+ trap cleanup EXIT
38
+
39
+ echo "install-smoke.test.sh — packaged Claude + Codex install"
40
+ echo ""
41
+
42
+ PACK_OUT="$TMP/npm-pack.out"
43
+ PACK_ERR="$TMP/npm-pack.err"
44
+ (cd "$ROOT" && env \
45
+ NPM_CONFIG_CACHE="$CACHE_DIR" \
46
+ NPM_CONFIG_LOGS_DIR="$LOGS_DIR" \
47
+ NPM_CONFIG_USERCONFIG="$USERCONFIG" \
48
+ npm pack --silent --pack-destination "$PACK_DIR" >"$PACK_OUT" 2>"$PACK_ERR")
49
+ PACK_EXIT=$?
50
+ TARBALL=$(tail -1 "$PACK_OUT" 2>/dev/null | tr -d '[:space:]')
51
+ TARBALL_PATH="$PACK_DIR/$TARBALL"
52
+
53
+ if [ -f "$TARBALL_PATH" ]; then
54
+ pass "npm pack produced $TARBALL"
55
+ else
56
+ fail_case "npm pack tarball missing" "exit=$PACK_EXIT expected=$TARBALL_PATH stderr=$(cat "$PACK_ERR" 2>/dev/null)"
57
+ echo ""
58
+ echo "Results: $PASS passed, $FAIL failed"
59
+ exit 1
60
+ fi
61
+
62
+ tar -xzf "$TARBALL_PATH" -C "$TMP" 2>"$TMP/tar.err"
63
+ if [ -f "$TMP/package/bin/install.js" ] \
64
+ && [ -f "$TMP/package/bin/report-payload.js" ] \
65
+ && [ -f "$TMP/package/bin/project-snapshot.js" ] \
66
+ && [ -f "$TMP/package/AGENTS.md" ] \
67
+ && [ -f "$TMP/package/CLAUDE.md" ]; then
68
+ pass "tarball contains installer + Claude/Codex instruction roots"
69
+ else
70
+ fail_case "tarball missing install surfaces" "$(cat "$TMP/tar.err" 2>/dev/null)"
71
+ fi
72
+
73
+ printf 'QS-FAWZI-01\n3\n' | HOME="$HOME_DIR" "$NODE" "$TMP/package/bin/install.js" >"$TMP/install.log" 2>&1
74
+ EXIT=$?
75
+ if [ "$EXIT" -eq 0 ]; then
76
+ pass "packaged installer exits 0 for target=Both"
77
+ else
78
+ fail_case "packaged installer failed" "exit=$EXIT log=$(tail -20 "$TMP/install.log" 2>/dev/null)"
79
+ fi
80
+
81
+ if [ -f "$HOME_DIR/.claude/CLAUDE.md" ] \
82
+ && grep -q "Role: OWNER" "$HOME_DIR/.claude/CLAUDE.md" \
83
+ && ! grep -q "{{ROLE}}" "$HOME_DIR/.claude/CLAUDE.md"; then
84
+ pass "Claude CLAUDE.md installed with OWNER role"
85
+ else
86
+ fail_case "Claude CLAUDE.md role substitution failed"
87
+ fi
88
+
89
+ if [ -f "$HOME_DIR/.codex/AGENTS.md" ] \
90
+ && [ -f "$HOME_DIR/.codex/hooks.json" ] \
91
+ && [ -f "$HOME_DIR/.codex/config.toml" ] \
92
+ && [ -f "$HOME_DIR/.codex/bin/statusline.js" ] \
93
+ && [ -f "$HOME_DIR/.codex/bin/project-snapshot.js" ] \
94
+ && [ -f "$HOME_DIR/.codex/agents/planner.toml" ] \
95
+ && [ -f "$HOME_DIR/.codex/skills/qualia-new/SKILL.md" ] \
96
+ && [ -f "$HOME_DIR/.codex/qualia-references/questioning.md" ] \
97
+ && grep -q "Role: OWNER" "$HOME_DIR/.codex/AGENTS.md" \
98
+ && ! grep -q "{{ROLE}}" "$HOME_DIR/.codex/AGENTS.md" \
99
+ && grep -q "Qualia deploy gate" "$HOME_DIR/.codex/hooks.json" \
100
+ && ! grep -R "\.claude/bin" "$HOME_DIR/.codex/skills" >/dev/null 2>&1; then
101
+ pass "Codex runtime installed with OWNER role"
102
+ else
103
+ fail_case "Codex runtime install failed"
104
+ fi
105
+
106
+ if [ -d "$HOME_DIR/.claude/hooks" ] \
107
+ && [ "$(find "$HOME_DIR/.claude/hooks" -maxdepth 1 -name '*.js' | wc -l | tr -d ' ')" = "11" ] \
108
+ && [ ! -f "$HOME_DIR/.claude/hooks/pre-compact.js" ]; then
109
+ pass "packaged install has 11 hooks and no pre-compact"
110
+ else
111
+ fail_case "packaged hook set mismatch"
112
+ fi
113
+
114
+ if [ -f "$HOME_DIR/.claude/bin/report-payload.js" ] \
115
+ && [ -f "$HOME_DIR/.claude/bin/project-snapshot.js" ] \
116
+ && grep -q "report-payload.js" "$HOME_DIR/.claude/skills/qualia-report/SKILL.md"; then
117
+ pass "packaged install includes ERP report/snapshot helpers"
118
+ else
119
+ fail_case "packaged install missing ERP report/snapshot helpers"
120
+ fi
121
+
122
+ PKG_VERSION=$("$NODE" -e "console.log(require(process.argv[1]).version)" "$TMP/package/package.json")
123
+ CONFIG_VERSION=$("$NODE" -e "console.log(require(process.argv[1]).version)" "$HOME_DIR/.claude/.qualia-config.json" 2>/dev/null || echo "")
124
+ if [ "$PKG_VERSION" = "$CONFIG_VERSION" ]; then
125
+ pass "installed config version matches package ($PKG_VERSION)"
126
+ else
127
+ fail_case "installed config version mismatch" "package=$PKG_VERSION config=$CONFIG_VERSION"
128
+ fi
129
+
130
+ echo ""
131
+ echo "Results: $PASS passed, $FAIL failed"
132
+
133
+ if [ "$FAIL" -gt 0 ]; then
134
+ exit 1
135
+ fi
136
+
137
+ exit 0
@@ -0,0 +1,126 @@
1
+ #!/usr/bin/env bash
2
+ # published-install-smoke.test.sh — release-only smoke for the public npm path.
3
+ # This intentionally is not part of npm test: it should fail until the current
4
+ # package.json version has been published to the npm latest dist-tag.
5
+
6
+ set -u
7
+
8
+ PASS=0
9
+ FAIL=0
10
+
11
+ pass() {
12
+ echo " ✓ $1"
13
+ PASS=$((PASS + 1))
14
+ }
15
+
16
+ fail_case() {
17
+ echo " ✗ $1"
18
+ [ -n "${2:-}" ] && echo " $2"
19
+ FAIL=$((FAIL + 1))
20
+ }
21
+
22
+ ROOT="$(cd "$(dirname "$0")/.." && pwd)"
23
+ NODE="${NODE:-node}"
24
+ TMP="$(mktemp -d 2>/dev/null || mktemp -d -t qualia-published-install-smoke)"
25
+ HOME_DIR="$TMP/home"
26
+ CACHE_DIR="$TMP/npm-cache"
27
+ LOGS_DIR="$TMP/npm-logs"
28
+ USERCONFIG="$TMP/npmrc"
29
+ mkdir -p "$HOME_DIR" "$CACHE_DIR" "$LOGS_DIR"
30
+ : >"$USERCONFIG"
31
+ REGISTRY_TIMEOUT_SECONDS="${REGISTRY_TIMEOUT_SECONDS:-45}"
32
+ INSTALL_TIMEOUT_SECONDS="${INSTALL_TIMEOUT_SECONDS:-120}"
33
+
34
+ cleanup() {
35
+ rm -rf "$TMP"
36
+ }
37
+ trap cleanup EXIT
38
+
39
+ echo "published-install-smoke.test.sh — npm @latest Claude + Codex install"
40
+ echo ""
41
+
42
+ LOCAL_VERSION=$("$NODE" -e "console.log(require(process.argv[1]).version)" "$ROOT/package.json")
43
+ timeout "$REGISTRY_TIMEOUT_SECONDS" env \
44
+ NPM_CONFIG_CACHE="$CACHE_DIR" \
45
+ NPM_CONFIG_LOGS_DIR="$LOGS_DIR" \
46
+ NPM_CONFIG_USERCONFIG="$USERCONFIG" \
47
+ npm view qualia-framework version >"$TMP/npm-view.out" 2>"$TMP/npm-view.err"
48
+ VIEW_EXIT=$?
49
+ LATEST_VERSION=$(tail -1 "$TMP/npm-view.out" 2>/dev/null | tr -d '[:space:]')
50
+
51
+ if [ "$VIEW_EXIT" -eq 124 ]; then
52
+ fail_case "npm view timed out" "after=${REGISTRY_TIMEOUT_SECONDS}s"
53
+ echo ""
54
+ echo "Results: $PASS passed, $FAIL failed"
55
+ exit 1
56
+ elif [ "$VIEW_EXIT" -ne 0 ]; then
57
+ fail_case "npm view failed" "exit=$VIEW_EXIT err=$(cat "$TMP/npm-view.err" 2>/dev/null)"
58
+ echo ""
59
+ echo "Results: $PASS passed, $FAIL failed"
60
+ exit 1
61
+ fi
62
+
63
+ if [ "$LATEST_VERSION" = "$LOCAL_VERSION" ]; then
64
+ pass "npm latest matches package.json ($LOCAL_VERSION)"
65
+ else
66
+ fail_case "npm latest does not match package.json" "package.json=$LOCAL_VERSION npm_latest=${LATEST_VERSION:-unavailable} npm_err=$(cat "$TMP/npm-view.err" 2>/dev/null)"
67
+ echo ""
68
+ echo "Results: $PASS passed, $FAIL failed"
69
+ exit 1
70
+ fi
71
+
72
+ printf 'QS-FAWZI-01\n3\n' >"$TMP/install.input"
73
+ timeout "$INSTALL_TIMEOUT_SECONDS" env \
74
+ HOME="$HOME_DIR" \
75
+ NPM_CONFIG_CACHE="$CACHE_DIR" \
76
+ NPM_CONFIG_LOGS_DIR="$LOGS_DIR" \
77
+ NPM_CONFIG_USERCONFIG="$USERCONFIG" \
78
+ npx --yes qualia-framework@latest install <"$TMP/install.input" >"$TMP/install.log" 2>&1
79
+ EXIT=$?
80
+ if [ "$EXIT" -eq 0 ]; then
81
+ pass "npx qualia-framework@latest install exits 0 for target=Both"
82
+ elif [ "$EXIT" -eq 124 ]; then
83
+ fail_case "npx qualia-framework@latest install timed out" "after=${INSTALL_TIMEOUT_SECONDS}s log=$(tail -30 "$TMP/install.log" 2>/dev/null)"
84
+ else
85
+ fail_case "npx qualia-framework@latest install failed" "exit=$EXIT log=$(tail -30 "$TMP/install.log" 2>/dev/null)"
86
+ fi
87
+
88
+ if [ -f "$HOME_DIR/.claude/CLAUDE.md" ] \
89
+ && grep -q "Role: OWNER" "$HOME_DIR/.claude/CLAUDE.md" \
90
+ && ! grep -q "{{ROLE}}" "$HOME_DIR/.claude/CLAUDE.md"; then
91
+ pass "public install writes Claude CLAUDE.md with OWNER role"
92
+ else
93
+ fail_case "public install Claude CLAUDE.md role substitution failed"
94
+ fi
95
+
96
+ if [ -f "$HOME_DIR/.codex/AGENTS.md" ] \
97
+ && grep -q "Role: OWNER" "$HOME_DIR/.codex/AGENTS.md" \
98
+ && ! grep -q "{{ROLE}}" "$HOME_DIR/.codex/AGENTS.md"; then
99
+ pass "public install writes Codex AGENTS.md with OWNER role"
100
+ else
101
+ fail_case "public install Codex AGENTS.md role substitution failed"
102
+ fi
103
+
104
+ if [ -d "$HOME_DIR/.claude/hooks" ] \
105
+ && [ "$(find "$HOME_DIR/.claude/hooks" -maxdepth 1 -name '*.js' | wc -l | tr -d ' ')" = "11" ] \
106
+ && [ ! -f "$HOME_DIR/.claude/hooks/pre-compact.js" ]; then
107
+ pass "public install has 11 hooks and no pre-compact"
108
+ else
109
+ fail_case "public install hook set mismatch"
110
+ fi
111
+
112
+ CONFIG_VERSION=$("$NODE" -e "console.log(require(process.argv[1]).version)" "$HOME_DIR/.claude/.qualia-config.json" 2>/dev/null || echo "")
113
+ if [ "$LOCAL_VERSION" = "$CONFIG_VERSION" ]; then
114
+ pass "public install config version matches package ($LOCAL_VERSION)"
115
+ else
116
+ fail_case "public install config version mismatch" "package=$LOCAL_VERSION config=$CONFIG_VERSION"
117
+ fi
118
+
119
+ echo ""
120
+ echo "Results: $PASS passed, $FAIL failed"
121
+
122
+ if [ "$FAIL" -gt 0 ]; then
123
+ exit 1
124
+ fi
125
+
126
+ exit 0
@@ -21,6 +21,7 @@
21
21
 
22
22
  FRAMEWORK_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
23
23
  SKILLS_DIR="$FRAMEWORK_ROOT/skills"
24
+ NODE="${NODE:-node}"
24
25
 
25
26
  PASS=0
26
27
  FAIL=0
@@ -42,7 +43,7 @@ EXCLUDE_REGEX='/docs/reviews/|/docs/research/|/docs/playwright-loop-pilot-result
42
43
  # When a `/qualia-foo` ref appears AFTER one of these context tokens on the same line,
43
44
  # it's a migration-explainer ("Replaces /qualia-quick" / "deprecated in v5.7"), not
44
45
  # a live command reference. Treat it as exempt.
45
- MIGRATION_CONTEXT_REGEX='Replaces|Removed|removed in|consolidated|deprecated|renamed|former|previously|was the|now the|now\s+`?/qualia|absorbed|superseded|legacy|migrated|after\s+`?/qualia.*-(quick|task|prd|design|polish-loop)'
46
+ MIGRATION_CONTEXT_REGEX='Replaces|Removed|removed in|consolidated|deprecated|renamed|former|previously|was the|now the|now\s+`?/qualia|absorbed|superseded|legacy|migrated|dead|after\s+`?/qualia.*-(quick|task|prd|design|polish-loop)'
46
47
 
47
48
  ACTIVE_DIRS=(
48
49
  "$FRAMEWORK_ROOT/rules"
@@ -130,6 +131,47 @@ for ref in $(printf '%s\n' "${!SEEN_REFS[@]}" | sort); do
130
131
  fail_case "$ref" "no skills/$name/SKILL.md — referenced by: $locations"
131
132
  done
132
133
 
134
+ PACKAGE_VERSION="$("$NODE" -e 'console.log(require(process.argv[1]).version)' "$FRAMEWORK_ROOT/package.json" 2>/dev/null || echo "")"
135
+ if [ -n "$PACKAGE_VERSION" ] && grep -q "# Qualia Framework v$PACKAGE_VERSION" "$FRAMEWORK_ROOT/README.md"; then
136
+ pass "README title matches package.json version ($PACKAGE_VERSION)"
137
+ else
138
+ fail_case "README version drift" "README.md title must match package.json version $PACKAGE_VERSION"
139
+ fi
140
+
141
+ forbidden_surface_patterns=(
142
+ 'ERP reads.*tracking\.json'
143
+ 'tracking\.json.*ERP reads'
144
+ 'reads it via git'
145
+ 'reads it automatically'
146
+ 'passive monitoring'
147
+ 'Pre-compact.*Saves state'
148
+ 'Stamps tracking\.json via a bot commit'
149
+ 'Surface stays at 32 skills'
150
+ 'Same 32 skills'
151
+ 'orchestrator treats.*silent PASS'
152
+ 'skip silently and note'
153
+ '"client_id": "acme"'
154
+ '"workspace_id": "qualia-solutions"'
155
+ )
156
+
157
+ for pattern in "${forbidden_surface_patterns[@]}"; do
158
+ hits=""
159
+ while IFS= read -r file; do
160
+ [ -z "$file" ] && continue
161
+ found=$(grep -nE "$pattern" "$file" 2>/dev/null || true)
162
+ if [ -n "$found" ]; then
163
+ rel="${file#$FRAMEWORK_ROOT/}"
164
+ hits="${hits}${hits:+; }${rel}: $(echo "$found" | head -1)"
165
+ fi
166
+ done <<<"$SCAN_FILES"
167
+
168
+ if [ -n "$hits" ]; then
169
+ fail_case "forbidden stale surface pattern: $pattern" "$hits"
170
+ else
171
+ pass "no stale surface pattern: $pattern"
172
+ fi
173
+ done
174
+
133
175
  echo ""
134
176
  echo "Results: $PASS passed, $FAIL failed"
135
177
 
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env bash
2
+ # tests/run-all.sh — fail-collect test orchestrator.
3
+ # Runs every suite even if earlier ones fail, then reports which suites failed.
4
+ # Exits non-zero if any suite failed.
5
+
6
+ set -u
7
+
8
+ DIR="$(cd "$(dirname "$0")" && pwd)"
9
+
10
+ SUITES=(
11
+ "statusline"
12
+ "state"
13
+ "hooks"
14
+ "bin"
15
+ "lib"
16
+ "skills"
17
+ "refs"
18
+ "install-smoke"
19
+ "slop-detect"
20
+ )
21
+
22
+ FAILED=()
23
+
24
+ for suite in "${SUITES[@]}"; do
25
+ file="$DIR/$suite.test.sh"
26
+ if [ ! -f "$file" ]; then
27
+ echo "MISSING: $file"
28
+ FAILED+=("$suite (missing file)")
29
+ continue
30
+ fi
31
+ if bash "$file"; then
32
+ :
33
+ else
34
+ FAILED+=("$suite")
35
+ fi
36
+ done
37
+
38
+ echo ""
39
+ echo "===================================================="
40
+ if [ ${#FAILED[@]} -eq 0 ]; then
41
+ echo "All ${#SUITES[@]} suites passed."
42
+ exit 0
43
+ else
44
+ echo "${#FAILED[@]} of ${#SUITES[@]} suites FAILED:"
45
+ for s in "${FAILED[@]}"; do
46
+ echo " - $s"
47
+ done
48
+ exit 1
49
+ fi
package/tests/runner.js CHANGED
@@ -147,6 +147,7 @@ describe("CLI", () => {
147
147
  assert.match(clean, /analytics/);
148
148
  assert.match(clean, /set-erp-key/);
149
149
  assert.match(clean, /erp-ping/);
150
+ assert.match(clean, /project-snapshot/);
150
151
  assert.match(clean, /doctor/);
151
152
  assert.match(clean, /flush/);
152
153
  } finally {
@@ -987,6 +988,9 @@ waves: 1
987
988
  // New v3.6 fields (default to empty / 0, but must be present)
988
989
  assert.ok("team_id" in t, "team_id missing");
989
990
  assert.ok("project_id" in t, "project_id missing");
991
+ assert.ok("erp_project_id" in t, "erp_project_id missing");
992
+ assert.ok("client_id" in t, "client_id missing");
993
+ assert.ok("workspace_id" in t, "workspace_id missing");
990
994
  assert.ok("git_remote" in t, "git_remote missing");
991
995
  assert.ok("session_started_at" in t, "session_started_at missing");
992
996
  assert.ok("last_pushed_at" in t, "last_pushed_at missing");
@@ -1507,25 +1511,18 @@ describe("Hooks", () => {
1507
1511
 
1508
1512
  // v3.4.2: behavioral test — the stamp must actually mutate tracking.json
1509
1513
  // AND create a real commit so the push includes it.
1510
- //
1511
- // v4.1.1 NOTE: skipped on Windows. The stamp-commit interacts with git's
1512
- // autocrlf in ways that are not fully reproducible without a live Windows
1513
- // box — pre-push.js now passes `-c core.autocrlf=false` on its own git
1514
- // commands (defensive), but the test's seed-commit path still hits an
1515
- // edge case on Windows that needs platform-specific investigation. This
1516
- // is tracked as a v4.1.2 follow-up; the Linux+macOS paths (which are the
1517
- // overwhelming majority of installs) are fully covered here.
1518
- it("pre-push.js mutates tracking.json AND commits the stamp", { skip: process.platform === "win32" ? "pre-existing autocrlf edge case — investigate in v4.1.2" : false }, () => {
1514
+ // v6.2.0: pre-push.js no longer commits — it stamps tracking.json locally
1515
+ // only. Test asserts the inverse of the pre-v6.2 behavior: file is mutated,
1516
+ // HEAD is unchanged, file shows as modified in the working tree.
1517
+ it("pre-push.js stamps tracking.json locally without creating a commit", () => {
1519
1518
  const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "qualia-push-real-"));
1520
1519
  try {
1521
- // Init a real git repo
1522
1520
  const gitOpts = { cwd: tmpDir, encoding: "utf8", timeout: 5000, stdio: ["pipe", "pipe", "pipe"] };
1523
1521
  spawnSync("git", ["init", "--initial-branch=main"], gitOpts);
1524
1522
  spawnSync("git", ["config", "user.email", "test@example.com"], gitOpts);
1525
1523
  spawnSync("git", ["config", "user.name", "Test"], gitOpts);
1526
1524
  spawnSync("git", ["config", "commit.gpgsign", "false"], gitOpts);
1527
1525
 
1528
- // Seed .planning/tracking.json + an initial commit
1529
1526
  fs.mkdirSync(path.join(tmpDir, ".planning"));
1530
1527
  const tFile = path.join(tmpDir, ".planning", "tracking.json");
1531
1528
  fs.writeFileSync(tFile, JSON.stringify({
@@ -1536,25 +1533,25 @@ describe("Hooks", () => {
1536
1533
 
1537
1534
  const headBefore = spawnSync("git", ["rev-parse", "HEAD"], gitOpts).stdout.trim();
1538
1535
 
1539
- // Run the hook
1540
1536
  const r = spawnSync(NODE, [path.join(HOOKS, "pre-push.js")], {
1541
1537
  encoding: "utf8", cwd: tmpDir, timeout: 10000, stdio: ["pipe", "pipe", "pipe"],
1542
1538
  });
1543
1539
  assert.equal(r.status, 0, `pre-push exited ${r.status}: ${r.stderr}`);
1544
1540
 
1545
- // tracking.json must have been mutated
1541
+ // tracking.json must have been mutated on disk
1546
1542
  const t = JSON.parse(fs.readFileSync(tFile, "utf8"));
1547
1543
  assert.notEqual(t.last_commit, "OLD", "last_commit should have been updated");
1548
1544
  assert.notEqual(t.last_updated, "2020-01-01T00:00:00Z", "last_updated should have been updated");
1549
1545
  assert.match(t.last_updated, /^\d{4}-\d{2}-\d{2}T/);
1546
+ assert.match(t.last_pushed_at, /^\d{4}-\d{2}-\d{2}T/, "last_pushed_at must be set");
1550
1547
 
1551
- // A NEW commit must exist (this is the smoking-gun fix from v3.4.2)
1548
+ // HEAD must NOT advance v6.2.0 contract: no bot commit
1552
1549
  const headAfter = spawnSync("git", ["rev-parse", "HEAD"], gitOpts).stdout.trim();
1553
- assert.notEqual(headAfter, headBefore, "pre-push must commit the stamp so it ships with the push");
1550
+ assert.equal(headAfter, headBefore, "pre-push must NOT create a commit (v6.2.0)");
1554
1551
 
1555
- // The new commit must be authored by the bot, not the user
1556
- const author = spawnSync("git", ["log", "-1", "--format=%an <%ae>"], gitOpts).stdout.trim();
1557
- assert.match(author, /Qualia Framework/);
1552
+ // The file should appear as modified-not-staged in the working tree
1553
+ const status = spawnSync("git", ["status", "--porcelain", "--", ".planning/tracking.json"], gitOpts).stdout;
1554
+ assert.match(status, /^ M /, "tracking.json should be unstaged modified");
1558
1555
  } finally {
1559
1556
  fs.rmSync(tmpDir, { recursive: true, force: true });
1560
1557
  }
@@ -1601,19 +1598,7 @@ describe("Hooks", () => {
1601
1598
  }
1602
1599
  });
1603
1600
 
1604
- // --- pre-compact.js ---
1605
-
1606
- it("pre-compact.js exits 0 with no STATE.md", () => {
1607
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "qualia-pc-"));
1608
- try {
1609
- const r = spawnSync(NODE, [path.join(HOOKS, "pre-compact.js")], {
1610
- encoding: "utf8", cwd: tmpDir, timeout: 5000, stdio: ["pipe", "pipe", "pipe"],
1611
- });
1612
- assert.equal(r.status, 0);
1613
- } finally {
1614
- fs.rmSync(tmpDir, { recursive: true, force: true });
1615
- }
1616
- });
1601
+ // pre-compact.js removed in v6.2.0 — state.js journal provides crash safety.
1617
1602
 
1618
1603
  // --- auto-update.js ---
1619
1604
 
@@ -2593,12 +2578,13 @@ describe("install.js", () => {
2593
2578
  }
2594
2579
  });
2595
2580
 
2596
- it("9 hooks installed (block-env-edit removed in v3.2.0; git-guardrails + stop-session-log added in v4.2.0)", () => {
2581
+ it("11 hooks installed (v6.2.0: pre-compact removed; v5.0: vercel-account-guard, env-empty-guard, supabase-destructive-guard added)", () => {
2597
2582
  const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "qualia-install-"));
2598
2583
  try {
2599
2584
  runInstall("QS-FAWZI-01", tmpHome);
2600
2585
  const hooks = fs.readdirSync(path.join(tmpHome, ".claude", "hooks")).filter(f => f.endsWith(".js"));
2601
- assert.equal(hooks.length, 9);
2586
+ assert.equal(hooks.length, 11, `expected 11 hooks, got ${hooks.length}: ${hooks.join(", ")}`);
2587
+ assert.ok(!hooks.includes("pre-compact.js"), "pre-compact.js must NOT be installed (removed in v6.2.0)");
2602
2588
  } finally {
2603
2589
  fs.rmSync(tmpHome, { recursive: true, force: true });
2604
2590
  }
@@ -86,11 +86,13 @@ export default function Page() {
86
86
  return <p>Welcome — to our amazing platform</p>;
87
87
  }
88
88
  EOF
89
- OUT=$($NODE "$SLOP_DETECT" "$TMP2/emdash.tsx" 2>&1 || true)
90
- if echo "$OUT" | grep -qiE "em.?dash|—"; then
89
+ EXIT_CODE=0
90
+ OUT=$($NODE "$SLOP_DETECT" "$TMP2/emdash.tsx" 2>&1) || EXIT_CODE=$?
91
+ # Em-dash is HIGH (non-blocking) so exit MUST be 0; finding MUST be in output.
92
+ if [ "$EXIT_CODE" = "0" ] && echo "$OUT" | grep -qiE "em.?dash|—"; then
91
93
  pass "reports em-dash finding (HIGH severity, non-blocking)"
92
94
  else
93
- fail_case "em-dash detection" "no em-dash mention in output: $(echo "$OUT" | head -c 120)"
95
+ fail_case "em-dash detection" "exit=$EXIT_CODE, output: $(echo "$OUT" | head -c 120)"
94
96
  fi
95
97
 
96
98
  # ── Banned-font detection ─────────────────────────────────────────────
@@ -138,8 +140,12 @@ fi
138
140
  # ── --json flag produces JSON output ─────────────────────────────────
139
141
  TMP5=$(mktmp)
140
142
  cp "$TMP3/font.css" "$TMP5/font.css"
141
- JSON_OUT=$($NODE "$SLOP_DETECT" --json "$TMP5/font.css" 2>/dev/null || true)
142
- if echo "$JSON_OUT" | head -1 | grep -qE "^[\{\[]"; then
143
+ # --json may exit 1 (banned font is CRITICAL); we only assert output shape.
144
+ EXIT_CODE=0
145
+ JSON_OUT=$($NODE "$SLOP_DETECT" --json "$TMP5/font.css" 2>/dev/null) || EXIT_CODE=$?
146
+ if [ "$EXIT_CODE" != "0" ] && [ "$EXIT_CODE" != "1" ]; then
147
+ fail_case "--json output" "unexpected exit=$EXIT_CODE (only 0/1 acceptable for CRITICAL)"
148
+ elif echo "$JSON_OUT" | head -1 | grep -qE "^[\{\[]"; then
143
149
  pass "--json flag produces JSON-shaped output"
144
150
  else
145
151
  fail_case "--json output" "first line is not JSON-shaped: '$(echo "$JSON_OUT" | head -c 80)'"
@@ -109,7 +109,10 @@ fi
109
109
  if grep -q '"project": "TestProject"' "$TMP/.planning/tracking.json" \
110
110
  && grep -q '"total_phases": 2' "$TMP/.planning/tracking.json" \
111
111
  && grep -q '"phase": 1' "$TMP/.planning/tracking.json" \
112
- && grep -q '"status": "setup"' "$TMP/.planning/tracking.json"; then
112
+ && grep -q '"status": "setup"' "$TMP/.planning/tracking.json" \
113
+ && grep -q '"erp_project_id": ""' "$TMP/.planning/tracking.json" \
114
+ && grep -q '"client_id": ""' "$TMP/.planning/tracking.json" \
115
+ && grep -q '"workspace_id": ""' "$TMP/.planning/tracking.json"; then
113
116
  pass "cmdInit tracking.json has correct fields"
114
117
  else
115
118
  fail_case "cmdInit tracking.json fields"
@@ -1,125 +0,0 @@
1
- #!/usr/bin/env node
2
- // ~/.claude/hooks/pre-compact.js — commit STATE.md before context compaction.
3
- // PreCompact hook. Silent on failure — context compaction must never be blocked.
4
- // Cross-platform (Windows/macOS/Linux).
5
- //
6
- // BY DEFAULT this commit uses --no-verify + --no-gpg-sign. The auto-save is a
7
- // framework bot commit, and pre-commit hooks that run full test suites would
8
- // routinely fail (context compaction happens at any moment) and lose the
9
- // STATE.md snapshot. But compliance-sensitive projects can opt into strict
10
- // mode via ~/.claude/.qualia-config.json:
11
- //
12
- // {
13
- // "pre_compact": {
14
- // "respect_user_hooks": true,
15
- // "respect_gpg_signing": true
16
- // }
17
- // }
18
- //
19
- // When either is true, the corresponding --no-* flag is dropped.
20
-
21
- const fs = require("fs");
22
- const path = require("path");
23
- const os = require("os");
24
- const { spawnSync } = require("child_process");
25
-
26
- const _traceStart = Date.now();
27
-
28
- const STATE_FILE = path.join(".planning", "STATE.md");
29
- const TRACKING_FILE = path.join(".planning", "tracking.json");
30
- const PLANNING_FILES = [STATE_FILE, TRACKING_FILE];
31
- const CONFIG_FILE = path.join(os.homedir(), ".claude", ".qualia-config.json");
32
-
33
- function readCompactConfig() {
34
- try {
35
- const cfg = JSON.parse(fs.readFileSync(CONFIG_FILE, "utf8"));
36
- return cfg.pre_compact || {};
37
- } catch {
38
- return {};
39
- }
40
- }
41
-
42
- function git(args, opts = {}) {
43
- return spawnSync("git", args, {
44
- encoding: "utf8",
45
- timeout: 3000,
46
- shell: process.platform === "win32",
47
- ...opts,
48
- });
49
- }
50
-
51
- function planningHasPendingChanges() {
52
- // v5.0: also stage tracking.json. The CLAUDE.md compaction contract says to
53
- // preserve tracking.json across compactions, but pre-v5 only committed
54
- // STATE.md — tracking.json mutations from state.js transitions were lost
55
- // across compactions, leaving the ERP with stale milestone/phase/tasks data.
56
- const diff = git(["diff", "--name-only", "--", ...PLANNING_FILES]);
57
- const diffLines = (diff.stdout || "").split(/\r?\n/);
58
- if (PLANNING_FILES.some((f) => diffLines.includes(f))) return true;
59
-
60
- const untracked = git(["ls-files", "--others", "--exclude-standard", "--", ...PLANNING_FILES]);
61
- const untrackedLines = (untracked.stdout || "").split(/\r?\n/);
62
- return PLANNING_FILES.some((f) => untrackedLines.includes(f));
63
- }
64
-
65
- let _commitStatus = null;
66
- let _commitReason = "no-state-file";
67
- let _commitFlags = null;
68
-
69
- try {
70
- if (fs.existsSync(STATE_FILE)) {
71
- console.log("QUALIA: Saving state before compaction...");
72
- _commitReason = "state-clean";
73
- // Check if STATE.md or tracking.json has tracked or untracked changes.
74
- if (planningHasPendingChanges()) {
75
- // Stage both planning files. `git add` silently no-ops on files that don't exist
76
- // or have no changes, so passing both is safe even when only one is dirty.
77
- const addRes = git(["add", ...PLANNING_FILES]);
78
- const cfg = readCompactConfig();
79
- const commitArgs = ["commit"];
80
- if (!cfg.respect_user_hooks) commitArgs.push("--no-verify");
81
- if (!cfg.respect_gpg_signing) commitArgs.push("--no-gpg-sign");
82
- commitArgs.push("--author=Qualia Framework <bot@qualia.solutions>");
83
- commitArgs.push("-m", "state: pre-compaction save (STATE.md + tracking.json)");
84
- _commitFlags = {
85
- no_verify: !cfg.respect_user_hooks,
86
- no_gpg_sign: !cfg.respect_gpg_signing,
87
- };
88
- const commitRes = spawnSync("git", commitArgs, {
89
- timeout: 5000,
90
- stdio: ["ignore", "ignore", "pipe"],
91
- encoding: "utf8",
92
- shell: process.platform === "win32",
93
- });
94
- _commitStatus = commitRes.status;
95
- _commitReason = addRes.status === 0 && commitRes.status === 0
96
- ? "committed"
97
- : "commit-failed";
98
- } else {
99
- _commitReason = "state-clean";
100
- }
101
- }
102
- } catch {
103
- // Silent — never block compaction
104
- _commitReason = "exception";
105
- }
106
-
107
- function _trace(hookName, result, extra) {
108
- try {
109
- const os = require("os");
110
- const traceDir = path.join(os.homedir(), ".claude", ".qualia-traces");
111
- if (!fs.existsSync(traceDir)) fs.mkdirSync(traceDir, { recursive: true });
112
- const entry = {
113
- hook: hookName,
114
- result,
115
- timestamp: new Date().toISOString(),
116
- duration_ms: Date.now() - _traceStart,
117
- ...extra,
118
- };
119
- const file = path.join(traceDir, `${new Date().toISOString().split("T")[0]}.jsonl`);
120
- fs.appendFileSync(file, JSON.stringify(entry) + "\n");
121
- } catch {}
122
- }
123
-
124
- _trace("pre-compact", "allow", { commit_status: _commitStatus, commit_reason: _commitReason, commit_flags: _commitFlags });
125
- process.exit(0);