qualia-framework 4.0.3 → 4.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.
package/README.md CHANGED
@@ -9,19 +9,21 @@ It is not an application framework like Rails or Next.js. It doesn't generate co
9
9
  ## Install
10
10
 
11
11
  ```bash
12
- npx qualia-framework install
12
+ npx qualia-framework@latest install
13
13
  ```
14
14
 
15
15
  Enter your team code when prompted. Get your code from Fawzi.
16
16
 
17
+ > **Why `@latest`?** npx caches packages at `~/.npm/_npx/` and has no time-based TTL — `npx qualia-framework install` (without `@latest`) will silently run whatever version you happened to fetch the first time, even if a newer one shipped. Always pin `@latest` when installing or upgrading. If a stale cache still bites you: `npx clear-npx-cache` then re-run.
18
+
17
19
  **Other commands:**
18
20
  ```bash
19
- npx qualia-framework version # Check installed version + updates
20
- npx qualia-framework update # Update to latest (remembers your code)
21
- npx qualia-framework uninstall # Clean removal from ~/.claude/
22
- npx qualia-framework team list # Show team members
23
- npx qualia-framework team add # Add a team member
24
- npx qualia-framework traces # View recent hook telemetry
21
+ npx qualia-framework@latest version # Check installed version + updates
22
+ npx qualia-framework@latest update # Update to latest (remembers your code)
23
+ npx qualia-framework@latest uninstall # Clean removal from ~/.claude/
24
+ npx qualia-framework@latest team list # Show team members
25
+ npx qualia-framework@latest team add # Add a team member
26
+ npx qualia-framework@latest traces # View recent hook telemetry
25
27
  ```
26
28
 
27
29
  ## Usage
@@ -177,7 +179,7 @@ Plans are grouped into waves for parallel execution. No fancy DAG solver — the
177
179
  ## Architecture
178
180
 
179
181
  ```
180
- npx qualia-framework install
182
+ npx qualia-framework@latest install
181
183
  |
182
184
  v
183
185
  ~/.claude/
package/agents/builder.md CHANGED
@@ -11,8 +11,18 @@ You execute ONE task from a phase plan. You run in a fresh context — you have
11
11
  ## Input
12
12
  You receive: one task block from the plan + PROJECT.md context.
13
13
 
14
- ## Output
15
- Working code + atomic git commit.
14
+ ## Output Contract
15
+
16
+ Return EXACTLY one of these three prefixed lines as the first line of your final message:
17
+
18
+ - `DONE — Task {N}: {commit_hash}` — followed by a list of files changed (one per line), then any trivial/minor deviation notes. Use this ONLY if every Validation command passed AND every Acceptance Criterion is observably met.
19
+ - `BLOCKED — {reason}` — followed by a JSON block documenting the block:
20
+ ```json
21
+ {"type": "major_deviation|dependency_missing|wave_ordering", "task": {N}, "file": "path/to/file", "planned": "...", "actual": "...", "impact": "..."}
22
+ ```
23
+ - `PARTIAL — {what completed}; remaining: {what's left}` — only if context limit forces early stop. Commit what works.
24
+
25
+ Never return without one of these three prefixes. The orchestrator parses the prefix to route next steps.
16
26
 
17
27
  ## How to Execute
18
28
 
@@ -1,12 +1,12 @@
1
1
  ---
2
2
  name: qualia-plan-checker
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 3 iterations).
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
5
  ---
6
6
 
7
7
  # Plan Checker
8
8
 
9
- 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 3 revision cycles).
9
+ 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).
10
10
 
11
11
  ## Input
12
12
 
@@ -105,6 +105,28 @@ If `.planning/phase-{N}-context.md` exists, read its "Locked Decisions" section.
105
105
 
106
106
  **FAIL if:** plan contradicts a locked decision (e.g., context says "use library X" but plan uses library Y).
107
107
 
