qualia-framework 3.4.0 → 4.0.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 (42) hide show
  1. package/README.md +96 -51
  2. package/agents/builder.md +25 -14
  3. package/agents/plan-checker.md +29 -16
  4. package/agents/planner.md +33 -24
  5. package/agents/research-synthesizer.md +25 -12
  6. package/agents/roadmapper.md +89 -84
  7. package/agents/verifier.md +11 -2
  8. package/bin/cli.js +13 -2
  9. package/bin/install.js +28 -5
  10. package/bin/qualia-ui.js +267 -1
  11. package/bin/state.js +377 -52
  12. package/bin/statusline.js +40 -20
  13. package/docs/erp-contract.md +23 -2
  14. package/guide.md +84 -21
  15. package/hooks/auto-update.js +54 -70
  16. package/hooks/branch-guard.js +64 -6
  17. package/hooks/migration-guard.js +85 -10
  18. package/hooks/pre-compact.js +28 -4
  19. package/hooks/pre-deploy-gate.js +46 -6
  20. package/hooks/pre-push.js +94 -27
  21. package/hooks/session-start.js +6 -0
  22. package/package.json +1 -1
  23. package/skills/qualia/SKILL.md +3 -1
  24. package/skills/qualia-build/SKILL.md +40 -5
  25. package/skills/qualia-handoff/SKILL.md +87 -12
  26. package/skills/qualia-idk/SKILL.md +155 -3
  27. package/skills/qualia-map/SKILL.md +4 -4
  28. package/skills/qualia-milestone/SKILL.md +122 -79
  29. package/skills/qualia-new/SKILL.md +151 -230
  30. package/skills/qualia-optimize/SKILL.md +4 -4
  31. package/skills/qualia-plan/SKILL.md +14 -9
  32. package/skills/qualia-quick/SKILL.md +1 -1
  33. package/skills/qualia-report/SKILL.md +12 -0
  34. package/skills/qualia-verify/SKILL.md +59 -5
  35. package/templates/help.html +98 -31
  36. package/templates/journey.md +113 -0
  37. package/templates/plan.md +56 -11
  38. package/templates/requirements.md +82 -22
  39. package/templates/roadmap.md +41 -14
  40. package/templates/tracking.json +12 -1
  41. package/tests/runner.js +560 -0
  42. package/tests/state.test.sh +40 -0
@@ -1,30 +1,34 @@
1
1
  ---
2
2
  name: qualia-roadmapper
3
- description: Creates REQUIREMENTS.md (v1 requirements with REQ-IDs) and ROADMAP.md (phases mapped to requirements) from PROJECT.md and research. Spawned by qualia-new after research completes.
4
- tools: Read, Write
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
+ tools: Read, Write, Bash
5
5
  ---
6
6
 
7
7
  # Qualia Roadmapper
8
8
 
9
- You create two files: `REQUIREMENTS.md` (v1 requirements with REQ-IDs) and `ROADMAP.md` (phases mapped to requirements). You work from PROJECT.md + research SUMMARY.md. You don't run research yourself that's already done.
9
+ 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.
10
+
11
+ You do NOT run research — that's already done upstream.
10
12
 
11
13
  ## Input
12
14
 
13
15
  You receive:
14
16
  - `.planning/PROJECT.md` — core value, constraints, what they're building
15
- - `.planning/research/SUMMARY.md` — research synthesis with suggested phase structure (optional — may not exist if research was skipped)
16
- - `.planning/config.json` — project config including `depth` (quick | standard | comprehensive)
17
+ - `.planning/research/SUMMARY.md` — research synthesis (optional — may not exist if research was skipped)
18
+ - `.planning/config.json` — project config (`depth`, `template_type`)
17
19
  - User's confirmed feature scope (from the scoping conversation in qualia-new)
18
20
 
19
21
  ## Output
20
22
 
21
- Write two files:
22
- - `.planning/REQUIREMENTS.md` using template `~/.claude/qualia-templates/requirements.md`
23
- - `.planning/ROADMAP.md` using template `~/.claude/qualia-templates/roadmap.md`
23
+ Write THREE files:
24
+
25
+ 1. `.planning/JOURNEY.md` — the full arc (all milestones) using `~/.claude/qualia-templates/journey.md`
26
+ 2. `.planning/REQUIREMENTS.md` — v1 requirements grouped by milestone, using `~/.claude/qualia-templates/requirements.md`
27
+ 3. `.planning/ROADMAP.md` — **only the current (first) milestone's phase detail**, using `~/.claude/qualia-templates/roadmap.md`
24
28
 
25
- Also update `.planning/STATE.md` via `state.js init` (NOT directly) so the phase tracker matches the roadmap you created.
29
+ Then update `.planning/STATE.md` via `state.js init` (NOT directly) so the state machine matches Milestone 1's phases.
26
30
 
27
- ## How to Build the Roadmap
31
+ ## How to Build the Journey
28
32
 
29
33
  ### 1. Read Context
30
34
 
@@ -32,126 +36,127 @@ Also update `.planning/STATE.md` via `state.js init` (NOT directly) so the phase
32
36
  Read: .planning/PROJECT.md
33
37
  Read: .planning/research/SUMMARY.md (if exists)
