@holoscript/holoscript-agent 2.0.0

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.
@@ -0,0 +1,1167 @@
1
+ // src/supervisor.ts
2
+ import { homedir } from "os";
3
+ import { join as join2 } from "path";
4
+
5
+ // src/holomesh-client.ts
6
+ var HolomeshClient = class {
7
+ constructor(opts) {
8
+ this.apiBase = opts.apiBase.replace(/\/$/, "");
9
+ this.bearer = opts.bearer;
10
+ this.teamId = opts.teamId;
11
+ this.fetchImpl = opts.fetchImpl ?? fetch;
12
+ }
13
+ async heartbeat(payload) {
14
+ await this.req("POST", `/team/${this.teamId}/presence`, payload);
15
+ }
16
+ async getOpenTasks() {
17
+ const data = await this.req(
18
+ "GET",
19
+ `/team/${this.teamId}/board`
20
+ );
21
+ return data.tasks ?? data.open ?? [];
22
+ }
23
+ async claim(taskId) {
24
+ return this.req("PATCH", `/team/${this.teamId}/board/${taskId}`, { action: "claim" });
25
+ }
26
+ async joinTeam() {
27
+ return this.req(
28
+ "POST",
29
+ `/team/${this.teamId}/join`,
30
+ {}
31
+ );
32
+ }
33
+ async sendMessageOnTask(taskId, body) {
34
+ await this.req("POST", `/team/${this.teamId}/message`, {
35
+ to: "team",
36
+ subject: `task:${taskId}`,
37
+ content: body
38
+ });
39
+ }
40
+ async markDone(taskId, summary, commitHash) {
41
+ await this.req("PATCH", `/team/${this.teamId}/board/${taskId}`, {
42
+ action: "done",
43
+ summary,
44
+ commitHash
45
+ });
46
+ }
47
+ // POST CAEL audit records for this agent. Server validator at
48
+ // packages/mcp-server/src/holomesh/routes/core-routes.ts:472-533 requires
49
+ // bearer == handle owner OR founder; the per-surface x402 bearer is the
50
+ // handle owner so this resolves correctly. Records that fail shape
51
+ // validation (layer_hashes != 7 elements, missing tick_iso/operation/
52
+ // fnv1a_chain) are silently dropped server-side, not rejected as a batch.
53
+ async postAuditRecords(handle, records) {
54
+ return this.req(
55
+ "POST",
56
+ `/agent/${encodeURIComponent(handle)}/audit`,
57
+ { records }
58
+ );
59
+ }
60
+ async whoAmI() {
61
+ const raw = await this.req("GET", "/me");
62
+ return {
63
+ agentId: raw.agentId,
64
+ surface: deriveSurface(raw.name),
65
+ wallet: raw.wallet
66
+ };
67
+ }
68
+ async req(method, path, body) {
69
+ const url = `${this.apiBase}${path}`;
70
+ const res = await this.fetchImpl(url, {
71
+ method,
72
+ headers: {
73
+ Authorization: `Bearer ${this.bearer}`,
74
+ "content-type": "application/json"
75
+ },
76
+ body: body ? JSON.stringify(body) : void 0
77
+ });
78
+ if (!res.ok) {
79
+ const txt = await res.text().catch(() => "");
80
+ throw new Error(`HoloMesh ${method} ${path} ${res.status}: ${txt.slice(0, 300)}`);
81
+ }
82
+ if (res.status === 204) return void 0;
83
+ return await res.json();
84
+ }
85
+ };
86
+ function deriveSurface(seatName) {
87
+ if (!seatName) return "unknown";
88
+ const n = seatName.toLowerCase();
89
+ if (n.startsWith("claudecode")) return "claude-code";
90
+ if (n.startsWith("cursor")) return "claude-cursor";
91
+ if (n.startsWith("claudedesktop")) return "claude-desktop";
92
+ if (n.startsWith("vscode-claude") || n.startsWith("claude-vscode")) return "claude-vscode";
93
+ if (n.startsWith("gemini")) return "gemini-antigravity";
94
+ if (n.startsWith("copilot")) return "copilot-vscode";
95
+ return "unknown";
96
+ }
97
+ function pickClaimableTask(tasks, brainCapabilityTags) {
98
+ const wanted = new Set(brainCapabilityTags.map((t) => t.toLowerCase()));
99
+ const open = tasks.filter((t) => t.status === "open" && !t.claimedBy);
100
+ const scored = open.map((t) => ({ task: t, score: scoreTask(t, wanted) })).filter((s) => s.score > 0).sort((a, b) => b.score - a.score || priority(a.task) - priority(b.task));
101
+ return scored[0]?.task;
102
+ }
103
+ function scoreTask(task, wanted) {
104
+ const tags = (task.tags ?? []).map((t) => t.toLowerCase());
105
+ const text = `${task.title} ${task.description ?? ""}`.toLowerCase();
106
+ let score = 0;
107
+ for (const tag of tags) if (wanted.has(tag)) score += 2;
108
+ for (const w of wanted) if (text.includes(w)) score += 1;
109
+ return score;
110
+ }
111
+ function priority(t) {
112
+ if (typeof t.priority === "number") return t.priority;
113
+ const map = { critical: 1, high: 2, medium: 4, low: 6 };
114
+ return map[String(t.priority).toLowerCase()] ?? 5;
115
+ }
116
+
117
+ // src/cael-builder.ts
118
+ import { createHash } from "crypto";
119
+ function sha(input) {
120
+ return createHash("sha256").update(input, "utf8").digest("hex");
121
+ }
122
+ function brainClassOf(brain) {
123
+ const p = String(brain.brainPath ?? "");
124
+ let m = p.match(/compositions[\\/]([\w-]+)-brain\.hsplus$/);
125
+ if (m) return m[1];
126
+ m = p.match(/([\w-]+)-brain\.hsplus$/);
127
+ if (m) return m[1];
128
+ m = p.match(/([\w-]+)\.hsplus$/);
129
+ if (m) return m[1];
130
+ const domain = String(brain.domain ?? "").trim();
131
+ if (domain && domain !== "unknown") return domain;
132
+ return "unknown";
133
+ }
134
+ function buildCaelRecord(input) {
135
+ const { identity, brain, task, messages, finalText, usage, costUsd, spentUsd, prevChain, runtimeVersion } = input;
136
+ const l0 = sha(brain.systemPrompt);
137
+ const l1 = sha(`${task.id}|${task.title}|${task.description ?? ""}`);
138
+ const l2 = sha(JSON.stringify(messages));
139
+ const l3 = sha(finalText);
140
+ const l4 = sha(JSON.stringify(usage));
141
+ const l5 = sha(`${costUsd.toFixed(6)}|${spentUsd.toFixed(6)}`);
142
+ const l6 = sha([l0, l1, l2, l3, l4, l5].join("|"));
143
+ const fnv1a_chain = sha(`${prevChain ?? ""}|${l6}`);
144
+ return {
145
+ tick_iso: (/* @__PURE__ */ new Date()).toISOString(),
146
+ layer_hashes: [l0, l1, l2, l3, l4, l5, l6],
147
+ operation: `task-executed:${task.id}`,
148
+ prev_hash: prevChain,
149
+ fnv1a_chain,
150
+ version_vector_fingerprint: `agent@${runtimeVersion}|brain@${brainClassOf(brain)}|provider@${identity.llmProvider}|model@${identity.llmModel}`,
151
+ brain_class: brainClassOf(brain)
152
+ };
153
+ }
154
+
155
+ // src/tools.ts
156
+ import { readFile, writeFile, readdir, mkdir, stat } from "fs/promises";
157
+ import { resolve, dirname } from "path";
158
+ import { spawn } from "child_process";
159
+ var ALLOWED_READ_ROOTS = [
160
+ "/root/msc-paper-22",
161
+ // Paper 22 mechanization inputs (scp'd by deploy)
162
+ "/root/holoscript-mesh",
163
+ // Read-only repo view (clone path on instance)
164
+ "/root/agent-output"
165
+ // Read back what we wrote
166
+ ];
167
+ var ALLOWED_WRITE_ROOTS = [
168
+ "/root/agent-output"
169
+ // Single write sink — keeps deliverables in one place
170
+ ];
171
+ var BASH_WHITELIST = [
172
+ "lake build",
173
+ "lake env",
174
+ "lake clean",
175
+ "lean ",
176
+ "ls ",
177
+ "ls\n",
178
+ "ls$",
179
+ "cat ",
180
+ "grep ",
181
+ "rg ",
182
+ "find ",
183
+ "wc ",
184
+ "head ",
185
+ "tail ",
186
+ "git status",
187
+ "git log",
188
+ "git diff",
189
+ "git show",
190
+ "pnpm --filter",
191
+ "pnpm vitest",
192
+ "vitest run",
193
+ "pwd",
194
+ "echo "
195
+ ];
196
+ var MESH_TOOLS = [
197
+ {
198
+ name: "read_file",
199
+ description: "Read a file from the agent sandbox. Allowed roots: /root/msc-paper-22, /root/holoscript-mesh, /root/agent-output. Returns the file content as text. Use this to inspect inputs scp'd to the instance (e.g. MSC/Invariants.lean).",
200
+ input_schema: {
201
+ type: "object",
202
+ properties: {
203
+ path: { type: "string", description: "Absolute path under an allowed read root" }
204
+ },
205
+ required: ["path"]
206
+ }
207
+ },
208
+ {
209
+ name: "list_dir",
210
+ description: "List entries in a directory under the agent sandbox. Same root restrictions as read_file. Returns a newline-separated list of entries.",
211
+ input_schema: {
212
+ type: "object",
213
+ properties: {
214
+ path: { type: "string", description: "Absolute path under an allowed read root" }
215
+ },
216
+ required: ["path"]
217
+ }
218
+ },
219
+ {
220
+ name: "write_file",
221
+ description: "Write a file to /root/agent-output/. This is the deliverable sink \u2014 anything you want to emit as task output (a Lean proof, a markdown report, a JSON dataset) goes here. Creates parent directories. Will refuse paths outside the write root.",
222
+ input_schema: {
223
+ type: "object",
224
+ properties: {
225
+ path: { type: "string", description: "Absolute path under /root/agent-output/" },
226
+ content: { type: "string", description: "File content to write (UTF-8)" }
227
+ },
228
+ required: ["path", "content"]
229
+ }
230
+ },
231
+ {
232
+ name: "bash",
233
+ description: "Run a shell command. Whitelisted prefixes only: lake build, lean, ls, cat, grep, find, wc, head, tail, git status/log/diff/show, pnpm --filter, vitest run, pwd, echo. Hard 60s wall timeout, 1MB stdout cap. Use for lake build / lean kernel-checks, git inspection, repo greps. Refuses rm, curl, ssh, sudo, eval.",
234
+ input_schema: {
235
+ type: "object",
236
+ properties: {
237
+ cmd: { type: "string", description: "Whitelisted shell command" },
238
+ cwd: { type: "string", description: "Optional working directory (defaults to /root)" }
239
+ },
240
+ required: ["cmd"]
241
+ }
242
+ }
243
+ ];
244
+ function isUnderRoot(absPath, root) {
245
+ const resolved = resolve(absPath);
246
+ const rootResolved = resolve(root);
247
+ return resolved === rootResolved || resolved.startsWith(rootResolved + "/");
248
+ }
249
+ function checkReadAllowed(path) {
250
+ if (!path.startsWith("/")) return `path must be absolute, got "${path}"`;
251
+ for (const root of ALLOWED_READ_ROOTS) {
252
+ if (isUnderRoot(path, root)) return null;
253
+ }
254
+ return `read denied \u2014 path "${path}" not under allowed roots: ${ALLOWED_READ_ROOTS.join(", ")}`;
255
+ }
256
+ function checkWriteAllowed(path) {
257
+ if (!path.startsWith("/")) return `path must be absolute, got "${path}"`;
258
+ for (const root of ALLOWED_WRITE_ROOTS) {
259
+ if (isUnderRoot(path, root)) return null;
260
+ }
261
+ return `write denied \u2014 path "${path}" not under allowed roots: ${ALLOWED_WRITE_ROOTS.join(", ")}`;
262
+ }
263
+ function checkBashAllowed(cmd) {
264
+ const trimmed = cmd.trim();
265
+ if (trimmed.length === 0) return "empty command";
266
+ if (/[;&|`$<>]|>>|\|\||&&/.test(trimmed)) {
267
+ return `command contains shell metachars (; & | \` $ < > >> || &&) \u2014 not allowed for safety`;
268
+ }
269
+ for (const prefix of BASH_WHITELIST) {
270
+ if (trimmed.startsWith(prefix.trim())) return null;
271
+ }
272
+ return `command not on whitelist. Allowed prefixes: ${BASH_WHITELIST.join(" / ")}`;
273
+ }
274
+ async function runTool(use) {
275
+ try {
276
+ if (use.name === "read_file") {
277
+ const path = use.input.path;
278
+ const denied = checkReadAllowed(path);
279
+ if (denied) return errResult(use.id, denied);
280
+ const text = await readFile(path, "utf8");
281
+ const truncated = text.length > 2e5 ? text.slice(0, 2e5) + `
282
+ \u2026[truncated, full file is ${text.length} bytes]` : text;
283
+ return okResult(use.id, truncated);
284
+ }
285
+ if (use.name === "list_dir") {
286
+ const path = use.input.path;
287
+ const denied = checkReadAllowed(path);
288
+ if (denied) return errResult(use.id, denied);
289
+ const entries = await readdir(path, { withFileTypes: true });
290
+ const lines = entries.map((e) => `${e.isDirectory() ? "d" : "-"} ${e.name}`);
291
+ return okResult(use.id, lines.join("\n"));
292
+ }
293
+ if (use.name === "write_file") {
294
+ const path = use.input.path;
295
+ const content = use.input.content;
296
+ const denied = checkWriteAllowed(path);
297
+ if (denied) return errResult(use.id, denied);
298
+ await mkdir(dirname(path), { recursive: true });
299
+ await writeFile(path, content, "utf8");
300
+ const s = await stat(path);
301
+ return okResult(use.id, `wrote ${s.size} bytes to ${path}`);
302
+ }
303
+ if (use.name === "bash") {
304
+ const cmd = use.input.cmd;
305
+ const cwd = use.input.cwd ?? "/root";
306
+ const denied = checkBashAllowed(cmd);
307
+ if (denied) return errResult(use.id, denied);
308
+ const result = await runBash(cmd, cwd);
309
+ return result.code === 0 ? okResult(use.id, result.stdout) : errResult(use.id, `exit=${result.code}
310
+ ${result.stderr || result.stdout}`);
311
+ }
312
+ return errResult(use.id, `unknown tool: ${use.name}`);
313
+ } catch (err) {
314
+ return errResult(use.id, err instanceof Error ? err.message : String(err));
315
+ }
316
+ }
317
+ function runBash(cmd, cwd) {
318
+ return new Promise((resolveProm) => {
319
+ const child = spawn("bash", ["-c", cmd], { cwd, env: process.env });
320
+ let stdout = "";
321
+ let stderr = "";
322
+ let killed = false;
323
+ const STDOUT_CAP = 1e6;
324
+ const TIMEOUT_MS = 6e4;
325
+ const killer = setTimeout(() => {
326
+ killed = true;
327
+ child.kill("SIGKILL");
328
+ }, TIMEOUT_MS);
329
+ child.stdout.on("data", (d) => {
330
+ if (stdout.length < STDOUT_CAP) stdout += d.toString("utf8");
331
+ });
332
+ child.stderr.on("data", (d) => {
333
+ if (stderr.length < STDOUT_CAP) stderr += d.toString("utf8");
334
+ });
335
+ child.on("error", (err) => {
336
+ clearTimeout(killer);
337
+ resolveProm({ code: 1, stdout, stderr: stderr + "\nspawn-error: " + err.message });
338
+ });
339
+ child.on("exit", (code) => {
340
+ clearTimeout(killer);
341
+ const finalStdout = stdout.length >= STDOUT_CAP ? stdout + `
342
+ \u2026[stdout truncated at ${STDOUT_CAP} bytes]` : stdout;
343
+ const note = killed ? `
344
+ [bash killed after ${TIMEOUT_MS}ms timeout]` : "";
345
+ resolveProm({ code: code ?? 1, stdout: finalStdout + note, stderr });
346
+ });
347
+ });
348
+ }
349
+ function okResult(id, content) {
350
+ return { type: "tool_result", tool_use_id: id, content };
351
+ }
352
+ function errResult(id, message) {
353
+ return { type: "tool_result", tool_use_id: id, content: message, is_error: true };
354
+ }
355
+
356
+ // src/runner.ts
357
+ var RUNTIME_VERSION = "1.0.0";
358
+ var AgentRunner = class {
359
+ constructor(opts) {
360
+ this.opts = opts;
361
+ this.stopped = false;
362
+ // CAEL audit hash chain — survives across ticks within a single runner
363
+ // process. On process restart it resets to null; the first post-restart
364
+ // record breaks the chain, which is honest (the runner has no memory of
365
+ // its prior chain state and shouldn't fake continuity). prev_hash=null
366
+ // is a valid value the audit-store accepts.
367
+ this.prevCaelChain = null;
368
+ // Self-recovery flag for the auto-rejoin path (task_1777112258989_eeyp).
369
+ // When the heartbeat returns 403 "Not a member of this team" — typical of
370
+ // a fresh Vast.ai worker whose provisioning didn't atomically /join, or of
371
+ // a worker whose membership was reaped — the runner calls mesh.joinTeam()
372
+ // ONCE per process and retries the heartbeat. After a successful rejoin
373
+ // we set this flag so subsequent 403s on the same process don't loop back
374
+ // into joinTeam (avoiding a retry storm if the team-cap is full or the
375
+ // join itself is permanently rejected). On process restart the flag
376
+ // resets, which is the correct semantics: a fresh process gets one fresh
377
+ // chance to self-rejoin. Discovered 2026-04-25 SSH-probing 5 fleet
378
+ // workers stuck in indefinite 403→tick-error→sleep→retry loops; without
379
+ // this, a fresh-deploy of an unjoined agent stays silent forever.
380
+ this.joinedThisProcess = false;
381
+ }
382
+ async tick() {
383
+ const { identity, brain, mesh, costGuard, provider, logger } = this.opts;
384
+ const log = logger ?? (() => void 0);
385
+ await this.heartbeatWithAutoRejoin();
386
+ if (costGuard.isOverBudget()) {
387
+ const state = costGuard.getState();
388
+ log({ ev: "over-budget", spentUsd: state.spentUsd, budget: identity.budgetUsdPerDay });
389
+ return {
390
+ action: "over-budget",
391
+ spentUsd: state.spentUsd,
392
+ remainingUsd: 0,
393
+ message: `daily budget $${identity.budgetUsdPerDay} exhausted`
394
+ };
395
+ }
396
+ const tasks = await mesh.getOpenTasks();
397
+ const target = pickClaimableTask(tasks, brain.capabilityTags);
398
+ if (!target) {
399
+ log({ ev: "no-claimable-task", open: tasks.length });
400
+ return {
401
+ action: "no-claimable-task",
402
+ spentUsd: costGuard.getState().spentUsd,
403
+ remainingUsd: costGuard.getRemainingUsd()
404
+ };
405
+ }
406
+ log({ ev: "claim", taskId: target.id, title: target.title });
407
+ await mesh.claim(target.id);
408
+ const start = Date.now();
409
+ const messages = [
410
+ { role: "system", content: brain.systemPrompt },
411
+ { role: "user", content: buildTaskPrompt(target) }
412
+ ];
413
+ let aggUsage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
414
+ let finalText = "";
415
+ let iters = 0;
416
+ const MAX_TOOL_ITERS = 30;
417
+ let lastResponse;
418
+ const toolsCalled = /* @__PURE__ */ new Set();
419
+ while (true) {
420
+ iters++;
421
+ if (iters > MAX_TOOL_ITERS) {
422
+ log({ ev: "tool-loop-cap", taskId: target.id, iters });
423
+ finalText = finalText || `[tool-loop hit ${MAX_TOOL_ITERS}-iter cap before final text]`;
424
+ break;
425
+ }
426
+ const resp = await provider.complete(
427
+ {
428
+ messages,
429
+ maxTokens: 4096,
430
+ temperature: 0.4,
431
+ tools: MESH_TOOLS
432
+ },
433
+ identity.llmModel
434
+ );
435
+ lastResponse = resp;
436
+ aggUsage = {
437
+ promptTokens: aggUsage.promptTokens + resp.usage.promptTokens,
438
+ completionTokens: aggUsage.completionTokens + resp.usage.completionTokens,
439
+ totalTokens: aggUsage.totalTokens + resp.usage.totalTokens
440
+ };
441
+ if (resp.finishReason === "tool_use" && resp.toolUses && resp.toolUses.length > 0) {
442
+ log({ ev: "tool-call", taskId: target.id, iter: iters, tools: resp.toolUses.map((t) => t.name) });
443
+ for (const u of resp.toolUses) toolsCalled.add(u.name);
444
+ messages.push({
445
+ role: "assistant",
446
+ content: resp.assistantBlocks ?? []
447
+ });
448
+ const toolResults = await Promise.all(resp.toolUses.map((u) => runTool(u)));
449
+ messages.push({
450
+ role: "user",
451
+ content: toolResults
452
+ });
453
+ continue;
454
+ }
455
+ finalText = resp.content;
456
+ break;
457
+ }
458
+ const durationMs = Date.now() - start;
459
+ const SIDE_EFFECTING_TOOLS = /* @__PURE__ */ new Set(["write_file", "bash"]);
460
+ const sideEffectingCalled = [...toolsCalled].some((t) => SIDE_EFFECTING_TOOLS.has(t));
461
+ if (!sideEffectingCalled) {
462
+ log({
463
+ ev: "no-artifact",
464
+ taskId: target.id,
465
+ tool_iters: iters,
466
+ toolsCalled: [...toolsCalled],
467
+ message: "task execution called no side-effecting tool (write_file/bash) \u2014 refusing to mark executed. Likely a pure-text or read-only-inspection response. Task remains open for a grounded attempt."
468
+ });
469
+ return {
470
+ action: "no-artifact",
471
+ taskId: target.id,
472
+ spentUsd: costGuard.getState().spentUsd,
473
+ remainingUsd: costGuard.getRemainingUsd(),
474
+ message: `no side-effecting tool called (toolsCalled=[${[...toolsCalled].join(",")}], iters=${iters})`
475
+ };
476
+ }
477
+ const cost = costGuard.recordUsage(identity.llmModel, aggUsage);
478
+ log({
479
+ ev: "executed",
480
+ taskId: target.id,
481
+ costUsd: cost.costUsd.toFixed(4),
482
+ spentUsd: cost.spentUsd.toFixed(4),
483
+ tokens: aggUsage.totalTokens,
484
+ tool_iters: iters
485
+ });
486
+ const response = { ...lastResponse ?? { content: finalText, usage: aggUsage }, content: finalText, usage: aggUsage };
487
+ const execResult = {
488
+ taskId: target.id,
489
+ responseText: response.content,
490
+ usage: response.usage,
491
+ costUsd: cost.costUsd,
492
+ durationMs
493
+ };
494
+ if (this.opts.auditLog) {
495
+ try {
496
+ this.opts.auditLog.recordTaskExecuted({
497
+ identity,
498
+ task: target,
499
+ result: execResult
500
+ });
501
+ } catch (err) {
502
+ log({ ev: "audit-log-error", message: err instanceof Error ? err.message : String(err) });
503
+ }
504
+ }
505
+ try {
506
+ const caelRecord = buildCaelRecord({
507
+ identity,
508
+ brain,
509
+ task: target,
510
+ messages,
511
+ finalText,
512
+ usage: aggUsage,
513
+ costUsd: cost.costUsd,
514
+ spentUsd: cost.spentUsd,
515
+ prevChain: this.prevCaelChain,
516
+ runtimeVersion: RUNTIME_VERSION
517
+ });
518
+ const posted = await mesh.postAuditRecords(identity.handle, [caelRecord]);
519
+ this.prevCaelChain = caelRecord.fnv1a_chain;
520
+ log({ ev: "cael-posted", taskId: target.id, appended: posted.appended, rejected: posted.rejected });
521
+ } catch (err) {
522
+ log({ ev: "cael-post-error", message: err instanceof Error ? err.message : String(err) });
523
+ }
524
+ if (this.opts.onTaskExecuted) {
525
+ await this.opts.onTaskExecuted(execResult, target);
526
+ } else {
527
+ await mesh.sendMessageOnTask(
528
+ target.id,
529
+ `[${identity.handle}] response (${response.usage.totalTokens} tok, $${cost.costUsd.toFixed(4)}):
530
+
531
+ ${response.content}`
532
+ );
533
+ }
534
+ return {
535
+ action: "executed",
536
+ taskId: target.id,
537
+ spentUsd: cost.spentUsd,
538
+ remainingUsd: cost.remainingUsd
539
+ };
540
+ }
541
+ async runForever(opts = {}) {
542
+ const interval = opts.tickIntervalMs ?? 6e4;
543
+ while (!this.stopped) {
544
+ try {
545
+ await this.tick();
546
+ } catch (err) {
547
+ const log = this.opts.logger ?? (() => void 0);
548
+ log({ ev: "tick-error", message: err instanceof Error ? err.message : String(err) });
549
+ }
550
+ await sleep(interval + jitter(interval));
551
+ }
552
+ }
553
+ stop() {
554
+ this.stopped = true;
555
+ }
556
+ /**
557
+ * Heartbeat with one-shot self-rejoin on 403 "Not a member of this team".
558
+ *
559
+ * Pairs with task_1777112258989_eeyp: fresh-deploy fleet workers whose
560
+ * provisioning didn't atomically call /join (or whose membership was
561
+ * reaped) hit 403 every tick and never recover. We detect the specific
562
+ * server error string (see packages/mcp-server/src/holomesh/routes/
563
+ * team-routes.ts:903 → `{ error: 'Not a member' }` for /presence), call
564
+ * mesh.joinTeam() ONCE per runner process, and retry the heartbeat.
565
+ *
566
+ * Strict scope:
567
+ * - Only retries on 403 + "Not a member" body. Any other 403 (insufficient
568
+ * permissions, signing failure) re-throws unchanged.
569
+ * - Only retries ONCE per process. If we already rejoined this process and
570
+ * the heartbeat is *still* 403, the team is rejecting us for a reason
571
+ * /join can't fix (e.g. capacity, ban) — surface the error.
572
+ * - If joinTeam() itself throws, we DO mark joinedThisProcess=true before
573
+ * re-throwing so we don't slam the join endpoint on every subsequent
574
+ * tick. The next tick will surface the same heartbeat 403 and the
575
+ * runner-level catch in runForever logs tick-error and sleeps. Operator
576
+ * inspection (SSH/log) is the recovery path at that point.
577
+ */
578
+ async heartbeatWithAutoRejoin() {
579
+ const { identity, mesh, logger } = this.opts;
580
+ const log = logger ?? (() => void 0);
581
+ try {
582
+ await mesh.heartbeat({ agentName: identity.handle, surface: identity.surface });
583
+ } catch (err) {
584
+ if (!this.isNotAMemberError(err) || this.joinedThisProcess) {
585
+ throw err;
586
+ }
587
+ log({ ev: "auto-rejoin-attempt", reason: "heartbeat-403-not-a-member" });
588
+ this.joinedThisProcess = true;
589
+ try {
590
+ const joinResult = await mesh.joinTeam();
591
+ log({ ev: "auto-rejoin-success", role: joinResult.role, members: joinResult.members });
592
+ } catch (joinErr) {
593
+ log({
594
+ ev: "auto-rejoin-failed",
595
+ message: joinErr instanceof Error ? joinErr.message : String(joinErr)
596
+ });
597
+ throw joinErr;
598
+ }
599
+ await mesh.heartbeat({ agentName: identity.handle, surface: identity.surface });
600
+ log({ ev: "auto-rejoin-heartbeat-recovered" });
601
+ }
602
+ }
603
+ /**
604
+ * Detect the server's "Not a member" 403 error from HolomeshClient.req().
605
+ * The error message format is: `HoloMesh POST /team/<id>/presence 403: <body>`
606
+ * where body contains `{"error":"Not a member"}` (or "Not a member of this team").
607
+ * Match conservatively: BOTH a "403" status marker AND the "Not a member"
608
+ * substring must appear, so unrelated 403s (insufficient permissions,
609
+ * signing failures) do NOT trigger a rejoin.
610
+ */
611
+ isNotAMemberError(err) {
612
+ const msg = err instanceof Error ? err.message : String(err);
613
+ return / 403:/.test(msg) && /Not a member/i.test(msg);
614
+ }
615
+ };
616
+ function buildTaskPrompt(task) {
617
+ return [
618
+ `Board task to execute: ${task.id}`,
619
+ `Title: ${task.title}`,
620
+ `Priority: ${task.priority}`,
621
+ `Tags: ${(task.tags ?? []).join(", ")}`,
622
+ "",
623
+ "Description:",
624
+ task.description ?? "(no description)",
625
+ "",
626
+ "Produce the deliverable described in the task. Apply your brain composition rules \u2014 anti-patterns, decision loop, and scope tier all bind. Return the response as plain text suitable for posting to /room as a message on this task."
627
+ ].join("\n");
628
+ }
629
+ function sleep(ms) {
630
+ return new Promise((r) => setTimeout(r, ms));
631
+ }
632
+ function jitter(base) {
633
+ return Math.floor((Math.random() - 0.5) * base * 0.2);
634
+ }
635
+
636
+ // src/cost-guard.ts
637
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
638
+ import { dirname as dirname2 } from "path";
639
+ var ANTHROPIC_PRICING_USD_PER_MTOK = {
640
+ "claude-opus-4-7": { input: 15, output: 75 },
641
+ "claude-opus-4-6": { input: 15, output: 75 },
642
+ "claude-sonnet-4-6": { input: 3, output: 15 },
643
+ "claude-haiku-4-5-20251001": { input: 1, output: 5 },
644
+ "claude-haiku-4-5": { input: 1, output: 5 }
645
+ };
646
+ function defaultAnthropicPricer(model, usage) {
647
+ const price = ANTHROPIC_PRICING_USD_PER_MTOK[model];
648
+ if (!price) {
649
+ throw new Error(
650
+ `No pricing configured for model "${model}" \u2014 add to ANTHROPIC_PRICING_USD_PER_MTOK or pass a custom pricer`
651
+ );
652
+ }
653
+ return (usage.promptTokens * price.input + usage.completionTokens * price.output) / 1e6;
654
+ }
655
+ var CostGuard = class {
656
+ constructor(opts) {
657
+ this.statePath = opts.statePath;
658
+ this.dailyBudgetUsd = opts.dailyBudgetUsd;
659
+ this.pricer = opts.pricer ?? defaultAnthropicPricer;
660
+ this.state = this.loadOrInit();
661
+ }
662
+ recordUsage(model, usage) {
663
+ this.rolloverIfNewDay();
664
+ const costUsd = this.pricer(model, usage);
665
+ this.state.spentUsd += costUsd;
666
+ this.state.promptTokens += usage.promptTokens;
667
+ this.state.completionTokens += usage.completionTokens;
668
+ this.state.callCount += 1;
669
+ this.persist();
670
+ return {
671
+ costUsd,
672
+ spentUsd: this.state.spentUsd,
673
+ remainingUsd: Math.max(0, this.dailyBudgetUsd - this.state.spentUsd)
674
+ };
675
+ }
676
+ isOverBudget() {
677
+ if (this.dailyBudgetUsd === 0) return false;
678
+ this.rolloverIfNewDay();
679
+ return this.state.spentUsd >= this.dailyBudgetUsd;
680
+ }
681
+ getRemainingUsd() {
682
+ if (this.dailyBudgetUsd === 0) return Number.POSITIVE_INFINITY;
683
+ this.rolloverIfNewDay();
684
+ return Math.max(0, this.dailyBudgetUsd - this.state.spentUsd);
685
+ }
686
+ getState() {
687
+ this.rolloverIfNewDay();
688
+ return { ...this.state };
689
+ }
690
+ rolloverIfNewDay() {
691
+ const today = todayUtc();
692
+ if (this.state.date !== today) {
693
+ this.state = { date: today, spentUsd: 0, promptTokens: 0, completionTokens: 0, callCount: 0 };
694
+ this.persist();
695
+ }
696
+ }
697
+ loadOrInit() {
698
+ if (existsSync(this.statePath)) {
699
+ const raw = readFileSync(this.statePath, "utf8");
700
+ const parsed = JSON.parse(raw);
701
+ if (parsed.date === todayUtc()) return parsed;
702
+ }
703
+ return { date: todayUtc(), spentUsd: 0, promptTokens: 0, completionTokens: 0, callCount: 0 };
704
+ }
705
+ persist() {
706
+ mkdirSync(dirname2(this.statePath), { recursive: true });
707
+ writeFileSync(this.statePath, JSON.stringify(this.state, null, 2), "utf8");
708
+ }
709
+ };
710
+ function todayUtc() {
711
+ return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
712
+ }
713
+
714
+ // src/brain.ts
715
+ import { readFile as readFile2 } from "fs/promises";
716
+ async function loadBrain(brainPath, scopeTier = "warm") {
717
+ const systemPrompt = await readFile2(brainPath, "utf8");
718
+ const { domain, capabilityTags } = extractIdentity(systemPrompt);
719
+ return { brainPath, systemPrompt, capabilityTags, domain, scopeTier };
720
+ }
721
+ function extractIdentity(brain) {
722
+ const identityBlock = sliceNamedBlock(brain, "identity");
723
+ if (!identityBlock) return { domain: "unknown", capabilityTags: [] };
724
+ const domain = scalarField(identityBlock, "domain") ?? "unknown";
725
+ const capabilityTags = listField(identityBlock, "capability_tags") ?? [];
726
+ return { domain, capabilityTags };
727
+ }
728
+ function sliceNamedBlock(src, name) {
729
+ const re = new RegExp(`\\b${name}\\s*:?\\s*\\{`, "g");
730
+ const match = re.exec(src);
731
+ if (!match) return void 0;
732
+ const headerEnd = match.index + match[0].length;
733
+ let depth = 1;
734
+ for (let i = headerEnd; i < src.length; i++) {
735
+ const ch = src[i];
736
+ if (ch === "{") depth++;
737
+ else if (ch === "}") {
738
+ depth--;
739
+ if (depth === 0) return src.slice(headerEnd, i);
740
+ }
741
+ }
742
+ return void 0;
743
+ }
744
+ function scalarField(block, key) {
745
+ const idx = block.indexOf(`${key}:`);
746
+ if (idx < 0) return void 0;
747
+ const after = block.slice(idx + key.length + 1).trimStart();
748
+ if (after.startsWith('"')) {
749
+ const end = after.indexOf('"', 1);
750
+ if (end > 0) return after.slice(1, end);
751
+ }
752
+ const eol = after.indexOf("\n");
753
+ return after.slice(0, eol < 0 ? void 0 : eol).trim();
754
+ }
755
+ function listField(block, key) {
756
+ const idx = block.indexOf(`${key}:`);
757
+ if (idx < 0) return void 0;
758
+ const after = block.slice(idx + key.length + 1).trimStart();
759
+ if (!after.startsWith("[")) return void 0;
760
+ let depth = 0;
761
+ let end = -1;
762
+ for (let i = 0; i < after.length; i++) {
763
+ if (after[i] === "[") depth++;
764
+ else if (after[i] === "]") {
765
+ depth--;
766
+ if (depth === 0) {
767
+ end = i;
768
+ break;
769
+ }
770
+ }
771
+ }
772
+ if (end < 0) return void 0;
773
+ const inner = after.slice(1, end);
774
+ return inner.split(",").map((s) => s.trim().replace(/^["']|["']$/g, "")).filter((s) => s.length > 0);
775
+ }
776
+
777
+ // src/commit-hook.ts
778
+ import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
779
+ import { dirname as dirname3, join, resolve as resolve2 } from "path";
780
+ import { spawnSync } from "child_process";
781
+ var SAFE_HANDLE = /^[a-z0-9_-]{1,64}$/i;
782
+ function makeCommitHook(opts) {
783
+ if (!opts.outputDir || opts.outputDir.trim().length === 0) {
784
+ throw new Error("CommitHookOptions.outputDir is required");
785
+ }
786
+ const spawn2 = opts.spawn ?? spawnSync;
787
+ const cwd = opts.workingDir ?? process.cwd();
788
+ const outputDir = resolve2(cwd, opts.outputDir);
789
+ const now = opts.now ?? (() => /* @__PURE__ */ new Date());
790
+ const scope = opts.scope ?? "agent";
791
+ return async (result, task, identity) => {
792
+ if (!SAFE_HANDLE.test(identity.handle)) {
793
+ throw new Error(`Refusing to commit: handle "${identity.handle}" must match ${SAFE_HANDLE}`);
794
+ }
795
+ const date = now().toISOString().slice(0, 10);
796
+ const safeTaskId = task.id.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 80);
797
+ const fileName = `${date}_${safeTaskId}_${identity.handle}.md`;
798
+ const filePath = join(outputDir, fileName);
799
+ mkdirSync2(dirname3(filePath), { recursive: true });
800
+ writeFileSync2(filePath, renderMemo(result, task, identity, date), "utf8");
801
+ const relPath = relativeTo(cwd, filePath);
802
+ const addRes = spawn2("git", ["add", relPath], { cwd, encoding: "utf8" });
803
+ if (addRes.status !== 0) {
804
+ throw new Error(`git add failed: ${addRes.stderr || addRes.stdout || `exit ${addRes.status}`}`);
805
+ }
806
+ const message = renderCommitMessage({ scope, task, identity, result });
807
+ const commitArgs = ["commit", "-m", message];
808
+ if (opts.authorName && opts.authorEmail) {
809
+ commitArgs.push("--author", `${opts.authorName} <${opts.authorEmail}>`);
810
+ }
811
+ const commitRes = spawn2("git", commitArgs, { cwd, encoding: "utf8" });
812
+ if (commitRes.status !== 0) {
813
+ throw new Error(`git commit failed: ${commitRes.stderr || commitRes.stdout || `exit ${commitRes.status}`}`);
814
+ }
815
+ const hashRes = spawn2("git", ["rev-parse", "HEAD"], { cwd, encoding: "utf8" });
816
+ const commitHash = hashRes.status === 0 ? hashRes.stdout.trim() : void 0;
817
+ return { filePath, commitHash, staged: [relPath], message };
818
+ };
819
+ }
820
+ function renderMemo(result, task, identity, date) {
821
+ return [
822
+ "---",
823
+ `title: "${task.title.replace(/"/g, "'")}"`,
824
+ `task_id: ${task.id}`,
825
+ `agent: ${identity.handle}`,
826
+ `surface: ${identity.surface}`,
827
+ `provider: ${identity.llmProvider}`,
828
+ `model: ${identity.llmModel}`,
829
+ `wallet: ${identity.wallet}`,
830
+ `date: ${date}`,
831
+ `tokens: ${result.usage.totalTokens}`,
832
+ `cost_usd: ${result.costUsd.toFixed(4)}`,
833
+ `duration_ms: ${result.durationMs}`,
834
+ `tags: [${(task.tags ?? []).map((t) => JSON.stringify(t)).join(", ")}]`,
835
+ "---",
836
+ "",
837
+ `# ${task.title}`,
838
+ "",
839
+ "## Task description",
840
+ "",
841
+ task.description ?? "(no description)",
842
+ "",
843
+ "## Agent response",
844
+ "",
845
+ result.responseText.trim(),
846
+ ""
847
+ ].join("\n");
848
+ }
849
+ var SUBJECT_MAX = 72;
850
+ function renderCommitMessage(opts) {
851
+ const suffix = ` [agent:${opts.identity.handle}]`;
852
+ const prefix = `${opts.scope}: `;
853
+ const titleBudget = Math.max(8, SUBJECT_MAX - prefix.length - suffix.length);
854
+ const subject = `${prefix}${truncate(opts.task.title, titleBudget)}${suffix}`;
855
+ const body = [
856
+ "",
857
+ `task: ${opts.task.id}`,
858
+ `agent: ${opts.identity.handle} (${opts.identity.llmProvider}/${opts.identity.llmModel})`,
859
+ `wallet: ${opts.identity.wallet}`,
860
+ `cost: $${opts.result.costUsd.toFixed(4)} / ${opts.result.usage.totalTokens} tok / ${opts.result.durationMs}ms`
861
+ ].join("\n");
862
+ return `${subject}
863
+ ${body}
864
+ `;
865
+ }
866
+ function truncate(s, max) {
867
+ return s.length <= max ? s : `${s.slice(0, max - 1)}\u2026`;
868
+ }
869
+ function relativeTo(base, target) {
870
+ const b = base.replace(/\\/g, "/");
871
+ const t = target.replace(/\\/g, "/");
872
+ if (t.startsWith(b + "/")) return t.slice(b.length + 1);
873
+ if (t === b) return ".";
874
+ return t;
875
+ }
876
+
877
+ // src/audit-log.ts
878
+ import { mkdirSync as mkdirSync3, appendFileSync, readFileSync as readFileSync2, existsSync as existsSync2, statSync, renameSync } from "fs";
879
+ import { dirname as dirname4 } from "path";
880
+ var AuditLog = class {
881
+ constructor(opts) {
882
+ this.logPath = opts.logPath;
883
+ this.maxBytes = opts.maxBytes ?? 50 * 1024 * 1024;
884
+ mkdirSync3(dirname4(this.logPath), { recursive: true });
885
+ }
886
+ record(event) {
887
+ this.rotateIfFull();
888
+ appendFileSync(this.logPath, `${JSON.stringify(event)}
889
+ `, "utf8");
890
+ }
891
+ recordTaskExecuted(opts) {
892
+ this.record({
893
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
894
+ kind: "task-executed",
895
+ agent: agentFromIdentity(opts.identity),
896
+ task: { id: opts.task.id, title: opts.task.title, tags: opts.task.tags ?? [] },
897
+ execution: {
898
+ promptTokens: opts.result.usage.promptTokens,
899
+ completionTokens: opts.result.usage.completionTokens,
900
+ totalTokens: opts.result.usage.totalTokens,
901
+ costUsd: opts.result.costUsd,
902
+ durationMs: opts.result.durationMs
903
+ },
904
+ result: {
905
+ commitHash: opts.commitHash,
906
+ filePath: opts.filePath
907
+ }
908
+ });
909
+ }
910
+ recordAblationCell(opts) {
911
+ this.record({
912
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
913
+ kind: "ablation-cell",
914
+ agent: agentFromIdentity(opts.identity),
915
+ task: { id: opts.taskId, title: opts.taskTitle, tags: ["ablation"] },
916
+ execution: {
917
+ promptTokens: opts.promptTokens,
918
+ completionTokens: opts.completionTokens,
919
+ totalTokens: opts.promptTokens + opts.completionTokens,
920
+ costUsd: opts.costUsd,
921
+ durationMs: opts.durationMs,
922
+ finishReason: opts.finishReason,
923
+ promptHash: opts.promptHash
924
+ },
925
+ result: { errorMessage: opts.errorMessage },
926
+ ablation: { label: opts.label, matrixId: opts.matrixId }
927
+ });
928
+ }
929
+ query(filter = {}) {
930
+ if (!existsSync2(this.logPath)) return [];
931
+ const raw = readFileSync2(this.logPath, "utf8");
932
+ const lines = raw.split("\n").filter((l) => l.length > 0);
933
+ const events = [];
934
+ for (const line of lines) {
935
+ try {
936
+ events.push(JSON.parse(line));
937
+ } catch {
938
+ }
939
+ }
940
+ return applyFilter(events, filter);
941
+ }
942
+ rollup(filter = {}) {
943
+ const events = this.query(filter);
944
+ const byAgent = {};
945
+ const byProvider = {};
946
+ let totalCostUsd = 0;
947
+ let totalTokens = 0;
948
+ for (const e of events) {
949
+ const agent = e.agent.handle;
950
+ const provider = e.agent.provider;
951
+ const cost = e.execution?.costUsd ?? 0;
952
+ const tokens = e.execution?.totalTokens ?? 0;
953
+ byAgent[agent] = byAgent[agent] ?? { events: 0, costUsd: 0, tokens: 0 };
954
+ byAgent[agent].events += 1;
955
+ byAgent[agent].costUsd += cost;
956
+ byAgent[agent].tokens += tokens;
957
+ byProvider[provider] = byProvider[provider] ?? { events: 0, costUsd: 0, tokens: 0 };
958
+ byProvider[provider].events += 1;
959
+ byProvider[provider].costUsd += cost;
960
+ byProvider[provider].tokens += tokens;
961
+ totalCostUsd += cost;
962
+ totalTokens += tokens;
963
+ }
964
+ return { totalEvents: events.length, byAgent, byProvider, totalCostUsd, totalTokens };
965
+ }
966
+ rotateIfFull() {
967
+ if (!existsSync2(this.logPath)) return;
968
+ const size = statSync(this.logPath).size;
969
+ if (size < this.maxBytes) return;
970
+ const archived = `${this.logPath}.${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.jsonl`;
971
+ renameSync(this.logPath, archived);
972
+ }
973
+ };
974
+ function agentFromIdentity(identity) {
975
+ return {
976
+ handle: identity.handle,
977
+ surface: identity.surface,
978
+ wallet: identity.wallet,
979
+ walletShort: `${identity.wallet.slice(0, 6)}\u2026${identity.wallet.slice(-4)}`,
980
+ provider: identity.llmProvider,
981
+ model: identity.llmModel,
982
+ brainPath: identity.brainPath
983
+ };
984
+ }
985
+ function applyFilter(events, filter) {
986
+ let result = events;
987
+ if (filter.agent) result = result.filter((e) => e.agent.handle === filter.agent);
988
+ if (filter.provider) result = result.filter((e) => e.agent.provider === filter.provider);
989
+ if (filter.task) result = result.filter((e) => e.task?.id === filter.task);
990
+ if (filter.kind) result = result.filter((e) => e.kind === filter.kind);
991
+ if (filter.since) result = result.filter((e) => e.ts >= filter.since);
992
+ if (filter.until) result = result.filter((e) => e.ts <= filter.until);
993
+ if (filter.limit != null) result = result.slice(-filter.limit);
994
+ return result;
995
+ }
996
+
997
+ // src/supervisor.ts
998
+ var Supervisor = class {
999
+ constructor(opts) {
1000
+ this.agents = /* @__PURE__ */ new Map();
1001
+ this.stopped = false;
1002
+ this.opts = opts;
1003
+ this.globalBudgetUsdPerDay = opts.config.globalBudgetUsdPerDay;
1004
+ this.auditLog = opts.auditLogPath ? new AuditLog({ logPath: opts.auditLogPath }) : void 0;
1005
+ }
1006
+ async start() {
1007
+ const enabledAgents = this.opts.config.agents.filter((a) => a.enabled !== false);
1008
+ for (const spec of enabledAgents) {
1009
+ const managed = await this.bootAgent(spec);
1010
+ this.agents.set(spec.handle, managed);
1011
+ }
1012
+ this.log({ ev: "supervisor-started", count: enabledAgents.length });
1013
+ for (const managed of this.agents.values()) {
1014
+ this.spawnLoop(managed);
1015
+ }
1016
+ }
1017
+ async stop() {
1018
+ this.stopped = true;
1019
+ for (const managed of this.agents.values()) {
1020
+ managed.runner.stop();
1021
+ managed.status.state = "stopped";
1022
+ }
1023
+ this.log({ ev: "supervisor-stopped", count: this.agents.size });
1024
+ }
1025
+ status() {
1026
+ const agents = [...this.agents.values()].map((m) => ({ ...m.status }));
1027
+ const globalSpentUsd = agents.reduce((s, a) => s + a.spentUsd, 0);
1028
+ const globalBudgetExhausted = this.globalBudgetUsdPerDay != null ? globalSpentUsd >= this.globalBudgetUsdPerDay : false;
1029
+ const globalRemainingUsd = this.globalBudgetUsdPerDay != null ? Math.max(0, this.globalBudgetUsdPerDay - globalSpentUsd) : Infinity;
1030
+ return { globalSpentUsd, globalRemainingUsd, globalBudgetExhausted, agents };
1031
+ }
1032
+ async tickOnce(handle) {
1033
+ const managed = this.agents.get(handle);
1034
+ if (!managed) throw new Error(`No agent: ${handle}`);
1035
+ await this.runOneTick(managed);
1036
+ return { ...managed.status };
1037
+ }
1038
+ async bootAgent(spec) {
1039
+ const identity = this.identityFromSpec(spec);
1040
+ const brain = await loadBrain(spec.brainPath, spec.scopeTier ?? "warm");
1041
+ const provider = await this.opts.providerFactory(spec, identity);
1042
+ const stateDir = this.opts.stateDir ?? join2(homedir(), ".holoscript-agent", "cost-state");
1043
+ const isFree = spec.provider === "mock" || spec.provider === "local-llm" || spec.provider === "bitnet";
1044
+ const costGuard = new CostGuard({
1045
+ statePath: join2(stateDir, `${spec.handle}.json`),
1046
+ dailyBudgetUsd: identity.budgetUsdPerDay,
1047
+ pricer: isFree ? () => 0 : void 0
1048
+ });
1049
+ const mesh = new HolomeshClient({
1050
+ apiBase: identity.meshApiBase,
1051
+ bearer: identity.x402Bearer,
1052
+ teamId: identity.teamId,
1053
+ fetchImpl: this.opts.fetchImpl
1054
+ });
1055
+ const onTaskExecuted = spec.enableCommitHook ? this.buildCommitHook(spec, identity, mesh) : void 0;
1056
+ const runner = new AgentRunner({
1057
+ identity,
1058
+ brain,
1059
+ provider,
1060
+ costGuard,
1061
+ mesh,
1062
+ onTaskExecuted,
1063
+ auditLog: this.auditLog,
1064
+ logger: (ev) => this.log({ agent: spec.handle, ...ev })
1065
+ });
1066
+ const status = {
1067
+ handle: spec.handle,
1068
+ state: "starting",
1069
+ spentUsd: 0,
1070
+ remainingUsd: identity.budgetUsdPerDay,
1071
+ restarts: 0
1072
+ };
1073
+ return { spec, identity, brain, runner, costGuard, status };
1074
+ }
1075
+ buildCommitHook(spec, identity, mesh) {
1076
+ const writer = makeCommitHook({
1077
+ outputDir: spec.outputDir ?? "agent-out",
1078
+ workingDir: spec.workingDir,
1079
+ scope: `agent(${spec.handle})`
1080
+ });
1081
+ return async (result, task) => {
1082
+ const out = await writer(result, task, identity);
1083
+ if (out.commitHash) {
1084
+ await mesh.markDone(task.id, `auto: ${task.title}`, out.commitHash);
1085
+ }
1086
+ };
1087
+ }
1088
+ identityFromSpec(spec) {
1089
+ const bearer = process.env[spec.bearerEnvKey];
1090
+ if (!bearer || bearer.trim().length === 0) {
1091
+ throw new Error(`Missing bearer env var "${spec.bearerEnvKey}" for agent "${spec.handle}"`);
1092
+ }
1093
+ const wallet = process.env[spec.walletEnvKey];
1094
+ if (!wallet || !/^0x[0-9a-fA-F]{40}$/.test(wallet)) {
1095
+ throw new Error(`Missing or malformed wallet env var "${spec.walletEnvKey}" for agent "${spec.handle}"`);
1096
+ }
1097
+ return {
1098
+ handle: spec.handle,
1099
+ surface: spec.handle,
1100
+ wallet,
1101
+ x402Bearer: bearer,
1102
+ llmProvider: spec.provider,
1103
+ llmModel: spec.model,
1104
+ brainPath: spec.brainPath,
1105
+ budgetUsdPerDay: spec.budgetUsdPerDay ?? 5,
1106
+ teamId: this.opts.teamId,
1107
+ meshApiBase: this.opts.meshApiBase ?? "https://mcp.holoscript.net/api/holomesh"
1108
+ };
1109
+ }
1110
+ async spawnLoop(managed) {
1111
+ const interval = managed.spec.tickIntervalMs ?? this.opts.config.defaultTickIntervalMs ?? 6e4;
1112
+ while (!this.stopped) {
1113
+ try {
1114
+ await this.runOneTick(managed);
1115
+ } catch (err) {
1116
+ managed.status.state = "crashed";
1117
+ managed.status.lastError = err instanceof Error ? err.message : String(err);
1118
+ managed.status.restarts += 1;
1119
+ const backoffMs = Math.min(6e4, 2 ** Math.min(managed.status.restarts, 6) * 1e3);
1120
+ this.log({
1121
+ ev: "agent-crashed-restarting",
1122
+ agent: managed.spec.handle,
1123
+ backoffMs,
1124
+ restarts: managed.status.restarts,
1125
+ message: managed.status.lastError
1126
+ });
1127
+ await sleep2(backoffMs);
1128
+ continue;
1129
+ }
1130
+ await sleep2(interval + jitter2(interval));
1131
+ }
1132
+ }
1133
+ async runOneTick(managed) {
1134
+ if (this.globalBudgetUsdPerDay != null) {
1135
+ const totalSpent = [...this.agents.values()].reduce((s, a) => s + a.status.spentUsd, 0);
1136
+ if (totalSpent >= this.globalBudgetUsdPerDay) {
1137
+ managed.status.state = "over-budget";
1138
+ this.log({ ev: "global-budget-exhausted", agent: managed.spec.handle, totalSpent });
1139
+ return;
1140
+ }
1141
+ }
1142
+ const result = await managed.runner.tick();
1143
+ const cs = managed.costGuard.getState();
1144
+ managed.status.spentUsd = cs.spentUsd;
1145
+ managed.status.remainingUsd = managed.costGuard.getRemainingUsd();
1146
+ managed.status.lastTickAt = (this.opts.now ?? (() => /* @__PURE__ */ new Date()))().toISOString();
1147
+ managed.status.state = result.action === "over-budget" ? "over-budget" : "running";
1148
+ if (result.action === "errored") {
1149
+ managed.status.lastError = result.message;
1150
+ }
1151
+ }
1152
+ log(event) {
1153
+ if (this.opts.logger) {
1154
+ this.opts.logger({ ts: (/* @__PURE__ */ new Date()).toISOString(), ...event });
1155
+ }
1156
+ }
1157
+ };
1158
+ function sleep2(ms) {
1159
+ return new Promise((r) => setTimeout(r, ms));
1160
+ }
1161
+ function jitter2(base) {
1162
+ return Math.floor((Math.random() - 0.5) * base * 0.2);
1163
+ }
1164
+ export {
1165
+ Supervisor
1166
+ };
1167
+ //# sourceMappingURL=supervisor.js.map