qualia-framework 4.5.0 → 5.1.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 (64) hide show
  1. package/AGENTS.md +24 -0
  2. package/CLAUDE.md +12 -75
  3. package/README.md +23 -16
  4. package/agents/builder.md +9 -21
  5. package/agents/planner.md +8 -0
  6. package/agents/verifier.md +8 -0
  7. package/agents/visual-evaluator.md +132 -0
  8. package/bin/cli.js +54 -18
  9. package/bin/install.js +369 -29
  10. package/bin/qualia-ui.js +208 -1
  11. package/bin/slop-detect.mjs +5 -0
  12. package/bin/state.js +34 -1
  13. package/docs/install-redesign-builder-prompt.md +290 -0
  14. package/docs/install-redesign-pilot.md +234 -0
  15. package/docs/playwright-loop-builder-prompt.md +185 -0
  16. package/docs/playwright-loop-design-notes.md +108 -0
  17. package/docs/playwright-loop-pilot-results.md +170 -0
  18. package/docs/playwright-loop-review-2026-05-03.md +65 -0
  19. package/docs/playwright-loop-tester-prompt.md +213 -0
  20. package/docs/reviews/matt-pocock-skills-analysis.md +300 -0
  21. package/guide.md +9 -5
  22. package/hooks/env-empty-guard.js +74 -0
  23. package/hooks/pre-compact.js +19 -9
  24. package/hooks/pre-deploy-gate.js +8 -2
  25. package/hooks/pre-push.js +26 -12
  26. package/hooks/supabase-destructive-guard.js +62 -0
  27. package/hooks/vercel-account-guard.js +91 -0
  28. package/package.json +2 -1
  29. package/rules/design-brand.md +4 -0
  30. package/rules/design-laws.md +4 -0
  31. package/rules/design-product.md +4 -0
  32. package/rules/design-rubric.md +4 -0
  33. package/rules/grounding.md +4 -0
  34. package/skills/qualia-build/SKILL.md +40 -46
  35. package/skills/qualia-discuss/SKILL.md +51 -68
  36. package/skills/qualia-handoff/SKILL.md +1 -0
  37. package/skills/qualia-issues/SKILL.md +151 -0
  38. package/skills/qualia-map/SKILL.md +78 -35
  39. package/skills/qualia-new/REFERENCE.md +139 -0
  40. package/skills/qualia-new/SKILL.md +45 -121
  41. package/skills/qualia-optimize/REFERENCE.md +202 -0
  42. package/skills/qualia-optimize/SKILL.md +72 -237
  43. package/skills/qualia-plan/SKILL.md +58 -65
  44. package/skills/qualia-polish-loop/REFERENCE.md +265 -0
  45. package/skills/qualia-polish-loop/SKILL.md +201 -0
  46. package/skills/qualia-polish-loop/fixtures/broken.html +117 -0
  47. package/skills/qualia-polish-loop/fixtures/clean.html +196 -0
  48. package/skills/qualia-polish-loop/scripts/loop.mjs +302 -0
  49. package/skills/qualia-polish-loop/scripts/playwright-capture.mjs +197 -0
  50. package/skills/qualia-polish-loop/scripts/score.mjs +176 -0
  51. package/skills/qualia-report/SKILL.md +141 -200
  52. package/skills/qualia-research/SKILL.md +28 -33
  53. package/skills/qualia-road/SKILL.md +103 -0
  54. package/skills/qualia-ship/SKILL.md +1 -0
  55. package/skills/qualia-task/SKILL.md +1 -1
  56. package/skills/qualia-test/SKILL.md +50 -2
  57. package/skills/qualia-triage/SKILL.md +152 -0
  58. package/skills/qualia-verify/SKILL.md +63 -104
  59. package/skills/qualia-zoom/SKILL.md +51 -0
  60. package/skills/zoho-workflow/SKILL.md +1 -1
  61. package/templates/CONTEXT.md +36 -0
  62. package/templates/decisions/ADR-template.md +30 -0
  63. package/tests/bin.test.sh +451 -7
  64. package/tests/state.test.sh +58 -0
@@ -1,123 +1,160 @@
1
1
  ---
2
2
  name: qualia-report
3
- description: "Generate session report and commit to repo. Mandatory before clock-out."
3
+ description: "Generate session report, commit to git, push, and upload to the Qualia ERP — the mandatory clock-out flow. Use when the user says 'qualia-report', 'clock out', 'end of day', 'wrap up', 'session report', 'submit report', 'I'm done for today', or before stopping work. Handles empty days (no commits), missing API key, ERP outages, and dry-run preview gracefully."
4
4
  allowed-tools:
5
5
  - Bash
6
6
  - Read
7
7
  - Write
8
8
  - Edit
9
+ - AskUserQuestion
9
10
  ---
