tryharness 0.7.23 → 0.7.26

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 (61) hide show
  1. package/assets/opencode-plugin.js +48 -11
  2. package/assets/pi-extension.ts +105 -26
  3. package/cli.mjs +1238 -322
  4. package/lib/activity.mjs +197 -40
  5. package/lib/adapters.mjs +337 -54
  6. package/lib/agentnode.mjs +175 -35
  7. package/lib/atlas.mjs +383 -83
  8. package/lib/cards.mjs +174 -40
  9. package/lib/claudemem.mjs +74 -18
  10. package/lib/commands.mjs +13 -5
  11. package/lib/context.mjs +409 -71
  12. package/lib/control-client.mjs +5 -2
  13. package/lib/coordinator.mjs +274 -57
  14. package/lib/daemon.mjs +5268 -1024
  15. package/lib/desktop.mjs +149 -35
  16. package/lib/digest.mjs +54 -13
  17. package/lib/environment.mjs +103 -21
  18. package/lib/files.mjs +246 -13
  19. package/lib/firewall.mjs +169 -35
  20. package/lib/folders.mjs +179 -36
  21. package/lib/frames.mjs +214 -40
  22. package/lib/inference.mjs +463 -126
  23. package/lib/learn.mjs +78 -16
  24. package/lib/live.mjs +286 -61
  25. package/lib/localmodels.mjs +424 -83
  26. package/lib/mcp-stdio.mjs +868 -151
  27. package/lib/media.mjs +138 -28
  28. package/lib/members.json +1 -1
  29. package/lib/members.mjs +35 -9
  30. package/lib/models.mjs +342 -80
  31. package/lib/network.mjs +201 -42
  32. package/lib/notify.mjs +49 -12
  33. package/lib/nudge.mjs +184 -33
  34. package/lib/ocr.mjs +223 -60
  35. package/lib/onboard.mjs +321 -70
  36. package/lib/parsers.mjs +132 -41
  37. package/lib/paths.mjs +33 -10
  38. package/lib/personal.mjs +51 -13
  39. package/lib/phones.mjs +384 -60
  40. package/lib/places.mjs +555 -107
  41. package/lib/provenance.mjs +19 -4
  42. package/lib/receipts.mjs +51 -11
  43. package/lib/redact.mjs +512 -116
  44. package/lib/runner.mjs +345 -55
  45. package/lib/runtime-install.mjs +55 -14
  46. package/lib/screens.mjs +154 -38
  47. package/lib/sealed.mjs +157 -28
  48. package/lib/secrets.mjs +18 -5
  49. package/lib/service.mjs +170 -34
  50. package/lib/snippets.mjs +94 -18
  51. package/lib/speech.mjs +688 -175
  52. package/lib/store.mjs +1654 -307
  53. package/lib/swarm.mjs +596 -117
  54. package/lib/t3.mjs +400 -79
  55. package/lib/trails.mjs +285 -61
  56. package/lib/ui.mjs +95 -23
  57. package/lib/vectors.mjs +5 -2
  58. package/lib/wakes.mjs +284 -68
  59. package/lib/watches.mjs +247 -48
  60. package/lib/worktrees.mjs +186 -38
  61. package/package.json +3 -3
@@ -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,77 +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
+ }
40
65
  // An agent that exits before reading the prompt breaks the pipe; its exit code rejects below.
41
66
  child.stdin.on("error", () => {});
42
67
  child.stdin.end(invocation.stdin ?? "");
43
68
  let out = "";
44
- child.stdout.on("data", (chunk) => { out += chunk; });
69
+ child.stdout.on("data", (chunk) => {
70
+ out += chunk;
71
+ });
45
72
  child.on("error", reject);
46
73
  child.on("close", (code) => {
47
- 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
+ }
48
78
  // Claude answers one JSON object with its text in `result`; Codex streams events, its words in agent_message.
49
79
  let text = "";
50
80
  for (const line of out.split("\n")) {
51
81
  try {
52
82
  const event = JSON.parse(line);
53
83
  if (typeof event.result === "string") text = event.result;
54
- 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;
55
89
  } catch {}
56
90
  }
57
91
  const match = text.match(/\{[\s\S]*\}/);
58
- 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
+ }
59
97
  });
