team 0.0.1 → 0.1.1

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 (169) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +310 -0
  3. package/dist/approve/approval.d.ts +38 -0
  4. package/dist/approve/approval.js +78 -0
  5. package/dist/approve/diff.d.ts +13 -0
  6. package/dist/approve/diff.js +47 -0
  7. package/dist/approve/fingerprint.d.ts +44 -0
  8. package/dist/approve/fingerprint.js +109 -0
  9. package/dist/args.d.ts +7 -0
  10. package/dist/args.js +22 -0
  11. package/dist/budgets/checks.d.ts +35 -0
  12. package/dist/budgets/checks.js +77 -0
  13. package/dist/budgets/gate.d.ts +24 -0
  14. package/dist/budgets/gate.js +127 -0
  15. package/dist/budgets/readings.d.ts +69 -0
  16. package/dist/budgets/readings.js +156 -0
  17. package/dist/budgets/run.d.ts +66 -0
  18. package/dist/budgets/run.js +119 -0
  19. package/dist/budgets/table.d.ts +24 -0
  20. package/dist/budgets/table.js +90 -0
  21. package/dist/caller.d.ts +48 -0
  22. package/dist/caller.js +126 -0
  23. package/dist/check/config.d.ts +51 -0
  24. package/dist/check/config.js +50 -0
  25. package/dist/check/git.d.ts +27 -0
  26. package/dist/check/git.js +97 -0
  27. package/dist/check/message.d.ts +27 -0
  28. package/dist/check/message.js +101 -0
  29. package/dist/check/run.d.ts +31 -0
  30. package/dist/check/run.js +77 -0
  31. package/dist/check/signature.d.ts +10 -0
  32. package/dist/check/signature.js +22 -0
  33. package/dist/cli.d.ts +9 -0
  34. package/dist/cli.js +76 -0
  35. package/dist/clis.d.ts +7 -0
  36. package/dist/clis.js +9 -0
  37. package/dist/commands/add.d.ts +23 -0
  38. package/dist/commands/add.js +445 -0
  39. package/dist/commands/approve.d.ts +11 -0
  40. package/dist/commands/approve.js +146 -0
  41. package/dist/commands/check.d.ts +17 -0
  42. package/dist/commands/check.js +103 -0
  43. package/dist/commands/doctor.d.ts +38 -0
  44. package/dist/commands/doctor.js +232 -0
  45. package/dist/commands/down.d.ts +37 -0
  46. package/dist/commands/down.js +258 -0
  47. package/dist/commands/init.d.ts +9 -0
  48. package/dist/commands/init.js +138 -0
  49. package/dist/commands/remove.d.ts +23 -0
  50. package/dist/commands/remove.js +235 -0
  51. package/dist/commands/status.d.ts +41 -0
  52. package/dist/commands/status.js +147 -0
  53. package/dist/commands/up.d.ts +49 -0
  54. package/dist/commands/up.js +362 -0
  55. package/dist/commands/watch.d.ts +35 -0
  56. package/dist/commands/watch.js +324 -0
  57. package/dist/commands/worktree.d.ts +12 -0
  58. package/dist/commands/worktree.js +388 -0
  59. package/dist/end/condition.d.ts +28 -0
  60. package/dist/end/condition.js +77 -0
  61. package/dist/file/current.d.ts +13 -0
  62. package/dist/file/current.js +53 -0
  63. package/dist/file/lines.d.ts +25 -0
  64. package/dist/file/lines.js +277 -0
  65. package/dist/file/load.d.ts +7 -0
  66. package/dist/file/load.js +90 -0
  67. package/dist/file/paths.d.ts +6 -0
  68. package/dist/file/paths.js +77 -0
  69. package/dist/file/secrets.d.ts +6 -0
  70. package/dist/file/secrets.js +54 -0
  71. package/dist/file/signature.d.ts +13 -0
  72. package/dist/file/signature.js +36 -0
  73. package/dist/file/types.d.ts +130 -0
  74. package/dist/file/types.js +1 -0
  75. package/dist/file/validate.d.ts +13 -0
  76. package/dist/file/validate.js +702 -0
  77. package/dist/file/write.d.ts +11 -0
  78. package/dist/file/write.js +13 -0
  79. package/dist/herdr.d.ts +38 -0
  80. package/dist/herdr.js +227 -0
  81. package/dist/index.d.ts +10 -0
  82. package/dist/index.js +6 -0
  83. package/dist/io.d.ts +10 -0
  84. package/dist/io.js +1 -0
  85. package/dist/launch/deliver.d.ts +10 -0
  86. package/dist/launch/deliver.js +39 -0
  87. package/dist/launch/execute.d.ts +46 -0
  88. package/dist/launch/execute.js +284 -0
  89. package/dist/launch/plan.d.ts +185 -0
  90. package/dist/launch/plan.js +280 -0
  91. package/dist/launch/rules.d.ts +25 -0
  92. package/dist/launch/rules.js +36 -0
  93. package/dist/log.d.ts +1 -0
  94. package/dist/log.js +24 -0
  95. package/dist/profiles/antigravity.yaml +66 -0
  96. package/dist/profiles/claude-code.yaml +69 -0
  97. package/dist/profiles/codex.yaml +77 -0
  98. package/dist/profiles/cursor.yaml +68 -0
  99. package/dist/profiles/index.d.ts +1 -0
  100. package/dist/profiles/index.js +1 -0
  101. package/dist/profiles/profile.d.ts +60 -0
  102. package/dist/profiles/profile.js +313 -0
  103. package/dist/profiles/quota.d.ts +27 -0
  104. package/dist/profiles/quota.js +57 -0
  105. package/dist/state.d.ts +61 -0
  106. package/dist/state.js +127 -0
  107. package/dist/status/compare.d.ts +26 -0
  108. package/dist/status/compare.js +110 -0
  109. package/dist/status/statusline.d.ts +9 -0
  110. package/dist/status/statusline.js +26 -0
  111. package/dist/store/store.d.ts +62 -0
  112. package/dist/store/store.js +116 -0
  113. package/dist/watch/check.d.ts +82 -0
  114. package/dist/watch/check.js +29 -0
  115. package/dist/watch/checks/approval.d.ts +2 -0
  116. package/dist/watch/checks/approval.js +16 -0
  117. package/dist/watch/checks/attention.d.ts +2 -0
  118. package/dist/watch/checks/attention.js +23 -0
  119. package/dist/watch/checks/budget.d.ts +2 -0
  120. package/dist/watch/checks/budget.js +141 -0
  121. package/dist/watch/checks/disk.d.ts +2 -0
  122. package/dist/watch/checks/disk.js +13 -0
  123. package/dist/watch/checks/extra.d.ts +2 -0
  124. package/dist/watch/checks/extra.js +18 -0
  125. package/dist/watch/checks/idle.d.ts +2 -0
  126. package/dist/watch/checks/idle.js +32 -0
  127. package/dist/watch/checks/load.d.ts +2 -0
  128. package/dist/watch/checks/load.js +11 -0
  129. package/dist/watch/checks/memory.d.ts +2 -0
  130. package/dist/watch/checks/memory.js +11 -0
  131. package/dist/watch/checks/missing.d.ts +2 -0
  132. package/dist/watch/checks/missing.js +12 -0
  133. package/dist/watch/checks/model-drift.d.ts +2 -0
  134. package/dist/watch/checks/model-drift.js +14 -0
  135. package/dist/watch/checks/swap-free.d.ts +2 -0
  136. package/dist/watch/checks/swap-free.js +12 -0
  137. package/dist/watch/checks/swap-growth.d.ts +2 -0
  138. package/dist/watch/checks/swap-growth.js +18 -0
  139. package/dist/watch/checks/team-idle.d.ts +2 -0
  140. package/dist/watch/checks/team-idle.js +18 -0
  141. package/dist/watch/checks/unsent.d.ts +2 -0
  142. package/dist/watch/checks/unsent.js +18 -0
  143. package/dist/watch/close.d.ts +19 -0
  144. package/dist/watch/close.js +40 -0
  145. package/dist/watch/dialect.d.ts +5 -0
  146. package/dist/watch/dialect.js +430 -0
  147. package/dist/watch/end.d.ts +11 -0
  148. package/dist/watch/end.js +17 -0
  149. package/dist/watch/machine.d.ts +38 -0
  150. package/dist/watch/machine.js +137 -0
  151. package/dist/watch/notify.d.ts +1 -0
  152. package/dist/watch/notify.js +15 -0
  153. package/dist/watch/pass.d.ts +42 -0
  154. package/dist/watch/pass.js +242 -0
  155. package/dist/watch/screen-core.d.ts +20 -0
  156. package/dist/watch/screen-core.js +356 -0
  157. package/dist/watch/screen-data.d.ts +65 -0
  158. package/dist/watch/screen-data.js +1 -0
  159. package/dist/watch/screen-file.d.ts +2 -0
  160. package/dist/watch/screen-file.js +269 -0
  161. package/dist/watch/screen.d.ts +24 -0
  162. package/dist/watch/screen.js +47 -0
  163. package/dist/worktree/place.d.ts +18 -0
  164. package/dist/worktree/place.js +126 -0
  165. package/dist/yaml.d.ts +30 -0
  166. package/dist/yaml.js +380 -0
  167. package/examples/checks/codex-quota +217 -0
  168. package/examples/team.yaml +76 -0
  169. package/package.json +29 -10
