@assistkick/create 1.0.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 (178) hide show
  1. package/dist/bin/create.d.ts +2 -0
  2. package/dist/bin/create.js +25 -0
  3. package/dist/bin/create.js.map +1 -0
  4. package/dist/src/scaffolder.d.ts +22 -0
  5. package/dist/src/scaffolder.js +120 -0
  6. package/dist/src/scaffolder.js.map +1 -0
  7. package/package.json +24 -0
  8. package/templates/product-system/.env.example +8 -0
  9. package/templates/product-system/CLAUDE.md +45 -0
  10. package/templates/product-system/package.json +32 -0
  11. package/templates/product-system/packages/backend/package.json +37 -0
  12. package/templates/product-system/packages/backend/src/middleware/auth_middleware.test.ts +86 -0
  13. package/templates/product-system/packages/backend/src/middleware/auth_middleware.ts +35 -0
  14. package/templates/product-system/packages/backend/src/routes/auth.ts +463 -0
  15. package/templates/product-system/packages/backend/src/routes/coherence.ts +187 -0
  16. package/templates/product-system/packages/backend/src/routes/graph.ts +67 -0
  17. package/templates/product-system/packages/backend/src/routes/kanban.ts +201 -0
  18. package/templates/product-system/packages/backend/src/routes/pipeline.ts +41 -0
  19. package/templates/product-system/packages/backend/src/routes/projects.ts +122 -0
  20. package/templates/product-system/packages/backend/src/routes/users.ts +97 -0
  21. package/templates/product-system/packages/backend/src/server.ts +159 -0
  22. package/templates/product-system/packages/backend/src/services/auth_service.test.ts +115 -0
  23. package/templates/product-system/packages/backend/src/services/auth_service.ts +82 -0
  24. package/templates/product-system/packages/backend/src/services/coherence-review.ts +339 -0
  25. package/templates/product-system/packages/backend/src/services/email_service.ts +75 -0
  26. package/templates/product-system/packages/backend/src/services/init.ts +80 -0
  27. package/templates/product-system/packages/backend/src/services/invitation_service.test.ts +235 -0
  28. package/templates/product-system/packages/backend/src/services/invitation_service.ts +193 -0
  29. package/templates/product-system/packages/backend/src/services/password_reset_service.test.ts +151 -0
  30. package/templates/product-system/packages/backend/src/services/password_reset_service.ts +135 -0
  31. package/templates/product-system/packages/backend/src/services/project_service.test.ts +215 -0
  32. package/templates/product-system/packages/backend/src/services/project_service.ts +171 -0
  33. package/templates/product-system/packages/backend/src/services/pty_session_manager.test.ts +88 -0
  34. package/templates/product-system/packages/backend/src/services/pty_session_manager.ts +279 -0
  35. package/templates/product-system/packages/backend/src/services/terminal_ws_handler.ts +133 -0
  36. package/templates/product-system/packages/backend/src/services/user_management_service.test.ts +158 -0
  37. package/templates/product-system/packages/backend/src/services/user_management_service.ts +128 -0
  38. package/templates/product-system/packages/backend/tsconfig.json +22 -0
  39. package/templates/product-system/packages/frontend/index.html +13 -0
  40. package/templates/product-system/packages/frontend/package-lock.json +2666 -0
  41. package/templates/product-system/packages/frontend/package.json +30 -0
  42. package/templates/product-system/packages/frontend/public/favicon.svg +16 -0
  43. package/templates/product-system/packages/frontend/src/App.tsx +29 -0
  44. package/templates/product-system/packages/frontend/src/api/client.ts +386 -0
  45. package/templates/product-system/packages/frontend/src/api/client_projects.test.ts +104 -0
  46. package/templates/product-system/packages/frontend/src/api/client_refresh.test.ts +145 -0
  47. package/templates/product-system/packages/frontend/src/components/CoherenceView.tsx +414 -0
  48. package/templates/product-system/packages/frontend/src/components/GraphLegend.tsx +124 -0
  49. package/templates/product-system/packages/frontend/src/components/GraphSettings.tsx +112 -0
  50. package/templates/product-system/packages/frontend/src/components/GraphView.tsx +370 -0
  51. package/templates/product-system/packages/frontend/src/components/InviteUserDialog.tsx +85 -0
  52. package/templates/product-system/packages/frontend/src/components/KanbanView.tsx +470 -0
  53. package/templates/product-system/packages/frontend/src/components/LoginPage.tsx +116 -0
  54. package/templates/product-system/packages/frontend/src/components/ProjectSelector.tsx +187 -0
  55. package/templates/product-system/packages/frontend/src/components/QaIssueSheet.tsx +192 -0
  56. package/templates/product-system/packages/frontend/src/components/SidePanel.tsx +231 -0
  57. package/templates/product-system/packages/frontend/src/components/TerminalView.tsx +200 -0
  58. package/templates/product-system/packages/frontend/src/components/Toolbar.tsx +84 -0
  59. package/templates/product-system/packages/frontend/src/components/UsersView.tsx +249 -0
  60. package/templates/product-system/packages/frontend/src/constants/graph.ts +191 -0
  61. package/templates/product-system/packages/frontend/src/hooks/useAuth.tsx +54 -0
  62. package/templates/product-system/packages/frontend/src/hooks/useGraph.ts +27 -0
  63. package/templates/product-system/packages/frontend/src/hooks/useKanban.ts +21 -0
  64. package/templates/product-system/packages/frontend/src/hooks/useProjects.ts +86 -0
  65. package/templates/product-system/packages/frontend/src/hooks/useTheme.ts +26 -0
  66. package/templates/product-system/packages/frontend/src/hooks/useToast.tsx +62 -0
  67. package/templates/product-system/packages/frontend/src/hooks/use_projects_logic.test.ts +61 -0
  68. package/templates/product-system/packages/frontend/src/main.tsx +12 -0
  69. package/templates/product-system/packages/frontend/src/pages/accept_invitation_page.tsx +167 -0
  70. package/templates/product-system/packages/frontend/src/pages/forgot_password_page.tsx +100 -0
  71. package/templates/product-system/packages/frontend/src/pages/register_page.tsx +137 -0
  72. package/templates/product-system/packages/frontend/src/pages/reset_password_page.tsx +146 -0
  73. package/templates/product-system/packages/frontend/src/routes/ProtectedRoute.tsx +12 -0
  74. package/templates/product-system/packages/frontend/src/routes/accept_invitation.tsx +14 -0
  75. package/templates/product-system/packages/frontend/src/routes/dashboard.tsx +221 -0
  76. package/templates/product-system/packages/frontend/src/routes/forgot_password.tsx +13 -0
  77. package/templates/product-system/packages/frontend/src/routes/login.tsx +14 -0
  78. package/templates/product-system/packages/frontend/src/routes/register.tsx +14 -0
  79. package/templates/product-system/packages/frontend/src/routes/reset_password.tsx +13 -0
  80. package/templates/product-system/packages/frontend/src/styles/index.css +3358 -0
  81. package/templates/product-system/packages/frontend/src/utils/auth_validation.test.ts +51 -0
  82. package/templates/product-system/packages/frontend/src/utils/auth_validation.ts +19 -0
  83. package/templates/product-system/packages/frontend/src/utils/login_validation.test.ts +61 -0
  84. package/templates/product-system/packages/frontend/src/utils/login_validation.ts +24 -0
  85. package/templates/product-system/packages/frontend/src/utils/logout.test.ts +63 -0
  86. package/templates/product-system/packages/frontend/src/utils/node_sizing.test.ts +62 -0
  87. package/templates/product-system/packages/frontend/src/utils/node_sizing.ts +24 -0
  88. package/templates/product-system/packages/frontend/src/utils/task_status.test.ts +53 -0
  89. package/templates/product-system/packages/frontend/src/utils/task_status.ts +14 -0
  90. package/templates/product-system/packages/frontend/tsconfig.json +21 -0
  91. package/templates/product-system/packages/frontend/vite.config.ts +20 -0
  92. package/templates/product-system/packages/shared/.env.example +3 -0
  93. package/templates/product-system/packages/shared/README.md +1 -0
  94. package/templates/product-system/packages/shared/db/migrate.ts +32 -0
  95. package/templates/product-system/packages/shared/db/migrations/0000_dashing_gorgon.sql +128 -0
  96. package/templates/product-system/packages/shared/db/migrations/meta/0000_snapshot.json +819 -0
  97. package/templates/product-system/packages/shared/db/migrations/meta/_journal.json +13 -0
  98. package/templates/product-system/packages/shared/db/schema.ts +137 -0
  99. package/templates/product-system/packages/shared/drizzle.config.js +14 -0
  100. package/templates/product-system/packages/shared/lib/claude-service.ts +215 -0
  101. package/templates/product-system/packages/shared/lib/coherence.ts +278 -0
  102. package/templates/product-system/packages/shared/lib/completeness.ts +30 -0
  103. package/templates/product-system/packages/shared/lib/constants.ts +327 -0
  104. package/templates/product-system/packages/shared/lib/db.ts +81 -0
  105. package/templates/product-system/packages/shared/lib/git_workflow.ts +110 -0
  106. package/templates/product-system/packages/shared/lib/graph.ts +186 -0
  107. package/templates/product-system/packages/shared/lib/kanban.ts +161 -0
  108. package/templates/product-system/packages/shared/lib/markdown.ts +205 -0
  109. package/templates/product-system/packages/shared/lib/pipeline-state-store.ts +124 -0
  110. package/templates/product-system/packages/shared/lib/pipeline.ts +489 -0
  111. package/templates/product-system/packages/shared/lib/prompt_builder.ts +170 -0
  112. package/templates/product-system/packages/shared/lib/relevance_search.ts +159 -0
  113. package/templates/product-system/packages/shared/lib/session.ts +152 -0
  114. package/templates/product-system/packages/shared/lib/validator.ts +117 -0
  115. package/templates/product-system/packages/shared/lib/work_summary_parser.ts +130 -0
  116. package/templates/product-system/packages/shared/package.json +30 -0
  117. package/templates/product-system/packages/shared/scripts/assign-project.ts +52 -0
  118. package/templates/product-system/packages/shared/tools/add_edge.ts +61 -0
  119. package/templates/product-system/packages/shared/tools/add_node.ts +101 -0
  120. package/templates/product-system/packages/shared/tools/end_session.ts +87 -0
  121. package/templates/product-system/packages/shared/tools/get_gaps.ts +87 -0
  122. package/templates/product-system/packages/shared/tools/get_kanban.ts +125 -0
  123. package/templates/product-system/packages/shared/tools/get_node.ts +78 -0
  124. package/templates/product-system/packages/shared/tools/get_status.ts +98 -0
  125. package/templates/product-system/packages/shared/tools/migrate_to_turso.ts +385 -0
  126. package/templates/product-system/packages/shared/tools/move_card.ts +143 -0
  127. package/templates/product-system/packages/shared/tools/rebuild_index.ts +77 -0
  128. package/templates/product-system/packages/shared/tools/remove_edge.ts +59 -0
  129. package/templates/product-system/packages/shared/tools/remove_node.ts +96 -0
  130. package/templates/product-system/packages/shared/tools/resolve_question.ts +75 -0
  131. package/templates/product-system/packages/shared/tools/search_nodes.ts +106 -0
  132. package/templates/product-system/packages/shared/tools/start_session.ts +144 -0
  133. package/templates/product-system/packages/shared/tools/update_node.ts +133 -0
  134. package/templates/product-system/packages/shared/tsconfig.json +24 -0
  135. package/templates/product-system/pnpm-workspace.yaml +2 -0
  136. package/templates/product-system/smoke_test.ts +219 -0
  137. package/templates/product-system/tests/coherence_review.test.ts +562 -0
  138. package/templates/product-system/tests/db_sqlite_fallback.test.ts +75 -0
  139. package/templates/product-system/tests/edge_type_color_coding.test.ts +147 -0
  140. package/templates/product-system/tests/emit-tool-use-events.test.ts +85 -0
  141. package/templates/product-system/tests/feature_kind.test.ts +139 -0
  142. package/templates/product-system/tests/gap_indicators.test.ts +199 -0
  143. package/templates/product-system/tests/graceful_init.test.ts +142 -0
  144. package/templates/product-system/tests/graph_legend.test.ts +314 -0
  145. package/templates/product-system/tests/graph_settings_sheet.test.ts +804 -0
  146. package/templates/product-system/tests/hide_defined_filter.test.ts +205 -0
  147. package/templates/product-system/tests/kanban.test.ts +529 -0
  148. package/templates/product-system/tests/neighborhood_focus.test.ts +132 -0
  149. package/templates/product-system/tests/node_search.test.ts +340 -0
  150. package/templates/product-system/tests/node_sizing.test.ts +170 -0
  151. package/templates/product-system/tests/node_type_toggle_filters.test.ts +285 -0
  152. package/templates/product-system/tests/node_type_visual_encoding.test.ts +103 -0
  153. package/templates/product-system/tests/pipeline-state-store.test.ts +268 -0
  154. package/templates/product-system/tests/pipeline-unit.test.ts +593 -0
  155. package/templates/product-system/tests/pipeline.test.ts +195 -0
  156. package/templates/product-system/tests/pipeline_stats_all_cards.test.ts +193 -0
  157. package/templates/product-system/tests/play_all.test.ts +296 -0
  158. package/templates/product-system/tests/qa_issue_sheet.test.ts +464 -0
  159. package/templates/product-system/tests/relevance_search.test.ts +186 -0
  160. package/templates/product-system/tests/search_reorder.test.ts +88 -0
  161. package/templates/product-system/tests/serve_ui.test.ts +281 -0
  162. package/templates/product-system/tests/serve_ui_drizzle.test.ts +114 -0
  163. package/templates/product-system/tests/session_context_recall.test.ts +135 -0
  164. package/templates/product-system/tests/side_panel.test.ts +345 -0
  165. package/templates/product-system/tests/spec_completeness_label.test.ts +69 -0
  166. package/templates/product-system/tests/url_routing_test.ts +122 -0
  167. package/templates/product-system/tests/user_login.test.ts +150 -0
  168. package/templates/product-system/tests/user_registration.test.ts +205 -0
  169. package/templates/product-system/tests/web_terminal.test.ts +572 -0
  170. package/templates/product-system/tests/work_summary.test.ts +211 -0
  171. package/templates/product-system/tests/zoom_pan.test.ts +43 -0
  172. package/templates/product-system/tsconfig.json +24 -0
  173. package/templates/skills/product-bootstrap/SKILL.md +312 -0
  174. package/templates/skills/product-code-reviewer/SKILL.md +147 -0
  175. package/templates/skills/product-debugger/SKILL.md +206 -0
  176. package/templates/skills/product-debugger/references/agent-browser.md +1156 -0
  177. package/templates/skills/product-developer/SKILL.md +182 -0
  178. package/templates/skills/product-interview/SKILL.md +220 -0
