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
@@ -9,6 +9,7 @@ import { existsSync, readFileSync } from "node:fs";
9
9
  import { homedir } from "node:os";
10
10
  import { join } from "node:path";
11
11
  import { loadCanonicalSource, ensureGitignoreEntry, ensureShellEnvSource, upsertServerYaml, writeSecretsPolicyExtraction } from "../core/canonical.js";
12
+ import { deliveredName } from "../lib/secretEnv.js";
12
13
  import { openBackupSession } from "../lib/backup.js";
13
14
  import { parseDotenv, writeLocalSecretValue } from "../lib/secretEnv.js";
14
15
  import { collectMcpSyncReport, printReport as printMcpSyncReport } from "./mcp.js";
@@ -115,7 +116,7 @@ function semanticIdentity(def) {
115
116
  enabled: def.enabled,
116
117
  agents: def.agents ? sorted(def.agents) : undefined,
117
118
  staticEnv: def.staticEnv,
118
- envKeys: sorted([...(def.env ?? []), ...Object.keys(def.envAliases ?? {})]),
119
+ envKeys: sorted([...(def.env ?? []).map(deliveredName), ...Object.keys(def.envAliases ?? {})]),
119
120
  headerKeys: sorted(Object.keys(def.headers ?? {})),
120
121
  });
121
122
  }
@@ -35,7 +35,7 @@ export interface SecretsFinding {
35
35
  * `~/.trellis/mcp/servers.yaml` itself (see module doc comment). */
36
36
  agent: AgentId | "environment" | "canonical";
37
37
  file: string;
38
- kind: "literal-secret" | "unexpected-var-name" | "missing-env-value";
38
+ kind: "literal-secret" | "unexpected-var-name" | "missing-env-value" | "malformed-reference";
39
39
  detail: string;
40
40
  }
