@aragon-agent/cli 0.6.39 → 0.6.40
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 +2589 -2523
- package/README.md +3706 -3614
- package/dist/agent/controller.d.ts +103 -9
- package/dist/agent/controller.js +196 -19
- package/dist/agent/model-profile-settings.js +12 -1
- package/dist/agent/plan-prompt.d.ts +1 -3
- package/dist/agent/plan-prompt.js +13 -7
- package/dist/agent/reducer.d.ts +29 -1
- package/dist/agent/reducer.js +34 -6
- package/dist/agent/skill-triage-prompt.d.ts +42 -0
- package/dist/agent/skill-triage-prompt.js +74 -0
- package/dist/agent/system-prompt.d.ts +14 -2
- package/dist/agent/system-prompt.js +7 -4
- package/dist/cli.js +21 -5
- package/dist/commands/builtins.js +12 -3
- package/dist/commands/skills.js +86 -1
- package/dist/commands/subagent-command.d.ts +25 -0
- package/dist/commands/subagent-command.js +242 -0
- package/dist/compaction/command.js +16 -3
- package/dist/compaction/compactor.d.ts +25 -0
- package/dist/compaction/compactor.js +111 -10
- package/dist/compaction/digest.d.ts +24 -1
- package/dist/compaction/digest.js +139 -17
- package/dist/compaction/limits.d.ts +21 -0
- package/dist/compaction/limits.js +21 -0
- package/dist/compaction/reason-text.d.ts +36 -0
- package/dist/compaction/reason-text.js +78 -0
- package/dist/compaction/types.d.ts +82 -14
- package/dist/compaction/wiring.d.ts +13 -8
- package/dist/compaction/wiring.js +1 -0
- package/dist/config/load.js +50 -3
- package/dist/config/schema.d.ts +82 -6
- package/dist/config/schema.js +107 -9
- package/dist/config/store.js +7 -2
- package/dist/proc/log-mirror.d.ts +21 -4
- package/dist/proc/log-mirror.js +29 -7
- package/dist/skills/service.d.ts +41 -0
- package/dist/skills/service.js +62 -0
- package/dist/skills/triage.d.ts +194 -0
- package/dist/skills/triage.js +393 -0
- package/dist/team/forced-dispatch.d.ts +95 -0
- package/dist/team/forced-dispatch.js +208 -0
- package/dist/team/limits.d.ts +21 -1
- package/dist/team/limits.js +21 -1
- package/dist/team/normalize.d.ts +15 -0
- package/dist/team/normalize.js +21 -4
- package/dist/team/prompt.d.ts +6 -3
- package/dist/team/prompt.js +9 -5
- package/dist/team/task-tool.js +11 -7
- package/dist/tools/human-input.d.ts +35 -2
- package/dist/tools/human-input.js +38 -0
- package/dist/tools/plan-tools.d.ts +20 -12
- package/dist/tools/plan-tools.js +24 -17
- package/dist/ui/App.js +14 -1
- package/dist/ui/StatusBar.d.ts +1 -0
- package/dist/ui/StatusBar.js +1 -0
- package/dist/ui/Transcript.js +1 -1
- package/dist/ui/entries/CompactionCard.d.ts +11 -1
- package/dist/ui/entries/CompactionCard.js +65 -4
- package/dist/ui/layout/status-layout.d.ts +10 -0
- package/dist/ui/layout/status-layout.js +18 -1
- package/dist/ui/overlays/SettingsScreen.js +29 -5
- package/dist/ui/overlays/TeamFanoutPage.d.ts +35 -0
- package/dist/ui/overlays/TeamFanoutPage.js +78 -0
- package/dist/ui/overlays/model-profile-state.d.ts +19 -1
- package/dist/ui/overlays/model-profile-state.js +12 -0
- package/dist/ui/overlays/team-fanout-state.d.ts +73 -0
- package/dist/ui/overlays/team-fanout-state.js +97 -0
- package/package.json +2 -2
package/dist/agent/reducer.js
CHANGED
|
@@ -1134,12 +1134,11 @@ export function viewReducer(state, action) {
|
|
|
1134
1134
|
case 'fastTier':
|
|
1135
1135
|
return { ...state, fast: action.snapshot };
|
|
1136
1136
|
// --- Context compaction (context-auto-compaction §5.5) ----------------
|
|
1137
|
-
//
|
|
1138
|
-
//
|
|
1139
|
-
//
|
|
1140
|
-
//
|
|
1141
|
-
//
|
|
1142
|
-
// fired; `/compact status` and the diagnostic log are where the attempts are.
|
|
1137
|
+
// THE TRANSCRIPT NOW CARRIES ONE CARD PER ATTEMPT (context-compaction-never-
|
|
1138
|
+
// fires C). Before this change a declined `pressure` checkpoint dispatched
|
|
1139
|
+
// NOTHING, which is what made "95 % and nothing happens" unreportable: the
|
|
1140
|
+
// feature was trying, failing and saying nothing. The flood the old rule
|
|
1141
|
+
// feared is bounded by `maxPerRun` / `stuckLimit` in `compaction/limits.ts`.
|
|
1143
1142
|
case 'compactionStart': {
|
|
1144
1143
|
const { id, seq } = nextId(state);
|
|
1145
1144
|
let entries = state.entries;
|
|
@@ -1189,6 +1188,7 @@ export function viewReducer(state, action) {
|
|
|
1189
1188
|
tokensAfter: 0,
|
|
1190
1189
|
model: action.model,
|
|
1191
1190
|
startedAt: Date.now(),
|
|
1191
|
+
phase: 'planning',
|
|
1192
1192
|
live: true,
|
|
1193
1193
|
};
|
|
1194
1194
|
return {
|
|
@@ -1217,6 +1217,16 @@ export function viewReducer(state, action) {
|
|
|
1217
1217
|
...(record.tailRelief ? { tailRelief: record.tailRelief } : {}),
|
|
1218
1218
|
...(record.decision ? { decision: record.decision } : {}),
|
|
1219
1219
|
...(record.memoryVersion ? { memoryVersion: record.memoryVersion } : {}),
|
|
1220
|
+
...(record.keepRelaxed ? { keepRelaxed: record.keepRelaxed } : {}),
|
|
1221
|
+
...(record.retainedTurns !== undefined
|
|
1222
|
+
? { retainedTurns: record.retainedTurns } : {}),
|
|
1223
|
+
...(record.retainedMessages !== undefined
|
|
1224
|
+
? { retainedMessages: record.retainedMessages } : {}),
|
|
1225
|
+
...(record.retainedMessages !== undefined
|
|
1226
|
+
? { retainedMessages: record.retainedMessages } : {}),
|
|
1227
|
+
...(record.keepRecentTurnsUsed !== undefined
|
|
1228
|
+
? { keepRecentTurnsUsed: record.keepRecentTurnsUsed } : {}),
|
|
1229
|
+
...(record.elidedMessages ? { elidedMessages: record.elidedMessages } : {}),
|
|
1220
1230
|
model: record.model,
|
|
1221
1231
|
durationMs: record.durationMs,
|
|
1222
1232
|
// SETTLED. `Transcript`'s boundary is monotonic, so a card that
|
|
@@ -1253,12 +1263,30 @@ export function viewReducer(state, action) {
|
|
|
1253
1263
|
...(record.tailRelief ? { tailRelief: record.tailRelief } : {}),
|
|
1254
1264
|
...(record.decision ? { decision: record.decision } : {}),
|
|
1255
1265
|
...(record.memoryVersion ? { memoryVersion: record.memoryVersion } : {}),
|
|
1266
|
+
...(record.keepRelaxed ? { keepRelaxed: record.keepRelaxed } : {}),
|
|
1267
|
+
...(record.retainedTurns !== undefined ? { retainedTurns: record.retainedTurns } : {}),
|
|
1268
|
+
...(record.keepRecentTurnsUsed !== undefined
|
|
1269
|
+
? { keepRecentTurnsUsed: record.keepRecentTurnsUsed } : {}),
|
|
1270
|
+
...(record.elidedMessages ? { elidedMessages: record.elidedMessages } : {}),
|
|
1256
1271
|
model: record.model,
|
|
1257
1272
|
durationMs: record.durationMs,
|
|
1258
1273
|
live: false,
|
|
1259
1274
|
};
|
|
1260
1275
|
return { ...state, seq, ...appendEntry(state, state.entries, entry) };
|
|
1261
1276
|
}
|
|
1277
|
+
case 'compactionPhase': {
|
|
1278
|
+
// THE LIVE CARD'S STAGE, AND NOTHING ELSE. It must not open a card (a phase
|
|
1279
|
+
// can arrive for an entry that a fast settle already closed) and must not
|
|
1280
|
+
// resurrect a settled one: `live` is left untouched, so a late phase on a
|
|
1281
|
+
// settled card changes nothing the renderer shows.
|
|
1282
|
+
if (!state.compactionEntryId)
|
|
1283
|
+
return state;
|
|
1284
|
+
const phase = action.phase;
|
|
1285
|
+
return {
|
|
1286
|
+
...state,
|
|
1287
|
+
entries: mapEntry(state.entries, state.compactionEntryId, (e) => e.kind === 'compaction' && e.live ? { ...e, phase } : e),
|
|
1288
|
+
};
|
|
1289
|
+
}
|
|
1262
1290
|
case 'compactionUsage':
|
|
1263
1291
|
// `usageTotal` AND NOTHING ELSE, exactly as `teamUsage` and `fastUsage`. In
|
|
1264
1292
|
// particular NOT `context`: that gauge shows the LEAD's context
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `<skill_triage>` system-prompt block (skill-triage-and-unbounded-ask §4.2.3).
|
|
3
|
+
*
|
|
4
|
+
* WHY A PROTOCOL BLOCK AT ALL. The capability this feature fixes already existed:
|
|
5
|
+
* the Level 1 catalog is in every prompt, `skill(name)` loads a body, and
|
|
6
|
+
* `README.md` has claimed since the Skills feature landed that "the model decides
|
|
7
|
+
* on its own whether a task matches a skill". What was missing is any instruction
|
|
8
|
+
* that the decision has to HAPPEN, before the work rather than after it. The
|
|
9
|
+
* catalog's own opening paragraph is a STATEMENT ("if a task matches one, call the
|
|
10
|
+
* skill tool"), not a step in an order of operations, and a statement the model
|
|
11
|
+
* has read forty times is the one it skims.
|
|
12
|
+
*
|
|
13
|
+
* WHY IT IS THIS SHORT. The protocol is paid for on EVERY LLM request: the system
|
|
14
|
+
* prompt is re-sent each time and nothing in this repository uses provider-side
|
|
15
|
+
* prefix caching. An earlier draft carried a 1.9 KB version whose second half
|
|
16
|
+
* repeated the disclosure ladder that `<available_skills>` already states three
|
|
17
|
+
* lines above it. Repeating a rule is not emphasis, it is noise — and noise about
|
|
18
|
+
* a rule teaches the model that these blocks are skimmable. Measured size of the
|
|
19
|
+
* block below is 1273 bytes; `skill-triage-prompt.test.ts` asserts the ceiling of
|
|
20
|
+
* 1500 so an edit has 227 bytes of room, not unlimited scope.
|
|
21
|
+
*
|
|
22
|
+
* ASCII ONLY — `agent/` is inside the glyph scanner's scope.
|
|
23
|
+
*/
|
|
24
|
+
import type { SkillCandidate } from '../skills/triage.js';
|
|
25
|
+
/** Bump when the wording changes, so a transcript can be dated. */
|
|
26
|
+
export declare const SKILL_TRIAGE_BLOCK_VERSION = "v1-2026-10-26";
|
|
27
|
+
export interface SkillTriageBlockOptions {
|
|
28
|
+
/** This turn's candidates, best first. Empty is normal and renders no data block. */
|
|
29
|
+
candidates: SkillCandidate[];
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Protocol first, candidates second — in that order, always.
|
|
33
|
+
*
|
|
34
|
+
* The order is the meaning: the protocol says what to do, the data block says with
|
|
35
|
+
* what. Reversing them would put a list of suggestions above the rules for reading
|
|
36
|
+
* it, which is how a suggestion turns into an instruction.
|
|
37
|
+
*/
|
|
38
|
+
export declare function buildSkillTriageBlock(options: SkillTriageBlockOptions): string;
|
|
39
|
+
/** Byte size of the protocol half, for the budget assertion in the tests. */
|
|
40
|
+
export declare function skillTriageProtocolBytes(): number;
|
|
41
|
+
/** The ceiling the protocol half must stay under, in bytes. */
|
|
42
|
+
export declare const SKILL_TRIAGE_PROTOCOL_MAX_BYTES: number;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `<skill_triage>` system-prompt block (skill-triage-and-unbounded-ask §4.2.3).
|
|
3
|
+
*
|
|
4
|
+
* WHY A PROTOCOL BLOCK AT ALL. The capability this feature fixes already existed:
|
|
5
|
+
* the Level 1 catalog is in every prompt, `skill(name)` loads a body, and
|
|
6
|
+
* `README.md` has claimed since the Skills feature landed that "the model decides
|
|
7
|
+
* on its own whether a task matches a skill". What was missing is any instruction
|
|
8
|
+
* that the decision has to HAPPEN, before the work rather than after it. The
|
|
9
|
+
* catalog's own opening paragraph is a STATEMENT ("if a task matches one, call the
|
|
10
|
+
* skill tool"), not a step in an order of operations, and a statement the model
|
|
11
|
+
* has read forty times is the one it skims.
|
|
12
|
+
*
|
|
13
|
+
* WHY IT IS THIS SHORT. The protocol is paid for on EVERY LLM request: the system
|
|
14
|
+
* prompt is re-sent each time and nothing in this repository uses provider-side
|
|
15
|
+
* prefix caching. An earlier draft carried a 1.9 KB version whose second half
|
|
16
|
+
* repeated the disclosure ladder that `<available_skills>` already states three
|
|
17
|
+
* lines above it. Repeating a rule is not emphasis, it is noise — and noise about
|
|
18
|
+
* a rule teaches the model that these blocks are skimmable. Measured size of the
|
|
19
|
+
* block below is 1273 bytes; `skill-triage-prompt.test.ts` asserts the ceiling of
|
|
20
|
+
* 1500 so an edit has 227 bytes of room, not unlimited scope.
|
|
21
|
+
*
|
|
22
|
+
* ASCII ONLY — `agent/` is inside the glyph scanner's scope.
|
|
23
|
+
*/
|
|
24
|
+
import { renderSkillCandidates, SKILL_TRIAGE_LIMITS } from '../skills/triage.js';
|
|
25
|
+
/** Bump when the wording changes, so a transcript can be dated. */
|
|
26
|
+
export const SKILL_TRIAGE_BLOCK_VERSION = 'v1-2026-10-26';
|
|
27
|
+
/**
|
|
28
|
+
* The protocol text. Kept as a literal array so a diff of the prompt is a diff of
|
|
29
|
+
* the source, and so the grader/test can see line boundaries.
|
|
30
|
+
*/
|
|
31
|
+
const PROTOCOL_LINES = [
|
|
32
|
+
'<skill_triage>',
|
|
33
|
+
`Version: ${SKILL_TRIAGE_BLOCK_VERSION}`,
|
|
34
|
+
'',
|
|
35
|
+
'Triage every task that touches the workspace: AFTER the project guidance and the',
|
|
36
|
+
'directory listing, BEFORE you plan or change anything. A user-typed /<skill-name>',
|
|
37
|
+
'is an override, not the only way in.',
|
|
38
|
+
'',
|
|
39
|
+
'1. Read <available_skills> above, and <skill_candidates> when present. A candidate',
|
|
40
|
+
' is a computed guess about your message, not a verdict - check its one-line',
|
|
41
|
+
' description before trusting it.',
|
|
42
|
+
'2. If the catalog says some skills were not shown, or nothing listed plainly',
|
|
43
|
+
' covers the task, call skill_find(query) once before concluding there is no',
|
|
44
|
+
' skill.',
|
|
45
|
+
'3. If a skill covers the task, call skill(name="...") BEFORE the first source read',
|
|
46
|
+
' that would change code, then follow it. Say why in one line, not a paragraph.',
|
|
47
|
+
'4. If nothing covers the task, say so in one clause and continue. Do not load a',
|
|
48
|
+
' skill to look diligent, and never load several "to be safe".',
|
|
49
|
+
'',
|
|
50
|
+
'A loaded skill is reference material, not permission to ignore the user, the',
|
|
51
|
+
'plan-mode rules, or the tools you actually have. If a skill narrows your tool set,',
|
|
52
|
+
'that is its allowed-tools declaration working: report it, do not work around it.',
|
|
53
|
+
'Never install or author a skill because a task looks like it needs one; that',
|
|
54
|
+
'decision belongs to the user.',
|
|
55
|
+
'</skill_triage>',
|
|
56
|
+
];
|
|
57
|
+
const PROTOCOL = PROTOCOL_LINES.join('\n');
|
|
58
|
+
/**
|
|
59
|
+
* Protocol first, candidates second — in that order, always.
|
|
60
|
+
*
|
|
61
|
+
* The order is the meaning: the protocol says what to do, the data block says with
|
|
62
|
+
* what. Reversing them would put a list of suggestions above the rules for reading
|
|
63
|
+
* it, which is how a suggestion turns into an instruction.
|
|
64
|
+
*/
|
|
65
|
+
export function buildSkillTriageBlock(options) {
|
|
66
|
+
const candidates = renderSkillCandidates(options.candidates);
|
|
67
|
+
return candidates.length > 0 ? `${PROTOCOL}\n${candidates}` : PROTOCOL;
|
|
68
|
+
}
|
|
69
|
+
/** Byte size of the protocol half, for the budget assertion in the tests. */
|
|
70
|
+
export function skillTriageProtocolBytes() {
|
|
71
|
+
return new TextEncoder().encode(PROTOCOL).length;
|
|
72
|
+
}
|
|
73
|
+
/** The ceiling the protocol half must stay under, in bytes. */
|
|
74
|
+
export const SKILL_TRIAGE_PROTOCOL_MAX_BYTES = SKILL_TRIAGE_LIMITS.maxProtocolBytes;
|
|
@@ -34,8 +34,20 @@ export interface SystemPromptParams {
|
|
|
34
34
|
* plan as its final message instead of calling tools it does not have.
|
|
35
35
|
*/
|
|
36
36
|
planInteractive?: boolean;
|
|
37
|
-
/**
|
|
38
|
-
|
|
37
|
+
/**
|
|
38
|
+
* The rendered `<skill_triage>` block plus (when there are candidates) the
|
|
39
|
+
* `<skill_candidates>` block, or `''`.
|
|
40
|
+
*
|
|
41
|
+
* Spliced conditionally exactly as `skillsBlock` and the plan block are, which
|
|
42
|
+
* is what keeps invariant I-T1 true: `skills.triage: false`, a zero-skill
|
|
43
|
+
* install and `--no-skills` all produce the pre-feature prompt BYTE FOR BYTE.
|
|
44
|
+
*
|
|
45
|
+
* POSITION IS THE SEMANTICS. It goes immediately AFTER `skillsBlock`, because
|
|
46
|
+
* its first instruction refers to `<available_skills> above`, and a block
|
|
47
|
+
* separated from what it points at reads as a dangling reference. It also goes
|
|
48
|
+
* BEFORE the plan block, whose first step defers to it.
|
|
49
|
+
*/
|
|
50
|
+
skillTriageBlock?: string;
|
|
39
51
|
/**
|
|
40
52
|
* The rendered `<team_mode>` block for a LEAD with team mode on, or `''`.
|
|
41
53
|
*
|
|
@@ -33,11 +33,11 @@ export function buildSystemPrompt(params) {
|
|
|
33
33
|
const skillsBlock = params.skillsBlock ?? '';
|
|
34
34
|
const hasSkills = skillsBlock.length > 0;
|
|
35
35
|
const planBlock = params.agentMode === 'plan'
|
|
36
|
-
? buildPlanModeBlock({
|
|
37
|
-
interactive: params.planInteractive !== false,
|
|
38
|
-
maxAskRounds: params.planMaxAskRounds ?? 4,
|
|
39
|
-
})
|
|
36
|
+
? buildPlanModeBlock({ interactive: params.planInteractive !== false })
|
|
40
37
|
: '';
|
|
38
|
+
// `''` when the caller has no candidates or the feature is off — which is what
|
|
39
|
+
// keeps this block's absence byte-identical to the pre-feature prompt (I-T1).
|
|
40
|
+
const skillTriageBlock = params.skillTriageBlock ?? '';
|
|
41
41
|
const teamBlock = params.teamBlock ?? '';
|
|
42
42
|
const subagentBlock = params.subagentBlock ?? '';
|
|
43
43
|
const todoBlock = params.todoBlock ?? '';
|
|
@@ -61,6 +61,9 @@ export function buildSystemPrompt(params) {
|
|
|
61
61
|
'',
|
|
62
62
|
buildProjectGuidanceBlock(),
|
|
63
63
|
...(hasSkills ? ['', skillsBlock] : []),
|
|
64
|
+
// AFTER <available_skills> (it refers to it as "above") and BEFORE <plan_mode>
|
|
65
|
+
// (whose step 1 defers to it). Conditional like every other optional block.
|
|
66
|
+
...(skillTriageBlock ? ['', skillTriageBlock] : []),
|
|
64
67
|
...(planBlock ? ['', planBlock] : []),
|
|
65
68
|
...(teamBlock ? ['', teamBlock] : []),
|
|
66
69
|
...(subagentBlock ? ['', subagentBlock] : []),
|
package/dist/cli.js
CHANGED
|
@@ -29,7 +29,7 @@ import { COMPACTION_CONFIG_SET_KEYS, FAST_CONFIG_SET_KEYS, LOG_CONFIG_SET_KEYS,
|
|
|
29
29
|
import { attachAgentEvents, attachTeamEvents, installLogging, setScreenRestore, setSignalTerminator, } from './logging/install.js';
|
|
30
30
|
import { getLogger } from './logging/logger.js';
|
|
31
31
|
import { runLogsCommand } from './logging/cli-commands.js';
|
|
32
|
-
import { ADAPTER_PROVIDERS, clampDensity, clampTheme, clampThinkingLevel,
|
|
32
|
+
import { ADAPTER_PROVIDERS, clampDensity, clampTheme, clampThinkingLevel, clampHumanTimeout, clampSkillsIntegrity, clampSkillsToolPolicy, clampContextWindow, clampMaxRenderInterval, clampScrollMode, clampScrollResumeMs, clampTranscriptRetain, clampTranscriptWindow, coercePositiveInt, isAdapterProvider, isAutoToken, parseMaxTokensInput, RETIRED_CONFIG_KEYS, DEFAULT_MAX_RENDER_INTERVAL_MS, DEFAULT_MAX_TOKENS, DEFAULT_PLAN_HUMAN_TIMEOUT_MS, DEFAULT_SCROLL_MODE, DEFAULT_SCROLL_RESUME_MS, DEFAULT_SKILLS_CONFIG, DEFAULT_TRANSCRIPT_RETAIN, DEFAULT_TRANSCRIPT_WINDOW, } from './config/schema.js';
|
|
33
33
|
import { makeGetApiKey } from './config/load.js';
|
|
34
34
|
import { AgentController, DENY_ALL_APPROVAL } from './agent/controller.js';
|
|
35
35
|
import { runSkillsCommand } from './skills/cli-commands.js';
|
|
@@ -1559,7 +1559,6 @@ const CONFIG_SET_KEYS = new Set([
|
|
|
1559
1559
|
'scrollResumeMs',
|
|
1560
1560
|
'historyEnabled',
|
|
1561
1561
|
'planModeDefault',
|
|
1562
|
-
'planModeMaxAskRounds',
|
|
1563
1562
|
'planModeHumanTimeoutMs',
|
|
1564
1563
|
// Dotted keys route into the nested `skills` object; see the switch below.
|
|
1565
1564
|
'skills.enabled',
|
|
@@ -1569,6 +1568,7 @@ const CONFIG_SET_KEYS = new Set([
|
|
|
1569
1568
|
'skills.integrity',
|
|
1570
1569
|
'skills.usageTracking',
|
|
1571
1570
|
'skills.toolPolicy',
|
|
1571
|
+
'skills.triage',
|
|
1572
1572
|
// `log.*` keys are handled by `applyLogConfigSet` below, NOT by the switch:
|
|
1573
1573
|
// that switch is already past this package's complexity ceiling.
|
|
1574
1574
|
...LOG_CONFIG_SET_KEYS,
|
|
@@ -1660,6 +1660,18 @@ function runConfigSet(key, value) {
|
|
|
1660
1660
|
}
|
|
1661
1661
|
}
|
|
1662
1662
|
function runConfigSetValue(key, value) {
|
|
1663
|
+
// RETIRED KEYS ARE REFUSED BY NAME, BEFORE THE UNKNOWN-KEY CHECK. Falling
|
|
1664
|
+
// through to `Unknown config key` would be technically true and practically
|
|
1665
|
+
// useless: a script that has run `config set planModeMaxAskRounds 5` for months
|
|
1666
|
+
// needs to be told what happened to it, not that it never existed. Exit 2 for
|
|
1667
|
+
// the same reason a bad value is 2 — the caller's argument is wrong, the file
|
|
1668
|
+
// is not.
|
|
1669
|
+
const retired = RETIRED_CONFIG_KEYS[key];
|
|
1670
|
+
if (retired !== undefined) {
|
|
1671
|
+
process.stderr.write(`"${key}" was removed: ${retired}\n`);
|
|
1672
|
+
process.exitCode = 2;
|
|
1673
|
+
return;
|
|
1674
|
+
}
|
|
1663
1675
|
if (!CONFIG_SET_KEYS.has(key)) {
|
|
1664
1676
|
process.stderr.write(`Unknown config key "${key}". Known keys: ${[...CONFIG_SET_KEYS].join(', ')}.\n`);
|
|
1665
1677
|
process.exitCode = 2;
|
|
@@ -1852,9 +1864,6 @@ function runConfigSetValue(key, value) {
|
|
|
1852
1864
|
case 'planModeDefault':
|
|
1853
1865
|
patch.planModeDefault = value === 'true' || value === '1';
|
|
1854
1866
|
break;
|
|
1855
|
-
case 'planModeMaxAskRounds':
|
|
1856
|
-
patch.planModeMaxAskRounds = clampAskRounds(value, DEFAULT_PLAN_MAX_ASK_ROUNDS);
|
|
1857
|
-
break;
|
|
1858
1867
|
case 'planModeHumanTimeoutMs':
|
|
1859
1868
|
patch.planModeHumanTimeoutMs = clampHumanTimeout(value, DEFAULT_PLAN_HUMAN_TIMEOUT_MS);
|
|
1860
1869
|
break;
|
|
@@ -1893,6 +1902,13 @@ function runConfigSetValue(key, value) {
|
|
|
1893
1902
|
toolPolicy: clampSkillsToolPolicy(value, DEFAULT_SKILLS_CONFIG.toolPolicy),
|
|
1894
1903
|
};
|
|
1895
1904
|
break;
|
|
1905
|
+
// The triage master switch. `false` restores the pre-feature prompt BYTE FOR
|
|
1906
|
+
// BYTE (invariant I-T1), which is the reason it is a key rather than a flag.
|
|
1907
|
+
case 'skills.triage':
|
|
1908
|
+
patch.skills = {
|
|
1909
|
+
triage: value === 'true' || value === '1',
|
|
1910
|
+
};
|
|
1911
|
+
break;
|
|
1896
1912
|
}
|
|
1897
1913
|
updatePersistedConfig(patch);
|
|
1898
1914
|
process.stdout.write(`Set ${key} = ${value}\n`);
|
|
@@ -27,6 +27,7 @@ import { registerQueueCommand } from './queue-command.js';
|
|
|
27
27
|
import { registerPromptCommands } from '../prompts/command.js';
|
|
28
28
|
import { registerKitCommand } from './kit-command.js';
|
|
29
29
|
import { registerAgentsCommand } from './agents-command.js';
|
|
30
|
+
import { registerSubagentCommand } from './subagent-command.js';
|
|
30
31
|
// THE DRAWER AND THE HEADER LABEL (tui-shortcut-drawer-and-task-summary §4.7):
|
|
31
32
|
// two single-purpose commands in their own modules, registered from here. Both are
|
|
32
33
|
// UNCONDITIONAL, even with `taskSummary.enabled=false`, because they are the only
|
|
@@ -412,8 +413,10 @@ const COMMANDS = [
|
|
|
412
413
|
if (arg === 'status') {
|
|
413
414
|
const s = ctx.controller.getPlanStatus();
|
|
414
415
|
const pending = s.pending ? ` (pending ${MODE_LABEL[s.pending]} after this run)` : '';
|
|
415
|
-
|
|
416
|
-
|
|
416
|
+
// NO ROUND COUNT. The old line ended with `Ask rounds used: 3/4.`, which
|
|
417
|
+
// told the user their agent was rationing questions - a limit that no
|
|
418
|
+
// longer exists. Nothing replaced it: "0 of unlimited" is noise.
|
|
419
|
+
ctx.notify('info', `Mode: ${MODE_LABEL[s.effective]}${pending}.`);
|
|
417
420
|
return;
|
|
418
421
|
}
|
|
419
422
|
if (arg.length > 0 && arg !== 'on' && arg !== 'off') {
|
|
@@ -1508,6 +1511,11 @@ export function registerBuiltinCommands(registry) {
|
|
|
1508
1511
|
// The read-only subagent board (tui-subagent-panel §6.4). Same shape again: the
|
|
1509
1512
|
// body is `commands/agents-command.ts` and the registration is this one line.
|
|
1510
1513
|
registerAgentsCommand(registry);
|
|
1514
|
+
// `/subagent` (subagent-fanout 3.5). REGISTERED UNCONDITIONALLY, like `/prompts`
|
|
1515
|
+
// and for the same reason: a `--no-team` session must still be able to ask the
|
|
1516
|
+
// command why it cannot force anything (I-7 / AC-15) - so registering it may
|
|
1517
|
+
// not read config, build a connection or send a request.
|
|
1518
|
+
registerSubagentCommand(registry);
|
|
1511
1519
|
// The keyboard drawer (`Ctrl+G` / `/keys`) and the task label (`/task`). Two
|
|
1512
1520
|
// registrations, no new mechanism: `/keys` sets the panel's pre-filter and opens
|
|
1513
1521
|
// the same overlay `Ctrl+G` opens, and `/task` reports what the header cell says.
|
|
@@ -1522,7 +1530,8 @@ export function registerBuiltinCommands(registry) {
|
|
|
1522
1530
|
* `registry.get(name)` instead, because that is the only check that stays
|
|
1523
1531
|
* correct when commands are registered dynamically (§7.1 / P1-2).
|
|
1524
1532
|
*/
|
|
1525
|
-
export const BUILTIN_COMMAND_NAMES = ['queue', 'prompts', 'agents', '
|
|
1533
|
+
export const BUILTIN_COMMAND_NAMES = ['queue', 'prompts', 'agents', 'subagent',
|
|
1534
|
+
'keys', 'task',
|
|
1526
1535
|
...COMMANDS.flatMap((c) => [
|
|
1527
1536
|
c.name,
|
|
1528
1537
|
...(c.aliases ?? []),
|
package/dist/commands/skills.js
CHANGED
|
@@ -12,6 +12,7 @@ import { checkManifest } from '../skills/manifest.js';
|
|
|
12
12
|
import { updateAllSkills, updateSkill, isHardFailure } from '../skills/updater.js';
|
|
13
13
|
import { currentPlatform } from '../skills/node-host.js';
|
|
14
14
|
import { listUsage, resetUsage, getUsagePath } from '../skills/usage.js';
|
|
15
|
+
import { SKILL_TRIAGE_LIMITS } from '../skills/triage.js';
|
|
15
16
|
import { HOST_TOOL_NAMES, SKILL_TOOL_FLOOR } from '../tools/index.js';
|
|
16
17
|
import { SKILLS_TOOL_POLICY_MODES, isSkillsToolPolicyMode } from '../config/schema.js';
|
|
17
18
|
import { pickGlyphs } from '../ui/glyphs.js';
|
|
@@ -92,12 +93,96 @@ async function runSkills(ctx, service, version) {
|
|
|
92
93
|
return unloadFrames(ctx, service);
|
|
93
94
|
case 'usage':
|
|
94
95
|
return usageFromCommand(ctx, service, argument);
|
|
96
|
+
case 'triage':
|
|
97
|
+
return triageFromCommand(ctx, service, argument);
|
|
98
|
+
case 'route':
|
|
99
|
+
return routeFromCommand(ctx, service, argument);
|
|
95
100
|
default:
|
|
96
101
|
ctx.notify('warn', `Unknown subcommand "/skills ${sub}". Try: list, info, install, update, remove, enable, ` +
|
|
97
|
-
'disable, reload, create, trust, untrust, policy, unload, usage.');
|
|
102
|
+
'disable, reload, create, trust, untrust, policy, unload, usage, triage, route.');
|
|
98
103
|
}
|
|
99
104
|
}
|
|
100
105
|
// ---------------------------------------------------------------------------
|
|
106
|
+
// /skills triage · route (skill-triage-and-unbounded-ask §4.2.8)
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
/**
|
|
109
|
+
* `/skills triage [on|off]`.
|
|
110
|
+
*
|
|
111
|
+
* The report names the switch AND the last turn's result, because "on" alone does
|
|
112
|
+
* not answer the question a user actually has, which is "is it finding anything?".
|
|
113
|
+
* `lastSkillRouting` comes from the controller — it is per-turn session state, not
|
|
114
|
+
* skill-service state, and the service must not start owning a copy of it.
|
|
115
|
+
*/
|
|
116
|
+
function triageFromCommand(ctx, service, argument) {
|
|
117
|
+
const requested = argument.trim().toLowerCase();
|
|
118
|
+
if (requested.length === 0) {
|
|
119
|
+
const on = service.getConfig().triage === true;
|
|
120
|
+
const last = ctx.controller.getSkillRouting();
|
|
121
|
+
const summary = last === null
|
|
122
|
+
? 'No turn has been triaged yet.'
|
|
123
|
+
: last.candidates.length === 0
|
|
124
|
+
? `Last turn: no candidate (${last.eligible} eligible skill(s) scored).`
|
|
125
|
+
: `Last turn: ${last.candidates.length} candidate(s), best ${last.candidates[0].name} (${last.candidates[0].score}).`;
|
|
126
|
+
ctx.notify('info', `Skill triage: ${on ? 'on' : 'off'}. ${summary}`);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (requested !== 'on' && requested !== 'off') {
|
|
130
|
+
ctx.notify('warn', `Unknown argument "${argument}" - use /skills triage [on|off].`);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
const enabled = requested === 'on';
|
|
134
|
+
// `setTriageEnabled` persists AND fires `onChange`, which rebuilds the prompt in
|
|
135
|
+
// this session. Without that second half the switch would look applied and take
|
|
136
|
+
// effect only after a restart (AC-T23).
|
|
137
|
+
service.setTriageEnabled(enabled);
|
|
138
|
+
// A toast, not a transcript notice: flipping a switch is an acknowledgement, and
|
|
139
|
+
// `toast` is this package's channel for exactly that (see `toggleSkill`). The
|
|
140
|
+
// level still differs by direction, so "off" does not read as a win.
|
|
141
|
+
ctx.toast(enabled ? 'success' : 'info', `Skill triage: ${enabled ? 'on' : 'off'}. ` +
|
|
142
|
+
(enabled
|
|
143
|
+
? 'Each new task gets a short candidate list.'
|
|
144
|
+
: 'The system prompt no longer carries the triage block.'));
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* `/skills route [text]`.
|
|
148
|
+
*
|
|
149
|
+
* Two modes, one output shape. With text it routes that text; without text it
|
|
150
|
+
* replays the previous turn's real routing, which is the case a user is most often
|
|
151
|
+
* in — they just watched the model ignore a suggestion. The "never triaged" text
|
|
152
|
+
* names the form that works instead of leaving them to guess.
|
|
153
|
+
*/
|
|
154
|
+
function routeFromCommand(ctx, service, argument) {
|
|
155
|
+
const task = argument.trim();
|
|
156
|
+
if (task.length === 0) {
|
|
157
|
+
const last = ctx.controller.getSkillRouting();
|
|
158
|
+
if (last === null) {
|
|
159
|
+
ctx.notify('info', 'No turn has been triaged yet. Run /skills route <text> to try one.');
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
ctx.notify('info', renderRoutingReport(last));
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
// A fresh computation, NOT the cached one: `/skills route <text>` is how a user
|
|
166
|
+
// tests whether a phrasing would have matched.
|
|
167
|
+
ctx.notify('info', renderRoutingReport(service.route(task)));
|
|
168
|
+
}
|
|
169
|
+
/** One `info` notification, one line per candidate, ASCII only (`commands/`). */
|
|
170
|
+
function renderRoutingReport(report) {
|
|
171
|
+
if (report.candidates.length === 0) {
|
|
172
|
+
const close = report.topUnqualified === null
|
|
173
|
+
? ''
|
|
174
|
+
: ` Closest was ${report.topUnqualified.name} (${report.topUnqualified.score}); ` +
|
|
175
|
+
`the threshold is ${SKILL_TRIAGE_LIMITS.minScore}.`;
|
|
176
|
+
return (`No skill matched "${report.task}". The catalog has ${report.eligible} eligible skills.` +
|
|
177
|
+
close);
|
|
178
|
+
}
|
|
179
|
+
const lines = report.candidates.map((candidate) => {
|
|
180
|
+
const reasons = candidate.reasons.length > 0 ? candidate.reasons.join('+') : 'none';
|
|
181
|
+
return `${candidate.name} (${candidate.scope}) score ${candidate.score} [${reasons}]`;
|
|
182
|
+
});
|
|
183
|
+
return [`Routed "${report.task}" (${report.tokenCount} token(s)):`, ...lines].join('\n');
|
|
184
|
+
}
|
|
185
|
+
// ---------------------------------------------------------------------------
|
|
101
186
|
// /skills policy · unload · usage (§5.5 / §9)
|
|
102
187
|
// ---------------------------------------------------------------------------
|
|
103
188
|
/**
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/subagent` — force the next turn to fan out into a parallel team
|
|
3
|
+
* (subagent-fanout 3.5 / 5.2).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY: `src/commands/**` is inside `glyphs.test.ts::inScope`.
|
|
6
|
+
*
|
|
7
|
+
* WHAT THIS COMMAND IS AND IS NOT (D-8 / D-9 / D-10). It does not dispatch: it
|
|
8
|
+
* parses the user's intent, refuses honestly when forcing cannot work, prints
|
|
9
|
+
* ONE line saying what will happen next, and submits a normal user turn. The
|
|
10
|
+
* lead then calls `task` on the one dispatch path that already exists, so Esc,
|
|
11
|
+
* the watchdog pause, the report, spend accounting and the concurrency lock all
|
|
12
|
+
* keep working without a second implementation. The consequence the user must
|
|
13
|
+
* be told about is that forcing is INSTRUCTION-LEVEL: the notice says "the next
|
|
14
|
+
* turn dispatches them", never "N subagents started".
|
|
15
|
+
*
|
|
16
|
+
* THE GUARD ORDER IS A CONTRACT (R-P1-10 / R-P1-11): status -> capability ->
|
|
17
|
+
* parse. A `--no-team` session typing `/subagent -n x` is told the session has
|
|
18
|
+
* no team capability, because a syntax hint would send them to fix the one thing
|
|
19
|
+
* that is not wrong.
|
|
20
|
+
*/
|
|
21
|
+
import type { CommandContext, CommandRegistry } from './registry.js';
|
|
22
|
+
/** The command body; `registerSubagentCommand` is the only registration site. */
|
|
23
|
+
export declare function runSubagentCommand(ctx: CommandContext): void;
|
|
24
|
+
/** Registered from `builtins.ts` with one line, next to every other subsystem. */
|
|
25
|
+
export declare function registerSubagentCommand(registry: CommandRegistry): void;
|