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
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@ Node 22 or later runs the built command.
|
|
|
18
18
|
|
|
19
19
|
```sh
|
|
20
20
|
npm install -g team # or run it without installing: npx team
|
|
21
|
-
team --version # 0.
|
|
21
|
+
team --version # 0.2.0
|
|
22
22
|
```
|
|
23
23
|
|
|
24
24
|
## The file is private to each clone
|
|
@@ -34,13 +34,14 @@ yourself.
|
|
|
34
34
|
A documented subset of YAML, read by the library's own parser: maps, lists, one-line `{ }` and
|
|
35
35
|
`[ ]`, plain and quoted values, comments. Anchors, aliases, tags, block scalars, several documents
|
|
36
36
|
in one file and duplicate keys are refused, with the line number. The file starts with `format: 1`.
|
|
37
|
+
The package ships the JSON Schema at `schema/team.schema.json`, and `team init` writes a `# yaml-language-server: $schema=…` line at the top of the file so editors validate it.
|
|
37
38
|
By example:
|
|
38
39
|
|
|
39
40
|
```yaml
|
|
40
41
|
format: 1 # the only format this version reads
|
|
41
42
|
project: hello
|
|
42
|
-
coordinator:
|
|
43
|
-
operator:
|
|
43
|
+
coordinator: coordinator # the seat that dispatches work
|
|
44
|
+
operator: coordinator # the seat the watch reports to
|
|
44
45
|
|
|
45
46
|
identity:
|
|
46
47
|
signature:
|
|
@@ -59,7 +60,7 @@ workspace:
|
|
|
59
60
|
|
|
60
61
|
seats:
|
|
61
62
|
- role: coordinator
|
|
62
|
-
name:
|
|
63
|
+
name: coordinator
|
|
63
64
|
cli: claude-code # the launch profile
|
|
64
65
|
vendor: anthropic # the model's maker
|
|
65
66
|
model: Claude Opus # the model's name, without its version
|
|
@@ -67,7 +68,7 @@ seats:
|
|
|
67
68
|
launch: claude --model claude-opus-5-5 # no approval flags: the profile adds them
|
|
68
69
|
|
|
69
70
|
- role: implementer
|
|
70
|
-
name:
|
|
71
|
+
name: implementer
|
|
71
72
|
cli: codex
|
|
72
73
|
vendor: openai
|
|
73
74
|
account: openai-hello # the seat's account, when one vendor has two; absent, its vendor
|
|
@@ -78,17 +79,17 @@ seats:
|
|
|
78
79
|
parked: true # running, and not reported while idle
|
|
79
80
|
|
|
80
81
|
- role: implementer
|
|
81
|
-
name: deepseek
|
|
82
|
+
name: implementer-deepseek
|
|
82
83
|
cli: claude-code # DeepSeek's model, run by Claude Code
|
|
83
84
|
vendor: deepseek
|
|
84
85
|
model: DeepSeek Flash
|
|
85
86
|
version: "V4.1"
|
|
86
87
|
display: DeepSeek V4.1 Flash
|
|
87
88
|
launch: team-deepseek # a launcher on the PATH, holding the account's key and endpoint
|
|
88
|
-
count: 2 # deepseek
|
|
89
|
+
count: 2 # implementer-deepseek and implementer-deepseek-2
|
|
89
90
|
|
|
90
91
|
- role: reviewer
|
|
91
|
-
name:
|
|
92
|
+
name: reviewer
|
|
92
93
|
cli: grok
|
|
93
94
|
vendor: xai
|
|
94
95
|
model: Grok
|
|
@@ -98,7 +99,7 @@ seats:
|
|
|
98
99
|
|
|
99
100
|
budgets: # the owner's: reserve or floor per account, marks, freshness
|
|
100
101
|
accounts:
|
|
101
|
-
openai-hello: # the account
|
|
102
|
+
openai-hello: # the account implementer spends
|
|
102
103
|
kind: subscription
|
|
103
104
|
reserve: 10% # refuse a launch on a figure inside it
|
|
104
105
|
sources: [status_line] # the figure comes off Codex's status line
|
|
@@ -125,6 +126,15 @@ budgets: # the owner's: reserve or floor per account, marks
|
|
|
125
126
|
`trust` and outside every protected checkout — never in the project root; `up` and `add` refuse a
|
|
126
127
|
seat whose folder, lobby included, would be protected or untrusted.
|
|
127
128
|
|
|
129
|
+
### Naming seats
|
|
130
|
+
|
|
131
|
+
The herdr session carries the project, so a seat's name is its role: `coordinator`, `implementer`,
|
|
132
|
+
`reviewer`. When a role is used twice, the model is added: `implementer-deepseek`. The label is the
|
|
133
|
+
herdr workspace title. Left out of the file, it is the model and version in lowercase
|
|
134
|
+
(`claude opus 5.5`), taken from that seat's own fields, so a model change retitles the pane. A
|
|
135
|
+
label written in the file is kept. `team doctor` warns, and does not refuse the file, when a name
|
|
136
|
+
or a label repeats the project or the session.
|
|
137
|
+
|
|
128
138
|
The Codex profile is tested with CLI 0.157.0. Its status line is read for a weekly figure
|
|
129
139
|
(`weekly N% left`) when the pane is wide enough to show the number; a cut line is not a figure.
|
|
130
140
|
It adds `-a never -s danger-full-access`
|
|
@@ -147,7 +157,8 @@ under ~/.cursor/projects for that folder; team writes no trust (.workspace-trust
|
|
|
147
157
|
config.
|
|
148
158
|
|
|
149
159
|
`budgets` is the owner's: marks (percent used), how long a figure stays fresh, and each
|
|
150
|
-
account's reserve or floor.
|
|
160
|
+
account's reserve or floor. An account's `shared` key is informational; `team` does not act
|
|
161
|
+
on it. A seat spends its own `account:` when the file names one, its `vendor`
|
|
151
162
|
when it doesn't, so one vendor's two accounts are two buckets; a pattern names the account it
|
|
152
163
|
measures, not the seat's. A `check` command is resolved to a file and hashed when the
|
|
153
164
|
owner approves. A change to that file leaves that account's check unapproved: it is
|
|
@@ -264,7 +275,7 @@ cd team
|
|
|
264
275
|
bun install
|
|
265
276
|
bun run build
|
|
266
277
|
npm install -g . # puts `team` on the PATH
|
|
267
|
-
team --version # 0.
|
|
278
|
+
team --version # 0.2.0
|
|
268
279
|
```
|
|
269
280
|
|
|
270
281
|
Bun builds and tests the sources:
|
package/dist/approve/approval.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { homedir } from 'node:os';
|
|
2
2
|
import { defaultBudgets, defaultWatch, validateTeamFile } from "../file/validate.js";
|
|
3
3
|
import { readApproval, storePath, writeApproval } from "../store/store.js";
|
|
4
|
-
import { compare, describe, fingerprints, legacySeatDigests, OWNER_SECTIONS } from "./fingerprint.js";
|
|
4
|
+
import { compare, describe, fingerprints, legacyLabelDigests, legacySeatDigests, OWNER_SECTIONS } from "./fingerprint.js";
|
|
5
5
|
/** The ceilings an approval fixes: `up` and `add` read them from the record, never from the file. */
|
|
6
6
|
export function ceilingsOf(team) {
|
|
7
7
|
return { seats: team.limits.seats, temporary: team.limits.temporary, vendors: { ...team.limits.vendors } };
|
|
@@ -31,25 +31,30 @@ export function approvedFingerprints(record) {
|
|
|
31
31
|
if (!OWNER_SECTIONS.every((name) => stored.sections[name] !== undefined) && checked.ok) {
|
|
32
32
|
sections = { ...fingerprints(checked.team).sections, ...stored.sections };
|
|
33
33
|
}
|
|
34
|
-
const seats = checked.ok ?
|
|
34
|
+
const seats = checked.ok ? adoptLegacyDigests(stored.seats, checked.team) : stored.seats;
|
|
35
35
|
if (sections === stored.sections && seats === stored.seats)
|
|
36
36
|
return stored;
|
|
37
37
|
return { sections, seats };
|
|
38
38
|
}
|
|
39
39
|
/**
|
|
40
|
-
* A record from before `parked` and `stopped` were in the digest
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
40
|
+
* A record from before `parked` and `stopped` were in the digest, or from when
|
|
41
|
+
* an omitted label was the seat's name, still names the seat as it was
|
|
42
|
+
* approved, read from the stored copy. A digest already in the new shape is
|
|
43
|
+
* left alone, so a later edit of the file is not adopted from a stale copy.
|
|
44
|
+
* A copy that can't be read is left alone.
|
|
44
45
|
*/
|
|
45
|
-
function
|
|
46
|
+
function adoptLegacyDigests(stored, team) {
|
|
46
47
|
const current = fingerprints(team).seats;
|
|
47
|
-
const
|
|
48
|
+
const legacyFlags = legacySeatDigests(team);
|
|
49
|
+
const legacyLabels = legacyLabelDigests(team);
|
|
48
50
|
let changed = false;
|
|
49
51
|
const next = { ...stored };
|
|
50
52
|
for (const [name, previous] of Object.entries(stored)) {
|
|
51
53
|
const adopted = current[name];
|
|
52
|
-
|
|
54
|
+
const old = previous === legacyFlags[name] ||
|
|
55
|
+
previous === legacyLabels.named[name] ||
|
|
56
|
+
previous === legacyLabels.namedWithoutFlags[name];
|
|
57
|
+
if (adopted !== undefined && old && previous !== adopted) {
|
|
53
58
|
next[name] = adopted;
|
|
54
59
|
changed = true;
|
|
55
60
|
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The sections only the owner changes. An edit to any of them needs a new
|
|
3
|
-
* approval before the file runs.
|
|
3
|
+
* approval before the file runs. Generated from the section modules, in their
|
|
4
|
+
* list's order — the order feeds the approval digest, so the list keeps it:
|
|
5
|
+
* `watch` last with `watch.checks`, the line inside it, after it.
|
|
4
6
|
*/
|
|
5
|
-
export declare const OWNER_SECTIONS: readonly [
|
|
7
|
+
export declare const OWNER_SECTIONS: readonly string[];
|
|
6
8
|
/** A validated team file, as far as an approval reads it. */
|
|
7
9
|
export type Approvable = Record<string, unknown> & {
|
|
8
10
|
seats: readonly (Record<string, unknown> & {
|
|
@@ -23,6 +25,16 @@ export declare function fingerprints(team: Approvable): Fingerprints;
|
|
|
23
25
|
* them. An approval from then still matches a file that has not changed.
|
|
24
26
|
*/
|
|
25
27
|
export declare function legacySeatDigests(team: Approvable): Record<string, string>;
|
|
28
|
+
/**
|
|
29
|
+
* Seat digests as a record written when an omitted label was the seat's name.
|
|
30
|
+
* `v0.1.2` hashed `parked` and `stopped`; `v0.1.1` left them out. A written
|
|
31
|
+
* label hashes the same title in every shape, so these equal a stored digest
|
|
32
|
+
* only where the title was the name.
|
|
33
|
+
*/
|
|
34
|
+
export declare function legacyLabelDigests(team: Approvable): {
|
|
35
|
+
named: Record<string, string>;
|
|
36
|
+
namedWithoutFlags: Record<string, string>;
|
|
37
|
+
};
|
|
26
38
|
export type Difference = {
|
|
27
39
|
kind: 'section';
|
|
28
40
|
name: string;
|
|
@@ -1,28 +1,12 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
+
import { SECTIONS } from "../file/sections/index.js";
|
|
2
3
|
/**
|
|
3
4
|
* The sections only the owner changes. An edit to any of them needs a new
|
|
4
|
-
* approval before the file runs.
|
|
5
|
+
* approval before the file runs. Generated from the section modules, in their
|
|
6
|
+
* list's order — the order feeds the approval digest, so the list keeps it:
|
|
7
|
+
* `watch` last with `watch.checks`, the line inside it, after it.
|
|
5
8
|
*/
|
|
6
|
-
export const OWNER_SECTIONS =
|
|
7
|
-
'trust',
|
|
8
|
-
'limits',
|
|
9
|
-
'machine',
|
|
10
|
-
'rules',
|
|
11
|
-
'identity',
|
|
12
|
-
'workspace',
|
|
13
|
-
'coordinator',
|
|
14
|
-
'operator',
|
|
15
|
-
'session',
|
|
16
|
-
'visibility',
|
|
17
|
-
'tools',
|
|
18
|
-
'budgets',
|
|
19
|
-
// The watch's own timings, thresholds included: a seat allowed to stretch `unsent_after` or
|
|
20
|
-
// `idle_first` could silence the watch itself, so the section is the owner's like the rest.
|
|
21
|
-
'watch',
|
|
22
|
-
// And turning a check off is the finer line inside it: the digest above leaves the checks out,
|
|
23
|
-
// so turning one off reads as `watch.checks` alone, never as a threshold change too.
|
|
24
|
-
'watch.checks',
|
|
25
|
-
];
|
|
9
|
+
export const OWNER_SECTIONS = SECTIONS.filter((section) => section.owner).map((section) => section.name);
|
|
26
10
|
/** One owner section, read from the file; the two watch sections sit inside `watch`, not at the top. */
|
|
27
11
|
function sectionOf(team, name) {
|
|
28
12
|
if (name === 'watch.checks')
|
|
@@ -82,6 +66,22 @@ export function legacySeatDigests(team) {
|
|
|
82
66
|
seats[seat.name] = seatDigest(seat, LEGACY_SEAT_FREE_FIELDS);
|
|
83
67
|
return seats;
|
|
84
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* Seat digests as a record written when an omitted label was the seat's name.
|
|
71
|
+
* `v0.1.2` hashed `parked` and `stopped`; `v0.1.1` left them out. A written
|
|
72
|
+
* label hashes the same title in every shape, so these equal a stored digest
|
|
73
|
+
* only where the title was the name.
|
|
74
|
+
*/
|
|
75
|
+
export function legacyLabelDigests(team) {
|
|
76
|
+
const named = {};
|
|
77
|
+
const namedWithoutFlags = {};
|
|
78
|
+
for (const seat of team.seats) {
|
|
79
|
+
const titled = { ...seat, label: seat.name };
|
|
80
|
+
named[seat.name] = seatDigest(titled, SEAT_FREE_FIELDS);
|
|
81
|
+
namedWithoutFlags[seat.name] = seatDigest(titled, LEGACY_SEAT_FREE_FIELDS);
|
|
82
|
+
}
|
|
83
|
+
return { named, namedWithoutFlags };
|
|
84
|
+
}
|
|
85
85
|
function seatDigest(seat, free) {
|
|
86
86
|
const fields = Object.fromEntries(Object.entries(seat).filter(([key]) => !free.has(key)));
|
|
87
87
|
return digest(fields);
|
package/dist/budgets/gate.js
CHANGED
|
@@ -125,7 +125,12 @@ function age(ms) {
|
|
|
125
125
|
return `${hours}h`;
|
|
126
126
|
return `${minutes}m`;
|
|
127
127
|
}
|
|
128
|
-
/**
|
|
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
|
+
*/
|
|
129
133
|
function money(amount) {
|
|
130
|
-
|
|
134
|
+
const written = amount.toFixed(4).replace(/0+$/, '');
|
|
135
|
+
return written.padEnd(written.indexOf('.') + 3, '0');
|
|
131
136
|
}
|
|
@@ -27,7 +27,7 @@ export type StoredReading = {
|
|
|
27
27
|
confirmed: boolean;
|
|
28
28
|
};
|
|
29
29
|
export type Verdict = {
|
|
30
|
-
kind: 'fresh' | 'unconfirmed' | 'stale' | 'refusing';
|
|
30
|
+
kind: 'fresh' | 'unconfirmed' | 'stale' | 'refusing' | 'last-seen';
|
|
31
31
|
reading: Seen;
|
|
32
32
|
} | {
|
|
33
33
|
kind: 'unknown';
|
|
@@ -62,7 +62,9 @@ export declare function checkOf(list: readonly Seen[]): Seen[];
|
|
|
62
62
|
* Among confirmed live readings, the newest change wins. A first sight counts
|
|
63
63
|
* only when no confirmed reading is still live. Staleness is measured from
|
|
64
64
|
* `changedAt`. A stale reading inside the reserve refuses until its reset;
|
|
65
|
-
* 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.
|
|
66
68
|
*/
|
|
67
69
|
export declare function verdict(list: readonly Seen[], now: number, staleAfterMs: number, reserve: number | null): Verdict;
|
|
68
70
|
export type CountedReading = {
|
|
@@ -81,6 +83,8 @@ export type CountedReading = {
|
|
|
81
83
|
* through when it is unknown, and a bare first sight falls through too, so a fresh check below it
|
|
82
84
|
* still counts (§ 4.3 rule 5). A stale reading inside its reserve with its reset ahead is kept as
|
|
83
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.
|
|
84
88
|
*/
|
|
85
89
|
export declare function countedFor(sources: readonly ReadingSource[], screen: readonly Seen[], checks: readonly Seen[], now: number, staleAfterMs: number, reserve: number | null): CountedReading;
|
|
86
90
|
/** Readings whose reset has passed are left out. One with no reset time is kept. */
|
package/dist/budgets/readings.js
CHANGED
|
@@ -62,12 +62,22 @@ export function screenOf(list) {
|
|
|
62
62
|
export function checkOf(list) {
|
|
63
63
|
return list.filter((item) => item.source === 'check');
|
|
64
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
|
+
}
|
|
65
73
|
/**
|
|
66
74
|
* The reading that counts. A figure from before a known reset is dropped.
|
|
67
75
|
* Among confirmed live readings, the newest change wins. A first sight counts
|
|
68
76
|
* only when no confirmed reading is still live. Staleness is measured from
|
|
69
77
|
* `changedAt`. A stale reading inside the reserve refuses until its reset;
|
|
70
|
-
* 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.
|
|
71
81
|
*/
|
|
72
82
|
export function verdict(list, now, staleAfterMs, reserve) {
|
|
73
83
|
const live = list.filter((item) => item.resetsAt === null || item.resetsAt > now);
|
|
@@ -87,8 +97,11 @@ export function verdict(list, now, staleAfterMs, reserve) {
|
|
|
87
97
|
const inside = reserve !== null && reading.left <= reserve;
|
|
88
98
|
if (inside && reading.resetsAt !== null)
|
|
89
99
|
return { kind: 'refusing', reading };
|
|
90
|
-
if (reading.resetsAt === null)
|
|
100
|
+
if (reading.resetsAt === null) {
|
|
101
|
+
if (reserve !== null && pastTheReserveAgain(reading.left, reserve))
|
|
102
|
+
return { kind: 'last-seen', reading };
|
|
91
103
|
return { kind: 'unknown' };
|
|
104
|
+
}
|
|
92
105
|
return { kind: 'stale', reading };
|
|
93
106
|
}
|
|
94
107
|
/**
|
|
@@ -98,9 +111,12 @@ export function verdict(list, now, staleAfterMs, reserve) {
|
|
|
98
111
|
* through when it is unknown, and a bare first sight falls through too, so a fresh check below it
|
|
99
112
|
* still counts (§ 4.3 rule 5). A stale reading inside its reserve with its reset ahead is kept as
|
|
100
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.
|
|
101
116
|
*/
|
|
102
117
|
export function countedFor(sources, screen, checks, now, staleAfterMs, reserve) {
|
|
103
118
|
let fallback = null;
|
|
119
|
+
let lastSeen = null;
|
|
104
120
|
let unconfirmed = null;
|
|
105
121
|
for (const source of sources) {
|
|
106
122
|
if (source === 'check') {
|
|
@@ -112,6 +128,10 @@ export function countedFor(sources, screen, checks, now, staleAfterMs, reserve)
|
|
|
112
128
|
const inside = checks.find((item) => item.resetsAt !== null && item.resetsAt > now && reserve !== null && item.left <= reserve);
|
|
113
129
|
if (inside)
|
|
114
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;
|
|
115
135
|
continue;
|
|
116
136
|
}
|
|
117
137
|
const result = verdict(screen, now, staleAfterMs, reserve);
|
|
@@ -123,10 +143,16 @@ export function countedFor(sources, screen, checks, now, staleAfterMs, reserve)
|
|
|
123
143
|
}
|
|
124
144
|
if (result.kind === 'fresh')
|
|
125
145
|
return { kind: 'counted', reading: result.reading };
|
|
146
|
+
if (result.kind === 'last-seen') {
|
|
147
|
+
lastSeen ??= result.reading;
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
126
150
|
fallback ??= result.reading;
|
|
127
151
|
}
|
|
128
152
|
if (fallback)
|
|
129
153
|
return { kind: 'counted', reading: fallback };
|
|
154
|
+
if (lastSeen)
|
|
155
|
+
return { kind: 'counted', reading: lastSeen };
|
|
130
156
|
if (unconfirmed)
|
|
131
157
|
return { kind: 'unconfirmed', reading: unconfirmed };
|
|
132
158
|
return { kind: 'unknown' };
|
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
|
@@ -11,6 +11,8 @@ export type BudgetRow = {
|
|
|
11
11
|
seat: string | null;
|
|
12
12
|
age: string | null;
|
|
13
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
|
@@ -18,10 +18,11 @@ export function budgetTable(budgets, list, now) {
|
|
|
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
|
|
21
|
+
const sources = sourcesOf(budgets, account);
|
|
22
|
+
const result = countedFor(sources, screenOf(group), checkOf(group), now, staleAfterMs, reserve);
|
|
22
23
|
rows.push(result.kind === 'unknown'
|
|
23
24
|
? { ...blank(account, reserve), window }
|
|
24
|
-
: rowOf(result, now, staleAfterMs, reserve));
|
|
25
|
+
: rowOf(result, now, staleAfterMs, reserve, result.reading.source !== sources[0]));
|
|
25
26
|
}
|
|
26
27
|
for (const account of Object.keys(budgets.accounts)) {
|
|
27
28
|
if (!named.has(account))
|
|
@@ -40,15 +41,17 @@ export function budgetLine(row) {
|
|
|
40
41
|
}
|
|
41
42
|
const reset = row.resetsIn === null ? 'resets unknown' : `resets in ${row.resetsIn}`;
|
|
42
43
|
const from = row.source === 'status_line' ? 'status line' : row.source === 'check' ? 'check' : 'unknown source';
|
|
43
|
-
const
|
|
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';
|
|
44
47
|
const state = row.inside && row.reserve !== null ? `${row.state}, inside reserve ${row.reserve}%` : row.state;
|
|
45
|
-
return `${row.account} ${row.window} left ${row.left}% used ${row.used}% ${reset} ${row.seat ?? '-'} ${when} ${row.age} ago ${
|
|
48
|
+
return `${row.account} ${row.window} left ${row.left}% used ${row.used}% ${reset} ${row.seat ?? '-'} ${when} ${row.age} ago ${source} ${state}`;
|
|
46
49
|
}
|
|
47
50
|
/** The sources the account's figures are read from, in order: a status line when the file is silent. */
|
|
48
51
|
function sourcesOf(budgets, account) {
|
|
49
52
|
return budgets.accounts[account]?.sources ?? ['status_line'];
|
|
50
53
|
}
|
|
51
|
-
function rowOf(result, now, staleAfterMs, reserve) {
|
|
54
|
+
function rowOf(result, now, staleAfterMs, reserve, fallback) {
|
|
52
55
|
const reading = result.reading;
|
|
53
56
|
const inside = reserve !== null && reading.left <= reserve;
|
|
54
57
|
const state = result.kind === 'unconfirmed'
|
|
@@ -65,6 +68,7 @@ function rowOf(result, now, staleAfterMs, reserve) {
|
|
|
65
68
|
seat: reading.seat,
|
|
66
69
|
age: span(now - reading.changedAt),
|
|
67
70
|
source: reading.source,
|
|
71
|
+
fallback,
|
|
68
72
|
state,
|
|
69
73
|
inside,
|
|
70
74
|
reserve,
|
|
@@ -80,6 +84,7 @@ function blank(account, reserve) {
|
|
|
80
84
|
seat: null,
|
|
81
85
|
age: null,
|
|
82
86
|
source: null,
|
|
87
|
+
fallback: false,
|
|
83
88
|
state: 'unknown',
|
|
84
89
|
inside: false,
|
|
85
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}`);
|
package/dist/commands/add.d.ts
CHANGED
package/dist/commands/add.js
CHANGED
|
@@ -49,7 +49,7 @@ export const realSources = {
|
|
|
49
49
|
agents: (session) => agentList(aim(session)),
|
|
50
50
|
workspaces(session) {
|
|
51
51
|
const listed = workspaceList(aim(session));
|
|
52
|
-
return listed === null ? null : listed.map((workspace) => ({ id: workspace.id
|
|
52
|
+
return listed === null ? null : listed.map((workspace) => ({ id: workspace.id }));
|
|
53
53
|
},
|
|
54
54
|
doctor: doctorSources,
|
|
55
55
|
machine: readMachine,
|
|
@@ -123,6 +123,7 @@ export async function runAdd(argv, io, sources = realSources) {
|
|
|
123
123
|
return 1;
|
|
124
124
|
}
|
|
125
125
|
const agents = live === 'running' ? sources.agents(session) : [];
|
|
126
|
+
// Read so a session whose workspace list can't be read is refused. The title is not a key.
|
|
126
127
|
const workspaces = live === 'running' ? sources.workspaces(session) : [];
|
|
127
128
|
if (agents === null || workspaces === null) {
|
|
128
129
|
io.stderr(`team add: session ${session} runs, and its agents can't be read\n`);
|
|
@@ -203,7 +204,7 @@ export async function runAdd(argv, io, sources = realSources) {
|
|
|
203
204
|
// changes no section of its own, and no unapproved reserve may unblock a launch (#50).
|
|
204
205
|
const budgets = budgetsInForce(prepared.team, root, sources.home);
|
|
205
206
|
const decision = seatBudget(budgets, loadReadings(dir), built.seat, sources.now().getTime(), loadSpendReadings(dir));
|
|
206
|
-
const stray = unnamedIn(built.
|
|
207
|
+
const stray = unnamedIn(recorded.seats[built.name]?.workspace, agents);
|
|
207
208
|
const starting = seatPlan(prepared.team, built.seat, start);
|
|
208
209
|
const planned = stray
|
|
209
210
|
? { ...starting, stage: 'launched', pane: stray.pane, workspace: stray.workspace, agentLive: true }
|
|
@@ -354,9 +355,10 @@ function seatPlan(team, seat, start) {
|
|
|
354
355
|
...(start.lobby ? { lobby: true } : {}),
|
|
355
356
|
};
|
|
356
357
|
}
|
|
357
|
-
function unnamedIn(
|
|
358
|
-
|
|
359
|
-
|
|
358
|
+
function unnamedIn(workspace, agents) {
|
|
359
|
+
if (!workspace)
|
|
360
|
+
return undefined;
|
|
361
|
+
return agents.find((agent) => !agent.name && agent.workspace === workspace);
|
|
360
362
|
}
|
|
361
363
|
function runningOf(agents, team, state) {
|
|
362
364
|
const seats = new Map(team.seats.map((seat) => [seat.name, seat]));
|
package/dist/commands/approve.js
CHANGED
|
@@ -11,6 +11,7 @@ import { callerOf, describeCaller, isOwner } from "../caller.js";
|
|
|
11
11
|
import { loadTeamFile, placedProblems } from "../file/load.js";
|
|
12
12
|
import { validateTeamFile } from "../file/validate.js";
|
|
13
13
|
import { logLine } from "../log.js";
|
|
14
|
+
import { OVERRIDE_CHANGED, overrideFile } from "../profiles/overrides.js";
|
|
14
15
|
import { readApproval, storePath, writeApproval } from "../store/store.js";
|
|
15
16
|
export const realSources = {
|
|
16
17
|
async ask(question) {
|
|
@@ -86,6 +87,12 @@ export async function runApprove(argv, io, sources) {
|
|
|
86
87
|
for (const warning of checked.warnings)
|
|
87
88
|
io.stderr(`team approve: warning, line ${warning.line}: ${warning.message}\n`);
|
|
88
89
|
const store = storePath(team.project, root, sources.home);
|
|
90
|
+
const live = overrideFile(team.project, root, sources.home);
|
|
91
|
+
if (live.problems.length) {
|
|
92
|
+
for (const problem of live.problems)
|
|
93
|
+
io.stderr(`team approve: ${problem}\n`);
|
|
94
|
+
return 2;
|
|
95
|
+
}
|
|
89
96
|
const problem = storeProblem(store, root, team);
|
|
90
97
|
if (problem) {
|
|
91
98
|
io.stderr(`team approve: ${problem}\n`);
|
|
@@ -119,6 +126,15 @@ export async function runApprove(argv, io, sources) {
|
|
|
119
126
|
io.stdout(`${path}: against the copy approved on ${previous.approval.approvedAt}:\n\n`);
|
|
120
127
|
io.stdout(`${lines.map((line) => ` ${line}`).join('\n')}\n\n`);
|
|
121
128
|
}
|
|
129
|
+
const recorded = Object.hasOwn(previous.approval, 'overrides') ? previous.approval.overrides ?? null : null;
|
|
130
|
+
if (recorded !== live.text) {
|
|
131
|
+
changes.push(OVERRIDE_CHANGED);
|
|
132
|
+
const overrideLines = formatDiff(recorded ?? '', live.text ?? '');
|
|
133
|
+
if (overrideLines.length) {
|
|
134
|
+
io.stdout(`overrides.yaml: against the copy approved on ${previous.approval.approvedAt}:\n\n`);
|
|
135
|
+
io.stdout(`${overrideLines.map((line) => ` ${line}`).join('\n')}\n\n`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
122
138
|
io.stdout(changes.length ? `Needs a new approval: ${changes.join('; ')}.\n` : 'Nothing in it needs a new approval.\n');
|
|
123
139
|
if (ceilingsLine(previous.approval.ceilings) !== ceilingsLine(ceilings)) {
|
|
124
140
|
io.stdout(`Ceilings approved: ${ceilingsLine(previous.approval.ceilings)}.\n`);
|
|
@@ -139,7 +155,7 @@ export async function runApprove(argv, io, sources) {
|
|
|
139
155
|
return 1;
|
|
140
156
|
}
|
|
141
157
|
const now = sources.now();
|
|
142
|
-
writeApproval(store, { approval: approvalOf(team, root, now, resolved.checks), file: text }, team.seats);
|
|
158
|
+
writeApproval(store, { approval: { ...approvalOf(team, root, now, resolved.checks), overrides: live.text }, file: text }, team.seats);
|
|
143
159
|
logLine(dirname(path), 'approve', describeCaller(caller), `approved ${seats} seats; ceilings: ${ceilingsLine(ceilings)}`, now);
|
|
144
160
|
io.stdout(`Approved. The record is in ${store}; check the rest with \`team doctor\`.\n`);
|
|
145
161
|
return 0;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type Caller } from '../caller.ts';
|
|
1
2
|
import type { TeamFile } from '../file/types.ts';
|
|
2
3
|
import type { Command, Io } from '../io.ts';
|
|
3
4
|
import { type Profile } from '../profiles/profile.ts';
|
|
@@ -9,6 +10,7 @@ export type DoctorSources = {
|
|
|
9
10
|
sessionRunning(session: string): boolean | null;
|
|
10
11
|
now(): Date;
|
|
11
12
|
home: string;
|
|
13
|
+
runCheck?(path: string): string | null;
|
|
12
14
|
};
|
|
13
15
|
export type CommandRunner = (binary: string, args: string[]) => {
|
|
14
16
|
status: number | null;
|
|
@@ -30,11 +32,23 @@ export type Finding = {
|
|
|
30
32
|
export declare function blocksLaunch(finding: Finding): boolean;
|
|
31
33
|
export declare const doctor: Command;
|
|
32
34
|
export default doctor;
|
|
35
|
+
/**
|
|
36
|
+
* The budget checks, one line per account, plus the accounts nothing can read. The checks run
|
|
37
|
+
* the way the watch runs them (RFC 0003 § 5): the approved command's file, hashed again before
|
|
38
|
+
* it runs, in a clean environment with the watch's timeout. The raw output is parsed and
|
|
39
|
+
* dropped — never printed, never logged, never written anywhere. Only the owner's `doctor` runs
|
|
40
|
+
* a check, for the same reason the watch is the owner's: a check reads a vendor home. Any other
|
|
41
|
+
* caller gets the same report without the readings, and the one line that says so.
|
|
42
|
+
*/
|
|
43
|
+
export declare function budgetCheckFindings(team: TeamFile, root: string, sources: DoctorSources, caller: Caller): Finding[];
|
|
44
|
+
export declare function seatNameFindings(team: TeamFile, session: string): Finding[];
|
|
33
45
|
export declare function doctorFindings(team: TeamFile, root: string, dir: string, session: string, sources: DoctorSources, warnings: {
|
|
34
46
|
line: number;
|
|
35
47
|
message: string;
|
|
36
48
|
}[],
|
|
37
49
|
/** The file as approved. `add` hands the team about to run, whose `stopped` mark is already cleared. */
|
|
38
|
-
approved?: TeamFile
|
|
50
|
+
approved?: TeamFile,
|
|
51
|
+
/** The budget-check lines, which only the report runs: `up` and `add` scan, they never run a check. */
|
|
52
|
+
budgetChecks?: Finding[]): Finding[];
|
|
39
53
|
export declare function doctorLoginFindings(team: TeamFile, sources: DoctorSources): Finding[];
|
|
40
54
|
export declare function runDoctor(argv: string[], io: Io, sources: DoctorSources): Promise<number>;
|