agent-trellis 0.7.0 → 0.8.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 (104) hide show
  1. package/README.md +97 -38
  2. package/dist/adapters/claude-code.js +3 -2
  3. package/dist/adapters/codex.js +3 -2
  4. package/dist/adapters/jsonMcp.d.ts +3 -1
  5. package/dist/adapters/jsonMcp.js +18 -4
  6. package/dist/adapters/kimi-code.d.ts +21 -0
  7. package/dist/adapters/kimi-code.js +107 -0
  8. package/dist/adapters/kiro.js +3 -2
  9. package/dist/adapters/mcpPlan.d.ts +6 -1
  10. package/dist/adapters/mcpPlan.js +106 -8
  11. package/dist/adapters/pi.js +22 -3
  12. package/dist/adapters/symlinkPlan.d.ts +7 -0
  13. package/dist/adapters/symlinkPlan.js +16 -7
  14. package/dist/cli.js +121 -21
  15. package/dist/commands/doctor.d.ts +7 -2
  16. package/dist/commands/doctor.js +38 -2
  17. package/dist/commands/init.d.ts +4 -2
  18. package/dist/commands/init.js +26 -13
  19. package/dist/commands/kimi.d.ts +16 -0
  20. package/dist/commands/kimi.js +64 -0
  21. package/dist/commands/manage.d.ts +39 -0
  22. package/dist/commands/manage.js +126 -0
  23. package/dist/commands/mcp.d.ts +22 -1
  24. package/dist/commands/mcp.js +65 -2
  25. package/dist/commands/mcpGateway.d.ts +6 -0
  26. package/dist/commands/mcpGateway.js +34 -14
  27. package/dist/commands/mcpImport.d.ts +27 -0
  28. package/dist/commands/mcpImport.js +336 -0
  29. package/dist/commands/memory.d.ts +5 -3
  30. package/dist/commands/memory.js +12 -8
  31. package/dist/commands/migrate.d.ts +16 -5
  32. package/dist/commands/migrate.js +90 -17
  33. package/dist/commands/onboard.d.ts +74 -2
  34. package/dist/commands/onboard.js +458 -71
  35. package/dist/commands/rollback.js +4 -1
  36. package/dist/commands/skill.d.ts +17 -0
  37. package/dist/commands/skill.js +47 -2
  38. package/dist/commands/sync.js +2 -0
  39. package/dist/core/canonical.d.ts +15 -7
  40. package/dist/core/canonical.js +101 -13
  41. package/dist/core/capabilitySelection.d.ts +45 -0
  42. package/dist/core/capabilitySelection.js +124 -0
  43. package/dist/core/types.d.ts +25 -2
  44. package/dist/core/types.js +7 -0
  45. package/dist/lib/backup.d.ts +2 -0
  46. package/dist/lib/backup.js +4 -0
  47. package/dist/lib/builtinSkills.d.ts +2 -0
  48. package/dist/lib/builtinSkills.js +4 -0
  49. package/dist/lib/cliMetadata.d.ts +1 -0
  50. package/dist/lib/cliMetadata.js +14 -0
  51. package/dist/lib/gatewayBackend.d.ts +28 -0
  52. package/dist/lib/gatewayBackend.js +72 -1
  53. package/dist/lib/installAgent.d.ts +7 -5
  54. package/dist/lib/installAgent.js +4 -3
  55. package/dist/lib/mcpConnect.js +5 -1
  56. package/dist/lib/mcpMigrateRead.d.ts +1 -0
  57. package/dist/lib/mcpMigrateRead.js +3 -0
  58. package/dist/lib/mcpRuntime.d.ts +46 -0
  59. package/dist/lib/mcpRuntime.js +185 -0
  60. package/dist/lib/mcpStatusProvider.d.ts +10 -0
  61. package/dist/lib/mcpStatusProvider.js +25 -0
  62. package/dist/lib/mcpToolRegistry.d.ts +18 -0
  63. package/dist/lib/mcpToolRegistry.js +107 -14
  64. package/dist/lib/memoryGraph.d.ts +1 -1
  65. package/dist/lib/memoryGraph.js +9 -3
  66. package/dist/lib/memoryProvider.d.ts +58 -0
  67. package/dist/lib/memoryProvider.js +175 -0
  68. package/dist/lib/memoryStatus.d.ts +17 -0
  69. package/dist/lib/memoryStatus.js +88 -0
  70. package/dist/lib/nativeMemory.d.ts +19 -0
  71. package/dist/lib/nativeMemory.js +129 -0
  72. package/dist/lib/runtimeControlProvider.d.ts +16 -0
  73. package/dist/lib/runtimeControlProvider.js +168 -0
  74. package/dist/lib/secretEnv.d.ts +2 -1
  75. package/dist/lib/secretEnv.js +6 -2
  76. package/dist/lib/skillProvider.d.ts +13 -0
  77. package/dist/lib/skillProvider.js +210 -0
  78. package/dist/lib/taskProvider.d.ts +7 -0
  79. package/dist/lib/taskProvider.js +43 -0
  80. package/dist/lib/taskStore.d.ts +46 -0
  81. package/dist/lib/taskStore.js +156 -0
  82. package/dist/lib/terminalPicker.d.ts +23 -49
  83. package/dist/lib/terminalPicker.js +53 -218
  84. package/dist/pi-bridge/bundle.js +475 -37
  85. package/dist/pi-bridge/index.js +42 -21
  86. package/dist/pi-bridge/schemaTranslate.d.ts +4 -8
  87. package/dist/pi-bridge/schemaTranslate.js +4 -8
  88. package/dist/probes/kimi-code.d.ts +12 -0
  89. package/dist/probes/kimi-code.js +55 -0
  90. package/dist/sdk.d.ts +1 -1
  91. package/docs/architecture.md +224 -26
  92. package/docs/assets/agent-trellis-architecture-horizontal.svg +80 -0
  93. package/docs/assets/agent-trellis-architecture-vertical.svg +82 -0
  94. package/docs/assets/trellis-mark.svg +12 -0
  95. package/docs/getting-started.md +328 -35
  96. package/docs/overview.md +131 -0
  97. package/docs/overview.zh-CN.md +100 -0
  98. package/docs/research.md +59 -0
  99. package/docs/roadmap.md +9 -5
  100. package/package.json +3 -2
  101. package/schema/builtin-skills/trellis-runtime/SKILL.md +117 -0
  102. package/schema/capability-selection.example.yaml +23 -0
  103. package/schema/scope.example.yaml +1 -1
  104. package/schema/servers.example.yaml +15 -1
