qualia-framework 5.5.0 → 5.9.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 (45) hide show
  1. package/README.md +17 -13
  2. package/agents/plan-checker.md +8 -0
  3. package/agents/qa-browser.md +7 -0
  4. package/agents/research-synthesizer.md +4 -1
  5. package/agents/researcher.md +6 -1
  6. package/agents/roadmapper.md +8 -0
  7. package/agents/verifier.md +14 -1
  8. package/agents/visual-evaluator.md +1 -1
  9. package/bin/cli.js +30 -1
  10. package/bin/erp-retry.js +289 -0
  11. package/bin/install.js +12 -6
  12. package/bin/slop-detect.mjs +1 -1
  13. package/bin/state.js +10 -1
  14. package/docs/onboarding.html +621 -0
  15. package/docs/playwright-loop-pilot-results.md +7 -5
  16. package/docs/research/2026-05-11-deep-research.md +189 -0
  17. package/guide.md +5 -6
  18. package/hooks/session-start.js +19 -1
  19. package/package.json +3 -2
  20. package/rules/speed.md +1 -2
  21. package/skills/qualia-discuss/SKILL.md +106 -6
  22. package/skills/qualia-feature/SKILL.md +216 -0
  23. package/skills/qualia-milestone/SKILL.md +73 -1
  24. package/skills/qualia-new/SKILL.md +52 -25
  25. package/skills/qualia-optimize/SKILL.md +1 -1
  26. package/skills/{qualia-polish-loop → qualia-polish}/REFERENCE.md +5 -5
  27. package/skills/qualia-polish/SKILL.md +13 -4
  28. package/skills/{qualia-polish-loop → qualia-polish}/scripts/loop.mjs +2 -2
  29. package/skills/{qualia-polish-loop → qualia-polish}/scripts/playwright-capture.mjs +1 -1
  30. package/skills/qualia-report/SKILL.md +64 -2
  31. package/skills/qualia-road/SKILL.md +10 -11
  32. package/skills/qualia-verify/SKILL.md +16 -0
  33. package/templates/help.html +2 -3
  34. package/templates/project-discovery.md +83 -0
  35. package/templates/project.md +7 -0
  36. package/tests/bin.test.sh +97 -67
  37. package/tests/refs.test.sh +146 -0
  38. package/tests/slop-detect.test.sh +2 -2
  39. package/skills/qualia-polish-loop/SKILL.md +0 -201
  40. package/skills/qualia-prd/SKILL.md +0 -199
  41. package/skills/qualia-quick/SKILL.md +0 -44
  42. package/skills/qualia-task/SKILL.md +0 -98
  43. /package/skills/{qualia-polish-loop → qualia-polish}/fixtures/broken.html +0 -0
  44. /package/skills/{qualia-polish-loop → qualia-polish}/fixtures/clean.html +0 -0
  45. /package/skills/{qualia-polish-loop → qualia-polish}/scripts/score.mjs +0 -0
package/README.md CHANGED
@@ -1,14 +1,18 @@
1
- # Qualia Framework v5.3
1
+ # Qualia Framework v5.8
2
2
 