34
38
  Read: .planning/config.json
39
+ Read: ~/.claude/qualia-templates/journey.md
35
40
  Read: ~/.claude/qualia-templates/requirements.md
36
41
  Read: ~/.claude/qualia-templates/roadmap.md
37
42
  ```
38
43
 
39
- ### 2. Build REQUIREMENTS.md First
44
+ ### 2. Build REQUIREMENTS.md — grouped by milestone
40
45
 
41
- Before defining phases, define what "done" means as a list of atomic, testable requirements.
46
+ Define what "done" means as atomic, testable requirements.
42
47
 
43
48
  **Format:** `{CATEGORY}-{NUMBER}` — `AUTH-01`, `CONT-02`, `SOCIAL-03`
44
49
 
45
- **Categories** come from:
46
- - Research FEATURES.md categories (if research exists)
47
- - User's confirmed feature scope from the scoping conversation
48
- - Common sense: Authentication, Content, Social, Notifications, Admin, etc.
49
-
50
50
  **Each requirement is:**
51
- - **Specific and testable:** "User can reset password via email link" (not "handle password reset")
52
51
  - **User-centric:** "User can X" (not "System does Y")
53
- - **Atomic:** One capability per requirement
54
- - **Independent:** Minimal dependencies on other requirements
52
+ - **Atomic:** one capability per requirement
53
+ - **Testable:** you can name the observable behavior
54
+ - **Assigned to exactly one milestone**
55
55
 
56
- Put v1 requirements under `## v1 Requirements` grouped by category.
57
- Put deferred features under `## v2 Requirements`.
58
- Put explicit exclusions under `## Out of Scope` with reasoning.
56
+ Organize requirements under `## Milestone 1 · {Name}`, `## Milestone 2 · {Name}`, ..., `## Handoff` sections. Put deferred features under `## Post-Handoff (v2)` and exclusions under `## Out of Scope`.
59
57
 
60
- ### 3. Derive Phases
58
+ ### 3. Derive the Milestone Arc (JOURNEY.md)
61
59
 
62
- **Rules:**
63
- 1. **Feature phases only.** Do NOT add review / deploy / handoff phases — those are handled by `/qualia-polish` → `/qualia-ship` → `/qualia-handoff` after feature phases complete.
64
- 2. **Phase count depends on `depth` config:**
65
- - `quick`: 3-5 phases
66
- - `standard`: 5-8 phases
67
- - `comprehensive`: 7-12 phases
68
- 3. **Each phase is independently verifiable.** A phase completes when its success criteria are observable in a running app.
69
- 4. **Each v1 requirement maps to exactly ONE phase.** No duplicates, no gaps.
70
- 5. **Order by dependency, not priority.** Phase 2 should be able to use Phase 1's outputs.
60
+ This is the most important step.
71
61
 
72
- **Typical phase shapes:**
62
+ **Hard rules:**
63
+ - **Ceiling: 5 milestones** (including Handoff). If the project needs more, defer remainder to post-handoff v2.
64
+ - **Floor: 2 milestones** (one feature milestone + Handoff). If smaller, the project should use `/qualia-new --quick` instead.
65
+ - **Final milestone is ALWAYS "Handoff"** with 4 standard phases: Polish, Content + SEO, Final QA, Handoff (credentials + walkthrough + domain transfer).
66
+ - **Every non-Handoff milestone must have ≥ 2 phases** OR be an explicit shipped release gate. Single-phase milestones are phases, not milestones — merge them into the preceding milestone.
67
+ - **Milestones are ordered by dependency, not priority.** M2 must be able to use M1's outputs.
73
68
 
74
- - **Phase 1: Foundation** DB schema, auth, base layout, deploy pipeline
75
- - **Phase 2-4: Core features** — the main value-delivering capabilities
76
- - **Phase N-1: Content / UX polish** — copy, media, responsive, animations
77
- - **Phase N: Final polish** — SEO, analytics, performance, a11y
69
+ **Typical milestone arcs by project type:**
78
70
 
79
- But don't force-fit this template. Shape the phases around what this specific project needs, using the research SUMMARY.md as your starting point.
71
+ | Type | Arc |
72
+ |---|---|
73
+ | Landing / marketing | 2 milestones: Foundation → Handoff |
74
+ | SaaS / dashboard | 4 milestones: Foundation → Core Features → Admin & Reporting → Handoff |
75
+ | Voice / AI agent | 4 milestones: Foundation → Core Flow → Integrations → Handoff |
76
+ | Mobile app | 5 milestones: Foundation → Core → Offline & Notifications → Store Prep → Handoff |
77
+ | Multi-tenant platform | 5 milestones: Foundation → Core → Admin → Scale → Handoff |
80
78
 
81
- ### 4. Derive Success Criteria per Phase
79
+ Use the research SUMMARY.md as your starting point. Don't force-fit the template — shape to this specific project.
82
80
 
