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,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `host` facade — structured machine-readable checks
|
|
3
|
+
* (host-extension-points spec).
|
|
4
|
+
*
|
|
5
|
+
* One composition of the three read-only checkers — doctor findings,
|
|
6
|
+
* `sync --check` plan items, secrets audit — into a flat, comparable
|
|
7
|
+
* result set. Every entry carries:
|
|
8
|
+
*
|
|
9
|
+
* - a STABLE `id` whose identity is path-normalized (`<home>` /
|
|
10
|
+
* `<workspace>` placeholders), so the same logical finding produced
|
|
11
|
+
* against a real home and against a staging copy compares equal and
|
|
12
|
+
* two runs can be differenced as sets;
|
|
13
|
+
* - layer attribution (global vs a concrete workspace root), so a
|
|
14
|
+
* workspace-layer candidate can be evaluated without the global
|
|
15
|
+
* layer's own noise drowning it;
|
|
16
|
+
* - the collector's original `message` text (comparison is by `id`
|
|
17
|
+
* only — the message stays human-faithful).
|
|
18
|
+
*
|
|
19
|
+
* Checks stay read-only by construction: doctor runs with `probeMcp:
|
|
20
|
+
* false` (no handshakes, no PATH scan — that's `runDoctor` CLI surface),
|
|
21
|
+
* sync runs with `check: true` (zero writes, no backup session), and the
|
|
22
|
+
* secrets audit only ever reads.
|
|
23
|
+
*/
|
|
24
|
+
import { collectDoctorReport, resolveKnownHostInjected } from "../commands/doctor.js";
|
|
25
|
+
import { collectSyncReport } from "../commands/sync.js";
|
|
26
|
+
import { collectSecretsAuditReport } from "../commands/secretsAudit.js";
|
|
27
|
+
/** Escape a path root and replace it with a label only at path
|
|
28
|
+
* boundaries (`/`, `:` as in `path:line:col`, or end of string), so a
|
|
29
|
+
* home like `/tmp/ho` can never corrupt `/tmp/home/...`. Longest roots
|
|
30
|
+
* must be replaced first by the caller (workspace before home). */
|
|
31
|
+
function replaceRoot(text, root, label) {
|
|
32
|
+
const escaped = root.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
33
|
+
return text.replace(new RegExp(`${escaped}(?=[/:]|$)`, "g"), label);
|
|
34
|
+
}
|
|
35
|
+
function makeNormalizer(homeDir, roots) {
|
|
36
|
+
const pairs = [];
|
|
37
|
+
for (const root of roots) {
|
|
38
|
+
if (root)
|
|
39
|
+
pairs.push({ root, label: "<workspace>" });
|
|
40
|
+
}
|
|
41
|
+
pairs.push({ root: homeDir, label: "<home>" });
|
|
42
|
+
// Longest first: a workspace nested under home must be replaced before
|
|
43
|
+
// the home prefix swallows it.
|
|
44
|
+
pairs.sort((a, b) => b.root.length - a.root.length);
|
|
45
|
+
return (text) => {
|
|
46
|
+
let out = text;
|
|
47
|
+
for (const { root, label } of pairs)
|
|
48
|
+
out = replaceRoot(out, root, label);
|
|
49
|
+
return out;
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function layerOf(kind, rawText, ctx) {
|
|
53
|
+
const root = ctx.logicalWorkspaceRoot ?? ctx.physicalWorkspaceRoot;
|
|
54
|
+
if (!root)
|
|
55
|
+
return { kind: "global" };
|
|
56
|
+
// Explicit kinds first (workspace-toml-invalid is workspace-born by
|
|
57
|
+
// definition — it is parsed from a workspace-layer config file).
|
|
58
|
+
if (kind === "workspace-toml-invalid")
|
|
59
|
+
return { kind: "workspace", root };
|
|
60
|
+
if (ctx.normalizedText.includes("<workspace>"))
|
|
61
|
+
return { kind: "workspace", root };
|
|
62
|
+
if (ctx.physicalWorkspaceRoot && rawText.includes(ctx.physicalWorkspaceRoot))
|
|
63
|
+
return { kind: "workspace", root };
|
|
64
|
+
return { kind: "global" };
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Run the three machine-readable checks against one surface and return
|
|
68
|
+
* their findings as one comparable set. Read-only: nothing on disk is
|
|
69
|
+
* created, modified, or deleted by this function or its collectors.
|
|
70
|
+
*/
|
|
71
|
+
export async function runChecks(options) {
|
|
72
|
+
const homeDir = options.homeDir;
|
|
73
|
+
const physicalWorkspaceRoot = options.workspaceRoot ?? null;
|
|
74
|
+
const logicalWorkspaceRoot = options.logicalWorkspaceRoot ?? physicalWorkspaceRoot;
|
|
75
|
+
const normalize = makeNormalizer(homeDir, [physicalWorkspaceRoot, logicalWorkspaceRoot]);
|
|
76
|
+
const entries = [];
|
|
77
|
+
const doctorReport = await collectDoctorReport(homeDir, resolveKnownHostInjected({ homeDir }), false, physicalWorkspaceRoot);
|
|
78
|
+
for (const finding of doctorReport.findings) {
|
|
79
|
+
const normalized = normalize(finding.message);
|
|
80
|
+
entries.push({
|
|
81
|
+
id: `doctor:${finding.kind}:${finding.agent ?? "-"}:${normalized}`,
|
|
82
|
+
source: "doctor",
|
|
83
|
+
kind: finding.kind,
|
|
84
|
+
layer: layerOf(finding.kind, finding.message, { physicalWorkspaceRoot, logicalWorkspaceRoot, normalizedText: normalized }),
|
|
85
|
+
message: finding.message,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
// `check: true` — plan only, zero writes, no backup session. The
|
|
89
|
+
// managed set always comes from the surface's own canonical; and the
|
|
90
|
+
// workspace layer is passed explicitly (null means "no layer", never
|
|
91
|
+
// "guess from the caller's cwd").
|
|
92
|
+
const syncReport = await collectSyncReport({
|
|
93
|
+
homeDir,
|
|
94
|
+
workspaceDir: physicalWorkspaceRoot,
|
|
95
|
+
check: true,
|
|
96
|
+
});
|
|
97
|
+
for (const agentReport of syncReport.reports) {
|
|
98
|
+
for (const item of agentReport.items) {
|
|
99
|
+
const rawIdentity = `${item.action}:${item.kind}:${item.target}:${item.linkTarget ?? ""}:${item.mcpRemove?.name ?? item.mcpWrite?.name ?? ""}`;
|
|
100
|
+
const normalized = normalize(rawIdentity);
|
|
101
|
+
entries.push({
|
|
102
|
+
id: `sync:${item.kind}:${item.action}:${normalized}`,
|
|
103
|
+
source: "sync",
|
|
104
|
+
kind: item.kind,
|
|
105
|
+
layer: item.layer === "workspace"
|
|
106
|
+
? { kind: "workspace", root: logicalWorkspaceRoot ?? physicalWorkspaceRoot ?? "" }
|
|
107
|
+
: layerOf(item.kind, rawIdentity, { physicalWorkspaceRoot, logicalWorkspaceRoot, normalizedText: normalized }),
|
|
108
|
+
message: item.description,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const secretsReport = await collectSecretsAuditReport({ homeDir });
|
|
113
|
+
for (const finding of secretsReport.findings) {
|
|
114
|
+
const rawIdentity = `${finding.agent}:${finding.file}:${finding.detail}`;
|
|
115
|
+
const normalized = normalize(rawIdentity);
|
|
116
|
+
entries.push({
|
|
117
|
+
id: `secrets:${finding.kind}:${normalized}`,
|
|
118
|
+
source: "secrets",
|
|
119
|
+
kind: finding.kind,
|
|
120
|
+
layer: layerOf(finding.kind, rawIdentity, { physicalWorkspaceRoot, logicalWorkspaceRoot, normalizedText: normalized }),
|
|
121
|
+
message: `${finding.agent} ${finding.file}: ${finding.detail}`,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
entries.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
125
|
+
return { entries };
|
|
126
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 采纳生效侧的窄门面(host-extension-points;trellis-evolve 12.5 的生产接线)。
|
|
3
|
+
*
|
|
4
|
+
* 两个问题的答案,都是托管知识、不是进化知识:
|
|
5
|
+
* - `triggerRenderSync`:现在触发一次渲染同步(原生渲染 agent 采纳后
|
|
6
|
+
* 生效的唯一途径)。消费者边界(10.3)禁止 `src/evolve/` 引
|
|
7
|
+
* `commands/*`,而 12.5 的"采纳后触发同步"需要真实现——与 checks/
|
|
8
|
+
* 同款:门面内部组合 commands 层,对外只出自己的形状。
|
|
9
|
+
* - `isMemoryDeliveryEnabled`:canonical 记忆管道是否已启用(记忆 MCP
|
|
10
|
+
* server 存在且配置了 `staticEnv.MEMORY_FILE_PATH`,与
|
|
11
|
+
* `commands/memory.ts` 的 `resolveMemoryServerGraphPath` 同判据)。
|
|
12
|
+
* 12.6 的 lesson 由此决定"走记忆管道"还是"只留档案"。
|
|
13
|
+
*
|
|
14
|
+
* 只读(第二个)与显式触发写(第一个)分离;本模块不做任何静默写入。
|
|
15
|
+
*/
|
|
16
|
+
import { type AgentId } from "../core/types.js";
|
|
17
|
+
import type { EvolveObservation } from "./levels.js";
|
|
18
|
+
/**
|
|
19
|
+
* 触发一次渲染同步(skills + instructions),返回进程语义的退出码。
|
|
20
|
+
* `homeDir` 缺省走真实 `~`(与 `runSync` 同款测试/sandbox 注入缝)。
|
|
21
|
+
*/
|
|
22
|
+
export declare function triggerRenderSync(options?: {
|
|
23
|
+
homeDir?: string;
|
|
24
|
+
}): Promise<{
|
|
25
|
+
exitCode: number;
|
|
26
|
+
}>;
|
|
27
|
+
/**
|
|
28
|
+
* 记忆管道是否已启用:memory MCP server 存在且带
|
|
29
|
+
* `staticEnv.MEMORY_FILE_PATH`(判据与 `commands/memory.ts` 一致——认
|
|
30
|
+
* 显式配置,不做启发式扫描)。canonical 缺失时按未启用处理。
|
|
31
|
+
*/
|
|
32
|
+
export declare function isMemoryDeliveryEnabled(homeDir: string): boolean;
|
|
33
|
+
/**
|
|
34
|
+
* 托管 agent 的观测形态分类(11.4 的"capability delivery"判据,与
|
|
35
|
+
* `commands/doctor.ts` 同源):runtime 交付("mcp"/"both")的 agent 走
|
|
36
|
+
* gateway 事件观察("events");仅原生渲染("native"/未配置)只有
|
|
37
|
+
* doctor/sync 信号("doctor-only")。canonical 缺失返回空列表。
|
|
38
|
+
*/
|
|
39
|
+
export declare function classifyManagedObservation(homeDir: string): {
|
|
40
|
+
agent: AgentId;
|
|
41
|
+
observation: EvolveObservation;
|
|
42
|
+
}[];
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 采纳生效侧的窄门面(host-extension-points;trellis-evolve 12.5 的生产接线)。
|
|
3
|
+
*
|
|
4
|
+
* 两个问题的答案,都是托管知识、不是进化知识:
|
|
5
|
+
* - `triggerRenderSync`:现在触发一次渲染同步(原生渲染 agent 采纳后
|
|
6
|
+
* 生效的唯一途径)。消费者边界(10.3)禁止 `src/evolve/` 引
|
|
7
|
+
* `commands/*`,而 12.5 的"采纳后触发同步"需要真实现——与 checks/
|
|
8
|
+
* 同款:门面内部组合 commands 层,对外只出自己的形状。
|
|
9
|
+
* - `isMemoryDeliveryEnabled`:canonical 记忆管道是否已启用(记忆 MCP
|
|
10
|
+
* server 存在且配置了 `staticEnv.MEMORY_FILE_PATH`,与
|
|
11
|
+
* `commands/memory.ts` 的 `resolveMemoryServerGraphPath` 同判据)。
|
|
12
|
+
* 12.6 的 lesson 由此决定"走记忆管道"还是"只留档案"。
|
|
13
|
+
*
|
|
14
|
+
* 只读(第二个)与显式触发写(第一个)分离;本模块不做任何静默写入。
|
|
15
|
+
*/
|
|
16
|
+
import { loadCanonicalSource } from "../core/canonical.js";
|
|
17
|
+
import { capabilityDeliveryForAgent } from "../core/types.js";
|
|
18
|
+
import { MEMORY_SERVER_NAME } from "../commands/memory.js";
|
|
19
|
+
import { runSync } from "../commands/sync.js";
|
|
20
|
+
/**
|
|
21
|
+
* 触发一次渲染同步(skills + instructions),返回进程语义的退出码。
|
|
22
|
+
* `homeDir` 缺省走真实 `~`(与 `runSync` 同款测试/sandbox 注入缝)。
|
|
23
|
+
*/
|
|
24
|
+
export async function triggerRenderSync(options = {}) {
|
|
25
|
+
return await runSync(options.homeDir !== undefined ? { homeDir: options.homeDir } : {});
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* 记忆管道是否已启用:memory MCP server 存在且带
|
|
29
|
+
* `staticEnv.MEMORY_FILE_PATH`(判据与 `commands/memory.ts` 一致——认
|
|
30
|
+
* 显式配置,不做启发式扫描)。canonical 缺失时按未启用处理。
|
|
31
|
+
*/
|
|
32
|
+
export function isMemoryDeliveryEnabled(homeDir) {
|
|
33
|
+
let server;
|
|
34
|
+
try {
|
|
35
|
+
server = loadCanonicalSource(homeDir).mcp.servers[MEMORY_SERVER_NAME];
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
return Boolean(server && server.enabled !== false && server.staticEnv?.MEMORY_FILE_PATH);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* 托管 agent 的观测形态分类(11.4 的"capability delivery"判据,与
|
|
44
|
+
* `commands/doctor.ts` 同源):runtime 交付("mcp"/"both")的 agent 走
|
|
45
|
+
* gateway 事件观察("events");仅原生渲染("native"/未配置)只有
|
|
46
|
+
* doctor/sync 信号("doctor-only")。canonical 缺失返回空列表。
|
|
47
|
+
*/
|
|
48
|
+
export function classifyManagedObservation(homeDir) {
|
|
49
|
+
try {
|
|
50
|
+
const canonical = loadCanonicalSource(homeDir);
|
|
51
|
+
return canonical.managedAgents.map((agent) => ({
|
|
52
|
+
agent,
|
|
53
|
+
observation: capabilityDeliveryForAgent(agent, canonical.mcp) !== "native" ? "events" : "doctor-only",
|
|
54
|
+
}));
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `host` facade — apply + rollback (host-extension-points spec).
|
|
3
|
+
*
|
|
4
|
+
* The write half of the narrow host boundary. Two guarantees the
|
|
5
|
+
* self-evolution consumer depends on:
|
|
6
|
+
*
|
|
7
|
+
* - every edit runs inside ONE backup session, so a batch of edits is
|
|
8
|
+
* undoable as a unit by the standard `trellis rollback` machinery
|
|
9
|
+
* (and by `rollbackSession` below);
|
|
10
|
+
* - `rollbackSession` restores the prior bytes exactly — the spec's
|
|
11
|
+
* "多编辑备份会话内应用后回滚逐字节还原" scenario.
|
|
12
|
+
*
|
|
13
|
+
* Nothing here bypasses the backup layer: the facade maps `EditOp`s onto
|
|
14
|
+
* the same `BackupSession` operations every command already uses, and
|
|
15
|
+
* rollback goes through the same plan/apply functions `trellis rollback`
|
|
16
|
+
* runs. No new write paths.
|
|
17
|
+
*/
|
|
18
|
+
/** One edit to apply inside the backup session. Paths are absolute and
|
|
19
|
+
* already point at the intended surface (the caller decides real home vs
|
|
20
|
+
* staging scratch — this module never relocates anything itself). */
|
|
21
|
+
export type EditOp = {
|
|
22
|
+
op: "write-file";
|
|
23
|
+
path: string;
|
|
24
|
+
content: string;
|
|
25
|
+
} | {
|
|
26
|
+
op: "create-symlink";
|
|
27
|
+
path: string;
|
|
28
|
+
target: string;
|
|
29
|
+
} | {
|
|
30
|
+
op: "repair-symlink";
|
|
31
|
+
path: string;
|
|
32
|
+
target: string;
|
|
33
|
+
} | {
|
|
34
|
+
op: "remove-symlink";
|
|
35
|
+
path: string;
|
|
36
|
+
};
|
|
37
|
+
export interface ApplyEditsOptions {
|
|
38
|
+
homeDir: string;
|
|
39
|
+
edits: readonly EditOp[];
|
|
40
|
+
/** Recorded in the backup manifest as the originating command.
|
|
41
|
+
* Defaults to `host-edit` so a consumer-applied batch is identifiable
|
|
42
|
+
* in `trellis rollback --list` without a human command to match it. */
|
|
43
|
+
command?: string;
|
|
44
|
+
}
|
|
45
|
+
export interface ApplyEditsResult {
|
|
46
|
+
runId: string;
|
|
47
|
+
applied: number;
|
|
48
|
+
}
|
|
49
|
+
/** An apply batch that failed partway. `runId` names the finalized
|
|
50
|
+
* backup session for whatever DID get applied, so the caller can offer
|
|
51
|
+
* a one-step undo instead of leaving a silent partial state. */
|
|
52
|
+
export declare class HostEditError extends Error {
|
|
53
|
+
readonly runId: string;
|
|
54
|
+
constructor(runId: string, cause: unknown);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Apply all edits inside one backup session. The session is finalized
|
|
58
|
+
* even on failure — a partial batch must still be recorded so rollback
|
|
59
|
+
* has something to undo — then a `HostEditError` carrying the runId is
|
|
60
|
+
* thrown.
|
|
61
|
+
*/
|
|
62
|
+
export declare function applyEdits(options: ApplyEditsOptions): Promise<ApplyEditsResult>;
|
|
63
|
+
/** Host-owned view of one rollback plan item. Mirrors the command
|
|
64
|
+
* layer's plan item without re-exporting it (the facade contract). */
|
|
65
|
+
export interface RollbackItem {
|
|
66
|
+
action: "restore" | "conflict" | "already-reverted";
|
|
67
|
+
path: string;
|
|
68
|
+
description: string;
|
|
69
|
+
}
|
|
70
|
+
export interface RollbackSessionOptions {
|
|
71
|
+
homeDir: string;
|
|
72
|
+
runId: string;
|
|
73
|
+
/** Plan only — report what WOULD be restored without touching disk. */
|
|
74
|
+
dryRun?: boolean;
|
|
75
|
+
}
|
|
76
|
+
export interface RollbackSessionResult {
|
|
77
|
+
runId: string;
|
|
78
|
+
items: RollbackItem[];
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Roll back one backup session: plan it, then (unless dryRun) apply the
|
|
82
|
+
* plan. The plan runs through the same conflict checks `trellis
|
|
83
|
+
* rollback` uses — a path whose on-disk state no longer matches the
|
|
84
|
+
* session's recorded post-write state is reported as `conflict` and left
|
|
85
|
+
* untouched, never force-overwritten.
|
|
86
|
+
*/
|
|
87
|
+
export declare function rollbackSession(options: RollbackSessionOptions): Promise<RollbackSessionResult>;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `host` facade — apply + rollback (host-extension-points spec).
|
|
3
|
+
*
|
|
4
|
+
* The write half of the narrow host boundary. Two guarantees the
|
|
5
|
+
* self-evolution consumer depends on:
|
|
6
|
+
*
|
|
7
|
+
* - every edit runs inside ONE backup session, so a batch of edits is
|
|
8
|
+
* undoable as a unit by the standard `trellis rollback` machinery
|
|
9
|
+
* (and by `rollbackSession` below);
|
|
10
|
+
* - `rollbackSession` restores the prior bytes exactly — the spec's
|
|
11
|
+
* "多编辑备份会话内应用后回滚逐字节还原" scenario.
|
|
12
|
+
*
|
|
13
|
+
* Nothing here bypasses the backup layer: the facade maps `EditOp`s onto
|
|
14
|
+
* the same `BackupSession` operations every command already uses, and
|
|
15
|
+
* rollback goes through the same plan/apply functions `trellis rollback`
|
|
16
|
+
* runs. No new write paths.
|
|
17
|
+
*/
|
|
18
|
+
import { lstat } from "node:fs/promises";
|
|
19
|
+
import { currentLinkTarget, openBackupSession } from "../lib/backup.js";
|
|
20
|
+
import { applyRollbackPlan, collectRollbackPlan, loadManifest } from "../commands/rollback.js";
|
|
21
|
+
/** An apply batch that failed partway. `runId` names the finalized
|
|
22
|
+
* backup session for whatever DID get applied, so the caller can offer
|
|
23
|
+
* a one-step undo instead of leaving a silent partial state. */
|
|
24
|
+
export class HostEditError extends Error {
|
|
25
|
+
runId;
|
|
26
|
+
constructor(runId, cause) {
|
|
27
|
+
super(`host edit batch failed after partial application (run ${runId}): ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
28
|
+
this.name = "HostEditError";
|
|
29
|
+
this.runId = runId;
|
|
30
|
+
this.cause = cause;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Apply all edits inside one backup session. The session is finalized
|
|
35
|
+
* even on failure — a partial batch must still be recorded so rollback
|
|
36
|
+
* has something to undo — then a `HostEditError` carrying the runId is
|
|
37
|
+
* thrown.
|
|
38
|
+
*/
|
|
39
|
+
export async function applyEdits(options) {
|
|
40
|
+
const session = openBackupSession(options.homeDir, options.command ?? "host-edit");
|
|
41
|
+
let applied = 0;
|
|
42
|
+
try {
|
|
43
|
+
for (const edit of options.edits) {
|
|
44
|
+
switch (edit.op) {
|
|
45
|
+
case "write-file":
|
|
46
|
+
session.writeFile(edit.path, edit.content);
|
|
47
|
+
break;
|
|
48
|
+
case "create-symlink":
|
|
49
|
+
await session.createSymlink(edit.path, edit.target);
|
|
50
|
+
break;
|
|
51
|
+
case "repair-symlink": {
|
|
52
|
+
// The session records the OLD target for rollback; read it from
|
|
53
|
+
// disk rather than trusting the caller to have probed first.
|
|
54
|
+
const previousTarget = await currentLinkTarget(edit.path);
|
|
55
|
+
if (previousTarget === undefined) {
|
|
56
|
+
throw new Error(`cannot repair ${edit.path}: not a symlink`);
|
|
57
|
+
}
|
|
58
|
+
await session.repairSymlink(edit.path, previousTarget, edit.target);
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
case "remove-symlink": {
|
|
62
|
+
const previousTarget = await currentLinkTarget(edit.path);
|
|
63
|
+
if (previousTarget === undefined) {
|
|
64
|
+
// 幂等重放:目标已不存在视为成功(上次部分失败的批次重试时
|
|
65
|
+
// 该链接可能已被删掉);存在但不是 symlink 仍是错误。
|
|
66
|
+
const stat = await lstat(edit.path).catch(() => null);
|
|
67
|
+
if (stat === null)
|
|
68
|
+
break;
|
|
69
|
+
throw new Error(`cannot remove ${edit.path}: not a symlink`);
|
|
70
|
+
}
|
|
71
|
+
await session.removeSymlink(edit.path, previousTarget);
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
applied += 1;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
session.finalize();
|
|
80
|
+
throw new HostEditError(session.runId, err);
|
|
81
|
+
}
|
|
82
|
+
session.finalize();
|
|
83
|
+
return { runId: session.runId, applied };
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Roll back one backup session: plan it, then (unless dryRun) apply the
|
|
87
|
+
* plan. The plan runs through the same conflict checks `trellis
|
|
88
|
+
* rollback` uses — a path whose on-disk state no longer matches the
|
|
89
|
+
* session's recorded post-write state is reported as `conflict` and left
|
|
90
|
+
* untouched, never force-overwritten.
|
|
91
|
+
*/
|
|
92
|
+
export async function rollbackSession(options) {
|
|
93
|
+
const report = await collectRollbackPlan(options.homeDir, options.runId);
|
|
94
|
+
if (!options.dryRun) {
|
|
95
|
+
const manifest = loadManifest(options.homeDir, report.runId);
|
|
96
|
+
await applyRollbackPlan(options.homeDir, report.runId, manifest, report.items);
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
runId: report.runId,
|
|
100
|
+
items: report.items.map((item) => ({ action: item.action, path: item.path, description: item.description })),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host-extension-point event types (host-extension-points spec).
|
|
3
|
+
*
|
|
4
|
+
* The field whitelist is enforced at the type layer: an event object carries
|
|
5
|
+
* only the metadata listed here — never tool inputs, tool results, or any
|
|
6
|
+
* content derived from them. Factories copy exactly the whitelist fields (so
|
|
7
|
+
* even untyped callers cannot smuggle a payload through) and freeze the
|
|
8
|
+
* result. "session_start" carries the workspace root observed by the process;
|
|
9
|
+
* consumers derive layer attribution from it (null = no workspace).
|
|
10
|
+
*/
|
|
11
|
+
/** How a tool call ended — the only result information an event may carry. */
|
|
12
|
+
export type ToolResultCategory = "ok" | "error" | "timeout";
|
|
13
|
+
export interface ToolCallEvent {
|
|
14
|
+
readonly kind: "tool_call";
|
|
15
|
+
/** Epoch milliseconds. */
|
|
16
|
+
readonly ts: number;
|
|
17
|
+
readonly agent: string;
|
|
18
|
+
readonly session: string;
|
|
19
|
+
readonly provider: string;
|
|
20
|
+
/** The externally exposed (aggregated) tool name. */
|
|
21
|
+
readonly tool: string;
|
|
22
|
+
readonly durationMs: number;
|
|
23
|
+
readonly result: ToolResultCategory;
|
|
24
|
+
}
|
|
25
|
+
export interface SessionStartEvent {
|
|
26
|
+
readonly kind: "session_start";
|
|
27
|
+
readonly ts: number;
|
|
28
|
+
readonly agent: string;
|
|
29
|
+
readonly session: string;
|
|
30
|
+
/** Workspace root observed by the process at startup; null when the session
|
|
31
|
+
* has no workspace attribution (signal-layer attribution derives from it). */
|
|
32
|
+
readonly workspaceRoot: string | null;
|
|
33
|
+
}
|
|
34
|
+
export interface SessionEndEvent {
|
|
35
|
+
readonly kind: "session_end";
|
|
36
|
+
readonly ts: number;
|
|
37
|
+
readonly agent: string;
|
|
38
|
+
readonly session: string;
|
|
39
|
+
}
|
|
40
|
+
export type HostEvent = ToolCallEvent | SessionStartEvent | SessionEndEvent;
|
|
41
|
+
/** Metadata for one routed tool call — the only inputs an event choke point
|
|
42
|
+
* accepts (never call arguments, never result content). */
|
|
43
|
+
export interface ToolCallInfo {
|
|
44
|
+
provider: string;
|
|
45
|
+
tool: string;
|
|
46
|
+
durationMs: number;
|
|
47
|
+
result: ToolResultCategory;
|
|
48
|
+
}
|
|
49
|
+
/** Build a tool-call event from metadata only — call inputs and results never
|
|
50
|
+
* reach this factory, and only the whitelist fields land on the event. */
|
|
51
|
+
export declare function toolCallEvent(input: {
|
|
52
|
+
ts: number;
|
|
53
|
+
agent: string;
|
|
54
|
+
session: string;
|
|
55
|
+
provider: string;
|
|
56
|
+
tool: string;
|
|
57
|
+
durationMs: number;
|
|
58
|
+
result: ToolResultCategory;
|
|
59
|
+
}): ToolCallEvent;
|
|
60
|
+
/** Build a session-start event; `workspaceRoot` is the process-observed root
|
|
61
|
+
* or null when the session has no workspace attribution. */
|
|
62
|
+
export declare function sessionStartEvent(input: {
|
|
63
|
+
ts: number;
|
|
64
|
+
agent: string;
|
|
65
|
+
session: string;
|
|
66
|
+
workspaceRoot: string | null;
|
|
67
|
+
}): SessionStartEvent;
|
|
68
|
+
/** Build a session-end event. Emitted before upstream release on orderly
|
|
69
|
+
* shutdown; not guaranteed on SIGKILL/crash (spec says so explicitly). */
|
|
70
|
+
export declare function sessionEndEvent(input: {
|
|
71
|
+
ts: number;
|
|
72
|
+
agent: string;
|
|
73
|
+
session: string;
|
|
74
|
+
}): SessionEndEvent;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host-extension-point event types (host-extension-points spec).
|
|
3
|
+
*
|
|
4
|
+
* The field whitelist is enforced at the type layer: an event object carries
|
|
5
|
+
* only the metadata listed here — never tool inputs, tool results, or any
|
|
6
|
+
* content derived from them. Factories copy exactly the whitelist fields (so
|
|
7
|
+
* even untyped callers cannot smuggle a payload through) and freeze the
|
|
8
|
+
* result. "session_start" carries the workspace root observed by the process;
|
|
9
|
+
* consumers derive layer attribution from it (null = no workspace).
|
|
10
|
+
*/
|
|
11
|
+
/** Build a tool-call event from metadata only — call inputs and results never
|
|
12
|
+
* reach this factory, and only the whitelist fields land on the event. */
|
|
13
|
+
export function toolCallEvent(input) {
|
|
14
|
+
return Object.freeze({
|
|
15
|
+
kind: "tool_call",
|
|
16
|
+
ts: input.ts,
|
|
17
|
+
agent: input.agent,
|
|
18
|
+
session: input.session,
|
|
19
|
+
provider: input.provider,
|
|
20
|
+
tool: input.tool,
|
|
21
|
+
durationMs: input.durationMs,
|
|
22
|
+
result: input.result,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
/** Build a session-start event; `workspaceRoot` is the process-observed root
|
|
26
|
+
* or null when the session has no workspace attribution. */
|
|
27
|
+
export function sessionStartEvent(input) {
|
|
28
|
+
return Object.freeze({
|
|
29
|
+
kind: "session_start",
|
|
30
|
+
ts: input.ts,
|
|
31
|
+
agent: input.agent,
|
|
32
|
+
session: input.session,
|
|
33
|
+
workspaceRoot: input.workspaceRoot,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
/** Build a session-end event. Emitted before upstream release on orderly
|
|
37
|
+
* shutdown; not guaranteed on SIGKILL/crash (spec says so explicitly). */
|
|
38
|
+
export function sessionEndEvent(input) {
|
|
39
|
+
return Object.freeze({
|
|
40
|
+
kind: "session_end",
|
|
41
|
+
ts: input.ts,
|
|
42
|
+
agent: input.agent,
|
|
43
|
+
session: input.session,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `agent-trellis/host` — the narrow host facade (host-extension-points).
|
|
3
|
+
*
|
|
4
|
+
* The ONLY symbols published under the `./host` subpath. Everything here
|
|
5
|
+
* is host-owned: functions defined in this directory, plus their own
|
|
6
|
+
* option/result types. No `src/adapters/*` or `src/commands/*` symbol is
|
|
7
|
+
* re-exported — the facade composes those layers internally and hands
|
|
8
|
+
* out its own shapes, so a consumer (the self-evolution engine) can
|
|
9
|
+
* never come to depend on internal surfaces the spec says it must not
|
|
10
|
+
* see.
|
|
11
|
+
*/
|
|
12
|
+
export { readSurface } from "./surface.js";
|
|
13
|
+
export type { ReadSurfaceOptions, SurfaceLayer, SurfaceRead } from "./surface.js";
|
|
14
|
+
export { describeEvolveLevels, evolveLevelPolicyPath, LEVELS, loadEvolveLevelPolicy, resolveEvolveLevel, writeEvolveLevelPolicy } from "./levels.js";
|
|
15
|
+
export type { EvolveLevelPolicy, EvolveLevelSection, Level } from "./levels.js";
|
|
16
|
+
export { createStaging, NATIVE_CHECK_SURFACE } from "./staging.js";
|
|
17
|
+
export type { CreateStagingOptions, Staging } from "./staging.js";
|
|
18
|
+
export { runChecks } from "./checks.js";
|
|
19
|
+
export type { CheckEntry, CheckResult, CheckSource, RunChecksOptions } from "./checks.js";
|
|
20
|
+
export { applyEdits, HostEditError, rollbackSession } from "./edits.js";
|
|
21
|
+
export type { ApplyEditsOptions, ApplyEditsResult, EditOp, RollbackItem, RollbackSessionOptions, RollbackSessionResult } from "./edits.js";
|
|
22
|
+
export { classifyManagedObservation, isMemoryDeliveryEnabled, triggerRenderSync } from "./delivery.js";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `agent-trellis/host` — the narrow host facade (host-extension-points).
|
|
3
|
+
*
|
|
4
|
+
* The ONLY symbols published under the `./host` subpath. Everything here
|
|
5
|
+
* is host-owned: functions defined in this directory, plus their own
|
|
6
|
+
* option/result types. No `src/adapters/*` or `src/commands/*` symbol is
|
|
7
|
+
* re-exported — the facade composes those layers internally and hands
|
|
8
|
+
* out its own shapes, so a consumer (the self-evolution engine) can
|
|
9
|
+
* never come to depend on internal surfaces the spec says it must not
|
|
10
|
+
* see.
|
|
11
|
+
*/
|
|
12
|
+
export { readSurface } from "./surface.js";
|
|
13
|
+
export { describeEvolveLevels, evolveLevelPolicyPath, LEVELS, loadEvolveLevelPolicy, resolveEvolveLevel, writeEvolveLevelPolicy } from "./levels.js";
|
|
14
|
+
export { createStaging, NATIVE_CHECK_SURFACE } from "./staging.js";
|
|
15
|
+
export { runChecks } from "./checks.js";
|
|
16
|
+
export { applyEdits, HostEditError, rollbackSession } from "./edits.js";
|
|
17
|
+
export { classifyManagedObservation, isMemoryDeliveryEnabled, triggerRenderSync } from "./delivery.js";
|