qualia-framework 5.8.0 → 5.9.1

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.
@@ -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.
@@ -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
 
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
@@ -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
  }
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];
@@ -505,8 +505,8 @@
505
505
  <div class="kit-group">
506
506
  <h4>Mid-flight design &amp; tests</h4>
507
507
  <dl>
508
- <dt>/qualia-polish</dt><dd>Manual design pass on a component, route, or full app.</dd>
509
- <dt>/qualia-polish-loop</dt><dd>Autonomous screenshot → score → fix loop until rubric passes.</dd>
508
+ <dt>/qualia-polish</dt><dd>Design pass scope-adaptive: component, route, full app, redesign, critique, quick.</dd>
509
+ <dt>/qualia-polish --loop</dt><dd>Autonomous screenshot → score → fix loop until rubric passes.</dd>
510
510
  <dt>/qualia-test</dt><dd>Generate tests, run tests, or drive a feature TDD-style.</dd>
511
511
  </dl>
512
512
  </div>
@@ -521,8 +521,7 @@
521
521
  <div class="kit-group">
522
522
  <h4>Build something small</h4>
523
523
  <dl>
524
- <dt>/qualia-quick</dt><dd>Trivial inline fix. One file, no spawn. Typo, config tweak.</dd>
525
- <dt>/qualia-task</dt><dd>Single feature, fresh builder spawn, atomic commit. 1 to 5 files.</dd>
524
+ <dt>/qualia-feature</dt><dd>Auto-scoped: inline for trivia (typo, config tweak), fresh builder spawn for 1-5 file features. Refuses and routes to /qualia-plan when scope is phase-sized.</dd>
526
525
  </dl>
527
526
  </div>
528
527
  <div class="kit-group">
@@ -553,7 +552,6 @@
553
552
  <dl>
554
553
  <dt>/qualia-discuss</dt><dd>Without args: project-level non-technical discovery (mandatory at kickoff). With <code>N</code>: phase-level technical alignment with locked decisions and ADRs.</dd>
555
554
  <dt>/qualia-research</dt><dd>Deep-research a niche library or domain before planning a phase.</dd>
556
- <dt>/qualia-prd</dt><dd>Synthesize the current conversation into a durable PRD document.</dd>
557
555
  </dl>
558
556
  </div>
559
557
  <div class="kit-group">
@@ -1,10 +1,12 @@
1
- # /qualia-polish-loop — Pilot results
1
+ # /qualia-polish --loop — Pilot results
2
+
3
+ > Historical pilot from v5.1.0 (when the command was `/qualia-polish-loop`). Paths updated to the current `skills/qualia-polish/` location after the v5.8.0 consolidation into the `--loop` flag.
2
4
 
3
5
  **Run date:** 2026-05-03
4
6
  **Framework version:** 5.1.0 (this commit)
5
7
  **Operator:** Claude Opus 4.7 (1M context), main session, autonomous build
6
8
  **Browser backend used:** Playwright cached chromium 1217 (`~/.cache/ms-playwright/chromium-1217/chrome-linux64/chrome`) — auto-selected by `playwright-capture.mjs` after `import('playwright')` failed (no `playwright` npm package in the framework repo) and the cache lookup found a usable binary
7
- **Fixture server:** `python3 -m http.server 18080` against `skills/qualia-polish-loop/fixtures/`
9
+ **Fixture server:** `python3 -m http.server 18080` against `skills/qualia-polish/fixtures/`
8
10
  **Captures:** `/tmp/qpl-pilot-1777778113/{scenario1,scenario2}/{mobile,tablet,desktop}-*.png`
9
11
 
10
12
  This pilot replaces an earlier draft that pre-dated the actual capture pipeline. The numbers below come from real runs of `playwright-capture.mjs` against the committed fixtures, not from architectural reasoning.
@@ -13,7 +15,7 @@ This pilot replaces an earlier draft that pre-dated the actual capture pipeline.
13
15
 
14
16
  ## Scenario 1 — Synthetic clean page
15
17
 
16
- **Fixture:** `skills/qualia-polish-loop/fixtures/clean.html` (170 lines). Fraunces + JetBrains Mono pair, OKLCH palette tinted toward 220° (cyan), asymmetric hero (`1.4fr / 1fr`), full-width with `clamp()` padding, varied work-grid (no card monotony), border-only headers, focus-visible rings, `prefers-reduced-motion` respected, 65ch line-length cap on prose.
18
+ **Fixture:** `skills/qualia-polish/fixtures/clean.html` (170 lines). Fraunces + JetBrains Mono pair, OKLCH palette tinted toward 220° (cyan), asymmetric hero (`1.4fr / 1fr`), full-width with `clamp()` padding, varied work-grid (no card monotony), border-only headers, focus-visible rings, `prefers-reduced-motion` respected, 65ch line-length cap on prose.
17
19
 
18
20
  **Expected per spec:** SUCCESS in 1-2 iterations with all dims ≥ 4.
19
21
 
@@ -53,7 +55,7 @@ This pilot replaces an earlier draft that pre-dated the actual capture pipeline.
53
55
 
54
56
  ## Scenario 2 — Synthetic broken page
55
57
 
56
- **Fixture:** `skills/qualia-polish-loop/fixtures/broken.html` (115 lines). Deliberately shipped slop. Inter font as primary, pure `#fff` + `#000`, blue→purple linear-gradient hero, `background-clip: text` gradient on h1, three identical 1/3-width feature cards with `border-left: 4px solid #2563eb` side-stripes, "Get Started" + "Learn More" CTAs, `max-width: 1280px` container, `outline: none` on nav links without replacement.
58
+ **Fixture:** `skills/qualia-polish/fixtures/broken.html` (115 lines). Deliberately shipped slop. Inter font as primary, pure `#fff` + `#000`, blue→purple linear-gradient hero, `background-clip: text` gradient on h1, three identical 1/3-width feature cards with `border-left: 4px solid #2563eb` side-stripes, "Get Started" + "Learn More" CTAs, `max-width: 1280px` container, `outline: none` on nav links without replacement.
57
59
 
58
60
  **Expected per spec:** Loop identifies all anti-patterns, fixes them, ends SUCCESS in 4-6 iterations.
59
61
 
@@ -110,7 +112,7 @@ This pilot replaces an earlier draft that pre-dated the actual capture pipeline.
110
112
 
111
113
  ```bash
112
114
  # Initialize a fresh state file
113
- node skills/qualia-polish-loop/scripts/loop.mjs init \
115
+ node skills/qualia-polish/scripts/loop.mjs init \
114
116
  --state /tmp/.../qpl-kill.json --url http://localhost:3000 --max 8
115
117
 
116
118
  # Iteration 1: write an eval where typography==1 with a fixed issue fingerprint