@hanzlaa/rcode 3.4.33 → 3.6.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 (106) hide show
  1. package/AGENTS.md +6 -6
  2. package/CONTRIBUTING.md +2 -0
  3. package/LICENSE +21 -0
  4. package/README.md +66 -403
  5. package/cli/doctor.js +87 -1
  6. package/cli/install.js +122 -31
  7. package/cli/lib/schemas.cjs +318 -0
  8. package/cli/postinstall.js +19 -3
  9. package/dist/rcode.js +316 -23
  10. package/package.json +14 -4
  11. package/rihal/agents/rihal-cross-platform-auditor.md +1 -1
  12. package/rihal/agents/rihal-dep-auditor.md +1 -1
  13. package/rihal/agents/rihal-docs-auditor.md +3 -145
  14. package/rihal/agents/rihal-i18n-auditor.md +1 -1
  15. package/rihal/agents/rihal-nyquist-auditor.md +4 -156
  16. package/rihal/agents/rihal-observability-auditor.md +1 -1
  17. package/rihal/bin/rihal-hooks.cjs +394 -4
  18. package/rihal/bin/rihal-tools.cjs +891 -24
  19. package/rihal/commands/create-prd.md +18 -0
  20. package/rihal/commands/execute-milestone.md +18 -0
  21. package/rihal/commands/plan-milestone.md +18 -0
  22. package/rihal/commands/scaffold-milestone.md +18 -0
  23. package/rihal/commands/scaffold-skill.md +18 -0
  24. package/rihal/references/REFERENCES_INDEX.md +49 -7
  25. package/rihal/references/agent-contracts.md +10 -0
  26. package/rihal/references/design-tokens.md +98 -0
  27. package/rihal/references/docs-auditor-playbook.md +148 -0
  28. package/rihal/references/git-preflight.md +117 -0
  29. package/rihal/references/iterative-retrieval.md +85 -0
  30. package/rihal/references/nyquist-auditor-playbook.md +157 -0
  31. package/rihal/references/workstream-flag.md +2 -2
  32. package/rihal/skills/actions/1-analysis/rihal-prfaq/SKILL.md +9 -0
  33. package/rihal/skills/actions/4-implementation/rihal-checkpoint-preview/SKILL.md +9 -0
  34. package/rihal/skills/actions/4-implementation/rihal-ci/SKILL.md +4 -0
  35. package/rihal/skills/actions/4-implementation/rihal-code-review/steps/step-02-review.md +2 -2
  36. package/rihal/skills/actions/4-implementation/rihal-harden/SKILL.md +4 -0
  37. package/rihal/skills/actions/4-implementation/rihal-migrate/SKILL.md +4 -0
  38. package/rihal/skills/agents/haitham-frontend/SKILL.md +2 -0
  39. package/rihal/templates/settings-hooks.json +39 -0
  40. package/rihal/workflows/check-todos.md +4 -0
  41. package/rihal/workflows/code-review-fix.md +4 -3
  42. package/rihal/workflows/code-review.md +1 -1
  43. package/rihal/workflows/debug.md +1 -1
  44. package/rihal/workflows/dev-story.md +4 -0
  45. package/rihal/workflows/diff.md +2 -2
  46. package/rihal/workflows/do.md +16 -8
  47. package/rihal/workflows/docs-update.md +2 -2
  48. package/rihal/workflows/enable-hooks.md +6 -1
  49. package/rihal/workflows/execute-milestone.md +139 -0
  50. package/rihal/workflows/execute-regression-gates.md +1 -1
  51. package/rihal/workflows/execute-sprint.md +54 -2
  52. package/rihal/workflows/execute-verify-phase-goal.md +31 -4
  53. package/rihal/workflows/execute-waves.md +33 -5
  54. package/rihal/workflows/execute.md +40 -6
  55. package/rihal/workflows/help.md +1 -1
  56. package/rihal/workflows/import.md +1 -1
  57. package/rihal/workflows/lens-audit.md +39 -23
  58. package/rihal/workflows/list-workspaces.md +1 -1
  59. package/rihal/workflows/map-codebase.md +4 -4
  60. package/rihal/workflows/new-milestone.md +18 -1
  61. package/rihal/workflows/new-project-research.md +53 -1
  62. package/rihal/workflows/new-workspace.md +1 -1
  63. package/rihal/workflows/plan-milestone.md +105 -0
  64. package/rihal/workflows/plan-research-validation.md +1 -1
  65. package/rihal/workflows/plan-spawn-planner.md +1 -1
  66. package/rihal/workflows/plan.md +31 -3
  67. package/rihal/workflows/plant-seed.md +6 -0
  68. package/rihal/workflows/quick.md +11 -5
  69. package/rihal/workflows/research-phase.md +24 -0
  70. package/rihal/workflows/scaffold-milestone.md +60 -0
  71. package/rihal/workflows/scaffold-skill.md +137 -0
  72. package/rihal/workflows/scan.md +1 -1
  73. package/rihal/workflows/session-report.md +43 -3
  74. package/rihal/workflows/verify-work.md +3 -3
  75. package/server/dashboard.js +154 -5
  76. package/server/lib/html/client/agents-data.js +27 -0
  77. package/server/lib/html/client/app.js +15 -0
  78. package/server/lib/html/client/components/App.js +211 -0
  79. package/server/lib/html/client/components/OrchPanel.js +293 -0
  80. package/server/lib/html/client/components/Sidebar.js +73 -0
  81. package/server/lib/html/client/components/Topbar.js +53 -0
  82. package/server/lib/html/client/components/XtermPanel.js +220 -0
  83. package/server/lib/html/client/components/shared.js +330 -0
  84. package/server/lib/html/client/icons-client.js +85 -0
  85. package/server/lib/html/client/orchestrator.js +279 -0
  86. package/server/lib/html/client/preact.js +34 -0
  87. package/server/lib/html/client/store.js +91 -0
  88. package/server/lib/html/client/util.js +186 -0
  89. package/server/lib/html/client/views/AgentsView.js +83 -0
  90. package/server/lib/html/client/views/DecisionsView.js +102 -0
  91. package/server/lib/html/client/views/FilesView.js +223 -0
  92. package/server/lib/html/client/views/KanbanView.js +236 -0
  93. package/server/lib/html/client/views/MemoryView.js +157 -0
  94. package/server/lib/html/client/views/MilestonesView.js +136 -0
  95. package/server/lib/html/client/views/OrchestrationView.js +167 -0
  96. package/server/lib/html/client/views/OverviewView.js +221 -0
  97. package/server/lib/html/client/views/PhasesView.js +184 -0
  98. package/server/lib/html/client/views/RoadmapView.js +238 -0
  99. package/server/lib/html/client/views/SprintsView.js +178 -0
  100. package/server/lib/html/client/views/TasksView.js +148 -0
  101. package/server/lib/html/client.js +42 -1064
  102. package/server/lib/html/css.js +2266 -466
  103. package/server/lib/html/icons.js +68 -0
  104. package/server/lib/html/shell.js +16 -210
  105. package/server/lib/scanner.js +109 -0
  106. package/server/orchestrator.js +362 -0
