@4fu/arbor-environment-dir 2026.1106.0 → 2026.1106.1
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 +8 -5
- package/directory-environment.d.ts +22 -2
- package/directory-environment.js +76 -58
- package/mcp.js +4 -1
- package/package.json +5 -3
- package/skills/arbor-settings/SKILL.md +3 -1
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
|
|
31
|
-
|
|
32
|
-
|
|
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.
|
|
@@ -118,7 +120,8 @@ window gets, and how a continuation keeps working in it, are in
|
|
|
118
120
|
| [`@4fu/arbor-journal-sqlite`](packages/journal-sqlite) | the default `Journal` and `BlobStore` on SQLite |
|
|
119
121
|
| [`@4fu/arbor-notes-sqlite`](packages/notes-sqlite) | the default `NoteStore` on SQLite, beside the journal |
|
|
120
122
|
| [`@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
|
|
123
|
+
| [`@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 |
|
|
124
|
+
| [`@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
125
|
| [`@4fu/arbor-environment-dir`](packages/environment-dir) | the default `Environment`: one `/env` directory, a revision per item, validation and deltas |
|
|
123
126
|
| [`@4fu/arbor-supervisor-process`](packages/supervisor-process) | the default `Supervisor`: one host process per service, restart with backoff, logs |
|
|
124
127
|
| [`@4fu/arbor-judge-jev`](packages/judge-jev) | the default `Judge`: Jev, TypeSafe AI's System One classifier, answering the dispatch decisions |
|
|
@@ -151,7 +154,7 @@ deployment can see what it would be taking on before it adopts one.
|
|
|
151
154
|
| [docs/coordination.md](docs/coordination.md) | the board, expectations, deliveries and broadcasts, staying current, effects |
|
|
152
155
|
| [docs/environment.md](docs/environment.md) | skills, MCP servers and services: using, changing, reload, built-in skills, deltas |
|
|
153
156
|
| [docs/memory.md](docs/memory.md) | notes, the active set, anchors, links |
|
|
154
|
-
| [docs/workspaces.md](docs/workspaces.md) | per-window
|
|
157
|
+
| [docs/workspaces.md](docs/workspaces.md) | the shared folder and per-window checkouts: isolation, continuity, what the agent does |
|
|
155
158
|
| [docs/tools.md](docs/tools.md) | direct tools and code mode modules |
|
|
156
159
|
| [docs/prompting.md](docs/prompting.md) | how every model-facing text is written: layers, notes, deliveries, errors, judge questions, terms |
|
|
157
160
|
| [docs/release.md](docs/release.md) | calendar versioning, the manual release steps, how a package is published |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The default `Environment`: one shared directory,
|
|
3
|
-
*
|
|
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;
|
package/directory-environment.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The default `Environment`: one shared directory,
|
|
3
|
-
*
|
|
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
|
-
|
|
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: "
|
|
124
|
-
nextAction: "
|
|
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:
|
|
233
|
-
nextAction: "
|
|
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:
|
|
243
|
-
nextAction: "
|
|
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:
|
|
282
|
-
nextAction: "
|
|
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:
|
|
345
|
-
nextAction: "
|
|
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:
|
|
355
|
-
nextAction: "
|
|
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:
|
|
365
|
-
nextAction: "
|
|
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:
|
|
379
|
-
nextAction: "
|
|
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:
|
|
392
|
-
|
|
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: "
|
|
400
|
-
nextAction: "
|
|
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:
|
|
416
|
-
nextAction: "
|
|
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:
|
|
427
|
-
nextAction: "
|
|
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:
|
|
452
|
-
nextAction: "
|
|
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:
|
|
472
|
-
nextAction: "
|
|
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:
|
|
482
|
-
nextAction: "
|
|
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:
|
|
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:
|
|
573
|
-
nextAction: "
|
|
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:
|
|
616
|
-
|
|
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:
|
|
632
|
-
nextAction: "
|
|
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:
|
|
641
|
-
nextAction: "
|
|
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
|
-
|
|
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(
|
|
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:
|
|
773
|
-
|
|
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
|
-
|
|
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.
|
|
3
|
+
"version": "2026.1106.1",
|
|
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.
|
|
40
|
-
"@4fu/arbor-interfaces": "2026.1106.
|
|
39
|
+
"@4fu/arbor-errors": "2026.1106.1",
|
|
40
|
+
"@4fu/arbor-interfaces": "2026.1106.1",
|
|
41
|
+
"@4fu/arbor-prompt": "2026.1106.1",
|
|
42
|
+
"@4fu/arbor-tools": "2026.1106.1"
|
|
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
|
|