tryharness 0.7.23 → 0.7.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +5248 -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 +382 -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/worktrees.mjs CHANGED
@@ -6,12 +6,25 @@ import { spawnSync } from "node:child_process";
6
6
  // at once without touching each other's files or the person's checkout. Each agent turn is committed on the branch,
7
7
  // which gives every turn its diff; merge and discard are the person's, never the swarm's.
8
8
 
9
- const IDENTITY = ["-c", "user.name=Harness", "-c", "user.email=harness@localhost"];
9
+ const IDENTITY = [
10
+ "-c",
11
+ "user.name=Harness",
12
+ "-c",
13
+ "user.email=harness@localhost",
14
+ ];
10
15
  const PATCH_LIMIT = 200_000;
11
16
 
12
17
  function git(cwd, args) {
13
- const result = spawnSync("git", [...IDENTITY, ...args], { cwd, encoding: "utf8", maxBuffer: 16 * 1024 * 1024 });
14
- return { ok: result.status === 0, out: (result.stdout || "").trim(), err: (result.stderr || "").trim() };
18
+ const result = spawnSync("git", [...IDENTITY, ...args], {
19
+ cwd,
20
+ encoding: "utf8",
21
+ maxBuffer: 16 * 1024 * 1024,
22
+ });
23
+ return {
24
+ ok: result.status === 0,
25
+ out: (result.stdout || "").trim(),
26
+ err: (result.stderr || "").trim(),
27
+ };
15
28
  }
16
29
 
17
30
  /** The repository a folder is in, and the folder's place inside it; null when it is not in one. */
@@ -21,7 +34,10 @@ export function repositoryOf(folder) {
21
34
  if (!top.ok || !top.out) return null;
22
35
  const head = git(folder, ["rev-parse", "--verify", "HEAD"]);
23
36
  if (!head.ok) return null;
24
- return { root: fs.realpathSync(top.out), inside: path.relative(fs.realpathSync(top.out), fs.realpathSync(folder)) };
37
+ return {
38
+ root: fs.realpathSync(top.out),
39
+ inside: path.relative(fs.realpathSync(top.out), fs.realpathSync(folder)),
40
+ };
25
41
  }
26
42
 
27
43
  /**
@@ -31,20 +47,38 @@ export function repositoryOf(folder) {
31
47
  */
32
48
  export function mainCheckout(folder) {
33
49
  if (!folder || !fs.existsSync(folder)) return folder;
34
- const found = git(folder, ["rev-parse", "--path-format=absolute", "--show-toplevel", "--git-common-dir"]);
50
+ const found = git(folder, [
51
+ "rev-parse",
52
+ "--path-format=absolute",
53
+ "--show-toplevel",
54
+ "--git-common-dir",
55
+ ]);
35
56
  const [top, common] = found.ok ? found.out.split("\n") : [];
36
57
  if (!top || !common || path.basename(common) !== ".git") return folder;
37
58
  const main = path.dirname(common);
38
- if (!fs.existsSync(main) || fs.realpathSync(main) === fs.realpathSync(top)) return folder;
39
- return path.join(main, path.relative(fs.realpathSync(top), fs.realpathSync(folder)));
59
+ if (!fs.existsSync(main) || fs.realpathSync(main) === fs.realpathSync(top))
60
+ return folder;
61
+ return path.join(
62
+ main,
63
+ path.relative(fs.realpathSync(top), fs.realpathSync(folder))
64
+ );
40
65
  }
41
66
 
42
- const slug = (text) => String(text).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 32) || "thread";
67
+ const slug = (text) =>
68
+ String(text)
69
+ .toLowerCase()
70
+ .replace(/[^a-z0-9]+/g, "-")
71
+ .replace(/^-|-$/g, "")
72
+ .slice(0, 32) || "thread";
43
73
 