83
- For each phase, write 2-5 success criteria. Each must be:
84
- - **Observable** — someone running the app can see it work
85
- - **User-centric** — "user can X" not "code does Y"
86
- - **Phase-specific** — not generic ("tests pass" applies to every phase)
81
+ **For each milestone:**
82
+ - **Name** — short and evocative (e.g., "Core Feature Loop", not "Phase 2 Work")
83
+ - **Why now** — one sentence explaining why this milestone comes *after* the previous and *before* the next. In plain language a non-technical team member understands.
84
+ - **Exit criteria** — 2-3 observable outcomes that define "shipped" for this milestone
85
+ - **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
+ - **Requirements covered** — list the REQ-IDs this milestone delivers
87
87
 
88
- **Example (good):**
89
- - User can sign up with email and receive verification email
90
- - User can log in and stay logged in across browser refresh
91
- - User can log out from any page
88
+ ### 4. Build ROADMAP.md — ONLY Milestone 1's phases (fully detailed)
92
89
 
93
- **Example (bad too vague):**
94
- - Authentication works
95
- - Tests pass
96
- - Code is clean
90
+ The current milestone gets full phase detail. Future milestones stay as sketches in JOURNEY.md until they open.
91
+
92
+ For each phase in Milestone 1:
93
+ - **Name** + **goal** (one line)
94
+ - **Success criteria** — 2-5 observable user-facing behaviors
95
+ - **Requirements covered** — REQ-IDs from REQUIREMENTS.md Milestone 1 section
97
96
 
98
97
  ### 5. Validate Coverage
99
98
 
100
- Before writing the files, verify:
101
- - [ ] Every v1 requirement maps to exactly one phase
102
- - [ ] Every phase has 2-5 success criteria
103
- - [ ] No phase depends on a later phase
104
- - [ ] Phase count is within the range for the `depth` config
105
- - [ ] No "review" / "deploy" / "handoff" phases
99
+ Before writing, verify:
100
+ - [ ] Every v1 requirement (all milestones excluding Handoff) has a REQ-ID
101
+ - [ ] Every v1 requirement maps to exactly one milestone
102
+ - [ ] Every milestone has 2 phases (except Handoff which has the fixed 4)
103
+ - [ ] Milestone count is 2-5 total
104
+ - [ ] Final milestone is literally named "Handoff" with the 4 standard phases
105
+ - [ ] No milestone depends on a later milestone
106
+ - [ ] 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)
106
108
 
107
- If any requirement is unmapped, the roadmap is incomplete. Either add it to a phase or explicitly move it to v2.
109
+ If any check fails, fix it. The orchestrator trusts your output.
108
110
 
109
111
  ### 6. Write the Files
110
112
 
111
- Write both files to `.planning/`. Use the templates as structural guides. Fill in every `{placeholder}` with concrete content.
113
+ Write all three files to `.planning/`. Fill every `{placeholder}` with concrete content.
112
114
 
113
115
  ### 7. Update STATE.md via state.js
114
116
 
115
- **Do not edit STATE.md directly.** Call the state machine:
117
+ **Do not edit STATE.md directly.** Call the state machine with Milestone 1's phases:
116
118
 
117
119
  ```bash
118
120
  node ~/.claude/bin/state.js init \
119
121
  --project "{project name from PROJECT.md}" \
120
122
  --client "{client from PROJECT.md}" \
121
123
  --type "{type from PROJECT.md}" \
122
- --phases '<JSON array of {name, goal} objects>' \
123
- --total_phases {N}
124
+ --milestone_name "{Milestone 1 name}" \
125
+ --phases '<JSON array of {name, goal} objects for Milestone 1 only>' \
126
+ --total_phases {count of Milestone 1 phases}
124
127
  ```
125
128
 
126
- This ensures STATE.md + tracking.json stay consistent and the status bar updates correctly.
129
+ `--milestone_name` is the human name of Milestone 1 (e.g. "Foundation"). tracking.json records it so the status bar and ERP tree render correctly.
127
130
 
128
131
  ### 8. Return a Summary
129
132
 
130
- Report back to the orchestrator:
131
-
132
133
  ```
133
- Wrote: .planning/REQUIREMENTS.md ({X} v1 requirements, {Y} categories)
134
- Wrote: .planning/ROADMAP.md ({N} phases, 100% coverage)
135
- Wrote: .planning/STATE.md (via state.js init)
136
-
137
- Phase summary:
138
- 1. {name} — {REQ-IDs}
139
- 2. {name} — {REQ-IDs}
134
+ Wrote: .planning/JOURNEY.md ({N} milestones to handoff)
135
+ Wrote: .planning/REQUIREMENTS.md ({X} v1 requirements, {Y} categories, grouped across {N-1} feature milestones + Handoff)
136
+ Wrote: .planning/ROADMAP.md (Milestone 1: {name} — {P} phases, ready for /qualia-plan 1)
137
+ Wrote: .planning/STATE.md (via state.js init, milestone_name={Milestone 1 name})
138
+
139
+ Journey:
140
+ M1. {Name} — {REQ-IDs}, {P} phases [CURRENT, full detail]
141
+ M2. {Name} — {REQ-IDs}, {P} phases [sketched]
140
142
  ...
143
+ M{N}. Handoff — 4 phases [standard]
141
144
 
142
- Research flags: {count} phases may need deeper research during planning
145
+ Research flags: {count} milestones may need deeper research during planning
143
146
  ```
144
147
 
145
- ## Quality Gates
148
+ ## Quality Gates (self-check)
146
149
 
