jettypod 4.4.118 → 4.4.121

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 (240) hide show
  1. package/.env +4 -3
  2. package/Cargo.lock +6450 -0
  3. package/Cargo.toml +35 -0
  4. package/README.md +5 -1
  5. package/TAURI-MIGRATION-PLAN.md +840 -0
  6. package/apps/dashboard/app/connect-claude/page.tsx +5 -6
  7. package/apps/dashboard/app/decision/[id]/page.tsx +63 -58
  8. package/apps/dashboard/app/demo/gates/page.tsx +43 -45
  9. package/apps/dashboard/app/design-system/page.tsx +868 -0
  10. package/apps/dashboard/app/globals.css +80 -4
  11. package/apps/dashboard/app/install-claude/page.tsx +4 -6
  12. package/apps/dashboard/app/login/page.tsx +72 -54
  13. package/apps/dashboard/app/page.tsx +101 -48
  14. package/apps/dashboard/app/settings/page.tsx +61 -13
  15. package/apps/dashboard/app/signup/page.tsx +242 -0
  16. package/apps/dashboard/app/subscribe/page.tsx +0 -2
  17. package/apps/dashboard/app/tests/page.tsx +37 -4
  18. package/apps/dashboard/app/welcome/page.tsx +13 -16
  19. package/apps/dashboard/app/work/[id]/page.tsx +117 -118
  20. package/apps/dashboard/app/work/[id]/proof/page.tsx +1489 -0
  21. package/apps/dashboard/components/AppShell.tsx +92 -85
  22. package/apps/dashboard/components/CardMenu.tsx +45 -12
  23. package/apps/dashboard/components/ClaudePanel.tsx +771 -850
  24. package/apps/dashboard/components/ClaudePanelInput.tsx +43 -15
  25. package/apps/dashboard/components/ConnectClaudeScreen.tsx +17 -34
  26. package/apps/dashboard/components/CopyableId.tsx +3 -4
  27. package/apps/dashboard/components/DetailReviewActions.tsx +100 -0
  28. package/apps/dashboard/components/DragContext.tsx +134 -63
  29. package/apps/dashboard/components/DraggableCard.tsx +3 -5
  30. package/apps/dashboard/components/DropZone.tsx +6 -7
  31. package/apps/dashboard/components/EditableDetailDescription.tsx +7 -13
  32. package/apps/dashboard/components/EditableDetailTitle.tsx +6 -13
  33. package/apps/dashboard/components/EditableTitle.tsx +26 -7
  34. package/apps/dashboard/components/ElapsedTimer.tsx +66 -0
  35. package/apps/dashboard/components/EpicGroup.tsx +359 -0
  36. package/apps/dashboard/components/GateCard.tsx +79 -17
  37. package/apps/dashboard/components/GateChoiceCard.tsx +15 -18
  38. package/apps/dashboard/components/InstallClaudeScreen.tsx +15 -32
  39. package/apps/dashboard/components/JettyLoader.tsx +37 -0
  40. package/apps/dashboard/components/KanbanBoard.tsx +368 -958
  41. package/apps/dashboard/components/KanbanCard.tsx +740 -0
  42. package/apps/dashboard/components/LazyCard.tsx +62 -0
  43. package/apps/dashboard/components/LazyMarkdown.tsx +11 -0
  44. package/apps/dashboard/components/MainNav.tsx +38 -73
  45. package/apps/dashboard/components/MessageBlock.tsx +468 -0
  46. package/apps/dashboard/components/ModeStartCard.tsx +15 -16
  47. package/apps/dashboard/components/OnboardingWelcome.tsx +213 -0
  48. package/apps/dashboard/components/PlaceholderCard.tsx +3 -4
  49. package/apps/dashboard/components/ProjectSwitcher.tsx +30 -30
  50. package/apps/dashboard/components/PrototypeTimeline.tsx +72 -51
  51. package/apps/dashboard/components/RealTimeKanbanWrapper.tsx +406 -388
  52. package/apps/dashboard/components/RealTimeTestsWrapper.tsx +373 -235
  53. package/apps/dashboard/components/ReviewFooter.tsx +139 -0
  54. package/apps/dashboard/components/SessionList.tsx +19 -19
  55. package/apps/dashboard/components/SubscribeContent.tsx +91 -47
  56. package/apps/dashboard/components/TestTree.tsx +16 -16
  57. package/apps/dashboard/components/TipCard.tsx +16 -17
  58. package/apps/dashboard/components/Toast.tsx +5 -6
  59. package/apps/dashboard/components/TypeIcon.tsx +55 -0
  60. package/apps/dashboard/components/ViewModeToolbar.tsx +104 -0
  61. package/apps/dashboard/components/WaveCompletionAnimation.tsx +52 -65
  62. package/apps/dashboard/components/WelcomeScreen.tsx +19 -35
  63. package/apps/dashboard/components/WorkItemHeader.tsx +4 -5
  64. package/apps/dashboard/components/WorkItemTree.tsx +11 -32
  65. package/apps/dashboard/components/settings/AccountSection.tsx +55 -35
  66. package/apps/dashboard/components/settings/AiContextSection.tsx +89 -0
  67. package/apps/dashboard/components/settings/ContextDocumentsSection.tsx +317 -0
  68. package/apps/dashboard/components/settings/EnvVarsSection.tsx +74 -152
  69. package/apps/dashboard/components/settings/GeneralSection.tsx +162 -56
  70. package/apps/dashboard/components/settings/ProjectStackSection.tsx +948 -0
  71. package/apps/dashboard/components/settings/SettingsLayout.tsx +4 -5
  72. package/apps/dashboard/components/ui/Button.tsx +104 -0
  73. package/apps/dashboard/components/ui/Input.tsx +78 -0
  74. package/apps/dashboard/components.json +1 -1
  75. package/apps/dashboard/contexts/ClaudeSessionContext.tsx +711 -418
  76. package/apps/dashboard/contexts/ConnectionStatusContext.tsx +25 -5
  77. package/apps/dashboard/contexts/UsageContext.tsx +87 -32
  78. package/apps/dashboard/dev.sh +35 -0
  79. package/apps/dashboard/eslint.config.mjs +9 -9
  80. package/apps/dashboard/hooks/useKanbanAnimation.ts +29 -0
  81. package/apps/dashboard/hooks/useKanbanUndo.ts +83 -0
  82. package/apps/dashboard/hooks/useWebSocket.ts +138 -83
  83. package/apps/dashboard/index.html +73 -0
  84. package/apps/dashboard/lib/constants.ts +43 -0
  85. package/apps/dashboard/lib/data-bridge.ts +722 -0
  86. package/apps/dashboard/lib/db.ts +69 -1265
  87. package/apps/dashboard/lib/environment-config.ts +173 -0
  88. package/apps/dashboard/lib/environment-verification.ts +119 -0
  89. package/apps/dashboard/lib/kanban-utils.ts +270 -0
  90. package/apps/dashboard/lib/proof-run.ts +495 -0
  91. package/apps/dashboard/lib/proof-scenario-runner.ts +346 -0
  92. package/apps/dashboard/lib/run-migrations.js +27 -2
  93. package/apps/dashboard/lib/service-recovery.ts +326 -0
  94. package/apps/dashboard/lib/session-state-machine.ts +1 -0
  95. package/apps/dashboard/lib/session-state-utils.ts +0 -164
  96. package/apps/dashboard/lib/session-stream-manager.ts +308 -134
  97. package/apps/dashboard/lib/shadows.ts +7 -0
  98. package/apps/dashboard/lib/stream-manager-registry.ts +46 -6
  99. package/apps/dashboard/lib/tauri-bridge.ts +102 -0
  100. package/apps/dashboard/lib/tauri.ts +106 -0
  101. package/apps/dashboard/lib/utils.ts +6 -0
  102. package/apps/dashboard/next-env.d.ts +1 -1
  103. package/apps/dashboard/package.json +21 -32
  104. package/apps/dashboard/public/bug-icon.png +0 -0
  105. package/apps/dashboard/public/buoy-icon.png +0 -0
  106. package/apps/dashboard/public/fonts/Satoshi-Variable.woff2 +0 -0
  107. package/apps/dashboard/public/fonts/Satoshi-VariableItalic.woff2 +0 -0
  108. package/apps/dashboard/public/in-flight-seagull.png +0 -0
  109. package/apps/dashboard/public/jetty-icon-loading-alt.svg +11 -0
  110. package/apps/dashboard/public/jetty-icon-loading.svg +11 -0
  111. package/apps/dashboard/public/jettypod_logo.png +0 -0
  112. package/apps/dashboard/public/pier-icon.png +0 -0
  113. package/apps/dashboard/public/star-icon.png +0 -0
  114. package/apps/dashboard/public/wrench-icon.png +0 -0
  115. package/apps/dashboard/scripts/tauri-build.js +228 -0
  116. package/apps/dashboard/scripts/upload-tauri-to-r2.js +125 -0
  117. package/apps/dashboard/scripts/ws-server.js +191 -0
  118. package/apps/dashboard/src/main.tsx +12 -0
  119. package/apps/dashboard/src/router.tsx +107 -0
  120. package/apps/dashboard/src/vite-env.d.ts +1 -0
  121. package/apps/dashboard/tsconfig.json +7 -12
  122. package/apps/dashboard/tsconfig.tsbuildinfo +1 -1
  123. package/apps/dashboard/vite.config.ts +33 -0
  124. package/apps/update-server/src/index.ts +228 -80
  125. package/claude-hooks/global-guardrails.js +14 -13
  126. package/crates/jettypod-cli/Cargo.toml +19 -0
  127. package/crates/jettypod-cli/src/commands.rs +1249 -0
  128. package/crates/jettypod-cli/src/main.rs +595 -0
  129. package/crates/jettypod-core/Cargo.toml +26 -0
  130. package/crates/jettypod-core/build.rs +98 -0
  131. package/crates/jettypod-core/migrations/V1__baseline.sql +197 -0
  132. package/crates/jettypod-core/migrations/V2__work_items_indexes.sql +6 -0
  133. package/crates/jettypod-core/migrations/V3__qa_steps.sql +2 -0
  134. package/crates/jettypod-core/src/auth.rs +294 -0
  135. package/crates/jettypod-core/src/config.rs +397 -0
  136. package/crates/jettypod-core/src/db/mod.rs +507 -0
  137. package/crates/jettypod-core/src/db/recovery.rs +114 -0
  138. package/crates/jettypod-core/src/db/startup.rs +101 -0
  139. package/crates/jettypod-core/src/db/validate.rs +149 -0
  140. package/crates/jettypod-core/src/error.rs +76 -0
  141. package/crates/jettypod-core/src/git.rs +458 -0
  142. package/crates/jettypod-core/src/lib.rs +20 -0
  143. package/crates/jettypod-core/src/sessions.rs +625 -0
  144. package/crates/jettypod-core/src/skills.rs +556 -0
  145. package/crates/jettypod-core/src/work.rs +1086 -0
  146. package/crates/jettypod-core/src/worktree.rs +628 -0
  147. package/crates/jettypod-core/src/ws.rs +767 -0
  148. package/cucumber-test.cjs +6 -0
  149. package/cucumber.js +9 -3
  150. package/docs/COMMAND_REFERENCE.md +34 -0
  151. package/hooks/post-checkout +32 -75
  152. package/hooks/post-merge +111 -10
  153. package/jest.setup.js +1 -0
  154. package/jettypod.js +145 -116
  155. package/lib/bdd-preflight.js +96 -0
  156. package/lib/chore-taxonomy.js +33 -10
  157. package/lib/database.js +36 -16
  158. package/lib/db-watcher.js +1 -1
  159. package/lib/git-hooks/pre-commit +1 -1
  160. package/lib/jettypod-backup.js +27 -4
  161. package/lib/merge-lock.js +111 -253
  162. package/lib/migrations/027-plan-at-creation-column.js +3 -1
  163. package/lib/migrations/029-remove-autoincrement.js +307 -0
  164. package/lib/migrations/029-rename-corrupted-to-cleaned.js +149 -0
  165. package/lib/migrations/030-rejection-round-columns.js +54 -0
  166. package/lib/migrations/031-session-isolation-index.js +17 -0
  167. package/lib/migrations/index.js +47 -4
  168. package/lib/schema.js +10 -5
  169. package/lib/seed-onboarding.js +1 -1
  170. package/lib/update-command/index.js +9 -175
  171. package/lib/work-commands/index.js +144 -19
  172. package/lib/work-tracking/index.js +148 -27
  173. package/lib/worktree-diagnostics.js +16 -16
  174. package/lib/worktree-facade.js +1 -1
  175. package/lib/worktree-manager.js +8 -8
  176. package/lib/worktree-reconciler.js +5 -5
  177. package/package.json +9 -2
  178. package/scripts/ndjson-to-cucumber-json.js +152 -0
  179. package/scripts/postinstall.js +25 -0
  180. package/skills-templates/bug-mode/SKILL.md +79 -20
  181. package/skills-templates/bug-planning/SKILL.md +25 -29
  182. package/skills-templates/chore-mode/SKILL.md +171 -69
  183. package/skills-templates/chore-mode/verification.js +51 -10
  184. package/skills-templates/chore-planning/SKILL.md +47 -18
  185. package/skills-templates/design-system-selection/SKILL.md +273 -0
  186. package/skills-templates/epic-planning/SKILL.md +82 -48
  187. package/skills-templates/external-transition/SKILL.md +47 -47
  188. package/skills-templates/feature-planning/SKILL.md +173 -74
  189. package/skills-templates/production-mode/SKILL.md +69 -49
  190. package/skills-templates/request-routing/SKILL.md +4 -4
  191. package/skills-templates/simple-improvement/SKILL.md +74 -29
  192. package/skills-templates/speed-mode/SKILL.md +217 -141
  193. package/skills-templates/stable-mode/SKILL.md +148 -89
  194. package/apps/dashboard/README.md +0 -36
  195. package/apps/dashboard/app/api/claude/[workItemId]/message/route.ts +0 -386
  196. package/apps/dashboard/app/api/claude/[workItemId]/pin/route.ts +0 -24
  197. package/apps/dashboard/app/api/claude/[workItemId]/route.ts +0 -167
  198. package/apps/dashboard/app/api/claude/sessions/[sessionId]/content/route.ts +0 -52
  199. package/apps/dashboard/app/api/claude/sessions/[sessionId]/message/route.ts +0 -378
  200. package/apps/dashboard/app/api/claude/sessions/[sessionId]/pin/route.ts +0 -24
  201. package/apps/dashboard/app/api/claude/sessions/cleanup/route.ts +0 -34
  202. package/apps/dashboard/app/api/claude/sessions/route.ts +0 -184
  203. package/apps/dashboard/app/api/decisions/[id]/route.ts +0 -25
  204. package/apps/dashboard/app/api/internal/set-project/route.ts +0 -17
  205. package/apps/dashboard/app/api/kanban/route.ts +0 -15
  206. package/apps/dashboard/app/api/settings/env-vars/route.ts +0 -125
  207. package/apps/dashboard/app/api/settings/general/route.ts +0 -21
  208. package/apps/dashboard/app/api/tests/route.ts +0 -9
  209. package/apps/dashboard/app/api/tests/run/route.ts +0 -82
  210. package/apps/dashboard/app/api/tests/run/stream/route.ts +0 -71
  211. package/apps/dashboard/app/api/tests/undefined/route.ts +0 -9
  212. package/apps/dashboard/app/api/usage/route.ts +0 -17
  213. package/apps/dashboard/app/api/work/[id]/description/route.ts +0 -21
  214. package/apps/dashboard/app/api/work/[id]/epic/route.ts +0 -21
  215. package/apps/dashboard/app/api/work/[id]/order/route.ts +0 -21
  216. package/apps/dashboard/app/api/work/[id]/status/route.ts +0 -21
  217. package/apps/dashboard/app/api/work/[id]/title/route.ts +0 -21
  218. package/apps/dashboard/app/layout.tsx +0 -43
  219. package/apps/dashboard/components/UpgradeBanner.tsx +0 -29
  220. package/apps/dashboard/electron/ipc-handlers.js +0 -1028
  221. package/apps/dashboard/electron/main.js +0 -2124
  222. package/apps/dashboard/electron/preload.js +0 -123
  223. package/apps/dashboard/electron/session-manager.js +0 -141
  224. package/apps/dashboard/electron-builder.config.js +0 -357
  225. package/apps/dashboard/hooks/useClaudeSessions.ts +0 -299
  226. package/apps/dashboard/lib/claude-process-manager.ts +0 -492
  227. package/apps/dashboard/lib/db-bridge.ts +0 -282
  228. package/apps/dashboard/lib/prototypes.ts +0 -202
  229. package/apps/dashboard/lib/test-results-db.ts +0 -307
  230. package/apps/dashboard/lib/tests.ts +0 -282
  231. package/apps/dashboard/next.config.js +0 -50
  232. package/apps/dashboard/postcss.config.mjs +0 -7
  233. package/apps/dashboard/public/file.svg +0 -1
  234. package/apps/dashboard/public/globe.svg +0 -1
  235. package/apps/dashboard/public/next.svg +0 -1
  236. package/apps/dashboard/public/vercel.svg +0 -1
  237. package/apps/dashboard/public/window.svg +0 -1
  238. package/apps/dashboard/scripts/download-node.js +0 -104
  239. package/apps/dashboard/scripts/upload-to-r2.js +0 -89
  240. package/docs/bdd-guidance.md +0 -390