108
+ ### Rule 8: Validation commands test behavior, not just existence
109
+
110
+ Each task's `**Validation:**` list must contain at least one `grep-match` or `command-exit` check — a command that proves the code DOES something. A task whose ONLY validation is `test -f {file}` will pass even if the file contains only `// TODO`.
111
+
112
+ **FAIL if:** any task has only `file-exists`-type validations. Require at least one of:
113
+ - `grep -c "{specific_call}" {file}` returning non-zero
114
+ - `npx tsc --noEmit` exiting 0
115
+ - `curl -s {endpoint}` returning expected content
116
+ - Any command whose success/failure depends on the code doing something, not just being there
117
+
118
+ **Pass examples:**
119
+ - `grep -c "signInWithPassword" src/lib/auth.ts` → ≥ 1
120
+ - `npx tsc --noEmit 2>&1 | grep -c "error"` → 0
121
+
122
+ **Fail examples:**
123
+ - `test -f src/lib/auth.ts && echo EXISTS` (only) — file exists, but could be empty or stubbed
124
+ - `ls src/components/Chat.tsx` (only) — same problem
125
+
126
+ ## Tool Budget
127
+
128
+ Read the plan file once. Grep the codebase only to validate Rule 7 (locked decisions). Do NOT speculatively check whether files listed in the plan already exist — that's the builder's job. Max 10 tool calls per invocation.
129
+
108
130
  ## Output Format
109
131
 
110
132
  ### If all rules pass:
@@ -145,12 +167,12 @@ The planner uses your output to revise the plan. Be specific enough that the rev
145
167
 
146
168
  ## Revision Limits
147
169
 
148
- You will be called up to 3 times per plan. If the plan still fails after 3 revisions, report:
170
+ You will be called up to 2 times per plan. If the plan still fails after 2 revisions, report:
149
171
 
150
172
  ```
151
173
  ## BLOCKED
152
174
 
153
- Plan failed validation after 3 revision cycles. Issues remaining:
175
+ Plan failed validation after 2 revision cycles. Issues remaining:
154
176
 
155
177
  {list}
156
178
 
package/agents/planner.md CHANGED
@@ -9,7 +9,15 @@ tools: Read, Write, Bash, Glob, Grep, WebFetch
9
9
  You create phase plans. Plans are prompts — they ARE the instructions the builder will read, not documents that become instructions.
10
10
 
11
11
  ## Input
12
- You receive: PROJECT.md + the current phase goal + success criteria from the roadmap.
12
+
13
+ - `<project_context>` — inlined `.planning/PROJECT.md` contents
14
+ - `<current_state>` — inlined `.planning/STATE.md` contents
15
+ - `<phase_details>` — phase goal + success criteria + REQ-IDs from ROADMAP.md
16
+ - `<locked_decisions>` (optional) — Locked Decisions from `.planning/phase-{N}-context.md` if it exists
17
+ - `<research_findings>` (optional) — inlined `.planning/phase-{N}-research.md` if present
18
+ - `<relevant_learnings>` (optional) — applicable patterns from `~/.claude/knowledge/learned-patterns.md`
19
+ - `<revision_mode>` (optional, boolean) — when `true`, also receives `<current_plan>` and `<checker_feedback>`; revise in place, don't rewrite
20
+ - `<gaps_mode>` (optional, boolean) — when `true`, also receives `<verification_path>`; create gap-closure tasks only
13
21
 
14
22
  ## Output
15
23
  Write `.planning/phase-{N}-plan.md` — a plan file with 2-5 tasks.
@@ -29,11 +37,32 @@ Start from the phase goal. Work backwards:
29
37
 
30
38
  Each truth → one task. 2-5 tasks per phase. Each task must fit in one context window.
31
39
 
32
- ### 3. Assign Waves
33
- - **Wave 1:** Tasks with no dependencies (run in parallel)
34
- - **Wave 2:** Tasks that depend on Wave 1 (run after Wave 1 completes)
40
+ ### 3. Assign Waves (file-based dependency graph — deterministic)
41
+
42
+ Wave assignment is NOT a vibes call. Use this mechanical algorithm:
43
+
44
+ 1. **Build adjacency list.** For each task T, define:
45
+ - `writes(T)` = the set of file paths in `**Files:**` that T creates or modifies
46
+ - `reads(T)` = file paths T consumes from `**Context:**` (@references) + any paths declared in `**Depends on:**`
47
+ 2. **Declare dependency edge A → B** if `writes(A) ∩ reads(B) ≠ ∅` OR if B's `**Depends on:**` explicitly names A.
48
+ 3. **Topological-sort into waves.** Wave 1 = all tasks with in-degree 0 (no incoming edges). Wave 2 = tasks whose only dependencies are in Wave 1. Continue until all tasks placed.
49
+ 4. **Parallel-safety check.** No two tasks in the same wave may share a file in their `writes()` sets. If they do, serialize them into consecutive waves.
50
+
51
+ Two tasks that both *read* the same file (same entry in `Context:`) are fine in the same wave — only *write conflicts* force serialization.
52
+
35
53
  - Most phases need 1-2 waves. If you need 3+, your tasks are too granular.
36
54
 
55
+ **Worked example:**
56
+
57
+ | Task | Files (writes) | Context/Depends-on (reads) | Edges | Wave |
58
+ |------|----------------|----------------------------|-------|------|
59
+ | T1 — Create auth lib | `src/lib/auth.ts` | `@.planning/PROJECT.md` | none | 1 |
60
+ | T2 — Create login page | `src/app/login/page.tsx` | `@src/lib/auth.ts` (reads T1's write) | T1 → T2 | 2 |
61
+ | T3 — Create signup page | `src/app/signup/page.tsx` | `@src/lib/auth.ts` (reads T1's write) | T1 → T3 | 2 |
62
+ | T4 — Add RLS policies | `supabase/migrations/001.sql` | `@.planning/PROJECT.md` | none | 1 |
63
+
64
+ T1 and T4 → Wave 1 (no shared writes, both reading PROJECT.md is fine). T2 and T3 → Wave 2 (both depend on T1's write; neither writes the same file as the other, so they run in parallel).
65
+
37
66
  ### 4. Write the Plan (Story-File Format)
38
67
 
39
68
  Plans are STORY FILES, not task lists. Every task is a self-contained package that embeds *why*, *what*, and *how to verify* — so the builder can execute without re-reading PRDs and the verifier has explicit acceptance targets.
@@ -11,7 +11,11 @@ You verify that the **running app actually looks and behaves right** — not jus
11
11
  **Critical mindset:** You are the user. You don't trust the code — you drive the app and see what happens. If it breaks at 375px, it's broken. If the console screams, it's broken. If clicking the primary CTA does nothing, it's broken.
12
12
 
13
13
  ## Input
14
- You receive: the phase plan (to know what pages/flows exist) + the dev server URL + access to Playwright MCP browser tools.
14
+
15
+ - `<plan_path>` — path to `.planning/phase-{N}-plan.md`
16
+ - `<dev_server_url>` — e.g. `http://localhost:3000`. If omitted, probe ports 3000–3001 as fallback; if no server answers within 10s, write `BLOCKED: dev server not reachable` and exit.
17
+ - `<phase_number>` — integer, used for the verification filename
18
+ - Access to Playwright MCP browser tools
15
19
 
