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
|
@@ -9,6 +9,8 @@ export type StatusSources = {
|
|
|
9
9
|
watchInForce(team: TeamFile, root: string): TeamFile['watch'];
|
|
10
10
|
budgetsInForce(team: TeamFile, root: string): TeamFile['budgets'];
|
|
11
11
|
now(): Date;
|
|
12
|
+
/** The home whose store holds the override file. Absent in a test that does not set one. */
|
|
13
|
+
home?: string;
|
|
12
14
|
};
|
|
13
15
|
export declare const realSources: StatusSources;
|
|
14
16
|
export declare const status: Command;
|
package/dist/commands/status.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
2
3
|
import { relative, resolve } from 'node:path';
|
|
3
4
|
import { approvalDifferences, budgetsInForce, watchInForce } from "../approve/approval.js";
|
|
5
|
+
import { overridesInForce } from "../profiles/overrides.js";
|
|
4
6
|
import { readArgs } from "../args.js";
|
|
5
7
|
import { budgetLine, budgetTable } from "../budgets/table.js";
|
|
6
8
|
import { recall } from "../budgets/readings.js";
|
|
@@ -41,6 +43,7 @@ export const realSources = {
|
|
|
41
43
|
watchInForce: (team, root) => watchInForce(team, root),
|
|
42
44
|
budgetsInForce: (team, root) => budgetsInForce(team, root),
|
|
43
45
|
now: () => new Date(),
|
|
46
|
+
home: homedir(),
|
|
44
47
|
};
|
|
45
48
|
export const status = (argv, io) => runStatus(argv, io, realSources);
|
|
46
49
|
export default status;
|
|
@@ -62,17 +65,21 @@ export async function runStatus(argv, io, sources) {
|
|
|
62
65
|
for (const warning of current.warnings)
|
|
63
66
|
io.stderr(`team status: warning, line ${warning.line}: ${warning.message}\n`);
|
|
64
67
|
const session = args.values.session ?? team.session;
|
|
68
|
+
const overrides = sources.home ? overridesInForce(team.project, root, sources.home) : emptyOverrides();
|
|
69
|
+
for (const problem of overrides.problems)
|
|
70
|
+
io.stderr(`team status: ${problem}\n`);
|
|
65
71
|
const live = sources.live(session, team);
|
|
66
72
|
if (!live) {
|
|
67
73
|
io.stderr('team status: herdr doesn\'t answer; is it installed and running?\n');
|
|
68
74
|
return 2;
|
|
69
75
|
}
|
|
70
|
-
const
|
|
71
|
-
const
|
|
76
|
+
const whole = readState(dir);
|
|
77
|
+
const state = whole.sessions[session] ?? emptySession();
|
|
78
|
+
const budgets = budgetTable(sources.budgetsInForce(team, root), recall(whole.budgets), sources.now().getTime());
|
|
72
79
|
const comparison = compare(team, session, state, live, sources.now(), sources.watchInForce(team, root));
|
|
73
80
|
if (!live.running)
|
|
74
81
|
comparison.notes.unshift(`the herdr session "${session}" is not running`);
|
|
75
|
-
comparison.differences.push(...protectedCheckouts(team, root, sources), ...approvalDrift(sources.approval(team, root)));
|
|
82
|
+
comparison.differences.push(...protectedCheckouts(team, root, sources), ...approvalDrift(sources.approval(team, root)), ...overrideDrift(overrides));
|
|
76
83
|
if (args.flags.has('json')) {
|
|
77
84
|
const doc = {
|
|
78
85
|
format: 1,
|
|
@@ -91,6 +98,18 @@ export async function runStatus(argv, io, sources) {
|
|
|
91
98
|
}
|
|
92
99
|
return comparison.differences.length ? 1 : 0;
|
|
93
100
|
}
|
|
101
|
+
function emptyOverrides() {
|
|
102
|
+
return { profiles: [], differences: [], problems: [] };
|
|
103
|
+
}
|
|
104
|
+
function overrideDrift(report) {
|
|
105
|
+
return [
|
|
106
|
+
...report.differences.map((line) => ({
|
|
107
|
+
what: `the overrides differ from the approved copy: ${line}`,
|
|
108
|
+
repair: 'the owner runs team approve',
|
|
109
|
+
})),
|
|
110
|
+
...report.problems.map((problem) => ({ what: problem, repair: 'fix the overrides file' })),
|
|
111
|
+
];
|
|
112
|
+
}
|
|
94
113
|
// A file that was never approved, or was changed since, runs nothing until the owner approves it.
|
|
95
114
|
export function approvalDrift(differences) {
|
|
96
115
|
if (differences === null)
|
package/dist/commands/up.d.ts
CHANGED
|
@@ -35,6 +35,8 @@ export type Launch = {
|
|
|
35
35
|
typeText?(session: string, pane: string, text: string): boolean;
|
|
36
36
|
pressEnter?(session: string, pane: string): boolean;
|
|
37
37
|
agentStatus?(session: string, pane: string): string | null;
|
|
38
|
+
/** Foreground argv0 names, or null when the pane can't be read. */
|
|
39
|
+
foreground(session: string, pane: string): string[] | null;
|
|
38
40
|
sleep(ms: number): Promise<void>;
|
|
39
41
|
now(): Date;
|
|
40
42
|
};
|
|
@@ -44,6 +46,6 @@ export declare const realSources: UpSources;
|
|
|
44
46
|
export declare const USAGE = "Usage: team up [--dry-run] [--session <name>] [--file <path>]\n";
|
|
45
47
|
export declare const up: Command;
|
|
46
48
|
export default up;
|
|
47
|
-
/** The rules one seat gets at launch, with its own signature lines. */
|
|
49
|
+
/** The rules one seat gets at launch, with its own signature lines, as its delivery carries them. */
|
|
48
50
|
export declare function rulesOf(team: TeamFile, seat: Seat): string;
|
|
49
51
|
export declare function runUp(argv: string[], io: Io, sources: UpSources): Promise<number>;
|
package/dist/commands/up.js
CHANGED
|
@@ -7,7 +7,7 @@ import { readArgs } from "../args.js";
|
|
|
7
7
|
import { callerOf, describeCaller, isOwner } from "../caller.js";
|
|
8
8
|
import { loadTeamFile } from "../file/load.js";
|
|
9
9
|
import { renderSignature } from "../file/signature.js";
|
|
10
|
-
import { agentList, agentStatus, agentRename, paneRead, paneRun, typeText, pressEnter, sessionRunning, sessionState, startServer, workspaceClose, workspaceCreate, workspaceList, } from "../herdr.js";
|
|
10
|
+
import { agentList, agentStatus, agentRename, paneForeground, paneRead, paneRun, typeText, pressEnter, sessionRunning, sessionState, startServer, workspaceClose, workspaceCreate, workspaceList, } from "../herdr.js";
|
|
11
11
|
import { executePlan } from "../launch/execute.js";
|
|
12
12
|
import { formatPlan, upPlan } from "../launch/plan.js";
|
|
13
13
|
import { rulesText } from "../launch/rules.js";
|
|
@@ -49,6 +49,7 @@ const realLaunch = {
|
|
|
49
49
|
typeText: (session, pane, text) => typeText(pane, text, aim(session)),
|
|
50
50
|
pressEnter: (session, pane) => pressEnter(pane, aim(session)),
|
|
51
51
|
agentStatus: (session, pane) => agentStatus(pane, aim(session)),
|
|
52
|
+
foreground: (session, pane) => paneForeground(pane, aim(session)),
|
|
52
53
|
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
53
54
|
now: () => new Date(),
|
|
54
55
|
};
|
|
@@ -77,9 +78,11 @@ export const realSources = {
|
|
|
77
78
|
export const USAGE = 'Usage: team up [--dry-run] [--session <name>] [--file <path>]\n';
|
|
78
79
|
export const up = (argv, io) => runUp(argv, io, realSources);
|
|
79
80
|
export default up;
|
|
80
|
-
/** The rules one seat gets at launch, with its own signature lines. */
|
|
81
|
+
/** The rules one seat gets at launch, with its own signature lines, as its delivery carries them. */
|
|
81
82
|
export function rulesOf(team, seat) {
|
|
82
83
|
const { commits, pullRequests } = team.identity.signature;
|
|
84
|
+
const profile = profileFor(seat.cli);
|
|
85
|
+
const delivery = profile && profile.rulesOption !== null ? 'option' : 'message';
|
|
83
86
|
return rulesText({
|
|
84
87
|
coordinator: team.coordinator,
|
|
85
88
|
rules: team.rules,
|
|
@@ -89,7 +92,7 @@ export function rulesOf(team, seat) {
|
|
|
89
92
|
commitPosition: commits.position,
|
|
90
93
|
},
|
|
91
94
|
workspace: { mode: seat.mode, protected: team.workspace.protected, branch: team.workspace.branch },
|
|
92
|
-
});
|
|
95
|
+
}, delivery);
|
|
93
96
|
}
|
|
94
97
|
function resolveState(sources, session) {
|
|
95
98
|
if (sources.sessionState)
|
|
@@ -208,8 +211,8 @@ export async function runUp(argv, io, sources) {
|
|
|
208
211
|
}
|
|
209
212
|
const recorded = readState(dir).sessions[session];
|
|
210
213
|
const workspaces = sources.workspaces?.(session) ?? null;
|
|
211
|
-
const readings = loadReadings(dir
|
|
212
|
-
const spend = loadSpendReadings(dir
|
|
214
|
+
const readings = loadReadings(dir);
|
|
215
|
+
const spend = loadSpendReadings(dir);
|
|
213
216
|
const seats = [];
|
|
214
217
|
const refused = new Set();
|
|
215
218
|
for (const seat of team.seats) {
|
|
@@ -305,6 +308,7 @@ export async function runUp(argv, io, sources) {
|
|
|
305
308
|
status: () => launch.agentStatus?.(session, pane) ?? null,
|
|
306
309
|
type: (value) => launch.typeText?.(session, pane, value) ?? false,
|
|
307
310
|
enter: () => launch.pressEnter?.(session, pane) ?? false,
|
|
311
|
+
foreground: () => launch.foreground(session, pane),
|
|
308
312
|
now: () => now().getTime(),
|
|
309
313
|
sleep: sources.sleep ?? launch.sleep,
|
|
310
314
|
}),
|
|
@@ -331,7 +335,9 @@ export async function runUp(argv, io, sources) {
|
|
|
331
335
|
updateState(dir, (file) => {
|
|
332
336
|
const current = (file.sessions[session] ??= emptySession());
|
|
333
337
|
const prior = current.seats[name] ?? { stage: patch.stage };
|
|
334
|
-
|
|
338
|
+
// The CLI the seat was launched with: `down` needs it when the file no longer names the seat.
|
|
339
|
+
const cli = team.seats.find((seat) => seat.name === name)?.cli;
|
|
340
|
+
current.seats[name] = { ...prior, ...patch, ...(cli ? { cli } : {}) };
|
|
335
341
|
});
|
|
336
342
|
},
|
|
337
343
|
running(name) {
|
package/dist/commands/watch.d.ts
CHANGED
|
@@ -14,6 +14,8 @@ export type WatchSources = {
|
|
|
14
14
|
readChecks(team: TeamFile, root: string, now: number): CheckOutcome[];
|
|
15
15
|
screen(pane: string, session: string): string | null;
|
|
16
16
|
status(pane: string, session: string): string | null;
|
|
17
|
+
/** Foreground argv0 names, or null when the pane can't be read. */
|
|
18
|
+
foreground(pane: string, session: string): string[] | null;
|
|
17
19
|
typeText(pane: string, text: string, session: string): boolean;
|
|
18
20
|
pressEnter(pane: string, session: string): boolean;
|
|
19
21
|
notify(text: string): void;
|
|
@@ -27,6 +29,8 @@ export type WatchSources = {
|
|
|
27
29
|
stopSeat?(session: string, seat: DownSeat): Promise<boolean>;
|
|
28
30
|
/** Removes one merged worktree. The real watch calls `worktree remove`. */
|
|
29
31
|
removeWorktree?(task: string): number;
|
|
32
|
+
/** The home whose store holds the override file. Absent in a test that does not set one. */
|
|
33
|
+
home?: string;
|
|
30
34
|
};
|
|
31
35
|
export declare const realWatchSources: WatchSources;
|
|
32
36
|
export declare const watch: Command;
|
package/dist/commands/watch.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { readArgs } from "../args.js";
|
|
2
2
|
import { budgetsInForce, watchInForce } from "../approve/approval.js";
|
|
3
|
-
import {
|
|
3
|
+
import { callerOf, describeCaller, isOwner } from "../caller.js";
|
|
4
|
+
import { loadReadings, saveSpendReadings, updateReadings } from "../budgets/readings.js";
|
|
4
5
|
import { runChecks } from "../budgets/run.js";
|
|
5
6
|
import { currentTeam } from "../file/current.js";
|
|
6
7
|
import { homedir } from 'node:os';
|
|
7
|
-
import { agentStatus, paneRead, pressEnter, typeText } from "../herdr.js";
|
|
8
|
+
import { agentStatus, paneForeground, paneRead, pressEnter, typeText } from "../herdr.js";
|
|
8
9
|
import { logLine } from "../log.js";
|
|
9
10
|
import { emptySession, readState, updateState } from "../state.js";
|
|
10
11
|
import { readMachine } from "../watch/machine.js";
|
|
@@ -13,9 +14,13 @@ import { newMemory, pass } from "../watch/pass.js";
|
|
|
13
14
|
import { readScreen } from "../watch/screen.js";
|
|
14
15
|
import { judgeTemporary, judgeWorktree } from "../watch/close.js";
|
|
15
16
|
import { readEnd } from "../watch/end.js";
|
|
17
|
+
import { reportedLiveAgent } from "../launch/agent.js";
|
|
18
|
+
import { boxHoldsText } from "../launch/deliver.js";
|
|
16
19
|
import { stopRunning, realSources as removeSources } from "./remove.js";
|
|
17
20
|
import { removeWorktree } from "./worktree.js";
|
|
18
21
|
import { stateOf } from "./down.js";
|
|
22
|
+
import { profileFor } from "../profiles/index.js";
|
|
23
|
+
import { classifyWith, overridesInForce, quotaWith } from "../profiles/overrides.js";
|
|
19
24
|
import { realSources } from "./status.js";
|
|
20
25
|
function waitOrStop(seconds) {
|
|
21
26
|
return new Promise((done) => {
|
|
@@ -41,6 +46,7 @@ export const realWatchSources = {
|
|
|
41
46
|
readChecks: (team, root, now) => runChecks(team, root, now),
|
|
42
47
|
screen: (pane, session) => paneRead(pane, 14, session),
|
|
43
48
|
status: agentStatus,
|
|
49
|
+
foreground: (pane, session) => paneForeground(pane, session),
|
|
44
50
|
typeText,
|
|
45
51
|
pressEnter,
|
|
46
52
|
notify,
|
|
@@ -56,6 +62,7 @@ export const realWatchSources = {
|
|
|
56
62
|
}
|
|
57
63
|
},
|
|
58
64
|
pid: process.pid,
|
|
65
|
+
home: homedir(),
|
|
59
66
|
};
|
|
60
67
|
export const watch = (argv, io) => runWatch(argv, io, realWatchSources);
|
|
61
68
|
export default watch;
|
|
@@ -74,17 +81,30 @@ export async function runWatch(argv, io, sources) {
|
|
|
74
81
|
}
|
|
75
82
|
const session = args.values.session ?? first.team.session;
|
|
76
83
|
const { dir } = first;
|
|
84
|
+
// A seat that starts the session's only watch must not be able to drop the operator's nudge
|
|
85
|
+
// or the operator's desktop notices. The owner's own `--no-notify` still cannot silence a
|
|
86
|
+
// report addressed to the owner: those are routed below.
|
|
87
|
+
if (args.flags.has('no-nudge') || args.flags.has('no-notify')) {
|
|
88
|
+
const caller = callerOf(io, session);
|
|
89
|
+
if (!isOwner(caller)) {
|
|
90
|
+
io.stderr(`team watch: --no-nudge and --no-notify are the owner's, from a terminal outside herdr; this call is ${describeCaller(caller)}\n`);
|
|
91
|
+
return 1;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
77
94
|
const other = readState(dir).sessions[session]?.watch;
|
|
78
95
|
if (other && other.pid !== sources.pid && sources.alive(other.pid)) {
|
|
79
96
|
io.stderr(`team watch: a watch already runs for the session "${session}" (pid ${other.pid})\n`);
|
|
80
97
|
return 1;
|
|
81
98
|
}
|
|
82
|
-
|
|
99
|
+
// The log line is written for every report. `--no-notify` never removes it, and it never
|
|
100
|
+
// removes a desktop notice addressed to the owner. It does silence every other notice.
|
|
101
|
+
const tell = (text, notify) => {
|
|
83
102
|
io.stdout(`${sources.now().toISOString()} ${text}\n`);
|
|
84
103
|
logLine(dir, 'watch', 'watch', text, sources.now());
|
|
85
|
-
if (
|
|
104
|
+
if (notify)
|
|
86
105
|
sources.notify(text);
|
|
87
106
|
};
|
|
107
|
+
const say = (text, desktop) => tell(text, desktop && !args.flags.has('no-notify'));
|
|
88
108
|
const beat = () => updateState(dir, (state) => {
|
|
89
109
|
const record = (state.sessions[session] ??= emptySession());
|
|
90
110
|
record.watch = { pid: sources.pid, heartbeat: sources.now().toISOString() };
|
|
@@ -102,11 +122,32 @@ export async function runWatch(argv, io, sources) {
|
|
|
102
122
|
const current = currentTeam(io.cwd, args.values.file, sources.now());
|
|
103
123
|
const team = current.ok ? current.team : first.team;
|
|
104
124
|
const root = current.ok ? current.root : first.root;
|
|
125
|
+
const overrides = sources.home
|
|
126
|
+
? overridesInForce(team.project, root, sources.home)
|
|
127
|
+
: { profiles: [], differences: [], problems: [] };
|
|
128
|
+
for (const line of [
|
|
129
|
+
...overrides.problems,
|
|
130
|
+
...overrides.differences.map((difference) => `the overrides differ from the approved copy: ${difference}`),
|
|
131
|
+
]) {
|
|
132
|
+
if (told.has(line))
|
|
133
|
+
continue;
|
|
134
|
+
told.add(line);
|
|
135
|
+
say(line, false);
|
|
136
|
+
}
|
|
105
137
|
const problem = current.ok ? current.notice : `team.yaml can't be read (${current.errors[0]?.message ?? 'unknown'}); watching with the team as it was`;
|
|
106
138
|
if (problem !== notice) {
|
|
107
139
|
notice = problem;
|
|
108
140
|
if (problem)
|
|
109
|
-
|
|
141
|
+
tell(problem, true);
|
|
142
|
+
}
|
|
143
|
+
// Only the watch of the file's own session saves readings. A watch on another session —
|
|
144
|
+
// anyone may run one, from any terminal — reads and reports, but the figures it sees are
|
|
145
|
+
// not the project's cache that `up` and `add` count (§ 4.4): it says so once and writes
|
|
146
|
+
// none, budget or spend.
|
|
147
|
+
const foreign = session !== team.session;
|
|
148
|
+
if (foreign && !told.has('foreign-session')) {
|
|
149
|
+
told.add('foreign-session');
|
|
150
|
+
say(`the session "${session}" is not this file's "${team.session}": its readings are not saved`, false);
|
|
110
151
|
}
|
|
111
152
|
// The values in force, read with the file: until the owner approves an edit to `watch`,
|
|
112
153
|
// the watch keeps running with what was approved, or with the defaults.
|
|
@@ -117,7 +158,7 @@ export async function runWatch(argv, io, sources) {
|
|
|
117
158
|
const live = sources.live(session, team);
|
|
118
159
|
if (!live) {
|
|
119
160
|
if (!silent)
|
|
120
|
-
|
|
161
|
+
tell('herdr doesn\'t answer; the watch keeps trying', true);
|
|
121
162
|
silent = true;
|
|
122
163
|
}
|
|
123
164
|
else {
|
|
@@ -134,7 +175,8 @@ export async function runWatch(argv, io, sources) {
|
|
|
134
175
|
// The money a spend check counted is kept, like the pass's screen readings: the launch
|
|
135
176
|
// gate of `up` and `add` reads it later, and one that is stale by then reads unknown
|
|
136
177
|
// (§ 5). An account whose check did not run keeps its stored reading.
|
|
137
|
-
|
|
178
|
+
if (!foreign)
|
|
179
|
+
saveSpendReadings(dir, spendOf(outcomes));
|
|
138
180
|
for (const outcome of outcomes) {
|
|
139
181
|
const key = `check:${outcome.account}`;
|
|
140
182
|
if (outcome.state === 'read') {
|
|
@@ -149,21 +191,38 @@ export async function runWatch(argv, io, sources) {
|
|
|
149
191
|
: `the check for ${outcome.account} is unapproved; that account reads unknown`, false);
|
|
150
192
|
}
|
|
151
193
|
}
|
|
152
|
-
|
|
194
|
+
// Herdr's process list per pane, read with the screen: a figure is only read off a pane
|
|
195
|
+
// where herdr reports the seat's CLI. A pane it can't read is null, and a null is not a
|
|
196
|
+
// CLI: no figure.
|
|
197
|
+
const foreground = {};
|
|
198
|
+
for (const agent of live.agents)
|
|
199
|
+
foreground[agent.pane] = sources.foreground(agent.pane, session);
|
|
200
|
+
const run = (stored) => pass({
|
|
153
201
|
team, state, live, machine: sources.machine(root), now, memory,
|
|
154
202
|
approval: sources.approval(team, root), watch: inForce, outcomes, budgets: budget,
|
|
203
|
+
quotaFor: (cli) => quotaWith(cli, overrides.profiles),
|
|
204
|
+
readScreen: (cli, pane) => classifyWith(cli, pane, overrides.profiles),
|
|
205
|
+
readings: stored, foreground,
|
|
155
206
|
});
|
|
207
|
+
// The pass folds its figures where the state is held: two watches of the project fold one
|
|
208
|
+
// after the other, not over each other. A watch on a foreign session still folds, for its
|
|
209
|
+
// reports, and saves nothing.
|
|
210
|
+
const result = foreign
|
|
211
|
+
? run(loadReadings(dir))
|
|
212
|
+
: updateReadings(dir, now, (stored) => {
|
|
213
|
+
const folded = run(stored);
|
|
214
|
+
return { readings: folded.readings, value: folded };
|
|
215
|
+
});
|
|
156
216
|
for (const report of result.reports)
|
|
157
|
-
|
|
158
|
-
saveReadings(dir, session, result.readings, now);
|
|
217
|
+
tell(report.text, report.to === 'owner' || !args.flags.has('no-notify'));
|
|
159
218
|
if (result.nudge) {
|
|
160
219
|
if (args.flags.has('no-nudge'))
|
|
161
220
|
say(`nudge not typed (--no-nudge): ${result.nudge.text}`, false);
|
|
162
221
|
else
|
|
163
|
-
deliver(result.nudge, team, session, sources, memory, say);
|
|
222
|
+
deliver(result.nudge, team, session, sources, memory, say, tell, told);
|
|
164
223
|
}
|
|
165
224
|
if (result.fallback)
|
|
166
|
-
|
|
225
|
+
tell(result.fallback, true);
|
|
167
226
|
await closeEnded({ team, root, dir, session, live, sources, say, io, told });
|
|
168
227
|
}
|
|
169
228
|
beat();
|
|
@@ -177,7 +236,7 @@ export async function runWatch(argv, io, sources) {
|
|
|
177
236
|
if (record?.watch?.pid === sources.pid)
|
|
178
237
|
delete record.watch;
|
|
179
238
|
});
|
|
180
|
-
|
|
239
|
+
tell(`the watch of "${session}" stopped`, true);
|
|
181
240
|
}
|
|
182
241
|
return 0;
|
|
183
242
|
}
|
|
@@ -198,7 +257,7 @@ function spendOf(outcomes) {
|
|
|
198
257
|
}
|
|
199
258
|
// Types the nudge, after reading the operator's screen once more: the pass saw it free, and a
|
|
200
259
|
// prompt may have appeared since. Anything but an empty idle prompt keeps the nudge pending.
|
|
201
|
-
function deliver(nudge, team, session, sources, memory, say) {
|
|
260
|
+
function deliver(nudge, team, session, sources, memory, say, tell, told) {
|
|
202
261
|
const cli = team.seats.find((seat) => seat.name === team.operator)?.cli ?? '';
|
|
203
262
|
const look = () => readScreen(cli, sources.screen(nudge.pane, session) ?? undefined).kind;
|
|
204
263
|
const status = sources.status(nudge.pane, session);
|
|
@@ -208,15 +267,30 @@ function deliver(nudge, team, session, sources, memory, say) {
|
|
|
208
267
|
memory.pending.push(...nudge.pending);
|
|
209
268
|
memory.pendingSince ??= sources.now().getTime();
|
|
210
269
|
};
|
|
270
|
+
const names = profileFor(cli)?.processNames ?? [];
|
|
271
|
+
const live = () => reportedLiveAgent(sources.foreground(nudge.pane, session), names);
|
|
272
|
+
const noAgent = 'nudge:no-agent';
|
|
273
|
+
if (!live()) {
|
|
274
|
+
tellOnce(told, noAgent, 'a nudge was not typed: no live agent in the operator\'s pane', tell);
|
|
275
|
+
keep();
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
told.delete(noAgent);
|
|
211
279
|
if ((status !== 'idle' && status !== 'done') || look() !== 'idle' || !sources.typeText(nudge.pane, nudge.text, session)) {
|
|
212
280
|
keep();
|
|
213
281
|
return;
|
|
214
282
|
}
|
|
215
|
-
// The text is in the box.
|
|
216
|
-
//
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
283
|
+
// The text is in the box. The agent is read again before Enter: it may have exited
|
|
284
|
+
// since the text was typed, and an unframed line is not a box to send.
|
|
285
|
+
if (!live()) {
|
|
286
|
+
tellOnce(told, noAgent, 'a nudge was not typed: no live agent in the operator\'s pane', tell);
|
|
287
|
+
keep();
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
// The box is read back before the Enter: it must hold exactly the nudge's own text. An idle
|
|
291
|
+
// or changed screen here is the text not rendered, and unsent text alone is not this nudge.
|
|
292
|
+
if (!boxHoldsText(cli, nudge.text, sources.screen(nudge.pane, session) ?? undefined)) {
|
|
293
|
+
tell('a nudge was typed and not sent: the operator\'s box does not hold it', true);
|
|
220
294
|
keep();
|
|
221
295
|
return;
|
|
222
296
|
}
|
|
@@ -225,6 +299,12 @@ function deliver(nudge, team, session, sources, memory, say) {
|
|
|
225
299
|
else
|
|
226
300
|
keep();
|
|
227
301
|
}
|
|
302
|
+
function tellOnce(told, key, text, tell) {
|
|
303
|
+
if (told.has(key))
|
|
304
|
+
return;
|
|
305
|
+
told.add(key);
|
|
306
|
+
tell(text, true);
|
|
307
|
+
}
|
|
228
308
|
function noteWorked(dir, session, live) {
|
|
229
309
|
const recorded = readState(dir).sessions[session];
|
|
230
310
|
if (!recorded)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { createInterface } from 'node:readline';
|
|
2
|
+
import { classify, classifyComposer } from "../watch/screen.js";
|
|
3
|
+
import { parseYaml } from "../yaml.js";
|
|
4
|
+
/** The answer for one request line. A line that is not a request is an error object. */
|
|
5
|
+
export function respond(line) {
|
|
6
|
+
let request;
|
|
7
|
+
try {
|
|
8
|
+
request = JSON.parse(line);
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return JSON.stringify({ error: 'the line is not JSON' });
|
|
12
|
+
}
|
|
13
|
+
if (request === null || typeof request !== 'object')
|
|
14
|
+
return JSON.stringify({ error: 'a request needs an op' });
|
|
15
|
+
if (request.op === 'classify' || request.op === 'composer') {
|
|
16
|
+
if (typeof request.cli !== 'string' || !Array.isArray(request.lines) || request.lines.some((item) => typeof item !== 'string')) {
|
|
17
|
+
return JSON.stringify({ error: 'classify needs a cli and lines of text' });
|
|
18
|
+
}
|
|
19
|
+
const screen = request.op === 'classify' ? classify(request.cli, request.lines) : classifyComposer(request.cli, request.lines);
|
|
20
|
+
return JSON.stringify({ kind: screen.kind });
|
|
21
|
+
}
|
|
22
|
+
if (request.op === 'yaml') {
|
|
23
|
+
if (typeof request.text !== 'string')
|
|
24
|
+
return JSON.stringify({ error: 'yaml needs text' });
|
|
25
|
+
try {
|
|
26
|
+
parseYaml(request.text);
|
|
27
|
+
return JSON.stringify({ ok: true });
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return JSON.stringify({ ok: false });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return JSON.stringify({ error: 'unknown op' });
|
|
34
|
+
}
|
|
35
|
+
const adapter = async (_argv, io) => {
|
|
36
|
+
const lines = createInterface({ input: process.stdin });
|
|
37
|
+
for await (const line of lines) {
|
|
38
|
+
if (line.length === 0)
|
|
39
|
+
continue;
|
|
40
|
+
io.stdout(`${respond(line)}\n`);
|
|
41
|
+
}
|
|
42
|
+
return 0;
|
|
43
|
+
};
|
|
44
|
+
export default adapter;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { YamlEntry, YamlNode } from '../yaml.ts';
|
|
2
|
+
import type { Problem } from './types.ts';
|
|
3
|
+
export type Fields = Map<string, YamlEntry>;
|
|
4
|
+
export declare class Check {
|
|
5
|
+
problems: Problem[];
|
|
6
|
+
warnings: Problem[];
|
|
7
|
+
fail(line: number, message: string): void;
|
|
8
|
+
fields(node: YamlNode | undefined, where: string, known: string[]): Fields;
|
|
9
|
+
text(entry: YamlEntry | undefined, name: string): string | undefined;
|
|
10
|
+
required(entry: YamlEntry | undefined, name: string, line: number): string | undefined;
|
|
11
|
+
oneOf<T extends string>(entry: YamlEntry | undefined, name: string, values: T[]): T | undefined;
|
|
12
|
+
flag(entry: YamlEntry | undefined, name: string): boolean;
|
|
13
|
+
whole(entry: YamlEntry | undefined, name: string, least: number): number | undefined;
|
|
14
|
+
number(entry: YamlEntry | undefined, name: string): number | undefined;
|
|
15
|
+
measure(entry: YamlEntry | undefined, name: string, units: Record<string, number>, example: string): number | undefined;
|
|
16
|
+
list(entry: YamlEntry | undefined, name: string): {
|
|
17
|
+
value: string;
|
|
18
|
+
line: number;
|
|
19
|
+
}[];
|
|
20
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
export class Check {
|
|
2
|
+
problems = [];
|
|
3
|
+
warnings = [];
|
|
4
|
+
fail(line, message) {
|
|
5
|
+
this.problems.push({ line, message });
|
|
6
|
+
}
|
|
7
|
+
// The fields of a map, with every field outside `known` reported.
|
|
8
|
+
fields(node, where, known) {
|
|
9
|
+
const fields = new Map();
|
|
10
|
+
if (!node)
|
|
11
|
+
return fields;
|
|
12
|
+
if (node.kind !== 'map') {
|
|
13
|
+
if (!(node.kind === 'scalar' && node.value === null))
|
|
14
|
+
this.fail(node.line, `${where} must be a map of fields`);
|
|
15
|
+
return fields;
|
|
16
|
+
}
|
|
17
|
+
for (const entry of node.entries) {
|
|
18
|
+
if (known.includes(entry.key))
|
|
19
|
+
fields.set(entry.key, entry);
|
|
20
|
+
else
|
|
21
|
+
this.fail(entry.line, `unknown field "${entry.key}" in ${where}`);
|
|
22
|
+
}
|
|
23
|
+
return fields;
|
|
24
|
+
}
|
|
25
|
+
text(entry, name) {
|
|
26
|
+
if (!entry)
|
|
27
|
+
return undefined;
|
|
28
|
+
const node = entry.value;
|
|
29
|
+
if (node.kind === 'scalar' && typeof node.value === 'string' && node.value !== '')
|
|
30
|
+
return node.value;
|
|
31
|
+
this.fail(node.line, `${name} must be text${node.kind === 'scalar' && node.value !== null && node.value !== '' ? ': quote it' : ''}`);
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
required(entry, name, line) {
|
|
35
|
+
if (!entry)
|
|
36
|
+
this.fail(line, `${name} is required`);
|
|
37
|
+
return this.text(entry, name);
|
|
38
|
+
}
|
|
39
|
+
oneOf(entry, name, values) {
|
|
40
|
+
if (!entry)
|
|
41
|
+
return undefined;
|
|
42
|
+
const node = entry.value;
|
|
43
|
+
if (node.kind === 'scalar' && typeof node.value === 'string' && values.includes(node.value)) {
|
|
44
|
+
return node.value;
|
|
45
|
+
}
|
|
46
|
+
this.fail(node.line, `${name} must be one of: ${values.join(', ')}`);
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
flag(entry, name) {
|
|
50
|
+
if (!entry)
|
|
51
|
+
return false;
|
|
52
|
+
const node = entry.value;
|
|
53
|
+
if (node.kind === 'scalar' && typeof node.value === 'boolean' && !node.quoted)
|
|
54
|
+
return node.value;
|
|
55
|
+
this.fail(node.line, `${name} must be true or false`);
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
whole(entry, name, least) {
|
|
59
|
+
if (!entry)
|
|
60
|
+
return undefined;
|
|
61
|
+
const node = entry.value;
|
|
62
|
+
if (node.kind === 'scalar' && typeof node.value === 'number' && Number.isInteger(node.value) && node.value >= least) {
|
|
63
|
+
return node.value;
|
|
64
|
+
}
|
|
65
|
+
this.fail(node.line, `${name} must be a whole number, ${least} or more`);
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
number(entry, name) {
|
|
69
|
+
if (!entry)
|
|
70
|
+
return undefined;
|
|
71
|
+
const node = entry.value;
|
|
72
|
+
if (node.kind === 'scalar' && typeof node.value === 'number' && Number.isFinite(node.value) && node.value > 0) {
|
|
73
|
+
return node.value;
|
|
74
|
+
}
|
|
75
|
+
this.fail(node.line, `${name} must be a number above 0`);
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
// A value with its unit, such as 120s, 25% or 10GB.
|
|
79
|
+
measure(entry, name, units, example) {
|
|
80
|
+
if (!entry)
|
|
81
|
+
return undefined;
|
|
82
|
+
const node = entry.value;
|
|
83
|
+
const raw = node.kind === 'scalar' ? node.raw : '';
|
|
84
|
+
const match = /^([0-9]+(?:\.[0-9]+)?)([A-Za-z%]+)$/.exec(raw);
|
|
85
|
+
const unit = match?.[2];
|
|
86
|
+
if (match && unit !== undefined && unit in units && node.kind === 'scalar' && !node.quoted) {
|
|
87
|
+
return Number(match[1]) * units[unit];
|
|
88
|
+
}
|
|
89
|
+
this.fail(node.line, `${name} needs a value with its unit (${Object.keys(units).join(', ')}), such as ${example}`);
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
list(entry, name) {
|
|
93
|
+
if (!entry)
|
|
94
|
+
return [];
|
|
95
|
+
const node = entry.value;
|
|
96
|
+
if (node.kind !== 'seq') {
|
|
97
|
+
if (!(node.kind === 'scalar' && node.value === null))
|
|
98
|
+
this.fail(node.line, `${name} must be a list`);
|
|
99
|
+
return [];
|
|
100
|
+
}
|
|
101
|
+
const out = [];
|
|
102
|
+
for (const item of node.items) {
|
|
103
|
+
if (item.kind === 'scalar' && typeof item.value === 'string' && item.value !== '') {
|
|
104
|
+
out.push({ value: item.value, line: item.line });
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
this.fail(item.line, `each item of ${name} must be text${item.kind === 'map' ? ': quote a line that contains ": "' : ''}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return out;
|
|
111
|
+
}
|
|
112
|
+
}
|
package/dist/file/lines.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Edits the team file by the line. Comments, order and every other seat stay as written.
|
|
2
2
|
// Nothing here re-serialises the file.
|
|
3
|
+
import { defaultLabel } from "./validate.js";
|
|
3
4
|
function indent(line) {
|
|
4
5
|
let n = 0;
|
|
5
6
|
while (line[n] === ' ')
|
|
@@ -100,6 +101,11 @@ export function rewriteCount(text, declared) {
|
|
|
100
101
|
return null;
|
|
101
102
|
const lines = body(text, block);
|
|
102
103
|
const label = field(lines, 'label');
|
|
104
|
+
const model = field(lines, 'model');
|
|
105
|
+
const version = field(lines, 'version');
|
|
106
|
+
// A counted seat with no label is titled with the model. Later instances need that title
|
|
107
|
+
// written out, or they collide with the first on the same default.
|
|
108
|
+
const implied = label === null && model !== null && version !== null ? defaultLabel(model, version) : null;
|
|
103
109
|
const copies = [];
|
|
104
110
|
for (let instance = 1; instance <= block.count; instance++) {
|
|
105
111
|
const suffix = instance === 1 ? '' : `-${instance}`;
|
|
@@ -108,8 +114,13 @@ export function rewriteCount(text, declared) {
|
|
|
108
114
|
return [];
|
|
109
115
|
if (instance === 1)
|
|
110
116
|
return [line];
|
|
111
|
-
if (/^\s*(?:-\s+)?name:/.test(line))
|
|
112
|
-
|
|
117
|
+
if (/^\s*(?:-\s+)?name:/.test(line)) {
|
|
118
|
+
const renamed = [setValue(line, `${declared}${suffix}`)];
|
|
119
|
+
if (implied === null)
|
|
120
|
+
return renamed;
|
|
121
|
+
const pad = /^(\s*)/.exec(line)?.[1] ?? '';
|
|
122
|
+
return [...renamed, `${pad}label: ${implied}${suffix}`];
|
|
123
|
+
}
|
|
113
124
|
if (label !== null && /^\s*label:/.test(line))
|
|
114
125
|
return [setValue(line, `${label}${suffix}`)];
|
|
115
126
|
return [line];
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { TeamFile } from '../types.ts';
|
|
2
|
+
import type { Section } from './section.ts';
|
|
3
|
+
/** What the budgets section hands the rest of the file: its value, and the account names it declares. */
|
|
4
|
+
export interface Budgeted {
|
|
5
|
+
budgets: TeamFile['budgets'];
|
|
6
|
+
/** Every name the accounts map writes, whether or not the account itself reads cleanly. */
|
|
7
|
+
declared: Set<string>;
|
|
8
|
+
}
|
|
9
|
+
export declare const budgets: Section;
|
|
10
|
+
/**
|
|
11
|
+
* The budget values a file that sets none runs with — and, since the section is the owner's, the
|
|
12
|
+
* values a file runs with until the owner approves what it sets instead. No accounts: nothing is
|
|
13
|
+
* budgeted until the owner names one.
|
|
14
|
+
*/
|
|
15
|
+
export declare function defaultBudgets(): TeamFile['budgets'];
|