team 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/README.md +22 -11
  2. package/dist/approve/approval.js +14 -9
  3. package/dist/approve/fingerprint.d.ts +14 -2
  4. package/dist/approve/fingerprint.js +21 -21
  5. package/dist/budgets/gate.js +7 -2
  6. package/dist/budgets/readings.d.ts +6 -2
  7. package/dist/budgets/readings.js +28 -2
  8. package/dist/budgets/run.js +8 -2
  9. package/dist/budgets/table.d.ts +2 -0
  10. package/dist/budgets/table.js +10 -5
  11. package/dist/cli.d.ts +2 -1
  12. package/dist/cli.js +8 -5
  13. package/dist/commands/add.d.ts +0 -1
  14. package/dist/commands/add.js +7 -5
  15. package/dist/commands/approve.js +17 -1
  16. package/dist/commands/doctor.d.ts +15 -1
  17. package/dist/commands/doctor.js +126 -8
  18. package/dist/commands/down.d.ts +4 -0
  19. package/dist/commands/down.js +32 -13
  20. package/dist/commands/init.d.ts +1 -1
  21. package/dist/commands/init.js +4 -2
  22. package/dist/commands/remove.js +5 -2
  23. package/dist/commands/status.d.ts +2 -0
  24. package/dist/commands/status.js +19 -1
  25. package/dist/commands/up.js +3 -1
  26. package/dist/commands/watch.d.ts +2 -0
  27. package/dist/commands/watch.js +28 -4
  28. package/dist/conformance/adapter.d.ts +5 -0
  29. package/dist/conformance/adapter.js +44 -0
  30. package/dist/file/check.d.ts +20 -0
  31. package/dist/file/check.js +112 -0
  32. package/dist/file/lines.js +13 -2
  33. package/dist/file/sections/budgets.d.ts +15 -0
  34. package/dist/file/sections/budgets.js +180 -0
  35. package/dist/file/sections/coordinator.d.ts +2 -0
  36. package/dist/file/sections/coordinator.js +11 -0
  37. package/dist/file/sections/format.d.ts +2 -0
  38. package/dist/file/sections/format.js +13 -0
  39. package/dist/file/sections/identity.d.ts +2 -0
  40. package/dist/file/sections/identity.js +103 -0
  41. package/dist/file/sections/index.d.ts +9 -0
  42. package/dist/file/sections/index.js +43 -0
  43. package/dist/file/sections/lead.d.ts +4 -0
  44. package/dist/file/sections/lead.js +18 -0
  45. package/dist/file/sections/limits.d.ts +2 -0
  46. package/dist/file/sections/limits.js +43 -0
  47. package/dist/file/sections/machine.d.ts +2 -0
  48. package/dist/file/sections/machine.js +38 -0
  49. package/dist/file/sections/operator.d.ts +2 -0
  50. package/dist/file/sections/operator.js +11 -0
  51. package/dist/file/sections/project.d.ts +2 -0
  52. package/dist/file/sections/project.js +13 -0
  53. package/dist/file/sections/rules.d.ts +2 -0
  54. package/dist/file/sections/rules.js +9 -0
  55. package/dist/file/sections/seats.d.ts +7 -0
  56. package/dist/file/sections/seats.js +161 -0
  57. package/dist/file/sections/section.d.ts +32 -0
  58. package/dist/file/sections/section.js +7 -0
  59. package/dist/file/sections/session.d.ts +2 -0
  60. package/dist/file/sections/session.js +16 -0
  61. package/dist/file/sections/tools.d.ts +2 -0
  62. package/dist/file/sections/tools.js +43 -0
  63. package/dist/file/sections/trust.d.ts +2 -0
  64. package/dist/file/sections/trust.js +20 -0
  65. package/dist/file/sections/units.d.ts +16 -0
  66. package/dist/file/sections/units.js +8 -0
  67. package/dist/file/sections/visibility.d.ts +2 -0
  68. package/dist/file/sections/visibility.js +12 -0
  69. package/dist/file/sections/watch-checks.d.ts +9 -0
  70. package/dist/file/sections/watch-checks.js +14 -0
  71. package/dist/file/sections/watch.d.ts +14 -0
  72. package/dist/file/sections/watch.js +111 -0
  73. package/dist/file/sections/workspace.d.ts +4 -0
  74. package/dist/file/sections/workspace.js +77 -0
  75. package/dist/file/types.d.ts +1 -0
  76. package/dist/file/validate.d.ts +4 -12
  77. package/dist/file/validate.js +45 -688
  78. package/dist/herdr.d.ts +1 -0
  79. package/dist/herdr.js +11 -1
  80. package/dist/launch/agent.js +4 -3
  81. package/dist/launch/deliver.d.ts +4 -0
  82. package/dist/launch/deliver.js +151 -8
  83. package/dist/launch/execute.d.ts +2 -0
  84. package/dist/launch/execute.js +21 -11
  85. package/dist/launch/plan.js +16 -2
  86. package/dist/profiles/antigravity.yaml +12 -2
  87. package/dist/profiles/claude-code.yaml +21 -6
  88. package/dist/profiles/codex.yaml +14 -1
  89. package/dist/profiles/cursor.yaml +45 -5
  90. package/dist/profiles/overrides.d.ts +67 -0
  91. package/dist/profiles/overrides.js +190 -0
  92. package/dist/profiles/profile.d.ts +3 -0
  93. package/dist/profiles/profile.js +5 -1
  94. package/dist/state.d.ts +2 -0
  95. package/dist/status/compare.js +7 -4
  96. package/dist/store/store.d.ts +5 -0
  97. package/dist/version.d.ts +1 -0
  98. package/dist/version.js +7 -0
  99. package/dist/watch/checks/budget.js +49 -53
  100. package/dist/watch/dialect.d.ts +6 -1
  101. package/dist/watch/dialect.js +68 -12
  102. package/dist/watch/pass.d.ts +8 -1
  103. package/dist/watch/pass.js +19 -9
  104. package/dist/watch/screen-core.d.ts +41 -4
  105. package/dist/watch/screen-core.js +458 -93
  106. package/dist/watch/screen-data.d.ts +25 -0
  107. package/dist/watch/screen-file.d.ts +8 -2
  108. package/dist/watch/screen-file.js +325 -38
  109. package/dist/watch/screen-profile.d.ts +19 -0
  110. package/dist/watch/screen-profile.js +1 -0
  111. package/dist/watch/screen.d.ts +22 -4
  112. package/dist/watch/screen.js +39 -5
  113. package/package.json +9 -3
  114. package/schema/team.schema.json +498 -0