16
20
  ## Output
17
21
  Append a `## Browser QA` section to `.planning/phase-{N}-verification.md` with PASS/FAIL per check.
@@ -48,6 +48,8 @@ Don't duplicate full documents. Summarize the 3-5 most important items from each
48
48
 
49
49
  This is the most important section. Suggest the **full milestone arc**, not just a v1 phase list.
50
50
 
51
+ **Evidence requirement:** Every milestone suggestion MUST cite at least one research finding from STACK.md, FEATURES.md, ARCHITECTURE.md, or PITFALLS.md as justification. Format the citation as `[DIMENSION.md: <specific finding or item>]` — e.g., `[FEATURES.md: table-stakes AUTH-*]` or `[PITFALLS.md: risk P3, stall risk for downstream milestones]`. Milestones without a citable finding are speculative — mark them explicitly with `[speculative — no source]` and the roadmapper will scrutinize.
52
+
51
53
  Based on:
52
54
  - FEATURES.md split (table stakes = v1 across milestones, differentiators = later milestones or post-handoff)
53
55
  - ARCHITECTURE.md build order → what depends on what, which foundation must land in Milestone 1 to support final-milestone requirements
@@ -17,6 +17,10 @@ You receive from the orchestrator:
17
17
  - `<milestone_context>` — greenfield or subsequent
18
18
  - `<output_path>` — absolute path where you write your research file
19
19
 
