@orchestrator-claude/cli 3.25.1 → 3.26.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/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/templates/base/claude/agents/debug-sidecar.md +133 -0
- package/dist/templates/base/claude/agents/doc-sidecar.md +60 -0
- package/dist/templates/base/claude/agents/implementer.md +162 -31
- package/dist/templates/base/claude/agents/test-sidecar.md +106 -0
- package/dist/templates/base/claude/hooks/mailbox-listener.ts +246 -0
- package/dist/templates/base/claude/hooks/sprint-registry.ts +85 -0
- package/dist/templates/base/claude/settings.json +19 -0
- package/dist/templates/base/claude/skills/sprint-launch/SKILL.md +176 -0
- package/dist/templates/base/claude/skills/sprint-teammate/sprint-teammate.md +79 -0
- package/dist/templates/base/scripts/lib/SprintLauncher.ts +325 -0
- package/dist/templates/base/scripts/lib/TmuxManager.ts +296 -0
- package/dist/templates/base/scripts/lib/WorktreeIsolator.ts +165 -0
- package/dist/templates/base/scripts/lib/WorktreeManager.ts +106 -0
- package/dist/templates/base/scripts/lib/mailbox/types.ts +175 -0
- package/dist/templates/base/scripts/lib/sidecar/SidecarWatcher.ts +249 -0
- package/dist/templates/base/scripts/lib/sidecar/run.ts +90 -0
- package/dist/templates/base/scripts/sprint-launch.ts +285 -0
- package/package.json +1 -1
- package/templates/base/claude/agents/debug-sidecar.md +133 -0
- package/templates/base/claude/agents/doc-sidecar.md +60 -0
- package/templates/base/claude/agents/implementer.md +162 -31
- package/templates/base/claude/agents/test-sidecar.md +106 -0
- package/templates/base/claude/hooks/mailbox-listener.ts +246 -0
- package/templates/base/claude/hooks/sprint-registry.ts +85 -0
- package/templates/base/claude/settings.json +19 -0
- package/templates/base/claude/skills/sprint-launch/SKILL.md +176 -0
- package/templates/base/claude/skills/sprint-teammate/sprint-teammate.md +79 -0
- package/templates/base/scripts/lib/SprintLauncher.ts +325 -0
- package/templates/base/scripts/lib/TmuxManager.ts +296 -0
- package/templates/base/scripts/lib/WorktreeIsolator.ts +165 -0
- package/templates/base/scripts/lib/WorktreeManager.ts +106 -0
- package/templates/base/scripts/lib/mailbox/types.ts +175 -0
- package/templates/base/scripts/lib/sidecar/SidecarWatcher.ts +249 -0
- package/templates/base/scripts/lib/sidecar/run.ts +90 -0
- package/templates/base/scripts/sprint-launch.ts +285 -0
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
#!/usr/bin/env npx tsx
|
|
2
|
+
/**
|
|
3
|
+
* sprint-launch.ts — TypeScript CLI entry point for RFC-025 Sprint Launcher
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* npx tsx scripts/sprint-launch.ts <preset> [workflowId]
|
|
7
|
+
*
|
|
8
|
+
* Presets:
|
|
9
|
+
* duo — 1 implementer pane
|
|
10
|
+
* duo-doc — 1 implementer + 1 doc-sidecar pane
|
|
11
|
+
* squad — 2 implementers + 1 debug-sidecar pane
|
|
12
|
+
* squad-review — 1 implementer + 1 debug-sidecar + 1 doc-sidecar pane
|
|
13
|
+
* platoon — 3 implementers + 1 reviewer + 1 debug-sidecar pane
|
|
14
|
+
* platoon-full — 2 implementers + 2 doc-sidecars + 1 debug-sidecar + 1 test-sidecar + 1 reviewer pane
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import * as childProcess from 'child_process';
|
|
18
|
+
import * as crypto from 'crypto';
|
|
19
|
+
import { createWorktreeManager } from './lib/WorktreeManager.js';
|
|
20
|
+
import { createWorktreeIsolator } from './lib/WorktreeIsolator.js';
|
|
21
|
+
import { createTmuxManager } from './lib/TmuxManager.js';
|
|
22
|
+
import { createSprintLauncher } from './lib/SprintLauncher.js';
|
|
23
|
+
import type { SprintTask } from './lib/SprintLauncher.js';
|
|
24
|
+
import { createMailboxManager } from './lib/mailbox/MailboxManager.js';
|
|
25
|
+
import { generateLayout } from '../src/domain/value-objects/generateLayout.js';
|
|
26
|
+
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Constants
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
const VERSION = '1.0.0';
|
|
32
|
+
const VALID_PRESETS = [
|
|
33
|
+
'duo',
|
|
34
|
+
'duo-doc',
|
|
35
|
+
'squad',
|
|
36
|
+
'squad-review',
|
|
37
|
+
'platoon',
|
|
38
|
+
'platoon-full',
|
|
39
|
+
] as const;
|
|
40
|
+
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// Helpers
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* parseTaskFlags — Extract SprintTask objects from CLI args.
|
|
47
|
+
*
|
|
48
|
+
* Supports:
|
|
49
|
+
* --task "TASK-001: Title text" (separate value)
|
|
50
|
+
* --task=TASK-001: Title text (equals-sign form)
|
|
51
|
+
*
|
|
52
|
+
* When the value has a colon, everything before the first colon is the taskId
|
|
53
|
+
* (trimmed) and everything after is the title (trimmed).
|
|
54
|
+
* When there is no colon, a generated taskId is used and the full string is the title.
|
|
55
|
+
*/
|
|
56
|
+
export function parseTaskFlags(args: string[]): SprintTask[] {
|
|
57
|
+
const tasks: SprintTask[] = [];
|
|
58
|
+
let taskCounter = 0;
|
|
59
|
+
|
|
60
|
+
for (let i = 0; i < args.length; i++) {
|
|
61
|
+
const arg = args[i];
|
|
62
|
+
let value: string | undefined;
|
|
63
|
+
|
|
64
|
+
if (arg === '--task' && i + 1 < args.length) {
|
|
65
|
+
value = args[i + 1];
|
|
66
|
+
i++; // consume next arg
|
|
67
|
+
} else if (arg.startsWith('--task=')) {
|
|
68
|
+
value = arg.slice('--task='.length);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (value !== undefined) {
|
|
72
|
+
const colonIdx = value.indexOf(':');
|
|
73
|
+
let taskId: string;
|
|
74
|
+
let title: string;
|
|
75
|
+
if (colonIdx !== -1) {
|
|
76
|
+
taskId = value.slice(0, colonIdx).trim();
|
|
77
|
+
title = value.slice(colonIdx + 1).trim();
|
|
78
|
+
} else {
|
|
79
|
+
taskCounter++;
|
|
80
|
+
taskId = `task-${taskCounter}`;
|
|
81
|
+
title = value.trim();
|
|
82
|
+
}
|
|
83
|
+
tasks.push({ taskId, title, description: '', acceptanceCriteria: [] });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return tasks;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function printUsage(): void {
|
|
91
|
+
console.log(`sprint-launch.ts v${VERSION} — RFC-025 Sprint Launcher
|
|
92
|
+
|
|
93
|
+
Usage:
|
|
94
|
+
npx tsx scripts/sprint-launch.ts <preset> [workflowId] [--mailbox] [--task "..."]
|
|
95
|
+
|
|
96
|
+
Presets:
|
|
97
|
+
duo 1 implementer pane (orchestrator = calling session)
|
|
98
|
+
duo-doc 1 implementer + 1 doc-sidecar pane (orchestrator = calling session)
|
|
99
|
+
squad 2 implementers + 1 debug-sidecar pane
|
|
100
|
+
squad-review 1 implementer + 1 debug-sidecar + 1 doc-sidecar pane
|
|
101
|
+
platoon 3 implementers + 1 reviewer + 1 debug-sidecar pane
|
|
102
|
+
platoon-full 2 implementers + 2 doc-sidecars + 1 debug-sidecar + 1 test-sidecar + 1 reviewer pane
|
|
103
|
+
|
|
104
|
+
Arguments:
|
|
105
|
+
preset Required. One of: ${VALID_PRESETS.join(', ')}
|
|
106
|
+
workflowId Optional. UUID of the active workflow (default: generated random UUID)
|
|
107
|
+
|
|
108
|
+
Flags:
|
|
109
|
+
--mailbox Enable mailbox protocol: creates /tmp/<session>/mailbox/<role>/inbox/ for each pane.
|
|
110
|
+
--task "ID: Title" Assign a task to the next implementer pane (repeatable, round-robin).
|
|
111
|
+
Example: --task "TASK-001: Implement auth" --task "TASK-002: Write tests"
|
|
112
|
+
--fetch-tasks Fetch tasks from the REST API (graceful fallback if unavailable).
|
|
113
|
+
|
|
114
|
+
Task Distribution:
|
|
115
|
+
Tasks are distributed round-robin to implementer panes only.
|
|
116
|
+
Sidecar agents (doc-sidecar, debug-sidecar, test-sidecar) receive tasks via mailbox reactions,
|
|
117
|
+
not via direct assignment. Each implementer pane gets one task per round.
|
|
118
|
+
Reference: sprint-teammate skill for agent-side protocol.
|
|
119
|
+
|
|
120
|
+
Examples:
|
|
121
|
+
npx tsx scripts/sprint-launch.ts duo
|
|
122
|
+
npx tsx scripts/sprint-launch.ts duo-doc adaaa9a4-ef49-4e26-af14-2b239f62b40c
|
|
123
|
+
npx tsx scripts/sprint-launch.ts squad --mailbox
|
|
124
|
+
npx tsx scripts/sprint-launch.ts squad --mailbox --task "TASK-001: Implement feature X" --task "TASK-002: Write docs"
|
|
125
|
+
|
|
126
|
+
Notes:
|
|
127
|
+
- Agent panes use 'claude --agent <role>' — compatible with Claude Max (OAuth) and API key.
|
|
128
|
+
- The calling session (orchestrator) is NOT included in tmux — you continue orchestrating here.
|
|
129
|
+
- Worktrees are created at ../wt-{session}-{index} (0-indexed).
|
|
130
|
+
- The tmux session is named sprint-{workflowId_prefix}.
|
|
131
|
+
- Bash MVP removed (v2) — TypeScript is the only launcher
|
|
132
|
+
`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
// Main
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
|
|
139
|
+
async function main(): Promise<void> {
|
|
140
|
+
const args = process.argv.slice(2);
|
|
141
|
+
|
|
142
|
+
// Help flag
|
|
143
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
144
|
+
printUsage();
|
|
145
|
+
process.exit(0);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const mailboxFlag = args.includes('--mailbox');
|
|
149
|
+
const fetchTasksFlag = args.includes('--fetch-tasks');
|
|
150
|
+
const positional = args.filter((a) => !a.startsWith('--'));
|
|
151
|
+
const presetArg = positional[0];
|
|
152
|
+
const workflowIdArg = positional[1];
|
|
153
|
+
|
|
154
|
+
// Require preset
|
|
155
|
+
if (!presetArg) {
|
|
156
|
+
console.error('Error: preset argument is required.\n');
|
|
157
|
+
printUsage();
|
|
158
|
+
process.exit(1);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Validate preset early (before creating deps) to give a clean error
|
|
162
|
+
if (!VALID_PRESETS.includes(presetArg as (typeof VALID_PRESETS)[number])) {
|
|
163
|
+
console.error(
|
|
164
|
+
`Error: invalid preset '${presetArg}'. Valid presets: ${VALID_PRESETS.join(', ')}`,
|
|
165
|
+
);
|
|
166
|
+
process.exit(1);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// SEC-002: Validate workflowId format if provided by the caller
|
|
170
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
171
|
+
if (workflowIdArg && !UUID_RE.test(workflowIdArg)) {
|
|
172
|
+
console.error('Error: workflowId must be a valid UUID');
|
|
173
|
+
process.exit(1);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Default workflowId to a fresh UUID
|
|
177
|
+
const workflowId = workflowIdArg ?? crypto.randomUUID();
|
|
178
|
+
if (!workflowIdArg) {
|
|
179
|
+
console.warn(`[sprint-launch] No workflowId provided — using generated: ${workflowId}`);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Resolve project root (one level up from scripts/)
|
|
183
|
+
const projectRoot = new URL('..', import.meta.url).pathname.replace(/\/$/, '');
|
|
184
|
+
|
|
185
|
+
// Build real dependencies
|
|
186
|
+
const worktreeManager = createWorktreeManager(projectRoot, {
|
|
187
|
+
execSync: childProcess.execSync,
|
|
188
|
+
});
|
|
189
|
+
const worktreeIsolator = createWorktreeIsolator(projectRoot);
|
|
190
|
+
const tmuxManager = createTmuxManager({ execSync: childProcess.execSync });
|
|
191
|
+
|
|
192
|
+
const sessionName = `sprint-${workflowId.slice(0, 8)}`;
|
|
193
|
+
const mailboxBaseDir = `/tmp/${sessionName}/mailbox`;
|
|
194
|
+
const mailboxManager = mailboxFlag ? createMailboxManager(mailboxBaseDir) : undefined;
|
|
195
|
+
if (mailboxFlag) {
|
|
196
|
+
console.log(`[sprint-launch] Mailbox enabled — inbox directories at ${mailboxBaseDir}/`);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Collect tasks from --task flags
|
|
200
|
+
let tasks = parseTaskFlags(args);
|
|
201
|
+
|
|
202
|
+
// Fetch tasks from REST API if --fetch-tasks flag provided
|
|
203
|
+
if (fetchTasksFlag) {
|
|
204
|
+
try {
|
|
205
|
+
const apiUrl = process.env['ORCHESTRATOR_API_URL'] ?? 'http://localhost:3000';
|
|
206
|
+
const response = await fetch(`${apiUrl}/api/workflows/${workflowId}/tasks`);
|
|
207
|
+
if (!response.ok) {
|
|
208
|
+
console.warn(`[sprint-launch] --fetch-tasks: API returned ${response.status} — continuing with ${tasks.length} CLI tasks`);
|
|
209
|
+
} else {
|
|
210
|
+
const data = await response.json() as { tasks?: SprintTask[] };
|
|
211
|
+
const fetchedTasks = data.tasks ?? [];
|
|
212
|
+
tasks = [...tasks, ...fetchedTasks];
|
|
213
|
+
console.log(`[sprint-launch] --fetch-tasks: loaded ${fetchedTasks.length} tasks from API`);
|
|
214
|
+
}
|
|
215
|
+
} catch (err) {
|
|
216
|
+
console.warn(`[sprint-launch] --fetch-tasks: failed to reach API (${err instanceof Error ? err.message : String(err)}) — continuing with ${tasks.length} CLI tasks`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (tasks.length > 0) {
|
|
221
|
+
console.log(`[sprint-launch] Task distribution: ${tasks.length} task(s) → round-robin to implementer panes`);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const launcher = createSprintLauncher({
|
|
225
|
+
worktreeManager,
|
|
226
|
+
worktreeIsolator,
|
|
227
|
+
tmuxManager,
|
|
228
|
+
generateLayout,
|
|
229
|
+
execSync: childProcess.execSync,
|
|
230
|
+
projectRoot,
|
|
231
|
+
mailboxManager,
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// Register cleanup handlers for SIGINT / SIGTERM
|
|
235
|
+
let launched = false;
|
|
236
|
+
|
|
237
|
+
function onSignal(): void {
|
|
238
|
+
if (launched) {
|
|
239
|
+
console.warn('\n[sprint-launch] Interrupted — cleaning up...');
|
|
240
|
+
launcher.cleanup(sessionName);
|
|
241
|
+
}
|
|
242
|
+
process.exit(1);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
process.on('SIGINT', onSignal);
|
|
246
|
+
process.on('SIGTERM', onSignal);
|
|
247
|
+
|
|
248
|
+
// Launch
|
|
249
|
+
let result: Awaited<ReturnType<typeof launcher.launch>>;
|
|
250
|
+
try {
|
|
251
|
+
result = await launcher.launch(presetArg, workflowId, tasks.length > 0 ? tasks : undefined);
|
|
252
|
+
launched = true;
|
|
253
|
+
} catch (err) {
|
|
254
|
+
launcher.cleanup(sessionName);
|
|
255
|
+
throw err;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Print summary
|
|
259
|
+
console.log('');
|
|
260
|
+
console.log(`[sprint-launch] Sprint session launched: ${result.sessionName}`);
|
|
261
|
+
console.log('');
|
|
262
|
+
console.log(` Preset: ${result.preset}`);
|
|
263
|
+
console.log(` WorkflowId: ${result.workflowId}`);
|
|
264
|
+
console.log(` Session: ${result.sessionName}`);
|
|
265
|
+
console.log(` Worktrees:`);
|
|
266
|
+
for (const wt of result.worktreePaths) {
|
|
267
|
+
console.log(` ${wt}`);
|
|
268
|
+
}
|
|
269
|
+
console.log('');
|
|
270
|
+
console.log('Attach to agent panes with:');
|
|
271
|
+
console.log(` tmux attach -t ${result.sessionName}`);
|
|
272
|
+
console.log('');
|
|
273
|
+
console.log('You (orchestrator) continue in this session.');
|
|
274
|
+
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Guard: only run main() when this file is the entry point (not when imported for tests)
|
|
278
|
+
const isMain = process.argv[1]?.endsWith('sprint-launch.ts') ||
|
|
279
|
+
process.argv[1]?.endsWith('sprint-launch.js');
|
|
280
|
+
if (isMain) {
|
|
281
|
+
main().catch((err: unknown) => {
|
|
282
|
+
console.error('[sprint-launch] Error:', err instanceof Error ? err.message : String(err));
|
|
283
|
+
process.exit(1);
|
|
284
|
+
});
|
|
285
|
+
}
|