team 0.1.1 → 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 (127) hide show
  1. package/README.md +38 -12
  2. package/dist/ansi.d.ts +13 -0
  3. package/dist/ansi.js +75 -0
  4. package/dist/approve/approval.d.ts +11 -0
  5. package/dist/approve/approval.js +80 -8
  6. package/dist/approve/fingerprint.d.ts +29 -6
  7. package/dist/approve/fingerprint.js +57 -32
  8. package/dist/budgets/gate.d.ts +5 -4
  9. package/dist/budgets/gate.js +23 -14
  10. package/dist/budgets/readings.d.ts +57 -9
  11. package/dist/budgets/readings.js +134 -18
  12. package/dist/budgets/run.js +8 -2
  13. package/dist/budgets/table.d.ts +4 -2
  14. package/dist/budgets/table.js +29 -11
  15. package/dist/cli.d.ts +2 -1
  16. package/dist/cli.js +8 -5
  17. package/dist/commands/add.d.ts +0 -1
  18. package/dist/commands/add.js +18 -9
  19. package/dist/commands/approve.js +17 -1
  20. package/dist/commands/doctor.d.ts +17 -1
  21. package/dist/commands/doctor.js +128 -8
  22. package/dist/commands/down.d.ts +6 -2
  23. package/dist/commands/down.js +40 -14
  24. package/dist/commands/init.d.ts +1 -1
  25. package/dist/commands/init.js +4 -2
  26. package/dist/commands/remove.d.ts +2 -0
  27. package/dist/commands/remove.js +26 -4
  28. package/dist/commands/status.d.ts +2 -0
  29. package/dist/commands/status.js +22 -3
  30. package/dist/commands/up.d.ts +3 -1
  31. package/dist/commands/up.js +12 -6
  32. package/dist/commands/watch.d.ts +4 -0
  33. package/dist/commands/watch.js +99 -19
  34. package/dist/conformance/adapter.d.ts +5 -0
  35. package/dist/conformance/adapter.js +44 -0
  36. package/dist/file/check.d.ts +20 -0
  37. package/dist/file/check.js +112 -0
  38. package/dist/file/lines.js +13 -2
  39. package/dist/file/sections/budgets.d.ts +15 -0
  40. package/dist/file/sections/budgets.js +180 -0
  41. package/dist/file/sections/coordinator.d.ts +2 -0
  42. package/dist/file/sections/coordinator.js +11 -0
  43. package/dist/file/sections/format.d.ts +2 -0
  44. package/dist/file/sections/format.js +13 -0
  45. package/dist/file/sections/identity.d.ts +2 -0
  46. package/dist/file/sections/identity.js +103 -0
  47. package/dist/file/sections/index.d.ts +9 -0
  48. package/dist/file/sections/index.js +43 -0
  49. package/dist/file/sections/lead.d.ts +4 -0
  50. package/dist/file/sections/lead.js +18 -0
  51. package/dist/file/sections/limits.d.ts +2 -0
  52. package/dist/file/sections/limits.js +43 -0
  53. package/dist/file/sections/machine.d.ts +2 -0
  54. package/dist/file/sections/machine.js +38 -0
  55. package/dist/file/sections/operator.d.ts +2 -0
  56. package/dist/file/sections/operator.js +11 -0
  57. package/dist/file/sections/project.d.ts +2 -0
  58. package/dist/file/sections/project.js +13 -0
  59. package/dist/file/sections/rules.d.ts +2 -0
  60. package/dist/file/sections/rules.js +9 -0
  61. package/dist/file/sections/seats.d.ts +7 -0
  62. package/dist/file/sections/seats.js +161 -0
  63. package/dist/file/sections/section.d.ts +32 -0
  64. package/dist/file/sections/section.js +7 -0
  65. package/dist/file/sections/session.d.ts +2 -0
  66. package/dist/file/sections/session.js +16 -0
  67. package/dist/file/sections/tools.d.ts +2 -0
  68. package/dist/file/sections/tools.js +43 -0
  69. package/dist/file/sections/trust.d.ts +2 -0
  70. package/dist/file/sections/trust.js +20 -0
  71. package/dist/file/sections/units.d.ts +16 -0
  72. package/dist/file/sections/units.js +8 -0
  73. package/dist/file/sections/visibility.d.ts +2 -0
  74. package/dist/file/sections/visibility.js +12 -0
  75. package/dist/file/sections/watch-checks.d.ts +9 -0
  76. package/dist/file/sections/watch-checks.js +14 -0
  77. package/dist/file/sections/watch.d.ts +14 -0
  78. package/dist/file/sections/watch.js +111 -0
  79. package/dist/file/sections/workspace.d.ts +4 -0
  80. package/dist/file/sections/workspace.js +77 -0
  81. package/dist/file/types.d.ts +6 -0
  82. package/dist/file/validate.d.ts +4 -12
  83. package/dist/file/validate.js +45 -668
  84. package/dist/herdr.d.ts +5 -0
  85. package/dist/herdr.js +51 -12
  86. package/dist/launch/agent.d.ts +1 -0
  87. package/dist/launch/agent.js +13 -0
  88. package/dist/launch/deliver.d.ts +7 -1
  89. package/dist/launch/deliver.js +159 -7
  90. package/dist/launch/execute.d.ts +4 -2
  91. package/dist/launch/execute.js +36 -13
  92. package/dist/launch/plan.js +16 -2
  93. package/dist/launch/rules.d.ts +4 -2
  94. package/dist/launch/rules.js +17 -3
  95. package/dist/profiles/antigravity.yaml +25 -2
  96. package/dist/profiles/claude-code.yaml +51 -6
  97. package/dist/profiles/codex.yaml +14 -1
  98. package/dist/profiles/cursor.yaml +46 -3
  99. package/dist/profiles/overrides.d.ts +67 -0
  100. package/dist/profiles/overrides.js +190 -0
  101. package/dist/profiles/profile.d.ts +3 -0
  102. package/dist/profiles/profile.js +5 -1
  103. package/dist/state.d.ts +9 -4
  104. package/dist/state.js +37 -0
  105. package/dist/status/compare.js +7 -4
  106. package/dist/status/statusline.js +3 -1
  107. package/dist/store/store.d.ts +5 -0
  108. package/dist/version.d.ts +1 -0
  109. package/dist/version.js +7 -0
  110. package/dist/watch/check.d.ts +1 -0
  111. package/dist/watch/checks/budget.js +50 -51
  112. package/dist/watch/dialect.d.ts +6 -1
  113. package/dist/watch/dialect.js +68 -12
  114. package/dist/watch/pass.d.ts +10 -2
  115. package/dist/watch/pass.js +70 -29
  116. package/dist/watch/screen-core.d.ts +41 -4
  117. package/dist/watch/screen-core.js +510 -89
  118. package/dist/watch/screen-data.d.ts +34 -0
  119. package/dist/watch/screen-file.d.ts +8 -2
  120. package/dist/watch/screen-file.js +344 -39
  121. package/dist/watch/screen-profile.d.ts +19 -0
  122. package/dist/watch/screen-profile.js +1 -0
  123. package/dist/watch/screen.d.ts +25 -6
  124. package/dist/watch/screen.js +48 -9
  125. package/examples/team.yaml +8 -0
  126. package/package.json +9 -3
  127. package/schema/team.schema.json +498 -0
