tryharness 0.7.22 → 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/README.md +48 -8
- package/assets/opencode-plugin.js +48 -10
- package/assets/pi-extension.ts +106 -26
- package/cli.mjs +1281 -289
- package/lib/activity.mjs +197 -40
- package/lib/adapters.mjs +337 -54
- package/lib/agentnode.mjs +175 -35
- package/lib/atlas.mjs +604 -0
- package/lib/bip39-english.txt +2048 -0
- package/lib/cards.mjs +177 -39
- package/lib/claudemem.mjs +74 -18
- package/lib/commands.mjs +13 -5
- package/lib/context.mjs +628 -0
- package/lib/control-client.mjs +5 -2
- package/lib/coordinator.mjs +281 -56
- package/lib/daemon.mjs +5590 -838
- package/lib/desktop.mjs +149 -35
- package/lib/digest.mjs +81 -0
- package/lib/environment.mjs +180 -0
- package/lib/files.mjs +246 -13
- package/lib/firewall.mjs +288 -0
- package/lib/folders.mjs +223 -57
- package/lib/frames.mjs +333 -0
- package/lib/inference.mjs +663 -0
- package/lib/learn.mjs +182 -0
- package/lib/live.mjs +286 -61
- package/lib/localmodels.mjs +571 -0
- package/lib/mcp-stdio.mjs +912 -103
- package/lib/media.mjs +228 -0
- 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 +190 -32
- package/lib/ocr.mjs +380 -0
- package/lib/onboard.mjs +321 -70
- package/lib/parsers.mjs +132 -41
- package/lib/paths.mjs +33 -10
- package/lib/personal.mjs +77 -0
- package/lib/phones.mjs +398 -51
- package/lib/places.mjs +566 -102
- package/lib/provenance.mjs +19 -4
- package/lib/receipts.mjs +82 -0
- package/lib/redact.mjs +730 -0
- package/lib/runner.mjs +347 -55
- package/lib/runtime-install.mjs +59 -17
- package/lib/screens.mjs +212 -0
- package/lib/sealed.mjs +160 -28
- package/lib/secrets.mjs +18 -5
- package/lib/service.mjs +173 -35
- package/lib/snippets.mjs +140 -0
- package/lib/speech.mjs +993 -0
- package/lib/store.mjs +1678 -301
- package/lib/swarm.mjs +598 -117
- package/lib/t3.mjs +400 -79
- package/lib/trails.mjs +401 -0
- package/lib/ui.mjs +95 -23
- package/lib/vectors.mjs +5 -2
- package/lib/wakes.mjs +284 -68
- package/lib/watches.mjs +362 -0
- package/lib/worktrees.mjs +186 -38
- package/package.json +6 -4
- package/lib/embedder.mjs +0 -156
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,33 +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
|
-
|
|
125
|
-
|
|
276
|
+
// `lastWorked`: where its agent last worked, when Harness picked another folder from the message, for the person to take.
|
|
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
|
+
});
|
|
126
291
|
|
|
127
|
-
const read = (thread) => {
|
|
292
|
+
const read = (thread, hostedCaller = false) => {
|
|
128
293
|
const runs = store.threadRuns(thread.id).map((run) => ({
|
|
129
|
-
id: run.id,
|
|
130
|
-
|
|
131
|
-
|
|
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
|
+
})),
|
|
132
309
|
}));
|
|
133
|
-
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
|
+
};
|
|
134
317
|
};
|
|
135
318
|
|
|
136
319
|
// `to`: the one agent the person wrote to (the desktop's agent list), so their words go as written, with no @name.
|
|
@@ -138,191 +321,467 @@ export function createSwarm({ store, roomAgents, mentionsIn, wakeFor, notifyBoar
|
|
|
138
321
|
const picked = picksIn(store, text, roomAgents);
|
|
139
322
|
if (picked.error) return { ok: false, error: picked.error };
|
|
140
323
|
// A new name in the person's message joins the thread.
|
|
141
|
-
const joining = [
|
|
142
|
-
|
|
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]);
|
|
143
329
|
if (thread.status !== "open") {
|
|
144
330
|
store.setGoalState(thread.id, { status: "open" });
|
|
145
|
-
store.roomPost({
|
|
331
|
+
store.roomPost({
|
|
332
|
+
goalId: thread.id,
|
|
333
|
+
author: "user",
|
|
334
|
+
kind: "note",
|
|
335
|
+
text: "Picked up again by you.",
|
|
336
|
+
});
|
|
146
337
|
}
|
|
147
338
|
const current = store.getTask(thread.id);
|
|
148
|
-
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
|
+
);
|
|
149
348
|
};
|
|
150
349
|
|
|
151
350
|
return function swarmCommand(request, agent) {
|
|
152
351
|
const command = String(request.command);
|
|
153
352
|
if (command === "board.swarm.list") {
|
|
154
|
-
if (agent !== "user" && agent !== "harness")
|
|
155
|
-
|
|
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
|
+
};
|
|
156
364
|
}
|
|
157
365
|
// The swarm's shared memory, for any of the person's agents: what any agent wrote in any thread, and the coding
|
|
158
366
|
// sessions on this machine. A caller limited to its own threads (a hosted agent while the machine is sealed) gets only those.
|
|
159
367
|
if (command === "board.swarm.search") {
|
|
160
|
-
const query = String(request.query || "")
|
|
368
|
+
const query = String(request.query || "")
|
|
369
|
+
.trim()
|
|
370
|
+
.slice(0, 500);
|
|
161
371
|
if (!query) return { ok: false, error: "search needs a query" };
|
|
162
|
-
const mine = request.teamOnly
|
|
163
|
-
|
|
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
|
+
};
|
|
164
383
|
}
|
|
165
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
|
|
166
385
|
// session starts. Open to every caller, so an agent picks a name instead of guessing one.
|
|
167
386
|
if (command === "board.swarm.agents") {
|
|
168
|
-
const agents = swarmCards(
|
|
169
|
-
|
|
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
|
+
};
|
|
170
397
|
}
|
|
171
398
|
// Each coding agent's models as it lists them, the person's choice for it and their routing words (models.mjs). Open
|
|
172
399
|
// to every caller, so an agent names a model that exists: post_task's model, or `@codex:model/effort` in a post.
|
|
173
400
|
if (command === "board.swarm.models") {
|
|
174
401
|
const named = request.target ? resolveMember(request.target) : null;
|
|
175
|
-
if (named && !named.ok)
|
|
402
|
+
if (named && !named.ok)
|
|
403
|
+
return { ok: false, error: `no agent called "${request.target}"` };
|
|
176
404
|
const ids = pickable().filter((id) => !named || id === named.id);
|
|
177
|
-
if (named && !ids.length)
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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();
|
|
183
430
|
}
|
|
184
431
|
// Any of the person's agents may start a thread and stop one it started. The thread carries the agent's trust, so
|
|
185
432
|
// nothing in it runs without the person's Start, and one started through Harness gets no coordinator rounds (they
|
|
186
433
|
// are paid) until the person sets a spend ceiling.
|
|
187
434
|
if (agent !== "user" && command === "board.swarm.start") {
|
|
188
|
-
const text = String(request.text || "")
|
|
435
|
+
const text = String(request.text || "")
|
|
436
|
+
.trim()
|
|
437
|
+
.slice(0, 20_000);
|
|
189
438
|
if (!text) return { ok: false, error: "a thread starts with a message" };
|
|
190
439
|
const folder = request.folder ? String(request.folder) : null;
|
|
191
|
-
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
|
+
};
|
|
192
445
|
const team = new Set(mentionsIn(text, roomAgents));
|
|
193
446
|
const picked = picksIn(store, text, roomAgents);
|
|
194
447
|
if (picked.error) return { ok: false, error: picked.error };
|
|
195
|
-
if (!team.size)
|
|
196
|
-
|
|
197
|
-
|
|
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
|
+
});
|
|
198
470
|
if (request.hostedCaller) store.meta(`hosted-thread:${thread.id}`, "1");
|
|
199
|
-
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
|
+
});
|
|
200
478
|
wakeFor(thread.id, first, agent);
|
|
201
479
|
notifyBoard(store.getTask(thread.id));
|
|
202
|
-
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
|
+
};
|
|
203
485
|
}
|
|
204
486
|
if (agent !== "user" && command === "board.swarm.stop") {
|
|
205
487
|
const thread = store.getTask(String(request.id || ""));
|
|
206
|
-
if (!thread || thread.kind !== "goal" || thread.origin_agent !== agent)
|
|
207
|
-
|
|
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}` };
|
|
208
492
|
stopThreadRuns(thread.id);
|
|
209
493
|
return { ok: true, thread: summary(store.getTask(thread.id)) };
|
|
210
494
|
}
|
|
211
|
-
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
|
+
};
|
|
212
501
|
// Where a thread works, for the composer (folders.mjs): the folders the person works in, any folder on the computer
|
|
213
502
|
// from a typed path, and where a message would work: the folder chosen (`folder`) or the one Harness suggests, and
|
|
214
503
|
// where each coding agent named in it (`text`, `to`, `agents`) runs its turns there.
|
|
215
|
-
if (command === "board.swarm.places")
|
|
504
|
+
if (command === "board.swarm.places")
|
|
505
|
+
return { ok: true, places: folders.known(), home: os.homedir() };
|
|
216
506
|
if (command === "board.swarm.browse") return folders.browse(request.path);
|
|
217
507
|
if (command === "board.swarm.place") {
|
|
218
508
|
const text = String(request.text || "").slice(0, 20_000);
|
|
219
509
|
const to = recipient(request);
|
|
220
|
-
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
|
+
];
|
|
221
521
|
if (request.folder) {
|
|
222
522
|
const folder = String(request.folder);
|
|
223
|
-
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` };
|
|
224
526
|
}
|
|
225
527
|
if (!needsFolder(team)) return placeOf(null, team, null);
|
|
226
|
-
return {
|
|
528
|
+
return {
|
|
529
|
+
pending: folders
|
|
530
|
+
.suggest(text, team)
|
|
531
|
+
.then(({ folder, by, lastWorked }) =>
|
|
532
|
+
placeOf(folder, team, by, lastWorked)
|
|
533
|
+
),
|
|
534
|
+
};
|
|
227
535
|
}
|
|
228
536
|
// The person's own note on one agent's card ("use it for reviews"), told to every other agent; no text clears it.
|
|
229
537
|
if (command === "board.swarm.note") {
|
|
230
538
|
const named = resolveMember(request.target);
|
|
231
|
-
if (!named.ok)
|
|
232
|
-
|
|
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
|
+
};
|
|
233
551
|
}
|
|
234
552
|
// The person's choice of model and effort for one agent: a model it lists, "auto", or "default" for its own settings.
|
|
235
553
|
if (command === "board.swarm.model") {
|
|
236
554
|
const named = resolveMember(request.target);
|
|
237
|
-
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
|
+
};
|
|
238
560
|
const set = setChoice(store, named.id, request.model, request.effort);
|
|
239
561
|
return set.ok ? { ok: true, agent: named.id, choice: set.choice } : set;
|
|
240
562
|
}
|
|
241
563
|
// The person's words on how Auto picks models for swarm work; no text goes back to the default words.
|
|
242
|
-
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
|
+
};
|
|
243
570
|
// The coordinator's record: what each round saw, chose and cost. The evidence for its limits.
|
|
244
|
-
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
|
+
};
|
|
245
579
|
if (command === "board.swarm.coordinator") {
|
|
246
|
-
if (request.mode === "harness" || request.mode === "local")
|
|
580
|
+
if (request.mode === "harness" || request.mode === "local")
|
|
581
|
+
coordinator.setMode(request.mode);
|
|
247
582
|
return { ok: true, mode: coordinator.mode() };
|
|
248
583
|
}
|
|
249
584
|
if (command === "board.swarm.stop_all") {
|
|
250
|
-
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
|
+
});
|
|
251
592
|
return { ok: true, stopped };
|
|
252
593
|
}
|
|
253
594
|
if (command === "board.swarm.start") {
|
|
254
|
-
const text = String(request.text || "")
|
|
595
|
+
const text = String(request.text || "")
|
|
596
|
+
.trim()
|
|
597
|
+
.slice(0, 20_000);
|
|
255
598
|
if (!text) return { ok: false, error: "a thread starts with a message" };
|
|
256
|
-
const from =
|
|
599
|
+
const from =
|
|
600
|
+
request.continueSession && typeof request.continueSession === "object"
|
|
601
|
+
? request.continueSession
|
|
602
|
+
: null;
|
|
257
603
|
let folder = request.folder ? String(request.folder) : null;
|
|
258
|
-
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
|
+
};
|
|
259
609
|
const to = recipient(request);
|
|
260
|
-
if (to === false)
|
|
261
|
-
|
|
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
|
+
]);
|
|
262
619
|
const picked = picksIn(store, text, roomAgents);
|
|
263
620
|
if (picked.error) return { ok: false, error: picked.error };
|
|
264
621
|
if (from) {
|
|
265
|
-
const agentName = String(from.agent || ""),
|
|
266
|
-
|
|
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
|
+
};
|
|
267
629
|
const session = store.read({ agent: agentName, sessionId });
|
|
268
630
|
if (!session) return { ok: false, error: "session not found" };
|
|
269
|
-
const already = store.threadForSession(
|
|
631
|
+
const already = store.threadForSession(
|
|
632
|
+
agentName,
|
|
633
|
+
nativeSessionId(agentName, sessionId)
|
|
634
|
+
);
|
|
270
635
|
if (already) return post(store.getTask(already), text);
|
|
271
|
-
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
|
+
};
|
|
272
642
|
team.add(agentName);
|
|
273
643
|
folder ??= isDir(session.cwd) ? session.cwd : null;
|
|
274
644
|
}
|
|
275
645
|
// Where the work happens: the folder the person chose, else one picked from what they wrote, else where its agent
|
|
276
646
|
// last worked (folders.mjs); never the home folder or a hidden one.
|
|
277
647
|
const begin = (where) => {
|
|
278
|
-
const objective =
|
|
279
|
-
|
|
280
|
-
|
|
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
|
+
}
|
|
281
673
|
const opened = store.getTask(thread.id);
|
|
282
|
-
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
|
+
});
|
|
283
681
|
wakeFor(thread.id, first, "user");
|
|
284
682
|
notifyBoard(store.getTask(thread.id));
|
|
285
683
|
return { ok: true, thread: summary(store.getTask(thread.id)) };
|
|
286
684
|
};
|
|
287
|
-
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
|
+
};
|
|
288
692
|
}
|
|
289
693
|
const thread = store.getTask(String(request.id || ""));
|
|
290
|
-
if (!thread || thread.kind !== "goal")
|
|
291
|
-
|
|
292
|
-
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
|
+
);
|
|
293
703
|
if (command === "board.swarm.merge" || command === "board.swarm.discard") {
|
|
294
704
|
// Both change the person's repository, so both wait for the person, and never while an agent is changing files.
|
|
295
|
-
if (
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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
|
+
});
|
|
299
736
|
notifyBoard(store.getTask(thread.id));
|
|
300
|
-
return done.ok
|
|
737
|
+
return done.ok
|
|
738
|
+
? { ok: true, thread: summary(store.getTask(thread.id)) }
|
|
739
|
+
: done;
|
|
301
740
|
}
|
|
302
741
|
// The person's Start for one run in the thread that waits for it.
|
|
303
742
|
if (command === "board.swarm.start_run") {
|
|
304
|
-
const run = store
|
|
305
|
-
|
|
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" };
|
|
306
748
|
const started = authorizeRun(run.id);
|
|
307
|
-
return started?.ok === false
|
|
749
|
+
return started?.ok === false
|
|
750
|
+
? started
|
|
751
|
+
: { ok: true, thread: summary(store.getTask(thread.id)) };
|
|
308
752
|
}
|
|
309
753
|
if (command === "board.swarm.post") {
|
|
310
|
-
const text = String(request.text || "")
|
|
754
|
+
const text = String(request.text || "")
|
|
755
|
+
.trim()
|
|
756
|
+
.slice(0, 20_000);
|
|
311
757
|
if (!text) return { ok: false, error: "a message needs text" };
|
|
312
758
|
const to = recipient(request);
|
|
313
|
-
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
|
+
};
|
|
314
764
|
const posted = post(thread, text, to);
|
|
315
|
-
return posted.ok
|
|
765
|
+
return posted.ok
|
|
766
|
+
? { ...posted, thread: summary(store.getTask(thread.id)) }
|
|
767
|
+
: posted;
|
|
316
768
|
}
|
|
317
769
|
if (command === "board.swarm.stop") {
|
|
318
|
-
if (thread.status !== "open")
|
|
770
|
+
if (thread.status !== "open")
|
|
771
|
+
return { ok: false, error: `the thread is ${thread.status}` };
|
|
319
772
|
stopThreadRuns(thread.id);
|
|
320
773
|
return { ok: true, thread: summary(store.getTask(thread.id)) };
|
|
321
774
|
}
|
|
322
775
|
if (command === "board.swarm.resume") {
|
|
323
|
-
if (thread.status !== "paused")
|
|
776
|
+
if (thread.status !== "paused")
|
|
777
|
+
return { ok: false, error: `the thread is ${thread.status}` };
|
|
324
778
|
store.setGoalState(thread.id, { status: "open" });
|
|
325
|
-
store.roomPost({
|
|
779
|
+
store.roomPost({
|
|
780
|
+
goalId: thread.id,
|
|
781
|
+
author: "user",
|
|
782
|
+
kind: "note",
|
|
783
|
+
text: "Resumed by you.",
|
|
784
|
+
});
|
|
326
785
|
notifyBoard(store.getTask(thread.id));
|
|
327
786
|
return { ok: true, thread: summary(store.getTask(thread.id)) };
|
|
328
787
|
}
|
|
@@ -330,16 +789,38 @@ export function createSwarm({ store, roomAgents, mentionsIn, wakeFor, notifyBoar
|
|
|
330
789
|
// and a T3 thread belong to the folder they ran in.
|
|
331
790
|
if (command === "board.swarm.folder") {
|
|
332
791
|
const folder = String(request.folder || "");
|
|
333
|
-
if (!isDir(folder))
|
|
792
|
+
if (!isDir(folder))
|
|
793
|
+
return {
|
|
794
|
+
ok: false,
|
|
795
|
+
error: `${folder} is not a folder on this computer`,
|
|
796
|
+
};
|
|
334
797
|
const where = path.resolve(folder);
|
|
335
|
-
if (where === thread.goal.workspace)
|
|
336
|
-
|
|
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
|
+
};
|
|
337
810
|
const moved = worktrees.moved(thread.id);
|
|
338
811
|
if (!moved.ok) return moved;
|
|
339
812
|
store.setGoalFolder(thread.id, where);
|
|
340
|
-
for (const name of Object.keys(thread.goal.sessions || {}))
|
|
341
|
-
|
|
342
|
-
|
|
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
|
+
});
|
|
343
824
|
notifyBoard(store.getTask(thread.id));
|
|
344
825
|
return { ok: true, thread: summary(store.getTask(thread.id)) };
|
|
345
826
|
}
|