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
package/lib/service.mjs CHANGED
@@ -9,14 +9,27 @@ import { paths } from "./paths.mjs";
9
9
  const LABEL = "ai.tryharness.daemon";
10
10
 
11
11
  function xml(value) {
12
- return String(value).replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&apos;");
12
+ return String(value)
13
+ .replaceAll("&", "&amp;")
14
+ .replaceAll("<", "&lt;")
15
+ .replaceAll(">", "&gt;")
16
+ .replaceAll('"', "&quot;")
17
+ .replaceAll("'", "&apos;");
13
18
  }
14
19
 
15
20
  const WINDOWS_TASK = "tryharness-daemon";
16
21
 
17
22
  function servicePath(platform = process.platform, homeDir = os.homedir()) {
18
- if (platform === "darwin") return path.join(homeDir, "Library", "LaunchAgents", `${LABEL}.plist`);
19
- if (platform === "linux") return path.join(homeDir, ".config", "systemd", "user", "tryharness-daemon.service");
23
+ if (platform === "darwin")
24
+ return path.join(homeDir, "Library", "LaunchAgents", `${LABEL}.plist`);
25
+ if (platform === "linux")
26
+ return path.join(
27
+ homeDir,
28
+ ".config",
29
+ "systemd",
30
+ "user",
31
+ "tryharness-daemon.service"
32
+ );
20
33
  if (platform === "win32") return path.join(paths.home, `${WINDOWS_TASK}.xml`);
21
34
  return null;
22
35
  }
@@ -59,35 +72,76 @@ export function windowsLauncher({ nodePath, entry }) {
59
72
  }
60
73
 
61
74
  function windowsUser(env = process.env) {
62
- return env.USERDOMAIN && env.USERNAME ? `${env.USERDOMAIN}\\${env.USERNAME}` : env.USERNAME || os.userInfo().username;
75
+ return env.USERDOMAIN && env.USERNAME
76
+ ? `${env.USERDOMAIN}\\${env.USERNAME}`
77
+ : env.USERNAME || os.userInfo().username;
63
78
  }
64
79
 
