killeros 1.4.9 → 1.5.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.
@@ -0,0 +1,66 @@
1
+ import { closeSync, existsSync, openSync, readFileSync, readSync } from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { CONFIG_DIR_NAME, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
6
+ import type { InitRuntime } from "./runtime.ts";
7
+
8
+ const PERSONAL_INSTRUCTIONS_FILE = "AGENTS.local.md";
9
+ const PERSONAL_INSTRUCTIONS_LIMIT = 32 * 1024;
10
+
11
+ function readBoundedText(filePath: string, limit = PERSONAL_INSTRUCTIONS_LIMIT): string | undefined {
12
+ let descriptor: number | undefined;
13
+ try {
14
+ descriptor = openSync(filePath, "r");
15
+ const buffer = Buffer.alloc(limit + 1);
16
+ const bytesRead = readSync(descriptor, buffer, 0, buffer.length, 0);
17
+ const content = buffer.toString("utf8", 0, Math.min(bytesRead, limit));
18
+ if (!content.trim()) return undefined;
19
+ return bytesRead > limit
20
+ ? `${content}\n\n[Personal instructions truncated by KillerOS]`
21
+ : content;
22
+ } catch {
23
+ return undefined;
24
+ } finally {
25
+ if (descriptor !== undefined) {
26
+ try {
27
+ closeSync(descriptor);
28
+ } catch {
29
+ // Ignore cleanup failures after a bounded best-effort read.
30
+ }
31
+ }
32
+ }
33
+ }
34
+
35
+ export function resolvePersonalInstructions(cwd: string): { content: string; source: string } | undefined {
36
+ const localPath = path.join(cwd, PERSONAL_INSTRUCTIONS_FILE);
37
+ const local = readBoundedText(localPath);
38
+ if (!local) return undefined;
39
+
40
+ const importMatch = local.trim().match(/^@(.+)$/u);
41
+ if (!importMatch) return { content: local, source: localPath };
42
+
43
+ const requestedPath = importMatch[1]!.trim();
44
+ const importedPath = requestedPath.startsWith("~/") || requestedPath.startsWith("~\\")
45
+ ? path.join(os.homedir(), requestedPath.slice(2))
46
+ : path.resolve(cwd, requestedPath);
47
+ const imported = readBoundedText(importedPath);
48
+ return imported ? { content: imported, source: importedPath } : { content: local, source: localPath };
49
+ }
50
+
51
+ export function registerPersonalInstructions(pi: ExtensionAPI, initState: InitRuntime): void {
52
+ pi.on("before_agent_start", (event, ctx) => {
53
+ if (initState.active || !ctx.isProjectTrusted()) return;
54
+ const personal = resolvePersonalInstructions(ctx.cwd);
55
+ if (!personal) return;
56
+ return {
57
+ systemPrompt: [
58
+ event.systemPrompt,
59
+ "",
60
+ `<personal_instructions source="${personal.source}">`,
61
+ personal.content,
62
+ "</personal_instructions>",
63
+ ].join("\n"),
64
+ };
65
+ });
66
+ }
@@ -0,0 +1,468 @@
1
+ import { type ExtensionAPI, type ThemeColor } from "@earendil-works/pi-coding-agent";
2
+ import {
3
+ Container,
4
+ decodeKittyPrintable,
5
+ Editor,
6
+ Key,
7
+ Markdown,
8
+ matchesKey,
9
+ SelectList,
10
+ Text,
11
+ truncateToWidth,
12
+ visibleWidth,
13
+ wrapTextWithAnsi,
14
+ type EditorTheme,
15
+ } from "@earendil-works/pi-tui";
16
+ import { Type } from "typebox";
17
+
18
+ const OptionSchema = Type.Object({
19
+ label: Type.String({ minLength: 1, maxLength: 200, description: "Display label for the option" }),
20
+ description: Type.Optional(Type.String({ maxLength: 500, description: "Optional detail shown for the selected option" })),
21
+ preview: Type.Optional(Type.String({ maxLength: 8_000, description: "Optional markdown proposal preview shown for the selected option" })),
22
+ });
23
+
24
+ const QuestionParams = Type.Object({
25
+ question: Type.String({ minLength: 1, maxLength: 1_000, description: "The question to ask the user" }),
26
+ options: Type.Array(OptionSchema, {
27
+ minItems: 1,
28
+ maxItems: 9,
29
+ description: "Between 1 and 9 options for the user to choose from",
30
+ }),
31
+ });
32
+
33
+ interface DisplayOption {
34
+ label: string;
35
+ description?: string;
36
+ preview?: string;
37
+ originalIndex: number;
38
+ isOther: boolean;
39
+ }
40
+
41
+ interface QuestionDetails {
42
+ question: string;
43
+ options: string[];
44
+ answer: string | null;
45
+ selectedIndex?: number;
46
+ wasCustom?: boolean;
47
+ cancelled?: boolean;
48
+ }
49
+
50
+ type QuestionSelection =
51
+ | { kind: "selected"; answer: string; originalIndex: number }
52
+ | { kind: "custom"; answer: string }
53
+ | { kind: "cancelled" }
54
+ | { kind: "aborted" };
55
+
56
+ const CUSTOM_INPUT_MAX_CHARACTERS = 4_000;
57
+ const CUSTOM_INPUT_HISTORY_LIMIT = 100;
58
+ const CUSTOM_INPUT_HISTORY_BYTES = 64 * 1024;
59
+
60
+ function isPrintableInput(data: string): boolean {
61
+ return data.length > 0 && !/[\u0000-\u001F\u007F-\u009F]/u.test(data);
62
+ }
63
+
64
+ const graphemeSegmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" });
65
+
66
+ function decodeQuestionFilterInput(data: string): string | undefined {
67
+ const kittyPrintable = decodeKittyPrintable(data);
68
+ if (kittyPrintable !== undefined) return isPrintableInput(kittyPrintable) ? kittyPrintable : undefined;
69
+
70
+ const pasteStart = "\x1B[200~";
71
+ const pasteEnd = "\x1B[201~";
72
+ const startIndex = data.indexOf(pasteStart);
73
+ const endIndex = data.indexOf(pasteEnd, startIndex + pasteStart.length);
74
+ if (startIndex >= 0 && endIndex >= 0) {
75
+ return data
76
+ .slice(startIndex + pasteStart.length, endIndex)
77
+ .replace(/\r\n|\r|\n/gu, "")
78
+ .replace(/\t/gu, " ")
79
+ .replace(/[\u0000-\u001F\u007F-\u009F]/gu, "");
80
+ }
81
+
82
+ return isPrintableInput(data) ? data : undefined;
83
+ }
84
+
85
+ function removeLastGrapheme(value: string): string {
86
+ const segments = [...graphemeSegmenter.segment(value)];
87
+ const last = segments.at(-1);
88
+ return last ? value.slice(0, last.index) : "";
89
+ }
90
+
91
+ export function registerQuestionTool(pi: ExtensionAPI): void {
92
+ const customInputHistory: string[] = [];
93
+ let customInputHistoryBytes = 0;
94
+ const clearCustomInputHistory = (): void => {
95
+ customInputHistory.length = 0;
96
+ customInputHistoryBytes = 0;
97
+ };
98
+ const rememberCustomInput = (value: string): boolean => {
99
+ const bytes = Buffer.byteLength(value, "utf8");
100
+ if (bytes > CUSTOM_INPUT_HISTORY_BYTES) return false;
101
+ const existingIndex = customInputHistory.indexOf(value);
102
+ if (existingIndex >= 0) {
103
+ customInputHistoryBytes -= Buffer.byteLength(customInputHistory[existingIndex]!, "utf8");
104
+ customInputHistory.splice(existingIndex, 1);
105
+ }
106
+ while (customInputHistory.length >= CUSTOM_INPUT_HISTORY_LIMIT || customInputHistoryBytes + bytes > CUSTOM_INPUT_HISTORY_BYTES) {
107
+ const removed = customInputHistory.shift();
108
+ if (removed !== undefined) customInputHistoryBytes -= Buffer.byteLength(removed, "utf8");
109
+ }
110
+ customInputHistory.push(value);
111
+ customInputHistoryBytes += bytes;
112
+ return true;
113
+ };
114
+ const inputCharacterCount = (value: string): number => {
115
+ let count = 0;
116
+ for (const _character of value) count += 1;
117
+ return count;
118
+ };
119
+ pi.on("session_start", clearCustomInputHistory);
120
+ pi.on("session_tree", clearCustomInputHistory);
121
+ pi.on("session_shutdown", clearCustomInputHistory);
122
+
123
+ pi.registerTool<typeof QuestionParams, QuestionDetails>({
124
+ name: "question",
125
+ label: "Question",
126
+ description: "Ask one interactive multiple-choice question. Provide 1-9 concise options. The user can filter options or type a custom answer.",
127
+ promptSnippet: "Ask the user one multiple-choice question when a decision is required to proceed",
128
+ promptGuidelines: [
129
+ "Use question only when user input is required to choose between concrete alternatives; do not use question for rhetorical or optional follow-up prompts.",
130
+ ],
131
+ parameters: QuestionParams,
132
+ executionMode: "sequential",
133
+
134
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
135
+ if (ctx.mode !== "tui") throw new Error("The question tool requires interactive TUI mode");
136
+ if (signal?.aborted) throw new Error("Question cancelled before it opened");
137
+
138
+ const options: DisplayOption[] = [
139
+ ...params.options.map((option, index) => ({
140
+ label: option.label,
141
+ description: option.description,
142
+ preview: option.preview,
143
+ originalIndex: index + 1,
144
+ isOther: false,
145
+ })),
146
+ {
147
+ label: "Type a custom answer",
148
+ originalIndex: params.options.length + 1,
149
+ isOther: true,
150
+ },
151
+ ];
152
+
153
+ let finishFromAbort: (() => void) | undefined;
154
+ const resultPromise = ctx.ui.custom<QuestionSelection>((tui, theme, _keybindings, done) => {
155
+ let optionIndex = 0;
156
+ let editMode = false;
157
+ let filterQuery = "";
158
+ let cachedWidth: number | undefined;
159
+ let cachedLines: string[] | undefined;
160
+ let completed = false;
161
+
162
+ const finish = (selection: QuestionSelection): void => {
163
+ if (completed) return;
164
+ completed = true;
165
+ done(selection);
166
+ };
167
+ finishFromAbort = () => finish({ kind: "aborted" });
168
+
169
+ const editorTheme: EditorTheme = {
170
+ borderColor: (text) => theme.fg("accent", text),
171
+ selectList: {
172
+ selectedPrefix: (text) => theme.fg("accent", text),
173
+ selectedText: (text) => theme.fg("accent", text),
174
+ description: (text) => theme.fg("muted", text),
175
+ scrollInfo: (text) => theme.fg("dim", text),
176
+ noMatch: (text) => theme.fg("warning", text),
177
+ },
178
+ };
179
+ const editor = new Editor(tui, editorTheme);
180
+ customInputHistory.forEach((value) => editor.addToHistory(value));
181
+
182
+ const filteredOptions = (): DisplayOption[] => {
183
+ const query = filterQuery.trim().toLocaleLowerCase();
184
+ return options.filter((option) => option.isOther
185
+ || query.length === 0
186
+ || option.label.toLocaleLowerCase().includes(query)
187
+ || option.description?.toLocaleLowerCase().includes(query));
188
+ };
189
+
190
+ const invalidate = (): void => {
191
+ cachedWidth = undefined;
192
+ cachedLines = undefined;
193
+ editor.invalidate();
194
+ };
195
+
196
+ const refresh = (): void => {
197
+ invalidate();
198
+ tui.requestRender();
199
+ };
200
+
201
+ editor.onSubmit = (value) => {
202
+ const answer = value.trim();
203
+ if (answer) {
204
+ if (inputCharacterCount(answer) > CUSTOM_INPUT_MAX_CHARACTERS) {
205
+ ctx.ui.notify(`Custom answers are limited to ${CUSTOM_INPUT_MAX_CHARACTERS} characters`, "error");
206
+ return;
207
+ }
208
+ if (!rememberCustomInput(answer)) {
209
+ ctx.ui.notify(`Custom answer history is limited to ${CUSTOM_INPUT_HISTORY_BYTES} bytes`, "error");
210
+ return;
211
+ }
212
+ finish({ kind: "custom", answer });
213
+ return;
214
+ }
215
+ editMode = false;
216
+ editor.setText("");
217
+ refresh();
218
+ };
219
+
220
+ const enterCustomMode = (): void => {
221
+ editMode = true;
222
+ refresh();
223
+ };
224
+
225
+ const handleInput = (data: string): void => {
226
+ if (editMode) {
227
+ if (matchesKey(data, Key.escape)) {
228
+ editMode = false;
229
+ editor.setText("");
230
+ refresh();
231
+ return;
232
+ }
233
+ const before = editor.getExpandedText();
234
+ editor.handleInput(data);
235
+ const after = editor.getExpandedText();
236
+ if (inputCharacterCount(after) > CUSTOM_INPUT_MAX_CHARACTERS) {
237
+ editor.setText(before);
238
+ ctx.ui.notify(`Custom answers are limited to ${CUSTOM_INPUT_MAX_CHARACTERS} characters`, "error");
239
+ }
240
+ refresh();
241
+ return;
242
+ }
243
+
244
+ const visibleOptions = filteredOptions();
245
+ if (optionIndex >= visibleOptions.length) optionIndex = Math.max(0, visibleOptions.length - 1);
246
+ if (matchesKey(data, Key.up)) {
247
+ optionIndex = Math.max(0, optionIndex - 1);
248
+ refresh();
249
+ return;
250
+ }
251
+ if (matchesKey(data, Key.down)) {
252
+ optionIndex = Math.min(visibleOptions.length - 1, optionIndex + 1);
253
+ refresh();
254
+ return;
255
+ }
256
+ if (matchesKey(data, Key.enter)) {
257
+ const selected = visibleOptions[optionIndex];
258
+ if (!selected) return;
259
+ if (selected.isOther) enterCustomMode();
260
+ else finish({ kind: "selected", answer: selected.label, originalIndex: selected.originalIndex });
261
+ return;
262
+ }
263
+ if (matchesKey(data, Key.escape)) {
264
+ if (filterQuery) {
265
+ filterQuery = "";
266
+ optionIndex = 0;
267
+ refresh();
268
+ } else {
269
+ finish({ kind: "cancelled" });
270
+ }
271
+ return;
272
+ }
273
+ if (matchesKey(data, Key.backspace)) {
274
+ if (filterQuery) {
275
+ filterQuery = removeLastGrapheme(filterQuery);
276
+ optionIndex = 0;
277
+ refresh();
278
+ }
279
+ return;
280
+ }
281
+ const printableInput = decodeQuestionFilterInput(data);
282
+ const isPasteInput = data.includes("\x1B[200~");
283
+ if (!isPasteInput && printableInput && /^[1-9]$/.test(printableInput)) {
284
+ const selected = visibleOptions[Number(printableInput) - 1];
285
+ if (!selected) return;
286
+ if (selected.isOther) enterCustomMode();
287
+ else finish({ kind: "selected", answer: selected.label, originalIndex: selected.originalIndex });
288
+ return;
289
+ }
290
+ if (printableInput) {
291
+ filterQuery += printableInput;
292
+ optionIndex = 0;
293
+ refresh();
294
+ }
295
+ };
296
+
297
+ const render = (width: number): string[] => {
298
+ const renderWidth = Math.max(1, width);
299
+ if (cachedLines && cachedWidth === renderWidth) return cachedLines;
300
+ const lines: string[] = [];
301
+ const addWrapped = (text: string): void => {
302
+ lines.push(...wrapTextWithAnsi(text, renderWidth));
303
+ };
304
+ const addWrappedWithPrefix = (prefix: string, text: string): void => {
305
+ const prefixWidth = visibleWidth(prefix);
306
+ if (prefixWidth >= renderWidth) {
307
+ addWrapped(prefix + text);
308
+ return;
309
+ }
310
+ const wrapped = wrapTextWithAnsi(text, renderWidth - prefixWidth);
311
+ const continuation = " ".repeat(prefixWidth);
312
+ wrapped.forEach((line, index) => lines.push(`${index === 0 ? prefix : continuation}${line}`));
313
+ };
314
+
315
+ lines.push(theme.fg("accent", "─".repeat(renderWidth)));
316
+ addWrappedWithPrefix(" ", theme.fg("text", params.question));
317
+ lines.push("");
318
+ if (!editMode && filterQuery) {
319
+ addWrappedWithPrefix(" ", `${theme.fg("muted", "Filter: ")}${theme.fg("accent", filterQuery)}`);
320
+ lines.push("");
321
+ }
322
+
323
+ const visibleOptions = filteredOptions();
324
+ if (optionIndex >= visibleOptions.length) optionIndex = Math.max(0, visibleOptions.length - 1);
325
+ visibleOptions.forEach((option, index) => {
326
+ const selected = index === optionIndex;
327
+ const prefix = selected ? theme.fg("accent", "> ") : " ";
328
+ const color: ThemeColor = selected ? "accent" : "text";
329
+ addWrappedWithPrefix(prefix, theme.fg(color, `${index + 1}. ${option.label}`));
330
+ if (selected && option.description) {
331
+ addWrappedWithPrefix(" ", theme.fg("muted", option.description));
332
+ }
333
+ });
334
+
335
+ const selectedPreview = visibleOptions[optionIndex]?.preview;
336
+ if (!editMode && selectedPreview) {
337
+ const footerRows = 3;
338
+ const previewChromeRows = 2;
339
+ const availableRows = tui.terminal.rows - lines.length - footerRows;
340
+ if (availableRows > previewChromeRows) {
341
+ lines.push("");
342
+ addWrappedWithPrefix(" ", theme.fg("accent", theme.bold("Proposal preview")));
343
+ const markdownLines = new Markdown(
344
+ selectedPreview,
345
+ 1,
346
+ 0,
347
+ {
348
+ heading: (text) => theme.fg("accent", theme.bold(text)),
349
+ link: (text) => theme.fg("accent", text),
350
+ linkUrl: (text) => theme.fg("dim", text),
351
+ code: (text) => theme.fg("mdCode", text),
352
+ codeBlock: (text) => theme.fg("mdCodeBlock", text),
353
+ codeBlockBorder: (text) => theme.fg("mdCodeBlockBorder", text),
354
+ quote: (text) => theme.fg("mdQuote", text),
355
+ quoteBorder: (text) => theme.fg("mdQuoteBorder", text),
356
+ hr: (text) => theme.fg("mdHr", text),
357
+ listBullet: (text) => theme.fg("mdListBullet", text),
358
+ bold: (text) => theme.bold(text),
359
+ italic: (text) => theme.italic(text),
360
+ strikethrough: (text) => theme.strikethrough(text),
361
+ underline: (text) => theme.underline(text),
362
+ },
363
+ { color: (text) => theme.fg("muted", text) },
364
+ ).render(renderWidth);
365
+ const maxPreviewRows = Math.min(12, availableRows - previewChromeRows);
366
+ if (markdownLines.length <= maxPreviewRows) {
367
+ lines.push(...markdownLines);
368
+ } else {
369
+ const visiblePreviewRows = Math.max(0, maxPreviewRows - 1);
370
+ lines.push(...markdownLines.slice(0, visiblePreviewRows));
371
+ const hiddenRows = markdownLines.length - visiblePreviewRows;
372
+ lines.push(theme.fg("dim", ` … ${hiddenRows} more line${hiddenRows === 1 ? "" : "s"}`));
373
+ }
374
+ }
375
+ }
376
+
377
+ if (editMode) {
378
+ lines.push("");
379
+ addWrappedWithPrefix(" ", theme.fg("muted", "Your answer:"));
380
+ editor.render(Math.max(1, renderWidth - 2)).forEach((line) => lines.push(` ${line}`));
381
+ }
382
+
383
+ lines.push("");
384
+ const hint = editMode
385
+ ? `Enter submit • Esc options${customInputHistory.length ? " • ↑↓ history" : ""}`
386
+ : filterQuery
387
+ ? "1-9 select • ↑↓ navigate • Enter select • Esc clear filter"
388
+ : "1-9 select • type to filter • ↑↓ navigate • Enter select • Esc cancel";
389
+ addWrappedWithPrefix(" ", theme.fg("dim", hint));
390
+ lines.push(theme.fg("accent", "─".repeat(renderWidth)));
391
+ cachedWidth = renderWidth;
392
+ cachedLines = lines.map((line) => truncateToWidth(line, renderWidth, ""));
393
+ return cachedLines;
394
+ };
395
+
396
+ let focused = false;
397
+ return {
398
+ get focused(): boolean { return focused; },
399
+ set focused(value: boolean) {
400
+ focused = value;
401
+ editor.focused = value;
402
+ },
403
+ render,
404
+ handleInput,
405
+ invalidate,
406
+ };
407
+ });
408
+
409
+ const abortHandler = (): void => finishFromAbort?.();
410
+ signal?.addEventListener("abort", abortHandler, { once: true });
411
+ if (signal?.aborted) abortHandler();
412
+ let result: QuestionSelection;
413
+ try {
414
+ result = await resultPromise;
415
+ } finally {
416
+ signal?.removeEventListener("abort", abortHandler);
417
+ }
418
+
419
+ const simpleOptions = params.options.map((option) => option.label);
420
+ if (result.kind === "aborted") throw new Error("Question cancelled because the agent operation was aborted");
421
+ if (result.kind === "cancelled") {
422
+ return {
423
+ content: [{ type: "text", text: "User cancelled the question" }],
424
+ details: { question: params.question, options: simpleOptions, answer: null, cancelled: true },
425
+ };
426
+ }
427
+ if (result.kind === "custom") {
428
+ return {
429
+ content: [{ type: "text", text: `User wrote: ${result.answer}` }],
430
+ details: { question: params.question, options: simpleOptions, answer: result.answer, wasCustom: true },
431
+ };
432
+ }
433
+ return {
434
+ content: [{ type: "text", text: `User selected: ${result.answer}` }],
435
+ details: {
436
+ question: params.question,
437
+ options: simpleOptions,
438
+ answer: result.answer,
439
+ selectedIndex: result.originalIndex,
440
+ wasCustom: false,
441
+ },
442
+ };
443
+ },
444
+
445
+ renderCall(args, theme) {
446
+ let text = `${theme.fg("toolTitle", theme.bold("question "))}${theme.fg("muted", args.question)}`;
447
+ if (args.options.length) {
448
+ const numbered = [...args.options.map((option) => option.label), "Type a custom answer"]
449
+ .map((option, index) => `${index + 1}. ${option}`);
450
+ text += `\n${theme.fg("dim", ` Options: ${numbered.join(", ")}`)}`;
451
+ }
452
+ return new Text(text, 0, 0);
453
+ },
454
+
455
+ renderResult(result, _options, theme) {
456
+ const details = result.details;
457
+ if (!details) {
458
+ const first = result.content[0];
459
+ return new Text(first?.type === "text" ? first.text : "", 0, 0);
460
+ }
461
+ if (details.cancelled || details.answer === null) return new Text(theme.fg("warning", "Cancelled"), 0, 0);
462
+ if (details.wasCustom) {
463
+ return new Text(`${theme.fg("success", "✓ ")}${theme.fg("muted", "(wrote) ")}${theme.fg("accent", details.answer)}`, 0, 0);
464
+ }
465
+ return new Text(`${theme.fg("success", "✓ ")}${theme.fg("accent", details.answer)}`, 0, 0);
466
+ },
467
+ });
468
+ }
@@ -0,0 +1,61 @@
1
+ export interface InitRuntime {
2
+ active: boolean;
3
+ targetPath?: string;
4
+ writeAttempted: boolean;
5
+ writeSucceeded: boolean;
6
+ projectRoot?: string;
7
+ activeTools?: string[];
8
+ settle?: (writeSucceeded: boolean) => void;
9
+ }
10
+
11
+ export type GoalStatus = "active" | "paused" | "blocked" | "complete";
12
+
13
+ export interface GoalState {
14
+ version: 1;
15
+ revision: number;
16
+ objective: string;
17
+ status: GoalStatus;
18
+ createdAt: number;
19
+ updatedAt: number;
20
+ activeMilliseconds: number;
21
+ activeStartedAt?: number;
22
+ turns: number;
23
+ blockedAuditStartTurn: number;
24
+ baselineTokens: number;
25
+ result?: string;
26
+ }
27
+
28
+ export interface GoalRuntime {
29
+ state?: GoalState;
30
+ continuationScheduled: boolean;
31
+ continuationHeld: boolean;
32
+ goalTurnInFlight: boolean;
33
+ agentEndObserved: boolean;
34
+ persistenceRetryNeeded: boolean;
35
+ lastStopReason?: string;
36
+ lastError?: string;
37
+ requestRender?: () => void;
38
+ }
39
+
40
+ export function createInitRuntime(): InitRuntime {
41
+ return { active: false, writeAttempted: false, writeSucceeded: false };
42
+ }
43
+
44
+ export function createGoalRuntime(): GoalRuntime {
45
+ return {
46
+ continuationScheduled: false,
47
+ continuationHeld: false,
48
+ goalTurnInFlight: false,
49
+ agentEndObserved: false,
50
+ persistenceRetryNeeded: false,
51
+ };
52
+ }
53
+
54
+ export function resetInitRuntime(state: InitRuntime): void {
55
+ state.active = false;
56
+ state.targetPath = undefined;
57
+ state.writeAttempted = false;
58
+ state.writeSucceeded = false;
59
+ state.projectRoot = undefined;
60
+ state.activeTools = undefined;
61
+ }