package/dist/yaml.js ADDED
@@ -0,0 +1,380 @@
1
+ // A parser for the YAML subset a team file is written in: block maps and sequences, flow maps
2
+ // and sequences on one line, plain and quoted scalars, comments. Anything else is refused with
3
+ // its line number, since this reads a file that runs commands.
4
+ export class YamlError extends Error {
5
+ line;
6
+ constructor(line, message) {
7
+ super(message);
8
+ this.name = 'YamlError';
9
+ this.line = line;
10
+ }
11
+ }
12
+ export function parseYaml(text) {
13
+ const lines = readLines(text);
14
+ if (!lines.length)
15
+ throw new YamlError(1, 'the file is empty');
16
+ const state = { lines, at: 0 };
17
+ const first = lines[0];
18
+ if (first.indent !== 0)
19
+ throw new YamlError(first.n, 'the first line must not be indented');
20
+ const node = parseBlock(state, 0);
21
+ const rest = state.lines[state.at];
22
+ if (rest)
23
+ throw new YamlError(rest.n, 'unexpected indentation');
24
+ return node;
25
+ }
26
+ export function toValue(node) {
27
+ if (node.kind === 'scalar')
28
+ return node.value;
29
+ if (node.kind === 'seq')
30
+ return node.items.map(toValue);
31
+ const out = {};
32
+ for (const entry of node.entries)
33
+ out[entry.key] = toValue(entry.value);
34
+ return out;
35
+ }
36
+ function readLines(text) {
37
+ const lines = [];
38
+ const source = text.replace(/^/, '').split(/\r?\n/);
39
+ for (let i = 0; i < source.length; i++) {
40
+ const n = i + 1;
41
+ const full = source[i];
42
+ const indent = full.length - full.trimStart().length;
43
+ if (full.slice(0, indent).includes('\t'))
44
+ throw new YamlError(n, 'a tab in the indentation: use spaces');
45
+ const body = stripComment(full.slice(indent), n).trimEnd();
46
+ if (!body)
47
+ continue;
48
+ if (body === '---' || body === '...' || body.startsWith('--- ')) {
49
+ throw new YamlError(n, 'document markers are not supported: one document per file');
50
+ }
51
+ if (body.startsWith('%'))
52
+ throw new YamlError(n, 'directives are not supported');
53
+ lines.push({ n, indent, text: body });
54
+ }
55
+ return lines;
56
+ }
57
+ // A comment starts at a `#` that begins the line or follows a space, outside quotes.
58
+ function stripComment(text, n) {
59
+ let quote = '';
60
+ for (let i = 0; i < text.length; i++) {
61
+ const c = text[i];
62
+ if (quote) {
63
+ if (quote === '"' && c === '\\')
64
+ i++;
65
+ else if (c === quote) {
66
+ if (quote === "'" && text[i + 1] === "'")
67
+ i++;
68
+ else
69
+ quote = '';
70
+ }
71
+ }
72
+ else if ((c === '"' || c === "'") && startsScalar(text, i))
73
+ quote = c;
74
+ else if (c === '#' && (i === 0 || text[i - 1] === ' ' || text[i - 1] === '\t'))
75
+ return text.slice(0, i);
76
+ }
77
+ if (quote)
78
+ throw new YamlError(n, 'a quoted string is not closed on its line');
79
+ return text;
80
+ }
81
+ // A quote opens a string only where a scalar can start; inside a plain scalar it is a character.
82
+ function startsScalar(text, i) {
83
+ const before = text.slice(0, i).trimEnd();
84
+ return before === '' || /[:,\[{-]$/.test(before);
85
+ }
86
+ function parseBlock(state, indent) {
87
+ const line = state.lines[state.at];
88
+ return isItem(line.text) ? parseSeq(state, indent) : parseMap(state, indent);
89
+ }
90
+ function isItem(text) {
91
+ return text === '-' || text.startsWith('- ');
92
+ }
93
+ function parseMap(state, indent) {
94
+ const start = state.lines[state.at];
95
+ const entries = [];
96
+ const seen = new Set();
97
+ for (;;) {
98
+ const line = state.lines[state.at];
99
+ if (!line || line.indent < indent)
100
+ break;
101
+ if (line.indent > indent)
102
+ throw new YamlError(line.n, 'unexpected indentation');
103
+ if (isItem(line.text))
104
+ throw new YamlError(line.n, 'a list item where a key was expected');
105
+ const split = splitKey(line.text, line.n);
106
+ if (!split)
107
+ throw new YamlError(line.n, `expected "key: value", found "${clip(line.text)}"`);
108
+ if (seen.has(split.key))
109
+ throw new YamlError(line.n, `duplicate key "${split.key}"`);
110
+ seen.add(split.key);
111
+ state.at++;
112
+ entries.push({ key: split.key, line: line.n, value: valueAfterKey(state, indent, split.rest, line.n) });
113
+ }
114
+ return { kind: 'map', line: start.n, entries };
115
+ }
116
+ function valueAfterKey(state, indent, rest, n) {
117
+ if (rest)
118
+ return parseInline(rest, n);
119
+ const next = state.lines[state.at];
120
+ if (next && next.indent > indent)
121
+ return parseBlock(state, next.indent);
122
+ // A list may sit at its key's own indentation.
123
+ if (next && next.indent === indent && isItem(next.text))
124
+ return parseSeq(state, indent);
125
+ return { kind: 'scalar', line: n, value: null, quoted: false, raw: '' };
126
+ }
127
+ function parseSeq(state, indent) {
128
+ const start = state.lines[state.at];
129
+ const items = [];
130
+ for (;;) {
131
+ const line = state.lines[state.at];
132
+ if (!line || line.indent < indent)
133
+ break;
134
+ if (line.indent > indent)
135
+ throw new YamlError(line.n, 'unexpected indentation');
136
+ if (!isItem(line.text))
137
+ break;
138
+ const after = line.text.slice(1);
139
+ const rest = after.trimStart();
140
+ if (!rest) {
141
+ state.at++;
142
+ const next = state.lines[state.at];
143
+ if (next && next.indent > indent)
144
+ items.push(parseBlock(state, next.indent));
145
+ else
146
+ items.push({ kind: 'scalar', line: line.n, value: null, quoted: false, raw: '' });
147
+ }
148
+ else if (isItem(rest)) {
149
+ throw new YamlError(line.n, 'a list directly inside a list item is not supported');
150
+ }
151
+ else if (splitKey(rest, line.n)) {
152
+ // "- key: value" opens a map whose keys align with the first one.
153
+ const inner = indent + 1 + (after.length - rest.length);
154
+ state.lines[state.at] = { n: line.n, indent: inner, text: rest };
155
+ items.push(parseMap(state, inner));
156
+ }
157
+ else {
158
+ state.at++;
159
+ items.push(parseInline(rest, line.n));
160
+ }
161
+ }
162
+ return { kind: 'seq', line: start.n, items };
163
+ }
164
+ // Splits "key: rest" or "key:". Returns null when the text is not a map entry.
165
+ function splitKey(text, n) {
166
+ const c = text[0];
167
+ if (c === '"' || c === "'") {
168
+ const read = readQuoted(text, 0, n);
169
+ const tail = text.slice(read.end);
170
+ if (tail === ':')
171
+ return { key: read.value, rest: '' };
172
+ if (tail.startsWith(': '))
173
+ return { key: read.value, rest: tail.slice(2).trim() };
174
+ return null;
175
+ }
176
+ if (c === '{' || c === '[')
177
+ return null;
178
+ if (c === '?' && (text.length === 1 || text[1] === ' '))
179
+ throw new YamlError(n, 'complex keys ("? ") are not supported');
180
+ for (let i = 0; i < text.length; i++) {
181
+ if (text[i] !== ':')
182
+ continue;
183
+ if (i === text.length - 1)
184
+ return checkKey(text.slice(0, i), '', n);
185
+ if (text[i + 1] === ' ')
186
+ return checkKey(text.slice(0, i), text.slice(i + 2).trim(), n);
187
+ }
188
+ return null;
189
+ }
190
+ function checkKey(key, rest, n) {
191
+ const trimmed = key.trimEnd();
192
+ if (!trimmed)
193
+ throw new YamlError(n, 'an empty key');
194
+ refuseIndicator(trimmed, n);
195
+ return { key: trimmed, rest };
196
+ }
197
+ function parseInline(text, n) {
198
+ const c = text[0];
199
+ // "a: b: c" and "a: - x" are not values in YAML; a full parser refuses them too.
200
+ if (c !== '"' && c !== "'" && c !== '{' && c !== '[') {
201
+ if (/: |:$/.test(text))
202
+ throw new YamlError(n, 'a value that contains ": " must be quoted');
203
+ if (isItem(text))
204
+ throw new YamlError(n, 'a list can\'t start on its key\'s line: put each "- item" on a line of its own');
205
+ }
206
+ if (c === '{' || c === '[') {
207
+ const read = readFlow(text, 0, n);
208
+ if (text.slice(read.end).trim())
209
+ throw new YamlError(n, `unexpected text after "${c === '{' ? '}' : ']'}"`);
210
+ return read.node;
211
+ }
212
+ if (c === '"' || c === "'") {
213
+ const read = readQuoted(text, 0, n);
214
+ if (text.slice(read.end).trim())
215
+ throw new YamlError(n, 'unexpected text after a quoted string');
216
+ return { kind: 'scalar', line: n, value: read.value, quoted: true, raw: text.slice(0, read.end) };
217
+ }
218
+ return plain(text, n);
219
+ }
220
+ function plain(text, n) {
221
+ refuseIndicator(text, n);
222
+ return { kind: 'scalar', line: n, value: resolve(text), quoted: false, raw: text };
223
+ }
224
+ function refuseIndicator(text, n) {
225
+ const c = text[0];
226
+ if (c === '&')
227
+ throw new YamlError(n, 'anchors ("&") are not supported');
228
+ if (c === '*')
229
+ throw new YamlError(n, 'aliases ("*") are not supported: quote a value that starts with "*"');
230
+ if (c === '!')
231
+ throw new YamlError(n, 'tags ("!") are not supported');
232
+ if (c === '|' || c === '>')
233
+ throw new YamlError(n, 'block scalars ("|", ">") are not supported: write the value on one line');
234
+ if (c === '@' || c === '`' || c === '%')
235
+ throw new YamlError(n, `a value can't start with "${c}": quote it`);
236
+ if (text === '<<')
237
+ throw new YamlError(n, 'merge keys ("<<") are not supported');
238
+ }
239
+ // YAML 1.2 core schema.
240
+ function resolve(text) {
241
+ if (text === '~' || /^(null|Null|NULL)$/.test(text))
242
+ return null;
243
+ if (/^(true|True|TRUE)$/.test(text))
244
+ return true;
245
+ if (/^(false|False|FALSE)$/.test(text))
246
+ return false;
247
+ if (/^[-+]?[0-9]+$/.test(text))
248
+ return Number(text);
249
+ if (/^0o[0-7]+$/.test(text))
250
+ return parseInt(text.slice(2), 8);
251
+ if (/^0x[0-9a-fA-F]+$/.test(text))
252
+ return parseInt(text.slice(2), 16);
253
+ if (/^[-+]?(\.[0-9]+|[0-9]+(\.[0-9]*)?)([eE][-+]?[0-9]+)?$/.test(text))
254
+ return Number(text);
255
+ if (/^[-+]?\.(inf|Inf|INF)$/.test(text))
256
+ return text.startsWith('-') ? -Infinity : Infinity;
257
+ if (/^\.(nan|NaN|NAN)$/.test(text))
258
+ return NaN;
259
+ return text;
260
+ }
261
+ const ESCAPES = {
262
+ '0': '\0', a: '\x07', b: '\b', t: '\t', n: '\n', v: '\v', f: '\f', r: '\r', e: '\x1b',
263
+ ' ': ' ', '"': '"', '/': '/', '\\': '\\', N: '\x85', _: '\xa0', L: '
', P: '
',
264
+ };
265
+ function readQuoted(text, at, n) {
266
+ const quote = text[at];
267
+ let out = '';
268
+ for (let i = at + 1; i < text.length; i++) {
269
+ const c = text[i];
270
+ if (quote === "'") {
271
+ if (c !== "'")
272
+ out += c;
273
+ else if (text[i + 1] === "'") {
274
+ out += "'";
275
+ i++;
276
+ }
277
+ else
278
+ return { value: out, end: i + 1 };
279
+ }
280
+ else if (c === '"') {
281
+ return { value: out, end: i + 1 };
282
+ }
283
+ else if (c === '\\') {
284
+ const e = text[++i];
285
+ const width = e === 'x' ? 2 : e === 'u' ? 4 : e === 'U' ? 8 : 0;
286
+ if (width) {
287
+ const hex = text.slice(i + 1, i + 1 + width);
288
+ if (!new RegExp(`^[0-9a-fA-F]{${width}}$`).test(hex))
289
+ throw new YamlError(n, `a bad "\\${e}" escape`);
290
+ out += String.fromCodePoint(parseInt(hex, 16));
291
+ i += width;
292
+ }
293
+ else if (e !== undefined && e in ESCAPES)
294
+ out += ESCAPES[e];
295
+ else
296
+ throw new YamlError(n, `an unknown escape "\\${e ?? ''}" in a double-quoted string`);
297
+ }
298
+ else
299
+ out += c;
300
+ }
301
+ throw new YamlError(n, 'a quoted string is not closed on its line');
302
+ }
303
+ function readFlow(text, at, n) {
304
+ const open = text[at];
305
+ const close = open === '{' ? '}' : ']';
306
+ const entries = [];
307
+ const items = [];
308
+ const seen = new Set();
309
+ let i = skipSpaces(text, at + 1);
310
+ for (;;) {
311
+ if (i >= text.length)
312
+ throw new YamlError(n, `"${open}" is not closed on its line`);
313
+ if (text[i] === close) {
314
+ const node = open === '{' ? { kind: 'map', line: n, entries } : { kind: 'seq', line: n, items };
315
+ return { node, end: i + 1 };
316
+ }
317
+ if (open === '{') {
318
+ const key = readFlowScalar(text, i, n, true);
319
+ if (key.node.kind !== 'scalar')
320
+ throw new YamlError(n, 'a key in "{ }" must be a plain or quoted word');
321
+ i = skipSpaces(text, key.end);
322
+ if (text[i] !== ':')
323
+ throw new YamlError(n, 'expected "key: value" inside "{ }"');
324
+ const name = key.node.quoted ? String(key.node.value) : key.node.raw;
325
+ if (seen.has(name))
326
+ throw new YamlError(n, `duplicate key "${name}"`);
327
+ seen.add(name);
328
+ i = skipSpaces(text, i + 1);
329
+ const value = readFlowScalar(text, i, n, false);
330
+ entries.push({ key: name, line: n, value: value.node });
331
+ i = skipSpaces(text, value.end);
332
+ }
333
+ else {
334
+ const value = readFlowScalar(text, i, n, false);
335
+ items.push(value.node);
336
+ i = skipSpaces(text, value.end);
337
+ }
338
+ if (text[i] === ',')
339
+ i = skipSpaces(text, i + 1);
340
+ else if (text[i] !== close)
341
+ throw new YamlError(n, `expected "," or "${close}"`);
342
+ }
343
+ }
344
+ function readFlowScalar(text, at, n, key) {
345
+ const c = text[at];
346
+ if (c === '{' || c === '[') {
347
+ if (key)
348
+ throw new YamlError(n, 'a key in "{ }" must be a plain or quoted word');
349
+ return readFlow(text, at, n);
350
+ }
351
+ if (c === '"' || c === "'") {
352
+ const read = readQuoted(text, at, n);
353
+ return { node: { kind: 'scalar', line: n, value: read.value, quoted: true, raw: text.slice(at, read.end) }, end: read.end };
354
+ }
355
+ let end = at;
356
+ while (end < text.length) {
357
+ const d = text[end];
358
+ if (d === ',' || d === ']' || d === '}' || d === '[' || d === '{')
359
+ break;
360
+ if (d === ':' && (key || text[end + 1] === ' ' || end + 1 === text.length)) {
361
+ if (!key)
362
+ throw new YamlError(n, 'a ": " inside a plain value in "[ ]" or "{ }": quote it');
363
+ break;
364
+ }
365
+ end++;
366
+ }
367
+ const raw = text.slice(at, end).trim();
368
+ if (!raw)
369
+ throw new YamlError(n, 'an empty value inside "[ ]" or "{ }"');
370
+ return { node: plain(raw, n), end };
371
+ }
372
+ function skipSpaces(text, at) {
373
+ let i = at;
374
+ while (text[i] === ' ')
375
+ i++;
376
+ return i;
377
+ }
378
+ function clip(text) {
379
+ return text.length > 40 ? `${text.slice(0, 40)}…` : text;
380
+ }
@@ -0,0 +1,217 @@
1
+ #!/usr/bin/env bun
2
+ // Prints one subscription line from a Codex session rollout.
3
+ //
4
+ // Bun runs this. The check is one JSONL scan, and the owner already has Bun on
5
+ // PATH to run team. It uses only the built-in file modules, so there is no
6
+ // package to install beside the script.
7
+ //
8
+ // team runs a check with an empty environment plus PATH and HOME. CODEX_HOME
9
+ // from the owner's shell never reaches this process, so sessions are read from
10
+ // $HOME/.codex/sessions. When Codex's home is somewhere else, install a two-line
11
+ // wrapper, name that wrapper as the account's check, and let it set CODEX_HOME
12
+ // before it execs this script:
13
+ //
14
+ // #!/bin/sh
15
+ // CODEX_HOME=/path/to/codex exec /path/to/codex-quota
16
+ //
17
+ // When that variable is set, sessions are $CODEX_HOME/sessions
18
+ // (codex-rs/utils/home-dir and rollout's SESSIONS_SUBDIR, tag rust-v0.157.0).
19
+ //
20
+ // Files named rollout-*.jsonl are taken newest first. The first that has a
21
+ // token_count event with a primary window is the one used, and at most ten
22
+ // files are opened. A newer session that has only session_meta does not hide
23
+ // the last figure. From that file the last such event is kept. The line's at
24
+ // time is that event's timestamp, so an older figure stays dated. The fields
25
+ // used are that window's used_percent, window_minutes and resets_at, plus the
26
+ // event's timestamp. Nothing else from the file is printed.
27
+ //
28
+ // window_minutes maps to a contract name: 300 session, 1440 daily, 10080 weekly.
29
+ // Any other length has no name, and inventing one would be a false figure.
30
+ // used_percent is rounded to the nearest integer. Below 0 or above 100 cannot
31
+ // be written. A reset in the past, or none, is left off the line.
32
+ //
33
+ // primary is the first line. When it cannot be written, nothing is printed, and
34
+ // an older file is not used in its place.
35
+ // RateLimitSnapshot.secondary (protocol.rs, tag rust-v0.157.0) is a second
36
+ // window in the same event. It is a second line when its window_minutes maps to
37
+ // a different one of the three names. The same window is left off: a repeated
38
+ // window makes the whole reading unknown. A secondary figure that cannot be
39
+ // written is left off, and the primary line still prints. Both lines share the
40
+ // event's timestamp.
41
+
42
+ import { readdirSync, readFileSync, statSync } from 'node:fs';
43
+ import { join } from 'node:path';
44
+
45
+ const WINDOWS = { 300: 'session', 1440: 'daily', 10080: 'weekly' };
46
+ const MAX_ROLLOUTS = 10;
47
+ const LINE =
48
+ /^(session|daily|weekly) (100|[0-9]{1,2})% used( resets (?:[0-9]+h(?:[0-9]+m)?|[0-9]+m))? at [0-9]{10}$/;
49
+
50
+ function main() {
51
+ for (const path of newestRollouts(sessionsDir(), MAX_ROLLOUTS)) {
52
+ const line = lineFromRollout(readFileSync(path, 'utf8'));
53
+ if (line === undefined) continue;
54
+ if (line) process.stdout.write(`${line}\n`);
55
+ return;
56
+ }
57
+ }
58
+
59
+ function sessionsDir() {
60
+ const codex = process.env.CODEX_HOME;
61
+ if (codex) return join(codex, 'sessions');
62
+ const home = process.env.HOME;
63
+ if (!home) return null;
64
+ return join(home, '.codex', 'sessions');
65
+ }
66
+
67
+ /** @param {string | null} root @param {number} limit */
68
+ function newestRollouts(root, limit) {
69
+ if (!root) return [];
70
+ /** @type {{ path: string, mtimeMs: number, name: string }[]} */
71
+ const found = [];
72
+ walk(root, found);
73
+ found.sort((a, b) => b.mtimeMs - a.mtimeMs || (a.name < b.name ? 1 : a.name > b.name ? -1 : 0));
74
+ return found.slice(0, limit).map((file) => file.path);
75
+ }
76
+
77
+ /** @param {string} dir @param {{ path: string, mtimeMs: number, name: string }[]} found */
78
+ function walk(dir, found) {
79
+ let entries;
80
+ try {
81
+ entries = readdirSync(dir, { withFileTypes: true });
82
+ } catch {
83
+ return;
84
+ }
85
+ for (const entry of entries) {
86
+ if (entry.isSymbolicLink()) continue;
87
+ const path = join(dir, entry.name);
88
+ if (entry.isDirectory()) {
89
+ walk(path, found);
90
+ continue;
91
+ }
92
+ if (!entry.isFile() || !entry.name.startsWith('rollout-') || !entry.name.endsWith('.jsonl')) continue;
93
+ try {
94
+ found.push({ path, mtimeMs: statSync(path).mtimeMs, name: entry.name });
95
+ } catch {
96
+ // A file that disappears between listing and stat is not the newest.
97
+ }
98
+ }
99
+ }
100
+
101
+ /**
102
+ * The contract line, null when a primary window is present but cannot be
103
+ * written, or undefined when this file has no token_count with a primary.
104
+ * A null result stops the scan: this file already has a primary window, and an
105
+ * older file must not stand in for it.
106
+ * @param {string} text
107
+ * @returns {string | null | undefined}
108
+ */
109
+ function lineFromRollout(text) {
110
+ const lines = text.split('\n');
111
+ for (let index = lines.length - 1; index >= 0; index--) {
112
+ const raw = lines[index]?.trim();
113
+ if (!raw) continue;
114
+ const event = tokenCount(raw);
115
+ if (event === undefined) continue;
116
+ return event;
117
+ }
118
+ return undefined;
119
+ }
120
+
121
+ /**
122
+ * undefined: not a token_count with a primary window; keep looking.
123
+ * null: the latest primary window cannot be written; stop.
124
+ * string: one contract line, or two when secondary is another window.
125
+ * @param {string} raw
126
+ * @returns {string | null | undefined}
127
+ */
128
+ function tokenCount(raw) {
129
+ let row;
130
+ try {
131
+ row = JSON.parse(raw);
132
+ } catch {
133
+ return undefined;
134
+ }
135
+ const payload = row?.payload;
136
+ if (!payload || payload.type !== 'token_count') return undefined;
137
+ const limits = payload.rate_limits;
138
+ const primary = limits?.primary;
139
+ if (primary == null) return undefined;
140
+ if (typeof primary !== 'object') return null;
141
+ const line = formatWindow(primary, row.timestamp);
142
+ if (line === null) return null;
143
+ const extra = secondaryLine(limits.secondary, row.timestamp, windowName(primary.window_minutes));
144
+ return extra ? `${line}\n${extra}` : line;
145
+ }
146
+
147
+ /**
148
+ * The second line, or '' when secondary is absent, the same window, or cannot
149
+ * be written. A bad secondary never drops the primary line.
150
+ * @param {unknown} secondary
151
+ * @param {unknown} timestamp
152
+ * @param {string | null} primaryWindow
153
+ * @returns {string}
154
+ */
155
+ function secondaryLine(secondary, timestamp, primaryWindow) {
156
+ if (secondary == null || typeof secondary !== 'object') return '';
157
+ const record = /** @type {Record<string, unknown>} */ (secondary);
158
+ const window = windowName(record.window_minutes);
159
+ if (window === null || window === primaryWindow) return '';
160
+ return formatWindow(record, timestamp) ?? '';
161
+ }
162
+
163
+ /** @param {Record<string, unknown>} window @param {unknown} timestamp */
164
+ function formatWindow(window, timestamp) {
165
+ const used = roundedPercent(window.used_percent);
166
+ const name = windowName(window.window_minutes);
167
+ const at = unixSeconds(timestamp);
168
+ if (used === null || name === null || at === null) return null;
169
+ const resets = resetPhrase(window.resets_at);
170
+ if (resets === null) return null;
171
+ const line = `${name} ${used}% used${resets} at ${at}`;
172
+ return LINE.test(line) ? line : null;
173
+ }
174
+
175
+ /** @param {unknown} value */
176
+ function roundedPercent(value) {
177
+ if (typeof value !== 'number' || !Number.isFinite(value) || value < 0 || value > 100) return null;
178
+ return Math.round(value);
179
+ }
180
+
181
+ /** @param {unknown} minutes */
182
+ function windowName(minutes) {
183
+ if (typeof minutes !== 'number' || !Number.isFinite(minutes)) return null;
184
+ return WINDOWS[minutes] ?? null;
185
+ }
186
+
187
+ /** @param {unknown} timestamp */
188
+ function unixSeconds(timestamp) {
189
+ const ms = typeof timestamp === 'string' ? Date.parse(timestamp) : NaN;
190
+ if (!Number.isFinite(ms)) return null;
191
+ const seconds = Math.floor(ms / 1000);
192
+ return seconds >= 1_000_000_000 && seconds <= 9_999_999_999 ? String(seconds) : null;
193
+ }
194
+
195
+ /**
196
+ * '' when the reset is absent or already past. null when the field is present
197
+ * and not a finite number of seconds.
198
+ * @param {unknown} resetsAt
199
+ */
200
+ function resetPhrase(resetsAt) {
201
+ if (resetsAt == null) return '';
202
+ if (typeof resetsAt !== 'number' || !Number.isFinite(resetsAt)) return null;
203
+ const remaining = Math.floor(resetsAt) - Math.floor(Date.now() / 1000);
204
+ if (remaining <= 0) return '';
205
+ const minutes = Math.floor(remaining / 60);
206
+ const hours = Math.floor(minutes / 60);
207
+ const rest = minutes % 60;
208
+ if (hours > 0 && rest > 0) return ` resets ${hours}h${rest}m`;
209
+ if (hours > 0) return ` resets ${hours}h`;
210
+ return ` resets ${rest}m`;
211
+ }
212
+
213
+ try {
214
+ main();
215
+ } catch {
216
+ // No file, a bad path, or a read error is an unknown figure. Exit 0, no text.
217
+ }
@@ -0,0 +1,76 @@
1
+ # beacon's team, in format 1: a fictional example. Every project, seat, model and
2
+ # command in it is invented; nothing of a real roster is here.
3
+
4
+ format: 1
5
+ project: beacon
6
+ coordinator: claude-keeper # dispatches work
7
+ operator: claude-signal # receives the watch's reports and nudges
8
+
9
+ identity:
10
+ signature:
11
+ commits:
12
+ position: trailer # last-line | trailer | anywhere
13
+ exempt: [merge]
14
+ pull_requests:
15
+ position: last-line
16
+ template: "**Agent:** {display} · {role}"
17
+
18
+ workspace:
19
+ mode: shared # every seat works in the project root
20
+
21
+ machine: # checked before each seat is launched, and by the watch
22
+ load_start: 2.0 # 1-minute load per core above which `up` and `add` refuse
23
+ load_max: 8.0 # per core, above which the watch reports
24
+ memory_start: 30% # free memory below which `up` and `add` refuse
25
+ memory_min: 10% # below which the watch reports
26
+ disk_min: 20GB # free on the project's volume; both refuse and report
27
+
28
+ seats:
29
+ - role: coordinator
30
+ name: claude-keeper
31
+ label: coordinator
32
+ cli: claude-code
33
+ vendor: anthropic
34
+ model: Claude Nova
35
+ version: "2"
36
+ launch: claude
37
+
38
+ - role: operator
39
+ name: claude-signal
40
+ label: operator
41
+ cli: claude-code
42
+ vendor: anthropic
43
+ model: Claude Nova
44
+ version: "2"
45
+ launch: claude
46
+
47
+ - role: implementer
48
+ name: codex-beacon
49
+ label: codex
50
+ cli: codex
51
+ vendor: openai
52
+ model: GPT Comet
53
+ version: "3"
54
+ display: GPT-3 Comet # the vendor's spelling, for the signature
55
+ launch: codex -m gpt-comet-3
56
+ parked: true # running, and not reported while idle
57
+
58
+ - role: implementer
59
+ name: cursor-beacon
60
+ label: cursor
61
+ cli: cursor
62
+ vendor: meridian
63
+ model: Meridian
64
+ version: "1"
65
+ launch: cursor-agent # the model is chosen inside Cursor
66
+
67
+ # three seats from one entry: nimbus-beacon, nimbus-beacon-2 and -3
68
+ - role: implementer
69
+ name: nimbus-beacon
70
+ label: nimbus
71
+ cli: claude-code # the vendor's model, run by Claude Code
72
+ vendor: nimbus
73
+ model: Nimbus
74
+ version: "1"
75
+ launch: nimbus-claude # a launcher on the PATH, holding the account's key and endpoint
76
+ count: 3