create-claude-workspace 1.1.151 → 2.0.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.
Files changed (56) hide show
  1. package/README.md +33 -1
  2. package/dist/index.js +29 -56
  3. package/dist/scheduler/agents/health-checker.mjs +98 -0
  4. package/dist/scheduler/agents/health-checker.spec.js +143 -0
  5. package/dist/scheduler/agents/orchestrator.mjs +149 -0
  6. package/dist/scheduler/agents/orchestrator.spec.js +87 -0
  7. package/dist/scheduler/agents/prompt-builder.mjs +204 -0
  8. package/dist/scheduler/agents/prompt-builder.spec.js +240 -0
  9. package/dist/scheduler/agents/worker-pool.mjs +137 -0
  10. package/dist/scheduler/agents/worker-pool.spec.js +45 -0
  11. package/dist/scheduler/git/ci-watcher.mjs +93 -0
  12. package/dist/scheduler/git/ci-watcher.spec.js +35 -0
  13. package/dist/scheduler/git/manager.mjs +228 -0
  14. package/dist/scheduler/git/manager.spec.js +198 -0
  15. package/dist/scheduler/git/release.mjs +117 -0
  16. package/dist/scheduler/git/release.spec.js +175 -0
  17. package/dist/scheduler/index.mjs +309 -0
  18. package/dist/scheduler/index.spec.js +72 -0
  19. package/dist/scheduler/integration.spec.js +289 -0
  20. package/dist/scheduler/loop.mjs +435 -0
  21. package/dist/scheduler/loop.spec.js +139 -0
  22. package/dist/scheduler/state/session.mjs +14 -0
  23. package/dist/scheduler/state/session.spec.js +36 -0
  24. package/dist/scheduler/state/state.mjs +102 -0
  25. package/dist/scheduler/state/state.spec.js +175 -0
  26. package/dist/scheduler/tasks/inbox.mjs +98 -0
  27. package/dist/scheduler/tasks/inbox.spec.js +168 -0
  28. package/dist/scheduler/tasks/parser.mjs +228 -0
  29. package/dist/scheduler/tasks/parser.spec.js +303 -0
  30. package/dist/scheduler/tasks/queue.mjs +152 -0
  31. package/dist/scheduler/tasks/queue.spec.js +223 -0
  32. package/dist/scheduler/types.mjs +20 -0
  33. package/dist/{scripts/lib → scheduler/ui}/tui.mjs +84 -41
  34. package/dist/{scripts/lib → scheduler/ui}/tui.spec.js +56 -0
  35. package/dist/scheduler/util/memory.mjs +126 -0
  36. package/dist/scheduler/util/memory.spec.js +165 -0
  37. package/dist/template/.claude/{profiles/angular.md → agents/angular-engineer.md} +9 -4
  38. package/dist/template/.claude/{profiles/react.md → agents/react-engineer.md} +9 -4
  39. package/dist/template/.claude/{profiles/svelte.md → agents/svelte-engineer.md} +9 -4
  40. package/dist/template/.claude/{profiles/vue.md → agents/vue-engineer.md} +9 -4
  41. package/package.json +3 -4
  42. package/dist/scripts/autonomous.mjs +0 -492
  43. package/dist/scripts/autonomous.spec.js +0 -46
  44. package/dist/scripts/docker-run.mjs +0 -462
  45. package/dist/scripts/integration.spec.js +0 -108
  46. package/dist/scripts/lib/formatter.mjs +0 -309
  47. package/dist/scripts/lib/formatter.spec.js +0 -262
  48. package/dist/scripts/lib/state.mjs +0 -44
  49. package/dist/scripts/lib/state.spec.js +0 -59
  50. package/dist/template/.claude/docker/.dockerignore +0 -8
  51. package/dist/template/.claude/docker/Dockerfile +0 -54
  52. package/dist/template/.claude/docker/docker-compose.yml +0 -22
  53. package/dist/template/.claude/docker/docker-entrypoint.sh +0 -101
  54. /package/dist/{scripts/lib/types.mjs → scheduler/shared-types.mjs} +0 -0
  55. /package/dist/{scripts/lib → scheduler/util}/idle-poll.mjs +0 -0
  56. /package/dist/{scripts/lib → scheduler/util}/idle-poll.spec.js +0 -0
