agent-termo 0.0.0-stage → 1.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/.claude/agents/backend-architecture-designer.md +1250 -0
  2. package/.claude/agents/backend-expert.md +1731 -0
  3. package/.claude/agents/deployment-expert.md +1126 -0
  4. package/.claude/agents/dev-environment-initiator.md +788 -0
  5. package/.claude/agents/frontend-expert.md +1343 -0
  6. package/.claude/agents/frontend-planner.md +1044 -0
  7. package/.claude/agents/git-manager.md +811 -0
  8. package/.claude/agents/project-manager.md +843 -0
  9. package/.claude/agents/security-expert.md +835 -0
  10. package/.claude/agents/tech-stack-expert.md +1357 -0
  11. package/.claude/agents/test-agent.md +1372 -0
  12. package/.claude/agents/uiux-designer.md +955 -0
  13. package/.claude/commands/build.md +5 -0
  14. package/.claude/commands/deploy.md +3 -0
  15. package/.claude/commands/feature.md +3 -0
  16. package/.claude/commands/fix.md +3 -0
  17. package/.claude/skills/pdf-reader/.skillfish.json +10 -0
  18. package/.claude/skills/pdf-reader/SKILL.md +94 -0
  19. package/.claude/skills/pdf-reader/pdf-reader +203 -0
  20. package/README.md +169 -3
  21. package/dist/.claude/agents/backend-architecture-designer.md +1250 -0
  22. package/dist/.claude/agents/backend-expert.md +1731 -0
  23. package/dist/.claude/agents/deployment-expert.md +1126 -0
  24. package/dist/.claude/agents/dev-environment-initiator.md +788 -0
  25. package/dist/.claude/agents/frontend-expert.md +1343 -0
  26. package/dist/.claude/agents/frontend-planner.md +1044 -0
  27. package/dist/.claude/agents/git-manager.md +811 -0
  28. package/dist/.claude/agents/project-manager.md +843 -0
  29. package/dist/.claude/agents/security-expert.md +835 -0
  30. package/dist/.claude/agents/tech-stack-expert.md +1357 -0
  31. package/dist/.claude/agents/test-agent.md +1372 -0
  32. package/dist/.claude/agents/uiux-designer.md +955 -0
  33. package/dist/.claude/commands/build.md +5 -0
  34. package/dist/.claude/commands/deploy.md +3 -0
  35. package/dist/.claude/commands/feature.md +3 -0
  36. package/dist/.claude/commands/fix.md +3 -0
  37. package/dist/.claude/skills/pdf-reader/.skillfish.json +10 -0
  38. package/dist/.claude/skills/pdf-reader/SKILL.md +94 -0
  39. package/dist/.claude/skills/pdf-reader/pdf-reader +203 -0
  40. package/dist/cli.js +206 -0
  41. package/dist/cli.js.map +7 -0
  42. package/dist/main.js +173 -0
  43. package/dist/main.js.map +7 -0
  44. package/package.json +89 -5
  45. package/src/agents/index.ts +94 -0
  46. package/src/cli.ts +76 -0
  47. package/src/diff/index.ts +255 -0
  48. package/src/llm/client.ts +192 -0
  49. package/src/llm/tools.ts +206 -0
  50. package/src/main.ts +427 -0
  51. package/src/memory/index.ts +130 -0
  52. package/src/tools/index.ts +371 -0
  53. package/src/types/index.ts +151 -0
  54. package/src/ui/index.ts +519 -0
  55. package/src/utils/ansi.ts +134 -0
  56. package/src/utils/config.ts +75 -0
