team 0.0.1 → 0.1.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.
Files changed (169) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +310 -0
  3. package/dist/approve/approval.d.ts +38 -0
  4. package/dist/approve/approval.js +78 -0
  5. package/dist/approve/diff.d.ts +13 -0
  6. package/dist/approve/diff.js +47 -0
  7. package/dist/approve/fingerprint.d.ts +44 -0
  8. package/dist/approve/fingerprint.js +109 -0
  9. package/dist/args.d.ts +7 -0
  10. package/dist/args.js +22 -0
  11. package/dist/budgets/checks.d.ts +35 -0
  12. package/dist/budgets/checks.js +77 -0
  13. package/dist/budgets/gate.d.ts +20 -0
  14. package/dist/budgets/gate.js +96 -0
  15. package/dist/budgets/readings.d.ts +47 -0
  16. package/dist/budgets/readings.js +119 -0
  17. package/dist/budgets/run.d.ts +66 -0
  18. package/dist/budgets/run.js +119 -0
  19. package/dist/budgets/table.d.ts +24 -0
  20. package/dist/budgets/table.js +90 -0
  21. package/dist/caller.d.ts +48 -0
  22. package/dist/caller.js +126 -0
  23. package/dist/check/config.d.ts +51 -0
  24. package/dist/check/config.js +50 -0
  25. package/dist/check/git.d.ts +27 -0
  26. package/dist/check/git.js +97 -0
  27. package/dist/check/message.d.ts +27 -0
  28. package/dist/check/message.js +101 -0
  29. package/dist/check/run.d.ts +31 -0
  30. package/dist/check/run.js +77 -0
  31. package/dist/check/signature.d.ts +10 -0
  32. package/dist/check/signature.js +22 -0
  33. package/dist/cli.d.ts +9 -0
  34. package/dist/cli.js +76 -0
  35. package/dist/clis.d.ts +7 -0
  36. package/dist/clis.js +9 -0
  37. package/dist/commands/add.d.ts +23 -0
  38. package/dist/commands/add.js +445 -0
  39. package/dist/commands/approve.d.ts +11 -0
  40. package/dist/commands/approve.js +146 -0
  41. package/dist/commands/check.d.ts +17 -0
  42. package/dist/commands/check.js +103 -0
  43. package/dist/commands/doctor.d.ts +38 -0
  44. package/dist/commands/doctor.js +232 -0
  45. package/dist/commands/down.d.ts +37 -0
  46. package/dist/commands/down.js +258 -0
  47. package/dist/commands/init.d.ts +9 -0
  48. package/dist/commands/init.js +138 -0
  49. package/dist/commands/remove.d.ts +23 -0
  50. package/dist/commands/remove.js +235 -0
  51. package/dist/commands/status.d.ts +41 -0
  52. package/dist/commands/status.js +147 -0
  53. package/dist/commands/up.d.ts +49 -0
  54. package/dist/commands/up.js +361 -0
  55. package/dist/commands/watch.d.ts +35 -0
  56. package/dist/commands/watch.js +302 -0
  57. package/dist/commands/worktree.d.ts +12 -0
  58. package/dist/commands/worktree.js +388 -0
  59. package/dist/end/condition.d.ts +28 -0
  60. package/dist/end/condition.js +77 -0
  61. package/dist/file/current.d.ts +13 -0
  62. package/dist/file/current.js +53 -0
  63. package/dist/file/lines.d.ts +25 -0
  64. package/dist/file/lines.js +277 -0
  65. package/dist/file/load.d.ts +7 -0
  66. package/dist/file/load.js +90 -0
  67. package/dist/file/paths.d.ts +6 -0
  68. package/dist/file/paths.js +77 -0
  69. package/dist/file/secrets.d.ts +6 -0
  70. package/dist/file/secrets.js +54 -0
  71. package/dist/file/signature.d.ts +13 -0
  72. package/dist/file/signature.js +36 -0
  73. package/dist/file/types.d.ts +130 -0
  74. package/dist/file/types.js +1 -0
  75. package/dist/file/validate.d.ts +13 -0
  76. package/dist/file/validate.js +702 -0
  77. package/dist/file/write.d.ts +11 -0
  78. package/dist/file/write.js +13 -0
  79. package/dist/herdr.d.ts +38 -0
  80. package/dist/herdr.js +227 -0
  81. package/dist/index.d.ts +10 -0
  82. package/dist/index.js +6 -0
  83. package/dist/io.d.ts +10 -0
  84. package/dist/io.js +1 -0
  85. package/dist/launch/deliver.d.ts +10 -0
  86. package/dist/launch/deliver.js +39 -0
  87. package/dist/launch/execute.d.ts +46 -0
  88. package/dist/launch/execute.js +284 -0
  89. package/dist/launch/plan.d.ts +185 -0
  90. package/dist/launch/plan.js +280 -0
  91. package/dist/launch/rules.d.ts +25 -0
  92. package/dist/launch/rules.js +36 -0
  93. package/dist/log.d.ts +1 -0
  94. package/dist/log.js +24 -0
  95. package/dist/profiles/antigravity.yaml +66 -0
  96. package/dist/profiles/claude-code.yaml +69 -0
  97. package/dist/profiles/codex.yaml +77 -0
  98. package/dist/profiles/cursor.yaml +68 -0
  99. package/dist/profiles/index.d.ts +1 -0
  100. package/dist/profiles/index.js +1 -0
  101. package/dist/profiles/profile.d.ts +60 -0
  102. package/dist/profiles/profile.js +313 -0
  103. package/dist/profiles/quota.d.ts +22 -0
  104. package/dist/profiles/quota.js +56 -0
  105. package/dist/state.d.ts +59 -0
  106. package/dist/state.js +127 -0
  107. package/dist/status/compare.d.ts +26 -0
  108. package/dist/status/compare.js +110 -0
  109. package/dist/status/statusline.d.ts +9 -0
  110. package/dist/status/statusline.js +26 -0
  111. package/dist/store/store.d.ts +62 -0
  112. package/dist/store/store.js +116 -0
  113. package/dist/watch/check.d.ts +82 -0
  114. package/dist/watch/check.js +29 -0
  115. package/dist/watch/checks/approval.d.ts +2 -0
  116. package/dist/watch/checks/approval.js +16 -0
  117. package/dist/watch/checks/attention.d.ts +2 -0
  118. package/dist/watch/checks/attention.js +23 -0
  119. package/dist/watch/checks/budget.d.ts +2 -0
  120. package/dist/watch/checks/budget.js +141 -0
  121. package/dist/watch/checks/disk.d.ts +2 -0
  122. package/dist/watch/checks/disk.js +13 -0
  123. package/dist/watch/checks/extra.d.ts +2 -0
  124. package/dist/watch/checks/extra.js +18 -0
  125. package/dist/watch/checks/idle.d.ts +2 -0
  126. package/dist/watch/checks/idle.js +32 -0
  127. package/dist/watch/checks/load.d.ts +2 -0
  128. package/dist/watch/checks/load.js +11 -0
  129. package/dist/watch/checks/memory.d.ts +2 -0
  130. package/dist/watch/checks/memory.js +11 -0
  131. package/dist/watch/checks/missing.d.ts +2 -0
  132. package/dist/watch/checks/missing.js +12 -0
  133. package/dist/watch/checks/model-drift.d.ts +2 -0
  134. package/dist/watch/checks/model-drift.js +14 -0
  135. package/dist/watch/checks/swap-free.d.ts +2 -0
  136. package/dist/watch/checks/swap-free.js +12 -0
  137. package/dist/watch/checks/swap-growth.d.ts +2 -0
  138. package/dist/watch/checks/swap-growth.js +18 -0
  139. package/dist/watch/checks/team-idle.d.ts +2 -0
  140. package/dist/watch/checks/team-idle.js +18 -0
  141. package/dist/watch/checks/unsent.d.ts +2 -0
  142. package/dist/watch/checks/unsent.js +18 -0
  143. package/dist/watch/close.d.ts +19 -0
  144. package/dist/watch/close.js +40 -0
  145. package/dist/watch/dialect.d.ts +5 -0
  146. package/dist/watch/dialect.js +430 -0
  147. package/dist/watch/end.d.ts +11 -0
  148. package/dist/watch/end.js +17 -0
  149. package/dist/watch/machine.d.ts +38 -0
  150. package/dist/watch/machine.js +137 -0
  151. package/dist/watch/notify.d.ts +1 -0
  152. package/dist/watch/notify.js +15 -0
  153. package/dist/watch/pass.d.ts +30 -0
  154. package/dist/watch/pass.js +237 -0
  155. package/dist/watch/screen-core.d.ts +14 -0
  156. package/dist/watch/screen-core.js +320 -0
  157. package/dist/watch/screen-data.d.ts +65 -0
  158. package/dist/watch/screen-data.js +1 -0
  159. package/dist/watch/screen-file.d.ts +2 -0
  160. package/dist/watch/screen-file.js +269 -0
  161. package/dist/watch/screen.d.ts +20 -0
  162. package/dist/watch/screen.js +38 -0
  163. package/dist/worktree/place.d.ts +18 -0
  164. package/dist/worktree/place.js +126 -0
  165. package/dist/yaml.d.ts +30 -0
  166. package/dist/yaml.js +380 -0
  167. package/examples/checks/codex-quota +217 -0
  168. package/examples/team.yaml +76 -0
  169. package/package.json +29 -10
