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.
Files changed (100) hide show
  1. package/dist/cli.js +12 -1
  2. package/dist/commands/doctor.d.ts +8 -0
  3. package/dist/commands/doctor.js +87 -2
  4. package/dist/commands/evolve.d.ts +43 -0
  5. package/dist/commands/evolve.js +479 -0
  6. package/dist/commands/mcpGateway.d.ts +28 -0
  7. package/dist/commands/mcpGateway.js +40 -6
  8. package/dist/evolve/agentView.d.ts +32 -0
  9. package/dist/evolve/agentView.js +34 -0
  10. package/dist/evolve/apply.d.ts +71 -0
  11. package/dist/evolve/apply.js +162 -0
  12. package/dist/evolve/archive.d.ts +23 -0
  13. package/dist/evolve/archive.js +58 -0
  14. package/dist/evolve/erase.d.ts +17 -0
  15. package/dist/evolve/erase.js +65 -0
  16. package/dist/evolve/extension.d.ts +37 -0
  17. package/dist/evolve/extension.js +63 -0
  18. package/dist/evolve/gate.d.ts +95 -0
  19. package/dist/evolve/gate.js +295 -0
  20. package/dist/evolve/importHistory.d.ts +91 -0
  21. package/dist/evolve/importHistory.js +239 -0
  22. package/dist/evolve/jobMain.d.ts +22 -0
  23. package/dist/evolve/jobMain.js +52 -0
  24. package/dist/evolve/jobRun.d.ts +94 -0
  25. package/dist/evolve/jobRun.js +273 -0
  26. package/dist/evolve/jobShell.d.ts +46 -0
  27. package/dist/evolve/jobShell.js +75 -0
  28. package/dist/evolve/l2.d.ts +93 -0
  29. package/dist/evolve/l2.js +81 -0
  30. package/dist/evolve/lesson.d.ts +57 -0
  31. package/dist/evolve/lesson.js +97 -0
  32. package/dist/evolve/pending.d.ts +69 -0
  33. package/dist/evolve/pending.js +116 -0
  34. package/dist/evolve/probeWorkspace.d.ts +57 -0
  35. package/dist/evolve/probeWorkspace.js +69 -0
  36. package/dist/evolve/propose.d.ts +73 -0
  37. package/dist/evolve/propose.js +136 -0
  38. package/dist/evolve/recorder.d.ts +68 -0
  39. package/dist/evolve/recorder.js +169 -0
  40. package/dist/evolve/report.d.ts +26 -0
  41. package/dist/evolve/report.js +37 -0
  42. package/dist/evolve/reportView.d.ts +96 -0
  43. package/dist/evolve/reportView.js +311 -0
  44. package/dist/evolve/risk.d.ts +106 -0
  45. package/dist/evolve/risk.js +224 -0
  46. package/dist/evolve/sources.d.ts +19 -0
  47. package/dist/evolve/sources.js +29 -0
  48. package/dist/evolve/stateDir.d.ts +21 -0
  49. package/dist/evolve/stateDir.js +56 -0
  50. package/dist/evolve/surface.d.ts +34 -0
  51. package/dist/evolve/surface.js +72 -0
  52. package/dist/evolve/trajectory.d.ts +25 -0
  53. package/dist/evolve/trajectory.js +88 -0
  54. package/dist/evolve/treeToCanonical.d.ts +28 -0
  55. package/dist/evolve/treeToCanonical.js +79 -0
  56. package/dist/evolve/trellisSubstrate.d.ts +65 -0
  57. package/dist/evolve/trellisSubstrate.js +154 -0
  58. package/dist/evolve/trigger.d.ts +177 -0
  59. package/dist/evolve/trigger.js +400 -0
  60. package/dist/evolve/types.d.ts +126 -0
  61. package/dist/evolve/types.js +15 -0
  62. package/dist/evolve/validate.d.ts +139 -0
  63. package/dist/evolve/validate.js +315 -0
  64. package/dist/host/checks.d.ts +57 -0
  65. package/dist/host/checks.js +126 -0
  66. package/dist/host/delivery.d.ts +42 -0
  67. package/dist/host/delivery.js +59 -0
  68. package/dist/host/edits.d.ts +87 -0
  69. package/dist/host/edits.js +102 -0
  70. package/dist/host/hostEvents.d.ts +74 -0
  71. package/dist/host/hostEvents.js +45 -0
  72. package/dist/host/index.d.ts +22 -0
  73. package/dist/host/index.js +17 -0
  74. package/dist/host/levels.d.ts +106 -0
  75. package/dist/host/levels.js +206 -0
  76. package/dist/host/staging.d.ts +56 -0
  77. package/dist/host/staging.js +106 -0
  78. package/dist/host/surface.d.ts +36 -0
  79. package/dist/host/surface.js +20 -0
  80. package/dist/lib/backup.js +12 -3
  81. package/dist/lib/fsAtomic.d.ts +7 -0
  82. package/dist/lib/fsAtomic.js +14 -0
  83. package/dist/lib/gatewayBackend.d.ts +72 -2
  84. package/dist/lib/gatewayBackend.js +254 -13
  85. package/dist/lib/hostEventBus.d.ts +47 -0
  86. package/dist/lib/hostEventBus.js +94 -0
  87. package/dist/lib/hostExtensions.d.ts +75 -0
  88. package/dist/lib/hostExtensions.js +196 -0
  89. package/dist/lib/mcpRuntime.d.ts +12 -1
  90. package/dist/lib/mcpRuntime.js +34 -2
  91. package/dist/lib/mcpToolRegistry.d.ts +28 -2
  92. package/dist/lib/mcpToolRegistry.js +47 -8
  93. package/dist/lib/upstreamResources.d.ts +5 -0
  94. package/dist/lib/upstreamResources.js +10 -0
  95. package/dist/pi-bridge/bundle.js +33 -5
  96. package/docs/magpie-gateway-research.md +422 -0
  97. package/docs/roadmap.md +22 -12
  98. package/docs/runtime-infra.md +74 -0
  99. package/docs/self-evolution.md +138 -44
  100. package/package.json +7 -2
