trantor 0.18.88 → 0.18.90
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/drill-report.mjs +10 -2
- package/bin/drill-report.test.mjs +61 -3
- package/bin/keys.mjs +13 -5
- package/lib/identity.mjs +15 -3
- package/lib/key-prune.mjs +2 -3
- package/lib/test-identity-names.mjs +4 -0
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trantor",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.90",
|
|
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/drill-report.mjs
CHANGED
|
@@ -60,6 +60,7 @@ export class DrillReport {
|
|
|
60
60
|
|
|
61
61
|
exitCode() {
|
|
62
62
|
// Manual recipes stay visible as SKIP; only missing/failed proof or a refused close is red.
|
|
63
|
+
// "gone" is neither: the seam proved, the card just isn't on the board anymore (#11498).
|
|
63
64
|
return Object.values(this.results()).some(result => result.status === "fail" || result.closure.startsWith("failed:")) ? 1 : 0;
|
|
64
65
|
}
|
|
65
66
|
|
|
@@ -70,11 +71,18 @@ export class DrillReport {
|
|
|
70
71
|
id: Number(id), project: this.project, by: this.session, status: "done",
|
|
71
72
|
note: `Drill: trantor drill PASS\n${result.evidence.join("\n")}`.slice(0, 2000), // "Drill:" prefix: the hub's done gate (#6452) reads it as the drill line
|
|
72
73
|
}, { project: this.project, session: this.session, timeoutMs: 15000 });
|
|
74
|
+
// A 404 on /task/update is the hub saying the card isn't on the board — the fleet cap deletes
|
|
75
|
+
// old cards (#11142), so a green drill can hold a stale map. That is not a failed seam: record
|
|
76
|
+
// it as gone and stay green. Any other refusal (403 cross-project, 409 done-gate, 500) is a
|
|
77
|
+
// real hub "no" and stays red (#11498). Never renumber the map to chase new card ids.
|
|
73
78
|
this.closures[id] = response.ok && response.json?.task?.status === "done"
|
|
74
79
|
&& response.json.task.id === Number(id) && response.json.task.project === this.project
|
|
75
|
-
? "done"
|
|
80
|
+
? "done"
|
|
81
|
+
: response.status === 404 && response.json?.error === "no such task" ? "gone: card no longer on the board"
|
|
82
|
+
: `failed: hub ${response.status} ${response.json?.error || "did not confirm done"}`;
|
|
76
83
|
this.write();
|
|
77
84
|
}
|
|
78
|
-
|
|
85
|
+
// "gone" closures are not failures — the verdict must not red on a stale map.
|
|
86
|
+
return Object.values(this.closures).every(value => !value.startsWith("failed:"));
|
|
79
87
|
}
|
|
80
88
|
}
|
|
@@ -3,6 +3,7 @@ import assert from "node:assert/strict";
|
|
|
3
3
|
import { mkdtempSync, mkdirSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { createServer, createConnection } from "node:net";
|
|
6
|
+
import { createServer as createHttpServer } from "node:http";
|
|
6
7
|
import { DrillReport } from "./drill-report.mjs";
|
|
7
8
|
import { CARD_STEPS, findHandoff } from "./drill-surface.mjs";
|
|
8
9
|
import { appVerdict, startDrillHub, stopChild, checkSocketHome } from "./drill-seams.mjs";
|
|
@@ -72,12 +73,14 @@ test("signed closer on an enforce hub: complete evidence closes, partial/failure
|
|
|
72
73
|
assert.equal(blocked.results()[blockedId].status, status);
|
|
73
74
|
assert.equal((await signedGet("/tasks?project=trantor")).json.tasks.find(row => row.id === blockedId).status, "testing");
|
|
74
75
|
}
|
|
76
|
+
// #11498: a green seam mapped to a card the board no longer has (the fleet cap deletes old
|
|
77
|
+
// cards, #11142) — the hub's 404 "no such task" is a stale map, recorded as gone, never red.
|
|
75
78
|
const missing = new DrillReport({ 999999: { steps: ["step"], autoClose: true } }, path, { project, session });
|
|
76
79
|
missing.record("step", "pass", "proof");
|
|
77
80
|
missing.complete("step");
|
|
78
|
-
assert.equal(await missing.closePassed(),
|
|
79
|
-
assert.
|
|
80
|
-
assert.equal(missing.exitCode(),
|
|
81
|
+
assert.equal(await missing.closePassed(), true);
|
|
82
|
+
assert.equal(missing.results()[999999].closure, "gone: card no longer on the board");
|
|
83
|
+
assert.equal(missing.exitCode(), 0);
|
|
81
84
|
const fresh = new DrillReport({ [id]: { steps: ["one"], autoClose: true } }, path, { project, session });
|
|
82
85
|
assert.equal(fresh.results()[id].status, "fail", "a rerun cannot inherit old PASS evidence");
|
|
83
86
|
} finally {
|
|
@@ -88,6 +91,61 @@ test("signed closer on an enforce hub: complete evidence closes, partial/failure
|
|
|
88
91
|
}
|
|
89
92
|
});
|
|
90
93
|
|
|
94
|
+
test("gone vs refused (#11498): a 404 no-such-task close is green, any other refusal stays red", async () => {
|
|
95
|
+
const out = join(import.meta.dirname, "..", ".agent-bus-out");
|
|
96
|
+
mkdirSync(out, { recursive: true });
|
|
97
|
+
const world = mkdtempSync(join(out, "refusal-test-"));
|
|
98
|
+
const bus = join(world, "bus");
|
|
99
|
+
mkdirSync(bus, { recursive: true });
|
|
100
|
+
// A stub hub that refuses per card id: 777 is gone (404 no-such-task, the fleet cap's answer,
|
|
101
|
+
// #11142), 4242 is a cross-project refusal, 5150 a server error — the closes a drill must keep red.
|
|
102
|
+
const answers = { 777: [404, "no such task"], 4242: [403, "cross-project writes are refused"], 5150: [500, "hub exploded"] };
|
|
103
|
+
const stub = createHttpServer((req, res) => {
|
|
104
|
+
let body = "";
|
|
105
|
+
req.on("data", d => { body += d; });
|
|
106
|
+
req.on("end", () => {
|
|
107
|
+
let id = null; try { id = JSON.parse(body).id; } catch {}
|
|
108
|
+
const [code, error] = answers[id] || [404, "no such task"];
|
|
109
|
+
res.writeHead(code, { "content-type": "application/json" });
|
|
110
|
+
res.end(JSON.stringify({ error }));
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
await new Promise(resolve => stub.listen(0, "127.0.0.1", resolve));
|
|
114
|
+
const saved = { ...process.env };
|
|
115
|
+
Object.assign(process.env, { RELAY_URL: `http://127.0.0.1:${stub.address().port}`, AGENT_BUS_DIR: bus, RELAY_SESSION: "drill:trantor", RELAY_PROJECT: "trantor" });
|
|
116
|
+
const ctx = { project: "trantor", session: "drill:trantor" };
|
|
117
|
+
try {
|
|
118
|
+
const report = (id) => {
|
|
119
|
+
const r = new DrillReport({ [id]: { steps: ["step"], autoClose: true } }, join(world, `result-${id}.json`), ctx);
|
|
120
|
+
r.record("step", "pass", "proof", "real evidence");
|
|
121
|
+
r.complete("step");
|
|
122
|
+
return r;
|
|
123
|
+
};
|
|
124
|
+
const gone = report(777);
|
|
125
|
+
assert.equal(await gone.closePassed(), true, "404 no-such-task is a stale map, not a failed seam");
|
|
126
|
+
assert.equal(gone.results()[777].closure, "gone: card no longer on the board");
|
|
127
|
+
assert.equal(gone.exitCode(), 0);
|
|
128
|
+
|
|
129
|
+
// gone + refused together: one real hub refusal among gones still reds the drill
|
|
130
|
+
const mixed = new DrillReport({ 777: { steps: ["step"], autoClose: true }, 4242: { steps: ["step"], autoClose: true } }, join(world, "result-mixed.json"), ctx);
|
|
131
|
+
for (const id of [777, 4242]) { mixed.record("step", "pass", "proof", "real evidence"); mixed.complete("step"); }
|
|
132
|
+
assert.equal(await mixed.closePassed(), false);
|
|
133
|
+
assert.equal(mixed.results()[777].closure, "gone: card no longer on the board");
|
|
134
|
+
assert.match(mixed.results()[4242].closure, /^failed: hub 403/);
|
|
135
|
+
assert.equal(mixed.exitCode(), 1);
|
|
136
|
+
|
|
137
|
+
const dead = report(5150);
|
|
138
|
+
assert.equal(await dead.closePassed(), false, "a server error is a real refusal");
|
|
139
|
+
assert.match(dead.results()[5150].closure, /^failed: hub 500/);
|
|
140
|
+
assert.equal(dead.exitCode(), 1);
|
|
141
|
+
} finally {
|
|
142
|
+
for (const key of Object.keys(process.env)) if (!(key in saved)) delete process.env[key];
|
|
143
|
+
Object.assign(process.env, saved);
|
|
144
|
+
stub.close();
|
|
145
|
+
rmSync(world, { recursive: true, force: true });
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
|
|
91
149
|
test("app proof requires every leg and rejects skipped key targets", () => {
|
|
92
150
|
assert.deepEqual(CARD_STEPS[6317].steps, ["S6-key-post", "S6-key-throw"]);
|
|
93
151
|
assert.equal(appVerdict("ask", "ask-drill PASS:"), false);
|
package/bin/keys.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// trantor keys — identity private keys in the macOS Keychain instead of plaintext files (#11370).
|
|
3
|
-
// status [--json] · migrate [--dry-run] [--json] · prune [--apply] [--idle-days N]
|
|
3
|
+
// status [--json] · migrate [name...] [--include-tests] [--dry-run] [--json] · prune [--apply] [--idle-days N]
|
|
4
4
|
// migrate copies each file-held key into the Keychain (service trantor-identity), proves a
|
|
5
5
|
// signature round-trip, and only then rewrites the file as a pubkey-only stub. Keys are never printed.
|
|
6
6
|
import { readFileSync } from "node:fs";
|
|
@@ -15,7 +15,7 @@ const verb = args[0] || "status";
|
|
|
15
15
|
const JSON_MODE = args.includes("--json");
|
|
16
16
|
const C = { dim: "\x1b[2m", grn: "\x1b[32m", yel: "\x1b[33m", red: "\x1b[31m", off: "\x1b[0m" };
|
|
17
17
|
const usage = () => {
|
|
18
|
-
console.error("usage: trantor keys status [--json] | migrate [--dry-run] [--json] | prune [--apply] [--idle-days N] [--json]");
|
|
18
|
+
console.error("usage: trantor keys status [--json] | migrate [name...] [--include-tests] [--dry-run] [--json] | prune [--apply] [--idle-days N] [--json]");
|
|
19
19
|
process.exit(1);
|
|
20
20
|
};
|
|
21
21
|
const tilde = (p) => String(p).replace(process.env.HOME || "", "~");
|
|
@@ -37,14 +37,22 @@ function status() {
|
|
|
37
37
|
|
|
38
38
|
function migrate() {
|
|
39
39
|
const dryRun = args.includes("--dry-run");
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
const flags = new Set(["--dry-run", "--json", "--include-tests"]);
|
|
41
|
+
const names = [];
|
|
42
|
+
for (const arg of args.slice(1)) {
|
|
43
|
+
if (flags.has(arg)) continue;
|
|
44
|
+
if (arg.startsWith("--")) throw new Error(`unknown migrate option: ${arg}`);
|
|
45
|
+
names.push(arg);
|
|
46
|
+
}
|
|
47
|
+
const r = migrateKeysToKeychain({ dryRun, names, includeTests: args.includes("--include-tests") });
|
|
48
|
+
if (JSON_MODE) { console.log(JSON.stringify(r, null, 2)); if (r.failed.length) process.exitCode = 1; return; }
|
|
49
|
+
if (!r.usable && !dryRun) {
|
|
43
50
|
console.log("no Keychain on this platform (or TRANTOR_NO_KEYCHAIN=1) — identity keys stay in files; nothing to do");
|
|
44
51
|
return;
|
|
45
52
|
}
|
|
46
53
|
const did = dryRun ? "would move" : "moved";
|
|
47
54
|
console.log(`${dryRun ? `${C.dim}dry run${C.off} · ` : ""}keychain service: ${r.service}`);
|
|
55
|
+
console.log(`skipped ${r.skippedTests} test identities (use --include-tests to include them)`);
|
|
48
56
|
for (const n of r.moved) console.log(` ${C.grn}✓${C.off} ${did} ${n}${dryRun ? "" : " (file is now a pubkey-only stub)"}`);
|
|
49
57
|
for (const n of r.already) console.log(` ${C.dim}already in the keychain: ${n}${C.off}`);
|
|
50
58
|
for (const f of r.failed) console.log(` ${C.red}✗${C.off} ${f.name}: ${f.reason} — the file was left untouched`);
|
package/lib/identity.mjs
CHANGED
|
@@ -5,6 +5,7 @@ import { readFileSync, writeFileSync, existsSync, mkdirSync, renameSync, chmodSy
|
|
|
5
5
|
import { spawnSync } from "node:child_process";
|
|
6
6
|
import { join } from "node:path";
|
|
7
7
|
import { homedir } from "node:os";
|
|
8
|
+
import { isTestIdentity } from "./test-identity-names.mjs";
|
|
8
9
|
import { asFunction, asString } from "./decode.mjs";
|
|
9
10
|
|
|
10
11
|
export const SCHEME = "trantor-v1";
|
|
@@ -227,9 +228,11 @@ function writeKeyStub(f, id) {
|
|
|
227
228
|
// Copy each file-held private key into the Keychain, prove the stored copy SIGNS (a round-trip
|
|
228
229
|
// against the file's pubkey), and only then rewrite the file as a pubkey-only stub. A key that
|
|
229
230
|
// fails to store or to verify keeps its file untouched.
|
|
230
|
-
export function migrateKeysToKeychain({ dryRun = false } = {}) {
|
|
231
|
-
const result = { service: IDENTITY_SERVICE, usable: keychainUsable(), moved: [], already: [], failed: [], dryRun };
|
|
232
|
-
if (!result.usable) return result;
|
|
231
|
+
export function migrateKeysToKeychain({ dryRun = false, names = [], includeTests = false } = {}) {
|
|
232
|
+
const result = { service: IDENTITY_SERVICE, usable: keychainUsable(), moved: [], already: [], failed: [], skippedTests: 0, dryRun };
|
|
233
|
+
if (!result.usable && !dryRun) return result;
|
|
234
|
+
const requested = new Set(names);
|
|
235
|
+
const found = new Set();
|
|
233
236
|
for (const fileBase of listKeyNames()) {
|
|
234
237
|
let id = null;
|
|
235
238
|
try { id = JSON.parse(readFileSync(keyPath(fileBase), "utf8")); } catch {}
|
|
@@ -238,6 +241,12 @@ export function migrateKeysToKeychain({ dryRun = false } = {}) {
|
|
|
238
241
|
// raw name, so storing under "name_proj" while hooks look for "name:proj" would orphan
|
|
239
242
|
// every migrated key on its first read.
|
|
240
243
|
const name = id?.name ?? fileBase;
|
|
244
|
+
if (requested.size && !requested.has(name)) continue;
|
|
245
|
+
found.add(name);
|
|
246
|
+
if (!requested.size && !includeTests && isTestIdentity(name)) {
|
|
247
|
+
result.skippedTests++;
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
241
250
|
if (!id?.pubkey) { result.failed.push({ name, reason: "unreadable key file" }); continue; }
|
|
242
251
|
if (!id.privkey) { result.already.push(name); continue; }
|
|
243
252
|
if (dryRun) { result.moved.push(name); continue; }
|
|
@@ -255,6 +264,9 @@ export function migrateKeysToKeychain({ dryRun = false } = {}) {
|
|
|
255
264
|
result.failed.push({ name, reason: String(e?.message || e) });
|
|
256
265
|
}
|
|
257
266
|
}
|
|
267
|
+
for (const name of requested) {
|
|
268
|
+
if (!found.has(name)) result.failed.push({ name, reason: "identity not found" });
|
|
269
|
+
}
|
|
258
270
|
return result;
|
|
259
271
|
}
|
|
260
272
|
|
package/lib/key-prune.mjs
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { lstatSync, readFileSync, unlinkSync } from "node:fs";
|
|
2
2
|
import { keyPath, listKeyNames } from "./identity.mjs";
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
export const isTestIdentity = name => TEST_NAME.test(name);
|
|
4
|
+
import { isTestIdentity } from "./test-identity-names.mjs";
|
|
5
|
+
export { isTestIdentity } from "./test-identity-names.mjs";
|
|
7
6
|
|
|
8
7
|
export function pruneTestKeys({ snapshots, idleDays = 7, apply = false, now = Date.now() }) {
|
|
9
8
|
if (!Number.isFinite(idleDays) || idleDays <= 0) throw new Error("--idle-days must be a positive number");
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
// Exact fixture families with numeric run IDs; a generic test/demo substring is not enough.
|
|
2
|
+
// Bare defer/supersede PID names come from test/handoff/test-handoff.mjs (#11501).
|
|
3
|
+
const TEST_NAME = /^(?:[A-Za-z0-9.-]+[_:])?(?:(?:defer|supersede)-\d+|(?:relay-selftest|hoff-selftest|consumedby|tt-h5|pctest|trantor-handoff|trantor-supersede|trantor-defer|cap-e2e|injcap|manual-skill)-\d+(?:[_:.-][\w:.-]+)?)$/;
|
|
4
|
+
export const isTestIdentity = name => TEST_NAME.test(name);
|