@aragon-agent/cli 0.6.45 → 0.6.46
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/README.md +51 -0
- 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 +5 -0
- package/dist/agent/reducer.js +12 -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/session/persist.js +10 -0
- package/dist/session/validate-session.js +4 -2
- package/dist/team/forced-mode.d.ts +19 -0
- package/dist/team/forced-mode.js +42 -20
- package/dist/ui/App.js +25 -6
- package/dist/ui/Transcript.js +4 -1
- package/dist/ui/entries/InvocationCard.d.ts +20 -0
- package/dist/ui/entries/InvocationCard.js +32 -0
- package/dist/ui/layout/invocation-card.d.ts +41 -0
- package/dist/ui/layout/invocation-card.js +91 -0
- package/dist/ui/layout/virtual-window.js +11 -1
- package/dist/ui/transcript-text.js +4 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -3013,6 +3013,57 @@ declares no placeholder, the arguments are appended under an `## Arguments`
|
|
|
3013
3013
|
heading instead. Where a skill's name collides with a built-in command, the
|
|
3014
3014
|
**built-in wins** and the skill is reachable as `/skill:<name>`.
|
|
3015
3015
|
|
|
3016
|
+
### Invocation cards
|
|
3017
|
+
|
|
3018
|
+
A command that starts a task by submitting a long prompt on your behalf does not
|
|
3019
|
+
print that prompt. `/pdf-forms`, `/skill:<name>`, `/team <brief>`, `/todo continue`
|
|
3020
|
+
and `/gov resume` each leave one **card** in the transcript instead: what you
|
|
3021
|
+
started, a few facts, and your own words.
|
|
3022
|
+
|
|
3023
|
+
```
|
|
3024
|
+
> skill . pdf-forms v1.2.0 . user . 31 KB . 4 files
|
|
3025
|
+
Fill the W-9 form in ./tax/w9.pdf with the values from data.json
|
|
3026
|
+
|
|
3027
|
+
> team 4 subagents . read-only
|
|
3028
|
+
Audit the auth module for session fixation and add regression tests
|
|
3029
|
+
Asked for 12; team.maxSubagents is 8.
|
|
3030
|
+
|
|
3031
|
+
> todo . continue 3 of 7 steps left
|
|
3032
|
+
```
|
|
3033
|
+
|
|
3034
|
+
(`.` is `·` on a Unicode terminal.) The first row names the command and its
|
|
3035
|
+
facts and is shortened to the width of the window - facts go first, then the name.
|
|
3036
|
+
Your own words follow, capped at 8 lines / 480 characters with an honest
|
|
3037
|
+
`... +N chars not shown` tail. A line below it appears only when the command
|
|
3038
|
+
changed what you asked for (a fan-out above `team.maxSubagents`). `Ctrl+I` shows a
|
|
3039
|
+
`ctrl+i` fact and no input row: the sentence it sends the model is not something
|
|
3040
|
+
you said.
|
|
3041
|
+
|
|
3042
|
+
What changes and what does not:
|
|
3043
|
+
|
|
3044
|
+
- **The model receives exactly the bytes it did before.** Only the transcript
|
|
3045
|
+
entry is different; the skill body, the team directive and the continuation text
|
|
3046
|
+
are untouched.
|
|
3047
|
+
- The `Command:` echo and the old `Skill "x" loaded (...)` / `Forcing a fan-out...`
|
|
3048
|
+
notices are folded into the card, so one act is stated once.
|
|
3049
|
+
- Up-arrow history and the header task label hold the command you typed
|
|
3050
|
+
(`/skill:pdf-forms a.pdf`), not the prompt behind it. Canned turns the commands
|
|
3051
|
+
submit for you (`/team`, `/todo continue`, `/gov resume`) still stay out of
|
|
3052
|
+
history and do not trigger a task label, exactly as before.
|
|
3053
|
+
- A skill run now adds one history entry instead of two to the hint-fade counter.
|
|
3054
|
+
- Secrets in your arguments are redacted **before** anything is cut, so a key that
|
|
3055
|
+
straddles a cut cannot leave a readable prefix in the transcript, the session
|
|
3056
|
+
file or the exit replay.
|
|
3057
|
+
- The exit replay prints the command line plus one `[skill pdf-forms v1.2.0 - ...]`
|
|
3058
|
+
line. A very long brief is capped at 2000 characters there; the full text is in
|
|
3059
|
+
the up-arrow history and in the conversation itself.
|
|
3060
|
+
- Sessions written before cards existed load unchanged; a damaged card in a session
|
|
3061
|
+
file degrades to the plain command line instead of failing the load.
|
|
3062
|
+
|
|
3063
|
+
`ARAGON_NO_INVOCATION_CARDS=1` restores the previous behaviour completely (echo,
|
|
3064
|
+
notices and the full prompt in the transcript). It is an emergency switch, not a
|
|
3065
|
+
setting.
|
|
3066
|
+
|
|
3016
3067
|
### Where skills come from
|
|
3017
3068
|
|
|
3018
3069
|
Four scopes, later ones overriding earlier ones by name:
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Invocation cards (tui-invocation-cards): what a transcript shows INSTEAD of the
|
|
3
|
+
* system prompt a command submitted on the user's behalf.
|
|
4
|
+
*
|
|
5
|
+
* Pure and total: no IO, no React, no Ink, nothing but `import type`. ASCII ONLY -
|
|
6
|
+
* `src/agent/**` new files sit inside the `glyphs.test.ts` scan scope.
|
|
7
|
+
*
|
|
8
|
+
* ONE CONSTRUCTOR (I-3). `buildInvocationCard` is the only function that turns raw
|
|
9
|
+
* command-layer material into a card, `App.submitMessage` is its only caller, and
|
|
10
|
+
* `sanitizeInvocationCard` (the session-load path) shares its normaliser so a card
|
|
11
|
+
* written today and a card read back tomorrow cannot drift apart.
|
|
12
|
+
*
|
|
13
|
+
* ORDER IS REDACT -> NORMALISE -> LIMIT, and it is the load-bearing part (I-6). Cut
|
|
14
|
+
* first and a secret that straddles the cut leaves a prefix shorter than the
|
|
15
|
+
* redaction patterns' minimum length: the half-key sails through into the
|
|
16
|
+
* transcript, the session file and the exit replay.
|
|
17
|
+
*/
|
|
18
|
+
export type InvocationVariant = 'skill' | 'team' | 'todo' | 'govern';
|
|
19
|
+
/** Everything one card needs; persisted, queued and rendered in this one shape. */
|
|
20
|
+
export interface InvocationCard {
|
|
21
|
+
readonly variant: InvocationVariant;
|
|
22
|
+
/** Skill name / 'continue' / 'resume'; '' for team. */
|
|
23
|
+
readonly name: string;
|
|
24
|
+
/** What the user typed (or its equivalent), redacted and capped; also `Entry.text`. */
|
|
25
|
+
readonly commandLine: string;
|
|
26
|
+
/** The user's own words (args / brief), redacted and trimmed; may be ''. */
|
|
27
|
+
readonly input: string;
|
|
28
|
+
/** Characters cut from `input`; 0 when nothing was cut. */
|
|
29
|
+
readonly inputHidden: number;
|
|
30
|
+
/** Fact fragments, each at most `factMaxChars`, at most `factsMax` of them. */
|
|
31
|
+
readonly facts: readonly string[];
|
|
32
|
+
/** One disclosure sentence. */
|
|
33
|
+
readonly note?: string;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The renderer, the height estimate and the loader all read THESE numbers (I-4).
|
|
37
|
+
* A card's height is a function of its data and these limits, so it is bounded:
|
|
38
|
+
* header + `inputMaxLines` + hidden tail + note.
|
|
39
|
+
*/
|
|
40
|
+
export declare const INVOCATION_LIMITS: {
|
|
41
|
+
readonly commandLineMaxChars: 2000;
|
|
42
|
+
readonly inputMaxChars: 480;
|
|
43
|
+
readonly inputMaxLines: 8;
|
|
44
|
+
readonly nameMaxChars: 64;
|
|
45
|
+
readonly factsMax: 5;
|
|
46
|
+
readonly factMaxChars: 28;
|
|
47
|
+
readonly noteMaxChars: 140;
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* Raw material from the command layer: NOT redacted, NOT trimmed. Commands must not
|
|
51
|
+
* cut or scrub it themselves - both happen exactly once, in `buildInvocationCard`.
|
|
52
|
+
*/
|
|
53
|
+
export interface InvocationCardInput {
|
|
54
|
+
variant: InvocationVariant;
|
|
55
|
+
name?: string;
|
|
56
|
+
commandLine: string;
|
|
57
|
+
input?: string;
|
|
58
|
+
facts?: readonly string[];
|
|
59
|
+
note?: string;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Total, never throws. `redact` is injected (the same closure `Command:` echo uses) so
|
|
63
|
+
* this module depends on nothing; the default is the identity for tests and the loader.
|
|
64
|
+
*/
|
|
65
|
+
export declare function buildInvocationCard(input: InvocationCardInput, redact?: (text: string) => string): InvocationCard;
|
|
66
|
+
/**
|
|
67
|
+
* Session-load path: `null` for anything that is not a plausible card, so the caller
|
|
68
|
+
* drops the field and the entry degrades to a plain user message (I-7). Re-derives
|
|
69
|
+
* `inputHidden` from nothing - it trusts the stored count only when it is a sane integer.
|
|
70
|
+
*/
|
|
71
|
+
export declare function sanitizeInvocationCard(value: unknown): InvocationCard | null;
|
|
72
|
+
/**
|
|
73
|
+
* Emergency kill switch (`ARAGON_NO_INVOCATION_CARDS=1`): every command and the App fall
|
|
74
|
+
* back to the pre-card behaviour. Read at call time, not import time, so a test (or a
|
|
75
|
+
* wrapper script) that sets the variable after load is honoured.
|
|
76
|
+
*/
|
|
77
|
+
export declare function invocationCardsEnabled(env?: NodeJS.ProcessEnv): boolean;
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Invocation cards (tui-invocation-cards): what a transcript shows INSTEAD of the
|
|
3
|
+
* system prompt a command submitted on the user's behalf.
|
|
4
|
+
*
|
|
5
|
+
* Pure and total: no IO, no React, no Ink, nothing but `import type`. ASCII ONLY -
|
|
6
|
+
* `src/agent/**` new files sit inside the `glyphs.test.ts` scan scope.
|
|
7
|
+
*
|
|
8
|
+
* ONE CONSTRUCTOR (I-3). `buildInvocationCard` is the only function that turns raw
|
|
9
|
+
* command-layer material into a card, `App.submitMessage` is its only caller, and
|
|
10
|
+
* `sanitizeInvocationCard` (the session-load path) shares its normaliser so a card
|
|
11
|
+
* written today and a card read back tomorrow cannot drift apart.
|
|
12
|
+
*
|
|
13
|
+
* ORDER IS REDACT -> NORMALISE -> LIMIT, and it is the load-bearing part (I-6). Cut
|
|
14
|
+
* first and a secret that straddles the cut leaves a prefix shorter than the
|
|
15
|
+
* redaction patterns' minimum length: the half-key sails through into the
|
|
16
|
+
* transcript, the session file and the exit replay.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* The renderer, the height estimate and the loader all read THESE numbers (I-4).
|
|
20
|
+
* A card's height is a function of its data and these limits, so it is bounded:
|
|
21
|
+
* header + `inputMaxLines` + hidden tail + note.
|
|
22
|
+
*/
|
|
23
|
+
export const INVOCATION_LIMITS = {
|
|
24
|
+
commandLineMaxChars: 2000,
|
|
25
|
+
inputMaxChars: 480,
|
|
26
|
+
inputMaxLines: 8,
|
|
27
|
+
nameMaxChars: 64,
|
|
28
|
+
factsMax: 5,
|
|
29
|
+
factMaxChars: 28,
|
|
30
|
+
noteMaxChars: 140,
|
|
31
|
+
};
|
|
32
|
+
const VARIANTS = ['skill', 'team', 'todo', 'govern'];
|
|
33
|
+
// C0 except TAB (\x09) and LF (\x0a), then DEL and C1 (which includes the 8-bit CSI
|
|
34
|
+
// \x9b). Matches the range `QueueOverlay` strips, so the card is no more permissive.
|
|
35
|
+
// eslint-disable-next-line no-control-regex
|
|
36
|
+
const CONTROL_CHARS = /[\x00-\x08\x0b-\x1f\x7f-\x9f]/g;
|
|
37
|
+
const CAP_MARK = '...';
|
|
38
|
+
/** Cut at a code-point boundary: a surrogate pair is never split. */
|
|
39
|
+
function sliceCodePoints(text, count) {
|
|
40
|
+
if (count <= 0)
|
|
41
|
+
return '';
|
|
42
|
+
if (text.length <= count)
|
|
43
|
+
return text;
|
|
44
|
+
let end = count;
|
|
45
|
+
const last = text.charCodeAt(end - 1);
|
|
46
|
+
// A high surrogate at the cut would orphan its low half.
|
|
47
|
+
if (last >= 0xd800 && last <= 0xdbff)
|
|
48
|
+
end -= 1;
|
|
49
|
+
return text.slice(0, end);
|
|
50
|
+
}
|
|
51
|
+
function countCodePoints(text) {
|
|
52
|
+
let count = 0;
|
|
53
|
+
for (const _ of text)
|
|
54
|
+
count += 1;
|
|
55
|
+
return count;
|
|
56
|
+
}
|
|
57
|
+
/** CRLF and bare CR to LF (a bare CR returns Ink's cursor to column 0), then strip controls. */
|
|
58
|
+
function normalizeText(text) {
|
|
59
|
+
return text.replace(/\r\n?/g, '\n').replace(CONTROL_CHARS, '');
|
|
60
|
+
}
|
|
61
|
+
function capCommandLine(text) {
|
|
62
|
+
const max = INVOCATION_LIMITS.commandLineMaxChars;
|
|
63
|
+
if (text.length <= max)
|
|
64
|
+
return text;
|
|
65
|
+
return sliceCodePoints(text, max - CAP_MARK.length) + CAP_MARK;
|
|
66
|
+
}
|
|
67
|
+
function capInput(text) {
|
|
68
|
+
const trimmed = text.trim();
|
|
69
|
+
if (trimmed === '')
|
|
70
|
+
return { input: '', hidden: 0 };
|
|
71
|
+
const lines = trimmed.split('\n');
|
|
72
|
+
let kept = lines.length > INVOCATION_LIMITS.inputMaxLines
|
|
73
|
+
? lines.slice(0, INVOCATION_LIMITS.inputMaxLines).join('\n') : trimmed;
|
|
74
|
+
kept = sliceCodePoints(kept, INVOCATION_LIMITS.inputMaxChars).trimEnd();
|
|
75
|
+
return { input: kept, hidden: countCodePoints(trimmed) - countCodePoints(kept) };
|
|
76
|
+
}
|
|
77
|
+
function capSingleLine(text, max) {
|
|
78
|
+
const flat = text.replace(/\s+/g, ' ').trim();
|
|
79
|
+
if (countCodePoints(flat) <= max)
|
|
80
|
+
return flat;
|
|
81
|
+
return sliceCodePoints(flat, max - CAP_MARK.length) + CAP_MARK;
|
|
82
|
+
}
|
|
83
|
+
function capFacts(facts) {
|
|
84
|
+
return facts
|
|
85
|
+
.map((fact) => capSingleLine(fact, INVOCATION_LIMITS.factMaxChars))
|
|
86
|
+
.filter((fact) => fact !== '')
|
|
87
|
+
.slice(0, INVOCATION_LIMITS.factsMax);
|
|
88
|
+
}
|
|
89
|
+
function capNote(note) {
|
|
90
|
+
if (note === undefined)
|
|
91
|
+
return undefined;
|
|
92
|
+
const capped = capSingleLine(note, INVOCATION_LIMITS.noteMaxChars);
|
|
93
|
+
return capped === '' ? undefined : capped;
|
|
94
|
+
}
|
|
95
|
+
/** Shared by the write path and the load path so the two cannot diverge. */
|
|
96
|
+
function assemble(parts) {
|
|
97
|
+
const { input, hidden } = capInput(normalizeText(parts.input));
|
|
98
|
+
const note = capNote(normalizeText(parts.note ?? ''));
|
|
99
|
+
return {
|
|
100
|
+
variant: parts.variant,
|
|
101
|
+
name: capSingleLine(normalizeText(parts.name), INVOCATION_LIMITS.nameMaxChars),
|
|
102
|
+
commandLine: capCommandLine(normalizeText(parts.commandLine).trim()),
|
|
103
|
+
input,
|
|
104
|
+
inputHidden: hidden,
|
|
105
|
+
facts: capFacts(parts.facts.map(normalizeText)),
|
|
106
|
+
...(note !== undefined ? { note } : {}),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Total, never throws. `redact` is injected (the same closure `Command:` echo uses) so
|
|
111
|
+
* this module depends on nothing; the default is the identity for tests and the loader.
|
|
112
|
+
*/
|
|
113
|
+
export function buildInvocationCard(input, redact = (text) => text) {
|
|
114
|
+
const scrub = (text) => {
|
|
115
|
+
if (text === undefined)
|
|
116
|
+
return '';
|
|
117
|
+
try {
|
|
118
|
+
return redact(text);
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
return '';
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
return assemble({
|
|
125
|
+
variant: input.variant,
|
|
126
|
+
name: scrub(input.name),
|
|
127
|
+
commandLine: scrub(input.commandLine),
|
|
128
|
+
input: scrub(input.input),
|
|
129
|
+
facts: (input.facts ?? []).map((fact) => scrub(fact)),
|
|
130
|
+
note: input.note === undefined ? undefined : scrub(input.note),
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
function asString(value) {
|
|
134
|
+
return typeof value === 'string' ? value : null;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Session-load path: `null` for anything that is not a plausible card, so the caller
|
|
138
|
+
* drops the field and the entry degrades to a plain user message (I-7). Re-derives
|
|
139
|
+
* `inputHidden` from nothing - it trusts the stored count only when it is a sane integer.
|
|
140
|
+
*/
|
|
141
|
+
export function sanitizeInvocationCard(value) {
|
|
142
|
+
if (typeof value !== 'object' || value === null)
|
|
143
|
+
return null;
|
|
144
|
+
const raw = value;
|
|
145
|
+
const variant = VARIANTS.find((candidate) => candidate === raw.variant);
|
|
146
|
+
const commandLine = asString(raw.commandLine);
|
|
147
|
+
if (variant === undefined || commandLine === null)
|
|
148
|
+
return null;
|
|
149
|
+
const name = asString(raw.name) ?? '';
|
|
150
|
+
const input = asString(raw.input) ?? '';
|
|
151
|
+
const facts = Array.isArray(raw.facts)
|
|
152
|
+
? raw.facts.filter((fact) => typeof fact === 'string') : [];
|
|
153
|
+
const note = asString(raw.note) ?? undefined;
|
|
154
|
+
const card = assemble({ variant, name, commandLine, input, facts, note });
|
|
155
|
+
// A stored card that was already capped must keep its recorded `inputHidden`; the
|
|
156
|
+
// re-cap above can only ever add to it.
|
|
157
|
+
const stored = raw.inputHidden;
|
|
158
|
+
const storedHidden = typeof stored === 'number' && Number.isInteger(stored) && stored > 0
|
|
159
|
+
? Math.min(stored, 10_000_000) : 0;
|
|
160
|
+
return { ...card, inputHidden: Math.max(card.inputHidden, storedHidden) };
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Emergency kill switch (`ARAGON_NO_INVOCATION_CARDS=1`): every command and the App fall
|
|
164
|
+
* back to the pre-card behaviour. Read at call time, not import time, so a test (or a
|
|
165
|
+
* wrapper script) that sets the variable after load is honoured.
|
|
166
|
+
*/
|
|
167
|
+
export function invocationCardsEnabled(env = process.env) {
|
|
168
|
+
return env.ARAGON_NO_INVOCATION_CARDS !== '1';
|
|
169
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { InvocationCardInput } from './invocation-card.js';
|
|
1
2
|
/** Explicit TUI task boundary; omission retains legacy programmatic behavior. */
|
|
2
3
|
export interface PromptOptions {
|
|
3
4
|
todoPolicy?: 'new-task' | 'continue';
|
|
@@ -5,6 +6,11 @@ export interface PromptOptions {
|
|
|
5
6
|
/** History recording is independent of whether this message continues a plan. */
|
|
6
7
|
export interface SubmitMessageOptions extends PromptOptions {
|
|
7
8
|
userInitiated?: boolean;
|
|
9
|
+
/**
|
|
10
|
+
* Raw material for the transcript card. `submitMessage` builds the card exactly once
|
|
11
|
+
* (redact -> normalise -> limit); the payload sent to the model is unaffected.
|
|
12
|
+
*/
|
|
13
|
+
invocation?: InvocationCardInput;
|
|
8
14
|
}
|
|
9
15
|
/** Startup outcome only; engine events still describe the result of the run. */
|
|
10
16
|
export type PromptOutcome = {
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import type { Entry } from './reducer.js';
|
|
2
|
+
import type { InvocationCard } from './invocation-card.js';
|
|
2
3
|
/** Full user text retained until Core acknowledges its opaque ID. */
|
|
3
4
|
export interface PendingSteering {
|
|
4
5
|
readonly queueId: string;
|
|
5
6
|
readonly text: string;
|
|
7
|
+
/** Carried so the `queued -> user` rewrite keeps the card (tui-invocation-cards). */
|
|
8
|
+
readonly invocation?: InvocationCard;
|
|
6
9
|
}
|
|
7
10
|
/** Hide only active pending entries, using the same projection for measurement and paint. */
|
|
8
11
|
export declare function selectTranscriptEntries(entries: Entry[], pending: readonly PendingSteering[]): Entry[];
|
|
@@ -13,7 +13,10 @@ export function reconcileSteeringReceipt(options) {
|
|
|
13
13
|
if (!ids.size)
|
|
14
14
|
return { entries: options.entries, pending: [...options.pending], changed: false };
|
|
15
15
|
const entries = mergePendingEntries(options.entries, options.pending).map((entry) => entry.kind === 'queued' && ids.has(entry.queueId ?? '')
|
|
16
|
-
? {
|
|
16
|
+
? {
|
|
17
|
+
id: entry.id, kind: 'user', text: entry.text,
|
|
18
|
+
...(entry.invocation ? { invocation: entry.invocation } : {}),
|
|
19
|
+
} : entry);
|
|
17
20
|
return { entries, pending: options.pending.filter((item) => !ids.has(item.queueId)), changed: true };
|
|
18
21
|
}
|
|
19
22
|
/** Supplement trimmed/cleared history for save and exit without deduplicating text. */
|
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';
|
|
@@ -47,6 +48,7 @@ export type Entry = {
|
|
|
47
48
|
id: string;
|
|
48
49
|
kind: 'user';
|
|
49
50
|
text: string;
|
|
51
|
+
invocation?: InvocationCard;
|
|
50
52
|
}
|
|
51
53
|
/**
|
|
52
54
|
* A message submitted while a run was in flight (tui-shift-enter-copy-queue
|
|
@@ -63,6 +65,7 @@ export type Entry = {
|
|
|
63
65
|
kind: 'queued';
|
|
64
66
|
text: string;
|
|
65
67
|
queueId?: string;
|
|
68
|
+
invocation?: InvocationCard;
|
|
66
69
|
} | {
|
|
67
70
|
id: string;
|
|
68
71
|
kind: 'assistant';
|
|
@@ -507,6 +510,7 @@ export declare function initialViewState(seed?: ViewStateSeed): ViewState;
|
|
|
507
510
|
export type ViewAction = {
|
|
508
511
|
type: 'submit';
|
|
509
512
|
text: string;
|
|
513
|
+
invocation?: InvocationCard;
|
|
510
514
|
}
|
|
511
515
|
/**
|
|
512
516
|
* `controller.steer` was accepted; append the persistent queue marker
|
|
@@ -517,6 +521,7 @@ export type ViewAction = {
|
|
|
517
521
|
type: 'steerQueued';
|
|
518
522
|
queueId: string;
|
|
519
523
|
text: string;
|
|
524
|
+
invocation?: InvocationCard;
|
|
520
525
|
} | {
|
|
521
526
|
type: 'steeringAccepted';
|
|
522
527
|
ids: readonly string[];
|
package/dist/agent/reducer.js
CHANGED
|
@@ -484,7 +484,10 @@ export function viewReducer(state, action) {
|
|
|
484
484
|
switch (action.type) {
|
|
485
485
|
case 'submit': {
|
|
486
486
|
const { id, seq } = nextId(state);
|
|
487
|
-
const entry = {
|
|
487
|
+
const entry = {
|
|
488
|
+
id, kind: 'user', text: action.text,
|
|
489
|
+
...(action.invocation ? { invocation: action.invocation } : {}),
|
|
490
|
+
};
|
|
488
491
|
return {
|
|
489
492
|
...state,
|
|
490
493
|
seq,
|
|
@@ -507,7 +510,10 @@ export function viewReducer(state, action) {
|
|
|
507
510
|
if (state.seenSteeringIds.has(action.queueId))
|
|
508
511
|
return state;
|
|
509
512
|
const { id, seq } = nextId(state);
|
|
510
|
-
const entry = {
|
|
513
|
+
const entry = {
|
|
514
|
+
id, kind: 'queued', queueId: action.queueId, text: action.text,
|
|
515
|
+
...(action.invocation ? { invocation: action.invocation } : {}),
|
|
516
|
+
};
|
|
511
517
|
// NOTHING ELSE RESETS. This is not a new turn -- `turnProduced`,
|
|
512
518
|
// `errorNoticed` and `aborted` describe the run that is still in
|
|
513
519
|
// flight, and clearing them here would let the next `runEnd` failure
|
|
@@ -515,7 +521,10 @@ export function viewReducer(state, action) {
|
|
|
515
521
|
return {
|
|
516
522
|
...state, seq, ...appendEntry(state, state.entries, entry),
|
|
517
523
|
seenSteeringIds: new Set([...state.seenSteeringIds, action.queueId]),
|
|
518
|
-
pendingSteering: [...state.pendingSteering, {
|
|
524
|
+
pendingSteering: [...state.pendingSteering, {
|
|
525
|
+
queueId: action.queueId, text: action.text,
|
|
526
|
+
...(action.invocation ? { invocation: action.invocation } : {}),
|
|
527
|
+
}],
|
|
519
528
|
};
|
|
520
529
|
}
|
|
521
530
|
case 'steeringAccepted': {
|
|
@@ -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
|
}
|