@cephalization/math 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +190 -0
- package/index.ts +155 -0
- package/package.json +50 -0
- package/src/agent.test.ts +179 -0
- package/src/agent.ts +275 -0
- package/src/commands/init.ts +65 -0
- package/src/commands/iterate.ts +92 -0
- package/src/commands/plan.ts +16 -0
- package/src/commands/prune.ts +63 -0
- package/src/commands/run.test.ts +27 -0
- package/src/commands/run.ts +16 -0
- package/src/commands/status.ts +55 -0
- package/src/constants.ts +1 -0
- package/src/loop.test.ts +537 -0
- package/src/loop.ts +325 -0
- package/src/plan.ts +263 -0
- package/src/prune.test.ts +174 -0
- package/src/prune.ts +146 -0
- package/src/tasks.ts +204 -0
- package/src/templates.ts +172 -0
- package/src/ui/app.test.ts +228 -0
- package/src/ui/buffer.test.ts +222 -0
- package/src/ui/buffer.ts +131 -0
- package/src/ui/server.test.ts +271 -0
- package/src/ui/server.ts +124 -0
package/src/agent.ts
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent interface for the loop.
|
|
3
|
+
* This module provides an abstraction over the opencode CLI that can be
|
|
4
|
+
* satisfied by either the real CLI or a mock for testing.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Log entry categories for loop status messages.
|
|
9
|
+
*/
|
|
10
|
+
export type LogCategory = "info" | "success" | "warning" | "error";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A log entry emitted by the agent or loop.
|
|
14
|
+
*/
|
|
15
|
+
export interface LogEntry {
|
|
16
|
+
timestamp: Date;
|
|
17
|
+
category: LogCategory;
|
|
18
|
+
message: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Agent output event - raw text from the agent.
|
|
23
|
+
*/
|
|
24
|
+
export interface AgentOutput {
|
|
25
|
+
timestamp: Date;
|
|
26
|
+
text: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Events emitted by an agent during execution.
|
|
31
|
+
*/
|
|
32
|
+
export interface AgentEvents {
|
|
33
|
+
onLog?: (entry: LogEntry) => void;
|
|
34
|
+
onOutput?: (output: AgentOutput) => void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Options for running the agent.
|
|
39
|
+
*/
|
|
40
|
+
export interface AgentRunOptions {
|
|
41
|
+
model: string;
|
|
42
|
+
prompt: string;
|
|
43
|
+
files: string[];
|
|
44
|
+
events?: AgentEvents;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Result of an agent run.
|
|
49
|
+
*/
|
|
50
|
+
export interface AgentRunResult {
|
|
51
|
+
exitCode: number;
|
|
52
|
+
logs: LogEntry[];
|
|
53
|
+
output: AgentOutput[];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Agent interface that can be satisfied by opencode or a mock.
|
|
58
|
+
*/
|
|
59
|
+
export interface Agent {
|
|
60
|
+
/**
|
|
61
|
+
* Run the agent with the given options.
|
|
62
|
+
*/
|
|
63
|
+
run(options: AgentRunOptions): Promise<AgentRunResult>;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Check if the agent is available.
|
|
67
|
+
*/
|
|
68
|
+
isAvailable(): Promise<boolean>;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Create a log entry helper.
|
|
73
|
+
*/
|
|
74
|
+
export function createLogEntry(
|
|
75
|
+
category: LogCategory,
|
|
76
|
+
message: string
|
|
77
|
+
): LogEntry {
|
|
78
|
+
return {
|
|
79
|
+
timestamp: new Date(),
|
|
80
|
+
category,
|
|
81
|
+
message,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Create an agent output helper.
|
|
87
|
+
*/
|
|
88
|
+
export function createAgentOutput(text: string): AgentOutput {
|
|
89
|
+
return {
|
|
90
|
+
timestamp: new Date(),
|
|
91
|
+
text,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* OpenCode agent implementation that wraps the CLI.
|
|
97
|
+
*/
|
|
98
|
+
export class OpenCodeAgent implements Agent {
|
|
99
|
+
async isAvailable(): Promise<boolean> {
|
|
100
|
+
try {
|
|
101
|
+
const result = await Bun.$`which opencode`.quiet();
|
|
102
|
+
return result.exitCode === 0;
|
|
103
|
+
} catch {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async run(options: AgentRunOptions): Promise<AgentRunResult> {
|
|
109
|
+
const logs: LogEntry[] = [];
|
|
110
|
+
const output: AgentOutput[] = [];
|
|
111
|
+
|
|
112
|
+
const emitLog = (category: LogCategory, message: string) => {
|
|
113
|
+
const entry = createLogEntry(category, message);
|
|
114
|
+
logs.push(entry);
|
|
115
|
+
options.events?.onLog?.(entry);
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const emitOutput = (text: string) => {
|
|
119
|
+
const out = createAgentOutput(text);
|
|
120
|
+
output.push(out);
|
|
121
|
+
options.events?.onOutput?.(out);
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
emitLog("info", "Invoking opencode agent...");
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
// Build the command arguments
|
|
128
|
+
const fileArgs = options.files.flatMap((f) => ["-f", f]);
|
|
129
|
+
|
|
130
|
+
// Run opencode and capture output
|
|
131
|
+
const proc = Bun.spawn(
|
|
132
|
+
["opencode", "run", "-m", options.model, options.prompt, ...fileArgs],
|
|
133
|
+
{
|
|
134
|
+
stdout: "pipe",
|
|
135
|
+
stderr: "pipe",
|
|
136
|
+
}
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
// Stream stdout and stderr, emitting output as it arrives
|
|
140
|
+
const decoder = new TextDecoder();
|
|
141
|
+
|
|
142
|
+
const streamOutput = async (
|
|
143
|
+
stream: ReadableStream<Uint8Array> | null
|
|
144
|
+
) => {
|
|
145
|
+
if (!stream) return;
|
|
146
|
+
const reader = stream.getReader();
|
|
147
|
+
try {
|
|
148
|
+
while (true) {
|
|
149
|
+
const { done, value } = await reader.read();
|
|
150
|
+
if (done) break;
|
|
151
|
+
const text = decoder.decode(value, { stream: true });
|
|
152
|
+
if (text) emitOutput(text);
|
|
153
|
+
}
|
|
154
|
+
} finally {
|
|
155
|
+
reader.releaseLock();
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
// Read both streams concurrently while waiting for process to exit
|
|
160
|
+
await Promise.all([streamOutput(proc.stdout), streamOutput(proc.stderr)]);
|
|
161
|
+
|
|
162
|
+
const exitCode = await proc.exited;
|
|
163
|
+
|
|
164
|
+
if (exitCode === 0) {
|
|
165
|
+
emitLog("success", "Agent completed successfully");
|
|
166
|
+
} else {
|
|
167
|
+
emitLog("error", `Agent exited with code ${exitCode}`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return { exitCode, logs, output };
|
|
171
|
+
} catch (error) {
|
|
172
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
173
|
+
emitLog("error", `Error running agent: ${message}`);
|
|
174
|
+
return { exitCode: 1, logs, output };
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Mock agent for testing that doesn't call an LLM.
|
|
181
|
+
* Emits configurable log messages and output events.
|
|
182
|
+
*/
|
|
183
|
+
export class MockAgent implements Agent {
|
|
184
|
+
private available: boolean;
|
|
185
|
+
private mockLogs: Array<{ category: LogCategory; message: string }>;
|
|
186
|
+
private mockOutput: string[];
|
|
187
|
+
private mockExitCode: number;
|
|
188
|
+
private mockDelay: number;
|
|
189
|
+
|
|
190
|
+
constructor(
|
|
191
|
+
config: {
|
|
192
|
+
available?: boolean;
|
|
193
|
+
logs?: Array<{ category: LogCategory; message: string }>;
|
|
194
|
+
output?: string[];
|
|
195
|
+
exitCode?: number;
|
|
196
|
+
delay?: number;
|
|
197
|
+
} = {}
|
|
198
|
+
) {
|
|
199
|
+
this.available = config.available ?? true;
|
|
200
|
+
this.mockLogs = config.logs ?? [
|
|
201
|
+
{ category: "info", message: "Mock agent starting..." },
|
|
202
|
+
{ category: "success", message: "Mock agent completed" },
|
|
203
|
+
];
|
|
204
|
+
this.mockOutput = config.output ?? ["Mock agent output\n"];
|
|
205
|
+
this.mockExitCode = config.exitCode ?? 0;
|
|
206
|
+
this.mockDelay = config.delay ?? 0;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async isAvailable(): Promise<boolean> {
|
|
210
|
+
return this.available;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async run(options: AgentRunOptions): Promise<AgentRunResult> {
|
|
214
|
+
const logs: LogEntry[] = [];
|
|
215
|
+
const output: AgentOutput[] = [];
|
|
216
|
+
|
|
217
|
+
// Simulate delay if configured
|
|
218
|
+
if (this.mockDelay > 0) {
|
|
219
|
+
await Bun.sleep(this.mockDelay);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Emit configured logs
|
|
223
|
+
for (const { category, message } of this.mockLogs) {
|
|
224
|
+
const entry = createLogEntry(category, message);
|
|
225
|
+
logs.push(entry);
|
|
226
|
+
options.events?.onLog?.(entry);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Emit configured output
|
|
230
|
+
for (const text of this.mockOutput) {
|
|
231
|
+
const out = createAgentOutput(text);
|
|
232
|
+
output.push(out);
|
|
233
|
+
options.events?.onOutput?.(out);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
exitCode: this.mockExitCode,
|
|
238
|
+
logs,
|
|
239
|
+
output,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Configure the mock agent's behavior.
|
|
245
|
+
*/
|
|
246
|
+
configure(config: {
|
|
247
|
+
available?: boolean;
|
|
248
|
+
logs?: Array<{ category: LogCategory; message: string }>;
|
|
249
|
+
output?: string[];
|
|
250
|
+
exitCode?: number;
|
|
251
|
+
delay?: number;
|
|
252
|
+
}): void {
|
|
253
|
+
if (config.available !== undefined) this.available = config.available;
|
|
254
|
+
if (config.logs !== undefined) this.mockLogs = config.logs;
|
|
255
|
+
if (config.output !== undefined) this.mockOutput = config.output;
|
|
256
|
+
if (config.exitCode !== undefined) this.mockExitCode = config.exitCode;
|
|
257
|
+
if (config.delay !== undefined) this.mockDelay = config.delay;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Create the default agent (OpenCodeAgent).
|
|
263
|
+
*/
|
|
264
|
+
export function createAgent(): Agent {
|
|
265
|
+
return new OpenCodeAgent();
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Create a mock agent for testing.
|
|
270
|
+
*/
|
|
271
|
+
export function createMockAgent(
|
|
272
|
+
config?: Parameters<typeof MockAgent.prototype.configure>[0]
|
|
273
|
+
): MockAgent {
|
|
274
|
+
return new MockAgent(config);
|
|
275
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { mkdir } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import {
|
|
5
|
+
PROMPT_TEMPLATE,
|
|
6
|
+
TASKS_TEMPLATE,
|
|
7
|
+
LEARNINGS_TEMPLATE,
|
|
8
|
+
} from "../templates";
|
|
9
|
+
import { runPlanningMode, askToRunPlanning } from "../plan";
|
|
10
|
+
|
|
11
|
+
const colors = {
|
|
12
|
+
reset: "\x1b[0m",
|
|
13
|
+
green: "\x1b[32m",
|
|
14
|
+
yellow: "\x1b[33m",
|
|
15
|
+
cyan: "\x1b[36m",
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export async function init(
|
|
19
|
+
options: { skipPlan?: boolean; model?: string } = {}
|
|
20
|
+
) {
|
|
21
|
+
const todoDir = join(process.cwd(), "todo");
|
|
22
|
+
|
|
23
|
+
if (existsSync(todoDir)) {
|
|
24
|
+
console.log(
|
|
25
|
+
`${colors.yellow}todo/ directory already exists${colors.reset}`
|
|
26
|
+
);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Create todo directory
|
|
31
|
+
await mkdir(todoDir, { recursive: true });
|
|
32
|
+
|
|
33
|
+
// Write template files
|
|
34
|
+
await Bun.write(join(todoDir, "PROMPT.md"), PROMPT_TEMPLATE);
|
|
35
|
+
await Bun.write(join(todoDir, "TASKS.md"), TASKS_TEMPLATE);
|
|
36
|
+
await Bun.write(join(todoDir, "LEARNINGS.md"), LEARNINGS_TEMPLATE);
|
|
37
|
+
|
|
38
|
+
console.log(`${colors.green}✓${colors.reset} Created todo/ directory with:`);
|
|
39
|
+
console.log(
|
|
40
|
+
` ${colors.cyan}PROMPT.md${colors.reset} - System prompt with guardrails`
|
|
41
|
+
);
|
|
42
|
+
console.log(` ${colors.cyan}TASKS.md${colors.reset} - Task tracker`);
|
|
43
|
+
console.log(` ${colors.cyan}LEARNINGS.md${colors.reset} - Knowledge log`);
|
|
44
|
+
|
|
45
|
+
// Ask to run planning mode unless --no-plan flag
|
|
46
|
+
if (!options.skipPlan) {
|
|
47
|
+
const shouldPlan = await askToRunPlanning();
|
|
48
|
+
if (shouldPlan) {
|
|
49
|
+
await runPlanningMode({ todoDir, options: { model: options.model } });
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
console.log();
|
|
55
|
+
console.log(`Next steps:`);
|
|
56
|
+
console.log(
|
|
57
|
+
` 1. Edit ${colors.cyan}todo/TASKS.md${colors.reset} to add your tasks`
|
|
58
|
+
);
|
|
59
|
+
console.log(
|
|
60
|
+
` 2. Customize ${colors.cyan}todo/PROMPT.md${colors.reset} for your project`
|
|
61
|
+
);
|
|
62
|
+
console.log(
|
|
63
|
+
` 3. Run ${colors.cyan}math run${colors.reset} to start the agent loop`
|
|
64
|
+
);
|
|
65
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { TASKS_TEMPLATE, LEARNINGS_TEMPLATE } from "../templates";
|
|
4
|
+
import { runPlanningMode, askToRunPlanning } from "../plan";
|
|
5
|
+
|
|
6
|
+
const colors = {
|
|
7
|
+
reset: "\x1b[0m",
|
|
8
|
+
bold: "\x1b[1m",
|
|
9
|
+
green: "\x1b[32m",
|
|
10
|
+
yellow: "\x1b[33m",
|
|
11
|
+
cyan: "\x1b[36m",
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export async function iterate(
|
|
15
|
+
options: { skipPlan?: boolean; model?: string } = {}
|
|
16
|
+
) {
|
|
17
|
+
const todoDir = join(process.cwd(), "todo");
|
|
18
|
+
|
|
19
|
+
if (!existsSync(todoDir)) {
|
|
20
|
+
throw new Error("todo/ directory not found. Run 'math init' first.");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Generate backup directory name: todo-{M}-{D}-{Y}
|
|
24
|
+
const now = new Date();
|
|
25
|
+
const month = now.getMonth() + 1;
|
|
26
|
+
const day = now.getDate();
|
|
27
|
+
const year = now.getFullYear();
|
|
28
|
+
const backupDir = join(process.cwd(), `todo-${month}-${day}-${year}`);
|
|
29
|
+
|
|
30
|
+
// Handle existing backup for same day
|
|
31
|
+
let finalBackupDir = backupDir;
|
|
32
|
+
let counter = 1;
|
|
33
|
+
while (existsSync(finalBackupDir)) {
|
|
34
|
+
finalBackupDir = `${backupDir}-${counter}`;
|
|
35
|
+
counter++;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
console.log(`${colors.bold}Iterating to new sprint${colors.reset}\n`);
|
|
39
|
+
|
|
40
|
+
// Step 1: Backup current todo directory
|
|
41
|
+
console.log(
|
|
42
|
+
`${colors.cyan}1.${colors.reset} Backing up todo/ to ${finalBackupDir
|
|
43
|
+
.split("/")
|
|
44
|
+
.pop()}/`
|
|
45
|
+
);
|
|
46
|
+
await Bun.$`cp -r ${todoDir} ${finalBackupDir}`;
|
|
47
|
+
console.log(` ${colors.green}✓${colors.reset} Backup complete\n`);
|
|
48
|
+
|
|
49
|
+
// Step 2: Reset TASKS.md
|
|
50
|
+
console.log(`${colors.cyan}2.${colors.reset} Resetting TASKS.md`);
|
|
51
|
+
await Bun.write(join(todoDir, "TASKS.md"), TASKS_TEMPLATE);
|
|
52
|
+
console.log(
|
|
53
|
+
` ${colors.green}✓${colors.reset} TASKS.md reset to template\n`
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
// Step 3: Reset LEARNINGS.md
|
|
57
|
+
console.log(`${colors.cyan}3.${colors.reset} Resetting LEARNINGS.md`);
|
|
58
|
+
await Bun.write(join(todoDir, "LEARNINGS.md"), LEARNINGS_TEMPLATE);
|
|
59
|
+
console.log(
|
|
60
|
+
` ${colors.green}✓${colors.reset} LEARNINGS.md reset to template\n`
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
// Step 4: Keep PROMPT.md (signs are preserved)
|
|
64
|
+
console.log(
|
|
65
|
+
`${colors.cyan}4.${colors.reset} Preserving PROMPT.md (signs retained)\n`
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
console.log(`${colors.green}Done!${colors.reset} Ready for new sprint.`);
|
|
69
|
+
console.log(
|
|
70
|
+
`${colors.yellow}Previous sprint preserved at:${
|
|
71
|
+
colors.reset
|
|
72
|
+
} ${finalBackupDir.split("/").pop()}/`
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
// Ask to run planning mode unless --no-plan flag
|
|
76
|
+
if (!options.skipPlan) {
|
|
77
|
+
const shouldPlan = await askToRunPlanning();
|
|
78
|
+
if (shouldPlan) {
|
|
79
|
+
await runPlanningMode({ todoDir, options: { model: options.model } });
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
console.log();
|
|
85
|
+
console.log(`${colors.bold}Next steps:${colors.reset}`);
|
|
86
|
+
console.log(
|
|
87
|
+
` 1. Edit ${colors.cyan}todo/TASKS.md${colors.reset} to add new tasks`
|
|
88
|
+
);
|
|
89
|
+
console.log(
|
|
90
|
+
` 2. Run ${colors.cyan}math run${colors.reset} to start the agent loop`
|
|
91
|
+
);
|
|
92
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { runPlanningMode } from "../plan";
|
|
4
|
+
|
|
5
|
+
export async function plan(options: { model?: string; quick?: boolean } = {}) {
|
|
6
|
+
const todoDir = join(process.cwd(), "todo");
|
|
7
|
+
|
|
8
|
+
if (!existsSync(todoDir)) {
|
|
9
|
+
throw new Error("todo/ directory not found. Run 'math init' first.");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
await runPlanningMode({
|
|
13
|
+
todoDir,
|
|
14
|
+
options: { model: options.model, quick: options.quick },
|
|
15
|
+
});
|
|
16
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { basename } from "node:path";
|
|
2
|
+
import { findArtifacts, confirmPrune, deleteArtifacts } from "../prune";
|
|
3
|
+
|
|
4
|
+
const colors = {
|
|
5
|
+
reset: "\x1b[0m",
|
|
6
|
+
bold: "\x1b[1m",
|
|
7
|
+
dim: "\x1b[2m",
|
|
8
|
+
red: "\x1b[31m",
|
|
9
|
+
green: "\x1b[32m",
|
|
10
|
+
yellow: "\x1b[33m",
|
|
11
|
+
cyan: "\x1b[36m",
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export async function prune(options: { force?: boolean } = {}) {
|
|
15
|
+
// Find all artifacts
|
|
16
|
+
const artifacts = findArtifacts();
|
|
17
|
+
|
|
18
|
+
if (artifacts.length === 0) {
|
|
19
|
+
console.log(`${colors.dim}No artifacts found to clean up.${colors.reset}`);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
console.log(
|
|
24
|
+
`${colors.bold}Found ${artifacts.length} artifact${artifacts.length === 1 ? "" : "s"}:${colors.reset}`
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
// Ask for confirmation (skipped if --force)
|
|
28
|
+
const { confirmed } = await confirmPrune(artifacts, { force: options.force });
|
|
29
|
+
|
|
30
|
+
if (!confirmed) {
|
|
31
|
+
console.log(`${colors.yellow}Aborted.${colors.reset}`);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Delete the artifacts
|
|
36
|
+
const result = deleteArtifacts(artifacts);
|
|
37
|
+
|
|
38
|
+
// Report results
|
|
39
|
+
if (result.deleted.length > 0) {
|
|
40
|
+
console.log(
|
|
41
|
+
`${colors.green}✓${colors.reset} Deleted ${result.deleted.length} artifact${result.deleted.length === 1 ? "" : "s"}:`
|
|
42
|
+
);
|
|
43
|
+
for (const path of result.deleted) {
|
|
44
|
+
console.log(` ${colors.dim}${basename(path)}/${colors.reset}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (result.failed.length > 0) {
|
|
49
|
+
console.log();
|
|
50
|
+
console.log(
|
|
51
|
+
`${colors.red}✗${colors.reset} Failed to delete ${result.failed.length} artifact${result.failed.length === 1 ? "" : "s"}:`
|
|
52
|
+
);
|
|
53
|
+
for (const { path, error } of result.failed) {
|
|
54
|
+
console.log(` ${colors.red}${basename(path)}/${colors.reset}: ${error}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Summary
|
|
59
|
+
if (result.deleted.length > 0 && result.failed.length === 0) {
|
|
60
|
+
console.log();
|
|
61
|
+
console.log(`${colors.green}All artifacts cleaned up.${colors.reset}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { test, expect, describe } from "bun:test";
|
|
2
|
+
import { runLoop } from "../loop";
|
|
3
|
+
|
|
4
|
+
describe("run command --no-ui option", () => {
|
|
5
|
+
// The run command transforms `--no-ui` CLI flag to `ui: false` option for runLoop.
|
|
6
|
+
// Since runLoop already has comprehensive tests for ui: false behavior in loop.test.ts,
|
|
7
|
+
// we just need a simple test to verify the transformation logic.
|
|
8
|
+
|
|
9
|
+
test("--no-ui flag results in ui: false", () => {
|
|
10
|
+
// This tests the transformation logic in run.ts:
|
|
11
|
+
// ui: !options["no-ui"]
|
|
12
|
+
|
|
13
|
+
// When --no-ui is present, options["no-ui"] = true
|
|
14
|
+
// So ui = !true = false
|
|
15
|
+
const options: Record<string, string | boolean> = { "no-ui": true };
|
|
16
|
+
const uiValue = !options["no-ui"];
|
|
17
|
+
expect(uiValue).toBe(false);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("without --no-ui flag, ui defaults to true", () => {
|
|
21
|
+
// When --no-ui is absent, options["no-ui"] = undefined
|
|
22
|
+
// So ui = !undefined = true
|
|
23
|
+
const options: Record<string, string | boolean> = {};
|
|
24
|
+
const uiValue = !options["no-ui"];
|
|
25
|
+
expect(uiValue).toBe(true);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { runLoop } from "../loop";
|
|
2
|
+
|
|
3
|
+
export async function run(options: Record<string, string | boolean>) {
|
|
4
|
+
await runLoop({
|
|
5
|
+
model: typeof options.model === "string" ? options.model : undefined,
|
|
6
|
+
maxIterations:
|
|
7
|
+
typeof options["max-iterations"] === "string"
|
|
8
|
+
? parseInt(options["max-iterations"], 10)
|
|
9
|
+
: undefined,
|
|
10
|
+
pauseSeconds:
|
|
11
|
+
typeof options.pause === "string"
|
|
12
|
+
? parseInt(options.pause, 10)
|
|
13
|
+
: undefined,
|
|
14
|
+
ui: !!options.ui,
|
|
15
|
+
});
|
|
16
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { readTasks, countTasks, findNextTask } from "../tasks";
|
|
2
|
+
|
|
3
|
+
const colors = {
|
|
4
|
+
reset: "\x1b[0m",
|
|
5
|
+
bold: "\x1b[1m",
|
|
6
|
+
dim: "\x1b[2m",
|
|
7
|
+
red: "\x1b[31m",
|
|
8
|
+
green: "\x1b[32m",
|
|
9
|
+
yellow: "\x1b[33m",
|
|
10
|
+
blue: "\x1b[34m",
|
|
11
|
+
cyan: "\x1b[36m",
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export async function status() {
|
|
15
|
+
const { tasks } = await readTasks();
|
|
16
|
+
const counts = countTasks(tasks);
|
|
17
|
+
|
|
18
|
+
console.log(`${colors.bold}Task Status${colors.reset}\n`);
|
|
19
|
+
|
|
20
|
+
// Progress bar
|
|
21
|
+
const barWidth = 30;
|
|
22
|
+
const completedWidth = Math.round((counts.complete / counts.total) * barWidth);
|
|
23
|
+
const inProgressWidth = Math.round((counts.in_progress / counts.total) * barWidth);
|
|
24
|
+
const pendingWidth = barWidth - completedWidth - inProgressWidth;
|
|
25
|
+
|
|
26
|
+
const progressBar =
|
|
27
|
+
colors.green + "█".repeat(completedWidth) +
|
|
28
|
+
colors.yellow + "█".repeat(inProgressWidth) +
|
|
29
|
+
colors.dim + "░".repeat(pendingWidth) +
|
|
30
|
+
colors.reset;
|
|
31
|
+
|
|
32
|
+
console.log(` ${progressBar} ${counts.complete}/${counts.total}`);
|
|
33
|
+
console.log();
|
|
34
|
+
|
|
35
|
+
// Counts
|
|
36
|
+
console.log(` ${colors.green}✓ Complete:${colors.reset} ${counts.complete}`);
|
|
37
|
+
console.log(` ${colors.yellow}◐ In Progress:${colors.reset} ${counts.in_progress}`);
|
|
38
|
+
console.log(` ${colors.dim}○ Pending:${colors.reset} ${counts.pending}`);
|
|
39
|
+
console.log();
|
|
40
|
+
|
|
41
|
+
// Next task
|
|
42
|
+
const nextTask = findNextTask(tasks);
|
|
43
|
+
if (nextTask) {
|
|
44
|
+
console.log(`${colors.bold}Next Task${colors.reset}`);
|
|
45
|
+
console.log(` ${colors.cyan}${nextTask.id}${colors.reset}`);
|
|
46
|
+
console.log(` ${colors.dim}${nextTask.content}${colors.reset}`);
|
|
47
|
+
} else if (counts.complete === counts.total) {
|
|
48
|
+
console.log(`${colors.green}All tasks complete!${colors.reset}`);
|
|
49
|
+
} else if (counts.in_progress > 0) {
|
|
50
|
+
const inProgressTask = tasks.find((t) => t.status === "in_progress");
|
|
51
|
+
console.log(`${colors.yellow}Task in progress:${colors.reset} ${inProgressTask?.id}`);
|
|
52
|
+
} else {
|
|
53
|
+
console.log(`${colors.yellow}No tasks ready (check dependencies)${colors.reset}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const DEFAULT_MODEL = "anthropic/claude-opus-4-5";
|