agent-trellis 0.19.0 → 0.21.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 (127) hide show
  1. package/dist/adapters/claude-code.js +2 -2
  2. package/dist/adapters/codex.js +3 -3
  3. package/dist/adapters/jsonMcp.d.ts +1 -1
  4. package/dist/adapters/jsonMcp.js +2 -2
  5. package/dist/adapters/kimi-code.js +2 -2
  6. package/dist/adapters/kiro.js +11 -9
  7. package/dist/adapters/mcpPlan.d.ts +1 -1
  8. package/dist/adapters/mcpPlan.js +35 -12
  9. package/dist/adapters/pi.js +5 -5
  10. package/dist/adapters/zcode.d.ts +1 -1
  11. package/dist/adapters/zcode.js +6 -6
  12. package/dist/cli.js +12 -1
  13. package/dist/commands/doctor.d.ts +9 -1
  14. package/dist/commands/doctor.js +92 -7
  15. package/dist/commands/evolve.d.ts +43 -0
  16. package/dist/commands/evolve.js +479 -0
  17. package/dist/commands/mcp.js +1 -1
  18. package/dist/commands/mcpAuth.js +1 -1
  19. package/dist/commands/mcpGateway.d.ts +30 -2
  20. package/dist/commands/mcpGateway.js +44 -10
  21. package/dist/commands/mcpImport.js +2 -1
  22. package/dist/commands/secretsAudit.d.ts +1 -1
  23. package/dist/commands/secretsAudit.js +36 -13
  24. package/dist/evolve/agentView.d.ts +32 -0
  25. package/dist/evolve/agentView.js +34 -0
  26. package/dist/evolve/apply.d.ts +71 -0
  27. package/dist/evolve/apply.js +162 -0
  28. package/dist/evolve/archive.d.ts +23 -0
  29. package/dist/evolve/archive.js +58 -0
  30. package/dist/evolve/erase.d.ts +17 -0
  31. package/dist/evolve/erase.js +65 -0
  32. package/dist/evolve/extension.d.ts +37 -0
  33. package/dist/evolve/extension.js +63 -0
  34. package/dist/evolve/gate.d.ts +95 -0
  35. package/dist/evolve/gate.js +295 -0
  36. package/dist/evolve/importHistory.d.ts +91 -0
  37. package/dist/evolve/importHistory.js +239 -0
  38. package/dist/evolve/jobMain.d.ts +22 -0
  39. package/dist/evolve/jobMain.js +52 -0
  40. package/dist/evolve/jobRun.d.ts +94 -0
  41. package/dist/evolve/jobRun.js +273 -0
  42. package/dist/evolve/jobShell.d.ts +46 -0
  43. package/dist/evolve/jobShell.js +75 -0
  44. package/dist/evolve/l2.d.ts +93 -0
  45. package/dist/evolve/l2.js +81 -0
  46. package/dist/evolve/lesson.d.ts +57 -0
  47. package/dist/evolve/lesson.js +97 -0
  48. package/dist/evolve/pending.d.ts +69 -0
  49. package/dist/evolve/pending.js +116 -0
  50. package/dist/evolve/probeWorkspace.d.ts +57 -0
  51. package/dist/evolve/probeWorkspace.js +69 -0
  52. package/dist/evolve/propose.d.ts +73 -0
  53. package/dist/evolve/propose.js +136 -0
  54. package/dist/evolve/recorder.d.ts +68 -0
  55. package/dist/evolve/recorder.js +169 -0
  56. package/dist/evolve/report.d.ts +26 -0
  57. package/dist/evolve/report.js +37 -0
  58. package/dist/evolve/reportView.d.ts +96 -0
  59. package/dist/evolve/reportView.js +311 -0
  60. package/dist/evolve/risk.d.ts +106 -0
  61. package/dist/evolve/risk.js +224 -0
  62. package/dist/evolve/sources.d.ts +19 -0
  63. package/dist/evolve/sources.js +29 -0
  64. package/dist/evolve/stateDir.d.ts +21 -0
  65. package/dist/evolve/stateDir.js +56 -0
  66. package/dist/evolve/surface.d.ts +34 -0
  67. package/dist/evolve/surface.js +72 -0
  68. package/dist/evolve/trajectory.d.ts +25 -0
  69. package/dist/evolve/trajectory.js +88 -0
  70. package/dist/evolve/treeToCanonical.d.ts +28 -0
  71. package/dist/evolve/treeToCanonical.js +79 -0
  72. package/dist/evolve/trellisSubstrate.d.ts +65 -0
  73. package/dist/evolve/trellisSubstrate.js +154 -0
  74. package/dist/evolve/trigger.d.ts +177 -0
  75. package/dist/evolve/trigger.js +400 -0
  76. package/dist/evolve/types.d.ts +126 -0
  77. package/dist/evolve/types.js +15 -0
  78. package/dist/evolve/validate.d.ts +139 -0
  79. package/dist/evolve/validate.js +315 -0
  80. package/dist/host/checks.d.ts +57 -0
  81. package/dist/host/checks.js +126 -0
  82. package/dist/host/delivery.d.ts +42 -0
  83. package/dist/host/delivery.js +59 -0
  84. package/dist/host/edits.d.ts +87 -0
  85. package/dist/host/edits.js +102 -0
  86. package/dist/host/hostEvents.d.ts +74 -0
  87. package/dist/host/hostEvents.js +45 -0
  88. package/dist/host/index.d.ts +22 -0
  89. package/dist/host/index.js +17 -0
  90. package/dist/host/levels.d.ts +106 -0
  91. package/dist/host/levels.js +206 -0
  92. package/dist/host/staging.d.ts +56 -0
  93. package/dist/host/staging.js +106 -0
  94. package/dist/host/surface.d.ts +36 -0
  95. package/dist/host/surface.js +20 -0
  96. package/dist/lib/backup.js +12 -3
  97. package/dist/lib/envVarNames.js +7 -0
  98. package/dist/lib/fsAtomic.d.ts +7 -0
  99. package/dist/lib/fsAtomic.js +14 -0
  100. package/dist/lib/hostEventBus.d.ts +47 -0
  101. package/dist/lib/hostEventBus.js +94 -0
  102. package/dist/lib/hostExtensions.d.ts +75 -0
  103. package/dist/lib/hostExtensions.js +196 -0
  104. package/dist/lib/mcpConnect.d.ts +1 -1
  105. package/dist/lib/mcpConnect.js +19 -8
  106. package/dist/lib/mcpMigrateRead.js +14 -2
  107. package/dist/lib/mcpRuntime.d.ts +12 -1
  108. package/dist/lib/mcpRuntime.js +34 -2
  109. package/dist/lib/mcpToolRegistry.d.ts +18 -2
  110. package/dist/lib/mcpToolRegistry.js +29 -9
  111. package/dist/lib/secretEnv.d.ts +53 -8
  112. package/dist/lib/secretEnv.js +78 -15
  113. package/dist/lib/tomlSection.js +3 -2
  114. package/dist/lib/workspaceDoctor.d.ts +1 -1
  115. package/dist/lib/workspaceDoctor.js +8 -4
  116. package/dist/lib/workspaceMcp.d.ts +1 -1
  117. package/dist/lib/workspaceMcp.js +5 -5
  118. package/dist/pi-bridge/bundle.js +42236 -2701
  119. package/dist/pi-bridge/index.js +2 -2
  120. package/docs/magpie-gateway-research.md +422 -0
  121. package/docs/research.md +4 -1
  122. package/docs/roadmap.md +22 -12
  123. package/docs/runtime-infra.md +41 -22
  124. package/docs/self-evolution.md +138 -44
  125. package/package.json +8 -2
  126. package/schema/secrets.policy.example.yaml +5 -0
  127. package/schema/servers.example.yaml +5 -1
