@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.
- package/CHANGELOG.md +90 -0
- package/README.md +128 -1
- package/dist/agent/controller.d.ts +51 -1
- package/dist/agent/controller.js +177 -2
- package/dist/agent/headless.d.ts +12 -0
- package/dist/agent/headless.js +41 -1
- package/dist/agent/reducer.d.ts +48 -0
- package/dist/agent/reducer.js +91 -2
- package/dist/agent/system-prompt.d.ts +17 -0
- package/dist/agent/system-prompt.js +4 -0
- package/dist/cli.js +66 -2
- package/dist/commands/builtins.js +82 -0
- package/dist/config/app-paths.d.ts +17 -0
- package/dist/config/app-paths.js +21 -0
- package/dist/config/cli-commands.d.ts +26 -2
- package/dist/config/cli-commands.js +63 -4
- package/dist/config/env.d.ts +2 -2
- package/dist/config/env.js +28 -2
- package/dist/config/history-commands.d.ts +20 -0
- package/dist/config/history-commands.js +71 -0
- package/dist/config/load.d.ts +13 -0
- package/dist/config/load.js +65 -5
- package/dist/config/migrate-state-out-of-config.d.ts +32 -0
- package/dist/config/migrate-state-out-of-config.js +85 -0
- package/dist/config/prompt-history.d.ts +86 -0
- package/dist/config/prompt-history.js +332 -0
- package/dist/config/schema.d.ts +112 -16
- package/dist/config/schema.js +89 -6
- package/dist/config/store.js +13 -4
- package/dist/config/ui-state.d.ts +52 -0
- package/dist/config/ui-state.js +149 -0
- package/dist/logging/install.d.ts +85 -0
- package/dist/logging/install.js +94 -0
- package/dist/logging/logger.d.ts +1 -1
- package/dist/session/persist.d.ts +16 -0
- package/dist/session/persist.js +19 -1
- package/dist/team/activity.d.ts +67 -0
- package/dist/team/activity.js +130 -0
- package/dist/team/bus.d.ts +190 -0
- package/dist/team/bus.js +366 -0
- package/dist/team/comm-tools.d.ts +61 -0
- package/dist/team/comm-tools.js +221 -0
- package/dist/team/human-queue.d.ts +55 -0
- package/dist/team/human-queue.js +72 -0
- package/dist/team/limits.d.ts +123 -0
- package/dist/team/limits.js +128 -0
- package/dist/team/normalize.d.ts +39 -0
- package/dist/team/normalize.js +106 -0
- package/dist/team/panel-rows.d.ts +48 -0
- package/dist/team/panel-rows.js +75 -0
- package/dist/team/prompt.d.ts +49 -0
- package/dist/team/prompt.js +87 -0
- package/dist/team/report.d.ts +58 -0
- package/dist/team/report.js +205 -0
- package/dist/team/retry.d.ts +48 -0
- package/dist/team/retry.js +56 -0
- package/dist/team/runtime.d.ts +122 -0
- package/dist/team/runtime.js +406 -0
- package/dist/team/subagent.d.ts +90 -0
- package/dist/team/subagent.js +294 -0
- package/dist/team/task-tool.d.ts +47 -0
- package/dist/team/task-tool.js +118 -0
- package/dist/team/types.d.ts +172 -0
- package/dist/team/types.js +13 -0
- package/dist/tools/index.d.ts +48 -2
- package/dist/tools/index.js +26 -1
- package/dist/ui/App.js +97 -13
- package/dist/ui/StatusBar.d.ts +21 -0
- package/dist/ui/StatusBar.js +5 -2
- package/dist/ui/TeamPanel.d.ts +57 -0
- package/dist/ui/TeamPanel.js +134 -0
- package/dist/ui/Transcript.js +10 -0
- package/dist/ui/entries/TeamCard.d.ts +31 -0
- package/dist/ui/entries/TeamCard.js +39 -0
- package/dist/ui/glyphs.d.ts +10 -0
- package/dist/ui/glyphs.js +6 -0
- package/dist/ui/layout/AppShell.d.ts +11 -1
- package/dist/ui/layout/AppShell.js +3 -3
- package/dist/ui/overlays/HelpOverlay.js +12 -0
- package/dist/ui/transcript-text.js +20 -0
- package/dist/ui/use-startup-notices.d.ts +1 -1
- package/dist/ui/use-startup-notices.js +2 -1
- package/package.json +2 -2
package/dist/agent/headless.js
CHANGED
|
@@ -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)
|
package/dist/agent/reducer.d.ts
CHANGED
|
@@ -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;
|
package/dist/agent/reducer.js
CHANGED
|
@@ -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 {
|
|
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 {
|
|
25
|
-
import {
|
|
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. */
|
package/dist/config/app-paths.js
CHANGED
|
@@ -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
|
}
|