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.
- package/README.md +96 -51
- package/agents/builder.md +25 -14
- package/agents/plan-checker.md +29 -16
- package/agents/planner.md +33 -24
- package/agents/research-synthesizer.md +25 -12
- package/agents/roadmapper.md +89 -84
- package/agents/verifier.md +11 -2
- package/bin/cli.js +13 -2
- package/bin/install.js +28 -5
- package/bin/qualia-ui.js +267 -1
- package/bin/state.js +377 -52
- package/bin/statusline.js +40 -20
- package/docs/erp-contract.md +23 -2
- package/guide.md +84 -21
- package/hooks/auto-update.js +54 -70
- package/hooks/branch-guard.js +64 -6
- package/hooks/migration-guard.js +85 -10
- package/hooks/pre-compact.js +28 -4
- package/hooks/pre-deploy-gate.js +46 -6
- package/hooks/pre-push.js +94 -27
- package/hooks/session-start.js +6 -0
- package/package.json +1 -1
- package/skills/qualia/SKILL.md +3 -1
- package/skills/qualia-build/SKILL.md +40 -5
- package/skills/qualia-handoff/SKILL.md +87 -12
- package/skills/qualia-idk/SKILL.md +155 -3
- package/skills/qualia-map/SKILL.md +4 -4
- package/skills/qualia-milestone/SKILL.md +122 -79
- package/skills/qualia-new/SKILL.md +151 -230
- package/skills/qualia-optimize/SKILL.md +4 -4
- package/skills/qualia-plan/SKILL.md +14 -9
- package/skills/qualia-quick/SKILL.md +1 -1
- package/skills/qualia-report/SKILL.md +12 -0
- package/skills/qualia-verify/SKILL.md +59 -5
- package/templates/help.html +98 -31
- package/templates/journey.md +113 -0
- package/templates/plan.md +56 -11
- package/templates/requirements.md +82 -22
- package/templates/roadmap.md +41 -14
- package/templates/tracking.json +12 -1
- package/tests/runner.js +560 -0
- package/tests/state.test.sh +40 -0
package/agents/roadmapper.md
CHANGED
|
@@ -1,30 +1,34 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: qualia-roadmapper
|
|
3
|
-
description: Creates
|
|
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
|
|
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
|
|
16
|
-
- `.planning/config.json` — project config
|
|
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
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
44
|
+
### 2. Build REQUIREMENTS.md — grouped by milestone
|
|
40
45
|
|
|
41
|
-
|
|
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:**
|
|
54
|
-
- **
|
|
52
|
+
- **Atomic:** one capability per requirement
|
|
53
|
+
- **Testable:** you can name the observable behavior
|
|
54
|
+
- **Assigned to exactly one milestone**
|
|
55
55
|
|
|
56
|
-
Put
|
|
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
|
|
58
|
+
### 3. Derive the Milestone Arc (JOURNEY.md)
|
|
61
59
|
|
|
62
|
-
|
|
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
|
-
**
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
84
|
-
- **
|
|
85
|
-
- **
|
|
86
|
-
- **
|
|
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
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
-
|
|
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
|
|
101
|
-
- [ ] Every v1 requirement
|
|
102
|
-
- [ ] Every
|
|
103
|
-
- [ ]
|
|
104
|
-
- [ ]
|
|
105
|
-
- [ ]
|
|
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
|
|
109
|
+
If any check fails, fix it. The orchestrator trusts your output.
|
|
108
110
|
|
|
109
111
|
### 6. Write the Files
|
|
110
112
|
|
|
111
|
-
Write
|
|
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
|
-
--
|
|
123
|
-
--
|
|
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
|
-
|
|
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/
|
|
134
|
-
Wrote: .planning/
|
|
135
|
-
Wrote: .planning/
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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}
|
|
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
|
|
150
|
+
Before returning:
|
|
148
151
|
|
|
149
|
-
- [ ]
|
|
150
|
-
- [ ]
|
|
151
|
-
- [ ]
|
|
152
|
-
- [ ]
|
|
153
|
-
- [ ]
|
|
154
|
-
- [ ]
|
|
155
|
-
- [ ]
|
|
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.
|
|
162
|
+
If any check fails, fix it before returning. Incomplete roadmaps cost downstream time and cascade errors into every phase that follows.
|
package/agents/verifier.md
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
506
|
-
|
|
507
|
-
|
|
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
|
-
|
|
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
|
}
|