@aragon-agent/cli 0.5.4 → 0.5.8
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.
- package/CHANGELOG.md +747 -577
- package/README.md +1163 -995
- package/dist/agent/controller.d.ts +71 -1
- package/dist/agent/controller.js +135 -1
- package/dist/agent/entry-limits.d.ts +65 -0
- package/dist/agent/entry-limits.js +133 -0
- package/dist/agent/headless.d.ts +29 -0
- package/dist/agent/headless.js +86 -1
- package/dist/agent/reducer.d.ts +53 -0
- package/dist/agent/reducer.js +145 -14
- package/dist/agent/system-prompt.d.ts +8 -0
- package/dist/agent/system-prompt.js +2 -0
- package/dist/cli.js +76 -3
- package/dist/commands/builtins.js +171 -0
- package/dist/commands/perf.d.ts +45 -0
- package/dist/commands/perf.js +78 -0
- package/dist/commands/registry.d.ts +27 -1
- package/dist/config/env.d.ts +3 -1
- package/dist/config/env.js +74 -1
- package/dist/config/load.d.ts +41 -0
- package/dist/config/load.js +67 -2
- package/dist/config/schema.d.ts +108 -8
- package/dist/config/schema.js +81 -6
- package/dist/config/store.js +10 -1
- package/dist/session/persist.d.ts +19 -1
- package/dist/session/persist.js +21 -2
- package/dist/team/subagent.js +11 -0
- package/dist/todo/follow-through.d.ts +117 -0
- package/dist/todo/follow-through.js +186 -0
- package/dist/todo/limits.d.ts +114 -0
- package/dist/todo/limits.js +114 -0
- package/dist/todo/normalize.d.ts +33 -0
- package/dist/todo/normalize.js +117 -0
- package/dist/todo/panel-rows.d.ts +53 -0
- package/dist/todo/panel-rows.js +103 -0
- package/dist/todo/prompt.d.ts +54 -0
- package/dist/todo/prompt.js +76 -0
- package/dist/todo/store.d.ts +86 -0
- package/dist/todo/store.js +183 -0
- package/dist/todo/todo-tool.d.ts +38 -0
- package/dist/todo/todo-tool.js +134 -0
- package/dist/todo/types.d.ts +62 -0
- package/dist/todo/types.js +12 -0
- package/dist/tools/index.d.ts +16 -2
- package/dist/tools/index.js +13 -0
- package/dist/ui/App.js +328 -14
- package/dist/ui/Markdown.d.ts +7 -1
- package/dist/ui/Markdown.js +15 -48
- package/dist/ui/StatusBar.d.ts +31 -0
- package/dist/ui/StatusBar.js +7 -2
- package/dist/ui/TodoPanel.d.ts +48 -0
- package/dist/ui/TodoPanel.js +74 -0
- package/dist/ui/TodoStrip.d.ts +36 -0
- package/dist/ui/TodoStrip.js +32 -0
- package/dist/ui/Transcript.d.ts +56 -4
- package/dist/ui/Transcript.js +123 -11
- package/dist/ui/entries/AssistantEntry.d.ts +18 -1
- package/dist/ui/entries/AssistantEntry.js +33 -3
- package/dist/ui/entries/TeamCard.d.ts +9 -1
- package/dist/ui/entries/TeamCard.js +22 -1
- package/dist/ui/entries/TodoCard.d.ts +39 -0
- package/dist/ui/entries/TodoCard.js +57 -0
- package/dist/ui/entries/ToolCard.d.ts +13 -1
- package/dist/ui/entries/ToolCard.js +39 -5
- package/dist/ui/entries/ToolPreview.d.ts +2 -2
- package/dist/ui/entries/UserEntry.d.ts +9 -1
- package/dist/ui/entries/UserEntry.js +17 -1
- package/dist/ui/exit-snapshot.d.ts +7 -0
- package/dist/ui/glyphs.d.ts +12 -0
- package/dist/ui/glyphs.js +8 -0
- package/dist/ui/layout/AppShell.d.ts +34 -1
- package/dist/ui/layout/AppShell.js +3 -3
- package/dist/ui/layout/MeasuredEntry.d.ts +21 -0
- package/dist/ui/layout/MeasuredEntry.js +30 -0
- package/dist/ui/layout/ScrollViewport.d.ts +13 -1
- package/dist/ui/layout/ScrollViewport.js +10 -4
- package/dist/ui/layout/rail.d.ts +56 -0
- package/dist/ui/layout/rail.js +68 -0
- package/dist/ui/layout/viewport-geometry.d.ts +28 -0
- package/dist/ui/layout/viewport-geometry.js +27 -0
- package/dist/ui/layout/virtual-window.d.ts +100 -0
- package/dist/ui/layout/virtual-window.js +204 -0
- package/dist/ui/live-clamp.d.ts +42 -0
- package/dist/ui/live-clamp.js +74 -0
- package/dist/ui/markdown-blocks.d.ts +25 -1
- package/dist/ui/markdown-blocks.js +43 -0
- package/dist/ui/overlays/HelpOverlay.js +4 -0
- package/dist/ui/render-cache.d.ts +81 -0
- package/dist/ui/render-cache.js +195 -0
- package/dist/ui/render-governor.d.ts +54 -0
- package/dist/ui/render-governor.js +78 -0
- package/dist/ui/transcript-text.d.ts +11 -0
- package/dist/ui/transcript-text.js +5 -0
- package/dist/ui/use-height-store.d.ts +31 -0
- package/dist/ui/use-height-store.js +76 -0
- package/dist/ui/use-render-governor.d.ts +39 -0
- package/dist/ui/use-render-governor.js +57 -0
- package/dist/ui/use-startup-notices.d.ts +9 -1
- package/dist/ui/use-startup-notices.js +35 -1
- package/package.json +3 -2
|
@@ -9,9 +9,10 @@
|
|
|
9
9
|
* from the event stream by the reducer (TUI) and the headless writer.
|
|
10
10
|
*/
|
|
11
11
|
import { ModelRegistry, type AgentEvent, type AgentTool, type Message, type ModelInfo, type ProviderRegistry, type ThinkingLevel } from '@aragon-agent/core';
|
|
12
|
-
import type { CliConfig, TeamConfig, ThemeName } from '../config/schema.js';
|
|
12
|
+
import type { CliConfig, TeamConfig, ThemeName, TodoConfig } from '../config/schema.js';
|
|
13
13
|
import { type ConfirmRequest } from '../tools/index.js';
|
|
14
14
|
import type { TeamEvent, TeamSnapshot } from '../team/types.js';
|
|
15
|
+
import type { TodoEventListener, TodoSnapshot } from '../todo/types.js';
|
|
15
16
|
import { type HumanInputGate } from '../tools/human-input.js';
|
|
16
17
|
import { SkillService, type ApprovalGate } from '../skills/service.js';
|
|
17
18
|
import type { AgentMode } from './agent-mode.js';
|
|
@@ -48,6 +49,23 @@ export interface ControllerDeps {
|
|
|
48
49
|
* wizard or a plan card.
|
|
49
50
|
*/
|
|
50
51
|
humanInput?: HumanInputGate;
|
|
52
|
+
/**
|
|
53
|
+
* Whether this session could EVER render the todo rail (todo-plan-execution
|
|
54
|
+
* §3.7 / P1-6). Full-screen and interactive; `-p` and inline mode are false.
|
|
55
|
+
*
|
|
56
|
+
* It varies ONE SENTENCE of the `<todo_planning>` block, and nothing else.
|
|
57
|
+
* Telling the model "the user sees this list in a panel beside the
|
|
58
|
+
* conversation" under `-p` is the same error `composeSystemPrompt` already
|
|
59
|
+
* guards against for team mode with its two flags. Defaults to `false` so a
|
|
60
|
+
* caller that forgets it gets the honest sentence rather than a false claim
|
|
61
|
+
* about the user's screen.
|
|
62
|
+
*
|
|
63
|
+
* Deliberately STATIC (D-25). The two dynamic causes — a resize below
|
|
64
|
+
* `TODO_RAIL_MIN_TOTAL_COLS` and an open overlay — do NOT re-compose the
|
|
65
|
+
* prompt: rebuilding it on every resize would rewrite the system prompt
|
|
66
|
+
* mid-run, and the sentence is guidance rather than a contract.
|
|
67
|
+
*/
|
|
68
|
+
todoPanelCapable?: boolean;
|
|
51
69
|
}
|
|
52
70
|
/** What `setAgentMode` actually adopted — see `AgentController.setAgentMode`. */
|
|
53
71
|
export interface AgentModeState {
|
|
@@ -84,6 +102,11 @@ export declare class AgentController {
|
|
|
84
102
|
private readonly teamRegistered;
|
|
85
103
|
private teamEnabled;
|
|
86
104
|
private readonly teamRuntime;
|
|
105
|
+
private readonly todoRegistered;
|
|
106
|
+
private todoEnabled;
|
|
107
|
+
private readonly todos;
|
|
108
|
+
/** Static: whether this session could ever render the rail. See the dep. */
|
|
109
|
+
private readonly todoPanelCapable;
|
|
87
110
|
constructor(config: CliConfig, deps?: ControllerDeps);
|
|
88
111
|
private resolveKey;
|
|
89
112
|
/**
|
|
@@ -229,6 +252,53 @@ export declare class AgentController {
|
|
|
229
252
|
subscribeTeam(listener: (event: TeamEvent) => void): () => void;
|
|
230
253
|
/** Abort every child and clear timers. Idempotent; safe to call twice. */
|
|
231
254
|
dispose(): void;
|
|
255
|
+
/** Whether `todo_write` is in `this.tools`. Fixed at construction (C-1). */
|
|
256
|
+
isTodoRegistered(): boolean;
|
|
257
|
+
/** Whether `todo_write` will actually write. Flipped live by `/todo on|off`. */
|
|
258
|
+
isTodoEnabled(): boolean;
|
|
259
|
+
/**
|
|
260
|
+
* Flip the live switch and RE-RENDER THE PROMPT.
|
|
261
|
+
*
|
|
262
|
+
* THE REBUILD IS NOT OPTIONAL (P1-1 / AC-37). `composeSystemPrompt()` gates
|
|
263
|
+
* `todoBlock` on `todoRegistered && todoEnabled`; without the rebuild
|
|
264
|
+
* `<todo_planning>` survives `/todo off`, every call comes back with
|
|
265
|
+
* `TODO_OFF_REFUSAL`, and the instructions telling the model to keep calling
|
|
266
|
+
* are still in its own context — a refusal loop it cannot diagnose.
|
|
267
|
+
*
|
|
268
|
+
* Turning it ON in a session that started with `--no-todo` deliberately does
|
|
269
|
+
* NOT register the tool: the array is immutable (C-1), and advertising an
|
|
270
|
+
* unregistered tool is the dead end `/team on` already documents. `/todo`
|
|
271
|
+
* reports that honestly by checking `isTodoRegistered()` first.
|
|
272
|
+
*/
|
|
273
|
+
setTodoEnabled(enabled: boolean): void;
|
|
274
|
+
getTodoConfig(): TodoConfig;
|
|
275
|
+
/**
|
|
276
|
+
* Adopt a new todo config for this session.
|
|
277
|
+
*
|
|
278
|
+
* THIS EXISTS BECAUSE `persistConfig` DOES NOT TOUCH THE RUNTIME (P1-2 /
|
|
279
|
+
* AC-38). `App` reads `controller.getConfig()` on every render, and that
|
|
280
|
+
* returns THIS object; the App's `persistConfig` only writes the file. So
|
|
281
|
+
* `/todo panel off` must call both, exactly as `/team max` does — with only
|
|
282
|
+
* the persist call the command reports success and changes nothing until the
|
|
283
|
+
* next launch.
|
|
284
|
+
*
|
|
285
|
+
* Clamped here as well as on the persist path, so a bad value can reach
|
|
286
|
+
* neither the runtime nor the file. The prompt rebuild carries §3.7's
|
|
287
|
+
* `panelVisible` sentence, which is part of the block.
|
|
288
|
+
*/
|
|
289
|
+
setTodoConfig(patch: Partial<TodoConfig>): TodoConfig;
|
|
290
|
+
getTodoSnapshot(): TodoSnapshot | null;
|
|
291
|
+
/**
|
|
292
|
+
* Subscribe to the CLI-local todo event stream.
|
|
293
|
+
*
|
|
294
|
+
* Returns a no-op unsubscribe when todo planning was off at construction, so
|
|
295
|
+
* every caller can subscribe unconditionally.
|
|
296
|
+
*/
|
|
297
|
+
subscribeTodos(listener: TodoEventListener): () => void;
|
|
298
|
+
/** `/resume`. An absent or empty list CLEARS — see `TodoStore.restore` (P0-2). */
|
|
299
|
+
restoreTodos(items: unknown): void;
|
|
300
|
+
/** `/todo clear`. A user override of a projection; refused mid-run by `/todo`. */
|
|
301
|
+
clearTodos(): void;
|
|
232
302
|
setModel(provider: string, model: string, baseUrl?: string): void;
|
|
233
303
|
setThinkingLevel(level: ThinkingLevel): void;
|
|
234
304
|
setMaxTokens(value: number | undefined): void;
|
package/dist/agent/controller.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* from the event stream by the reducer (TUI) and the headless writer.
|
|
10
10
|
*/
|
|
11
11
|
import { Agent, initProviders, ModelRegistry, } from '@aragon-agent/core';
|
|
12
|
-
import { clampTeamConfig, isAdapterProvider } from '../config/schema.js';
|
|
12
|
+
import { clampTeamConfig, clampTodoConfig, isAdapterProvider } from '../config/schema.js';
|
|
13
13
|
import { makeGetApiKey } from '../config/load.js';
|
|
14
14
|
import { updatePersistedConfig } from '../config/store.js';
|
|
15
15
|
import { createBuiltinTools } from '../tools/index.js';
|
|
@@ -17,6 +17,9 @@ import { TeamRuntime } from '../team/runtime.js';
|
|
|
17
17
|
import { TeamHumanQueue } from '../team/human-queue.js';
|
|
18
18
|
import { createTaskTool } from '../team/task-tool.js';
|
|
19
19
|
import { buildTeamBlock } from '../team/prompt.js';
|
|
20
|
+
import { TodoStore } from '../todo/store.js';
|
|
21
|
+
import { createTodoTool } from '../todo/todo-tool.js';
|
|
22
|
+
import { buildTodoBlock } from '../todo/prompt.js';
|
|
20
23
|
import { createPlanTools } from '../tools/plan-tools.js';
|
|
21
24
|
import { DENY_ALL_HUMAN_INPUT } from '../tools/human-input.js';
|
|
22
25
|
import { createNodeSkillHost } from '../skills/node-host.js';
|
|
@@ -71,6 +74,26 @@ export class AgentController {
|
|
|
71
74
|
teamRegistered;
|
|
72
75
|
teamEnabled;
|
|
73
76
|
teamRuntime;
|
|
77
|
+
// -----------------------------------------------------------------------
|
|
78
|
+
// Todo planning (todo-plan-execution §3.6a) — THE SAME TWO FLAGS, FOR THE
|
|
79
|
+
// SAME REASON.
|
|
80
|
+
//
|
|
81
|
+
// `todoRegistered` is decided ONCE, at construction: whether `todo_write` is
|
|
82
|
+
// in `this.tools` at all. The array is immutable for the life of the session
|
|
83
|
+
// (C-1), so `/todo on` in a session started with `--no-todo` CANNOT add it.
|
|
84
|
+
//
|
|
85
|
+
// `todoEnabled` is live. `/todo off` flips it, which makes `todo_write` refuse
|
|
86
|
+
// through its own closure AND drops `<todo_planning>` from the next prompt
|
|
87
|
+
// rebuild — and BOTH halves are required (P1-1). Flip the flag without the
|
|
88
|
+
// rebuild and the block survives `/todo off`: every call comes back with the
|
|
89
|
+
// refusal while the instructions telling the model to keep calling are still
|
|
90
|
+
// in its own context, so it cannot diagnose the loop.
|
|
91
|
+
// -----------------------------------------------------------------------
|
|
92
|
+
todoRegistered;
|
|
93
|
+
todoEnabled;
|
|
94
|
+
todos;
|
|
95
|
+
/** Static: whether this session could ever render the rail. See the dep. */
|
|
96
|
+
todoPanelCapable;
|
|
74
97
|
constructor(config, deps = {}) {
|
|
75
98
|
this.config = config;
|
|
76
99
|
this.cwd = config.cwd;
|
|
@@ -104,6 +127,11 @@ export class AgentController {
|
|
|
104
127
|
this.skills.reportUnknownForcedSkills();
|
|
105
128
|
}
|
|
106
129
|
const confirm = deps.confirm;
|
|
130
|
+
// --- Todo planning -----------------------------------------------------
|
|
131
|
+
this.todoRegistered = config.todo.enabled;
|
|
132
|
+
this.todoEnabled = config.todo.enabled;
|
|
133
|
+
this.todoPanelCapable = deps.todoPanelCapable === true;
|
|
134
|
+
this.todos = this.todoRegistered ? new TodoStore() : null;
|
|
107
135
|
// --- Team mode ---------------------------------------------------------
|
|
108
136
|
this.teamRegistered = config.team.enabled;
|
|
109
137
|
this.teamEnabled = config.team.enabled;
|
|
@@ -167,6 +195,18 @@ export class AgentController {
|
|
|
167
195
|
withHumanWait: (fn) => this.withHumanWait(fn),
|
|
168
196
|
onPlanApproved: () => this.setAgentMode('build', { force: true }),
|
|
169
197
|
}),
|
|
198
|
+
// THROUGH THE FACTORY for the reason `teamTools` records below (C-2), and
|
|
199
|
+
// empty when todo planning was off at construction — which leaves the
|
|
200
|
+
// array byte-identical to the pre-todo build (AC-30).
|
|
201
|
+
todoTools: this.todos
|
|
202
|
+
? [
|
|
203
|
+
createTodoTool({
|
|
204
|
+
store: this.todos,
|
|
205
|
+
// Read LIVE: `/todo off` flips the flag, it cannot unregister.
|
|
206
|
+
isEnabled: () => this.todoEnabled,
|
|
207
|
+
}),
|
|
208
|
+
]
|
|
209
|
+
: [],
|
|
170
210
|
// THROUGH THE FACTORY, NEVER APPENDED AFTERWARDS (D-15 / P0-1).
|
|
171
211
|
// `tools.test.ts::C7` asserts `HOST_TOOL_NAMES` equals what
|
|
172
212
|
// `createBuiltinTools` produces, so appending `task` here would turn that
|
|
@@ -286,6 +326,16 @@ export class AgentController {
|
|
|
286
326
|
maxConcurrent: this.config.team.maxConcurrent,
|
|
287
327
|
})
|
|
288
328
|
: '',
|
|
329
|
+
// BOTH FLAGS, exactly as team mode uses both, and for the identical
|
|
330
|
+
// reason: `todoRegistered` false means there is no `todo_write` to
|
|
331
|
+
// advertise, `todoEnabled` false means the user turned it off for this
|
|
332
|
+
// session, and advertising in either case would describe a capability the
|
|
333
|
+
// model does not have.
|
|
334
|
+
todoBlock: this.todoRegistered && this.todoEnabled
|
|
335
|
+
? buildTodoBlock({
|
|
336
|
+
panelVisible: this.todoPanelCapable && this.config.todo.panel,
|
|
337
|
+
})
|
|
338
|
+
: '',
|
|
289
339
|
});
|
|
290
340
|
}
|
|
291
341
|
rebuildSystemPrompt() {
|
|
@@ -382,6 +432,11 @@ export class AgentController {
|
|
|
382
432
|
// steering "actually, use Postgres" into a running plan is answering the
|
|
383
433
|
// questions, not buying four more rounds of them.
|
|
384
434
|
this.askRounds = 0;
|
|
435
|
+
// Same asymmetry, same reason (§3.5): a completed plan is dropped HERE
|
|
436
|
+
// rather than at `agent_end`, so the user gets their `7/7 done` moment, and
|
|
437
|
+
// an unfinished one survives "continue" / "now do the rest" but not four
|
|
438
|
+
// unrelated turns.
|
|
439
|
+
this.todos?.beginUserTurn();
|
|
385
440
|
return this.agent.prompt(text);
|
|
386
441
|
}
|
|
387
442
|
abort() {
|
|
@@ -579,6 +634,80 @@ export class AgentController {
|
|
|
579
634
|
this.teamRuntime?.dispose();
|
|
580
635
|
}
|
|
581
636
|
// -----------------------------------------------------------------------
|
|
637
|
+
// Todo planning (todo-plan-execution §3.6a)
|
|
638
|
+
// -----------------------------------------------------------------------
|
|
639
|
+
/** Whether `todo_write` is in `this.tools`. Fixed at construction (C-1). */
|
|
640
|
+
isTodoRegistered() {
|
|
641
|
+
return this.todoRegistered;
|
|
642
|
+
}
|
|
643
|
+
/** Whether `todo_write` will actually write. Flipped live by `/todo on|off`. */
|
|
644
|
+
isTodoEnabled() {
|
|
645
|
+
return this.todoEnabled;
|
|
646
|
+
}
|
|
647
|
+
/**
|
|
648
|
+
* Flip the live switch and RE-RENDER THE PROMPT.
|
|
649
|
+
*
|
|
650
|
+
* THE REBUILD IS NOT OPTIONAL (P1-1 / AC-37). `composeSystemPrompt()` gates
|
|
651
|
+
* `todoBlock` on `todoRegistered && todoEnabled`; without the rebuild
|
|
652
|
+
* `<todo_planning>` survives `/todo off`, every call comes back with
|
|
653
|
+
* `TODO_OFF_REFUSAL`, and the instructions telling the model to keep calling
|
|
654
|
+
* are still in its own context — a refusal loop it cannot diagnose.
|
|
655
|
+
*
|
|
656
|
+
* Turning it ON in a session that started with `--no-todo` deliberately does
|
|
657
|
+
* NOT register the tool: the array is immutable (C-1), and advertising an
|
|
658
|
+
* unregistered tool is the dead end `/team on` already documents. `/todo`
|
|
659
|
+
* reports that honestly by checking `isTodoRegistered()` first.
|
|
660
|
+
*/
|
|
661
|
+
setTodoEnabled(enabled) {
|
|
662
|
+
this.todoEnabled = enabled;
|
|
663
|
+
this.rebuildSystemPrompt();
|
|
664
|
+
}
|
|
665
|
+
getTodoConfig() {
|
|
666
|
+
return this.config.todo;
|
|
667
|
+
}
|
|
668
|
+
/**
|
|
669
|
+
* Adopt a new todo config for this session.
|
|
670
|
+
*
|
|
671
|
+
* THIS EXISTS BECAUSE `persistConfig` DOES NOT TOUCH THE RUNTIME (P1-2 /
|
|
672
|
+
* AC-38). `App` reads `controller.getConfig()` on every render, and that
|
|
673
|
+
* returns THIS object; the App's `persistConfig` only writes the file. So
|
|
674
|
+
* `/todo panel off` must call both, exactly as `/team max` does — with only
|
|
675
|
+
* the persist call the command reports success and changes nothing until the
|
|
676
|
+
* next launch.
|
|
677
|
+
*
|
|
678
|
+
* Clamped here as well as on the persist path, so a bad value can reach
|
|
679
|
+
* neither the runtime nor the file. The prompt rebuild carries §3.7's
|
|
680
|
+
* `panelVisible` sentence, which is part of the block.
|
|
681
|
+
*/
|
|
682
|
+
setTodoConfig(patch) {
|
|
683
|
+
const todo = clampTodoConfig({ ...this.config.todo, ...patch });
|
|
684
|
+
this.config = { ...this.config, todo };
|
|
685
|
+
this.rebuildSystemPrompt();
|
|
686
|
+
return todo;
|
|
687
|
+
}
|
|
688
|
+
getTodoSnapshot() {
|
|
689
|
+
return this.todos?.snapshot() ?? null;
|
|
690
|
+
}
|
|
691
|
+
/**
|
|
692
|
+
* Subscribe to the CLI-local todo event stream.
|
|
693
|
+
*
|
|
694
|
+
* Returns a no-op unsubscribe when todo planning was off at construction, so
|
|
695
|
+
* every caller can subscribe unconditionally.
|
|
696
|
+
*/
|
|
697
|
+
subscribeTodos(listener) {
|
|
698
|
+
if (!this.todos)
|
|
699
|
+
return () => { };
|
|
700
|
+
return this.todos.subscribe(listener);
|
|
701
|
+
}
|
|
702
|
+
/** `/resume`. An absent or empty list CLEARS — see `TodoStore.restore` (P0-2). */
|
|
703
|
+
restoreTodos(items) {
|
|
704
|
+
this.todos?.restore(items);
|
|
705
|
+
}
|
|
706
|
+
/** `/todo clear`. A user override of a projection; refused mid-run by `/todo`. */
|
|
707
|
+
clearTodos() {
|
|
708
|
+
this.todos?.clear('user');
|
|
709
|
+
}
|
|
710
|
+
// -----------------------------------------------------------------------
|
|
582
711
|
// Mutators
|
|
583
712
|
// -----------------------------------------------------------------------
|
|
584
713
|
setModel(provider, model, baseUrl) {
|
|
@@ -620,6 +749,11 @@ export class AgentController {
|
|
|
620
749
|
// A new conversation has no loaded skills, so the "already loaded earlier
|
|
621
750
|
// in this conversation" hint would otherwise start lying (P2-9).
|
|
622
751
|
this.skills.getRegistry().clearActive();
|
|
752
|
+
// `/reset` clears `messages`, so the belief that justifies the panel is gone
|
|
753
|
+
// with them and the list must go too (I-2). `/clear` reaches the transcript
|
|
754
|
+
// and NOT this method, which is the whole distinction: it tells the model
|
|
755
|
+
// nothing, so it may not change the list.
|
|
756
|
+
this.todos?.clear('reset');
|
|
623
757
|
}
|
|
624
758
|
/** Read-only view of the live system prompt (regression tests, /debug). */
|
|
625
759
|
getSystemPrompt() {
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bounded view state (tui-render-performance L1 / R5).
|
|
3
|
+
*
|
|
4
|
+
* `ViewState.entries` used to grow without any ceiling: `argsRaw`, assistant
|
|
5
|
+
* `text` and `thinking` all concatenated forever, and the entry array was never
|
|
6
|
+
* trimmed. That makes the per-frame cost of the renderer proportional to the
|
|
7
|
+
* total number of characters the session has ever produced, because Ink's
|
|
8
|
+
* `renderNodeToOutput` walks and measures every mounted text node whether or not
|
|
9
|
+
* `overflow: hidden` clips it.
|
|
10
|
+
*
|
|
11
|
+
* Two bounds live here, both pure and React-free:
|
|
12
|
+
* - `appendBounded` caps one field by eliding its MIDDLE, and
|
|
13
|
+
* - `trimEntries` ring-trims the entry array itself.
|
|
14
|
+
*
|
|
15
|
+
* ASCII ONLY. This module is produced outside `src/ui/**`, where no terminal
|
|
16
|
+
* capabilities are available, so the elision marker has to be safe on a legacy
|
|
17
|
+
* `cmd.exe` -- the same rule `PREVIEW_TRUNCATION_MARK` in `reducer.ts` records.
|
|
18
|
+
*/
|
|
19
|
+
import type { Entry } from './reducer.js';
|
|
20
|
+
export interface EntryLimits {
|
|
21
|
+
/** Assistant answer text ceiling, in characters. */
|
|
22
|
+
text: number;
|
|
23
|
+
/** Assistant thinking ceiling. */
|
|
24
|
+
thinking: number;
|
|
25
|
+
/** Streamed tool-argument JSON ceiling. */
|
|
26
|
+
argsRaw: number;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The per-field ceilings.
|
|
30
|
+
*
|
|
31
|
+
* `text` is the generous one on purpose: a 256 KiB answer is already far past
|
|
32
|
+
* anything a terminal can usefully show, and the middle is what gets elided, so
|
|
33
|
+
* both the opening and the live cursor survive. `argsRaw` is the tight one --
|
|
34
|
+
* it is a streamed JSON fragment rendered as a 60-character card summary, so
|
|
35
|
+
* anything past a few KiB is invisible by construction.
|
|
36
|
+
*/
|
|
37
|
+
export declare const ENTRY_LIMITS: EntryLimits;
|
|
38
|
+
/** Marker inserted at the elision point. ASCII -- produced outside `src/ui/**`. */
|
|
39
|
+
export declare const ELISION_MARK_PREFIX = "\n[... ";
|
|
40
|
+
export declare const ELISION_MARK_SUFFIX = " characters elided ...]\n";
|
|
41
|
+
/**
|
|
42
|
+
* Append `delta` to `prev`, keeping the result at or below `cap` by eliding the
|
|
43
|
+
* MIDDLE. Head and tail are both preserved: the head is the answer's opening,
|
|
44
|
+
* the tail is where the cursor is. Idempotent -- re-eliding an already elided
|
|
45
|
+
* string collapses the two markers into one.
|
|
46
|
+
*
|
|
47
|
+
* The returned string is NEVER longer than `cap` (AC-7). The marker's own digit
|
|
48
|
+
* count is bounded before the tail budget is computed, so the arithmetic cannot
|
|
49
|
+
* overshoot by a digit the way a naive `cap - marker.length` would.
|
|
50
|
+
*/
|
|
51
|
+
export declare function appendBounded(prev: string, delta: string, cap: number): string;
|
|
52
|
+
export interface TrimResult {
|
|
53
|
+
entries: Entry[];
|
|
54
|
+
dropped: number;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Ring-trim to `retain` entries; returns the SAME array reference when no-op.
|
|
58
|
+
*
|
|
59
|
+
* The identity return is load-bearing: the reducer spreads this result into a
|
|
60
|
+
* new `ViewState` on every append, and a fresh array on every action would
|
|
61
|
+
* defeat the `React.memo` boundaries L2 installs on the transcript.
|
|
62
|
+
*/
|
|
63
|
+
export declare function trimEntries(entries: Entry[], retain: number): TrimResult;
|
|
64
|
+
export declare function setEntryRetain(value: number): void;
|
|
65
|
+
export declare function entryRetain(): number;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bounded view state (tui-render-performance L1 / R5).
|
|
3
|
+
*
|
|
4
|
+
* `ViewState.entries` used to grow without any ceiling: `argsRaw`, assistant
|
|
5
|
+
* `text` and `thinking` all concatenated forever, and the entry array was never
|
|
6
|
+
* trimmed. That makes the per-frame cost of the renderer proportional to the
|
|
7
|
+
* total number of characters the session has ever produced, because Ink's
|
|
8
|
+
* `renderNodeToOutput` walks and measures every mounted text node whether or not
|
|
9
|
+
* `overflow: hidden` clips it.
|
|
10
|
+
*
|
|
11
|
+
* Two bounds live here, both pure and React-free:
|
|
12
|
+
* - `appendBounded` caps one field by eliding its MIDDLE, and
|
|
13
|
+
* - `trimEntries` ring-trims the entry array itself.
|
|
14
|
+
*
|
|
15
|
+
* ASCII ONLY. This module is produced outside `src/ui/**`, where no terminal
|
|
16
|
+
* capabilities are available, so the elision marker has to be safe on a legacy
|
|
17
|
+
* `cmd.exe` -- the same rule `PREVIEW_TRUNCATION_MARK` in `reducer.ts` records.
|
|
18
|
+
*/
|
|
19
|
+
import { DEFAULT_TRANSCRIPT_RETAIN } from '../config/schema.js';
|
|
20
|
+
/**
|
|
21
|
+
* The per-field ceilings.
|
|
22
|
+
*
|
|
23
|
+
* `text` is the generous one on purpose: a 256 KiB answer is already far past
|
|
24
|
+
* anything a terminal can usefully show, and the middle is what gets elided, so
|
|
25
|
+
* both the opening and the live cursor survive. `argsRaw` is the tight one --
|
|
26
|
+
* it is a streamed JSON fragment rendered as a 60-character card summary, so
|
|
27
|
+
* anything past a few KiB is invisible by construction.
|
|
28
|
+
*/
|
|
29
|
+
export const ENTRY_LIMITS = {
|
|
30
|
+
text: 262_144,
|
|
31
|
+
thinking: 65_536,
|
|
32
|
+
argsRaw: 16_384,
|
|
33
|
+
};
|
|
34
|
+
/** Marker inserted at the elision point. ASCII -- produced outside `src/ui/**`. */
|
|
35
|
+
export const ELISION_MARK_PREFIX = '\n[... ';
|
|
36
|
+
export const ELISION_MARK_SUFFIX = ' characters elided ...]\n';
|
|
37
|
+
/** Matches a marker this module wrote, so re-elision collapses rather than nests. */
|
|
38
|
+
const ELISION_RE = /\n\[\.\.\. (\d+) characters elided \.\.\.\]\n/;
|
|
39
|
+
/** Fixed cost of the marker, before the digits of the count. */
|
|
40
|
+
const MARKER_FIXED = ELISION_MARK_PREFIX.length + ELISION_MARK_SUFFIX.length;
|
|
41
|
+
/**
|
|
42
|
+
* Fraction of the cap reserved for the HEAD. The tail gets the rest because that
|
|
43
|
+
* is where the cursor is; the head exists so the answer still starts where the
|
|
44
|
+
* model started it.
|
|
45
|
+
*/
|
|
46
|
+
const HEAD_FRACTION = 4;
|
|
47
|
+
/** Undo one elision, so a second one merges into it rather than nesting. */
|
|
48
|
+
function splitElided(prev) {
|
|
49
|
+
const m = ELISION_RE.exec(prev);
|
|
50
|
+
if (!m)
|
|
51
|
+
return { head: '', elided: 0, tail: prev };
|
|
52
|
+
const index = m.index;
|
|
53
|
+
return {
|
|
54
|
+
head: prev.slice(0, index),
|
|
55
|
+
elided: Number.parseInt(m[1], 10) || 0,
|
|
56
|
+
tail: prev.slice(index + m[0].length),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Append `delta` to `prev`, keeping the result at or below `cap` by eliding the
|
|
61
|
+
* MIDDLE. Head and tail are both preserved: the head is the answer's opening,
|
|
62
|
+
* the tail is where the cursor is. Idempotent -- re-eliding an already elided
|
|
63
|
+
* string collapses the two markers into one.
|
|
64
|
+
*
|
|
65
|
+
* The returned string is NEVER longer than `cap` (AC-7). The marker's own digit
|
|
66
|
+
* count is bounded before the tail budget is computed, so the arithmetic cannot
|
|
67
|
+
* overshoot by a digit the way a naive `cap - marker.length` would.
|
|
68
|
+
*/
|
|
69
|
+
export function appendBounded(prev, delta, cap) {
|
|
70
|
+
if (!Number.isFinite(cap) || cap <= 0)
|
|
71
|
+
return '';
|
|
72
|
+
const split = splitElided(prev);
|
|
73
|
+
let head = split.head;
|
|
74
|
+
let tail = split.tail + delta;
|
|
75
|
+
let elided = split.elided;
|
|
76
|
+
// Fast path: nothing has ever been elided and it still fits. This is the path
|
|
77
|
+
// every ordinary answer takes, so it must allocate nothing beyond the concat.
|
|
78
|
+
if (elided === 0 && tail.length <= cap)
|
|
79
|
+
return tail;
|
|
80
|
+
const headBudget = Math.max(0, Math.min(Math.floor(cap / HEAD_FRACTION), cap - MARKER_FIXED - 1));
|
|
81
|
+
if (elided === 0) {
|
|
82
|
+
// First elision: the head has to be carved out of the accumulated text.
|
|
83
|
+
head = tail.slice(0, headBudget);
|
|
84
|
+
tail = tail.slice(headBudget);
|
|
85
|
+
}
|
|
86
|
+
else if (head.length > headBudget) {
|
|
87
|
+
// A later `cap` reduction can leave the stored head oversized; charge the
|
|
88
|
+
// difference to the elision count rather than silently keeping it.
|
|
89
|
+
elided += head.length - headBudget;
|
|
90
|
+
head = head.slice(0, headBudget);
|
|
91
|
+
}
|
|
92
|
+
if (head.length + MARKER_FIXED >= cap) {
|
|
93
|
+
// Degenerate cap (only reachable from a hand-written test): keep the tail.
|
|
94
|
+
return tail.slice(Math.max(0, tail.length - cap));
|
|
95
|
+
}
|
|
96
|
+
// `elided + tail.length` is an UPPER BOUND on the final count, so its digit
|
|
97
|
+
// count is an upper bound on the marker's width. Budgeting against the bound
|
|
98
|
+
// is what makes the `<= cap` guarantee hold without a second pass.
|
|
99
|
+
const digitBound = String(elided + tail.length).length;
|
|
100
|
+
const markerBound = MARKER_FIXED + digitBound;
|
|
101
|
+
const tailKeep = Math.max(0, Math.min(tail.length, cap - head.length - markerBound));
|
|
102
|
+
const total = elided + (tail.length - tailKeep);
|
|
103
|
+
return `${head}${ELISION_MARK_PREFIX}${total}${ELISION_MARK_SUFFIX}${tail.slice(tail.length - tailKeep)}`;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Ring-trim to `retain` entries; returns the SAME array reference when no-op.
|
|
107
|
+
*
|
|
108
|
+
* The identity return is load-bearing: the reducer spreads this result into a
|
|
109
|
+
* new `ViewState` on every append, and a fresh array on every action would
|
|
110
|
+
* defeat the `React.memo` boundaries L2 installs on the transcript.
|
|
111
|
+
*/
|
|
112
|
+
export function trimEntries(entries, retain) {
|
|
113
|
+
const keep = Math.max(1, Math.floor(retain));
|
|
114
|
+
if (entries.length <= keep)
|
|
115
|
+
return { entries, dropped: 0 };
|
|
116
|
+
return { entries: entries.slice(entries.length - keep), dropped: entries.length - keep };
|
|
117
|
+
}
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
// The resolved retain ceiling
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
/**
|
|
122
|
+
* `viewReducer` is pure and takes no config, so the resolved `transcriptRetain`
|
|
123
|
+
* reaches it through a module-level singleton -- the shape `prompt-history.ts`
|
|
124
|
+
* already uses for `setHistoryEnabled`. `load.ts` is the ONLY writer.
|
|
125
|
+
*/
|
|
126
|
+
let resolvedRetain = DEFAULT_TRANSCRIPT_RETAIN;
|
|
127
|
+
export function setEntryRetain(value) {
|
|
128
|
+
if (Number.isFinite(value) && value > 0)
|
|
129
|
+
resolvedRetain = Math.floor(value);
|
|
130
|
+
}
|
|
131
|
+
export function entryRetain() {
|
|
132
|
+
return resolvedRetain;
|
|
133
|
+
}
|
package/dist/agent/headless.d.ts
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
import type { AgentEvent, ModelInfo } from '@aragon-agent/core';
|
|
8
8
|
import type { PreflightResult } from './controller.js';
|
|
9
9
|
import type { TeamEvent } from '../team/types.js';
|
|
10
|
+
import type { TodoEvent, TodoSnapshot } from '../todo/types.js';
|
|
11
|
+
import { type FollowThroughMode } from '../todo/follow-through.js';
|
|
10
12
|
/** The minimal controller surface headless mode needs (real or stubbed). */
|
|
11
13
|
export interface HeadlessController {
|
|
12
14
|
preflight(): PreflightResult;
|
|
@@ -24,11 +26,38 @@ export interface HeadlessController {
|
|
|
24
26
|
* the `--no-team` headless path byte-identical to today's.
|
|
25
27
|
*/
|
|
26
28
|
subscribeTeam?(listener: (event: TeamEvent) => void): () => void;
|
|
29
|
+
/**
|
|
30
|
+
* The CLI-local todo stream — OPTIONAL, for the identical reason
|
|
31
|
+
* `subscribeTeam` is (todo-plan-execution §3.12). This interface is minimal so
|
|
32
|
+
* tests can satisfy it with an object literal; making the member REQUIRED
|
|
33
|
+
* would break every existing stub for no benefit, and a controller with no
|
|
34
|
+
* store genuinely has no todo events. `runHeadless` calls it with `?.`, which
|
|
35
|
+
* keeps a `--no-todo` headless run byte-identical to today's.
|
|
36
|
+
*/
|
|
37
|
+
subscribeTodos?(listener: (event: TodoEvent) => void): () => void;
|
|
38
|
+
/**
|
|
39
|
+
* The current plan — OPTIONAL, for the identical reason the two subscriptions
|
|
40
|
+
* above are (todo-plan-followthrough §3.6 rule 2). ABSENT MEANS THE
|
|
41
|
+
* CONTINUATION LOOP NEVER RUNS, which is what keeps a stubbed controller's
|
|
42
|
+
* stdout, stderr and exit code byte-identical to today's (AC-23).
|
|
43
|
+
*/
|
|
44
|
+
getTodoSnapshot?(): TodoSnapshot | null;
|
|
27
45
|
}
|
|
28
46
|
export interface HeadlessOptions {
|
|
29
47
|
quiet?: boolean;
|
|
30
48
|
stdout?: NodeJS.WritableStream;
|
|
31
49
|
stderr?: NodeJS.WritableStream;
|
|
50
|
+
/**
|
|
51
|
+
* Follow-through mode for `-p` (todo-plan-followthrough §3.6). Defaults to
|
|
52
|
+
* `'notify'`, so an omitted option behaves exactly as the resolved default
|
|
53
|
+
* config does.
|
|
54
|
+
*
|
|
55
|
+
* PASSED IN RATHER THAN READ OFF THE CONTROLLER, because `HeadlessController`
|
|
56
|
+
* is deliberately the minimal surface a test can satisfy with an object
|
|
57
|
+
* literal; `runOneShot` holds the real `AgentController` and reads
|
|
58
|
+
* `getTodoConfig().followThrough` there.
|
|
59
|
+
*/
|
|
60
|
+
followThrough?: FollowThroughMode;
|
|
32
61
|
}
|
|
33
62
|
/** Run a single prompt headlessly and resolve with the process exit code. */
|
|
34
63
|
export declare function runHeadless(controller: HeadlessController, prompt: string, options?: HeadlessOptions): Promise<number>;
|
package/dist/agent/headless.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { formatCost, formatTokens, formatDuration, computeCost } from './usage.js';
|
|
8
8
|
import { formatStreamError } from './reducer.js';
|
|
9
|
+
import { advanceBudget, decideFollowThrough, emptyBudget, } from '../todo/follow-through.js';
|
|
9
10
|
/** Run a single prompt headlessly and resolve with the process exit code. */
|
|
10
11
|
export async function runHeadless(controller, prompt, options = {}) {
|
|
11
12
|
const out = options.stdout ?? process.stdout;
|
|
@@ -96,12 +97,96 @@ export async function runHeadless(controller, prompt, options = {}) {
|
|
|
96
97
|
`${event.outcome.aborted ? ' (aborted)' : ''}\n`);
|
|
97
98
|
}
|
|
98
99
|
});
|
|
100
|
+
// ONE LINE PER TRANSITION OF THE ACTIVE ITEM, not one per call: a model that
|
|
101
|
+
// rewrites the list without moving the cursor produces no output at all.
|
|
102
|
+
//
|
|
103
|
+
// These lines are interleaved with the generic `> todo_write (1ms)` pairs
|
|
104
|
+
// above, because that loop keys on the EVENT and not on the tool name (P2-2).
|
|
105
|
+
// `SELF_RENDERING_TOOLS` is a TRANSCRIPT concept and teaching this writer
|
|
106
|
+
// about it would put the same name in two places for a cosmetic gain on a
|
|
107
|
+
// stream that is already `--quiet`-able in full.
|
|
108
|
+
let announcedTotal = 0;
|
|
109
|
+
let lastActive = -1;
|
|
110
|
+
const unsubscribeTodos = controller.subscribeTodos?.((event) => {
|
|
111
|
+
if (quiet || event.type !== 'updated')
|
|
112
|
+
return;
|
|
113
|
+
const { total, doneCount, activeIndex, items } = event.snapshot;
|
|
114
|
+
if (total !== announcedTotal) {
|
|
115
|
+
announcedTotal = total;
|
|
116
|
+
err.write(`\n[todo] ${total} ${total === 1 ? 'step' : 'steps'} planned\n`);
|
|
117
|
+
lastActive = -1;
|
|
118
|
+
}
|
|
119
|
+
if (activeIndex < 0) {
|
|
120
|
+
if (lastActive !== -2) {
|
|
121
|
+
lastActive = -2;
|
|
122
|
+
err.write(`[todo] ${doneCount}/${total} done\n`);
|
|
123
|
+
}
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (activeIndex === lastActive)
|
|
127
|
+
return;
|
|
128
|
+
lastActive = activeIndex;
|
|
129
|
+
err.write(`[todo] ${activeIndex + 1}/${total} ${items[activeIndex]?.content ?? ''}\n`);
|
|
130
|
+
});
|
|
131
|
+
// --- The bounded continuation loop (todo-plan-followthrough §3.6). --------
|
|
132
|
+
//
|
|
133
|
+
// THE THREE `unsubscribe` CALLS STAY OUTSIDE IT (rule 4). The loop wraps the
|
|
134
|
+
// whole `try/finally` rather than sitting inside it, because unsubscribing per
|
|
135
|
+
// iteration would silently cost iteration 2 onwards its `[todo]` progress
|
|
136
|
+
// lines, its team usage accounting and — worst — the `agent_end` handler that
|
|
137
|
+
// computes `errored`, so a failing second iteration would exit 0.
|
|
138
|
+
//
|
|
139
|
+
// Deciding the moment `prompt()` resolves is safe: `agent.prompt()` awaits
|
|
140
|
+
// `runLoopWithLifecycle()`, whose `finally` emits `agent_end` BEFORE resolving,
|
|
141
|
+
// and `emit` is synchronous — so `errored` and `sawTurnEnd` are already final.
|
|
142
|
+
const followThrough = options.followThrough ?? 'notify';
|
|
143
|
+
let budget = emptyBudget();
|
|
144
|
+
let wasAutoContinuation = false;
|
|
99
145
|
try {
|
|
100
|
-
|
|
146
|
+
let message = prompt;
|
|
147
|
+
for (;;) {
|
|
148
|
+
// PER-ITERATION RESET (rule 1). `sawTurnEnd` is the silent-failure
|
|
149
|
+
// detector; left sticky, a swallowed throw on iteration 2 is invisible
|
|
150
|
+
// because iteration 1 set the flag. `errored` is deliberately NOT reset —
|
|
151
|
+
// once a run has errored the exit code is 1 and the loop must stop.
|
|
152
|
+
sawTurnEnd = false;
|
|
153
|
+
await controller.prompt(message);
|
|
154
|
+
const snapshot = controller.getTodoSnapshot?.() ?? null;
|
|
155
|
+
budget = advanceBudget(budget, snapshot, wasAutoContinuation);
|
|
156
|
+
wasAutoContinuation = false;
|
|
157
|
+
const decision = decideFollowThrough({
|
|
158
|
+
mode: followThrough,
|
|
159
|
+
snapshot,
|
|
160
|
+
// `aborted` is STRUCTURALLY false here (C-2): `-p` has no Esc and
|
|
161
|
+
// installs no signal handler that marks one, so there is nothing to
|
|
162
|
+
// derive and nothing to plumb. An errored run takes the `notify` branch
|
|
163
|
+
// below and ends the loop, which is what keeps exit code 1 reachable.
|
|
164
|
+
runEnd: { aborted: false, errored },
|
|
165
|
+
budget,
|
|
166
|
+
interactive: false,
|
|
167
|
+
});
|
|
168
|
+
if (decision.kind === 'none')
|
|
169
|
+
break;
|
|
170
|
+
if (decision.kind === 'notify') {
|
|
171
|
+
if (!quiet)
|
|
172
|
+
err.write(`\n[todo] ${decision.text}\n`);
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
// CHARGED BEFORE THE RE-PROMPT, and this line is the whole of the headless
|
|
176
|
+
// cap (P1-2 / I-5 / AC-37). The TUI charges `used` in its grace-timer
|
|
177
|
+
// callback; headless has no timer, so without this `maxAutoContinuesPerList`
|
|
178
|
+
// would never bind in the one environment with no human to press Esc.
|
|
179
|
+
budget = { ...budget, used: budget.used + 1 };
|
|
180
|
+
wasAutoContinuation = true;
|
|
181
|
+
if (!quiet)
|
|
182
|
+
err.write(`\n[todo] ${decision.notice}\n`);
|
|
183
|
+
message = decision.message;
|
|
184
|
+
}
|
|
101
185
|
}
|
|
102
186
|
finally {
|
|
103
187
|
unsubscribe();
|
|
104
188
|
unsubscribeTeam?.();
|
|
189
|
+
unsubscribeTodos?.();
|
|
105
190
|
}
|
|
106
191
|
// Ensure a trailing newline so piped output is well-formed.
|
|
107
192
|
if (wroteText)
|