@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,593 @@
1
+ /**
2
+ * Unit tests for lib/pipeline.js with mocked dependencies.
3
+ * Uses node:test built-in runner + assert (matching existing test patterns).
4
+ */
5
+ import { describe, it, beforeEach } from 'node:test';
6
+ import assert from 'node:assert/strict';
7
+ import { join, dirname } from 'node:path';
8
+ import { fileURLToPath } from 'node:url';
9
+ import { Pipeline } from '../packages/shared/lib/pipeline.js';
10
+ import { PromptBuilder } from '../packages/shared/lib/prompt_builder.js';
11
+ import { GitWorkflow } from '../packages/shared/lib/git_workflow.js';
12
+
13
+ const __dirname = dirname(fileURLToPath(import.meta.url));
14
+ const SKILL_DIR = join(__dirname, '..');
15
+
16
+ // --- Mock factories ---
17
+
18
+ const createMockKanban = (initialEntries = {}) => {
19
+ const store = new Map(Object.entries(initialEntries));
20
+ return {
21
+ getKanbanEntry: async (id) => {
22
+ const entry = store.get(id);
23
+ // Return a deep copy so mutations don't affect the store directly
24
+ return entry ? JSON.parse(JSON.stringify(entry)) : null;
25
+ },
26
+ saveKanbanEntry: async (id, entry) => {
27
+ store.set(id, JSON.parse(JSON.stringify(entry)));
28
+ },
29
+ // Helper to read current state in assertions
30
+ _get: (id) => store.get(id),
31
+ };
32
+ };
33
+
34
+ const createMockClaudeService = (overrides = {}) => {
35
+ const calls = { spawnClaude: [], spawnCommand: [] };
36
+ return {
37
+ spawnClaude: overrides.spawnClaude || (async (prompt, cwd, label) => {
38
+ calls.spawnClaude.push({ prompt, cwd, label });
39
+ return 'mock output';
40
+ }),
41
+ spawnCommand: overrides.spawnCommand || (async (cmd, args, cwd) => {
42
+ calls.spawnCommand.push({ cmd, args, cwd });
43
+ return '';
44
+ }),
45
+ _calls: calls,
46
+ };
47
+ };
48
+
49
+ const mockPaths = {
50
+ projectRoot: '/mock/project',
51
+ worktreesDir: '/mock/worktrees',
52
+ skillsDir: join(SKILL_DIR, '..', '.claude', 'skills'),
53
+ toolsDir: join(SKILL_DIR, 'tools'),
54
+ dataDir: SKILL_DIR,
55
+ developerSkillPath: join(SKILL_DIR, '..', '.claude', 'skills', 'product-developer', 'SKILL.md'),
56
+ reviewerSkillPath: join(SKILL_DIR, '..', '.claude', 'skills', 'product-code-reviewer', 'SKILL.md'),
57
+ debuggerSkillPath: join(SKILL_DIR, '..', '.claude', 'skills', 'product-debugger', 'SKILL.md'),
58
+ };
59
+
60
+ const noop = () => {};
61
+
62
+ /** Create a Pipeline instance with mock dependencies. */
63
+ const createTestPipeline = (mockClaude, mockKanban) => {
64
+ const promptBuilder = new PromptBuilder({ paths: mockPaths, log: noop });
65
+ const gitWorkflow = new GitWorkflow({ claudeService: mockClaude, projectRoot: mockPaths.projectRoot, worktreesDir: mockPaths.worktreesDir, log: noop });
66
+ return new Pipeline({
67
+ promptBuilder,
68
+ gitWorkflow,
69
+ claudeService: mockClaude,
70
+ kanban: mockKanban,
71
+ paths: mockPaths,
72
+ log: noop,
73
+ });
74
+ };
75
+
76
+ // --- Tests ---
77
+
78
+ describe('pipeline.start() validation', () => {
79
+ it('rejects if feature not found in kanban', async () => {
80
+ const mockKanban = createMockKanban({});
81
+ const mockClaude = createMockClaudeService();
82
+ const pipeline = createTestPipeline(mockClaude, mockKanban);
83
+
84
+ const result = await pipeline.start('feat_999');
85
+ assert.equal(result.status, 404);
86
+ assert.ok(result.error.includes('feat_999'));
87
+ });
88
+
89
+ it('rejects if feature not in todo column', async () => {
90
+ const mockKanban = createMockKanban({
91
+ feat_010: { column: 'in_progress', rejection_count: 0, notes: [] },
92
+ });
93
+ const mockClaude = createMockClaudeService();
94
+ const pipeline = createTestPipeline(mockClaude, mockKanban);
95
+
96
+ const result = await pipeline.start('feat_010');
97
+ assert.equal(result.status, 400);
98
+ assert.ok(result.error.includes('todo'));
99
+ });
100
+
101
+ it('rejects if feature is dev_blocked', async () => {
102
+ const mockKanban = createMockKanban({
103
+ feat_010: { column: 'todo', rejection_count: 0, notes: [], dev_blocked: true },
104
+ });
105
+ const mockClaude = createMockClaudeService();
106
+ const pipeline = createTestPipeline(mockClaude, mockKanban);
107
+
108
+ const result = await pipeline.start('feat_010');
109
+ assert.equal(result.status, 400);
110
+ assert.ok(result.error.includes('dev_blocked'));
111
+ });
112
+
113
+ it('rejects if pipeline already running', async () => {
114
+ const mockKanban = createMockKanban({
115
+ feat_010: { column: 'todo', rejection_count: 0, notes: [] },
116
+ });
117
+ // spawnCommand succeeds for worktree creation, spawnClaude never resolves (simulates running)
118
+ const mockClaude = createMockClaudeService({
119
+ spawnClaude: () => new Promise(() => {}), // never resolves
120
+ spawnCommand: async () => '',
121
+ });
122
+ const pipeline = createTestPipeline(mockClaude, mockKanban);
123
+
124
+ // First start succeeds
125
+ const result1 = await pipeline.start('feat_010');
126
+ assert.equal(result1.status, 200);
127
+
128
+ // Reset kanban to todo so it's not blocked by column check
129
+ await mockKanban.saveKanbanEntry('feat_010', { column: 'todo', rejection_count: 0, notes: [] });
130
+
131
+ // Second start should be rejected (pipeline still running)
132
+ const result2 = await pipeline.start('feat_010');
133
+ assert.equal(result2.status, 409);
134
+ assert.ok(result2.error.includes('already running'));
135
+ });
136
+ });
137
+
138
+ describe('pipeline.getStatus()', () => {
139
+ it('returns idle when no pipeline exists', async () => {
140
+ const mockKanban = createMockKanban({});
141
+ const mockClaude = createMockClaudeService();
142
+ const pipeline = createTestPipeline(mockClaude, mockKanban);
143
+
144
+ const result = await pipeline.getStatus('feat_999');
145
+ assert.equal(result.status, 'idle');
146
+ assert.equal(result.feature_id, 'feat_999');
147
+ });
148
+ });
149
+
150
+ describe('pipeline.unblock()', () => {
151
+ it('returns 404 for unknown feature', async () => {
152
+ const mockKanban = createMockKanban({});
153
+ const mockClaude = createMockClaudeService();
154
+ const pipeline = createTestPipeline(mockClaude, mockKanban);
155
+
156
+ const result = await pipeline.unblock('feat_999');
157
+ assert.equal(result.status, 404);
158
+ });
159
+
160
+ it('clears dev_blocked and pipeline state', async () => {
161
+ const mockKanban = createMockKanban({
162
+ feat_010: { column: 'todo', rejection_count: 3, notes: [], dev_blocked: true },
163
+ });
164
+ const mockClaude = createMockClaudeService();
165
+ const pipeline = createTestPipeline(mockClaude, mockKanban);
166
+
167
+ // Simulate a blocked pipeline state
168
+ pipeline.pipelines.set('feat_010', { status: 'blocked', cycle: 3 });
169
+
170
+ const result = await pipeline.unblock('feat_010');
171
+ assert.equal(result.status, 200);
172
+ assert.equal(result.unblocked, true);
173
+
174
+ // Verify dev_blocked is removed from kanban
175
+ const entry = mockKanban._get('feat_010');
176
+ assert.equal(entry.dev_blocked, undefined);
177
+
178
+ // Verify pipeline state is cleared
179
+ assert.equal(pipeline.pipelines.has('feat_010'), false);
180
+ });
181
+ });
182
+
183
+ describe('pipeline.run() — full cycle with reviewer approving', () => {
184
+ it('completes when reviewer moves card to qa', async () => {
185
+ let claudeCallCount = 0;
186
+ const mockKanban = createMockKanban({
187
+ feat_010: { column: 'in_progress', rejection_count: 0, notes: [] },
188
+ });
189
+
190
+ const mockClaude = createMockClaudeService({
191
+ spawnClaude: async (prompt, cwd, label) => {
192
+ claudeCallCount++;
193
+ // When reviewer runs, move the card to qa
194
+ if (label.startsWith('review:')) {
195
+ const entry = await mockKanban.getKanbanEntry('feat_010');
196
+ entry.column = 'qa';
197
+ entry.moved_at = new Date().toISOString();
198
+ await mockKanban.saveKanbanEntry('feat_010', entry);
199
+ }
200
+ return 'mock output';
201
+ },
202
+ spawnCommand: async () => '',
203
+ });
204
+
205
+ const pipeline = createTestPipeline(mockClaude, mockKanban);
206
+
207
+ // Set up pipeline state as if start() ran the setup
208
+ pipeline.pipelines.set('feat_010', { status: 'developing', cycle: 1, worktree_path: '/mock/worktrees/feat_010', process: null, error: null });
209
+
210
+ await pipeline.run('feat_010');
211
+
212
+ const state = pipeline.pipelines.get('feat_010');
213
+ assert.equal(state.status, 'completed');
214
+ // Developer + Reviewer = 2 Claude calls
215
+ assert.equal(claudeCallCount, 2);
216
+ });
217
+ });
218
+
219
+ describe('pipeline.run() — reviewer rejecting 3x causes block', () => {
220
+ it('blocks card after 3 rejected cycles', async () => {
221
+ let claudeCallCount = 0;
222
+ const mockKanban = createMockKanban({
223
+ feat_010: { column: 'in_progress', rejection_count: 0, notes: [] },
224
+ });
225
+
226
+ const mockClaude = createMockClaudeService({
227
+ spawnClaude: async (prompt, cwd, label) => {
228
+ claudeCallCount++;
229
+ // Reviewer always rejects — moves card to todo
230
+ if (label.startsWith('review:')) {
231
+ const entry = await mockKanban.getKanbanEntry('feat_010');
232
+ entry.column = 'todo';
233
+ entry.moved_at = new Date().toISOString();
234
+ entry.notes = [{ id: 'n1', text: 'Needs fixes' }];
235
+ await mockKanban.saveKanbanEntry('feat_010', entry);
236
+ }
237
+ return 'mock output';
238
+ },
239
+ spawnCommand: async () => '',
240
+ });
241
+
242
+ const pipeline = createTestPipeline(mockClaude, mockKanban);
243
+
244
+ pipeline.pipelines.set('feat_010', { status: 'developing', cycle: 1, worktree_path: '/mock/worktrees/feat_010', process: null, error: null });
245
+
246
+ await pipeline.run('feat_010');
247
+
248
+ const state = pipeline.pipelines.get('feat_010');
249
+ assert.equal(state.status, 'blocked');
250
+
251
+ // 3 cycles × (dev + reviewer) = 6 Claude calls
252
+ assert.equal(claudeCallCount, 6);
253
+
254
+ // Kanban entry should be dev_blocked
255
+ const entry = mockKanban._get('feat_010');
256
+ assert.equal(entry.dev_blocked, true);
257
+ });
258
+ });
259
+
260
+ describe('pipeline.run() — debug-first step', () => {
261
+ it('runs debugger when unaddressed notes exist', async () => {
262
+ const claudeCalls = [];
263
+ const mockKanban = createMockKanban({
264
+ feat_010: {
265
+ column: 'in_progress',
266
+ rejection_count: 1,
267
+ notes: [
268
+ { id: 'n1', text: 'Bug: missing validation', addressed: false },
269
+ { id: 'n2', text: 'Bug: wrong color', addressed: true },
270
+ ],
271
+ },
272
+ });
273
+
274
+ const mockClaude = createMockClaudeService({
275
+ spawnClaude: async (prompt, cwd, label) => {
276
+ claudeCalls.push(label);
277
+ // Reviewer approves on first cycle
278
+ if (label.startsWith('review:')) {
279
+ const entry = await mockKanban.getKanbanEntry('feat_010');
280
+ entry.column = 'qa';
281
+ entry.moved_at = new Date().toISOString();
282
+ await mockKanban.saveKanbanEntry('feat_010', entry);
283
+ }
284
+ return 'mock debugger findings';
285
+ },
286
+ spawnCommand: async () => '',
287
+ });
288
+
289
+ const pipeline = createTestPipeline(mockClaude, mockKanban);
290
+
291
+ pipeline.pipelines.set('feat_010', { status: 'developing', cycle: 1, worktree_path: '/mock/worktrees/feat_010', process: null, error: null });
292
+
293
+ await pipeline.run('feat_010');
294
+
295
+ // First call should be the debugger
296
+ assert.equal(claudeCalls[0], 'debug:feat_010');
297
+ // Then developer, then reviewer
298
+ assert.equal(claudeCalls[1], 'dev:feat_010');
299
+ assert.equal(claudeCalls[2], 'review:feat_010');
300
+
301
+ // The unaddressed note should now be marked as addressed
302
+ const entry = mockKanban._get('feat_010');
303
+ const note = entry.notes.find(n => n.id === 'n1');
304
+ assert.equal(note.addressed, true);
305
+ });
306
+
307
+ it('skips debugger when all notes are already addressed', async () => {
308
+ const claudeCalls = [];
309
+ const mockKanban = createMockKanban({
310
+ feat_010: {
311
+ column: 'in_progress',
312
+ rejection_count: 1,
313
+ notes: [
314
+ { id: 'n1', text: 'Bug: fixed', addressed: true },
315
+ ],
316
+ },
317
+ });
318
+
319
+ const mockClaude = createMockClaudeService({
320
+ spawnClaude: async (prompt, cwd, label) => {
321
+ claudeCalls.push(label);
322
+ if (label.startsWith('review:')) {
323
+ const entry = await mockKanban.getKanbanEntry('feat_010');
324
+ entry.column = 'qa';
325
+ entry.moved_at = new Date().toISOString();
326
+ await mockKanban.saveKanbanEntry('feat_010', entry);
327
+ }
328
+ return 'mock output';
329
+ },
330
+ spawnCommand: async () => '',
331
+ });
332
+
333
+ const pipeline = createTestPipeline(mockClaude, mockKanban);
334
+
335
+ pipeline.pipelines.set('feat_010', { status: 'developing', cycle: 1, worktree_path: '/mock/worktrees/feat_010', process: null, error: null });
336
+
337
+ await pipeline.run('feat_010');
338
+
339
+ // No debugger call — first call should be developer
340
+ assert.equal(claudeCalls[0], 'dev:feat_010');
341
+ assert.equal(claudeCalls.length, 2); // dev + reviewer only
342
+ });
343
+ });
344
+
345
+ describe('pipeline task progress tracking', () => {
346
+ it('initializes tasks field in pipeline state on start', async () => {
347
+ const mockKanban = createMockKanban({
348
+ feat_010: { column: 'todo', rejection_count: 0, notes: [] },
349
+ });
350
+ const mockClaude = createMockClaudeService({
351
+ spawnClaude: () => new Promise(() => {}), // never resolves
352
+ spawnCommand: async () => '',
353
+ });
354
+ const pipeline = createTestPipeline(mockClaude, mockKanban);
355
+
356
+ await pipeline.start('feat_010');
357
+
358
+ const state = pipeline.pipelines.get('feat_010');
359
+ assert.deepEqual(state.tasks, { completed: 0, total: 0, items: [] });
360
+ });
361
+
362
+ it('resets tasks at the start of each cycle', async () => {
363
+ let claudeCallCount = 0;
364
+ const mockKanban = createMockKanban({
365
+ feat_010: { column: 'in_progress', rejection_count: 0, notes: [] },
366
+ });
367
+
368
+ const mockClaude = createMockClaudeService({
369
+ spawnClaude: async (prompt, cwd, label, opts) => {
370
+ claudeCallCount++;
371
+ if (label.startsWith('dev:')) {
372
+ // Simulate TodoWrite callback if present
373
+ if (opts?.onToolUse) {
374
+ opts.onToolUse('TodoWrite', {
375
+ todos: [
376
+ { content: 'Task A', status: 'completed', activeForm: 'Doing A' },
377
+ { content: 'Task B', status: 'in_progress', activeForm: 'Doing B' },
378
+ ],
379
+ });
380
+ }
381
+ }
382
+ // Reviewer rejects on first cycle, approves on second
383
+ if (label.startsWith('review:')) {
384
+ const entry = await mockKanban.getKanbanEntry('feat_010');
385
+ if (claudeCallCount <= 2) {
386
+ entry.column = 'todo';
387
+ entry.notes = [{ id: 'n1', text: 'Fix it' }];
388
+ } else {
389
+ entry.column = 'qa';
390
+ }
391
+ entry.moved_at = new Date().toISOString();
392
+ await mockKanban.saveKanbanEntry('feat_010', entry);
393
+ }
394
+ return 'mock output';
395
+ },
396
+ spawnCommand: async () => '',
397
+ });
398
+
399
+ const pipeline = createTestPipeline(mockClaude, mockKanban);
400
+ pipeline.pipelines.set('feat_010', {
401
+ status: 'developing', cycle: 1, worktree_path: '/mock/worktrees/feat_010',
402
+ process: null, error: null, tasks: { completed: 0, total: 0, items: [] },
403
+ });
404
+
405
+ await pipeline.run('feat_010');
406
+
407
+ // Pipeline should have completed after 2 cycles
408
+ const state = pipeline.pipelines.get('feat_010');
409
+ assert.equal(state.status, 'completed');
410
+ });
411
+
412
+ it('getStatus includes tasks when total > 0', async () => {
413
+ const mockKanban = createMockKanban({});
414
+ const mockClaude = createMockClaudeService();
415
+ const pipeline = createTestPipeline(mockClaude, mockKanban);
416
+
417
+ pipeline.pipelines.set('feat_010', {
418
+ status: 'developing', cycle: 1, worktree_path: '/mock',
419
+ process: null, error: null,
420
+ tasks: { completed: 3, total: 7, items: [
421
+ { name: 'Task A', status: 'completed' },
422
+ { name: 'Task B', status: 'completed' },
423
+ { name: 'Task C', status: 'completed' },
424
+ { name: 'Task D', status: 'in_progress' },
425
+ { name: 'Task E', status: 'pending' },
426
+ { name: 'Task F', status: 'pending' },
427
+ { name: 'Task G', status: 'pending' },
428
+ ] },
429
+ });
430
+
431
+ const result = await pipeline.getStatus('feat_010');
432
+ assert.equal(result.tasks.completed, 3);
433
+ assert.equal(result.tasks.total, 7);
434
+ assert.equal(result.tasks.items.length, 7);
435
+ });
436
+
437
+ it('getStatus omits tasks when total is 0', async () => {
438
+ const mockKanban = createMockKanban({});
439
+ const mockClaude = createMockClaudeService();
440
+ const pipeline = createTestPipeline(mockClaude, mockKanban);
441
+
442
+ pipeline.pipelines.set('feat_010', {
443
+ status: 'developing', cycle: 1, worktree_path: '/mock',
444
+ process: null, error: null,
445
+ tasks: { completed: 0, total: 0, items: [] },
446
+ });
447
+
448
+ const result = await pipeline.getStatus('feat_010');
449
+ assert.equal(result.tasks, undefined);
450
+ });
451
+ });
452
+
453
+ describe('pipeline tool call activity counters', () => {
454
+ it('initializes toolCalls field in pipeline state on start', async () => {
455
+ const mockKanban = createMockKanban({
456
+ feat_010: { column: 'todo', rejection_count: 0, notes: [] },
457
+ });
458
+ const mockClaude = createMockClaudeService({
459
+ spawnClaude: () => new Promise(() => {}),
460
+ spawnCommand: async () => '',
461
+ });
462
+ const pipeline = createTestPipeline(mockClaude, mockKanban);
463
+
464
+ await pipeline.start('feat_010');
465
+
466
+ const state = pipeline.pipelines.get('feat_010');
467
+ assert.deepEqual(state.toolCalls, { total: 0, write: 0, edit: 0, read: 0, bash: 0 });
468
+ });
469
+
470
+ it('counts tool calls by type via onToolUse callback', async () => {
471
+ const mockKanban = createMockKanban({
472
+ feat_010: { column: 'in_progress', rejection_count: 0, notes: [] },
473
+ });
474
+
475
+ const mockClaude = createMockClaudeService({
476
+ spawnClaude: async (prompt, cwd, label, opts) => {
477
+ if (label.startsWith('dev:') && opts?.onToolUse) {
478
+ opts.onToolUse('Read', { file_path: '/some/file.js' });
479
+ opts.onToolUse('Read', { file_path: '/another/file.js' });
480
+ opts.onToolUse('Edit', { file_path: '/some/file.js', old_string: 'a', new_string: 'b' });
481
+ opts.onToolUse('Write', { file_path: '/new/file.js', content: '...' });
482
+ opts.onToolUse('Bash', { command: 'npm test' });
483
+ opts.onToolUse('Bash', { command: 'ls' });
484
+ opts.onToolUse('TodoWrite', { todos: [{ content: 'Task', status: 'pending', activeForm: 'Doing' }] });
485
+ opts.onToolUse('Grep', { pattern: 'foo' });
486
+ }
487
+ if (label.startsWith('review:')) {
488
+ const entry = await mockKanban.getKanbanEntry('feat_010');
489
+ entry.column = 'qa';
490
+ entry.moved_at = new Date().toISOString();
491
+ await mockKanban.saveKanbanEntry('feat_010', entry);
492
+ }
493
+ return 'mock output';
494
+ },
495
+ spawnCommand: async () => '',
496
+ });
497
+
498
+ const pipeline = createTestPipeline(mockClaude, mockKanban);
499
+ pipeline.pipelines.set('feat_010', {
500
+ status: 'developing', cycle: 1, worktree_path: '/mock/worktrees/feat_010',
501
+ process: null, error: null,
502
+ tasks: { completed: 0, total: 0, items: [] },
503
+ toolCalls: { total: 0, write: 0, edit: 0, read: 0, bash: 0 },
504
+ });
505
+
506
+ await pipeline.run('feat_010');
507
+
508
+ const state = pipeline.pipelines.get('feat_010');
509
+ assert.equal(state.toolCalls.total, 8);
510
+ assert.equal(state.toolCalls.read, 2);
511
+ assert.equal(state.toolCalls.edit, 1);
512
+ assert.equal(state.toolCalls.write, 1);
513
+ assert.equal(state.toolCalls.bash, 2);
514
+ });
515
+
516
+ it('resets toolCalls at the start of each cycle', async () => {
517
+ let claudeCallCount = 0;
518
+ const mockKanban = createMockKanban({
519
+ feat_010: { column: 'in_progress', rejection_count: 0, notes: [] },
520
+ });
521
+
522
+ const mockClaude = createMockClaudeService({
523
+ spawnClaude: async (prompt, cwd, label, opts) => {
524
+ claudeCallCount++;
525
+ if (label.startsWith('dev:') && opts?.onToolUse) {
526
+ opts.onToolUse('Write', { file_path: '/f.js', content: '...' });
527
+ opts.onToolUse('Edit', { file_path: '/f.js', old_string: 'a', new_string: 'b' });
528
+ }
529
+ if (label.startsWith('review:')) {
530
+ const entry = await mockKanban.getKanbanEntry('feat_010');
531
+ if (claudeCallCount <= 2) {
532
+ entry.column = 'todo';
533
+ entry.notes = [{ id: 'n1', text: 'Fix' }];
534
+ } else {
535
+ entry.column = 'qa';
536
+ }
537
+ entry.moved_at = new Date().toISOString();
538
+ await mockKanban.saveKanbanEntry('feat_010', entry);
539
+ }
540
+ return 'mock output';
541
+ },
542
+ spawnCommand: async () => '',
543
+ });
544
+
545
+ const pipeline = createTestPipeline(mockClaude, mockKanban);
546
+ pipeline.pipelines.set('feat_010', {
547
+ status: 'developing', cycle: 1, worktree_path: '/mock/worktrees/feat_010',
548
+ process: null, error: null,
549
+ tasks: { completed: 0, total: 0, items: [] },
550
+ toolCalls: { total: 0, write: 0, edit: 0, read: 0, bash: 0 },
551
+ });
552
+
553
+ await pipeline.run('feat_010');
554
+
555
+ // After 2 cycles (reject + approve), toolCalls should reflect only cycle 2
556
+ const state = pipeline.pipelines.get('feat_010');
557
+ assert.equal(state.toolCalls.total, 2);
558
+ assert.equal(state.toolCalls.write, 1);
559
+ assert.equal(state.toolCalls.edit, 1);
560
+ });
561
+
562
+ it('getStatus includes toolCalls when total > 0', async () => {
563
+ const mockKanban = createMockKanban({});
564
+ const mockClaude = createMockClaudeService();
565
+ const pipeline = createTestPipeline(mockClaude, mockKanban);
566
+
567
+ pipeline.pipelines.set('feat_010', {
568
+ status: 'developing', cycle: 1, worktree_path: '/mock',
569
+ process: null, error: null,
570
+ tasks: { completed: 0, total: 0, items: [] },
571
+ toolCalls: { total: 5, write: 1, edit: 2, read: 1, bash: 1 },
572
+ });
573
+
574
+ const result = await pipeline.getStatus('feat_010');
575
+ assert.deepEqual(result.toolCalls, { total: 5, write: 1, edit: 2, read: 1, bash: 1 });
576
+ });
577
+
578
+ it('getStatus omits toolCalls when total is 0', async () => {
579
+ const mockKanban = createMockKanban({});
580
+ const mockClaude = createMockClaudeService();
581
+ const pipeline = createTestPipeline(mockClaude, mockKanban);
582
+
583
+ pipeline.pipelines.set('feat_010', {
584
+ status: 'developing', cycle: 1, worktree_path: '/mock',
585
+ process: null, error: null,
586
+ tasks: { completed: 0, total: 0, items: [] },
587
+ toolCalls: { total: 0, write: 0, edit: 0, read: 0, bash: 0 },
588
+ });
589
+
590
+ const result = await pipeline.getStatus('feat_010');
591
+ assert.equal(result.toolCalls, undefined);
592
+ });
593
+ });