@@ -0,0 +1,326 @@
1
+ /**
2
+ * Service Recovery — pre-flight checks and stderr-based recovery
3
+ * for the QA environment launch flow.
4
+ *
5
+ * Phase 1 (pre-flight): Detects and fixes common issues BEFORE spawning.
6
+ * Phase 2 (stderr recovery): Matches crash output to known patterns and retries.
7
+ *
8
+ * Uses Tauri IPC (run_shell_command) for all shell operations since this
9
+ * runs in browser context where child_process is unavailable.
10
+ */
11
+
12
+ import { runShellCommand, isTauri } from './tauri';
13
+ import type { ServiceDefinition } from './environment-config';
14
+
15
+ // ─── Types ──────────────────────────────────────────────────
16
+
17
+ export interface RecoveryStatus {
18
+ phase: 'preflight' | 'recovery';
19
+ service: string;
20
+ action: string;
21
+ severity?: 'info' | 'warning';
22
+ }
23
+
24
+ export interface PreflightResult {
25
+ check: string;
26
+ label: string;
27
+ fixed: boolean;
28
+ reason?: string;
29
+ }
30
+
31
+ export interface RecoveryResult {
32
+ shouldRetry: boolean;
33
+ fixApplied: string | null;
34
+ reason: string;
35
+ }
36
+
37
+ interface PreflightCheck {
38
+ name: string;
39
+ label: (ctx: Record<string, unknown>) => string;
40
+ detect: (service: ServiceDefinition, cwd: string | null) => Promise<Record<string, unknown> | null>;
41
+ fix: (ctx: Record<string, unknown>) => Promise<{ fixed: boolean; reason?: string }>;
42
+ }
43
+
44
+ interface StderrPattern {
45
+ name: string;
46
+ pattern: RegExp;
47
+ label: () => string;
48
+ fix: (ctx: { stderr: string; service: ServiceDefinition; cwd: string | null }) => Promise<{ fixed: boolean; reason?: string }>;
49
+ }
50
+
51
+ // ─── Dependency Install Strategies ──────────────────────────
52
+
53
+ interface DependencyStrategy {
54
+ /** File that indicates this ecosystem */
55
+ marker: string;
56
+ /** Command to run */
57
+ command: string;
58
+ /** Arguments */
59
+ args: string[];
60
+ /** Timeout in ms */
61
+ timeoutMs: number;
62
+ }
63
+
64
+ const DEPENDENCY_STRATEGIES: DependencyStrategy[] = [
65
+ { marker: 'package.json', command: 'npm', args: ['install'], timeoutMs: 120_000 },
66
+ { marker: 'requirements.txt', command: 'pip', args: ['install', '-r', 'requirements.txt'], timeoutMs: 120_000 },
67
+ { marker: 'pyproject.toml', command: 'pip', args: ['install', '.'], timeoutMs: 120_000 },
68
+ { marker: 'Gemfile', command: 'bundle', args: ['install'], timeoutMs: 120_000 },
69
+ { marker: 'go.mod', command: 'go', args: ['mod', 'download'], timeoutMs: 120_000 },
70
+ { marker: 'Cargo.toml', command: 'cargo', args: ['build'], timeoutMs: 300_000 },
71
+ ];
72
+
73
+ /** Detect which dependency strategy applies by checking for marker files. */
74
+ async function detectDependencyStrategy(cwd: string): Promise<DependencyStrategy | null> {
75
+ for (const strategy of DEPENDENCY_STRATEGIES) {
76
+ const result = await runShellCommand('test', ['-f', strategy.marker], { cwd });
77
+ if (result.exit_code === 0) return strategy;
78
+ }
79
+ return null;
80
+ }
81
+
82
+ /** Check if a dependency directory exists (node_modules, venv, vendor, etc.) */
83
+ async function hasDependencyDir(cwd: string, strategy: DependencyStrategy): Promise<boolean> {
84
+ const dirMap: Record<string, string> = {
85
+ 'package.json': 'node_modules',
86
+ 'Gemfile': 'vendor/bundle',
87
+ 'go.mod': 'vendor',
88
+ };
89
+ const dir = dirMap[strategy.marker];
90
+ if (!dir) return true; // No dependency dir concept for this ecosystem
91
+ const result = await runShellCommand('test', ['-d', dir], { cwd });
92
+ return result.exit_code === 0;
93
+ }
94
+
95
+ // ─── Pre-flight Checks ─────────────────────────────────────
96
+
97
+ export const PREFLIGHT_CHECKS: PreflightCheck[] = [
98
+ {
99
+ name: 'port-in-use',
100
+ label: (ctx) => `Freeing port ${ctx.port}...`,
101
+ detect: async (service) => {
102
+ if (!service.port) return null;
103
+ // Use lsof to check if port is in use
104
+ const result = await runShellCommand('lsof', ['-ti', `:${service.port}`], { timeoutMs: 5000 });
105
+ if (result.exit_code === 0 && result.stdout.trim()) {
106
+ return { port: service.port, pid: result.stdout.trim().split('\n')[0] };
107
+ }
108
+ return null;
109
+ },
110
+ fix: async (ctx) => {
111
+ const pid = ctx.pid as string;
112
+ if (!pid) return { fixed: false, reason: 'Could not identify process holding port' };
113
+
114
+ // Kill the process holding the port
115
+ await runShellCommand('kill', ['-TERM', pid], { timeoutMs: 5000 });
116
+
117
+ // Wait briefly then verify it's free
118
+ await new Promise(resolve => setTimeout(resolve, 500));
119
+
120
+ const check = await runShellCommand('lsof', ['-ti', `:${ctx.port}`], { timeoutMs: 5000 });
121
+ if (check.exit_code === 0 && check.stdout.trim()) {
122
+ // Still in use — force kill
123
+ await runShellCommand('kill', ['-KILL', pid], { timeoutMs: 5000 });
124
+ await new Promise(resolve => setTimeout(resolve, 300));
125
+ }
126
+ return { fixed: true };
127
+ },
128
+ },
129
+ {
130
+ name: 'missing-dependencies',
131
+ label: () => 'Installing dependencies...',
132
+ detect: async (service, cwd) => {
133
+ if (!cwd) return null;
134
+ const strategy = await detectDependencyStrategy(cwd);
135
+ if (!strategy) return null;
136
+ const hasDeps = await hasDependencyDir(cwd, strategy);
137
+ return hasDeps ? null : { cwd, strategy };
138
+ },
139
+ fix: async (ctx) => {
140
+ const strategy = ctx.strategy as DependencyStrategy;
141
+ const cwd = ctx.cwd as string;
142
+ const result = await runShellCommand(strategy.command, strategy.args, {
143
+ cwd,
144
+ timeoutMs: strategy.timeoutMs,
145
+ });
146
+ if (result.exit_code === 0) return { fixed: true };
147
+ return { fixed: false, reason: `${strategy.command} ${strategy.args.join(' ')} failed: ${result.stderr.slice(0, 200)}` };
148
+ },
149
+ },
150
+ {
151
+ name: 'command-not-found',
152
+ label: (ctx) => `Command "${ctx.command}" not found`,
153
+ detect: async (service) => {
154
+ const command = service.command.split(/\s+/)[0];
155
+ const result = await runShellCommand('which', [command], { timeoutMs: 5000 });
156
+ if (result.exit_code !== 0) return { command };
157
+ return null;
158
+ },
159
+ fix: async (ctx) => {
160
+ // Can't auto-fix missing commands — but we fail fast with a clear message
161
+ return { fixed: false, reason: `"${ctx.command}" is not installed` };
162
+ },
163
+ },
164
+ ];
165
+
166
+ // ─── Stderr Patterns ────────────────────────────────────────
167
+
168
+ export const STDERR_PATTERNS: StderrPattern[] = [
169
+ {
170
+ name: 'eaddrinuse',
171
+ pattern: /EADDRINUSE|address already in use|port.*already.*in.*use/i,
172
+ label: () => 'Port conflict detected, freeing port...',
173
+ fix: async (ctx) => {
174
+ // Extract port from stderr if possible, otherwise use service port
175
+ const portMatch = ctx.stderr.match(/port\s+(\d+)|:(\d+)/);
176
+ const port = portMatch ? parseInt(portMatch[1] || portMatch[2]) : ctx.service.port;
177
+ if (!port) return { fixed: false, reason: 'Could not determine port' };
178
+
179
+ const lsof = await runShellCommand('lsof', ['-ti', `:${port}`], { timeoutMs: 5000 });
180
+ const pid = lsof.stdout.trim().split('\n')[0];
181
+ if (pid) {
182
+ await runShellCommand('kill', ['-TERM', pid], { timeoutMs: 5000 });
183
+ await new Promise(resolve => setTimeout(resolve, 500));
184
+ return { fixed: true };
185
+ }
186
+ // Port may have been freed by the crashing process itself
187
+ const recheck = await runShellCommand('lsof', ['-ti', `:${port}`], { timeoutMs: 5000 });
188
+ return { fixed: recheck.exit_code !== 0 || !recheck.stdout.trim() };
189
+ },
190
+ },
191
+ {
192
+ name: 'module-not-found',
193
+ pattern: /Cannot find module|MODULE_NOT_FOUND|Error: Cannot find package|ModuleNotFoundError|No module named|cannot load such file|cannot find package/i,
194
+ label: () => 'Missing dependency detected, installing...',
195
+ fix: async (ctx) => {
196
+ const cwd = ctx.cwd;
197
+ if (!cwd) return { fixed: false, reason: 'No project directory' };
198
+ const strategy = await detectDependencyStrategy(cwd);
199
+ if (!strategy) return { fixed: false, reason: 'Unknown project type' };
200
+ const result = await runShellCommand(strategy.command, strategy.args, {
201
+ cwd,
202
+ timeoutMs: strategy.timeoutMs,
203
+ });
204
+ return { fixed: result.exit_code === 0, reason: result.exit_code !== 0 ? result.stderr.slice(0, 200) : undefined };
205
+ },
206
+ },
207
+ {
208
+ name: 'eacces-permission',
209
+ pattern: /EACCES|permission denied/i,
210
+ label: () => 'Permission error detected',
211
+ fix: async () => {
212
+ return { fixed: false, reason: 'Permission denied — check file permissions' };
213
+ },
214
+ },
215
+ {
216
+ name: 'env-var-missing',
217
+ pattern: /env.*not.*defined|missing.*environment|undefined.*env|process\.env\.\w+.*undefined/i,
218
+ label: () => 'Missing environment variable detected',
219
+ fix: async () => {
220
+ return { fixed: false, reason: 'Missing environment variable — check .env file' };
221
+ },
222
+ },
223
+ ];
224
+
225
+ // ─── Service Recovery Orchestrator ──────────────────────────
226
+
227
+ export class ServiceRecovery {
228
+ private onStatus: (status: RecoveryStatus) => void;
229
+
230
+ constructor(onStatus?: (status: RecoveryStatus) => void) {
231
+ this.onStatus = onStatus || (() => {});
232
+ }
233
+
234
+ /**
235
+ * Phase 1: Run pre-flight checks before spawning a service.
236
+ * Returns list of issues found and whether they were fixed.
237
+ */
238
+ async runPreflightChecks(
239
+ service: ServiceDefinition,
240
+ cwd: string | null,
241
+ ): Promise<PreflightResult[]> {
242
+ if (!isTauri()) return [];
243
+
244
+ const results: PreflightResult[] = [];
245
+
246
+ for (const check of PREFLIGHT_CHECKS) {
247
+ try {
248
+ const issue = await check.detect(service, cwd);
249
+ if (issue) {
250
+ const label = check.label(issue);
251
+ this.onStatus({ phase: 'preflight', service: service.name, action: label });
252
+
253
+ try {
254
+ const result = await check.fix(issue);
255
+ results.push({ check: check.name, label, ...result });
256
+
257
+ if (!result.fixed) {
258
+ this.onStatus({
259
+ phase: 'preflight',
260
+ service: service.name,
261
+ action: `Could not fix: ${result.reason}`,
262
+ severity: 'warning',
263
+ });
264
+ }
265
+ } catch (fixErr) {
266
+ const reason = fixErr instanceof Error ? fixErr.message : String(fixErr);
267
+ console.error(`[ServiceRecovery] Fix "${check.name}" threw for "${service.name}":`, reason);
268
+ results.push({ check: check.name, label, fixed: false, reason });
269
+ this.onStatus({
270
+ phase: 'preflight',
271
+ service: service.name,
272
+ action: `Could not fix: ${reason}`,
273
+ severity: 'warning',
274
+ });
275
+ }
276
+ }
277
+ } catch (detectErr) {
278
+ const reason = detectErr instanceof Error ? detectErr.message : String(detectErr);
279
+ console.error(`[ServiceRecovery] Check "${check.name}" threw for "${service.name}":`, reason);
280
+ results.push({ check: check.name, label: check.name, fixed: false, reason });
281
+ this.onStatus({
282
+ phase: 'preflight',
283
+ service: service.name,
284
+ action: `Check failed: ${reason}`,
285
+ severity: 'warning',
286
+ });
287
+ }
288
+ }
289
+
290
+ return results;
291
+ }
292
+
293
+ /**
294
+ * Phase 2: Analyze stderr from a crashed service and attempt recovery.
295
+ * Returns whether the service should be retried.
296
+ */
297
+ async analyzeFailure(
298
+ service: ServiceDefinition,
299
+ stderr: string,
300
+ cwd: string | null,
301
+ ): Promise<RecoveryResult> {
302
+ if (!isTauri()) return { shouldRetry: false, fixApplied: null, reason: 'Not in Tauri' };
303
+
304
+ for (const pattern of STDERR_PATTERNS) {
305
+ if (pattern.pattern.test(stderr)) {
306
+ const label = pattern.label();
307
+ this.onStatus({ phase: 'recovery', service: service.name, action: label });
308
+
309
+ try {
310
+ const result = await pattern.fix({ stderr, service, cwd });
311
+ if (result.fixed) {
312
+ return { shouldRetry: true, fixApplied: pattern.name, reason: label };
313
+ } else {
314
+ return { shouldRetry: false, fixApplied: null, reason: result.reason || 'Fix failed' };
315
+ }
316
+ } catch (fixErr) {
317
+ const reason = fixErr instanceof Error ? fixErr.message : String(fixErr);
318
+ console.error(`[ServiceRecovery] Recovery fix "${pattern.name}" threw for "${service.name}":`, reason);
319
+ return { shouldRetry: false, fixApplied: null, reason };
320
+ }
321
+ }
322
+ }
323
+
324
+ return { shouldRetry: false, fixApplied: null, reason: 'Unrecognized failure' };
325
+ }
326
+ }
@@ -280,6 +280,7 @@ export function streamStatusToSessionState(status: string): SessionState {
280
280
  case 'idle':
281
281
  return 'idle';
282
282
  case 'connecting':
283
+ case 'creating': // Stream manager uses 'creating' for first-message delay
283
284
  return 'connecting';
284
285
  case 'streaming':
285
286
  return 'streaming';
@@ -57,170 +57,6 @@ export function createSyncRef<T>(initialValue: T): SyncRef<T> {
57
57
  };
58
58
  }
