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,13 +1,14 @@
1
- import { verdict } from "./readings.js";
1
+ import { checkOf, countedFor, screenOf } from "./readings.js";
2
2
  const RANK = { session: 0, daily: 1, weekly: 2 };
3
3
  /**
4
- * The seat's account is its vendor. There is no separate account field on a seat. `budgets` is
5
- * the section in force: an unapproved edit to a reserve refuses no one until it is approved,
6
- * and an account only the unapproved edit names is not an account at all. `spend` holds the
7
- * stored spend check readings; a subscription account never looks at them.
4
+ * The seat's account is its own `account:` when the file names one — one vendor with two accounts
5
+ * is two buckets (§ 3b) — and its vendor otherwise. `budgets` is the section in force: an
6
+ * unapproved edit to a reserve refuses no one until it is approved, and an account only the
7
+ * unapproved edit names is not an account at all. `spend` holds the stored spend check readings;
8
+ * a subscription account never looks at them.
8
9
  */
9
10
  export function seatBudget(budgets, readings, seat, now, spend = []) {
10
- const name = seat.vendor;
11
+ const name = seat.account ?? seat.vendor;
11
12
  const account = budgets.accounts[name];
12
13
  if (!account)
13
14
  return { kind: 'clear' };
@@ -35,7 +36,9 @@ export function seatBudget(budgets, readings, seat, now, spend = []) {
35
36
  let counted = false;
36
37
  let worst = null;
37
38
  for (const group of windowsOf(mine)) {
38
- const result = verdict(group, now, staleAfterMs, account.reserve);
39
+ // The window's reading from the sources the account names, in order (§ 3, § 5): a check
40
+ // reading counts by its own freshness, ahead of a screen reading below it.
41
+ const result = countedFor(account.sources, screenOf(group), checkOf(group), now, staleAfterMs, account.reserve);
39
42
  if (result.kind === 'unknown') {
40
43
  unknown = true;
41
44
  continue;
@@ -46,8 +49,7 @@ export function seatBudget(budgets, readings, seat, now, spend = []) {
46
49
  continue;
47
50
  }
48
51
  counted = true;
49
- const inside = account.reserve !== null && result.reading.left <= account.reserve;
50
- if (!inside && result.kind !== 'refusing')
52
+ if (account.reserve === null || result.reading.left > account.reserve)
51
53
  continue;
52
54
  if (!worst
53
55
  || result.reading.left < worst.left
@@ -57,9 +59,11 @@ export function seatBudget(budgets, readings, seat, now, spend = []) {
57
59
  }
58
60
  if (worst && account.reserve !== null) {
59
61
  const room = accountsWithRoom(budgets, readings, now).filter((accountName) => accountName !== name);
62
+ // A check reading was measured, not changed on a screen: § 5's word for it.
63
+ const when = worst.source === 'check' ? 'read' : 'changed';
60
64
  return {
61
65
  kind: 'refuse',
62
- why: `${name} ${worst.window} left ${worst.left}%, inside its ${account.reserve}% reserve, changed ${age(now - worst.changedAt)} ago; accounts with room: ${room.length ? room.join(', ') : 'none'}`,
66
+ why: `${name} ${worst.window} left ${worst.left}%, inside its ${account.reserve}% reserve, ${when} ${age(now - worst.changedAt)} ago; accounts with room: ${room.length ? room.join(', ') : 'none'}`,
63
67
  };
64
68
  }
65
69
  if (unknown)
@@ -90,11 +94,11 @@ export function accountsWithRoom(budgets, readings, now, spend = []) {
90
94
  let counted = 0;
91
95
  let inside = false;
92
96
  for (const group of groups) {
93
- const result = verdict(group, now, staleAfterMs, account.reserve);
97
+ const result = countedFor(account.sources, screenOf(group), checkOf(group), now, staleAfterMs, account.reserve);
94
98
  if (result.kind === 'unknown' || result.kind === 'unconfirmed')
95
99
  continue;
96
100
  counted += 1;
97
- if (result.kind === 'refusing' || result.reading.left <= account.reserve)
101
+ if (result.reading.left <= account.reserve)
98
102
  inside = true;
99
103
  }
100
104
  if (counted > 0 && !inside)
@@ -121,7 +125,12 @@ function age(ms) {
121
125
  return `${hours}h`;
122
126
  return `${minutes}m`;
123
127
  }
124
- /** A money figure in a refusal: the cents are shown even when the file wrote none. */
128
+ /**
129
+ * A money figure in a refusal, as read: up to four decimals — the most a check or a floor may
130
+ * carry — with the trailing zeros beyond the cents dropped, and never fewer than two decimals.
131
+ * The figure the words judge is the figure the eyes see.
132
+ */
125
133
  function money(amount) {
126
- return amount.toFixed(2);
134
+ const written = amount.toFixed(4).replace(/0+$/, '');
135
+ return written.padEnd(written.indexOf('.') + 3, '0');
127
136
  }
@@ -1,4 +1,7 @@
1
1
  import type { QuotaFigure, WindowName } from '../profiles/quota.ts';
2
+ import type { CheckWindow } from './run.ts';
3
+ /** Where a reading came from: a pane's own status line, or an approved check command (§ 3). */
4
+ export type ReadingSource = 'status_line' | 'check';
2
5
  export type Seen = {
3
6
  account: string;
4
7
  window: WindowName;
@@ -6,7 +9,9 @@ export type Seen = {
6
9
  used: number;
7
10
  changedAt: number;
8
11
  resetsAt: number | null;
9
- seat: string;
12
+ /** The seat whose screen showed it; null for a check reading, which belongs to no seat. */
13
+ seat: string | null;
14
+ source: ReadingSource;
10
15
  confirmed: boolean;
11
16
  };
12
17
  export type StoredReading = {
@@ -16,11 +21,13 @@ export type StoredReading = {
16
21
  used: number;
17
22
  changedAt: string;
18
23
  resetsAt: string | null;
19
- seat: string;
24
+ seat: string | null;
25
+ /** Absent in state files written before sources were recorded: a screen reading then. */
26
+ source?: ReadingSource;
20
27
  confirmed: boolean;
21
28
  };
22
29
  export type Verdict = {
23
- kind: 'fresh' | 'unconfirmed' | 'stale' | 'refusing';
30
+ kind: 'fresh' | 'unconfirmed' | 'stale' | 'refusing' | 'last-seen';
24
31
  reading: Seen;
25
32
  } | {
26
33
  kind: 'unknown';
@@ -41,26 +48,67 @@ export type StoredSpend = {
41
48
  };
42
49
  /** Fold one seat's figure into the readings kept for this pass. */
43
50
  export declare function observe(list: readonly Seen[], figure: QuotaFigure, seat: string, now: number): Seen[];
51
+ /**
52
+ * Fold a check command's windows into the readings kept for this pass (§ 5). A check reading is
53
+ * confirmed at first sight and belongs to no seat; one is kept per account and window, so a new
54
+ * check replaces the last, and the screen readings of the same window are left alone.
55
+ */
56
+ export declare function observeCheck(list: readonly Seen[], account: string, windows: readonly CheckWindow[]): Seen[];
57
+ /** The screen readings in a list, and the check readings: the two slots of the same account. */
58
+ export declare function screenOf(list: readonly Seen[]): Seen[];
59
+ export declare function checkOf(list: readonly Seen[]): Seen[];
44
60
  /**
45
61
  * The reading that counts. A figure from before a known reset is dropped.
46
62
  * Among confirmed live readings, the newest change wins. A first sight counts
47
63
  * only when no confirmed reading is still live. Staleness is measured from
48
64
  * `changedAt`. A stale reading inside the reserve refuses until its reset;
49
- * with no reset time it is unknown.
65
+ * with no reset time it is unknown when it could still matter — inside the
66
+ * reserve or within the reserve again outside it — and last seen when it sits
67
+ * further out, room the account may still hold. It never refuses.
50
68
  */
51
69
  export declare function verdict(list: readonly Seen[], now: number, staleAfterMs: number, reserve: number | null): Verdict;
70
+ export type CountedReading = {
71
+ kind: 'counted';
72
+ reading: Seen;
73
+ } | {
74
+ kind: 'unconfirmed';
75
+ reading: Seen;
76
+ } | {
77
+ kind: 'unknown';
78
+ };
79
+ /**
80
+ * The reading that counts for one window, from the sources the account names in order (§ 3, § 5):
81
+ * a lower source is used only when every higher one is failed, unknown or stale. A check reading
82
+ * counts while it is fresh by § 5's own time and its reset has not passed; a screen reading falls
83
+ * through when it is unknown, and a bare first sight falls through too, so a fresh check below it
84
+ * still counts (§ 4.3 rule 5). A stale reading inside its reserve with its reset ahead is kept as
85
+ * the fallback, whichever source it came from, so rule 4 still refuses when nothing below counts.
86
+ * A stale figure with no known reset counts as the room last seen only when it is well outside
87
+ * the reserve, and it sits below any inside-reserve figure: a last sight never clears a refusal.
88
+ */
89
+ export declare function countedFor(sources: readonly ReadingSource[], screen: readonly Seen[], checks: readonly Seen[], now: number, staleAfterMs: number, reserve: number | null): CountedReading;
52
90
  /** Readings whose reset has passed are left out. One with no reset time is kept. */
53
91
  export declare function remember(list: readonly Seen[], now: number): Record<string, StoredReading>;
54
92
  export declare function recall(stored: Record<string, StoredReading> | undefined): Seen[];
55
- /** Write the readings that still count. The state file is the per-project cache. */
56
- export declare function saveReadings(dir: string, session: string, list: readonly Seen[], now?: number): void;
57
- export declare function loadReadings(dir: string, session: string): Seen[];
93
+ /** Write the readings that still count. The state file is the per-project cache (§ 4.4). */
94
+ export declare function saveReadings(dir: string, list: readonly Seen[], now?: number): void;
95
+ /**
96
+ * One pass's fold, read and written under the state lock (§ 4.4): `fold` is handed the readings
97
+ * the state holds and returns the list to keep, so a watch folding while another watch writes
98
+ * folds onto what was written, never over a list it read before it. Whatever the fold returns as
99
+ * its `value` is handed back.
100
+ */
101
+ export declare function updateReadings<T>(dir: string, now: number, fold: (stored: Seen[]) => {
102
+ readings: Seen[];
103
+ value: T;
104
+ }): T;
105
+ export declare function loadReadings(dir: string): Seen[];
58
106
  /**
59
107
  * Write the spend readings a pass's checks read, merging by account: an account whose check did
60
108
  * not run this pass keeps the reading the state already holds. Nothing read, nothing written.
61
109
  */
62
- export declare function saveSpendReadings(dir: string, session: string, list: readonly SpendReading[]): void;
63
- export declare function loadSpendReadings(dir: string, session: string): SpendReading[];
110
+ export declare function saveSpendReadings(dir: string, list: readonly SpendReading[]): void;
111
+ export declare function loadSpendReadings(dir: string): SpendReading[];
64
112
  export declare function storeSpend(reading: SpendReading): StoredSpend;
65
113
  export declare function recallSpend(stored: Record<string, StoredSpend> | undefined): SpendReading[];
66
114
  export declare function store(reading: Seen): StoredReading;
@@ -1,4 +1,4 @@
1
- import { emptySession, readState, updateState } from "../state.js";
1
+ import { readState, updateState } from "../state.js";
2
2
  /** Fold one seat's figure into the readings kept for this pass. */
3
3
  export function observe(list, figure, seat, now) {
4
4
  const next = list.slice();
@@ -19,6 +19,7 @@ export function observe(list, figure, seat, now) {
19
19
  changedAt: same && previous ? previous.changedAt : now,
20
20
  resetsAt: same && previous && previous.resetsAt !== null ? previous.resetsAt : resetsFrom(figure.resets, now),
21
21
  seat,
22
+ source: 'status_line',
22
23
  confirmed: previous === null ? agreed : same ? previous.confirmed || agreed : true,
23
24
  };
24
25
  if (at < 0)
@@ -27,12 +28,56 @@ export function observe(list, figure, seat, now) {
27
28
  next[at] = reading;
28
29
  return next;
29
30
  }
31
+ /**
32
+ * Fold a check command's windows into the readings kept for this pass (§ 5). A check reading is
33
+ * confirmed at first sight and belongs to no seat; one is kept per account and window, so a new
34
+ * check replaces the last, and the screen readings of the same window are left alone.
35
+ */
36
+ export function observeCheck(list, account, windows) {
37
+ const next = list.slice();
38
+ for (const window of windows) {
39
+ const reading = {
40
+ account,
41
+ window: window.window,
42
+ left: window.left,
43
+ used: window.used,
44
+ changedAt: window.at,
45
+ resetsAt: window.resetsAt,
46
+ seat: null,
47
+ source: 'check',
48
+ confirmed: true,
49
+ };
50
+ const at = next.findIndex((item) => item.account === account && item.window === window.window && item.source === 'check');
51
+ if (at < 0)
52
+ next.push(reading);
53
+ else
54
+ next[at] = reading;
55
+ }
56
+ return next;
57
+ }
58
+ /** The screen readings in a list, and the check readings: the two slots of the same account. */
59
+ export function screenOf(list) {
60
+ return list.filter((item) => item.source === 'status_line');
61
+ }
62
+ export function checkOf(list) {
63
+ return list.filter((item) => item.source === 'check');
64
+ }
65
+ /**
66
+ * The distance rule for a stale figure with no known reset (§ 5), in one place: more than the
67
+ * reserve again outside it, the figure is the room last seen rather than unknown. `verdict` and
68
+ * the check branch of `countedFor` both ask this, so the two cannot be edited apart.
69
+ */
70
+ function pastTheReserveAgain(left, reserve) {
71
+ return left > reserve * 2;
72
+ }
30
73
  /**
31
74
  * The reading that counts. A figure from before a known reset is dropped.
32
75
  * Among confirmed live readings, the newest change wins. A first sight counts
33
76
  * only when no confirmed reading is still live. Staleness is measured from
34
77
  * `changedAt`. A stale reading inside the reserve refuses until its reset;
35
- * with no reset time it is unknown.
78
+ * with no reset time it is unknown when it could still matter — inside the
79
+ * reserve or within the reserve again outside it — and last seen when it sits
80
+ * further out, room the account may still hold. It never refuses.
36
81
  */
37
82
  export function verdict(list, now, staleAfterMs, reserve) {
38
83
  const live = list.filter((item) => item.resetsAt === null || item.resetsAt > now);
@@ -52,17 +97,77 @@ export function verdict(list, now, staleAfterMs, reserve) {
52
97
  const inside = reserve !== null && reading.left <= reserve;
53
98
  if (inside && reading.resetsAt !== null)
54
99
  return { kind: 'refusing', reading };
55
- if (reading.resetsAt === null)
100
+ if (reading.resetsAt === null) {
101
+ if (reserve !== null && pastTheReserveAgain(reading.left, reserve))
102
+ return { kind: 'last-seen', reading };
56
103
  return { kind: 'unknown' };
104
+ }
57
105
  return { kind: 'stale', reading };
58
106
  }
107
+ /**
108
+ * The reading that counts for one window, from the sources the account names in order (§ 3, § 5):
109
+ * a lower source is used only when every higher one is failed, unknown or stale. A check reading
110
+ * counts while it is fresh by § 5's own time and its reset has not passed; a screen reading falls
111
+ * through when it is unknown, and a bare first sight falls through too, so a fresh check below it
112
+ * still counts (§ 4.3 rule 5). A stale reading inside its reserve with its reset ahead is kept as
113
+ * the fallback, whichever source it came from, so rule 4 still refuses when nothing below counts.
114
+ * A stale figure with no known reset counts as the room last seen only when it is well outside
115
+ * the reserve, and it sits below any inside-reserve figure: a last sight never clears a refusal.
116
+ */
117
+ export function countedFor(sources, screen, checks, now, staleAfterMs, reserve) {
118
+ let fallback = null;
119
+ let lastSeen = null;
120
+ let unconfirmed = null;
121
+ for (const source of sources) {
122
+ if (source === 'check') {
123
+ const reading = checks.find((item) => (item.resetsAt === null || item.resetsAt > now) && now - item.changedAt < staleAfterMs);
124
+ if (reading)
125
+ return { kind: 'counted', reading };
126
+ // Rule 4 is about the reserve, not the source: a stale check figure inside it keeps
127
+ // refusing until its known reset, and a fresh source below still wins over this fallback.
128
+ const inside = checks.find((item) => item.resetsAt !== null && item.resetsAt > now && reserve !== null && item.left <= reserve);
129
+ if (inside)
130
+ fallback ??= inside;
131
+ // A stale check figure with no reset, past the reserve again, is the room last seen.
132
+ const sight = checks.find((item) => item.resetsAt === null && reserve !== null && pastTheReserveAgain(item.left, reserve));
133
+ if (sight)
134
+ lastSeen ??= sight;
135
+ continue;
136
+ }
137
+ const result = verdict(screen, now, staleAfterMs, reserve);
138
+ if (result.kind === 'unknown')
139
+ continue;
140
+ if (result.kind === 'unconfirmed') {
141
+ unconfirmed ??= result.reading;
142
+ continue;
143
+ }
144
+ if (result.kind === 'fresh')
145
+ return { kind: 'counted', reading: result.reading };
146
+ if (result.kind === 'last-seen') {
147
+ lastSeen ??= result.reading;
148
+ continue;
149
+ }
150
+ fallback ??= result.reading;
151
+ }
152
+ if (fallback)
153
+ return { kind: 'counted', reading: fallback };
154
+ if (lastSeen)
155
+ return { kind: 'counted', reading: lastSeen };
156
+ if (unconfirmed)
157
+ return { kind: 'unconfirmed', reading: unconfirmed };
158
+ return { kind: 'unknown' };
159
+ }
59
160
  /** Readings whose reset has passed are left out. One with no reset time is kept. */
60
161
  export function remember(list, now) {
61
162
  const out = {};
62
163
  for (const reading of list) {
63
164
  if (reading.resetsAt !== null && reading.resetsAt <= now)
64
165
  continue;
65
- out[`${reading.account}/${reading.window}/${reading.seat}`] = store(reading);
166
+ // A screen reading is kept per seat; a check reading belongs to no seat and is kept alone.
167
+ const key = reading.source === 'check'
168
+ ? `${reading.account}/${reading.window}`
169
+ : `${reading.account}/${reading.window}/${reading.seat}`;
170
+ out[key] = store(reading);
66
171
  }
67
172
  return out;
68
173
  }
@@ -71,35 +176,44 @@ export function recall(stored) {
71
176
  return [];
72
177
  return Object.values(stored).map(revive);
73
178
  }
74
- /** Write the readings that still count. The state file is the per-project cache. */
75
- export function saveReadings(dir, session, list, now = Date.now()) {
179
+ /** Write the readings that still count. The state file is the per-project cache (§ 4.4). */
180
+ export function saveReadings(dir, list, now = Date.now()) {
76
181
  updateState(dir, (state) => {
77
- const current = state.sessions[session] ?? emptySession();
78
- current.budgets = remember(list, now);
79
- state.sessions[session] = current;
182
+ state.budgets = remember(list, now);
183
+ });
184
+ }
185
+ /**
186
+ * One pass's fold, read and written under the state lock (§ 4.4): `fold` is handed the readings
187
+ * the state holds and returns the list to keep, so a watch folding while another watch writes
188
+ * folds onto what was written, never over a list it read before it. Whatever the fold returns as
189
+ * its `value` is handed back.
190
+ */
191
+ export function updateReadings(dir, now, fold) {
192
+ return updateState(dir, (state) => {
193
+ const folded = fold(recall(state.budgets));
194
+ state.budgets = remember(folded.readings, now);
195
+ return folded.value;
80
196
  });
81
197
  }
82
- export function loadReadings(dir, session) {
83
- return recall(readState(dir).sessions[session]?.budgets);
198
+ export function loadReadings(dir) {
199
+ return recall(readState(dir).budgets);
84
200
  }
85
201
  /**
86
202
  * Write the spend readings a pass's checks read, merging by account: an account whose check did
87
203
  * not run this pass keeps the reading the state already holds. Nothing read, nothing written.
88
204
  */
89
- export function saveSpendReadings(dir, session, list) {
205
+ export function saveSpendReadings(dir, list) {
90
206
  if (!list.length)
91
207
  return;
92
208
  updateState(dir, (state) => {
93
- const current = state.sessions[session] ?? emptySession();
94
- const spend = current.spend ?? {};
209
+ const spend = state.spend ?? {};
95
210
  for (const reading of list)
96
211
  spend[reading.account] = storeSpend(reading);
97
- current.spend = spend;
98
- state.sessions[session] = current;
212
+ state.spend = spend;
99
213
  });
100
214
  }
101
- export function loadSpendReadings(dir, session) {
102
- return recallSpend(readState(dir).sessions[session]?.spend);
215
+ export function loadSpendReadings(dir) {
216
+ return recallSpend(readState(dir).spend);
103
217
  }
104
218
  export function storeSpend(reading) {
105
219
  return {
@@ -128,6 +242,7 @@ export function store(reading) {
128
242
  changedAt: new Date(reading.changedAt).toISOString(),
129
243
  resetsAt: reading.resetsAt === null ? null : new Date(reading.resetsAt).toISOString(),
130
244
  seat: reading.seat,
245
+ source: reading.source,
131
246
  confirmed: reading.confirmed,
132
247
  };
133
248
  }
@@ -140,6 +255,7 @@ export function revive(stored) {
140
255
  changedAt: Date.parse(stored.changedAt),
141
256
  resetsAt: stored.resetsAt === null ? null : Date.parse(stored.resetsAt),
142
257
  seat: stored.seat,
258
+ source: stored.source ?? 'status_line',
143
259
  confirmed: stored.confirmed,
144
260
  };
145
261
  }
@@ -12,10 +12,16 @@ const SPEND = /^([0-9]+(?:\.[0-9]{1,4})?) ([A-Z]{3})$/;
12
12
  // § 5: one to three lines, each a window, each figure whole and at most 100. The
13
13
  // printed reset is a duration; the printed measurement time is unix seconds.
14
14
  const LINE = /^(session|daily|weekly) (100|[0-9]{1,2})% (used|left)(?: resets ([0-9]+h(?:[0-9]+m)?|[0-9]+m))?(?: at ([0-9]{10}))?$/;
15
- /** An output's lines: one trailing newline allowed, and nothing else. */
15
+ /**
16
+ * An output's lines: one trailing newline allowed, and nothing else. A "\r\n" ending reads
17
+ * as its line — the CR goes with the newline — and a CR anywhere else stays a character of
18
+ * the line, off the contract.
19
+ */
16
20
  function linesOf(text) {
17
21
  const body = text.endsWith('\n') ? text.slice(0, -1) : text;
18
- return body === '' ? null : body.split('\n');
22
+ if (body === '')
23
+ return null;
24
+ return body.split('\n').map((line) => (line.endsWith('\r') ? line.slice(0, -1) : line));
19
25
  }
20
26
  /**
21
27
  * A subscription output against § 5. Any line off the contract, a figure over
@@ -1,6 +1,6 @@
1
1
  import type { TeamFile } from '../file/types.ts';
2
2
  import type { WindowName } from '../profiles/quota.ts';
3
- import { type Seen } from './readings.ts';
3
+ import { type ReadingSource, type Seen } from './readings.ts';
4
4
  export type BudgetState = 'fresh' | 'unconfirmed' | 'stale' | 'refusing' | 'unknown';
5
5
  export type BudgetRow = {
6
6
  account: string;
@@ -10,7 +10,9 @@ export type BudgetRow = {
10
10
  resetsIn: string | null;
11
11
  seat: string | null;
12
12
  age: string | null;
13
- source: 'status_line' | null;
13
+ source: ReadingSource | null;
14
+ /** True when the counted figure did not come from the first source the account names (§ 3). */
15
+ fallback: boolean;
14
16
  state: BudgetState;
15
17
  /** True when a subscription's left figure is at or inside its reserve. */
16
18
  inside: boolean;
@@ -1,4 +1,4 @@
1
- import { verdict } from "./readings.js";
1
+ import { checkOf, countedFor, screenOf } from "./readings.js";
2
2
  const WINDOWS = ['session', 'daily', 'weekly'];
3
3
  /** Rows for the accounts the budgets in force name, and for any reading still in the state. */
4
4
  export function budgetTable(budgets, list, now) {
@@ -17,7 +17,12 @@ export function budgetTable(budgets, list, now) {
17
17
  const window = group[0]?.window ?? null;
18
18
  named.add(account);
19
19
  const reserve = reserveOf(budgets, account);
20
- rows.push(rowOf(account, window, verdict(group, now, staleAfterMs, reserve), now, reserve));
20
+ // The window's reading from the sources the account names, in order (§ 3, § 5).
21
+ const sources = sourcesOf(budgets, account);
22
+ const result = countedFor(sources, screenOf(group), checkOf(group), now, staleAfterMs, reserve);
23
+ rows.push(result.kind === 'unknown'
24
+ ? { ...blank(account, reserve), window }
25
+ : rowOf(result, now, staleAfterMs, reserve, result.reading.source !== sources[0]));
21
26
  }
22
27
  for (const account of Object.keys(budgets.accounts)) {
23
28
  if (!named.has(account))
@@ -35,25 +40,37 @@ export function budgetLine(row) {
35
40
  return [row.account, row.window, 'unknown'].filter((part) => part).join(' ');
36
41
  }
37
42
  const reset = row.resetsIn === null ? 'resets unknown' : `resets in ${row.resetsIn}`;
38
- const from = row.source === 'status_line' ? 'status line' : 'unknown source';
43
+ const from = row.source === 'status_line' ? 'status line' : row.source === 'check' ? 'check' : 'unknown source';
44
+ const source = row.fallback ? `${from} (fallback)` : from;
45
+ // A stale row with no known reset is the room last seen, not a reading aged out.
46
+ const when = row.state === 'stale' && row.resetsIn === null ? 'last seen' : row.source === 'check' ? 'read' : 'changed';
39
47
  const state = row.inside && row.reserve !== null ? `${row.state}, inside reserve ${row.reserve}%` : row.state;
40
- return `${row.account} ${row.window} left ${row.left}% used ${row.used}% ${reset} ${row.seat} changed ${row.age} ago ${from} ${state}`;
48
+ return `${row.account} ${row.window} left ${row.left}% used ${row.used}% ${reset} ${row.seat ?? '-'} ${when} ${row.age} ago ${source} ${state}`;
41
49
  }
42
- function rowOf(account, window, result, now, reserve) {
43
- if (result.kind === 'unknown')
44
- return { ...blank(account, reserve), window };
50
+ /** The sources the account's figures are read from, in order: a status line when the file is silent. */
51
+ function sourcesOf(budgets, account) {
52
+ return budgets.accounts[account]?.sources ?? ['status_line'];
53
+ }
54
+ function rowOf(result, now, staleAfterMs, reserve, fallback) {
45
55
  const reading = result.reading;
56
+ const inside = reserve !== null && reading.left <= reserve;
57
+ const state = result.kind === 'unconfirmed'
58
+ ? 'unconfirmed'
59
+ : now - reading.changedAt < staleAfterMs
60
+ ? 'fresh'
61
+ : inside && reading.resetsAt !== null ? 'refusing' : 'stale';
46
62
  return {
47
- account,
63
+ account: reading.account,
48
64
  window: reading.window,
49
65
  left: reading.left,
50
66
  used: reading.used,
51
67
  resetsIn: reading.resetsAt === null ? null : span(reading.resetsAt - now),
52
68
  seat: reading.seat,
53
69
  age: span(now - reading.changedAt),
54
- source: 'status_line',
55
- state: result.kind,
56
- inside: reserve !== null && reading.left <= reserve,
70
+ source: reading.source,
71
+ fallback,
72
+ state,
73
+ inside,
57
74
  reserve,
58
75
  };
59
76
  }
@@ -67,6 +84,7 @@ function blank(account, reserve) {
67
84
  seat: null,
68
85
  age: null,
69
86
  source: null,
87
+ fallback: false,
70
88
  state: 'unknown',
71
89
  inside: false,
72
90
  reserve,
package/dist/cli.d.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import type { Command, Io } from './io.ts';
3
+ import { version } from './version.ts';
3
4
  export declare const commands: Record<string, () => Promise<{
4
5
  default: Command;
5
6
  USAGE: string;
6
7
  }>>;
7
- export declare function version(): string;
8
+ export { version };
8
9
  export declare function main(argv: string[], io: Io): Promise<number>;
9
10
  export declare function processIo(): Io;
package/dist/cli.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import { readFileSync, realpathSync } from 'node:fs';
2
+ import { realpathSync } from 'node:fs';
3
3
  import { fileURLToPath } from 'node:url';
4
+ import { version } from "./version.js";
4
5
  // Each command is loaded only when it is called. A slice adds its line here. The table is exported
5
6
  // so a test can walk it: every command, `--help`, `-h` and a usage line.
6
7
  export const commands = {
@@ -29,10 +30,7 @@ Options:
29
30
 
30
31
  The team is declared in <project>/.agents/team.yaml.
31
32
  `;
32
- export function version() {
33
- const text = readFileSync(new URL('../package.json', import.meta.url), 'utf8');
34
- return JSON.parse(text).version;
35
- }
33
+ export { version };
36
34
  export async function main(argv, io) {
37
35
  const [name, ...rest] = argv;
38
36
  const names = Object.keys(commands);
@@ -45,6 +43,11 @@ export async function main(argv, io) {
45
43
  io.stdout(`${version()}\n`);
46
44
  return 0;
47
45
  }
46
+ // Hidden: a port speaks this protocol. It is not a command in the help text.
47
+ if (name === 'conformance-adapter') {
48
+ const { default: run } = await import("./conformance/adapter.js");
49
+ return run(rest, io);
50
+ }
48
51
  const load = commands[name];
49
52
  if (!load) {
50
53
  io.stderr(`team: unknown command "${name}"\n\n${usage}`);
@@ -9,7 +9,6 @@ export type AddSources = {
9
9
  agents(session: string): HerdrAgent[] | null;
10
10
  workspaces(session: string): {
11
11
  id: string;
12
- label: string;
13
12
  }[] | null;
14
13
  doctor: DoctorSources;
15
14
  machine?: (root: string) => Machine;