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,138 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
3
+ import { basename, dirname, join } from 'node:path';
4
+ import { readArgs } from "../args.js";
5
+ import { callerOf, describeCaller, isOwner } from "../caller.js";
6
+ import { TEAM_FILE, findRoot } from "../file/load.js";
7
+ import { validateTeamFile } from "../file/validate.js";
8
+ import { logLine } from "../log.js";
9
+ import { LOCK_FILE, LOG_FILE, STATE_FILE, writeAtomic } from "../state.js";
10
+ import { approvedCopy } from "../store/store.js";
11
+ // What git must never pick up: the file and the runtime files beside it.
12
+ export const EXCLUDED = [TEAM_FILE, `.agents/${STATE_FILE}`, `.agents/${LOG_FILE}*`, `.agents/${LOCK_FILE}`];
13
+ export const PRIVACY = `The team file is private to this clone: it is listed in .git/info/exclude, never in .gitignore,
14
+ so git doesn't see it and a fresh clone doesn't carry it. A collaborator writes their own, or
15
+ receives one and approves it themselves.`;
16
+ function git(root, ...args) {
17
+ try {
18
+ return execFileSync('git', ['-C', root, ...args], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
19
+ }
20
+ catch {
21
+ return null;
22
+ }
23
+ }
24
+ export function skeleton(project, head) {
25
+ return `# The team of ${project}. Private to this clone: see .git/info/exclude.
26
+ # Nothing here runs until the owner has read it and run \`team approve\`.
27
+ format: 1
28
+ project: ${project}
29
+ # visibility: public # public | private: whether forbidden_public applies
30
+ # session: ${project} # the herdr session; never "default"
31
+ coordinator: coordinator # the seat that dispatches work
32
+ operator: coordinator # the seat the watch reports to
33
+
34
+ # identity:
35
+ # signature:
36
+ # template: "Agent: {display} · {role}"
37
+ # commits:
38
+ # position: trailer # last-line | trailer | anywhere
39
+ # exempt: [merge]
40
+ # pull_requests:
41
+ # position: last-line
42
+ # template: "**Agent:** {display} · {role}"
43
+ # humans: [] # commit authors who don't sign
44
+ ${head ? `# since: ${head} # check reads no commit reachable from this one\n` : ''}# forbidden: [] # added to the defaults
45
+ # forbidden_public: []
46
+
47
+ # rules: # added to every seat's rules at launch
48
+ # - Run the tests your change touches.
49
+
50
+ # trust: # applied only by the owner, with \`team trust\`
51
+ # - .
52
+ # - ../worktrees/${project}/*
53
+
54
+ workspace:
55
+ mode: shared # worktree | shared
56
+ # path: ../worktrees/{repo}/{task}
57
+ # base: main
58
+
59
+ seats:
60
+ - role: coordinator
61
+ name: coordinator
62
+ cli: claude-code # claude-code | codex | cursor | grok | antigravity
63
+ vendor: anthropic
64
+ model: Claude Opus # the model's name without its version
65
+ version: "0" # the release number alone, quoted
66
+ launch: claude # the command and its model options; no approval flags
67
+ `;
68
+ }
69
+ // Adds the lines git must exclude, once each, to the common directory's info/exclude.
70
+ export function exclude(root) {
71
+ const common = git(root, 'rev-parse', '--path-format=absolute', '--git-common-dir');
72
+ if (!common)
73
+ throw new Error('git doesn\'t answer for this folder');
74
+ const path = join(common, 'info', 'exclude');
75
+ mkdirSync(dirname(path), { recursive: true });
76
+ const current = existsSync(path) ? readFileSync(path, 'utf8') : '';
77
+ const present = new Set(current.split('\n').map((line) => line.trim()));
78
+ const missing = EXCLUDED.filter((line) => !present.has(line));
79
+ if (missing.length) {
80
+ writeAtomic(path, `${current}${current && !current.endsWith('\n') ? '\n' : ''}${missing.join('\n')}\n`);
81
+ }
82
+ return missing;
83
+ }
84
+ export const init = (argv, io) => runInit(argv, io);
85
+ export default init;
86
+ export const USAGE = 'Usage: team init [--restore]\n';
87
+ // `home` is where the user-level store is looked for; tests hand in a temporary one.
88
+ export async function runInit(argv, io, home) {
89
+ const args = readArgs(argv, [], ['restore']);
90
+ if (args.error || args.rest.length) {
91
+ io.stderr(`team init: ${args.error ?? `unexpected "${args.rest[0]}"`}\n${USAGE}`);
92
+ return 2;
93
+ }
94
+ const caller = callerOf(io);
95
+ if (!isOwner(caller)) {
96
+ io.stderr(`team init: only the owner runs init, from a terminal outside herdr; this call is ${describeCaller(caller)}\n`);
97
+ return 1;
98
+ }
99
+ const root = findRoot(io.cwd);
100
+ if (!root) {
101
+ io.stderr('team init: not inside a git repository\n');
102
+ return 2;
103
+ }
104
+ const path = join(root, TEAM_FILE);
105
+ if (git(root, 'ls-files', '--error-unmatch', TEAM_FILE) !== null) {
106
+ io.stderr(`team init: ${TEAM_FILE} is tracked by git, and a team file is private. Untrack it, keeping the file:\n git rm --cached ${TEAM_FILE}\nthen run team init again. History is not rewritten.\n`);
107
+ return 1;
108
+ }
109
+ if (existsSync(path)) {
110
+ exclude(root);
111
+ io.stderr(`team init: ${TEAM_FILE} exists already; it is left as it is. Its lines in .git/info/exclude were checked, and added where missing.\n`);
112
+ return 1;
113
+ }
114
+ let text;
115
+ if (args.flags.has('restore')) {
116
+ const copy = home === undefined ? approvedCopy(root) : approvedCopy(root, home);
117
+ if (copy === null) {
118
+ io.stderr('team init: nothing to restore: no team file was approved for this folder on this machine. Run team init for a skeleton.\n');
119
+ return 1;
120
+ }
121
+ text = copy;
122
+ }
123
+ else {
124
+ text = skeleton(basename(root), git(root, 'rev-parse', 'HEAD'));
125
+ const check = validateTeamFile(text);
126
+ if (!check.ok)
127
+ throw new Error(`the skeleton doesn't validate: ${check.errors[0]?.message}`);
128
+ }
129
+ mkdirSync(dirname(path), { recursive: true });
130
+ const added = exclude(root);
131
+ writeFileSync(path, text, { flag: 'wx' });
132
+ const what = args.flags.has('restore') ? 'restored the approved copy' : 'wrote a skeleton';
133
+ logLine(dirname(path), 'init', describeCaller(caller), `${what} as ${TEAM_FILE}; excluded ${added.length} path(s)`);
134
+ io.stdout(args.flags.has('restore')
135
+ ? `Restored ${TEAM_FILE} from the copy you last approved on this machine.\n\n${PRIVACY}\n`
136
+ : `Wrote ${TEAM_FILE}: a skeleton with one seat. Edit it, then run team approve.\n\n${PRIVACY}\n`);
137
+ return 0;
138
+ }
@@ -0,0 +1,23 @@
1
+ import type { Command, Io } from '../io.ts';
2
+ import { type DownSeat } from '../launch/plan.ts';
3
+ import { type DownSources } from './down.ts';
4
+ export type RemoveSources = DownSources & {
5
+ /** Foreground process names in the pane, or null when the pane can't be read. */
6
+ foreground(session: string, pane: string): string[] | null;
7
+ };
8
+ export declare const realSources: RemoveSources;
9
+ export declare const USAGE = "Usage: team remove <name> [--keep] [--abandon] [--session <name>] [--file <path>]\n";
10
+ export declare const remove: Command;
11
+ export default remove;
12
+ export declare function runRemove(argv: string[], io: Io, sources?: RemoveSources): Promise<number>;
13
+ /** Types the exit, waits for the shell, and closes the workspace. False leaves the seat as it is. */
14
+ export declare function stopRunning(input: {
15
+ io: Io;
16
+ dir: string;
17
+ session: string;
18
+ seat: DownSeat;
19
+ abandon: boolean;
20
+ sources: RemoveSources;
21
+ logCommand: string;
22
+ caller: string;
23
+ }): Promise<boolean>;
@@ -0,0 +1,235 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { dirname } from 'node:path';
3
+ import { readArgs } from "../args.js";
4
+ import { callerOf, describeCaller, isOwner, mayChangeTeam } from "../caller.js";
5
+ import { loadTeamFile } from "../file/load.js";
6
+ import { markStopped, takeOut } from "../file/lines.js";
7
+ import { validateTeamFile } from "../file/validate.js";
8
+ import { writeTeamFile } from "../file/write.js";
9
+ import { paneForeground } from "../herdr.js";
10
+ import { executePlan } from "../launch/execute.js";
11
+ import { downPlan } from "../launch/plan.js";
12
+ import { logLine } from "../log.js";
13
+ import { profileFor } from "../profiles/index.js";
14
+ import { emptySession, readState, updateState, withLock } from "../state.js";
15
+ import { paneStillRunning, realSources as downSources, stateOf } from "./down.js";
16
+ function aim(session) {
17
+ return session === 'default' ? undefined : session;
18
+ }
19
+ export const realSources = {
20
+ ...downSources,
21
+ foreground: (session, pane) => paneForeground(pane, aim(session)),
22
+ };
23
+ export const USAGE = 'Usage: team remove <name> [--keep] [--abandon] [--session <name>] [--file <path>]\n';
24
+ const LEFT = {
25
+ working: 'is working; left as it is',
26
+ blocked: 'is blocked at a prompt, which team never answers',
27
+ unknown: 'shows a screen the profile does not recognise; left as it is',
28
+ unsent: 'holds unsent text in its input box; left as it is',
29
+ };
30
+ export const remove = (argv, io) => runRemove(argv, io, realSources);
31
+ export default remove;
32
+ /** The file edit `remove` would write, or null when that edit is valid or changes nothing. */
33
+ function editProblems(path, name, keep) {
34
+ const text = readFileSync(path, 'utf8');
35
+ const next = keep ? markStopped(text, name) : takeOut(text, name);
36
+ if (next === text)
37
+ return null;
38
+ const parsed = validateTeamFile(next);
39
+ return parsed.ok ? null : parsed.errors;
40
+ }
41
+ export async function runRemove(argv, io, sources = realSources) {
42
+ const args = readArgs(argv, ['session', 'file'], ['keep', 'abandon']);
43
+ if (args.error || args.rest.length !== 1) {
44
+ io.stderr(`team remove: ${args.error ?? (args.rest.length ? `unexpected "${args.rest[0]}"` : 'a seat name is required')}\n${USAGE}`);
45
+ return 2;
46
+ }
47
+ const name = args.rest[0] ?? '';
48
+ const loaded = loadTeamFile(io.cwd, args.values.file ? { file: args.values.file } : {});
49
+ if (!loaded.ok) {
50
+ for (const problem of loaded.errors)
51
+ io.stderr(`team remove: ${problem.line ? `line ${problem.line}: ` : ''}${problem.message}\n`);
52
+ return 2;
53
+ }
54
+ const caller = callerOf(io);
55
+ if (args.values.file && !isOwner(caller)) {
56
+ io.stderr(`team remove: --file is the owner's, from a terminal outside herdr; this call is ${describeCaller(caller)}\n`);
57
+ return 1;
58
+ }
59
+ if (!mayChangeTeam(caller, loaded.team)) {
60
+ io.stderr(`team remove: only the owner, the coordinator or the operator runs it; this call is ${describeCaller(caller)}\n`);
61
+ return 1;
62
+ }
63
+ const { team, path } = loaded;
64
+ const session = args.values.session ?? team.session;
65
+ if (session === 'default') {
66
+ io.stderr('team remove: session can\'t be "default", herdr\'s own session\n');
67
+ return 1;
68
+ }
69
+ const abandon = args.flags.has('abandon');
70
+ if (abandon && caller.kind !== 'owner') {
71
+ io.stderr('team remove: only the owner abandons a seat, from a terminal outside herdr\n');
72
+ return 1;
73
+ }
74
+ if ((name === team.coordinator || name === team.operator) && caller.kind !== 'owner') {
75
+ io.stderr(`team remove: only the owner removes the coordinator's or the operator's seat; this call is ${describeCaller(caller)}\n`);
76
+ return 1;
77
+ }
78
+ const dir = dirname(path);
79
+ const recorded = readState(dir).sessions[session]?.seats[name];
80
+ const declared = team.seats.find((seat) => seat.name === name);
81
+ if (!declared && !recorded) {
82
+ io.stderr(`team remove: the team has no seat ${JSON.stringify(name)}\n`);
83
+ return 1;
84
+ }
85
+ const temporary = recorded?.temporary;
86
+ if (args.flags.has('keep') && temporary) {
87
+ io.stderr('team remove: a temporary seat is not in the file; there is nothing to keep\n');
88
+ return 1;
89
+ }
90
+ const live = sources.sessionRunning(session);
91
+ if (live === null) {
92
+ io.stderr('team remove: herdr doesn\'t answer; nothing was changed\n');
93
+ return 1;
94
+ }
95
+ const agents = live ? sources.agents(session) : [];
96
+ if (agents === null) {
97
+ io.stderr(`team remove: session ${session} runs, and its agents can't be read; nothing was changed\n`);
98
+ return 1;
99
+ }
100
+ const agent = agents.find((item) => item.name === name);
101
+ const cli = declared?.cli ?? team.seats.find((seat) => seat.name === temporary?.like)?.cli ?? '';
102
+ if (agent) {
103
+ const screen = sources.screen(session, agent.pane, cli);
104
+ const where = stateOf(agent.status, screen);
105
+ if (where !== 'free' && !abandon) {
106
+ io.stderr(`team remove: ${name} ${LEFT[where]}\n`);
107
+ return 1;
108
+ }
109
+ if (!profileFor(cli) && !abandon) {
110
+ io.stderr(`team remove: no launch profile for \`${cli}\`; left as it is\n`);
111
+ return 1;
112
+ }
113
+ }
114
+ // An edit that will not validate is refused before the seat is stopped. Stopping first would
115
+ // end the live session and then report that nothing was written.
116
+ if (!temporary) {
117
+ const ahead = editProblems(path, name, args.flags.has('keep'));
118
+ if (ahead) {
119
+ for (const problem of ahead) {
120
+ io.stderr(`team remove: ${problem.line ? `line ${problem.line}: ` : ''}${problem.message}\n`);
121
+ }
122
+ return 2;
123
+ }
124
+ }
125
+ if (agent) {
126
+ const screen = sources.screen(session, agent.pane, cli);
127
+ const where = stateOf(agent.status, screen);
128
+ const stopped = await stopRunning({
129
+ io, dir, session, sources, logCommand: 'remove', caller: describeCaller(caller),
130
+ seat: { name, cli, pane: agent.pane, workspace: agent.workspace, state: where === 'free' ? 'free' : where },
131
+ abandon: abandon && where !== 'free',
132
+ });
133
+ if (!stopped)
134
+ return 1;
135
+ }
136
+ if (!temporary) {
137
+ const refused = withLock(dir, () => {
138
+ const text = readFileSync(path, 'utf8');
139
+ const next = args.flags.has('keep') ? markStopped(text, name) : takeOut(text, name);
140
+ if (next === text)
141
+ return null;
142
+ const wrote = writeTeamFile(path, next);
143
+ return wrote.ok ? null : wrote.errors;
144
+ });
145
+ if (refused) {
146
+ for (const problem of refused) {
147
+ io.stderr(`team remove: ${problem.line ? `line ${problem.line}: ` : ''}${problem.message}\n`);
148
+ }
149
+ return 2;
150
+ }
151
+ }
152
+ if (!agent && recorded) {
153
+ updateState(dir, (file) => {
154
+ const seats = file.sessions[session]?.seats;
155
+ if (seats)
156
+ delete seats[name];
157
+ });
158
+ }
159
+ const who = describeCaller(caller);
160
+ const what = temporary ? `removed temporary ${name}` : args.flags.has('keep') ? `stopped ${name}` : `removed ${name}`;
161
+ logLine(dir, 'remove', who, what, sources.now());
162
+ io.stdout(`${what}\n`);
163
+ return 0;
164
+ }
165
+ /** Types the exit, waits for the shell, and closes the workspace. False leaves the seat as it is. */
166
+ export async function stopRunning(input) {
167
+ const launch = input.sources.launch;
168
+ if (!launch) {
169
+ input.io.stderr('team remove: this call has no way to reach herdr\n');
170
+ return false;
171
+ }
172
+ const { seat, session, sources } = input;
173
+ let stopped = false;
174
+ const steps = downPlan({
175
+ session,
176
+ seats: [seat],
177
+ extra: 1,
178
+ watchPid: null,
179
+ keep: [],
180
+ abandon: input.abandon,
181
+ }).filter((step) => step.kind !== 'skip');
182
+ const host = {
183
+ startServer: () => false,
184
+ sessionUp: () => true,
185
+ createWorkspace: () => null,
186
+ paneRun: () => false,
187
+ typeLine(sessionName, pane, text) {
188
+ const look = () => sources.screen(sessionName, pane, seat.cli).kind;
189
+ const resting = () => {
190
+ const status = sources.status(sessionName, pane);
191
+ return status === 'idle' || status === 'done';
192
+ };
193
+ if (!resting() || look() !== 'idle')
194
+ return false;
195
+ if (!launch.typeText(sessionName, pane, text))
196
+ return false;
197
+ const after = look();
198
+ if (!resting() || (after !== 'idle' && after !== 'unsent'))
199
+ return false;
200
+ return launch.pressEnter(sessionName, pane);
201
+ },
202
+ renameAgent: () => false,
203
+ closeWorkspace: launch.closeWorkspace,
204
+ stopSession: () => false,
205
+ kill: () => false,
206
+ agentPanes(sessionName) {
207
+ const listed = launch.agentPanes(sessionName);
208
+ if (!listed)
209
+ return null;
210
+ const names = profileFor(seat.cli)?.processNames;
211
+ if (!names)
212
+ return listed;
213
+ return listed.filter((pane) => paneStillRunning(sources.foreground(sessionName, pane), names));
214
+ },
215
+ classify: () => 'unknown',
216
+ sleep: sources.sleep ?? launch.sleep,
217
+ now: () => sources.now().getTime(),
218
+ allow: () => null,
219
+ record() { },
220
+ running() { },
221
+ drop(dropped) {
222
+ if (dropped !== seat.name)
223
+ return;
224
+ stopped = true;
225
+ updateState(input.dir, (file) => {
226
+ const seats = (file.sessions[session] ??= emptySession()).seats;
227
+ delete seats[dropped];
228
+ });
229
+ },
230
+ say: (line) => input.io.stdout(line),
231
+ log: (who, what) => logLine(input.dir, input.logCommand, input.caller, `${who}: ${what}`, sources.now()),
232
+ };
233
+ await executePlan(steps, session, host);
234
+ return stopped;
235
+ }
@@ -0,0 +1,41 @@
1
+ import { type BudgetRow } from '../budgets/table.ts';
2
+ import type { TeamFile } from '../file/types.ts';
3
+ import type { Command, Io } from '../io.ts';
4
+ import type { Difference, Live } from '../status/compare.ts';
5
+ export type StatusSources = {
6
+ live(session: string, team: TeamFile): Live | null;
7
+ branch(path: string): string | null;
8
+ approval(team: TeamFile, root: string): string[] | null;
9
+ watchInForce(team: TeamFile, root: string): TeamFile['watch'];
10
+ budgetsInForce(team: TeamFile, root: string): TeamFile['budgets'];
11
+ now(): Date;
12
+ };
13
+ export declare const realSources: StatusSources;
14
+ export declare const status: Command;
15
+ export default status;
16
+ /**
17
+ * The JSON shape printed by `team status --json`.
18
+ * Format 1.
19
+ */
20
+ export interface StatusJson {
21
+ format: 1;
22
+ project: string;
23
+ session: string;
24
+ rows: Array<{
25
+ name: string;
26
+ state: string;
27
+ model: string;
28
+ pane: string;
29
+ }>;
30
+ notes: string[];
31
+ differences: Array<{
32
+ what: string;
33
+ repair: string;
34
+ }>;
35
+ notice: string | null;
36
+ /** Present when the file names an account, or a reading is stored. */
37
+ budgets?: BudgetRow[];
38
+ }
39
+ export declare const USAGE = "Usage: team status [--session <name>] [--file <path>] [--json]\n";
40
+ export declare function runStatus(argv: string[], io: Io, sources: StatusSources): Promise<number>;
41
+ export declare function approvalDrift(differences: string[] | null): Difference[];
@@ -0,0 +1,147 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { relative, resolve } from 'node:path';
3
+ import { approvalDifferences, budgetsInForce, watchInForce } from "../approve/approval.js";
4
+ import { readArgs } from "../args.js";
5
+ import { budgetLine, budgetTable } from "../budgets/table.js";
6
+ import { recall } from "../budgets/readings.js";
7
+ import { currentTeam } from "../file/current.js";
8
+ import { agentList, paneRead, sessionRunning, workspaceList } from "../herdr.js";
9
+ import { emptySession, readState } from "../state.js";
10
+ import { compare } from "../status/compare.js";
11
+ export const realSources = {
12
+ live(session, team) {
13
+ const running = sessionRunning(session);
14
+ if (running === null)
15
+ return null;
16
+ if (!running)
17
+ return { running: false, agents: [], workspaces: [], screens: {} };
18
+ const agents = agentList(session);
19
+ const workspaces = workspaceList(session);
20
+ if (!agents || !workspaces)
21
+ return null;
22
+ const screens = {};
23
+ for (const agent of agents) {
24
+ if (!team.seats.some((seat) => seat.name === agent.name))
25
+ continue;
26
+ const screen = paneRead(agent.pane, 12, session);
27
+ if (screen !== null)
28
+ screens[agent.pane] = screen;
29
+ }
30
+ return { running: true, agents, workspaces, screens };
31
+ },
32
+ branch(path) {
33
+ try {
34
+ return execFileSync('git', ['-C', path, 'rev-parse', '--abbrev-ref', 'HEAD'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
35
+ }
36
+ catch {
37
+ return null;
38
+ }
39
+ },
40
+ approval: (team, root) => approvalDifferences(team, root),
41
+ watchInForce: (team, root) => watchInForce(team, root),
42
+ budgetsInForce: (team, root) => budgetsInForce(team, root),
43
+ now: () => new Date(),
44
+ };
45
+ export const status = (argv, io) => runStatus(argv, io, realSources);
46
+ export default status;
47
+ export const USAGE = 'Usage: team status [--session <name>] [--file <path>] [--json]\n';
48
+ export async function runStatus(argv, io, sources) {
49
+ const args = readArgs(argv, ['session', 'file'], ['json']);
50
+ if (args.error || args.rest.length) {
51
+ io.stderr(`team status: ${args.error ?? `unexpected "${args.rest[0]}"`}\n${USAGE}`);
52
+ return 2;
53
+ }
54
+ const current = currentTeam(io.cwd, args.values.file, sources.now());
55
+ if (!current.ok) {
56
+ printProblems(io, current.errors);
57
+ return 2;
58
+ }
59
+ const { team, root, dir } = current;
60
+ if (!args.flags.has('json') && current.notice)
61
+ io.stdout(`${current.notice}\n`);
62
+ for (const warning of current.warnings)
63
+ io.stderr(`team status: warning, line ${warning.line}: ${warning.message}\n`);
64
+ const session = args.values.session ?? team.session;
65
+ const live = sources.live(session, team);
66
+ if (!live) {
67
+ io.stderr('team status: herdr doesn\'t answer; is it installed and running?\n');
68
+ return 2;
69
+ }
70
+ const state = readState(dir).sessions[session] ?? emptySession();
71
+ const budgets = budgetTable(sources.budgetsInForce(team, root), recall(state.budgets), sources.now().getTime());
72
+ const comparison = compare(team, session, state, live, sources.now(), sources.watchInForce(team, root));
73
+ if (!live.running)
74
+ comparison.notes.unshift(`the herdr session "${session}" is not running`);
75
+ comparison.differences.push(...protectedCheckouts(team, root, sources), ...approvalDrift(sources.approval(team, root)));
76
+ if (args.flags.has('json')) {
77
+ const doc = {
78
+ format: 1,
79
+ project: team.project,
80
+ session,
81
+ rows: comparison.rows,
82
+ notes: comparison.notes,
83
+ differences: comparison.differences,
84
+ notice: current.notice ?? null,
85
+ ...(budgets.length ? { budgets } : {}),
86
+ };
87
+ io.stdout(`${JSON.stringify(doc, null, 2)}\n`);
88
+ }
89
+ else {
90
+ io.stdout(render(team, session, comparison, budgets));
91
+ }
92
+ return comparison.differences.length ? 1 : 0;
93
+ }
94
+ // A file that was never approved, or was changed since, runs nothing until the owner approves it.
95
+ export function approvalDrift(differences) {
96
+ if (differences === null)
97
+ return [{ what: 'the file was never approved on this machine', repair: 'the owner runs team approve' }];
98
+ return differences.map((line) => ({ what: `the file differs from the approved one: ${line}`, repair: 'the owner runs team approve' }));
99
+ }
100
+ function protectedCheckouts(team, root, sources) {
101
+ const base = team.workspace.base;
102
+ if (!base)
103
+ return [];
104
+ const out = [];
105
+ for (const path of team.workspace.protected) {
106
+ const full = resolve(root, path);
107
+ const branch = sources.branch(full);
108
+ if (branch === null || branch === base)
109
+ continue;
110
+ const shown = relative(root, full) || '.';
111
+ out.push({
112
+ what: `the protected checkout "${shown}" is on ${branch === 'HEAD' ? 'a detached commit' : `"${branch}"`}, not on "${base}"`,
113
+ repair: `the owner switches it back: git -C ${shown} switch ${base}`,
114
+ });
115
+ }
116
+ return out;
117
+ }
118
+ function render(team, session, comparison, budgets) {
119
+ const lines = [`team ${team.project}, session "${session}"`];
120
+ const widths = [0, 0, 0];
121
+ for (const row of comparison.rows) {
122
+ widths[0] = Math.max(widths[0], row.name.length);
123
+ widths[1] = Math.max(widths[1], row.state.length);
124
+ widths[2] = Math.max(widths[2], row.model.length);
125
+ }
126
+ for (const row of comparison.rows) {
127
+ lines.push(` ${row.name.padEnd(widths[0])} ${row.state.padEnd(widths[1])} ${row.model.padEnd(widths[2])} ${row.pane}`.trimEnd());
128
+ }
129
+ if (budgets.length) {
130
+ lines.push('budgets:');
131
+ // The row carries its own reserve: the line and the JSON can't disagree (queue 79).
132
+ for (const row of budgets)
133
+ lines.push(` ${budgetLine(row)}`);
134
+ }
135
+ for (const note of comparison.notes)
136
+ lines.push(`note: ${note}`);
137
+ for (const difference of comparison.differences) {
138
+ lines.push(`difference: ${difference.what}`, ` repair: ${difference.repair}`);
139
+ }
140
+ lines.push(`${comparison.differences.length} difference(s)`);
141
+ return `${lines.join('\n')}\n`;
142
+ }
143
+ function printProblems(io, problems) {
144
+ for (const problem of problems) {
145
+ io.stderr(`team status: ${problem.line ? `team.yaml line ${problem.line}: ` : ''}${problem.message}\n`);
146
+ }
147
+ }
@@ -0,0 +1,49 @@
1
+ import type { Seat, TeamFile } from '../file/types.ts';
2
+ import { type HerdrAgent, type SessionState } from '../herdr.ts';
3
+ import type { Command, Io } from '../io.ts';
4
+ import { type DoctorSources } from './doctor.ts';
5
+ import { type Machine } from '../watch/machine.ts';
6
+ export type UpSources = {
7
+ sessionRunning(session: string): boolean | null;
8
+ sessionState?(session: string): SessionState | null;
9
+ agents(session: string): HerdrAgent[] | null;
10
+ workspaces?(session: string): {
11
+ id: string;
12
+ }[] | null;
13
+ home: string;
14
+ doctor?: DoctorSources;
15
+ machine?(root: string): Machine;
16
+ now?(): Date;
17
+ sleep?(ms: number): Promise<void>;
18
+ alive?(pid: number): boolean;
19
+ watchCommand?(session: string): string;
20
+ launch?: Launch;
21
+ };
22
+ export type Launch = {
23
+ sessionState(session: string): SessionState | null;
24
+ startServer(session: string): boolean;
25
+ sessionUp(session: string): boolean | null;
26
+ createWorkspace(session: string, cwd: string, label: string): {
27
+ pane: string;
28
+ workspace: string;
29
+ } | null;
30
+ paneRun(session: string, pane: string, command: string): boolean;
31
+ renameAgent(session: string, pane: string, name: string): boolean;
32
+ closeWorkspace(session: string, workspace: string): boolean;
33
+ agentPanes(session: string): string[] | null;
34
+ paneText(session: string, pane: string): string | null;
35
+ typeText?(session: string, pane: string, text: string): boolean;
36
+ pressEnter?(session: string, pane: string): boolean;
37
+ agentStatus?(session: string, pane: string): string | null;
38
+ sleep(ms: number): Promise<void>;
39
+ now(): Date;
40
+ };
41
+ /** The watchdog command, from this process and this install: `team` may not be on the PATH. */
42
+ export declare function watchCommand(session: string): string;
43
+ export declare const realSources: UpSources;
44
+ export declare const USAGE = "Usage: team up [--dry-run] [--session <name>] [--file <path>]\n";
45
+ export declare const up: Command;
46
+ export default up;
47
+ /** The rules one seat gets at launch, with its own signature lines. */
48
+ export declare function rulesOf(team: TeamFile, seat: Seat): string;
49
+ export declare function runUp(argv: string[], io: Io, sources: UpSources): Promise<number>;