popilot 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (165) hide show
  1. package/bin/cli.mjs +204 -2
  2. package/lib/doctor.mjs +38 -1
  3. package/lib/hydrate.mjs +15 -0
  4. package/lib/scaffold.mjs +5 -0
  5. package/lib/setup-wizard.mjs +35 -2
  6. package/package.json +1 -1
  7. package/scaffold/.context/project.yaml.example +19 -0
  8. package/scaffold/mcp-notification-server/package.json +18 -0
  9. package/scaffold/mcp-notification-server/src/index.ts +275 -0
  10. package/scaffold/mcp-notification-server/src/turso-client.ts +142 -0
  11. package/scaffold/mcp-notification-server/tsconfig.json +14 -0
  12. package/scaffold/mcp-pm/package.json +19 -0
  13. package/scaffold/mcp-pm/src/api-client.ts +69 -0
  14. package/scaffold/mcp-pm/src/index.ts +660 -0
  15. package/scaffold/mcp-pm/tsconfig.json +14 -0
  16. package/scaffold/pm-api/package.json +21 -0
  17. package/scaffold/pm-api/sql/001-memo-v2.sql +49 -0
  18. package/scaffold/pm-api/sql/002-notifications.sql +18 -0
  19. package/scaffold/pm-api/sql/003-content.sql +66 -0
  20. package/scaffold/pm-api/sql/004-agent-events.sql +21 -0
  21. package/scaffold/pm-api/sql/005-epic-sprint-decoupling.sql +6 -0
  22. package/scaffold/pm-api/sql/schema-core.sql +331 -0
  23. package/scaffold/pm-api/sql/schema-docs.sql +25 -0
  24. package/scaffold/pm-api/sql/schema-meetings.sql +17 -0
  25. package/scaffold/pm-api/sql/schema-rewards.sql +16 -0
  26. package/scaffold/pm-api/src/auth.ts +28 -0
  27. package/scaffold/pm-api/src/blockchain/adapter.ts +20 -0
  28. package/scaffold/pm-api/src/blockchain/tron.ts +62 -0
  29. package/scaffold/pm-api/src/db/adapter.ts +36 -0
  30. package/scaffold/pm-api/src/db/turso.ts +147 -0
  31. package/scaffold/pm-api/src/index.ts +114 -0
  32. package/scaffold/pm-api/src/mcp-tools/dashboard.ts +40 -0
  33. package/scaffold/pm-api/src/mcp-tools/epic.ts +67 -0
  34. package/scaffold/pm-api/src/mcp-tools/event.ts +89 -0
  35. package/scaffold/pm-api/src/mcp-tools/index.ts +11 -0
  36. package/scaffold/pm-api/src/mcp-tools/initiative.ts +51 -0
  37. package/scaffold/pm-api/src/mcp-tools/memo.ts +164 -0
  38. package/scaffold/pm-api/src/mcp-tools/notification.ts +37 -0
  39. package/scaffold/pm-api/src/mcp-tools/retro.ts +183 -0
  40. package/scaffold/pm-api/src/mcp-tools/sprint.ts +204 -0
  41. package/scaffold/pm-api/src/mcp-tools/standup.ts +136 -0
  42. package/scaffold/pm-api/src/mcp-tools/story.ts +230 -0
  43. package/scaffold/pm-api/src/mcp-tools/task.ts +187 -0
  44. package/scaffold/pm-api/src/mcp-tools/utils.ts +83 -0
  45. package/scaffold/pm-api/src/mcp.ts +871 -0
  46. package/scaffold/pm-api/src/nudge.ts +283 -0
  47. package/scaffold/pm-api/src/routes/auth.ts +32 -0
  48. package/scaffold/pm-api/src/routes/v2-activity.ts +27 -0
  49. package/scaffold/pm-api/src/routes/v2-admin.ts +165 -0
  50. package/scaffold/pm-api/src/routes/v2-dashboard.ts +189 -0
  51. package/scaffold/pm-api/src/routes/v2-docs.ts +34 -0
  52. package/scaffold/pm-api/src/routes/v2-initiatives.ts +118 -0
  53. package/scaffold/pm-api/src/routes/v2-kickoff.ts +265 -0
  54. package/scaffold/pm-api/src/routes/v2-meetings.ts +324 -0
  55. package/scaffold/pm-api/src/routes/v2-memos.ts +257 -0
  56. package/scaffold/pm-api/src/routes/v2-nav.ts +260 -0
  57. package/scaffold/pm-api/src/routes/v2-notifications.ts +79 -0
  58. package/scaffold/pm-api/src/routes/v2-page-content.ts +35 -0
  59. package/scaffold/pm-api/src/routes/v2-pm.ts +380 -0
  60. package/scaffold/pm-api/src/routes/v2-policy.ts +58 -0
  61. package/scaffold/pm-api/src/routes/v2-retro.ts +221 -0
  62. package/scaffold/pm-api/src/routes/v2-rewards.ts +132 -0
  63. package/scaffold/pm-api/src/routes/v2-scenarios.ts +48 -0
  64. package/scaffold/pm-api/src/routes/v2-search.ts +32 -0
  65. package/scaffold/pm-api/src/routes/v2-standup.ts +127 -0
  66. package/scaffold/pm-api/src/routes/v2-user.ts +38 -0
  67. package/scaffold/pm-api/src/types.ts +11 -0
  68. package/scaffold/pm-api/src/utils/activity.ts +22 -0
  69. package/scaffold/pm-api/src/utils/admin.ts +9 -0
  70. package/scaffold/pm-api/src/utils/agent-notify.ts +62 -0
  71. package/scaffold/pm-api/src/utils/assignee.ts +69 -0
  72. package/scaffold/pm-api/src/utils/db.ts +45 -0
  73. package/scaffold/pm-api/src/utils/initiative.ts +23 -0
  74. package/scaffold/pm-api/src/utils/retro-link.ts +32 -0
  75. package/scaffold/pm-api/src/utils/sprint-lifecycle.ts +96 -0
  76. package/scaffold/pm-api/tsconfig.json +15 -0
  77. package/scaffold/pm-api/wrangler.toml.hbs +11 -0
  78. package/scaffold/spec-site/package-lock.json +892 -0
  79. package/scaffold/spec-site/package.json +15 -1
  80. package/scaffold/spec-site/src/api/types.ts +6 -0
  81. package/scaffold/spec-site/src/components/AppHeader.vue +429 -55
  82. package/scaffold/spec-site/src/components/AuthGate.vue +117 -0
  83. package/scaffold/spec-site/src/components/BurndownChart.vue +78 -0
  84. package/scaffold/spec-site/src/components/DocComments.vue +137 -0
  85. package/scaffold/spec-site/src/components/DocEditor.vue +118 -0
  86. package/scaffold/spec-site/src/components/DocExportBar.vue +110 -0
  87. package/scaffold/spec-site/src/components/DocsSidebar.vue +309 -0
  88. package/scaffold/spec-site/src/components/EmptyState.vue +30 -0
  89. package/scaffold/spec-site/src/components/ErrorBanner.vue +38 -0
  90. package/scaffold/spec-site/src/components/Icon.vue +58 -0
  91. package/scaffold/spec-site/src/components/MemberSelect.vue +48 -0
  92. package/scaffold/spec-site/src/components/MemoChecklist.vue +88 -0
  93. package/scaffold/spec-site/src/components/MemoGraph.vue +75 -0
  94. package/scaffold/spec-site/src/components/MemoItem.vue +353 -0
  95. package/scaffold/spec-site/src/components/MemoRelations.vue +101 -0
  96. package/scaffold/spec-site/src/components/MemoTimeline.vue +53 -0
  97. package/scaffold/spec-site/src/components/MentionInput.vue +174 -0
  98. package/scaffold/spec-site/src/components/NotificationDropdown.vue +116 -0
  99. package/scaffold/spec-site/src/components/PriorityBadge.vue +23 -0
  100. package/scaffold/spec-site/src/components/SearchModal.vue +102 -0
  101. package/scaffold/spec-site/src/components/SlashCommand.ts +123 -0
  102. package/scaffold/spec-site/src/components/StateDisplay.vue +54 -0
  103. package/scaffold/spec-site/src/components/TreeNode.vue +82 -0
  104. package/scaffold/spec-site/src/components/UserAvatar.vue +24 -0
  105. package/scaffold/spec-site/src/components/VelocityChart.vue +77 -0
  106. package/scaffold/spec-site/src/composables/navTypes.ts +3 -0
  107. package/scaffold/spec-site/src/composables/pmTypes.ts +15 -2
  108. package/scaffold/spec-site/src/composables/useBottomSheet.ts +103 -0
  109. package/scaffold/spec-site/src/composables/useDashboard.ts +221 -0
  110. package/scaffold/spec-site/src/composables/useMediaQuery.ts +28 -0
  111. package/scaffold/spec-site/src/composables/useMemo.ts +39 -0
  112. package/scaffold/spec-site/src/composables/useNotification.ts +200 -0
  113. package/scaffold/spec-site/src/composables/usePmStore.ts +48 -1
  114. package/scaffold/spec-site/src/composables/useRetro.ts +6 -0
  115. package/scaffold/spec-site/src/composables/useStandup.ts +201 -0
  116. package/scaffold/spec-site/src/composables/useTheme.ts +37 -0
  117. package/scaffold/spec-site/src/composables/useTurso.ts +17 -0
  118. package/scaffold/spec-site/src/composables/useUser.ts +19 -1
  119. package/scaffold/spec-site/src/composables/useViewport.ts +26 -0
  120. package/scaffold/spec-site/src/features.ts +108 -0
  121. package/scaffold/spec-site/src/mockup/ComponentPalette.vue +61 -0
  122. package/scaffold/spec-site/src/mockup/MockupCanvas.vue +459 -0
  123. package/scaffold/spec-site/src/mockup/PropertyPanel.vue +217 -0
  124. package/scaffold/spec-site/src/mockup/componentCatalog.ts +68 -0
  125. package/scaffold/spec-site/src/mockup/useScenarios.ts +67 -0
  126. package/scaffold/spec-site/src/pages/AdminPage.vue +299 -0
  127. package/scaffold/spec-site/src/pages/DashboardPage.vue +650 -0
  128. package/scaffold/spec-site/src/pages/DocsEditor.vue +119 -0
  129. package/scaffold/spec-site/src/pages/DocsHub.vue +157 -0
  130. package/scaffold/spec-site/src/pages/DocsPage.vue +444 -0
  131. package/scaffold/spec-site/src/pages/InboxPage.vue +156 -0
  132. package/scaffold/spec-site/src/pages/MeetingsPage.vue +294 -0
  133. package/scaffold/spec-site/src/pages/MemosPage.vue +857 -0
  134. package/scaffold/spec-site/src/pages/MockupEditorPage.vue +611 -0
  135. package/scaffold/spec-site/src/pages/MockupListPage.vue +121 -0
  136. package/scaffold/spec-site/src/pages/MockupViewerPage.vue +199 -0
  137. package/scaffold/spec-site/src/pages/MyPage.vue +343 -0
  138. package/scaffold/spec-site/src/pages/NotificationSettingsPage.vue +59 -0
  139. package/scaffold/spec-site/src/pages/RewardsPage.vue +266 -0
  140. package/scaffold/spec-site/src/pages/SprintAdmin.vue +521 -0
  141. package/scaffold/spec-site/src/pages/SprintTimeline.vue +159 -0
  142. package/scaffold/spec-site/src/pages/board/BoardAdmin.vue +422 -0
  143. package/scaffold/spec-site/src/pages/board/BoardEpicSection.vue +54 -0
  144. package/scaffold/spec-site/src/pages/board/BoardPage.vue +884 -0
  145. package/scaffold/spec-site/src/pages/board/BoardStoryCard.vue +67 -0
  146. package/scaffold/spec-site/src/pages/board/BoardTaskItem.vue +52 -0
  147. package/scaffold/spec-site/src/pages/board/KanbanBoard.vue +93 -0
  148. package/scaffold/spec-site/src/pages/board/MyTasksPage.vue +202 -0
  149. package/scaffold/spec-site/src/pages/board/SprintClose.vue +167 -0
  150. package/scaffold/spec-site/src/pages/board/SprintColumn.vue +49 -0
  151. package/scaffold/spec-site/src/pages/board/SprintKickoff.vue +389 -0
  152. package/scaffold/spec-site/src/pages/board/StatusBadge.vue +52 -0
  153. package/scaffold/spec-site/src/pages/board/StoryDetailPanel.vue +495 -0
  154. package/scaffold/spec-site/src/pages/board/TaskCard.vue +42 -0
  155. package/scaffold/spec-site/src/pages/retro/RetroCard.vue +36 -2
  156. package/scaffold/spec-site/src/pages/retro/RetroHeader.vue +82 -66
  157. package/scaffold/spec-site/src/pages/retro/RetroPage.vue +47 -18
  158. package/scaffold/spec-site/src/pages/standup/StandupEntryCard.vue +551 -0
  159. package/scaffold/spec-site/src/pages/standup/StandupForm.vue +68 -0
  160. package/scaffold/spec-site/src/pages/standup/StandupList.vue +71 -0
  161. package/scaffold/spec-site/src/pages/standup/StandupPage.vue +225 -0
  162. package/scaffold/spec-site/src/router.ts +141 -0
  163. package/scaffold/spec-site/src/styles/buttons.css +124 -0
  164. package/scaffold/spec-site/src/utils/parseMentions.ts +56 -0
  165. package/scaffold/spec-site/src/utils/timezone.ts +18 -0
