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
@@ -48,10 +48,10 @@ export class ClaudeCodeAdapter {
48
48
  canonicalRoot,
49
49
  kind: "instructions",
50
50
  });
51
- const mcpItems = this.planMcp(canonical);
51
+ const mcpItems = await this.planMcp(canonical);
52
52
  return [...skillItems, ...instructionsItems, ...mcpItems];
53
53
  }
54
- planMcp(canonical) {
54
+ async planMcp(canonical) {
55
55
  const configPath = join(this.homeDir, ".claude.json");
56
56
  const parsed = existsSync(configPath) ? JSON.parse(readFileSync(configPath, "utf-8")) : undefined;
57
57
  const ownership = ownedByAgent(loadMcpOwnership(this.homeDir), this.id);
@@ -50,7 +50,7 @@ export class CodexAdapter {
50
50
  canonicalRoot: join(canonicalRoot, "skills"),
51
51
  kind: "skill",
52
52
  });
53
- const mcpItems = this.planMcp(canonical);
53
+ const mcpItems = await this.planMcp(canonical);
54
54
  const configuredInstructionsPath = readInstructionsPath(join(this.homeDir, ".codex", "config.toml"));
55
55
  if (!configuredInstructionsPath) {
56
56
  // Codex has no `instructions` key set — not Trellis's to decide.
@@ -65,10 +65,10 @@ export class CodexAdapter {
65
65
  });
66
66
  return [...skillItems, ...instructionsItems, ...mcpItems];
67
67
  }
