agent-trellis 0.18.0 → 0.20.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/dist/cli.js +12 -1
- package/dist/commands/doctor.d.ts +8 -0
- package/dist/commands/doctor.js +87 -2
- package/dist/commands/evolve.d.ts +43 -0
- package/dist/commands/evolve.js +479 -0
- package/dist/commands/mcpGateway.d.ts +28 -0
- package/dist/commands/mcpGateway.js +40 -6
- package/dist/evolve/agentView.d.ts +32 -0
- package/dist/evolve/agentView.js +34 -0
- package/dist/evolve/apply.d.ts +71 -0
- package/dist/evolve/apply.js +162 -0
- package/dist/evolve/archive.d.ts +23 -0
- package/dist/evolve/archive.js +58 -0
- package/dist/evolve/erase.d.ts +17 -0
- package/dist/evolve/erase.js +65 -0
- package/dist/evolve/extension.d.ts +37 -0
- package/dist/evolve/extension.js +63 -0
- package/dist/evolve/gate.d.ts +95 -0
- package/dist/evolve/gate.js +295 -0
- package/dist/evolve/importHistory.d.ts +91 -0
- package/dist/evolve/importHistory.js +239 -0
- package/dist/evolve/jobMain.d.ts +22 -0
- package/dist/evolve/jobMain.js +52 -0
- package/dist/evolve/jobRun.d.ts +94 -0
- package/dist/evolve/jobRun.js +273 -0
- package/dist/evolve/jobShell.d.ts +46 -0
- package/dist/evolve/jobShell.js +75 -0
- package/dist/evolve/l2.d.ts +93 -0
- package/dist/evolve/l2.js +81 -0
- package/dist/evolve/lesson.d.ts +57 -0
- package/dist/evolve/lesson.js +97 -0
- package/dist/evolve/pending.d.ts +69 -0
- package/dist/evolve/pending.js +116 -0
- package/dist/evolve/probeWorkspace.d.ts +57 -0
- package/dist/evolve/probeWorkspace.js +69 -0
- package/dist/evolve/propose.d.ts +73 -0
- package/dist/evolve/propose.js +136 -0
- package/dist/evolve/recorder.d.ts +68 -0
- package/dist/evolve/recorder.js +169 -0
- package/dist/evolve/report.d.ts +26 -0
- package/dist/evolve/report.js +37 -0
- package/dist/evolve/reportView.d.ts +96 -0
- package/dist/evolve/reportView.js +311 -0
- package/dist/evolve/risk.d.ts +106 -0
- package/dist/evolve/risk.js +224 -0
- package/dist/evolve/sources.d.ts +19 -0
- package/dist/evolve/sources.js +29 -0
- package/dist/evolve/stateDir.d.ts +21 -0
- package/dist/evolve/stateDir.js +56 -0
- package/dist/evolve/surface.d.ts +34 -0
- package/dist/evolve/surface.js +72 -0
- package/dist/evolve/trajectory.d.ts +25 -0
- package/dist/evolve/trajectory.js +88 -0
- package/dist/evolve/treeToCanonical.d.ts +28 -0
- package/dist/evolve/treeToCanonical.js +79 -0
- package/dist/evolve/trellisSubstrate.d.ts +65 -0
- package/dist/evolve/trellisSubstrate.js +154 -0
- package/dist/evolve/trigger.d.ts +177 -0
- package/dist/evolve/trigger.js +400 -0
- package/dist/evolve/types.d.ts +126 -0
- package/dist/evolve/types.js +15 -0
- package/dist/evolve/validate.d.ts +139 -0
- package/dist/evolve/validate.js +315 -0
- package/dist/host/checks.d.ts +57 -0
- package/dist/host/checks.js +126 -0
- package/dist/host/delivery.d.ts +42 -0
- package/dist/host/delivery.js +59 -0
- package/dist/host/edits.d.ts +87 -0
- package/dist/host/edits.js +102 -0
- package/dist/host/hostEvents.d.ts +74 -0
- package/dist/host/hostEvents.js +45 -0
- package/dist/host/index.d.ts +22 -0
- package/dist/host/index.js +17 -0
- package/dist/host/levels.d.ts +106 -0
- package/dist/host/levels.js +206 -0
- package/dist/host/staging.d.ts +56 -0
- package/dist/host/staging.js +106 -0
- package/dist/host/surface.d.ts +36 -0
- package/dist/host/surface.js +20 -0
- package/dist/lib/backup.js +12 -3
- package/dist/lib/fsAtomic.d.ts +7 -0
- package/dist/lib/fsAtomic.js +14 -0
- package/dist/lib/gatewayBackend.d.ts +72 -2
- package/dist/lib/gatewayBackend.js +254 -13
- package/dist/lib/hostEventBus.d.ts +47 -0
- package/dist/lib/hostEventBus.js +94 -0
- package/dist/lib/hostExtensions.d.ts +75 -0
- package/dist/lib/hostExtensions.js +196 -0
- package/dist/lib/mcpRuntime.d.ts +12 -1
- package/dist/lib/mcpRuntime.js +34 -2
- package/dist/lib/mcpToolRegistry.d.ts +28 -2
- package/dist/lib/mcpToolRegistry.js +47 -8
- package/dist/lib/upstreamResources.d.ts +5 -0
- package/dist/lib/upstreamResources.js +10 -0
- package/dist/pi-bridge/bundle.js +33 -5
- package/docs/magpie-gateway-research.md +422 -0
- package/docs/roadmap.md +22 -12
- package/docs/runtime-infra.md +74 -0
- package/docs/self-evolution.md +138 -44
- package/package.json +7 -2
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 可选信号源探测(trellis-evolve 任务 11.3)。
|
|
3
|
+
*
|
|
4
|
+
* agent-bridge 委托记录是可选信号源:缺席是合法状态,SHALL NOT 使运行
|
|
5
|
+
* 失败——探测返回 `unavailable` 而非抛错,作业评估(11.5)以此继续并把
|
|
6
|
+
* 状态记录下来。委托记录存储取 Trellis 真实落盘的 agent 间委托(task
|
|
7
|
+
* store,`~/.trellis/tasks/tasks.json`);委托记录到信号的映射设计未定,
|
|
8
|
+
* 本期只做源可用性探测,不派生信号内容。
|
|
9
|
+
*/
|
|
10
|
+
import { existsSync } from "node:fs";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
export function agentBridgeRecordsPath(homeDir) {
|
|
13
|
+
return join(homeDir, ".trellis", "tasks", "tasks.json");
|
|
14
|
+
}
|
|
15
|
+
/** 探测 agent-bridge 委托记录是否可用;永不抛错。 */
|
|
16
|
+
export function probeAgentBridgeSource(homeDir) {
|
|
17
|
+
if (existsSync(agentBridgeRecordsPath(homeDir))) {
|
|
18
|
+
return {
|
|
19
|
+
source: "agent-bridge",
|
|
20
|
+
status: "available",
|
|
21
|
+
note: "agent-bridge 委托记录存在",
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
source: "agent-bridge",
|
|
26
|
+
status: "unavailable",
|
|
27
|
+
note: "无 agent-bridge 委托存储,运行以其余信号源继续",
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 进化状态目录(trellis-evolve 任务 10.4)。
|
|
3
|
+
*
|
|
4
|
+
* 路径按 8.1 定案:`~/.trellis/evolve/`,与 canonical 内容面(agents.md/
|
|
5
|
+
* skills/MCP 定义/memories)分离——进化子系统独占写入、只追加,`trellis
|
|
6
|
+
* sync` 等读面不消费它。其下按用途分子目录:
|
|
7
|
+
* `signals/` recorder 按进程分文件追加的信号(11.1);
|
|
8
|
+
* `archive/` 运行档案:提案/探针/verdict/备份会话标识/L2 比较记录;
|
|
9
|
+
* `pending/` 待批提案存储(12.4)。
|
|
10
|
+
*/
|
|
11
|
+
export declare const EVOLVE_STATE_SUBDIRS: readonly ["signals", "archive", "pending"];
|
|
12
|
+
export declare function evolveStateDir(homeDir: string): string;
|
|
13
|
+
/** 幂等创建状态目录骨架(0700);只在 `~/.trellis/evolve/` 下落盘。 */
|
|
14
|
+
export declare function ensureEvolveStateDir(homeDir: string): string;
|
|
15
|
+
/**
|
|
16
|
+
* "加入 .gitignore 如适用":状态根本身位于 git work tree 时(如用户把
|
|
17
|
+
* `~/.trellis` 收进 dotfiles 仓库),把 `evolve/` 写进状态根的
|
|
18
|
+
* `.gitignore`,让只追加状态不进版本库;不在 work tree 内则什么都不
|
|
19
|
+
* 做(零副作用)。幂等,重复调用不产生重复条目。
|
|
20
|
+
*/
|
|
21
|
+
export declare function ensureStateGitignored(homeDir: string): "added" | "present" | "not-a-repo";
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 进化状态目录(trellis-evolve 任务 10.4)。
|
|
3
|
+
*
|
|
4
|
+
* 路径按 8.1 定案:`~/.trellis/evolve/`,与 canonical 内容面(agents.md/
|
|
5
|
+
* skills/MCP 定义/memories)分离——进化子系统独占写入、只追加,`trellis
|
|
6
|
+
* sync` 等读面不消费它。其下按用途分子目录:
|
|
7
|
+
* `signals/` recorder 按进程分文件追加的信号(11.1);
|
|
8
|
+
* `archive/` 运行档案:提案/探针/verdict/备份会话标识/L2 比较记录;
|
|
9
|
+
* `pending/` 待批提案存储(12.4)。
|
|
10
|
+
*/
|
|
11
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
12
|
+
import { dirname, join, resolve } from "node:path";
|
|
13
|
+
export const EVOLVE_STATE_SUBDIRS = ["signals", "archive", "pending"];
|
|
14
|
+
export function evolveStateDir(homeDir) {
|
|
15
|
+
return join(homeDir, ".trellis", "evolve");
|
|
16
|
+
}
|
|
17
|
+
/** 幂等创建状态目录骨架(0700);只在 `~/.trellis/evolve/` 下落盘。 */
|
|
18
|
+
export function ensureEvolveStateDir(homeDir) {
|
|
19
|
+
const root = evolveStateDir(homeDir);
|
|
20
|
+
for (const name of EVOLVE_STATE_SUBDIRS)
|
|
21
|
+
mkdirSync(join(root, name), { recursive: true, mode: 0o700 });
|
|
22
|
+
return root;
|
|
23
|
+
}
|
|
24
|
+
function findGitRoot(startDir) {
|
|
25
|
+
let dir = resolve(startDir);
|
|
26
|
+
for (;;) {
|
|
27
|
+
if (existsSync(join(dir, ".git")))
|
|
28
|
+
return dir;
|
|
29
|
+
const parent = dirname(dir);
|
|
30
|
+
if (parent === dir)
|
|
31
|
+
return null;
|
|
32
|
+
dir = parent;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* "加入 .gitignore 如适用":状态根本身位于 git work tree 时(如用户把
|
|
37
|
+
* `~/.trellis` 收进 dotfiles 仓库),把 `evolve/` 写进状态根的
|
|
38
|
+
* `.gitignore`,让只追加状态不进版本库;不在 work tree 内则什么都不
|
|
39
|
+
* 做(零副作用)。幂等,重复调用不产生重复条目。
|
|
40
|
+
*/
|
|
41
|
+
export function ensureStateGitignored(homeDir) {
|
|
42
|
+
const stateRoot = join(homeDir, ".trellis");
|
|
43
|
+
if (findGitRoot(stateRoot) === null)
|
|
44
|
+
return "not-a-repo";
|
|
45
|
+
const ignorePath = join(stateRoot, ".gitignore");
|
|
46
|
+
if (existsSync(ignorePath)) {
|
|
47
|
+
const existing = readFileSync(ignorePath, "utf-8");
|
|
48
|
+
const lines = existing.split("\n").map((line) => line.trim());
|
|
49
|
+
if (lines.some((line) => line === "evolve/" || line === "/evolve/" || line === "evolve"))
|
|
50
|
+
return "present";
|
|
51
|
+
appendFileSync(ignorePath, existing.endsWith("\n") || existing.length === 0 ? "evolve/\n" : "\nevolve/\n");
|
|
52
|
+
return "added";
|
|
53
|
+
}
|
|
54
|
+
appendFileSync(ignorePath, "evolve/\n");
|
|
55
|
+
return "added";
|
|
56
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TrellisSubstrate 的 `surface()`(trellis-evolve 任务 9.1)。
|
|
3
|
+
*
|
|
4
|
+
* 声明 Trellis 的可编辑树 schema —— 引擎只认识这个 schema 内的路径,
|
|
5
|
+
* 除此之外的目标(Class C:secret/凭据材料、MCP auth 配置、受管 agent
|
|
6
|
+
* 集合、自我进化自身配置、用户手写文件)根本不在 schema 里,引擎的
|
|
7
|
+
* `is_allowed` 对它们一律返回 false,不需要也不采信模型自报。
|
|
8
|
+
*
|
|
9
|
+
* 可编辑面恰好四类,与任务书一一对应:
|
|
10
|
+
*
|
|
11
|
+
* - `agents.md` —— 共享 instructions 段落
|
|
12
|
+
* - `skills/<name>/` —— skill 条目(SKILL.md + scripts/*)
|
|
13
|
+
* - `mcp/routes.yaml` —— MCP 路由(mode/每 agent 的 server 列表;
|
|
14
|
+
* 注意 `mcp/servers.yaml` 不在 schema 内 ——
|
|
15
|
+
* server 定义与 auth 材料是 Class C)
|
|
16
|
+
* - `memory/policy.yaml` —— memory 策略(启用/范围策略本身,
|
|
17
|
+
* 不是记忆内容)
|
|
18
|
+
*
|
|
19
|
+
* 静默失效模式(编辑"成功"但什么都不改变的形态,editor 契约必须知道):
|
|
20
|
+
*
|
|
21
|
+
* 1. 加了 skill 但没有触发线 —— Trellis 的 skill 按需加载,
|
|
22
|
+
* `agents.md` 里没有触发线时新条目静默无效(trigger_hint /
|
|
23
|
+
* autotrigger_file / trigger_line 三个字段就是为这个模式存在);
|
|
24
|
+
* 2. routes.yaml 里给某 agent 的路由其 scope 不含该 agent —— 渲染面
|
|
25
|
+
* 对该 agent 静默为空;
|
|
26
|
+
* 3. memory/policy.yaml 的策略改动只在 memory 已启用的安装上生效。
|
|
27
|
+
*
|
|
28
|
+
* 故意不收进 schema 的 harness2 类目:sub_agents(BenchSpec 的
|
|
29
|
+
* `allow_subagents` 默认 false,Trellis 探针任务不开)、plugin、hooks
|
|
30
|
+
* (Trellis 没有这两个概念)。root `scripts/` 保留 harness2 原语义
|
|
31
|
+
* (确定性辅助脚本),skill 脚本不编译(非 Python 约定)。
|
|
32
|
+
*/
|
|
33
|
+
import { type SurfaceSpec } from "@trellis/evolve-engine";
|
|
34
|
+
export declare const TRELLIS_SURFACE: SurfaceSpec;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TrellisSubstrate 的 `surface()`(trellis-evolve 任务 9.1)。
|
|
3
|
+
*
|
|
4
|
+
* 声明 Trellis 的可编辑树 schema —— 引擎只认识这个 schema 内的路径,
|
|
5
|
+
* 除此之外的目标(Class C:secret/凭据材料、MCP auth 配置、受管 agent
|
|
6
|
+
* 集合、自我进化自身配置、用户手写文件)根本不在 schema 里,引擎的
|
|
7
|
+
* `is_allowed` 对它们一律返回 false,不需要也不采信模型自报。
|
|
8
|
+
*
|
|
9
|
+
* 可编辑面恰好四类,与任务书一一对应:
|
|
10
|
+
*
|
|
11
|
+
* - `agents.md` —— 共享 instructions 段落
|
|
12
|
+
* - `skills/<name>/` —— skill 条目(SKILL.md + scripts/*)
|
|
13
|
+
* - `mcp/routes.yaml` —— MCP 路由(mode/每 agent 的 server 列表;
|
|
14
|
+
* 注意 `mcp/servers.yaml` 不在 schema 内 ——
|
|
15
|
+
* server 定义与 auth 材料是 Class C)
|
|
16
|
+
* - `memory/policy.yaml` —— memory 策略(启用/范围策略本身,
|
|
17
|
+
* 不是记忆内容)
|
|
18
|
+
*
|
|
19
|
+
* 静默失效模式(编辑"成功"但什么都不改变的形态,editor 契约必须知道):
|
|
20
|
+
*
|
|
21
|
+
* 1. 加了 skill 但没有触发线 —— Trellis 的 skill 按需加载,
|
|
22
|
+
* `agents.md` 里没有触发线时新条目静默无效(trigger_hint /
|
|
23
|
+
* autotrigger_file / trigger_line 三个字段就是为这个模式存在);
|
|
24
|
+
* 2. routes.yaml 里给某 agent 的路由其 scope 不含该 agent —— 渲染面
|
|
25
|
+
* 对该 agent 静默为空;
|
|
26
|
+
* 3. memory/policy.yaml 的策略改动只在 memory 已启用的安装上生效。
|
|
27
|
+
*
|
|
28
|
+
* 故意不收进 schema 的 harness2 类目:sub_agents(BenchSpec 的
|
|
29
|
+
* `allow_subagents` 默认 false,Trellis 探针任务不开)、plugin、hooks
|
|
30
|
+
* (Trellis 没有这两个概念)。root `scripts/` 保留 harness2 原语义
|
|
31
|
+
* (确定性辅助脚本),skill 脚本不编译(非 Python 约定)。
|
|
32
|
+
*/
|
|
33
|
+
import { makeSurfaceSpec } from "@trellis/evolve-engine";
|
|
34
|
+
export const TRELLIS_SURFACE = makeSurfaceSpec({
|
|
35
|
+
name: "trellis",
|
|
36
|
+
always_on: ["agents.md", "mcp/routes.yaml", "memory/policy.yaml"],
|
|
37
|
+
always_on_max: 48_000,
|
|
38
|
+
always_on_cap_reason: "every always-on byte is injected into every managed agent's context on every session",
|
|
39
|
+
skill_dir: "skills",
|
|
40
|
+
skill_name_rule: "must_match",
|
|
41
|
+
trigger_hint: "Fix it by adding a trigger line naming `{name}` to agents.md, e.g. " +
|
|
42
|
+
'"When <the situation this skill handles>, load the `{name}` skill." ' +
|
|
43
|
+
"A skill with no trigger line is silently inert: Trellis loads skills " +
|
|
44
|
+
"on demand, so nothing references it.",
|
|
45
|
+
autotrigger_file: "agents.md",
|
|
46
|
+
trigger_line: "- When {desc}, load the `{name}` skill.\n",
|
|
47
|
+
subagent_requires_mode: false,
|
|
48
|
+
plugin_dir: null,
|
|
49
|
+
hooks_file: null,
|
|
50
|
+
stubs: [
|
|
51
|
+
[
|
|
52
|
+
"skills/README.md",
|
|
53
|
+
"<!-- Reusable capability entries, delivered to managed agents per " +
|
|
54
|
+
"scope. Add one as skills/<name>/SKILL.md with frontmatter `name:` " +
|
|
55
|
+
"matching the directory and a `description:`; helper files live under " +
|
|
56
|
+
"skills/<name>/scripts/. Inert until a trigger line in agents.md " +
|
|
57
|
+
"references it. -->\n",
|
|
58
|
+
],
|
|
59
|
+
[
|
|
60
|
+
"mcp/README.md",
|
|
61
|
+
"<!-- MCP routing policy lives in mcp/routes.yaml (per-agent mode and " +
|
|
62
|
+
"server lists). Server definitions and auth material are NOT editable " +
|
|
63
|
+
"here — they are protected (Class C) targets. -->\n",
|
|
64
|
+
],
|
|
65
|
+
[
|
|
66
|
+
"memory/README.md",
|
|
67
|
+
"<!-- Memory policy lives in memory/policy.yaml (enable/scope policy " +
|
|
68
|
+
"itself, not memory content). Changes take effect only where memory " +
|
|
69
|
+
"is enabled. -->\n",
|
|
70
|
+
],
|
|
71
|
+
],
|
|
72
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 会话记录 → 共享语法渲染(trellis-evolve 任务 9.3 的 read_trajectory)。
|
|
3
|
+
*
|
|
4
|
+
* Trellis 探针会话的捕获格式:probe launcher(13.3)写出的规范化 NDJSON
|
|
5
|
+
* 事件流,每行一个 JSON 对象:
|
|
6
|
+
*
|
|
7
|
+
* {"type": "text", "text": ...} → [ASSISTANT]
|
|
8
|
+
* {"type": "thinking", "text": ...} → [THINKING]
|
|
9
|
+
* {"type": "tool_call", "name": ..., "input": ..., → [ACTION] name(args)
|
|
10
|
+
* "status": "ok"|"error", → [OBSERVATION] output
|
|
11
|
+
* "output"/"error": ...} 或 [OBSERVATION ERROR]
|
|
12
|
+
* {"type": "error", "message": ...} → [ERROR]
|
|
13
|
+
*
|
|
14
|
+
* 渲染语义对齐 harness2 substrate 的 render_events(共享语法,substrate
|
|
15
|
+
* 各自实现):防御性逐行解析(非 JSON 行跳过)、`_clip` 截断标记
|
|
16
|
+
* `…(+N chars — render truncation)`、整体 max_chars 帽。Trellis 的
|
|
17
|
+
* args_trunc 默认 400(协议默认值)。
|
|
18
|
+
*/
|
|
19
|
+
export interface RenderSessionOptions {
|
|
20
|
+
obsTrunc: number;
|
|
21
|
+
maxChars: number;
|
|
22
|
+
argsTrunc?: number;
|
|
23
|
+
}
|
|
24
|
+
/** Render a captured session stream into the shared trajectory grammar. */
|
|
25
|
+
export declare function renderSessionEvents(streamPath: string, opts: RenderSessionOptions): string;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 会话记录 → 共享语法渲染(trellis-evolve 任务 9.3 的 read_trajectory)。
|
|
3
|
+
*
|
|
4
|
+
* Trellis 探针会话的捕获格式:probe launcher(13.3)写出的规范化 NDJSON
|
|
5
|
+
* 事件流,每行一个 JSON 对象:
|
|
6
|
+
*
|
|
7
|
+
* {"type": "text", "text": ...} → [ASSISTANT]
|
|
8
|
+
* {"type": "thinking", "text": ...} → [THINKING]
|
|
9
|
+
* {"type": "tool_call", "name": ..., "input": ..., → [ACTION] name(args)
|
|
10
|
+
* "status": "ok"|"error", → [OBSERVATION] output
|
|
11
|
+
* "output"/"error": ...} 或 [OBSERVATION ERROR]
|
|
12
|
+
* {"type": "error", "message": ...} → [ERROR]
|
|
13
|
+
*
|
|
14
|
+
* 渲染语义对齐 harness2 substrate 的 render_events(共享语法,substrate
|
|
15
|
+
* 各自实现):防御性逐行解析(非 JSON 行跳过)、`_clip` 截断标记
|
|
16
|
+
* `…(+N chars — render truncation)`、整体 max_chars 帽。Trellis 的
|
|
17
|
+
* args_trunc 默认 400(协议默认值)。
|
|
18
|
+
*/
|
|
19
|
+
import { readFileSync } from "node:fs";
|
|
20
|
+
const truncMarker = (dropped) => `…(+${dropped} chars — render truncation)`;
|
|
21
|
+
/** harness2 `_clip` 的等价物:去 NUL,超长截断并加渲染截断标记。 */
|
|
22
|
+
function clip(value, cap) {
|
|
23
|
+
const s = (value === null || value === undefined ? "" : String(value)).replaceAll("\x00", "");
|
|
24
|
+
if (cap <= 0 || s.length <= cap)
|
|
25
|
+
return s;
|
|
26
|
+
return s.slice(0, cap) + truncMarker(s.length - cap);
|
|
27
|
+
}
|
|
28
|
+
/** Render a captured session stream into the shared trajectory grammar. */
|
|
29
|
+
export function renderSessionEvents(streamPath, opts) {
|
|
30
|
+
const argsCap = Math.trunc(opts.argsTrunc ?? 400);
|
|
31
|
+
let raw;
|
|
32
|
+
try {
|
|
33
|
+
raw = readFileSync(streamPath, "utf-8");
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
return `(failed to read trajectory: ${err instanceof Error ? err.message : String(err)})`;
|
|
37
|
+
}
|
|
38
|
+
const lines = [];
|
|
39
|
+
for (const ln of raw.split("\n")) {
|
|
40
|
+
const trimmed = ln.trim();
|
|
41
|
+
if (!trimmed.startsWith("{"))
|
|
42
|
+
continue;
|
|
43
|
+
let ev;
|
|
44
|
+
try {
|
|
45
|
+
ev = JSON.parse(trimmed);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (typeof ev !== "object" || ev === null || Array.isArray(ev))
|
|
51
|
+
continue;
|
|
52
|
+
const event = ev;
|
|
53
|
+
const type = event.type;
|
|
54
|
+
if (type === "text" || type === "assistant") {
|
|
55
|
+
const text = typeof event.text === "string" ? event.text : undefined;
|
|
56
|
+
if (text?.trim())
|
|
57
|
+
lines.push(`[ASSISTANT] ${clip(text, opts.obsTrunc)}`);
|
|
58
|
+
}
|
|
59
|
+
else if (type === "thinking" || type === "reasoning") {
|
|
60
|
+
const text = typeof event.text === "string" ? event.text : undefined;
|
|
61
|
+
if (text?.trim())
|
|
62
|
+
lines.push(`[THINKING] ${clip(text, opts.obsTrunc)}`);
|
|
63
|
+
}
|
|
64
|
+
else if (type === "tool_call" || type === "tool") {
|
|
65
|
+
const name = typeof event.name === "string" && event.name ? event.name : "tool";
|
|
66
|
+
const args = event.input !== undefined ? JSON.stringify(event.input) : "";
|
|
67
|
+
lines.push(`[ACTION] ${name}(${clip(args, argsCap)})`);
|
|
68
|
+
if (event.status === "error") {
|
|
69
|
+
lines.push(`[OBSERVATION ERROR] ${clip(event.error, opts.obsTrunc)}`);
|
|
70
|
+
}
|
|
71
|
+
else if (event.output !== undefined) {
|
|
72
|
+
lines.push(`[OBSERVATION] ${clip(event.output, opts.obsTrunc)}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
else if (type === "error") {
|
|
76
|
+
lines.push(`[ERROR] ${clip(event.message, 300)}`);
|
|
77
|
+
}
|
|
78
|
+
// 未识别的 type:跳过(捕获格式向前兼容的余地)。
|
|
79
|
+
}
|
|
80
|
+
let rendered = lines.join("\n").trim();
|
|
81
|
+
if (!rendered)
|
|
82
|
+
return "(no parseable events in trajectory)";
|
|
83
|
+
if (rendered.length > opts.maxChars) {
|
|
84
|
+
const dropped = rendered.length - opts.maxChars;
|
|
85
|
+
rendered = rendered.slice(0, opts.maxChars) + "\n" + truncMarker(dropped);
|
|
86
|
+
}
|
|
87
|
+
return rendered;
|
|
88
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 可编辑树 → canonical staging 的布局映射(trellis-evolve 任务 9.2)。
|
|
3
|
+
*
|
|
4
|
+
* 树的四个可编辑类目落到 canonical 的对应物:
|
|
5
|
+
*
|
|
6
|
+
* agents.md → <home>/.trellis/agents.md 整文件替换
|
|
7
|
+
* skills/<name>/** → <home>/.trellis/skills/<name>/** 原样落盘
|
|
8
|
+
* mcp/routes.yaml → <home>/.trellis/mcp/servers.yaml 只替换 routes: 节
|
|
9
|
+
* memory/policy.yaml → <home>/.trellis/scope.yaml 只替换 memories: 节
|
|
10
|
+
*
|
|
11
|
+
* 两个 section 文件的关键性质:servers.yaml 的 server 定义/auth 与
|
|
12
|
+
* scope.yaml 的 skills/agents 节都不在可编辑 schema 内(Class C / 托管
|
|
13
|
+
* 自有),所以落盘是节级 splice —— 解析目标文件、只替换对应顶层键、其余
|
|
14
|
+
* 内容(含注释)原样保留 —— 而不是整文件覆盖。
|
|
15
|
+
*
|
|
16
|
+
* 严格性约定:section 文件本身必须是可解析的 YAML(语法级严格,坏树是
|
|
17
|
+
* materialise 错误);section 内容语义由 canonical 加载器自行容错(语义级
|
|
18
|
+
* 宽松,坏路由在检查结果里显形而不是炸掉 staging)。
|
|
19
|
+
*/
|
|
20
|
+
import type { Tree } from "@trellis/evolve-engine";
|
|
21
|
+
/** Replace (or, for empty/null section content, delete) one top-level key
|
|
22
|
+
* in a YAML file, preserving every other section byte-for-byte where the
|
|
23
|
+
* parser allows. Creates the file when absent. */
|
|
24
|
+
export declare function spliceYamlSection(path: string, key: string, sectionContent: string): void;
|
|
25
|
+
/** The tree rel paths this mapping knows how to place. Anything else is a
|
|
26
|
+
* bug in the caller (the engine's read_tree already filters by the
|
|
27
|
+
* surface schema) — refuse loudly rather than drop a file silently. */
|
|
28
|
+
export declare function applyTreeToStaging(tree: Tree, stagingHome: string): void;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 可编辑树 → canonical staging 的布局映射(trellis-evolve 任务 9.2)。
|
|
3
|
+
*
|
|
4
|
+
* 树的四个可编辑类目落到 canonical 的对应物:
|
|
5
|
+
*
|
|
6
|
+
* agents.md → <home>/.trellis/agents.md 整文件替换
|
|
7
|
+
* skills/<name>/** → <home>/.trellis/skills/<name>/** 原样落盘
|
|
8
|
+
* mcp/routes.yaml → <home>/.trellis/mcp/servers.yaml 只替换 routes: 节
|
|
9
|
+
* memory/policy.yaml → <home>/.trellis/scope.yaml 只替换 memories: 节
|
|
10
|
+
*
|
|
11
|
+
* 两个 section 文件的关键性质:servers.yaml 的 server 定义/auth 与
|
|
12
|
+
* scope.yaml 的 skills/agents 节都不在可编辑 schema 内(Class C / 托管
|
|
13
|
+
* 自有),所以落盘是节级 splice —— 解析目标文件、只替换对应顶层键、其余
|
|
14
|
+
* 内容(含注释)原样保留 —— 而不是整文件覆盖。
|
|
15
|
+
*
|
|
16
|
+
* 严格性约定:section 文件本身必须是可解析的 YAML(语法级严格,坏树是
|
|
17
|
+
* materialise 错误);section 内容语义由 canonical 加载器自行容错(语义级
|
|
18
|
+
* 宽松,坏路由在检查结果里显形而不是炸掉 staging)。
|
|
19
|
+
*/
|
|
20
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
21
|
+
import { dirname, join } from "node:path";
|
|
22
|
+
import { Document, parse as parseYaml, parseDocument } from "yaml";
|
|
23
|
+
/** `routes:` 节的语义守卫:值必须是 {agent: {mode: direct|gateway|hub, servers?}}。
|
|
24
|
+
* 语法可解析但形状明显错误的候选仍写进 staging(canonical 加载器容错,
|
|
25
|
+
* 检查差集会显形);这里只拦"根本不是 map"的类别错误。 */
|
|
26
|
+
function assertSectionIsMap(key, value) {
|
|
27
|
+
if (value !== null && (typeof value !== "object" || Array.isArray(value))) {
|
|
28
|
+
throw new Error(`tree section for "${key}" must be a YAML mapping, got ${Array.isArray(value) ? "array" : typeof value}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/** Replace (or, for empty/null section content, delete) one top-level key
|
|
32
|
+
* in a YAML file, preserving every other section byte-for-byte where the
|
|
33
|
+
* parser allows. Creates the file when absent. */
|
|
34
|
+
export function spliceYamlSection(path, key, sectionContent) {
|
|
35
|
+
const value = parseYaml(sectionContent);
|
|
36
|
+
assertSectionIsMap(key, value);
|
|
37
|
+
const doc = existsSync(path) ? parseDocumentOrThrow(path) : new Document({});
|
|
38
|
+
if (value === null || value === undefined) {
|
|
39
|
+
doc.delete(key);
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
doc.set(key, value);
|
|
43
|
+
}
|
|
44
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
45
|
+
writeFileSync(path, String(doc));
|
|
46
|
+
}
|
|
47
|
+
function parseDocumentOrThrow(path) {
|
|
48
|
+
const text = readFileSync(path, "utf-8");
|
|
49
|
+
const parsed = parseYaml(text);
|
|
50
|
+
if (parsed !== null && parsed !== undefined && (typeof parsed !== "object" || Array.isArray(parsed))) {
|
|
51
|
+
throw new Error(`${path} is not a YAML mapping at the top level`);
|
|
52
|
+
}
|
|
53
|
+
return parseDocument(text);
|
|
54
|
+
}
|
|
55
|
+
/** The tree rel paths this mapping knows how to place. Anything else is a
|
|
56
|
+
* bug in the caller (the engine's read_tree already filters by the
|
|
57
|
+
* surface schema) — refuse loudly rather than drop a file silently. */
|
|
58
|
+
export function applyTreeToStaging(tree, stagingHome) {
|
|
59
|
+
const trellisRoot = join(stagingHome, ".trellis");
|
|
60
|
+
for (const [rel, content] of Object.entries(tree)) {
|
|
61
|
+
if (rel === "agents.md") {
|
|
62
|
+
writeFileSync(join(trellisRoot, "agents.md"), content);
|
|
63
|
+
}
|
|
64
|
+
else if (rel === "mcp/routes.yaml") {
|
|
65
|
+
spliceYamlSection(join(trellisRoot, "mcp", "servers.yaml"), "routes", content);
|
|
66
|
+
}
|
|
67
|
+
else if (rel === "memory/policy.yaml") {
|
|
68
|
+
spliceYamlSection(join(trellisRoot, "scope.yaml"), "memories", content);
|
|
69
|
+
}
|
|
70
|
+
else if (rel.startsWith("skills/")) {
|
|
71
|
+
const target = join(trellisRoot, rel);
|
|
72
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
73
|
+
writeFileSync(target, content);
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
throw new Error(`tree path "${rel}" is on the surface schema but has no canonical mapping`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TrellisSubstrate(trellis-evolve 任务 9.1–9.3)。
|
|
3
|
+
*
|
|
4
|
+
* 引擎 Substrate 协议的 Trellis 实现。内部只经 `src/host/` 窄门面接入
|
|
5
|
+
* 托管(单向依赖:引擎包 + host 门面,见 10.3 的边界测试):
|
|
6
|
+
*
|
|
7
|
+
* surface() → TRELLIS_SURFACE(任务 9.1)
|
|
8
|
+
* materialise() → host 门面的 staging 上渲染候选树;真实 canonical
|
|
9
|
+
* 逐字节不动(9.2)
|
|
10
|
+
* preflight() → 花钱前的 doctor 级自检;canonical 缺席或检查有
|
|
11
|
+
* 发现时抛错,消息带修复命令;不启动任何模型链(9.2)
|
|
12
|
+
* execute() → 委托构造时供给的 launcher body 拉起探针会话,
|
|
13
|
+
* 每 run_id 一个 run_dir;无 launcher 即报错(9.3)
|
|
14
|
+
* read_trajectory() → 会话记录渲染为共享语法(9.3,见 trajectory.ts)
|
|
15
|
+
*/
|
|
16
|
+
import { type Install, type Launcher, type Path, type Substrate, type SurfaceSpec } from "@trellis/evolve-engine";
|
|
17
|
+
export interface TrellisSubstrateOptions {
|
|
18
|
+
homeDir: string;
|
|
19
|
+
/** 进程观察到的 workspace root;null/缺省 = 只评全局层。 */
|
|
20
|
+
workspaceRoot?: string | null;
|
|
21
|
+
/** staging 落盘位置(测试缝,默认 OS tmpdir)。 */
|
|
22
|
+
tmpRoot?: string;
|
|
23
|
+
/** benchmark/探针侧供给的 launcher body(harness2 分工:substrate 拥有
|
|
24
|
+
* 表面与 staging,launcher 拥有 runner 选择、env 接线与读回位置;
|
|
25
|
+
* 每个 run_id 一个 run_dir 由 launcher 落地)。缺席时 execute 报错,
|
|
26
|
+
* 不会退化到任何隐式 spawn。 */
|
|
27
|
+
launcher?: Launcher;
|
|
28
|
+
}
|
|
29
|
+
export declare class TrellisSubstrate implements Substrate {
|
|
30
|
+
readonly name = "trellis";
|
|
31
|
+
private readonly homeDir;
|
|
32
|
+
private readonly workspaceRoot;
|
|
33
|
+
private readonly tmpRoot?;
|
|
34
|
+
private readonly launcher?;
|
|
35
|
+
/** fingerprint → staging,以便 execute 结束后释放 scratch。 */
|
|
36
|
+
private readonly stagings;
|
|
37
|
+
constructor(options: TrellisSubstrateOptions);
|
|
38
|
+
surface(): SurfaceSpec;
|
|
39
|
+
/**
|
|
40
|
+
* 把候选树渲染到 canonical 的 scratch 副本上。离 schema 的路径是
|
|
41
|
+
* materialise 错误而不是静默丢弃 —— 编辑写了什么就必须 staged 什么,
|
|
42
|
+
* 否则 verdict 在评一个与候选不一致的东西。
|
|
43
|
+
*/
|
|
44
|
+
materialise(treeDir: Path, _spec: unknown, _ctx: Readonly<Record<string, string>>): Promise<Install>;
|
|
45
|
+
/** 释放 materialise 建立的 scratch(execute 结束后由调用方触发)。 */
|
|
46
|
+
releaseInstall(install: Install): void;
|
|
47
|
+
/**
|
|
48
|
+
* 花钱前自检:canonical 必须已初始化(否则修修复命令是 `trellis init`);
|
|
49
|
+
* host 检查必须干净(否则指向 `trellis doctor` / `trellis secrets
|
|
50
|
+
* audit`)。只读,不 spawn 任何模型链。
|
|
51
|
+
*/
|
|
52
|
+
preflight(): Promise<void>;
|
|
53
|
+
/**
|
|
54
|
+
* 探针会话执行(harness2 原分工):委托给构造时供给的 launcher body,
|
|
55
|
+
* 每个 run_id 一个 run_dir 由 launcher 落地。substrate 自己不 spawn —
|
|
56
|
+
* 没有 launcher 就报错,不存在隐式默认 runner。
|
|
57
|
+
*/
|
|
58
|
+
execute(taskRef: string, install: Install, runIds: readonly string[], timeout: number): Promise<Path[]>;
|
|
59
|
+
/** 会话记录渲染为共享语法(见 trajectory.ts 的捕获格式与截断语义)。 */
|
|
60
|
+
read_trajectory(streamPath: Path, opts: {
|
|
61
|
+
obs_trunc: number;
|
|
62
|
+
max_chars: number;
|
|
63
|
+
args_trunc?: number;
|
|
64
|
+
}): Promise<string>;
|
|
65
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TrellisSubstrate(trellis-evolve 任务 9.1–9.3)。
|
|
3
|
+
*
|
|
4
|
+
* 引擎 Substrate 协议的 Trellis 实现。内部只经 `src/host/` 窄门面接入
|
|
5
|
+
* 托管(单向依赖:引擎包 + host 门面,见 10.3 的边界测试):
|
|
6
|
+
*
|
|
7
|
+
* surface() → TRELLIS_SURFACE(任务 9.1)
|
|
8
|
+
* materialise() → host 门面的 staging 上渲染候选树;真实 canonical
|
|
9
|
+
* 逐字节不动(9.2)
|
|
10
|
+
* preflight() → 花钱前的 doctor 级自检;canonical 缺席或检查有
|
|
11
|
+
* 发现时抛错,消息带修复命令;不启动任何模型链(9.2)
|
|
12
|
+
* execute() → 委托构造时供给的 launcher body 拉起探针会话,
|
|
13
|
+
* 每 run_id 一个 run_dir;无 launcher 即报错(9.3)
|
|
14
|
+
* read_trajectory() → 会话记录渲染为共享语法(9.3,见 trajectory.ts)
|
|
15
|
+
*/
|
|
16
|
+
import { createHash } from "node:crypto";
|
|
17
|
+
import { join } from "node:path";
|
|
18
|
+
import { is_allowed, makeInstall, makeRunRequest, read_tree, } from "@trellis/evolve-engine";
|
|
19
|
+
import { createStaging, readSurface, runChecks } from "../host/index.js";
|
|
20
|
+
import { TRELLIS_SURFACE } from "./surface.js";
|
|
21
|
+
import { applyTreeToStaging } from "./treeToCanonical.js";
|
|
22
|
+
import { renderSessionEvents } from "./trajectory.js";
|
|
23
|
+
/** preflight 的通过规则:doctor 发现除 `mcp-unreachable`(某台 server 没
|
|
24
|
+
* 应答握手是信息而不是环境病)之外都阻断;secrets audit 的任何发现都阻断
|
|
25
|
+
* (凭据字面值在场时不做任何探针);`sync --check` 的 pending 项不阻断 ——
|
|
26
|
+
* 未同步的渲染是常态,L1 的 verdict 差集自己会看 sync 条目。 */
|
|
27
|
+
const PREFLIGHT_ALLOWED_DOCTOR_KINDS = new Set(["mcp-unreachable"]);
|
|
28
|
+
export class TrellisSubstrate {
|
|
29
|
+
name = "trellis";
|
|
30
|
+
homeDir;
|
|
31
|
+
workspaceRoot;
|
|
32
|
+
tmpRoot;
|
|
33
|
+
launcher;
|
|
34
|
+
/** fingerprint → staging,以便 execute 结束后释放 scratch。 */
|
|
35
|
+
stagings = new Map();
|
|
36
|
+
constructor(options) {
|
|
37
|
+
this.homeDir = options.homeDir;
|
|
38
|
+
this.workspaceRoot = options.workspaceRoot ?? null;
|
|
39
|
+
if (options.tmpRoot !== undefined)
|
|
40
|
+
this.tmpRoot = options.tmpRoot;
|
|
41
|
+
if (options.launcher !== undefined)
|
|
42
|
+
this.launcher = options.launcher;
|
|
43
|
+
}
|
|
44
|
+
surface() {
|
|
45
|
+
return TRELLIS_SURFACE;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* 把候选树渲染到 canonical 的 scratch 副本上。离 schema 的路径是
|
|
49
|
+
* materialise 错误而不是静默丢弃 —— 编辑写了什么就必须 staged 什么,
|
|
50
|
+
* 否则 verdict 在评一个与候选不一致的东西。
|
|
51
|
+
*/
|
|
52
|
+
async materialise(treeDir, _spec, _ctx) {
|
|
53
|
+
const rejected = [];
|
|
54
|
+
const tree = await read_tree(TRELLIS_SURFACE, treeDir, { rejected });
|
|
55
|
+
if (rejected.length > 0) {
|
|
56
|
+
throw new Error(`tree contains paths outside the editable surface: ${rejected.sort().join(", ")}`);
|
|
57
|
+
}
|
|
58
|
+
for (const rel of Object.keys(tree)) {
|
|
59
|
+
if (!is_allowed(TRELLIS_SURFACE, rel)) {
|
|
60
|
+
throw new Error(`tree path "${rel}" is not on the editable surface`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const staging = createStaging({
|
|
64
|
+
homeDir: this.homeDir,
|
|
65
|
+
workspaceRoot: this.workspaceRoot,
|
|
66
|
+
...(this.tmpRoot !== undefined ? { tmpRoot: this.tmpRoot } : {}),
|
|
67
|
+
});
|
|
68
|
+
try {
|
|
69
|
+
applyTreeToStaging(tree, staging.homeDir);
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
staging.dispose();
|
|
73
|
+
throw err;
|
|
74
|
+
}
|
|
75
|
+
const fingerprint = fingerprintTree(tree);
|
|
76
|
+
this.stagings.set(fingerprint, staging);
|
|
77
|
+
return makeInstall({
|
|
78
|
+
staging_dir: staging.root,
|
|
79
|
+
fingerprint,
|
|
80
|
+
// 只暴露 scratch 路径:真实 home/workspace 的路径字符串都不进
|
|
81
|
+
// Install —— 探针工作区不含工作区之外的用户数据,从数据结构上成立。
|
|
82
|
+
extras: {
|
|
83
|
+
home: staging.homeDir,
|
|
84
|
+
...(staging.workspaceRoot ? { workspace: staging.workspaceRoot } : {}),
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
/** 释放 materialise 建立的 scratch(execute 结束后由调用方触发)。 */
|
|
89
|
+
releaseInstall(install) {
|
|
90
|
+
const staging = this.stagings.get(install.fingerprint);
|
|
91
|
+
if (staging) {
|
|
92
|
+
staging.dispose();
|
|
93
|
+
this.stagings.delete(install.fingerprint);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* 花钱前自检:canonical 必须已初始化(否则修修复命令是 `trellis init`);
|
|
98
|
+
* host 检查必须干净(否则指向 `trellis doctor` / `trellis secrets
|
|
99
|
+
* audit`)。只读,不 spawn 任何模型链。
|
|
100
|
+
*/
|
|
101
|
+
async preflight() {
|
|
102
|
+
try {
|
|
103
|
+
readSurface({ homeDir: this.homeDir, workspaceRoot: this.workspaceRoot });
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
throw new Error(`trellis-evolve preflight: no canonical source under ${join(this.homeDir, ".trellis")} — run \`trellis init\` first`);
|
|
107
|
+
}
|
|
108
|
+
const result = await runChecks({ homeDir: this.homeDir, workspaceRoot: this.workspaceRoot });
|
|
109
|
+
const failing = result.entries.filter((entry) => !(entry.source === "doctor" && PREFLIGHT_ALLOWED_DOCTOR_KINDS.has(entry.kind)) && entry.source !== "sync");
|
|
110
|
+
if (failing.length > 0) {
|
|
111
|
+
const bySource = new Map();
|
|
112
|
+
for (const entry of failing)
|
|
113
|
+
bySource.set(entry.source, (bySource.get(entry.source) ?? 0) + 1);
|
|
114
|
+
const summary = [...bySource.entries()].map(([source, n]) => `${source}: ${n}`).join(", ");
|
|
115
|
+
const remediation = [...bySource.keys()]
|
|
116
|
+
.map((source) => (source === "secrets" ? "trellis secrets audit" : "trellis doctor"))
|
|
117
|
+
.map((cmd) => `\`${cmd}\``)
|
|
118
|
+
.join(" / ");
|
|
119
|
+
const sample = failing.slice(0, 5).map((entry) => ` - ${entry.id}`).join("\n");
|
|
120
|
+
throw new Error(`trellis-evolve preflight: ${failing.length} check finding(s) on the live surface (${summary}) — inspect with ${remediation}:\n${sample}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* 探针会话执行(harness2 原分工):委托给构造时供给的 launcher body,
|
|
125
|
+
* 每个 run_id 一个 run_dir 由 launcher 落地。substrate 自己不 spawn —
|
|
126
|
+
* 没有 launcher 就报错,不存在隐式默认 runner。
|
|
127
|
+
*/
|
|
128
|
+
async execute(taskRef, install, runIds, timeout) {
|
|
129
|
+
if (!this.launcher) {
|
|
130
|
+
throw new Error("TrellisSubstrate was constructed without a launcher body; the probe adapter must supply one (it owns runner choice, env wiring and read-back locations)");
|
|
131
|
+
}
|
|
132
|
+
return this.launcher(makeRunRequest({ task_ref: taskRef, run_ids: runIds, timeout: Math.trunc(timeout) }), install);
|
|
133
|
+
}
|
|
134
|
+
/** 会话记录渲染为共享语法(见 trajectory.ts 的捕获格式与截断语义)。 */
|
|
135
|
+
async read_trajectory(streamPath, opts) {
|
|
136
|
+
return renderSessionEvents(streamPath, {
|
|
137
|
+
obsTrunc: opts.obs_trunc,
|
|
138
|
+
maxChars: opts.max_chars,
|
|
139
|
+
...(opts.args_trunc !== undefined ? { argsTrunc: opts.args_trunc } : {}),
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/** sha256 over the sorted (rel, content) pairs — digests the SOURCE tree,
|
|
144
|
+
* matching Install.fingerprint's contract. */
|
|
145
|
+
function fingerprintTree(tree) {
|
|
146
|
+
const hash = createHash("sha256");
|
|
147
|
+
for (const rel of Object.keys(tree).sort()) {
|
|
148
|
+
hash.update(rel);
|
|
149
|
+
hash.update("\0");
|
|
150
|
+
hash.update(tree[rel]);
|
|
151
|
+
hash.update("\0");
|
|
152
|
+
}
|
|
153
|
+
return `sha256:${hash.digest("hex")}`;
|
|
154
|
+
}
|