shariq-pi-extensions 0.1.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 (123) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +94 -0
  3. package/THIRD_PARTY_NOTICES.md +11 -0
  4. package/docs/ARCHITECTURE.md +54 -0
  5. package/docs/DEVELOPMENT.md +88 -0
  6. package/docs/EXTENSIONS.md +89 -0
  7. package/extensions/antigravity-provider/LICENSE +21 -0
  8. package/extensions/antigravity-provider/README.md +23 -0
  9. package/extensions/antigravity-provider/antigravity/cloud-code-assist.ts +519 -0
  10. package/extensions/antigravity-provider/antigravity/index.ts +67 -0
  11. package/extensions/antigravity-provider/antigravity/models.ts +184 -0
  12. package/extensions/antigravity-provider/antigravity/oauth.ts +499 -0
  13. package/extensions/antigravity-provider/index.ts +3 -0
  14. package/extensions/ask-user/README.md +21 -0
  15. package/extensions/ask-user/index.ts +417 -0
  16. package/extensions/background-terminals/README.md +43 -0
  17. package/extensions/background-terminals/index.ts +400 -0
  18. package/extensions/background-terminals/src/manager.ts +480 -0
  19. package/extensions/background-terminals/src/output-buffer.ts +110 -0
  20. package/extensions/background-terminals/src/presentation.ts +74 -0
  21. package/extensions/background-terminals/src/types.ts +47 -0
  22. package/extensions/background-terminals/src/ui.ts +478 -0
  23. package/extensions/context-usage/LICENSE +21 -0
  24. package/extensions/context-usage/README.md +20 -0
  25. package/extensions/context-usage/index.ts +2006 -0
  26. package/extensions/context-usage/lib/ansi.ts +60 -0
  27. package/extensions/context-usage/lib/boundary.ts +27 -0
  28. package/extensions/context-usage/lib/chat.ts +190 -0
  29. package/extensions/context-usage/lib/config.ts +30 -0
  30. package/extensions/context-usage/lib/fmt.ts +59 -0
  31. package/extensions/context-usage/lib/style.ts +260 -0
  32. package/extensions/copy-all/README.md +15 -0
  33. package/extensions/copy-all/index.ts +72 -0
  34. package/extensions/factory-provider/API_KEYS.md +17 -0
  35. package/extensions/factory-provider/AUTH.md +15 -0
  36. package/extensions/factory-provider/README.md +90 -0
  37. package/extensions/factory-provider/REFRESH_MODELS.md +185 -0
  38. package/extensions/factory-provider/UPDATING.md +97 -0
  39. package/extensions/factory-provider/factory/api-keys.ts +407 -0
  40. package/extensions/factory-provider/factory/auth.ts +132 -0
  41. package/extensions/factory-provider/factory/constants.ts +15 -0
  42. package/extensions/factory-provider/factory/dashboard.ts +363 -0
  43. package/extensions/factory-provider/factory/droid.ts +84 -0
  44. package/extensions/factory-provider/factory/limits.ts +342 -0
  45. package/extensions/factory-provider/factory/models.ts +283 -0
  46. package/extensions/factory-provider/factory/responses.ts +331 -0
  47. package/extensions/factory-provider/factory/warm-theme.ts +53 -0
  48. package/extensions/factory-provider/factory/websocket.ts +130 -0
  49. package/extensions/factory-provider/index.ts +204 -0
  50. package/extensions/factory-provider/scripts/audit-reasoning-efforts.py +84 -0
  51. package/extensions/firecrawl-web/README.md +31 -0
  52. package/extensions/firecrawl-web/auth.ts +92 -0
  53. package/extensions/firecrawl-web/client.ts +238 -0
  54. package/extensions/firecrawl-web/index.ts +96 -0
  55. package/extensions/firecrawl-web/output.ts +125 -0
  56. package/extensions/git-info/README.md +25 -0
  57. package/extensions/git-info/index.ts +162 -0
  58. package/extensions/git-info/src/git.ts +206 -0
  59. package/extensions/git-info/src/ui.ts +291 -0
  60. package/extensions/goal/README.md +44 -0
  61. package/extensions/goal/constants.ts +4 -0
  62. package/extensions/goal/extension.ts +1064 -0
  63. package/extensions/goal/index.ts +2 -0
  64. package/extensions/goal/types.ts +56 -0
  65. package/extensions/goal/ui.ts +334 -0
  66. package/extensions/orchestration/README.md +58 -0
  67. package/extensions/orchestration/dashboard.ts +192 -0
  68. package/extensions/orchestration/engine.ts +706 -0
  69. package/extensions/orchestration/index.ts +243 -0
  70. package/extensions/orchestration/prompts.ts +124 -0
  71. package/extensions/orchestration/protocol.ts +116 -0
  72. package/extensions/orchestration/settings-ui.ts +41 -0
  73. package/extensions/orchestration/settings.ts +59 -0
  74. package/extensions/orchestration/storage.ts +49 -0
  75. package/extensions/orchestration/types.ts +105 -0
  76. package/extensions/performance-status/README.md +5 -0
  77. package/extensions/performance-status/index.ts +171 -0
  78. package/extensions/pi-memory/README.md +53 -0
  79. package/extensions/pi-memory/index.ts +326 -0
  80. package/extensions/pi-memory/src/config.ts +75 -0
  81. package/extensions/pi-memory/src/database.ts +617 -0
  82. package/extensions/pi-memory/src/extraction.ts +178 -0
  83. package/extensions/pi-memory/src/project.ts +80 -0
  84. package/extensions/pi-memory/src/projection.ts +59 -0
  85. package/extensions/pi-memory/src/redaction.ts +60 -0
  86. package/extensions/pi-memory/src/retrieval.ts +30 -0
  87. package/extensions/pi-memory/src/service.ts +194 -0
  88. package/extensions/pi-memory/src/session.ts +146 -0
  89. package/extensions/pi-memory/src/types.ts +82 -0
  90. package/extensions/shared/README.md +18 -0
  91. package/extensions/shared/activity-status.ts +31 -0
  92. package/extensions/shared/child-session.ts +150 -0
  93. package/extensions/shared/context-utilization.ts +47 -0
  94. package/extensions/shared/dashboard-state.ts +99 -0
  95. package/extensions/shared/tool-call-timeout.ts +104 -0
  96. package/extensions/shared/tui-dashboard.ts +139 -0
  97. package/extensions/shell-shortcuts/README.md +17 -0
  98. package/extensions/shell-shortcuts/features/exit-alias.ts +10 -0
  99. package/extensions/shell-shortcuts/index.ts +6 -0
  100. package/extensions/subagents/README.md +89 -0
  101. package/extensions/subagents/index.ts +1386 -0
  102. package/extensions/subagents/src/backend.ts +69 -0
  103. package/extensions/subagents/src/backends/pi.ts +781 -0
  104. package/extensions/subagents/src/catalog.ts +85 -0
  105. package/extensions/subagents/src/config.ts +241 -0
  106. package/extensions/subagents/src/context.ts +57 -0
  107. package/extensions/subagents/src/coordinator.ts +64 -0
  108. package/extensions/subagents/src/domain.ts +294 -0
  109. package/extensions/subagents/src/format.ts +74 -0
  110. package/extensions/subagents/src/manager.ts +813 -0
  111. package/extensions/subagents/src/prompt.ts +94 -0
  112. package/extensions/subagents/src/result-delivery.ts +20 -0
  113. package/extensions/subagents/src/runtime.ts +48 -0
  114. package/extensions/subagents/src/ui/takeover.ts +695 -0
  115. package/extensions/subagents/src/ui/transcript.ts +182 -0
  116. package/extensions/subagents/src/worktree.ts +175 -0
  117. package/extensions/web-fetch/README.md +13 -0
  118. package/extensions/web-fetch/index.ts +441 -0
  119. package/package.json +117 -0
  120. package/skills/background-terminals/SKILL.md +42 -0
  121. package/skills/orchestration/SKILL.md +33 -0
  122. package/skills/subagents/SKILL.md +89 -0
  123. package/themes/ember-warm-dark.json +85 -0
