@aragon-agent/cli 0.5.8 → 0.5.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/CHANGELOG.md +279 -0
  2. package/README.md +1429 -1163
  3. package/dist/agent/coalesce.d.ts +13 -0
  4. package/dist/agent/coalesce.js +50 -11
  5. package/dist/agent/controller.d.ts +141 -2
  6. package/dist/agent/controller.js +396 -4
  7. package/dist/agent/headless.d.ts +8 -0
  8. package/dist/agent/headless.js +40 -0
  9. package/dist/agent/reducer.d.ts +245 -3
  10. package/dist/agent/reducer.js +490 -17
  11. package/dist/agent/retry-view.d.ts +68 -0
  12. package/dist/agent/retry-view.js +95 -0
  13. package/dist/agent/system-prompt.d.ts +11 -0
  14. package/dist/agent/system-prompt.js +2 -0
  15. package/dist/cli.js +86 -2
  16. package/dist/commands/builtins.d.ts +1 -1
  17. package/dist/commands/builtins.js +308 -2
  18. package/dist/config/cli-commands.d.ts +53 -2
  19. package/dist/config/cli-commands.js +136 -3
  20. package/dist/config/env.d.ts +6 -3
  21. package/dist/config/env.js +103 -3
  22. package/dist/config/load.d.ts +49 -0
  23. package/dist/config/load.js +83 -1
  24. package/dist/config/schema.d.ts +212 -2
  25. package/dist/config/schema.js +207 -1
  26. package/dist/config/store.js +20 -1
  27. package/dist/fast/critique.d.ts +43 -0
  28. package/dist/fast/critique.js +62 -0
  29. package/dist/fast/digest.d.ts +42 -0
  30. package/dist/fast/digest.js +85 -0
  31. package/dist/fast/frames.d.ts +99 -0
  32. package/dist/fast/frames.js +180 -0
  33. package/dist/fast/limits.d.ts +98 -0
  34. package/dist/fast/limits.js +98 -0
  35. package/dist/fast/prompt.d.ts +72 -0
  36. package/dist/fast/prompt.js +94 -0
  37. package/dist/fast/resolve.d.ts +43 -0
  38. package/dist/fast/resolve.js +105 -0
  39. package/dist/fast/review-call.d.ts +99 -0
  40. package/dist/fast/review-call.js +130 -0
  41. package/dist/fast/reviewer.d.ts +248 -0
  42. package/dist/fast/reviewer.js +664 -0
  43. package/dist/fast/types.d.ts +128 -0
  44. package/dist/fast/types.js +14 -0
  45. package/dist/fast/wiring.d.ts +170 -0
  46. package/dist/fast/wiring.js +316 -0
  47. package/dist/logging/install.d.ts +15 -1
  48. package/dist/logging/install.js +37 -4
  49. package/dist/logging/logger.d.ts +1 -1
  50. package/dist/session/persist.js +57 -1
  51. package/dist/team/normalize.d.ts +26 -1
  52. package/dist/team/normalize.js +23 -3
  53. package/dist/team/prompt.d.ts +11 -0
  54. package/dist/team/prompt.js +10 -0
  55. package/dist/team/report.d.ts +18 -0
  56. package/dist/team/report.js +27 -1
  57. package/dist/team/runtime.d.ts +13 -2
  58. package/dist/team/runtime.js +33 -6
  59. package/dist/team/subagent.d.ts +19 -1
  60. package/dist/team/subagent.js +72 -10
  61. package/dist/team/task-tool.d.ts +26 -0
  62. package/dist/team/task-tool.js +48 -4
  63. package/dist/team/types.d.ts +80 -1
  64. package/dist/tools/bash-tool.js +19 -3
  65. package/dist/tools/diff.d.ts +25 -5
  66. package/dist/tools/diff.js +72 -41
  67. package/dist/tools/file-change-store.d.ts +34 -0
  68. package/dist/tools/file-change-store.js +57 -0
  69. package/dist/tools/fs-tools.d.ts +26 -0
  70. package/dist/tools/fs-tools.js +62 -5
  71. package/dist/tools/index.d.ts +31 -0
  72. package/dist/tools/index.js +7 -1
  73. package/dist/tools/patch.d.ts +115 -0
  74. package/dist/tools/patch.js +453 -0
  75. package/dist/tools/terminal-output.d.ts +107 -0
  76. package/dist/tools/terminal-output.js +198 -0
  77. package/dist/tools/tool-output-store.d.ts +68 -0
  78. package/dist/tools/tool-output-store.js +102 -0
  79. package/dist/ui/ActivityLine.d.ts +48 -0
  80. package/dist/ui/ActivityLine.js +22 -0
  81. package/dist/ui/App.d.ts +1 -4
  82. package/dist/ui/App.js +273 -37
  83. package/dist/ui/BottomStatusRow.d.ts +43 -0
  84. package/dist/ui/BottomStatusRow.js +14 -0
  85. package/dist/ui/Composer.d.ts +1 -6
  86. package/dist/ui/Composer.js +2 -2
  87. package/dist/ui/PromptInput.d.ts +1 -15
  88. package/dist/ui/PromptInput.js +1 -38
  89. package/dist/ui/StatusBar.d.ts +37 -0
  90. package/dist/ui/StatusBar.js +5 -3
  91. package/dist/ui/TeamPanel.d.ts +5 -4
  92. package/dist/ui/TeamPanel.js +1 -1
  93. package/dist/ui/TodoPanel.d.ts +5 -4
  94. package/dist/ui/Transcript.d.ts +26 -3
  95. package/dist/ui/Transcript.js +76 -10
  96. package/dist/ui/activity-phrases.d.ts +33 -0
  97. package/dist/ui/activity-phrases.js +50 -0
  98. package/dist/ui/entries/AssistantEntry.d.ts +20 -0
  99. package/dist/ui/entries/AssistantEntry.js +3 -2
  100. package/dist/ui/entries/DiffView.d.ts +67 -0
  101. package/dist/ui/entries/DiffView.js +103 -0
  102. package/dist/ui/entries/FastCard.d.ts +53 -0
  103. package/dist/ui/entries/FastCard.js +87 -0
  104. package/dist/ui/entries/RetryCard.d.ts +56 -0
  105. package/dist/ui/entries/RetryCard.js +60 -0
  106. package/dist/ui/entries/TeamCard.js +6 -1
  107. package/dist/ui/entries/ToolCard.d.ts +38 -1
  108. package/dist/ui/entries/ToolCard.js +68 -5
  109. package/dist/ui/glyphs.d.ts +9 -0
  110. package/dist/ui/glyphs.js +2 -0
  111. package/dist/ui/layout/AppShell.d.ts +13 -22
  112. package/dist/ui/layout/AppShell.js +3 -38
  113. package/dist/ui/layout/virtual-window.d.ts +1 -1
  114. package/dist/ui/layout/virtual-window.js +109 -5
  115. package/dist/ui/overlays/HelpOverlay.js +16 -2
  116. package/dist/ui/overlays/SettingsScreen.d.ts +82 -1
  117. package/dist/ui/overlays/SettingsScreen.js +147 -1
  118. package/dist/ui/transcript-text.js +27 -0
  119. package/dist/ui/use-wheel-routing.d.ts +16 -32
  120. package/dist/ui/use-wheel-routing.js +21 -38
  121. package/package.json +2 -2
  122. package/dist/ui/layout/regions.d.ts +0 -55
  123. package/dist/ui/layout/regions.js +0 -52
