tryharness 0.7.16 → 0.7.18

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/lib/daemon.mjs CHANGED
@@ -601,13 +601,15 @@ export async function startDaemon() {
601
601
  const roomDigest = (goalId, limit = 30) => store.roomPosts(goalId).slice(-limit).map((post) => `[${post.seq}] ${post.author}${post.kind !== "post" ? ` (${post.kind})` : ""}: ${post.text.slice(0, 1_200)}`).join("\n");
602
602
  // A thread's coding agent works in T3 Code when the person uses it there: its home is T3, Harness may send into T3,
603
603
  // T3 is open with a project for the thread's folder, and the agent has run in T3 before (its model there is the
604
- // person's last choice). A thread already in Harness's own copy, or carrying on a session in place, stays there; one
605
- // in T3 stays in T3, and a turn T3 can't take then fails saying why. Reviews keep the agent's read-only switch here.
604
+ // person's last choice). A thread in Harness's own copy moves into T3 on its next turn, in that same checkout (a git
605
+ // worktree of T3's project, which T3 takes as it is); one carrying on a session in place stays there; one in T3 stays
606
+ // in T3, and a turn T3 can't take then fails saying why. Reviews keep the agent's read-only switch here.
606
607
  const inT3 = (thread, agent, review) => {
607
608
  if (review || !t3Provider(agent) || !thread.goal.workspace) return false;
608
- const tree = worktrees.get(thread.id)?.state ?? null;
609
- if (tree === "t3") return true;
610
- return tree === null && t3Access()?.state === "send" && homeOf(agent)?.kind === "t3" && t3Open() && Boolean(t3Project(thread.goal.workspace)) && Boolean(t3Model(agent));
609
+ const tree = worktrees.get(thread.id);
610
+ if (tree?.state === "t3") return true;
611
+ if (tree && !(tree.state === "open" && fs.existsSync(tree.path))) return false;
612
+ return t3Access()?.state === "send" && homeOf(agent)?.kind === "t3" && t3Open() && Boolean(t3Project(thread.goal.workspace)) && Boolean(t3Model(agent));
611
613
  };
612
614
  // A turn T3 carries out (t3.mjs runInT3): into the agent's T3 thread in this thread, else a new one in T3's project
613
615
  // for the folder, on the thread's T3 checkout. The thread works in T3 from its first launch.
@@ -1030,9 +1032,10 @@ export async function startDaemon() {
1030
1032
  if (thread && t3OpensLinks(place.app)) return { opensIn: "T3 Code", link: t3Link(thread) };
1031
1033
  if (thread || !sessionId) return { opensIn: "T3 Code", app: place.app, ...(thread ? { note: T3_OLD } : {}) };
1032
1034
  // A session T3 never had (a run Harness started in its own folder, say): T3 comes forward as it is, saying so,
1033
- // and the session can still be resumed in a terminal instead.
1035
+ // and the session can still be resumed in a terminal instead. Until Harness may send into T3, its threads run in
1036
+ // that copy: approving it is what moves them into T3 (inT3).
1034
1037
  return { opensIn: "T3 Code", app: place.app, resumable: true, note: harnessRun(folder)
1035
- ? "This run happened in Harness's own copy of the repo, which T3 Code doesn't list, so T3 Code is in front as it is."
1038
+ ? `This run happened in Harness's own copy of the repo, which T3 Code doesn't list, so T3 Code is in front as it is.${t3Access()?.state === "send" ? "" : " Approve Harness in T3 Code and this thread's next turns run there, where Open finds them."}`
1036
1039
  : "T3 Code doesn't list this session, so it is in front as it is." };
1037
1040
  }
1038
1041
  if (place.kind === "editor") {
package/lib/places.mjs CHANGED
@@ -3,6 +3,7 @@ import os from "node:os";
3
3
  import path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { spawn, spawnSync } from "node:child_process";
6
+ import { mainCheckout } from "./worktrees.mjs";
6
7
 
7
8
  // Where a coding session runs, and how it opens there again. A session's place comes from its own environment: the
8
9
  // Harness piece inside the agent sends it when the session starts (the Claude Code and Codex hooks, nudge.mjs; the
@@ -226,7 +227,9 @@ const realDir = (dir) => { try { return fs.realpathSync(dir); } catch { return n
226
227
  export function t3Project(folder, home = t3Home()) {
227
228
  if (!folder || !t3Tables(home).has("projection_projects")) return null;
228
229
  const top = spawnSync("git", ["rev-parse", "--show-toplevel"], { cwd: folder, encoding: "utf8" });
229
- const wanted = new Set([realDir(folder), top.status === 0 ? realDir(top.stdout.trim()) : null].filter(Boolean));
230
+ const root = top.status === 0 ? top.stdout.trim() : null;
231
+ // A worktree of a project's checkout (a thread's copy here, T3's own) is that project's, as T3 counts it.
232
+ const wanted = new Set([realDir(folder), root && realDir(root), root && realDir(mainCheckout(root))].filter(Boolean));
230
233
  const rows = (t3Query(home, "select project_id || char(9) || workspace_root from projection_projects where deleted_at is null;") || "").split("\n").map((line) => line.split("\t"));
231
234
  const found = rows.find(([id, root]) => id && root && wanted.has(realDir(root)));
232
235
  return found ? { id: found[0], root: realDir(found[1]) } : null;
package/lib/swarm.mjs CHANGED
@@ -2,6 +2,7 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { spawnSync } from "node:child_process";
4
4
  import { readRunSteps } from "./runner.mjs";
5
+ import { mainCheckout } from "./worktrees.mjs";
5
6
  import { nameOf, resolveMember } from "./members.mjs";
6
7
  import { setNote, swarmBlock, swarmCards, swarmDigest } from "./cards.mjs";
7
8
 
@@ -221,9 +222,11 @@ export function createSwarm({ store, roomAgents, mentionsIn, wakeFor, notifyBoar
221
222
  team.add(agentName);
222
223
  folder ??= isDir(session.cwd) ? session.cwd : null;
223
224
  }
224
- // Where the work happens: the named agent's last folder, else the latest coding session's on this computer.
225
- folder ??= [...team].map((name) => PROCESS_NAMES[name] ? lastFolder(name) : null).find(Boolean)
225
+ // Where the work happens: the named agent's last folder, else the latest coding session's on this computer; one in
226
+ // a worktree (a thread's copy here, T3 Code's) is the repository's own checkout, where the person works.
227
+ const last = [...team].map((name) => PROCESS_NAMES[name] ? lastFolder(name) : null).find(Boolean)
226
228
  ?? store.recentSessions(100).find((session) => PROCESS_NAMES[session.agent] && isDir(session.cwd))?.cwd ?? null;
229
+ folder ??= last ? mainCheckout(last) : null;
227
230
  const objective = text.split("\n").map((line) => line.trim()).find(Boolean).replace(/@([a-z][a-z0-9-]*)\s*/g, "").slice(0, 120) || text.slice(0, 120);
228
231
  const thread = store.createGoal({ objective, originAgent: "user", provenance: provenanceFor("user"), team: [...team, "harness"], facilitator: "harness", workspace: folder ? path.resolve(folder) : null, maxPosts: defaults.maxPosts, maxWakes: defaults.maxWakes });
229
232
  if (from) { store.setGoalSession(thread.id, String(from.agent), nativeSessionId(String(from.agent), String(from.sessionId))); worktrees.inPlace(thread.id); }
package/lib/worktrees.mjs CHANGED
@@ -24,6 +24,21 @@ export function repositoryOf(folder) {
24
24
  return { root: fs.realpathSync(top.out), inside: path.relative(fs.realpathSync(top.out), fs.realpathSync(folder)) };
25
25
  }
26
26
 
27
+ /**
28
+ * The folder a linked git worktree stands for: the same place in its repository's main checkout. A thread's copy here,
29
+ * T3 Code's and the person's own are all worktrees of the checkout the person works in (T3 counts their history as
30
+ * that checkout's too). Any other folder is itself.
31
+ */
32
+ export function mainCheckout(folder) {
33
+ if (!folder || !fs.existsSync(folder)) return folder;
34
+ const found = git(folder, ["rev-parse", "--path-format=absolute", "--show-toplevel", "--git-common-dir"]);
35
+ const [top, common] = found.ok ? found.out.split("\n") : [];
36
+ if (!top || !common || path.basename(common) !== ".git") return folder;
37
+ 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)));
40
+ }
41
+
27
42
  const slug = (text) => String(text).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 32) || "thread";
28
43
 
29
44
  export function createWorktrees({ store, root }) {
@@ -132,12 +147,13 @@ export function createWorktrees({ store, root }) {
132
147
 
133
148
  /**
134
149
  * Where T3 Code runs a thread's agents (t3.mjs runInT3): the checkout T3 made for the thread's first agent, shared by
135
- * the rest as its own worktree is; before that, a new one on its own branch from what the person has checked out (as
136
- * folderFor's), or T3's project folder when the thread's folder is in no repository.
150
+ * the rest as its own worktree is, or the thread's own copy when it worked here before; else a new one on its own
151
+ * branch from what the person has checked out (as folderFor's), or T3's project folder when the thread's folder is in
152
+ * no repository.
137
153
  */
138
154
  const t3Workspace = (thread) => {
139
155
  const current = get(thread.id);
140
- if (current?.state === "t3" && current.path) return { type: "existing_worktree", worktreePath: current.path, ...(current.branch ? { branch: current.branch } : {}) };
156
+ if ((current?.state === "t3" || current?.state === "open") && current.path) return { type: "existing_worktree", worktreePath: current.path, ...(current.branch ? { branch: current.branch } : {}) };
141
157
  const repo = repositoryOf(thread.goal.workspace);
142
158
  if (!repo) return { type: "root" };
143
159
  const branch = git(repo.root, ["rev-parse", "--abbrev-ref", "HEAD"]).out;
@@ -147,7 +163,7 @@ export function createWorktrees({ store, root }) {
147
163
  /** The thread works in T3 Code from now on; `where` and `branch` once T3 has made its checkout. */
148
164
  const inT3 = (threadId, where = null, branch = null) => {
149
165
  const current = get(threadId);
150
- if (current?.state !== "t3" || (where && where !== current.path)) save(threadId, { state: "t3", path: where ?? current?.path ?? null, branch: branch ?? current?.branch ?? null });
166
+ if (current?.state !== "t3" || (where && where !== current.path)) save(threadId, { ...current, state: "t3", path: where ?? current?.path ?? null, branch: branch ?? current?.branch ?? null });
151
167
  };
152
168
 
153
169
  return { folderFor, commitTurn, diff, summary, merge, discard, inPlace, t3Workspace, inT3, get };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tryharness",
3
- "version": "0.7.16",
3
+ "version": "0.7.18",
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"
@@ -39,8 +39,8 @@
39
39
  "ws": "^8.18.3"
40
40
  },
41
41
  "optionalDependencies": {
42
- "tryharness-desktop-darwin-arm64": "0.7.16",
43
- "tryharness-desktop-darwin-x64": "0.7.16",
42
+ "tryharness-desktop-darwin-arm64": "0.7.17",
43
+ "tryharness-desktop-darwin-x64": "0.7.17",
44
44
  "tryharness-desktop-linux-x64": "0.7.4",
45
45
  "tryharness-desktop-win32-x64": "0.7.4"
46
46
  }