@turmind/halo 2.2.2 → 2.2.4

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.
Files changed (42) hide show
  1. package/README.md +2 -1
  2. package/admin-out/404.html +1 -1
  3. package/admin-out/_next/static/chunks/1989-9d30cdb1a86e4beb.js +1 -0
  4. package/admin-out/_next/static/chunks/4362.115405260299210d.js +1 -0
  5. package/admin-out/_next/static/chunks/{7613.fe24922a6a839656.js → 7073.4081ed209e6748bd.js} +1 -1
  6. package/admin-out/_next/static/chunks/{7073.9d5f9dd7a77827b7.js → 7613.9c1657bb3b52d1b5.js} +1 -1
  7. package/admin-out/_next/static/chunks/app/{page-10a4afac9cbc27a2.js → page-c9d174a5432b0007.js} +1 -1
  8. package/admin-out/_next/static/chunks/{webpack-9532ec7f1fa9a757.js → webpack-1970c5140e4019b7.js} +1 -1
  9. package/admin-out/_next/static/css/87d4fc049f865389.css +1 -0
  10. package/admin-out/index.html +1 -1
  11. package/admin-out/index.txt +5 -5
  12. package/bundled-docs/design/a2a.md +3 -1
  13. package/bundled-docs/design/architecture.md +1 -1
  14. package/bundled-docs/design/cron.md +1 -1
  15. package/bundled-docs/design/evolution.md +6 -6
  16. package/bundled-docs/design/node.md +18 -39
  17. package/bundled-docs/design/observability.md +2 -2
  18. package/bundled-docs/design/relay.md +1 -1
  19. package/bundled-docs/design/session.md +3 -3
  20. package/bundled-docs/design/storage.md +5 -2
  21. package/bundled-docs/design/stt-local.md +2 -2
  22. package/bundled-docs/design/ws.md +3 -3
  23. package/bundled-docs/dev/api.md +10 -9
  24. package/bundled-docs/dev/deploy.md +1 -1
  25. package/bundled-docs/dev/env.md +2 -1
  26. package/bundled-docs/dev/tools.md +10 -6
  27. package/bundled-docs/guide/README.md +1 -1
  28. package/bundled-docs/guide/cli.md +4 -31
  29. package/bundled-docs/guide/node.md +57 -36
  30. package/bundled-docs/requirements/command.md +1 -1
  31. package/bundled-docs/requirements/settings.md +39 -2
  32. package/dist/evo-wrapper.js +368 -268
  33. package/dist/index.js +2330 -2531
  34. package/package.json +2 -1
  35. package/templates/agents/default/agent.yaml +1 -1
  36. package/templates/models/aws-bedrock-openai.yaml +81 -7
  37. package/templates/skills/cron/SKILL.md +1 -3
  38. package/admin-out/_next/static/chunks/1989-3b2faa2731d973c4.js +0 -1
  39. package/admin-out/_next/static/chunks/4362.9d2ee4f9b6facc49.js +0 -1
  40. package/admin-out/_next/static/css/8fc43fce83e5ce5d.css +0 -1
  41. /package/admin-out/_next/static/{u9pg60rITCj73MBYeXsW1 → vf5W8US5y8B2f1E6zJJw3}/_buildManifest.js +0 -0
  42. /package/admin-out/_next/static/{u9pg60rITCj73MBYeXsW1 → vf5W8US5y8B2f1E6zJJw3}/_ssgManifest.js +0 -0
@@ -1,8 +1,8 @@
1
1
  // ../server/dist/evolution/evo-wrapper.js
2
- import fs11 from "node:fs";
3
- import path13 from "node:path";
2
+ import fs12 from "node:fs";
3
+ import path14 from "node:path";
4
4
  import { spawn as spawn2 } from "node:child_process";
5
- import { homedir as homedir6 } from "node:os";
5
+ import { homedir as homedir7 } from "node:os";
6
6
  import { fileURLToPath as fileURLToPath2 } from "node:url";
7
7
  import { parseArgs } from "node:util";
8
8
  import { eq as eq3, and as and3, inArray as inArray2 } from "drizzle-orm";
@@ -237,41 +237,133 @@ function evoApplyLogFile(applyId) {
237
237
  }
238
238
 
239
239
  // ../server/dist/config.js
240
+ import fs9 from "node:fs";
241
+ import path12 from "node:path";
242
+ import { homedir as homedir6 } from "node:os";
243
+
244
+ // ../server/dist/settings-schema.js
240
245
  import fs8 from "node:fs";
241
246
  import path11 from "node:path";
242
247
  import { homedir as homedir5 } from "node:os";
243
248
  import YAML5 from "yaml";
244
249
 
245
- // ../server/dist/settings-schema.js
246
- import fs7 from "node:fs";
247
- import path10 from "node:path";
248
- import { homedir as homedir4 } from "node:os";
249
- import YAML4 from "yaml";
250
-
251
250
  // ../server/dist/tools/sandbox.js
252
251
  import { exec, execFile, spawn } from "node:child_process";
253
252
  import { promisify } from "node:util";
253
+ import path4 from "node:path";
254
+ import { homedir as homedir3 } from "node:os";
255
+
256
+ // ../server/dist/settings-file.js
257
+ import fs2 from "node:fs";
254
258
  import path3 from "node:path";
255
259
  import { homedir as homedir2 } from "node:os";
260
+ import YAML from "yaml";
261
+ function loadYamlFile(filePath) {
262
+ try {
263
+ const raw = fs2.readFileSync(filePath, "utf-8");
264
+ return YAML.parse(raw) ?? {};
265
+ } catch {
266
+ return {};
267
+ }
268
+ }
269
+ var SETTINGS_PATH = path3.join(homedir2(), ".halo", "secrets", "settings.yaml");
270
+ var _settingsCache = null;
271
+ function getSettings() {
272
+ let mtimeMs = 0;
273
+ try {
274
+ mtimeMs = fs2.statSync(SETTINGS_PATH).mtimeMs;
275
+ } catch {
276
+ }
277
+ if (!_settingsCache || _settingsCache.mtimeMs !== mtimeMs) {
278
+ _settingsCache = { mtimeMs, data: loadYamlFile(SETTINGS_PATH) };
279
+ }
280
+ return _settingsCache.data;
281
+ }
282
+ var ENV_PATTERN = /<<([A-Z_][A-Z0-9_]*)>>/g;
283
+ function expandEnv(raw) {
284
+ if (!raw)
285
+ return "";
286
+ return raw.replace(ENV_PATTERN, (m, name) => process.env[name] ?? m);
287
+ }
288
+
289
+ // ../server/dist/outbound-proxy.js
290
+ var DEFAULT_NO_PROXY = ["localhost", "127.0.0.1", "::1"];
291
+ function checkProxyUrl(raw) {
292
+ let url;
293
+ try {
294
+ url = new URL(raw);
295
+ } catch {
296
+ return "not a valid URL";
297
+ }
298
+ if (url.protocol !== "http:" && url.protocol !== "https:")
299
+ return "scheme must be http:// or https://";
300
+ if (url.username || url.password)
301
+ return "credentials are not allowed in the URL \u2014 put them in Token (general.outbound_proxy.token)";
302
+ return null;
303
+ }
304
+ var _cache = null;
305
+ function outboundProxy() {
306
+ const settings = getSettings();
307
+ if (_cache?.settings !== settings)
308
+ _cache = { settings, proxy: resolve(settings) };
309
+ return _cache.proxy;
310
+ }
311
+ function resolve(settings) {
312
+ const general = settings.general;
313
+ const cfg = general?.outbound_proxy;
314
+ const enabled = ["true", "1"].includes(String(cfg?.enabled ?? "").trim().toLowerCase());
315
+ const raw = typeof cfg?.url === "string" ? cfg.url.trim() : "";
316
+ if (!enabled || !raw)
317
+ return null;
318
+ const bad = checkProxyUrl(raw);
319
+ if (bad) {
320
+ console.warn(`[OutboundProxy] general.outbound_proxy.url ignored (${bad}) \u2014 proxy off`);
321
+ return null;
322
+ }
323
+ const url = new URL(raw).origin;
324
+ const cred = cfg?.token == null ? "" : expandEnv(String(cfg.token)).trim();
325
+ const colon = cred.indexOf(":");
326
+ const user = colon < 0 ? cred : cred.slice(0, colon);
327
+ const pass = colon < 0 ? "" : cred.slice(colon + 1);
328
+ const userinfo = `${encodeURIComponent(user)}:${encodeURIComponent(pass)}`;
329
+ const basic = Buffer.from(`${user}:${pass}`).toString("base64");
330
+ const inherited = [process.env.NO_PROXY, process.env.no_proxy].flatMap((v) => (v ?? "").split(","));
331
+ const merged = [.../* @__PURE__ */ new Set([...DEFAULT_NO_PROXY, ...inherited.map((s) => s.trim()).filter(Boolean)])];
332
+ const noProxy = merged.includes("*") ? ["*"] : merged;
333
+ console.log(`[OutboundProxy] on: ${url}${cred ? " (with token)" : ""}`);
334
+ return {
335
+ url,
336
+ envUrl: cred ? url.replace("://", `://${userinfo}@`) : url,
337
+ authorization: cred ? `Basic ${basic}` : "",
338
+ noProxy,
339
+ credential: cred ? { raw: cred, plain: `${user}:${pass}`, userinfo, basic } : null
340
+ };
341
+ }
256
342
 
257
343
  // ../server/dist/child-env.js
258
344
  var settingsOtelEnvKeys = /* @__PURE__ */ new Set();