59
59
 
60
- // ============================================================================
61
- // Atomic State Updates
62
- // ============================================================================
63
-
64
- /**
65
- * Result of an atomic update operation.
66
- */
67
- export interface AtomicUpdateResult<T> {
68
- /** Whether the update was applied */
69
- applied: boolean;
70
- /** The resulting state (whether updated or not) */
71
- state: T;
72
- /** If not applied, the reason */
73
- reason?: 'version_mismatch' | 'condition_failed' | 'locked';
74
- }
75
-
76
- /**
77
- * Options for atomic update.
78
- */
79
- export interface AtomicUpdateOptions<T> {
80
- /** Only apply if this condition returns true */
81
- condition?: (currentState: T) => boolean;
82
- /** Timeout for acquiring lock (ms). Default: 5000 */
83
- timeoutMs?: number;
84
- }
85
-
86
- /**
87
- * Atomic state container that prevents concurrent update races.
88
- * Uses optimistic locking with version numbers.
89
- */
90
- export class AtomicState<T> {
91
- private state: T;
92
- private version: number = 0;
93
- private locked: boolean = false;
94
- private lockQueue: Array<() => void> = [];
95
-
96
- constructor(initialState: T) {
97
- this.state = initialState;
98
- }
99
-
100
- /**
101
- * Get current state and version.
102
- */
103
- get(): { state: T; version: number } {
104
- return { state: this.state, version: this.version };
105
- }
106
-
107
- /**
108
- * Attempt to update state atomically.
109
- * Only succeeds if the version matches (no concurrent updates).
110
- *
111
- * @param expectedVersion - The version you read before making changes
112
- * @param newState - The new state to apply
113
- * @param options - Optional conditions and timeout
114
- */
115
- compareAndSet(
116
- expectedVersion: number,
117
- newState: T,
118
- options?: AtomicUpdateOptions<T>
119
- ): AtomicUpdateResult<T> {
120
- // Check version
121
- if (this.version !== expectedVersion) {
122
- return {
123
- applied: false,
124
- state: this.state,
125
- reason: 'version_mismatch',
126
- };
127
- }
128
-
129
- // Check condition if provided
130
- if (options?.condition && !options.condition(this.state)) {
131
- return {
132
- applied: false,
133
- state: this.state,
134
- reason: 'condition_failed',
135
- };
136
- }
137
-
138
- // Apply update
139
- this.state = newState;
140
- this.version++;
141
-
142
- return {
143
- applied: true,
144
- state: this.state,
145
- };
146
- }
147
-
148
- /**
149
- * Update state with a transform function, handling retries automatically.
150
- * This is the recommended way to update state when you need read-modify-write.
151
- *
152
- * @param transform - Function that takes current state and returns new state
153
- * @param maxRetries - Maximum retries on version conflict. Default: 3
154
- */
155
- update(transform: (current: T) => T, maxRetries: number = 3): AtomicUpdateResult<T> {
156
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
157
- const { state: current, version } = this.get();
158
- const newState = transform(current);
159
- const result = this.compareAndSet(version, newState);
160
-
161
- if (result.applied) {
162
- return result;
163
- }
164
-
165
- // Version mismatch - retry with fresh state
166
- if (result.reason !== 'version_mismatch') {
167
- return result;
168
- }
169
- }
170
-
171
- // All retries exhausted
172
- return {
173
- applied: false,
174
- state: this.state,
175
- reason: 'version_mismatch',
176
- };
177
- }
178
-
179
- /**
180
- * Acquire exclusive lock for complex multi-step operations.
181
- * Use sparingly - prefer compareAndSet or update for simple cases.
182
- */
183
- async lock(timeoutMs: number = 5000): Promise<() => void> {
184
- if (!this.locked) {
185
- this.locked = true;
186
- return () => this.unlock();
187
- }
188
-
189
- // Wait for lock
190
- return new Promise((resolve, reject) => {
191
- const timeout = setTimeout(() => {
192
- const index = this.lockQueue.indexOf(tryAcquire);
193
- if (index >= 0) this.lockQueue.splice(index, 1);
194
- reject(new Error('Lock acquisition timeout'));
195
- }, timeoutMs);
196
-
197
- const tryAcquire = () => {
198
- clearTimeout(timeout);
199
- this.locked = true;
200
- resolve(() => this.unlock());
201
- };
202
-
203
- this.lockQueue.push(tryAcquire);
204
- });
205
- }
206
-
207
- private unlock(): void {
208
- if (this.lockQueue.length > 0) {
209
- const next = this.lockQueue.shift();
210
- next?.();
211
- } else {
212
- this.locked = false;
213
- }
214
- }
215
-
216
- /**
217
- * Get current version (for debugging/monitoring).
218
- */
219
- getVersion(): number {
220
- return this.version;
221
- }
222
- }
223
-
224
60
  // ============================================================================
225
61
  // Session-Specific Helpers
226
62
  // ============================================================================