@@ -1,309 +0,0 @@
1
- // ─── Beautiful terminal output for autonomous loop ───
2
- import { appendFileSync } from 'node:fs';
3
- // ─── ANSI colors ───
4
- const c = {
5
- reset: '\x1b[0m',
6
- bold: '\x1b[1m',
7
- dim: '\x1b[2m',
8
- italic: '\x1b[3m',
9
- // Foreground
10
- black: '\x1b[30m',
11
- red: '\x1b[31m',
12
- green: '\x1b[32m',
13
- yellow: '\x1b[33m',
14
- blue: '\x1b[34m',
15
- magenta: '\x1b[35m',
16
- cyan: '\x1b[36m',
17
- white: '\x1b[37m',
18
- // Bright foreground
19
- gray: '\x1b[90m',
20
- brightRed: '\x1b[91m',
21
- brightGreen: '\x1b[92m',
22
- brightYellow: '\x1b[93m',
23
- brightBlue: '\x1b[94m',
24
- brightMagenta: '\x1b[95m',
25
- brightCyan: '\x1b[96m',
26
- // Background
27
- bgBlue: '\x1b[44m',
28
- bgMagenta: '\x1b[45m',
29
- bgCyan: '\x1b[46m',
30
- bgGray: '\x1b[100m',
31
- };
32
- // ─── Agent colors (rotating) ───
33
- const AGENT_COLORS = [c.brightCyan, c.brightMagenta, c.brightGreen, c.brightYellow, c.brightBlue, c.brightRed];
34
- const agentColorMap = new Map();
35
- let colorIndex = 0;
36
- function agentColor(name) {
37
- if (!agentColorMap.has(name)) {
38
- agentColorMap.set(name, AGENT_COLORS[colorIndex % AGENT_COLORS.length]);
39
- colorIndex++;
40
- }
41
- return agentColorMap.get(name);
42
- }
43
- // ─── Tool icons ───
44
- const TOOL_ICONS = {
45
- Bash: '⚡',
46
- Read: '📖',
47
- Write: '✏️',
48
- Edit: '🔧',
49
- Glob: '🔍',
50
- Grep: '🔎',
51
- Agent: '🤖',
52
- TodoRead: '📋',
53
- TodoWrite: '📝',
54
- WebSearch: '🌐',
55
- WebFetch: '🌐',
56
- AskUserQuestion: '❓',
57
- };
58
- function toolIcon(name) {
59
- return TOOL_ICONS[name] || '⚙️';
60
- }
61
- // ─── Time formatting ───
62
- function ts() {
63
- return new Date().toLocaleTimeString('en-GB', { hour12: false });
64
- }
65
- function duration(ms) {
66
- if (ms < 1000)
67
- return `${ms}ms`;
68
- if (ms < 60_000)
69
- return `${(ms / 1000).toFixed(1)}s`;
70
- if (ms < 3600_000)
71
- return `${(ms / 60_000).toFixed(1)}m`;
72
- return `${(ms / 3600_000).toFixed(1)}h`;
73
- }
74
- // ─── Formatting helpers ───
75
- function truncate(s, max) {
76
- const clean = s.replace(/\n/g, ' ').trim();
77
- return clean.length > max ? clean.slice(0, max) + '…' : clean;
78
- }
79
- function formatPath(path) {
80
- // Shorten long paths: /project/libs/shared/ui/src/lib/component.ts → .../ui/src/lib/component.ts
81
- const short = path.replace(/^\/project\//, '');
82
- return `${c.cyan}${short}${c.reset}`;
83
- }
84
- function formatCommand(cmd) {
85
- const short = truncate(cmd, 80);
86
- return `${c.yellow}${short}${c.reset}`;
87
- }
88
- // ─── Main formatter class ───
89
- export class Formatter {
90
- logFile;
91
- iterStartTime = 0;
92
- totalTokens = { input: 0, output: 0 };
93
- toolCalls = 0;
94
- agentStack = [];
95
- constructor(logFile) {
96
- this.logFile = logFile;
97
- }
98
- // ── Raw log (file only, no formatting) ──
99
- fileLog(msg) {
100
- if (!this.logFile)
101
- return;
102
- try {
103
- appendFileSync(this.logFile, `[${new Date().toISOString()}] ${msg}\n`);
104
- }
105
- catch { /* */ }
106
- }
107
- // ── Print to console + file ──
108
- print(formatted, raw) {
109
- console.log(formatted);
110
- this.fileLog(raw || formatted.replace(/\x1b\[[0-9;]*m/g, ''));
111
- }
112
- // ─── Public API ───
113
- banner() {
114
- this.print('');
115
- this.print(`${c.bold}${c.cyan} ╔══════════════════════════════════════════════╗${c.reset}`);
116
- this.print(`${c.bold}${c.cyan} ║ ${c.white}Claude Starter Kit — Autonomous Loop${c.cyan} ║${c.reset}`);
117
- this.print(`${c.bold}${c.cyan} ╚══════════════════════════════════════════════╝${c.reset}`);
118
- this.print('');
119
- }
120
- info(msg) {
121
- this.print(` ${c.gray}${ts()}${c.reset} ${c.blue}ℹ${c.reset} ${msg}`);
122
- }
123
- warn(msg) {
124
- this.print(` ${c.gray}${ts()}${c.reset} ${c.yellow}⚠${c.reset} ${c.yellow}${msg}${c.reset}`);
125
- }
126
- error(msg) {
127
- this.print(` ${c.gray}${ts()}${c.reset} ${c.red}✗${c.reset} ${c.red}${msg}${c.reset}`);
128
- }
129
- success(msg) {
130
- this.print(` ${c.gray}${ts()}${c.reset} ${c.green}✓${c.reset} ${c.green}${msg}${c.reset}`);
131
- }
132
- iterationStart(i, max) {
133
- this.iterStartTime = Date.now();
134
- this.toolCalls = 0;
135
- const pct = Math.round((i / max) * 100);
136
- const bar = progressBar(pct, 20);
137
- this.print('');
138
- this.print(` ${c.bold}${c.white}─── Iteration ${i}/${max} ${c.gray}${bar} ${pct}%${c.reset}`);
139
- this.print('');
140
- }
141
- iterationEnd() {
142
- const elapsed = Date.now() - this.iterStartTime;
143
- const tokens = this.totalTokens.input + this.totalTokens.output;
144
- this.print('');
145
- this.print(` ${c.gray}──── ${duration(elapsed)} │ ${this.toolCalls} tools │ ${formatTokens(tokens)} tokens ────${c.reset}`);
146
- }
147
- // ─── SDK Message handling ───
148
- handleMessage(message) {
149
- const msg = message;
150
- switch (message.type) {
151
- case 'assistant':
152
- this.handleAssistant(msg);
153
- break;
154
- case 'user':
155
- this.handleToolResult(msg);
156
- break;
157
- case 'system':
158
- this.handleSystem(msg);
159
- break;
160
- case 'result':
161
- this.handleResult(msg);
162
- break;
163
- default:
164
- // Silently skip unknown types (stream_event, etc.)
165
- break;
166
- }
167
- }
168
- handleAssistant(msg) {
169
- const content = msg.message?.content;
170
- if (!Array.isArray(content))
171
- return;
172
- for (const block of content) {
173
- if (block.type === 'text' && block.text?.trim()) {
174
- const text = truncate(block.text, 300);
175
- const indent = this.indent();
176
- this.print(`${indent}${c.white}${text}${c.reset}`, `TEXT: ${text}`);
177
- }
178
- if (block.type === 'tool_use') {
179
- this.handleToolUse(block);
180
- }
181
- }
182
- // Track tokens from usage
183
- if (msg.message?.usage) {
184
- this.totalTokens.input += msg.message.usage.input_tokens || 0;
185
- this.totalTokens.output += msg.message.usage.output_tokens || 0;
186
- }
187
- }
188
- handleToolUse(block) {
189
- this.toolCalls++;
190
- const name = block.name || 'unknown';
191
- const icon = toolIcon(name);
192
- const input = block.input || {};
193
- const indent = this.indent();
194
- if (name === 'Agent') {
195
- // Agent delegation — show agent name and description
196
- const agentType = input.subagent_type || input.type || 'agent';
197
- const desc = truncate(input.description || input.prompt || '', 60);
198
- const col = agentColor(agentType);
199
- this.agentStack.push(agentType);
200
- this.print(`${indent}${icon} ${col}${c.bold}${agentType}${c.reset} ${c.gray}${desc}${c.reset}`, `AGENT: ${agentType} — ${desc}`);
201
- return;
202
- }
203
- // Format based on tool type
204
- let detail = '';
205
- if (name === 'Bash') {
206
- detail = formatCommand(input.command || '');
207
- }
208
- else if (name === 'Read') {
209
- detail = formatPath(input.file_path || '');
210
- }
211
- else if (name === 'Write' || name === 'Edit') {
212
- detail = formatPath(input.file_path || '');
213
- }
214
- else if (name === 'Glob') {
215
- detail = `${c.cyan}${input.pattern || ''}${c.reset}`;
216
- }
217
- else if (name === 'Grep') {
218
- detail = `${c.cyan}/${input.pattern || ''}/`;
219
- if (input.path)
220
- detail += ` ${c.gray}in ${input.path}`;
221
- detail += c.reset;
222
- }
223
- else if (name === 'TodoWrite') {
224
- detail = `${c.gray}updating task list${c.reset}`;
225
- }
226
- else if (name === 'TodoRead') {
227
- detail = `${c.gray}reading task list${c.reset}`;
228
- }
229
- else {
230
- const summary = truncate(JSON.stringify(input), 80);
231
- detail = `${c.gray}${summary}${c.reset}`;
232
- }
233
- this.print(`${indent}${c.gray}${ts()}${c.reset} ${icon} ${c.bold}${name}${c.reset} ${detail}`, `TOOL: ${name} ${JSON.stringify(input).slice(0, 200)}`);
234
- }
235
- handleToolResult(msg) {
236
- const content = msg.message?.content;
237
- if (!Array.isArray(content))
238
- return;
239
- for (const block of content) {
240
- if (block.type !== 'tool_result')
241
- continue;
242
- const output = String(block.content || '').trim();
243
- const isError = block.is_error === true;
244
- const indent = this.indent();
245
- if (isError) {
246
- const short = truncate(output, 120);
247
- this.print(`${indent} ${c.red}✗ ${short}${c.reset}`, `ERROR: ${short}`);
248
- }
249
- else if (output && output.length > 0) {
250
- // Show short results inline, skip very long ones
251
- if (output.length < 200) {
252
- const short = truncate(output, 120);
253
- this.print(`${indent} ${c.green}✓${c.reset} ${c.gray}${short}${c.reset}`, `RESULT: ${short}`);
254
- }
255
- else {
256
- const lines = output.split('\n').length;
257
- this.print(`${indent} ${c.green}✓${c.reset} ${c.gray}${lines} lines${c.reset}`, `RESULT: (${lines} lines)`);
258
- }
259
- }
260
- }
261
- }
262
- handleSystem(msg) {
263
- if (msg.subtype === 'task_progress') {
264
- const desc = msg.description || '';
265
- if (desc) {
266
- const indent = this.indent();
267
- this.print(`${indent} ${c.dim}${truncate(desc, 80)}${c.reset}`, `PROGRESS: ${desc}`);
268
- }
269
- return;
270
- }
271
- // Other system messages
272
- const text = msg.message || JSON.stringify(msg).slice(0, 150);
273
- this.info(`${c.dim}${truncate(String(text), 120)}${c.reset}`);
274
- }
275
- handleResult(msg) {
276
- const sessionId = msg.session_id;
277
- if (sessionId) {
278
- this.fileLog(`SESSION: ${sessionId}`);
279
- }
280
- this.success('Iteration complete');
281
- }
282
- // ─── Helpers ───
283
- indent() {
284
- const depth = this.agentStack.length;
285
- if (depth === 0)
286
- return ' ';
287
- return ' ' + '│ '.repeat(depth);
288
- }
289
- // Reset agent stack between iterations
290
- resetAgentStack() {
291
- this.agentStack = [];
292
- }
293
- getSessionId(msg) {
294
- return msg.session_id ?? null;
295
- }
296
- }
297
- // ─── Utilities ───
298
- function progressBar(pct, width) {
299
- const filled = Math.round((pct / 100) * width);
300
- const empty = width - filled;
301
- return `${c.green}${'█'.repeat(filled)}${c.gray}${'░'.repeat(empty)}${c.reset}`;
302
- }
303
- function formatTokens(n) {
304
- if (n < 1000)
305
- return String(n);
306
- if (n < 1_000_000)
307
- return `${(n / 1000).toFixed(1)}K`;
308
- return `${(n / 1_000_000).toFixed(2)}M`;
309
- }
@@ -1,262 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
- import { mkdirSync, rmSync, readFileSync } from 'node:fs';
3
- import { resolve, dirname } from 'node:path';
4
- import { fileURLToPath } from 'node:url';
5
- import { Formatter } from './formatter.mjs';
6
- const TEST_DIR = resolve(dirname(fileURLToPath(import.meta.url)), '__fmt_test_tmp__');
7
- const LOG_FILE = resolve(TEST_DIR, 'test.log');
8
- beforeEach(() => {
9
- mkdirSync(TEST_DIR, { recursive: true });
10
- vi.spyOn(console, 'log').mockImplementation(() => { });
11
- });
12
- afterEach(() => {
13
- vi.restoreAllMocks();
14
- rmSync(TEST_DIR, { recursive: true, force: true });
15
- });
16
- function readLog() {
17
- try {
18
- return readFileSync(LOG_FILE, 'utf-8');
19
- }
20
- catch {
21
- return '';
22
- }
23
- }
24
- describe('Formatter', () => {
25
- it('creates instance with log file', () => {
26
- const fmt = new Formatter(LOG_FILE);
27
- expect(fmt).toBeDefined();
28
- });
29
- it('banner outputs to console', () => {
30
- const fmt = new Formatter(LOG_FILE);
31
- fmt.banner();
32
- expect(console.log).toHaveBeenCalled();
33
- });
34
- it('info writes to console and file', () => {
35
- const fmt = new Formatter(LOG_FILE);
36
- fmt.info('test message');
37
- expect(console.log).toHaveBeenCalled();
38
- const log = readLog();
39
- expect(log).toContain('test message');
40
- });
41
- it('warn writes with warning indicator', () => {
42
- const fmt = new Formatter(LOG_FILE);
43
- fmt.warn('warning msg');
44
- const log = readLog();
45
- expect(log).toContain('warning msg');
46
- });
47
- it('error writes with error indicator', () => {
48
- const fmt = new Formatter(LOG_FILE);
49
- fmt.error('error msg');
50
- const log = readLog();
51
- expect(log).toContain('error msg');
52
- });
53
- it('success writes with success indicator', () => {
54
- const fmt = new Formatter(LOG_FILE);
55
- fmt.success('done');
56
- const log = readLog();
57
- expect(log).toContain('done');
58
- });
59
- it('iterationStart writes iteration header', () => {
60
- const fmt = new Formatter(LOG_FILE);
61
- fmt.iterationStart(3, 50);
62
- const calls = console.log.mock.calls.map((c) => c[0]);
63
- const joined = calls.join('\n');
64
- expect(joined).toContain('3');
65
- expect(joined).toContain('50');
66
- });
67
- it('iterationEnd writes summary', () => {
68
- const fmt = new Formatter(LOG_FILE);
69
- fmt.iterationStart(1, 10);
70
- fmt.iterationEnd();
71
- const calls = console.log.mock.calls.map((c) => c[0]);
72
- const joined = calls.join('\n');
73
- expect(joined).toContain('tools');
74
- expect(joined).toContain('tokens');
75
- });
76
- });
77
- describe('Formatter.handleMessage', () => {
78
- it('handles assistant text messages', () => {
79
- const fmt = new Formatter(LOG_FILE);
80
- fmt.handleMessage({
81
- type: 'assistant',
82
- message: { content: [{ type: 'text', text: 'Hello from Claude' }] },
83
- });
84
- const log = readLog();
85
- expect(log).toContain('Hello from Claude');
86
- });
87
- it('handles tool_use Bash', () => {
88
- const fmt = new Formatter(LOG_FILE);
89
- fmt.handleMessage({
90
- type: 'assistant',
91
- message: { content: [{ type: 'tool_use', name: 'Bash', input: { command: 'ls -la' } }] },
92
- });
93
- const log = readLog();
94
- expect(log).toContain('Bash');
95
- expect(log).toContain('ls -la');
96
- });
97
- it('handles tool_use Read', () => {
98
- const fmt = new Formatter(LOG_FILE);
99
- fmt.handleMessage({
100
- type: 'assistant',
101
- message: { content: [{ type: 'tool_use', name: 'Read', input: { file_path: '/project/src/index.ts' } }] },
102
- });
103
- const log = readLog();
104
- expect(log).toContain('Read');
105
- expect(log).toContain('index.ts');
106
- });
107
- it('handles tool_use Write', () => {
108
- const fmt = new Formatter(LOG_FILE);
109
- fmt.handleMessage({
110
- type: 'assistant',
111
- message: { content: [{ type: 'tool_use', name: 'Write', input: { file_path: '/project/new-file.ts' } }] },
112
- });
113
- const log = readLog();
114
- expect(log).toContain('Write');
115
- expect(log).toContain('new-file.ts');
116
- });
117
- it('handles tool_use Edit', () => {
118
- const fmt = new Formatter(LOG_FILE);
119
- fmt.handleMessage({
120
- type: 'assistant',
121
- message: { content: [{ type: 'tool_use', name: 'Edit', input: { file_path: '/project/file.ts' } }] },
122
- });
123
- const log = readLog();
124
- expect(log).toContain('Edit');
125
- });
126
- it('handles tool_use Glob', () => {
127
- const fmt = new Formatter(LOG_FILE);
128
- fmt.handleMessage({
129
- type: 'assistant',
130
- message: { content: [{ type: 'tool_use', name: 'Glob', input: { pattern: '**/*.ts' } }] },
131
- });
132
- const log = readLog();
133
- expect(log).toContain('Glob');
134
- expect(log).toContain('**/*.ts');
135
- });
136
- it('handles tool_use Grep', () => {
137
- const fmt = new Formatter(LOG_FILE);
138
- fmt.handleMessage({
139
- type: 'assistant',
140
- message: { content: [{ type: 'tool_use', name: 'Grep', input: { pattern: 'TODO', path: 'src/' } }] },
141
- });
142
- const log = readLog();
143
- expect(log).toContain('Grep');
144
- expect(log).toContain('TODO');
145
- });
146
- it('handles Agent delegation', () => {
147
- const fmt = new Formatter(LOG_FILE);
148
- fmt.handleMessage({
149
- type: 'assistant',
150
- message: { content: [{ type: 'tool_use', name: 'Agent', input: { subagent_type: 'backend-ts-architect', description: 'Plan API design' } }] },
151
- });
152
- const log = readLog();
153
- expect(log).toContain('backend-ts-architect');
154
- expect(log).toContain('Plan API design');
155
- });
156
- it('handles tool results (short)', () => {
157
- const fmt = new Formatter(LOG_FILE);
158
- fmt.handleMessage({
159
- type: 'user',
160
- message: { content: [{ type: 'tool_result', content: 'file1.ts\nfile2.ts', is_error: false }] },
161
- });
162
- const log = readLog();
163
- expect(log).toContain('file1.ts');
164
- });
165
- it('handles tool results (long → line count)', () => {
166
- const fmt = new Formatter(LOG_FILE);
167
- const longOutput = Array.from({ length: 50 }, (_, i) => `line ${i}`).join('\n');
168
- fmt.handleMessage({
169
- type: 'user',
170
- message: { content: [{ type: 'tool_result', content: longOutput, is_error: false }] },
171
- });
172
- const log = readLog();
173
- expect(log).toContain('50 lines');
174
- });
175
- it('handles tool error results', () => {
176
- const fmt = new Formatter(LOG_FILE);
177
- fmt.handleMessage({
178
- type: 'user',
179
- message: { content: [{ type: 'tool_result', content: 'Permission denied', is_error: true }] },
180
- });
181
- const log = readLog();
182
- expect(log).toContain('Permission denied');
183
- });
184
- it('handles system task_progress', () => {
185
- const fmt = new Formatter(LOG_FILE);
186
- fmt.handleMessage({
187
- type: 'system',
188
- subtype: 'task_progress',
189
- description: 'Reading package.json',
190
- });
191
- const log = readLog();
192
- expect(log).toContain('Reading package.json');
193
- });
194
- it('handles result message', () => {
195
- const fmt = new Formatter(LOG_FILE);
196
- fmt.handleMessage({
197
- type: 'result',
198
- session_id: 'abc-123',
199
- });
200
- const log = readLog();
201
- expect(log).toContain('SESSION: abc-123');
202
- });
203
- it('getSessionId extracts session_id', () => {
204
- const fmt = new Formatter(LOG_FILE);
205
- expect(fmt.getSessionId({ session_id: 'test-id' })).toBe('test-id');
206
- expect(fmt.getSessionId({})).toBeNull();
207
- });
208
- it('ignores unknown message types silently', () => {
209
- const fmt = new Formatter(LOG_FILE);
210
- fmt.handleMessage({ type: 'stream_event', data: 'something' });
211
- const log = readLog();
212
- expect(log).toBe('');
213
- });
214
- it('handles multiple content blocks in one message', () => {
215
- const fmt = new Formatter(LOG_FILE);
216
- fmt.handleMessage({
217
- type: 'assistant',
218
- message: {
219
- content: [
220
- { type: 'text', text: 'Planning the implementation' },
221
- { type: 'tool_use', name: 'Read', input: { file_path: '/project/src/main.ts' } },
222
- { type: 'tool_use', name: 'Bash', input: { command: 'npm test' } },
223
- ],
224
- },
225
- });
226
- const log = readLog();
227
- expect(log).toContain('Planning the implementation');
228
- expect(log).toContain('Read');
229
- expect(log).toContain('Bash');
230
- expect(log).toContain('npm test');
231
- });
232
- it('tracks tokens from usage', () => {
233
- const fmt = new Formatter(LOG_FILE);
234
- fmt.iterationStart(1, 10);
235
- fmt.handleMessage({
236
- type: 'assistant',
237
- message: {
238
- content: [{ type: 'text', text: 'hi' }],
239
- usage: { input_tokens: 1000, output_tokens: 500 },
240
- },
241
- });
242
- fmt.iterationEnd();
243
- const calls = console.log.mock.calls.map((c) => c[0]);
244
- const summary = calls.join('\n');
245
- expect(summary).toContain('1.5K');
246
- });
247
- it('increments tool call counter', () => {
248
- const fmt = new Formatter(LOG_FILE);
249
- fmt.iterationStart(1, 10);
250
- fmt.handleMessage({
251
- type: 'assistant',
252
- message: { content: [
253
- { type: 'tool_use', name: 'Read', input: { file_path: 'a.ts' } },
254
- { type: 'tool_use', name: 'Read', input: { file_path: 'b.ts' } },
255
- ] },
256
- });
257
- fmt.iterationEnd();
258
- const calls = console.log.mock.calls.map((c) => c[0]);
259
- const summary = calls.join('\n');
260
- expect(summary).toContain('2 tools');
261
- });
262
- });
@@ -1,44 +0,0 @@
1
- // ─── Checkpoint state: crash recovery via .claude/autonomous-state.json ───
2
- import { readFileSync, writeFileSync, renameSync, unlinkSync } from 'node:fs';
3
- import { resolve } from 'node:path';
4
- const STATE_FILE = '.claude/autonomous-state.json';
5
- export function emptyStats() {
6
- return { startTime: Date.now(), iterations: 0, tasksCompleted: [] };
7
- }
8
- export function emptyCheckpoint() {
9
- return {
10
- iteration: 0,
11
- lastSessionId: null,
12
- lastTaskSeen: null,
13
- completionVerified: false,
14
- stats: emptyStats(),
15
- };
16
- }
17
- export function readCheckpoint(projectDir) {
18
- const path = resolve(projectDir, STATE_FILE);
19
- try {
20
- const raw = readFileSync(path, 'utf-8');
21
- const parsed = JSON.parse(raw);
22
- if (typeof parsed.iteration !== 'number')
23
- return null;
24
- const defaults = emptyCheckpoint();
25
- return { ...defaults, ...parsed, stats: { ...defaults.stats, ...parsed.stats } };
26
- }
27
- catch {
28
- return null;
29
- }
30
- }
31
- export function writeCheckpoint(projectDir, state) {
32
- const path = resolve(projectDir, STATE_FILE);
33
- const tmp = path + '.tmp';
34
- try {
35
- writeFileSync(tmp, JSON.stringify(state, null, 2) + '\n', 'utf-8');
36
- renameSync(tmp, path);
37
- }
38
- catch {
39
- try {
40
- unlinkSync(tmp);
41
- }
42
- catch { /* */ }
43
- }
44
- }
@@ -1,59 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
- import { mkdirSync, rmSync, writeFileSync, existsSync } from 'node:fs';
3
- import { resolve, dirname } from 'node:path';
4
- import { fileURLToPath } from 'node:url';
5
- import { emptyCheckpoint, emptyStats, readCheckpoint, writeCheckpoint } from './state.mjs';
6
- const TEST_DIR = resolve(dirname(fileURLToPath(import.meta.url)), '__test_tmp__');
7
- beforeEach(() => {
8
- mkdirSync(resolve(TEST_DIR, '.claude'), { recursive: true });
9
- });
10
- afterEach(() => {
11
- rmSync(TEST_DIR, { recursive: true, force: true });
12
- });
13
- describe('emptyStats', () => {
14
- it('creates stats with zero values', () => {
15
- const stats = emptyStats();
16
- expect(stats.iterations).toBe(0);
17
- expect(stats.tasksCompleted).toEqual([]);
18
- });
19
- });
20
- describe('emptyCheckpoint', () => {
21
- it('creates checkpoint with zero values', () => {
22
- const cp = emptyCheckpoint();
23
- expect(cp.iteration).toBe(0);
24
- expect(cp.lastSessionId).toBeNull();
25
- });
26
- });
27
- describe('writeCheckpoint + readCheckpoint', () => {
28
- it('round-trips checkpoint data', () => {
29
- const cp = emptyCheckpoint();
30
- cp.iteration = 5;
31
- cp.lastTaskSeen = '#42';
32
- writeCheckpoint(TEST_DIR, cp);
33
- const read = readCheckpoint(TEST_DIR);
34
- expect(read).not.toBeNull();
35
- expect(read.iteration).toBe(5);
36
- expect(read.lastTaskSeen).toBe('#42');
37
- });
38
- it('uses atomic write (tmp + rename)', () => {
39
- writeCheckpoint(TEST_DIR, emptyCheckpoint());
40
- expect(existsSync(resolve(TEST_DIR, '.claude/autonomous-state.json.tmp'))).toBe(false);
41
- expect(existsSync(resolve(TEST_DIR, '.claude/autonomous-state.json'))).toBe(true);
42
- });
43
- it('returns null for missing file', () => {
44
- expect(readCheckpoint(TEST_DIR)).toBeNull();
45
- });
46
- it('returns null for corrupted file', () => {
47
- writeFileSync(resolve(TEST_DIR, '.claude/autonomous-state.json'), '{corrupt!!!');
48
- expect(readCheckpoint(TEST_DIR)).toBeNull();
49
- });
50
- it('merges with defaults for forward compatibility', () => {
51
- const path = resolve(TEST_DIR, '.claude/autonomous-state.json');
52
- writeFileSync(path, JSON.stringify({ iteration: 3, lastSessionId: 'abc', stats: { iterations: 3, tasksCompleted: ['#1'] } }));
53
- const read = readCheckpoint(TEST_DIR);
54
- expect(read).not.toBeNull();
55
- expect(read.iteration).toBe(3);
56
- expect(read.completionVerified).toBe(false); // default
57
- expect(read.stats.tasksCompleted).toEqual(['#1']);
58
- });
59
- });
@@ -1,8 +0,0 @@
1
- node_modules
2
- .git
3
- dist
4
- tmp
5
- .angular
6
- .nx
7
- .npmrc
8
- .docker-compose.auth.yml