@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.
Files changed (37) hide show
  1. package/dist/index.d.ts +1 -1
  2. package/dist/index.js +1 -1
  3. package/dist/templates/base/claude/agents/debug-sidecar.md +133 -0
  4. package/dist/templates/base/claude/agents/doc-sidecar.md +60 -0
  5. package/dist/templates/base/claude/agents/implementer.md +162 -31
  6. package/dist/templates/base/claude/agents/test-sidecar.md +106 -0
  7. package/dist/templates/base/claude/hooks/mailbox-listener.ts +246 -0
  8. package/dist/templates/base/claude/hooks/sprint-registry.ts +85 -0
  9. package/dist/templates/base/claude/settings.json +19 -0
  10. package/dist/templates/base/claude/skills/sprint-launch/SKILL.md +176 -0
  11. package/dist/templates/base/claude/skills/sprint-teammate/sprint-teammate.md +79 -0
  12. package/dist/templates/base/scripts/lib/SprintLauncher.ts +325 -0
  13. package/dist/templates/base/scripts/lib/TmuxManager.ts +296 -0
  14. package/dist/templates/base/scripts/lib/WorktreeIsolator.ts +165 -0
  15. package/dist/templates/base/scripts/lib/WorktreeManager.ts +106 -0
  16. package/dist/templates/base/scripts/lib/mailbox/types.ts +175 -0
  17. package/dist/templates/base/scripts/lib/sidecar/SidecarWatcher.ts +249 -0
  18. package/dist/templates/base/scripts/lib/sidecar/run.ts +90 -0
  19. package/dist/templates/base/scripts/sprint-launch.ts +285 -0
  20. package/package.json +1 -1
  21. package/templates/base/claude/agents/debug-sidecar.md +133 -0
  22. package/templates/base/claude/agents/doc-sidecar.md +60 -0
  23. package/templates/base/claude/agents/implementer.md +162 -31
  24. package/templates/base/claude/agents/test-sidecar.md +106 -0
  25. package/templates/base/claude/hooks/mailbox-listener.ts +246 -0
  26. package/templates/base/claude/hooks/sprint-registry.ts +85 -0
  27. package/templates/base/claude/settings.json +19 -0
  28. package/templates/base/claude/skills/sprint-launch/SKILL.md +176 -0
  29. package/templates/base/claude/skills/sprint-teammate/sprint-teammate.md +79 -0
  30. package/templates/base/scripts/lib/SprintLauncher.ts +325 -0
  31. package/templates/base/scripts/lib/TmuxManager.ts +296 -0
  32. package/templates/base/scripts/lib/WorktreeIsolator.ts +165 -0
  33. package/templates/base/scripts/lib/WorktreeManager.ts +106 -0
  34. package/templates/base/scripts/lib/mailbox/types.ts +175 -0
  35. package/templates/base/scripts/lib/sidecar/SidecarWatcher.ts +249 -0
  36. package/templates/base/scripts/lib/sidecar/run.ts +90 -0
  37. 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
+ }