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.
Files changed (64) hide show
  1. package/README.md +48 -8
  2. package/assets/opencode-plugin.js +48 -10
  3. package/assets/pi-extension.ts +106 -26
  4. package/cli.mjs +1281 -289
  5. package/lib/activity.mjs +197 -40
  6. package/lib/adapters.mjs +337 -54
  7. package/lib/agentnode.mjs +175 -35
  8. package/lib/atlas.mjs +604 -0
  9. package/lib/bip39-english.txt +2048 -0
  10. package/lib/cards.mjs +177 -39
  11. package/lib/claudemem.mjs +74 -18
  12. package/lib/commands.mjs +13 -5
  13. package/lib/context.mjs +628 -0
  14. package/lib/control-client.mjs +5 -2
  15. package/lib/coordinator.mjs +281 -56
  16. package/lib/daemon.mjs +5590 -838
  17. package/lib/desktop.mjs +149 -35
  18. package/lib/digest.mjs +81 -0
  19. package/lib/environment.mjs +180 -0
  20. package/lib/files.mjs +246 -13
  21. package/lib/firewall.mjs +288 -0
  22. package/lib/folders.mjs +223 -57
  23. package/lib/frames.mjs +333 -0
  24. package/lib/inference.mjs +663 -0
  25. package/lib/learn.mjs +182 -0
  26. package/lib/live.mjs +286 -61
  27. package/lib/localmodels.mjs +571 -0
  28. package/lib/mcp-stdio.mjs +912 -103
  29. package/lib/media.mjs +228 -0
  30. package/lib/members.json +1 -1
  31. package/lib/members.mjs +35 -9
  32. package/lib/models.mjs +342 -80
  33. package/lib/network.mjs +201 -42
  34. package/lib/notify.mjs +49 -12
  35. package/lib/nudge.mjs +190 -32
  36. package/lib/ocr.mjs +380 -0
  37. package/lib/onboard.mjs +321 -70
  38. package/lib/parsers.mjs +132 -41
  39. package/lib/paths.mjs +33 -10
  40. package/lib/personal.mjs +77 -0
  41. package/lib/phones.mjs +398 -51
  42. package/lib/places.mjs +566 -102
  43. package/lib/provenance.mjs +19 -4
  44. package/lib/receipts.mjs +82 -0
  45. package/lib/redact.mjs +730 -0
  46. package/lib/runner.mjs +347 -55
  47. package/lib/runtime-install.mjs +59 -17
  48. package/lib/screens.mjs +212 -0
  49. package/lib/sealed.mjs +160 -28
  50. package/lib/secrets.mjs +18 -5
  51. package/lib/service.mjs +173 -35
  52. package/lib/snippets.mjs +140 -0
  53. package/lib/speech.mjs +993 -0
  54. package/lib/store.mjs +1678 -301
  55. package/lib/swarm.mjs +598 -117
  56. package/lib/t3.mjs +400 -79
  57. package/lib/trails.mjs +401 -0
  58. package/lib/ui.mjs +95 -23
  59. package/lib/vectors.mjs +5 -2
  60. package/lib/wakes.mjs +284 -68
  61. package/lib/watches.mjs +362 -0
  62. package/lib/worktrees.mjs +186 -38
  63. package/package.json +6 -4
  64. package/lib/embedder.mjs +0 -156
@@ -11,14 +11,19 @@ import { MACHINE_RUNNERS, MEMBERS, nameOf } from "./members.mjs";
11
11
 
12
12
  const CODING = new Set(MACHINE_RUNNERS);
13
13
  // The agents with a read-only switch (runner.mjs, review in the roster): only they can be handed a review.
14
- const REVIEWERS = new Set(MEMBERS.filter((entry) => entry.review).map((entry) => entry.id));
14
+ const REVIEWERS = new Set(
15
+ MEMBERS.filter((entry) => entry.review).map((entry) => entry.id)
16
+ );
15
17
  const TRANSCRIPT_POSTS = 30;
16
18
  const QUIET_KINDS = new Set(["note", "held", "round"]);
17
19
  // The coordinator's own posts are Harness's; Harness's answer to work it was given is a result like any agent's.
18
- const ownPost = (thread, post) => post.author === thread.goal.facilitator && post.kind !== "result";
20
+ const ownPost = (thread, post) =>
21
+ post.author === thread.goal.facilitator && post.kind !== "result";
19
22
  // Harness's part with no Harness window open waits on the board for one (daemon.mjs offerToServer); the thread goes on.
