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
package/src/ai/session.ts DELETED
@@ -1,319 +0,0 @@
1
- import { spawn, spawnSync } from 'node:child_process';
2
- import { ProcessManager } from '@src/ai/process-manager.ts';
3
- import { assertSafeCwd } from '@src/utils/paths.ts';
4
- import { type ProviderAdapter } from '@src/providers/types.ts';
5
- import { getActiveProvider } from '@src/providers/index.ts';
6
-
7
- // Re-export types from providers for backward compatibility
8
- export type { HeadlessSpawnOptions, SpawnResult } from '@src/providers/types.ts';
9
- export type { SpawnSyncOptions, SpawnAsyncOptions } from '@src/providers/types.ts';
10
-
11
- // Local import aliases for use in function signatures
12
- import type { HeadlessSpawnOptions, SpawnResult, SpawnSyncOptions, SpawnAsyncOptions } from '@src/providers/types.ts';
13
-
14
- /** Parsed JSON result from provider CLI --output-format json */
15
- export interface ProviderJsonResult {
16
- type: string;
17
- subtype: string;
18
- is_error: boolean;
19
- result: string;
20
- session_id: string;
21
- duration_ms: number;
22
- total_cost_usd: number;
23
- num_turns: number;
24
- }
25
-
26
- export class SpawnError extends Error {
27
- public readonly stderr: string;
28
- public readonly exitCode: number;
29
- public readonly rateLimited: boolean;
30
- public readonly retryAfterMs: number | null;
31
- /** Session ID if available (for resume after rate limit) */
32
- public readonly sessionId: string | null;
33
-
34
- constructor(
35
- message: string,
36
- stderr: string,
37
- exitCode: number,
38
- sessionId?: string | null,
39
- provider?: ProviderAdapter
40
- ) {
41
- super(message);
42
- this.name = 'SpawnError';
43
- this.stderr = stderr;
44
- this.exitCode = exitCode;
45
- this.sessionId = sessionId ?? null;
46
- const rl = provider ? provider.detectRateLimit(stderr) : detectRateLimitFallback(stderr);
47
- this.rateLimited = rl.rateLimited;
48
- this.retryAfterMs = rl.retryAfterMs;
49
- }
50
- }
51
-
52
- /**
53
- * Fallback rate limit detection (used when no provider is available).
54
- */
55
- function detectRateLimitFallback(stderr: string): { rateLimited: boolean; retryAfterMs: number | null } {
56
- const patterns = [/rate.?limit/i, /\b429\b/, /too many requests/i, /overloaded/i, /\b529\b/];
57
- const isRateLimited = patterns.some((p) => p.test(stderr));
58
- if (!isRateLimited) {
59
- return { rateLimited: false, retryAfterMs: null };
60
- }
61
- const retryMatch = /retry.?after:?\s*(\d+)/i.exec(stderr);
62
- const retryAfterMs = retryMatch?.[1] ? parseInt(retryMatch[1], 10) * 1000 : null;
63
- return { rateLimited: true, retryAfterMs };
64
- }
65
-
66
- /**
67
- * Detect rate limit signals in stderr output.
68
- * @deprecated Use provider.detectRateLimit() instead.
69
- */
70
- export function detectRateLimit(stderr: string): { rateLimited: boolean; retryAfterMs: number | null } {
71
- return detectRateLimitFallback(stderr);
72
- }
73
-
74
- /**
75
- * Parse JSON output from provider CLI --output-format json.
76
- * @deprecated Use provider.parseJsonOutput() instead.
77
- */
78
- export function parseJsonOutput(stdout: string): { result: string; sessionId: string | null } {
79
- try {
80
- const parsed = JSON.parse(stdout) as Partial<ProviderJsonResult>;
81
- return {
82
- result: parsed.result ?? stdout,
83
- sessionId: parsed.session_id ?? null,
84
- };
85
- } catch {
86
- return { result: stdout, sessionId: null };
87
- }
88
- }
89
-
90
- /**
91
- * Spawn AI CLI for interactive session.
92
- *
93
- * Starts a single interactive session with an optional initial prompt.
94
- * The prompt is passed as a CLI argument, keeping everything in one session.
95
- * User sees and interacts with the AI directly in the terminal.
96
- *
97
- * @param prompt - Optional initial prompt to start the session with.
98
- * @param options - Spawn options (cwd, args, env).
99
- * @param provider - Provider adapter (defaults to active provider resolved from config).
100
- */
101
- export function spawnInteractive(
102
- prompt: string,
103
- options: SpawnSyncOptions,
104
- provider?: ProviderAdapter
105
- ): { code: number; error?: string } {
106
- assertSafeCwd(options.cwd);
107
-
108
- // If no provider given, use a synchronous fallback (claude) since we can't await here
109
- const p =
110
- provider ??
111
- ({
112
- binary: 'claude',
113
- baseArgs: ['--permission-mode', 'acceptEdits'],
114
- buildInteractiveArgs: (pr: string, extra: string[] = []) => [
115
- ...['--permission-mode', 'acceptEdits'],
116
- ...extra,
117
- '--',
118
- pr,
119
- ],
120
- } as Pick<ProviderAdapter, 'binary' | 'baseArgs' | 'buildInteractiveArgs'>);
121
-
122
- const args = prompt ? p.buildInteractiveArgs(prompt, options.args ?? []) : [...p.baseArgs, ...(options.args ?? [])];
123
-
124
- const env = options.env ? { ...process.env, ...options.env } : undefined;
125
-
126
- const result = spawnSync(p.binary, args, {
127
- cwd: options.cwd,
128
- stdio: 'inherit',
129
- env,
130
- });
131
-
132
- if (result.error) {
133
- return { code: 1, error: `Failed to spawn ${p.binary} CLI: ${result.error.message}` };
134
- }
135
-
136
- return { code: result.status ?? 1 };
137
- }
138
-
139
- /**
140
- * Spawn AI CLI in print mode for headless execution.
141
- * Captures stdout and returns the text result.
142
- *
143
- * Uses --output-format json internally to capture session IDs.
144
- * The returned string is the extracted `result` field from the JSON output.
145
- */
146
- export async function spawnHeadless(
147
- options: SpawnAsyncOptions & { prompt?: string },
148
- provider?: ProviderAdapter
149
- ): Promise<string> {
150
- const result = await spawnHeadlessRaw(options as HeadlessSpawnOptions, provider);
151
- return result.stdout;
152
- }
153
-
154
- /**
155
- * Low-level headless spawn returning structured result.
156
- *
157
- * Uses --output-format json to capture session_id for resumability.
158
- * Extracts the text result from JSON and returns it in stdout.
159
- * Session ID is available in the returned SpawnResult.
160
- *
161
- * Throws SpawnError on non-zero exit (includes rate limit detection + session ID).
162
- */
163
- export async function spawnHeadlessRaw(
164
- options: HeadlessSpawnOptions,
165
- provider?: ProviderAdapter
166
- ): Promise<SpawnResult> {
167
- assertSafeCwd(options.cwd);
168
- const p = provider ?? (await getActiveProvider());
169
-
170
- return new Promise((resolve, reject) => {
171
- const allArgs = p.buildHeadlessArgs(options.args ?? []);
172
-
173
- // Add --resume if resuming a session (validate format to prevent argument injection)
174
- if (options.resumeSessionId) {
175
- if (!/^[a-zA-Z0-9_][a-zA-Z0-9_-]{0,127}$/.test(options.resumeSessionId)) {
176
- reject(new SpawnError('Invalid session ID format', '', 1, null, p));
177
- return;
178
- }
179
- allArgs.push('--resume', options.resumeSessionId);
180
- }
181
-
182
- const child = spawn(p.binary, allArgs, {
183
- cwd: options.cwd,
184
- stdio: ['pipe', 'pipe', 'pipe'],
185
- env: options.env ? { ...process.env, ...options.env } : undefined,
186
- });
187
-
188
- // Register child with ProcessManager for signal handling
189
- const manager = ProcessManager.getInstance();
190
- try {
191
- manager.registerChild(child);
192
- } catch {
193
- reject(new SpawnError('Cannot spawn during shutdown', '', 1, null, p));
194
- return;
195
- }
196
-
197
- // Write prompt to stdin if provided, then close
198
- const MAX_PROMPT_SIZE = 1_000_000; // 1MB
199
- if (options.prompt) {
200
- if (options.prompt.length > MAX_PROMPT_SIZE) {
201
- reject(new SpawnError('Prompt exceeds maximum size (1MB)', '', 1, null, p));
202
- return;
203
- }
204
- child.stdin.write(options.prompt);
205
- }
206
- child.stdin.end();
207
-
208
- let rawStdout = '';
209
- let stderr = '';
210
-
211
- child.stdout.on('data', (data: Buffer) => {
212
- rawStdout += data.toString();
213
- });
214
-
215
- child.stderr.on('data', (data: Buffer) => {
216
- stderr += data.toString();
217
- });
218
-
219
- child.on('close', (code) => {
220
- void (async () => {
221
- const exitCode = code ?? 1;
222
-
223
- // Parse output to extract result text and session ID.
224
- // For Claude: JSON output contains session_id directly.
225
- // For Copilot: plain text output; session ID captured via --share file.
226
- const { result, sessionId: parsedSessionId } = p.parseJsonOutput(rawStdout);
227
- const sessionId = parsedSessionId ?? (await p.extractSessionId?.(options.cwd)) ?? null;
228
-
229
- if (exitCode !== 0) {
230
- reject(
231
- new SpawnError(
232
- `${p.displayName} CLI exited with code ${String(exitCode)}: ${stderr}`,
233
- stderr,
234
- exitCode,
235
- sessionId,
236
- p
237
- )
238
- );
239
- } else {
240
- resolve({ stdout: result, stderr, exitCode: 0, sessionId });
241
- }
242
- })().catch((err: unknown) => {
243
- reject(new SpawnError(`Unexpected error in close handler: ${String(err)}`, '', 1, null, p));
244
- });
245
- });
246
-
247
- child.on('error', (err) => {
248
- reject(new SpawnError(`Failed to spawn ${p.binary} CLI: ${err.message}`, '', 1, null, p));
249
- });
250
- });
251
- }
252
-
253
- const DEFAULT_MAX_RETRIES = 5;
254
- const BASE_DELAY_MS = 2000;
255
- const MAX_DELAY_MS = 120_000;
256
- const DEFAULT_TOTAL_TIMEOUT_MS = 600_000; // 10 minutes across all retries
257
-
258
- function sleep(ms: number): Promise<void> {
259
- return new Promise((resolve) => setTimeout(resolve, ms));
260
- }
261
-
262
- function jitter(): number {
263
- return Math.floor(Math.random() * 1000);
264
- }
265
-
266
- /**
267
- * Spawn AI CLI with automatic retry on rate limit errors.
268
- * Uses exponential backoff with jitter.
269
- *
270
- * On rate limit failures, automatically resumes the session using the
271
- * captured session ID so the AI picks up where it left off.
272
- */
273
- export async function spawnWithRetry(
274
- options: HeadlessSpawnOptions,
275
- retryOptions?: {
276
- maxRetries?: number;
277
- totalTimeoutMs?: number;
278
- onRetry?: (attempt: number, delayMs: number, error: SpawnError) => void;
279
- },
280
- provider?: ProviderAdapter
281
- ): Promise<SpawnResult> {
282
- const p = provider ?? (await getActiveProvider());
283
- const maxRetries = retryOptions?.maxRetries ?? DEFAULT_MAX_RETRIES;
284
- const totalTimeoutMs = retryOptions?.totalTimeoutMs ?? DEFAULT_TOTAL_TIMEOUT_MS;
285
- const startTime = Date.now();
286
- let resumeSessionId = options.resumeSessionId;
287
-
288
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
289
- // Check total elapsed time before each attempt
290
- const elapsed = Date.now() - startTime;
291
- if (attempt > 0 && elapsed >= totalTimeoutMs) {
292
- throw new SpawnError(`Total retry timeout exceeded (${String(totalTimeoutMs)}ms)`, '', 1, resumeSessionId, p);
293
- }
294
-
295
- try {
296
- return await spawnHeadlessRaw({ ...options, resumeSessionId }, p);
297
- } catch (err) {
298
- if (!(err instanceof SpawnError) || !err.rateLimited) {
299
- throw err;
300
- }
301
-
302
- // Capture session ID for resume on next attempt
303
- if (err.sessionId) {
304
- resumeSessionId = err.sessionId;
305
- }
306
-
307
- if (attempt >= maxRetries) {
308
- throw err;
309
- }
310
-
311
- const delay = Math.min(err.retryAfterMs ?? BASE_DELAY_MS * Math.pow(2, attempt), MAX_DELAY_MS) + jitter();
312
- retryOptions?.onRetry?.(attempt + 1, delay, err);
313
- await sleep(delay);
314
- }
315
- }
316
-
317
- // Unreachable, but satisfies TypeScript
318
- throw new Error('Max retries exceeded');
319
- }
@@ -1,270 +0,0 @@
1
- import { execSync } from 'node:child_process';
2
- import { writeFile } from 'node:fs/promises';
3
- import { join } from 'node:path';
4
- import { muted, warning } from '@src/theme/index.ts';
5
- import { checkTaskPermissions } from '@src/ai/permissions.ts';
6
- import { getProject, ProjectNotFoundError } from '@src/store/project.ts';
7
- import type { AiProvider, Project, Sprint, Task } from '@src/schemas/index.ts';
8
- import { assertSafeCwd } from '@src/utils/paths.ts';
9
-
10
- // ============================================================================
11
- // TYPES
12
- // ============================================================================
13
-
14
- export interface TaskContext {
15
- sprint: Sprint;
16
- task: Task;
17
- project?: Project;
18
- }
19
-
20
- /** Outcome of a check script for a single project path. */
21
- export type CheckStatus = { ran: true; script: string } | { ran: false; reason: 'no-script' };
22
-
23
- /** Map from projectPath → CheckStatus, populated by runCheckScripts. */
24
- export type CheckResults = Map<string, CheckStatus>;
25
-
26
- // ============================================================================
27
- // UTILITY FUNCTIONS
28
- // ============================================================================
29
-
30
- /**
31
- * Get recent git history for a project path.
32
- */
33
- export function getRecentGitHistory(projectPath: string, count = 20): string {
34
- try {
35
- assertSafeCwd(projectPath);
36
- const result = execSync(`git log -${String(count)} --oneline --no-decorate`, {
37
- cwd: projectPath,
38
- encoding: 'utf-8',
39
- stdio: ['pipe', 'pipe', 'pipe'],
40
- });
41
- return result.trim();
42
- } catch {
43
- return '(Unable to retrieve git history)';
44
- }
45
- }
46
-
47
- /**
48
- * Get check script from explicit repository config only.
49
- * Returns null if no check script is configured — no runtime auto-detection.
50
- * Heuristic detection is used only as suggestions during `project add`.
51
- */
52
- export function getEffectiveCheckScript(project: Project | undefined, projectPath: string): string | null {
53
- if (project) {
54
- const repo = project.repositories.find((r) => r.path === projectPath);
55
- if (repo?.checkScript) {
56
- return repo.checkScript;
57
- }
58
- }
59
- return null;
60
- }
61
-
62
- export function formatTask(ctx: TaskContext): string {
63
- const lines: string[] = [];
64
-
65
- // ═══ TASK DIRECTIVE (highest attention) ═══
66
- lines.push('## Task Directive');
67
- lines.push('');
68
- lines.push(`**Task:** ${ctx.task.name}`);
69
- lines.push(`**ID:** ${ctx.task.id}`);
70
- lines.push(`**Project:** ${ctx.task.projectPath}`);
71
- lines.push('');
72
- lines.push('**ONE TASK ONLY.** Complete THIS task and nothing else. Do not continue to other tasks.');
73
-
74
- if (ctx.task.description) {
75
- lines.push('');
76
- lines.push(ctx.task.description);
77
- }
78
-
79
- // ═══ TASK STEPS (primary content — positioned first for maximum attention) ═══
80
- if (ctx.task.steps.length > 0) {
81
- lines.push('');
82
- lines.push('## Implementation Steps');
83
- lines.push('');
84
- lines.push('Follow these steps precisely and in order:');
85
- lines.push('');
86
- ctx.task.steps.forEach((step, i) => {
87
- lines.push(`${String(i + 1)}. ${step}`);
88
- });
89
- }
90
-
91
- return lines.join('\n');
92
- }
93
-
94
- /**
95
- * Build the full task context with primacy/recency optimization.
96
- *
97
- * Layout applies the primacy/recency effect:
98
- * - HIGH ATTENTION (start): Task directive, steps, check script
99
- * - REFERENCE (middle): Prior learnings, ticket requirements, git history
100
- * - HIGH ATTENTION (end): Instructions (appended by writeTaskContextFile)
101
- */
102
- export function buildFullTaskContext(
103
- ctx: TaskContext,
104
- progressSummary: string | null,
105
- gitHistory: string,
106
- checkScript: string | null,
107
- checkStatus?: CheckStatus
108
- ): string {
109
- const lines: string[] = [];
110
-
111
- // ═══ HIGH ATTENTION ZONE (beginning) ═══
112
-
113
- lines.push(formatTask(ctx));
114
-
115
- // Branch awareness — tell the agent which branch it's on
116
- if (ctx.sprint.branch) {
117
- lines.push('');
118
- lines.push('## Branch');
119
- lines.push('');
120
- lines.push(
121
- `You are working on branch \`${ctx.sprint.branch}\`. All commits go to this branch. Do not switch branches.`
122
- );
123
- }
124
-
125
- // Check script — near the top so it's easy to find
126
- lines.push('');
127
- lines.push('## Check Script');
128
- lines.push('');
129
- if (checkScript) {
130
- lines.push('The harness runs this command at sprint start and after every task as a post-task gate:');
131
- lines.push('');
132
- lines.push('```bash');
133
- lines.push(checkScript);
134
- lines.push('```');
135
- lines.push('');
136
- lines.push('Your task is NOT marked done unless this command passes after completion.');
137
- } else {
138
- lines.push('No check script is configured. Read CLAUDE.md in the project root to find verification commands.');
139
- }
140
-
141
- // Check status awareness — tell the agent what happened during stage zero
142
- if (checkStatus) {
143
- lines.push('');
144
- lines.push('## Environment Status');
145
- lines.push('');
146
- if (checkStatus.ran) {
147
- lines.push('The check script ran successfully at sprint start. Dependencies are current.');
148
- lines.push('Do not re-run the install portion unless you encounter dependency errors.');
149
- } else {
150
- lines.push(
151
- 'No check script is configured for this repository. ' +
152
- 'Read CLAUDE.md or project configuration files (package.json, pyproject.toml, etc.) ' +
153
- 'to discover build, test, and lint commands.'
154
- );
155
- }
156
- }
157
-
158
- // ═══ REFERENCE ZONE (middle — lower attention is OK) ═══
159
-
160
- lines.push('');
161
- lines.push('---');
162
- lines.push('');
163
-
164
- // Prior task learnings (summarized, not raw progress dump)
165
- if (progressSummary) {
166
- lines.push('## Prior Task Learnings');
167
- lines.push('');
168
- lines.push('_Reference — consult when relevant to your implementation._');
169
- lines.push('');
170
- lines.push(progressSummary);
171
- lines.push('');
172
- }
173
-
174
- // Ticket requirements (reference only, explicitly deprioritized)
175
- if (ctx.task.ticketId) {
176
- const ticket = ctx.sprint.tickets.find((t) => t.id === ctx.task.ticketId);
177
- if (ticket?.requirements) {
178
- lines.push('## Ticket Requirements');
179
- lines.push('');
180
- lines.push(
181
- '_Reference — these describe the full ticket scope. This task implements a specific part. ' +
182
- 'Use to validate your work and understand constraints, but follow the Implementation Steps above. ' +
183
- 'Do not expand scope beyond declared steps._'
184
- );
185
- lines.push('');
186
- lines.push(ticket.requirements);
187
- lines.push('');
188
- }
189
- }
190
-
191
- // Git history — awareness of recent changes
192
- lines.push('## Git History (recent commits)');
193
- lines.push('');
194
- lines.push('```');
195
- lines.push(gitHistory);
196
- lines.push('```');
197
-
198
- // ═══ HIGH ATTENTION ZONE (end) — Instructions appended by writeTaskContextFile ═══
199
-
200
- return lines.join('\n');
201
- }
202
-
203
- export function getContextFileName(sprintId: string, taskId: string): string {
204
- return `.ralphctl-sprint-${sprintId}-task-${taskId}-context.md`;
205
- }
206
-
207
- export async function writeTaskContextFile(
208
- projectPath: string,
209
- taskContent: string,
210
- instructions: string,
211
- sprintId: string,
212
- taskId: string
213
- ): Promise<string> {
214
- const contextFile = join(projectPath, getContextFileName(sprintId, taskId));
215
- const warning = `<!-- TEMPORARY FILE - DO NOT COMMIT -->
216
- <!-- This file is auto-generated by ralphctl for task execution context -->
217
- <!-- It will be automatically cleaned up after task completion -->
218
-
219
- `;
220
- const fullContent = `${warning}${taskContent}\n\n---\n\n## Instructions\n\n${instructions}`;
221
- await writeFile(contextFile, fullContent, { encoding: 'utf-8', mode: 0o600 });
222
- return contextFile;
223
- }
224
-
225
- /**
226
- * Try to get the project for a task (via ticket reference).
227
- */
228
- export async function getProjectForTask(task: Task, sprint: Sprint): Promise<Project | undefined> {
229
- if (!task.ticketId) return undefined;
230
-
231
- const ticket = sprint.tickets.find((t) => t.id === task.ticketId);
232
- if (!ticket) return undefined;
233
-
234
- try {
235
- return await getProject(ticket.projectName);
236
- } catch (err) {
237
- if (err instanceof ProjectNotFoundError) {
238
- return undefined;
239
- }
240
- throw err;
241
- }
242
- }
243
-
244
- // ============================================================================
245
- // PERMISSION CHECKS
246
- // ============================================================================
247
-
248
- /**
249
- * Run permission checks and display any warnings.
250
- *
251
- * For Claude: warns about operations that may need approval in settings files.
252
- * For Copilot: no-op — all tools are granted via --allow-all-tools.
253
- */
254
- export function runPermissionCheck(ctx: TaskContext, noCommit: boolean, provider?: AiProvider): void {
255
- const checkScript = getEffectiveCheckScript(ctx.project, ctx.task.projectPath);
256
-
257
- const warnings = checkTaskPermissions(ctx.task.projectPath, {
258
- checkScript,
259
- needsCommit: !noCommit,
260
- provider,
261
- });
262
-
263
- if (warnings.length > 0) {
264
- console.log(warning('\n Permission warnings:'));
265
- for (const w of warnings) {
266
- console.log(muted(` - ${w.message}`));
267
- }
268
- console.log(muted(' Consider adjusting tool permissions for your AI provider\n'));
269
- }
270
- }
@@ -1,7 +0,0 @@
1
- import pkg from '../package.json' with { type: 'json' };
2
-
3
- export const cliMetadata = {
4
- name: 'ralphctl',
5
- version: pkg.version,
6
- description: "I'm helping! Plan sprints and execute tasks with AI",
7
- } as const;
package/src/cli.ts DELETED
@@ -1,65 +0,0 @@
1
- import { Command } from 'commander';
2
- import { showBanner } from '@src/theme/ui.ts';
3
- import { interactiveMode } from '@src/interactive/index.ts';
4
- import { registerProjectCommands } from '@src/commands/project/index.ts';
5
- import { registerSprintCommands } from '@src/commands/sprint/index.ts';
6
- import { registerTaskCommands } from '@src/commands/task/index.ts';
7
- import { registerTicketCommands } from '@src/commands/ticket/index.ts';
8
- import { registerProgressCommands } from '@src/commands/progress/index.ts';
9
- import { registerDashboardCommands } from '@src/commands/dashboard/index.ts';
10
- import { registerConfigCommands } from '@src/commands/config/index.ts';
11
- import { registerCompletionCommands } from '@src/commands/completion/index.ts';
12
- import { registerDoctorCommands } from '@src/commands/doctor/index.ts';
13
- import { error } from '@src/theme/index.ts';
14
- import { cliMetadata } from '@src/cli-metadata.ts';
15
-
16
- const program = new Command();
17
- program
18
- .name(cliMetadata.name)
19
- .description(cliMetadata.description)
20
- .version(cliMetadata.version)
21
- .addHelpText(
22
- 'after',
23
- `
24
- Examples:
25
- $ ralphctl # Interactive mode
26
- $ ralphctl status # Show current sprint status
27
- $ ralphctl sprint create --name "v1.0" # Create sprint
28
- $ ralphctl ticket add --project api # Add ticket
29
- $ ralphctl task list -b # Brief task list
30
-
31
- Run any command with --help for details.
32
- `
33
- );
34
-
35
- registerProjectCommands(program);
36
- registerSprintCommands(program);
37
- registerTaskCommands(program);
38
- registerTicketCommands(program);
39
- registerProgressCommands(program);
40
- registerDashboardCommands(program);
41
- registerConfigCommands(program);
42
- registerCompletionCommands(program);
43
- registerDoctorCommands(program);
44
-
45
- async function main(): Promise<void> {
46
- // Shell completion: intercept before any output (banner, interactive mode)
47
- if (process.env['COMP_CWORD'] && process.env['COMP_POINT'] && process.env['COMP_LINE']) {
48
- const { handleCompletionRequest } = await import('@src/completion/handle.ts');
49
- if (await handleCompletionRequest(program)) return;
50
- }
51
-
52
- // No args or 'interactive' → interactive mode
53
- if (process.argv.length <= 2 || process.argv[2] === 'interactive') {
54
- // Interactive mode shows its own banner
55
- await interactiveMode();
56
- } else {
57
- showBanner();
58
- await program.parseAsync(process.argv);
59
- }
60
- }
61
-
62
- main().catch((err: unknown) => {
63
- console.error(error('Fatal error:'), err);
64
- process.exit(1);
65
- });
@@ -1,33 +0,0 @@
1
- import type { Command } from 'commander';
2
- import { showSuccess } from '@src/theme/ui.ts';
3
-
4
- export function registerCompletionCommands(program: Command): void {
5
- const completion = program.command('completion').description('Manage shell tab-completion');
6
-
7
- completion.addHelpText(
8
- 'after',
9
- `
10
- Examples:
11
- $ ralphctl completion install # Enable tab-completion for your shell
12
- $ ralphctl completion uninstall # Remove tab-completion
13
- `
14
- );
15
-
16
- completion
17
- .command('install')
18
- .description('Install shell tab-completion (bash, zsh, fish)')
19
- .action(async () => {
20
- const tabtab = (await import('tabtab')).default;
21
- await tabtab.install({ name: 'ralphctl', completer: 'ralphctl' });
22
- showSuccess('Shell completion installed. Restart your shell or source your profile to activate.');
23
- });
24
-
25
- completion
26
- .command('uninstall')
27
- .description('Remove shell tab-completion')
28
- .action(async () => {
29
- const tabtab = (await import('tabtab')).default;
30
- await tabtab.uninstall({ name: 'ralphctl' });
31
- showSuccess('Shell completion removed.');
32
- });
33
- }