totemlayer 0.1.5 → 0.1.6
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/bin/collaboration-listener.mjs +83 -83
- package/bin/docker-test-sandbox.mjs +1 -1
- package/bin/mcp.mjs +1 -1
- package/bin/mission-worker.mjs +201 -47
- package/bin/totem.mjs +1 -1
- package/package.json +1 -1
|
@@ -7,9 +7,9 @@ import { setTimeout as sleep } from "node:timers/promises";
|
|
|
7
7
|
|
|
8
8
|
import { inspectLocalRepository } from "./repository.mjs";
|
|
9
9
|
import { localClientEnvironment, streamingClientInvocation } from "./platform-client.mjs";
|
|
10
|
+
import { runFile } from "./mission-worker.mjs";
|
|
10
11
|
|
|
11
12
|
const MAX_RUNS_PER_TASK = 16;
|
|
12
|
-
const MAX_OUTPUT_BYTES = 2 * 1024 * 1024;
|
|
13
13
|
const MAX_PROMPT_BYTES = 32 * 1024;
|
|
14
14
|
const DEFAULT_RUN_TIMEOUT_MS = 15 * 60 * 1000;
|
|
15
15
|
|
|
@@ -84,7 +84,10 @@ function runnerCommand(provider, sessionId, platform = process.platform) {
|
|
|
84
84
|
if (provider === "codex")
|
|
85
85
|
invocation = sessionId
|
|
86
86
|
? { command: "codex", args: ["exec", "resume", "--json", sessionId, "-"] }
|
|
87
|
-
: {
|
|
87
|
+
: {
|
|
88
|
+
command: "codex",
|
|
89
|
+
args: ["exec", "--sandbox", "workspace-write", "--json", "-"],
|
|
90
|
+
};
|
|
88
91
|
else if (provider === "claude")
|
|
89
92
|
invocation = {
|
|
90
93
|
command: "claude",
|
|
@@ -106,7 +109,9 @@ function runnerCommand(provider, sessionId, platform = process.platform) {
|
|
|
106
109
|
}
|
|
107
110
|
|
|
108
111
|
function runnerEnvironment(repositoryRoot, environment = process.env) {
|
|
109
|
-
return localClientEnvironment(environment, {
|
|
112
|
+
return localClientEnvironment(environment, {
|
|
113
|
+
TOTEM_REPOSITORY_ROOT: repositoryRoot,
|
|
114
|
+
});
|
|
110
115
|
}
|
|
111
116
|
|
|
112
117
|
export async function runLocalCollaborationAgent({
|
|
@@ -120,79 +125,42 @@ export async function runLocalCollaborationAgent({
|
|
|
120
125
|
spawnProcess = spawn,
|
|
121
126
|
}) {
|
|
122
127
|
const { command, args } = runnerCommand(provider, sessionId, platform);
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
let totalOutputBytes = 0;
|
|
132
|
-
let observedSession = sessionId ?? null;
|
|
133
|
-
let finalResponse = "";
|
|
134
|
-
let finished = false;
|
|
135
|
-
const fail = (error) => {
|
|
136
|
-
if (finished) return;
|
|
137
|
-
finished = true;
|
|
138
|
-
clearTimeout(timer);
|
|
139
|
-
signal?.removeEventListener("abort", abort);
|
|
140
|
-
rejectRun(error);
|
|
141
|
-
};
|
|
142
|
-
const abort = () => child.kill("SIGTERM");
|
|
143
|
-
const timer = setTimeout(() => child.kill("SIGTERM"), timeoutMs);
|
|
144
|
-
signal?.addEventListener("abort", abort, { once: true });
|
|
145
|
-
if (signal?.aborted) abort();
|
|
146
|
-
child.stdout.on("data", (chunk) => {
|
|
147
|
-
totalOutputBytes += chunk.byteLength;
|
|
148
|
-
output += chunk.toString("utf8");
|
|
149
|
-
if (totalOutputBytes > MAX_OUTPUT_BYTES) child.kill("SIGTERM");
|
|
150
|
-
if (provider !== "codex") return;
|
|
151
|
-
let newline;
|
|
152
|
-
while ((newline = output.indexOf("\n")) >= 0) {
|
|
153
|
-
const line = output.slice(0, newline);
|
|
154
|
-
output = output.slice(newline + 1);
|
|
155
|
-
try {
|
|
156
|
-
const event = JSON.parse(line);
|
|
157
|
-
if (event.type === "thread.started" && typeof event.thread_id === "string")
|
|
158
|
-
observedSession = event.thread_id;
|
|
159
|
-
if (event.type === "item.completed" && event.item?.type === "agent_message")
|
|
160
|
-
finalResponse = String(event.item.text ?? "");
|
|
161
|
-
} catch {
|
|
162
|
-
// Ignore non-JSON progress lines; never echo model output to CLI logs.
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
});
|
|
166
|
-
child.stderr.on("data", () => {});
|
|
167
|
-
child.once("error", (error) => fail(error));
|
|
168
|
-
child.once("close", (code) => {
|
|
169
|
-
if (finished) return;
|
|
170
|
-
if (signal?.aborted) return fail(new Error("The collaboration listener was stopped."));
|
|
171
|
-
if (code !== 0)
|
|
172
|
-
return fail(
|
|
173
|
-
new Error(
|
|
174
|
-
`The ${provider} run exited with status ${code ?? "unknown"}. Check that client's local logs and permissions.`,
|
|
175
|
-
),
|
|
176
|
-
);
|
|
177
|
-
if (provider === "claude") {
|
|
178
|
-
try {
|
|
179
|
-
const result = JSON.parse(output);
|
|
180
|
-
observedSession = result.session_id ?? observedSession;
|
|
181
|
-
finalResponse = typeof result.result === "string" ? result.result : "";
|
|
182
|
-
if (result.is_error) return fail(new Error("Claude reported a failed run."));
|
|
183
|
-
} catch {
|
|
184
|
-
return fail(new Error("Claude returned an invalid JSON result."));
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
if (!observedSession)
|
|
188
|
-
return fail(new Error(`${provider} did not return a resumable session ID.`));
|
|
189
|
-
finished = true;
|
|
190
|
-
clearTimeout(timer);
|
|
191
|
-
signal?.removeEventListener("abort", abort);
|
|
192
|
-
resolveRun({ sessionId: observedSession, finalResponse });
|
|
193
|
-
});
|
|
194
|
-
child.stdin.end(prompt);
|
|
128
|
+
const { stdout } = await runFile(command, args, {
|
|
129
|
+
cwd: repositoryRoot,
|
|
130
|
+
env: runnerEnvironment(repositoryRoot),
|
|
131
|
+
input: prompt,
|
|
132
|
+
signal,
|
|
133
|
+
timeoutMs,
|
|
134
|
+
platform,
|
|
135
|
+
spawnProcess,
|
|
195
136
|
});
|
|
137
|
+
let observedSession = sessionId ?? null;
|
|
138
|
+
let finalResponse = "";
|
|
139
|
+
if (provider === "claude") {
|
|
140
|
+
let result;
|
|
141
|
+
try {
|
|
142
|
+
result = JSON.parse(stdout);
|
|
143
|
+
} catch {
|
|
144
|
+
throw new Error("Claude returned an invalid JSON result.");
|
|
145
|
+
}
|
|
146
|
+
observedSession = result.session_id ?? observedSession;
|
|
147
|
+
finalResponse = typeof result.result === "string" ? result.result : "";
|
|
148
|
+
if (result.is_error) throw new Error("Claude reported a failed run.");
|
|
149
|
+
} else {
|
|
150
|
+
for (const line of stdout.split("\n")) {
|
|
151
|
+
try {
|
|
152
|
+
const event = JSON.parse(line);
|
|
153
|
+
if (event.type === "thread.started" && typeof event.thread_id === "string")
|
|
154
|
+
observedSession = event.thread_id;
|
|
155
|
+
if (event.type === "item.completed" && event.item?.type === "agent_message")
|
|
156
|
+
finalResponse = String(event.item.text ?? "");
|
|
157
|
+
} catch {
|
|
158
|
+
// Ignore non-JSON progress lines; never echo model output to CLI logs.
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (!observedSession) throw new Error(`${provider} did not return a resumable session ID.`);
|
|
163
|
+
return { sessionId: observedSession, finalResponse };
|
|
196
164
|
}
|
|
197
165
|
|
|
198
166
|
async function readJournal(file) {
|
|
@@ -216,7 +184,12 @@ async function latestOutboundTaskCursor(client, taskId, cursor, signal) {
|
|
|
216
184
|
let latest = cursor;
|
|
217
185
|
for (let page = 0; page < 20; page++) {
|
|
218
186
|
const result = await client.messages.list(
|
|
219
|
-
{
|
|
187
|
+
{
|
|
188
|
+
direction: "outbound",
|
|
189
|
+
taskId,
|
|
190
|
+
limit: 100,
|
|
191
|
+
...(latest ? { cursor: latest } : {}),
|
|
192
|
+
},
|
|
220
193
|
{ signal },
|
|
221
194
|
);
|
|
222
195
|
const messages = result.data ?? [];
|
|
@@ -247,7 +220,9 @@ export async function runCollaborationListener({
|
|
|
247
220
|
if (!Number.isInteger(pollMs) || pollMs < 1000 || pollMs > 60000)
|
|
248
221
|
throw new Error("--poll-ms must be between 1000 and 60000.");
|
|
249
222
|
const root = resolve(repositoryRoot);
|
|
250
|
-
const repository = await inspectLocalRepository({
|
|
223
|
+
const repository = await inspectLocalRepository({
|
|
224
|
+
TOTEM_REPOSITORY_ROOT: root,
|
|
225
|
+
});
|
|
251
226
|
const identity = await client.agent({ signal });
|
|
252
227
|
const peer = await client.resolve(peerAddress, { signal });
|
|
253
228
|
if (!peer?.id || peer.address !== peerAddress) throw new Error("The allowed peer was not found.");
|
|
@@ -267,7 +242,12 @@ export async function runCollaborationListener({
|
|
|
267
242
|
}
|
|
268
243
|
try {
|
|
269
244
|
const journal = await readJournal(journalFile);
|
|
270
|
-
const binding = {
|
|
245
|
+
const binding = {
|
|
246
|
+
agentId: identity.id,
|
|
247
|
+
peerAddress,
|
|
248
|
+
repository: repository.repository,
|
|
249
|
+
root,
|
|
250
|
+
};
|
|
271
251
|
if (journal.binding && JSON.stringify(journal.binding) !== JSON.stringify(binding))
|
|
272
252
|
throw new Error("This journal belongs to a different agent, peer, or checkout.");
|
|
273
253
|
if (!journal.binding) {
|
|
@@ -301,7 +281,11 @@ export async function runCollaborationListener({
|
|
|
301
281
|
continue;
|
|
302
282
|
}
|
|
303
283
|
if (prior?.status === "started" || prior?.status === "failed") {
|
|
304
|
-
emit({
|
|
284
|
+
emit({
|
|
285
|
+
type: "needs_attention",
|
|
286
|
+
messageId: message.id,
|
|
287
|
+
taskId: message.taskId,
|
|
288
|
+
});
|
|
305
289
|
continue;
|
|
306
290
|
}
|
|
307
291
|
const task = await client.tasks.get(message.taskId, { signal });
|
|
@@ -352,7 +336,12 @@ export async function runCollaborationListener({
|
|
|
352
336
|
const outboundBefore = await latestOutboundTaskCursor(client, task.id, undefined, signal);
|
|
353
337
|
journal.messages[message.id] = { status: "started", taskId: task.id };
|
|
354
338
|
await saveJournal(journalFile, journal);
|
|
355
|
-
emit({
|
|
339
|
+
emit({
|
|
340
|
+
type: "started",
|
|
341
|
+
messageId: message.id,
|
|
342
|
+
taskId: task.id,
|
|
343
|
+
provider,
|
|
344
|
+
});
|
|
356
345
|
try {
|
|
357
346
|
const result = await runAgent({
|
|
358
347
|
provider,
|
|
@@ -377,12 +366,23 @@ export async function runCollaborationListener({
|
|
|
377
366
|
sessionId: result.sessionId,
|
|
378
367
|
runCount: (savedTask?.runCount ?? 0) + 1,
|
|
379
368
|
};
|
|
380
|
-
journal.messages[message.id] = {
|
|
369
|
+
journal.messages[message.id] = {
|
|
370
|
+
status: "completed",
|
|
371
|
+
taskId: task.id,
|
|
372
|
+
};
|
|
381
373
|
await saveJournal(journalFile, journal);
|
|
382
374
|
await client.messages.ack(message.id, { signal });
|
|
383
|
-
emit({
|
|
375
|
+
emit({
|
|
376
|
+
type: "completed",
|
|
377
|
+
messageId: message.id,
|
|
378
|
+
taskId: task.id,
|
|
379
|
+
provider,
|
|
380
|
+
});
|
|
384
381
|
} catch (error) {
|
|
385
|
-
journal.messages[message.id] = {
|
|
382
|
+
journal.messages[message.id] = {
|
|
383
|
+
status: "failed",
|
|
384
|
+
taskId: task.id,
|
|
385
|
+
};
|
|
386
386
|
await saveJournal(journalFile, journal);
|
|
387
387
|
await client.tasks
|
|
388
388
|
.postUpdate(task.id, listenerStatusText(provider, "failed"), {
|
|
@@ -36,7 +36,7 @@ export async function runDockerNodeTests(checkout, signal, runFile) {
|
|
|
36
36
|
"--env",
|
|
37
37
|
"TMPDIR=/tmp",
|
|
38
38
|
"--mount",
|
|
39
|
-
`type=bind,source=${root},target=/workspace`,
|
|
39
|
+
`type=bind,source=${root},target=/workspace,readonly`,
|
|
40
40
|
"--workdir",
|
|
41
41
|
"/workspace",
|
|
42
42
|
NODE_TEST_IMAGE,
|
package/bin/mcp.mjs
CHANGED
|
@@ -8,7 +8,7 @@ import * as z from "zod/v4";
|
|
|
8
8
|
import { Totem } from "./sdk/index.js";
|
|
9
9
|
import { inspectLocalRepository, verifyLocalRepoClaim } from "./repository.mjs";
|
|
10
10
|
|
|
11
|
-
const VERSION = "0.1.
|
|
11
|
+
const VERSION = "0.1.6";
|
|
12
12
|
const MAX_KEY_BYTES = 8 * 1024;
|
|
13
13
|
const MAX_ATTACHMENT_BYTES = 2 * 1024 * 1024;
|
|
14
14
|
const collaborationSchema = z
|
package/bin/mission-worker.mjs
CHANGED
|
@@ -52,7 +52,10 @@ const SOURCE_CHANGE = {
|
|
|
52
52
|
...FILE_CHANGE,
|
|
53
53
|
properties: {
|
|
54
54
|
...FILE_CHANGE.properties,
|
|
55
|
-
path: {
|
|
55
|
+
path: {
|
|
56
|
+
...CODE_PATH,
|
|
57
|
+
pattern: CODE_PATH.pattern.replace("^(src|test|tests)/", "^src/"),
|
|
58
|
+
},
|
|
56
59
|
},
|
|
57
60
|
};
|
|
58
61
|
const TEST_CHANGE = {
|
|
@@ -98,7 +101,10 @@ const OUTPUT_SCHEMAS = {
|
|
|
98
101
|
},
|
|
99
102
|
engineering: {
|
|
100
103
|
summary: TEXT,
|
|
101
|
-
verdict: {
|
|
104
|
+
verdict: {
|
|
105
|
+
type: "string",
|
|
106
|
+
enum: ["feasible", "changes_required", "rejected"],
|
|
107
|
+
},
|
|
102
108
|
findings: STRINGS,
|
|
103
109
|
spec: TEXT,
|
|
104
110
|
candidateTitle: TEXT,
|
|
@@ -113,11 +119,19 @@ const OUTPUT_SCHEMAS = {
|
|
|
113
119
|
code: {
|
|
114
120
|
summary: TEXT,
|
|
115
121
|
changes: { type: "array", items: SOURCE_CHANGE, minItems: 1, maxItems: 3 },
|
|
116
|
-
testChanges: {
|
|
122
|
+
testChanges: {
|
|
123
|
+
type: "array",
|
|
124
|
+
items: TEST_CHANGE,
|
|
125
|
+
minItems: 1,
|
|
126
|
+
maxItems: 1,
|
|
127
|
+
},
|
|
117
128
|
},
|
|
118
129
|
review: {
|
|
119
130
|
summary: TEXT,
|
|
120
|
-
verdict: {
|
|
131
|
+
verdict: {
|
|
132
|
+
type: "string",
|
|
133
|
+
enum: ["approved", "changes_required", "rejected"],
|
|
134
|
+
},
|
|
121
135
|
findings: STRINGS,
|
|
122
136
|
probes: { type: "array", items: REVIEW_PROBE, maxItems: 3 },
|
|
123
137
|
},
|
|
@@ -252,7 +266,12 @@ function rejectedResponseEvidence(response) {
|
|
|
252
266
|
} catch {
|
|
253
267
|
// Malformed text cannot be safely interpreted as credential-bearing fields.
|
|
254
268
|
// Preserve a fingerprint and length without storing its raw fragments.
|
|
255
|
-
return {
|
|
269
|
+
return {
|
|
270
|
+
...metadata,
|
|
271
|
+
response: null,
|
|
272
|
+
responseOmitted: "malformed_json",
|
|
273
|
+
truncated: false,
|
|
274
|
+
};
|
|
256
275
|
}
|
|
257
276
|
}
|
|
258
277
|
|
|
@@ -449,7 +468,9 @@ function structuredEvent(stage, response) {
|
|
|
449
468
|
outcome: output.outcome.slice(0, 4000),
|
|
450
469
|
selected: output.selected,
|
|
451
470
|
...(typeof output.selectedCandidateTitle === "string"
|
|
452
|
-
? {
|
|
471
|
+
? {
|
|
472
|
+
selectedCandidateTitle: output.selectedCandidateTitle.slice(0, 160),
|
|
473
|
+
}
|
|
453
474
|
: {}),
|
|
454
475
|
...(typeof output.rationale === "string"
|
|
455
476
|
? { rationale: output.rationale.slice(0, 4000) }
|
|
@@ -461,7 +482,10 @@ function structuredEvent(stage, response) {
|
|
|
461
482
|
break;
|
|
462
483
|
return {
|
|
463
484
|
type: "completion",
|
|
464
|
-
payload: {
|
|
485
|
+
payload: {
|
|
486
|
+
summary: output.summary.slice(0, 4000),
|
|
487
|
+
result: output.result,
|
|
488
|
+
},
|
|
465
489
|
};
|
|
466
490
|
}
|
|
467
491
|
throw new Error("The agent returned a malformed stage result.");
|
|
@@ -540,13 +564,22 @@ export function verifiedHandoff(mission, events, response) {
|
|
|
540
564
|
export function runFile(
|
|
541
565
|
command,
|
|
542
566
|
args,
|
|
543
|
-
{
|
|
567
|
+
{
|
|
568
|
+
cwd,
|
|
569
|
+
input,
|
|
570
|
+
signal,
|
|
571
|
+
timeoutMs = 15 * 60_000,
|
|
572
|
+
env,
|
|
573
|
+
acceptNonZero = false,
|
|
574
|
+
platform = process.platform,
|
|
575
|
+
spawnProcess = spawn,
|
|
576
|
+
} = {},
|
|
544
577
|
) {
|
|
545
578
|
if (signal?.aborted)
|
|
546
579
|
return Promise.reject(signal.reason ?? new Error("The command was interrupted."));
|
|
547
580
|
return new Promise((resolveRun, rejectRun) => {
|
|
548
|
-
const grouped =
|
|
549
|
-
const child =
|
|
581
|
+
const grouped = platform !== "win32";
|
|
582
|
+
const child = spawnProcess(command, args, {
|
|
550
583
|
cwd,
|
|
551
584
|
env,
|
|
552
585
|
shell: false,
|
|
@@ -597,13 +630,24 @@ export function runFile(
|
|
|
597
630
|
if (!grouped) {
|
|
598
631
|
// Windows has no POSIX process groups; taskkill /T includes descendants.
|
|
599
632
|
await new Promise((resolveKill, rejectKill) => {
|
|
600
|
-
const killer =
|
|
633
|
+
const killer = spawnProcess("taskkill", ["/PID", String(child.pid), "/T", "/F"], {
|
|
601
634
|
shell: false,
|
|
602
635
|
stdio: "ignore",
|
|
603
636
|
windowsHide: true,
|
|
604
637
|
});
|
|
605
|
-
|
|
606
|
-
|
|
638
|
+
const killTimer = setTimeout(() => {
|
|
639
|
+
killer.kill("SIGKILL");
|
|
640
|
+
rejectKill(new Error("Windows process-tree cleanup timed out."));
|
|
641
|
+
}, 5_000);
|
|
642
|
+
killer.once("error", (error) => {
|
|
643
|
+
clearTimeout(killTimer);
|
|
644
|
+
rejectKill(error);
|
|
645
|
+
});
|
|
646
|
+
killer.once("close", (code) => {
|
|
647
|
+
clearTimeout(killTimer);
|
|
648
|
+
if (code === 0) resolveKill();
|
|
649
|
+
else rejectKill(new Error(`Windows process-tree cleanup exited with status ${code}.`));
|
|
650
|
+
});
|
|
607
651
|
});
|
|
608
652
|
return;
|
|
609
653
|
}
|
|
@@ -622,6 +666,8 @@ export function runFile(
|
|
|
622
666
|
// Give the kernel time to reap killed descendants before checkout cleanup.
|
|
623
667
|
for (let wait = 0; wait < 20 && groupExists(); wait += 1) await sleep(25);
|
|
624
668
|
})().catch((cleanupError) => {
|
|
669
|
+
// Report an uncertain cleanup failure even if the client never closes.
|
|
670
|
+
closed = true;
|
|
625
671
|
stopError = new Error(
|
|
626
672
|
`${command} was interrupted but process cleanup failed: ${cleanupError.message}`,
|
|
627
673
|
);
|
|
@@ -1021,7 +1067,15 @@ function testSourceTokens(source) {
|
|
|
1021
1067
|
}
|
|
1022
1068
|
if (char === "\\") {
|
|
1023
1069
|
const escaped = source[index++];
|
|
1024
|
-
const simple = {
|
|
1070
|
+
const simple = {
|
|
1071
|
+
n: "\n",
|
|
1072
|
+
r: "\r",
|
|
1073
|
+
t: "\t",
|
|
1074
|
+
b: "\b",
|
|
1075
|
+
f: "\f",
|
|
1076
|
+
v: "\v",
|
|
1077
|
+
0: "\0",
|
|
1078
|
+
};
|
|
1025
1079
|
if (escaped === "\n") continue;
|
|
1026
1080
|
if (escaped === "\r") {
|
|
1027
1081
|
if (source[index] === "\n") index += 1;
|
|
@@ -1294,7 +1348,9 @@ async function existingNodeTestTitles(checkout) {
|
|
|
1294
1348
|
let entriesSeen = 0;
|
|
1295
1349
|
let bytes = 0;
|
|
1296
1350
|
const visit = async (relative = "") => {
|
|
1297
|
-
for (const entry of await readdir(join(checkout, relative), {
|
|
1351
|
+
for (const entry of await readdir(join(checkout, relative), {
|
|
1352
|
+
withFileTypes: true,
|
|
1353
|
+
})) {
|
|
1298
1354
|
if ([".git", "node_modules"].includes(entry.name)) continue;
|
|
1299
1355
|
if (++entriesSeen > 5000)
|
|
1300
1356
|
throw new Error(
|
|
@@ -1498,7 +1554,10 @@ async function runStructuredMissionAgent({
|
|
|
1498
1554
|
...TEST_CHANGE,
|
|
1499
1555
|
properties: {
|
|
1500
1556
|
...TEST_CHANGE.properties,
|
|
1501
|
-
path: {
|
|
1557
|
+
path: {
|
|
1558
|
+
...TEST_CHANGE.properties.path,
|
|
1559
|
+
const: regressionFile.path,
|
|
1560
|
+
},
|
|
1502
1561
|
},
|
|
1503
1562
|
},
|
|
1504
1563
|
},
|
|
@@ -1511,7 +1570,11 @@ async function runStructuredMissionAgent({
|
|
|
1511
1570
|
required: Object.keys(properties),
|
|
1512
1571
|
additionalProperties: false,
|
|
1513
1572
|
};
|
|
1514
|
-
const endpoint = missionModelEndpoint(provider, {
|
|
1573
|
+
const endpoint = missionModelEndpoint(provider, {
|
|
1574
|
+
ollamaUrl,
|
|
1575
|
+
qwenUrl,
|
|
1576
|
+
nimUrl,
|
|
1577
|
+
});
|
|
1515
1578
|
model ??=
|
|
1516
1579
|
provider === "qwen-local"
|
|
1517
1580
|
? "qwen3.5-9b"
|
|
@@ -1556,7 +1619,11 @@ async function runStructuredMissionAgent({
|
|
|
1556
1619
|
: {
|
|
1557
1620
|
think: useReasoning,
|
|
1558
1621
|
format: schema,
|
|
1559
|
-
options: {
|
|
1622
|
+
options: {
|
|
1623
|
+
num_ctx: 16_384,
|
|
1624
|
+
num_predict: generationLimit,
|
|
1625
|
+
temperature: 0.1,
|
|
1626
|
+
},
|
|
1560
1627
|
}),
|
|
1561
1628
|
messages: [
|
|
1562
1629
|
{
|
|
@@ -1585,7 +1652,10 @@ async function runStructuredMissionAgent({
|
|
|
1585
1652
|
});
|
|
1586
1653
|
} catch (error) {
|
|
1587
1654
|
if (rejectionEvidenceDirectory) {
|
|
1588
|
-
await mkdir(rejectionEvidenceDirectory, {
|
|
1655
|
+
await mkdir(rejectionEvidenceDirectory, {
|
|
1656
|
+
recursive: true,
|
|
1657
|
+
mode: 0o700,
|
|
1658
|
+
});
|
|
1589
1659
|
await chmod(rejectionEvidenceDirectory, 0o700);
|
|
1590
1660
|
await saveJournal(join(rejectionEvidenceDirectory, `response-${attempt + 1}.json`), {
|
|
1591
1661
|
capturedAt: new Date().toISOString(),
|
|
@@ -1654,7 +1724,9 @@ export async function preflightMissionRepository(mission, repositoryRoot) {
|
|
|
1654
1724
|
const base = mission.repository?.baseRevision;
|
|
1655
1725
|
if (!mission.repository || !COMMIT.test(base ?? ""))
|
|
1656
1726
|
throw new Error("The mission needs a selected repository with a full commit revision.");
|
|
1657
|
-
const local = await inspectLocalRepository({
|
|
1727
|
+
const local = await inspectLocalRepository({
|
|
1728
|
+
TOTEM_REPOSITORY_ROOT: repositoryRoot,
|
|
1729
|
+
});
|
|
1658
1730
|
if (local.repository !== mission.repository.url)
|
|
1659
1731
|
throw new Error("The configured local repository does not match the mission repository.");
|
|
1660
1732
|
if ((await git(repositoryRoot, "cat-file", "-t", base)) !== "commit")
|
|
@@ -1676,7 +1748,9 @@ export async function prepareMissionCheckout(
|
|
|
1676
1748
|
throw new Error("Mission code work needs an immutable repository revision and work item ID.");
|
|
1677
1749
|
if (retryAttempt !== undefined && (!Number.isInteger(retryAttempt) || retryAttempt < 2))
|
|
1678
1750
|
throw new Error("A retry checkout needs a later server work-item attempt.");
|
|
1679
|
-
const local = await inspectLocalRepository({
|
|
1751
|
+
const local = await inspectLocalRepository({
|
|
1752
|
+
TOTEM_REPOSITORY_ROOT: repositoryRoot,
|
|
1753
|
+
});
|
|
1680
1754
|
if (local.repository !== mission.repository.url)
|
|
1681
1755
|
throw new Error("The selected local checkout does not match the mission repository.");
|
|
1682
1756
|
const base = await git(repositoryRoot, "rev-parse", mission.repository.baseRevision);
|
|
@@ -1714,7 +1788,10 @@ export async function prepareMissionCheckout(
|
|
|
1714
1788
|
} catch (error) {
|
|
1715
1789
|
if (error.code !== "ENOENT") throw error;
|
|
1716
1790
|
}
|
|
1717
|
-
await mkdir(join(stateDirectory, "checkouts"), {
|
|
1791
|
+
await mkdir(join(stateDirectory, "checkouts"), {
|
|
1792
|
+
recursive: true,
|
|
1793
|
+
mode: 0o700,
|
|
1794
|
+
});
|
|
1718
1795
|
await git(repositoryRoot, "worktree", "add", "-b", branch, checkout, startRevision);
|
|
1719
1796
|
return { checkout, branch, baseRevision: base, startRevision };
|
|
1720
1797
|
}
|
|
@@ -1732,7 +1809,9 @@ export async function prepareReviewCheckout(mission, workItem, repositoryRoot, s
|
|
|
1732
1809
|
!UUID.test(workItem.id)
|
|
1733
1810
|
)
|
|
1734
1811
|
throw new Error("Review requires a visible local code artifact with full Git revisions.");
|
|
1735
|
-
const local = await inspectLocalRepository({
|
|
1812
|
+
const local = await inspectLocalRepository({
|
|
1813
|
+
TOTEM_REPOSITORY_ROOT: repositoryRoot,
|
|
1814
|
+
});
|
|
1736
1815
|
if (local.repository !== mission.repository.url)
|
|
1737
1816
|
throw new Error("The selected checkout does not match the mission repository.");
|
|
1738
1817
|
const [resolvedHead, mergeBase] = await Promise.all([
|
|
@@ -1753,16 +1832,26 @@ export async function prepareReviewCheckout(mission, workItem, repositoryRoot, s
|
|
|
1753
1832
|
} catch (error) {
|
|
1754
1833
|
if (error.code !== "ENOENT") throw error;
|
|
1755
1834
|
}
|
|
1756
|
-
await mkdir(join(stateDirectory, "checkouts"), {
|
|
1835
|
+
await mkdir(join(stateDirectory, "checkouts"), {
|
|
1836
|
+
recursive: true,
|
|
1837
|
+
mode: 0o700,
|
|
1838
|
+
});
|
|
1757
1839
|
await git(repositoryRoot, "worktree", "add", "--detach", checkout, head);
|
|
1758
|
-
return {
|
|
1840
|
+
return {
|
|
1841
|
+
checkout,
|
|
1842
|
+
baseRevision: base,
|
|
1843
|
+
headCommit: head,
|
|
1844
|
+
branch: codeArtifact.metadata?.branch,
|
|
1845
|
+
};
|
|
1759
1846
|
}
|
|
1760
1847
|
|
|
1761
1848
|
export async function prepareBaseReadCheckout(mission, workItem, repositoryRoot, stateDirectory) {
|
|
1762
1849
|
const base = mission.repository?.baseRevision;
|
|
1763
1850
|
if (!mission.repository || !COMMIT.test(base ?? "") || !UUID.test(workItem.id))
|
|
1764
1851
|
throw new Error("Engineering needs a selected immutable repository revision.");
|
|
1765
|
-
const local = await inspectLocalRepository({
|
|
1852
|
+
const local = await inspectLocalRepository({
|
|
1853
|
+
TOTEM_REPOSITORY_ROOT: repositoryRoot,
|
|
1854
|
+
});
|
|
1766
1855
|
if (local.repository !== mission.repository.url)
|
|
1767
1856
|
throw new Error("The local repository does not match the mission selection.");
|
|
1768
1857
|
const resolved = await git(repositoryRoot, "rev-parse", base);
|
|
@@ -1777,7 +1866,10 @@ export async function prepareBaseReadCheckout(mission, workItem, repositoryRoot,
|
|
|
1777
1866
|
} catch (error) {
|
|
1778
1867
|
if (error.code !== "ENOENT") throw error;
|
|
1779
1868
|
}
|
|
1780
|
-
await mkdir(join(stateDirectory, "checkouts"), {
|
|
1869
|
+
await mkdir(join(stateDirectory, "checkouts"), {
|
|
1870
|
+
recursive: true,
|
|
1871
|
+
mode: 0o700,
|
|
1872
|
+
});
|
|
1781
1873
|
await git(repositoryRoot, "worktree", "add", "--detach", checkout, base);
|
|
1782
1874
|
return { checkout, baseRevision: base };
|
|
1783
1875
|
}
|
|
@@ -1950,7 +2042,10 @@ export async function runIndependentReviewProbes(checkout, changedFiles, probes,
|
|
|
1950
2042
|
try {
|
|
1951
2043
|
await writeFile(probePath, contents, { flag: "wx", mode: 0o600 });
|
|
1952
2044
|
const check = await runIsolatedNodeTests(checkoutPath, signal);
|
|
1953
|
-
return {
|
|
2045
|
+
return {
|
|
2046
|
+
...check,
|
|
2047
|
+
command: `${check.command}; independent reviewer probes`,
|
|
2048
|
+
};
|
|
1954
2049
|
} finally {
|
|
1955
2050
|
await rm(probePath, { force: true });
|
|
1956
2051
|
}
|
|
@@ -2106,7 +2201,9 @@ async function settleKnownLocalTestFailure(
|
|
|
2106
2201
|
checkoutInfo: record.checkoutInfo,
|
|
2107
2202
|
failure: record.failure,
|
|
2108
2203
|
});
|
|
2109
|
-
const remote = await client.missions.listEffects(missionId, workItemId, {
|
|
2204
|
+
const remote = await client.missions.listEffects(missionId, workItemId, {
|
|
2205
|
+
signal,
|
|
2206
|
+
});
|
|
2110
2207
|
for (const [index, effectId] of record.effectIds.entries()) {
|
|
2111
2208
|
const effect = remote.find((item) => item.id === effectId);
|
|
2112
2209
|
if (
|
|
@@ -2171,7 +2268,11 @@ async function recoverKnownLocalTestFailures(
|
|
|
2171
2268
|
);
|
|
2172
2269
|
} catch (error) {
|
|
2173
2270
|
if (error.status === 401 || error.status === 403) throw error;
|
|
2174
|
-
emit({
|
|
2271
|
+
emit({
|
|
2272
|
+
type: "recovery_waiting",
|
|
2273
|
+
workItemId,
|
|
2274
|
+
message: String(error.message ?? error),
|
|
2275
|
+
});
|
|
2175
2276
|
}
|
|
2176
2277
|
}
|
|
2177
2278
|
return handled;
|
|
@@ -2207,7 +2308,9 @@ async function settleCompletedResult(client, missionId, workItemId, path, record
|
|
|
2207
2308
|
throw new CompletedResultReconciliationFailed(
|
|
2208
2309
|
"The completed local result cannot be safely reconciled.",
|
|
2209
2310
|
);
|
|
2210
|
-
const remote = await client.missions.listEffects(missionId, workItemId, {
|
|
2311
|
+
const remote = await client.missions.listEffects(missionId, workItemId, {
|
|
2312
|
+
signal,
|
|
2313
|
+
});
|
|
2211
2314
|
const effects = record.effectIds.map((id, index) => {
|
|
2212
2315
|
const effect = remote.find((entry) => entry.id === id);
|
|
2213
2316
|
if (
|
|
@@ -2284,7 +2387,10 @@ async function recoverReadyCompletions(
|
|
|
2284
2387
|
"Review the saved result and its lease or assignment before continuing. The worker will not repeat completed actions.",
|
|
2285
2388
|
},
|
|
2286
2389
|
},
|
|
2287
|
-
{
|
|
2390
|
+
{
|
|
2391
|
+
idempotencyKey: key(agentId, workItemId, "completion-blocker"),
|
|
2392
|
+
signal,
|
|
2393
|
+
},
|
|
2288
2394
|
);
|
|
2289
2395
|
await saveJournal(path, { ...blocked, blockerRecorded: true });
|
|
2290
2396
|
};
|
|
@@ -2301,7 +2407,11 @@ async function recoverReadyCompletions(
|
|
|
2301
2407
|
completionInput(workItemId, record.leaseToken, record.result),
|
|
2302
2408
|
{ idempotencyKey: key(agentId, workItemId, "complete"), signal },
|
|
2303
2409
|
);
|
|
2304
|
-
await saveJournal(path, {
|
|
2410
|
+
await saveJournal(path, {
|
|
2411
|
+
state: "done",
|
|
2412
|
+
stage: record.stage,
|
|
2413
|
+
result: record.result,
|
|
2414
|
+
});
|
|
2305
2415
|
emit({ type: "work_recovered", workItemId });
|
|
2306
2416
|
} catch (error) {
|
|
2307
2417
|
if (error.status === 401 || error.status === 403) throw error;
|
|
@@ -2323,7 +2433,11 @@ async function recoverReadyCompletions(
|
|
|
2323
2433
|
}
|
|
2324
2434
|
continue;
|
|
2325
2435
|
}
|
|
2326
|
-
emit({
|
|
2436
|
+
emit({
|
|
2437
|
+
type: "recovery_waiting",
|
|
2438
|
+
workItemId,
|
|
2439
|
+
message: String(error.message ?? error),
|
|
2440
|
+
});
|
|
2327
2441
|
}
|
|
2328
2442
|
}
|
|
2329
2443
|
return handled;
|
|
@@ -2509,7 +2623,10 @@ export async function runMissionWorker({
|
|
|
2509
2623
|
"Assign a runtime that supports this work item, or set its stage to a supported kind.",
|
|
2510
2624
|
},
|
|
2511
2625
|
},
|
|
2512
|
-
{
|
|
2626
|
+
{
|
|
2627
|
+
idempotencyKey: key(agent.id, workItem.id, "unsupported-stage"),
|
|
2628
|
+
signal,
|
|
2629
|
+
},
|
|
2513
2630
|
)
|
|
2514
2631
|
.catch(() => {});
|
|
2515
2632
|
emit({
|
|
@@ -2525,7 +2642,11 @@ export async function runMissionWorker({
|
|
|
2525
2642
|
if (prior?.state === "claim_blocked") {
|
|
2526
2643
|
if (!reportedAttention.has(workItem.id)) {
|
|
2527
2644
|
await recordRecoveryBlocker(prior.pendingWorkItem, prior.message);
|
|
2528
|
-
emit({
|
|
2645
|
+
emit({
|
|
2646
|
+
type: "needs_attention",
|
|
2647
|
+
workItemId: workItem.id,
|
|
2648
|
+
message: prior.message,
|
|
2649
|
+
});
|
|
2529
2650
|
}
|
|
2530
2651
|
continue;
|
|
2531
2652
|
}
|
|
@@ -2745,7 +2866,10 @@ export async function runMissionWorker({
|
|
|
2745
2866
|
// preceded the request, so another worker's later effects cannot
|
|
2746
2867
|
// be bypassed by a claim key which never owned that attempt.
|
|
2747
2868
|
if (claimRecord.previousState) {
|
|
2748
|
-
const restored = {
|
|
2869
|
+
const restored = {
|
|
2870
|
+
...claimRecord,
|
|
2871
|
+
state: claimRecord.previousState,
|
|
2872
|
+
};
|
|
2749
2873
|
for (const field of [
|
|
2750
2874
|
"claimKey",
|
|
2751
2875
|
"expectedPriorAttempt",
|
|
@@ -2769,7 +2893,11 @@ export async function runMissionWorker({
|
|
|
2769
2893
|
};
|
|
2770
2894
|
await saveJournal(journalPath, blocked);
|
|
2771
2895
|
await recordRecoveryBlocker(claimRecord.pendingWorkItem, blocked.message);
|
|
2772
|
-
emit({
|
|
2896
|
+
emit({
|
|
2897
|
+
type: "needs_attention",
|
|
2898
|
+
workItemId: workItem.id,
|
|
2899
|
+
message: blocked.message,
|
|
2900
|
+
});
|
|
2773
2901
|
continue;
|
|
2774
2902
|
}
|
|
2775
2903
|
// Another worker may have claimed a stale listing, or the mission may
|
|
@@ -2810,7 +2938,9 @@ export async function runMissionWorker({
|
|
|
2810
2938
|
}, 10_000);
|
|
2811
2939
|
const renew = setInterval(() => {
|
|
2812
2940
|
void client.missions
|
|
2813
|
-
.renew(missionId, workItem.id, claim.leaseToken, {
|
|
2941
|
+
.renew(missionId, workItem.id, claim.leaseToken, {
|
|
2942
|
+
signal: combined,
|
|
2943
|
+
})
|
|
2814
2944
|
.catch((error) => {
|
|
2815
2945
|
renewError = error;
|
|
2816
2946
|
workSignal.abort();
|
|
@@ -2909,7 +3039,11 @@ export async function runMissionWorker({
|
|
|
2909
3039
|
});
|
|
2910
3040
|
}
|
|
2911
3041
|
if (effectIds.length) {
|
|
2912
|
-
await saveJournal(journalPath, {
|
|
3042
|
+
await saveJournal(journalPath, {
|
|
3043
|
+
state: "dispatching",
|
|
3044
|
+
stage,
|
|
3045
|
+
effectIds,
|
|
3046
|
+
});
|
|
2913
3047
|
for (const effectId of effectIds) {
|
|
2914
3048
|
const dispatch = await client.missions.dispatchEffect(
|
|
2915
3049
|
missionId,
|
|
@@ -3029,7 +3163,9 @@ export async function runMissionWorker({
|
|
|
3029
3163
|
}
|
|
3030
3164
|
for (const event of promptEvents(events).filter((item) => UUID.test(item.id ?? ""))) {
|
|
3031
3165
|
try {
|
|
3032
|
-
await client.missions.events.ack(missionId, event.id, {
|
|
3166
|
+
await client.missions.events.ack(missionId, event.id, {
|
|
3167
|
+
signal: combined,
|
|
3168
|
+
});
|
|
3033
3169
|
} catch (error) {
|
|
3034
3170
|
emit({
|
|
3035
3171
|
type: "event_ack_pending",
|
|
@@ -3046,7 +3182,10 @@ export async function runMissionWorker({
|
|
|
3046
3182
|
for (let repair = 0; repair < 3; repair += 1) {
|
|
3047
3183
|
await authorizeLocalLaunch();
|
|
3048
3184
|
const check = await runIsolatedNodeTests(checkout, combined);
|
|
3049
|
-
commands.push({
|
|
3185
|
+
commands.push({
|
|
3186
|
+
command: check.command,
|
|
3187
|
+
exitCode: check.exitCode,
|
|
3188
|
+
});
|
|
3050
3189
|
localTestRuns.push(check);
|
|
3051
3190
|
if (check.exitCode === 0) break;
|
|
3052
3191
|
if (repair === 2) throw new LocalCodeTestsFailed(localTestRuns);
|
|
@@ -3186,12 +3325,18 @@ export async function runMissionWorker({
|
|
|
3186
3325
|
if (reviewCheck)
|
|
3187
3326
|
generated.commands = [
|
|
3188
3327
|
...(generated.commands ?? []),
|
|
3189
|
-
{
|
|
3328
|
+
{
|
|
3329
|
+
command: reviewCheck.command,
|
|
3330
|
+
exitCode: reviewCheck.exitCode,
|
|
3331
|
+
},
|
|
3190
3332
|
];
|
|
3191
3333
|
if (reviewProbeCheck)
|
|
3192
3334
|
generated.commands = [
|
|
3193
3335
|
...(generated.commands ?? []),
|
|
3194
|
-
{
|
|
3336
|
+
{
|
|
3337
|
+
command: reviewProbeCheck.command,
|
|
3338
|
+
exitCode: reviewProbeCheck.exitCode,
|
|
3339
|
+
},
|
|
3195
3340
|
];
|
|
3196
3341
|
const resultSummary = handoff?.summary ?? generated.text.slice(0, 2000);
|
|
3197
3342
|
result = {
|
|
@@ -3224,7 +3369,10 @@ export async function runMissionWorker({
|
|
|
3224
3369
|
contentText: handoff
|
|
3225
3370
|
? JSON.stringify(handoff.detail, null, 2).slice(0, 32000)
|
|
3226
3371
|
: generated.text.slice(0, 32000),
|
|
3227
|
-
metadata: {
|
|
3372
|
+
metadata: {
|
|
3373
|
+
...(evidence ?? {}),
|
|
3374
|
+
commands: generated.commands ?? [],
|
|
3375
|
+
},
|
|
3228
3376
|
},
|
|
3229
3377
|
};
|
|
3230
3378
|
if (effectIds.length) {
|
|
@@ -3260,7 +3408,10 @@ export async function runMissionWorker({
|
|
|
3260
3408
|
missionId,
|
|
3261
3409
|
workItem.id,
|
|
3262
3410
|
completionInput(workItem.id, claim.leaseToken, result),
|
|
3263
|
-
{
|
|
3411
|
+
{
|
|
3412
|
+
idempotencyKey: key(agent.id, workItem.id, "complete"),
|
|
3413
|
+
signal: combined,
|
|
3414
|
+
},
|
|
3264
3415
|
);
|
|
3265
3416
|
await saveJournal(journalPath, { state: "done", stage, result });
|
|
3266
3417
|
reportedAttention.delete(workItem.id);
|
|
@@ -3301,7 +3452,10 @@ export async function runMissionWorker({
|
|
|
3301
3452
|
prior?.state === "local_tests_failed"
|
|
3302
3453
|
? (prior.verifiedFailedEffectIds ?? prior.effectIds ?? [])
|
|
3303
3454
|
: [],
|
|
3304
|
-
failure: {
|
|
3455
|
+
failure: {
|
|
3456
|
+
checks: error.checks,
|
|
3457
|
+
completedAt: new Date().toISOString(),
|
|
3458
|
+
},
|
|
3305
3459
|
};
|
|
3306
3460
|
await saveJournal(journalPath, record);
|
|
3307
3461
|
knownLocalTestFailure = true;
|
package/bin/totem.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import { createHash, randomUUID } from "node:crypto";
|
|
|
4
4
|
import { createReadStream } from "node:fs";
|
|
5
5
|
import { stripVTControlCharacters } from "node:util";
|
|
6
6
|
|
|
7
|
-
const CLI_VERSION = "0.1.
|
|
7
|
+
const CLI_VERSION = "0.1.6";
|
|
8
8
|
const DEFAULT_BASE_URL = "https://totemlayer.com";
|
|
9
9
|
const API_KEY_PATTERN = /^at_(?:live|test)_[A-Za-z0-9_-]{20,}$/;
|
|
10
10
|
const ACTION_PATTERN = /^[a-z0-9][a-z0-9._:-]*$/;
|