@@ -0,0 +1,190 @@
1
+ // The owner's override file. It sits beside the approval and may only add dialog
2
+ // patterns and quota patterns to a profile this version ships. It cannot take a
3
+ // shipped pattern out, and it cannot change a composer, a prompt, a footer, a
4
+ // launch line or the order of the stages: anything else in the file is refused
5
+ // with its line. An edit takes effect only once `approve` records the text: until
6
+ // then the approved copy stays in force, and a copy that cannot be read leaves
7
+ // the shipped profiles.
8
+ import { readFileSync } from 'node:fs';
9
+ import { join } from 'node:path';
10
+ import { profileFor, quotaFor, quotaList } from "./profile.js";
11
+ import { readApproval, storePath } from "../store/store.js";
12
+ import { addedRules } from "../watch/screen-file.js";
13
+ import { classifyData, screenData } from "../watch/screen.js";
14
+ import { YamlError, parseYaml } from "../yaml.js";
15
+ export const DIALOG_STAGES = ['unknown', 'trust', 'permission', 'question'];
16
+ /** `<store>/overrides.yaml`, the same folder as the approval for this root. */
17
+ export function overridesPath(project, root, home) {
18
+ return join(storePath(project, root, home), 'overrides.yaml');
19
+ }
20
+ /** The file's added patterns, or the line that refuses it. One error, the first. */
21
+ export function parseOverrides(text) {
22
+ try {
23
+ return { ok: true, profiles: profilesOf(parseYaml(text)) };
24
+ }
25
+ catch (error) {
26
+ if (error instanceof YamlError)
27
+ return { ok: false, errors: [{ line: error.line, message: error.message }] };
28
+ throw error;
29
+ }
30
+ }
31
+ /**
32
+ * Shipped rules stay first, and the added ones follow. The composer and the
33
+ * chrome are the shipped profile's own objects, so an override cannot retune
34
+ * them by merging.
35
+ */
36
+ export function mergeScreen(base, added) {
37
+ const next = { ...base };
38
+ for (const name of DIALOG_STAGES) {
39
+ const extra = added.screen[name];
40
+ if (!extra)
41
+ continue;
42
+ const existing = base[name];
43
+ next[name] = { rules: [...(existing?.rules ?? []), ...extra.rules] };
44
+ }
45
+ return next;
46
+ }
47
+ const KEPT = new Set(['permission', 'trust', 'question']);
48
+ const DIALOG = new Set(['unknown', 'trust', 'permission', 'question']);
49
+ /**
50
+ * An added pattern may move a reading toward a dialog. It may not make a screen
51
+ * idle or unsent when the shipped profile does not, and it may not stop a
52
+ * shipped permission, trust or question pattern from matching.
53
+ */
54
+ export function guardReading(shipped, merged) {
55
+ if (KEPT.has(shipped.kind))
56
+ return shipped;
57
+ if ((merged.kind === 'idle' || merged.kind === 'unsent') && merged.kind !== shipped.kind)
58
+ return shipped;
59
+ if (!DIALOG.has(merged.kind))
60
+ return shipped;
61
+ return merged;
62
+ }
63
+ /** The shipped reading, then the same window with this profile's added patterns, guarded. */
64
+ export function classifyWith(cli, screen, profiles) {
65
+ if (screen === undefined)
66
+ return { kind: 'unknown' };
67
+ const base = screenData(cli);
68
+ if (!base)
69
+ return { kind: 'unknown' };
70
+ const lines = screen.split('\n');
71
+ const shipped = classifyData(base, lines);
72
+ const added = profiles.find((profile) => profile.cli === cli);
73
+ if (!added || DIALOG_STAGES.every((stage) => added.screen[stage] === undefined))
74
+ return shipped;
75
+ return guardReading(shipped, classifyData(mergeScreen(base, added), lines));
76
+ }
77
+ /** Shipped quota patterns, then the override's. The shipped ones are never dropped. */
78
+ export function quotaWith(cli, profiles) {
79
+ const added = profiles.find((profile) => profile.cli === cli)?.quota ?? [];
80
+ return [...quotaFor(cli), ...added];
81
+ }
82
+ /** What `approve` records when the override file itself is the change. */
83
+ export const OVERRIDE_CHANGED = '`overrides` changed';
84
+ /** A stored copy that does not parse. The shipped profiles stay in force. */
85
+ export const OVERRIDE_UNREADABLE = "the approved overrides can't be read";
86
+ /** The override file's text, and why it was refused. A missing file is an empty text and no problem. */
87
+ export function overrideFile(project, root, home) {
88
+ const path = overridesPath(project, root, home);
89
+ try {
90
+ const text = readFileSync(path, 'utf8');
91
+ const parsed = parseOverrides(text);
92
+ return { path, text, problems: parsed.ok ? [] : parsed.errors.map((problem) => overrideProblem(path, problem)) };
93
+ }
94
+ catch (error) {
95
+ if (error.code === 'ENOENT')
96
+ return { path, text: null, problems: [] };
97
+ return { path, text: null, problems: [overrideProblem(path, { line: 0, message: "can't be read" })] };
98
+ }
99
+ }
100
+ /**
101
+ * The patterns in force for this project. Keyed on the project root, the same
102
+ * store as the team-file approval: another checkout of the same name has its
103
+ * own. No file, or a stored copy that cannot be read, leaves the shipped profiles.
104
+ * The live file is used only when it is the text `approve` recorded.
105
+ */
106
+ export function overridesInForce(project, root, home) {
107
+ const record = readApproval(storePath(project, root, home));
108
+ const recorded = record !== null && Object.hasOwn(record.approval, 'overrides');
109
+ const stored = recorded ? record?.approval.overrides : undefined;
110
+ const approvedText = typeof stored === 'string' ? stored : null;
111
+ const live = overrideFile(project, root, home);
112
+ const differences = [];
113
+ const profiles = [];
114
+ if (approvedText !== null) {
115
+ const parsed = parseOverrides(approvedText);
116
+ if (parsed.ok)
117
+ profiles.push(...parsed.profiles);
118
+ else
119
+ differences.push(OVERRIDE_UNREADABLE);
120
+ }
121
+ else if (recorded && stored !== null && stored !== undefined) {
122
+ differences.push(OVERRIDE_UNREADABLE);
123
+ }
124
+ if (live.text !== approvedText) {
125
+ if (record === null) {
126
+ if (live.text !== null)
127
+ differences.push('the overrides were never approved');
128
+ }
129
+ else
130
+ differences.push(OVERRIDE_CHANGED);
131
+ }
132
+ return { profiles, differences, problems: live.problems };
133
+ }
134
+ export function overrideProblem(path, problem) {
135
+ return problem.line ? `${path}: line ${problem.line}: ${problem.message}` : `${path}: ${problem.message}`;
136
+ }
137
+ function profilesOf(root) {
138
+ const entries = mapping(root, 'an override file');
139
+ const format = required(entries, 'format', root.line);
140
+ if (format.value.kind !== 'scalar' || format.value.value !== 1)
141
+ fail(format.line, '"format" must be 1');
142
+ only(entries, ['format', 'profiles']);
143
+ const profiles = required(entries, 'profiles', root.line);
144
+ const named = mapping(profiles.value, '"profiles"');
145
+ if (named.length === 0)
146
+ fail(profiles.line, '"profiles" must name a profile');
147
+ return named.map(profileOf);
148
+ }
149
+ function profileOf(entry) {
150
+ if (!profileFor(entry.key))
151
+ fail(entry.line, `unknown profile "${entry.key}"`);
152
+ const entries = mapping(entry.value, 'a profile override');
153
+ only(entries, ['screen', 'quota']);
154
+ const screen = optional(entries, 'screen');
155
+ const quota = optional(entries, 'quota');
156
+ if (!screen && !quota)
157
+ fail(entry.line, 'a profile override adds dialog patterns or quota patterns');
158
+ const stages = screen ? screenOf(screen.value) : {};
159
+ if (screen && Object.keys(stages).length === 0 && !quota) {
160
+ fail(screen.line, 'a profile override adds dialog patterns or quota patterns');
161
+ }
162
+ return { cli: entry.key, screen: stages, quota: quota ? quotaList(quota.value) : [] };
163
+ }
164
+ function screenOf(node) {
165
+ const entries = mapping(node, 'screen');
166
+ only(entries, DIALOG_STAGES);
167
+ const screen = {};
168
+ for (const entry of entries)
169
+ screen[entry.key] = { rules: addedRules(entry.value) };
170
+ return screen;
171
+ }
172
+ function mapping(node, what) {
173
+ if (node.kind !== 'map')
174
+ fail(node.line, `${what} must be a map`);
175
+ return node.entries;
176
+ }
177
+ function only(entries, allowed) {
178
+ for (const entry of entries)
179
+ if (!allowed.includes(entry.key))
180
+ fail(entry.line, `unknown key "${entry.key}"`);
181
+ }
182
+ function required(entries, key, line) {
183
+ return optional(entries, key) ?? fail(line, `missing "${key}"`);
184
+ }
185
+ function optional(entries, key) {
186
+ return entries.find((entry) => entry.key === key);
187
+ }
188
+ function fail(line, message) {
189
+ throw new YamlError(line, message);
190
+ }
@@ -1,4 +1,5 @@
1
1
  import { type QuotaPattern } from './quota.ts';
