ashlrcode 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 (133) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +295 -0
  3. package/package.json +46 -0
  4. package/src/__tests__/branded-types.test.ts +47 -0
  5. package/src/__tests__/context.test.ts +163 -0
  6. package/src/__tests__/cost-tracker.test.ts +274 -0
  7. package/src/__tests__/cron.test.ts +197 -0
  8. package/src/__tests__/dream.test.ts +204 -0
  9. package/src/__tests__/error-handler.test.ts +192 -0
  10. package/src/__tests__/features.test.ts +69 -0
  11. package/src/__tests__/file-history.test.ts +177 -0
  12. package/src/__tests__/hooks.test.ts +145 -0
  13. package/src/__tests__/keybindings.test.ts +159 -0
  14. package/src/__tests__/model-patches.test.ts +82 -0
  15. package/src/__tests__/permissions-rules.test.ts +121 -0
  16. package/src/__tests__/permissions.test.ts +108 -0
  17. package/src/__tests__/project-config.test.ts +63 -0
  18. package/src/__tests__/retry.test.ts +321 -0
  19. package/src/__tests__/router.test.ts +158 -0
  20. package/src/__tests__/session-compact.test.ts +191 -0
  21. package/src/__tests__/session.test.ts +145 -0
  22. package/src/__tests__/skill-registry.test.ts +130 -0
  23. package/src/__tests__/speculation.test.ts +196 -0
  24. package/src/__tests__/tasks-v2.test.ts +267 -0
  25. package/src/__tests__/telemetry.test.ts +149 -0
  26. package/src/__tests__/tool-executor.test.ts +141 -0
  27. package/src/__tests__/tool-registry.test.ts +166 -0
  28. package/src/__tests__/undercover.test.ts +93 -0
  29. package/src/__tests__/workflow.test.ts +195 -0
  30. package/src/agent/async-context.ts +64 -0
  31. package/src/agent/context.ts +245 -0
  32. package/src/agent/cron.ts +189 -0
  33. package/src/agent/dream.ts +165 -0
  34. package/src/agent/error-handler.ts +108 -0
  35. package/src/agent/ipc.ts +256 -0
  36. package/src/agent/kairos.ts +207 -0
  37. package/src/agent/loop.ts +314 -0
  38. package/src/agent/model-patches.ts +68 -0
  39. package/src/agent/speculation.ts +219 -0
  40. package/src/agent/sub-agent.ts +125 -0
  41. package/src/agent/system-prompt.ts +231 -0
  42. package/src/agent/team.ts +220 -0
  43. package/src/agent/tool-executor.ts +162 -0
  44. package/src/agent/workflow.ts +189 -0
  45. package/src/agent/worktree-manager.ts +86 -0
  46. package/src/autopilot/queue.ts +186 -0
  47. package/src/autopilot/scanner.ts +245 -0
  48. package/src/autopilot/types.ts +58 -0
  49. package/src/bridge/bridge-client.ts +57 -0
  50. package/src/bridge/bridge-server.ts +81 -0
  51. package/src/cli.ts +1120 -0
  52. package/src/config/features.ts +51 -0
  53. package/src/config/git.ts +137 -0
  54. package/src/config/hooks.ts +201 -0
  55. package/src/config/permissions.ts +251 -0
  56. package/src/config/project-config.ts +63 -0
  57. package/src/config/remote-settings.ts +163 -0
  58. package/src/config/settings-sync.ts +170 -0
  59. package/src/config/settings.ts +113 -0
  60. package/src/config/undercover.ts +76 -0
  61. package/src/config/upgrade-notice.ts +65 -0
  62. package/src/mcp/client.ts +197 -0
  63. package/src/mcp/manager.ts +125 -0
  64. package/src/mcp/oauth.ts +252 -0
  65. package/src/mcp/types.ts +61 -0
  66. package/src/persistence/memory.ts +129 -0
  67. package/src/persistence/session.ts +289 -0
  68. package/src/planning/plan-mode.ts +128 -0
  69. package/src/planning/plan-tools.ts +138 -0
  70. package/src/providers/anthropic.ts +177 -0
  71. package/src/providers/cost-tracker.ts +184 -0
  72. package/src/providers/retry.ts +264 -0
  73. package/src/providers/router.ts +159 -0
  74. package/src/providers/types.ts +79 -0
  75. package/src/providers/xai.ts +217 -0
  76. package/src/repl.tsx +1384 -0
  77. package/src/setup.ts +119 -0
  78. package/src/skills/loader.ts +78 -0
  79. package/src/skills/registry.ts +78 -0
  80. package/src/skills/types.ts +11 -0
  81. package/src/state/file-history.ts +264 -0
  82. package/src/telemetry/event-log.ts +116 -0
  83. package/src/tools/agent.ts +133 -0
  84. package/src/tools/ask-user.ts +229 -0
  85. package/src/tools/bash.ts +146 -0
  86. package/src/tools/config.ts +147 -0
  87. package/src/tools/diff.ts +137 -0
  88. package/src/tools/file-edit.ts +123 -0
  89. package/src/tools/file-read.ts +82 -0
  90. package/src/tools/file-write.ts +82 -0
  91. package/src/tools/glob.ts +76 -0
  92. package/src/tools/grep.ts +187 -0
  93. package/src/tools/ls.ts +77 -0
  94. package/src/tools/lsp.ts +375 -0
  95. package/src/tools/mcp-resources.ts +83 -0
  96. package/src/tools/mcp-tool.ts +47 -0
  97. package/src/tools/memory.ts +148 -0
  98. package/src/tools/notebook-edit.ts +133 -0
  99. package/src/tools/peers.ts +113 -0
  100. package/src/tools/powershell.ts +83 -0
  101. package/src/tools/registry.ts +114 -0
  102. package/src/tools/send-message.ts +75 -0
  103. package/src/tools/sleep.ts +50 -0
  104. package/src/tools/snip.ts +143 -0
  105. package/src/tools/tasks.ts +349 -0
  106. package/src/tools/team.ts +309 -0
  107. package/src/tools/todo-write.ts +93 -0
  108. package/src/tools/tool-search.ts +83 -0
  109. package/src/tools/types.ts +52 -0
  110. package/src/tools/web-browser.ts +263 -0
  111. package/src/tools/web-fetch.ts +118 -0
  112. package/src/tools/web-search.ts +107 -0
  113. package/src/tools/workflow.ts +188 -0
  114. package/src/tools/worktree.ts +143 -0
  115. package/src/types/branded.ts +22 -0
  116. package/src/ui/App.tsx +184 -0
  117. package/src/ui/BuddyPanel.tsx +52 -0
  118. package/src/ui/PermissionPrompt.tsx +29 -0
  119. package/src/ui/banner.ts +217 -0
  120. package/src/ui/buddy-ai.ts +108 -0
  121. package/src/ui/buddy.ts +466 -0
  122. package/src/ui/context-bar.ts +60 -0
  123. package/src/ui/effort.ts +65 -0
  124. package/src/ui/keybindings.ts +143 -0
  125. package/src/ui/markdown.ts +271 -0
  126. package/src/ui/message-renderer.ts +73 -0
  127. package/src/ui/mode.ts +80 -0
  128. package/src/ui/notifications.ts +57 -0
  129. package/src/ui/speech-bubble.ts +95 -0
  130. package/src/ui/spinner.ts +116 -0
  131. package/src/ui/theme.ts +98 -0
  132. package/src/version.ts +5 -0
  133. package/src/voice/voice-mode.ts +169 -0