3
3
  A harness engineering framework for [Claude Code](https://claude.ai/code). It installs into `~/.claude/` and wraps your AI-assisted development workflow with structured planning, execution, verification, and deployment gates.
4
4
 
5
- It is not an application framework like Rails or Next.js. It doesn't generate code, run servers, or process data. It's an opinionated workflow layer that tells Claude how to plan, build, and verify your projects end-to-end, from "tell me what you want to make" to "here's the handoff doc for your client."
5
+ It is not an application framework like Rails or Next.js. It doesn't generate code, run servers, or process data. It's an opinionated workflow layer that tells Claude how to plan, build, and verify your projects end-to-end, from "tell me what you want to make" to "here's the handoff doc for your client."
6
6
 
7
- **The v5 line in three releases:**
8
- - **v5.0** alignment discipline. CONTEXT.md domain glossary, decisions/ ADRs, `/qualia-zoom`, `/qualia-issues`, `/qualia-triage`, slim CLAUDE.md per Matt Pocock's instruction-budget rule, insights-driven hooks (Vercel account, empty env-var, Supabase destructive guards).
9
- - **v5.1** — `/qualia-polish-loop` (autonomous visual-polish loop: screenshots a URL at three viewports, scores 8 design dimensions with vision, fixes top issues, loops until pass or kill-switch); multi-target installer (Claude Code + Codex AGENTS.md + Both); live-progress install UI.
10
- - **v5.2** polish-loop reliability. `--reduced-motion` capture flag, `--routes URL1,URL2` multi-route mode, first supervised end-to-end run.
11
- - **v5.3** Matt Pocock gaps closed. `/qualia-prd` (synthesize conversation → durable PRD), `/qualia-hook-gen` (CLAUDE.md instruction deterministic Claude Code hook), `/qualia-optimize --deepen` Step 5b parallel-interface design (3 fan-out agents producing radically different interfaces).
7
+ **The v5 line:**
8
+ - **v5.0**, alignment discipline. CONTEXT.md domain glossary, decisions/ ADRs, `/qualia-zoom`, `/qualia-issues`, `/qualia-triage`, slim CLAUDE.md per Matt Pocock's instruction-budget rule, insights-driven hooks.
9
+ - **v5.1**, autonomous visual-polish loop. Screenshots a URL at three viewports, scores 8 design dimensions with vision, fixes top issues, loops until pass or kill-switch. Multi-target installer (Claude Code + Codex AGENTS.md + Both).
10
+ - **v5.2**, polish-loop reliability. `--reduced-motion` capture flag, `--routes URL1,URL2` multi-route mode, first supervised end-to-end run.
11
+ - **v5.3**, Matt Pocock gaps closed. `/qualia-hook-gen` (CLAUDE.md instruction to deterministic Claude Code hook), `/qualia-optimize --deepen` Step 5b parallel-interface design (3 fan-out agents producing radically different interfaces).
12
+ - **v5.4-5.5**, token-discipline and plan-discipline. Cache-aware spawn ordering, scope-reduction prohibition, decision-coverage audit, requirement-coverage check.
13
+ - **v5.6**, Demo vs Full Project gate at kickoff. Mandatory discovery interview via `/qualia-discuss` in PROJECT MODE (8 questions for demos, 14 for full projects). Demo-extension branch in `/qualia-milestone` for client-signs-after-demo conversion.
14
+ - **v5.7**, `/qualia-feature` consolidates `/qualia-quick` + `/qualia-task` into one auto-scoped command.
15
+ - **v5.8**, surface cleanup. `/qualia-polish --loop` replaces `/qualia-polish-loop`. `/qualia-quick`, `/qualia-task`, and `/qualia-prd` removed (deprecated in v5.7).
12
16
 
13
17
  The Full Journey architecture carries forward: `/qualia-new` maps the entire project arc from kickoff to client handoff upfront, and the Road chains end-to-end in `--auto` mode with only two human gates per project.
14
18
 
@@ -40,6 +44,8 @@ npx qualia-framework@latest traces # View recent hook telemetry
40
44
 
41
45
  Open Claude Code in any project directory.
42
46
 
47
+ > **New to Qualia?** Open [`docs/onboarding.html`](docs/onboarding.html) in a browser for a one-page roadmap of the golden path. Best file to send a new hire.
48
+
43
49
  ### The Road — guided mode (default)
44
50
 
45
51
  ```
@@ -89,15 +95,13 @@ Two human gates per project. One halt case (gap-cycle limit exceeded on a failin
89
95
  /qualia-debug # Structured debugging
90
96
  /qualia-review # Production audit (scored diagnostics)
91
97
  /qualia-optimize # Deep optimization pass (parallel specialist agents, --deepen mode with parallel-interface design)
92
- /qualia-quick # Fast path for trivial fixes (skips planning)
93
- /qualia-task # Build one thing properly (fresh builder, atomic commit, no phase plan)
98
+ /qualia-feature # Auto-scoped single-feature build (inline for trivia, fresh spawn for 1-5 files)
94
99
  /qualia-test # Generate or run tests (--tdd mode for test-first workflow)
95
100
  /qualia-zoom # Focus on a single file or function with full context
96
101
  /qualia-issues # Break a phase plan into vertical-slice GitHub issues
97
102
  /qualia-triage # Triage open issues through the ready-for-agent state machine
98
103
  /qualia-road # View and navigate the project road (journey/milestone/phase status)
99
- /qualia-polish-loop # Autonomous visual-polish loop: screenshot vision-eval fix repeat (v5.1+)
100
- /qualia-prd # Synthesize current conversation into a durable feature spec (v5.3+)
104
+ /qualia-polish --loop # Autonomous visual-polish loop: screenshot, vision-eval, fix, repeat
101
105
  /qualia-hook-gen # Convert a CLAUDE.md/rules instruction into a deterministic hook (v5.3+)
102
106
  ```
103
107
 
@@ -139,9 +143,9 @@ Project
139
143
 
140
144
  **Why it matters:** non-technical team members can follow the ladder from any entry point. `/qualia` and `/qualia-milestone` render JOURNEY.md as a visual ladder with current position highlighted. In the ERP, the primary operational dates are project deadline, milestone deadline, and employee shift submission date; framework tasks stay internal to agent execution.
141
145
 
142
- ## What's Inside (v5.3.0)
146
+ ## What's Inside (v5.8.0)
143
147
 
144
- - **35 skills** full Road (new / plan / build / verify / milestone / polish / ship / handoff / report), depth (discuss, research, map), navigation (qualia router, idk, pause, resume, road, help), quality (debug, review, optimize with `--deepen` parallel-interface design, quick, task, test, zoom, issues, triage), v5 flagships (`qualia-polish-loop`, `qualia-prd`, `qualia-hook-gen`), and meta (learn, skill-new, flush, postmortem)
148
+ - **32 skills**, full Road (new / plan / build / verify / milestone / polish / ship / handoff / report), depth (discuss, research, map), navigation (qualia router, idk, pause, resume, road, help), quality (debug, review, optimize with `--deepen` parallel-interface design, feature, test, zoom, issues, triage), v5 flagships (`qualia-polish --loop`, `qualia-hook-gen`), and meta (learn, skill-new, flush, postmortem)
145
149
  - **9 agents** (each runs in fresh context): planner, builder, verifier, qa-browser, researcher, research-synthesizer, roadmapper, plan-checker, visual-evaluator
146
150
  - **12 hooks** (pure Node.js, cross-platform): session-start, auto-update, git-guardrails, branch-guard, pre-push tracking sync, migration-guard, pre-deploy-gate, pre-compact state save, stop-session-log, vercel-account-guard, env-empty-guard, supabase-destructive-guard
147
151
  - **6 always-loaded rules** (`rules/`): grounding, security, infrastructure, deployment, speed (CLI-first / MCP tier-list), architecture (deep modules / scout-for-shallow-code)
@@ -2,8 +2,16 @@
2
2
  name: qualia-plan-checker
3
3
  description: Validates a phase plan before execution. Checks task specificity, wave assignment, verification contracts, and coverage of success criteria. Spawned by qualia-plan in a revision loop (max 2 iterations).
4
4
  tools: Read, Bash, Grep
5
+ model: sonnet
5
6
  ---
6
7
 
8
+ <!-- v5.9: Sonnet, not Opus. The checker runs an 11-rule checklist against the
9
+ plan — every rule is a deterministic match (task has a Why?, AC is
10
+ observable?, wave assignment correct?). Structured validation, not plan
11
+ synthesis. Plan WRITING is on Opus (agents/planner.md); plan CHECKING is
12
+ on Sonnet because it's pattern-matching. -->
13
+
14
+
7
15
  # Plan Checker
8
16
 
9
17
  You validate phase plans before they go to the builder. You do NOT write plans — you evaluate them. If a plan has issues, return a structured list; the planner will revise and you'll check again (max 2 revision cycles).
@@ -2,8 +2,15 @@
2
2
  name: qualia-qa-browser
3
3
  description: Real-browser QA. Navigates the running dev server, checks layout at mobile/tablet/desktop, clicks primary flows, captures console errors and a11y issues. Spawned by /qualia-verify on phases with frontend work.
4
4
  tools: Read, Bash, Grep, Glob
5
+ model: sonnet
5
6
  ---
6
7
 
8
+ <!-- v5.9: Sonnet, not Opus. QA-browser drives the browser through scripted
9
+ flows and reports console + a11y findings. Mechanical interaction +
10
+ finding-collection, not architectural reasoning. Vision interpretation
11
+ for design quality lives in visual-evaluator.md, which stays on Opus. -->
12
+
13
+
7
14
  # Qualia QA Browser
8
15
 
9
16
  You verify that the **running app actually looks and behaves right** — not just that the code compiles and greps clean. Fresh context, no memory of what was built.
@@ -33,6 +33,7 @@ You receive:
33
33
  - `.planning/research/ARCHITECTURE.md`
34
34
  - `.planning/research/PITFALLS.md`
35
35
  - Project context (PROJECT.md summary)
36
+ - `<scope>` — optional: `quick` for demo projects, `standard` (default) for full projects
36
37
 
37
38
  ## Output
38
39
 
@@ -72,7 +73,9 @@ Based on:
72
73
  - ARCHITECTURE.md build order → what depends on what, which foundation must land in Milestone 1 to support final-milestone requirements
73
74
  - PITFALLS.md → which risks stall later milestones and need to be addressed in Milestone 1 foundations
74
75
 
75
- Suggest a **2-5 milestone arc ending in Handoff**:
76
+ **Quick scope (`<scope>quick</scope>`, demo projects):** Suggest a **single milestone** (no Handoff, no multi-milestone arc). The milestone is the demo itself — 2 to 4 phases that ship a real working surface end-to-end. Skip the "Handoff implications" section. The demo extends into a full project later via `/qualia-milestone` if the client signs; that conversion is handled there, not here.
77
+
78
+ **Standard scope (default):** Suggest a **2-5 milestone arc ending in Handoff**:
76
79
 
77
80
  - **Milestone 1 · Foundation** — almost always. DB, auth, base layout, deploy pipeline.
78
81
  - **Milestone 2-{N-1} · Core + Expansion** — the value-delivering capabilities, ordered by dependency.
@@ -25,11 +25,16 @@ You receive from the orchestrator:
25
25
  - `<domain>` — the project domain (e.g., "legal case management", "dental clinic booking", "voice agent for restaurants")
26
26
  - `<project_context>` — summary of PROJECT.md (core value, constraints, what they're building)
27
27
  - `<milestone_context>` — greenfield or subsequent
28
+ - `<scope>` — optional: `quick` for demo projects, `standard` (default) for full projects
28
29
  - `<output_path>` — absolute path where you write your research file
29
30
 
30
31
  ## Tool Budget
31
32
 
32
- Maximum 8 external calls total per invocation: 3 Context7 queries + 3 WebFetch calls + 2 WebSearch queries. If you exhaust this budget, write what you have and mark remaining sections as `confidence: LOW`. Research is time-boxed, not exhaustive — a 10-minute deep dive with concrete sources beats a 30-minute wander.
33
+ **Standard scope (default):** Maximum 8 external calls total per invocation 3 Context7 queries + 3 WebFetch calls + 2 WebSearch queries.
34
+
35
+ **Quick scope (`<scope>quick</scope>`, used by demo projects):** Maximum 3 external calls total — 1 Context7 query + 1 WebFetch + 1 WebSearch. The demo only needs enough research to validate the stack and surface the top pitfall — depth is wasted when there's a single milestone to ship. Drain local sources first (Steps 0a + 0b below); if local sources cover the dimension, skip external calls entirely.
36
+
37
+ If you exhaust the budget, write what you have and mark remaining sections as `confidence: LOW`. Research is time-boxed, not exhaustive — a 10-minute deep dive with concrete sources beats a 30-minute wander.
33
38
 
34
39
  **Local-first.** Before any external call, exhaust local sources (Steps 0a + 0b in *How to Research* below). Most domains have already been researched and the answers live in NotebookLM notebooks or `~/qualia-memory`. Hitting the web for content we already have is silent token waste — and the local source is usually higher-quality (curated synthesis vs raw search results).
35
40
 
@@ -2,8 +2,16 @@
2
2
  name: qualia-roadmapper
3
3
  description: Creates JOURNEY.md (full multi-milestone arc), REQUIREMENTS.md (multi-milestone, REQ-IDs), and ROADMAP.md (current milestone's phase detail) from PROJECT.md and research. Spawned by qualia-new after research completes.
4
4
  tools: Read, Write, Bash
5
+ model: sonnet
5
6
  ---
6
7
 
8
+ <!-- v5.9: Sonnet, not Opus. The roadmapper fills mostly-deterministic templates
9
+ (JOURNEY.md, REQUIREMENTS.md, ROADMAP.md) from PROJECT.md + research
10
+ synthesis. Project-specific shape, but the milestone-decomposition logic
11
+ is bounded and structured — not novel synthesis. Builder and planner stay
12
+ on Opus where real architectural reasoning lives. -->
13
+
14
+
7
15
  # Qualia Roadmapper
8
16
 
9
17
  You produce the **full project journey** — every milestone from kickoff to handoff. This is the North Star for the rest of the project. Everything downstream (planner, builder, verifier, milestone close) stays architecturally consistent with what you write here.
@@ -2,8 +2,17 @@
2
2
  name: qualia-verifier
3
3
  description: Goal-backward verification. Checks if the phase ACTUALLY works, not just if tasks ran.
4
4
  tools: Read, Bash, Grep, Glob
5
+ model: sonnet
5
6
  ---
6
7
 
8
+ <!-- v5.9: Sonnet, not Opus. The verifier executes a deterministic protocol —
9
+ run greps against acceptance criteria, score the 8-dim design rubric, walk
10
+ stub-detection patterns. Pattern-matching + structured output, not novel
11
+ architectural reasoning. Opus is overkill; the inherited-Opus default cost
12
+ ~3x what Sonnet does without measurably better verdicts.
13
+ Builder and planner stay on Opus (they do real synthesis). -->
14
+
15
+
7
16
  # Qualia Verifier
8
17
 
9
18
  You verify that a phase achieved its GOAL, not just completed its TASKS.
@@ -44,7 +53,11 @@ Write `.planning/phase-{N}-verification.md` — PASS or FAIL with evidence. Appl
44
53
 
45
54
  ## Tool Budget
46
55
 
47
- Maximum 25 Bash/Grep calls per invocation. Prefer one multi-pattern grep over many single-pattern greps. If you exhaust the budget, write what you found and mark unchecked criteria as `INSUFFICIENT EVIDENCE` — do not fabricate.
56
+ Budget scales with phase size: **`max(25, task_count * 5)` Bash/Grep calls** per invocation. The plan file's `## Task N` count determines `task_count` a 3-task phase gets 25 calls, a 10-task phase gets 50.
57
+
58
+ Prefer one multi-pattern grep over many single-pattern greps. If you exhaust the budget, write what you found and mark unchecked criteria as `INSUFFICIENT EVIDENCE` — do not fabricate.
59
+
60
+ **INSUFFICIENT EVIDENCE is a hard FAIL signal.** The orchestrator (`/qualia-verify`) treats any `INSUFFICIENT EVIDENCE` line in the verification file as a phase FAIL, not silent PASS. Don't use it as a pass-through — only when budget genuinely exhausted before a criterion could be checked.
48
61
 
49
62
  ## Goal-Backward Verification
50
63
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: qualia-visual-evaluator
3
- description: Vision-anchored evaluator for /qualia-polish-loop. Reads screenshots, scores 8 design dimensions against the rubric with cited evidence, returns top 3 issues + severity. Default: 3 (acceptable). Only deviates with quoted evidence.
3
+ description: Vision-anchored evaluator for /qualia-polish --loop. Reads screenshots, scores 8 design dimensions against the rubric with cited evidence, returns top 3 issues + severity. Default: 3 (acceptable). Only deviates with quoted evidence.
4
4
  tools: Read, Grep, Glob
5
5
  ---
6
6
 
package/bin/cli.js CHANGED
@@ -160,7 +160,7 @@ const QUALIA_AGENT_FILES = [
160
160
  ];
161
161
 
162
162
  // 3 Qualia bin scripts.
163
- const QUALIA_BIN_FILES = ["state.js", "qualia-ui.js", "statusline.js", "knowledge.js", "knowledge-flush.js", "plan-contract.js", "agent-runs.js", "slop-detect.mjs"];
163
+ const QUALIA_BIN_FILES = ["state.js", "qualia-ui.js", "statusline.js", "knowledge.js", "knowledge-flush.js", "plan-contract.js", "agent-runs.js", "slop-detect.mjs", "erp-retry.js"];
164
164
 
165
165
  // Qualia rules — security, deployment, infra, grounding, plus the v4.5.0 design substrate.
166
166
  // frontend.md and design-reference.md are kept for backward compat; new projects use design-laws/brand/product/rubric.
@@ -1035,6 +1035,29 @@ function cmdSetErpKey() {
1035
1035
  // ─── Flush: convenience wrapper around knowledge-flush.js ───────
1036
1036
  // Exposes the cron-runnable script as a top-level CLI command so users can
1037
1037
  // run `qualia-framework flush` ad-hoc. All args after the command pass through.
1038
+ // ─── erp-flush: drain the ERP retry queue verbosely ───────────
1039
+ // Thin wrapper around bin/erp-retry.js. Used when an employee wants to
1040
+ // retry stranded reports on demand (e.g., after the ERP came back online,
1041
+ // or after rotating the API key). All args pass through.
1042
+ function cmdErpFlush() {
1043
+ const retryScript = path.join(CLAUDE_DIR, "bin", "erp-retry.js");
1044
+ if (!fs.existsSync(retryScript)) {
1045
+ console.log(` ${RED}✗${RESET} erp-retry.js not installed at ${retryScript}`);
1046
+ console.log(` ${DIM}Run: npx qualia-framework@latest install${RESET}`);
1047
+ process.exit(1);
1048
+ }
1049
+ banner();
1050
+ console.log("");
1051
+ const args = process.argv.slice(3);
1052
+ // Default: drain action. Allow `qualia-framework erp-flush show` / `clear` too.
1053
+ const action = (args[0] && !args[0].startsWith("--")) ? args.shift() : "drain";
1054
+ const r = spawnSync(process.execPath, [retryScript, action, ...args], {
1055
+ stdio: "inherit",
1056
+ shell: false,
1057
+ });
1058
+ process.exit(r.status || 0);
1059
+ }
1060
+
1038
1061
  function cmdFlush() {
1039
1062
  const flushScript = path.join(CLAUDE_DIR, "bin", "knowledge-flush.js");
1040
1063
  if (!fs.existsSync(flushScript)) {
@@ -1073,6 +1096,7 @@ function cmdDoctor() {
1073
1096
  path.join(CLAUDE_DIR, "bin", "statusline.js"),
1074
1097
  path.join(CLAUDE_DIR, "bin", "knowledge.js"),
1075
1098
  path.join(CLAUDE_DIR, "bin", "knowledge-flush.js"),
1099
+ path.join(CLAUDE_DIR, "bin", "erp-retry.js"),
1076
1100
  path.join(CLAUDE_DIR, "CLAUDE.md"),
1077
1101
  CONFIG_FILE,
1078
1102
  ];
@@ -1247,6 +1271,7 @@ function cmdHelp() {
1247
1271
  console.log(` qualia-framework ${TEAL}agents${RESET} Show per-agent run history (${DIM}--failed|--task ID|--phase N|prune --before${RESET})`);
1248
1272
  console.log(` qualia-framework ${TEAL}set-erp-key${RESET} Save/enable the ERP API key`);
1249
1273
  console.log(` qualia-framework ${TEAL}erp-ping${RESET} Verify ERP connectivity + API key`);
1274
+ console.log(` qualia-framework ${TEAL}erp-flush${RESET} Retry queued ERP report uploads (${DIM}show|clear${RESET})`);
1250
1275
  console.log(` qualia-framework ${TEAL}doctor${RESET} Health-check the install (files, hooks, settings)`);
1251
1276
  console.log(` qualia-framework ${TEAL}flush${RESET} Promote daily-log → curated knowledge (memory layer)`);
1252
1277
  console.log("");
@@ -1312,6 +1337,10 @@ switch (cmd) {
1312
1337
  case "ping":
1313
1338
  cmdErpPing();
1314
1339
  break;
1340
+ case "erp-flush":
1341
+ case "erp-retry":
1342
+ cmdErpFlush();
1343
+ break;
1315
1344
  case "doctor":
1316
1345
  case "health":
1317
1346
  case "health-check":
@@ -0,0 +1,289 @@
1
+ #!/usr/bin/env node
2
+ // ~/.claude/bin/erp-retry.js
3
+ //
4
+ // ERP report retry queue. /qualia-report enqueues a report here if the
5
+ // inline 3-attempt-with-backoff upload fails. session-start.js drains the
6
+ // queue quietly on the next Claude Code launch; `qualia-framework erp-flush`
7
+ // drains it verbosely on demand.
8
+ //
9
+ // Why a queue: the prior v5.8 implementation told the user "$REPORT will
10
+ // appear in ERP after retry" but had no retry mechanism — data was stranded
11
+ // locally until the employee manually re-ran /qualia-report. Found by the
12
+ // 2026-05-11 deep-research audit. This module makes the promise real.
13
+ //
14
+ // Idempotency: every enqueued item carries the Idempotency-Key header that
15
+ // the original report attempt used. The ERP UPSERTs on
16
+ // (project_id, client_report_id) and deduplicates Idempotency-Key for 24h,
17
+ // so retry-of-a-just-succeeded item is safe.
18
+ //
19
+ // Hard rules:
20
+ // - Never throw out the queue file on parse error — back it up and start fresh.
21
+ // - Max 10 attempts per item before marking give_up=true (stops the cycle).
22
+ // - 401/422 are permanent failures: keep the item but mark give_up=true so
23
+ // the user can see it and resolve manually (typically: fix the API key).
24
+ // - The CLI invocation NEVER exits non-zero unless the queue file is unreadable —
25
+ // we don't want session-start.js to surface "hook error" red banners.
26
+ //
27
+ // Usage:
28
+ // node erp-retry.js # drain all (with default 3-attempt budget per item)
29
+ // node erp-retry.js --quiet # no stdout unless errors
30
+ // node erp-retry.js --max=3 # drain at most 3 items this run
31
+ // node erp-retry.js --timeout=3000 # ms timeout per upload attempt
32
+ // node erp-retry.js show # print queue contents, drain nothing
33
+ // node erp-retry.js clear # delete the queue (use after manual fix)
34
+
35
+ const fs = require("fs");
36
+ const path = require("path");
37
+ const os = require("os");
38
+ const https = require("https");
39
+ const http = require("http");
40
+ const urlLib = require("url");
41
+
42
+ const HOME = os.homedir();
43
+ const QUEUE_FILE = path.join(HOME, ".claude", ".erp-retry-queue.json");
44
+ const API_KEY_FILE = path.join(HOME, ".claude", ".erp-api-key");
45
+ const CONFIG_FILE = path.join(HOME, ".claude", ".qualia-config.json");
46
+
47
+ const MAX_GIVE_UP_ATTEMPTS = 10;
48
+ const DEFAULT_TIMEOUT_MS = 5000;
49
+ const DEFAULT_MAX_ITEMS = 100;
50
+
51
+ // ─── Args ───────────────────────────────────────────────
52
+ const args = process.argv.slice(2);
53
+ const ACTION = (args[0] && !args[0].startsWith("--")) ? args[0] : "drain";
54
+ const QUIET = args.includes("--quiet");
55
+ function flag(name, fallback) {
56
+ const found = args.find((a) => a.startsWith(`--${name}=`));
57
+ if (!found) return fallback;
58
+ const v = found.split("=", 2)[1];
59
+ const n = Number(v);
60
+ return Number.isFinite(n) ? n : fallback;
61
+ }
62
+ const MAX_ITEMS = flag("max", DEFAULT_MAX_ITEMS);
63
+ const TIMEOUT_MS = flag("timeout", DEFAULT_TIMEOUT_MS);
64
+
65
+ function log(msg) { if (!QUIET) process.stdout.write(msg + "\n"); }
66
+ function logErr(msg) { process.stderr.write(msg + "\n"); }
67
+
68
+ // ─── Queue I/O ──────────────────────────────────────────
69
+ function readQueue() {
70
+ if (!fs.existsSync(QUEUE_FILE)) return { queue: [] };
71
+ try {
72
+ const raw = fs.readFileSync(QUEUE_FILE, "utf8");
73
+ const parsed = JSON.parse(raw);
74
+ if (!parsed || !Array.isArray(parsed.queue)) return { queue: [] };
75
+ return parsed;
76
+ } catch (e) {
77
+ // Corrupt queue — back up and start fresh. Never silently destroy data.
78
+ const bak = `${QUEUE_FILE}.bak.${new Date().toISOString().replace(/[:.]/g, "-")}`;
79
+ try { fs.copyFileSync(QUEUE_FILE, bak); } catch {}
80
+ logErr(`erp-retry: queue file unparseable; backed up to ${bak} and starting fresh`);
81
+ return { queue: [] };
82
+ }
83
+ }
84
+
85
+ function writeQueue(data) {
86
+ const dir = path.dirname(QUEUE_FILE);
87
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
88
+ // Atomic: write tmp, rename. Mode 0600 because the payload contains
89
+ // session notes that may reference internal project state.
90
+ const tmp = `${QUEUE_FILE}.tmp.${process.pid}`;
91
+ fs.writeFileSync(tmp, JSON.stringify(data, null, 2) + "\n", { mode: 0o600 });
92
+ try { fs.chmodSync(tmp, 0o600); } catch {}
93
+ fs.renameSync(tmp, QUEUE_FILE);
94
+ }
95
+
96
+ function enqueue({ client_report_id, idempotency_key, url, payload, last_error }) {
97
+ if (!client_report_id || !url || !payload) {
98
+ throw new Error("enqueue: client_report_id, url, payload are required");
99
+ }
100
+ const data = readQueue();
101
+ // Dedupe — if this report id is already queued, update the existing item
102
+ // instead of appending a duplicate.
103
+ const existing = data.queue.find((it) => it.client_report_id === client_report_id);
104
+ if (existing) {
105
+ existing.idempotency_key = idempotency_key || existing.idempotency_key;
106
+ existing.url = url;
107
+ existing.payload = payload;
108
+ existing.last_error = last_error || existing.last_error || "";
109
+ existing.attempts = existing.attempts || 0;
110
+ existing.give_up = false; // unblock a retry if user fixed the underlying issue
111
+ existing.enqueued_at = new Date().toISOString();
112
+ } else {
113
+ data.queue.push({
114
+ client_report_id,
115
+ idempotency_key: idempotency_key || "",
116
+ url,
117
+ payload,
118
+ enqueued_at: new Date().toISOString(),
119
+ attempts: 0,
120
+ last_error: last_error || "",
121
+ give_up: false,
122
+ });
123
+ }
124
+ writeQueue(data);
125
+ }
126
+
127
+ // ─── HTTP upload (native https — no curl bearer leak via /proc) ──
128
+ function postOnce(item, apiKey) {
129
+ return new Promise((resolve) => {
130
+ let u;
131
+ try { u = urlLib.parse(item.url); } catch {
132
+ return resolve({ code: "—", body: "", error: "invalid url" });
133
+ }
134
+ const lib = u.protocol === "https:" ? https : http;
135
+ const headers = {
136
+ "Authorization": `Bearer ${apiKey}`,
137
+ "Content-Type": "application/json",
138
+ "Content-Length": Buffer.byteLength(item.payload),
139
+ };
140
+ if (item.idempotency_key) headers["Idempotency-Key"] = item.idempotency_key;
141
+ const req = lib.request({
142
+ method: "POST",
143
+ hostname: u.hostname,
144
+ port: u.port || (u.protocol === "https:" ? 443 : 80),
145
+ path: u.path,
146
+ headers,
147
+ timeout: TIMEOUT_MS,
148
+ }, (res) => {
149
+ let chunks = "";
150
+ res.setEncoding("utf8");
151
+ res.on("data", (c) => { chunks += c; });
152
+ res.on("end", () => resolve({ code: String(res.statusCode), body: chunks.trim(), error: null }));
153
+ });
154
+ req.on("error", (e) => resolve({ code: "—", body: "", error: e.message || "request failed" }));
155
+ req.on("timeout", () => { try { req.destroy(new Error("timeout")); } catch {} });
156
+ req.write(item.payload);
157
+ req.end();
158
+ });
159
+ }
160
+
161
+ function readApiKey() {
162
+ try { return fs.readFileSync(API_KEY_FILE, "utf8").trim(); } catch { return ""; }
163
+ }
164
+
165
+ function readConfig() {
166
+ try { return JSON.parse(fs.readFileSync(CONFIG_FILE, "utf8")); } catch { return {}; }
167
+ }
168
+
169
+ // ─── Actions ────────────────────────────────────────────
170
+ async function actionDrain() {
171
+ const data = readQueue();
172
+ if (data.queue.length === 0) {
173
+ log("erp-retry: queue empty");
174
+ return { drained: 0, kept: 0, give_up: 0 };
175
+ }
176
+
177
+ const cfg = readConfig();
178
+ const erpEnabled = !cfg.erp || cfg.erp.enabled !== false;
179
+ if (!erpEnabled) {
180
+ log("erp-retry: ERP disabled in config; skipping drain (queue preserved)");
181
+ return { drained: 0, kept: data.queue.length, give_up: 0 };
182
+ }
183
+
184
+ const apiKey = readApiKey();
185
+ if (!apiKey) {
186
+ log("erp-retry: API key missing; skipping drain (queue preserved)");
187
+ return { drained: 0, kept: data.queue.length, give_up: 0 };
188
+ }
189
+
190
+ let drained = 0;
191
+ let give_up = 0;
192
+ const remaining = [];
193
+ let processed = 0;
194
+
195
+ for (const item of data.queue) {
196
+ // Already given up — keep but don't try.
197
+ if (item.give_up) {
198
+ remaining.push(item);
199
+ continue;
200
+ }
201
+ // Respect the per-run cap.
202
+ if (processed >= MAX_ITEMS) {
203
+ remaining.push(item);
204
+ continue;
205
+ }
206
+ processed++;
207
+
208
+ const result = await postOnce(item, apiKey);
209
+
210
+ if (result.code === "200") {
211
+ drained++;
212
+ log(` ✓ uploaded ${item.client_report_id}`);
213
+ continue; // omit from remaining
214
+ }
215
+
216
+ item.attempts = (item.attempts || 0) + 1;
217
+ item.last_error = result.error
218
+ ? `network: ${result.error}`
219
+ : `HTTP ${result.code}${result.body ? `: ${result.body.slice(0, 200)}` : ""}`;
220
+
221
+ if (result.code === "401" || result.code === "422") {
222
+ // Permanent — surface to user.
223
+ item.give_up = true;
224
+ give_up++;
225
+ log(` ✗ ${item.client_report_id} permanent fail (HTTP ${result.code}) — leaving in queue for manual review`);
226
+ } else if (item.attempts >= MAX_GIVE_UP_ATTEMPTS) {
227
+ item.give_up = true;
228
+ give_up++;
229
+ log(` ✗ ${item.client_report_id} gave up after ${item.attempts} attempts (last: ${item.last_error})`);
230
+ } else {
231
+ log(` · ${item.client_report_id} retry pending (attempt ${item.attempts}, last: ${item.last_error})`);
232
+ }
233
+ remaining.push(item);
234
+ }
235
+
236
+ writeQueue({ queue: remaining });
237
+
238
+ if (drained > 0 || give_up > 0 || !QUIET) {
239
+ log(`erp-retry: drained=${drained} kept=${remaining.length} give_up=${give_up}`);
240
+ }
241
+ return { drained, kept: remaining.length, give_up };
242
+ }
243
+
244
+ function actionShow() {
245
+ const data = readQueue();
246
+ if (data.queue.length === 0) {
247
+ log("queue empty");
248
+ return;
249
+ }
250
+ log(`${data.queue.length} item(s) in queue:`);
251
+ for (const item of data.queue) {
252
+ log(` ${item.client_report_id} enqueued=${item.enqueued_at} attempts=${item.attempts || 0}${item.give_up ? " GIVE_UP" : ""}`);
253
+ if (item.last_error) log(` last_error: ${item.last_error}`);
254
+ }
255
+ }
256
+
257
+ function actionClear() {
258
+ if (!fs.existsSync(QUEUE_FILE)) {
259
+ log("queue already absent");
260
+ return;
261
+ }
262
+ // Back up rather than rm — destructive ops on user data deserve a recovery point.
263
+ const bak = `${QUEUE_FILE}.bak.${new Date().toISOString().replace(/[:.]/g, "-")}`;
264
+ try { fs.copyFileSync(QUEUE_FILE, bak); } catch {}
265
+ try { fs.unlinkSync(QUEUE_FILE); } catch {}
266
+ log(`queue cleared (backup at ${bak})`);
267
+ }
268
+
269
+ // ─── Export for in-process use (qualia-report skill enqueues directly) ──
270
+ module.exports = { enqueue, readQueue, writeQueue };
271
+
272
+ // ─── CLI entrypoint ─────────────────────────────────────
273
+ if (require.main === module) {
274
+ (async () => {
275
+ try {
276
+ if (ACTION === "drain") await actionDrain();
277
+ else if (ACTION === "show") actionShow();
278
+ else if (ACTION === "clear") actionClear();
279
+ else {
280
+ logErr(`erp-retry: unknown action "${ACTION}". Use: drain | show | clear`);
281
+ process.exit(2);
282
+ }
283
+ } catch (e) {
284
+ logErr(`erp-retry: ${e && e.message ? e.message : e}`);
285
+ // Soft-fail so session-start.js never blocks.
286
+ process.exit(0);
287
+ }
288
+ })();
289
+ }
package/bin/install.js CHANGED
@@ -390,10 +390,10 @@ async function main() {
390
390
  if (fs.existsSync(refSrc)) {
391
391
  copy(refSrc, path.join(CLAUDE_DIR, "skills", skill, "REFERENCE.md"));
392
392
  }
393
- // v5.1: Copy scripts/ subfolder if present (e.g. qualia-polish-loop ships
394
- // playwright-capture.mjs, loop.mjs, score.mjs that the skill invokes at
395
- // runtime). Recursive preserves nested files. fixtures/ also copied
396
- // for self-test scenarios.
393
+ // v5.1: Copy scripts/ subfolder if present (e.g. qualia-polish ships
394
+ // playwright-capture.mjs, loop.mjs, score.mjs that the --loop mode
395
+ // invokes at runtime). Recursive, preserves nested files. fixtures/
396
+ // also copied for self-test scenarios.
397
397
  for (const sub of ["scripts", "fixtures"]) {
398
398
  const subSrc = path.join(skillsDir, skill, sub);
399
399
  if (fs.existsSync(subSrc) && fs.statSync(subSrc).isDirectory()) {
@@ -665,6 +665,12 @@ async function main() {
665
665
  );
666
666
  fs.chmodSync(path.join(binDest, "slop-detect.mjs"), 0o755);
667
667
  ok("slop-detect.mjs (anti-pattern scanner — runs pre-commit on frontend builds)");
668
+ copy(
669
+ path.join(FRAMEWORK_DIR, "bin", "erp-retry.js"),
670
+ path.join(binDest, "erp-retry.js")
671
+ );
672
+ fs.chmodSync(path.join(binDest, "erp-retry.js"), 0o755);
673
+ ok("erp-retry.js (ERP report retry queue — drained by session-start hook and erp-flush CLI)");
668
674
  } catch (e) {
669
675
  warn(`scripts — ${e.message}`);
670
676
  }
@@ -893,7 +899,7 @@ Client-specific preferences, design choices, and requirements. Loaded by \`/qual
893
899
  excludeDefault: true,
894
900
  tips: [
895
901
  "⬢ Lost? Type /qualia for the next step",
896
- "⬢ Small fix? Use /qualia-quick to skip planning",
902
+ "⬢ Single feature? Use /qualia-feature, it auto-scopes",
897
903
  "⬢ End of day? /qualia-report submits your shift before clock-out",
898
904
  "⬢ Context isolation: every task gets a fresh AI brain",
899
905
  "⬢ The verifier doesn't trust claims — it greps the code",
@@ -1104,7 +1110,7 @@ function printSummary({ member, target, claudeInstalled }) {
1104
1110
  }
1105
1111
  console.log("");
1106
1112
  console.log(` ${DIM}New project?${RESET} ${TEAL}/qualia-new${RESET}`);
1107
- console.log(` ${DIM}Quick fix?${RESET} ${TEAL}/qualia-quick${RESET}`);
1113
+ console.log(` ${DIM}Single feature?${RESET} ${TEAL}/qualia-feature${RESET}`);
1108
1114
  console.log(` ${DIM}End of day?${RESET} ${TEAL}/qualia-report${RESET} ${DIM}(shift submission)${RESET}`);
1109
1115
  console.log(` ${DIM}Stuck?${RESET} ${TEAL}/qualia${RESET}`);
1110
1116
  console.log("");
@@ -187,7 +187,7 @@ const SKIP_DIRS = new Set([
187
187
  "coverage", ".cache", "out", ".vercel", ".vscode", ".idea",
188
188
  ".planning", ".qa-screenshots",
189
189
  // v5.1: skip test fixtures by convention. Fixtures used as regression
190
- // targets (e.g. /qualia-polish-loop's broken.html) intentionally violate
190
+ // targets (e.g. /qualia-polish --loop's broken.html) intentionally violate
191
191
  // the rules slop-detect enforces; scanning them flags real fixture bugs
192
192
  // as production slop.
193
193
  "fixtures", "__fixtures__",
package/bin/state.js CHANGED
@@ -329,7 +329,16 @@ function parseStateMd(content) {
329
329
 
330
330
  // ─── STATE.md Writer ─────────────────────────────────────
331
331
  function writeStateMd(s) {
332
- const phaseFrac = Math.round(((s.phase - 1) / s.total_phases) * 100);
332
+ // Completed phases count toward progress. A phase counts as "done" once the
333
+ // state has advanced past `built` (i.e. status in verified/polished/shipped/handed_off).
334
+ // Without this adjustment, a 3-phase project shows 66% at completion and a
335
+ // 1-phase project shows 0% — the bar would never reach 100%.
336
+ const DONE_STATUSES = new Set(["verified", "polished", "shipped", "handed_off"]);
337
+ const currentDone = DONE_STATUSES.has(s.status) && s.verification !== "fail" ? 1 : 0;
338
+ const phaseFrac = Math.min(
339
+ 100,
340
+ Math.round(((s.phase - 1 + currentDone) / s.total_phases) * 100)
341
+ );
333
342
  const filled = Math.round(phaseFrac / 10);
334
343
  const bar = "█".repeat(filled) + "░".repeat(10 - filled);
335
344
  const now = new Date().toISOString().split("T")[0];