@the-bearded-bear/claude-craft 7.35.0 → 8.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/Dev/scripts/install-php-rules.sh +1 -1
  2. package/Dev/scripts/validate-skills-spec.sh +121 -0
  3. package/README.md +13 -11
  4. package/cli/index.js +6 -0
  5. package/cli/kanban/client/index.html +17 -0
  6. package/cli/kanban/client/src/App.svelte +106 -0
  7. package/cli/kanban/client/src/app.css +175 -0
  8. package/cli/kanban/client/src/lib/router.svelte.js +19 -0
  9. package/cli/kanban/client/src/lib/store.svelte.js +132 -0
  10. package/cli/kanban/client/src/main.js +6 -0
  11. package/cli/kanban/client/src/views/BacklogView.svelte +344 -0
  12. package/cli/kanban/client/src/views/BurndownView.svelte +189 -0
  13. package/cli/kanban/client/src/views/DepsView.svelte +334 -0
  14. package/cli/kanban/client/src/views/DocsView.svelte +451 -0
  15. package/cli/kanban/client/src/views/KanbanView.svelte +227 -0
  16. package/cli/kanban/client/vite.config.js +21 -0
  17. package/cli/kanban/server/app.js +201 -0
  18. package/cli/kanban/server/middleware/security.js +53 -0
  19. package/cli/kanban/server/services/event-bus.js +33 -0
  20. package/cli/kanban/server/services/file-scanner.js +113 -0
  21. package/cli/kanban/server/services/file-watcher.js +68 -0
  22. package/cli/kanban/server/services/file-writer.js +107 -0
  23. package/cli/kanban/server/services/frontmatter.js +55 -0
  24. package/cli/kanban/server/services/repository.js +173 -0
  25. package/cli/kanban/server/services/sprint-cache.js +208 -0
  26. package/cli/kanban/server/services/state-machine.js +156 -0
  27. package/cli/kanban/shared/schemas.js +127 -0
  28. package/cli/lib/help.js +4 -0
  29. package/cli/lib/kanban.js +103 -0
  30. package/package.json +21 -3
