@assistkick/create 1.2.0 → 1.4.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 (54) hide show
  1. package/package.json +2 -1
  2. package/templates/assistkick-product-system/GITHUB_APP_SETUP.md +88 -0
  3. package/templates/assistkick-product-system/packages/backend/src/routes/git.ts +231 -0
  4. package/templates/assistkick-product-system/packages/backend/src/routes/kanban.ts +4 -4
  5. package/templates/assistkick-product-system/packages/backend/src/routes/pipeline.ts +49 -2
  6. package/templates/assistkick-product-system/packages/backend/src/routes/terminal.ts +82 -0
  7. package/templates/assistkick-product-system/packages/backend/src/server.ts +19 -6
  8. package/templates/assistkick-product-system/packages/backend/src/services/github_app_service.ts +146 -0
  9. package/templates/assistkick-product-system/packages/backend/src/services/init.ts +69 -2
  10. package/templates/assistkick-product-system/packages/backend/src/services/project_service.ts +71 -0
  11. package/templates/assistkick-product-system/packages/backend/src/services/project_workspace_service.test.ts +87 -0
  12. package/templates/assistkick-product-system/packages/backend/src/services/project_workspace_service.ts +194 -0
  13. package/templates/assistkick-product-system/packages/backend/src/services/pty_session_manager.test.ts +88 -17
  14. package/templates/assistkick-product-system/packages/backend/src/services/pty_session_manager.ts +114 -39
  15. package/templates/assistkick-product-system/packages/backend/src/services/terminal_ws_handler.ts +28 -14
  16. package/templates/assistkick-product-system/packages/frontend/src/App.tsx +1 -1
  17. package/templates/assistkick-product-system/packages/frontend/src/api/client.ts +151 -0
  18. package/templates/assistkick-product-system/packages/frontend/src/components/GitRepoModal.tsx +352 -0
  19. package/templates/assistkick-product-system/packages/frontend/src/components/KanbanView.tsx +208 -95
  20. package/templates/assistkick-product-system/packages/frontend/src/components/ProjectSelector.tsx +17 -1
  21. package/templates/assistkick-product-system/packages/frontend/src/components/TerminalView.tsx +238 -105
  22. package/templates/assistkick-product-system/packages/frontend/src/components/Toolbar.tsx +15 -13
  23. package/templates/assistkick-product-system/packages/frontend/src/constants/graph.ts +1 -0
  24. package/templates/assistkick-product-system/packages/frontend/src/hooks/useProjects.ts +4 -0
  25. package/templates/assistkick-product-system/packages/frontend/src/routes/dashboard.tsx +22 -4
  26. package/templates/assistkick-product-system/packages/frontend/src/styles/index.css +486 -38
  27. package/templates/assistkick-product-system/packages/shared/db/migrations/0001_vengeful_wallop.sql +1 -0
  28. package/templates/assistkick-product-system/packages/shared/db/migrations/0002_greedy_excalibur.sql +4 -0
  29. package/templates/assistkick-product-system/packages/shared/db/migrations/0003_lonely_cyclops.sql +17 -0
  30. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0001_snapshot.json +826 -0
  31. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0002_snapshot.json +854 -0
  32. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0003_snapshot.json +862 -0
  33. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/_journal.json +21 -0
  34. package/templates/assistkick-product-system/packages/shared/db/schema.ts +10 -3
  35. package/templates/assistkick-product-system/packages/shared/lib/claude-service.ts +54 -1
  36. package/templates/assistkick-product-system/packages/shared/lib/git_workflow.ts +25 -0
  37. package/templates/assistkick-product-system/packages/shared/lib/pipeline-state-store.ts +4 -0
  38. package/templates/assistkick-product-system/packages/shared/lib/pipeline.ts +329 -89
  39. package/templates/assistkick-product-system/packages/shared/lib/pipeline_orchestrator.ts +186 -0
  40. package/templates/assistkick-product-system/packages/shared/lib/session.ts +10 -6
  41. package/templates/assistkick-product-system/packages/shared/tools/db_explorer.ts +275 -0
  42. package/templates/assistkick-product-system/packages/shared/tools/end_session.ts +2 -2
  43. package/templates/assistkick-product-system/packages/shared/tools/get_kanban.ts +2 -1
  44. package/templates/assistkick-product-system/packages/shared/tools/move_card.ts +3 -2
  45. package/templates/assistkick-product-system/packages/shared/tools/update_node.ts +2 -2
  46. package/templates/assistkick-product-system/tests/kanban.test.ts +1 -1
  47. package/templates/assistkick-product-system/tests/pipeline_stats_all_cards.test.ts +1 -1
  48. package/templates/assistkick-product-system/tests/web_terminal.test.ts +189 -150
  49. package/templates/skills/assistkick-bootstrap/SKILL.md +33 -25
  50. package/templates/skills/assistkick-code-reviewer/SKILL.md +23 -15
  51. package/templates/skills/assistkick-db-explorer/SKILL.md +86 -0
  52. package/templates/skills/assistkick-debugger/SKILL.md +30 -22
  53. package/templates/skills/assistkick-developer/SKILL.md +37 -29
  54. package/templates/skills/assistkick-interview/SKILL.md +34 -26
