ralphctl 0.1.0 → 0.1.2

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 (130) hide show
  1. package/README.md +58 -24
  2. package/dist/add-HGJCLWED.mjs +14 -0
  3. package/dist/add-MRGCS3US.mjs +14 -0
  4. package/dist/chunk-6PYTKGB5.mjs +316 -0
  5. package/dist/chunk-7TG3EAQ2.mjs +20 -0
  6. package/dist/chunk-EKMZZRWI.mjs +521 -0
  7. package/dist/chunk-JON4GCLR.mjs +59 -0
  8. package/dist/chunk-LOR7QBXX.mjs +3683 -0
  9. package/dist/chunk-MNMQC36F.mjs +556 -0
  10. package/dist/chunk-MRKOFVTM.mjs +537 -0
  11. package/dist/chunk-NTWO2LXB.mjs +52 -0
  12. package/dist/chunk-QBXHAXHI.mjs +562 -0
  13. package/dist/chunk-WGHJI3OI.mjs +214 -0
  14. package/dist/cli.mjs +4245 -0
  15. package/dist/create-MG7E7PLQ.mjs +10 -0
  16. package/dist/handle-UG5M2OON.mjs +22 -0
  17. package/dist/multiline-OHSNFCRG.mjs +40 -0
  18. package/dist/project-NT3L4FTB.mjs +28 -0
  19. package/dist/resolver-WSFWKACM.mjs +153 -0
  20. package/dist/sprint-4VHDLGFN.mjs +37 -0
  21. package/dist/wizard-LRELAN2J.mjs +196 -0
  22. package/package.json +19 -28
  23. package/CHANGELOG.md +0 -94
  24. package/bin/ralphctl +0 -13
  25. package/src/ai/executor.ts +0 -973
  26. package/src/ai/lifecycle.ts +0 -45
  27. package/src/ai/parser.ts +0 -40
  28. package/src/ai/permissions.ts +0 -207
  29. package/src/ai/process-manager.ts +0 -248
  30. package/src/ai/prompts/index.ts +0 -89
  31. package/src/ai/rate-limiter.ts +0 -89
  32. package/src/ai/runner.ts +0 -478
  33. package/src/ai/session.ts +0 -319
  34. package/src/ai/task-context.ts +0 -270
  35. package/src/cli-metadata.ts +0 -7
  36. package/src/cli.ts +0 -65
  37. package/src/commands/completion/index.ts +0 -33
  38. package/src/commands/config/config.ts +0 -58
  39. package/src/commands/config/index.ts +0 -33
  40. package/src/commands/dashboard/dashboard.ts +0 -5
  41. package/src/commands/dashboard/index.ts +0 -6
  42. package/src/commands/doctor/doctor.ts +0 -271
  43. package/src/commands/doctor/index.ts +0 -25
  44. package/src/commands/progress/index.ts +0 -25
  45. package/src/commands/progress/log.ts +0 -64
  46. package/src/commands/progress/show.ts +0 -14
  47. package/src/commands/project/add.ts +0 -336
  48. package/src/commands/project/index.ts +0 -104
  49. package/src/commands/project/list.ts +0 -31
  50. package/src/commands/project/remove.ts +0 -43
  51. package/src/commands/project/repo.ts +0 -118
  52. package/src/commands/project/show.ts +0 -49
  53. package/src/commands/sprint/close.ts +0 -180
  54. package/src/commands/sprint/context.ts +0 -109
  55. package/src/commands/sprint/create.ts +0 -60
  56. package/src/commands/sprint/current.ts +0 -75
  57. package/src/commands/sprint/delete.ts +0 -72
  58. package/src/commands/sprint/health.ts +0 -229
  59. package/src/commands/sprint/ideate.ts +0 -496
  60. package/src/commands/sprint/index.ts +0 -226
  61. package/src/commands/sprint/list.ts +0 -86
  62. package/src/commands/sprint/plan-utils.ts +0 -207
  63. package/src/commands/sprint/plan.ts +0 -549
  64. package/src/commands/sprint/refine.ts +0 -359
  65. package/src/commands/sprint/requirements.ts +0 -58
  66. package/src/commands/sprint/show.ts +0 -140
  67. package/src/commands/sprint/start.ts +0 -119
  68. package/src/commands/sprint/switch.ts +0 -20
  69. package/src/commands/task/add.ts +0 -316
  70. package/src/commands/task/import.ts +0 -150
  71. package/src/commands/task/index.ts +0 -123
  72. package/src/commands/task/list.ts +0 -145
  73. package/src/commands/task/next.ts +0 -45
  74. package/src/commands/task/remove.ts +0 -47
  75. package/src/commands/task/reorder.ts +0 -45
  76. package/src/commands/task/show.ts +0 -111
  77. package/src/commands/task/status.ts +0 -99
  78. package/src/commands/ticket/add.ts +0 -265
  79. package/src/commands/ticket/edit.ts +0 -166
  80. package/src/commands/ticket/index.ts +0 -114
  81. package/src/commands/ticket/list.ts +0 -128
  82. package/src/commands/ticket/refine-utils.ts +0 -89
  83. package/src/commands/ticket/refine.ts +0 -268
  84. package/src/commands/ticket/remove.ts +0 -48
  85. package/src/commands/ticket/show.ts +0 -74
  86. package/src/completion/handle.ts +0 -30
  87. package/src/completion/resolver.ts +0 -241
  88. package/src/interactive/dashboard.ts +0 -268
  89. package/src/interactive/escapable.ts +0 -81
  90. package/src/interactive/file-browser.ts +0 -153
  91. package/src/interactive/index.ts +0 -429
  92. package/src/interactive/menu.ts +0 -403
  93. package/src/interactive/selectors.ts +0 -273
  94. package/src/interactive/wizard.ts +0 -221
  95. package/src/providers/claude.ts +0 -53
  96. package/src/providers/copilot.ts +0 -86
  97. package/src/providers/index.ts +0 -43
  98. package/src/providers/types.ts +0 -85
  99. package/src/schemas/index.ts +0 -130
  100. package/src/store/config.ts +0 -74
  101. package/src/store/progress.ts +0 -230
  102. package/src/store/project.ts +0 -276
  103. package/src/store/sprint.ts +0 -229
  104. package/src/store/task.ts +0 -443
  105. package/src/store/ticket.ts +0 -178
  106. package/src/theme/index.ts +0 -215
  107. package/src/theme/ui.ts +0 -872
  108. package/src/utils/detect-scripts.ts +0 -247
  109. package/src/utils/editor-input.ts +0 -41
  110. package/src/utils/editor.ts +0 -37
  111. package/src/utils/exit-codes.ts +0 -27
  112. package/src/utils/file-lock.ts +0 -135
  113. package/src/utils/git.ts +0 -185
  114. package/src/utils/ids.ts +0 -37
  115. package/src/utils/issue-fetch.ts +0 -244
  116. package/src/utils/json-extract.ts +0 -62
  117. package/src/utils/multiline.ts +0 -61
  118. package/src/utils/path-selector.ts +0 -236
  119. package/src/utils/paths.ts +0 -108
  120. package/src/utils/provider.ts +0 -34
  121. package/src/utils/requirements-export.ts +0 -63
  122. package/src/utils/storage.ts +0 -107
  123. package/tsconfig.json +0 -25
  124. /package/{src/ai → dist}/prompts/ideate-auto.md +0 -0
  125. /package/{src/ai → dist}/prompts/ideate.md +0 -0
  126. /package/{src/ai → dist}/prompts/plan-auto.md +0 -0
  127. /package/{src/ai → dist}/prompts/plan-common.md +0 -0
  128. /package/{src/ai → dist}/prompts/plan-interactive.md +0 -0
  129. /package/{src/ai → dist}/prompts/task-execution.md +0 -0
  130. /package/{src/ai → dist}/prompts/ticket-refine.md +0 -0
