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,341 @@
|
|
|
1
|
+
# Implementation plan
|
|
2
|
+
|
|
3
|
+
Breaks `docs/roadmap.md`'s phases into concrete tasks. P0–P2 are specified at
|
|
4
|
+
file/function level since they're next. P3–P4 are specified at
|
|
5
|
+
module/decision level. P5–P7 stay at the roadmap's level of detail — planning
|
|
6
|
+
them further now would be speculative, since their shape depends on what P0–P4
|
|
7
|
+
actually reveal.
|
|
8
|
+
|
|
9
|
+
Every task below ends in something that can fail. If a task can't fail, it
|
|
10
|
+
isn't specified enough yet.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## P0 — `trellis doctor` (read-only scan, no canonical source required)
|
|
15
|
+
|
|
16
|
+
> **Superseded by `openspec/changes/trellis-doctor-p0/`.** The section below
|
|
17
|
+
> is kept as a historical note only — the authoritative proposal, design,
|
|
18
|
+
> specs, and tasks for P0 live there now. Don't edit this section further.
|
|
19
|
+
|
|
20
|
+
**Key simplification**: P0 does not require `.trellis/` to exist. It
|
|
21
|
+
cross-compares the four agents' *own current state* against each other and
|
|
22
|
+
flags drift/duplication directly — this is the exact manual process used
|
|
23
|
+
throughout `docs/research.md`'s investigation (symlink-vs-realpath checks,
|
|
24
|
+
MCP handshake probes, hash comparisons), now formalized into code instead of
|
|
25
|
+
one-off shell commands.
|
|
26
|
+
|
|
27
|
+
### 0.1 Shared probing library — `src/lib/`
|
|
28
|
+
|
|
29
|
+
- `src/lib/mcpProbe.ts`
|
|
30
|
+
- `probeMcpServer(def: McpServerDef, env: NodeJS.ProcessEnv, timeoutMs = 10_000): Promise<McpProbeResult>`
|
|
31
|
+
- Spawns the server, sends a JSON-RPC `initialize`, resolves on the first
|
|
32
|
+
`id:1` response or process exit, whichever comes first. This is a direct
|
|
33
|
+
port of the probe script used repeatedly by hand in this project's
|
|
34
|
+
research — it needs to become a tested module, not stay a throwaway
|
|
35
|
+
`/tmp/mcp-probe.mjs`.
|
|
36
|
+
- Fails the task if: a known-good server (e.g. `@modelcontextprotocol/server-memory`)
|
|
37
|
+
does not return `serverInfo` within the timeout in CI.
|
|
38
|
+
|
|
39
|
+
- `src/lib/fsIdentity.ts`
|
|
40
|
+
- `isSymlinkTo(path: string, target: string): boolean`
|
|
41
|
+
- `realpathDedupe(paths: string[]): Map<string /* realpath */, string[] /* original paths pointing there */>`
|
|
42
|
+
- This directly encodes the Codex finding from `docs/research.md`: dedup
|
|
43
|
+
must happen on realpath, not on path string or content hash alone
|
|
44
|
+
(two physical copies with identical content are *not* the same thing to
|
|
45
|
+
an agent's discovery logic — they show up as two entries).
|
|
46
|
+
- Fails the task if: given two physical copies of the same skill directory
|
|
47
|
+
(not symlinks), it correctly reports them as **not** deduplicated —
|
|
48
|
+
inverse of what `isSymlinkTo` would report for an actual symlink pair.
|
|
49
|
+
|
|
50
|
+
- `src/lib/skillFile.ts`
|
|
51
|
+
- `findSkillFile(dir: string): { path: string; caseCorrect: boolean } | null`
|
|
52
|
+
- Case-sensitivity check is explicit and reported, not silently corrected —
|
|
53
|
+
this is the exact bug (`skill.md` vs `SKILL.md`) found and fixed by hand
|
|
54
|
+
in this project's own history.
|
|
55
|
+
|
|
56
|
+
### 0.2 Per-agent probes — `src/probes/`
|
|
57
|
+
|
|
58
|
+
Each file implements `probe(): Promise<AgentSnapshot>` where
|
|
59
|
+
`AgentSnapshot` is a new type (add to `src/core/types.ts`):
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
interface AgentSnapshot {
|
|
63
|
+
agent: "claude-code" | "codex" | "kiro" | "pi";
|
|
64
|
+
present: boolean;
|
|
65
|
+
version?: string;
|
|
66
|
+
skillRoots: { path: string; isSymlink: boolean; target?: string; skillCount: number }[];
|
|
67
|
+
mcpServers: { name: string; source: "static-config" | "runtime-injected"; probe?: McpProbeResult }[];
|
|
68
|
+
instructionsFile?: { path: string; isSymlink: boolean; target?: string };
|
|
69
|
+
subagentsDir?: { path: string; isSymlink: boolean; count: number };
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
- `src/probes/claude-code.ts` — reads `~/.claude.json` (`mcpServers` key),
|
|
74
|
+
`~/.claude/skills`, `~/.claude/agents`, `~/.claude/CLAUDE.md`. Best-understood
|
|
75
|
+
agent; build this one first to validate the `AgentSnapshot` shape before
|
|
76
|
+
committing to it for the other three.
|
|
77
|
+
|
|
78
|
+
- `src/probes/codex.ts` — shells out to `codex mcp list` rather than
|
|
79
|
+
hand-parsing `config.toml` (TOML section patching in P2 will need real
|
|
80
|
+
parsing, but P0 is read-only and `codex` already exposes this). Skill root
|
|
81
|
+
is `~/.agents/skills` per Codex's own convention (confirmed in
|
|
82
|
+
`docs/research.md`) — the probe should also check whether `~/.codex/skills`
|
|
83
|
+
independently contains any *non-symlink* skill directories, since that's
|
|
84
|
+
exactly the duplication bug already found and fixed once in this
|
|
85
|
+
environment and is exactly the kind of regression `doctor` exists to catch.
|
|
86
|
+
|
|
87
|
+
- `src/probes/kiro.ts` — same shape as Claude Code:
|
|
88
|
+
`~/.kiro/settings/mcp.json`, `~/.kiro/skills`, `~/.kiro/steering/CLAUDE.md`.
|
|
89
|
+
|
|
90
|
+
- `src/probes/pi.ts` — **has an open unknown, resolve it first**: pi's skill
|
|
91
|
+
discovery path was never pinned down by direct observation in this
|
|
92
|
+
project's research (only confirmed pi *reads* `SKILL.md`-format files and
|
|
93
|
+
`AGENTS.md`/`CLAUDE.md`; the actual discovery directory wasn't isolated
|
|
94
|
+
from the minified bundle). Before writing this probe:
|
|
95
|
+
1. Run `pi --skill <path>` against a known skill dir and confirm it loads
|
|
96
|
+
(`pi -p "what skills do you have"` or similar, non-interactively).
|
|
97
|
+
2. Check `pi config` (interactive TUI) for any skill-path setting, and
|
|
98
|
+
check `~/.pi/agent/settings.json` again after that interaction — the
|
|
99
|
+
settings file only had four keys when checked during research; a skill
|
|
100
|
+
path may be set here that wasn't visible before because nothing had
|
|
101
|
+
configured one yet.
|
|
102
|
+
3. If no fixed discovery directory exists (i.e. skills are strictly
|
|
103
|
+
`--skill`-flag-only, per-invocation), document that as a real
|
|
104
|
+
constraint — the pi adapter in P4 would then need to inject the flag
|
|
105
|
+
via a wrapper rather than write to a filesystem location, which changes
|
|
106
|
+
P4's design meaningfully. **This determination gates P4's design, not
|
|
107
|
+
just P0's probe — do it early.**
|
|
108
|
+
|
|
109
|
+
### 0.3 Command — `src/commands/doctor.ts`
|
|
110
|
+
|
|
111
|
+
- `runDoctor(opts: { json?: boolean }): Promise<{ exitCode: number }>`
|
|
112
|
+
- Calls all four probes in parallel (`Promise.allSettled` — one agent being
|
|
113
|
+
absent or erroring must not blank out the other three's results).
|
|
114
|
+
- Cross-compares snapshots pairwise for:
|
|
115
|
+
- Same skill name present with different realpaths across agents (drift)
|
|
116
|
+
- Any skill root containing a non-symlinked physical copy where a symlink
|
|
117
|
+
pattern is otherwise established for that agent (duplication risk)
|
|
118
|
+
- MCP server names present in one agent's static config that also appear
|
|
119
|
+
in `known_host_injected` for another — surfaces the exact class of bug
|
|
120
|
+
documented in `docs/research.md` (Codex + mirasim same-name collision)
|
|
121
|
+
*before* it causes a startup failure, not after.
|
|
122
|
+
- Human-readable output matches the table style already used throughout this
|
|
123
|
+
project's manual investigations (✅/⚠️/❌ per row) — this isn't cosmetic,
|
|
124
|
+
it's continuity with how these findings were actually communicated and
|
|
125
|
+
understood during research.
|
|
126
|
+
- `--json` emits `AgentSnapshot[]` for P5's SDK and any future GUI to
|
|
127
|
+
consume without re-parsing terminal output.
|
|
128
|
+
- Exit code is non-zero if any ⚠️/❌ finding exists. `doctor` is meant to be
|
|
129
|
+
runnable in CI/pre-commit, not just read by a human.
|
|
130
|
+
|
|
131
|
+
### P0 acceptance
|
|
132
|
+
|
|
133
|
+
Run against *this* machine (which has known, already-diagnosed issues
|
|
134
|
+
documented in `docs/research.md`) and confirm `doctor` independently
|
|
135
|
+
rediscovers at least these three already-known findings without being told
|
|
136
|
+
where to look:
|
|
137
|
+
1. The Codex `openspec-*` skill duplication (already fixed — confirm it now
|
|
138
|
+
reports clean).
|
|
139
|
+
2. Any current MCP server name collision against `known_host_injected`.
|
|
140
|
+
3. Any skill file with incorrect case (currently none — confirm it reports
|
|
141
|
+
clean, and separately unit-test the detector against a synthetic
|
|
142
|
+
`skill.md`).
|
|
143
|
+
|
|
144
|
+
If `doctor` can't independently reproduce findings that were previously only
|
|
145
|
+
found by hand, it isn't done — restating `docs/research.md`'s conclusions as
|
|
146
|
+
hardcoded checks doesn't count.
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## P1 — `trellis sync skills` / `trellis sync instructions`
|
|
151
|
+
|
|
152
|
+
> **Done and archived** (`openspec/changes/archive/2026-09-12-trellis-sync-p1/`).
|
|
153
|
+
> Section kept as a historical note only — see `docs/roadmap.md` for the
|
|
154
|
+
> actual outcome and the two real bugs the acceptance pass caught. Don't
|
|
155
|
+
> edit this section further.
|
|
156
|
+
|
|
157
|
+
Depends on P0's `AgentSnapshot` to decide *what* needs syncing; this phase
|
|
158
|
+
adds the *write* path.
|
|
159
|
+
|
|
160
|
+
- `.trellis/` canonical source becomes real for the first time here — not
|
|
161
|
+
before. `src/core/canonical.ts`: `loadCanonicalSource(): CanonicalSource`
|
|
162
|
+
reads `~/.trellis/skills/*/SKILL.md`, `~/.trellis/agents/*.md`,
|
|
163
|
+
`~/.trellis/agents.md`, `~/.trellis/scope.yaml`. **Global only** — see
|
|
164
|
+
`docs/architecture.md` "Global vs. workspace scope". No `root` parameter,
|
|
165
|
+
no workspace merge; don't build toward the `.agents Protocol` draft's
|
|
166
|
+
global+workspace precedence model until that's an explicit decision, not
|
|
167
|
+
a default carried over from the draft.
|
|
168
|
+
- `src/adapters/claude-code.ts`, `src/adapters/codex.ts`, `src/adapters/kiro.ts`,
|
|
169
|
+
`src/adapters/pi.ts` each implement `TrellisAdapter` (`src/core/adapter.ts`)
|
|
170
|
+
for skills + instructions only — MCP is P2/P4, deliberately kept out of
|
|
171
|
+
this phase's scope. Pi's adapter is the same symlink shape as the other
|
|
172
|
+
three (`~/.pi/agent/skills`, `~/.pi/agent/AGENTS.md` — confirmed real,
|
|
173
|
+
persistent paths per design.md D5 in the archived P0 change; see
|
|
174
|
+
docs/architecture.md's corrected pi adapter notes) — P4 is only pi's MCP
|
|
175
|
+
bridge, not its skills/instructions, which need nothing more than what
|
|
176
|
+
every other agent here needs.
|
|
177
|
+
`plan()` filters skills/subagent profiles through `isInScope` before
|
|
178
|
+
producing any plan item — a skill scoped away from that adapter's `id`
|
|
179
|
+
must produce zero plan items for it, not a plan item that apply() later
|
|
180
|
+
skips. `apply()` handles three cases, not just creation:
|
|
181
|
+
- **create/repair**: no symlink, or a symlink pointing at the wrong
|
|
182
|
+
target — safe to (re)write.
|
|
183
|
+
- **remove**: an agent's skill directory has a Trellis-managed symlink
|
|
184
|
+
(its realpath resolves inside the canonical `skills/` root) whose
|
|
185
|
+
corresponding skill either no longer exists in canonical or was just
|
|
186
|
+
scoped away from this agent — this is the "delete" half of "add once,
|
|
187
|
+
remove once, reaches every agent," and it's easy to build only the
|
|
188
|
+
create path and quietly never finish this half.
|
|
189
|
+
- **refuse**: the path exists as a *real* directory, not a symlink at
|
|
190
|
+
all — this is not Trellis's to touch. Surface it as a conflict and
|
|
191
|
+
stop; never delete something that might be a user's own content just
|
|
192
|
+
because its name matches a skill Trellis also knows about.
|
|
193
|
+
|
|
194
|
+
The realpath check in the remove case is not optional: it's what makes
|
|
195
|
+
removal safe. Only ever delete a symlink Trellis can prove it created
|
|
196
|
+
(points inside the canonical source), never a same-named real directory.
|
|
197
|
+
- `verify()` re-runs the relevant P0 probe and diffs against canonical —
|
|
198
|
+
this is why P0's probes are written as reusable functions, not
|
|
199
|
+
doctor-command-only logic.
|
|
200
|
+
|
|
201
|
+
### P1 acceptance
|
|
202
|
+
|
|
203
|
+
Point at a scratch `$HOME` (or a container) with none of the four agents'
|
|
204
|
+
skill directories populated, run `trellis sync skills`, then run `trellis
|
|
205
|
+
doctor` and confirm zero findings. Then manually corrupt one symlink
|
|
206
|
+
(point it somewhere wrong) and confirm `doctor` catches it before re-running
|
|
207
|
+
sync to confirm `apply()` repairs it idempotently. Separately: add one
|
|
208
|
+
skill scoped to `[claude-code]` only in `scope.yaml`, run `trellis sync
|
|
209
|
+
skills`, and confirm it appears only in Claude Code's skills directory —
|
|
210
|
+
not Codex's, Kiro's, or pi's.
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
## P2 — `trellis mcp sync`
|
|
215
|
+
|
|
216
|
+
> **Done and archived** (`openspec/changes/archive/2026-09-12-trellis-mcp-sync-p2/`).
|
|
217
|
+
> Section kept as a historical note only — see `docs/roadmap.md` for the
|
|
218
|
+
> actual outcome. Two corrections against this plan's own assumptions,
|
|
219
|
+
> found before/while implementing: neither `@iarna/toml` nor `smol-toml`
|
|
220
|
+
> survives a round-trip (both drop comments, reformat arrays) — Codex is
|
|
221
|
+
> patched by a hand-rolled line-based section locator/splicer instead
|
|
222
|
+
> (`src/lib/tomlSection.ts`), no library dependency at all; and automatic
|
|
223
|
+
> MCP removal turned out to be unsafe without an ownership marker a bare
|
|
224
|
+
> TOML/JSON key has no equivalent of (a symlink's realpath proves it for
|
|
225
|
+
> skills) — deferred until a lock-file mechanism exists, `plan()` only
|
|
226
|
+
> ever produces create/repair + conflict for MCP. Don't edit this section
|
|
227
|
+
> further.
|
|
228
|
+
|
|
229
|
+
The highest-risk phase — this is where `docs/research.md`'s hardest-won
|
|
230
|
+
constraint applies directly: **Codex's adapter must patch a TOML section in
|
|
231
|
+
place, never regenerate the whole file**, because Codex's own `config.toml`
|
|
232
|
+
holds settings Trellis has no business touching (models, trust levels), and
|
|
233
|
+
because mirasim writes to the same file independently.
|
|
234
|
+
|
|
235
|
+
- `src/adapters/codex.ts` (extended): needs a real TOML parser that
|
|
236
|
+
preserves comments/formatting outside the touched section (`@iarna/toml`
|
|
237
|
+
or `smol-toml` — evaluate both for round-trip fidelity before picking one;
|
|
238
|
+
this is exactly the kind of "don't reinvent" call `docs/research.md`
|
|
239
|
+
already commits to). Patch strategy: locate `[mcp_servers.<name>]` by
|
|
240
|
+
section header, replace only that block, leave every byte outside it
|
|
241
|
+
untouched.
|
|
242
|
+
- Collision check runs `known_host_injected` from `mcp/servers.yaml` against
|
|
243
|
+
every server name about to be written; refuses to write and exits non-zero
|
|
244
|
+
on collision, with the exact error class from `docs/research.md`
|
|
245
|
+
(`url is not supported for stdio`) quoted in the failure message so a user
|
|
246
|
+
hitting this via Trellis gets the real cause immediately instead of
|
|
247
|
+
rediscovering it as a mysterious Codex crash.
|
|
248
|
+
- Every adapter's `plan()` filters servers through `isInScope` using each
|
|
249
|
+
server's inline `agents:` field (not `scope.yaml` — see
|
|
250
|
+
`docs/architecture.md` "Private / agent-specific capabilities" for why
|
|
251
|
+
MCP scoping is inline) before generating anything for that agent.
|
|
252
|
+
- `src/adapters/claude-code.ts` / `kiro.ts` (extended): plain JSON deep-merge
|
|
253
|
+
under `mcpServers`, values always emitted as `${VAR}` — never a literal,
|
|
254
|
+
enforced by a check against `secrets.policy.yaml`'s `reject_patterns`
|
|
255
|
+
*before* the write happens, not just audited after (P3 audits adapter
|
|
256
|
+
*output*; this is a pre-write guard specific to the MCP adapter, cheaper
|
|
257
|
+
to catch here than after the fact).
|
|
258
|
+
- **Hub mode branch** (`docs/architecture.md` "MCP hub mode"): every
|
|
259
|
+
adapter checks `canonical.mcp.hub` first. If set, skip the whole
|
|
260
|
+
N-server merge/patch logic above and instead ensure exactly one static
|
|
261
|
+
entry (name TBD, e.g. `trellis-hub`) pointing at `hub.url` exists —
|
|
262
|
+
Codex still patches in place, Claude/Kiro still JSON-merge, just one
|
|
263
|
+
entry instead of N. The collision check still runs, just against that
|
|
264
|
+
one name instead of every server name.
|
|
265
|
+
|
|
266
|
+
### P2 acceptance
|
|
267
|
+
|
|
268
|
+
Reproduce the exact Codex incident from `docs/research.md` in a sandboxed
|
|
269
|
+
`config.toml` (a stdio server statically defined, then a same-name `url`
|
|
270
|
+
server "injected" via `-c` the way mirasim does) and confirm Trellis's
|
|
271
|
+
collision check refuses the write *before* attempting it, rather than
|
|
272
|
+
letting Codex fail to start. Separately, with `hub.url` set in the fixture's
|
|
273
|
+
`servers.yaml`, confirm each agent's generated config contains exactly one
|
|
274
|
+
MCP entry regardless of how many servers are defined.
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
## P3 — `trellis secrets audit`
|
|
279
|
+
|
|
280
|
+
> **Done and archived** (`openspec/changes/archive/2026-09-12-trellis-secrets-audit-p3/`).
|
|
281
|
+
> Section kept as a historical note only — see `docs/roadmap.md` for the
|
|
282
|
+
> actual outcome. Built as planned, with no dependency added: env-var-name
|
|
283
|
+
> extraction turned out to need only real `JSON.parse` (Claude Code/Kiro)
|
|
284
|
+
> and a five-line regex over one TOML line shape (Codex), not a general
|
|
285
|
+
> parser or a reused write-path module. Don't edit this section further.
|
|
286
|
+
|
|
287
|
+
- `src/commands/secretsAudit.ts`: reads every adapter's *actual output file*
|
|
288
|
+
(not the canonical source — the point is catching what actually landed on
|
|
289
|
+
disk) and runs `secrets.policy.yaml`'s `reject_patterns` against it.
|
|
290
|
+
- Must catch, as regression tests, the two real incidents already on
|
|
291
|
+
record: a GitLab PAT stored under the wrong variable name (this is a
|
|
292
|
+
*naming* bug the pattern-match alone won't catch — audit also needs an
|
|
293
|
+
"unexpected variable name" check against `allowed_vars`, not just a
|
|
294
|
+
literal-value regex) and a literal token value embedded directly in a
|
|
295
|
+
generated config (the regex case).
|
|
296
|
+
|
|
297
|
+
## P4 — pi MCP bridge extension
|
|
298
|
+
|
|
299
|
+
> **Done and archived** (`openspec/changes/archive/2026-09-12-trellis-pi-mcp-bridge-p4/`).
|
|
300
|
+
> Section kept as a historical note only — see `docs/roadmap.md` for the
|
|
301
|
+
> actual outcome. Built largely as sketched (a symlinked extension, no
|
|
302
|
+
> settings.json write, `registerTool` per MCP tool), but the delivered
|
|
303
|
+
> file had to become a fully bundled, dependency-free build output
|
|
304
|
+
> (`dist/pi-bridge/bundle.js` via `esbuild`) rather than raw TypeScript
|
|
305
|
+
> source — a real sandbox run against the actual `pi` binary showed a
|
|
306
|
+
> symlinked file's own imports resolve relative to the symlink's path,
|
|
307
|
+
> not its target, so an unbundled file could never resolve its
|
|
308
|
+
> dependencies once placed in a real user's home directory. Don't edit
|
|
309
|
+
> this section further.
|
|
310
|
+
|
|
311
|
+
**MCP only** — pi's skills/instructions adapter is P1's `src/adapters/pi.ts`
|
|
312
|
+
(same symlink shape as every other agent, see P1 above); P4 is exclusively
|
|
313
|
+
the MCP bridge, since pi has no native MCP client at all (docs/research.md).
|
|
314
|
+
|
|
315
|
+
§0.2's investigation resolved during P0 (design.md D5 in the archived
|
|
316
|
+
`trellis-doctor-p0` change): pi has a real discovery directory
|
|
317
|
+
(`~/.pi/agent/skills`), settling which of the two candidate shapes below
|
|
318
|
+
applies — the first one:
|
|
319
|
+
|
|
320
|
+
- A pi extension package, installed once, that on startup reads
|
|
321
|
+
`.trellis/mcp/servers.yaml`. If `mcp.hub` is unset, it opens an
|
|
322
|
+
`@modelcontextprotocol/sdk` `StdioClientTransport` per server and calls
|
|
323
|
+
pi's `registerTool` for each tool the server reports; if `mcp.hub.url` is
|
|
324
|
+
set, it instead opens a single `StreamableHTTPClientTransport` to that
|
|
325
|
+
URL — one connection instead of N processes to keep alive, see
|
|
326
|
+
`docs/architecture.md` "MCP hub mode".
|
|
327
|
+
|
|
328
|
+
This is the one adapter that is genuine runtime code, not a config
|
|
329
|
+
generator — budget real testing time against a live pi session, not just
|
|
330
|
+
unit tests of the bridging logic in isolation.
|
|
331
|
+
|
|
332
|
+
---
|
|
333
|
+
|
|
334
|
+
## Sequencing note
|
|
335
|
+
|
|
336
|
+
Build order: **Claude Code → Codex → Kiro → pi**, in that order, for every
|
|
337
|
+
phase through P4. Claude Code and Codex are already the best-understood
|
|
338
|
+
(most of `docs/research.md`'s hard-won findings are about them); Kiro is
|
|
339
|
+
structurally identical to Claude Code and should be fast once that shape is
|
|
340
|
+
validated; pi is last because it has the one open unknown (§0.2) and the one
|
|
341
|
+
genuinely novel engineering problem (§P4).
|
package/docs/research.md
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# Research: what already exists
|
|
2
|
+
|
|
3
|
+
Surveyed 2026-09-11. This is why Trellis is a thin orchestration layer, not a
|
|
4
|
+
from-scratch rebuild of anything below.
|
|
5
|
+
|
|
6
|
+
## Rules / skills sync (crowded — don't rebuild)
|
|
7
|
+
|
|
8
|
+
| Project | Covers | Kiro | pi | MCP | Memory | Secrets |
|
|
9
|
+
|---|---|---|---|---|---|---|
|
|
10
|
+
| [block/ai-rules](https://github.com/block/ai-rules) (Rust, Apache-2.0, Block/Square) | 11 agents (AMP, Claude Code, Cline, Codex, Copilot, Cursor, Firebender, Gemini, Goose, Kilocode, Roo) | ❌ | ❌ | config generation only | ❌ | ❌ |
|
|
11
|
+
| [lbb00/ai-rules-sync](https://github.com/lbb00/ai-rules-sync) | Cursor/Claude/Copilot/OpenCode/Trae/Codex/Gemini/Warp | ❌ | ❌ | ❌ | ❌ | ❌ |
|
|
12
|
+
| [shanliuling/skills-link](https://github.com/shanliuling/skills-link) | 41+ agents, pure symlink | unlisted | unlisted | ❌ | ❌ | ❌ |
|
|
13
|
+
| [runkids/skillshare](https://github.com/runkids/skillshare), [qufei1993/skills-hub](https://github.com/qufei1993/skills-hub) | Codex/Claude/OpenClaw etc. | ❌ | ❌ | ❌ | ❌ | ❌ |
|
|
14
|
+
| [PanisHandsome/ai-rules-sync](https://github.com/PanisHandsome/ai-rules-sync) | AGENTS.md/CLAUDE.md/.cursorrules/Copilot/Windsurf/Cline/Aider/Gemini | ❌ | ❌ | ❌ | ❌ | ❌ |
|
|
15
|
+
| [yelmuratoff/agent_sync](https://github.com/yelmuratoff/agent_sync) | Claude/Cursor/Copilot/Gemini + 10 more | ❌ | ❌ | ❌ | ❌ | ❌ |
|
|
16
|
+
|
|
17
|
+
**No existing tool covers Kiro or pi.** None combine rules+skills with MCP,
|
|
18
|
+
memory, and secret hygiene as one system. That's the actual gap.
|
|
19
|
+
|
|
20
|
+
## The `.agents Protocol` draft
|
|
21
|
+
|
|
22
|
+
[dotagentsprotocol.com](https://dotagentsprotocol.com) (status: DRAFT,
|
|
23
|
+
2026-02-24, community-maintained, no vendor backing yet) proposes exactly the
|
|
24
|
+
directory shape this problem needs:
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
.agents/
|
|
28
|
+
├── agents.md # instructions (compatible with the AGENTS.md standard)
|
|
29
|
+
├── mcp.json # MCP servers
|
|
30
|
+
├── skills/*/skill.md # skill definitions
|
|
31
|
+
├── agents/*/agent.md # subagent profiles
|
|
32
|
+
├── tasks/*/task.md # recurring tasks
|
|
33
|
+
└── memories/*.md # persistent memory entries
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
It explicitly has **no reference implementation** yet. Trellis targets
|
|
37
|
+
alignment with this draft rather than inventing a competing schema — being
|
|
38
|
+
the first working implementation is more leverage than being a sixth
|
|
39
|
+
standard. See [`schema/`](../schema) for where Trellis's schema matches the
|
|
40
|
+
draft and where it deliberately extends it (Kiro/pi adapters, secret
|
|
41
|
+
references, MCP env passthrough semantics — none of which the draft
|
|
42
|
+
specifies).
|
|
43
|
+
|
|
44
|
+
## MCP aggregation (reuse, don't rebuild)
|
|
45
|
+
|
|
46
|
+
| Project | Stars | Model | Notes |
|
|
47
|
+
|---|---|---|---|
|
|
48
|
+
| [MetaMCP](https://github.com/metatool-ai/metamcp) | 2.6k, MIT | Docker + Postgres, namespaces, OAuth | Heaviest, most complete; remote-only endpoints need a local stdio bridge for desktop clients |
|
|
49
|
+
| [mcp-hub](https://github.com/ravitemer/mcp-hub) | 516★, 227 commits, MIT | Plain Node process, no Docker, single HTTP endpoint (`/mcp`), JSON config with `command`/`args`/`env` (stdio) or `url`/`headers` (remote) — matches Trellis's own `servers.yaml` fields almost 1:1 | Dynamic add/remove without client restart — SSE-pushes `servers_updated`/`tool_list_changed` to already-connected agents |
|
|
50
|
+
| [mcp-router](https://github.com/mcp-router/mcp-router) | — | Desktop app + CLI, Sustainable Use License | Already deployed in this environment; token-based, has a GUI. **First-hand incident**: its `MCPR_TOKEN` existed in three different values across `secrets.env`/`config.toml`/`kiro/mcp.json` simultaneously, one silently invalid — see below |
|
|
51
|
+
|
|
52
|
+
Decision: Trellis still does not implement aggregation/routing logic
|
|
53
|
+
itself. It generates native MCP config for each agent directly — that's
|
|
54
|
+
what actually needed solving, since none of the above touch Kiro/pi/Codex's
|
|
55
|
+
TOML format. But it now natively *supports* routing every agent through a
|
|
56
|
+
single external endpoint instead of N direct definitions ("hub mode," see
|
|
57
|
+
`docs/architecture.md`) — not a product integration, just an optional
|
|
58
|
+
`hub.url` field every adapter checks. What runs behind that URL (mcp-hub,
|
|
59
|
+
mcp-router, anything else) is the user's choice, not Trellis's; the type
|
|
60
|
+
(`HubConfig`) deliberately has no engine/product field to keep in sync.
|
|
61
|
+
|
|
62
|
+
The mcp-router incident above is the concrete argument for preferring a
|
|
63
|
+
self-hosted hub whose config Trellis generates from the same
|
|
64
|
+
`servers.yaml` over one managed through an external dashboard: the latter
|
|
65
|
+
becomes a second place "what servers exist" is defined, outside `.trellis/`
|
|
66
|
+
entirely, and this project hit real drift from exactly that shape of setup
|
|
67
|
+
before hub mode existed as a documented option.
|
|
68
|
+
|
|
69
|
+
## Shared memory (reuse)
|
|
70
|
+
|
|
71
|
+
Checked directly against this real machine before writing this section,
|
|
72
|
+
rather than trusting an earlier draft's claim at face value — the result
|
|
73
|
+
is more nuanced than either "confirmed" or "wrong":
|
|
74
|
+
|
|
75
|
+
- `src/commands/doctor.ts`'s `DEFAULT_KNOWN_HOST_INJECTED` already lists
|
|
76
|
+
`"memory"`, documented there as "mirasim's actual known connectors on
|
|
77
|
+
this project's own machine" (P0's own empirical finding, not a guess).
|
|
78
|
+
Runtime-injected connectors are, by design, invisible in any agent's
|
|
79
|
+
*static* config — that's the entire reason `known_host_injected` exists
|
|
80
|
+
as a concept distinct from what a config file shows. So this likely
|
|
81
|
+
*is* real: mirasim probably does inject a `server-memory`-class
|
|
82
|
+
connector named `memory` at runtime for every session on this machine,
|
|
83
|
+
and no amount of reading `.claude.json`/`config.toml`/`mcp.json` could
|
|
84
|
+
confirm or deny that either way.
|
|
85
|
+
- What static config *does* show, independent of whatever mirasim
|
|
86
|
+
injects: Kiro alone has an *additional* memory server explicitly
|
|
87
|
+
configured — [`totalrecallai`](https://github.com/Auriti-Labs/kiro-memory)
|
|
88
|
+
(npm `totalrecallai`) — SQLite-backed (`better-sqlite3`) with a local
|
|
89
|
+
embedding model for semantic/vector search (`fastembed` + `onnxruntime`),
|
|
90
|
+
a bundled React web viewer, AGPL-3.0 licensed, its own repo literally
|
|
91
|
+
named `kiro-memory` (purpose-built for Kiro, later marketed as
|
|
92
|
+
MCP-client-agnostic). Claude Code and Codex have no additional static
|
|
93
|
+
memory server at all. Whatever mirasim injects at runtime is presumably
|
|
94
|
+
uniform across all three; this static difference is real, additional,
|
|
95
|
+
and Kiro-specific — the single-source fragmentation problem Trellis
|
|
96
|
+
exists to solve, found in the wild rather than assumed on paper.
|
|
97
|
+
|
|
98
|
+
The official reference server
|
|
99
|
+
[`@modelcontextprotocol/server-memory`](https://github.com/modelcontextprotocol/servers/tree/main/src/memory)
|
|
100
|
+
is zero-dependency, local JSON knowledge graph, MIT-licensed, maintained
|
|
101
|
+
as part of the official `modelcontextprotocol/servers` repo. Trellis
|
|
102
|
+
documents this as the default (`schema/servers.example.yaml`'s `memory`
|
|
103
|
+
entry) — the safe, unopinionated choice for a default any `.trellis/`
|
|
104
|
+
setup can adopt without pulling in native bindings or a copyleft license
|
|
105
|
+
obligation. It's very likely already what a mirasim-hosted session gets
|
|
106
|
+
via runtime injection (see `known_host_injected` above) — on a host like
|
|
107
|
+
that, declaring it again in `mcp/servers.yaml` collides with the
|
|
108
|
+
already-injected connector, exactly the class of incident
|
|
109
|
+
`known_host_injected` exists to refuse (see the schema example's own
|
|
110
|
+
comment for how to tell which situation applies). On a host with no such
|
|
111
|
+
injection, declaring it is what actually wires the default. Either way,
|
|
112
|
+
`totalrecallai`'s semantic-search/SQLite/viewer feature set belongs in
|
|
113
|
+
the same *opt-in upgrade* category as mem0/OpenMemory below, not silently
|
|
114
|
+
adopted as the default just because it happened to already be configured
|
|
115
|
+
on one agent, on one machine.
|
|
116
|
+
|
|
117
|
+
[mem0 / OpenMemory MCP](https://github.com/mem0ai/mem0) — "private,
|
|
118
|
+
local-first memory layer with a built-in UI, compatible with all MCP
|
|
119
|
+
clients." Mature, but needs Docker + an LLM key. Documented as the
|
|
120
|
+
upgrade path for cross-machine or richer semantic memory, alongside
|
|
121
|
+
`totalrecallai` as a second real option in that same category.
|
|
122
|
+
|
|
123
|
+
## Secrets (formalize existing practice, don't build new infra)
|
|
124
|
+
|
|
125
|
+
The 2026 consensus across 1Password, Vault, Infisical writeups is uniform:
|
|
126
|
+
**configs hold references, real values resolve only at process-spawn time,
|
|
127
|
+
never persisted twice.** This is exactly what this environment was already
|
|
128
|
+
doing by hand (`${VAR}` in Claude Code's `mcpServers`, `env_vars` passthrough
|
|
129
|
+
in Codex's `config.toml`) before Trellis existed — confirmed working via two
|
|
130
|
+
real incidents during development (a GitLab PAT stored under the wrong
|
|
131
|
+
variable name, and three divergent values of the same router token, one of
|
|
132
|
+
which was silently invalid). Trellis's contribution here is `trellis secrets
|
|
133
|
+
audit` — a linter that fails a build if any adapter output contains a raw
|
|
134
|
+
credential — not a new secret store.
|
|
135
|
+
|
|
136
|
+
## pi coding agent — the one place we write real code, not config generation
|
|
137
|
+
|
|
138
|
+
Reverse-engineered directly from the installed binary
|
|
139
|
+
(`@earendil-works/pi-coding-agent`, confirmed via `strings` on
|
|
140
|
+
`dist/bundle/chunks/*.js`, not from documentation):
|
|
141
|
+
|
|
142
|
+
- pi **natively discovers** `AGENTS.override.md`, `AGENTS.md`, `CLAUDE.md`
|
|
143
|
+
(in that priority order) — instructions and `SKILL.md`-format skills need
|
|
144
|
+
no adapter work at all.
|
|
145
|
+
- pi has **no native MCP client**. The only `mcpServers` string in its bundle
|
|
146
|
+
belongs to the Google Gen AI SDK's Vertex tool-schema translator, which
|
|
147
|
+
explicitly throws `"mcpServers parameter is not supported"` — unrelated to
|
|
148
|
+
MCP proper.
|
|
149
|
+
- pi's capability surface is **extensions**: JS modules loaded via
|
|
150
|
+
`--extension`/discovery, with a real API (`createExtension`,
|
|
151
|
+
`registerTool`). Giving pi access to the same MCP servers every other agent
|
|
152
|
+
uses means writing a bridge extension that spins up
|
|
153
|
+
`@modelcontextprotocol/sdk` stdio clients from `mcp.json` and registers
|
|
154
|
+
their tools through this API. This is the one adapter in Trellis that is
|
|
155
|
+
genuine code, not config templating — see
|
|
156
|
+
[`src/adapters/pi/`](../src/adapters/pi).
|
|
157
|
+
|
|
158
|
+
## Codex — three hard constraints learned by breaking them
|
|
159
|
+
|
|
160
|
+
All three below came from real incidents, not upfront design, and any Codex
|
|
161
|
+
adapter must respect them:
|
|
162
|
+
|
|
163
|
+
1. **Skill discovery dedups by realpath, not by content.** Two physical
|
|
164
|
+
copies of an identical skill directory both get listed (duplicate tool
|
|
165
|
+
entries); a symlink to the same target is correctly merged into one. Never
|
|
166
|
+
copy a skill into `~/.codex/skills` — always symlink.
|
|
167
|
+
2. **`SKILL.md` must match byte-for-byte**, including case. A file saved as
|
|
168
|
+
`skill.md` is silently dropped from discovery — no warning, no error.
|
|
169
|
+
3. **Same-name MCP server across a static config and a `-c` runtime override
|
|
170
|
+
is not "last write wins" — it's a field-level merge.** If Codex's own
|
|
171
|
+
`config.toml` defines a server as stdio (`command`) and something injects
|
|
172
|
+
a `url` under the same name at runtime, the *entire* `codex` process fails
|
|
173
|
+
to start (`url is not supported for stdio`), not just that one server.
|
|
174
|
+
Trellis must never let a locally-defined server name collide with a name
|
|
175
|
+
a host environment (like mirasim) is known to inject.
|