trantor 0.18.53 → 0.18.55

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 (62) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +2 -0
  3. package/bin/crew/herdr.mjs +60 -4
  4. package/bin/crew/open.mjs +16 -6
  5. package/bin/crew/state.mjs +10 -1
  6. package/bin/crew-runner.mjs +111 -320
  7. package/bin/slop-gate.mjs +69 -41
  8. package/hooks/agent-notify.mjs +4 -13
  9. package/hooks/file-claim.mjs +6 -22
  10. package/hooks/handoff-now.mjs +5 -9
  11. package/hooks/heartbeat.mjs +16 -51
  12. package/hooks/inbox-deliver.mjs +8 -36
  13. package/hooks/lib/api.mjs +17 -66
  14. package/hooks/lib/handoff.mjs +69 -217
  15. package/hooks/lib/inbox-ledger.mjs +3 -11
  16. package/hooks/lib/resources.mjs +6 -16
  17. package/hooks/lib/update-check.mjs +4 -12
  18. package/hooks/overseer-warn.mjs +4 -16
  19. package/hooks/precompact.mjs +5 -10
  20. package/hooks/prompt-focus.mjs +6 -12
  21. package/hooks/sessionstart.mjs +38 -125
  22. package/hooks/statusline.mjs +4 -9
  23. package/hooks/stop-inbox.mjs +9 -49
  24. package/hooks/subagent-cost.mjs +2 -6
  25. package/hooks/subagent-start.mjs +4 -18
  26. package/hooks/todo-sync.mjs +2 -5
  27. package/hub/reaper.mjs +7 -2
  28. package/lib/autonomy.mjs +6 -30
  29. package/lib/balances.mjs +12 -48
  30. package/lib/classify-failure.mjs +6 -36
  31. package/lib/duty-nudges.mjs +2 -13
  32. package/lib/enroll.mjs +2 -12
  33. package/lib/identity.mjs +4 -22
  34. package/lib/integrate.mjs +6 -20
  35. package/lib/overseer.mjs +2 -15
  36. package/lib/project.mjs +22 -72
  37. package/lib/provider-keys.mjs +2 -5
  38. package/lib/providers.mjs +6 -27
  39. package/lib/redact.mjs +4 -24
  40. package/lib/same-project.mjs +2 -15
  41. package/lib/scrub.mjs +4 -13
  42. package/lib/seat-why.mjs +2 -9
  43. package/lib/seats.mjs +3 -18
  44. package/lib/signed-fetch.mjs +2 -8
  45. package/lib/splitbrain.mjs +4 -24
  46. package/lib/state/apply.mjs +7 -26
  47. package/lib/state/assemble.mjs +10 -33
  48. package/lib/state/cost.mjs +10 -32
  49. package/lib/state/derive.mjs +14 -52
  50. package/lib/state/driver.mjs +26 -108
  51. package/lib/state/gate.mjs +13 -71
  52. package/lib/state/migrate.mjs +3 -6
  53. package/lib/state/promote.mjs +7 -27
  54. package/lib/state/schema.mjs +12 -33
  55. package/lib/state/store.mjs +35 -121
  56. package/lib/state/validate.mjs +7 -31
  57. package/lib/store-contract.mjs +17 -40
  58. package/lib/store-pg.mjs +27 -32
  59. package/lib/subagent-manifest.mjs +2 -17
  60. package/lib/subagent-scan.mjs +0 -0
  61. package/lib/turn-policy.mjs +10 -40
  62. package/package.json +1 -1
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trantor",
3
- "version": "0.18.53",
3
+ "version": "0.18.55",
4
4
  "description": "Trantor — the hub-world for AI agent crews: live message bus, presence, project Kanban/flow board + crew orchestration for independent AI coding agents (Claude, Codex, Gemini, Kimi, DeepSeek)",
