team 0.0.1 → 0.1.1
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/LICENSE +21 -0
- package/README.md +310 -0
- package/dist/approve/approval.d.ts +38 -0
- package/dist/approve/approval.js +78 -0
- package/dist/approve/diff.d.ts +13 -0
- package/dist/approve/diff.js +47 -0
- package/dist/approve/fingerprint.d.ts +44 -0
- package/dist/approve/fingerprint.js +109 -0
- package/dist/args.d.ts +7 -0
- package/dist/args.js +22 -0
- package/dist/budgets/checks.d.ts +35 -0
- package/dist/budgets/checks.js +77 -0
- package/dist/budgets/gate.d.ts +24 -0
- package/dist/budgets/gate.js +127 -0
- package/dist/budgets/readings.d.ts +69 -0
- package/dist/budgets/readings.js +156 -0
- package/dist/budgets/run.d.ts +66 -0
- package/dist/budgets/run.js +119 -0
- package/dist/budgets/table.d.ts +24 -0
- package/dist/budgets/table.js +90 -0
- package/dist/caller.d.ts +48 -0
- package/dist/caller.js +126 -0
- package/dist/check/config.d.ts +51 -0
- package/dist/check/config.js +50 -0
- package/dist/check/git.d.ts +27 -0
- package/dist/check/git.js +97 -0
- package/dist/check/message.d.ts +27 -0
- package/dist/check/message.js +101 -0
- package/dist/check/run.d.ts +31 -0
- package/dist/check/run.js +77 -0
- package/dist/check/signature.d.ts +10 -0
- package/dist/check/signature.js +22 -0
- package/dist/cli.d.ts +9 -0
- package/dist/cli.js +76 -0
- package/dist/clis.d.ts +7 -0
- package/dist/clis.js +9 -0
- package/dist/commands/add.d.ts +23 -0
- package/dist/commands/add.js +445 -0
- package/dist/commands/approve.d.ts +11 -0
- package/dist/commands/approve.js +146 -0
- package/dist/commands/check.d.ts +17 -0
- package/dist/commands/check.js +103 -0
- package/dist/commands/doctor.d.ts +38 -0
- package/dist/commands/doctor.js +232 -0
- package/dist/commands/down.d.ts +37 -0
- package/dist/commands/down.js +258 -0
- package/dist/commands/init.d.ts +9 -0
- package/dist/commands/init.js +138 -0
- package/dist/commands/remove.d.ts +23 -0
- package/dist/commands/remove.js +235 -0
- package/dist/commands/status.d.ts +41 -0
- package/dist/commands/status.js +147 -0
- package/dist/commands/up.d.ts +49 -0
- package/dist/commands/up.js +362 -0
- package/dist/commands/watch.d.ts +35 -0
- package/dist/commands/watch.js +324 -0
- package/dist/commands/worktree.d.ts +12 -0
- package/dist/commands/worktree.js +388 -0
- package/dist/end/condition.d.ts +28 -0
- package/dist/end/condition.js +77 -0
- package/dist/file/current.d.ts +13 -0
- package/dist/file/current.js +53 -0
- package/dist/file/lines.d.ts +25 -0
- package/dist/file/lines.js +277 -0
- package/dist/file/load.d.ts +7 -0
- package/dist/file/load.js +90 -0
- package/dist/file/paths.d.ts +6 -0
- package/dist/file/paths.js +77 -0
- package/dist/file/secrets.d.ts +6 -0
- package/dist/file/secrets.js +54 -0
- package/dist/file/signature.d.ts +13 -0
- package/dist/file/signature.js +36 -0
- package/dist/file/types.d.ts +130 -0
- package/dist/file/types.js +1 -0
- package/dist/file/validate.d.ts +13 -0
- package/dist/file/validate.js +702 -0
- package/dist/file/write.d.ts +11 -0
- package/dist/file/write.js +13 -0
- package/dist/herdr.d.ts +38 -0
- package/dist/herdr.js +227 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +6 -0
- package/dist/io.d.ts +10 -0
- package/dist/io.js +1 -0
- package/dist/launch/deliver.d.ts +10 -0
- package/dist/launch/deliver.js +39 -0
- package/dist/launch/execute.d.ts +46 -0
- package/dist/launch/execute.js +284 -0
- package/dist/launch/plan.d.ts +185 -0
- package/dist/launch/plan.js +280 -0
- package/dist/launch/rules.d.ts +25 -0
- package/dist/launch/rules.js +36 -0
- package/dist/log.d.ts +1 -0
- package/dist/log.js +24 -0
- package/dist/profiles/antigravity.yaml +66 -0
- package/dist/profiles/claude-code.yaml +69 -0
- package/dist/profiles/codex.yaml +77 -0
- package/dist/profiles/cursor.yaml +68 -0
- package/dist/profiles/index.d.ts +1 -0
- package/dist/profiles/index.js +1 -0
- package/dist/profiles/profile.d.ts +60 -0
- package/dist/profiles/profile.js +313 -0
- package/dist/profiles/quota.d.ts +27 -0
- package/dist/profiles/quota.js +57 -0
- package/dist/state.d.ts +61 -0
- package/dist/state.js +127 -0
- package/dist/status/compare.d.ts +26 -0
- package/dist/status/compare.js +110 -0
- package/dist/status/statusline.d.ts +9 -0
- package/dist/status/statusline.js +26 -0
- package/dist/store/store.d.ts +62 -0
- package/dist/store/store.js +116 -0
- package/dist/watch/check.d.ts +82 -0
- package/dist/watch/check.js +29 -0
- package/dist/watch/checks/approval.d.ts +2 -0
- package/dist/watch/checks/approval.js +16 -0
- package/dist/watch/checks/attention.d.ts +2 -0
- package/dist/watch/checks/attention.js +23 -0
- package/dist/watch/checks/budget.d.ts +2 -0
- package/dist/watch/checks/budget.js +141 -0
- package/dist/watch/checks/disk.d.ts +2 -0
- package/dist/watch/checks/disk.js +13 -0
- package/dist/watch/checks/extra.d.ts +2 -0
- package/dist/watch/checks/extra.js +18 -0
- package/dist/watch/checks/idle.d.ts +2 -0
- package/dist/watch/checks/idle.js +32 -0
- package/dist/watch/checks/load.d.ts +2 -0
- package/dist/watch/checks/load.js +11 -0
- package/dist/watch/checks/memory.d.ts +2 -0
- package/dist/watch/checks/memory.js +11 -0
- package/dist/watch/checks/missing.d.ts +2 -0
- package/dist/watch/checks/missing.js +12 -0
- package/dist/watch/checks/model-drift.d.ts +2 -0
- package/dist/watch/checks/model-drift.js +14 -0
- package/dist/watch/checks/swap-free.d.ts +2 -0
- package/dist/watch/checks/swap-free.js +12 -0
- package/dist/watch/checks/swap-growth.d.ts +2 -0
- package/dist/watch/checks/swap-growth.js +18 -0
- package/dist/watch/checks/team-idle.d.ts +2 -0
- package/dist/watch/checks/team-idle.js +18 -0
- package/dist/watch/checks/unsent.d.ts +2 -0
- package/dist/watch/checks/unsent.js +18 -0
- package/dist/watch/close.d.ts +19 -0
- package/dist/watch/close.js +40 -0
- package/dist/watch/dialect.d.ts +5 -0
- package/dist/watch/dialect.js +430 -0
- package/dist/watch/end.d.ts +11 -0
- package/dist/watch/end.js +17 -0
- package/dist/watch/machine.d.ts +38 -0
- package/dist/watch/machine.js +137 -0
- package/dist/watch/notify.d.ts +1 -0
- package/dist/watch/notify.js +15 -0
- package/dist/watch/pass.d.ts +42 -0
- package/dist/watch/pass.js +242 -0
- package/dist/watch/screen-core.d.ts +20 -0
- package/dist/watch/screen-core.js +356 -0
- package/dist/watch/screen-data.d.ts +65 -0
- package/dist/watch/screen-data.js +1 -0
- package/dist/watch/screen-file.d.ts +2 -0
- package/dist/watch/screen-file.js +269 -0
- package/dist/watch/screen.d.ts +24 -0
- package/dist/watch/screen.js +47 -0
- package/dist/worktree/place.d.ts +18 -0
- package/dist/worktree/place.js +126 -0
- package/dist/yaml.d.ts +30 -0
- package/dist/yaml.js +380 -0
- package/examples/checks/codex-quota +217 -0
- package/examples/team.yaml +76 -0
- package/package.json +29 -10
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import { readArgs } from "../args.js";
|
|
2
|
+
import { budgetsInForce, watchInForce } from "../approve/approval.js";
|
|
3
|
+
import { saveReadings, saveSpendReadings } 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
|
+
// The money a spend check counted is kept, like the pass's screen readings: the launch
|
|
135
|
+
// gate of `up` and `add` reads it later, and one that is stale by then reads unknown
|
|
136
|
+
// (§ 5). An account whose check did not run keeps its stored reading.
|
|
137
|
+
saveSpendReadings(dir, session, spendOf(outcomes));
|
|
138
|
+
for (const outcome of outcomes) {
|
|
139
|
+
const key = `check:${outcome.account}`;
|
|
140
|
+
if (outcome.state === 'read') {
|
|
141
|
+
told.delete(key);
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (told.has(key))
|
|
145
|
+
continue;
|
|
146
|
+
told.add(key);
|
|
147
|
+
say(outcome.state === 'unreadable'
|
|
148
|
+
? `${outcome.account}: its check is unreadable`
|
|
149
|
+
: `the check for ${outcome.account} is unapproved; that account reads unknown`, false);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
const result = pass({
|
|
153
|
+
team, state, live, machine: sources.machine(root), now, memory,
|
|
154
|
+
approval: sources.approval(team, root), watch: inForce, outcomes, budgets: budget,
|
|
155
|
+
});
|
|
156
|
+
for (const report of result.reports)
|
|
157
|
+
say(report.text, true);
|
|
158
|
+
saveReadings(dir, session, result.readings, now);
|
|
159
|
+
if (result.nudge) {
|
|
160
|
+
if (args.flags.has('no-nudge'))
|
|
161
|
+
say(`nudge not typed (--no-nudge): ${result.nudge.text}`, false);
|
|
162
|
+
else
|
|
163
|
+
deliver(result.nudge, team, session, sources, memory, say);
|
|
164
|
+
}
|
|
165
|
+
if (result.fallback)
|
|
166
|
+
say(result.fallback, true);
|
|
167
|
+
await closeEnded({ team, root, dir, session, live, sources, say, io, told });
|
|
168
|
+
}
|
|
169
|
+
beat();
|
|
170
|
+
if (!(await sources.wait(inForce.interval)))
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
finally {
|
|
175
|
+
updateState(dir, (state) => {
|
|
176
|
+
const record = state.sessions[session];
|
|
177
|
+
if (record?.watch?.pid === sources.pid)
|
|
178
|
+
delete record.watch;
|
|
179
|
+
});
|
|
180
|
+
say(`the watch of "${session}" stopped`, true);
|
|
181
|
+
}
|
|
182
|
+
return 0;
|
|
183
|
+
}
|
|
184
|
+
/** The spend readings among a pass's check outcomes, for the state. */
|
|
185
|
+
function spendOf(outcomes) {
|
|
186
|
+
const list = [];
|
|
187
|
+
for (const outcome of outcomes) {
|
|
188
|
+
if (outcome.state !== 'read' || outcome.reading.kind !== 'spend')
|
|
189
|
+
continue;
|
|
190
|
+
list.push({
|
|
191
|
+
account: outcome.account,
|
|
192
|
+
amount: outcome.reading.amount,
|
|
193
|
+
currency: outcome.reading.currency,
|
|
194
|
+
at: outcome.reading.at,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
return list;
|
|
198
|
+
}
|
|
199
|
+
// Types the nudge, after reading the operator's screen once more: the pass saw it free, and a
|
|
200
|
+
// prompt may have appeared since. Anything but an empty idle prompt keeps the nudge pending.
|
|
201
|
+
function deliver(nudge, team, session, sources, memory, say) {
|
|
202
|
+
const cli = team.seats.find((seat) => seat.name === team.operator)?.cli ?? '';
|
|
203
|
+
const look = () => readScreen(cli, sources.screen(nudge.pane, session) ?? undefined).kind;
|
|
204
|
+
const status = sources.status(nudge.pane, session);
|
|
205
|
+
const keep = () => {
|
|
206
|
+
// The nudge's text carries no report, so the reports it was raised for go back to pending:
|
|
207
|
+
// the fallback notification and the next passes need them.
|
|
208
|
+
memory.pending.push(...nudge.pending);
|
|
209
|
+
memory.pendingSince ??= sources.now().getTime();
|
|
210
|
+
};
|
|
211
|
+
if ((status !== 'idle' && status !== 'done') || look() !== 'idle' || !sources.typeText(nudge.pane, nudge.text, session)) {
|
|
212
|
+
keep();
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
// The text is in the box. A dialog that opened meanwhile must not get the Enter: the text
|
|
216
|
+
// then stays unsent, which the next passes report, and the nudge is kept.
|
|
217
|
+
const after = look();
|
|
218
|
+
if (after !== 'idle' && after !== 'unsent') {
|
|
219
|
+
say('a nudge was typed and not sent: the operator\'s screen changed before the Enter', true);
|
|
220
|
+
keep();
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
if (sources.pressEnter(nudge.pane, session))
|
|
224
|
+
say(`nudged the operator: ${nudge.text}`, false);
|
|
225
|
+
else
|
|
226
|
+
keep();
|
|
227
|
+
}
|
|
228
|
+
function noteWorked(dir, session, live) {
|
|
229
|
+
const recorded = readState(dir).sessions[session];
|
|
230
|
+
if (!recorded)
|
|
231
|
+
return;
|
|
232
|
+
const due = live.agents.some((agent) => {
|
|
233
|
+
const seat = agent.name ? recorded.seats[agent.name] : undefined;
|
|
234
|
+
return seat?.temporary && seat.stage === 'ready' && !seat.worked && agent.status === 'working';
|
|
235
|
+
});
|
|
236
|
+
if (!due)
|
|
237
|
+
return;
|
|
238
|
+
updateState(dir, (file) => {
|
|
239
|
+
const seats = (file.sessions[session] ??= emptySession()).seats;
|
|
240
|
+
for (const agent of live.agents) {
|
|
241
|
+
const seat = agent.name ? seats[agent.name] : undefined;
|
|
242
|
+
if (seat?.temporary && seat.stage === 'ready' && agent.status === 'working')
|
|
243
|
+
seat.worked = true;
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
function sayOnce(told, key, text, say) {
|
|
248
|
+
if (!text || told.has(key))
|
|
249
|
+
return;
|
|
250
|
+
told.add(key);
|
|
251
|
+
say(text, true);
|
|
252
|
+
}
|
|
253
|
+
// Closes a temporary seat whose end holds, and removes an on-merge worktree whose branch is merged.
|
|
254
|
+
// Anything that is not free, or not proved, is left as it is.
|
|
255
|
+
async function closeEnded(input) {
|
|
256
|
+
const { team, root, dir, session, live, sources, say, io, told } = input;
|
|
257
|
+
const endOf = sources.readEnd ?? readEnd;
|
|
258
|
+
const state = readState(dir).sessions[session] ?? emptySession();
|
|
259
|
+
for (const [name, recorded] of Object.entries(state.seats)) {
|
|
260
|
+
const temporary = recorded.temporary;
|
|
261
|
+
if (!temporary)
|
|
262
|
+
continue;
|
|
263
|
+
const task = temporary.task;
|
|
264
|
+
const work = task ? state.worktrees[task] : undefined;
|
|
265
|
+
const ownBefore = work ? work.own_commits === true : temporary.own_commits === true;
|
|
266
|
+
const end = endOf(root, temporary.until, team.workspace.base, ownBefore);
|
|
267
|
+
const agent = live.agents.find((item) => item.name === name);
|
|
268
|
+
const cli = team.seats.find((seat) => seat.name === temporary.like)?.cli ?? '';
|
|
269
|
+
const free = agent
|
|
270
|
+
? stateOf(sources.status(agent.pane, session) ?? agent.status, readScreen(cli, sources.screen(agent.pane, session) ?? undefined)) === 'free'
|
|
271
|
+
: false;
|
|
272
|
+
const decision = judgeTemporary({ worked: recorded.worked === true, free, ownBefore, end });
|
|
273
|
+
if (decision.ownCommits) {
|
|
274
|
+
updateState(dir, (file) => {
|
|
275
|
+
const current = (file.sessions[session] ??= emptySession());
|
|
276
|
+
if (work && task) {
|
|
277
|
+
const tree = current.worktrees[task];
|
|
278
|
+
if (tree)
|
|
279
|
+
tree.own_commits = true;
|
|
280
|
+
}
|
|
281
|
+
else {
|
|
282
|
+
const seat = current.seats[name];
|
|
283
|
+
if (seat?.temporary)
|
|
284
|
+
seat.temporary.own_commits = true;
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
sayOnce(told, `end:${name}`, decision.report, say);
|
|
289
|
+
if (!decision.close || !agent)
|
|
290
|
+
continue;
|
|
291
|
+
const seat = { name, cli, pane: agent.pane, workspace: agent.workspace, state: 'free' };
|
|
292
|
+
const stopped = sources.stopSeat
|
|
293
|
+
? await sources.stopSeat(session, seat)
|
|
294
|
+
: await stopRunning({
|
|
295
|
+
io, dir, session, seat, abandon: false, sources: removeSources, logCommand: 'watch', caller: 'watch',
|
|
296
|
+
});
|
|
297
|
+
if (stopped)
|
|
298
|
+
say(`closed ${name}; its end ${temporary.until} holds`, false);
|
|
299
|
+
}
|
|
300
|
+
const again = readState(dir);
|
|
301
|
+
const here = again.sessions[session] ?? emptySession();
|
|
302
|
+
for (const [task, work] of Object.entries(here.worktrees)) {
|
|
303
|
+
const occupied = Object.values(again.sessions).some((recorded) => Object.values(recorded.seats).some((seat) => seat.temporary?.task === task));
|
|
304
|
+
const end = endOf(root, `merged:${work.branch}`, team.workspace.base, work.own_commits === true);
|
|
305
|
+
const decision = judgeWorktree({
|
|
306
|
+
policy: team.workspace.remove, occupied, ownBefore: work.own_commits === true, end,
|
|
307
|
+
});
|
|
308
|
+
if (decision.ownCommits) {
|
|
309
|
+
updateState(dir, (file) => {
|
|
310
|
+
const tree = file.sessions[session]?.worktrees[task];
|
|
311
|
+
if (tree)
|
|
312
|
+
tree.own_commits = true;
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
sayOnce(told, `worktree:${task}`, decision.report, say);
|
|
316
|
+
if (!decision.close)
|
|
317
|
+
continue;
|
|
318
|
+
const code = sources.removeWorktree
|
|
319
|
+
? sources.removeWorktree(task)
|
|
320
|
+
: removeWorktree(io, { home: homedir(), now: sources.now }, team, root, dir, session, task, 'watch');
|
|
321
|
+
if (code !== 0)
|
|
322
|
+
say(`worktree ${task} was not removed`, true);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
@@ -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;
|