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
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Paul Leo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,127 @@
1
+ # Trellis
2
+
3
+ **A single source of capability for every coding agent you run.**
4
+
5
+ Skills, MCP servers, subagent definitions, shared memory, and secret policy —
6
+ defined once, adapted natively into Claude Code, Codex, Kiro, and the [pi coding
7
+ agent](https://github.com/earendil-works/pi), without drift and without a second
8
+ copy of anything.
9
+
10
+ Trellis does not replace any of these agents' native config. It generates and
11
+ verifies each one's native adapter from one canonical source, and refuses to let
12
+ a plaintext secret or a duplicated skill file slip through.
13
+
14
+ ## Why
15
+
16
+ By late 2026 the "sync my AI agent rules across tools" space is crowded — at
17
+ least seven open source projects do it (`block/ai-rules`, `ai-rules-sync`,
18
+ `skillshare`, `skills-hub`, `skills-link`, `agent_sync`, and others). None of
19
+ them cover **Kiro** or **pi**, and none of them go past rules/skills into MCP
20
+ servers, shared memory, and secret hygiene as one coherent system. That's the
21
+ gap Trellis fills — see [`docs/research.md`](docs/research.md) for the full
22
+ landscape survey and [`docs/architecture.md`](docs/architecture.md) for what we
23
+ build versus what we deliberately reuse.
24
+
25
+ Trellis aligns with the emerging [`.agents Protocol`](https://dotagentsprotocol.com)
26
+ draft rather than inventing a sixth competing standard, and is likely its first
27
+ working implementation.
28
+
29
+ ## Install
30
+
31
+ ```
32
+ npm install -g agent-trellis
33
+ ```
34
+
35
+ ## Quick start
36
+
37
+ See [`docs/getting-started.md`](docs/getting-started.md) for the detailed
38
+ walkthrough — example output for each command, what each `migrate`/`sync`
39
+ conflict action means, and troubleshooting.
40
+
41
+ **One command:**
42
+
43
+ ```
44
+ trellis onboard
45
+ ```
46
+
47
+ Creates canonical source, detects which of Claude Code/Codex/Kiro/pi are on
48
+ this machine, picks a base agent to migrate from (auto-selected if only one
49
+ is present; prompts if more than one — pass `--agent <id>` to skip the
50
+ prompt), then runs `migrate` and `sync`. If no agent is detected, it prints
51
+ each supported agent's real install command/URL and stops — it never
52
+ installs anything itself. Add `--dry-run` to preview the whole thing with
53
+ zero writes.
54
+
55
+ **Or step by step** (what `onboard` is actually doing under the hood):
56
+
57
+ **Already using Claude Code, Codex, Kiro, or pi and want to migrate what you
58
+ already have?**
59
+
60
+ 1. `trellis init` — creates `~/.trellis/` with a minimal skeleton (only what's
61
+ missing; never overwrites a file you already have) and tells you which of
62
+ the four agents it found on this machine.
63
+ 2. `trellis migrate --from <agent>` — once per agent you already use. Copies
64
+ that agent's real skills and instructions into canonical source. Never
65
+ overwrites: an already-identical skill is reported and skipped, a genuine
66
+ conflict is reported and left for you to resolve by hand. Add `--dry-run`
67
+ to preview first.
68
+ 3. `trellis sync` — distributes canonical skills/instructions to every agent
69
+ present on this machine (including the ones you didn't migrate from).
70
+ Add `--dry-run` to preview first.
71
+ 4. `trellis mcp sync` — distributes `~/.trellis/mcp/servers.yaml` (see
72
+ [`schema/servers.example.yaml`](schema/servers.example.yaml)) to every
73
+ agent's native MCP config (create/repair only — see Known limitations).
74
+ 5. `trellis secrets audit` — fails non-zero if any agent's real config holds
75
+ a literal credential or an unexpected env var name.
76
+ 6. `trellis doctor` — read-only scan of every present agent's current state;
77
+ run any time to check for drift.
78
+
79
+ **Starting from nothing?** Skip step 2 — `trellis init`'s placeholder
80
+ `agents.md` and empty `skills/` are a fine starting point; edit them by hand.
81
+
82
+ ## Status
83
+
84
+ **Early, pre-1.0.** All seven CLI commands above (`onboard`, `init`,
85
+ `migrate`, `doctor`, `sync`, `mcp sync`, `secrets audit`) are implemented,
86
+ unit-tested, and verified end-to-end against real Docker containers (never a
87
+ developer's own dotfiles during development — see
88
+ [`docs/architecture.md`](docs/architecture.md)'s testing philosophy). See
89
+ [`docs/roadmap.md`](docs/roadmap.md) for what's shipped (P0–P6) vs. planned
90
+ (P7, a GUI). `onboard` picks one agent as the migration base when more than
91
+ one is present — merging differing content across multiple agents into one
92
+ result is named future work, not built yet.
93
+
94
+ **Known limitations, honestly stated rather than discovered the hard way:**
95
+ - MCP servers are never spawned/handshake-tested by `trellis mcp sync` or
96
+ `trellis sync` — only that the *config* is written correctly.
97
+ `trellis doctor --probe-mcp` is the one command that actually connects,
98
+ and it's opt-in.
99
+ - The pi bridge extension (`trellis-pi-mcp-bridge`) has been verified to
100
+ load and register tools without erroring, never against a real LLM tool
101
+ call in production.
102
+ - Verification has run against real Docker containers and a real,
103
+ isolated pi CLI install — not yet against a developer's actual, existing
104
+ `~/.claude`/`~/.codex`/`~/.kiro`/`~/.pi` in daily use. If you hit
105
+ something a clean-room sandbox wouldn't have caught, please open an
106
+ issue.
107
+ - Automatic removal of an MCP server is deliberately unsupported (create/
108
+ repair only) until an ownership-tracking mechanism exists — see
109
+ `docs/roadmap.md`'s P2 note.
110
+
111
+ ## Design principles
112
+
113
+ 1. **One canonical source, many adapters.** Each agent's native config file is
114
+ a generated or symlinked artifact, never hand-edited.
115
+ 2. **Reuse before building.** MCP aggregation, memory, and secret-reference
116
+ patterns already have mature open source answers — Trellis wires to them
117
+ instead of reimplementing them. See [`docs/architecture.md`](docs/architecture.md).
118
+ 3. **No plaintext secrets, ever.** Every adapter output holds variable
119
+ references only; `trellis secrets audit` scans for and rejects raw values.
120
+ 4. **Verify, don't assume.** Every claim this project makes about an agent's
121
+ behavior (file format, discovery path, MCP support) is backed by an
122
+ executed probe, not documentation-reading. Adapters that can't be verified
123
+ this way don't ship.
124
+
125
+ ## License
126
+
127
+ MIT — see [`LICENSE`](LICENSE).
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Claude Code adapter: `~/.claude/skills/<name>` symlinks per in-scope
3
+ * skill, `~/.claude/CLAUDE.md` symlinked to canonical `agents.md`. Builds
4
+ * out the create/repair/remove/refuse shape end-to-end (tasks.md 3.1)
5
+ * before the other three adapters commit to reusing `symlinkPlan.ts`.
6
+ *
7
+ * MCP servers: plain JSON parse → merge under `mcpServers` → stringify
8
+ * (trellis-mcp-sync-p2 design.md D4) — create/repair only, no automatic
9
+ * removal (D7).
10
+ */
11
+ import type { AdapterPlanItem, AdapterProbeResult, AdapterVerifyResult, TrellisAdapter } from "../core/adapter.js";
12
+ import type { CanonicalSource } from "../core/types.js";
13
+ export declare class ClaudeCodeAdapter implements TrellisAdapter {
14
+ private readonly homeDir;
15
+ readonly name = "Claude Code";
16
+ readonly id: "claude-code";
17
+ constructor(homeDir?: string);
18
+ probe(): Promise<AdapterProbeResult>;
19
+ plan(canonical: CanonicalSource): Promise<AdapterPlanItem[]>;
20
+ private planMcp;
21
+ apply(plan: AdapterPlanItem[]): Promise<void>;
22
+ verify(canonical: CanonicalSource): Promise<AdapterVerifyResult>;
23
+ }
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Claude Code adapter: `~/.claude/skills/<name>` symlinks per in-scope
3
+ * skill, `~/.claude/CLAUDE.md` symlinked to canonical `agents.md`. Builds
4
+ * out the create/repair/remove/refuse shape end-to-end (tasks.md 3.1)
5
+ * before the other three adapters commit to reusing `symlinkPlan.ts`.
6
+ *
7
+ * MCP servers: plain JSON parse → merge under `mcpServers` → stringify
8
+ * (trellis-mcp-sync-p2 design.md D4) — create/repair only, no automatic
9
+ * removal (D7).
10
+ */
11
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
12
+ import { dirname, join } from "node:path";
13
+ import { homedir } from "node:os";
14
+ import { isInScope } from "../core/adapter.js";
15
+ import * as claudeCodeProbe from "../probes/claude-code.js";
16
+ import { applySymlinkPlan, planSymlinks } from "./symlinkPlan.js";
17
+ import { applyJsonMcp, planJsonMcp } from "./jsonMcp.js";
18
+ export class ClaudeCodeAdapter {
19
+ homeDir;
20
+ name = "Claude Code";
21
+ id = "claude-code";
22
+ constructor(homeDir = homedir()) {
23
+ this.homeDir = homeDir;
24
+ }
25
+ async probe() {
26
+ const snapshot = await claudeCodeProbe.probe(this.homeDir);
27
+ return { present: snapshot.present, version: snapshot.version };
28
+ }
29
+ async plan(canonical) {
30
+ const canonicalRoot = dirname(canonical.instructionsFile);
31
+ const skillsRoot = join(this.homeDir, ".claude", "skills");
32
+ const desiredSkills = canonical.skills
33
+ .filter((skill) => isInScope(this.id, skill.scope))
34
+ .map((skill) => ({ name: skill.name, target: skill.dir }));
35
+ const skillItems = planSymlinks({
36
+ rootDir: skillsRoot,
37
+ desired: desiredSkills,
38
+ canonicalRoot: join(canonicalRoot, "skills"),
39
+ kind: "skill",
40
+ });
41
+ const instructionsItems = planSymlinks({
42
+ rootDir: join(this.homeDir, ".claude"),
43
+ desired: [{ name: "CLAUDE.md", target: canonical.instructionsFile }],
44
+ canonicalRoot,
45
+ kind: "instructions",
46
+ });
47
+ const mcpItems = this.planMcp(canonical);
48
+ return [...skillItems, ...instructionsItems, ...mcpItems];
49
+ }
50
+ planMcp(canonical) {
51
+ const configPath = join(this.homeDir, ".claude.json");
52
+ const parsed = existsSync(configPath) ? JSON.parse(readFileSync(configPath, "utf-8")) : undefined;
53
+ return planJsonMcp({ configPath, parsed, mcp: canonical.mcp, agentId: this.id });
54
+ }
55
+ async apply(plan) {
56
+ await applySymlinkPlan(plan.filter((item) => item.kind !== "mcp"));
57
+ const mcpCreates = plan.filter((item) => item.kind === "mcp" && item.action === "create" && item.mcpWrite);
58
+ if (mcpCreates.length === 0) {
59
+ return;
60
+ }
61
+ const configPath = mcpCreates[0].target;
62
+ const parsed = existsSync(configPath) ? JSON.parse(readFileSync(configPath, "utf-8")) : undefined;
63
+ const merged = applyJsonMcp(parsed, mcpCreates);
64
+ writeFileSync(configPath, `${JSON.stringify(merged, null, 2)}\n`);
65
+ }
66
+ async verify(canonical) {
67
+ const snapshot = await claudeCodeProbe.probe(this.homeDir);
68
+ if (!snapshot.present) {
69
+ return { ok: false, mismatches: ["claude-code is not present on this machine"] };
70
+ }
71
+ const mismatches = [];
72
+ const desiredNames = new Set(canonical.skills.filter((s) => isInScope(this.id, s.scope)).map((s) => s.name));
73
+ const actualSkills = new Set(snapshot.skillRoots.flatMap((root) => root.skills).map((s) => s.name));
74
+ for (const name of desiredNames) {
75
+ if (!actualSkills.has(name)) {
76
+ mismatches.push(`skill "${name}" is in canonical scope for claude-code but missing on disk`);
77
+ }
78
+ }
79
+ for (const name of actualSkills) {
80
+ if (!desiredNames.has(name)) {
81
+ mismatches.push(`skill "${name}" is present on disk but not in canonical scope for claude-code`);
82
+ }
83
+ }
84
+ return mismatches.length === 0 ? { ok: true } : { ok: false, mismatches };
85
+ }
86
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Codex adapter: `~/.agents/skills/<name>` symlinks (Codex's own built-in
3
+ * convention, per docs/research.md — never `~/.codex/skills`, that's the
4
+ * duplication bug this project already found and fixed once). Instructions
5
+ * sync targets whatever `config.toml`'s `instructions` key names; if unset,
6
+ * this is a no-op with a diagnostic — Trellis never guesses or writes a
7
+ * path Codex itself didn't declare (design.md Risks, trellis-sync-p1).
8
+ *
9
+ * MCP servers are written via `src/lib/tomlSection.ts`'s line-based
10
+ * section splicer, never a TOML library (trellis-mcp-sync-p2 design.md
11
+ * D1/D2 — both rejected with evidence) — create/repair only, no
12
+ * automatic removal (D7: no ownership marker exists for a bare TOML key
13
+ * the way a symlink's realpath provides for skills).
14
+ */
15
+ import type { AdapterPlanItem, AdapterProbeResult, AdapterVerifyResult, TrellisAdapter } from "../core/adapter.js";
16
+ import type { CanonicalSource } from "../core/types.js";
17
+ export declare class CodexAdapter implements TrellisAdapter {
18
+ private readonly homeDir;
19
+ readonly name = "Codex";
20
+ readonly id: "codex";
21
+ constructor(homeDir?: string);
22
+ probe(): Promise<AdapterProbeResult>;
23
+ plan(canonical: CanonicalSource): Promise<AdapterPlanItem[]>;
24
+ private planMcp;
25
+ apply(plan: AdapterPlanItem[]): Promise<void>;
26
+ verify(canonical: CanonicalSource): Promise<AdapterVerifyResult>;
27
+ }
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Codex adapter: `~/.agents/skills/<name>` symlinks (Codex's own built-in
3
+ * convention, per docs/research.md — never `~/.codex/skills`, that's the
4
+ * duplication bug this project already found and fixed once). Instructions
5
+ * sync targets whatever `config.toml`'s `instructions` key names; if unset,
6
+ * this is a no-op with a diagnostic — Trellis never guesses or writes a
7
+ * path Codex itself didn't declare (design.md Risks, trellis-sync-p1).
8
+ *
9
+ * MCP servers are written via `src/lib/tomlSection.ts`'s line-based
10
+ * section splicer, never a TOML library (trellis-mcp-sync-p2 design.md
11
+ * D1/D2 — both rejected with evidence) — create/repair only, no
12
+ * automatic removal (D7: no ownership marker exists for a bare TOML key
13
+ * the way a symlink's realpath provides for skills).
14
+ */
15
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
16
+ import { basename, dirname, join } from "node:path";
17
+ import { homedir } from "node:os";
18
+ import { isInScope } from "../core/adapter.js";
19
+ import * as codexProbe from "../probes/codex.js";
20
+ import { readInstructionsPath } from "../probes/codex.js";
21
+ import { applySymlinkPlan, planSymlinks } from "./symlinkPlan.js";
22
+ import { resolveMcpPlan } from "./mcpPlan.js";
23
+ import { currentServerSectionText, renderServerSection, upsertSection } from "../lib/tomlSection.js";
24
+ export class CodexAdapter {
25
+ homeDir;
26
+ name = "Codex";
27
+ id = "codex";
28
+ constructor(homeDir = homedir()) {
29
+ this.homeDir = homeDir;
30
+ }
31
+ async probe() {
32
+ const snapshot = await codexProbe.probe(this.homeDir);
33
+ return { present: snapshot.present, version: snapshot.version };
34
+ }
35
+ async plan(canonical) {
36
+ const canonicalRoot = dirname(canonical.instructionsFile);
37
+ const skillsRoot = join(this.homeDir, ".agents", "skills");
38
+ const desiredSkills = canonical.skills
39
+ .filter((skill) => isInScope(this.id, skill.scope))
40
+ .map((skill) => ({ name: skill.name, target: skill.dir }));
41
+ const skillItems = planSymlinks({
42
+ rootDir: skillsRoot,
43
+ desired: desiredSkills,
44
+ canonicalRoot: join(canonicalRoot, "skills"),
45
+ kind: "skill",
46
+ });
47
+ const mcpItems = this.planMcp(canonical);
48
+ const configuredInstructionsPath = readInstructionsPath(join(this.homeDir, ".codex", "config.toml"));
49
+ if (!configuredInstructionsPath) {
50
+ // Codex has no `instructions` key set — not Trellis's to decide.
51
+ // MCP planning is independent of this and must still run.
52
+ return [...skillItems, ...mcpItems];
53
+ }
54
+ const instructionsItems = planSymlinks({
55
+ rootDir: dirname(configuredInstructionsPath),
56
+ desired: [{ name: basename(configuredInstructionsPath), target: canonical.instructionsFile }],
57
+ canonicalRoot,
58
+ kind: "instructions",
59
+ });
60
+ return [...skillItems, ...instructionsItems, ...mcpItems];
61
+ }
62
+ planMcp(canonical) {
63
+ const configTomlPath = join(this.homeDir, ".codex", "config.toml");
64
+ const content = existsSync(configTomlPath) ? readFileSync(configTomlPath, "utf-8") : "";
65
+ const { desired, conflicts } = resolveMcpPlan(this.id, canonical.mcp);
66
+ const items = [];
67
+ for (const { name, def } of desired) {
68
+ const current = currentServerSectionText(content, name);
69
+ const rendered = renderServerSection(name, def);
70
+ if (current === rendered) {
71
+ continue; // already correct — no-op
72
+ }
73
+ items.push({
74
+ action: "create",
75
+ kind: "mcp",
76
+ target: configTomlPath,
77
+ mcpWrite: { name, def },
78
+ description: `MCP server "${name}" ${current === null ? "created" : "updated"} in ${configTomlPath}`,
79
+ });
80
+ }
81
+ for (const conflict of conflicts) {
82
+ items.push({ action: "conflict", kind: "mcp", target: configTomlPath, description: conflict.message });
83
+ }
84
+ return items;
85
+ }
86
+ async apply(plan) {
87
+ await applySymlinkPlan(plan.filter((item) => item.kind !== "mcp"));
88
+ const mcpCreates = plan.filter((item) => item.kind === "mcp" && item.action === "create" && item.mcpWrite);
89
+ if (mcpCreates.length === 0) {
90
+ return;
91
+ }
92
+ const configTomlPath = mcpCreates[0].target;
93
+ let content = existsSync(configTomlPath) ? readFileSync(configTomlPath, "utf-8") : "";
94
+ for (const item of mcpCreates) {
95
+ content = upsertSection(content, item.mcpWrite.name, item.mcpWrite.def);
96
+ }
97
+ writeFileSync(configTomlPath, content);
98
+ }
99
+ async verify(canonical) {
100
+ const snapshot = await codexProbe.probe(this.homeDir);
101
+ if (!snapshot.present) {
102
+ return { ok: false, mismatches: ["codex is not present on this machine"] };
103
+ }
104
+ const mismatches = [];
105
+ const desiredNames = new Set(canonical.skills.filter((s) => isInScope(this.id, s.scope)).map((s) => s.name));
106
+ const actualSkills = new Set(snapshot.skillRoots.flatMap((root) => root.skills).map((s) => s.name));
107
+ for (const name of desiredNames) {
108
+ if (!actualSkills.has(name)) {
109
+ mismatches.push(`skill "${name}" is in canonical scope for codex but missing on disk`);
110
+ }
111
+ }
112
+ for (const name of actualSkills) {
113
+ if (!desiredNames.has(name)) {
114
+ mismatches.push(`skill "${name}" is present on disk but not in canonical scope for codex`);
115
+ }
116
+ }
117
+ return mismatches.length === 0 ? { ok: true } : { ok: false, mismatches };
118
+ }
119
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Shared JSON MCP config handling for Claude Code (`~/.claude.json`) and
3
+ * Kiro (`~/.kiro/settings/mcp.json`) — plain parse → merge under
4
+ * `mcpServers` → stringify (trellis-mcp-sync-p2 design.md D4). Unlike
5
+ * Codex's TOML, JSON has no comments to lose, so a full parse/stringify
6
+ * round-trip is safe as long as every sibling top-level key is carried
7
+ * through untouched.
8
+ */
9
+ import type { AgentId, McpConfig, McpServerDef } from "../core/types.js";
10
+ import type { AdapterPlanItem } from "../core/adapter.js";
11
+ /** Renders the native JSON shape Claude Code/Kiro's `mcpServers` map
12
+ * expects — `env` values are always `${VAR}` references, never literals
13
+ * (docs/research.md "Secrets"). */
14
+ export declare function renderJsonServerEntry(def: McpServerDef): Record<string, unknown>;
15
+ export declare function planJsonMcp(opts: {
16
+ configPath: string;
17
+ parsed: Record<string, unknown> | undefined;
18
+ mcp: McpConfig;
19
+ agentId: AgentId;
20
+ }): AdapterPlanItem[];
21
+ /** Merges every `"create"` MCP item into `parsed` (or a fresh `{}` if the
22
+ * file didn't exist), returning the object to stringify — every sibling
23
+ * top-level key on `parsed` is spread through untouched. */
24
+ export declare function applyJsonMcp(parsed: Record<string, unknown> | undefined, items: AdapterPlanItem[]): Record<string, unknown>;
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Shared JSON MCP config handling for Claude Code (`~/.claude.json`) and
3
+ * Kiro (`~/.kiro/settings/mcp.json`) — plain parse → merge under
4
+ * `mcpServers` → stringify (trellis-mcp-sync-p2 design.md D4). Unlike
5
+ * Codex's TOML, JSON has no comments to lose, so a full parse/stringify
6
+ * round-trip is safe as long as every sibling top-level key is carried
7
+ * through untouched.
8
+ */
9
+ import { resolveMcpPlan } from "./mcpPlan.js";
10
+ function deepEqual(a, b) {
11
+ if (a === b)
12
+ return true;
13
+ if (a === null || b === null || typeof a !== typeof b)
14
+ return false;
15
+ if (Array.isArray(a) || Array.isArray(b)) {
16
+ if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length)
17
+ return false;
18
+ return a.every((value, index) => deepEqual(value, b[index]));
19
+ }
20
+ if (typeof a === "object" && typeof b === "object") {
21
+ const aKeys = Object.keys(a);
22
+ const bKeys = Object.keys(b);
23
+ if (aKeys.length !== bKeys.length)
24
+ return false;
25
+ return aKeys.every((key) => deepEqual(a[key], b[key]));
26
+ }
27
+ return false;
28
+ }
29
+ /** Renders the native JSON shape Claude Code/Kiro's `mcpServers` map
30
+ * expects — `env` values are always `${VAR}` references, never literals
31
+ * (docs/research.md "Secrets"). */
32
+ export function renderJsonServerEntry(def) {
33
+ if (def.transport === "http" || def.transport === "sse") {
34
+ const entry = { type: def.transport, url: def.url };
35
+ if (def.headers && Object.keys(def.headers).length > 0) {
36
+ entry.headers = def.headers;
37
+ }
38
+ return entry;
39
+ }
40
+ const entry = { type: "stdio", command: def.command, args: def.args ?? [] };
41
+ if (def.env && def.env.length > 0) {
42
+ entry.env = Object.fromEntries(def.env.map((name) => [name, `\${${name}}`]));
43
+ }
44
+ return entry;
45
+ }
46
+ export function planJsonMcp(opts) {
47
+ const { configPath, parsed, mcp, agentId } = opts;
48
+ const { desired, conflicts } = resolveMcpPlan(agentId, mcp);
49
+ const existingServers = parsed?.mcpServers ?? {};
50
+ const items = [];
51
+ for (const { name, def } of desired) {
52
+ const rendered = renderJsonServerEntry(def);
53
+ const current = existingServers[name];
54
+ if (deepEqual(current, rendered)) {
55
+ continue; // already correct — no-op
56
+ }
57
+ items.push({
58
+ action: "create",
59
+ kind: "mcp",
60
+ target: configPath,
61
+ mcpWrite: { name, def },
62
+ description: `MCP server "${name}" ${current === undefined ? "created" : "updated"} in ${configPath}`,
63
+ });
64
+ }
65
+ for (const conflict of conflicts) {
66
+ items.push({ action: "conflict", kind: "mcp", target: configPath, description: conflict.message });
67
+ }
68
+ return items;
69
+ }
70
+ /** Merges every `"create"` MCP item into `parsed` (or a fresh `{}` if the
71
+ * file didn't exist), returning the object to stringify — every sibling
72
+ * top-level key on `parsed` is spread through untouched. */
73
+ export function applyJsonMcp(parsed, items) {
74
+ const base = parsed ?? {};
75
+ const existingServers = base.mcpServers ?? {};
76
+ const mergedServers = { ...existingServers };
77
+ for (const item of items) {
78
+ if (item.kind !== "mcp" || item.action !== "create" || !item.mcpWrite) {
79
+ continue;
80
+ }
81
+ mergedServers[item.mcpWrite.name] = renderJsonServerEntry(item.mcpWrite.def);
82
+ }
83
+ return { ...base, mcpServers: mergedServers };
84
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Kiro adapter: same shape as Claude Code — `~/.kiro/skills/<name>`
3
+ * symlinks, `~/.kiro/steering/CLAUDE.md` symlinked to canonical `agents.md`.
4
+ *
5
+ * MCP servers: plain JSON parse → merge under `mcpServers` → stringify
6
+ * (trellis-mcp-sync-p2 design.md D4) — create/repair only, no automatic
7
+ * removal (D7).
8
+ *
9
+ * Kiro's own `${VAR}` substitution (real, found by reading Kiro's
10
+ * installed extension source — trellis-kiro-approved-env-vars design.md
11
+ * Context) is gated by `kiroAgent.mcpApprovedEnvVars`, a completely
12
+ * separate, VS-Code-style global settings.json — not the mcp.json above.
13
+ * A name absent from that list is silently never substituted. This
14
+ * adapter keeps that list a superset of every env name it references,
15
+ * additive only.
16
+ */
17
+ import type { AdapterPlanItem, AdapterProbeResult, AdapterVerifyResult, TrellisAdapter } from "../core/adapter.js";
18
+ import type { CanonicalSource } from "../core/types.js";
19
+ export declare class KiroAdapter implements TrellisAdapter {
20
+ private readonly homeDir;
21
+ readonly name = "Kiro";
22
+ readonly id: "kiro";
23
+ constructor(homeDir?: string);
24
+ probe(): Promise<AdapterProbeResult>;
25
+ plan(canonical: CanonicalSource): Promise<AdapterPlanItem[]>;
26
+ private planMcp;
27
+ /** Names come from `resolveMcpPlan`, not a raw scan of `canonical.mcp.servers`
28
+ * (design.md D2) — a server scoped away from Kiro, or refused for a
29
+ * known_host_injected collision, never contributes a name here either. */
30
+ private desiredApprovedEnvVars;
31
+ private planApprovedEnvVars;
32
+ apply(plan: AdapterPlanItem[]): Promise<void>;
33
+ verify(canonical: CanonicalSource): Promise<AdapterVerifyResult>;
34
+ }