@4fu/arbor-environment-dir 2026.1106.0 → 2026.1107.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -27,9 +27,11 @@ everything that takes judgment, such as git, merges and conflicts, to the agent.
27
27
  guarantees what an agent cannot guarantee for itself - the record, isolation,
28
28
  exactly-once effects, the checks, dispatch. Everything that takes judgment,
29
29
  such as git, merges and conflicts, is the agent's.
30
- - **A directory per window.** Each window gets its own directory, and a
31
- continuation keeps working in its predecessor's directory in place. It is not
32
- bound to a repository; the agent clones and pushes what it needs.
30
+ - **A shared workspace with per-window checkouts.** The default workspace is
31
+ ovnfs: one shared folder every window reads, and a window opens its own
32
+ copy-on-write checkout of the project it needs, works there and merges back
33
+ with `apply`. It falls back to a plain directory per window when ovnfs is not
34
+ installed. It is not a sandbox: ovnfs isolates files, not processes.
33
35
  - **A self-changing environment.** The model adds and changes skills, MCP
34
36
  servers and resident services itself; a `reload` validates the change and
35
37
  broadcasts it to every live window as a delta of names and descriptions.
@@ -54,7 +56,9 @@ npm install @4fu/arbor-core @4fu/arbor-journal-sqlite @4fu/arbor-notes-sqlite \
54
56
  ```
55
57
 
56
58
  `@4fu/arbor-plugin-http` is optional; it is the example plugin below.
57
- `@4fu/arbor-segmenter-jieba` is an optional peer of the journal.
59
+ `@4fu/arbor-segmenter-jieba` is an optional peer of the lexical package, and
60
+ `@4fu/arbor-search-zvec` an optional peer of the journal: absent either one, the
61
+ journal searches lexically and nothing else changes.
58
62
 
59
63
  ## A minimal host
60
64
 
@@ -118,7 +122,10 @@ window gets, and how a continuation keeps working in it, are in
118
122
  | [`@4fu/arbor-journal-sqlite`](packages/journal-sqlite) | the default `Journal` and `BlobStore` on SQLite |
119
123
  | [`@4fu/arbor-notes-sqlite`](packages/notes-sqlite) | the default `NoteStore` on SQLite, beside the journal |
120
124
  | [`@4fu/arbor-segmenter-jieba`](packages/segmenter-jieba) | the default `Segmenter`: jieba's dictionary segmentation for the word index, optional |
121
- | [`@4fu/arbor-workspace-dir`](packages/workspace-dir) | the default `Workspace`: one directory per window, with a continuation working in its predecessor's directory in place |
125
+ | [`@4fu/arbor-lexical`](packages/lexical) | the lexical search the journal and notes share: word segmentation, the FTS5 helpers and the merge order |
126
+ | [`@4fu/arbor-search-zvec`](packages/search-zvec) | the optional semantic channel: event text embedded and searched by meaning with zvec |
127
+ | [`@4fu/arbor-workspace-dir`](packages/workspace-dir) | the fallback `Workspace`: one directory per window, with a continuation working in its predecessor's directory in place |
128
+ | [`@4fu/arbor-workspace-ovnfs`](packages/workspace-ovnfs) | the default `Workspace` when ovnfs is installed: one shared folder, per-window copy-on-write checkouts the model opens and applies back |
122
129
  | [`@4fu/arbor-environment-dir`](packages/environment-dir) | the default `Environment`: one `/env` directory, a revision per item, validation and deltas |
123
130
  | [`@4fu/arbor-supervisor-process`](packages/supervisor-process) | the default `Supervisor`: one host process per service, restart with backoff, logs |
124
131
  | [`@4fu/arbor-judge-jev`](packages/judge-jev) | the default `Judge`: Jev, TypeSafe AI's System One classifier, answering the dispatch decisions |
@@ -151,7 +158,7 @@ deployment can see what it would be taking on before it adopts one.
151
158
  | [docs/coordination.md](docs/coordination.md) | the board, expectations, deliveries and broadcasts, staying current, effects |
152
159
  | [docs/environment.md](docs/environment.md) | skills, MCP servers and services: using, changing, reload, built-in skills, deltas |
153
160
  | [docs/memory.md](docs/memory.md) | notes, the active set, anchors, links |
154
- | [docs/workspaces.md](docs/workspaces.md) | per-window directories: isolation, continuity, what the agent does |
161
+ | [docs/workspaces.md](docs/workspaces.md) | the shared folder and per-window checkouts: isolation, continuity, what the agent does |
155
162
  | [docs/tools.md](docs/tools.md) | direct tools and code mode modules |
156
163
  | [docs/prompting.md](docs/prompting.md) | how every model-facing text is written: layers, notes, deliveries, errors, judge questions, terms |
157
164
  | [docs/release.md](docs/release.md) | calendar versioning, the manual release steps, how a package is published |
@@ -168,6 +175,7 @@ tarball would contain ([docs/release.md](docs/release.md)).
168
175
  | `packages/errors` | the error shape every refusal uses: the rule and the next action |
169
176
  | `packages/journal-sqlite` | the default `Journal` and `BlobStore` on SQLite |
170
177
  | `packages/segmenter-jieba` | the default `Segmenter`: jieba's dictionary segmentation for the word index, optional |
178
+ | `packages/lexical` | the lexical search the journal and notes share: word segmentation, the FTS5 helpers and the merge order |
171
179
  | `packages/notes-sqlite` | the default `NoteStore` on SQLite, beside the journal |
172
180
  | `packages/judge-jev` | the default `Judge`: Jev, TypeSafe AI's System One classifier, answering the dispatch decisions |
173
181
  | `packages/workspace-dir` | the default `Workspace`: one directory per window, with a continuation working in its predecessor's directory in place, and the lookup of another window's directory it contributes to the catalog |
@@ -1,6 +1,6 @@
1
1
  /**
2
- * The default `Environment`: one shared directory, mounted read-only into every
3
- * window at `/env` (docs/environment.md).
2
+ * The default `Environment`: one shared directory, read-only for every window
3
+ * at `/env` (docs/environment.md).
4
4
  *
5
5
  * ```text
