tryharness 0.7.23 → 0.7.25
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/assets/opencode-plugin.js +48 -11
- package/assets/pi-extension.ts +105 -26
- package/cli.mjs +1238 -322
- package/lib/activity.mjs +197 -40
- package/lib/adapters.mjs +337 -54
- package/lib/agentnode.mjs +175 -35
- package/lib/atlas.mjs +383 -83
- package/lib/cards.mjs +174 -40
- package/lib/claudemem.mjs +74 -18
- package/lib/commands.mjs +13 -5
- package/lib/context.mjs +409 -71
- package/lib/control-client.mjs +5 -2
- package/lib/coordinator.mjs +274 -57
- package/lib/daemon.mjs +5248 -1024
- package/lib/desktop.mjs +149 -35
- package/lib/digest.mjs +54 -13
- package/lib/environment.mjs +103 -21
- package/lib/files.mjs +246 -13
- package/lib/firewall.mjs +169 -35
- package/lib/folders.mjs +179 -36
- package/lib/frames.mjs +214 -40
- package/lib/inference.mjs +463 -126
- package/lib/learn.mjs +78 -16
- package/lib/live.mjs +286 -61
- package/lib/localmodels.mjs +424 -83
- package/lib/mcp-stdio.mjs +868 -151
- package/lib/media.mjs +138 -28
- package/lib/members.json +1 -1
- package/lib/members.mjs +35 -9
- package/lib/models.mjs +342 -80
- package/lib/network.mjs +201 -42
- package/lib/notify.mjs +49 -12
- package/lib/nudge.mjs +184 -33
- package/lib/ocr.mjs +223 -60
- package/lib/onboard.mjs +321 -70
- package/lib/parsers.mjs +132 -41
- package/lib/paths.mjs +33 -10
- package/lib/personal.mjs +51 -13
- package/lib/phones.mjs +382 -60
- package/lib/places.mjs +555 -107
- package/lib/provenance.mjs +19 -4
- package/lib/receipts.mjs +51 -11
- package/lib/redact.mjs +512 -116
- package/lib/runner.mjs +345 -55
- package/lib/runtime-install.mjs +55 -14
- package/lib/screens.mjs +154 -38
- package/lib/sealed.mjs +157 -28
- package/lib/secrets.mjs +18 -5
- package/lib/service.mjs +170 -34
- package/lib/snippets.mjs +94 -18
- package/lib/speech.mjs +688 -175
- package/lib/store.mjs +1654 -307
- package/lib/swarm.mjs +596 -117
- package/lib/t3.mjs +400 -79
- package/lib/trails.mjs +285 -61
- package/lib/ui.mjs +95 -23
- package/lib/vectors.mjs +5 -2
- package/lib/wakes.mjs +284 -68
- package/lib/watches.mjs +247 -48
- package/lib/worktrees.mjs +186 -38
- package/package.json +3 -3
package/lib/swarm.mjs
CHANGED
|
@@ -4,15 +4,35 @@ import path from "node:path";
|
|
|
4
4
|
import { spawnSync } from "node:child_process";
|
|
5
5
|
import { readRunSteps } from "./runner.mjs";
|
|
6
6
|
import { nameOf, resolveMember } from "./members.mjs";
|
|
7
|
-
import {
|
|
8
|
-
|
|
7
|
+
import {
|
|
8
|
+
modelOf,
|
|
9
|
+
setNote,
|
|
10
|
+
swarmBlock,
|
|
11
|
+
swarmCards,
|
|
12
|
+
swarmDigest,
|
|
13
|
+
} from "./cards.mjs";
|
|
14
|
+
import {
|
|
15
|
+
DEFAULT_ROUTING,
|
|
16
|
+
PICKABLE,
|
|
17
|
+
choiceOf,
|
|
18
|
+
listingOf,
|
|
19
|
+
picksIn,
|
|
20
|
+
routingOf,
|
|
21
|
+
setChoice,
|
|
22
|
+
setRouting,
|
|
23
|
+
} from "./models.mjs";
|
|
9
24
|
|
|
10
25
|
// The person's swarm as threads. A thread is a goal under the hood: its room is the transcript, its team the agents in
|
|
11
26
|
// it, its folder where coding agents work, and it runs on its own authority. A message the person sends wakes the
|
|
12
27
|
// agents it names (or the only agent in the thread), each resuming its own session; their runs' steps are read back
|
|
13
28
|
// from the run logs. The person's own terminal sessions are listed too and continue in a thread once idle.
|
|
14
29
|
|
|
15
|
-
export const PROCESS_NAMES = {
|
|
30
|
+
export const PROCESS_NAMES = {
|
|
31
|
+
"claude-code": "claude",
|
|
32
|
+
codex: "codex",
|
|
33
|
+
pi: "pi",
|
|
34
|
+
opencode: "opencode",
|
|
35
|
+
};
|
|
16
36
|
const SAY_LIMIT = 4_000;
|
|
17
37
|
const UUID = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i;
|
|
18
38
|
|
|
@@ -20,7 +40,9 @@ const UUID = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i;
|
|
|
20
40
|
export function nativeSessionId(agent, sessionId) {
|
|
21
41
|
const bare = String(sessionId).replace(/\.jsonl$/, "");
|
|
22
42
|
// Codex and pi name a session's file by its time and id; the id is what resumes it.
|
|
23
|
-
return agent === "codex" || agent === "pi"
|
|
43
|
+
return agent === "codex" || agent === "pi"
|
|
44
|
+
? (bare.match(UUID)?.[0] ?? bare)
|
|
45
|
+
: bare;
|
|
24
46
|
}
|
|
25
47
|
|
|
26
48
|
// The folder each running coding agent works in, by agent, leaving out the runs the daemon started itself.
|
|
@@ -33,12 +55,18 @@ export function agentProcessCwds(exclude = new Set()) {
|
|
|
33
55
|
for (const line of ps.stdout.split("\n")) {
|
|
34
56
|
const match = line.trim().match(/^(\d+)\s+(.+)$/);
|
|
35
57
|
if (!match || exclude.has(Number(match[1]))) continue;
|
|
36
|
-
const agent = Object.keys(PROCESS_NAMES).find(
|
|
58
|
+
const agent = Object.keys(PROCESS_NAMES).find(
|
|
59
|
+
(name) => path.basename(match[2]) === PROCESS_NAMES[name]
|
|
60
|
+
);
|
|
37
61
|
if (agent) byPid.set(match[1], agent);
|
|
38
62
|
}
|
|
39
63
|
const cwds = new Map();
|
|
40
64
|
if (!byPid.size) return cwds;
|
|
41
|
-
const lsof = spawnSync(
|
|
65
|
+
const lsof = spawnSync(
|
|
66
|
+
"lsof",
|
|
67
|
+
["-a", "-d", "cwd", "-Fn", "-p", [...byPid.keys()].join(",")],
|
|
68
|
+
{ encoding: "utf8" }
|
|
69
|
+
);
|
|
42
70
|
if (lsof.status !== 0 && !lsof.stdout) return null;
|
|
43
71
|
let pid = null;
|
|
44
72
|
for (const line of lsof.stdout.split("\n")) {
|
|
@@ -52,10 +80,39 @@ export function agentProcessCwds(exclude = new Set()) {
|
|
|
52
80
|
return cwds;
|
|
53
81
|
}
|
|
54
82
|
|
|
55
|
-
export function createSwarm({
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
83
|
+
export function createSwarm({
|
|
84
|
+
store,
|
|
85
|
+
roomAgents,
|
|
86
|
+
mentionsIn,
|
|
87
|
+
wakeFor,
|
|
88
|
+
notifyBoard,
|
|
89
|
+
goalCommand,
|
|
90
|
+
stopThreadRuns,
|
|
91
|
+
runLog,
|
|
92
|
+
runningPids,
|
|
93
|
+
provenanceFor,
|
|
94
|
+
defaults,
|
|
95
|
+
coordinator,
|
|
96
|
+
worktrees,
|
|
97
|
+
roster,
|
|
98
|
+
authorizeRun,
|
|
99
|
+
pickable,
|
|
100
|
+
listModels,
|
|
101
|
+
folders,
|
|
102
|
+
runsIn,
|
|
103
|
+
threadFiles = () => ({ files: [], rendering: [] }),
|
|
104
|
+
whyWaiting = () => null,
|
|
105
|
+
}) {
|
|
106
|
+
const awaitingStart = (run) =>
|
|
107
|
+
run.status === "open" &&
|
|
108
|
+
Boolean(run.run_workspace) &&
|
|
109
|
+
!run.run_authorized_at;
|
|
110
|
+
const isDir = (folder) =>
|
|
111
|
+
typeof folder === "string" &&
|
|
112
|
+
path.isAbsolute(folder) &&
|
|
113
|
+
fs.statSync(folder, { throwIfNoEntry: false })?.isDirectory();
|
|
114
|
+
const agentsOf = (thread) =>
|
|
115
|
+
thread.goal.team.filter((agent) => agent !== thread.goal.facilitator);
|
|
59
116
|
|
|
60
117
|
// Who a message is for: the agents it names, else the only agent in the thread, else whoever coordinates.
|
|
61
118
|
const addressed = (thread, text) => {
|
|
@@ -67,15 +124,33 @@ export function createSwarm({ store, roomAgents, mentionsIn, wakeFor, notifyBoar
|
|
|
67
124
|
|
|
68
125
|
// What the thread waits on the person for, if anything: a pause, an agent's question, a run's question.
|
|
69
126
|
const waitingOn = (thread, posts, runs) => {
|
|
70
|
-
if (thread.status === "paused")
|
|
71
|
-
|
|
72
|
-
|
|
127
|
+
if (thread.status === "paused")
|
|
128
|
+
return { kind: "paused", text: thread.goal.paused_reason || "Paused." };
|
|
129
|
+
const asking = runs.find(
|
|
130
|
+
(run) => run.status === "needs_input" && run.holder === "user"
|
|
131
|
+
);
|
|
132
|
+
if (asking)
|
|
133
|
+
return {
|
|
134
|
+
kind: "question",
|
|
135
|
+
agent: asking.claimed_by || asking.target_agent,
|
|
136
|
+
text: asking.result || "",
|
|
137
|
+
};
|
|
73
138
|
// Work another agent asked for that only the person's Start lets run (docs/member-runs.md).
|
|
74
139
|
const held = runs.find(awaitingStart);
|
|
75
|
-
if (held)
|
|
140
|
+
if (held)
|
|
141
|
+
return {
|
|
142
|
+
kind: "start",
|
|
143
|
+
agent: held.target_agent,
|
|
144
|
+
runId: held.id,
|
|
145
|
+
text: `${nameOf(held.target_agent)} waits for your Start`,
|
|
146
|
+
};
|
|
76
147
|
const lastMine = posts.findLastIndex((post) => post.author === "user");
|
|
77
|
-
const question = posts
|
|
78
|
-
|
|
148
|
+
const question = posts
|
|
149
|
+
.slice(lastMine + 1)
|
|
150
|
+
.findLast((post) => post.kind === "question");
|
|
151
|
+
return question
|
|
152
|
+
? { kind: "question", agent: question.author, text: question.text }
|
|
153
|
+
: null;
|
|
79
154
|
};
|
|
80
155
|
|
|
81
156
|
const summary = (thread) => {
|
|
@@ -85,17 +160,64 @@ export function createSwarm({ store, roomAgents, mentionsIn, wakeFor, notifyBoar
|
|
|
85
160
|
const live = runs.find((run) => run.status === "running");
|
|
86
161
|
const step = live ? readRunSteps(runLog(live.id), 1)[0] : null;
|
|
87
162
|
return {
|
|
88
|
-
id: thread.id,
|
|
163
|
+
id: thread.id,
|
|
164
|
+
title: thread.objective,
|
|
165
|
+
status: thread.status,
|
|
166
|
+
agents: agentsOf(thread),
|
|
167
|
+
coordinator: thread.goal.facilitator,
|
|
89
168
|
// The task this room was opened for, when it is a task's own room.
|
|
90
169
|
root: store.meta(`task-room:${thread.id}`) || null,
|
|
91
|
-
folder: thread.goal.workspace,
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
170
|
+
folder: thread.goal.workspace,
|
|
171
|
+
working: [
|
|
172
|
+
...new Set(
|
|
173
|
+
runs
|
|
174
|
+
.filter((run) => ["claimed", "running"].includes(run.status))
|
|
175
|
+
.map((run) => run.target_agent)
|
|
176
|
+
),
|
|
177
|
+
],
|
|
178
|
+
queued: [
|
|
179
|
+
...new Set(
|
|
180
|
+
runs
|
|
181
|
+
.filter((run) => run.status === "open")
|
|
182
|
+
.map((run) => run.target_agent)
|
|
183
|
+
),
|
|
184
|
+
],
|
|
185
|
+
// Why each agent given work here has not started, in a sentence: every surface says it the same way.
|
|
186
|
+
waits: runs
|
|
187
|
+
.map((run) => ({
|
|
188
|
+
agent: run.target_agent,
|
|
189
|
+
text: whyWaiting(thread.id, run),
|
|
190
|
+
}))
|
|
191
|
+
.filter((wait) => wait.text),
|
|
192
|
+
step: step
|
|
193
|
+
? {
|
|
194
|
+
agent: live.claimed_by || live.target_agent,
|
|
195
|
+
kind: step.kind,
|
|
196
|
+
text: step.text.split("\n")[0].slice(0, 200),
|
|
197
|
+
}
|
|
198
|
+
: null,
|
|
199
|
+
waiting: waitingOn(thread, posts, runs),
|
|
200
|
+
last: last
|
|
201
|
+
? {
|
|
202
|
+
author: last.author,
|
|
203
|
+
kind: last.kind,
|
|
204
|
+
text: last.text.slice(0, 280),
|
|
205
|
+
at: last.created_at,
|
|
206
|
+
mentions: last.mentions,
|
|
207
|
+
}
|
|
208
|
+
: null,
|
|
209
|
+
posts: thread.goal.posts,
|
|
210
|
+
spend: thread.goal.spend,
|
|
211
|
+
createdAt: thread.created_at,
|
|
212
|
+
updatedAt: last?.created_at ?? thread.updated_at,
|
|
96
213
|
// Parts the swarm moved past: never silent, one tap sends one back.
|
|
97
|
-
openItems: posts
|
|
98
|
-
|
|
214
|
+
openItems: posts
|
|
215
|
+
.filter((post) => post.kind === "open_item")
|
|
216
|
+
.map((post) => ({ seq: post.seq, text: post.text })),
|
|
217
|
+
coordination: {
|
|
218
|
+
mode: coordinator.mode(),
|
|
219
|
+
rounds: coordinator.state(thread.id).rounds,
|
|
220
|
+
},
|
|
99
221
|
changes: worktrees.summary(thread.id),
|
|
100
222
|
};
|
|
101
223
|
};
|
|
@@ -104,35 +226,94 @@ export function createSwarm({ store, roomAgents, mentionsIn, wakeFor, notifyBoar
|
|
|
104
226
|
const sessions = () => {
|
|
105
227
|
const live = agentProcessCwds(new Set(runningPids()));
|
|
106
228
|
const newestIn = new Set();
|
|
107
|
-
return store
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
229
|
+
return store
|
|
230
|
+
.recentSessions(30)
|
|
231
|
+
.filter(
|
|
232
|
+
(session) =>
|
|
233
|
+
PROCESS_NAMES[session.agent] &&
|
|
234
|
+
!store.threadForSession(
|
|
235
|
+
session.agent,
|
|
236
|
+
nativeSessionId(session.agent, session.session_id)
|
|
237
|
+
)
|
|
238
|
+
)
|
|
239
|
+
.map((session) => {
|
|
240
|
+
const key = `${session.agent}:${session.cwd}`;
|
|
241
|
+
const newest = !newestIn.has(key);
|
|
242
|
+
newestIn.add(key);
|
|
243
|
+
const open =
|
|
244
|
+
live === null ||
|
|
245
|
+
(newest && Boolean(live.get(session.agent)?.has(session.cwd)));
|
|
246
|
+
return {
|
|
247
|
+
agent: session.agent,
|
|
248
|
+
sessionId: session.session_id,
|
|
249
|
+
title: session.title,
|
|
250
|
+
folder: session.cwd,
|
|
251
|
+
startedAt: session.started_at,
|
|
252
|
+
endedAt: session.ended_at,
|
|
253
|
+
messages: session.message_count,
|
|
254
|
+
open,
|
|
255
|
+
};
|
|
256
|
+
});
|
|
114
257
|
};
|
|
115
258
|
|
|
116
|
-
const sessionOpen = (agent, sessionId) =>
|
|
259
|
+
const sessionOpen = (agent, sessionId) =>
|
|
260
|
+
sessions().find(
|
|
261
|
+
(session) => session.agent === agent && session.sessionId === sessionId
|
|
262
|
+
)?.open ?? false;
|
|
117
263
|
|
|
118
264
|
// The agent a request names in `to`: null for none, false for one no thread can have.
|
|
119
|
-
const recipient = (request) =>
|
|
265
|
+
const recipient = (request) =>
|
|
266
|
+
request.to == null || request.to === ""
|
|
267
|
+
? null
|
|
268
|
+
: roomAgents.includes(String(request.to))
|
|
269
|
+
? String(request.to)
|
|
270
|
+
: false;
|
|
120
271
|
|
|
121
272
|
// A thread needs a folder when a coding agent is in it, or when no one is named and the coordinator picks who works.
|
|
122
|
-
const needsFolder = (team) =>
|
|
273
|
+
const needsFolder = (team) =>
|
|
274
|
+
!team.length || team.some((name) => PROCESS_NAMES[name]);
|
|
123
275
|
// Where each coding agent in a thread would run its turn there: in T3 Code, or here, and why not T3 when the person uses it there.
|
|
124
276
|
// `lastWorked`: where its agent last worked, when Harness picked another folder from the message, for the person to take.
|
|
125
|
-
const placeOf = (folder, team, by, lastWorked = null) => ({
|
|
126
|
-
|
|
127
|
-
|
|
277
|
+
const placeOf = (folder, team, by, lastWorked = null) => ({
|
|
278
|
+
ok: true,
|
|
279
|
+
folder,
|
|
280
|
+
name: folder ? path.basename(folder) : null,
|
|
281
|
+
by,
|
|
282
|
+
lastWorked: lastWorked
|
|
283
|
+
? { folder: lastWorked, name: path.basename(lastWorked) }
|
|
284
|
+
: null,
|
|
285
|
+
runs: folder
|
|
286
|
+
? team
|
|
287
|
+
.filter((name) => PROCESS_NAMES[name])
|
|
288
|
+
.map((name) => ({ agent: name, ...runsIn(name, folder) }))
|
|
289
|
+
: [],
|
|
290
|
+
});
|
|
128
291
|
|
|
129
292
|
const read = (thread, hostedCaller = false) => {
|
|
130
293
|
const runs = store.threadRuns(thread.id).map((run) => ({
|
|
131
|
-
id: run.id,
|
|
132
|
-
|
|
133
|
-
|
|
294
|
+
id: run.id,
|
|
295
|
+
agent: run.claimed_by || run.target_agent,
|
|
296
|
+
kind: run.kind,
|
|
297
|
+
status: run.status,
|
|
298
|
+
awaitingStart: awaitingStart(run),
|
|
299
|
+
startedAt: run.created_at,
|
|
300
|
+
endedAt: run.reported_at,
|
|
301
|
+
sessionId: run.session_id,
|
|
302
|
+
folder: run.run_workspace,
|
|
303
|
+
usage: run.usage,
|
|
304
|
+
model: run.run_model,
|
|
305
|
+
steps: readRunSteps(runLog(run.id), 300).map((step) => ({
|
|
306
|
+
...step,
|
|
307
|
+
text: step.text.slice(0, SAY_LIMIT),
|
|
308
|
+
})),
|
|
134
309
|
}));
|
|
135
|
-
return {
|
|
310
|
+
return {
|
|
311
|
+
ok: true,
|
|
312
|
+
thread: summary(thread),
|
|
313
|
+
posts: store.roomPosts(thread.id, 0, 500),
|
|
314
|
+
runs,
|
|
315
|
+
...threadFiles(thread, hostedCaller),
|
|
316
|
+
};
|
|
136
317
|
};
|
|
137
318
|
|
|
138
319
|
// `to`: the one agent the person wrote to (the desktop's agent list), so their words go as written, with no @name.
|
|
@@ -140,191 +321,467 @@ export function createSwarm({ store, roomAgents, mentionsIn, wakeFor, notifyBoar
|
|
|
140
321
|
const picked = picksIn(store, text, roomAgents);
|
|
141
322
|
if (picked.error) return { ok: false, error: picked.error };
|
|
142
323
|
// A new name in the person's message joins the thread.
|
|
143
|
-
const joining = [
|
|
144
|
-
|
|
324
|
+
const joining = [
|
|
325
|
+
...new Set([...mentionsIn(text, roomAgents), ...(to ? [to] : [])]),
|
|
326
|
+
].filter((agent) => !thread.goal.team.includes(agent));
|
|
327
|
+
if (joining.length)
|
|
328
|
+
store.setGoalTeam(thread.id, [...thread.goal.team, ...joining]);
|
|
145
329
|
if (thread.status !== "open") {
|
|
146
330
|
store.setGoalState(thread.id, { status: "open" });
|
|
147
|
-
store.roomPost({
|
|
331
|
+
store.roomPost({
|
|
332
|
+
goalId: thread.id,
|
|
333
|
+
author: "user",
|
|
334
|
+
kind: "note",
|
|
335
|
+
text: "Picked up again by you.",
|
|
336
|
+
});
|
|
148
337
|
}
|
|
149
338
|
const current = store.getTask(thread.id);
|
|
150
|
-
return goalCommand(
|
|
339
|
+
return goalCommand(
|
|
340
|
+
{
|
|
341
|
+
command: "board.room.post",
|
|
342
|
+
id: thread.id,
|
|
343
|
+
text,
|
|
344
|
+
mentions: to ? [to] : addressed(current, text),
|
|
345
|
+
},
|
|
346
|
+
"user"
|
|
347
|
+
);
|
|
151
348
|
};
|
|
152
349
|
|
|
153
350
|
return function swarmCommand(request, agent) {
|
|
154
351
|
const command = String(request.command);
|
|
155
352
|
if (command === "board.swarm.list") {
|
|
156
|
-
if (agent !== "user" && agent !== "harness")
|
|
157
|
-
|
|
353
|
+
if (agent !== "user" && agent !== "harness")
|
|
354
|
+
return {
|
|
355
|
+
ok: false,
|
|
356
|
+
error: "only the person and Harness read the swarm",
|
|
357
|
+
};
|
|
358
|
+
return {
|
|
359
|
+
ok: true,
|
|
360
|
+
threads: store.goals({ all: request.all === true }).map(summary),
|
|
361
|
+
sessions: request.sessions === false ? [] : sessions(),
|
|
362
|
+
agents: roster(),
|
|
363
|
+
};
|
|
158
364
|
}
|
|
159
365
|
// The swarm's shared memory, for any of the person's agents: what any agent wrote in any thread, and the coding
|
|
160
366
|
// sessions on this machine. A caller limited to its own threads (a hosted agent while the machine is sealed) gets only those.
|
|
161
367
|
if (command === "board.swarm.search") {
|
|
162
|
-
const query = String(request.query || "")
|
|
368
|
+
const query = String(request.query || "")
|
|
369
|
+
.trim()
|
|
370
|
+
.slice(0, 500);
|
|
163
371
|
if (!query) return { ok: false, error: "search needs a query" };
|
|
164
|
-
const mine = request.teamOnly
|
|
165
|
-
|
|
372
|
+
const mine = request.teamOnly
|
|
373
|
+
? store
|
|
374
|
+
.goals({ all: true })
|
|
375
|
+
.filter((thread) => thread.goal.team.includes(agent))
|
|
376
|
+
.map((thread) => thread.id)
|
|
377
|
+
: null;
|
|
378
|
+
return {
|
|
379
|
+
ok: true,
|
|
380
|
+
threads: store.searchThreads(query, { goalIds: mine, k: 8 }),
|
|
381
|
+
sessions: request.teamOnly ? [] : store.search({ query, limit: 5 }),
|
|
382
|
+
};
|
|
166
383
|
}
|
|
167
384
|
// Who can be asked: every other agent's card (cards.mjs), and the same cards as the block an agent is told when its
|
|
168
385
|
// session starts. Open to every caller, so an agent picks a name instead of guessing one.
|
|
169
386
|
if (command === "board.swarm.agents") {
|
|
170
|
-
const agents = swarmCards(
|
|
171
|
-
|
|
387
|
+
const agents = swarmCards(
|
|
388
|
+
store,
|
|
389
|
+
roster().filter((entry) => entry.id !== agent)
|
|
390
|
+
);
|
|
391
|
+
return {
|
|
392
|
+
ok: true,
|
|
393
|
+
agents,
|
|
394
|
+
text: swarmBlock(agents),
|
|
395
|
+
digest: swarmDigest(agents),
|
|
396
|
+
};
|
|
172
397
|
}
|
|
173
398
|
// Each coding agent's models as it lists them, the person's choice for it and their routing words (models.mjs). Open
|
|
174
399
|
// to every caller, so an agent names a model that exists: post_task's model, or `@codex:model/effort` in a post.
|
|
175
400
|
if (command === "board.swarm.models") {
|
|
176
401
|
const named = request.target ? resolveMember(request.target) : null;
|
|
177
|
-
if (named && !named.ok)
|
|
402
|
+
if (named && !named.ok)
|
|
403
|
+
return { ok: false, error: `no agent called "${request.target}"` };
|
|
178
404
|
const ids = pickable().filter((id) => !named || id === named.id);
|
|
179
|
-
if (named && !ids.length)
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
405
|
+
if (named && !ids.length)
|
|
406
|
+
return {
|
|
407
|
+
ok: false,
|
|
408
|
+
error: `the relay sets the model of ${[...PICKABLE].join(", ")} when they are installed here, not ${nameOf(named.id)}`,
|
|
409
|
+
};
|
|
410
|
+
const listing = () => ({
|
|
411
|
+
ok: true,
|
|
412
|
+
routing: routingOf(store),
|
|
413
|
+
defaultRouting: DEFAULT_ROUTING,
|
|
414
|
+
agents: ids.map((id) => {
|
|
415
|
+
const listed = listingOf(store, id);
|
|
416
|
+
return {
|
|
417
|
+
id,
|
|
418
|
+
name: nameOf(id),
|
|
419
|
+
choice: choiceOf(store, id),
|
|
420
|
+
ownModel: modelOf(id),
|
|
421
|
+
listedAt: listed?.at ?? null,
|
|
422
|
+
...(listed?.error ? { error: listed.error } : {}),
|
|
423
|
+
models: listed?.models ?? [],
|
|
424
|
+
};
|
|
425
|
+
}),
|
|
426
|
+
});
|
|
427
|
+
return request.refresh === true && agent === "user"
|
|
428
|
+
? { pending: listModels().then(listing) }
|
|
429
|
+
: listing();
|
|
185
430
|
}
|
|
186
431
|
// Any of the person's agents may start a thread and stop one it started. The thread carries the agent's trust, so
|
|
187
432
|
// nothing in it runs without the person's Start, and one started through Harness gets no coordinator rounds (they
|
|
188
433
|
// are paid) until the person sets a spend ceiling.
|
|
189
434
|
if (agent !== "user" && command === "board.swarm.start") {
|
|
190
|
-
const text = String(request.text || "")
|
|
435
|
+
const text = String(request.text || "")
|
|
436
|
+
.trim()
|
|
437
|
+
.slice(0, 20_000);
|
|
191
438
|
if (!text) return { ok: false, error: "a thread starts with a message" };
|
|
192
439
|
const folder = request.folder ? String(request.folder) : null;
|
|
193
|
-
if (folder && !isDir(folder))
|
|
440
|
+
if (folder && !isDir(folder))
|
|
441
|
+
return {
|
|
442
|
+
ok: false,
|
|
443
|
+
error: `${folder} is not an absolute path to a folder`,
|
|
444
|
+
};
|
|
194
445
|
const team = new Set(mentionsIn(text, roomAgents));
|
|
195
446
|
const picked = picksIn(store, text, roomAgents);
|
|
196
447
|
if (picked.error) return { ok: false, error: picked.error };
|
|
197
|
-
if (!team.size)
|
|
198
|
-
|
|
199
|
-
|
|
448
|
+
if (!team.size)
|
|
449
|
+
return {
|
|
450
|
+
ok: false,
|
|
451
|
+
error: `name who the thread is for with @, one of: ${roomAgents.filter((name) => name !== agent && name !== "harness").join(", ")}`,
|
|
452
|
+
};
|
|
453
|
+
const objective =
|
|
454
|
+
text
|
|
455
|
+
.split("\n")
|
|
456
|
+
.map((line) => line.trim())
|
|
457
|
+
.find(Boolean)
|
|
458
|
+
.replace(/@([a-z][a-z0-9-]*)(:\S+)?\s*/g, "")
|
|
459
|
+
.slice(0, 120) || text.slice(0, 120);
|
|
460
|
+
const thread = store.createGoal({
|
|
461
|
+
objective,
|
|
462
|
+
originAgent: agent,
|
|
463
|
+
provenance: provenanceFor(agent),
|
|
464
|
+
team: [...team, agent, "harness"],
|
|
465
|
+
facilitator: "harness",
|
|
466
|
+
workspace: folder ? path.resolve(folder) : null,
|
|
467
|
+
maxPosts: defaults.maxPosts,
|
|
468
|
+
maxWakes: defaults.maxWakes,
|
|
469
|
+
});
|
|
200
470
|
if (request.hostedCaller) store.meta(`hosted-thread:${thread.id}`, "1");
|
|
201
|
-
const first = store.roomPost({
|
|
471
|
+
const first = store.roomPost({
|
|
472
|
+
goalId: thread.id,
|
|
473
|
+
author: agent,
|
|
474
|
+
kind: "post",
|
|
475
|
+
text,
|
|
476
|
+
mentions: addressed(store.getTask(thread.id), text),
|
|
477
|
+
});
|
|
202
478
|
wakeFor(thread.id, first, agent);
|
|
203
479
|
notifyBoard(store.getTask(thread.id));
|
|
204
|
-
return {
|
|
480
|
+
return {
|
|
481
|
+
ok: true,
|
|
482
|
+
thread: summary(store.getTask(thread.id)),
|
|
483
|
+
note: "Agents here run only after the person's Start; read the thread with read_room and add to it with post_to_room.",
|
|
484
|
+
};
|
|
205
485
|
}
|
|
206
486
|
if (agent !== "user" && command === "board.swarm.stop") {
|
|
207
487
|
const thread = store.getTask(String(request.id || ""));
|
|
208
|
-
if (!thread || thread.kind !== "goal" || thread.origin_agent !== agent)
|
|
209
|
-
|
|
488
|
+
if (!thread || thread.kind !== "goal" || thread.origin_agent !== agent)
|
|
489
|
+
return { ok: false, error: "thread not found" };
|
|
490
|
+
if (thread.status !== "open")
|
|
491
|
+
return { ok: false, error: `the thread is ${thread.status}` };
|
|
210
492
|
stopThreadRuns(thread.id);
|
|
211
493
|
return { ok: true, thread: summary(store.getTask(thread.id)) };
|
|
212
494
|
}
|
|
213
|
-
if (agent !== "user")
|
|
495
|
+
if (agent !== "user")
|
|
496
|
+
return {
|
|
497
|
+
ok: false,
|
|
498
|
+
error:
|
|
499
|
+
"only the person writes in, stops or resumes a thread they did not start; post in it with post_to_room",
|
|
500
|
+
};
|
|
214
501
|
// Where a thread works, for the composer (folders.mjs): the folders the person works in, any folder on the computer
|
|
215
502
|
// from a typed path, and where a message would work: the folder chosen (`folder`) or the one Harness suggests, and
|
|
216
503
|
// where each coding agent named in it (`text`, `to`, `agents`) runs its turns there.
|
|
217
|
-
if (command === "board.swarm.places")
|
|
504
|
+
if (command === "board.swarm.places")
|
|
505
|
+
return { ok: true, places: folders.known(), home: os.homedir() };
|
|
218
506
|
if (command === "board.swarm.browse") return folders.browse(request.path);
|
|
219
507
|
if (command === "board.swarm.place") {
|
|
220
508
|
const text = String(request.text || "").slice(0, 20_000);
|
|
221
509
|
const to = recipient(request);
|
|
222
|
-
const team = [
|
|
510
|
+
const team = [
|
|
511
|
+
...new Set([
|
|
512
|
+
...mentionsIn(text, roomAgents),
|
|
513
|
+
...(to ? [to] : []),
|
|
514
|
+
...(Array.isArray(request.agents)
|
|
515
|
+
? request.agents
|
|
516
|
+
.map(String)
|
|
517
|
+
.filter((name) => roomAgents.includes(name))
|
|
518
|
+
: []),
|
|
519
|
+
]),
|
|
520
|
+
];
|
|
223
521
|
if (request.folder) {
|
|
224
522
|
const folder = String(request.folder);
|
|
225
|
-
return isDir(folder)
|
|
523
|
+
return isDir(folder)
|
|
524
|
+
? placeOf(path.resolve(folder), team, "you")
|
|
525
|
+
: { ok: false, error: `${folder} is not a folder on this computer` };
|
|
226
526
|
}
|
|
227
527
|
if (!needsFolder(team)) return placeOf(null, team, null);
|
|
228
|
-
return {
|
|
528
|
+
return {
|
|
529
|
+
pending: folders
|
|
530
|
+
.suggest(text, team)
|
|
531
|
+
.then(({ folder, by, lastWorked }) =>
|
|
532
|
+
placeOf(folder, team, by, lastWorked)
|
|
533
|
+
),
|
|
534
|
+
};
|
|
229
535
|
}
|
|
230
536
|
// The person's own note on one agent's card ("use it for reviews"), told to every other agent; no text clears it.
|
|
231
537
|
if (command === "board.swarm.note") {
|
|
232
538
|
const named = resolveMember(request.target);
|
|
233
|
-
if (!named.ok)
|
|
234
|
-
|
|
539
|
+
if (!named.ok)
|
|
540
|
+
return {
|
|
541
|
+
ok: false,
|
|
542
|
+
error: named.candidates.length
|
|
543
|
+
? `"${request.target}" could be ${named.candidates.join(" or ")}; name one`
|
|
544
|
+
: `no agent called "${request.target}"`,
|
|
545
|
+
};
|
|
546
|
+
return {
|
|
547
|
+
ok: true,
|
|
548
|
+
agent: named.id,
|
|
549
|
+
note: setNote(store, named.id, request.text),
|
|
550
|
+
};
|
|
235
551
|
}
|
|
236
552
|
// The person's choice of model and effort for one agent: a model it lists, "auto", or "default" for its own settings.
|
|
237
553
|
if (command === "board.swarm.model") {
|
|
238
554
|
const named = resolveMember(request.target);
|
|
239
|
-
if (!named.ok || !PICKABLE.has(named.id))
|
|
555
|
+
if (!named.ok || !PICKABLE.has(named.id))
|
|
556
|
+
return {
|
|
557
|
+
ok: false,
|
|
558
|
+
error: `the relay sets the model of ${[...PICKABLE].join(", ")}, not "${request.target}"`,
|
|
559
|
+
};
|
|
240
560
|
const set = setChoice(store, named.id, request.model, request.effort);
|
|
241
561
|
return set.ok ? { ok: true, agent: named.id, choice: set.choice } : set;
|
|
242
562
|
}
|
|
243
563
|
// The person's words on how Auto picks models for swarm work; no text goes back to the default words.
|
|
244
|
-
if (command === "board.swarm.routing")
|
|
564
|
+
if (command === "board.swarm.routing")
|
|
565
|
+
return {
|
|
566
|
+
ok: true,
|
|
567
|
+
routing: setRouting(store, request.text),
|
|
568
|
+
defaultRouting: DEFAULT_ROUTING,
|
|
569
|
+
};
|
|
245
570
|
// The coordinator's record: what each round saw, chose and cost. The evidence for its limits.
|
|
246
|
-
if (command === "board.swarm.rounds")
|
|
571
|
+
if (command === "board.swarm.rounds")
|
|
572
|
+
return {
|
|
573
|
+
ok: true,
|
|
574
|
+
rounds: store.rounds(
|
|
575
|
+
request.id ? String(request.id) : null,
|
|
576
|
+
request.limit
|
|
577
|
+
),
|
|
578
|
+
};
|
|
247
579
|
if (command === "board.swarm.coordinator") {
|
|
248
|
-
if (request.mode === "harness" || request.mode === "local")
|
|
580
|
+
if (request.mode === "harness" || request.mode === "local")
|
|
581
|
+
coordinator.setMode(request.mode);
|
|
249
582
|
return { ok: true, mode: coordinator.mode() };
|
|
250
583
|
}
|
|
251
584
|
if (command === "board.swarm.stop_all") {
|
|
252
|
-
const stopped = store
|
|
585
|
+
const stopped = store
|
|
586
|
+
.goals()
|
|
587
|
+
.filter((thread) => thread.status === "open")
|
|
588
|
+
.map((thread) => {
|
|
589
|
+
stopThreadRuns(thread.id);
|
|
590
|
+
return thread.id;
|
|
591
|
+
});
|
|
253
592
|
return { ok: true, stopped };
|
|
254
593
|
}
|
|
255
594
|
if (command === "board.swarm.start") {
|
|
256
|
-
const text = String(request.text || "")
|
|
595
|
+
const text = String(request.text || "")
|
|
596
|
+
.trim()
|
|
597
|
+
.slice(0, 20_000);
|
|
257
598
|
if (!text) return { ok: false, error: "a thread starts with a message" };
|
|
258
|
-
const from =
|
|
599
|
+
const from =
|
|
600
|
+
request.continueSession && typeof request.continueSession === "object"
|
|
601
|
+
? request.continueSession
|
|
602
|
+
: null;
|
|
259
603
|
let folder = request.folder ? String(request.folder) : null;
|
|
260
|
-
if (folder && !isDir(folder))
|
|
604
|
+
if (folder && !isDir(folder))
|
|
605
|
+
return {
|
|
606
|
+
ok: false,
|
|
607
|
+
error: `${folder} is not an absolute path to a folder`,
|
|
608
|
+
};
|
|
261
609
|
const to = recipient(request);
|
|
262
|
-
if (to === false)
|
|
263
|
-
|
|
610
|
+
if (to === false)
|
|
611
|
+
return {
|
|
612
|
+
ok: false,
|
|
613
|
+
error: `${request.to} can't be written to in a thread`,
|
|
614
|
+
};
|
|
615
|
+
const team = new Set([
|
|
616
|
+
...mentionsIn(text, roomAgents),
|
|
617
|
+
...(to ? [to] : []),
|
|
618
|
+
]);
|
|
264
619
|
const picked = picksIn(store, text, roomAgents);
|
|
265
620
|
if (picked.error) return { ok: false, error: picked.error };
|
|
266
621
|
if (from) {
|
|
267
|
-
const agentName = String(from.agent || ""),
|
|
268
|
-
|
|
622
|
+
const agentName = String(from.agent || ""),
|
|
623
|
+
sessionId = String(from.sessionId || "");
|
|
624
|
+
if (!PROCESS_NAMES[agentName])
|
|
625
|
+
return {
|
|
626
|
+
ok: false,
|
|
627
|
+
error: "only a coding agent's session continues in a thread",
|
|
628
|
+
};
|
|
269
629
|
const session = store.read({ agent: agentName, sessionId });
|
|
270
630
|
if (!session) return { ok: false, error: "session not found" };
|
|
271
|
-
const already = store.threadForSession(
|
|
631
|
+
const already = store.threadForSession(
|
|
632
|
+
agentName,
|
|
633
|
+
nativeSessionId(agentName, sessionId)
|
|
634
|
+
);
|
|
272
635
|
if (already) return post(store.getTask(already), text);
|
|
273
|
-
if (sessionOpen(agentName, sessionId))
|
|
636
|
+
if (sessionOpen(agentName, sessionId))
|
|
637
|
+
return {
|
|
638
|
+
ok: false,
|
|
639
|
+
error:
|
|
640
|
+
"that session is still open in a terminal; it continues here once it is closed there",
|
|
641
|
+
};
|
|
274
642
|
team.add(agentName);
|
|
275
643
|
folder ??= isDir(session.cwd) ? session.cwd : null;
|
|
276
644
|
}
|
|
277
645
|
// Where the work happens: the folder the person chose, else one picked from what they wrote, else where its agent
|
|
278
646
|
// last worked (folders.mjs); never the home folder or a hidden one.
|
|
279
647
|
const begin = (where) => {
|
|
280
|
-
const objective =
|
|
281
|
-
|
|
282
|
-
|
|
648
|
+
const objective =
|
|
649
|
+
text
|
|
650
|
+
.split("\n")
|
|
651
|
+
.map((line) => line.trim())
|
|
652
|
+
.find(Boolean)
|
|
653
|
+
.replace(/@([a-z][a-z0-9-]*)(:\S+)?\s*/g, "")
|
|
654
|
+
.slice(0, 120) || text.slice(0, 120);
|
|
655
|
+
const thread = store.createGoal({
|
|
656
|
+
objective,
|
|
657
|
+
originAgent: "user",
|
|
658
|
+
provenance: provenanceFor("user"),
|
|
659
|
+
team: [...new Set([...team, "harness"])],
|
|
660
|
+
facilitator: "harness",
|
|
661
|
+
workspace: where ? path.resolve(where) : null,
|
|
662
|
+
maxPosts: defaults.maxPosts,
|
|
663
|
+
maxWakes: defaults.maxWakes,
|
|
664
|
+
});
|
|
665
|
+
if (from) {
|
|
666
|
+
store.setGoalSession(
|
|
667
|
+
thread.id,
|
|
668
|
+
String(from.agent),
|
|
669
|
+
nativeSessionId(String(from.agent), String(from.sessionId))
|
|
670
|
+
);
|
|
671
|
+
worktrees.inPlace(thread.id);
|
|
672
|
+
}
|
|
283
673
|
const opened = store.getTask(thread.id);
|
|
284
|
-
const first = store.roomPost({
|
|
674
|
+
const first = store.roomPost({
|
|
675
|
+
goalId: thread.id,
|
|
676
|
+
author: "user",
|
|
677
|
+
kind: "post",
|
|
678
|
+
text,
|
|
679
|
+
mentions: addressed(opened, text),
|
|
680
|
+
});
|
|
285
681
|
wakeFor(thread.id, first, "user");
|
|
286
682
|
notifyBoard(store.getTask(thread.id));
|
|
287
683
|
return { ok: true, thread: summary(store.getTask(thread.id)) };
|
|
288
684
|
};
|
|
289
|
-
return folder || !needsFolder([...team])
|
|
685
|
+
return folder || !needsFolder([...team])
|
|
686
|
+
? begin(folder)
|
|
687
|
+
: {
|
|
688
|
+
pending: folders
|
|
689
|
+
.suggest(text, [...team])
|
|
690
|
+
.then((suggested) => begin(suggested.folder)),
|
|
691
|
+
};
|
|
290
692
|
}
|
|
291
693
|
const thread = store.getTask(String(request.id || ""));
|
|
292
|
-
if (!thread || thread.kind !== "goal")
|
|
293
|
-
|
|
294
|
-
if (command === "board.swarm.
|
|
694
|
+
if (!thread || thread.kind !== "goal")
|
|
695
|
+
return { ok: false, error: "thread not found" };
|
|
696
|
+
if (command === "board.swarm.read")
|
|
697
|
+
return read(thread, request.hostedCaller === true);
|
|
698
|
+
if (command === "board.swarm.diff")
|
|
699
|
+
return worktrees.diff(
|
|
700
|
+
thread.id,
|
|
701
|
+
request.runId ? String(request.runId) : null
|
|
702
|
+
);
|
|
295
703
|
if (command === "board.swarm.merge" || command === "board.swarm.discard") {
|
|
296
704
|
// Both change the person's repository, so both wait for the person, and never while an agent is changing files.
|
|
297
|
-
if (
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
705
|
+
if (
|
|
706
|
+
store
|
|
707
|
+
.threadRuns(thread.id)
|
|
708
|
+
.some((run) => ["open", "claimed", "running"].includes(run.status))
|
|
709
|
+
)
|
|
710
|
+
return {
|
|
711
|
+
ok: false,
|
|
712
|
+
error:
|
|
713
|
+
"an agent is still working in this thread; stop it or wait for it first",
|
|
714
|
+
};
|
|
715
|
+
const done =
|
|
716
|
+
command === "board.swarm.merge"
|
|
717
|
+
? worktrees.merge(thread.id, thread.objective)
|
|
718
|
+
: worktrees.discard(thread.id);
|
|
719
|
+
if (done.ok)
|
|
720
|
+
store.roomPost({
|
|
721
|
+
goalId: thread.id,
|
|
722
|
+
author: "user",
|
|
723
|
+
kind: "note",
|
|
724
|
+
text:
|
|
725
|
+
command === "board.swarm.merge"
|
|
726
|
+
? `Merged into ${done.into}.`
|
|
727
|
+
: "Discarded this thread's changes.",
|
|
728
|
+
});
|
|
729
|
+
else if (done.conflict)
|
|
730
|
+
store.roomPost({
|
|
731
|
+
goalId: thread.id,
|
|
732
|
+
author: thread.goal.facilitator,
|
|
733
|
+
kind: "question",
|
|
734
|
+
text: `${done.error[0].toUpperCase()}${done.error.slice(1)}. Resolve it in your checkout, or ask an agent here to bring the thread's branch up to date first.`,
|
|
735
|
+
});
|
|
301
736
|
notifyBoard(store.getTask(thread.id));
|
|
302
|
-
return done.ok
|
|
737
|
+
return done.ok
|
|
738
|
+
? { ok: true, thread: summary(store.getTask(thread.id)) }
|
|
739
|
+
: done;
|
|
303
740
|
}
|
|
304
741
|
// The person's Start for one run in the thread that waits for it.
|
|
305
742
|
if (command === "board.swarm.start_run") {
|
|
306
|
-
const run = store
|
|
307
|
-
|
|
743
|
+
const run = store
|
|
744
|
+
.threadRuns(thread.id)
|
|
745
|
+
.find((candidate) => candidate.id === String(request.runId || ""));
|
|
746
|
+
if (!run || !awaitingStart(run))
|
|
747
|
+
return { ok: false, error: "that run is not waiting for a Start" };
|
|
308
748
|
const started = authorizeRun(run.id);
|
|
309
|
-
return started?.ok === false
|
|
749
|
+
return started?.ok === false
|
|
750
|
+
? started
|
|
751
|
+
: { ok: true, thread: summary(store.getTask(thread.id)) };
|
|
310
752
|
}
|
|
311
753
|
if (command === "board.swarm.post") {
|
|
312
|
-
const text = String(request.text || "")
|
|
754
|
+
const text = String(request.text || "")
|
|
755
|
+
.trim()
|
|
756
|
+
.slice(0, 20_000);
|
|
313
757
|
if (!text) return { ok: false, error: "a message needs text" };
|
|
314
758
|
const to = recipient(request);
|
|
315
|
-
if (to === false)
|
|
759
|
+
if (to === false)
|
|
760
|
+
return {
|
|
761
|
+
ok: false,
|
|
762
|
+
error: `${request.to} can't be written to in a thread`,
|
|
763
|
+
};
|
|
316
764
|
const posted = post(thread, text, to);
|
|
317
|
-
return posted.ok
|
|
765
|
+
return posted.ok
|
|
766
|
+
? { ...posted, thread: summary(store.getTask(thread.id)) }
|
|
767
|
+
: posted;
|
|
318
768
|
}
|
|
319
769
|
if (command === "board.swarm.stop") {
|
|
320
|
-
if (thread.status !== "open")
|
|
770
|
+
if (thread.status !== "open")
|
|
771
|
+
return { ok: false, error: `the thread is ${thread.status}` };
|
|
321
772
|
stopThreadRuns(thread.id);
|
|
322
773
|
return { ok: true, thread: summary(store.getTask(thread.id)) };
|
|
323
774
|
}
|
|
324
775
|
if (command === "board.swarm.resume") {
|
|
325
|
-
if (thread.status !== "paused")
|
|
776
|
+
if (thread.status !== "paused")
|
|
777
|
+
return { ok: false, error: `the thread is ${thread.status}` };
|
|
326
778
|
store.setGoalState(thread.id, { status: "open" });
|
|
327
|
-
store.roomPost({
|
|
779
|
+
store.roomPost({
|
|
780
|
+
goalId: thread.id,
|
|
781
|
+
author: "user",
|
|
782
|
+
kind: "note",
|
|
783
|
+
text: "Resumed by you.",
|
|
784
|
+
});
|
|
328
785
|
notifyBoard(store.getTask(thread.id));
|
|
329
786
|
return { ok: true, thread: summary(store.getTask(thread.id)) };
|
|
330
787
|
}
|
|
@@ -332,16 +789,38 @@ export function createSwarm({ store, roomAgents, mentionsIn, wakeFor, notifyBoar
|
|
|
332
789
|
// and a T3 thread belong to the folder they ran in.
|
|
333
790
|
if (command === "board.swarm.folder") {
|
|
334
791
|
const folder = String(request.folder || "");
|
|
335
|
-
if (!isDir(folder))
|
|
792
|
+
if (!isDir(folder))
|
|
793
|
+
return {
|
|
794
|
+
ok: false,
|
|
795
|
+
error: `${folder} is not a folder on this computer`,
|
|
796
|
+
};
|
|
336
797
|
const where = path.resolve(folder);
|
|
337
|
-
if (where === thread.goal.workspace)
|
|
338
|
-
|
|
798
|
+
if (where === thread.goal.workspace)
|
|
799
|
+
return { ok: true, thread: summary(thread) };
|
|
800
|
+
if (
|
|
801
|
+
store
|
|
802
|
+
.threadRuns(thread.id)
|
|
803
|
+
.some((run) => ["open", "claimed", "running"].includes(run.status))
|
|
804
|
+
)
|
|
805
|
+
return {
|
|
806
|
+
ok: false,
|
|
807
|
+
error:
|
|
808
|
+
"an agent is still working in this thread; stop it or wait for it first",
|
|
809
|
+
};
|
|
339
810
|
const moved = worktrees.moved(thread.id);
|
|
340
811
|
if (!moved.ok) return moved;
|
|
341
812
|
store.setGoalFolder(thread.id, where);
|
|
342
|
-
for (const name of Object.keys(thread.goal.sessions || {}))
|
|
343
|
-
|
|
344
|
-
|
|
813
|
+
for (const name of Object.keys(thread.goal.sessions || {}))
|
|
814
|
+
store.setGoalSession(thread.id, name, null);
|
|
815
|
+
for (const name of thread.goal.team)
|
|
816
|
+
if (store.meta(`t3-thread:${thread.id}:${name}`))
|
|
817
|
+
store.meta(`t3-thread:${thread.id}:${name}`, "");
|
|
818
|
+
store.roomPost({
|
|
819
|
+
goalId: thread.id,
|
|
820
|
+
author: "user",
|
|
821
|
+
kind: "note",
|
|
822
|
+
text: `Now working in ${where}.`,
|
|
823
|
+
});
|
|
345
824
|
notifyBoard(store.getTask(thread.id));
|
|
346
825
|
return { ok: true, thread: summary(store.getTask(thread.id)) };
|
|
347
826
|
}
|