@@ -0,0 +1,180 @@
1
+ /**
2
+ * TurnFrameRing — the reviewer's bounded record of what the lead just did
3
+ * (fast-model-tier §3.5.1, extracted here by fast-model-tier-hardening W3).
4
+ *
5
+ * ASCII ONLY: `src/fast/**` is inside the glyph scanner's scope.
6
+ *
7
+ * THIS IS THE `data` HALF OF THE SPLIT, AND IT IS A LEAF ON PURPOSE. `reviewer.ts`
8
+ * was at 871 of the repo's 1 000-line cap before round 2 added a budget to it, so
9
+ * the ring and the transport moved out. The split line is drawn along data and
10
+ * transport BECAUSE BOTH ARE LEAVES: the protocol — the two drain guards and the
11
+ * call-cancellation stamping — stays whole in `reviewer.ts`, where a module
12
+ * boundary through it would reopen a P0 (§3.3 / AC-H11 / AC-H16).
13
+ *
14
+ * WHAT THIS CLASS OWNS: the open frame, the retired ring, the outstanding-call
15
+ * counter and the tool-argument map. WHAT IT DECIDES: nothing. It reports that a
16
+ * batch has emptied; the caller decides what that means.
17
+ */
18
+ import { FAST_LIMITS } from './limits.js';
19
+ // The one `unknown -> text` helper of this tree lives with the module that
20
+ // classifies errors. A second copy here would be a second answer to one question.
21
+ import { errorText } from './review-call.js';
22
+ /** The one argument a tool call is worth naming in a digest. */
23
+ function pickToolArg(args) {
24
+ const a = (args ?? {});
25
+ for (const key of ['path', 'pattern', 'command', 'query', 'label']) {
26
+ const value = a[key];
27
+ if (typeof value === 'string' && value.length > 0) {
28
+ return value.slice(0, FAST_LIMITS.toolArgChars);
29
+ }
30
+ }
31
+ return undefined;
32
+ }
33
+ export class TurnFrameRing {
34
+ deps;
35
+ retained = [];
36
+ open = null;
37
+ /**
38
+ * Tool calls still to run in the current batch.
39
+ *
40
+ * ASSIGNED at `turn_end`, NEVER CARRIED ACROSS TURNS (RV-7), and the
41
+ * difference is not cosmetic. Two paths end a batch without emitting a
42
+ * `tool_execution_end` for every call: checkpoint 2 skips the whole batch
43
+ * (`agent-loop.ts:199-207`) and an abort breaks out mid-batch (`:211`). A
44
+ * counter that only ever decremented would sit permanently above zero after
45
+ * either, and the injection window would never open again for the rest of the
46
+ * run. Re-assigning at each `turn_end` makes both cases self-heal on the next
47
+ * turn, which is correct: those batches genuinely had no window.
48
+ */
49
+ outstanding = 0;
50
+ completedTurns = 0;
51
+ /** Tool args captured at `tool_execution_start`, keyed by call id. */
52
+ toolArgs = new Map();
53
+ constructor(deps) {
54
+ this.deps = deps;
55
+ }
56
+ /** Completed turns of the CURRENT run — the cadence and staleness clock. */
57
+ get turns() {
58
+ return this.completedTurns;
59
+ }
60
+ /** Always 0 at the injection window; logged there as the assertion, recorded. */
61
+ get outstandingCalls() {
62
+ return this.outstanding;
63
+ }
64
+ /** The ring, oldest first. `buildReviewDigest` filters it for sealed frames. */
65
+ frames() {
66
+ return this.retained;
67
+ }
68
+ /** `agent_start`. */
69
+ reset() {
70
+ this.retained = [];
71
+ this.open = null;
72
+ this.outstanding = 0;
73
+ this.completedTurns = 0;
74
+ this.toolArgs.clear();
75
+ }
76
+ /** `agent_end` — the ring survives for a late answer; the open frame does not. */
77
+ endRun() {
78
+ this.open = null;
79
+ this.toolArgs.clear();
80
+ }
81
+ /** `turn_start`. */
82
+ beginTurn() {
83
+ this.open = {
84
+ index: this.completedTurns + 1,
85
+ textTail: '',
86
+ tools: [],
87
+ sealed: false,
88
+ };
89
+ }
90
+ /**
91
+ * The frame recorder.
92
+ *
93
+ * GATED ON `text_delta` AS AN ALLOW-TEST, NEVER A DENY-LIST (RV-10 / R-16).
94
+ * `agent-loop.ts:170` forwards EVERY stream event through `message_update` -
95
+ * `text_delta`, `thinking_delta`, `tool_call_delta`, `done`, and (as of
96
+ * `llm-api-retry-backoff`) `retry_scheduled` / `retry_attempt` - so this is a
97
+ * synchronous listener on the hottest path in the package. A deny-list would
98
+ * have silently started recording the two new members the moment they shipped.
99
+ *
100
+ * The remaining conditions are economic rather than structural: after the last
101
+ * permitted review of a run there is nothing left that can consume a frame.
102
+ */
103
+ recordDelta(streamEvent) {
104
+ if (streamEvent.type === 'error') {
105
+ if (this.open)
106
+ this.open.error = errorText(streamEvent.error);
107
+ return;
108
+ }
109
+ if (streamEvent.type !== 'text_delta')
110
+ return;
111
+ if (!this.open)
112
+ return;
113
+ if (!this.deps.canRecord())
114
+ return;
115
+ const delta = streamEvent.delta;
116
+ if (delta.length === 0)
117
+ return;
118
+ // O(1) per delta: one bounded concat and one bounded slice, the same rolling
119
+ // tail `subagent.ts:392` already uses for the activity line.
120
+ this.open.textTail = (this.open.textTail + delta).slice(-FAST_LIMITS.turnTailChars);
121
+ }
122
+ /** `tool_execution_start`. */
123
+ noteToolArgs(toolCallId, args) {
124
+ const arg = pickToolArg(args);
125
+ if (arg !== undefined)
126
+ this.toolArgs.set(toolCallId, arg);
127
+ }
128
+ /** `turn_end`. A turn with NO tool calls ends the run, so its frame seals here. */
129
+ endTurn(message) {
130
+ this.completedTurns += 1;
131
+ const calls = Array.isArray(message?.content)
132
+ ? message.content.filter((b) => b.type === 'tool_call').length
133
+ : 0;
134
+ this.outstanding = calls;
135
+ if (this.open)
136
+ this.open.index = this.completedTurns;
137
+ if (calls === 0)
138
+ this.seal();
139
+ }
140
+ /**
141
+ * `tool_execution_end`. Returns whether this call EMPTIED the batch — which is
142
+ * both the sealing point (§3.5.1) and the one injection window (§3.5.4). The
143
+ * ring reports the fact; `reviewer.ts` owns what follows from it.
144
+ */
145
+ endTool(toolCallId, toolName, isError, duration) {
146
+ if (this.open) {
147
+ const arg = this.toolArgs.get(toolCallId);
148
+ this.open.tools.push({
149
+ name: toolName,
150
+ ...(arg !== undefined ? { arg } : {}),
151
+ ...(isError ? { isError: true } : {}),
152
+ ms: duration,
153
+ });
154
+ }
155
+ this.toolArgs.delete(toolCallId);
156
+ if (this.outstanding > 0)
157
+ this.outstanding -= 1;
158
+ return this.outstanding === 0;
159
+ }
160
+ /**
161
+ * Seal the open frame and retire it into the ring.
162
+ *
163
+ * A REVIEW IS TRIGGERED ON SEALING, NOT AT `turn_end` (RV-2 / D-22).
164
+ * `turn_end` fires at `agent-loop.ts:188`, BEFORE any tool runs, while
165
+ * `TurnFrame.tools` carries `isError` and `ms` - fields that exist only once
166
+ * `tool_execution_end` has been emitted (`:249-256`). Triggering there meant
167
+ * the newest frame of every digest was structurally missing the single fact a
168
+ * reviewer most needs: whether the batch it is reviewing just failed.
169
+ */
170
+ seal() {
171
+ if (!this.open || this.open.sealed)
172
+ return;
173
+ this.open.sealed = true;
174
+ this.retained.push(this.open);
175
+ if (this.retained.length > FAST_LIMITS.maxFramesRetained) {
176
+ this.retained.splice(0, this.retained.length - FAST_LIMITS.maxFramesRetained);
177
+ }
178
+ this.open = null;
179
+ }
180
+ }
@@ -0,0 +1,98 @@
1
+ /**
2
+ * FAST_LIMITS — the single authority on every bound the fast tier enforces
3
+ * (fast-model-tier §5.5).
4
+ *
5
+ * ASCII ONLY: `src/fast/**` is inside the glyph scanner's scope
6
+ * (`glyphs.test.ts`), so no literal in this tree may hold a non-ASCII byte. The
7
+ * regex there is a hardcoded directory list, which is why adding this tree and
8
+ * adding `fast` to that list are the SAME change (C-4 / AC-27) - a scanner that
9
+ * silently stops scanning is worse than no scanner.
10
+ *
11
+ * TWO KINDS OF NUMBER, AND THEY ARE NOT INTERCHANGEABLE - the distinction
12
+ * `TEAM_LIMITS` and `TODO_LIMITS` both state:
13
+ *
14
+ * - The entries below are STRUCTURAL. They describe what a review call, a
15
+ * digest and a card can physically carry. A user has no business tuning them.
16
+ * - The ten `fast.*` keys in `config/schema.ts` are POLICY: which model, how
17
+ * often, how long a critique may be. Those are clamped, persisted and
18
+ * user-facing.
19
+ *
20
+ * `reviewMaxChars` therefore lives in the CONFIG and not here: it is the one
21
+ * number whose right value depends on how much of their own context the user is
22
+ * willing to spend on advice. `reviewOutputTokens` stays here because 512 tokens
23
+ * is a bound on a protocol, not a preference.
24
+ */
25
+ export declare const FAST_LIMITS: {
26
+ /**
27
+ * The flat input cost of one review (D-6 / R-2).
28
+ *
29
+ * THIS NUMBER IS THE WHOLE ECONOMIC ARGUMENT. The reviewer builds its own
30
+ * bounded frame ring and never reads `agent.state.messages`, which after four
31
+ * 60 KB file reads carries 240 KB of file bodies; shipping that to the "cheap"
32
+ * model would make a review cost MORE than the turn it is reviewing.
33
+ */
34
+ readonly digestMaxChars: 4000;
35
+ /** The user's request for the run, clamped. Without it the reviewer is asked
36
+ * whether work is on track without being told what the track is. */
37
+ readonly goalChars: 500;
38
+ /** Per-frame assistant-text tail. Rolling, clipped on every append. */
39
+ readonly turnTailChars: 600;
40
+ /** Per picked tool argument. Mirrors `TEAM_LIMITS.activityArgChars`. */
41
+ readonly toolArgChars: 120;
42
+ /** Frames kept per run. `>=` the max of `reviewContextTurns`, with slack. */
43
+ readonly maxFramesRetained: 12;
44
+ /** Hard cap on the review's own output. */
45
+ readonly reviewOutputTokens: 512;
46
+ /**
47
+ * Wall-clock budget for ONE review call INCLUDING RETRIES (D-H11).
48
+ *
49
+ * THE BUDGET HAS TWO TERMS AND ONLY ONE OF THEM SHRANK. Round 1 set 30 s as
50
+ * IF-3's fallback: `ProviderRegistry.complete()` routed through a retrying
51
+ * `stream()` with `maxRetries: 10`, so the budget covered
52
+ * `attempts x (call + backoff)`. Round 2 gives the reviewer its own
53
+ * `maxRetries: 1` registry (`wiring.ts`), which collapses that term to about
54
+ * one call plus a one-second backoff - but it does nothing at all to the
55
+ * other term, THE FAST MODEL'S OWN GENERATION LATENCY. The review asks for up
56
+ * to `reviewOutputTokens` from a `digestMaxChars` prompt, and the tier is by
57
+ * construction pointed at the cheapest endpoint the user has: the population
58
+ * where a 15-25 s completion is ordinary rather than pathological.
59
+ *
60
+ * SO THIS IS 20 000 AND NOT 12 000. A timeout IS a strike (see `runReview`),
61
+ * and `maxConsecutiveFailures` is 3, so a 12 s bound would convert "slow but
62
+ * healthy provider" into a tier that self-disables for the session and
63
+ * reports itself as MISCONFIGURED - the one register D-24 reserves for faults
64
+ * that never self-heal, spent on one that does. 20 s keeps a margin over a
65
+ * slow completion and still cuts a stuck review's occupancy of the
66
+ * single-flight slot by a third. Lower it only with a measured p99, which
67
+ * `manual-test.md` row 6 exists to produce.
68
+ */
69
+ readonly reviewTimeoutMs: 20000;
70
+ /** Reviews STARTED per run. A 40-turn run at cadence 5 would otherwise buy 8
71
+ * critiques the lead increasingly ignores. */
72
+ readonly maxReviewsPerRun: 6;
73
+ /** Consecutive NON-TRANSIENT failures before the reviewer self-disables. */
74
+ readonly maxConsecutiveFailures: 3;
75
+ /**
76
+ * A critique waiting for its injection window goes stale (RV-12).
77
+ *
78
+ * The window is not guaranteed to be prompt: one `task` dispatch is a single
79
+ * tool call that can run for `team.dispatchTimeoutMs`, so a critique triggered
80
+ * just before one arrives at the far side describing a state five children
81
+ * have since rewritten. Advice that is merely late is worse than no advice,
82
+ * because the lead cannot tell that it is late.
83
+ */
84
+ readonly pendingMaxAgeMs: 120000;
85
+ /** ... or is overtaken by the run itself, in turns. */
86
+ readonly pendingMaxTurnsBehind: 2;
87
+ /** Rows the transcript card renders before eliding. */
88
+ readonly cardTextRows: 4;
89
+ /** Below this many columns the status chip drops (D-20's treatment, not
90
+ * `agentMode`'s - the guaranteed reporting surface is `/fast status`). */
91
+ readonly statusCompactCols: 100;
92
+ };
93
+ /**
94
+ * Bumped whenever the wording of `<fast_tier>` or the review system prompt
95
+ * changes, so a behaviour report can be tied to a block revision with one grep -
96
+ * exactly what `TEAM_BLOCK_VERSION` and `TODO_BLOCK_VERSION` are for.
97
+ */
98
+ export declare const FAST_BLOCK_VERSION = "v1-2026-07";
@@ -0,0 +1,98 @@
1
+ /**
2
+ * FAST_LIMITS — the single authority on every bound the fast tier enforces
3
+ * (fast-model-tier §5.5).
4
+ *
5
+ * ASCII ONLY: `src/fast/**` is inside the glyph scanner's scope
6
+ * (`glyphs.test.ts`), so no literal in this tree may hold a non-ASCII byte. The
7
+ * regex there is a hardcoded directory list, which is why adding this tree and
8
+ * adding `fast` to that list are the SAME change (C-4 / AC-27) - a scanner that
9
+ * silently stops scanning is worse than no scanner.
10
+ *
11
+ * TWO KINDS OF NUMBER, AND THEY ARE NOT INTERCHANGEABLE - the distinction
12
+ * `TEAM_LIMITS` and `TODO_LIMITS` both state:
13
+ *
14
+ * - The entries below are STRUCTURAL. They describe what a review call, a
15
+ * digest and a card can physically carry. A user has no business tuning them.
16
+ * - The ten `fast.*` keys in `config/schema.ts` are POLICY: which model, how
17
+ * often, how long a critique may be. Those are clamped, persisted and
18
+ * user-facing.
19
+ *
20
+ * `reviewMaxChars` therefore lives in the CONFIG and not here: it is the one
21
+ * number whose right value depends on how much of their own context the user is
22
+ * willing to spend on advice. `reviewOutputTokens` stays here because 512 tokens
23
+ * is a bound on a protocol, not a preference.
24
+ */
25
+ export const FAST_LIMITS = {
26
+ /**
27
+ * The flat input cost of one review (D-6 / R-2).
28
+ *
29
+ * THIS NUMBER IS THE WHOLE ECONOMIC ARGUMENT. The reviewer builds its own
30
+ * bounded frame ring and never reads `agent.state.messages`, which after four
31
+ * 60 KB file reads carries 240 KB of file bodies; shipping that to the "cheap"
32
+ * model would make a review cost MORE than the turn it is reviewing.
33
+ */
34
+ digestMaxChars: 4_000,
35
+ /** The user's request for the run, clamped. Without it the reviewer is asked
36
+ * whether work is on track without being told what the track is. */
37
+ goalChars: 500,
38
+ /** Per-frame assistant-text tail. Rolling, clipped on every append. */
39
+ turnTailChars: 600,
40
+ /** Per picked tool argument. Mirrors `TEAM_LIMITS.activityArgChars`. */
41
+ toolArgChars: 120,
42
+ /** Frames kept per run. `>=` the max of `reviewContextTurns`, with slack. */
43
+ maxFramesRetained: 12,
44
+ /** Hard cap on the review's own output. */
45
+ reviewOutputTokens: 512,
46
+ /**
47
+ * Wall-clock budget for ONE review call INCLUDING RETRIES (D-H11).
48
+ *
49
+ * THE BUDGET HAS TWO TERMS AND ONLY ONE OF THEM SHRANK. Round 1 set 30 s as
50
+ * IF-3's fallback: `ProviderRegistry.complete()` routed through a retrying
51
+ * `stream()` with `maxRetries: 10`, so the budget covered
52
+ * `attempts x (call + backoff)`. Round 2 gives the reviewer its own
53
+ * `maxRetries: 1` registry (`wiring.ts`), which collapses that term to about
54
+ * one call plus a one-second backoff - but it does nothing at all to the
55
+ * other term, THE FAST MODEL'S OWN GENERATION LATENCY. The review asks for up
56
+ * to `reviewOutputTokens` from a `digestMaxChars` prompt, and the tier is by
57
+ * construction pointed at the cheapest endpoint the user has: the population
58
+ * where a 15-25 s completion is ordinary rather than pathological.
59
+ *
60
+ * SO THIS IS 20 000 AND NOT 12 000. A timeout IS a strike (see `runReview`),
61
+ * and `maxConsecutiveFailures` is 3, so a 12 s bound would convert "slow but
62
+ * healthy provider" into a tier that self-disables for the session and
63
+ * reports itself as MISCONFIGURED - the one register D-24 reserves for faults
64
+ * that never self-heal, spent on one that does. 20 s keeps a margin over a
65
+ * slow completion and still cuts a stuck review's occupancy of the
66
+ * single-flight slot by a third. Lower it only with a measured p99, which
67
+ * `manual-test.md` row 6 exists to produce.
68
+ */
69
+ reviewTimeoutMs: 20_000,
70
+ /** Reviews STARTED per run. A 40-turn run at cadence 5 would otherwise buy 8
71
+ * critiques the lead increasingly ignores. */
72
+ maxReviewsPerRun: 6,
73
+ /** Consecutive NON-TRANSIENT failures before the reviewer self-disables. */
74
+ maxConsecutiveFailures: 3,
75
+ /**
76
+ * A critique waiting for its injection window goes stale (RV-12).
77
+ *
78
+ * The window is not guaranteed to be prompt: one `task` dispatch is a single
79
+ * tool call that can run for `team.dispatchTimeoutMs`, so a critique triggered
80
+ * just before one arrives at the far side describing a state five children
81
+ * have since rewritten. Advice that is merely late is worse than no advice,
82
+ * because the lead cannot tell that it is late.
83
+ */
84
+ pendingMaxAgeMs: 120_000,
85
+ /** ... or is overtaken by the run itself, in turns. */
86
+ pendingMaxTurnsBehind: 2,
87
+ /** Rows the transcript card renders before eliding. */
88
+ cardTextRows: 4,
89
+ /** Below this many columns the status chip drops (D-20's treatment, not
90
+ * `agentMode`'s - the guaranteed reporting surface is `/fast status`). */
91
+ statusCompactCols: 100,
92
+ };
93
+ /**
94
+ * Bumped whenever the wording of `<fast_tier>` or the review system prompt
95
+ * changes, so a behaviour report can be tied to a block revision with one grep -
96
+ * exactly what `TEAM_BLOCK_VERSION` and `TODO_BLOCK_VERSION` are for.
97
+ */
98
+ export const FAST_BLOCK_VERSION = 'v1-2026-07';
@@ -0,0 +1,72 @@
1
+ /**
2
+ * The prompt text the fast tier adds (fast-model-tier §4.6).
3
+ *
4
+ * ASCII ONLY: `src/fast/**` is inside the glyph scanner's scope, and these
5
+ * strings also reach a model that may be running in a `cmd.exe` terminal.
6
+ *
7
+ * `<fast_tier>` IS SPLICED CONDITIONALLY by `buildSystemPrompt`, which is what
8
+ * preserves invariant I-2: with the tier off the prompt is BYTE-IDENTICAL to the
9
+ * pre-feature output for a fixed tool array. An unconditional guidance line here
10
+ * would quietly break `--no-fast`, `--no-team`, `--no-skills` and `--no-todo`
11
+ * all at once.
12
+ *
13
+ * `FAST_BLOCK_VERSION` exists so a change to this wording is greppable from a
14
+ * behaviour report.
15
+ */
16
+ import { FAST_BLOCK_VERSION } from './limits.js';
17
+ export { FAST_BLOCK_VERSION };
18
+ export interface FastBlockParams {
19
+ /** The resolved fast model id, interpolated so the prompt never names a model
20
+ * the tier will not actually use (§3.9 / RV-3). */
21
+ model: string;
22
+ /** `fast.delegate` — whether `model: "fast"` is honoured on `task`. */
23
+ delegate: boolean;
24
+ /** `fast.review` — whether `<fast_review>` blocks can appear at all. */
25
+ review: boolean;
26
+ }
27
+ /**
28
+ * The lead's `<fast_tier>` block. Under 1100 characters on purpose: it is paid
29
+ * for on EVERY turn of every fast-enabled session, so anything that does not
30
+ * change a delegation decision does not belong in it (R-11).
31
+ *
32
+ * EACH PARAGRAPH IS PRESENT ONLY WHEN ITS CAPABILITY IS. `fast.delegate: false,
33
+ * fast.review: true` advertises only the review, because telling a model about a
34
+ * `model: "fast"` field that the normalizer will downgrade every time is how you
35
+ * get a model that keeps asking for something it can never have.
36
+ *
37
+ * Returns `''` when neither capability is live, which the splice treats as "no
38
+ * block" and which keeps the byte-identity branch intact.
39
+ */
40
+ export declare function buildFastBlock(params: FastBlockParams): string;
41
+ export interface ReviewSystemPromptParams {
42
+ /** `fast.reviewMaxChars` — quoted so the model and the clamp agree. */
43
+ maxChars: number;
44
+ }
45
+ /**
46
+ * The reviewer's own system prompt.
47
+ *
48
+ * IT ASKS FOR AN EXACT STRING when there is nothing to say (`OK - on track.`)
49
+ * and `normalizeCritique` still matches it with a NORMALIZED REGEX rather than
50
+ * equality (D-17). The two are not redundant: the prompt makes compliance the
51
+ * likely outcome, the regex makes non-compliance harmless.
52
+ *
53
+ * NO TOOLS ARE OFFERED AND NONE ARE MENTIONED (D-11). A tool-using reviewer
54
+ * would be a second agent editing the same working tree with none of the gates
55
+ * the lead's tools carry.
56
+ */
57
+ export declare function buildReviewSystemPrompt(params: ReviewSystemPromptParams): string;
58
+ export interface ReviewInjectionParams {
59
+ turn: number;
60
+ model: string;
61
+ text: string;
62
+ }
63
+ /**
64
+ * The block that is actually steered into the run.
65
+ *
66
+ * THE ATTRIBUTES ARE NOT DECORATION. Steering delivers this in the `user` role
67
+ * (`agent-loop.ts:129`), so the wrapper is the only thing distinguishing an
68
+ * automated critique from something the human typed - and it is also what makes
69
+ * the residual case in §3.5.4a (a stranded block the reviewer may not clear)
70
+ * self-identifying if it ever does surface.
71
+ */
72
+ export declare function renderReviewInjection(params: ReviewInjectionParams): string;
@@ -0,0 +1,94 @@
1
+ /**
2
+ * The prompt text the fast tier adds (fast-model-tier §4.6).
3
+ *
4
+ * ASCII ONLY: `src/fast/**` is inside the glyph scanner's scope, and these
5
+ * strings also reach a model that may be running in a `cmd.exe` terminal.
6
+ *
7
+ * `<fast_tier>` IS SPLICED CONDITIONALLY by `buildSystemPrompt`, which is what
8
+ * preserves invariant I-2: with the tier off the prompt is BYTE-IDENTICAL to the
9
+ * pre-feature output for a fixed tool array. An unconditional guidance line here
10
+ * would quietly break `--no-fast`, `--no-team`, `--no-skills` and `--no-todo`
11
+ * all at once.
12
+ *
13
+ * `FAST_BLOCK_VERSION` exists so a change to this wording is greppable from a
14
+ * behaviour report.
15
+ */
16
+ import { FAST_BLOCK_VERSION } from './limits.js';
17
+ export { FAST_BLOCK_VERSION };
18
+ /**
19
+ * The lead's `<fast_tier>` block. Under 1100 characters on purpose: it is paid
20
+ * for on EVERY turn of every fast-enabled session, so anything that does not
21
+ * change a delegation decision does not belong in it (R-11).
22
+ *
23
+ * EACH PARAGRAPH IS PRESENT ONLY WHEN ITS CAPABILITY IS. `fast.delegate: false,
24
+ * fast.review: true` advertises only the review, because telling a model about a
25
+ * `model: "fast"` field that the normalizer will downgrade every time is how you
26
+ * get a model that keeps asking for something it can never have.
27
+ *
28
+ * Returns `''` when neither capability is live, which the splice treats as "no
29
+ * block" and which keeps the byte-identity branch intact.
30
+ */
31
+ export function buildFastBlock(params) {
32
+ if (!params.delegate && !params.review)
33
+ return '';
34
+ const lines = [
35
+ '<fast_tier>',
36
+ `A second, cheaper model is available in this session (${params.model}).`,
37
+ ];
38
+ if (params.delegate) {
39
+ lines.push('', 'Delegation: pass model:"fast" to a task subagent whose work is mechanical and', 'high-volume - reading or searching many files, summarizing long command output,', 'applying the same small edit in several places. Keep model:"main" (the default)', 'for design decisions, tricky debugging, and anything that has to be right the', 'first time. A fast subagent has the same tools and the same permissions as any', 'other; only the model differs.');
40
+ }
41
+ if (params.review) {
42
+ lines.push('', 'Reviews: every few turns a <fast_review> block may appear in the conversation.', 'It is an automated second opinion from the fast model - NOT a message from the', 'user. Treat it as advice: act on it when it is right, say so briefly and carry', 'on when it is not, and never ask the user to confirm it.');
43
+ }
44
+ lines.push('</fast_tier>');
45
+ return lines.join('\n');
46
+ }
47
+ /**
48
+ * The reviewer's own system prompt.
49
+ *
50
+ * IT ASKS FOR AN EXACT STRING when there is nothing to say (`OK - on track.`)
51
+ * and `normalizeCritique` still matches it with a NORMALIZED REGEX rather than
52
+ * equality (D-17). The two are not redundant: the prompt makes compliance the
53
+ * likely outcome, the regex makes non-compliance harmless.
54
+ *
55
+ * NO TOOLS ARE OFFERED AND NONE ARE MENTIONED (D-11). A tool-using reviewer
56
+ * would be a second agent editing the same working tree with none of the gates
57
+ * the lead's tools carry.
58
+ */
59
+ export function buildReviewSystemPrompt(params) {
60
+ const max = Math.max(1, Math.floor(params.maxChars) || 1);
61
+ return [
62
+ 'You are a fast reviewer watching another agent work. You see a short digest of',
63
+ 'its recent turns: the goal, the tools it called, and the tail of what it said.',
64
+ '',
65
+ 'Answer ONE question: is this run still on track for the goal?',
66
+ '',
67
+ `If it is, reply with EXACTLY: OK - on track.`,
68
+ 'If it is not, reply with plain prose naming the single most useful correction -',
69
+ `at most ${max} characters, no preamble, no code fences, no lists, no headings.`,
70
+ '',
71
+ 'Things worth saying: repeated edits with no verification, a failing tool call',
72
+ 'that was ignored, work that has drifted from the goal, an obvious cheaper route.',
73
+ 'Things not worth saying: style opinions, praise, restating what the agent did.',
74
+ '',
75
+ 'You cannot run tools and you are not talking to the user. You are advising the',
76
+ 'agent, and it will decide.',
77
+ ].join('\n');
78
+ }
79
+ /**
80
+ * The block that is actually steered into the run.
81
+ *
82
+ * THE ATTRIBUTES ARE NOT DECORATION. Steering delivers this in the `user` role
83
+ * (`agent-loop.ts:129`), so the wrapper is the only thing distinguishing an
84
+ * automated critique from something the human typed - and it is also what makes
85
+ * the residual case in §3.5.4a (a stranded block the reviewer may not clear)
86
+ * self-identifying if it ever does surface.
87
+ */
88
+ export function renderReviewInjection(params) {
89
+ return [
90
+ `<fast_review turn="${params.turn}" model="${params.model}">`,
91
+ params.text,
92
+ '</fast_review>',
93
+ ].join('\n');
94
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * `resolveFastTier` — THE single authority on whether a fast tier exists and
3
+ * what it points at (fast-model-tier §3.2).
4
+ *
5
+ * ASCII ONLY: `src/fast/**` is inside the glyph scanner's scope.
6
+ *
7
+ * EVERY CONSUMER READS THE TIER THROUGH THIS FUNCTION AND NOWHERE ELSE - the
8
+ * controller, the subagent factory, the reviewer, `/fast`, the settings screen
9
+ * and the status bar. Four call sites recomputing "is fast usable?" from raw
10
+ * config keys is exactly how a UI ends up showing a chip for a tier that refuses
11
+ * every call.
12
+ *
13
+ * IT IS ALSO RE-RUN ON EVERY CONFIG MUTATION, NOT ONCE (rule 8 / RV-3). Rules 2
14
+ * and 6 make `fast.provider: ''` and `fast.baseUrl: ''` INHERIT from
15
+ * `config.provider` / `config.baseUrl`, and rule 5 reads `apiKeys`. All of those
16
+ * are live: `/model`, `/provider`, the settings screen's save, `/reload` and a
17
+ * one-shot key edit each rewrite one of them mid-session. A tier resolved once
18
+ * at construction is a tier that can be describing a provider the session no
19
+ * longer uses.
20
+ */
21
+ import { type CliConfig } from '../config/schema.js';
22
+ import type { FastTier } from './types.js';
23
+ /**
24
+ * Resolve the fast tier from the CURRENT config.
25
+ *
26
+ * @param config The live effective config. Read fresh; never snapshot it.
27
+ * @param hasKey `AgentController.hasApiKey`, which already resolves the
28
+ * config-file key, the provider environment variable and the one-shot
29
+ * `--api-key` override. Injected rather than reimplemented, because a second
30
+ * key resolver is a second answer to "why does it say no key?".
31
+ */
32
+ export declare function resolveFastTier(config: CliConfig, hasKey: (providerId: string) => boolean): FastTier;
33
+ /**
34
+ * The one-line notice for a tier the user switched on that cannot resolve, or
35
+ * `null` when there is nothing worth saying.
36
+ *
37
+ * `disabled` returns `null` - the tier being off because the user left it off is
38
+ * not news. The other three are CONFIGURATION MISTAKES that are otherwise
39
+ * completely invisible: nothing else in the session would ever mention them.
40
+ */
41
+ export declare function describeFastTierProblem(tier: FastTier, provider: string): string | null;
42
+ /** The provider a tier resolution was about, for the notice above. */
43
+ export declare function fastProviderOf(config: CliConfig): string;