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 +10 -7
- package/lib/places.mjs +4 -1
- package/lib/swarm.mjs +5 -2
- package/lib/worktrees.mjs +20 -4
- package/package.json +3 -3
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
|
|
605
|
-
//
|
|
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)
|
|
609
|
-
if (tree === "t3") return true;
|
|
610
|
-
|
|
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
|
-
?
|
|
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
|
|
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
|
-
|
|
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
|
|
136
|
-
* folderFor's), or T3's project folder when the thread's folder is in
|
|
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.
|
|
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.
|
|
43
|
-
"tryharness-desktop-darwin-x64": "0.7.
|
|
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
|
}
|