ralphctl 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.
Files changed (118) hide show
  1. package/CHANGELOG.md +94 -0
  2. package/LICENSE +21 -0
  3. package/README.md +189 -0
  4. package/bin/ralphctl +13 -0
  5. package/package.json +92 -0
  6. package/schemas/config.schema.json +20 -0
  7. package/schemas/ideate-output.schema.json +22 -0
  8. package/schemas/projects.schema.json +53 -0
  9. package/schemas/requirements-output.schema.json +24 -0
  10. package/schemas/sprint.schema.json +109 -0
  11. package/schemas/task-import.schema.json +49 -0
  12. package/schemas/tasks.schema.json +72 -0
  13. package/src/ai/executor.ts +973 -0
  14. package/src/ai/lifecycle.ts +45 -0
  15. package/src/ai/parser.ts +40 -0
  16. package/src/ai/permissions.ts +207 -0
  17. package/src/ai/process-manager.ts +248 -0
  18. package/src/ai/prompts/ideate-auto.md +144 -0
  19. package/src/ai/prompts/ideate.md +165 -0
  20. package/src/ai/prompts/index.ts +89 -0
  21. package/src/ai/prompts/plan-auto.md +131 -0
  22. package/src/ai/prompts/plan-common.md +157 -0
  23. package/src/ai/prompts/plan-interactive.md +190 -0
  24. package/src/ai/prompts/task-execution.md +159 -0
  25. package/src/ai/prompts/ticket-refine.md +230 -0
  26. package/src/ai/rate-limiter.ts +89 -0
  27. package/src/ai/runner.ts +478 -0
  28. package/src/ai/session.ts +319 -0
  29. package/src/ai/task-context.ts +270 -0
  30. package/src/cli-metadata.ts +7 -0
  31. package/src/cli.ts +65 -0
  32. package/src/commands/completion/index.ts +33 -0
  33. package/src/commands/config/config.ts +58 -0
  34. package/src/commands/config/index.ts +33 -0
  35. package/src/commands/dashboard/dashboard.ts +5 -0
  36. package/src/commands/dashboard/index.ts +6 -0
  37. package/src/commands/doctor/doctor.ts +271 -0
  38. package/src/commands/doctor/index.ts +25 -0
  39. package/src/commands/progress/index.ts +25 -0
  40. package/src/commands/progress/log.ts +64 -0
  41. package/src/commands/progress/show.ts +14 -0
  42. package/src/commands/project/add.ts +336 -0
  43. package/src/commands/project/index.ts +104 -0
  44. package/src/commands/project/list.ts +31 -0
  45. package/src/commands/project/remove.ts +43 -0
  46. package/src/commands/project/repo.ts +118 -0
  47. package/src/commands/project/show.ts +49 -0
  48. package/src/commands/sprint/close.ts +180 -0
  49. package/src/commands/sprint/context.ts +109 -0
  50. package/src/commands/sprint/create.ts +60 -0
  51. package/src/commands/sprint/current.ts +75 -0
  52. package/src/commands/sprint/delete.ts +72 -0
  53. package/src/commands/sprint/health.ts +229 -0
  54. package/src/commands/sprint/ideate.ts +496 -0
  55. package/src/commands/sprint/index.ts +226 -0
  56. package/src/commands/sprint/list.ts +86 -0
  57. package/src/commands/sprint/plan-utils.ts +207 -0
  58. package/src/commands/sprint/plan.ts +549 -0
  59. package/src/commands/sprint/refine.ts +359 -0
  60. package/src/commands/sprint/requirements.ts +58 -0
  61. package/src/commands/sprint/show.ts +140 -0
  62. package/src/commands/sprint/start.ts +119 -0
  63. package/src/commands/sprint/switch.ts +20 -0
  64. package/src/commands/task/add.ts +316 -0
  65. package/src/commands/task/import.ts +150 -0
  66. package/src/commands/task/index.ts +123 -0
  67. package/src/commands/task/list.ts +145 -0
  68. package/src/commands/task/next.ts +45 -0
  69. package/src/commands/task/remove.ts +47 -0
  70. package/src/commands/task/reorder.ts +45 -0
  71. package/src/commands/task/show.ts +111 -0
  72. package/src/commands/task/status.ts +99 -0
  73. package/src/commands/ticket/add.ts +265 -0
  74. package/src/commands/ticket/edit.ts +166 -0
  75. package/src/commands/ticket/index.ts +114 -0
  76. package/src/commands/ticket/list.ts +128 -0
  77. package/src/commands/ticket/refine-utils.ts +89 -0
  78. package/src/commands/ticket/refine.ts +268 -0
  79. package/src/commands/ticket/remove.ts +48 -0
  80. package/src/commands/ticket/show.ts +74 -0
  81. package/src/completion/handle.ts +30 -0
  82. package/src/completion/resolver.ts +241 -0
  83. package/src/interactive/dashboard.ts +268 -0
  84. package/src/interactive/escapable.ts +81 -0
  85. package/src/interactive/file-browser.ts +153 -0
  86. package/src/interactive/index.ts +429 -0
  87. package/src/interactive/menu.ts +403 -0
  88. package/src/interactive/selectors.ts +273 -0
  89. package/src/interactive/wizard.ts +221 -0
  90. package/src/providers/claude.ts +53 -0
  91. package/src/providers/copilot.ts +86 -0
  92. package/src/providers/index.ts +43 -0
  93. package/src/providers/types.ts +85 -0
  94. package/src/schemas/index.ts +130 -0
  95. package/src/store/config.ts +74 -0
  96. package/src/store/progress.ts +230 -0
  97. package/src/store/project.ts +276 -0
  98. package/src/store/sprint.ts +229 -0
  99. package/src/store/task.ts +443 -0
  100. package/src/store/ticket.ts +178 -0
  101. package/src/theme/index.ts +215 -0
  102. package/src/theme/ui.ts +872 -0
  103. package/src/utils/detect-scripts.ts +247 -0
  104. package/src/utils/editor-input.ts +41 -0
  105. package/src/utils/editor.ts +37 -0
  106. package/src/utils/exit-codes.ts +27 -0
  107. package/src/utils/file-lock.ts +135 -0
  108. package/src/utils/git.ts +185 -0
  109. package/src/utils/ids.ts +37 -0
  110. package/src/utils/issue-fetch.ts +244 -0
  111. package/src/utils/json-extract.ts +62 -0
  112. package/src/utils/multiline.ts +61 -0
  113. package/src/utils/path-selector.ts +236 -0
  114. package/src/utils/paths.ts +108 -0
  115. package/src/utils/provider.ts +34 -0
  116. package/src/utils/requirements-export.ts +63 -0
  117. package/src/utils/storage.ts +107 -0
  118. package/tsconfig.json +25 -0