@@ -0,0 +1,21 @@
1
+ # Ask User
2
+
3
+ Presents one structured decision to the user when an unresolved choice materially blocks safe progress.
4
+
5
+ ## Tool
6
+
7
+ `ask_user` accepts one question and two to five distinct options. Each option has a concise label and may include a short consequence or clarification. The interface also lets the user write a custom answer.
8
+
9
+ The tool is deliberately narrow: agents should first inspect available context and use a reversible low-risk default when that would not change scope or authority. It should not be used for discoverable answers, routine confirmation, or a choice the user already made.
10
+
11
+ ## Interface behavior
12
+
13
+ - Available interactively in Pi's TUI as a centered decision overlay.
14
+ - Supports arrow keys, `j`/`k`, number keys, Enter, and a custom-answer editor.
15
+ - Escape or Ctrl+C dismisses the question without inventing an answer.
16
+ - In non-TUI modes, returns an unavailable result so the agent can ask plainly only if still blocked.
17
+ - Questions, labels, descriptions, and custom answers are bounded and sanitized before display.
18
+
19
+ ## Validation
20
+
21
+ From the repository root, run `npm run validate`.
@@ -0,0 +1,417 @@
1
+ import type {
2
+ ExtensionAPI,
3
+ ExtensionContext,
4
+ Theme,
5
+ } from "@earendil-works/pi-coding-agent";
6
+ import {
7
+ Editor,
8
+ type EditorTheme,
9
+ type Focusable,
10
+ Key,
11
+ matchesKey,
12
+ truncateToWidth,
13
+ wrapTextWithAnsi,
14
+ } from "@earendil-works/pi-tui";
15
+ import { Type } from "typebox";
16
+ import {
17
+ frameBottom,
18
+ frameTop,
19
+ oneLine,
20
+ padLine,
21
+ sanitizeTerminalText,
22
+ } from "../shared/tui-dashboard.ts";
23
+
24
+ const MIN_OPTIONS = 2;
25
+ const MAX_OPTIONS = 5;
26
+ const MAX_ANSWER_LENGTH = 20_000;
27
+
28
+ const OptionSchema = Type.Object({
29
+ label: Type.String({
30
+ minLength: 1,
31
+ maxLength: 200,
32
+ description: "Concise answer shown to the user",
33
+ }),
34
+ description: Type.Optional(
35
+ Type.String({
36
+ maxLength: 500,
37
+ description: "Optional consequence or clarification",
38
+ }),
39
+ ),
40
+ });
41
+
42
+ const AskUserParameters = Type.Object({
43
+ question: Type.String({
44
+ minLength: 1,
45
+ maxLength: 4_000,
46
+ description: "One concrete decision the user needs to make",
47
+ }),
48
+ options: Type.Array(OptionSchema, {
49
+ minItems: MIN_OPTIONS,
50
+ maxItems: MAX_OPTIONS,
51
+ description: "Distinct choices; include the recommended safe default when one exists",
52
+ }),
53
+ });
54
+
55
+ interface DisplayOption {
56
+ label: string;
57
+ description?: string;
58
+ custom?: boolean;
59
+ }
60
+
61
+ interface Selection {
62
+ answer: string;
63
+ custom: boolean;
64
+ optionIndex?: number;
65
+ }
66
+
67
+ interface AskUserDetails {
68
+ question: string;
69
+ options: string[];
70
+ answer: string | null;
71
+ custom: boolean;
72
+ cancelled: boolean;
73
+ unavailable?: boolean;
74
+ }
75
+
76
+ function cleanMultiline(text: string): string {
77
+ return sanitizeTerminalText(text).trim();
78
+ }
79
+
80
+ function normalizedChoiceKey(label: string): string {
81
+ return label.normalize("NFKC").toLocaleLowerCase("en-US");
82
+ }
83
+
84
+ function normalizeQuestionInput(
85
+ question: string,
86
+ options: ReadonlyArray<{ label: string; description?: string }>,
87
+ ): { question: string; options: DisplayOption[] } {
88
+ const cleanQuestion = cleanMultiline(question);
89
+ if (!cleanQuestion) throw new Error("question must contain visible text.");
90
+ const cleanOptions = options.map((option) => ({
91
+ label: cleanMultiline(option.label),
92
+ description: option.description ? cleanMultiline(option.description) || undefined : undefined,
93
+ }));
94
+ if (cleanOptions.some((option) => !option.label)) {
95
+ throw new Error("option labels must contain visible text.");
96
+ }
97
+ const keys = cleanOptions.map((option) => normalizedChoiceKey(option.label));
98
+ if (new Set(keys).size !== keys.length) {
99
+ throw new Error("option labels must be distinct after normalization.");
100
+ }
101
+ return { question: cleanQuestion, options: cleanOptions };
102
+ }
103
+
104
+ export function answerMessage(selection: Selection | null): string {
105
+ if (!selection) return "The user dismissed the question. Do not guess repeatedly; continue only if a safe path remains, otherwise explain what is blocked.";
106
+ return selection.custom
107
+ ? `The user wrote: ${selection.answer}`
108
+ : `The user selected option ${selection.optionIndex}: ${selection.answer}`;
109
+ }
110
+
111
+ function createEditorTheme(theme: Theme): EditorTheme {
112
+ return {
113
+ borderColor: (text) => theme.fg("accent", text),
114
+ selectList: {
115
+ selectedPrefix: (text) => theme.fg("accent", text),
116
+ selectedText: (text) => theme.fg("accent", text),
117
+ description: (text) => theme.fg("muted", text),
118
+ scrollInfo: (text) => theme.fg("dim", text),
119
+ noMatch: (text) => theme.fg("warning", text),
120
+ },
121
+ };
122
+ }
123
+
124
+ export class AskUserView implements Focusable {
125
+ private selected = 0;
126
+ private editing = false;
127
+ private cachedWidth?: number;
128
+ private cachedLines?: string[];
129
+ private _focused = false;
130
+ private readonly question: string;
131
+ private readonly options: DisplayOption[];
132
+ private readonly theme: Theme;
133
+ private readonly requestRender: () => void;
134
+ private readonly finish: (selection: Selection | null) => void;
135
+ private readonly editor: Editor;
136
+
137
+ constructor(
138
+ question: string,
139
+ options: DisplayOption[],
140
+ theme: Theme,
141
+ requestRender: () => void,
142
+ finish: (selection: Selection | null) => void,
143
+ editor: Editor,
144
+ ) {
145
+ this.question = question;
146
+ this.options = options;
147
+ this.theme = theme;
148
+ this.requestRender = requestRender;
149
+ this.finish = finish;
150
+ this.editor = editor;
151
+ this.editor.onSubmit = (value) => {
152
+ const answer = cleanMultiline(value).slice(0, MAX_ANSWER_LENGTH);
153
+ if (!answer) {
154
+ this.editing = false;
155
+ this.editor.setText("");
156
+ this.refresh();
157
+ return;
158
+ }
159
+ this.finish({ answer, custom: true });
160
+ };
161
+ }
162
+
163
+ get focused(): boolean {
164
+ return this._focused;
165
+ }
166
+
167
+ set focused(value: boolean) {
168
+ this._focused = value;
169
+ this.editor.focused = value && this.editing;
170
+ }
171
+
172
+ private refresh(): void {
173
+ this.cachedWidth = undefined;
174
+ this.cachedLines = undefined;
175
+ this.editor.focused = this._focused && this.editing;
176
+ this.requestRender();
177
+ }
178
+
179
+ private choose(index: number): void {
180
+ const option = this.options[index];
181
+ if (!option) return;
182
+ if (option.custom) {
183
+ this.selected = index;
184
+ this.editing = true;
185
+ this.refresh();
186
+ return;
187
+ }
188
+ this.finish({ answer: option.label, custom: false, optionIndex: index + 1 });
189
+ }
190
+
191
+ handleInput(data: string): void {
192
+ if (this.editing) {
193
+ if (matchesKey(data, Key.escape)) {
194
+ this.editing = false;
195
+ this.editor.setText("");
196
+ this.refresh();
197
+ return;
198
+ }
199
+ this.editor.handleInput(data);
200
+ this.refresh();
201
+ return;
202
+ }
203
+
204
+ if (matchesKey(data, Key.up) || data === "k") {
205
+ this.selected = (this.selected - 1 + this.options.length) % this.options.length;
206
+ this.refresh();
207
+ return;
208
+ }
209
+ if (matchesKey(data, Key.down) || data === "j") {
210
+ this.selected = (this.selected + 1) % this.options.length;
211
+ this.refresh();
212
+ return;
213
+ }
214
+ if (/^[1-6]$/.test(data)) {
215
+ const index = Number(data) - 1;
216
+ if (index < this.options.length) this.choose(index);
217
+ return;
218
+ }
219
+ if (matchesKey(data, Key.enter)) {
220
+ this.choose(this.selected);
221
+ return;
222
+ }
223
+ if (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl("c"))) {
224
+ this.finish(null);
225
+ }
226
+ }
227
+
228
+ render(width: number): string[] {
229
+ if (this.cachedWidth === width && this.cachedLines) return this.cachedLines;
230
+ const safeWidth = Math.max(1, width);
231
+ const inner = Math.max(1, safeWidth - 4);
232
+ const lines: string[] = [frameTop(this.theme, safeWidth, "Decision needed")];
233
+
234
+ for (const line of wrapTextWithAnsi(this.theme.bold(cleanMultiline(this.question)), inner)) {
235
+ lines.push(padLine(` ${this.theme.fg("text", line)}`, safeWidth));
236
+ }
237
+ lines.push(padLine("", safeWidth));
238
+
239
+ this.options.forEach((option, index) => {
240
+ const active = index === this.selected;
241
+ const marker = option.custom ? "✎" : `${index + 1}.`;
242
+ const prefix = active ? this.theme.fg("accent", "❯") : " ";
243
+ const label = `${prefix} ${marker} ${oneLine(option.label)}`;
244
+ lines.push(
245
+ padLine(
246
+ active
247
+ ? this.theme.bg("selectedBg", this.theme.fg("accent", label))
248
+ : this.theme.fg(option.custom ? "muted" : "text", label),
249
+ safeWidth,
250
+ ),
251
+ );
252
+ if (option.description) {
253
+ for (const description of wrapTextWithAnsi(oneLine(option.description), Math.max(8, inner - 4))) {
254
+ lines.push(padLine(` ${this.theme.fg("muted", description)}`, safeWidth));
255
+ }
256
+ }
257
+ });
258
+
259
+ if (this.editing) {
260
+ lines.push(padLine("", safeWidth));
261
+ lines.push(padLine(` ${this.theme.fg("muted", "Your answer")}`, safeWidth));
262
+ for (const line of this.editor.render(Math.max(10, safeWidth - 4))) {
263
+ lines.push(padLine(` ${line}`, safeWidth));
264
+ }
265
+ }
266
+
267
+ lines.push(padLine("", safeWidth));
268
+ lines.push(
269
+ padLine(
270
+ ` ${this.theme.fg(
271
+ "dim",
272
+ this.editing
273
+ ? "enter submit · escape choices"
274
+ : `up/down or 1-${this.options.length} choose · enter confirm · escape dismiss`,
275
+ )}`,
276
+ safeWidth,
277
+ ),
278
+ );
279
+ lines.push(frameBottom(this.theme, safeWidth));
280
+ this.cachedWidth = width;
281
+ this.cachedLines = lines.map((line) => truncateToWidth(line, safeWidth, ""));
282
+ return this.cachedLines;
283
+ }
284
+
285
+ invalidate(): void {
286
+ this.cachedWidth = undefined;
287
+ this.cachedLines = undefined;
288
+ this.editor.invalidate();
289
+ }
290
+ }
291
+
292
+ async function openQuestion(
293
+ ctx: ExtensionContext,
294
+ question: string,
295
+ options: DisplayOption[],
296
+ signal: AbortSignal | undefined,
297
+ ): Promise<Selection | null> {
298
+ if (ctx.mode !== "tui") return null;
299
+ return ctx.ui.custom<Selection | null>(
300
+ (tui, theme, _keybindings, done) => {
301
+ let settled = false;
302
+ const complete = (selection: Selection | null) => {
303
+ if (settled) return;
304
+ settled = true;
305
+ signal?.removeEventListener("abort", abort);
306
+ done(selection);
307
+ };
308
+ const abort = () => complete(null);
309
+ signal?.addEventListener("abort", abort, { once: true });
310
+ if (signal?.aborted) queueMicrotask(abort);
311
+ const view = new AskUserView(
312
+ question,
313
+ options,
314
+ theme,
315
+ () => tui.requestRender(),
316
+ complete,
317
+ new Editor(tui, createEditorTheme(theme)),
318
+ );
319
+ return {
320
+ get focused() {
321
+ return view.focused;
322
+ },
323
+ set focused(value: boolean) {
324
+ view.focused = value;
325
+ },
326
+ render: (width) => view.render(width),
327
+ invalidate: () => view.invalidate(),
328
+ handleInput: (data) => view.handleInput(data),
329
+ dispose: () => signal?.removeEventListener("abort", abort),
330
+ };
331
+ },
332
+ {
333
+ overlay: true,
334
+ overlayOptions: {
335
+ width: "72%",
336
+ minWidth: 48,
337
+ maxHeight: "86%",
338
+ anchor: "center",
339
+ margin: 1,
340
+ },
341
+ },
342
+ );
343
+ }
344
+
345
+ export default function askUserExtension(pi: ExtensionAPI) {
346
+ pi.registerTool({
347
+ name: "ask_user",
348
+ label: "Ask User",
349
+ description:
350
+ "Ask one multiple-choice question only when a missing user decision materially blocks safe progress. Do not use it when the answer is discoverable, the user already supplied it, or a reversible low-risk default is reasonable.",
351
+ promptSnippet: "Ask one materially blocking multiple-choice question with an optional custom answer",
352
+ promptGuidelines: [
353
+ "Use ask_user only when a missing choice materially blocks safe progress; first inspect available context and prefer a reversible low-risk default when that would not change scope or authority.",
354
+ "When using ask_user, ask one decision at a time with distinct options, explain consequences briefly, and include the recommended safe default when one exists.",
355
+ ],
356
+ parameters: AskUserParameters,
357
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
358
+ const normalized = normalizeQuestionInput(params.question, params.options);
359
+ if (ctx.mode !== "tui") {
360
+ return {
361
+ content: [{
362
+ type: "text",
363
+ text: "Interactive choices are unavailable in this mode. Ask the user plainly in the final response only if the decision still blocks progress.",
364
+ }],
365
+ details: {
366
+ question: normalized.question,
367
+ options: normalized.options.map((option) => option.label),
368
+ answer: null,
369
+ custom: false,
370
+ cancelled: true,
371
+ unavailable: true,
372
+ } satisfies AskUserDetails,
373
+ };
374
+ }
375
+ const choices: DisplayOption[] = [
376
+ ...normalized.options,
377
+ { label: "Write my own answer…", custom: true },
378
+ ];
379
+ const selection = await openQuestion(
380
+ ctx,
381
+ normalized.question,
382
+ choices,
383
+ signal,
384
+ );
385
+ const details: AskUserDetails = {
386
+ question: normalized.question,
387
+ options: choices.filter((option) => !option.custom).map((option) => option.label),
388
+ answer: selection?.answer ?? null,
389
+ custom: selection?.custom ?? false,
390
+ cancelled: !selection,
391
+ };
392
+ return {
393
+ content: [{ type: "text", text: answerMessage(selection) }],
394
+ details,
395
+ };
396
+ },
397
+ renderCall(args, theme) {
398
+ const question = typeof args.question === "string" ? oneLine(args.question) : "";
399
+ return {
400
+ render: (width: number) => [
401
+ truncateToWidth(
402
+ `${theme.fg("toolTitle", theme.bold("ask user"))} ${theme.fg("muted", question)}`,
403
+ width,
404
+ ),
405
+ ],
406
+ invalidate() {},
407
+ };
408
+ },
409
+ renderResult(result, _options, theme) {
410
+ const details = result.details as AskUserDetails | undefined;
411
+ const text = !details || details.cancelled
412
+ ? theme.fg("warning", "● dismissed")
413
+ : `${theme.fg("success", "● answered")} ${theme.fg("accent", oneLine(details.answer ?? ""))}`;
414
+ return { render: (width: number) => [truncateToWidth(text, width)], invalidate() {} };
415
+ },
416
+ });
417
+ }
@@ -0,0 +1,43 @@
1
+ # Pi background terminals
2
+
3
+ Session-scoped background pseudo-terminals for Pi. The extension combines Codex-style PTY input and cursor reads with bounded capture, size-limited private logs, automatic completion delivery, process-group shutdown, and a live Pi control center.
4
+
5
+ ## Model tools
6
+
7
+ - `start_terminal` — start a long-running or interactive command in a PTY.
8
+ - `read_terminal` — read retained or incremental output, with optional long polling.
9
+ - `write_terminal` — send input or control characters and collect the response.
10
+ - `list_terminals` — list running and settled terminals.
11
+ - `stop_terminal` — stop complete process groups with TERM-to-KILL escalation.
12
+
13
+ Each output response carries a byte cursor. Pass it to the next read/write operation to avoid repeating output. Long or uncertain commands should use `start_terminal` instead of a large blocking `bash` timeout. Completion wakes the parent automatically, so it can continue other work or end its turn rather than poll.
14
+
15
+ ## User interface
16
+
17
+ `/term`, `/term list`, and `/ps` open the fullscreen control center. `/term start <command>` starts the process and immediately returns to the editor; `/term stop <id> [id…]` stops exact processes. The control center provides:
18
+
19
+ - live counts and status;
20
+ - terminal list with a selected-session inspector;
21
+ - live output, elapsed time, dimensions, command, cwd, and full-log size;
22
+ - real PTY input;
23
+ - Ctrl+C delivery to the PTY;
24
+ - scrolling and top/bottom navigation;
25
+ - two-press process-stop guard.
26
+
27
+ ## Lifecycle and safety
28
+
29
+ - Maximum eight concurrent terminals and 32 tracked entries.
30
+ - Newest 2 MiB retained in memory per terminal.
31
+ - Raw output spills to a mode-`0600` file under a mode-`0700` temporary session directory, capped at 64 MiB per terminal.
32
+ - Spill backpressure pauses and resumes the PTY; reaching the cap stops log growth while retaining the newest live-output tail.
33
+ - Pruning an old settled terminal deletes its spill file immediately; session shutdown removes the complete temporary directory.
34
+ - Output is sanitized before TUI or model rendering.
35
+ - Processes run in their own PTY process group and are stopped on session shutdown, replacement, or reload.
36
+ - Shutdown and stop operations are bounded and escalate from SIGTERM to SIGKILL.
37
+ - Model-started terminals automatically deliver one completion/failure follow-up that starts the next parent turn.
38
+ - Reading settled output does not consume or suppress the automatic completion delivery.
39
+ - Completion delivery is keyed by terminal id to prevent duplicate follow-ups.
40
+
41
+ ## Validation
42
+
43
+ From the repository root, run `npm run validate`.