popilot 0.5.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/adapters/codex/.codex/commands/_domain.md.hbs +33 -0
- package/adapters/codex/.codex/commands/analytics.md.hbs +55 -0
- package/adapters/codex/.codex/commands/daily.md.hbs +301 -0
- package/adapters/codex/.codex/commands/dev.md.hbs +62 -0
- package/adapters/codex/.codex/commands/gtm.md +82 -0
- package/adapters/codex/.codex/commands/handoff.md +259 -0
- package/adapters/codex/.codex/commands/market.md +120 -0
- package/adapters/codex/.codex/commands/metrics.md +123 -0
- package/adapters/codex/.codex/commands/oscar-loop.md +436 -0
- package/adapters/codex/.codex/commands/party.md +85 -0
- package/adapters/codex/.codex/commands/plan.md +43 -0
- package/adapters/codex/.codex/commands/research.md +203 -0
- package/adapters/codex/.codex/commands/retro.md +68 -0
- package/adapters/codex/.codex/commands/save.md +440 -0
- package/adapters/codex/.codex/commands/sessions.md +139 -0
- package/adapters/codex/.codex/commands/sprint.md +106 -0
- package/adapters/codex/.codex/commands/start.md +396 -0
- package/adapters/codex/.codex/commands/strategy.md +41 -0
- package/adapters/codex/.codex/commands/task.md +220 -0
- package/adapters/codex/.codex/commands/tracking.md +116 -0
- package/adapters/codex/.codex/commands/validate.md +58 -0
- package/adapters/codex/AGENTS.md.hbs +210 -0
- package/adapters/codex/manifest.yaml +36 -0
- package/adapters/gemini/.gemini/commands/_domain.md.hbs +33 -0
- package/adapters/gemini/.gemini/commands/analytics.md.hbs +55 -0
- package/adapters/gemini/.gemini/commands/daily.md.hbs +301 -0
- package/adapters/gemini/.gemini/commands/dev.md.hbs +62 -0
- package/adapters/gemini/.gemini/commands/gtm.md +82 -0
- package/adapters/gemini/.gemini/commands/handoff.md +259 -0
- package/adapters/gemini/.gemini/commands/market.md +120 -0
- package/adapters/gemini/.gemini/commands/metrics.md +123 -0
- package/adapters/gemini/.gemini/commands/oscar-loop.md +436 -0
- package/adapters/gemini/.gemini/commands/party.md +85 -0
- package/adapters/gemini/.gemini/commands/plan.md +43 -0
- package/adapters/gemini/.gemini/commands/research.md +203 -0
- package/adapters/gemini/.gemini/commands/retro.md +68 -0
- package/adapters/gemini/.gemini/commands/save.md +440 -0
- package/adapters/gemini/.gemini/commands/sessions.md +139 -0
- package/adapters/gemini/.gemini/commands/sprint.md +106 -0
- package/adapters/gemini/.gemini/commands/start.md +396 -0
- package/adapters/gemini/.gemini/commands/strategy.md +41 -0
- package/adapters/gemini/.gemini/commands/task.md +220 -0
- package/adapters/gemini/.gemini/commands/tracking.md +116 -0
- package/adapters/gemini/.gemini/commands/validate.md +58 -0
- package/adapters/gemini/GEMINI.md.hbs +210 -0
- package/adapters/gemini/manifest.yaml +36 -0
- package/bin/cli.mjs +215 -4
- package/lib/doctor.mjs +38 -1
- package/lib/hydrate.mjs +15 -0
- package/lib/industry-presets.mjs +135 -0
- package/lib/scaffold.mjs +5 -0
- package/lib/setup-wizard.mjs +71 -2
- package/package.json +1 -1
- package/scaffold/.context/agents/TEMPLATE.md +14 -0
- package/scaffold/.context/agents/analyst.md.hbs +3 -3
- package/scaffold/.context/agents/developer.md.hbs +5 -5
- package/scaffold/.context/agents/gtm-strategist.md.hbs +3 -3
- package/scaffold/.context/agents/handoff-specialist.md.hbs +18 -18
- package/scaffold/.context/agents/market-researcher.md.hbs +6 -6
- package/scaffold/.context/agents/orchestrator.md.hbs +8 -8
- package/scaffold/.context/agents/planner.md.hbs +6 -6
- package/scaffold/.context/agents/qa.md.hbs +5 -5
- package/scaffold/.context/agents/researcher.md.hbs +33 -6
- package/scaffold/.context/agents/strategist.md.hbs +8 -8
- package/scaffold/.context/agents/tracking-governor.md.hbs +2 -2
- package/scaffold/.context/project.yaml.example +25 -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,230 @@
|
|
|
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 toolListStories(args: Record<string, unknown>): Promise<ToolResult> {
|
|
5
|
+
const isBacklog = (args.sprint as string)?.toLowerCase() === 'backlog'
|
|
6
|
+
const sprint = isBacklog ? null : await resolveSprint(args.sprint as string | undefined)
|
|
7
|
+
if (!isBacklog && !sprint) return err('Please specify a sprint.')
|
|
8
|
+
|
|
9
|
+
let sql = `SELECT s.id, s.title, s.status, s.priority, s.assignee, s.area, s.story_points,
|
|
10
|
+
e.id as epic_id, e.title as epic_title
|
|
11
|
+
FROM pm_stories s LEFT JOIN pm_epics e ON s.epic_id = e.id`
|
|
12
|
+
const sqlArgs: (string | number)[] = []
|
|
13
|
+
|
|
14
|
+
if (isBacklog) {
|
|
15
|
+
sql += ' WHERE s.sprint IS NULL'
|
|
16
|
+
} else {
|
|
17
|
+
sql += ' WHERE s.sprint = ?'
|
|
18
|
+
sqlArgs.push(sprint!)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (args.epic_id !== undefined) { sql += ' AND s.epic_id = ?'; sqlArgs.push(args.epic_id as number) }
|
|
22
|
+
if (args.status) { sql += ' AND s.status = ?'; sqlArgs.push(args.status as string) }
|
|
23
|
+
if (args.assignee) { sql += ' AND s.assignee LIKE ?'; sqlArgs.push(`%${args.assignee as string}%`) }
|
|
24
|
+
sql += ' ORDER BY e.title, s.sort_order'
|
|
25
|
+
|
|
26
|
+
const result = await query<{
|
|
27
|
+
id: number; title: string; status: string; priority: string; assignee: string | null
|
|
28
|
+
area: string; story_points: number | null; epic_id: number | null; epic_title: string | null
|
|
29
|
+
}>(sql, sqlArgs)
|
|
30
|
+
if (result.error) return err(result.error)
|
|
31
|
+
const label = isBacklog ? 'Backlog' : sprint!.toUpperCase()
|
|
32
|
+
if (result.rows.length === 0) return text(`No stories in ${label}.`)
|
|
33
|
+
|
|
34
|
+
const statusIcon: Record<string, string> = { draft: '📝', backlog: '📋', ready: '🟡', 'in-progress': '🔵', review: '🟣', done: '✅' }
|
|
35
|
+
const priorityIcon: Record<string, string> = { low: '⬇️', medium: '➡️', high: '⬆️', critical: '🔴' }
|
|
36
|
+
|
|
37
|
+
const lines = [`📖 ${label} Stories (${result.rows.length})`, '─────────────']
|
|
38
|
+
let lastEpic: string | null = null
|
|
39
|
+
for (const s of result.rows) {
|
|
40
|
+
const epic = s.epic_title ?? '(No epic)'
|
|
41
|
+
if (epic !== lastEpic) { lines.push(`\n🏷 ${epic}`); lastEpic = epic }
|
|
42
|
+
const pts = s.story_points ? ` ${s.story_points}pt` : ''
|
|
43
|
+
lines.push(` ${statusIcon[s.status] ?? '⬜'} [S${s.id}] ${s.title} ${priorityIcon[s.priority] ?? ''}${pts} (${s.assignee ?? 'Unassigned'})`)
|
|
44
|
+
}
|
|
45
|
+
return text(lines.join('\n'))
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function toolListBacklog(args: Record<string, unknown>): Promise<ToolResult> {
|
|
49
|
+
let sql = `SELECT s.id, s.title, s.status, s.priority, s.assignee, s.area, s.story_points,
|
|
50
|
+
e.id as epic_id, e.title as epic_title
|
|
51
|
+
FROM pm_stories s LEFT JOIN pm_epics e ON s.epic_id = e.id WHERE s.sprint IS NULL`
|
|
52
|
+
const sqlArgs: (string | number)[] = []
|
|
53
|
+
|
|
54
|
+
if (args.epic_id !== undefined) { sql += ' AND s.epic_id = ?'; sqlArgs.push(args.epic_id as number) }
|
|
55
|
+
if (args.status) { sql += ' AND s.status = ?'; sqlArgs.push(args.status as string) }
|
|
56
|
+
if (args.assignee) { sql += ' AND s.assignee LIKE ?'; sqlArgs.push(`%${args.assignee as string}%`) }
|
|
57
|
+
sql += ' ORDER BY e.title, s.sort_order'
|
|
58
|
+
|
|
59
|
+
const result = await query<{
|
|
60
|
+
id: number; title: string; status: string; priority: string; assignee: string | null
|
|
61
|
+
area: string; story_points: number | null; epic_id: number | null; epic_title: string | null
|
|
62
|
+
}>(sql, sqlArgs)
|
|
63
|
+
if (result.error) return err(result.error)
|
|
64
|
+
if (result.rows.length === 0) return text('No stories in backlog.')
|
|
65
|
+
|
|
66
|
+
const statusIcon: Record<string, string> = { draft: '📝', backlog: '📋', ready: '🟡', 'in-progress': '🔵', review: '🟣', done: '✅' }
|
|
67
|
+
const priorityIcon: Record<string, string> = { low: '⬇️', medium: '➡️', high: '⬆️', critical: '🔴' }
|
|
68
|
+
|
|
69
|
+
const lines = [`📦 Backlog Stories (${result.rows.length})`, '─────────────']
|
|
70
|
+
let lastEpic: string | null = null
|
|
71
|
+
for (const s of result.rows) {
|
|
72
|
+
const epic = s.epic_title ?? '(No epic)'
|
|
73
|
+
if (epic !== lastEpic) { lines.push(`\n🏷 ${epic}`); lastEpic = epic }
|
|
74
|
+
const pts = s.story_points ? ` ${s.story_points}pt` : ''
|
|
75
|
+
lines.push(` ${statusIcon[s.status] ?? '⬜'} [S${s.id}] ${s.title} ${priorityIcon[s.priority] ?? ''}${pts} (${s.assignee ?? 'Unassigned'})`)
|
|
76
|
+
}
|
|
77
|
+
return text(lines.join('\n'))
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function toolAddStory(user: string, args: Record<string, unknown>): Promise<ToolResult> {
|
|
81
|
+
const title = args.title as string
|
|
82
|
+
const sprintArg = args.sprint as string | undefined
|
|
83
|
+
const isBacklog = !sprintArg || sprintArg.toLowerCase() === 'backlog'
|
|
84
|
+
const sprint = isBacklog ? null : await resolveSprint(sprintArg)
|
|
85
|
+
|
|
86
|
+
const epicId = (args.epic_id as number) ?? null
|
|
87
|
+
const description = (args.description as string) ?? null
|
|
88
|
+
const acceptanceCriteria = (args.acceptance_criteria as string) ?? null
|
|
89
|
+
const assignee = (args.assignee as string) ?? null
|
|
90
|
+
const assigneeErr = await validateAssignee(assignee)
|
|
91
|
+
if (assigneeErr) return err(assigneeErr)
|
|
92
|
+
const assigneeId = assignee ? await resolveMemberId(assignee) : null
|
|
93
|
+
const priority = (args.priority as string) || 'medium'
|
|
94
|
+
const area = (args.area as string) || 'FE'
|
|
95
|
+
const storyPoints = (args.story_points as number) ?? null
|
|
96
|
+
|
|
97
|
+
const maxResult = sprint
|
|
98
|
+
? await query<{ max_order: number | null }>('SELECT MAX(sort_order) as max_order FROM pm_stories WHERE sprint = ?', [sprint])
|
|
99
|
+
: await query<{ max_order: number | null }>('SELECT MAX(sort_order) as max_order FROM pm_stories WHERE sprint IS NULL')
|
|
100
|
+
const sortOrder = (maxResult.rows[0]?.max_order ?? -1) + 1
|
|
101
|
+
|
|
102
|
+
const epicUid = epicId ? `pm:${epicId}` : 'pm:0'
|
|
103
|
+
|
|
104
|
+
const result = await execute(
|
|
105
|
+
'INSERT INTO pm_stories (epic_id, epic_uid, sprint, title, description, acceptance_criteria, assignee, assignee_id, status, priority, area, story_points, sort_order, start_date, due_date) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
|
106
|
+
[epicId, epicUid, sprint, title, description, acceptanceCriteria, assignee, assigneeId, 'backlog', priority, area, storyPoints, sortOrder, (args.start_date as string) ?? null, (args.due_date as string) ?? null],
|
|
107
|
+
)
|
|
108
|
+
if (result.error) return err(result.error)
|
|
109
|
+
const idResult = await query<{ id: number }>('SELECT last_insert_rowid() as id')
|
|
110
|
+
const newId = idResult.rows[0]?.id ?? '?'
|
|
111
|
+
|
|
112
|
+
// Notify assignee(s)
|
|
113
|
+
if (assignee) {
|
|
114
|
+
for (const a of assignee.split(',').map(s => s.trim()).filter(Boolean)) {
|
|
115
|
+
await notify(a, 'story_assigned', `Story assigned: ${title}`, `${user} assigned story [S${newId}].`, 'story', String(newId), 'board', user)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const epicLabel = epicId ? ` (Epic #${epicId})` : ''
|
|
120
|
+
const sprintLabel = sprint ? sprint.toUpperCase() : 'Backlog'
|
|
121
|
+
return text(`✅ Story created${epicLabel}: ${title} (ID: ${newId}, ${sprintLabel})`)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export async function toolUpdateStory(user: string, args: Record<string, unknown>): Promise<ToolResult> {
|
|
125
|
+
const storyId = args.story_id as number
|
|
126
|
+
|
|
127
|
+
// Get current story for notification comparison
|
|
128
|
+
const current = await query<{ assignee: string | null; title: string }>(
|
|
129
|
+
'SELECT assignee, title FROM pm_stories WHERE id = ?', [storyId],
|
|
130
|
+
)
|
|
131
|
+
if (current.error) return err(current.error)
|
|
132
|
+
if (current.rows.length === 0) return err(`Story #${storyId} not found.`)
|
|
133
|
+
const oldAssignee = current.rows[0].assignee
|
|
134
|
+
const storyTitle = current.rows[0].title
|
|
135
|
+
|
|
136
|
+
if (args.assignee !== undefined) {
|
|
137
|
+
const assigneeErr = await validateAssignee(args.assignee as string | null)
|
|
138
|
+
if (assigneeErr) return err(assigneeErr)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Normalize sprint='backlog' to NULL
|
|
142
|
+
if (args.sprint !== undefined) {
|
|
143
|
+
const sv = args.sprint as string | null
|
|
144
|
+
if (sv === null || (typeof sv === 'string' && sv.toLowerCase() === 'backlog')) {
|
|
145
|
+
args.sprint = null
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const fieldMap: Record<string, string> = {
|
|
150
|
+
title: 'title', description: 'description', acceptance_criteria: 'acceptance_criteria',
|
|
151
|
+
assignee: 'assignee', status: 'status', priority: 'priority', area: 'area',
|
|
152
|
+
story_points: 'story_points', figma_url: 'figma_url', epic_id: 'epic_id', sprint: 'sprint',
|
|
153
|
+
}
|
|
154
|
+
const sets: string[] = []
|
|
155
|
+
const sqlArgs: (string | number | null)[] = []
|
|
156
|
+
|
|
157
|
+
for (const [key, col] of Object.entries(fieldMap)) {
|
|
158
|
+
if (args[key] !== undefined) {
|
|
159
|
+
sets.push(`${col} = ?`)
|
|
160
|
+
sqlArgs.push(args[key] as string | number | null)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Also update assignee_id when assignee changes
|
|
165
|
+
if (args.assignee !== undefined) {
|
|
166
|
+
const newAssignee = args.assignee as string | null
|
|
167
|
+
const newAssigneeId = newAssignee ? await resolveMemberId(newAssignee) : null
|
|
168
|
+
sets.push('assignee_id = ?')
|
|
169
|
+
sqlArgs.push(newAssigneeId)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (sets.length === 0) return text('No fields to update.')
|
|
173
|
+
|
|
174
|
+
sets.push('updated_at = CURRENT_TIMESTAMP')
|
|
175
|
+
sqlArgs.push(storyId)
|
|
176
|
+
const result = await execute(`UPDATE pm_stories SET ${sets.join(', ')} WHERE id = ?`, sqlArgs)
|
|
177
|
+
if (result.error) return err(result.error)
|
|
178
|
+
if (result.rowsAffected === 0) return err(`Story #${storyId} not found.`)
|
|
179
|
+
|
|
180
|
+
// Notify new assignee(s) on assignment change
|
|
181
|
+
if (args.assignee !== undefined) {
|
|
182
|
+
const newAssignee = args.assignee as string | null
|
|
183
|
+
if (newAssignee) {
|
|
184
|
+
const oldSet = new Set((oldAssignee ?? '').split(',').map(s => s.trim()).filter(Boolean))
|
|
185
|
+
for (const a of newAssignee.split(',').map(s => s.trim()).filter(Boolean)) {
|
|
186
|
+
if (!oldSet.has(a)) {
|
|
187
|
+
await notify(a, 'story_assigned', `Story assigned: ${storyTitle}`, `${user} assigned story [S${storyId}].`, 'story', String(storyId), 'board', user)
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Agent webhook notification (on status change)
|
|
194
|
+
if (args.status !== undefined) {
|
|
195
|
+
const assignee = (args.assignee as string) ?? oldAssignee
|
|
196
|
+
if (assignee) {
|
|
197
|
+
const { notifyByName } = await import('../utils/agent-notify.js')
|
|
198
|
+
await notifyByName(assignee, `📌 Status changed`, `S${storyId}: ${storyTitle}\nStatus: ${args.status}`)
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return text(`✅ Story #${storyId} updated`)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export async function toolDeleteStory(args: Record<string, unknown>): Promise<ToolResult> {
|
|
206
|
+
const storyId = args.story_id as number
|
|
207
|
+
const r1 = await execute('DELETE FROM pm_tasks WHERE story_id = ?', [storyId])
|
|
208
|
+
if (r1.error) return err(r1.error)
|
|
209
|
+
const r2 = await execute('DELETE FROM pm_stories WHERE id = ?', [storyId])
|
|
210
|
+
if (r2.error) return err(r2.error)
|
|
211
|
+
if (r2.rowsAffected === 0) return err(`Story #${storyId} not found.`)
|
|
212
|
+
return text(`✅ Story #${storyId} deleted (${r1.rowsAffected} tasks also deleted)`)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export async function toolAssignStory(args: Record<string, unknown>): Promise<ToolResult> {
|
|
216
|
+
const storyId = args.story_id as number
|
|
217
|
+
const sprintId = args.sprint_id as string
|
|
218
|
+
if (!storyId || !sprintId) return err('story_id, sprint_id required')
|
|
219
|
+
const r = await execute('UPDATE pm_stories SET sprint = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?', [sprintId, storyId])
|
|
220
|
+
if (r.error) return err(r.error)
|
|
221
|
+
return text(`✅ S${storyId} → ${sprintId} assigned`)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export async function toolUnassignStory(args: Record<string, unknown>): Promise<ToolResult> {
|
|
225
|
+
const storyId = args.story_id as number
|
|
226
|
+
if (!storyId) return err('story_id required')
|
|
227
|
+
const r = await execute('UPDATE pm_stories SET sprint = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ?', [storyId])
|
|
228
|
+
if (r.error) return err(r.error)
|
|
229
|
+
return text(`✅ S${storyId} → backlog unassigned`)
|
|
230
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
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 toolListTasks(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 status = args.status as string | undefined
|
|
8
|
+
|
|
9
|
+
let sql = `SELECT t.id as task_id, t.title as task_title, t.status as task_status, s.id as story_id, s.title as story_title, e.id as epic_id, e.title as epic_title 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 t.assignee = ? AND s.sprint = ?`
|
|
10
|
+
const sqlArgs: (string | number)[] = [user, sprint]
|
|
11
|
+
if (status) { sql += ' AND t.status = ?'; sqlArgs.push(status) }
|
|
12
|
+
sql += ' ORDER BY e.title, s.sort_order, t.sort_order'
|
|
13
|
+
|
|
14
|
+
const result = await query<{ task_id: number; task_title: string; task_status: string; story_id: number; story_title: string; epic_id: number | null; epic_title: string | null }>(sql, sqlArgs)
|
|
15
|
+
if (result.error) return err(result.error)
|
|
16
|
+
if (result.rows.length === 0) return text(`No tasks found for ${user}.`)
|
|
17
|
+
|
|
18
|
+
const statusIcon: Record<string, string> = { todo: '⬜', 'in-progress': '🔵', done: '✅' }
|
|
19
|
+
const tree = new Map<string, Map<string, Array<{ id: number; title: string; status: string }>>>()
|
|
20
|
+
for (const r of result.rows) {
|
|
21
|
+
const ek = r.epic_title ?? '(No epic)'
|
|
22
|
+
if (!tree.has(ek)) tree.set(ek, new Map())
|
|
23
|
+
const em = tree.get(ek)!
|
|
24
|
+
const sk = `[S${r.story_id}] ${r.story_title}`
|
|
25
|
+
if (!em.has(sk)) em.set(sk, [])
|
|
26
|
+
em.get(sk)!.push({ id: r.task_id, title: r.task_title, status: r.task_status })
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const lines: string[] = [`📋 ${user}'s Tasks (${sprint.toUpperCase()})`, '─────────────']
|
|
30
|
+
for (const [epicTitle, stories] of tree) {
|
|
31
|
+
lines.push(`\n🏷 ${epicTitle}`)
|
|
32
|
+
for (const [storyTitle, tasks] of stories) {
|
|
33
|
+
lines.push(` 📖 ${storyTitle}`)
|
|
34
|
+
for (const t of tasks) lines.push(` ${statusIcon[t.status] ?? '⬜'} [T${t.id}] ${t.title}`)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return text(lines.join('\n'))
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function toolGetTask(args: Record<string, unknown>): Promise<ToolResult> {
|
|
41
|
+
const taskId = args.task_id as number
|
|
42
|
+
const taskResult = await query<{ id: number; story_id: number; title: string; assignee: string | null; status: string; description: string | null; sort_order: number; created_at: string; updated_at: string }>('SELECT * FROM pm_tasks WHERE id = ?', [taskId])
|
|
43
|
+
if (taskResult.error) return err(taskResult.error)
|
|
44
|
+
if (taskResult.rows.length === 0) return err(`Task #${taskId} not found.`)
|
|
45
|
+
|
|
46
|
+
const t = taskResult.rows[0]
|
|
47
|
+
const [storyResult, siblingsResult] = await Promise.all([
|
|
48
|
+
query<{ id: number; title: string; description: string | null; acceptance_criteria: string | null; assignee: string | null; status: string; sprint: string }>('SELECT id, title, description, acceptance_criteria, assignee, status, sprint FROM pm_stories WHERE id = ?', [t.story_id]),
|
|
49
|
+
query<{ id: number; title: string; status: string; assignee: string | null }>('SELECT id, title, status, assignee FROM pm_tasks WHERE story_id = ? ORDER BY sort_order', [t.story_id]),
|
|
50
|
+
])
|
|
51
|
+
|
|
52
|
+
const si: Record<string, string> = { todo: '⬜', 'in-progress': '🔵', done: '✅' }
|
|
53
|
+
const s = storyResult.rows[0]
|
|
54
|
+
const lines = [`📋 Task #${t.id}: ${t.title}`, '─────────────', `Status: ${si[t.status] ?? '⬜'} ${t.status}`, `Assignee: ${t.assignee ?? 'Unassigned'}`, t.description ? `Description: ${t.description}` : '', `Created: ${t.created_at} | Modified: ${t.updated_at}`].filter(Boolean)
|
|
55
|
+
if (s) {
|
|
56
|
+
lines.push('', `📖 Parent Story [S${s.id}]: ${s.title}`, ` Sprint: ${s.sprint} | Status: ${s.status} | Assignee: ${s.assignee ?? 'Unassigned'}`)
|
|
57
|
+
if (s.description) lines.push(` Description: ${s.description}`)
|
|
58
|
+
if (s.acceptance_criteria) lines.push(` AC: ${s.acceptance_criteria}`)
|
|
59
|
+
}
|
|
60
|
+
if (siblingsResult.rows.length > 0) {
|
|
61
|
+
lines.push('', '👥 Sibling Tasks:')
|
|
62
|
+
for (const sb of siblingsResult.rows) {
|
|
63
|
+
const marker = sb.id === taskId ? ' ← current' : ''
|
|
64
|
+
lines.push(` ${si[sb.status] ?? '⬜'} [T${sb.id}] ${sb.title} (${sb.assignee ?? 'Unassigned'})${marker}`)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return text(lines.join('\n'))
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function toolUpdateTaskStatus(args: Record<string, unknown>): Promise<ToolResult> {
|
|
71
|
+
const taskId = args.task_id as number
|
|
72
|
+
const status = args.status as string
|
|
73
|
+
const result = await execute('UPDATE pm_tasks SET status = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?', [status, taskId])
|
|
74
|
+
if (result.error) return err(result.error)
|
|
75
|
+
if (result.rowsAffected === 0) return err(`Task #${taskId} not found.`)
|
|
76
|
+
return text(`✅ Task #${taskId} → ${status}`)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function toolUpdateTask(user: string, args: Record<string, unknown>): Promise<ToolResult> {
|
|
80
|
+
const taskId = args.task_id as number
|
|
81
|
+
|
|
82
|
+
// Get current for notification
|
|
83
|
+
const current = await query<{ assignee: string | null; title: string }>(
|
|
84
|
+
'SELECT assignee, title FROM pm_tasks WHERE id = ?', [taskId],
|
|
85
|
+
)
|
|
86
|
+
if (current.error) return err(current.error)
|
|
87
|
+
if (current.rows.length === 0) return err(`Task #${taskId} not found.`)
|
|
88
|
+
const oldAssignee = current.rows[0].assignee
|
|
89
|
+
const taskTitle = current.rows[0].title
|
|
90
|
+
|
|
91
|
+
if (args.assignee !== undefined) {
|
|
92
|
+
const assigneeErr = await validateAssignee(args.assignee as string | null)
|
|
93
|
+
if (assigneeErr) return err(assigneeErr)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const fieldMap: Record<string, string> = { title: 'title', assignee: 'assignee', status: 'status', description: 'description', story_points: 'story_points' }
|
|
97
|
+
const sets: string[] = []
|
|
98
|
+
const sqlArgs: (string | number | null)[] = []
|
|
99
|
+
|
|
100
|
+
for (const [key, col] of Object.entries(fieldMap)) {
|
|
101
|
+
if (args[key] !== undefined) {
|
|
102
|
+
sets.push(`${col} = ?`)
|
|
103
|
+
sqlArgs.push(args[key] as string | number | null)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Also update assignee_id when assignee changes
|
|
108
|
+
if (args.assignee !== undefined) {
|
|
109
|
+
const newAssignee = args.assignee as string | null
|
|
110
|
+
const newAssigneeId = newAssignee ? await resolveMemberId(newAssignee) : null
|
|
111
|
+
sets.push('assignee_id = ?')
|
|
112
|
+
sqlArgs.push(newAssigneeId)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (sets.length === 0) return text('No fields to update.')
|
|
116
|
+
|
|
117
|
+
sets.push('updated_at = CURRENT_TIMESTAMP')
|
|
118
|
+
sqlArgs.push(taskId)
|
|
119
|
+
const result = await execute(`UPDATE pm_tasks SET ${sets.join(', ')} WHERE id = ?`, sqlArgs)
|
|
120
|
+
if (result.error) return err(result.error)
|
|
121
|
+
|
|
122
|
+
// Auto-sum story SP when task SP changes
|
|
123
|
+
if (args.story_points !== undefined) {
|
|
124
|
+
const storyResult = await query<{ story_id: number }>('SELECT story_id FROM pm_tasks WHERE id = ?', [taskId])
|
|
125
|
+
if (!storyResult.error && storyResult.rows.length > 0) {
|
|
126
|
+
const sid = storyResult.rows[0].story_id
|
|
127
|
+
const spResult = await query<{ total: number | null }>('SELECT SUM(story_points) as total FROM pm_tasks WHERE story_id = ?', [sid])
|
|
128
|
+
if (!spResult.error && spResult.rows.length > 0) {
|
|
129
|
+
await execute('UPDATE pm_stories SET story_points = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?', [spResult.rows[0].total ?? 0, sid])
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Notify on assignee change
|
|
135
|
+
if (args.assignee !== undefined) {
|
|
136
|
+
const newAssignee = args.assignee as string | null
|
|
137
|
+
if (newAssignee && newAssignee !== oldAssignee) {
|
|
138
|
+
await notify(newAssignee, 'task_assigned', `Task assigned: ${taskTitle}`, `${user} assigned task [T${taskId}].`, 'task', String(taskId), 'board', user)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return text(`✅ Task #${taskId} updated`)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export async function toolAddTask(user: string, args: Record<string, unknown>): Promise<ToolResult> {
|
|
146
|
+
const storyId = args.story_id as number
|
|
147
|
+
const title = args.title as string
|
|
148
|
+
const assignee = (args.assignee as string) || user
|
|
149
|
+
const assigneeErr = await validateAssignee(assignee)
|
|
150
|
+
if (assigneeErr) return err(assigneeErr)
|
|
151
|
+
const assigneeId = await resolveMemberId(assignee)
|
|
152
|
+
const description = args.description as string | undefined
|
|
153
|
+
|
|
154
|
+
const storyPoints = (args.story_points as number) ?? null
|
|
155
|
+
|
|
156
|
+
const maxResult = await query<{ max_order: number | null }>('SELECT MAX(sort_order) as max_order FROM pm_tasks WHERE story_id = ?', [storyId])
|
|
157
|
+
const sortOrder = (maxResult.rows[0]?.max_order ?? -1) + 1
|
|
158
|
+
const result = await execute('INSERT INTO pm_tasks (story_id, title, assignee, assignee_id, description, sort_order, story_points) VALUES (?, ?, ?, ?, ?, ?, ?)', [storyId, title, assignee, assigneeId, description ?? null, sortOrder, storyPoints])
|
|
159
|
+
if (result.error) return err(result.error)
|
|
160
|
+
|
|
161
|
+
const idResult = await query<{ id: number }>('SELECT last_insert_rowid() as id')
|
|
162
|
+
const newId = idResult.rows[0]?.id ?? '?'
|
|
163
|
+
|
|
164
|
+
// Auto-sum story SP
|
|
165
|
+
if (storyPoints != null) {
|
|
166
|
+
const spResult = await query<{ total: number | null }>('SELECT SUM(story_points) as total FROM pm_tasks WHERE story_id = ?', [storyId])
|
|
167
|
+
if (!spResult.error && spResult.rows.length > 0) {
|
|
168
|
+
await execute('UPDATE pm_stories SET story_points = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?', [spResult.rows[0].total ?? 0, storyId])
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Notify assignee
|
|
173
|
+
if (assignee !== user) {
|
|
174
|
+
await notify(assignee, 'task_assigned', `Task assigned: ${title}`, `${user} assigned task [T${newId}].`, 'task', String(newId), 'board', user)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const spLabel = storyPoints != null ? ` ${storyPoints}SP` : ''
|
|
178
|
+
return text(`✅ Task added (Story #${storyId}): ${title}${spLabel} (ID: ${newId})`)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export async function toolDeleteTask(args: Record<string, unknown>): Promise<ToolResult> {
|
|
182
|
+
const taskId = args.task_id as number
|
|
183
|
+
const result = await execute('DELETE FROM pm_tasks WHERE id = ?', [taskId])
|
|
184
|
+
if (result.error) return err(result.error)
|
|
185
|
+
if (result.rowsAffected === 0) return err(`Task #${taskId} not found.`)
|
|
186
|
+
return text(`✅ Task #${taskId} deleted`)
|
|
187
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { query, execute } from '../db/adapter.js'
|
|
2
|
+
|
|
3
|
+
export type ToolResult = { content: Array<{ type: string; text: string }>; isError?: boolean }
|
|
4
|
+
|
|
5
|
+
export function today(): string {
|
|
6
|
+
const d = new Date()
|
|
7
|
+
const y = d.getFullYear()
|
|
8
|
+
const m = String(d.getMonth() + 1).padStart(2, '0')
|
|
9
|
+
const day = String(d.getDate()).padStart(2, '0')
|
|
10
|
+
return `${y}-${m}-${day}`
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function text(t: string): ToolResult {
|
|
14
|
+
return { content: [{ type: 'text', text: t }] }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function err(msg: string): ToolResult {
|
|
18
|
+
return text(`Error: ${msg}`)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function getActiveSprint(): Promise<string | null> {
|
|
22
|
+
const result = await query<{ id: string }>('SELECT id FROM nav_sprints WHERE active = 1 LIMIT 1')
|
|
23
|
+
return result.rows?.[0]?.id ?? null
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function resolveSprint(arg?: string): Promise<string | null> {
|
|
27
|
+
return arg || await getActiveSprint()
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function notify(
|
|
31
|
+
userName: string, type: string, title: string, body: string,
|
|
32
|
+
sourceType: string, sourceId: string | number, pageId: string, actor: string,
|
|
33
|
+
): Promise<void> {
|
|
34
|
+
if (userName === actor) return
|
|
35
|
+
await execute(
|
|
36
|
+
'INSERT INTO notifications (user_name, type, title, body, source_type, source_id, page_id, actor) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
|
|
37
|
+
[userName, type, title, body, sourceType, String(sourceId), pageId, actor],
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export { validateAssignee, resolveMemberId } from '../utils/assignee.js'
|
|
42
|
+
|
|
43
|
+
const RATE_LIMIT = 30
|
|
44
|
+
const emitRateMap = new Map<string, number[]>()
|
|
45
|
+
|
|
46
|
+
export function checkRateLimit(user: string): boolean {
|
|
47
|
+
const now = Date.now()
|
|
48
|
+
const window = 60_000
|
|
49
|
+
if (emitRateMap.size > 100) {
|
|
50
|
+
for (const [k, v] of emitRateMap) {
|
|
51
|
+
const filtered = v.filter(t => now - t < window)
|
|
52
|
+
if (filtered.length === 0) emitRateMap.delete(k)
|
|
53
|
+
else emitRateMap.set(k, filtered)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const timestamps = (emitRateMap.get(user) ?? []).filter(t => now - t < window)
|
|
57
|
+
if (timestamps.length >= RATE_LIMIT) return false
|
|
58
|
+
timestamps.push(now)
|
|
59
|
+
emitRateMap.set(user, timestamps)
|
|
60
|
+
return true
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function emitAgentEvent(
|
|
64
|
+
eventType: string, sourceAgent: string, targetAgent: string,
|
|
65
|
+
targetUser: string, payload: Record<string, unknown>,
|
|
66
|
+
): Promise<void> {
|
|
67
|
+
try {
|
|
68
|
+
const expiresAt = new Date(Date.now() + 24 * 3600_000).toISOString()
|
|
69
|
+
await execute(
|
|
70
|
+
`INSERT INTO agent_events (event_type, source_agent, target_agent, target_user, payload, expires_at)
|
|
71
|
+
VALUES (?, ?, ?, ?, ?, ?)`,
|
|
72
|
+
[eventType, sourceAgent, targetAgent, targetUser, JSON.stringify(payload), expiresAt],
|
|
73
|
+
)
|
|
74
|
+
} catch {
|
|
75
|
+
// fire-and-forget
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function progressBar(done: number, total: number, width = 10): string {
|
|
80
|
+
if (total === 0) return '░'.repeat(width)
|
|
81
|
+
const filled = Math.round((done / total) * width)
|
|
82
|
+
return '█'.repeat(filled) + '░'.repeat(width - filled)
|
|
83
|
+
}
|