20
- const waitsForWindow = (run) => run.status === "open" && run.target_agent === "harness";
21
- const going = (run) => ["open", "claimed", "running"].includes(run.status) && !waitsForWindow(run);
23
+ const waitsForWindow = (run) =>
24
+ run.status === "open" && run.target_agent === "harness";
25
+ const going = (run) =>
26
+ ["open", "claimed", "running"].includes(run.status) && !waitsForWindow(run);
22
27
 
23
28
  // The privacy setting: the round's facts go to an agent on this machine with its tools off, which answers in the
24
29
  // same shape Harness does. Nothing reaches Harness's server.
@@ -32,69 +37,199 @@ If the same attempt or failure keeps coming back, say so in loop and ask the nex
32
37
  function localDecision(agent, facts) {
33
38
  return new Promise((resolve, reject) => {
34
39
  const prompt = `${LOCAL_PROMPT}\n\n${JSON.stringify(facts)}`;
35
- const invocation = agent === "codex"
36
- ? { command: "codex", args: ["exec", "--json", "--sandbox", "read-only", "--skip-git-repo-check", "-"], stdin: prompt }
37
- : { command: "claude", args: ["-p", prompt, "--output-format", "json", "--tools", ""] };
40
+ const invocation =
41
+ agent === "codex"
42
+ ? {
43
+ command: "codex",
44
+ args: [
45
+ "exec",
46
+ "--json",
47
+ "--sandbox",
48
+ "read-only",
49
+ "--skip-git-repo-check",
50
+ "-",
51
+ ],
52
+ stdin: prompt,
53
+ }
54
+ : {
55
+ command: "claude",
56
+ args: ["-p", prompt, "--output-format", "json", "--tools", ""],
57
+ };
38
58
  let child;
39
- try { child = spawn(invocation.command, invocation.args, { cwd: os.tmpdir() }); } catch (error) { reject(error); return; }
59
+ try {
60
+ child = spawn(invocation.command, invocation.args, { cwd: os.tmpdir() });
61
+ } catch (error) {
62
+ reject(error);
63
+ return;
64
+ }
65
+ // An agent that exits before reading the prompt breaks the pipe; its exit code rejects below.
66
+ child.stdin.on("error", () => {});
40
67
  child.stdin.end(invocation.stdin ?? "");
41
68
  let out = "";
42
- child.stdout.on("data", (chunk) => { out += chunk; });
69
+ child.stdout.on("data", (chunk) => {
70
+ out += chunk;
71
+ });
43
72
  child.on("error", reject);
44
73
  child.on("close", (code) => {
45
- if (code !== 0) { reject(new Error(`${agent} exited with code ${code}`)); return; }
74
+ if (code !== 0) {
75
+ reject(new Error(`${agent} exited with code ${code}`));
76
+ return;
77
+ }
46
78
  // Claude answers one JSON object with its text in `result`; Codex streams events, its words in agent_message.
47
79
  let text = "";
48
80
  for (const line of out.split("\n")) {
49
81
  try {
50
82
  const event = JSON.parse(line);
51
83
  if (typeof event.result === "string") text = event.result;
52
- if (event.type === "item.completed" && event.item?.type === "agent_message") text = event.item.text;
84
+ if (
85
+ event.type === "item.completed" &&
86
+ event.item?.type === "agent_message"
87
+ )
88
+ text = event.item.text;
53
89
  } catch {}
54
90
  }
55
91
  const match = text.match(/\{[\s\S]*\}/);
56
- try { resolve(JSON.parse(match ? match[0] : text)); } catch { reject(new Error(`${agent} did not answer in the agreed shape`)); }
92
+ try {
93
+ resolve(JSON.parse(match ? match[0] : text));
94
+ } catch {
95
+ reject(new Error(`${agent} did not answer in the agreed shape`));
96
+ }
57
97
  });
58
98
  });
59
99
  }
60
100
 