2
+ import { type YamlNode } from '../yaml.ts';
2
3
  /**
3
4
  * A launch profile: what `team` knows about one CLI, so a seat can't start
4
5
  * blocked. Keyed by the seat's `cli`.
@@ -53,6 +54,8 @@ export declare function profileFor(cli: string): Profile | null;
53
54
  export declare function quotaFor(cli: string): readonly QuotaPattern[];
54
55
  /** Patterns from a YAML list, for a profile snippet. A bad pattern throws. */
55
56
  export declare function quotaPatterns(text: string): QuotaPattern[];
57
+ /** Quota patterns from a list already parsed. A bad pattern throws. */
58
+ export declare function quotaList(node: YamlNode): QuotaPattern[];
56
59
  /** The model a status line names. `unreadable` is a line the rules claim that does not name one. */
57
60
  export declare function statusOnLine(cli: string, line: string): {
58
61
  model: string;
@@ -61,6 +61,10 @@ export function quotaFor(cli) {
61
61
  export function quotaPatterns(text) {
62
62
  return quotaOf(parseYaml(text));
63
63
  }
64
+ /** Quota patterns from a list already parsed. A bad pattern throws. */
65
+ export function quotaList(node) {
66
+ return quotaOf(node);
67
+ }
64
68
  /** The model a status line names. `unreadable` is a line the rules claim that does not name one. */
65
69
  export function statusOnLine(cli, line) {
66
70
  const rules = SHIPPED[cli]?.status;
@@ -84,7 +88,7 @@ function launchOf(root) {
84
88
  const format = required(entries, 'format', root.line);
85
89
  if (format.value.kind !== 'scalar' || format.value.value !== 1)
86
90
  fail(format.line, '"format" must be 1');
87
- only(entries, ['format', 'cli', 'screen', 'quota', ...LAUNCH_KEYS]);
91
+ only(entries, ['format', 'cli', 'screen', 'screen_module', 'quota', ...LAUNCH_KEYS]);
88
92
  const cli = text(required(entries, 'cli', root.line), 'cli');
89
93
  required(entries, 'screen', root.line);
90
94
  const quotaEntry = optional(entries, 'quota');
package/dist/state.d.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import type { StoredReading, StoredSpend } from './budgets/readings.ts';
2
2
  export type SeatState = {
3
3
  stage: 'launched' | 'named' | 'ready';
4
+ /** The CLI the seat was launched with: `down` stops it under it when the file renamed it. */
5
+ cli?: string;
4
6
  pane?: string;
5
7
  /** The herdr workspace, so a later command can close it without listing agents. */
6
8
  workspace?: string;
@@ -35,14 +35,17 @@ export function compare(team, session, state, live, now = new Date(), watch = te
35
35
  rows.push({ name: seat.name, state: 'stopped', model: seat.display, pane: '-' });
36
36
  continue;
37
37
  }
38
- // An agent sits in the seat's workspace under another name, or under none.
39
- const stray = live.agents.find((candidate) => !claimed.has(candidate.pane) && labels.get(candidate.workspace) === seat.label
40
- && !team.seats.some((other) => other.name === candidate.name));
38
+ // An agent sits in the workspace recorded for this name, under another name or under none.
39
+ // The display label is not a key: two seats may share it.
40
+ const stray = recorded?.workspace
41
+ ? live.agents.find((candidate) => !claimed.has(candidate.pane) && candidate.workspace === recorded.workspace
42
+ && !team.seats.some((other) => other.name === candidate.name))
43
+ : undefined;
41
44
  if (stray) {
42
45
  claimed.add(stray.pane);
43
46
  rows.push({ name: seat.name, state: 'wrong name', model: seat.display, pane: stray.pane });
44
47
  differences.push({
45
- what: `${seat.name}: the agent in its workspace "${seat.label}" is ${stray.name ? `named "${stray.name}"` : 'unnamed'}`,
48
+ what: `${seat.name}: the agent in ${stray.pane} is ${stray.name ? `named "${stray.name}"` : 'unnamed'}`,
46
49
  repair: herdrCommand(session, 'agent', 'rename', stray.pane, seat.name),
47
50
  });
48
51
  continue;
@@ -23,6 +23,11 @@ export interface Approval {
23
23
  path: string;
24
24
  hash: string;
25
25
  }>;
26
+ /**
27
+ * The override file as approved, or null when the owner approved there being
28
+ * none. Absent on a record written before the file existed.
29
+ */
30
+ overrides?: string | null;
26
31
  }
27
32
  export interface LedgerEntry {
28
33
  display: string;
@@ -0,0 +1 @@
1
+ export declare function version(): string;
@@ -0,0 +1,7 @@
1
+ import { readFileSync } from 'node:fs';
2
+ // The one place the CLI's version is read. `--version` prints it and `team init` pins the
3
+ // schema URL's tag to it, so the URL can never name a release the CLI doesn't report.
4
+ export function version() {
5
+ const text = readFileSync(new URL('../package.json', import.meta.url), 'utf8');
6
+ return JSON.parse(text).version;
7
+ }
@@ -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,63 +40,59 @@ 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
- // The check readings of the same account live in this slot too: this source reads screens.
71
- if (reading.source !== 'status_line')
72
- continue;
73
- const group = groups.get(reading.window) ?? [];
74
- group.push(reading);
75
- groups.set(reading.window, group);
76
- }
77
- const windows = [];
78
- let seen = false;
79
- for (const group of groups.values()) {
80
- const result = verdict(group, now, staleMs, account.reserve);
81
- if (result.kind === 'unknown')
82
- continue;
83
- if (result.kind === 'unconfirmed') {
84
- seen = true;
85
- continue;
86
- }
87
- windows.push({
88
- window: result.reading.window,
89
- left: result.reading.left,
90
- used: result.reading.used,
91
- resetsAt: result.reading.resetsAt,
92
- });
93
- }
94
- if (windows.length)
95
- return { kind: 'subscription', windows };
96
- if (seen)
97
- return 'unseen';
98
- 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
+ });
99
91
  }
92
+ if (windows.length)
93
+ return { kind: 'subscription', windows };
94
+ if (seen)
95
+ return 'unseen';
100
96
  return null;
101
97
  }
102
98
  /** The account reads unknown while a seat that spends it is running: the operator can stop it. */
@@ -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>;
@@ -39,5 +41,10 @@ export type PassInput = {
39
41
  outcomes?: readonly CheckOutcome[];
40
42
  budgets?: TeamFile['budgets'];
41
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[];
42
49
  };
43
- export declare function pass({ team, state, live, machine, now, memory, approval, watch, outcomes, budgets, readings: stored, }: 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;