agent-trellis 0.1.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 (80) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +127 -0
  3. package/dist/adapters/claude-code.d.ts +23 -0
  4. package/dist/adapters/claude-code.js +86 -0
  5. package/dist/adapters/codex.d.ts +27 -0
  6. package/dist/adapters/codex.js +119 -0
  7. package/dist/adapters/jsonMcp.d.ts +24 -0
  8. package/dist/adapters/jsonMcp.js +84 -0
  9. package/dist/adapters/kiro.d.ts +34 -0
  10. package/dist/adapters/kiro.js +175 -0
  11. package/dist/adapters/mcpPlan.d.ts +28 -0
  12. package/dist/adapters/mcpPlan.js +83 -0
  13. package/dist/adapters/pi.d.ts +23 -0
  14. package/dist/adapters/pi.js +108 -0
  15. package/dist/adapters/symlinkPlan.d.ts +33 -0
  16. package/dist/adapters/symlinkPlan.js +120 -0
  17. package/dist/cli.d.ts +7 -0
  18. package/dist/cli.js +135 -0
  19. package/dist/commands/doctor.d.ts +88 -0
  20. package/dist/commands/doctor.js +269 -0
  21. package/dist/commands/init.d.ts +44 -0
  22. package/dist/commands/init.js +150 -0
  23. package/dist/commands/mcp.d.ts +28 -0
  24. package/dist/commands/mcp.js +70 -0
  25. package/dist/commands/migrate.d.ts +38 -0
  26. package/dist/commands/migrate.js +132 -0
  27. package/dist/commands/onboard.d.ts +50 -0
  28. package/dist/commands/onboard.js +155 -0
  29. package/dist/commands/secretsAudit.d.ts +35 -0
  30. package/dist/commands/secretsAudit.js +115 -0
  31. package/dist/commands/sync.d.ts +40 -0
  32. package/dist/commands/sync.js +91 -0
  33. package/dist/core/adapter.d.ts +133 -0
  34. package/dist/core/adapter.js +16 -0
  35. package/dist/core/canonical.d.ts +16 -0
  36. package/dist/core/canonical.js +148 -0
  37. package/dist/core/types.d.ts +201 -0
  38. package/dist/core/types.js +15 -0
  39. package/dist/lib/dirEquals.d.ts +7 -0
  40. package/dist/lib/dirEquals.js +39 -0
  41. package/dist/lib/envVarNames.d.ts +35 -0
  42. package/dist/lib/envVarNames.js +79 -0
  43. package/dist/lib/fsIdentity.d.ts +16 -0
  44. package/dist/lib/fsIdentity.js +53 -0
  45. package/dist/lib/mcpProbe.d.ts +14 -0
  46. package/dist/lib/mcpProbe.js +96 -0
  47. package/dist/lib/probeCommon.d.ts +24 -0
  48. package/dist/lib/probeCommon.js +108 -0
  49. package/dist/lib/secretEnv.d.ts +19 -0
  50. package/dist/lib/secretEnv.js +46 -0
  51. package/dist/lib/skillFile.d.ts +12 -0
  52. package/dist/lib/skillFile.js +26 -0
  53. package/dist/lib/syncArgs.d.ts +16 -0
  54. package/dist/lib/syncArgs.js +17 -0
  55. package/dist/lib/tomlSection.d.ts +57 -0
  56. package/dist/lib/tomlSection.js +162 -0
  57. package/dist/pi-bridge/bundle.js +32074 -0
  58. package/dist/pi-bridge/index.d.ts +48 -0
  59. package/dist/pi-bridge/index.js +188 -0
  60. package/dist/pi-bridge/schemaTranslate.d.ts +55 -0
  61. package/dist/pi-bridge/schemaTranslate.js +40 -0
  62. package/dist/probes/claude-code.d.ts +13 -0
  63. package/dist/probes/claude-code.js +48 -0
  64. package/dist/probes/codex.d.ts +24 -0
  65. package/dist/probes/codex.js +78 -0
  66. package/dist/probes/kiro.d.ts +12 -0
  67. package/dist/probes/kiro.js +48 -0
  68. package/dist/probes/pi.d.ts +14 -0
  69. package/dist/probes/pi.js +53 -0
  70. package/dist/sdk.d.ts +14 -0
  71. package/dist/sdk.js +13 -0
  72. package/docs/architecture.md +367 -0
  73. package/docs/getting-started.md +235 -0
  74. package/docs/implementation-plan.md +341 -0
  75. package/docs/research.md +175 -0
  76. package/docs/roadmap.md +484 -0
  77. package/package.json +59 -0
  78. package/schema/scope.example.yaml +33 -0
  79. package/schema/secrets.policy.example.yaml +43 -0
  80. package/schema/servers.example.yaml +87 -0