package/src/main.ts ADDED
@@ -0,0 +1,427 @@
1
+ // =============================================================================
2
+ // Agent-Termo — Main REPL entry point
3
+ // =============================================================================
4
+
5
+ import { NimClient } from './llm/client.js';
6
+ import { TOOL_DEFINITIONS } from './llm/tools.js';
7
+ import { executeTool, applyFileChange } from './tools/index.js';
8
+ import { loadMemory, logToTimeline, logAgentTermoTimeline } from './memory/index.js';
9
+ import { loadAllAgents } from './agents/index.js';
10
+ import { buildSystemPrompt } from './agents/index.js';
11
+ import { resolveProjectRoot } from './utils/config.js';
12
+ import {
13
+ printWelcome,
14
+ printUserMessage,
15
+ printAssistantMessage,
16
+ printToolCall,
17
+ printToolResult,
18
+ printError,
19
+ printSuccess,
20
+ printInfo,
21
+ printHelp,
22
+ printAgentsList,
23
+ printMemoryFile,
24
+ printTimelineFile,
25
+ startSpinner,
26
+ stopSpinner,
27
+ clearScreen,
28
+ createInput,
29
+ moveToFreshLine,
30
+ promptAcceptReject,
31
+ } from './ui/index.js';
32
+ import type { ConversationMessage, ToolCall, ProposedFileChange } from './types/index.js';
33
+
34
+ const TOOL_KEYWORDS = [
35
+ 'build', 'create', 'write', 'edit', 'modify', 'file', 'code', 'function', 'class',
36
+ 'component', 'debug', 'fix', 'git', 'commit', 'push', 'install', 'npm', 'run',
37
+ 'bash', 'shell', 'command', 'read', 'grep', 'check', 'show', 'implement',
38
+ 'configure', 'setup', 'src', 'package', 'app', 'project', 'api', 'deploy',
39
+ 'test', 'compile', 'typescript', 'javascript', 'python', 'node', 'add', 'delete',
40
+ ];
41
+
42
+ function mightNeedTools(message: string): boolean {
43
+ const lower = message.toLowerCase();
44
+ return TOOL_KEYWORDS.some((w) => lower.includes(w));
45
+ }
46
+
47
+ function dedup(arr: ToolCall[]): ToolCall[] {
48
+ const seen = new Set<string>();
49
+ return arr.filter((tc) => {
50
+ if (seen.has(tc.id)) return false;
51
+ seen.add(tc.id);
52
+ return true;
53
+ });
54
+ }
55
+
56
+ function extractProposedChange(toolName: string, resultOutput: string): ProposedFileChange | undefined {
57
+ const isFileTool = toolName === 'file:write' || toolName === 'file:edit';
58
+ if (!isFileTool) return undefined;
59
+ try {
60
+ const parsed = JSON.parse(resultOutput);
61
+ if (parsed?.needsApproval && parsed?.proposedChange) {
62
+ return parsed.proposedChange as ProposedFileChange;
63
+ }
64
+ } catch {
65
+ // Not JSON — tool returned a simple string
66
+ }
67
+ return undefined;
68
+ }
69
+
70
+ interface InputController {
71
+ close(): void;
72
+ showPrompt(): void;
73
+ }
74
+
75
+ /**
76
+ * Open a VS Code diff window for the proposed change.
77
+ * Writes original + proposed content to temp files and launches `code --diff`.
78
+ */
79
+ async function openVsCodeDiff(change: ProposedFileChange): Promise<void> {
80
+ try {
81
+ const { exec } = await import('node:child_process');
82
+ const { promisify } = await import('node:util');
83
+ const { mkdtemp, writeFile } = await import('node:fs/promises');
84
+ const { basename } = await import('node:path');
85
+ const { tmpdir } = await import('node:os');
86
+
87
+ const execAsync = promisify(exec);
88
+
89
+ const originalPath = change.path;
90
+ const tmpDir = await mkdtemp(tmpdir() + '/agent-termo-diff-');
91
+ const tmpFile = tmpDir + '/' + basename(originalPath);
92
+
93
+ await writeFile(tmpFile, change.proposedContent, 'utf-8');
94
+
95
+ // Write original content to a temp file for diff display
96
+ if (!change.isNewFile && change.originalContent.length > 0) {
97
+ const originalTmp = tmpDir + '/original-' + basename(originalPath);
98
+ await writeFile(originalTmp, change.originalContent, 'utf-8');
99
+ execAsync(`code --diff "${originalTmp}" "${tmpFile}" --wait`).catch(() => {
100
+ // VS Code not available or user closed — continue silently
101
+ });
102
+ } else {
103
+ // New file: just open the proposed file
104
+ execAsync(`code "${tmpFile}" --wait`).catch(() => {
105
+ // Silently fall back
106
+ });
107
+ }
108
+ } catch {
109
+ // Silently fall back to terminal diff view
110
+ }
111
+ }
112
+
113
+ async function executeToolWithReview(
114
+ toolName: string,
115
+ args: Record<string, unknown>,
116
+ projectRoot: string,
117
+ input: InputController,
118
+ ): Promise<{ toolResult: { success: boolean; output?: string; error?: string }; change?: ProposedFileChange; applied: boolean }> {
119
+ console.log('');
120
+ printToolCall(toolName, args);
121
+ startSpinner('Running ' + toolName);
122
+ const toolResult = await executeTool(toolName, args, projectRoot);
123
+ stopSpinner(toolResult.success);
124
+
125
+ const proposedChange = extractProposedChange(toolName, toolResult.output ?? '');
126
+
127
+ if (proposedChange) {
128
+ // Pause REPL input to prevent stale listeners from interfering with promptAcceptReject
129
+ input.close();
130
+
131
+ // Show terminal diff viewer
132
+ printToolResult(toolName, { success: toolResult.success, output: toolResult.output, error: toolResult.error }, { needsApproval: true, proposedChange });
133
+
134
+ // Also open VS Code diff window (non-blocking, fire-and-forget)
135
+ openVsCodeDiff(proposedChange);
136
+
137
+ const decision = await promptAcceptReject('Accept this change?');
138
+
139
+ // Restore input for next REPL iteration
140
+ input.showPrompt();
141
+
142
+ if (decision === 'accept') {
143
+ startSpinner('Writing to disk');
144
+ const applyResult = await applyFileChange(proposedChange, projectRoot);
145
+ stopSpinner(applyResult.success);
146
+ if (applyResult.success) {
147
+ printSuccess(applyResult.output ?? 'Change applied');
148
+ return { toolResult: applyResult, change: proposedChange, applied: true };
149
+ } else {
150
+ printError(applyResult.error ?? 'Failed to apply change');
151
+ return { toolResult: applyResult, change: proposedChange, applied: false };
152
+ }
153
+ } else {
154
+ printInfo('Change rejected — file was not modified');
155
+ return { toolResult, change: proposedChange, applied: false };
156
+ }
157
+ }
158
+
159
+ printToolResult(toolName, toolResult);
160
+ return { toolResult, applied: false };
161
+ }
162
+
163
+ /**
164
+ * Run the tool-call loop: execute tools, feed results back to the LLM,
165
+ * and repeat until the LLM stops issuing tool calls.
166
+ */
167
+ async function runToolLoop(
168
+ conversation: ConversationMessage[],
169
+ projectRoot: string,
170
+ initialToolCalls: ToolCall[],
171
+ input: InputController,
172
+ nimClient: NimClient,
173
+ ): Promise<string> {
174
+ let toolCalls = initialToolCalls;
175
+ let finalText = '';
176
+
177
+ // Continue looping while the LLM keeps issuing tool calls
178
+ while (toolCalls.length > 0) {
179
+ // Execute all tool calls from this turn
180
+ for (const tc of toolCalls) {
181
+ const execution = await executeToolWithReview(tc.name, tc.arguments, projectRoot, input);
182
+
183
+ const toolResult = execution.toolResult;
184
+ const statusText = toolResult.success ? 'OK' : ('ERR: ' + (toolResult.error ? toolResult.error.slice(0, 80) : 'unknown error'));
185
+ const changeNote = execution.change ? (execution.applied ? ' (applied)' : ' (rejected)') : '';
186
+
187
+ conversation.push({
188
+ id: crypto.randomUUID(),
189
+ role: 'tool',
190
+ content: (toolResult.success ? 'Result: ' : 'Error: ') + (toolResult.success ? (toolResult.output ?? '') : toolResult.error) + changeNote,
191
+ timestamp: new Date(),
192
+ });
193
+
194
+ logToTimeline(projectRoot, 'Tool: ' + tc.name + ' -- ' + statusText + changeNote);
195
+ logAgentTermoTimeline('Tool: ' + tc.name + ' -- ' + statusText + changeNote);
196
+ }
197
+
198
+ // Send conversation (including tool results) back to the LLM
199
+ // The LLM will either produce text or more tool calls
200
+ moveToFreshLine();
201
+ startSpinner('Working...');
202
+ try {
203
+ const tools = nimClient.buildToolDefs(TOOL_DEFINITIONS);
204
+ const result = await nimClient.complete(conversation, tools);
205
+ finalText = result.content;
206
+ toolCalls = dedup(result.toolCalls);
207
+ process.stdout.write('\n');
208
+ stopSpinner(true);
209
+
210
+ // If the LLM produced text alongside tool calls, save it
211
+ if (finalText && finalText.trim() && finalText.trim() !== 'null') {
212
+ conversation.push({
213
+ id: crypto.randomUUID(),
214
+ role: 'assistant',
215
+ content: finalText.trim(),
216
+ timestamp: new Date(),
217
+ });
218
+ }
219
+ } catch (err) {
220
+ process.stdout.write('\n');
221
+ stopSpinner(false);
222
+ printError('LLM error: ' + (err instanceof Error ? err.message : String(err)));
223
+ break;
224
+ }
225
+ }
226
+
227
+ return finalText;
228
+ }
229
+
230
+ export async function runRepl(projectRoot?: string): Promise<void> {
231
+ projectRoot = resolveProjectRoot(projectRoot);
232
+
233
+ const memory = await loadMemory(projectRoot);
234
+ const nimClient = new NimClient(projectRoot);
235
+ const systemPrompt = buildSystemPrompt(memory, '', projectRoot);
236
+
237
+ const session = { id: crypto.randomUUID(), projectRoot, startedAt: new Date() };
238
+
239
+ const conversation: ConversationMessage[] = [
240
+ {
241
+ id: crypto.randomUUID(),
242
+ role: 'system',
243
+ content: systemPrompt,
244
+ timestamp: new Date(),
245
+ },
246
+ ];
247
+
248
+ printWelcome(projectRoot);
249
+ console.log('Model: ' + nimClient.getModel());
250
+ logAgentTermoTimeline('Agent-Termo session started. Model: ' + nimClient.getModel());
251
+ logToTimeline(projectRoot, 'Agent-Termo session started. Model: ' + nimClient.getModel());
252
+ logAgentTermoTimeline('Commands: /help /clear /memory /timeline /model /agents /history');
253
+ logToTimeline(projectRoot, 'Commands: /help /clear /memory /timeline /model /agents /history');
254
+
255
+ const input = createInput();
256
+
257
+ const cleanup = () => {
258
+ input.close();
259
+ process.stdout.write('\n');
260
+ console.log(
261
+ 'Session complete. ' +
262
+ Math.round((Date.now() - session.startedAt.getTime()) / 60000) +
263
+ ' minutes.',
264
+ );
265
+ process.exit(0);
266
+ };
267
+ process.on('SIGINT', cleanup);
268
+
269
+ mainLoop: while (true) {
270
+ try {
271
+ const userInput = await input.question('');
272
+ if (!userInput) continue;
273
+ if (userInput === 'exit' || userInput === 'quit') {
274
+ cleanup();
275
+ break;
276
+ }
277
+
278
+ if (userInput.startsWith('/')) {
279
+ const cmd = userInput.toLowerCase().trim();
280
+
281
+ if (cmd === '/help' || cmd === '/') {
282
+ printHelp();
283
+ continue mainLoop;
284
+ }
285
+ if (cmd === '/clear' || cmd === '/cls') {
286
+ conversation.length = 1;
287
+ clearScreen();
288
+ printWelcome(projectRoot);
289
+ printSuccess('Conversation cleared.');
290
+ continue mainLoop;
291
+ }
292
+ if (cmd === '/memory') {
293
+ await printMemoryFile();
294
+ continue mainLoop;
295
+ }
296
+ if (cmd === '/timeline' || cmd === '/tl') {
297
+ await printTimelineFile();
298
+ continue mainLoop;
299
+ }
300
+ if (cmd === '/model') {
301
+ printInfo('Current model: ' + nimClient.getModel());
302
+ continue mainLoop;
303
+ }
304
+ if (cmd === '/agents') {
305
+ const agents = loadAllAgents(process.env.AGENT_TERMO_DIR || '.claude');
306
+ printAgentsList(agents);
307
+ continue mainLoop;
308
+ }
309
+ if (cmd === '/history' || cmd === '/hist') {
310
+ printInfo('Conversation (excluding system): ' + (conversation.length - 1) + ' messages');
311
+ continue mainLoop;
312
+ }
313
+ if (cmd === '/exit' || cmd === '/quit') {
314
+ cleanup();
315
+ break;
316
+ }
317
+
318
+ console.log('');
319
+ printHelp();
320
+ console.log('Unknown command: ' + cmd);
321
+ console.log('Type /help for available commands.\n');
322
+ continue mainLoop;
323
+ }
324
+
325
+ const userMsg: ConversationMessage = {
326
+ id: crypto.randomUUID(),
327
+ role: 'user',
328
+ content: userInput,
329
+ timestamp: new Date(),
330
+ };
331
+ conversation.push(userMsg);
332
+ printUserMessage(userMsg);
333
+
334
+ moveToFreshLine();
335
+
336
+ const useTools = mightNeedTools(userInput);
337
+ const tools = useTools ? nimClient.buildToolDefs(TOOL_DEFINITIONS) : undefined;
338
+
339
+ if (useTools) {
340
+ startSpinner('Working...');
341
+ try {
342
+ const result = await nimClient.complete(conversation, tools);
343
+ const fullText = result.content;
344
+ let toolCalls = dedup(result.toolCalls);
345
+ stopSpinner(true);
346
+
347
+ if (toolCalls.length === 0) {
348
+ // No tool calls — just display the text response
349
+ const cleanText = (fullText ?? '').trim().replace(/^null$/, '').trim();
350
+ if (cleanText) {
351
+ conversation.push({
352
+ id: crypto.randomUUID(),
353
+ role: 'assistant',
354
+ content: cleanText,
355
+ timestamp: new Date(),
356
+ });
357
+ }
358
+ } else {
359
+ // Run the tool-call loop: execute tools, feed results back, repeat
360
+ const finalText = await runToolLoop(conversation, projectRoot, toolCalls, input, nimClient);
361
+ if (finalText && finalText.trim() && finalText.trim() !== 'null') {
362
+ printAssistantMessage(finalText.trim());
363
+ }
364
+ }
365
+ } catch (err) {
366
+ process.stdout.write('\n');
367
+ stopSpinner(false);
368
+ printError('LLM error: ' + (err instanceof Error ? err.message : String(err)));
369
+ conversation.pop();
370
+ continue mainLoop;
371
+ }
372
+ } else {
373
+ // No tools — stream the response
374
+ startSpinner('Thinking...');
375
+ try {
376
+ const gen = nimClient.streamComplete(conversation, undefined, (text: string) => {
377
+ process.stdout.write(text);
378
+ });
379
+ let receivedContent = false;
380
+ for await (const result of gen) {
381
+ if (result.content && result.content.trim()) {
382
+ receivedContent = true;
383
+ }
384
+ }
385
+ if (receivedContent) {
386
+ process.stdout.write('\n');
387
+ stopSpinner(true);
388
+ } else {
389
+ process.stdout.write('\n');
390
+ stopSpinner(undefined);
391
+ }
392
+ } catch {
393
+ process.stdout.write('\n');
394
+ stopSpinner(false);
395
+ startSpinner('Retrying...');
396
+ try {
397
+ const result = await nimClient.complete(conversation, undefined);
398
+ process.stdout.write('\n');
399
+ stopSpinner(true);
400
+ const cleanText = (result.content ?? '').trim();
401
+ if (cleanText) {
402
+ conversation.push({
403
+ id: crypto.randomUUID(),
404
+ role: 'assistant',
405
+ content: cleanText,
406
+ timestamp: new Date(),
407
+ });
408
+ }
409
+ } catch (err2) {
410
+ process.stdout.write('\n');
411
+ stopSpinner(false);
412
+ printError('LLM error: ' + (err2 instanceof Error ? err2.message : String(err2)));
413
+ conversation.pop();
414
+ continue mainLoop;
415
+ }
416
+ }
417
+ }
418
+
419
+ if (conversation.length > 42) {
420
+ conversation.splice(1, conversation.length - 42);
421
+ }
422
+ } catch (err) {
423
+ stopSpinner(false);
424
+ printError('Session error: ' + (err instanceof Error ? err.message : String(err)));
425
+ }
426
+ }
427
+ }
@@ -0,0 +1,130 @@
1
+ // =============================================================================
2
+ // Memory & Timeline Runtime
3
+ // =============================================================================
4
+
5
+ import { readFileSync as readFile, appendFileSync as appendFile, writeFileSync as writeFile, existsSync, mkdirSync } from 'node:fs';
6
+ import { resolve } from 'node:path';
7
+
8
+ // ── Agent-Termo Home Memory ──────────────────────────────────────────────────
9
+
10
+ function ensureAgentTermoDir(): string {
11
+ const dir = resolve(process.env.HOME || process.env.USERPROFILE || '.', '.agent-termo');
12
+ try {
13
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
14
+ } catch {
15
+ // non-fatal; callers will handle missing file
16
+ }
17
+ return dir;
18
+ }
19
+
20
+ const AGENT_TERMO_MEMORY_PATH = resolve(ensureAgentTermoDir(), 'memory.md');
21
+
22
+ export function loadAgentTermoMemory(): string {
23
+ if (!existsSync(AGENT_TERMO_MEMORY_PATH)) return '';
24
+ try {
25
+ return readFile(AGENT_TERMO_MEMORY_PATH, 'utf-8');
26
+ } catch {
27
+ return '';
28
+ }
29
+ }
30
+
31
+ export function appendToAgentTermoMemory(entry: string): void {
32
+ const timestamp = new Date().toISOString().split('T')[0];
33
+ const content = '\n\n## ' + timestamp + '\n\n' + entry;
34
+ try {
35
+ appendFile(AGENT_TERMO_MEMORY_PATH, content, 'utf-8');
36
+ } catch {
37
+ // ignore persistence failure
38
+ }
39
+ }
40
+
41
+ const AGENT_TERMO_TIMELINE_PATH = resolve(ensureAgentTermoDir(), 'timeline.md');
42
+
43
+ export function loadAgentTermoTimeline(): string {
44
+ if (!existsSync(AGENT_TERMO_TIMELINE_PATH)) return '';
45
+ try {
46
+ return readFile(AGENT_TERMO_TIMELINE_PATH, 'utf-8');
47
+ } catch {
48
+ return '';
49
+ }
50
+ }
51
+
52
+ export function logAgentTermoTimeline(message: string): void {
53
+ const timestamp = new Date().toISOString().replace('T', ' ').slice(0, 16);
54
+ const entry = '\n- [' + timestamp + '] ' + message;
55
+ try {
56
+ appendFile(AGENT_TERMO_TIMELINE_PATH, entry, 'utf-8');
57
+ } catch {
58
+ // ignore persistence failure
59
+ }
60
+ }
61
+
62
+ // ── Project Memory & Timeline Runtime ──────────────────────────────────────
63
+
64
+ export async function loadMemory(projectRoot: string): Promise<string> {
65
+ const path = resolve(projectRoot, 'docs', 'MEMORY.md');
66
+ if (!existsSync(path)) return '';
67
+ try {
68
+ return readFile(path, 'utf-8');
69
+ } catch {
70
+ return '';
71
+ }
72
+ }
73
+
74
+ export async function appendToMemory(projectRoot: string, entry: string): Promise<void> {
75
+ const path = resolve(projectRoot, 'docs', 'MEMORY.md');
76
+ const timestamp = new Date().toISOString().split('T')[0];
77
+ appendFile(path, '\n\n## ' + timestamp + '\n\n' + entry, 'utf-8');
78
+ }
79
+
80
+ // ── Timeline System ──────────────────────────────────────────────────────────
81
+
82
+ const TIMELINE_TEMPLATE = '# Project Timeline\n\nThis document tracks the complete chronological history of the project.\n\n---\n\n';
83
+
84
+ export async function loadTimeline(projectRoot: string): Promise<string> {
85
+ const path = resolve(projectRoot, 'docs', 'TIMELINE.md');
86
+ if (!existsSync(path)) {
87
+ try {
88
+ writeFile(path, TIMELINE_TEMPLATE, 'utf-8');
89
+ } catch {
90
+ // ignore
91
+ }
92
+ return '';
93
+ }
94
+ try {
95
+ return readFile(path, 'utf-8');
96
+ } catch {
97
+ return '';
98
+ }
99
+ }
100
+
101
+ export async function appendToTimeline(
102
+ projectRoot: string,
103
+ agent: string,
104
+ phase: string,
105
+ actions: string[],
106
+ notes: string[] = [],
107
+ ): Promise<void> {
108
+ const path = resolve(projectRoot, 'docs', 'TIMELINE.md');
109
+ const timestamp = new Date().toISOString().replace('T', ' ').slice(0, 16);
110
+
111
+ const entry =
112
+ '\n\n---\n\n## [' +
113
+ timestamp +
114
+ '] - ' +
115
+ agent +
116
+ '\n\n### Phase/Activity\n' +
117
+ phase +
118
+ '\n\n### Actions Taken\n' +
119
+ actions.map((a) => '- ' + a).join('\n') +
120
+ '\n\n### Notes\n' +
121
+ (notes.length > 0 ? notes.map((n) => '- ' + n).join('\n') : '- (none)');
122
+
123
+ appendFile(path, entry, 'utf-8');
124
+ }
125
+
126
+ export async function logToTimeline(projectRoot: string, message: string): Promise<void> {
127
+ const path = resolve(projectRoot, 'docs', 'TIMELINE.md');
128
+ const timestamp = new Date().toISOString().replace('T', ' ').slice(0, 16);
129
+ appendFile(path, '\n- [' + timestamp + '] ' + message, 'utf-8');
130
+ }