@aragon-agent/cli 0.5.2 → 0.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +90 -0
- package/README.md +128 -1
- package/dist/agent/controller.d.ts +51 -1
- package/dist/agent/controller.js +177 -2
- package/dist/agent/headless.d.ts +12 -0
- package/dist/agent/headless.js +41 -1
- package/dist/agent/reducer.d.ts +48 -0
- package/dist/agent/reducer.js +91 -2
- package/dist/agent/system-prompt.d.ts +17 -0
- package/dist/agent/system-prompt.js +4 -0
- package/dist/cli.js +66 -2
- package/dist/commands/builtins.js +82 -0
- package/dist/config/app-paths.d.ts +17 -0
- package/dist/config/app-paths.js +21 -0
- package/dist/config/cli-commands.d.ts +26 -2
- package/dist/config/cli-commands.js +63 -4
- package/dist/config/env.d.ts +2 -2
- package/dist/config/env.js +28 -2
- package/dist/config/history-commands.d.ts +20 -0
- package/dist/config/history-commands.js +71 -0
- package/dist/config/load.d.ts +13 -0
- package/dist/config/load.js +65 -5
- package/dist/config/migrate-state-out-of-config.d.ts +32 -0
- package/dist/config/migrate-state-out-of-config.js +85 -0
- package/dist/config/prompt-history.d.ts +86 -0
- package/dist/config/prompt-history.js +332 -0
- package/dist/config/schema.d.ts +112 -16
- package/dist/config/schema.js +89 -6
- package/dist/config/store.js +13 -4
- package/dist/config/ui-state.d.ts +52 -0
- package/dist/config/ui-state.js +149 -0
- package/dist/logging/install.d.ts +85 -0
- package/dist/logging/install.js +94 -0
- package/dist/logging/logger.d.ts +1 -1
- package/dist/session/persist.d.ts +16 -0
- package/dist/session/persist.js +19 -1
- package/dist/team/activity.d.ts +67 -0
- package/dist/team/activity.js +130 -0
- package/dist/team/bus.d.ts +190 -0
- package/dist/team/bus.js +366 -0
- package/dist/team/comm-tools.d.ts +61 -0
- package/dist/team/comm-tools.js +221 -0
- package/dist/team/human-queue.d.ts +55 -0
- package/dist/team/human-queue.js +72 -0
- package/dist/team/limits.d.ts +123 -0
- package/dist/team/limits.js +128 -0
- package/dist/team/normalize.d.ts +39 -0
- package/dist/team/normalize.js +106 -0
- package/dist/team/panel-rows.d.ts +48 -0
- package/dist/team/panel-rows.js +75 -0
- package/dist/team/prompt.d.ts +49 -0
- package/dist/team/prompt.js +87 -0
- package/dist/team/report.d.ts +58 -0
- package/dist/team/report.js +205 -0
- package/dist/team/retry.d.ts +48 -0
- package/dist/team/retry.js +56 -0
- package/dist/team/runtime.d.ts +122 -0
- package/dist/team/runtime.js +406 -0
- package/dist/team/subagent.d.ts +90 -0
- package/dist/team/subagent.js +294 -0
- package/dist/team/task-tool.d.ts +47 -0
- package/dist/team/task-tool.js +118 -0
- package/dist/team/types.d.ts +172 -0
- package/dist/team/types.js +13 -0
- package/dist/tools/index.d.ts +48 -2
- package/dist/tools/index.js +26 -1
- package/dist/ui/App.js +97 -13
- package/dist/ui/StatusBar.d.ts +21 -0
- package/dist/ui/StatusBar.js +5 -2
- package/dist/ui/TeamPanel.d.ts +57 -0
- package/dist/ui/TeamPanel.js +134 -0
- package/dist/ui/Transcript.js +10 -0
- package/dist/ui/entries/TeamCard.d.ts +31 -0
- package/dist/ui/entries/TeamCard.js +39 -0
- package/dist/ui/glyphs.d.ts +10 -0
- package/dist/ui/glyphs.js +6 -0
- package/dist/ui/layout/AppShell.d.ts +11 -1
- package/dist/ui/layout/AppShell.js +3 -3
- package/dist/ui/overlays/HelpOverlay.js +12 -0
- package/dist/ui/transcript-text.js +20 -0
- package/dist/ui/use-startup-notices.d.ts +1 -1
- package/dist/ui/use-startup-notices.js +2 -1
- package/package.json +2 -2
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `team_send` / `team_wait` and the `withMailboxTail` wrapper — the subagent
|
|
3
|
+
* half of the team bus (team-subagents §3.6 / §4.2).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY: `src/team/**` is inside the glyph scanner's scope.
|
|
6
|
+
*
|
|
7
|
+
* REGISTERED ON CHILDREN ONLY. Neither tool ever reaches a lead, which is why
|
|
8
|
+
* neither is in `HOST_TOOL_NAMES` (P2-5) and why both are in
|
|
9
|
+
* `TEAM_SUBAGENT_TOOL_NAMES` — the `policyExempt` set that keeps the lead's
|
|
10
|
+
* `allowed-tools` ceiling from refusing tools it has never heard of and
|
|
11
|
+
* dead-ending the channel (§3.11).
|
|
12
|
+
*
|
|
13
|
+
* EVERY RESULT IS A NON-ERROR `textResult`, refusals included (D-7). An
|
|
14
|
+
* `errorResult` reads as a malfunction and invites an immediate retry, which is
|
|
15
|
+
* exactly the behaviour the rate limits exist to prevent.
|
|
16
|
+
*/
|
|
17
|
+
import { type AgentTool } from '@aragon-agent/core';
|
|
18
|
+
import { TeamBus } from './bus.js';
|
|
19
|
+
import type { WatchdogPausable } from './human-queue.js';
|
|
20
|
+
/**
|
|
21
|
+
* `team_send`.
|
|
22
|
+
*
|
|
23
|
+
* The bus owns quota, interval and delivery; this tool owns only the wording,
|
|
24
|
+
* because the wording is what the model acts on and it must name the limit that
|
|
25
|
+
* was hit and what to do instead.
|
|
26
|
+
*/
|
|
27
|
+
export declare function makeTeamSend(bus: TeamBus, selfKey: string): AgentTool;
|
|
28
|
+
export interface TeamWaitDeps {
|
|
29
|
+
/**
|
|
30
|
+
* The child's own `Agent`, read LAZILY.
|
|
31
|
+
*
|
|
32
|
+
* The tool array has to exist before `new Agent(...)` can be constructed, so
|
|
33
|
+
* there is a window in which this returns `null`. Resolving it eagerly would
|
|
34
|
+
* capture that `null` for the life of the child and silently drop the
|
|
35
|
+
* watchdog pause — the failure would only show up as a mysterious
|
|
36
|
+
* `idle watchdog fired` during a legitimate wait.
|
|
37
|
+
*/
|
|
38
|
+
getAgent: () => WatchdogPausable | null;
|
|
39
|
+
/** Ceiling on any single wait: never longer than the child's own budget. */
|
|
40
|
+
maxWaitMs: number;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* `team_wait`.
|
|
44
|
+
*
|
|
45
|
+
* Runs with THAT CHILD'S idle watchdog paused, in a `try/finally` — the same
|
|
46
|
+
* treatment `withHumanWait` gives an overlay and for the same reason: a
|
|
47
|
+
* deliberate wait is not a wedged tool, and the child emits nothing while it is
|
|
48
|
+
* blocked here (I-3, one level down).
|
|
49
|
+
*/
|
|
50
|
+
export declare function makeTeamWait(bus: TeamBus, selfKey: string, deps: TeamWaitDeps): AgentTool;
|
|
51
|
+
/**
|
|
52
|
+
* Append anything queued for `key` to the tool's result, inside a `<team_mail>`
|
|
53
|
+
* block.
|
|
54
|
+
*
|
|
55
|
+
* Same `{...tool, execute}` shape as `withConfirmation` / `withToolPolicy` /
|
|
56
|
+
* `withPlanModeGate`, so nothing in the engine needs to know this happened. The
|
|
57
|
+
* mailbox is drained EVEN WHEN the wrapped tool errored: a refused `bash` call
|
|
58
|
+
* is still a delivery opportunity, and holding mail back until a successful call
|
|
59
|
+
* would make delivery depend on whether the child happened to be doing well.
|
|
60
|
+
*/
|
|
61
|
+
export declare function withMailboxTail(tool: AgentTool, bus: TeamBus, key: string): AgentTool;
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `team_send` / `team_wait` and the `withMailboxTail` wrapper — the subagent
|
|
3
|
+
* half of the team bus (team-subagents §3.6 / §4.2).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY: `src/team/**` is inside the glyph scanner's scope.
|
|
6
|
+
*
|
|
7
|
+
* REGISTERED ON CHILDREN ONLY. Neither tool ever reaches a lead, which is why
|
|
8
|
+
* neither is in `HOST_TOOL_NAMES` (P2-5) and why both are in
|
|
9
|
+
* `TEAM_SUBAGENT_TOOL_NAMES` — the `policyExempt` set that keeps the lead's
|
|
10
|
+
* `allowed-tools` ceiling from refusing tools it has never heard of and
|
|
11
|
+
* dead-ending the channel (§3.11).
|
|
12
|
+
*
|
|
13
|
+
* EVERY RESULT IS A NON-ERROR `textResult`, refusals included (D-7). An
|
|
14
|
+
* `errorResult` reads as a malfunction and invites an immediate retry, which is
|
|
15
|
+
* exactly the behaviour the rate limits exist to prevent.
|
|
16
|
+
*/
|
|
17
|
+
import { textResult } from '@aragon-agent/core';
|
|
18
|
+
import { TEAM_LIMITS } from './limits.js';
|
|
19
|
+
import { renderMailBlock } from './bus.js';
|
|
20
|
+
const SEND_DESCRIPTION = 'Message a teammate. Use it only when it changes what they should do - a finding they ' +
|
|
21
|
+
'would otherwise duplicate, or a file you are about to change. You get ' +
|
|
22
|
+
`${TEAM_LIMITS.messagesPerAgent} messages for this whole task and at most one every ` +
|
|
23
|
+
`${Math.round(TEAM_LIMITS.minSendIntervalMs / 1000)} seconds; spending them on status updates means you cannot send ` +
|
|
24
|
+
'the one that matters. Incoming messages are appended to your next tool result ' +
|
|
25
|
+
'automatically; you do not need to poll.';
|
|
26
|
+
const WAIT_DESCRIPTION = 'Block until a teammate messages you. Use this only when you genuinely cannot continue ' +
|
|
27
|
+
'without an answer - it spends wall-clock time and your teammate may never reply. ' +
|
|
28
|
+
'Messages you are simply expecting arrive on your next tool result without waiting.';
|
|
29
|
+
/**
|
|
30
|
+
* `team_send`.
|
|
31
|
+
*
|
|
32
|
+
* The bus owns quota, interval and delivery; this tool owns only the wording,
|
|
33
|
+
* because the wording is what the model acts on and it must name the limit that
|
|
34
|
+
* was hit and what to do instead.
|
|
35
|
+
*/
|
|
36
|
+
export function makeTeamSend(bus, selfKey) {
|
|
37
|
+
return {
|
|
38
|
+
name: 'team_send',
|
|
39
|
+
label: 'Message teammate',
|
|
40
|
+
description: SEND_DESCRIPTION,
|
|
41
|
+
parameters: {
|
|
42
|
+
type: 'object',
|
|
43
|
+
properties: {
|
|
44
|
+
to: { type: 'string', description: 'A peer label, "lead", or "all".' },
|
|
45
|
+
subject: { type: 'string', description: `<= ${TEAM_LIMITS.subjectChars} chars.` },
|
|
46
|
+
body: { type: 'string', description: `<= ${TEAM_LIMITS.bodyChars} chars.` },
|
|
47
|
+
},
|
|
48
|
+
required: ['to', 'subject', 'body'],
|
|
49
|
+
},
|
|
50
|
+
async execute(_id, params) {
|
|
51
|
+
const p = params;
|
|
52
|
+
const to = typeof p.to === 'string' ? p.to : '';
|
|
53
|
+
const subject = typeof p.subject === 'string' ? p.subject : '';
|
|
54
|
+
const body = typeof p.body === 'string' ? p.body : '';
|
|
55
|
+
if (to.trim().length === 0 || subject.trim().length === 0) {
|
|
56
|
+
return textResult('Not sent: team_send needs a "to" and a "subject".');
|
|
57
|
+
}
|
|
58
|
+
const verdict = bus.send(selfKey, to, subject, body);
|
|
59
|
+
if (verdict.ok) {
|
|
60
|
+
return textResult(`Delivered to ${verdict.delivered.join(', ')}. ${verdict.remaining} of ` +
|
|
61
|
+
`${TEAM_LIMITS.messagesPerAgent} messages left.`);
|
|
62
|
+
}
|
|
63
|
+
switch (verdict.reason) {
|
|
64
|
+
case 'unknown_recipient':
|
|
65
|
+
return textResult(`Not sent: no subagent labelled "${to.trim()}". Teammates: ` +
|
|
66
|
+
`${verdict.recipients.filter((r) => r !== selfKey).join(', ')}.`);
|
|
67
|
+
case 'agent_quota':
|
|
68
|
+
return textResult(`Not sent: message quota spent (${verdict.used}/${verdict.max}). Finish your work ` +
|
|
69
|
+
'and put it in your final summary.');
|
|
70
|
+
case 'dispatch_quota':
|
|
71
|
+
return textResult(`Not sent: the team has spent its message budget (${verdict.used}/${verdict.max}). ` +
|
|
72
|
+
'Continue on your own and report what you found.');
|
|
73
|
+
case 'too_soon':
|
|
74
|
+
return textResult(`Not sent: one message every ${Math.round(TEAM_LIMITS.minSendIntervalMs / 1000)}s. Try again in ` +
|
|
75
|
+
`${verdict.retryInSeconds}s, or continue and report instead.`);
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Label lists for a refusal sentence: joined with `and`, capped at three names.
|
|
82
|
+
*
|
|
83
|
+
* A 10-way dispatch must not produce a 200-character refusal - the child has to
|
|
84
|
+
* read it, and the point of the sentence is what to do next, not a roll call.
|
|
85
|
+
*/
|
|
86
|
+
function nameList(names) {
|
|
87
|
+
if (names.length === 0)
|
|
88
|
+
return 'nobody';
|
|
89
|
+
if (names.length === 1)
|
|
90
|
+
return names[0];
|
|
91
|
+
if (names.length <= 3)
|
|
92
|
+
return `${names.slice(0, -1).join(', ')} and ${names[names.length - 1]}`;
|
|
93
|
+
return `${names.slice(0, 3).join(', ')} and ${names.length - 3} others`;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* What the child is told instead of blocking (F-3).
|
|
97
|
+
*
|
|
98
|
+
* Following the refusal style `makeTeamSend` already uses: name the reason and
|
|
99
|
+
* say what to do instead, never as an `errorResult` (D-7 / D-11).
|
|
100
|
+
*
|
|
101
|
+
* `unknown_sender` is its own reason rather than folded into `no_peers` (P1-2)
|
|
102
|
+
* because it is the only one of the four the child can act on IN THIS TURN. The
|
|
103
|
+
* other three say "give up on the message"; this one says "you asked for a name
|
|
104
|
+
* nobody has, here are the names" - and telling a child in a five-way fan-out
|
|
105
|
+
* that it is "the only subagent in this dispatch" is simply false.
|
|
106
|
+
*/
|
|
107
|
+
function refusalText(refusal) {
|
|
108
|
+
switch (refusal.reason) {
|
|
109
|
+
case 'no_peers':
|
|
110
|
+
return ('Nobody to wait for: you are the only subagent in this dispatch. Continue and put ' +
|
|
111
|
+
'what you found in your final summary.');
|
|
112
|
+
case 'unknown_sender':
|
|
113
|
+
return (`Nobody here is called "${refusal.asked}". Teammates: ${nameList(refusal.peers)}. ` +
|
|
114
|
+
'(The lead never sends messages.) Continue, or wait again naming one of those.');
|
|
115
|
+
case 'all_finished':
|
|
116
|
+
return (`Nobody can answer: ${nameList(refusal.who)} already finished. Continue without it ` +
|
|
117
|
+
'and say so in your summary.');
|
|
118
|
+
case 'all_blocked':
|
|
119
|
+
return (`Deadlock avoided: ${nameList(refusal.who)} is waiting for a message too, and cannot ` +
|
|
120
|
+
'be released before your own deadline. Continue with what you have and report it.');
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* `team_wait`.
|
|
125
|
+
*
|
|
126
|
+
* Runs with THAT CHILD'S idle watchdog paused, in a `try/finally` — the same
|
|
127
|
+
* treatment `withHumanWait` gives an overlay and for the same reason: a
|
|
128
|
+
* deliberate wait is not a wedged tool, and the child emits nothing while it is
|
|
129
|
+
* blocked here (I-3, one level down).
|
|
130
|
+
*/
|
|
131
|
+
export function makeTeamWait(bus, selfKey, deps) {
|
|
132
|
+
return {
|
|
133
|
+
name: 'team_wait',
|
|
134
|
+
label: 'Wait for teammate',
|
|
135
|
+
description: WAIT_DESCRIPTION,
|
|
136
|
+
parameters: {
|
|
137
|
+
type: 'object',
|
|
138
|
+
properties: {
|
|
139
|
+
from: {
|
|
140
|
+
type: 'string',
|
|
141
|
+
description: 'Only accept a message from this label. Omit for any.',
|
|
142
|
+
},
|
|
143
|
+
timeoutSeconds: {
|
|
144
|
+
type: 'number',
|
|
145
|
+
default: TEAM_LIMITS.waitDefaultSeconds,
|
|
146
|
+
description: `${TEAM_LIMITS.waitMinSeconds}-${TEAM_LIMITS.waitMaxSeconds}.`,
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
async execute(_id, params, ctx) {
|
|
151
|
+
const p = params;
|
|
152
|
+
const from = typeof p.from === 'string' && p.from.trim().length > 0 ? p.from.trim() : undefined;
|
|
153
|
+
const requested = typeof p.timeoutSeconds === 'number' && Number.isFinite(p.timeoutSeconds)
|
|
154
|
+
? p.timeoutSeconds
|
|
155
|
+
: TEAM_LIMITS.waitDefaultSeconds;
|
|
156
|
+
const seconds = Math.min(TEAM_LIMITS.waitMaxSeconds, Math.max(TEAM_LIMITS.waitMinSeconds, Math.round(requested)));
|
|
157
|
+
const timeoutMs = Math.min(seconds * 1000, deps.maxWaitMs);
|
|
158
|
+
// `timeoutMs`, not a deadline: the bus owns the clock (P1-1). Checked
|
|
159
|
+
// BEFORE the watchdog pause on purpose - the refusal path returns
|
|
160
|
+
// synchronously, and a pause/resume pair around a synchronous return is a
|
|
161
|
+
// needless state transition on the one object whose state is hardest to
|
|
162
|
+
// reason about.
|
|
163
|
+
const refusal = bus.checkWaitable(selfKey, {
|
|
164
|
+
...(from ? { from } : {}),
|
|
165
|
+
timeoutMs,
|
|
166
|
+
});
|
|
167
|
+
if (refusal) {
|
|
168
|
+
bus.recordBlockedWait(selfKey);
|
|
169
|
+
return textResult(refusalText(refusal));
|
|
170
|
+
}
|
|
171
|
+
// Past the gate, `from` is known to name a real peer, so hand `wait()` the
|
|
172
|
+
// CANONICAL label rather than what the model typed (P1-3). `wait()` and
|
|
173
|
+
// `deliverToWaiter` compare exactly; this is the one line that makes them
|
|
174
|
+
// agree with the gate that just permitted the call.
|
|
175
|
+
const fromLabel = from ? bus.resolveLabel(from) : undefined;
|
|
176
|
+
const agent = deps.getAgent();
|
|
177
|
+
agent?.pauseIdleWatchdog();
|
|
178
|
+
let message;
|
|
179
|
+
try {
|
|
180
|
+
message = await bus.wait(selfKey, {
|
|
181
|
+
...(fromLabel ? { from: fromLabel } : {}),
|
|
182
|
+
timeoutMs,
|
|
183
|
+
...(ctx.signal ? { signal: ctx.signal } : {}),
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
finally {
|
|
187
|
+
agent?.resumeIdleWatchdog();
|
|
188
|
+
}
|
|
189
|
+
if (!message) {
|
|
190
|
+
return textResult(`No message arrived within ${Math.round(timeoutMs / 1000)}s. Continue without it.`);
|
|
191
|
+
}
|
|
192
|
+
return textResult(renderMailBlock([message]));
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Append anything queued for `key` to the tool's result, inside a `<team_mail>`
|
|
198
|
+
* block.
|
|
199
|
+
*
|
|
200
|
+
* Same `{...tool, execute}` shape as `withConfirmation` / `withToolPolicy` /
|
|
201
|
+
* `withPlanModeGate`, so nothing in the engine needs to know this happened. The
|
|
202
|
+
* mailbox is drained EVEN WHEN the wrapped tool errored: a refused `bash` call
|
|
203
|
+
* is still a delivery opportunity, and holding mail back until a successful call
|
|
204
|
+
* would make delivery depend on whether the child happened to be doing well.
|
|
205
|
+
*/
|
|
206
|
+
export function withMailboxTail(tool, bus, key) {
|
|
207
|
+
const original = tool.execute;
|
|
208
|
+
return {
|
|
209
|
+
...tool,
|
|
210
|
+
async execute(id, params, ctx) {
|
|
211
|
+
const result = await original(id, params, ctx);
|
|
212
|
+
const mail = bus.drain(key);
|
|
213
|
+
if (mail.length === 0)
|
|
214
|
+
return result;
|
|
215
|
+
return {
|
|
216
|
+
...result,
|
|
217
|
+
content: [...result.content, { type: 'text', text: renderMailBlock(mail) }],
|
|
218
|
+
};
|
|
219
|
+
},
|
|
220
|
+
};
|
|
221
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TeamHumanQueue — serializes subagent confirmations onto the ONE human slot
|
|
3
|
+
* (team-subagents §3.11 / I-4).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY: `src/team/**` is inside the glyph scanner's scope.
|
|
6
|
+
*
|
|
7
|
+
* `App.tsx` holds `humanRequest` as a SINGLE state slot and `resolveHuman`
|
|
8
|
+
* broadcasts one response to every pending entry, so two children raising a
|
|
9
|
+
* `--confirm` dialog at the same instant would double-resolve it. This queue is
|
|
10
|
+
* a plain promise chain that admits one request at a time and prefixes the
|
|
11
|
+
* summary with the child's label, so the user can always tell WHICH agent is
|
|
12
|
+
* asking.
|
|
13
|
+
*
|
|
14
|
+
* THE WATCHDOG IS PAUSED ON ENQUEUE, NOT ON DISPLAY (D-18 / P0-4). This is
|
|
15
|
+
* invariant I-3 one level down, and it is the second-most likely way to ship
|
|
16
|
+
* this feature broken. A child sitting third in the FIFO is emitting nothing,
|
|
17
|
+
* and its own `idleTimeout` (210 s by default) is armed: a user who takes four
|
|
18
|
+
* minutes over the first dialog would otherwise lose children 2 and 3 to their
|
|
19
|
+
* own watchdogs, each printing `[Agent] idle watchdog fired` over the TUI and
|
|
20
|
+
* each landing in the report as a failure that has nothing to do with the work.
|
|
21
|
+
* `controller.ts` records fixing exactly this for the LEAD's `--confirm` gate;
|
|
22
|
+
* repeating it for children would regress a fix this codebase already paid for.
|
|
23
|
+
* The wait starts when the promise is chained, which is where the time actually
|
|
24
|
+
* goes — hence enqueue, not display.
|
|
25
|
+
*/
|
|
26
|
+
import type { ConfirmRequest } from '../tools/index.js';
|
|
27
|
+
/** The slice of `Agent` this queue needs. Structural, so tests can stub it. */
|
|
28
|
+
export interface WatchdogPausable {
|
|
29
|
+
pauseIdleWatchdog(): void;
|
|
30
|
+
resumeIdleWatchdog(): void;
|
|
31
|
+
}
|
|
32
|
+
export declare class TeamHumanQueue {
|
|
33
|
+
private readonly confirm;
|
|
34
|
+
/** The chain. Never rejects: a failed request resolves `false` (deny). */
|
|
35
|
+
private tail;
|
|
36
|
+
constructor(confirm: (req: ConfirmRequest) => Promise<boolean>);
|
|
37
|
+
/**
|
|
38
|
+
* Ask the human about `req` on behalf of `label`, one at a time.
|
|
39
|
+
*
|
|
40
|
+
* @param child Paused for the whole wait, queue time included. May be `null`
|
|
41
|
+
* in the window before the child's `Agent` exists.
|
|
42
|
+
* @param signal The child's `ctx.signal`. An abort resolves `false` so the
|
|
43
|
+
* child is never left owing an answer to a dialog nobody is
|
|
44
|
+
* going to reach.
|
|
45
|
+
*/
|
|
46
|
+
request(child: WatchdogPausable | null, label: string, req: ConfirmRequest, signal?: AbortSignal): Promise<boolean>;
|
|
47
|
+
/**
|
|
48
|
+
* Chain `run` after everything already queued.
|
|
49
|
+
*
|
|
50
|
+
* The `catch` is what keeps the chain alive: an unhandled rejection here would
|
|
51
|
+
* poison `this.tail` and every LATER request would reject without ever being
|
|
52
|
+
* shown, which presents as "the second file write silently did nothing".
|
|
53
|
+
*/
|
|
54
|
+
private enqueue;
|
|
55
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TeamHumanQueue — serializes subagent confirmations onto the ONE human slot
|
|
3
|
+
* (team-subagents §3.11 / I-4).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY: `src/team/**` is inside the glyph scanner's scope.
|
|
6
|
+
*
|
|
7
|
+
* `App.tsx` holds `humanRequest` as a SINGLE state slot and `resolveHuman`
|
|
8
|
+
* broadcasts one response to every pending entry, so two children raising a
|
|
9
|
+
* `--confirm` dialog at the same instant would double-resolve it. This queue is
|
|
10
|
+
* a plain promise chain that admits one request at a time and prefixes the
|
|
11
|
+
* summary with the child's label, so the user can always tell WHICH agent is
|
|
12
|
+
* asking.
|
|
13
|
+
*
|
|
14
|
+
* THE WATCHDOG IS PAUSED ON ENQUEUE, NOT ON DISPLAY (D-18 / P0-4). This is
|
|
15
|
+
* invariant I-3 one level down, and it is the second-most likely way to ship
|
|
16
|
+
* this feature broken. A child sitting third in the FIFO is emitting nothing,
|
|
17
|
+
* and its own `idleTimeout` (210 s by default) is armed: a user who takes four
|
|
18
|
+
* minutes over the first dialog would otherwise lose children 2 and 3 to their
|
|
19
|
+
* own watchdogs, each printing `[Agent] idle watchdog fired` over the TUI and
|
|
20
|
+
* each landing in the report as a failure that has nothing to do with the work.
|
|
21
|
+
* `controller.ts` records fixing exactly this for the LEAD's `--confirm` gate;
|
|
22
|
+
* repeating it for children would regress a fix this codebase already paid for.
|
|
23
|
+
* The wait starts when the promise is chained, which is where the time actually
|
|
24
|
+
* goes — hence enqueue, not display.
|
|
25
|
+
*/
|
|
26
|
+
export class TeamHumanQueue {
|
|
27
|
+
confirm;
|
|
28
|
+
/** The chain. Never rejects: a failed request resolves `false` (deny). */
|
|
29
|
+
tail = Promise.resolve();
|
|
30
|
+
constructor(confirm) {
|
|
31
|
+
this.confirm = confirm;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Ask the human about `req` on behalf of `label`, one at a time.
|
|
35
|
+
*
|
|
36
|
+
* @param child Paused for the whole wait, queue time included. May be `null`
|
|
37
|
+
* in the window before the child's `Agent` exists.
|
|
38
|
+
* @param signal The child's `ctx.signal`. An abort resolves `false` so the
|
|
39
|
+
* child is never left owing an answer to a dialog nobody is
|
|
40
|
+
* going to reach.
|
|
41
|
+
*/
|
|
42
|
+
request(child, label, req, signal) {
|
|
43
|
+
child?.pauseIdleWatchdog();
|
|
44
|
+
const queued = this.enqueue(async () => {
|
|
45
|
+
if (signal?.aborted)
|
|
46
|
+
return false;
|
|
47
|
+
return this.confirm({ ...req, summary: `[${label}] ${req.summary}` });
|
|
48
|
+
});
|
|
49
|
+
const raced = signal ? Promise.race([queued, abortsToFalse(signal)]) : queued;
|
|
50
|
+
return raced.finally(() => child?.resumeIdleWatchdog());
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Chain `run` after everything already queued.
|
|
54
|
+
*
|
|
55
|
+
* The `catch` is what keeps the chain alive: an unhandled rejection here would
|
|
56
|
+
* poison `this.tail` and every LATER request would reject without ever being
|
|
57
|
+
* shown, which presents as "the second file write silently did nothing".
|
|
58
|
+
*/
|
|
59
|
+
enqueue(run) {
|
|
60
|
+
const next = this.tail.then(run, run).catch(() => false);
|
|
61
|
+
this.tail = next;
|
|
62
|
+
return next;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/** Resolve `false` when `signal` aborts, with the listener cleaned up after. */
|
|
66
|
+
function abortsToFalse(signal) {
|
|
67
|
+
if (signal.aborted)
|
|
68
|
+
return Promise.resolve(false);
|
|
69
|
+
return new Promise((resolve) => {
|
|
70
|
+
signal.addEventListener('abort', () => resolve(false), { once: true });
|
|
71
|
+
});
|
|
72
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TEAM_LIMITS — the single authority on every bound this subsystem enforces
|
|
3
|
+
* (team-subagents §3.5 / §3.6).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY: `src/team/**` is inside the glyph scanner's scope
|
|
6
|
+
* (`glyphs.test.ts`), so no literal in this tree may hold a non-ASCII byte.
|
|
7
|
+
*
|
|
8
|
+
* TWO KINDS OF NUMBER LIVE HERE AND THEY ARE NOT INTERCHANGEABLE.
|
|
9
|
+
*
|
|
10
|
+
* - The `TEAM_LIMITS` entries below are STRUCTURAL: they describe what the
|
|
11
|
+
* report format, the message channel and the panel can physically carry, and
|
|
12
|
+
* a user has no business tuning them. They are constants.
|
|
13
|
+
* - The six `team.*` config keys (`config/schema.ts`) are POLICY: how wide to
|
|
14
|
+
* fan out, how long to wait. They are clamped, persisted and user-facing.
|
|
15
|
+
*
|
|
16
|
+
* `hardMaxSubagents` sits on the structural side ON PURPOSE even though it
|
|
17
|
+
* bounds a config key: the requirement caps the fan-out at 10 (R-g), so a
|
|
18
|
+
* hand-edited `config.json` must not be able to raise it. It is applied twice —
|
|
19
|
+
* once in `clampTeamConfig` and once in `normalizeSubagentSpecs` — because a
|
|
20
|
+
* clamp that only runs on the read path leaves a bad value on disk.
|
|
21
|
+
*
|
|
22
|
+
* BYTES, NOT CHARACTERS, for every report budget (D-8 / I-9). `ToolExecutor`
|
|
23
|
+
* truncates combined tool text at 100 000 BYTES; a 24 000-character CJK report
|
|
24
|
+
* is 72 000 bytes, so a character-denominated cap would let the executor chop
|
|
25
|
+
* the report mid-section and take the failure count with it.
|
|
26
|
+
*/
|
|
27
|
+
export declare const TEAM_LIMITS: {
|
|
28
|
+
/**
|
|
29
|
+
* The requirement's ceiling (R-g). Enforced in `clampTeamConfig` AND in
|
|
30
|
+
* `normalizeSubagentSpecs`, so neither a hand-edited file nor a model asking
|
|
31
|
+
* for thirty can exceed it.
|
|
32
|
+
*/
|
|
33
|
+
readonly hardMaxSubagents: 10;
|
|
34
|
+
/** `SubagentSpec.description`, clamped (never rejected). */
|
|
35
|
+
readonly descriptionChars: 60;
|
|
36
|
+
/** `SubagentSpec.prompt`, clamped. The child cannot see the conversation, so
|
|
37
|
+
* this has to be generous enough to carry a whole brief. */
|
|
38
|
+
readonly promptChars: 8000;
|
|
39
|
+
/** Slugified `SubagentSpec.label`. Long enough to be meaningful in the panel,
|
|
40
|
+
* short enough that five of them fit one row of an 80-column terminal. */
|
|
41
|
+
readonly labelChars: 12;
|
|
42
|
+
readonly reportMaxBytes: 48000;
|
|
43
|
+
readonly summaryMaxBytes: 6000;
|
|
44
|
+
/**
|
|
45
|
+
* Floor a summary is trimmed TO, never below. A section trimmed to nothing is
|
|
46
|
+
* indistinguishable from a child that reported nothing, which is exactly the
|
|
47
|
+
* distinction the report exists to preserve.
|
|
48
|
+
*/
|
|
49
|
+
readonly summaryFloorBytes: 400;
|
|
50
|
+
/** Deduped `write_file` / `edit_file` paths recorded per child (§3.4). */
|
|
51
|
+
readonly filesTouchedMax: 30;
|
|
52
|
+
/** `team_send` calls one child may spend on a whole dispatch. */
|
|
53
|
+
readonly messagesPerAgent: 6;
|
|
54
|
+
/** `team_send` calls the whole dispatch may spend. */
|
|
55
|
+
readonly messagesPerDispatch: 24;
|
|
56
|
+
/** Minimum gap between one child's sends. */
|
|
57
|
+
readonly minSendIntervalMs: 15000;
|
|
58
|
+
/** Quota units a `to: "all"` broadcast costs. */
|
|
59
|
+
readonly broadcastCost: 2;
|
|
60
|
+
readonly subjectChars: 80;
|
|
61
|
+
readonly bodyChars: 800;
|
|
62
|
+
/** `team_wait` timeout clamp, in seconds. */
|
|
63
|
+
readonly waitMinSeconds: 5;
|
|
64
|
+
readonly waitMaxSeconds: 120;
|
|
65
|
+
readonly waitDefaultSeconds: 30;
|
|
66
|
+
/**
|
|
67
|
+
* `agent_update` coalescing window (§5.2). Five children streaming tokens must
|
|
68
|
+
* not push five React renders per token; phase transitions always pass.
|
|
69
|
+
*/
|
|
70
|
+
readonly agentUpdateThrottleMs: 120;
|
|
71
|
+
/** Agent rows the live panel renders before collapsing to `+N more` (§6.1). */
|
|
72
|
+
readonly panelMaxRows: 5;
|
|
73
|
+
/** Below this many terminal rows the panel collapses to its header line. */
|
|
74
|
+
readonly panelCollapseRows: 20;
|
|
75
|
+
/** Below this many columns the status-bar readout degrades to `[3]` (D-20). */
|
|
76
|
+
readonly statusCompactCols: 100;
|
|
77
|
+
/**
|
|
78
|
+
* STORAGE ceiling for a prose activity tail (P0-1). The most a `SubagentRun`
|
|
79
|
+
* may carry, NOT the width of the column: `TeamPanel.activityBudget(cols)`
|
|
80
|
+
* derives the display budget from the terminal and is always <= this.
|
|
81
|
+
*/
|
|
82
|
+
readonly activityChars: 64;
|
|
83
|
+
/**
|
|
84
|
+
* Floor under `activityBudget`. Below roughly 46 columns the roster has bigger
|
|
85
|
+
* problems than an elided activity line, and a column narrower than this shows
|
|
86
|
+
* nothing but an ellipsis - at which point the phase word is more informative.
|
|
87
|
+
*/
|
|
88
|
+
readonly activityMinChars: 12;
|
|
89
|
+
/** Rolling text buffer kept per child. The only per-token allocation. */
|
|
90
|
+
readonly activityTailChars: 160;
|
|
91
|
+
/** Per picked tool argument (P1-4). A path or a command longer than this is
|
|
92
|
+
* elided on capture, so a run can never carry more than a few hundred bytes
|
|
93
|
+
* of argument however large the real call was. */
|
|
94
|
+
readonly activityArgChars: 120;
|
|
95
|
+
/** At or above this many columns the activity line carries a full path or
|
|
96
|
+
* command; below it, a basename or a program name. Mirrors `statusCompactCols`.
|
|
97
|
+
* This chooses WHICH FACT to show; `activityBudget` chooses how much of it. */
|
|
98
|
+
readonly activityWideCols: 100;
|
|
99
|
+
/** Retries of a child that produced NOTHING. One, and not configurable: a
|
|
100
|
+
* second retry doubles the worst-case latency of a failing dispatch and buys
|
|
101
|
+
* nothing a re-dispatch by the lead would not buy better. */
|
|
102
|
+
readonly maxColdStartRetries: 1;
|
|
103
|
+
readonly retryBackoffMs: 2000;
|
|
104
|
+
};
|
|
105
|
+
/**
|
|
106
|
+
* The two comm tools, which are POLICY-EXEMPT inside a child (§3.11).
|
|
107
|
+
*
|
|
108
|
+
* They are not in the lead's registered tool list, so evaluating them against
|
|
109
|
+
* the lead's `allowed-tools` ceiling would refuse them and dead-end the channel
|
|
110
|
+
* — the same dead end `SKILL_TOOL_FLOOR` exists to prevent one level up.
|
|
111
|
+
*
|
|
112
|
+
* Deliberately NOT added to `HOST_TOOL_NAMES` (P2-5). That constant answers
|
|
113
|
+
* "can a skill's `allowed-tools` declaration ever take effect?", and these two
|
|
114
|
+
* are never registered on a lead — so a skill naming them genuinely IS
|
|
115
|
+
* unenforceable, and `aragon skills doctor` reporting it as such is correct
|
|
116
|
+
* rather than a gap.
|
|
117
|
+
*/
|
|
118
|
+
export declare const TEAM_SUBAGENT_TOOL_NAMES: ReadonlySet<string>;
|
|
119
|
+
/**
|
|
120
|
+
* Bumped whenever the wording of either prompt block changes, so a behaviour
|
|
121
|
+
* report can be tied to a block revision with one grep.
|
|
122
|
+
*/
|
|
123
|
+
export declare const TEAM_BLOCK_VERSION = "v1-2026-07";
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TEAM_LIMITS — the single authority on every bound this subsystem enforces
|
|
3
|
+
* (team-subagents §3.5 / §3.6).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY: `src/team/**` is inside the glyph scanner's scope
|
|
6
|
+
* (`glyphs.test.ts`), so no literal in this tree may hold a non-ASCII byte.
|
|
7
|
+
*
|
|
8
|
+
* TWO KINDS OF NUMBER LIVE HERE AND THEY ARE NOT INTERCHANGEABLE.
|
|
9
|
+
*
|
|
10
|
+
* - The `TEAM_LIMITS` entries below are STRUCTURAL: they describe what the
|
|
11
|
+
* report format, the message channel and the panel can physically carry, and
|
|
12
|
+
* a user has no business tuning them. They are constants.
|
|
13
|
+
* - The six `team.*` config keys (`config/schema.ts`) are POLICY: how wide to
|
|
14
|
+
* fan out, how long to wait. They are clamped, persisted and user-facing.
|
|
15
|
+
*
|
|
16
|
+
* `hardMaxSubagents` sits on the structural side ON PURPOSE even though it
|
|
17
|
+
* bounds a config key: the requirement caps the fan-out at 10 (R-g), so a
|
|
18
|
+
* hand-edited `config.json` must not be able to raise it. It is applied twice —
|
|
19
|
+
* once in `clampTeamConfig` and once in `normalizeSubagentSpecs` — because a
|
|
20
|
+
* clamp that only runs on the read path leaves a bad value on disk.
|
|
21
|
+
*
|
|
22
|
+
* BYTES, NOT CHARACTERS, for every report budget (D-8 / I-9). `ToolExecutor`
|
|
23
|
+
* truncates combined tool text at 100 000 BYTES; a 24 000-character CJK report
|
|
24
|
+
* is 72 000 bytes, so a character-denominated cap would let the executor chop
|
|
25
|
+
* the report mid-section and take the failure count with it.
|
|
26
|
+
*/
|
|
27
|
+
export const TEAM_LIMITS = {
|
|
28
|
+
/**
|
|
29
|
+
* The requirement's ceiling (R-g). Enforced in `clampTeamConfig` AND in
|
|
30
|
+
* `normalizeSubagentSpecs`, so neither a hand-edited file nor a model asking
|
|
31
|
+
* for thirty can exceed it.
|
|
32
|
+
*/
|
|
33
|
+
hardMaxSubagents: 10,
|
|
34
|
+
// --- Spec normalization (§3.3.1) ---
|
|
35
|
+
/** `SubagentSpec.description`, clamped (never rejected). */
|
|
36
|
+
descriptionChars: 60,
|
|
37
|
+
/** `SubagentSpec.prompt`, clamped. The child cannot see the conversation, so
|
|
38
|
+
* this has to be generous enough to carry a whole brief. */
|
|
39
|
+
promptChars: 8000,
|
|
40
|
+
/** Slugified `SubagentSpec.label`. Long enough to be meaningful in the panel,
|
|
41
|
+
* short enough that five of them fit one row of an 80-column terminal. */
|
|
42
|
+
labelChars: 12,
|
|
43
|
+
// --- Report budgeting (§3.7). BYTES. ---
|
|
44
|
+
reportMaxBytes: 48_000,
|
|
45
|
+
summaryMaxBytes: 6_000,
|
|
46
|
+
/**
|
|
47
|
+
* Floor a summary is trimmed TO, never below. A section trimmed to nothing is
|
|
48
|
+
* indistinguishable from a child that reported nothing, which is exactly the
|
|
49
|
+
* distinction the report exists to preserve.
|
|
50
|
+
*/
|
|
51
|
+
summaryFloorBytes: 400,
|
|
52
|
+
/** Deduped `write_file` / `edit_file` paths recorded per child (§3.4). */
|
|
53
|
+
filesTouchedMax: 30,
|
|
54
|
+
// --- The team bus (§3.6). The "bu neng tai pin fan" clause, in numbers. ---
|
|
55
|
+
/** `team_send` calls one child may spend on a whole dispatch. */
|
|
56
|
+
messagesPerAgent: 6,
|
|
57
|
+
/** `team_send` calls the whole dispatch may spend. */
|
|
58
|
+
messagesPerDispatch: 24,
|
|
59
|
+
/** Minimum gap between one child's sends. */
|
|
60
|
+
minSendIntervalMs: 15_000,
|
|
61
|
+
/** Quota units a `to: "all"` broadcast costs. */
|
|
62
|
+
broadcastCost: 2,
|
|
63
|
+
subjectChars: 80,
|
|
64
|
+
bodyChars: 800,
|
|
65
|
+
/** `team_wait` timeout clamp, in seconds. */
|
|
66
|
+
waitMinSeconds: 5,
|
|
67
|
+
waitMaxSeconds: 120,
|
|
68
|
+
waitDefaultSeconds: 30,
|
|
69
|
+
/**
|
|
70
|
+
* `agent_update` coalescing window (§5.2). Five children streaming tokens must
|
|
71
|
+
* not push five React renders per token; phase transitions always pass.
|
|
72
|
+
*/
|
|
73
|
+
agentUpdateThrottleMs: 120,
|
|
74
|
+
/** Agent rows the live panel renders before collapsing to `+N more` (§6.1). */
|
|
75
|
+
panelMaxRows: 5,
|
|
76
|
+
/** Below this many terminal rows the panel collapses to its header line. */
|
|
77
|
+
panelCollapseRows: 20,
|
|
78
|
+
/** Below this many columns the status-bar readout degrades to `[3]` (D-20). */
|
|
79
|
+
statusCompactCols: 100,
|
|
80
|
+
// --- Live activity (team-live-activity F-1) ---
|
|
81
|
+
/**
|
|
82
|
+
* STORAGE ceiling for a prose activity tail (P0-1). The most a `SubagentRun`
|
|
83
|
+
* may carry, NOT the width of the column: `TeamPanel.activityBudget(cols)`
|
|
84
|
+
* derives the display budget from the terminal and is always <= this.
|
|
85
|
+
*/
|
|
86
|
+
activityChars: 64,
|
|
87
|
+
/**
|
|
88
|
+
* Floor under `activityBudget`. Below roughly 46 columns the roster has bigger
|
|
89
|
+
* problems than an elided activity line, and a column narrower than this shows
|
|
90
|
+
* nothing but an ellipsis - at which point the phase word is more informative.
|
|
91
|
+
*/
|
|
92
|
+
activityMinChars: 12,
|
|
93
|
+
/** Rolling text buffer kept per child. The only per-token allocation. */
|
|
94
|
+
activityTailChars: 160,
|
|
95
|
+
/** Per picked tool argument (P1-4). A path or a command longer than this is
|
|
96
|
+
* elided on capture, so a run can never carry more than a few hundred bytes
|
|
97
|
+
* of argument however large the real call was. */
|
|
98
|
+
activityArgChars: 120,
|
|
99
|
+
/** At or above this many columns the activity line carries a full path or
|
|
100
|
+
* command; below it, a basename or a program name. Mirrors `statusCompactCols`.
|
|
101
|
+
* This chooses WHICH FACT to show; `activityBudget` chooses how much of it. */
|
|
102
|
+
activityWideCols: 100,
|
|
103
|
+
// --- Cold-start retry (team-live-activity F-4) ---
|
|
104
|
+
/** Retries of a child that produced NOTHING. One, and not configurable: a
|
|
105
|
+
* second retry doubles the worst-case latency of a failing dispatch and buys
|
|
106
|
+
* nothing a re-dispatch by the lead would not buy better. */
|
|
107
|
+
maxColdStartRetries: 1,
|
|
108
|
+
retryBackoffMs: 2_000,
|
|
109
|
+
};
|
|
110
|
+
/**
|
|
111
|
+
* The two comm tools, which are POLICY-EXEMPT inside a child (§3.11).
|
|
112
|
+
*
|
|
113
|
+
* They are not in the lead's registered tool list, so evaluating them against
|
|
114
|
+
* the lead's `allowed-tools` ceiling would refuse them and dead-end the channel
|
|
115
|
+
* — the same dead end `SKILL_TOOL_FLOOR` exists to prevent one level up.
|
|
116
|
+
*
|
|
117
|
+
* Deliberately NOT added to `HOST_TOOL_NAMES` (P2-5). That constant answers
|
|
118
|
+
* "can a skill's `allowed-tools` declaration ever take effect?", and these two
|
|
119
|
+
* are never registered on a lead — so a skill naming them genuinely IS
|
|
120
|
+
* unenforceable, and `aragon skills doctor` reporting it as such is correct
|
|
121
|
+
* rather than a gap.
|
|
122
|
+
*/
|
|
123
|
+
export const TEAM_SUBAGENT_TOOL_NAMES = new Set(['team_send', 'team_wait']);
|
|
124
|
+
/**
|
|
125
|
+
* Bumped whenever the wording of either prompt block changes, so a behaviour
|
|
126
|
+
* report can be tied to a block revision with one grep.
|
|
127
|
+
*/
|
|
128
|
+
export const TEAM_BLOCK_VERSION = 'v1-2026-07';
|