agent-trellis 0.1.0 → 0.2.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 +57 -17
- package/dist/adapters/claude-code.d.ts +2 -1
- package/dist/adapters/claude-code.js +7 -7
- package/dist/adapters/codex.d.ts +2 -1
- package/dist/adapters/codex.js +7 -7
- package/dist/adapters/jsonMcp.d.ts +1 -0
- package/dist/adapters/jsonMcp.js +2 -2
- package/dist/adapters/kiro.d.ts +2 -1
- package/dist/adapters/kiro.js +9 -9
- package/dist/adapters/mcpPlan.d.ts +1 -1
- package/dist/adapters/mcpPlan.js +2 -2
- 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 +41 -11
- package/dist/commands/init.js +11 -0
- package/dist/commands/mcp.d.ts +13 -0
- package/dist/commands/mcp.js +31 -7
- package/dist/commands/onboard.d.ts +42 -7
- package/dist/commands/onboard.js +207 -34
- 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/sync.d.ts +13 -0
- package/dist/commands/sync.js +31 -5
- package/dist/core/adapter.d.ts +10 -3
- package/dist/core/adapter.js +2 -2
- package/dist/core/canonical.js +22 -0
- package/dist/core/types.d.ts +12 -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/installAgent.d.ts +26 -0
- package/dist/lib/installAgent.js +46 -0
- package/dist/pi-bridge/bundle.js +26 -7
- package/dist/pi-bridge/index.js +8 -1
- package/docs/getting-started.md +104 -26
- package/docs/roadmap.md +133 -0
- package/package.json +1 -1
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Install-then-manage (trellis-managed-agents design.md D5): selecting a
|
|
3
|
+
* not-yet-present agent into the managed set is itself the authorization
|
|
4
|
+
* to install it, but never silently — one confirmation, then a real
|
|
5
|
+
* `npm install -g <package>` child process. Kiro has no npm package (a
|
|
6
|
+
* desktop download) and is refused before this module is ever reached.
|
|
7
|
+
*/
|
|
8
|
+
import type { AgentId } from "../core/types.js";
|
|
9
|
+
/** Only agents with a real `npm install -g <pkg>` command — Kiro's own
|
|
10
|
+
* `INSTALL_HINTS` entry is a download URL, not a package, and is never
|
|
11
|
+
* looked up here. */
|
|
12
|
+
export declare const NPM_INSTALLABLE: Record<Exclude<AgentId, "kiro">, string>;
|
|
13
|
+
export interface ConfirmAndInstallOptions {
|
|
14
|
+
/** Test/real seam, same pattern as onboard's `promptForAgent` — a real
|
|
15
|
+
* terminal confirmation by default. */
|
|
16
|
+
confirm?: (agent: AgentId, pkg: string) => Promise<boolean>;
|
|
17
|
+
/** Test/real seam — never a real `npm install` in a unit test. */
|
|
18
|
+
runInstall?: (pkg: string) => void;
|
|
19
|
+
}
|
|
20
|
+
export interface ConfirmAndInstallResult {
|
|
21
|
+
installed: boolean;
|
|
22
|
+
/** False only when the agent has no npm package at all (Kiro) — the
|
|
23
|
+
* caller refuses this agent with its download URL instead of prompting. */
|
|
24
|
+
installable: boolean;
|
|
25
|
+
}
|
|
26
|
+
export declare function confirmAndInstall(agent: AgentId, opts?: ConfirmAndInstallOptions): Promise<ConfirmAndInstallResult>;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Install-then-manage (trellis-managed-agents design.md D5): selecting a
|
|
3
|
+
* not-yet-present agent into the managed set is itself the authorization
|
|
4
|
+
* to install it, but never silently — one confirmation, then a real
|
|
5
|
+
* `npm install -g <package>` child process. Kiro has no npm package (a
|
|
6
|
+
* desktop download) and is refused before this module is ever reached.
|
|
7
|
+
*/
|
|
8
|
+
import { execFileSync } from "node:child_process";
|
|
9
|
+
import { createInterface } from "node:readline/promises";
|
|
10
|
+
/** Only agents with a real `npm install -g <pkg>` command — Kiro's own
|
|
11
|
+
* `INSTALL_HINTS` entry is a download URL, not a package, and is never
|
|
12
|
+
* looked up here. */
|
|
13
|
+
export const NPM_INSTALLABLE = {
|
|
14
|
+
"claude-code": "@anthropic-ai/claude-code",
|
|
15
|
+
codex: "@openai/codex",
|
|
16
|
+
pi: "@earendil-works/pi-coding-agent",
|
|
17
|
+
};
|
|
18
|
+
async function confirmReal(agent, pkg) {
|
|
19
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
20
|
+
try {
|
|
21
|
+
const answer = (await rl.question(`${agent} is not installed. Install it now (npm install -g ${pkg})? [y/N] `)).trim().toLowerCase();
|
|
22
|
+
return answer === "y" || answer === "yes";
|
|
23
|
+
}
|
|
24
|
+
finally {
|
|
25
|
+
rl.close();
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function runInstallReal(pkg) {
|
|
29
|
+
// argv array, never a shell string — the package name is never
|
|
30
|
+
// interpolated into anything a shell parses.
|
|
31
|
+
execFileSync("npm", ["install", "-g", pkg], { stdio: "inherit" });
|
|
32
|
+
}
|
|
33
|
+
export async function confirmAndInstall(agent, opts = {}) {
|
|
34
|
+
if (agent === "kiro") {
|
|
35
|
+
return { installed: false, installable: false };
|
|
36
|
+
}
|
|
37
|
+
const pkg = NPM_INSTALLABLE[agent];
|
|
38
|
+
const confirm = opts.confirm ?? confirmReal;
|
|
39
|
+
const runInstall = opts.runInstall ?? runInstallReal;
|
|
40
|
+
const agreed = await confirm(agent, pkg);
|
|
41
|
+
if (!agreed) {
|
|
42
|
+
return { installed: false, installable: true };
|
|
43
|
+
}
|
|
44
|
+
runInstall(pkg);
|
|
45
|
+
return { installed: true, installable: true };
|
|
46
|
+
}
|
package/dist/pi-bridge/bundle.js
CHANGED
|
@@ -27254,8 +27254,9 @@ var ALL_AGENTS = [
|
|
|
27254
27254
|
"kiro",
|
|
27255
27255
|
"pi"
|
|
27256
27256
|
];
|
|
27257
|
-
function resolveScope(scope) {
|
|
27258
|
-
|
|
27257
|
+
function resolveScope(scope, managedAgents) {
|
|
27258
|
+
const candidates = scope ?? managedAgents;
|
|
27259
|
+
return candidates.filter((id) => managedAgents.includes(id));
|
|
27259
27260
|
}
|
|
27260
27261
|
|
|
27261
27262
|
// src/core/canonical.ts
|
|
@@ -27319,6 +27320,22 @@ function loadSecretsPolicyYaml(path, homeDir) {
|
|
|
27319
27320
|
envFile: parsed.env_file ? parsed.env_file.replace(/^~(?=$|\/)/, homeDir) : void 0
|
|
27320
27321
|
};
|
|
27321
27322
|
}
|
|
27323
|
+
function loadManagedYaml(path, diagnostics) {
|
|
27324
|
+
if (!existsSync(path)) {
|
|
27325
|
+
return [];
|
|
27326
|
+
}
|
|
27327
|
+
const parsed = (0, import_yaml.parse)(readFileSync(path, "utf-8")) ?? {};
|
|
27328
|
+
const raw = parsed.agents ?? [];
|
|
27329
|
+
const valid = [];
|
|
27330
|
+
for (const id of raw) {
|
|
27331
|
+
if (ALL_AGENTS.includes(id)) {
|
|
27332
|
+
valid.push(id);
|
|
27333
|
+
} else {
|
|
27334
|
+
diagnostics.push(`managed.yaml: "${id}" is not a recognized agent id \u2014 ignored`);
|
|
27335
|
+
}
|
|
27336
|
+
}
|
|
27337
|
+
return valid;
|
|
27338
|
+
}
|
|
27322
27339
|
function scopeFor(map, name) {
|
|
27323
27340
|
return map?.[name];
|
|
27324
27341
|
}
|
|
@@ -27334,6 +27351,7 @@ function loadCanonicalSource(homeDir = homedir()) {
|
|
|
27334
27351
|
);
|
|
27335
27352
|
}
|
|
27336
27353
|
const diagnostics = [];
|
|
27354
|
+
const managedAgents = loadManagedYaml(join(root, "managed.yaml"), diagnostics);
|
|
27337
27355
|
const scopeYaml = loadScopeYaml(join(root, "scope.yaml"));
|
|
27338
27356
|
const skillDirs = listSkillDirs(join(root, "skills"));
|
|
27339
27357
|
const knownSkillNames = new Set(skillDirs.map((s) => s.name));
|
|
@@ -27368,6 +27386,7 @@ function loadCanonicalSource(homeDir = homedir()) {
|
|
|
27368
27386
|
}
|
|
27369
27387
|
return {
|
|
27370
27388
|
instructionsFile: join(root, "agents.md"),
|
|
27389
|
+
managedAgents,
|
|
27371
27390
|
skills,
|
|
27372
27391
|
agents,
|
|
27373
27392
|
memories,
|
|
@@ -27381,8 +27400,8 @@ function loadCanonicalSource(homeDir = homedir()) {
|
|
|
27381
27400
|
}
|
|
27382
27401
|
|
|
27383
27402
|
// src/core/adapter.ts
|
|
27384
|
-
function isInScope(id, scope) {
|
|
27385
|
-
return resolveScope(scope).includes(id);
|
|
27403
|
+
function isInScope(id, scope, managedAgents) {
|
|
27404
|
+
return resolveScope(scope, managedAgents).includes(id);
|
|
27386
27405
|
}
|
|
27387
27406
|
|
|
27388
27407
|
// src/lib/tomlSection.ts
|
|
@@ -27420,7 +27439,7 @@ function findLiteralSecret(def) {
|
|
|
27420
27439
|
}
|
|
27421
27440
|
return void 0;
|
|
27422
27441
|
}
|
|
27423
|
-
function resolveMcpPlan(agentId, mcp) {
|
|
27442
|
+
function resolveMcpPlan(agentId, mcp, managedAgents) {
|
|
27424
27443
|
if (mcp.hub) {
|
|
27425
27444
|
if (mcp.knownHostInjected.includes(HUB_ENTRY_NAME)) {
|
|
27426
27445
|
return { desired: [], conflicts: [{ name: HUB_ENTRY_NAME, message: collisionMessage(HUB_ENTRY_NAME, agentId) }] };
|
|
@@ -27430,7 +27449,7 @@ function resolveMcpPlan(agentId, mcp) {
|
|
|
27430
27449
|
const desired = [];
|
|
27431
27450
|
const conflicts = [];
|
|
27432
27451
|
for (const [name, def] of Object.entries(mcp.servers)) {
|
|
27433
|
-
if (!isInScope(agentId, def.agents)) {
|
|
27452
|
+
if (!isInScope(agentId, def.agents, managedAgents)) {
|
|
27434
27453
|
continue;
|
|
27435
27454
|
}
|
|
27436
27455
|
if (mcp.knownHostInjected.includes(name)) {
|
|
@@ -32010,7 +32029,7 @@ function registerServerTools(pi, serverName, client, timeoutMs) {
|
|
|
32010
32029
|
}
|
|
32011
32030
|
async function trellisMcpBridge(pi, homeDir = homedir2(), connectTimeoutMs = DEFAULT_CONNECT_TIMEOUT_MS) {
|
|
32012
32031
|
const canonical = loadCanonicalSource(homeDir);
|
|
32013
|
-
const { desired } = resolveMcpPlan("pi", canonical.mcp);
|
|
32032
|
+
const { desired } = resolveMcpPlan("pi", canonical.mcp, ALL_AGENTS);
|
|
32014
32033
|
const clients = /* @__PURE__ */ new Set();
|
|
32015
32034
|
const closeClient = async (client) => {
|
|
32016
32035
|
if (!clients.delete(client)) return;
|
package/dist/pi-bridge/index.js
CHANGED
|
@@ -20,6 +20,7 @@ import { loadCanonicalSource } from "../core/canonical.js";
|
|
|
20
20
|
import { resolveMcpPlan } from "../adapters/mcpPlan.js";
|
|
21
21
|
import { resolveSecretEnv } from "../lib/secretEnv.js";
|
|
22
22
|
import { extractTemplateVarNames } from "../lib/envVarNames.js";
|
|
23
|
+
import { ALL_AGENTS } from "../core/types.js";
|
|
23
24
|
import { bridgedToolName, toParametersSchema, toPiContent } from "./schemaTranslate.js";
|
|
24
25
|
const CLIENT_INFO = { name: "trellis-mcp-bridge", version: "0.0.0" };
|
|
25
26
|
const DEFAULT_CONNECT_TIMEOUT_MS = 10_000;
|
|
@@ -128,7 +129,13 @@ function registerServerTools(pi, serverName, client, timeoutMs) {
|
|
|
128
129
|
*/
|
|
129
130
|
export default async function trellisMcpBridge(pi, homeDir = homedir(), connectTimeoutMs = DEFAULT_CONNECT_TIMEOUT_MS) {
|
|
130
131
|
const canonical = loadCanonicalSource(homeDir);
|
|
131
|
-
|
|
132
|
+
// Deliberately ALL_AGENTS, not canonical.managedAgents: this bridge only
|
|
133
|
+
// ever runs because pi itself loaded the extension — that's already the
|
|
134
|
+
// strongest possible consent signal (trellis-managed-agents design.md),
|
|
135
|
+
// independent of whether `trellis onboard` was ever run to add pi to
|
|
136
|
+
// managed.yaml. managedAgents governs static config-file writes; this
|
|
137
|
+
// is pi reading canonical directly at its own runtime, P4's own concern.
|
|
138
|
+
const { desired } = resolveMcpPlan("pi", canonical.mcp, ALL_AGENTS);
|
|
132
139
|
const clients = new Set();
|
|
133
140
|
const closeClient = async (client) => {
|
|
134
141
|
if (!clients.delete(client))
|
package/docs/getting-started.md
CHANGED
|
@@ -16,39 +16,59 @@ $ trellis onboard
|
|
|
16
16
|
```
|
|
17
17
|
|
|
18
18
|
Runs `init`, detects which of Claude Code/Codex/Kiro/pi are on this machine,
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
- **
|
|
25
|
-
|
|
26
|
-
|
|
19
|
+
then resolves two independent choices before running `migrate`, `sync`,
|
|
20
|
+
`mcp sync`, and `secrets audit` — the whole onboarding path, no follow-up
|
|
21
|
+
commands to type by hand:
|
|
22
|
+
|
|
23
|
+
1. **Migration source** — read from, at most one, never written back to.
|
|
24
|
+
- **No agent has real content**: skipped — canonical starts from `init`'s
|
|
25
|
+
placeholder.
|
|
26
|
+
- **Exactly one agent has real content**: auto-selected, no prompt.
|
|
27
|
+
- **Two or more**: prompts you with a numbered choice (if you're at a real
|
|
28
|
+
terminal), or pass `--agent <id>` to skip the prompt.
|
|
29
|
+
2. **Managed set** — zero or more agents to actually write to. Always an
|
|
30
|
+
explicit choice: pass `--manage <ids>` (comma-separated, e.g. `--manage
|
|
31
|
+
pi,codex`) or `--manage none`, or answer the numbered multi-select prompt.
|
|
32
|
+
**The source is not included by default** — migrating from Claude Code
|
|
33
|
+
doesn't mean Trellis starts managing Claude Code too, unless you say so.
|
|
34
|
+
Selecting an agent that isn't installed yet is itself the authorization to
|
|
35
|
+
install it (one confirmation, then a real `npm install -g <package>`);
|
|
36
|
+
Kiro has no CLI package and is refused with its download URL instead.
|
|
27
37
|
|
|
28
38
|
```
|
|
29
|
-
$ trellis onboard --agent claude-code
|
|
30
|
-
Using claude-code as the migration
|
|
39
|
+
$ trellis onboard --agent claude-code --manage pi
|
|
40
|
+
Using claude-code as the migration source (--agent).
|
|
41
|
+
Managed agents: pi
|
|
31
42
|
|
|
32
43
|
migrate --from claude-code
|
|
33
44
|
[create] skill "my-skill" — will copy from /Users/you/.claude/skills/my-skill
|
|
34
45
|
...
|
|
35
46
|
|
|
36
47
|
sync
|
|
37
|
-
✅
|
|
48
|
+
✅ pi — 1 created, 0 removed, 0 conflict(s)
|
|
38
49
|
...
|
|
39
50
|
|
|
40
|
-
|
|
51
|
+
mcp sync
|
|
52
|
+
✅ pi — already in sync
|
|
53
|
+
|
|
54
|
+
secrets audit
|
|
55
|
+
✅ no findings — every present agent's real config and every declared env var passed all checks
|
|
41
56
|
```
|
|
42
57
|
|
|
43
|
-
|
|
44
|
-
|
|
58
|
+
No agent named `claude-code` appears in the `sync`/`mcp sync` output above —
|
|
59
|
+
it's present and was the migration source, but it isn't managed, so it's
|
|
60
|
+
never even probed as a sync target, not just left with zero items.
|
|
61
|
+
|
|
62
|
+
Add `--dry-run` to preview the entire chain — init/migrate/sync/mcp
|
|
63
|
+
sync, including what would be written to `~/.trellis/managed.yaml` — with
|
|
64
|
+
zero writes anywhere (secrets audit is always read-only, with or without
|
|
65
|
+
the flag).
|
|
45
66
|
|
|
46
|
-
**
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
isn't built yet — see [README's Status](../README.md#status).
|
|
67
|
+
**A managed agent's own real content still isn't overwritten.** If you
|
|
68
|
+
explicitly include the source in `--manage`, sync still never overwrites
|
|
69
|
+
its real files — see the conflict table below. Merging differing content
|
|
70
|
+
across multiple agents into one canonical result isn't built yet — see
|
|
71
|
+
[README's Status](../README.md#status).
|
|
52
72
|
|
|
53
73
|
The rest of this page is the same flow broken into its individual steps —
|
|
54
74
|
useful if you want more control over any one part, or just want to
|
|
@@ -138,8 +158,10 @@ before moving on to `sync`.
|
|
|
138
158
|
|
|
139
159
|
## `trellis sync`
|
|
140
160
|
|
|
141
|
-
Distributes canonical skills and instructions to every agent
|
|
142
|
-
|
|
161
|
+
Distributes canonical skills and instructions to every **managed** agent
|
|
162
|
+
(`~/.trellis/managed.yaml` — empty by default; `trellis onboard` writes it,
|
|
163
|
+
or edit it yourself). A present-but-unmanaged agent gets no report line at
|
|
164
|
+
all, not just zero items:
|
|
143
165
|
|
|
144
166
|
```
|
|
145
167
|
$ trellis sync
|
|
@@ -162,12 +184,18 @@ canonical, that's what `migrate` is for.
|
|
|
162
184
|
Run `trellis sync skills` or `trellis sync instructions` to distribute just
|
|
163
185
|
one half. Add `--dry-run` (in any position — `trellis sync --dry-run` and
|
|
164
186
|
`trellis sync skills --dry-run` both work) to preview the plan with zero
|
|
165
|
-
writes
|
|
187
|
+
writes (and, per the same rule, records nothing to back up — see
|
|
188
|
+
`trellis rollback` below).
|
|
189
|
+
|
|
190
|
+
Every create/repair/remove this actually performs is recorded first,
|
|
191
|
+
automatically, so `trellis rollback` can undo the whole run later — see
|
|
192
|
+
[`trellis rollback`](#trellis-rollback--undoing-a-syncmcp-synconboard-run)
|
|
193
|
+
below.
|
|
166
194
|
|
|
167
195
|
## `trellis mcp sync`
|
|
168
196
|
|
|
169
|
-
Distributes `~/.trellis/mcp/servers.yaml` to every
|
|
170
|
-
MCP config. See [`schema/servers.example.yaml`](../schema/servers.example.yaml)
|
|
197
|
+
Distributes `~/.trellis/mcp/servers.yaml` to every **managed** agent's
|
|
198
|
+
native MCP config — same restriction as `sync`, see above. See [`schema/servers.example.yaml`](../schema/servers.example.yaml)
|
|
171
199
|
for the full documented shape — server definitions, per-agent scoping,
|
|
172
200
|
known-host-injected collision avoidance, and hub mode.
|
|
173
201
|
|
|
@@ -180,6 +208,11 @@ does not remove it from any agent's native config yet (see
|
|
|
180
208
|
[README's Known limitations](../README.md#status)). Remove it by hand on
|
|
181
209
|
each agent in the meantime.
|
|
182
210
|
|
|
211
|
+
Every native-config file this rewrites in place is snapshotted first,
|
|
212
|
+
automatically — see
|
|
213
|
+
[`trellis rollback`](#trellis-rollback--undoing-a-syncmcp-synconboard-run)
|
|
214
|
+
below to undo a run that turned out to be wrong.
|
|
215
|
+
|
|
183
216
|
`env:` in `servers.yaml` lists variable **names** only, never literal
|
|
184
217
|
values — the real values come from wherever your shell/secret manager
|
|
185
218
|
already populates them. See
|
|
@@ -193,7 +226,8 @@ narrow exception (pi's bridge has to read a value into its own process).
|
|
|
193
226
|
$ trellis secrets audit
|
|
194
227
|
```
|
|
195
228
|
|
|
196
|
-
Scans every
|
|
229
|
+
Scans every **managed** agent's **real, on-disk** config (never canonical)
|
|
230
|
+
for — same restriction as `sync`, see above:
|
|
197
231
|
|
|
198
232
|
1. A literal value matching one of `secrets.policy.yaml`'s
|
|
199
233
|
`reject_patterns` (a credential-shaped string that should have been a
|
|
@@ -220,6 +254,50 @@ Safe to run any time; nothing here writes anything.
|
|
|
220
254
|
stdio MCP server (some reaching real external services) — not something a
|
|
221
255
|
"just check my config" command should do by default.
|
|
222
256
|
|
|
257
|
+
## `trellis rollback` — undoing a `sync`/`mcp sync`/`onboard` run
|
|
258
|
+
|
|
259
|
+
Every real write those three commands perform is recorded, before it
|
|
260
|
+
happens, to a structured run directory under `~/.trellis/backups/` — no
|
|
261
|
+
flag needed, this is always on for any run that actually writes
|
|
262
|
+
something. `--dry-run` never creates one, since nothing was written.
|
|
263
|
+
|
|
264
|
+
```
|
|
265
|
+
$ trellis rollback
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
Omitting a run id targets the most recent run. Pass one explicitly to
|
|
269
|
+
undo an older run — see `trellis rollback --list` for what's available:
|
|
270
|
+
|
|
271
|
+
```
|
|
272
|
+
$ trellis rollback --list
|
|
273
|
+
2026-09-13T04-52-18-727Z-mcp-sync — mcp-sync, 3 operation(s), 2026-09-13T04:52:18.727Z
|
|
274
|
+
2026-09-13T04-44-56-967Z-sync — sync, 1 operation(s), 2026-09-13T04:44:56.967Z
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
For each recorded operation, rollback checks whether that path's
|
|
278
|
+
**current** state still matches what the run itself left behind:
|
|
279
|
+
|
|
280
|
+
| Current state vs. recorded | Result |
|
|
281
|
+
|---|---|
|
|
282
|
+
| Unchanged since the run | `restore` — the file's exact prior bytes, or the symlink's exact prior target, or removed if the run created it |
|
|
283
|
+
| Something else touched it since | `conflict` — reported, left untouched, never force-restored over |
|
|
284
|
+
|
|
285
|
+
One path's `conflict` never blocks any other path in the same rollback
|
|
286
|
+
from restoring. Exit code is non-zero if any `conflict` occurred. Add
|
|
287
|
+
`--dry-run` to preview the restore/conflict plan with zero writes, or
|
|
288
|
+
`--json` for machine-readable output.
|
|
289
|
+
|
|
290
|
+
`onboard` shares one backup run across its whole chained `sync`/`mcp
|
|
291
|
+
sync` stages — one `trellis rollback` undoes an entire `onboard`
|
|
292
|
+
invocation, not just its last stage. `migrate` is not covered: it only
|
|
293
|
+
ever creates a new canonical entry or refuses on conflict, never
|
|
294
|
+
overwrites existing canonical content, so there's nothing a snapshot
|
|
295
|
+
would add — undoing a migrate mistake is just deleting the newly
|
|
296
|
+
created file under `~/.trellis/skills/`.
|
|
297
|
+
|
|
298
|
+
`~/.trellis/backups/` has no automatic pruning — delete old run
|
|
299
|
+
directories by hand once you're done with them.
|
|
300
|
+
|
|
223
301
|
## Troubleshooting
|
|
224
302
|
|
|
225
303
|
- **A `conflict` I don't understand**: `migrate` and `sync` both name the
|
package/docs/roadmap.md
CHANGED
|
@@ -467,6 +467,139 @@ 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
|
+
|
|
470
603
|
| Phase | Deliverable | Depends on |
|
|
471
604
|
|---|---|---|
|
|
472
605
|
| P0 | ✅ `trellis doctor` — read-only, opt-in-for-handshakes scan of all four agents' current skills/MCP/instructions state, reports drift and duplicates | nothing |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-trellis",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.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",
|