@@ -0,0 +1,161 @@
1
+ /**
2
+ * Kanban board operations backed by Drizzle/Turso.
3
+ * Replaces direct kanban.json file I/O in tools.
4
+ */
5
+
6
+ import {eq, sql} from 'drizzle-orm';
7
+ import {getDb} from './db.js';
8
+ /**
9
+ * Load the full kanban state as an object keyed by feature ID.
10
+ * Returns the same shape as the old kanban.json:
11
+ * { feat_001: { column, rejection_count, notes, dev_blocked?, moved_at } }
12
+ *
13
+ * Note: rejection_count, notes, dev_blocked, and moved_at are stored as JSON
14
+ * in a notes-like approach. Since the kanban table schema only has nodeId,
15
+ * columnName, and position, we store extra metadata in the node's body or
16
+ * a separate mechanism. For simplicity, we store extended kanban metadata
17
+ * as a JSON string in the position field's companion — actually we need
18
+ * to handle this differently.
19
+ *
20
+ * The kanban table has: nodeId, columnName, position.
21
+ * The old kanban.json had: column, rejection_count, notes[], dev_blocked, moved_at.
22
+ *
23
+ * We'll store the extra metadata (rejection_count, notes, dev_blocked, moved_at)
24
+ * as a JSON blob in a convention: we use the review_meta table with key pattern
25
+ * "kanban:{nodeId}" to store the extra data.
26
+ */
27
+ import {kanban, nodes, reviewMeta} from '../db/schema.js';
28
+
29
+ const kanbanMetaKey = (nodeId) => `kanban:${nodeId}`;
30
+
31
+ const getKanbanMeta = async (db, nodeId) => {
32
+ const rows = await db.select().from(reviewMeta)
33
+ .where(eq(reviewMeta.key, kanbanMetaKey(nodeId)));
34
+ if (!rows[0]) return { rejection_count: 0, notes: [], reviews: [] };
35
+ return JSON.parse(rows[0].value);
36
+ };
37
+
38
+ const setKanbanMeta = async (db, nodeId, meta) => {
39
+ const key = kanbanMetaKey(nodeId);
40
+ const value = JSON.stringify(meta);
41
+ const existing = await db.select().from(reviewMeta).where(eq(reviewMeta.key, key));
42
+ if (existing[0]) {
43
+ await db.update(reviewMeta).set({ value }).where(eq(reviewMeta.key, key));
44
+ } else {
45
+ await db.insert(reviewMeta).values({ key, value });
46
+ }
47
+ };
48
+
49
+ /**
50
+ * Load the full kanban board state.
51
+ * Returns an object matching the old kanban.json shape.
52
+ */
53
+ export const loadKanban = async (projectId?: string) => {
54
+ const db = getDb();
55
+
56
+ let rows;
57
+ if (projectId) {
58
+ // Join kanban with nodes to filter by project_id
59
+ rows = await db.select({nodeId: kanban.nodeId, columnName: kanban.columnName, position: kanban.position})
60
+ .from(kanban)
61
+ .innerJoin(nodes, eq(kanban.nodeId, nodes.id))
62
+ .where(eq(nodes.projectId, projectId));
63
+ } else {
64
+ rows = await db.select().from(kanban);
65
+ }
66
+
67
+ const allMeta = await db.select().from(reviewMeta).where(sql`key LIKE 'kanban:%'`);
68
+
69
+ // Build a lookup map from reviewMeta rows
70
+ const metaMap = new Map();
71
+ for (const m of allMeta) {
72
+ const nodeId = m.key.slice('kanban:'.length);
73
+ metaMap.set(nodeId, JSON.parse(m.value));
74
+ }
75
+
76
+ const result = {};
77
+ for (const row of rows) {
78
+ const meta = metaMap.get(row.nodeId) || { rejection_count: 0, notes: [], reviews: [] };
79
+ result[row.nodeId] = {
80
+ column: row.columnName,
81
+ rejection_count: meta.rejection_count || 0,
82
+ notes: meta.notes || [],
83
+ reviews: meta.reviews || [],
84
+ ...(meta.dev_blocked ? { dev_blocked: true } : {}),
85
+ ...(meta.moved_at ? { moved_at: meta.moved_at } : {}),
86
+ };
87
+ }
88
+
89
+ return result;
90
+ };
91
+
92
+ /**
93
+ * Check if the kanban board has any entries.
94
+ */
95
+ export const kanbanExists = async (projectId?: string) => {
96
+ const db = getDb();
97
+ if (projectId) {
98
+ const rows = await db.select({ nodeId: kanban.nodeId })
99
+ .from(kanban)
100
+ .innerJoin(nodes, eq(kanban.nodeId, nodes.id))
101
+ .where(eq(nodes.projectId, projectId))
102
+ .limit(1);
103
+ return !!rows[0];
104
+ }
105
+ const rows = await db.select({ nodeId: kanban.nodeId }).from(kanban).limit(1);
106
+ return !!rows[0];
107
+ };
108
+
109
+ /**
110
+ * Get a single kanban entry by feature ID.
111
+ */
112
+ export const getKanbanEntry = async (featureId) => {
113
+ const db = getDb();
114
+ const rows = await db.select().from(kanban).where(eq(kanban.nodeId, featureId));
115
+ if (!rows[0]) return null;
116
+
117
+ const meta = await getKanbanMeta(db, featureId);
118
+ return {
119
+ column: rows[0].columnName,
120
+ rejection_count: meta.rejection_count || 0,
121
+ notes: meta.notes || [],
122
+ reviews: meta.reviews || [],
123
+ ...(meta.dev_blocked ? { dev_blocked: true } : {}),
124
+ ...(meta.moved_at ? { moved_at: meta.moved_at } : {}),
125
+ };
126
+ };
127
+
128
+ /**
129
+ * Save a full kanban entry for a feature.
130
+ */
131
+ export const saveKanbanEntry = async (featureId, entry, projectId?: string) => {
132
+ const db = getDb();
133
+
134
+ const existing = await db.select().from(kanban).where(eq(kanban.nodeId, featureId));
135
+ if (existing[0]) {
136
+ await db.update(kanban).set({
137
+ columnName: entry.column,
138
+ }).where(eq(kanban.nodeId, featureId));
139
+ } else {
140
+ // Determine position (append to end of column)
141
+ const colEntries = await db.select().from(kanban)
142
+ .where(eq(kanban.columnName, entry.column));
143
+ const maxPos = colEntries.reduce((max, r) => Math.max(max, r.position), -1);
144
+
145
+ await db.insert(kanban).values({
146
+ nodeId: featureId,
147
+ columnName: entry.column,
148
+ position: maxPos + 1,
149
+ projectId: projectId || null,
150
+ });
151
+ }
152
+
153
+ // Store extra metadata
154
+ await setKanbanMeta(db, featureId, {
155
+ rejection_count: entry.rejection_count || 0,
156
+ notes: entry.notes || [],
157
+ reviews: entry.reviews || [],
158
+ ...(entry.dev_blocked ? { dev_blocked: true } : {}),
159
+ ...(entry.moved_at ? { moved_at: entry.moved_at } : {}),
160
+ });
161
+ };
@@ -0,0 +1,205 @@
1
+ /**
2
+ * Node content operations backed by Drizzle/Turso.
3
+ * Node body (sections) is stored in the nodes.body column as serialized markdown.
4
+ */
5
+
6
+ import { eq } from 'drizzle-orm';
7
+ import { getDb } from './db.js';
8
+ import { nodes } from '../db/schema.js';
9
+ import { REQUIRED_SECTIONS, COMMON_OPTIONAL_SECTIONS } from './constants.js';
10
+ import { scoreNode } from './completeness.js';
11
+
12
+ /**
13
+ * Parse a section-based markdown body into a { sectionName: content } map.
14
+ * Sections are delimited by `## Section Name` headers.
15
+ */
16
+ const parseSections = (body) => {
17
+ const sections = {};
18
+ const lines = body.split('\n');
19
+ let currentSection = null;
20
+ let currentLines = [];
21
+
22
+ for (const line of lines) {
23
+ const match = line.match(/^## (.+)$/);
24
+ if (match) {
25
+ if (currentSection !== null) {
26
+ sections[currentSection] = currentLines.join('\n').trim();
27
+ }
28
+ currentSection = match[1].trim();
29
+ currentLines = [];
30
+ } else if (currentSection !== null) {
31
+ currentLines.push(line);
32
+ }
33
+ }
34
+ if (currentSection !== null) {
35
+ sections[currentSection] = currentLines.join('\n').trim();
36
+ }
37
+
38
+ return sections;
39
+ };
40
+
41
+ /**
42
+ * Serialize sections map back into markdown body text.
43
+ */
44
+ const serializeSections = (sections) => {
45
+ const parts = [];
46
+ for (const [name, content] of Object.entries(sections)) {
47
+ parts.push(`## ${name}`);
48
+ if (content.trim()) {
49
+ parts.push(content.trim());
50
+ }
51
+ parts.push(''); // blank line after section
52
+ }
53
+ return parts.join('\n');
54
+ };
55
+
56
+ /**
57
+ * Read a node's content from the database.
58
+ * Accepts a node ID string (e.g. "feat_001").
59
+ * Returns { frontmatter, sections }.
60
+ */
61
+ export const readNode = async (nodeId) => {
62
+ const db = getDb();
63
+
64
+ const rows = await db.select().from(nodes).where(eq(nodes.id, nodeId));
65
+ const row = rows[0];
66
+ if (!row) {
67
+ throw new Error(`Node not found in database: ${nodeId}`);
68
+ }
69
+
70
+ // Reconstruct frontmatter from structured columns
71
+ const frontmatter = {
72
+ id: row.id,
73
+ type: row.type,
74
+ name: row.name,
75
+ status: row.status,
76
+ priority: row.priority,
77
+ ...(row.kind ? { kind: row.kind } : {}),
78
+ created_at: row.createdAt,
79
+ updated_at: row.updatedAt,
80
+ };
81
+
82
+ // Parse sections from body column
83
+ const body = row.body || '';
84
+ const sections = parseSections(body);
85
+
86
+ return { frontmatter, sections };
87
+ };
88
+
89
+ /**
90
+ * Write a node's content to the database.
91
+ * Accepts a node ID string (e.g. "feat_001").
92
+ */
93
+ export const writeNode = async (nodeId, frontmatter, sections) => {
94
+ const db = getDb();
95
+
96
+ const body = serializeSections(sections);
97
+
98
+ const updates = {
99
+ type: frontmatter.type,
100
+ name: frontmatter.name,
101
+ status: frontmatter.status,
102
+ priority: frontmatter.priority || 'medium',
103
+ kind: frontmatter.kind || null,
104
+ updatedAt: frontmatter.updated_at || new Date().toISOString(),
105
+ body,
106
+ };
107
+
108
+ // Check if node exists — update or insert
109
+ const existing = await db.select({ id: nodes.id }).from(nodes).where(eq(nodes.id, nodeId));
110
+ if (existing.length > 0) {
111
+ await db.update(nodes).set(updates).where(eq(nodes.id, nodeId));
112
+ } else {
113
+ await db.insert(nodes).values({
114
+ id: nodeId,
115
+ ...updates,
116
+ completeness: 0,
117
+ openQuestionsCount: 0,
118
+ createdAt: frontmatter.created_at || new Date().toISOString(),
119
+ });
120
+ }
121
+ };
122
+
123
+ /**
124
+ * Get the content of a specific section.
125
+ */
126
+ export const getSection = (sections, name) => {
127
+ return sections[name] || '';
128
+ };
129
+
130
+ /**
131
+ * Set the content of a specific section (creates it if missing).
132
+ */
133
+ export const setSection = (sections, name, content) => {
134
+ sections[name] = content;
135
+ return sections;
136
+ };
137
+
138
+ /**
139
+ * Append a line to a section (creates section if missing).
140
+ */
141
+ export const appendToSection = (sections, name, line) => {
142
+ const existing = sections[name] || '';
143
+ sections[name] = existing ? existing + '\n' + line : line;
144
+ return sections;
145
+ };
146
+
147
+ /**
148
+ * Move a line from one section to another (e.g., open question → resolved).
149
+ * The line is matched by substring. Returns true if moved, false if not found.
150
+ */
151
+ export const moveLineAcrossSections = (sections, fromSection, toSection, line) => {
152
+ const fromContent = sections[fromSection] || '';
153
+ const lines = fromContent.split('\n');
154
+ const idx = lines.findIndex(l => l.includes(line));
155
+
156
+ if (idx === -1) return false;
157
+
158
+ const removed = lines.splice(idx, 1)[0];
159
+ sections[fromSection] = lines.join('\n').trim();
160
+
161
+ const toContent = sections[toSection] || '';
162
+ sections[toSection] = toContent ? toContent + '\n' + removed : removed;
163
+
164
+ return true;
165
+ };
166
+
167
+ /**
168
+ * Derive graph.json metadata from a parsed node's frontmatter and sections.
169
+ * Returns { completeness, open_questions_count, status }.
170
+ */
171
+ export const deriveMetadata = (frontmatter, sections, type) => {
172
+ const completeness = scoreNode(type, frontmatter, sections);
173
+
174
+ // Count open questions (unchecked checkboxes)
175
+ const oq = sections['Open Questions'] || '';
176
+ const open_questions_count = oq
177
+ .split('\n')
178
+ .filter(l => l.trim().startsWith('- [ ]'))
179
+ .length;
180
+
181
+ // Derive status if not explicitly set
182
+ let status = frontmatter.status || 'draft';
183
+ if (status === 'draft' && completeness > 0) {
184
+ status = 'partially_defined';
185
+ }
186
+
187
+ return { completeness, open_questions_count, status };
188
+ };
189
+
190
+ /**
191
+ * Build a template sections object for a given node type with all required
192
+ * section headers (empty content).
193
+ */
194
+ export const templateSections = (type) => {
195
+ const required = REQUIRED_SECTIONS[type] || [];
196
+ const sections = {};
197
+ for (const name of required) {
198
+ sections[name] = '';
199
+ }
200
+ // Always include common optional sections
201
+ for (const name of COMMON_OPTIONAL_SECTIONS) {
202
+ sections[name] = '';
203
+ }
204
+ return sections;
205
+ };
@@ -0,0 +1,124 @@
1
+ /**
2
+ * PipelineStateStore — persists pipeline state to Turso via Drizzle.
3
+ * Dual-layer: in-memory Map for real-time updates, DB for persistence.
4
+ */
5
+
6
+ import { eq } from 'drizzle-orm';
7
+ import { pipelineState } from '../db/schema.js';
8
+
9
+ export class PipelineStateStore {
10
+ constructor({ getDb }) {
11
+ this.getDb = getDb;
12
+ }
13
+
14
+ /**
15
+ * Persist the current pipeline state for a feature.
16
+ * @param {string} featureId
17
+ * @param {{ status: string, cycle: number, tasks?: object, toolCalls?: object, error?: string }} state
18
+ */
19
+ save = async (featureId, state, projectId?: string) => {
20
+ const db = this.getDb();
21
+ const row = {
22
+ featureId,
23
+ status: state.status,
24
+ cycle: state.cycle || 1,
25
+ tasksJson: state.tasks ? JSON.stringify(state.tasks.items || []) : null,
26
+ toolCallsJson: state.toolCalls ? JSON.stringify(state.toolCalls) : null,
27
+ workSummariesJson: state.workSummaries ? JSON.stringify(state.workSummaries) : null,
28
+ error: state.error || null,
29
+ updatedAt: new Date().toISOString(),
30
+ projectId: projectId || null,
31
+ };
32
+
33
+ await db.insert(pipelineState)
34
+ .values(row)
35
+ .onConflictDoUpdate({
36
+ target: pipelineState.featureId,
37
+ set: {
38
+ status: row.status,
39
+ cycle: row.cycle,
40
+ tasksJson: row.tasksJson,
41
+ toolCallsJson: row.toolCallsJson,
42
+ workSummariesJson: row.workSummariesJson,
43
+ error: row.error,
44
+ updatedAt: row.updatedAt,
45
+ projectId: row.projectId,
46
+ },
47
+ });
48
+ };
49
+
50
+ /**
51
+ * Load persisted pipeline state for a feature.
52
+ * Returns null if no entry exists.
53
+ * @param {string} featureId
54
+ * @returns {Promise<object|null>}
55
+ */
56
+ load = async (featureId) => {
57
+ const db = this.getDb();
58
+ const rows = await db.select().from(pipelineState).where(eq(pipelineState.featureId, featureId));
59
+ const row = rows[0];
60
+ if (!row) return null;
61
+
62
+ const tasks = row.tasksJson ? JSON.parse(row.tasksJson) : [];
63
+ const toolCalls = row.toolCallsJson ? JSON.parse(row.toolCallsJson) : null;
64
+ const rawSummaries = row.workSummariesJson ? JSON.parse(row.workSummariesJson) : [];
65
+
66
+ // Migrate old flat filesChanged format to categorized format
67
+ const workSummaries = rawSummaries.map((ws) => {
68
+ if (ws.filesChanged && !ws.filesUpdated) {
69
+ return {
70
+ ...ws,
71
+ filesCreated: [],
72
+ filesUpdated: ws.filesChanged,
73
+ filesDeleted: [],
74
+ filesChanged: undefined,
75
+ };
76
+ }
77
+ return ws;
78
+ });
79
+
80
+ return {
81
+ status: row.status,
82
+ cycle: row.cycle,
83
+ feature_id: row.featureId,
84
+ error: row.error || null,
85
+ tasks: tasks.length > 0 ? {
86
+ completed: tasks.filter(t => t.status === 'completed').length,
87
+ total: tasks.length,
88
+ items: tasks,
89
+ } : undefined,
90
+ toolCalls: toolCalls && toolCalls.total > 0 ? toolCalls : undefined,
91
+ workSummaries: workSummaries.length > 0 ? workSummaries : undefined,
92
+ updatedAt: row.updatedAt,
93
+ };
94
+ };
95
+
96
+ /**
97
+ * Remove persisted pipeline state for a feature.
98
+ * @param {string} featureId
99
+ */
100
+ remove = async (featureId) => {
101
+ const db = this.getDb();
102
+ await db.delete(pipelineState).where(eq(pipelineState.featureId, featureId));
103
+ };
104
+
105
+ /**
106
+ * Mark any active (non-terminal) pipeline states as 'interrupted'.
107
+ * Called on server startup to handle pipelines that were running when the server stopped.
108
+ */
109
+ markInterrupted = async (projectId?: string) => {
110
+ const db = this.getDb();
111
+ const activeStatuses = ['developing', 'reviewing', 'fixing_review', 'fixing_merge', 'merging', 'debugging', 'creating_worktree'];
112
+ const query = projectId
113
+ ? db.select().from(pipelineState).where(eq(pipelineState.projectId, projectId))
114
+ : db.select().from(pipelineState);
115
+ const allRows = await query;
116
+ for (const row of allRows) {
117
+ if (activeStatuses.includes(row.status)) {
118
+ await db.update(pipelineState)
119
+ .set({ status: 'interrupted', updatedAt: new Date().toISOString() })
120
+ .where(eq(pipelineState.featureId, row.featureId));
121
+ }
122
+ }
123
+ };
124
+ }