@prmichaelsen/acp-visualizer 0.8.1 → 0.8.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prmichaelsen/acp-visualizer",
3
- "version": "0.8.1",
3
+ "version": "0.8.2",
4
4
  "type": "module",
5
5
  "description": "Browser-based dashboard for visualizing ACP progress.yaml data",
6
6
  "bin": {
@@ -0,0 +1,100 @@
1
+ import { useState, useEffect, useMemo } from 'react'
2
+ import { Breadcrumb } from './Breadcrumb'
3
+ import { MarkdownContent, buildLinkMap } from './MarkdownContent'
4
+ import { getMarkdownContent, listAgentDirectory } from '../services/markdown.service'
5
+ import { useProgressData } from '../contexts/ProgressContext'
6
+ import type { MarkdownResult, AgentFile } from '../services/markdown.service'
7
+
8
+ interface DocumentDetailProps {
9
+ slug: string
10
+ dirPath: string
11
+ sectionLabel: string
12
+ sectionHref: string
13
+ }
14
+
15
+ /** Read ?repo=owner/repo from URL */
16
+ function getGitHubParams(): { owner: string; repo: string } | undefined {
17
+ if (typeof window === 'undefined') return undefined
18
+ const params = new URLSearchParams(window.location.search)
19
+ const repo = params.get('repo')
20
+ if (!repo) return undefined
21
+ const parts = repo.split('/')
22
+ if (parts.length < 2) return undefined
23
+ return { owner: parts[0], repo: parts[1] }
24
+ }
25
+
26
+ export function DocumentDetail({ slug, dirPath, sectionLabel, sectionHref }: DocumentDetailProps) {
27
+ const data = useProgressData()
28
+ const [markdown, setMarkdown] = useState<string | null>(null)
29
+ const [error, setError] = useState<string | null>(null)
30
+ const [loading, setLoading] = useState(true)
31
+ const [filePath, setFilePath] = useState<string | null>(null)
32
+ const linkMap = useMemo(() => (data ? buildLinkMap(data) : {}), [data])
33
+
34
+ useEffect(() => {
35
+ setLoading(true)
36
+ setMarkdown(null)
37
+ setError(null)
38
+
39
+ const github = getGitHubParams()
40
+ const relativePath = `${dirPath}/${slug}.md`
41
+
42
+ // Try the direct path first, fallback to directory listing if needed
43
+ getMarkdownContent({ data: { filePath: relativePath, github } })
44
+ .then((result: MarkdownResult) => {
45
+ if (result.ok) {
46
+ setMarkdown(result.content)
47
+ setFilePath(result.filePath)
48
+ } else {
49
+ // Fallback: search directory for a file containing the slug
50
+ return listAgentDirectory({ data: { dirPath, github } }).then((listResult) => {
51
+ if (listResult.ok) {
52
+ const match = listResult.files.find((f: AgentFile) => f.name === slug)
53
+ if (match) {
54
+ return getMarkdownContent({ data: { filePath: match.relativePath, github } })
55
+ .then((mdResult: MarkdownResult) => {
56
+ if (mdResult.ok) {
57
+ setMarkdown(mdResult.content)
58
+ setFilePath(mdResult.filePath)
59
+ } else {
60
+ setError(mdResult.error)
61
+ }
62
+ })
63
+ }
64
+ }
65
+ setError(result.error)
66
+ })
67
+ }
68
+ })
69
+ .catch((err: Error) => setError(err.message))
70
+ .finally(() => setLoading(false))
71
+ }, [slug, dirPath])
72
+
73
+ const displayName = slug
74
+ .replace(/^[a-z0-9-]+\./, '')
75
+ .replace(/[-_]/g, ' ')
76
+ .replace(/\b\w/g, (c) => c.toUpperCase())
77
+
78
+ return (
79
+ <div className="p-6 max-w-4xl">
80
+ <Breadcrumb
81
+ items={[
82
+ { label: sectionLabel, href: sectionHref },
83
+ { label: displayName },
84
+ ]}
85
+ />
86
+
87
+ <h1 className="text-xl font-semibold text-gray-100 mb-6">{displayName}</h1>
88
+
89
+ {loading ? (
90
+ <p className="text-sm text-gray-600">Loading document...</p>
91
+ ) : markdown ? (
92
+ <MarkdownContent content={markdown} basePath={filePath ?? undefined} linkMap={linkMap} />
93
+ ) : error ? (
94
+ <div className="bg-gray-900/50 border border-gray-800 rounded-xl p-4 text-sm text-gray-500">
95
+ No document found — {error}
96
+ </div>
97
+ ) : null}
98
+ </div>
99
+ )
100
+ }
@@ -0,0 +1,90 @@
1
+ import { Link } from '@tanstack/react-router'
2
+ import { useState, useEffect } from 'react'
3
+ import { listAgentDirectory } from '../services/markdown.service'
4
+ import type { AgentFile } from '../services/markdown.service'
5
+ import { FileText } from 'lucide-react'
6
+
7
+ interface DocumentListProps {
8
+ title: string
9
+ dirPath: string
10
+ baseTo: string
11
+ github?: { owner: string; repo: string; branch?: string; token?: string }
12
+ }
13
+
14
+ export function DocumentList({ title, dirPath, baseTo, github }: DocumentListProps) {
15
+ const [files, setFiles] = useState<AgentFile[]>([])
16
+ const [error, setError] = useState<string | null>(null)
17
+ const [loading, setLoading] = useState(true)
18
+
19
+ useEffect(() => {
20
+ setLoading(true)
21
+ listAgentDirectory({ data: { dirPath, github } })
22
+ .then((result) => {
23
+ if (result.ok) {
24
+ setFiles(result.files)
25
+ } else {
26
+ setError(result.error)
27
+ }
28
+ })
29
+ .catch((err: Error) => setError(err.message))
30
+ .finally(() => setLoading(false))
31
+ }, [dirPath, github])
32
+
33
+ if (loading) {
34
+ return (
35
+ <div className="p-6">
36
+ <h2 className="text-lg font-semibold mb-4">{title}</h2>
37
+ <p className="text-sm text-gray-600">Loading...</p>
38
+ </div>
39
+ )
40
+ }
41
+
42
+ if (error) {
43
+ return (
44
+ <div className="p-6">
45
+ <h2 className="text-lg font-semibold mb-4">{title}</h2>
46
+ <div className="bg-gray-900/50 border border-gray-800 rounded-xl p-4 text-sm text-gray-500">
47
+ {error}
48
+ </div>
49
+ </div>
50
+ )
51
+ }
52
+
53
+ if (files.length === 0) {
54
+ return (
55
+ <div className="p-6">
56
+ <h2 className="text-lg font-semibold mb-4">{title}</h2>
57
+ <p className="text-sm text-gray-500">No documents found in <code className="text-gray-400">{dirPath}/</code></p>
58
+ </div>
59
+ )
60
+ }
61
+
62
+ return (
63
+ <div className="p-6">
64
+ <h2 className="text-lg font-semibold mb-4">{title}</h2>
65
+ <div className="bg-gray-900/50 border border-gray-800 rounded-xl divide-y divide-gray-800">
66
+ {files.map((file) => (
67
+ <Link
68
+ key={file.name}
69
+ to={baseTo + '/$slug'}
70
+ params={{ slug: file.name }}
71
+ className="flex items-center gap-3 px-4 py-3 text-sm hover:bg-gray-800/50 transition-colors first:rounded-t-xl last:rounded-b-xl"
72
+ >
73
+ <FileText className="w-4 h-4 text-gray-500 shrink-0" />
74
+ <span className="text-gray-200">{formatName(file.name)}</span>
75
+ </Link>
76
+ ))}
77
+ </div>
78
+ <p className="text-xs text-gray-600 mt-3">{files.length} document{files.length !== 1 ? 's' : ''}</p>
79
+ </div>
80
+ )
81
+ }
82
+
83
+ /** Turn "local.dashboard-layout-routing" into "Dashboard Layout Routing" */
84
+ function formatName(name: string): string {
85
+ // Strip common prefixes like "local." or "core-sdk."
86
+ const stripped = name.replace(/^[a-z0-9-]+\./, '')
87
+ return stripped
88
+ .replace(/[-_]/g, ' ')
89
+ .replace(/\b\w/g, (c) => c.toUpperCase())
90
+ }
@@ -1,5 +1,6 @@
1
1
  import { Link } from '@tanstack/react-router'
2
2
  import { StatusBadge } from './StatusBadge'
3
+ import { PriorityBadge } from './PriorityBadge'
3
4
  import { ProgressBar } from './ProgressBar'
4
5
  import { TaskList } from './TaskList'
5
6
  import type { Milestone, Task, Status } from '../lib/types'
@@ -37,6 +38,7 @@ function KanbanCard({
37
38
  >
38
39
  {milestone.name}
39
40
  </Link>
41
+ <PriorityBadge priority={milestone.priority} />
40
42
  </div>
41
43
  <div className="flex items-center gap-2 mb-2">
42
44
  <div className="flex-1">
@@ -10,6 +10,7 @@ import {
10
10
  } from '@tanstack/react-table'
11
11
  import { ChevronDown, ChevronRight, ArrowUpDown } from 'lucide-react'
12
12
  import { StatusBadge } from './StatusBadge'
13
+ import { PriorityBadge } from './PriorityBadge'
13
14
  import { ProgressBar } from './ProgressBar'
14
15
  import { TaskList } from './TaskList'
15
16
  import type { Milestone, Task } from '../lib/types'
@@ -68,6 +69,11 @@ export function MilestoneTable({ milestones, tasks }: MilestoneTableProps) {
68
69
  cell: (info) => <StatusBadge status={info.getValue()} />,
69
70
  size: 120,
70
71
  }),
72
+ columnHelper.accessor('priority', {
73
+ header: 'Priority',
74
+ cell: (info) => <PriorityBadge priority={info.getValue()} />,
75
+ size: 100,
76
+ }),
71
77
  columnHelper.accessor('progress', {
72
78
  header: 'Progress',
73
79
  cell: (info) => (
@@ -2,6 +2,7 @@ import { useState } from 'react'
2
2
  import { Link } from '@tanstack/react-router'
3
3
  import { ChevronDown, ChevronRight } from 'lucide-react'
4
4
  import { StatusBadge } from './StatusBadge'
5
+ import { PriorityBadge } from './PriorityBadge'
5
6
  import { ProgressBar } from './ProgressBar'
6
7
  import { TaskList } from './TaskList'
7
8
  import { useCollapse } from '../lib/useCollapse'
@@ -45,6 +46,7 @@ function MilestoneTreeRow({
45
46
  {milestone.name}
46
47
  </Link>
47
48
  <StatusBadge status={milestone.status} />
49
+ <PriorityBadge priority={milestone.priority} />
48
50
  <div className="w-20">
49
51
  <ProgressBar value={milestone.progress} size="sm" />
50
52
  </div>
@@ -0,0 +1,20 @@
1
+ import type { Priority } from '../lib/types'
2
+
3
+ const priorityStyles: Record<Priority, string> = {
4
+ critical: 'bg-rose-500/20 text-rose-400',
5
+ high: 'bg-amber-500/20 text-amber-400',
6
+ medium: 'bg-blue-500/20 text-blue-400',
7
+ low: 'bg-zinc-500/20 text-zinc-400',
8
+ }
9
+
10
+ export function PriorityBadge({ priority }: { priority: Priority | undefined }) {
11
+ if (!priority) return null
12
+
13
+ return (
14
+ <span
15
+ className={`inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium uppercase ${priorityStyles[priority] ?? priorityStyles.medium}`}
16
+ >
17
+ {priority}
18
+ </span>
19
+ )
20
+ }
@@ -1,5 +1,5 @@
1
1
  import { Link, useRouterState } from '@tanstack/react-router'
2
- import { LayoutDashboard, Flag, CheckSquare, Clock, Search } from 'lucide-react'
2
+ import { LayoutDashboard, Flag, CheckSquare, Clock, Search, PenTool, Puzzle, FileBarChart } from 'lucide-react'
3
3
  import { ProjectSelector } from './ProjectSelector'
4
4
  import { GitHubInput } from './GitHubInput'
5
5
  import type { AcpProject } from '../services/projects.service'
@@ -9,6 +9,9 @@ const navItems = [
9
9
  { to: '/milestones' as const, icon: Flag, label: 'Milestones' },
10
10
  { to: '/tasks' as const, icon: CheckSquare, label: 'Tasks' },
11
11
  { to: '/activity' as const, icon: Clock, label: 'Activity' },
12
+ { to: '/designs' as const, icon: PenTool, label: 'Designs' },
13
+ { to: '/patterns' as const, icon: Puzzle, label: 'Patterns' },
14
+ { to: '/reports' as const, icon: FileBarChart, label: 'Reports' },
12
15
  ]
13
16
 
14
17
  interface SidebarProps {
@@ -1,5 +1,6 @@
1
1
  import { Link } from '@tanstack/react-router'
2
2
  import { StatusDot } from './StatusDot'
3
+ import { PriorityBadge } from './PriorityBadge'
3
4
  import { ExtraFieldsBadge } from './ExtraFieldsBadge'
4
5
  import type { Task } from '../lib/types'
5
6
 
@@ -26,6 +27,7 @@ export function TaskList({ tasks }: { tasks: Task[] }) {
26
27
  >
27
28
  {task.name}
28
29
  </Link>
30
+ <PriorityBadge priority={task.priority} />
29
31
  {task.notes && (
30
32
  <span className="text-xs text-gray-600 ml-auto truncate max-w-[200px]">
31
33
  {task.notes}
package/src/lib/types.ts CHANGED
@@ -1,6 +1,9 @@
1
1
  /** Status values for milestones and tasks */
2
2
  export type Status = 'completed' | 'in_progress' | 'not_started' | 'wont_do'
3
3
 
4
+ /** Priority values for milestones and tasks (ACP 6.0.0+) */
5
+ export type Priority = 'critical' | 'high' | 'medium' | 'low'
6
+
4
7
  /** Unknown properties from agent-maintained YAML are preserved here */
5
8
  export type ExtraFields = Record<string, unknown>
6
9
 
@@ -29,6 +32,8 @@ export interface ProjectMetadata {
29
32
  export interface Milestone {
30
33
  id: string
31
34
  name: string
35
+ priority: Priority
36
+ file: string
32
37
  status: Status
33
38
  progress: number // 0-100
34
39
  started: string | null
@@ -43,10 +48,13 @@ export interface Milestone {
43
48
  export interface Task {
44
49
  id: string
45
50
  name: string
51
+ priority: Priority
46
52
  status: Status
47
53
  milestone_id: string
48
54
  file: string
49
55
  estimated_hours: string
56
+ actual_hours: number | null
57
+ started: string | null
50
58
  completed_date: string | null
51
59
  notes: string
52
60
  extra: ExtraFields
@@ -8,6 +8,7 @@ import type {
8
8
  DocumentationStats,
9
9
  ProgressSummary,
10
10
  Status,
11
+ Priority,
11
12
  ExtraFields,
12
13
  } from './types'
13
14
 
@@ -51,6 +52,14 @@ function normalizeStatus(value: unknown): Status {
51
52
  return 'not_started'
52
53
  }
53
54
 
55
+ function normalizePriority(value: unknown): Priority {
56
+ const s = String(value || 'medium').toLowerCase()
57
+ if (s === 'critical') return 'critical'
58
+ if (s === 'high') return 'high'
59
+ if (s === 'low') return 'low'
60
+ return 'medium'
61
+ }
62
+
54
63
  function safeString(value: unknown, fallback = ''): string {
55
64
  if (value == null) return fallback
56
65
  return String(value)
@@ -127,7 +136,7 @@ function normalizeProject(raw: unknown): ProjectMetadata {
127
136
  }
128
137
 
129
138
  const MILESTONE_KEYS = [
130
- 'id', 'name', 'status', 'progress', 'started', 'completed',
139
+ 'id', 'name', 'priority', 'file', 'status', 'progress', 'started', 'completed',
131
140
  'estimated_weeks', 'tasks_completed', 'tasks_total', 'notes',
132
141
  ]
133
142
 
@@ -137,6 +146,8 @@ function normalizeMilestone(raw: unknown, index: number): Milestone {
137
146
  return {
138
147
  id: safeString(known.id, `milestone_${index + 1}`),
139
148
  name: safeString(known.name, `Milestone ${index + 1}`),
149
+ priority: normalizePriority(known.priority),
150
+ file: safeString(known.file),
140
151
  status: normalizeStatus(known.status),
141
152
  progress: known.progress != null
142
153
  ? safeNumber(known.progress)
@@ -154,13 +165,25 @@ function normalizeMilestone(raw: unknown, index: number): Milestone {
154
165
  }
155
166
 
156
167
  function normalizeMilestones(raw: unknown): Milestone[] {
157
- if (!Array.isArray(raw)) return []
158
- return raw.map((item, i) => normalizeMilestone(item, i))
168
+ // v6 format: milestones is a map keyed by ID (e.g. { M1: {...}, M2: {...} })
169
+ if (raw && typeof raw === 'object' && !Array.isArray(raw)) {
170
+ const obj = raw as Record<string, unknown>
171
+ return Object.entries(obj).map(([key, value], i) => {
172
+ const m = normalizeMilestone(value, i)
173
+ // The map key IS the milestone ID — override whatever was parsed
174
+ return { ...m, id: key }
175
+ })
176
+ }
177
+ // Pre-v6 format: milestones is an array
178
+ if (Array.isArray(raw)) {
179
+ return raw.map((item, i) => normalizeMilestone(item, i))
180
+ }
181
+ return []
159
182
  }
160
183
 
161
184
  const TASK_KEYS = [
162
- 'id', 'name', 'status', 'milestone_id', 'file',
163
- 'estimated_hours', 'completed_date', 'notes',
185
+ 'id', 'name', 'priority', 'status', 'milestone_id', 'file',
186
+ 'estimated_hours', 'actual_hours', 'started', 'completed_date', 'notes',
164
187
  ]
165
188
 
166
189
  function normalizeTask(raw: unknown, milestoneId: string, index: number): Task {
@@ -169,10 +192,13 @@ function normalizeTask(raw: unknown, milestoneId: string, index: number): Task {
169
192
  return {
170
193
  id: safeString(known.id, `task_${index + 1}`),
171
194
  name: safeString(known.name, `Task ${index + 1}`),
195
+ priority: normalizePriority(known.priority),
172
196
  status: normalizeStatus(known.status),
173
197
  milestone_id: safeString(known.milestone_id, milestoneId),
174
198
  file: safeString(known.file),
175
199
  estimated_hours: safeString(known.estimated_hours, '0'),
200
+ actual_hours: known.actual_hours != null ? safeNumber(known.actual_hours) : null,
201
+ started: known.started ? safeString(known.started) : null,
176
202
  completed_date: known.completed_date ? safeString(known.completed_date) : null,
177
203
  notes: safeString(known.notes),
178
204
  extra,
@@ -388,20 +414,30 @@ export function parseProgressYaml(raw: string): ProgressData {
388
414
  if (looseTasks.length > 0) {
389
415
  const existing = allTasks['_unassigned'] || []
390
416
  allTasks['_unassigned'] = [...existing, ...looseTasks]
391
- // Add a synthetic milestone for unassigned tasks if not already present
392
- if (!seenIds.has('_unassigned')) {
393
- seenIds.add('_unassigned')
417
+ }
418
+
419
+ // Create synthetic milestones for any task keys without a matching milestone.
420
+ // This handles `unassigned:` (or any other key) inside the `tasks:` object
421
+ // that doesn't correspond to a declared milestone.
422
+ for (const key of Object.keys(allTasks)) {
423
+ if (!seenIds.has(key)) {
424
+ seenIds.add(key)
425
+ const tasks = allTasks[key]
426
+ const isUnassigned = key === '_unassigned' || key.toLowerCase() === 'unassigned'
427
+ const displayName = isUnassigned ? 'Unassigned Tasks' : key
394
428
  allMilestones.push({
395
- id: '_unassigned',
396
- name: 'Unassigned Tasks',
429
+ id: key,
430
+ name: displayName,
431
+ priority: 'medium',
432
+ file: '',
397
433
  status: 'in_progress',
398
434
  progress: 0,
399
435
  started: null,
400
436
  completed: null,
401
437
  estimated_weeks: '0',
402
- tasks_completed: looseTasks.filter((t) => t.status === 'completed').length,
403
- tasks_total: looseTasks.length,
404
- notes: 'Tasks not assigned to a specific milestone',
438
+ tasks_completed: tasks.filter((t) => t.status === 'completed').length,
439
+ tasks_total: tasks.length,
440
+ notes: isUnassigned ? 'Tasks not assigned to a specific milestone' : `Tasks under "${key}"`,
405
441
  extra: { synthetic: true },
406
442
  })
407
443
  }
@@ -11,13 +11,22 @@
11
11
  import { Route as rootRouteImport } from './routes/__root'
12
12
  import { Route as TasksRouteImport } from './routes/tasks'
13
13
  import { Route as SearchRouteImport } from './routes/search'
14
+ import { Route as ReportsRouteImport } from './routes/reports'
15
+ import { Route as PatternsRouteImport } from './routes/patterns'
14
16
  import { Route as MilestonesRouteImport } from './routes/milestones'
17
+ import { Route as DesignsRouteImport } from './routes/designs'
15
18
  import { Route as ActivityRouteImport } from './routes/activity'
16
19
  import { Route as IndexRouteImport } from './routes/index'
17
20
  import { Route as TasksIndexRouteImport } from './routes/tasks.index'
21
+ import { Route as ReportsIndexRouteImport } from './routes/reports.index'
22
+ import { Route as PatternsIndexRouteImport } from './routes/patterns.index'
18
23
  import { Route as MilestonesIndexRouteImport } from './routes/milestones.index'
24
+ import { Route as DesignsIndexRouteImport } from './routes/designs.index'
19
25
  import { Route as TasksTaskIdRouteImport } from './routes/tasks.$taskId'
26
+ import { Route as ReportsSlugRouteImport } from './routes/reports.$slug'
27
+ import { Route as PatternsSlugRouteImport } from './routes/patterns.$slug'
20
28
  import { Route as MilestonesMilestoneIdRouteImport } from './routes/milestones.$milestoneId'
29
+ import { Route as DesignsSlugRouteImport } from './routes/designs.$slug'
21
30
  import { Route as ApiWatchRouteImport } from './routes/api/watch'
22
31
 
23
32
  const TasksRoute = TasksRouteImport.update({
@@ -30,11 +39,26 @@ const SearchRoute = SearchRouteImport.update({
30
39
  path: '/search',
31
40
  getParentRoute: () => rootRouteImport,
32
41
  } as any)
42
+ const ReportsRoute = ReportsRouteImport.update({
43
+ id: '/reports',
44
+ path: '/reports',
45
+ getParentRoute: () => rootRouteImport,
46
+ } as any)
47
+ const PatternsRoute = PatternsRouteImport.update({
48
+ id: '/patterns',
49
+ path: '/patterns',
50
+ getParentRoute: () => rootRouteImport,
51
+ } as any)
33
52
  const MilestonesRoute = MilestonesRouteImport.update({
34
53
  id: '/milestones',
35
54
  path: '/milestones',
36
55
  getParentRoute: () => rootRouteImport,
37
56
  } as any)
57
+ const DesignsRoute = DesignsRouteImport.update({
58
+ id: '/designs',
59
+ path: '/designs',
60
+ getParentRoute: () => rootRouteImport,
61
+ } as any)
38
62
  const ActivityRoute = ActivityRouteImport.update({
39
63
  id: '/activity',
40
64
  path: '/activity',
@@ -50,21 +74,51 @@ const TasksIndexRoute = TasksIndexRouteImport.update({
50
74
  path: '/',
51
75
  getParentRoute: () => TasksRoute,
52
76
  } as any)
77
+ const ReportsIndexRoute = ReportsIndexRouteImport.update({
78
+ id: '/',
79
+ path: '/',
80
+ getParentRoute: () => ReportsRoute,
81
+ } as any)
82
+ const PatternsIndexRoute = PatternsIndexRouteImport.update({
83
+ id: '/',
84
+ path: '/',
85
+ getParentRoute: () => PatternsRoute,
86
+ } as any)
53
87
  const MilestonesIndexRoute = MilestonesIndexRouteImport.update({
54
88
  id: '/',
55
89
  path: '/',
56
90
  getParentRoute: () => MilestonesRoute,
57
91
  } as any)
92
+ const DesignsIndexRoute = DesignsIndexRouteImport.update({
93
+ id: '/',
94
+ path: '/',
95
+ getParentRoute: () => DesignsRoute,
96
+ } as any)
58
97
  const TasksTaskIdRoute = TasksTaskIdRouteImport.update({
59
98
  id: '/$taskId',
60
99
  path: '/$taskId',
61
100
  getParentRoute: () => TasksRoute,
62
101
  } as any)
102
+ const ReportsSlugRoute = ReportsSlugRouteImport.update({
103
+ id: '/$slug',
104
+ path: '/$slug',
105
+ getParentRoute: () => ReportsRoute,
106
+ } as any)
107
+ const PatternsSlugRoute = PatternsSlugRouteImport.update({
108
+ id: '/$slug',
109
+ path: '/$slug',
110
+ getParentRoute: () => PatternsRoute,
111
+ } as any)
63
112
  const MilestonesMilestoneIdRoute = MilestonesMilestoneIdRouteImport.update({
64
113
  id: '/$milestoneId',
65
114
  path: '/$milestoneId',
66
115
  getParentRoute: () => MilestonesRoute,
67
116
  } as any)
117
+ const DesignsSlugRoute = DesignsSlugRouteImport.update({
118
+ id: '/$slug',
119
+ path: '/$slug',
120
+ getParentRoute: () => DesignsRoute,
121
+ } as any)
68
122
  const ApiWatchRoute = ApiWatchRouteImport.update({
69
123
  id: '/api/watch',
70
124
  path: '/api/watch',
@@ -74,13 +128,22 @@ const ApiWatchRoute = ApiWatchRouteImport.update({
74
128
  export interface FileRoutesByFullPath {
75
129
  '/': typeof IndexRoute
76
130
  '/activity': typeof ActivityRoute
131
+ '/designs': typeof DesignsRouteWithChildren
77
132
  '/milestones': typeof MilestonesRouteWithChildren
133
+ '/patterns': typeof PatternsRouteWithChildren
134
+ '/reports': typeof ReportsRouteWithChildren
78
135
  '/search': typeof SearchRoute
79
136
  '/tasks': typeof TasksRouteWithChildren
80
137
  '/api/watch': typeof ApiWatchRoute
138
+ '/designs/$slug': typeof DesignsSlugRoute
81
139
  '/milestones/$milestoneId': typeof MilestonesMilestoneIdRoute
140
+ '/patterns/$slug': typeof PatternsSlugRoute
141
+ '/reports/$slug': typeof ReportsSlugRoute
82
142
  '/tasks/$taskId': typeof TasksTaskIdRoute
143
+ '/designs/': typeof DesignsIndexRoute
83
144
  '/milestones/': typeof MilestonesIndexRoute
145
+ '/patterns/': typeof PatternsIndexRoute
146
+ '/reports/': typeof ReportsIndexRoute
84
147
  '/tasks/': typeof TasksIndexRoute
85
148
  }
86
149
  export interface FileRoutesByTo {
@@ -88,22 +151,37 @@ export interface FileRoutesByTo {
88
151
  '/activity': typeof ActivityRoute
89
152
  '/search': typeof SearchRoute
90
153
  '/api/watch': typeof ApiWatchRoute
154
+ '/designs/$slug': typeof DesignsSlugRoute
91
155
  '/milestones/$milestoneId': typeof MilestonesMilestoneIdRoute
156
+ '/patterns/$slug': typeof PatternsSlugRoute
157
+ '/reports/$slug': typeof ReportsSlugRoute
92
158
  '/tasks/$taskId': typeof TasksTaskIdRoute
159
+ '/designs': typeof DesignsIndexRoute
93
160
  '/milestones': typeof MilestonesIndexRoute
161
+ '/patterns': typeof PatternsIndexRoute
162
+ '/reports': typeof ReportsIndexRoute
94
163
  '/tasks': typeof TasksIndexRoute
95
164
  }
96
165
  export interface FileRoutesById {
97
166
  __root__: typeof rootRouteImport
98
167
  '/': typeof IndexRoute
99
168
  '/activity': typeof ActivityRoute
169
+ '/designs': typeof DesignsRouteWithChildren
100
170
  '/milestones': typeof MilestonesRouteWithChildren
171
+ '/patterns': typeof PatternsRouteWithChildren
172
+ '/reports': typeof ReportsRouteWithChildren
101
173
  '/search': typeof SearchRoute
102
174
  '/tasks': typeof TasksRouteWithChildren
103
175
  '/api/watch': typeof ApiWatchRoute
176
+ '/designs/$slug': typeof DesignsSlugRoute
104
177
  '/milestones/$milestoneId': typeof MilestonesMilestoneIdRoute
178
+ '/patterns/$slug': typeof PatternsSlugRoute
179
+ '/reports/$slug': typeof ReportsSlugRoute
105
180
  '/tasks/$taskId': typeof TasksTaskIdRoute
181
+ '/designs/': typeof DesignsIndexRoute
106
182
  '/milestones/': typeof MilestonesIndexRoute
183
+ '/patterns/': typeof PatternsIndexRoute
184
+ '/reports/': typeof ReportsIndexRoute
107
185
  '/tasks/': typeof TasksIndexRoute
108
186
  }
109
187
  export interface FileRouteTypes {
@@ -111,13 +189,22 @@ export interface FileRouteTypes {
111
189
  fullPaths:
112
190
  | '/'
113
191
  | '/activity'
192
+ | '/designs'
114
193
  | '/milestones'
194
+ | '/patterns'
195
+ | '/reports'
115
196
  | '/search'
116
197
  | '/tasks'
117
198
  | '/api/watch'
199
+ | '/designs/$slug'
118
200
  | '/milestones/$milestoneId'
201
+ | '/patterns/$slug'
202
+ | '/reports/$slug'
119
203
  | '/tasks/$taskId'
204
+ | '/designs/'
120
205
  | '/milestones/'
206
+ | '/patterns/'
207
+ | '/reports/'
121
208
  | '/tasks/'
122
209
  fileRoutesByTo: FileRoutesByTo
123
210
  to:
@@ -125,28 +212,46 @@ export interface FileRouteTypes {
125
212
  | '/activity'
126
213
  | '/search'
127
214
  | '/api/watch'
215
+ | '/designs/$slug'
128
216
  | '/milestones/$milestoneId'
217
+ | '/patterns/$slug'
218
+ | '/reports/$slug'
129
219
  | '/tasks/$taskId'
220
+ | '/designs'
130
221
  | '/milestones'
222
+ | '/patterns'
223
+ | '/reports'
131
224
  | '/tasks'
132
225
  id:
133
226
  | '__root__'
134
227
  | '/'
135
228
  | '/activity'
229
+ | '/designs'
136
230
  | '/milestones'
231
+ | '/patterns'
232
+ | '/reports'
137
233
  | '/search'
138
234
  | '/tasks'
139
235
  | '/api/watch'
236
+ | '/designs/$slug'
140
237
  | '/milestones/$milestoneId'
238
+ | '/patterns/$slug'
239
+ | '/reports/$slug'
141
240
  | '/tasks/$taskId'
241
+ | '/designs/'
142
242
  | '/milestones/'
243
+ | '/patterns/'
244
+ | '/reports/'
143
245
  | '/tasks/'
144
246
  fileRoutesById: FileRoutesById
145
247
  }
146
248
  export interface RootRouteChildren {
147
249
  IndexRoute: typeof IndexRoute
148
250
  ActivityRoute: typeof ActivityRoute
251
+ DesignsRoute: typeof DesignsRouteWithChildren
149
252
  MilestonesRoute: typeof MilestonesRouteWithChildren
253
+ PatternsRoute: typeof PatternsRouteWithChildren
254
+ ReportsRoute: typeof ReportsRouteWithChildren
150
255
  SearchRoute: typeof SearchRoute
151
256
  TasksRoute: typeof TasksRouteWithChildren
152
257
  ApiWatchRoute: typeof ApiWatchRoute
@@ -168,6 +273,20 @@ declare module '@tanstack/react-router' {
168
273
  preLoaderRoute: typeof SearchRouteImport
169
274
  parentRoute: typeof rootRouteImport
170
275
  }
276
+ '/reports': {
277
+ id: '/reports'
278
+ path: '/reports'
279
+ fullPath: '/reports'
280
+ preLoaderRoute: typeof ReportsRouteImport
281
+ parentRoute: typeof rootRouteImport
282
+ }
283
+ '/patterns': {
284
+ id: '/patterns'
285
+ path: '/patterns'
286
+ fullPath: '/patterns'
287
+ preLoaderRoute: typeof PatternsRouteImport
288
+ parentRoute: typeof rootRouteImport
289
+ }
171
290
  '/milestones': {
172
291
  id: '/milestones'
173
292
  path: '/milestones'
@@ -175,6 +294,13 @@ declare module '@tanstack/react-router' {
175
294
  preLoaderRoute: typeof MilestonesRouteImport
176
295
  parentRoute: typeof rootRouteImport
177
296
  }
297
+ '/designs': {
298
+ id: '/designs'
299
+ path: '/designs'
300
+ fullPath: '/designs'
301
+ preLoaderRoute: typeof DesignsRouteImport
302
+ parentRoute: typeof rootRouteImport
303
+ }
178
304
  '/activity': {
179
305
  id: '/activity'
180
306
  path: '/activity'
@@ -196,6 +322,20 @@ declare module '@tanstack/react-router' {
196
322
  preLoaderRoute: typeof TasksIndexRouteImport
197
323
  parentRoute: typeof TasksRoute
198
324
  }
325
+ '/reports/': {
326
+ id: '/reports/'
327
+ path: '/'
328
+ fullPath: '/reports/'
329
+ preLoaderRoute: typeof ReportsIndexRouteImport
330
+ parentRoute: typeof ReportsRoute
331
+ }
332
+ '/patterns/': {
333
+ id: '/patterns/'
334
+ path: '/'
335
+ fullPath: '/patterns/'
336
+ preLoaderRoute: typeof PatternsIndexRouteImport
337
+ parentRoute: typeof PatternsRoute
338
+ }
199
339
  '/milestones/': {
200
340
  id: '/milestones/'
201
341
  path: '/'
@@ -203,6 +343,13 @@ declare module '@tanstack/react-router' {
203
343
  preLoaderRoute: typeof MilestonesIndexRouteImport
204
344
  parentRoute: typeof MilestonesRoute
205
345
  }
346
+ '/designs/': {
347
+ id: '/designs/'
348
+ path: '/'
349
+ fullPath: '/designs/'
350
+ preLoaderRoute: typeof DesignsIndexRouteImport
351
+ parentRoute: typeof DesignsRoute
352
+ }
206
353
  '/tasks/$taskId': {
207
354
  id: '/tasks/$taskId'
208
355
  path: '/$taskId'
@@ -210,6 +357,20 @@ declare module '@tanstack/react-router' {
210
357
  preLoaderRoute: typeof TasksTaskIdRouteImport
211
358
  parentRoute: typeof TasksRoute
212
359
  }
360
+ '/reports/$slug': {
361
+ id: '/reports/$slug'
362
+ path: '/$slug'
363
+ fullPath: '/reports/$slug'
364
+ preLoaderRoute: typeof ReportsSlugRouteImport
365
+ parentRoute: typeof ReportsRoute
366
+ }
367
+ '/patterns/$slug': {
368
+ id: '/patterns/$slug'
369
+ path: '/$slug'
370
+ fullPath: '/patterns/$slug'
371
+ preLoaderRoute: typeof PatternsSlugRouteImport
372
+ parentRoute: typeof PatternsRoute
373
+ }
213
374
  '/milestones/$milestoneId': {
214
375
  id: '/milestones/$milestoneId'
215
376
  path: '/$milestoneId'
@@ -217,6 +378,13 @@ declare module '@tanstack/react-router' {
217
378
  preLoaderRoute: typeof MilestonesMilestoneIdRouteImport
218
379
  parentRoute: typeof MilestonesRoute
219
380
  }
381
+ '/designs/$slug': {
382
+ id: '/designs/$slug'
383
+ path: '/$slug'
384
+ fullPath: '/designs/$slug'
385
+ preLoaderRoute: typeof DesignsSlugRouteImport
386
+ parentRoute: typeof DesignsRoute
387
+ }
220
388
  '/api/watch': {
221
389
  id: '/api/watch'
222
390
  path: '/api/watch'
@@ -227,6 +395,19 @@ declare module '@tanstack/react-router' {
227
395
  }
228
396
  }
229
397
 
398
+ interface DesignsRouteChildren {
399
+ DesignsSlugRoute: typeof DesignsSlugRoute
400
+ DesignsIndexRoute: typeof DesignsIndexRoute
401
+ }
402
+
403
+ const DesignsRouteChildren: DesignsRouteChildren = {
404
+ DesignsSlugRoute: DesignsSlugRoute,
405
+ DesignsIndexRoute: DesignsIndexRoute,
406
+ }
407
+
408
+ const DesignsRouteWithChildren =
409
+ DesignsRoute._addFileChildren(DesignsRouteChildren)
410
+
230
411
  interface MilestonesRouteChildren {
231
412
  MilestonesMilestoneIdRoute: typeof MilestonesMilestoneIdRoute
232
413
  MilestonesIndexRoute: typeof MilestonesIndexRoute
@@ -241,6 +422,33 @@ const MilestonesRouteWithChildren = MilestonesRoute._addFileChildren(
241
422
  MilestonesRouteChildren,
242
423
  )
243
424
 
425
+ interface PatternsRouteChildren {
426
+ PatternsSlugRoute: typeof PatternsSlugRoute
427
+ PatternsIndexRoute: typeof PatternsIndexRoute
428
+ }
429
+
430
+ const PatternsRouteChildren: PatternsRouteChildren = {
431
+ PatternsSlugRoute: PatternsSlugRoute,
432
+ PatternsIndexRoute: PatternsIndexRoute,
433
+ }
434
+
435
+ const PatternsRouteWithChildren = PatternsRoute._addFileChildren(
436
+ PatternsRouteChildren,
437
+ )
438
+
439
+ interface ReportsRouteChildren {
440
+ ReportsSlugRoute: typeof ReportsSlugRoute
441
+ ReportsIndexRoute: typeof ReportsIndexRoute
442
+ }
443
+
444
+ const ReportsRouteChildren: ReportsRouteChildren = {
445
+ ReportsSlugRoute: ReportsSlugRoute,
446
+ ReportsIndexRoute: ReportsIndexRoute,
447
+ }
448
+
449
+ const ReportsRouteWithChildren =
450
+ ReportsRoute._addFileChildren(ReportsRouteChildren)
451
+
244
452
  interface TasksRouteChildren {
245
453
  TasksTaskIdRoute: typeof TasksTaskIdRoute
246
454
  TasksIndexRoute: typeof TasksIndexRoute
@@ -256,7 +464,10 @@ const TasksRouteWithChildren = TasksRoute._addFileChildren(TasksRouteChildren)
256
464
  const rootRouteChildren: RootRouteChildren = {
257
465
  IndexRoute: IndexRoute,
258
466
  ActivityRoute: ActivityRoute,
467
+ DesignsRoute: DesignsRouteWithChildren,
259
468
  MilestonesRoute: MilestonesRouteWithChildren,
469
+ PatternsRoute: PatternsRouteWithChildren,
470
+ ReportsRoute: ReportsRouteWithChildren,
260
471
  SearchRoute: SearchRoute,
261
472
  TasksRoute: TasksRouteWithChildren,
262
473
  ApiWatchRoute: ApiWatchRoute,
@@ -0,0 +1,18 @@
1
+ import { createFileRoute } from '@tanstack/react-router'
2
+ import { DocumentDetail } from '../components/DocumentDetail'
3
+
4
+ export const Route = createFileRoute('/designs/$slug')({
5
+ component: DesignDetailPage,
6
+ })
7
+
8
+ function DesignDetailPage() {
9
+ const { slug } = Route.useParams()
10
+ return (
11
+ <DocumentDetail
12
+ slug={slug}
13
+ dirPath="agent/design"
14
+ sectionLabel="Designs"
15
+ sectionHref="/designs"
16
+ />
17
+ )
18
+ }
@@ -0,0 +1,10 @@
1
+ import { createFileRoute } from '@tanstack/react-router'
2
+ import { DocumentList } from '../components/DocumentList'
3
+
4
+ export const Route = createFileRoute('/designs/')({
5
+ component: DesignsPage,
6
+ })
7
+
8
+ function DesignsPage() {
9
+ return <DocumentList title="Designs" dirPath="agent/design" baseTo="/designs" />
10
+ }
@@ -0,0 +1,9 @@
1
+ import { createFileRoute, Outlet } from '@tanstack/react-router'
2
+
3
+ export const Route = createFileRoute('/designs')({
4
+ component: DesignsLayout,
5
+ })
6
+
7
+ function DesignsLayout() {
8
+ return <Outlet />
9
+ }
@@ -5,6 +5,7 @@ import { Breadcrumb } from '../components/Breadcrumb'
5
5
  import { DetailHeader } from '../components/DetailHeader'
6
6
  import { ProgressBar } from '../components/ProgressBar'
7
7
  import { StatusDot } from '../components/StatusDot'
8
+ import { PriorityBadge } from '../components/PriorityBadge'
8
9
  import { MarkdownContent, buildLinkMap } from '../components/MarkdownContent'
9
10
  import { getMarkdownContent, resolveMilestoneFile } from '../services/markdown.service'
10
11
  import type { MarkdownResult, ResolveFileResult } from '../services/markdown.service'
@@ -105,6 +106,10 @@ function MilestoneDetailPage() {
105
106
  <span className="text-xs text-gray-500">{milestone.progress}%</span>
106
107
  </div>
107
108
 
109
+ <div className="flex items-center gap-2 mb-4">
110
+ <PriorityBadge priority={milestone.priority} />
111
+ </div>
112
+
108
113
  <DetailHeader status={milestone.status} fields={fields} />
109
114
 
110
115
  {milestone.notes && (
@@ -140,6 +145,7 @@ function MilestoneDetailPage() {
140
145
  <span className={task.status === 'completed' ? 'text-gray-500' : 'text-gray-200'}>
141
146
  {task.name}
142
147
  </span>
148
+ <PriorityBadge priority={task.priority} />
143
149
  {task.estimated_hours && (
144
150
  <span className="text-xs text-gray-600 ml-auto">{task.estimated_hours}h</span>
145
151
  )}
@@ -0,0 +1,18 @@
1
+ import { createFileRoute } from '@tanstack/react-router'
2
+ import { DocumentDetail } from '../components/DocumentDetail'
3
+
4
+ export const Route = createFileRoute('/patterns/$slug')({
5
+ component: PatternDetailPage,
6
+ })
7
+
8
+ function PatternDetailPage() {
9
+ const { slug } = Route.useParams()
10
+ return (
11
+ <DocumentDetail
12
+ slug={slug}
13
+ dirPath="agent/patterns"
14
+ sectionLabel="Patterns"
15
+ sectionHref="/patterns"
16
+ />
17
+ )
18
+ }
@@ -0,0 +1,10 @@
1
+ import { createFileRoute } from '@tanstack/react-router'
2
+ import { DocumentList } from '../components/DocumentList'
3
+
4
+ export const Route = createFileRoute('/patterns/')({
5
+ component: PatternsPage,
6
+ })
7
+
8
+ function PatternsPage() {
9
+ return <DocumentList title="Patterns" dirPath="agent/patterns" baseTo="/patterns" />
10
+ }
@@ -0,0 +1,9 @@
1
+ import { createFileRoute, Outlet } from '@tanstack/react-router'
2
+
3
+ export const Route = createFileRoute('/patterns')({
4
+ component: PatternsLayout,
5
+ })
6
+
7
+ function PatternsLayout() {
8
+ return <Outlet />
9
+ }
@@ -0,0 +1,18 @@
1
+ import { createFileRoute } from '@tanstack/react-router'
2
+ import { DocumentDetail } from '../components/DocumentDetail'
3
+
4
+ export const Route = createFileRoute('/reports/$slug')({
5
+ component: ReportDetailPage,
6
+ })
7
+
8
+ function ReportDetailPage() {
9
+ const { slug } = Route.useParams()
10
+ return (
11
+ <DocumentDetail
12
+ slug={slug}
13
+ dirPath="agent/reports"
14
+ sectionLabel="Reports"
15
+ sectionHref="/reports"
16
+ />
17
+ )
18
+ }
@@ -0,0 +1,10 @@
1
+ import { createFileRoute } from '@tanstack/react-router'
2
+ import { DocumentList } from '../components/DocumentList'
3
+
4
+ export const Route = createFileRoute('/reports/')({
5
+ component: ReportsPage,
6
+ })
7
+
8
+ function ReportsPage() {
9
+ return <DocumentList title="Reports" dirPath="agent/reports" baseTo="/reports" />
10
+ }
@@ -0,0 +1,9 @@
1
+ import { createFileRoute, Outlet } from '@tanstack/react-router'
2
+
3
+ export const Route = createFileRoute('/reports')({
4
+ component: ReportsLayout,
5
+ })
6
+
7
+ function ReportsLayout() {
8
+ return <Outlet />
9
+ }
@@ -3,6 +3,7 @@ import { useState, useEffect, useMemo } from 'react'
3
3
  import { useProgressData } from '../contexts/ProgressContext'
4
4
  import { Breadcrumb } from '../components/Breadcrumb'
5
5
  import { DetailHeader } from '../components/DetailHeader'
6
+ import { PriorityBadge } from '../components/PriorityBadge'
6
7
  import { MarkdownContent, buildLinkMap } from '../components/MarkdownContent'
7
8
  import { getMarkdownContent } from '../services/markdown.service'
8
9
  import { resolveTaskFile } from '../services/markdown.service'
@@ -94,8 +95,13 @@ function TaskDetailPage() {
94
95
  )
95
96
  }
96
97
 
98
+ const hoursDisplay = task.actual_hours != null
99
+ ? `Est: ${task.estimated_hours}h | Actual: ${task.actual_hours}h`
100
+ : `${task.estimated_hours}h`
101
+
97
102
  const fields = [
98
- { label: 'Est', value: `${task.estimated_hours}h` },
103
+ { label: 'Est', value: hoursDisplay },
104
+ ...(task.started ? [{ label: 'Started', value: task.started }] : []),
99
105
  ...(task.completed_date ? [{ label: 'Completed', value: task.completed_date }] : []),
100
106
  {
101
107
  label: 'Milestone',
@@ -123,6 +129,10 @@ function TaskDetailPage() {
123
129
 
124
130
  <h1 className="text-xl font-semibold text-gray-100 mb-3">{task.name}</h1>
125
131
 
132
+ <div className="flex items-center gap-2 mb-4">
133
+ <PriorityBadge priority={task.priority} />
134
+ </div>
135
+
126
136
  <DetailHeader status={task.status} fields={fields} />
127
137
 
128
138
  {task.notes && (
@@ -115,6 +115,108 @@ async function fetchMarkdownFromGitHub(
115
115
  }
116
116
  }
117
117
 
118
+ // ---------- listAgentDirectory ----------
119
+
120
+ export type AgentFile = {
121
+ name: string
122
+ /** Relative path from project root, e.g. "agent/design/local.foo.md" */
123
+ relativePath: string
124
+ }
125
+
126
+ export type ListDirResult =
127
+ | { ok: true; files: AgentFile[] }
128
+ | { ok: false; files: []; error: string }
129
+
130
+ export const listAgentDirectory = createServerFn({ method: 'GET' })
131
+ .inputValidator((input: { dirPath: string; github?: { owner: string; repo: string; branch?: string; token?: string } }) => input)
132
+ .handler(async ({ data: input }): Promise<ListDirResult> => {
133
+ if (input.github) {
134
+ return listDirFromGitHub(input.dirPath, input.github)
135
+ }
136
+ return listDirFromDisk(input.dirPath)
137
+ })
138
+
139
+ async function listDirFromDisk(dirPath: string): Promise<ListDirResult> {
140
+ try {
141
+ const fs = await import('fs')
142
+ const path = await import('path')
143
+
144
+ const basePath = getBasePath()
145
+ const fullDir = path.resolve(basePath, dirPath)
146
+
147
+ if (!fs.existsSync(fullDir)) {
148
+ return { ok: false, files: [], error: `Directory not found: ${dirPath}` }
149
+ }
150
+
151
+ const entries = fs.readdirSync(fullDir)
152
+ const files: AgentFile[] = entries
153
+ .filter((f: string) => f.endsWith('.md') && !f.includes('template'))
154
+ .sort()
155
+ .map((f: string) => ({
156
+ name: f.replace(/\.md$/, ''),
157
+ relativePath: `${dirPath}/${f}`,
158
+ }))
159
+
160
+ return { ok: true, files }
161
+ } catch (err: any) {
162
+ return { ok: false, files: [], error: `Failed to list directory: ${dirPath}` }
163
+ }
164
+ }
165
+
166
+ async function listDirFromGitHub(
167
+ dirPath: string,
168
+ github: { owner: string; repo: string; branch?: string; token?: string },
169
+ ): Promise<ListDirResult> {
170
+ try {
171
+ let branch = github.branch
172
+ if (!branch) {
173
+ try {
174
+ const metaRes = await fetch(`https://api.github.com/repos/${github.owner}/${github.repo}`, {
175
+ headers: github.token ? { Authorization: `token ${github.token}` } : {},
176
+ })
177
+ if (metaRes.ok) {
178
+ const meta = (await metaRes.json()) as { default_branch?: string }
179
+ branch = meta.default_branch || 'main'
180
+ } else {
181
+ branch = 'main'
182
+ }
183
+ } catch {
184
+ branch = 'main'
185
+ }
186
+ }
187
+
188
+ const url = `https://api.github.com/repos/${github.owner}/${github.repo}/contents/${dirPath}?ref=${branch}`
189
+ const headers: Record<string, string> = {
190
+ Accept: 'application/vnd.github.v3+json',
191
+ }
192
+ if (github.token) {
193
+ headers['Authorization'] = `token ${github.token}`
194
+ }
195
+
196
+ const response = await fetch(url, { headers })
197
+
198
+ if (!response.ok) {
199
+ if (response.status === 404) {
200
+ return { ok: true, files: [] }
201
+ }
202
+ return { ok: false, files: [], error: `GitHub returned ${response.status}` }
203
+ }
204
+
205
+ const entries = (await response.json()) as Array<{ name: string; path: string; type: string }>
206
+ const files: AgentFile[] = entries
207
+ .filter((e) => e.name.endsWith('.md') && !e.name.includes('template'))
208
+ .sort((a, b) => a.name.localeCompare(b.name))
209
+ .map((e) => ({
210
+ name: e.name.replace(/\.md$/, ''),
211
+ relativePath: e.path,
212
+ }))
213
+
214
+ return { ok: true, files }
215
+ } catch (err) {
216
+ return { ok: false, files: [], error: `Failed to list directory from GitHub` }
217
+ }
218
+ }
219
+
118
220
  // ---------- resolveMilestoneFile ----------
119
221
 
120
222
  export const resolveMilestoneFile = createServerFn({ method: 'GET' })