@holoscript/holoscript-agent 2.0.0 → 2.0.2

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.
@@ -9,9 +9,14 @@ var HolomeshClient = class {
9
9
  this.bearer = opts.bearer;
10
10
  this.teamId = opts.teamId;
11
11
  this.fetchImpl = opts.fetchImpl ?? fetch;
12
+ this.signer = opts.signer;
13
+ }
14
+ /** Wrap body in a signed envelope when a signer is available (strict-mode endpoints). */
15
+ async signBody(body) {
16
+ return this.signer ? await this.signer(body) : body;
12
17
  }
13
18
  async heartbeat(payload) {
14
- await this.req("POST", `/team/${this.teamId}/presence`, payload);
19
+ await this.req("POST", `/team/${this.teamId}/presence`, await this.signBody(payload));
15
20
  }
16
21
  async getOpenTasks() {
17
22
  const data = await this.req(
@@ -21,28 +26,33 @@ var HolomeshClient = class {
21
26
  return data.tasks ?? data.open ?? [];
22
27
  }
23
28
  async claim(taskId) {
24
- return this.req("PATCH", `/team/${this.teamId}/board/${taskId}`, { action: "claim" });
29
+ return this.req("PATCH", `/team/${this.teamId}/board/${taskId}`, await this.signBody({ action: "claim" }));
25
30
  }
26
31
  async joinTeam() {
27
32
  return this.req(
28
33
  "POST",
29
34
  `/team/${this.teamId}/join`,
30
- {}
35
+ await this.signBody({})
31
36
  );
32
37
  }
33
38
  async sendMessageOnTask(taskId, body) {
34
- await this.req("POST", `/team/${this.teamId}/message`, {
39
+ await this.req("POST", `/team/${this.teamId}/message`, await this.signBody({
35
40
  to: "team",
36
41
  subject: `task:${taskId}`,
37
42
  content: body
38
- });
43
+ }));
39
44
  }
40
45
  async markDone(taskId, summary, commitHash) {
41
- await this.req("PATCH", `/team/${this.teamId}/board/${taskId}`, {
46
+ await this.req("PATCH", `/team/${this.teamId}/board/${taskId}`, await this.signBody({
42
47
  action: "done",
43
48
  summary,
44
- commitHash
45
- });
49
+ // verification_evidence required by server before task can be closed.
50
+ verification_evidence: summary,
51
+ // Exclude commitHash when undefined — JSON.stringify drops undefined but
52
+ // canonicalizeSigning preserves it as the literal string "undefined",
53
+ // causing a signature-mismatch vs what the server sees after JSON.parse.
54
+ ...commitHash !== void 0 ? { commitHash } : {}
55
+ }));
46
56
  }
47
57
  // POST CAEL audit records for this agent. Server validator at
48
58
  // packages/mcp-server/src/holomesh/routes/core-routes.ts:472-533 requires
@@ -65,6 +75,40 @@ var HolomeshClient = class {
65
75
  wallet: raw.wallet
66
76
  };
67
77
  }
78
+ // ── Team Message Surface (E4 delegated-authority protocol) ───────────────────
79
+ /** Read recent team messages. */
80
+ async getTeamMessages(limit = 20) {
81
+ const data = await this.req(
82
+ "GET",
83
+ `/team/${this.teamId}/messages?limit=${limit}`
84
+ );
85
+ return data.messages ?? [];
86
+ }
87
+ /** Post a message to the team feed. */
88
+ async sendTeamMessage(content, messageType = "text") {
89
+ await this.req("POST", `/team/${this.teamId}/message`, await this.signBody({ content, type: messageType }));
90
+ }
91
+ // ── Owner-op API wrappers (E4) ─────────────────────────────────────────────
92
+ /** Switch team mode. Requires owner or founder role. */
93
+ async setTeamMode(mode, reason) {
94
+ return this.req("POST", `/team/${this.teamId}/mode`, await this.signBody({ mode, reason }));
95
+ }
96
+ /** Update room preferences. Requires config:write permission. */
97
+ async patchRoomPrefs(prefs) {
98
+ return this.req("PATCH", `/team/${this.teamId}/room`, await this.signBody(prefs));
99
+ }
100
+ /** Update a board task. */
101
+ async updateTask(taskId, updates) {
102
+ return this.req("PATCH", `/team/${this.teamId}/board/${taskId}`, await this.signBody({ action: "update", ...updates }));
103
+ }
104
+ /** Delete a board task. */
105
+ async deleteTask(taskId) {
106
+ return this.req("PATCH", `/team/${this.teamId}/board/${taskId}`, await this.signBody({ action: "delete" }));
107
+ }
108
+ /** Delegate a board task to another agent. */
109
+ async delegateTask(taskId, toAgentId) {
110
+ return this.req("PATCH", `/team/${this.teamId}/board/${taskId}`, await this.signBody({ action: "delegate", toAgentId }));
111
+ }
68
112
  async req(method, path, body) {
69
113
  const url = `${this.apiBase}${path}`;
70
114
  const res = await this.fetchImpl(url, {
@@ -132,7 +176,18 @@ function brainClassOf(brain) {
132
176
  return "unknown";
133
177
  }
134
178
  function buildCaelRecord(input) {
135
- const { identity, brain, task, messages, finalText, usage, costUsd, spentUsd, prevChain, runtimeVersion } = input;
179
+ const {
180
+ identity,
181
+ brain,
182
+ task,
183
+ messages,
184
+ finalText,
185
+ usage,
186
+ costUsd,
187
+ spentUsd,
188
+ prevChain,
189
+ runtimeVersion
190
+ } = input;
136
191
  const l0 = sha(brain.systemPrompt);
137
192
  const l1 = sha(`${task.id}|${task.title}|${task.description ?? ""}`);
138
193
  const l2 = sha(JSON.stringify(messages));
@@ -148,15 +203,16 @@ function buildCaelRecord(input) {
148
203
  prev_hash: prevChain,
149
204
  fnv1a_chain,
150
205
  version_vector_fingerprint: `agent@${runtimeVersion}|brain@${brainClassOf(brain)}|provider@${identity.llmProvider}|model@${identity.llmModel}`,
151
- brain_class: brainClassOf(brain)
206
+ brain_class: brainClassOf(brain),
207
+ trust_epoch: "post-w107"
152
208
  };
153
209
  }
154
210
 
155
211
  // src/tools.ts
156
212
  import { readFile, writeFile, readdir, mkdir, stat } from "fs/promises";
157
- import { resolve, dirname } from "path";
213
+ import { resolve, dirname, delimiter, isAbsolute, sep } from "path";
158
214
  import { spawn } from "child_process";
159
- var ALLOWED_READ_ROOTS = [
215
+ var FLEET_READ_ROOTS = [
160
216
  "/root/msc-paper-22",
161
217
  // Paper 22 mechanization inputs (scp'd by deploy)
162
218
  "/root/holoscript-mesh",
@@ -164,15 +220,24 @@ var ALLOWED_READ_ROOTS = [
164
220
  "/root/agent-output"
165
221
  // Read back what we wrote
166
222
  ];
167
- var ALLOWED_WRITE_ROOTS = [
223
+ var FLEET_WRITE_ROOTS = [
168
224
  "/root/agent-output"
169
225
  // Single write sink — keeps deliverables in one place
170
226
  ];
171
- var BASH_WHITELIST = [
172
- "lake build",
173
- "lake env",
174
- "lake clean",
175
- "lean ",
227
+ function parseRootsEnv(raw, fallback) {
228
+ if (!raw) return fallback;
229
+ const roots = raw.split(delimiter).map((r) => r.trim()).filter((r) => r.length > 0 && isAbsolute(r));
230
+ return roots.length > 0 ? roots : fallback;
231
+ }
232
+ var ALLOWED_READ_ROOTS = parseRootsEnv(
233
+ process.env.HOLOSCRIPT_AGENT_READ_ROOTS,
234
+ FLEET_READ_ROOTS
235
+ );
236
+ var ALLOWED_WRITE_ROOTS = parseRootsEnv(
237
+ process.env.HOLOSCRIPT_AGENT_WRITE_ROOTS,
238
+ FLEET_WRITE_ROOTS
239
+ );
240
+ var BASH_READ_ONLY_PREFIXES = [
176
241
  "ls ",
177
242
  "ls\n",
178
243
  "ls$",
@@ -187,16 +252,36 @@ var BASH_WHITELIST = [
187
252
  "git log",
188
253
  "git diff",
189
254
  "git show",
255
+ "pwd",
256
+ "echo ",
257
+ "lake env"
258
+ ];
259
+ var BASH_PRODUCTIVE_PREFIXES = [
260
+ "lake build",
261
+ "lake clean",
262
+ "lean ",
190
263
  "pnpm --filter",
191
264
  "pnpm vitest",
192
265
  "vitest run",
193
- "pwd",
194
- "echo "
266
+ // Robotics / edge-node (Jetson) productive commands — without these, every
267
+ // ros2/colcon/tegrastats task fails the W.107 artifact gate and is abandoned
268
+ // as no-artifact. (jetson-orin-01 lane.)
269
+ "ros2 launch",
270
+ "ros2 topic pub",
271
+ "ros2 service call",
272
+ "colcon build",
273
+ "tegrastats"
195
274
  ];
275
+ var BASH_WHITELIST = [...BASH_READ_ONLY_PREFIXES, ...BASH_PRODUCTIVE_PREFIXES];
276
+ function isProductiveBashCommand(cmd) {
277
+ const trimmed = String(cmd ?? "").trim();
278
+ if (!trimmed) return false;
279
+ return BASH_PRODUCTIVE_PREFIXES.some((prefix) => trimmed.startsWith(prefix.trim()));
280
+ }
196
281
  var MESH_TOOLS = [
197
282
  {
198
283
  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).",
284
+ description: `Read a file from the agent sandbox. Allowed roots: ${ALLOWED_READ_ROOTS.join(", ")}. Returns the file content as text. Use this to inspect task inputs and the read-only repo view.`,
200
285
  input_schema: {
201
286
  type: "object",
202
287
  properties: {
@@ -218,11 +303,11 @@ var MESH_TOOLS = [
218
303
  },
219
304
  {
220
305
  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.",
306
+ description: `Write a file to the deliverable sink (write roots: ${ALLOWED_WRITE_ROOTS.join(", ")}). Anything you want to emit as task output (a Lean proof, a markdown report, a JSON dataset, a .holo scene) goes here. Creates parent directories. Will refuse paths outside the write root(s).`,
222
307
  input_schema: {
223
308
  type: "object",
224
309
  properties: {
225
- path: { type: "string", description: "Absolute path under /root/agent-output/" },
310
+ path: { type: "string", description: `Absolute path under a write root: ${ALLOWED_WRITE_ROOTS.join(", ")}` },
226
311
  content: { type: "string", description: "File content to write (UTF-8)" }
227
312
  },
228
313
  required: ["path", "content"]
@@ -230,7 +315,7 @@ var MESH_TOOLS = [
230
315
  },
231
316
  {
232
317
  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.",
318
+ 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, ros2 launch/topic/service, colcon build, tegrastats. Hard 60s wall timeout, 1MB stdout cap. Use for builds, tests, hardware probes. Refuses rm, curl, ssh, sudo, eval.",
234
319
  input_schema: {
235
320
  type: "object",
236
321
  properties: {
@@ -239,22 +324,52 @@ var MESH_TOOLS = [
239
324
  },
240
325
  required: ["cmd"]
241
326
  }
327
+ },
328
+ {
329
+ name: "emit_hardware_receipt",
330
+ description: "Emit a portable hardware receipt (PortableHardwareReceiptMetadata v1) capturing device identity, runtime, and measured performance. Writes a JSON receipt to the agent output dir. Use after running tegrastats or colcon build to record hardware evidence for the CAEL audit chain. Accepts either pre-parsed measurements or raw tegrastats output (the tool parses it automatically).",
331
+ input_schema: {
332
+ type: "object",
333
+ properties: {
334
+ device_kind: {
335
+ type: "string",
336
+ description: 'Device identifier, e.g. "jetson-orin-nano-super", "raspberry-pi-5"'
337
+ },
338
+ accelerator: {
339
+ description: 'Accelerator string, e.g. "NVIDIA CUDA 8.7", or null for CPU-only'
340
+ },
341
+ runtime_name: { type: "string", description: 'Inference runtime, e.g. "Ollama", "llama.cpp"' },
342
+ runtime_version: { type: "string", description: 'Runtime version, e.g. "0.30.8"' },
343
+ host_os: { type: "string", description: 'OS + firmware, e.g. "JetPack 6.2.1 / Ubuntu 22.04"' },
344
+ composition_id: { type: "string", description: 'Brain composition reference, e.g. "jetson-orin-brain"' },
345
+ measurements: {
346
+ type: "array",
347
+ description: "Pre-parsed measurements. Each item: {metric: string, value: number, unit: string}",
348
+ items: { type: "object" }
349
+ },
350
+ tegrastats_output: {
351
+ type: "string",
352
+ description: "Raw tegrastats output line(s) \u2014 tool auto-parses GPU%, RAM, temp, power"
353
+ }
354
+ },
355
+ required: ["device_kind", "runtime_name", "runtime_version", "host_os"]
356
+ }
242
357
  }
243
358
  ];
244
359
  function isUnderRoot(absPath, root) {
245
360
  const resolved = resolve(absPath);
246
361
  const rootResolved = resolve(root);
247
- return resolved === rootResolved || resolved.startsWith(rootResolved + "/");
362
+ return resolved === rootResolved || resolved.startsWith(rootResolved + sep);
248
363
  }
249
364
  function checkReadAllowed(path) {
250
- if (!path.startsWith("/")) return `path must be absolute, got "${path}"`;
365
+ if (!isAbsolute(path)) return `path must be absolute, got "${path}"`;
251
366
  for (const root of ALLOWED_READ_ROOTS) {
252
367
  if (isUnderRoot(path, root)) return null;
253
368
  }
254
369
  return `read denied \u2014 path "${path}" not under allowed roots: ${ALLOWED_READ_ROOTS.join(", ")}`;
255
370
  }
256
371
  function checkWriteAllowed(path) {
257
- if (!path.startsWith("/")) return `path must be absolute, got "${path}"`;
372
+ if (!isAbsolute(path)) return `path must be absolute, got "${path}"`;
258
373
  for (const root of ALLOWED_WRITE_ROOTS) {
259
374
  if (isUnderRoot(path, root)) return null;
260
375
  }
@@ -309,12 +424,113 @@ async function runTool(use) {
309
424
  return result.code === 0 ? okResult(use.id, result.stdout) : errResult(use.id, `exit=${result.code}
310
425
  ${result.stderr || result.stdout}`);
311
426
  }
427
+ if (use.name === "emit_hardware_receipt") {
428
+ const deviceKind = String(use.input.device_kind ?? "unknown-device");
429
+ const accelerator = use.input.accelerator === null || use.input.accelerator === "null" ? null : String(use.input.accelerator ?? "").trim() || null;
430
+ const runtimeName = String(use.input.runtime_name ?? "Ollama");
431
+ const runtimeVersion = String(use.input.runtime_version ?? "unknown");
432
+ const hostOs = String(use.input.host_os ?? "unknown");
433
+ const compositionId = String(use.input.composition_id ?? "unknown");
434
+ let measurements = [];
435
+ if (Array.isArray(use.input.measurements)) {
436
+ for (const m of use.input.measurements) {
437
+ const metric = String(m.metric ?? "");
438
+ const value = Number(m.value ?? 0);
439
+ const unit = String(m.unit ?? "");
440
+ if (metric && Number.isFinite(value)) {
441
+ measurements.push({ metric, value, unit, method: "measured" });
442
+ }
443
+ }
444
+ }
445
+ if (typeof use.input.tegrastats_output === "string" && use.input.tegrastats_output.length > 0) {
446
+ measurements = [...measurements, ...parseTegrastats(use.input.tegrastats_output)];
447
+ }
448
+ if (measurements.length === 0) {
449
+ measurements.push({ metric: "agent-tick", value: 1, unit: "count", method: "presence" });
450
+ }
451
+ const capturedAt = (/* @__PURE__ */ new Date()).toISOString();
452
+ const receipt = {
453
+ schemaVersion: "holoscript.hardware-receipt-metadata.v1",
454
+ target: {
455
+ id: `${deviceKind}-${Date.now()}`,
456
+ kind: deviceKind,
457
+ architecture: /jetson|orin|nano|agx|xavier/i.test(deviceKind) ? "arm64" : "unknown",
458
+ artifactKind: "measurement-trace"
459
+ },
460
+ device: {
461
+ vendor: /jetson|orin|nvidia/i.test(deviceKind) ? "nvidia" : "unknown",
462
+ model: deviceKind,
463
+ accelerator
464
+ },
465
+ runtime: { name: runtimeName, version: runtimeVersion, hostOS: hostOs },
466
+ compilerVersion: "holoscript-agent-1.0.0",
467
+ constraints: [],
468
+ measuredResults: measurements,
469
+ replayInputs: [
470
+ { kind: "composition-ref", uri: `compositions/${compositionId}`, sha256: "unknown" }
471
+ ],
472
+ provenance: {
473
+ capturedAt,
474
+ sourceCompositionHash: compositionId
475
+ },
476
+ owner: {
477
+ agent: process.env.HOLOSCRIPT_AGENT_HANDLE ?? "unknown",
478
+ ...process.env.HOLOMESH_TEAM_ID ? { team: process.env.HOLOMESH_TEAM_ID } : {}
479
+ }
480
+ };
481
+ const ts = capturedAt.replace(/[:.]/g, "-");
482
+ const outPath = resolve(ALLOWED_WRITE_ROOTS[0], `hardware-receipt-${ts}.json`);
483
+ const denied = checkWriteAllowed(outPath);
484
+ if (denied) return errResult(use.id, `Cannot write receipt: ${denied}`);
485
+ await mkdir(dirname(outPath), { recursive: true });
486
+ await writeFile(outPath, JSON.stringify(receipt, null, 2), "utf8");
487
+ return okResult(
488
+ use.id,
489
+ `Hardware receipt written to ${outPath} \u2014 ${measurements.length} measurements, accelerator=${accelerator ?? "none"}`
490
+ );
491
+ }
312
492
  return errResult(use.id, `unknown tool: ${use.name}`);
313
493
  } catch (err) {
314
494
  return errResult(use.id, err instanceof Error ? err.message : String(err));
315
495
  }
316
496
  }
497
+ function parseTegrastats(raw) {
498
+ const results = [];
499
+ const m = (pattern, metric, unit, transform) => {
500
+ const match = raw.match(pattern);
501
+ if (match?.[1]) {
502
+ const value = transform ? transform(match[1]) : Number(match[1]);
503
+ if (Number.isFinite(value)) results.push({ metric, value, unit, method: "tegrastats" });
504
+ }
505
+ };
506
+ const ram = raw.match(/RAM\s+(\d+)\/(\d+)MB/);
507
+ if (ram) {
508
+ const used = Number(ram[1]);
509
+ const total = Number(ram[2]);
510
+ results.push({ metric: "ram-used", value: used, unit: "MB", method: "tegrastats" });
511
+ results.push({ metric: "ram-total", value: total, unit: "MB", method: "tegrastats" });
512
+ if (total > 0)
513
+ results.push({ metric: "ram-pct", value: Math.round(used / total * 100), unit: "%", method: "tegrastats" });
514
+ }
515
+ m(/GR3D_FREQ\s+(\d+)%/, "gpu-util", "%");
516
+ m(/EMC_FREQ\s+(\d+)%/, "emc-freq-pct", "%");
517
+ m(/tj@([\d.]+)C/, "temp-tj", "C", parseFloat);
518
+ m(/cpu@([\d.]+)C/, "temp-cpu", "C", parseFloat);
519
+ m(/gpu@([\d.]+)C/, "temp-gpu", "C", parseFloat);
520
+ m(/VDD_SOC\s+(\d+)mW/, "power-soc", "mW");
521
+ m(/VDD_CPU_CV\s+(\d+)mW/, "power-cpu-cv", "mW");
522
+ m(/VDD_IN\s+(\d+)mW/, "power-total", "mW");
523
+ m(/CPU\s+\[(\d+)%/, "cpu-util-core0", "%");
524
+ return results;
525
+ }
317
526
  function runBash(cmd, cwd) {
527
+ if (process.env.VITEST === "true" || process.env.NODE_ENV === "test") {
528
+ return Promise.resolve({
529
+ code: 0,
530
+ stdout: `[mock-bash under vitest] cmd="${cmd}" cwd="${cwd}"`,
531
+ stderr: ""
532
+ });
533
+ }
318
534
  return new Promise((resolveProm) => {
319
535
  const child = spawn("bash", ["-c", cmd], { cwd, env: process.env });
320
536
  let stdout = "";
@@ -383,6 +599,35 @@ var AgentRunner = class {
383
599
  const { identity, brain, mesh, costGuard, provider, logger } = this.opts;
384
600
  const log = logger ?? (() => void 0);
385
601
  await this.heartbeatWithAutoRejoin();
602
+ if (this.opts.messageHandler) {
603
+ try {
604
+ const receipts = await this.opts.messageHandler.processMessages();
605
+ if (receipts.length > 0) {
606
+ log({
607
+ ev: "messages-processed",
608
+ count: receipts.length,
609
+ statuses: receipts.map((r) => r.status)
610
+ });
611
+ if (brain.capabilityTags.length === 0 || brain.capabilityTags.every((t) => t.startsWith("delegated"))) {
612
+ return {
613
+ action: "messages-processed",
614
+ spentUsd: costGuard.getState().spentUsd,
615
+ remainingUsd: costGuard.getRemainingUsd(),
616
+ receipts: receipts.map((r) => ({
617
+ status: r.status,
618
+ action: r.action,
619
+ reason: r.reason
620
+ }))
621
+ };
622
+ }
623
+ }
624
+ } catch (err) {
625
+ log({
626
+ ev: "message-handler-error",
627
+ message: err instanceof Error ? err.message : String(err)
628
+ });
629
+ }
630
+ }
386
631
  if (costGuard.isOverBudget()) {
387
632
  const state = costGuard.getState();
388
633
  log({ ev: "over-budget", spentUsd: state.spentUsd, budget: identity.budgetUsdPerDay });
@@ -416,6 +661,8 @@ var AgentRunner = class {
416
661
  const MAX_TOOL_ITERS = 30;
417
662
  let lastResponse;
418
663
  const toolsCalled = /* @__PURE__ */ new Set();
664
+ let productiveCallCount = 0;
665
+ let lastCommitHash;
419
666
  while (true) {
420
667
  iters++;
421
668
  if (iters > MAX_TOOL_ITERS) {
@@ -423,12 +670,16 @@ var AgentRunner = class {
423
670
  finalText = finalText || `[tool-loop hit ${MAX_TOOL_ITERS}-iter cap before final text]`;
424
671
  break;
425
672
  }
673
+ const activeTools = brain.requires.includes("local-llm") ? MESH_TOOLS.filter((t) => t.name === "write_file") : MESH_TOOLS;
426
674
  const resp = await provider.complete(
427
675
  {
428
676
  messages,
429
- maxTokens: 4096,
677
+ // 8192 for local thinking models (qwen3:4b uses ~3800 tokens on thinking
678
+ // before the tool-call JSON; 4096 cuts off mid-generation). Frontier
679
+ // models ignore this ceiling and stop naturally earlier.
680
+ maxTokens: 8192,
430
681
  temperature: 0.4,
431
- tools: MESH_TOOLS
682
+ tools: activeTools
432
683
  },
433
684
  identity.llmModel
434
685
  );
@@ -439,13 +690,39 @@ var AgentRunner = class {
439
690
  totalTokens: aggUsage.totalTokens + resp.usage.totalTokens
440
691
  };
441
692
  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);
693
+ log({
694
+ ev: "tool-call",
695
+ taskId: target.id,
696
+ iter: iters,
697
+ tools: resp.toolUses.map((t) => t.name)
698
+ });
699
+ for (const u of resp.toolUses) {
700
+ toolsCalled.add(u.name);
701
+ if (u.name === "write_file") {
702
+ const content = String(u.input?.content ?? "");
703
+ if (content.length > 0) productiveCallCount++;
704
+ } else if (u.name === "bash") {
705
+ const cmd = String(u.input?.cmd ?? "");
706
+ if (isProductiveBashCommand(cmd)) productiveCallCount++;
707
+ } else if (u.name === "emit_hardware_receipt") {
708
+ productiveCallCount++;
709
+ }
710
+ }
444
711
  messages.push({
445
712
  role: "assistant",
446
713
  content: resp.assistantBlocks ?? []
447
714
  });
448
715
  const toolResults = await Promise.all(resp.toolUses.map((u) => runTool(u)));
716
+ for (let ti = 0; ti < resp.toolUses.length; ti++) {
717
+ const tu = resp.toolUses[ti];
718
+ if (tu.name === "bash") {
719
+ const tr = toolResults[ti];
720
+ if (tr && !tr.is_error) {
721
+ const shaMatch = tr.content.match(/\b([0-9a-f]{7,40})\b/);
722
+ if (shaMatch) lastCommitHash = shaMatch[1];
723
+ }
724
+ }
725
+ }
449
726
  messages.push({
450
727
  role: "user",
451
728
  content: toolResults
@@ -456,24 +733,75 @@ var AgentRunner = class {
456
733
  break;
457
734
  }
458
735
  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) {
736
+ if (productiveCallCount === 0) {
462
737
  log({
463
738
  ev: "no-artifact",
464
739
  taskId: target.id,
465
740
  tool_iters: iters,
466
741
  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."
742
+ productiveCallCount,
743
+ message: "task execution did not produce a real artifact \u2014 refusing to mark executed. Required: write_file with non-empty content OR bash with a productive prefix (lake build / pnpm --filter / vitest run / lean / pnpm vitest). Pure-text, read-only inspection, and trivial-bash-bypass (`echo`, `cat`, etc.) do not satisfy the gate."
468
744
  });
469
745
  return {
470
746
  action: "no-artifact",
471
747
  taskId: target.id,
472
748
  spentUsd: costGuard.getState().spentUsd,
473
749
  remainingUsd: costGuard.getRemainingUsd(),
474
- message: `no side-effecting tool called (toolsCalled=[${[...toolsCalled].join(",")}], iters=${iters})`
750
+ message: `no productive tool call observed (toolsCalled=[${[...toolsCalled].join(",")}], productiveCallCount=${productiveCallCount}, iters=${iters})`
475
751
  };
476
752
  }
753
+ let reflectVerdict;
754
+ if (brain.reflect) {
755
+ try {
756
+ const reflectResp = await provider.complete(
757
+ {
758
+ messages: [
759
+ {
760
+ role: "system",
761
+ content: "You are a strict reviewer. Evaluate the work against the criteria; do not rewrite it."
762
+ },
763
+ {
764
+ role: "user",
765
+ content: `Reflect on the artifact produced for this task. Evaluate it for: ${brain.reflect.criteria}.
766
+
767
+ --- artifact / final response ---
768
+ ${finalText.slice(0, 4e3)}
769
+ --- end ---
770
+
771
+ Give a one-line reason, then end with exactly "VERDICT: PASS" or "VERDICT: FAIL".`
772
+ }
773
+ ],
774
+ maxTokens: 512,
775
+ temperature: 0.1
776
+ },
777
+ identity.llmModel
778
+ );
779
+ aggUsage = {
780
+ promptTokens: aggUsage.promptTokens + reflectResp.usage.promptTokens,
781
+ completionTokens: aggUsage.completionTokens + reflectResp.usage.completionTokens,
782
+ totalTokens: aggUsage.totalTokens + reflectResp.usage.totalTokens
783
+ };
784
+ const verdictMatch = /VERDICT:\s*(PASS|FAIL)/i.exec(reflectResp.content);
785
+ const pass = verdictMatch ? verdictMatch[1].toUpperCase() === "PASS" : true;
786
+ reflectVerdict = {
787
+ pass,
788
+ reason: reflectResp.content.replace(/VERDICT:\s*(PASS|FAIL)/i, "").trim().slice(0, 300)
789
+ };
790
+ log({
791
+ ev: "reflect",
792
+ taskId: target.id,
793
+ pass,
794
+ escalateOnFail: brain.reflect.escalateOnFail,
795
+ reason: reflectVerdict.reason.slice(0, 120)
796
+ });
797
+ } catch (err) {
798
+ log({
799
+ ev: "reflect-error",
800
+ taskId: target.id,
801
+ message: err instanceof Error ? err.message : String(err)
802
+ });
803
+ }
804
+ }
477
805
  const cost = costGuard.recordUsage(identity.llmModel, aggUsage);
478
806
  log({
479
807
  ev: "executed",
@@ -483,7 +811,11 @@ var AgentRunner = class {
483
811
  tokens: aggUsage.totalTokens,
484
812
  tool_iters: iters
485
813
  });
486
- const response = { ...lastResponse ?? { content: finalText, usage: aggUsage }, content: finalText, usage: aggUsage };
814
+ const response = {
815
+ ...lastResponse ?? { content: finalText, usage: aggUsage },
816
+ content: finalText,
817
+ usage: aggUsage
818
+ };
487
819
  const execResult = {
488
820
  taskId: target.id,
489
821
  responseText: response.content,
@@ -517,10 +849,32 @@ var AgentRunner = class {
517
849
  });
518
850
  const posted = await mesh.postAuditRecords(identity.handle, [caelRecord]);
519
851
  this.prevCaelChain = caelRecord.fnv1a_chain;
520
- log({ ev: "cael-posted", taskId: target.id, appended: posted.appended, rejected: posted.rejected });
852
+ log({
853
+ ev: "cael-posted",
854
+ taskId: target.id,
855
+ appended: posted.appended,
856
+ rejected: posted.rejected
857
+ });
521
858
  } catch (err) {
522
859
  log({ ev: "cael-post-error", message: err instanceof Error ? err.message : String(err) });
523
860
  }
861
+ if (reflectVerdict && !reflectVerdict.pass && brain.reflect?.escalateOnFail) {
862
+ try {
863
+ await mesh.sendMessageOnTask(
864
+ target.id,
865
+ `[${identity.handle}] reflect gate FAILED \u2014 escalating to the fleet instead of marking done. Reason: ${reflectVerdict.reason}`
866
+ );
867
+ } catch {
868
+ }
869
+ log({ ev: "reflect-escalate", taskId: target.id, reason: reflectVerdict.reason.slice(0, 120) });
870
+ return {
871
+ action: "reflect-escalate",
872
+ taskId: target.id,
873
+ spentUsd: costGuard.getState().spentUsd,
874
+ remainingUsd: costGuard.getRemainingUsd(),
875
+ message: `reflect self-evaluation failed; escalated to fleet (reason: ${reflectVerdict.reason.slice(0, 120)})`
876
+ };
877
+ }
524
878
  if (this.opts.onTaskExecuted) {
525
879
  await this.opts.onTaskExecuted(execResult, target);
526
880
  } else {
@@ -531,6 +885,16 @@ var AgentRunner = class {
531
885
  ${response.content}`
532
886
  );
533
887
  }
888
+ try {
889
+ await mesh.markDone(target.id, finalText.slice(0, 500), lastCommitHash);
890
+ log({ ev: "mark-done", taskId: target.id, commitHash: lastCommitHash });
891
+ } catch (err) {
892
+ log({
893
+ ev: "mark-done-error",
894
+ taskId: target.id,
895
+ message: err instanceof Error ? err.message : String(err)
896
+ });
897
+ }
534
898
  return {
535
899
  action: "executed",
536
900
  taskId: target.id,
@@ -623,7 +987,7 @@ function buildTaskPrompt(task) {
623
987
  "Description:",
624
988
  task.description ?? "(no description)",
625
989
  "",
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."
990
+ "Produce the deliverable: call write_file (or bash with a build command) to create all required output files FIRST. Apply your brain composition rules \u2014 anti-patterns, decision loop, and scope tier all bind. After calling the tool(s), return a short plain-text summary of what you did for posting to /room."
627
991
  ].join("\n");
628
992
  }
629
993
  function sleep(ms) {
@@ -637,8 +1001,10 @@ function jitter(base) {
637
1001
  import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
638
1002
  import { dirname as dirname2 } from "path";
639
1003
  var ANTHROPIC_PRICING_USD_PER_MTOK = {
640
- "claude-opus-4-7": { input: 15, output: 75 },
641
- "claude-opus-4-6": { input: 15, output: 75 },
1004
+ "claude-opus-4-8": { input: 10, output: 50 },
1005
+ // 3× cheaper than 4.7 on total cost; A-020 2026-06-08
1006
+ "claude-opus-4-7": { input: 5, output: 25 },
1007
+ "claude-opus-4-6": { input: 5, output: 25 },
642
1008
  "claude-sonnet-4-6": { input: 3, output: 15 },
643
1009
  "claude-haiku-4-5-20251001": { input: 1, output: 5 },
644
1010
  "claude-haiku-4-5": { input: 1, output: 5 }
@@ -714,16 +1080,52 @@ function todayUtc() {
714
1080
  // src/brain.ts
715
1081
  import { readFile as readFile2 } from "fs/promises";
716
1082
  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 };
1083
+ const raw = await readFile2(brainPath, "utf8");
1084
+ const { domain, capabilityTags, requires, prefers, avoids } = extractIdentity(raw);
1085
+ const systemPrompt = extractSystemPromptPreamble(raw);
1086
+ return {
1087
+ brainPath,
1088
+ systemPrompt,
1089
+ capabilityTags,
1090
+ domain,
1091
+ scopeTier,
1092
+ requires,
1093
+ prefers,
1094
+ avoids,
1095
+ reflect: extractReflect(raw)
1096
+ };
1097
+ }
1098
+ function extractReflect(brain) {
1099
+ const block = sliceNamedBlock(brain, "reflect");
1100
+ if (block === void 0) return void 0;
1101
+ const criteria = scalarField(block, "criteria") ?? scalarField(block, "scorer") ?? scalarField(block, "of") ?? "correctness, completeness, and valid HoloScript syntax";
1102
+ const escRaw = scalarField(block, "escalate_on_fail") ?? scalarField(block, "escalateOnFail") ?? scalarField(block, "escalate");
1103
+ return { criteria, escalateOnFail: (escRaw ?? "").split(",")[0].trim().toLowerCase() === "true" };
1104
+ }
1105
+ function extractSystemPromptPreamble(src) {
1106
+ const lines = src.split("\n");
1107
+ const BLOCK_START = /^(#version|#target|#mode|identity\s*\{|state\s*\{|computed\s*\{|traits\s*\[|capabilities\s*\{|directives\s*\{|behavior\s)/;
1108
+ let cutLine = -1;
1109
+ for (let i = 0; i < lines.length; i++) {
1110
+ if (BLOCK_START.test(lines[i].trim())) {
1111
+ cutLine = i;
1112
+ break;
1113
+ }
1114
+ }
1115
+ if (cutLine <= 0) return src;
1116
+ return lines.slice(0, cutLine).join("\n").trimEnd();
720
1117
  }
721
1118
  function extractIdentity(brain) {
722
1119
  const identityBlock = sliceNamedBlock(brain, "identity");
723
- if (!identityBlock) return { domain: "unknown", capabilityTags: [] };
1120
+ if (!identityBlock) {
1121
+ return { domain: "unknown", capabilityTags: [], requires: [], prefers: [], avoids: [] };
1122
+ }
724
1123
  const domain = scalarField(identityBlock, "domain") ?? "unknown";
725
1124
  const capabilityTags = listField(identityBlock, "capability_tags") ?? [];
726
- return { domain, capabilityTags };
1125
+ const requires = listField(identityBlock, "requires") ?? [];
1126
+ const prefers = listField(identityBlock, "prefers") ?? [];
1127
+ const avoids = listField(identityBlock, "avoids") ?? [];
1128
+ return { domain, capabilityTags, requires, prefers, avoids };
727
1129
  }
728
1130
  function sliceNamedBlock(src, name) {
729
1131
  const re = new RegExp(`\\b${name}\\s*:?\\s*\\{`, "g");
@@ -801,7 +1203,9 @@ function makeCommitHook(opts) {
801
1203
  const relPath = relativeTo(cwd, filePath);
802
1204
  const addRes = spawn2("git", ["add", relPath], { cwd, encoding: "utf8" });
803
1205
  if (addRes.status !== 0) {
804
- throw new Error(`git add failed: ${addRes.stderr || addRes.stdout || `exit ${addRes.status}`}`);
1206
+ throw new Error(
1207
+ `git add failed: ${addRes.stderr || addRes.stdout || `exit ${addRes.status}`}`
1208
+ );
805
1209
  }
806
1210
  const message = renderCommitMessage({ scope, task, identity, result });
807
1211
  const commitArgs = ["commit", "-m", message];
@@ -810,7 +1214,9 @@ function makeCommitHook(opts) {
810
1214
  }
811
1215
  const commitRes = spawn2("git", commitArgs, { cwd, encoding: "utf8" });
812
1216
  if (commitRes.status !== 0) {
813
- throw new Error(`git commit failed: ${commitRes.stderr || commitRes.stdout || `exit ${commitRes.status}`}`);
1217
+ throw new Error(
1218
+ `git commit failed: ${commitRes.stderr || commitRes.stdout || `exit ${commitRes.status}`}`
1219
+ );
814
1220
  }
815
1221
  const hashRes = spawn2("git", ["rev-parse", "HEAD"], { cwd, encoding: "utf8" });
816
1222
  const commitHash = hashRes.status === 0 ? hashRes.stdout.trim() : void 0;
@@ -994,6 +1400,181 @@ function applyFilter(events, filter) {
994
1400
  return result;
995
1401
  }
996
1402
 
1403
+ // src/capability-router.ts
1404
+ import {
1405
+ ANTHROPIC_CAPABILITIES,
1406
+ OPENAI_CAPABILITIES,
1407
+ GEMINI_CAPABILITIES,
1408
+ XAI_CAPABILITIES,
1409
+ OPENROUTER_CAPABILITIES,
1410
+ LOCAL_LLM_CAPABILITIES,
1411
+ BITNET_CAPABILITIES,
1412
+ MOCK_CAPABILITIES
1413
+ } from "@holoscript/llm-provider";
1414
+ var NoEligibleProviderError = class extends Error {
1415
+ constructor(requires, avoids, considered, excludedByAvoids) {
1416
+ super(
1417
+ `No provider satisfies brain requires=[${requires.join(", ")}] avoids=[${avoids.join(", ")}]. Considered: [${considered.join(", ")}]. Excluded by avoids: [${excludedByAvoids.join(", ")}].`
1418
+ );
1419
+ this.requires = requires;
1420
+ this.avoids = avoids;
1421
+ this.considered = considered;
1422
+ this.excludedByAvoids = excludedByAvoids;
1423
+ this.name = "NoEligibleProviderError";
1424
+ }
1425
+ };
1426
+ function satisfies(capabilities, key) {
1427
+ const value = capabilities[key];
1428
+ if (typeof value === "boolean") return value;
1429
+ if (typeof value === "number") return value > 0;
1430
+ return false;
1431
+ }
1432
+ function countMatches(capabilities, keys) {
1433
+ let count = 0;
1434
+ for (const key of keys) {
1435
+ if (satisfies(capabilities, key)) count++;
1436
+ }
1437
+ return count;
1438
+ }
1439
+ function unsatisfiedKeys(capabilities, keys) {
1440
+ return keys.filter((key) => !satisfies(capabilities, key));
1441
+ }
1442
+ function pickProvider(opts) {
1443
+ const { brain, envOverride, candidates } = opts;
1444
+ const tieBreaker = opts.tieBreakerOrder ?? candidates.map((c) => c.name);
1445
+ if (candidates.length === 0) {
1446
+ throw new Error("pickProvider: no candidates supplied");
1447
+ }
1448
+ const excludedByAvoids = [];
1449
+ const notAvoided = [];
1450
+ for (const candidate of candidates) {
1451
+ const matchesAvoid = brain.avoids.some((a) => satisfies(candidate.capabilities, a));
1452
+ if (matchesAvoid) {
1453
+ excludedByAvoids.push(candidate.name);
1454
+ } else {
1455
+ notAvoided.push(candidate);
1456
+ }
1457
+ }
1458
+ if (brain.requires.length === 0) {
1459
+ if (envOverride !== void 0) {
1460
+ const envCandidate = candidates.find((c) => c.name === envOverride);
1461
+ const matchedPrefers = envCandidate ? brain.prefers.filter((p) => satisfies(envCandidate.capabilities, p)) : [];
1462
+ return {
1463
+ picked: envOverride,
1464
+ reason: "env-override-no-requirements",
1465
+ unsatisfiedRequires: [],
1466
+ matchedPrefers,
1467
+ excludedByAvoids,
1468
+ alternatives: candidates.filter((c) => c.name !== envOverride).map((c) => c.name)
1469
+ };
1470
+ }
1471
+ const ordered = orderCandidates(notAvoided, tieBreaker);
1472
+ if (ordered.length === 0) {
1473
+ return {
1474
+ picked: candidates[0].name,
1475
+ reason: "open-routing-default",
1476
+ unsatisfiedRequires: [],
1477
+ matchedPrefers: brain.prefers.filter((p) => satisfies(candidates[0].capabilities, p)),
1478
+ excludedByAvoids,
1479
+ alternatives: candidates.slice(1).map((c) => c.name)
1480
+ };
1481
+ }
1482
+ return {
1483
+ picked: ordered[0].name,
1484
+ reason: "open-routing-default",
1485
+ unsatisfiedRequires: [],
1486
+ matchedPrefers: brain.prefers.filter((p) => satisfies(ordered[0].capabilities, p)),
1487
+ excludedByAvoids,
1488
+ alternatives: ordered.slice(1).map((c) => c.name)
1489
+ };
1490
+ }
1491
+ const eligible = notAvoided.filter(
1492
+ (c) => unsatisfiedKeys(c.capabilities, brain.requires).length === 0
1493
+ );
1494
+ if (eligible.length === 0) {
1495
+ if (envOverride !== void 0) {
1496
+ const envCandidate = candidates.find((c) => c.name === envOverride);
1497
+ const unsatisfied = envCandidate ? unsatisfiedKeys(envCandidate.capabilities, brain.requires) : brain.requires.slice();
1498
+ const matchedPrefers = envCandidate ? brain.prefers.filter((p) => satisfies(envCandidate.capabilities, p)) : [];
1499
+ return {
1500
+ picked: envOverride,
1501
+ reason: "env-override-mismatch",
1502
+ unsatisfiedRequires: unsatisfied,
1503
+ matchedPrefers,
1504
+ excludedByAvoids,
1505
+ alternatives: []
1506
+ };
1507
+ }
1508
+ throw new NoEligibleProviderError(
1509
+ brain.requires,
1510
+ brain.avoids,
1511
+ candidates.map((c) => c.name),
1512
+ excludedByAvoids
1513
+ );
1514
+ }
1515
+ const ranked = [...eligible].sort((a, b) => {
1516
+ const aMatches = countMatches(a.capabilities, brain.prefers);
1517
+ const bMatches = countMatches(b.capabilities, brain.prefers);
1518
+ if (aMatches !== bMatches) return bMatches - aMatches;
1519
+ const aIdx = tieBreaker.indexOf(a.name);
1520
+ const bIdx = tieBreaker.indexOf(b.name);
1521
+ const aRank = aIdx === -1 ? Number.MAX_SAFE_INTEGER : aIdx;
1522
+ const bRank = bIdx === -1 ? Number.MAX_SAFE_INTEGER : bIdx;
1523
+ return aRank - bRank;
1524
+ });
1525
+ if (envOverride !== void 0) {
1526
+ const envEligible = ranked.find((c) => c.name === envOverride);
1527
+ if (envEligible) {
1528
+ return {
1529
+ picked: envOverride,
1530
+ reason: "env-override-satisfies",
1531
+ unsatisfiedRequires: [],
1532
+ matchedPrefers: brain.prefers.filter((p) => satisfies(envEligible.capabilities, p)),
1533
+ excludedByAvoids,
1534
+ alternatives: ranked.filter((c) => c.name !== envOverride).map((c) => c.name)
1535
+ };
1536
+ }
1537
+ const envCandidate = candidates.find((c) => c.name === envOverride);
1538
+ const unsatisfied = envCandidate ? unsatisfiedKeys(envCandidate.capabilities, brain.requires) : brain.requires.slice();
1539
+ return {
1540
+ picked: envOverride,
1541
+ reason: "env-override-mismatch",
1542
+ unsatisfiedRequires: unsatisfied,
1543
+ matchedPrefers: envCandidate ? brain.prefers.filter((p) => satisfies(envCandidate.capabilities, p)) : [],
1544
+ excludedByAvoids,
1545
+ alternatives: ranked.map((c) => c.name)
1546
+ };
1547
+ }
1548
+ const top = ranked[0];
1549
+ return {
1550
+ picked: top.name,
1551
+ reason: "capability-best-fit",
1552
+ unsatisfiedRequires: [],
1553
+ matchedPrefers: brain.prefers.filter((p) => satisfies(top.capabilities, p)),
1554
+ excludedByAvoids,
1555
+ alternatives: ranked.slice(1).map((c) => c.name)
1556
+ };
1557
+ }
1558
+ var BUILT_IN_CANDIDATES = [
1559
+ { name: "anthropic", capabilities: ANTHROPIC_CAPABILITIES },
1560
+ { name: "openai", capabilities: OPENAI_CAPABILITIES },
1561
+ { name: "gemini", capabilities: GEMINI_CAPABILITIES },
1562
+ { name: "xai", capabilities: XAI_CAPABILITIES },
1563
+ { name: "openrouter", capabilities: OPENROUTER_CAPABILITIES },
1564
+ { name: "local-llm", capabilities: LOCAL_LLM_CAPABILITIES },
1565
+ { name: "bitnet", capabilities: BITNET_CAPABILITIES },
1566
+ { name: "mock", capabilities: MOCK_CAPABILITIES }
1567
+ ];
1568
+ function orderCandidates(candidates, tieBreaker) {
1569
+ return [...candidates].sort((a, b) => {
1570
+ const aIdx = tieBreaker.indexOf(a.name);
1571
+ const bIdx = tieBreaker.indexOf(b.name);
1572
+ const aRank = aIdx === -1 ? Number.MAX_SAFE_INTEGER : aIdx;
1573
+ const bRank = bIdx === -1 ? Number.MAX_SAFE_INTEGER : bIdx;
1574
+ return aRank - bRank;
1575
+ });
1576
+ }
1577
+
997
1578
  // src/supervisor.ts
998
1579
  var Supervisor = class {
999
1580
  constructor(opts) {
@@ -1036,13 +1617,29 @@ var Supervisor = class {
1036
1617
  return { ...managed.status };
1037
1618
  }
1038
1619
  async bootAgent(spec) {
1039
- const identity = this.identityFromSpec(spec);
1040
1620
  const brain = await loadBrain(spec.brainPath, spec.scopeTier ?? "warm");
1041
- const provider = await this.opts.providerFactory(spec, identity);
1621
+ const decision = pickProvider({
1622
+ brain,
1623
+ envOverride: spec.provider,
1624
+ candidates: BUILT_IN_CANDIDATES
1625
+ });
1626
+ const effectiveSpec = decision.picked === spec.provider ? spec : { ...spec, provider: decision.picked };
1627
+ const identity = this.identityFromSpec(effectiveSpec);
1628
+ if (decision.reason === "env-override-mismatch" && this.opts.logger) {
1629
+ this.opts.logger({
1630
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
1631
+ ev: "capability-router-mismatch",
1632
+ handle: spec.handle,
1633
+ envOverride: spec.provider,
1634
+ unsatisfiedRequires: decision.unsatisfiedRequires,
1635
+ excludedByAvoids: decision.excludedByAvoids
1636
+ });
1637
+ }
1638
+ const provider = await this.opts.providerFactory(effectiveSpec, identity);
1042
1639
  const stateDir = this.opts.stateDir ?? join2(homedir(), ".holoscript-agent", "cost-state");
1043
- const isFree = spec.provider === "mock" || spec.provider === "local-llm" || spec.provider === "bitnet";
1640
+ const isFree = effectiveSpec.provider === "mock" || effectiveSpec.provider === "local-llm" || effectiveSpec.provider === "bitnet";
1044
1641
  const costGuard = new CostGuard({
1045
- statePath: join2(stateDir, `${spec.handle}.json`),
1642
+ statePath: join2(stateDir, `${effectiveSpec.handle}.json`),
1046
1643
  dailyBudgetUsd: identity.budgetUsdPerDay,
1047
1644
  pricer: isFree ? () => 0 : void 0
1048
1645
  });
@@ -1052,7 +1649,7 @@ var Supervisor = class {
1052
1649
  teamId: identity.teamId,
1053
1650
  fetchImpl: this.opts.fetchImpl
1054
1651
  });
1055
- const onTaskExecuted = spec.enableCommitHook ? this.buildCommitHook(spec, identity, mesh) : void 0;
1652
+ const onTaskExecuted = effectiveSpec.enableCommitHook ? this.buildCommitHook(effectiveSpec, identity, mesh) : void 0;
1056
1653
  const runner = new AgentRunner({
1057
1654
  identity,
1058
1655
  brain,
@@ -1061,16 +1658,16 @@ var Supervisor = class {
1061
1658
  mesh,
1062
1659
  onTaskExecuted,
1063
1660
  auditLog: this.auditLog,
1064
- logger: (ev) => this.log({ agent: spec.handle, ...ev })
1661
+ logger: (ev) => this.log({ agent: effectiveSpec.handle, ...ev })
1065
1662
  });
1066
1663
  const status = {
1067
- handle: spec.handle,
1664
+ handle: effectiveSpec.handle,
1068
1665
  state: "starting",
1069
1666
  spentUsd: 0,
1070
1667
  remainingUsd: identity.budgetUsdPerDay,
1071
1668
  restarts: 0
1072
1669
  };
1073
- return { spec, identity, brain, runner, costGuard, status };
1670
+ return { spec: effectiveSpec, identity, brain, runner, costGuard, status };
1074
1671
  }
1075
1672
  buildCommitHook(spec, identity, mesh) {
1076
1673
  const writer = makeCommitHook({
@@ -1092,7 +1689,9 @@ var Supervisor = class {
1092
1689
  }
1093
1690
  const wallet = process.env[spec.walletEnvKey];
1094
1691
  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}"`);
1692
+ throw new Error(
1693
+ `Missing or malformed wallet env var "${spec.walletEnvKey}" for agent "${spec.handle}"`
1694
+ );
1096
1695
  }
1097
1696
  return {
1098
1697
  handle: spec.handle,