147
- Before returning, self-check:
150
+ Before returning:
148
151
 
149
- - [ ] Every v1 requirement has a REQ-ID in correct format
150
- - [ ] Every v1 requirement maps to exactly one phase
151
- - [ ] Every phase has 2-5 success criteria (observable, user-centric)
152
- - [ ] No phase depends on a later phase
153
- - [ ] No non-feature phases (no review/deploy/handoff)
154
- - [ ] STATE.md was updated via state.js, not directly
155
- - [ ] Requirements traceability table is populated
152
+ - [ ] JOURNEY.md exists with all {N} milestones and exit criteria per milestone
153
+ - [ ] REQUIREMENTS.md exists, requirements grouped by milestone, REQ-IDs present
154
+ - [ ] ROADMAP.md exists with Milestone 1's phase detail
155
+ - [ ] Final milestone is literally named "Handoff" with Polish / Content + SEO / Final QA / Handoff phases
156
+ - [ ] Every feature milestone has ≥ 2 phases
157
+ - [ ] Milestone count is between 2 and 5
158
+ - [ ] STATE.md was updated via `state.js init` with `--milestone_name`, never edited by hand
159
+ - [ ] M1's phases are fully detailed (goals + success criteria ready for planner)
160
+ - [ ] M2..M{N-1} are sketched (phase names + one-line goals, detail later)
156
161
 
157
- If any check fails, fix it before returning. The orchestrator trusts your output don't return half-baked roadmaps.
162
+ If any check fails, fix it before returning. Incomplete roadmaps cost downstream time and cascade errors into every phase that follows.
@@ -71,9 +71,18 @@ If the plan has no `## Verification Contract` section (older plans), skip this s
71
71
  ## How to Verify
72
72
 
73
73
  ### 1. Read the Plan
74
- Extract success criteria from the phase plan's `## Success Criteria` section. Also extract the `## Verification Contract` if present.
75
74
 
76
- ### 2. For Each Criterion, Run the 3-Level Check
75
+ Extract THREE layers of truth from the plan file:
76
+
77
+ 1. **Phase-level truths** — the `## Success Criteria` section
78
+ 2. **Task-level Acceptance Criteria** — the `**Acceptance Criteria:**` bullets inside each `## Task N` block. These describe user-observable behavior PER TASK and should all be true.
79
+ 3. **Verification Contract** — the `## Verification Contract` section with testable commands.
80
+
81
+ Contracts (layer 3) take priority because they're machine-executable. Acceptance Criteria (layer 2) are the bridge between task and phase — if all AC across all tasks pass, the phase success criteria should follow.
82
+
83
+ ### 2. For Each Criterion (Phase + Per-Task AC), Run the 3-Level Check
84
+
85
+ First, walk every task's Acceptance Criteria. For each AC, ask: does the code produce this observable behavior? Grep the artifacts, trace the wiring, inspect the route handler. Then run the 3-level check below on each phase-level Success Criterion.
77
86
 