41
41
  export interface SecretsAuditReport {
@@ -29,7 +29,7 @@ import { KiroAdapter } from "../adapters/kiro.js";
29
29
  import { ZcodeAdapter } from "../adapters/zcode.js";
30
30
  import { declaredEnvNames, extractJsonEnvVarNames, extractTomlEnvVarNames, extractZcodeEnvVarNames } from "../lib/envVarNames.js";
31
31
  import { resolveZcodeProfile } from "../probes/zcode.js";
32
- import { resolveSecretEnv } from "../lib/secretEnv.js";
32
+ import { parseEnvEntry, resolveSecretEnv } from "../lib/secretEnv.js";
33
33
  /** Only agents in `managedAgents` — a present-but-unmanaged agent's config
34
34
  * file is never even read (trellis-managed-agents), same restriction as
35
35
  * `sync`/`mcp sync`. pi has no static, generated MCP config file to audit
@@ -49,7 +49,7 @@ function auditedAgents(homeDir, managedAgents) {
49
49
  * a problem regardless of which agents' `servers.yaml` `agents:` field
50
50
  * would route it to (trellis-secrets-env-management design.md D3).
51
51
  */
52
- function findMissingEnvValues(servers, policy) {
52
+ async function findMissingEnvValues(servers, policy) {
53
53
  const names = new Set();
54
54
  for (const def of Object.values(servers)) {
55
55
  for (const name of declaredEnvNames(def))
@@ -57,16 +57,39 @@ function findMissingEnvValues(servers, policy) {
57
57
  }
58
58
  if (names.size === 0)
59
59
  return [];
60
- const resolved = resolveSecretEnv([...names], policy);
61
- const source = policy.envFile ?? "process environment";
62
- return [...names]
63
- .filter((name) => !resolved[name])
64
- .map((name) => ({
65
- agent: "environment",
66
- file: source,
67
- kind: "missing-env-value",
68
- detail: `"${name}" is declared by a canonical MCP server's env but has no resolvable value`,
69
- }));
60
+ // Malformed `mxenv://` entries are findings in their own right — never
61
+ // skipped, never treated as bare names (mx-env-secret-source spec).
62
+ const entries = [...names];
63
+ const parsed = entries.map(parseEnvEntry);
64
+ const findings = [];
65
+ for (const p of parsed) {
66
+ if (p.kind === "invalid") {
67
+ findings.push({
68
+ agent: "environment",
69
+ file: policy.envFile ?? "process environment",
70
+ kind: "malformed-reference",
71
+ detail: `"${p.entry}" is not a valid mxenv://<profile>/<KEY> reference`,
72
+ });
73
+ }
74
+ }
75
+ const resolvable = entries.filter((_, i) => parsed[i].kind !== "invalid");
76
+ if (resolvable.length === 0)
77
+ return findings;
78
+ // Findings name the DELIVERED name (the KEY segment for a reference).
79
+ const resolved = await resolveSecretEnv(resolvable, policy);
80
+ for (const entry of resolvable) {
81
+ const p = parseEnvEntry(entry);
82
+ const delivered = p.kind === "invalid" ? entry : p.delivered;
83
+ if (!resolved[delivered]) {
84
+ findings.push({
85
+ agent: "environment",
86
+ file: p.kind === "ref" ? `the mxenv composition (profile "${p.profile}")` : (policy.envFile ?? "process environment"),
87
+ kind: "missing-env-value",
88
+ detail: `"${delivered}" is declared by a canonical MCP server's env but has no resolvable value`,
89
+ });
90
+ }
91
+ }
92
+ return findings;
70
93
  }
71
94
  /**
72
95
  * Reject-pattern-only scan of canonical's own `mcp/servers.yaml` (design.md
@@ -118,7 +141,7 @@ export async function collectSecretsAuditReport(opts = {}) {
118
141
  const content = readFileSync(file, "utf-8");
119
142
  findings.push(...auditFile(agent.id, file, content, canonical.secretsPolicy, agent.extractNames));
120
143
  }
121
- findings.push(...findMissingEnvValues(canonical.mcp.servers, canonical.secretsPolicy));
144
+ findings.push(...await findMissingEnvValues(canonical.mcp.servers, canonical.secretsPolicy));
122
145
  findings.push(...auditCanonicalServersYaml(homeDir, canonical.secretsPolicy));
123
146
  return { findings };
124
147
  }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * agent 只读接口(trellis-evolve 任务 12.4,spec"pending 提案对 agent
3
+ * 可见")。
4
+ *
5
+ * 暴露给被进化 agent 的进化接口只有两类:读 pending 摘要(本模块)与
6
+ * 上报用户纠正(`report.ts`)。本模块**没有任何**批准、拒绝、应用或写
7
+ * 入操作,也不 re-export 它们 —— 批准只走管理面命令的交互式终端
8
+ * (spec"批准只走带外通道")。返回内容:摘要、证据与风险级别;文本经
9
+ * secrets 擦除(纵深,存储层已拒凭据形态内容),不含凭据材料、不含
10
+ * 编辑正文。
11
+ */
12
+ import type { ProposalStatus } from "./pending.js";
13
+ import type { RiskClass } from "./types.js";
14
+ /** 证据的 agent 可见描述(只读;ref 为稳定标识,已擦除)。 */
15
+ export interface AgentEvidenceView {
16
+ readonly kind: string;
17
+ readonly ref: string;
18
+ }
19
+ /** 一条 pending 提案的 agent 可见摘要。 */
20
+ export interface AgentPendingSummary {
21
+ readonly id: string;
22
+ readonly summary: string;
23
+ readonly riskClass: RiskClass;
24
+ readonly status: ProposalStatus;
25
+ readonly evidence: readonly AgentEvidenceView[];
26
+ readonly createdAt: number;
27
+ }
28
+ /**
29
+ * 列出全部 pending 提案的只读摘要。存在几条返回几条(spec 场景:两条
30
+ * pending → 两条摘要、证据与风险级别)。只读:不建目录、不写档案。
31
+ */
32
+ export declare function listPendingSummaries(homeDir: string): AgentPendingSummary[];
@@ -0,0 +1,34 @@
1
+ /**
2
+ * agent 只读接口(trellis-evolve 任务 12.4,spec"pending 提案对 agent
3
+ * 可见")。
4
+ *
5
+ * 暴露给被进化 agent 的进化接口只有两类:读 pending 摘要(本模块)与
6
+ * 上报用户纠正(`report.ts`)。本模块**没有任何**批准、拒绝、应用或写
7
+ * 入操作,也不 re-export 它们 —— 批准只走管理面命令的交互式终端
8
+ * (spec"批准只走带外通道")。返回内容:摘要、证据与风险级别;文本经
9
+ * secrets 擦除(纵深,存储层已拒凭据形态内容),不含凭据材料、不含
10
+ * 编辑正文。
11
+ */
12
+ import { eraseSecrets } from "./erase.js";
13
+ import { readProposalsByStatus } from "./pending.js";
14
+ function evidenceRef(link) {
15
+ const raw = link.kind === "run" ? link.runId : link.kind === "doctor-finding" ? link.findingId : link.signalId;
16
+ return eraseSecrets(raw);
17
+ }
18
+ /**
19
+ * 列出全部 pending 提案的只读摘要。存在几条返回几条(spec 场景:两条
20
+ * pending → 两条摘要、证据与风险级别)。只读:不建目录、不写档案。
21
+ */
22
+ export function listPendingSummaries(homeDir) {
23
+ return readProposalsByStatus(homeDir, "pending").map((record) => ({
24
+ id: record.id,
25
+ summary: eraseSecrets(record.proposal.summary),
26
+ riskClass: record.riskClass,
27
+ status: record.status,
28
+ evidence: record.proposal.evidence.map((link) => ({
29
+ kind: link.kind,
30
+ ref: evidenceRef(link),
31
+ })),
32
+ createdAt: record.createdAt,
33
+ }));
34
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * 应用与生效(trellis-evolve 任务 12.5)。
3
+ *
4
+ * 把**已批准**提案的全部编辑写入托管备份会话的同一会话内
5
+ * (spec"原子运行与回滚":一次运行的全部写入在同一备份会话,一次回滚
6
+ * 恢复运行前状态),并把备份会话标识记入运行档案(kind `apply-run`)。
7
+ *
8
+ * 采纳后的生效边界(spec"采纳的生效边界"):
9
+ * - runtime 投递("events")的 agent:后续会话读取 canonical 时自然
10
+ * 生效,无需同步(9.4 定案:runtime provider 每请求实时读盘);
11
+ * - 仅原生渲染("doctor-only")的 agent:触发渲染同步,报告同步结果。
12
+ *
13
+ * 同步经注入缝触发:`triggerSync` 由管理面/作业层注入 `runSync` 包装
14
+ * (消费者边界 10.3:本目录不 import `commands/*`),测试注入桩。缺省
15
+ * 未注入时只报告生效时点,不假装同步已经发生。
16
+ *
17
+ * 已应用查重以档案为准(apply-run 的 proposalIds):重复调用跳过,
18
+ * 不开第二个会话;部分失败的 HostEditError 批次照记档案(failed 标记,
19
+ * 不计入查重),提案不标已应用,重试重放(编辑幂等:write-file 整文件
20
+ * 写,remove-symlink 对已不存在目标视为成功)。
21
+ *
22
+ * 生效时点说明(effects)与同步结果随 apply-run 档案入档(trellis-
23
+ * evolve-review-fixes:此前算完即弃,报告不可回放)。同步触发失败收口
24
+ * 为效果文本并入档,不丢应用档案——编辑已写入,同步可人工补。
25
+ */
26
+ /** 运行档案 kind:一次应用运行(含备份会话标识与提案集合)。 */
27
+ export declare const APPLY_ARCHIVE_KIND = "apply-run";
28
+ /** 与 host/levels 的 EvolveObservation 同语义:由调用方按 delivery
29
+ * 分类注入("events" = runtime 交付;"doctor-only" = 仅原生渲染)。 */
30
+ export type ApplyObservation = "events" | "doctor-only";
31
+ export interface ApplyAgent {
32
+ readonly agent: string;
33
+ readonly observation: ApplyObservation;
34
+ }
35
+ export interface AgentEffectiveness {
36
+ readonly agent: string;
37
+ readonly observation: ApplyObservation;
38
+ /** 人类可读的生效时点说明(应用后向用户展示)。 */
39
+ readonly effect: string;
40
+ }
41
+ export interface ApplyApprovedOptions {
42
+ readonly homeDir: string;
43
+ /** 托管 agent 及其交付形态;缺省空(只应用,不逐 agent 说明)。 */
44
+ readonly agents?: readonly ApplyAgent[];
45
+ /** 原生渲染 agent 的同步触发注入缝;存在 doctor-only agent 且本次
46
+ * 有编辑被应用时调用一次。返回值的 exitCode 进入结果与生效说明。 */
47
+ readonly triggerSync?: () => Promise<{
48
+ exitCode: number;
49
+ }>;
50
+ /** 时间源;缺省 Date.now(仅透传档案打戳用,档案自带 now 缺省)。 */
51
+ readonly now?: () => Date;
52
+ }
53
+ export interface ApplyApprovedResult {
54
+ /** 本次备份会话标识;无未应用的已批准提案时 null(不开会话)。 */
55
+ readonly runId: string | null;
56
+ readonly appliedProposalIds: string[];
57
+ readonly alreadyAppliedIds: string[];
58
+ readonly effects: AgentEffectiveness[];
59
+ /** 原生渲染同步的退出码;未触发为 null。 */
60
+ readonly syncExitCode: number | null;
61
+ /** 同步触发抛错时的错误文本(收口为效果文本,不中断应用);无为 null。 */
62
+ readonly syncError: string | null;
63
+ /** 应用前重校验不通过而跳过的提案 id(纵深防御,见实现注释)。 */
64
+ readonly skippedInvalidIds: string[];
65
+ }
66
+ /**
67
+ * 应用全部已批准且未应用的提案:编辑合并进**同一个**备份会话写入,
68
+ * 档案记会话标识,再按 agent 交付形态说明生效时点并对原生渲染 agent
69
+ * 触发同步。无事可做时零副作用(不开会话、不触发同步、不写档案)。
70
+ */
71
+ export declare function applyApprovedProposals(options: ApplyApprovedOptions): Promise<ApplyApprovedResult>;
@@ -0,0 +1,162 @@
1
+ /**
2
+ * 应用与生效(trellis-evolve 任务 12.5)。
3
+ *
4
+ * 把**已批准**提案的全部编辑写入托管备份会话的同一会话内
5
+ * (spec"原子运行与回滚":一次运行的全部写入在同一备份会话,一次回滚
6
+ * 恢复运行前状态),并把备份会话标识记入运行档案(kind `apply-run`)。
7
+ *
8
+ * 采纳后的生效边界(spec"采纳的生效边界"):
9
+ * - runtime 投递("events")的 agent:后续会话读取 canonical 时自然
10
+ * 生效,无需同步(9.4 定案:runtime provider 每请求实时读盘);
11
+ * - 仅原生渲染("doctor-only")的 agent:触发渲染同步,报告同步结果。
12
+ *
13
+ * 同步经注入缝触发:`triggerSync` 由管理面/作业层注入 `runSync` 包装
14
+ * (消费者边界 10.3:本目录不 import `commands/*`),测试注入桩。缺省
15
+ * 未注入时只报告生效时点,不假装同步已经发生。
16
+ *
17
+ * 已应用查重以档案为准(apply-run 的 proposalIds):重复调用跳过,
18
+ * 不开第二个会话;部分失败的 HostEditError 批次照记档案(failed 标记,
19
+ * 不计入查重),提案不标已应用,重试重放(编辑幂等:write-file 整文件
20
+ * 写,remove-symlink 对已不存在目标视为成功)。
21
+ *
22
+ * 生效时点说明(effects)与同步结果随 apply-run 档案入档(trellis-
23
+ * evolve-review-fixes:此前算完即弃,报告不可回放)。同步触发失败收口
24
+ * 为效果文本并入档,不丢应用档案——编辑已写入,同步可人工补。
25
+ */
26
+ import { appendArchiveRecord, readArchiveRecords } from "./archive.js";
27
+ import { readProposalsByStatus } from "./pending.js";
28
+ import { validateProposal } from "./validate.js";
29
+ import { applyEdits, HostEditError } from "../host/index.js";
30
+ /** 运行档案 kind:一次应用运行(含备份会话标识与提案集合)。 */
31
+ export const APPLY_ARCHIVE_KIND = "apply-run";
32
+ function appliedIdsFromArchive(homeDir) {
33
+ const applied = new Set();
34
+ for (const entry of readArchiveRecords(homeDir)) {
35
+ if (entry.kind !== APPLY_ARCHIVE_KIND)
36
+ continue;
37
+ const payload = entry.payload;
38
+ // 部分失败的批次(failed)不计入"已应用":重试按幂等重放全部编辑。
39
+ if (payload === null || payload.failed === true || !Array.isArray(payload.proposalIds))
40
+ continue;
41
+ for (const id of payload.proposalIds) {
42
+ if (typeof id === "string")
43
+ applied.add(id);
44
+ }
45
+ }
46
+ return applied;
47
+ }
48
+ function effectivenessFor(agent, syncExitCode, syncTriggered, syncError) {
49
+ if (agent.observation === "events") {
50
+ return {
51
+ agent: agent.agent,
52
+ observation: agent.observation,
53
+ effect: "后续会话读取 canonical 时自然生效(runtime 投递,无需同步)",
54
+ };
55
+ }
56
+ if (syncTriggered) {
57
+ return {
58
+ agent: agent.agent,
59
+ observation: agent.observation,
60
+ effect: `已触发渲染同步(exit ${String(syncExitCode ?? "?")}),同步完成后生效`,
61
+ };
62
+ }
63
+ if (syncError !== null) {
64
+ return {
65
+ agent: agent.agent,
66
+ observation: agent.observation,
67
+ effect: `触发渲染同步失败:${syncError};需运行 \`trellis sync\` 后生效`,
68
+ };
69
+ }
70
+ return {
71
+ agent: agent.agent,
72
+ observation: agent.observation,
73
+ effect: "需运行 `trellis sync` 后生效(本次未提供同步触发)",
74
+ };
75
+ }
76
+ /**
77
+ * 应用全部已批准且未应用的提案:编辑合并进**同一个**备份会话写入,
78
+ * 档案记会话标识,再按 agent 交付形态说明生效时点并对原生渲染 agent
79
+ * 触发同步。无事可做时零副作用(不开会话、不触发同步、不写档案)。
80
+ */
81
+ export async function applyApprovedProposals(options) {
82
+ const { homeDir } = options;
83
+ const alreadyApplied = appliedIdsFromArchive(homeDir);
84
+ const approved = readProposalsByStatus(homeDir, "approved");
85
+ const appliedProposalIds = [];
86
+ const alreadyAppliedIds = [];
87
+ const skippedInvalidIds = [];
88
+ const edits = [];
89
+ for (const record of approved) {
90
+ if (alreadyApplied.has(record.id)) {
91
+ alreadyAppliedIds.push(record.id);
92
+ continue;
93
+ }
94
+ // 纵深防御:应用前重跑机械校验。pending 是可变磁盘状态,approve 只
95
+ // 翻 status 字段——入队时的白名单/路径穿越约束在这一步重放,磁盘
96
+ // 漂移或被改写的编辑到不了写入面。
97
+ const validation = validateProposal(record.proposal, { homeDir });
98
+ if (!validation.ok) {
99
+ skippedInvalidIds.push(record.id);
100
+ continue;
101
+ }
102
+ appliedProposalIds.push(record.id);
103
+ for (const edit of record.proposal.edits)
104
+ edits.push(edit);
105
+ }
106
+ if (appliedProposalIds.length === 0) {
107
+ return {
108
+ runId: null,
109
+ appliedProposalIds: [],
110
+ alreadyAppliedIds,
111
+ effects: [],
112
+ syncExitCode: null,
113
+ syncError: null,
114
+ skippedInvalidIds,
115
+ };
116
+ }
117
+ let runId;
118
+ try {
119
+ ({ runId } = await applyEdits({ homeDir, edits, command: "evolve-apply" }));
120
+ }
121
+ catch (err) {
122
+ if (err instanceof HostEditError) {
123
+ // 部分失败也落 apply-run 档案(failed 不计入已应用查重):runId
124
+ // 可经 `trellis rollback` 撤销已应用部分,审计不留空白。
125
+ appendArchiveRecord(homeDir, {
126
+ kind: APPLY_ARCHIVE_KIND,
127
+ payload: { runId: err.runId, proposalIds: appliedProposalIds, failed: true, error: err.message },
128
+ }, options.now ?? (() => new Date()));
129
+ }
130
+ throw err;
131
+ }
132
+ // 生效:原生渲染 agent 触发同步(runtime agent 无需——9.4 实时读取)。
133
+ // 同步失败收口为效果文本(syncError),不丢应用档案:编辑已写入,同步可人工补。
134
+ const agents = options.agents ?? [];
135
+ const needsSync = agents.some((a) => a.observation === "doctor-only");
136
+ let syncExitCode = null;
137
+ let syncTriggered = false;
138
+ let syncError = null;
139
+ if (needsSync && options.triggerSync !== undefined) {
140
+ try {
141
+ syncExitCode = (await options.triggerSync()).exitCode;
142
+ syncTriggered = true;
143
+ }
144
+ catch (err) {
145
+ syncError = err instanceof Error ? err.message : String(err);
146
+ }
147
+ }
148
+ const effects = agents.map((agent) => effectivenessFor(agent, syncExitCode, syncTriggered, syncError));
149
+ // 档案记会话标识 + 逐 agent 生效时点 + 同步结果(review-fixes ❌3:此前
150
+ // effects 算完即弃);单条 apply-run 记录承载全部应用元数据,报告回放同源。
151
+ appendArchiveRecord(homeDir, {
152
+ kind: APPLY_ARCHIVE_KIND,
153
+ payload: {
154
+ runId,
155
+ proposalIds: appliedProposalIds,
156
+ effects,
157
+ syncExitCode,
158
+ ...(syncError !== null ? { syncError } : {}),
159
+ },
160
+ }, options.now ?? (() => new Date()));
161
+ return { runId, appliedProposalIds, alreadyAppliedIds, effects, syncExitCode, syncError, skippedInvalidIds };
162
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * 运行档案写入器(trellis-evolve 任务 10.4)。
3
+ *
4
+ * `archive/records.jsonl` 只追加 JSONL:提案/探针/verdict/备份会话标识/
5
+ * L2 比较记录按发生顺序落行,永不覆盖、不重排、不去重。读取侧严格解析
6
+ * ——档案是治理审计数据,坏行必须带行号抛出而不是静默跳过。
7
+ *
8
+ * 记录格式:`{ kind, recordedAt, payload }`。`kind` 开放(各任务逐步收
9
+ * 敛:proposal/verdict/lesson/L2 比较等),`payload` 为 JSON 安全的具体
10
+ * 记录;`recordedAt` 由写入器打戳(ISO 8601)。
11
+ */
12
+ export interface ArchiveRecord {
13
+ readonly kind: string;
14
+ readonly payload: unknown;
15
+ }
16
+ export interface ArchiveEntry extends ArchiveRecord {
17
+ readonly recordedAt: string;
18
+ }
19
+ export declare function archiveRecordsPath(homeDir: string): string;
20
+ /** 只追加一条记录,返回落盘后的完整条目(含打戳)。 */
21
+ export declare function appendArchiveRecord(homeDir: string, record: ArchiveRecord, now?: () => Date): ArchiveEntry;
22
+ /** 读回全部档案条目;文件不存在返回 [](读取零副作用,不建目录)。 */
23
+ export declare function readArchiveRecords(homeDir: string): ArchiveEntry[];
@@ -0,0 +1,58 @@
1
+ /**
2
+ * 运行档案写入器(trellis-evolve 任务 10.4)。
3
+ *
4
+ * `archive/records.jsonl` 只追加 JSONL:提案/探针/verdict/备份会话标识/
5
+ * L2 比较记录按发生顺序落行,永不覆盖、不重排、不去重。读取侧严格解析
6
+ * ——档案是治理审计数据,坏行必须带行号抛出而不是静默跳过。
7
+ *
8
+ * 记录格式:`{ kind, recordedAt, payload }`。`kind` 开放(各任务逐步收
9
+ * 敛:proposal/verdict/lesson/L2 比较等),`payload` 为 JSON 安全的具体
10
+ * 记录;`recordedAt` 由写入器打戳(ISO 8601)。
11
+ */
12
+ import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
13
+ import { join } from "node:path";
14
+ import { evolveStateDir } from "./stateDir.js";
15
+ export function archiveRecordsPath(homeDir) {
16
+ return join(evolveStateDir(homeDir), "archive", "records.jsonl");
17
+ }
18
+ /** 只追加一条记录,返回落盘后的完整条目(含打戳)。 */
19
+ export function appendArchiveRecord(homeDir, record, now = () => new Date()) {
20
+ const entry = { kind: record.kind, recordedAt: now().toISOString(), payload: record.payload };
21
+ mkdirSync(join(evolveStateDir(homeDir), "archive"), { recursive: true, mode: 0o700 });
22
+ // 单行约定:JSON.stringify 会把 payload 内的换行转义,行边界天然成立。
23
+ // 新建 0600:档案含提案内容(配置文件片段),不收世界可读。
24
+ appendFileSync(archiveRecordsPath(homeDir), JSON.stringify(entry) + "\n", { mode: 0o600 });
25
+ return entry;
26
+ }
27
+ /** 读回全部档案条目;文件不存在返回 [](读取零副作用,不建目录)。 */
28
+ export function readArchiveRecords(homeDir) {
29
+ const path = archiveRecordsPath(homeDir);
30
+ if (!existsSync(path))
31
+ return [];
32
+ const entries = [];
33
+ const lines = readFileSync(path, "utf-8").split("\n");
34
+ for (const [index, line] of lines.entries()) {
35
+ // 末尾换行产生一条尾随空行,不算记录;中间空行是损坏。
36
+ if (line === "" && index === lines.length - 1)
37
+ continue;
38
+ if (line.trim() === "")
39
+ throw new Error(`archive: 第 ${index + 1} 行为空行(只追加档案不允许空行)`);
40
+ let parsed;
41
+ try {
42
+ parsed = JSON.parse(line);
43
+ }
44
+ catch {
45
+ throw new Error(`archive: 第 ${index + 1} 行不是合法 JSON`);
46
+ }
47
+ const entry = parsed;
48
+ if (typeof entry !== "object" ||
49
+ entry === null ||
50
+ typeof entry.kind !== "string" ||
51
+ typeof entry.recordedAt !== "string" ||
52
+ !("payload" in entry)) {
53
+ throw new Error(`archive: 第 ${index + 1} 行缺少 kind/recordedAt/payload`);
54
+ }
55
+ entries.push(entry);
56
+ }
57
+ return entries;
58
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * secrets 擦除(trellis-evolve 任务 11.1)。
3
+ *
4
+ * 信号写入前,所有字符串字段经此擦除(11.2 的用户纠正文本同样如此)。
5
+ * 模式取已知凭据形态:API token 前缀、Bearer 头、key=value 凭据赋值、
6
+ * PEM 私钥块。不承诺穷尽 —— 主防线是事件层根本不收载荷,这里是纵深。
7
+ */
8
+ /** 对单个字符串字段做 secrets 擦除;非敏感内容原样保留。 */
9
+ export declare function eraseSecrets(text: string): string;
10
+ /** 对记录中所有字符串值递归擦除(载荷级防御)。 */
11
+ export declare function eraseSecretsDeep(value: unknown): unknown;
12
+ /**
13
+ * 凭据形态**字面值**检测(12.2 的 workspace 可提交约束):先把
14
+ * `${VAR}` 引用挖掉(合法引用不算字面值),再用与擦除同源的凭据形态
15
+ * 模式判定剩余文本。命中即 true —— 只认形态,不承诺穷尽。
16
+ */
17
+ export declare function containsCredentialLiteral(text: string): boolean;
@@ -0,0 +1,65 @@
1
+ /**
2
+ * secrets 擦除(trellis-evolve 任务 11.1)。
3
+ *
4
+ * 信号写入前,所有字符串字段经此擦除(11.2 的用户纠正文本同样如此)。
5
+ * 模式取已知凭据形态:API token 前缀、Bearer 头、key=value 凭据赋值、
6
+ * PEM 私钥块。不承诺穷尽 —— 主防线是事件层根本不收载荷,这里是纵深。
7
+ */
8
+ const PATTERNS = [
9
+ // OpenAI / Anthropic 风格:sk-...、sk-ant-...
10
+ [/sk-[A-Za-z0-9_-]{8,}/g, "[REDACTED]"],
11
+ // GitHub token:ghp_/gho_/ghs_/ghr_/github_pat_
12
+ [/gh[pousr]_[A-Za-z0-9]{16,}/g, "[REDACTED]"],
13
+ [/github_pat_[A-Za-z0-9_]{20,}/g, "[REDACTED]"],
14
+ // AWS access key id
15
+ [/AKIA[0-9A-Z]{12,}/g, "[REDACTED]"],
16
+ // JWT(三段 base64url,header 恒以 eyJ 开头)
17
+ [/eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}/g, "[REDACTED]"],
18
+ // Slack token:xoxb/xoxp/xoxa/xoxs/xoxr
19
+ [/xox[bpasr]-[A-Za-z0-9-]{8,}/g, "[REDACTED]"],
20
+ // Bearer 头
21
+ [/Bearer\s+[A-Za-z0-9._~+/=-]{8,}/g, "Bearer [REDACTED]"],
22
+ // key=value / key: value 凭据赋值形态(值若是 Bearer/Basic 头留给上面
23
+ // 的 Bearer 规则处理,避免只吃掉 scheme 词、漏掉真实 token)。
24
+ [
25
+ /((?:api[_-]?key|apikey|token|secret|password|passwd|credential|authorization|access[_-]?key)["']?\s*[:=]\s*["']?)(?!(?:Bearer|Basic|Token)\b)[^\s"'&;,]{4,}/gi,
26
+ "$1[REDACTED]",
27
+ ],
28
+ // PEM 私钥块
29
+ [/-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g, "[REDACTED]"],
30
+ ];
31
+ /** 对单个字符串字段做 secrets 擦除;非敏感内容原样保留。 */
32
+ export function eraseSecrets(text) {
33
+ let out = text;
34
+ for (const [pattern, replacement] of PATTERNS)
35
+ out = out.replace(pattern, replacement);
36
+ return out;
37
+ }
38
+ /** 对记录中所有字符串值递归擦除(载荷级防御)。 */
39
+ export function eraseSecretsDeep(value) {
40
+ if (typeof value === "string")
41
+ return eraseSecrets(value);
42
+ if (Array.isArray(value))
43
+ return value.map(eraseSecretsDeep);
44
+ if (typeof value === "object" && value !== null) {
45
+ const out = {};
46
+ for (const [key, v] of Object.entries(value))
47
+ out[key] = eraseSecretsDeep(v);
48
+ return out;
49
+ }
50
+ return value;
51
+ }
52
+ /** `${VAR}` 引用(workspace 可提交约束明确允许)。 */
53
+ const VAR_REF = /\$\{[A-Za-z_][A-Za-z0-9_]*\}/g;
54
+ /**
55
+ * 凭据形态**字面值**检测(12.2 的 workspace 可提交约束):先把
56
+ * `${VAR}` 引用挖掉(合法引用不算字面值),再用与擦除同源的凭据形态
57
+ * 模式判定剩余文本。命中即 true —— 只认形态,不承诺穷尽。
58
+ */
59
+ export function containsCredentialLiteral(text) {
60
+ const withoutRefs = text.replace(VAR_REF, "");
61
+ return PATTERNS.some(([pattern]) => {
62
+ pattern.lastIndex = 0;
63
+ return pattern.test(withoutRefs);
64
+ });
65
+ }
@@ -0,0 +1,37 @@
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 type { HostEvent } from "../host/hostEvents.js";
20
+ /** 清单条目 id(进化启用命令写入 `~/.trellis/mcp/extensions.yaml` 用)。 */
21
+ export declare const EVOLVE_EXTENSION_ID = "evolve";
22
+ /** 装载契约的结构形状(与 src/lib/hostExtensions.ts 同形,不 import)。 */
23
+ interface ActivateContext {
24
+ readonly homeDir: string;
25
+ readonly agent: string;
26
+ readonly workspaceRoot: string | null;
27
+ }
28
+ interface Activation {
29
+ readonly subscribers?: ReadonlyArray<(event: HostEvent) => void>;
30
+ }
31
+ export declare function activate(context: ActivateContext): Activation;
32
+ /**
33
+ * 清单条目的 `load` 模块说明符:本模块的绝对路径(dist 的 .js;经 tsx
34
+ * 直跑源码时为 .ts,动态 import 都能吃)。进化启用命令写清单时取此值。
35
+ */
36
+ export declare function evolveExtensionLoadSpecifier(): string;
37
+ export {};