@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.
- package/dist/bin/create.d.ts +2 -0
- package/dist/bin/create.js +25 -0
- package/dist/bin/create.js.map +1 -0
- package/dist/src/scaffolder.d.ts +22 -0
- package/dist/src/scaffolder.js +120 -0
- package/dist/src/scaffolder.js.map +1 -0
- package/package.json +24 -0
- package/templates/product-system/.env.example +8 -0
- package/templates/product-system/CLAUDE.md +45 -0
- package/templates/product-system/package.json +32 -0
- package/templates/product-system/packages/backend/package.json +37 -0
- package/templates/product-system/packages/backend/src/middleware/auth_middleware.test.ts +86 -0
- package/templates/product-system/packages/backend/src/middleware/auth_middleware.ts +35 -0
- package/templates/product-system/packages/backend/src/routes/auth.ts +463 -0
- package/templates/product-system/packages/backend/src/routes/coherence.ts +187 -0
- package/templates/product-system/packages/backend/src/routes/graph.ts +67 -0
- package/templates/product-system/packages/backend/src/routes/kanban.ts +201 -0
- package/templates/product-system/packages/backend/src/routes/pipeline.ts +41 -0
- package/templates/product-system/packages/backend/src/routes/projects.ts +122 -0
- package/templates/product-system/packages/backend/src/routes/users.ts +97 -0
- package/templates/product-system/packages/backend/src/server.ts +159 -0
- package/templates/product-system/packages/backend/src/services/auth_service.test.ts +115 -0
- package/templates/product-system/packages/backend/src/services/auth_service.ts +82 -0
- package/templates/product-system/packages/backend/src/services/coherence-review.ts +339 -0
- package/templates/product-system/packages/backend/src/services/email_service.ts +75 -0
- package/templates/product-system/packages/backend/src/services/init.ts +80 -0
- package/templates/product-system/packages/backend/src/services/invitation_service.test.ts +235 -0
- package/templates/product-system/packages/backend/src/services/invitation_service.ts +193 -0
- package/templates/product-system/packages/backend/src/services/password_reset_service.test.ts +151 -0
- package/templates/product-system/packages/backend/src/services/password_reset_service.ts +135 -0
- package/templates/product-system/packages/backend/src/services/project_service.test.ts +215 -0
- package/templates/product-system/packages/backend/src/services/project_service.ts +171 -0
- package/templates/product-system/packages/backend/src/services/pty_session_manager.test.ts +88 -0
- package/templates/product-system/packages/backend/src/services/pty_session_manager.ts +279 -0
- package/templates/product-system/packages/backend/src/services/terminal_ws_handler.ts +133 -0
- package/templates/product-system/packages/backend/src/services/user_management_service.test.ts +158 -0
- package/templates/product-system/packages/backend/src/services/user_management_service.ts +128 -0
- package/templates/product-system/packages/backend/tsconfig.json +22 -0
- package/templates/product-system/packages/frontend/index.html +13 -0
- package/templates/product-system/packages/frontend/package-lock.json +2666 -0
- package/templates/product-system/packages/frontend/package.json +30 -0
- package/templates/product-system/packages/frontend/public/favicon.svg +16 -0
- package/templates/product-system/packages/frontend/src/App.tsx +29 -0
- package/templates/product-system/packages/frontend/src/api/client.ts +386 -0
- package/templates/product-system/packages/frontend/src/api/client_projects.test.ts +104 -0
- package/templates/product-system/packages/frontend/src/api/client_refresh.test.ts +145 -0
- package/templates/product-system/packages/frontend/src/components/CoherenceView.tsx +414 -0
- package/templates/product-system/packages/frontend/src/components/GraphLegend.tsx +124 -0
- package/templates/product-system/packages/frontend/src/components/GraphSettings.tsx +112 -0
- package/templates/product-system/packages/frontend/src/components/GraphView.tsx +370 -0
- package/templates/product-system/packages/frontend/src/components/InviteUserDialog.tsx +85 -0
- package/templates/product-system/packages/frontend/src/components/KanbanView.tsx +470 -0
- package/templates/product-system/packages/frontend/src/components/LoginPage.tsx +116 -0
- package/templates/product-system/packages/frontend/src/components/ProjectSelector.tsx +187 -0
- package/templates/product-system/packages/frontend/src/components/QaIssueSheet.tsx +192 -0
- package/templates/product-system/packages/frontend/src/components/SidePanel.tsx +231 -0
- package/templates/product-system/packages/frontend/src/components/TerminalView.tsx +200 -0
- package/templates/product-system/packages/frontend/src/components/Toolbar.tsx +84 -0
- package/templates/product-system/packages/frontend/src/components/UsersView.tsx +249 -0
- package/templates/product-system/packages/frontend/src/constants/graph.ts +191 -0
- package/templates/product-system/packages/frontend/src/hooks/useAuth.tsx +54 -0
- package/templates/product-system/packages/frontend/src/hooks/useGraph.ts +27 -0
- package/templates/product-system/packages/frontend/src/hooks/useKanban.ts +21 -0
- package/templates/product-system/packages/frontend/src/hooks/useProjects.ts +86 -0
- package/templates/product-system/packages/frontend/src/hooks/useTheme.ts +26 -0
- package/templates/product-system/packages/frontend/src/hooks/useToast.tsx +62 -0
- package/templates/product-system/packages/frontend/src/hooks/use_projects_logic.test.ts +61 -0
- package/templates/product-system/packages/frontend/src/main.tsx +12 -0
- package/templates/product-system/packages/frontend/src/pages/accept_invitation_page.tsx +167 -0
- package/templates/product-system/packages/frontend/src/pages/forgot_password_page.tsx +100 -0
- package/templates/product-system/packages/frontend/src/pages/register_page.tsx +137 -0
- package/templates/product-system/packages/frontend/src/pages/reset_password_page.tsx +146 -0
- package/templates/product-system/packages/frontend/src/routes/ProtectedRoute.tsx +12 -0
- package/templates/product-system/packages/frontend/src/routes/accept_invitation.tsx +14 -0
- package/templates/product-system/packages/frontend/src/routes/dashboard.tsx +221 -0
- package/templates/product-system/packages/frontend/src/routes/forgot_password.tsx +13 -0
- package/templates/product-system/packages/frontend/src/routes/login.tsx +14 -0
- package/templates/product-system/packages/frontend/src/routes/register.tsx +14 -0
- package/templates/product-system/packages/frontend/src/routes/reset_password.tsx +13 -0
- package/templates/product-system/packages/frontend/src/styles/index.css +3358 -0
- package/templates/product-system/packages/frontend/src/utils/auth_validation.test.ts +51 -0
- package/templates/product-system/packages/frontend/src/utils/auth_validation.ts +19 -0
- package/templates/product-system/packages/frontend/src/utils/login_validation.test.ts +61 -0
- package/templates/product-system/packages/frontend/src/utils/login_validation.ts +24 -0
- package/templates/product-system/packages/frontend/src/utils/logout.test.ts +63 -0
- package/templates/product-system/packages/frontend/src/utils/node_sizing.test.ts +62 -0
- package/templates/product-system/packages/frontend/src/utils/node_sizing.ts +24 -0
- package/templates/product-system/packages/frontend/src/utils/task_status.test.ts +53 -0
- package/templates/product-system/packages/frontend/src/utils/task_status.ts +14 -0
- package/templates/product-system/packages/frontend/tsconfig.json +21 -0
- package/templates/product-system/packages/frontend/vite.config.ts +20 -0
- package/templates/product-system/packages/shared/.env.example +3 -0
- package/templates/product-system/packages/shared/README.md +1 -0
- package/templates/product-system/packages/shared/db/migrate.ts +32 -0
- package/templates/product-system/packages/shared/db/migrations/0000_dashing_gorgon.sql +128 -0
- package/templates/product-system/packages/shared/db/migrations/meta/0000_snapshot.json +819 -0
- package/templates/product-system/packages/shared/db/migrations/meta/_journal.json +13 -0
- package/templates/product-system/packages/shared/db/schema.ts +137 -0
- package/templates/product-system/packages/shared/drizzle.config.js +14 -0
- package/templates/product-system/packages/shared/lib/claude-service.ts +215 -0
- package/templates/product-system/packages/shared/lib/coherence.ts +278 -0
- package/templates/product-system/packages/shared/lib/completeness.ts +30 -0
- package/templates/product-system/packages/shared/lib/constants.ts +327 -0
- package/templates/product-system/packages/shared/lib/db.ts +81 -0
- package/templates/product-system/packages/shared/lib/git_workflow.ts +110 -0
- package/templates/product-system/packages/shared/lib/graph.ts +186 -0
- package/templates/product-system/packages/shared/lib/kanban.ts +161 -0
- package/templates/product-system/packages/shared/lib/markdown.ts +205 -0
- package/templates/product-system/packages/shared/lib/pipeline-state-store.ts +124 -0
- package/templates/product-system/packages/shared/lib/pipeline.ts +489 -0
- package/templates/product-system/packages/shared/lib/prompt_builder.ts +170 -0
- package/templates/product-system/packages/shared/lib/relevance_search.ts +159 -0
- package/templates/product-system/packages/shared/lib/session.ts +152 -0
- package/templates/product-system/packages/shared/lib/validator.ts +117 -0
- package/templates/product-system/packages/shared/lib/work_summary_parser.ts +130 -0
- package/templates/product-system/packages/shared/package.json +30 -0
- package/templates/product-system/packages/shared/scripts/assign-project.ts +52 -0
- package/templates/product-system/packages/shared/tools/add_edge.ts +61 -0
- package/templates/product-system/packages/shared/tools/add_node.ts +101 -0
- package/templates/product-system/packages/shared/tools/end_session.ts +87 -0
- package/templates/product-system/packages/shared/tools/get_gaps.ts +87 -0
- package/templates/product-system/packages/shared/tools/get_kanban.ts +125 -0
- package/templates/product-system/packages/shared/tools/get_node.ts +78 -0
- package/templates/product-system/packages/shared/tools/get_status.ts +98 -0
- package/templates/product-system/packages/shared/tools/migrate_to_turso.ts +385 -0
- package/templates/product-system/packages/shared/tools/move_card.ts +143 -0
- package/templates/product-system/packages/shared/tools/rebuild_index.ts +77 -0
- package/templates/product-system/packages/shared/tools/remove_edge.ts +59 -0
- package/templates/product-system/packages/shared/tools/remove_node.ts +96 -0
- package/templates/product-system/packages/shared/tools/resolve_question.ts +75 -0
- package/templates/product-system/packages/shared/tools/search_nodes.ts +106 -0
- package/templates/product-system/packages/shared/tools/start_session.ts +144 -0
- package/templates/product-system/packages/shared/tools/update_node.ts +133 -0
- package/templates/product-system/packages/shared/tsconfig.json +24 -0
- package/templates/product-system/pnpm-workspace.yaml +2 -0
- package/templates/product-system/smoke_test.ts +219 -0
- package/templates/product-system/tests/coherence_review.test.ts +562 -0
- package/templates/product-system/tests/db_sqlite_fallback.test.ts +75 -0
- package/templates/product-system/tests/edge_type_color_coding.test.ts +147 -0
- package/templates/product-system/tests/emit-tool-use-events.test.ts +85 -0
- package/templates/product-system/tests/feature_kind.test.ts +139 -0
- package/templates/product-system/tests/gap_indicators.test.ts +199 -0
- package/templates/product-system/tests/graceful_init.test.ts +142 -0
- package/templates/product-system/tests/graph_legend.test.ts +314 -0
- package/templates/product-system/tests/graph_settings_sheet.test.ts +804 -0
- package/templates/product-system/tests/hide_defined_filter.test.ts +205 -0
- package/templates/product-system/tests/kanban.test.ts +529 -0
- package/templates/product-system/tests/neighborhood_focus.test.ts +132 -0
- package/templates/product-system/tests/node_search.test.ts +340 -0
- package/templates/product-system/tests/node_sizing.test.ts +170 -0
- package/templates/product-system/tests/node_type_toggle_filters.test.ts +285 -0
- package/templates/product-system/tests/node_type_visual_encoding.test.ts +103 -0
- package/templates/product-system/tests/pipeline-state-store.test.ts +268 -0
- package/templates/product-system/tests/pipeline-unit.test.ts +593 -0
- package/templates/product-system/tests/pipeline.test.ts +195 -0
- package/templates/product-system/tests/pipeline_stats_all_cards.test.ts +193 -0
- package/templates/product-system/tests/play_all.test.ts +296 -0
- package/templates/product-system/tests/qa_issue_sheet.test.ts +464 -0
- package/templates/product-system/tests/relevance_search.test.ts +186 -0
- package/templates/product-system/tests/search_reorder.test.ts +88 -0
- package/templates/product-system/tests/serve_ui.test.ts +281 -0
- package/templates/product-system/tests/serve_ui_drizzle.test.ts +114 -0
- package/templates/product-system/tests/session_context_recall.test.ts +135 -0
- package/templates/product-system/tests/side_panel.test.ts +345 -0
- package/templates/product-system/tests/spec_completeness_label.test.ts +69 -0
- package/templates/product-system/tests/url_routing_test.ts +122 -0
- package/templates/product-system/tests/user_login.test.ts +150 -0
- package/templates/product-system/tests/user_registration.test.ts +205 -0
- package/templates/product-system/tests/web_terminal.test.ts +572 -0
- package/templates/product-system/tests/work_summary.test.ts +211 -0
- package/templates/product-system/tests/zoom_pan.test.ts +43 -0
- package/templates/product-system/tsconfig.json +24 -0
- package/templates/skills/product-bootstrap/SKILL.md +312 -0
- package/templates/skills/product-code-reviewer/SKILL.md +147 -0
- package/templates/skills/product-debugger/SKILL.md +206 -0
- package/templates/skills/product-debugger/references/agent-browser.md +1156 -0
- package/templates/skills/product-developer/SKILL.md +182 -0
- 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
|
+
}
|