@@ -0,0 +1,126 @@
1
+ /**
2
+ * 进化核心类型(trellis-evolve 任务 10.1)。
3
+ *
4
+ * Signal、Proposal、Verdict、Lesson、RiskClass、Level。`layer` 字段贯穿
5
+ * 所有落盘类型(Signal 信封、Proposal、Verdict、Lesson);workspaceRoot 不
6
+ * 入库 —— 层归属在采集端从会话观察派生,随每条记录完整序列化往返。
7
+ *
8
+ * 事件字段白名单与 `src/host/hostEvents.ts` 同源:结果类别从那里
9
+ * re-export(10.3 的边界允许 `@trellis/evolve-engine` 与 `src/host/*`)。
10
+ */
11
+ import type { CheckEntry, EditOp, SurfaceLayer } from "../host/index.js";
12
+ import type { ToolResultCategory } from "../host/hostEvents.js";
13
+ export { LEVELS } from "../host/levels.js";
14
+ export type { Level } from "../host/levels.js";
15
+ /**
16
+ * 风险分类:A 仅在 `auto-A` 且通过校验时自动采纳;B 永远需人工批准;
17
+ * C 在校验阶段硬排除。无法分类按 B;仅单 workspace 证据支撑的全局升格
18
+ * 一律 B(12.3 的分类器与采纳门)。
19
+ */
20
+ export type RiskClass = "A" | "B" | "C";
21
+ /** 结果类别白名单的单一事实源在 `src/host/hostEvents.ts`。 */
22
+ export type { ToolResultCategory } from "../host/hostEvents.js";
23
+ /** 信号来源:网关事件、agent 显式上报、agent-bridge 委托记录、历史导入
24
+ * (run-backfill:用户显式选定的 session 数据回填,stableId `import:*`)。 */
25
+ export type SignalSource = "gateway-event" | "agent-report" | "agent-bridge" | "history-import";
26
+ export type SignalKind = "tool-call" | "session-start" | "session-end" | "user-correction";
27
+ /**
28
+ * 信号信封:来源种类、来源提供的稳定标识(来源没有则 null)、采集时间、
29
+ * 层归属。事件负载只保留字段白名单 —— ts / agent / session / provider /
30
+ * tool / durationMs / result;入参与结果内容一律不入库(11.1 的 recorder
31
+ * 负责擦除)。
32
+ */
33
+ interface SignalEnvelope {
34
+ readonly kind: SignalKind;
35
+ readonly source: SignalSource;
36
+ readonly stableId: string | null;
37
+ readonly collectedAt: number;
38
+ readonly layer: SurfaceLayer;
39
+ }
40
+ export interface ToolCallSignal extends SignalEnvelope {
41
+ readonly kind: "tool-call";
42
+ readonly ts: number;
43
+ readonly agent: string;
44
+ readonly session: string;
45
+ readonly provider: string;
46
+ readonly tool: string;
47
+ readonly durationMs: number;
48
+ readonly result: ToolResultCategory;
49
+ }
50
+ export interface SessionStartSignal extends SignalEnvelope {
51
+ readonly kind: "session-start";
52
+ readonly ts: number;
53
+ readonly agent: string;
54
+ readonly session: string;
55
+ }
56
+ export interface SessionEndSignal extends SignalEnvelope {
57
+ readonly kind: "session-end";
58
+ readonly ts: number;
59
+ readonly agent: string;
60
+ readonly session: string;
61
+ }
62
+ /** 用户纠正(text 已过 secrets 擦除;原始文本与 workspaceRoot 均不入库)。 */
63
+ export interface UserCorrectionSignal extends SignalEnvelope {
64
+ readonly kind: "user-correction";
65
+ readonly ts: number;
66
+ readonly agent: string;
67
+ readonly session: string;
68
+ readonly text: string;
69
+ }
70
+ export type Signal = ToolCallSignal | SessionStartSignal | SessionEndSignal | UserCorrectionSignal;
71
+ /**
72
+ * 证据链接三形态:引擎运行、doctor 发现、用户纠正信号。`layer` 可选 ——
73
+ * 证据的采集层归属(12.2 的"不自动升格"判定据此识别单 workspace 证据);
74
+ * 缺省视为层未知,不计入"另一上下文"的佐证。
75
+ */
76
+ export type EvidenceLink = {
77
+ readonly kind: "run";
78
+ readonly runId: string;
79
+ readonly layer?: SurfaceLayer;
80
+ } | {
81
+ readonly kind: "doctor-finding";
82
+ readonly findingId: string;
83
+ readonly layer?: SurfaceLayer;
84
+ } | {
85
+ readonly kind: "user-correction";
86
+ readonly signalId: string;
87
+ readonly layer?: SurfaceLayer;
88
+ };
89
+ /**
90
+ * 提案:`kind` / `target` 的准入白名单与机械校验在 12.2(配置类 kind 才
91
+ * 进入提案通道);风险分类与采纳门在 12.3。
92
+ */
93
+ export interface Proposal {
94
+ readonly id: string;
95
+ readonly createdAt: number;
96
+ readonly kind: string;
97
+ readonly target: string;
98
+ readonly layer: SurfaceLayer;
99
+ readonly evidence: readonly EvidenceLink[];
100
+ readonly riskClass: RiskClass;
101
+ readonly summary: string;
102
+ readonly edits: readonly EditOp[];
103
+ }
104
+ /** Verdict 条目与 host 检查条目同形:稳定 id 参与比较,layer 随行。 */
105
+ export type VerdictEntry = CheckEntry;
106
+ /**
107
+ * L1 差集的测量结果:baseline 与 candidate 各一份完整条目集;可采纳
108
+ * 判定留给 12.1(无新条目 ∧ 目标条目被消除 ∧ 其余不变)。测量失败记
109
+ * `unknown`,不得表示为零分或通过;意外错误按"不采纳"收口在采纳侧。
110
+ */
111
+ export interface Verdict {
112
+ readonly status: "ok" | "unknown";
113
+ readonly layer: SurfaceLayer;
114
+ readonly baseline: readonly VerdictEntry[];
115
+ readonly candidate: readonly VerdictEntry[];
116
+ }
117
+ /** Lesson:cue / 正文 / 证据链接 / 可选 confidence / 层作用域。 */
118
+ export interface Lesson {
119
+ readonly id: string;
120
+ readonly createdAt: number;
121
+ readonly cue: string;
122
+ readonly body: string;
123
+ readonly evidence: readonly EvidenceLink[];
124
+ readonly confidence?: number;
125
+ readonly layer: SurfaceLayer;
126
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * 进化核心类型(trellis-evolve 任务 10.1)。
3
+ *
4
+ * Signal、Proposal、Verdict、Lesson、RiskClass、Level。`layer` 字段贯穿
5
+ * 所有落盘类型(Signal 信封、Proposal、Verdict、Lesson);workspaceRoot 不
6
+ * 入库 —— 层归属在采集端从会话观察派生,随每条记录完整序列化往返。
7
+ *
8
+ * 事件字段白名单与 `src/host/hostEvents.ts` 同源:结果类别从那里
9
+ * re-export(10.3 的边界允许 `@trellis/evolve-engine` 与 `src/host/*`)。
10
+ */
11
+ // Level/LEVELS 的单一事实源在 `src/host/levels.ts`(10.2 的分级开启:
12
+ // 级别策略是 canonical 配置,格式与解析归 host,doctor 与本消费者同源);
13
+ // 这里 re-export 保持核心类型面完整。直接取 `../host/levels.js` 而非
14
+ // 整个门面,避免类型模块拖入 checks/edits 的运行时链。
15
+ export { LEVELS } from "../host/levels.js";
@@ -0,0 +1,139 @@
1
+ /**
2
+ * 证据要求与机械校验(trellis-evolve 任务 12.2)。
3
+ *
4
+ * editor 输出不可信:机械校验器逐条复核 kind 白名单、target 白名单与
5
+ * 证据要求;校验错误以可执行的说明文字反馈给 editor 并至多重试一次,再
6
+ * 失败则丢弃并入档。分级与目标合法性由代码按 kind 与 target 计算,不
7
+ * 采信 editor 自报的风险等级(12.3 重算)。
8
+ *
9
+ * 检查项(顺序即反馈顺序,全部累积,不短路):
10
+ *
11
+ * 1. 证据要求 —— 无证据 → `no-evidence`,probe 前丢弃;
12
+ * 2. kind 白名单 —— 与 L1 前闸同源(`L1_CONFIG_KINDS`),域外
13
+ * → `kind-not-allowed`;
14
+ * 3. 受保护目标硬排除 —— secret/凭据材料、MCP auth 配置(含
15
+ * `mcp/servers.yaml` 整文件与 auth 块形态的 target 描述)、受管
16
+ * agent 集合、Trellis 自有表面之外的用户手写文件、自我进化自身
17
+ * 配置 → `target-protected` / `target-not-on-surface`;
18
+ * 4. 层检查 —— 目标必须落在提案声明层之内;全局层目标不得指向
19
+ * workspace 的 `.agents/` 路径 → `layer-mismatch`;
20
+ * 5. workspace 可提交约束 —— workspace 层编辑内容只允许 `${VAR}`
21
+ * 引用,绝不写凭据形态的字面值 → `workspace-commit-violation`;
22
+ * 6. 不自动升格标记 —— 全局层目标 + 仅单 workspace 证据(或证据层
23
+ * 未知)→ `needsExplicitApproval` 标记(不拒绝;12.3 一律 B,
24
+ * 呈现等待显式批准)。
25
+ *
26
+ * 反馈通道接引擎 `validate_tree` 的错误喂回约定(recursion.ts 的
27
+ * `# YOUR PREVIOUS ATTEMPT WAS REJECTED` 形态);`validateProposalWithRetry`
28
+ * 走完整闭环:校验 → 反馈 → editor 至多修订一次 → 再校验 → 仍违规丢弃
29
+ * + 归档(`proposal-reject`)。
30
+ *
31
+ * target 白名单路径表:全局层 = `~/.trellis` 的可编辑面(canonical 五
32
+ * 文件/skills 树)+ `NATIVE_CHECK_SURFACE` 各 agent home 路径;workspace
33
+ * 层 = `<root>/.agents/**` + workspace MCP 配置文件七形态。七形态在此
34
+ * 本地再声明(import 边界 10.3:evolve 不得引 `src/lib/workspaceMcp`)。
35
+ */
36
+ import type { Proposal } from "./types.js";
37
+ /** 档案 kind:机械校验拒绝(含丢弃原因与 issue 列表)。 */
38
+ export declare const PROPOSAL_REJECT_ARCHIVE_KIND = "proposal-reject";
39
+ /** 机械校验拒绝码(spec 场景逐条对应)。 */
40
+ export type ValidationRejectCode =
41
+ /** 没有任何证据链接(probe 前丢弃,档案以本码记录)。 */
42
+ "no-evidence"
43
+ /** kind 不在配置类白名单。 */
44
+ | "kind-not-allowed"
45
+ /** 受保护目标(secret/凭据、MCP auth、受管集合、进化自身配置)。 */
46
+ | "target-protected"
47
+ /** Trellis 自有表面之外的用户手写文件。 */
48
+ | "target-not-on-surface"
49
+ /** 目标不属于提案声明层(含全局层指向 workspace `.agents/`)。 */
50
+ | "layer-mismatch"
51
+ /** workspace 层编辑写了凭据形态字面值(违反可提交约束)。 */
52
+ | "workspace-commit-violation";
53
+ export interface ValidationIssue {
54
+ readonly code: ValidationRejectCode;
55
+ readonly message: string;
56
+ }
57
+ /**
58
+ * 升格上下文判定(不自动升格标记的依据):证据层未知不算"另一上下文"。
59
+ */
60
+ export type PromotionScope =
61
+ /** 有全局层证据,或 ≥2 个不同 workspace root 的证据。 */
62
+ "multi-context"
63
+ /** 证据全部来自同一 workspace root。 */
64
+ | "single-workspace"
65
+ /** 证据都在但层归属未知 —— 无法证明另一上下文。 */
66
+ | "unknown"
67
+ /** 目标层是 workspace,不存在升格问题。 */
68
+ | "not-applicable";
69
+ export interface ProposalValidation {
70
+ readonly ok: boolean;
71
+ readonly issues: readonly ValidationIssue[];
72
+ /**
73
+ * 全局层目标的不自动升格标记:仅单 workspace 证据(或证据层未知)时
74
+ * 置 true —— 提案不被拒绝,但其全局版本只被呈现等待显式批准(12.3
75
+ * 的采纳门据此一律按 B 处理)。
76
+ */
77
+ readonly needsExplicitApproval: boolean;
78
+ readonly promotionScope: PromotionScope;
79
+ }
80
+ /**
81
+ * workspace MCP 配置文件七形态(`src/lib/workspaceMcp.ts`
82
+ * `workspaceMcpConfigPath` 的路径表;import 边界下本地再声明)。
83
+ */
84
+ export declare const WORKSPACE_CONFIG_RELATIVE_PATHS: readonly string[];
85
+ /** 绝对路径级的受保护判定(12.3 风险分类器的 C 类规则与 pathIssues 同源)。 */
86
+ export declare function protectedPathReason(path: string, homeDir: string): string | null;
87
+ /** 升格上下文判定(12.3 分类器的降级规则与此同源)。 */
88
+ export declare function evaluatePromotion(proposal: Proposal): {
89
+ scope: PromotionScope;
90
+ needsExplicitApproval: boolean;
91
+ };
92
+ /**
93
+ * 机械校验一条提案(不触发任何测量/IO 写入;可提交约束只读内容形态)。
94
+ * 全部 issue 累积返回,顺序即反馈顺序;`needsExplicitApproval` 是标记
95
+ * 不是 issue。
96
+ */
97
+ export declare function validateProposal(proposal: Proposal, options: {
98
+ readonly homeDir: string;
99
+ }): ProposalValidation;
100
+ /**
101
+ * 校验错误 → 可执行反馈文案。通道接引擎 `validate_tree` 的错误喂回约定
102
+ * (recursion.ts `_run_stage`:`# YOUR PREVIOUS ATTEMPT WAS REJECTED` +
103
+ * 错误正文 + 修复指示)。
104
+ */
105
+ export declare function formatValidationFeedback(issues: readonly ValidationIssue[]): string;
106
+ /** 校验闭环的丢弃阶段。 */
107
+ export type RejectPhase = "initial" | "retry-declined" | "retry-exhausted";
108
+ export interface ValidateProposalWithRetryOptions {
109
+ readonly homeDir: string;
110
+ readonly proposal: Proposal;
111
+ /**
112
+ * editor 重试通道:接收上一轮提案与可执行反馈,返回修订提案;返回
113
+ * null 表示 editor 放弃。缺省 = 无 editor 可用,不重试直接丢弃。
114
+ */
115
+ readonly retryEditor?: (previous: Proposal, feedback: string) => Promise<Proposal | null>;
116
+ /** 时间源(毫秒);缺省 Date.now(档案打戳)。 */
117
+ readonly now?: () => number;
118
+ }
119
+ export type ValidateProposalWithRetryResult = {
120
+ readonly status: "accepted";
121
+ /** 最终通过的提案(可能是 editor 修订版)。 */
122
+ readonly proposal: Proposal;
123
+ readonly attempts: 1 | 2;
124
+ readonly validation: ProposalValidation;
125
+ } | {
126
+ readonly status: "discarded";
127
+ readonly proposalId: string;
128
+ readonly code: ValidationRejectCode;
129
+ readonly reason: string;
130
+ readonly attempts: 1 | 2;
131
+ readonly phase: RejectPhase;
132
+ readonly issues: readonly ValidationIssue[];
133
+ };
134
+ /**
135
+ * 机械校验闭环:校验 → 反馈给 editor → 至多修订一次 → 再校验 → 仍违规
136
+ * 丢弃并入档。通过返回最终提案(含升格标记);丢弃时档案以主因码(如
137
+ * `no-evidence`)记录。
138
+ */
139
+ export declare function validateProposalWithRetry(options: ValidateProposalWithRetryOptions): Promise<ValidateProposalWithRetryResult>;
@@ -0,0 +1,315 @@
1
+ /**
2
+ * 证据要求与机械校验(trellis-evolve 任务 12.2)。
3
+ *
4
+ * editor 输出不可信:机械校验器逐条复核 kind 白名单、target 白名单与
5
+ * 证据要求;校验错误以可执行的说明文字反馈给 editor 并至多重试一次,再
6
+ * 失败则丢弃并入档。分级与目标合法性由代码按 kind 与 target 计算,不
7
+ * 采信 editor 自报的风险等级(12.3 重算)。
8
+ *
9
+ * 检查项(顺序即反馈顺序,全部累积,不短路):
10
+ *
11
+ * 1. 证据要求 —— 无证据 → `no-evidence`,probe 前丢弃;
12
+ * 2. kind 白名单 —— 与 L1 前闸同源(`L1_CONFIG_KINDS`),域外
13
+ * → `kind-not-allowed`;
14
+ * 3. 受保护目标硬排除 —— secret/凭据材料、MCP auth 配置(含
15
+ * `mcp/servers.yaml` 整文件与 auth 块形态的 target 描述)、受管
16
+ * agent 集合、Trellis 自有表面之外的用户手写文件、自我进化自身
17
+ * 配置 → `target-protected` / `target-not-on-surface`;
18
+ * 4. 层检查 —— 目标必须落在提案声明层之内;全局层目标不得指向
19
+ * workspace 的 `.agents/` 路径 → `layer-mismatch`;
20
+ * 5. workspace 可提交约束 —— workspace 层编辑内容只允许 `${VAR}`
21
+ * 引用,绝不写凭据形态的字面值 → `workspace-commit-violation`;
22
+ * 6. 不自动升格标记 —— 全局层目标 + 仅单 workspace 证据(或证据层
23
+ * 未知)→ `needsExplicitApproval` 标记(不拒绝;12.3 一律 B,
24
+ * 呈现等待显式批准)。
25
+ *
26
+ * 反馈通道接引擎 `validate_tree` 的错误喂回约定(recursion.ts 的
27
+ * `# YOUR PREVIOUS ATTEMPT WAS REJECTED` 形态);`validateProposalWithRetry`
28
+ * 走完整闭环:校验 → 反馈 → editor 至多修订一次 → 再校验 → 仍违规丢弃
29
+ * + 归档(`proposal-reject`)。
30
+ *
31
+ * target 白名单路径表:全局层 = `~/.trellis` 的可编辑面(canonical 五
32
+ * 文件/skills 树)+ `NATIVE_CHECK_SURFACE` 各 agent home 路径;workspace
33
+ * 层 = `<root>/.agents/**` + workspace MCP 配置文件七形态。七形态在此
34
+ * 本地再声明(import 边界 10.3:evolve 不得引 `src/lib/workspaceMcp`)。
35
+ */
36
+ import { isAbsolute, join, relative, resolve, sep } from "node:path";
37
+ import { appendArchiveRecord } from "./archive.js";
38
+ import { containsCredentialLiteral } from "./erase.js";
39
+ import { L1_CONFIG_KINDS } from "./gate.js";
40
+ import { NATIVE_CHECK_SURFACE } from "../host/index.js";
41
+ /** 档案 kind:机械校验拒绝(含丢弃原因与 issue 列表)。 */
42
+ export const PROPOSAL_REJECT_ARCHIVE_KIND = "proposal-reject";
43
+ /**
44
+ * workspace MCP 配置文件七形态(`src/lib/workspaceMcp.ts`
45
+ * `workspaceMcpConfigPath` 的路径表;import 边界下本地再声明)。
46
+ */
47
+ export const WORKSPACE_CONFIG_RELATIVE_PATHS = [
48
+ join(".mcp.json"),
49
+ join(".codex", "config.toml"),
50
+ join(".kiro", "settings", "mcp.json"),
51
+ join(".pi", "mcp.json"),
52
+ join(".kimi-code", "mcp.json"),
53
+ join("zcode.json"),
54
+ join(".zcode", "config.json"),
55
+ ];
56
+ /** 受保护目标:凭据材料 / MCP auth 配置 / 受管集合 / 进化自身配置。 */
57
+ function protectedPathReasonInner(rel, base) {
58
+ const file = base;
59
+ // 凭据材料:OAuth token 目录、auth.json、.credentials*、.env*、
60
+ // claude 账号缓存(与 staging 的凭据边界同源)。
61
+ if (rel === join(".trellis", "mcp", "oauth") || rel.startsWith(join(".trellis", "mcp", "oauth") + sep)) {
62
+ return "secret/凭据材料(.trellis/mcp/oauth/)受硬排除";
63
+ }
64
+ if (file === "auth.json" ||
65
+ file === ".credentials" ||
66
+ file === ".credentials.json" ||
67
+ file.startsWith(".credentials.") ||
68
+ file === ".env" ||
69
+ file.startsWith(".env.")) {
70
+ return `secret/凭据材料(${file})受硬排除`;
71
+ }
72
+ if (rel === join(".claude", "projects") || rel.startsWith(join(".claude", "projects") + sep)) {
73
+ return "secret/凭据材料(.claude/projects 账号缓存)受硬排除";
74
+ }
75
+ // MCP auth 配置:server 定义与 auth 材料整文件受保护(surface.ts
76
+ // Class C 并集从严)——mcp-config-fix 只面向 routes.yaml。
77
+ if (rel === join(".trellis", "mcp", "servers.yaml")) {
78
+ return "MCP auth 配置(mcp/servers.yaml 整文件)受硬排除";
79
+ }
80
+ // 受管 agent 集合。
81
+ if (rel === join(".trellis", "managed.yaml")) {
82
+ return "受管 agent 集合(managed.yaml)受硬排除";
83
+ }
84
+ // 自我进化自身配置。
85
+ if (rel === join(".trellis", "evolve.yaml") || rel.startsWith(join(".trellis", "evolve") + sep)) {
86
+ return "自我进化自身配置(evolve.yaml / evolve/)受硬排除";
87
+ }
88
+ return null;
89
+ }
90
+ /** `path` 是否为 root 本身或其之下(含根本身;层归属检查用)。
91
+ * 谓词与 gate 的重定位谓词同形,多收根本身一层。 */
92
+ function isUnderRootInclusive(path, root) {
93
+ const rel = relative(resolve(root), resolve(path));
94
+ return rel === "" || (rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel));
95
+ }
96
+ /** homeDir 相对路径的 `.agents` 段是否为 workspace 形态(parent ≠ home)。 */
97
+ function hasWorkspaceAgentsSegment(rel) {
98
+ const segments = rel.split(sep);
99
+ const idx = segments.indexOf(".agents");
100
+ return idx > 0;
101
+ }
102
+ /** 全局层表面白名单:canonical 可编辑面 + NATIVE_CHECK_SURFACE。 */
103
+ function onGlobalSurface(rel) {
104
+ const canonical = [
105
+ join(".trellis", "agents.md"),
106
+ join(".trellis", "mcp", "routes.yaml"),
107
+ join(".trellis", "memory", "policy.yaml"),
108
+ join(".trellis", "secrets.policy.yaml"),
109
+ ];
110
+ if (canonical.includes(rel))
111
+ return true;
112
+ if (rel === join(".trellis", "skills") || rel.startsWith(join(".trellis", "skills") + sep))
113
+ return true;
114
+ return NATIVE_CHECK_SURFACE.some((entry) => rel === entry || rel.startsWith(entry + sep));
115
+ }
116
+ /** workspace 层表面白名单:`.agents/**` + MCP 配置七形态。 */
117
+ function onWorkspaceSurface(rel) {
118
+ if (rel === ".agents" || rel.startsWith(`.agents${sep}`))
119
+ return true;
120
+ return WORKSPACE_CONFIG_RELATIVE_PATHS.includes(rel);
121
+ }
122
+ /** 绝对路径级的受保护判定(12.3 风险分类器的 C 类规则与 pathIssues 同源)。 */
123
+ export function protectedPathReason(path, homeDir) {
124
+ const resolved = resolve(path);
125
+ const rel = relative(resolve(homeDir), resolved);
126
+ const file = resolved.split(sep).pop() ?? resolved;
127
+ return protectedPathReasonInner(isUnderRootInclusive(path, homeDir) ? rel : "", file);
128
+ }
129
+ function pathIssues(path, layer, homeDir) {
130
+ const issues = [];
131
+ const resolved = resolve(path);
132
+ const rel = relative(resolve(homeDir), resolved);
133
+ const inHome = isUnderRootInclusive(path, homeDir);
134
+ // 受保护目标(与层无关,先拒)。
135
+ const file = resolved.split(sep).pop() ?? resolved;
136
+ const protectedReason = inHome ? protectedPathReasonInner(rel, file) : protectedPathReasonInner("", file);
137
+ if (protectedReason !== null) {
138
+ issues.push({ code: "target-protected", message: `编辑路径 ${path}:${protectedReason}` });
139
+ }
140
+ // 层检查:目标必须在声明层之内;全局层不得指向 workspace `.agents/`。
141
+ if (layer.kind === "workspace") {
142
+ if (!isUnderRootInclusive(path, layer.root)) {
143
+ issues.push({
144
+ code: "layer-mismatch",
145
+ message: `编辑路径 ${path} 不在提案声明的 workspace 层(${layer.root})之内`,
146
+ });
147
+ }
148
+ }
149
+ else {
150
+ if (!inHome) {
151
+ issues.push({
152
+ code: "layer-mismatch",
153
+ message: `编辑路径 ${path} 不在提案声明的全局层(HOME ${homeDir})之内`,
154
+ });
155
+ }
156
+ else if (hasWorkspaceAgentsSegment(rel)) {
157
+ issues.push({
158
+ code: "layer-mismatch",
159
+ message: `编辑路径 ${path} 是 workspace 的 .agents/ 路径,不得作全局层目标(声明与目标层不符)`,
160
+ });
161
+ }
162
+ }
163
+ // 表面白名单(按声明层取对应白名单)。
164
+ const onSurface = layer.kind === "workspace"
165
+ ? isUnderRootInclusive(path, layer.root) && onWorkspaceSurface(relative(resolve(layer.root), resolved))
166
+ : inHome && onGlobalSurface(rel);
167
+ if (!onSurface) {
168
+ issues.push({
169
+ code: "target-not-on-surface",
170
+ message: `编辑路径 ${path} 不在 Trellis 自有表面之内(用户手写文件受硬排除)`,
171
+ });
172
+ }
173
+ return issues;
174
+ }
175
+ /** 单个编辑的内容面检查(可提交约束);路径面在 pathIssues。 */
176
+ function contentIssues(edit, layer) {
177
+ if (layer.kind !== "workspace")
178
+ return [];
179
+ if (edit.op === "remove-symlink")
180
+ return [];
181
+ const text = edit.op === "write-file" ? edit.content : edit.target;
182
+ if (containsCredentialLiteral(text)) {
183
+ return [
184
+ {
185
+ code: "workspace-commit-violation",
186
+ message: `编辑 ${edit.path} 写入了凭据形态的字面值,违反 workspace 可提交约束` +
187
+ "(只允许 `${VAR}` 引用,绝不写凭据形态的字面值)",
188
+ },
189
+ ];
190
+ }
191
+ return [];
192
+ }
193
+ /** target 描述符层面的硬排除:MCP auth 块形态的描述。 */
194
+ function targetDescriptorIssues(target) {
195
+ if (/\bauth\b/i.test(target)) {
196
+ return [
197
+ {
198
+ code: "target-protected",
199
+ message: `提案目标 "${target}" 为 MCP auth 配置(auth 块受硬排除)`,
200
+ },
201
+ ];
202
+ }
203
+ return [];
204
+ }
205
+ /** 升格上下文判定(12.3 分类器的降级规则与此同源)。 */
206
+ export function evaluatePromotion(proposal) {
207
+ if (proposal.layer.kind !== "global")
208
+ return { scope: "not-applicable", needsExplicitApproval: false };
209
+ const layers = proposal.evidence
210
+ .map((link) => link.layer)
211
+ .filter((layer) => layer !== undefined);
212
+ const hasGlobal = layers.some((layer) => layer.kind === "global");
213
+ const workspaceRoots = new Set(layers.flatMap((layer) => (layer.kind === "workspace" ? [layer.root] : [])));
214
+ if (hasGlobal || workspaceRoots.size >= 2)
215
+ return { scope: "multi-context", needsExplicitApproval: false };
216
+ if (workspaceRoots.size === 1)
217
+ return { scope: "single-workspace", needsExplicitApproval: true };
218
+ // 证据都在但层未知:无法证明另一上下文,升格需显式批准(从严)。
219
+ return { scope: "unknown", needsExplicitApproval: true };
220
+ }
221
+ /**
222
+ * 机械校验一条提案(不触发任何测量/IO 写入;可提交约束只读内容形态)。
223
+ * 全部 issue 累积返回,顺序即反馈顺序;`needsExplicitApproval` 是标记
224
+ * 不是 issue。
225
+ */
226
+ export function validateProposal(proposal, options) {
227
+ const issues = [];
228
+ // 1. 证据要求。
229
+ if (proposal.evidence.length === 0) {
230
+ issues.push({
231
+ code: "no-evidence",
232
+ message: "提案没有任何证据链接,probe 前丢弃(每条编辑提案必须携带至少一个证据链接)",
233
+ });
234
+ }
235
+ // 2. kind 白名单(与 L1 前闸同源)。
236
+ if (!L1_CONFIG_KINDS.has(proposal.kind)) {
237
+ issues.push({
238
+ code: "kind-not-allowed",
239
+ message: `提案 kind "${proposal.kind}" 不在配置类白名单(域外提案不进提案通道)`,
240
+ });
241
+ }
242
+ // 3. target 描述符硬排除。
243
+ issues.push(...targetDescriptorIssues(proposal.target));
244
+ // 4–5. 逐编辑:受保护目标 / 层检查 / 表面白名单 / 可提交约束。
245
+ for (const edit of proposal.edits) {
246
+ issues.push(...pathIssues(edit.path, proposal.layer, options.homeDir));
247
+ issues.push(...contentIssues(edit, proposal.layer));
248
+ }
249
+ // 6. 不自动升格标记(不是 issue)。
250
+ const { scope, needsExplicitApproval } = evaluatePromotion(proposal);
251
+ return { ok: issues.length === 0, issues, needsExplicitApproval, promotionScope: scope };
252
+ }
253
+ /**
254
+ * 校验错误 → 可执行反馈文案。通道接引擎 `validate_tree` 的错误喂回约定
255
+ * (recursion.ts `_run_stage`:`# YOUR PREVIOUS ATTEMPT WAS REJECTED` +
256
+ * 错误正文 + 修复指示)。
257
+ */
258
+ export function formatValidationFeedback(issues) {
259
+ const lines = issues.map((issue) => `- [${issue.code}] ${issue.message}`);
260
+ return [
261
+ "# YOUR PREVIOUS ATTEMPT WAS REJECTED",
262
+ "",
263
+ ...lines,
264
+ "",
265
+ "Fix the listed problems and reply with a revised proposal.",
266
+ ].join("\n");
267
+ }
268
+ function discard(options, proposalId, issues, attempts, phase) {
269
+ const code = issues[0]?.code ?? "no-evidence";
270
+ const reason = issues[0]?.message ?? "机械校验拒绝";
271
+ const stamp = () => new Date((options.now ?? Date.now)());
272
+ appendArchiveRecord(options.homeDir, {
273
+ kind: PROPOSAL_REJECT_ARCHIVE_KIND,
274
+ payload: {
275
+ proposalId,
276
+ phase,
277
+ code,
278
+ reason,
279
+ retried: phase !== "initial",
280
+ issues,
281
+ },
282
+ }, stamp);
283
+ return { status: "discarded", proposalId, code, reason, attempts, phase, issues };
284
+ }
285
+ /**
286
+ * 机械校验闭环:校验 → 反馈给 editor → 至多修订一次 → 再校验 → 仍违规
287
+ * 丢弃并入档。通过返回最终提案(含升格标记);丢弃时档案以主因码(如
288
+ * `no-evidence`)记录。
289
+ */
290
+ export async function validateProposalWithRetry(options) {
291
+ const first = validateProposal(options.proposal, { homeDir: options.homeDir });
292
+ if (first.ok) {
293
+ return { status: "accepted", proposal: options.proposal, attempts: 1, validation: first };
294
+ }
295
+ const feedback = formatValidationFeedback(first.issues);
296
+ const retryEditor = options.retryEditor;
297
+ if (retryEditor === undefined) {
298
+ return discard(options, options.proposal.id, first.issues, 1, "initial");
299
+ }
300
+ let revised;
301
+ try {
302
+ revised = await retryEditor(options.proposal, feedback);
303
+ }
304
+ catch {
305
+ return discard(options, options.proposal.id, first.issues, 1, "retry-declined");
306
+ }
307
+ if (revised === null) {
308
+ return discard(options, options.proposal.id, first.issues, 1, "retry-declined");
309
+ }
310
+ const second = validateProposal(revised, { homeDir: options.homeDir });
311
+ if (second.ok) {
312
+ return { status: "accepted", proposal: revised, attempts: 2, validation: second };
313
+ }
314
+ return discard(options, options.proposal.id, second.issues, 2, "retry-exhausted");
315
+ }
@@ -0,0 +1,57 @@
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 type { SurfaceLayer } from "./surface.js";
25
+ export type CheckSource = "doctor" | "sync" | "secrets";
26
+ export interface CheckEntry {
27
+ /** Stable, path-normalized identity. Equal for the same logical finding
28
+ * across a real run and a staging run; safe to use as a set key. */
29
+ id: string;
30
+ source: CheckSource;
31
+ /** Collector-specific finding kind (doctor `FindingKind`, sync
32
+ * `AdapterPlanItem.kind`, secrets audit's finding kind). */
33
+ kind: string;
34
+ layer: SurfaceLayer;
35
+ /** The collector's original message text. NOT part of the identity. */
36
+ message: string;
37
+ }
38
+ export interface CheckResult {
39
+ /** Sorted by `id` for deterministic output. */
40
+ entries: CheckEntry[];
41
+ }
42
+ export interface RunChecksOptions {
43
+ homeDir: string;
44
+ /** Physical workspace root to check — the scratch root when checking a
45
+ * staging copy. Always passed explicitly; never inferred from cwd. */
46
+ workspaceRoot?: string | null;
47
+ /** The real workspace root a staging copy stands for. Used for id
48
+ * normalization and as the layer label so scratch paths never leak
49
+ * into comparable identities. Defaults to `workspaceRoot`. */
50
+ logicalWorkspaceRoot?: string | null;
51
+ }
52
+ /**
53
+ * Run the three machine-readable checks against one surface and return
54
+ * their findings as one comparable set. Read-only: nothing on disk is
55
+ * created, modified, or deleted by this function or its collectors.
56
+ */
57
+ export declare function runChecks(options: RunChecksOptions): Promise<CheckResult>;