78
87
  ```bash
79
88
  # Level 2: Does the file exist?
package/bin/cli.js CHANGED
@@ -564,9 +564,19 @@ function cmdMigrate() {
564
564
  }
565
565
  if (!bashEntry.hooks) bashEntry.hooks = [];
566
566
 
567
+ // Compare by basename, not by absolute-path substring. If the user moved
568
+ // ~ between OS reinstalls, the OLD path embedded in settings.json no
569
+ // longer matches the NEW path and migrate would otherwise append a
570
+ // duplicate entry on every re-run.
571
+ const extractScriptName = (command) => {
572
+ const match = command && command.match(/["']([^"']+\.js)["']/);
573
+ return match ? path.basename(match[1]) : null;
574
+ };
575
+
567
576
  for (const hookFile of requiredBashHooks) {
568
577
  const cmd = nodeCmd(hookFile);
569
- const exists = bashEntry.hooks.some(h => h.command && h.command.includes(hookFile));
578
+ const targetName = path.basename(hookFile);
579
+ const exists = bashEntry.hooks.some(h => extractScriptName(h.command) === targetName);
570
580
  if (!exists) {
571
581
  const hookDef = { type: "command", command: cmd, timeout: hookFile === "pre-deploy-gate.js" ? 180 : 5 };
572
582
  if (hookFile === "branch-guard.js") hookDef.if = "Bash(git push*)";
@@ -588,7 +598,8 @@ function cmdMigrate() {
588
598
 
589
599
  for (const hookFile of requiredEditHooks) {
590
600
  const cmd = nodeCmd(hookFile);
591
- const exists = editEntry.hooks.some(h => h.command && h.command.includes(hookFile));
601
+ const targetName = path.basename(hookFile);
602
+ const exists = editEntry.hooks.some(h => extractScriptName(h.command) === targetName);
592
603
  if (!exists) {
593
604
  const hookDef = { type: "command", command: cmd, timeout: hookFile === "migration-guard.js" ? 10 : 5 };
594
605
  if (hookFile === "migration-guard.js") hookDef.if = "Edit(*migration*)|Write(*migration*)|Edit(*.sql)|Write(*.sql)";
package/bin/install.js CHANGED
@@ -497,16 +497,39 @@ Client-specific preferences, design choices, and requirements. Loaded by \`/qual
497
497
  api_key_file: ".erp-api-key",
498
498
  },
499
499
  };
500
- fs.writeFileSync(configFile, JSON.stringify(config, null, 2) + "\n");
500
+ // mode 0o600: this file holds the role bit (OWNER vs EMPLOYEE) which the
501
+ // branch-guard hook trusts. Default 0644 would let any local user edit it
502
+ // and self-elevate.
503
+ fs.writeFileSync(configFile, JSON.stringify(config, null, 2) + "\n", { mode: 0o600 });
504
+ try { fs.chmodSync(configFile, 0o600); } catch {}
501
505
 
502
506
  // ─── ERP API key (for report uploads) ──────────────────
507
+ // Per-user keys, never a hardcoded shared default. Sources, in order:
508
+ // 1. $QUALIA_ERP_KEY env var at install time (CI / scripted installs)
509
+ // 2. Existing ~/.claude/.erp-api-key (preserved across re-installs)
510
+ // 3. Skip — ERP disabled in config until user runs `qualia-framework set-erp-key`
503
511
  printSection("ERP Integration");
504
512
  const erpKeyFile = path.join(CLAUDE_DIR, ".erp-api-key");
505
- if (!fs.existsSync(erpKeyFile)) {
506
- fs.writeFileSync(erpKeyFile, "qualia-claude-2026", { mode: 0o600 });
507
- ok(".erp-api-key (created)");
513
+ const envKey = (process.env.QUALIA_ERP_KEY || "").trim();
514
+ if (envKey) {
515
+ fs.writeFileSync(erpKeyFile, envKey, { mode: 0o600 });
516
+ try { fs.chmodSync(erpKeyFile, 0o600); } catch {}
517
+ ok(".erp-api-key (from $QUALIA_ERP_KEY)");
518
+ } else if (fs.existsSync(erpKeyFile)) {
519
+ try { fs.chmodSync(erpKeyFile, 0o600); } catch {}
520
+ ok(".erp-api-key (existing — preserved)");
508
521
  } else {
509
- ok(".erp-api-key (exists)");
522
+ // Disable ERP in the config we just wrote.
523
+ try {
524
+ const cfg = JSON.parse(fs.readFileSync(configFile, "utf8"));
525
+ cfg.erp = { ...(cfg.erp || {}), enabled: false };
526
+ fs.writeFileSync(configFile, JSON.stringify(cfg, null, 2) + "\n", { mode: 0o600 });
527
+ try { fs.chmodSync(configFile, 0o600); } catch {}
528
+ } catch {}
529
+ log(`${YELLOW}!${RESET} ERP key not configured — reports won't upload until set.`);
530
+ log(`${DIM} Set with:${RESET} ${TEAL}export QUALIA_ERP_KEY=...${RESET} ${DIM}then re-install,${RESET}`);
531
+ log(`${DIM} or write the key to:${RESET} ${WHITE}${erpKeyFile}${RESET} ${DIM}(mode 0600).${RESET}`);
532
+ log(`${DIM} Get a key from Fawzi.${RESET}`);
510
533
  }
511
534
 
512
535
  // ─── Configure settings.json ───────────────────────────
package/bin/qualia-ui.js CHANGED
@@ -17,6 +17,9 @@
17
17
  // next <command> — "Run: /qualia-X" footer
18
18
  // end <status> [next-command] — closing banner with optional next
19
19
  // update <current> <latest> — sticky framework update banner
20
+ // plan-summary <path/to/plan.md> — story-file dashboard for a plan
21
+ // journey-tree [path/to/JOURNEY.md] — ladder view of all milestones, current highlighted
22
+ // milestone-complete <num> <name> <next> — celebration banner on milestone close
20
23
 
21
24
  const fs = require("fs");
22
25
  const path = require("path");
@@ -63,6 +66,11 @@ const ACTIONS = {
63
66
  welcome: { label: "WELCOME", glyph: "⬢" },
64
67
  test: { label: "TESTING", glyph: "⊡" },
65
68
  analytics: { label: "ANALYTICS", glyph: "◈" },
69
+ milestone: { label: "MILESTONE", glyph: "◆" },
70
+ journey: { label: "JOURNEY", glyph: "◯" },
71
+ auto: { label: "AUTO MODE", glyph: "⚡" },
72
+ research: { label: "RESEARCH", glyph: "◱" },
73
+ roadmap: { label: "ROADMAP", glyph: "◐" },
66
74
  };
67
75
 
68
76
  // ─── State Reading ───────────────────────────────────────
@@ -259,6 +267,261 @@ function cmdEnd(status, nextCmd) {
259
267
  console.log("");
260
268
  }
261
269
 
