popilot 0.7.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 (56) hide show
  1. package/package.json +1 -1
  2. package/scaffold/mcp-notification-server/package.json +18 -0
  3. package/scaffold/mcp-notification-server/src/index.ts +275 -0
  4. package/scaffold/mcp-notification-server/src/turso-client.ts +142 -0
  5. package/scaffold/mcp-notification-server/tsconfig.json +14 -0
  6. package/scaffold/pm-api/sql/001-memo-v2.sql +49 -0
  7. package/scaffold/pm-api/sql/002-notifications.sql +18 -0
  8. package/scaffold/pm-api/sql/003-content.sql +66 -0
  9. package/scaffold/pm-api/sql/004-agent-events.sql +21 -0
  10. package/scaffold/pm-api/sql/005-epic-sprint-decoupling.sql +6 -0
  11. package/scaffold/pm-api/src/utils/retro-link.ts +32 -0
  12. package/scaffold/spec-site/package-lock.json +852 -0
  13. package/scaffold/spec-site/package.json +12 -1
  14. package/scaffold/spec-site/src/components/AuthGate.vue +117 -0
  15. package/scaffold/spec-site/src/components/BurndownChart.vue +78 -0
  16. package/scaffold/spec-site/src/components/DocComments.vue +137 -0
  17. package/scaffold/spec-site/src/components/DocEditor.vue +118 -0
  18. package/scaffold/spec-site/src/components/DocExportBar.vue +110 -0
  19. package/scaffold/spec-site/src/components/DocsSidebar.vue +309 -0
  20. package/scaffold/spec-site/src/components/EmptyState.vue +30 -0
  21. package/scaffold/spec-site/src/components/ErrorBanner.vue +38 -0
  22. package/scaffold/spec-site/src/components/Icon.vue +58 -0
  23. package/scaffold/spec-site/src/components/MemoChecklist.vue +88 -0
  24. package/scaffold/spec-site/src/components/MemoGraph.vue +75 -0
  25. package/scaffold/spec-site/src/components/MemoItem.vue +353 -0
  26. package/scaffold/spec-site/src/components/MemoRelations.vue +101 -0
  27. package/scaffold/spec-site/src/components/MemoTimeline.vue +53 -0
  28. package/scaffold/spec-site/src/components/MentionInput.vue +174 -0
  29. package/scaffold/spec-site/src/components/PriorityBadge.vue +23 -0
  30. package/scaffold/spec-site/src/components/SlashCommand.ts +123 -0
  31. package/scaffold/spec-site/src/components/StateDisplay.vue +54 -0
  32. package/scaffold/spec-site/src/components/TreeNode.vue +82 -0
  33. package/scaffold/spec-site/src/components/UserAvatar.vue +24 -0
  34. package/scaffold/spec-site/src/composables/navTypes.ts +3 -0
  35. package/scaffold/spec-site/src/composables/useBottomSheet.ts +103 -0
  36. package/scaffold/spec-site/src/composables/useMemo.ts +39 -0
  37. package/scaffold/spec-site/src/composables/useTurso.ts +17 -0
  38. package/scaffold/spec-site/src/composables/useViewport.ts +26 -0
  39. package/scaffold/spec-site/src/mockup/ComponentPalette.vue +61 -0
  40. package/scaffold/spec-site/src/mockup/MockupCanvas.vue +459 -0
  41. package/scaffold/spec-site/src/mockup/PropertyPanel.vue +217 -0
  42. package/scaffold/spec-site/src/mockup/componentCatalog.ts +68 -0
  43. package/scaffold/spec-site/src/mockup/useScenarios.ts +67 -0
  44. package/scaffold/spec-site/src/pages/DocsEditor.vue +119 -0
  45. package/scaffold/spec-site/src/pages/DocsPage.vue +444 -0
  46. package/scaffold/spec-site/src/pages/MemosPage.vue +857 -0
  47. package/scaffold/spec-site/src/pages/MockupEditorPage.vue +611 -0
  48. package/scaffold/spec-site/src/pages/MockupListPage.vue +121 -0
  49. package/scaffold/spec-site/src/pages/MockupViewerPage.vue +199 -0
  50. package/scaffold/spec-site/src/pages/NotificationSettingsPage.vue +59 -0
  51. package/scaffold/spec-site/src/pages/SprintAdmin.vue +521 -0
  52. package/scaffold/spec-site/src/pages/SprintTimeline.vue +159 -0
  53. package/scaffold/spec-site/src/pages/board/KanbanBoard.vue +93 -0
  54. package/scaffold/spec-site/src/styles/buttons.css +124 -0
  55. package/scaffold/spec-site/src/utils/parseMentions.ts +56 -0
  56. package/scaffold/spec-site/src/utils/timezone.ts +18 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "popilot",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Multi-agent PO/PM system scaffold for Claude Code",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "mcp-notification-server",
