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.
- package/README.md +22 -11
- package/dist/approve/approval.js +14 -9
- package/dist/approve/fingerprint.d.ts +14 -2
- package/dist/approve/fingerprint.js +21 -21
- package/dist/budgets/gate.js +7 -2
- package/dist/budgets/readings.d.ts +6 -2
- package/dist/budgets/readings.js +28 -2
- package/dist/budgets/run.js +8 -2
- package/dist/budgets/table.d.ts +2 -0
- package/dist/budgets/table.js +10 -5
- package/dist/cli.d.ts +2 -1
- package/dist/cli.js +8 -5
- package/dist/commands/add.d.ts +0 -1
- package/dist/commands/add.js +7 -5
- package/dist/commands/approve.js +17 -1
- package/dist/commands/doctor.d.ts +15 -1
- package/dist/commands/doctor.js +126 -8
- package/dist/commands/down.d.ts +4 -0
- package/dist/commands/down.js +32 -13
- package/dist/commands/init.d.ts +1 -1
- package/dist/commands/init.js +4 -2
- package/dist/commands/remove.js +5 -2
- package/dist/commands/status.d.ts +2 -0
- package/dist/commands/status.js +19 -1
- package/dist/commands/up.js +3 -1
- package/dist/commands/watch.d.ts +2 -0
- package/dist/commands/watch.js +28 -4
- package/dist/conformance/adapter.d.ts +5 -0
- package/dist/conformance/adapter.js +44 -0
- package/dist/file/check.d.ts +20 -0
- package/dist/file/check.js +112 -0
- package/dist/file/lines.js +13 -2
- package/dist/file/sections/budgets.d.ts +15 -0
- package/dist/file/sections/budgets.js +180 -0
- package/dist/file/sections/coordinator.d.ts +2 -0
- package/dist/file/sections/coordinator.js +11 -0
- package/dist/file/sections/format.d.ts +2 -0
- package/dist/file/sections/format.js +13 -0
- package/dist/file/sections/identity.d.ts +2 -0
- package/dist/file/sections/identity.js +103 -0
- package/dist/file/sections/index.d.ts +9 -0
- package/dist/file/sections/index.js +43 -0
- package/dist/file/sections/lead.d.ts +4 -0
- package/dist/file/sections/lead.js +18 -0
- package/dist/file/sections/limits.d.ts +2 -0
- package/dist/file/sections/limits.js +43 -0
- package/dist/file/sections/machine.d.ts +2 -0
- package/dist/file/sections/machine.js +38 -0
- package/dist/file/sections/operator.d.ts +2 -0
- package/dist/file/sections/operator.js +11 -0
- package/dist/file/sections/project.d.ts +2 -0
- package/dist/file/sections/project.js +13 -0
- package/dist/file/sections/rules.d.ts +2 -0
- package/dist/file/sections/rules.js +9 -0
- package/dist/file/sections/seats.d.ts +7 -0
- package/dist/file/sections/seats.js +161 -0
- package/dist/file/sections/section.d.ts +32 -0
- package/dist/file/sections/section.js +7 -0
- package/dist/file/sections/session.d.ts +2 -0
- package/dist/file/sections/session.js +16 -0
- package/dist/file/sections/tools.d.ts +2 -0
- package/dist/file/sections/tools.js +43 -0
- package/dist/file/sections/trust.d.ts +2 -0
- package/dist/file/sections/trust.js +20 -0
- package/dist/file/sections/units.d.ts +16 -0
- package/dist/file/sections/units.js +8 -0
- package/dist/file/sections/visibility.d.ts +2 -0
- package/dist/file/sections/visibility.js +12 -0
- package/dist/file/sections/watch-checks.d.ts +9 -0
- package/dist/file/sections/watch-checks.js +14 -0
- package/dist/file/sections/watch.d.ts +14 -0
- package/dist/file/sections/watch.js +111 -0
- package/dist/file/sections/workspace.d.ts +4 -0
- package/dist/file/sections/workspace.js +77 -0
- package/dist/file/types.d.ts +1 -0
- package/dist/file/validate.d.ts +4 -12
- package/dist/file/validate.js +45 -688
- package/dist/herdr.d.ts +1 -0
- package/dist/herdr.js +11 -1
- package/dist/launch/agent.js +4 -3
- package/dist/launch/deliver.d.ts +4 -0
- package/dist/launch/deliver.js +151 -8
- package/dist/launch/execute.d.ts +2 -0
- package/dist/launch/execute.js +21 -11
- package/dist/launch/plan.js +16 -2
- package/dist/profiles/antigravity.yaml +12 -2
- package/dist/profiles/claude-code.yaml +21 -6
- package/dist/profiles/codex.yaml +14 -1
- package/dist/profiles/cursor.yaml +45 -5
- package/dist/profiles/overrides.d.ts +67 -0
- package/dist/profiles/overrides.js +190 -0
- package/dist/profiles/profile.d.ts +3 -0
- package/dist/profiles/profile.js +5 -1
- package/dist/state.d.ts +2 -0
- package/dist/status/compare.js +7 -4
- package/dist/store/store.d.ts +5 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +7 -0
- package/dist/watch/checks/budget.js +49 -53
- package/dist/watch/dialect.d.ts +6 -1
- package/dist/watch/dialect.js +68 -12
- package/dist/watch/pass.d.ts +8 -1
- package/dist/watch/pass.js +19 -9
- package/dist/watch/screen-core.d.ts +41 -4
- package/dist/watch/screen-core.js +458 -93
- package/dist/watch/screen-data.d.ts +25 -0
- package/dist/watch/screen-file.d.ts +8 -2
- package/dist/watch/screen-file.js +325 -38
- package/dist/watch/screen-profile.d.ts +19 -0
- package/dist/watch/screen-profile.js +1 -0
- package/dist/watch/screen.d.ts +22 -4
- package/dist/watch/screen.js +39 -5
- package/package.json +9 -3
- 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;
|
package/dist/profiles/profile.js
CHANGED
|
@@ -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;
|
package/dist/status/compare.js
CHANGED
|
@@ -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
|
|
39
|
-
|
|
40
|
-
|
|
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
|
|
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;
|
package/dist/store/store.d.ts
CHANGED
|
@@ -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;
|
package/dist/version.js
ADDED
|
@@ -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 {
|
|
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
|
|
44
|
-
*
|
|
45
|
-
*
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
.
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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. */
|
package/dist/watch/dialect.d.ts
CHANGED
|
@@ -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;
|
package/dist/watch/dialect.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
}
|
package/dist/watch/pass.d.ts
CHANGED
|
@@ -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;
|