5
5
  "mcpServers": {
6
6
  "relay": {
package/README.md CHANGED
@@ -368,6 +368,8 @@ Waking works the same way from the other end: hover a sleeping project in the ap
368
368
  **Wake** hosts its session as an in-app pane — `trantor open` under the hood, opened in the
369
369
  project's own checkout wherever it is called from, handoff-beats-resume, and the same kickoff
370
370
  prompt so the woken session catches up (handoff, board, memory) and recaps on its own.
371
+ A reboot can leave herdr with a pane in its layout and no terminal behind it; `trantor open`,
372
+ the app's Resume and `trantor prune` all treat such a pane as dead and host a fresh one.
371
373
 
372
374
  Why crews never exhaust the orchestrator: bus messages are **by reference** (~70 tokens),
373
375
  work products stay in each agent's own context — the orchestrator burns at coordination
@@ -37,11 +37,26 @@ export function splitPane(ctx, pane, direction, cwd = "", execute = herdrCall) {
37
37
  return parsed?.result?.pane?.pane_id || "";
38
38
  }
39
39
 
40
+ // A pane herdr LISTS is not always a pane herdr can RUN (#7247: a restart kept w2:p8 in the
41
+ // layout, so list/get/rename answered, while read/process-info/run said pane_not_found). Rename
42
+ // is a layout question; process-info is the terminal question, and only a terminal hosts a session.
43
+ export function paneAlive(ctx, pane) {
44
+ if (!pane) return false;
45
+ const result = herdrCall(ctx, ["pane", "process-info", "--pane", pane]);
46
+ const parsed = parseJsonOutput(result.stdout);
47
+ return Boolean(result.ok && parsed && !parsed.error && parsed.result?.process_info);
48
+ }
49
+
40
50
  export function workspacePane(ctx, workspace, preferredCwd) {
41
- const parsed = parseJsonOutput(herdrCall(ctx, ["pane", "list"]).stdout);
42
- const panes = Array.isArray(parsed) ? parsed : parsed?.panes || parsed?.result?.panes || [];
51
+ const panes = listPanes(ctx);
43
52
  const matches = panes.filter(pane => (pane.workspace_id || pane.workspace || "") === workspace);
44
- return matches.find(pane => (pane.cwd || "") === preferredCwd)?.pane_id || matches[0]?.pane_id || matches[0]?.id || "";
53
+ const preferred = matches.filter(pane => (pane.cwd || "") === preferredCwd);
54
+ const ordered = [...preferred, ...matches.filter(pane => !preferred.includes(pane))];
55
+ for (const pane of ordered) {
56
+ const id = pane.pane_id || pane.id || "";
57
+ if (id && paneAlive(ctx, id)) return id;
58
+ }
59
+ return "";
45
60
  }
46
61
 
47
62
  export function closeWorkspace(ctx, id) {
@@ -70,13 +85,47 @@ function runSeat(ctx, pane, agent, command) {
70
85
  }
71
86
 
72
87
  function trackedWorkspace(ctx) {
73
- const ids = readRows(ctx).filter(row => row.project === ctx.project && row.kind === "herdrws").map(row => row.handle);
88
+ // #7285: duplicate rows of ONE workspace must dedupe a takeover used to append a second row of
89
+ // the live workspace, and slice(0,-1) then handed that live workspace to the stale list.
90
+ const ids = [];
91
+ for (const id of readRows(ctx).filter(row => row.project === ctx.project && row.kind === "herdrws").map(row => row.handle)) {
92
+ const at = ids.indexOf(id);
93
+ if (at >= 0) ids.splice(at, 1);
94
+ ids.push(id);
95
+ }
74
96
  return { reuse: ids.at(-1) || "", stale: ids.slice(0, -1) };
75
97
  }
76
98
 
99
+ function listPanes(ctx) {
100
+ const parsed = parseJsonOutput(herdrCall(ctx, ["pane", "list"]).stdout);
101
+ return Array.isArray(parsed) ? parsed : parsed?.panes || parsed?.result?.panes || [];
102
+ }
103
+
104
+ // #7285: the workspaces hosting THIS project's tracked orchestrator pane. Herdr pane ids embed
105
+ // their workspace (#7247 forensics: 'w2:p4Y' lived in w2), so the handle prefix names the home
106
+ // without a round trip; when herdr answers, the pane list is a second opinion (stub ids carry no
107
+ // prefix). Closing such a workspace kills the orchestrator's shell.
108
+ function orchPaneWorkspaces(ctx) {
109
+ const handles = readRows(ctx).filter(row => row.project === ctx.project && row.kind === "orch").map(row => row.handle);
110
+ if (!handles.length) return new Set();
111
+ const spaces = new Set(handles.map(handle => handle.split(":")[0]).filter(Boolean));
112
+ if (ctx.dry) return spaces;
113
+ const tracked = new Set(handles);
114
+ for (const pane of listPanes(ctx)) {
115
+ if (!tracked.has(pane.pane_id || pane.id || "")) continue;
116
+ const space = pane.workspace_id || pane.workspace || "";
117
+ if (space) spaces.add(space);
118
+ }
119
+ return spaces;
120
+ }
121
+
77
122
  function prepareWorkspace(ctx, prune) {
78
123
  const tracked = trackedWorkspace(ctx);
124
+ const orchSpaces = orchPaneWorkspaces(ctx);
79
125
  for (const id of tracked.stale) {
126
+ // #7285: never close reuse itself, nor a workspace hosting the tracked orch pane — `up` once
127
+ // killed its own orchestrator that way. The row stays: the workspace is still live.
128
+ if (id === tracked.reuse || orchSpaces.has(id)) continue;
80
129
  console.log(` → closing stale stacked crew workspace for ${ctx.project} (${id})`);
81
130
  closeWorkspace(ctx, id);
82
131
  dropState(ctx, ctx.project, "herdrws", "", id);
@@ -92,6 +141,12 @@ function prepareWorkspace(ctx, prune) {
92
141
  console.log(` → adopting existing untracked crew workspace for ${ctx.project} (${id})`);
93
142
  reuse = id;
94
143
  recordState(ctx, ctx.project, "herdrws", "__ws__", id);
144
+ } else if (orchSpaces.has(id)) {
145
+ // #7285: a labeled workspace hosting the tracked orch pane is the orchestrator's home, not a
146
+ // stray — adopt it, never close it (the #7285 rule extends past the stale loop).
147
+ console.log(` → adopting the crew workspace hosting the tracked orchestrator for ${ctx.project} (${id})`);
148
+ reuse = id;
149
+ recordState(ctx, ctx.project, "herdrws", "__ws__", id);
95
150
  } else {
96
151
  console.log(` → closing stray crew workspace for ${ctx.project} (${id})`);
97
152
  closeWorkspace(ctx, id);
@@ -179,5 +234,6 @@ export function createHerdrAdapter(ctx) {
179
234
  closeWorkspace: id => closeWorkspace(ctx, id),
180
235
  closePane: id => closePane(ctx, id),
181
236
  liveWorkspaces: () => workspaceList(ctx),
237
+ paneAlive: pane => paneAlive(ctx, pane),
182
238
  };
183
239
  }
package/bin/crew/open.mjs CHANGED
@@ -4,7 +4,7 @@ import { tmpdir } from "node:os";
4
4
  import { basename, join } from "node:path";
5
5
  import { spawn, execSync } from "node:child_process";
6
6
  import { call, parseJsonOutput } from "./core.mjs";
7
- import { createWorkspace, herdrCall, reportAgent, splitPane, workspaceList, workspacePane } from "./herdr.mjs";
7
+ import { createWorkspace, herdrCall, paneAlive, reportAgent, splitPane, workspaceList, workspacePane } from "./herdr.mjs";
8
8
  import { dropState, readRows, recordState } from "./state.mjs";
9
9
  import { resolveOrchestratorDir } from "./worktrees.mjs";
10
10
 
@@ -103,6 +103,14 @@ function tracked(ctx, live) {
103
103
  function reattach(ctx, workspace, pane, id) {
104
104
  if (!pane) return false;
105
105
  if (ctx.dry) { console.log(`herdr:${workspace || "%DRYWS"}/${pane}`); return true; }
106
+ // Two probes, because herdr answers them from different places: a pane a restart left in the
107
+ // layout without a terminal still answers rename, and only process-info says it cannot host
108
+ // anything. Either way the row is dead and the open path hosts a fresh pane.
109
+ if (!paneAlive(ctx, pane)) {
110
+ dropState(ctx, ctx.project, "orch");
111
+ console.error(`— tracked orchestrator pane ${pane} has no terminal behind it (herdr restarted without it): hosting a fresh pane —`);
112
+ return false;
113
+ }
106
114
  const renamed = herdrCall(ctx, ["pane", "rename", pane, `orchestrator · ${ctx.project}`]);
107
115
  if (!renamed.ok) { dropState(ctx, ctx.project, "orch"); return false; }
108
116
  if (!paneHasAgent(ctx, pane)) {
@@ -166,11 +174,10 @@ export function maybeBuildGraft(dir) {
166
174
  const cur = existsSync(gi) ? readFileSync(gi, "utf8") : "";
167
175
  if (!/^graft\/?$/m.test(cur)) writeFileSync(gi, cur + (cur && !cur.endsWith("\n") ? "\n" : "") + "graft/\n");
168
176
  } catch {}
169
- // The build is backgrounded, so we cannot gate the pane on its exit code — but a failure has to
170
- // land SOMEWHERE. It used to go to stdio:"ignore", which meant a graft whose native parsers were
171
- // never built (npm v12 skips node-gyp by default; see deploy/setup.sh) failed in total silence
172
- // while the line below still claimed an index was being built.
173
- //
177
+ // The build is backgrounded, so its exit code cannot gate the pane, but a failure has to land
178
+ // SOMEWHERE: under stdio:"ignore" a graft without native parsers (see deploy/setup.sh) failed in
179
+ // silence while the line below still claimed an index was being built.
180
+
174
181
  // The log goes to the OS temp dir rather than the project: nothing to gitignore, nothing to clean
175
182
  // up, and no exit handler — after unref() the parent usually dies first, so an on-exit cleanup
176
183
  // would be a promise we cannot keep. It is simply overwritten by the next open of this project.
@@ -203,6 +210,9 @@ export function openOrchestrator(ctx, args) {
203
210
  const prior = tracked(ctx, live);
204
211
  if (reattach(ctx, prior.workspace, prior.pane, id)) return 0;
205
212
  const chosen = chooseWorkspace(ctx, live, prior.workspace);
213
+ // #7285: ONE herdrws row per project — re-record exactly like the orch row below, never append.
214
+ // The appended duplicate made the next `up` close the "stale" copy of the live workspace.
215
+ dropState(ctx, ctx.project, "herdrws");
206
216
  recordState(ctx, ctx.project, "herdrws", "__ws__", chosen.workspace);
207
217
  const pane = hostPane(ctx, chosen, id);
208
218
  dropState(ctx, ctx.project, "orch");
@@ -27,6 +27,9 @@ export function writeRows(ctx, rows) {
27
27
  export function recordState(ctx, project, kind, agent, handle) {
28
28
  if (ctx.dry) return;
29
29
  const rows = readRows(ctx);
30
+ // #7285: an exact duplicate carries no new fact, and a doubled herdrws row is how `up` came to
31
+ // see the live workspace as its own stale copy. One row per fact — refuse the push.
32
+ if (rows.some(row => row.project === project && row.kind === kind && row.agent === agent && row.handle === handle)) return;
30
33
  rows.push({ project, kind, agent, handle });
31
34
  writeRows(ctx, rows);
32
35
  }
@@ -139,7 +142,13 @@ export function prune(ctx, adapters) {
139
142
  if (row.kind === "cmuxws") return !cmuxLive.proven || cmuxLive.ids.has(row.handle);
140
143
  if (row.kind === "cmux") return !cmuxLive.proven || cmuxLive.names.has(`trantor:${row.project}`);
141
144
  if (row.kind === "herdrws") return !herdrLive.proven || herdrLive.ids.has(row.handle);
142
- if (["herdr", "orch"].includes(row.kind)) return !herdrLive.proven || herdrLive.names.has(`trantor:${row.project}`);
145
+ // Workspace first (the cmux 0.17.61 lesson: pane ids there were not a liveness fact), then
146
+ // the one per-pane fact herdr does state plainly: a pane whose terminal is gone answers
147
+ // process-info with pane_not_found. A pane that merely hosts a shell stays.
148
+ if (["herdr", "orch"].includes(row.kind)) {
149
+ if (!herdrLive.proven) return true;
150
+ return herdrLive.names.has(`trantor:${row.project}`) && (!adapters.herdr.paneAlive || adapters.herdr.paneAlive(row.handle));
151
+ }
143
152
  return true;
144
153
  });
145
154
  writeRows(ctx, kept);