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.
- package/README.md +17 -13
- package/agents/plan-checker.md +8 -0
- package/agents/qa-browser.md +7 -0
- package/agents/research-synthesizer.md +4 -1
- package/agents/researcher.md +6 -1
- package/agents/roadmapper.md +8 -0
- package/agents/verifier.md +14 -1
- package/agents/visual-evaluator.md +1 -1
- package/bin/cli.js +30 -1
- package/bin/erp-retry.js +289 -0
- package/bin/install.js +12 -6
- package/bin/slop-detect.mjs +1 -1
- package/bin/state.js +10 -1
- package/docs/onboarding.html +621 -0
- package/docs/playwright-loop-pilot-results.md +7 -5
- package/docs/research/2026-05-11-deep-research.md +189 -0
- package/guide.md +5 -6
- package/hooks/session-start.js +19 -1
- package/package.json +3 -2
- package/rules/speed.md +1 -2
- package/skills/qualia-discuss/SKILL.md +106 -6
- package/skills/qualia-feature/SKILL.md +216 -0
- package/skills/qualia-milestone/SKILL.md +73 -1
- package/skills/qualia-new/SKILL.md +52 -25
- package/skills/qualia-optimize/SKILL.md +1 -1
- package/skills/{qualia-polish-loop → qualia-polish}/REFERENCE.md +5 -5
- package/skills/qualia-polish/SKILL.md +13 -4
- package/skills/{qualia-polish-loop → qualia-polish}/scripts/loop.mjs +2 -2
- package/skills/{qualia-polish-loop → qualia-polish}/scripts/playwright-capture.mjs +1 -1
- package/skills/qualia-report/SKILL.md +64 -2
- package/skills/qualia-road/SKILL.md +10 -11
- package/skills/qualia-verify/SKILL.md +16 -0
- package/templates/help.html +2 -3
- package/templates/project-discovery.md +83 -0
- package/templates/project.md +7 -0
- package/tests/bin.test.sh +97 -67
- package/tests/refs.test.sh +146 -0
- package/tests/slop-detect.test.sh +2 -2
- package/skills/qualia-polish-loop/SKILL.md +0 -201
- package/skills/qualia-prd/SKILL.md +0 -199
- package/skills/qualia-quick/SKILL.md +0 -44
- package/skills/qualia-task/SKILL.md +0 -98
- /package/skills/{qualia-polish-loop → qualia-polish}/fixtures/broken.html +0 -0
- /package/skills/{qualia-polish-loop → qualia-polish}/fixtures/clean.html +0 -0
- /package/skills/{qualia-polish-loop → qualia-polish}/scripts/score.mjs +0 -0
package/README.md
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
|
-
# Qualia Framework v5.
|
|
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
|
|
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
|
|
8
|
-
- **v5.0
|
|
9
|
-
- **v5.1
|
|
10
|
-
- **v5.2
|
|
11
|
-
- **v5.3
|
|
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-
|
|
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
|
|
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.
|
|
146
|
+
## What's Inside (v5.8.0)
|
|
143
147
|
|
|
144
|
-
- **
|
|
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)
|
package/agents/plan-checker.md
CHANGED
|
@@ -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).
|
package/agents/qa-browser.md
CHANGED
|
@@ -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-
|
|
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.
|
package/agents/researcher.md
CHANGED
|
@@ -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
|
|
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
|
|
package/agents/roadmapper.md
CHANGED
|
@@ -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.
|
package/agents/verifier.md
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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":
|
package/bin/erp-retry.js
ADDED
|
@@ -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
|
|
394
|
-
// playwright-capture.mjs, loop.mjs, score.mjs that the
|
|
395
|
-
// runtime). Recursive
|
|
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
|
-
"⬢
|
|
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}
|
|
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("");
|
package/bin/slop-detect.mjs
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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];
|