68
- planMcp(canonical) {
68
+ async planMcp(canonical) {
69
69
  const configTomlPath = join(this.homeDir, ".codex", "config.toml");
70
70
  const content = existsSync(configTomlPath) ? readFileSync(configTomlPath, "utf-8") : "";
71
- const { desired, conflicts } = resolveMcpPlan(this.id, globalLayerMcp(canonical), canonical.managedAgents, canonical.secretsPolicy);
71
+ const { desired, conflicts } = await resolveMcpPlan(this.id, globalLayerMcp(canonical), canonical.managedAgents, canonical.secretsPolicy);
72
72
  const items = [];
73
73
  for (const { name, def } of desired) {
74
74
  const current = currentServerSectionText(content, name);
@@ -29,7 +29,7 @@ export declare function planJsonMcp(opts: {
29
29
  * zero removal candidates. */
30
30
  ownership?: Record<string, unknown>;
31
31
  render?: JsonMcpEntryRenderer;
32
- }): AdapterPlanItem[];
32
+ }): Promise<AdapterPlanItem[]>;
33
33
  /** Merges every `"create"` MCP item into `parsed` (or a fresh `{}` if the
34
34
  * file didn't exist) and deletes every `"remove"` item's key, returning
35
35
  * the object to stringify — every sibling top-level key on `parsed` is
@@ -30,10 +30,10 @@ export function renderJsonServerEntry(def) {
30
30
  }
31
31
  return entry;
32
32
  }
33
- export function planJsonMcp(opts) {
33
+ export async function planJsonMcp(opts) {
34
34
  const { configPath, parsed, mcp, agentId, managedAgents, policy, ownership } = opts;
35
35
  const render = opts.render ?? ((def, _name) => renderJsonServerEntry(def));
36
- const { desired, conflicts } = resolveMcpPlan(agentId, mcp, managedAgents, policy);
36
+ const { desired, conflicts } = await resolveMcpPlan(agentId, mcp, managedAgents, policy);
37
37
  const existingServers = parsed?.mcpServers ?? {};
38
38
  const items = [];
39
39
  for (const { name, def } of desired) {
@@ -63,7 +63,7 @@ export class KimiCodeAdapter {
63
63
  const configPath = join(this.homeDir, ".kimi-code", "mcp.json");
64
64
  const parsed = existsSync(configPath) ? JSON.parse(readFileSync(configPath, "utf-8")) : undefined;
65
65
  const ownership = ownedByAgent(loadMcpOwnership(this.homeDir), this.id);
66
- items.push(...planJsonMcp({
66
+ items.push(...(await planJsonMcp({
67
67
  configPath,
68
68
  parsed,
69
69
  mcp: globalLayerMcp(canonical),
@@ -72,7 +72,7 @@ export class KimiCodeAdapter {
72
72
  policy: canonical.secretsPolicy,
73
73
  ownership,
74
74
  render: renderKimiServerEntry,
75
- }));
75
+ })));
76
76
  return items;
77
77
  }
78
78
  async apply(plan, backup) {
@@ -18,6 +18,7 @@
18
18
  import { existsSync, mkdirSync, readFileSync } from "node:fs";
19
19
  import { dirname, join } from "node:path";
20
20
  import { homedir } from "node:os";
21
+ import { deliveredName } from "../lib/secretEnv.js";
21
22
  import { isInScope } from "../core/adapter.js";
22
23
  import { usesNativeCapabilityDelivery } from "../core/types.js";
23
24
  import * as kiroProbe from "../probes/kiro.js";
@@ -64,11 +65,11 @@ export class KiroAdapter {
64
65
  canonicalRoot,
65
66
  kind: "instructions",
66
67
  });
67
- const mcpItems = this.planMcp(canonical);
68
- const approvedEnvVarsItems = this.planApprovedEnvVars(canonical);
68
+ const mcpItems = await this.planMcp(canonical);
69
+ const approvedEnvVarsItems = await this.planApprovedEnvVars(canonical);
69
70
  return [...skillItems, ...instructionsItems, ...mcpItems, ...approvedEnvVarsItems];
70
71
  }
71
- planMcp(canonical) {
72
+ async planMcp(canonical) {
72
73
  const configPath = join(this.homeDir, ".kiro", "settings", "mcp.json");
73
74
  const parsed = existsSync(configPath) ? JSON.parse(readFileSync(configPath, "utf-8")) : undefined;
74
75
  const ownership = ownedByAgent(loadMcpOwnership(this.homeDir), this.id);
@@ -77,17 +78,18 @@ export class KiroAdapter {
77
78
  /** Names come from `resolveMcpPlan`, not a raw scan of `canonical.mcp.servers`
78
79
  * (design.md D2) — a server scoped away from Kiro, or refused for a
79
80
  * known_host_injected collision, never contributes a name here either. */
80
- desiredApprovedEnvVars(canonical) {
81
- const { desired } = resolveMcpPlan(this.id, globalLayerMcp(canonical), canonical.managedAgents, canonical.secretsPolicy);
81
+ async desiredApprovedEnvVars(canonical) {
82
+ const { desired } = await resolveMcpPlan(this.id, globalLayerMcp(canonical), canonical.managedAgents, canonical.secretsPolicy);
82
83
  const names = new Set();
83
84
  for (const { def } of desired) {
85
+ // Approved list holds DELIVERED names (what renderers write), not raw entries.
84
86
  for (const name of declaredEnvNames(def))
85
- names.add(name);
87
+ names.add(deliveredName(name));
86
88
  }
87
89
  return [...names];
88
90
  }
89
- planApprovedEnvVars(canonical) {
90
- const desired = this.desiredApprovedEnvVars(canonical);
91
+ async planApprovedEnvVars(canonical) {
92
+ const desired = await this.desiredApprovedEnvVars(canonical);
91
93
  if (desired.length === 0) {
92
94
  return [];
93
95
  }
@@ -188,7 +190,7 @@ export class KiroAdapter {
188
190
  return [];
189
191
  }
190
192
  })();
191
- for (const name of this.desiredApprovedEnvVars(canonical)) {
193
+ for (const name of await this.desiredApprovedEnvVars(canonical)) {
192
194
  if (!approved.includes(name)) {
193
195
  mismatches.push(`env var "${name}" is needed by a canonical MCP server for kiro but missing from ${APPROVED_ENV_VARS_KEY}`);
194
196
  }
@@ -91,4 +91,4 @@ export interface LiteralSecretMatch {
91
91
  * as before this function reported a field at all.
92
92
  */
93
93
  export declare function findLiteralSecret(def: McpServerDef): LiteralSecretMatch | undefined;
94
- export declare function resolveMcpPlan(agentId: AgentId, mcp: McpConfig, managedAgents: readonly AgentId[], policy: SecretsPolicy): McpPlanResult;
94
+ export declare function resolveMcpPlan(agentId: AgentId, mcp: McpConfig, managedAgents: readonly AgentId[], policy: SecretsPolicy): Promise<McpPlanResult>;
@@ -20,7 +20,7 @@ import { isInScope } from "../core/adapter.js";
20
20
  import { capabilityDeliveryForAgent } from "../core/types.js";
21
21
  import { isOAuthAuth, oauthOwner } from "../core/types.js";
22
22
  import { codexBearerTokenEnvVar } from "../lib/tomlSection.js";
23
- import { resolveSecretEnv } from "../lib/secretEnv.js";
23
+ import { parseEnvEntry, resolveSecretEnv } from "../lib/secretEnv.js";
24
24
  export const HUB_ENTRY_NAME = "trellis-hub";
25
25
  export const GATEWAY_ENTRY_NAME = "trellis";
26
26
  export const LEGACY_GATEWAY_ENTRY_NAME = "trellis-gateway";
@@ -138,14 +138,25 @@ export function findLiteralSecret(def) {
138
138
  * the target key it would have been written under
139
139
  * (trellis-migrate-env-var-alias).
140
140
  */
141
- function findUnresolvedEnvName(def, policy) {
141
+ async function findUnresolvedEnvName(def, policy) {
142
142
  const names = [...(def.env ?? []), ...Object.values(def.envAliases ?? {})];
143
143
  if (names.length === 0)
144
144
  return undefined;
145
- const resolved = resolveSecretEnv(names, policy);
146
- return names.find((name) => !resolved[name]);
145
+ // A malformed `mxenv://` entry conflicts here too — it is rejected input,
146
+ // never a silently-resolved bare name (mx-env-secret-source D6). The
147
+ // returned text is the DECLARED entry; resolution checks use delivered
148
+ // names (the `KEY` segment for a reference).
149
+ const parsed = names.map(parseEnvEntry);
150
+ const invalid = parsed.find((p) => p.kind === "invalid");
151
+ if (invalid)
152
+ return invalid.entry;
153
+ const resolved = await resolveSecretEnv(names, policy);
154
+ return names.find((name) => {
155
+ const p = parseEnvEntry(name);
156
+ return !resolved[p.kind === "invalid" ? name : p.delivered];
157
+ });
147
158
  }
148
- export function resolveMcpPlan(agentId, mcp, managedAgents, policy) {
159
+ export async function resolveMcpPlan(agentId, mcp, managedAgents, policy) {
149
160
  const route = routeForAgent(agentId, mcp, managedAgents);
150
161
  const runtimeEnabled = capabilityDeliveryForAgent(agentId, mcp) !== "native";
151
162
  // Checked before `hub`: only one entry can be "the" MCP entry for an
@@ -161,7 +172,7 @@ export function resolveMcpPlan(agentId, mcp, managedAgents, policy) {
161
172
  conflicts: [{ name: entryName, message: collisionMessage(entryName, agentId), remediation: collisionRemediation(entryName) }],
162
173
  };
163
174
  }
164
- const oauth = planDirectServers(agentId, mcp, managedAgents, policy, route, isAgentOwnedOAuth);
175
+ const oauth = await planDirectServers(agentId, mcp, managedAgents, policy, route, isAgentOwnedOAuth);
165
176
  // Every ordinary per-server check (enabled, scope, literal secrets,
166
177
  // unresolved env names, Codex's header shape) still runs at gateway
167
178
  // startup. OAuth servers are the deliberate exception: they are written
@@ -237,7 +248,7 @@ export function resolveMcpPlan(agentId, mcp, managedAgents, policy) {
237
248
  // Runtime mode owns the agent-facing MCP edge. It resolves the same
238
249
  // direct/gateway-compatible upstream route at startup, so native
239
250
  // per-server entries are not duplicated here.
240
- const oauth = planDirectServers(agentId, mcp, managedAgents, policy, route, isAgentOwnedOAuth);
251
+ const oauth = await planDirectServers(agentId, mcp, managedAgents, policy, route, isAgentOwnedOAuth);
241
252
  return {
242
253
  desired: [{
243
254
  name: RUNTIME_ENTRY_NAME,
@@ -246,14 +257,14 @@ export function resolveMcpPlan(agentId, mcp, managedAgents, policy) {
246
257
  conflicts: oauth.conflicts,
247
258
  };
248
259
  }
249
- const native = planDirectServers(agentId, mcp, managedAgents, policy, route);
260
+ const native = await planDirectServers(agentId, mcp, managedAgents, policy, route);
250
261
  const hosted = native.desired.filter(({ def }) => isOAuthMcpServer(def) && oauthOwner(def.auth) === "trellis");
251
262
  return {
252
263
  desired: native.desired.filter(({ def }) => !isOAuthMcpServer(def) || oauthOwner(def.auth) === "agent"),
253
264
  conflicts: [...native.conflicts, ...hosted.map(({ name }) => ({ name, message: `Trellis-owned OAuth server "${name}" requires gateway or runtime delivery for ${agentId}`, remediation: "enable gateway/runtime delivery or set auth.owner to agent" }))],
254
265
  };
255
266
  }
256
- function planDirectServers(agentId, mcp, managedAgents, policy, route, filter) {
267
+ async function planDirectServers(agentId, mcp, managedAgents, policy, route, filter) {
257
268
  const desired = [];
258
269
  const conflicts = [];
259
270
  for (const [name, def] of Object.entries(mcp.servers)) {
@@ -306,13 +317,25 @@ function planDirectServers(agentId, mcp, managedAgents, policy, route, filter) {
306
317
  });
307
318
  continue;
308
319
  }
309
- const unresolvedName = findUnresolvedEnvName(def, policy);
320
+ const unresolvedName = await findUnresolvedEnvName(def, policy);
310
321
  if (unresolvedName) {
311
- const source = policy.envFile ?? "process environment";
322
+ const entry = parseEnvEntry(unresolvedName);
323
+ if (entry.kind === "invalid") {
324
+ conflicts.push({
325
+ name,
326
+ message: `refusing to write MCP server "${name}": its declared env entry "${unresolvedName}" is not a valid mxenv://<profile>/<KEY> reference (mx-env-secret-source)`,
327
+ remediation: `fix the entry in servers.yaml to the mxenv://<profile>/<KEY> form or a plain variable name, then re-run mcp sync`,
328
+ });
329
+ continue;
330
+ }
331
+ const source = entry.kind === "ref"
332
+ ? `the mxenv composition (profile "${entry.profile}")`
333
+ : (policy.envFile ?? "process environment");
334
+ const remediationTarget = entry.kind === "ref" ? entry.key : unresolvedName;
312
335
  conflicts.push({
313
336
  name,
314
337
  message: `refusing to write MCP server "${name}": its declared env var "${unresolvedName}" has no resolvable value in ${source} — writing it now would silently break this server's connection once the agent starts it (trellis-mcp-static-env-and-disabled-servers)`,
315
- remediation: `set "${unresolvedName}" in ${source}, or fix the name in servers.yaml if it was a typo, then re-run mcp sync`,
338
+ remediation: `set "${remediationTarget}" in ${source}, or fix the name in servers.yaml if it was a typo, then re-run mcp sync`,
316
339
  });
317
340
  continue;
318
341
  }
@@ -114,7 +114,7 @@ export class PiAdapter {
114
114
  kind: "instructions",
115
115
  });
116
116
  if (supportsNativeMcp(this.versionProbe())) {
117
- return [...skillItems, ...instructionsItems, ...this.planNativeMcp(canonical)];
117
+ return [...skillItems, ...instructionsItems, ...await this.planNativeMcp(canonical)];
118
118
  }
119
119
  const { file: bridgeFile, symlinkName } = resolveBridgeFile();
120
120
  const extensionItems = planSymlinks({
@@ -139,12 +139,12 @@ export class PiAdapter {
139
139
  * apply, and a bare skills/instructions sync can neither remove the
140
140
  * bridge early nor resurrect it afterwards.
141
141
  */
142
- planNativeMcp(canonical) {
142
+ async planNativeMcp(canonical) {
143
143
  const agentDir = join(this.homeDir, ".pi", "agent");
144
144
  const configPath = join(agentDir, "mcp.json");
145
145
  const parsed = existsSync(configPath) ? JSON.parse(readFileSync(configPath, "utf-8")) : undefined;
146
146
  const ownership = ownedByAgent(loadMcpOwnership(this.homeDir), this.id);
147
- const items = planJsonMcp({
147
+ const items = (await planJsonMcp({
148
148
  configPath,
149
149
  parsed,
150
150
  mcp: globalLayerMcp(canonical),
@@ -153,7 +153,7 @@ export class PiAdapter {
153
153
  policy: canonical.secretsPolicy,
154
154
  ownership,
155
155
  render: piRenderer,
156
- }).map((item) => {
156
+ })).map((item) => {
157
157
  // pi speaks stdio and streamable HTTP only — an sse server gets an
158
158
  // honest conflict instead of an entry pi could never connect.
159
159
  const write = item.mcpWrite;
@@ -251,7 +251,7 @@ export class PiAdapter {
251
251
  // definition drift. The plan only ever names Trellis-owned entries
252
252
  // (the ownership ledger gates removals), so user-authored servers
253
253
  // can never surface here.
254
- for (const item of this.planNativeMcp(canonical)) {
254
+ for (const item of await this.planNativeMcp(canonical)) {
255
255
  if (item.action === "create" && item.mcpWrite) {
256
256
  mismatches.push(`mcp.json entry "${item.mcpWrite.name}" is missing or differs from canonical — run trellis mcp sync`);
257
257
  }
@@ -16,7 +16,7 @@ export declare function planZcodeMcp(opts: {
16
16
  managedAgents: readonly import("../core/types.js").AgentId[];
17
17
  policy: SecretsPolicy;
18
18
  ownership: JsonRecord;
19
- }): AdapterPlanItem[];
19
+ }): Promise<AdapterPlanItem[]>;
20
20
  export declare function applyZcodeMcp(parsed: JsonRecord | undefined, items: readonly AdapterPlanItem[]): JsonRecord;
21
21
  export declare class ZcodeAdapter implements TrellisAdapter {
22
22
  private readonly homeDir;
@@ -29,8 +29,8 @@ function record(value) {
29
29
  function zcodeServers(parsed) {
30
30
  return record(record(parsed?.mcp).servers);
31
31
  }
32
- export function planZcodeMcp(opts) {
33
- const { desired, conflicts } = resolveMcpPlan("zcode", opts.mcp, opts.managedAgents, opts.policy);
32
+ export async function planZcodeMcp(opts) {
33
+ const { desired, conflicts } = await resolveMcpPlan("zcode", opts.mcp, opts.managedAgents, opts.policy);
34
34
  const existing = zcodeServers(opts.parsed);
35
35
  const items = [];
36
36
  for (const { name, def } of desired) {
@@ -136,14 +136,14 @@ export class ZcodeAdapter {
136
136
  canonicalRoot,
137
137
  kind: "instructions",
138
138
  }));
139
- items.push(...planZcodeMcp({
139
+ items.push(...(await planZcodeMcp({
140
140
  configPath: profile.configPath,
141
141
  parsed,
142
142
  mcp: globalLayerMcp(canonical),
143
143
  managedAgents: canonical.managedAgents,
144
144
  policy: canonical.secretsPolicy,
145
145
  ownership: ownedByAgent(loadMcpOwnership(this.homeDir), this.id),
146
- }));
146
+ })));
147
147
  const current = currentSkillControls(parsed);
148
148
  const ownership = loadZcodeSettingsOwnership(this.homeDir);
149
149
  if (runtimeOnly && (current.featuresSkill !== false || current.skillsEnabled !== false)) {
@@ -214,14 +214,14 @@ export class ZcodeAdapter {
214
214
  for (const name of desiredNativeSkills)
215
215
  if (!actualNativeSkills.has(name))
216
216
  mismatches.push(`skill "${name}" is missing on ZCode`);
217
- const expectedMcp = planZcodeMcp({
217
+ const expectedMcp = (await planZcodeMcp({
218
218
  configPath: profile.configPath,
219
219
  parsed: readConfig(profile.configPath),
220
220
  mcp: globalLayerMcp(canonical),
221
221
  managedAgents: canonical.managedAgents,
222
222
  policy: canonical.secretsPolicy,
223
223
  ownership: ownedByAgent(loadMcpOwnership(this.homeDir), this.id),
224
- }).filter((item) => item.action === "create" || item.action === "conflict");
224
+ })).filter((item) => item.action === "create" || item.action === "conflict");
225
225
  mismatches.push(...expectedMcp.map((item) => item.description));
226
226
  if (!snapshot.instructionsFile?.isSymlink || snapshot.instructionsFile.target !== canonical.instructionsFile)
227
227
  mismatches.push("shared instructions are not linked into ~/.zcode/AGENTS.md");
package/dist/cli.js CHANGED
@@ -18,6 +18,10 @@ import { runMcpAuth } from "./commands/mcpAuth.js";
18
18
  import { runMcpRenew } from "./commands/mcpRenew.js";
19
19
  import { runSecretsAudit } from "./commands/secretsAudit.js";
20
20
  import { runRollback } from "./commands/rollback.js";
21
+ // consumer-wiring(8.7):`trellis evolve` 管理面挂载。这条 import 是
22
+ // 托管层仅有的消费者接线,登记在 test/unit/hostingIndependence.test.ts
23
+ // 的 CONSUMER_WIRING;物理移出自我进化时与下文 evolve 分支一并删除。
24
+ import { runEvolveCommand } from "./commands/evolve.js";
21
25
  import { runSkillList, runSkillAdd, runSkillRemove, runSkillScope, runSkillUpdateBuiltin } from "./commands/skill.js";
22
26
  import { runRemoteSkillAdd, runRemoteSkillList, runRemoteSkillUpdate } from "./commands/remoteSkill.js";
23
27
  import { collectMemoryList, runMemoryExtraction, runMemorySync } from "./commands/memory.js";
@@ -26,7 +30,7 @@ import { runKimi } from "./commands/kimi.js";
26
30
  import { TRELLIS_VERSION } from "./lib/cliMetadata.js";
27
31
  import { flagValue } from "./lib/cliArgs.js";
28
32
  import { parseSyncArgs } from "./lib/syncArgs.js";
29
- const KNOWN_COMMANDS = ["onboard", "init", "migrate", "doctor", "sync", "mcp", "mcp-gateway", "mcp-runtime", "skill", "add", "update", "memory", "manage", "kimi", "secrets", "rollback"];
33
+ const KNOWN_COMMANDS = ["onboard", "init", "migrate", "doctor", "sync", "mcp", "mcp-gateway", "mcp-runtime", "skill", "add", "update", "memory", "manage", "kimi", "secrets", "rollback", "evolve"];
30
34
  function printUsage() {
31
35
  console.log(`trellis - a single source of capability for every coding agent
32
36
 
@@ -625,6 +629,13 @@ async function main(argv) {
625
629
  process.exitCode = exitCode;
626
630
  return;
627
631
  }
632
+ if (command === "evolve") {
633
+ // 自我进化管理面(trellis-evolve 12.4):批准仅限交互式终端,由
634
+ // runEvolveCommand 内部检测;argv 原样传入,不拼 shell。
635
+ const { exitCode } = await runEvolveCommand(rest);
636
+ process.exitCode = exitCode;
637
+ return;
638
+ }
628
639
  console.error(`\`trellis ${command}\` is not implemented yet — this is a pre-alpha scaffold.\nSee docs/roadmap.md for status.`);
629
640
  process.exitCode = 1;
630
641
  }
@@ -8,6 +8,7 @@ import { loadCanonicalSource } from "../core/canonical.js";
8
8
  import type { AgentId, AgentSnapshot, CanonicalSource } from "../core/types.js";
9
9
  import { type VersionExecutor } from "../lib/cliEntrypoints.js";
10
10
  import { type WorkspaceDoctorSection } from "../lib/workspaceDoctor.js";
11
+ import { type EvolveLevelSection } from "../host/levels.js";
11
12
  /**
12
13
  * Interim default until P1 wires this from `.trellis/mcp/servers.yaml`'s
13
14
  * real `known_host_injected` field (P0 has no canonical source at all —
@@ -74,6 +75,13 @@ export interface DoctorReport {
74
75
  /** Per-entry layer attribution when a `.agents/` workspace layer
75
76
  * participated (trellis-workspace-scope task 3.5). */
76
77
  workspace?: WorkspaceDoctorSection;
78
+ /** Self-evolution levels when `~/.trellis/evolve.yaml` has any explicit
79
+ * setting (trellis-evolve task 10.2). Absent when unconfigured — a
80
+ * machine that never enabled evolution sees byte-identical output. */
81
+ evolve?: EvolveLevelSection;
82
+ /** Pending proposal count (trellis-evolve task 12.4; spec: doctor SHALL
83
+ * list it). Present when > 0 or the evolve section is shown. Read-only. */
84
+ evolvePending?: number;
77
85
  }
78
86
  /** Exported so tests can assert on this resolution directly, without
79
87
  * routing through real per-agent probes `runDoctor` also runs. */
@@ -129,7 +137,7 @@ export declare function detectMcpUnreachable(snapshots: AgentSnapshot[]): Findin
129
137
  /** Compares runtime entries against the same canonical MCP plan that sync
130
138
  * uses. Only missing Trellis-owned runtime edges are reported; extra
131
139
  * user-authored MCP entries remain outside doctor ownership. */
132
- export declare function detectRuntimeDrift(snapshots: AgentSnapshot[], canonical: Pick<ReturnType<typeof loadCanonicalSource>, "mcp" | "managedAgents" | "secretsPolicy">): Finding[];
140
+ export declare function detectRuntimeDrift(snapshots: AgentSnapshot[], canonical: Pick<ReturnType<typeof loadCanonicalSource>, "mcp" | "managedAgents" | "secretsPolicy">): Promise<Finding[]>;
133
141
  /** Exported so `trellis onboard` can reuse it verbatim for its own final
134
142
  * stage (trellis-onboard-closed-loop) rather than a second copy of this
135
143
  * formatting. */
@@ -4,6 +4,8 @@
4
4
  * current state against each other, the same manual process used
5
5
  * throughout docs/research.md's investigation, now formalized.
6
6
  */
7
+ import { readdirSync, readFileSync } from "node:fs";
8
+ import { join } from "node:path";
7
9
  import { homedir } from "node:os";
8
10
  import * as claudeCodeProbe from "../probes/claude-code.js";
9
11
  import * as codexProbe from "../probes/codex.js";
@@ -13,12 +15,43 @@ import * as kimiCodeProbe from "../probes/kimi-code.js";
13
15
  import * as zcodeProbe from "../probes/zcode.js";
14
16
  import { loadCanonicalSource } from "../core/canonical.js";
15
17
  import { GATEWAY_ENTRY_NAME, RUNTIME_ENTRY_NAME, resolveMcpPlan } from "../adapters/mcpPlan.js";
16
- import { ALL_AGENTS, isOAuthAuth, oauthOwner } from "../core/types.js";
18
+ import { ALL_AGENTS, capabilityDeliveryForAgent, isOAuthAuth, oauthOwner } from "../core/types.js";
17
19
  import { classifyStoredToken, remediationFor } from "../lib/oauth/renew.js";
18
20
  import { readToken } from "../lib/oauth/store.js";
19
21
  import { findWorkspaceRoot } from "../lib/workspace.js";
20
22
  import { detectCliVersionSplit, findCliEntrypoints } from "../lib/cliEntrypoints.js";
21
23
  import { collectWorkspaceDoctorSection } from "../lib/workspaceDoctor.js";
24
+ import { describeEvolveLevels, loadEvolveLevelPolicy, } from "../host/levels.js";
25
+ /**
26
+ * Pending proposal count via the on-disk data contract (8.1 状态目录 /
27
+ * 12.4 pending 存储:`~/.trellis/evolve/pending/<safeId>.json`,记录含
28
+ * `status` 字段)。doctor 属托管层,只读数据、不 import 自我进化代码,
29
+ * 物理移出消费者后 doctor 仍自洽(目录不存在 → 0)。
30
+ */
31
+ function countPendingProposalRecords(homeDir) {
32
+ const dir = join(homeDir, ".trellis", "evolve", "pending");
33
+ let names;
34
+ try {
35
+ names = readdirSync(dir);
36
+ }
37
+ catch {
38
+ return 0;
39
+ }
40
+ let count = 0;
41
+ for (const name of names) {
42
+ if (!name.endsWith(".json"))
43
+ continue;
44
+ try {
45
+ const parsed = JSON.parse(readFileSync(join(dir, name), "utf-8"));
46
+ if (parsed.status === "pending")
47
+ count += 1;
48
+ }
49
+ catch {
50
+ // 坏文件不阻塞 doctor 扫描。
51
+ }
52
+ }
53
+ return count;
54
+ }
22
55
  /**
23
56
  * Interim default until P1 wires this from `.trellis/mcp/servers.yaml`'s
24
57
  * real `known_host_injected` field (P0 has no canonical source at all —
@@ -145,7 +178,7 @@ export async function collectDoctorReport(homeDir = homedir(), knownHostInjected
145
178
  // `collectWorkspaceDoctorSection`'s per-file inspection is what we want.
146
179
  let workspace;
147
180
  if (canonical?.workspace) {
148
- workspace = collectWorkspaceDoctorSection(canonical);
181
+ workspace = await collectWorkspaceDoctorSection(canonical);
149
182
  }
150
183
  else {
151
184
  const workspaceRoot = workspaceDir != null ? findWorkspaceRoot(workspaceDir) : null;
@@ -165,7 +198,7 @@ export async function collectDoctorReport(homeDir = homedir(), knownHostInjected
165
198
  diagnostics: [],
166
199
  workspace: { root: workspaceRoot, mcpProvenance: {}, mcpGlobalServers: {}, skillProvenance: {}, globalSkills: [], shadowed: [] },
167
200
  };
168
- workspace = collectWorkspaceDoctorSection(minimal);
201
+ workspace = await collectWorkspaceDoctorSection(minimal);
169
202
  }
170
203
  }
171
204
  // Workspace TOML findings become top-level findings so the exit code
@@ -182,8 +215,40 @@ export async function collectDoctorReport(homeDir = homedir(), knownHostInjected
182
215
  }
183
216
  }
184
217
  const oauth = canonical ? collectOAuthHealth(homeDir, canonical) : [];
185
- findings.push(...detectDuplication(snapshots), ...detectCollisions(snapshots, knownHostInjected), ...detectCrossAgentDrift(snapshots), ...detectCaseMismatches(snapshots), ...detectParseDiagnostics(snapshots), ...(canonical ? detectRuntimeDrift(snapshots, canonical) : []), ...detectMcpUnreachable(snapshots), ...oauth.flatMap((entry) => (entry.status === "valid" ? [] : [oauthFinding(entry)])));
186
- return { snapshots, findings, ...(oauth.length > 0 ? { oauth } : {}), ...(workspace ? { workspace } : {}) };
218
+ findings.push(...detectDuplication(snapshots), ...detectCollisions(snapshots, knownHostInjected), ...detectCrossAgentDrift(snapshots), ...detectCaseMismatches(snapshots), ...detectParseDiagnostics(snapshots), ...(canonical ? await detectRuntimeDrift(snapshots, canonical) : []), ...detectMcpUnreachable(snapshots), ...oauth.flatMap((entry) => (entry.status === "valid" ? [] : [oauthFinding(entry)])));
219
+ // 分级开启(trellis-evolve 10.2):级别策略是显式用户选择,读取零副作用。
220
+ // 未配置(或全空)时整段缺席 —— 从未开启自我进化的机器输出与未安装时
221
+ // 一致;损坏的策略文件不拖垮扫描,降级为段内诊断呈现。
222
+ let evolve;
223
+ try {
224
+ const policy = loadEvolveLevelPolicy(homeDir);
225
+ // 整段只在存在显式设置时出现(与未安装时输出一致);托管 agent 的
226
+ // 列表只是展示面,不构成出现理由。
227
+ const hasExplicitSetting = policy.defaultLevel !== undefined || Object.keys(policy.agents).length > 0 || Object.keys(policy.workspaces).length > 0;
228
+ if (hasExplicitSetting) {
229
+ // 11.4:观测形态按 capability delivery 分类——runtime 交付
230
+ // ("mcp"/"both")的 agent 走 gateway 事件观察;仅原生渲染
231
+ // ("native"/未配置)只有 doctor/sync 信号。
232
+ const observationOf = (agent) => canonical !== undefined && capabilityDeliveryForAgent(agent, canonical.mcp) !== "native"
233
+ ? "events"
234
+ : "doctor-only";
235
+ evolve = describeEvolveLevels(policy, (canonical?.managedAgents ?? []).map((agent) => ({ agent, observation: observationOf(agent) })));
236
+ }
237
+ }
238
+ catch (err) {
239
+ evolve = { agents: [], workspaces: [], diagnostics: [err.message] };
240
+ }
241
+ // 12.4:pending 提案数量(spec "trellis doctor SHALL 同样列出")。
242
+ // 只读状态目录;未开启进化且无存储时为 0,输出保持零噪音。
243
+ const evolvePending = countPendingProposalRecords(homeDir);
244
+ return {
245
+ snapshots,
246
+ findings,
247
+ ...(oauth.length > 0 ? { oauth } : {}),
248
+ ...(workspace ? { workspace } : {}),
249
+ ...(evolve ? { evolve } : {}),
250
+ ...(evolve || evolvePending > 0 ? { evolvePending } : {}),
251
+ };
187
252
  }
188
253
  /**
189
254
  * Read-only OAuth credential health (trellis-mcp-oauth-renew design.md
@@ -362,12 +427,12 @@ export function detectMcpUnreachable(snapshots) {
362
427
  /** Compares runtime entries against the same canonical MCP plan that sync
363
428
  * uses. Only missing Trellis-owned runtime edges are reported; extra
364
429
  * user-authored MCP entries remain outside doctor ownership. */
365
- export function detectRuntimeDrift(snapshots, canonical) {
430
+ export async function detectRuntimeDrift(snapshots, canonical) {
366
431
  const findings = [];
367
432
  for (const snap of snapshots) {
368
433
  if (!snap.present || !canonical.managedAgents.includes(snap.agent) || snap.agent === "pi")
369
434
  continue;
370
- const expected = resolveMcpPlan(snap.agent, canonical.mcp, canonical.managedAgents, canonical.secretsPolicy).desired
435
+ const expected = (await resolveMcpPlan(snap.agent, canonical.mcp, canonical.managedAgents, canonical.secretsPolicy)).desired
371
436
  .map((entry) => entry.name)
372
437
  .filter((name) => name === RUNTIME_ENTRY_NAME || name === GATEWAY_ENTRY_NAME);
373
438
  const actual = new Set(snap.mcpServers.map((server) => server.name));
@@ -463,4 +528,24 @@ export function printReport(report) {
463
528
  console.log(` ⚠ ${warning}`);
464
529
  }
465
530
  }
531
+ if (report.evolve) {
532
+ const ev = report.evolve;
533
+ console.log(`\nSelf-evolution (evolve.yaml):`);
534
+ if (ev.defaultLevel !== undefined)
535
+ console.log(` default level: ${ev.defaultLevel}`);
536
+ for (const entry of ev.agents) {
537
+ console.log(` agent ${entry.agent}: ${entry.level} (${entry.observation})`);
538
+ }
539
+ for (const entry of ev.workspaces) {
540
+ console.log(` workspace ${entry.root}: ${entry.level}`);
541
+ }
542
+ for (const diagnostic of ev.diagnostics) {
543
+ console.log(` ⚠ ${diagnostic}`);
544
+ }
545
+ console.log(` pending proposals: ${report.evolvePending ?? 0}`);
546
+ }
547
+ else if ((report.evolvePending ?? 0) > 0) {
548
+ // 无级别配置但有 pending 存储:同样列出(spec SHALL),单行提示。
549
+ console.log(`\nSelf-evolution: ${report.evolvePending} pending proposal(s) — run \`trellis evolve pending\``);
550
+ }
466
551
  }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * 自我进化管理面命令(trellis-evolve 任务 12.4)。
3
+ *
4
+ * 消费者归属(8.7):本文件是自我进化子系统的一部分,物理移出消费者时
5
+ * 与 src/evolve/**、CLI 挂载(src/cli.ts 的 CONSUMER_WIRING 接线行)一并
6
+ * 删除;hostingIndependence.test.ts 的 CONSUMER_OWNED/CONSUMER_WIRING
7
+ * 登记了这条移出清单。
8
+ *
9
+ * `trellis evolve <status | report | pending | approve | reject | apply | run | import | enable | disable | rollback>`
10
+ *
11
+ * 批准只走这里,且**仅限交互式终端**(spec"批准只走带外通道"):非交互
12
+ * 环境一律拒绝,提案保持 pending。不存在任何 `--yes` / `--confirm` 之类的
13
+ * 非交互批准捷径 —— agent 传入的"确认"参数不构成批准,加了也只会被当
14
+ * 未知参数忽略。循环副作用边界:除 `run`(唯一命令行触发入口,手动
15
+ * 触发一次运行)外的命令不写信号、不触发观察/提案/probe。approve
16
+ * 决策后**立即应用**(spec"批准即应用与显式应用入口":持单飞作业锁经
17
+ * 备份会话写入,锁忙暂缓并告知);apply 幂等应用全部已批准未应用的编辑
18
+ * (失败重试与滞留消化入口);reject 只做状态迁移 + 档案;enable/disable
19
+ * 写 canonical 级别策略,并按 design 8.1 同步扩展清单条目——级别升至
20
+ * observe 及以上时写入、置回 off 时移除;rollback 转发既有备份会话回滚。
21
+ */
22
+ import { type ApplyAgent } from "../evolve/apply.js";
23
+ export interface EvolveCommandOptions {
24
+ /** 测试/sandbox 注入缝;缺省用户 HOME。永不作为 CLI 标志暴露。 */
25
+ readonly homeDir?: string;
26
+ /** TTY 检测缝;缺省 stdin+stdout 同时为 TTY。 */
27
+ readonly isTTY?: boolean;
28
+ /** 时间源(毫秒);缺省 Date.now。 */
29
+ readonly now?: () => number;
30
+ /** 应用阶段的 agent 分类(测试/sandbox 注入缝;缺省按 delivery 分类)。 */
31
+ readonly applyAgents?: readonly ApplyAgent[];
32
+ /** 原生渲染同步触发缝(测试注入;缺省 host `triggerRenderSync`)。 */
33
+ readonly triggerSync?: () => Promise<{
34
+ exitCode: number;
35
+ }>;
36
+ }
37
+ /**
38
+ * 管理面命令入口。`args` 为 `evolve` 之后的 argv(子命令 + 参数),
39
+ * 由 cli 原样传入 —— 不经 shell 拼接。
40
+ */
41
+ export declare function runEvolveCommand(args: readonly string[], options?: EvolveCommandOptions): Promise<{
42
+ exitCode: number;
43
+ }>;