agent-trellis 0.1.0 → 0.3.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/README.md +69 -17
- package/dist/adapters/claude-code.d.ts +5 -3
- package/dist/adapters/claude-code.js +27 -14
- package/dist/adapters/codex.d.ts +8 -4
- package/dist/adapters/codex.js +47 -16
- package/dist/adapters/jsonMcp.d.ts +16 -5
- package/dist/adapters/jsonMcp.js +38 -29
- package/dist/adapters/kiro.d.ts +5 -3
- package/dist/adapters/kiro.js +29 -16
- package/dist/adapters/mcpPlan.d.ts +11 -6
- package/dist/adapters/mcpPlan.js +40 -7
- package/dist/adapters/pi.d.ts +2 -1
- package/dist/adapters/pi.js +4 -4
- package/dist/adapters/symlinkPlan.d.ts +7 -3
- package/dist/adapters/symlinkPlan.js +42 -16
- package/dist/cli.js +161 -18
- package/dist/commands/init.js +11 -0
- package/dist/commands/mcp.d.ts +114 -7
- package/dist/commands/mcp.js +258 -17
- package/dist/commands/memory.d.ts +39 -0
- package/dist/commands/memory.js +78 -0
- package/dist/commands/migrate.d.ts +30 -4
- package/dist/commands/migrate.js +83 -16
- package/dist/commands/onboard.d.ts +52 -7
- package/dist/commands/onboard.js +318 -35
- package/dist/commands/rollback.d.ts +44 -0
- package/dist/commands/rollback.js +201 -0
- package/dist/commands/secretsAudit.d.ts +7 -0
- package/dist/commands/secretsAudit.js +14 -7
- package/dist/commands/skill.d.ts +51 -0
- package/dist/commands/skill.js +104 -0
- package/dist/commands/sync.d.ts +13 -0
- package/dist/commands/sync.js +31 -5
- package/dist/core/adapter.d.ts +28 -11
- package/dist/core/adapter.js +2 -2
- package/dist/core/canonical.d.ts +26 -1
- package/dist/core/canonical.js +103 -3
- package/dist/core/types.d.ts +29 -1
- package/dist/core/types.js +11 -2
- package/dist/lib/backup.d.ts +56 -0
- package/dist/lib/backup.js +98 -0
- package/dist/lib/deepEqual.d.ts +8 -0
- package/dist/lib/deepEqual.js +26 -0
- package/dist/lib/dirEquals.d.ts +9 -0
- package/dist/lib/dirEquals.js +15 -1
- package/dist/lib/installAgent.d.ts +26 -0
- package/dist/lib/installAgent.js +46 -0
- package/dist/lib/mcpMigrateRead.d.ts +69 -0
- package/dist/lib/mcpMigrateRead.js +188 -0
- package/dist/lib/mcpOwnership.d.ts +25 -0
- package/dist/lib/mcpOwnership.js +50 -0
- package/dist/lib/memoryGraph.d.ts +60 -0
- package/dist/lib/memoryGraph.js +101 -0
- package/dist/lib/realHomeSnapshot.d.ts +26 -0
- package/dist/lib/realHomeSnapshot.js +77 -0
- package/dist/lib/terminalPicker.d.ts +45 -0
- package/dist/lib/terminalPicker.js +193 -0
- package/dist/lib/tomlSection.d.ts +20 -6
- package/dist/lib/tomlSection.js +78 -12
- package/dist/pi-bridge/bundle.js +100 -51
- package/dist/pi-bridge/index.js +14 -2
- package/dist/probes/codex.js +10 -2
- package/docs/architecture.md +7 -4
- package/docs/getting-started.md +267 -33
- package/docs/roadmap.md +444 -0
- package/package.json +1 -1
- package/schema/servers.example.yaml +39 -2
package/docs/roadmap.md
CHANGED
|
@@ -467,6 +467,438 @@ symlink-safe — the reminder here: a real installed-package check is
|
|
|
467
467
|
not a redundant formality alongside the unit suite, it's the only
|
|
468
468
|
thing in this project that exercises the actual `bin` symlink at all.
|
|
469
469
|
|
|
470
|
+
**`trellis-managed-agents`, done and archived**
|
|
471
|
+
(`openspec/changes/archive/2026-09-13-trellis-managed-agents/`; adds
|
|
472
|
+
`agent-management-scope`, modifies `onboarding-flow`,
|
|
473
|
+
`skill-instructions-sync`, `mcp-server-sync`, `secrets-audit`). Fixes a
|
|
474
|
+
real gap found using `onboard` for its first real migration (Claude Code
|
|
475
|
+
→ pi, on this project's own developer machine): `sync`/`mcp sync`/
|
|
476
|
+
`secrets audit` acted on **every present agent** unconditionally, with no
|
|
477
|
+
way to say "only manage these ones." New persisted state,
|
|
478
|
+
`~/.trellis/managed.yaml` — absent or `agents: []` both mean zero managed
|
|
479
|
+
agents, never "everyone" (a deliberate pre-1.0 default reversal, no
|
|
480
|
+
back-compat shim). `resolveScope`'s no-scope fallback changed from
|
|
481
|
+
`ALL_AGENTS` to the managed set, and an item's own explicit scope is now
|
|
482
|
+
intersected with it, never used verbatim — the managed set is the hard
|
|
483
|
+
outer boundary every other scoping decision lives inside.
|
|
484
|
+
|
|
485
|
+
`onboard` splits what used to be one "pick a base agent" choice into two
|
|
486
|
+
independent ones: a **migration source** (read-only, at most one, same
|
|
487
|
+
resolution rules as before) and a **managed set** (zero or more, written
|
|
488
|
+
to). The source is offered in the managed-set prompt but starts
|
|
489
|
+
unchecked by default — importing from Claude Code no longer implies
|
|
490
|
+
Trellis should also manage Claude Code. Selecting an agent that isn't
|
|
491
|
+
installed yet (e.g. pi) is itself the authorization to install it — one
|
|
492
|
+
confirmation, then a real `npm install -g <package>` (`src/lib/
|
|
493
|
+
installAgent.ts`), never silent even under `--manage`; Kiro has no CLI
|
|
494
|
+
package and is refused with its download URL instead. Re-running
|
|
495
|
+
`onboard`'s managed-set selection is a union with whatever was already
|
|
496
|
+
in `managed.yaml`, never a replacement — otherwise a later run adding
|
|
497
|
+
codex, forgetting to reselect pi out of habit, would silently unmanage
|
|
498
|
+
it. `~/.agents` (Codex's own `~/.ai-config`-sourced skill convention) is
|
|
499
|
+
now a directly tested non-goal rather than an incidental consequence of
|
|
500
|
+
the ownership-conflict fix below.
|
|
501
|
+
|
|
502
|
+
Found and fixed a second real bug in the same investigation, upstream of
|
|
503
|
+
this change's own scope but caught while diagnosing "did the first real
|
|
504
|
+
`onboard` run corrupt anything": `src/adapters/symlinkPlan.ts` treated
|
|
505
|
+
*any* existing symlink at a target path as safe to repair, without
|
|
506
|
+
checking whether its stored target was actually inside Trellis's own
|
|
507
|
+
canonical source. A real `onboard` run silently repointed
|
|
508
|
+
`~/.codex/instructions.md` and `~/.kiro/steering/CLAUDE.md` — both
|
|
509
|
+
previously symlinks into a separate, user-owned `~/.ai-config` setup —
|
|
510
|
+
at `~/.trellis/agents.md` instead, with zero warning. Fixed by checking
|
|
511
|
+
the existing symlink's raw `readlink` target against `canonicalRoot`
|
|
512
|
+
before treating it as repairable; anything pointing elsewhere is now a
|
|
513
|
+
`"conflict"`, left untouched, same as a real non-symlink file always
|
|
514
|
+
was. Verified against the real machine: the two symlinks were restored
|
|
515
|
+
by hand, the fix confirmed via `sync instructions --dry-run` reporting
|
|
516
|
+
conflicts instead of creates, and 194→197 tests passing throughout.
|
|
517
|
+
|
|
518
|
+
Verified in the real Docker sandbox: baseline `sync` with all four
|
|
519
|
+
fixture agents listed in `managed.yaml` reproduces the exact same output
|
|
520
|
+
this project's earlier sandbox runs documented (no regression), then a
|
|
521
|
+
narrowed `managed.yaml` (`agents: [pi]`) reproduces zero writes and zero
|
|
522
|
+
report lines for the other three, and a full `onboard --agent
|
|
523
|
+
claude-code --manage pi` run reproduces this session's own real
|
|
524
|
+
use case end to end — migrate from claude-code, manage only pi, source
|
|
525
|
+
left completely untouched.
|
|
526
|
+
|
|
527
|
+
Then run for real, once, on this project's own developer machine (not a
|
|
528
|
+
sandbox): `onboard --agent claude-code --manage pi` genuinely installed
|
|
529
|
+
pi (`npm install -g @earendil-works/pi-coding-agent`, real confirmation
|
|
530
|
+
prompt, real 132-package install), wrote `managed.yaml` as `agents:
|
|
531
|
+
[pi]`, re-ran `migrate --from claude-code` idempotently against already-
|
|
532
|
+
migrated canonical, and left claude-code, codex, and kiro completely
|
|
533
|
+
untouched — including confirming codex's and kiro's instructions files
|
|
534
|
+
are still real symlinks into the developer's own `~/.ai-config`, not
|
|
535
|
+
touched by this run. Surfaced one genuine, undocumented boundary in the
|
|
536
|
+
process: `sync`/`mcp sync` still reported pi as "not installed"
|
|
537
|
+
immediately after the install, because pi's own presence probe
|
|
538
|
+
(`src/probes/pi.ts`) checks for `~/.pi/agent/settings.json` or `~/.pi/
|
|
539
|
+
agent/skills` on disk — neither of which `npm install` creates. pi only
|
|
540
|
+
writes those itself on its own first real invocation (confirmed by
|
|
541
|
+
reading its installed source: `pi list` bootstraps `~/.pi/auth.json` and
|
|
542
|
+
`~/.pi/models-store.json`, but not the `agent/` subdirectory — that
|
|
543
|
+
appears to need pi's own first-time-setup flow, which is interactive and
|
|
544
|
+
out of scope for Trellis to force). This isn't a bug to patch around —
|
|
545
|
+
faking presence would violate this project's own "verify, don't assume"
|
|
546
|
+
principle — but it is a real onboarding-order gap worth documenting: run
|
|
547
|
+
the newly-installed agent once yourself before `trellis sync` can do
|
|
548
|
+
anything for it.
|
|
549
|
+
|
|
550
|
+
**`trellis-backup-rollback`, done and archived**
|
|
551
|
+
(`openspec/changes/archive/2026-09-13-trellis-backup-rollback/`; adds
|
|
552
|
+
`backup-and-rollback`, modifies `skill-instructions-sync`,
|
|
553
|
+
`mcp-server-sync`, `onboarding-flow`). Direct follow-up to
|
|
554
|
+
`trellis-managed-agents`' own real regression (a foreign symlink silently
|
|
555
|
+
repointed with zero warning): that fix made the *known* unsafe case a
|
|
556
|
+
`conflict` instead, but `mcp sync`'s native-config writes were never
|
|
557
|
+
provably safe the same way — `~/.claude.json`, `~/.codex/config.toml`,
|
|
558
|
+
and Kiro's two settings files are read, merged or TOML-section-patched,
|
|
559
|
+
and rewritten in place, and a bug in that merge/patch logic produces a
|
|
560
|
+
clean write with silently wrong output, not a `conflict` any existing
|
|
561
|
+
check would catch. Two earlier archived changes (`trellis-sync-p1`,
|
|
562
|
+
`trellis-mcp-sync-p2`) each waved at "rollback" by pointing at their own
|
|
563
|
+
create/remove mechanics; `mcp-sync-p2`'s story didn't actually hold —
|
|
564
|
+
automatic MCP server removal still isn't built, so there was no real
|
|
565
|
+
undo path for an `mcp sync` mistake besides hand-editing the file.
|
|
566
|
+
|
|
567
|
+
Every real write `sync`/`mcp sync` perform is now recorded, before it
|
|
568
|
+
happens, into a structured, timestamped run under `~/.trellis/backups/`
|
|
569
|
+
(new `src/lib/backup.ts`) — enough per operation to invert it exactly:
|
|
570
|
+
a file's prior bytes for an overwrite, a symlink's prior target for a
|
|
571
|
+
repair or removal, or just "this didn't exist before" for a create. The
|
|
572
|
+
write itself moved *into* the backup session (`session.writeFile`/
|
|
573
|
+
`createSymlink`/`repairSymlink`/`removeSymlink`) rather than the session
|
|
574
|
+
being an optional thing call sites remember to also invoke — the same
|
|
575
|
+
lesson `trellis-managed-agents`' symlinkPlan bug taught: a safety check
|
|
576
|
+
that's opt-in gets skipped eventually. `TrellisAdapter.apply()` gained a
|
|
577
|
+
mandatory `BackupSession` parameter across all four adapters; there is no
|
|
578
|
+
code path left that writes one of these files without going through it.
|
|
579
|
+
|
|
580
|
+
New `trellis rollback [<run-id>] [--list] [--dry-run] [--json]`: restores
|
|
581
|
+
one recorded run, but only where the current on-disk state still matches
|
|
582
|
+
what that run itself left behind — a path touched again since (another
|
|
583
|
+
sync, a hand edit) is a `conflict`, reported and left untouched, same
|
|
584
|
+
"verify, never guess" posture every other conflict in this project
|
|
585
|
+
already holds itself to. One path's conflict never blocks any other path
|
|
586
|
+
in the same rollback. `onboard` opens one session and shares it across
|
|
587
|
+
its whole chained `sync`+`mcp sync` run rather than one per stage, so a
|
|
588
|
+
single `trellis rollback` undoes an entire `onboard` invocation.
|
|
589
|
+
`migrate` is explicitly out of scope — it only ever creates a new
|
|
590
|
+
canonical entry or refuses on conflict, never overwrites existing
|
|
591
|
+
canonical content, so there's nothing real to lose there.
|
|
592
|
+
|
|
593
|
+
Verified in the real Docker sandbox against `test/fixtures/home`: a real
|
|
594
|
+
`mcp sync` run rewrote `.claude.json`/`.codex/config.toml`/
|
|
595
|
+
`.kiro/settings/mcp.json` with several new servers, `trellis rollback`
|
|
596
|
+
restored all three to their exact original bytes, confirmed byte-for-
|
|
597
|
+
byte; then a second run, followed by a hand-edit simulating something
|
|
598
|
+
else touching `.claude.json` after the fact, confirmed rollback reports
|
|
599
|
+
exactly that one path as a `conflict` (exit 1) while still correctly
|
|
600
|
+
restoring the other two, untouched, unaffected paths in the same
|
|
601
|
+
invocation.
|
|
602
|
+
|
|
603
|
+
**`trellis-mcp-static-env-and-disabled-servers`, implemented and
|
|
604
|
+
sandbox-verified** (modifies `mcp-server-sync`). Found by dogfooding
|
|
605
|
+
`mcp sync` against a real machine's actual, actively-used Codex
|
|
606
|
+
`config.toml` rather than a fixture: `[mcp_servers.supabase_db]` had
|
|
607
|
+
`enabled = false` (a definition kept on hand, deliberately off — no way
|
|
608
|
+
to represent that in canonical short of deleting it), and
|
|
609
|
+
`[mcp_servers.tanka]` used a hardcoded literal `env` table, not the
|
|
610
|
+
`env_vars` name-forwarding array every other real server on the same
|
|
611
|
+
machine uses. Migrating `tanka` as-is into the old names-only model
|
|
612
|
+
would have had `mcp sync` rewrite a working config into a broken one —
|
|
613
|
+
`TANKA_EMAIL`/`TANKA_ENV` were never real process env vars, just plain
|
|
614
|
+
values written straight into the file — and neither `mcp sync` nor
|
|
615
|
+
`secrets audit` would have caught it before the write happened.
|
|
616
|
+
|
|
617
|
+
`McpServerDef` gained `enabled?: boolean` (filtered in `resolveMcpPlan`
|
|
618
|
+
itself, the one choke point every adapter already funnels through — a
|
|
619
|
+
disabled server gets no write and no conflict, not just on Codex but on
|
|
620
|
+
every agent) and `staticEnv?: Record<string, string>` for a value that
|
|
621
|
+
was never a secret in the first place — Codex renders it as an adjacent
|
|
622
|
+
`[mcp_servers.<name>.env]` table (extending `tomlSection.ts`'s section
|
|
623
|
+
boundary logic to treat the pair as one atomic create/repair/remove
|
|
624
|
+
unit), Claude Code/Kiro merge it into the same `env` map their `${VAR}`
|
|
625
|
+
references already use. `resolveMcpPlan` also gained a mandatory fourth
|
|
626
|
+
`policy: SecretsPolicy` parameter: before writing any name-only `env`
|
|
627
|
+
entry, it now resolves it through the same `resolveSecretEnv` `secrets
|
|
628
|
+
audit`/the pi bridge already call, refusing (as a new conflict scoped to
|
|
629
|
+
just that server, on just that agent) a name that wouldn't actually
|
|
630
|
+
resolve — the exact silent-breakage scenario `tanka` would have hit.
|
|
631
|
+
|
|
632
|
+
The on-disk YAML key is `static_env` (snake_case, matching every other
|
|
633
|
+
multi-word key across `.trellis/*.yaml` — `known_host_injected`,
|
|
634
|
+
`allowed_vars` — translated to camelCase in `loadServersYaml`), a real
|
|
635
|
+
gap the design doc missed until implementation: `McpServerDef` had
|
|
636
|
+
always been parsed as-is with zero field translation, since every prior
|
|
637
|
+
field name happened to already be a single word.
|
|
638
|
+
|
|
639
|
+
Sandbox-verified, not just unit-tested: reproducing this machine's exact
|
|
640
|
+
`tanka`/`supabase-db` shapes in `test/fixtures/home` and running a real
|
|
641
|
+
`mcp sync` inside `scripts/sandbox.sh` produced Codex's two-table output
|
|
642
|
+
byte-for-byte identical to the real, working config this change was
|
|
643
|
+
motivated by — and surfaced one more real regression before it could
|
|
644
|
+
ship: the sandbox's own pre-existing fixture servers declare `env` names
|
|
645
|
+
that don't resolve inside the container, which the new pre-write check
|
|
646
|
+
would have refused outright. Fixed by exporting their fixture values in
|
|
647
|
+
`docker/entrypoint.sh`, matching what a real working setup would
|
|
648
|
+
actually have — found only because the fixture was actually run, not
|
|
649
|
+
just reasoned about.
|
|
650
|
+
|
|
651
|
+
Also removed a dead field from `schema/servers.example.yaml`: the
|
|
652
|
+
`figma` example's `auth: oauth` was never a real property on
|
|
653
|
+
`McpServerDef`, never read by any adapter, and never asserted by any
|
|
654
|
+
spec — the YAML parser has no field validation, so it silently did
|
|
655
|
+
nothing. No real OAuth support exists; the line taught a capability that
|
|
656
|
+
was never there.
|
|
657
|
+
|
|
658
|
+
**P11 is done and archived**
|
|
659
|
+
(`openspec/changes/archive/2026-09-13-trellis-migrate-category-selection/`;
|
|
660
|
+
modifies `canonical-source-migration` and `onboarding-flow`). `trellis
|
|
661
|
+
migrate --from <agent>` always planned skills and instructions
|
|
662
|
+
together, one unit, no subset selection — no CLI flag, and no
|
|
663
|
+
interactive picker for this choice either. `--only skills|instructions`
|
|
664
|
+
now restricts a run to just one category, filtered inside
|
|
665
|
+
`collectMigratePlan` itself (not computed then discarded) — the
|
|
666
|
+
excluded kind is never read for comparison and never appears in the
|
|
667
|
+
plan. `trellis onboard`'s migrate step gained a checkbox reusing
|
|
668
|
+
`src/lib/terminalPicker.ts` (the same module `trellis-onboard-
|
|
669
|
+
interactive-picker` built), but only offered when the resolved source
|
|
670
|
+
actually has both real skills and real instructions — a source with
|
|
671
|
+
only one real kind, or `--json`, or a terminal that can't support the
|
|
672
|
+
picker all default silently to migrating whichever kind(s) actually
|
|
673
|
+
have content, with no second, numbered-text fallback UI built, since
|
|
674
|
+
this choice never existed before this change to have a fallback for. An
|
|
675
|
+
empty selection is a valid, distinct outcome ("migrate skipped — no
|
|
676
|
+
categories selected"), not an error, and doesn't stop sync/mcp sync/
|
|
677
|
+
secrets audit from running. A real gap named rather than silently
|
|
678
|
+
folded in: `codex`/`pi`/`kiro` as migration sources remain untested —
|
|
679
|
+
`test/unit/migrate.test.ts` only ever exercises `claude-code`, despite
|
|
680
|
+
`collectMigratePlan`'s own dispatch being fully symmetric by design.
|
|
681
|
+
That's P13's job. Verified with 22 new unit tests (275/275 project-wide,
|
|
682
|
+
zero regressions) — no sandbox pass needed, this change touches no
|
|
683
|
+
adapter or native-config write path.
|
|
684
|
+
|
|
685
|
+
**P12 (✅ done, archived
|
|
686
|
+
[2026-09-13-trellis-canonical-cli-crud](../openspec/changes/archive/2026-09-13-trellis-canonical-cli-crud/)):
|
|
687
|
+
canonical CRUD via CLI.** Neither skills nor MCP servers had any
|
|
688
|
+
command-line add/remove/list surface — `cli.ts`'s `mcp` command
|
|
689
|
+
recognized exactly one subcommand, `sync`, and there was no `skill`
|
|
690
|
+
command at all. `trellis skill list/add/remove` and `trellis mcp
|
|
691
|
+
list/add/remove` are new, canonical-side-only commands (a new
|
|
692
|
+
`canonical-content-management` capability): `skill add`/`mcp add`
|
|
693
|
+
refuse (no write, no `--force`) on an existing name with different
|
|
694
|
+
content, mirroring `migrate`'s own conflict posture exactly, reusing
|
|
695
|
+
the same comparison logic via a newly-extracted
|
|
696
|
+
`decideDirImport(sourceDir, canonicalDir)` (src/lib/dirEquals.ts),
|
|
697
|
+
which also replaced `migrate.ts`'s own inline check
|
|
698
|
+
(behavior-preserving, confirmed by its full pre-existing suite passing
|
|
699
|
+
unmodified). `servers.yaml`'s new writer (`upsertServerYaml`/
|
|
700
|
+
`removeServerYaml` in `canonical.ts`) uses the `yaml` package's
|
|
701
|
+
`Document`-based `parseDocument`/`setIn`/`deleteIn`/`toString`, never a
|
|
702
|
+
full parse-then-restringify, specifically so a hand-authored file's
|
|
703
|
+
comments and untouched entries survive byte-for-byte — verified with a
|
|
704
|
+
dedicated test asserting exactly that. `skill remove` needed zero new
|
|
705
|
+
removal-propagation code: skills already carry an ownership marker (the
|
|
706
|
+
symlink itself), so `sync`'s pre-existing stale-symlink detection
|
|
707
|
+
un-syncs a removed skill automatically on the next run (proven
|
|
708
|
+
end-to-end in a test). `mcp remove` stays canonical-only by design —
|
|
709
|
+
MCP has no such marker yet, so an already-synced agent's native config
|
|
710
|
+
is untouched until P14 closes that gap. `mcp list` never resolves a
|
|
711
|
+
secret: `env` entries print as bare names (never read from
|
|
712
|
+
`process.env`), while `static_env` values print in full since they were
|
|
713
|
+
never secrets by `McpServerDef`'s own contract. Verified with 21 new
|
|
714
|
+
unit tests (296/296 project-wide, zero regressions) plus manual
|
|
715
|
+
smoke-testing of every subcommand (list/add/remove, conflict/
|
|
716
|
+
already-present/invalid-input, `--dry-run`, `--json`) against a
|
|
717
|
+
throwaway sandbox `$HOME` — no sandbox-container pass needed, this
|
|
718
|
+
change touches no adapter or native-config write path.
|
|
719
|
+
|
|
720
|
+
**P13 (✅ done, archived
|
|
721
|
+
[2026-09-13-trellis-real-sandbox-verification](../openspec/changes/archive/2026-09-13-trellis-real-sandbox-verification/)):
|
|
722
|
+
sandbox verification against this machine's real state.** Every sandbox
|
|
723
|
+
run before this (`scripts/sandbox.sh`, `docker/entrypoint.sh`) mounted
|
|
724
|
+
the same single, git-tracked synthetic fixture (`test/fixtures/home`).
|
|
725
|
+
Compounding this, `test/unit/migrate.test.ts` only ever exercised
|
|
726
|
+
`"claude-code"` as a migration source — codex/pi/kiro had never been
|
|
727
|
+
verified as sources even though `collectMigratePlan`'s `PROBES:
|
|
728
|
+
Record<AgentId, ...>` dispatch is fully symmetric by design (claude-code
|
|
729
|
+
was, however, already covered as a sync/mcp-sync *target*, in
|
|
730
|
+
`test/unit/sync.test.ts`/`test/unit/mcp.test.ts` — the earlier draft of
|
|
731
|
+
this entry claimed otherwise; corrected here after checking, not
|
|
732
|
+
assumed). `scripts/sandbox.sh --real` builds a throwaway snapshot from a
|
|
733
|
+
real `$HOME` using an **allowlist**, not the denylist first sketched here
|
|
734
|
+
— every path each probe (`src/probes/*.ts`) is already confirmed to
|
|
735
|
+
read, and nothing else, since this project has no complete knowledge of
|
|
736
|
+
where third-party agents' own real OAuth token flows store credentials
|
|
737
|
+
(design.md D1 explains the reasoning). Actually running this against a
|
|
738
|
+
real, in-use machine (not just reasoning about it) found two real bugs
|
|
739
|
+
before it could even complete: `sync`'s own real output is a symlink
|
|
740
|
+
back into `~/.trellis/`, which a naive symlink-preserving copy leaves
|
|
741
|
+
dangling once mounted into a container with no such path — fixed by
|
|
742
|
+
dereferencing during copy; and a case-insensitive filesystem (macOS
|
|
743
|
+
default) collides two of pi's own case-sensitive instructions-file
|
|
744
|
+
candidates (`AGENTS.md`/`AGENTS.MD`), which needed a
|
|
745
|
+
dest-already-exists guard to avoid a crash. Before any Docker build, the
|
|
746
|
+
snapshot is gated through `trellis secrets audit`'s own, already-shipped
|
|
747
|
+
`homeDir` seam — on the actual real-machine run, this correctly found 5
|
|
748
|
+
genuine `unexpected-var-name` findings and refused to proceed, exactly
|
|
749
|
+
as designed; clearing that machine's own `secrets.policy.yaml` gap and
|
|
750
|
+
running a full container pass against it remains a separate,
|
|
751
|
+
human-initiated action, not something this change forced through.
|
|
752
|
+
codex/kiro/pi as migrate sources are now covered in
|
|
753
|
+
`test/unit/migrateSources.test.ts`, using each probe's own confirmed
|
|
754
|
+
real dotfile paths. Verified with 10 new unit tests (306/306
|
|
755
|
+
project-wide) plus the fixture-based (default, non-`--real`)
|
|
756
|
+
`scripts/sandbox.sh` re-run end-to-end against Docker to confirm zero
|
|
757
|
+
regression to the existing path.
|
|
758
|
+
|
|
759
|
+
**P14 (✅ done — removal half only, archived
|
|
760
|
+
[2026-09-13-trellis-mcp-sync-removal](../openspec/changes/archive/2026-09-13-trellis-mcp-sync-removal/);
|
|
761
|
+
migrate-in remains open, see below): MCP server lifecycle parity with
|
|
762
|
+
skills.** Skills have a full migrate (import) + sync (create/repair) +
|
|
763
|
+
conflict story; MCP had sync only. This was always two distinct, real
|
|
764
|
+
gaps, not one — this change closed the harder, more clearly-specified
|
|
765
|
+
half: `resolveMcpPlan` was deliberately create/repair-only
|
|
766
|
+
(`mcpPlan.ts`'s own stated reasoning: a bare TOML/JSON key has no
|
|
767
|
+
ownership marker to prove Trellis, not the user, put it there), so
|
|
768
|
+
removing a server from `servers.yaml` never removed it from any
|
|
769
|
+
agent's native config. A new `src/lib/mcpOwnership.ts` ledger
|
|
770
|
+
(`~/.trellis/mcp/ownership.json`) records, per agent and server name,
|
|
771
|
+
the exact rendered value Trellis itself last wrote; on a later sync, a
|
|
772
|
+
name gone from canonical is only actually removed from an agent's
|
|
773
|
+
native config if that config's current entry still exactly matches
|
|
774
|
+
what the ledger recorded — a hand-edited entry is left alone,
|
|
775
|
+
indefinitely, never forced. Reused rather than rebuilt:
|
|
776
|
+
`src/lib/tomlSection.ts`'s `removeSection` (built earlier for the
|
|
777
|
+
static-env atomic-range work, never wired into an actual removal path
|
|
778
|
+
until now) for Codex, and each JSON agent's own existing `deepEqual`
|
|
779
|
+
for the "unchanged since" check. **Still genuinely open, not done
|
|
780
|
+
here:** MCP *migrate-in* (importing an already-hand-configured server
|
|
781
|
+
into canonical) — each static-config probe (`codex.ts`/`claude-code.ts`/
|
|
782
|
+
`kiro.ts`) still discards a server's complete real definition (e.g.
|
|
783
|
+
`codex mcp list --json`'s `CodexMcpEntry`, with `command`/`args`/
|
|
784
|
+
`env_vars` intact) down to `{name, transport, probe}`
|
|
785
|
+
(`AgentSnapshotMcpServer`) before anything downstream sees it, and
|
|
786
|
+
there is still no conversion from that discarded, richer shape into
|
|
787
|
+
canonical's `McpServerDef`. pi has no static config to read at all —
|
|
788
|
+
inherently import-less; P12's CLI remains its only route in. A real
|
|
789
|
+
gap, named rather than silently dropped, left for a future change.
|
|
790
|
+
Verified with 3 new/rewritten unit tests across both Claude Code's JSON
|
|
791
|
+
path and Codex's TOML path (308/308 project-wide, zero regressions).
|
|
792
|
+
|
|
793
|
+
**P15 (✅ done — ingestion half only, archived
|
|
794
|
+
[2026-09-13-trellis-memory-sync](../openspec/changes/archive/2026-09-13-trellis-memory-sync/);
|
|
795
|
+
per-agent extraction remains open, see below): shared memory — real
|
|
796
|
+
ingestion and per-agent extraction.** `memories` was parsed
|
|
797
|
+
(`canonical.ts`) into `CanonicalSource.memories: MemoryEntry[]` and
|
|
798
|
+
consumed nowhere — no adapter, no command, no `trellis memory` CLI
|
|
799
|
+
surface existed at all. P6 explicitly left "auto-ingesting
|
|
800
|
+
`~/.trellis/memories/*.md` content into the running memory server's
|
|
801
|
+
store" out of scope; `trellis memory sync` closes that gap (A): each
|
|
802
|
+
canonical memory file becomes one entity
|
|
803
|
+
(`entityType: "trellis-memory"`) in `@modelcontextprotocol/
|
|
804
|
+
server-memory`'s own on-disk JSON-lines graph file (the exact file that
|
|
805
|
+
server itself reads at startup — Trellis never spawns or talks to a
|
|
806
|
+
running server process, a plain file write like everything else this
|
|
807
|
+
project does), requiring `mcp/servers.yaml`'s `memory` server to set
|
|
808
|
+
`static_env.MEMORY_FILE_PATH` explicitly (the server's own unset-env
|
|
809
|
+
default resolves relative to wherever `npx` cached the package, not a
|
|
810
|
+
predictable location). The in-band `entityType` tag is the ownership
|
|
811
|
+
marker — deliberately not a separate ledger file like P14's MCP
|
|
812
|
+
removal, since (unlike per-agent MCP config) every agent connected to
|
|
813
|
+
this one server shares the exact same graph, so there's no per-agent
|
|
814
|
+
render to track. Every entity/relation Trellis didn't create is left
|
|
815
|
+
completely untouched, unconditionally; a name collision with a
|
|
816
|
+
non-Trellis-tagged entity is a conflict, never overwritten. **Still
|
|
817
|
+
genuinely open, not done here (B):** extracting an agent's own
|
|
818
|
+
already-accumulated memory content back into canonical — e.g. Claude
|
|
819
|
+
Code's own per-project memory feature. Investigated, not attempted, for
|
|
820
|
+
two concrete reasons: that content lives under
|
|
821
|
+
`~/.claude/projects/<project-slug>/memory/`, a path
|
|
822
|
+
`trellis-real-sandbox-verification`'s own allowlist already deliberately
|
|
823
|
+
excludes (mixed with real session transcripts); and the project-slug
|
|
824
|
+
encoding scheme Claude Code uses to derive that path from a working
|
|
825
|
+
directory has no authoritative documented source this project could
|
|
826
|
+
verify against, so it was not guessed at (this project's own "verify,
|
|
827
|
+
don't assume" discipline). Kiro's `totalrecallai` (SQLite + local vector
|
|
828
|
+
embeddings) is a further, genuinely different data shape, and was never
|
|
829
|
+
in scope for this half either. A real gap, named rather than silently
|
|
830
|
+
dropped, left for a future change. Verified with 14 new unit tests
|
|
831
|
+
(322/322 project-wide, zero regressions).
|
|
832
|
+
|
|
833
|
+
**P16 (✅ done, archived
|
|
834
|
+
[2026-09-13-trellis-migrate-mcp-servers](../openspec/changes/archive/2026-09-13-trellis-migrate-mcp-servers/)):
|
|
835
|
+
MCP migrate-in — the gap P14 named.** `trellis migrate --from <agent>`
|
|
836
|
+
gains a third `--only` value, `mcp`: claude-code, kiro, and codex each
|
|
837
|
+
get a new, purpose-built reader (`src/lib/mcpMigrateRead.ts`) that
|
|
838
|
+
converts that agent's real, already-configured MCP servers into
|
|
839
|
+
canonical's `McpServerDef` shape — kept entirely separate from each
|
|
840
|
+
probe's own thin `AgentSnapshotMcpServer` (`doctor`'s read path, left
|
|
841
|
+
untouched). Same conflict posture as skill/instructions migration
|
|
842
|
+
throughout: identical is a no-op, a differing definition under the same
|
|
843
|
+
name is a conflict, never overwritten. pi is still never a migrate-in
|
|
844
|
+
source — no static config to read, unchanged from P14's own framing.
|
|
845
|
+
Two fidelity limits, found by actually running each agent's real
|
|
846
|
+
tooling rather than assumed, are handled by refusing rather than
|
|
847
|
+
guessing: **Codex is stdio-transport only at first** (closed as a
|
|
848
|
+
same-day follow-up, see below) — `codex mcp list --json` had no
|
|
849
|
+
evidence in this codebase for any other transport shape at design time;
|
|
850
|
+
a real, one-off run against a locally-installed `codex-cli 0.154.0`
|
|
851
|
+
during implementation *did* observe a `streamable_http` shape (`url`,
|
|
852
|
+
`bearer_token_env_var`, plus three further undocumented fields) for a
|
|
853
|
+
hand-written `url`-based server — recorded as a concrete lead for a
|
|
854
|
+
future change, not built at first, since one data point from one
|
|
855
|
+
version isn't a contract; a non-stdio Codex server was reported
|
|
856
|
+
`skip-unsupported`, named, not silently dropped. **Codex's `static_env`**
|
|
857
|
+
is recovered by reading `[mcp_servers.<name>.env]` directly from
|
|
858
|
+
`config.toml` (a new `readServerEnvTable` in `tomlSection.ts`, the exact
|
|
859
|
+
inverse of that module's own existing writer) since `codex mcp list
|
|
860
|
+
--json` only ever reports variable *names*, never the literal table
|
|
861
|
+
Trellis itself renders separately. **`headers`** — missing from both
|
|
862
|
+
`claude-code.ts`'s and `kiro.ts`'s own probe-facing JSON types despite
|
|
863
|
+
being a real field Trellis's own writer (`jsonMcp.ts`) already produces
|
|
864
|
+
for http/sse servers — is recovered via a parallel, richer local type
|
|
865
|
+
scoped only to this new reader module, leaving both probes' existing,
|
|
866
|
+
`doctor`-tested types untouched. Verified with 19 new unit tests,
|
|
867
|
+
including one exercising the real, locally-installed `codex` binary
|
|
868
|
+
end-to-end against a scratch, HOME-scoped `.codex/config.toml` (341/341
|
|
869
|
+
project-wide, zero regressions). One related, pre-existing gap
|
|
870
|
+
surfaced but deliberately not fixed at first (out of scope for this
|
|
871
|
+
change, named instead): `src/probes/codex.ts`'s own `probe()` never
|
|
872
|
+
scoped its `codex mcp list --json` subprocess call to a passed-in
|
|
873
|
+
`homeDir` via `HOME` env override the way this change's own new reader
|
|
874
|
+
did — meaning that probe's MCP listing always reflected the real
|
|
875
|
+
machine's real codex config regardless of what `homeDir` a caller passed
|
|
876
|
+
it, an inconsistency with the rest of `probe()`'s own home-scoped reads.
|
|
877
|
+
|
|
878
|
+
**Same-day follow-up, both of the above closed (not a separately
|
|
879
|
+
numbered phase — direct fixes to already-shipped, already-specced
|
|
880
|
+
behavior, not new requirements, so no new OpenSpec change):**
|
|
881
|
+
`src/probes/codex.ts`'s `probe()` now scopes both of its `execFileSync`
|
|
882
|
+
calls to the passed-in `homeDir` the same way — verified with a new
|
|
883
|
+
regression test (`test/unit/codexProbe.test.ts`) using two distinct
|
|
884
|
+
scratch homes with two distinct MCP servers, confirming neither leaks
|
|
885
|
+
into the other; confirmed the test actually catches the bug by reverting
|
|
886
|
+
the fix and watching it fail against this real machine's real MCP
|
|
887
|
+
servers before restoring it. Separately, `buildCodexMcpReadResult`
|
|
888
|
+
(`src/lib/mcpMigrateRead.ts`) now converts a non-stdio Codex server when
|
|
889
|
+
it uses only `url` and, optionally, `bearer_token_env_var` — verified
|
|
890
|
+
this exact shape against the real `codex` binary a second time
|
|
891
|
+
(including with a real `bearer_token_env_var` set, not just a bare
|
|
892
|
+
`url`) — while a server using any of the three still-unexplained header
|
|
893
|
+
fields (`http_headers`/`env_http_headers`/`http_headers_helper`, all
|
|
894
|
+
`null` when unused, confirmed by the same real run) still refuses rather
|
|
895
|
+
than guesses. A migrated remote Codex server is always labeled `http`,
|
|
896
|
+
never `sse` — not a guess: Codex's own `config.toml` schema has no field
|
|
897
|
+
distinguishing the two (`upsertSection`'s own codex TOML rendering is
|
|
898
|
+
byte-identical for both transports, now covered by its own test), so
|
|
899
|
+
that distinction was never stored in the first place. 350/350 tests
|
|
900
|
+
passing.
|
|
901
|
+
|
|
470
902
|
| Phase | Deliverable | Depends on |
|
|
471
903
|
|---|---|---|
|
|
472
904
|
| P0 | ✅ `trellis doctor` — read-only, opt-in-for-handshakes scan of all four agents' current skills/MCP/instructions state, reports drift and duplicates | nothing |
|
|
@@ -480,5 +912,17 @@ thing in this project that exercises the actual `bin` symlink at all.
|
|
|
480
912
|
| P8 | ✅ Kiro `${VAR}` fix: adapter also manages `kiroAgent.mcpApprovedEnvVars`, without which Kiro silently never substitutes any env reference Trellis writes | P2 |
|
|
481
913
|
| 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
914
|
| P10 | GUI: evaluate embedding into mcp-router's or skills-hub's existing interface before building anything new | P3–P9 |
|
|
915
|
+
| P11 | ✅ Migrate category selection: `--only skills\|instructions` on `migrate`, same interactive picker `onboard` already uses | P1 |
|
|
916
|
+
| P12 | ✅ Canonical CRUD via CLI: `trellis skill add/remove/list`, `trellis mcp add/remove/list` | P1, P2 |
|
|
917
|
+
| P13 | ✅ Sandbox verification against this machine's real, structurally-relevant state (not just fixtures); every agent verified as both migrate source and sync/mcp-sync target | P0–P2 |
|
|
918
|
+
| P14 | ✅ MCP server lifecycle parity with skills: ownership-tracked safe removal on sync (migrate-in closed separately by P16) | P2, P13 |
|
|
919
|
+
| P15 | ✅ Shared memory: ingest canonical `memories/*.md` into the `server-memory` store's own graph file (per-agent extraction into canonical remains a separate, open gap) | P6 |
|
|
920
|
+
| P16 | ✅ MCP migrate-in: `trellis migrate --only mcp` for claude-code/kiro/codex (codex remote transport + probe HOME-scoping closed same-day, see prose above), closing the gap P14 named | P12, P14 |
|
|
483
921
|
|
|
484
922
|
No dates. This is scoped by verification milestones, not calendar time.
|
|
923
|
+
Execution order for P11–P16: P11 → P12 → P13 → P14 → P15 → P16 — CRUD
|
|
924
|
+
(P12) lands before the larger features (P14, P15, P16) so each can be
|
|
925
|
+
adjusted via command line instead of hand-edited files while it's being
|
|
926
|
+
built, and the realistic sandbox (P13) lands before all three so each
|
|
927
|
+
gets verified against real data once, immediately, instead of against
|
|
928
|
+
another synthetic fixture.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-trellis",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
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
5
|
"license": "MIT",
|
|
6
6
|
"author": "Paul Leo",
|
|
@@ -5,6 +5,16 @@
|
|
|
5
5
|
# - Real values come from whatever already populates process env
|
|
6
6
|
# (a plain `.env`-style file, 1Password's `op run`, etc.) — Trellis
|
|
7
7
|
# does not store or transport secret values itself.
|
|
8
|
+
# - `static_env` is the one deliberate exception: values written into
|
|
9
|
+
# the agent's config verbatim, for something that was never a secret
|
|
10
|
+
# in the first place (an email address, an environment tag) — still
|
|
11
|
+
# scanned against `reject_patterns` like everything else, so a real
|
|
12
|
+
# credential accidentally placed there is still refused, not silently
|
|
13
|
+
# written (trellis-mcp-static-env-and-disabled-servers).
|
|
14
|
+
# - `enabled: false` keeps a server's definition in canonical without
|
|
15
|
+
# writing it to any agent — for a server you want to keep configured
|
|
16
|
+
# but not currently active everywhere, matching how a real agent's
|
|
17
|
+
# own config often already represents "off, not deleted".
|
|
8
18
|
# - A server name that a host environment is known to inject at runtime
|
|
9
19
|
# (see `known_host_injected` below) must not be redefined here — on
|
|
10
20
|
# Codex this causes the entire process to fail to start, not just that
|
|
@@ -35,15 +45,32 @@ servers:
|
|
|
35
45
|
# mem0/OpenMemory or a totalrecallai-class semantic-search server are
|
|
36
46
|
# documented opt-in upgrades from this default, not silent defaults —
|
|
37
47
|
# same section of docs/research.md.
|
|
48
|
+
#
|
|
49
|
+
# static_env.MEMORY_FILE_PATH: without this, the server's own unset-env
|
|
50
|
+
# default resolves relative to wherever `npx` happened to cache the
|
|
51
|
+
# package — not a stable location. Setting it explicitly to a path
|
|
52
|
+
# under ~/.trellis/ is also what lets `trellis memory sync`
|
|
53
|
+
# (trellis-memory-sync) find and write the same file the running
|
|
54
|
+
# server itself reads at startup.
|
|
38
55
|
# memory:
|
|
39
56
|
# transport: stdio
|
|
40
57
|
# command: npx
|
|
41
58
|
# args: ["-y", "@modelcontextprotocol/server-memory"]
|
|
59
|
+
# static_env:
|
|
60
|
+
# MEMORY_FILE_PATH: "~/.trellis/memories/graph.jsonl"
|
|
42
61
|
|
|
62
|
+
# `static_env`: a value that was never a secret in the first place (an
|
|
63
|
+
# email address, an environment tag) — written verbatim, never treated
|
|
64
|
+
# as a name to resolve. Prefer `env` (names only) whenever a value
|
|
65
|
+
# actually is a secret; `static_env` is the deliberate, visibly-different
|
|
66
|
+
# exception, still scanned against `reject_patterns` like any other
|
|
67
|
+
# literal field.
|
|
43
68
|
tanka:
|
|
44
69
|
transport: stdio
|
|
45
70
|
command: tanka-mcp
|
|
46
|
-
|
|
71
|
+
static_env:
|
|
72
|
+
TANKA_EMAIL: "you@example.com"
|
|
73
|
+
TANKA_ENV: "sd-or"
|
|
47
74
|
|
|
48
75
|
gitlab:
|
|
49
76
|
transport: stdio
|
|
@@ -51,10 +78,20 @@ servers:
|
|
|
51
78
|
args: ["-y", "@zereight/mcp-gitlab"]
|
|
52
79
|
env: [GITLAB_PERSONAL_ACCESS_TOKEN, GITLAB_API_URL]
|
|
53
80
|
|
|
81
|
+
# `enabled: false` keeps this definition in canonical without writing it
|
|
82
|
+
# to any agent — for something you want configured but not currently
|
|
83
|
+
# active anywhere. Remove the line (or set it to `true`) to turn it back
|
|
84
|
+
# on everywhere at once, with nothing to redefine.
|
|
85
|
+
supabase-db:
|
|
86
|
+
transport: stdio
|
|
87
|
+
command: npx
|
|
88
|
+
args: ["-y", "@supabase/mcp-server-supabase"]
|
|
89
|
+
env: [SUPABASE_ACCESS_TOKEN]
|
|
90
|
+
enabled: false
|
|
91
|
+
|
|
54
92
|
figma:
|
|
55
93
|
transport: http
|
|
56
94
|
url: "https://mcp.figma.com/mcp"
|
|
57
|
-
auth: oauth
|
|
58
95
|
|
|
59
96
|
# Example of a private/agent-specific server: only Claude Code has a use
|
|
60
97
|
# for this one, so the other three agents never see it defined at all.
|