20
+ ## Tool Budget
21
+
22
+ Maximum 8 external calls total per invocation: 3 Context7 queries + 3 WebFetch calls + 2 WebSearch queries. If you exhaust this budget, write what you have and mark remaining sections as `confidence: LOW`. Research is time-boxed, not exhaustive — a 10-minute deep dive with concrete sources beats a 30-minute wander.
23
+
20
24
  ## Output
21
25
 
22
26
  Write exactly ONE file to `<output_path>`, using the template matching your dimension:
@@ -17,6 +17,7 @@ You receive:
17
17
  - `.planning/research/SUMMARY.md` — research synthesis (optional — may not exist if research was skipped)
18
18
  - `.planning/config.json` — project config (`depth`, `template_type`)
19
19
  - User's confirmed feature scope (from the scoping conversation in qualia-new)
20
+ - `<full_detail>` — boolean (default `false`). When `true`, write full phase detail for EVERY milestone in ROADMAP.md, not just M1. Passed by the orchestrator when the user runs `/qualia-new --full-plan`.
20
21
 
21
22
  ## Output
22
23
 
@@ -85,14 +86,18 @@ Use the research SUMMARY.md as your starting point. Don't force-fit the template
85
86
  - **Phases** — 2-5 phases. For Milestone 1, include full detail (goal + success criteria). For M2..M{N-1}, names + one-line goals are enough (progressive detail — full detail gets written when that milestone opens). For Handoff, use the fixed 4-phase template.
86
87
  - **Requirements covered** — list the REQ-IDs this milestone delivers
87
88
 
88
- ### 4. Build ROADMAP.md — ONLY Milestone 1's phases (fully detailed)
89
+ ### 4. Build ROADMAP.md — Milestone 1's phases (progressive detail by default)
89
90
 
90
- The current milestone gets full phase detail. Future milestones stay as sketches in JOURNEY.md until they open.
91
+ Check the `<full_detail>` flag in your prompt:
91
92
 
92
- For each phase in Milestone 1:
93
+ **`full_detail=false` (default):** Only Milestone 1 gets full phase detail in ROADMAP.md. Future milestones stay as sketches in JOURNEY.md until they open. This matches progressive-detail planning and is the recommended default.
94
+
95
+ **`full_detail=true`:** Write full phase detail for EVERY milestone (M1..Handoff) in ROADMAP.md, sectioned by milestone. Use when the client wants a fully-committed plan at kickoff. Trade-off: M2+ detail often needs revision as M1 ships — flag this in your summary.
96
+
97
+ For each phase in the milestone(s) you're detailing:
93
98
  - **Name** + **goal** (one line)
94
99
  - **Success criteria** — 2-5 observable user-facing behaviors
95
- - **Requirements covered** — REQ-IDs from REQUIREMENTS.md Milestone 1 section
100
+ - **Requirements covered** — REQ-IDs from REQUIREMENTS.md section for that milestone
96
101
 
97
102
  ### 5. Validate Coverage
98
103
 
@@ -104,7 +109,8 @@ Before writing, verify:
104
109
  - [ ] Final milestone is literally named "Handoff" with the 4 standard phases
105
110
  - [ ] No milestone depends on a later milestone
106
111
  - [ ] Milestone 1 has full phase-level detail (goals + success criteria) ready for `/qualia-plan 1`
107
- - [ ] M2..M{N-1} have phase names + one-line goals (sketch, not full detail)
112
+ - [ ] If `full_detail=false` (default): M2..M{N-1} have phase names + one-line goals (sketch, not full detail)
113
+ - [ ] If `full_detail=true`: every milestone in ROADMAP.md has full phase detail; flag this mode explicitly in your summary output
108
114
 
109
115
  If any check fails, fix it. The orchestrator trusts your output.
110
116
 
@@ -11,10 +11,17 @@ You verify that a phase achieved its GOAL, not just completed its TASKS.
11
11
  **Critical mindset:** Do NOT trust claims about what was built. Summaries document what Claude SAID it did. You verify what ACTUALLY EXISTS in the code. These often differ.
12
12
 
13
13
  ## Input
14
- You receive: the phase plan with success criteria + access to the codebase.
14
+
15
+ - `<plan_path>` — path to `.planning/phase-{N}-plan.md`
16
+ - `<project_context>` — inlined `.planning/PROJECT.md` contents (for Quality scoring against project conventions)
17
+ - `<previous_verification>` (optional) — inlined `.planning/phase-{N}-verification.md` from a prior run
15
18
 