@@ -26,6 +26,44 @@ interface CardData {
26
26
 
27
27
  const ALL_COLUMNS = COLUMNS.map(c => c.id);
28
28
 
29
+ const STAGE_TABS = [
30
+ { key: 'in_progress', label: 'Dev' },
31
+ { key: 'in_review', label: 'Review' },
32
+ { key: 'qa', label: 'QA' },
33
+ ] as const;
34
+
35
+ const hasStageData = (stage: any): boolean => {
36
+ if (!stage) return false;
37
+ const tc = stage.toolCalls;
38
+ return (tc && tc.total > 0)
39
+ || stage.numTurns != null
40
+ || stage.costUsd != null
41
+ || stage.usage != null
42
+ || stage.stopReason != null;
43
+ };
44
+
45
+ const formatContextFill = (lastTurnUsage: any, contextWindow: number | null): string => {
46
+ if (!lastTurnUsage) return '';
47
+ const fill = (lastTurnUsage.input_tokens || 0)
48
+ + (lastTurnUsage.cache_creation_input_tokens || 0)
49
+ + (lastTurnUsage.cache_read_input_tokens || 0);
50
+ const denom = contextWindow || 200_000;
51
+ const pct = Math.round((fill / denom) * 100);
52
+ return `${pct}%`;
53
+ };
54
+
55
+ const formatCost = (costUsd: number | null): string => {
56
+ if (costUsd == null) return '';
57
+ return `$${costUsd.toFixed(4)}`;
58
+ };
59
+
60
+ const formatModel = (model: string | null): string => {
61
+ if (!model) return '';
62
+ // Shorten long model names: "claude-sonnet-4-20250514" → "sonnet-4"
63
+ const match = model.match(/(opus|sonnet|haiku)-(\d[\d.]*)/);
64
+ return match ? `${match[1]}-${match[2]}` : model;
65
+ };
66
+
29
67
  export function KanbanView({ graphData, projectId, onCardClick, onIssuesClick }: KanbanViewProps) {
30
68
  const [kanbanData, setKanbanData] = useState<any>(null);
31
69
  const [error, setError] = useState<string | null>(null);
@@ -35,10 +73,11 @@ export function KanbanView({ graphData, projectId, onCardClick, onIssuesClick }:
35
73
  const [playAllRunning, setPlayAllRunning] = useState(false);
36
74
  const [playAllCurrentFeature, setPlayAllCurrentFeature] = useState<string | null>(null);
37
75
  const [copiedId, setCopiedId] = useState<string | null>(null);
76
+ const [activeStageTab, setActiveStageTab] = useState<Record<string, string>>({});
38
77
  const { showToast } = useToast();
39
78
 
40
79
  const pollersRef = useRef<Map<string, ReturnType<typeof setInterval>>>(new Map());
41
- const playAllAbortedRef = useRef(false);
80
+ const orchestratorPollerRef = useRef<ReturnType<typeof setInterval> | null>(null);
42
81
  const kanbanDataRef = useRef(kanbanData);
43
82
  kanbanDataRef.current = kanbanData;
44
83
 
@@ -62,6 +101,10 @@ export function KanbanView({ graphData, projectId, onCardClick, onIssuesClick }:
62
101
  clearInterval(intervalId);
63
102
  }
64
103
  pollersRef.current.clear();
104
+ if (orchestratorPollerRef.current) {
105
+ clearInterval(orchestratorPollerRef.current);
106
+ orchestratorPollerRef.current = null;
107
+ }
65
108
  };
66
109
  }, [fetchKanban]);
