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,302 @@
1
+ import { readArgs } from "../args.js";
2
+ import { budgetsInForce, watchInForce } from "../approve/approval.js";
3
+ import { saveReadings } from "../budgets/readings.js";
4
+ import { runChecks } from "../budgets/run.js";
5
+ import { currentTeam } from "../file/current.js";
6
+ import { homedir } from 'node:os';
7
+ import { agentStatus, paneRead, pressEnter, typeText } from "../herdr.js";
8
+ import { logLine } from "../log.js";
9
+ import { emptySession, readState, updateState } from "../state.js";
10
+ import { readMachine } from "../watch/machine.js";
11
+ import { notify } from "../watch/notify.js";
12
+ import { newMemory, pass } from "../watch/pass.js";
13
+ import { readScreen } from "../watch/screen.js";
14
+ import { judgeTemporary, judgeWorktree } from "../watch/close.js";
15
+ import { readEnd } from "../watch/end.js";
16
+ import { stopRunning, realSources as removeSources } from "./remove.js";
17
+ import { removeWorktree } from "./worktree.js";
18
+ import { stateOf } from "./down.js";
19
+ import { realSources } from "./status.js";
20
+ function waitOrStop(seconds) {
21
+ return new Promise((done) => {
22
+ const stop = () => {
23
+ clearTimeout(timer);
24
+ done(false);
25
+ };
26
+ const timer = setTimeout(() => {
27
+ process.off('SIGINT', stop);
28
+ process.off('SIGTERM', stop);
29
+ done(true);
30
+ }, seconds * 1000);
31
+ process.once('SIGINT', stop);
32
+ process.once('SIGTERM', stop);
33
+ });
34
+ }
35
+ export const realWatchSources = {
36
+ live: realSources.live,
37
+ machine: readMachine,
38
+ approval: realSources.approval,
39
+ watchInForce: (team, root) => watchInForce(team, root),
40
+ budgetsInForce: (team, root) => budgetsInForce(team, root),
41
+ readChecks: (team, root, now) => runChecks(team, root, now),
42
+ screen: (pane, session) => paneRead(pane, 14, session),
43
+ status: agentStatus,
44
+ typeText,
45
+ pressEnter,
46
+ notify,
47
+ now: () => new Date(),
48
+ wait: waitOrStop,
49
+ alive(pid) {
50
+ try {
51
+ process.kill(pid, 0);
52
+ return true;
53
+ }
54
+ catch (error) {
55
+ return error.code === 'EPERM';
56
+ }
57
+ },
58
+ pid: process.pid,
59
+ };
60
+ export const watch = (argv, io) => runWatch(argv, io, realWatchSources);
61
+ export default watch;
62
+ export const USAGE = 'Usage: team watch [--session <name>] [--file <path>] [--no-nudge] [--no-notify]\n';
63
+ export async function runWatch(argv, io, sources) {
64
+ const args = readArgs(argv, ['session', 'file'], ['no-nudge', 'no-notify']);
65
+ if (args.error || args.rest.length) {
66
+ io.stderr(`team watch: ${args.error ?? `unexpected "${args.rest[0]}"`}\n${USAGE}`);
67
+ return 2;
68
+ }
69
+ const first = currentTeam(io.cwd, args.values.file, sources.now());
70
+ if (!first.ok) {
71
+ for (const problem of first.errors)
72
+ io.stderr(`team watch: ${problem.line ? `team.yaml line ${problem.line}: ` : ''}${problem.message}\n`);
73
+ return 2;
74
+ }
75
+ const session = args.values.session ?? first.team.session;
76
+ const { dir } = first;
77
+ const other = readState(dir).sessions[session]?.watch;
78
+ if (other && other.pid !== sources.pid && sources.alive(other.pid)) {
79
+ io.stderr(`team watch: a watch already runs for the session "${session}" (pid ${other.pid})\n`);
80
+ return 1;
81
+ }
82
+ const say = (text, desktop) => {
83
+ io.stdout(`${sources.now().toISOString()} ${text}\n`);
84
+ logLine(dir, 'watch', 'watch', text, sources.now());
85
+ if (desktop && !args.flags.has('no-notify'))
86
+ sources.notify(text);
87
+ };
88
+ const beat = () => updateState(dir, (state) => {
89
+ const record = (state.sessions[session] ??= emptySession());
90
+ record.watch = { pid: sources.pid, heartbeat: sources.now().toISOString() };
91
+ });
92
+ const memory = newMemory();
93
+ const told = new Set();
94
+ let outcomes = [];
95
+ let checksAt = null;
96
+ let notice;
97
+ let silent = false;
98
+ say(`watching the session "${session}" every ${sources.watchInForce(first.team, first.root).interval}s${args.flags.has('no-nudge') ? ', without nudges' : ''}`, false);
99
+ try {
100
+ for (;;) {
101
+ // The file is read again on every pass, so a seat parked or stopped since is seen.
102
+ const current = currentTeam(io.cwd, args.values.file, sources.now());
103
+ const team = current.ok ? current.team : first.team;
104
+ const root = current.ok ? current.root : first.root;
105
+ 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
+ if (problem !== notice) {
107
+ notice = problem;
108
+ if (problem)
109
+ say(problem, true);
110
+ }
111
+ // The values in force, read with the file: until the owner approves an edit to `watch`,
112
+ // the watch keeps running with what was approved, or with the defaults.
113
+ const inForce = sources.watchInForce(team, root);
114
+ // The budget values in force, read with the file like the watch's: an unapproved edit to the
115
+ // marks, the accounts or the cadence silences nothing here until the owner approves it.
116
+ const budget = sources.budgetsInForce(team, root);
117
+ const live = sources.live(session, team);
118
+ if (!live) {
119
+ if (!silent)
120
+ say('herdr doesn\'t answer; the watch keeps trying', true);
121
+ silent = true;
122
+ }
123
+ else {
124
+ silent = false;
125
+ noteWorked(dir, session, live);
126
+ const state = readState(dir).sessions[session] ?? emptySession();
127
+ const now = sources.now().getTime();
128
+ // The check commands run here, outside the pass, at most every `check_every` (§ 5): the
129
+ // pass reads what they read, and nothing else. Only their state is ever said — a
130
+ // contract failure or a timeout says `unreadable`, never a line of what they printed.
131
+ if (checksAt === null || now - checksAt >= budget.checkEvery * 1000) {
132
+ checksAt = now;
133
+ outcomes = sources.readChecks(team, root, now);
134
+ for (const outcome of outcomes) {
135
+ const key = `check:${outcome.account}`;
136
+ if (outcome.state === 'read') {
137
+ told.delete(key);
138
+ continue;
139
+ }
140
+ if (told.has(key))
141
+ continue;
142
+ told.add(key);
143
+ say(outcome.state === 'unreadable'
144
+ ? `${outcome.account}: its check is unreadable`
145
+ : `the check for ${outcome.account} is unapproved; that account reads unknown`, false);
146
+ }
147
+ }
148
+ const result = pass(team, state, live, sources.machine(root), now, memory, sources.approval(team, root), inForce, outcomes, budget);
149
+ for (const report of result.reports)
150
+ say(report.text, true);
151
+ saveReadings(dir, session, result.readings, now);
152
+ if (result.nudge) {
153
+ if (args.flags.has('no-nudge'))
154
+ say(`nudge not typed (--no-nudge): ${result.nudge.text}`, false);
155
+ else
156
+ deliver(result.nudge, team, session, sources, memory, say);
157
+ }
158
+ if (result.fallback)
159
+ say(result.fallback, true);
160
+ await closeEnded({ team, root, dir, session, live, sources, say, io, told });
161
+ }
162
+ beat();
163
+ if (!(await sources.wait(inForce.interval)))
164
+ break;
165
+ }
166
+ }
167
+ finally {
168
+ updateState(dir, (state) => {
169
+ const record = state.sessions[session];
170
+ if (record?.watch?.pid === sources.pid)
171
+ delete record.watch;
172
+ });
173
+ say(`the watch of "${session}" stopped`, true);
174
+ }
175
+ return 0;
176
+ }
177
+ // Types the nudge, after reading the operator's screen once more: the pass saw it free, and a
178
+ // prompt may have appeared since. Anything but an empty idle prompt keeps the nudge pending.
179
+ function deliver(nudge, team, session, sources, memory, say) {
180
+ const cli = team.seats.find((seat) => seat.name === team.operator)?.cli ?? '';
181
+ const look = () => readScreen(cli, sources.screen(nudge.pane, session) ?? undefined).kind;
182
+ const status = sources.status(nudge.pane, session);
183
+ const keep = () => {
184
+ // The nudge's text carries no report, so the reports it was raised for go back to pending:
185
+ // the fallback notification and the next passes need them.
186
+ memory.pending.push(...nudge.pending);
187
+ memory.pendingSince ??= sources.now().getTime();
188
+ };
189
+ if ((status !== 'idle' && status !== 'done') || look() !== 'idle' || !sources.typeText(nudge.pane, nudge.text, session)) {
190
+ keep();
191
+ return;
192
+ }
193
+ // The text is in the box. A dialog that opened meanwhile must not get the Enter: the text
194
+ // then stays unsent, which the next passes report, and the nudge is kept.
195
+ const after = look();
196
+ if (after !== 'idle' && after !== 'unsent') {
197
+ say('a nudge was typed and not sent: the operator\'s screen changed before the Enter', true);
198
+ keep();
199
+ return;
200
+ }
201
+ if (sources.pressEnter(nudge.pane, session))
202
+ say(`nudged the operator: ${nudge.text}`, false);
203
+ else
204
+ keep();
205
+ }
206
+ function noteWorked(dir, session, live) {
207
+ const recorded = readState(dir).sessions[session];
208
+ if (!recorded)
209
+ return;
210
+ const due = live.agents.some((agent) => {
211
+ const seat = agent.name ? recorded.seats[agent.name] : undefined;
212
+ return seat?.temporary && seat.stage === 'ready' && !seat.worked && agent.status === 'working';
213
+ });
214
+ if (!due)
215
+ return;
216
+ updateState(dir, (file) => {
217
+ const seats = (file.sessions[session] ??= emptySession()).seats;
218
+ for (const agent of live.agents) {
219
+ const seat = agent.name ? seats[agent.name] : undefined;
220
+ if (seat?.temporary && seat.stage === 'ready' && agent.status === 'working')
221
+ seat.worked = true;
222
+ }
223
+ });
224
+ }
225
+ function sayOnce(told, key, text, say) {
226
+ if (!text || told.has(key))
227
+ return;
228
+ told.add(key);
229
+ say(text, true);
230
+ }
231
+ // Closes a temporary seat whose end holds, and removes an on-merge worktree whose branch is merged.
232
+ // Anything that is not free, or not proved, is left as it is.
233
+ async function closeEnded(input) {
234
+ const { team, root, dir, session, live, sources, say, io, told } = input;
235
+ const endOf = sources.readEnd ?? readEnd;
236
+ const state = readState(dir).sessions[session] ?? emptySession();
237
+ for (const [name, recorded] of Object.entries(state.seats)) {
238
+ const temporary = recorded.temporary;
239
+ if (!temporary)
240
+ continue;
241
+ const task = temporary.task;
242
+ const work = task ? state.worktrees[task] : undefined;
243
+ const ownBefore = work ? work.own_commits === true : temporary.own_commits === true;
244
+ const end = endOf(root, temporary.until, team.workspace.base, ownBefore);
245
+ const agent = live.agents.find((item) => item.name === name);
246
+ const cli = team.seats.find((seat) => seat.name === temporary.like)?.cli ?? '';
247
+ const free = agent
248
+ ? stateOf(sources.status(agent.pane, session) ?? agent.status, readScreen(cli, sources.screen(agent.pane, session) ?? undefined)) === 'free'
249
+ : false;
250
+ const decision = judgeTemporary({ worked: recorded.worked === true, free, ownBefore, end });
251
+ if (decision.ownCommits) {
252
+ updateState(dir, (file) => {
253
+ const current = (file.sessions[session] ??= emptySession());
254
+ if (work && task) {
255
+ const tree = current.worktrees[task];
256
+ if (tree)
257
+ tree.own_commits = true;
258
+ }
259
+ else {
260
+ const seat = current.seats[name];
261
+ if (seat?.temporary)
262
+ seat.temporary.own_commits = true;
263
+ }
264
+ });
265
+ }
266
+ sayOnce(told, `end:${name}`, decision.report, say);
267
+ if (!decision.close || !agent)
268
+ continue;
269
+ const seat = { name, cli, pane: agent.pane, workspace: agent.workspace, state: 'free' };
270
+ const stopped = sources.stopSeat
271
+ ? await sources.stopSeat(session, seat)
272
+ : await stopRunning({
273
+ io, dir, session, seat, abandon: false, sources: removeSources, logCommand: 'watch', caller: 'watch',
274
+ });
275
+ if (stopped)
276
+ say(`closed ${name}; its end ${temporary.until} holds`, false);
277
+ }
278
+ const again = readState(dir);
279
+ const here = again.sessions[session] ?? emptySession();
280
+ for (const [task, work] of Object.entries(here.worktrees)) {
281
+ const occupied = Object.values(again.sessions).some((recorded) => Object.values(recorded.seats).some((seat) => seat.temporary?.task === task));
282
+ const end = endOf(root, `merged:${work.branch}`, team.workspace.base, work.own_commits === true);
283
+ const decision = judgeWorktree({
284
+ policy: team.workspace.remove, occupied, ownBefore: work.own_commits === true, end,
285
+ });
286
+ if (decision.ownCommits) {
287
+ updateState(dir, (file) => {
288
+ const tree = file.sessions[session]?.worktrees[task];
289
+ if (tree)
290
+ tree.own_commits = true;
291
+ });
292
+ }
293
+ sayOnce(told, `worktree:${task}`, decision.report, say);
294
+ if (!decision.close)
295
+ continue;
296
+ const code = sources.removeWorktree
297
+ ? sources.removeWorktree(task)
298
+ : removeWorktree(io, { home: homedir(), now: sources.now }, team, root, dir, session, task, 'watch');
299
+ if (code !== 0)
300
+ say(`worktree ${task} was not removed`, true);
301
+ }
302
+ }
@@ -0,0 +1,12 @@
1
+ import type { TeamFile } from '../file/types.ts';
2
+ import type { Command, Io } from '../io.ts';
3
+ export type WorktreeSources = {
4
+ home: string;
5
+ now(): Date;
6
+ };
7
+ export declare const realSources: WorktreeSources;
8
+ export declare const USAGE = "Usage: team worktree new <task> [--kind <kind>] [--seat <name>] [--session <name>] [--file <path>]\n team worktree remove <task> [--session <name>] [--file <path>]\nIgnored files in the worktree are deleted with it.\n";
9
+ export declare const worktree: Command;
10
+ export default worktree;
11
+ export declare function runWorktree(argv: string[], io: Io, sources?: WorktreeSources): Promise<number>;
12
+ export declare function removeWorktree(io: Io, sources: WorktreeSources, team: TeamFile, root: string, dir: string, session: string, task: string, who: string): number;