tryharness 0.7.16 → 0.7.22

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
@@ -13,9 +13,10 @@ import { parseSessionFile } from "./parsers.mjs";
13
13
  import { historyRoots, paths } from "./paths.mjs";
14
14
  import { openStore } from "./store.mjs";
15
15
  import { readRunSteps, runAgentTask, runningPids, stopAgentTask } from "./runner.mjs";
16
- import { createSwarm, nativeSessionId } from "./swarm.mjs";
16
+ import { PROCESS_NAMES, createSwarm, nativeSessionId } from "./swarm.mjs";
17
17
  import { createCoordinator } from "./coordinator.mjs";
18
18
  import { createWorktrees } from "./worktrees.mjs";
19
+ import { createFolders } from "./folders.mjs";
19
20
  import { notificationFor, notifyDesktop } from "./notify.mjs";
20
21
  import { secretReason } from "./secrets.mjs";
21
22
  import { provenanceFor, provenanceLine } from "./provenance.mjs";
@@ -27,11 +28,13 @@ import { ASKED_BY_TASK, CLOUD_HOMED, MACHINE_RUNNERS, TARGETS, aboutOf, member,
27
28
  import { fingerprint, openBound, openBytes, relayKey, sealBound, sealBytes } from "./sealed.mjs";
28
29
  import { claudeMemNotes } from "./claudemem.mjs";
29
30
  import { machineActivity } from "./activity.mjs";
30
- import { LIVE_AGENTS, focusTerminal, hostApp, liveSessions, processTable, sessionFeed, sessionFile, sessionOfProcess, workingDirectories } from "./live.mjs";
31
- import { EDITOR_IDS, agentCommand, editorFolder, editorInstall, editorName, editorTerminalStarts, openEditorFolder, panelLink, placeEnv, placeFrom, placeKey, processEnv, resumeLine, t3App, t3Link, t3Model, t3Open, t3OpensLinks, t3Project, t3Provider, t3Runs, t3Thread } from "./places.mjs";
31
+ import { LIVE_AGENTS, focusTerminal, hostApp, liveSessions, opencodeFeed, processTable, sessionFeed, sessionFile, sessionOfProcess, workingDirectories } from "./live.mjs";
32
+ import { EDITOR_IDS, agentCommand, editorFolder, editorInstall, editorName, editorRunsOpenTasks, editorSessionWorkspace, editorTerminalStarts, openEditorFolder, panelLink, placeEnv, placeFrom, placeKey, processEnv, resumeLine, t3App, t3Link, t3Model, t3Open, t3OpensLinks, t3Project, t3Provider, t3Runs, t3Thread } from "./places.mjs";
32
33
  import { detectAgentApps, detectMcpClients } from "./adapters.mjs";
33
- import { cardLine, swarmBlock, swarmCards, swarmDigest } from "./cards.mjs";
34
+ import { cardLine, modelOf, swarmBlock, swarmCards, swarmDigest } from "./cards.mjs";
35
+ import { PICKABLE, createModelRouter, picksIn, refreshCatalogs, resolvePick, setRunPick } from "./models.mjs";
34
36
  import { forgetT3, handedToT3, runInT3, sendToT3, startT3SignIn, t3Access } from "./t3.mjs";
37
+ import { createNetwork } from "./network.mjs";
35
38
 
36
39
  // What may be referenced on a thread with another person (the server's rule, src/server/relay/refs.ts, kept
37
40
  // here since a sealed request is opened only on the machines).
@@ -301,6 +304,8 @@ export async function startDaemon() {
301
304
  try { if (fs.existsSync(paths.socket)) fs.unlinkSync(paths.socket); } catch {}
302
305
  }
303
306
  let socket = null;
307
+ // What the Harness this machine is connected to says it can do, told when the connection opens.
308
+ let serverFeatures = new Set();
304
309
  const relayWaiters = new Map();
305
310
  const cloudSeen = new Map();
306
311
  const send = (message) => {
@@ -464,15 +469,15 @@ export async function startDaemon() {
464
469
  // as themselves, never as a root. Anything another agent wrote never starts. Links are followed, so no path inside a
465
470
  // covered folder leads out of it.
466
471
  const RUNNERS = new Set(MACHINE_RUNNERS);
467
- const POSTERS_WITH_FOLDER = new Set(["claude-code", "codex"]);
468
- // A session the person typed in: its prompt hook names the agent process, the parent of the board MCP it posts through.
472
+ // A session the person typed in: its prompt hook (Claude Code, Codex) or its extension or plugin (pi, OpenCode) names
473
+ // the agent process, the parent of the board MCP it posts through.
469
474
  // A run this daemon started is never one (the daemon is its parent), and an agent holding another agent's task writes
470
475
  // as an agent until it reports.
471
476
  const promptedBy = new Map();
472
477
  // Unknown parent: not the person's.
473
478
  const parentOf = (pid) => Number(spawnSync(PS, ["-o", "ppid=", "-p", String(pid)], { encoding: "utf8" }).stdout?.trim()) || null;
474
479
  const personSession = (agent, pid) => {
475
- if (!POSTERS_WITH_FOLDER.has(agent) || !Number.isInteger(pid) || promptedBy.get(pid) !== agent) return false;
480
+ if (!RUNNERS.has(agent) || !Number.isInteger(pid) || promptedBy.get(pid) !== agent) return false;
476
481
  const parent = parentOf(pid);
477
482
  return parent !== null && parent !== process.pid && !store.holdsAgentTask(agent);
478
483
  };
@@ -586,12 +591,18 @@ export async function startDaemon() {
586
591
  };
587
592
  // Work a thread starts on its own runs on the thread's authority only when the person gave it: a thread they set, or,
588
593
  // for a task's room, their go on that task. Anything else waits for the person's Start like any agent-written run.
589
- // Work another agent asked for runs on a goal's authority only where it can be held in (docs/member-runs.md): Claude
590
- // Code in restricted mode, in a copy of the folder this daemon made. Anything else waits for the person's Start.
594
+ // In such a thread, the person's own agents asking for work carry the person's go, as their @mention does (owner,
595
+ // October 13; docs/member-runs.md): Harness and its agents on its server (Bankr, Quotient), and the coding agents on
596
+ // this computer; full permissions, Stop always there. An agent the person set to wait still waits. Work another agent
597
+ // asked for (Grok Bot, Muse, another person's agent, a call through Harness) runs on a goal's authority only where it
598
+ // can be held in: Claude Code in restricted mode, in a copy of the folder this daemon made. Anything else waits.
591
599
  const worktreeRoot = path.join(paths.home, "worktrees");
592
600
  const restrictable = (agent, folder) => agent === "claude-code" && Boolean(folder) && within(folder, worktreeRoot);
593
- const restrictedWait = (agent) => agent === "claude-code"
594
- ? `${nameOf(agent)} would work in your own folder here, not a copy, so work another agent asked for waits for your Start.`
601
+ const setToWait = (agent) => standingRules().find((rule) => rule.agent === agent)?.mode === "off";
602
+ const personsGo = (asker, agent, { hosted = false } = {}) => asker === "user"
603
+ || ((member(asker)?.runs === "server" || (!hosted && member(asker)?.runs === "machine")) && !setToWait(agent));
604
+ const restrictedWait = (agent) => setToWait(agent) ? `${nameOf(agent)} is set to wait for your Start.`
605
+ : agent === "claude-code" ? `${nameOf(agent)} would work in your own folder here, not a copy, so work another agent asked for waits for your Start.`
595
606
  : `${nameOf(agent)} has no restricted mode, so work another agent asked of it waits for your Start.`;
596
607
  const threadMayRun = (goal) => {
597
608
  if (goal.trust !== "user") return false;
@@ -601,24 +612,54 @@ export async function startDaemon() {
601
612
  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
613
  // 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
614
  // 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.
615
+ // person's last choice). A thread in Harness's own copy moves into T3 on its next turn, in that same checkout (a git
616
+ // worktree of T3's project, which T3 takes as it is); one carrying on a session in place stays there; one in T3 stays
617
+ // in T3, and a turn T3 can't take then fails saying why. Reviews keep the agent's read-only switch here.
606
618
  const inT3 = (thread, agent, review) => {
607
619
  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));
620
+ const tree = worktrees.get(thread.id);
621
+ if (tree?.state === "t3") return true;
622
+ // A task's room works in place, which T3 does in its project's own checkout; a thread carrying on a terminal
623
+ // session in place resumes that session here.
624
+ const movable = !tree || (tree.state === "open" && fs.existsSync(tree.path)) || (tree.state === "in_place" && Boolean(store.meta(`task-room:${thread.id}`)));
625
+ return movable && homeOf(agent)?.kind === "t3" && !t3Gap(agent, thread.goal.workspace);
626
+ };
627
+ // Why an agent the person uses in T3 Code would not run there in a folder; null when it would, or when its home is not T3.
628
+ const t3Gap = (agent, folder) => {
629
+ if (!t3Provider(agent) || homeOf(agent)?.kind !== "t3") return null;
630
+ if (t3Access()?.state !== "send") return "Harness isn't approved to send into T3 Code";
631
+ if (!t3Open()) return "T3 Code isn't open";
632
+ if (!t3Project(folder)) return `T3 Code has no project for ${path.basename(folder)}`;
633
+ if (!t3Model(agent)) return `${nameOf(agent)} hasn't run in T3 Code yet`;
634
+ return null;
635
+ };
636
+ // Where an agent's turns in a folder run, for the composer: in T3 Code, or here, with why not T3 when it is the agent's home.
637
+ const runsIn = (agent, folder) => {
638
+ const gap = t3Gap(agent, folder);
639
+ return homeOf(agent)?.kind === "t3" && !gap ? { in: "t3", why: null } : { in: "here", why: gap };
640
+ };
641
+ // A task's own run (its room's first job) goes where its room's turns go: into T3 Code when the person uses the agent
642
+ // there. Not when it would run restricted (work another agent asked for on a goal's authority): only this computer
643
+ // holds a run to that.
644
+ const taskInT3 = (task, agent) => {
645
+ const room = task.parent_id ? store.getTask(task.parent_id) : null;
646
+ if (task.kind !== "task" || room?.kind !== "goal" || store.meta(`task-room:${room.id}`) !== task.id || !task.run_workspace) return false;
647
+ const restricted = task.trust !== "user" && task.run_authorized_by === "goal" && store.meta(`person-wake:${task.id}`) !== "1";
648
+ return !restricted && inT3(room, agent, store.meta(`review:${task.id}`) === "1");
611
649
  };
612
650
  // 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
651
  // for the folder, on the thread's T3 checkout. The thread works in T3 from its first launch.
614
- const t3Turn = (task, agent) => {
652
+ // A model the relay chose goes to T3 as its selection, effort under the option name T3 gives each agent.
653
+ const t3Turn = (task, agent, chosen) => {
615
654
  const thread = store.getTask(task.parent_id);
616
655
  const key = `t3-thread:${thread.id}:${agent}`;
617
656
  const model = t3Model(agent);
618
657
  const project = t3Project(thread.goal.workspace);
658
+ const effort = chosen.effort ? [{ id: agent === "codex" ? "reasoningEffort" : "effort", value: chosen.effort }] : [];
619
659
  return {
620
660
  threadId: store.meta(key) || null,
621
661
  title: thread.objective.slice(0, 120),
662
+ model: chosen.model ? { instanceId: t3Provider(agent), model: chosen.model, options: effort } : null,
622
663
  launch: { ...(project ? { projectId: project.id } : {}), ...(model ? { modelSelection: model } : {}), workspaceStrategy: worktrees.t3Workspace(thread) },
623
664
  onThread: (threadId) => { store.meta(key, threadId); worktrees.inT3(thread.id); },
624
665
  onWorktree: (where, branch) => worktrees.inT3(thread.id, where, branch),
@@ -627,33 +668,46 @@ export async function startDaemon() {
627
668
  // Wake one teammate. A coding agent here gets a run in the goal's folder that resumes its last session on this goal;
628
669
  // Harness and agents that cannot be started here learn of it through the board change and their own reads.
629
670
  const pendingWakes = new Map();
630
- // byPerson: the person's own post named this agent, which is their go for this turn; any other wake (the coordinator,
631
- // another agent) is work another agent asked for and runs restricted or waits (docs/member-runs.md).
632
- const wakeAgent = (goalId, agent, reason, { review = false, byPerson = false } = {}) => {
671
+ // go: the person's go for this turn (personsGo), their own post naming this agent or one of their agents here asking;
672
+ // any other wake is work another agent asked for and runs restricted or waits (docs/member-runs.md).
673
+ // asker: who asked for this turn, the person or an agent, named on the card that waits for the person.
674
+ // pick: the model and effort the post named for this turn (models.mjs).
675
+ const wakeAgent = (goalId, agent, reason, { review = false, go = false, asker = "harness", pick = null } = {}) => {
633
676
  const task = store.getTask(goalId);
634
677
  if (!task || task.status !== "open" || !RUNNERS.has(agent) || !installedAgents.includes(agent)) return;
635
678
  const goal = task.goal;
636
679
  if (!goal.workspace) {
637
680
  if (!store.meta(`goal-folder-note:${goalId}:${agent}`)) {
638
681
  store.meta(`goal-folder-note:${goalId}:${agent}`, "1");
639
- store.roomPost({ goalId, author: "harness", kind: "note", text: `${nameOf(agent)} cannot start: this thread has no folder to work in. Add one with: npx tryharness goal folder ${goalId} <dir>` });
682
+ store.roomPost({ goalId, author: "harness", kind: "note", text: `${nameOf(agent)} cannot start: this thread has no folder to work in. Choose one for the thread in Harness, or: npx tryharness goal folder ${goalId} <dir>` });
640
683
  }
641
684
  return;
642
685
  }
643
- if (store.openWake(goalId, agent)) { pendingWakes.set(`${goalId}:${agent}`, { reason, byPerson: byPerson || Boolean(pendingWakes.get(`${goalId}:${agent}`)?.byPerson) }); return; }
686
+ if (store.openWake(goalId, agent)) { pendingWakes.set(`${goalId}:${agent}`, { reason, asker, go: go || Boolean(pendingWakes.get(`${goalId}:${agent}`)?.go), pick: pick ?? pendingWakes.get(`${goalId}:${agent}`)?.pick ?? null }); return; }
644
687
  if (store.wakeCount(goalId, agent) >= goal.max_wakes) { pauseGoal(goalId, `${agent} was woken ${goal.max_wakes} times`); return; }
645
688
  const brief = swarmBrief(task, agent, reason, { review });
646
689
  // A coding thread in a repository works in its own worktree, so threads never share a checkout; one whose agents the
647
690
  // person uses in T3 Code works in T3 (inT3), where T3 makes and keeps the checkout.
648
691
  const t3 = inT3(task, agent, review);
692
+ // The person uses this agent in T3 Code but this turn runs here: the thread says why, once for each reason.
693
+ const gap = t3 || review ? null : t3Gap(agent, goal.workspace);
694
+ if (gap && !store.meta(`t3-gap-note:${goalId}:${agent}:${gap}`)) {
695
+ store.meta(`t3-gap-note:${goalId}:${agent}:${gap}`, "1");
696
+ store.roomPost({ goalId, author: "harness", kind: "note", text: `${nameOf(agent)} is working on this computer, not in T3 Code: ${gap}.` });
697
+ }
649
698
  let folder = goal.workspace;
650
699
  if (!t3) try { folder = worktrees.folderFor(task); } catch (error) { store.roomPost({ goalId, author: "harness", kind: "note", text: `${agent} cannot start: ${error.message}` }); notifyBoard(store.getTask(goalId)); return; }
651
- const wake = store.postTask({ objective: `Room: ${task.objective}`.slice(0, 500), brief, originAgent: "harness", provenance: { trust: "agent", chain: ["room"] }, targetAgent: agent, parentId: goalId, runWorkspace: folder, kind: "wake" });
700
+ // Whose words the turn carries: whoever wrote the thread when an agent did, then the agent that asked. The person's
701
+ // own ask in their own thread names no one; Harness posts every wake.
702
+ const writers = task.trust === "agent" ? task.origin_chain : [];
703
+ const chain = asker === "user" || writers.at(-1) === asker ? writers : [...writers, asker];
704
+ const wake = store.postTask({ objective: `Room: ${task.objective}`.slice(0, 500), brief, originAgent: "harness", provenance: { trust: "agent", chain: chain.slice(-16) }, targetAgent: agent, parentId: goalId, runWorkspace: folder, kind: "wake" });
652
705
  if (t3) store.meta(`t3-run:${wake.id}`, "1");
706
+ if (pick) setRunPick(store, wake.id, pick, asker);
653
707
  // A review runs with the agent's own read-only switch (runner.mjs); only Claude Code and Codex have one.
654
708
  if (review) store.meta(`review:${wake.id}`, "1");
655
709
  if (threadMayRun(task)) {
656
- if (byPerson) { store.meta(`person-wake:${wake.id}`, "1"); store.authorizeGoalRun(wake.id); }
710
+ if (go) { store.meta(`person-wake:${wake.id}`, "1"); store.authorizeGoalRun(wake.id); }
657
711
  else if (restrictable(agent, folder)) store.authorizeGoalRun(wake.id);
658
712
  else if (!store.meta(`restricted-note:${goalId}:${agent}`)) {
659
713
  store.meta(`restricted-note:${goalId}:${agent}`, "1");
@@ -662,9 +716,13 @@ export async function startDaemon() {
662
716
  }
663
717
  notifyBoard(store.getTask(wake.id));
664
718
  };
719
+ // Harness asked by name answers from the person's open Harness window with its own tools (src/main/agents/harnessInbox.ts):
720
+ // the words whole and where to read the rest. With no window open the ask waits on the board.
721
+ const harnessAsk = (task, words) => `Asked in the thread "${task.objective}" (thread ${task.id}):\n\n${words}\n\n`
722
+ + `Read the thread with read_goal_room (id ${task.id}) before you answer. Your answer here goes back into the thread.`;
665
723
  // Ask an agent that is not started here: the ask is a task in the thread, posted in the asker's name, so its
666
724
  // authority is the asker's (an agent's ask to Bankr waits for the person). One open ask per agent per thread.
667
- const askAgent = (goalId, agent, reason, asker) => {
725
+ const askAgent = (goalId, agent, reason, asker, words = reason) => {
668
726
  const task = store.getTask(goalId);
669
727
  if (!task || task.status !== "open") return;
670
728
  if (store.threadRuns(goalId).some((run) => run.target_agent === agent && ["open", "claimed", "running", "needs_input"].includes(run.status))) return;
@@ -673,18 +731,48 @@ export async function startDaemon() {
673
731
  // with money (Bankr, Quotient) waits for the person under its own rules.
674
732
  const written = from === "user" ? {} : { provenance: { trust: "agent", chain: [...(task.origin_chain || []), from].slice(-16) } };
675
733
  const posted = boardCommand({ command: "board.post", agent: from, objective: `Asked in "${task.objective}": ${reason}`.slice(0, 500),
676
- brief: swarmBrief(task, agent, reason, { ask: true }), targetAgent: agent, parentId: goalId, ...written });
734
+ brief: agent === "harness" ? harnessAsk(task, words) : swarmBrief(task, agent, reason, { ask: true }), targetAgent: agent, parentId: goalId, ...written });
677
735
  const failed = (error) => { store.roomPost({ goalId, author: task.goal.facilitator, kind: "note", text: `${nameOf(agent)} could not be asked: ${error}` }); notifyBoard(store.getTask(goalId)); };
678
736
  if (posted?.pending) posted.pending.catch((error) => failed(error.message));
679
737
  else if (posted && !posted.ok) failed(posted.error);
738
+ else if (agent === "harness" && posted?.task) offerToServer(task, posted.task.id, from, words);
680
739
  };
681
- const wakeFor = (goalId, post, author, names = post.mentions) => {
740
+ // Harness's server takes a task given to Harness in a thread while no Harness window is open (src/server/swarm/harnessTask.ts),
741
+ // only while this computer's coordinator runs on Harness: on the privacy setting the thread's words stay here and the task
742
+ // waits for a window. Harness holds it meanwhile, so a window that opens does not take it too; what the server leaves for
743
+ // the browser goes back on the board, and the window takes it over when it next opens (owner, October 14).
744
+ const offerToServer = (goal, taskId, asker, words) => {
745
+ if (coordinatorMode() !== "harness" || !state.connected || !serverFeatures.has("harness_task")) return;
746
+ if (!boardCommand({ command: "board.claim", agent: "harness", id: taskId })?.ok) return;
747
+ const transcript = store.roomPosts(goal.id, 0, 500).filter((post) => !["note", "held", "round"].includes(post.kind)).slice(-30)
748
+ .map((post) => ({ author: post.author, kind: post.kind, text: post.text.slice(0, 20_000) }));
749
+ // Back to the board for a window, as the person would put it back, with what the thread should hear.
750
+ const back = (post) => {
751
+ if (post) store.roomPost({ goalId: goal.id, author: "harness", ...post });
752
+ boardCommand({ command: "board.release", agent: "user", id: taskId });
753
+ notifyBoard(store.getTask(goal.id));
754
+ };
755
+ const waits = (why) => back({ kind: "note", text: `This waits for your Harness window: ${why}.` });
756
+ askHarness("harness_task", { taskId, thread: { id: goal.id, title: goal.objective.slice(0, 500) }, asker, words: String(words).slice(0, 20_000), transcript }, null).then((reply) => {
757
+ // askHarness spreads Harness's answer into the reply.
758
+ const outcome = reply?.ok ? reply : null;
759
+ if (!outcome) return waits(`Harness could not take it here (${reply?.error || "no answer"})`);
760
+ if (!outcome.taken) return outcome.why === "window_open" ? back(null) : waits(outcome.detail || "Harness could not take it here");
761
+ if (outcome.status === "done") return boardCommand({ command: "board.report", agent: "harness", id: taskId, status: "done", result: outcome.text });
762
+ if (outcome.status === "window") return back({ kind: "post", text: `${outcome.text}\n\nThe rest needs your browser: ${outcome.windowOpen ? "your Harness window is taking it over" : "Harness takes it over when your Harness window next opens"}.` });
763
+ waits(`Harness could not finish it here${outcome.text ? ` (${outcome.text.slice(0, 300)})` : ""}`);
764
+ }, (error) => waits(`Harness could not take it here (${error.message})`));
765
+ };
766
+ const wakeFor = (goalId, post, author, names = post.mentions, { hosted = false } = {}) => {
682
767
  const goal = store.goal(goalId);
768
+ const { picks } = picksIn(store, post.text, names);
683
769
  for (const agent of names) {
684
770
  if (agent === author) continue;
685
771
  const reason = `${author} mentioned you: ${post.text.slice(0, 300)}`;
686
- if (RUNNERS.has(agent)) wakeAgent(goalId, agent, reason, { byPerson: author === "user" });
772
+ if (RUNNERS.has(agent)) wakeAgent(goalId, agent, reason, { go: personsGo(author, agent, { hosted }), asker: author, pick: picks.get(agent) });
687
773
  else if (ASKABLE.has(agent)) askAgent(goalId, agent, reason, author);
774
+ // Harness by name: the person's @harness or an agent's ask. A message naming no one falls to Harness as coordinator.
775
+ else if (agent === "harness" && post.kind === "post" && (author !== "user" || mentionsIn(post.text, [agent]).length)) askAgent(goalId, agent, reason, author, post.text);
688
776
  }
689
777
  // Harness follows through the board change; it is woken by mentions, results and questions, not by every note.
690
778
  if (goal && names.includes(goal.facilitator)) notifyBoard(store.getTask(goalId));
@@ -698,7 +786,7 @@ export async function startDaemon() {
698
786
  const stopped = kind === "failed" && store.getTask(goalId)?.status !== "open";
699
787
  if (!stopped && !store.postsBy(goalId, task.target_agent, startedAt) && text) store.roomPost({ goalId, author: task.target_agent, kind: kind === "done" ? "post" : "failed", text: kind === "done" ? text : `run failed: ${text}` });
700
788
  const pending = pendingWakes.get(`${goalId}:${task.target_agent}`);
701
- if (pending) { pendingWakes.delete(`${goalId}:${task.target_agent}`); wakeAgent(goalId, task.target_agent, pending.reason, { byPerson: pending.byPerson }); }
789
+ if (pending) { pendingWakes.delete(`${goalId}:${task.target_agent}`); wakeAgent(goalId, task.target_agent, pending.reason, { go: pending.go, asker: pending.asker, pick: pending.pick }); }
702
790
  notifyBoard(store.getTask(goalId));
703
791
  };
704
792
  // A job under a goal changed: its result or question lands in the room and wakes whoever posted it.
@@ -709,8 +797,10 @@ export async function startDaemon() {
709
797
  const who = task.claimed_by || task.target_agent || "an agent";
710
798
  // A task's own job reports to whoever posted it the way any task does; it does not wake them.
711
799
  const root = store.meta(`task-room:${goal.id}`) === task.id;
800
+ // An answer to an ask (askAgent) follows the ask in the thread, so it stands as said; a job's result names the job.
801
+ const answer = task.status !== "failed" && String(task.result || "").trim() && task.objective.startsWith(`Asked in "${goal.objective}": `);
712
802
  const post = store.roomPost({ goalId: goal.id, author: who, kind: task.status === "needs_input" ? "question" : "result",
713
- text: `${task.status === "done" ? "Finished" : task.status === "failed" ? "Could not finish" : "Asks about"} job "${task.objective}": ${task.result || ""}`.slice(0, 20_000),
803
+ text: (answer ? task.result : `${task.status === "done" ? "Finished" : task.status === "failed" ? "Could not finish" : "Asks about"} job "${task.objective}": ${task.result || ""}`).slice(0, 20_000),
714
804
  mentions: root ? [] : [task.origin_agent].filter((agent) => goal.goal.team.includes(agent)) });
715
805
  // A room where only the task's own agent worked closes with the task; one that grew into a conversation stays open.
716
806
  if (root && task.status !== "needs_input" && goal.status === "open" && !store.roomPosts(goal.id, 0, 500).some((entry) => entry.kind === "post")
@@ -766,6 +856,9 @@ export async function startDaemon() {
766
856
  if (goal.status !== "open") return { ok: false, error: `the goal is ${goal.status}` };
767
857
  const text = String(request.text || "").trim().slice(0, 20_000);
768
858
  if (!text) return { ok: false, error: "a post needs text" };
859
+ // A model named for an agent (`@codex:gpt-6-luna/high`) must be one it lists, or the post is refused saying why.
860
+ const picked = picksIn(store, text, ROOM_AGENTS);
861
+ if (picked.error) return { ok: false, error: picked.error };
769
862
  const flag = secretReason(text);
770
863
  if (flag) {
771
864
  const held = store.roomPost({ goalId: goal.id, author: agent, kind: "held", text });
@@ -794,7 +887,7 @@ export async function startDaemon() {
794
887
  if (sent.ok && sent.message.state === "sent") sendThreadMessage(sent.message);
795
888
  }
796
889
  if (store.roomCount(goal.id) >= goal.goal.max_posts) { pauseGoal(goal.id, `the room reached ${goal.goal.max_posts} posts`); return { ok: true, post, paused: true }; }
797
- wakeFor(goal.id, post, agent, wakes);
890
+ wakeFor(goal.id, post, agent, wakes, { hosted: request.hostedCaller === true });
798
891
  notifyBoard(store.getTask(goal.id));
799
892
  const notes = [...(unwoken.length ? [`Named ${unwoken.map((name) => `@${name}`).join(", ")} without asking, so not woken. To ask an agent, put its name in ask.`] : []), ...wakes.map(setupNote).filter(Boolean)];
800
893
  return { ok: true, post, ...(notes.length ? { note: notes.join(" ") } : {}) };
@@ -837,11 +930,10 @@ export async function startDaemon() {
837
930
  notifyBoard(task);
838
931
  return { ok: true, task };
839
932
  }
933
+ // The same move as the composer's (swarm.mjs board.swarm.folder): between turns, each agent fresh in the new folder.
840
934
  if (request.command === "board.goal.folder") {
841
- const workspace = String(request.workspace || "");
842
- if (!path.isAbsolute(workspace) || !fs.statSync(workspace, { throwIfNoEntry: false })?.isDirectory()) return { ok: false, error: `${workspace} is not an absolute path to a folder` };
843
- store.setGoalFolder(goal.id, path.resolve(workspace));
844
- return { ok: true, task: store.getTask(goal.id) };
935
+ const moved = swarmCommand({ command: "board.swarm.folder", id: goal.id, folder: String(request.workspace || "") }, "user");
936
+ return moved.ok ? { ok: true, task: store.getTask(goal.id) } : moved;
845
937
  }
846
938
  if (goal.status !== "paused") return { ok: false, error: `the goal is ${goal.status}` };
847
939
  const task = store.setGoalState(goal.id, { status: "open", maxPosts: Number.isInteger(request.maxPosts) && request.maxPosts > 0 ? request.maxPosts : undefined });
@@ -863,10 +955,23 @@ export async function startDaemon() {
863
955
  // "harness" (default): rounds go to Harness, Jev decides. "local": an agent on this machine decides with its tools off.
864
956
  const coordinatorMode = () => (readJson(paths.config, {}).coordinator === "local" ? "local" : "harness");
865
957
  const setCoordinatorMode = (mode) => writeJsonAtomic(paths.config, { ...readJson(paths.config, {}), coordinator: mode });
958
+ // The model each run starts with (models.mjs). Auto asks Harness, never on the privacy setting, and only a Harness that
959
+ // said it can pick models: one from before that would leave the run waiting for an answer that never comes.
960
+ // Each agent's own list of models, asked at start, once a day and when the person asks (models.mjs).
961
+ const listModels = () => refreshCatalogs(store, installedAgents.filter((agent) => PICKABLE.has(agent)), log);
962
+ // What Auto reads: the work itself, not the brief's instructions; a thread's turn is its request and its recent posts.
963
+ const workOf = (task) => {
964
+ if (task.kind !== "wake") return { work: `${task.objective}\n\n${task.brief}`.trim(), transcript: [] };
965
+ const thread = store.getTask(task.parent_id);
966
+ return { work: `${thread.objective}${thread.brief ? `\n\n${thread.brief}` : ""}`,
967
+ transcript: store.roomPosts(thread.id, 0, 500).filter((post) => !["note", "held", "round"].includes(post.kind)).slice(-30).map((post) => ({ author: post.author, text: post.text.slice(0, 20_000) })) };
968
+ };
969
+ const modelForRun = createModelRouter({ store, askHarness, canAsk: () => state.connected && serverFeatures.has("route_model"), privacy: () => coordinatorMode() === "local", ownModel: (agent) => modelOf(agent), workOf, log });
866
970
  const coordinator = createCoordinator({
867
- store, mode: coordinatorMode, askHarness, connected: () => state.connected, localAgents: () => installedAgents.filter((agent) => RUNNERS.has(agent)),
868
- roster: () => cardsFor(available().filter((agent) => agent !== "harness" && setupOf(agent)?.state !== "not_set_up")),
869
- handOn: (goalId, agent, reason, { review }) => (RUNNERS.has(agent) ? wakeAgent(goalId, agent, reason, { review }) : askAgent(goalId, agent, reason, "harness")),
971
+ store, mode: coordinatorMode, askHarness, connected: () => state.connected, serverHas: (feature) => serverFeatures.has(feature), localAgents: () => installedAgents.filter((agent) => RUNNERS.has(agent)),
972
+ // Harness is one of them (owner, October 14): handed work, its open window or Harness's server takes it (offerToServer).
973
+ roster: () => cardsFor(available().filter((agent) => setupOf(agent)?.state !== "not_set_up")),
974
+ handOn: (goalId, agent, reason, { review, words }) => (RUNNERS.has(agent) ? wakeAgent(goalId, agent, reason, { review, go: personsGo("harness", agent) }) : askAgent(goalId, agent, reason, "harness", words)),
870
975
  pauseGoal, notifyBoard, searchContext, log,
871
976
  });
872
977
  const swarmCommand = createSwarm({
@@ -875,7 +980,10 @@ export async function startDaemon() {
875
980
  coordinator: { mode: coordinatorMode, setMode: setCoordinatorMode, state: (id) => coordinator.state(id) },
876
981
  roster: () => available().map((id) => ({ id, about: aboutOf(id), setup: setupOf(id) })),
877
982
  worktrees,
983
+ folders: createFolders({ store, askHarness, canAsk: () => state.connected && serverFeatures.has("pick_folder"), privacy: () => coordinatorMode() === "local", log }),
984
+ runsIn,
878
985
  authorizeRun: (id) => boardCommand({ command: "board.run", agent: "user", id }),
986
+ pickable: () => installedAgents.filter((agent) => PICKABLE.has(agent)), listModels,
879
987
  });
880
988
 
881
989
  // The person's own sessions running now (live.mjs), and what they sent one from Harness: it waits here until that
@@ -925,6 +1033,8 @@ export async function startDaemon() {
925
1033
  let env = placeEnv(request.env);
926
1034
  let host = pids.map((pid) => hostApp(table, pid)).find(Boolean) ?? null;
927
1035
  let tty = pids.map((pid) => table.get(pid)?.tty).find(Boolean) ?? null;
1036
+ // The agent's own process (not the shell a hook ran in), kept with the place: the session runs while it does.
1037
+ let agentPid = pids.find((pid) => path.basename(table.get(pid).comm) === PROCESS_NAMES[agentId]) ?? null;
928
1038
  if (agentId === "codex" && pids.length && !host && !tty) {
929
1039
  const clients = [...table].filter(([, row]) => path.basename(row.comm) === "codex" && row.tty).map(([pid]) => pid);
930
1040
  const cwds = workingDirectories(clients);
@@ -933,8 +1043,11 @@ export async function startDaemon() {
933
1043
  env = processEnv(client);
934
1044
  host = hostApp(table, client);
935
1045
  tty = table.get(client).tty;
1046
+ // Codex's shared server outlives its sessions; the terminal's Codex is this one.
1047
+ agentPid = client;
936
1048
  }
937
- return placeFrom({ env, host, tty, folder });
1049
+ const place = placeFrom({ env, host, tty, folder });
1050
+ return place && agentPid ? { ...place, pid: agentPid, comm: table.get(agentPid).comm } : place;
938
1051
  };
939
1052
  // A custom harness may say how one of its sessions opens again: a page it serves on this computer, or a command to type.
940
1053
  const reopenOf = (value) => {
@@ -954,7 +1067,10 @@ export async function startDaemon() {
954
1067
  };
955
1068
  const linksOk = t3LinksOk();
956
1069
  const handed = handedToT3();
957
- const sessions = liveSessions({ exclude: new Set(runningPids()), recent })
1070
+ // The sessions that named their own process when they started (pi's extension, OpenCode's plugin, hooks).
1071
+ const reported = LIVE_AGENTS.flatMap((agentId) => Object.entries(placesOf(agentId)).filter(([, place]) => Number.isInteger(place.pid))
1072
+ .map(([sessionId, place]) => ({ agent: agentId, sessionId, folder: place.folder || null, pid: place.pid, comm: place.comm, at: place.at })));
1073
+ const sessions = liveSessions({ exclude: new Set(runningPids()), recent, reported })
958
1074
  // A T3 thread running a turn Harness handed it is Harness's run, shown in its thread.
959
1075
  .filter((session) => !handed.has(session.t3Thread))
960
1076
  // A T3 Code that cannot open links is brought forward instead, and messages reach its sessions through their hooks.
@@ -1020,34 +1136,51 @@ export async function startDaemon() {
1020
1136
  const ownSession = (row) => !String(row.cwd || "").startsWith(paths.home + path.sep);
1021
1137
  const short = (name) => String(name || "").replace(/ \(.*\)$/, "");
1022
1138
 
1139
+ /** Whether one of Harness's runs is still working in this session: it is not resumed beside it. */
1140
+ const runBusy = (sessionId) => Boolean(sessionId) && [...activeRuns.values()].some(({ id }) => store.getTask(id)?.session_id === sessionId);
1023
1141
  /** How a session opens in one place (or the agent itself, with no session); null when that place cannot show it. */
1024
1142
  const placePlan = (agentId, place, sessionId, folder, act) => {
1025
1143
  if (!place) return null;
1026
1144
  const name = short(place.name);
1145
+ const busy = runBusy(sessionId)
1146
+ ? `${nameOf(agentId)} is still working on this${folder ? ` in ${path.basename(folder)}` : ""}; Open it again once it's done to carry on in the same session.` : null;
1027
1147
  if (place.kind === "t3") {
1028
1148
  if (!place.app) return null;
1029
1149
  const thread = sessionId ? t3Thread(sessionId) : null;
1030
1150
  if (thread && t3OpensLinks(place.app)) return { opensIn: "T3 Code", link: t3Link(thread) };
1031
1151
  if (thread || !sessionId) return { opensIn: "T3 Code", app: place.app, ...(thread ? { note: T3_OLD } : {}) };
1032
1152
  // 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.
1153
+ // and the session can still be resumed in a terminal instead. Until Harness may send into T3, its threads run in
1154
+ // that copy: approving it is what moves them into T3 (inT3).
1034
1155
  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."
1156
+ ? `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
1157
  : "T3 Code doesn't list this session, so it is in front as it is." };
1037
1158
  }
1038
1159
  if (place.kind === "editor") {
1039
- // Its window first (the folder the editor has open around the session's), so a panel link lands there.
1160
+ // Its window (the folder the editor has open around the session's), first when a panel link is to land there.
1040
1161
  const window = editorFolder(place.editor, folder);
1041
- if (act && window) openEditorFolder(place, window);
1162
+ // Not in its panel or a terminal beside a run still working in it: its folder comes up, and the buddy shows the work.
1163
+ if (busy && place.app) { if (act && window) openEditorFolder(place, window); return { opensIn: name, app: place.app, note: busy }; }
1042
1164
  const link = sessionId ? panelLink(agentId, place.editor, sessionId) : null;
1043
- if (link) return { opensIn: name, link };
1165
+ if (link) { if (act && window) openEditorFolder(place, window); return { opensIn: name, link }; }
1044
1166
  if (!place.app) return null;
1045
- // An editor's terminal takes no command from another app: the session's own command is copied to paste there.
1046
1167
  const line = sessionId ? resumeLine(agentId, sessionId, folder) : null;
1168
+ // The session itself, resumed in the editor's own terminal in a window on its folder: a workspace whose task runs
1169
+ // when the window opens (the editor asks once). The command is copied too, should the person not allow it.
1170
+ if (line && editorRunsOpenTasks(place.editor)) {
1171
+ if (act) openEditorFolder(place, editorSessionWorkspace(agentId, sessionId, folder, window));
1172
+ return { opensIn: name, app: place.app, copy: line, note: `${name} opens ${path.basename(window || folder)} with this session in its terminal. The first time, it asks you to trust the folder and to allow the task that resumes it; the command is copied too.` };
1173
+ }
1174
+ // Automatic tasks turned off there: the session's own command is copied to paste in its terminal.
1175
+ if (act && window) openEditorFolder(place, window);
1047
1176
  return { opensIn: name, app: place.app, ...(line ? { copy: line, note: `${name} is up${window ? ` on ${path.basename(window)}` : ""}. The command that resumes this session is copied: paste it in a terminal there.` } : {}) };
1048
1177
  }
1049
1178
  if (place.kind === "codex-app") return agentId === "codex" && sessionId ? { opensIn: "Codex", link: `codex://threads/${sessionId}` } : place.app ? { opensIn: "Codex", app: place.app } : null;
1050
- if (place.kind === "terminal") return CODING_AGENTS.includes(agentId) ? { opensIn: name, resume: { agent: agentId, session: sessionId ?? null, cwd: folder ?? null, app: place.app ?? null } } : null;
1179
+ if (place.kind === "terminal") {
1180
+ if (!CODING_AGENTS.includes(agentId)) return null;
1181
+ if (busy) return place.app ? { opensIn: name, app: place.app, note: busy } : { opensIn: name, note: busy };
1182
+ return { opensIn: name, resume: { agent: agentId, session: sessionId ?? null, cwd: folder ?? null, app: place.app ?? null } };
1183
+ }
1051
1184
  return place.app ? { opensIn: name, app: place.app } : null;
1052
1185
  };
1053
1186
  /** A running session, where it runs: its T3 thread, its panel, its editor's window, its terminal pane and tab. */
@@ -1123,7 +1256,8 @@ export async function startDaemon() {
1123
1256
  // A session's latest messages and steps, from its own file (running or not).
1124
1257
  if (request.command === "board.live.read") {
1125
1258
  const agentId = String(request.to || "");
1126
- if (!LIVE_AGENTS.includes(agentId) && agentId !== "pi") return { ok: false, error: "only a coding agent's session can be read here" };
1259
+ if (!LIVE_AGENTS.includes(agentId)) return { ok: false, error: "only a coding agent's session can be read here" };
1260
+ if (agentId === "opencode") return { ok: true, items: opencodeFeed(opencodeExecutable || "opencode", sessionId, Math.min(200, Number(request.limit) || 80)) };
1127
1261
  const file = sessionFile(agentId, sessionId, folderOf(request), store.recentSessions(200));
1128
1262
  if (!file) return { ok: true, items: [] };
1129
1263
  return { ok: true, items: sessionFeed(agentId, file, Math.min(200, Number(request.limit) || 80)) };
@@ -1160,6 +1294,8 @@ export async function startDaemon() {
1160
1294
  // Lid-Closed Mode's Standby: when anything last came in (a phone message, a wake, a new task).
1161
1295
  let listenerAt = Date.now();
1162
1296
  const touchListener = () => { listenerAt = Date.now(); };
1297
+ // Swarms talking to swarms (network.mjs): set once the relay's helpers below exist.
1298
+ let network = null;
1163
1299
  const boardCommand = (request) => {
1164
1300
  if (!validAgent(request.agent)) return { ok: false, error: "board requests need an agent name" };
1165
1301
  if (request.command === "board.post") touchListener();
@@ -1171,6 +1307,7 @@ export async function startDaemon() {
1171
1307
  let outcome;
1172
1308
  if (String(request.command).startsWith("board.goal.") || String(request.command).startsWith("board.room.")) return goalCommand(request, agent);
1173
1309
  if (String(request.command).startsWith("board.swarm.")) return swarmCommand(request, agent);
1310
+ if (String(request.command).startsWith("board.relay.")) return network ? network.command(request, agent) : { ok: false, error: "the daemon is still starting" };
1174
1311
  if (request.command === "board.prompted") {
1175
1312
  for (const pid of Array.isArray(request.pids) ? request.pids.slice(0, 2) : []) if (Number.isInteger(pid) && pid > 1) promptedBy.set(pid, agent);
1176
1313
  // A session that started before its hook said where it runs says so on its next prompt.
@@ -1189,9 +1326,12 @@ export async function startDaemon() {
1189
1326
  if (request.command === "board.post") {
1190
1327
  const objective = String(request.objective || "").trim();
1191
1328
  if (!objective) return { ok: false, error: "a task needs an objective" };
1329
+ // A thread with another person themselves (not one of their agents) is the person's own: no agent here claims or runs it.
1330
+ const personThread = Boolean(request.thread) && request.targetAgent === "person";
1331
+ if (personThread) request = { ...request, targetAgent: "user" };
1192
1332
  // A target is named the way people write it ("Grok", "Claude Code") and resolved through the roster; a name
1193
1333
  // several members start ("claude") or none matches is refused with who it could be, never guessed.
1194
- if (request.targetAgent != null && !String(request.targetAgent).includes("/")) {
1334
+ if (request.targetAgent != null && !personThread && !String(request.targetAgent).includes("/")) {
1195
1335
  const named = resolveMember(request.targetAgent, { extra: store.agentsSeen().map((row) => row.agent).filter((seen) => !member(seen)) });
1196
1336
  if (!named.ok) {
1197
1337
  return { ok: false, code: "unknown_agent", candidates: named.candidates, error: named.candidates.length
@@ -1202,6 +1342,14 @@ export async function startDaemon() {
1202
1342
  request = { ...request, targetAgent: named.id };
1203
1343
  }
1204
1344
  if (request.targetAgent != null && !validAgent(request.targetAgent)) return { ok: false, error: "invalid target agent" };
1345
+ // The model and effort the poster names for the run, checked against the agent's own list (models.mjs).
1346
+ let pick = null;
1347
+ if (request.model) {
1348
+ if (!PICKABLE.has(String(request.targetAgent || ""))) return { ok: false, error: "a model can be named only for one of claude-code, codex, pi or opencode, as target_agent" };
1349
+ const resolved = resolvePick(store, String(request.targetAgent), request.model, request.effort);
1350
+ if (!resolved.ok) return { ok: false, error: resolved.error };
1351
+ pick = resolved.pick;
1352
+ }
1205
1353
  // Provenance is code's: the local parent's label and what Harness sent from above the poster can only tighten it.
1206
1354
  const parentTask = request.parentId ? store.getTask(String(request.parentId)) : null;
1207
1355
  const claimed = request.provenance && typeof request.provenance === "object" && request.provenance.trust === "agent" ? request.provenance : null;
@@ -1223,8 +1371,8 @@ export async function startDaemon() {
1223
1371
  return forwardToHarness({ ...request, provenance: upstream });
1224
1372
  }
1225
1373
  // Left out, a task from a coding agent for a coding agent runs in the folder the poster works in (its stdio MCP's,
1226
- // which Claude Code and Codex start in the session's folder); never home or above it.
1227
- const posterFolder = !request.thread && !goalParent && request.inSession !== true && POSTERS_WITH_FOLDER.has(agent) && RUNNERS.has(String(request.targetAgent || ""))
1374
+ // which each agent starts in the session's folder); never home or above it.
1375
+ const posterFolder = !request.thread && !goalParent && request.inSession !== true && RUNNERS.has(agent) && RUNNERS.has(String(request.targetAgent || ""))
1228
1376
  && typeof request.posterCwd === "string" && path.isAbsolute(request.posterCwd) && !aboveHome(request.posterCwd) ? request.posterCwd : null;
1229
1377
  // The person's own task for a coding agent that names no folder runs in that agent's scratch folder, so it starts like
1230
1378
  // any covered run (same daily limit, same off switch) instead of waiting for a session to look at the board.
@@ -1265,14 +1413,18 @@ export async function startDaemon() {
1265
1413
  originAgent: agent, provenance, targetAgent: request.targetAgent ? String(request.targetAgent) : null, parentId: request.parentId ? String(request.parentId) : null, contextRefs: refs,
1266
1414
  // Asking for a run in a folder is the person's Start; a scratch folder they never asked for goes by standing permission.
1267
1415
  runWorkspace: thread ? null : runWorkspace, runAuthorizedBy: !thread && runWorkspace && !scratch && agent === "user" ? "user" : null });
1268
- if (thread) store.openThread(task.id, { withAgent: String(thread.with), role: thread.role, peerKey: thread[OPENED] && thread.peerKey ? String(thread.peerKey) : null });
1269
- // A job in a goal's room runs on the goal's authority; anywhere else, standing permission decides. Until runs
1270
- // started by members are restricted, the goal covers jobs from the person, Harness and the coding agents on this
1271
- // machine; a job from a member that runs elsewhere (Grok Bot, Muse, a connected app) waits for the person's Start.
1416
+ if (thread) { store.openThread(task.id, { withAgent: String(thread.with), role: thread.role, peerKey: thread[OPENED] && thread.peerKey ? String(thread.peerKey) : null }); network?.opened(task.id); }
1417
+ if (pick) setRunPick(store, task.id, pick, agent);
1418
+ // A job in a goal's room runs on the goal's authority; anywhere else, standing permission decides. The goal covers
1419
+ // jobs from the person, Harness and the coding agents on this machine, with the person's go (personsGo) unless an
1420
+ // agent's words came from above the poster; a job from a member that runs elsewhere (Grok Bot, Muse, a connected
1421
+ // app) waits for the person's Start.
1272
1422
  const onGoal = agent === "user" || agent === "harness" || (!request.hostedCaller && member(agent)?.runs === "machine");
1273
1423
  const agentWritten = task.trust !== "user";
1274
- const mayStart = goalParent && task.run_workspace && threadMayRun(goalParent) && onGoal && (!agentWritten || restrictable(task.target_agent, task.run_workspace));
1275
- const standing = thread ? undefined : goalParent ? (!task.run_workspace ? undefined : mayStart && store.authorizeGoalRun(task.id) ? { started: true, by: "goal", ...(agentWritten ? { restricted: true } : {}) }
1424
+ const go = onGoal && (!agentWritten || (!claimed && !setToWait(String(task.target_agent))));
1425
+ const mayStart = goalParent && task.run_workspace && threadMayRun(goalParent) && (go || (onGoal && restrictable(task.target_agent, task.run_workspace)));
1426
+ const startOnGoal = () => { if (go) store.meta(`person-wake:${task.id}`, "1"); return store.authorizeGoalRun(task.id); };
1427
+ const standing = thread ? undefined : goalParent ? (!task.run_workspace ? undefined : mayStart && startOnGoal() ? { started: true, by: "goal", ...(go ? {} : { restricted: true }) }
1276
1428
  : !threadMayRun(goalParent) ? undefined
1277
1429
  : { started: false, reason: !onGoal ? `${nameOf(agent)} does not run on this machine, so its job waits for the person's Start.` : restrictedWait(String(task.target_agent)) }) : applyStanding(task);
1278
1430
  if (!thread && !goalParent && !request.parentId && task.target_agent && task.target_agent !== "harness" && ROOM_AGENTS.includes(task.target_agent)) openTaskRoom(store.getTask(task.id), agent, provenance, { hosted: Boolean(request.hostedCaller) });
@@ -1399,7 +1551,8 @@ export async function startDaemon() {
1399
1551
  outcome = store.claimTask(id, agent, expected, Number.isInteger(request.leaseMinutes) ? request.leaseMinutes : 60);
1400
1552
  // The person's own session that took it, so the task opens there (its MCP server is a child of that session).
1401
1553
  const claimed = outcome?.ok ? store.getTask(id) : null;
1402
- const session = claimed && !claimed.session_id && LIVE_AGENTS.includes(agent) ? sessionOfProcess(agent, request.claimerPid, store.recentSessions(200)) : null;
1554
+ const reportedBy = (pid) => Object.entries(placesOf(agent)).find(([, place]) => place.pid === pid)?.[0] ?? null;
1555
+ const session = claimed && !claimed.session_id && LIVE_AGENTS.includes(agent) ? sessionOfProcess(agent, request.claimerPid, store.recentSessions(200)) ?? reportedBy(request.claimerPid) : null;
1403
1556
  if (session) { store.setRunSession(id, session); outcome = { ...outcome, task: store.getTask(id) }; }
1404
1557
  }
1405
1558
  else if (request.command === "board.report") outcome = store.reportTask(id, agent, { status: String(request.status || ""), result: String(request.result || "").slice(0, 200_000) }, expected);
@@ -1452,6 +1605,8 @@ export async function startDaemon() {
1452
1605
  // One run per folder at a time: each thread's worktree is its own folder, so threads run side by side.
1453
1606
  for (const task of store.runnableTasks(installedAgents)) {
1454
1607
  const agent = task.target_agent;
1608
+ // Where a task runs is settled once, before its slot: a run T3 carries out holds its thread, not a folder.
1609
+ if (task.kind === "task" && store.meta(`t3-run:${task.id}`) === null) store.meta(`t3-run:${task.id}`, taskInT3(task, agent) ? "1" : "0");
1455
1610
  const slot = slotOf(task);
1456
1611
  if (activeRuns.has(slot)) continue;
1457
1612
  const claimed = store.claimTask(task.id, agent, task.version, null);
@@ -1487,10 +1642,15 @@ export async function startDaemon() {
1487
1642
  const resumeSession = wake ? store.goal(task.parent_id)?.sessions[agent] : stoppedFor ? task.session_id || undefined : undefined;
1488
1643
  const again = stoppedFor ? `This task was stopped partway because ${stoppedFor}. Continue from where you left off; check the folder for what you already changed.` : "";
1489
1644
  const readOnly = store.meta(`review:${task.id}`) === "1";
1490
- // Another agent's words on a goal's authority, not the person's tap: restricted (docs/member-runs.md).
1645
+ // Another agent's words on a goal's authority, without the person's tap or go (person-wake): restricted (docs/member-runs.md).
1491
1646
  const restricted = task.trust !== "user" && task.run_authorized_by === "goal" && store.meta(`person-wake:${task.id}`) !== "1";
1492
- const t3 = wake && store.meta(`t3-run:${task.id}`) === "1" ? t3Turn(task, agent) : null;
1493
- startTask({ runId: task.id, agent, title: task.objective.slice(0, 120), brief: resumeSession && again && !wake ? again : [again, brief].filter(Boolean).join("\n\n"), cwd: task.run_workspace, ...(stoppedFor ? { again: true } : {}), ...(resumeSession && !t3 ? { resumeSession } : {}), ...(readOnly ? { readOnly } : {}), ...(restricted ? { restricted } : {}), ...(t3 ? { t3 } : {}) }, (kind, text, extra) => {
1647
+ // The model and effort it starts with (models.mjs), kept on the run with who chose them; without a choice, the model
1648
+ // the agent's own settings name, so each model's record covers every run.
1649
+ const chosen = await modelForRun(task, agent);
1650
+ const { model = null, effort = null } = chosen ?? {};
1651
+ store.setRunModel(task.id, { ...chosen, model: model ?? modelOf(agent), effort, by: model ? chosen.by : chosen?.by ?? "its settings", ...(model ? {} : { ownSettings: true }) });
1652
+ const t3 = store.meta(`t3-run:${task.id}`) === "1" ? t3Turn(task, agent, { model, effort }) : null;
1653
+ startTask({ runId: task.id, agent, title: task.objective.slice(0, 120), brief: resumeSession && again && !wake ? again : [again, brief].filter(Boolean).join("\n\n"), cwd: task.run_workspace, ...(model ? { model } : {}), ...(effort ? { effort } : {}), ...(stoppedFor ? { again: true } : {}), ...(resumeSession && !t3 ? { resumeSession } : {}), ...(readOnly ? { readOnly } : {}), ...(restricted ? { restricted } : {}), ...(t3 ? { t3 } : {}) }, (kind, text, extra) => {
1494
1654
  // The agent's session in a thread is resumed when the thread calls on it again; in T3 it is the agent's T3 thread.
1495
1655
  if (kind === "session" && text) { store.setRunSession(task.id, text); if (!t3 && (wake || room?.kind === "goal")) store.setGoalSession(task.parent_id, agent, text); notifyBoard(store.getTask(task.id)); }
1496
1656
  // A step the agent took: whoever has the thread open reads it again, at most once a second per run.
@@ -1600,6 +1760,7 @@ export async function startDaemon() {
1600
1760
  const opened = await openBound(await relayKey(relayIdentity.privateKey, peer), `request|${sealed.sealId}`, sealed);
1601
1761
  return { about: String(opened.about || "").slice(0, 500), fingerprint: await fingerprint(peer) };
1602
1762
  };
1763
+ network = createNetwork({ store, askHarness, relayRequest, openRelayRequest, fingerprint, myKey: () => relayIdentity?.publicKey ?? null });
1603
1764
 
1604
1765
  // Once a browser is paired (store.sealedOnly), Harness's plain calls get metadata only: who, which state,
1605
1766
  // when, the same as every board_changed already carries, and never the words (objectives, briefs,
@@ -1891,6 +2052,7 @@ export async function startDaemon() {
1891
2052
  offlineSince = null;
1892
2053
  state.connected = true;
1893
2054
  state.lastError = null;
2055
+ serverFeatures = new Set();
1894
2056
  saveStatus();
1895
2057
  send({ type: "hello", protocolVersion: PROTOCOL_VERSION, daemonVersion: DAEMON_VERSION, agents: installedAgents, health: state.agents, capabilities: ["sessions.search", "sessions.read", "tasks.status", "board", "threads", "sealed", "sealed_threads", "sealed_files"], identityKey: relayIdentity?.publicKey });
1896
2058
  // Threads that went quiet while Harness was out of reach get their round now.
@@ -1905,6 +2067,7 @@ export async function startDaemon() {
1905
2067
  return;
1906
2068
  }
1907
2069
  if (message.type === "sync") { void sync(); return; }
2070
+ if (message.type === "server_features" && Array.isArray(message.features)) { serverFeatures = new Set(message.features.map(String)); return; }
1908
2071
  if (message.type === "agent_setup" && Array.isArray(message.agents)) {
1909
2072
  cloudSetup = new Map(message.agents.filter((entry) => entry && typeof entry.agent === "string" && SETUP_STATES.includes(entry.state))
1910
2073
  .map((entry) => [entry.agent, { state: entry.state, how: typeof entry.how === "string" ? entry.how : null, detail: typeof entry.detail === "string" ? entry.detail : null }]));
@@ -1976,7 +2139,7 @@ export async function startDaemon() {
1976
2139
  store.roomPost({ goalId: guestGoal, author: String(input.from || "guest"), kind: "guest", text: words.text.slice(0, 20_000) });
1977
2140
  notifyBoard(store.getTask(guestGoal));
1978
2141
  }
1979
- if (notificationsOn()) notifyDesktop(`${input.from} replied on: ${String(task?.objective || "").slice(0, 80)}`);
2142
+ if (notificationsOn()) notifyDesktop(String(input.from || "").endsWith("/person") ? `${handleOf(input.from)} wrote to you: ${words.text.slice(0, 100)}` : `${input.from} replied on: ${String(task?.objective || "").slice(0, 80)}`);
1980
2143
  }
1981
2144
  })().catch((error) => send({ type: "bridge_response", requestId: message.requestId, ok: false, error: error.message }));
1982
2145
  return;
@@ -1997,6 +2160,12 @@ export async function startDaemon() {
1997
2160
  return;
1998
2161
  }
1999
2162
  if (message.type === "thread_ack") { store.markDelivered(String(message.messageId || "")); return; }
2163
+ // Someone asked to reach this person's swarm (network.mjs lists it): told on the desktop, answered in Swarms.
2164
+ if (message.type === "relay_requested") {
2165
+ const input = message.input || {};
2166
+ if (notificationsOn() && validAgent(input.from)) notifyDesktop(`${input.from} asked to reach ${input.to === "person" ? "you" : `your ${nameOf(String(input.to || ""))}`}. Answer it in Swarms.`);
2167
+ return;
2168
+ }
2000
2169
  if (message.type === "bridge_response" && relayWaiters.has(message.requestId)) {
2001
2170
  relayWaiters.get(message.requestId)(message.ok ? { ok: true, ...(message.result || {}) } : { ok: false, error: message.error || "Harness refused the request" });
2002
2171
  relayWaiters.delete(message.requestId);
@@ -2089,12 +2258,15 @@ export async function startDaemon() {
2089
2258
  }
2090
2259
  }
2091
2260
  setImmediate(() => void sync());
2261
+ setImmediate(() => void listModels());
2262
+ const relist = setInterval(() => void listModels(), 24 * 60 * 60_000);
2092
2263
 
2093
2264
  const shutdown = () => {
2094
2265
  clearInterval(heartbeat);
2095
2266
  clearInterval(staleCheck);
2096
2267
  clearInterval(embedTimer);
2097
2268
  clearInterval(rescan);
2269
+ clearInterval(relist);
2098
2270
  for (const watcher of watchers) watcher.close();
2099
2271
  try { socket?.close(); } catch {}
2100
2272
  try { opencodeProcess?.kill(); } catch {}