trantor 0.18.64 → 0.18.65
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/.claude-plugin/plugin.json +1 -1
- package/bin/baton-pane.mjs +1 -1
- package/hooks/lib/handoff.mjs +67 -2
- package/hooks/sessionstart.mjs +7 -4
- package/hooks/stop-inbox.mjs +22 -4
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trantor",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.65",
|
|
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/bin/baton-pane.mjs
CHANGED
|
@@ -33,7 +33,7 @@ const log = (s) => { try { appendFileSync(logFile, `${new Date().toISOString()}
|
|
|
33
33
|
|
|
34
34
|
// Same text as the app's kickoff (lib.rs KICKOFF_PROMPT) — one boot prompt so the successor
|
|
35
35
|
// recaps unprompted instead of sitting idle until a human types (the 15-minute silence, #5649).
|
|
36
|
-
const KICKOFF_PROMPT = "You have just taken over via handoff.
|
|
36
|
+
const KICKOFF_PROMPT = "You have just taken over via handoff. FIRST open every file it names as read-first — memory, PRD, TDD — before you answer or touch anything; the summary points at them, it does not replace them (#8162). THEN recap per your instructions.";
|
|
37
37
|
|
|
38
38
|
// The pane, resolved exactly like the app does (orch_pane_from_rows): last orch row wins.
|
|
39
39
|
export function orchPane(rows, project) {
|
package/hooks/lib/handoff.mjs
CHANGED
|
@@ -794,9 +794,74 @@ export function maybeSpawn(projectDir, conf = readConfig(), handoffFile = "", de
|
|
|
794
794
|
} catch (e) { process.stderr.write(`[trantor] maybeSpawn error: ${e?.message}\n`); return false; }
|
|
795
795
|
}
|
|
796
796
|
|
|
797
|
+
/** The files a handoff tells its successor to read before doing anything (#8162).
|
|
798
|
+
*
|
|
799
|
+
* A handoff has always been able to SAY "read these first" — crebral-health's named three memory
|
|
800
|
+
* files on 2026-09-19 and the successor opened none of them. Saying it was the whole mechanism.
|
|
801
|
+
* This pulls the list out as data so something downstream can check it.
|
|
802
|
+
*
|
|
803
|
+
* Recognised: a `READ FIRST` / `READ-FIRST` / `Read first:` heading or line, and every path-looking
|
|
804
|
+
* token on it and the lines beneath it until the next blank line or heading. Deliberately narrow —
|
|
805
|
+
* a handoff that mentions a file in passing is not asking anyone to read it, and a gate that fires
|
|
806
|
+
* on every path in a 4k summary would be noise the successor learns to ignore.
|
|
807
|
+
* @returns {string[]} project-relative or absolute paths, de-duplicated, capped
|
|
808
|
+
*/
|
|
809
|
+
export function readFirstPaths(summary, { max = 12 } = {}) {
|
|
810
|
+
const text = String(summary || "");
|
|
811
|
+
const out = [];
|
|
812
|
+
const lines = text.split(/\r?\n/);
|
|
813
|
+
let collecting = false;
|
|
814
|
+
for (const raw of lines) {
|
|
815
|
+
const line = raw.trim();
|
|
816
|
+
const opens = /read[\s-]?first/i.test(line);
|
|
817
|
+
if (!collecting && !opens) continue;
|
|
818
|
+
if (collecting && (line === "" || /^#{1,6}\s/.test(line))) break;
|
|
819
|
+
collecting = true;
|
|
820
|
+
// Paths: a token with a slash or a known doc/memory extension, stripped of markdown furniture.
|
|
821
|
+
for (const m of line.matchAll(/[`"'(\[]?([~./\w-]*[\w-]+\.(?:md|mjs|js|ts|tsx|json|sql|rs|py|sh|txt))[`"')\]]?/g)) {
|
|
822
|
+
const p = m[1].replace(/^[`"'(\[]+|[`"')\]]+$/g, "");
|
|
823
|
+
if (p && !out.includes(p)) out.push(p);
|
|
824
|
+
if (out.length >= max) return out;
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
return out;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
/** Which of `paths` this session actually OPENED, read off its own transcript (#8162).
|
|
831
|
+
*
|
|
832
|
+
* Ground truth rather than testimony, the same rule the state gate learned the hard way: a
|
|
833
|
+
* successor saying "I have read the handoff" is not evidence that it did. A Read/Grep/Glob tool
|
|
834
|
+
* call naming the path is. Matches on basename as well as full path, because a handoff written by
|
|
835
|
+
* a model may name `revenue-integrity-build.md` where the Read call carries the absolute path.
|
|
836
|
+
* @returns {{read: string[], missed: string[]}}
|
|
837
|
+
*/
|
|
838
|
+
export function pathsReadIn(transcriptPath, paths) {
|
|
839
|
+
const want = (paths || []).filter(Boolean);
|
|
840
|
+
if (!want.length) return { read: [], missed: [] };
|
|
841
|
+
let body = "";
|
|
842
|
+
try { body = readFileSync(transcriptPath, "utf8"); } catch { return { read: [], missed: want }; }
|
|
843
|
+
// Only the tool CALLS count. A path quoted in the injected handoff summary, or in the model's own
|
|
844
|
+
// prose, must never read as "opened" — that is precisely the proxy this card exists to kill.
|
|
845
|
+
const opened = new Set();
|
|
846
|
+
for (const m of body.matchAll(/"name"\s*:\s*"(Read|Grep|Glob|NotebookRead)"\s*,\s*"input"\s*:\s*\{([^}]*)\}/g)) {
|
|
847
|
+
for (const f of m[2].matchAll(/"(?:file_path|path|pattern)"\s*:\s*"([^"]+)"/g)) opened.add(f[1]);
|
|
848
|
+
}
|
|
849
|
+
const hit = (p) => {
|
|
850
|
+
const base = p.split("/").pop();
|
|
851
|
+
for (const o of opened) if (o === p || o.endsWith("/" + p) || (base && o.endsWith("/" + base)) || o === base) return true;
|
|
852
|
+
return false;
|
|
853
|
+
};
|
|
854
|
+
const read = want.filter(hit);
|
|
855
|
+
return { read, missed: want.filter(p => !read.includes(p)) };
|
|
856
|
+
}
|
|
857
|
+
|
|
797
858
|
// The self-announcing fresh session command (single-quoted so it survives osascript→shell un-escaped).
|
|
798
|
-
// Brevity is part of the prompt: a takeover that answers with 5k-character status dumps loses the
|
|
799
|
-
|
|
859
|
+
// Brevity is part of the prompt: a takeover that answers with 5k-character status dumps loses the
|
|
860
|
+
// operator. But brevity is about what you SAY, and #8162 found it had quietly become permission not
|
|
861
|
+
// to READ: the summary is injected at SessionStart, so a 3-sentence recap is producible without
|
|
862
|
+
// opening a file, and a successor doing exactly as asked never opened one. Three days of takeovers
|
|
863
|
+
// went straight to code off a summary. So the order is now read-then-recap, and the recap stays short.
|
|
864
|
+
export const RECAP_CMD = "claude 'You have just taken over via handoff. FIRST open every file the handoff names as read-first — its memory files, its PRD and TDD — and do not answer until you have. They are the context the handoff exists to carry, and the summary is a pointer to them, not a substitute. THEN recap in at most 3 sentences: task, state, next step. Then wait for me. Keep all replies short by default: no status tables, no headers, no walls of text unless I explicitly ask for detail.'";
|
|
800
865
|
|
|
801
866
|
// ONE suppression check for every path that can open a terminal window: two names for it once let a
|
|
802
867
|
// drill set the wrong one and open eight live sessions in deleted temp directories, so both are honoured.
|
package/hooks/sessionstart.mjs
CHANGED
|
@@ -10,7 +10,7 @@ import { fileURLToPath } from "node:url";
|
|
|
10
10
|
import { resolveProject, hostId, resolveHubInfo, knownProjects, nonSeatReason, nestedProjects, handoffDir, readOrchSession, writeOrchSession } from "../lib/project.mjs";
|
|
11
11
|
import { formatSubagentManifest } from "../lib/subagent-manifest.mjs";
|
|
12
12
|
import { updateAvailable, maybeNotifyDesktop, readConfig } from "./lib/update-check.mjs";
|
|
13
|
-
import { renderStateBlock } from "./lib/handoff.mjs";
|
|
13
|
+
import { renderStateBlock, readFirstPaths} from "./lib/handoff.mjs";
|
|
14
14
|
import { maybeCheckBalances } from "./lib/balance-check.mjs";
|
|
15
15
|
import { getJSON, signedGet, signedPost, loadIdentity } from "./lib/api.mjs";
|
|
16
16
|
import { ledgerPaths, ensureStart, anchorCursor, writeCursor } from "./lib/inbox-ledger.mjs";
|
|
@@ -78,8 +78,11 @@ function loadPendingHandoff(projectName, { claim = true, freshSession = null } =
|
|
|
78
78
|
try {
|
|
79
79
|
// #5645: the mandate rides the stamp too, so prompt-focus's recap reminder pins
|
|
80
80
|
// the SAME rec.mode the injection below announces (attended=WAIT / unattended=RESUME).
|
|
81
|
+
// #8162: the READ-FIRST list rides the stamp as DATA, so the Stop hook can check the
|
|
82
|
+
// successor opened it instead of accepting a reply as proof it understood anything.
|
|
83
|
+
const readFirst = readFirstPaths(rec.summary || "");
|
|
81
84
|
writeFileSync(join(dir, `recap-pending-${String(freshSession.session_id).replace(/[^A-Za-z0-9_.-]/g, "_")}.json`),
|
|
82
|
-
JSON.stringify({ handoffId: rec.id, ts: nowSec(), mode: rec.mode === "unattended" ? "unattended" : "attended" }));
|
|
85
|
+
JSON.stringify({ handoffId: rec.id, ts: nowSec(), mode: rec.mode === "unattended" ? "unattended" : "attended", readFirst }));
|
|
83
86
|
} catch {}
|
|
84
87
|
}
|
|
85
88
|
}
|
|
@@ -499,9 +502,9 @@ try {
|
|
|
499
502
|
// attended (default) = recap-then-WAIT; unattended = recap-then-RESUME — the handoff's OPEN
|
|
500
503
|
// THREADS are the work order ("handoffs must never be a break").
|
|
501
504
|
if (handoff.mode === "unattended") {
|
|
502
|
-
additionalContext += `🔄 **You are taking over from a prior session that hit its context limit, in UNATTENDED (long-run) mode.** This is a fresh full window. Resume the work below — the prior session's summary, git state, and a pointer to its full transcript (searchable; Foundation/Gaia has it ingested) follow. Continue from "OPEN THREADS & NEXT STEPS"; do not restart from scratch.
|
|
505
|
+
additionalContext += `🔄 **You are taking over from a prior session that hit its context limit, in UNATTENDED (long-run) mode.** This is a fresh full window. Resume the work below — the prior session's summary, git state, and a pointer to its full transcript (searchable; Foundation/Gaia has it ingested) follow. Continue from "OPEN THREADS & NEXT STEPS"; do not restart from scratch.\n\n**READ BEFORE YOU BUILD (#8162).** This summary is a POINTER to the project's context, not a replacement for it. Open every file this handoff names as read-first — its memory files, its PRD and its TDD — BEFORE you touch code. Reading is not waiting, and it is not optional: three days running, successors recapped from this summary alone and went straight to code, burning a fifth of a context window rediscovering what was already written down. The ledger now checks which files you opened, not whether you replied.\n\nThen recap the task, state, and next step in at most 3 sentences, then RESUME the open threads — they are your work order. Do NOT wait for the user; keep building. Keep replies short: no status tables, no headers, no walls of text.\n\n`;
|
|
503
506
|
} else {
|
|
504
|
-
additionalContext += `🔄 **You are taking over from a prior session that hit its context limit.** This is a fresh full window. Resume the work below — the prior session's summary, git state, and a pointer to its full transcript (searchable; Foundation/Gaia has it ingested) follow. Continue from "OPEN THREADS & NEXT STEPS"; do not restart from scratch.
|
|
507
|
+
additionalContext += `🔄 **You are taking over from a prior session that hit its context limit.** This is a fresh full window. Resume the work below — the prior session's summary, git state, and a pointer to its full transcript (searchable; Foundation/Gaia has it ingested) follow. Continue from "OPEN THREADS & NEXT STEPS"; do not restart from scratch.\n\n**READ BEFORE YOU ANSWER (#8162).** This summary is a POINTER to the project's context, not a replacement for it. Open every file this handoff names as read-first — its memory files, its PRD and its TDD — before you recap. A 3-sentence recap is producible from this summary alone, which is exactly how three days of takeovers went straight to code without opening anything. The ledger now checks which files you opened, not whether you replied.\n\nThen recap the task, state, and next step in at most 3 sentences, then wait. Keep replies short: no status tables, no headers, no walls of text unless the user explicitly asks for detail.\n\n`;
|
|
505
508
|
}
|
|
506
509
|
// Verification gates FIRST — these are structured "must verify before shipping" claims the prior
|
|
507
510
|
// session couldn't independently prove. They go above the summary on purpose: a safety-critical
|
package/hooks/stop-inbox.mjs
CHANGED
|
@@ -11,7 +11,7 @@ import { homedir } from "node:os";
|
|
|
11
11
|
import { resolveProject, hostId, handoffDir, busDir } from "../lib/project.mjs";
|
|
12
12
|
import { signedGet } from "./lib/api.mjs"; // signed: enforce hubs 401 unsigned reads — unsigned, T2 delivery is silently dead
|
|
13
13
|
import { ledgerPaths, ensureStart, anchorCursor, writeCursor } from "./lib/inbox-ledger.mjs";
|
|
14
|
-
import { readArm, clearArm, markHandedOff, appendHandoffState, subagentsActive } from "./lib/handoff.mjs";
|
|
14
|
+
import { readArm, clearArm, markHandedOff, appendHandoffState, subagentsActive, pathsReadIn} from "./lib/handoff.mjs";
|
|
15
15
|
|
|
16
16
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
17
17
|
|
|
@@ -162,9 +162,27 @@ async function main() {
|
|
|
162
162
|
if (existsSync(stampPath)) {
|
|
163
163
|
try {
|
|
164
164
|
const stamp = JSON.parse(readFileSync(stampPath, "utf8"));
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
165
|
+
// #8162: RECAPPED used to mean "a reply exists by Stop time", which a successor satisfies
|
|
166
|
+
// from the injected summary alone — so a handoff naming read-first files certified a
|
|
167
|
+
// takeover that opened none of them, three days running. Now the ledger asks the same
|
|
168
|
+
// question the state gate learned to ask: ground truth, not testimony. A Read/Grep tool
|
|
169
|
+
// call naming the path is evidence; the successor's own say-so is not.
|
|
170
|
+
const want = Array.isArray(stamp.readFirst) ? stamp.readFirst : [];
|
|
171
|
+
const { missed } = want.length
|
|
172
|
+
? pathsReadIn(input.transcript_path || "", want)
|
|
173
|
+
: { missed: [] };
|
|
174
|
+
if (missed.length) {
|
|
175
|
+
// The stamp STAYS. Not recapped, so the next boundary asks again — and the successor is
|
|
176
|
+
// told exactly what it skipped rather than left to discover it when the operator does.
|
|
177
|
+
process.stderr.write(
|
|
178
|
+
`[trantor] handoff ${stamp.handoffId}: NOT recapped — the handoff named ${want.length} file(s) to read first and ${missed.length} ${missed.length === 1 ? "was" : "were"} never opened: ${missed.join(", ")}. Read them before going further; this is the context the handoff exists to carry.\n`);
|
|
179
|
+
} else {
|
|
180
|
+
appendHandoffState(stamp.handoffId, "recapped", sid);
|
|
181
|
+
try { unlinkSync(stampPath); } catch {}
|
|
182
|
+
}
|
|
183
|
+
} catch {
|
|
184
|
+
try { unlinkSync(stampPath); } catch {}
|
|
185
|
+
}
|
|
168
186
|
}
|
|
169
187
|
}
|
|
170
188
|
} catch {}
|