popilot 0.6.0 ā 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.mjs +204 -2
- package/lib/doctor.mjs +38 -1
- package/lib/hydrate.mjs +15 -0
- package/lib/scaffold.mjs +5 -0
- package/lib/setup-wizard.mjs +35 -2
- package/package.json +1 -1
- package/scaffold/.context/project.yaml.example +19 -0
- package/scaffold/mcp-pm/package.json +19 -0
- package/scaffold/mcp-pm/src/api-client.ts +69 -0
- package/scaffold/mcp-pm/src/index.ts +660 -0
- package/scaffold/mcp-pm/tsconfig.json +14 -0
- package/scaffold/pm-api/package.json +21 -0
- package/scaffold/pm-api/sql/schema-core.sql +331 -0
- package/scaffold/pm-api/sql/schema-docs.sql +25 -0
- package/scaffold/pm-api/sql/schema-meetings.sql +17 -0
- package/scaffold/pm-api/sql/schema-rewards.sql +16 -0
- package/scaffold/pm-api/src/auth.ts +28 -0
- package/scaffold/pm-api/src/blockchain/adapter.ts +20 -0
- package/scaffold/pm-api/src/blockchain/tron.ts +62 -0
- package/scaffold/pm-api/src/db/adapter.ts +36 -0
- package/scaffold/pm-api/src/db/turso.ts +147 -0
- package/scaffold/pm-api/src/index.ts +114 -0
- package/scaffold/pm-api/src/mcp-tools/dashboard.ts +40 -0
- package/scaffold/pm-api/src/mcp-tools/epic.ts +67 -0
- package/scaffold/pm-api/src/mcp-tools/event.ts +89 -0
- package/scaffold/pm-api/src/mcp-tools/index.ts +11 -0
- package/scaffold/pm-api/src/mcp-tools/initiative.ts +51 -0
- package/scaffold/pm-api/src/mcp-tools/memo.ts +164 -0
- package/scaffold/pm-api/src/mcp-tools/notification.ts +37 -0
- package/scaffold/pm-api/src/mcp-tools/retro.ts +183 -0
- package/scaffold/pm-api/src/mcp-tools/sprint.ts +204 -0
- package/scaffold/pm-api/src/mcp-tools/standup.ts +136 -0
- package/scaffold/pm-api/src/mcp-tools/story.ts +230 -0
- package/scaffold/pm-api/src/mcp-tools/task.ts +187 -0
- package/scaffold/pm-api/src/mcp-tools/utils.ts +83 -0
- package/scaffold/pm-api/src/mcp.ts +871 -0
- package/scaffold/pm-api/src/nudge.ts +283 -0
- package/scaffold/pm-api/src/routes/auth.ts +32 -0
- package/scaffold/pm-api/src/routes/v2-activity.ts +27 -0
- package/scaffold/pm-api/src/routes/v2-admin.ts +165 -0
- package/scaffold/pm-api/src/routes/v2-dashboard.ts +189 -0
- package/scaffold/pm-api/src/routes/v2-docs.ts +34 -0
- package/scaffold/pm-api/src/routes/v2-initiatives.ts +118 -0
- package/scaffold/pm-api/src/routes/v2-kickoff.ts +265 -0
- package/scaffold/pm-api/src/routes/v2-meetings.ts +324 -0
- package/scaffold/pm-api/src/routes/v2-memos.ts +257 -0
- package/scaffold/pm-api/src/routes/v2-nav.ts +260 -0
- package/scaffold/pm-api/src/routes/v2-notifications.ts +79 -0
- package/scaffold/pm-api/src/routes/v2-page-content.ts +35 -0
- package/scaffold/pm-api/src/routes/v2-pm.ts +380 -0
- package/scaffold/pm-api/src/routes/v2-policy.ts +58 -0
- package/scaffold/pm-api/src/routes/v2-retro.ts +221 -0
- package/scaffold/pm-api/src/routes/v2-rewards.ts +132 -0
- package/scaffold/pm-api/src/routes/v2-scenarios.ts +48 -0
- package/scaffold/pm-api/src/routes/v2-search.ts +32 -0
- package/scaffold/pm-api/src/routes/v2-standup.ts +127 -0
- package/scaffold/pm-api/src/routes/v2-user.ts +38 -0
- package/scaffold/pm-api/src/types.ts +11 -0
- package/scaffold/pm-api/src/utils/activity.ts +22 -0
- package/scaffold/pm-api/src/utils/admin.ts +9 -0
- package/scaffold/pm-api/src/utils/agent-notify.ts +62 -0
- package/scaffold/pm-api/src/utils/assignee.ts +69 -0
- package/scaffold/pm-api/src/utils/db.ts +45 -0
- package/scaffold/pm-api/src/utils/initiative.ts +23 -0
- package/scaffold/pm-api/src/utils/sprint-lifecycle.ts +96 -0
- package/scaffold/pm-api/tsconfig.json +15 -0
- package/scaffold/pm-api/wrangler.toml.hbs +11 -0
- package/scaffold/spec-site/package-lock.json +40 -0
- package/scaffold/spec-site/package.json +4 -1
- package/scaffold/spec-site/src/api/types.ts +6 -0
- package/scaffold/spec-site/src/components/AppHeader.vue +429 -55
- package/scaffold/spec-site/src/components/MemberSelect.vue +48 -0
- package/scaffold/spec-site/src/components/NotificationDropdown.vue +116 -0
- package/scaffold/spec-site/src/components/SearchModal.vue +102 -0
- package/scaffold/spec-site/src/components/VelocityChart.vue +77 -0
- package/scaffold/spec-site/src/composables/pmTypes.ts +15 -2
- package/scaffold/spec-site/src/composables/useDashboard.ts +221 -0
- package/scaffold/spec-site/src/composables/useMediaQuery.ts +28 -0
- package/scaffold/spec-site/src/composables/useNotification.ts +200 -0
- package/scaffold/spec-site/src/composables/usePmStore.ts +48 -1
- package/scaffold/spec-site/src/composables/useRetro.ts +6 -0
- package/scaffold/spec-site/src/composables/useStandup.ts +201 -0
- package/scaffold/spec-site/src/composables/useTheme.ts +37 -0
- package/scaffold/spec-site/src/composables/useUser.ts +19 -1
- package/scaffold/spec-site/src/features.ts +108 -0
- package/scaffold/spec-site/src/pages/AdminPage.vue +299 -0
- package/scaffold/spec-site/src/pages/DashboardPage.vue +650 -0
- package/scaffold/spec-site/src/pages/DocsHub.vue +157 -0
- package/scaffold/spec-site/src/pages/InboxPage.vue +156 -0
- package/scaffold/spec-site/src/pages/MeetingsPage.vue +294 -0
- package/scaffold/spec-site/src/pages/MyPage.vue +343 -0
- package/scaffold/spec-site/src/pages/RewardsPage.vue +266 -0
- package/scaffold/spec-site/src/pages/board/BoardAdmin.vue +422 -0
- package/scaffold/spec-site/src/pages/board/BoardEpicSection.vue +54 -0
- package/scaffold/spec-site/src/pages/board/BoardPage.vue +884 -0
- package/scaffold/spec-site/src/pages/board/BoardStoryCard.vue +67 -0
- package/scaffold/spec-site/src/pages/board/BoardTaskItem.vue +52 -0
- package/scaffold/spec-site/src/pages/board/MyTasksPage.vue +202 -0
- package/scaffold/spec-site/src/pages/board/SprintClose.vue +167 -0
- package/scaffold/spec-site/src/pages/board/SprintColumn.vue +49 -0
- package/scaffold/spec-site/src/pages/board/SprintKickoff.vue +389 -0
- package/scaffold/spec-site/src/pages/board/StatusBadge.vue +52 -0
- package/scaffold/spec-site/src/pages/board/StoryDetailPanel.vue +495 -0
- package/scaffold/spec-site/src/pages/board/TaskCard.vue +42 -0
- package/scaffold/spec-site/src/pages/retro/RetroCard.vue +36 -2
- package/scaffold/spec-site/src/pages/retro/RetroHeader.vue +82 -66
- package/scaffold/spec-site/src/pages/retro/RetroPage.vue +47 -18
- package/scaffold/spec-site/src/pages/standup/StandupEntryCard.vue +551 -0
- package/scaffold/spec-site/src/pages/standup/StandupForm.vue +68 -0
- package/scaffold/spec-site/src/pages/standup/StandupList.vue +71 -0
- package/scaffold/spec-site/src/pages/standup/StandupPage.vue +225 -0
- package/scaffold/spec-site/src/router.ts +141 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { query, execute } from '../db/adapter.js'
|
|
2
|
+
import { text, err, today, resolveSprint, notify, checkRateLimit, emitAgentEvent, validateAssignee, resolveMemberId, type ToolResult } from './utils.js'
|
|
3
|
+
|
|
4
|
+
export async function toolGetRetroSession(user: string, args: Record<string, unknown>): Promise<ToolResult> {
|
|
5
|
+
const sprint = await resolveSprint(args.sprint as string | undefined)
|
|
6
|
+
if (!sprint) return err('Please specify a sprint.')
|
|
7
|
+
|
|
8
|
+
const sessionResult = await query<{ id: number; sprint: string; title: string; phase: string; created_at: string }>(
|
|
9
|
+
'SELECT * FROM retro_sessions WHERE sprint = ? LIMIT 1', [sprint],
|
|
10
|
+
)
|
|
11
|
+
if (sessionResult.error) return err(sessionResult.error)
|
|
12
|
+
if (sessionResult.rows.length === 0) return text(`${sprint.toUpperCase()} retro session not found.`)
|
|
13
|
+
|
|
14
|
+
const session = sessionResult.rows[0]
|
|
15
|
+
const [itemsResult, actionsResult] = await Promise.all([
|
|
16
|
+
query<{ id: number; category: string; content: string; author: string; vote_count: number; has_voted: number }>(
|
|
17
|
+
`SELECT i.*, COUNT(v.item_id) as vote_count,
|
|
18
|
+
CASE WHEN SUM(CASE WHEN v.voter = ? THEN 1 ELSE 0 END) > 0 THEN 1 ELSE 0 END as has_voted
|
|
19
|
+
FROM retro_items i LEFT JOIN retro_votes v ON v.item_id = i.id
|
|
20
|
+
WHERE i.session_id = ? GROUP BY i.id ORDER BY vote_count DESC`,
|
|
21
|
+
[user, session.id],
|
|
22
|
+
),
|
|
23
|
+
query<{ id: number; content: string; assignee: string | null; status: string }>(
|
|
24
|
+
'SELECT * FROM retro_actions WHERE session_id = ? ORDER BY created_at ASC',
|
|
25
|
+
[session.id],
|
|
26
|
+
),
|
|
27
|
+
])
|
|
28
|
+
|
|
29
|
+
const catIcon: Record<string, string> = { keep: 'š', problem: 'š“', try: 'š”' }
|
|
30
|
+
const lines = [
|
|
31
|
+
`š Retrospective: ${session.title} (${sprint.toUpperCase()})`,
|
|
32
|
+
`Phase: ${session.phase} | Created: ${session.created_at}`,
|
|
33
|
+
'āāāāāāāāāāāāā',
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
if (!itemsResult.error && itemsResult.rows.length > 0) {
|
|
37
|
+
for (const cat of ['keep', 'problem', 'try']) {
|
|
38
|
+
const items = itemsResult.rows.filter(i => i.category === cat)
|
|
39
|
+
if (items.length > 0) {
|
|
40
|
+
lines.push(`\n${catIcon[cat] ?? 'ā'} ${cat.toUpperCase()} (${items.length} items)`)
|
|
41
|
+
for (const i of items) {
|
|
42
|
+
const voted = i.has_voted ? 'ā
' : 'ā'
|
|
43
|
+
lines.push(` ${voted} [R${i.id}] ${i.content} (${i.author}, ${i.vote_count} votes)`)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
} else {
|
|
48
|
+
lines.push('\nNo items found.')
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!actionsResult.error && actionsResult.rows.length > 0) {
|
|
52
|
+
const si: Record<string, string> = { todo: 'ā¬', 'in-progress': 'šµ', done: 'ā
' }
|
|
53
|
+
lines.push('\nš Action Items:')
|
|
54
|
+
for (const a of actionsResult.rows) {
|
|
55
|
+
lines.push(` ${si[a.status] ?? 'ā¬'} [A${a.id}] ${a.content} (${a.assignee ?? 'Unassigned'})`)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return text(lines.join('\n'))
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function toolAddRetroItem(user: string, args: Record<string, unknown>): Promise<ToolResult> {
|
|
63
|
+
const sessionId = args.session_id as number
|
|
64
|
+
const category = args.category as string
|
|
65
|
+
const content = args.content as string
|
|
66
|
+
|
|
67
|
+
const result = await execute(
|
|
68
|
+
'INSERT INTO retro_items (session_id, category, content, author) VALUES (?, ?, ?, ?)',
|
|
69
|
+
[sessionId, category, content, user],
|
|
70
|
+
)
|
|
71
|
+
if (result.error) return err(result.error)
|
|
72
|
+
return text(`ā
Retro item added (${category}): ${content}`)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function toolVoteRetroItem(user: string, args: Record<string, unknown>): Promise<ToolResult> {
|
|
76
|
+
const itemId = args.item_id as number
|
|
77
|
+
|
|
78
|
+
// Check if already voted
|
|
79
|
+
const existing = await query<{ item_id: number }>(
|
|
80
|
+
'SELECT item_id FROM retro_votes WHERE item_id = ? AND voter = ?', [itemId, user],
|
|
81
|
+
)
|
|
82
|
+
if (existing.rows.length > 0) {
|
|
83
|
+
await execute('DELETE FROM retro_votes WHERE item_id = ? AND voter = ?', [itemId, user])
|
|
84
|
+
return text(`ā
Vote removed: item #${itemId}`)
|
|
85
|
+
} else {
|
|
86
|
+
await execute('INSERT OR IGNORE INTO retro_votes (item_id, voter) VALUES (?, ?)', [itemId, user])
|
|
87
|
+
return text(`ā
Voted: item #${itemId}`)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function toolChangeRetroPhase(args: Record<string, unknown>): Promise<ToolResult> {
|
|
92
|
+
const sessionId = args.session_id as number
|
|
93
|
+
const phase = args.phase as string
|
|
94
|
+
const result = await execute(
|
|
95
|
+
'UPDATE retro_sessions SET phase = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?',
|
|
96
|
+
[phase, sessionId],
|
|
97
|
+
)
|
|
98
|
+
if (result.error) return err(result.error)
|
|
99
|
+
if (result.rowsAffected === 0) return err(`Session #${sessionId} not found.`)
|
|
100
|
+
return text(`ā
Retro session #${sessionId} ā ${phase} phase`)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export async function toolAddRetroAction(user: string, args: Record<string, unknown>): Promise<ToolResult> {
|
|
104
|
+
const sessionId = args.session_id as number
|
|
105
|
+
const content = args.content as string
|
|
106
|
+
const assignee = (args.assignee as string) ?? null
|
|
107
|
+
|
|
108
|
+
const result = await execute(
|
|
109
|
+
'INSERT INTO retro_actions (session_id, content, assignee) VALUES (?, ?, ?)',
|
|
110
|
+
[sessionId, content, assignee],
|
|
111
|
+
)
|
|
112
|
+
if (result.error) return err(result.error)
|
|
113
|
+
|
|
114
|
+
// Notify assignee
|
|
115
|
+
if (assignee) {
|
|
116
|
+
const idResult = await query<{ id: number }>('SELECT last_insert_rowid() as id')
|
|
117
|
+
const actionId = idResult.rows[0]?.id ?? 0
|
|
118
|
+
await notify(assignee, 'retro_action', `Retro action assigned: ${content.slice(0, 30)}`, `${user} assigned retro action: ${content}`, 'retro_action', actionId, 'retro', user)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return text(`ā
Action added: ${content}${assignee ? ` (assignee: ${assignee})` : ''}`)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export async function toolUpdateRetroActionStatus(args: Record<string, unknown>): Promise<ToolResult> {
|
|
125
|
+
const actionId = args.action_id as number
|
|
126
|
+
const status = args.status as string
|
|
127
|
+
const result = await execute(
|
|
128
|
+
'UPDATE retro_actions SET status = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?',
|
|
129
|
+
[status, actionId],
|
|
130
|
+
)
|
|
131
|
+
if (result.error) return err(result.error)
|
|
132
|
+
if (result.rowsAffected === 0) return err(`Action #${actionId} not found.`)
|
|
133
|
+
return text(`ā
Action #${actionId} ā ${status}`)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export async function toolExportRetro(args: Record<string, unknown>): Promise<ToolResult> {
|
|
137
|
+
const sprint = await resolveSprint(args.sprint as string | undefined)
|
|
138
|
+
if (!sprint) return err('Please specify a sprint.')
|
|
139
|
+
|
|
140
|
+
const sessionResult = await query<{ id: number; title: string; phase: string }>(
|
|
141
|
+
'SELECT id, title, phase FROM retro_sessions WHERE sprint = ? LIMIT 1', [sprint],
|
|
142
|
+
)
|
|
143
|
+
if (sessionResult.error) return err(sessionResult.error)
|
|
144
|
+
if (sessionResult.rows.length === 0) return err(`${sprint.toUpperCase()} retro session not found.`)
|
|
145
|
+
|
|
146
|
+
const session = sessionResult.rows[0]
|
|
147
|
+
const [itemsResult, actionsResult] = await Promise.all([
|
|
148
|
+
query<{ category: string; content: string; author: string; vote_count: number }>(
|
|
149
|
+
`SELECT i.category, i.content, i.author, COUNT(v.item_id) as vote_count
|
|
150
|
+
FROM retro_items i LEFT JOIN retro_votes v ON v.item_id = i.id
|
|
151
|
+
WHERE i.session_id = ? GROUP BY i.id ORDER BY i.category, vote_count DESC`,
|
|
152
|
+
[session.id],
|
|
153
|
+
),
|
|
154
|
+
query<{ content: string; assignee: string | null; status: string }>(
|
|
155
|
+
'SELECT content, assignee, status FROM retro_actions WHERE session_id = ? ORDER BY created_at',
|
|
156
|
+
[session.id],
|
|
157
|
+
),
|
|
158
|
+
])
|
|
159
|
+
|
|
160
|
+
const lines = [
|
|
161
|
+
`# ${session.title} (${sprint.toUpperCase()})`,
|
|
162
|
+
'',
|
|
163
|
+
]
|
|
164
|
+
|
|
165
|
+
if (!itemsResult.error) {
|
|
166
|
+
for (const cat of ['keep', 'problem', 'try']) {
|
|
167
|
+
const items = itemsResult.rows.filter(i => i.category === cat)
|
|
168
|
+
lines.push(`## ${cat.toUpperCase()} (${items.length} items)`)
|
|
169
|
+
for (const i of items) lines.push(`- ${i.content} ā ${i.author} (${i.vote_count} votes)`)
|
|
170
|
+
lines.push('')
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (!actionsResult.error && actionsResult.rows.length > 0) {
|
|
175
|
+
lines.push('## Action Items')
|
|
176
|
+
for (const a of actionsResult.rows) {
|
|
177
|
+
const check = a.status === 'done' ? '[x]' : '[ ]'
|
|
178
|
+
lines.push(`- ${check} ${a.content}${a.assignee ? ` @${a.assignee}` : ''}`)
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return text(lines.join('\n'))
|
|
183
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { query, execute } from '../db/adapter.js'
|
|
2
|
+
import { text, err, today, resolveSprint, notify, checkRateLimit, emitAgentEvent, validateAssignee, resolveMemberId, progressBar, type ToolResult } from './utils.js'
|
|
3
|
+
|
|
4
|
+
export async function toolListSprints(): Promise<ToolResult> {
|
|
5
|
+
const result = await query<{ id: string; title: string; active: number; start_date: string | null; end_date: string | null }>(
|
|
6
|
+
'SELECT id, title, active, start_date, end_date FROM nav_sprints ORDER BY id DESC',
|
|
7
|
+
)
|
|
8
|
+
if (result.error) return err(result.error)
|
|
9
|
+
if (result.rows.length === 0) return text('No sprints found.')
|
|
10
|
+
|
|
11
|
+
const lines = ['š
Sprint List', 'āāāāāāāāāāāāā']
|
|
12
|
+
for (const s of result.rows) {
|
|
13
|
+
const marker = s.active ? ' ā active' : ''
|
|
14
|
+
const icon = s.active ? 'š¢' : 'āŖ'
|
|
15
|
+
const dates = s.start_date && s.end_date ? ` (${s.start_date} ~ ${s.end_date})` : ''
|
|
16
|
+
lines.push(`${icon} ${s.id}: ${s.title}${dates}${marker}`)
|
|
17
|
+
}
|
|
18
|
+
return text(lines.join('\n'))
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function toolActivateSprint(args: Record<string, unknown>): Promise<ToolResult> {
|
|
22
|
+
const sprintId = args.sprint_id as string
|
|
23
|
+
const r1 = await execute('UPDATE nav_sprints SET active = 0 WHERE active = 1', [])
|
|
24
|
+
if (r1.error) return err(r1.error)
|
|
25
|
+
const r2 = await execute('UPDATE nav_sprints SET active = 1 WHERE id = ?', [sprintId])
|
|
26
|
+
if (r2.error) return err(r2.error)
|
|
27
|
+
if (r2.rowsAffected === 0) return err(`Sprint '${sprintId}' not found.`)
|
|
28
|
+
return text(`ā
Sprint ${sprintId} activated.`)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function toolCloseSprint(args: Record<string, unknown>): Promise<ToolResult> {
|
|
32
|
+
const sprintId = args.sprint_id as string
|
|
33
|
+
|
|
34
|
+
// Check status
|
|
35
|
+
const sprint = await query<{ id: string; status: string }>('SELECT id, status FROM nav_sprints WHERE id = ?', [sprintId])
|
|
36
|
+
if (sprint.error) return err(sprint.error)
|
|
37
|
+
if (!sprint.rows.length) return err(`Sprint '${sprintId}' not found`)
|
|
38
|
+
if (sprint.rows[0].status !== 'active') return err(`Can only close sprints in active state (current: ${sprint.rows[0].status})`)
|
|
39
|
+
|
|
40
|
+
// Fetch stories
|
|
41
|
+
const stories = await query<{ id: number; title: string; sprint: string | null; status: string; story_points: number | null; assignee: string | null }>(
|
|
42
|
+
'SELECT id, title, sprint, status, story_points, assignee FROM pm_stories WHERE sprint = ?', [sprintId])
|
|
43
|
+
if (stories.error) return err(stories.error)
|
|
44
|
+
|
|
45
|
+
const { getIncompleteStories, getCompletedStories, generateCloseSummary } = await import('../utils/sprint-lifecycle.js')
|
|
46
|
+
const completed = getCompletedStories(stories.rows)
|
|
47
|
+
const incomplete = getIncompleteStories(stories.rows)
|
|
48
|
+
const summary = generateCloseSummary(sprintId, stories.rows)
|
|
49
|
+
|
|
50
|
+
// Incomplete ā backlog
|
|
51
|
+
if (incomplete.length > 0) {
|
|
52
|
+
const ids = incomplete.map((s: { id: number }) => s.id)
|
|
53
|
+
const ph = ids.map(() => '?').join(', ')
|
|
54
|
+
await execute(`UPDATE pm_stories SET sprint = NULL, updated_at = CURRENT_TIMESTAMP WHERE id IN (${ph})`, ids)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Close
|
|
58
|
+
await execute(`UPDATE nav_sprints SET status = 'closed', active = 0, velocity = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [summary.doneSP, sprintId])
|
|
59
|
+
|
|
60
|
+
// Create retro session
|
|
61
|
+
try { await execute(`INSERT OR IGNORE INTO retro_sessions (sprint, phase) VALUES (?, 'collect')`, [sprintId]) } catch (_) {}
|
|
62
|
+
|
|
63
|
+
const lines = [
|
|
64
|
+
`š ${sprintId} Sprint closed!`,
|
|
65
|
+
'āāāāāāāāāāāāā',
|
|
66
|
+
`Completed: ${completed.length} (${summary.doneSP} SP)`,
|
|
67
|
+
`Incomplete: ${incomplete.length} ā Returned to backlog`,
|
|
68
|
+
`Completion rate: ${summary.completionRate}%`,
|
|
69
|
+
]
|
|
70
|
+
if (incomplete.length > 0) {
|
|
71
|
+
lines.push('', 'Stories returned to backlog:')
|
|
72
|
+
for (const s of incomplete) lines.push(` ⢠S${s.id}: ${s.title}`)
|
|
73
|
+
}
|
|
74
|
+
return text(lines.join('\n'))
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function toolCheckinSprint(args: Record<string, unknown>): Promise<ToolResult> {
|
|
78
|
+
const sprintId = args.sprint_id as string
|
|
79
|
+
const memberIds = args.member_ids as number[]
|
|
80
|
+
if (!sprintId || !memberIds?.length) return err('sprint_id, member_ids required')
|
|
81
|
+
await execute('DELETE FROM sprint_members WHERE sprint_id = ?', [sprintId])
|
|
82
|
+
for (const mid of memberIds) {
|
|
83
|
+
await execute('INSERT OR REPLACE INTO sprint_members (sprint_id, member_id, working_days) VALUES (?, ?, 0)', [sprintId, mid])
|
|
84
|
+
}
|
|
85
|
+
await execute('UPDATE nav_sprints SET team_size = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?', [memberIds.length, sprintId])
|
|
86
|
+
return text(`ā
${sprintId} check-in: ${memberIds.length} members registered`)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function toolAddAbsence(args: Record<string, unknown>): Promise<ToolResult> {
|
|
90
|
+
const sprintId = args.sprint_id as string
|
|
91
|
+
const memberId = args.member_id as number
|
|
92
|
+
const dates = args.dates as string[]
|
|
93
|
+
const reason = (args.reason as string) ?? null
|
|
94
|
+
if (!sprintId || !memberId || !dates?.length) return err('sprint_id, member_id, dates required')
|
|
95
|
+
for (const d of dates) {
|
|
96
|
+
await execute('INSERT OR IGNORE INTO member_absences (sprint_id, member_id, absence_date, reason) VALUES (?, ?, ?, ?)', [sprintId, memberId, d, reason])
|
|
97
|
+
}
|
|
98
|
+
return text(`ā
${sprintId} absence registered: ${dates.length} day(s)`)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export async function toolGetVelocity(): Promise<ToolResult> {
|
|
102
|
+
const result = await query<{ sprint: string; done_sp: number; total_sp: number; story_count: number }>(
|
|
103
|
+
`SELECT s.sprint,
|
|
104
|
+
SUM(CASE WHEN s.status = 'done' THEN COALESCE(s.story_points, 0) ELSE 0 END) as done_sp,
|
|
105
|
+
SUM(COALESCE(s.story_points, 0)) as total_sp,
|
|
106
|
+
COUNT(*) as story_count
|
|
107
|
+
FROM pm_stories s
|
|
108
|
+
JOIN nav_sprints ns ON s.sprint = ns.id
|
|
109
|
+
WHERE ns.status = 'closed' AND s.sprint IS NOT NULL
|
|
110
|
+
GROUP BY s.sprint
|
|
111
|
+
ORDER BY ns.sort_order`)
|
|
112
|
+
if (result.error) return err(result.error)
|
|
113
|
+
|
|
114
|
+
const sprints = result.rows
|
|
115
|
+
const doneSPs = sprints.map(s => s.done_sp)
|
|
116
|
+
const avgVelocity = doneSPs.length ? Math.round(doneSPs.reduce((a, b) => a + b, 0) / doneSPs.length) : 0
|
|
117
|
+
const lastThree = doneSPs.slice(-3)
|
|
118
|
+
const recentAvg = lastThree.length ? Math.round(lastThree.reduce((a, b) => a + b, 0) / lastThree.length) : 0
|
|
119
|
+
|
|
120
|
+
const lines = [
|
|
121
|
+
'š Velocity Report',
|
|
122
|
+
'āāāāāāāāāāāāā',
|
|
123
|
+
`Overall average: ${avgVelocity} SP (${sprints.length} sprints)`,
|
|
124
|
+
`Recent 3 average: ${recentAvg} SP`,
|
|
125
|
+
'',
|
|
126
|
+
'Sprint results:',
|
|
127
|
+
]
|
|
128
|
+
for (const s of sprints) {
|
|
129
|
+
lines.push(` ${s.sprint}: ${s.done_sp}/${s.total_sp} SP (${s.story_count} stories)`)
|
|
130
|
+
}
|
|
131
|
+
if (!sprints.length) lines.push(' (No completed sprints)')
|
|
132
|
+
return text(lines.join('\n'))
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export async function toolKickoffSprint(args: Record<string, unknown>): Promise<ToolResult> {
|
|
136
|
+
const sprintId = args.sprint_id as string
|
|
137
|
+
const storyIds = args.story_ids as number[]
|
|
138
|
+
const teamMembers = (args.team_members as string[]) ?? []
|
|
139
|
+
const velocity = (args.velocity as number) ?? null
|
|
140
|
+
|
|
141
|
+
if (!storyIds?.length) return err('story_ids required')
|
|
142
|
+
|
|
143
|
+
// Check sprint status
|
|
144
|
+
const sprint = await query<{ id: string; status: string }>('SELECT id, status FROM nav_sprints WHERE id = ?', [sprintId])
|
|
145
|
+
if (sprint.error) return err(sprint.error)
|
|
146
|
+
if (!sprint.rows.length) return err(`Sprint '${sprintId}' not found`)
|
|
147
|
+
if (sprint.rows[0].status !== 'planning') return err(`Kickoff is only available in planning state (current: ${sprint.rows[0].status})`)
|
|
148
|
+
|
|
149
|
+
// Total SP
|
|
150
|
+
const placeholders = storyIds.map(() => '?').join(', ')
|
|
151
|
+
const stories = await query<{ id: number; title: string; story_points: number | null }>(
|
|
152
|
+
`SELECT id, title, story_points FROM pm_stories WHERE id IN (${placeholders})`, storyIds)
|
|
153
|
+
if (stories.error) return err(stories.error)
|
|
154
|
+
const totalSP = stories.rows.reduce((sum, s) => sum + (s.story_points ?? 0), 0)
|
|
155
|
+
|
|
156
|
+
// Assign stories
|
|
157
|
+
const assignResult = await execute(
|
|
158
|
+
`UPDATE pm_stories SET sprint = ?, updated_at = CURRENT_TIMESTAMP WHERE id IN (${placeholders})`,
|
|
159
|
+
[sprintId, ...storyIds])
|
|
160
|
+
if (assignResult.error) return err(assignResult.error)
|
|
161
|
+
|
|
162
|
+
// Activate sprint
|
|
163
|
+
await execute('UPDATE nav_sprints SET active = 0, status = \'closed\', updated_at = CURRENT_TIMESTAMP WHERE active = 1 AND id != ?', [sprintId])
|
|
164
|
+
await execute('UPDATE nav_sprints SET status = \'active\', active = 1, velocity = ?, team_size = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?',
|
|
165
|
+
[velocity, teamMembers.length || null, sprintId])
|
|
166
|
+
|
|
167
|
+
const lines = [
|
|
168
|
+
`š ${sprintId} Kickoff complete!`,
|
|
169
|
+
`āāāāāāāāāāāāā`,
|
|
170
|
+
`Stories: ${storyIds.length} assigned`,
|
|
171
|
+
`SP total: ${totalSP}${velocity ? ` / velocity: ${velocity}` : ''}`,
|
|
172
|
+
]
|
|
173
|
+
if (velocity && totalSP > velocity) lines.push(`ā ļø SP exceeds velocity target!`)
|
|
174
|
+
if (teamMembers.length) lines.push(`Members: ${teamMembers.join(', ')}`)
|
|
175
|
+
return text(lines.join('\n'))
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export async function toolSprintSummary(args: Record<string, unknown>): Promise<ToolResult> {
|
|
179
|
+
const sprint = await resolveSprint(args.sprint as string | undefined)
|
|
180
|
+
if (!sprint) return err('Please specify a sprint.')
|
|
181
|
+
|
|
182
|
+
const [epicResult, assigneeResult, blockerResult] = await Promise.all([
|
|
183
|
+
query<{ epic_title: string; total: number; done: number }>(`SELECT COALESCE(e.title, '(No epic)') as epic_title, COUNT(t.id) as total, SUM(CASE WHEN t.status = 'done' THEN 1 ELSE 0 END) as done FROM pm_tasks t JOIN pm_stories s ON t.story_id = s.id LEFT JOIN pm_epics e ON s.epic_id = e.id WHERE s.sprint = ? GROUP BY e.title ORDER BY e.title`, [sprint]),
|
|
184
|
+
query<{ assignee: string; total: number; done: number; in_progress: number; todo: number }>(`SELECT COALESCE(t.assignee, 'Unassigned') as assignee, COUNT(t.id) as total, SUM(CASE WHEN t.status = 'done' THEN 1 ELSE 0 END) as done, SUM(CASE WHEN t.status = 'in-progress' THEN 1 ELSE 0 END) as in_progress, SUM(CASE WHEN t.status = 'todo' THEN 1 ELSE 0 END) as todo FROM pm_tasks t JOIN pm_stories s ON t.story_id = s.id WHERE s.sprint = ? GROUP BY t.assignee ORDER BY total DESC`, [sprint]),
|
|
185
|
+
query<{ user_name: string; blockers: string; entry_date: string }>(`SELECT user_name, blockers, entry_date FROM pm_standup_entries WHERE sprint = ? AND blockers IS NOT NULL AND blockers != '' ORDER BY entry_date DESC LIMIT 10`, [sprint]),
|
|
186
|
+
])
|
|
187
|
+
|
|
188
|
+
const lines = [`š ${sprint.toUpperCase()} Sprint Summary`, 'āāāāāāāāāāāāā']
|
|
189
|
+
if (!epicResult.error) for (const e of epicResult.rows) {
|
|
190
|
+
const pct = e.total > 0 ? Math.round((e.done / e.total) * 100) : 0
|
|
191
|
+
lines.push(`š· ${e.epic_title}: ${progressBar(e.done, e.total)} ${e.done}/${e.total} (${pct}%)`)
|
|
192
|
+
}
|
|
193
|
+
if (!assigneeResult.error && assigneeResult.rows.length > 0) {
|
|
194
|
+
lines.push('')
|
|
195
|
+
for (const a of assigneeResult.rows) lines.push(`š¤ ${a.assignee}: ${a.total} tasks (done ${a.done}, in-progress ${a.in_progress}, todo ${a.todo})`)
|
|
196
|
+
}
|
|
197
|
+
if (!blockerResult.error && blockerResult.rows.length > 0) {
|
|
198
|
+
lines.push('', `š§ Blockers: ${blockerResult.rows.length}`)
|
|
199
|
+
for (const b of blockerResult.rows) lines.push(` - [${b.entry_date}] ${b.user_name}: ${b.blockers}`)
|
|
200
|
+
} else {
|
|
201
|
+
lines.push('', 'š§ Blockers: 0')
|
|
202
|
+
}
|
|
203
|
+
return text(lines.join('\n'))
|
|
204
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { query, execute } from '../db/adapter.js'
|
|
2
|
+
import { text, err, today, resolveSprint, notify, checkRateLimit, emitAgentEvent, validateAssignee, resolveMemberId, type ToolResult } from './utils.js'
|
|
3
|
+
|
|
4
|
+
export async function toolGetStandup(user: string, args: Record<string, unknown>): Promise<ToolResult> {
|
|
5
|
+
const sprint = await resolveSprint(args.sprint as string | undefined)
|
|
6
|
+
if (!sprint) return err('Please specify a sprint.')
|
|
7
|
+
const d = (args.date as string) || today()
|
|
8
|
+
|
|
9
|
+
const result = await query<{ id: number; done_text: string | null; plan_text: string | null; plan_story_ids: string | null; blockers: string | null; created_at: string; updated_at: string }>('SELECT * FROM pm_standup_entries WHERE sprint = ? AND user_name = ? AND entry_date = ?', [sprint, user, d])
|
|
10
|
+
if (result.error) return err(result.error)
|
|
11
|
+
if (result.rows.length === 0) return text(`š No standup found for ${d}.`)
|
|
12
|
+
|
|
13
|
+
const e = result.rows[0]
|
|
14
|
+
const storyIds: number[] = e.plan_story_ids ? JSON.parse(e.plan_story_ids) : []
|
|
15
|
+
let storyTitles = ''
|
|
16
|
+
if (storyIds.length > 0) {
|
|
17
|
+
const ph = storyIds.map(() => '?').join(',')
|
|
18
|
+
const sr = await query<{ id: number; title: string }>(`SELECT id, title FROM pm_stories WHERE id IN (${ph})`, storyIds)
|
|
19
|
+
if (!sr.error) storyTitles = sr.rows.map(s => ` - [S${s.id}] ${s.title}`).join('\n')
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const lines = [`š ${user}'s Standup (${d})`, 'āāāāāāāāāāāāā', `ā
Done: ${e.done_text ?? '(none)'}`, `š Plan: ${e.plan_text ?? '(none)'}`]
|
|
23
|
+
if (storyTitles) lines.push(`š Planned Stories:\n${storyTitles}`)
|
|
24
|
+
if (e.blockers) lines.push(`š§ Blockers: ${e.blockers}`)
|
|
25
|
+
return text(lines.join('\n'))
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function toolSaveStandup(user: string, args: Record<string, unknown>): Promise<ToolResult> {
|
|
29
|
+
const sprint = await resolveSprint(args.sprint as string | undefined)
|
|
30
|
+
if (!sprint) return err('Please specify a sprint.')
|
|
31
|
+
const d = (args.date as string) || today()
|
|
32
|
+
const storyIdsJson = (args.plan_story_ids as number[] | undefined)?.length ? JSON.stringify(args.plan_story_ids) : null
|
|
33
|
+
|
|
34
|
+
const result = await execute(
|
|
35
|
+
`INSERT INTO pm_standup_entries (sprint, user_name, entry_date, done_text, plan_text, plan_story_ids, blockers, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) ON CONFLICT(sprint, user_name, entry_date) DO UPDATE SET done_text = excluded.done_text, plan_text = excluded.plan_text, plan_story_ids = excluded.plan_story_ids, blockers = excluded.blockers, updated_at = CURRENT_TIMESTAMP`,
|
|
36
|
+
[sprint, user, d, (args.done_text as string) ?? null, (args.plan_text as string) ?? null, storyIdsJson, (args.blockers as string) ?? null],
|
|
37
|
+
)
|
|
38
|
+
if (result.error) return err(result.error)
|
|
39
|
+
return text(`ā
${d} standup saved`)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function toolListStandupEntries(args: Record<string, unknown>): Promise<ToolResult> {
|
|
43
|
+
const sprint = await resolveSprint(args.sprint as string | undefined)
|
|
44
|
+
const date = args.date as string | undefined
|
|
45
|
+
|
|
46
|
+
let sql: string
|
|
47
|
+
const sqlArgs: (string | number)[] = []
|
|
48
|
+
|
|
49
|
+
if (sprint && date) {
|
|
50
|
+
sql = 'SELECT * FROM pm_standup_entries WHERE sprint = ? AND entry_date = ? ORDER BY user_name'
|
|
51
|
+
sqlArgs.push(sprint, date)
|
|
52
|
+
} else if (date) {
|
|
53
|
+
sql = 'SELECT * FROM pm_standup_entries WHERE entry_date = ? ORDER BY user_name'
|
|
54
|
+
sqlArgs.push(date)
|
|
55
|
+
} else if (sprint) {
|
|
56
|
+
sql = 'SELECT * FROM pm_standup_entries WHERE sprint = ? ORDER BY entry_date DESC, user_name LIMIT 50'
|
|
57
|
+
sqlArgs.push(sprint)
|
|
58
|
+
} else {
|
|
59
|
+
return err('Please specify a sprint or date.')
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const result = await query<{ user_name: string; entry_date: string; done_text: string | null; plan_text: string | null; blockers: string | null }>(sql, sqlArgs)
|
|
63
|
+
if (result.error) return err(result.error)
|
|
64
|
+
if (result.rows.length === 0) return text('No standup records found.')
|
|
65
|
+
|
|
66
|
+
const lines = ['š Standup Records', 'āāāāāāāāāāāāā']
|
|
67
|
+
for (const e of result.rows) {
|
|
68
|
+
lines.push(`\nš¤ ${e.user_name} (${e.entry_date})`)
|
|
69
|
+
if (e.done_text) lines.push(` ā
${e.done_text}`)
|
|
70
|
+
if (e.plan_text) lines.push(` š ${e.plan_text}`)
|
|
71
|
+
if (e.blockers) lines.push(` š§ ${e.blockers}`)
|
|
72
|
+
}
|
|
73
|
+
return text(lines.join('\n'))
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
77
|
+
// Standup Feedback
|
|
78
|
+
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
79
|
+
|
|
80
|
+
export async function toolReviewStandup(user: string, args: Record<string, unknown>): Promise<ToolResult> {
|
|
81
|
+
const entryId = args.standup_entry_id as number
|
|
82
|
+
const sprint = args.sprint as string
|
|
83
|
+
const targetUser = args.target_user as string
|
|
84
|
+
const feedbackText = args.feedback_text as string
|
|
85
|
+
const reviewType = (args.review_type as string) || 'comment'
|
|
86
|
+
|
|
87
|
+
if (!['comment', 'approve', 'request_changes'].includes(reviewType)) {
|
|
88
|
+
return text('ā review_type must be one of: comment, approve, request_changes.')
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Verify the standup entry exists and validate consistency
|
|
92
|
+
const entryCheck = await query<{ id: number; user_name: string; sprint: string }>(
|
|
93
|
+
'SELECT id, user_name, sprint FROM pm_standup_entries WHERE id = ?', [entryId],
|
|
94
|
+
)
|
|
95
|
+
if (entryCheck.rows.length === 0) return text('ā Standup entry not found.')
|
|
96
|
+
const entry = entryCheck.rows[0]
|
|
97
|
+
if (entry.sprint !== sprint) return text(`ā Sprint mismatch: entry belongs to ${entry.sprint} but ${sprint} was requested.`)
|
|
98
|
+
if (entry.user_name !== targetUser) return text(`ā Target user mismatch: entry author is ${entry.user_name} but ${targetUser} was requested.`)
|
|
99
|
+
|
|
100
|
+
const result = await execute(
|
|
101
|
+
'INSERT INTO pm_standup_feedback (standup_entry_id, sprint, target_user, feedback_by, feedback_text, review_type) VALUES (?, ?, ?, ?, ?, ?)',
|
|
102
|
+
[entryId, sprint, targetUser, user, feedbackText, reviewType],
|
|
103
|
+
)
|
|
104
|
+
if (result.error) return text(`ā ${result.error}`)
|
|
105
|
+
|
|
106
|
+
const icon = reviewType === 'approve' ? 'ā
' : reviewType === 'request_changes' ? 'š' : 'š¬'
|
|
107
|
+
return text(`${icon} Feedback registered for ${targetUser}'s standup (${reviewType})`)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export async function toolGetStandupFeedback(args: Record<string, unknown>): Promise<ToolResult> {
|
|
111
|
+
const entryId = args.standup_entry_id as number | undefined
|
|
112
|
+
const sprint = args.sprint as string | undefined
|
|
113
|
+
const user = args.user as string | undefined
|
|
114
|
+
|
|
115
|
+
let result
|
|
116
|
+
if (entryId) {
|
|
117
|
+
result = await query('SELECT * FROM pm_standup_feedback WHERE standup_entry_id = ? ORDER BY created_at ASC', [entryId])
|
|
118
|
+
} else if (sprint && user) {
|
|
119
|
+
result = await query('SELECT * FROM pm_standup_feedback WHERE sprint = ? AND target_user = ? ORDER BY created_at DESC LIMIT 50', [sprint, user])
|
|
120
|
+
} else {
|
|
121
|
+
return text('ā standup_entry_id or (sprint + user) parameters required.')
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (result.error) return text(`ā ${result.error}`)
|
|
125
|
+
if (result.rows.length === 0) return text('š No feedback found.')
|
|
126
|
+
|
|
127
|
+
const lines = ['š Standup Feedback', 'āāāāāāāāāāāāā']
|
|
128
|
+
for (const f of result.rows as Array<Record<string, unknown>>) {
|
|
129
|
+
const icon = f.review_type === 'approve' ? 'ā
' : f.review_type === 'request_changes' ? 'š' : 'š¬'
|
|
130
|
+
lines.push(`${icon} [${f.review_type}] ${f.feedback_by} ā ${f.target_user}`)
|
|
131
|
+
lines.push(` ${f.feedback_text}`)
|
|
132
|
+
lines.push(` ${f.created_at}`)
|
|
133
|
+
lines.push('')
|
|
134
|
+
}
|
|
135
|
+
return text(lines.join('\n'))
|
|
136
|
+
}
|