@aragon-agent/cli 0.6.45 → 0.6.47
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 +15 -0
- package/README.md +104 -6
- package/dist/agent/coalesce.d.ts +4 -0
- package/dist/agent/coalesce.js +13 -0
- package/dist/agent/controller.d.ts +21 -0
- package/dist/agent/controller.js +56 -3
- package/dist/agent/invocation-card.d.ts +77 -0
- package/dist/agent/invocation-card.js +169 -0
- package/dist/agent/prompt-options.d.ts +6 -0
- package/dist/agent/queued-messages.d.ts +3 -0
- package/dist/agent/queued-messages.js +4 -1
- package/dist/agent/reducer.d.ts +25 -0
- package/dist/agent/reducer.js +43 -5
- package/dist/agent/user-attention.d.ts +17 -0
- package/dist/agent/user-attention.js +32 -0
- package/dist/cli.js +2 -3
- package/dist/commands/builtins.js +12 -0
- package/dist/commands/governance-command.js +13 -1
- package/dist/commands/index-build.d.ts +8 -0
- package/dist/commands/index-build.js +11 -0
- package/dist/commands/registry.d.ts +23 -0
- package/dist/commands/registry.js +20 -7
- package/dist/commands/skills.js +36 -2
- package/dist/commands/subagent-command.js +3 -1
- package/dist/commands/team-command.d.ts +6 -0
- package/dist/commands/team-command.js +40 -3
- package/dist/config/load.js +4 -25
- package/dist/config/schema.d.ts +4 -4
- package/dist/config/schema.js +7 -1
- package/dist/proc/limits.d.ts +27 -13
- package/dist/proc/limits.js +27 -13
- package/dist/proc/progress.d.ts +7 -0
- package/dist/proc/progress.js +35 -7
- package/dist/proc/prompt.js +7 -4
- package/dist/proc/supervisor.js +7 -1
- package/dist/proc/types.d.ts +6 -0
- package/dist/proc/wait-plan.d.ts +39 -0
- package/dist/proc/wait-plan.js +77 -0
- package/dist/proc/wait-view.d.ts +65 -0
- package/dist/proc/wait-view.js +66 -0
- package/dist/proc/wait.d.ts +36 -3
- package/dist/proc/wait.js +67 -4
- package/dist/session/checkpoint.js +2 -0
- package/dist/session/persist.js +29 -2
- package/dist/session/validate-session.js +7 -2
- package/dist/team/forced-mode.d.ts +19 -0
- package/dist/team/forced-mode.js +42 -20
- package/dist/tools/job-tools.d.ts +13 -2
- package/dist/tools/job-tools.js +156 -33
- package/dist/ui/ActivityLine.js +5 -2
- package/dist/ui/App.js +59 -6
- package/dist/ui/StatusBar.d.ts +1 -0
- package/dist/ui/Transcript.js +11 -1
- package/dist/ui/entries/InvocationCard.d.ts +20 -0
- package/dist/ui/entries/InvocationCard.js +32 -0
- package/dist/ui/entries/ToolCard.js +11 -2
- package/dist/ui/entries/WaitCard.d.ts +37 -0
- package/dist/ui/entries/WaitCard.js +92 -0
- package/dist/ui/interaction-copy.d.ts +2 -0
- package/dist/ui/interaction-copy.js +3 -0
- package/dist/ui/layout/invocation-card.d.ts +41 -0
- package/dist/ui/layout/invocation-card.js +91 -0
- package/dist/ui/layout/status-layout.d.ts +1 -0
- package/dist/ui/layout/status-layout.js +10 -1
- package/dist/ui/layout/virtual-window.js +18 -2
- package/dist/ui/layout/wait-bar.d.ts +34 -0
- package/dist/ui/layout/wait-bar.js +48 -0
- package/dist/ui/run-status-row.d.ts +4 -2
- package/dist/ui/scrollback/live-preview.js +12 -0
- package/dist/ui/transcript-text.js +4 -1
- package/package.json +2 -2
package/dist/agent/reducer.d.ts
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* synthesizes a generic error notice (a swallowed throw is never a no-op).
|
|
12
12
|
*/
|
|
13
13
|
import type { PendingSteering } from './queued-messages.js';
|
|
14
|
+
import type { InvocationCard } from './invocation-card.js';
|
|
14
15
|
import type { AgentEvent, ModelCost, ToolResult, TokenUsage } from '@aragon-agent/core';
|
|
15
16
|
import type { AgentMode } from './agent-mode.js';
|
|
16
17
|
import type { DispatchOutcome, SubagentRun, SubagentSpec, TeamSnapshot } from '../team/types.js';
|
|
@@ -18,6 +19,7 @@ import type { FastReview, FastSnapshot } from '../fast/types.js';
|
|
|
18
19
|
import type { CompactionMode, CompactionPhase, CompactionRecord, CompactionSnapshot, CompactionUiTrigger, ContextUsageSnapshot } from '../compaction/types.js';
|
|
19
20
|
import type { AcceptanceSnapshot, TodoItem, TodoSnapshot } from '../todo/types.js';
|
|
20
21
|
import type { JobKind, ServiceSnapshot, ServiceStatus } from '../proc/types.js';
|
|
22
|
+
import type { WaitView } from '../proc/wait-view.js';
|
|
21
23
|
import type { RetryPhase } from './retry-view.js';
|
|
22
24
|
import type { FilePatch } from '../tools/patch.js';
|
|
23
25
|
import type { TaskSummaryView } from '../fast/task-summary-types.js';
|
|
@@ -47,6 +49,7 @@ export type Entry = {
|
|
|
47
49
|
id: string;
|
|
48
50
|
kind: 'user';
|
|
49
51
|
text: string;
|
|
52
|
+
invocation?: InvocationCard;
|
|
50
53
|
}
|
|
51
54
|
/**
|
|
52
55
|
* A message submitted while a run was in flight (tui-shift-enter-copy-queue
|
|
@@ -63,6 +66,7 @@ export type Entry = {
|
|
|
63
66
|
kind: 'queued';
|
|
64
67
|
text: string;
|
|
65
68
|
queueId?: string;
|
|
69
|
+
invocation?: InvocationCard;
|
|
66
70
|
} | {
|
|
67
71
|
id: string;
|
|
68
72
|
kind: 'assistant';
|
|
@@ -137,6 +141,16 @@ export type Entry = {
|
|
|
137
141
|
liveSeq?: number;
|
|
138
142
|
/** Epoch ms of the last output row, for the stall row (§3.3.4). */
|
|
139
143
|
lastOutputAt?: number;
|
|
144
|
+
/**
|
|
145
|
+
* The live view of a running `job_wait` (extended-job-wait §3.8.1), refreshed
|
|
146
|
+
* at 1 Hz. RUNTIME ONLY, and it lives and dies with `live` (I-9): cleared by
|
|
147
|
+
* `toolExecEnd` AND by every idle path through `releaseLiveTails`, and never
|
|
148
|
+
* persisted. `job_wait` never streams, so `live` is always empty for it - which
|
|
149
|
+
* is exactly why the idle clean-up cannot key off `live` alone.
|
|
150
|
+
*/
|
|
151
|
+
wait?: WaitView;
|
|
152
|
+
/** Monotonic counter: the revision term that invalidates the render cache. */
|
|
153
|
+
waitSeq?: number;
|
|
140
154
|
} | {
|
|
141
155
|
id: string;
|
|
142
156
|
kind: 'notice';
|
|
@@ -507,6 +521,7 @@ export declare function initialViewState(seed?: ViewStateSeed): ViewState;
|
|
|
507
521
|
export type ViewAction = {
|
|
508
522
|
type: 'submit';
|
|
509
523
|
text: string;
|
|
524
|
+
invocation?: InvocationCard;
|
|
510
525
|
}
|
|
511
526
|
/**
|
|
512
527
|
* `controller.steer` was accepted; append the persistent queue marker
|
|
@@ -517,6 +532,7 @@ export type ViewAction = {
|
|
|
517
532
|
type: 'steerQueued';
|
|
518
533
|
queueId: string;
|
|
519
534
|
text: string;
|
|
535
|
+
invocation?: InvocationCard;
|
|
520
536
|
} | {
|
|
521
537
|
type: 'steeringAccepted';
|
|
522
538
|
ids: readonly string[];
|
|
@@ -575,6 +591,15 @@ export type ViewAction = {
|
|
|
575
591
|
toolCallId: string;
|
|
576
592
|
rows: readonly string[];
|
|
577
593
|
at: number;
|
|
594
|
+
}
|
|
595
|
+
/**
|
|
596
|
+
* `job_wait`'s 1 Hz view (extended-job-wait). Carries the WHOLE view, so the
|
|
597
|
+
* coalescer keeps only the last per `toolCallId`, like `toolOutputDelta`.
|
|
598
|
+
*/
|
|
599
|
+
| {
|
|
600
|
+
type: 'waitUpdate';
|
|
601
|
+
toolCallId: string;
|
|
602
|
+
view: WaitView;
|
|
578
603
|
} | {
|
|
579
604
|
type: 'turnEnd';
|
|
580
605
|
usage: TokenUsage;
|
package/dist/agent/reducer.js
CHANGED
|
@@ -400,12 +400,22 @@ function settleRetryCard(state, entries, phase) {
|
|
|
400
400
|
function releaseLiveTails(entries) {
|
|
401
401
|
let touched = false;
|
|
402
402
|
const next = entries.map((e) => {
|
|
403
|
-
|
|
403
|
+
// `!e.live && !e.wait`, NOT `!e.live` (extended-job-wait RV-2): a `job_wait`
|
|
404
|
+
// never streams, so its `live` is always empty and the old guard would skip it,
|
|
405
|
+
// leaving a wait card that says "running" in the transcript forever.
|
|
406
|
+
if (e.kind !== 'tool' || (!e.live && !e.wait))
|
|
404
407
|
return e;
|
|
405
408
|
if (e.status !== 'running' && e.status !== 'pending')
|
|
406
409
|
return e;
|
|
407
410
|
touched = true;
|
|
408
|
-
return {
|
|
411
|
+
return {
|
|
412
|
+
...e,
|
|
413
|
+
live: undefined,
|
|
414
|
+
lastOutputAt: undefined,
|
|
415
|
+
liveSeq: (e.liveSeq ?? 0) + 1,
|
|
416
|
+
wait: undefined,
|
|
417
|
+
waitSeq: (e.waitSeq ?? 0) + 1,
|
|
418
|
+
};
|
|
409
419
|
});
|
|
410
420
|
return touched ? next : entries;
|
|
411
421
|
}
|
|
@@ -484,7 +494,10 @@ export function viewReducer(state, action) {
|
|
|
484
494
|
switch (action.type) {
|
|
485
495
|
case 'submit': {
|
|
486
496
|
const { id, seq } = nextId(state);
|
|
487
|
-
const entry = {
|
|
497
|
+
const entry = {
|
|
498
|
+
id, kind: 'user', text: action.text,
|
|
499
|
+
...(action.invocation ? { invocation: action.invocation } : {}),
|
|
500
|
+
};
|
|
488
501
|
return {
|
|
489
502
|
...state,
|
|
490
503
|
seq,
|
|
@@ -507,7 +520,10 @@ export function viewReducer(state, action) {
|
|
|
507
520
|
if (state.seenSteeringIds.has(action.queueId))
|
|
508
521
|
return state;
|
|
509
522
|
const { id, seq } = nextId(state);
|
|
510
|
-
const entry = {
|
|
523
|
+
const entry = {
|
|
524
|
+
id, kind: 'queued', queueId: action.queueId, text: action.text,
|
|
525
|
+
...(action.invocation ? { invocation: action.invocation } : {}),
|
|
526
|
+
};
|
|
511
527
|
// NOTHING ELSE RESETS. This is not a new turn -- `turnProduced`,
|
|
512
528
|
// `errorNoticed` and `aborted` describe the run that is still in
|
|
513
529
|
// flight, and clearing them here would let the next `runEnd` failure
|
|
@@ -515,7 +531,10 @@ export function viewReducer(state, action) {
|
|
|
515
531
|
return {
|
|
516
532
|
...state, seq, ...appendEntry(state, state.entries, entry),
|
|
517
533
|
seenSteeringIds: new Set([...state.seenSteeringIds, action.queueId]),
|
|
518
|
-
pendingSteering: [...state.pendingSteering, {
|
|
534
|
+
pendingSteering: [...state.pendingSteering, {
|
|
535
|
+
queueId: action.queueId, text: action.text,
|
|
536
|
+
...(action.invocation ? { invocation: action.invocation } : {}),
|
|
537
|
+
}],
|
|
519
538
|
};
|
|
520
539
|
}
|
|
521
540
|
case 'steeringAccepted': {
|
|
@@ -660,6 +679,23 @@ export function viewReducer(state, action) {
|
|
|
660
679
|
: e),
|
|
661
680
|
};
|
|
662
681
|
}
|
|
682
|
+
/**
|
|
683
|
+
* ONLY A RUNNING, MATCHING ENTRY. `begin` travels from the controller and can
|
|
684
|
+
* beat `tool_execution_start` through the agent-event pipe, so a miss is
|
|
685
|
+
* normal and is dropped on purpose: the next 1 Hz beat repairs it (RV-5).
|
|
686
|
+
*/
|
|
687
|
+
case 'waitUpdate': {
|
|
688
|
+
const id = findToolEntryId(state, action.toolCallId);
|
|
689
|
+
if (!id)
|
|
690
|
+
return state;
|
|
691
|
+
const target = state.entries.find((e) => e.id === id);
|
|
692
|
+
if (!target || target.kind !== 'tool' || target.status !== 'running')
|
|
693
|
+
return state;
|
|
694
|
+
return {
|
|
695
|
+
...state,
|
|
696
|
+
entries: mapEntry(state.entries, id, (e) => e.kind === 'tool' ? { ...e, wait: action.view, waitSeq: (e.waitSeq ?? 0) + 1 } : e),
|
|
697
|
+
};
|
|
698
|
+
}
|
|
663
699
|
case 'toolExecEnd': {
|
|
664
700
|
const id = findToolEntryId(state, action.toolCallId);
|
|
665
701
|
if (!id)
|
|
@@ -680,6 +716,8 @@ export function viewReducer(state, action) {
|
|
|
680
716
|
// `bashBadge`'s last-line parse (`ToolPreview.tsx:35-54`).
|
|
681
717
|
live: undefined,
|
|
682
718
|
lastOutputAt: undefined,
|
|
719
|
+
wait: undefined,
|
|
720
|
+
waitSeq: undefined,
|
|
683
721
|
}
|
|
684
722
|
: e),
|
|
685
723
|
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* "The user just spoke" as a subscribable edge (extended-job-wait §3.4).
|
|
3
|
+
*
|
|
4
|
+
* ASCII ONLY: `src/agent/**` is inside the glyph scanner's scope.
|
|
5
|
+
*
|
|
6
|
+
* EDGE-TRIGGERED ON PURPOSE: there is no "is anything pending" probe. A wait that
|
|
7
|
+
* could be woken by pre-existing state would return at once whenever that state
|
|
8
|
+
* went stale, and a model that gets an instant return from every `job_wait` burns
|
|
9
|
+
* tokens in a hot loop. Only a `notify()` that happens after `subscribe()` counts.
|
|
10
|
+
*/
|
|
11
|
+
export interface UserAttention {
|
|
12
|
+
/** Announce one new user input to every current subscriber. */
|
|
13
|
+
notify(): void;
|
|
14
|
+
/** Register a listener; returns its unsubscribe. */
|
|
15
|
+
subscribe(listener: () => void): () => void;
|
|
16
|
+
}
|
|
17
|
+
export declare function createUserAttention(): UserAttention;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* "The user just spoke" as a subscribable edge (extended-job-wait §3.4).
|
|
3
|
+
*
|
|
4
|
+
* ASCII ONLY: `src/agent/**` is inside the glyph scanner's scope.
|
|
5
|
+
*
|
|
6
|
+
* EDGE-TRIGGERED ON PURPOSE: there is no "is anything pending" probe. A wait that
|
|
7
|
+
* could be woken by pre-existing state would return at once whenever that state
|
|
8
|
+
* went stale, and a model that gets an instant return from every `job_wait` burns
|
|
9
|
+
* tokens in a hot loop. Only a `notify()` that happens after `subscribe()` counts.
|
|
10
|
+
*/
|
|
11
|
+
export function createUserAttention() {
|
|
12
|
+
const listeners = new Set();
|
|
13
|
+
return {
|
|
14
|
+
notify() {
|
|
15
|
+
// Snapshot first: a listener may unsubscribe itself while we iterate.
|
|
16
|
+
for (const listener of [...listeners]) {
|
|
17
|
+
try {
|
|
18
|
+
listener();
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
// One broken listener must not deprive the others of the wake-up.
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
subscribe(listener) {
|
|
26
|
+
listeners.add(listener);
|
|
27
|
+
return () => {
|
|
28
|
+
listeners.delete(listener);
|
|
29
|
+
};
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -1765,9 +1765,8 @@ function runConfigSetValue(key, value) {
|
|
|
1765
1765
|
const jobsPatch = applyJobsConfigSet(key, value);
|
|
1766
1766
|
if (jobsPatch) {
|
|
1767
1767
|
// Echoed from the STORED value, so `config set jobs.waitSeconds 1` prints 5
|
|
1768
|
-
// and `jobs.waitSeconds 9999` prints
|
|
1769
|
-
// states.
|
|
1770
|
-
// tool ceiling by I-4, so the stored value is the only honest answer.
|
|
1768
|
+
// and `jobs.waitSeconds 9999` prints 1200 — the rule every section above
|
|
1769
|
+
// states.
|
|
1771
1770
|
const merged = updatePersistedConfig(jobsPatch);
|
|
1772
1771
|
const field = key.slice('jobs.'.length);
|
|
1773
1772
|
process.stdout.write(`Set ${key} = ${String(merged.jobs[field])}\n`);
|
|
@@ -621,6 +621,8 @@ const COMMANDS = [
|
|
|
621
621
|
},
|
|
622
622
|
{
|
|
623
623
|
name: 'todo',
|
|
624
|
+
// Same verb parse as `run` below, so the echo is suppressed exactly when a card can appear (I-8).
|
|
625
|
+
rendersInvocationCard: (args) => args.trim().toLowerCase().split(/\s+/)[0] === 'continue',
|
|
624
626
|
description: 'Todo planning: status | on | off | panel on|off | follow <mode> | ' +
|
|
625
627
|
'acceptance [on|off|reset] | clear | continue',
|
|
626
628
|
/**
|
|
@@ -782,6 +784,16 @@ const COMMANDS = [
|
|
|
782
784
|
// the more verbose of the two forms (AC-17).
|
|
783
785
|
ctx.submit(buildContinuationMessage(snapshot), {
|
|
784
786
|
userInitiated: false, todoPolicy: 'continue',
|
|
787
|
+
// The transcript shows a card instead of the canned paragraph; the payload above
|
|
788
|
+
// is unchanged (tui-invocation-cards I-1).
|
|
789
|
+
...(ctx.invocationCards === false ? {} : {
|
|
790
|
+
invocation: {
|
|
791
|
+
variant: 'todo',
|
|
792
|
+
name: 'continue',
|
|
793
|
+
commandLine: '/todo continue',
|
|
794
|
+
facts: [`${Math.max(0, snapshot.total - snapshot.doneCount)} of ${snapshot.total} steps left`],
|
|
795
|
+
},
|
|
796
|
+
}),
|
|
785
797
|
});
|
|
786
798
|
return;
|
|
787
799
|
}
|
|
@@ -198,7 +198,17 @@ async function resume(ctx) {
|
|
|
198
198
|
return;
|
|
199
199
|
// `userInitiated: false` keeps the canned turn out of the prompt history, exactly like
|
|
200
200
|
// `/todo continue` (todo-plan-followthrough C-12).
|
|
201
|
-
ctx.submit(resumeTurnText(runId), {
|
|
201
|
+
ctx.submit(resumeTurnText(runId), {
|
|
202
|
+
userInitiated: false,
|
|
203
|
+
...(ctx.invocationCards === false ? {} : {
|
|
204
|
+
invocation: {
|
|
205
|
+
variant: 'govern',
|
|
206
|
+
name: 'resume',
|
|
207
|
+
commandLine: requested === '' ? '/gov resume' : `/gov resume ${requested}`,
|
|
208
|
+
facts: [`run ${runId.slice(0, 8)}`],
|
|
209
|
+
},
|
|
210
|
+
}),
|
|
211
|
+
});
|
|
202
212
|
}
|
|
203
213
|
function abort(ctx) {
|
|
204
214
|
if (!ctx.controller.isGovernanceStageRunning()) {
|
|
@@ -286,6 +296,8 @@ export async function runGovernanceCommand(ctx) {
|
|
|
286
296
|
export function registerGovernanceCommand(registry) {
|
|
287
297
|
registry.register({
|
|
288
298
|
name: 'gov',
|
|
299
|
+
// Same verb parse as `runGovernanceCommand`, so the echo is suppressed exactly when a card can appear (I-8).
|
|
300
|
+
rendersInvocationCard: (args) => (args.trim().split(/\s+/)[0] ?? '').toLowerCase() === 'resume',
|
|
289
301
|
description: GOV_COMMAND_SUMMARY,
|
|
290
302
|
run: (ctx) => runGovernanceCommand(ctx),
|
|
291
303
|
});
|
|
@@ -26,6 +26,14 @@ export declare function hasProjectIndex(cwd: string): boolean;
|
|
|
26
26
|
* second time (the skill's own edge-case list says exactly that).
|
|
27
27
|
*/
|
|
28
28
|
export declare function indexBuildInvocationArgs(hasExistingIndex: boolean): string;
|
|
29
|
+
/**
|
|
30
|
+
* True when `args` is one of the two machine-written sentences above. The invocation
|
|
31
|
+
* card uses this to NOT present them as something the user said (tui-invocation-cards
|
|
32
|
+
* RV-3): it shows a `ctrl+i` fact instead. Same file as the generator so the two cannot
|
|
33
|
+
* drift - a reworded sentence that stops matching would silently put machine words
|
|
34
|
+
* back on the card.
|
|
35
|
+
*/
|
|
36
|
+
export declare function isIndexBuildArgs(args: string): boolean;
|
|
29
37
|
export interface IndexConfirmInput {
|
|
30
38
|
cwd: string;
|
|
31
39
|
/** True when `.claude-index/index.md` already exists (rebuild, not first build). */
|
|
@@ -40,6 +40,17 @@ export function indexBuildInvocationArgs(hasExistingIndex) {
|
|
|
40
40
|
? 'The user pressed Ctrl+I and confirmed. Rebuild the project index for this workspace now, reusing the existing .claude-index/config.md preferences.'
|
|
41
41
|
: 'The user pressed Ctrl+I and confirmed. Build the project index for this workspace now, asking the configuration questions first.';
|
|
42
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* True when `args` is one of the two machine-written sentences above. The invocation
|
|
45
|
+
* card uses this to NOT present them as something the user said (tui-invocation-cards
|
|
46
|
+
* RV-3): it shows a `ctrl+i` fact instead. Same file as the generator so the two cannot
|
|
47
|
+
* drift - a reworded sentence that stops matching would silently put machine words
|
|
48
|
+
* back on the card.
|
|
49
|
+
*/
|
|
50
|
+
export function isIndexBuildArgs(args) {
|
|
51
|
+
const text = args.trim();
|
|
52
|
+
return text === indexBuildInvocationArgs(true) || text === indexBuildInvocationArgs(false);
|
|
53
|
+
}
|
|
43
54
|
/**
|
|
44
55
|
* The Yes/No dialog body. It names WHAT WILL BE WRITTEN - an index run is a
|
|
45
56
|
* batch of file writes the user never typed, and the one question a confirm
|
|
@@ -201,6 +201,13 @@ export interface CommandContext {
|
|
|
201
201
|
* TYPE-ONLY import, for the reason the `branch` note above records.
|
|
202
202
|
*/
|
|
203
203
|
taskSummary?: TaskSummaryCommandPort;
|
|
204
|
+
/**
|
|
205
|
+
* Whether transcript invocation cards are on (tui-invocation-cards section 3.6).
|
|
206
|
+
* `false` only under `ARAGON_NO_INVOCATION_CARDS=1`; commands then fall back to the
|
|
207
|
+
* pre-card notices and submit without `invocation`. Absent (every fixture built by
|
|
208
|
+
* literal) means ON.
|
|
209
|
+
*/
|
|
210
|
+
invocationCards?: boolean;
|
|
204
211
|
}
|
|
205
212
|
/**
|
|
206
213
|
* What `/keys` and `/task` may do.
|
|
@@ -240,6 +247,12 @@ export interface SlashCommand {
|
|
|
240
247
|
aliases?: string[];
|
|
241
248
|
description: string;
|
|
242
249
|
run: (ctx: CommandContext) => void | Promise<void>;
|
|
250
|
+
/**
|
|
251
|
+
* True when running with these args will submit a message that renders as an invocation
|
|
252
|
+
* card, so the `Command:` echo would only repeat it. MUST be the same predicate the
|
|
253
|
+
* command itself branches on (I-8); absent means false.
|
|
254
|
+
*/
|
|
255
|
+
rendersInvocationCard?: (args: string) => boolean;
|
|
243
256
|
}
|
|
244
257
|
export declare class CommandRegistry {
|
|
245
258
|
private readonly byName;
|
|
@@ -247,6 +260,16 @@ export declare class CommandRegistry {
|
|
|
247
260
|
get(name: string): SlashCommand | undefined;
|
|
248
261
|
all(): SlashCommand[];
|
|
249
262
|
}
|
|
263
|
+
/**
|
|
264
|
+
* Split raw composer input into a command word and its trimmed args; `null` when the
|
|
265
|
+
* input is not a slash command (no leading `/`, or the `//` literal-slash escape).
|
|
266
|
+
* `runSlashInput` and the App's echo suppression both go through here so they cannot
|
|
267
|
+
* disagree about what counts as a command.
|
|
268
|
+
*/
|
|
269
|
+
export declare function parseSlashInput(input: string): {
|
|
270
|
+
name: string;
|
|
271
|
+
args: string;
|
|
272
|
+
} | null;
|
|
250
273
|
/**
|
|
251
274
|
* Try to run a slash command from raw input. Returns `true` if the input was a
|
|
252
275
|
* (known or unknown) slash command and was consumed; `false` if it is a normal
|
|
@@ -19,21 +19,34 @@ export class CommandRegistry {
|
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
21
|
/**
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
22
|
+
* Split raw composer input into a command word and its trimmed args; `null` when the
|
|
23
|
+
* input is not a slash command (no leading `/`, or the `//` literal-slash escape).
|
|
24
|
+
* `runSlashInput` and the App's echo suppression both go through here so they cannot
|
|
25
|
+
* disagree about what counts as a command.
|
|
25
26
|
*/
|
|
26
|
-
export
|
|
27
|
+
export function parseSlashInput(input) {
|
|
27
28
|
const trimmed = input.trimStart();
|
|
28
29
|
if (!trimmed.startsWith('/'))
|
|
29
|
-
return
|
|
30
|
-
// `//...` escapes a literal leading slash
|
|
30
|
+
return null;
|
|
31
|
+
// `//...` escapes a literal leading slash - not a command.
|
|
31
32
|
if (trimmed.startsWith('//'))
|
|
32
|
-
return
|
|
33
|
+
return null;
|
|
33
34
|
const withoutSlash = trimmed.slice(1);
|
|
34
35
|
const spaceIdx = withoutSlash.search(/\s/);
|
|
35
36
|
const name = spaceIdx === -1 ? withoutSlash : withoutSlash.slice(0, spaceIdx);
|
|
36
37
|
const args = spaceIdx === -1 ? '' : withoutSlash.slice(spaceIdx + 1).trim();
|
|
38
|
+
return { name, args };
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Try to run a slash command from raw input. Returns `true` if the input was a
|
|
42
|
+
* (known or unknown) slash command and was consumed; `false` if it is a normal
|
|
43
|
+
* message that should go to the agent.
|
|
44
|
+
*/
|
|
45
|
+
export async function runSlashInput(registry, input, makeCtx) {
|
|
46
|
+
const parsed = parseSlashInput(input);
|
|
47
|
+
if (parsed === null)
|
|
48
|
+
return false;
|
|
49
|
+
const { name, args } = parsed;
|
|
37
50
|
const command = registry.get(name);
|
|
38
51
|
const ctx = makeCtx(args);
|
|
39
52
|
if (!command) {
|
package/dist/commands/skills.js
CHANGED
|
@@ -16,6 +16,7 @@ import { SKILL_TRIAGE_LIMITS } from '../skills/triage.js';
|
|
|
16
16
|
import { HOST_TOOL_NAMES, SKILL_TOOL_FLOOR } from '../tools/index.js';
|
|
17
17
|
import { SKILLS_TOOL_POLICY_MODES, isSkillsToolPolicyMode } from '../config/schema.js';
|
|
18
18
|
import { pickGlyphs } from '../ui/glyphs.js';
|
|
19
|
+
import { INDEX_SKILL_NAME, isIndexBuildArgs } from './index-build.js';
|
|
19
20
|
function marksFor(ctx) {
|
|
20
21
|
const cfg = ctx.controller.getConfig();
|
|
21
22
|
const glyphs = pickGlyphs({ colorLevel: cfg.colorLevel ?? 0, unicode: cfg.unicode ?? false });
|
|
@@ -673,10 +674,37 @@ export function registerSkillCommands(registry, service) {
|
|
|
673
674
|
}
|
|
674
675
|
}
|
|
675
676
|
}
|
|
677
|
+
/**
|
|
678
|
+
* The card's raw material for one skill run (tui-invocation-cards section 3.3). The
|
|
679
|
+
* command line always uses the namespaced `/skill:<name>` form - the stable handle -
|
|
680
|
+
* even when the user typed the bare name. Ctrl+I's machine-written sentence is NOT the
|
|
681
|
+
* user's input (RV-3): the card says `ctrl+i` and shows no input row.
|
|
682
|
+
*/
|
|
683
|
+
function skillCardInput(record, args, sent) {
|
|
684
|
+
const machine = record.name === INDEX_SKILL_NAME && isIndexBuildArgs(args);
|
|
685
|
+
const version = record.frontmatter.version;
|
|
686
|
+
const facts = [
|
|
687
|
+
...(machine ? ['ctrl+i'] : []),
|
|
688
|
+
...(version ? [`v${version}`] : []),
|
|
689
|
+
record.scope,
|
|
690
|
+
`${sent.kb} KB`,
|
|
691
|
+
...(sent.files > 0 ? [`${sent.files} files`] : []),
|
|
692
|
+
...(sent.policy ? ['tool ceiling'] : []),
|
|
693
|
+
];
|
|
694
|
+
const typed = machine ? '' : args;
|
|
695
|
+
return {
|
|
696
|
+
variant: 'skill',
|
|
697
|
+
name: record.name,
|
|
698
|
+
commandLine: typed === '' ? `/skill:${record.name}` : `/skill:${record.name} ${typed}`,
|
|
699
|
+
input: typed,
|
|
700
|
+
facts,
|
|
701
|
+
};
|
|
702
|
+
}
|
|
676
703
|
function makeSkillCommand(record, service) {
|
|
677
704
|
return {
|
|
678
705
|
name: record.name,
|
|
679
706
|
description: shortDescription(record),
|
|
707
|
+
rendersInvocationCard: () => true,
|
|
680
708
|
run: (ctx) => {
|
|
681
709
|
let loaded;
|
|
682
710
|
try {
|
|
@@ -703,8 +731,14 @@ function makeSkillCommand(record, service) {
|
|
|
703
731
|
// The SUBMITTED size, not the file's. Reporting 480 KB while sending 30 KB
|
|
704
732
|
// is how a user concludes the truncation marker is a rendering glitch.
|
|
705
733
|
const kb = Math.max(1, Math.round(Buffer.byteLength(message, 'utf-8') / 1024));
|
|
706
|
-
ctx.
|
|
707
|
-
|
|
734
|
+
if (ctx.invocationCards === false) {
|
|
735
|
+
ctx.notify('info', `Skill "${record.name}" loaded (${kb} KB submitted, ${loaded.files.length} bundled files).`);
|
|
736
|
+
ctx.submit(message);
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
ctx.submit(message, {
|
|
740
|
+
invocation: skillCardInput(record, ctx.args, { kb, files: loaded.files.length, policy: !!policy }),
|
|
741
|
+
});
|
|
708
742
|
},
|
|
709
743
|
};
|
|
710
744
|
}
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* file therefore holds ONE delegation and one line of deprecation text, and
|
|
18
18
|
* `subagent-command.test.ts` asserts both (I-8).
|
|
19
19
|
*/
|
|
20
|
-
import { runTeamCommand } from './team-command.js';
|
|
20
|
+
import { runTeamCommand, teamRendersInvocationCard } from './team-command.js';
|
|
21
21
|
/**
|
|
22
22
|
* Run `/team`, having first said out loud that this is the same thing.
|
|
23
23
|
*
|
|
@@ -33,6 +33,8 @@ export function runSubagentCommand(ctx) {
|
|
|
33
33
|
export function registerSubagentCommand(registry) {
|
|
34
34
|
registry.register({
|
|
35
35
|
name: 'subagent',
|
|
36
|
+
// A fresh object inherits nothing from `/team`: reference the SAME predicate (RV-5).
|
|
37
|
+
rendersInvocationCard: teamRendersInvocationCard,
|
|
36
38
|
description: 'Deprecated alias of /team - same arguments',
|
|
37
39
|
run: (ctx) => { runSubagentCommand(ctx); },
|
|
38
40
|
});
|
|
@@ -43,6 +43,12 @@
|
|
|
43
43
|
* `Team 4` chip would wait for an unrelated state change before appearing.
|
|
44
44
|
*/
|
|
45
45
|
import type { CommandContext, CommandRegistry } from './registry.js';
|
|
46
|
+
/**
|
|
47
|
+
* Whether `/team <args>` ends in a card. The SAME function the command branches on
|
|
48
|
+
* (`readTeamIntent`), and shared by `/team` and the `/subagent` alias by reference: the
|
|
49
|
+
* alias builds a fresh object and inherits nothing (RV-5, I-8).
|
|
50
|
+
*/
|
|
51
|
+
export declare function teamRendersInvocationCard(args: string): boolean;
|
|
46
52
|
/** The command body; `registerTeamCommand` is the only registration site. */
|
|
47
53
|
export declare function runTeamCommand(ctx: CommandContext): void;
|
|
48
54
|
/** Registered from `builtins.ts` with one line, next to every other subsystem. */
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
* `Team 4` chip would wait for an unrelated state change before appearing.
|
|
44
44
|
*/
|
|
45
45
|
import { buildForcedDirective } from '../team/forced-dispatch.js';
|
|
46
|
-
import { DEFAULT_FORCED_COUNT, planForced, planNotice, readTeamIntent, } from '../team/forced-mode.js';
|
|
46
|
+
import { DEFAULT_FORCED_COUNT, forcedCardNote, planForced, planNotice, readTeamIntent, } from '../team/forced-mode.js';
|
|
47
47
|
import { subagentModelSummary } from '../ui/overlays/model-profile-state.js';
|
|
48
48
|
/**
|
|
49
49
|
* The subagent model line, phrased by the same authority the settings root row
|
|
@@ -199,6 +199,34 @@ function enterForcedMode(ctx) {
|
|
|
199
199
|
ctx.notify('info', `Forced team mode on: ${adopted} subagents per turn. `
|
|
200
200
|
+ 'Your next message dispatches.');
|
|
201
201
|
}
|
|
202
|
+
/**
|
|
203
|
+
* The card's raw material for `/team <brief>` (tui-invocation-cards section 3.3). The
|
|
204
|
+
* command line is always spelled `/team`, even when the user came in through the
|
|
205
|
+
* deprecated `/subagent` alias: the card names the canonical command.
|
|
206
|
+
*/
|
|
207
|
+
function forcedCardInput(args, planned, note, ctx) {
|
|
208
|
+
const noun = planned.count === 1 ? 'subagent' : 'subagents';
|
|
209
|
+
const facts = [
|
|
210
|
+
planned.parts.length > 0 ? `parts ${planned.parts.join(', ')}` : `${planned.count} ${noun}`,
|
|
211
|
+
...(planned.readOnly ? ['read-only'] : []),
|
|
212
|
+
...(ctx.controller.getAgentMode() === 'plan' ? ['plan mode'] : []),
|
|
213
|
+
];
|
|
214
|
+
return {
|
|
215
|
+
variant: 'team',
|
|
216
|
+
commandLine: args === '' ? '/team' : `/team ${args}`,
|
|
217
|
+
input: planned.brief,
|
|
218
|
+
facts,
|
|
219
|
+
...(note !== undefined ? { note } : {}),
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Whether `/team <args>` ends in a card. The SAME function the command branches on
|
|
224
|
+
* (`readTeamIntent`), and shared by `/team` and the `/subagent` alias by reference: the
|
|
225
|
+
* alias builds a fresh object and inherits nothing (RV-5, I-8).
|
|
226
|
+
*/
|
|
227
|
+
export function teamRendersInvocationCard(args) {
|
|
228
|
+
return readTeamIntent(args).kind === 'brief';
|
|
229
|
+
}
|
|
202
230
|
/** `/team [-n n|--parts a,b] [--read-only] <brief>`: arm and submit in one act. */
|
|
203
231
|
function armAndSubmit(ctx, intent) {
|
|
204
232
|
const team = ctx.controller.getTeamConfig();
|
|
@@ -209,7 +237,11 @@ function armAndSubmit(ctx, intent) {
|
|
|
209
237
|
// directive names: the later turns reuse this count, so the first turn and the
|
|
210
238
|
// rest of the session cannot disagree about how wide the fan-out is (D-7).
|
|
211
239
|
ctx.controller.armForcedTeam(planned.count);
|
|
212
|
-
|
|
240
|
+
// With cards on, the disclosure rides on the card (`forcedCardNote`, same source as
|
|
241
|
+
// `planNotice`); with them off this is the pre-card notice, byte for byte.
|
|
242
|
+
const cards = ctx.invocationCards !== false;
|
|
243
|
+
if (!cards)
|
|
244
|
+
ctx.notify('info', planNotice(planned, team));
|
|
213
245
|
const directive = buildForcedDirective({
|
|
214
246
|
brief: planned.brief,
|
|
215
247
|
count: planned.count,
|
|
@@ -222,7 +254,11 @@ function armAndSubmit(ctx, intent) {
|
|
|
222
254
|
});
|
|
223
255
|
// `userInitiated: false`: a directive the user did not type belongs in neither
|
|
224
256
|
// the prompt history nor the hint-fade counter (todo-plan-followthrough C-12).
|
|
225
|
-
|
|
257
|
+
const note = forcedCardNote(planned, team);
|
|
258
|
+
ctx.submit(directive, {
|
|
259
|
+
userInitiated: false,
|
|
260
|
+
...(cards ? { invocation: forcedCardInput(ctx.args, planned, note, ctx) } : {}),
|
|
261
|
+
});
|
|
226
262
|
}
|
|
227
263
|
/** The command body; `registerTeamCommand` is the only registration site. */
|
|
228
264
|
export function runTeamCommand(ctx) {
|
|
@@ -253,6 +289,7 @@ export function runTeamCommand(ctx) {
|
|
|
253
289
|
export function registerTeamCommand(registry) {
|
|
254
290
|
registry.register({
|
|
255
291
|
name: 'team',
|
|
292
|
+
rendersInvocationCard: teamRendersInvocationCard,
|
|
256
293
|
description: 'Force team mode and start now: /team <brief>; /team [on|off|status]',
|
|
257
294
|
run: (ctx) => { runTeamCommand(ctx); },
|
|
258
295
|
});
|
package/dist/config/load.js
CHANGED
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
* legitimately long single tool run can never be watchdog-killed.
|
|
7
7
|
*/
|
|
8
8
|
import process from 'node:process';
|
|
9
|
-
import { JOB_WAIT_MARGIN_MS } from '../proc/limits.js';
|
|
10
9
|
import { DEFAULT_CONFIG, DEFAULT_SCROLL_MODE, IDLE_TIMEOUT_MARGIN_MS, clampDensity, clampHumanTimeout, clampCompactionConfig, clampLogConfig, clampFastConfig, clampRetryConfig, clampSkillsConfig, clampTeamConfig, clampTodoConfig, clampBashConfig, clampJobsConfig, clampPromptsConfig, clampTaskSummaryConfig, clampUpdateConfig, clampTheme, clampThinkingLevel, clampContextWindow, clampMaxTokens, clampMaxRenderInterval, clampScrollMode, clampScrollResumeMs, clampTranscriptRetain, clampTranscriptWindow, coercePositiveInt, isAutoToken, isSkillsToolPolicyMode, legacyTeamCeilingPatch, legacyTeamFanoutPatch, LEGACY_TEAM_FANOUT_DEFAULTS, parseThresholdInput, } from './schema.js';
|
|
11
10
|
import { loadDotenv, readEnvConfig, readEnvDisabledSkills } from './env.js';
|
|
12
11
|
import { readConfigFile, updatePersistedConfig } from './store.js';
|
|
@@ -622,32 +621,14 @@ export function loadConfig(flags = {}, options = {}) {
|
|
|
622
621
|
DEFAULT_CONFIG.planModeDefault;
|
|
623
622
|
const planModeHumanTimeoutMs = clampHumanTimeout(file.planModeHumanTimeoutMs, DEFAULT_CONFIG.planModeHumanTimeoutMs);
|
|
624
623
|
const toolTimeoutMs = coercePositiveInt(pick(flags.toolTimeout, file.toolTimeoutMs), DEFAULT_CONFIG.toolTimeoutMs);
|
|
625
|
-
//
|
|
626
|
-
//
|
|
627
|
-
//
|
|
628
|
-
// big config literal a hundred lines further down — a reference to it here would
|
|
629
|
-
// be a use-before-declaration, and "resolve it later and patch the object" is
|
|
630
|
-
// how the invariant gets silently skipped. Resolving it first makes the clamp
|
|
631
|
-
// impossible to omit.
|
|
624
|
+
// `jobs.waitSeconds` is NOT converged against `toolTimeoutMs` (extended-job-wait
|
|
625
|
+
// I-4'): `job_wait` has its own executor ceiling and pauses the idle watchdog,
|
|
626
|
+
// so the section is resolved once, as-is.
|
|
632
627
|
const jobs = resolveJobsConfig(env.partial, file);
|
|
633
628
|
let idleTimeoutMs = coercePositiveInt(pick(flags.idleTimeout, file.idleTimeoutMs), DEFAULT_CONFIG.idleTimeoutMs);
|
|
634
629
|
// Timeout invariant (R1): idle must be strictly greater than the tool ceiling
|
|
635
630
|
// so a long single tool run is never aborted by the idle watchdog.
|
|
636
631
|
idleTimeoutMs = Math.max(idleTimeoutMs, toolTimeoutMs + IDLE_TIMEOUT_MARGIN_MS);
|
|
637
|
-
// INVARIANT I-4 (long-task-jobs): a wait must fit inside the tool call that
|
|
638
|
-
// hosts it, `jobs.waitSeconds * 1000 + JOB_WAIT_MARGIN_MS <= toolTimeoutMs`.
|
|
639
|
-
//
|
|
640
|
-
// `Math.min`, AND THE DIRECTION IS THE POINT. The invariant above RAISES the
|
|
641
|
-
// idle window to accommodate the tool; this one LOWERS the wait to accommodate
|
|
642
|
-
// the tool. Copying `Math.max` here would produce a wait that outlives its own
|
|
643
|
-
// tool call — which is exactly the `Command timed out` failure the feature
|
|
644
|
-
// exists to remove, re-created by its own config check.
|
|
645
|
-
//
|
|
646
|
-
// With an operator-set ceiling below the margin the floor of 1s applies rather
|
|
647
|
-
// than a negative wait: the invariant wins over the minimum, because a wait
|
|
648
|
-
// longer than its host is worse than a short wait.
|
|
649
|
-
const waitCeilingSeconds = Math.floor((toolTimeoutMs - JOB_WAIT_MARGIN_MS) / 1000);
|
|
650
|
-
jobs.waitSeconds = Math.max(1, Math.min(jobs.waitSeconds, waitCeilingSeconds));
|
|
651
632
|
// Resolve API keys: config-file key wins over the provider env var.
|
|
652
633
|
const fileKeys = file.apiKeys ?? {};
|
|
653
634
|
const apiKeys = {};
|
|
@@ -798,9 +779,7 @@ export function loadConfig(flags = {}, options = {}) {
|
|
|
798
779
|
update: resolveUpdateConfig(flags, env.partial, file),
|
|
799
780
|
compaction: resolveCompactionConfig(flags, env.partial, file),
|
|
800
781
|
bash: resolveBashConfig(env.partial, file),
|
|
801
|
-
// The already-resolved
|
|
802
|
-
// NOT `resolveJobsConfig(...)` a second time: that would re-apply the
|
|
803
|
-
// un-converged `waitSeconds` and silently undo I-4.
|
|
782
|
+
// The already-resolved section from the top of this function.
|
|
804
783
|
jobs,
|
|
805
784
|
// The quick-prompt library's three policy scalars (quick-prompt-library D-19).
|
|
806
785
|
// Resolved through the SAME clamp the read and write paths use, so an env-only
|