claude-tempo 0.1.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.
- package/.mcp.json +9 -0
- package/CLAUDE.md +84 -0
- package/README.md +400 -0
- package/dist/cli.js +169 -0
- package/dist/server.js +234 -0
- package/package.json +34 -0
- package/src/channel.ts +35 -0
- package/src/cli/commands.ts +579 -0
- package/src/cli/output.ts +36 -0
- package/src/cli/preflight.ts +77 -0
- package/src/cli.ts +151 -0
- package/src/config.ts +25 -0
- package/src/server.ts +213 -0
- package/src/spawn.ts +213 -0
- package/src/tools/cue.ts +60 -0
- package/src/tools/ensemble.ts +102 -0
- package/src/tools/helpers.ts +16 -0
- package/src/tools/listen.ts +43 -0
- package/src/tools/recruit.ts +129 -0
- package/src/tools/report.ts +55 -0
- package/src/tools/resolve.ts +39 -0
- package/src/tools/set-name.ts +57 -0
- package/src/tools/set-part.ts +32 -0
- package/src/tools/terminate.ts +61 -0
- package/src/types.ts +64 -0
- package/src/worker.ts +34 -0
- package/src/workflows/session.ts +204 -0
- package/src/workflows/signals.ts +44 -0
- package/tests/recruit-terminal-test-plan.md +201 -0
- package/tsconfig.json +18 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import {
|
|
2
|
+
setHandler,
|
|
3
|
+
condition,
|
|
4
|
+
continueAsNew,
|
|
5
|
+
workflowInfo,
|
|
6
|
+
allHandlersFinished,
|
|
7
|
+
upsertSearchAttributes,
|
|
8
|
+
getExternalWorkflowHandle,
|
|
9
|
+
uuid4,
|
|
10
|
+
} from '@temporalio/workflow';
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
SessionInput,
|
|
14
|
+
Message,
|
|
15
|
+
SentMessage,
|
|
16
|
+
Command,
|
|
17
|
+
PlayerReport,
|
|
18
|
+
HistoryEntry,
|
|
19
|
+
receiveMessageSignal,
|
|
20
|
+
setPartSignal,
|
|
21
|
+
setNameSignal,
|
|
22
|
+
shutdownSignal,
|
|
23
|
+
markDeliveredSignal,
|
|
24
|
+
getPartQuery,
|
|
25
|
+
getMetadataQuery,
|
|
26
|
+
pendingMessagesQuery,
|
|
27
|
+
allMessagesQuery,
|
|
28
|
+
recordSentMessageSignal,
|
|
29
|
+
allSentMessagesQuery,
|
|
30
|
+
commandSignal,
|
|
31
|
+
playerReportSignal,
|
|
32
|
+
historyQuery,
|
|
33
|
+
} from './signals';
|
|
34
|
+
|
|
35
|
+
export async function claudeSessionWorkflow(input: SessionInput): Promise<void> {
|
|
36
|
+
const STALE_MESSAGE_MS = 3 * 60 * 1000; // 3 minutes
|
|
37
|
+
|
|
38
|
+
// State (carried across continue-as-new)
|
|
39
|
+
let part = input.part ?? input.autoSummary ?? 'No description set';
|
|
40
|
+
const messages: Message[] = input.messages ?? [];
|
|
41
|
+
const sentMessages: SentMessage[] = input.sentMessages ?? [];
|
|
42
|
+
let shuttingDown = false;
|
|
43
|
+
|
|
44
|
+
// ── Player Signal Handlers ──
|
|
45
|
+
|
|
46
|
+
setHandler(receiveMessageSignal, (msg) => {
|
|
47
|
+
messages.push({
|
|
48
|
+
id: uuid4(),
|
|
49
|
+
from: msg.from,
|
|
50
|
+
text: msg.text,
|
|
51
|
+
timestamp: new Date().toISOString(),
|
|
52
|
+
delivered: false,
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
setHandler(setPartSignal, (newPart) => {
|
|
57
|
+
part = newPart;
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
setHandler(setNameSignal, (newName) => {
|
|
61
|
+
input.metadata.playerId = newName;
|
|
62
|
+
upsertSearchAttributes({ ClaudeTempoPlayerId: [newName] });
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
setHandler(shutdownSignal, () => {
|
|
66
|
+
shuttingDown = true;
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
setHandler(markDeliveredSignal, (ids) => {
|
|
70
|
+
for (const msg of messages) {
|
|
71
|
+
if (ids.includes(msg.id)) {
|
|
72
|
+
msg.delivered = true;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
setHandler(recordSentMessageSignal, (msg) => {
|
|
78
|
+
sentMessages.push({
|
|
79
|
+
id: uuid4(),
|
|
80
|
+
to: msg.to,
|
|
81
|
+
text: msg.text,
|
|
82
|
+
timestamp: new Date().toISOString(),
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// ── Player Query Handlers ──
|
|
87
|
+
|
|
88
|
+
setHandler(getPartQuery, () => part);
|
|
89
|
+
setHandler(getMetadataQuery, () => input.metadata);
|
|
90
|
+
setHandler(pendingMessagesQuery, () => messages.filter((m) => !m.delivered));
|
|
91
|
+
setHandler(allMessagesQuery, () => messages);
|
|
92
|
+
setHandler(allSentMessagesQuery, () => sentMessages);
|
|
93
|
+
|
|
94
|
+
// ── Conductor State ──
|
|
95
|
+
|
|
96
|
+
const commandHistory: Command[] = input.commandHistory ?? [];
|
|
97
|
+
const reportHistory: PlayerReport[] = input.reportHistory ?? [];
|
|
98
|
+
|
|
99
|
+
// ── Conductor-specific Handlers ──
|
|
100
|
+
|
|
101
|
+
if (input.metadata.isConductor) {
|
|
102
|
+
|
|
103
|
+
setHandler(commandSignal, (cmd) => {
|
|
104
|
+
commandHistory.push({
|
|
105
|
+
...cmd,
|
|
106
|
+
timestamp: new Date().toISOString(),
|
|
107
|
+
});
|
|
108
|
+
// Deliver command as a message to self so the conductor's Claude session sees it
|
|
109
|
+
messages.push({
|
|
110
|
+
id: uuid4(),
|
|
111
|
+
from: cmd.source,
|
|
112
|
+
text: cmd.text,
|
|
113
|
+
timestamp: new Date().toISOString(),
|
|
114
|
+
delivered: false,
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
setHandler(playerReportSignal, (report) => {
|
|
119
|
+
reportHistory.push({
|
|
120
|
+
...report,
|
|
121
|
+
timestamp: new Date().toISOString(),
|
|
122
|
+
});
|
|
123
|
+
// Deliver report as a message to self
|
|
124
|
+
messages.push({
|
|
125
|
+
id: uuid4(),
|
|
126
|
+
from: report.playerId,
|
|
127
|
+
text: `[${report.type}] ${report.text}`,
|
|
128
|
+
timestamp: new Date().toISOString(),
|
|
129
|
+
delivered: false,
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
setHandler(historyQuery, (): HistoryEntry[] => {
|
|
134
|
+
const entries: HistoryEntry[] = [
|
|
135
|
+
...commandHistory.map((c): HistoryEntry => ({
|
|
136
|
+
type: 'command',
|
|
137
|
+
timestamp: c.timestamp,
|
|
138
|
+
data: c,
|
|
139
|
+
})),
|
|
140
|
+
...reportHistory.map((r): HistoryEntry => ({
|
|
141
|
+
type: 'report',
|
|
142
|
+
timestamp: r.timestamp,
|
|
143
|
+
data: r,
|
|
144
|
+
})),
|
|
145
|
+
];
|
|
146
|
+
return entries.sort((a, b) => a.timestamp.localeCompare(b.timestamp));
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ── Main Loop ──
|
|
151
|
+
|
|
152
|
+
let staleExit = false;
|
|
153
|
+
|
|
154
|
+
while (!shuttingDown) {
|
|
155
|
+
await condition(() => shuttingDown, '5 minutes');
|
|
156
|
+
|
|
157
|
+
if (shuttingDown) break;
|
|
158
|
+
|
|
159
|
+
// Detect stale session: messages pending longer than threshold means poller is dead
|
|
160
|
+
if (!input.disableStaleDetection) {
|
|
161
|
+
const now = Date.now();
|
|
162
|
+
const staleMessages = messages.filter(
|
|
163
|
+
(m) => !m.delivered && now - new Date(m.timestamp).getTime() > STALE_MESSAGE_MS,
|
|
164
|
+
);
|
|
165
|
+
if (staleMessages.length > 0) {
|
|
166
|
+
staleExit = true;
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Prevent unbounded history growth
|
|
172
|
+
const info = workflowInfo();
|
|
173
|
+
if (info.continueAsNewSuggested || info.historyLength > 10_000) {
|
|
174
|
+
await condition(allHandlersFinished);
|
|
175
|
+
await continueAsNew<typeof claudeSessionWorkflow>({
|
|
176
|
+
...input,
|
|
177
|
+
part,
|
|
178
|
+
messages: messages.filter((m) => !m.delivered),
|
|
179
|
+
sentMessages: sentMessages.slice(-50),
|
|
180
|
+
...(input.metadata.isConductor ? { commandHistory, reportHistory } : {}),
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Notify conductor with undelivered messages before exiting
|
|
186
|
+
if (staleExit && !input.metadata.isConductor) {
|
|
187
|
+
try {
|
|
188
|
+
const undelivered = messages.filter((m) => !m.delivered);
|
|
189
|
+
const summary = undelivered.map((m) => ` From ${m.from}: ${m.text}`).join('\n');
|
|
190
|
+
const conductorWfId = `claude-session-${input.metadata.ensemble}-conductor`;
|
|
191
|
+
const handle = getExternalWorkflowHandle(conductorWfId);
|
|
192
|
+
await handle.signal(playerReportSignal, {
|
|
193
|
+
playerId: input.metadata.playerId,
|
|
194
|
+
text: `Session ended — ${undelivered.length} undelivered message(s):\n${summary}`,
|
|
195
|
+
type: 'blocker',
|
|
196
|
+
});
|
|
197
|
+
} catch {
|
|
198
|
+
// No conductor running — that's fine
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Graceful shutdown — wait for in-flight handlers
|
|
203
|
+
await condition(allHandlersFinished);
|
|
204
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { defineSignal, defineQuery } from '@temporalio/workflow';
|
|
2
|
+
import type {
|
|
3
|
+
SessionMetadata,
|
|
4
|
+
Message,
|
|
5
|
+
SentMessage,
|
|
6
|
+
HistoryEntry,
|
|
7
|
+
} from '../types';
|
|
8
|
+
|
|
9
|
+
// Re-export types for convenience within workflow code
|
|
10
|
+
export type {
|
|
11
|
+
SessionMetadata,
|
|
12
|
+
SessionInput,
|
|
13
|
+
Message,
|
|
14
|
+
Command,
|
|
15
|
+
PlayerReport,
|
|
16
|
+
SentMessage,
|
|
17
|
+
HistoryEntry,
|
|
18
|
+
} from '../types';
|
|
19
|
+
|
|
20
|
+
// ── Player Signals ──
|
|
21
|
+
|
|
22
|
+
export const receiveMessageSignal = defineSignal<[{ from: string; text: string }]>('receiveMessage');
|
|
23
|
+
export const recordSentMessageSignal = defineSignal<[{ to: string; text: string }]>('recordSentMessage');
|
|
24
|
+
export const setPartSignal = defineSignal<[string]>('setPart');
|
|
25
|
+
export const shutdownSignal = defineSignal('shutdown');
|
|
26
|
+
export const markDeliveredSignal = defineSignal<[string[]]>('markDelivered');
|
|
27
|
+
export const setNameSignal = defineSignal<[string]>('setName');
|
|
28
|
+
|
|
29
|
+
// ── Player Queries ──
|
|
30
|
+
|
|
31
|
+
export const getPartQuery = defineQuery<string>('getPart');
|
|
32
|
+
export const getMetadataQuery = defineQuery<SessionMetadata>('getMetadata');
|
|
33
|
+
export const pendingMessagesQuery = defineQuery<Message[]>('pendingMessages');
|
|
34
|
+
export const allMessagesQuery = defineQuery<Message[]>('allMessages');
|
|
35
|
+
export const allSentMessagesQuery = defineQuery<SentMessage[]>('allSentMessages');
|
|
36
|
+
|
|
37
|
+
// ── Conductor Signals ──
|
|
38
|
+
|
|
39
|
+
export const commandSignal = defineSignal<[{ text: string; source: string; replyTo?: string }]>('command');
|
|
40
|
+
export const playerReportSignal = defineSignal<[{ playerId: string; text: string; type: 'result' | 'blocker' | 'question' }]>('playerReport');
|
|
41
|
+
|
|
42
|
+
// ── Conductor Queries ──
|
|
43
|
+
|
|
44
|
+
export const historyQuery = defineQuery<HistoryEntry[]>('history');
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# Recruit Terminal Spawn — Manual Test Plan
|
|
2
|
+
|
|
3
|
+
Tests for `src/tools/recruit.ts` terminal spawning across platforms and configurations.
|
|
4
|
+
|
|
5
|
+
## Prerequisites
|
|
6
|
+
|
|
7
|
+
- Temporal dev server running (`temporal server start-dev`)
|
|
8
|
+
- Project built (`npm run build`)
|
|
9
|
+
- Claude Code installed and on PATH
|
|
10
|
+
- `CLAUDE_TEMPO_ENSEMBLE` set in the conductor session
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## macOS — Ghostty
|
|
15
|
+
|
|
16
|
+
### GT-1: Basic recruit opens Ghostty window
|
|
17
|
+
- **Setup**: Ghostty is the active terminal
|
|
18
|
+
- **Steps**: Call `recruit` with a name and workDir
|
|
19
|
+
- **Expected**: A new Ghostty window opens, fish (or default shell) initializes, `claude` launches with the correct name in the title bar
|
|
20
|
+
- **Verify**: `ensemble` shows the new session within 30 seconds
|
|
21
|
+
|
|
22
|
+
### GT-2: Environment variables are passed through
|
|
23
|
+
- **Setup**: Conductor is in ensemble "myband"
|
|
24
|
+
- **Steps**: Recruit a new session
|
|
25
|
+
- **Expected**: The recruited session's MCP server activates (not idle mode), confirming `CLAUDE_TEMPO_ENSEMBLE=myband` was received
|
|
26
|
+
- **Verify**: New session appears in `ensemble` output with matching ensemble
|
|
27
|
+
|
|
28
|
+
### GT-3: Node manager (fnm/nvm) PATH is preserved
|
|
29
|
+
- **Setup**: Node is managed via fnm or nvm (not a system install)
|
|
30
|
+
- **Steps**: Recruit a new session
|
|
31
|
+
- **Expected**: The MCP server starts successfully (requires `node` on PATH)
|
|
32
|
+
- **Verify**: `/mcp` in the new session shows claude-tempo connected, not failed
|
|
33
|
+
|
|
34
|
+
### GT-4: Shell with slow startup
|
|
35
|
+
- **Setup**: Add `sleep 2` to fish config (`~/.config/fish/config.fish`)
|
|
36
|
+
- **Steps**: Recruit a new session
|
|
37
|
+
- **Expected**: `initial input` waits for the shell prompt; claude command runs correctly after shell init completes
|
|
38
|
+
- **Verify**: Session registers in ensemble (may take longer than usual)
|
|
39
|
+
- **Cleanup**: Remove the `sleep 2` from fish config
|
|
40
|
+
|
|
41
|
+
### GT-5: Working directory is set correctly
|
|
42
|
+
- **Setup**: Use a workDir different from the conductor's (e.g., `/tmp`)
|
|
43
|
+
- **Steps**: `recruit({ workDir: "/tmp", name: "test-dir" })`
|
|
44
|
+
- **Expected**: New Ghostty window opens in `/tmp`
|
|
45
|
+
- **Verify**: `ensemble` shows `Dir: /tmp` for the new session
|
|
46
|
+
|
|
47
|
+
### GT-6: Special characters in workDir
|
|
48
|
+
- **Setup**: Create a directory with spaces: `mkdir -p "/tmp/my project"`
|
|
49
|
+
- **Steps**: `recruit({ workDir: "/tmp/my project", name: "test-spaces" })`
|
|
50
|
+
- **Expected**: Claude launches in the correct directory without shell quoting errors
|
|
51
|
+
- **Cleanup**: `rm -rf "/tmp/my project"`
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## macOS — iTerm2
|
|
56
|
+
|
|
57
|
+
### IT-1: Basic recruit opens iTerm2 window
|
|
58
|
+
- **Setup**: iTerm2 is the active terminal (Ghostty not running)
|
|
59
|
+
- **Steps**: Call `recruit`
|
|
60
|
+
- **Expected**: A new iTerm2 window opens with default profile, shell initializes, `claude` launches
|
|
61
|
+
- **Verify**: `ensemble` shows the new session
|
|
62
|
+
|
|
63
|
+
### IT-2: Environment and PATH preserved
|
|
64
|
+
- **Setup**: Node managed via nvm/fnm, iTerm2 as terminal
|
|
65
|
+
- **Steps**: Recruit a new session
|
|
66
|
+
- **Expected**: MCP server connects to Temporal (node is on PATH, env vars passed)
|
|
67
|
+
- **Verify**: `/mcp` in new session shows claude-tempo connected
|
|
68
|
+
|
|
69
|
+
### IT-3: Uses `write text` not `command`
|
|
70
|
+
- **Setup**: iTerm2 active
|
|
71
|
+
- **Steps**: Recruit and observe the new window
|
|
72
|
+
- **Expected**: Shell prompt appears briefly, then the claude command is typed in (not a direct command execution). Shell profile is fully loaded before claude starts.
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## macOS — Terminal.app
|
|
77
|
+
|
|
78
|
+
### TA-1: Basic recruit opens Terminal.app window
|
|
79
|
+
- **Setup**: Neither Ghostty nor iTerm2 running; Terminal.app is default
|
|
80
|
+
- **Steps**: Call `recruit`
|
|
81
|
+
- **Expected**: Terminal.app opens via `.command` file, shell profiles are sourced, `claude` launches
|
|
82
|
+
- **Verify**: `ensemble` shows the new session
|
|
83
|
+
|
|
84
|
+
### TA-2: zsh user with nvm
|
|
85
|
+
- **Setup**: Terminal.app, zsh default shell, nvm installed
|
|
86
|
+
- **Steps**: Recruit a new session
|
|
87
|
+
- **Expected**: `.command` script sources `.zshrc` and `.nvm/nvm.sh`, node is available
|
|
88
|
+
- **Verify**: MCP server connects successfully
|
|
89
|
+
|
|
90
|
+
### TA-3: fish user on Terminal.app
|
|
91
|
+
- **Setup**: Terminal.app, `SHELL=/usr/local/bin/fish` (or `/opt/homebrew/bin/fish`)
|
|
92
|
+
- **Steps**: Recruit a new session
|
|
93
|
+
- **Expected**: `.command` script detects fish and re-execs into `fish -c` for proper environment
|
|
94
|
+
- **Verify**: MCP server connects, node is on PATH
|
|
95
|
+
|
|
96
|
+
### TA-4: .command file cleanup
|
|
97
|
+
- **Steps**: Recruit a session, note the timestamp
|
|
98
|
+
- **Expected**: A `.command` file exists in `$TMPDIR` named `claude-tempo-recruit-<timestamp>.command`
|
|
99
|
+
- **Note**: These are not auto-cleaned; consider periodic cleanup in future
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## Terminal Detection
|
|
104
|
+
|
|
105
|
+
### TD-1: TERM_PROGRAM takes priority
|
|
106
|
+
- **Setup**: `TERM_PROGRAM=ghostty` in environment, iTerm2 also running
|
|
107
|
+
- **Steps**: Recruit a new session
|
|
108
|
+
- **Expected**: Ghostty path is used (not iTerm2)
|
|
109
|
+
- **Verify**: Check MCP server logs for "Using Ghostty initial-input path"
|
|
110
|
+
|
|
111
|
+
### TD-2: Frontmost app fallback when TERM_PROGRAM missing
|
|
112
|
+
- **Setup**: Unset `TERM_PROGRAM` in MCP server env, Ghostty is frontmost app
|
|
113
|
+
- **Steps**: Recruit a new session
|
|
114
|
+
- **Expected**: AppleScript detects Ghostty as frontmost app, uses Ghostty path
|
|
115
|
+
- **Verify**: Check MCP server logs
|
|
116
|
+
|
|
117
|
+
### TD-3: pgrep fallback
|
|
118
|
+
- **Setup**: `TERM_PROGRAM` unset, AppleScript frontmost detection returns a non-terminal app (e.g., Finder is focused)
|
|
119
|
+
- **Steps**: Recruit a new session
|
|
120
|
+
- **Expected**: Falls back to pgrep, finds the correct terminal
|
|
121
|
+
- **Verify**: Check MCP server logs
|
|
122
|
+
|
|
123
|
+
### TD-4: No terminal detected defaults to Terminal.app
|
|
124
|
+
- **Setup**: Neither Ghostty nor iTerm2 installed/running
|
|
125
|
+
- **Steps**: Recruit a new session
|
|
126
|
+
- **Expected**: Falls through to Terminal.app `.command` file path
|
|
127
|
+
- **Verify**: Terminal.app opens
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## Claude Binary Resolution
|
|
132
|
+
|
|
133
|
+
### CB-1: claude on PATH via node manager
|
|
134
|
+
- **Setup**: `claude` installed in `~/.local/bin/` or via npm global
|
|
135
|
+
- **Steps**: Recruit a new session
|
|
136
|
+
- **Expected**: `resolveClaudePath()` returns the absolute path, used in spawn command
|
|
137
|
+
|
|
138
|
+
### CB-2: claude not on MCP server PATH
|
|
139
|
+
- **Setup**: `claude` is only on the user's shell PATH (via fnm), not in the MCP server's PATH
|
|
140
|
+
- **Steps**: Recruit a new session
|
|
141
|
+
- **Expected**: `resolveClaudePath()` falls back to bare `claude`, but the `initial input` approach runs in the user's shell where `claude` is on PATH
|
|
142
|
+
- **Verify**: Session starts successfully despite MCP server not finding claude
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## Error Handling
|
|
147
|
+
|
|
148
|
+
### EH-1: Duplicate name rejected
|
|
149
|
+
- **Steps**: Recruit "Alice", then recruit "Alice" again
|
|
150
|
+
- **Expected**: Second recruit returns error: "Session Alice is already active"
|
|
151
|
+
|
|
152
|
+
### EH-2: Invalid name rejected
|
|
153
|
+
- **Steps**: `recruit({ name: "bad name!", workDir: "/tmp" })`
|
|
154
|
+
- **Expected**: Returns error about invalid characters
|
|
155
|
+
|
|
156
|
+
### EH-3: Session spawned but slow to register
|
|
157
|
+
- **Steps**: Recruit with a very slow workDir (e.g., network mount)
|
|
158
|
+
- **Expected**: Returns "spawned but did not register within 15 seconds" message
|
|
159
|
+
- **Verify**: Session eventually appears in `ensemble`
|
|
160
|
+
|
|
161
|
+
### EH-4: Initial message delivered after registration
|
|
162
|
+
- **Steps**: Recruit with an `initialMessage`
|
|
163
|
+
- **Expected**: After the session registers, it receives the name instruction AND the initial message
|
|
164
|
+
- **Verify**: New session calls `set_name` and then acts on the initial message
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## Cross-Platform (if applicable)
|
|
169
|
+
|
|
170
|
+
### WIN-1: Windows recruit opens cmd.exe window
|
|
171
|
+
- **Setup**: Windows with Claude Code installed
|
|
172
|
+
- **Steps**: Call `recruit`
|
|
173
|
+
- **Expected**: New cmd.exe window opens with `claude` running, env vars set via process env
|
|
174
|
+
|
|
175
|
+
### LIN-1: Linux with gnome-terminal
|
|
176
|
+
- **Setup**: GNOME desktop with gnome-terminal
|
|
177
|
+
- **Steps**: Call `recruit`
|
|
178
|
+
- **Expected**: New gnome-terminal window opens with `claude` running
|
|
179
|
+
|
|
180
|
+
### LIN-2: Linux headless fallback
|
|
181
|
+
- **Setup**: No GUI terminal emulator available
|
|
182
|
+
- **Steps**: Call `recruit`
|
|
183
|
+
- **Expected**: Falls back to headless `bash -c` spawn, logs warning
|
|
184
|
+
- **Verify**: Session registers in ensemble (no visible window)
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## Regression Checks
|
|
189
|
+
|
|
190
|
+
### RG-1: Ghostty `command` property is NOT used
|
|
191
|
+
- **Verify**: The Ghostty AppleScript uses `initial input`, not `set command of cfg`
|
|
192
|
+
- **Why**: `command` bypasses shell init, breaking node managers
|
|
193
|
+
|
|
194
|
+
### RG-2: No `export` syntax in Ghostty/iTerm2 paths
|
|
195
|
+
- **Verify**: The command typed into the shell uses inline `KEY=val` syntax, not `export KEY=val`
|
|
196
|
+
- **Why**: `export` is not valid fish syntax
|
|
197
|
+
|
|
198
|
+
### RG-3: Shell quoting handles single quotes in paths
|
|
199
|
+
- **Steps**: Create dir `/tmp/it's-a-test`, recruit into it
|
|
200
|
+
- **Expected**: shellQuote escapes the apostrophe correctly
|
|
201
|
+
- **Cleanup**: Remove the directory
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"lib": ["ES2022"],
|
|
6
|
+
"outDir": "dist",
|
|
7
|
+
"rootDir": "src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"declaration": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"forceConsistentCasingInFileNames": true,
|
|
13
|
+
"resolveJsonModule": true,
|
|
14
|
+
"moduleResolution": "node"
|
|
15
|
+
},
|
|
16
|
+
"include": ["src/**/*"],
|
|
17
|
+
"exclude": ["node_modules", "dist"]
|
|
18
|
+
}
|