@@ -0,0 +1,45 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { assertSafeCwd } from '@src/utils/paths.ts';
3
+
4
+ /** Lifecycle events where hooks can fire. Extend this union for new phases. */
5
+ export type LifecycleEvent = 'sprintStart' | 'taskComplete';
6
+
7
+ export interface HookResult {
8
+ passed: boolean;
9
+ output: string;
10
+ }
11
+
12
+ /** Default timeout for lifecycle hooks: 5 minutes. Override via RALPHCTL_SETUP_TIMEOUT_MS. */
13
+ const DEFAULT_HOOK_TIMEOUT_MS = 5 * 60 * 1000;
14
+
15
+ function getHookTimeoutMs(): number {
16
+ const envVal = process.env['RALPHCTL_SETUP_TIMEOUT_MS'];
17
+ if (envVal) {
18
+ const parsed = Number(envVal);
19
+ if (!Number.isNaN(parsed) && parsed > 0) return parsed;
20
+ }
21
+ return DEFAULT_HOOK_TIMEOUT_MS;
22
+ }
23
+
24
+ /**
25
+ * Run a lifecycle hook script in a project directory.
26
+ *
27
+ * Scripts are user-configured via `project add` or `project repo add` —
28
+ * they are NOT arbitrary AI-generated commands.
29
+ */
30
+ export function runLifecycleHook(projectPath: string, script: string, event: LifecycleEvent): HookResult {
31
+ assertSafeCwd(projectPath);
32
+ const timeoutMs = getHookTimeoutMs();
33
+
34
+ const result = spawnSync(script, {
35
+ cwd: projectPath,
36
+ shell: true,
37
+ stdio: ['pipe', 'pipe', 'pipe'],
38
+ encoding: 'utf-8',
39
+ timeout: timeoutMs,
40
+ env: { ...process.env, RALPHCTL_LIFECYCLE_EVENT: event },
41
+ });
42
+
43
+ const output = [result.stdout, result.stderr].filter(Boolean).join('\n').trim();
44
+ return { passed: result.status === 0, output };
45
+ }
@@ -0,0 +1,40 @@
1
+ export interface ExecutionResult {
2
+ success: boolean;
3
+ output: string;
4
+ blockedReason?: string;
5
+ verified?: boolean;
6
+ verificationOutput?: string;
7
+ }
8
+
9
+ /**
10
+ * Parse execution result from AI provider output.
11
+ * Checks for task-verified, task-complete, and task-blocked signals.
12
+ */
13
+ export function parseExecutionResult(output: string): ExecutionResult {
14
+ // Check for verification signal
15
+ const verifiedMatch = /<task-verified>([\s\S]*?)<\/task-verified>/.exec(output);
16
+ const verified = verifiedMatch !== null;
17
+ const verificationOutput = verifiedMatch?.[1]?.trim();
18
+
19
+ // Check for completion signal
20
+ if (output.includes('<task-complete>')) {
21
+ if (!verified) {
22
+ return {
23
+ success: false,
24
+ output,
25
+ blockedReason:
26
+ 'Task marked complete without verification. Output <task-verified> with verification results before <task-complete>.',
27
+ };
28
+ }
29
+ return { success: true, output, verified, verificationOutput };
30
+ }
31
+
32
+ // Check for blocked signal
33
+ const blockedMatch = /<task-blocked>([\s\S]*?)<\/task-blocked>/.exec(output);
34
+ if (blockedMatch) {
35
+ return { success: false, output, blockedReason: blockedMatch[1]?.trim(), verified, verificationOutput };
36
+ }
37
+
38
+ // No signal found - treat as incomplete
39
+ return { success: false, output, blockedReason: 'No completion signal received', verified, verificationOutput };
40
+ }
@@ -0,0 +1,207 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { homedir } from 'node:os';
4
+ import type { AiProvider } from '@src/schemas/index.ts';
5
+
6
+ interface PermissionsConfig {
7
+ allow?: string[];
8
+ deny?: string[];
9
+ }
10
+
11
+ interface SettingsFile {
12
+ permissions?: PermissionsConfig;
13
+ }
14
+
15
+ export interface ProviderPermissions {
16
+ allow: string[];
17
+ deny: string[];
18
+ }
19
+
20
+ /**
21
+ * Get AI provider permissions from settings files.
22
+ * For Claude: checks .claude/settings.local.json and ~/.claude/settings.json
23
+ * For Copilot: returns empty permissions (Copilot uses --available-tools/--excluded-tools flags)
24
+ *
25
+ * @param projectPath - Project directory to check for settings
26
+ * @param provider - AI provider (defaults to 'claude' for backward compat)
27
+ * @returns Combined permissions from both sources
28
+ */
29
+ export function getProviderPermissions(projectPath: string, provider?: AiProvider): ProviderPermissions {
30
+ const permissions: ProviderPermissions = {
31
+ allow: [],
32
+ deny: [],
33
+ };
34
+
35
+ // Copilot manages permissions via CLI flags, not settings files
36
+ if (provider === 'copilot') {
37
+ return permissions;
38
+ }
39
+
40
+ // Check project-level settings (.claude/settings.local.json)
41
+ const projectSettingsPath = join(projectPath, '.claude', 'settings.local.json');
42
+ if (existsSync(projectSettingsPath)) {
43
+ try {
44
+ const content = readFileSync(projectSettingsPath, 'utf-8');
45
+ const settings = JSON.parse(content) as SettingsFile;
46
+ if (settings.permissions?.allow) {
47
+ permissions.allow.push(...settings.permissions.allow);
48
+ }
49
+ if (settings.permissions?.deny) {
50
+ permissions.deny.push(...settings.permissions.deny);
51
+ }
52
+ } catch {
53
+ // Ignore parse errors
54
+ }
55
+ }
56
+
57
+ // Check user-level settings (~/.claude/settings.json)
58
+ const userSettingsPath = join(homedir(), '.claude', 'settings.json');
59
+ if (existsSync(userSettingsPath)) {
60
+ try {
61
+ const content = readFileSync(userSettingsPath, 'utf-8');
62
+ const settings = JSON.parse(content) as SettingsFile;
63
+ if (settings.permissions?.allow) {
64
+ permissions.allow.push(...settings.permissions.allow);
65
+ }
66
+ if (settings.permissions?.deny) {
67
+ permissions.deny.push(...settings.permissions.deny);
68
+ }
69
+ } catch {
70
+ // Ignore parse errors
71
+ }
72
+ }
73
+
74
+ return permissions;
75
+ }
76
+
77
+ /**
78
+ * Check if a specific tool/command is allowed in permissions.
79
+ *
80
+ * Permission patterns:
81
+ * - "Bash(command:*)" - matches "Bash" tool with command starting with "command"
82
+ * - "Bash(git commit:*)" - matches git commit with any message
83
+ * - "Bash(*)" - matches any Bash command
84
+ *
85
+ * @returns true if explicitly allowed, false if denied, 'ask' if no match
86
+ */
87
+ export function isToolAllowed(permissions: ProviderPermissions, tool: string, specifier?: string): boolean | 'ask' {
88
+ // Check deny list first (deny takes precedence)
89
+ for (const pattern of permissions.deny) {
90
+ if (matchesPattern(pattern, tool, specifier)) {
91
+ return false;
92
+ }
93
+ }
94
+
95
+ // Check allow list
96
+ for (const pattern of permissions.allow) {
97
+ if (matchesPattern(pattern, tool, specifier)) {
98
+ return true;
99
+ }
100
+ }
101
+
102
+ // No explicit permission - will ask
103
+ return 'ask';
104
+ }
105
+
106
+ /**
107
+ * Match a permission pattern against a tool call.
108
+ *
109
+ * Pattern formats:
110
+ * - "ToolName" - matches tool name exactly
111
+ * - "ToolName(specifier)" - matches exact specifier
112
+ * - "ToolName(prefix*)" - matches specifier starting with prefix
113
+ * - "ToolName(*)" - matches any specifier for this tool
114
+ */
115
+ function matchesPattern(pattern: string, tool: string, specifier?: string): boolean {
116
+ // Parse pattern
117
+ const parenIdx = pattern.indexOf('(');
118
+
119
+ if (parenIdx === -1) {
120
+ // Pattern is just tool name
121
+ return pattern === tool;
122
+ }
123
+
124
+ const patternTool = pattern.slice(0, parenIdx);
125
+ if (patternTool !== tool) {
126
+ return false;
127
+ }
128
+
129
+ // Extract specifier pattern (remove parentheses)
130
+ const specPattern = pattern.slice(parenIdx + 1, -1);
131
+
132
+ if (specPattern === '*') {
133
+ // Matches any specifier
134
+ return true;
135
+ }
136
+
137
+ if (!specifier) {
138
+ return false;
139
+ }
140
+
141
+ if (specPattern.endsWith(':*')) {
142
+ // Prefix match (e.g., "git commit:*" matches "git commit -m 'msg'")
143
+ const prefix = specPattern.slice(0, -2);
144
+ return specifier.startsWith(prefix);
145
+ }
146
+
147
+ if (specPattern.endsWith('*')) {
148
+ // Simple prefix match
149
+ const prefix = specPattern.slice(0, -1);
150
+ return specifier.startsWith(prefix);
151
+ }
152
+
153
+ // Exact match
154
+ return specPattern === specifier;
155
+ }
156
+
157
+ export interface PermissionWarning {
158
+ tool: string;
159
+ specifier?: string;
160
+ message: string;
161
+ }
162
+
163
+ /**
164
+ * Check permissions for common operations needed during task execution.
165
+ *
166
+ * For Claude: reads settings files and warns about operations that may need approval.
167
+ * For Copilot: returns no warnings (all tools granted via --allow-all-tools).
168
+ *
169
+ * @returns Array of warnings for operations that may need approval
170
+ */
171
+ export function checkTaskPermissions(
172
+ projectPath: string,
173
+ options: {
174
+ checkScript?: string | null;
175
+ needsCommit?: boolean;
176
+ provider?: AiProvider;
177
+ }
178
+ ): PermissionWarning[] {
179
+ const warnings: PermissionWarning[] = [];
180
+ const permissions = getProviderPermissions(projectPath, options.provider);
181
+
182
+ // Check git commit permission
183
+ if (options.needsCommit !== false) {
184
+ const commitAllowed = isToolAllowed(permissions, 'Bash', 'git commit');
185
+ if (commitAllowed !== true) {
186
+ warnings.push({
187
+ tool: 'Bash',
188
+ specifier: 'git commit',
189
+ message: 'Git commits may require manual approval',
190
+ });
191
+ }
192
+ }
193
+
194
+ // Check check script permission
195
+ if (options.checkScript) {
196
+ const checkAllowed = isToolAllowed(permissions, 'Bash', options.checkScript);
197
+ if (checkAllowed !== true) {
198
+ warnings.push({
199
+ tool: 'Bash',
200
+ specifier: options.checkScript,
201
+ message: `Check script "${options.checkScript}" may require approval`,
202
+ });
203
+ }
204
+ }
205
+
206
+ return warnings;
207
+ }
@@ -0,0 +1,248 @@
1
+ import type { ChildProcess } from 'node:child_process';
2
+ import { EXIT_INTERRUPTED } from '@src/utils/exit-codes.ts';
3
+
4
+ /**
5
+ * Graceful shutdown timeout - how long to wait for children to exit after SIGTERM
6
+ */
7
+ const GRACEFUL_SHUTDOWN_TIMEOUT_MS = 5000;
8
+
9
+ /**
10
+ * Double-signal window - time window for second Ctrl+C to trigger force-quit
11
+ */
12
+ const FORCE_QUIT_WINDOW_MS = 5000;
13
+
14
+ /**
15
+ * Singleton manager for all AI provider child processes.
16
+ * Ensures proper cleanup on SIGINT/SIGTERM with graceful shutdown sequence.
17
+ *
18
+ * Features:
19
+ * - First SIGINT: Graceful shutdown (SIGTERM to children, wait 5s, then SIGKILL)
20
+ * - Second SIGINT (within 5s): Force-quit (immediate SIGKILL, exit code 1)
21
+ * - Exit code 130 for SIGINT (standard Unix convention: 128 + 2)
22
+ * - Automatic child cleanup via event listeners
23
+ * - Cleanup callbacks for spinners and temp resources
24
+ */
25
+ export class ProcessManager {
26
+ private static instance: ProcessManager | null = null;
27
+
28
+ /** All active AI child processes */
29
+ private children = new Set<ChildProcess>();
30
+
31
+ /** Cleanup callbacks (for stopping spinners, removing temp files) */
32
+ private cleanupCallbacks = new Set<() => void>();
33
+
34
+ /** Whether we're currently shutting down */
35
+ private exiting = false;
36
+
37
+ /** Whether signal handlers have been installed */
38
+ private handlersInstalled = false;
39
+
40
+ /** Timestamp of first SIGINT (for double-signal detection) */
41
+ private firstSigintAt: number | null = null;
42
+
43
+ private constructor() {
44
+ // Private constructor for singleton
45
+ }
46
+
47
+ /**
48
+ * Get the singleton instance.
49
+ */
50
+ public static getInstance(): ProcessManager {
51
+ ProcessManager.instance ??= new ProcessManager();
52
+ return ProcessManager.instance;
53
+ }
54
+
55
+ /**
56
+ * Reset the singleton for testing.
57
+ * @internal
58
+ */
59
+ public static resetForTesting(): void {
60
+ if (ProcessManager.instance) {
61
+ ProcessManager.instance.dispose();
62
+ ProcessManager.instance = null;
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Register a child process for tracking.
68
+ * Automatically installs signal handlers on first registration.
69
+ * Throws an error if called during shutdown.
70
+ *
71
+ * @throws Error if called during shutdown
72
+ */
73
+ public registerChild(child: ChildProcess): void {
74
+ if (this.exiting) {
75
+ throw new Error('Cannot register child process during shutdown');
76
+ }
77
+
78
+ this.children.add(child);
79
+
80
+ // Auto-cleanup when child exits
81
+ child.once('close', () => {
82
+ this.children.delete(child);
83
+ });
84
+
85
+ // Install signal handlers on first child registration
86
+ if (!this.handlersInstalled) {
87
+ this.installSignalHandlers();
88
+ this.handlersInstalled = true;
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Eagerly install signal handlers without requiring a child registration.
94
+ * Call this at the top of execution loops so Ctrl+C works even before
95
+ * the first AI process is spawned (e.g. while the spinner is visible).
96
+ * Idempotent — safe to call multiple times.
97
+ */
98
+ public ensureHandlers(): void {
99
+ if (!this.handlersInstalled) {
100
+ this.installSignalHandlers();
101
+ this.handlersInstalled = true;
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Check if a shutdown is in progress.
107
+ * Used by execution loops to break immediately on Ctrl+C.
108
+ */
109
+ public isShuttingDown(): boolean {
110
+ return this.exiting;
111
+ }
112
+
113
+ /**
114
+ * Manually unregister a child process.
115
+ * Normally not needed - children auto-unregister via event listeners.
116
+ */
117
+ public unregisterChild(child: ChildProcess): void {
118
+ this.children.delete(child);
119
+ }
120
+
121
+ /**
122
+ * Register a cleanup callback (for spinners, temp files, etc.).
123
+ * Returns a deregister function.
124
+ */
125
+ public registerCleanup(callback: () => void): () => void {
126
+ this.cleanupCallbacks.add(callback);
127
+ return () => {
128
+ this.cleanupCallbacks.delete(callback);
129
+ };
130
+ }
131
+
132
+ /**
133
+ * Kill all tracked child processes with the given signal.
134
+ * Catches errors (ESRCH = already dead, EPERM = permission denied).
135
+ */
136
+ public killAll(signal: NodeJS.Signals): void {
137
+ for (const child of this.children) {
138
+ try {
139
+ child.kill(signal);
140
+ } catch (err) {
141
+ const error = err as NodeJS.ErrnoException;
142
+ if (error.code === 'ESRCH') {
143
+ // Process already dead - silently remove
144
+ this.children.delete(child);
145
+ } else if (error.code === 'EPERM') {
146
+ // Permission denied - log warning
147
+ console.warn(`Warning: Permission denied killing process ${String(child.pid)}`);
148
+ } else {
149
+ // Unknown error - log but continue
150
+ console.error(`Error killing process ${String(child.pid)}:`, error.message);
151
+ }
152
+ }
153
+ }
154
+ }
155
+
156
+ /**
157
+ * Graceful shutdown sequence:
158
+ * 1. Run all cleanup callbacks (stop spinners)
159
+ * 2. Send SIGINT to all children (what AI CLI processes expect)
160
+ * 3. Wait up to 5 seconds for children to exit
161
+ * 4. Send SIGKILL to any remaining children (force)
162
+ * 5. Exit with code 130 (SIGINT) or 1 (force-quit)
163
+ *
164
+ * Double Ctrl+C: immediate SIGKILL + exit(1)
165
+ */
166
+ public async shutdown(signal: NodeJS.Signals): Promise<void> {
167
+ // Double-signal force-quit check MUST run before the exiting guard,
168
+ // otherwise the second Ctrl+C is swallowed and force-quit never fires.
169
+ if (signal === 'SIGINT' && this.firstSigintAt) {
170
+ const now = Date.now();
171
+ if (now - this.firstSigintAt < FORCE_QUIT_WINDOW_MS) {
172
+ console.log('\n\nForce quit (double signal) — killing all processes immediately...');
173
+ this.killAll('SIGKILL');
174
+ process.exit(1);
175
+ return;
176
+ }
177
+ }
178
+
179
+ if (this.exiting) {
180
+ return; // Already shutting down (non-SIGINT duplicate)
181
+ }
182
+
183
+ this.exiting = true;
184
+
185
+ // Record timestamp for double-signal detection
186
+ if (signal === 'SIGINT') {
187
+ this.firstSigintAt = Date.now();
188
+ }
189
+
190
+ console.log('\n\nShutting down gracefully... (press Ctrl+C again to force-quit)');
191
+
192
+ // Run cleanup callbacks
193
+ for (const callback of this.cleanupCallbacks) {
194
+ try {
195
+ callback();
196
+ } catch (err) {
197
+ const error = err as Error;
198
+ console.error('Error in cleanup callback:', error.message);
199
+ }
200
+ }
201
+ this.cleanupCallbacks.clear();
202
+
203
+ // Send SIGINT to children — claude CLI handles SIGINT for graceful shutdown.
204
+ // SIGTERM may be ignored by some child process trees.
205
+ this.killAll('SIGINT');
206
+
207
+ // Wait for children to exit (with timeout)
208
+ const waitStart = Date.now();
209
+ while (this.children.size > 0 && Date.now() - waitStart < GRACEFUL_SHUTDOWN_TIMEOUT_MS) {
210
+ await new Promise((resolve) => setTimeout(resolve, 100));
211
+ }
212
+
213
+ // Force-kill any remaining children
214
+ if (this.children.size > 0) {
215
+ console.log(`Force-killing ${String(this.children.size)} remaining process(es)...`);
216
+ this.killAll('SIGKILL');
217
+ }
218
+
219
+ // Exit with appropriate code
220
+ process.exit(signal === 'SIGINT' ? EXIT_INTERRUPTED : 1);
221
+ }
222
+
223
+ /**
224
+ * Clean up all resources (for testing).
225
+ * @internal
226
+ */
227
+ public dispose(): void {
228
+ this.children.clear();
229
+ this.cleanupCallbacks.clear();
230
+ this.exiting = false;
231
+ this.handlersInstalled = false;
232
+ this.firstSigintAt = null;
233
+ }
234
+
235
+ /**
236
+ * Install signal handlers for SIGINT and SIGTERM.
237
+ * Uses process.on() (persistent) not process.once() (one-shot).
238
+ */
239
+ private installSignalHandlers(): void {
240
+ process.on('SIGINT', () => {
241
+ void this.shutdown('SIGINT');
242
+ });
243
+
244
+ process.on('SIGTERM', () => {
245
+ void this.shutdown('SIGTERM');
246
+ });
247
+ }
248
+ }
@@ -0,0 +1,144 @@
1
+ # Autonomous Ideation to Implementation
2
+
3
+ You are a combined requirements analyst and task planner working autonomously. Your goal is to turn a rough idea into
4
+ refined requirements and a dependency-ordered set of implementation tasks. Make all decisions based on the idea
5
+ description and codebase analysis — there is no user to interact with.
6
+
7
+ ## Two-Phase Protocol
8
+
9
+ ### Phase 1: Refine Requirements (WHAT)
10
+
11
+ Analyze the idea and produce complete, implementation-agnostic requirements:
12
+
13
+ - **Problem statement** — What problem are we solving and for whom?
14
+ - **Functional requirements** — What should the system do? (behavior, not implementation)
15
+ - **Acceptance criteria** — Testable conditions (Given/When/Then format preferred)
16
+ - **Scope boundaries** — What's in vs out of scope
17
+ - **Constraints** — Performance, compatibility, business rules if applicable
18
+
19
+ **Output format:**
20
+
21
+ ```markdown
22
+ ## Problem
23
+
24
+ [Clear problem statement]
25
+
26
+ ## Requirements
27
+
28
+ - [Functional requirement 1]
29
+ - [Functional requirement 2]
30
+
31
+ ## Acceptance Criteria
32
+
33
+ - Given [precondition], When [action], Then [expected result]
34
+ - [Additional criteria...]
35
+
36
+ ## Scope
37
+
38
+ **In scope:**
39
+
40
+ - [What's included]
41
+
42
+ **Out of scope:**
43
+
44
+ - [What's explicitly excluded or deferred]
45
+
46
+ ## Constraints
47
+
48
+ - [Business/technical constraints if any]
49
+ ```
50
+
51
+ ### Phase 2: Plan Implementation (HOW)
52
+
53
+ Explore the selected repositories and produce implementation tasks:
54
+
55
+ 1. **Explore codebase** — Read CLAUDE.md (if exists), understand project structure, find patterns
56
+ 2. **Map requirements to implementation** — Determine which parts map to which repository
57
+ 3. **Create tasks** — Following the Planning Common Context guidelines below
58
+ 4. **Validate** — Ensure tasks are non-overlapping, properly ordered, and completable
59
+
60
+ ## Idea to Implement
61
+
62
+ **Title:** {{IDEA_TITLE}}
63
+
64
+ **Project:** {{PROJECT_NAME}}
65
+
66
+ **Description:**
67
+
68
+ {{IDEA_DESCRIPTION}}
69
+
70
+ ## Selected Repositories
71
+
72
+ You have access to these repositories:
73
+
74
+ {{REPOSITORIES}}
75
+
76
+ ## Planning Common Context
77
+
78
+ {{COMMON}}
79
+
80
+ ## Pre-Output Validation
81
+
82
+ Before outputting JSON, verify:
83
+
84
+ 1. **Requirements complete** — Problem statement, acceptance criteria, and scope boundaries are all present
85
+ 2. **No file overlap** — No two tasks modify the same files (or overlap is delineated in steps)
86
+ 3. **Correct order** — Foundations before dependents, all `blockedBy` references point to earlier tasks
87
+ 4. **Maximized parallelism** — Independent tasks do NOT block each other unnecessarily
88
+ 5. **Precise steps** — Every task has 3+ specific, actionable steps with file references
89
+ 6. **Verification steps** — Every task ends with project-appropriate verification commands
90
+ 7. **projectPath assigned** — Every task uses a path from the Selected Repositories
91
+
92
+ If you cannot produce a valid plan, signal: `<planning-blocked>reason</planning-blocked>`
93
+
94
+ ## Output Format
95
+
96
+ Output a single JSON object with both requirements and tasks.
97
+ If you cannot produce a valid plan, output `<planning-blocked>reason</planning-blocked>` instead of JSON.
98
+
99
+ ```json
100
+ {{SCHEMA}}
101
+ ```
102
+
103
+ **Requirements:**
104
+
105
+ - Complete markdown string with the structure shown in Phase 1
106
+ - Implementation-agnostic (WHAT, not HOW)
107
+ - Clear acceptance criteria
108
+
109
+ **Tasks:**
110
+
111
+ - Each task has `id`, `name`, `projectPath`, `steps`, and optional `blockedBy`
112
+ - `projectPath` must be one of the Selected Repositories paths
113
+ - Steps reference actual files discovered during exploration
114
+ - Verification steps use commands from CLAUDE.md if available
115
+ - Tasks properly ordered by dependencies
116
+
117
+ **Example:**
118
+
119
+ ```json
120
+ {
121
+ "requirements": "## Problem\n\nUsers cannot filter exports by date range...\n\n## Requirements\n\n- Support optional start/end date query parameters...\n\n## Acceptance Criteria\n\n- Given valid ISO dates, When GET /exports?startDate=...&endDate=..., Then only matching exports returned\n\n## Scope\n\n**In scope:** Date filtering on export endpoint\n**Out of scope:** Date filtering on other endpoints\n\n## Constraints\n\n- Must use ISO8601 date format",
122
+ "tasks": [
123
+ {
124
+ "id": "1",
125
+ "name": "Add date range validation schema and export filter",
126
+ "projectPath": "/Users/dev/my-app",
127
+ "steps": [
128
+ "Create src/schemas/date-range.ts with DateRangeSchema using Zod — validate ISO8601 format, ensure startDate <= endDate",
129
+ "Modify src/controllers/export.ts to accept optional startDate/endDate query params using DateRangeSchema",
130
+ "Update src/repositories/export.ts findExports() to add WHERE clause for date filtering",
131
+ "Add unit tests in src/schemas/__tests__/date-range.test.ts covering valid ranges, invalid formats, and reversed dates",
132
+ "Add integration test in src/controllers/__tests__/export.test.ts for filtered and unfiltered queries",
133
+ "Run pnpm typecheck && pnpm lint && pnpm test — all pass"
134
+ ],
135
+ "blockedBy": []
136
+ }
137
+ ]
138
+ }
139
+ ```
140
+
141
+ ---
142
+
143
+ Proceed autonomously: refine the idea into clear requirements, explore the codebase, then generate tasks. Output only
144
+ the final JSON when complete.