@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
@@ -12,7 +12,7 @@ I18N_DIR="$(dirname "$SCRIPT_DIR")/i18n"
12
12
  TECH_NAME="PHP"
13
13
  TECH_DISPLAY_NAME="PHP"
14
14
  TECH_NAMESPACE="php"
15
- DEFAULT_STACK="PHP 8.3+, Composer, PSR Standards, PHPUnit"
15
+ DEFAULT_STACK="PHP 8.5, Composer, PSR Standards, Pest 4, PHPUnit 12"
16
16
 
17
17
  # --- TCL version ---
18
18
  source "${SCRIPT_DIR}/tcl-common.sh"
@@ -0,0 +1,121 @@
1
+ #!/usr/bin/env bash
2
+ # validate-skills-spec.sh — Verify all .claude/skills/* conform to Anthropic Agent Skills spec.
3
+ # v8.0.0+ : failure blocks CI.
4
+ #
5
+ # Spec: https://github.com/anthropics/skills/blob/main/spec/agent-skills-spec.md
6
+ # Rules enforced:
7
+ # 1. Each skill is a directory under .claude/skills/
8
+ # 2. Each skill has a SKILL.md file at its root
9
+ # 3. SKILL.md starts with YAML frontmatter (---)
10
+ # 4. Frontmatter contains `name:` (matches dir name) and `description:` (non-empty)
11
+ # 5. No absolute paths (/home/, /Users/, C:\) in any skill file
12
+ # 6. Skill name is lowercase kebab-case
13
+
14
+ set -euo pipefail
15
+
16
+ SKILLS_DIR="${1:-.claude/skills}"
17
+ ERRORS=0
18
+ WARNINGS=0
19
+
20
+ fail() {
21
+ echo "❌ FAIL: $1" >&2
22
+ ERRORS=$((ERRORS + 1))
23
+ }
24
+
25
+ warn() {
26
+ echo "⚠️ WARN: $1" >&2
27
+ WARNINGS=$((WARNINGS + 1))
28
+ }
29
+
30
+ pass() {
31
+ echo "✅ $1"
32
+ }
33
+
34
+ if [[ ! -d "$SKILLS_DIR" ]]; then
35
+ fail "Skills directory not found: $SKILLS_DIR"
36
+ exit 1
37
+ fi
38
+
39
+ echo "🔍 Validating skills in $SKILLS_DIR"
40
+ echo ""
41
+
42
+ SKILL_COUNT=0
43
+ for skill_path in "$SKILLS_DIR"/*/; do
44
+ [[ -d "$skill_path" ]] || continue
45
+ skill_name=$(basename "$skill_path")
46
+ SKILL_COUNT=$((SKILL_COUNT + 1))
47
+
48
+ # Rule 6: kebab-case
49
+ if [[ ! "$skill_name" =~ ^[a-z0-9]+(-[a-z0-9]+)*$ ]]; then
50
+ fail "[$skill_name] name must be lowercase kebab-case"
51
+ continue
52
+ fi
53
+
54
+ skill_md="${skill_path}SKILL.md"
55
+
56
+ # Rule 2: SKILL.md exists
57
+ if [[ ! -f "$skill_md" ]]; then
58
+ fail "[$skill_name] missing SKILL.md"
59
+ continue
60
+ fi
61
+
62
+ # Rule 3: starts with ---
63
+ first_line=$(head -1 "$skill_md")
64
+ if [[ "$first_line" != "---" ]]; then
65
+ fail "[$skill_name] SKILL.md must start with --- (YAML frontmatter)"
66
+ continue
67
+ fi
68
+
69
+ # Extract frontmatter (between first two --- lines)
70
+ frontmatter=$(awk '/^---$/{c++; next} c==1{print} c==2{exit}' "$skill_md")
71
+
72
+ # Rule 4a: name field present
73
+ if ! echo "$frontmatter" | grep -q "^name:"; then
74
+ fail "[$skill_name] frontmatter missing 'name:' field"
75
+ continue
76
+ fi
77
+
78
+ declared_name=$(echo "$frontmatter" | grep "^name:" | head -1 | sed 's/^name:[[:space:]]*//' | tr -d '"' | tr -d "'")
79
+ if [[ "$declared_name" != "$skill_name" ]]; then
80
+ fail "[$skill_name] frontmatter name '$declared_name' does not match directory name"
81
+ continue
82
+ fi
83
+
84
+ # Rule 4b: description field present and non-empty
85
+ if ! echo "$frontmatter" | grep -q "^description:"; then
86
+ fail "[$skill_name] frontmatter missing 'description:' field"
87
+ continue
88
+ fi
89
+
90
+ description=$(echo "$frontmatter" | grep "^description:" | head -1 | sed 's/^description:[[:space:]]*//')
91
+ if [[ -z "$description" ]]; then
92
+ fail "[$skill_name] description is empty"
93
+ continue
94
+ fi
95
+
96
+ # Warn if description too short (< 30 chars) — won't help Claude decide
97
+ if [[ ${#description} -lt 30 ]]; then
98
+ warn "[$skill_name] description too short (${#description} chars). Aim for >= 80 chars with 'Use when...'"
99
+ fi
100
+
101
+ # Rule 5: no absolute paths in any file of the skill
102
+ if grep -rn "/home/\|/Users/\|C:\\\\" "$skill_path" 2>/dev/null | grep -v '\.git' | head -1 &>/dev/null; then
103
+ fail "[$skill_name] contains absolute path (forbidden by spec)"
104
+ continue
105
+ fi
106
+
107
+ pass "[$skill_name]"
108
+ done
109
+
110
+ echo ""
111
+ echo "───────────────────────────────────────────"
112
+ echo "Summary: $SKILL_COUNT skills | $ERRORS errors | $WARNINGS warnings"
113
+ echo "───────────────────────────────────────────"
114
+
115
+ if [[ "$ERRORS" -gt 0 ]]; then
116
+ echo "❌ Validation FAILED"
117
+ exit 1
118
+ fi
119
+
120
+ echo "✅ All skills conform to Anthropic Agent Skills spec"
121
+ exit 0
package/README.md CHANGED
@@ -6,14 +6,16 @@
6
6
 
7
7
  A comprehensive framework for AI-assisted development with [Claude Code](https://claude.ai/code). Install standardized rules, agents, and commands for your projects across multiple technology stacks.
8
8
 
9
- ## What's New in v7.28
10
-
11
- - **Claude Code v2.1.107 compatibility** -- 45 new versions (v2.1.63-v2.1.107) fully documented
12
- - **Auto Mode** -- AI-powered permission classifier replacing `--dangerously-skip-permissions`
13
- - **New commands** -- /loop, /effort, /context, /powerup, /proactive, /team-onboarding
14
- - **8 new hook events** -- PostCompact, StopFailure, TaskCreated, CwdChanged, FileChanged, PermissionDenied, Elicitation, ElicitationResult
15
- - **MCP Tool Search** -- lazy loading reduces context usage by 95%
16
- - **Security hardening** -- 7 CVE fixes, subprocess sandboxing, source code leak incident documented
9
+ ## What's New in v8.0 (Breaking — Spec Anthropic)
10
+
11
+ - **Strict alignment to Anthropic Agent Skills spec** -- 41/41 skills conformes, validation CI (`Dev/scripts/validate-skills-spec.sh`)
12
+ - **5 new skills** (v7.31-v7.32) -- `atomic-tasks` (GSD), `design-md-convention`, `architect`, `debug-methodical`, `socratic-brainstorm`
13
+ - **4 new agents** (v7.34) -- `@security-auditor`, `@data-analyst`, `@migration-specialist`, `@cost-optimizer`
14
+ - **2 new commands** (v7.33-v7.34) -- `/common:pack-repo` (Repomix wrapper + fallback shell), `/uiux:generate-design-md`
15
+ - **Memory lifecycle hooks** (v7.35) -- 5 hooks + SQLite local, inspired by claude-mem
16
+ - **Rule 23 Karpathy** (v7.31) -- principes AI-first development
17
+ - **Convention DESIGN.md** (v7.31) -- template AI-friendly + `/uiux:generate-design-md`
18
+ - **Migration guide** -- [docs/MIGRATION-v7-to-v8.md](docs/MIGRATION-v7-to-v8.md)
17
19
  - See [CHANGELOG](CHANGELOG.md) for full details
18
20
 
19
21
  ## Install and First Result
@@ -118,7 +120,7 @@ These are the commands you'll use most:
118
120
  | `/common:ralph-run "task"` | Run Claude in continuous loop until task is done |
119
121
  | `/qa:recette` | Automated acceptance testing via Chrome |
120
122
 
121
- See [CLI Reference](docs/CLI-REFERENCE.md) for all 204 commands across 26 namespaces.
123
+ See [CLI Reference](docs/CLI-REFERENCE.md) for all 214 commands across 27 namespaces.
122
124
 
123
125
  ## Installation
124
126
 
@@ -183,8 +185,8 @@ Context usage is optimized: ~3,500 tokens always loaded vs ~70,000 if everything
183
185
  | [Installation](docs/INSTALLATION.md) | All installation methods |
184
186
  | [Configuration](docs/CONFIGURATION.md) | Project configuration |
185
187
  | [CLI Reference](docs/CLI-REFERENCE.md) | Full CLI documentation |
186
- | [Commands](docs/COMMANDS.md) | All 204 commands |
187
- | [Agents](docs/AGENTS.md) | All 63 agents |
188
+ | [Commands](docs/COMMANDS.md) | All 214 commands |
189
+ | [Agents](docs/AGENTS.md) | All 67 agents |
188
190
  | [Skills](docs/SKILLS.md) | Best practices reference |
189
191
  | [Technologies](docs/TECHNOLOGIES.md) | Stack-specific guides |
190
192
  | [BMAD Guide](docs/BMAD-PRACTICAL-GUIDE.md) | Project management framework |
package/cli/index.js CHANGED
@@ -38,6 +38,7 @@ import { runCheck } from './lib/check.js';
38
38
  import { runList } from './lib/list.js';
39
39
  import { runDoctor } from './lib/doctor.js';
40
40
  import { runUpdate } from './lib/update.js';
41
+ import { runKanban } from './lib/kanban.js';
41
42
 
42
43
  // Flattener module
43
44
  import { flatten as flattenCodebaseFn } from './flattener.js';
@@ -204,6 +205,11 @@ class ClaudeCraftCLI {
204
205
  await runRalph(this, args.slice(1), options, ctx);
205
206
  break;
206
207
 
208
+ case 'kanban':
209
+ printBanner(VERSION);
210
+ await runKanban({ targetPath: this.config.targetPath, options });
211
+ break;
212
+
207
213
  case 'help':
208
214
  case '--help':
209
215
  case '-h':
@@ -0,0 +1,17 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>claude-craft kanban</title>
7
+ <meta
8
+ http-equiv="Content-Security-Policy"
9
+ content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self';"
10
+ />
11
+ <link rel="stylesheet" href="./src/app.css" />
12
+ </head>
13
+ <body>
14
+ <div id="app"></div>
15
+ <script type="module" src="./src/main.js"></script>
16
+ </body>
17
+ </html>
@@ -0,0 +1,106 @@
1
+ <script>
2
+ import { onMount, onDestroy } from 'svelte';
3
+ import { route } from './lib/router.svelte.js';
4
+ import { store, loadStories, loadSprint, connectEvents, disconnectEvents, dismissToast } from './lib/store.svelte.js';
5
+ import KanbanView from './views/KanbanView.svelte';
6
+ import BacklogView from './views/BacklogView.svelte';
7
+ // Lazy-loaded : pulls heavy viz libs (uPlot, Cytoscape, marked+dompurify).
8
+ const lazyBurndown = () => import('./views/BurndownView.svelte');
9
+ const lazyDeps = () => import('./views/DepsView.svelte');
10
+ const lazyDocs = () => import('./views/DocsView.svelte');
11
+
12
+ onMount(async () => {
13
+ await Promise.all([loadStories(), loadSprint()]);
14
+ connectEvents();
15
+ });
16
+
17
+ onDestroy(() => disconnectEvents());
18
+
19
+ const navItems = [
20
+ { path: '/kanban', label: 'Kanban' },
21
+ { path: '/backlog', label: 'Backlog' },
22
+ { path: '/burndown', label: 'Burndown' },
23
+ { path: '/deps', label: 'Dependencies' },
24
+ { path: '/docs', label: 'Docs' },
25
+ ];
26
+
27
+ function currentPath() {
28
+ return '/' + (route.parts[0] ?? 'kanban');
29
+ }
30
+
31
+ let sprintLabel = $derived(store.sprint ? `${store.sprint.name} (${store.sprint.sprint_id})` : 'no sprint');
32
+ </script>
33
+
34
+ <div class="app-shell">
35
+ <aside class="sidebar" aria-label="Navigation">
36
+ <h1>claude-craft</h1>
37
+ <nav class="nav" aria-label="Views">
38
+ {#each navItems as item}
39
+ <a
40
+ href={'#' + item.path}
41
+ aria-current={currentPath() === item.path ? 'page' : undefined}
42
+ >{item.label}</a>
43
+ {/each}
44
+ </nav>
45
+
46
+ <h1>Sprint</h1>
47
+ <div style="font-size:12px; color: var(--fg-dim); padding: 0 10px;">
48
+ {sprintLabel}
49
+ </div>
50
+ </aside>
51
+
52
+ <header class="topbar" role="banner">
53
+ <div class="title">
54
+ {#if store.sprint}
55
+ {store.sprint.goal || store.sprint.name}
56
+ {:else}
57
+ Kanban
58
+ {/if}
59
+ </div>
60
+ <div class="status {store.connected ? 'connected' : 'disconnected'}" aria-live="polite">
61
+ {store.connected ? '● live' : '○ offline'}
62
+ </div>
63
+ </header>
64
+
65
+ <main class="main" id="main">
66
+ {#if store.error && store.stories.length === 0}
67
+ <div class="empty">
68
+ <strong>Cannot reach server.</strong><br/>
69
+ {store.error}
70
+ </div>
71
+ {:else if currentPath() === '/kanban'}
72
+ <KanbanView />
73
+ {:else if currentPath() === '/backlog'}
74
+ <BacklogView />
75
+ {:else if currentPath() === '/burndown'}
76
+ {#await lazyBurndown()}
77
+ <div class="empty">Loading…</div>
78
+ {:then mod}
79
+ {@const Comp = mod.default}
80
+ <Comp />
81
+ {/await}
82
+ {:else if currentPath() === '/deps'}
83
+ {#await lazyDeps()}
84
+ <div class="empty">Loading…</div>
85
+ {:then mod}
86
+ {@const Comp = mod.default}
87
+ <Comp />
88
+ {/await}
89
+ {:else if currentPath() === '/docs'}
90
+ {#await lazyDocs()}
91
+ <div class="empty">Loading…</div>
92
+ {:then mod}
93
+ {@const Comp = mod.default}
94
+ <Comp />
95
+ {/await}
96
+ {/if}
97
+ </main>
98
+ </div>
99
+
100
+ <div class="toast-layer" aria-live="polite">
101
+ {#each store.toasts as t (t.id)}
102
+ <div class="toast {t.kind}" role="status" onclick={() => dismissToast(t.id)}>
103
+ {t.message}
104
+ </div>
105
+ {/each}
106
+ </div>
@@ -0,0 +1,175 @@
1
+ :root {
2
+ color-scheme: light dark;
3
+ --bg: #fafafa;
4
+ --bg-elev: #ffffff;
5
+ --bg-sidebar: #f3f3f5;
6
+ --fg: #1a1a1a;
7
+ --fg-dim: #6b7280;
8
+ --border: #e4e4e7;
9
+ --accent: #7c3aed;
10
+ --accent-fg: #ffffff;
11
+ --danger: #dc2626;
12
+ --warn: #d97706;
13
+ --ok: #16a34a;
14
+ --info: #0284c7;
15
+ --shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
16
+ --radius: 6px;
17
+ --font: 'Inter', system-ui, -apple-system, 'Segoe UI', sans-serif;
18
+ --mono: 'JetBrains Mono', ui-monospace, 'SF Mono', monospace;
19
+ }
20
+
21
+ @media (prefers-color-scheme: dark) {
22
+ :root {
23
+ --bg: #0f0f11;
24
+ --bg-elev: #1a1a1d;
25
+ --bg-sidebar: #141417;
26
+ --fg: #f4f4f5;
27
+ --fg-dim: #a1a1aa;
28
+ --border: #27272a;
29
+ --accent: #a78bfa;
30
+ --shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
31
+ }
32
+ }
33
+
34
+ * {
35
+ box-sizing: border-box;
36
+ }
37
+
38
+ html,
39
+ body,
40
+ #app {
41
+ margin: 0;
42
+ padding: 0;
43
+ height: 100%;
44
+ background: var(--bg);
45
+ color: var(--fg);
46
+ font-family: var(--font);
47
+ font-size: 14px;
48
+ line-height: 1.45;
49
+ }
50
+
51
+ button,
52
+ input,
53
+ select {
54
+ font: inherit;
55
+ color: inherit;
56
+ }
57
+
58
+ a {
59
+ color: var(--accent);
60
+ text-decoration: none;
61
+ }
62
+
63
+ .app-shell {
64
+ display: grid;
65
+ grid-template-columns: 240px 1fr;
66
+ grid-template-rows: 48px 1fr;
67
+ grid-template-areas:
68
+ 'sidebar topbar'
69
+ 'sidebar main';
70
+ height: 100vh;
71
+ }
72
+
73
+ .sidebar {
74
+ grid-area: sidebar;
75
+ background: var(--bg-sidebar);
76
+ border-right: 1px solid var(--border);
77
+ padding: 16px 12px;
78
+ overflow-y: auto;
79
+ }
80
+
81
+ .sidebar h1 {
82
+ font-size: 13px;
83
+ font-weight: 700;
84
+ margin: 0 0 12px;
85
+ color: var(--fg-dim);
86
+ letter-spacing: 0.04em;
87
+ text-transform: uppercase;
88
+ }
89
+
90
+ .nav {
91
+ display: flex;
92
+ flex-direction: column;
93
+ gap: 2px;
94
+ margin-bottom: 20px;
95
+ }
96
+
97
+ .nav a {
98
+ padding: 6px 10px;
99
+ border-radius: var(--radius);
100
+ color: var(--fg);
101
+ font-weight: 500;
102
+ }
103
+ .nav a:hover {
104
+ background: var(--border);
105
+ }
106
+ .nav a[aria-current='page'] {
107
+ background: var(--accent);
108
+ color: var(--accent-fg);
109
+ }
110
+
111
+ .topbar {
112
+ grid-area: topbar;
113
+ background: var(--bg-elev);
114
+ border-bottom: 1px solid var(--border);
115
+ padding: 0 20px;
116
+ display: flex;
117
+ align-items: center;
118
+ gap: 16px;
119
+ }
120
+
121
+ .topbar .title {
122
+ font-weight: 600;
123
+ flex: 1;
124
+ }
125
+
126
+ .topbar .status {
127
+ color: var(--fg-dim);
128
+ font-size: 12px;
129
+ font-family: var(--mono);
130
+ }
131
+ .topbar .status.connected {
132
+ color: var(--ok);
133
+ }
134
+ .topbar .status.disconnected {
135
+ color: var(--danger);
136
+ }
137
+
138
+ .main {
139
+ grid-area: main;
140
+ overflow: auto;
141
+ padding: 20px;
142
+ }
143
+
144
+ .empty {
145
+ text-align: center;
146
+ padding: 80px 20px;
147
+ color: var(--fg-dim);
148
+ }
149
+
150
+ .toast-layer {
151
+ position: fixed;
152
+ bottom: 20px;
153
+ right: 20px;
154
+ display: flex;
155
+ flex-direction: column;
156
+ gap: 8px;
157
+ z-index: 1000;
158
+ }
159
+ .toast {
160
+ background: var(--bg-elev);
161
+ border: 1px solid var(--border);
162
+ border-radius: var(--radius);
163
+ padding: 10px 14px;
164
+ box-shadow: var(--shadow);
165
+ max-width: 360px;
166
+ }
167
+ .toast.error {
168
+ border-left: 3px solid var(--danger);
169
+ }
170
+ .toast.success {
171
+ border-left: 3px solid var(--ok);
172
+ }
173
+ .toast.info {
174
+ border-left: 3px solid var(--info);
175
+ }
@@ -0,0 +1,19 @@
1
+ // Minimal hash-based router using Svelte 5 runes.
2
+ // Routes : #/kanban, #/backlog, #/burndown, #/deps, #/docs[/<path>]
3
+
4
+ function parse() {
5
+ const raw = window.location.hash.slice(1) || '/kanban';
6
+ const [pathname, query = ''] = raw.split('?');
7
+ const parts = pathname.split('/').filter(Boolean);
8
+ return { pathname, parts, query };
9
+ }
10
+
11
+ export const route = $state(parse());
12
+
13
+ window.addEventListener('hashchange', () => {
14
+ Object.assign(route, parse());
15
+ });
16
+
17
+ export function navigate(path) {
18
+ window.location.hash = path.startsWith('#') ? path : `#${path}`;
19
+ }
@@ -0,0 +1,132 @@
1
+ // Central store for the Kanban UI, backed by Svelte 5 runes.
2
+ // Normalized caches + SSE-driven invalidation.
3
+
4
+ export const store = $state({
5
+ stories: [],
6
+ epics: [],
7
+ sprint: null,
8
+ burndown: null,
9
+ connected: false,
10
+ loading: false,
11
+ error: null,
12
+ toasts: [],
13
+ });
14
+
15
+ let es = null;
16
+ let toastSeq = 0;
17
+
18
+ export function toast({ kind = 'info', message, ttl = 3500 }) {
19
+ const id = ++toastSeq;
20
+ store.toasts.push({ id, kind, message });
21
+ setTimeout(() => {
22
+ const idx = store.toasts.findIndex((t) => t.id === id);
23
+ if (idx >= 0) store.toasts.splice(idx, 1);
24
+ }, ttl);
25
+ }
26
+
27
+ export function dismissToast(id) {
28
+ const idx = store.toasts.findIndex((t) => t.id === id);
29
+ if (idx >= 0) store.toasts.splice(idx, 1);
30
+ }
31
+
32
+ async function request(url, init = {}) {
33
+ const res = await fetch(url, {
34
+ ...init,
35
+ headers: { 'Content-Type': 'application/json', ...(init.headers ?? {}) },
36
+ });
37
+ if (!res.ok) {
38
+ let body;
39
+ try {
40
+ body = await res.json();
41
+ } catch {
42
+ body = { error: res.statusText };
43
+ }
44
+ const err = new Error(body.error || `HTTP ${res.status}`);
45
+ err.status = res.status;
46
+ err.body = body;
47
+ throw err;
48
+ }
49
+ return res.json();
50
+ }
51
+
52
+ export async function loadStories() {
53
+ store.loading = true;
54
+ try {
55
+ const data = await request('/api/stories');
56
+ store.stories = data.stories;
57
+ store.epics = data.epics;
58
+ store.error = null;
59
+ } catch (err) {
60
+ store.error = err.message;
61
+ toast({ kind: 'error', message: `Failed to load stories: ${err.message}` });
62
+ } finally {
63
+ store.loading = false;
64
+ }
65
+ }
66
+
67
+ export async function loadSprint() {
68
+ try {
69
+ const data = await request('/api/sprints/current');
70
+ store.sprint = data.sprint;
71
+ store.burndown = data.burndown;
72
+ } catch (err) {
73
+ if (err.status !== 404) toast({ kind: 'error', message: `Sprint load failed: ${err.message}` });
74
+ store.sprint = null;
75
+ store.burndown = null;
76
+ }
77
+ }
78
+
79
+ export async function patchStatus(id, body) {
80
+ const prev = store.stories.find((s) => s.id === id);
81
+ if (!prev) throw new Error('story not found');
82
+ // Optimistic update on structural transitions (skip on blocked without reason)
83
+ const idx = store.stories.findIndex((s) => s.id === id);
84
+ const before = { ...prev };
85
+ store.stories[idx] = { ...prev, status: body.status };
86
+ try {
87
+ const data = await request(`/api/stories/${id}/status`, {
88
+ method: 'PATCH',
89
+ body: JSON.stringify(body),
90
+ });
91
+ store.stories[idx] = data.story;
92
+ toast({ kind: 'success', message: `${id} -> ${data.story.status}` });
93
+ return data.story;
94
+ } catch (err) {
95
+ store.stories[idx] = before;
96
+ const details = err.body?.missing?.length ? ` (${err.body.missing.join(', ')})` : '';
97
+ toast({ kind: 'error', message: `Transition failed: ${err.body?.reason ?? err.message}${details}`, ttl: 5000 });
98
+ throw err;
99
+ }
100
+ }
101
+
102
+ export function connectEvents() {
103
+ if (es) return;
104
+ es = new EventSource('/api/events');
105
+ es.addEventListener('open', () => {
106
+ store.connected = true;
107
+ });
108
+ es.addEventListener('error', () => {
109
+ store.connected = false;
110
+ });
111
+ es.addEventListener('story:updated', async () => {
112
+ await loadStories();
113
+ });
114
+ es.addEventListener('file:changed', async (e) => {
115
+ try {
116
+ const { payload } = JSON.parse(e.data);
117
+ if (payload?.category === 'story' || payload?.category === 'task' || payload?.category === 'epic') {
118
+ await loadStories();
119
+ }
120
+ } catch {
121
+ await loadStories();
122
+ }
123
+ });
124
+ }
125
+
126
+ export function disconnectEvents() {
127
+ if (es) {
128
+ es.close();
129
+ es = null;
130
+ store.connected = false;
131
+ }
132
+ }
@@ -0,0 +1,6 @@
1
+ import { mount } from 'svelte';
2
+ import App from './App.svelte';
3
+
4
+ const app = mount(App, { target: document.getElementById('app') });
5
+
6
+ export default app;