@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,489 @@
1
+ /**
2
+ * Pipeline Orchestration Module.
3
+ * The full develop→review→merge cycle, using injected dependencies.
4
+ */
5
+
6
+ const TOOL_CALL_COUNTERS = { Write: 'write', Edit: 'edit', Read: 'read', Bash: 'bash' };
7
+
8
+ /**
9
+ * Pipeline class — orchestrates the develop→review→merge cycle.
10
+ * Receives PromptBuilder, GitWorkflow, kanban, stateStore, claudeService,
11
+ * and log via constructor injection.
12
+ */
13
+ export class Pipeline {
14
+ constructor({ promptBuilder, gitWorkflow, claudeService, kanban, paths, log, stateStore = null, workSummaryParser = null, getNode = null }) {
15
+ this.promptBuilder = promptBuilder;
16
+ this.gitWorkflow = gitWorkflow;
17
+ this.claudeService = claudeService;
18
+ this.kanban = kanban;
19
+ this.paths = paths;
20
+ this.log = log;
21
+ this.stateStore = stateStore;
22
+ this.workSummaryParser = workSummaryParser;
23
+ this.getNode = getNode;
24
+ this.pipelines = new Map();
25
+ this.lastLoggedStatus = new Map();
26
+ }
27
+
28
+ /** Persist state to DB if stateStore is available (fire-and-forget with logging). */
29
+ persistState = (featureId) => {
30
+ if (!this.stateStore) return;
31
+ const state = this.pipelines.get(featureId);
32
+ if (!state) return;
33
+ this.stateStore.save(featureId, state, state.projectId).catch(err => {
34
+ this.log('PERSIST', `Failed to persist state for ${featureId}: ${err.message}`);
35
+ });
36
+ };
37
+
38
+ run = async (featureId) => {
39
+ const state = this.pipelines.get(featureId);
40
+ if (!state) {
41
+ this.log('PIPELINE', `No pipeline state found for ${featureId}, aborting`);
42
+ return;
43
+ }
44
+
45
+ this.log('PIPELINE', `=== Starting pipeline for ${featureId} ===`);
46
+ this.log('PIPELINE', `Worktree: ${state.worktree_path}`);
47
+ let previousReviewNotes = null;
48
+ let debuggerFindings = null;
49
+
50
+ // Debug-first step: check for unaddressed QA notes before cycle 1
51
+ debuggerFindings = await this.runDebugStep(featureId, state);
52
+
53
+ for (let cycle = 1; cycle <= 3; cycle++) {
54
+ state.cycle = cycle;
55
+ this.log('PIPELINE', `--- Cycle ${cycle}/3 for ${featureId} ---`);
56
+
57
+ // 1. Move card to in_progress (on retries, reviewer left it in todo)
58
+ try {
59
+ const devEntry = await this.kanban.getKanbanEntry(featureId);
60
+ if (devEntry && devEntry.column !== 'in_progress') {
61
+ devEntry.column = 'in_progress';
62
+ devEntry.moved_at = new Date().toISOString();
63
+ await this.kanban.saveKanbanEntry(featureId, devEntry);
64
+ this.log('PIPELINE', `Moved ${featureId} → in_progress (cycle ${cycle})`);
65
+ }
66
+ } catch (err) {
67
+ this.log('PIPELINE', `WARN: Failed to move ${featureId} to in_progress: ${err.message}`);
68
+ }
69
+
70
+ // 2. Run developer
71
+ state.status = cycle === 1 ? 'developing' : 'fixing_review';
72
+ state.tasks = { completed: 0, total: 0, items: [] };
73
+ state.toolCalls = { total: 0, write: 0, edit: 0, read: 0, bash: 0 };
74
+ this.log('PIPELINE', `Status → ${state.status}`);
75
+ this.persistState(featureId);
76
+ try {
77
+ const devPrompt = await this.promptBuilder.buildDeveloperPrompt(featureId, cycle, previousReviewNotes, debuggerFindings, state.projectId);
78
+ if (cycle === 1) debuggerFindings = null;
79
+ this.log('PIPELINE', `Spawning developer Claude for ${featureId}...`);
80
+ const onToolUse = (toolName, input) => {
81
+ if (toolName === 'TodoWrite' && Array.isArray(input.todos)) {
82
+ state.tasks.items = input.todos.map(t => ({ name: t.content || t.activeForm || '', status: t.status || 'pending' }));
83
+ state.tasks.total = state.tasks.items.length;
84
+ state.tasks.completed = state.tasks.items.filter(t => t.status === 'completed').length;
85
+ }
86
+ state.toolCalls.total += 1;
87
+ const counterKey = TOOL_CALL_COUNTERS[toolName];
88
+ if (counterKey) {
89
+ state.toolCalls[counterKey] += 1;
90
+ }
91
+ this.persistState(featureId);
92
+ };
93
+ const devOutput = await this.claudeService.spawnClaude(devPrompt, state.worktree_path, `dev:${featureId}`, { onToolUse });
94
+ this.log('PIPELINE', `Developer Claude completed for ${featureId}`);
95
+
96
+ // Capture work summary from dev output and git diff --stat
97
+ await this.captureWorkSummary(featureId, state, cycle, devOutput);
98
+ } catch (err) {
99
+ this.log('PIPELINE', `Developer Claude FAILED for ${featureId}: ${err.message}`);
100
+ state.status = 'failed';
101
+ state.error = `Developer failed: ${err.message}`;
102
+ this.persistState(featureId);
103
+ return;
104
+ }
105
+
106
+ // 1b. Commit any uncommitted changes in the worktree
107
+ try {
108
+ await this.gitWorkflow.commitChanges(featureId, state.worktree_path);
109
+ } catch (commitErr) {
110
+ this.log('PIPELINE', `WARN: Auto-commit failed for ${featureId}: ${commitErr.message}`);
111
+ }
112
+
113
+ // 1c. Rebase feature branch onto current main
114
+ await this.gitWorkflow.rebaseBranch(featureId, state.worktree_path);
115
+
116
+ // 2. Pipeline moves card to in_review
117
+ this.log('PIPELINE', `Moving ${featureId} to in_review...`);
118
+ try {
119
+ const reviewEntry = await this.kanban.getKanbanEntry(featureId);
120
+ if (reviewEntry) {
121
+ reviewEntry.column = 'in_review';
122
+ reviewEntry.moved_at = new Date().toISOString();
123
+ await this.kanban.saveKanbanEntry(featureId, reviewEntry);
124
+ this.log('PIPELINE', `Moved ${featureId} → in_review`);
125
+ }
126
+ } catch (err) {
127
+ this.log('PIPELINE', `WARN: Failed to move ${featureId} to in_review: ${err.message}`);
128
+ }
129
+
130
+ // 3. Run reviewer
131
+ state.status = 'reviewing';
132
+ this.log('PIPELINE', `Status → reviewing`);
133
+ this.persistState(featureId);
134
+ try {
135
+ const revPrompt = await this.promptBuilder.buildReviewerPrompt(featureId, state.projectId);
136
+ this.log('PIPELINE', `Spawning reviewer Claude for ${featureId}...`);
137
+ await this.claudeService.spawnClaude(revPrompt, state.worktree_path, `review:${featureId}`);
138
+ this.log('PIPELINE', `Reviewer Claude completed for ${featureId}`);
139
+ } catch (err) {
140
+ this.log('PIPELINE', `Reviewer Claude FAILED for ${featureId}: ${err.message}`);
141
+ state.status = 'failed';
142
+ state.error = `Reviewer failed: ${err.message}`;
143
+ this.persistState(featureId);
144
+ return;
145
+ }
146
+
147
+ // 4. Check kanban outcome
148
+ this.log('PIPELINE', `Checking kanban outcome for ${featureId}...`);
149
+ let entry;
150
+ try { entry = await this.kanban.getKanbanEntry(featureId); } catch (err) {
151
+ this.log('PIPELINE', `FAIL reading kanban: ${err.message}`);
152
+ break;
153
+ }
154
+ if (!entry) {
155
+ this.log('PIPELINE', `Feature ${featureId} no longer in kanban — aborting`);
156
+ break;
157
+ }
158
+ this.log('PIPELINE', `Kanban column for ${featureId}: ${entry.column}`);
159
+
160
+ // If reviewer didn't move the card at all (still in_review), treat as rejection
161
+ if (entry.column === 'in_review') {
162
+ this.log('PIPELINE', `Reviewer did not move ${featureId} (still in_review) — treating as rejection`);
163
+ entry.column = 'todo';
164
+ entry.moved_at = new Date().toISOString();
165
+ await this.kanban.saveKanbanEntry(featureId, entry);
166
+ }
167
+
168
+ if (entry.column === 'qa') {
169
+ // Store approved review outcome
170
+ if (!entry.reviews) entry.reviews = [];
171
+ entry.reviews.push({ cycle, status: 'approved', timestamp: new Date().toISOString() });
172
+ await this.kanban.saveKanbanEntry(featureId, entry);
173
+ this.log('PIPELINE', `Review PASSED for ${featureId} — starting merge`);
174
+
175
+ await this.handleMerge(featureId, state);
176
+ return;
177
+ }
178
+
179
+ // Review rejected — collect notes for next cycle
180
+ this.log('PIPELINE', `Review REJECTED for ${featureId} (cycle ${cycle}) — card in column: ${entry.column}`);
181
+ const latestNotes = (entry.notes || []).map(n => n.text).join('\n');
182
+ previousReviewNotes = latestNotes || 'Review rejected without specific notes.';
183
+ this.log('PIPELINE', `Review notes for next cycle: ${previousReviewNotes.slice(0, 200)}`);
184
+
185
+ // Store rejected review outcome
186
+ if (!entry.reviews) entry.reviews = [];
187
+ entry.reviews.push({ cycle, status: 'rejected', reason: previousReviewNotes, timestamp: new Date().toISOString() });
188
+ await this.kanban.saveKanbanEntry(featureId, entry);
189
+ }
190
+
191
+ // All 3 cycles exhausted — block the card
192
+ await this.handleBlock(featureId, state);
193
+ };
194
+
195
+ /** Capture work summary after a developer cycle completes. */
196
+ captureWorkSummary = async (featureId, state, cycle, devOutput) => {
197
+ if (!this.workSummaryParser) return;
198
+ try {
199
+ // Get git diff --name-status comparing worktree branch to main
200
+ let diffNameStatus = '';
201
+ try {
202
+ diffNameStatus = await this.claudeService.spawnCommand('git', ['diff', '--name-status', 'main...HEAD'], state.worktree_path);
203
+ } catch (err) {
204
+ this.log('PIPELINE', `WARN: git diff --name-status failed for ${featureId}: ${err.message}`);
205
+ }
206
+
207
+ const summary = this.workSummaryParser.buildSummary(cycle, diffNameStatus, devOutput || '');
208
+ if (!state.workSummaries) state.workSummaries = [];
209
+ state.workSummaries.push(summary);
210
+ this.persistState(featureId);
211
+ const totalFiles = summary.filesCreated.length + summary.filesUpdated.length + summary.filesDeleted.length;
212
+ this.log('PIPELINE', `Work summary captured for ${featureId} cycle ${cycle}: ${totalFiles} files (${summary.filesCreated.length} created, ${summary.filesUpdated.length} updated, ${summary.filesDeleted.length} deleted), ${summary.decisions.length} decisions`);
213
+ } catch (err) {
214
+ this.log('PIPELINE', `WARN: Failed to capture work summary for ${featureId}: ${err.message}`);
215
+ }
216
+ };
217
+
218
+ runDebugStep = async (featureId, state) => {
219
+ try {
220
+ const debugEntry = await this.kanban.getKanbanEntry(featureId);
221
+ if (debugEntry && debugEntry.notes && debugEntry.notes.length > 0) {
222
+ const unaddressedNotes = debugEntry.notes.filter(n => !n.addressed);
223
+ if (unaddressedNotes.length > 0) {
224
+ this.log('PIPELINE', `Found ${unaddressedNotes.length} unaddressed QA notes for ${featureId} — running debugger first`);
225
+ state.status = 'debugging';
226
+ this.persistState(featureId);
227
+ let debuggerFindings = null;
228
+ try {
229
+ const debugPrompt = await this.promptBuilder.buildDebuggerPrompt(featureId, unaddressedNotes, state.projectId);
230
+ this.log('PIPELINE', `Spawning debugger Claude for ${featureId}...`);
231
+ debuggerFindings = await this.claudeService.spawnClaude(debugPrompt, state.worktree_path, `debug:${featureId}`);
232
+ this.log('PIPELINE', `Debugger Claude completed for ${featureId} — findings: ${(debuggerFindings || '').length} chars`);
233
+ } catch (err) {
234
+ this.log('PIPELINE', `Debugger Claude FAILED for ${featureId}: ${err.message} — continuing without findings`);
235
+ debuggerFindings = null;
236
+ }
237
+
238
+ // Mark processed notes as addressed
239
+ const postEntry = await this.kanban.getKanbanEntry(featureId);
240
+ if (postEntry && postEntry.notes) {
241
+ for (const note of postEntry.notes) {
242
+ if (!note.addressed) {
243
+ note.addressed = true;
244
+ }
245
+ }
246
+ await this.kanban.saveKanbanEntry(featureId, postEntry);
247
+ this.log('PIPELINE', `Marked ${unaddressedNotes.length} notes as addressed for ${featureId}`);
248
+ }
249
+ return debuggerFindings;
250
+ } else {
251
+ this.log('PIPELINE', `All QA notes for ${featureId} are already addressed — skipping debugger`);
252
+ }
253
+ } else {
254
+ this.log('PIPELINE', `No QA notes for ${featureId} — skipping debugger`);
255
+ }
256
+ } catch (err) {
257
+ this.log('PIPELINE', `WARN: Failed to check QA notes for debug step: ${err.message} — continuing without debugger`);
258
+ }
259
+ return null;
260
+ };
261
+
262
+ handleMerge = async (featureId, state) => {
263
+ state.status = 'merging';
264
+ this.log('PIPELINE', `Status → merging`);
265
+ this.persistState(featureId);
266
+
267
+ // Step 1: Stash any local changes on the parent branch before merging
268
+ let stashed = false;
269
+ try {
270
+ stashed = await this.gitWorkflow.stash();
271
+ } catch (stashErr) {
272
+ this.log('PIPELINE', `Stash failed (non-fatal): ${stashErr.message}`);
273
+ }
274
+
275
+ // Step 2: Merge the feature branch
276
+ let mergeSucceeded = false;
277
+ try {
278
+ await this.gitWorkflow.mergeBranch(featureId);
279
+ mergeSucceeded = true;
280
+ } catch (mergeErr) {
281
+ this.log('PIPELINE', `Merge FAILED: ${mergeErr.message} — attempting fix`);
282
+ state.status = 'fixing_merge';
283
+ this.log('PIPELINE', `Status → fixing_merge`);
284
+ this.persistState(featureId);
285
+ try {
286
+ const fixed = await this.gitWorkflow.fixMergeConflicts(featureId);
287
+ if (fixed) {
288
+ this.log('PIPELINE', `Merge fix succeeded — branch verified as merged`);
289
+ mergeSucceeded = true;
290
+ } else {
291
+ this.log('PIPELINE', `Merge fix agent ran but branch is NOT merged — treating as failure`);
292
+ state.status = 'failed';
293
+ state.error = `Merge fix did not result in a successful merge: ${mergeErr.message}`;
294
+ this.persistState(featureId);
295
+ }
296
+ } catch (fixErr) {
297
+ this.log('PIPELINE', `Merge fix FAILED: ${fixErr.message}`);
298
+ state.status = 'failed';
299
+ state.error = `Merge fix failed: ${mergeErr.message}`;
300
+ this.persistState(featureId);
301
+ }
302
+ }
303
+
304
+ // Step 3: Pop stash to restore local changes
305
+ if (stashed) {
306
+ try {
307
+ await this.gitWorkflow.unstash();
308
+ } catch (popErr) {
309
+ this.log('PIPELINE', `Stash pop failed — changes remain in stash: ${popErr.message}`);
310
+ }
311
+ }
312
+
313
+ // Step 4: Cleanup worktree + branch
314
+ if (mergeSucceeded) {
315
+ this.log('PIPELINE', `Cleaning up worktree and branch for ${featureId}...`);
316
+ await this.gitWorkflow.removeWorktree(state.worktree_path);
317
+ await this.gitWorkflow.deleteBranch(featureId);
318
+ state.status = 'completed';
319
+ this.log('PIPELINE', `=== Pipeline COMPLETED for ${featureId} ===`);
320
+ this.persistState(featureId);
321
+ }
322
+ };
323
+
324
+ handleBlock = async (featureId, state) => {
325
+ this.log('PIPELINE', `=== All 3 cycles exhausted for ${featureId} — BLOCKING ===`);
326
+ try {
327
+ const blockEntry = await this.kanban.getKanbanEntry(featureId);
328
+ if (blockEntry) {
329
+ blockEntry.dev_blocked = true;
330
+ await this.kanban.saveKanbanEntry(featureId, blockEntry);
331
+ this.log('PIPELINE', `Set dev_blocked=true for ${featureId}`);
332
+ }
333
+ this.log('PIPELINE', `Cleaning up worktree for ${featureId}...`);
334
+ await this.gitWorkflow.removeWorktree(state.worktree_path);
335
+ await this.gitWorkflow.deleteBranch(featureId, true);
336
+ } catch (err) {
337
+ this.log('PIPELINE', `Block/cleanup error (non-fatal): ${err.message}`);
338
+ }
339
+ state.status = 'blocked';
340
+ this.log('PIPELINE', `=== Pipeline BLOCKED for ${featureId} ===`);
341
+ this.persistState(featureId);
342
+ };
343
+
344
+ start = async (featureId) => {
345
+ this.log('API', `POST /api/kanban/${featureId}/develop`);
346
+
347
+ // Check for existing active pipeline
348
+ if (this.pipelines.has(featureId)) {
349
+ const existing = this.pipelines.get(featureId);
350
+ this.log('DEVELOP', `Existing pipeline for ${featureId}: status=${existing.status}`);
351
+ if (!['completed', 'blocked', 'failed'].includes(existing.status)) {
352
+ this.log('DEVELOP', `REJECT: pipeline already running for ${featureId}`);
353
+ return { error: `Pipeline already running for ${featureId}`, status: 409 };
354
+ }
355
+ }
356
+
357
+ const entry = await this.kanban.getKanbanEntry(featureId);
358
+
359
+ if (!entry) {
360
+ this.log('DEVELOP', `REJECT: ${featureId} not found in kanban`);
361
+ return { error: `Feature "${featureId}" not found in kanban`, status: 404 };
362
+ }
363
+
364
+ this.log('DEVELOP', `${featureId}: column=${entry.column} dev_blocked=${entry.dev_blocked || false}`);
365
+
366
+ if (entry.column !== 'todo') {
367
+ this.log('DEVELOP', `REJECT: ${featureId} not in todo (is in ${entry.column})`);
368
+ return { error: `Feature must be in "todo" column (currently "${entry.column}")`, status: 400 };
369
+ }
370
+ if (entry.dev_blocked) {
371
+ this.log('DEVELOP', `REJECT: ${featureId} is dev_blocked`);
372
+ return { error: `Feature "${featureId}" is dev_blocked. Unblock it first.`, status: 400 };
373
+ }
374
+
375
+ // Pre-flight: reject if working tree is dirty to avoid stash conflicts on merge
376
+ const dirtyFiles = await this.gitWorkflow.getDirtyFiles();
377
+ if (dirtyFiles.length > 0) {
378
+ this.log('DEVELOP', `REJECT: working tree has ${dirtyFiles.length} uncommitted change(s)`);
379
+ return { error: `Working tree has uncommitted changes. Commit or stash them before starting a pipeline.\n${dirtyFiles.join('\n')}`, status: 400 };
380
+ }
381
+
382
+ // Move to in_progress
383
+ entry.column = 'in_progress';
384
+ entry.moved_at = new Date().toISOString();
385
+ await this.kanban.saveKanbanEntry(featureId, entry);
386
+ this.log('DEVELOP', `Moved ${featureId} to in_progress`);
387
+
388
+ // Create worktree
389
+ let worktreePath;
390
+ try {
391
+ worktreePath = await this.gitWorkflow.createWorktree(featureId);
392
+ } catch (err) {
393
+ this.log('DEVELOP', `Worktree creation FAILED: ${err.message}`);
394
+ // Revert kanban change
395
+ entry.column = 'todo';
396
+ await this.kanban.saveKanbanEntry(featureId, entry);
397
+ this.log('DEVELOP', `Reverted ${featureId} back to todo`);
398
+ return { error: `Failed to create worktree: ${err.message}`, status: 500 };
399
+ }
400
+
401
+ // Look up the projectId from the feature node
402
+ let projectId = null;
403
+ if (this.getNode) {
404
+ try {
405
+ const node = await this.getNode(featureId);
406
+ if (node) projectId = node.project_id || null;
407
+ } catch {}
408
+ }
409
+
410
+ const state = { status: 'developing', cycle: 1, worktree_path: worktreePath, process: null, error: null, tasks: { completed: 0, total: 0, items: [] }, toolCalls: { total: 0, write: 0, edit: 0, read: 0, bash: 0 }, workSummaries: [], projectId };
411
+ this.pipelines.set(featureId, state);
412
+ this.persistState(featureId);
413
+ this.log('DEVELOP', `Pipeline state initialized for ${featureId}`);
414
+
415
+ // Start async pipeline (fire and forget)
416
+ this.log('DEVELOP', `Launching async pipeline for ${featureId}...`);
417
+ this.run(featureId).catch(err => {
418
+ this.log('DEVELOP', `Pipeline UNCAUGHT ERROR for ${featureId}: ${err.message}`);
419
+ const s = this.pipelines.get(featureId);
420
+ if (s) { s.status = 'failed'; s.error = err.message; this.persistState(featureId); }
421
+ });
422
+
423
+ return { started: true, feature_id: featureId, status: 200 };
424
+ };
425
+
426
+ getStatus = async (featureId) => {
427
+ const state = this.pipelines.get(featureId);
428
+ if (!state) {
429
+ // Fall back to DB if no in-memory state
430
+ if (this.stateStore) {
431
+ try {
432
+ const dbState = await this.stateStore.load(featureId);
433
+ if (dbState) return dbState;
434
+ } catch (err) {
435
+ this.log('STATUS', `Failed to load DB state for ${featureId}: ${err.message}`);
436
+ }
437
+ }
438
+ return { status: 'idle', feature_id: featureId };
439
+ }
440
+
441
+ // Only log when status or cycle changes
442
+ const key = `${state.status}:${state.cycle}`;
443
+ if (this.lastLoggedStatus.get(featureId) !== key) {
444
+ this.log('STATUS', `${featureId}: status=${state.status} cycle=${state.cycle} error=${state.error || 'none'}`);
445
+ this.lastLoggedStatus.set(featureId, key);
446
+ }
447
+
448
+ const result = {
449
+ status: state.status,
450
+ cycle: state.cycle,
451
+ feature_id: featureId,
452
+ error: state.error || null,
453
+ };
454
+ if (state.tasks && state.tasks.total > 0) {
455
+ result.tasks = state.tasks;
456
+ }
457
+ if (state.toolCalls && state.toolCalls.total > 0) {
458
+ result.toolCalls = state.toolCalls;
459
+ }
460
+ if (state.workSummaries && state.workSummaries.length > 0) {
461
+ result.workSummaries = state.workSummaries;
462
+ }
463
+ return result;
464
+ };
465
+
466
+ unblock = async (featureId) => {
467
+ this.log('API', `POST /api/kanban/${featureId}/unblock`);
468
+
469
+ const entry = await this.kanban.getKanbanEntry(featureId);
470
+ if (!entry) {
471
+ this.log('UNBLOCK', `REJECT: ${featureId} not found`);
472
+ return { error: `Feature "${featureId}" not found in kanban`, status: 404 };
473
+ }
474
+
475
+ this.log('UNBLOCK', `${featureId}: dev_blocked was ${entry.dev_blocked || false}`);
476
+ delete entry.dev_blocked;
477
+ await this.kanban.saveKanbanEntry(featureId, entry);
478
+
479
+ // Clear blocked pipeline state
480
+ const state = this.pipelines.get(featureId);
481
+ if (state && state.status === 'blocked') {
482
+ this.pipelines.delete(featureId);
483
+ this.log('UNBLOCK', `Cleared blocked pipeline state for ${featureId}`);
484
+ }
485
+
486
+ this.log('UNBLOCK', `OK: ${featureId} unblocked`);
487
+ return { unblocked: true, feature_id: featureId, status: 200 };
488
+ };
489
+ }
@@ -0,0 +1,170 @@
1
+ /**
2
+ * PromptBuilder — responsible for building developer, reviewer, and debugger
3
+ * prompts from skill files and context.
4
+ */
5
+
6
+ import { readFile } from 'node:fs/promises';
7
+
8
+ export class PromptBuilder {
9
+ constructor({ paths, log }) {
10
+ this.paths = paths;
11
+ this.log = log;
12
+ }
13
+
14
+ buildDeveloperPrompt = async (featureId, cycle, previousReviewNotes, debuggerFindings = null, projectId = null) => {
15
+ this.log('PROMPT', `Building developer prompt for ${featureId} cycle=${cycle} hasReviewNotes=${!!previousReviewNotes} hasDebugFindings=${!!debuggerFindings}`);
16
+ let skillContent = '';
17
+ try { skillContent = await readFile(this.paths.developerSkillPath, 'utf-8'); } catch { this.log('PROMPT', 'WARN: Could not read developer SKILL.md'); }
18
+
19
+ const mainToolsDir = this.paths.toolsDir;
20
+ const mainSkillDir = this.paths.dataDir;
21
+
22
+ let prompt = skillContent + '\n\n';
23
+
24
+ prompt += `## CRITICAL: Data Isolation\n`;
25
+ prompt += `You are working in a git worktree. The worktree is ONLY for code changes.\n`;
26
+ prompt += `All kanban, graph, and spec data lives in the MAIN REPO — never use the worktree copy.\n`;
27
+ prompt += `\n`;
28
+ prompt += `**All tool commands MUST use absolute paths to the main repo's tools:**\n`;
29
+ prompt += `- Tools directory: ${mainToolsDir}\n`;
30
+ prompt += `- All tool commands MUST be run from: ${mainSkillDir}\n`;
31
+ const pidFlag = projectId ? ` --project-id ${projectId}` : '';
32
+ prompt += `- Example: cd ${mainSkillDir} && node tools/get_node.ts ${featureId}${pidFlag}\n`;
33
+ prompt += `- Example: cd ${mainSkillDir} && node tools/move_card.ts ${featureId} in_review${pidFlag}\n`;
34
+ prompt += `\n`;
35
+ prompt += `NEVER run tools from the worktree directory. NEVER use relative paths like \`product-system/tools/\`.\n`;
36
+ prompt += `ALWAYS \`cd ${mainSkillDir}\` before running any tool command.\n`;
37
+ if (projectId) {
38
+ prompt += `ALWAYS include --project-id ${projectId} when running any tool command.\n`;
39
+ }
40
+ prompt += `\n`;
41
+
42
+ prompt += `## Development Guidelines\n`;
43
+ prompt += `Before writing any code, load the development guidelines:\n`;
44
+ prompt += ` cd ${mainSkillDir} && node tools/get_node.ts nfr_001${pidFlag}\n`;
45
+ prompt += `These guidelines define mandatory coding standards (class-based architecture, arrow functions, constructor DI, naming conventions). All code you write MUST follow them.\n\n`;
46
+
47
+ prompt += `## Current Task\n`;
48
+ prompt += `Implement feature ${featureId}. Read the feature spec using get_node, understand all related nodes, and implement it.\n`;
49
+
50
+ if (debuggerFindings) {
51
+ prompt += `\n## Debugger Investigation Findings\n`;
52
+ prompt += `A debugger agent has investigated the QA rejection notes and identified root causes.\n`;
53
+ prompt += `Use these findings to fix the actual root causes rather than just the symptom descriptions:\n\n`;
54
+ prompt += debuggerFindings + '\n';
55
+ this.log('PROMPT', `Included debugger findings — ${debuggerFindings.length} chars`);
56
+ }
57
+
58
+ if (cycle > 1 && previousReviewNotes) {
59
+ prompt += `\n## Previous Review Feedback (Cycle ${cycle})\n`;
60
+ prompt += `The previous implementation was rejected. Fix the following issues:\n`;
61
+ prompt += previousReviewNotes + '\n';
62
+ this.log('PROMPT', `Included review feedback for cycle ${cycle}`);
63
+ }
64
+
65
+ prompt += `\nWhen done implementing, do NOT move the card yourself — the pipeline will handle card transitions.\n`;
66
+
67
+ prompt += `\n## REQUIRED: Work Summary Output\n`;
68
+ prompt += `After completing your implementation, you MUST output a structured work summary block as your final text output.\n`;
69
+ prompt += `The pipeline will parse this block to record what was done. Use this exact format:\n\n`;
70
+ prompt += '```\n';
71
+ prompt += `## Work Summary\n`;
72
+ prompt += `### Approach\n`;
73
+ prompt += `<A short paragraph explaining what you implemented and why you chose this approach.>\n`;
74
+ prompt += `### Decisions\n`;
75
+ prompt += `- <Key decision 1 you made during implementation>\n`;
76
+ prompt += `- <Key decision 2>\n`;
77
+ prompt += '```\n\n';
78
+ prompt += `This block MUST appear in your final output. Do not skip it.\n`;
79
+
80
+ this.log('PROMPT', `Developer prompt built — ${prompt.length} chars`);
81
+ return prompt;
82
+ };
83
+
84
+ buildDebuggerPrompt = async (featureId, unaddressedNotes, projectId = null) => {
85
+ this.log('PROMPT', `Building debugger prompt for ${featureId} with ${unaddressedNotes.length} unaddressed notes`);
86
+ let skillContent = '';
87
+ try { skillContent = await readFile(this.paths.debuggerSkillPath, 'utf-8'); } catch { this.log('PROMPT', 'WARN: Could not read debugger SKILL.md'); }
88
+
89
+ const mainToolsDir = this.paths.toolsDir;
90
+ const mainSkillDir = this.paths.dataDir;
91
+
92
+ let prompt = skillContent + '\n\n';
93
+
94
+ prompt += `## CRITICAL: Data Isolation\n`;
95
+ prompt += `You are investigating bugs in a git worktree. The worktree is ONLY for code changes.\n`;
96
+ prompt += `All kanban, graph, and spec data lives in the MAIN REPO — never use the worktree copy.\n`;
97
+ prompt += `\n`;
98
+ prompt += `**All tool commands MUST use absolute paths to the main repo's tools:**\n`;
99
+ prompt += `- Tools directory: ${mainToolsDir}\n`;
100
+ prompt += `- All tool commands MUST be run from: ${mainSkillDir}\n`;
101
+ const pidFlag = projectId ? ` --project-id ${projectId}` : '';
102
+ prompt += `- Example: cd ${mainSkillDir} && node tools/get_node.ts ${featureId}${pidFlag}\n`;
103
+ prompt += `\n`;
104
+ prompt += `NEVER run tools from the worktree directory. NEVER use relative paths.\n`;
105
+ prompt += `ALWAYS \`cd ${mainSkillDir}\` before running any tool command.\n`;
106
+ if (projectId) {
107
+ prompt += `ALWAYS include --project-id ${projectId} when running any tool command.\n`;
108
+ }
109
+ prompt += `\n`;
110
+
111
+ prompt += `## Current Task\n`;
112
+ prompt += `You are running as part of an automated pipeline. Do NOT create bugfix features or move kanban cards.\n`;
113
+ prompt += `Instead, investigate the QA rejection notes below and output your findings as a structured report.\n\n`;
114
+ prompt += `Feature: ${featureId}\n\n`;
115
+ prompt += `## QA Rejection Notes to Investigate\n`;
116
+ for (const note of unaddressedNotes) {
117
+ prompt += `- [${note.id || 'no-id'}] ${note.text}\n`;
118
+ }
119
+ prompt += `\n`;
120
+ prompt += `## Instructions\n`;
121
+ prompt += `1. Read the feature spec using get_node to understand what the feature should do\n`;
122
+ prompt += `2. Read all related nodes to understand the full context\n`;
123
+ prompt += `3. Trace through the code to find the root cause of each reported issue\n`;
124
+ prompt += `4. Output a structured report with your findings. For each issue:\n`;
125
+ prompt += ` - The original QA note text\n`;
126
+ prompt += ` - Root cause analysis with specific file paths and line numbers\n`;
127
+ prompt += ` - Why the current code fails\n`;
128
+ prompt += ` - Specific guidance on what needs to change to fix it\n`;
129
+ prompt += `5. Do NOT create bugfix features, do NOT move kanban cards, do NOT write code fixes\n`;
130
+
131
+ this.log('PROMPT', `Debugger prompt built — ${prompt.length} chars`);
132
+ return prompt;
133
+ };
134
+
135
+ buildReviewerPrompt = async (featureId, projectId = null) => {
136
+ this.log('PROMPT', `Building reviewer prompt for ${featureId}`);
137
+ let skillContent = '';
138
+ try { skillContent = await readFile(this.paths.reviewerSkillPath, 'utf-8'); } catch { this.log('PROMPT', 'WARN: Could not read reviewer SKILL.md'); }
139
+
140
+ const mainToolsDir = this.paths.toolsDir;
141
+ const mainSkillDir = this.paths.dataDir;
142
+
143
+ let prompt = skillContent + '\n\n';
144
+
145
+ prompt += `## CRITICAL: Data Isolation\n`;
146
+ prompt += `You are reviewing code in a git worktree. The worktree is ONLY for code changes.\n`;
147
+ prompt += `All kanban, graph, and spec data lives in the MAIN REPO — never use the worktree copy.\n`;
148
+ prompt += `\n`;
149
+ prompt += `**All tool commands MUST use absolute paths to the main repo's tools:**\n`;
150
+ prompt += `- Tools directory: ${mainToolsDir}\n`;
151
+ prompt += `- All tool commands MUST be run from: ${mainSkillDir}\n`;
152
+ const pidFlag = projectId ? ` --project-id ${projectId}` : '';
153
+ prompt += `- Example: cd ${mainSkillDir} && node tools/get_node.ts ${featureId}${pidFlag}\n`;
154
+ prompt += `- Example: cd ${mainSkillDir} && node tools/move_card.ts ${featureId} qa${pidFlag}\n`;
155
+ prompt += `\n`;
156
+ prompt += `NEVER run tools from the worktree directory. NEVER use relative paths like \`product-system/tools/\`.\n`;
157
+ prompt += `ALWAYS \`cd ${mainSkillDir}\` before running any tool command.\n`;
158
+ if (projectId) {
159
+ prompt += `ALWAYS include --project-id ${projectId} when running any tool command.\n`;
160
+ }
161
+ prompt += `\n`;
162
+
163
+ prompt += `## Current Task\n`;
164
+ prompt += `Review feature ${featureId}. Read the feature spec using get_node, understand all related nodes, and review the implementation.\n`;
165
+ prompt += `If the implementation passes review, approve it by running: cd ${mainSkillDir} && node tools/move_card.ts ${featureId} qa${pidFlag}\n`;
166
+ prompt += `If it fails, reject it with a note: cd ${mainSkillDir} && node tools/move_card.ts ${featureId} todo${pidFlag} --note "description of issues"\n`;
167
+ this.log('PROMPT', `Reviewer prompt built — ${prompt.length} chars`);
168
+ return prompt;
169
+ };
170
+ }