@@ -0,0 +1,256 @@
1
+ /**
2
+ * IPC — Inter-Process Communication via Unix Domain Sockets.
3
+ * Allows multiple AshlrCode instances to discover and message each other.
4
+ *
5
+ * Each running instance registers itself by writing a .json peer-info file
6
+ * and listening on a .sock Unix domain socket. Other instances discover
7
+ * peers by scanning the sockets directory and can send newline-delimited
8
+ * JSON messages over the socket.
9
+ */
10
+
11
+ import { createServer, connect, type Server, type Socket } from "net";
12
+ import { existsSync } from "fs";
13
+ import { readdir, readFile, writeFile, unlink, mkdir } from "fs/promises";
14
+ import { join } from "path";
15
+ import { getConfigDir } from "../config/settings.ts";
16
+ import { randomUUID } from "crypto";
17
+
18
+ // ── Types ────────────────────────────────────────────────────────────
19
+
20
+ export interface PeerInfo {
21
+ id: string;
22
+ pid: number;
23
+ cwd: string;
24
+ sessionId: string;
25
+ startedAt: string;
26
+ socketPath: string;
27
+ }
28
+
29
+ export interface IPCMessage {
30
+ from: string;
31
+ to: string;
32
+ type: "ping" | "pong" | "message" | "task" | "result";
33
+ payload: string;
34
+ timestamp: string;
35
+ }
36
+
37
+ // ── Internal state ───────────────────────────────────────────────────
38
+
39
+ function getSocketsDir(): string {
40
+ return join(getConfigDir(), "sockets");
41
+ }
42
+
43
+ function getSocketPath(id: string): string {
44
+ return join(getSocketsDir(), `${id}.sock`);
45
+ }
46
+
47
+ function getPeerInfoPath(id: string): string {
48
+ return join(getSocketsDir(), `${id}.json`);
49
+ }
50
+
51
+ let _server: Server | null = null;
52
+ let _peerId: string | null = null;
53
+ let _inbox: IPCMessage[] = [];
54
+ let _onMessage: ((msg: IPCMessage) => void) | null = null;
55
+
56
+ // ── Server lifecycle ─────────────────────────────────────────────────
57
+
58
+ /**
59
+ * Start listening for IPC messages on a Unix domain socket.
60
+ * Registers this instance as a discoverable peer.
61
+ */
62
+ export async function startIPCServer(
63
+ sessionId: string,
64
+ cwd: string,
65
+ onMessage?: (msg: IPCMessage) => void,
66
+ ): Promise<string> {
67
+ const dir = getSocketsDir();
68
+ await mkdir(dir, { recursive: true });
69
+
70
+ // Clean up sockets left behind by dead processes
71
+ await cleanStaleSockets();
72
+
73
+ _peerId = randomUUID().slice(0, 8);
74
+ _onMessage = onMessage ?? null;
75
+ const socketPath = getSocketPath(_peerId);
76
+
77
+ // Write peer info so other instances can discover us
78
+ const peerInfo: PeerInfo = {
79
+ id: _peerId,
80
+ pid: process.pid,
81
+ cwd,
82
+ sessionId,
83
+ startedAt: new Date().toISOString(),
84
+ socketPath,
85
+ };
86
+ await writeFile(getPeerInfoPath(_peerId), JSON.stringify(peerInfo), "utf-8");
87
+
88
+ // Create the UDS server
89
+ _server = createServer((socket: Socket) => {
90
+ let buffer = "";
91
+ socket.on("data", (data) => {
92
+ buffer += data.toString();
93
+ const lines = buffer.split("\n");
94
+ buffer = lines.pop() ?? ""; // keep incomplete trailing line
95
+ for (const line of lines) {
96
+ if (!line.trim()) continue;
97
+ try {
98
+ const msg = JSON.parse(line) as IPCMessage;
99
+ _inbox.push(msg);
100
+ _onMessage?.(msg);
101
+ } catch {
102
+ // Ignore malformed messages
103
+ }
104
+ }
105
+ });
106
+ });
107
+
108
+ // Remove leftover socket file if it exists
109
+ if (existsSync(socketPath)) {
110
+ await unlink(socketPath).catch(() => {});
111
+ }
112
+
113
+ _server.listen(socketPath);
114
+ return _peerId;
115
+ }
116
+
117
+ /**
118
+ * Stop the IPC server and remove this peer's registration files.
119
+ */
120
+ export async function stopIPCServer(): Promise<void> {
121
+ if (_server) {
122
+ _server.close();
123
+ _server = null;
124
+ }
125
+ if (_peerId) {
126
+ await unlink(getSocketPath(_peerId)).catch(() => {});
127
+ await unlink(getPeerInfoPath(_peerId)).catch(() => {});
128
+ _peerId = null;
129
+ }
130
+ _inbox = [];
131
+ _onMessage = null;
132
+ }
133
+
134
+ // ── Peer discovery ───────────────────────────────────────────────────
135
+
136
+ /**
137
+ * List all active AshlrCode peers (including self).
138
+ * Dead peers whose process no longer exists are excluded.
139
+ */
140
+ export async function listPeers(): Promise<PeerInfo[]> {
141
+ const dir = getSocketsDir();
142
+ if (!existsSync(dir)) return [];
143
+
144
+ const files = await readdir(dir);
145
+ const peers: PeerInfo[] = [];
146
+
147
+ for (const file of files.filter((f) => f.endsWith(".json"))) {
148
+ try {
149
+ const raw = await readFile(join(dir, file), "utf-8");
150
+ const peer = JSON.parse(raw) as PeerInfo;
151
+
152
+ // Verify the process is still alive (signal 0 = existence check)
153
+ try {
154
+ process.kill(peer.pid, 0);
155
+ peers.push(peer);
156
+ } catch {
157
+ // Process is dead — skip (cleanup happens on server start)
158
+ }
159
+ } catch {
160
+ // Corrupt file — skip
161
+ }
162
+ }
163
+
164
+ return peers;
165
+ }
166
+
167
+ // ── Messaging ────────────────────────────────────────────────────────
168
+
169
+ /**
170
+ * Send a message to a specific peer by ID.
171
+ * Returns true if the message was delivered, false otherwise.
172
+ */
173
+ export async function sendToPeer(
174
+ peerId: string,
175
+ type: IPCMessage["type"],
176
+ payload: string,
177
+ ): Promise<boolean> {
178
+ const peers = await listPeers();
179
+ const peer = peers.find((p) => p.id === peerId);
180
+ if (!peer) return false;
181
+
182
+ const msg: IPCMessage = {
183
+ from: _peerId ?? "unknown",
184
+ to: peerId,
185
+ type,
186
+ payload,
187
+ timestamp: new Date().toISOString(),
188
+ };
189
+
190
+ return new Promise<boolean>((resolve) => {
191
+ const socket = connect(peer.socketPath, () => {
192
+ socket.write(JSON.stringify(msg) + "\n");
193
+ socket.end();
194
+ resolve(true);
195
+ });
196
+ socket.on("error", () => resolve(false));
197
+ // 5-second timeout to avoid hanging
198
+ const timer = setTimeout(() => {
199
+ socket.destroy();
200
+ resolve(false);
201
+ }, 5000);
202
+ socket.on("close", () => clearTimeout(timer));
203
+ });
204
+ }
205
+
206
+ /**
207
+ * Read and clear the inbox — returns all messages received since last read.
208
+ */
209
+ export function readInbox(): IPCMessage[] {
210
+ const msgs = [..._inbox];
211
+ _inbox = [];
212
+ return msgs;
213
+ }
214
+
215
+ /**
216
+ * Peek at inbox without clearing it.
217
+ */
218
+ export function peekInbox(): readonly IPCMessage[] {
219
+ return _inbox;
220
+ }
221
+
222
+ /**
223
+ * Get this instance's peer ID (null if IPC server not started).
224
+ */
225
+ export function getPeerId(): string | null {
226
+ return _peerId;
227
+ }
228
+
229
+ // ── Maintenance ──────────────────────────────────────────────────────
230
+
231
+ /**
232
+ * Remove socket and info files for peers whose process is no longer alive.
233
+ */
234
+ async function cleanStaleSockets(): Promise<void> {
235
+ const dir = getSocketsDir();
236
+ if (!existsSync(dir)) return;
237
+
238
+ const files = await readdir(dir);
239
+
240
+ for (const file of files.filter((f) => f.endsWith(".json"))) {
241
+ try {
242
+ const raw = await readFile(join(dir, file), "utf-8");
243
+ const peer = JSON.parse(raw) as PeerInfo;
244
+ try {
245
+ process.kill(peer.pid, 0);
246
+ } catch {
247
+ // Process is dead — clean up both files
248
+ await unlink(join(dir, file)).catch(() => {});
249
+ await unlink(peer.socketPath).catch(() => {});
250
+ }
251
+ } catch {
252
+ // Corrupt info file — remove it
253
+ await unlink(join(dir, file)).catch(() => {});
254
+ }
255
+ }
256
+ }
@@ -0,0 +1,207 @@
1
+ /**
2
+ * KAIROS — Autonomous Agent Mode.
3
+ *
4
+ * Heartbeat-driven loop that keeps the agent alive between user inputs.
5
+ * Terminal focus detection adjusts autonomy level:
6
+ * - focused → collaborative (ask before big changes)
7
+ * - unfocused → full-auto (lean into autonomous action)
8
+ * - unknown → autonomous (balanced default)
9
+ */
10
+
11
+ import { runAgentLoop } from "./loop.ts";
12
+ import type { ProviderRouter } from "../providers/router.ts";
13
+ import type { ToolRegistry } from "../tools/registry.ts";
14
+ import type { ToolContext } from "../tools/types.ts";
15
+ import type { Message } from "../providers/types.ts";
16
+
17
+ /* ── Configuration ──────────────────────────────────────────────── */
18
+
19
+ export interface KairosConfig {
20
+ router: ProviderRouter;
21
+ toolRegistry: ToolRegistry;
22
+ toolContext: ToolContext;
23
+ systemPrompt: string;
24
+ /** Milliseconds between autonomous heartbeats. Default: 30_000 */
25
+ heartbeatIntervalMs: number;
26
+ /** Max tool-loop iterations per heartbeat tick. Default: 5 */
27
+ maxAutonomousIterations: number;
28
+ onOutput: (text: string) => void;
29
+ onToolStart?: (name: string, input: Record<string, unknown>) => void;
30
+ onToolEnd?: (name: string, result: string, isError: boolean) => void;
31
+ }
32
+
33
+ export type AutonomyLevel = "collaborative" | "autonomous" | "full-auto";
34
+
35
+ /* ── Focus detection ────────────────────────────────────────────── */
36
+
37
+ export type FocusState = "focused" | "unfocused" | "unknown";
38
+
39
+ /**
40
+ * Detect whether a terminal application is the frontmost window.
41
+ * macOS: uses osascript. Linux: uses xdotool. Otherwise: unknown.
42
+ */
43
+ export async function detectTerminalFocus(): Promise<FocusState> {
44
+ try {
45
+ const platform = process.platform;
46
+
47
+ if (platform === "darwin") {
48
+ const proc = Bun.spawn(
49
+ ["osascript", "-e", 'tell application "System Events" to get name of first process whose frontmost is true'],
50
+ { stdout: "pipe", stderr: "pipe" },
51
+ );
52
+ const output = (await new Response(proc.stdout).text()).trim();
53
+ await proc.exited;
54
+ const terminals = ["Terminal", "iTerm2", "Alacritty", "kitty", "Warp", "Hyper", "WezTerm"];
55
+ return terminals.some(t => output.includes(t)) ? "focused" : "unfocused";
56
+ }
57
+
58
+ if (platform === "linux") {
59
+ const proc = Bun.spawn(
60
+ ["bash", "-c", "xdotool getactivewindow getwindowname 2>/dev/null || echo unknown"],
61
+ { stdout: "pipe", stderr: "pipe" },
62
+ );
63
+ const output = (await new Response(proc.stdout).text()).trim().toLowerCase();
64
+ await proc.exited;
65
+ if (output === "unknown") return "unknown";
66
+ const terminals = ["terminal", "konsole", "alacritty", "kitty", "tmux", "screen"];
67
+ return terminals.some(t => output.includes(t)) ? "focused" : "unfocused";
68
+ }
69
+
70
+ return "unknown";
71
+ } catch {
72
+ return "unknown";
73
+ }
74
+ }
75
+
76
+ /* ── Autonomy helpers ───────────────────────────────────────────── */
77
+
78
+ function getAutonomyLevel(focusState: FocusState): AutonomyLevel {
79
+ switch (focusState) {
80
+ case "unfocused": return "full-auto";
81
+ case "focused": return "collaborative";
82
+ default: return "autonomous";
83
+ }
84
+ }
85
+
86
+ function getAutonomyPrompt(level: AutonomyLevel): string {
87
+ switch (level) {
88
+ case "full-auto":
89
+ return "\n\n[AUTONOMOUS MODE — User is away]\nLean heavily into autonomous action. Read files, make changes, commit, push. Decide independently. Only pause for truly irreversible or ambiguous decisions.";
90
+ case "collaborative":
91
+ return "\n\n[COLLABORATIVE MODE — User is watching]\nBe more collaborative. Ask before making significant changes. Explain your reasoning. Use AskUser for decisions.";
92
+ case "autonomous":
93
+ return "\n\n[AUTONOMOUS MODE]\nYou are running autonomously. Complete tasks independently but use good judgment about what requires confirmation.";
94
+ }
95
+ }
96
+
97
+ /* ── KAIROS loop ────────────────────────────────────────────────── */
98
+
99
+ export class KairosLoop {
100
+ private running = false;
101
+ private timer: ReturnType<typeof setInterval> | null = null;
102
+ private focusTimer: ReturnType<typeof setInterval> | null = null;
103
+ private history: Message[] = [];
104
+ private config: KairosConfig;
105
+ private tickCount = 0;
106
+ private _focusState: FocusState = "unknown";
107
+
108
+ constructor(config: KairosConfig) {
109
+ this.config = config;
110
+ }
111
+
112
+ /** Kick off the autonomous loop with an initial goal. */
113
+ async start(initialGoal: string): Promise<void> {
114
+ if (this.running) return;
115
+ this.running = true;
116
+ this.tickCount = 0;
117
+
118
+ this.config.onOutput(
119
+ ` KAIROS active — heartbeat every ${this.config.heartbeatIntervalMs / 1000}s\n`,
120
+ );
121
+
122
+ // Poll terminal focus every 10 s
123
+ this.focusTimer = setInterval(async () => {
124
+ this._focusState = await detectTerminalFocus();
125
+ }, 10_000);
126
+
127
+ // Initial goal execution
128
+ await this.executeTick(initialGoal);
129
+
130
+ // Recurring heartbeat
131
+ this.timer = setInterval(() => {
132
+ if (this.running) this.heartbeat().catch(() => {});
133
+ }, this.config.heartbeatIntervalMs);
134
+ }
135
+
136
+ /** Gracefully stop the loop. */
137
+ async stop(): Promise<void> {
138
+ this.running = false;
139
+ if (this.timer) { clearInterval(this.timer); this.timer = null; }
140
+ if (this.focusTimer) { clearInterval(this.focusTimer); this.focusTimer = null; }
141
+ this.config.onOutput(" KAIROS stopped\n");
142
+ }
143
+
144
+ isRunning(): boolean {
145
+ return this.running;
146
+ }
147
+
148
+ /** Inject a user message into the running autonomous loop. */
149
+ async injectMessage(message: string): Promise<void> {
150
+ await this.executeTick(message);
151
+ }
152
+
153
+ /* ── internals ──────────────────────────────────────────────── */
154
+
155
+ private async heartbeat(): Promise<void> {
156
+ if (!this.running) return;
157
+ this.tickCount++;
158
+
159
+ const level = getAutonomyLevel(this._focusState);
160
+ const tick = [
161
+ `<tick count="${this.tickCount}" focus="${this._focusState}" autonomy="${level}" time="${new Date().toISOString()}">`,
162
+ "Continue your current work. Check task list for pending items.",
163
+ "If nothing is pending, check if there are improvements to make or tests to run.",
164
+ ].join("\n");
165
+
166
+ this.config.onOutput(` tick #${this.tickCount} [${level}]\n`);
167
+ await this.executeTick(tick);
168
+ }
169
+
170
+ private async executeTick(prompt: string): Promise<void> {
171
+ const level = getAutonomyLevel(this._focusState);
172
+ const autonomyPrompt = getAutonomyPrompt(level);
173
+
174
+ try {
175
+ const result = await runAgentLoop(prompt, this.history, {
176
+ systemPrompt: this.config.systemPrompt + autonomyPrompt,
177
+ router: this.config.router,
178
+ toolRegistry: this.config.toolRegistry,
179
+ toolContext: this.config.toolContext,
180
+ maxIterations: this.config.maxAutonomousIterations,
181
+ onText: (text) => this.config.onOutput(text),
182
+ onToolStart: this.config.onToolStart,
183
+ onToolEnd: this.config.onToolEnd,
184
+ });
185
+
186
+ this.history = result.messages;
187
+
188
+ // Cap history to prevent unbounded growth
189
+ const MAX_HISTORY = 100;
190
+ if (this.history.length > MAX_HISTORY) {
191
+ this.history = this.history.slice(-MAX_HISTORY);
192
+ }
193
+
194
+ // Auto-stop if the model signals completion
195
+ if (
196
+ result.finalText.includes("[KAIROS_STOP]") ||
197
+ result.finalText.includes("nothing left to do")
198
+ ) {
199
+ await this.stop();
200
+ }
201
+ } catch (err) {
202
+ this.config.onOutput(
203
+ ` KAIROS error: ${err instanceof Error ? err.message : String(err)}\n`,
204
+ );
205
+ }
206
+ }
207
+ }