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.
- package/LICENSE +21 -0
- package/README.md +127 -0
- package/dist/adapters/claude-code.d.ts +23 -0
- package/dist/adapters/claude-code.js +86 -0
- package/dist/adapters/codex.d.ts +27 -0
- package/dist/adapters/codex.js +119 -0
- package/dist/adapters/jsonMcp.d.ts +24 -0
- package/dist/adapters/jsonMcp.js +84 -0
- package/dist/adapters/kiro.d.ts +34 -0
- package/dist/adapters/kiro.js +175 -0
- package/dist/adapters/mcpPlan.d.ts +28 -0
- package/dist/adapters/mcpPlan.js +83 -0
- package/dist/adapters/pi.d.ts +23 -0
- package/dist/adapters/pi.js +108 -0
- package/dist/adapters/symlinkPlan.d.ts +33 -0
- package/dist/adapters/symlinkPlan.js +120 -0
- package/dist/cli.d.ts +7 -0
- package/dist/cli.js +135 -0
- package/dist/commands/doctor.d.ts +88 -0
- package/dist/commands/doctor.js +269 -0
- package/dist/commands/init.d.ts +44 -0
- package/dist/commands/init.js +150 -0
- package/dist/commands/mcp.d.ts +28 -0
- package/dist/commands/mcp.js +70 -0
- package/dist/commands/migrate.d.ts +38 -0
- package/dist/commands/migrate.js +132 -0
- package/dist/commands/onboard.d.ts +50 -0
- package/dist/commands/onboard.js +155 -0
- package/dist/commands/secretsAudit.d.ts +35 -0
- package/dist/commands/secretsAudit.js +115 -0
- package/dist/commands/sync.d.ts +40 -0
- package/dist/commands/sync.js +91 -0
- package/dist/core/adapter.d.ts +133 -0
- package/dist/core/adapter.js +16 -0
- package/dist/core/canonical.d.ts +16 -0
- package/dist/core/canonical.js +148 -0
- package/dist/core/types.d.ts +201 -0
- package/dist/core/types.js +15 -0
- package/dist/lib/dirEquals.d.ts +7 -0
- package/dist/lib/dirEquals.js +39 -0
- package/dist/lib/envVarNames.d.ts +35 -0
- package/dist/lib/envVarNames.js +79 -0
- package/dist/lib/fsIdentity.d.ts +16 -0
- package/dist/lib/fsIdentity.js +53 -0
- package/dist/lib/mcpProbe.d.ts +14 -0
- package/dist/lib/mcpProbe.js +96 -0
- package/dist/lib/probeCommon.d.ts +24 -0
- package/dist/lib/probeCommon.js +108 -0
- package/dist/lib/secretEnv.d.ts +19 -0
- package/dist/lib/secretEnv.js +46 -0
- package/dist/lib/skillFile.d.ts +12 -0
- package/dist/lib/skillFile.js +26 -0
- package/dist/lib/syncArgs.d.ts +16 -0
- package/dist/lib/syncArgs.js +17 -0
- package/dist/lib/tomlSection.d.ts +57 -0
- package/dist/lib/tomlSection.js +162 -0
- package/dist/pi-bridge/bundle.js +32074 -0
- package/dist/pi-bridge/index.d.ts +48 -0
- package/dist/pi-bridge/index.js +188 -0
- package/dist/pi-bridge/schemaTranslate.d.ts +55 -0
- package/dist/pi-bridge/schemaTranslate.js +40 -0
- package/dist/probes/claude-code.d.ts +13 -0
- package/dist/probes/claude-code.js +48 -0
- package/dist/probes/codex.d.ts +24 -0
- package/dist/probes/codex.js +78 -0
- package/dist/probes/kiro.d.ts +12 -0
- package/dist/probes/kiro.js +48 -0
- package/dist/probes/pi.d.ts +14 -0
- package/dist/probes/pi.js +53 -0
- package/dist/sdk.d.ts +14 -0
- package/dist/sdk.js +13 -0
- package/docs/architecture.md +367 -0
- package/docs/getting-started.md +235 -0
- package/docs/implementation-plan.md +341 -0
- package/docs/research.md +175 -0
- package/docs/roadmap.md +484 -0
- package/package.json +59 -0
- package/schema/scope.example.yaml +33 -0
- package/schema/secrets.policy.example.yaml +43 -0
- 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.
|