qualia-framework 4.1.0 → 4.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.
@@ -0,0 +1,263 @@
1
+ # Qualia Framework v4.1.0 — Production Review
2
+
3
+ **Date:** 2026-04-22
4
+ **Scope:** 26 skills, 8 agents, 7 hooks, 4 bin scripts, 6 rules, templates, docs, HTML surfaces
5
+ **Method:** 4 parallel specialist audits (commands/skills, hooks/state, memory/knowledge, design/UX)
6
+ **Findings:** 142 total across 4 dimensions
7
+
8
+ ---
9
+
10
+ ## Summary
11
+
12
+ | Dimension | Critical | High | Medium | Low | Score |
13
+ |-----------|----------|------|--------|-----|-------|
14
+ | Commands & Skills | 5 | 14 | 17 | 9 | **2/5** |
15
+ | Hooks & State | 0 | 3 | 16 | 13 | **3/5** |
16
+ | Memory & Knowledge | 3 | 5 | 6 | 6 | **2/5** |
17
+ | Design & UX | 0 | 11 | 15 | 11 | **2/5** |
18
+ | **Total** | **8** | **33** | **54** | **47** | **2.25/5** |
19
+
20
+ **Verdict: FAIL** — 8 critical blockers, 33 high findings. Framework is functional for OWNER daily use but structurally unsound for multi-employee scale. Ship v4.2.0 with the critical-fix batch before onboarding new team members.
21
+
22
+ ---
23
+
24
+ ## Top 10 Critical & High Issues (fix first)
25
+
26
+ ### 1. `grounding.md` is a phantom for existing installs [CRITICAL]
27
+ **Where:** `skills/qualia-{build,plan,verify,design}/SKILL.md`, `agents/{builder,planner,plan-checker,verifier}.md` — all `@~/.claude/rules/grounding.md` references.
28
+ **Reality:** File exists in repo `rules/grounding.md` but **not in installed** `~/.claude/rules/` for users who upgraded from v4.0.x (current local install is missing it). install.js copies it on fresh install only; no self-healing check on version bump.
29
+ **Impact:** Every planner, builder, plan-checker, verifier spawn silently gets no Grounding Protocol and no Severity Rubric. All 1–5 scores by verifier are improvised. Affects every Road phase, every project, silently.
30
+ **Fix:** Add a hook or install.js post-upgrade routine that compares `rules/` contents and copies missing files. Emit a warning if critical files missing.
31
+
32
+ ### 2. Builder agent never reads knowledge base [CRITICAL]
33
+ **Where:** `agents/builder.md` — zero references to `~/.claude/knowledge/`.
34
+ **Impact:** Patterns in `learned-patterns.md`, `supabase-patterns.md`, `common-fixes.md` reach the planner but not the agent that writes code. "Always use `lib/supabase/server.ts` for mutations" never reaches the coder.
35
+ **Fix:** Add a "load relevant patterns" step to builder.md's preamble; pipe through `@~/.claude/knowledge/learned-patterns.md` like grounding.md.
36
+
37
+ ### 3. 11 of 14 knowledge files are invisible to every skill [CRITICAL]
38
+ **Where:** `~/.claude/knowledge/` has `qualia-context.md`, `deployment-map.md`, `supabase-patterns.md`, `voice-agent-patterns.md`, `employees.md`, `claudecode-bible.md`, `optimization-research-2026.md`, `session-digest.md`, `email-signature.html`, etc. — grepping the framework for these filenames returns zero results.
39
+ **Impact:** Every skill hardcodes filenames (`cat ~/.claude/knowledge/common-fixes.md`). New knowledge files are dead weight. `session-digest.md` hasn't updated since Apr 12 — the hook that wrote it no longer exists.
40
+ **Fix:** Replace hardcoded cats with `node ~/.claude/bin/knowledge.js load --type <X>` — a discovery utility that indexes and merges tiers. See Memory Architecture proposal §B below.
41
+
42
+ ### 4. No per-employee memory scoping [CRITICAL]
43
+ **Where:** All memory is in `~/.claude/knowledge/` with no user scope. Hasan, Fawzi, Moayad all share the same global knowledge files.
44
+ **Impact:** Personal preferences mix with universal patterns. `employees.md` is a flat roster, not a scoping mechanism.
45
+ **Fix:** 3-tier memory architecture — see §B.
46
+
47
+ ### 5. `qualia-report` ERP upload: silent injection & auth failures [CRITICAL]
48
+ **Where:** `skills/qualia-report/SKILL.md:84-87, 114-163`.
49
+ - Empty `API_KEY` → sends `Authorization: Bearer ` (blank); 401 handler says "Ask Fawzi" without diagnosing.
50
+ - `SUBMITTED_BY` shell-interpolated into `node -e` string → single quote in git user.name breaks silently.
51
+ - `CLIENT_REPORT_ID` fallback to empty string on state.js failure → commit message loses the ID; ERP payload corrupted.
52
+ **Fix:** Add `[ -z "$API_KEY" ] && fail "No ERP key"` guard. Pipe SUBMITTED_BY via env var, not string interp. Validate report_id non-empty before proceeding.
53
+
54
+ ### 6. Triple overlap: `qualia-optimize` vs `qualia-review` vs `qualia-polish` [HIGH]
55
+ **Where:** All three run the same grep patterns (fonts, max-width, gradients, service_role, empty catches, sequential awaits, `<img>` without next/image). Different output files (OPTIMIZE.md / REVIEW.md / inline fix). Different severity scales. User gets three reports about the same issues.
56
+ **Fix:** Pick one. Recommend: `qualia-review` = detect-only (grep + score), `qualia-polish` = fix-only (applies known transforms), delete `qualia-optimize` or repurpose it as a parallel-agent wrapper around review+polish.
57
+
58
+ ### 7. 4 skills are orphans — never invoked by the Road [HIGH]
59
+ **Where:** `qualia-test`, `qualia-debug`, `qualia-task`, `qualia-quick` are never auto-triggered by any Road step. The Road has no automated test gate between build and verify. If verification fails, the Road goes to `/qualia-plan --gaps`, not `/qualia-debug`.
60
+ **Fix:** Add `/qualia-test` as a hard gate inside `/qualia-build` post-wave. Route verify-fail → `/qualia-debug` → `/qualia-plan --gaps` chain. Kill `qualia-task` (absorbed into `/qualia-quick`).
61
+
62
+ ### 8. Color palette drift across surfaces [HIGH]
63
+ **Where:** `TEAL_DIM` differs `rgb(0,130,135)` (statusline.js:19) vs `rgb(0,140,145)` (qualia-ui.js:31). `DIM` differs `rgb(80,90,100)` vs `rgb(100,110,120)` across 3 files. Brand teal in demo.html `#00ced9` vs help.html `#00ced1`. Display fonts split: Fraunces (demo) vs Outfit (help).
64
+ **Fix:** Create `bin/colors.js` as single source of truth; import in all 5 surfaces. Unify brand token vocabulary between demo.html and help.html.
65
+
66
+ ### 9. Statusline shows installer name, not operator [HIGH]
67
+ **Where:** `bin/statusline.js:292` — reads `.qualia-config.json` which is set at install time.
68
+ **Impact:** On a shared machine, all operators see the installer's name. `Qualia member: Hasan` even when Fawzi is driving.
69
+ **Fix:** Read from a session-scoped identity (env var, git user.name) or add `/qualia-whoami` switch.
70
+
71
+ ### 10. `/qualia-ship` has no state guard and no security depth [HIGH]
72
+ **Where:** `skills/qualia-ship/SKILL.md`.
73
+ - No `state.js check` at start; can invoke from any state.
74
+ - Security check only greps `service_role` — misses hardcoded keys, tracked `.env`, `dangerouslySetInnerHTML`.
75
+ - Post-deploy verification uses literal `{domain}` placeholder — expects LLM to hallucinate URL.
76
+ **Fix:** Add state-preconditions block (must be `polished` or final-phase verified). Inline the full qualia-review CRITICAL checks. Read `deployed_url` from tracking.json like `/qualia-handoff` does.
77
+
78
+ ---
79
+
80
+ ## Dimension Deep-Dives
81
+
82
+ ### A. Silent Fails (selected — 15 most dangerous)
83
+
84
+ | # | File:line | Severity | Silent behavior |
85
+ |---|-----------|----------|-----------------|
86
+ | A1 | `state.js:179,229` | MED | `readTracking/readState` swallow corrupt JSON as "missing" → user told "No .planning/" when file is corrupt |
87
+ | A2 | `state.js:59` | MED | Corrupt journal silently cleared, no log |
88
+ | A3 | `state.js:107` | HIGH | Lock timeout falls through silently — concurrent writes possible |
89
+ | A4 | `pre-push.js:22,47` | HIGH | No lock coordination with state.js; assumes cwd is repo root |
90
+ | A5 | `pre-deploy-gate.js:51` | MED | `package.json` missing → ALL gates silently skipped |
91
+ | A6 | `pre-deploy-gate.js:146` | MED | File read failure → security scan silently skips that file |
92
+ | A7 | `session-start.js:128` | LOW | Top-level catch; crash inside try → no banner, no error, trace records "allow" anyway |
93
+ | A8 | `qualia-report/SKILL.md:84` | CRIT | Empty pipe chain → report_id becomes `""`, commit loses ID |
94
+ | A9 | `qualia-report/SKILL.md:114` | CRIT | Empty API key → blank Bearer token sent to ERP |
95
+ | A10 | `qualia-handoff/SKILL.md:39` | CRIT | Malformed tracking.json → empty URL; verification curl hits blank target |
96
+ | A11 | `qualia-help/SKILL.md:44` | HIGH | Missing template → sed writes empty file → blank help page opens; "ok" message still printed |
97
+ | A12 | `qualia-verify/SKILL.md:65` | HIGH | `FRONTEND=true` never set — frontend detection broken; browser QA never spawns |
98
+ | A13 | `migration-guard.js:96` | MED | DROP TABLE check is file-global, not per-statement — one `IF EXISTS` masks another unguarded DROP |
99
+ | A14 | `state.js:329` | MED | Progress bar: phase 1/5 shows 0%, phase 5/5 shows 80% — never 100% |
100
+ | A15 | `cli.js:827` | HIGH | ERP ping uses `curl` — unavailable on older Windows |
101
+
102
+ ### B. Memory Architecture — 3-Tier Proposal
103
+
104
+ Current state is flat, untiered, and half-invisible. Proposed structure:
105
+
106
+ ```
107
+ Tier 1 — FRAMEWORK (universal, ships with framework)
108
+ ~/.claude/knowledge/framework/
109
+ ├─ stack-patterns.md ← Next.js, Supabase, Vercel
110
+ ├─ voice-patterns.md ← Retell, ElevenLabs, Telnyx
111
+ ├─ security-patterns.md ← RLS, service_role, env
112
+ ├─ common-fixes.md ← cross-project fixes
113
+ └─ KNOWLEDGE-INDEX.md ← auto-generated manifest
114
+
115
+ Tier 2 — EMPLOYEE (per-user, ~/.claude/ scoped by team code)
116
+ ~/.claude/knowledge/employee/
117
+ ├─ preferences.md ← my coding style, timezone, tools
118
+ ├─ my-patterns.md ← things I've learned
119
+ ├─ my-fixes.md ← errors I've solved
120
+ └─ KNOWLEDGE-INDEX.md
121
+
122
+ Tier 3 — PROJECT (per-repo, version-controlled with project)
123
+ .planning/knowledge/
124
+ ├─ client-prefs.md ← this client's brand/stack
125
+ ├─ project-patterns.md ← project-specific gotchas
126
+ ├─ deployment.md ← this project's deploy notes
127
+ └─ KNOWLEDGE-INDEX.md
128
+ ```
129
+
130
+ **Discovery utility:** `bin/knowledge.js` — single CLI for all skills.
131
+ ```
132
+ knowledge.js load --type patterns # merges all 3 tiers, project overrides employee overrides framework
133
+ knowledge.js search "supabase RLS" # searches all tiers, labels source
134
+ knowledge.js index # regenerates INDEX files
135
+ knowledge.js promote --from employee --to framework # for /qualia-learn
136
+ ```
137
+
138
+ **Staleness:** INDEX tracks `last_verified` per file. Entries >30 days get `[STALE]` tag in merge output.
139
+
140
+ **Integration with Claude native memory:** Keep `CLAUDE_CODE_DISABLE_AUTO_MEMORY=0`. Native memory captures conversational context; Qualia knowledge captures structured reusable patterns. Add `/qualia-learn --promote` to migrate native → structured. Statusline shows both counts: `K:14 M:4`.
141
+
142
+ **Migration (one-off script):**
143
+ 1. Move universal bits of `learned-patterns.md` → `framework/stack-patterns.md`
144
+ 2. Move project-specific section → each project's `.planning/knowledge/`
145
+ 3. Move `email-signature.html` out of knowledge/ (not knowledge — asset)
146
+ 4. Delete `session-digest.md` (dead)
147
+ 5. Update 5 reader skills to use `knowledge.js load`
148
+
149
+ ### C. Commands That Don't Fit the Road
150
+
151
+ | Skill | Status | Recommendation |
152
+ |-------|--------|----------------|
153
+ | `qualia-task` | Orphan, overlaps `qualia-quick` | **Delete** — fold into `/qualia-quick --proper` flag |
154
+ | `qualia-quick` | Orphan | Keep, add `--proper` flag for atomic-commit behavior of task |
155
+ | `qualia-test` | Orphan | Integrate into `/qualia-build` post-wave as hard gate |
156
+ | `qualia-debug` | Orphan, only suggested | Auto-invoke after verify-fail before `/qualia-plan --gaps` |
157
+ | `qualia-design` | Duplicates polish ~90% | **Merge into** `/qualia-polish`; delete standalone |
158
+ | `qualia-optimize` | Overlaps review+polish | Repurpose as parallel-agent wrapper; specialists run review+polish in fan-out |
159
+ | `qualia-review` | Orphan, no state guard | Integrate as mandatory step between build and verify (code review before goal check) |
160
+ | `qualia-map` | Optional brownfield | Keep, but add auto-trigger on `/qualia-new` if `package.json` exists |
161
+ | `qualia-research` | Optional | Keep as phase-depth option |
162
+ | `qualia-discuss` | Optional | Keep |
163
+ | `qualia-idk` | Alternate router | Keep — genuine diagnostic, not alias (fix help.html:410 description) |
164
+ | `qualia-learn` | Meta | Keep, extend with tier promotion (see §B) |
165
+ | `qualia-help` | Reference | Keep |
166
+ | `qualia-skill-new` | Authoring | Keep |
167
+
168
+ ### D. Missing from the Road (gaps)
169
+
170
+ 1. **Automated test gate** — no skill runs tests after build, before verify. Add to `/qualia-build`.
171
+ 2. **Inter-phase code review** — goal-backward verify doesn't check code quality. Add `/qualia-review` between build and verify.
172
+ 3. **Adversarial/red-team build loop** — per harness-engineering research (Anthropic GAN pattern, Archon adversarial-dev). Generator ↔ evaluator with pre-negotiated sprint contract. Add `/qualia-adversarial` or fold into `/qualia-build --adversarial`.
173
+ 4. **Continuous reviewer agents** — no background persona reviewers (security, reliability, frontend-architect) on each commit. Stripe Minion pattern; Lopopolo "every push" pattern.
174
+ 5. **Component freshness** — no skill pulls latest shadcn/Radix versions or checks for staleness. Proposal below.
175
+
176
+ ### E. Component Fetching (user's explicit ask)
177
+
178
+ No current mechanism. Framework has no `shadcn`, `radix`, or `component` references.
179
+
180
+ **Proposal: `/qualia-components` skill**
181
+ ```
182
+ /qualia-components add button card dialog # pulls latest shadcn via `npx shadcn@latest add`
183
+ /qualia-components update # bumps versions; writes .planning/component-changelog.md
184
+ /qualia-components check # reports N components > X days stale
185
+ ```
186
+ - Integrates with existing `vercel-plugin:shadcn` skill for shadcn ops
187
+ - Uses WebFetch from researcher agent to pull latest docs (`https://ui.shadcn.com/docs/components/{name}`)
188
+ - Writes a diff summary so planner knows what changed
189
+ - Add a mandatory check inside `/qualia-verify`: flag components > 1 major version behind
190
+
191
+ **Do not auto-update during builds.** Component breaks should be explicit, versioned, verified.
192
+
193
+ ### F. Design Polish Roadmap (5 phases)
194
+
195
+ **Phase 1 — Color unification (1 day)**
196
+ Create `bin/colors.js`; delete inline palettes in statusline, qualia-ui, session-start, install, cli. Fix TEAL_DIM, DIM, brand teal mismatches. DIM must pass WCAG AA (4.5:1) on dark terminals — currently fails at ~3.2:1.
197
+
198
+ **Phase 2 — Statusline ergonomics (1 day)**
199
+ Terminal-width guard on Line 1 (progressive truncation: identity → memory → worktree → agent). Add milestone indicator `M2 P3/5 T2/4`. Add last-error from `tracking.json.last_error`. Format duration as `Xh Ym` over 60min. Add burn rate `$/min`.
200
+
201
+ **Phase 3 — HTML modernization (1 day)**
202
+ help.html: replace `@import` with `<link rel="preconnect"><link rel="stylesheet">`. Unify design tokens with demo.html. Adopt `@container` queries for card grids. Add `color-scheme: dark`. Fix **demo.html:506 `</span>` → `</div>`** (broken DOM). Fix help.html:410 `/qualia-idk` description (currently says "Alias for /qualia" — wrong).
203
+
204
+ **Phase 4 — Skill description cleanup (half day)**
205
+ Trim 10 skills over 30-word descriptions. Add trigger phrases to 4 that have none (report, ship, verify, build). Cap at 20 words + triggers for autocomplete readability.
206
+
207
+ **Phase 5 — Component fetching (1-2 days)** — see §E.
208
+
209
+ ### G. Team Ergonomics — What's Missing
210
+
211
+ For a 5-person team operating from a shared fleet:
212
+ - **Per-operator identity** on statusline (not installer)
213
+ - **Active agent name/count** — which subagent is running right now
214
+ - **Milestone progress** alongside phase progress (`M2/4 P3/5`)
215
+ - **Shared-state indicator** — is someone else editing this project?
216
+ - **Last verification score** from tracking.json
217
+ - **Blocker summary** beyond the pill (currently only shows count, no first-line)
218
+
219
+ ---
220
+
221
+ ## Action Roadmap (priority-ordered)
222
+
223
+ ### v4.1.1 Hotfix (ship this week)
224
+ 1. Auto-copy missing rules on version bump (fixes grounding.md phantom)
225
+ 2. Guard empty API_KEY in qualia-report
226
+ 3. Fix broken `</span>` in demo.html:506
227
+ 4. Fix help.html:410 `/qualia-idk` description
228
+ 5. Add state guard to `/qualia-ship`
229
+
230
+ ### v4.2.0 — Structural (2 weeks)
231
+ 1. 3-tier memory architecture + `bin/knowledge.js`
232
+ 2. Merge `qualia-design` into `qualia-polish`, delete orphan `qualia-task`
233
+ 3. Unified `bin/colors.js`
234
+ 4. Builder agent loads knowledge
235
+ 5. Integrate `/qualia-test` as build gate, `/qualia-debug` on verify-fail
236
+ 6. cwd → git root resolution for state.js, pre-push, pre-compact (fixes 3 MED findings at once)
237
+
238
+ ### v4.3.0 — Harness patterns (4 weeks)
239
+ 1. Adversarial build mode (generator↔evaluator GAN-style)
240
+ 2. Continuous reviewer agents (security/reliability/frontend personas on every push)
241
+ 3. Source-code structural tests (file<350 lines, schema dedup, no-unknown)
242
+ 4. `/qualia-components` skill
243
+
244
+ ### v4.4.0 — Team-scale (ongoing)
245
+ 1. Per-operator identity on statusline
246
+ 2. Shared-state coordination lock
247
+ 3. Token burn rate + milestone progress indicators
248
+ 4. Knowledge staleness tracking + auto-prune
249
+
250
+ ---
251
+
252
+ ## Verdict
253
+
254
+ **Strengths** — The Road skeleton is sound. Context isolation, goal-backward verification, story-file plans, wave-based parallelization, and the state machine are all genuinely good harness patterns (confirmed by comparison to Anthropic/OpenAI/Archon research).
255
+
256
+ **Structural debt** — Three clusters compound risk:
257
+ 1. **Invisible dependencies** — grounding.md, 11 knowledge files, builder↔knowledge disconnect. The framework believes things are loaded that aren't.
258
+ 2. **Silent failures** — 15 high-impact silent-fail paths where users get "ok" messages while the underlying operation produced garbage.
259
+ 3. **Orphan skills** — 4 commands never touch the Road; 3 triple-overlap. Mental overhead for the team is real.
260
+
261
+ **Shipping bar** — This review would FAIL `/qualia-ship` on a client project. The framework needs v4.1.1 hotfix before onboarding the next employee and v4.2.0 before the next major client delivery.
262
+
263
+ Raw audit transcripts (142 findings in full) are available via the 4 background agent outputs if deeper context is needed on any specific finding.
@@ -110,6 +110,11 @@ try {
110
110
  detected_at: new Date().toISOString(),
111
111
  }, null, 2));