67
110
 
@@ -204,6 +247,17 @@ export function KanbanView({ graphData, projectId, onCardClick, onIssuesClick }:
204
247
  }
205
248
  };
206
249
 
250
+ const handleResumePipeline = async (featureId: string) => {
251
+ try {
252
+ await apiClient.resumePipeline(featureId);
253
+ startPipelinePolling(featureId);
254
+ await fetchKanban();
255
+ } catch (err: any) {
256
+ console.error('Failed to resume pipeline:', err);
257
+ showToast(err?.message || 'Failed to resume pipeline');
258
+ }
259
+ };
260
+
207
261
  const handleUnblock = async (featureId: string) => {
208
262
  try {
209
263
  await apiClient.unblockCard(featureId);
@@ -222,95 +276,71 @@ export function KanbanView({ graphData, projectId, onCardClick, onIssuesClick }:
222
276
  } catch { /* ignore */ }
223
277
  };
224
278
 
225
- // Play All
279
+ // Orchestrator status polling
280
+ const startOrchestratorPolling = useCallback(() => {
281
+ if (orchestratorPollerRef.current) return;
282
+ orchestratorPollerRef.current = setInterval(async () => {
283
+ try {
284
+ const status = await apiClient.getOrchestratorStatus();
285
+ setPlayAllRunning(status.active);
286
+ setPlayAllCurrentFeature(status.currentFeatureId);
287
+ if (status.active) {
288
+ await fetchKanban();
289
+ } else {
290
+ // Orchestrator stopped — clean up poller
291
+ if (orchestratorPollerRef.current) {
292
+ clearInterval(orchestratorPollerRef.current);
293
+ orchestratorPollerRef.current = null;
294
+ }
295
+ await fetchKanban();
296
+ }
297
+ } catch {
298
+ if (orchestratorPollerRef.current) {
299
+ clearInterval(orchestratorPollerRef.current);
300
+ orchestratorPollerRef.current = null;
301
+ }
302
+ setPlayAllRunning(false);
303
+ setPlayAllCurrentFeature(null);
304
+ }
305
+ }, 5000);
306
+ }, [fetchKanban]);
307
+
308
+ // Check orchestrator status on mount to sync with server state
309
+ useEffect(() => {
310
+ apiClient.getOrchestratorStatus().then(status => {
311
+ setPlayAllRunning(status.active);
312
+ setPlayAllCurrentFeature(status.currentFeatureId);
313
+ if (status.active) {
314
+ startOrchestratorPolling();
315
+ }
316
+ }).catch(() => { /* ignore */ });
317
+
318
+ return () => {
319
+ if (orchestratorPollerRef.current) {
320
+ clearInterval(orchestratorPollerRef.current);
321
+ orchestratorPollerRef.current = null;
322
+ }
323
+ };
324
+ }, [startOrchestratorPolling]);
325
+
326
+ // Play All — thin wrapper around backend orchestrator
226
327
  const startPlayAll = async () => {
227
328
  if (playAllRunning) return;
228
- setPlayAllRunning(true);
229
- playAllAbortedRef.current = false;
230
- setPlayAllCurrentFeature(null);
231
-
232
329
  try {
233
- await playAllLoop();
234
- } finally {
235
- setPlayAllRunning(false);
330
+ await apiClient.startPlayAll(projectId ?? undefined);
331
+ setPlayAllRunning(true);
236
332
  setPlayAllCurrentFeature(null);
333
+ startOrchestratorPolling();
334
+ } catch (err: any) {
335
+ showToast(err?.message || 'Failed to start Play All');
237
336
  }
238
337
  };
239
338
 
240
- const stopPlayAll = () => {
241
- playAllAbortedRef.current = true;
242
- };
243
-
244
- const playAllLoop = async () => {
245
- while (!playAllAbortedRef.current) {
246
- let freshKanban: any;
247
- let freshGraph: any;
248
- try {
249
- freshKanban = await apiClient.fetchKanban(projectId ?? undefined);
250
- freshGraph = await apiClient.fetchGraph(projectId ?? undefined);
251
- setKanbanData(freshKanban);
252
- } catch { break; }
253
-
254
- const featureNodes = new Map<string, any>();
255
- freshGraph.nodes
256
- .filter((n: any) => n.type === 'feature')
257
- .forEach((n: any) => featureNodes.set(n.id, n));
258
-
259
- const todoCards = Object.entries(freshKanban)
260
- .filter(([id, entry]: [string, any]) => entry.column === 'todo' && featureNodes.has(id) && !entry.dev_blocked)
261
- .map(([id, entry]: [string, any]) => ({
262
- id,
263
- completeness: featureNodes.get(id).completeness || 0,
264
- }))
265
- .sort((a, b) => b.completeness - a.completeness);
266
-
267
- if (todoCards.length === 0) break;
268
-
269
- let processed = false;
270
- for (const card of todoCards) {
271
- if (playAllAbortedRef.current) return;
272
-
273
- // Check deps
274
- const deps = freshGraph.edges
275
- .filter((e: any) => e.from === card.id && e.relation === 'depends_on')
276
- .map((e: any) => e.to)
277
- .filter((depId: string) => freshGraph.nodes.some((n: any) => n.id === depId && n.type === 'feature'));
278
- const blocked = deps.some((depId: string) => !freshKanban[depId] || freshKanban[depId].column !== 'done');
279
- if (blocked) continue;
280
-
281
- setPlayAllCurrentFeature(card.id);
282
-
283
- try {
284
- await apiClient.startPipeline(card.id);
285
- } catch (err: any) {
286
- console.error(`Play All: failed to start pipeline for ${card.id}:`, err);
287
- showToast(err?.message || `Failed to start pipeline for ${card.id}`);
288
- break;
289
- }
290
-
291
- // Wait for pipeline completion
292
- await new Promise<void>((resolve) => {
293
- const poll = async () => {
294
- if (playAllAbortedRef.current) { resolve(); return; }
295
- try {
296
- const status = await apiClient.getPipelineStatus(card.id);
297
- setPipelineStatuses(prev => ({ ...prev, [card.id]: status }));
298
- if (['idle', 'completed', 'blocked', 'failed', 'interrupted'].includes(status.status)) {
299
- resolve();
300
- return;
301
- }
302
- } catch { resolve(); return; }
303
- setTimeout(poll, 5000);
304
- };
305
- poll();
306
- });
307
-
308
- if (playAllAbortedRef.current) return;
309
- processed = true;
310
- break;
311
- }
312
-
313
- if (!processed) break;
339
+ const stopPlayAll = async () => {
340
+ try {
341
+ await apiClient.stopPlayAll();
342
+ } catch (err: any) {
343
+ showToast(err?.message || 'Failed to stop Play All');
314
344
  }
315
345
  };
316
346
 
@@ -356,6 +386,10 @@ export function KanbanView({ graphData, projectId, onCardClick, onIssuesClick }:
356
386
  const pStatus = pipelineStatuses[card.id];
357
387
  const isActive = pStatus && !['idle', 'completed', 'blocked', 'failed', 'interrupted'].includes(pStatus.status);
358
388
  const isTerminal = pStatus && ['completed', 'failed', 'blocked', 'interrupted'].includes(pStatus.status);
389
+ const pipelineStatusValue = pStatus?.status ?? 'idle';
390
+ const showResumeBtn = ['in_progress', 'in_review'].includes(card.column)
391
+ && !isActive
392
+ && ['interrupted', 'failed', 'idle'].includes(pipelineStatusValue);
359
393
 
360
394
  return (
361
395
  <div
@@ -400,6 +434,15 @@ export function KanbanView({ graphData, projectId, onCardClick, onIssuesClick }:
400
434
  {'\u25B6'}
401
435
  </button>
402
436
  )}
437
+ {showResumeBtn && (
438
+ <button
439
+ className="kanban-play-btn kanban-resume-btn"
440
+ title="Resume pipeline from last completed step"
441
+ onClick={(e) => { e.stopPropagation(); handleResumePipeline(card.id); }}
442
+ >
443
+ {'\u25B6'}
444
+ </button>
445
+ )}
403
446
  {card.devBlocked && (
404
447
  <span className="kanban-blocked-badge">Blocked</span>
405
448
  )}
@@ -422,18 +465,88 @@ export function KanbanView({ graphData, projectId, onCardClick, onIssuesClick }:
422
465
  </div>
423
466
  )}
424
467
 
425
- {pStatus?.toolCalls?.total > 0 && (
426
- <div className="kanban-tool-calls" style={{ display: 'flex' }}>
427
- {[
428
- { label: 'Write', count: pStatus.toolCalls.write },
429
- { label: 'Edit', count: pStatus.toolCalls.edit },
430
- { label: 'Read', count: pStatus.toolCalls.read },
431
- { label: 'Bash', count: pStatus.toolCalls.bash },
432
- ].filter(i => i.count > 0).map(i => (
433
- <span key={i.label} className="kanban-tool-badge">{i.label}: {i.count}</span>
434
- ))}
435
- </div>
436
- )}
468
+ {(() => {
469
+ const ss = pStatus?.stageStats;
470
+ const availableTabs = ss
471
+ ? STAGE_TABS.filter(t => hasStageData(ss[t.key]))
472
+ : [];
473
+ // Fall back to flat toolCalls for old data without stageStats
474
+ if (availableTabs.length === 0 && pStatus?.toolCalls?.total > 0) {
475
+ return (
476
+ <div className="kanban-tool-calls">
477
+ {[
478
+ { label: 'Write', count: pStatus.toolCalls.write },
479
+ { label: 'Edit', count: pStatus.toolCalls.edit },
480
+ { label: 'Read', count: pStatus.toolCalls.read },
481
+ { label: 'Bash', count: pStatus.toolCalls.bash },
482
+ ].filter(i => i.count > 0).map(i => (
483
+ <span key={i.label} className="kanban-tool-badge">{i.label}: {i.count}</span>
484
+ ))}
485
+ </div>
486
+ );
487
+ }
488
+ if (availableTabs.length === 0) return null;
489
+
490
+ const currentTab = activeStageTab[card.id] || availableTabs[availableTabs.length - 1].key;
491
+ const stage = ss[currentTab];
492
+ const tc = stage?.toolCalls;
493
+
494
+ return (
495
+ <div className="kanban-stage-stats" onClick={(e) => e.stopPropagation()}>
496
+ <div className="kanban-stage-tabs">
497
+ {availableTabs.map(t => (
498
+ <button
499
+ key={t.key}
500
+ className={`kanban-stage-tab${currentTab === t.key ? ' active' : ''}`}
501
+ onClick={(e) => {
502
+ e.stopPropagation();
503
+ setActiveStageTab(prev => ({ ...prev, [card.id]: t.key }));
504
+ }}
505
+ >
506
+ {t.label}
507
+ </button>
508
+ ))}
509
+ </div>
510
+ <div className="kanban-stage-body">
511
+ {tc && tc.total > 0 && (
512
+ <div className="kanban-tool-calls">
513
+ {[
514
+ { label: 'Write', count: tc.write },
515
+ { label: 'Edit', count: tc.edit },
516
+ { label: 'Read', count: tc.read },
517
+ { label: 'Bash', count: tc.bash },
518
+ ].filter(i => i.count > 0).map(i => (
519
+ <span key={i.label} className="kanban-tool-badge">{i.label}: {i.count}</span>
520
+ ))}
521
+ </div>
522
+ )}
523
+ <div className="kanban-stage-meta">
524
+ {stage?.lastTurnUsage && (
525
+ <span className="kanban-stage-meta-item" title="Peak context window utilization">
526
+ Ctx: {formatContextFill(stage.lastTurnUsage, stage.contextWindow)}
527
+ </span>
528
+ )}
529
+ {stage?.numTurns != null && (
530
+ <span className="kanban-stage-meta-item" title="Agentic turns">
531
+ {stage.numTurns} turns
532
+ </span>
533
+ )}
534
+ {stage?.costUsd != null && (
535
+ <span className="kanban-stage-meta-item" title={`Model: ${stage.model || 'unknown'}`}>
536
+ {formatCost(stage.costUsd)}
537
+ {stage.model && <span className="kanban-stage-model"> {formatModel(stage.model)}</span>}
538
+ </span>
539
+ )}
540
+ {stage?.stopReason && (
541
+ <span className="kanban-stage-meta-item kanban-stop-reason" title="Stop reason">
542
+ {stage.stopReason}
543
+ </span>
544
+ )}
545
+ </div>
546
+ </div>
547
+ </div>
548
+ );
549
+ })()}
437
550
 
438
551
  {card.devBlocked && (
439
552
  <button
@@ -8,10 +8,11 @@ interface ProjectSelectorProps {
8
8
  onCreate: (name: string) => Promise<any>;
9
9
  onRename: (id: string, name: string) => Promise<void>;
10
10
  onArchive: (id: string) => Promise<void>;
11
+ onOpenGitModal?: (project: Project) => void;
11
12
  }
12
13
 
13
14
  export function ProjectSelector({
14
- projects, selectedProjectId, onSelect, onCreate, onRename, onArchive,
15
+ projects, selectedProjectId, onSelect, onCreate, onRename, onArchive, onOpenGitModal,
15
16
  }: ProjectSelectorProps) {
16
17
  const [open, setOpen] = useState(false);
17
18
  const [creating, setCreating] = useState(false);
@@ -123,6 +124,21 @@ export function ProjectSelector({
123
124
  {project.name}
124
125
  </button>
125
126
  <div className="project-selector-item-actions">
127
+ {onOpenGitModal && (
128
+ <button
129
+ className={`project-selector-action-btn project-selector-git-btn${project.repoUrl ? ' connected' : ''}`}
130
+ title="Git Repository"
131
+ onClick={(e) => {
132
+ e.stopPropagation();
133
+ setOpen(false);
134
+ onOpenGitModal(project);
135
+ }}
136
+ >
137
+ <svg viewBox="0 0 16 16" width="14" height="14" fill="currentColor">
138
+ <path d="M8 0c4.42 0 8 3.58 8 8a8.013 8.013 0 0 1-5.45 7.59c-.4.08-.55-.17-.55-.38 0-.27.01-1.13.01-2.2 0-.75-.25-1.23-.54-1.48 1.78-.2 3.65-.88 3.65-3.95 0-.88-.31-1.59-.82-2.15.08-.2.36-1.02-.08-2.12 0 0-.67-.22-2.2.82-.64-.18-1.32-.27-2-.27-.68 0-1.36.09-2 .27-1.53-1.03-2.2-.82-2.2-.82-.44 1.1-.16 1.92-.08 2.12-.51.56-.82 1.28-.82 2.15 0 3.06 1.86 3.75 3.64 3.95-.23.2-.44.55-.51 1.07-.46.21-1.61.55-2.33-.66-.15-.24-.6-.83-1.23-.82-.67.01-.27.38.01.53.34.19.73.9.82 1.13.16.45.68 1.31 2.69.94 0 .67.01 1.3.01 1.49 0 .21-.15.45-.55.38A7.995 7.995 0 0 1 0 8c0-4.42 3.58-8 8-8Z" />
139
+ </svg>
140
+ </button>
141
+ )}
126
142
  <button
127
143
  className="project-selector-action-btn"
128
144
  title="Rename"