@aragon-agent/cli 0.5.2 → 0.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/CHANGELOG.md +90 -0
  2. package/README.md +128 -1
  3. package/dist/agent/controller.d.ts +51 -1
  4. package/dist/agent/controller.js +177 -2
  5. package/dist/agent/headless.d.ts +12 -0
  6. package/dist/agent/headless.js +41 -1
  7. package/dist/agent/reducer.d.ts +48 -0
  8. package/dist/agent/reducer.js +91 -2
  9. package/dist/agent/system-prompt.d.ts +17 -0
  10. package/dist/agent/system-prompt.js +4 -0
  11. package/dist/cli.js +66 -2
  12. package/dist/commands/builtins.js +82 -0
  13. package/dist/config/app-paths.d.ts +17 -0
  14. package/dist/config/app-paths.js +21 -0
  15. package/dist/config/cli-commands.d.ts +26 -2
  16. package/dist/config/cli-commands.js +63 -4
  17. package/dist/config/env.d.ts +2 -2
  18. package/dist/config/env.js +28 -2
  19. package/dist/config/history-commands.d.ts +20 -0
  20. package/dist/config/history-commands.js +71 -0
  21. package/dist/config/load.d.ts +13 -0
  22. package/dist/config/load.js +65 -5
  23. package/dist/config/migrate-state-out-of-config.d.ts +32 -0
  24. package/dist/config/migrate-state-out-of-config.js +85 -0
  25. package/dist/config/prompt-history.d.ts +86 -0
  26. package/dist/config/prompt-history.js +332 -0
  27. package/dist/config/schema.d.ts +112 -16
  28. package/dist/config/schema.js +89 -6
  29. package/dist/config/store.js +13 -4
  30. package/dist/config/ui-state.d.ts +52 -0
  31. package/dist/config/ui-state.js +149 -0
  32. package/dist/logging/install.d.ts +85 -0
  33. package/dist/logging/install.js +94 -0
  34. package/dist/logging/logger.d.ts +1 -1
  35. package/dist/session/persist.d.ts +16 -0
  36. package/dist/session/persist.js +19 -1
  37. package/dist/team/activity.d.ts +67 -0
  38. package/dist/team/activity.js +130 -0
  39. package/dist/team/bus.d.ts +190 -0
  40. package/dist/team/bus.js +366 -0
  41. package/dist/team/comm-tools.d.ts +61 -0
  42. package/dist/team/comm-tools.js +221 -0
  43. package/dist/team/human-queue.d.ts +55 -0
  44. package/dist/team/human-queue.js +72 -0
  45. package/dist/team/limits.d.ts +123 -0
  46. package/dist/team/limits.js +128 -0
  47. package/dist/team/normalize.d.ts +39 -0
  48. package/dist/team/normalize.js +106 -0
  49. package/dist/team/panel-rows.d.ts +48 -0
  50. package/dist/team/panel-rows.js +75 -0
  51. package/dist/team/prompt.d.ts +49 -0
  52. package/dist/team/prompt.js +87 -0
  53. package/dist/team/report.d.ts +58 -0
  54. package/dist/team/report.js +205 -0
  55. package/dist/team/retry.d.ts +48 -0
  56. package/dist/team/retry.js +56 -0
  57. package/dist/team/runtime.d.ts +122 -0
  58. package/dist/team/runtime.js +406 -0
  59. package/dist/team/subagent.d.ts +90 -0
  60. package/dist/team/subagent.js +294 -0
  61. package/dist/team/task-tool.d.ts +47 -0
  62. package/dist/team/task-tool.js +118 -0
  63. package/dist/team/types.d.ts +172 -0
  64. package/dist/team/types.js +13 -0
  65. package/dist/tools/index.d.ts +48 -2
  66. package/dist/tools/index.js +26 -1
  67. package/dist/ui/App.js +97 -13
  68. package/dist/ui/StatusBar.d.ts +21 -0
  69. package/dist/ui/StatusBar.js +5 -2
  70. package/dist/ui/TeamPanel.d.ts +57 -0
  71. package/dist/ui/TeamPanel.js +134 -0
  72. package/dist/ui/Transcript.js +10 -0
  73. package/dist/ui/entries/TeamCard.d.ts +31 -0
  74. package/dist/ui/entries/TeamCard.js +39 -0
  75. package/dist/ui/glyphs.d.ts +10 -0
  76. package/dist/ui/glyphs.js +6 -0
  77. package/dist/ui/layout/AppShell.d.ts +11 -1
  78. package/dist/ui/layout/AppShell.js +3 -3
  79. package/dist/ui/overlays/HelpOverlay.js +12 -0
  80. package/dist/ui/transcript-text.js +20 -0
  81. package/dist/ui/use-startup-notices.d.ts +1 -1
  82. package/dist/ui/use-startup-notices.js +2 -1
  83. package/package.json +2 -2
@@ -4,7 +4,7 @@
4
4
  * derived from the event stream (R2), not from the resolved promise:
5
5
  * 0 = success, 1 = agent error (incl. a silently-swallowed throw), 2 = config.
6
6
  */
7
- import { formatCost, formatTokens, computeCost } from './usage.js';
7
+ import { formatCost, formatTokens, formatDuration, computeCost } from './usage.js';
8
8
  import { formatStreamError } from './reducer.js';
9
9
  /** Run a single prompt headlessly and resolve with the process exit code. */
10
10
  export async function runHeadless(controller, prompt, options = {}) {
@@ -57,11 +57,51 @@ export async function runHeadless(controller, prompt, options = {}) {
57
57
  break;
58
58
  }
59
59
  });
60
+ // Team mode is ON in headless: a fan-out needs no human (§3.12). What the
61
+ // children's spend must NOT do is vanish from the footer — the `[usage]` line
62
+ // is the only cost readout `-p` has, and a dispatch is where most of the
63
+ // tokens go.
64
+ const started = new Set();
65
+ const finished = new Set();
66
+ const unsubscribeTeam = controller.subscribeTeam?.((event) => {
67
+ if (event.type === 'usage') {
68
+ total.inputTokens += event.usage.inputTokens;
69
+ total.outputTokens += event.usage.outputTokens;
70
+ return;
71
+ }
72
+ if (quiet)
73
+ return;
74
+ if (event.type === 'dispatch_start') {
75
+ err.write(`\n[team] dispatch ${event.specs.length} subagents\n`);
76
+ return;
77
+ }
78
+ if (event.type === 'agent_update') {
79
+ const { run } = event;
80
+ if (run.phase !== 'queued' && !started.has(run.label)) {
81
+ started.add(run.label);
82
+ err.write(`[team] ${run.label} start "${run.description}"\n`);
83
+ }
84
+ const terminal = run.phase === 'done' || run.phase === 'failed' || run.phase === 'aborted';
85
+ if (terminal && !finished.has(run.label)) {
86
+ finished.add(run.label);
87
+ const ms = Math.max(0, (run.endedAt ?? 0) - (run.startedAt ?? 0));
88
+ const verdict = run.phase === 'done' && !run.error ? 'ok' : run.phase;
89
+ err.write(`[team] ${run.label} ${verdict} ${formatDuration(ms)} ${run.toolCalls} tools\n`);
90
+ }
91
+ return;
92
+ }
93
+ if (event.type === 'dispatch_end') {
94
+ const ok = event.outcome.runs.filter((r) => r.phase === 'done' && !r.error).length;
95
+ err.write(`[team] done ${ok}/${event.outcome.runs.length} ok` +
96
+ `${event.outcome.aborted ? ' (aborted)' : ''}\n`);
97
+ }
98
+ });
60
99
  try {
61
100
  await controller.prompt(prompt);
62
101
  }
63
102
  finally {
64
103
  unsubscribe();
104
+ unsubscribeTeam?.();
65
105
  }
66
106
  // Ensure a trailing newline so piped output is well-formed.
67
107
  if (wroteText)
@@ -12,6 +12,7 @@
12
12
  */
13
13
  import type { AgentEvent, ModelCost, ToolResult, TokenUsage } from '@aragon-agent/core';
14
14
  import type { AgentMode } from './agent-mode.js';
15
+ import type { DispatchOutcome, SubagentRun, SubagentSpec, TeamSnapshot } from '../team/types.js';
15
16
  export type NoticeLevel = 'info' | 'warn' | 'error';
16
17
  export type ToastLevel = NoticeLevel | 'success';
17
18
  export type Overlay = null | 'settings' | 'model' | 'help' | 'confirm' | 'question' | 'plan';
@@ -55,6 +56,28 @@ export type Entry = {
55
56
  kind: 'notice';
56
57
  level: NoticeLevel;
57
58
  text: string;
59
+ }
60
+ /**
61
+ * A team dispatch (team-subagents §5.3). JSON-SERIALIZABLE by construction, so
62
+ * `/save` and `/resume` need no format change.
63
+ *
64
+ * `active: true` is normalized away ON LOAD, in `session/persist.ts`, not here
65
+ * (P1-5). A session saved mid-dispatch would otherwise resume with a card that
66
+ * claims to be running while nothing is: it spins forever, and because
67
+ * `Transcript`'s settled boundary is MONOTONIC an entry that never settles
68
+ * never reaches `<Static>` and is re-rendered on every frame for the rest of
69
+ * the session. The reducer never legitimately sees a stale-active entry — only
70
+ * the file does — which is why the fix belongs in the load path.
71
+ */
72
+ | {
73
+ id: string;
74
+ kind: 'team';
75
+ dispatchId: string;
76
+ requested: number;
77
+ runs: SubagentRun[];
78
+ aborted: boolean;
79
+ durationMs?: number;
80
+ active: boolean;
58
81
  };