112
112
  } catch {}
113
+ // Invalidate the session-start health cache so the next session re-checks
114
+ // whether new critical files shipped in the latest version are installed.
115
+ try {
116
+ fs.unlinkSync(path.join(CLAUDE_DIR, ".qualia-install-health.json"));
117
+ } catch {}
113
118
  _trace("auto-update", "allow", { reason: "notification-written", current: cfg.version, latest });
114
119
  } else {
115
120
  // Already up to date — clear any stale notification file.
@@ -0,0 +1,167 @@
1
+ #!/usr/bin/env node
2
+ // ~/.claude/hooks/git-guardrails.js — block destructive git commands.
3
+ //
4
+ // PreToolUse hook on `Bash`. Reads the proposed command from the Claude Code
5
+ // hook payload (stdin JSON: tool_input.command) and exits 2 to BLOCK, 0 to
6
+ // allow. Cross-platform (Windows/macOS/Linux), zero shell dependencies.
7
+ //
8
+ // Patterns blocked unconditionally:
9
+ // • git push --force / -f to main or master (use --force-with-lease instead)
10
+ // • git push --force / -f from current branch when current branch is main
11
+ // • git reset --hard <anything> when on main/master (would discard uncommitted)
12
+ // • git clean -fd / -fdx (no recovery — irreversible)
13
+ // • git branch -D main / master (destroys the branch)
14
+ // • rm -rf .git (nukes repo history)
15
+ //
16
+ // Escape hatch:
17
+ // QUALIA_ALLOW_DESTRUCTIVE=1 in env → all checks pass through (use sparingly,
18
+ // document the reason in your commit message).
19
+ //
20
+ // This hook complements branch-guard.js, which blocks NORMAL pushes by EMPLOYEE
21
+ // to main. git-guardrails.js applies to EVERYONE (OWNER too) for irreversible
22
+ // operations — guardrails, not permissions.
23
+
24
+ const fs = require("fs");
25
+ const path = require("path");
26
+ const os = require("os");
27
+ const { spawnSync } = require("child_process");
28
+
29
+ const _traceStart = Date.now();
30
+
31
+ function _trace(result, extra) {
32
+ try {
33
+ const traceDir = path.join(os.homedir(), ".claude", ".qualia-traces");
34
+ if (!fs.existsSync(traceDir)) fs.mkdirSync(traceDir, { recursive: true });
35
+ const entry = {
36
+ hook: "git-guardrails",
37
+ result,
38
+ timestamp: new Date().toISOString(),
39
+ duration_ms: Date.now() - _traceStart,
40
+ ...extra,
41
+ };
42
+ const file = path.join(traceDir, `${new Date().toISOString().split("T")[0]}.jsonl`);
43
+ fs.appendFileSync(file, JSON.stringify(entry) + "\n");
44
+ } catch {}
45
+ }
46
+
47
+ function block(reason, suggestion) {
48
+ const lines = [
49
+ `BLOCKED: ${reason}`,
50
+ suggestion ? `Suggestion: ${suggestion}` : null,
51
+ `Override (use sparingly): QUALIA_ALLOW_DESTRUCTIVE=1`,
52
+ ].filter(Boolean);
53
+ for (const l of lines) {
54
+ console.error(l);
55
+ console.log(l);
56
+ }
57
+ _trace("block", { reason });
58
+ process.exit(2);
59
+ }
60
+
61
+ // Honor escape hatch first — keeps tests and emergency overrides cheap.
62
+ if (process.env.QUALIA_ALLOW_DESTRUCTIVE === "1") {
63
+ _trace("allow", { reason: "QUALIA_ALLOW_DESTRUCTIVE=1" });
64
+ process.exit(0);
65
+ }
66
+
67
+ // Read hook payload from stdin (Claude Code passes JSON; if no stdin, allow).
68
+ let payload = "";
69
+ try {
70
+ if (!process.stdin.isTTY) payload = fs.readFileSync(0, "utf8");
71
+ } catch {}
72
+
73
+ let command = "";
74
+ try {
75
+ if (payload) command = (JSON.parse(payload).tool_input || {}).command || "";
76
+ } catch {}
77
+
78
+ if (!command) {
79
+ _trace("allow", { reason: "no-command" });
80
+ process.exit(0);
81
+ }
82
+
83
+ // Normalize for matching: collapse whitespace, lowercase the flags.
84
+ // We deliberately match on substrings — chained commands (`x && y`) and
85
+ // quoted arguments still get scanned for the dangerous primitive.
86
+ const cmd = command.replace(/\s+/g, " ").trim();
87
+
88
+ // Helper: detect current git branch (silent on failure).
89
+ function currentBranch() {
90
+ try {
91
+ const r = spawnSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
92
+ encoding: "utf8",
93
+ timeout: 2000,
94
+ shell: process.platform === "win32",
95
+ });
96
+ return (r.stdout || "").trim();
97
+ } catch {
98
+ return "";
99
+ }
100
+ }
101
+
102
+ // ── Rule 1: nuking .git ───────────────────────────────────
103
+ if (/\brm\s+(-[a-z]*r[a-z]*f|-[a-z]*f[a-z]*r)\b[^|;&]*\.git(\b|$|\/)/.test(cmd)) {
104
+ block(
105
+ "rm -rf .git would destroy all repository history",
106
+ "If you really need to reinitialize, do it deliberately — back up first",
107
+ );
108
+ }
109
+
110
+ // ── Rule 2: git clean -fd / -fdx ──────────────────────────
111
+ // `git clean -fd` deletes untracked files and directories with no recovery.
112
+ // We block this because the recovery cost is high and the agent rarely needs
113
+ // it — `git stash` or moving the file is almost always the right answer.
114
+ if (/\bgit\s+clean\s+(-[a-zA-Z]*f[a-zA-Z]*d|-[a-zA-Z]*d[a-zA-Z]*f)/.test(cmd)) {
115
+ block(
116
+ "git clean -fd permanently deletes untracked files",
117
+ "Use `git stash --include-untracked` if you need a clean tree but might want the files later",
118
+ );
119
+ }
120
+
121
+ // ── Rule 3: git branch -D main / master ───────────────────
122
+ if (/\bgit\s+branch\s+-D\s+(main|master)\b/.test(cmd)) {
123
+ block(
124
+ "deleting main/master branch is destructive",
125
+ "If this is intentional, run it manually outside Claude with QUALIA_ALLOW_DESTRUCTIVE=1",
126
+ );
127
+ }
128
+
129
+ // ── Rule 4: git push --force to main / master ─────────────
130
+ // Match `git push ... --force ... main` or `git push ... -f ... main` (and
131
+ // master). Excludes --force-with-lease which is the safe variant.
132
+ const isPush = /\bgit\s+push\b/.test(cmd);
133
+ const hasForce =
134
+ /\s(--force|-f)(\s|$)/.test(" " + cmd + " ") &&
135
+ !/--force-with-lease/.test(cmd);
136
+
137
+ if (isPush && hasForce) {
138
+ // Targets main/master explicitly: `git push origin main`, `git push --force origin main`
139
+ if (/\b(main|master)\b/.test(cmd)) {
140
+ block(
141
+ "force-pushing to main/master rewrites shared history",
142
+ "Use `git push --force-with-lease origin <branch>` on a feature branch instead",
143
+ );
144
+ }
145
+ // Or pushing the current branch when on main
146
+ const branch = currentBranch();
147
+ if (branch === "main" || branch === "master") {
148
+ block(
149
+ `force-pushing while on ${branch} (rewrites shared history)`,
150
+ "Switch to a feature branch first: `git checkout -b feat/<name>`",
151
+ );
152
+ }
153
+ }
154
+
155
+ // ── Rule 5: git reset --hard while on main / master ───────
156
+ if (/\bgit\s+reset\s+--hard\b/.test(cmd)) {
157
+ const branch = currentBranch();
158
+ if (branch === "main" || branch === "master") {
159
+ block(
160
+ `git reset --hard while on ${branch} discards uncommitted work and rewrites local history`,
161
+ "Switch to a feature branch first, or use `git stash` to preserve work",
162
+ );
163
+ }
164
+ }
165
+
166
+ _trace("allow", {});
167
+ process.exit(0);
package/hooks/pre-push.js CHANGED
@@ -74,10 +74,16 @@ function commitStamp() {
74
74
  // --no-verify: skip user pre-commit hooks (this is a bot commit).
75
75
  // --no-gpg-sign: don't pop a signing prompt for a chore commit.
76
76
  // --author: attribute to bot, not user.
77
- const add = git(["add", TRACKING]);
77
+ // -c core.autocrlf=false: without this, Windows installs with autocrlf=true
78
+ // normalize the just-written LF-terminated JSON to CRLF in the index, so the
79
+ // diff against HEAD is empty, the commit fails, and we roll back the stamp.
80
+ // Forcing autocrlf=false for this one add/commit pair preserves the JSON as
81
+ // written and keeps the stamp consistent across platforms.
82
+ const add = git(["-c", "core.autocrlf=false", "add", TRACKING]);
78
83
  if (add.status !== 0) return { skipped: "git-add-failed", error: add.stderr };
79
84
 
80
85
  const commit = git([
86
+ "-c", "core.autocrlf=false",
81
87
  "commit",
82
88
  "--no-verify",
83
89
  "--no-gpg-sign",
@@ -25,6 +25,41 @@ const UI = path.join(HOME, ".claude", "bin", "qualia-ui.js");
25
25
  const STATE_FILE = path.join(".planning", "STATE.md");
26
26
  const CONTINUE_HERE = ".continue-here.md";
27
27
  const NOTIF_FILE = path.join(HOME, ".claude", ".qualia-update-available.json");
28
+ const HEALTH_FILE = path.join(HOME, ".claude", ".qualia-install-health.json");
29
+
30
+ // Critical files referenced by skills via @-import. If any are missing, skills
31
+ // silently get empty context and produce ungrounded output. We spot-check these
32
+ // on session-start and write a cached result (1-per-day) to HEALTH_FILE so we
33
+ // don't stat on every session.
34
+ const CRITICAL_FILES = [
35
+ path.join(HOME, ".claude", "rules", "grounding.md"),
36
+ path.join(HOME, ".claude", "rules", "security.md"),
37
+ path.join(HOME, ".claude", "rules", "frontend.md"),
38
+ path.join(HOME, ".claude", "rules", "deployment.md"),
39
+ path.join(HOME, ".claude", "bin", "state.js"),
40
+ ];
41
+
42
+ function checkInstallHealth() {
43
+ // Returns null if healthy, or an array of missing files if damaged.
44
+ // Caches result in HEALTH_FILE for 24h to avoid repeated stat overhead.
45
+ try {
46
+ if (fs.existsSync(HEALTH_FILE)) {
47
+ const cached = JSON.parse(fs.readFileSync(HEALTH_FILE, "utf8"));
48
+ const age = Date.now() - (cached.checked_at || 0);
49
+ if (age < 24 * 60 * 60 * 1000) {
50
+ return cached.missing && cached.missing.length ? cached.missing : null;
51
+ }
52
+ }
53
+ } catch {}
54
+ const missing = CRITICAL_FILES.filter((f) => !fs.existsSync(f));
55
+ try {
56
+ fs.writeFileSync(
57
+ HEALTH_FILE,
58
+ JSON.stringify({ checked_at: Date.now(), missing }, null, 2),
59
+ );
60
+ } catch {}
61
+ return missing.length ? missing : null;
62
+ }
28
63
 
29
64
  function runUi(...args) {
30
65
  if (!fs.existsSync(UI)) return;
@@ -94,9 +129,25 @@ function maybeRenderUpdateBanner() {
94
129
  } catch {}
95
130
  }
96
131
 
132
+ function renderHealthWarning(missing) {
133
+ // Loud, non-blocking warning when critical install files are missing.
134
+ // Tells the user exactly what to run — never silent.
135
+ const label = missing.map((f) => path.basename(f)).join(", ");
136
+ if (fs.existsSync(UI)) {
137
+ runUi("warn", `Install incomplete — missing: ${label}`);
138
+ runUi("info", "Run: npx qualia-framework@latest install");
139
+ } else {
140
+ console.log(`QUALIA: Install incomplete — missing ${label}`);
141
+ console.log(`QUALIA: Run: npx qualia-framework@latest install`);
142
+ }
143
+ }
144
+
97
145
  try {
98
146
  maybeRenderUpdateBanner();
99
147
 
148
+ const healthMissing = checkInstallHealth();
149
+ if (healthMissing) renderHealthWarning(healthMissing);
150
+
100
151
  if (!fs.existsSync(UI)) {
101
152
  fallbackText();
102
153
  } else if (fs.existsSync(STATE_FILE)) {
@@ -125,8 +176,23 @@ try {
125
176
  console.log(` ${TEAL}/qualia-quick${RESET} ${DIM}Quick fix (skip planning)${RESET}`);
126
177
  console.log("");
127
178
  }
128
- } catch {
129
- // Deliberately silent hook must never fail
179
+ } catch (e) {
180
+ // Hook must never exit non-zero. Log to trace so silent crashes are visible
181
+ // in analytics, but do not print to stderr (would clutter the banner).
182
+ try {
183
+ const traceDir = path.join(os.homedir(), ".claude", ".qualia-traces");
184
+ if (!fs.existsSync(traceDir)) fs.mkdirSync(traceDir, { recursive: true });
185
+ const file = path.join(traceDir, `${new Date().toISOString().split("T")[0]}.jsonl`);
186
+ fs.appendFileSync(
187
+ file,
188
+ JSON.stringify({
189
+ hook: "session-start",
190
+ result: "error",
191
+ error: String(e && e.message ? e.message : e),
192
+ timestamp: new Date().toISOString(),
193
+ }) + "\n",
194
+ );
195
+ } catch {}
130
196
  }
131
197
 
132
198
  function _trace(hookName, result, extra) {