@@ -0,0 +1,344 @@
1
+ <script>
2
+ import { store } from '../lib/store.svelte.js';
3
+
4
+ const epicGroups = $derived.by(() => {
5
+ const groups = new Map();
6
+ const orphans = [];
7
+
8
+ for (const epic of store.epics) {
9
+ groups.set(epic.id, {
10
+ epic,
11
+ stories: [],
12
+ expanded: false
13
+ });
14
+ }
15
+
16
+ for (const story of store.stories) {
17
+ if (story.epic_id && groups.has(story.epic_id)) {
18
+ groups.get(story.epic_id).stories.push(story);
19
+ } else if (!story.epic_id) {
20
+ orphans.push(story);
21
+ }
22
+ }
23
+
24
+ const sorted = Array.from(groups.values()).sort((a, b) => a.epic.id.localeCompare(b.epic.id));
25
+ for (const group of sorted) {
26
+ group.stories.sort((a, b) => a.id.localeCompare(b.id));
27
+ }
28
+ orphans.sort((a, b) => a.id.localeCompare(b.id));
29
+
30
+ return { groups: sorted, orphans };
31
+ });
32
+
33
+ let expanded = $state(new Set());
34
+
35
+ function toggleEpic(epicId) {
36
+ const next = new Set(expanded);
37
+ if (next.has(epicId)) {
38
+ next.delete(epicId);
39
+ } else {
40
+ next.add(epicId);
41
+ }
42
+ expanded = next;
43
+ }
44
+
45
+ function epicProgress(stories) {
46
+ const done = stories.filter(s => s.status === 'done').length;
47
+ const total = stories.length;
48
+ return { done, total, percent: total > 0 ? Math.round((done / total) * 100) : 0 };
49
+ }
50
+
51
+ function epicTotalPoints(stories) {
52
+ return stories.reduce((sum, s) => sum + (s.story_points || 0), 0);
53
+ }
54
+
55
+ function businessValueColor(value) {
56
+ if (value === 'high') return 'var(--danger)';
57
+ if (value === 'medium') return 'var(--warn)';
58
+ if (value === 'low') return 'var(--info)';
59
+ return 'var(--fg-dim)';
60
+ }
61
+
62
+ function statusColor(status) {
63
+ if (status === 'done') return 'var(--ok)';
64
+ if (status === 'blocked') return 'var(--danger)';
65
+ if (status === 'in-progress') return 'var(--accent)';
66
+ if (status === 'review') return 'var(--info)';
67
+ return 'var(--fg-dim)';
68
+ }
69
+
70
+ function tddBadge(phase) {
71
+ if (phase === 'red') return { char: '●', color: 'var(--danger)', label: 'red' };
72
+ if (phase === 'green') return { char: '●', color: 'var(--ok)', label: 'green' };
73
+ if (phase === 'refactor') return { char: '●', color: 'var(--info)', label: 'refactor' };
74
+ if (phase === 'done') return { char: '✓', color: 'var(--ok)', label: 'done' };
75
+ return null;
76
+ }
77
+ </script>
78
+
79
+ <div class="backlog">
80
+ {#if store.epics.length === 0 && store.stories.length === 0}
81
+ <div class="empty">No epics or stories yet</div>
82
+ {:else}
83
+ {#each epicGroups.groups as { epic, stories } (epic.id)}
84
+ {@const progress = epicProgress(stories)}
85
+ {@const totalPoints = epicTotalPoints(stories)}
86
+ {@const isExpanded = expanded.has(epic.id)}
87
+
88
+ <article class="epic-card">
89
+ <header class="epic-header">
90
+ <button
91
+ class="expand-btn"
92
+ onclick={() => toggleEpic(epic.id)}
93
+ aria-label={isExpanded ? 'Collapse' : 'Expand'}
94
+ aria-expanded={isExpanded}
95
+ >
96
+ {isExpanded ? '▾' : '▸'}
97
+ </button>
98
+ <span class="epic-id">{epic.id}</span>
99
+ <span class="epic-title">{epic.title}</span>
100
+ <span class="business-value" style="background: {businessValueColor(epic.business_value)}">
101
+ {epic.business_value}
102
+ </span>
103
+ <span class="story-count">{stories.length} stories</span>
104
+ <span class="total-points">{totalPoints}p</span>
105
+ </header>
106
+
107
+ <div class="epic-status">
108
+ <span class="status-badge" style="color: {statusColor(epic.status)}">{epic.status}</span>
109
+ </div>
110
+
111
+ <div class="epic-progress" role="region" aria-label="epic progress">
112
+ <div class="progress-bar-bg">
113
+ <div class="progress-bar" style="width: {progress.percent}%"></div>
114
+ </div>
115
+ <span class="progress-text">{progress.done}/{progress.total} ({progress.percent}%)</span>
116
+ </div>
117
+
118
+ {#if isExpanded && stories.length > 0}
119
+ <div class="stories-list">
120
+ {#each stories as story (story.id)}
121
+ {@const tdd = tddBadge(story.tdd_phase)}
122
+
123
+ <div class="story-item">
124
+ <span class="story-id">{story.id}</span>
125
+ <span class="story-title">{story.title}</span>
126
+ <span class="story-status" style="color: {statusColor(story.status)}">{story.status}</span>
127
+ <span class="story-points">{story.story_points}p</span>
128
+ {#if tdd}
129
+ <span class="story-tdd" style="color: {tdd.color}" title="TDD: {tdd.label}">{tdd.char}</span>
130
+ {/if}
131
+ </div>
132
+ {/each}
133
+ </div>
134
+ {/if}
135
+ </article>
136
+ {/each}
137
+
138
+ {#if epicGroups.orphans.length > 0}
139
+ <article class="epic-card orphans">
140
+ <header class="epic-header">
141
+ <button
142
+ class="expand-btn"
143
+ onclick={() => toggleEpic('__orphans__')}
144
+ aria-label={expanded.has('__orphans__') ? 'Collapse' : 'Expand'}
145
+ aria-expanded={expanded.has('__orphans__')}
146
+ >
147
+ {expanded.has('__orphans__') ? '▾' : '▸'}
148
+ </button>
149
+ <span class="epic-title">No Epic</span>
150
+ <span class="story-count">{epicGroups.orphans.length} stories</span>
151
+ <span class="total-points">{epicTotalPoints(epicGroups.orphans)}p</span>
152
+ </header>
153
+
154
+ {#if expanded.has('__orphans__')}
155
+ <div class="stories-list">
156
+ {#each epicGroups.orphans as story (story.id)}
157
+ {@const tdd = tddBadge(story.tdd_phase)}
158
+
159
+ <div class="story-item">
160
+ <span class="story-id">{story.id}</span>
161
+ <span class="story-title">{story.title}</span>
162
+ <span class="story-status" style="color: {statusColor(story.status)}">{story.status}</span>
163
+ <span class="story-points">{story.story_points}p</span>
164
+ {#if tdd}
165
+ <span class="story-tdd" style="color: {tdd.color}" title="TDD: {tdd.label}">{tdd.char}</span>
166
+ {/if}
167
+ </div>
168
+ {/each}
169
+ </div>
170
+ {/if}
171
+ </article>
172
+ {/if}
173
+ {/if}
174
+ </div>
175
+
176
+ <style>
177
+ .backlog {
178
+ max-width: 1200px;
179
+ margin: 0 auto;
180
+ display: flex;
181
+ flex-direction: column;
182
+ gap: 12px;
183
+ }
184
+
185
+ .epic-card {
186
+ background: var(--bg-elev);
187
+ border: 1px solid var(--border);
188
+ border-radius: var(--radius);
189
+ padding: 14px;
190
+ box-shadow: var(--shadow);
191
+ }
192
+
193
+ .epic-card.orphans {
194
+ border-left: 3px solid var(--warn);
195
+ }
196
+
197
+ .epic-header {
198
+ display: flex;
199
+ align-items: center;
200
+ gap: 10px;
201
+ margin-bottom: 8px;
202
+ }
203
+
204
+ .expand-btn {
205
+ background: transparent;
206
+ border: none;
207
+ cursor: pointer;
208
+ padding: 4px 6px;
209
+ font-size: 14px;
210
+ color: var(--fg-dim);
211
+ border-radius: 3px;
212
+ }
213
+
214
+ .expand-btn:hover {
215
+ background: var(--border);
216
+ }
217
+
218
+ .expand-btn:focus {
219
+ outline: 2px solid var(--accent);
220
+ outline-offset: 1px;
221
+ }
222
+
223
+ .epic-id {
224
+ font-family: var(--mono);
225
+ font-weight: 600;
226
+ font-size: 12px;
227
+ color: var(--accent);
228
+ }
229
+
230
+ .epic-title {
231
+ font-weight: 600;
232
+ flex: 1;
233
+ }
234
+
235
+ .business-value {
236
+ text-transform: uppercase;
237
+ font-size: 10px;
238
+ font-weight: 700;
239
+ padding: 3px 8px;
240
+ border-radius: 10px;
241
+ color: var(--accent-fg);
242
+ }
243
+
244
+ .story-count {
245
+ font-size: 12px;
246
+ color: var(--fg-dim);
247
+ font-family: var(--mono);
248
+ }
249
+
250
+ .total-points {
251
+ font-size: 12px;
252
+ font-weight: 600;
253
+ color: var(--accent);
254
+ font-family: var(--mono);
255
+ }
256
+
257
+ .epic-status {
258
+ margin-bottom: 8px;
259
+ }
260
+
261
+ .status-badge {
262
+ text-transform: uppercase;
263
+ font-size: 11px;
264
+ font-weight: 600;
265
+ }
266
+
267
+ .epic-progress {
268
+ display: flex;
269
+ align-items: center;
270
+ gap: 10px;
271
+ margin-bottom: 12px;
272
+ }
273
+
274
+ .progress-bar-bg {
275
+ flex: 1;
276
+ background: var(--bg-sidebar);
277
+ height: 8px;
278
+ border-radius: 4px;
279
+ overflow: hidden;
280
+ }
281
+
282
+ .progress-bar {
283
+ background: var(--accent);
284
+ height: 100%;
285
+ transition: width 0.3s ease;
286
+ }
287
+
288
+ .progress-text {
289
+ font-size: 11px;
290
+ color: var(--fg-dim);
291
+ font-family: var(--mono);
292
+ min-width: 80px;
293
+ text-align: right;
294
+ }
295
+
296
+ .stories-list {
297
+ margin-top: 12px;
298
+ border-top: 1px solid var(--border);
299
+ padding-top: 12px;
300
+ display: flex;
301
+ flex-direction: column;
302
+ gap: 6px;
303
+ }
304
+
305
+ .story-item {
306
+ display: flex;
307
+ align-items: center;
308
+ gap: 8px;
309
+ padding: 6px 10px;
310
+ background: var(--bg-sidebar);
311
+ border-radius: var(--radius);
312
+ font-size: 13px;
313
+ }
314
+
315
+ .story-id {
316
+ font-family: var(--mono);
317
+ font-weight: 600;
318
+ font-size: 11px;
319
+ color: var(--accent);
320
+ }
321
+
322
+ .story-title {
323
+ flex: 1;
324
+ overflow: hidden;
325
+ text-overflow: ellipsis;
326
+ white-space: nowrap;
327
+ }
328
+
329
+ .story-status {
330
+ text-transform: uppercase;
331
+ font-size: 10px;
332
+ font-weight: 600;
333
+ }
334
+
335
+ .story-points {
336
+ font-family: var(--mono);
337
+ font-size: 11px;
338
+ color: var(--fg-dim);
339
+ }
340
+
341
+ .story-tdd {
342
+ font-size: 13px;
343
+ }
344
+ </style>
@@ -0,0 +1,189 @@
1
+ <script>
2
+ import { store } from '../lib/store.svelte.js';
3
+ import uPlot from 'uplot';
4
+ import 'uplot/dist/uPlot.min.css';
5
+
6
+ let chartContainer = $state(null);
7
+ let chart = $state(null);
8
+
9
+ const hasData = $derived(store.burndown?.ideal?.length > 0);
10
+
11
+ const onTrackColor = $derived.by(() => {
12
+ if (!store.burndown?.on_track) return 'var(--fg-dim)';
13
+ if (store.burndown.on_track === 'on-track') return 'var(--ok)';
14
+ if (store.burndown.on_track === 'at-risk') return 'var(--warn)';
15
+ if (store.burndown.on_track === 'behind') return 'var(--danger)';
16
+ return 'var(--fg-dim)';
17
+ });
18
+
19
+ $effect(() => {
20
+ if (!chartContainer || !hasData) {
21
+ if (chart) {
22
+ chart.destroy();
23
+ chart = null;
24
+ }
25
+ return;
26
+ }
27
+
28
+ const idealData = store.burndown.ideal.map(p => ({
29
+ ts: Math.floor(Date.parse(p.date) / 1000),
30
+ pts: p.points
31
+ }));
32
+
33
+ const actualData = store.burndown.actual.map(p => ({
34
+ ts: Math.floor(Date.parse(p.date) / 1000),
35
+ pts: p.points
36
+ }));
37
+
38
+ const allTimestamps = new Set([
39
+ ...idealData.map(p => p.ts),
40
+ ...actualData.map(p => p.ts)
41
+ ]);
42
+ const timestamps = Array.from(allTimestamps).sort((a, b) => a - b);
43
+
44
+ const idealValues = timestamps.map(ts => {
45
+ const point = idealData.find(p => p.ts === ts);
46
+ return point ? point.pts : null;
47
+ });
48
+
49
+ const actualValues = timestamps.map(ts => {
50
+ const point = actualData.find(p => p.ts === ts);
51
+ return point ? point.pts : null;
52
+ });
53
+
54
+ const data = [
55
+ timestamps,
56
+ idealValues,
57
+ actualValues
58
+ ];
59
+
60
+ const opts = {
61
+ width: chartContainer.clientWidth || 800,
62
+ height: 300,
63
+ scales: {
64
+ x: { time: true }
65
+ },
66
+ axes: [
67
+ {
68
+ stroke: 'var(--fg-dim)',
69
+ grid: { stroke: 'var(--border)' },
70
+ values: (u, vals) => vals.map(v => {
71
+ const d = new Date(v * 1000);
72
+ return `${String(d.getDate()).padStart(2, '0')}/${String(d.getMonth() + 1).padStart(2, '0')}`;
73
+ })
74
+ },
75
+ {
76
+ stroke: 'var(--fg-dim)',
77
+ grid: { stroke: 'var(--border)' }
78
+ }
79
+ ],
80
+ series: [
81
+ { label: 'Date' },
82
+ {
83
+ label: 'Ideal',
84
+ stroke: 'var(--fg-dim)',
85
+ width: 2,
86
+ dash: [5, 5],
87
+ points: { show: false }
88
+ },
89
+ {
90
+ label: 'Actual',
91
+ stroke: 'var(--accent)',
92
+ width: 3,
93
+ points: { show: true, size: 5, fill: 'var(--accent)' }
94
+ }
95
+ ],
96
+ legend: {
97
+ show: true
98
+ }
99
+ };
100
+
101
+ if (chart) chart.destroy();
102
+ chart = new uPlot(opts, data, chartContainer);
103
+
104
+ const resizeObserver = new ResizeObserver(() => {
105
+ if (chart && chartContainer) {
106
+ chart.setSize({ width: chartContainer.clientWidth, height: 300 });
107
+ }
108
+ });
109
+ resizeObserver.observe(chartContainer);
110
+
111
+ return () => {
112
+ resizeObserver.disconnect();
113
+ if (chart) {
114
+ chart.destroy();
115
+ chart = null;
116
+ }
117
+ };
118
+ });
119
+ </script>
120
+
121
+ <div class="burndown">
122
+ {#if !store.burndown || !hasData}
123
+ <div class="empty">No burndown data (sprint not started or no start/end date)</div>
124
+ {:else}
125
+ <header class="burndown-header">
126
+ <h2 class="sprint-name">{store.sprint?.name || 'Current Sprint'}</h2>
127
+ <div class="meta">
128
+ <span class="on-track-badge" style="background: {onTrackColor}">
129
+ {store.burndown.on_track || 'unknown'}
130
+ </span>
131
+ <span class="total-points">{store.burndown.total_points}p total</span>
132
+ </div>
133
+ </header>
134
+
135
+ <div class="chart-wrapper" bind:this={chartContainer}></div>
136
+ {/if}
137
+ </div>
138
+
139
+ <style>
140
+ .burndown {
141
+ max-width: 1200px;
142
+ margin: 0 auto;
143
+ }
144
+
145
+ .burndown-header {
146
+ display: flex;
147
+ align-items: center;
148
+ justify-content: space-between;
149
+ margin-bottom: 20px;
150
+ }
151
+
152
+ .sprint-name {
153
+ font-size: 18px;
154
+ font-weight: 700;
155
+ margin: 0;
156
+ color: var(--fg);
157
+ }
158
+
159
+ .meta {
160
+ display: flex;
161
+ align-items: center;
162
+ gap: 12px;
163
+ }
164
+
165
+ .on-track-badge {
166
+ text-transform: uppercase;
167
+ font-size: 11px;
168
+ font-weight: 700;
169
+ padding: 4px 10px;
170
+ border-radius: 12px;
171
+ color: var(--accent-fg);
172
+ }
173
+
174
+ .total-points {
175
+ font-family: var(--mono);
176
+ font-size: 13px;
177
+ color: var(--fg-dim);
178
+ font-weight: 600;
179
+ }
180
+
181
+ .chart-wrapper {
182
+ background: var(--bg-elev);
183
+ border: 1px solid var(--border);
184
+ border-radius: var(--radius);
185
+ padding: 20px;
186
+ box-shadow: var(--shadow);
187
+ min-height: 340px;
188
+ }
189
+ </style>