team 0.1.2 → 0.2.0

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 (114) hide show
  1. package/README.md +22 -11
  2. package/dist/approve/approval.js +14 -9
  3. package/dist/approve/fingerprint.d.ts +14 -2
  4. package/dist/approve/fingerprint.js +21 -21
  5. package/dist/budgets/gate.js +7 -2
  6. package/dist/budgets/readings.d.ts +6 -2
  7. package/dist/budgets/readings.js +28 -2
  8. package/dist/budgets/run.js +8 -2
  9. package/dist/budgets/table.d.ts +2 -0
  10. package/dist/budgets/table.js +10 -5
  11. package/dist/cli.d.ts +2 -1
  12. package/dist/cli.js +8 -5
  13. package/dist/commands/add.d.ts +0 -1
  14. package/dist/commands/add.js +7 -5
  15. package/dist/commands/approve.js +17 -1
  16. package/dist/commands/doctor.d.ts +15 -1
  17. package/dist/commands/doctor.js +126 -8
  18. package/dist/commands/down.d.ts +4 -0
  19. package/dist/commands/down.js +32 -13
  20. package/dist/commands/init.d.ts +1 -1
  21. package/dist/commands/init.js +4 -2
  22. package/dist/commands/remove.js +5 -2
  23. package/dist/commands/status.d.ts +2 -0
  24. package/dist/commands/status.js +19 -1
  25. package/dist/commands/up.js +3 -1
  26. package/dist/commands/watch.d.ts +2 -0
  27. package/dist/commands/watch.js +28 -4
  28. package/dist/conformance/adapter.d.ts +5 -0
  29. package/dist/conformance/adapter.js +44 -0
  30. package/dist/file/check.d.ts +20 -0
  31. package/dist/file/check.js +112 -0
  32. package/dist/file/lines.js +13 -2
  33. package/dist/file/sections/budgets.d.ts +15 -0
  34. package/dist/file/sections/budgets.js +180 -0
  35. package/dist/file/sections/coordinator.d.ts +2 -0
  36. package/dist/file/sections/coordinator.js +11 -0
  37. package/dist/file/sections/format.d.ts +2 -0
  38. package/dist/file/sections/format.js +13 -0
  39. package/dist/file/sections/identity.d.ts +2 -0
  40. package/dist/file/sections/identity.js +103 -0
  41. package/dist/file/sections/index.d.ts +9 -0
  42. package/dist/file/sections/index.js +43 -0
  43. package/dist/file/sections/lead.d.ts +4 -0
  44. package/dist/file/sections/lead.js +18 -0
  45. package/dist/file/sections/limits.d.ts +2 -0
  46. package/dist/file/sections/limits.js +43 -0
  47. package/dist/file/sections/machine.d.ts +2 -0
  48. package/dist/file/sections/machine.js +38 -0
  49. package/dist/file/sections/operator.d.ts +2 -0
  50. package/dist/file/sections/operator.js +11 -0
  51. package/dist/file/sections/project.d.ts +2 -0
  52. package/dist/file/sections/project.js +13 -0
  53. package/dist/file/sections/rules.d.ts +2 -0
  54. package/dist/file/sections/rules.js +9 -0
  55. package/dist/file/sections/seats.d.ts +7 -0
  56. package/dist/file/sections/seats.js +161 -0
  57. package/dist/file/sections/section.d.ts +32 -0
  58. package/dist/file/sections/section.js +7 -0
  59. package/dist/file/sections/session.d.ts +2 -0
  60. package/dist/file/sections/session.js +16 -0
  61. package/dist/file/sections/tools.d.ts +2 -0
  62. package/dist/file/sections/tools.js +43 -0
  63. package/dist/file/sections/trust.d.ts +2 -0
  64. package/dist/file/sections/trust.js +20 -0
  65. package/dist/file/sections/units.d.ts +16 -0
  66. package/dist/file/sections/units.js +8 -0
  67. package/dist/file/sections/visibility.d.ts +2 -0
  68. package/dist/file/sections/visibility.js +12 -0
  69. package/dist/file/sections/watch-checks.d.ts +9 -0
  70. package/dist/file/sections/watch-checks.js +14 -0
  71. package/dist/file/sections/watch.d.ts +14 -0
  72. package/dist/file/sections/watch.js +111 -0
  73. package/dist/file/sections/workspace.d.ts +4 -0
  74. package/dist/file/sections/workspace.js +77 -0
  75. package/dist/file/types.d.ts +1 -0
  76. package/dist/file/validate.d.ts +4 -12
  77. package/dist/file/validate.js +45 -688
  78. package/dist/herdr.d.ts +1 -0
  79. package/dist/herdr.js +11 -1
  80. package/dist/launch/agent.js +4 -3
  81. package/dist/launch/deliver.d.ts +4 -0
  82. package/dist/launch/deliver.js +151 -8
  83. package/dist/launch/execute.d.ts +2 -0
  84. package/dist/launch/execute.js +21 -11
  85. package/dist/launch/plan.js +16 -2
  86. package/dist/profiles/antigravity.yaml +12 -2
  87. package/dist/profiles/claude-code.yaml +21 -6
  88. package/dist/profiles/codex.yaml +14 -1
  89. package/dist/profiles/cursor.yaml +45 -5
  90. package/dist/profiles/overrides.d.ts +67 -0
  91. package/dist/profiles/overrides.js +190 -0
  92. package/dist/profiles/profile.d.ts +3 -0
  93. package/dist/profiles/profile.js +5 -1
  94. package/dist/state.d.ts +2 -0
  95. package/dist/status/compare.js +7 -4
  96. package/dist/store/store.d.ts +5 -0
  97. package/dist/version.d.ts +1 -0
  98. package/dist/version.js +7 -0
  99. package/dist/watch/checks/budget.js +49 -53
  100. package/dist/watch/dialect.d.ts +6 -1
  101. package/dist/watch/dialect.js +68 -12
  102. package/dist/watch/pass.d.ts +8 -1
  103. package/dist/watch/pass.js +19 -9
  104. package/dist/watch/screen-core.d.ts +41 -4
  105. package/dist/watch/screen-core.js +458 -93
  106. package/dist/watch/screen-data.d.ts +25 -0
  107. package/dist/watch/screen-file.d.ts +8 -2
  108. package/dist/watch/screen-file.js +325 -38
  109. package/dist/watch/screen-profile.d.ts +19 -0
  110. package/dist/watch/screen-profile.js +1 -0
  111. package/dist/watch/screen.d.ts +22 -4
  112. package/dist/watch/screen.js +39 -5
  113. package/package.json +9 -3
  114. package/schema/team.schema.json +498 -0