6
6
  * <root>/env/skills/<name>/ a skill, with its scripts beside it
@@ -34,6 +34,22 @@ export interface DirectoryEnvironmentOptions {
34
34
  supervisor?: Supervisor;
35
35
  /** Where a secret's value comes from. A configuration names one, never holds it. */
36
36
  secretOf?: (name: string) => string | undefined;
37
+ /**
38
+ * One fixed string from the deployment's text table, so this implementation
39
+ * writes none of its own (docs/prompting.md#language-and-the-text-table).
40
+ * Defaults to the framework's own Chinese table.
41
+ */
42
+ text?: (key: string, args?: Record<string, string | number>) => string;
43
+ /**
44
+ * Extra built-in skills to seed read-only on first use, each a name and the
45
+ * directory its `SKILL.md` lives in. The workspace implementation contributes
46
+ * its own this way - the ovnfs workspace seeds the `ovnfs` skill
47
+ * (docs/workspaces.md).
48
+ */
49
+ builtinSkills?: readonly {
50
+ name: string;
51
+ source: string;
52
+ }[];
37
53
  /** The clock. Tests pass a fixed one. */
38
54
  now?: () => number;
39
55
  }
@@ -45,7 +61,11 @@ export declare class DirectoryEnvironment implements Environment {
45
61
  private readonly root;
46
62
  private readonly supervisor;
47
63
  private readonly secretOf;
64
+ /** The table every refusal this implementation raises is written from. */
65
+ private readonly text;
48
66
  private readonly clock;
67
+ /** Extra built-in skills an implementation seeds, name and source directory. */
68
+ private readonly extraSkills;
49
69
  private index;
50
70
  /** Every environment revision, oldest first. */
51
71
  private log;
@@ -1,6 +1,6 @@
1
1
  /**
2
- * The default `Environment`: one shared directory, mounted read-only into every
3
- * window at `/env` (docs/environment.md).
2
+ * The default `Environment`: one shared directory, read-only for every window
3
+ * at `/env` (docs/environment.md).
4
4
  *
5
5
  * ```text
6
6
  * <root>/env/skills/<name>/ a skill, with its scripts beside it
@@ -25,7 +25,8 @@ import { dirname, isAbsolute, join, resolve } from "node:path";
25
25
  import { fileURLToPath } from "node:url";
26
26
  import { arborError } from "@4fu/arbor-errors";
27
27
  import { StdioMcpServer } from "./mcp.js";
28
- /** What a window reads the environment at, which is mounted read-only. */
28
+ import { textOf, zhTextTable } from "@4fu/arbor-prompt";
29
+ /** What a window reads the environment at, which is read-only for it. */
29
30
  const ENV_PREFIX = "/env";
30
31
  /** The built-in skills, seeded read-only on first use. */
31
32
  export const BUILTIN_SKILLS = ["arbor-skill", "arbor-mcp", "arbor-service", "arbor-settings"];
@@ -66,7 +67,11 @@ export class DirectoryEnvironment {
66
67
  root;
67
68
  supervisor;
68
69
  secretOf;
70
+ /** The table every refusal this implementation raises is written from. */
71
+ text;
69
72
  clock;
73
+ /** Extra built-in skills an implementation seeds, name and source directory. */
74
+ extraSkills;
70
75
  index = { revision: 0, items: [] };
71
76
  /** Every environment revision, oldest first. */
72
77
  log = [];
@@ -77,7 +82,9 @@ export class DirectoryEnvironment {
77
82
  this.directory = join(options.root, "env");
78
83
  this.supervisor = options.supervisor;
79
84
  this.secretOf = options.secretOf;
85
+ this.text = options.text ?? ((key, args) => textOf(zhTextTable, key, args));
80
86
  this.clock = options.now ?? (() => Date.now());
87
+ this.extraSkills = options.builtinSkills ?? [];
81
88
  }
82
89
  async revision() {
83
90
  return `e${(await this.read()).revision}`;
@@ -120,8 +127,8 @@ export class DirectoryEnvironment {
120
127
  throw arborError({
121
128
  code: "invalid_input",
122
129
  rule: "env.empty",
123
- message: "reload:install 和 remove 至少要给一个",
124
- nextAction: "给出 install 或 remove 之一。",
130
+ message: this.text("refused.env.empty"),
131
+ nextAction: this.text("next.env.empty"),
125
132
  });
126
133
  }
127
134
  // Everything is staged and checked before anything is replaced, so a refusal
@@ -228,9 +235,9 @@ export class DirectoryEnvironment {
228
235
  if (entry === undefined) {
229
236
  throw arborError({
230
237
  code: "not_found",
231
- rule: "env.notfound",
232
- message: `环境:没有 MCP 服务器 ${server}`,
233
- nextAction: "用 env.list 查看当前安装的服务器。",
238
+ rule: "env.mcp.notfound",
239
+ message: this.text("refused.env.mcp.notfound", { name: server }),
240
+ nextAction: this.text("next.env.mcp.notfound"),
234
241
  details: { server },
235
242
  });
236
243
  }
@@ -239,8 +246,8 @@ export class DirectoryEnvironment {
239
246
  throw arborError({
240
247
  code: "unavailable",
241
248
  rule: "mcp.url.unsupported",
242
- message: `环境:MCP 服务器 ${server} 是一个远程地址,本实现只调用 stdio 服务器`,
243
- nextAction: "把该服务器换成本地 command 形式,或给主机换一个 Environment 实现。",
249
+ message: this.text("refused.mcp.url.unsupported", { name: server }),
250
+ nextAction: this.text("next.mcp.url.unsupported"),
244
251
  details: { server },
245
252
  });
246
253
  }
@@ -278,8 +285,8 @@ export class DirectoryEnvironment {
278
285
  throw arborError({
279
286
  code: "not_found",
280
287
  rule: "env.draft.missing",
281
- message: `环境:草稿 ${draft.path} 不存在`,
282
- nextAction: "用 workspace.ls 查看工作区里的草稿路径。",
288
+ message: this.text("refused.env.draft.missing", { path: draft.path }),
289
+ nextAction: this.text("next.env.draft.missing"),
283
290
  details: { path: draft.path },
284
291
  });
285
292
  }
@@ -331,7 +338,7 @@ export class DirectoryEnvironment {
331
338
  return { ok: true, value: await this.checkService(draft) };
332
339
  }
333
340
  catch (error) {
334
- return { ok: false, error: errorInfo(error, draft) };
341
+ return { ok: false, error: errorInfo(this.text, error, draft) };
335
342
  }
336
343
  }
337
344
  /** A skill parses: a title, then one line that says what it is for. */
@@ -341,8 +348,8 @@ export class DirectoryEnvironment {
341
348
  throw arborError({
342
349
  code: "invalid_input",
343
350
  rule: "env.draft.skill",
344
- message: `环境:草稿 ${draft.name} 中没有 SKILL.md`,
345
- nextAction: "读 /env/skills/arbor-skill/SKILL.md 的「结构」一节,按它补上 SKILL.md。",
351
+ message: this.text("refused.env.draft.skill", { name: draft.name }),
352
+ nextAction: this.text("next.env.draft.skill"),
346
353
  details: { name: draft.name },
347
354
  });
348
355
  }
@@ -350,9 +357,9 @@ export class DirectoryEnvironment {
350
357
  if (lines[0]?.startsWith("# ") !== true) {
351
358
  throw arborError({
352
359
  code: "invalid_input",
353
- rule: "env.draft.skill",
354
- message: `环境:技能 ${draft.name} 的第一行不是一级标题`,
355
- nextAction: "读 /env/skills/arbor-skill/SKILL.md 的「结构」一节,把第一行写成 # 标题。",
360
+ rule: "env.draft.skill.title",
361
+ message: this.text("refused.env.draft.skill.title", { name: draft.name }),
362
+ nextAction: this.text("next.env.draft.skill.title"),
356
363
  details: { name: draft.name },
357
364
  });
358
365
  }
@@ -360,9 +367,9 @@ export class DirectoryEnvironment {
360
367
  if (description === "") {
361
368
  throw arborError({
362
369
  code: "invalid_input",
363
- rule: "env.draft.skill",
364
- message: `环境:技能 ${draft.name} 没有一行说明`,
365
- nextAction: "读 /env/skills/arbor-skill/SKILL.md 的「结构」一节,在标题下写一行说明。",
370
+ rule: "env.draft.skill.line",
371
+ message: this.text("refused.env.draft.skill.line", { name: draft.name }),
372
+ nextAction: this.text("next.env.draft.skill.line"),
366
373
  details: { name: draft.name },
367
374
  });
368
375
  }
@@ -374,9 +381,9 @@ export class DirectoryEnvironment {
374
381
  if (text === null) {
375
382
  throw arborError({
376
383
  code: "invalid_input",
377
- rule: "env.draft.settings",
378
- message: `环境:草稿 ${draft.path} 中没有 ${draft.name}.json`,
379
- nextAction: "读 /env/skills/arbor-settings/SKILL.md 的「结构」一节,按它写设置文件。",
384
+ rule: "env.draft.settings.missing",
385
+ message: this.text("refused.env.draft.settings.missing", { path: draft.path, name: draft.name }),
386
+ nextAction: this.text("next.env.draft.settings.missing"),
380
387
  details: { name: draft.name },
381
388
  });
382
389
  }
@@ -388,16 +395,18 @@ export class DirectoryEnvironment {
388
395
  throw arborError({
389
396
  code: "invalid_input",
390
397
  rule: "env.draft.settings",
391
- message: `环境:设置文件解析失败:${error instanceof Error ? error.message : String(error)}`,
392
- nextAction: "读 /env/skills/arbor-settings/SKILL.md 的「结构」一节,修正 JSON 后重新调用 reload。",
398
+ message: this.text("refused.env.draft.settings", {
399
+ message: error instanceof Error ? error.message : String(error),
400
+ }),
401
+ nextAction: this.text("next.env.draft.settings"),
393
402
  });
394
403
  }
395
404
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
396
405
  throw arborError({
397
406
  code: "invalid_input",
398
- rule: "env.draft.settings",
399
- message: "环境:设置文件必须是一个 JSON 对象",
400
- nextAction: "读 /env/skills/arbor-settings/SKILL.md 的「结构」一节,按字段写成一个对象。",
407
+ rule: "env.draft.settings.shape",
408
+ message: this.text("refused.env.draft.settings.shape"),
409
+ nextAction: this.text("next.env.draft.settings.shape"),
401
410
  });
402
411
  }
403
412
  }
@@ -412,8 +421,8 @@ export class DirectoryEnvironment {
412
421
  throw arborError({
413
422
  code: "invalid_input",
414
423
  rule: "env.draft.mcp",
415
- message: `环境:MCP 服务器 ${draft.name} 的配置缺失或不是 JSON`,
416
- nextAction: "读 /env/skills/arbor-mcp/SKILL.md 的「结构」一节,按它写配置文件。",
424
+ message: this.text("refused.env.draft.mcp", { name: draft.name }),
425
+ nextAction: this.text("next.env.draft.mcp"),
417
426
  details: { name: draft.name },
418
427
  });
419
428
  }
@@ -422,9 +431,9 @@ export class DirectoryEnvironment {
422
431
  if (command === "" && stringField(config, "url") === "") {
423
432
  throw arborError({
424
433
  code: "invalid_input",
425
- rule: "env.draft.mcp",
426
- message: `环境:MCP 服务器 ${draft.name} 的配置中没有 command 或 url`,
427
- nextAction: "读 /env/skills/arbor-mcp/SKILL.md 的「结构」一节,补上 command 或 url。",
434
+ rule: "env.draft.mcp.command",
435
+ message: this.text("refused.env.draft.mcp.command", { name: draft.name }),
436
+ nextAction: this.text("next.env.draft.mcp.command"),
428
437
  details: { name: draft.name },
429
438
  });
430
439
  }
@@ -447,9 +456,9 @@ export class DirectoryEnvironment {
447
456
  }).catch((error) => {
448
457
  throw arborError({
449
458
  code: "unavailable",
450
- rule: "env.draft.mcp",
451
- message: `环境:MCP 服务器 ${draft.name} 启动失败`,
452
- nextAction: "读 /env/skills/arbor-mcp/SKILL.md 的「自测」一节,把服务器在本窗口工作区里跑通后再调用 reload。",
459
+ rule: "env.draft.mcp.start",
460
+ message: this.text("refused.env.draft.mcp.start", { name: draft.name }),
461
+ nextAction: this.text("next.env.draft.mcp.start"),
453
462
  details: { name: draft.name, upstream: error instanceof Error ? error.message : String(error) },
454
463
  cause: error,
455
464
  });
@@ -468,8 +477,8 @@ export class DirectoryEnvironment {
468
477
  throw arborError({
469
478
  code: "invalid_input",
470
479
  rule: "env.draft.service",
471
- message: `环境:服务 ${draft.name} 的配置缺失或不是 JSON`,
472
- nextAction: "读 /env/skills/arbor-service/SKILL.md 的「结构」一节,按它写 svc.json。",
480
+ message: this.text("refused.env.draft.service", { name: draft.name }),
481
+ nextAction: this.text("next.env.draft.service"),
473
482
  details: { name: draft.name },
474
483
  });
475
484
  }
@@ -477,9 +486,9 @@ export class DirectoryEnvironment {
477
486
  if (command === "") {
478
487
  throw arborError({
479
488
  code: "invalid_input",
480
- rule: "env.draft.service",
481
- message: `环境:服务 ${draft.name} 的配置中没有 command`,
482
- nextAction: "读 /env/skills/arbor-service/SKILL.md 的「结构」一节,补上 command。",
489
+ rule: "env.draft.service.command",
490
+ message: this.text("refused.env.draft.service.command", { name: draft.name }),
491
+ nextAction: this.text("next.env.draft.service.command"),
483
492
  details: { name: draft.name },
484
493
  });
485
494
  }
@@ -487,8 +496,8 @@ export class DirectoryEnvironment {
487
496
  throw arborError({
488
497
  code: "unavailable",
489
498
  rule: "env.supervisor.absent",
490
- message: `环境:本主机没有配置服务监督器,无法安装服务 ${draft.name}`,
491
- nextAction: "移除该服务项,或在配置了监督器的主机上安装它。",
499
+ message: this.text("refused.env.supervisor", { name: draft.name }),
500
+ nextAction: this.text("next.env.supervisor"),
492
501
  details: { name: draft.name },
493
502
  });
494
503
  }
@@ -568,9 +577,9 @@ export class DirectoryEnvironment {
568
577
  await this.rollback(entry, previous);
569
578
  throw arborError({
570
579
  code: "unavailable",
571
- rule: "env.draft.service",
572
- message: `环境:服务 ${draft.draft.name} 没有通过健康检查`,
573
- nextAction: "用 services.logs 查看服务的输出,修好后重新调用 reload。",
580
+ rule: "env.draft.service.health",
581
+ message: this.text("refused.env.draft.service.health", { name: draft.draft.name }),
582
+ nextAction: this.text("next.env.draft.service.health"),
574
583
  details: { name: draft.draft.name, upstream: error instanceof Error ? error.message : String(error) },
575
584
  cause: error,
576
585
  });
@@ -612,8 +621,12 @@ export class DirectoryEnvironment {
612
621
  throw arborError({
613
622
  code: "conflict",
614
623
  rule: "env.draft.stale",
615
- message: `环境:${installed.ref} 已被其他窗口更新到第 ${installed.revision} 版,草稿基于旧的 ${read} 版`,
616
- nextAction: `读 ${BUILTIN_SKILL_PATH[draft.draft.kind]} 的「修订冲突」一节,把新旧内容合并进草稿,再重新调用 reload。`,
624
+ message: this.text("refused.env.stale", {
625
+ item: installed.ref,
626
+ revision: installed.revision,
627
+ read,
628
+ }),
629
+ nextAction: this.text("next.env.stale", { skill: BUILTIN_SKILL_PATH[draft.draft.kind] }),
617
630
  details: { item: installed.ref, revision: installed.revision, read },
618
631
  });
619
632
  }
@@ -628,8 +641,8 @@ export class DirectoryEnvironment {
628
641
  throw arborError({
629
642
  code: "not_found",
630
643
  rule: "env.notfound",
631
- message: `环境:没有环境项 ${wanted}`,
632
- nextAction: "用 env.list 查看当前的环境项和它们的引用。",
644
+ message: this.text("refused.env.notfound", { item: wanted }),
645
+ nextAction: this.text("next.env.notfound"),
633
646
  details: { item: wanted },
634
647
  });
635
648
  }
@@ -637,8 +650,8 @@ export class DirectoryEnvironment {
637
650
  throw arborError({
638
651
  code: "refused",
639
652
  rule: "env.readonly",
640
- message: `环境:${entry.ref} 是框架内置的技能,只读`,
641
- nextAction: "内置技能只读;要改就写一个自己的技能,用 env.list 看它的路径。",
653
+ message: this.text("refused.env.readonly", { item: entry.ref }),
654
+ nextAction: this.text("next.env.readonly"),
642
655
  details: { item: entry.ref },
643
656
  });
644
657
  }
@@ -673,12 +686,13 @@ export class DirectoryEnvironment {
673
686
  /** Copy the built-in skills in, read-only, when they are not there yet. */
674
687
  async seedBuiltins() {
675
688
  const here = fileURLToPath(new URL(".", import.meta.url));
676
- for (const name of BUILTIN_SKILLS) {
689
+ const shipped = BUILTIN_SKILLS.map((name) => ({ name, source: join(here, "skills", name) }));
690
+ for (const { name, source } of [...shipped, ...this.extraSkills]) {
677
691
  if (this.index.items.some((entry) => entry.name === name))
678
692
  continue;
679
693
  const target = join(this.root, "env", "skills", name);
680
694
  await mkdir(dirname(target), { recursive: true });
681
- await cp(join(here, "skills", name), target, { recursive: true });
695
+ await cp(source, target, { recursive: true });
682
696
  const text = await readText(join(target, "SKILL.md"));
683
697
  const lines = (text ?? "").split("\n").map((line) => line.trim());
684
698
  const entry = {
@@ -763,14 +777,18 @@ async function copyTree(from, to) {
763
777
  async function discard(draft) {
764
778
  await rm(draft.path, { recursive: true, force: true }).catch(() => { });
765
779
  }
766
- function errorInfo(error, draft) {
780
+ function errorInfo(text, error, draft) {
767
781
  const info = error;
768
782
  if (info.rule === undefined) {
769
783
  return {
770
784
  code: "invalid_input",
771
785
  rule: "env.draft",
772
- message: `环境:草稿 ${draft.name} 无效:${error instanceof Error ? error.message : String(error)}`,
773
- nextAction: "修正草稿后重新调用 reload。",
786
+ message: text("refused.env.draft", {
787
+ path: draft.path,
788
+ kind: draft.kind,
789
+ message: error instanceof Error ? error.message : String(error),
790
+ }),
791
+ nextAction: text("next.env.draft"),
774
792
  };
775
793
  }
776
794
  return {
package/mcp.js CHANGED
@@ -14,6 +14,7 @@
14
14
  import { spawn } from "node:child_process";
15
15
  import { createInterface } from "node:readline";
16
16
  import { arborError } from "@4fu/arbor-errors";
17
+ import { childEnvironment } from "@4fu/arbor-tools";
17
18
  /** The MCP protocol version this client speaks. */
18
19
  const PROTOCOL_VERSION = "2024-11-05";
19
20
  /** How long one request waits for an answer. */
@@ -38,7 +39,9 @@ export class StdioMcpServer {
38
39
  }
39
40
  this.child = spawn(options.command, [...(options.args ?? [])], {
40
41
  cwd: options.dir,
41
- env: { ...process.env, ...(options.env ?? {}), ...secrets },
42
+ // A server gets the host's environment, its own directory, its own `env`
43
+ // and the secrets it names (docs/environment.md#what-a-subprocess-gets).
44
+ env: childEnvironment({ workspace: options.dir, env: options.env, secrets }),
42
45
  stdio: ["pipe", "pipe", "pipe"],
43
46
  });
44
47
  this.child.stderr?.on("data", (chunk) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@4fu/arbor-environment-dir",
3
- "version": "2026.1106.0",
3
+ "version": "2026.1107.0",
4
4
  "type": "module",
5
5
  "description": "The default Environment: skills, MCP servers, services and settings in one shared /env directory, with a revision per item, validation and deltas.",
6
6
  "exports": {
@@ -36,7 +36,9 @@
36
36
  "access": "public"
37
37
  },
38
38
  "dependencies": {
39
- "@4fu/arbor-errors": "2026.1106.0",
40
- "@4fu/arbor-interfaces": "2026.1106.0"
39
+ "@4fu/arbor-errors": "2026.1107.0",
40
+ "@4fu/arbor-interfaces": "2026.1107.0",
41
+ "@4fu/arbor-prompt": "2026.1107.0",
42
+ "@4fu/arbor-tools": "2026.1107.0"
41
43
  }
42
44
  }
@@ -25,7 +25,9 @@
25
25
  - `modelTiers`、`defaultTier`:每一层映射到后端能跑的一个模型。
26
26
  - `window`:空闲秒数、上下文预算、预留 token。
27
27
  - `snapshot.minIntervalMs`:系统提示和记忆快照最多多久重建一次。
28
- - `workspaces.retentionMs`:窗口链最后一个窗口结束后,它的工作区目录再保留多久由主机回收;不设就不回收。
28
+ - `workspaces.retentionMs`:窗口链最后一个窗口结束后,它的工作区检出再保留多久由主机回收;不设就不回收。
29
+ - `workspaces.shared`:ovnfs 工作区的共享文件夹,所有窗口都在里面读写,按约定只读;不放多个项目和仓库时留空。
30
+ - `workspaces.ovnfs.state` / `workspaces.ovnfs.mount`:ovnfs 守护进程的状态目录和检出挂载目录,默认在主机数据目录下。
29
31
  - `judge.modes`:每个决策是 `on`、`off` 还是 `shadow`。
30
32
  - `secrets`:允许使用的秘密的名字,只写名字,不写值。
31
33