@@ -0,0 +1,361 @@
1
+ import { mkdirSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { dirname, join } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { approvalDifferences, budgetsInForce } from "../approve/approval.js";
6
+ import { readArgs } from "../args.js";
7
+ import { callerOf, describeCaller, isOwner } from "../caller.js";
8
+ import { loadTeamFile } from "../file/load.js";
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";
11
+ import { executePlan } from "../launch/execute.js";
12
+ import { formatPlan, upPlan } from "../launch/plan.js";
13
+ import { rulesText } from "../launch/rules.js";
14
+ import { deliverRules } from "../launch/deliver.js";
15
+ import { logLine } from "../log.js";
16
+ import { shellQuote } from "../profiles/profile.js";
17
+ import { profileFor } from "../profiles/index.js";
18
+ import { readApproval, storePath } from "../store/store.js";
19
+ import { emptySession, readState, updateState } from "../state.js";
20
+ import { seatBudget } from "../budgets/gate.js";
21
+ import { loadReadings } from "../budgets/readings.js";
22
+ import { blocksLaunch, doctorFindings, realSources as doctorSources } from "./doctor.js";
23
+ import { launchLimit, readMachine } from "../watch/machine.js";
24
+ import { readScreen } from "../watch/screen.js";
25
+ import { seatStart } from "../worktree/place.js";
26
+ function aim(session) {
27
+ return session === 'default' ? undefined : session;
28
+ }
29
+ /** The watchdog command, from this process and this install: `team` may not be on the PATH. */
30
+ export function watchCommand(session) {
31
+ const here = fileURLToPath(import.meta.url);
32
+ const cli = join(dirname(here), '..', here.endsWith('.ts') ? 'cli.ts' : 'cli.js');
33
+ const suffix = session === 'default' ? '' : ` --session ${shellQuote(session)}`;
34
+ return `${shellQuote(process.execPath)} ${shellQuote(cli)} watch${suffix}`;
35
+ }
36
+ const realLaunch = {
37
+ sessionState,
38
+ startServer,
39
+ sessionUp: sessionRunning,
40
+ createWorkspace: (session, cwd, label) => workspaceCreate(cwd, label, aim(session)),
41
+ paneRun: (session, pane, command) => paneRun(pane, command, aim(session)),
42
+ renameAgent: (session, pane, name) => agentRename(pane, name, aim(session)),
43
+ closeWorkspace: (session, workspace) => workspaceClose(workspace, aim(session)),
44
+ agentPanes(session) {
45
+ const agents = agentList(aim(session));
46
+ return agents === null ? null : agents.map((agent) => agent.pane);
47
+ },
48
+ paneText: (session, pane) => paneRead(pane, 200, aim(session)),
49
+ typeText: (session, pane, text) => typeText(pane, text, aim(session)),
50
+ pressEnter: (session, pane) => pressEnter(pane, aim(session)),
51
+ agentStatus: (session, pane) => agentStatus(pane, aim(session)),
52
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
53
+ now: () => new Date(),
54
+ };
55
+ export const realSources = {
56
+ sessionRunning,
57
+ sessionState,
58
+ agents: (session) => agentList(aim(session)),
59
+ workspaces: (session) => workspaceList(aim(session)),
60
+ home: homedir(),
61
+ doctor: doctorSources,
62
+ machine: readMachine,
63
+ now: () => new Date(),
64
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
65
+ alive(pid) {
66
+ try {
67
+ process.kill(pid, 0);
68
+ return true;
69
+ }
70
+ catch {
71
+ return false;
72
+ }
73
+ },
74
+ watchCommand,
75
+ launch: realLaunch,
76
+ };
77
+ export const USAGE = 'Usage: team up [--dry-run] [--session <name>] [--file <path>]\n';
78
+ export const up = (argv, io) => runUp(argv, io, realSources);
79
+ export default up;
80
+ /** The rules one seat gets at launch, with its own signature lines. */
81
+ export function rulesOf(team, seat) {
82
+ const { commits, pullRequests } = team.identity.signature;
83
+ return rulesText({
84
+ coordinator: team.coordinator,
85
+ rules: team.rules,
86
+ signature: {
87
+ commit: renderSignature(commits.template, seat),
88
+ pullRequest: renderSignature(pullRequests.template, seat),
89
+ commitPosition: commits.position,
90
+ },
91
+ workspace: { mode: seat.mode, protected: team.workspace.protected, branch: team.workspace.branch },
92
+ });
93
+ }
94
+ function resolveState(sources, session) {
95
+ if (sources.sessionState)
96
+ return sources.sessionState(session);
97
+ const running = sources.sessionRunning(session);
98
+ if (running === null)
99
+ return null;
100
+ return running ? 'running' : 'absent';
101
+ }
102
+ function overCeiling(ceilings, running, seat) {
103
+ if (running.some((item) => item.name === seat.name))
104
+ return null;
105
+ const count = running.length + 1;
106
+ if (count > ceilings.seats)
107
+ return `the approval allows ${ceilings.seats} seats; ${count} would be running`;
108
+ const cap = ceilings.vendors[seat.vendor];
109
+ if (cap !== undefined) {
110
+ const vendors = running.filter((item) => item.vendor === seat.vendor).length + 1;
111
+ if (vendors > cap)
112
+ return `the approval allows ${cap} ${seat.vendor} seats; ${vendors} would be running`;
113
+ }
114
+ return null;
115
+ }
116
+ function seatPlan(team, seat, recorded, agents, workspaces, resume) {
117
+ const planned = {
118
+ name: seat.name,
119
+ cli: seat.cli,
120
+ launch: seat.launch,
121
+ cwd: seat.cwd,
122
+ label: seat.label,
123
+ stopped: seat.stopped,
124
+ rules: rulesOf(team, seat),
125
+ };
126
+ if (!resume || !recorded || seat.stopped)
127
+ return planned;
128
+ const named = agents.find((agent) => agent.name === seat.name);
129
+ const onPane = recorded.pane ? agents.some((agent) => agent.pane === recorded.pane) : false;
130
+ if (recorded.stage === 'ready')
131
+ return named || onPane ? { ...planned, stage: 'ready' } : planned;
132
+ const workspaceLive = recorded.workspace && workspaces ? workspaces.some((workspace) => workspace.id === recorded.workspace) : null;
133
+ const live = Boolean(named || onPane || workspaceLive);
134
+ if (!live)
135
+ return planned;
136
+ return {
137
+ ...planned,
138
+ stage: recorded.stage,
139
+ pane: recorded.pane,
140
+ workspace: recorded.workspace,
141
+ agentLive: Boolean(named || onPane),
142
+ };
143
+ }
144
+ export async function runUp(argv, io, sources) {
145
+ const args = readArgs(argv, ['session', 'file'], ['dry-run']);
146
+ if (args.error || args.rest.length) {
147
+ io.stderr(`team up: ${args.error ?? `unexpected "${args.rest[0]}"`}\n${USAGE}`);
148
+ return 2;
149
+ }
150
+ const dry = args.flags.has('dry-run');
151
+ const loaded = loadTeamFile(io.cwd, args.values.file ? { file: args.values.file } : {});
152
+ if (!loaded.ok) {
153
+ for (const problem of loaded.errors) {
154
+ io.stderr(`team up: ${problem.line ? `line ${problem.line}: ` : ''}${problem.message}\n`);
155
+ }
156
+ return 2;
157
+ }
158
+ const { team, root } = loaded;
159
+ const session = args.values.session ?? team.session;
160
+ const dir = dirname(loaded.path);
161
+ const state = resolveState(sources, session);
162
+ // What would make `up` refuse. A dry run prints the plan anyway; a real run stops first.
163
+ const refusals = [];
164
+ const caller = callerOf(io);
165
+ if (!isOwner(caller)) {
166
+ refusals.push(`only the owner runs \`up\`, from a terminal outside herdr; this call is ${describeCaller(caller)}`);
167
+ }
168
+ const differences = approvalDifferences(team, root, sources.home);
169
+ if (differences === null)
170
+ refusals.push('the file was never approved on this machine: run `team approve`');
171
+ else if (differences.length) {
172
+ refusals.push(`the file is not the approved one (${differences.join('; ')}): run \`team approve\``);
173
+ }
174
+ // The budget readings the gate refuses on are the approved ones: an unapproved lower reserve
175
+ // unblocks nothing, not even the seat a dry run would plan.
176
+ const budgets = budgetsInForce(team, root, sources.home);
177
+ if (sources.doctor) {
178
+ const findings = doctorFindings(team, root, dir, session, sources.doctor, loaded.warnings);
179
+ for (const finding of findings)
180
+ if (blocksLaunch(finding))
181
+ refusals.push(finding.text);
182
+ }
183
+ const samples = [];
184
+ const readAt = () => (sources.now?.() ?? new Date()).getTime();
185
+ const crossed = (machine) => launchLimit(machine, team.machine, samples, readAt());
186
+ const machine = sources.machine?.(root);
187
+ if (machine) {
188
+ const problem = crossed(machine);
189
+ if (problem)
190
+ refusals.push(problem);
191
+ }
192
+ if (state === null)
193
+ refusals.push("herdr doesn't answer");
194
+ if (state === 'stopped') {
195
+ refusals.push(`session ${session} is stopped; clear it with \`herdr session delete ${session}\``);
196
+ }
197
+ const agents = state === 'running' ? sources.agents(session) : [];
198
+ if (state === 'running') {
199
+ const recorded = readState(dir).sessions[session]?.seats ?? {};
200
+ if (agents === null)
201
+ refusals.push(`session ${session} runs, and its agents can't be read`);
202
+ else {
203
+ const unknown = agents.filter((agent) => !agent.name || !Object.hasOwn(recorded, agent.name));
204
+ if (unknown.length) {
205
+ refusals.push(`session ${session} has ${unknown.length} agent${unknown.length === 1 ? '' : 's'} this file's state doesn't record: \`up\` never touches a running team`);
206
+ }
207
+ }
208
+ }
209
+ const recorded = readState(dir).sessions[session];
210
+ const workspaces = sources.workspaces?.(session) ?? null;
211
+ const readings = loadReadings(dir, session);
212
+ const seats = [];
213
+ const refused = new Set();
214
+ for (const seat of team.seats) {
215
+ const planned = seatPlan(team, seat, recorded?.seats[seat.name], agents ?? [], workspaces, state === 'running');
216
+ // A stopped seat, one without a profile, and one already ready are left out of the budget.
217
+ // A seat resumed into a live workspace starts nowhere new, but its reading is still said.
218
+ if (planned.stopped || !profileFor(seat.cli) || planned.stage === 'ready') {
219
+ seats.push(planned);
220
+ continue;
221
+ }
222
+ const placed = planned.stage === undefined || !planned.pane;
223
+ if (!placed) {
224
+ const budget = seatBudget(budgets, readings, seat, readAt());
225
+ seats.push({ ...planned, ...(budget.kind === 'clear' ? {} : { budget }) });
226
+ continue;
227
+ }
228
+ const start = seatStart(team, seat, root);
229
+ if ('problem' in start) {
230
+ if (!start.once || !refused.has(start.problem)) {
231
+ refused.add(start.problem);
232
+ refusals.push(start.problem);
233
+ }
234
+ continue;
235
+ }
236
+ const budget = seatBudget(budgets, readings, seat, readAt());
237
+ seats.push({
238
+ ...planned,
239
+ cwd: start.cwd,
240
+ ...(start.lobby ? { lobby: true } : {}),
241
+ ...(budget.kind === 'clear' ? {} : { budget }),
242
+ });
243
+ }
244
+ const watch = recorded?.watch;
245
+ const plan = upPlan({
246
+ root,
247
+ session,
248
+ // A stopped session is not started. The refusal above names the delete command.
249
+ sessionRunning: state === 'running' || state === 'stopped',
250
+ seats,
251
+ watchAlive: Boolean(watch && sources.alive?.(watch.pid)),
252
+ watchLine: (sources.watchCommand ?? watchCommand)(session),
253
+ });
254
+ if (dry) {
255
+ for (const refusal of refusals)
256
+ io.stdout(`! up would refuse: ${refusal}\n`);
257
+ io.stdout(formatPlan(plan));
258
+ return 0;
259
+ }
260
+ if (refusals.length) {
261
+ for (const refusal of refusals)
262
+ io.stderr(`team up: ${refusal}\n`);
263
+ return 1;
264
+ }
265
+ const launch = sources.launch;
266
+ if (!launch) {
267
+ io.stderr('team up: this call has no way to reach herdr\n');
268
+ return 1;
269
+ }
270
+ const ceilings = readApproval(storePath(team.project, root, sources.home))?.approval.ceilings ?? null;
271
+ const running = [];
272
+ if (agents) {
273
+ const byName = new Map(team.seats.map((seat) => [seat.name, seat]));
274
+ for (const agent of agents) {
275
+ if (!agent.name)
276
+ continue;
277
+ const seat = byName.get(agent.name);
278
+ const known = recorded?.seats[agent.name];
279
+ if (!seat && !known)
280
+ continue;
281
+ const like = known?.temporary?.like;
282
+ const vendor = seat?.vendor ?? (like ? byName.get(like)?.vendor : undefined) ?? 'unknown';
283
+ running.push({ name: agent.name, vendor, temporary: Boolean(known?.temporary) });
284
+ }
285
+ }
286
+ const now = () => sources.now?.() ?? launch.now();
287
+ const host = {
288
+ startServer: launch.startServer,
289
+ sessionUp: launch.sessionUp,
290
+ makeDir(path) {
291
+ try {
292
+ mkdirSync(path, { recursive: true });
293
+ return true;
294
+ }
295
+ catch {
296
+ return false;
297
+ }
298
+ },
299
+ createWorkspace: launch.createWorkspace,
300
+ paneRun: launch.paneRun,
301
+ typeLine: () => false,
302
+ deliverRules: (session, pane, cli, text, seconds) => deliverRules(cli, text, seconds, {
303
+ screen: () => launch.paneText(session, pane) ?? undefined,
304
+ status: () => launch.agentStatus?.(session, pane) ?? null,
305
+ type: (value) => launch.typeText?.(session, pane, value) ?? false,
306
+ enter: () => launch.pressEnter?.(session, pane) ?? false,
307
+ now: () => now().getTime(),
308
+ sleep: sources.sleep ?? launch.sleep,
309
+ }),
310
+ renameAgent: launch.renameAgent,
311
+ closeWorkspace: launch.closeWorkspace,
312
+ stopSession: () => false,
313
+ kill: () => false,
314
+ agentPanes: launch.agentPanes,
315
+ classify: (name, pane, cli) => readScreen(cli, launch.paneText(name, pane) ?? undefined).kind,
316
+ sleep: sources.sleep ?? launch.sleep,
317
+ now: () => now().getTime(),
318
+ allow(name) {
319
+ if (sources.machine) {
320
+ const problem = crossed(sources.machine(root));
321
+ if (problem)
322
+ return problem;
323
+ }
324
+ const seat = team.seats.find((item) => item.name === name);
325
+ if (!seat || !ceilings)
326
+ return null;
327
+ return overCeiling(ceilings, running, seat);
328
+ },
329
+ record(name, patch) {
330
+ updateState(dir, (file) => {
331
+ const current = (file.sessions[session] ??= emptySession());
332
+ const prior = current.seats[name] ?? { stage: patch.stage };
333
+ current.seats[name] = { ...prior, ...patch };
334
+ });
335
+ },
336
+ running(name) {
337
+ if (running.some((item) => item.name === name))
338
+ return;
339
+ const seat = team.seats.find((item) => item.name === name);
340
+ if (seat)
341
+ running.push({ name, vendor: seat.vendor, temporary: false });
342
+ },
343
+ drop(name) {
344
+ updateState(dir, (file) => {
345
+ const seats = file.sessions[session]?.seats;
346
+ if (seats)
347
+ delete seats[name];
348
+ });
349
+ },
350
+ say: (line) => io.stdout(line),
351
+ log: (who, what) => logLine(dir, 'up', describeCaller(caller), `${who}: ${what}`, now()),
352
+ };
353
+ const report = await executePlan(plan, session, host);
354
+ const afterwards = readState(dir).sessions[session]?.seats ?? {};
355
+ const pending = team.seats.filter((seat) => {
356
+ if (seat.stopped || !profileFor(seat.cli))
357
+ return false;
358
+ return afterwards[seat.name]?.stage !== 'ready';
359
+ });
360
+ return pending.length || report.serverFailed || report.watchFailed ? 1 : 0;
361
+ }
@@ -0,0 +1,35 @@
1
+ import { type CheckOutcome } from '../budgets/run.ts';
2
+ import type { TeamFile } from '../file/types.ts';
3
+ import type { Command, Io } from '../io.ts';
4
+ import type { Live } from '../status/compare.ts';
5
+ import type { Machine } from '../watch/machine.ts';
6
+ import { type EndView } from '../watch/end.ts';
7
+ import type { DownSeat } from '../launch/plan.ts';
8
+ export type WatchSources = {
9
+ live(session: string, team: TeamFile): Live | null;
10
+ machine(root: string): Machine;
11
+ approval(team: TeamFile, root: string): string[] | null;
12
+ watchInForce(team: TeamFile, root: string): TeamFile['watch'];
13
+ budgetsInForce(team: TeamFile, root: string): TeamFile['budgets'];
14
+ readChecks(team: TeamFile, root: string, now: number): CheckOutcome[];
15
+ screen(pane: string, session: string): string | null;
16
+ status(pane: string, session: string): string | null;
17
+ typeText(pane: string, text: string, session: string): boolean;
18
+ pressEnter(pane: string, session: string): boolean;
19
+ notify(text: string): void;
20
+ now(): Date;
21
+ wait(seconds: number): Promise<boolean>;
22
+ alive(pid: number): boolean;
23
+ pid: number;
24
+ /** How an end is read. The real watch uses git and the filesystem. */
25
+ readEnd?(root: string, until: string, base: string | null, ownBefore: boolean): EndView;
26
+ /** Stops one free seat. The real watch uses the same steps as `remove`. */
27
+ stopSeat?(session: string, seat: DownSeat): Promise<boolean>;
28
+ /** Removes one merged worktree. The real watch calls `worktree remove`. */
29
+ removeWorktree?(task: string): number;
30
+ };
31
+ export declare const realWatchSources: WatchSources;
32
+ export declare const watch: Command;
33
+ export default watch;
34
+ export declare const USAGE = "Usage: team watch [--session <name>] [--file <path>] [--no-nudge] [--no-notify]\n";
35
+ export declare function runWatch(argv: string[], io: Io, sources: WatchSources): Promise<number>;