package/README.md CHANGED
@@ -1,15 +1,42 @@
1
1
  # Trellis
2
2
 
3
- **A single source of capability for every coding agent you run.**
3
+ <p align="center">
4
+ <img src="docs/assets/trellis-mark.svg" width="88" alt="Trellis logo" />
5
+ </p>
4
6
 
5
- Skills, MCP servers, subagent definitions, shared memory, and secret policy —
6
- defined once, adapted natively into Claude Code, Codex, Kiro, and the [pi coding
7
- agent](https://github.com/earendil-works/pi), without drift and without a second
8
- copy of anything.
7
+ <p align="center"><strong>Unified Code Agent Runtime</strong></p>
9
8
 
10
- Trellis does not replace any of these agents' native config. It generates and
11
- verifies each one's native adapter from one canonical source, and refuses to let
12
- a plaintext secret or a duplicated skill file slip through.
9
+ Trellis gives Claude Code, Codex, Kiro, pi, and Kimi Code one Runtime for
10
+ Skills, MCP, Memory, and shared instructions.
11
+
12
+ > 中文:Trellis 是统一的 Code Agent Runtime,为多个 Code Agent 提供一致的
13
+ > Skill、MCP、Memory 与共享指令能力。
14
+
15
+ Trellis does not replace an Agent's native runtime. It manages one canonical
16
+ source and generates verified native or Runtime projections from it.
17
+
18
+ [Read the product overview →](docs/overview.md)
19
+
20
+ ![Agent Trellis architecture](docs/assets/agent-trellis-architecture-horizontal.svg)
21
+
22
+ ## Install
23
+
24
+ ```bash
25
+ npm install -g agent-trellis
26
+ ```
27
+
28
+ ## Quick start
29
+
30
+ ```bash
31
+ trellis onboard --dry-run
32
+ trellis onboard
33
+ trellis doctor
34
+ ```
35
+
36
+ Use the dry-run to review migration source, managed Agents, MCP mode, and
37
+ Memory before applying. See [`docs/overview.md`](docs/overview.md) for the
38
+ short product explanation and [`docs/getting-started.md`](docs/getting-started.md)
39
+ for the complete workflow.
13
40
 
14
41
  ## Why
15
42
 
@@ -26,14 +53,6 @@ Trellis aligns with the emerging [`.agents Protocol`](https://dotagentsprotocol.
26
53
  draft rather than inventing a sixth competing standard, and is likely its first
27
54
  working implementation.
28
55
 
29
- ## Install
30
-
31
- ```
32
- npm install -g agent-trellis
33
- ```
34
-
35
- ## Quick start
36
-
37
56
  See [`docs/getting-started.md`](docs/getting-started.md) for the detailed
38
57
  walkthrough — example output for each command, what each `migrate`/`sync`
39
58
  conflict action means, and troubleshooting.
@@ -44,7 +63,7 @@ conflict action means, and troubleshooting.
44
63
  trellis onboard
45
64
  ```
46
65
 
47
- Creates canonical source, detects which of Claude Code/Codex/Kiro/pi are on
66
+ Creates canonical source, detects which of Claude Code/Codex/Kiro/pi/Kimi Code are on
48
67
  this machine, then resolves two independent choices: a **migration source**
49
68
  (read from — auto-selected if only one present agent has real content,
50
69
  prompted with a numbered choice if more than one, skippable if starting
@@ -62,19 +81,63 @@ type by hand. If no agent is detected, it prints each supported agent's
62
81
  real install command/URL and stops — it never installs anything on its own
63
82
  initiative. Add `--dry-run` to preview the whole thing with zero writes.
64
83
 
84
+ Onboarding only adds to the existing managed set. To deliberately narrow or
85
+ replace Trellis's future write boundary without touching an agent's current
86
+ native state, use `trellis manage list|set|add|remove`; these mutations are
87
+ dry-runnable and backed by `trellis rollback`.
88
+
65
89
  ```
66
90
  trellis onboard --agent claude-code --manage pi
67
91
  ```
68
92
 
93
+ ## Trellis itself as a Skill
94
+
95
+ Trellis ships one package-owned Skill, `trellis-runtime`, with two layers:
96
+
97
+ | Layer | Who uses it | What it covers |
98
+ | --- | --- | --- |
99
+ | Takeover and migration | the operator, with Agent assistance | inspect, dry-run, choose a migration source, choose the managed boundary, select Skills/MCP/Memory, authorize OAuth, sync, audit, verify, and rollback |
100
+ | Runtime consumption | every managed Agent | discover and read canonical Skills, inspect shared Memory and global instructions, diagnose MCP, see managed Agents, and inspect or explicitly confirm task handoffs |
101
+
102
+ `trellis init` bootstraps this Skill into
103
+ `~/.trellis/skills/trellis-runtime/SKILL.md`. After an Agent is managed,
104
+ `trellis sync` delivers it in the Agent's native Skill location for native or
105
+ `both` delivery. For Runtime-only delivery, `SkillProvider` exposes the same
106
+ canonical Skill through the `trellis` MCP Runtime; Kimi Code uses
107
+ `trellis kimi` to prevent a second native Skill discovery path. This is one
108
+ canonical Skill, not a copied per-Agent variant.
109
+
110
+ Before takeover, the Skill cannot be consumed from an Agent that has not been
111
+ connected to Trellis yet; use the CLI and
112
+ [`docs/getting-started.md`](docs/getting-started.md#trellis-runtime-skill)
113
+ for the first-run path. After takeover, the Skill is the Agent's operating
114
+ guide for Trellis-managed capabilities. The MCP client may display names such
115
+ as `mcp__trellis__skills_search`; `mcp__` is client-generated, while Trellis's
116
+ portable names are `skills_search`, `memory_search`, `runtime_status`, and so
117
+ on.
118
+
69
119
  **Or step by step** (what `onboard` is actually doing under the hood, if you
70
120
  want to run any one stage on its own):
71
121
 
122
+ For standard MCP Router/client exports, use `trellis mcp import <json-file>`.
123
+ It reads `mcpServers` without changing the source, de-duplicates existing
124
+ servers, extracts credential-like environment values into the ignored local
125
+ secret file, converts supported `mcp-remote` Basic Auth entries to native HTTP,
126
+ and reports unsupported inline credentials or unavailable paths.
127
+ Always run it with `--dry-run` first; see the detailed importer rules in
128
+ [`docs/getting-started.md`](docs/getting-started.md).
129
+
130
+ After upgrading Trellis, refresh the package-owned Runtime Skill with
131
+ `trellis skill update-builtin --dry-run` followed by
132
+ `trellis skill update-builtin`; the operation is backed up and does not require
133
+ editing Agent-native files.
134
+
72
135
  **Already using Claude Code, Codex, Kiro, or pi and want to migrate what you
73
136
  already have?**
74
137
 
75
138
  1. `trellis init` — creates `~/.trellis/` with a minimal skeleton (only what's
76
139
  missing; never overwrites a file you already have) and tells you which of
77
- the four agents it found on this machine.
140
+ the five supported agents it found on this machine.
78
141
  2. `trellis migrate --from <agent>` — once per agent you already use. Copies
79
142
  that agent's real skills and instructions into canonical source. Never
80
143
  overwrites: an already-identical skill is reported and skipped, a genuine
@@ -86,8 +149,7 @@ already have?**
86
149
  but not managed is never touched. Add `--dry-run` to preview first.
87
150
  4. `trellis mcp sync` — distributes `~/.trellis/mcp/servers.yaml` (see
88
151
  [`schema/servers.example.yaml`](schema/servers.example.yaml)) to every
89
- managed agent's native MCP config (create/repair only — see Known
90
- limitations).
152
+ managed agent's native MCP config, including ownership-safe removal.
91
153
  5. `trellis secrets audit` — fails non-zero if any managed agent's real
92
154
  config holds a literal credential or an unexpected env var name.
93
155
  6. `trellis doctor` — read-only scan of every present agent's current state
@@ -118,15 +180,16 @@ restore with zero writes.
118
180
 
119
181
  ## Status
120
182
 
121
- **Early, pre-1.0.** All eight CLI commands above (`onboard`, `init`,
122
- `migrate`, `doctor`, `sync`, `mcp sync`, `secrets audit`, `rollback`) are implemented,
123
- unit-tested, and verified end-to-end against real Docker containers (never a
124
- developer's own dotfiles during development — see
125
- [`docs/architecture.md`](docs/architecture.md)'s testing philosophy). See
126
- [`docs/roadmap.md`](docs/roadmap.md) for what's shipped (P0–P6) vs. planned
127
- (P7, a GUI). `onboard` picks one agent as the migration base when more than
128
- one is present — merging differing content across multiple agents into one
129
- result is named future work, not built yet.
183
+ **Early, pre-1.0.** The CLI commands above are implemented, unit-tested, and
184
+ verified against isolated Linux Docker scenarios (never a developer's own
185
+ dotfiles during development — see
186
+ [`docs/architecture.md`](docs/architecture.md)'s testing philosophy). The
187
+ sandbox matrix covers migration, steady-state management, runtime routing,
188
+ failure isolation, and expected conflicts. A separate agent image installs
189
+ real Codex, Claude Code, Kiro CLI, and pi binaries to verify their isolated
190
+ configuration behavior. `onboard` picks one agent as the migration base when
191
+ more than one is present; merging differing content across multiple agents
192
+ into one result is named future work, not built yet.
130
193
 
131
194
  **Known limitations, honestly stated rather than discovered the hard way:**
132
195
  - MCP servers are never spawned/handshake-tested by `trellis mcp sync` or
@@ -136,16 +199,12 @@ result is named future work, not built yet.
136
199
  - The pi bridge extension (`trellis-pi-mcp-bridge`) has been verified to
137
200
  load and register tools without erroring, never against a real LLM tool
138
201
  call in production.
139
- - Verification has run against real Docker containers and a real,
140
- isolated pi CLI install — not yet against a developer's actual, existing
141
- `~/.claude`/`~/.codex`/`~/.kiro`/`~/.pi` in daily use. If you hit
142
- something a clean-room sandbox wouldn't have caught, please open an
143
- issue.
144
- - Automatic removal of an MCP server is deliberately unsupported (create/
145
- repair only) until an ownership-tracking mechanism exists — see
146
- `docs/roadmap.md`'s P2 note.
202
+ - Verification has run against real Docker containers and real isolated
203
+ Codex, Claude Code, Kiro CLI, and pi installs — not against a developer's
204
+ actual daily-use agent directories. If you hit something a clean-room
205
+ sandbox wouldn't have caught, please open an issue.
147
206
  - `~/.trellis/backups/` has no automatic pruning yet — every `sync`/
148
- `mcp sync`/`onboard` run that performs a real write adds one more run
207
+ `mcp sync`/`onboard`/`mcp import` run that performs a real write adds one more run
149
208
  directory, with no cap. Delete old ones by hand for now.
150
209
 
151
210
  A canonical MCP server can also declare `enabled: false` (kept defined,
@@ -13,6 +13,7 @@ import { existsSync, readFileSync } from "node:fs";
13
13
  import { dirname, join } from "node:path";
14
14
  import { homedir } from "node:os";
15
15
  import { isInScope } from "../core/adapter.js";
16
+ import { usesNativeCapabilityDelivery } from "../core/types.js";
16
17
  import * as claudeCodeProbe from "../probes/claude-code.js";
17
18
  import { applySymlinkPlan, planSymlinks } from "./symlinkPlan.js";
18
19
  import { applyJsonMcp, planJsonMcp, renderJsonServerEntry } from "./jsonMcp.js";
@@ -32,7 +33,7 @@ export class ClaudeCodeAdapter {
32
33
  const canonicalRoot = dirname(canonical.instructionsFile);
33
34
  const skillsRoot = join(this.homeDir, ".claude", "skills");
34
35
  const desiredSkills = canonical.skills
35
- .filter((skill) => isInScope(this.id, skill.scope, canonical.managedAgents))
36
+ .filter((skill) => usesNativeCapabilityDelivery(this.id, canonical.mcp) && isInScope(this.id, skill.scope, canonical.managedAgents))
36
37
  .map((skill) => ({ name: skill.name, target: skill.dir }));
37
38
  const skillItems = planSymlinks({
38
39
  rootDir: skillsRoot,
@@ -82,7 +83,7 @@ export class ClaudeCodeAdapter {
82
83
  return { ok: false, mismatches: ["claude-code is not present on this machine"] };
83
84
  }
84
85
  const mismatches = [];
85
- const desiredNames = new Set(canonical.skills.filter((s) => isInScope(this.id, s.scope, canonical.managedAgents)).map((s) => s.name));
86
+ const desiredNames = new Set(canonical.skills.filter((s) => usesNativeCapabilityDelivery(this.id, canonical.mcp) && isInScope(this.id, s.scope, canonical.managedAgents)).map((s) => s.name));
86
87
  const actualSkills = new Set(snapshot.skillRoots.flatMap((root) => root.skills).map((s) => s.name));
87
88
  for (const name of desiredNames) {
88
89
  if (!actualSkills.has(name)) {
@@ -19,6 +19,7 @@ import { existsSync, readFileSync } from "node:fs";
19
19
  import { basename, dirname, join } from "node:path";
20
20
  import { homedir } from "node:os";
21
21
  import { isInScope } from "../core/adapter.js";
22
+ import { usesNativeCapabilityDelivery } from "../core/types.js";
22
23
  import * as codexProbe from "../probes/codex.js";
23
24
  import { readInstructionsPath } from "../probes/codex.js";
24
25
  import { applySymlinkPlan, planSymlinks } from "./symlinkPlan.js";
@@ -40,7 +41,7 @@ export class CodexAdapter {
40
41
  const canonicalRoot = dirname(canonical.instructionsFile);
41
42
  const skillsRoot = join(this.homeDir, ".agents", "skills");
42
43
  const desiredSkills = canonical.skills
43
- .filter((skill) => isInScope(this.id, skill.scope, canonical.managedAgents))
44
+ .filter((skill) => usesNativeCapabilityDelivery(this.id, canonical.mcp) && isInScope(this.id, skill.scope, canonical.managedAgents))
44
45
  .map((skill) => ({ name: skill.name, target: skill.dir }));
45
46
  const skillItems = planSymlinks({
46
47
  rootDir: skillsRoot,
@@ -136,7 +137,7 @@ export class CodexAdapter {
136
137
  return { ok: false, mismatches: ["codex is not present on this machine"] };
137
138
  }
138
139
  const mismatches = [];
139
- const desiredNames = new Set(canonical.skills.filter((s) => isInScope(this.id, s.scope, canonical.managedAgents)).map((s) => s.name));
140
+ const desiredNames = new Set(canonical.skills.filter((s) => usesNativeCapabilityDelivery(this.id, canonical.mcp) && isInScope(this.id, s.scope, canonical.managedAgents)).map((s) => s.name));
140
141
  const actualSkills = new Set(snapshot.skillRoots.flatMap((root) => root.skills).map((s) => s.name));
141
142
  for (const name of desiredNames) {
142
143
  if (!actualSkills.has(name)) {
@@ -15,6 +15,7 @@ import type { AdapterPlanItem } from "../core/adapter.js";
15
15
  * Codex's TOML does — see tomlSection.ts's two-table rendering)
16
16
  * (trellis-mcp-static-env-and-disabled-servers design.md D4). */
17
17
  export declare function renderJsonServerEntry(def: McpServerDef): Record<string, unknown>;
18
+ export type JsonMcpEntryRenderer = (def: McpServerDef, name: string) => Record<string, unknown>;
18
19
  export declare function planJsonMcp(opts: {
19
20
  configPath: string;
20
21
  parsed: Record<string, unknown> | undefined;
@@ -27,9 +28,10 @@ export declare function planJsonMcp(opts: {
27
28
  * pass `{}`) to get today's create/repair/conflict-only behavior with
28
29
  * zero removal candidates. */
29
30
  ownership?: Record<string, unknown>;
31
+ render?: JsonMcpEntryRenderer;
30
32
  }): AdapterPlanItem[];
31
33
  /** Merges every `"create"` MCP item into `parsed` (or a fresh `{}` if the
32
34
  * file didn't exist) and deletes every `"remove"` item's key, returning
33
35
  * the object to stringify — every sibling top-level key on `parsed` is
34
36
  * spread through untouched. */
35
- export declare function applyJsonMcp(parsed: Record<string, unknown> | undefined, items: AdapterPlanItem[]): Record<string, unknown>;
37
+ export declare function applyJsonMcp(parsed: Record<string, unknown> | undefined, items: AdapterPlanItem[], render?: JsonMcpEntryRenderer): Record<string, unknown>;
@@ -8,7 +8,7 @@
8
8
  */
9
9
  import { deepEqual } from "../lib/deepEqual.js";
10
10
  import { resolvedEnvTextMap } from "../lib/mcpMigrateRead.js";
11
- import { resolveMcpPlan } from "./mcpPlan.js";
11
+ import { LEGACY_GATEWAY_ENTRY_NAME, LEGACY_RUNTIME_ENTRY_NAME, resolveMcpPlan } from "./mcpPlan.js";
12
12
  /** Renders the native JSON shape Claude Code/Kiro's `mcpServers` map
13
13
  * expects — `env` names render as `${VAR}` references, `staticEnv`
14
14
  * entries render as their literal values, both in the same map (JSON's
@@ -32,11 +32,12 @@ export function renderJsonServerEntry(def) {
32
32
  }
33
33
  export function planJsonMcp(opts) {
34
34
  const { configPath, parsed, mcp, agentId, managedAgents, policy, ownership } = opts;
35
+ const render = opts.render ?? ((def, _name) => renderJsonServerEntry(def));
35
36
  const { desired, conflicts } = resolveMcpPlan(agentId, mcp, managedAgents, policy);
36
37
  const existingServers = parsed?.mcpServers ?? {};
37
38
  const items = [];
38
39
  for (const { name, def } of desired) {
39
- const rendered = renderJsonServerEntry(def);
40
+ const rendered = render(def, name);
40
41
  const current = existingServers[name];
41
42
  if (deepEqual(current, rendered)) {
42
43
  continue; // already correct — no-op
@@ -59,6 +60,19 @@ export function planJsonMcp(opts) {
59
60
  for (const [name, expected] of Object.entries(ownership ?? {})) {
60
61
  if (desiredNames.has(name))
61
62
  continue; // still wanted — not a removal candidate at all
63
+ if ((name === LEGACY_GATEWAY_ENTRY_NAME || name === LEGACY_RUNTIME_ENTRY_NAME) && desiredNames.has("trellis")) {
64
+ const current = existingServers[name];
65
+ if (current !== undefined && deepEqual(current, expected)) {
66
+ items.push({
67
+ action: "remove",
68
+ kind: "mcp",
69
+ target: configPath,
70
+ mcpRemove: { name },
71
+ description: `legacy MCP server "${name}" removed from ${configPath} after Trellis Runtime key migration`,
72
+ });
73
+ }
74
+ continue;
75
+ }
62
76
  const current = existingServers[name];
63
77
  if (current === undefined)
64
78
  continue; // already gone — nothing to remove, ledger is pruned separately
@@ -78,7 +92,7 @@ export function planJsonMcp(opts) {
78
92
  * file didn't exist) and deletes every `"remove"` item's key, returning
79
93
  * the object to stringify — every sibling top-level key on `parsed` is
80
94
  * spread through untouched. */
81
- export function applyJsonMcp(parsed, items) {
95
+ export function applyJsonMcp(parsed, items, render = (def, _name) => renderJsonServerEntry(def)) {
82
96
  const base = parsed ?? {};
83
97
  const existingServers = base.mcpServers ?? {};
84
98
  const mergedServers = { ...existingServers };
@@ -86,7 +100,7 @@ export function applyJsonMcp(parsed, items) {
86
100
  if (item.kind !== "mcp")
87
101
  continue;
88
102
  if (item.action === "create" && item.mcpWrite) {
89
- mergedServers[item.mcpWrite.name] = renderJsonServerEntry(item.mcpWrite.def);
103
+ mergedServers[item.mcpWrite.name] = render(item.mcpWrite.def, item.mcpWrite.name);
90
104
  }
91
105
  else if (item.action === "remove" && item.mcpRemove) {
92
106
  delete mergedServers[item.mcpRemove.name];
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Kimi Code Runtime-first adapter.
3
+ *
4
+ * Runtime delivery owns only ~/.kimi-code/mcp.json. Native Kimi Skills and
5
+ * AGENTS.md remain opt-in for `native`/`both`; Runtime-only delivery uses
6
+ * `trellis kimi`, which launches Kimi with an empty --skills-dir root so the
7
+ * shared ~/.agents/skills tree is not discovered a second time.
8
+ */
9
+ import type { AdapterPlanItem, AdapterProbeResult, AdapterVerifyResult, TrellisAdapter } from "../core/adapter.js";
10
+ import type { CanonicalSource } from "../core/types.js";
11
+ import type { BackupSession } from "../lib/backup.js";
12
+ export declare class KimiCodeAdapter implements TrellisAdapter {
13
+ private readonly homeDir;
14
+ readonly name = "Kimi Code";
15
+ readonly id: "kimi-code";
16
+ constructor(homeDir?: string);
17
+ probe(): Promise<AdapterProbeResult>;
18
+ plan(canonical: CanonicalSource): Promise<AdapterPlanItem[]>;
19
+ apply(plan: AdapterPlanItem[], backup: BackupSession): Promise<void>;
20
+ verify(canonical: CanonicalSource): Promise<AdapterVerifyResult>;
21
+ }
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Kimi Code Runtime-first adapter.
3
+ *
4
+ * Runtime delivery owns only ~/.kimi-code/mcp.json. Native Kimi Skills and
5
+ * AGENTS.md remain opt-in for `native`/`both`; Runtime-only delivery uses
6
+ * `trellis kimi`, which launches Kimi with an empty --skills-dir root so the
7
+ * shared ~/.agents/skills tree is not discovered a second time.
8
+ */
9
+ import { existsSync, readFileSync } from "node:fs";
10
+ import { homedir } from "node:os";
11
+ import { dirname, join } from "node:path";
12
+ import { isInScope } from "../core/adapter.js";
13
+ import { usesNativeCapabilityDelivery } from "../core/types.js";
14
+ import * as kimiProbe from "../probes/kimi-code.js";
15
+ import { applyJsonMcp, planJsonMcp, renderJsonServerEntry } from "./jsonMcp.js";
16
+ import { RUNTIME_COMMAND, RUNTIME_ENTRY_NAME } from "./mcpPlan.js";
17
+ import { applySymlinkPlan, planSymlinks } from "./symlinkPlan.js";
18
+ import { loadMcpOwnership, ownedByAgent, recordOwned, forgetOwned, saveMcpOwnership } from "../lib/mcpOwnership.js";
19
+ const KIMI_RUNTIME_STARTUP_TIMEOUT_MS = 30_000;
20
+ function renderKimiServerEntry(def, name) {
21
+ const entry = renderJsonServerEntry(def);
22
+ if (def.transport === "stdio")
23
+ delete entry.type;
24
+ if (name === RUNTIME_ENTRY_NAME && def.command === RUNTIME_COMMAND) {
25
+ entry.deferred = true;
26
+ entry.startupTimeoutMs = KIMI_RUNTIME_STARTUP_TIMEOUT_MS;
27
+ }
28
+ return entry;
29
+ }
30
+ export class KimiCodeAdapter {
31
+ homeDir;
32
+ name = "Kimi Code";
33
+ id = "kimi-code";
34
+ constructor(homeDir = homedir()) {
35
+ this.homeDir = homeDir;
36
+ }
37
+ async probe() {
38
+ const snapshot = await kimiProbe.probe(this.homeDir);
39
+ return { present: snapshot.present, version: snapshot.version };
40
+ }
41
+ async plan(canonical) {
42
+ const native = usesNativeCapabilityDelivery(this.id, canonical.mcp);
43
+ const items = [];
44
+ const root = join(this.homeDir, ".kimi-code");
45
+ const canonicalRoot = dirname(canonical.instructionsFile);
46
+ items.push(...planSymlinks({
47
+ rootDir: join(root, "skills"),
48
+ desired: native
49
+ ? canonical.skills
50
+ .filter((skill) => isInScope(this.id, skill.scope, canonical.managedAgents))
51
+ .map((skill) => ({ name: skill.name, target: skill.dir }))
52
+ : [],
53
+ canonicalRoot: join(canonicalRoot, "skills"),
54
+ kind: "skill",
55
+ }));
56
+ items.push(...planSymlinks({
57
+ rootDir: root,
58
+ desired: native ? [{ name: "AGENTS.md", target: canonical.instructionsFile }] : [],
59
+ canonicalRoot,
60
+ kind: "instructions",
61
+ }));
62
+ const configPath = join(this.homeDir, ".kimi-code", "mcp.json");
63
+ const parsed = existsSync(configPath) ? JSON.parse(readFileSync(configPath, "utf-8")) : undefined;
64
+ const ownership = ownedByAgent(loadMcpOwnership(this.homeDir), this.id);
65
+ items.push(...planJsonMcp({
66
+ configPath,
67
+ parsed,
68
+ mcp: canonical.mcp,
69
+ agentId: this.id,
70
+ managedAgents: canonical.managedAgents,
71
+ policy: canonical.secretsPolicy,
72
+ ownership,
73
+ render: renderKimiServerEntry,
74
+ }));
75
+ return items;
76
+ }
77
+ async apply(plan, backup) {
78
+ await applySymlinkPlan(plan.filter((item) => item.kind === "skill" || item.kind === "instructions"), backup);
79
+ const mcpWrites = plan.filter((item) => item.kind === "mcp" && (item.action === "create" || item.action === "remove"));
80
+ if (mcpWrites.length === 0)
81
+ return;
82
+ const configPath = mcpWrites[0].target;
83
+ const parsed = existsSync(configPath) ? JSON.parse(readFileSync(configPath, "utf-8")) : undefined;
84
+ const merged = applyJsonMcp(parsed, mcpWrites, renderKimiServerEntry);
85
+ backup.writeFile(configPath, `${JSON.stringify(merged, null, 2)}\n`);
86
+ let ownership = loadMcpOwnership(this.homeDir);
87
+ for (const item of mcpWrites) {
88
+ if (item.action === "create" && item.mcpWrite)
89
+ ownership = recordOwned(ownership, this.id, item.mcpWrite.name, renderKimiServerEntry(item.mcpWrite.def, item.mcpWrite.name));
90
+ if (item.action === "remove" && item.mcpRemove)
91
+ ownership = forgetOwned(ownership, this.id, item.mcpRemove.name);
92
+ }
93
+ saveMcpOwnership(this.homeDir, ownership);
94
+ }
95
+ async verify(canonical) {
96
+ const snapshot = await kimiProbe.probe(this.homeDir);
97
+ if (!snapshot.present)
98
+ return { ok: false, mismatches: ["kimi-code is not present on this machine"] };
99
+ const desired = new Set(canonical.skills.filter((skill) => usesNativeCapabilityDelivery(this.id, canonical.mcp) && isInScope(this.id, skill.scope, canonical.managedAgents)).map((skill) => skill.name));
100
+ const actual = new Set(snapshot.skillRoots.flatMap((root) => root.skills).filter((skill) => skill.dir.includes("/.kimi-code/skills/")).map((skill) => skill.name));
101
+ const mismatches = [
102
+ ...[...desired].filter((name) => !actual.has(name)).map((name) => `skill "${name}" is missing on Kimi Code`),
103
+ ...[...actual].filter((name) => !desired.has(name)).map((name) => `skill "${name}" is not in Kimi Code's canonical scope`),
104
+ ];
105
+ return mismatches.length === 0 ? { ok: true } : { ok: false, mismatches };
106
+ }
107
+ }
@@ -19,6 +19,7 @@ import { existsSync, mkdirSync, readFileSync } from "node:fs";
19
19
  import { dirname, join } from "node:path";