@@ -0,0 +1,184 @@
1
+ /**
2
+ * PhasesView — Preact component.
3
+ *
4
+ * Ports renderPhases(subId) from client-render.js.
5
+ * List mode: filter + phase cards.
6
+ * Detail mode: entity header, attr grid, progress bar, Run/Terminal/View-plan
7
+ * buttons, sprint velocity bars, sprint cards, command hints accordion.
8
+ */
9
+
10
+ import { html, useState } from '../preact.js';
11
+ import { useStore } from '../store.js';
12
+ import { pct, humanDate, phaseHints } from '../util.js';
13
+ import {
14
+ Chip, ProgressBar, Breadcrumb, CmdHints, RunningBadge, SprintCard, PhaseCard,
15
+ } from '../components/shared.js';
16
+ import { runAndOpenTerm, openTermPanel, runningInPhase } from '../orchestrator.js';
17
+ import { Icon } from '../icons-client.js';
18
+
19
+ function AttrItem({ label, value }) {
20
+ return html`
21
+ <div class="attr-item">
22
+ <span class="attr-label">${label}</span>
23
+ <span class="attr-value">${value}</span>
24
+ </div>
25
+ `;
26
+ }
27
+
28
+ function VelocityBars({ sprints }) {
29
+ const sprintsWithVel = sprints.filter(s => s.velocity_actual != null || s.velocity_target != null);
30
+ if (!sprintsWithVel.length) return null;
31
+ const maxV = Math.max(
32
+ ...sprintsWithVel.map(s => Math.max(s.velocity_actual || 0, s.velocity_target || 0)),
33
+ 1,
34
+ );
35
+ return html`
36
+ <div>
37
+ <div class="view-title" style="margin-top:var(--space-6)">Sprint Velocity</div>
38
+ <div style="max-width:600px;">
39
+ ${sprintsWithVel.map(s => html`
40
+ <div key=${s.id} class="velocity-bar">
41
+ <div class="velocity-bar-label">S${s.id}</div>
42
+ <div class="velocity-bar-track">
43
+ <div class="velocity-bar-fill"
44
+ style=${'width:' + ((s.velocity_actual || 0) / maxV * 100) + '%;'}></div>
45
+ </div>
46
+ <div class="velocity-bar-val">${s.velocity_actual || 0}/${s.velocity_target || '—'}</div>
47
+ </div>
48
+ `)}
49
+ </div>
50
+ </div>
51
+ `;
52
+ }
53
+
54
+ function PhaseDetail({ phase: p, S }) {
55
+ const sps = Array.isArray(p.sprints) ? p.sprints : [];
56
+ const stories = sps.flatMap(s => (Array.isArray(s.stories) ? s.stories : []));
57
+ const done = stories.filter(t => t.status === 'done' || t.status === 'completed').length;
58
+ const running = runningInPhase(p);
59
+ const hints = phaseHints(p);
60
+
61
+ function handleRun(e) {
62
+ e.stopPropagation();
63
+ runAndOpenTerm('phase-' + p.id, '/rihal-execute ' + p.id, 'Phase ' + p.id);
64
+ }
65
+ function handleTerm(e) {
66
+ e.stopPropagation();
67
+ openTermPanel('phase-' + p.id, 'Phase ' + p.id);
68
+ }
69
+ function handleViewPlan(e) {
70
+ e.stopPropagation();
71
+ // Navigate to files view — viewPlanFile was a legacy DOM function
72
+ window.location.hash = 'files';
73
+ }
74
+
75
+ return html`
76
+ <div>
77
+ <${Breadcrumb} items=${[{ label: 'All Phases', hash: 'phases' }]}/>
78
+ <div class="entity-header">
79
+ <div class="entity-title">
80
+ <${Icon} name="clipboard-list" size=${18}/> Phase ${p.id} — ${p.name}
81
+ <${RunningBadge} count=${running}/>
82
+ </div>
83
+ <div class="attr-grid">
84
+ <${AttrItem} label="Status" value=${html`<${Chip} status=${p.status}/>`}/>
85
+ <${AttrItem} label="Sprints" value=${sps.length}/>
86
+ <${AttrItem} label="Tasks Done" value=${done + '/' + stories.length}/>
87
+ <${AttrItem} label="Progress" value=${pct(done, stories.length)}/>
88
+ ${p.completed_at ? html`<${AttrItem} label="Completed" value=${humanDate(p.completed_at)}/>` : null}
89
+ </div>
90
+ </div>
91
+ <div style="margin-bottom:var(--space-4);">
92
+ <${ProgressBar} done=${done} total=${stories.length}/>
93
+ </div>
94
+ <div class="term-action-bar">
95
+ <button class="term-run-btn" onClick=${handleRun}>▶ Run Phase</button>
96
+ <button class="term-run-btn outline" onClick=${handleTerm}><${Icon} name="monitor" size=${14}/> Terminal</button>
97
+ <button class="back-btn" onClick=${handleViewPlan}><${Icon} name="file-text" size=${14}/> View plan file →</button>
98
+ </div>
99
+ <${VelocityBars} sprints=${sps}/>
100
+ <div class="view-title" style="margin-top:var(--space-6)">Sprints</div>
101
+ <div class="phase-list">
102
+ ${sps.length
103
+ ? sps.map(s => html`
104
+ <${SprintCard} key=${s.id}
105
+ sprint=${Object.assign({}, s, { phaseId: p.id, phaseName: p.name })}
106
+ S=${S}/>
107
+ `)
108
+ : html`
109
+ <div class="empty">
110
+ No sprints in this phase yet.
111
+ <div class="empty-action">Run /rihal-plan to create sprints</div>
112
+ </div>
113
+ `}
114
+ </div>
115
+ <${CmdHints} hints=${hints}/>
116
+ </div>
117
+ `;
118
+ }
119
+
120
+ export function PhasesView({ subId }) {
121
+ const S = useStore();
122
+ const phases = S.phases || [];
123
+ const [filter, setFilter] = useState('');
124
+
125
+ if (subId) {
126
+ const p = phases.find(
127
+ ph => String(ph.id) === String(subId) || String(ph.number) === String(subId),
128
+ );
129
+ if (!p) {
130
+ return html`
131
+ <div id="view-phases" class="view active">
132
+ <${Breadcrumb} items=${[{ label: 'Phases', hash: 'phases' }]}/>
133
+ <div class="empty">Phase not found.</div>
134
+ </div>
135
+ `;
136
+ }
137
+ return html`
138
+ <div id="view-phases" class="view active">
139
+ <${PhaseDetail} phase=${p} S=${S}/>
140
+ </div>
141
+ `;
142
+ }
143
+
144
+ // List mode
145
+ const allComplete =
146
+ phases.length > 0 &&
147
+ phases.every(ph => ph.status === 'complete' || ph.status === 'completed' || ph.status === 'done');
148
+ const plHints = [
149
+ ['/rihal-add-phase', 'Add a new phase'],
150
+ ['/rihal-stats', 'Project statistics'],
151
+ ['/rihal-progress', 'Overall progress'],
152
+ ];
153
+ if (allComplete) {
154
+ plHints.push(['/rihal-audit-milestone', 'Audit milestone completion']);
155
+ plHints.push(['/rihal-complete-milestone', 'Complete and archive milestone']);
156
+ plHints.push(['/rihal-ship', 'Create PR and ship']);
157
+ }
158
+
159
+ const q = filter.toLowerCase();
160
+ const filtered = q
161
+ ? phases.filter(p => p.name.toLowerCase().includes(q) || String(p.id).includes(q))
162
+ : phases;
163
+
164
+ return html`
165
+ <div id="view-phases" class="view active">
166
+ <div class="view-title">Phases</div>
167
+ <div class="filter-bar">
168
+ <input class="filter-input" type="text" placeholder="Filter…"
169
+ value=${filter} onInput=${e => setFilter(e.target.value)}/>
170
+ </div>
171
+ <div id="phases-inner" class="phase-list">
172
+ ${filtered.length
173
+ ? filtered.map(p => html`<${PhaseCard} key=${p.id} phase=${p} S=${S}/>`)
174
+ : html`
175
+ <div class="empty">
176
+ No phases yet.
177
+ <div class="empty-action">Run /rihal-new-project to start</div>
178
+ </div>
179
+ `}
180
+ </div>
181
+ <${CmdHints} hints=${plHints}/>
182
+ </div>
183
+ `;
184
+ }
@@ -0,0 +1,238 @@
1
+ /**
2
+ * RoadmapView — Preact component.
3
+ *
4
+ * Ports renderRoadmap() + filterRoadmap() + toggleNode/toggleAllRoadmap
5
+ * from client-render.js / client-main.js to a Preact component tree.
6
+ *
7
+ * Key differences from legacy:
8
+ * - Tree expansion is component useState per node — no DOM style.display hacks.
9
+ * - Filter is component useState — no querySelectorAll style.display hacks.
10
+ * - Keyboard E/C (expand/collapse-all) fires a CustomEvent on the roadmap tree
11
+ * container; PhaseNode and SprintNode listen and update their open state.
12
+ * Shortcuts only fire when the Roadmap view is active (view root is mounted).
13
+ */
14
+
15
+ import { html, useState, useEffect } from '../preact.js';
16
+ import { useStore } from '../store.js';
17
+ import { pctNum, sprintHints, phaseHints } from '../util.js';
18
+ import { Chip, ProgressBar, CmdHints, RunBtn, RunningBadge } from '../components/shared.js';
19
+ import { runningInPhase, runAndOpenTerm } from '../orchestrator.js';
20
+ import { Icon } from '../icons-client.js';
21
+
22
+ /**
23
+ * Recursive tree node with local expansion state.
24
+ * expandSignal: { key: number, open: boolean } — when key changes, force open state.
25
+ */
26
+ function TreeNode({ label, icon, badge, status, children, defaultOpen, onDoubleClick, expandSignal }) {
27
+ const [open, setOpen] = useState(defaultOpen || false);
28
+ const toggle = () => setOpen(o => !o);
29
+
30
+ useEffect(() => {
31
+ if (expandSignal && expandSignal.key > 0) setOpen(expandSignal.open);
32
+ }, [expandSignal && expandSignal.key]);
33
+
34
+ return html`
35
+ <div class="tree-node">
36
+ <div class="tree-row" onClick=${toggle} onDblClick=${onDoubleClick}>
37
+ <span class="tree-chevron">${open ? '▼' : '▶'}</span>
38
+ <span class="tree-icon">${icon}</span>
39
+ <span class="tree-label">${label}</span>
40
+ ${status ? html`<${Chip} status=${status}/>` : null}
41
+ ${badge ? html`<span class="tree-badge">${badge}</span>` : null}
42
+ </div>
43
+ ${open ? html`<div class="tree-children">${children}</div>` : null}
44
+ </div>
45
+ `;
46
+ }
47
+
48
+ /** Leaf node (task row — no expand). */
49
+ function TaskLeaf({ task: t }) {
50
+ const done = t.status === 'done' || t.status === 'completed';
51
+ return html`
52
+ <div class="tree-node task-leaf">
53
+ <div class="tree-row">
54
+ <span class="tree-icon">${done ? '✓' : '○'}</span>
55
+ <span class="tree-label" style=${done ? 'opacity:.6;text-decoration:line-through' : ''}>
56
+ ${t.title}
57
+ </span>
58
+ <${Chip} status=${t.status}/>
59
+ ${t.points ? html`<span class="tree-badge">${t.points}pts</span>` : null}
60
+ </div>
61
+ </div>
62
+ `;
63
+ }
64
+
65
+ /** Phase row with inline mini progress bar. */
66
+ function PhaseNode({ phase: p, filterQuery, expandSignal }) {
67
+ const [open, setOpen] = useState(false);
68
+
69
+ useEffect(() => {
70
+ if (expandSignal && expandSignal.key > 0) setOpen(expandSignal.open);
71
+ }, [expandSignal && expandSignal.key]);
72
+ const sps = p.sprints || [];
73
+ const pStories = sps.flatMap(s => s.stories || []);
74
+ const pDone = pStories.filter(t => t.status === 'done' || t.status === 'completed').length;
75
+ const pp = pctNum(pDone, pStories.length);
76
+ const running = runningInPhase(p);
77
+
78
+ // Filter: hide this node if query doesn't match phase name
79
+ if (filterQuery && !p.name.toLowerCase().includes(filterQuery)) return null;
80
+
81
+ function handleDblClick(e) {
82
+ e.stopPropagation();
83
+ location.hash = 'phases/' + p.id;
84
+ }
85
+
86
+ return html`
87
+ <div class="tree-node" data-filter-text=${p.name.toLowerCase()}>
88
+ <div class="tree-row" onClick=${() => setOpen(o => !o)} onDblClick=${handleDblClick}>
89
+ <span class="tree-chevron">${open ? '▼' : '▶'}</span>
90
+ <span class="tree-icon"><${Icon} name="clipboard-list" size=${14}/></span>
91
+ ${sps.length ? html`<${RunBtn} storyId=${'phase-' + p.id} cmd=${'/rihal-execute ' + p.id} label=${'Phase ' + p.id}/>` : null}
92
+ <span class="tree-label">P${p.id} — ${p.name}</span>
93
+ <${Chip} status=${p.status}/>
94
+ <${RunningBadge} count=${running}/>
95
+ <span style="width:60px;display:inline-block;margin:0 8px;">
96
+ <div class="progress-bar" style="height:4px;">
97
+ <div class="progress-bar-fill" style=${'width:' + pp + '%;height:100%;'}></div>
98
+ </div>
99
+ </span>
100
+ <span class="tree-badge">${sps.length} sprints · ${pDone}/${pStories.length}</span>
101
+ </div>
102
+ ${open ? html`
103
+ <div class="tree-children">
104
+ ${sps.length ? sps.map(s => html`<${SprintNode} key=${s.id} sprint=${s} expandSignal=${expandSignal}/>`) : html`
105
+ <div style="padding:var(--space-2) var(--space-6);">
106
+ <div style="color:var(--text-muted);font-size:var(--text-xs);margin-bottom:var(--space-2);">
107
+ No sprints yet — plan this phase:
108
+ </div>
109
+ <${CmdHints} hints=${phaseHints(p)}/>
110
+ </div>
111
+ `}
112
+ </div>
113
+ ` : null}
114
+ </div>
115
+ `;
116
+ }
117
+
118
+ /** Sprint row inside roadmap. */
119
+ function SprintNode({ sprint: s, expandSignal }) {
120
+ const [open, setOpen] = useState(false);
121
+
122
+ useEffect(() => {
123
+ if (expandSignal && expandSignal.key > 0) setOpen(expandSignal.open);
124
+ }, [expandSignal && expandSignal.key]);
125
+ const sts = s.stories || [];
126
+ const sDone = sts.filter(t => t.status === 'done' || t.status === 'completed').length;
127
+
128
+ return html`
129
+ <div class="tree-node">
130
+ <div class="tree-row" onClick=${() => setOpen(o => !o)}>
131
+ <span class="tree-chevron">${open ? '▼' : '▶'}</span>
132
+ <span class="tree-icon"><${Icon} name="zap" size=${14}/></span>
133
+ <${RunBtn} storyId=${'sprint-' + s.id} cmd=${'/rihal-execute-sprint ' + s.id} label=${'Sprint ' + s.id}/>
134
+ <span class="tree-label">Sprint ${s.id} — ${s.goal || 'No goal'}</span>
135
+ <${Chip} status=${s.status}/>
136
+ <span class="tree-badge">${sDone}/${sts.length}</span>
137
+ </div>
138
+ ${open ? html`
139
+ <div class="tree-children">
140
+ ${sts.length ? sts.map(t => html`<${TaskLeaf} key=${t.id || t.title} task=${t}/>`) : html`
141
+ <div style="padding:var(--space-2) var(--space-6);">
142
+ <div style="color:var(--text-muted);font-size:var(--text-xs);margin-bottom:var(--space-2);">
143
+ No tasks yet — add them:
144
+ </div>
145
+ <${CmdHints} hints=${sprintHints(s)}/>
146
+ </div>
147
+ `}
148
+ </div>
149
+ ` : null}
150
+ </div>
151
+ `;
152
+ }
153
+
154
+ export function RoadmapView() {
155
+ const S = useStore();
156
+ const phases = S.phases || [];
157
+ const ms = S.milestone || 'M1';
158
+ const [filterQuery, setFilterQuery] = useState('');
159
+ // rootOpen is always true (the milestone root stays expanded)
160
+ const [rootOpen] = useState(true);
161
+
162
+ // expandSignal drives E/C keyboard shortcuts.
163
+ // { key: number, open: boolean } — incrementing key triggers child useEffects.
164
+ const [expandSignal, setExpandSignal] = useState({ key: 0, open: false });
165
+
166
+ // E = expand all, C = collapse all — only active while this view is mounted.
167
+ useEffect(() => {
168
+ function onKeyDown(e) {
169
+ if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
170
+ if (e.key === 'e' || e.key === 'E') {
171
+ setExpandSignal(prev => ({ key: prev.key + 1, open: true }));
172
+ } else if (e.key === 'c' || e.key === 'C') {
173
+ setExpandSignal(prev => ({ key: prev.key + 1, open: false }));
174
+ }
175
+ }
176
+ window.addEventListener('keydown', onKeyDown);
177
+ return () => window.removeEventListener('keydown', onKeyDown);
178
+ }, []);
179
+
180
+ // Total stats for root badge
181
+ const totalTasks = phases.flatMap(p => (p.sprints || []).flatMap(s => s.stories || []));
182
+ const doneTasks = totalTasks.filter(t => t.status === 'done' || t.status === 'completed');
183
+
184
+ // Roadmap command hints
185
+ const allPDone = phases.length > 0 && phases.every(
186
+ ph => ph.status === 'complete' || ph.status === 'completed' || ph.status === 'done'
187
+ );
188
+ const rmHints = [
189
+ ['/rihal-autonomous', 'Execute all remaining phases (autonomous)'],
190
+ ['/rihal-audit', 'Audit the project'],
191
+ ['/rihal-add-phase', 'Add a new phase'],
192
+ ['/rihal-milestone-summary','View milestone summary'],
193
+ ['/rihal-new-milestone', 'Start a new milestone'],
194
+ ];
195
+ if (allPDone) {
196
+ rmHints.push(['/rihal-audit-milestone', 'Audit milestone completion']);
197
+ rmHints.push(['/rihal-complete-milestone', 'Complete and archive milestone']);
198
+ }
199
+
200
+ const q = filterQuery.toLowerCase().trim();
201
+
202
+ return html`
203
+ <div id="view-roadmap" class="view active">
204
+ <div class="view-title">Roadmap</div>
205
+ <div class="filter-bar">
206
+ <input class="filter-input" type="text" placeholder="Filter roadmap…"
207
+ value=${filterQuery}
208
+ onInput=${e => setFilterQuery(e.target.value)}/>
209
+ </div>
210
+ <div class="tree-container" id="roadmap-tree">
211
+ <div class="tree-node tree-ms">
212
+ <div class="tree-row tree-header">
213
+ <span class="tree-chevron">▼</span>
214
+ <span class="tree-icon"><${Icon} name="flag" size=${14}/></span>
215
+ <span class="tree-label">${ms}</span>
216
+ <span class="tree-badge">
217
+ ${phases.length} phases · ${doneTasks.length}/${totalTasks.length} tasks
218
+ </span>
219
+ <span class="ms-actions">
220
+ <button class="card-run-btn" title="Execute every remaining phase (autonomous run — pauses at checkpoints)"
221
+ onClick=${e => { e.stopPropagation(); runAndOpenTerm('milestone-execute-all', '/rihal-autonomous', 'Execute all phases'); }}>
222
+ <${Icon} name="play" size=${12}/> Run All
223
+ </button>
224
+ <button class="card-run-btn ms-audit-btn" title="Audit the whole milestone"
225
+ onClick=${e => { e.stopPropagation(); runAndOpenTerm('milestone-audit', '/rihal-audit-milestone', 'Audit milestone'); }}>
226
+ <${Icon} name="clipboard-list" size=${12}/> Audit
227
+ </button>
228
+ </span>
229
+ </div>
230
+ <div class="tree-children">
231
+ ${phases.map(p => html`<${PhaseNode} key=${p.id} phase=${p} filterQuery=${q} expandSignal=${expandSignal}/>`)}
232
+ </div>
233
+ </div>
234
+ </div>
235
+ <${CmdHints} hints=${rmHints}/>
236
+ </div>
237
+ `;
238
+ }
@@ -0,0 +1,178 @@
1
+ /**
2
+ * SprintsView — Preact component.
3
+ *
4
+ * Ports renderSprints(subId) from client-render.js.
5
+ * List mode: filter + sprint cards.
6
+ * Detail mode: breadcrumb chain, entity header, attr grid, progress bar,
7
+ * Run/Terminal action bar, task cards, acceptance-criteria section,
8
+ * command hints.
9
+ */
10
+
11
+ import { html, useState } from '../preact.js';
12
+ import { useStore } from '../store.js';
13
+ import { pct, humanDate, allSprints, sprintHints } from '../util.js';
14
+ import {
15
+ Chip, ProgressBar, Breadcrumb, CmdHints, RunningBadge, SprintCard, TaskCard,
16
+ } from '../components/shared.js';
17
+ import { runAndOpenTerm, openTermPanel, runningInSprint } from '../orchestrator.js';
18
+ import { Icon } from '../icons-client.js';
19
+
20
+ function AttrItem({ label, value }) {
21
+ return html`
22
+ <div class="attr-item">
23
+ <span class="attr-label">${label}</span>
24
+ <span class="attr-value">${value}</span>
25
+ </div>
26
+ `;
27
+ }
28
+
29
+ function SprintDetail({ sprint: s, S }) {
30
+ const rawStories = Array.isArray(s.stories) ? s.stories : [];
31
+ const stories = rawStories.map(t =>
32
+ Object.assign({}, t, {
33
+ sprintId: s.id,
34
+ sprintGoal: s.goal || '',
35
+ phaseId: s.phaseId,
36
+ phaseName: s.phaseName,
37
+ }),
38
+ );
39
+ const done = stories.filter(t => t.status === 'done' || t.status === 'completed').length;
40
+ const running = runningInSprint(s);
41
+ const hints = sprintHints(s);
42
+
43
+ // Acceptance criteria section
44
+ const storiesWithAc = stories.filter(t => t.acceptance);
45
+
46
+ // Breadcrumb includes both "All Sprints" and optional "Phase N" link
47
+ const breadcrumbItems = [{ label: 'All Sprints', hash: 'sprints' }];
48
+ if (s.phaseId) breadcrumbItems.push({ label: 'Phase ' + s.phaseId, hash: 'phases/' + s.phaseId });
49
+
50
+ function handleRun(e) {
51
+ e.stopPropagation();
52
+ runAndOpenTerm('sprint-' + s.id, '/rihal-execute-sprint ' + s.id, 'Sprint ' + s.id);
53
+ }
54
+ function handleTerm(e) {
55
+ e.stopPropagation();
56
+ openTermPanel('sprint-' + s.id, 'Sprint ' + s.id);
57
+ }
58
+
59
+ return html`
60
+ <div>
61
+ <${Breadcrumb} items=${breadcrumbItems}/>
62
+ <div class="entity-header">
63
+ <div class="entity-title">
64
+ <${Icon} name="zap" size=${18}/> Sprint ${s.id}
65
+ <${RunningBadge} count=${running}/>
66
+ </div>
67
+ <div class="attr-grid">
68
+ <${AttrItem} label="Goal" value=${s.goal || '—'}/>
69
+ <${AttrItem} label="Status" value=${html`<${Chip} status=${s.status}/>`}/>
70
+ <${AttrItem} label="Phase" value=${'P' + s.phaseId + (s.phaseName ? ' — ' + s.phaseName : '')}/>
71
+ <${AttrItem} label="Velocity"
72
+ value=${(s.velocity_actual != null ? s.velocity_actual : '—') + ' / ' +
73
+ (s.velocity_target != null ? s.velocity_target : '—') + ' pts'}/>
74
+ <${AttrItem} label="Tasks Done" value=${done + '/' + stories.length}/>
75
+ <${AttrItem} label="Progress" value=${pct(done, stories.length)}/>
76
+ ${s.started_at ? html`<${AttrItem} label="Started" value=${humanDate(s.started_at)}/>` : null}
77
+ ${s.completed_at ? html`<${AttrItem} label="Completed" value=${humanDate(s.completed_at)}/>` : null}
78
+ </div>
79
+ </div>
80
+ <div style="margin-bottom:var(--space-4);">
81
+ <${ProgressBar} done=${done} total=${stories.length}/>
82
+ </div>
83
+ <div class="term-action-bar">
84
+ <button class="term-run-btn" onClick=${handleRun}>▶ Run Sprint</button>
85
+ <button class="term-run-btn outline" onClick=${handleTerm}><${Icon} name="monitor" size=${14}/> Terminal</button>
86
+ </div>
87
+ <div class="view-title" style="margin-top:var(--space-4)">Tasks</div>
88
+ <div class="phase-list">
89
+ ${stories.length
90
+ ? stories.map(t => html`<${TaskCard} key=${t.id || t.title} task=${t}/>`)
91
+ : html`
92
+ <div class="empty">
93
+ No tasks in this sprint yet.
94
+ <div class="empty-action">Run /rihal-create-story to add tasks</div>
95
+ </div>
96
+ `}
97
+ </div>
98
+ ${storiesWithAc.length ? html`
99
+ <div class="view-title" style="margin-top:var(--space-6)">Acceptance Criteria</div>
100
+ <div class="phase-list">
101
+ ${storiesWithAc.map(t => html`
102
+ <div key=${t.id || t.title} class="item">
103
+ <div class="item-title">${t.title}</div>
104
+ <div style="color:var(--text-secondary);font-size:var(--text-sm);margin-top:4px;">
105
+ ✓ ${t.acceptance}
106
+ </div>
107
+ </div>
108
+ `)}
109
+ </div>
110
+ ` : null}
111
+ <${CmdHints} hints=${hints}/>
112
+ </div>
113
+ `;
114
+ }
115
+
116
+ export function SprintsView({ subId }) {
117
+ const S = useStore();
118
+ const sprints = allSprints(S.phases || []);
119
+ const [filter, setFilter] = useState('');
120
+
121
+ if (subId) {
122
+ const s = sprints.find(sp => String(sp.id) === String(subId));
123
+ if (!s) {
124
+ return html`
125
+ <div id="view-sprints" class="view active">
126
+ <${Breadcrumb} items=${[{ label: 'All Sprints', hash: 'sprints' }]}/>
127
+ <div class="empty">Sprint not found.</div>
128
+ </div>
129
+ `;
130
+ }
131
+ return html`
132
+ <div id="view-sprints" class="view active">
133
+ <${SprintDetail} sprint=${s} S=${S}/>
134
+ </div>
135
+ `;
136
+ }
137
+
138
+ // List mode
139
+ const curSp = sprints.find(sp => sp.id === S.currentSprint);
140
+ const slHints = [
141
+ ['/rihal-sprint-planning','Plan a new sprint'],
142
+ ['/rihal-stats', 'Project statistics'],
143
+ ];
144
+ if (curSp) {
145
+ slHints.push(['/rihal-execute', 'Execute current sprint ' + curSp.id]);
146
+ slHints.push(['/rihal-sprint-status','Status of Sprint ' + curSp.id]);
147
+ }
148
+
149
+ const q = filter.toLowerCase();
150
+ const filtered = q
151
+ ? sprints.filter(s =>
152
+ String(s.id).includes(q) ||
153
+ (s.goal || '').toLowerCase().includes(q) ||
154
+ (s.phaseName || '').toLowerCase().includes(q),
155
+ )
156
+ : sprints;
157
+
158
+ return html`
159
+ <div id="view-sprints" class="view active">
160
+ <div class="view-title">Sprints</div>
161
+ <div class="filter-bar">
162
+ <input class="filter-input" type="text" placeholder="Filter…"
163
+ value=${filter} onInput=${e => setFilter(e.target.value)}/>
164
+ </div>
165
+ <div id="sprints-inner" class="phase-list">
166
+ ${filtered.length
167
+ ? filtered.map(s => html`<${SprintCard} key=${s.id} sprint=${s} S=${S}/>`)
168
+ : html`
169
+ <div class="empty">
170
+ No sprints yet.
171
+ <div class="empty-action">Run /rihal-plan to create sprints</div>
172
+ </div>
173
+ `}
174
+ </div>
175
+ <${CmdHints} hints=${slHints}/>
176
+ </div>
177
+ `;
178
+ }