61
- export function createCoordinator({ store, mode, askHarness, connected, serverHas, roster, handOn, localAgents, pauseGoal, notifyBoard, searchContext, log }) {
101
+ export function createCoordinator({
102
+ store,
103
+ mode,
104
+ askHarness,
105
+ connected,
106
+ serverHas,
107
+ roster,
108
+ handOn,
109
+ localAgents,
110
+ pauseGoal,
111
+ notifyBoard,
112
+ searchContext,
113
+ log,
114
+ frame = () => null,
115
+ }) {
62
116
  const busy = new Set();
63
117
  const again = new Set();
64
- const stateOf = (goalId) => JSON.parse(store.meta(`coordinator:${goalId}`) || '{"lastSeenSeq":0,"rounds":0,"withoutProgress":0,"lastRoundAt":null,"lastError":null}');
65
- const saveState = (goalId, state) => store.meta(`coordinator:${goalId}`, JSON.stringify(state));
118
+ const stateOf = (goalId) =>
119
+ JSON.parse(
120
+ store.meta(`coordinator:${goalId}`) ||
121
+ '{"lastSeenSeq":0,"rounds":0,"withoutProgress":0,"lastRoundAt":null,"lastError":null}'
122
+ );
123
+ const saveState = (goalId, state) =>
124
+ store.meta(`coordinator:${goalId}`, JSON.stringify(state));
66
125
 
67
126
  // What this machine knows that the round needs, whole: the transcript, who can run, and progress since last time.
68
127
  const facts = async (thread, state) => {
69
- const posts = store.roomPosts(thread.id, 0, 500).filter((post) => !QUIET_KINDS.has(post.kind));
70
- const transcript = posts.slice(-TRANSCRIPT_POSTS).map((post) => ({ seq: post.seq, author: post.author, kind: post.kind, text: post.text }));
128
+ const posts = store
129
+ .roomPosts(thread.id, 0, 500)
130
+ .filter((post) => !QUIET_KINDS.has(post.kind));
131
+ const transcript = posts.slice(-TRANSCRIPT_POSTS).map((post) => ({
132
+ seq: post.seq,
133
+ author: post.author,
134
+ kind: post.kind,
135
+ text: post.text,
136
+ }));
71
137
  const runs = store.threadRuns(thread.id);
72
138
  const since = state.lastRoundAt ?? "";
73
- const runsFinished = runs.filter((run) => run.reported_at && run.reported_at > since).length;
74
- const newResults = posts.filter((post) => post.seq > state.lastSeenSeq && post.author !== "user" && !ownPost(thread, post)).length;
75
- const failedJobs = runs.filter((run) => run.status === "failed").slice(-5).map((run) => ({ agent: run.target_agent || "agent", objective: run.objective.slice(0, 500), result: String(run.result || "").slice(0, 4_000) }));
139
+ const runsFinished = runs.filter(
140
+ (run) => run.reported_at && run.reported_at > since
141
+ ).length;
142
+ const newResults = posts.filter(
143
+ (post) =>
144
+ post.seq > state.lastSeenSeq &&
145
+ post.author !== "user" &&
146
+ !ownPost(thread, post)
147
+ ).length;
148
+ const failedJobs = runs
149
+ .filter((run) => run.status === "failed")
150
+ .slice(-5)
151
+ .map((run) => ({
152
+ agent: run.target_agent || "agent",
153
+ objective: run.objective.slice(0, 500),
154
+ result: String(run.result || "").slice(0, 4_000),
155
+ }));
76
156
  const waiting = runs.filter(waitsForWindow);
77
157
  // Every agent the person can ask, each with its card (cards.mjs); a coding agent only where the thread has a folder for it
78
158
  // to work in, and Harness not while its part here waits for a window.
79
- const candidates = roster().filter((candidate) => (!CODING.has(candidate.id) || thread.goal.workspace) && !(candidate.id === "harness" && waiting.length));
159
+ // Each card's receipts go only to a Harness that takes them (the same release as frames); an older one would refuse the round.
160
+ const framesTaken = mode() === "local" || serverHas("swarm_frame");
161
+ const candidates = roster()
162
+ .filter(
163
+ (candidate) =>
164
+ (!CODING.has(candidate.id) || thread.goal.workspace) &&
165
+ !(candidate.id === "harness" && waiting.length)
166
+ )
167
+ .map((candidate) =>
168
+ framesTaken || !candidate.record?.checks
169
+ ? candidate
170
+ : { ...candidate, record: { ...candidate.record, checks: undefined } }
171
+ );
80
172
  // Earlier words close in meaning to the newest post that the transcript no longer carries, from this thread and the
81
173
  // person's sessions on this machine.
82
- const newest = posts.filter((post) => post.seq > state.lastSeenSeq).map((post) => post.text).join("\n").slice(0, 2_000);
174
+ const newest = posts
175
+ .filter((post) => post.seq > state.lastSeenSeq)
176
+ .map((post) => post.text)
177
+ .join("\n")
178
+ .slice(0, 2_000);
83
179
  const shown = new Set(transcript.map((post) => post.text));
84
180
  let related = [];
85
181
  if (newest.trim()) {
86
- const found = await searchContext(thread.id, newest, 6).catch(() => ({ entries: [] }));
87
- related = found.entries.filter((entry) => !shown.has(entry.text)).slice(0, 4).map((entry) => ({ source: `this thread, ${entry.author}`, text: entry.text.slice(0, 4_000) }));
88
- for (const hit of store.search({ query: newest.split(/\s+/).slice(0, 12).join(" "), limit: 3 })) {
89
- if (hit.excerpt) related.push({ source: `${nameOf(hit.agent)} session "${String(hit.title || "").slice(0, 120)}"`, text: String(hit.excerpt).slice(0, 4_000) });
182
+ const found = await searchContext(thread.id, newest, 6).catch(() => ({
183
+ entries: [],
184
+ }));
185
+ related = found.entries
186
+ .filter((entry) => !shown.has(entry.text))
187
+ .slice(0, 4)
188
+ .map((entry) => ({
189
+ source: `this thread, ${entry.author}`,
190
+ text: entry.text.slice(0, 4_000),
191
+ }));
192
+ for (const hit of store.search({
193
+ query: newest.split(/\s+/).slice(0, 12).join(" "),
194
+ limit: 3,
195
+ })) {
196
+ if (hit.excerpt)
197
+ related.push({
198
+ source: `${nameOf(hit.agent)} session "${String(hit.title || "").slice(0, 120)}"`,
199
+ text: String(hit.excerpt).slice(0, 4_000),
200
+ });
90
201
  }
91
202
  }
203
+ // The thread's task frame (frames.mjs): each run's receipt, other threads in the repository, its state.
204
+ const framed = frame(thread.id);
92
205
  return {
93
- thread: { id: thread.id, title: thread.objective, folder: thread.goal.workspace, agents: thread.goal.team.filter((agent) => agent !== thread.goal.facilitator) },
94
- transcript, lastSeenSeq: state.lastSeenSeq, candidates,
95
- progress: { runsFinished, newResults, stillRunning: runs.filter(going).length, roundsWithoutProgress: state.withoutProgress, failedJobs,
206
+ ...(framed ? { frame: framed } : {}),
207
+ thread: {
208
+ id: thread.id,
209
+ title: thread.objective,
210
+ folder: thread.goal.workspace,
211
+ agents: thread.goal.team.filter(
212
+ (agent) => agent !== thread.goal.facilitator
213
+ ),
214
+ },
215
+ transcript,
216
+ lastSeenSeq: state.lastSeenSeq,
217
+ candidates,
218
+ progress: {
219
+ runsFinished,
220
+ newResults,
221
+ stillRunning: runs.filter(going).length,
222
+ roundsWithoutProgress: state.withoutProgress,
223
+ failedJobs,
96
224
  // A Harness that does not know the field would refuse the round: it goes only to one that takes Harness's tasks.
97
- ...(waiting.length && (mode() === "local" || serverHas("harness_task")) ? { waitingForWindow: waiting.slice(-10).map((run) => ({ objective: run.objective.slice(0, 500) })) } : {}) },
225
+ ...(waiting.length && (mode() === "local" || serverHas("harness_task"))
226
+ ? {
227
+ waitingForWindow: waiting
228
+ .slice(-10)
229
+ .map((run) => ({ objective: run.objective.slice(0, 500) })),
230
+ }
231
+ : {}),
232
+ },
98
233
  related: related.slice(0, 8),
99
234
  };
100
235
  };
@@ -102,16 +237,52 @@ export function createCoordinator({ store, mode, askHarness, connected, serverHa
102
237
  // Carry a decision out under the thread's rules. A brief from the coordinator is agent-written work inside the
103
238
  // thread, so it runs on the thread's authority the way a goal's work does; everything else only posts.
104
239
  const apply = (thread, decision, coordinator, candidates) => {
105
- if (decision.openItem) store.roomPost({ goalId: thread.id, author: coordinator, kind: "open_item", text: String(decision.openItem).slice(0, 2_000) });
106
- if (decision.action === "pause") { pauseGoal(thread.id, String(decision.text || "the coordinator could not go on without you").slice(0, 300)); return; }
107
- if (decision.action === "ask_person" && decision.text) store.roomPost({ goalId: thread.id, author: coordinator, kind: "question", text: String(decision.text).slice(0, 4_000) });
108
- if (decision.action === "hand_on" && candidates.some((candidate) => candidate.id === decision.agent) && decision.text) {
109
- if (!thread.goal.team.includes(decision.agent)) store.setGoalTeam(thread.id, [...thread.goal.team, decision.agent]);
110
- const review = decision.mode === "review" && REVIEWERS.has(decision.agent);
240
+ if (decision.openItem)
241
+ store.roomPost({
242
+ goalId: thread.id,
243
+ author: coordinator,
244
+ kind: "open_item",
245
+ text: String(decision.openItem).slice(0, 2_000),
246
+ });
247
+ if (decision.action === "pause") {
248
+ pauseGoal(
249
+ thread.id,
250
+ String(
251
+ decision.text || "the coordinator could not go on without you"
252
+ ).slice(0, 300)
253
+ );
254
+ return;
255
+ }
256
+ if (decision.action === "ask_person" && decision.text)
257
+ store.roomPost({
258
+ goalId: thread.id,
259
+ author: coordinator,
260
+ kind: "question",
261
+ text: String(decision.text).slice(0, 4_000),
262
+ });
263
+ if (
264
+ decision.action === "hand_on" &&
265
+ candidates.some((candidate) => candidate.id === decision.agent) &&
266
+ decision.text
267
+ ) {
268
+ if (!thread.goal.team.includes(decision.agent))
269
+ store.setGoalTeam(thread.id, [...thread.goal.team, decision.agent]);
270
+ const review =
271
+ decision.mode === "review" && REVIEWERS.has(decision.agent);
111
272
  const text = String(decision.text).slice(0, 4_000);
112
273
  // The brief is posted for everyone to read and the agent is woken here, so a review runs with its read-only switch.
113
- store.roomPost({ goalId: thread.id, author: coordinator, kind: "post", text: `@${decision.agent} ${review ? "(review, no changes) " : ""}${text}` });
114
- handOn(thread.id, decision.agent, `${coordinator} asked you: ${text.slice(0, 300)}`, { review, words: text });
274
+ store.roomPost({
275
+ goalId: thread.id,
276
+ author: coordinator,
277
+ kind: "post",
278
+ text: `@${decision.agent} ${review ? "(review, no changes) " : ""}${text}`,
279
+ });
280
+ handOn(
281
+ thread.id,
282
+ decision.agent,
283
+ `${coordinator} asked you: ${text.slice(0, 300)}`,
284
+ { review, words: text }
285
+ );
115
286
  }
116
287
  notifyBoard(store.getTask(thread.id));
117
288
  };
@@ -120,52 +291,106 @@ export function createCoordinator({ store, mode, askHarness, connected, serverHa
120
291
  const thread = store.getTask(goalId);
121
292
  if (!thread || thread.kind !== "goal" || thread.status !== "open") return;
122
293
  const state = stateOf(goalId);
123
- const posts = store.roomPosts(goalId, state.lastSeenSeq, 500).filter((post) => !QUIET_KINDS.has(post.kind) && !ownPost(thread, post));
294
+ const posts = store
295
+ .roomPosts(goalId, state.lastSeenSeq, 500)
296
+ .filter((post) => !QUIET_KINDS.has(post.kind) && !ownPost(thread, post));
124
297
  const active = store.threadRuns(goalId).some(going);
125
298
  if (!posts.length || active) return;
126
299
  // A task's own room is coordinated once it becomes a conversation: someone posted in it, not only the task and its result.
127
- if (store.meta(`task-room:${goalId}`) && !store.roomPosts(goalId, 0, 500).some((post) => post.kind === "post")) return;
300
+ if (
301
+ store.meta(`task-room:${goalId}`) &&
302
+ !store.roomPosts(goalId, 0, 500).some((post) => post.kind === "post")
303
+ )
304
+ return;
128
305
  // A thread started through Harness by an agent that runs elsewhere gets no rounds until the person sets a spend ceiling.
129
306
  if (store.meta(`hosted-thread:${goalId}`)) return;
130
307
  const local = mode() === "local";
131
308
  if (!local && !connected()) return;
132
- const localAgent = local ? localAgents().find((agent) => agent === "claude-code" || agent === "codex") : null;
309
+ const localAgent = local
310
+ ? localAgents().find(
311
+ (agent) => agent === "claude-code" || agent === "codex"
312
+ )
313
+ : null;
133
314
  if (local && !localAgent) return;
134
315
  const gathered = await facts(thread, state);
135
- const lastSeq = store.roomPosts(goalId, 0, 500).at(-1)?.seq ?? state.lastSeenSeq;
136
- const progressed = gathered.progress.runsFinished > 0 || gathered.progress.newResults > 0;
316
+ const lastSeq =
317
+ store.roomPosts(goalId, 0, 500).at(-1)?.seq ?? state.lastSeenSeq;
318
+ const progressed =
319
+ gathered.progress.runsFinished > 0 || gathered.progress.newResults > 0;
137
320
  const startedAt = Date.now();
138
- let decision = null, error = null;
321
+ let decision = null,
322
+ error = null;
139
323
  try {
140
324
  if (local) decision = await localDecision(localAgent, gathered);
141
325
  else {
142
326
  // No deadline of its own: the round ends when Harness answers or the connection drops.
143
327
  const reply = await askHarness("coordinate", gathered, null);
144
- if (!reply.ok) throw new Error(reply.error || "Harness did not coordinate this round");
328
+ if (!reply.ok)
329
+ throw new Error(
330
+ reply.error || "Harness did not coordinate this round"
331
+ );
145
332
  decision = reply.result ?? reply;
146
333
  }
147
- } catch (failure) { error = failure.message; }
148
- const next = { ...state, lastSeenSeq: lastSeq, rounds: state.rounds + 1, withoutProgress: progressed ? 0 : state.withoutProgress + 1, lastRoundAt: new Date().toISOString(), lastError: error };
334
+ } catch (failure) {
335
+ error = failure.message;
336
+ }
337
+ const next = {
338
+ ...state,
339
+ lastSeenSeq: lastSeq,
340
+ rounds: state.rounds + 1,
341
+ withoutProgress: progressed ? 0 : state.withoutProgress + 1,
342
+ lastRoundAt: new Date().toISOString(),
343
+ lastError: error,
344
+ };
149
345
  saveState(goalId, next);
150
- store.recordRound({ goalId, coordinator: local ? localAgent : "harness", action: decision?.action ?? null, agent: decision?.agent ?? null, loop: decision?.loop ?? null,
151
- judgments: decision?.judgments ?? null, progress: gathered.progress, estimatedMicroUsd: decision?.estimatedMicroUsd ?? null, elapsedMs: Date.now() - startedAt, error });
346
+ store.recordRound({
347
+ goalId,
348
+ coordinator: local ? localAgent : "harness",
349
+ action: decision?.action ?? null,
350
+ agent: decision?.agent ?? null,
351
+ loop: decision?.loop ?? null,
352
+ judgments: decision?.judgments ?? null,
353
+ progress: gathered.progress,
354
+ estimatedMicroUsd: decision?.estimatedMicroUsd ?? null,
355
+ elapsedMs: Date.now() - startedAt,
356
+ error,
357
+ });
152
358
  if (error) {
153
359
  log(`coordinator round for ${goalId}: ${error}`);
154
- if (error !== state.lastError) store.roomPost({ goalId, author: thread.goal.facilitator, kind: "note", text: `Harness could not coordinate: ${error}` });
360
+ if (error !== state.lastError)
361
+ store.roomPost({
362
+ goalId,
363
+ author: thread.goal.facilitator,
364
+ kind: "note",
365
+ text: `Harness could not coordinate: ${error}`,
366
+ });
155
367
  notifyBoard(store.getTask(goalId));
156
368
  return;
157
369
  }
158
- apply(store.getTask(goalId), decision, thread.goal.facilitator, gathered.candidates);
370
+ apply(
371
+ store.getTask(goalId),
372
+ decision,
373
+ thread.goal.facilitator,
374
+ gathered.candidates
375
+ );
159
376
  };
160
377
 
161
378
  /** A thread changed: run a round once it is quiet; one at a time per thread, again if more came in meanwhile. */
162
379
  const poke = (goalId) => {
163
- if (busy.has(goalId)) { again.add(goalId); return; }
380
+ if (busy.has(goalId)) {
381
+ again.add(goalId);
382
+ return;
383
+ }
164
384
  busy.add(goalId);
165
- setImmediate(() => void round(goalId).catch((failure) => log(`coordinator ${goalId}: ${failure.message}`)).finally(() => {
166
- busy.delete(goalId);
167
- if (again.delete(goalId)) poke(goalId);
168
- }));
385
+ setImmediate(
386
+ () =>
387
+ void round(goalId)
388
+ .catch((failure) => log(`coordinator ${goalId}: ${failure.message}`))
389
+ .finally(() => {
390
+ busy.delete(goalId);
391
+ if (again.delete(goalId)) poke(goalId);
392
+ })
393
+ );
169
394
  };
170
395
  return { poke, state: stateOf };
171
396
  }