10
11
 
11
- # /qualia-report — Session Report
12
+ # /qualia-report — Daily Clock-Out Report
12
13
 
13
- Generate a concise report of what was done. Committed to git and uploaded to the ERP for clock-out.
14
+ The end-of-day flow. Generates a report, commits it, pushes, uploads to the ERP, and tells the employee they can stop. Designed so Hasan and Moayad never get stuck on it.
14
15
 
15
16
  ## Flags
16
-
17
17
  - `/qualia-report` — normal flow (generate, commit, push, upload to ERP)
18
- - `/qualia-report --dry-run` — generate + show payload, SKIP upload and SKIP commit. Useful for debugging or previewing before a real clock-out.
18
+ - `/qualia-report --dry-run` — preview the payload without committing/uploading
19
19
 
20
20
  ## Process
21
21
 
22
+ ### Step 0 — Pre-flight (graceful)
23
+
22
24
  ```bash
23
25
  node ~/.claude/bin/qualia-ui.js banner report
26
+
27
+ # Sanity checks — soft failures only, never block the report
28
+ test -d .git || node ~/.claude/bin/qualia-ui.js warn "Not a git repo — local commit will be skipped, ERP upload will still try"
29
+ test -f .planning/tracking.json || node ~/.claude/bin/qualia-ui.js warn "No tracking.json yet — ERP payload will use defaults"
30
+
31
+ DRY_RUN="${DRY_RUN:-false}"
32
+ echo "$ARGUMENTS" | grep -q -- '--dry-run' && DRY_RUN="true"
24
33
  ```
25
34
 
26
- ### 1. Gather Data
35
+ ### Step 1 Gather
27
36
 
28
37
  ```bash
29
38
  SINCE="8 hours ago"
30
- echo "---COMMITS---"
31
- git log --oneline --since="$SINCE" 2>/dev/null | head -20
32
- echo "---STATS---"
33
- echo "COUNT:$(git log --oneline --since="$SINCE" 2>/dev/null | wc -l)"
34
- echo "---PROJECT---"
35
- echo "DIR:$(basename $(pwd))"
36
- echo "BRANCH:$(git branch --show-current 2>/dev/null)"
37
- echo "---STATE---"
38
- node ~/.claude/bin/state.js check 2>/dev/null
39
+ COMMITS=$(git log --oneline --since="$SINCE" 2>/dev/null)
40
+ COUNT=$(echo "$COMMITS" | grep -c . 2>/dev/null || echo 0)
41
+ BRANCH=$(git branch --show-current 2>/dev/null || echo "no-branch")
42
+ PROJECT=$(basename "$(pwd)")
43
+ ```
44
+
45
+ ### Step 2 — Synthesize (with empty-day handling)
46
+
47
+ **If `COUNT > 0`** — build a structured summary using this template:
48
+
49
+ ```markdown
50
+ ## What Was Done
51
+ - {Verb} {object}. Why: {one-clause reason}. ← write 3–6 of these, one per related-commits group
52
+ verbs: Built, Fixed, Refactored, Added, Removed,
53
+ Migrated, Documented, Investigated, Reverted
54
+
55
+ ## Blockers
56
+ None. ← or list 1–N actual blockers (NOT "had to read docs" — that's normal)
57
+
58
+ ## Next Steps
59
+ 1. {Concrete next action — a command or a decision needed}
60
+ 2. ...
39
61
  ```
40
62
 
