@towles/tool 0.0.41 → 0.0.49

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 (53) hide show
  1. package/README.md +67 -109
  2. package/package.json +51 -41
  3. package/src/commands/base.ts +3 -18
  4. package/src/commands/config.ts +9 -8
  5. package/src/commands/doctor.ts +4 -1
  6. package/src/commands/gh/branch-clean.ts +10 -4
  7. package/src/commands/gh/branch.ts +6 -3
  8. package/src/commands/gh/pr.ts +10 -3
  9. package/src/commands/graph-template.html +1214 -0
  10. package/src/commands/graph.test.ts +176 -0
  11. package/src/commands/graph.ts +970 -0
  12. package/src/commands/install.ts +8 -2
  13. package/src/commands/journal/daily-notes.ts +9 -5
  14. package/src/commands/journal/meeting.ts +12 -6
  15. package/src/commands/journal/note.ts +12 -6
  16. package/src/commands/ralph/plan/add.ts +75 -0
  17. package/src/commands/ralph/plan/done.ts +82 -0
  18. package/src/commands/ralph/{task → plan}/list.test.ts +5 -5
  19. package/src/commands/ralph/{task → plan}/list.ts +28 -39
  20. package/src/commands/ralph/plan/remove.ts +71 -0
  21. package/src/commands/ralph/run.test.ts +521 -0
  22. package/src/commands/ralph/run.ts +126 -189
  23. package/src/commands/ralph/show.ts +88 -0
  24. package/src/config/settings.ts +8 -27
  25. package/src/{commands/ralph/lib → lib/ralph}/execution.ts +4 -14
  26. package/src/lib/ralph/formatter.ts +238 -0
  27. package/src/{commands/ralph/lib → lib/ralph}/state.ts +17 -42
  28. package/src/utils/date-utils.test.ts +2 -1
  29. package/src/utils/date-utils.ts +2 -2
  30. package/LICENSE.md +0 -20
  31. package/src/commands/index.ts +0 -55
  32. package/src/commands/observe/graph.test.ts +0 -89
  33. package/src/commands/observe/graph.ts +0 -1640
  34. package/src/commands/observe/report.ts +0 -166
  35. package/src/commands/observe/session.ts +0 -385
  36. package/src/commands/observe/setup.ts +0 -180
  37. package/src/commands/observe/status.ts +0 -146
  38. package/src/commands/ralph/lib/formatter.ts +0 -298
  39. package/src/commands/ralph/lib/marker.ts +0 -108
  40. package/src/commands/ralph/marker/create.ts +0 -23
  41. package/src/commands/ralph/plan.ts +0 -73
  42. package/src/commands/ralph/progress.ts +0 -44
  43. package/src/commands/ralph/ralph.test.ts +0 -673
  44. package/src/commands/ralph/task/add.ts +0 -105
  45. package/src/commands/ralph/task/done.ts +0 -73
  46. package/src/commands/ralph/task/remove.ts +0 -62
  47. package/src/config/context.ts +0 -7
  48. package/src/constants.ts +0 -3
  49. package/src/utils/anthropic/types.ts +0 -158
  50. package/src/utils/exec.ts +0 -8
  51. package/src/utils/git/git.ts +0 -25
  52. /package/src/{commands → lib}/journal/utils.ts +0 -0
  53. /package/src/{commands/ralph/lib → lib/ralph}/index.ts +0 -0