259
- function cleanChildEnv() {
345
+ function cleanChildEnv(proxyEnv = outboundProxyEnv()) {
260
346
  const env = { ...process.env };
261
347
  delete env.HALO_PASSWORD;
262
348
  delete env.HALO_JWT_SECRET;
263
349
  delete env.HALO_NODE_TOKEN;
264
350
  for (const key of settingsOtelEnvKeys)
265
351
  delete env[key];
266
- return env;
352
+ return Object.assign(env, proxyEnv);
353
+ }
354
+ function outboundProxyEnv(proxy = outboundProxy()) {
355
+ if (!proxy)
356
+ return {};
357
+ const noProxy = proxy.noProxy.join(",");
358
+ return { http_proxy: proxy.envUrl, https_proxy: proxy.envUrl, HTTP_PROXY: proxy.envUrl, HTTPS_PROXY: proxy.envUrl, no_proxy: noProxy, NO_PROXY: noProxy };
267
359
  }
268
360
 
269
361
  // ../server/dist/tools/sandbox.js
270
362
  var execAsync = promisify(exec);
271
363
  var execFileAsync = promisify(execFile);
272
- var HOME = homedir2();
273
- var HALO_HOME = path3.join(HOME, ".halo");
274
- var EMPTY_MASK_FILE = path3.join(HALO_HOME, ".sandbox-empty");
364
+ var HOME = homedir3();
365
+ var HALO_HOME = path4.join(HOME, ".halo");
366
+ var EMPTY_MASK_FILE = path4.join(HALO_HOME, ".sandbox-empty");
275
367
  var DEFAULT_HIDDEN_DIRS = [
276
368
  "~/.halo/secrets",
277
369
  "~/.aws",
@@ -313,9 +405,9 @@ var DEFAULT_HIDDEN_FILES = [
313
405
  ];
314
406
 
315
407
  // ../server/dist/models/registry.js
316
- import fs2 from "node:fs";
317
- import path4 from "node:path";
318
- import YAML from "yaml";
408
+ import fs3 from "node:fs";
409
+ import path5 from "node:path";
410
+ import YAML2 from "yaml";
319
411
  var MODEL_RUNTIME_NAMES = [
320
412
  "anthropic-messages",
321
413
  "openai-chat",
@@ -346,15 +438,15 @@ function logOnce(msg) {
346
438
  function readProviderDir(dir, source) {
347
439
  let names;
348
440
  try {
349
- names = fs2.readdirSync(dir, { withFileTypes: true }).filter((e) => e.isFile() && e.name.endsWith(".yaml")).map((e) => e.name).sort();
441
+ names = fs3.readdirSync(dir, { withFileTypes: true }).filter((e) => e.isFile() && e.name.endsWith(".yaml")).map((e) => e.name).sort();
350
442
  } catch {
351
443
  return [];
352
444
  }
353
445
  const out = [];
354
446
  for (const name of names) {
355
- const file = path4.join(dir, name);
447
+ const file = path5.join(dir, name);
356
448
  try {
357
- const parsed = YAML.parse(fs2.readFileSync(file, "utf-8"));
449
+ const parsed = YAML2.parse(fs3.readFileSync(file, "utf-8"));
358
450
  if (parsed && typeof parsed === "object" && typeof parsed.id === "string") {
359
451
  out.push({
360
452
  id: parsed.id,
@@ -402,34 +494,34 @@ function loadProviders(bundledDir = globalModelsDir(), hubDir = globalHubModelsD
402
494
  }
403
495
 
404
496
  // ../server/dist/extensions/registry.js
405
- import fs4 from "node:fs";
406
- import path7 from "node:path";
497
+ import fs5 from "node:fs";
498
+ import path8 from "node:path";
407
499
 
408
500
  // ../server/dist/extensions/state.js
409
- import fs3 from "node:fs";
410
- import path6 from "node:path";
501
+ import fs4 from "node:fs";
502
+ import path7 from "node:path";
411
503
 
412
504
  // ../server/dist/init.js
413
- import path5 from "node:path";
505
+ import path6 from "node:path";
414
506
  import { fileURLToPath } from "node:url";
415
- import YAML2 from "yaml";
416
- var __dirname = path5.dirname(fileURLToPath(import.meta.url));
417
- var TEMPLATES_DIR = path5.resolve(__dirname, "..", "templates");
507
+ import YAML3 from "yaml";
508
+ var __dirname = path6.dirname(fileURLToPath(import.meta.url));
509
+ var TEMPLATES_DIR = path6.resolve(__dirname, "..", "templates");
418
510
  var DOCS_SRC_CANDIDATES = [
419
- path5.resolve(__dirname, "..", "bundled-docs"),
420
- path5.resolve(__dirname, "..", "..", "..", ".halo", "docs")
511
+ path6.resolve(__dirname, "..", "bundled-docs"),
512
+ path6.resolve(__dirname, "..", "..", "..", ".halo", "docs")
421
513
  ];
422
514
 
423
515
  // ../server/dist/extensions/state.js
424
516
  function extensionStatePath() {
425
- return path6.join(globalExtensionsDir(), ".state.json");
517
+ return path7.join(globalExtensionsDir(), ".state.json");
426
518
  }
427
519
  var cache = null;
428
520
  function load() {
429
521
  const file = extensionStatePath();
430
522
  let st;
431
523
  try {
432
- st = fs3.statSync(file);
524
+ st = fs4.statSync(file);
433
525
  } catch {
434
526
  return { states: {}, disabled: /* @__PURE__ */ new Set() };
435
527
  }
@@ -438,7 +530,7 @@ function load() {
438
530
  return cache;
439
531
  let states = null;
440
532
  try {
441
- const raw = JSON.parse(fs3.readFileSync(file, "utf-8"));
533
+ const raw = JSON.parse(fs4.readFileSync(file, "utf-8"));
442
534
  if (typeof raw === "object" && raw !== null && !Array.isArray(raw))
443
535
  states = raw;
444
536
  } catch {
@@ -683,17 +775,17 @@ function parseManifest(raw, dirName, entryExists) {
683
775
  ...m.source !== void 0 ? { source: m.source } : {}
684
776
  };
685
777
  }
686
- function readExtensionDir(dir, id = path7.basename(dir)) {
778
+ function readExtensionDir(dir, id = path8.basename(dir)) {
687
779
  let raw;
688
780
  try {
689
- raw = JSON.parse(fs4.readFileSync(path7.join(dir, MANIFEST_FILE), "utf-8"));
781
+ raw = JSON.parse(fs5.readFileSync(path8.join(dir, MANIFEST_FILE), "utf-8"));
690
782
  } catch (err) {
691
783
  const code = err.code;
692
784
  return { id, error: code === "ENOENT" ? `${MANIFEST_FILE} missing` : `${MANIFEST_FILE} unreadable: ${err instanceof Error ? err.message : String(err)}` };
693
785
  }
694
786
  const parsed = parseManifest(raw, id, (entry) => {
695
787
  try {
696
- return fs4.statSync(path7.join(dir, entry)).isFile();
788
+ return fs5.statSync(path8.join(dir, entry)).isFile();
697
789
  } catch {
698
790
  return false;
699
791
  }
@@ -702,7 +794,7 @@ function readExtensionDir(dir, id = path7.basename(dir)) {
702
794
  return { id, error: parsed.error };
703
795
  let installedAt = 0;
704
796
  try {
705
- installedAt = Math.round(fs4.statSync(dir).mtimeMs);
797
+ installedAt = Math.round(fs5.statSync(dir).mtimeMs);
706
798
  } catch {
707
799
  }
708
800
  return { ...parsed, installedAt };
@@ -713,9 +805,9 @@ function getSnapshot() {
713
805
  }
714
806
 
715
807
  // ../server/dist/agents/skill-sources.js
716
- import fs5 from "node:fs";
717
- import path8 from "node:path";
718
- import { homedir as homedir3 } from "node:os";
808
+ import fs6 from "node:fs";
809
+ import path9 from "node:path";
810
+ import { homedir as homedir4 } from "node:os";
719
811
 
720
812
  // ../server/dist/routes/mtime-cache.js
721
813
  function createMtimeCache() {
@@ -733,11 +825,11 @@ function createMtimeCache() {
733
825
  }
734
826
 
735
827
  // ../server/dist/agents/skill-sources.js
736
- var GLOBAL_SKILLS_DIR = path8.join(homedir3(), ".halo", "global", "skills");
828
+ var GLOBAL_SKILLS_DIR = path9.join(homedir4(), ".halo", "global", "skills");
737
829
  var SKILL_ID_RE = /^[\w-]+$/;
738
830
  function isFile(p) {
739
831
  try {
740
- return fs5.statSync(p).isFile();
832
+ return fs6.statSync(p).isFile();
741
833
  } catch {
742
834
  return false;
743
835
  }
@@ -745,13 +837,13 @@ function isFile(p) {
745
837
  function childDirs(parent) {
746
838
  let names;
747
839
  try {
748
- names = fs5.readdirSync(parent);
840
+ names = fs6.readdirSync(parent);
749
841
  } catch {
750
842
  return [];
751
843
  }
752
844
  return names.filter((n) => {
753
845
  try {
754
- return fs5.statSync(path8.join(parent, n)).isDirectory();
846
+ return fs6.statSync(path9.join(parent, n)).isDirectory();
755
847
  } catch {
756
848
  return false;
757
849
  }
@@ -766,7 +858,7 @@ function extensionSkillDirs() {
766
858
  const root = extensionsRoot();
767
859
  let extIds;
768
860
  try {
769
- extIds = fs5.readdirSync(root);
861
+ extIds = fs6.readdirSync(root);
770
862
  } catch {
771
863
  return [];
772
864
  }
@@ -774,31 +866,31 @@ function extensionSkillDirs() {
774
866
  const taken = /* @__PURE__ */ new Set();
775
867
  let off;
776
868
  for (const extId of extIds.filter(isExtensionId).sort()) {
777
- const extDir = path8.join(root, extId);
778
- const ids = childDirs(path8.join(extDir, "skills"));
869
+ const extDir = path9.join(root, extId);
870
+ const ids = childDirs(path9.join(extDir, "skills"));
779
871
  if (ids.length === 0)
780
872
  continue;
781
873
  off ??= disabledExtensionIds();
782
874
  if (off.has(extId))
783
875
  continue;
784
- const manifest = path8.join(extDir, MANIFEST_FILE);
876
+ const manifest = path9.join(extDir, MANIFEST_FILE);
785
877
  let mtimeMs;
786
878
  try {
787
- mtimeMs = fs5.statSync(manifest).mtimeMs;
879
+ mtimeMs = fs6.statSync(manifest).mtimeMs;
788
880
  } catch {
789
881
  continue;
790
882
  }
791
883
  let entry = _manifestEntry.get(manifest, mtimeMs);
792
884
  if (entry === void 0) {
793
885
  const ext = readExtensionDir(extDir);
794
- entry = _manifestEntry.set(manifest, mtimeMs, "error" in ext ? null : path8.join(extDir, ext.entry));
886
+ entry = _manifestEntry.set(manifest, mtimeMs, "error" in ext ? null : path9.join(extDir, ext.entry));
795
887
  }
796
888
  if (!entry || !isFile(entry))
797
889
  continue;
798
890
  const ignored = [];
799
891
  for (const id of ids.sort()) {
800
- const dir = path8.join(extDir, "skills", id);
801
- if (!fs5.existsSync(path8.join(dir, "SKILL.md")))
892
+ const dir = path9.join(extDir, "skills", id);
893
+ if (!fs6.existsSync(path9.join(dir, "SKILL.md")))
802
894
  continue;
803
895
  if (!isExtensionSkillId(extId, id) || taken.has(id)) {
804
896
  ignored.push(id);
@@ -823,28 +915,28 @@ function listSkillDirs(workspaceRoot) {
823
915
  seen.add(s.id);
824
916
  };
825
917
  if (workspaceRoot) {
826
- const wsDir = path8.join(workspaceRoot, ".halo", "skills");
918
+ const wsDir = path9.join(workspaceRoot, ".halo", "skills");
827
919
  for (const id of childDirs(wsDir))
828
- add({ id, dir: path8.join(wsDir, id), scope: "workspace" });
920
+ add({ id, dir: path9.join(wsDir, id), scope: "workspace" });
829
921
  }
830
922
  for (const id of childDirs(GLOBAL_SKILLS_DIR))
831
- add({ id, dir: path8.join(GLOBAL_SKILLS_DIR, id), scope: "global" });
923
+ add({ id, dir: path9.join(GLOBAL_SKILLS_DIR, id), scope: "global" });
832
924
  for (const s of extensionSkillDirs())
833
925
  add(s);
834
926
  return out;
835
927
  }
836
928
 
837
929
  // ../server/dist/a2a/remotes.js
838
- import fs6 from "node:fs";
839
- import path9 from "node:path";
840
- import YAML3 from "yaml";
930
+ import fs7 from "node:fs";
931
+ import path10 from "node:path";
932
+ import YAML4 from "yaml";
841
933
  function remotesFile() {
842
- return path9.join(secretsDir(), "a2a-remotes.yaml");
934
+ return path10.join(secretsDir(), "a2a-remotes.yaml");
843
935
  }
844
936
  function loadRemotes() {
845
937
  let doc;
846
938
  try {
847
- doc = YAML3.parse(fs6.readFileSync(remotesFile(), "utf8")) ?? {};
939
+ doc = YAML4.parse(fs7.readFileSync(remotesFile(), "utf8")) ?? {};
848
940
  } catch {
849
941
  return {};
850
942
  }
@@ -858,7 +950,7 @@ function loadRemotes() {
858
950
  }
859
951
 
860
952
  // ../server/dist/settings-schema.js
861
- var GLOBAL_AGENTS_DIR = path10.join(homedir4(), ".halo", "global", "agents");
953
+ var GLOBAL_AGENTS_DIR = path11.join(homedir5(), ".halo", "global", "agents");
862
954
  var DEFAULT_A2A_URL_ALLOWLIST = "100.64.0.0/10,*.ts.net";
863
955
  var DEFAULT_FILES_SUMMARY = DEFAULT_HIDDEN_FILES.filter((f) => !/-(wal|shm)$/.test(f)).join(", ");
864
956
  function generalSection() {
@@ -869,9 +961,12 @@ function generalSection() {
869
961
  displayName_zh: "\u5E38\u89C4",
870
962
  description: "Server behavior parameters (session limits, compaction, sandbox, logging).",
871
963
  description_zh: "\u670D\u52A1\u7AEF\u884C\u4E3A\u53C2\u6570\uFF08\u4F1A\u8BDD\u9650\u5236\u3001\u538B\u7F29\u3001\u6C99\u7BB1\u3001\u65E5\u5FD7\uFF09\u3002",
872
- // Every general.* field is globalOnly: config.ts reads them via
873
- // settingsValue() from the global settings.yaml only. Per-workspace
874
- // layering exists solely for namespaced params/secrets (getServerSecret).
964
+ // Every general.* field is globalOnly — config.ts reads them via
965
+ // settingsValue() from the global settings.yaml only; per-workspace
966
+ // layering is for namespaced params/secrets (getServerSecret) — except
967
+ // evolution.level and evolution.auto_apply_threshold: how far evo may act
968
+ // on its own is a property of the workspace (an open one at L0, a private
969
+ // one at L2/L3), read per workspace by evoLevelFor / autoApplyThresholdFor.
875
970
  fields: [
876
971
  // system-wide language preference. Read by internal agents (evo, score)
877
972
  // that don't load INSTRUCTIONS.md, and available to anything else that
@@ -917,11 +1012,18 @@ function generalSection() {
917
1012
  // a workspace must not widen; public_url names this server to peers.
918
1013
  { key: "a2a.public_url", type: "string", globalOnly: true, description: "Origin peers use to reach this server for A2A (e.g. http://myhost.tailnet.ts.net:9527): the agent card's interface URL and the webhook URL given to remotes. Empty = derived from the request Host. Env HALO_A2A_PUBLIC_URL overrides.", description_zh: "\u5BF9\u7AEF\u8BBF\u95EE\u672C\u670D\u52A1\u7684 A2A \u6E90\u5730\u5740\uFF08\u5982 http://myhost.tailnet.ts.net:9527\uFF09\uFF1A\u7528\u4E8E agent card \u7684\u63A5\u53E3 URL \u548C\u4EA4\u7ED9\u8FDC\u7AEF\u7684 webhook URL\u3002\u7559\u7A7A\u5219\u6309\u8BF7\u6C42 Host \u63A8\u5BFC\u3002\u73AF\u5883\u53D8\u91CF HALO_A2A_PUBLIC_URL \u4F18\u5148\u3002", default: "" },
919
1014
  { key: "a2a.url_allowlist", type: "string", globalOnly: true, description: "Comma-separated CIDRs / host patterns A2A may reach besides public https (push webhooks, remote cards and RPC). Private / loopback ranges are refused unless listed; plain http is allowed only to listed hosts / ranges. Link-local (169.254.0.0/16, fe80::/10) is always refused.", description_zh: "A2A \u9664\u516C\u7F51 https \u4E4B\u5916\u5141\u8BB8\u8BBF\u95EE\u7684 CIDR / \u4E3B\u673A\u6A21\u5F0F\uFF08\u9017\u53F7\u5206\u9694\uFF0C\u7528\u4E8E push webhook\u3001\u8FDC\u7AEF card \u548C RPC\uFF09\u3002\u79C1\u7F51 / \u56DE\u73AF\u5730\u5740\u5FC5\u987B\u5217\u51FA\u624D\u5141\u8BB8\uFF1B\u660E\u6587 http \u53EA\u5141\u8BB8\u5217\u51FA\u7684\u4E3B\u673A / \u7F51\u6BB5\u3002\u94FE\u8DEF\u672C\u5730\u5730\u5740\uFF08169.254.0.0/16\u3001fe80::/10\uFF09\u59CB\u7EC8\u62D2\u7EDD\u3002", default: DEFAULT_A2A_URL_ALLOWLIST },
920
- // self-evolution (see plans/self-evolution.md). All evo settings are
921
- // global-only — the worker / ticker live in the server process and
922
- // would have to reload mid-flight if a workspace could override them.
923
- { key: "evolution.level", type: "enum", options: ["L0", "L1", "L2", "L3"], globalOnly: true, description: "Self-evolution capability ladder. L0: manual only \u2014 drafts via /evo, manual approve/reject. L1: L0 plus automatic drafting on pre-compact. L2: L1 plus auto-applying prompt-only changes that score above evolution.auto_apply_threshold. L3: L2 plus auto-applying capability changes (agent.yaml tools / model, skill invocation). Auto-applied changes still pass the regression check and can be rolled back from the Evolution tab. Queued runs always execute regardless of level.", description_zh: "\u81EA\u6211\u8FDB\u5316\u7B49\u7EA7\u3002L0\uFF1A\u4EC5\u624B\u52A8\u2014\u2014\u901A\u8FC7 /evo \u8D77\u8349\uFF0C\u624B\u52A8 approve/reject\u3002L1\uFF1A\u5728 L0 \u57FA\u7840\u4E0A\uFF0C\u538B\u7F29\u524D\uFF08pre-compact\uFF09\u81EA\u52A8\u8D77\u8349\u3002L2\uFF1A\u5728 L1 \u57FA\u7840\u4E0A\uFF0C\u5BF9\u8BC4\u5206\u8D85\u8FC7 evolution.auto_apply_threshold \u7684\u7EAF\u63D0\u793A\u8BCD\u6539\u52A8\u81EA\u52A8\u5E94\u7528\u3002L3\uFF1A\u5728 L2 \u57FA\u7840\u4E0A\uFF0C\u5BF9\u80FD\u529B\u7C7B\u6539\u52A8\uFF08agent.yaml \u7684\u5DE5\u5177/\u6A21\u578B\u3001skill \u8C03\u7528\u65B9\u5F0F\uFF09\u4E5F\u81EA\u52A8\u5E94\u7528\u3002\u81EA\u52A8\u5E94\u7528\u7167\u6837\u8FC7\u56DE\u5F52\u68C0\u67E5\uFF0C\u53EF\u5728 Evolution \u9875\u56DE\u6EDA\u3002\u5DF2\u6392\u961F\u7684 run \u4E0D\u53D7\u7B49\u7EA7\u5F71\u54CD\uFF0C\u59CB\u7EC8\u6267\u884C\u3002", default: "L0" },
924
- { key: "evolution.auto_apply_threshold", type: "int", min: 0, max: 100, globalOnly: true, description: "Minimum average score (0\u2013100) for auto-apply at L2/L3. Also required: confidence high, behavior \u2265 70, lint 100, all sandbox checks passed. Ignored at L0/L1.", description_zh: "L2/L3 \u81EA\u52A8\u5E94\u7528\u8981\u6C42\u7684\u6700\u4F4E\u5E73\u5747\u5206\uFF080\u2013100\uFF09\u3002\u53E6\u9700\u540C\u65F6\u6EE1\u8DB3\uFF1Aconfidence \u4E3A high\u3001behavior \u2265 70\u3001lint \u4E3A 100\u3001\u6C99\u7BB1\u68C0\u67E5\u5168\u90E8\u901A\u8FC7\u3002L0/L1 \u4E0B\u5FFD\u7565\u3002", default: "80" },
1015
+ // Outbound proxy (outbound-proxy.ts). Global-only: a machine's way out,
1016
+ // not a workspace's. Read per spawn / request — no restart.
1017
+ { key: "outbound_proxy.enabled", type: "boolean", globalOnly: true, description: "Route outbound traffic through the proxy below: shell_exec, newly opened Terminals, notebook kernels and evolution runs get http_proxy / https_proxy (and the upper-case forms) in their env, and web_fetch and A2A requests to remotes go through it. localhost / 127.0.0.1 / ::1 always connect directly. Off = Halo sets no proxy vars (any the server inherited still pass). Applies on the next command / request; a Terminal already open keeps its env.", description_zh: "\u8BA9\u51FA\u7AD9\u6D41\u91CF\u8D70\u4E0B\u9762\u7684\u4EE3\u7406\uFF1Ashell_exec\u3001\u65B0\u5F00\u7684 Terminal\u3001notebook \u5185\u6838\u548C evolution \u8FD0\u884C\u7684\u73AF\u5883\u53D8\u91CF\u91CC\u4F1A\u8BBE\u4E0A http_proxy / https_proxy\uFF08\u5927\u5199\u5F62\u5F0F\u4E5F\u4E00\u8D77\u8BBE\uFF09\uFF0Cweb_fetch \u548C\u53D1\u5F80 A2A \u8FDC\u7AEF\u7684\u8BF7\u6C42\u4E5F\u8D70\u4EE3\u7406\u3002localhost / 127.0.0.1 / ::1 \u59CB\u7EC8\u76F4\u8FDE\u3002\u5173\u95ED\u540E Halo \u4E0D\u8BBE\u4EFB\u4F55\u4EE3\u7406\u53D8\u91CF\uFF08\u670D\u52A1\u8FDB\u7A0B\u672C\u8EAB\u7EE7\u627F\u7684\u7167\u5E38\u4F20\u4E0B\u53BB\uFF09\u3002\u4E0B\u4E00\u6761\u547D\u4EE4 / \u8BF7\u6C42\u5373\u751F\u6548\uFF1B\u5DF2\u7ECF\u6253\u5F00\u7684 Terminal \u4FDD\u6301\u539F\u6765\u7684\u73AF\u5883\u3002", default: "false" },
1018
+ { key: "outbound_proxy.url", type: "string", globalOnly: true, description: "Proxy address, http:// or https:// (e.g. http://127.0.0.1:12334 for a local Hiddify / Clash). No user:pass@ in the URL \u2014 put credentials in Token. Empty = proxy off even when enabled.", description_zh: "\u4EE3\u7406\u5730\u5740\uFF0Chttp:// \u6216 https://\uFF08\u5982\u672C\u5730 Hiddify / Clash \u7684 http://127.0.0.1:12334\uFF09\u3002URL \u91CC\u4E0D\u80FD\u5E26 user:pass@\uFF0C\u51ED\u8BC1\u586B\u5230 Token\u3002\u7559\u7A7A\u65F6\u5373\u4F7F\u6253\u5F00\u5F00\u5173\u4E5F\u4E0D\u8D70\u4EE3\u7406\u3002", default: "", placeholder: "http://127.0.0.1:12334" },
1019
+ { key: "outbound_proxy.token", type: "string", secret: true, globalOnly: true, description: "Optional proxy credential: user:password, or a bare token (sent as the user name, empty password). Sent as Basic auth \u2014 in the proxy URL's user-info for child processes (the form curl / git / pip read), as Proxy-Authorization for web_fetch and A2A. Leave empty for a proxy without auth.", description_zh: "\u53EF\u9009\u7684\u4EE3\u7406\u51ED\u8BC1\uFF1Auser:password\uFF0C\u6216\u5355\u72EC\u4E00\u4E2A token\uFF08\u4F5C\u4E3A\u7528\u6237\u540D\uFF0C\u5BC6\u7801\u4E3A\u7A7A\uFF09\u3002\u4EE5 Basic \u8BA4\u8BC1\u53D1\u9001\u2014\u2014\u5B50\u8FDB\u7A0B\u653E\u5728\u4EE3\u7406 URL \u7684 user-info \u91CC\uFF08curl / git / pip \u90FD\u8BA4\u8FD9\u79CD\u5199\u6CD5\uFF09\uFF0Cweb_fetch \u548C A2A \u7528 Proxy-Authorization \u5934\u3002\u4EE3\u7406\u4E0D\u9700\u8981\u9274\u6743\u5C31\u7559\u7A7A\u3002", default: "" },
1020
+ // self-evolution (see plans/self-evolution.md). The level and the
1021
+ // auto-apply threshold are per workspace (read where a run's workspace is
1022
+ // known: the pre-compact trigger, the wrapper's auto-apply verdict and
1023
+ // recheck). The rest stays global-only: the ticker's slots, timeouts and
1024
+ // retries are server-wide, and pre-compact snapshots are a server switch.
1025
+ { key: "evolution.level", type: "enum", options: ["L0", "L1", "L2", "L3"], description: "Self-evolution capability ladder. L0: manual only \u2014 drafts via /evo, manual approve/reject. L1: L0 plus automatic drafting on pre-compact. L2: L1 plus auto-applying prompt-only changes that score above evolution.auto_apply_threshold. L3: L2 plus auto-applying capability changes (agent.yaml tools / model, skill invocation). Auto-applied changes still pass the regression check and can be rolled back from the Evolution tab. Queued runs always execute regardless of level.", description_zh: "\u81EA\u6211\u8FDB\u5316\u7B49\u7EA7\u3002L0\uFF1A\u4EC5\u624B\u52A8\u2014\u2014\u901A\u8FC7 /evo \u8D77\u8349\uFF0C\u624B\u52A8 approve/reject\u3002L1\uFF1A\u5728 L0 \u57FA\u7840\u4E0A\uFF0C\u538B\u7F29\u524D\uFF08pre-compact\uFF09\u81EA\u52A8\u8D77\u8349\u3002L2\uFF1A\u5728 L1 \u57FA\u7840\u4E0A\uFF0C\u5BF9\u8BC4\u5206\u8D85\u8FC7 evolution.auto_apply_threshold \u7684\u7EAF\u63D0\u793A\u8BCD\u6539\u52A8\u81EA\u52A8\u5E94\u7528\u3002L3\uFF1A\u5728 L2 \u57FA\u7840\u4E0A\uFF0C\u5BF9\u80FD\u529B\u7C7B\u6539\u52A8\uFF08agent.yaml \u7684\u5DE5\u5177/\u6A21\u578B\u3001skill \u8C03\u7528\u65B9\u5F0F\uFF09\u4E5F\u81EA\u52A8\u5E94\u7528\u3002\u81EA\u52A8\u5E94\u7528\u7167\u6837\u8FC7\u56DE\u5F52\u68C0\u67E5\uFF0C\u53EF\u5728 Evolution \u9875\u56DE\u6EDA\u3002\u5DF2\u6392\u961F\u7684 run \u4E0D\u53D7\u7B49\u7EA7\u5F71\u54CD\uFF0C\u59CB\u7EC8\u6267\u884C\u3002", default: "L0" },
1026
+ { key: "evolution.auto_apply_threshold", type: "int", min: 0, max: 100, description: "Minimum average score (0\u2013100) for auto-apply at L2/L3. Also required: confidence high, behavior \u2265 70, lint 100, all sandbox checks passed. Ignored at L0/L1.", description_zh: "L2/L3 \u81EA\u52A8\u5E94\u7528\u8981\u6C42\u7684\u6700\u4F4E\u5E73\u5747\u5206\uFF080\u2013100\uFF09\u3002\u53E6\u9700\u540C\u65F6\u6EE1\u8DB3\uFF1Aconfidence \u4E3A high\u3001behavior \u2265 70\u3001lint \u4E3A 100\u3001\u6C99\u7BB1\u68C0\u67E5\u5168\u90E8\u901A\u8FC7\u3002L0/L1 \u4E0B\u5FFD\u7565\u3002", default: "80" },
925
1027
  { key: "evolution.max_concurrent_run", type: "int", globalOnly: true, description: "Max concurrent evo run wrappers (drafting patches).", description_zh: "\u540C\u65F6\u8FD0\u884C\u7684 evo run wrapper \u4E0A\u9650\uFF08\u8D77\u8349 patch\uFF09\u3002", default: "1" },
926
1028
  { key: "evolution.max_concurrent_apply", type: "int", globalOnly: true, description: "Max concurrent evo apply wrappers (merging patches).", description_zh: "\u540C\u65F6\u8FD0\u884C\u7684 evo apply wrapper \u4E0A\u9650\uFF08\u5408\u5E76 patch\uFF09\u3002", default: "1" },
927
1029
  { key: "evolution.run_timeout_minutes", type: "int", globalOnly: true, description: "Heartbeat timeout for run wrappers. The wrapper writes a heartbeat every 60s for as long as it lives, so exceeding this means the wrapper process died \u2014 the row is requeued (or marked timeout past max_attempts). This is dead-wrapper detection latency, NOT a budget for slow models; the per-phase model budget is the wrapper-internal cap (draft 60min, other phases 45min).", description_zh: "run wrapper \u5FC3\u8DF3\u8D85\u65F6\uFF08\u5206\u949F\uFF09\u3002wrapper \u5B58\u6D3B\u671F\u95F4\u6BCF 60 \u79D2\u5199\u4E00\u6B21\u5FC3\u8DF3\uFF0C\u8D85\u65F6\u5373\u89C6\u4E3A wrapper \u8FDB\u7A0B\u5DF2\u6B7B\uFF0C\u4EFB\u52A1\u91CD\u6392\uFF08\u8D85\u8FC7 max_attempts \u5219\u6807 timeout\uFF09\u3002\u8FD9\u662F\u6B7B wrapper \u7684\u68C0\u6D4B\u5EF6\u8FDF\uFF0C\u4E0D\u662F\u6162\u6A21\u578B\u7684\u8FD0\u884C\u9884\u7B97\u2014\u2014\u5355\u9636\u6BB5\u6A21\u578B\u9884\u7B97\u662F wrapper \u5185\u90E8\u7684\u4E0A\u9650\uFF08\u8D77\u8349 60 \u5206\u949F\uFF0C\u5176\u4F59\u9636\u6BB5 45 \u5206\u949F\uFF09\u3002", default: "12" },
@@ -951,11 +1053,11 @@ var _yamlCache = createMtimeCache();
951
1053
  var _skillNameCache = createMtimeCache();
952
1054
  function readYamlFile(p) {
953
1055
  try {
954
- const { mtimeMs } = fs7.statSync(p);
1056
+ const { mtimeMs } = fs8.statSync(p);
955
1057
  const hit = _yamlCache.get(p, mtimeMs);
956
1058
  if (hit !== void 0)
957
1059
  return hit;
958
- const parsed = YAML4.parse(fs7.readFileSync(p, "utf-8"));
1060
+ const parsed = YAML5.parse(fs8.readFileSync(p, "utf-8"));
959
1061
  return _yamlCache.set(p, mtimeMs, parsed && typeof parsed === "object" ? parsed : null);
960
1062
  } catch {
961
1063
  return null;
@@ -1010,8 +1112,8 @@ function providerSections() {
1010
1112
  return sections;
1011
1113
  }
1012
1114
  function buildSkillSection(skillsDir, skillId) {
1013
- const configPath = path10.join(skillsDir, skillId, "config.yaml");
1014
- if (!fs7.existsSync(configPath))
1115
+ const configPath = path11.join(skillsDir, skillId, "config.yaml");
1116
+ if (!fs8.existsSync(configPath))
1015
1117
  return null;
1016
1118
  const parsed = readYamlFile(configPath);
1017
1119
  if (!parsed)
@@ -1022,11 +1124,11 @@ function buildSkillSection(skillsDir, skillId) {
1022
1124
  return null;
1023
1125
  let displayName = skillId;
1024
1126
  try {
1025
- const mdPath = path10.join(skillsDir, skillId, "SKILL.md");
1026
- const { mtimeMs } = fs7.statSync(mdPath);
1127
+ const mdPath = path11.join(skillsDir, skillId, "SKILL.md");
1128
+ const { mtimeMs } = fs8.statSync(mdPath);
1027
1129
  let name = _skillNameCache.get(mdPath, mtimeMs);
1028
1130
  if (name === void 0) {
1029
- const m = fs7.readFileSync(mdPath, "utf-8").match(/^---\s*\n([\s\S]*?)\n---/);
1131
+ const m = fs8.readFileSync(mdPath, "utf-8").match(/^---\s*\n([\s\S]*?)\n---/);
1030
1132
  const nameMatch = m?.[1].match(/^name:\s*(.+)$/m);
1031
1133
  name = _skillNameCache.set(mdPath, mtimeMs, nameMatch ? nameMatch[1].trim() : null);
1032
1134
  }
@@ -1047,7 +1149,7 @@ function buildSkillSection(skillsDir, skillId) {
1047
1149
  function skillSections(workspaceRoot) {
1048
1150
  const byId = /* @__PURE__ */ new Map();
1049
1151
  for (const { id, dir } of listSkillDirs(workspaceRoot).reverse()) {
1050
- const section = buildSkillSection(path10.dirname(dir), id);
1152
+ const section = buildSkillSection(path11.dirname(dir), id);
1051
1153
  if (section)
1052
1154
  byId.set(section.namespaceId, section);
1053
1155
  }
@@ -1057,7 +1159,7 @@ function agentSections() {
1057
1159
  const sections = [];
1058
1160
  let entries = [];
1059
1161
  try {
1060
- entries = fs7.readdirSync(GLOBAL_AGENTS_DIR, { withFileTypes: true });
1162
+ entries = fs8.readdirSync(GLOBAL_AGENTS_DIR, { withFileTypes: true });
1061
1163
  } catch {
1062
1164
  return sections;
1063
1165
  }
@@ -1065,8 +1167,8 @@ function agentSections() {
1065
1167
  if (!entry.isDirectory())
1066
1168
  continue;
1067
1169
  const agentId = entry.name;
1068
- const configPath = path10.join(GLOBAL_AGENTS_DIR, agentId, "agent-config.yaml");
1069
- if (!fs7.existsSync(configPath))
1170
+ const configPath = path11.join(GLOBAL_AGENTS_DIR, agentId, "agent-config.yaml");
1171
+ if (!fs8.existsSync(configPath))
1070
1172
  continue;
1071
1173
  const parsed = readYamlFile(configPath);
1072
1174
  if (!parsed)
@@ -1077,7 +1179,7 @@ function agentSections() {
1077
1179
  continue;
1078
1180
  let displayName = agentId;
1079
1181
  try {
1080
- const ay = readYamlFile(path10.join(GLOBAL_AGENTS_DIR, agentId, "agent.yaml"));
1182
+ const ay = readYamlFile(path11.join(GLOBAL_AGENTS_DIR, agentId, "agent.yaml"));
1081
1183
  if (ay && typeof ay.name === "string")
1082
1184
  displayName = ay.name;
1083
1185
  } catch {
@@ -1140,23 +1242,15 @@ function extensionSections() {
1140
1242
  }
1141
1243
 
1142
1244
  // ../server/dist/config.js
1143
- var HALO_HOME2 = path11.join(homedir5(), ".halo");
1144
- var HALO_GLOBAL_DIR = path11.join(HALO_HOME2, "global");
1145
- var HALO_SECRETS_DIR = path11.join(HALO_HOME2, "secrets");
1146
- function loadYamlFile(filePath) {
1147
- try {
1148
- const raw = fs8.readFileSync(filePath, "utf-8");
1149
- return YAML5.parse(raw) ?? {};
1150
- } catch {
1151
- return {};
1152
- }
1153
- }
1154
- var SYSTEM_CONFIG_PATH = path11.join(HALO_SECRETS_DIR, "config.yaml");
1245
+ var HALO_HOME2 = path12.join(homedir6(), ".halo");
1246
+ var HALO_GLOBAL_DIR = path12.join(HALO_HOME2, "global");
1247
+ var HALO_SECRETS_DIR = path12.join(HALO_HOME2, "secrets");
1248
+ var SYSTEM_CONFIG_PATH = path12.join(HALO_SECRETS_DIR, "config.yaml");
1155
1249
  var _systemConfigCache = null;
1156
1250
  function getSystemConfig() {
1157
1251
  let mtimeMs = 0;
1158
1252
  try {
1159
- mtimeMs = fs8.statSync(SYSTEM_CONFIG_PATH).mtimeMs;
1253
+ mtimeMs = fs9.statSync(SYSTEM_CONFIG_PATH).mtimeMs;
1160
1254
  } catch {
1161
1255
  }
1162
1256
  if (!_systemConfigCache || _systemConfigCache.mtimeMs !== mtimeMs) {
@@ -1164,19 +1258,6 @@ function getSystemConfig() {
1164
1258
  }
1165
1259
  return _systemConfigCache.data;
1166
1260
  }
1167
- var SETTINGS_PATH = path11.join(HALO_SECRETS_DIR, "settings.yaml");
1168
- var _settingsCache = null;
1169
- function getSettings() {
1170
- let mtimeMs = 0;
1171
- try {
1172
- mtimeMs = fs8.statSync(SETTINGS_PATH).mtimeMs;
1173
- } catch {
1174
- }
1175
- if (!_settingsCache || _settingsCache.mtimeMs !== mtimeMs) {
1176
- _settingsCache = { mtimeMs, data: loadYamlFile(SETTINGS_PATH) };
1177
- }
1178
- return _settingsCache.data;
1179
- }
1180
1261
  function readYamlValue(tree, dotPath) {
1181
1262
  const parts = dotPath.split(".");
1182
1263
  let cur = tree;
@@ -1303,6 +1384,21 @@ function systemString(envKey, configPath, fallback) {
1303
1384
  return cv;
1304
1385
  return fallback;
1305
1386
  }
1387
+ var _wsSettingsCache = /* @__PURE__ */ new Map();
1388
+ function getWorkspaceSettings(workspaceRoot) {
1389
+ const p = path12.join(workspaceRoot, ".halo", "settings.yaml");
1390
+ let mtimeMs = 0;
1391
+ try {
1392
+ mtimeMs = fs9.statSync(p).mtimeMs;
1393
+ } catch {
1394
+ }
1395
+ const hit = _wsSettingsCache.get(workspaceRoot);
1396
+ if (hit && hit.mtimeMs === mtimeMs)
1397
+ return hit.data;
1398
+ const entry = { mtimeMs, data: loadYamlFile(p) };
1399
+ _wsSettingsCache.set(workspaceRoot, entry);
1400
+ return entry.data;
1401
+ }
1306
1402
  var EVO_LEVELS = ["L0", "L1", "L2", "L3"];
1307
1403
  function parseEvoLevel(raw) {
1308
1404
  return EVO_LEVELS.includes(raw) ? raw : "L0";
@@ -1310,6 +1406,19 @@ function parseEvoLevel(raw) {
1310
1406
  function evoLevelRank(level) {
1311
1407
  return EVO_LEVELS.indexOf(level);
1312
1408
  }
1409
+ function generalValueFor(workspaceRoot, dotPath) {
1410
+ const ws = readYamlValue(getWorkspaceSettings(workspaceRoot), dotPath);
1411
+ if (ws != null && String(ws) !== "")
1412
+ return String(ws);
1413
+ return settingsValue(dotPath);
1414
+ }
1415
+ function evoLevelFor(workspaceRoot) {
1416
+ return parseEvoLevel(generalValueFor(workspaceRoot, "general.evolution.level") ?? schemaDefault("general.evolution.level", "L0"));
1417
+ }
1418
+ function autoApplyThresholdFor(workspaceRoot) {
1419
+ const n = parseInt(generalValueFor(workspaceRoot, "general.evolution.auto_apply_threshold") ?? "", 10);
1420
+ return isNaN(n) ? settingsInt("general.evolution.auto_apply_threshold", 80) : n;
1421
+ }
1313
1422
  var config = {
1314
1423
  // Goal entry points are retired, not the runtime. Internal/global-only, no UI:
1315
1424
  // set general.goal_mode_enabled in ~/.halo/secrets/settings.yaml, then restart.
@@ -1487,7 +1596,7 @@ var config = {
1487
1596
  a2a: {
1488
1597
  /** Base dir workspaces are exposed relative to: env HALO_A2A_ROOT, else the user's home. */
1489
1598
  get root() {
1490
- return process.env.HALO_A2A_ROOT?.trim() || homedir5();
1599
+ return process.env.HALO_A2A_ROOT?.trim() || homedir6();
1491
1600
  },
1492
1601
  /** Server origin as peers see it (no trailing slash); '' = derive from the request. */
1493
1602
  get publicUrl() {
@@ -1498,16 +1607,10 @@ var config = {
1498
1607
  }
1499
1608
  },
1500
1609
  // Self-evolution (see plans/self-evolution.md). Getter-based so user edits
1501
- // in settings.yaml take effect on the next ticker run without restart.
1610
+ // in settings.yaml take effect on the next ticker run without restart. The
1611
+ // level and auto-apply threshold are per workspace: evoLevelFor /
1612
+ // autoApplyThresholdFor.
1502
1613
  evolution: {
1503
- get level() {
1504
- return parseEvoLevel(settingsStr("general.evolution.level", "L0"));
1505
- },
1506
- // Minimum average score for L2/L3 auto-apply (see evolution/auto-apply.ts
1507
- // for the extra fixed floors).
1508
- get autoApplyThreshold() {
1509
- return settingsInt("general.evolution.auto_apply_threshold", 80);
1510
- },
1511
1614
  get maxConcurrentRun() {
1512
1615
  return settingsInt("general.evolution.max_concurrent_run", 1);
1513
1616
  },
@@ -1538,21 +1641,21 @@ var config = {
1538
1641
  };
1539
1642
 
1540
1643
  // ../server/dist/evolution/auto-apply.js
1541
- import fs9 from "node:fs";
1542
- import path12 from "node:path";
1644
+ import fs10 from "node:fs";
1645
+ import path13 from "node:path";
1543
1646
  import YAML6 from "yaml";
1544
1647
  var AUTO_APPLY_FILE = "auto-apply.json";
1545
1648
  var OVERRIDE_DIRS = /* @__PURE__ */ new Set(["agents", "skills", "prompts"]);
1546
1649
  function isDir(p) {
1547
1650
  try {
1548
- return fs9.statSync(p).isDirectory();
1651
+ return fs10.statSync(p).isDirectory();
1549
1652
  } catch {
1550
1653
  return false;
1551
1654
  }
1552
1655
  }
1553
1656
  function readOrNull(p) {
1554
1657
  try {
1555
- return fs9.readFileSync(p);
1658
+ return fs10.readFileSync(p);
1556
1659
  } catch {
1557
1660
  return null;
1558
1661
  }
@@ -1560,33 +1663,33 @@ function readOrNull(p) {
1560
1663
  function walkFiles(root, rel, out = []) {
1561
1664
  let st;
1562
1665
  try {
1563
- st = fs9.statSync(path12.join(root, rel));
1666
+ st = fs10.statSync(path13.join(root, rel));
1564
1667
  } catch {
1565
1668
  return out;
1566
1669
  }
1567
1670
  if (st.isFile())
1568
1671
  out.push(rel);
1569
1672
  else if (st.isDirectory()) {
1570
- for (const e of fs9.readdirSync(path12.join(root, rel)))
1571
- walkFiles(root, path12.join(rel, e), out);
1673
+ for (const e of fs10.readdirSync(path13.join(root, rel)))
1674
+ walkFiles(root, path13.join(rel, e), out);
1572
1675
  }
1573
1676
  return out;
1574
1677
  }
1575
- var toPosix = (rel) => rel.split(path12.sep).join("/");
1678
+ var toPosix = (rel) => rel.split(path13.sep).join("/");
1576
1679
  function isInternalAgentDir(dir) {
1577
- const raw = readOrNull(path12.join(dir, "agent.yaml"));
1680
+ const raw = readOrNull(path13.join(dir, "agent.yaml"));
1578
1681
  return raw !== null && parseYamlLoose(raw.toString("utf-8"))?.internal === true;
1579
1682
  }
1580
1683
  function liveBaseline(dirs, rel) {
1581
- const parts = rel.split(path12.sep);
1684
+ const parts = rel.split(path13.sep);
1582
1685
  if (!OVERRIDE_DIRS.has(parts[0]) || parts.length < 3)
1583
- return { file: path12.join(dirs.mainHalo, rel), newDir: false };
1584
- const dirRel = path12.join(parts[0], parts[1]);
1585
- const internal = (root) => parts[0] === "agents" && isInternalAgentDir(path12.join(root, dirRel));
1586
- if (isDir(path12.join(dirs.mainHalo, dirRel)))
1587
- return { file: path12.join(dirs.mainHalo, rel), newDir: false, internalAgent: internal(dirs.mainHalo) };
1588
- if (isDir(path12.join(dirs.globalHalo, dirRel)))
1589
- return { file: path12.join(dirs.globalHalo, rel), newDir: false, globalDir: dirRel, internalAgent: internal(dirs.globalHalo) };
1686
+ return { file: path13.join(dirs.mainHalo, rel), newDir: false };
1687
+ const dirRel = path13.join(parts[0], parts[1]);
1688
+ const internal = (root) => parts[0] === "agents" && isInternalAgentDir(path13.join(root, dirRel));
1689
+ if (isDir(path13.join(dirs.mainHalo, dirRel)))
1690
+ return { file: path13.join(dirs.mainHalo, rel), newDir: false, internalAgent: internal(dirs.mainHalo) };
1691
+ if (isDir(path13.join(dirs.globalHalo, dirRel)))
1692
+ return { file: path13.join(dirs.globalHalo, rel), newDir: false, globalDir: dirRel, internalAgent: internal(dirs.globalHalo) };
1590
1693
  return { file: null, newDir: true };
1591
1694
  }
1592
1695
  function fileChange(rel, base, after) {
@@ -1608,15 +1711,15 @@ function publishChanges(dirs, files) {
1608
1711
  const base = liveBaseline(dirs, rel);
1609
1712
  if (base.globalDir)
1610
1713
  globalBacked.add(base.globalDir);
1611
- const c = fileChange(rel, base, fs9.readFileSync(full));
1714
+ const c = fileChange(rel, base, fs10.readFileSync(full));
1612
1715
  if (c)
1613
1716
  out.push(c);
1614
1717
  }
1615
1718
  for (const dirRel of globalBacked) {
1616
1719
  for (const rel of walkFiles(dirs.globalHalo, dirRel)) {
1617
- if (fs9.existsSync(path12.join(dirs.sandboxHalo, rel)))
1720
+ if (fs10.existsSync(path13.join(dirs.sandboxHalo, rel)))
1618
1721
  continue;
1619
- out.push({ rel: toPosix(rel), before: fs9.readFileSync(path12.join(dirs.globalHalo, rel), "utf-8"), after: null, newDir: false });
1722
+ out.push({ rel: toPosix(rel), before: fs10.readFileSync(path13.join(dirs.globalHalo, rel), "utf-8"), after: null, newDir: false });
1620
1723
  }
1621
1724
  }
1622
1725
  return out;
@@ -1631,21 +1734,21 @@ function collectChanges(dirs) {
1631
1734
  const base = liveBaseline(dirs, rel);
1632
1735
  if (base.globalDir)
1633
1736
  globalBacked.add(base.globalDir);
1634
- const c = fileChange(rel, base, fs9.readFileSync(path12.join(dirs.sandboxHalo, rel)));
1737
+ const c = fileChange(rel, base, fs10.readFileSync(path13.join(dirs.sandboxHalo, rel)));
1635
1738
  if (c)
1636
1739
  out.push(c);
1637
1740
  }
1638
1741
  for (const rel of walkFiles(dirs.mainHalo, top)) {
1639
1742
  if (seen.has(rel))
1640
1743
  continue;
1641
- out.push({ rel: toPosix(rel), before: fs9.readFileSync(path12.join(dirs.mainHalo, rel), "utf-8"), after: null, newDir: false });
1744
+ out.push({ rel: toPosix(rel), before: fs10.readFileSync(path13.join(dirs.mainHalo, rel), "utf-8"), after: null, newDir: false });
1642
1745
  }
1643
1746
  }
1644
1747
  for (const dirRel of globalBacked) {
1645
1748
  for (const rel of walkFiles(dirs.globalHalo, dirRel)) {
1646
1749
  if (seen.has(rel))
1647
1750
  continue;
1648
- out.push({ rel: toPosix(rel), before: fs9.readFileSync(path12.join(dirs.globalHalo, rel), "utf-8"), after: null, newDir: false });
1751
+ out.push({ rel: toPosix(rel), before: fs10.readFileSync(path13.join(dirs.globalHalo, rel), "utf-8"), after: null, newDir: false });
1649
1752
  }
1650
1753
  }
1651
1754
  return out;
@@ -1857,7 +1960,7 @@ function parseScore(raw) {
1857
1960
  }
1858
1961
  function readScore(file) {
1859
1962
  try {
1860
- return parseScore(JSON.parse(fs9.readFileSync(file, "utf-8")));
1963
+ return parseScore(JSON.parse(fs10.readFileSync(file, "utf-8")));
1861
1964
  } catch {
1862
1965
  return null;
1863
1966
  }
@@ -1877,11 +1980,8 @@ function categoryGate(level, cls) {
1877
1980
  return null;
1878
1981
  }
1879
1982
  function sourceGate(sourceAccess) {
1880
- if (sourceAccess === SOURCE_WEBHOOK)
1881
- return "source_webhook";
1882
1983
  return sourceAccess === "full" ? null : "source_access";
1883
1984
  }
1884
- var SOURCE_WEBHOOK = "webhook";
1885
1985
  function scoreGate(score, threshold) {
1886
1986
  if (!score)
1887
1987
  return "score_missing";
@@ -1939,7 +2039,7 @@ function extractLargerObservation(md) {
1939
2039
  }
1940
2040
  function readObservation(file) {
1941
2041
  try {
1942
- return extractLargerObservation(fs9.readFileSync(file, "utf-8"));
2042
+ return extractLargerObservation(fs10.readFileSync(file, "utf-8"));
1943
2043
  } catch {
1944
2044
  return null;
1945
2045
  }
@@ -2001,11 +2101,11 @@ function failApply(db, applyId, input) {
2001
2101
  }
2002
2102
 
2003
2103
  // ../server/dist/evolution/rollback.js
2004
- import fs10 from "node:fs";
2104
+ import fs11 from "node:fs";
2005
2105
  import { createHash } from "node:crypto";
2006
2106
  import { and as and2, eq as eq2, inArray } from "drizzle-orm";
2007
2107
  function sha256File(file) {
2008
- return createHash("sha256").update(fs10.readFileSync(file)).digest("hex");
2108
+ return createHash("sha256").update(fs11.readFileSync(file)).digest("hex");
2009
2109
  }
2010
2110
 
2011
2111
  // ../server/dist/evolution/evo-wrapper.js
@@ -2067,9 +2167,9 @@ function spawnProc(bin, args, logFd, teePath, stdinInput, timeoutSec) {
2067
2167
  }
2068
2168
  writeLog(logFd, `[wrapper] $ ${bin} ${loggedArgs.join(" ")}${stdinInput === void 0 ? " <last-arg-omitted>" : " <prompt-on-stdin>"}${timeoutSec ? ` (timeout ${timeoutSec}s)` : ""}
2069
2169
  `);
2070
- const tee = teePath ? fs11.createWriteStream(teePath, { flags: "a" }) : null;
2170
+ const tee = teePath ? fs12.createWriteStream(teePath, { flags: "a" }) : null;
2071
2171
  const [spawnBin, spawnArgs] = process.platform === "win32" && bin.endsWith(".cmd") ? ["cmd.exe", ["/c", bin, ...args]] : [bin, args];
2072
- return new Promise((resolve) => {
2172
+ return new Promise((resolve2) => {
2073
2173
  const child = spawn2(spawnBin, spawnArgs, {
2074
2174
  // Pipe stdin only when we have input to write — the cli reads a prompt
2075
2175
  // from stdin when it's not a TTY, which is how long briefs are delivered
@@ -2141,7 +2241,7 @@ function spawnProc(bin, args, logFd, teePath, stdinInput, timeoutSec) {
2141
2241
  tee?.end();
2142
2242
  if (stopping)
2143
2243
  return;
2144
- resolve({ exitCode, stdout, stderr });
2244
+ resolve2({ exitCode, stdout, stderr });
2145
2245
  });
2146
2246
  child.on("error", (err) => {
2147
2247
  liveProcs.delete(killTree);
@@ -2157,17 +2257,17 @@ function spawnProc(bin, args, logFd, teePath, stdinInput, timeoutSec) {
2157
2257
  tee?.end();
2158
2258
  if (stopping)
2159
2259
  return;
2160
- resolve({ exitCode: 1, stdout, stderr: stderr + `
2260
+ resolve2({ exitCode: 1, stdout, stderr: stderr + `
2161
2261
  [spawn error] ${err.message}` });
2162
2262
  });
2163
2263
  });
2164
2264
  }
2165
2265
  function subCliLogPath(runDir) {
2166
- return path13.join(runDir, "sub-cli.log");
2266
+ return path14.join(runDir, "sub-cli.log");
2167
2267
  }
2168
2268
  function appendSubCliHeader(runDir, header) {
2169
2269
  try {
2170
- fs11.appendFileSync(subCliLogPath(runDir), `
2270
+ fs12.appendFileSync(subCliLogPath(runDir), `
2171
2271
  === ${(/* @__PURE__ */ new Date()).toISOString()} ${header} ===
2172
2272
  `);
2173
2273
  } catch {
@@ -2201,10 +2301,10 @@ function buildLanguageClause(langHint, scope) {
2201
2301
  ];
2202
2302
  }
2203
2303
  function readPatchFrontmatter(runDir) {
2204
- const patchPath = path13.join(runDir, "patch.md");
2304
+ const patchPath = path14.join(runDir, "patch.md");
2205
2305
  let raw;
2206
2306
  try {
2207
- raw = fs11.readFileSync(patchPath, "utf-8");
2307
+ raw = fs12.readFileSync(patchPath, "utf-8");
2208
2308
  } catch {
2209
2309
  return null;
2210
2310
  }
@@ -2234,14 +2334,14 @@ function extractTestScenario(fm) {
2234
2334
  return { agentId: o.agentId, testMessage, originalMessage };
2235
2335
  }
2236
2336
  function readEvoContext(runDir, logFd) {
2237
- const p = path13.join(runDir, "evo-context.json");
2238
- if (!fs11.existsSync(p)) {
2337
+ const p = path14.join(runDir, "evo-context.json");
2338
+ if (!fs12.existsSync(p)) {
2239
2339
  writeLog(logFd, `[brief] evo-context.json missing at ${p}
2240
2340
  `);
2241
2341
  return null;
2242
2342
  }
2243
2343
  try {
2244
- return JSON.parse(fs11.readFileSync(p, "utf-8"));
2344
+ return JSON.parse(fs12.readFileSync(p, "utf-8"));
2245
2345
  } catch (err) {
2246
2346
  writeLog(logFd, `[brief] evo-context.json parse failed: ${err instanceof Error ? err.message : String(err)}
2247
2347
  `);
@@ -2249,12 +2349,12 @@ function readEvoContext(runDir, logFd) {
2249
2349
  }
2250
2350
  }
2251
2351
  function dumpSnapshotImages(runDir, logFd) {
2252
- const snapshotPath = path13.join(runDir, "source-snapshot.json");
2253
- if (!fs11.existsSync(snapshotPath))
2352
+ const snapshotPath = path14.join(runDir, "source-snapshot.json");
2353
+ if (!fs12.existsSync(snapshotPath))
2254
2354
  return [];
2255
2355
  let parsed;
2256
2356
  try {
2257
- parsed = JSON.parse(fs11.readFileSync(snapshotPath, "utf-8"));
2357
+ parsed = JSON.parse(fs12.readFileSync(snapshotPath, "utf-8"));
2258
2358
  } catch {
2259
2359
  writeLog(logFd, `[dumpSnapshotImages] snapshot parse failed
2260
2360
  `);
@@ -2263,8 +2363,8 @@ function dumpSnapshotImages(runDir, logFd) {
2263
2363
  const messages = Array.isArray(parsed.rawMessages) ? parsed.rawMessages : [];
2264
2364
  if (messages.length === 0)
2265
2365
  return [];
2266
- const imagesDir = path13.join(runDir, "images");
2267
- fs11.mkdirSync(imagesDir, { recursive: true });
2366
+ const imagesDir = path14.join(runDir, "images");
2367
+ fs12.mkdirSync(imagesDir, { recursive: true });
2268
2368
  const out = [];
2269
2369
  const extByMime = {
2270
2370
  "image/png": "png",
@@ -2303,10 +2403,10 @@ function dumpSnapshotImages(runDir, logFd) {
2303
2403
  continue;
2304
2404
  const ext = extByMime[mediaType] ?? "bin";
2305
2405
  const fileName = c.subBlockIdx == null ? `${m}-${b}.${ext}` : `${m}-${b}-${c.subBlockIdx}.${ext}`;
2306
- const fullPath = path13.join(imagesDir, fileName);
2406
+ const fullPath = path14.join(imagesDir, fileName);
2307
2407
  try {
2308
2408
  const buf = Buffer.from(data, "base64");
2309
- fs11.writeFileSync(fullPath, buf);
2409
+ fs12.writeFileSync(fullPath, buf);
2310
2410
  out.push({
2311
2411
  relPath: `images/${fileName}`,
2312
2412
  msgIdx: m,
@@ -2393,15 +2493,15 @@ function renderEvoContextSections(ctx) {
2393
2493
  lines.push("");
2394
2494
  }
2395
2495
  if (ctx.skills.length > 0) {
2396
- const globalSkillsDir = path13.join(homedir6(), ".halo", "global", "skills");
2496
+ const globalSkillsDir = path14.join(homedir7(), ".halo", "global", "skills");
2397
2497
  lines.push("=== Skills in this workspace ===");
2398
2498
  for (const s of ctx.skills)
2399
2499
  lines.push(` ${s.id} [${s.scope}]`);
2400
2500
  lines.push("");
2401
2501
  lines.push("To read a skill's content, file_read its SKILL.md (you have full");
2402
2502
  lines.push("file access \u2014 absolute paths resolve):");
2403
- lines.push(` [workspace] \u2192 ${path13.join("<workspace>", ".halo", "skills", "<id>", "SKILL.md")}`);
2404
- lines.push(` [global] / [builtin] \u2192 ${path13.join(globalSkillsDir, "<id>", "SKILL.md")}`);
2503
+ lines.push(` [workspace] \u2192 ${path14.join("<workspace>", ".halo", "skills", "<id>", "SKILL.md")}`);
2504
+ lines.push(` [global] / [builtin] \u2192 ${path14.join(globalSkillsDir, "<id>", "SKILL.md")}`);
2405
2505
  lines.push("A [global]/[builtin] skill is NOT inside your sandbox \u2014 read it");
2406
2506
  lines.push(`from ${globalSkillsDir} directly; don't glob the sandbox for it.`);
2407
2507
  lines.push("Sibling resource files live next to SKILL.md in that same dir.");
@@ -2424,13 +2524,13 @@ function buildDraftBrief(args) {
2424
2524
  ];
2425
2525
  if (args.userHint)
2426
2526
  lines.push(`Reviewer hint: ${args.userHint}`);
2427
- lines.push("", "The triggering agent's full message log lives on disk:", ` ${path13.join(args.runDir, "tool-flow.md")} \u2014 clipped, fast skim`, ` ${path13.join(args.runDir, "source-snapshot.json")} \u2014 full rawMessages`, "", "Read tool-flow.md first to understand what the user wanted, what the", "agent did, and where it diverged. Each tool_result is clipped to a", "~200-char peek (with the `is_error` flag when the tool failed) \u2014", "enough to see the *shape* of the flow without re-tokenizing large", "grep / shell outputs. Fall back to source-snapshot.json only for", "messages whose full tool result actually matters to your patch.", "", ...renderEvoContextSections(args.evoContext), ...renderImageManifest(args.images), "=== Non-text media note ===", "", "Inline images in source-snapshot.json are base64 \u2014 you can't read them", "directly. The `images/` dump", "(see manifest above) decodes them to disk so you can `view_image` if", "a specific image is load-bearing. Video, audio, and binary documents", "(PDF, etc.) are even more limited: at trigger time the source agent", "worked with them via shell tooling (ffmpeg, pdftotext, etc.), and your", "view is limited to the textual results those tools produced (visible", "inside source-snapshot.json as tool_result text).", "", "When the patch you have in mind requires understanding what's INSIDE", "the media file (a video's visual content, an audio's acoustic", "content, a PDF's layout), the dry-run test scenario can't reliably", "verify it \u2014 skipping the run is the right call. When the patch is", "about the shell-tooling pattern itself (how to call ffmpeg, how to", "grep its output, how to handle credential errors), it can be", "verified at the textual level and the run proceeds normally.", "", "=== Task ===", "", "Decide what change in the prompt surface above would help the next time", "a similar conversation comes up. Three shapes, in order of preference:", "rewrite an existing rule; tighten / reorganize what's there; or, only", "when neither applies, add a new rule.", "", `Sandbox path: ${evoSandboxDir(args.runDir)}`, "", `When you have a change worth making, write ${path13.join(args.runDir, "patch.md")}`, "(frontmatter + body) and one new file at", `${evoSandboxHaloDir(args.runDir)}/<target> with the`, "full new contents. AGENT.md describes the patch.md schema.", "", "When the conversation has no signal worth a patch (rule already covers,", "too short to learn from, agent did fine, depends on media content", `you can't see), write ${path13.join(args.runDir, ".skip.md")} with a`, "one or two sentence reason instead, and skip the rest.", "", ...buildLanguageClause(args.langHint, "all natural-language content (patch.md body, prose drafted into sandbox files, .skip.md body)"));
2527
+ lines.push("", "The triggering agent's full message log lives on disk:", ` ${path14.join(args.runDir, "tool-flow.md")} \u2014 clipped, fast skim`, ` ${path14.join(args.runDir, "source-snapshot.json")} \u2014 full rawMessages`, "", "Read tool-flow.md first to understand what the user wanted, what the", "agent did, and where it diverged. Each tool_result is clipped to a", "~200-char peek (with the `is_error` flag when the tool failed) \u2014", "enough to see the *shape* of the flow without re-tokenizing large", "grep / shell outputs. Fall back to source-snapshot.json only for", "messages whose full tool result actually matters to your patch.", "", ...renderEvoContextSections(args.evoContext), ...renderImageManifest(args.images), "=== Non-text media note ===", "", "Inline images in source-snapshot.json are base64 \u2014 you can't read them", "directly. The `images/` dump", "(see manifest above) decodes them to disk so you can `view_image` if", "a specific image is load-bearing. Video, audio, and binary documents", "(PDF, etc.) are even more limited: at trigger time the source agent", "worked with them via shell tooling (ffmpeg, pdftotext, etc.), and your", "view is limited to the textual results those tools produced (visible", "inside source-snapshot.json as tool_result text).", "", "When the patch you have in mind requires understanding what's INSIDE", "the media file (a video's visual content, an audio's acoustic", "content, a PDF's layout), the dry-run test scenario can't reliably", "verify it \u2014 skipping the run is the right call. When the patch is", "about the shell-tooling pattern itself (how to call ffmpeg, how to", "grep its output, how to handle credential errors), it can be", "verified at the textual level and the run proceeds normally.", "", "=== Task ===", "", "Decide what change in the prompt surface above would help the next time", "a similar conversation comes up. Three shapes, in order of preference:", "rewrite an existing rule; tighten / reorganize what's there; or, only", "when neither applies, add a new rule.", "", `Sandbox path: ${evoSandboxDir(args.runDir)}`, "", `When you have a change worth making, write ${path14.join(args.runDir, "patch.md")}`, "(frontmatter + body) and one new file at", `${evoSandboxHaloDir(args.runDir)}/<target> with the`, "full new contents. AGENT.md describes the patch.md schema.", "", "When the conversation has no signal worth a patch (rule already covers,", "too short to learn from, agent did fine, depends on media content", `you can't see), write ${path14.join(args.runDir, ".skip.md")} with a`, "one or two sentence reason instead, and skip the rest.", "", ...buildLanguageClause(args.langHint, "all natural-language content (patch.md body, prose drafted into sandbox files, .skip.md body)"));
2428
2528
  return lines.join("\n");
2429
2529
  }
2430
2530
  function buildFixBrief(args) {
2431
2531
  let failLogText = "";
2432
2532
  try {
2433
- failLogText = fs11.readFileSync(args.failLogPath, "utf-8");
2533
+ failLogText = fs12.readFileSync(args.failLogPath, "utf-8");
2434
2534
  } catch {
2435
2535
  failLogText = `(failed to read ${args.failLogPath})`;
2436
2536
  }
@@ -2445,7 +2545,7 @@ function buildFixBrief(args) {
2445
2545
  `Fix attempt: ${args.attempt}/${args.maxAttempts}`,
2446
2546
  "",
2447
2547
  `An earlier draft pass produced patch.md + a sandbox at`,
2448
- `${path13.join(args.runDir, "patch.md")} and`,
2548
+ `${path14.join(args.runDir, "patch.md")} and`,
2449
2549
  `${evoSandboxHaloDir(args.runDir)} respectively.`,
2450
2550
  "The dry-run failed; the wrapper's failure log:",
2451
2551
  "",
@@ -2466,16 +2566,16 @@ function buildFixBrief(args) {
2466
2566
  function buildScoreBrief(args) {
2467
2567
  function readOrEmpty(p, label) {
2468
2568
  try {
2469
- return fs11.readFileSync(p, "utf-8");
2569
+ return fs12.readFileSync(p, "utf-8");
2470
2570
  } catch (err) {
2471
2571
  writeLog(args.logFd, `[buildScoreBrief] ${label} read failed: ${err instanceof Error ? err.message : String(err)}
2472
2572
  `);
2473
2573
  return `(${label} unavailable)`;
2474
2574
  }
2475
2575
  }
2476
- const patchMd = readOrEmpty(path13.join(args.runDir, "patch.md"), "patch.md");
2477
- const dryRun = readOrEmpty(path13.join(args.runDir, "dry-run-output.txt"), "dry-run-output.txt");
2478
- const meta = readOrEmpty(path13.join(args.runDir, "meta.json"), "meta.json");
2576
+ const patchMd = readOrEmpty(path14.join(args.runDir, "patch.md"), "patch.md");
2577
+ const dryRun = readOrEmpty(path14.join(args.runDir, "dry-run-output.txt"), "dry-run-output.txt");
2578
+ const meta = readOrEmpty(path14.join(args.runDir, "meta.json"), "meta.json");
2479
2579
  return [
2480
2580
  "You are running as the Evolution scorer.",
2481
2581
  "",
@@ -2486,8 +2586,8 @@ function buildScoreBrief(args) {
2486
2586
  "write score.json with the absolute path given below)",
2487
2587
  "",
2488
2588
  `The original conversation lives on disk:`,
2489
- ` ${path13.join(args.runDir, "tool-flow.md")} \u2014 clipped, fast skim`,
2490
- ` ${path13.join(args.runDir, "source-snapshot.json")} \u2014 full rawMessages, when needed`,
2589
+ ` ${path14.join(args.runDir, "tool-flow.md")} \u2014 clipped, fast skim`,
2590
+ ` ${path14.join(args.runDir, "source-snapshot.json")} \u2014 full rawMessages, when needed`,
2491
2591
  "Read tool-flow.md first to find the user turn matching",
2492
2592
  "`testScenario.originalMessage` and the assistant reply that followed",
2493
2593
  "it. Fall back to source-snapshot.json only when the full tool result",
@@ -2537,7 +2637,7 @@ function buildScoreBrief(args) {
2537
2637
  "find the assistant turn that followed it in tool-flow.md (or",
2538
2638
  "source-snapshot.json) \u2014 that's your baseline. Compare baseline to",
2539
2639
  "dry-run-output.txt, rate lint / behavior / scope, write",
2540
- `${path13.join(args.runDir, "score.json")} with the shape AGENT.md describes.`,
2640
+ `${path14.join(args.runDir, "score.json")} with the shape AGENT.md describes.`,
2541
2641
  "",
2542
2642
  ...buildLanguageClause(args.langHint, 'the score.json "notes" field'),
2543
2643
  'The numeric fields and "confidence" enum stay in their canonical form.'
@@ -2580,14 +2680,14 @@ async function phaseDraft(args) {
2580
2680
  return outcome;
2581
2681
  }
2582
2682
  function judgeDraft(runDir, workspacePath, exitCode) {
2583
- if (fs11.existsSync(path13.join(runDir, ".skip.md"))) {
2683
+ if (fs12.existsSync(path14.join(runDir, ".skip.md"))) {
2584
2684
  return { outcome: "skipped", why: ".skip.md present \u2014 evo declared skip" };
2585
2685
  }
2586
2686
  if (exitCode !== 0) {
2587
2687
  const timeout = exitCode === 124 ? " (timeout)" : "";
2588
2688
  return { outcome: "failed", why: `drafter exited ${exitCode}${timeout} \u2014 draft interrupted, not trusted` };
2589
2689
  }
2590
- if (!fs11.existsSync(path13.join(runDir, "patch.md"))) {
2690
+ if (!fs12.existsSync(path14.join(runDir, "patch.md"))) {
2591
2691
  return { outcome: "failed", why: "patch.md missing" };
2592
2692
  }
2593
2693
  const target = readPatchFrontmatter(runDir)?.target;
@@ -2595,16 +2695,16 @@ function judgeDraft(runDir, workspacePath, exitCode) {
2595
2695
  return { outcome: "drafted", why: "patch.md present, no target in frontmatter \u2014 target file not checked" };
2596
2696
  }
2597
2697
  const sandboxDir = evoSandboxDir(runDir);
2598
- const rel = path13.relative(sandboxDir, path13.resolve(sandboxDir, target));
2599
- if (!rel.startsWith(".halo" + path13.sep)) {
2698
+ const rel = path14.relative(sandboxDir, path14.resolve(sandboxDir, target));
2699
+ if (!rel.startsWith(".halo" + path14.sep)) {
2600
2700
  return { outcome: "failed", why: `target ${JSON.stringify(target)} does not resolve under sandbox/.halo/` };
2601
2701
  }
2602
- const sandboxFile = path13.join(sandboxDir, rel);
2603
- if (!fs11.statSync(sandboxFile, { throwIfNoEntry: false })?.isFile()) {
2702
+ const sandboxFile = path14.join(sandboxDir, rel);
2703
+ if (!fs12.statSync(sandboxFile, { throwIfNoEntry: false })?.isFile()) {
2604
2704
  return { outcome: "failed", why: `target not written to sandbox: ${target}` };
2605
2705
  }
2606
- const wsFile = path13.join(workspacePath, rel);
2607
- if (fs11.statSync(wsFile, { throwIfNoEntry: false })?.isFile() && fs11.readFileSync(wsFile).equals(fs11.readFileSync(sandboxFile))) {
2706
+ const wsFile = path14.join(workspacePath, rel);
2707
+ if (fs12.statSync(wsFile, { throwIfNoEntry: false })?.isFile() && fs12.readFileSync(wsFile).equals(fs12.readFileSync(sandboxFile))) {
2608
2708
  return { outcome: "failed", why: `target unchanged from workspace: ${target}` };
2609
2709
  }
2610
2710
  return { outcome: "drafted", why: `patch.md + target ${target} present` };
@@ -2631,15 +2731,15 @@ async function phaseDryRun(args) {
2631
2731
  }
2632
2732
  async function tryDryRun(args, attemptIndex) {
2633
2733
  const sandboxDir = evoSandboxDir(args.runDir);
2634
- const outputPath = path13.join(args.runDir, "dry-run-output.txt");
2635
- const failLogPath = path13.join(args.runDir, `dry-run-fail-${attemptIndex}.log`);
2734
+ const outputPath = path14.join(args.runDir, "dry-run-output.txt");
2735
+ const failLogPath = path14.join(args.runDir, `dry-run-fail-${attemptIndex}.log`);
2636
2736
  const cli = resolveHaloCli();
2637
2737
  const fm = readPatchFrontmatter(args.runDir);
2638
2738
  const scenario = extractTestScenario(fm);
2639
2739
  if (!scenario) {
2640
2740
  writeLog(args.logFd, `[tryDryRun:${attemptIndex}] patch.md frontmatter missing testScenario.{agentId, testMessage}
2641
2741
  `);
2642
- fs11.writeFileSync(failLogPath, "patch.md frontmatter missing testScenario.{agentId, testMessage} \u2014 wrapper cannot run dry-run without it.\n", "utf-8");
2742
+ fs12.writeFileSync(failLogPath, "patch.md frontmatter missing testScenario.{agentId, testMessage} \u2014 wrapper cannot run dry-run without it.\n", "utf-8");
2643
2743
  return { kind: "no-scenario", failLogPath };
2644
2744
  }
2645
2745
  writeLog(args.logFd, `[tryDryRun:${attemptIndex}] agent=${scenario.agentId} testMessage=${JSON.stringify(scenario.testMessage).slice(0, 200)}
@@ -2657,14 +2757,14 @@ async function tryDryRun(args, attemptIndex) {
2657
2757
  const timedOut = result.exitCode === 124;
2658
2758
  const ok = result.exitCode === 0 && result.stdout.trim().length > 0;
2659
2759
  if (ok) {
2660
- fs11.writeFileSync(outputPath, result.stdout, "utf-8");
2760
+ fs12.writeFileSync(outputPath, result.stdout, "utf-8");
2661
2761
  writeLog(args.logFd, `[tryDryRun:${attemptIndex}] success: wrote ${outputPath} (${result.stdout.length}B)
2662
2762
  `);
2663
2763
  return { kind: "ok" };
2664
2764
  }
2665
2765
  const stdoutTail = result.stdout.length > 4e3 ? "\u2026" + result.stdout.slice(-4e3) : result.stdout;
2666
2766
  const stderrTail = result.stderr.length > 4e3 ? "\u2026" + result.stderr.slice(-4e3) : result.stderr;
2667
- fs11.writeFileSync(failLogPath, [
2767
+ fs12.writeFileSync(failLogPath, [
2668
2768
  `Wrapper command: halo cli -a ${scenario.agentId} -n -w ${sandboxDir} <testMessage>`,
2669
2769
  `Test message: ${scenario.testMessage}`,
2670
2770
  `Exit code: ${result.exitCode}${timedOut ? " (timeout)" : ""}`,
@@ -2720,7 +2820,7 @@ async function phaseScore(args) {
2720
2820
  const sandboxWs = evoSandboxDir(args.runDir);
2721
2821
  buildEvoSandbox(args.workspacePath, args.runDir, args.logFd);
2722
2822
  const cliArgs = ["cli", "-a", "__score__", "-n", "-w", sandboxWs];
2723
- fs11.rmSync(path13.join(args.runDir, "score.json"), { force: true });
2823
+ fs12.rmSync(path14.join(args.runDir, "score.json"), { force: true });
2724
2824
  appendSubCliHeader(args.runDir, "Phase C: __score__");
2725
2825
  const result = await spawnProc(
2726
2826
  resolveHaloCli(),
@@ -2741,7 +2841,7 @@ async function phaseScore(args) {
2741
2841
  writeLog(args.logFd, `[phaseScore] stdout (last 1KB): ${result.stdout.slice(-1e3)}
2742
2842
  `);
2743
2843
  }
2744
- const hasScore = fs11.existsSync(path13.join(args.runDir, "score.json"));
2844
+ const hasScore = fs12.existsSync(path14.join(args.runDir, "score.json"));
2745
2845
  writeLog(args.logFd, `[phaseScore] score.json=${hasScore}
2746
2846
  `);
2747
2847
  return hasScore;
@@ -2755,7 +2855,7 @@ var RUN_INPUT_FILES = /* @__PURE__ */ new Set([
2755
2855
  function resetRunArtifacts(runDir, logFd) {
2756
2856
  let entries;
2757
2857
  try {
2758
- entries = fs11.readdirSync(runDir);
2858
+ entries = fs12.readdirSync(runDir);
2759
2859
  } catch {
2760
2860
  return;
2761
2861
  }
@@ -2763,7 +2863,7 @@ function resetRunArtifacts(runDir, logFd) {
2763
2863
  if (RUN_INPUT_FILES.has(entry))
2764
2864
  continue;
2765
2865
  try {
2766
- fs11.rmSync(path13.join(runDir, entry), { recursive: true, force: true });
2866
+ fs12.rmSync(path14.join(runDir, entry), { recursive: true, force: true });
2767
2867
  } catch (err) {
2768
2868
  writeLog(logFd, `[resetRunArtifacts] failed to remove ${entry}: ${err instanceof Error ? err.message : String(err)}
2769
2869
  `);
@@ -2773,7 +2873,7 @@ function resetRunArtifacts(runDir, logFd) {
2773
2873
  async function runMode(row, logFd) {
2774
2874
  const { id } = row;
2775
2875
  const runDir = wsEvoRunDir(row.workspacePath, id);
2776
- fs11.mkdirSync(runDir, { recursive: true });
2876
+ fs12.mkdirSync(runDir, { recursive: true });
2777
2877
  resetRunArtifacts(runDir, logFd);
2778
2878
  const hb = setInterval(() => heartbeat("run", id, row.attempts, logFd), 6e4);
2779
2879
  const langHint = resolveLangHint();
@@ -2817,14 +2917,14 @@ async function runMode(row, logFd) {
2817
2917
  }
2818
2918
  }
2819
2919
  function markObservation(id, attempts, runDir, outcome, logFd) {
2820
- const has = outcome !== "failed" && readObservation(path13.join(runDir, outcome === "skipped" ? ".skip.md" : "patch.md")) !== null;
2920
+ const has = outcome !== "failed" && readObservation(path14.join(runDir, outcome === "skipped" ? ".skip.md" : "patch.md")) !== null;
2821
2921
  getEvoDb().update(evolutionRuns).set({ hasObservation: has }).where(ownsRun(id, attempts)).run();
2822
2922
  if (has)
2823
2923
  writeLog(logFd, `[wrapper] draft carries a Larger observation
2824
2924
  `);
2825
2925
  }
2826
2926
  function maybeAutoApply(args) {
2827
- const level = config.evolution.level;
2927
+ const level = evoLevelFor(args.workspacePath);
2828
2928
  if (evoLevelRank(level) < 2)
2829
2929
  return false;
2830
2930
  let record;
@@ -2835,12 +2935,12 @@ function maybeAutoApply(args) {
2835
2935
  sandboxHalo: evoSandboxHaloDir(args.runDir),
2836
2936
  entries: PUBLISH_WHITELIST,
2837
2937
  level,
2838
- threshold: config.evolution.autoApplyThreshold,
2938
+ threshold: autoApplyThresholdFor(args.workspacePath),
2839
2939
  sourceAccess: args.sourceAccess,
2840
- score: readScore(path13.join(args.runDir, "score.json")),
2841
- observation: readObservation(path13.join(args.runDir, "patch.md"))
2940
+ score: readScore(path14.join(args.runDir, "score.json")),
2941
+ observation: readObservation(path14.join(args.runDir, "patch.md"))
2842
2942
  });
2843
- fs11.writeFileSync(path13.join(args.runDir, AUTO_APPLY_FILE), JSON.stringify(record, null, 2), "utf-8");
2943
+ fs12.writeFileSync(path14.join(args.runDir, AUTO_APPLY_FILE), JSON.stringify(record, null, 2), "utf-8");
2844
2944
  } catch (err) {
2845
2945
  writeLog(args.logFd, `[autoApply] evaluation failed, leaving the run for review: ${err instanceof Error ? err.message : String(err)}
2846
2946
  `);
@@ -2938,14 +3038,14 @@ function loadApplyRow(id) {
2938
3038
  function buildEvoSandbox(workspacePath, sandboxParent, logFd) {
2939
3039
  const srcWs = wsHaloDir(workspacePath);
2940
3040
  const dstWs = evoSandboxHaloDir(sandboxParent);
2941
- fs11.mkdirSync(dstWs, { recursive: true });
3041
+ fs12.mkdirSync(dstWs, { recursive: true });
2942
3042
  const whitelist = SANDBOX_WHITELIST;
2943
3043
  for (const entry of whitelist) {
2944
- const srcEntry = path13.join(srcWs, entry);
2945
- if (!fs11.existsSync(srcEntry))
3044
+ const srcEntry = path14.join(srcWs, entry);
3045
+ if (!fs12.existsSync(srcEntry))
2946
3046
  continue;
2947
- const dstEntry = path13.join(dstWs, entry);
2948
- if (fs11.existsSync(dstEntry))
3047
+ const dstEntry = path14.join(dstWs, entry);
3048
+ if (fs12.existsSync(dstEntry))
2949
3049
  continue;
2950
3050
  copyDereferenced(srcEntry, dstEntry);
2951
3051
  }
@@ -2953,13 +3053,13 @@ function buildEvoSandbox(workspacePath, sandboxParent, logFd) {
2953
3053
  `);
2954
3054
  }
2955
3055
  function copyDereferenced(src, dst) {
2956
- if (fs11.statSync(src).isDirectory()) {
2957
- fs11.mkdirSync(dst, { recursive: true });
2958
- for (const entry of fs11.readdirSync(src))
2959
- copyDereferenced(path13.join(src, entry), path13.join(dst, entry));
3056
+ if (fs12.statSync(src).isDirectory()) {
3057
+ fs12.mkdirSync(dst, { recursive: true });
3058
+ for (const entry of fs12.readdirSync(src))
3059
+ copyDereferenced(path14.join(src, entry), path14.join(dst, entry));
2960
3060
  return;
2961
3061
  }
2962
- fs11.copyFileSync(src, dst);
3062
+ fs12.copyFileSync(src, dst);
2963
3063
  }
2964
3064
  function buildApplySandbox(workspacePath, applyDir, logFd) {
2965
3065
  buildEvoSandbox(workspacePath, applyDir, logFd);
@@ -2987,11 +3087,11 @@ function buildApplyMergeBrief(args) {
2987
3087
  "",
2988
3088
  "Hard rules: only edit files under sandbox/.halo/ \u2014 main workspace",
2989
3089
  "and ~/.halo/global/ are off-limits. Do NOT spawn halo cli. Do NOT",
2990
- `write score.json. Write ${path13.join(args.applyDir, "apply.log")}`,
3090
+ `write score.json. Write ${path14.join(args.applyDir, "apply.log")}`,
2991
3091
  "with a summary of what you did.",
2992
3092
  "",
2993
3093
  "If the merge cannot proceed (irreconcilable patch conflicts, malformed",
2994
- `input, missing sandbox), write ${path13.join(args.applyDir, "ABORT.md")}`,
3094
+ `input, missing sandbox), write ${path14.join(args.applyDir, "ABORT.md")}`,
2995
3095
  'with the diagnosis \u2014 the wrapper treats its presence as "abort this',
2996
3096
  'apply" and fails the apply instead of publishing. You cannot signal',
2997
3097
  "abort via exit code; ABORT.md is the only abort channel.",
@@ -3012,19 +3112,19 @@ function buildApplyScoreBrief(args) {
3012
3112
  "The apply agent has merged this patch (and possibly others) into",
3013
3113
  "an apply sandbox. The wrapper just ran the patch's testScenario",
3014
3114
  "against the merged sandbox; stdout is in",
3015
- `${path13.join(args.regressDir, "dry-run-output.txt")}.`,
3115
+ `${path14.join(args.regressDir, "dry-run-output.txt")}.`,
3016
3116
  "An empty dry-run-output.txt means the dry-run against the merged",
3017
3117
  "sandbox failed \u2014 score lint 0 (that is the regression signal the",
3018
3118
  `wrapper acts on); the failure detail is in`,
3019
- `${path13.join(args.regressDir, "dry-run-fail.log")} when present.`,
3119
+ `${path14.join(args.regressDir, "dry-run-fail.log")} when present.`,
3020
3120
  "",
3021
3121
  "Follow your AGENT.md procedure. Read:",
3022
3122
  ` patch.md \u2192 ${args.patchMdPath}`,
3023
- ` tool-flow.md \u2192 ${path13.join(path13.dirname(args.patchMdPath), "tool-flow.md")} (baseline conversation)`,
3123
+ ` tool-flow.md \u2192 ${path14.join(path14.dirname(args.patchMdPath), "tool-flow.md")} (baseline conversation)`,
3024
3124
  ` source-snapshot.json \u2192 ${args.snapshotPath}`,
3025
- ` dry-run output \u2192 ${path13.join(args.regressDir, "dry-run-output.txt")}`,
3125
+ ` dry-run output \u2192 ${path14.join(args.regressDir, "dry-run-output.txt")}`,
3026
3126
  "",
3027
- `Write ${path13.join(args.regressDir, "score.json")} \u2014 NOT the source`,
3127
+ `Write ${path14.join(args.regressDir, "score.json")} \u2014 NOT the source`,
3028
3128
  "run's score.json. Same format as run mode.",
3029
3129
  "Do not modify any other file.",
3030
3130
  ...buildLanguageClause(args.langHint, 'the score.json "notes" field'),
@@ -3036,9 +3136,9 @@ async function phaseApplyMerge(ctx) {
3036
3136
  === Phase A': merge ===
3037
3137
  `);
3038
3138
  buildApplySandbox(ctx.workspacePath, ctx.applyDir, ctx.logFd);
3039
- const abortPath = path13.join(ctx.applyDir, "ABORT.md");
3139
+ const abortPath = path14.join(ctx.applyDir, "ABORT.md");
3040
3140
  try {
3041
- fs11.rmSync(abortPath, { force: true });
3141
+ fs12.rmSync(abortPath, { force: true });
3042
3142
  } catch {
3043
3143
  }
3044
3144
  const brief = buildApplyMergeBrief(ctx);
@@ -3057,10 +3157,10 @@ async function phaseApplyMerge(ctx) {
3057
3157
  `);
3058
3158
  return { ok: false, reason: `apply cli exited ${result.exitCode}` };
3059
3159
  }
3060
- if (fs11.existsSync(abortPath)) {
3160
+ if (fs12.existsSync(abortPath)) {
3061
3161
  let abortText = "";
3062
3162
  try {
3063
- abortText = fs11.readFileSync(abortPath, "utf-8").trim();
3163
+ abortText = fs12.readFileSync(abortPath, "utf-8").trim();
3064
3164
  } catch {
3065
3165
  }
3066
3166
  writeLog(ctx.logFd, `[phaseApplyMerge] ABORT.md present \u2014 agent aborted the merge:
@@ -3069,7 +3169,7 @@ ${abortText}
3069
3169
  const firstLine = abortText.split("\n")[0]?.slice(0, 300) ?? "";
3070
3170
  return { ok: false, reason: `apply agent aborted: ${firstLine || "see ABORT.md in apply dir"}` };
3071
3171
  }
3072
- const hasLog = fs11.existsSync(path13.join(ctx.applyDir, "apply.log"));
3172
+ const hasLog = fs12.existsSync(path14.join(ctx.applyDir, "apply.log"));
3073
3173
  writeLog(ctx.logFd, `[phaseApplyMerge] apply.log=${hasLog}
3074
3174
  `);
3075
3175
  if (!hasLog)
@@ -3081,17 +3181,17 @@ async function phaseApplyRegress(ctx) {
3081
3181
  === Phase B': regress ===
3082
3182
  `);
3083
3183
  const sandboxDir = evoSandboxDir(ctx.applyDir);
3084
- fs11.mkdirSync(evoRegressDir(ctx.applyDir), { recursive: true });
3184
+ fs12.mkdirSync(evoRegressDir(ctx.applyDir), { recursive: true });
3085
3185
  const outcomes = [];
3086
3186
  for (const runId of ctx.sourceRunIds) {
3087
3187
  writeLog(ctx.logFd, `
3088
3188
  --- regress for source run ${runId} ---
3089
3189
  `);
3090
3190
  const regressDir = evoRegressRunDir(ctx.applyDir, runId);
3091
- fs11.mkdirSync(regressDir, { recursive: true });
3191
+ fs12.mkdirSync(regressDir, { recursive: true });
3092
3192
  const runDir = wsEvoRunDir(ctx.workspacePath, runId);
3093
- const patchMdPath = path13.join(runDir, "patch.md");
3094
- const snapshotPath = path13.join(runDir, "source-snapshot.json");
3193
+ const patchMdPath = path14.join(runDir, "patch.md");
3194
+ const snapshotPath = path14.join(runDir, "source-snapshot.json");
3095
3195
  const fm = readPatchFrontmatter(runDir);
3096
3196
  const scenario = extractTestScenario(fm);
3097
3197
  if (!scenario) {
@@ -3118,14 +3218,14 @@ async function phaseApplyRegress(ctx) {
3118
3218
  // stdin, not argv
3119
3219
  DRY_RUN_TIMEOUT_SEC
3120
3220
  );
3121
- const dryRunOut = path13.join(regressDir, "dry-run-output.txt");
3221
+ const dryRunOut = path14.join(regressDir, "dry-run-output.txt");
3122
3222
  if (dryRun.exitCode === 0 && dryRun.stdout.trim().length > 0) {
3123
- fs11.writeFileSync(dryRunOut, dryRun.stdout, "utf-8");
3223
+ fs12.writeFileSync(dryRunOut, dryRun.stdout, "utf-8");
3124
3224
  } else {
3125
3225
  const tail = dryRun.stdout.length > 4e3 ? "\u2026" + dryRun.stdout.slice(-4e3) : dryRun.stdout;
3126
3226
  const errTail = dryRun.stderr.length > 4e3 ? "\u2026" + dryRun.stderr.slice(-4e3) : dryRun.stderr;
3127
- fs11.writeFileSync(dryRunOut, "", "utf-8");
3128
- fs11.writeFileSync(path13.join(regressDir, "dry-run-fail.log"), [
3227
+ fs12.writeFileSync(dryRunOut, "", "utf-8");
3228
+ fs12.writeFileSync(path14.join(regressDir, "dry-run-fail.log"), [
3129
3229
  `Wrapper command: halo cli -a ${scenario.agentId} -n -w ${sandboxDir} <testMessage>`,
3130
3230
  `Test message: ${scenario.testMessage}`,
3131
3231
  `Exit code: ${dryRun.exitCode}${dryRun.exitCode === 124 ? " (timeout)" : ""}`,
@@ -3146,7 +3246,7 @@ async function phaseApplyRegress(ctx) {
3146
3246
  langHint: ctx.langHint
3147
3247
  });
3148
3248
  const scoreSandboxWs = evoSandboxDir(ctx.applyDir);
3149
- fs11.rmSync(path13.join(regressDir, "score.json"), { force: true });
3249
+ fs12.rmSync(path14.join(regressDir, "score.json"), { force: true });
3150
3250
  const scoreRun = await spawnProc(
3151
3251
  resolveHaloCli(),
3152
3252
  ["cli", "-a", "__score__", "-n", "-w", scoreSandboxWs],
@@ -3160,10 +3260,10 @@ async function phaseApplyRegress(ctx) {
3160
3260
  writeLog(ctx.logFd, `[phaseApplyRegress:${runId}] scorer cli exited ${scoreRun.exitCode}
3161
3261
  `);
3162
3262
  }
3163
- const scorePath = path13.join(regressDir, "score.json");
3263
+ const scorePath = path14.join(regressDir, "score.json");
3164
3264
  let scoreJson = null;
3165
3265
  try {
3166
- scoreJson = JSON.parse(fs11.readFileSync(scorePath, "utf-8"));
3266
+ scoreJson = JSON.parse(fs12.readFileSync(scorePath, "utf-8"));
3167
3267
  } catch {
3168
3268
  }
3169
3269
  if (!scoreJson) {
@@ -3217,13 +3317,13 @@ function autoApplyRecheck(workspacePath, applyDir, changed) {
3217
3317
  globalHalo: globalDir(),
3218
3318
  sandboxHalo: evoSandboxHaloDir(applyDir)
3219
3319
  }, changed));
3220
- return categoryGate(config.evolution.level, cls);
3320
+ return categoryGate(evoLevelFor(workspacePath), cls);
3221
3321
  }
3222
3322
  async function applyMode(id, logFd) {
3223
3323
  const row = loadApplyRow(id);
3224
3324
  const applyDir = wsEvoApplyDir(row.workspacePath, id);
3225
- fs11.mkdirSync(applyDir, { recursive: true });
3226
- fs11.writeFileSync(path13.join(applyDir, "meta.json"), JSON.stringify({
3325
+ fs12.mkdirSync(applyDir, { recursive: true });
3326
+ fs12.writeFileSync(path14.join(applyDir, "meta.json"), JSON.stringify({
3227
3327
  id,
3228
3328
  sourceRunIds: row.sourceRunIds,
3229
3329
  reviewerHint: row.reviewerHint,
@@ -3314,28 +3414,28 @@ async function phaseApplyPreflight(ctx, isResume) {
3314
3414
  `);
3315
3415
  const sandboxHalo = evoSandboxHaloDir(ctx.applyDir);
3316
3416
  const mainHalo = wsHaloDir(ctx.workspacePath);
3317
- if (!fs11.existsSync(sandboxHalo))
3417
+ if (!fs12.existsSync(sandboxHalo))
3318
3418
  return { ok: false, reason: "sandbox missing \u2014 phase 11 must run first" };
3319
3419
  const whitelistTop = new Set(PUBLISH_WHITELIST);
3320
3420
  const filesToConsider = [];
3321
- for (const top of fs11.readdirSync(sandboxHalo)) {
3421
+ for (const top of fs12.readdirSync(sandboxHalo)) {
3322
3422
  if (!whitelistTop.has(top))
3323
3423
  continue;
3324
- walkInto(path13.join(sandboxHalo, top), top, filesToConsider);
3424
+ walkInto(path14.join(sandboxHalo, top), top, filesToConsider);
3325
3425
  }
3326
3426
  const historyDir = wsEvoHistoryDir(ctx.workspacePath, ctx.applyId);
3327
3427
  const changed = [];
3328
3428
  for (const { rel, full } of filesToConsider) {
3329
- const mainPath = path13.join(mainHalo, rel);
3429
+ const mainPath = path14.join(mainHalo, rel);
3330
3430
  let existsInMain = false;
3331
3431
  try {
3332
- fs11.accessSync(mainPath);
3432
+ fs12.accessSync(mainPath);
3333
3433
  existsInMain = true;
3334
3434
  } catch {
3335
3435
  }
3336
3436
  if (existsInMain) {
3337
- const sandboxBuf = fs11.readFileSync(full);
3338
- const mainBuf = fs11.readFileSync(mainPath);
3437
+ const sandboxBuf = fs12.readFileSync(full);
3438
+ const mainBuf = fs12.readFileSync(mainPath);
3339
3439
  if (sandboxBuf.equals(mainBuf))
3340
3440
  continue;
3341
3441
  }
@@ -3344,20 +3444,20 @@ async function phaseApplyPreflight(ctx, isResume) {
3344
3444
  writeLog(ctx.logFd, `[preflight] ${changed.length} of ${filesToConsider.length} whitelisted files differ from main
3345
3445
  `);
3346
3446
  if (!isResume)
3347
- fs11.rmSync(historyDir, { recursive: true, force: true });
3447
+ fs12.rmSync(historyDir, { recursive: true, force: true });
3348
3448
  if (changed.length === 0) {
3349
3449
  return { ok: true, result: { changed, historyDir, fileCount: 0 } };
3350
3450
  }
3351
- fs11.mkdirSync(historyDir, { recursive: true });
3352
- const manifestPath = path13.join(historyDir, "MANIFEST.json");
3451
+ fs12.mkdirSync(historyDir, { recursive: true });
3452
+ const manifestPath = path14.join(historyDir, "MANIFEST.json");
3353
3453
  if (!isResume) {
3354
3454
  for (const { rel, existsInMain } of changed) {
3355
3455
  if (!existsInMain)
3356
3456
  continue;
3357
- const mainPath = path13.join(mainHalo, rel);
3358
- const histPath = path13.join(historyDir, rel);
3359
- fs11.mkdirSync(path13.dirname(histPath), { recursive: true });
3360
- fs11.copyFileSync(mainPath, histPath);
3457
+ const mainPath = path14.join(mainHalo, rel);
3458
+ const histPath = path14.join(historyDir, rel);
3459
+ fs12.mkdirSync(path14.dirname(histPath), { recursive: true });
3460
+ fs12.copyFileSync(mainPath, histPath);
3361
3461
  }
3362
3462
  const effective = new Set(publishChanges({ mainHalo, globalHalo: globalDir(), sandboxHalo }, changed).map((c) => c.rel));
3363
3463
  const manifest = {
@@ -3367,13 +3467,13 @@ async function phaseApplyPreflight(ctx, isResume) {
3367
3467
  files: changed.map((c) => ({
3368
3468
  rel: c.rel,
3369
3469
  kind: c.existsInMain ? "overwrite" : "new",
3370
- before: c.existsInMain ? sha256File(path13.join(mainHalo, c.rel)) : null,
3470
+ before: c.existsInMain ? sha256File(path14.join(mainHalo, c.rel)) : null,
3371
3471
  after: sha256File(c.full),
3372
- ...effective.has(c.rel.split(path13.sep).join("/")) ? {} : { unchanged: true }
3472
+ ...effective.has(c.rel.split(path14.sep).join("/")) ? {} : { unchanged: true }
3373
3473
  })),
3374
3474
  createdDirs: dirsCreatedBy(mainHalo, changed.filter((c) => !c.existsInMain).map((c) => c.rel))
3375
3475
  };
3376
- fs11.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), "utf-8");
3476
+ fs12.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), "utf-8");
3377
3477
  } else {
3378
3478
  writeLog(ctx.logFd, `[preflight] resume path, skipping history backup (the pre-apply state is already preserved from the fresh claim's preflight)
3379
3479
  `);
@@ -3388,9 +3488,9 @@ async function phaseApplyPublish(ctx, preflight) {
3388
3488
  `);
3389
3489
  const mainHalo = wsHaloDir(ctx.workspacePath);
3390
3490
  for (const { rel, full } of preflight.changed) {
3391
- const mainPath = path13.join(mainHalo, rel);
3392
- fs11.mkdirSync(path13.dirname(mainPath), { recursive: true });
3393
- fs11.copyFileSync(full, mainPath);
3491
+ const mainPath = path14.join(mainHalo, rel);
3492
+ fs12.mkdirSync(path14.dirname(mainPath), { recursive: true });
3493
+ fs12.copyFileSync(full, mainPath);
3394
3494
  }
3395
3495
  writeLog(ctx.logFd, `[publish] copied ${preflight.changed.length} files to ${mainHalo}
3396
3496
  `);
@@ -3398,10 +3498,10 @@ async function phaseApplyPublish(ctx, preflight) {
3398
3498
  function dirsCreatedBy(mainHalo, newFiles) {
3399
3499
  const dirs = /* @__PURE__ */ new Set();
3400
3500
  for (const rel of newFiles) {
3401
- for (let d = path13.dirname(rel); d !== "." && !fs11.existsSync(path13.join(mainHalo, d)); d = path13.dirname(d))
3501
+ for (let d = path14.dirname(rel); d !== "." && !fs12.existsSync(path14.join(mainHalo, d)); d = path14.dirname(d))
3402
3502
  dirs.add(d);
3403
3503
  }
3404
- return [...dirs].sort((a, b) => b.split(path13.sep).length - a.split(path13.sep).length || (a < b ? -1 : 1));
3504
+ return [...dirs].sort((a, b) => b.split(path14.sep).length - a.split(path14.sep).length || (a < b ? -1 : 1));
3405
3505
  }
3406
3506
  function ownsApply(id, status, attempts) {
3407
3507
  return and3(eq3(evolutionApplies.id, id), eq3(evolutionApplies.status, status), eq3(evolutionApplies.attempts, attempts));
@@ -3427,15 +3527,15 @@ function releaseSyncing(applyId, attempts, reason, logFd) {
3427
3527
  `);
3428
3528
  }
3429
3529
  function walkInto(absPath, relPath, out) {
3430
- const stat = fs11.statSync(absPath);
3530
+ const stat = fs12.statSync(absPath);
3431
3531
  if (stat.isFile()) {
3432
3532
  out.push({ rel: relPath, full: absPath });
3433
3533
  return;
3434
3534
  }
3435
3535
  if (!stat.isDirectory())
3436
3536
  return;
3437
- for (const entry of fs11.readdirSync(absPath)) {
3438
- walkInto(path13.join(absPath, entry), path13.join(relPath, entry), out);
3537
+ for (const entry of fs12.readdirSync(absPath)) {
3538
+ walkInto(path14.join(absPath, entry), path14.join(relPath, entry), out);
3439
3539
  }
3440
3540
  }
3441
3541
  function finalizeApplied(applyId, attempts, sourceRunIds, logFd) {
@@ -3454,19 +3554,19 @@ function finalizeApplied(applyId, attempts, sourceRunIds, logFd) {
3454
3554
  }
3455
3555
  function writeLog(fd, msg) {
3456
3556
  try {
3457
- fs11.writeSync(fd, msg);
3557
+ fs12.writeSync(fd, msg);
3458
3558
  } catch {
3459
3559
  }
3460
3560
  }
3461
3561
  function openLogFor(mode, id) {
3462
- fs11.mkdirSync(evoLogsDir(), { recursive: true });
3562
+ fs12.mkdirSync(evoLogsDir(), { recursive: true });
3463
3563
  const logPath = mode === "run" ? evoWrapperLogFile(id) : evoApplyLogFile(id);
3464
- const fd = fs11.openSync(logPath, "a");
3564
+ const fd = fs12.openSync(logPath, "a");
3465
3565
  return { fd, logPath };
3466
3566
  }
3467
3567
  async function main() {
3468
3568
  const { mode, id } = parseCli(process.argv.slice(2));
3469
- setEvoDb(createEvoDb(path13.join(homedir6(), ".halo", "global")));
3569
+ setEvoDb(createEvoDb(path14.join(homedir7(), ".halo", "global")));
3470
3570
  const { fd, logPath } = openLogFor(mode, id);
3471
3571
  writeLog(fd, `
3472
3572
  === ${(/* @__PURE__ */ new Date()).toISOString()} wrapper start mode=${mode} id=${id} pid=${process.pid} ===
@@ -3494,7 +3594,7 @@ async function main() {
3494
3594
  process.exit(1);
3495
3595
  }
3496
3596
  }
3497
- if (process.argv[1] && path13.resolve(process.argv[1]) === fileURLToPath2(import.meta.url)) {
3597
+ if (process.argv[1] && path14.resolve(process.argv[1]) === fileURLToPath2(import.meta.url)) {
3498
3598
  main().catch((err) => {
3499
3599
  process.stderr.write(`[wrapper] startup error: ${err instanceof Error ? err.message : String(err)}
3500
3600
  `);