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.
- package/CHANGELOG.md +94 -0
- package/LICENSE +21 -0
- package/README.md +189 -0
- package/bin/ralphctl +13 -0
- package/package.json +92 -0
- package/schemas/config.schema.json +20 -0
- package/schemas/ideate-output.schema.json +22 -0
- package/schemas/projects.schema.json +53 -0
- package/schemas/requirements-output.schema.json +24 -0
- package/schemas/sprint.schema.json +109 -0
- package/schemas/task-import.schema.json +49 -0
- package/schemas/tasks.schema.json +72 -0
- package/src/ai/executor.ts +973 -0
- package/src/ai/lifecycle.ts +45 -0
- package/src/ai/parser.ts +40 -0
- package/src/ai/permissions.ts +207 -0
- package/src/ai/process-manager.ts +248 -0
- package/src/ai/prompts/ideate-auto.md +144 -0
- package/src/ai/prompts/ideate.md +165 -0
- package/src/ai/prompts/index.ts +89 -0
- package/src/ai/prompts/plan-auto.md +131 -0
- package/src/ai/prompts/plan-common.md +157 -0
- package/src/ai/prompts/plan-interactive.md +190 -0
- package/src/ai/prompts/task-execution.md +159 -0
- package/src/ai/prompts/ticket-refine.md +230 -0
- package/src/ai/rate-limiter.ts +89 -0
- package/src/ai/runner.ts +478 -0
- package/src/ai/session.ts +319 -0
- package/src/ai/task-context.ts +270 -0
- package/src/cli-metadata.ts +7 -0
- package/src/cli.ts +65 -0
- package/src/commands/completion/index.ts +33 -0
- package/src/commands/config/config.ts +58 -0
- package/src/commands/config/index.ts +33 -0
- package/src/commands/dashboard/dashboard.ts +5 -0
- package/src/commands/dashboard/index.ts +6 -0
- package/src/commands/doctor/doctor.ts +271 -0
- package/src/commands/doctor/index.ts +25 -0
- package/src/commands/progress/index.ts +25 -0
- package/src/commands/progress/log.ts +64 -0
- package/src/commands/progress/show.ts +14 -0
- package/src/commands/project/add.ts +336 -0
- package/src/commands/project/index.ts +104 -0
- package/src/commands/project/list.ts +31 -0
- package/src/commands/project/remove.ts +43 -0
- package/src/commands/project/repo.ts +118 -0
- package/src/commands/project/show.ts +49 -0
- package/src/commands/sprint/close.ts +180 -0
- package/src/commands/sprint/context.ts +109 -0
- package/src/commands/sprint/create.ts +60 -0
- package/src/commands/sprint/current.ts +75 -0
- package/src/commands/sprint/delete.ts +72 -0
- package/src/commands/sprint/health.ts +229 -0
- package/src/commands/sprint/ideate.ts +496 -0
- package/src/commands/sprint/index.ts +226 -0
- package/src/commands/sprint/list.ts +86 -0
- package/src/commands/sprint/plan-utils.ts +207 -0
- package/src/commands/sprint/plan.ts +549 -0
- package/src/commands/sprint/refine.ts +359 -0
- package/src/commands/sprint/requirements.ts +58 -0
- package/src/commands/sprint/show.ts +140 -0
- package/src/commands/sprint/start.ts +119 -0
- package/src/commands/sprint/switch.ts +20 -0
- package/src/commands/task/add.ts +316 -0
- package/src/commands/task/import.ts +150 -0
- package/src/commands/task/index.ts +123 -0
- package/src/commands/task/list.ts +145 -0
- package/src/commands/task/next.ts +45 -0
- package/src/commands/task/remove.ts +47 -0
- package/src/commands/task/reorder.ts +45 -0
- package/src/commands/task/show.ts +111 -0
- package/src/commands/task/status.ts +99 -0
- package/src/commands/ticket/add.ts +265 -0
- package/src/commands/ticket/edit.ts +166 -0
- package/src/commands/ticket/index.ts +114 -0
- package/src/commands/ticket/list.ts +128 -0
- package/src/commands/ticket/refine-utils.ts +89 -0
- package/src/commands/ticket/refine.ts +268 -0
- package/src/commands/ticket/remove.ts +48 -0
- package/src/commands/ticket/show.ts +74 -0
- package/src/completion/handle.ts +30 -0
- package/src/completion/resolver.ts +241 -0
- package/src/interactive/dashboard.ts +268 -0
- package/src/interactive/escapable.ts +81 -0
- package/src/interactive/file-browser.ts +153 -0
- package/src/interactive/index.ts +429 -0
- package/src/interactive/menu.ts +403 -0
- package/src/interactive/selectors.ts +273 -0
- package/src/interactive/wizard.ts +221 -0
- package/src/providers/claude.ts +53 -0
- package/src/providers/copilot.ts +86 -0
- package/src/providers/index.ts +43 -0
- package/src/providers/types.ts +85 -0
- package/src/schemas/index.ts +130 -0
- package/src/store/config.ts +74 -0
- package/src/store/progress.ts +230 -0
- package/src/store/project.ts +276 -0
- package/src/store/sprint.ts +229 -0
- package/src/store/task.ts +443 -0
- package/src/store/ticket.ts +178 -0
- package/src/theme/index.ts +215 -0
- package/src/theme/ui.ts +872 -0
- package/src/utils/detect-scripts.ts +247 -0
- package/src/utils/editor-input.ts +41 -0
- package/src/utils/editor.ts +37 -0
- package/src/utils/exit-codes.ts +27 -0
- package/src/utils/file-lock.ts +135 -0
- package/src/utils/git.ts +185 -0
- package/src/utils/ids.ts +37 -0
- package/src/utils/issue-fetch.ts +244 -0
- package/src/utils/json-extract.ts +62 -0
- package/src/utils/multiline.ts +61 -0
- package/src/utils/path-selector.ts +236 -0
- package/src/utils/paths.ts +108 -0
- package/src/utils/provider.ts +34 -0
- package/src/utils/requirements-export.ts +63 -0
- package/src/utils/storage.ts +107 -0
- 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
|
+
}
|
package/src/ai/parser.ts
ADDED
|
@@ -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.
|