59
82
  export interface UsageTotal {
60
83
  inputTokens: number;
@@ -81,7 +104,17 @@ export interface ViewState {
81
104
  agentMode: AgentMode;
82
105
  /** Set only by a DEFERRED `plan -> build`; drives the `PLAN -> BUILD` readout. */
83
106
  pendingAgentMode: AgentMode | null;
107
+ /**
108
+ * The live team roster, or `null` when no dispatch is running.
109
+ *
110
+ * NEVER PERSISTED. `/save` writes `entries`, and the settled `team` entry is
111
+ * the history; this field is the ephemeral panel, and a resumed session has no
112
+ * live dispatch to show (§6.4).
113
+ */
114
+ team: TeamSnapshot | null;
84
115
  seq: number;
116
+ /** The `kind: 'team'` entry the live dispatch is writing into. */
117
+ teamEntryId?: string;
85
118
  streamingId?: string;
86
119
  turnProduced: boolean;
87
120
  errorNoticed: boolean;
@@ -165,6 +198,21 @@ export type ViewAction = {
165
198
  type: 'setAgentMode';
166
199
  mode: AgentMode;
167
200
  pending: AgentMode | null;
201
+ } | {
202
+ type: 'teamStart';
203
+ dispatchId: string;
204
+ requested: number;
205
+ specs: SubagentSpec[];
206
+ } | {
207
+ type: 'teamUpdate';
208
+ snapshot: TeamSnapshot;
209
+ } | {
210
+ type: 'teamUsage';
211
+ usage: TokenUsage;
212
+ costDelta: number;
213
+ } | {
214
+ type: 'teamEnd';
215
+ outcome: DispatchOutcome;
168
216
  };
169
217
  export declare const STORED_PREVIEW_CHARS = 8000;
170
218
  export declare const STORED_PREVIEW_LINES = 200;
@@ -27,7 +27,9 @@ export function initialViewState() {
27
27
  // `controller.getAgentMode()` so `--plan` renders on the first frame.
28
28
  agentMode: 'build',
29
29
  pendingAgentMode: null,
30
+ team: null,
30
31
  seq: 0,
32
+ teamEntryId: undefined,
31
33
  streamingId: undefined,
32
34
  turnProduced: false,
33
35
  errorNoticed: false,
@@ -367,8 +369,17 @@ export function viewReducer(state, action) {
367
369
  entries: mapEntry(state.entries, state.streamingId, (e) => e.kind === 'assistant' ? { ...e, streaming: false, aborted: true } : e),
368
370
  };
369
371
  }
372
+ // `teamEntryId` is dropped with the entries it pointed at. Keeping it would
373
+ // leave `teamUpdate` mapping an id that no longer exists — harmless today,
374
+ // and a dangling reference the next reader has to reason about.
370
375
  case 'clearTranscript':
371
- return { ...state, entries: [], streamingId: undefined, expandedToolIds: {} };
376
+ return {
377
+ ...state,
378
+ entries: [],
379
+ streamingId: undefined,
380
+ expandedToolIds: {},
381
+ teamEntryId: undefined,
382
+ };
372
383
  case 'resetConversation':
373
384
  return {
374
385
  ...state,
@@ -377,6 +388,7 @@ export function viewReducer(state, action) {
377
388
  expandedToolIds: {},
378
389
  contextTokens: 0,
379
390
  usageTotal: { inputTokens: 0, outputTokens: 0, costUsd: 0 },
391
+ teamEntryId: undefined,
380
392
  };
381
393
  case 'setOverlay':
382
394
  return { ...state, overlay: action.overlay };
@@ -407,13 +419,90 @@ export function viewReducer(state, action) {
407
419
  next[action.id] = true;
408
420
  return { ...state, expandedToolIds: next };
409
421
  }
422
+ // --- Team mode (team-subagents §5.3) ---------------------------------
423
+ case 'teamStart': {
424
+ const { id, seq } = nextId(state);
425
+ const runs = action.specs.map((spec) => ({
426
+ label: spec.label,
427
+ description: spec.description,
428
+ phase: 'queued',
429
+ turns: 0,
430
+ toolCalls: 0,
431
+ usage: { inputTokens: 0, outputTokens: 0 },
432
+ filesTouched: [],
433
+ messagesSent: 0,
434
+ }));
435
+ const entry = {
436
+ id,
437
+ kind: 'team',
438
+ dispatchId: action.dispatchId,
439
+ requested: action.requested,
440
+ runs,
441
+ aborted: false,
442
+ active: true,
443
+ };
444
+ return {
445
+ ...state,
446
+ seq,
447
+ entries: [...state.entries, entry],
448
+ teamEntryId: id,
449
+ team: {
450
+ dispatchId: action.dispatchId,
451
+ active: true,
452
+ runs,
453
+ requested: action.requested,
454
+ startedAt: Date.now(),
455
+ messageCount: 0,
456
+ },
457
+ };
458
+ }
459
+ case 'teamUpdate': {
460
+ // One `mapEntry` on the KNOWN id, never a scan: this action arrives at up
461
+ // to `maxConcurrent` x 8/s and a reverse search over a long transcript on
462
+ // every one of them is exactly the cost the coalescer exists to avoid.
463
+ const entries = state.teamEntryId
464
+ ? mapEntry(state.entries, state.teamEntryId, (e) => e.kind === 'team' ? { ...e, runs: action.snapshot.runs } : e)
465
+ : state.entries;
466
+ return { ...state, entries, team: action.snapshot };
467
+ }
468
+ case 'teamUsage':
469
+ // `usageTotal` AND NOTHING ELSE (§3.9 / AC-15). In particular NOT
470
+ // `contextTokens`: that gauge shows the LEAD's context occupancy against
471
+ // the model's window, and folding five children into it would read 180%
472
+ // on a perfectly healthy session. Child spend is real money and has to
473
+ // appear in the cost readout; it is not the lead's context.
474
+ return {
475
+ ...state,
476
+ usageTotal: {
477
+ inputTokens: state.usageTotal.inputTokens + action.usage.inputTokens,
478
+ outputTokens: state.usageTotal.outputTokens + action.usage.outputTokens,
479
+ costUsd: state.usageTotal.costUsd + action.costDelta,
480
+ },
481
+ };
482
+ case 'teamEnd': {
483
+ const { outcome } = action;
484
+ const entries = state.teamEntryId
485
+ ? mapEntry(state.entries, state.teamEntryId, (e) => e.kind === 'team'
486
+ ? {
487
+ ...e,
488
+ runs: outcome.runs,
489
+ aborted: outcome.aborted,
490
+ durationMs: Math.max(0, outcome.endedAt - outcome.startedAt),
491
+ active: false,
492
+ }
493
+ : e)
494
+ : state.entries;
495
+ // `team: null` is what makes the panel disappear — the requirement's
496
+ // "(dang you de shi hou)": it renders only while a dispatch is running.
497
+ return { ...state, entries, team: null, teamEntryId: undefined };
498
+ }
410
499
  case 'restoreEntries': {
411
500
  // Adopt a seq beyond any restored id to keep future ids unique.
412
501
  const maxSeq = action.entries.reduce((acc, e) => {
413
502
  const n = Number.parseInt(e.id.replace(/^e/, ''), 10);
414
503
  return Number.isFinite(n) ? Math.max(acc, n) : acc;
415
504
  }, state.seq);
416
- return { ...state, entries: [...action.entries], seq: maxSeq };
505
+ return { ...state, entries: [...action.entries], seq: maxSeq, teamEntryId: undefined };
417
506
  }
418
507
  default:
419
508
  return state;
@@ -36,6 +36,23 @@ export interface SystemPromptParams {
36
36
  planInteractive?: boolean;
37
37
  /** `planModeMaxAskRounds`, substituted into the block so the two agree. */
38
38
  planMaxAskRounds?: number;
39
+ /**
40
+ * The rendered `<team_mode>` block for a LEAD with team mode on, or `''`.
41
+ *
42
+ * Spliced conditionally exactly as `skillsBlock` and the plan block are, which
43
+ * is what keeps invariant I-8 true: `team.enabled: false` (and `--no-team`)
44
+ * produce the pre-team prompt BYTE FOR BYTE.
45
+ */
46
+ teamBlock?: string;
47
+ /**
48
+ * The rendered `<subagent_role>` block for a CHILD, or `''`.
49
+ *
50
+ * Never set together with `teamBlock`: a child has no `task` tool (D-3), and a
51
+ * lead is not a subagent. Two fields rather than one because the two blocks
52
+ * carry opposite instructions and a single "team text" parameter would make
53
+ * mixing them a one-character mistake.
54
+ */
55
+ subagentBlock?: string;
39
56
  }
40
57
  /** Describe the active shell so the model does not emit incompatible syntax. */
41
58
  export declare function describeShell(): {
@@ -37,6 +37,8 @@ export function buildSystemPrompt(params) {
37
37
  maxAskRounds: params.planMaxAskRounds ?? 4,
38
38
  })
39
39
  : '';
40
+ const teamBlock = params.teamBlock ?? '';
41
+ const subagentBlock = params.subagentBlock ?? '';
40
42
  return [
41
43
  'You are AragonAgent, an autonomous coding assistant operating inside a terminal (TUI).',
42
44
  'You help the user accomplish software-engineering tasks by reasoning step by step and using the provided tools.',
@@ -50,6 +52,8 @@ export function buildSystemPrompt(params) {
50
52
  toolList,
51
53
  ...(hasSkills ? ['', skillsBlock] : []),
52
54
  ...(planBlock ? ['', planBlock] : []),
55
+ ...(teamBlock ? ['', teamBlock] : []),
56
+ ...(subagentBlock ? ['', subagentBlock] : []),
53
57
  '',
54
58
  'Operating guidance:',
55
59
  '- Prefer reading files and inspecting the workspace before making changes.',
package/dist/cli.js CHANGED
@@ -21,8 +21,10 @@ import { loadConfig } from './config/load.js';
21
21
  import { getConfigPath, readConfigFile, updatePersistedConfig } from './config/store.js';
22
22
  import { formatMigrationNotice, migrateLegacyState } from './config/migrate-legacy-state.js';
23
23
  import { formatHomeMigrationNotice, migrateToHome } from './config/migrate-home.js';
24
- import { LOG_CONFIG_SET_KEYS, applyLogConfigSet, runConfigEdit, runConfigGet, runConfigHome, runConfigList, warnIfRedactionDisabled, } from './config/cli-commands.js';
25
- import { attachAgentEvents, installLogging, setScreenRestore, setSignalTerminator, } from './logging/install.js';
24
+ import { migrateStateOutOfConfig } from './config/migrate-state-out-of-config.js';
25
+ import { runHistoryCommand } from './config/history-commands.js';
26
+ import { LOG_CONFIG_SET_KEYS, TEAM_CONFIG_SET_KEYS, applyLogConfigSet, applyTeamConfigSet, runConfigEdit, runConfigGet, runConfigHome, runConfigList, warnIfRedactionDisabled, } from './config/cli-commands.js';
27
+ import { attachAgentEvents, attachTeamEvents, installLogging, setScreenRestore, setSignalTerminator, } from './logging/install.js';
26
28
  import { getLogger } from './logging/logger.js';
27
29
  import { runLogsCommand } from './logging/cli-commands.js';
28
30
  import { ADAPTER_PROVIDERS, clampDensity, clampTheme, clampThinkingLevel, clampAskRounds, clampHumanTimeout, clampSkillsIntegrity, clampSkillsToolPolicy, clampTranscriptWindow, coerceMaxTokens, coercePositiveInt, isAdapterProvider, DEFAULT_PLAN_HUMAN_TIMEOUT_MS, DEFAULT_PLAN_MAX_ASK_ROUNDS, DEFAULT_SKILLS_CONFIG, } from './config/schema.js';
@@ -79,6 +81,11 @@ function toFlags(opts) {
79
81
  skillsYes: opts.skillsYes,
80
82
  skillToolPolicy: opts.skillToolPolicy,
81
83
  plan: opts.plan,
84
+ // These two lines are the same failure the `mouse` note above records, one
85
+ // feature later (P1-4): without them `--no-team` and `--team-max` never
86
+ // reach `CliFlags` and are inert, with no compile error and no runtime error.
87
+ team: opts.team,
88
+ teamMax: opts.teamMax,
82
89
  // `--verbose` is just a shorthand for `--log-level debug`; an explicit
83
90
  // `--log-level` still wins, so the two can be combined without surprise.
84
91
  logLevel: opts.logLevel ?? (opts.verbose ? 'debug' : undefined),
@@ -166,6 +173,11 @@ function runInteractive(flags, extras = {}) {
166
173
  const logger = getLogger();
167
174
  logger.info('cli', 'run_interactive', { mode, provider: config.provider, model: config.model });
168
175
  attachAgentEvents(logger, controller);
176
+ // The team stream is CLI-local (D-10), so `attachAgentEvents` cannot see it.
177
+ // Without this a dispatch leaves NO trace in the log file at all — the wrong
178
+ // outcome for the one feature here that runs five agents the user cannot
179
+ // watch (§3.13 / P1-2 / R-19).
180
+ attachTeamEvents(logger, controller);
169
181
  let screen = null;
170
182
  let replayed = false;
171
183
  // --- Mouse wheel region routing (mouse-wheel-region-routing §4.2). -------
@@ -264,11 +276,16 @@ function runInteractive(flags, extras = {}) {
264
276
  .then(() => {
265
277
  screen?.restore();
266
278
  disposeMouse();
279
+ // Orphaned subagents survive `Ctrl+C` otherwise, and a live child holds
280
+ // the event loop open so `aragon` never returns to the shell (R-15). The
281
+ // App's unmount cleanup also calls this; `dispose()` is idempotent.
282
+ controller.dispose();
267
283
  replayTranscript();
268
284
  })
269
285
  .catch(() => {
270
286
  screen?.restore();
271
287
  disposeMouse();
288
+ controller.dispose();
272
289
  });
273
290
  }
274
291
  async function runOneShot(flags, prompt, quiet) {
@@ -282,6 +299,11 @@ async function runOneShot(flags, prompt, quiet) {
282
299
  // toast channel `App.tsx` uses does not exist here.
283
300
  logger.onFailure((reason) => process.stderr.write(`[log] ${reason}\n`));
284
301
  attachAgentEvents(logger, controller);
302
+ // The team stream is CLI-local (D-10), so `attachAgentEvents` cannot see it.
303
+ // Without this a dispatch leaves NO trace in the log file at all — the wrong
304
+ // outcome for the one feature here that runs five agents the user cannot
305
+ // watch (§3.13 / P1-2 / R-19).
306
+ attachTeamEvents(logger, controller);
285
307
  const untrusted = controller.getSkillService().untrustedDirs();
286
308
  for (const dir of untrusted) {
287
309
  process.stderr.write(`[skills] project skills in ${dir} skipped (untrusted). Run: aragon skills trust ${dir}\n`);
@@ -354,6 +376,7 @@ const CONFIG_SET_KEYS = new Set([
354
376
  // falls through every case, writes nothing, and still prints
355
377
  // `Set mouse = false` (P1-2) -- exactly what happened to `density`/`hints`.
356
378
  'mouse',
379
+ 'historyEnabled',
357
380
  'planModeDefault',
358
381
  'planModeMaxAskRounds',
359
382
  'planModeHumanTimeoutMs',
@@ -368,6 +391,11 @@ const CONFIG_SET_KEYS = new Set([
368
391
  // `log.*` keys are handled by `applyLogConfigSet` below, NOT by the switch:
369
392
  // that switch is already past this package's complexity ceiling.
370
393
  ...LOG_CONFIG_SET_KEYS,
394
+ // `team.*` keys are handled by `applyTeamConfigSet`, for the same reason
395
+ // `log.*` are: the switch below is already past this package's complexity
396
+ // ceiling, and a key listed here but forgotten there writes nothing while
397
+ // still printing `Set team.enabled = false`.
398
+ ...TEAM_CONFIG_SET_KEYS,
371
399
  ]);
372
400
  function runConfigSet(key, value) {
373
401
  if (!CONFIG_SET_KEYS.has(key)) {
@@ -382,6 +410,16 @@ function runConfigSet(key, value) {
382
410
  process.stdout.write(`Set ${key} = ${value}\n`);
383
411
  return;
384
412
  }
413
+ const teamPatch = applyTeamConfigSet(key, value);
414
+ if (teamPatch) {
415
+ // The written value is echoed from the CLAMPED result rather than from the
416
+ // input, so `config set team.maxSubagents 40` says 10 instead of silently
417
+ // reporting a number that was never stored (R-g).
418
+ const merged = updatePersistedConfig(teamPatch);
419
+ const field = key.slice('team.'.length);
420
+ process.stdout.write(`Set ${key} = ${String(merged.team[field])}\n`);
421
+ return;
422
+ }
385
423
  const patch = {};
386
424
  switch (key) {
387
425
  case 'provider':
@@ -429,6 +467,11 @@ function runConfigSet(key, value) {
429
467
  case 'mouse':
430
468
  patch.mouse = value === 'true' || value === '1';
431
469
  break;
470
+ // Turning this off stops new prompts being recorded; it does NOT delete
471
+ // what is already in `prompt-history.jsonl` (that is `aragon history clear`).
472
+ case 'historyEnabled':
473
+ patch.historyEnabled = value === 'true' || value === '1';
474
+ break;
432
475
  // Clamped rather than rejected, for the same reason as `theme` and
433
476
  // `skills.integrity`: hardening only the READ path leaves a bad value on
434
477
  // disk that reverts to the default on every launch, which presents as
@@ -525,6 +568,13 @@ function buildProgram() {
525
568
  // config layer win when neither flag was given.
526
569
  .option('--plan', 'Start the session in PLAN mode (read-only research + review)')
527
570
  .option('--no-plan', 'Start the session in BUILD mode (overrides planModeDefault)')
571
+ // BOTH FORMS, positive first, and the pair is mandatory (team-subagents
572
+ // §4.4 / P1-4). Declaring only `--no-team` would make commander default
573
+ // `opts.team` to `true`, at which point the flag is indistinguishable from
574
+ // its own default and silently overrides `config.json` on every run.
575
+ .option('--team', 'Enable team subagents for this session (the default)')
576
+ .option('--no-team', 'Disable team subagents (the task tool is not registered at all)')
577
+ .option('--team-max <n>', 'Max subagents per dispatch for this run (1-10)')
528
578
  .option('--no-skills', 'Disable the skill system entirely (no catalog, no skill tools)')
529
579
  .option('--skill <name>', 'Force-load a skill at Level 2 for this run (repeatable)', (value, previous = []) => [...previous, value])
530
580
  .option('--skills-yes', 'Approve skill installs for this run (CI / headless)')
@@ -618,6 +668,16 @@ function buildProgram() {
618
668
  .action(async (subcommand, opts) => {
619
669
  process.exitCode = await runLogsCommand(subcommand ?? 'path', opts, toFlags(program.opts()));
620
670
  });
671
+ // aragon history <sub> [-n N] [--json] [--yes]
672
+ program
673
+ .command('history [subcommand]')
674
+ .description('Inspect the prompt history: path | list | clear')
675
+ .option('-n, --lines <n>', 'Entries for `history list` (default 20)')
676
+ .option('--json', 'Print raw entries instead of rendered lines')
677
+ .option('--yes', 'Confirm destructive actions (required by `history clear`)')
678
+ .action((subcommand, opts) => {
679
+ process.exitCode = runHistoryCommand(subcommand ?? 'path', opts);
680
+ });
621
681
  // aragon skills <sub> [arg] [--scope] [--name] [--description] [--yes] [--json]
622
682
  program
623
683
  .command('skills [subcommand] [argument]')
@@ -666,6 +726,10 @@ async function main() {
666
726
  // may just have created. What ① and ② did is replayed from `pending` rather
667
727
  // than lost.
668
728
  installLogging({ pending: migrationNotes(legacy, home) });
729
+ // ③.5 — after ③ because it writes a log record, and before anything that
730
+ // resolves config because every `loadConfig()` from here on must already see
731
+ // the post-migration world. Never throws, never prints (D-8).
732
+ migrateStateOutOfConfig();
669
733
  // An unparseable config.json still falls back to defaults — the CLI has to
670
734
  // start — but it must not do so in silence, or a stray comma presents as
671
735
  // "every setting I ever chose is gone". stderr, never stdout, so
@@ -90,6 +90,88 @@ const COMMANDS = [
90
90
  ctx.applyAgentMode(target);
91
91
  },
92
92
  },
93
+ {
94
+ name: 'team',
95
+ description: 'Team subagents: status | on | off | max <n>',
96
+ /**
97
+ * `/team on` CANNOT turn team mode on in a session that started without it,
98
+ * and this command must not pretend otherwise (§4.5 / D-17 / P0-2).
99
+ *
100
+ * `controller.ts` builds the tool array ONCE and its own comment forbids
101
+ * rebuilding it: `Agent.setTools()` mutates the live `ToolRegistry`, and
102
+ * `submit_plan` flips the session mode from INSIDE a tool execution, so a
103
+ * rebuild at that moment would mutate the registry mid-iteration. But
104
+ * `rebuildSystemPrompt()` IS a live path — so the naive reading of `/team on`
105
+ * (flip a flag, re-splice `<team_mode>`) produces a session where the model
106
+ * is told it has a `task` tool that was never registered. Every call comes
107
+ * back "unknown tool", and the model has no way to discover why.
108
+ *
109
+ * So the answer splits by what was decided at construction, and the honest
110
+ * branch — persist it, say the session is unchanged — is the whole point.
111
+ */
112
+ run: (ctx) => {
113
+ const arg = ctx.args.trim().toLowerCase();
114
+ const [verb, value] = arg.split(/\s+/, 2);
115
+ const controller = ctx.controller;
116
+ // Refused mid-dispatch for the reason `/reload` already records: half a
117
+ // dispatch under one ceiling and half under another produces a result
118
+ // nothing can afterwards explain.
119
+ if (verb && verb !== 'status' && controller.isTeamBusy()) {
120
+ ctx.notify('warn', 'Cannot change team settings mid-dispatch.');
121
+ return;
122
+ }
123
+ if (!verb || verb === 'status') {
124
+ const cfg = controller.getTeamConfig();
125
+ const state = !controller.isTeamRegistered()
126
+ ? 'off for this session (started with --no-team)'
127
+ : controller.isTeamEnabled()
128
+ ? 'on'
129
+ : 'off';
130
+ const snapshot = controller.getTeamSnapshot();
131
+ const roster = snapshot
132
+ ? `\nRunning: ${snapshot.runs.map((r) => `${r.label} (${r.phase})`).join(', ')}`
133
+ : '';
134
+ ctx.notify('info', `Team mode: ${state}. Max subagents: ${cfg.maxSubagents}, ` +
135
+ `concurrent: ${cfg.maxConcurrent}.${roster}`);
136
+ return;
137
+ }
138
+ if (verb === 'on' || verb === 'off') {
139
+ const enabled = verb === 'on';
140
+ // A PARTIAL `team` section, safe only because `updatePersistedConfig`
141
+ // deep-merges it — the same contract `/skills disable` relies on.
142
+ ctx.persistConfig({ team: { enabled } });
143
+ if (!controller.isTeamRegistered()) {
144
+ // The bottom-right cell of §4.5's table. It is honest, it is one
145
+ // sentence, and it keeps AC-11 intact: a `--no-team` session never
146
+ // grows a wrapper or an entry, whatever the user types afterwards.
147
+ ctx.notify('info', enabled
148
+ ? 'Team mode is off for this session (started with --no-team). Saved for next launch.'
149
+ : 'Team mode is already off for this session. Saved for next launch.');
150
+ return;
151
+ }
152
+ controller.setTeamEnabled(enabled);
153
+ ctx.toast('success', `Team mode ${enabled ? 'on' : 'off'}.`);
154
+ return;
155
+ }
156
+ if (verb === 'max') {
157
+ const n = Number.parseInt(value ?? '', 10);
158
+ if (!Number.isFinite(n) || n <= 0) {
159
+ ctx.notify('warn', 'Usage: /team max <n>');
160
+ return;
161
+ }
162
+ // Clamped by `setTeamConfig` and again on the persist path, so a value
163
+ // above the requirement's ceiling can reach neither the runtime nor the
164
+ // file (R-g).
165
+ const applied = controller.setTeamConfig({ maxSubagents: n });
166
+ ctx.persistConfig({
167
+ team: { maxSubagents: applied.maxSubagents },
168
+ });
169
+ ctx.toast('success', `Max subagents set to ${applied.maxSubagents}.`);
170
+ return;
171
+ }
172
+ ctx.notify('warn', `Unknown argument "${verb}" - use /team [status|on|off|max <n>].`);
173
+ },
174
+ },
93
175
  {
94
176
  name: 'expand',
95
177
  description: 'Expand / collapse a recent tool card (default: last)',
@@ -12,11 +12,18 @@
12
12
  * ~/.aragon-agent/
13
13
  * ├── config.json persisted config (0600 on POSIX)
14
14
  * ├── config.json.bak `aragon config edit` pre-edit backup
15
+ * ├── prompt-history.jsonl submitted prompts, append-only (0600 on POSIX)
16
+ * ├── state.json UI bookkeeping scalars (0600 on POSIX)
15
17
  * ├── logs/ JSONL logs (0700 on POSIX)
16
18
  * ├── sessions/ /save · /resume
17
19
  * ├── skills/ `aragon skills install` target
18
20
  * └── skill-usage.json
19
21
  *
22
+ * The two files added by config-state-separation are DIRECT CHILDREN of the
23
+ * root, siblings of `skill-usage.json` rather than children of any directory
24
+ * here: `logs/`, `sessions/` and `skills/` are all scan roots, and every extra
25
+ * file inside one is another thing each walk has to recognise and skip.
26
+ *
20
27
  * The same path on all three platforms, deliberately. Claude Code, git and npm
21
28
  * all put their dot-directory in the user's home; one path means documentation,
22
29
  * support scripts and community answers have exactly one thing to say. A user
@@ -62,6 +69,16 @@ export declare function getHomeRoot(): string;
62
69
  export declare function getConfigPath(): string;
63
70
  /** Written by `aragon config edit` before spawning the editor. */
64
71
  export declare function getConfigBackupPath(): string;
72
+ /**
73
+ * `<home>/prompt-history.jsonl` — the prompts the user submitted, one JSON
74
+ * object per line. Deliberately NOT under `logs/`: that directory is governed
75
+ * by `log.toFile`, `log.maxFiles` and `aragon logs clear`, and turning
76
+ * diagnostics off or reclaiming disk must not also delete the user's `↑`
77
+ * recall (config-state-separation D-1).
78
+ */
79
+ export declare function getPromptHistoryPath(): string;
80
+ /** `<home>/state.json` — UI bookkeeping scalars only; see `ui-state.ts`. */
81
+ export declare function getUiStatePath(): string;
65
82
  export declare function getLogsDir(): string;
66
83
  export declare function getSessionsDir(): string;
67
84
  /** `<home>` — parent of `skills/`, and where sidecar state such as usage lives. */
@@ -12,11 +12,18 @@
12
12
  * ~/.aragon-agent/
13
13
  * ├── config.json persisted config (0600 on POSIX)
14
14
  * ├── config.json.bak `aragon config edit` pre-edit backup
15
+ * ├── prompt-history.jsonl submitted prompts, append-only (0600 on POSIX)
16
+ * ├── state.json UI bookkeeping scalars (0600 on POSIX)
15
17
  * ├── logs/ JSONL logs (0700 on POSIX)
16
18
  * ├── sessions/ /save · /resume
17
19
  * ├── skills/ `aragon skills install` target
18
20
  * └── skill-usage.json
19
21
  *
22
+ * The two files added by config-state-separation are DIRECT CHILDREN of the
23
+ * root, siblings of `skill-usage.json` rather than children of any directory
24
+ * here: `logs/`, `sessions/` and `skills/` are all scan roots, and every extra
25
+ * file inside one is another thing each walk has to recognise and skip.
26
+ *
20
27
  * The same path on all three platforms, deliberately. Claude Code, git and npm
21
28
  * all put their dot-directory in the user's home; one path means documentation,
22
29
  * support scripts and community answers have exactly one thing to say. A user
@@ -119,6 +126,20 @@ export function getConfigPath() {
119
126
  export function getConfigBackupPath() {
120
127
  return join(resolution.root, 'config.json.bak');
121
128
  }
129
+ /**
130
+ * `<home>/prompt-history.jsonl` — the prompts the user submitted, one JSON
131
+ * object per line. Deliberately NOT under `logs/`: that directory is governed
132
+ * by `log.toFile`, `log.maxFiles` and `aragon logs clear`, and turning
133
+ * diagnostics off or reclaiming disk must not also delete the user's `↑`
134
+ * recall (config-state-separation D-1).
135
+ */
136
+ export function getPromptHistoryPath() {
137
+ return join(resolution.root, 'prompt-history.jsonl');
138
+ }
139
+ /** `<home>/state.json` — UI bookkeeping scalars only; see `ui-state.ts`. */
140
+ export function getUiStatePath() {
141
+ return join(resolution.root, 'state.json');
142
+ }
122
143
  export function getLogsDir() {
123
144
  return join(resolution.root, 'logs');
124
145
  }