270
+ // ─── Journey Tree (the North Star visualization) ────────
271
+ // Renders JOURNEY.md as an ASCII ladder with the current milestone highlighted.
272
+ // Called after /qualia-new to show the full arc, and by /qualia (router) to
273
+ // orient the user on "you are here".
274
+ function cmdJourneyTree(journeyPath) {
275
+ const p = journeyPath || ".planning/JOURNEY.md";
276
+ let content = "";
277
+ try {
278
+ content = fs.readFileSync(p, "utf8");
279
+ } catch {
280
+ console.log(` ${DIM}No JOURNEY.md at ${p}${RESET}`);
281
+ return;
282
+ }
283
+
284
+ const state = readState();
285
+ const currentMilestone = state && state.ok ? (state.milestone || 1) : 1;
286
+
287
+ // Parse milestone blocks: "## Milestone N · Name" or "## Milestone N · Handoff"
288
+ const milestoneRe = /^## Milestone (\d+)\s*·\s*(.+?)\s*(?:\[[^\]]*\])?\r?$/gm;
289
+ const milestones = [];
290
+ let m;
291
+ while ((m = milestoneRe.exec(content)) !== null) {
292
+ const num = parseInt(m[1]);
293
+ const name = m[2].trim();
294
+ // Extract the section body to pull Why-now and phases
295
+ const startIdx = m.index + m[0].length;
296
+ const nextMatch = milestoneRe.exec(content);
297
+ const endIdx = nextMatch ? nextMatch.index : content.length;
298
+ milestoneRe.lastIndex = startIdx; // rewind for next iteration
299
+ const body = content.slice(startIdx, endIdx);
300
+
301
+ const whyMatch = body.match(/\*\*Why now:\*\*\s*(.+?)\r?$/m);
302
+ const why = whyMatch ? whyMatch[1].trim() : "";
303
+
304
+ const phaseNames = [];
305
+ const phaseRe = /^\d+\.\s+\*\*([^*]+)\*\*/gm;
306
+ let pm;
307
+ while ((pm = phaseRe.exec(body)) !== null) {
308
+ phaseNames.push(pm[1].trim());
309
+ }
310
+
311
+ milestones.push({ num, name, why, phaseNames });
312
+ if (nextMatch) milestoneRe.lastIndex = nextMatch.index;
313
+ else break;
314
+ }
315
+
316
+ if (milestones.length === 0) {
317
+ console.log(` ${DIM}JOURNEY.md has no milestones to render${RESET}`);
318
+ return;
319
+ }
320
+
321
+ // Project name from frontmatter if present
322
+ const projMatch = content.match(/^project:\s*"?(.+?)"?\s*$/m);
323
+ const projectName = projMatch ? projMatch[1] : projectName();
324
+
325
+ console.log("");
326
+ console.log(` ${TEAL}${BOLD}◯${RESET} ${WHITE}${BOLD}JOURNEY${RESET} ${DIM}▸${RESET} ${WHITE}${projectName}${RESET}`);
327
+ console.log(` ${RULE_DIM}`);
328
+ console.log(` ${DIM}${milestones.length} milestones · currently at M${currentMilestone}${RESET}`);
329
+ console.log("");
330
+
331
+ for (let i = 0; i < milestones.length; i++) {
332
+ const ms = milestones[i];
333
+ const isCurrent = ms.num === currentMilestone;
334
+ const isPast = ms.num < currentMilestone;
335
+ const isFuture = ms.num > currentMilestone;
336
+ const isHandoff = /handoff/i.test(ms.name);
337
+
338
+ let marker;
339
+ let labelColor;
340
+ let connector = "│";
341
+
342
+ if (isPast) {
343
+ marker = `${GREEN}●${RESET}`;
344
+ labelColor = DIM;
345
+ } else if (isCurrent) {
346
+ marker = `${TEAL}${BOLD}◆${RESET}`;
347
+ labelColor = TEAL + BOLD;
348
+ } else {
349
+ marker = `${DIM2}○${RESET}`;
350
+ labelColor = WHITE;
351
+ }
352
+
353
+ const tag = isCurrent
354
+ ? ` ${TEAL}${BOLD}[CURRENT]${RESET}`
355
+ : isPast
356
+ ? ` ${GREEN}[shipped]${RESET}`
357
+ : isHandoff
358
+ ? ` ${DIM2}[FINAL]${RESET}`
359
+ : "";
360
+
361
+ console.log(` ${marker} ${labelColor}M${ms.num} · ${ms.name}${RESET}${tag}`);
362
+
363
+ if (ms.why && (isCurrent || isFuture)) {
364
+ const shortWhy = ms.why.length > 80 ? ms.why.slice(0, 77) + "…" : ms.why;
365
+ console.log(` ${DIM2}│${RESET} ${DIM}${shortWhy}${RESET}`);
366
+ }
367
+
368
+ if (ms.phaseNames.length > 0 && (isCurrent || isHandoff)) {
369
+ const phaseList = ms.phaseNames.slice(0, 4).join(` ${DIM2}→${RESET} ${DIM}`);
370
+ console.log(` ${DIM2}│${RESET} ${DIM}${phaseList}${DIM}${ms.phaseNames.length > 4 ? ` +${ms.phaseNames.length - 4}` : ""}${RESET}`);
371
+ }
372
+
373
+ // Connector between milestones (skip after last)
374
+ if (i < milestones.length - 1) {
375
+ console.log(` ${DIM2}│${RESET}`);
376
+ }
377
+ }
378
+ console.log("");
379
+ console.log(` ${RULE_DIM}`);
380
+ }
381
+
382
+ // ─── Milestone Complete (celebration banner) ─────────────
383
+ // Shown at milestone-boundary in auto mode, and by /qualia-milestone manually.
384
+ function cmdMilestoneComplete(num, name, nextName) {
385
+ console.log("");
386
+ console.log(` ${GREEN}${BOLD}◆${RESET} ${WHITE}${BOLD}MILESTONE ${num} SHIPPED${RESET} ${DIM}·${RESET} ${TEAL}${name || ""}${RESET}`);
387
+ console.log(` ${RULE_DIM}`);
388
+ if (nextName) {
389
+ if (/handoff/i.test(nextName)) {
390
+ console.log(` ${DIM}Next${RESET} ${TEAL}${BOLD}${nextName}${RESET} ${DIM}· the final milestone${RESET}`);
391
+ } else {
392
+ console.log(` ${DIM}Next${RESET} ${WHITE}${nextName}${RESET}`);
393
+ }
394
+ } else {
395
+ console.log(` ${GREEN}${BOLD}PROJECT COMPLETE${RESET} ${DIM}· last milestone reached${RESET}`);
396
+ }
397
+ console.log(` ${RULE_DIM}`);
398
+ console.log("");
399
+ }
400
+
401
+ // ─── Plan Summary (story-file dashboard) ─────────────────
402
+ // Renders a polished overview of a plan file: phase goal, tasks grouped by wave,
403
+ // persona chips, dependency lines, AC count, validation count. Called by
404
+ // /qualia-plan after the planner and plan-checker finish.
405
+ function cmdPlanSummary(planPath) {
406
+ if (!planPath) {
407
+ console.error("Usage: qualia-ui.js plan-summary <path-to-plan.md>");
408
+ process.exit(1);
409
+ }
410
+ let content = "";
411
+ try {
412
+ content = fs.readFileSync(planPath, "utf8");
413
+ } catch (e) {
414
+ console.error(`Cannot read plan: ${e.message}`);
415
+ process.exit(1);
416
+ }
417
+
418
+ // ─ Parse frontmatter + phase header ─
419
+ const fmMatch = content.match(/^---\n([\s\S]+?)\n---/);
420
+ const fm = {};
421
+ if (fmMatch) {
422
+ for (const line of fmMatch[1].split("\n")) {
423
+ const m = line.match(/^(\w+):\s*(.+?)\s*$/);
424
+ if (m) fm[m[1]] = m[2].replace(/^["']|["']$/g, "");
425
+ }
426
+ }
427
+ const phaseNum = fm.phase || "?";
428
+ const phaseGoal = fm.goal || "";
429
+ const phaseTitleMatch = content.match(/^# Phase \d+:?\s*(.+?)\r?$/m);
430
+ const phaseTitle = phaseTitleMatch ? phaseTitleMatch[1].trim() : `Phase ${phaseNum}`;
431
+ const whyPhaseMatch = content.match(/^\*\*Why this phase:\*\*\s*(.+?)\r?$/m);
432
+ const whyPhase = whyPhaseMatch ? whyPhaseMatch[1].trim() : "";
433
+
434
+ // ─ Parse tasks ─
435
+ const taskBlocks = content.split(/^(?=## Task \d+)/m).filter((b) => /^## Task \d+/.test(b));
436
+ const tasks = taskBlocks.map((block) => {
437
+ const titleMatch = block.match(/^## Task (\d+)\s*—\s*(.+?)\r?$/m);
438
+ const wave = parseInt((block.match(/\*\*Wave:\*\*\s*(\d+)/) || [])[1]) || 1;
439
+ const persona = ((block.match(/\*\*Persona:\*\*\s*(.+?)\r?$/m) || [])[1] || "").trim();
440
+ const deps = ((block.match(/\*\*Depends on:\*\*\s*(.+?)\r?$/m) || [])[1] || "").trim();
441
+ const why = ((block.match(/\*\*Why:\*\*\s*([\s\S]+?)(?=\r?\n\*\*|\r?\n##|$)/) || [])[1] || "").trim().replace(/\s+/g, " ");
442
+ const acBlock = (block.match(/\*\*Acceptance Criteria:\*\*\s*([\s\S]+?)(?=\r?\n\*\*|\r?\n##|$)/) || [])[1] || "";
443
+ const acCount = (acBlock.match(/^[-*]\s+/gm) || []).length;
444
+ const validationBlock = (block.match(/\*\*Validation:\*\*[^\n]*\n([\s\S]+?)(?=\r?\n\*\*|\r?\n##|$)/) || [])[1] || "";
445
+ const validationCount = (validationBlock.match(/^[-*]\s+/gm) || []).length;
446
+ return {
447
+ num: titleMatch ? parseInt(titleMatch[1]) : 0,
448
+ title: titleMatch ? titleMatch[2].trim() : "",
449
+ wave,
450
+ persona: (() => {
451
+ // Strip placeholder syntax ({...}), then only accept the known set
452
+ const cleaned = persona.replace(/[{}]/g, "").trim().toLowerCase();
453
+ const valid = ["security", "architect", "ux", "frontend", "backend", "performance"];
454
+ return valid.includes(cleaned) ? cleaned : "";
455
+ })(),
456
+ deps,
457
+ why,
458
+ acCount,
459
+ validationCount,
460
+ };
461
+ });
462
+
463
+ const contractCount = (content.match(/^### Contract for Task \d+/gm) || []).length;
464
+ const totalWaves = tasks.length > 0 ? Math.max(...tasks.map((t) => t.wave)) : 0;
465
+
466
+ // ─ Render ─
467
+ console.log("");
468
+ console.log(` ${TEAL}${BOLD}▣${RESET} ${WHITE}${BOLD}PLAN${RESET} ${DIM}▸${RESET} ${WHITE}Phase ${phaseNum} — ${phaseTitle}${RESET}`);
469
+ console.log(` ${RULE_DIM}`);
470
+ if (phaseGoal) {
471
+ console.log(` ${DIM}Goal${RESET} ${WHITE}${phaseGoal}${RESET}`);
472
+ }
473
+ if (whyPhase) {
474
+ console.log(` ${DIM}Why${RESET} ${WHITE}${whyPhase}${RESET}`);
475
+ }
476
+ console.log(` ${DIM}Shape${RESET} ${TEAL}${tasks.length}${RESET} ${DIM}tasks${RESET} ${DIM}·${RESET} ${TEAL}${totalWaves}${RESET} ${DIM}waves${RESET} ${DIM}·${RESET} ${TEAL}${contractCount}${RESET} ${DIM}contracts${RESET}`);
477
+ console.log(` ${RULE_DIM}`);
478
+
479
+ // Persona palette
480
+ const personaColors = {
481
+ security: RED,
482
+ architect: BLUE,
483
+ ux: "\x1b[38;2;255;182;193m",
484
+ frontend: TEAL,
485
+ backend: "\x1b[38;2;186;85;211m",
486
+ performance: YELLOW,
487
+ };
488
+
489
+ for (let w = 1; w <= totalWaves; w++) {
490
+ const waveTasks = tasks.filter((t) => t.wave === w);
491
+ if (!waveTasks.length) continue;
492
+ console.log("");
493
+ console.log(` ${TEAL}»${RESET} ${WHITE}${BOLD}Wave ${w}${RESET} ${DIM}(${waveTasks.length} ${waveTasks.length === 1 ? "task" : "tasks"}, parallel)${RESET}`);
494
+ for (const t of waveTasks) {
495
+ const personaChip = t.persona
496
+ ? ` ${(personaColors[t.persona] || DIM)}[${t.persona}]${RESET}`
497
+ : "";
498
+ // Only show the dep chip if it names a real task reference.
499
+ // Suppress blanks, "none", and template placeholders like "{none | Task N}".
500
+ const depsClean = (t.deps || "").trim();
501
+ const depsIsReal =
502
+ depsClean &&
503
+ !/^none$/i.test(depsClean) &&
504
+ !/[{}]/.test(depsClean);
505
+ const depChip = depsIsReal ? ` ${DIM}← ${depsClean}${RESET}` : "";
506
+ console.log(` ${DIM}${t.num}.${RESET} ${WHITE}${t.title}${RESET}${personaChip}${depChip}`);
507
+ // Suppress placeholder Why text (contains {} braces) to keep the
508
+ // dashboard clean when the planner hasn't filled it in yet.
509
+ if (t.why && !/[{}]/.test(t.why)) {
510
+ const shortWhy = t.why.length > 90 ? t.why.slice(0, 87) + "…" : t.why;
511
+ console.log(` ${DIM}${shortWhy}${RESET}`);
512
+ }
513
+ const metrics = [];
514
+ if (t.acCount > 0) metrics.push(`${TEAL}${t.acCount}${RESET} ${DIM}AC${RESET}`);
515
+ if (t.validationCount > 0) metrics.push(`${TEAL}${t.validationCount}${RESET} ${DIM}checks${RESET}`);
516
+ if (metrics.length) {
517
+ console.log(` ${metrics.join(` ${DIM}·${RESET} `)}`);
518
+ }
519
+ }
520
+ }
521
+ console.log("");
522
+ console.log(` ${RULE_DIM}`);
523
+ }
524
+
262
525
  function cmdUpdate(current, latest) {
263
526
  if (!current || !latest) return;
264
527
  console.log("");
@@ -291,9 +554,12 @@ switch (cmd) {
291
554
  case "next": cmdNext(rest.join(" ")); break;
292
555
  case "end": cmdEnd(rest[0], rest.slice(1).join(" ")); break;
293
556
  case "update": cmdUpdate(rest[0], rest[1]); break;
557
+ case "plan-summary": cmdPlanSummary(rest[0]); break;
558
+ case "journey-tree": cmdJourneyTree(rest[0]); break;
559
+ case "milestone-complete": cmdMilestoneComplete(rest[0], rest[1], rest.slice(2).join(" ")); break;
294
560
  default:
295
561
  console.error(
296
- `Usage: qualia-ui.js <banner|context|divider|ok|fail|warn|info|spawn|wave|task|done|next|end|update> [args]`
562
+ `Usage: qualia-ui.js <banner|context|divider|ok|fail|warn|info|spawn|wave|task|done|next|end|update|plan-summary|journey-tree|milestone-complete> [args]`
297
563
  );
298
564
  process.exit(1);
299
565
  }