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.
- package/README.md +38 -12
- package/dist/ansi.d.ts +13 -0
- package/dist/ansi.js +75 -0
- package/dist/approve/approval.d.ts +11 -0
- package/dist/approve/approval.js +80 -8
- package/dist/approve/fingerprint.d.ts +29 -6
- package/dist/approve/fingerprint.js +57 -32
- package/dist/budgets/gate.d.ts +5 -4
- package/dist/budgets/gate.js +23 -14
- package/dist/budgets/readings.d.ts +57 -9
- package/dist/budgets/readings.js +134 -18
- package/dist/budgets/run.js +8 -2
- package/dist/budgets/table.d.ts +4 -2
- package/dist/budgets/table.js +29 -11
- 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 +18 -9
- package/dist/commands/approve.js +17 -1
- package/dist/commands/doctor.d.ts +17 -1
- package/dist/commands/doctor.js +128 -8
- package/dist/commands/down.d.ts +6 -2
- package/dist/commands/down.js +40 -14
- package/dist/commands/init.d.ts +1 -1
- package/dist/commands/init.js +4 -2
- package/dist/commands/remove.d.ts +2 -0
- package/dist/commands/remove.js +26 -4
- package/dist/commands/status.d.ts +2 -0
- package/dist/commands/status.js +22 -3
- package/dist/commands/up.d.ts +3 -1
- package/dist/commands/up.js +12 -6
- package/dist/commands/watch.d.ts +4 -0
- package/dist/commands/watch.js +99 -19
- 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 +6 -0
- package/dist/file/validate.d.ts +4 -12
- package/dist/file/validate.js +45 -668
- package/dist/herdr.d.ts +5 -0
- package/dist/herdr.js +51 -12
- package/dist/launch/agent.d.ts +1 -0
- package/dist/launch/agent.js +13 -0
- package/dist/launch/deliver.d.ts +7 -1
- package/dist/launch/deliver.js +159 -7
- package/dist/launch/execute.d.ts +4 -2
- package/dist/launch/execute.js +36 -13
- package/dist/launch/plan.js +16 -2
- package/dist/launch/rules.d.ts +4 -2
- package/dist/launch/rules.js +17 -3
- package/dist/profiles/antigravity.yaml +25 -2
- package/dist/profiles/claude-code.yaml +51 -6
- package/dist/profiles/codex.yaml +14 -1
- package/dist/profiles/cursor.yaml +46 -3
- 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 +9 -4
- package/dist/state.js +37 -0
- package/dist/status/compare.js +7 -4
- package/dist/status/statusline.js +3 -1
- package/dist/store/store.d.ts +5 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +7 -0
- package/dist/watch/check.d.ts +1 -0
- package/dist/watch/checks/budget.js +50 -51
- package/dist/watch/dialect.d.ts +6 -1
- package/dist/watch/dialect.js +68 -12
- package/dist/watch/pass.d.ts +10 -2
- package/dist/watch/pass.js +70 -29
- package/dist/watch/screen-core.d.ts +41 -4
- package/dist/watch/screen-core.js +510 -89
- package/dist/watch/screen-data.d.ts +34 -0
- package/dist/watch/screen-file.d.ts +8 -2
- package/dist/watch/screen-file.js +344 -39
- package/dist/watch/screen-profile.d.ts +19 -0
- package/dist/watch/screen-profile.js +1 -0
- package/dist/watch/screen.d.ts +25 -6
- package/dist/watch/screen.js +48 -9
- package/examples/team.yaml +8 -0
- package/package.json +9 -3
- package/schema/team.schema.json +498 -0
package/dist/budgets/gate.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
import {
|
|
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
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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 =
|
|
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.
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
|
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,
|
|
57
|
-
|
|
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,
|
|
63
|
-
export declare function loadSpendReadings(dir: string
|
|
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;
|
package/dist/budgets/readings.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
|
83
|
-
return recall(readState(dir).
|
|
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,
|
|
205
|
+
export function saveSpendReadings(dir, list) {
|
|
90
206
|
if (!list.length)
|
|
91
207
|
return;
|
|
92
208
|
updateState(dir, (state) => {
|
|
93
|
-
const
|
|
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
|
-
|
|
98
|
-
state.sessions[session] = current;
|
|
212
|
+
state.spend = spend;
|
|
99
213
|
});
|
|
100
214
|
}
|
|
101
|
-
export function loadSpendReadings(dir
|
|
102
|
-
return recallSpend(readState(dir).
|
|
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
|
}
|
package/dist/budgets/run.js
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
-
|
|
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
|
package/dist/budgets/table.d.ts
CHANGED
|
@@ -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:
|
|
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;
|
package/dist/budgets/table.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
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}
|
|
48
|
+
return `${row.account} ${row.window} left ${row.left}% used ${row.used}% ${reset} ${row.seat ?? '-'} ${when} ${row.age} ago ${source} ${state}`;
|
|
41
49
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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:
|
|
55
|
-
|
|
56
|
-
|
|
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
|
|
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 {
|
|
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
|
|
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}`);
|