@towles/tool 0.0.41 → 0.0.48
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/README.md +67 -109
- package/package.json +51 -41
- package/src/commands/base.ts +3 -18
- package/src/commands/config.ts +9 -8
- package/src/commands/doctor.ts +4 -1
- package/src/commands/gh/branch-clean.ts +10 -4
- package/src/commands/gh/branch.ts +6 -3
- package/src/commands/gh/pr.ts +10 -3
- package/src/commands/graph-template.html +1214 -0
- package/src/commands/graph.test.ts +176 -0
- package/src/commands/graph.ts +970 -0
- package/src/commands/install.ts +8 -2
- package/src/commands/journal/daily-notes.ts +9 -5
- package/src/commands/journal/meeting.ts +12 -6
- package/src/commands/journal/note.ts +12 -6
- package/src/commands/ralph/plan/add.ts +75 -0
- package/src/commands/ralph/plan/done.ts +82 -0
- package/src/commands/ralph/{task → plan}/list.test.ts +5 -5
- package/src/commands/ralph/{task → plan}/list.ts +28 -39
- package/src/commands/ralph/plan/remove.ts +71 -0
- package/src/commands/ralph/run.test.ts +521 -0
- package/src/commands/ralph/run.ts +126 -189
- package/src/commands/ralph/show.ts +88 -0
- package/src/config/settings.ts +8 -27
- package/src/{commands/ralph/lib → lib/ralph}/execution.ts +4 -14
- package/src/lib/ralph/formatter.ts +238 -0
- package/src/{commands/ralph/lib → lib/ralph}/state.ts +17 -42
- package/src/utils/date-utils.test.ts +2 -1
- package/src/utils/date-utils.ts +2 -2
- package/LICENSE.md +0 -20
- package/src/commands/index.ts +0 -55
- package/src/commands/observe/graph.test.ts +0 -89
- package/src/commands/observe/graph.ts +0 -1640
- package/src/commands/observe/report.ts +0 -166
- package/src/commands/observe/session.ts +0 -385
- package/src/commands/observe/setup.ts +0 -180
- package/src/commands/observe/status.ts +0 -146
- package/src/commands/ralph/lib/formatter.ts +0 -298
- package/src/commands/ralph/lib/marker.ts +0 -108
- package/src/commands/ralph/marker/create.ts +0 -23
- package/src/commands/ralph/plan.ts +0 -73
- package/src/commands/ralph/progress.ts +0 -44
- package/src/commands/ralph/ralph.test.ts +0 -673
- package/src/commands/ralph/task/add.ts +0 -105
- package/src/commands/ralph/task/done.ts +0 -73
- package/src/commands/ralph/task/remove.ts +0 -62
- package/src/config/context.ts +0 -7
- package/src/constants.ts +0 -3
- package/src/utils/anthropic/types.ts +0 -158
- package/src/utils/exec.ts +0 -8
- package/src/utils/git/git.ts +0 -25
- /package/src/{commands → lib}/journal/utils.ts +0 -0
- /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
|
-
}
|