@@ -0,0 +1,63 @@
1
+ /**
2
+ * 进化订阅者型扩展的 `activate` 包装(design 8.1 定案的装载契约;trees
3
+ * 11.1/11.5 的生产接线)。
4
+ *
5
+ * 装载契约:模块导出 `activate(ctx)` 返回 `{ providers?, subscribers? }`,
6
+ * 由 `~/.trellis/mcp/extensions.yaml` 的清单条目(进化启用命令写入)
7
+ * 指向本模块;gateway 启动时动态 import 并装载。契约的结构形状与
8
+ * `src/lib/hostExtensions.ts` 的 `HostExtension*` 一致——import 边界
9
+ * (10.3)不允许 `src/evolve/` 引 `src/lib/*`,这里以结构类型对接,
10
+ * 由装载集成测试证明形状兼容。
11
+ *
12
+ * 订阅者按级别(activate 时解析,本进程恒定——gateway 按会话拉起):
13
+ * - `off`:零订阅者(结构上零副作用;清单条目本应在置 off 时被移除,
14
+ * 此处是防御)。
15
+ * - `observe` 及以上:recorder(事件 → 信号,enabled 恒真——级别已判)。
16
+ * - `propose`/`auto-A`:另有触发器订阅者(会话结束判定拉起作业、会话
17
+ * 启动补偿判定)。observe 是"只采集信号",不拉起作业。
18
+ */
19
+ import { fileURLToPath } from "node:url";
20
+ import { loadEvolveLevelPolicy, resolveEvolveLevel } from "../host/levels.js";
21
+ import { createSignalRecorder } from "./recorder.js";
22
+ import { maybeLaunchOnSessionEnd, maybeLaunchOnSessionStart } from "./trigger.js";
23
+ /** 清单条目 id(进化启用命令写入 `~/.trellis/mcp/extensions.yaml` 用)。 */
24
+ export const EVOLVE_EXTENSION_ID = "evolve";
25
+ function levelAt(context) {
26
+ const policy = loadEvolveLevelPolicy(context.homeDir);
27
+ return resolveEvolveLevel(policy, {
28
+ agent: context.agent,
29
+ workspaceRoot: context.workspaceRoot,
30
+ });
31
+ }
32
+ export function activate(context) {
33
+ const level = levelAt(context);
34
+ if (level === "off")
35
+ return {};
36
+ const recorder = createSignalRecorder({
37
+ homeDir: context.homeDir,
38
+ enabled: () => true,
39
+ });
40
+ const subscribers = [recorder.handle];
41
+ if (level !== "observe") {
42
+ const policy = loadEvolveLevelPolicy(context.homeDir);
43
+ const options = {
44
+ homeDir: context.homeDir,
45
+ enabled: () => true,
46
+ ...(policy.trigger !== undefined ? { limits: policy.trigger } : {}),
47
+ };
48
+ subscribers.push((event) => {
49
+ if (event.kind === "session_end")
50
+ maybeLaunchOnSessionEnd(options);
51
+ else if (event.kind === "session_start")
52
+ maybeLaunchOnSessionStart(options);
53
+ });
54
+ }
55
+ return { subscribers };
56
+ }
57
+ /**
58
+ * 清单条目的 `load` 模块说明符:本模块的绝对路径(dist 的 .js;经 tsx
59
+ * 直跑源码时为 .ts,动态 import 都能吃)。进化启用命令写清单时取此值。
60
+ */
61
+ export function evolveExtensionLoadSpecifier() {
62
+ return fileURLToPath(import.meta.url);
63
+ }
@@ -0,0 +1,95 @@
1
+ /**
2
+ * L1 doctor-oracle 前闸(trellis-evolve 任务 12.1)。
3
+ *
4
+ * 两副职责:
5
+ *
6
+ * 1. **提案类域分派**:仅 `L1_CONFIG_KINDS` 白名单内的配置类提案进入
7
+ * 提案通道;内容类提案本期不产出(等 L2 选择器执行落地)。design D8
8
+ * "L1 适用域的判定由 L4 治理层按 kind 白名单执行"——本模块就是该判定
9
+ * 点,12.2 的机械校验对提案 kind 的准入与此同源。
10
+ * 2. **确定性候选 verdict 差集**:对真实 baseline 与 staging 候选各运行
11
+ * 一组结构化检查(doctor findings、`sync --check`、secrets audit),
12
+ * 按 (稳定 id, 层归属) 集合差判定。在 L1 适用域内该差集是**唯一
13
+ * verdict**,配置类提案不运行引擎评分链。
14
+ *
15
+ * 可采纳判据(design D8,从严):无新条目 ∧ 全部目标条目被消除 ∧ 其余
16
+ * 条目不变(未声明的消除也算变化)。目标条目 = 提案 evidence 中
17
+ * `doctor-finding` 链接的 `findingId`(即 CheckEntry.id);`Proposal.target`
18
+ * 本身是被编辑面的锚点(spec:"目标为某 MCP server auth 块"、"目标路径
19
+ * 位于某仓库的 .agents/ 内"),不是检查条目——两者职责分开,本闸只消费
20
+ * findingId。目标条目不在 baseline 时"无法确认修复",按不采纳收口。
21
+ *
22
+ * staging 复现层分裂:`createStaging` 拷 scratch 全局 HOME + 临时
23
+ * workspace root(墓碑生效),提案 edits 由本模块把真实面绝对路径重定位
24
+ * 到 scratch(越界不采纳);baseline 与候选都带 `logicalWorkspaceRoot`
25
+ * 归一化,条目比较键含层归属(`workspace(<root>)` 与全局不混)。
26
+ *
27
+ * 失败收口(Risks 8):staging/应用/测量任何意外错误一律"不采纳",
28
+ * 测量结果记 `unknown`(空条目集,绝不表示为零分或通过);域外提案同样
29
+ * 不采纳。全部判定都写入运行档案(kind `"verdict"`,含 baseline 与
30
+ * candidate 两份完整测量)。
31
+ */
32
+ import type { EvidenceLink, Proposal, Verdict, VerdictEntry } from "./types.js";
33
+ import { type ApplyEditsOptions, type ApplyEditsResult, type CheckResult, type CreateStagingOptions, type RunChecksOptions, type Staging } from "../host/index.js";
34
+ /** 档案 kind:一次 L1 verdict(两份完整测量 + 判定原因)。 */
35
+ export declare const VERDICT_ARCHIVE_KIND = "verdict";
36
+ /**
37
+ * L1 适用域:配置类 kind 白名单(design D8)。名单内提案进入提案通道,
38
+ * L1 差集作唯一 verdict;名单外(含内容类——skill/记忆/指令正文等)本
39
+ * 期不产出提案,等 L2 选择器执行落地。命名与 12.2 rule-based/editor
40
+ * 提案的产出侧保持一致。
41
+ */
42
+ export declare const L1_CONFIG_KINDS: ReadonlySet<string>;
43
+ export interface ProposalDomainDispatch {
44
+ readonly domain: "config" | "content";
45
+ /** 仅配置类进入提案通道;内容类本期不产出。 */
46
+ readonly entersProposalChannel: boolean;
47
+ readonly note: string;
48
+ }
49
+ /** 提案类域分派:白名单内 = 配置类(进通道),其余 = 内容类(不产出)。 */
50
+ export declare function dispatchProposalDomain(kind: string): ProposalDomainDispatch;
51
+ /**
52
+ * 条目比较键:(稳定 id, 层归属)。"条目 SHALL 带着层归属参与比较"——
53
+ * 同一 id 在全局层与 workspace 层是两条不同记录,不互相抵消。
54
+ */
55
+ export declare function entryKey(entry: VerdictEntry): string;
56
+ export interface VerdictDiff {
57
+ /** 候选有、baseline 没有(引入的回归)。 */
58
+ readonly introduced: VerdictEntry[];
59
+ /** baseline 有、候选没有(被消除的条目)。 */
60
+ readonly resolved: VerdictEntry[];
61
+ }
62
+ /** 按 (id, layer) 做集合差;message 不参与比较(host/checks.ts 契约)。 */
63
+ export declare function diffVerdictEntries(baseline: readonly VerdictEntry[], candidate: readonly VerdictEntry[]): VerdictDiff;
64
+ /** 提案 evidence 中的目标条目(evidence 的 doctor-finding.findingId
65
+ * 即 CheckEntry.id;`Proposal.target` 是编辑面锚点,不在此列)。 */
66
+ export declare function doctorFindingTargetIds(evidence: readonly EvidenceLink[]): string[];
67
+ export interface EvaluateL1Options {
68
+ /** 真实面 HOME(档案也写在这里的 `~/.trellis/evolve/`)。 */
69
+ readonly homeDir: string;
70
+ /** 被测上下文的 workspace root;缺省/null = 纯全局上下文。 */
71
+ readonly workspaceRoot?: string | null;
72
+ readonly proposal: Proposal;
73
+ /** 注入缝(场景测试的强制报错收口);缺省走 host 真实实现。 */
74
+ readonly runChecksFn?: (options: RunChecksOptions) => Promise<CheckResult>;
75
+ readonly createStagingFn?: (options: CreateStagingOptions) => Staging;
76
+ readonly applyEditsFn?: (options: ApplyEditsOptions) => Promise<ApplyEditsResult>;
77
+ /** 时间源(毫秒);缺省 Date.now(档案打戳)。 */
78
+ readonly now?: () => number;
79
+ }
80
+ export interface L1GateResult {
81
+ readonly decision: "acceptable" | "not-acceptable";
82
+ readonly reason: string;
83
+ readonly domain: "config" | "content";
84
+ readonly verdict: Verdict;
85
+ readonly targetIds: readonly string[];
86
+ /** 引入的新条目(不可采纳时入档)。 */
87
+ readonly introduced: readonly VerdictEntry[];
88
+ readonly resolvedIds: readonly string[];
89
+ }
90
+ /**
91
+ * L1 前闸完整判定:域分派 → staging 副本(层分裂复现)→ edits 重定位并
92
+ * 应用 → baseline/候选各跑三组结构化检查 → (id, layer) 差集 → 判定并
93
+ * 入档。任何意外错误按"不采纳"收口、测量记 unknown;不吞掉入档写失败。
94
+ */
95
+ export declare function evaluateL1Proposal(options: EvaluateL1Options): Promise<L1GateResult>;
@@ -0,0 +1,295 @@
1
+ /**
2
+ * L1 doctor-oracle 前闸(trellis-evolve 任务 12.1)。
3
+ *
4
+ * 两副职责:
5
+ *
6
+ * 1. **提案类域分派**:仅 `L1_CONFIG_KINDS` 白名单内的配置类提案进入
7
+ * 提案通道;内容类提案本期不产出(等 L2 选择器执行落地)。design D8
8
+ * "L1 适用域的判定由 L4 治理层按 kind 白名单执行"——本模块就是该判定
9
+ * 点,12.2 的机械校验对提案 kind 的准入与此同源。
10
+ * 2. **确定性候选 verdict 差集**:对真实 baseline 与 staging 候选各运行
11
+ * 一组结构化检查(doctor findings、`sync --check`、secrets audit),
12
+ * 按 (稳定 id, 层归属) 集合差判定。在 L1 适用域内该差集是**唯一
13
+ * verdict**,配置类提案不运行引擎评分链。
14
+ *
15
+ * 可采纳判据(design D8,从严):无新条目 ∧ 全部目标条目被消除 ∧ 其余
16
+ * 条目不变(未声明的消除也算变化)。目标条目 = 提案 evidence 中
17
+ * `doctor-finding` 链接的 `findingId`(即 CheckEntry.id);`Proposal.target`
18
+ * 本身是被编辑面的锚点(spec:"目标为某 MCP server auth 块"、"目标路径
19
+ * 位于某仓库的 .agents/ 内"),不是检查条目——两者职责分开,本闸只消费
20
+ * findingId。目标条目不在 baseline 时"无法确认修复",按不采纳收口。
21
+ *
22
+ * staging 复现层分裂:`createStaging` 拷 scratch 全局 HOME + 临时
23
+ * workspace root(墓碑生效),提案 edits 由本模块把真实面绝对路径重定位
24
+ * 到 scratch(越界不采纳);baseline 与候选都带 `logicalWorkspaceRoot`
25
+ * 归一化,条目比较键含层归属(`workspace(<root>)` 与全局不混)。
26
+ *
27
+ * 失败收口(Risks 8):staging/应用/测量任何意外错误一律"不采纳",
28
+ * 测量结果记 `unknown`(空条目集,绝不表示为零分或通过);域外提案同样
29
+ * 不采纳。全部判定都写入运行档案(kind `"verdict"`,含 baseline 与
30
+ * candidate 两份完整测量)。
31
+ */
32
+ import { isAbsolute, join, relative, resolve, sep } from "node:path";
33
+ import { appendArchiveRecord } from "./archive.js";
34
+ import { applyEdits, createStaging, runChecks, } from "../host/index.js";
35
+ /** 档案 kind:一次 L1 verdict(两份完整测量 + 判定原因)。 */
36
+ export const VERDICT_ARCHIVE_KIND = "verdict";
37
+ /**
38
+ * L1 适用域:配置类 kind 白名单(design D8)。名单内提案进入提案通道,
39
+ * L1 差集作唯一 verdict;名单外(含内容类——skill/记忆/指令正文等)本
40
+ * 期不产出提案,等 L2 选择器执行落地。命名与 12.2 rule-based/editor
41
+ * 提案的产出侧保持一致。
42
+ */
43
+ export const L1_CONFIG_KINDS = new Set([
44
+ /** workspace 层配置面修复(如 `.codex/config.toml` 解析错误)。 */
45
+ "workspace-config-fix",
46
+ /** MCP 定义/路由等配置面修复(凭据字面值在校验阶段硬拒)。 */
47
+ "mcp-config-fix",
48
+ /** 托管投递面漂移修复(sync 可见面)。 */
49
+ "sync-config-fix",
50
+ /** secrets 策略配置修复(变量名/拒绝模式,不含任何凭据值)。 */
51
+ "secrets-policy-fix",
52
+ /** OAuth 配置元数据修复(kind/owner/client 元数据,不含凭据)。 */
53
+ "oauth-config-fix",
54
+ ]);
55
+ /** 提案类域分派:白名单内 = 配置类(进通道),其余 = 内容类(不产出)。 */
56
+ export function dispatchProposalDomain(kind) {
57
+ if (L1_CONFIG_KINDS.has(kind)) {
58
+ return {
59
+ domain: "config",
60
+ entersProposalChannel: true,
61
+ note: `kind "${kind}" 属配置类,进入提案通道(L1 差集为唯一 verdict)`,
62
+ };
63
+ }
64
+ return {
65
+ domain: "content",
66
+ entersProposalChannel: false,
67
+ note: `kind "${kind}" 不在配置类白名单,本期不产出提案(等 L2 选择器执行落地)`,
68
+ };
69
+ }
70
+ /**
71
+ * 条目比较键:(稳定 id, 层归属)。"条目 SHALL 带着层归属参与比较"——
72
+ * 同一 id 在全局层与 workspace 层是两条不同记录,不互相抵消。
73
+ */
74
+ export function entryKey(entry) {
75
+ const layerKey = entry.layer.kind === "global" ? "global" : `workspace(${entry.layer.root})`;
76
+ return `${entry.id} ${layerKey}`;
77
+ }
78
+ /** 按 (id, layer) 做集合差;message 不参与比较(host/checks.ts 契约)。 */
79
+ export function diffVerdictEntries(baseline, candidate) {
80
+ const baselineKeys = new Set(baseline.map(entryKey));
81
+ const candidateKeys = new Set(candidate.map(entryKey));
82
+ return {
83
+ introduced: candidate.filter((entry) => !baselineKeys.has(entryKey(entry))),
84
+ resolved: baseline.filter((entry) => !candidateKeys.has(entryKey(entry))),
85
+ };
86
+ }
87
+ /** 提案 evidence 中的目标条目(evidence 的 doctor-finding.findingId
88
+ * 即 CheckEntry.id;`Proposal.target` 是编辑面锚点,不在此列)。 */
89
+ export function doctorFindingTargetIds(evidence) {
90
+ return evidence
91
+ .filter((link) => link.kind === "doctor-finding")
92
+ .map((link) => link.findingId);
93
+ }
94
+ function unknownVerdict(layer) {
95
+ return { status: "unknown", layer, baseline: [], candidate: [] };
96
+ }
97
+ /** `path` 是否落在某个真实面根之下(不含根本身)。 */
98
+ function isInsideRoots(path, roots) {
99
+ const resolved = resolve(path);
100
+ for (const root of roots) {
101
+ if (!root)
102
+ continue;
103
+ const rel = relative(resolve(root), resolved);
104
+ if (rel !== "" && rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel))
105
+ return true;
106
+ }
107
+ return false;
108
+ }
109
+ /** 真实面绝对路径 → staging scratch 对应路径;越界抛错(不采纳)。
110
+ * workspace 根优先(可能嵌套在 HOME 下,与 checks 的归一化同序)。 */
111
+ function relocateIntoStaging(path, ctx) {
112
+ const roots = [];
113
+ if (ctx.workspaceRoot && ctx.staging.workspaceRoot) {
114
+ roots.push({ real: ctx.workspaceRoot, scratch: ctx.staging.workspaceRoot });
115
+ }
116
+ roots.push({ real: ctx.homeDir, scratch: ctx.staging.homeDir });
117
+ const resolved = resolve(path);
118
+ for (const { real, scratch } of roots) {
119
+ if (!scratch)
120
+ continue;
121
+ const rel = relative(resolve(real), resolved);
122
+ if (rel !== "" && rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel)) {
123
+ return join(scratch, rel);
124
+ }
125
+ }
126
+ throw new Error(`edit path escapes the measured surface: ${path}`);
127
+ }
128
+ /** 符号链接 target:落在真实面内才重定位到 scratch(链接不指向真实面);
129
+ * 相对路径与面外绝对路径保持原义(校验阶段的职责不在此)。 */
130
+ function relocateTargetIfInside(target, ctx) {
131
+ if (!isAbsolute(target))
132
+ return target;
133
+ if (!isInsideRoots(target, [ctx.workspaceRoot, ctx.homeDir]))
134
+ return target;
135
+ return relocateIntoStaging(target, ctx);
136
+ }
137
+ function relocateEdits(edits, ctx) {
138
+ return edits.map((edit) => {
139
+ const path = relocateIntoStaging(edit.path, ctx);
140
+ switch (edit.op) {
141
+ case "write-file":
142
+ case "remove-symlink":
143
+ return { ...edit, path };
144
+ case "create-symlink":
145
+ case "repair-symlink":
146
+ return { ...edit, path, target: relocateTargetIfInside(edit.target, ctx) };
147
+ }
148
+ });
149
+ }
150
+ /**
151
+ * L1 前闸完整判定:域分派 → staging 副本(层分裂复现)→ edits 重定位并
152
+ * 应用 → baseline/候选各跑三组结构化检查 → (id, layer) 差集 → 判定并
153
+ * 入档。任何意外错误按"不采纳"收口、测量记 unknown;不吞掉入档写失败。
154
+ */
155
+ export async function evaluateL1Proposal(options) {
156
+ const homeDir = options.homeDir;
157
+ const workspaceRoot = options.workspaceRoot ?? null;
158
+ const proposal = options.proposal;
159
+ const stamp = () => new Date((options.now ?? Date.now)());
160
+ const dispatch = dispatchProposalDomain(proposal.kind);
161
+ const settle = (outcome) => {
162
+ const result = {
163
+ decision: outcome.decision,
164
+ reason: outcome.reason,
165
+ domain: dispatch.domain,
166
+ verdict: outcome.verdict,
167
+ targetIds: outcome.targetIds,
168
+ introduced: outcome.introduced,
169
+ resolvedIds: outcome.resolvedIds,
170
+ };
171
+ // 两份 verdict 入档:baseline 与 candidate 完整条目集 + 判定原因。
172
+ appendArchiveRecord(homeDir, {
173
+ kind: VERDICT_ARCHIVE_KIND,
174
+ payload: {
175
+ proposalId: proposal.id,
176
+ kind: proposal.kind,
177
+ domain: dispatch.domain,
178
+ decision: outcome.decision,
179
+ reason: outcome.reason,
180
+ status: outcome.verdict.status,
181
+ layer: outcome.verdict.layer,
182
+ targetIds: outcome.targetIds,
183
+ introduced: outcome.introduced,
184
+ resolvedIds: outcome.resolvedIds,
185
+ baseline: outcome.verdict.baseline,
186
+ candidate: outcome.verdict.candidate,
187
+ },
188
+ }, stamp);
189
+ return result;
190
+ };
191
+ // 域分派:内容类本期不产出(等 L2)——不测量、不通过。
192
+ if (!dispatch.entersProposalChannel) {
193
+ return settle({
194
+ decision: "not-acceptable",
195
+ reason: dispatch.note,
196
+ verdict: unknownVerdict(proposal.layer),
197
+ targetIds: [],
198
+ introduced: [],
199
+ resolvedIds: [],
200
+ });
201
+ }
202
+ const targetIds = doctorFindingTargetIds(proposal.evidence);
203
+ if (targetIds.length === 0) {
204
+ return settle({
205
+ decision: "not-acceptable",
206
+ reason: "提案证据中没有 doctor-finding 目标条目,无法确认修复",
207
+ verdict: unknownVerdict(proposal.layer),
208
+ targetIds: [],
209
+ introduced: [],
210
+ resolvedIds: [],
211
+ });
212
+ }
213
+ const runChecksFn = options.runChecksFn ?? runChecks;
214
+ const createStagingFn = options.createStagingFn ?? createStaging;
215
+ const applyEditsFn = options.applyEditsFn ?? applyEdits;
216
+ let staging = null;
217
+ try {
218
+ staging = createStagingFn({ homeDir, workspaceRoot });
219
+ const edits = relocateEdits(proposal.edits, { homeDir, workspaceRoot, staging });
220
+ if (edits.length > 0) {
221
+ await applyEditsFn({ homeDir: staging.homeDir, edits, command: "evolve-l1-gate" });
222
+ }
223
+ // baseline:真实面;candidate:staging scratch。两侧都带
224
+ // logicalWorkspaceRoot 归一化,scratch 路径不进比较身份。
225
+ const baselineResult = await runChecksFn({
226
+ homeDir,
227
+ workspaceRoot,
228
+ logicalWorkspaceRoot: workspaceRoot,
229
+ });
230
+ const candidateResult = await runChecksFn({
231
+ homeDir: staging.homeDir,
232
+ workspaceRoot: staging.workspaceRoot,
233
+ logicalWorkspaceRoot: staging.logicalWorkspaceRoot,
234
+ });
235
+ const baseline = baselineResult.entries;
236
+ const candidate = candidateResult.entries;
237
+ const diff = diffVerdictEntries(baseline, candidate);
238
+ const targetIdSet = new Set(targetIds);
239
+ const baselineIds = new Set(baseline.map((entry) => entry.id));
240
+ const missingTargets = targetIds.filter((id) => !baselineIds.has(id));
241
+ const targetKeys = new Set(baseline.filter((entry) => targetIdSet.has(entry.id)).map(entryKey));
242
+ const resolvedKeys = new Set(diff.resolved.map(entryKey));
243
+ const resolvedIds = diff.resolved.map((entry) => entry.id);
244
+ const okVerdict = { status: "ok", layer: proposal.layer, baseline, candidate };
245
+ const common = { verdict: okVerdict, targetIds, introduced: diff.introduced, resolvedIds };
246
+ // 目标条目必须在 baseline 里——不在则"无法确认修复"。
247
+ if (missingTargets.length > 0) {
248
+ return settle({
249
+ ...common,
250
+ decision: "not-acceptable",
251
+ reason: `目标条目不在 baseline(${missingTargets.join(", ")}),无法确认修复`,
252
+ });
253
+ }
254
+ // 无新条目。
255
+ if (diff.introduced.length > 0) {
256
+ return settle({
257
+ ...common,
258
+ decision: "not-acceptable",
259
+ reason: `候选引入 ${diff.introduced.length} 个 baseline 中不存在的新条目`,
260
+ });
261
+ }
262
+ // 目标条目被消除 ∧ 其余不变(未声明的消除也算变化)。
263
+ const targetsAllResolved = [...targetKeys].every((key) => resolvedKeys.has(key));
264
+ const noExtraResolved = resolvedKeys.size === targetKeys.size && [...resolvedKeys].every((key) => targetKeys.has(key));
265
+ if (!targetsAllResolved) {
266
+ return settle({ ...common, decision: "not-acceptable", reason: "目标条目未被完全消除" });
267
+ }
268
+ if (!noExtraResolved) {
269
+ return settle({
270
+ ...common,
271
+ decision: "not-acceptable",
272
+ reason: "候选消除了未声明的条目,其余不满足不变约束",
273
+ });
274
+ }
275
+ return settle({
276
+ ...common,
277
+ decision: "acceptable",
278
+ reason: "候选消除全部目标条目,无新条目,其余不变",
279
+ });
280
+ }
281
+ catch (err) {
282
+ // Risks 8:意外错误一律不采纳,测量记 unknown,绝不表示为通过。
283
+ return settle({
284
+ decision: "not-acceptable",
285
+ reason: `测量/应用失败,按'不采纳'收口: ${err instanceof Error ? err.message : String(err)}`,
286
+ verdict: unknownVerdict(proposal.layer),
287
+ targetIds,
288
+ introduced: [],
289
+ resolvedIds: [],
290
+ });
291
+ }
292
+ finally {
293
+ staging?.dispose();
294
+ }
295
+ }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * 历史数据导入与幂等回填(trellis-evolve-run-backfill 任务 3.1/3.2)。
3
+ *
4
+ * 把用户**显式选定**的既有 session 数据回填为观察信号,供后续作业按
5
+ * 既有水位线消费。设计(D4):
6
+ * - 适配器契约 `{ name, sniff, extract }`,首个适配器 `claude-code-jsonl`
7
+ * (Claude Code 会话 transcript JSONL);
8
+ * - 白名单元数据:timestamp / agent / 会话 / 工具名 / 耗时 / 结果类别 /
9
+ * 层归属 —— 入参、结果载体与自由文本正文一律不入库(与 recorder 的
10
+ * 字段白名单同一条线);所有字符串字段写入前经 `eraseSecrets`;
11
+ * - 台账幂等:`evolve/imports/<sha256(内容)>.json` 记源摘要与已导条目
12
+ * 数,同一源重复导入为空操作;stableId `import:<digest 前 12 位>:<行号>`
13
+ * 确定性(重复导入/重复消费不产生重复信号或重复提案);
14
+ * - 写入复用 recorder 的批量 append(0600/0700、并发安全);整批先
15
+ * extract 校验、后写入,任何源不可解析即非零退出且零写入。
16
+ *
17
+ * 字段缺省语义(格式缺字段时):ImportedEntry 对应字段记 null —— 耗时
18
+ * 缺省 0(白名单无 unknown 态),结果类别缺省 "ok"(无错误指示),agent
19
+ * 缺省适配器常量 "claude-code",会话缺省源文件名(D4"会话 id=文件名")。
20
+ * 无工具名的记录(文本轮次等)不写为 tool-call 信号,计入 skippedNoTool。
21
+ * 结果类别按记录自带 is_error 归类(每条记录独立映射);tool_use 与其
22
+ * tool_result 在 transcript 中是不同记录,跨记录关联留待适配器后续细化。
23
+ */
24
+ /** 导入条目的白名单元数据(缺字段缺 null;写入侧按文档化缺省补全)。 */
25
+ export interface ImportedEntry {
26
+ readonly timestampMs: number | null;
27
+ readonly agent: string | null;
28
+ readonly session: string | null;
29
+ readonly tool: string | null;
30
+ readonly durationMs: number | null;
31
+ readonly result: "ok" | "error" | "timeout" | null;
32
+ }
33
+ export interface ExtractedEntry {
34
+ /** 源文件物理行号(1 起):stableId 序号,确定性不受过滤影响。 */
35
+ readonly lineNo: number;
36
+ readonly entry: ImportedEntry;
37
+ }
38
+ export interface ExtractResult {
39
+ readonly entries: readonly ExtractedEntry[];
40
+ /** 无法解析的坏行数(跳过计数,与读取侧容错同源)。 */
41
+ readonly badLines: number;
42
+ /** 无工具名的记录数(文本轮次等,不写为 tool-call 信号)。 */
43
+ readonly skippedNoTool: number;
44
+ }
45
+ /** 源适配器契约:sniff 判定形态,extract 抽白名单元数据(不写盘)。 */
46
+ export interface HistoryAdapter {
47
+ readonly name: string;
48
+ sniff(path: string): boolean;
49
+ extract(path: string): ExtractResult;
50
+ }
51
+ /** 源不可解析(不存在/不可读/不匹配任何适配器):整批零写入收口。 */
52
+ export declare class ImportSourceError extends Error {
53
+ readonly path: string;
54
+ constructor(path: string, reason: string);
55
+ }
56
+ /** 首个适配器:Claude Code 会话 transcript(JSONL)。 */
57
+ export declare const claudeCodeJsonlAdapter: HistoryAdapter;
58
+ /** 已注册适配器(按序 sniff;契约先行,更多源后置)。 */
59
+ export declare const HISTORY_ADAPTERS: readonly HistoryAdapter[];
60
+ export interface ImportHistoryOptions {
61
+ readonly homeDir: string;
62
+ /** 显式选定的源路径(仅此处理;不扫描目录、不自动发现)。 */
63
+ readonly paths: readonly string[];
64
+ /** `--workspace` 层归属;缺省全局层。 */
65
+ readonly workspaceRoot?: string;
66
+ /** 时间源(毫秒);缺省 Date.now(采集时刻与台账打戳)。 */
67
+ readonly now?: () => number;
68
+ }
69
+ export interface ImportedSourceResult {
70
+ readonly path: string;
71
+ readonly sourceDigest: string;
72
+ /** 本次写入条目数(已记账源为 0)。 */
73
+ readonly entries: number;
74
+ readonly badLines: number;
75
+ readonly skippedNoTool: number;
76
+ /** 已记账源(重复导入为空操作)。 */
77
+ readonly duplicate: boolean;
78
+ }
79
+ export interface ImportHistoryResult {
80
+ readonly sources: readonly ImportedSourceResult[];
81
+ readonly written: number;
82
+ readonly signalFile: string | null;
83
+ }
84
+ /** 台账文件路径(按源内容摘要命名;内容变 = 新源)。 */
85
+ export declare function importLedgerPath(homeDir: string, digest: string): string;
86
+ /**
87
+ * 整批导入:先对全部源 sniff + extract 校验(任何源不可解析即抛
88
+ * `ImportSourceError`,零写入),后一次性写信号与台账(台账最后记账,
89
+ * 中断重跑安全:未记账则重导,提案去重兜底)。不触发任何作业。
90
+ */
91
+ export declare function importHistory(options: ImportHistoryOptions): ImportHistoryResult;