@@ -1,45 +0,0 @@
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 DELETED
@@ -1,40 +0,0 @@
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
- }
@@ -1,207 +0,0 @@
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
- }
@@ -1,248 +0,0 @@
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
- }
@@ -1,89 +0,0 @@
1
- import { readFileSync } from 'node:fs';
2
- import { dirname, join } from 'node:path';
3
- import { fileURLToPath } from 'node:url';
4
-
5
- const __dirname = dirname(fileURLToPath(import.meta.url));
6
-
7
- function loadTemplate(name: string): string {
8
- return readFileSync(join(__dirname, `${name}.md`), 'utf-8');
9
- }
10
-
11
- function buildPlanPrompt(template: string, context: string, schema: string): string {
12
- const common = loadTemplate('plan-common');
13
- return template.replace('{{COMMON}}', common).replace('{{CONTEXT}}', context).replace('{{SCHEMA}}', schema);
14
- }
15
-
16
- export function buildInteractivePrompt(context: string, outputFile: string, schema: string): string {
17
- const template = loadTemplate('plan-interactive');
18
- return buildPlanPrompt(template, context, schema).replace('{{OUTPUT_FILE}}', outputFile);
19
- }
20
-
21
- export function buildAutoPrompt(context: string, schema: string): string {
22
- const template = loadTemplate('plan-auto');
23
- return buildPlanPrompt(template, context, schema);
24
- }
25
-
26
- export function buildTaskExecutionPrompt(progressFilePath: string, noCommit: boolean, contextFileName: string): string {
27
- const template = loadTemplate('task-execution');
28
- const commitStep = noCommit
29
- ? ''
30
- : '\n> **Before continuing:** Create a git commit with a descriptive message for the changes made.\n';
31
- const commitConstraint = noCommit ? '' : '- **Must commit** — Create a git commit before signaling completion.\n';
32
- return template
33
- .replace('{{PROGRESS_FILE}}', progressFilePath)
34
- .replace('{{COMMIT_STEP}}', commitStep)
35
- .replace('{{COMMIT_CONSTRAINT}}', commitConstraint)
36
- .replaceAll('{{CONTEXT_FILE}}', contextFileName);
37
- }
38
-
39
- export function buildTicketRefinePrompt(
40
- ticketContent: string,
41
- outputFile: string,
42
- schema: string,
43
- issueContext = ''
44
- ): string {
45
- const template = loadTemplate('ticket-refine');
46
- return template
47
- .replace('{{TICKET}}', ticketContent)
48
- .replace('{{OUTPUT_FILE}}', outputFile)
49
- .replace('{{SCHEMA}}', schema)
50
- .replace('{{ISSUE_CONTEXT}}', issueContext);
51
- }
52
-
53
- export function buildIdeatePrompt(
54
- ideaTitle: string,
55
- ideaDescription: string,
56
- projectName: string,
57
- repositories: string,
58
- outputFile: string,
59
- schema: string
60
- ): string {
61
- const template = loadTemplate('ideate');
62
- const common = loadTemplate('plan-common');
63
- return template
64
- .replace('{{IDEA_TITLE}}', ideaTitle)
65
- .replace('{{IDEA_DESCRIPTION}}', ideaDescription)
66
- .replace('{{PROJECT_NAME}}', projectName)
67
- .replace('{{REPOSITORIES}}', repositories)
68
- .replace('{{OUTPUT_FILE}}', outputFile)
69
- .replace('{{SCHEMA}}', schema)
70
- .replace('{{COMMON}}', common);
71
- }
72
-
73
- export function buildIdeateAutoPrompt(
74
- ideaTitle: string,
75
- ideaDescription: string,
76
- projectName: string,
77
- repositories: string,
78
- schema: string
79
- ): string {
80
- const template = loadTemplate('ideate-auto');
81
- const common = loadTemplate('plan-common');
82
- return template
83
- .replace('{{IDEA_TITLE}}', ideaTitle)
84
- .replace('{{IDEA_DESCRIPTION}}', ideaDescription)
85
- .replace('{{PROJECT_NAME}}', projectName)
86
- .replace('{{REPOSITORIES}}', repositories)
87
- .replace('{{SCHEMA}}', schema)
88
- .replace('{{COMMON}}', common);
89
- }