@aragon-agent/cli 0.5.3 → 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 -540
- package/README.md +1105 -932
- 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 +72 -6
- package/dist/commands/builtins.d.ts +2 -2
- package/dist/commands/builtins.js +217 -3
- package/dist/commands/registry.d.ts +27 -1
- package/dist/config/cli-commands.js +20 -2
- package/dist/config/env.d.ts +2 -1
- package/dist/config/env.js +60 -5
- package/dist/config/load.d.ts +27 -0
- package/dist/config/load.js +67 -2
- package/dist/config/schema.d.ts +111 -2
- package/dist/config/schema.js +111 -2
- package/dist/config/store.js +37 -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.d.ts +22 -1
- package/dist/ui/App.js +292 -13
- 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 +5 -0
- package/dist/ui/overlays/SettingsScreen.d.ts +12 -1
- package/dist/ui/overlays/SettingsScreen.js +38 -1
- 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
|
@@ -27,7 +27,7 @@ import { LOG_CONFIG_SET_KEYS, TEAM_CONFIG_SET_KEYS, applyLogConfigSet, applyTeam
|
|
|
27
27
|
import { attachAgentEvents, attachTeamEvents, installLogging, setScreenRestore, setSignalTerminator, } from './logging/install.js';
|
|
28
28
|
import { getLogger } from './logging/logger.js';
|
|
29
29
|
import { runLogsCommand } from './logging/cli-commands.js';
|
|
30
|
-
import { ADAPTER_PROVIDERS, clampDensity, clampTheme, clampThinkingLevel, clampAskRounds, clampHumanTimeout, clampSkillsIntegrity, clampSkillsToolPolicy, clampTranscriptWindow,
|
|
30
|
+
import { ADAPTER_PROVIDERS, clampDensity, clampTheme, clampThinkingLevel, clampAskRounds, clampHumanTimeout, clampSkillsIntegrity, clampSkillsToolPolicy, clampTranscriptWindow, coercePositiveInt, isAdapterProvider, parseMaxTokensInput, DEFAULT_MAX_TOKENS, DEFAULT_PLAN_HUMAN_TIMEOUT_MS, DEFAULT_PLAN_MAX_ASK_ROUNDS, DEFAULT_SKILLS_CONFIG, } from './config/schema.js';
|
|
31
31
|
import { makeGetApiKey } from './config/load.js';
|
|
32
32
|
import { AgentController, DENY_ALL_APPROVAL } from './agent/controller.js';
|
|
33
33
|
import { runSkillsCommand } from './skills/cli-commands.js';
|
|
@@ -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
|
|
@@ -397,6 +423,29 @@ const CONFIG_SET_KEYS = new Set([
|
|
|
397
423
|
// still printing `Set team.enabled = false`.
|
|
398
424
|
...TEAM_CONFIG_SET_KEYS,
|
|
399
425
|
]);
|
|
426
|
+
/**
|
|
427
|
+
* `aragon config set maxTokens <n|auto|default>`.
|
|
428
|
+
*
|
|
429
|
+
* REJECTS nonsense rather than clamping it, unlike `theme` and `thinkingLevel`.
|
|
430
|
+
* Those two clamp because their bad values are out-of-VOCABULARY; here a
|
|
431
|
+
* non-numeric argument is almost always a typo for a number the user meant, and
|
|
432
|
+
* storing 64000 for `3200O` would quietly discard their intent. An out-of-RANGE
|
|
433
|
+
* number is still clamped, in both directions, by the store's single gate.
|
|
434
|
+
*/
|
|
435
|
+
function runConfigSetMaxTokens(value) {
|
|
436
|
+
const raw = value.trim().toLowerCase();
|
|
437
|
+
const parsed = raw === 'default'
|
|
438
|
+
? { kind: 'value', value: DEFAULT_MAX_TOKENS, clamped: false }
|
|
439
|
+
: parseMaxTokensInput(value);
|
|
440
|
+
if (parsed.kind === 'invalid') {
|
|
441
|
+
process.stderr.write(`Invalid maxTokens "${value}". Use a number, "auto", or "default".\n`);
|
|
442
|
+
process.exitCode = 2;
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
const stored = parsed.kind === 'auto' ? null : parsed.value;
|
|
446
|
+
const merged = updatePersistedConfig({ maxTokens: stored });
|
|
447
|
+
process.stdout.write(`Set maxTokens = ${merged.maxTokens ?? 'auto'}\n`);
|
|
448
|
+
}
|
|
400
449
|
function runConfigSet(key, value) {
|
|
401
450
|
if (!CONFIG_SET_KEYS.has(key)) {
|
|
402
451
|
process.stderr.write(`Unknown config key "${key}". Known keys: ${[...CONFIG_SET_KEYS].join(', ')}.\n`);
|
|
@@ -410,6 +459,13 @@ function runConfigSet(key, value) {
|
|
|
410
459
|
process.stdout.write(`Set ${key} = ${value}\n`);
|
|
411
460
|
return;
|
|
412
461
|
}
|
|
462
|
+
// Handled ahead of the switch because it needs to echo the STORED value, not
|
|
463
|
+
// the typed one: `config set maxTokens 900000` must not report a number that
|
|
464
|
+
// was never written. Same reason `team.*` has its own branch.
|
|
465
|
+
if (key === 'maxTokens') {
|
|
466
|
+
runConfigSetMaxTokens(value);
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
413
469
|
const teamPatch = applyTeamConfigSet(key, value);
|
|
414
470
|
if (teamPatch) {
|
|
415
471
|
// The written value is echoed from the CLAMPED result rather than from the
|
|
@@ -434,9 +490,6 @@ function runConfigSet(key, value) {
|
|
|
434
490
|
case 'thinkingLevel':
|
|
435
491
|
patch.thinkingLevel = clampThinkingLevel(value, 'off');
|
|
436
492
|
break;
|
|
437
|
-
case 'maxTokens':
|
|
438
|
-
patch.maxTokens = coerceMaxTokens(value) ?? null;
|
|
439
|
-
break;
|
|
440
493
|
case 'theme':
|
|
441
494
|
patch.theme = clampTheme(value, 'auto');
|
|
442
495
|
break;
|
|
@@ -540,7 +593,7 @@ function buildProgram() {
|
|
|
540
593
|
.option('--base-url <url>', 'Override the provider base URL')
|
|
541
594
|
.option('--api-key <key>', 'One-shot API key override (not persisted)')
|
|
542
595
|
.option('--thinking <level>', 'off|minimal|low|medium|high|xhigh')
|
|
543
|
-
.option('--max-tokens <n>', 'Output token cap')
|
|
596
|
+
.option('--max-tokens <n|auto>', 'Output token cap: a number, or "auto" for the model ceiling (default 64000)')
|
|
544
597
|
.option('--cwd <dir>', 'Working directory for tools')
|
|
545
598
|
.option('--confirm', 'Confirm each mutating tool call')
|
|
546
599
|
.option('--tool-timeout <ms>', 'Per-tool executor ceiling (default 180000)')
|
|
@@ -575,6 +628,19 @@ function buildProgram() {
|
|
|
575
628
|
.option('--team', 'Enable team subagents for this session (the default)')
|
|
576
629
|
.option('--no-team', 'Disable team subagents (the task tool is not registered at all)')
|
|
577
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')
|
|
578
644
|
.option('--no-skills', 'Disable the skill system entirely (no catalog, no skill tools)')
|
|
579
645
|
.option('--skill <name>', 'Force-load a skill at Level 2 for this run (repeatable)', (value, previous = []) => [...previous, value])
|
|
580
646
|
.option('--skills-yes', 'Approve skill installs for this run (CI / headless)')
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Built-in slash commands (spec §5.2):
|
|
3
|
-
* /help /model /settings /thinking /tools /clear /reset /cwd /save
|
|
4
|
-
* /copy /exit (/quit).
|
|
3
|
+
* /help /model /settings /thinking /max-tokens /tools /clear /reset /cwd /save
|
|
4
|
+
* /resume /copy /exit (/quit).
|
|
5
5
|
*/
|
|
6
6
|
import { CommandRegistry } from './registry.js';
|
|
7
7
|
export declare function registerBuiltinCommands(registry: CommandRegistry): void;
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Built-in slash commands (spec §5.2):
|
|
3
|
-
* /help /model /settings /thinking /tools /clear /reset /cwd /save
|
|
4
|
-
* /copy /exit (/quit).
|
|
3
|
+
* /help /model /settings /thinking /max-tokens /tools /clear /reset /cwd /save
|
|
4
|
+
* /resume /copy /exit (/quit).
|
|
5
5
|
*/
|
|
6
6
|
import { spawn } from 'node:child_process';
|
|
7
7
|
import { existsSync, statSync } from 'node:fs';
|
|
8
8
|
import { isAbsolute, resolve } from 'node:path';
|
|
9
9
|
import process from 'node:process';
|
|
10
|
-
import {
|
|
10
|
+
import { resolveOutputTokens } from '@aragon-agent/core';
|
|
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';
|
|
11
14
|
import { MODE_LABEL, nextMode } from '../agent/agent-mode.js';
|
|
12
15
|
import { loadConfig } from '../config/load.js';
|
|
13
16
|
import { loadSession, resolveSessionPath, saveSession } from '../session/persist.js';
|
|
@@ -43,6 +46,53 @@ const COMMANDS = [
|
|
|
43
46
|
ctx.toast('info', `Thinking level set to "${clamped}".`);
|
|
44
47
|
},
|
|
45
48
|
},
|
|
49
|
+
{
|
|
50
|
+
name: 'max-tokens',
|
|
51
|
+
description: 'Show or set the output token cap (<n> | auto | default)',
|
|
52
|
+
/**
|
|
53
|
+
* Exists next to the settings screen for the reason `/theme` and `/plan` do:
|
|
54
|
+
* the slash palette is the discoverable surface, and this is one keystroke
|
|
55
|
+
* instead of five arrow presses.
|
|
56
|
+
*
|
|
57
|
+
* The no-argument branch reports the EFFECTIVE cap, not just the setting.
|
|
58
|
+
* "I set 64000" and "this model will produce 8192" are different facts, and
|
|
59
|
+
* only the second one explains a truncated answer.
|
|
60
|
+
*/
|
|
61
|
+
run: (ctx) => {
|
|
62
|
+
const arg = ctx.args.trim();
|
|
63
|
+
const cfg = ctx.controller.getConfig();
|
|
64
|
+
if (!arg) {
|
|
65
|
+
const resolution = resolveOutputTokens({
|
|
66
|
+
providerId: cfg.provider,
|
|
67
|
+
modelId: cfg.model,
|
|
68
|
+
...(cfg.maxTokens !== undefined ? { requested: cfg.maxTokens } : {}),
|
|
69
|
+
});
|
|
70
|
+
const setting = cfg.maxTokens === undefined ? 'auto' : String(cfg.maxTokens);
|
|
71
|
+
const why = resolution.clampedBy === 'ceiling' ? ` (${cfg.model} ceiling)` : '';
|
|
72
|
+
ctx.notify('info', `Max tokens: ${setting}. Effective for ${cfg.provider}:${cfg.model}: ` +
|
|
73
|
+
`${resolution.value}${why}.\nUse /max-tokens <n|auto|default>.`);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const parsed = arg.toLowerCase() === 'default'
|
|
77
|
+
? { kind: 'value', value: DEFAULT_MAX_TOKENS, clamped: false }
|
|
78
|
+
: parseMaxTokensInput(arg);
|
|
79
|
+
if (parsed.kind === 'invalid') {
|
|
80
|
+
// Change NOTHING on a typo: silently resolving it to a default would
|
|
81
|
+
// discard the number the user meant to type.
|
|
82
|
+
ctx.notify('warn', `Invalid value "${arg}" - use /max-tokens <n|auto|default>.`);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (parsed.kind === 'auto') {
|
|
86
|
+
ctx.controller.setMaxTokens(undefined);
|
|
87
|
+
ctx.persistConfig({ maxTokens: null });
|
|
88
|
+
ctx.toast('success', 'Max tokens: auto (per-model ceiling).');
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
ctx.controller.setMaxTokens(parsed.value);
|
|
92
|
+
ctx.persistConfig({ maxTokens: parsed.value });
|
|
93
|
+
ctx.toast('success', `Max tokens set to ${parsed.value}.`);
|
|
94
|
+
},
|
|
95
|
+
},
|
|
46
96
|
{
|
|
47
97
|
name: 'theme',
|
|
48
98
|
description: 'Switch the color theme (auto|warm|cool|light)',
|
|
@@ -172,6 +222,160 @@ const COMMANDS = [
|
|
|
172
222
|
ctx.notify('warn', `Unknown argument "${verb}" - use /team [status|on|off|max <n>].`);
|
|
173
223
|
},
|
|
174
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
|
+
},
|
|
175
379
|
{
|
|
176
380
|
name: 'expand',
|
|
177
381
|
description: 'Expand / collapse a recent tool card (default: last)',
|
|
@@ -248,6 +452,9 @@ const COMMANDS = [
|
|
|
248
452
|
},
|
|
249
453
|
messages: ctx.controller.getMessages(),
|
|
250
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 ?? [],
|
|
251
458
|
});
|
|
252
459
|
ctx.notify('info', `Saved session to ${path}`);
|
|
253
460
|
}
|
|
@@ -264,6 +471,13 @@ const COMMANDS = [
|
|
|
264
471
|
const path = resolveSessionPath(ctx.args, ctx.controller.getCwd());
|
|
265
472
|
const session = loadSession(path);
|
|
266
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 ?? []);
|
|
267
481
|
ctx.dispatch({ type: 'restoreEntries', entries: session.entries });
|
|
268
482
|
if (session.model?.providerId && session.model?.modelId) {
|
|
269
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
|
/**
|
|
@@ -135,6 +135,20 @@ function maskConfig(config) {
|
|
|
135
135
|
}
|
|
136
136
|
return { ...config, apiKeys };
|
|
137
137
|
}
|
|
138
|
+
/**
|
|
139
|
+
* The human-readable label for a value whose raw form would mislead, or
|
|
140
|
+
* `undefined` to leave the caller's own formatting alone.
|
|
141
|
+
*
|
|
142
|
+
* `maxTokens: null` is AUTO, not "unset". Printing a bare `null` reads as a
|
|
143
|
+
* missing setting and sends the user looking for a bug that is not there.
|
|
144
|
+
* `get` and `list` format everything else differently (unquoted vs. JSON), so
|
|
145
|
+
* this deliberately overrides one case rather than replacing both renderers.
|
|
146
|
+
*/
|
|
147
|
+
function displayOverride(key, value) {
|
|
148
|
+
if (key === 'maxTokens' && value === null)
|
|
149
|
+
return 'auto';
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
138
152
|
/** Resolve a flat or one-level-dotted key against the persisted config. */
|
|
139
153
|
function readKey(config, key) {
|
|
140
154
|
const [head, tail] = key.split('.', 2);
|
|
@@ -153,10 +167,14 @@ export function runConfigGet(key) {
|
|
|
153
167
|
process.exitCode = 2;
|
|
154
168
|
return;
|
|
155
169
|
}
|
|
156
|
-
|
|
170
|
+
const shown = displayOverride(key, value)
|
|
171
|
+
?? (typeof value === 'string' ? value : JSON.stringify(value));
|
|
172
|
+
process.stdout.write(`${shown}\n`);
|
|
157
173
|
}
|
|
158
174
|
export function runConfigList(opts = {}) {
|
|
159
175
|
const masked = maskConfig(loadPersistedConfig());
|
|
176
|
+
// `--json` stays RAW on purpose: it mirrors the file, and a consumer parsing
|
|
177
|
+
// it must see the `null` that is actually on disk.
|
|
160
178
|
if (opts.json) {
|
|
161
179
|
process.stdout.write(`${JSON.stringify(masked, null, 2)}\n`);
|
|
162
180
|
return;
|
|
@@ -168,7 +186,7 @@ export function runConfigList(opts = {}) {
|
|
|
168
186
|
}
|
|
169
187
|
continue;
|
|
170
188
|
}
|
|
171
|
-
process.stdout.write(`${key} = ${JSON.stringify(value)}\n`);
|
|
189
|
+
process.stdout.write(`${key} = ${displayOverride(key, value) ?? JSON.stringify(value)}\n`);
|
|
172
190
|
}
|
|
173
191
|
}
|
|
174
192
|
// ---------------------------------------------------------------------------
|
package/dist/config/env.d.ts
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* - Provider secrets: ANTHROPIC_API_KEY, OPENAI_API_KEY, GOOGLE_API_KEY /
|
|
6
6
|
* GEMINI_API_KEY.
|
|
7
7
|
* - Overrides: ARAGON_PROVIDER, ARAGON_MODEL, ARAGON_BASE_URL, ARAGON_THINKING,
|
|
8
|
-
* ARAGON_MAX_TOKENS
|
|
8
|
+
* ARAGON_MAX_TOKENS (a number, or `auto` / `0` for the per-model ceiling),
|
|
9
|
+
* ARAGON_THEME, ARAGON_MOUSE, ARAGON_PLAN, ARAGON_TEAM,
|
|
9
10
|
* ARAGON_TEAM_MAX, ARAGON_LOG_LEVEL, ARAGON_LOG_FILE, ARAGON_LOG_DIR.
|
|
10
11
|
*
|
|
11
12
|
* NOT here: `ARAGON_HOME`. It is read when `config/app-paths.ts` is first
|
package/dist/config/env.js
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* - Provider secrets: ANTHROPIC_API_KEY, OPENAI_API_KEY, GOOGLE_API_KEY /
|
|
6
6
|
* GEMINI_API_KEY.
|
|
7
7
|
* - Overrides: ARAGON_PROVIDER, ARAGON_MODEL, ARAGON_BASE_URL, ARAGON_THINKING,
|
|
8
|
-
* ARAGON_MAX_TOKENS
|
|
8
|
+
* ARAGON_MAX_TOKENS (a number, or `auto` / `0` for the per-model ceiling),
|
|
9
|
+
* ARAGON_THEME, ARAGON_MOUSE, ARAGON_PLAN, ARAGON_TEAM,
|
|
9
10
|
* ARAGON_TEAM_MAX, ARAGON_LOG_LEVEL, ARAGON_LOG_FILE, ARAGON_LOG_DIR.
|
|
10
11
|
*
|
|
11
12
|
* NOT here: `ARAGON_HOME`. It is read when `config/app-paths.ts` is first
|
|
@@ -18,7 +19,7 @@ import process from 'node:process';
|
|
|
18
19
|
import dotenv from 'dotenv';
|
|
19
20
|
import { registerSecretsFrom } from '../logging/secret-registry.js';
|
|
20
21
|
import { clampLogLevel } from '../logging/levels.js';
|
|
21
|
-
import { clampTheme, clampThinkingLevel,
|
|
22
|
+
import { clampMaxTokens, clampTheme, clampThinkingLevel, isAutoToken, } from './schema.js';
|
|
22
23
|
/**
|
|
23
24
|
* Load a project `.env` from the given directory into `process.env` (existing
|
|
24
25
|
* process env wins — dotenv never overrides already-set variables).
|
|
@@ -58,9 +59,20 @@ export function readEnvConfig() {
|
|
|
58
59
|
if (process.env.ARAGON_THINKING) {
|
|
59
60
|
partial.thinkingLevel = clampThinkingLevel(process.env.ARAGON_THINKING.trim(), 'off');
|
|
60
61
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
// `auto` / `0` mean AUTO (`null`); a number is clamped to the accepted range.
|
|
63
|
+
// Anything else is LEFT ABSENT rather than written as `null`: a typo here used
|
|
64
|
+
// to become AUTO and silently discard the number in the config file under it,
|
|
65
|
+
// which is the opposite of what the user asked for.
|
|
66
|
+
const maxTokens = process.env.ARAGON_MAX_TOKENS?.trim();
|
|
67
|
+
if (maxTokens !== undefined && maxTokens.length > 0) {
|
|
68
|
+
if (isAutoToken(maxTokens)) {
|
|
69
|
+
partial.maxTokens = null;
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
const clamped = clampMaxTokens(maxTokens);
|
|
73
|
+
if (clamped !== undefined)
|
|
74
|
+
partial.maxTokens = clamped;
|
|
75
|
+
}
|
|
64
76
|
}
|
|
65
77
|
if (process.env.ARAGON_THEME) {
|
|
66
78
|
partial.theme = clampTheme(process.env.ARAGON_THEME.trim(), 'auto');
|
|
@@ -122,6 +134,49 @@ export function readEnvConfig() {
|
|
|
122
134
|
if (Object.keys(section).length > 0)
|
|
123
135
|
partial.team = section;
|
|
124
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
|
+
}
|
|
125
180
|
const log = readEnvLogConfig();
|
|
126
181
|
if (log)
|
|
127
182
|
partial.log = log;
|