@@ -1,180 +0,0 @@
1
- import * as fs from "node:fs";
2
- import * as path from "node:path";
3
- import { homedir } from "node:os";
4
- import pc from "picocolors";
5
- import consola from "consola";
6
- import { BaseCommand } from "../base.js";
7
-
8
- const CLAUDE_DIR = path.join(homedir(), ".claude");
9
- const CLAUDE_SETTINGS_PATH = path.join(CLAUDE_DIR, "settings.json");
10
- const REPORTS_DIR = path.join(CLAUDE_DIR, "reports");
11
-
12
- interface ClaudeSettings {
13
- cleanupPeriodDays?: number;
14
- alwaysThinkingEnabled?: boolean;
15
- env?: Record<string, string>;
16
- hooks?: {
17
- SubagentStop?: Array<{
18
- matcher?: Record<string, unknown>;
19
- hooks?: Array<{
20
- type: string;
21
- command: string;
22
- }>;
23
- }>;
24
- [key: string]: unknown;
25
- };
26
- [key: string]: unknown;
27
- }
28
-
29
- const OTEL_ENV_VARS: Record<string, string> = {
30
- CLAUDE_CODE_ENABLE_TELEMETRY: "1",
31
- OTEL_METRICS_EXPORTER: "otlp",
32
- OTEL_LOGS_EXPORTER: "otlp",
33
- OTEL_EXPORTER_OTLP_ENDPOINT: "http://localhost:4317",
34
- };
35
-
36
- /**
37
- * Configure observability settings for Claude Code
38
- */
39
- export default class ObserveSetup extends BaseCommand {
40
- static override description = "Configure Claude Code observability settings";
41
-
42
- static override examples = [
43
- "<%= config.bin %> observe setup",
44
- "<%= config.bin %> observe setup # Adds SubagentStop hook for lineage tracking",
45
- ];
46
-
47
- async run(): Promise<void> {
48
- await this.parse(ObserveSetup);
49
-
50
- this.log(pc.bold("\n📊 Claude Code Observability Setup\n"));
51
-
52
- // Load or create Claude settings
53
- let claudeSettings: ClaudeSettings = {};
54
- if (fs.existsSync(CLAUDE_SETTINGS_PATH)) {
55
- try {
56
- const content = fs.readFileSync(CLAUDE_SETTINGS_PATH, "utf-8");
57
- claudeSettings = JSON.parse(content);
58
- this.log(pc.dim(`Found existing Claude settings at ${CLAUDE_SETTINGS_PATH}`));
59
- } catch {
60
- this.log(
61
- pc.yellow(`Warning: Could not parse ${CLAUDE_SETTINGS_PATH}, will create fresh settings`),
62
- );
63
- }
64
- } else {
65
- this.log(pc.dim(`No Claude settings file found, will create one`));
66
- }
67
-
68
- let modified = false;
69
-
70
- // 1. Ensure cleanupPeriodDays is set to prevent log deletion
71
- if (claudeSettings.cleanupPeriodDays !== 99999) {
72
- claudeSettings.cleanupPeriodDays = 99999;
73
- modified = true;
74
- this.log(pc.green("✓ Set cleanupPeriodDays: 99999 (prevent log deletion)"));
75
- } else {
76
- this.log(pc.dim("✓ cleanupPeriodDays already set to 99999"));
77
- }
78
-
79
- // 2. Configure SubagentStop hook for lineage tracking
80
- const subagentLogPath = path.join(REPORTS_DIR, "subagent-log.jsonl");
81
- const subagentHookCommand = `jq -c '. + {parent: env.SESSION_ID, timestamp: now}' >> ${subagentLogPath}`;
82
-
83
- if (!claudeSettings.hooks) {
84
- claudeSettings.hooks = {};
85
- }
86
-
87
- const existingSubagentHook = claudeSettings.hooks.SubagentStop;
88
- const hasSubagentHook =
89
- existingSubagentHook &&
90
- Array.isArray(existingSubagentHook) &&
91
- existingSubagentHook.length > 0;
92
-
93
- if (!hasSubagentHook) {
94
- claudeSettings.hooks.SubagentStop = [
95
- {
96
- hooks: [
97
- {
98
- type: "command",
99
- command: subagentHookCommand,
100
- },
101
- ],
102
- },
103
- ];
104
- modified = true;
105
- this.log(pc.green("✓ Added SubagentStop hook for subagent lineage tracking"));
106
- } else {
107
- this.log(pc.dim("✓ SubagentStop hook already configured"));
108
- }
109
-
110
- // 3. Add OTEL environment variables to settings
111
- if (!claudeSettings.env) {
112
- claudeSettings.env = {};
113
- }
114
-
115
- const addedVars: string[] = [];
116
- const skippedVars: string[] = [];
117
- for (const [key, value] of Object.entries(OTEL_ENV_VARS)) {
118
- if (claudeSettings.env[key] === undefined) {
119
- claudeSettings.env[key] = value;
120
- addedVars.push(key);
121
- modified = true;
122
- } else {
123
- skippedVars.push(key);
124
- }
125
- }
126
-
127
- if (addedVars.length > 0) {
128
- this.log(pc.green(`✓ Added env vars: ${addedVars.join(", ")}`));
129
- }
130
- if (skippedVars.length > 0) {
131
- this.log(pc.dim(`✓ Env vars already set: ${skippedVars.join(", ")}`));
132
- }
133
-
134
- // Save settings if modified
135
- if (modified) {
136
- this.saveClaudeSettings(claudeSettings);
137
- this.log(pc.green(`\n✓ Saved settings to ${CLAUDE_SETTINGS_PATH}`));
138
- }
139
-
140
- // 4. Create reports directory
141
- if (!fs.existsSync(REPORTS_DIR)) {
142
- fs.mkdirSync(REPORTS_DIR, { recursive: true });
143
- this.log(pc.green(`✓ Created reports directory at ${REPORTS_DIR}`));
144
- } else {
145
- this.log(pc.dim(`✓ Reports directory exists at ${REPORTS_DIR}`));
146
- }
147
-
148
- // 5. Show OTEL environment variables setup
149
- this.log(pc.bold("\n🔧 OTEL Environment Variables\n"));
150
- this.log(pc.cyan("Add these to your shell profile (~/.bashrc, ~/.zshrc, etc.):\n"));
151
-
152
- consola.box(`export CLAUDE_CODE_ENABLE_TELEMETRY=1
153
- export OTEL_METRICS_EXPORTER=otlp
154
- export OTEL_LOGS_EXPORTER=otlp
155
- export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317`);
156
-
157
- this.log("");
158
- this.log(pc.dim("For a full monitoring stack, see:"));
159
- this.log(pc.dim(" https://github.com/anthropics/claude-code-monitoring-guide"));
160
- this.log("");
161
-
162
- // Quick usage tips
163
- this.log(pc.bold("📈 Quick Analysis Commands\n"));
164
- this.log(pc.dim(" tt observe status # Check current config"));
165
- this.log(pc.dim(" tt observe report # Token/cost breakdown"));
166
- this.log(pc.dim(" tt observe session # List sessions"));
167
- this.log(pc.dim(" tt observe graph # Visualize token usage"));
168
- this.log("");
169
-
170
- this.log(pc.bold(pc.green("✅ Observability setup complete!\n")));
171
- }
172
-
173
- private saveClaudeSettings(settings: ClaudeSettings): void {
174
- const dir = path.dirname(CLAUDE_SETTINGS_PATH);
175
- if (!fs.existsSync(dir)) {
176
- fs.mkdirSync(dir, { recursive: true });
177
- }
178
- fs.writeFileSync(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2));
179
- }
180
- }
@@ -1,146 +0,0 @@
1
- import * as fs from "node:fs";
2
- import * as path from "node:path";
3
- import { homedir } from "node:os";
4
- import pc from "picocolors";
5
- import { BaseCommand } from "../base.js";
6
-
7
- const CLAUDE_DIR = path.join(homedir(), ".claude");
8
- const CLAUDE_SETTINGS_PATH = path.join(CLAUDE_DIR, "settings.json");
9
- const REPORTS_DIR = path.join(CLAUDE_DIR, "reports");
10
-
11
- interface ClaudeSettings {
12
- cleanupPeriodDays?: number;
13
- alwaysThinkingEnabled?: boolean;
14
- hooks?: {
15
- SubagentStop?: unknown[];
16
- PreToolUse?: unknown[];
17
- PostToolUse?: unknown[];
18
- Stop?: unknown[];
19
- [key: string]: unknown;
20
- };
21
- [key: string]: unknown;
22
- }
23
-
24
- /**
25
- * Display current observability configuration status
26
- */
27
- export default class ObserveStatus extends BaseCommand {
28
- static override description = "Display current observability configuration status";
29
-
30
- static override examples = [
31
- "<%= config.bin %> observe status",
32
- "<%= config.bin %> observe status # Check if observability is properly configured",
33
- ];
34
-
35
- async run(): Promise<void> {
36
- await this.parse(ObserveStatus);
37
-
38
- this.log(pc.bold("\n📊 Observability Status\n"));
39
-
40
- // Load Claude settings
41
- let settings: ClaudeSettings = {};
42
- if (fs.existsSync(CLAUDE_SETTINGS_PATH)) {
43
- try {
44
- const content = fs.readFileSync(CLAUDE_SETTINGS_PATH, "utf-8");
45
- settings = JSON.parse(content);
46
- } catch {
47
- this.log(pc.red(`✗ Could not parse ${CLAUDE_SETTINGS_PATH}`));
48
- }
49
- } else {
50
- this.log(pc.yellow(`⚠ No settings file at ${CLAUDE_SETTINGS_PATH}`));
51
- this.log(pc.dim(" Run: tt observe setup"));
52
- this.log("");
53
- }
54
-
55
- // 1. Claude Settings
56
- this.log(pc.bold("Claude Settings"));
57
- this.log(pc.dim(` Path: ${CLAUDE_SETTINGS_PATH}\n`));
58
-
59
- // cleanupPeriodDays
60
- const cleanup = settings.cleanupPeriodDays;
61
- if (cleanup === 99999) {
62
- this.log(pc.green(" ✓ cleanupPeriodDays: 99999 (logs preserved)"));
63
- } else if (cleanup !== undefined) {
64
- this.log(pc.yellow(` ⚠ cleanupPeriodDays: ${cleanup} (logs may be deleted)`));
65
- } else {
66
- this.log(pc.red(" ✗ cleanupPeriodDays: not set (default cleanup applies)"));
67
- }
68
-
69
- // alwaysThinkingEnabled
70
- if (settings.alwaysThinkingEnabled) {
71
- this.log(pc.green(" ✓ alwaysThinkingEnabled: true"));
72
- } else {
73
- this.log(pc.dim(" ○ alwaysThinkingEnabled: false"));
74
- }
75
-
76
- this.log("");
77
-
78
- // 2. Hooks
79
- this.log(pc.bold("Hooks Configured"));
80
- const hooks = settings.hooks || {};
81
- const hookNames = ["SubagentStop", "PreToolUse", "PostToolUse", "Stop"];
82
- let hasAnyHook = false;
83
-
84
- for (const name of hookNames) {
85
- const hook = hooks[name];
86
- if (hook && Array.isArray(hook) && hook.length > 0) {
87
- this.log(pc.green(` ✓ ${name}: ${hook.length} handler(s)`));
88
- hasAnyHook = true;
89
- }
90
- }
91
-
92
- // Check for other hooks
93
- const otherHooks = Object.keys(hooks).filter((k) => !hookNames.includes(k));
94
- for (const name of otherHooks) {
95
- const hook = hooks[name];
96
- if (hook && Array.isArray(hook) && hook.length > 0) {
97
- this.log(pc.green(` ✓ ${name}: ${(hook as unknown[]).length} handler(s)`));
98
- hasAnyHook = true;
99
- }
100
- }
101
-
102
- if (!hasAnyHook) {
103
- this.log(pc.dim(" ○ No hooks configured"));
104
- }
105
-
106
- this.log("");
107
-
108
- // 3. Reports Directory
109
- this.log(pc.bold("Reports Directory"));
110
- if (fs.existsSync(REPORTS_DIR)) {
111
- const files = fs.readdirSync(REPORTS_DIR);
112
- this.log(pc.green(` ✓ ${REPORTS_DIR}`));
113
- this.log(pc.dim(` ${files.length} file(s)`));
114
- } else {
115
- this.log(pc.yellow(` ⚠ ${REPORTS_DIR} does not exist`));
116
- this.log(pc.dim(" Run: tt observe setup"));
117
- }
118
-
119
- this.log("");
120
-
121
- // 4. OTEL Environment Variables
122
- this.log(pc.bold("OTEL Environment Variables"));
123
-
124
- const otelVars = [
125
- { name: "CLAUDE_CODE_ENABLE_TELEMETRY", expected: "1" },
126
- { name: "OTEL_METRICS_EXPORTER", expected: "otlp" },
127
- { name: "OTEL_LOGS_EXPORTER", expected: "otlp" },
128
- { name: "OTEL_EXPORTER_OTLP_ENDPOINT", expected: undefined },
129
- ];
130
-
131
- for (const { name, expected } of otelVars) {
132
- const value = process.env[name];
133
- if (value) {
134
- if (expected && value !== expected) {
135
- this.log(pc.yellow(` ⚠ ${name}=${value} (expected: ${expected})`));
136
- } else {
137
- this.log(pc.green(` ✓ ${name}=${value}`));
138
- }
139
- } else {
140
- this.log(pc.dim(` ○ ${name}: not set`));
141
- }
142
- }
143
-
144
- this.log("");
145
- }
146
- }
@@ -1,298 +0,0 @@
1
- import { execFileSync } from "node:child_process";
2
- import type { RalphTask, TaskStatus, RalphState } from "./state.js";
3
-
4
- // ============================================================================
5
- // Clipboard Utility
6
- // ============================================================================
7
-
8
- export function copyToClipboard(text: string): boolean {
9
- try {
10
- const platform = process.platform;
11
- if (platform === "darwin") {
12
- execFileSync("pbcopy", [], { input: text });
13
- } else if (platform === "linux") {
14
- // Try xclip first, then xsel
15
- try {
16
- execFileSync("xclip", ["-selection", "clipboard"], { input: text });
17
- } catch {
18
- execFileSync("xsel", ["--clipboard", "--input"], { input: text });
19
- }
20
- } else if (platform === "win32") {
21
- execFileSync("clip", [], { input: text });
22
- } else {
23
- return false;
24
- }
25
- return true;
26
- } catch {
27
- return false;
28
- }
29
- }
30
-
31
- // ============================================================================
32
- // Task Formatting
33
- // ============================================================================
34
-
35
- export function formatTasksForPrompt(tasks: RalphTask[]): string {
36
- if (tasks.length === 0) {
37
- return "No tasks.";
38
- }
39
-
40
- const statusIcon = (status: TaskStatus): string => {
41
- switch (status) {
42
- case "done":
43
- return "✓";
44
- case "ready":
45
- return "○";
46
- case "blocked":
47
- return "⏸";
48
- case "cancelled":
49
- return "✗";
50
- }
51
- };
52
-
53
- const lines: string[] = [];
54
- for (const t of tasks) {
55
- const checkbox = t.status === "done" ? "[x]" : "[ ]";
56
- lines.push(`- ${checkbox} #${t.id} ${t.description} \`${statusIcon(t.status)} ${t.status}\``);
57
- }
58
-
59
- return lines.join("\n");
60
- }
61
-
62
- /**
63
- * Format tasks as markdown with checkboxes and status badges.
64
- */
65
- export function formatTasksAsMarkdown(tasks: RalphTask[]): string {
66
- if (tasks.length === 0) {
67
- return "# Tasks\n\nNo tasks.\n";
68
- }
69
-
70
- const statusBadge = (status: TaskStatus): string => {
71
- switch (status) {
72
- case "done":
73
- return "`✓ done`";
74
- case "ready":
75
- return "`○ ready`";
76
- case "blocked":
77
- return "`⏸ blocked`";
78
- case "cancelled":
79
- return "`✗ cancelled`";
80
- }
81
- };
82
-
83
- const ready = tasks.filter((t) => t.status === "ready");
84
- const done = tasks.filter((t) => t.status === "done");
85
-
86
- const lines: string[] = ["# Tasks", ""];
87
- lines.push(
88
- `**Total:** ${tasks.length} | **Done:** ${done.length} | **Ready:** ${ready.length}`,
89
- "",
90
- );
91
-
92
- if (ready.length > 0) {
93
- lines.push("## Ready", "");
94
- for (const t of ready) {
95
- lines.push(`- [ ] **#${t.id}** ${t.description} ${statusBadge(t.status)}`);
96
- }
97
- lines.push("");
98
- }
99
-
100
- if (done.length > 0) {
101
- lines.push("## Done", "");
102
- for (const t of done) {
103
- lines.push(`- [x] **#${t.id}** ${t.description} ${statusBadge(t.status)}`);
104
- }
105
- lines.push("");
106
- }
107
-
108
- return lines.join("\n");
109
- }
110
-
111
- /**
112
- * Format tasks as a plan with markdown and optional mermaid graph.
113
- */
114
- export function formatPlanAsMarkdown(tasks: RalphTask[], state: RalphState): string {
115
- const lines: string[] = ["# Ralph Plan", ""];
116
-
117
- // Summary section
118
- const ready = tasks.filter((t) => t.status === "ready").length;
119
- const done = tasks.filter((t) => t.status === "done").length;
120
-
121
- lines.push("## Summary", "");
122
- lines.push(`- **Status:** ${state.status}`);
123
- lines.push(`- **Iteration:** ${state.iteration}/${state.maxIterations}`);
124
- lines.push(`- **Total Tasks:** ${tasks.length}`);
125
- lines.push(`- **Done:** ${done} | **Ready:** ${ready}`);
126
- if (state.sessionId) {
127
- lines.push(`- **Session ID:** ${state.sessionId.slice(0, 8)}...`);
128
- }
129
- lines.push("");
130
-
131
- // Tasks section with checkboxes
132
- lines.push("## Tasks", "");
133
- for (const t of tasks) {
134
- const checkbox = t.status === "done" ? "[x]" : "[ ]";
135
- const status = t.status === "done" ? "`done`" : "`ready`";
136
- lines.push(`- ${checkbox} **#${t.id}** ${t.description} ${status}`);
137
- }
138
- lines.push("");
139
-
140
- // Mermaid graph section
141
- lines.push("## Progress Graph", "");
142
- lines.push("```mermaid");
143
- lines.push("graph LR");
144
- lines.push(` subgraph Progress["Tasks: ${done}/${tasks.length} done"]`);
145
-
146
- for (const t of tasks) {
147
- const shortDesc =
148
- t.description.length > 30 ? t.description.slice(0, 27) + "..." : t.description;
149
- // Escape quotes in descriptions
150
- const safeDesc = shortDesc.replace(/"/g, "'");
151
- const nodeId = `T${t.id}`;
152
-
153
- if (t.status === "done") {
154
- lines.push(` ${nodeId}["#${t.id}: ${safeDesc}"]:::done`);
155
- } else {
156
- lines.push(` ${nodeId}["#${t.id}: ${safeDesc}"]:::ready`);
157
- }
158
- }
159
-
160
- lines.push(" end");
161
- lines.push(" classDef done fill:#22c55e,color:#fff");
162
- lines.push(" classDef ready fill:#94a3b8,color:#000");
163
- lines.push("```");
164
- lines.push("");
165
-
166
- return lines.join("\n");
167
- }
168
-
169
- /**
170
- * Format tasks as JSON for programmatic consumption.
171
- */
172
- export function formatPlanAsJson(tasks: RalphTask[], state: RalphState): string {
173
- return JSON.stringify(
174
- {
175
- status: state.status,
176
- iteration: state.iteration,
177
- maxIterations: state.maxIterations,
178
- sessionId: state.sessionId,
179
- summary: {
180
- total: tasks.length,
181
- done: tasks.filter((t) => t.status === "done").length,
182
- ready: tasks.filter((t) => t.status === "ready").length,
183
- },
184
- tasks: tasks.map((t) => ({
185
- id: t.id,
186
- description: t.description,
187
- status: t.status,
188
- addedAt: t.addedAt,
189
- completedAt: t.completedAt,
190
- })),
191
- },
192
- null,
193
- 2,
194
- );
195
- }
196
-
197
- // ============================================================================
198
- // Duration Formatting
199
- // ============================================================================
200
-
201
- export function formatDuration(ms: number): string {
202
- const seconds = Math.floor(ms / 1000);
203
- const minutes = Math.floor(seconds / 60);
204
- const hours = Math.floor(minutes / 60);
205
-
206
- if (hours > 0) {
207
- const remainingMins = minutes % 60;
208
- return `${hours}h ${remainingMins}m`;
209
- }
210
- if (minutes > 0) {
211
- const remainingSecs = seconds % 60;
212
- return `${minutes}m ${remainingSecs}s`;
213
- }
214
- return `${seconds}s`;
215
- }
216
-
217
- // ============================================================================
218
- // Output Summary
219
- // ============================================================================
220
-
221
- export function extractOutputSummary(output: string, maxLength: number = 2000): string {
222
- const lines = output
223
- .split("\n")
224
- .filter((l) => l.trim())
225
- .slice(-5);
226
- let summary = lines.join(" ").trim();
227
-
228
- if (summary.length > maxLength) {
229
- summary = summary.substring(0, maxLength) + "...";
230
- }
231
-
232
- return summary || "(no output)";
233
- }
234
-
235
- // ============================================================================
236
- // Prompt Building
237
- // ============================================================================
238
-
239
- export interface BuildPromptOptions {
240
- completionMarker: string;
241
- progressFile: string;
242
- focusedTaskId: number | null;
243
- skipCommit?: boolean;
244
- progressContent?: string;
245
- taskList: string;
246
- }
247
-
248
- export function buildIterationPrompt({
249
- completionMarker,
250
- progressFile,
251
- focusedTaskId,
252
- skipCommit = false,
253
- progressContent,
254
- taskList,
255
- }: BuildPromptOptions): string {
256
- // prompt inspired by https://www.aihero.dev/tips-for-ai-coding-with-ralph-wiggum#2-start-with-hitl-then-go-afk
257
-
258
- let step = 1;
259
-
260
- //IMPORTANT Always tell it to APPEND to progress file, save a lot of tokens by not reading it to update.
261
-
262
- const prompt = `
263
- <input-current-tasks>
264
- ${taskList}
265
- </input-current-tasks>
266
-
267
- <instructions>
268
- ${step++}. ${
269
- focusedTaskId
270
- ? `**Work on Task #${focusedTaskId}** (you've been asked to focus on this one).`
271
- : `**Choose** which ready task to work on next based on YOUR judgment of priority/dependencies.`
272
- }
273
- ${step++}. Work on that single task.
274
- ${step++}. Run type checks and tests.
275
- ${step++}. Mark the task done using CLI: \`tt ralph task done <id>\`
276
- ${step++}. Append to @${progressFile} with what you did.
277
- ${skipCommit ? "" : `${step++}. Make a git commit.`}
278
-
279
- **ONE TASK PER ITERATION**
280
-
281
- **Before ending:** Run \`tt ralph task list\` to check remaining tasks.
282
- **ONLY if ALL TASKS are done** then Output: <promise>${completionMarker}</promise>
283
- </instructions>
284
-
285
- <prior-context note="Reference only - these tasks are already completed, do not work on them">
286
- ${progressContent || "(No prior progress)"}
287
- </prior-context>
288
- `;
289
- return prompt.trim();
290
- }
291
-
292
- // ============================================================================
293
- // Marker Detection
294
- // ============================================================================
295
-
296
- export function detectCompletionMarker(output: string, marker: string): boolean {
297
- return output.includes(marker);
298
- }