@@ -0,0 +1,871 @@
1
+ import { Hono } from 'hono'
2
+ import type { AppEnv } from './types.js'
3
+ import { query, execute } from './db/adapter.js'
4
+
5
+ const mcp = new Hono<AppEnv>()
6
+
7
+ // ── Helpers ──
8
+
9
+
10
+ // Domain tool function imports
11
+ import {
12
+ toolDashboard, toolListTeamMembers,
13
+ toolListSprints, toolActivateSprint, toolCloseSprint, toolCheckinSprint,
14
+ toolAddAbsence, toolGetVelocity, toolKickoffSprint, toolSprintSummary,
15
+ toolListEpics, toolAddEpic, toolUpdateEpic, toolDeleteEpic,
16
+ toolListStories, toolListBacklog, toolAddStory, toolUpdateStory,
17
+ toolDeleteStory, toolAssignStory, toolUnassignStory,
18
+ toolListTasks, toolGetTask, toolUpdateTaskStatus, toolUpdateTask,
19
+ toolAddTask, toolDeleteTask,
20
+ toolSendMemo, toolListMemos, toolReadMemo, toolReplyMemo,
21
+ toolResolveMemo, toolRejectMemo,
22
+ toolCreateInitiative, toolListInitiatives, toolUpdateInitiativeStatus,
23
+ toolCheckNotifications, toolMarkNotificationRead, toolMarkAllNotificationsRead,
24
+ toolGetStandup, toolSaveStandup, toolListStandupEntries,
25
+ toolReviewStandup, toolGetStandupFeedback,
26
+ toolGetRetroSession, toolAddRetroItem, toolVoteRetroItem,
27
+ toolChangeRetroPhase, toolAddRetroAction, toolUpdateRetroActionStatus, toolExportRetro,
28
+ toolEmitEvent, toolPollEvents, toolAckEvent,
29
+ } from './mcp-tools/index.js'
30
+ import { text, err, checkRateLimit, type ToolResult } from './mcp-tools/utils.js'
31
+
32
+
33
+ const TOOLS = [
34
+ // ── Dashboard & Navigation ──
35
+ {
36
+ name: 'my_dashboard',
37
+ description: "Personal dashboard — task status, unread memos, notifications, today's standup",
38
+ inputSchema: { type: 'object', properties: {} },
39
+ },
40
+ {
41
+ name: 'list_sprints',
42
+ description: 'List all sprints (shows active sprint)',
43
+ inputSchema: { type: 'object', properties: {} },
44
+ },
45
+ {
46
+ name: 'activate_sprint',
47
+ description: 'Activate a sprint (deactivates others)',
48
+ inputSchema: {
49
+ type: 'object',
50
+ properties: { sprint_id: { type: 'string', description: 'Sprint ID (e.g. s55)' } },
51
+ required: ['sprint_id'],
52
+ },
53
+ },
54
+ {
55
+ name: 'kickoff_sprint',
56
+ description: 'Sprint kickoff — select stories from backlog + team + velocity check. Only available in planning state.',
57
+ inputSchema: {
58
+ type: 'object',
59
+ properties: {
60
+ sprint_id: { type: 'string', description: 'Sprint ID' },
61
+ story_ids: { type: 'array', items: { type: 'number' }, description: 'Story IDs selected from backlog' },
62
+ team_members: { type: 'array', items: { type: 'string' }, description: 'Participating team member names' },
63
+ velocity: { type: 'number', description: 'Target velocity (SP)' },
64
+ },
65
+ required: ['sprint_id', 'story_ids'],
66
+ },
67
+ },
68
+ {
69
+ name: 'close_sprint',
70
+ description: 'Close sprint — active→closed. Incomplete stories return to backlog + velocity saved + retro session created.',
71
+ inputSchema: {
72
+ type: 'object',
73
+ properties: {
74
+ sprint_id: { type: 'string', description: 'Sprint ID (e.g. s55)' },
75
+ },
76
+ required: ['sprint_id'],
77
+ },
78
+ },
79
+ {
80
+ name: 'create_initiative',
81
+ description: 'Create initiative — register proposals discovered in conversations',
82
+ inputSchema: {
83
+ type: 'object',
84
+ properties: {
85
+ title: { type: 'string', description: 'One-line summary (required)' },
86
+ content: { type: 'string', description: 'Details (what, why)' },
87
+ decider: { type: 'string', description: 'Decider name (nullable)' },
88
+ source_context: { type: 'string', description: 'Source context' },
89
+ },
90
+ required: ['title', 'content'],
91
+ },
92
+ },
93
+ {
94
+ name: 'list_initiatives',
95
+ description: 'List initiatives',
96
+ inputSchema: {
97
+ type: 'object',
98
+ properties: {
99
+ status: { type: 'string', enum: ['pending', 'approved', 'rejected', 'deferred'], description: 'Status filter' },
100
+ },
101
+ },
102
+ },
103
+ {
104
+ name: 'update_initiative_status',
105
+ description: 'Update initiative status (pending→approved/rejected/deferred)',
106
+ inputSchema: {
107
+ type: 'object',
108
+ properties: {
109
+ initiative_id: { type: 'number', description: 'Initiative ID' },
110
+ status: { type: 'string', enum: ['pending', 'approved', 'rejected', 'deferred'] },
111
+ decision_note: { type: 'string', description: 'Decision comment' },
112
+ },
113
+ required: ['initiative_id', 'status'],
114
+ },
115
+ },
116
+ {
117
+ name: 'get_velocity',
118
+ description: 'Velocity report — overall average + recent 3 sprint average',
119
+ inputSchema: { type: 'object', properties: {} },
120
+ },
121
+ {
122
+ name: 'sprint_summary',
123
+ description: 'Sprint status — progress by epic, workload by assignee, blockers',
124
+ inputSchema: {
125
+ type: 'object',
126
+ properties: {
127
+ sprint: { type: 'string', description: 'Sprint (default: active sprint)' },
128
+ },
129
+ },
130
+ },
131
+ {
132
+ name: 'list_team_members',
133
+ description: 'List team members (active users)',
134
+ inputSchema: { type: 'object', properties: {} },
135
+ },
136
+
137
+ // ── Epics ──
138
+ {
139
+ name: 'list_epics',
140
+ description: 'List epics with story counts',
141
+ inputSchema: { type: 'object', properties: {} },
142
+ },
143
+ {
144
+ name: 'add_epic',
145
+ description: 'Create new epic',
146
+ inputSchema: {
147
+ type: 'object',
148
+ properties: {
149
+ title: { type: 'string', description: 'Epic title' },
150
+ description: { type: 'string', description: 'Epic description' },
151
+ owner: { type: 'string', description: 'Epic owner (default: token user)' },
152
+ status: { type: 'string', enum: ['active', 'planned', 'completed', 'archived'], description: 'Status' },
153
+ },
154
+ required: ['title'],
155
+ },
156
+ },
157
+ {
158
+ name: 'update_epic',
159
+ description: 'Update epic',
160
+ inputSchema: {
161
+ type: 'object',
162
+ properties: {
163
+ epic_id: { type: 'number', description: 'Epic ID' },
164
+ title: { type: 'string', description: 'Title' },
165
+ description: { type: 'string', description: 'Description' },
166
+ status: { type: 'string', enum: ['active', 'planned', 'completed', 'archived'], description: 'Status' },
167
+ owner: { type: 'string', description: 'Owner' },
168
+ },
169
+ required: ['epic_id'],
170
+ },
171
+ },
172
+ {
173
+ name: 'delete_epic',
174
+ description: "Delete epic (stories' epic_id set to null)",
175
+ inputSchema: {
176
+ type: 'object',
177
+ properties: { epic_id: { type: 'number', description: 'Epic ID' } },
178
+ required: ['epic_id'],
179
+ },
180
+ },
181
+
182
+ // ── Stories ──
183
+ {
184
+ name: 'list_stories',
185
+ description: "List stories (filter by sprint/epic). sprint='backlog' shows unassigned stories",
186
+ inputSchema: {
187
+ type: 'object',
188
+ properties: {
189
+ sprint: { type: 'string', description: 'Sprint (default: active sprint)' },
190
+ epic_id: { type: 'number', description: 'Epic ID filter' },
191
+ status: { type: 'string', enum: ['draft', 'backlog', 'ready', 'in-progress', 'review', 'done'], description: 'Status' },
192
+ assignee: { type: 'string', description: 'Assignee filter' },
193
+ },
194
+ },
195
+ },
196
+ {
197
+ name: 'list_backlog',
198
+ description: 'List backlog stories — unassigned (sprint IS NULL) stories only',
199
+ inputSchema: {
200
+ type: 'object',
201
+ properties: {
202
+ epic_id: { type: 'number', description: 'Epic ID filter' },
203
+ status: { type: 'string', enum: ['draft', 'backlog', 'ready', 'in-progress', 'review', 'done'], description: 'Status' },
204
+ assignee: { type: 'string', description: 'Assignee filter' },
205
+ },
206
+ },
207
+ },
208
+ {
209
+ name: 'add_story',
210
+ description: 'Create new story (under epic). If sprint not specified, creates as backlog',
211
+ inputSchema: {
212
+ type: 'object',
213
+ properties: {
214
+ title: { type: 'string', description: 'Story title' },
215
+ epic_id: { type: 'number', description: 'Epic ID (optional, unassigned if omitted)' },
216
+ sprint: { type: 'string', description: 'Sprint. Enter "backlog" to move to backlog (sprint=NULL)' },
217
+ description: { type: 'string', description: 'Story description' },
218
+ acceptance_criteria: { type: 'string', description: 'Acceptance criteria' },
219
+ assignee: { type: 'string', description: 'Assignee (comma-separated for multiple)' },
220
+ priority: { type: 'string', enum: ['low', 'medium', 'high', 'critical'], description: 'Priority (default: medium)' },
221
+ area: { type: 'string', enum: ['FE', 'BE', 'Design', 'Data', 'Infra', 'PO'], description: 'Area (default: FE)' },
222
+ story_points: { type: 'number', description: 'Story points' },
223
+ start_date: { type: 'string', description: 'Start date (YYYY-MM-DD)' },
224
+ due_date: { type: 'string', description: 'Due date (YYYY-MM-DD)' },
225
+ },
226
+ required: ['title'],
227
+ },
228
+ },
229
+ {
230
+ name: 'update_story',
231
+ description: 'Update story',
232
+ inputSchema: {
233
+ type: 'object',
234
+ properties: {
235
+ story_id: { type: 'number', description: 'Story ID' },
236
+ title: { type: 'string', description: 'Title' },
237
+ description: { type: 'string', description: 'Description' },
238
+ acceptance_criteria: { type: 'string', description: 'Acceptance criteria' },
239
+ assignee: { type: 'string', description: 'Assignee (comma-separated for multiple)' },
240
+ status: { type: 'string', enum: ['draft', 'backlog', 'ready', 'in-progress', 'review', 'done'], description: 'Status' },
241
+ priority: { type: 'string', enum: ['low', 'medium', 'high', 'critical'], description: 'Priority' },
242
+ area: { type: 'string', enum: ['FE', 'BE', 'Design', 'Data', 'Infra', 'PO'], description: 'Area' },
243
+ story_points: { type: 'number', description: 'Story points' },
244
+ figma_url: { type: 'string', description: 'Figma URL' },
245
+ epic_id: { type: 'number', description: 'Epic ID' },
246
+ sprint: { type: 'string', description: 'Sprint. Enter "backlog" to move to backlog (sprint=NULL)' },
247
+ },
248
+ required: ['story_id'],
249
+ },
250
+ },
251
+ {
252
+ name: 'delete_story',
253
+ description: 'Delete story (tasks also deleted)',
254
+ inputSchema: {
255
+ type: 'object',
256
+ properties: { story_id: { type: 'number', description: 'Story ID' } },
257
+ required: ['story_id'],
258
+ },
259
+ },
260
+
261
+ // ── Tasks ──
262
+ {
263
+ name: 'list_my_tasks',
264
+ description: 'My task list (epic > story > task tree)',
265
+ inputSchema: {
266
+ type: 'object',
267
+ properties: {
268
+ status: { type: 'string', enum: ['todo', 'in-progress', 'done'], description: 'Filter by status' },
269
+ sprint: { type: 'string', description: 'Sprint (default: active sprint)' },
270
+ },
271
+ },
272
+ },
273
+ {
274
+ name: 'get_task',
275
+ description: 'Task detail + parent story context + sibling tasks',
276
+ inputSchema: {
277
+ type: 'object',
278
+ properties: { task_id: { type: 'number', description: 'Task ID' } },
279
+ required: ['task_id'],
280
+ },
281
+ },
282
+ {
283
+ name: 'update_task_status',
284
+ description: 'Change task status',
285
+ inputSchema: {
286
+ type: 'object',
287
+ properties: {
288
+ task_id: { type: 'number', description: 'Task ID' },
289
+ status: { type: 'string', enum: ['todo', 'in-progress', 'done'], description: 'New status' },
290
+ },
291
+ required: ['task_id', 'status'],
292
+ },
293
+ },
294
+ {
295
+ name: 'update_task',
296
+ description: 'Update task (title, assignee, status, description)',
297
+ inputSchema: {
298
+ type: 'object',
299
+ properties: {
300
+ task_id: { type: 'number', description: 'Task ID' },
301
+ title: { type: 'string', description: 'Title' },
302
+ assignee: { type: 'string', description: 'Assignee' },
303
+ status: { type: 'string', enum: ['todo', 'in-progress', 'done'], description: 'Status' },
304
+ description: { type: 'string', description: 'Description' },
305
+ },
306
+ required: ['task_id'],
307
+ },
308
+ },
309
+ {
310
+ name: 'add_task',
311
+ description: 'Add new task (under story)',
312
+ inputSchema: {
313
+ type: 'object',
314
+ properties: {
315
+ story_id: { type: 'number', description: 'Story ID' },
316
+ title: { type: 'string', description: 'Task title' },
317
+ assignee: { type: 'string', description: 'Assignee (default: token user)' },
318
+ description: { type: 'string', description: 'Task description' },
319
+ story_points: { type: 'number', description: 'Story points (per task)' },
320
+ },
321
+ required: ['story_id', 'title'],
322
+ },
323
+ },
324
+ {
325
+ name: 'delete_task',
326
+ description: 'Delete task',
327
+ inputSchema: {
328
+ type: 'object',
329
+ properties: { task_id: { type: 'number', description: 'Task ID' } },
330
+ required: ['task_id'],
331
+ },
332
+ },
333
+
334
+ // ── Standup ──
335
+ {
336
+ name: 'get_standup',
337
+ description: 'Get standup',
338
+ inputSchema: {
339
+ type: 'object',
340
+ properties: {
341
+ date: { type: 'string', description: 'Date (YYYY-MM-DD, default: today)' },
342
+ sprint: { type: 'string', description: 'Sprint (default: active sprint)' },
343
+ },
344
+ },
345
+ },
346
+ {
347
+ name: 'save_standup',
348
+ description: 'Save standup (UPSERT)',
349
+ inputSchema: {
350
+ type: 'object',
351
+ properties: {
352
+ done_text: { type: 'string', description: 'Completed work' },
353
+ plan_text: { type: 'string', description: "Today's plan" },
354
+ plan_story_ids: { type: 'array', items: { type: 'number' }, description: 'Planned story ID array' },
355
+ blockers: { type: 'string', description: 'Blockers' },
356
+ date: { type: 'string', description: 'Date (YYYY-MM-DD, default: today)' },
357
+ sprint: { type: 'string', description: 'Sprint (default: active sprint)' },
358
+ },
359
+ },
360
+ },
361
+ {
362
+ name: 'list_standup_entries',
363
+ description: 'List standup entries (filter by sprint/date)',
364
+ inputSchema: {
365
+ type: 'object',
366
+ properties: {
367
+ sprint: { type: 'string', description: 'Sprint (default: active sprint)' },
368
+ date: { type: 'string', description: 'Date (YYYY-MM-DD)' },
369
+ },
370
+ },
371
+ },
372
+
373
+ {
374
+ name: 'review_standup',
375
+ description: 'Write standup feedback (comment/approve/request_changes)',
376
+ inputSchema: {
377
+ type: 'object',
378
+ properties: {
379
+ standup_entry_id: { type: 'number', description: 'Target standup entry ID' },
380
+ sprint: { type: 'string', description: 'Sprint ID' },
381
+ target_user: { type: 'string', description: 'Standup author name' },
382
+ feedback_text: { type: 'string', description: 'Feedback content' },
383
+ review_type: { type: 'string', enum: ['comment', 'approve', 'request_changes'], description: 'Review type (default: comment)' },
384
+ },
385
+ required: ['standup_entry_id', 'sprint', 'target_user', 'feedback_text'],
386
+ },
387
+ },
388
+ {
389
+ name: 'get_standup_feedback',
390
+ description: 'Get standup feedback',
391
+ inputSchema: {
392
+ type: 'object',
393
+ properties: {
394
+ standup_entry_id: { type: 'number', description: 'Standup entry ID' },
395
+ sprint: { type: 'string', description: 'Sprint (used with user in get_standup_feedback)' },
396
+ user: { type: 'string', description: 'Target user (used with sprint)' },
397
+ },
398
+ },
399
+ },
400
+
401
+ // ── Memos ──
402
+ {
403
+ name: 'send_memo',
404
+ description: 'Send memo to team member. memo_type=decision requires title. review_required=true shows in dashboard approval queue.',
405
+ inputSchema: {
406
+ type: 'object',
407
+ properties: {
408
+ to_user: { type: 'string', description: 'Recipient name (comma-separated for multiple)' },
409
+ content: { type: 'string', description: 'Memo content' },
410
+ page_id: { type: 'string', description: 'Page ID (default: home)' },
411
+ memo_type: { type: 'string', enum: ['memo', 'question', 'blocker', 'task', 'decision', 'feature_request', 'policy_request'], description: 'Memo type (default: memo)' },
412
+ title: { type: 'string', description: 'Title — required for decision type' },
413
+ supersedes_id: { type: 'number', description: 'Previous memo ID this supersedes' },
414
+ review_required: { type: 'boolean', description: 'Review required (default: false)' },
415
+ },
416
+ required: ['to_user', 'content'],
417
+ },
418
+ },
419
+ {
420
+ name: 'list_my_memos',
421
+ description: 'My received memos',
422
+ inputSchema: {
423
+ type: 'object',
424
+ properties: {
425
+ unread_only: { type: 'boolean', description: 'Unread only (default: false)' },
426
+ sprint: { type: 'string', description: 'Sprint (default: active sprint)' },
427
+ },
428
+ },
429
+ },
430
+ {
431
+ name: 'read_memo',
432
+ description: 'Read memo detail + mark as read',
433
+ inputSchema: {
434
+ type: 'object',
435
+ properties: { memo_id: { type: 'number', description: 'Memo ID' } },
436
+ required: ['memo_id'],
437
+ },
438
+ },
439
+ {
440
+ name: 'reply_memo',
441
+ description: 'Reply to memo (with review type)',
442
+ inputSchema: {
443
+ type: 'object',
444
+ properties: {
445
+ memo_id: { type: 'number', description: 'Memo ID' },
446
+ content: { type: 'string', description: 'Reply content' },
447
+ review_type: { type: 'string', enum: ['comment', 'approve', 'request_changes'], description: 'Review type (default: comment)' },
448
+ },
449
+ required: ['memo_id', 'content'],
450
+ },
451
+ },
452
+ {
453
+ name: 'resolve_memo',
454
+ description: 'Resolve memo',
455
+ inputSchema: {
456
+ type: 'object',
457
+ properties: { memo_id: { type: 'number', description: 'Memo ID' } },
458
+ required: ['memo_id'],
459
+ },
460
+ },
461
+
462
+ // ── Retro ──
463
+ {
464
+ name: 'get_retro_session',
465
+ description: 'Get retro session (items + actions)',
466
+ inputSchema: {
467
+ type: 'object',
468
+ properties: {
469
+ sprint: { type: 'string', description: 'Sprint (default: active sprint)' },
470
+ },
471
+ },
472
+ },
473
+ {
474
+ name: 'add_retro_item',
475
+ description: 'Add retro item (keep/problem/try)',
476
+ inputSchema: {
477
+ type: 'object',
478
+ properties: {
479
+ session_id: { type: 'number', description: 'Session ID' },
480
+ category: { type: 'string', enum: ['keep', 'problem', 'try'], description: 'Category' },
481
+ content: { type: 'string', description: 'Content' },
482
+ },
483
+ required: ['session_id', 'category', 'content'],
484
+ },
485
+ },
486
+ {
487
+ name: 'vote_retro_item',
488
+ description: 'Vote/unvote retro item',
489
+ inputSchema: {
490
+ type: 'object',
491
+ properties: {
492
+ item_id: { type: 'number', description: 'Item ID' },
493
+ },
494
+ required: ['item_id'],
495
+ },
496
+ },
497
+ {
498
+ name: 'change_retro_phase',
499
+ description: 'Change retro session phase',
500
+ inputSchema: {
501
+ type: 'object',
502
+ properties: {
503
+ session_id: { type: 'number', description: 'Session ID' },
504
+ phase: { type: 'string', enum: ['collect', 'vote', 'discuss', 'action', 'done'], description: 'Phase' },
505
+ },
506
+ required: ['session_id', 'phase'],
507
+ },
508
+ },
509
+ {
510
+ name: 'add_retro_action',
511
+ description: 'Add retro action item',
512
+ inputSchema: {
513
+ type: 'object',
514
+ properties: {
515
+ session_id: { type: 'number', description: 'Session ID' },
516
+ content: { type: 'string', description: 'Action content' },
517
+ assignee: { type: 'string', description: 'Assignee' },
518
+ },
519
+ required: ['session_id', 'content'],
520
+ },
521
+ },
522
+ {
523
+ name: 'update_retro_action_status',
524
+ description: 'Update retro action status',
525
+ inputSchema: {
526
+ type: 'object',
527
+ properties: {
528
+ action_id: { type: 'number', description: 'Action ID' },
529
+ status: { type: 'string', enum: ['todo', 'in-progress', 'done'], description: 'Status' },
530
+ },
531
+ required: ['action_id', 'status'],
532
+ },
533
+ },
534
+ {
535
+ name: 'export_retro',
536
+ description: 'Export full retro summary (keep/problem/try + actions + votes)',
537
+ inputSchema: {
538
+ type: 'object',
539
+ properties: {
540
+ sprint: { type: 'string', description: 'Sprint (default: active sprint)' },
541
+ },
542
+ },
543
+ },
544
+
545
+ // ── Notifications ──
546
+ {
547
+ name: 'check_notifications',
548
+ description: 'Check my notifications — unread first',
549
+ inputSchema: {
550
+ type: 'object',
551
+ properties: {
552
+ unread_only: { type: 'boolean', description: 'Unread only (default: false)' },
553
+ },
554
+ },
555
+ },
556
+ {
557
+ name: 'mark_notification_read',
558
+ description: 'Mark notification as read',
559
+ inputSchema: {
560
+ type: 'object',
561
+ properties: { notification_id: { type: 'number', description: 'Notification ID' } },
562
+ required: ['notification_id'],
563
+ },
564
+ },
565
+ {
566
+ name: 'mark_all_notifications_read',
567
+ description: 'Mark all notifications as read',
568
+ inputSchema: { type: 'object', properties: {} },
569
+ },
570
+
571
+ // ── Agent Events (Push-Native) ──
572
+ {
573
+ name: 'emit_event',
574
+ description: 'Emit agent event — push notification to target agent/user',
575
+ inputSchema: {
576
+ type: 'object',
577
+ properties: {
578
+ event_type: {
579
+ type: 'string',
580
+ enum: ['memo_assigned', 'memo_replied', 'memo_resolved', 'review_requested', 'task_status_changed', 'decision_needed', 'sprint_alert'],
581
+ description: 'Event type',
582
+ },
583
+ target_agent: { type: 'string', description: 'Target agent (e.g. Oscar, Penny)' },
584
+ target_user: { type: 'string', description: 'Target user name' },
585
+ payload: { type: 'string', description: 'Event payload (JSON string)' },
586
+ ttl_hours: { type: 'number', description: 'TTL hours (default: 24)' },
587
+ source_agent: { type: 'string', description: 'Source agent name (default: calling user)' },
588
+ },
589
+ required: ['event_type', 'target_agent', 'target_user', 'payload'],
590
+ },
591
+ },
592
+ {
593
+ name: 'poll_events',
594
+ description: 'Poll pending events (SSE fallback for unsupported clients)',
595
+ inputSchema: {
596
+ type: 'object',
597
+ properties: {
598
+ event_type: { type: 'string', description: 'Event type filter (optional)' },
599
+ limit: { type: 'number', description: 'Max results (default: 20)' },
600
+ },
601
+ },
602
+ },
603
+ {
604
+ name: 'ack_event',
605
+ description: 'Acknowledge event',
606
+ inputSchema: {
607
+ type: 'object',
608
+ properties: {
609
+ event_id: { type: 'number', description: 'Event ID' },
610
+ },
611
+ required: ['event_id'],
612
+ },
613
+ },
614
+ ]
615
+
616
+ // ── Tool Handlers ──
617
+
618
+
619
+ async function handleTool(name: string, args: Record<string, unknown>, user: string): Promise<ToolResult> {
620
+ switch (name) {
621
+ // Dashboard & Navigation
622
+ case 'my_dashboard': return toolDashboard(user)
623
+ case 'list_sprints': return toolListSprints()
624
+ case 'activate_sprint': return toolActivateSprint(args)
625
+ case 'kickoff_sprint': return toolKickoffSprint(args)
626
+ case 'close_sprint': return toolCloseSprint(args)
627
+ case 'get_velocity': return toolGetVelocity()
628
+ case 'create_initiative': return toolCreateInitiative(user, args)
629
+ case 'list_initiatives': return toolListInitiatives(args)
630
+ case 'update_initiative_status': return toolUpdateInitiativeStatus(args)
631
+ case 'assign_story_to_sprint': return toolAssignStory(args)
632
+ case 'unassign_story_from_sprint': return toolUnassignStory(args)
633
+ case 'checkin_sprint': return toolCheckinSprint(args)
634
+ case 'add_absence': return toolAddAbsence(args)
635
+ case 'reject_memo': return toolRejectMemo(args)
636
+ case 'sprint_summary': return toolSprintSummary(args)
637
+ case 'list_team_members': return toolListTeamMembers()
638
+
639
+ // Epics
640
+ case 'list_epics': return toolListEpics()
641
+ case 'add_epic': return toolAddEpic(user, args)
642
+ case 'update_epic': return toolUpdateEpic(args)
643
+ case 'delete_epic': return toolDeleteEpic(args)
644
+
645
+ // Stories
646
+ case 'list_stories': return toolListStories(args)
647
+ case 'list_backlog': return toolListBacklog(args)
648
+ case 'add_story': return toolAddStory(user, args)
649
+ case 'update_story': return toolUpdateStory(user, args)
650
+ case 'delete_story': return toolDeleteStory(args)
651
+
652
+ // Tasks
653
+ case 'list_my_tasks': return toolListTasks(user, args)
654
+ case 'get_task': return toolGetTask(args)
655
+ case 'update_task_status': return toolUpdateTaskStatus(args)
656
+ case 'update_task': return toolUpdateTask(user, args)
657
+ case 'add_task': return toolAddTask(user, args)
658
+ case 'delete_task': return toolDeleteTask(args)
659
+
660
+ // Standup
661
+ case 'get_standup': return toolGetStandup(user, args)
662
+ case 'save_standup': return toolSaveStandup(user, args)
663
+ case 'list_standup_entries': return toolListStandupEntries(args)
664
+ case 'review_standup': return toolReviewStandup(user, args)
665
+ case 'get_standup_feedback': return toolGetStandupFeedback(args)
666
+
667
+ // Memos
668
+ case 'send_memo': return toolSendMemo(user, args)
669
+ case 'list_my_memos': return toolListMemos(user, args)
670
+ case 'read_memo': return toolReadMemo(args)
671
+ case 'reply_memo': return toolReplyMemo(user, args)
672
+ case 'resolve_memo': return toolResolveMemo(user, args)
673
+
674
+ // Retro
675
+ case 'get_retro_session': return toolGetRetroSession(user, args)
676
+ case 'add_retro_item': return toolAddRetroItem(user, args)
677
+ case 'vote_retro_item': return toolVoteRetroItem(user, args)
678
+ case 'change_retro_phase': return toolChangeRetroPhase(args)
679
+ case 'add_retro_action': return toolAddRetroAction(user, args)
680
+ case 'update_retro_action_status': return toolUpdateRetroActionStatus(args)
681
+ case 'export_retro': return toolExportRetro(args)
682
+
683
+ // Notifications
684
+ case 'check_notifications': return toolCheckNotifications(user, args)
685
+ case 'mark_notification_read': return toolMarkNotificationRead(args)
686
+ case 'mark_all_notifications_read': return toolMarkAllNotificationsRead(user)
687
+
688
+ // Agent Events
689
+ case 'emit_event': return toolEmitEvent(user, args)
690
+ case 'poll_events': return toolPollEvents(user, args)
691
+ case 'ack_event': return toolAckEvent(user, args)
692
+
693
+ default: return err(`Unknown tool: ${name}`)
694
+ }
695
+ }
696
+
697
+ // ── JSON-RPC Handler ──
698
+
699
+ interface JsonRpcRequest {
700
+ jsonrpc: '2.0'
701
+ method: string
702
+ params?: Record<string, unknown>
703
+ id?: string | number
704
+ }
705
+
706
+ function jsonrpcOk(id: string | number | undefined, result: unknown) {
707
+ return { jsonrpc: '2.0', result, id }
708
+ }
709
+
710
+ function jsonrpcError(id: string | number | undefined, code: number, message: string) {
711
+ return { jsonrpc: '2.0', error: { code, message }, id }
712
+ }
713
+
714
+ mcp.post('/', async (c) => {
715
+ const user = c.get('userName')
716
+ const body = await c.req.json() as JsonRpcRequest | JsonRpcRequest[]
717
+ const requests = Array.isArray(body) ? body : [body]
718
+ const responses: unknown[] = []
719
+
720
+ for (const req of requests) {
721
+ if (!req.method) {
722
+ responses.push(jsonrpcError(req.id, -32600, 'Invalid request'))
723
+ continue
724
+ }
725
+
726
+ switch (req.method) {
727
+ case 'initialize':
728
+ responses.push(jsonrpcOk(req.id, {
729
+ protocolVersion: '2025-03-26',
730
+ capabilities: { tools: {} },
731
+ serverInfo: { name: 'pm-api', version: '2.0.0' },
732
+ }))
733
+ break
734
+
735
+ case 'notifications/initialized':
736
+ break
737
+
738
+ case 'ping':
739
+ responses.push(jsonrpcOk(req.id, {}))
740
+ break
741
+
742
+ case 'tools/list':
743
+ responses.push(jsonrpcOk(req.id, { tools: TOOLS }))
744
+ break
745
+
746
+ case 'tools/call': {
747
+ const params = req.params as { name: string; arguments?: Record<string, unknown> } | undefined
748
+ if (!params?.name) {
749
+ responses.push(jsonrpcError(req.id, -32602, 'Missing tool name'))
750
+ break
751
+ }
752
+ try {
753
+ const result = await handleTool(params.name, params.arguments ?? {}, user)
754
+ responses.push(jsonrpcOk(req.id, result))
755
+ } catch (e: unknown) {
756
+ const msg = e instanceof Error ? e.message : 'Unknown error'
757
+ responses.push(jsonrpcOk(req.id, { content: [{ type: 'text', text: `Error: ${msg}` }], isError: true }))
758
+ }
759
+ break
760
+ }
761
+
762
+ default:
763
+ if (req.id !== undefined) {
764
+ responses.push(jsonrpcError(req.id, -32601, `Method not found: ${req.method}`))
765
+ }
766
+ }
767
+ }
768
+
769
+ if (responses.length === 0) return new Response(null, { status: 202 })
770
+ if (!Array.isArray(body) && responses.length === 1) {
771
+ return c.json(responses[0])
772
+ }
773
+ return c.json(responses)
774
+ })
775
+
776
+ mcp.get('/', async (c) => {
777
+ const user = c.get('userName')
778
+ const lastEventId = c.req.header('Last-Event-ID')
779
+
780
+ // SSE implementation via ReadableStream
781
+ const encoder = new TextEncoder()
782
+ let cancelled = false
783
+
784
+ const stream = new ReadableStream({
785
+ async start(controller) {
786
+ const send = (id: string, event: string, data: string) => {
787
+ try {
788
+ controller.enqueue(encoder.encode(`id: ${id}\nevent: ${event}\ndata: ${data}\n\n`))
789
+ } catch { cancelled = true }
790
+ }
791
+
792
+ // Initial connection confirmation
793
+ send('0', 'connected', JSON.stringify({ user, timestamp: new Date().toISOString() }))
794
+
795
+ // Poll every 5s for up to 25s (5s buffer for CF Worker 30s limit)
796
+ const MAX_DURATION = 25_000
797
+ const POLL_INTERVAL = 5_000
798
+ const startTime = Date.now()
799
+
800
+ let minId = 0
801
+ if (lastEventId) {
802
+ const parsed = parseInt(lastEventId, 10)
803
+ if (!isNaN(parsed)) minId = parsed
804
+ }
805
+
806
+ while (!cancelled && (Date.now() - startTime) < MAX_DURATION) {
807
+ // Query pending events (only those with id > minId)
808
+ const result = await query<{
809
+ id: number; event_type: string; source_agent: string
810
+ target_agent: string; payload: string; created_at: string
811
+ }>(
812
+ `SELECT id, event_type, source_agent, target_agent, payload, created_at
813
+ FROM agent_events
814
+ WHERE target_user = ? AND status = 'pending' AND id > ?
815
+ AND (expires_at IS NULL OR expires_at > datetime('now'))
816
+ ORDER BY id ASC LIMIT 10`,
817
+ [user, minId],
818
+ )
819
+
820
+ if (!result.error && result.rows.length > 0) {
821
+ for (const evt of result.rows) {
822
+ let parsedPayload: unknown
823
+ try { parsedPayload = JSON.parse(evt.payload) } catch { parsedPayload = evt.payload }
824
+ send(
825
+ String(evt.id),
826
+ evt.event_type,
827
+ JSON.stringify({
828
+ id: evt.id,
829
+ source_agent: evt.source_agent,
830
+ target_agent: evt.target_agent,
831
+ payload: parsedPayload,
832
+ created_at: evt.created_at,
833
+ }),
834
+ )
835
+ minId = evt.id
836
+
837
+ // Update status to delivered
838
+ await execute(
839
+ `UPDATE agent_events SET status = 'delivered', delivered_at = datetime('now') WHERE id = ? AND status = 'pending'`,
840
+ [evt.id],
841
+ )
842
+ }
843
+ }
844
+
845
+ // Heartbeat (keep connection alive)
846
+ send('0', 'heartbeat', JSON.stringify({ t: new Date().toISOString() }))
847
+
848
+ // Wait until next poll
849
+ await new Promise(resolve => setTimeout(resolve, POLL_INTERVAL))
850
+ }
851
+
852
+ // Close stream after 25s — client reconnects with Last-Event-ID
853
+ controller.close()
854
+ },
855
+ cancel() {
856
+ cancelled = true
857
+ },
858
+ })
859
+
860
+ return new Response(stream, {
861
+ headers: {
862
+ 'Content-Type': 'text/event-stream',
863
+ 'Cache-Control': 'no-cache',
864
+ 'Connection': 'keep-alive',
865
+ 'X-Accel-Buffering': 'no',
866
+ },
867
+ })
868
+ })
869
+ mcp.delete('/', (c) => c.json({ error: 'No sessions to terminate' }, 405))
870
+
871
+ export default mcp