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,484 @@
1
+ # Roadmap
2
+
3
+ Each phase must ship with a `trellis doctor` check that verifies its own
4
+ claim — no phase is "done" on the strength of a config file existing, only
5
+ on the strength of a passing verification against the real running agent.
6
+
7
+ See [`implementation-plan.md`](implementation-plan.md) for P1–P4 broken down
8
+ to file/function level.
9
+
10
+ **P0 is done and archived** (`openspec/changes/archive/2026-09-12-trellis-doctor-p0/`;
11
+ living spec at `openspec/specs/agent-state-probing/` and
12
+ `openspec/specs/capability-drift-detection/`). Verified against this real
13
+ machine, not just fixtures: independently rediscovered the already-fixed
14
+ Codex `openspec-*` duplication as clean, found a real, previously-unknown
15
+ MCP name collision (`sentry` statically configured on Claude Code and Kiro
16
+ while also being a `known_host_injected` name), and found a real wrong-case
17
+ skill file (`e2e-test`). `trellis doctor` (no flags) does none of this by
18
+ spawning anything — MCP handshake probing is opt-in via `--probe-mcp`
19
+ (see `docs/architecture.md`), added after a real run showed the default
20
+ had a much larger blast radius than "read-only" should mean.
21
+
22
+ This is the format every later phase uses: one `openspec/changes/<phase>/`
23
+ directory per phase, `openspec new change <name>` → tasks implemented →
24
+ `openspec archive <name>`. `implementation-plan.md`'s P0/P1 sections are
25
+ kept only as historical notes; don't edit them further.
26
+
27
+ **P1 is done and archived** (`openspec/changes/archive/2026-09-12-trellis-sync-p1/`;
28
+ living spec at `openspec/specs/canonical-source-loading/` and
29
+ `openspec/specs/skill-instructions-sync/`). Covers all four agents,
30
+ including pi — a corrected assumption found while starting this change
31
+ (pi previously assumed to need "no adapter"; it needs the same symlink
32
+ treatment as the other three once its real global directory,
33
+ `~/.pi/agent/skills`, was confirmed in P0). Verified against a scratch
34
+ `$HOME` and `scripts/sandbox.sh`'s real container, never this developer's
35
+ actual dotfiles. The acceptance pass itself caught two real bugs before
36
+ they'd have shipped: a removal check that used `realpathSync` and
37
+ silently never fired on a symlink whose canonical target had just been
38
+ deleted (exactly the case it existed to catch — broken symlinks throw on
39
+ realpath), and a plural/singular string mismatch between the CLI's
40
+ `target` option and `AdapterPlanItem.kind` that made `trellis sync skills`
41
+ silently apply nothing while reporting "already in sync." Both were only
42
+ found because the sandbox was actually run end-to-end, not just unit
43
+ tests of the pieces — see docs/architecture.md's testing philosophy.
44
+
45
+ **P2 is done and archived** (`openspec/changes/archive/2026-09-12-trellis-mcp-sync-p2/`;
46
+ living spec at `openspec/specs/mcp-server-sync/`). `trellis mcp sync`
47
+ covers create/repair only — automatic removal is deliberately deferred: a
48
+ symlink's realpath proves Trellis ownership for skills, but a bare TOML/
49
+ JSON key has no equivalent marker, so "gone from canonical" and "the user
50
+ configured this directly" are indistinguishable without a `trellis.lock.json`
51
+ ownership-tracking mechanism that doesn't exist yet. Two write mechanisms
52
+ were chosen only after empirically ruling out the obvious ones first: both
53
+ `@iarna/toml` and `smol-toml` silently drop comments and reformat arrays on
54
+ a bare parse→stringify round-trip (so Codex's `config.toml` is patched by a
55
+ hand-rolled, purpose-built line-based section locator/splicer,
56
+ `src/lib/tomlSection.ts`, never a general parser), and `codex mcp add
57
+ --env` only accepts literal `KEY=VALUE` with no bare-name form (so it's
58
+ never shelled out to for the general write path — writing a real secret
59
+ value into a config file is the exact thing the secrets policy forbids).
60
+ Claude Code and Kiro use a plain JSON parse/merge/stringify, safe because
61
+ JSON has no comments to lose. The sandbox acceptance pass caught a real
62
+ bug unit tests alone hadn't: a comment block immediately introducing the
63
+ *next* `[header]` was being swallowed into the *previous* section's range,
64
+ so updating that previous section would have silently deleted an
65
+ unrelated comment — see docs/architecture.md's testing philosophy for why
66
+ this project always runs the real container, not just fixtures in memory.
67
+
68
+ **P3 is done and archived** (`openspec/changes/archive/2026-09-12-trellis-secrets-audit-p3/`;
69
+ living spec at `openspec/specs/secrets-audit/`). `trellis secrets audit`
70
+ reads each present MCP-capable agent's real, on-disk config (never
71
+ canonical) and runs two independent checks: a whole-file scan against
72
+ `secrets.policy.yaml`'s `reject_patterns`, and a check of every declared
73
+ environment-variable *name* against `allowed_vars`. The second check
74
+ exists because a value-only scan structurally cannot catch the first real
75
+ incident on record — a GitLab PAT stored under the *wrong variable name*
76
+ was still a well-formed `${VAR}` reference, just the wrong name. Env-var-
77
+ name extraction stays dependency-free and format-narrow (real `JSON.parse`
78
+ for Claude Code/Kiro; a five-line regex over the one `env_vars = [...]`
79
+ line shape for Codex) rather than reusing P2's write-path
80
+ `tomlSection.ts` or adding a TOML library for reads — a deliberate,
81
+ separately-reasoned choice, not an oversight (P2's D2 rejected TOML
82
+ libraries specifically for round-trip fidelity on *writes*; that concern
83
+ doesn't apply to a read-only linter, but a full parser is still more than
84
+ this one narrow extraction needs). Both real incidents on record were
85
+ reproduced and caught live in the sandbox, not just in unit tests.
86
+
87
+ **P4 is done and archived** (`openspec/changes/archive/2026-09-12-trellis-pi-mcp-bridge-p4/`;
88
+ living spec at `openspec/specs/pi-mcp-bridge/`). pi's MCP access is a
89
+ single symlinked extension (`~/.pi/agent/extensions/trellis-mcp-bridge.js`),
90
+ not a settings.json entry — found by reading pi's own extension-loader
91
+ source directly (same technique as P0's D5): `~/.pi/agent/extensions/`
92
+ is auto-discovered on every startup, including symlinks, with zero
93
+ configuration. The genuinely hard problem, found only by actually running
94
+ the real `pi` binary against the real symlink in a dedicated sandbox
95
+ (`docker/pi-sandbox.Dockerfile`) rather than trusting the design on paper:
96
+ a symlinked file's own bare-specifier imports (`@modelcontextprotocol/sdk`,
97
+ `typebox`) resolve relative to the *symlink's own path*, not its
98
+ target — the reverse of the initial assumption — so no dependency
99
+ declared in Trellis's own `package.json` could ever make an unbundled
100
+ bridge file resolve once placed in an arbitrary user's home directory.
101
+ Fixed by bundling the whole bridge into one dependency-free
102
+ `dist/pi-bridge/bundle.js` via `esbuild`, confirmed by re-running the
103
+ same sandbox check: the bridge loaded cleanly (no more "Failed to load
104
+ extension"), reaching pi's own unrelated "no API key configured" failure
105
+ instead — proof positive without ever spending a real model call.
106
+
107
+ **P5 is done and archived** (`openspec/changes/archive/2026-09-12-trellis-sdk-p5/`;
108
+ living spec at `openspec/specs/trellis-sdk/`). `agent-trellis` now has a
109
+ real `"exports"` map: `import { loadCanonicalSource } from "agent-trellis"`
110
+ resolves without touching the CLI at all. Deliberately narrow (a curated
111
+ `src/sdk.ts` barrel — canonical-source loading and its types only, never
112
+ `src/adapters/*`/`src/commands/*`) and deliberately one package, not a
113
+ separately-published `@trellis/sdk` — the roadmap's naming was a working
114
+ label, not a monorepo commitment, and there is no second consumer yet to
115
+ justify that cost. Verified with a real package-resolution check
116
+ (`scripts/verify-sdk-export.sh`: pack the actual tarball, install it into
117
+ a throwaway scratch project, import via the bare `"agent-trellis"`
118
+ specifier), not just a source-relative `tsx` import — the two use
119
+ different resolution algorithms, and only the former would have caught an
120
+ `"exports"` map that looked right but didn't actually resolve for a real
121
+ consumer.
122
+
123
+ **P6 is done and archived** (`openspec/changes/archive/2026-09-12-trellis-memory-p6/`;
124
+ living spec at `openspec/specs/memory-defaults/`). `@modelcontextprotocol/server-memory`
125
+ is the documented default (`schema/servers.example.yaml`), confirmed by
126
+ the user over `totalrecallai` — a real, third-party memory server found
127
+ statically configured on this machine's Kiro install only (Claude
128
+ Code/Codex had none at all). No new adapter code: an unscoped `memory`
129
+ entry reaches all three native-config agents via P2's existing pipeline
130
+ and pi via P4's bridge, verified end-to-end in the real sandbox with the
131
+ actual `@modelcontextprotocol/server-memory` package name (never
132
+ spawned — `mcp sync` only writes config). One real wrinkle corrected
133
+ along the way: the schema example already lists `memory` under
134
+ `known_host_injected` (a genuine, empirically-grounded P0 finding about
135
+ mirasim's own runtime injection, not a stale guess), so the new default
136
+ entry ships commented out with guidance on which of the two situations
137
+ applies, rather than shipping an example that self-collides with
138
+ Trellis's own guard. Deliberately out of scope: auto-ingesting
139
+ `~/.trellis/memories/*.md` content into the running memory server's
140
+ store — a real, separate problem left as an open question, not silently
141
+ resolved.
142
+
143
+ **P7 is done and archived** (`openspec/changes/archive/2026-09-12-trellis-secrets-env-management-p7/`;
144
+ living specs at `openspec/specs/secret-env-resolution/`, plus
145
+ modifications to `pi-mcp-bridge` and `secrets-audit`). Two real gaps
146
+ found testing the pi bridge against this real machine, not assumed: (1)
147
+ `secrets audit` only ever checked declared env var *names* against an
148
+ allow-list, never whether a name actually resolves to a value — a
149
+ machine migration could carry every name across while every value stayed
150
+ unset, silently, until some agent's tool call 401'd; (2) the pi bridge's
151
+ `connectStdio` already read real secret values (unavoidable — it's
152
+ Trellis's own code spawning the MCP subprocess, unlike Claude Code/
153
+ Codex/Kiro's own native clients resolving `${VAR}` in their own process),
154
+ but did so unconditionally from ambient `process.env` — every credential
155
+ the parent `pi` process's shell exported, not just the one or two names
156
+ the active server declared. Fixed with one shared, dependency-free
157
+ resolver (`src/lib/secretEnv.ts`) both the bridge and the audit call, and
158
+ one new optional `secrets.policy.yaml` field, `env_file`: when set, it's
159
+ the *sole* source for a declared name (no fallback to ambient — a silent
160
+ fallback would defeat the isolation this exists to offer), read via a
161
+ narrow hand-rolled `KEY=VALUE` parser, same "no new dependency" reasoning
162
+ as P3's `envVarNames.ts`. Verified with a real spawned MCP subprocess
163
+ (not a mock) reading back its own env by name
164
+ (`test/unit/piBridge.test.ts`), proving `env_file`'s value wins even when
165
+ ambient holds a different one — and separately in the real (non-pi)
166
+ Docker sandbox, `trellis secrets audit` correctly reporting
167
+ `missing-env-value` and exiting non-zero. Deliberately scoped down from
168
+ the original plan: no second `pi`-sandbox rebuild, since the bridge's own
169
+ unit test already exercises the exact changed code path with the same
170
+ rigor, and P4's own sandbox pass already covers the pi-extension-loading
171
+ surface this change never touches (tasks.md 5.1 records the reasoning,
172
+ not a silent skip). Honestly scoped non-goal, stated rather than
173
+ discovered later: `missing-env-value` is authoritative for pi, but only a
174
+ best-effort proxy for Claude Code/Codex/Kiro — those three resolve
175
+ `${VAR}` in their own process, which this audit cannot observe directly.
176
+
177
+ **P8 is done and archived** (`openspec/changes/archive/2026-09-12-trellis-kiro-approved-env-vars/`;
178
+ living spec at `openspec/specs/kiro-env-var-approval/`). A real,
179
+ already-shipped correctness gap, found by reading Kiro's own installed
180
+ extension source directly
181
+ (`kiro.kiro-agent/dist/extension.js`'s `expandEnvironmentVariables`):
182
+ Kiro's `${VAR}` substitution is gated by a workspace/user setting,
183
+ `kiroAgent.mcpApprovedEnvVars` — a name absent from that list is
184
+ silently left as the literal, unresolved string, no error. On this real
185
+ machine that list was entirely empty, which is exactly why the one real
186
+ Kiro server needing a secret (`mcp-router`) held a literal token instead
187
+ of a `${VAR}` reference. Every Kiro MCP server Trellis had ever
188
+ generated with a `${VAR}` reference was, by default, silently broken —
189
+ P2 never knew this second gate existed. Fixed: the Kiro adapter now
190
+ also ensures every env name it references is present in
191
+ `kiroAgent.mcpApprovedEnvVars`, in Kiro's own global, VS-Code-style
192
+ `settings.json` (a different file from `~/.kiro/settings/mcp.json`,
193
+ shared with hundreds of unrelated editor preferences) — additive only,
194
+ parses the whole file and preserves every other key, refuses to touch a
195
+ file it can't parse. Verified in the real sandbox twice: a clean machine
196
+ gets the file created with exactly the needed name; an already-correct,
197
+ pre-seeded machine (including an unrelated key) produces zero writes and
198
+ comes out byte-identical. Deliberately narrow: only covers `env` names
199
+ (not the not-yet-built `headers` field), and only macOS's settings path
200
+ (Linux/Windows equivalents are the well-known convention but unverified
201
+ against a real install, stated as an open question).
202
+
203
+ **P9 is done and archived** (`openspec/changes/archive/2026-09-12-trellis-mcp-transport-auth/`;
204
+ living specs at modifications to `mcp-server-sync`, `pi-mcp-bridge`,
205
+ `secrets-audit`, and `kiro-env-var-approval`). `McpServerDef` gains
206
+ `headers?: Record<string,string>` (`${VAR}` references, same discipline
207
+ as `env`) and `Transport` gains `"sse"`; the dead `auth?: "oauth" |
208
+ "bearer-env"` field — never read anywhere in the codebase — is removed.
209
+ Four real, verified per-agent schemas (CLI `--help` output, Kiro's own
210
+ installed extension source, the MCP SDK's real `.d.ts`) converged on:
211
+ Claude Code and Kiro accept the identical plain headers map, rendered
212
+ verbatim; Codex has no generic headers concept at all, only a single
213
+ purpose-built `bearer_token_env_var` field (matching what its own `mcp
214
+ add --bearer-token-env-var` generates) — a server needing more than one
215
+ header is refused for Codex specifically (a real conflict, not a silent
216
+ drop or lossy approximation) while still reaching every other agent; the
217
+ pi bridge passes resolved headers into both `connectHttp` and a new
218
+ `connectSse` via the SDK's `requestInit.headers`. Real OAuth (browser
219
+ redirect, token refresh) is explicitly not built anywhere — all three
220
+ native-config agents already have their own working flow (`claude mcp
221
+ add --client-id`, `codex mcp login`, Kiro's own `oauth` schema fields);
222
+ pi has none and gains none here, a stated limitation. A cross-cutting
223
+ fix shipped alongside the feature rather than after: both P7's
224
+ `missing-env-value` check and P8's Kiro approved-env-vars list now see
225
+ names embedded in `headers` values too, via one shared
226
+ `declaredEnvNames` helper — shipping `headers` without teaching both of
227
+ them about it would have reproduced P8's own root cause for a new
228
+ field. Verified with a real, unmocked network test (a plain `node:http`
229
+ server capturing the bridge's actual outbound request headers) and in
230
+ the real Docker sandbox: both a bearer-token-shaped and a
231
+ two-header-shaped fixture server, confirming Claude Code/Kiro render
232
+ `headers` verbatim, Codex renders `bearer_token_env_var` or refuses with
233
+ the exact expected conflict message, and `secrets audit` catches a
234
+ headers-embedded name both on the canonical side and in a real,
235
+ already-written agent config file.
236
+
237
+ **Post-P9 hardening, done and archived**
238
+ (`openspec/changes/archive/2026-09-12-trellis-mcp-connect-timeout/`;
239
+ modifies `pi-mcp-bridge`). Found live, not hypothesized: a real, still-
240
+ published MCP server CLI (`@harness-fe/cli mcp`) that exits in under a
241
+ second with `stdio: "ignore"` but, spawned exactly the way a real stdio
242
+ MCP client must (a readable stdin pipe attached), never writes a byte
243
+ and never exits — indistinguishable from "still starting up" without a
244
+ bound. The pi bridge's `Promise.all` over every configured server had
245
+ no timeout on `client.connect()`, so one such server stalled the entire
246
+ extension load, plus every other configured server's tool registration
247
+ with it. Each of `connectStdio`/`connectHttp`/`connectSse` (and
248
+ `listTools()` after a successful connect) now races against a fixed
249
+ 10-second timeout — the same constant `doctor --probe-mcp`'s
250
+ `mcpProbe.ts` already used independently, confirmed by reading it, not
251
+ assumed. Fixing this surfaced a second, real bug beyond the plan: a
252
+ timed-out `connectStdio` was leaking its spawned child process forever
253
+ (discovered when a test run took 12-19s instead of the expected ~300ms,
254
+ and confirmed by a live orphaned process still in `ps` after the test
255
+ exited) — every connect function now calls `transport.close()` on any
256
+ failure, timeout or otherwise, which for a stdio transport kills the
257
+ underlying process. Verified three ways: a dedicated hanging-server
258
+ fixture (two modes — silent from the first byte, and silent only after
259
+ a real `initialize` reply) proves the isolation and the process-leak
260
+ fix in the unit suite; the real Docker pi sandbox re-ran end to end
261
+ with the fix in place, hitting the exact timeout path for real (a
262
+ `memory` server's `npx` cold-start exceeding 10s), while two other,
263
+ differently-broken servers failed fast and pi still reached its own
264
+ unrelated "no API key" stage — proof nothing stalled. A pre-existing,
265
+ unrelated gap was found (not fixed here, out of this change's scope):
266
+ none of `sync skills`/`sync instructions`/`mcp sync`'s target filters
267
+ cover the bridge extension symlink's `kind: "extension"` — only bare
268
+ `trellis sync` delivers it.
269
+
270
+ **`pi-bridge-lifecycle`, done and archived**
271
+ (`openspec/changes/archive/2026-09-12-pi-bridge-lifecycle/`; modifies
272
+ `pi-mcp-bridge`). The bridge now owns the full lifecycle of every MCP
273
+ client it connects — a `clients: Set<Client>` tracked from a successful
274
+ `connectStdio`/`connectHttp`/`connectSse`, closed (idempotently, best-
275
+ effort) on pi's `session_shutdown` event or when `tools/list` fails
276
+ after connecting. Started as an open proposal carried forward from
277
+ earlier work with only 1 of 3 declared scenarios under test; closed out
278
+ here by adding the two missing ones — multiple connected servers are
279
+ *all* released on shutdown (not just the first), and calling shutdown
280
+ twice is a no-op on the second call, not an error or a double-close —
281
+ both verified against real spawned subprocesses, not mocks. The fourth
282
+ declared scenario ("a server that never connects is not cleaned
283
+ twice") needed no dedicated test: it's enforced by control flow, not a
284
+ runtime check — a failed connect never reaches `clients.add(client)`,
285
+ so it was never a candidate for cleanup in the first place, and
286
+ `trellis-mcp-connect-timeout`'s own tests already exercise that exact
287
+ failure path. Real pi-sandbox verification was judged already covered
288
+ by `trellis-mcp-connect-timeout`'s own Docker run, which exercised this
289
+ same session_shutdown/`closeAllClients` code path with no dedicated
290
+ second real-CLI run needed.
291
+
292
+ **`trellis-cli-init`, done and archived**
293
+ (`openspec/changes/archive/2026-09-12-trellis-cli-init/`; adds
294
+ `canonical-source-bootstrap`, modifies `capability-drift-detection`).
295
+ New `trellis init` command: creates `~/.trellis/agents.md`,
296
+ `mcp/servers.yaml` (`servers: {}`, `known_host_injected: []`), and
297
+ `secrets.policy.yaml` (`reject_patterns` read live from
298
+ `schema/secrets.policy.example.yaml`, not retyped) whenever they don't
299
+ already exist — per file, never overwriting real content — then prints
300
+ which of the four agents are present, each pointing at `trellis migrate
301
+ --from <agent>` (next). Turns `sync`/`mcp sync`/`secrets audit`'s
302
+ existing "no canonical source, create it" refusal into something an
303
+ actual command satisfies, closing a gap this session's own real pi
304
+ onboarding had to route around entirely by hand (`mkdir`, hand-copied
305
+ skills, hand-written `scope.yaml`). Found and fixed alongside it, not
306
+ separately: `doctor`'s collision check had been permanently stuck on a
307
+ hardcoded `DEFAULT_KNOWN_HOST_INJECTED` list since before canonical
308
+ source loading existed — `collectDoctorReport` already accepted an
309
+ override, but `src/cli.ts` never passed one, so `doctor` and `mcp
310
+ sync`'s own collision refusal (which does read canonical) could
311
+ disagree about what counts as a collision on any machine whose real
312
+ host-injected connectors differ from this project's own development
313
+ machine. `doctor` now resolves `known_host_injected` from canonical
314
+ when it exists, falling back to the hardcoded default only when it
315
+ doesn't (unchanged P0 behavior). Verified in the real Docker sandbox —
316
+ and that run caught a second real, unrelated bug of its own: neither
317
+ `docker/sandbox.Dockerfile` nor `docker/pi-sandbox.Dockerfile` copied
318
+ `schema/` into the image, so `init` crashed on `ENOENT` reading
319
+ `schema/secrets.policy.example.yaml` the first time it ran inside a
320
+ container — both Dockerfiles fixed, `package.json`'s own `files` array
321
+ also gained `schema` for the same underlying reason (the published npm
322
+ package didn't ship it either, and README already told users to read
323
+ it).
324
+
325
+ **`trellis-cli-migrate`, done and archived**
326
+ (`openspec/changes/archive/2026-09-12-trellis-cli-migrate/`; adds
327
+ `canonical-source-migration`). `trellis migrate --from <agent>`: probes
328
+ the named agent, then per real (non-symlinked, case-correct) skill and
329
+ per real instructions file, plans one of `create` (no canonical entry
330
+ yet), `already-migrated` (byte-identical to what's already in
331
+ canonical — `src/lib/dirEquals.ts`, a real recursive directory-content
332
+ comparison, not a name/mtime/hash shortcut), or `conflict` (differs —
333
+ reported, never overwritten). A symlinked or case-broken skill is
334
+ skipped and reported rather than migrated, since there's nothing real
335
+ of that agent's own to import. Canonical `agents.md` still at `trellis
336
+ init`'s placeholder is treated the same as "doesn't exist yet" so a
337
+ first real migrate always lands. No `scope.yaml` entry is ever written
338
+ — a migrated skill stays unscoped (visible to all agents), matching
339
+ `sync`'s own default. `--dry-run` computes and prints the same plan
340
+ with zero writes. This closes `trellis init`'s own per-agent pointer
341
+ message (`trellis migrate --from <agent>` — next), completing the
342
+ init → migrate → sync → mcp sync → secrets audit onboarding path for a
343
+ user who already has real content in one of the four agents. Verified
344
+ in the real Docker sandbox in the sequence a real user would actually
345
+ run it: `migrate --from claude-code` against the fixture home correctly
346
+ created a new canonical skill from claude-code's real content and
347
+ correctly conflicted on instructions (fixture's canonical `agents.md`
348
+ already has real content); a subsequent `trellis sync` in the same
349
+ container then correctly symlinked the newly migrated skill out to
350
+ kiro and pi (byte-identical content confirmed via `diff`), while
351
+ correctly reporting conflicts — not overwriting — on claude-code and
352
+ codex, since both already have their own real, non-canonical
353
+ `sample-skill` at that exact path.
354
+
355
+ **Agent auto-install scope decision, addendum to `trellis-cli-migrate`:**
356
+ `trellis init` prints each undetected agent's real, currently-correct
357
+ install command/URL (`npm install -g @anthropic-ai/claude-code`,
358
+ `npm install -g @openai/codex`, `npm install -g
359
+ @earendil-works/pi-coding-agent`, and `https://kiro.dev/downloads/`
360
+ for Kiro — a desktop IDE with no CLI package) — never spawns an
361
+ installer itself. A global package install or IDE download is exactly
362
+ the kind of system-wide, hard-to-reverse action this project's own
363
+ safety discipline requires an explicit human "yes" for, not a silent
364
+ side effect of running `trellis init`; a detect-and-print pointer
365
+ delivers "help me install the agents you support" without that risk.
366
+
367
+ **`trellis-cli-onboard`, done and archived**
368
+ (`openspec/changes/archive/2026-09-12-trellis-cli-onboard/`; adds
369
+ `onboarding-flow`). `trellis onboard` chains `init` → detect all four
370
+ agents (skill names/count, real-instructions presence — the exact
371
+ subset `migrate` can act on, nothing about MCP) → resolve a single base
372
+ agent → `migrate --from <base>` → `sync`, purely by calling each
373
+ command's own already-tested plan/apply functions — no new skill-copy,
374
+ symlink, or conflict-detection judgment exists in `onboard.ts` itself.
375
+ Base-agent resolution: zero present prints every agent's install
376
+ hint and stops (exit 0 — not a failure, a correct stopping point);
377
+ exactly one present auto-selects with no prompt; two or more resolve
378
+ via `--agent <id>` (the scriptable/sandbox/`--json` path) or an
379
+ interactive `node:readline/promises` prompt when stdin is a real
380
+ terminal, refusing cleanly with the present-agent list rather than
381
+ guessing when neither is available. `--json` never prompts even if
382
+ stdin happens to be a TTY, matching the same principle
383
+ `scripts/sandbox.sh` already uses for its own `-it`/`-i` branching.
384
+ Found and fixed alongside it: `trellis sync` itself had no dry-run
385
+ mode at all before this change (`collectSyncReport` always called
386
+ `adapter.apply()` unconditionally) — added as `RunSyncOptions.dryRun`,
387
+ independently useful on its own (`trellis sync --dry-run`) and a
388
+ prerequisite for `onboard --dry-run`'s own true-no-writes guarantee
389
+ across the whole chain. Verified in the real Docker sandbox: `trellis
390
+ onboard --agent claude-code` against the fixture home (which has
391
+ multiple present agents) produced the identical real result to running
392
+ `migrate --from claude-code` then `sync` by hand — same canonical
393
+ `sample-skill` created, same instructions conflict correctly reported
394
+ and left untouched, same real distribution of the migrated skill out
395
+ to kiro and pi.
396
+
397
+ **Merge mode — named future work, not a silent gap:** today, two or
398
+ more present agents with genuinely *different* real content still
399
+ resolves to "pick one as the base"; the others' own differing content
400
+ stays exactly as `migrate`/`sync` already report it (conflict,
401
+ untouched — never silently dropped or overwritten). Actually merging
402
+ differing skills/instructions from more than one agent into one
403
+ canonical result is real, separate design work (whose content wins per
404
+ file? per skill? does the user review a diff before it's written?) —
405
+ deliberately out of scope for `trellis-cli-onboard`, tracked here as an
406
+ explicit next step rather than something a user has to discover is
407
+ missing.
408
+
409
+ **Pre-release closed-loop audit, before first publish.** A full,
410
+ skeptical review of the whole new-user path — every command's usage
411
+ text against its real implementation, every doc claim against the
412
+ actual code, `package.json`'s shipped files against everything read at
413
+ runtime — found and fixed five real issues, none caught by the
414
+ existing test suite because each lived in code path or a doc claim
415
+ nothing exercised directly:
416
+ - `trellis sync --dry-run` was broken as a flags-first invocation:
417
+ `src/cli.ts`'s target parsing only ever checked `rest[0]`, so a flag
418
+ placed before a target (or with no target at all) was misread as an
419
+ unknown target named e.g. `"--dry-run"`. Fixed by parsing the whole
420
+ `rest` array for a recognized target instead of assuming position,
421
+ and the parsing logic (`parseSyncArgs`) was pulled out into its own
422
+ small, directly unit-tested function (`test/unit/cli.test.ts`) —
423
+ `src/cli.ts` previously had zero direct test coverage of its own argv
424
+ dispatch, which is exactly why a pure-parsing bug like this shipped
425
+ unnoticed. Adding that test surfaced a second real issue: importing
426
+ `cli.ts` for the pure function ran the whole CLI against the test
427
+ runner's own argv as an unguarded side effect (`main(...)` had no
428
+ entrypoint check) — fixed with the standard `import.meta.url ===
429
+ file://${process.argv[1]}` guard.
430
+ - `sync --dry-run` was undocumented in `printUsage()`, `README.md`, and
431
+ `docs/getting-started.md` despite being real and (once fixed) working
432
+ — all three now document it.
433
+ - `onboard.ts` had its own, second copy of `migrate`'s and `sync`'s
434
+ report-printing logic, and that copy was missing `migrate.ts`'s own
435
+ "nothing to migrate" empty-plan case — a real gap for exactly the
436
+ fresh-Claude-Code-install scenario this project's own new-user
437
+ persona represents (config present, zero skills, no instructions
438
+ file). Fixed by exporting and reusing `migrate.ts`'s `printPlan` and
439
+ `sync.ts`'s `printReport` directly instead of a second copy that
440
+ could silently drift.
441
+ - `package.json`'s `files` shipped `dist` and `schema` but not `docs` —
442
+ every globally-installed user's local `README.md` (which *is*
443
+ shipped) links to `docs/getting-started.md`, `docs/architecture.md`,
444
+ and `docs/roadmap.md`, none of which existed on their machine. Same
445
+ root cause `trellis-cli-init`'s own entry already fixed once for
446
+ `schema/` — the fix pattern (add it to `files`) applied again here.
447
+ - `docs/architecture.md`'s canonical-schema diagram listed
448
+ `trellis.lock.json` with no "not built yet" annotation, unlike every
449
+ other not-yet-built thing described in the same document — annotated.
450
+
451
+ **A near-miss caught by re-running the real installed-tarball check, not
452
+ by the test suite:** the first fix for `parseSyncArgs`'s test coverage
453
+ gap put the pure function in `src/cli.ts` itself and guarded that
454
+ file's top-level `main()` call with `import.meta.url ===
455
+ file://${process.argv[1]}` so importing the function for a test
456
+ wouldn't run the whole CLI as a side effect. That guard is not
457
+ symlink-safe: npm's `bin` entry is a symlink, and a symlink's
458
+ `import.meta.url` (resolved) never equals its own symlink path
459
+ (`process.argv[1]`, unresolved) — so the real, installed `trellis`
460
+ binary silently did nothing and exited 0 on every invocation. `npm
461
+ test` stayed green throughout, because nothing in the unit suite runs
462
+ through an actual symlinked bin — only `scripts/verify-cli-install.sh`
463
+ does, and it caught this immediately on the very next run. Fixed
464
+ properly by moving `parseSyncArgs` to its own zero-side-effect module
465
+ (`src/lib/syncArgs.ts`) instead of trying to make the entrypoint guard
466
+ symlink-safe — the reminder here: a real installed-package check is
467
+ not a redundant formality alongside the unit suite, it's the only
468
+ thing in this project that exercises the actual `bin` symlink at all.
469
+
470
+ | Phase | Deliverable | Depends on |
471
+ |---|---|---|
472
+ | P0 | ✅ `trellis doctor` — read-only, opt-in-for-handshakes scan of all four agents' current skills/MCP/instructions state, reports drift and duplicates | nothing |
473
+ | P1 | ✅ `trellis sync skills` / `trellis sync instructions` — symlink-based distribution to Claude Code, Codex, Kiro, and pi | P0 |
474
+ | P2 | ✅ `trellis mcp sync` — incremental, in-place adapters for Claude Code (JSON merge), Codex (TOML section patch), Kiro (JSON merge); collision check against known host-injected server names | P1 |
475
+ | P3 | ✅ `trellis secrets audit` — scans every adapter's output for literal credential patterns and unexpected env var names, fails non-zero on any hit | P2 |
476
+ | P4 | ✅ pi bridge extension — MCP tool registration via `registerTool`, sourced from the same `mcp/servers.yaml` | P2 |
477
+ | P5 | ✅ `@trellis/sdk` — read-only API over the canonical source, for third-party agents to consume without depending on the CLI | P1–P4 stable |
478
+ | P6 | ✅ Memory: document and wire the `server-memory` default; write the mem0/OpenMemory upgrade guide | P2 |
479
+ | P7 | ✅ Secrets/env management: shared `resolveSecretEnv` + `secrets.policy.yaml`'s `env_file`, pi bridge stops reading raw ambient env, `secrets audit` gains a `missing-env-value` check | P3, P4 |
480
+ | P8 | ✅ Kiro `${VAR}` fix: adapter also manages `kiroAgent.mcpApprovedEnvVars`, without which Kiro silently never substitutes any env reference Trellis writes | P2 |
481
+ | P9 | ✅ MCP transport/auth expansion: `headers` field for static bearer/API-key remote auth (Claude Code/Codex/Kiro/pi bridge, each via its own real schema), `sse` transport; real OAuth flows explicitly delegated to each agent's own native support, not reimplemented | P2, P4, P8 |
482
+ | P10 | GUI: evaluate embedding into mcp-router's or skills-hub's existing interface before building anything new | P3–P9 |
483
+
484
+ No dates. This is scoped by verification milestones, not calendar time.
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "agent-trellis",
3
+ "version": "0.1.0",
4
+ "description": "A single source of capability for every coding agent — skills, MCP, subagents, memory, and secret policy, adapted natively into Claude Code, Codex, Kiro, and pi.",
5
+ "license": "MIT",
6
+ "author": "Paul Leo",
7
+ "type": "module",
8
+ "bin": {
9
+ "trellis": "./dist/cli.js"
10
+ },
11
+ "main": "./dist/sdk.js",
12
+ "types": "./dist/sdk.d.ts",
13
+ "exports": {
14
+ ".": {
15
+ "types": "./dist/sdk.d.ts",
16
+ "import": "./dist/sdk.js"
17
+ },
18
+ "./package.json": "./package.json"
19
+ },
20
+ "files": [
21
+ "dist",
22
+ "schema",
23
+ "docs"
24
+ ],
25
+ "scripts": {
26
+ "build": "tsc -p tsconfig.json && node scripts/build-pi-bridge.mjs",
27
+ "dev": "tsx src/cli.ts",
28
+ "typecheck": "tsc --noEmit",
29
+ "test": "tsx --test test/unit/**/*.test.ts",
30
+ "prepublishOnly": "npm run typecheck && npm test && npm run build"
31
+ },
32
+ "engines": {
33
+ "node": ">=20"
34
+ },
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "git+https://github.com/paul-leo/Trellis.git"
38
+ },
39
+ "keywords": [
40
+ "mcp",
41
+ "model-context-protocol",
42
+ "claude-code",
43
+ "codex",
44
+ "kiro",
45
+ "agent-skills",
46
+ "agents.md"
47
+ ],
48
+ "devDependencies": {
49
+ "@modelcontextprotocol/sdk": "^1.30.0",
50
+ "@types/node": "^22.0.0",
51
+ "esbuild": "^0.28.2",
52
+ "tsx": "^4.19.0",
53
+ "typebox": "^1.3.0",
54
+ "typescript": "^5.6.0"
55
+ },
56
+ "dependencies": {
57
+ "yaml": "^2.9.1"
58
+ }
59
+ }
@@ -0,0 +1,33 @@
1
+ # .trellis/scope.yaml — exceptions only. Anything NOT listed here is
2
+ # shared with all four agents — that's the default Trellis exists for.
3
+ # Only write an entry when a capability should NOT go everywhere.
4
+ #
5
+ # Why this lives in its own file instead of inside each SKILL.md/agent.md:
6
+ # Codex validates SKILL.md frontmatter against an allow-list of known keys
7
+ # and rejects unrecognized ones (see docs/research.md). A Trellis-only
8
+ # `scope:` field embedded in SKILL.md's own frontmatter would break that
9
+ # skill specifically on Codex. Scope declarations stay outside every file
10
+ # an agent itself parses.
11
+ #
12
+ # MCP servers are the one exception: `mcp/servers.yaml` is Trellis's own
13
+ # format, never handed to an agent directly (always translated by an
14
+ # adapter), so its per-server `agents:` field is declared inline there
15
+ # instead of here — see schema/servers.example.yaml.
16
+
17
+ skills:
18
+ # This skill only makes sense for Claude Code's tool surface (e.g. it
19
+ # references the Task tool for subagent delegation, which the other
20
+ # three agents don't have an equivalent of).
21
+ claude-subagent-dispatch: [claude-code]
22
+
23
+ agents:
24
+ # Subagent profiles are implicitly Claude-Code-only today — Codex has no
25
+ # persistent subagent concept, Kiro/pi unconfirmed (see docs/research.md).
26
+ # No entries needed here until another agent gains an equivalent
27
+ # mechanism and a specific profile should NOT go to all of them.
28
+
29
+ memories:
30
+ # A memory entry that only makes sense in the context of one agent's
31
+ # own quirks (e.g. a note about Codex's TOML in-place-patch behavior)
32
+ # rather than something every agent's context should carry.
33
+ codex-toml-patch-gotcha: [codex]
@@ -0,0 +1,43 @@
1
+ # .trellis/secrets.policy.yaml — allow-list of variable names an adapter
2
+ # output is permitted to reference. `trellis secrets audit` uses this to
3
+ # tell "a legitimate ${VAR} reference" apart from "a name that shouldn't be
4
+ # here" — it does NOT store or see the actual secret values, only names.
5
+ #
6
+ # The real values live wherever your environment already resolves them from
7
+ # (a local .env-style file sourced by your shell, 1Password's `op run`,
8
+ # anything that populates process env before an adapter's generated command
9
+ # runs). Trellis never reads, writes, or transports the values themselves —
10
+ # with one narrow, unavoidable exception: pi's MCP bridge is Trellis's own
11
+ # code spawning the MCP server subprocess (unlike Claude Code/Codex/Kiro,
12
+ # whose own native MCP client resolves `${VAR}` in its own process), so it
13
+ # already has to read a value into memory to hand to that subprocess.
14
+ #
15
+ # env_file (optional) narrows *where* the bridge reads those values from —
16
+ # a dotenv-format file (KEY=VALUE per line, no quoting/interpolation), never
17
+ # merged with ambient process env once set: a name absent from it resolves
18
+ # to missing, it does NOT fall back to the shell's own exported value. This
19
+ # also lets `trellis secrets audit` check that every name your canonical
20
+ # mcp/servers.yaml declares actually resolves to something, agent-agnostic.
21
+ #
22
+ # WARNING: whatever file you point this at holds plaintext secrets. Never
23
+ # point it at a path inside a git-tracked directory (this file's own
24
+ # directory, ~/.trellis/, included) — a `git add .` there would commit them.
25
+ # env_file: ~/.config/agent-env/secrets.env
26
+
27
+ allowed_vars:
28
+ - TANKA_EMAIL
29
+ - TANKA_ENV
30
+ - GITLAB_PERSONAL_ACCESS_TOKEN
31
+ - GITLAB_API_URL
32
+ - MCPR_TOKEN
33
+ - NOTION_OPENAPI_MCP_HEADERS
34
+
35
+ # Patterns `trellis secrets audit` treats as "this looks like a literal
36
+ # credential, not a reference" and fails the build on sight. Extend this
37
+ # list as new credential shapes turn up — it grows from real incidents,
38
+ # not speculative coverage.
39
+ reject_patterns:
40
+ - 'glpat-[A-Za-z0-9_-]{20,}' # GitLab personal access token
41
+ - 'mcpr_[A-Za-z0-9_-]{20,}' # mcp-router token
42
+ - 'sk-[A-Za-z0-9]{20,}' # OpenAI-style API key
43
+ - 'ghp_[A-Za-z0-9]{30,}' # GitHub personal access token