16
19
  ## Output
17
- Write `.planning/phase-{N}-verification.md` — PASS or FAIL with evidence.
20
+ Write `.planning/phase-{N}-verification.md` — PASS or FAIL with evidence. Apply the Grounding Protocol: every finding needs `file:line — "quoted"` evidence, no hedging, scores with rubric criterion citations.
21
+
22
+ ## Tool Budget
23
+
24
+ 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.
18
25
 
19
26
  ## Goal-Backward Verification
20
27
 
@@ -260,7 +267,19 @@ Phase goal: "Working real-time chat interface with message history."
260
267
 
261
268
  ## Design Verification (for phases with frontend work)
262
269
 
263
- If the phase involved UI/frontend tasks, add a **Design Quality** section to the report.
270
+ **Gate (run first):** Only execute this section if the phase touched frontend.
271
+
272
+ ```bash
273
+ # Is this a frontend phase?
274
+ FRONTEND=false
275
+ if grep -qE "\.tsx|\.jsx|\.css|\.scss|app/|components/|pages/|Persona:\s*(frontend|ux)" .planning/phase-{N}-plan.md 2>/dev/null; then
276
+ FRONTEND=true
277
+ fi
278
+ ```
279
+
280
+ If `FRONTEND=false`, write `Design Verification: N/A (no frontend tasks in phase)` in the report and skip the rest of this section. This saves ~40 greps on backend-only phases.
281
+
282
+ If `FRONTEND=true`, proceed. Add a **Design Quality** section to the report.
264
283
 
265
284
  First, read the project's DESIGN.md:
