@aragon-agent/cli 0.5.4 → 0.5.6
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 +670 -577
- package/README.md +1105 -995
- package/dist/agent/controller.d.ts +71 -1
- package/dist/agent/controller.js +135 -1
- package/dist/agent/headless.d.ts +29 -0
- package/dist/agent/headless.js +86 -1
- package/dist/agent/reducer.d.ts +45 -0
- package/dist/agent/reducer.js +80 -1
- package/dist/agent/system-prompt.d.ts +8 -0
- package/dist/agent/system-prompt.js +2 -0
- package/dist/cli.js +40 -1
- package/dist/commands/builtins.js +166 -0
- package/dist/commands/registry.d.ts +27 -1
- package/dist/config/env.js +43 -0
- package/dist/config/load.d.ts +27 -0
- package/dist/config/load.js +25 -1
- package/dist/config/schema.d.ts +47 -0
- package/dist/config/schema.js +38 -0
- 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 +244 -7
- package/dist/ui/StatusBar.d.ts +18 -0
- package/dist/ui/StatusBar.js +5 -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.js +11 -1
- package/dist/ui/entries/TodoCard.d.ts +31 -0
- package/dist/ui/entries/TodoCard.js +35 -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/ScrollViewport.d.ts +13 -1
- package/dist/ui/layout/ScrollViewport.js +2 -2
- package/dist/ui/layout/rail.d.ts +56 -0
- package/dist/ui/layout/rail.js +68 -0
- package/dist/ui/overlays/HelpOverlay.js +4 -0
- package/package.json +3 -2
|
@@ -53,6 +53,14 @@ export interface SystemPromptParams {
|
|
|
53
53
|
* mixing them a one-character mistake.
|
|
54
54
|
*/
|
|
55
55
|
subagentBlock?: string;
|
|
56
|
+
/**
|
|
57
|
+
* The rendered `<todo_planning>` block, or `''` (todo-plan-execution §3.7).
|
|
58
|
+
*
|
|
59
|
+
* Spliced conditionally exactly as `skillsBlock`, the plan block and
|
|
60
|
+
* `teamBlock` are, which is what keeps C-10 / I-5 true: `todo.enabled: false`
|
|
61
|
+
* (and `--no-todo`) produce the pre-todo prompt BYTE FOR BYTE.
|
|
62
|
+
*/
|
|
63
|
+
todoBlock?: string;
|
|
56
64
|
}
|
|
57
65
|
/** Describe the active shell so the model does not emit incompatible syntax. */
|
|
58
66
|
export declare function describeShell(): {
|
|
@@ -39,6 +39,7 @@ export function buildSystemPrompt(params) {
|
|
|
39
39
|
: '';
|
|
40
40
|
const teamBlock = params.teamBlock ?? '';
|
|
41
41
|
const subagentBlock = params.subagentBlock ?? '';
|
|
42
|
+
const todoBlock = params.todoBlock ?? '';
|
|
42
43
|
return [
|
|
43
44
|
'You are AragonAgent, an autonomous coding assistant operating inside a terminal (TUI).',
|
|
44
45
|
'You help the user accomplish software-engineering tasks by reasoning step by step and using the provided tools.',
|
|
@@ -54,6 +55,7 @@ export function buildSystemPrompt(params) {
|
|
|
54
55
|
...(planBlock ? ['', planBlock] : []),
|
|
55
56
|
...(teamBlock ? ['', teamBlock] : []),
|
|
56
57
|
...(subagentBlock ? ['', subagentBlock] : []),
|
|
58
|
+
...(todoBlock ? ['', todoBlock] : []),
|
|
57
59
|
'',
|
|
58
60
|
'Operating guidance:',
|
|
59
61
|
'- Prefer reading files and inspecting the workspace before making changes.',
|
package/dist/cli.js
CHANGED
|
@@ -86,6 +86,17 @@ function toFlags(opts) {
|
|
|
86
86
|
// reach `CliFlags` and are inert, with no compile error and no runtime error.
|
|
87
87
|
team: opts.team,
|
|
88
88
|
teamMax: opts.teamMax,
|
|
89
|
+
// Site three of three for `--todo` / `--todo-panel`, and the one this
|
|
90
|
+
// package has now paid for twice (`mouse` P1-2, `team` P1-4): a flag needs
|
|
91
|
+
// the `.option()` declaration below, a field on `CliFlags`, AND a line here.
|
|
92
|
+
// Miss this one and the flag is inert with no compile error and no runtime
|
|
93
|
+
// error.
|
|
94
|
+
todo: opts.todo,
|
|
95
|
+
todoPanel: opts.todoPanel,
|
|
96
|
+
// Site three of three for `--todo-follow` as well (todo-plan-followthrough
|
|
97
|
+
// C-9). A value flag is no less inert than a boolean one when this line is
|
|
98
|
+
// missing.
|
|
99
|
+
todoFollow: opts.todoFollow,
|
|
89
100
|
// `--verbose` is just a shorthand for `--log-level debug`; an explicit
|
|
90
101
|
// `--log-level` still wins, so the two can be combined without surprise.
|
|
91
102
|
logLevel: opts.logLevel ?? (opts.verbose ? 'debug' : undefined),
|
|
@@ -120,12 +131,21 @@ function makeController(flags, opts = { interactive: true }) {
|
|
|
120
131
|
// resolved a bootstrap level — it ran before commander and before `.env` were
|
|
121
132
|
// readable — and without this hand-off the fully resolved one never applies.
|
|
122
133
|
getLogger().reconfigure(config.log);
|
|
134
|
+
// Whether this session could EVER show the todo rail (todo-plan-execution
|
|
135
|
+
// §3.7 / P1-6). It varies ONE sentence of `<todo_planning>`; `decideRenderMode`
|
|
136
|
+
// is pure and `runInteractive` calls it again with the same inputs, so
|
|
137
|
+
// computing it here rather than threading it back costs nothing and keeps the
|
|
138
|
+
// controller's construction self-contained.
|
|
139
|
+
const todoPanelCapable = opts.interactive &&
|
|
140
|
+
decideRenderMode({ fullscreen: config.fullscreen }, process.env, process.stdout) ===
|
|
141
|
+
'fullscreen';
|
|
123
142
|
const confirmBridge = { handler: null };
|
|
124
143
|
// `cancelPending` is replaced by the App's own implementation on mount; this
|
|
125
144
|
// no-op keeps the object total so nothing has to null-check it.
|
|
126
145
|
const humanInputBridge = { handler: null, cancelPending: () => { } };
|
|
127
146
|
const controller = new AgentController(config, {
|
|
128
147
|
version: VERSION,
|
|
148
|
+
todoPanelCapable,
|
|
129
149
|
confirm: (req) => confirmBridge.handler ? confirmBridge.handler(req) : Promise.resolve(true),
|
|
130
150
|
approval: opts.interactive
|
|
131
151
|
? {
|
|
@@ -308,7 +328,13 @@ async function runOneShot(flags, prompt, quiet) {
|
|
|
308
328
|
for (const dir of untrusted) {
|
|
309
329
|
process.stderr.write(`[skills] project skills in ${dir} skipped (untrusted). Run: aragon skills trust ${dir}\n`);
|
|
310
330
|
}
|
|
311
|
-
|
|
331
|
+
// `followThrough` is read off the REAL controller here rather than through
|
|
332
|
+
// `HeadlessController`, which is deliberately the minimal surface a test can
|
|
333
|
+
// satisfy with an object literal (todo-plan-followthrough §3.6).
|
|
334
|
+
const code = await runHeadless(controller, prompt, {
|
|
335
|
+
quiet,
|
|
336
|
+
followThrough: controller.getTodoConfig().followThrough,
|
|
337
|
+
});
|
|
312
338
|
// Flushed HERE and not inside `runHeadless`: that module is deliberately
|
|
313
339
|
// injectable (its own controller interface, its own stdout/stderr) and has an
|
|
314
340
|
// early `return 2`, so a singleton dependency in there would be both a new
|
|
@@ -602,6 +628,19 @@ function buildProgram() {
|
|
|
602
628
|
.option('--team', 'Enable team subagents for this session (the default)')
|
|
603
629
|
.option('--no-team', 'Disable team subagents (the task tool is not registered at all)')
|
|
604
630
|
.option('--team-max <n>', 'Max subagents per dispatch for this run (1-10)')
|
|
631
|
+
// BOTH PAIRS, positive first (todo-plan-execution §4.3 / P1-8). `--todo` is
|
|
632
|
+
// the `--team` story verbatim; `--todo-panel` needs the pair for a second
|
|
633
|
+
// reason on top of it — `panel` is PERSISTED, so a lone `--no-todo-panel`
|
|
634
|
+
// would make commander default `opts.todoPanel` to `true` and silently
|
|
635
|
+
// overwrite a stored `panel: false` on every run that passed no flag.
|
|
636
|
+
.option('--todo', 'Enable todo planning for this session (the default)')
|
|
637
|
+
.option('--no-todo', 'Disable todo planning (the todo_write tool is not registered at all)')
|
|
638
|
+
.option('--todo-panel', 'Show the todo plan in a right-hand rail (the default)')
|
|
639
|
+
.option('--no-todo-panel', 'Keep todo planning but do not render the rail')
|
|
640
|
+
// NOT A PAIR, and it does not need to be: a value flag is absent unless
|
|
641
|
+
// passed, so `!== undefined` in `resolveTodoConfig` already distinguishes it
|
|
642
|
+
// from silence. An unrecognized mode is clamped to `notify`, not rejected.
|
|
643
|
+
.option('--todo-follow <mode>', 'What to do when a run ends with unfinished steps: notify (default) | auto | off')
|
|
605
644
|
.option('--no-skills', 'Disable the skill system entirely (no catalog, no skill tools)')
|
|
606
645
|
.option('--skill <name>', 'Force-load a skill at Level 2 for this run (repeatable)', (value, previous = []) => [...previous, value])
|
|
607
646
|
.option('--skills-yes', 'Approve skill installs for this run (CI / headless)')
|
|
@@ -9,6 +9,8 @@ import { isAbsolute, resolve } from 'node:path';
|
|
|
9
9
|
import process from 'node:process';
|
|
10
10
|
import { resolveOutputTokens } from '@aragon-agent/core';
|
|
11
11
|
import { clampThinkingLevel, clampTheme, isThemeName, parseMaxTokensInput, DEFAULT_MAX_TOKENS, LEGACY_DARK_THEME, THEME_NAMES, THINKING_LEVELS, } from '../config/schema.js';
|
|
12
|
+
import { TODO_FOLLOW_LIMITS } from '../todo/limits.js';
|
|
13
|
+
import { buildContinuationMessage } from '../todo/follow-through.js';
|
|
12
14
|
import { MODE_LABEL, nextMode } from '../agent/agent-mode.js';
|
|
13
15
|
import { loadConfig } from '../config/load.js';
|
|
14
16
|
import { loadSession, resolveSessionPath, saveSession } from '../session/persist.js';
|
|
@@ -220,6 +222,160 @@ const COMMANDS = [
|
|
|
220
222
|
ctx.notify('warn', `Unknown argument "${verb}" - use /team [status|on|off|max <n>].`);
|
|
221
223
|
},
|
|
222
224
|
},
|
|
225
|
+
{
|
|
226
|
+
name: 'todo',
|
|
227
|
+
description: 'Todo planning: status | on | off | panel on|off | follow <mode> | clear | continue',
|
|
228
|
+
/**
|
|
229
|
+
* `/todo on` CANNOT turn planning on in a session that started with
|
|
230
|
+
* `--no-todo`, for the reason `/team on` above records at length: the tool
|
|
231
|
+
* array is built ONCE and rebuilding it is forbidden (C-1), while
|
|
232
|
+
* `rebuildSystemPrompt()` IS a live path — so the naive reading produces a
|
|
233
|
+
* session where the model is told about a `todo_write` that was never
|
|
234
|
+
* registered.
|
|
235
|
+
*
|
|
236
|
+
* TWO CALLS, IN THIS ORDER, FOR BOTH `on|off` AND `panel on|off`, and
|
|
237
|
+
* neither pair is optional:
|
|
238
|
+
*
|
|
239
|
+
* `setTodoEnabled` re-splices `<todo_planning>` (P1-1). Without it the
|
|
240
|
+
* block survives `/todo off` and every call comes back
|
|
241
|
+
* refused while the instructions to keep calling are
|
|
242
|
+
* still in the model's context.
|
|
243
|
+
* `setTodoConfig` updates the object `App` reads at RENDER time (P1-2).
|
|
244
|
+
* `persistConfig` only writes the file, so without it
|
|
245
|
+
* `/todo panel off` reports success and changes nothing
|
|
246
|
+
* until the next launch.
|
|
247
|
+
* `persistConfig` is what survives the session, in both cases.
|
|
248
|
+
*/
|
|
249
|
+
run: (ctx) => {
|
|
250
|
+
const arg = ctx.args.trim().toLowerCase();
|
|
251
|
+
const [verb, value] = arg.split(/\s+/, 2);
|
|
252
|
+
const controller = ctx.controller;
|
|
253
|
+
if (!verb || verb === 'status') {
|
|
254
|
+
const snapshot = controller.getTodoSnapshot();
|
|
255
|
+
const state = !controller.isTodoRegistered()
|
|
256
|
+
? 'off for this session (started with --no-todo)'
|
|
257
|
+
: controller.isTodoEnabled()
|
|
258
|
+
? 'on'
|
|
259
|
+
: 'off';
|
|
260
|
+
const todoCfg = controller.getTodoConfig();
|
|
261
|
+
const panel = todoCfg.panel ? 'on' : 'off';
|
|
262
|
+
// THE COUNTERS APPEAR ONLY IN `auto` MODE (§4.3): printing `0/25` under
|
|
263
|
+
// `notify` would advertise a budget nothing is spending.
|
|
264
|
+
const follow = todoCfg.followThrough === 'auto'
|
|
265
|
+
? `auto (${ctx.followBudget?.used ?? 0}/` +
|
|
266
|
+
`${TODO_FOLLOW_LIMITS.maxAutoContinuesPerList} used)`
|
|
267
|
+
: todoCfg.followThrough;
|
|
268
|
+
if (!snapshot) {
|
|
269
|
+
ctx.notify('info', `No todo list. Todo planning: ${state}. Panel: ${panel}. ` +
|
|
270
|
+
`Follow-through: ${follow}.`);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
const active = snapshot.activeIndex >= 0
|
|
274
|
+
? ` In progress: "${snapshot.items[snapshot.activeIndex]?.content ?? ''}".`
|
|
275
|
+
: '';
|
|
276
|
+
ctx.notify('info', `Todos: ${snapshot.doneCount}/${snapshot.total} done.${active} Panel: ${panel}. ` +
|
|
277
|
+
`Follow-through: ${follow}.`);
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
if (verb === 'on' || verb === 'off') {
|
|
281
|
+
const enabled = verb === 'on';
|
|
282
|
+
// A PARTIAL `todo` section, safe only because `updatePersistedConfig`
|
|
283
|
+
// deep-merges it — the contract §4.2 spells out in full.
|
|
284
|
+
ctx.persistConfig({ todo: { enabled } });
|
|
285
|
+
if (!controller.isTodoRegistered()) {
|
|
286
|
+
ctx.notify('info', enabled
|
|
287
|
+
? 'Todo planning is off for this session (started with --no-todo). Saved for next launch.'
|
|
288
|
+
: 'Todo planning is already off for this session. Saved for next launch.');
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
controller.setTodoEnabled(enabled);
|
|
292
|
+
ctx.toast('success', `Todo planning ${enabled ? 'on' : 'off'}.`);
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
if (verb === 'panel') {
|
|
296
|
+
if (value !== 'on' && value !== 'off') {
|
|
297
|
+
ctx.notify('warn', 'Usage: /todo panel <on|off>');
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
const panel = value === 'on';
|
|
301
|
+
controller.setTodoConfig({ panel });
|
|
302
|
+
ctx.persistConfig({ todo: { panel } });
|
|
303
|
+
ctx.toast('success', `Todo panel ${panel ? 'on' : 'off'}.`);
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
if (verb === 'follow') {
|
|
307
|
+
if (value !== 'notify' && value !== 'auto' && value !== 'off') {
|
|
308
|
+
ctx.notify('warn', 'Usage: /todo follow <notify|auto|off>');
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
const followThrough = value;
|
|
312
|
+
// THE SAME TWO-CALL RULE AS `panel` ABOVE, minus `setTodoEnabled`:
|
|
313
|
+
// nothing in `<todo_planning>` mentions follow-through (D-3), so
|
|
314
|
+
// `todoEnabled` has no bearing here.
|
|
315
|
+
//
|
|
316
|
+
// `setTodoConfig` REBUILDS THE SYSTEM PROMPT UNCONDITIONALLY
|
|
317
|
+
// (`controller.ts`), and that is fine and must be left alone: the
|
|
318
|
+
// rebuild is idempotent for this key (the block does not mention
|
|
319
|
+
// follow-through, so the output is byte-identical), it costs one string
|
|
320
|
+
// composition PER COMMAND INVOCATION rather than per turn, and
|
|
321
|
+
// `setTodoConfig` is the ONLY method that updates the object `App` reads
|
|
322
|
+
// at decision time. A branch that bypassed it to avoid the rebuild would
|
|
323
|
+
// trade a free string concatenation for the exact "reports success,
|
|
324
|
+
// changes nothing" bug the method exists to prevent (P1-1 / P2-10).
|
|
325
|
+
controller.setTodoConfig({ followThrough });
|
|
326
|
+
ctx.persistConfig({ todo: { followThrough } });
|
|
327
|
+
ctx.toast('success', followThrough === 'auto'
|
|
328
|
+
? `Follow-through: auto (up to ${TODO_FOLLOW_LIMITS.maxAutoContinuesPerList} ` +
|
|
329
|
+
'continuations per plan).'
|
|
330
|
+
: `Follow-through: ${followThrough}.`);
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
if (verb === 'clear') {
|
|
334
|
+
// REFUSED MID-RUN, which bounds the model/panel disagreement window to
|
|
335
|
+
// zero (§1.2): between turns the model's next `todo_write` is a full
|
|
336
|
+
// replacement anyway.
|
|
337
|
+
if (controller.isRunning()) {
|
|
338
|
+
ctx.notify('warn', 'Cannot clear the todo list while a run is in progress.');
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
if (!controller.getTodoSnapshot()) {
|
|
342
|
+
ctx.notify('info', 'No todo list.');
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
controller.clearTodos();
|
|
346
|
+
ctx.toast('info', 'Todo list cleared.');
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
if (verb === 'continue') {
|
|
350
|
+
// REFUSED WHILE RUNNING TOO (P2-7). `ctx.submit` is the App's
|
|
351
|
+
// `submitMessage`, which routes to `controller.steer()` when the status
|
|
352
|
+
// is `running` — so the un-refused form would inject "continue the
|
|
353
|
+
// remaining items" into the run that is already doing exactly that.
|
|
354
|
+
if (controller.isRunning()) {
|
|
355
|
+
ctx.notify('warn', 'Already running.');
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
const snapshot = controller.getTodoSnapshot();
|
|
359
|
+
if (!snapshot || snapshot.doneCount >= snapshot.total) {
|
|
360
|
+
ctx.notify('info', 'Nothing left to continue.');
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
// THE SAME FUNCTION THE AUTO PATH USES, which is what keeps the two from
|
|
364
|
+
// drifting (D-11 / D-12). Round 1's fixed string "Continue with the
|
|
365
|
+
// remaining todo items." was a REFERENCE to a `todo_write` call that a
|
|
366
|
+
// long session may have scrolled out of view.
|
|
367
|
+
//
|
|
368
|
+
// `{ userInitiated: false }`: the user typed `/todo continue`, which the
|
|
369
|
+
// slash history already has. Putting this canned paragraph under the
|
|
370
|
+
// composer's up-arrow as well would be the same event recorded twice, in
|
|
371
|
+
// the more verbose of the two forms (AC-17).
|
|
372
|
+
ctx.submit(buildContinuationMessage(snapshot), { userInitiated: false });
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
ctx.notify('warn', `Unknown argument "${verb}" - use ` +
|
|
376
|
+
'/todo [status|on|off|panel on|off|follow <mode>|clear|continue].');
|
|
377
|
+
},
|
|
378
|
+
},
|
|
223
379
|
{
|
|
224
380
|
name: 'expand',
|
|
225
381
|
description: 'Expand / collapse a recent tool card (default: last)',
|
|
@@ -296,6 +452,9 @@ const COMMANDS = [
|
|
|
296
452
|
},
|
|
297
453
|
messages: ctx.controller.getMessages(),
|
|
298
454
|
entries: ctx.state.entries,
|
|
455
|
+
// `/resume` restores `messages`, so the belief that justifies the
|
|
456
|
+
// panel comes back — and the panel has to come back with it (§3.13).
|
|
457
|
+
todos: ctx.controller.getTodoSnapshot()?.items ?? [],
|
|
299
458
|
});
|
|
300
459
|
ctx.notify('info', `Saved session to ${path}`);
|
|
301
460
|
}
|
|
@@ -312,6 +471,13 @@ const COMMANDS = [
|
|
|
312
471
|
const path = resolveSessionPath(ctx.args, ctx.controller.getCwd());
|
|
313
472
|
const session = loadSession(path);
|
|
314
473
|
ctx.controller.replaceMessages(session.messages);
|
|
474
|
+
// IN THE SAME `try`, IMMEDIATELY AFTER `replaceMessages`, so a malformed
|
|
475
|
+
// file fails the whole resume rather than half of it — and AN ABSENT
|
|
476
|
+
// `todos` CLEARS RATHER THAN SKIPPING (P0-2 / D-22). `replaceMessages`
|
|
477
|
+
// has just discarded the conversation the current list belonged to, so
|
|
478
|
+
// leaving the rail up produces a panel that disagrees with the model,
|
|
479
|
+
// which §1.2 ranks below having no panel at all.
|
|
480
|
+
ctx.controller.restoreTodos(session.todos ?? []);
|
|
315
481
|
ctx.dispatch({ type: 'restoreEntries', entries: session.entries });
|
|
316
482
|
if (session.model?.providerId && session.model?.modelId) {
|
|
317
483
|
ctx.controller.setModel(session.model.providerId, session.model.modelId, session.model.baseUrl);
|
|
@@ -6,6 +6,7 @@ import type { AgentController } from '../agent/controller.js';
|
|
|
6
6
|
import type { AgentMode } from '../agent/agent-mode.js';
|
|
7
7
|
import type { NoticeLevel, Overlay, ToastLevel, ViewAction, ViewState } from '../agent/reducer.js';
|
|
8
8
|
import type { PersistedConfig } from '../config/schema.js';
|
|
9
|
+
import type { FollowThroughBudget } from '../todo/follow-through.js';
|
|
9
10
|
export interface CommandContext {
|
|
10
11
|
/** Raw argument string after the command word (may be empty). */
|
|
11
12
|
args: string;
|
|
@@ -24,8 +25,33 @@ export interface CommandContext {
|
|
|
24
25
|
* idle). Dynamic skill commands need this: they turn `/pdf-forms a.pdf` into a
|
|
25
26
|
* real user message. It is a separate entry point from `handleSubmit` on
|
|
26
27
|
* purpose — routing back through that would re-enter slash-command parsing.
|
|
28
|
+
*
|
|
29
|
+
* `userInitiated: false` suppresses `recordPrompt` (todo-plan-followthrough
|
|
30
|
+
* C-12 / AC-17), which skips TWO things and both are wanted: the prompt-history
|
|
31
|
+
* append, and the `submitCount` bump that drives the composer's hint fade. A
|
|
32
|
+
* canned string the user did not type belongs in neither. The parameter is
|
|
33
|
+
* named for the CAUSE rather than for one of its effects, because a flag
|
|
34
|
+
* called `recordHistory` would have made the second one look like a bug.
|
|
35
|
+
*
|
|
36
|
+
* WIDENING THIS SIGNATURE IS THE WHOLE REASON THIS FILE IS IN THE CHANGE PLAN
|
|
37
|
+
* (P1-5): `builtins.ts` reaches `submitMessage` only through here, so
|
|
38
|
+
* `/todo continue` cannot opt out of prompt history without it.
|
|
39
|
+
*/
|
|
40
|
+
submit: (text: string, opts?: {
|
|
41
|
+
userInitiated?: boolean;
|
|
42
|
+
}) => void;
|
|
43
|
+
/**
|
|
44
|
+
* The live auto-continue budget, for `/todo status`'s `(2/25 used)` clause
|
|
45
|
+
* (todo-plan-followthrough §4.3).
|
|
46
|
+
*
|
|
47
|
+
* READ-ONLY AND OPTIONAL. It lives in an `App` ref rather than on the
|
|
48
|
+
* controller or in `ViewState` (§4.4 / §5.2) because it counts a SESSION'S
|
|
49
|
+
* INTERACTION WITH THE USER, not the model's plan — so this is the only way a
|
|
50
|
+
* command can see it, and `makeCtx` is the only place that supplies it.
|
|
51
|
+
* Optional so the many test contexts that build this object by literal need no
|
|
52
|
+
* change; absent means the clause degrades to naming the ceiling alone.
|
|
27
53
|
*/
|
|
28
|
-
|
|
54
|
+
followBudget?: FollowThroughBudget;
|
|
29
55
|
/** Rebuild the system prompt and the dynamic command list after a skill change. */
|
|
30
56
|
refreshSkills: () => void;
|
|
31
57
|
/**
|
package/dist/config/env.js
CHANGED
|
@@ -134,6 +134,49 @@ export function readEnvConfig() {
|
|
|
134
134
|
if (Object.keys(section).length > 0)
|
|
135
135
|
partial.team = section;
|
|
136
136
|
}
|
|
137
|
+
// ARAGON_TODO=0 turns todo planning off entirely (no tool, no rail). Parsed by
|
|
138
|
+
// the POSITIVE list, exactly as ARAGON_FULLSCREEN / ARAGON_MOUSE / ARAGON_PLAN
|
|
139
|
+
// / ARAGON_TEAM are — NOT by `envBool` below, which uses a negative list. The
|
|
140
|
+
// two readers disagree on values like `ARAGON_TODO=disable`, and this file
|
|
141
|
+
// already records that once for team mode.
|
|
142
|
+
//
|
|
143
|
+
// There is no `ARAGON_TODO_PANEL`: the panel is a per-machine display
|
|
144
|
+
// preference, which is what `config.json` is for, and every env var here
|
|
145
|
+
// exists because a flag and a config file are BOTH unreachable in the channel
|
|
146
|
+
// it serves (containers, SSH, being spawned by another tool).
|
|
147
|
+
//
|
|
148
|
+
// ARAGON_TODO_FOLLOW=notify|auto|off PASSES THAT SAME TEST (D-18), which is
|
|
149
|
+
// why it exists where `ARAGON_TODO_PANEL` does not: follow-through is exactly
|
|
150
|
+
// the knob an unattended `-p` run in a container with a fixed entrypoint needs
|
|
151
|
+
// and can reach no other way.
|
|
152
|
+
//
|
|
153
|
+
// ONE ACCUMULATED SECTION, ASSIGNED ONCE — the shape the `team` branch above
|
|
154
|
+
// already uses, and NOT two `if` blocks each assigning `partial.todo` (P1-4).
|
|
155
|
+
// THE CAST IS WHAT WOULD MAKE THAT MISTAKE SILENT: a second assignment
|
|
156
|
+
// compiles clean and drops `enabled`, so `ARAGON_TODO=0 ARAGON_TODO_FOLLOW=auto`
|
|
157
|
+
// would quietly re-enable the tool the user turned off.
|
|
158
|
+
const todo = process.env.ARAGON_TODO?.trim().toLowerCase();
|
|
159
|
+
const todoFollow = process.env.ARAGON_TODO_FOLLOW?.trim().toLowerCase();
|
|
160
|
+
if ((todo !== undefined && todo.length > 0) ||
|
|
161
|
+
(todoFollow !== undefined && todoFollow.length > 0)) {
|
|
162
|
+
// Keyed by `TodoConfig` but VALUED `unknown`, which is what lets the mode be
|
|
163
|
+
// written through unvalidated (below) while still failing to compile if a
|
|
164
|
+
// future key is misspelled.
|
|
165
|
+
const section = {};
|
|
166
|
+
if (todo !== undefined && todo.length > 0) {
|
|
167
|
+
section.enabled = todo === '1' || todo === 'true' || todo === 'on' || todo === 'yes';
|
|
168
|
+
}
|
|
169
|
+
// Written through unvalidated: `clampTodoConfig` clamps an unrecognized mode
|
|
170
|
+
// to the default rather than rejecting it, so validating here would be a
|
|
171
|
+
// second gate that can disagree with the first.
|
|
172
|
+
if (todoFollow !== undefined && todoFollow.length > 0) {
|
|
173
|
+
section.followThrough = todoFollow;
|
|
174
|
+
}
|
|
175
|
+
// A PARTIAL section, safe only because every consumer deep-merges it — the
|
|
176
|
+
// same contract `partial.skills`, `partial.log` and `partial.team` rely on.
|
|
177
|
+
if (Object.keys(section).length > 0)
|
|
178
|
+
partial.todo = section;
|
|
179
|
+
}
|
|
137
180
|
const log = readEnvLogConfig();
|
|
138
181
|
if (log)
|
|
139
182
|
partial.log = log;
|
package/dist/config/load.d.ts
CHANGED
|
@@ -52,6 +52,33 @@ export interface CliFlags {
|
|
|
52
52
|
team?: boolean;
|
|
53
53
|
/** `--team-max <n>` — override `team.maxSubagents` for this run (still clamped). */
|
|
54
54
|
teamMax?: string | number;
|
|
55
|
+
/**
|
|
56
|
+
* `--todo` ⇒ true, `--no-todo` ⇒ false, absent ⇒ undefined
|
|
57
|
+
* (todo-plan-execution §4.3).
|
|
58
|
+
*/
|
|
59
|
+
todo?: boolean;
|
|
60
|
+
/**
|
|
61
|
+
* `--todo-panel` ⇒ true, `--no-todo-panel` ⇒ false, absent ⇒ undefined.
|
|
62
|
+
*
|
|
63
|
+
* DECLARED AS A PAIR, like `--todo` above, and unlike `--no-skills` (P1-8).
|
|
64
|
+
* `panel` is a PERSISTED setting, so "not passed" has to stay distinguishable
|
|
65
|
+
* from `true` — otherwise commander's lone-`--no-x` default silently
|
|
66
|
+
* overwrites a config-file `panel: false` on every run that passes no flag at
|
|
67
|
+
* all. `--no-skills` escapes the rule only because nothing needs to
|
|
68
|
+
* distinguish `--skills` from silence.
|
|
69
|
+
*/
|
|
70
|
+
todoPanel?: boolean;
|
|
71
|
+
/**
|
|
72
|
+
* `--todo-follow <mode>` — `notify` | `auto` | `off`
|
|
73
|
+
* (todo-plan-followthrough §4.2).
|
|
74
|
+
*
|
|
75
|
+
* A VALUE FLAG, NOT A PAIR, so commander materializes it only when passed and
|
|
76
|
+
* the `!== undefined` rule above applies to its presence rather than to a
|
|
77
|
+
* synthesized `true`. Typed `string` and not `FollowThroughMode`: an
|
|
78
|
+
* unrecognized value is CLAMPED to the default by `clampTodoConfig`, never
|
|
79
|
+
* rejected, which is what every other key in this file does.
|
|
80
|
+
*/
|
|
81
|
+
todoFollow?: string;
|
|
55
82
|
/** `--log-level <lv>` — this run only, never persisted. */
|
|
56
83
|
logLevel?: string;
|
|
57
84
|
/** `--log-file` ⇒ true, `--no-log-file` ⇒ false, absent ⇒ undefined. */
|
package/dist/config/load.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* legitimately long single tool run can never be watchdog-killed.
|
|
7
7
|
*/
|
|
8
8
|
import process from 'node:process';
|
|
9
|
-
import { DEFAULT_CONFIG, IDLE_TIMEOUT_MARGIN_MS, clampAskRounds, clampDensity, clampHumanTimeout, clampLogConfig, clampSkillsConfig, clampTeamConfig, clampTheme, clampThinkingLevel, clampMaxTokens, clampTranscriptWindow, coercePositiveInt, isAutoToken, isSkillsToolPolicyMode, } from './schema.js';
|
|
9
|
+
import { DEFAULT_CONFIG, IDLE_TIMEOUT_MARGIN_MS, clampAskRounds, clampDensity, clampHumanTimeout, clampLogConfig, clampSkillsConfig, clampTeamConfig, clampTodoConfig, clampTheme, clampThinkingLevel, clampMaxTokens, clampTranscriptWindow, coercePositiveInt, isAutoToken, isSkillsToolPolicyMode, } from './schema.js';
|
|
10
10
|
import { loadDotenv, readEnvConfig, readEnvDisabledSkills } from './env.js';
|
|
11
11
|
import { readConfigFile } from './store.js';
|
|
12
12
|
import { getConfigPath } from './app-paths.js';
|
|
@@ -97,6 +97,29 @@ function resolveTeamConfig(flags, env, file) {
|
|
|
97
97
|
: {}),
|
|
98
98
|
});
|
|
99
99
|
}
|
|
100
|
+
/**
|
|
101
|
+
* Merge the `todo` section across all four layers (defaults › file › env ›
|
|
102
|
+
* flags), todo-plan-execution §4.3.
|
|
103
|
+
*
|
|
104
|
+
* `!== undefined` ON BOTH FLAGS, and that is the whole point (P1-8). `commander`
|
|
105
|
+
* materializes a lone `--no-x` as `opts.x = true` when the flag is absent, so a
|
|
106
|
+
* truthiness check cannot tell `--no-todo-panel` from "not passed" — and both
|
|
107
|
+
* keys here are persisted, so both need the distinction. This is the same rule
|
|
108
|
+
* `resolveTeamConfig` above states, one feature later.
|
|
109
|
+
*/
|
|
110
|
+
function resolveTodoConfig(flags, env, file) {
|
|
111
|
+
return clampTodoConfig({
|
|
112
|
+
...DEFAULT_CONFIG.todo,
|
|
113
|
+
...(file.todo ?? {}),
|
|
114
|
+
...(env.todo ?? {}),
|
|
115
|
+
...(flags.todo !== undefined ? { enabled: flags.todo } : {}),
|
|
116
|
+
...(flags.todoPanel !== undefined ? { panel: flags.todoPanel } : {}),
|
|
117
|
+
// `!== undefined` for the third time, though for a value flag the risk is
|
|
118
|
+
// milder: commander leaves `opts.todoFollow` absent unless it was passed.
|
|
119
|
+
// Written this way anyway so the three lines read as one rule.
|
|
120
|
+
...(flags.todoFollow !== undefined ? { followThrough: flags.todoFollow } : {}),
|
|
121
|
+
});
|
|
122
|
+
}
|
|
100
123
|
/** Merge the `log` section across all four layers (defaults › file › env › flags). */
|
|
101
124
|
function resolveLogConfig(flags, env, file) {
|
|
102
125
|
const merged = clampLogConfig({
|
|
@@ -285,6 +308,7 @@ export function loadConfig(flags = {}) {
|
|
|
285
308
|
},
|
|
286
309
|
log: resolveLogConfig(flags, env.partial, file),
|
|
287
310
|
team: resolveTeamConfig(flags, env.partial, file),
|
|
311
|
+
todo: resolveTodoConfig(flags, env.partial, file),
|
|
288
312
|
cwd,
|
|
289
313
|
color,
|
|
290
314
|
colorLevel,
|
package/dist/config/schema.d.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { type ThinkingLevel } from '@aragon-agent/core';
|
|
9
9
|
import { type LogLevelName } from '../logging/levels.js';
|
|
10
|
+
import type { FollowThroughMode } from '../todo/types.js';
|
|
10
11
|
/**
|
|
11
12
|
* `ModelRegistry` seeds builtin lists for five providers, but `initProviders()`
|
|
12
13
|
* registers only three adapters. Running a model whose provider has no adapter
|
|
@@ -239,6 +240,40 @@ export declare const DEFAULT_TEAM_CONFIG: TeamConfig;
|
|
|
239
240
|
* the two disagree would make the panel's `n/m` readout confusing for no gain.
|
|
240
241
|
*/
|
|
241
242
|
export declare function clampTeamConfig(raw: unknown): TeamConfig;
|
|
243
|
+
export interface TodoConfig {
|
|
244
|
+
/** Register `todo_write` at all. Default TRUE. */
|
|
245
|
+
enabled: boolean;
|
|
246
|
+
/**
|
|
247
|
+
* Render the right rail.
|
|
248
|
+
*
|
|
249
|
+
* INDEPENDENT OF `enabled` ON PURPOSE (D-16): a screen-reader user wants the
|
|
250
|
+
* planning discipline without the column, and the prompt block varies one
|
|
251
|
+
* sentence rather than disappearing (§3.7).
|
|
252
|
+
*/
|
|
253
|
+
panel: boolean;
|
|
254
|
+
/**
|
|
255
|
+
* What happens when a run ends with unfinished steps
|
|
256
|
+
* (todo-plan-followthrough §4.1). Default `'notify'`, which reproduces round
|
|
257
|
+
* 1's behaviour byte for byte.
|
|
258
|
+
*
|
|
259
|
+
* THE THIRD SCALAR, and it stays a scalar for the reason the section header
|
|
260
|
+
* above gives: `store.ts` merges this section BY HAND, and both of its merges
|
|
261
|
+
* spread the whole object (`{ ...DEFAULT_CONFIG.todo, ...(partial.todo ?? {}) }`
|
|
262
|
+
* and `{ ...current.todo, ...(patch.todo ?? {}) }`), so a third scalar key is
|
|
263
|
+
* carried automatically and needs no change there. A NESTED field would not
|
|
264
|
+
* be, and would mean replacing both merges first.
|
|
265
|
+
*/
|
|
266
|
+
followThrough: FollowThroughMode;
|
|
267
|
+
}
|
|
268
|
+
export declare const DEFAULT_TODO_CONFIG: TodoConfig;
|
|
269
|
+
/**
|
|
270
|
+
* THE single gate for every todo-config read AND write, mirroring
|
|
271
|
+
* `clampSkillsConfig` / `clampLogConfig` / `clampTeamConfig` for the reason
|
|
272
|
+
* those record: hardening only the read path leaves a bad value on disk that
|
|
273
|
+
* reverts to the default on every launch, which presents to the user as "my
|
|
274
|
+
* setting won't stick".
|
|
275
|
+
*/
|
|
276
|
+
export declare function clampTodoConfig(raw: unknown): TodoConfig;
|
|
242
277
|
export interface PersistedConfig {
|
|
243
278
|
version: number;
|
|
244
279
|
provider: string;
|
|
@@ -305,6 +340,8 @@ export interface PersistedConfig {
|
|
|
305
340
|
log: LogConfig;
|
|
306
341
|
/** Team mode. The FOURTH nested object; scalars only (see TeamConfig). */
|
|
307
342
|
team: TeamConfig;
|
|
343
|
+
/** Todo planning. The FIFTH nested object; scalars only (see TodoConfig). */
|
|
344
|
+
todo: TodoConfig;
|
|
308
345
|
}
|
|
309
346
|
export declare const DEFAULT_CONFIG: PersistedConfig;
|
|
310
347
|
/**
|
|
@@ -399,6 +436,16 @@ export interface CliConfig {
|
|
|
399
436
|
* `/team` flips is a separate flag on the controller.
|
|
400
437
|
*/
|
|
401
438
|
team: TeamConfig;
|
|
439
|
+
/**
|
|
440
|
+
* Resolved todo-planning settings (todo-plan-execution §4.2).
|
|
441
|
+
*
|
|
442
|
+
* `enabled` here decides whether `todo_write` is REGISTERED, which happens
|
|
443
|
+
* once at construction and can never be undone (C-1). The live on/off switch
|
|
444
|
+
* `/todo` flips is a separate flag on the controller; `panel` is read at
|
|
445
|
+
* RENDER time through `controller.getConfig()`, which is why `/todo panel`
|
|
446
|
+
* must call `setTodoConfig` as well as persisting (P1-2).
|
|
447
|
+
*/
|
|
448
|
+
todo: TodoConfig;
|
|
402
449
|
/**
|
|
403
450
|
* Lifetime submit count driving the composer hint fade (`HINT_FADE_AFTER`).
|
|
404
451
|
*
|
package/dist/config/schema.js
CHANGED
|
@@ -312,6 +312,40 @@ export function clampTeamConfig(raw) {
|
|
|
312
312
|
maxTurnsPerSubagent: clampInt(src.maxTurnsPerSubagent, DEFAULT_TEAM_CONFIG.maxTurnsPerSubagent, TURNS_RANGE),
|
|
313
313
|
};
|
|
314
314
|
}
|
|
315
|
+
export const DEFAULT_TODO_CONFIG = {
|
|
316
|
+
enabled: true,
|
|
317
|
+
panel: true,
|
|
318
|
+
followThrough: 'notify',
|
|
319
|
+
};
|
|
320
|
+
/**
|
|
321
|
+
* There is no enum helper in this file, so this follows the `isThinkingLevel`
|
|
322
|
+
* shape already here rather than inventing a second idiom.
|
|
323
|
+
*/
|
|
324
|
+
const FOLLOW_THROUGH_MODES = ['notify', 'auto', 'off'];
|
|
325
|
+
function followMode(value, fallback) {
|
|
326
|
+
return typeof value === 'string' && FOLLOW_THROUGH_MODES.includes(value)
|
|
327
|
+
? value
|
|
328
|
+
: fallback;
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* THE single gate for every todo-config read AND write, mirroring
|
|
332
|
+
* `clampSkillsConfig` / `clampLogConfig` / `clampTeamConfig` for the reason
|
|
333
|
+
* those record: hardening only the read path leaves a bad value on disk that
|
|
334
|
+
* reverts to the default on every launch, which presents to the user as "my
|
|
335
|
+
* setting won't stick".
|
|
336
|
+
*/
|
|
337
|
+
export function clampTodoConfig(raw) {
|
|
338
|
+
const src = (raw && typeof raw === 'object' ? raw : {});
|
|
339
|
+
return {
|
|
340
|
+
enabled: bool(src.enabled, DEFAULT_TODO_CONFIG.enabled),
|
|
341
|
+
panel: bool(src.panel, DEFAULT_TODO_CONFIG.panel),
|
|
342
|
+
// CLAMPED TO THE DEFAULT, NEVER REJECTED — the discipline every other key in
|
|
343
|
+
// this file follows, and the trade round 1 already made for `--thinking`.
|
|
344
|
+
// `ARAGON_TODO_FOLLOW=garbage` therefore resolves to `'notify'` and throws
|
|
345
|
+
// nothing (AC-5).
|
|
346
|
+
followThrough: followMode(src.followThrough, DEFAULT_TODO_CONFIG.followThrough),
|
|
347
|
+
};
|
|
348
|
+
}
|
|
315
349
|
export const DEFAULT_CONFIG = {
|
|
316
350
|
version: CONFIG_VERSION,
|
|
317
351
|
provider: 'anthropic',
|
|
@@ -340,6 +374,10 @@ export const DEFAULT_CONFIG = {
|
|
|
340
374
|
skills: DEFAULT_SKILLS_CONFIG,
|
|
341
375
|
log: DEFAULT_LOG_CONFIG,
|
|
342
376
|
team: DEFAULT_TEAM_CONFIG,
|
|
377
|
+
// The load path's first spread is what supplies the section to a user with no
|
|
378
|
+
// `todo` key at all, so this line is as required as the two merges in
|
|
379
|
+
// `store.ts` (P0-1).
|
|
380
|
+
todo: DEFAULT_TODO_CONFIG,
|
|
343
381
|
};
|
|
344
382
|
/**
|
|
345
383
|
* Runtime state that used to be stored in `config.json` by mistake
|
package/dist/config/store.js
CHANGED
|
@@ -14,7 +14,7 @@ import { dirname } from 'node:path';
|
|
|
14
14
|
import process from 'node:process';
|
|
15
15
|
import { getConfigPath, getSessionsDir } from './app-paths.js';
|
|
16
16
|
import { registerSecretsFrom } from '../logging/secret-registry.js';
|
|
17
|
-
import { CONFIG_VERSION, DEFAULT_CONFIG, clampLogConfig, clampMaxTokens, clampSkillsConfig, clampTeamConfig, isAutoToken, stripLegacyStateKeys, } from './schema.js';
|
|
17
|
+
import { CONFIG_VERSION, DEFAULT_CONFIG, clampLogConfig, clampMaxTokens, clampSkillsConfig, clampTeamConfig, clampTodoConfig, isAutoToken, stripLegacyStateKeys, } from './schema.js';
|
|
18
18
|
/**
|
|
19
19
|
* Normalize `maxTokens` for both directions of the config file.
|
|
20
20
|
*
|
|
@@ -122,6 +122,10 @@ export function loadPersistedConfig() {
|
|
|
122
122
|
log: clampLogConfig({ ...DEFAULT_CONFIG.log, ...(partial.log ?? {}) }),
|
|
123
123
|
// `team` is the fourth. Same one-level rule, same merge.
|
|
124
124
|
team: clampTeamConfig({ ...DEFAULT_CONFIG.team, ...(partial.team ?? {}) }),
|
|
125
|
+
// `todo` is the fifth. Same one-level rule, same merge — and this line is
|
|
126
|
+
// the READ half of the pair that keeps `/todo panel off` from silently
|
|
127
|
+
// unregistering the tool on the next launch (P0-1 / AC-35).
|
|
128
|
+
todo: clampTodoConfig({ ...DEFAULT_CONFIG.todo, ...(partial.todo ?? {}) }),
|
|
125
129
|
};
|
|
126
130
|
}
|
|
127
131
|
/**
|
|
@@ -168,6 +172,11 @@ export function updatePersistedConfig(patch) {
|
|
|
168
172
|
// 4` sends `{ team: { maxSubagents: 4 } }`, which a shallow spread would
|
|
169
173
|
// turn into "changing the fan-out width silently reverted every timeout".
|
|
170
174
|
team: clampTeamConfig({ ...current.team, ...(patch.team ?? {}) }),
|
|
175
|
+
// The fifth section, and its omission is the worst of the five (P0-1):
|
|
176
|
+
// `/todo panel off` sends `{ todo: { panel: false } }`, which a shallow
|
|
177
|
+
// spread turns into "turning the panel off silently unregisters the tool",
|
|
178
|
+
// permanently, from the next launch onward, with nothing anywhere saying so.
|
|
179
|
+
todo: clampTodoConfig({ ...current.todo, ...(patch.todo ?? {}) }),
|
|
171
180
|
};
|
|
172
181
|
writeConfigFile(merged);
|
|
173
182
|
return merged;
|