team 0.1.0 → 0.1.2
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 +20 -5
- 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 +75 -8
- package/dist/approve/fingerprint.d.ts +15 -4
- package/dist/approve/fingerprint.js +37 -12
- package/dist/budgets/gate.d.ts +12 -7
- package/dist/budgets/gate.js +52 -17
- package/dist/budgets/readings.d.ts +71 -5
- package/dist/budgets/readings.js +136 -9
- package/dist/budgets/table.d.ts +2 -2
- package/dist/budgets/table.js +24 -11
- package/dist/commands/add.js +12 -5
- package/dist/commands/doctor.d.ts +3 -1
- package/dist/commands/doctor.js +4 -2
- package/dist/commands/down.d.ts +2 -2
- package/dist/commands/down.js +8 -1
- package/dist/commands/remove.d.ts +2 -0
- package/dist/commands/remove.js +21 -2
- package/dist/commands/status.js +3 -2
- package/dist/commands/up.d.ts +3 -1
- package/dist/commands/up.js +12 -7
- package/dist/commands/watch.d.ts +2 -0
- package/dist/commands/watch.js +94 -16
- package/dist/file/types.d.ts +5 -0
- package/dist/file/validate.js +31 -11
- package/dist/herdr.d.ts +4 -0
- package/dist/herdr.js +40 -11
- package/dist/launch/agent.d.ts +1 -0
- package/dist/launch/agent.js +12 -0
- package/dist/launch/deliver.d.ts +3 -1
- package/dist/launch/deliver.js +10 -1
- package/dist/launch/execute.d.ts +2 -2
- package/dist/launch/execute.js +15 -2
- package/dist/launch/rules.d.ts +4 -2
- package/dist/launch/rules.js +17 -3
- package/dist/profiles/antigravity.yaml +13 -0
- package/dist/profiles/claude-code.yaml +30 -0
- package/dist/profiles/cursor.yaml +4 -1
- package/dist/profiles/quota.d.ts +7 -2
- package/dist/profiles/quota.js +15 -14
- package/dist/state.d.ts +8 -3
- package/dist/state.js +37 -0
- package/dist/status/statusline.js +3 -1
- package/dist/watch/check.d.ts +1 -0
- package/dist/watch/checks/budget.js +4 -1
- package/dist/watch/pass.d.ts +14 -1
- package/dist/watch/pass.js +64 -28
- package/dist/watch/screen-core.d.ts +7 -1
- package/dist/watch/screen-core.js +146 -54
- package/dist/watch/screen-data.d.ts +9 -0
- package/dist/watch/screen-file.js +25 -7
- package/dist/watch/screen.d.ts +5 -0
- package/dist/watch/screen.js +16 -2
- package/examples/team.yaml +8 -0
- package/package.json +2 -2
package/dist/launch/rules.js
CHANGED
|
@@ -30,7 +30,21 @@ export function seatRules(input) {
|
|
|
30
30
|
}
|
|
31
31
|
return [...own, ...input.rules, ...signing, ...working];
|
|
32
32
|
}
|
|
33
|
-
/**
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
/**
|
|
34
|
+
* The line each form ends with. The message says what the rules are and never that they are not a
|
|
35
|
+
* task, so some models start complying by investigating instead of waiting for a brief. A first
|
|
36
|
+
* message is answered once, so it can ask for the ready reply; a system prompt (a launch option)
|
|
37
|
+
* stays in force on every later turn, so it must not: a Claude seat could answer "ready" to its
|
|
38
|
+
* real brief.
|
|
39
|
+
*/
|
|
40
|
+
const CLOSING = {
|
|
41
|
+
message: 'These are standing rules, not a task: reply ready and wait for your brief.',
|
|
42
|
+
option: 'These are standing rules, not a task.',
|
|
43
|
+
};
|
|
44
|
+
/** The rules as the one text a launch option or a first message carries, the closing line last. */
|
|
45
|
+
export function rulesText(input, delivery) {
|
|
46
|
+
// A file whose own rules already carry the form's closing line gets it once: as the closing line.
|
|
47
|
+
const closing = CLOSING[delivery];
|
|
48
|
+
const rules = seatRules(input).filter((rule) => rule !== closing);
|
|
49
|
+
return ['Rules for this session, from the team file:', ...rules.map((rule) => `- ${rule}`), closing].join('\n');
|
|
36
50
|
}
|
|
@@ -43,6 +43,12 @@ screen:
|
|
|
43
43
|
- 'Do you trust the contents of this project\?'
|
|
44
44
|
- '>\s*Yes, I trust this folder'
|
|
45
45
|
below_last_rule: 'enter Confirm'
|
|
46
|
+
# The captured trust dialog fills the pane: there is no composer rule.
|
|
47
|
+
- all:
|
|
48
|
+
- 'Do you trust the contents of this project\?'
|
|
49
|
+
- '>\s*Yes, I trust this folder'
|
|
50
|
+
- 'enter Confirm'
|
|
51
|
+
without_rule: true
|
|
46
52
|
permission:
|
|
47
53
|
ignore_case: true
|
|
48
54
|
rules:
|
|
@@ -50,6 +56,13 @@ screen:
|
|
|
50
56
|
- 'Requesting permission for:'
|
|
51
57
|
- '^\s*>\s*[0-9]\.\s+Yes\b'
|
|
52
58
|
below_last_rule: '↑/↓ Navigate'
|
|
59
|
+
# A long command pushes the one rule out of the window. The dialog is the
|
|
60
|
+
# three lines, with no composer rule left to sit below.
|
|
61
|
+
- all:
|
|
62
|
+
- 'Requesting permission for:'
|
|
63
|
+
- '^\s*>\s*[0-9]\.\s+Yes\b'
|
|
64
|
+
- '↑/↓ Navigate'
|
|
65
|
+
without_rule: true
|
|
53
66
|
working:
|
|
54
67
|
- any: ['\bGenerating\.\.\.']
|
|
55
68
|
composer:
|
|
@@ -50,12 +50,38 @@ screen:
|
|
|
50
50
|
footer: 'Esc to cancel · Tab to amend'
|
|
51
51
|
below_last_rule: 'Esc to cancel · Tab to amend'
|
|
52
52
|
- below_last_rule: '^\s*❯?\s*[0-9]\. (Yes|No)\b'
|
|
53
|
+
# A pane with no composer rule whose choice is the dialog, including the
|
|
54
|
+
# unmarked "1. Yes" while up is running. A choice quoted above an empty
|
|
55
|
+
# prompt is a transcript: the empty prompt comes after it.
|
|
56
|
+
- all:
|
|
57
|
+
- '^\s*❯?\s*[0-9]\. (Yes|No)\b'
|
|
58
|
+
without_rule: true
|
|
59
|
+
none_after:
|
|
60
|
+
anchor: '^\s*❯?\s*[0-9]\. (Yes|No)\b'
|
|
61
|
+
patterns:
|
|
62
|
+
- '^\s*[❯›>]\s*$'
|
|
53
63
|
question:
|
|
64
|
+
# The phrase is the dialog's last line, as a permission footer is. Prose
|
|
65
|
+
# above the box is not a question.
|
|
66
|
+
- any:
|
|
67
|
+
- 'Enter to select'
|
|
68
|
+
- '↑/↓ to navigate'
|
|
69
|
+
- 'Enter to confirm'
|
|
70
|
+
- 'Esc to cancel'
|
|
71
|
+
on_footer: true
|
|
72
|
+
- below_last_rule: 'Enter to select|↑/↓ to navigate|Enter to confirm|Esc to cancel'
|
|
73
|
+
# The footer has a status line under it and the rule is out of the window.
|
|
74
|
+
# A phrase quoted above an empty prompt is a transcript.
|
|
54
75
|
- any:
|
|
55
76
|
- 'Enter to select'
|
|
56
77
|
- '↑/↓ to navigate'
|
|
57
78
|
- 'Enter to confirm'
|
|
58
79
|
- 'Esc to cancel'
|
|
80
|
+
without_rule: true
|
|
81
|
+
none_after:
|
|
82
|
+
anchor: 'Enter to select|↑/↓ to navigate|Enter to confirm|Esc to cancel'
|
|
83
|
+
patterns:
|
|
84
|
+
- '^\s*[❯›>]\s*$'
|
|
59
85
|
working:
|
|
60
86
|
- any:
|
|
61
87
|
- '\besc to interrupt\b'
|
|
@@ -64,6 +90,10 @@ screen:
|
|
|
64
90
|
mode: box-to-rule
|
|
65
91
|
prompt: '^\s*[❯›>]'
|
|
66
92
|
rule: '^\s*[─━]{8,}'
|
|
93
|
+
# Claude Code greys every suggestion it shows — "Try …", "Resume briefly.", any other —
|
|
94
|
+
# with faint styling, while typed text carries none. The list below stays the fallback
|
|
95
|
+
# for a source without styling.
|
|
96
|
+
placeholder_style: dim
|
|
67
97
|
placeholders:
|
|
68
98
|
- equals: ''
|
|
69
99
|
- prefix: 'Try "'
|
|
@@ -48,8 +48,11 @@ screen:
|
|
|
48
48
|
- '^\s*→'
|
|
49
49
|
- '^\s+Grok\s+[0-9]'
|
|
50
50
|
working:
|
|
51
|
+
# The running turn is the braille spinner, or the prompt line ending in
|
|
52
|
+
# "ctrl+c to stop". Idle and rules-accepted captures do not paint that suffix.
|
|
53
|
+
# The same words quoted in the transcript are not a turn.
|
|
51
54
|
- any:
|
|
52
|
-
- 'ctrl\+c to stop'
|
|
55
|
+
- '^\s*→.*\s{2,}ctrl\+c to stop\s*$'
|
|
53
56
|
- '^\s*[⠀-⣿]+\s+(Working|Thinking)\b'
|
|
54
57
|
composer:
|
|
55
58
|
mode: status-then-one
|
package/dist/profiles/quota.d.ts
CHANGED
|
@@ -18,5 +18,10 @@ export type QuotaFigure = {
|
|
|
18
18
|
used: number;
|
|
19
19
|
resets: string | null;
|
|
20
20
|
};
|
|
21
|
-
/**
|
|
22
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Figures from one line: the composer's own status row, handed over by the caller (`statusRow`
|
|
23
|
+
* in the watch's screen core). Nothing else on the pane is ever read — a seat can print or type
|
|
24
|
+
* a line that only looks like a quota, and such a line is not the row. A row that is null, and
|
|
25
|
+
* a figure over 100, gives no figure.
|
|
26
|
+
*/
|
|
27
|
+
export declare function figuresOf(patterns: readonly QuotaPattern[], row: string | null): QuotaFigure[];
|
package/dist/profiles/quota.js
CHANGED
|
@@ -3,22 +3,23 @@
|
|
|
3
3
|
export const WINDOWS = ['session', 'daily', 'weekly'];
|
|
4
4
|
const RESET = /^(?:[0-9]+h(?:[0-9]+m)?|[0-9]+m)$/;
|
|
5
5
|
const PERCENT = /^([0-9]+)%$/;
|
|
6
|
-
/**
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Figures from one line: the composer's own status row, handed over by the caller (`statusRow`
|
|
8
|
+
* in the watch's screen core). Nothing else on the pane is ever read — a seat can print or type
|
|
9
|
+
* a line that only looks like a quota, and such a line is not the row. A row that is null, and
|
|
10
|
+
* a figure over 100, gives no figure.
|
|
11
|
+
*/
|
|
12
|
+
export function figuresOf(patterns, row) {
|
|
13
|
+
if (row === null)
|
|
14
|
+
return [];
|
|
9
15
|
const found = [];
|
|
10
16
|
for (const pattern of patterns) {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
if (next)
|
|
18
|
-
hit = next;
|
|
19
|
-
}
|
|
20
|
-
if (hit)
|
|
21
|
-
found.push(hit);
|
|
17
|
+
const match = pattern.match.exec(row);
|
|
18
|
+
if (!match)
|
|
19
|
+
continue;
|
|
20
|
+
const next = figureOf(pattern, match);
|
|
21
|
+
if (next)
|
|
22
|
+
found.push(next);
|
|
22
23
|
}
|
|
23
24
|
return found;
|
|
24
25
|
}
|
package/dist/state.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { StoredReading } from './budgets/readings.ts';
|
|
1
|
+
import type { StoredReading, StoredSpend } from './budgets/readings.ts';
|
|
2
2
|
export type SeatState = {
|
|
3
3
|
stage: 'launched' | 'named' | 'ready';
|
|
4
4
|
pane?: string;
|
|
@@ -35,8 +35,6 @@ export type SessionState = {
|
|
|
35
35
|
nudge?: {
|
|
36
36
|
pending_since: string | null;
|
|
37
37
|
};
|
|
38
|
-
/** The last screen readings for this project, keyed by account, window and seat. */
|
|
39
|
-
budgets?: Record<string, StoredReading>;
|
|
40
38
|
};
|
|
41
39
|
export type State = {
|
|
42
40
|
format: 1;
|
|
@@ -44,6 +42,13 @@ export type State = {
|
|
|
44
42
|
read_at: string;
|
|
45
43
|
file: string;
|
|
46
44
|
};
|
|
45
|
+
/**
|
|
46
|
+
* The project's last readings, whatever session saw them: a screen reading keyed by account,
|
|
47
|
+
* window and seat, a check reading by account and window (§ 4.4).
|
|
48
|
+
*/
|
|
49
|
+
budgets?: Record<string, StoredReading>;
|
|
50
|
+
/** The project's last spend check readings, keyed by account. */
|
|
51
|
+
spend?: Record<string, StoredSpend>;
|
|
47
52
|
sessions: Record<string, SessionState>;
|
|
48
53
|
};
|
|
49
54
|
export declare const STATE_FILE = "team.state.json";
|
package/dist/state.js
CHANGED
|
@@ -26,9 +26,46 @@ export function readState(dir) {
|
|
|
26
26
|
for (const session of Object.values(state.sessions)) {
|
|
27
27
|
session.seats ??= {};
|
|
28
28
|
session.worktrees ??= {};
|
|
29
|
+
// Read old, write new: readings used to be held under a session. They are the project's
|
|
30
|
+
// (§ 4.4), so every session's records are merged to the top level here — no reader loses one,
|
|
31
|
+
// and the next write persists the new shape.
|
|
32
|
+
const held = session;
|
|
33
|
+
if (held.budgets) {
|
|
34
|
+
state.budgets = mergeReadings(state.budgets, held.budgets);
|
|
35
|
+
delete held.budgets;
|
|
36
|
+
}
|
|
37
|
+
if (held.spend) {
|
|
38
|
+
state.spend = mergeSpend(state.spend, held.spend);
|
|
39
|
+
delete held.spend;
|
|
40
|
+
}
|
|
29
41
|
}
|
|
30
42
|
return state;
|
|
31
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* One record per slot when two sessions hold the same account, window and seat: the reading that
|
|
46
|
+
* counts wins — a confirmed one, and among those the newest change (§ 4.3).
|
|
47
|
+
*/
|
|
48
|
+
function mergeReadings(top, held) {
|
|
49
|
+
const merged = top ?? {};
|
|
50
|
+
for (const [key, reading] of Object.entries(held)) {
|
|
51
|
+
const current = merged[key];
|
|
52
|
+
merged[key] = !current
|
|
53
|
+
|| (reading.confirmed && !current.confirmed)
|
|
54
|
+
|| (reading.confirmed === current.confirmed && Date.parse(reading.changedAt) > Date.parse(current.changedAt))
|
|
55
|
+
? reading
|
|
56
|
+
: current;
|
|
57
|
+
}
|
|
58
|
+
return merged;
|
|
59
|
+
}
|
|
60
|
+
/** The same for spend: one record per account, the reading whose check ran last winning. */
|
|
61
|
+
function mergeSpend(top, held) {
|
|
62
|
+
const merged = top ?? {};
|
|
63
|
+
for (const [key, reading] of Object.entries(held)) {
|
|
64
|
+
const current = merged[key];
|
|
65
|
+
merged[key] = !current || Date.parse(reading.at) > Date.parse(current.at) ? reading : current;
|
|
66
|
+
}
|
|
67
|
+
return merged;
|
|
68
|
+
}
|
|
32
69
|
// Reads the state, lets `change` edit it, and writes it back, all under the lock.
|
|
33
70
|
export function updateState(dir, change) {
|
|
34
71
|
return withLock(dir, () => {
|
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
// visible text into the file's two fields, `model` and `version`, or null when the text doesn't
|
|
3
3
|
// show them: "unread" is never a mismatch. The last six lines are the window. A line the rules
|
|
4
4
|
// claim but cannot name clears an earlier match; that is how an unknown Codex footer stays unread.
|
|
5
|
+
import { stripSgr } from "../ansi.js";
|
|
5
6
|
import { statusOnLine } from "../profiles/profile.js";
|
|
6
7
|
export function runningModel(cli, screen) {
|
|
7
8
|
let found = null;
|
|
8
|
-
|
|
9
|
+
// The pane's text may keep its ANSI styling; the model is read from the plain form.
|
|
10
|
+
for (const line of stripSgr(screen).split('\n').slice(-6)) {
|
|
9
11
|
const hit = statusOnLine(cli, line);
|
|
10
12
|
if (hit === 'unreadable')
|
|
11
13
|
found = null;
|
package/dist/watch/check.d.ts
CHANGED
|
@@ -67,6 +67,9 @@ function countedOf(name, account, team, staleMs, now) {
|
|
|
67
67
|
for (const reading of team.readings) {
|
|
68
68
|
if (reading.account !== name)
|
|
69
69
|
continue;
|
|
70
|
+
// The check readings of the same account live in this slot too: this source reads screens.
|
|
71
|
+
if (reading.source !== 'status_line')
|
|
72
|
+
continue;
|
|
70
73
|
const group = groups.get(reading.window) ?? [];
|
|
71
74
|
group.push(reading);
|
|
72
75
|
groups.set(reading.window, group);
|
|
@@ -98,7 +101,7 @@ function countedOf(name, account, team, staleMs, now) {
|
|
|
98
101
|
}
|
|
99
102
|
/** The account reads unknown while a seat that spends it is running: the operator can stop it. */
|
|
100
103
|
function unknown(name, team, ctx) {
|
|
101
|
-
const on = team.seats.filter((seat) => seat.running && seat.
|
|
104
|
+
const on = team.seats.filter((seat) => seat.running && seat.account === name).map((seat) => seat.name);
|
|
102
105
|
if (!on.length)
|
|
103
106
|
return null;
|
|
104
107
|
const verb = on.length === 1 ? 'runs' : 'run';
|
package/dist/watch/pass.d.ts
CHANGED
|
@@ -27,4 +27,17 @@ export type PassResult = {
|
|
|
27
27
|
};
|
|
28
28
|
export declare const SEAT_CHECKS: SeatCheck[];
|
|
29
29
|
export declare const TEAM_CHECKS: TeamCheck[];
|
|
30
|
-
export
|
|
30
|
+
export type PassInput = {
|
|
31
|
+
team: TeamFile;
|
|
32
|
+
state: SessionState;
|
|
33
|
+
live: Live;
|
|
34
|
+
machine: Machine;
|
|
35
|
+
now: number;
|
|
36
|
+
memory: Memory;
|
|
37
|
+
approval?: string[] | null;
|
|
38
|
+
watch: TeamFile['watch'];
|
|
39
|
+
outcomes?: readonly CheckOutcome[];
|
|
40
|
+
budgets?: TeamFile['budgets'];
|
|
41
|
+
readings?: readonly Seen[];
|
|
42
|
+
};
|
|
43
|
+
export declare function pass({ team, state, live, machine, now, memory, approval, watch, outcomes, budgets, readings: stored, }: PassInput): PassResult;
|
package/dist/watch/pass.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
// runs the registered checks in the order it owns, applies the report-once rule, and dials the
|
|
3
3
|
// nudge. A check never reads herdr, a screen or the file itself — only the observation it is
|
|
4
4
|
// handed (RFC 0002 § 4.2).
|
|
5
|
-
import {
|
|
6
|
-
import { observe,
|
|
5
|
+
import { seatNamed } from "../approve/fingerprint.js";
|
|
6
|
+
import { observe, observeCheck } from "../budgets/readings.js";
|
|
7
7
|
import { quotaFor } from "../profiles/profile.js";
|
|
8
8
|
import { figuresOf } from "../profiles/quota.js";
|
|
9
9
|
import { seatModel } from "../status/statusline.js";
|
|
@@ -23,7 +23,7 @@ import { swapFree } from "./checks/swap-free.js";
|
|
|
23
23
|
import { swapGrowth } from "./checks/swap-growth.js";
|
|
24
24
|
import { teamIdle } from "./checks/team-idle.js";
|
|
25
25
|
import { unsent } from "./checks/unsent.js";
|
|
26
|
-
import { readScreen } from "./screen.js";
|
|
26
|
+
import { readScreen, statusRow } from "./screen.js";
|
|
27
27
|
export function newMemory() {
|
|
28
28
|
return { history: {}, slots: {}, active: new Set(), pending: [], pendingSince: null };
|
|
29
29
|
}
|
|
@@ -33,8 +33,8 @@ export function newMemory() {
|
|
|
33
33
|
export const NUDGE_TEXT = 'Team watch: reports are waiting in .agents/team.log';
|
|
34
34
|
// The seat checks, in the order their reports land in the log; the team checks, after them. The
|
|
35
35
|
// order is the core's: a seat's wrong model is reported before its permission prompt, and every
|
|
36
|
-
// seat before the team. Disabling a check in the
|
|
37
|
-
// except the four that can't be turned off.
|
|
36
|
+
// seat before the team. Disabling a check in the `watch` section in force takes it out of the
|
|
37
|
+
// run, except the four that can't be turned off.
|
|
38
38
|
export const SEAT_CHECKS = [missing, modelDrift, attention, unsent, idle];
|
|
39
39
|
export const TEAM_CHECKS = [extra, teamIdle, approval, load, memoryCheck, disk, swapFree, swapGrowth, budget];
|
|
40
40
|
// The one exclusive reading of a seat's screen and herdr status. herdr can report a seat at a
|
|
@@ -50,13 +50,18 @@ function attentionOf(screen, status, quiet) {
|
|
|
50
50
|
return 'unknown';
|
|
51
51
|
return null;
|
|
52
52
|
}
|
|
53
|
+
// The figures off a seat's own pane: only a composer screen — idle, unsent or working — shows a
|
|
54
|
+
// status row at all, and only that row's line is read. A dialog, a question, a trust screen or an
|
|
55
|
+
// unknown one gives no figures, and neither does a line the seat printed or typed.
|
|
56
|
+
function quotaOf(cli, screen, pane) {
|
|
57
|
+
if (pane === undefined)
|
|
58
|
+
return [];
|
|
59
|
+
if (screen.kind !== 'idle' && screen.kind !== 'unsent' && screen.kind !== 'working')
|
|
60
|
+
return [];
|
|
61
|
+
return figuresOf(quotaFor(cli), statusRow(cli, pane));
|
|
62
|
+
}
|
|
53
63
|
// One pass of the watch. Pure: it reads what it is handed and changes only `memory`.
|
|
54
|
-
|
|
55
|
-
// never approved, undefined when that wasn't looked at. `watch` and `budgets` are the sections in
|
|
56
|
-
// force — the approved ones, or the defaults while the file's own are not approved — and the
|
|
57
|
-
// checks read those, never the file's. `outcomes` is what the accounts' check commands read
|
|
58
|
-
// outside the pass, when the watch last ran them.
|
|
59
|
-
export function pass(team, state, live, machine, now, memory, approval, watch = team.watch, outcomes = [], budgets = team.budgets) {
|
|
64
|
+
export function pass({ team, state, live, machine, now, memory, approval, watch, outcomes = [], budgets = team.budgets, readings: stored = [], }) {
|
|
60
65
|
const reports = [];
|
|
61
66
|
const current = new Set();
|
|
62
67
|
// Reported when it starts, and again only after it has cleared.
|
|
@@ -76,6 +81,7 @@ export function pass(team, state, live, machine, now, memory, approval, watch =
|
|
|
76
81
|
name: seat.name,
|
|
77
82
|
cli: seat.cli,
|
|
78
83
|
vendor: seat.vendor,
|
|
84
|
+
account: seat.account ?? seat.vendor,
|
|
79
85
|
parked: seat.parked || seat.stopped,
|
|
80
86
|
stopped: seat.stopped,
|
|
81
87
|
seat,
|
|
@@ -83,10 +89,18 @@ export function pass(team, state, live, machine, now, memory, approval, watch =
|
|
|
83
89
|
...Object.entries(state.seats).filter(([, recorded]) => recorded.temporary).map(([name, recorded]) => {
|
|
84
90
|
// A temporary seat signs like the seat it is like: its CLI, and its account.
|
|
85
91
|
const like = team.seats.find((seat) => seat.name === recorded.temporary?.like);
|
|
86
|
-
return {
|
|
92
|
+
return {
|
|
93
|
+
name,
|
|
94
|
+
cli: like?.cli ?? '',
|
|
95
|
+
vendor: like?.vendor ?? '',
|
|
96
|
+
account: like?.account ?? like?.vendor ?? '',
|
|
97
|
+
parked: false,
|
|
98
|
+
stopped: false,
|
|
99
|
+
seat: undefined,
|
|
100
|
+
};
|
|
87
101
|
}),
|
|
88
102
|
];
|
|
89
|
-
for (const { name, cli, vendor, parked, stopped, seat } of seats) {
|
|
103
|
+
for (const { name, cli, vendor, account, parked, stopped, seat } of seats) {
|
|
90
104
|
const agent = live.agents.find((candidate) => candidate.name === name);
|
|
91
105
|
const lead = name === team.coordinator || name === team.operator;
|
|
92
106
|
if (!agent) {
|
|
@@ -99,6 +113,7 @@ export function pass(team, state, live, machine, now, memory, approval, watch =
|
|
|
99
113
|
screen: { kind: 'unknown' },
|
|
100
114
|
cli,
|
|
101
115
|
vendor,
|
|
116
|
+
account,
|
|
102
117
|
quota: [],
|
|
103
118
|
running: false,
|
|
104
119
|
quiet: false,
|
|
@@ -141,7 +156,8 @@ export function pass(team, state, live, machine, now, memory, approval, watch =
|
|
|
141
156
|
screen,
|
|
142
157
|
cli,
|
|
143
158
|
vendor,
|
|
144
|
-
|
|
159
|
+
account,
|
|
160
|
+
quota: quotaOf(cli, screen, pane),
|
|
145
161
|
running: true,
|
|
146
162
|
quiet,
|
|
147
163
|
working,
|
|
@@ -154,19 +170,40 @@ export function pass(team, state, live, machine, now, memory, approval, watch =
|
|
|
154
170
|
history: { lastWorking: history.lastWorking, idleSince: history.idleSince },
|
|
155
171
|
});
|
|
156
172
|
}
|
|
157
|
-
// The figures this pass saw, folded into the readings the
|
|
158
|
-
// whose `sources` name `status_line` takes a screen reading: a check-only account never
|
|
159
|
-
// one here, and neither does an account the budgets in force don't name (#50).
|
|
160
|
-
|
|
173
|
+
// The figures this pass saw, folded into the readings the project keeps (§ 4.3, § 4.4). Only an
|
|
174
|
+
// account whose `sources` name `status_line` takes a screen reading: a check-only account never
|
|
175
|
+
// records one here, and neither does an account the budgets in force don't name (#50). A figure
|
|
176
|
+
// names the account its pattern measures (§ 3b): one that measures this seat's vendor is this
|
|
177
|
+
// seat's account — its own `account:` when the file names one, so two accounts of one vendor
|
|
178
|
+
// keep two buckets — while a figure on another account (a launcher showing the vendor it really
|
|
179
|
+
// runs on) stays that account's, whichever seat's screen showed it.
|
|
180
|
+
//
|
|
181
|
+
// A seat the approval lists as changed — or as not in the approved file — is drift, and its
|
|
182
|
+
// figures are read as they are but none is folded: an unapproved edit to its `account:` must
|
|
183
|
+
// not move its figure into another account's bucket, where `up` and `add` would count it.
|
|
184
|
+
// Nothing is saved for that seat until the owner approves.
|
|
185
|
+
const drift = new Set((Array.isArray(approval) ? approval : []).map(seatNamed).filter((name) => name !== null));
|
|
186
|
+
let readings = stored.slice();
|
|
161
187
|
for (const seat of observations) {
|
|
162
|
-
if (!seat.running)
|
|
188
|
+
if (!seat.running || drift.has(seat.name))
|
|
163
189
|
continue;
|
|
164
190
|
for (const figure of seat.quota) {
|
|
165
|
-
|
|
166
|
-
|
|
191
|
+
const account = figure.account === seat.vendor ? seat.account : figure.account;
|
|
192
|
+
if (budgets.accounts[account]?.sources.includes('status_line')) {
|
|
193
|
+
readings = observe(readings, { ...figure, account }, seat.name, now);
|
|
167
194
|
}
|
|
168
195
|
}
|
|
169
196
|
}
|
|
197
|
+
// A check reading lands in the same slot (§ 5): what an approved check read outlives the watch,
|
|
198
|
+
// so `up` and `add` count it after the watch has exited. Only an account whose sources name
|
|
199
|
+
// `check`, and only when its check actually read this pass.
|
|
200
|
+
for (const outcome of outcomes) {
|
|
201
|
+
if (outcome.state !== 'read' || outcome.reading.kind !== 'subscription')
|
|
202
|
+
continue;
|
|
203
|
+
if (!budgets.accounts[outcome.account]?.sources.includes('check'))
|
|
204
|
+
continue;
|
|
205
|
+
readings = observeCheck(readings, outcome.account, outcome.reading.windows);
|
|
206
|
+
}
|
|
170
207
|
const teamObservation = {
|
|
171
208
|
team,
|
|
172
209
|
state,
|
|
@@ -191,15 +228,14 @@ export function pass(team, state, live, machine, now, memory, approval, watch =
|
|
|
191
228
|
return memory.slots[kind];
|
|
192
229
|
},
|
|
193
230
|
};
|
|
194
|
-
// The four § 4.2 keeps out of `watch.checks` run whatever the
|
|
231
|
+
// The four § 4.2 keeps out of `watch.checks` run whatever the list says: the validation refuses
|
|
195
232
|
// them, and this holds even for a file that reached memory another way.
|
|
196
233
|
//
|
|
197
|
-
//
|
|
198
|
-
// `
|
|
199
|
-
//
|
|
200
|
-
//
|
|
201
|
-
const
|
|
202
|
-
const off = approved ? new Set(team.watch.checks) : new Set();
|
|
234
|
+
// The off list is the section in force, which the caller passes as `watch`. A file that was
|
|
235
|
+
// approved — `approval` is a list, empty or not — runs that list. An unapproved `watch.checks`
|
|
236
|
+
// edit does not turn anything new off, and a check the approved list turned off stays off.
|
|
237
|
+
// A file never approved, or whose approval was not looked at, turns nothing off.
|
|
238
|
+
const off = Array.isArray(approval) ? new Set(watch.checks) : new Set();
|
|
203
239
|
const enabled = (name) => ALWAYS_ON.includes(name) || !off.has(name);
|
|
204
240
|
for (const seat of observations) {
|
|
205
241
|
for (const check of SEAT_CHECKS)
|
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import type { Screen } from './screen.ts';
|
|
2
2
|
import type { ScreenData } from './screen-data.ts';
|
|
3
|
-
export type { Composer, FallbackRule, LinePattern, Placeholder, Rule, ScreenData, Stage } from './screen-data.ts';
|
|
3
|
+
export type { Composer, FallbackRule, LinePattern, Placeholder, PlaceholderStyle, Rule, ScreenData, Stage } from './screen-data.ts';
|
|
4
4
|
export type ReadClock = {
|
|
5
5
|
now(): number;
|
|
6
6
|
budgetMs: number;
|
|
7
7
|
};
|
|
8
|
+
/**
|
|
9
|
+
* The one line a quota figure may be read from: the composer's own status row, or null when the
|
|
10
|
+
* window shows none. A dialog, a transcript line, the input box — anything the seat printed or
|
|
11
|
+
* typed — is never the row. The caller still has to know the screen is a composer screen.
|
|
12
|
+
*/
|
|
13
|
+
export declare function statusRowOf(data: ScreenData, lines: string[]): string | null;
|
|
8
14
|
/** `lines` is already the window: the last 20 lines, each trimmed at the end. */
|
|
9
15
|
export declare function classifyLines(data: ScreenData, lines: string[], clock?: ReadClock): Screen;
|
|
10
16
|
/**
|