60
98
  });
61
99
  }
62
100
 
63
- export function createCoordinator({ store, mode, askHarness, connected, serverHas, roster, handOn, localAgents, pauseGoal, notifyBoard, searchContext, log, frame = () => null }) {
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
+ }) {
64
116
  const busy = new Set();
65
117
  const again = new Set();
66
- const stateOf = (goalId) => JSON.parse(store.meta(`coordinator:${goalId}`) || '{"lastSeenSeq":0,"rounds":0,"withoutProgress":0,"lastRoundAt":null,"lastError":null}');
67
- 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));
68
125
 
69
126
  // What this machine knows that the round needs, whole: the transcript, who can run, and progress since last time.
70
127
  const facts = async (thread, state) => {
71
- const posts = store.roomPosts(thread.id, 0, 500).filter((post) => !QUIET_KINDS.has(post.kind));
72
- 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
+ }));
73
137
  const runs = store.threadRuns(thread.id);
74
138
  const since = state.lastRoundAt ?? "";
75
- const runsFinished = runs.filter((run) => run.reported_at && run.reported_at > since).length;
76
- const newResults = posts.filter((post) => post.seq > state.lastSeenSeq && post.author !== "user" && !ownPost(thread, post)).length;
77
- 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
+ }));
78
156
  const waiting = runs.filter(waitsForWindow);
79
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
80
158
  // to work in, and Harness not while its part here waits for a window.
81
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.
82
160
  const framesTaken = mode() === "local" || serverHas("swarm_frame");
83
- const candidates = roster().filter((candidate) => (!CODING.has(candidate.id) || thread.goal.workspace) && !(candidate.id === "harness" && waiting.length))
84
- .map((candidate) => (framesTaken || !candidate.record?.checks ? candidate : { ...candidate, record: { ...candidate.record, checks: undefined } }));
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
+ );
85
172
  // Earlier words close in meaning to the newest post that the transcript no longer carries, from this thread and the
86
173
  // person's sessions on this machine.
87
- 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);
88
179
  const shown = new Set(transcript.map((post) => post.text));
89
180
  let related = [];
90
181
  if (newest.trim()) {
91
- const found = await searchContext(thread.id, newest, 6).catch(() => ({ entries: [] }));
92
- 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) }));
93
- for (const hit of store.search({ query: newest.split(/\s+/).slice(0, 12).join(" "), limit: 3 })) {
94
- 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
+ });
95
201
  }
96
202
  }
97
203
  // The thread's task frame (frames.mjs): each run's receipt, other threads in the repository, its state.
98
204
  const framed = frame(thread.id);
99
205
  return {
100
206
  ...(framed ? { frame: framed } : {}),
101
- thread: { id: thread.id, title: thread.objective, folder: thread.goal.workspace, agents: thread.goal.team.filter((agent) => agent !== thread.goal.facilitator) },
102
- transcript, lastSeenSeq: state.lastSeenSeq, candidates,
103
- progress: { runsFinished, newResults, stillRunning: runs.filter(going).length, roundsWithoutProgress: state.withoutProgress, failedJobs,
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,
104
224
  // A Harness that does not know the field would refuse the round: it goes only to one that takes Harness's tasks.
105
- ...(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
+ },
106
233
  related: related.slice(0, 8),
107
234
  };
108
235
  };