@@ -1,4 +1,4 @@
1
- import { verdict } from "../../budgets/readings.js";
1
+ import { countedFor } from "../../budgets/readings.js";
2
2
  import { WINDOWS } from "../../profiles/quota.js";
3
3
  import { reported } from "../check.js";
4
4
  export const budget = {
@@ -40,65 +40,64 @@ export const budget = {
40
40
  },
41
41
  };
42
42
  /**
43
- * The account's figure this pass, from the first source that has one (§ 4.3, § 5). A check
44
- * reading counts while it is fresh; a screen reading counts when the verdict keeps it — fresh,
45
- * stale, or refusing — and a bare first sight is `unseen`. Null: nothing counts at all.
43
+ * The account's figures this pass, per window, from the first source that has one for it (§ 4.3,
44
+ * § 5) — the same rule `countedFor` applies for the launch gate and the table, so the three can't
45
+ * drift apart. A check reading counts while it is fresh; a screen reading counts when the verdict
46
+ * keeps it — fresh, stale, or refusing — and a bare first sight is `unseen`. Null: nothing counts
47
+ * at all. A spend check is one number and no window, fresh by § 5.
46
48
  */
47
49
  function countedOf(name, account, team, staleMs, now) {
48
- for (const source of account.sources) {
49
- if (source === 'check') {
50
- const outcome = team.outcomes.find((one) => one.account === name);
51
- if (!outcome || outcome.state !== 'read')
52
- continue;
53
- if (outcome.reading.kind === 'spend') {
54
- if (now - outcome.reading.at < staleMs) {
55
- return { kind: 'spend', amount: outcome.reading.amount, currency: outcome.reading.currency };
56
- }
57
- continue;
58
- }
59
- const windows = outcome.reading.windows
60
- .filter((window) => now - window.at < staleMs)
61
- .map(({ window, left, used, resetsAt }) => ({ window, left, used, resetsAt }));
62
- if (windows.length)
63
- return { kind: 'subscription', windows };
50
+ const outcome = team.outcomes.find((one) => one.account === name);
51
+ const read = outcome && outcome.state === 'read' ? outcome : null;
52
+ if (read && read.reading.kind === 'spend' && account.sources.includes('check') && now - read.reading.at < staleMs) {
53
+ return { kind: 'spend', amount: read.reading.amount, currency: read.reading.currency };
54
+ }
55
+ const windows = [];
56
+ let seen = false;
57
+ for (const window of WINDOWS) {
58
+ // The readings of one window, by source: the screen readings the core folded in, and the
59
+ // check's when this pass's outcome carries that window (§ 5).
60
+ const screen = team.readings.filter((reading) => reading.account === name && reading.window === window && reading.source === 'status_line');
61
+ const checks = read && read.reading.kind === 'subscription'
62
+ ? read.reading.windows
63
+ .filter((one) => one.window === window)
64
+ .map((one) => ({
65
+ account: name,
66
+ window: one.window,
67
+ left: one.left,
68
+ used: one.used,
69
+ changedAt: one.at,
70
+ resetsAt: one.resetsAt,
71
+ seat: null,
72
+ source: 'check',
73
+ confirmed: true,
74
+ }))
75
+ : [];
76
+ if (!screen.length && !checks.length)
77
+ continue;
78
+ const result = countedFor(account.sources, screen, checks, now, staleMs, account.reserve);
79
+ if (result.kind === 'unknown')
80
+ continue;
81
+ if (result.kind === 'unconfirmed') {
82
+ seen = true;
64
83
  continue;
65
84
  }
66
- const groups = new Map();
67
- for (const reading of team.readings) {
68
- if (reading.account !== name)
69
- continue;
70
- const group = groups.get(reading.window) ?? [];
71
- group.push(reading);
72
- groups.set(reading.window, group);
73
- }
74
- const windows = [];
75
- let seen = false;
76
- for (const group of groups.values()) {
77
- const result = verdict(group, now, staleMs, account.reserve);
78
- if (result.kind === 'unknown')
79
- continue;
80
- if (result.kind === 'unconfirmed') {
81
- seen = true;
82
- continue;
83
- }
84
- windows.push({
85
- window: result.reading.window,
86
- left: result.reading.left,
87
- used: result.reading.used,
88
- resetsAt: result.reading.resetsAt,
89
- });
90
- }
91
- if (windows.length)
92
- return { kind: 'subscription', windows };
93
- if (seen)
94
- return 'unseen';
95
- continue;
85
+ windows.push({
86
+ window: result.reading.window,
87
+ left: result.reading.left,
88
+ used: result.reading.used,
89
+ resetsAt: result.reading.resetsAt,
90
+ });
96
91
  }
92
+ if (windows.length)
93
+ return { kind: 'subscription', windows };
94
+ if (seen)
95
+ return 'unseen';
97
96
  return null;
98
97
  }
99
98
  /** The account reads unknown while a seat that spends it is running: the operator can stop it. */
100
99
  function unknown(name, team, ctx) {
101
- const on = team.seats.filter((seat) => seat.running && seat.vendor === name).map((seat) => seat.name);
100
+ const on = team.seats.filter((seat) => seat.running && seat.account === name).map((seat) => seat.name);
102
101
  if (!on.length)
103
102
  return null;
104
103
  const verb = on.length === 1 ? 'runs' : 'run';
@@ -1,5 +1,10 @@
1
1
  export declare class DialectError extends Error {
2
2
  constructor(message: string);
3
3
  }
4
- /** Compile one pattern. `ignoreCase` is the stage's flag, never an inline `(?i)`. */
4
+ /** Compile one pattern. `ignoreCase` is the stage's flag, never an inline `(?i)`. The
5
+ * flag is an ASCII fold: each ASCII letter of the pattern becomes its two-letter class,
6
+ * `[Aa]`, exactly what the hand-spelled patterns wrote out. Folding that lives in the
7
+ * pattern's own text travels with it — the shared dialect's other engine compiles the
8
+ * same source — and the engine's wider Unicode folding (a long s for s, the Kelvin
9
+ * sign for k) never applies. */
5
10
  export declare function compilePattern(source: string, ignoreCase?: boolean): RegExp;
@@ -20,25 +20,31 @@ export class DialectError extends Error {
20
20
  this.name = 'DialectError';
21
21
  }
22
22
  }
23
- /** Compile one pattern. `ignoreCase` is the stage's flag, never an inline `(?i)`. */
23
+ /** Compile one pattern. `ignoreCase` is the stage's flag, never an inline `(?i)`. The
24
+ * flag is an ASCII fold: each ASCII letter of the pattern becomes its two-letter class,
25
+ * `[Aa]`, exactly what the hand-spelled patterns wrote out. Folding that lives in the
26
+ * pattern's own text travels with it — the shared dialect's other engine compiles the
27
+ * same source — and the engine's wider Unicode folding (a long s for s, the Kelvin
28
+ * sign for k) never applies. */
24
29
  export function compilePattern(source, ignoreCase = false) {
25
30
  if (source.length === 0)
26
31
  throw new DialectError('a pattern is empty');
27
32
  if (source.length > MAX_PATTERN)
28
33
  throw new DialectError(`a pattern is longer than ${MAX_PATTERN} characters`);
29
34
  refuseSpelling(source);
30
- const parser = new Parser(source);
35
+ const parser = new Parser(source, ignoreCase);
31
36
  const body = parser.parseAlt();
32
37
  if (!parser.done())
33
38
  throw new DialectError('a pattern has an extra ")"');
34
- const flags = ignoreCase ? 'iu' : 'u';
35
- return new RegExp(body.source, flags);
39
+ return new RegExp(body.source, 'u');
36
40
  }
37
41
  class Parser {
38
42
  at = 0;
39
43
  source;
40
- constructor(source) {
44
+ ignoreCase;
45
+ constructor(source, ignoreCase) {
41
46
  this.source = source;
47
+ this.ignoreCase = ignoreCase;
42
48
  }
43
49
  done() {
44
50
  return this.at >= this.source.length;
@@ -114,7 +120,15 @@ class Parser {
114
120
  if (ch === ')')
115
121
  throw new DialectError('a pattern has an extra ")"');
116
122
  const cp = this.readCodePoint();
117
- return this.leaf(escapeLiteral(cp), true);
123
+ return this.leaf(this.fold(cp), true);
124
+ }
125
+ // A literal under ignore_case: an ASCII letter becomes its two-letter class, `[Aa]`,
126
+ // as the hand-spelled patterns wrote it; everything else stands as itself.
127
+ fold(cp) {
128
+ const other = asciiCaseSwapped(cp);
129
+ if (!this.ignoreCase || other === null)
130
+ return escapeLiteral(cp);
131
+ return emitClass(false, [cp, other]);
118
132
  }
119
133
  parseGroup() {
120
134
  this.expect('(');
@@ -213,7 +227,7 @@ class Parser {
213
227
  parts.push({ kind: 'char', cp: this.readCodePoint() });
214
228
  }
215
229
  this.expect(']');
216
- return classSource(negated, foldRanges(parts));
230
+ return classSource(negated, foldRanges(parts, this.ignoreCase));
217
231
  }
218
232
  parseClassEscape() {
219
233
  this.expect('\\');
@@ -318,7 +332,9 @@ function refuseSpelling(source) {
318
332
  }
319
333
  // A class is a finite set, except a complement shorthand (`\S`, `\D`, `\W`), whose
320
334
  // negation brings it back to a finite set. `[^\S\n]` is the whitespace set without
321
- // a newline, which is what the working-line pattern relies on.
335
+ // a newline, which is what the working-line pattern relies on. A positive class that
336
+ // mixed one with other members would compile to a class nobody wrote (`[\D0-9]` is
337
+ // everything), so it is refused rather than rewritten.
322
338
  function classSource(negated, parts) {
323
339
  const positive = new Set();
324
340
  const complements = [];
@@ -333,13 +349,16 @@ function classSource(negated, parts) {
333
349
  }
334
350
  if (complements.length === 0)
335
351
  return emitClass(negated, [...positive]);
352
+ if (!negated && positive.size > 0) {
353
+ throw new DialectError('a class cannot mix a complement shorthand with other members: negate the class, or write the shorthand alone');
354
+ }
336
355
  let inter = new Set(complements[0]);
337
356
  for (const extra of complements.slice(1))
338
357
  inter = new Set([...inter].filter((cp) => extra.includes(cp)));
339
358
  const remaining = [...inter].filter((cp) => !positive.has(cp));
340
359
  return emitClass(!negated, remaining);
341
360
  }
342
- function foldRanges(parts) {
361
+ function foldRanges(parts, ignoreCase = false) {
343
362
  const out = [];
344
363
  for (let i = 0; i < parts.length; i++) {
345
364
  const part = parts[i];
@@ -358,7 +377,38 @@ function foldRanges(parts) {
358
377
  else
359
378
  out.push(part);
360
379
  }
361
- return out;
380
+ if (!ignoreCase)
381
+ return out;
382
+ // Under ignore_case every ASCII letter of the class carries its other case too, as the
383
+ // letter classes of the hand-spelled patterns did. A complement shorthand (`\S` and
384
+ // the like) is written as its own set: its members are not the pattern's letters.
385
+ return out.map((part) => {
386
+ if (part.kind === 'char') {
387
+ const other = asciiCaseSwapped(part.cp);
388
+ return other === null ? part : { kind: 'chars', cps: [part.cp, other] };
389
+ }
390
+ if (part.kind === 'chars') {
391
+ const cps = [...part.cps];
392
+ for (const cp of part.cps) {
393
+ const other = asciiCaseSwapped(cp);
394
+ if (other !== null)
395
+ cps.push(other);
396
+ }
397
+ return { kind: 'chars', cps };
398
+ }
399
+ return part;
400
+ });
401
+ }
402
+ // The other case of an ASCII letter, or null for every other character: the whole of a
403
+ // pattern's ignore_case fold. The engine's Unicode folding — a long s (U+017F) for s,
404
+ // the Kelvin sign (U+212A) for k — is deliberately not applied; the hand-spelled
405
+ // classes this flag replaced never applied it.
406
+ function asciiCaseSwapped(cp) {
407
+ if (cp >= 0x41 && cp <= 0x5a)
408
+ return cp + 0x20;
409
+ if (cp >= 0x61 && cp <= 0x7a)
410
+ return cp - 0x20;
411
+ return null;
362
412
  }
363
413
  function emitClass(negated, cps) {
364
414
  const sorted = [...new Set(cps)].sort((a, b) => a - b);
@@ -395,7 +445,7 @@ function classChar(cp) {
395
445
  if (cp === 0x2d || cp === 0x5d || cp === 0x5e || cp === 0x5c)
396
446
  return `\\${String.fromCodePoint(cp)}`;
397
447
  if (cp < 0x20 || cp > 0x7e)
398
- return `\\u${cp.toString(16).padStart(4, '0')}`;
448
+ return codePointEscape(cp);
399
449
  return String.fromCodePoint(cp);
400
450
  }
401
451
  function escapeLiteral(cp) {
@@ -403,9 +453,15 @@ function escapeLiteral(cp) {
403
453
  if ('\\.^$|?*+()[]{}'.includes(ch))
404
454
  return `\\${ch}`;
405
455
  if (cp < 0x20 || cp > 0x7e)
406
- return `\\u${cp.toString(16).padStart(4, '0')}`;
456
+ return codePointEscape(cp);
407
457
  return ch;
408
458
  }
459
+ // A code point above U+FFFF needs the "\u{…}" form, which every pattern can carry: the "u"
460
+ // flag is always set (compilePattern). A four-digit escape cannot spell it — the digits
461
+ // would spill into the following character.
462
+ function codePointEscape(cp) {
463
+ return cp > 0xffff ? `\\u{${cp.toString(16)}}` : `\\u${cp.toString(16).padStart(4, '0')}`;
464
+ }
409
465
  function seqSource(atoms) {
410
466
  return atoms.map((atom) => atom.source).join('');
411
467
  }
@@ -1,10 +1,12 @@
1
1
  import { type Seen } from '../budgets/readings.ts';
2
2
  import type { CheckOutcome } from '../budgets/run.ts';
3
3
  import type { TeamFile } from '../file/types.ts';
4
+ import { type QuotaPattern } from '../profiles/quota.ts';
4
5
  import type { SessionState } from '../state.ts';
5
6
  import type { Live } from '../status/compare.ts';
6
7
  import { type Report, type SeatCheck, type SeatHistory, type TeamCheck } from './check.ts';
7
8
  import type { Machine } from './machine.ts';
9
+ import { type Screen } from './screen.ts';
8
10
  export type Memory = {
9
11
  history: Record<string, SeatHistory>;
10
12
  slots: Record<string, unknown>;
@@ -35,8 +37,14 @@ export type PassInput = {
35
37
  now: number;
36
38
  memory: Memory;
37
39
  approval?: string[] | null;
38
- watch?: TeamFile['watch'];
40
+ watch: TeamFile['watch'];
39
41
  outcomes?: readonly CheckOutcome[];
40
42
  budgets?: TeamFile['budgets'];
43
+ readings?: readonly Seen[];
44
+ foreground?: Readonly<Record<string, readonly string[] | null>>;
45
+ /** Screen readings with the approved overrides applied. The shipped profiles, when omitted. */
46
+ readScreen?: (cli: string, screen: string | undefined) => Screen;
47
+ /** Quota patterns in force, shipped plus the approved override. The shipped list, when omitted. */
48
+ quotaFor?: (cli: string) => readonly QuotaPattern[];
41
49
  };
42
- export declare function pass({ team, state, live, machine, now, memory, approval, watch, outcomes, budgets, }: PassInput): PassResult;
50
+ export declare function pass({ team, state, live, machine, now, memory, approval, watch, outcomes, budgets, readings: stored, foreground, readScreen: read, quotaFor: patternsOf, }: PassInput): PassResult;
@@ -2,9 +2,10 @@
2
2
  // runs the registered checks in the order it owns, applies the report-once rule, and dials the
3
3
  // nudge. A check never reads herdr, a screen or the file itself — only the observation it is
4
4
  // handed (RFC 0002 § 4.2).
5
- import { WATCH_CHECKS_CHANGED } from "../approve/fingerprint.js";
6
- import { observe, recall } from "../budgets/readings.js";
7
- import { quotaFor } from "../profiles/profile.js";
5
+ import { seatNamed } from "../approve/fingerprint.js";
6
+ import { observe, observeCheck } from "../budgets/readings.js";
7
+ import { reportedLiveAgent } from "../launch/agent.js";
8
+ import { profileFor, quotaFor as shippedQuota } from "../profiles/profile.js";
8
9
  import { figuresOf } from "../profiles/quota.js";
9
10
  import { seatModel } from "../status/statusline.js";
10
11
  import { ALWAYS_ON, } from "./check.js";
@@ -33,8 +34,8 @@ export function newMemory() {
33
34
  export const NUDGE_TEXT = 'Team watch: reports are waiting in .agents/team.log';
34
35
  // The seat checks, in the order their reports land in the log; the team checks, after them. The
35
36
  // order is the core's: a seat's wrong model is reported before its permission prompt, and every
36
- // seat before the team. Disabling a check in the file's `watch.checks` takes it out of the run,
37
- // except the four that can't be turned off.
37
+ // seat before the team. Disabling a check in the `watch` section in force takes it out of the
38
+ // run, except the four that can't be turned off.
38
39
  export const SEAT_CHECKS = [missing, modelDrift, attention, unsent, idle];
39
40
  export const TEAM_CHECKS = [extra, teamIdle, approval, load, memoryCheck, disk, swapFree, swapGrowth, budget];
40
41
  // The one exclusive reading of a seat's screen and herdr status. herdr can report a seat at a
@@ -52,16 +53,20 @@ function attentionOf(screen, status, quiet) {
52
53
  }
53
54
  // The figures off a seat's own pane: only a composer screen — idle, unsent or working — shows a
54
55
  // status row at all, and only that row's line is read. A dialog, a question, a trust screen or an
55
- // unknown one gives no figures, and neither does a line the seat printed or typed.
56
- function quotaOf(cli, screen, pane) {
57
- if (pane === undefined)
56
+ // unknown one gives no figures, and neither does a line the seat printed or typed. And only a
57
+ // pane where herdr reports the seat's CLI still running: a shell's last row is a last row by
58
+ // position, not a status row, and a pane herdr could not read gives none either — no figure is
59
+ // invented where the CLI was not seen. (`down` and `remove` read that same null the other way at
60
+ // their departure wait; not knowing must not end a wait.)
61
+ function quotaOf(cli, screen, pane, runs, patterns) {
62
+ if (pane === undefined || !runs)
58
63
  return [];
59
64
  if (screen.kind !== 'idle' && screen.kind !== 'unsent' && screen.kind !== 'working')
60
65
  return [];
61
- return figuresOf(quotaFor(cli), statusRow(cli, pane));
66
+ return figuresOf(patterns, statusRow(cli, pane));
62
67
  }
63
68
  // One pass of the watch. Pure: it reads what it is handed and changes only `memory`.
64
- export function pass({ team, state, live, machine, now, memory, approval, watch = team.watch, outcomes = [], budgets = team.budgets, }) {
69
+ export function pass({ team, state, live, machine, now, memory, approval, watch, outcomes = [], budgets = team.budgets, readings: stored = [], foreground, readScreen: read = readScreen, quotaFor: patternsOf = shippedQuota, }) {
65
70
  const reports = [];
66
71
  const current = new Set();
67
72
  // Reported when it starts, and again only after it has cleared.
@@ -81,6 +86,7 @@ export function pass({ team, state, live, machine, now, memory, approval, watch
81
86
  name: seat.name,
82
87
  cli: seat.cli,
83
88
  vendor: seat.vendor,
89
+ account: seat.account ?? seat.vendor,
84
90
  parked: seat.parked || seat.stopped,
85
91
  stopped: seat.stopped,
86
92
  seat,
@@ -88,10 +94,18 @@ export function pass({ team, state, live, machine, now, memory, approval, watch
88
94
  ...Object.entries(state.seats).filter(([, recorded]) => recorded.temporary).map(([name, recorded]) => {
89
95
  // A temporary seat signs like the seat it is like: its CLI, and its account.
90
96
  const like = team.seats.find((seat) => seat.name === recorded.temporary?.like);
91
- return { name, cli: like?.cli ?? '', vendor: like?.vendor ?? '', parked: false, stopped: false, seat: undefined };
97
+ return {
98
+ name,
99
+ cli: like?.cli ?? '',
100
+ vendor: like?.vendor ?? '',
101
+ account: like?.account ?? like?.vendor ?? '',
102
+ parked: false,
103
+ stopped: false,
104
+ seat: undefined,
105
+ };
92
106
  }),
93
107
  ];
94
- for (const { name, cli, vendor, parked, stopped, seat } of seats) {
108
+ for (const { name, cli, vendor, account, parked, stopped, seat } of seats) {
95
109
  const agent = live.agents.find((candidate) => candidate.name === name);
96
110
  const lead = name === team.coordinator || name === team.operator;
97
111
  if (!agent) {
@@ -104,6 +118,7 @@ export function pass({ team, state, live, machine, now, memory, approval, watch
104
118
  screen: { kind: 'unknown' },
105
119
  cli,
106
120
  vendor,
121
+ account,
107
122
  quota: [],
108
123
  running: false,
109
124
  quiet: false,
@@ -120,7 +135,7 @@ export function pass({ team, state, live, machine, now, memory, approval, watch
120
135
  }
121
136
  known.add(agent.pane);
122
137
  const pane = live.screens[agent.pane];
123
- const screen = readScreen(cli, pane);
138
+ const screen = read(cli, pane);
124
139
  const quiet = agent.status === 'idle' || agent.status === 'done';
125
140
  // Working is herdr's word or the screen's: a seat mid-turn is never idle, whatever its
126
141
  // status says.
@@ -146,7 +161,8 @@ export function pass({ team, state, live, machine, now, memory, approval, watch
146
161
  screen,
147
162
  cli,
148
163
  vendor,
149
- quota: quotaOf(cli, screen, pane),
164
+ account,
165
+ quota: quotaOf(cli, screen, pane, reportedLiveAgent(foreground?.[agent.pane] ?? null, profileFor(cli)?.processNames ?? []), patternsOf(cli)),
150
166
  running: true,
151
167
  quiet,
152
168
  working,
@@ -159,19 +175,45 @@ export function pass({ team, state, live, machine, now, memory, approval, watch
159
175
  history: { lastWorking: history.lastWorking, idleSince: history.idleSince },
160
176
  });
161
177
  }
162
- // The figures this pass saw, folded into the readings the state keeps (§ 4.3). Only an account
163
- // whose `sources` name `status_line` takes a screen reading: a check-only account never records
164
- // one here, and neither does an account the budgets in force don't name (#50).
165
- let readings = recall(state.budgets);
178
+ // The figures this pass saw, folded into the readings the project keeps (§ 4.3, § 4.4). Only an
179
+ // account whose `sources` name `status_line` takes a screen reading: a check-only account never
180
+ // records one here, and neither does an account the budgets in force don't name (#50). A figure
181
+ // names the account its pattern measures (§ 3b): one that measures this seat's vendor is this
182
+ // seat's account — its own `account:` when the file names one, so two accounts of one vendor
183
+ // keep two buckets — while a figure on another account (a launcher showing the vendor it really
184
+ // runs on) stays that account's, whichever seat's screen showed it.
185
+ //
186
+ // A seat the approval lists as changed — or as not in the approved file — is drift, and its
187
+ // figures are read as they are but none is folded: an unapproved edit to its `account:` must
188
+ // not move its figure into another account's bucket, where `up` and `add` would count it.
189
+ // Nothing is saved for that seat until the owner approves.
190
+ const drift = new Set((Array.isArray(approval) ? approval : []).map(seatNamed).filter((name) => name !== null));
191
+ for (const [name, recorded] of Object.entries(state.seats)) {
192
+ if (recorded.temporary && drift.has(recorded.temporary.like)) {
193
+ drift.add(name);
194
+ }
195
+ }
196
+ let readings = stored.slice();
166
197
  for (const seat of observations) {
167
- if (!seat.running)
198
+ if (!seat.running || drift.has(seat.name))
168
199
  continue;
169
200
  for (const figure of seat.quota) {
170
- if (budgets.accounts[figure.account]?.sources.includes('status_line')) {
171
- readings = observe(readings, figure, seat.name, now);
201
+ const account = figure.account === seat.vendor ? seat.account : figure.account;
202
+ if (budgets.accounts[account]?.sources.includes('status_line')) {
203
+ readings = observe(readings, { ...figure, account }, seat.name, now);
172
204
  }
173
205
  }
174
206
  }
207
+ // A check reading lands in the same slot (§ 5): what an approved check read outlives the watch,
208
+ // so `up` and `add` count it after the watch has exited. Only an account whose sources name
209
+ // `check`, and only when its check actually read this pass.
210
+ for (const outcome of outcomes) {
211
+ if (outcome.state !== 'read' || outcome.reading.kind !== 'subscription')
212
+ continue;
213
+ if (!budgets.accounts[outcome.account]?.sources.includes('check'))
214
+ continue;
215
+ readings = observeCheck(readings, outcome.account, outcome.reading.windows);
216
+ }
175
217
  const teamObservation = {
176
218
  team,
177
219
  state,
@@ -196,15 +238,14 @@ export function pass({ team, state, live, machine, now, memory, approval, watch
196
238
  return memory.slots[kind];
197
239
  },
198
240
  };
199
- // The four § 4.2 keeps out of `watch.checks` run whatever the file says: the validation refuses
241
+ // The four § 4.2 keeps out of `watch.checks` run whatever the list says: the validation refuses
200
242
  // them, and this holds even for a file that reached memory another way.
201
243
  //
202
- // And nothing is turned off until the owner approves (RFC 0002 § 4.2): a file whose
203
- // `watch.checks` differs from the approved one, a file never approved, and an approval that
204
- // wasn't looked at keep every check running and report the difference instead. A difference in
205
- // some other section leaves the checks turned off as approved — the owner owns both questions.
206
- const approved = Array.isArray(approval) && !approval.includes(WATCH_CHECKS_CHANGED);
207
- const off = approved ? new Set(team.watch.checks) : new Set();
244
+ // The off list is the section in force, which the caller passes as `watch`. A file that was
245
+ // approved — `approval` is a list, empty or not — runs that list. An unapproved `watch.checks`
246
+ // edit does not turn anything new off, and a check the approved list turned off stays off.
247
+ // A file never approved, or whose approval was not looked at, turns nothing off.
248
+ const off = Array.isArray(approval) ? new Set(watch.checks) : new Set();
208
249
  const enabled = (name) => ALWAYS_ON.includes(name) || !off.has(name);
209
250
  for (const seat of observations) {
210
251
  for (const check of SEAT_CHECKS)
@@ -226,7 +267,7 @@ export function pass({ team, state, live, machine, now, memory, approval, watch
226
267
  const operator = live.agents.find((agent) => agent.name === team.operator);
227
268
  const cli = team.seats.find((seat) => seat.name === team.operator)?.cli ?? '';
228
269
  const free = operator !== undefined && (operator.status === 'idle' || operator.status === 'done')
229
- && readScreen(cli, live.screens[operator.pane]).kind === 'idle';
270
+ && read(cli, live.screens[operator.pane]).kind === 'idle';
230
271
  if (operator && free) {
231
272
  nudge = { pane: operator.pane, text: NUDGE_TEXT, pending: memory.pending.slice() };
232
273
  }
@@ -1,6 +1,7 @@
1
1
  import type { Screen } from './screen.ts';
2
- import type { ScreenData } from './screen-data.ts';
3
- export type { Composer, FallbackRule, LinePattern, Placeholder, Rule, ScreenData, Stage } from './screen-data.ts';
2
+ import type { ScreenData, Wrap } from './screen-data.ts';
3
+ export type { Composer, FallbackRule, LinePattern, Placeholder, PlaceholderStyle, Rule, ScreenData, Stage, Wrap } from './screen-data.ts';
4
+ export type { ScreenProfile, ComposerReading } from './screen-profile.ts';
4
5
  export type ReadClock = {
5
6
  now(): number;
6
7
  budgetMs: number;
@@ -11,10 +12,46 @@ export type ReadClock = {
11
12
  * typed — is never the row. The caller still has to know the screen is a composer screen.
12
13
  */
13
14
  export declare function statusRowOf(data: ScreenData, lines: string[]): string | null;
15
+ export type Fold = {
16
+ count: number;
17
+ rows: string[];
18
+ width: number;
19
+ };
20
+ /** The input box a composer draws for its current text: the first line after the prompt, the
21
+ * continuation rows under it, the column those rows start at (the prompt's own width plus
22
+ * the separator the input row draws), and the profile's wrap rule when a capture shows how
23
+ * the box continues a line onto its next row. */
24
+ export type Box = {
25
+ first: string;
26
+ indent: number;
27
+ rows: string[];
28
+ wrap?: Wrap;
29
+ };
30
+ /**
31
+ * The fold a `two-rules-footer-below` composer's window shows, or null. A marker whose count is
32
+ * zero names no hidden rows to compare against, so it is no fold; it is still a marker, and
33
+ * `foldMarked` catches it.
34
+ */
35
+ export declare function foldOf(data: ScreenData, lines: string[]): Fold | null;
36
+ /**
37
+ * Whether the window shows a fold marker at all, whatever count it names: a marker claiming zero
38
+ * hidden rows (or a count a fold could not have) is still a box that is not showing the whole
39
+ * text, and it must never be taken for an ordinary unsent box and submitted unverified.
40
+ */
41
+ export declare function foldMarked(data: ScreenData, lines: string[]): boolean;
14
42
  /** `lines` is already the window: the last 20 lines, each trimmed at the end. */
15
43
  export declare function classifyLines(data: ScreenData, lines: string[], clock?: ReadClock): Screen;
16
44
  /**
17
- * The composer alone, for a turn that is still running. The unknown stage is
18
- * read first, as in the full classification, and the floor still applies.
45
+ * The composer alone, for a turn that is still running. The dialog stages and
46
+ * floor must not be overridden by composer readings returning idle or unsent.
19
47
  */
20
48
  export declare function composeLines(data: ScreenData, lines: string[], clock?: ReadClock): Screen;
49
+ /**
50
+ * The input box a composer draws for its current text, or null when the composer does not read
51
+ * `unsent`: an idle box holds nothing, and every other shape is not the box. The rows are the
52
+ * ones the mode's own frame delimits — between the prompt row and the box's closing line; the
53
+ * `status-then-one` fallback (the captured completion frame) delimits nothing, so its box is
54
+ * its one input row, the popup rows below it being its filter's to exclude. The caller compares
55
+ * the box to the text it typed; nothing here decides that.
56
+ */
57
+ export declare function composerBox(data: ScreenData, lines: string[]): Box | null;