65
- export function installWindowsTask({ runtimeRoot, nodePath = process.execPath, execute = run, env = process.env }) {
80
+ export function installWindowsTask({
81
+ runtimeRoot,
82
+ nodePath = process.execPath,
83
+ execute = run,
84
+ env = process.env,
85
+ }) {
66
86
  ensureDir(paths.home);
67
87
  const entry = path.join(runtimeRoot, "cli.mjs");
68
88
  const launcher = path.join(paths.home, `${WINDOWS_TASK}.vbs`);
69
89
  const definition = path.join(paths.home, `${WINDOWS_TASK}.xml`);
70
90
  fs.writeFileSync(launcher, windowsLauncher({ nodePath, entry }));
71
91
  // schtasks reads task XML as UTF-16 with a byte order mark.
72
- fs.writeFileSync(definition, "\ufeff" + windowsTaskDefinition({ nodePath, entry, user: windowsUser(env), launcher }), "utf16le");
73
- const created = execute("schtasks", ["/Create", "/TN", WINDOWS_TASK, "/XML", definition, "/F"]);
74
- if (!created.ok) throw new Error(`could not register the Harness daemon with Task Scheduler: ${created.stderr || created.stdout || created.error}`);
92
+ fs.writeFileSync(
93
+ definition,
94
+ "\ufeff" +
95
+ windowsTaskDefinition({
96
+ nodePath,
97
+ entry,
98
+ user: windowsUser(env),
99
+ launcher,
100
+ }),
101
+ "utf16le"
102
+ );
103
+ const created = execute("schtasks", [
104
+ "/Create",
105
+ "/TN",
106
+ WINDOWS_TASK,
107
+ "/XML",
108
+ definition,
109
+ "/F",
110
+ ]);
111
+ if (!created.ok)
112
+ throw new Error(
113
+ `could not register the Harness daemon with Task Scheduler: ${created.stderr || created.stdout || created.error}`
114
+ );
75
115
  execute("schtasks", ["/End", "/TN", WINDOWS_TASK]);
76
116
  const started = execute("schtasks", ["/Run", "/TN", WINDOWS_TASK]);
77
- if (!started.ok) throw new Error(`the Harness daemon task was registered but did not start: ${started.stderr || started.stdout || started.error}`);
117
+ if (!started.ok)
118
+ throw new Error(
119
+ `the Harness daemon task was registered but did not start: ${started.stderr || started.stdout || started.error}`
120
+ );
78
121
  return { manager: "task-scheduler", file: definition };
79
122
  }
80
123
 
81
124
  export function uninstallWindowsTask({ execute = run } = {}) {
82
125
  execute("schtasks", ["/End", "/TN", WINDOWS_TASK]);
83
126
  execute("schtasks", ["/Delete", "/TN", WINDOWS_TASK, "/F"]);
84
- for (const file of [path.join(paths.home, `${WINDOWS_TASK}.xml`), path.join(paths.home, `${WINDOWS_TASK}.vbs`)]) {
85
- try { fs.unlinkSync(file); } catch {}
127
+ for (const file of [
128
+ path.join(paths.home, `${WINDOWS_TASK}.xml`),
129
+ path.join(paths.home, `${WINDOWS_TASK}.vbs`),
130
+ ]) {
131
+ try {
132
+ fs.unlinkSync(file);
133
+ } catch {}
86
134
  }
87
135
  }
88
136
 
89
137
  function agentPath() {
90
- const dirs = new Set([path.dirname(process.execPath), "/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin"]);
138
+ const dirs = new Set([
139
+ path.dirname(process.execPath),
140
+ "/opt/homebrew/bin",
141
+ "/usr/local/bin",
142
+ "/usr/bin",
143
+ "/bin",
144
+ ]);
91
145
  for (const command of ["claude", "codex", "pi", "opencode"]) {
92
146
  const found = resolveCommand(command);
93
147
  if (found) dirs.add(path.dirname(found));
@@ -97,30 +151,58 @@ function agentPath() {
97
151
 
98
152
  function retireLegacyBridge() {
99
153
  if (process.platform === "darwin") {
100
- const legacy = path.join(os.homedir(), "Library", "LaunchAgents", "ai.tryharness.bridge.plist");
154
+ const legacy = path.join(
155
+ os.homedir(),
156
+ "Library",
157
+ "LaunchAgents",
158
+ "ai.tryharness.bridge.plist"
159
+ );
101
160
  if (!fs.existsSync(legacy)) return;
102
161
  run("launchctl", ["unload", legacy]);
103
- try { fs.renameSync(legacy, legacy + ".retired"); } catch {}
162
+ try {
163
+ fs.renameSync(legacy, legacy + ".retired");
164
+ } catch {}
104
165
  } else if (process.platform === "linux") {
105
- const legacy = path.join(os.homedir(), ".config", "systemd", "user", "tryharness-bridge.service");
166
+ const legacy = path.join(
167
+ os.homedir(),
168
+ ".config",
169
+ "systemd",
170
+ "user",
171
+ "tryharness-bridge.service"
172
+ );
106
173
  if (!fs.existsSync(legacy)) return;
107
- run("systemctl", ["--user", "disable", "--now", "tryharness-bridge.service"]);
108
- try { fs.renameSync(legacy, legacy + ".retired"); } catch {}
174
+ run("systemctl", [
175
+ "--user",
176
+ "disable",
177
+ "--now",
178
+ "tryharness-bridge.service",
179
+ ]);
180
+ try {
181
+ fs.renameSync(legacy, legacy + ".retired");
182
+ } catch {}
109
183
  }
110
184
  }
111
185
 
112
- export function installService({ runtimeRoot, nodePath = process.execPath, platform = process.platform, execute = run }) {
186
+ export function installService({
187
+ runtimeRoot,
188
+ nodePath = process.execPath,
189
+ platform = process.platform,
190
+ execute = run,
191
+ }) {
113
192
  ensureDir(paths.home);
114
193
  retireLegacyBridge();
115
194
  const entry = path.join(runtimeRoot, "cli.mjs");
116
195
  const envPath = agentPath();
117
- if (platform === "win32") return installWindowsTask({ runtimeRoot, nodePath, execute });
196
+ if (platform === "win32")
197
+ return installWindowsTask({ runtimeRoot, nodePath, execute });
118
198
  if (platform === "darwin") {
119
199
  const plist = servicePath();
120
200
  ensureDir(path.dirname(plist));
121
201
  // MallocLargeCache=0: macOS otherwise keeps the large blocks ONNX Runtime frees (a screenshot's OCR held 1.6 GB after
122
202
  // it finished; 255 MB with it off, image-redaction eval).
123
- fs.writeFileSync(plist, `<?xml version="1.0" encoding="UTF-8"?>
203
+ fs.writeFileSync(
204
+ plist,
205
+ `<?xml version="1.0" encoding="UTF-8"?>
124
206
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
125
207
  <plist version="1.0"><dict>
126
208
  <key>Label</key><string>${LABEL}</string>
@@ -128,49 +210,103 @@ export function installService({ runtimeRoot, nodePath = process.execPath, platf
128
210
  <key>EnvironmentVariables</key><dict><key>PATH</key><string>${xml(envPath)}</string><key>MallocLargeCache</key><string>0</string></dict>
129
211
  <key>RunAtLoad</key><true/><key>KeepAlive</key><true/><key>ThrottleInterval</key><integer>5</integer>
130
212
  <key>StandardOutPath</key><string>${xml(paths.log)}</string><key>StandardErrorPath</key><string>${xml(paths.log)}</string>
131
- </dict></plist>\n`, { mode: 0o600 });
213
+ </dict></plist>\n`,
214
+ { mode: 0o600 }
215
+ );
132
216
  const domain = `gui/${process.getuid()}`;
133
217
  run("launchctl", ["bootout", domain, plist]);
134
218
  const started = run("launchctl", ["bootstrap", domain, plist]);
135
- if (!started.ok) throw new Error(`could not start Harness daemon with launchd: ${started.stderr || started.error}`);
219
+ if (!started.ok)
220
+ throw new Error(
221
+ `could not start Harness daemon with launchd: ${started.stderr || started.error}`
222
+ );
136
223
  run("launchctl", ["kickstart", "-k", `${domain}/${LABEL}`]);
137
224
  return { manager: "launchd", file: plist };
138
225
  }
139
226
  if (platform === "linux") {
140
227
  const unit = servicePath();
141
228
  ensureDir(path.dirname(unit));
142
- fs.writeFileSync(unit, `[Unit]\nDescription=Harness local daemon\nAfter=network-online.target\n\n[Service]\nExecStart=${JSON.stringify(nodePath)} ${JSON.stringify(entry)} daemon\nEnvironment=PATH=${envPath}\nRestart=always\nRestartSec=5\nStandardOutput=append:${paths.log}\nStandardError=append:${paths.log}\n\n[Install]\nWantedBy=default.target\n`, { mode: 0o600 });
229
+ fs.writeFileSync(
230
+ unit,
231
+ `[Unit]\nDescription=Harness local daemon\nAfter=network-online.target\n\n[Service]\nExecStart=${JSON.stringify(nodePath)} ${JSON.stringify(entry)} daemon\nEnvironment=PATH=${envPath}\nRestart=always\nRestartSec=5\nStandardOutput=append:${paths.log}\nStandardError=append:${paths.log}\n\n[Install]\nWantedBy=default.target\n`,
232
+ { mode: 0o600 }
233
+ );
143
234
  const reload = run("systemctl", ["--user", "daemon-reload"]);
144
- const started = run("systemctl", ["--user", "enable", "--now", "tryharness-daemon.service"]);
145
- if (!reload.ok || !started.ok) throw new Error(`could not start Harness daemon with systemd: ${started.stderr || reload.stderr}`);
235
+ const started = run("systemctl", [
236
+ "--user",
237
+ "enable",
238
+ "--now",
239
+ "tryharness-daemon.service",
240
+ ]);
241
+ if (!reload.ok || !started.ok)
242
+ throw new Error(
243
+ `could not start Harness daemon with systemd: ${started.stderr || reload.stderr}`
244
+ );
146
245
  return { manager: "systemd", file: unit };
147
246
  }
148
247
  // Other Node platforms get a ready daemon for this login only. `doctor` reports that
149
248
  // login persistence is unavailable rather than pretending it was set.
150
- const child = spawn(nodePath, [entry, "daemon"], { detached: true, stdio: "ignore", env: { ...process.env, PATH: envPath, ...(process.platform === "darwin" ? { MallocLargeCache: "0" } : {}) } });
249
+ const child = spawn(nodePath, [entry, "daemon"], {
250
+ detached: true,
251
+ stdio: "ignore",
252
+ env: {
253
+ ...process.env,
254
+ PATH: envPath,
255
+ ...(process.platform === "darwin" ? { MallocLargeCache: "0" } : {}),
256
+ },
257
+ });
151
258
  child.unref();
152
259
  return { manager: "detached", file: null };
153
260
  }
154
261
 
155
- export function uninstallService({ platform = process.platform, execute = run } = {}) {
262
+ export function uninstallService({
263
+ platform = process.platform,
264
+ execute = run,
265
+ } = {}) {
156
266
  const file = servicePath(platform);
157
- if (platform === "win32") { uninstallWindowsTask({ execute }); return; }
267
+ if (platform === "win32") {
268
+ uninstallWindowsTask({ execute });
269
+ return;
270
+ }
158
271
  if (platform === "darwin" && file) {
159
272
  run("launchctl", ["bootout", `gui/${process.getuid()}`, file]);
160
- try { fs.unlinkSync(file); } catch {}
273
+ try {
274
+ fs.unlinkSync(file);
275
+ } catch {}
161
276
  } else if (platform === "linux" && file) {
162
- run("systemctl", ["--user", "disable", "--now", "tryharness-daemon.service"]);
163
- try { fs.unlinkSync(file); } catch {}
277
+ run("systemctl", [
278
+ "--user",
279
+ "disable",
280
+ "--now",
281
+ "tryharness-daemon.service",
282
+ ]);
283
+ try {
284
+ fs.unlinkSync(file);
285
+ } catch {}
164
286
  run("systemctl", ["--user", "daemon-reload"]);
165
287
  }
166
288
  }
167
289
 
168
- export function serviceInfo({ platform = process.platform, execute = run } = {}) {
290
+ export function serviceInfo({
291
+ platform = process.platform,
292
+ execute = run,
293
+ } = {}) {
169
294
  const file = servicePath(platform);
170
295
  if (platform === "win32") {
171
296
  // Installed means the task is registered, not merely that its files exist.
172
- const registered = Boolean(file && fs.existsSync(file)) && execute("schtasks", ["/Query", "/TN", WINDOWS_TASK]).ok;
297
+ const registered =
298
+ Boolean(file && fs.existsSync(file)) &&
299
+ execute("schtasks", ["/Query", "/TN", WINDOWS_TASK]).ok;
173
300
  return { manager: "task-scheduler", installed: registered, file };
174
301
  }
175
- return { manager: platform === "darwin" ? "launchd" : platform === "linux" ? "systemd" : "detached", installed: Boolean(file && fs.existsSync(file)), file };
302
+ return {
303
+ manager:
304
+ platform === "darwin"
305
+ ? "launchd"
306
+ : platform === "linux"
307
+ ? "systemd"
308
+ : "detached",
309
+ installed: Boolean(file && fs.existsSync(file)),
310
+ file,
311
+ };
176
312
  }
package/lib/snippets.mjs CHANGED
@@ -12,53 +12,129 @@ import { repositoryOf } from "./worktrees.mjs";
12
12
  const BEFORE = 5;
13
13
  const LINES = 40;
14
14
 
15
- export function createSnippets({ store, search, scrub, wake = () => {}, notify = () => {}, log = () => {} }) {
16
- const read = (id) => { try { return JSON.parse(store.meta(`snippet:${id}`) || "null"); } catch { return null; } };
15
+ export function createSnippets({
16
+ store,
17
+ search,
18
+ scrub,
19
+ wake = () => {},
20
+ notify = () => {},
21
+ log = () => {},
22
+ }) {
23
+ const read = (id) => {
24
+ try {
25
+ return JSON.parse(store.meta(`snippet:${id}`) || "null");
26
+ } catch {
27
+ return null;
28
+ }
29
+ };
17
30
  const save = (snippet) => {
18
31
  store.meta(`snippet:${snippet.id}`, JSON.stringify(snippet));
19
32
  const ids = JSON.parse(store.meta("snippets") || "[]");
20
- if (!ids.includes(snippet.id)) store.meta("snippets", JSON.stringify([...ids, snippet.id].slice(-200)));
33
+ if (!ids.includes(snippet.id))
34
+ store.meta("snippets", JSON.stringify([...ids, snippet.id].slice(-200)));
21
35
  };
22
36
 
23
37
  /** A cloud agent's request: { ok, snippet } held for the person, or why there is nothing to hold. */
24
38
  const request = async ({ threadId, agent, about }) => {
25
39
  const thread = store.getTask(threadId);
26
40
  const folder = thread?.kind === "goal" ? thread.goal.workspace : null;
27
- if (!folder) return { ok: false, error: "this thread has no folder on the person's computer to show code from" };
28
- const words = String(about || "").trim().slice(0, 500);
41
+ if (!folder)
42
+ return {
43
+ ok: false,
44
+ error:
45
+ "this thread has no folder on the person's computer to show code from",
46
+ };
47
+ const words = String(about || "")
48
+ .trim()
49
+ .slice(0, 500);
29
50
  if (!words) return { ok: false, error: "say what you need to see" };
30
51
  const found = await search({ query: words, folder, limit: 1 });
31
52
  const file = found?.ok ? found.files[0] : null;
32
- if (!file) return { ok: false, error: found?.error || "nothing in this thread's folder matched that" };
53
+ if (!file)
54
+ return {
55
+ ok: false,
56
+ error: found?.error || "nothing in this thread's folder matched that",
57
+ };
33
58
  const root = repositoryOf(folder)?.root ?? folder;
34
59
  let lines;
35
- try { lines = fs.readFileSync(path.join(root, file.path), "utf8").split("\n"); } catch (error) { return { ok: false, error: `${file.path} could not be read (${error.code || error.message})` }; }
36
- const from = Math.max(1, file.line - BEFORE), to = Math.min(lines.length, from + LINES - 1);
60
+ try {
61
+ lines = fs.readFileSync(path.join(root, file.path), "utf8").split("\n");
62
+ } catch (error) {
63
+ return {
64
+ ok: false,
65
+ error: `${file.path} could not be read (${error.code || error.message})`,
66
+ };
67
+ }
68
+ const from = Math.max(1, file.line - BEFORE),
69
+ to = Math.min(lines.length, from + LINES - 1);
37
70
  // A secret in the lines stays a placeholder for good: it is never filled back in for an agent elsewhere.
38
71
  const { text, masked } = await scrub(lines.slice(from - 1, to).join("\n"));
39
- const snippet = { id: randomUUID(), thread: threadId, agent, about: words, path: file.path, from, to, text, masked, state: "waiting", at: new Date().toISOString() };
72
+ const snippet = {
73
+ id: randomUUID(),
74
+ thread: threadId,
75
+ agent,
76
+ about: words,
77
+ path: file.path,
78
+ from,
79
+ to,
80
+ text,
81
+ masked,
82
+ state: "waiting",
83
+ at: new Date().toISOString(),
84
+ };
40
85
  save(snippet);
41
- store.roomPost({ goalId: threadId, author: "harness", kind: "note", text: `${nameOf(agent)} asks to see lines ${from} to ${to} of ${file.path} ("${words.slice(0, 120)}"). They stay on this computer until you share them.` });
42
- notify(`${nameOf(agent)} asks to see ${to - from + 1} lines of ${path.basename(file.path)}. Share or keep them in Harness.`, snippet);
43
- return { ok: true, snippet: { id: snippet.id, path: file.path, from, to, state: "waiting" }, note: "The person decides whether to share these lines; you are told in the thread." };
86
+ store.roomPost({
87
+ goalId: threadId,
88
+ author: "harness",
89
+ kind: "note",
90
+ text: `${nameOf(agent)} asks to see lines ${from} to ${to} of ${file.path} ("${words.slice(0, 120)}"). They stay on this computer until you share them.`,
91
+ });
92
+ notify(
93
+ `${nameOf(agent)} asks to see ${to - from + 1} lines of ${path.basename(file.path)}. Share or keep them in Harness.`,
94
+ snippet
95
+ );
96
+ return {
97
+ ok: true,
98
+ snippet: { id: snippet.id, path: file.path, from, to, state: "waiting" },
99
+ note: "The person decides whether to share these lines; you are told in the thread.",
100
+ };
44
101
  };
45
102
 
46
103
  /** The person shares or keeps a waiting snippet. */
47
104
  const decide = (id, share) => {
48
105
  const snippet = read(id);
49
106
  if (!snippet) return { ok: false, error: "no such request" };
50
- if (snippet.state !== "waiting") return { ok: false, error: `already ${snippet.state}` };
51
- const done = { ...snippet, state: share ? "shared" : "kept", decidedAt: new Date().toISOString() };
107
+ if (snippet.state !== "waiting")
108
+ return { ok: false, error: `already ${snippet.state}` };
109
+ const done = {
110
+ ...snippet,
111
+ state: share ? "shared" : "kept",
112
+ decidedAt: new Date().toISOString(),
113
+ };
52
114
  save(done);
53
115
  if (share) {
54
- const post = store.roomPost({ goalId: snippet.thread, author: "harness", kind: "post", mentions: [snippet.agent],
55
- text: `@${snippet.agent} the person shared lines ${snippet.from} to ${snippet.to} of ${snippet.path}${snippet.masked ? ` (${snippet.masked} secret${snippet.masked === 1 ? "" : "s"} in them stay masked)` : ""}:\n\n\`\`\`\n${snippet.text}\n\`\`\`` });
116
+ const post = store.roomPost({
117
+ goalId: snippet.thread,
118
+ author: "harness",
119
+ kind: "post",
120
+ mentions: [snippet.agent],
121
+ text: `@${snippet.agent} the person shared lines ${snippet.from} to ${snippet.to} of ${snippet.path}${snippet.masked ? ` (${snippet.masked} secret${snippet.masked === 1 ? "" : "s"} in them stay masked)` : ""}:\n\n\`\`\`\n${snippet.text}\n\`\`\``,
122
+ });
56
123
  wake(snippet.thread, post, [snippet.agent]);
57
- } else store.roomPost({ goalId: snippet.thread, author: "harness", kind: "note", text: `The person kept lines ${snippet.from} to ${snippet.to} of ${snippet.path} on this computer.` });
124
+ } else
125
+ store.roomPost({
126
+ goalId: snippet.thread,
127
+ author: "harness",
128
+ kind: "note",
129
+ text: `The person kept lines ${snippet.from} to ${snippet.to} of ${snippet.path} on this computer.`,
130
+ });
58
131
  log(`snippet ${id}: ${done.state}`);
59
132
  return { ok: true, snippet: { id, state: done.state } };
60
133
  };
61
134
 
62
- const list = ({ all = false } = {}) => JSON.parse(store.meta("snippets") || "[]").map(read).filter((snippet) => snippet && (all || snippet.state === "waiting"));
135
+ const list = ({ all = false } = {}) =>
136
+ JSON.parse(store.meta("snippets") || "[]")
137
+ .map(read)
138
+ .filter((snippet) => snippet && (all || snippet.state === "waiting"));
63
139
  return { request, decide, list };
64
140
  }