@@ -0,0 +1,367 @@
1
+ # Architecture
2
+
3
+ This document is for contributors and anyone evaluating Trellis's internals
4
+ — how adapters are built, why they're built the way they are, and the
5
+ testing discipline behind that. If you just want to *use* Trellis, see
6
+ [`docs/getting-started.md`](getting-started.md) instead; nothing below is
7
+ required reading for that.
8
+
9
+ ## Layers and who owns each one
10
+
11
+ ```
12
+ ┌─────────────────────────────────────────────────────────┐
13
+ │ Trellis canonical source (~/.trellis — global only, see │
14
+ │ "Global vs. workspace scope" below) │
15
+ │ instructions · skills · agents · mcp/servers.yaml · │
16
+ │ memories · secrets policy │
17
+ └───────────────┬─────────────────────────────────────────┘
18
+ │ trellis sync / trellis doctor
19
+ ┌───────┼────────┬─────────────┬──────────────┐
20
+ ▼ ▼ ▼ ▼
21
+ Claude Code Codex Kiro pi
22
+ (symlink adapter) (incremental (symlink adapter) (bridge
23
+ TOML writer) extension)
24
+ ```
25
+
26
+ Trellis owns exactly one thing: **the canonical source, and the generators
27
+ that turn it into each agent's native format.** It does not own MCP
28
+ transport, memory storage, or secret storage — those are delegated (see
29
+ [`research.md`](research.md)).
30
+
31
+ ## Canonical schema
32
+
33
+ Aligned to the `.agents Protocol` draft, extended where the draft is silent:
34
+
35
+ ```
36
+ .trellis/
37
+ ├── agents.md # instructions, AGENTS.md-compatible
38
+ ├── skills/<name>/SKILL.md # exact filename required — see research.md
39
+ ├── agents/<name>.md # subagent profiles (Claude-format frontmatter
40
+ │ # today; this is the layer with no cross-agent
41
+ │ # equivalent yet — Codex has no persistent
42
+ │ # subagent concept, see research.md)
43
+ ├── mcp/servers.yaml # single MCP source, values are var-name
44
+ │ # references only, never literals
45
+ ├── memories/*.md # shared memory entries (server-memory backed)
46
+ ├── scope.yaml # exceptions to "shared with all agents" —
47
+ │ # see "Private / agent-specific capabilities"
48
+ ├── secrets.policy.yaml # which var names are allowed, nothing else
49
+ └── trellis.lock.json # NOT YET BUILT — planned ownership-tracking
50
+ # file for MCP removal (docs/roadmap.md's P2
51
+ # note); nothing in src/ reads or writes this
52
+ ```
53
+
54
+ ## Global vs. workspace scope
55
+
56
+ **Global only, for now.** Trellis manages `~/.trellis` — one canonical
57
+ source per machine, applied to that machine's four agents. It does not yet
58
+ read or merge a project-local `.trellis/` in a specific repo.
59
+
60
+ The `.agents Protocol` draft (see `research.md`) describes a two-layer
61
+ merge — global defaults, workspace overrides on top, closest wins. That
62
+ model is a reasonable target eventually, but it's explicitly **out of
63
+ scope until a later phase**: it adds real complexity (merge precedence,
64
+ per-project drift, a second place secrets policy has to be checked) that
65
+ isn't justified before the single-layer global case is solid. Don't build
66
+ workspace resolution ahead of that decision — if you find yourself adding
67
+ a `root?: string` parameter or precedence logic to `loadCanonicalSource`,
68
+ that's scope creep against this section, not a natural extension.
69
+
70
+ ## Private / agent-specific capabilities
71
+
72
+ Not everything belongs to all four agents. A skill built around Claude
73
+ Code's Task-based subagent delegation has no equivalent to delegate to on
74
+ Codex; an MCP server might only make sense for one agent's workflow. Every
75
+ scopable item — skill, subagent profile, memory entry, MCP server —
76
+ defaults to "shared with all four," and can be restricted with an explicit
77
+ `scope` (skills/agents/memories) or inline `agents:` (MCP servers, see
78
+ below). Restriction is the exception you declare, not something you
79
+ configure for the common case.
80
+
81
+ **Skills, subagent profiles, and memory entries declare scope in
82
+ `.trellis/scope.yaml`, never inside the artifact file itself.** This
83
+ isn't a style preference: Codex validates `SKILL.md` frontmatter against
84
+ an allow-list of recognized keys and rejects files with unknown ones (see
85
+ `research.md`) — a Trellis-only `scope:` field written into a skill's own
86
+ frontmatter would break that skill specifically on Codex. Keeping scope
87
+ declarations in a separate manifest means every artifact stays a clean,
88
+ portable file exactly as its target agent's own spec expects; Trellis's
89
+ own bookkeeping lives next to it, not inside it. See
90
+ `schema/scope.example.yaml`.
91
+
92
+ **MCP servers are the one exception** — `agents:` is declared inline per
93
+ server in `mcp/servers.yaml` (see `schema/servers.example.yaml`), because
94
+ that file is Trellis's own format and is never handed to an agent
95
+ directly; every adapter translates it into that agent's native shape, so
96
+ there's no risk of an agent choking on an unrecognized field the way Codex
97
+ does with SKILL.md.
98
+
99
+ Every `TrellisAdapter.plan()` implementation must filter the canonical
100
+ source through scope before producing any plan item for it — an item
101
+ scoped away from that adapter's agent must never appear in its plan at
102
+ all. See `src/core/adapter.ts`'s `isInScope` helper and the obligation
103
+ documented on `plan()` itself.
104
+
105
+ ## Adapter contract
106
+
107
+ Every adapter must implement:
108
+
109
+ - `probe()` — does this agent exist on this machine, what version
110
+ - `plan(canonical)` — diffs canonical against this agent's current on-disk
111
+ state (read-only, but it does read — create/remove/no-op/conflict can't
112
+ be decided from canonical alone) and produces `AdapterPlanItem[]`, each
113
+ tagged `"create"`, `"remove"`, or `"conflict"`. **Must emit `"remove"`
114
+ items**, not just `"create"`: a Trellis-managed symlink (realpath
115
+ resolves inside the canonical source) whose entry was deleted from
116
+ canonical or scoped away from this agent is stale and belongs in the
117
+ plan — see `src/core/adapter.ts`'s `plan()` doc. A real, non-symlink path
118
+ occupying a spot Trellis would otherwise touch is a `"conflict"` item,
119
+ not a thrown error — it must show up in the same report as everything
120
+ else, not abort the whole run over one unrelated item.
121
+ - `apply(plan)` — perform the diff; must be idempotent and re-runnable for
122
+ "create"/"remove", and must treat "conflict" as report-only (no I/O,
123
+ never throws) — see `src/core/adapter.ts`'s `apply()` doc for why this
124
+ responsibility sits with the caller (`trellis sync`), not per-item
125
+ - `verify()` — re-read the agent's own state and confirm it matches
126
+ canonical; this is what `trellis doctor` calls
127
+
128
+ No adapter is allowed to overwrite fields it doesn't own. Codex's adapter in
129
+ particular must never rewrite `config.toml` wholesale — it must locate the
130
+ target `[mcp_servers.<name>]` section and patch only that block, because
131
+ Codex's own `config.toml` also holds mirasim-independent user settings
132
+ (models, trust levels) that Trellis has no business touching.
133
+
134
+ ### Adapter-specific notes
135
+
136
+ **Claude Code** — `~/.claude/skills`, `~/.claude/agents` become symlinks into
137
+ the canonical source. `~/.claude.json`'s `mcpServers` is patched in place
138
+ (JSON, so this is a simple merge). Values are always `${VAR}` references.
139
+ When `mcp.hub` is set, this becomes one entry (the hub URL) instead of N.
140
+
141
+ **Codex** — skills via `~/.agents/skills` symlink (Codex's own built-in
142
+ convention, requires no Trellis-specific path). MCP via `codex mcp add`
143
+ where possible; for env passthrough use `env_vars`, never `--env` with a
144
+ literal secret value. Must check for name collisions against any host-
145
+ injected servers (mirasim connectors) before writing — see research.md §3.
146
+ When `mcp.hub` is set, only the single hub entry needs this check — there's
147
+ nothing else defined locally for it to collide with.
148
+
149
+ **Kiro** — same shape as Claude Code: `~/.kiro/skills` symlink,
150
+ `~/.kiro/steering/CLAUDE.md` symlink, `~/.kiro/settings/mcp.json` patched
151
+ like Claude's. Same hub-mode simplification applies.
152
+
153
+ **pi** — same symlink shape as Claude Code/Kiro after all: `~/.pi/agent/skills`
154
+ symlink, instructions symlinked to whichever of `AGENTS.override.md` /
155
+ `AGENTS.md` / `CLAUDE.md` pi checks first (`~/.pi/agent/`, confirmed by
156
+ direct source read — see `openspec/specs/agent-state-probing/spec.md`'s pi
157
+ scenario). Pi *reads* these natively without any Trellis-specific parsing,
158
+ but the files still have to physically exist at that path — nothing
159
+ populates `~/.pi/agent/skills` on its own. (This corrects an earlier,
160
+ pre-P0 assumption that pi needed "no adapter" for skills/instructions;
161
+ that was written before pi's actual global discovery directory was
162
+ confirmed, when it wasn't yet known whether canonical skills would ever
163
+ reach it without one.) MCP is the one place pi is genuinely different: it
164
+ needs a real bridge either way, but which shape depends on `mcp.hub`:
165
+ without it, the bridge extension opens N `@modelcontextprotocol/sdk` stdio
166
+ clients (one per server) and registers each one's tools through pi's
167
+ `registerTool` API; with `mcp.hub` set, it opens exactly one HTTP client to
168
+ the hub instead — meaningfully less code and one fewer class of failure
169
+ (N processes to keep alive vs. one connection). This is the one piece of
170
+ the project that is an agent runtime extension, not a config generator,
171
+ regardless of hub mode.
172
+
173
+ ## MCP hub mode
174
+
175
+ Every agent's MCP surface can be either N direct server definitions
176
+ (default) or one static entry pointing at a single HTTP endpoint — set
177
+ `mcp.hub.url` in `mcp/servers.yaml` to switch (see
178
+ `schema/servers.example.yaml`). Nothing about *what* runs behind that URL
179
+ is part of Trellis's design: a self-hosted
180
+ [mcp-hub](https://github.com/ravitemer/mcp-hub) instance, a hosted
181
+ mcp-router account, anything else speaking MCP over HTTP all look
182
+ identical to every adapter — a URL. There is deliberately no "engine"
183
+ switch in the type (`HubConfig` is just `{ url: string }`) or in adapter
184
+ code — building one was tried and reverted as unneeded complexity for a
185
+ distinction (self-hosted vs. hosted, generated-config vs.
186
+ externally-managed) that only matters to the human choosing a hub, never
187
+ to the code writing one entry that points at it.
188
+
189
+ **What actually changes when `hub` is set:**
190
+ - Every adapter writes ONE entry instead of N. This is most of the value:
191
+ adding a new backend server means editing wherever the hub's own config
192
+ lives (this project doesn't prescribe that either) and never touching
193
+ any of the four agents' configs or restarting them, if the hub supports
194
+ live reload (mcp-hub does, via SSE).
195
+ - The collision check against `known_host_injected` (docs/research.md
196
+ §"Codex — three hard constraints") shrinks to checking one name instead
197
+ of N, since there's nothing else locally defined to collide with.
198
+ - pi's bridge (P4) becomes one HTTP client instead of N stdio clients.
199
+
200
+ **What doesn't change:** the boundary with mirasim (docs/research.md §3)
201
+ is unaffected — `mcp.hub`, self-hosted or not, only ever carries the
202
+ servers Trellis's own `servers.yaml` defines (local stdio, per that
203
+ boundary); mirasim's remote-OAuth connectors are injected at the agent's
204
+ own process level regardless of whether hub mode is on.
205
+
206
+ **The trade-off, stated plainly:** self-hosting a hub (or paying for a
207
+ hosted one) adds a moving part that direct mode doesn't have — if it's
208
+ down, every agent loses MCP capability at once, not just one server. And
209
+ if you point at a hub you manage outside Trellis (rather than one Trellis
210
+ generates config for), that hub's own dashboard/config becomes a second
211
+ place "what servers exist" is defined, outside `.trellis/` — this project
212
+ hit that exact problem first-hand with a token going stale across three
213
+ different values before `mcp.hub` existed as a concept (docs/research.md).
214
+ Direct mode remains fully supported for anyone who'd rather not take that
215
+ trade.
216
+
217
+ ## Static header auth vs. real OAuth for remote MCP servers
218
+
219
+ A remote (`http`/`sse`-transport) MCP server needing a credential comes
220
+ in two real shapes, and Trellis only ever builds for one of them.
221
+
222
+ **Static header auth** (a bearer token or API key that doesn't expire on
223
+ its own) is exactly like `env` for stdio servers: `McpServerDef.headers`
224
+ holds `${VAR}` references, never a value, and each adapter renders it
225
+ through its own real, verified schema — Claude Code and Kiro accept the
226
+ identical plain `Record<string,string>` map; Codex has no generic
227
+ headers concept at all, only a single purpose-built
228
+ `bearer_token_env_var` field (confirmed by what `codex mcp add
229
+ --bearer-token-env-var` itself generates) — a server needing more than
230
+ one header simply can't reach Codex through Trellis, and is refused
231
+ there (not silently dropped) while still reaching every other agent.
232
+
233
+ **Real OAuth** (browser redirect, short-lived access token, refresh
234
+ token) is not implemented anywhere in Trellis, on purpose. All three
235
+ native-config agents already have their own real, working flow for it:
236
+
237
+ - Claude Code: `claude mcp add --client-id/--client-secret/--callback-port`
238
+ - Codex: a dedicated `codex mcp login`/`codex mcp logout` pair
239
+ - Kiro: `oauth`/`oauthScopes` fields in its own real
240
+ `~/.kiro/settings/mcp.json` schema
241
+
242
+ pi has none of this — its bridge is Trellis's own code, and the MCP
243
+ SDK's `authProvider: OAuthClientProvider` option is a full
244
+ redirect-and-refresh flow that doesn't fit a synchronously-loaded
245
+ extension's lifecycle. A remote server that requires real OAuth simply
246
+ isn't reachable through the pi bridge today — a real, stated limitation,
247
+ not something papered over with a partial implementation.
248
+
249
+ ## What Trellis explicitly does not build
250
+
251
+ - An MCP aggregator/gateway's actual routing/proxy logic (hub mode above
252
+ lets you point every agent at one, but Trellis doesn't implement one)
253
+ - Real OAuth for remote MCP servers (browser redirect, token storage,
254
+ refresh) — every agent that has its own native flow keeps using it;
255
+ see "Static header auth vs. real OAuth" above
256
+ - A memory backend (defaults to `@modelcontextprotocol/server-memory`,
257
+ documented in `schema/servers.example.yaml`; mem0/OpenMemory and
258
+ totalrecallai-class semantic-search servers documented as opt-in
259
+ upgrades — see docs/research.md "Shared memory")
260
+ - A secret vault (reads `${VAR}` from whatever the environment already
261
+ provides — `~/.config/agent-env/secrets.env`, 1Password's `op run`,
262
+ anything that populates `process.env` before an adapter's generated
263
+ command runs)
264
+ - A GUI (P5 in the roadmap evaluates embedding into an existing one —
265
+ mcp-router's or skills-hub's — before building a new one)
266
+
267
+ ## MCP handshake probing is opt-in, not default
268
+
269
+ `trellis doctor`'s default run never spawns a configured MCP server — it
270
+ only reads static config (name, transport, collision against
271
+ `known_host_injected`). Live handshake probing (`src/lib/mcpProbe.ts`) is a
272
+ real capability, unit-tested against a fixture server, but running it
273
+ against every server configured on a real machine turned out not to be
274
+ "read-only" in the sense that actually matters: some servers reach real
275
+ external services with real credentials (OAuth-backed connectors,
276
+ `chrome-devtools-mcp`'s `--autoConnect`), and doing this for a dozen-plus
277
+ servers serially, once per agent, made a single default `trellis doctor`
278
+ run take minutes and spawn processes with a meaningfully larger blast
279
+ radius than "list what's configured." Pass `--probe-mcp` to opt in; the
280
+ default stays fast, side-effect-free, and safe to run in a pre-commit hook
281
+ or CI on every commit.
282
+
283
+ ## Testing philosophy: never verify against the developer's real environment
284
+
285
+ **Hard rule, not a preference.** P0's probes are read-only, so they're safe
286
+ to run against a real machine's real `~/.claude`, `~/.codex`, etc. — that's
287
+ how `docs/research.md`'s findings were originally discovered, by hand,
288
+ against a real machine, and P0 formalizes exactly that.
289
+
290
+ Every phase from P1 onward writes: symlinks, in-place TOML/JSON patches,
291
+ eventually credentials passing through a spawned process's env. **None of
292
+ that is ever exercised against a developer's actual dotfiles, actual
293
+ `~/.claude.json`, actual `~/.codex/config.toml`, or any other real
294
+ configuration a person depends on for their day-to-day work** — not during
295
+ development, not in CI, not for a "quick manual check." A bug in an adapter
296
+ that patches TOML in place is exactly the kind of thing that corrupts a
297
+ real config file if it's tested against one.
298
+
299
+ **Exception: pi.** This rule protects real state a person depends on — the
300
+ other three agents' adapters patch that state in place (symlinks over real
301
+ directories, in-place TOML/JSON edits). pi's adapter never does: skills/
302
+ instructions are the same symlink-or-noop regardless of machine, and the
303
+ MCP bridge (P4) is a single already-symlinked extension file that reads a
304
+ separate, Trellis-owned `servers.yaml` rather than writing into anything pi
305
+ itself depends on. A temporary, narrowly-scoped `~/.trellis/mcp/servers.yaml`
306
+ (deleted afterward) plus a real `pi -p` run therefore carries none of the
307
+ corruption risk this section exists to prevent, and it's the only way to
308
+ observe pi's actual extension-loading behavior on *this* installed pi/jiti
309
+ version — `docker/pi-sandbox.Dockerfile` verifies the same jiti-tolerance in
310
+ general (P4's own acceptance criteria), but doesn't stand in for a
311
+ spot-check against a specific real installation. Real-machine verification
312
+ of pi specifically is therefore permitted, not a hard-rule violation — the
313
+ other three agents are not exempted.
314
+
315
+ Verification happens against an isolated environment instead: a scratch
316
+ `$HOME` (or a container with one mounted) populated with synthetic
317
+ per-agent config that looks like the real thing but is expendable — created
318
+ fresh, asserted against, thrown away. `docs/implementation-plan.md`'s P1
319
+ acceptance criteria already describes this shape ("point at a scratch
320
+ `$HOME`... run `trellis sync skills`... confirm zero findings"); this
321
+ section exists to make it a project-wide rule that every later phase's
322
+ acceptance criteria must follow, not a detail specific to P1.
323
+
324
+ This is also why P0 needing to resolve pi's actual skill-discovery path
325
+ (§D5 in `openspec/changes/trellis-doctor-p0/design.md`) is investigated
326
+ directly against a real pi installation *as a read-only observation*, and
327
+ that finding then gets encoded as a fixture for the isolated test
328
+ environment — the real machine is where you learn the shape of the truth
329
+ once; it is never where you repeatedly verify against it.
330
+
331
+ ### The actual mechanism: `scripts/sandbox.sh`
332
+
333
+ ```
334
+ scripts/sandbox.sh # interactive shell in the sandbox
335
+ scripts/sandbox.sh npm run dev doctor # run a command in the sandbox
336
+ ```
337
+
338
+ Backed by OrbStack (this machine's Docker context — a Mac-native,
339
+ Apple-Virtualization-framework-backed runtime, not Docker Desktop; any
340
+ Docker-compatible daemon works identically). `docker/sandbox.Dockerfile`
341
+ builds a Node image containing the repo's `src/`; `docker/entrypoint.sh`
342
+ copies `test/fixtures/home` (mounted **read-only**) into a container-local
343
+ scratch `$HOME` before running anything, so no command executed inside the
344
+ container can ever write back to the fixture files checked into git —
345
+ verified directly: a write to the sandboxed `$HOME` during development
346
+ left the host-side fixture byte-for-byte unchanged.
347
+
348
+ `test/fixtures/home/` is a synthetic four-agent `$HOME` — fake
349
+ `.claude.json`, `.codex/config.toml`, `.kiro/settings/mcp.json`,
350
+ `.pi/agent/settings.json`, skill directories — built to exercise specific,
351
+ known findings, not just to look plausible:
352
+
353
+ - `.agents/skills/sample-skill/` vs. `.codex/skills/duplicate-skill/`:
354
+ byte-identical content at two different paths, deliberately, to exercise
355
+ `capability-drift-detection`'s duplication check against a real case
356
+ rather than an assertion with no failing fixture behind it.
357
+ - `.codex/config.toml` defines an `mcp_servers.sentry` entry — a name that
358
+ also appears in `known_host_injected` (`schema/servers.example.yaml`) —
359
+ reproducing the exact collision class from `docs/research.md` (same-name
360
+ static + host-injected server) as a fixture, not just a comment
361
+ describing the risk.
362
+ - `.kiro/skills/broken-case-skill/skill.md` — deliberately lowercase, to
363
+ exercise the case-sensitivity check.
364
+ - `test/fixtures/sample-mcp-server.js` — a minimal real MCP server (reads
365
+ `initialize` over stdio, replies with fixed `serverInfo`) so
366
+ `probeMcpServer` has something deterministic to handshake against inside
367
+ the container without depending on a real npm package or network access.
@@ -0,0 +1,235 @@
1
+ # Getting started
2
+
3
+ This is the detailed walkthrough. For the short version see the
4
+ [README Quick start](../README.md#quick-start).
5
+
6
+ ## Install
7
+
8
+ ```
9
+ npm install -g agent-trellis
10
+ ```
11
+
12
+ ## `trellis onboard` — the one-command path
13
+
14
+ ```
15
+ $ trellis onboard
16
+ ```
17
+
18
+ Runs `init`, detects which of Claude Code/Codex/Kiro/pi are on this machine,
19
+ picks one as the migration base, then runs `migrate` and `sync` against it.
20
+
21
+ - **No agent detected**: prints each agent's real install command/URL and
22
+ stops. Never installs anything itself — that's your call.
23
+ - **Exactly one agent detected**: auto-selected as the base, no prompt.
24
+ - **Two or more detected**: prompts you to pick one (if you're at a real
25
+ terminal), or pass `--agent <id>` to skip the prompt — useful in scripts,
26
+ CI, or when running with `--json`, which never prompts.
27
+
28
+ ```
29
+ $ trellis onboard --agent claude-code
30
+ Using claude-code as the migration base (--agent).
31
+
32
+ migrate --from claude-code
33
+ [create] skill "my-skill" — will copy from /Users/you/.claude/skills/my-skill
34
+ ...
35
+
36
+ sync
37
+ ✅ codex — 1 created, 0 removed, 0 conflict(s)
38
+ ...
39
+
40
+ Next: `trellis mcp sync` to distribute MCP servers, `trellis secrets audit` to check for leaked credentials.
41
+ ```
42
+
43
+ Add `--dry-run` to preview the entire chain — init/migrate/sync — with zero
44
+ writes anywhere.
45
+
46
+ **Picking a base agent only picks one.** If you use two or more agents with
47
+ genuinely different real content, onboard migrates from the one you (or it)
48
+ chose; the others' own differing content is untouched, exactly as `migrate`
49
+ would report it if run against them directly (see the conflict table
50
+ below). Merging differing content across multiple agents into one result
51
+ isn't built yet — see [README's Status](../README.md#status).
52
+
53
+ The rest of this page is the same flow broken into its individual steps —
54
+ useful if you want more control over any one part, or just want to
55
+ understand what `onboard` did.
56
+
57
+ ## Two starting points
58
+
59
+ **You already use one or more of Claude Code, Codex, Kiro, or pi** and have
60
+ real skills/instructions in them today. Go to
61
+ [Migrating from an existing agent](#migrating-from-an-existing-agent).
62
+
63
+ **You're starting from nothing** — no agent configured yet, or you'd rather
64
+ write canonical source by hand. Go to
65
+ [Starting from nothing](#starting-from-nothing).
66
+
67
+ Either way, run `trellis init` first.
68
+
69
+ ## `trellis init`
70
+
71
+ ```
72
+ $ trellis init
73
+ ```
74
+
75
+ Creates `~/.trellis/` with:
76
+
77
+ - `agents.md` — shared instructions, starts as a placeholder
78
+ - `mcp/servers.yaml` — starts as `servers: {}` (nothing defined yet)
79
+ - `secrets.policy.yaml` — starts with the real `reject_patterns` this
80
+ project ships (credential-shape regexes), an empty `allowed_vars`
81
+ - `skills/`, `agents/`, `memories/` — empty directories
82
+
83
+ **Never overwrites a file you already have.** Re-running `trellis init` on a
84
+ `~/.trellis/` that already exists just fills in whatever's still missing —
85
+ safe to run again any time, including after you've hand-edited things.
86
+
87
+ It then prints which of the four supported agents it found on this machine.
88
+ For each one **not** found, it prints that agent's real install command or
89
+ download link — `trellis init` never runs an installer itself; a global
90
+ package install or an IDE download is your call to make, not a silent side
91
+ effect of running this command.
92
+
93
+ ## Migrating from an existing agent
94
+
95
+ For each agent you already use:
96
+
97
+ ```
98
+ $ trellis migrate --from claude-code
99
+ ```
100
+
101
+ ```
102
+ migrate --from claude-code
103
+ [create] skill "my-skill" — will copy from /Users/you/.claude/skills/my-skill
104
+ [already-migrated] skill "shared-skill" — canonical content is byte-identical
105
+ [conflict] instructions — canonical agents.md already has different real content — resolve by hand
106
+ ```
107
+
108
+ `--from` accepts `claude-code`, `codex`, `kiro`, or `pi`. Run it once per
109
+ agent you actually use — it's independent per agent, order doesn't matter.
110
+
111
+ Add `--dry-run` to see the plan without writing anything:
112
+
113
+ ```
114
+ $ trellis migrate --from codex --dry-run
115
+ ```
116
+
117
+ **What each action means:**
118
+
119
+ | Action | Meaning |
120
+ |---|---|
121
+ | `create` | New to canonical source — copied in. |
122
+ | `already-migrated` | Canonical already has byte-identical content (safe re-run, nothing happens). |
123
+ | `conflict` | Canonical already has *different* real content — **left untouched**, resolve by hand. |
124
+ | `skip-symlink` | That agent's own copy is itself a symlink (already shared in from elsewhere) — nothing of that agent's own to import. |
125
+ | `skip-case-broken` | Found as `skill.md` instead of `SKILL.md` — fix the case on the source agent first. |
126
+
127
+ Migrate never overwrites a genuine conflict, and never scopes a migrated
128
+ skill to just the source agent — once in canonical, it's visible to every
129
+ agent by default (see `sync`, below). If migrate reports a `conflict`, open
130
+ the two files it names and decide by hand which content should actually be
131
+ canonical, then re-run.
132
+
133
+ ## Starting from nothing
134
+
135
+ Skip migrate. Edit `~/.trellis/agents.md` and add skills under
136
+ `~/.trellis/skills/<name>/SKILL.md` directly. There's nothing else to set up
137
+ before moving on to `sync`.
138
+
139
+ ## `trellis sync`
140
+
141
+ Distributes canonical skills and instructions to every agent present on this
142
+ machine:
143
+
144
+ ```
145
+ $ trellis sync
146
+ ```
147
+
148
+ ```
149
+ ✅ claude-code — 2 created, 0 removed, 0 conflict(s)
150
+ ✅ pi — 2 created, 0 removed, 0 conflict(s)
151
+ ⚠️ codex — 1 created, 0 removed, 1 conflict(s)
152
+ - [conflict] /Users/you/.agents/skills/my-skill exists and is not a Trellis-managed symlink — left untouched
153
+ ```
154
+
155
+ Distribution is done with symlinks (skills, subagents) or a symlinked
156
+ instructions file — never a copy, so canonical source stays the one place
157
+ you edit. A `conflict` here means that agent already has its own real,
158
+ non-symlinked content at that exact path; sync leaves it alone rather than
159
+ guessing which one should win. If you meant to bring that content into
160
+ canonical, that's what `migrate` is for.
161
+
162
+ Run `trellis sync skills` or `trellis sync instructions` to distribute just
163
+ one half. Add `--dry-run` (in any position — `trellis sync --dry-run` and
164
+ `trellis sync skills --dry-run` both work) to preview the plan with zero
165
+ writes.
166
+
167
+ ## `trellis mcp sync`
168
+
169
+ Distributes `~/.trellis/mcp/servers.yaml` to every present agent's native
170
+ MCP config. See [`schema/servers.example.yaml`](../schema/servers.example.yaml)
171
+ for the full documented shape — server definitions, per-agent scoping,
172
+ known-host-injected collision avoidance, and hub mode.
173
+
174
+ ```
175
+ $ trellis mcp sync
176
+ ```
177
+
178
+ Create/repair only — if you remove a server from `servers.yaml`, `mcp sync`
179
+ does not remove it from any agent's native config yet (see
180
+ [README's Known limitations](../README.md#status)). Remove it by hand on
181
+ each agent in the meantime.
182
+
183
+ `env:` in `servers.yaml` lists variable **names** only, never literal
184
+ values — the real values come from wherever your shell/secret manager
185
+ already populates them. See
186
+ [`schema/secrets.policy.example.yaml`](../schema/secrets.policy.example.yaml)
187
+ for exactly how that resolution works for each agent, including the one
188
+ narrow exception (pi's bridge has to read a value into its own process).
189
+
190
+ ## `trellis secrets audit`
191
+
192
+ ```
193
+ $ trellis secrets audit
194
+ ```
195
+
196
+ Scans every present agent's **real, on-disk** config (never canonical) for:
197
+
198
+ 1. A literal value matching one of `secrets.policy.yaml`'s
199
+ `reject_patterns` (a credential-shaped string that should have been a
200
+ `${VAR}` reference instead).
201
+ 2. An environment variable **name** not in `allowed_vars` — this catches
202
+ the case a value-only scan can't: a secret stored under the wrong
203
+ variable name is still a well-formed reference, just an unexpected one.
204
+
205
+ Fails non-zero on any hit. Run it after any MCP config change, and
206
+ periodically regardless.
207
+
208
+ ## `trellis doctor`
209
+
210
+ ```
211
+ $ trellis doctor
212
+ ```
213
+
214
+ Read-only. Scans all four agents' current state and reports drift —
215
+ mismatched skill content across agents, wrong-case skill files, and (with
216
+ `--probe-mcp`) whether each configured MCP server actually handshakes.
217
+ Safe to run any time; nothing here writes anything.
218
+
219
+ `--probe-mcp` is opt-in because it spawns a real process per configured
220
+ stdio MCP server (some reaching real external services) — not something a
221
+ "just check my config" command should do by default.
222
+
223
+ ## Troubleshooting
224
+
225
+ - **A `conflict` I don't understand**: `migrate` and `sync` both name the
226
+ exact file path in their output. Open it — it's real content that
227
+ differs from what Trellis expected, and resolving it is a decision only
228
+ you can make (which version is actually right).
229
+ - **An agent I use isn't showing up as present**: `trellis doctor` probes
230
+ the same real config files/directories `init`/`migrate`/`sync` do; if an
231
+ agent's config lives somewhere non-standard on your machine, that's worth
232
+ filing an issue with the exact path.
233
+ - Other known limitations are tracked honestly in
234
+ [README's Status section](../README.md#status) — read that before
235
+ assuming something is a bug rather than a documented gap.