3
+ "version": "1.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "scripts": {
8
+ "build": "tsc",
9
+ "dev": "tsc --watch"
10
+ },
11
+ "dependencies": {
12
+ "@modelcontextprotocol/sdk": "^1.12.1"
13
+ },
14
+ "devDependencies": {
15
+ "typescript": "^5.7.0",
16
+ "@types/node": "^22.0.0"
17
+ }
18
+ }
@@ -0,0 +1,275 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
2
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
3
+ import { z } from 'zod'
4
+ import { query, execute } from './turso-client.js'
5
+
6
+ const server = new McpServer({
7
+ name: 'spec-notifications',
8
+ version: '1.0.0',
9
+ })
10
+
11
+ // Tool 1: Check unread notifications
12
+ server.tool(
13
+ 'check_notifications',
14
+ 'Check unread notifications for a user (max 20)',
15
+ { user_name: z.string().describe('User name to check notifications for') },
16
+ async ({ user_name }) => {
17
+ const result = await query<{
18
+ id: number; type: string; title: string; body: string | null
19
+ source_type: string; source_id: number; page_id: string
20
+ actor: string; is_read: number; created_at: string
21
+ }>(
22
+ `SELECT id, type, title, body, source_type, source_id, page_id, actor, is_read, created_at
23
+ FROM notifications
24
+ WHERE user_name = ? AND is_read = 0
25
+ ORDER BY created_at DESC
26
+ LIMIT 20`,
27
+ [user_name],
28
+ )
29
+
30
+ if (result.error) {
31
+ return { content: [{ type: 'text' as const, text: `Error: ${result.error}` }] }
32
+ }
33
+
34
+ if (result.rows.length === 0) {
35
+ return { content: [{ type: 'text' as const, text: `No unread notifications for ${user_name}.` }] }
36
+ }
37
+
38
+ const lines = result.rows.map((n, i) => {
39
+ const icon = n.type === 'memo_assigned' ? '📩' : n.type === 'reply_received' ? '💬' : '📢'
40
+ return `${i + 1}. ${icon} [ID:${n.id}] ${n.title}\n ${n.body ?? ''}\n Page: ${n.page_id} | ${n.created_at}`
41
+ })
42
+
43
+ return {
44
+ content: [{
45
+ type: 'text' as const,
46
+ text: `Unread notifications for ${user_name} (${result.rows.length}):\n\n${lines.join('\n\n')}`,
47
+ }],
48
+ }
49
+ },
50
+ )
51
+
52
+ // Tool 2: Mark notification as read
53
+ server.tool(
54
+ 'mark_notification_read',
55
+ 'Mark a notification as read by ID',
56
+ { notification_id: z.number().describe('Notification ID to mark as read') },
57
+ async ({ notification_id }) => {
58
+ const result = await execute(
59
+ 'UPDATE notifications SET is_read = 1 WHERE id = ?',
60
+ [notification_id],
61
+ )
62
+
63
+ if (result.error) {
64
+ return { content: [{ type: 'text' as const, text: `Error: ${result.error}` }] }
65
+ }
66
+
67
+ return {
68
+ content: [{
69
+ type: 'text' as const,
70
+ text: result.rowsAffected > 0
71
+ ? `Notification #${notification_id} marked as read.`
72
+ : `Notification #${notification_id} not found.`,
73
+ }],
74
+ }
75
+ },
76
+ )
77
+
78
+ // Tool 3: Check open memos assigned to user
79
+ server.tool(
80
+ 'check_open_memos',
81
+ 'Check all open memos assigned to a user',
82
+ { user_name: z.string().describe('User name to check memos for') },
83
+ async ({ user_name }) => {
84
+ const result = await query<{
85
+ id: number; page_id: string; content: string; memo_type: string
86
+ created_by: string; assigned_to: string | null; created_at: string
87
+ }>(
88
+ `SELECT id, page_id, content, memo_type, created_by, assigned_to, created_at
89
+ FROM memos_v2
90
+ WHERE assigned_to = ? AND status = 'open'
91
+ ORDER BY created_at DESC`,
92
+ [user_name],
93
+ )
94
+
95
+ if (result.error) {
96
+ return { content: [{ type: 'text' as const, text: `Error: ${result.error}` }] }
97
+ }
98
+
99
+ if (result.rows.length === 0) {
100
+ return { content: [{ type: 'text' as const, text: `No open memos assigned to ${user_name}.` }] }
101
+ }
102
+
103
+ const typeIcons: Record<string, string> = {
104
+ memo: '📝', decision: '⚡', request: '📋', backlog: '💡',
105
+ }
106
+
107
+ const lines = result.rows.map((m, i) => {
108
+ const icon = typeIcons[m.memo_type] ?? '📝'
109
+ const preview = m.content.length > 80 ? m.content.slice(0, 80) + '…' : m.content
110
+ return `${i + 1}. ${icon} [ID:${m.id}] ${m.created_by} → ${m.assigned_to}\n ${preview}\n Page: ${m.page_id} | ${m.created_at}`
111
+ })
112
+
113
+ return {
114
+ content: [{
115
+ type: 'text' as const,
116
+ text: `Open memos for ${user_name} (${result.rows.length}):\n\n${lines.join('\n\n')}`,
117
+ }],
118
+ }
119
+ },
120
+ )
121
+
122
+ // Tool 4: Create notification
123
+ server.tool(
124
+ 'create_notification',
125
+ 'Create a notification for a user (memo events: assignment, reply, resolve, reopen)',
126
+ {
127
+ user_name: z.string().describe('Notification recipient'),
128
+ type: z.enum(['memo_assigned', 'memo_mention_all', 'reply_received', 'memo_resolved', 'memo_reopened'])
129
+ .describe('Notification type'),
130
+ title: z.string().describe('Notification title'),
131
+ body: z.string().optional().describe('Preview body (60 chars max)'),
132
+ source_id: z.number().describe('Related memo ID'),
133
+ page_id: z.string().describe('Page ID the memo belongs to'),
134
+ actor: z.string().describe('Name of the person who performed the action'),
135
+ },
136
+ async ({ user_name, type, title, body, source_id, page_id, actor }) => {
137
+ const result = await execute(
138
+ `INSERT INTO notifications (user_name, type, title, body, source_type, source_id, page_id, actor)
139
+ VALUES (?, ?, ?, ?, 'memo', ?, ?, ?)`,
140
+ [user_name, type, title, body ?? null, source_id, page_id, actor],
141
+ )
142
+
143
+ if (result.error) {
144
+ return { content: [{ type: 'text' as const, text: `Error: ${result.error}` }] }
145
+ }
146
+
147
+ return {
148
+ content: [{
149
+ type: 'text' as const,
150
+ text: `Notification created: [${type}] "${title}" for ${user_name}`,
151
+ }],
152
+ }
153
+ },
154
+ )
155
+
156
+ // Tool 5: Reply to memo
157
+ server.tool(
158
+ 'reply_memo',
159
+ 'Reply to a memo and notify the memo author',
160
+ {
161
+ memo_id: z.number().describe('Memo ID to reply to'),
162
+ content: z.string().describe('Reply content'),
163
+ created_by: z.string().describe('Reply author name'),
164
+ },
165
+ async ({ memo_id, content, created_by }) => {
166
+ const memo = await query<{
167
+ id: number; page_id: string; created_by: string; assigned_to: string | null; status: string
168
+ }>(
169
+ 'SELECT id, page_id, created_by, assigned_to, status FROM memos_v2 WHERE id = ?',
170
+ [memo_id],
171
+ )
172
+
173
+ if (memo.error) {
174
+ return { content: [{ type: 'text' as const, text: `Error: ${memo.error}` }] }
175
+ }
176
+ if (memo.rows.length === 0) {
177
+ return { content: [{ type: 'text' as const, text: `Memo #${memo_id} not found.` }] }
178
+ }
179
+
180
+ const m = memo.rows[0]
181
+
182
+ const insertResult = await execute(
183
+ 'INSERT INTO memo_replies (memo_id, content, created_by) VALUES (?, ?, ?)',
184
+ [memo_id, content, created_by],
185
+ )
186
+
187
+ if (insertResult.error) {
188
+ return { content: [{ type: 'text' as const, text: `Error: ${insertResult.error}` }] }
189
+ }
190
+
191
+ // Notify memo author (skip self-notification)
192
+ if (m.created_by !== created_by) {
193
+ await execute(
194
+ `INSERT INTO notifications (user_name, type, title, body, source_type, source_id, page_id, actor)
195
+ VALUES (?, 'reply_received', ?, ?, 'memo', ?, ?, ?)`,
196
+ [
197
+ m.created_by,
198
+ `${created_by} replied to your memo`,
199
+ content.length > 60 ? content.slice(0, 57) + '…' : content,
200
+ memo_id,
201
+ m.page_id,
202
+ created_by,
203
+ ],
204
+ )
205
+ }
206
+
207
+ return {
208
+ content: [{
209
+ type: 'text' as const,
210
+ text: `Reply posted on memo #${memo_id} by ${created_by}.\nContent: ${content}`,
211
+ }],
212
+ }
213
+ },
214
+ )
215
+
216
+ // Tool 6: Resolve memo
217
+ server.tool(
218
+ 'resolve_memo',
219
+ 'Mark a memo as resolved and notify related users',
220
+ {
221
+ memo_id: z.number().describe('Memo ID to resolve'),
222
+ resolved_by: z.string().describe('Name of resolver'),
223
+ },
224
+ async ({ memo_id, resolved_by }) => {
225
+ const memo = await query<{
226
+ id: number; page_id: string; created_by: string; assigned_to: string | null; status: string
227
+ }>(
228
+ 'SELECT id, page_id, created_by, assigned_to, status FROM memos_v2 WHERE id = ?',
229
+ [memo_id],
230
+ )
231
+
232
+ if (memo.error) {
233
+ return { content: [{ type: 'text' as const, text: `Error: ${memo.error}` }] }
234
+ }
235
+ if (memo.rows.length === 0) {
236
+ return { content: [{ type: 'text' as const, text: `Memo #${memo_id} not found.` }] }
237
+ }
238
+
239
+ const m = memo.rows[0]
240
+ if (m.status === 'resolved') {
241
+ return { content: [{ type: 'text' as const, text: `Memo #${memo_id} is already resolved.` }] }
242
+ }
243
+
244
+ const updateResult = await execute(
245
+ "UPDATE memos_v2 SET status = 'resolved' WHERE id = ?",
246
+ [memo_id],
247
+ )
248
+
249
+ if (updateResult.error) {
250
+ return { content: [{ type: 'text' as const, text: `Error: ${updateResult.error}` }] }
251
+ }
252
+
253
+ if (m.created_by !== resolved_by) {
254
+ await execute(
255
+ `INSERT INTO notifications (user_name, type, title, body, source_type, source_id, page_id, actor)
256
+ VALUES (?, 'memo_resolved', ?, NULL, 'memo', ?, ?, ?)`,
257
+ [m.created_by, `${resolved_by} resolved the memo`, memo_id, m.page_id, resolved_by],
258
+ )
259
+ }
260
+
261
+ return {
262
+ content: [{
263
+ type: 'text' as const,
264
+ text: `Memo #${memo_id} resolved by ${resolved_by}.`,
265
+ }],
266
+ }
267
+ },
268
+ )
269
+
270
+ async function main() {
271
+ const transport = new StdioServerTransport()
272
+ await server.connect(transport)
273
+ }
274
+
275
+ main().catch(console.error)
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Database HTTP API client (Turso Hrana v2 protocol)
3
+ * Reads DB_URL and DB_AUTH_TOKEN from environment.
4
+ */
5
+
6
+ const DB_URL = process.env.DB_URL ?? ''
7
+ const DB_AUTH_TOKEN = process.env.DB_AUTH_TOKEN ?? ''
8
+
9
+ type ArgValue = string | number | null
10
+
11
+ interface HranaValue {
12
+ type: 'text' | 'integer' | 'float' | 'null'
13
+ value?: string
14
+ }
15
+
16
+ function toHranaArg(v: ArgValue): HranaValue {
17
+ if (v === null) return { type: 'null' }
18
+ if (typeof v === 'number') {
19
+ return Number.isInteger(v)
20
+ ? { type: 'integer', value: String(v) }
21
+ : { type: 'float', value: String(v) }
22
+ }
23
+ return { type: 'text', value: String(v) }
24
+ }
25
+
26
+ interface PipelineResult {
27
+ results: Array<{
28
+ type: 'ok' | 'error'
29
+ response?: {
30
+ type: string
31
+ result?: {
32
+ cols: Array<{ name: string }>
33
+ rows: Array<Array<HranaValue>>
34
+ affected_row_count: number
35
+ last_insert_rowid: string | null
36
+ }
37
+ }
38
+ error?: { message: string }
39
+ }>
40
+ }
41
+
42
+ export async function query<T = Record<string, unknown>>(
43
+ sql: string,
44
+ args: ArgValue[] = [],
45
+ ): Promise<{ rows: T[]; error?: string }> {
46
+ if (!DB_URL || !DB_AUTH_TOKEN) {
47
+ return { rows: [], error: 'Missing DB_URL or DB_AUTH_TOKEN' }
48
+ }
49
+
50
+ try {
51
+ const resp = await fetch(DB_URL + '/v2/pipeline', {
52
+ method: 'POST',
53
+ headers: {
54
+ Authorization: 'Bearer ' + DB_AUTH_TOKEN,
55
+ 'Content-Type': 'application/json',
56
+ },
57
+ body: JSON.stringify({
58
+ requests: [
59
+ { type: 'execute', stmt: { sql, args: args.map(toHranaArg) } },
60
+ { type: 'close' },
61
+ ],
62
+ }),
63
+ signal: AbortSignal.timeout(10000),
64
+ })
65
+
66
+ if (!resp.ok) {
67
+ const text = await resp.text().catch(() => '')
68
+ return { rows: [], error: `HTTP ${resp.status}: ${text}` }
69
+ }
70
+
71
+ const data: PipelineResult = await resp.json()
72
+ const first = data.results[0]
73
+
74
+ if (first.type === 'error') {
75
+ return { rows: [], error: first.error?.message ?? 'Unknown error' }
76
+ }
77
+
78
+ const result = first.response?.result
79
+ if (!result) return { rows: [] }
80
+
81
+ const colNames = result.cols.map(c => c.name)
82
+ const rows = result.rows.map(row => {
83
+ const obj: Record<string, unknown> = {}
84
+ colNames.forEach((col, i) => {
85
+ const cell = row[i]
86
+ if (cell.type === 'null') obj[col] = null
87
+ else if (cell.type === 'integer') obj[col] = Number(cell.value)
88
+ else if (cell.type === 'float') obj[col] = Number(cell.value)
89
+ else obj[col] = cell.value
90
+ })
91
+ return obj as T
92
+ })
93
+
94
+ return { rows }
95
+ } catch (err: unknown) {
96
+ const message = err instanceof Error ? err.message : 'Unknown error'
97
+ return { rows: [], error: message }
98
+ }
99
+ }
100
+
101
+ export async function execute(
102
+ sql: string,
103
+ args: ArgValue[] = [],
104
+ ): Promise<{ rowsAffected: number; error?: string }> {
105
+ if (!DB_URL || !DB_AUTH_TOKEN) {
106
+ return { rowsAffected: 0, error: 'Missing DB_URL or DB_AUTH_TOKEN' }
107
+ }
108
+
109
+ try {
110
+ const resp = await fetch(DB_URL + '/v2/pipeline', {
111
+ method: 'POST',
112
+ headers: {
113
+ Authorization: 'Bearer ' + DB_AUTH_TOKEN,
114
+ 'Content-Type': 'application/json',
115
+ },
116
+ body: JSON.stringify({
117
+ requests: [
118
+ { type: 'execute', stmt: { sql, args: args.map(toHranaArg) } },
119
+ { type: 'close' },
120
+ ],
121
+ }),
122
+ signal: AbortSignal.timeout(10000),
123
+ })
124
+
125
+ if (!resp.ok) {
126
+ const text = await resp.text().catch(() => '')
127
+ return { rowsAffected: 0, error: `HTTP ${resp.status}: ${text}` }
128
+ }
129
+
130
+ const data: PipelineResult = await resp.json()
131
+ const first = data.results[0]
132
+
133
+ if (first.type === 'error') {
134
+ return { rowsAffected: 0, error: first.error?.message ?? 'Unknown error' }
135
+ }
136
+
137
+ return { rowsAffected: first.response?.result?.affected_row_count ?? 0 }
138
+ } catch (err: unknown) {
139
+ const message = err instanceof Error ? err.message : 'Unknown error'
140
+ return { rowsAffected: 0, error: message }
141
+ }
142
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "Node16",
5
+ "moduleResolution": "Node16",
6
+ "outDir": "dist",
7
+ "rootDir": "src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "declaration": true
12
+ },
13
+ "include": ["src"]
14
+ }
@@ -0,0 +1,49 @@
1
+ -- Memo v2 migration — authentication, user activity, memos, replies
2
+
3
+ -- 1. Auth tokens
4
+ CREATE TABLE IF NOT EXISTS auth_tokens (
5
+ token TEXT PRIMARY KEY,
6
+ user_name TEXT NOT NULL,
7
+ user_email TEXT,
8
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
9
+ expires_at TIMESTAMP,
10
+ is_active INTEGER DEFAULT 1
11
+ );
12
+
13
+ -- 2. User activity tracking
14
+ CREATE TABLE IF NOT EXISTS user_activity (
15
+ user_name TEXT PRIMARY KEY,
16
+ last_seen_at TIMESTAMP,
17
+ last_memo_seen TIMESTAMP,
18
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
19
+ );
20
+
21
+ -- 3. Memos v2
22
+ CREATE TABLE IF NOT EXISTS memos_v2 (
23
+ id INTEGER PRIMARY KEY,
24
+ page_id TEXT NOT NULL,
25
+ content TEXT NOT NULL,
26
+ memo_type TEXT DEFAULT 'memo',
27
+ status TEXT DEFAULT 'open',
28
+ created_by TEXT NOT NULL,
29
+ assigned_to TEXT,
30
+ resolved_by TEXT,
31
+ resolved_at TIMESTAMP,
32
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
33
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
34
+ );
35
+
36
+ CREATE INDEX IF NOT EXISTS idx_memos_v2_page ON memos_v2(page_id);
37
+ CREATE INDEX IF NOT EXISTS idx_memos_v2_assigned ON memos_v2(assigned_to, status);
38
+ CREATE INDEX IF NOT EXISTS idx_memos_v2_type ON memos_v2(memo_type, status);
39
+
40
+ -- 4. Memo replies
41
+ CREATE TABLE IF NOT EXISTS memo_replies (
42
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
43
+ memo_id INTEGER NOT NULL,
44
+ content TEXT NOT NULL,
45
+ created_by TEXT NOT NULL,
46
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
47
+ );
48
+
49
+ CREATE INDEX IF NOT EXISTS idx_memo_replies_memo ON memo_replies(memo_id);
@@ -0,0 +1,18 @@
1
+ -- Notifications table for per-user alert management
2
+
3
+ CREATE TABLE IF NOT EXISTS notifications (
4
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
5
+ user_name TEXT NOT NULL,
6
+ type TEXT NOT NULL DEFAULT 'memo_assigned',
7
+ title TEXT NOT NULL,
8
+ body TEXT,
9
+ source_type TEXT NOT NULL DEFAULT 'memo',
10
+ source_id INTEGER NOT NULL,
11
+ page_id TEXT NOT NULL,
12
+ actor TEXT NOT NULL,
13
+ is_read INTEGER DEFAULT 0,
14
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
15
+ );
16
+
17
+ CREATE INDEX IF NOT EXISTS idx_notifications_user_unread ON notifications(user_name, is_read, created_at DESC);
18
+ CREATE INDEX IF NOT EXISTS idx_notifications_source ON notifications(source_type, source_id);
@@ -0,0 +1,66 @@
1
+ -- Spec-site content schema — rules, scenarios, areas, versions, wireframe meta
2
+
3
+ -- 1. Rules (flat, individually queryable)
4
+ CREATE TABLE IF NOT EXISTS spec_rules (
5
+ id TEXT NOT NULL,
6
+ page_id TEXT NOT NULL,
7
+ rule_group TEXT NOT NULL,
8
+ category TEXT NOT NULL,
9
+ name TEXT NOT NULL,
10
+ condition TEXT NOT NULL,
11
+ severity TEXT NOT NULL,
12
+ home_message TEXT NOT NULL DEFAULT '',
13
+ action TEXT NOT NULL DEFAULT '',
14
+ data_source TEXT NOT NULL DEFAULT '',
15
+ impl_status TEXT NOT NULL DEFAULT 'logic-needed',
16
+ impl_note TEXT,
17
+ action_route TEXT,
18
+ sort_order INTEGER NOT NULL DEFAULT 0,
19
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
20
+ PRIMARY KEY (page_id, id)
21
+ );
22
+ CREATE INDEX IF NOT EXISTS idx_spec_rules_page ON spec_rules(page_id);
23
+
24
+ -- 2. Scenarios
25
+ CREATE TABLE IF NOT EXISTS spec_scenarios (
26
+ page_id TEXT NOT NULL,
27
+ scenario_id TEXT NOT NULL,
28
+ label TEXT NOT NULL,
29
+ data_json TEXT NOT NULL,
30
+ is_default INTEGER DEFAULT 0,
31
+ sort_order INTEGER NOT NULL DEFAULT 0,
32
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
33
+ PRIMARY KEY (page_id, scenario_id)
34
+ );
35
+
36
+ -- 3. Spec areas
37
+ CREATE TABLE IF NOT EXISTS spec_areas (
38
+ page_id TEXT NOT NULL,
39
+ area_id TEXT NOT NULL,
40
+ label TEXT NOT NULL,
41
+ short_label TEXT NOT NULL,
42
+ rule_count INTEGER NOT NULL DEFAULT 0,
43
+ sort_order INTEGER NOT NULL DEFAULT 0,
44
+ PRIMARY KEY (page_id, area_id)
45
+ );
46
+
47
+ -- 4. Page versions
48
+ CREATE TABLE IF NOT EXISTS spec_versions (
49
+ page_id TEXT PRIMARY KEY,
50
+ version TEXT NOT NULL,
51
+ last_updated TEXT NOT NULL,
52
+ sprint TEXT NOT NULL,
53
+ status TEXT NOT NULL DEFAULT 'draft',
54
+ changelog TEXT NOT NULL DEFAULT '[]',
55
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
56
+ );
57
+
58
+ -- 5. Wireframe metadata
59
+ CREATE TABLE IF NOT EXISTS spec_wireframe_meta (
60
+ page_id TEXT NOT NULL,
61
+ sprint TEXT NOT NULL,
62
+ default_scenario_id TEXT NOT NULL,
63
+ spec_title TEXT NOT NULL,
64
+ route_title TEXT NOT NULL,
65
+ PRIMARY KEY (page_id, sprint)
66
+ );
@@ -0,0 +1,21 @@
1
+ -- Agent events table for push-based inter-agent communication
2
+
3
+ CREATE TABLE IF NOT EXISTS agent_events (
4
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
5
+ event_type TEXT NOT NULL,
6
+ source_agent TEXT NOT NULL,
7
+ target_agent TEXT NOT NULL,
8
+ target_user TEXT NOT NULL,
9
+ payload TEXT NOT NULL,
10
+ status TEXT DEFAULT 'pending',
11
+ delivered_at TIMESTAMP,
12
+ acked_at TIMESTAMP,
13
+ expires_at TIMESTAMP,
14
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
15
+ );
16
+
17
+ CREATE INDEX IF NOT EXISTS idx_events_target
18
+ ON agent_events(target_user, status, created_at DESC);
19
+
20
+ CREATE INDEX IF NOT EXISTS idx_events_type
21
+ ON agent_events(event_type, status);
@@ -0,0 +1,6 @@
1
+ -- Epic-Sprint decoupling migration
2
+ -- Purpose: Make pm_epics the SSOT, remove nav_epics dependency
3
+ -- Epics are global; stories move between sprints.
4
+
5
+ ALTER TABLE pm_epics ADD COLUMN sort_order INTEGER NOT NULL DEFAULT 0;
6
+ ALTER TABLE pm_epics ADD COLUMN origin_sprint TEXT;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Retro → Kickoff Link — pure domain logic
3
+ */
4
+
5
+ export interface RetroAction {
6
+ id: number
7
+ content: string
8
+ assignee: string | null
9
+ status: string
10
+ }
11
+
12
+ export interface BacklogStory {
13
+ title: string
14
+ description: string
15
+ assignee: string | null
16
+ source: string
17
+ }
18
+
19
+ /** Convert action item → backlog story */
20
+ export function actionToStory(action: RetroAction, sprintId: string): BacklogStory {
21
+ return {
22
+ title: `[Retro] ${action.content.slice(0, 60)}`,
23
+ description: `Created from retro (${sprintId}) action item.\n\nOriginal: ${action.content}`,
24
+ assignee: action.assignee,
25
+ source: `retro:${sprintId}`,
26
+ }
27
+ }
28
+
29
+ /** Filter incomplete action items */
30
+ export function getPendingActions(actions: RetroAction[]): RetroAction[] {
31
+ return actions.filter(a => a.status !== 'done')
32
+ }