@@ -4,10 +4,10 @@
4
4
  // runs on the plain form, and the styling tells a greyed suggestion from typed text.
5
5
  import { readFileSync } from 'node:fs';
6
6
  import { stripSgr } from "../ansi.js";
7
- import { classifyLines, composeLines, statusRowOf } from "./screen-core.js";
7
+ import { classifyLines, composerBox, composeLines, foldMarked, foldOf, statusRowOf } from "./screen-core.js";
8
8
  import { loadScreen } from "./screen-file.js";
9
9
  function load(name) {
10
- return loadScreen(readFileSync(new URL(`../profiles/${name}.yaml`, import.meta.url), 'utf8'));
10
+ return loadScreen(readFileSync(new URL(`../profiles/${name}.yaml`, import.meta.url), 'utf8'), undefined, `${name}.yaml`);
11
11
  }
12
12
  // Every shipped CLI is data. The core owns the order.
13
13
  const DATA = {
@@ -16,21 +16,29 @@ const DATA = {
16
16
  cursor: load('cursor'),
17
17
  antigravity: load('antigravity'),
18
18
  };
19
+ /** The shipped screen for a CLI, or null when this version has none. */
20
+ export function screenData(cli) {
21
+ return DATA[cli] ?? null;
22
+ }
19
23
  // The window every pattern sees: the pane's last 20 lines. Styled lines are trimmed only past
20
24
  // their last escape; the core trims each line's plain form for matching.
21
25
  function windowOf(lines) {
22
26
  return lines.map((line) => line.trimEnd()).slice(-20);
23
27
  }
28
+ /** The same classification `classify` runs, against a screen that may carry added patterns. */
29
+ export function classifyData(data, lines) {
30
+ return classifyLines(data, windowOf(lines));
31
+ }
24
32
  /** Every stage, in the core's order. The first one that matches wins. */
25
33
  export function classify(cli, lines) {
26
- const data = DATA[cli];
34
+ const data = typeof cli === 'string' ? DATA[cli] : cli;
27
35
  if (!data)
28
36
  return { kind: 'unknown' };
29
37
  return classifyLines(data, windowOf(lines));
30
38
  }
31
39
  /** The composer alone. A running turn would otherwise hide an empty input box. */
32
40
  export function classifyComposer(cli, lines) {
33
- const data = DATA[cli];
41
+ const data = typeof cli === 'string' ? DATA[cli] : cli;
34
42
  if (!data)
35
43
  return { kind: 'unknown' };
36
44
  return composeLines(data, windowOf(lines));
@@ -40,12 +48,38 @@ export function readScreen(cli, screen) {
40
48
  return { kind: 'unknown' };
41
49
  return classify(cli, screen.split('\n'));
42
50
  }
51
+ /** The folded form of a paste the pane's composer shows, or null. The shape only; the caller
52
+ * verifies it holds the text it typed before trusting it. */
53
+ export function readFold(cli, screen) {
54
+ const data = typeof cli === 'string' ? DATA[cli] : cli;
55
+ if (data === undefined || screen === undefined)
56
+ return null;
57
+ return foldOf(data, windowOf(screen.split('\n')));
58
+ }
59
+ /** Whether the pane shows a fold marker at all, whatever count it names. The case `readFold`
60
+ * cannot report — a marker claiming zero hidden rows — must still keep an Enter from taking the
61
+ * box for an ordinary unsent one. */
62
+ export function readFoldMark(cli, screen) {
63
+ const data = DATA[cli];
64
+ if (data === undefined || screen === undefined)
65
+ return false;
66
+ return foldMarked(data, windowOf(screen.split('\n')));
67
+ }
68
+ /** The input box the composer draws for its current text, or null when it does not read `unsent`.
69
+ * The box's rows are the rows the pane drew for the text; the caller compares them to the text
70
+ * it typed before any Enter. */
71
+ export function readBox(cli, screen) {
72
+ const data = DATA[cli];
73
+ if (data === undefined || screen === undefined)
74
+ return null;
75
+ return composerBox(data, windowOf(screen.split('\n')));
76
+ }
43
77
  /** The one line a quota figure may come from: the composer's own status row, in the pane's last
44
78
  * 20 lines, read plain — a figure is never styled. Null for a CLI with no status line, and for
45
79
  * a window that shows no status row — a dialog, a question, a trust screen, a shell or an
46
80
  * unknown one reads no figures. */
47
81
  export function statusRow(cli, screen) {
48
- const data = DATA[cli];
82
+ const data = typeof cli === 'string' ? DATA[cli] : cli;
49
83
  if (data === undefined || screen === undefined)
50
84
  return null;
51
85
  return statusRowOf(data, windowOf(stripSgr(screen).split('\n')));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "team",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "description": "Set up, change and watch a project's team of AI agents from one file",
5
5
  "keywords": [
6
6
  "ai-agents",
@@ -16,7 +16,7 @@
16
16
  "type": "module",
17
17
  "bin": { "team": "dist/cli.js" },
18
18
  "exports": { ".": { "types": "./dist/index.d.ts", "default": "./dist/index.js" } },
19
- "files": ["dist", "examples", "README.md", "LICENSE"],
19
+ "files": ["dist", "examples", "schema", "README.md", "LICENSE"],
20
20
  "engines": { "node": ">=22" },
21
21
  "repository": { "type": "git", "url": "git+https://github.com/floor/team.git" },
22
22
  "scripts": {
@@ -24,7 +24,13 @@
24
24
  "test": "bun test",
25
25
  "e2e": "bun scripts/e2e.ts",
26
26
  "build": "bun scripts/build.ts",
27
- "ci": "bun run typecheck && bun test && bun run build && node dist/cli.js --version"
27
+ "conformance": "bun scripts/conformance.ts --impl \"node dist/cli.js conformance-adapter\"",
28
+ "coverage": "bun scripts/coverage.ts",
29
+ "coverage:check": "bun scripts/coverage.ts --check",
30
+ "schema:check": "bun scripts/schema.ts",
31
+ "contract": "bun scripts/contract.ts",
32
+ "contract:check": "bun scripts/contract.ts --check",
33
+ "ci": "bun run typecheck && bun test && bun run build && node dist/cli.js --version && bun run conformance && bun run schema:check && bun run contract:check && bun run coverage:check"
28
34
  },
29
35
  "devDependencies": {
30
36
  "@types/bun": "^1.2.0",
@@ -0,0 +1,498 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://github.com/floor/team/blob/main/schema/team.schema.json",
4
+ "title": "A team file",
5
+ "$comment": "generated from the section modules by scripts/schema.ts: the validator is the source of truth",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": [
9
+ "format",
10
+ "project",
11
+ "coordinator",
12
+ "operator",
13
+ "seats"
14
+ ],
15
+ "properties": {
16
+ "format": {
17
+ "const": 1,
18
+ "$comment": "unquoted: this version of team reads no other"
19
+ },
20
+ "project": {
21
+ "type": "string",
22
+ "minLength": 1,
23
+ "pattern": "^[A-Za-z0-9][A-Za-z0-9._-]*$"
24
+ },
25
+ "trust": {
26
+ "type": "array",
27
+ "items": {
28
+ "type": "string",
29
+ "minLength": 1
30
+ },
31
+ "$comment": "paths outside the project the seats may still work in, as the validator reads them"
32
+ },
33
+ "limits": {
34
+ "type": "object",
35
+ "additionalProperties": false,
36
+ "properties": {
37
+ "seats": {
38
+ "type": "integer",
39
+ "minimum": 1
40
+ },
41
+ "temporary": {
42
+ "type": "integer",
43
+ "minimum": 0
44
+ },
45
+ "vendors": {
46
+ "type": "object",
47
+ "additionalProperties": {
48
+ "type": "integer",
49
+ "minimum": 0
50
+ }
51
+ }
52
+ },
53
+ "$comment": "seats defaults to the declared seats plus temporary"
54
+ },
55
+ "machine": {
56
+ "type": "object",
57
+ "additionalProperties": false,
58
+ "properties": {
59
+ "load_start": {
60
+ "type": "number",
61
+ "exclusiveMinimum": 0
62
+ },
63
+ "load_max": {
64
+ "type": "number",
65
+ "exclusiveMinimum": 0
66
+ },
67
+ "memory_start": {
68
+ "type": "string",
69
+ "pattern": "^[0-9]+(?:\\.[0-9]+)?(%)$"
70
+ },
71
+ "memory_min": {
72
+ "type": "string",
73
+ "pattern": "^[0-9]+(?:\\.[0-9]+)?(%)$"
74
+ },
75
+ "disk_min": {
76
+ "type": "string",
77
+ "pattern": "^[0-9]+(?:\\.[0-9]+)?(MB|GB|TB)$"
78
+ },
79
+ "swap_free_min": {
80
+ "type": "string",
81
+ "pattern": "^[0-9]+(?:\\.[0-9]+)?(MB|GB|TB)$"
82
+ },
83
+ "swap_growth_max": {
84
+ "type": "string",
85
+ "pattern": "^[0-9]+(?:\\.[0-9]+)?(MB|GB|TB)$"
86
+ },
87
+ "swap_growth_window": {
88
+ "type": "string",
89
+ "pattern": "^[0-9]+(?:\\.[0-9]+)?(s|m|h)$"
90
+ }
91
+ }
92
+ },
93
+ "rules": {
94
+ "type": "array",
95
+ "items": {
96
+ "type": "string",
97
+ "minLength": 1
98
+ }
99
+ },
100
+ "identity": {
101
+ "type": "object",
102
+ "additionalProperties": false,
103
+ "properties": {
104
+ "signature": {
105
+ "type": "object",
106
+ "additionalProperties": false,
107
+ "properties": {
108
+ "template": {
109
+ "type": "string"
110
+ },
111
+ "commits": {
112
+ "type": "object",
113
+ "additionalProperties": false,
114
+ "properties": {
115
+ "template": {
116
+ "type": "string"
117
+ },
118
+ "position": {
119
+ "enum": [
120
+ "last-line",
121
+ "trailer",
122
+ "anywhere"
123
+ ]
124
+ },
125
+ "exempt": {
126
+ "type": "array",
127
+ "items": {
128
+ "const": "merge"
129
+ }
130
+ }
131
+ }
132
+ },
133
+ "pull_requests": {
134
+ "type": "object",
135
+ "additionalProperties": false,
136
+ "properties": {
137
+ "template": {
138
+ "type": "string"
139
+ },
140
+ "position": {
141
+ "enum": [
142
+ "last-line",
143
+ "trailer",
144
+ "anywhere"
145
+ ]
146
+ }
147
+ }
148
+ }
149
+ }
150
+ },
151
+ "humans": {
152
+ "type": "array",
153
+ "items": {
154
+ "type": "string",
155
+ "minLength": 1
156
+ }
157
+ },
158
+ "since": {
159
+ "type": "string"
160
+ },
161
+ "forbidden": {
162
+ "type": "array",
163
+ "items": {
164
+ "type": "string"
165
+ }
166
+ },
167
+ "forbidden_public": {
168
+ "type": "array",
169
+ "items": {
170
+ "type": "string"
171
+ }
172
+ }
173
+ },
174
+ "$comment": "forbidden and forbidden_public are regular expressions; a template in the position \"trailer\" must read as a git trailer"
175
+ },
176
+ "workspace": {
177
+ "type": "object",
178
+ "additionalProperties": false,
179
+ "properties": {
180
+ "mode": {
181
+ "enum": [
182
+ "worktree",
183
+ "shared"
184
+ ]
185
+ },
186
+ "path": {
187
+ "type": "string",
188
+ "pattern": "(^|/)\\{task\\}$"
189
+ },
190
+ "branch": {
191
+ "type": "string"
192
+ },
193
+ "base": {
194
+ "type": "string"
195
+ },
196
+ "setup": {
197
+ "type": "array",
198
+ "items": {
199
+ "type": "string"
200
+ }
201
+ },
202
+ "remove": {
203
+ "enum": [
204
+ "on-merge",
205
+ "manual"
206
+ ]
207
+ },
208
+ "protected": {
209
+ "type": "array",
210
+ "items": {
211
+ "type": "string"
212
+ }
213
+ },
214
+ "limit": {
215
+ "type": "integer",
216
+ "minimum": 1
217
+ }
218
+ },
219
+ "$comment": "path ends with the segment {task} under a fixed folder of its own, inside a trust pattern; branch defaults to {task}, base and path are required when a seat works in worktrees — the validator checks what a pattern cannot say"
220
+ },
221
+ "coordinator": {
222
+ "type": "string",
223
+ "minLength": 1,
224
+ "$comment": "must name a declared seat: one, without count, neither parked nor stopped"
225
+ },
226
+ "operator": {
227
+ "type": "string",
228
+ "minLength": 1,
229
+ "$comment": "must name a declared seat: one, without count, neither parked nor stopped"
230
+ },
231
+ "session": {
232
+ "type": "string",
233
+ "not": {
234
+ "const": "default"
235
+ }
236
+ },
237
+ "visibility": {
238
+ "enum": [
239
+ "public",
240
+ "private"
241
+ ]
242
+ },
243
+ "tools": {
244
+ "type": "object",
245
+ "additionalProperties": {
246
+ "type": "object",
247
+ "properties": {
248
+ "kind": {
249
+ "type": "string",
250
+ "minLength": 1
251
+ }
252
+ },
253
+ "required": [
254
+ "kind"
255
+ ],
256
+ "additionalProperties": {
257
+ "type": "string"
258
+ }
259
+ },
260
+ "$comment": "a tool is a name, then its kind and where it is; every field but the kind is text"
261
+ },
262
+ "budgets": {
263
+ "type": "object",
264
+ "additionalProperties": false,
265
+ "properties": {
266
+ "stale_after": {
267
+ "type": "string",
268
+ "pattern": "^[0-9]+(?:\\.[0-9]+)?(s|m|h)$"
269
+ },
270
+ "check_every": {
271
+ "type": "string",
272
+ "pattern": "^[0-9]+(?:\\.[0-9]+)?(s|m|h)$"
273
+ },
274
+ "marks": {
275
+ "type": "array",
276
+ "minItems": 1,
277
+ "items": {
278
+ "type": "number",
279
+ "exclusiveMinimum": 0,
280
+ "maximum": 100
281
+ }
282
+ },
283
+ "accounts": {
284
+ "type": "object",
285
+ "additionalProperties": {
286
+ "type": "object",
287
+ "additionalProperties": false,
288
+ "properties": {
289
+ "kind": {
290
+ "enum": [
291
+ "subscription",
292
+ "spend"
293
+ ]
294
+ },
295
+ "shared": {
296
+ "type": "boolean"
297
+ },
298
+ "reserve": {
299
+ "type": "string",
300
+ "pattern": "^[0-9]+(?:\\.[0-9]+)?(%)$"
301
+ },
302
+ "floor": {
303
+ "type": "string",
304
+ "pattern": "^[0-9]+(?:\\.[0-9]{1,4})? [A-Z]{3}$"
305
+ },
306
+ "sources": {
307
+ "type": "array",
308
+ "minItems": 1,
309
+ "items": {
310
+ "enum": [
311
+ "check",
312
+ "status_line"
313
+ ]
314
+ }
315
+ },
316
+ "check": {
317
+ "type": "string",
318
+ "pattern": "^\\S+$"
319
+ }
320
+ },
321
+ "required": [
322
+ "kind"
323
+ ],
324
+ "allOf": [
325
+ {
326
+ "if": {
327
+ "properties": {
328
+ "kind": {
329
+ "const": "subscription"
330
+ }
331
+ },
332
+ "required": [
333
+ "kind"
334
+ ]
335
+ },
336
+ "then": {
337
+ "required": [
338
+ "reserve"
339
+ ]
340
+ },
341
+ "else": {
342
+ "required": [
343
+ "floor"
344
+ ]
345
+ }
346
+ }
347
+ ]
348
+ },
349
+ "$comment": "a subscription has a reserve, a spend account a floor; a spend account never reads a screen"
350
+ }
351
+ }
352
+ },
353
+ "watch": {
354
+ "type": "object",
355
+ "additionalProperties": false,
356
+ "properties": {
357
+ "interval": {
358
+ "type": "string",
359
+ "pattern": "^[0-9]+(?:\\.[0-9]+)?(s|m|h)$"
360
+ },
361
+ "idle_first": {
362
+ "type": "string",
363
+ "pattern": "^[0-9]+(?:\\.[0-9]+)?(s|m|h)$"
364
+ },
365
+ "idle_repeat": {
366
+ "type": "string",
367
+ "pattern": "^[0-9]+(?:\\.[0-9]+)?(s|m|h)$"
368
+ },
369
+ "team_idle": {
370
+ "type": "string",
371
+ "pattern": "^[0-9]+(?:\\.[0-9]+)?(s|m|h)$"
372
+ },
373
+ "nudge_wait": {
374
+ "type": "string",
375
+ "pattern": "^[0-9]+(?:\\.[0-9]+)?(s|m|h)$"
376
+ },
377
+ "unsent_after": {
378
+ "type": "string",
379
+ "pattern": "^[0-9]+(?:\\.[0-9]+)?(s|m|h)$"
380
+ },
381
+ "quota_marks": {
382
+ "type": "array",
383
+ "items": {
384
+ "type": "number",
385
+ "exclusiveMinimum": 0,
386
+ "maximum": 100
387
+ },
388
+ "deprecated": true,
389
+ "$comment": "now budgets.marks, and still read"
390
+ },
391
+ "checks": {
392
+ "type": "object",
393
+ "propertyNames": {
394
+ "enum": [
395
+ "unsent",
396
+ "idle",
397
+ "extra",
398
+ "team-idle",
399
+ "load",
400
+ "memory",
401
+ "disk",
402
+ "swap-free",
403
+ "swap-growth",
404
+ "budget"
405
+ ]
406
+ },
407
+ "additionalProperties": {
408
+ "const": "off"
409
+ },
410
+ "$comment": "the four that always run (attention, missing, model-drift, approval) are refused here too"
411
+ }
412
+ }
413
+ },
414
+ "seats": {
415
+ "type": "array",
416
+ "minItems": 1,
417
+ "items": {
418
+ "type": "object",
419
+ "additionalProperties": false,
420
+ "properties": {
421
+ "role": {
422
+ "type": "string",
423
+ "minLength": 1
424
+ },
425
+ "name": {
426
+ "type": "string",
427
+ "pattern": "^[A-Za-z0-9][A-Za-z0-9._-]*$"
428
+ },
429
+ "cli": {
430
+ "enum": [
431
+ "claude-code",
432
+ "codex",
433
+ "cursor",
434
+ "grok",
435
+ "antigravity"
436
+ ]
437
+ },
438
+ "vendor": {
439
+ "type": "string",
440
+ "minLength": 1
441
+ },
442
+ "account": {
443
+ "type": "string"
444
+ },
445
+ "model": {
446
+ "type": "string",
447
+ "minLength": 1
448
+ },
449
+ "version": {
450
+ "type": "string",
451
+ "$comment": "quoted: unquoted, YAML reads 4.10 as the number 4.1"
452
+ },
453
+ "display": {
454
+ "type": "string",
455
+ "$comment": "defaults to \"model version\"; must contain the version as a token of its own"
456
+ },
457
+ "launch": {
458
+ "type": "string",
459
+ "minLength": 1
460
+ },
461
+ "cwd": {
462
+ "type": "string",
463
+ "$comment": "relative to the project, or a trust path"
464
+ },
465
+ "label": {
466
+ "type": "string"
467
+ },
468
+ "mode": {
469
+ "enum": [
470
+ "worktree",
471
+ "shared"
472
+ ]
473
+ },
474
+ "parked": {
475
+ "type": "boolean"
476
+ },
477
+ "stopped": {
478
+ "type": "boolean"
479
+ },
480
+ "count": {
481
+ "type": "integer",
482
+ "minimum": 1
483
+ }
484
+ },
485
+ "required": [
486
+ "role",
487
+ "name",
488
+ "cli",
489
+ "vendor",
490
+ "model",
491
+ "version",
492
+ "launch"
493
+ ],
494
+ "$comment": "account must name a key of budgets.accounts; label defaults to the model and version; a seat with count is neither lead"
495
+ }
496
+ }
497
+ }
498
+ }