266
285
  ```bash
package/bin/cli.js CHANGED
@@ -144,8 +144,17 @@ const QUALIA_LEGACY_HOOK_FILES = [
144
144
  "block-env-edit.js", // removed in v3.2.0
145
145
  ];
146
146
 
147
- // 4 Qualia agents — only these are removed.
148
- const QUALIA_AGENT_FILES = ["planner.md", "builder.md", "verifier.md", "qa-browser.md"];
147
+ // 8 Qualia agents — only these are removed.
148
+ const QUALIA_AGENT_FILES = [
149
+ "planner.md",
150
+ "builder.md",
151
+ "verifier.md",
152
+ "qa-browser.md",
153
+ "plan-checker.md",
154
+ "researcher.md",
155
+ "research-synthesizer.md",
156
+ "roadmapper.md",
157
+ ];
149
158
 
150
159
  // 3 Qualia bin scripts.
151
160
  const QUALIA_BIN_FILES = ["state.js", "qualia-ui.js", "statusline.js"];
@@ -557,7 +566,7 @@ function cmdMigrate() {
557
566
 
558
567
  // Check PreToolUse hooks — ensure all critical hooks are present
559
568
  const requiredBashHooks = ["auto-update.js", "branch-guard.js", "pre-push.js", "pre-deploy-gate.js"];
560
- const requiredEditHooks = ["block-env-edit.js", "migration-guard.js"];
569
+ const requiredEditHooks = ["migration-guard.js"];
561
570
 
562
571
  if (!settings.hooks.PreToolUse) settings.hooks.PreToolUse = [];
563
572
 
@@ -649,7 +658,7 @@ function cmdMigrate() {
649
658
  if (!settings.mcpServers["next-devtools"]) {
650
659
  settings.mcpServers["next-devtools"] = {
651
660
  command: "npx",
652
- args: ["next-devtools-mcp@0.3.10"],
661
+ args: ["next-devtools-mcp@latest"],
653
662
  disabled: false,
654
663
  };
655
664
  changes++;
@@ -760,6 +769,109 @@ function cmdAnalytics() {
760
769
  console.log("");
761
770
  }
762
771
 
772
+ // ─── ERP Ping ───────────────────────────────────────────
773
+ // Synthetic POST to ERP /api/v1/reports to verify connectivity, auth key
774
+ // validity, and endpoint health. Uses a distinct dry_run=true flag in the
775
+ // payload so receivers can filter these out of real report views.
776
+
777
+ function cmdErpPing() {
778
+ banner();
779
+ console.log("");
780
+
781
+ const cfg = readConfig();
782
+ const erpUrl = (cfg.erp && cfg.erp.url) || "https://portal.qualiasolutions.net";
783
+ const erpEnabled = !(cfg.erp && cfg.erp.enabled === false);
784
+ const keyFile = path.join(CLAUDE_DIR, ".erp-api-key");
785
+
786
+ console.log(` ${DIM}URL:${RESET} ${WHITE}${erpUrl}${RESET}`);
787
+ console.log(` ${DIM}Enabled:${RESET} ${erpEnabled ? `${GREEN}yes${RESET}` : `${YELLOW}no (erp.enabled=false)${RESET}`}`);
788
+
789
+ let apiKey = "";
790
+ try {
791
+ apiKey = fs.readFileSync(keyFile, "utf8").trim();
792
+ } catch {}
793
+ if (!apiKey) {
794
+ console.log(` ${DIM}Key:${RESET} ${RED}missing${RESET} ${DIM}(${keyFile})${RESET}`);
795
+ console.log("");
796
+ console.log(` ${RED}✗ Cannot ping — no API key. Ask Fawzi for one.${RESET}`);
797
+ console.log("");
798
+ process.exit(1);
799
+ }
800
+ console.log(` ${DIM}Key:${RESET} ${GREEN}present${RESET} ${DIM}(${apiKey.length} bytes)${RESET}`);
801
+ console.log("");
802
+
803
+ if (!erpEnabled) {
804
+ console.log(` ${YELLOW}ERP is disabled in config. Enable with:${RESET}`);
805
+ console.log(` ${DIM} qualia-framework erp-ping --enable${RESET}`);
806
+ console.log("");
807
+ process.exit(1);
808
+ }
809
+
810
+ const payload = JSON.stringify({
811
+ project: "qualia-framework-erp-ping",
812
+ project_id: "ping",
813
+ team_id: "qualia-solutions",
814
+ client_report_id: "QS-PING-00",
815
+ phase: 0,
816
+ phase_name: "ping",
817
+ status: "setup",
818
+ milestone: 0,
819
+ milestone_name: "ping",
820
+ submitted_by: cfg.installed_by || "ping",
821
+ submitted_at: new Date().toISOString(),
822
+ notes: "ERP PING — synthetic connectivity test, safe to ignore",
823
+ dry_run: true,
824
+ });
825
+
826
+ const started = Date.now();
827
+ const r = spawnSync("curl", [
828
+ "-sS", "-X", "POST",
829
+ "-H", `Authorization: Bearer ${apiKey}`,
830
+ "-H", "Content-Type: application/json",
831
+ "-d", payload,
832
+ "--max-time", "10",
833
+ "-w", "\n__HTTP__%{http_code}",
834
+ `${erpUrl}/api/v1/reports`,
835
+ ], { encoding: "utf8", timeout: 12000 });
836
+
837
+ const duration = Date.now() - started;
838
+ const raw = (r.stdout || "") + (r.stderr || "");
839
+ const httpMatch = raw.match(/__HTTP__(\d+)/);
840
+ const httpCode = httpMatch ? httpMatch[1] : "—";
841
+ const body = raw.replace(/\n?__HTTP__\d+/, "").trim();
842
+
843
+ console.log(` ${DIM}Response:${RESET} ${WHITE}HTTP ${httpCode}${RESET} ${DIM}(${duration}ms)${RESET}`);
844
+ if (body) {
845
+ try {
846
+ const j = JSON.parse(body);
847
+ if (j.ok && j.report_id) {
848
+ console.log(` ${DIM}report_id:${RESET} ${GREEN}${j.report_id}${RESET}`);
849
+ }
850
+ if (!j.ok && j.error) {
851
+ console.log(` ${DIM}error:${RESET} ${RED}${j.error}${RESET} ${DIM}${j.message || ""}${RESET}`);
852
+ }
853
+ } catch {
854
+ console.log(` ${DIM}body:${RESET} ${WHITE}${body.slice(0, 200)}${RESET}`);
855
+ }
856
+ }
857
+ console.log("");
858
+
859
+ if (httpCode === "200") {
860
+ console.log(` ${GREEN}✓ ERP reachable, key valid, endpoint healthy.${RESET}`);
861
+ console.log("");
862
+ process.exit(0);
863
+ }
864
+ if (httpCode === "401") {
865
+ console.log(` ${RED}✗ API key rejected. Ask Fawzi for a fresh key.${RESET}`);
866
+ } else if (httpCode === "—") {
867
+ console.log(` ${RED}✗ No response — DNS, TLS, or network issue.${RESET}`);
868
+ } else {
869
+ console.log(` ${YELLOW}! Unexpected response. Check ERP status.${RESET}`);
870
+ }
871
+ console.log("");
872
+ process.exit(1);
873
+ }
874
+
763
875
  function cmdHelp() {
764
876
  banner();
765
877
  console.log("");
@@ -772,6 +884,7 @@ function cmdHelp() {
772
884
  console.log(` qualia-framework ${TEAL}team${RESET} Manage team members (${DIM}list|add|remove${RESET})`);
773
885
  console.log(` qualia-framework ${TEAL}traces${RESET} View recent hook telemetry`);
774
886
  console.log(` qualia-framework ${TEAL}analytics${RESET} Show outcome scoring & gap cycle stats`);
887
+ console.log(` qualia-framework ${TEAL}erp-ping${RESET} Verify ERP connectivity + API key`);
775
888
  console.log("");
776
889
  console.log(` ${WHITE}After install:${RESET}`);
777
890
  console.log(` ${TG}/qualia${RESET} What should I do next?`);
@@ -824,6 +937,10 @@ switch (cmd) {
824
937
  case "stats":
825
938
  cmdAnalytics();
826
939
  break;
940
+ case "erp-ping":
941
+ case "ping":
942
+ cmdErpPing();
943
+ break;
827
944
  default:
828
945
  cmdHelp();
829
946
  }
package/bin/install.js CHANGED
@@ -517,7 +517,18 @@ Client-specific preferences, design choices, and requirements. Loaded by \`/qual
517
517
  ok(".erp-api-key (from $QUALIA_ERP_KEY)");
518
518
  } else if (fs.existsSync(erpKeyFile)) {
519
519
  try { fs.chmodSync(erpKeyFile, 0o600); } catch {}
520
- ok(".erp-api-key (existing — preserved)");
520
+ // Sanity check: warn on a clearly-empty/placeholder key. Genuine tokens
521
+ // from the ERP are ≥ 20 bytes; under 10 is almost certainly a mistake.
522
+ try {
523
+ const existingKey = fs.readFileSync(erpKeyFile, "utf8").trim();
524
+ if (existingKey.length < 10) {
525
+ warn(`.erp-api-key exists but looks truncated (${existingKey.length} bytes) — verify with 'qualia-framework erp-ping'`);
526
+ } else {
527
+ ok(".erp-api-key (existing — preserved)");
528
+ }
529
+ } catch {
530
+ ok(".erp-api-key (existing — preserved)");
531
+ }
521
532
  } else {
522
533
  // Disable ERP in the config we just wrote.
523
534
  try {
@@ -673,7 +684,7 @@ Client-specific preferences, design choices, and requirements. Loaded by \`/qual
673
684
  if (!settings.mcpServers["next-devtools"]) {
674
685
  settings.mcpServers["next-devtools"] = {
675
686
  command: "npx",
676
- args: ["next-devtools-mcp@0.3.10"],
687
+ args: ["next-devtools-mcp@latest"],
677
688
  disabled: false,
678
689
  };
679
690
  ok("MCP: next-devtools (runtime error visibility for Next.js projects)");
package/bin/state.js CHANGED
@@ -194,6 +194,7 @@ function ensureLifetime(t) {
194
194
  if (typeof t.milestone !== "number") t.milestone = 1;
195
195
  if (typeof t.milestone_name !== "string") t.milestone_name = "";
196
196
  if (!Array.isArray(t.milestones)) t.milestones = [];
197
+ if (typeof t.report_seq !== "number") t.report_seq = 0;
197
198
  if (!t.lifetime || typeof t.lifetime !== "object") {
198
199
  t.lifetime = {
199
200
  tasks_completed: 0,
@@ -205,6 +206,26 @@ function ensureLifetime(t) {
205
206
  return t;
206
207
  }
207
208
 
209
+ // Parse JOURNEY.md and extract the human name of the Nth milestone.
210
+ // Matches headers like:
211
+ // ## Milestone 2 · Core Features
212
+ // ## Milestone 2 · Core Features [CURRENT]
213
+ // ## Milestone 5 · Handoff [FINAL]
214
+ // Returns "" if JOURNEY.md is absent or the milestone isn't in it.
215
+ function readNextMilestoneNameFromJourney(milestoneNum) {
216
+ try {
217
+ const journeyPath = path.join(PLANNING, "JOURNEY.md");
218
+ if (!fs.existsSync(journeyPath)) return "";
219
+ const content = fs.readFileSync(journeyPath, "utf8");
220
+ const re = new RegExp(`^##\\s+Milestone\\s+${milestoneNum}\\s*[·•-]\\s*([^\\n\\[]+)`, "m");
221
+ const m = content.match(re);
222
+ if (m && m[1]) return m[1].trim();
223
+ return "";
224
+ } catch {
225
+ return "";
226
+ }
227
+ }
228
+
208
229
  function readState() {
209
230
  try {
210
231
  return fs.readFileSync(STATE_FILE, "utf8");
@@ -1141,7 +1162,12 @@ function cmdCloseMilestone(opts) {
1141
1162
  t.lifetime.total_phases += (parseInt(t.total_phases) || 0);
1142
1163
  t.lifetime.last_closed_milestone = closedMilestone;
1143
1164
  t.milestone = closedMilestone + 1;
1144
- t.milestone_name = ""; // cleared; /qualia-milestone reads next one from JOURNEY.md
1165
+ // Try to pre-populate next milestone's name from JOURNEY.md so the ERP
1166
+ // tree view doesn't show a blank between close-milestone and the next
1167
+ // state.js init --force (which happens in /qualia-milestone step 7).
1168
+ // If JOURNEY.md is missing or unparseable, fall through with blank —
1169
+ // /qualia-milestone will still set it via init --force.
1170
+ t.milestone_name = readNextMilestoneNameFromJourney(t.milestone);
1145
1171
  t.last_updated = new Date().toISOString();
1146
1172
 
1147
1173
  writeTracking(t);
@@ -1238,6 +1264,27 @@ function cmdBackfillLifetime(opts) {
1238
1264
  });
1239
1265
  }
1240
1266
 
1267
+ // ─── Next Report ID ──────────────────────────────────────
1268
+ // Increments report_seq and returns the next QS-REPORT-NN id. Per-project
1269
+ // counter (lives in tracking.json). /qualia-report calls this to tag each
1270
+ // session report with a stable, human-readable client ID before POSTing
1271
+ // to the ERP. If --peek is passed, the next id is returned WITHOUT
1272
+ // incrementing — useful for --dry-run previews.
1273
+ function cmdNextReportId(opts) {
1274
+ const t = readTracking();
1275
+ if (!t) return output(fail("NO_PROJECT", "No .planning/ found."));
1276
+ ensureLifetime(t);
1277
+ const peek = !!opts.peek;
1278
+ const next = (parseInt(t.report_seq) || 0) + 1;
1279
+ const id = `QS-REPORT-${String(next).padStart(2, "0")}`;
1280
+ if (!peek) {
1281
+ t.report_seq = next;
1282
+ t.last_updated = new Date().toISOString();
1283
+ writeTracking(t);
1284
+ }
1285
+ output({ ok: true, action: "next-report-id", report_id: id, report_seq: next, peeked: peek });
1286
+ }
1287
+
1241
1288
  // ─── Output ──────────────────────────────────────────────
1242
1289
  function output(obj) {
1243
1290
  console.log(JSON.stringify(obj, null, 2));
@@ -1289,11 +1336,14 @@ try {
1289
1336
  case "backfill-lifetime":
1290
1337
  cmdBackfillLifetime(opts);
1291
1338
  break;
1339
+ case "next-report-id":
1340
+ cmdNextReportId(opts);
1341
+ break;
1292
1342
  default:
1293
1343
  output(
1294
1344
  fail(
1295
1345
  "UNKNOWN_COMMAND",
1296
- `Usage: state.js <check|transition|init|fix|validate-plan|close-milestone|backfill-lifetime> [--options]`
1346
+ `Usage: state.js <check|transition|init|fix|validate-plan|close-milestone|backfill-lifetime|next-report-id> [--options]`
1297
1347
  )
1298
1348
  );
1299
1349
  }