41
- ### 2. Synthesize
63
+ **If `COUNT == 0`** — ask the employee gracefully (don't force a fake report):
64
+
65
+ Use `AskUserQuestion`:
66
+ - header: "Empty day?"
67
+ - question: "No commits in the last 8 hours. What did you do today?"
68
+ - options:
69
+ - "Investigation / research only"
70
+ - "Meetings / calls (no code)"
71
+ - "Blocked — tell me on what"
72
+ - "Time off / partial day"
42
73
 
43
- Build a concise summary:
44
- - **What was done:** 3-6 bullet points. Start with verbs (Built, Fixed, Added). Group related commits.
45
- - **Blockers:** Only if something is actually blocked.
46
- - **Next:** 1-3 clear next actions.
74
+ Capture the answer as the report body. Empty days are still valid clock-outs — the ERP needs to see them.
47
75
 
48
- ### 3. Generate Report
76
+ ### Step 3 Write report file
49
77
 
50
- Write to `.planning/reports/report-{YYYY-MM-DD}.md`:
78
+ `.planning/reports/report-{YYYY-MM-DD}.md`:
51
79
 
52
80
  ```markdown
53
81
  # Session Report — {YYYY-MM-DD}
54
82
 
55
- **Project:** {name}
83
+ **Project:** {PROJECT}
56
84
  **Employee:** {git user.name}
57
- **Branch:** {branch}
58
- **Phase:** {N}{name} ({status})
85
+ **Branch:** {BRANCH}
86
+ **Phase:** {N — name (status), or "no active phase"}
59
87
  **Date:** {YYYY-MM-DD}
60
88
 
61
- ## What Was Done
62
- - {accomplishment 1}
63
- - {accomplishment 2}
64
- - {accomplishment 3}
65
-
66
- ## Blockers
67
- None. / - {blocker}
68
-
69
- ## Next Steps
70
- 1. {next action}
71
- 2. {next action}
89
+ {synthesis from Step 2}
72
90
 
73
- ## Commits
74
- {list from git log}
91
+ ## Commits ({COUNT})
92
+ {git log output, or "none today"}
75
93
  ```
76
94
 
77
- ### 4. Obtain Client Report ID (QS-REPORT-NN)
95
+ ### Step 4 Allocate client report ID
78
96
 
79
- Each session report gets a stable, sequential client-side identifier that travels with the report all the way to the ERP. The sequence is per-project, persisted in `tracking.json.report_seq`.
97
+ Per-project sequential ID (QS-REPORT-01, 02, ...). State.js owns the counter never edit by hand.
80
98
 
81
99
  ```bash
82
- # --dry-run: peek without incrementing
83
- # Wrap the pipe in try/catch so a state.js failure (missing tracking.json,
84
- # corrupt JSON) produces a clear error instead of silently becoming "".
85
- PEEK_FLAG=""
86
- [ "$DRY_RUN" = "true" ] && PEEK_FLAG="--peek"
87
- CLIENT_REPORT_ID=$(node ~/.claude/bin/state.js next-report-id $PEEK_FLAG 2>/dev/null | node -e "
88
- try {
89
- const raw = require('fs').readFileSync(0,'utf8');
90
- if (!raw.trim()) process.exit(2);
91
- const j = JSON.parse(raw);
92
- if (!j.report_id) process.exit(3);
93
- process.stdout.write(j.report_id);
94
- } catch (e) { process.exit(1); }
95
- ")
100
+ PEEK=""
101
+ [ "$DRY_RUN" = "true" ] && PEEK="--peek"
102
+ CLIENT_REPORT_ID=$(node ~/.claude/bin/state.js next-report-id $PEEK 2>/dev/null \
103
+ | node -e "try{const j=JSON.parse(require('fs').readFileSync(0,'utf8'));process.stdout.write(j.report_id||'')}catch{}")
96
104
 
97
105
  if [ -z "$CLIENT_REPORT_ID" ]; then
98
- node ~/.claude/bin/qualia-ui.js fail "Could not obtain report ID from state.js is .planning/tracking.json valid?"
106
+ node ~/.claude/bin/qualia-ui.js fail "Could not allocate report ID. Try: cat .planning/tracking.json (is it valid JSON?)"
99
107
  exit 1
100
108
  fi
101
109
  ```
102
110
 
103
- Example: first report on a fresh project `QS-REPORT-01`. Next → `QS-REPORT-02`. Etc.
104
-
105
- ### 5. Commit and Push (SKIP on --dry-run)
111
+ ### Step 5 Commit + push (skip on --dry-run)
106
112
 
107
113
  ```bash
108
- if [ "$DRY_RUN" != "true" ]; then
114
+ if [ "$DRY_RUN" != "true" ] && [ -d .git ]; then
109
115
  mkdir -p .planning/reports
110
116
  git add .planning/reports/report-{date}.md .planning/tracking.json
111
- git commit -m "report: {CLIENT_REPORT_ID} session {YYYY-MM-DD}"
112
- git push
117
+ git commit -m "report: $CLIENT_REPORT_ID session {YYYY-MM-DD}" || node ~/.claude/bin/qualia-ui.js warn "Nothing to commit (clean tree?)"
118
+ git push 2>&1 | tail -3
119
+ PUSH_EXIT=${PIPESTATUS[0]}
120
+ [ "$PUSH_EXIT" != "0" ] && node ~/.claude/bin/qualia-ui.js warn "git push failed — report committed locally but not pushed. ERP upload will still try."
113
121
  fi
114
122
  ```
115
123
 
116
- ### 6. Upload to ERP (SKIP on --dry-run)
124
+ ### Step 6 Upload to ERP
125
+
126
+ The full payload-builder + 3-attempt-retry logic lives unchanged from v4 — see the **ERP Upload** section below for the canonical implementation. Behavior summary:
127
+ - ERP disabled in config → skip silently, note local commit
128
+ - API key missing → warn with self-service fix instructions, skip upload
129
+ - 401/422 → permanent failure, no retry, tell employee to ask Fawzi
130
+ - Transient (timeout/5xx) → 3 attempts with 1s/3s/9s backoff
131
+ - Success → "Uploaded as $CLIENT_REPORT_ID (ERP: {uuid})"
117
132
 
118
- Read `~/.claude/.qualia-config.json` and check the `erp` object:
119
- - If `erp.enabled` is `false`, skip this step and print: "ERP upload skipped (disabled in config)."
120
- - If `erp.enabled` is `true` (default) or the `erp` field is missing (backward compatibility), proceed with the upload.
133
+ ### Step 7 State + closing
134
+
135
+ ```bash
136
+ if [ "$DRY_RUN" != "true" ]; then
137
+ node ~/.claude/bin/state.js transition --to activity --notes "Session report $CLIENT_REPORT_ID generated" 2>/dev/null
138
+ fi
139
+
140
+ node ~/.claude/bin/qualia-ui.js divider
141
+ node ~/.claude/bin/qualia-ui.js ok "Report $CLIENT_REPORT_ID complete."
142
+ node ~/.claude/bin/qualia-ui.js info "You can clock out now. See you tomorrow."
143
+ ```
144
+
145
+ ## Common errors (read this when something goes wrong)
146
+
147
+ | Symptom | Likely cause | Self-service fix |
148
+ |---|---|---|
149
+ | "Could not allocate report ID" | tracking.json missing/corrupt | `cat .planning/tracking.json` to inspect, or restore from `git checkout HEAD -- .planning/tracking.json` |
150
+ | "ERP API key missing" | `~/.claude/.erp-api-key` empty | `qualia-framework set-erp-key <key>` (ask Fawzi for the key) |
151
+ | "ERP auth failed (401)" | Key revoked or wrong | Ask Fawzi for a fresh key |
152
+ | "ERP upload failed after 3 attempts" | ERP down or network issue | Local commit is safe. Re-run `/qualia-report` later. |
153
+ | "git push failed" | Auth or network or upstream issue | `git push` manually, see the error, fix, re-run |
154
+
155
+ ## ERP Upload (canonical implementation)
156
+
157
+ The Step 6 payload + retry logic. Inlined to avoid a script-file fetch on every clock-out. The agent runs this verbatim.
121
158
 
122
159
  ```bash
123
160
  # Read ERP config
@@ -129,171 +166,75 @@ REPORT_FILE=".planning/reports/report-{date}.md"
129
166
  SUBMITTED_BY=$(git config user.name || echo "unknown")
130
167
  SUBMITTED_AT=$(date -u +%Y-%m-%dT%H:%M:%SZ)
131
168
 
132
- # Guard: ERP upload requires a non-empty API key. Without this check, curl
133
- # would POST with "Authorization: Bearer " (blank bearer) and the server
134
- # returns a generic 401 that is hard to diagnose.
169
+ # Guard: API key required for upload (otherwise curl posts an empty bearer)
135
170
  if [ "$ERP_ENABLED" = "true" ] && [ -z "$API_KEY" ] && [ "$DRY_RUN" != "true" ]; then
136
- node ~/.claude/bin/qualia-ui.js warn "ERP API key missing (~/.claude/.erp-api-key is empty or unreadable). Skipping upload."
137
- node ~/.claude/bin/qualia-ui.js info "Ask Fawzi for the ERP key, run 'qualia-framework set-erp-key <key>', then run 'qualia-framework erp-ping'."
171
+ node ~/.claude/bin/qualia-ui.js warn "ERP API key missing (~/.claude/.erp-api-key). Run: qualia-framework set-erp-key <key>"
138
172
  ERP_ENABLED="false"
139
173
  fi
140
174
 
141
- # Build structured JSON payload from tracking.json (matches ERP contract /api/v1/reports)
142
- # v4: include milestone_name, milestones[], team_id, project_id, git_remote,
143
- # session_started_at, last_pushed_at, build_count, deploy_count — the ERP
144
- # uses these to render the project tree (milestone → phases → unphased) correctly.
145
- # v4.0.4: client_report_id carries the QS-REPORT-NN identifier.
146
- # Build payload. Pass user-controlled values (SUBMITTED_BY, CLIENT_REPORT_ID,
147
- # SUBMITTED_AT, REPORT_FILE) via env vars instead of shell interpolation — a
148
- # single quote or backslash in git user.name would otherwise break the node -e
149
- # script silently. process.env.* is inert to shell metacharacters.
175
+ # Build payload (env-var-passed user values to dodge shell escaping)
150
176
  PAYLOAD=$(
151
- SUBMITTED_BY="$SUBMITTED_BY" \
152
- SUBMITTED_AT="$SUBMITTED_AT" \
153
- CLIENT_REPORT_ID="$CLIENT_REPORT_ID" \
154
- REPORT_FILE="$REPORT_FILE" \
177
+ SUBMITTED_BY="$SUBMITTED_BY" SUBMITTED_AT="$SUBMITTED_AT" \
178
+ CLIENT_REPORT_ID="$CLIENT_REPORT_ID" REPORT_FILE="$REPORT_FILE" \
155
179
  node -e "
156
- const fs = require('fs');
157
- const path = require('path');
158
- const os = require('os');
159
- const { spawnSync } = require('child_process');
160
- const git = (args) => {
161
- const r = spawnSync('git', args, { encoding: 'utf8', timeout: 3000 });
162
- return r.status === 0 ? r.stdout.trim() : '';
163
- };
164
- const repoSlug = (remote) => (remote || '')
165
- .replace(/^git@github\\.com:/, 'github.com/')
166
- .replace(/^https?:\\/\\//, '')
167
- .replace(/\\.git$/, '')
168
- .split('/')
169
- .filter(Boolean)
170
- .pop();
171
- let config = {};
172
- try {
173
- config = JSON.parse(fs.readFileSync(path.join(os.homedir(), '.claude/.qualia-config.json'), 'utf8'));
174
- } catch {}
175
- const t = JSON.parse(fs.readFileSync('.planning/tracking.json', 'utf8'));
176
- const notes = fs.readFileSync(process.env.REPORT_FILE, 'utf8').substring(0, 60000);
177
- const commits = [];
178
- try {
179
- const r = spawnSync('git', ['log', '--oneline', '--since=8 hours ago', '--format=%h'], { encoding: 'utf8', timeout: 3000 });
180
- if (r.stdout) commits.push(...r.stdout.trim().split('\n').filter(Boolean));
181
- } catch {}
182
- const gitRemote = t.git_remote || git(['config', '--get', 'remote.origin.url']);
183
- const projectKey = t.project_id || repoSlug(gitRemote) || require('path').basename(process.cwd());
180
+ const fs=require('fs'),path=require('path'),os=require('os');
181
+ const {spawnSync}=require('child_process');
182
+ const git=(a)=>{const r=spawnSync('git',a,{encoding:'utf8',timeout:3000});return r.status===0?r.stdout.trim():'';};
183
+ const repoSlug=(r)=>(r||'').replace(/^git@github\\.com:/,'github.com/').replace(/^https?:\\/\\//,'').replace(/\\.git$/,'').split('/').filter(Boolean).pop();
184
+ let config={};try{config=JSON.parse(fs.readFileSync(path.join(os.homedir(),'.claude/.qualia-config.json'),'utf8'));}catch{}
185
+ const t=JSON.parse(fs.readFileSync('.planning/tracking.json','utf8'));
186
+ const notes=fs.readFileSync(process.env.REPORT_FILE,'utf8').substring(0,60000);
187
+ const commits=[];try{const r=spawnSync('git',['log','--oneline','--since=8 hours ago','--format=%h'],{encoding:'utf8',timeout:3000});if(r.stdout)commits.push(...r.stdout.trim().split('\n').filter(Boolean));}catch{}
188
+ const gitRemote=t.git_remote||git(['config','--get','remote.origin.url']);
189
+ const projectKey=t.project_id||repoSlug(gitRemote)||require('path').basename(process.cwd());
184
190
  console.log(JSON.stringify({
185
- project: t.project || require('path').basename(process.cwd()),
186
- project_id: projectKey,
187
- team_id: t.team_id || 'qualia-solutions',
188
- git_remote: gitRemote,
189
- client: t.client || '',
190
- client_report_id: process.env.CLIENT_REPORT_ID,
191
- framework_version: config.version || '',
192
- milestone: t.milestone || 1,
193
- milestone_name: t.milestone_name || '',
194
- milestones: Array.isArray(t.milestones) ? t.milestones : [],
195
- phase: t.phase,
196
- phase_name: t.phase_name,
197
- total_phases: t.total_phases,
198
- status: t.status,
199
- tasks_done: t.tasks_done || 0,
200
- tasks_total: t.tasks_total || 0,
201
- verification: t.verification || 'pending',
202
- gap_cycles: (t.gap_cycles || {})[String(t.phase)] || 0,
203
- build_count: t.build_count || 0,
204
- deploy_count: t.deploy_count || 0,
205
- deployed_url: t.deployed_url || '',
206
- session_started_at: t.session_started_at || '',
207
- last_pushed_at: t.last_pushed_at || '',
208
- lifetime: t.lifetime || {},
209
- commits: commits,
210
- notes: notes,
211
- submitted_by: process.env.SUBMITTED_BY || 'unknown',
212
- submitted_at: process.env.SUBMITTED_AT
191
+ project:t.project||require('path').basename(process.cwd()),
192
+ project_id:projectKey,team_id:t.team_id||'qualia-solutions',git_remote:gitRemote,
193
+ client:t.client||'',client_report_id:process.env.CLIENT_REPORT_ID,
194
+ framework_version:config.version||'',milestone:t.milestone||1,
195
+ milestone_name:t.milestone_name||'',milestones:Array.isArray(t.milestones)?t.milestones:[],
196
+ phase:t.phase,phase_name:t.phase_name,total_phases:t.total_phases,status:t.status,
197
+ tasks_done:t.tasks_done||0,tasks_total:t.tasks_total||0,verification:t.verification||'pending',
198
+ gap_cycles:(t.gap_cycles||{})[String(t.phase)]||0,build_count:t.build_count||0,
199
+ deploy_count:t.deploy_count||0,deployed_url:t.deployed_url||'',
200
+ session_started_at:t.session_started_at||'',last_pushed_at:t.last_pushed_at||'',
201
+ lifetime:t.lifetime||{},commits:commits,notes:notes,
202
+ submitted_by:process.env.SUBMITTED_BY||'unknown',submitted_at:process.env.SUBMITTED_AT
213
203
  }));
214
204
  "
215
205
  )
216
206
 
217
- # --dry-run: print payload and stop (no POST, no commit, no increment already handled in step 4)
207
+ # --dry-run: print and stop
218
208
  if [ "$DRY_RUN" = "true" ]; then
219
- echo "--- DRY RUN · payload ---"
220
- echo "$PAYLOAD" | node -e "const d=JSON.parse(require('fs').readFileSync(0,'utf8'));console.log(JSON.stringify(d,null,2))"
209
+ echo "--- DRY RUN · payload ---"; echo "$PAYLOAD" | node -e "console.log(JSON.stringify(JSON.parse(require('fs').readFileSync(0,'utf8')),null,2))"
221
210
  echo "--- DRY RUN · would POST to: $ERP_URL/api/v1/reports ---"
222
211
  echo "--- DRY RUN · client_report_id would be: $CLIENT_REPORT_ID ---"
223
212
  exit 0
224
213
  fi
225
214
 
226
- # Real upload — 3 attempts with exponential backoff (1s, 3s, 9s).
227
- # The local report file is already committed, so a failed upload doesn't
228
- # lose data — it just leaves the ERP view stale until the next push or
229
- # manual retry.
215
+ # Upload — 3 attempts with 1s/3s/9s backoff
230
216
  if [ "$ERP_ENABLED" = "true" ]; then
231
- MAX_ATTEMPTS=3
232
- ATTEMPT=1
233
- SUCCESS=false
234
- while [ $ATTEMPT -le $MAX_ATTEMPTS ]; do
217
+ for ATTEMPT in 1 2 3; do
235
218
  RESPONSE=$(curl -sS -X POST "$ERP_URL/api/v1/reports" \
236
- -H "Authorization: Bearer $API_KEY" \
237
- -H "Content-Type: application/json" \
238
- -d "$PAYLOAD" \
239
- --max-time 10 \
240
- -w "\n__HTTP__%{http_code}" 2>&1)
219
+ -H "Authorization: Bearer $API_KEY" -H "Content-Type: application/json" \
220
+ -d "$PAYLOAD" --max-time 10 -w "\n__HTTP__%{http_code}" 2>&1)
241
221
  HTTP_CODE=$(echo "$RESPONSE" | grep -o "__HTTP__[0-9]*" | sed 's/__HTTP__//')
242
222
  BODY=$(echo "$RESPONSE" | sed 's/__HTTP__[0-9]*//g')
243
223
 
244
224
  if [ "$HTTP_CODE" = "200" ]; then
245
- SUCCESS=true
246
- # Parse and display the ERP-returned report_id alongside our local QS-REPORT-NN
247
- ERP_REPORT_ID=$(echo "$BODY" | node -e "try{const d=JSON.parse(require('fs').readFileSync(0,'utf8'));process.stdout.write(d.report_id||'')}catch{}")
225
+ ERP_REPORT_ID=$(echo "$BODY" | node -e "try{process.stdout.write(JSON.parse(require('fs').readFileSync(0,'utf8')).report_id||'')}catch{}")
248
226
  node ~/.claude/bin/qualia-ui.js ok "Uploaded as $CLIENT_REPORT_ID (ERP: ${ERP_REPORT_ID:-none})"
249
227
  break
250
228
  fi
251
-
252
- # 401 / 422 are permanent failures — no retry.
253
229
  if [ "$HTTP_CODE" = "401" ] || [ "$HTTP_CODE" = "422" ]; then
254
- if [ "$HTTP_CODE" = "401" ]; then
255
- node ~/.claude/bin/qualia-ui.js warn "ERP auth failed (HTTP 401) API key in ~/.claude/.erp-api-key is invalid or revoked. Ask Fawzi for a fresh key."
256
- else
257
- node ~/.claude/bin/qualia-ui.js warn "ERP rejected payload (HTTP 422) — schema validation failed. Response body:"
258
- fi
259
- echo "$BODY" | head -3
230
+ node ~/.claude/bin/qualia-ui.js warn "ERP rejected ($HTTP_CODE) — $([ "$HTTP_CODE" = "401" ] && echo "API key invalid, ask Fawzi" || echo "schema mismatch:")"
231
+ [ "$HTTP_CODE" = "422" ] && echo "$BODY" | head -3
260
232
  break
261
233
  fi
262
-
263
- # Transient failure — back off and retry.
264
- if [ $ATTEMPT -lt $MAX_ATTEMPTS ]; then
265
- SLEEP=$(( 1 * 3 ** (ATTEMPT - 1) ))
266
- node ~/.claude/bin/qualia-ui.js warn "ERP upload attempt $ATTEMPT failed (HTTP ${HTTP_CODE:-timeout}), retrying in ${SLEEP}s..."
267
- sleep $SLEEP
268
- fi
269
- ATTEMPT=$(( ATTEMPT + 1 ))
234
+ [ $ATTEMPT -lt 3 ] && { SLEEP=$((1 * 3 ** (ATTEMPT - 1))); node ~/.claude/bin/qualia-ui.js warn "Attempt $ATTEMPT failed (HTTP ${HTTP_CODE:-timeout}), retrying in ${SLEEP}s..."; sleep $SLEEP; }
270
235
  done
271
-
272
- if [ "$SUCCESS" != "true" ]; then
273
- node ~/.claude/bin/qualia-ui.js warn "ERP upload failed after $MAX_ATTEMPTS attempts. $CLIENT_REPORT_ID is committed locally; it will NOT appear in the ERP until you retry with 'curl' or re-run /qualia-report."
274
- fi
236
+ [ "$ATTEMPT" = "3" ] && [ "$HTTP_CODE" != "200" ] && node ~/.claude/bin/qualia-ui.js warn "ERP upload failed after 3 attempts. $CLIENT_REPORT_ID is committed locally; will appear in ERP after retry."
275
237
  fi
276
238
 
277
- if [ "$ERP_ENABLED" != "true" ]; then
278
- node ~/.claude/bin/qualia-ui.js info "ERP upload skipped (disabled in config). Report committed locally as $CLIENT_REPORT_ID."
279
- fi
239
+ [ "$ERP_ENABLED" != "true" ] && node ~/.claude/bin/qualia-ui.js info "ERP upload skipped (disabled). $CLIENT_REPORT_ID committed locally."
280
240
  ```
281
-
282
- Summary rules:
283
- - **Upload succeeds:** print "Uploaded as QS-REPORT-NN (ERP: {uuid})". Employee can clock out.
284
- - **401/422:** no retry. Print the error, tell the employee to ask Fawzi.
285
- - **Transient (timeout, 5xx, network):** retry 3x with 1s/3s/9s backoff.
286
- - **All retries fail:** tell employee the report is committed locally, ERP will be stale until retry.
287
- - **ERP disabled:** skip silently with a note, local commit still happens.
288
-
289
- ### 7. Update State (SKIP on --dry-run)
290
-
291
- ```bash
292
- if [ "$DRY_RUN" != "true" ]; then
293
- node ~/.claude/bin/state.js transition --to activity --notes "Session report $CLIENT_REPORT_ID generated"
294
- fi
295
- ```
296
-
297
- Do NOT manually edit STATE.md or tracking.json — state.js handles both.
298
-
299
- Employee cannot skip this. Run `/qualia-report` before clock-out.
@@ -12,14 +12,14 @@ allowed-tools:
12
12
 
13
13
  # /qualia-research — Per-Phase Deep Research
14
14
 
15
- Runs targeted research on a domain, library, or integration that a specific phase depends on. Distinct from `/qualia-new` research (which covers 4 dimensions project-wide) — this one is narrow and phase-scoped.
15
+ Targeted research on domain, library, or integration for a specific phase. Narrow and phase-scoped (distinct from `/qualia-new` project-wide research).
16
16
 
17
17
  ## When to Use
18
18
 
19
- - A phase touches a library you've never used
20
- - A phase integrates with a niche API (FHIR, legal forms, payment gateways)
21
- - SUMMARY.md marked this phase as a "Research flag"
22
- - You're about to plan and realize you don't know the current best practice
19
+ - Phase touches unfamiliar library
20
+ - Phase integrates niche API (FHIR, legal forms, payment gateways)
21
+ - SUMMARY.md flagged phase for research
22
+ - Unsure of current best practice before planning
23
23
 
24
24
  ## Usage
25
25
 
@@ -33,7 +33,7 @@ Runs targeted research on a domain, library, or integration that a specific phas
33
33
  node ~/.claude/bin/state.js check 2>/dev/null
34
34
  ```
35
35
 
36
- Use phase N from args, or current phase from STATE.md.
36
+ Phase N from args, or current phase from STATE.md.
37
37
 
38
38
  ### 2. Load Context
39
39
 
@@ -43,21 +43,19 @@ cat .planning/ROADMAP.md 2>/dev/null
43
43
  cat .planning/phase-{N}-context.md 2>/dev/null # if /qualia-discuss was run first
44
44
  ```
45
45
 
46
- Identify what this phase needs to know.
46
+ Identify what phase needs to know.
47
47
 
48
- ### 3. Ask the User What to Research
48
+ ### 3. Ask User
49
49
 
50
- Inline free text:
50
+ **"Researching Phase {N}: {phase name}. What to dig into? Library, domain, integration, pattern?"**
51
51
 
52
- **"I'm about to research Phase {N}: {phase name}. What specifically do you want me to dig into? Library, domain, integration, pattern?"**
52
+ Wait for answer. Answer defines research question.
53
53
 
54
- Wait for their answer. Their answer defines the research question.
55
-
56
- ### 4. Spawn the Researcher
54
+ ### 4. Spawn Researcher
57
55
 
58
56
  ```
59
57
  Agent(prompt="
60
- Read your role: @~/.claude/agents/researcher.md
58
+ Role: @~/.claude/agents/researcher.md
61
59
 
62
60
  <dimension>phase-specific</dimension>
63
61
 
@@ -67,27 +65,24 @@ Read your role: @~/.claude/agents/researcher.md
67
65
 
68
66
  <phase_context>
69
67
  Phase: {N}
70
- Goal: {phase goal from ROADMAP.md}
71
- Requirements: {REQ-IDs covered by this phase}
68
+ Goal: {goal from ROADMAP.md}
69
+ Reqs: {REQ-IDs for this phase}
72
70
  </phase_context>
73
71
 
74
72
  <project_context>
75
73
  {PROJECT.md summary}
76
74
  </project_context>
77
75
 
78
- <output_path>
79
- .planning/phase-{N}-research.md
80
- </output_path>
76
+ <output_path>.planning/phase-{N}-research.md</output_path>
81
77
 
82
- Research using Context7 first, then WebFetch, then WebSearch. Be specific and concrete.
83
- Include: recommendation, rationale, version numbers (if applicable), code examples,
84
- alternatives considered, what to avoid, sources.
78
+ Priority: Context7 WebFetch WebSearch.
79
+ Include: recommendation, rationale, versions, code examples, alternatives, pitfalls, sources.
85
80
  ", subagent_type="qualia-researcher", description="Phase {N} research")
86
81
  ```
87
82
 
88
- ### 5. Review Output
83
+ ### 5. Review
89
84
 
90
- Read `.planning/phase-{N}-research.md`. Present the key findings:
85
+ Read `.planning/phase-{N}-research.md`. Present findings:
91
86
 
92
87
  ```bash
93
88
  node ~/.claude/bin/qualia-ui.js divider
@@ -100,15 +95,15 @@ Show:
100
95
  - Top 3 key findings
101
96
  - Sources used
102
97
 
103
- ### 6. User Confirms or Asks More
98
+ ### 6. Confirm or Continue
104
99
 
105
100
  - header: "Enough?"
106
- - question: "Is this enough research, or should I dig deeper?"
101
+ - question: "Enough research, or dig deeper?"
107
102
  - options:
108
- - "Enough" — Move to planning
109
- - "Dig deeper" — I have more questions
103
+ - "Enough"
104
+ - "Dig deeper"
110
105
 
111
- If "Dig deeper" ask what they want, re-spawn the researcher with additional questions.
106
+ "Dig deeper" ask what, re-spawn researcher with additional questions.
112
107
 
113
108
  ### 7. Commit
114
109
 
@@ -125,7 +120,7 @@ node ~/.claude/bin/qualia-ui.js end "PHASE {N} RESEARCH DONE" "/qualia-plan {N}"
125
120
 
126
121
  ## Rules
127
122
 
128
- 1. **One research session per run.** Don't try to research phases 1 through 5 in one call.
129
- 2. **Must produce a file.** The research is worthless if it only lives in conversation context.
130
- 3. **Honor locked decisions from phase-{N}-context.md.** Don't research alternatives to something already locked.
131
- 4. **Context7 first.** Always try Context7 MCP before WebFetch — it's fastest and most current for known libraries.
123
+ 1. **One session per run.** Don't research phases 1-5 in one call.
124
+ 2. **Must produce a file.** Research in conversation only is worthless.
125
+ 3. **Honor locked decisions.** Don't research alternatives to locked choices.
126
+ 4. **Context7 first.** Try Context7 MCP before WebFetch.