20
20
  import { homedir } from "node:os";
21
21
  import { isInScope } from "../core/adapter.js";
22
+ import { usesNativeCapabilityDelivery } from "../core/types.js";
22
23
  import * as kiroProbe from "../probes/kiro.js";
23
24
  import { applySymlinkPlan, planSymlinks } from "./symlinkPlan.js";
24
25
  import { applyJsonMcp, planJsonMcp, renderJsonServerEntry } from "./jsonMcp.js";
@@ -48,7 +49,7 @@ export class KiroAdapter {
48
49
  const canonicalRoot = dirname(canonical.instructionsFile);
49
50
  const skillsRoot = join(this.homeDir, ".kiro", "skills");
50
51
  const desiredSkills = canonical.skills
51
- .filter((skill) => isInScope(this.id, skill.scope, canonical.managedAgents))
52
+ .filter((skill) => usesNativeCapabilityDelivery(this.id, canonical.mcp) && isInScope(this.id, skill.scope, canonical.managedAgents))
52
53
  .map((skill) => ({ name: skill.name, target: skill.dir }));
53
54
  const skillItems = planSymlinks({
54
55
  rootDir: skillsRoot,
@@ -161,7 +162,7 @@ export class KiroAdapter {
161
162
  return { ok: false, mismatches: ["kiro is not present on this machine"] };
162
163
  }
163
164
  const mismatches = [];
164
- const desiredNames = new Set(canonical.skills.filter((s) => isInScope(this.id, s.scope, canonical.managedAgents)).map((s) => s.name));
165
+ const desiredNames = new Set(canonical.skills.filter((s) => usesNativeCapabilityDelivery(this.id, canonical.mcp) && isInScope(this.id, s.scope, canonical.managedAgents)).map((s) => s.name));
165
166
  const actualSkills = new Set(snapshot.skillRoots.flatMap((root) => root.skills).map((s) => s.name));
166
167
  for (const name of desiredNames) {
167
168
  if (!actualSkills.has(name)) {
@@ -18,12 +18,17 @@
18
18
  */
19
19
  import type { AgentId, McpConfig, McpServerDef, SecretsPolicy } from "../core/types.js";
20
20
  export declare const HUB_ENTRY_NAME = "trellis-hub";
21
- export declare const GATEWAY_ENTRY_NAME = "trellis-gateway";
21
+ export declare const GATEWAY_ENTRY_NAME = "trellis";
22
+ export declare const LEGACY_GATEWAY_ENTRY_NAME = "trellis-gateway";
23
+ export declare const RUNTIME_ENTRY_NAME = "trellis";
24
+ export declare const LEGACY_RUNTIME_ENTRY_NAME = "trellis-runtime";
22
25
  /** The command an agent spawns in gateway mode. Bare `trellis` rather than
23
26
  * an absolute path: the entry has to keep working across reinstalls and
24
27
  * version bumps, and every agent resolves it through the same PATH the
25
28
  * user installed the CLI onto. */
26
29
  export declare const GATEWAY_COMMAND = "trellis";
30
+ export declare const RUNTIME_COMMAND = "trellis";
31
+ export declare function isOAuthMcpServer(def: McpServerDef): boolean;
27
32
  /**
28
33
  * Gateway mode applies to every managed agent when `agents` is omitted —
29
34
  * the expected normal case (design.md D14). An explicit list narrows it,