@@ -110,16 +237,52 @@ export function createCoordinator({ store, mode, askHarness, connected, serverHa
110
237
  // Carry a decision out under the thread's rules. A brief from the coordinator is agent-written work inside the
111
238
  // thread, so it runs on the thread's authority the way a goal's work does; everything else only posts.
112
239
  const apply = (thread, decision, coordinator, candidates) => {
113
- if (decision.openItem) store.roomPost({ goalId: thread.id, author: coordinator, kind: "open_item", text: String(decision.openItem).slice(0, 2_000) });
114
- if (decision.action === "pause") { pauseGoal(thread.id, String(decision.text || "the coordinator could not go on without you").slice(0, 300)); return; }
115
- if (decision.action === "ask_person" && decision.text) store.roomPost({ goalId: thread.id, author: coordinator, kind: "question", text: String(decision.text).slice(0, 4_000) });
116
- if (decision.action === "hand_on" && candidates.some((candidate) => candidate.id === decision.agent) && decision.text) {
117
- if (!thread.goal.team.includes(decision.agent)) store.setGoalTeam(thread.id, [...thread.goal.team, decision.agent]);
118
- 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);
119
272
  const text = String(decision.text).slice(0, 4_000);
120
273
  // The brief is posted for everyone to read and the agent is woken here, so a review runs with its read-only switch.
121
- store.roomPost({ goalId: thread.id, author: coordinator, kind: "post", text: `@${decision.agent} ${review ? "(review, no changes) " : ""}${text}` });
122
- 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
+ );
123
286
  }
124
287
  notifyBoard(store.getTask(thread.id));
125
288
  };
@@ -128,52 +291,106 @@ export function createCoordinator({ store, mode, askHarness, connected, serverHa
128
291
  const thread = store.getTask(goalId);
129
292
  if (!thread || thread.kind !== "goal" || thread.status !== "open") return;
130
293
  const state = stateOf(goalId);
131
- 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));
132
297
  const active = store.threadRuns(goalId).some(going);
133
298
  if (!posts.length || active) return;
134
299
  // A task's own room is coordinated once it becomes a conversation: someone posted in it, not only the task and its result.
135
- 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;
136
305
  // A thread started through Harness by an agent that runs elsewhere gets no rounds until the person sets a spend ceiling.
137
306
  if (store.meta(`hosted-thread:${goalId}`)) return;
138
307
  const local = mode() === "local";
139
308
  if (!local && !connected()) return;
140
- 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;
141
314
  if (local && !localAgent) return;
142
315
  const gathered = await facts(thread, state);
143
- const lastSeq = store.roomPosts(goalId, 0, 500).at(-1)?.seq ?? state.lastSeenSeq;
144
- 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;
145
320
  const startedAt = Date.now();
146
- let decision = null, error = null;
321
+ let decision = null,
322
+ error = null;
147
323
  try {
148
324
  if (local) decision = await localDecision(localAgent, gathered);
149
325
  else {
150
326
  // No deadline of its own: the round ends when Harness answers or the connection drops.
151
327
  const reply = await askHarness("coordinate", gathered, null);
152
- 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
+ );
153
332
  decision = reply.result ?? reply;
154
333
  }
155
- } catch (failure) { error = failure.message; }
156
- 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
+ };
157
345
  saveState(goalId, next);
158
- store.recordRound({ goalId, coordinator: local ? localAgent : "harness", action: decision?.action ?? null, agent: decision?.agent ?? null, loop: decision?.loop ?? null,
159
- 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
+ });
160
358
  if (error) {
161
359
  log(`coordinator round for ${goalId}: ${error}`);
162
- 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
+ });
163
367
  notifyBoard(store.getTask(goalId));
164
368
  return;
165
369
  }
166
- 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
+ );
167
376
  };
168
377
 
169
378
  /** A thread changed: run a round once it is quiet; one at a time per thread, again if more came in meanwhile. */
170
379
  const poke = (goalId) => {
171
- if (busy.has(goalId)) { again.add(goalId); return; }
380
+ if (busy.has(goalId)) {
381
+ again.add(goalId);
382
+ return;
383
+ }
172
384
  busy.add(goalId);
173
- setImmediate(() => void round(goalId).catch((failure) => log(`coordinator ${goalId}: ${failure.message}`)).finally(() => {
174
- busy.delete(goalId);
175
- if (again.delete(goalId)) poke(goalId);
176
- }));
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
+ );
177
394
  };
178
395
  return { poke, state: stateOf };
179
396
  }