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.
- package/.env +4 -3
- package/Cargo.lock +6450 -0
- package/Cargo.toml +35 -0
- package/README.md +5 -1
- package/TAURI-MIGRATION-PLAN.md +840 -0
- package/apps/dashboard/app/connect-claude/page.tsx +5 -6
- package/apps/dashboard/app/decision/[id]/page.tsx +63 -58
- package/apps/dashboard/app/demo/gates/page.tsx +43 -45
- package/apps/dashboard/app/design-system/page.tsx +868 -0
- package/apps/dashboard/app/globals.css +80 -4
- package/apps/dashboard/app/install-claude/page.tsx +4 -6
- package/apps/dashboard/app/login/page.tsx +72 -54
- package/apps/dashboard/app/page.tsx +101 -48
- package/apps/dashboard/app/settings/page.tsx +61 -13
- package/apps/dashboard/app/signup/page.tsx +242 -0
- package/apps/dashboard/app/subscribe/page.tsx +0 -2
- package/apps/dashboard/app/tests/page.tsx +37 -4
- package/apps/dashboard/app/welcome/page.tsx +13 -16
- package/apps/dashboard/app/work/[id]/page.tsx +117 -118
- package/apps/dashboard/app/work/[id]/proof/page.tsx +1489 -0
- package/apps/dashboard/components/AppShell.tsx +92 -85
- package/apps/dashboard/components/CardMenu.tsx +45 -12
- package/apps/dashboard/components/ClaudePanel.tsx +771 -850
- package/apps/dashboard/components/ClaudePanelInput.tsx +43 -15
- package/apps/dashboard/components/ConnectClaudeScreen.tsx +17 -34
- package/apps/dashboard/components/CopyableId.tsx +3 -4
- package/apps/dashboard/components/DetailReviewActions.tsx +100 -0
- package/apps/dashboard/components/DragContext.tsx +134 -63
- package/apps/dashboard/components/DraggableCard.tsx +3 -5
- package/apps/dashboard/components/DropZone.tsx +6 -7
- package/apps/dashboard/components/EditableDetailDescription.tsx +7 -13
- package/apps/dashboard/components/EditableDetailTitle.tsx +6 -13
- package/apps/dashboard/components/EditableTitle.tsx +26 -7
- package/apps/dashboard/components/ElapsedTimer.tsx +66 -0
- package/apps/dashboard/components/EpicGroup.tsx +359 -0
- package/apps/dashboard/components/GateCard.tsx +79 -17
- package/apps/dashboard/components/GateChoiceCard.tsx +15 -18
- package/apps/dashboard/components/InstallClaudeScreen.tsx +15 -32
- package/apps/dashboard/components/JettyLoader.tsx +37 -0
- package/apps/dashboard/components/KanbanBoard.tsx +368 -958
- package/apps/dashboard/components/KanbanCard.tsx +740 -0
- package/apps/dashboard/components/LazyCard.tsx +62 -0
- package/apps/dashboard/components/LazyMarkdown.tsx +11 -0
- package/apps/dashboard/components/MainNav.tsx +38 -73
- package/apps/dashboard/components/MessageBlock.tsx +468 -0
- package/apps/dashboard/components/ModeStartCard.tsx +15 -16
- package/apps/dashboard/components/OnboardingWelcome.tsx +213 -0
- package/apps/dashboard/components/PlaceholderCard.tsx +3 -4
- package/apps/dashboard/components/ProjectSwitcher.tsx +30 -30
- package/apps/dashboard/components/PrototypeTimeline.tsx +72 -51
- package/apps/dashboard/components/RealTimeKanbanWrapper.tsx +406 -388
- package/apps/dashboard/components/RealTimeTestsWrapper.tsx +373 -235
- package/apps/dashboard/components/ReviewFooter.tsx +139 -0
- package/apps/dashboard/components/SessionList.tsx +19 -19
- package/apps/dashboard/components/SubscribeContent.tsx +91 -47
- package/apps/dashboard/components/TestTree.tsx +16 -16
- package/apps/dashboard/components/TipCard.tsx +16 -17
- package/apps/dashboard/components/Toast.tsx +5 -6
- package/apps/dashboard/components/TypeIcon.tsx +55 -0
- package/apps/dashboard/components/ViewModeToolbar.tsx +104 -0
- package/apps/dashboard/components/WaveCompletionAnimation.tsx +52 -65
- package/apps/dashboard/components/WelcomeScreen.tsx +19 -35
- package/apps/dashboard/components/WorkItemHeader.tsx +4 -5
- package/apps/dashboard/components/WorkItemTree.tsx +11 -32
- package/apps/dashboard/components/settings/AccountSection.tsx +55 -35
- package/apps/dashboard/components/settings/AiContextSection.tsx +89 -0
- package/apps/dashboard/components/settings/ContextDocumentsSection.tsx +317 -0
- package/apps/dashboard/components/settings/EnvVarsSection.tsx +74 -152
- package/apps/dashboard/components/settings/GeneralSection.tsx +162 -56
- package/apps/dashboard/components/settings/ProjectStackSection.tsx +948 -0
- package/apps/dashboard/components/settings/SettingsLayout.tsx +4 -5
- package/apps/dashboard/components/ui/Button.tsx +104 -0
- package/apps/dashboard/components/ui/Input.tsx +78 -0
- package/apps/dashboard/components.json +1 -1
- package/apps/dashboard/contexts/ClaudeSessionContext.tsx +711 -418
- package/apps/dashboard/contexts/ConnectionStatusContext.tsx +25 -5
- package/apps/dashboard/contexts/UsageContext.tsx +87 -32
- package/apps/dashboard/dev.sh +35 -0
- package/apps/dashboard/eslint.config.mjs +9 -9
- package/apps/dashboard/hooks/useKanbanAnimation.ts +29 -0
- package/apps/dashboard/hooks/useKanbanUndo.ts +83 -0
- package/apps/dashboard/hooks/useWebSocket.ts +138 -83
- package/apps/dashboard/index.html +73 -0
- package/apps/dashboard/lib/constants.ts +43 -0
- package/apps/dashboard/lib/data-bridge.ts +722 -0
- package/apps/dashboard/lib/db.ts +69 -1265
- package/apps/dashboard/lib/environment-config.ts +173 -0
- package/apps/dashboard/lib/environment-verification.ts +119 -0
- package/apps/dashboard/lib/kanban-utils.ts +270 -0
- package/apps/dashboard/lib/proof-run.ts +495 -0
- package/apps/dashboard/lib/proof-scenario-runner.ts +346 -0
- package/apps/dashboard/lib/run-migrations.js +27 -2
- package/apps/dashboard/lib/service-recovery.ts +326 -0
- package/apps/dashboard/lib/session-state-machine.ts +1 -0
- package/apps/dashboard/lib/session-state-utils.ts +0 -164
- package/apps/dashboard/lib/session-stream-manager.ts +308 -134
- package/apps/dashboard/lib/shadows.ts +7 -0
- package/apps/dashboard/lib/stream-manager-registry.ts +46 -6
- package/apps/dashboard/lib/tauri-bridge.ts +102 -0
- package/apps/dashboard/lib/tauri.ts +106 -0
- package/apps/dashboard/lib/utils.ts +6 -0
- package/apps/dashboard/next-env.d.ts +1 -1
- package/apps/dashboard/package.json +21 -32
- package/apps/dashboard/public/bug-icon.png +0 -0
- package/apps/dashboard/public/buoy-icon.png +0 -0
- package/apps/dashboard/public/fonts/Satoshi-Variable.woff2 +0 -0
- package/apps/dashboard/public/fonts/Satoshi-VariableItalic.woff2 +0 -0
- package/apps/dashboard/public/in-flight-seagull.png +0 -0
- package/apps/dashboard/public/jetty-icon-loading-alt.svg +11 -0
- package/apps/dashboard/public/jetty-icon-loading.svg +11 -0
- package/apps/dashboard/public/jettypod_logo.png +0 -0
- package/apps/dashboard/public/pier-icon.png +0 -0
- package/apps/dashboard/public/star-icon.png +0 -0
- package/apps/dashboard/public/wrench-icon.png +0 -0
- package/apps/dashboard/scripts/tauri-build.js +228 -0
- package/apps/dashboard/scripts/upload-tauri-to-r2.js +125 -0
- package/apps/dashboard/scripts/ws-server.js +191 -0
- package/apps/dashboard/src/main.tsx +12 -0
- package/apps/dashboard/src/router.tsx +107 -0
- package/apps/dashboard/src/vite-env.d.ts +1 -0
- package/apps/dashboard/tsconfig.json +7 -12
- package/apps/dashboard/tsconfig.tsbuildinfo +1 -1
- package/apps/dashboard/vite.config.ts +33 -0
- package/apps/update-server/src/index.ts +228 -80
- package/claude-hooks/global-guardrails.js +14 -13
- package/crates/jettypod-cli/Cargo.toml +19 -0
- package/crates/jettypod-cli/src/commands.rs +1249 -0
- package/crates/jettypod-cli/src/main.rs +595 -0
- package/crates/jettypod-core/Cargo.toml +26 -0
- package/crates/jettypod-core/build.rs +98 -0
- package/crates/jettypod-core/migrations/V1__baseline.sql +197 -0
- package/crates/jettypod-core/migrations/V2__work_items_indexes.sql +6 -0
- package/crates/jettypod-core/migrations/V3__qa_steps.sql +2 -0
- package/crates/jettypod-core/src/auth.rs +294 -0
- package/crates/jettypod-core/src/config.rs +397 -0
- package/crates/jettypod-core/src/db/mod.rs +507 -0
- package/crates/jettypod-core/src/db/recovery.rs +114 -0
- package/crates/jettypod-core/src/db/startup.rs +101 -0
- package/crates/jettypod-core/src/db/validate.rs +149 -0
- package/crates/jettypod-core/src/error.rs +76 -0
- package/crates/jettypod-core/src/git.rs +458 -0
- package/crates/jettypod-core/src/lib.rs +20 -0
- package/crates/jettypod-core/src/sessions.rs +625 -0
- package/crates/jettypod-core/src/skills.rs +556 -0
- package/crates/jettypod-core/src/work.rs +1086 -0
- package/crates/jettypod-core/src/worktree.rs +628 -0
- package/crates/jettypod-core/src/ws.rs +767 -0
- package/cucumber-test.cjs +6 -0
- package/cucumber.js +9 -3
- package/docs/COMMAND_REFERENCE.md +34 -0
- package/hooks/post-checkout +32 -75
- package/hooks/post-merge +111 -10
- package/jest.setup.js +1 -0
- package/jettypod.js +145 -116
- package/lib/bdd-preflight.js +96 -0
- package/lib/chore-taxonomy.js +33 -10
- package/lib/database.js +36 -16
- package/lib/db-watcher.js +1 -1
- package/lib/git-hooks/pre-commit +1 -1
- package/lib/jettypod-backup.js +27 -4
- package/lib/merge-lock.js +111 -253
- package/lib/migrations/027-plan-at-creation-column.js +3 -1
- package/lib/migrations/029-remove-autoincrement.js +307 -0
- package/lib/migrations/029-rename-corrupted-to-cleaned.js +149 -0
- package/lib/migrations/030-rejection-round-columns.js +54 -0
- package/lib/migrations/031-session-isolation-index.js +17 -0
- package/lib/migrations/index.js +47 -4
- package/lib/schema.js +10 -5
- package/lib/seed-onboarding.js +1 -1
- package/lib/update-command/index.js +9 -175
- package/lib/work-commands/index.js +144 -19
- package/lib/work-tracking/index.js +148 -27
- package/lib/worktree-diagnostics.js +16 -16
- package/lib/worktree-facade.js +1 -1
- package/lib/worktree-manager.js +8 -8
- package/lib/worktree-reconciler.js +5 -5
- package/package.json +9 -2
- package/scripts/ndjson-to-cucumber-json.js +152 -0
- package/scripts/postinstall.js +25 -0
- package/skills-templates/bug-mode/SKILL.md +79 -20
- package/skills-templates/bug-planning/SKILL.md +25 -29
- package/skills-templates/chore-mode/SKILL.md +171 -69
- package/skills-templates/chore-mode/verification.js +51 -10
- package/skills-templates/chore-planning/SKILL.md +47 -18
- package/skills-templates/design-system-selection/SKILL.md +273 -0
- package/skills-templates/epic-planning/SKILL.md +82 -48
- package/skills-templates/external-transition/SKILL.md +47 -47
- package/skills-templates/feature-planning/SKILL.md +173 -74
- package/skills-templates/production-mode/SKILL.md +69 -49
- package/skills-templates/request-routing/SKILL.md +4 -4
- package/skills-templates/simple-improvement/SKILL.md +74 -29
- package/skills-templates/speed-mode/SKILL.md +217 -141
- package/skills-templates/stable-mode/SKILL.md +148 -89
- package/apps/dashboard/README.md +0 -36
- package/apps/dashboard/app/api/claude/[workItemId]/message/route.ts +0 -386
- package/apps/dashboard/app/api/claude/[workItemId]/pin/route.ts +0 -24
- package/apps/dashboard/app/api/claude/[workItemId]/route.ts +0 -167
- package/apps/dashboard/app/api/claude/sessions/[sessionId]/content/route.ts +0 -52
- package/apps/dashboard/app/api/claude/sessions/[sessionId]/message/route.ts +0 -378
- package/apps/dashboard/app/api/claude/sessions/[sessionId]/pin/route.ts +0 -24
- package/apps/dashboard/app/api/claude/sessions/cleanup/route.ts +0 -34
- package/apps/dashboard/app/api/claude/sessions/route.ts +0 -184
- package/apps/dashboard/app/api/decisions/[id]/route.ts +0 -25
- package/apps/dashboard/app/api/internal/set-project/route.ts +0 -17
- package/apps/dashboard/app/api/kanban/route.ts +0 -15
- package/apps/dashboard/app/api/settings/env-vars/route.ts +0 -125
- package/apps/dashboard/app/api/settings/general/route.ts +0 -21
- package/apps/dashboard/app/api/tests/route.ts +0 -9
- package/apps/dashboard/app/api/tests/run/route.ts +0 -82
- package/apps/dashboard/app/api/tests/run/stream/route.ts +0 -71
- package/apps/dashboard/app/api/tests/undefined/route.ts +0 -9
- package/apps/dashboard/app/api/usage/route.ts +0 -17
- package/apps/dashboard/app/api/work/[id]/description/route.ts +0 -21
- package/apps/dashboard/app/api/work/[id]/epic/route.ts +0 -21
- package/apps/dashboard/app/api/work/[id]/order/route.ts +0 -21
- package/apps/dashboard/app/api/work/[id]/status/route.ts +0 -21
- package/apps/dashboard/app/api/work/[id]/title/route.ts +0 -21
- package/apps/dashboard/app/layout.tsx +0 -43
- package/apps/dashboard/components/UpgradeBanner.tsx +0 -29
- package/apps/dashboard/electron/ipc-handlers.js +0 -1028
- package/apps/dashboard/electron/main.js +0 -2124
- package/apps/dashboard/electron/preload.js +0 -123
- package/apps/dashboard/electron/session-manager.js +0 -141
- package/apps/dashboard/electron-builder.config.js +0 -357
- package/apps/dashboard/hooks/useClaudeSessions.ts +0 -299
- package/apps/dashboard/lib/claude-process-manager.ts +0 -492
- package/apps/dashboard/lib/db-bridge.ts +0 -282
- package/apps/dashboard/lib/prototypes.ts +0 -202
- package/apps/dashboard/lib/test-results-db.ts +0 -307
- package/apps/dashboard/lib/tests.ts +0 -282
- package/apps/dashboard/next.config.js +0 -50
- package/apps/dashboard/postcss.config.mjs +0 -7
- package/apps/dashboard/public/file.svg +0 -1
- package/apps/dashboard/public/globe.svg +0 -1
- package/apps/dashboard/public/next.svg +0 -1
- package/apps/dashboard/public/vercel.svg +0 -1
- package/apps/dashboard/public/window.svg +0 -1
- package/apps/dashboard/scripts/download-node.js +0 -104
- package/apps/dashboard/scripts/upload-to-r2.js +0 -89
- package/docs/bdd-guidance.md +0 -390
|
@@ -1,492 +0,0 @@
|
|
|
1
|
-
import { spawn, ChildProcess } from 'child_process';
|
|
2
|
-
import { EventEmitter } from 'events';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Manages persistent Claude CLI processes for dashboard sessions.
|
|
6
|
-
*
|
|
7
|
-
* Instead of spawning a new Claude process per message (-p mode),
|
|
8
|
-
* we spawn a persistent process with --input-format stream-json
|
|
9
|
-
* and pipe messages via stdin. This enables Claude's agentic loop
|
|
10
|
-
* to work properly - Claude continues until it decides to stop.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
interface ClaudeProcess {
|
|
14
|
-
process: ChildProcess;
|
|
15
|
-
emitter: EventEmitter;
|
|
16
|
-
sessionId: string;
|
|
17
|
-
createdAt: number;
|
|
18
|
-
lastActivityAt: number;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
// Singleton map of session ID -> Claude process
|
|
22
|
-
const processes = new Map<string, ClaudeProcess>();
|
|
23
|
-
|
|
24
|
-
// Sessions pinned by the UI (active in a tab) — exempt from idle cleanup
|
|
25
|
-
const pinnedSessions = new Set<string>();
|
|
26
|
-
|
|
27
|
-
// Maximum concurrent Claude processes allowed
|
|
28
|
-
const MAX_PROCESSES = 8;
|
|
29
|
-
|
|
30
|
-
// Timeout for idle processes (2 hours, only applies to unpinned sessions).
|
|
31
|
-
// Longer timeout reduces process respawn frequency, avoiding the cost of
|
|
32
|
-
// context restoration (dumping full conversation history into a single message).
|
|
33
|
-
const IDLE_TIMEOUT_MS = 2 * 60 * 60 * 1000;
|
|
34
|
-
|
|
35
|
-
// ============================================================================
|
|
36
|
-
// Auto Gate Emission
|
|
37
|
-
// ============================================================================
|
|
38
|
-
|
|
39
|
-
// Track last tool_use per session to correlate with tool_result
|
|
40
|
-
const lastToolUse = new Map<string, { name: string; input: Record<string, unknown>; id: string }>();
|
|
41
|
-
|
|
42
|
-
// Track whether implementing gate has been emitted this session (debounce)
|
|
43
|
-
const implementingEmitted = new Map<string, number>();
|
|
44
|
-
|
|
45
|
-
// Debounce interval for implementing gate (don't re-emit within 30s)
|
|
46
|
-
const IMPLEMENTING_DEBOUNCE_MS = 30_000;
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Pattern to detect work item creation from tool result content.
|
|
50
|
-
* Matches: "Created chore #12345: Title Here"
|
|
51
|
-
*/
|
|
52
|
-
const WORK_CREATED_PATTERN = /Created (chore|feature|bug|epic) #(\d+): ([^\n]+)/;
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Pattern to detect worktree creation from tool result content.
|
|
56
|
-
* Matches: "✅ Created worktree: /path/to/worktree"
|
|
57
|
-
*/
|
|
58
|
-
const WORKTREE_STARTED_PATTERN = /Created worktree: ([^\n]+)/;
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Check a parsed stream event for workflow patterns and emit synthetic gate events.
|
|
62
|
-
* Returns an array of gate events to inject into the stream (may be empty).
|
|
63
|
-
*/
|
|
64
|
-
function detectGates(
|
|
65
|
-
sessionId: string,
|
|
66
|
-
parsed: Record<string, unknown>
|
|
67
|
-
): Array<Record<string, unknown>> {
|
|
68
|
-
const gates: Array<Record<string, unknown>> = [];
|
|
69
|
-
|
|
70
|
-
// Track tool_use from assistant messages
|
|
71
|
-
if (parsed.type === 'assistant' && parsed.message) {
|
|
72
|
-
const msg = parsed.message as { content?: Array<{ type: string; name?: string; input?: Record<string, unknown>; id?: string }> };
|
|
73
|
-
if (Array.isArray(msg.content)) {
|
|
74
|
-
for (const block of msg.content) {
|
|
75
|
-
if (block.type === 'tool_use' && block.name && block.id) {
|
|
76
|
-
lastToolUse.set(sessionId, {
|
|
77
|
-
name: block.name,
|
|
78
|
-
input: block.input || {},
|
|
79
|
-
id: block.id,
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
// Emit implementing gate for Edit/Write tool_use (debounced)
|
|
83
|
-
if (block.name === 'Edit' || block.name === 'Write') {
|
|
84
|
-
const lastEmit = implementingEmitted.get(sessionId) || 0;
|
|
85
|
-
if (Date.now() - lastEmit > IMPLEMENTING_DEBOUNCE_MS) {
|
|
86
|
-
const filePath = (block.input as Record<string, string>)?.file_path || '';
|
|
87
|
-
const fileName = filePath.split('/').pop() || filePath;
|
|
88
|
-
gates.push(buildGateEvent('implementing', {
|
|
89
|
-
description: `Editing ${fileName}`,
|
|
90
|
-
files: [fileName],
|
|
91
|
-
}));
|
|
92
|
-
implementingEmitted.set(sessionId, Date.now());
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// Check tool_result from user messages for workflow command outputs
|
|
101
|
-
if (parsed.type === 'user' && parsed.message) {
|
|
102
|
-
const msg = parsed.message as { content?: Array<{ type: string; content?: string | Array<{ type: string; text?: string }> }> };
|
|
103
|
-
if (Array.isArray(msg.content)) {
|
|
104
|
-
for (const block of msg.content) {
|
|
105
|
-
if (block.type === 'tool_result') {
|
|
106
|
-
const resultText = typeof block.content === 'string'
|
|
107
|
-
? block.content
|
|
108
|
-
: Array.isArray(block.content)
|
|
109
|
-
? block.content.filter(p => p.type === 'text').map(p => p.text || '').join('')
|
|
110
|
-
: '';
|
|
111
|
-
|
|
112
|
-
// Check for work item creation
|
|
113
|
-
const workCreated = resultText.match(WORK_CREATED_PATTERN);
|
|
114
|
-
if (workCreated) {
|
|
115
|
-
gates.push(buildGateEvent('work-created', {
|
|
116
|
-
id: parseInt(workCreated[2], 10),
|
|
117
|
-
title: workCreated[3].trim(),
|
|
118
|
-
type: workCreated[1],
|
|
119
|
-
}));
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// Check for worktree creation
|
|
123
|
-
const worktreeStarted = resultText.match(WORKTREE_STARTED_PATTERN);
|
|
124
|
-
if (worktreeStarted) {
|
|
125
|
-
gates.push(buildGateEvent('worktree-started', {
|
|
126
|
-
path: worktreeStarted[1].trim(),
|
|
127
|
-
}));
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// Check for test runs
|
|
131
|
-
const lastTool = lastToolUse.get(sessionId);
|
|
132
|
-
if (lastTool?.name === 'Bash') {
|
|
133
|
-
const cmd = (lastTool.input as Record<string, string>)?.command || '';
|
|
134
|
-
|
|
135
|
-
// Test running detection
|
|
136
|
-
if (cmd.includes('cucumber-js') || cmd.includes('jest') || cmd.includes('npm test') || cmd.includes('npx test')) {
|
|
137
|
-
gates.push(buildGateEvent('tests-running', {}));
|
|
138
|
-
|
|
139
|
-
// If tests passed (exit code 0 implied by non-error result)
|
|
140
|
-
if (!resultText.includes('failing') && !resultText.includes('FAIL') && !resultText.includes('Error:')) {
|
|
141
|
-
gates.push(buildGateEvent('tests-passed', {}));
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// Merge detection
|
|
146
|
-
if (cmd.includes('jettypod work merge')) {
|
|
147
|
-
gates.push(buildGateEvent('merging', {}));
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// Completion detection
|
|
151
|
-
if (cmd.includes('jettypod work status') && cmd.includes('done')) {
|
|
152
|
-
gates.push(buildGateEvent('complete', {
|
|
153
|
-
summary: 'Work complete',
|
|
154
|
-
}));
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
return gates;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* Build a synthetic gate event in the format that session-stream-manager expects.
|
|
167
|
-
* Wraps the gate marker in a tool_result-style user message.
|
|
168
|
-
*/
|
|
169
|
-
function buildGateEvent(gateType: string, data: Record<string, unknown>): Record<string, unknown> {
|
|
170
|
-
const gateMarker = `[GATE:${gateType}]${JSON.stringify(data)}[/GATE]`;
|
|
171
|
-
return {
|
|
172
|
-
type: 'user',
|
|
173
|
-
message: {
|
|
174
|
-
role: 'user',
|
|
175
|
-
content: [{
|
|
176
|
-
type: 'tool_result',
|
|
177
|
-
tool_use_id: `synthetic-gate-${Date.now()}`,
|
|
178
|
-
content: gateMarker,
|
|
179
|
-
}],
|
|
180
|
-
},
|
|
181
|
-
isSyntheticGate: true,
|
|
182
|
-
};
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* Clean up gate tracking state for a session.
|
|
187
|
-
*/
|
|
188
|
-
function cleanupGateState(sessionId: string): void {
|
|
189
|
-
lastToolUse.delete(sessionId);
|
|
190
|
-
implementingEmitted.delete(sessionId);
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// ============================================================================
|
|
194
|
-
// Process Management
|
|
195
|
-
// ============================================================================
|
|
196
|
-
|
|
197
|
-
/**
|
|
198
|
-
* Get or create a persistent Claude process for a session.
|
|
199
|
-
* Returns an EventEmitter that emits 'data', 'error', and 'close' events.
|
|
200
|
-
* Returns error if max process limit is reached.
|
|
201
|
-
*/
|
|
202
|
-
export function getOrCreateProcess(
|
|
203
|
-
sessionId: string,
|
|
204
|
-
cwd: string,
|
|
205
|
-
settingsPath?: string
|
|
206
|
-
): { emitter: EventEmitter; isNew: boolean } | { error: string } {
|
|
207
|
-
const existing = processes.get(sessionId);
|
|
208
|
-
|
|
209
|
-
// Check if existing process is actually healthy (not just "not killed")
|
|
210
|
-
if (existing && isProcessHealthy(existing)) {
|
|
211
|
-
existing.lastActivityAt = Date.now();
|
|
212
|
-
return { emitter: existing.emitter, isNew: false };
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
// Clean up dead/unhealthy process if exists
|
|
216
|
-
if (existing) {
|
|
217
|
-
killProcess(sessionId);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// Check if we've hit the max process limit
|
|
221
|
-
const activeCount = getActiveProcessCount();
|
|
222
|
-
if (activeCount >= MAX_PROCESSES) {
|
|
223
|
-
return { error: `Maximum concurrent processes (${MAX_PROCESSES}) reached. Please close an existing session first.` };
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
const emitter = new EventEmitter();
|
|
227
|
-
|
|
228
|
-
// Build args for persistent mode
|
|
229
|
-
const args = [
|
|
230
|
-
'-p', // Print mode (required for input-format)
|
|
231
|
-
'--input-format', 'stream-json',
|
|
232
|
-
'--output-format', 'stream-json',
|
|
233
|
-
'--verbose',
|
|
234
|
-
'--permission-mode', 'bypassPermissions',
|
|
235
|
-
];
|
|
236
|
-
|
|
237
|
-
if (settingsPath) {
|
|
238
|
-
args.push('--settings', settingsPath);
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
const claudeProcess = spawn('claude', args, {
|
|
242
|
-
cwd,
|
|
243
|
-
env: { ...process.env, JETTYPOD_SESSION_ID: sessionId },
|
|
244
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
// Handle stdout - stream JSON responses
|
|
248
|
-
claudeProcess.stdout.on('data', (data: Buffer) => {
|
|
249
|
-
const lines = data.toString().split('\n').filter(line => line.trim());
|
|
250
|
-
for (const line of lines) {
|
|
251
|
-
try {
|
|
252
|
-
const parsed = JSON.parse(line);
|
|
253
|
-
|
|
254
|
-
// Auto-detect workflow events and emit synthetic gate events
|
|
255
|
-
const syntheticGates = detectGates(sessionId, parsed);
|
|
256
|
-
for (const gate of syntheticGates) {
|
|
257
|
-
emitter.emit('data', gate);
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
emitter.emit('data', parsed);
|
|
261
|
-
} catch {
|
|
262
|
-
// Non-JSON line, emit as raw text
|
|
263
|
-
emitter.emit('data', { type: 'text', content: line });
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
// Handle stderr
|
|
269
|
-
claudeProcess.stderr.on('data', (data: Buffer) => {
|
|
270
|
-
emitter.emit('error', { type: 'stderr', content: data.toString() });
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
// Handle process close
|
|
274
|
-
claudeProcess.on('close', (code) => {
|
|
275
|
-
emitter.emit('close', { exitCode: code });
|
|
276
|
-
cleanupGateState(sessionId);
|
|
277
|
-
processes.delete(sessionId);
|
|
278
|
-
});
|
|
279
|
-
|
|
280
|
-
// Handle process error
|
|
281
|
-
claudeProcess.on('error', (err) => {
|
|
282
|
-
emitter.emit('error', { type: 'process_error', content: err.message });
|
|
283
|
-
cleanupGateState(sessionId);
|
|
284
|
-
processes.delete(sessionId);
|
|
285
|
-
});
|
|
286
|
-
|
|
287
|
-
const claudeProc: ClaudeProcess = {
|
|
288
|
-
process: claudeProcess,
|
|
289
|
-
emitter,
|
|
290
|
-
sessionId,
|
|
291
|
-
createdAt: Date.now(),
|
|
292
|
-
lastActivityAt: Date.now(),
|
|
293
|
-
};
|
|
294
|
-
|
|
295
|
-
processes.set(sessionId, claudeProc);
|
|
296
|
-
|
|
297
|
-
return { emitter, isNew: true };
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
/**
|
|
301
|
-
* Send a user message to a session's Claude process via stdin.
|
|
302
|
-
* The message is formatted as stream-json.
|
|
303
|
-
*/
|
|
304
|
-
export function sendMessage(sessionId: string, message: string): boolean {
|
|
305
|
-
const proc = processes.get(sessionId);
|
|
306
|
-
|
|
307
|
-
if (!proc || !isProcessHealthy(proc)) {
|
|
308
|
-
// Clean up dead process if it exists
|
|
309
|
-
if (proc) {
|
|
310
|
-
killProcess(sessionId);
|
|
311
|
-
}
|
|
312
|
-
return false;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
proc.lastActivityAt = Date.now();
|
|
316
|
-
|
|
317
|
-
// Format as stream-json message
|
|
318
|
-
const jsonMessage = JSON.stringify({
|
|
319
|
-
type: 'user',
|
|
320
|
-
message: {
|
|
321
|
-
role: 'user',
|
|
322
|
-
content: message,
|
|
323
|
-
},
|
|
324
|
-
});
|
|
325
|
-
|
|
326
|
-
// Write to stdin with newline delimiter
|
|
327
|
-
// Safety check: stdin should exist since isProcessHealthy verified it
|
|
328
|
-
proc.process.stdin!.write(jsonMessage + '\n');
|
|
329
|
-
|
|
330
|
-
return true;
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
/**
|
|
334
|
-
* Kill a session's Claude process.
|
|
335
|
-
*/
|
|
336
|
-
export function killProcess(sessionId: string): boolean {
|
|
337
|
-
const proc = processes.get(sessionId);
|
|
338
|
-
|
|
339
|
-
if (!proc) {
|
|
340
|
-
return false;
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
if (!proc.process.killed) {
|
|
344
|
-
proc.process.kill('SIGTERM');
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
cleanupGateState(sessionId);
|
|
348
|
-
processes.delete(sessionId);
|
|
349
|
-
return true;
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
/**
|
|
353
|
-
* Check if a process is truly alive and responsive.
|
|
354
|
-
* Returns true if the process is healthy, false if dead/unresponsive.
|
|
355
|
-
*
|
|
356
|
-
* Checks:
|
|
357
|
-
* 1. Process hasn't been killed
|
|
358
|
-
* 2. Process hasn't exited (exitCode is null for running processes)
|
|
359
|
-
* 3. stdin is still writable
|
|
360
|
-
*/
|
|
361
|
-
function isProcessHealthy(proc: ClaudeProcess): boolean {
|
|
362
|
-
const childProcess = proc.process;
|
|
363
|
-
|
|
364
|
-
// Check if process was explicitly killed
|
|
365
|
-
if (childProcess.killed) {
|
|
366
|
-
return false;
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
// Check if process has exited (exitCode is non-null when exited)
|
|
370
|
-
if (childProcess.exitCode !== null) {
|
|
371
|
-
return false;
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
// Check if stdin is still writable
|
|
375
|
-
if (!childProcess.stdin || childProcess.stdin.destroyed || !childProcess.stdin.writable) {
|
|
376
|
-
return false;
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
return true;
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
/**
|
|
383
|
-
* Check process health and clean up if dead.
|
|
384
|
-
* Returns true if process is healthy, false if dead (and cleaned up).
|
|
385
|
-
*/
|
|
386
|
-
export function checkProcessHealth(sessionId: string): boolean {
|
|
387
|
-
const proc = processes.get(sessionId);
|
|
388
|
-
|
|
389
|
-
if (!proc) {
|
|
390
|
-
return false;
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
if (!isProcessHealthy(proc)) {
|
|
394
|
-
// Process is dead - clean it up
|
|
395
|
-
killProcess(sessionId);
|
|
396
|
-
return false;
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
return true;
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
/**
|
|
403
|
-
* Check if a session has an active process.
|
|
404
|
-
* Now includes health check to detect dead/zombie processes.
|
|
405
|
-
*/
|
|
406
|
-
export function hasActiveProcess(sessionId: string): boolean {
|
|
407
|
-
return checkProcessHealth(sessionId);
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
/**
|
|
411
|
-
* Get count of currently active (healthy) processes.
|
|
412
|
-
*/
|
|
413
|
-
export function getActiveProcessCount(): number {
|
|
414
|
-
let count = 0;
|
|
415
|
-
for (const proc of processes.values()) {
|
|
416
|
-
if (isProcessHealthy(proc)) {
|
|
417
|
-
count++;
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
return count;
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
/**
|
|
424
|
-
* Get stats about active processes (for debugging).
|
|
425
|
-
* Now uses health check to accurately report active processes.
|
|
426
|
-
*/
|
|
427
|
-
export function getProcessStats(): {
|
|
428
|
-
activeCount: number;
|
|
429
|
-
sessions: Array<{ sessionId: string; ageMs: number; idleMs: number; healthy: boolean }>;
|
|
430
|
-
} {
|
|
431
|
-
const now = Date.now();
|
|
432
|
-
const sessions = Array.from(processes.entries())
|
|
433
|
-
.map(([sessionId, proc]) => ({
|
|
434
|
-
sessionId,
|
|
435
|
-
ageMs: now - proc.createdAt,
|
|
436
|
-
idleMs: now - proc.lastActivityAt,
|
|
437
|
-
healthy: isProcessHealthy(proc),
|
|
438
|
-
}));
|
|
439
|
-
|
|
440
|
-
return {
|
|
441
|
-
activeCount: sessions.filter(s => s.healthy).length,
|
|
442
|
-
sessions,
|
|
443
|
-
};
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
/**
|
|
447
|
-
* Pin a session to prevent idle cleanup (session is active in UI).
|
|
448
|
-
*/
|
|
449
|
-
export function pinSession(sessionId: string): void {
|
|
450
|
-
pinnedSessions.add(sessionId);
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
/**
|
|
454
|
-
* Unpin a session to allow idle cleanup (session closed/switched away).
|
|
455
|
-
*/
|
|
456
|
-
export function unpinSession(sessionId: string): void {
|
|
457
|
-
pinnedSessions.delete(sessionId);
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
/**
|
|
461
|
-
* Check if a session is pinned.
|
|
462
|
-
*/
|
|
463
|
-
export function isSessionPinned(sessionId: string): boolean {
|
|
464
|
-
return pinnedSessions.has(sessionId);
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
/**
|
|
468
|
-
* Cleanup idle processes. Call this periodically.
|
|
469
|
-
* Skips pinned sessions (active in UI).
|
|
470
|
-
*/
|
|
471
|
-
export function cleanupIdleProcesses(): number {
|
|
472
|
-
const now = Date.now();
|
|
473
|
-
let cleaned = 0;
|
|
474
|
-
|
|
475
|
-
for (const [sessionId, proc] of processes.entries()) {
|
|
476
|
-
// Never kill pinned sessions — they're active in the UI
|
|
477
|
-
if (pinnedSessions.has(sessionId)) {
|
|
478
|
-
continue;
|
|
479
|
-
}
|
|
480
|
-
if (now - proc.lastActivityAt > IDLE_TIMEOUT_MS) {
|
|
481
|
-
killProcess(sessionId);
|
|
482
|
-
cleaned++;
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
return cleaned;
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
// Start periodic cleanup (every 15 minutes)
|
|
490
|
-
setInterval(() => {
|
|
491
|
-
cleanupIdleProcesses();
|
|
492
|
-
}, 15 * 60 * 1000);
|