44
74
  export function createWorktrees({ store, root }) {
45
75
  const key = (threadId) => `worktree:${threadId}`;
46
- const get = (threadId) => { const raw = store.meta(key(threadId)); return raw ? JSON.parse(raw) : null; };
47
- const save = (threadId, value) => store.meta(key(threadId), value ? JSON.stringify(value) : "null");
76
+ const get = (threadId) => {
77
+ const raw = store.meta(key(threadId));
78
+ return raw ? JSON.parse(raw) : null;
79
+ };
80
+ const save = (threadId, value) =>
81
+ store.meta(key(threadId), value ? JSON.stringify(value) : "null");
48
82
 
49
83
  /** Where a thread's coding runs work: its worktree when the folder is in a repository, else the folder itself. */
50
84
  const folderFor = (thread) => {
@@ -52,27 +86,54 @@ export function createWorktrees({ store, root }) {
52
86
  if (!folder) return null;
53
87
  const current = get(thread.id);
54
88
  if (current?.state === "in_place") return folder;
55
- if (current?.state === "t3") return current.path && fs.existsSync(current.path) ? current.path : folder;
56
- if (current && current.state === "open" && fs.existsSync(current.path)) return path.join(current.path, current.inside);
89
+ if (current?.state === "t3")
90
+ return current.path && fs.existsSync(current.path)
91
+ ? current.path
92
+ : folder;
93
+ if (current && current.state === "open" && fs.existsSync(current.path))
94
+ return path.join(current.path, current.inside);
57
95
  const repo = repositoryOf(folder);
58
96
  if (!repo) return folder;
59
97
  const where = path.join(root, thread.id);
60
98
  const branch = `harness/${slug(thread.objective)}-${thread.id.slice(0, 8)}`;
61
99
  const base = git(repo.root, ["rev-parse", "HEAD"]).out;
62
100
  fs.mkdirSync(root, { recursive: true });
63
- const added = git(repo.root, ["worktree", "add", "-b", branch, where, base]);
64
- if (!added.ok) throw new Error(`could not make the thread's copy of ${repo.root}: ${added.err}`);
65
- save(thread.id, { state: "open", repo: repo.root, inside: repo.inside, path: where, branch, base });
101
+ const added = git(repo.root, [
102
+ "worktree",
103
+ "add",
104
+ "-b",
105
+ branch,
106
+ where,
107
+ base,
108
+ ]);
109
+ if (!added.ok)
110
+ throw new Error(
111
+ `could not make the thread's copy of ${repo.root}: ${added.err}`
112
+ );
113
+ save(thread.id, {
114
+ state: "open",
115
+ repo: repo.root,
116
+ inside: repo.inside,
117
+ path: where,
118
+ branch,
119
+ base,
120
+ });
66
121
  return path.join(where, repo.inside);
67
122
  };
68
123
 
69
124
  /** Commits what a turn changed on the thread's branch; the commit is that turn's diff. Null when nothing changed. */
70
125
  const commitTurn = (threadId, runId, message) => {
71
126
  const tree = get(threadId);
72
- if (!tree || tree.state !== "open" || !fs.existsSync(tree.path)) return null;
127
+ if (!tree || tree.state !== "open" || !fs.existsSync(tree.path))
128
+ return null;
73
129
  git(tree.path, ["add", "-A"]);
74
130
  if (git(tree.path, ["diff", "--cached", "--quiet"]).ok) return null;
75
- const committed = git(tree.path, ["commit", "-q", "-m", String(message).slice(0, 200)]);
131
+ const committed = git(tree.path, [
132
+ "commit",
133
+ "-q",
134
+ "-m",
135
+ String(message).slice(0, 200),
136
+ ]);
76
137
  if (!committed.ok) return null;
77
138
  const sha = git(tree.path, ["rev-parse", "HEAD"]).out;
78
139
  store.meta(`turn:${runId}`, sha);
@@ -81,30 +142,53 @@ export function createWorktrees({ store, root }) {
81
142
 
82
143
  const stat = (cwd, range) => {
83
144
  const out = git(cwd, ["diff", "--numstat", range]).out;
84
- const files = out ? out.split("\n").map((line) => { const [added, removed, file] = line.split("\t"); return { file, added: Number(added) || 0, removed: Number(removed) || 0 }; }) : [];
145
+ const files = out
146
+ ? out.split("\n").map((line) => {
147
+ const [added, removed, file] = line.split("\t");
148
+ return {
149
+ file,
150
+ added: Number(added) || 0,
151
+ removed: Number(removed) || 0,
152
+ };
153
+ })
154
+ : [];
85
155
  return files;
86
156
  };
87
157
 
88
158
  /** What a thread changed: everything since it began, or one turn's commit. */
89
159
  const diff = (threadId, runId = null) => {
90
160
  const tree = get(threadId);
91
- if (!tree) return { ok: false, error: "this thread has no copy of a repository" };
161
+ if (!tree)
162
+ return { ok: false, error: "this thread has no copy of a repository" };
92
163
  const cwd = fs.existsSync(tree.path) ? tree.path : tree.repo;
93
164
  let range;
94
165
  if (runId) {
95
166
  const sha = store.meta(`turn:${runId}`);
96
167
  if (!sha) return { ok: true, files: [], patch: "" };
97
168
  range = `${sha}~1..${sha}`;
98
- } else range = `${tree.base}..${tree.state === "open" ? "HEAD" : tree.branch}`;
169
+ } else
170
+ range = `${tree.base}..${tree.state === "open" ? "HEAD" : tree.branch}`;
99
171
  const patch = git(cwd, ["diff", range]).out;
100
- return { ok: true, files: stat(cwd, range), patch: patch.length > PATCH_LIMIT ? `${patch.slice(0, PATCH_LIMIT)}\n… (cut here; the rest is on branch ${tree.branch})` : patch, branch: tree.branch, state: tree.state };
172
+ return {
173
+ ok: true,
174
+ files: stat(cwd, range),
175
+ patch:
176
+ patch.length > PATCH_LIMIT
177
+ ? `${patch.slice(0, PATCH_LIMIT)}\n… (cut here; the rest is on branch ${tree.branch})`
178
+ : patch,
179
+ branch: tree.branch,
180
+ state: tree.state,
181
+ };
101
182
  };
102
183
 
103
184
  const summary = (threadId) => {
104
185
  const tree = get(threadId);
105
186
  // A thread T3 Code works in keeps its changes in T3's own checkout, where T3 shows and merges them.
106
187
  if (!tree || tree.state === "in_place" || tree.state === "t3") return null;
107
- const files = tree.state === "open" && fs.existsSync(tree.path) ? stat(tree.path, `${tree.base}..HEAD`).length : 0;
188
+ const files =
189
+ tree.state === "open" && fs.existsSync(tree.path)
190
+ ? stat(tree.path, `${tree.base}..HEAD`).length
191
+ : 0;
108
192
  return { state: tree.state, branch: tree.branch, repo: tree.repo, files };
109
193
  };
110
194
 
@@ -116,26 +200,54 @@ export function createWorktrees({ store, root }) {
116
200
  /** Brings the thread's branch into the branch the person has checked out. A conflict is undone and reported. */
117
201
  const merge = (threadId, title) => {
118
202
  const tree = get(threadId);
119
- if (!tree || tree.state !== "open") return { ok: false, error: "there is nothing of this thread's to merge" };
120
- if (!stat(tree.path, `${tree.base}..HEAD`).length) return { ok: false, error: "this thread has not changed anything" };
203
+ if (!tree || tree.state !== "open")
204
+ return { ok: false, error: "there is nothing of this thread's to merge" };
205
+ if (!stat(tree.path, `${tree.base}..HEAD`).length)
206
+ return { ok: false, error: "this thread has not changed anything" };
121
207
  const into = git(tree.repo, ["rev-parse", "--abbrev-ref", "HEAD"]).out;
122
- const merged = git(tree.repo, ["merge", "--no-ff", "-m", `Merge ${tree.branch}: ${String(title).slice(0, 120)}`, tree.branch]);
208
+ const merged = git(tree.repo, [
209
+ "merge",
210
+ "--no-ff",
211
+ "-m",
212
+ `Merge ${tree.branch}: ${String(title).slice(0, 120)}`,
213
+ tree.branch,
214
+ ]);
123
215
  if (!merged.ok) {
124
- const conflicts = git(tree.repo, ["diff", "--name-only", "--diff-filter=U"]).out.split("\n").filter(Boolean);
216
+ const conflicts = git(tree.repo, [
217
+ "diff",
218
+ "--name-only",
219
+ "--diff-filter=U",
220
+ ])
221
+ .out.split("\n")
222
+ .filter(Boolean);
125
223
  git(tree.repo, ["merge", "--abort"]);
126
- return { ok: false, conflict: true, files: conflicts, error: conflicts.length
127
- ? `merging into ${into} conflicts in ${conflicts.join(", ")}; nothing was merged`
128
- : `git would not merge into ${into}: ${merged.err.split("\n")[0]}; nothing was merged` };
224
+ return {
225
+ ok: false,
226
+ conflict: true,
227
+ files: conflicts,
228
+ error: conflicts.length
229
+ ? `merging into ${into} conflicts in ${conflicts.join(", ")}; nothing was merged`
230
+ : `git would not merge into ${into}: ${merged.err.split("\n")[0]}; nothing was merged`,
231
+ };
129
232
  }
130
233
  retire(tree);
131
234
  git(tree.repo, ["branch", "-d", tree.branch]);
132
- save(threadId, { ...tree, state: "merged", into, mergedAt: new Date().toISOString() });
235
+ save(threadId, {
236
+ ...tree,
237
+ state: "merged",
238
+ into,
239
+ mergedAt: new Date().toISOString(),
240
+ });
133
241
  return { ok: true, into };
134
242
  };
135
243
 
136
244
  const discard = (threadId) => {
137
245
  const tree = get(threadId);
138
- if (!tree || tree.state !== "open") return { ok: false, error: "there is nothing of this thread's to discard" };
246
+ if (!tree || tree.state !== "open")
247
+ return {
248
+ ok: false,
249
+ error: "there is nothing of this thread's to discard",
250
+ };
139
251
  retire(tree);
140
252
  git(tree.repo, ["branch", "-D", tree.branch]);
141
253
  save(threadId, { ...tree, state: "discarded" });
@@ -149,7 +261,11 @@ export function createWorktrees({ store, root }) {
149
261
  const moved = (threadId) => {
150
262
  const tree = get(threadId);
151
263
  if (tree?.state === "open" && fs.existsSync(tree.path)) {
152
- if (stat(tree.path, `${tree.base}..HEAD`).length) return { ok: false, error: `this thread changed files on ${tree.branch}; merge or discard them first` };
264
+ if (stat(tree.path, `${tree.base}..HEAD`).length)
265
+ return {
266
+ ok: false,
267
+ error: `this thread changed files on ${tree.branch}; merge or discard them first`,
268
+ };
153
269
  retire(tree);
154
270
  git(tree.repo, ["branch", "-D", tree.branch]);
155
271
  }
@@ -168,19 +284,51 @@ export function createWorktrees({ store, root }) {
168
284
  */
169
285
  const t3Workspace = (thread) => {
170
286
  const current = get(thread.id);
171
- if (current?.state === "in_place" || current?.inPlace) return { type: "root" };
172
- if ((current?.state === "t3" || current?.state === "open") && current.path) return { type: "existing_worktree", worktreePath: current.path, ...(current.branch ? { branch: current.branch } : {}) };
287
+ if (current?.state === "in_place" || current?.inPlace)
288
+ return { type: "root" };
289
+ if ((current?.state === "t3" || current?.state === "open") && current.path)
290
+ return {
291
+ type: "existing_worktree",
292
+ worktreePath: current.path,
293
+ ...(current.branch ? { branch: current.branch } : {}),
294
+ };
173
295
  const repo = repositoryOf(thread.goal.workspace);
174
296
  if (!repo) return { type: "root" };
175
297
  const branch = git(repo.root, ["rev-parse", "--abbrev-ref", "HEAD"]).out;
176
- return { type: "worktree", baseRef: branch && branch !== "HEAD" ? branch : git(repo.root, ["rev-parse", "HEAD"]).out,
177
- branch: `harness/${slug(thread.objective)}-${thread.id.slice(0, 8)}`, startFromOrigin: false };
298
+ return {
299
+ type: "worktree",
300
+ baseRef:
301
+ branch && branch !== "HEAD"
302
+ ? branch
303
+ : git(repo.root, ["rev-parse", "HEAD"]).out,
304
+ branch: `harness/${slug(thread.objective)}-${thread.id.slice(0, 8)}`,
305
+ startFromOrigin: false,
306
+ };
178
307
  };
179
308
  /** The thread works in T3 Code from now on; `where` and `branch` once T3 has made its checkout. */
180
309
  const inT3 = (threadId, where = null, branch = null) => {
181
310
  const current = get(threadId);
182
- if (current?.state !== "t3" || (where && where !== current.path)) save(threadId, { ...current, ...(current?.state === "in_place" ? { inPlace: true } : {}), state: "t3", path: where ?? current?.path ?? null, branch: branch ?? current?.branch ?? null });
311
+ if (current?.state !== "t3" || (where && where !== current.path))
312
+ save(threadId, {
313
+ ...current,
314
+ ...(current?.state === "in_place" ? { inPlace: true } : {}),
315
+ state: "t3",
316
+ path: where ?? current?.path ?? null,
317
+ branch: branch ?? current?.branch ?? null,
318
+ });
183
319
  };
184
320
 
185
- return { folderFor, commitTurn, diff, summary, merge, discard, moved, inPlace, t3Workspace, inT3, get };
321
+ return {
322
+ folderFor,
323
+ commitTurn,
324
+ diff,
325
+ summary,
326
+ merge,
327
+ discard,
328
+ moved,
329
+ inPlace,
330
+ t3Workspace,
331
+ inT3,
332
+ get,
333
+ };
186
334
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tryharness",
3
- "version": "0.7.23",
3
+ "version": "0.7.25",
4
4
  "description": "Harness Relay: the daemon that turns the agents on this machine into your personal swarm. Memory, history, and work handed between any of them.",
5
5
  "bin": {
6
6
  "tryharness": "cli.mjs"
@@ -41,8 +41,8 @@
41
41
  "ws": "^8.18.3"
42
42
  },
43
43
  "optionalDependencies": {
44
- "tryharness-desktop-darwin-arm64": "0.7.23",
45
- "tryharness-desktop-darwin-x64": "0.7.23",
44
+ "tryharness-desktop-darwin-arm64": "0.7.25",
45
+ "tryharness-desktop-darwin-x64": "0.7.25",
46
46
  "tryharness-desktop-linux-x64": "0.7.4",
47
47
  "tryharness-desktop-win32-x64": "0.7.4"
48
48
  }