agent-trellis 0.3.0 → 0.4.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.
@@ -34,6 +34,14 @@ export type MemorySyncResult = {
34
34
  */
35
35
  export declare function collectMemorySyncResult(homeDir?: string): MemorySyncResult;
36
36
  export declare function applyMemorySync(result: MemorySyncResult): void;
37
+ /**
38
+ * Extracted from `runMemorySync` (trellis-onboard-mcp-memory) so
39
+ * `onboard`'s own chained output can print this stage in the exact same
40
+ * format without a second, drifting copy — mirrors the
41
+ * `collect...Report`/`print...Report` export pair every other chained
42
+ * stage (`sync`, `mcp sync`, `secrets audit`) already has.
43
+ */
44
+ export declare function printMemorySyncResult(result: MemorySyncResult, dryRun: boolean): void;
37
45
  export declare function runMemorySync(opts?: RunMemorySyncOptions): {
38
46
  exitCode: number;
39
47
  };
@@ -45,6 +45,26 @@ export function applyMemorySync(result) {
45
45
  mkdirSync(dirname(result.graphPath), { recursive: true });
46
46
  writeFileSync(result.graphPath, renderMemoryGraph(result.plan.nextGraph));
47
47
  }
48
+ /**
49
+ * Extracted from `runMemorySync` (trellis-onboard-mcp-memory) so
50
+ * `onboard`'s own chained output can print this stage in the exact same
51
+ * format without a second, drifting copy — mirrors the
52
+ * `collect...Report`/`print...Report` export pair every other chained
53
+ * stage (`sync`, `mcp sync`, `secrets audit`) already has.
54
+ */
55
+ export function printMemorySyncResult(result, dryRun) {
56
+ if (!result.configured) {
57
+ console.log(result.reason);
58
+ return;
59
+ }
60
+ console.log(`${dryRun ? "[dry run] " : ""}memory sync — ${result.graphPath}`);
61
+ if (result.plan.items.length === 0) {
62
+ console.log(" nothing in canonical memories/ yet");
63
+ }
64
+ for (const item of result.plan.items) {
65
+ console.log(` [${item.action}] "${item.name}" — ${item.detail}`);
66
+ }
67
+ }
48
68
  export function runMemorySync(opts = {}) {
49
69
  const homeDir = opts.homeDir ?? homedir();
50
70
  let result;
@@ -61,17 +81,8 @@ export function runMemorySync(opts = {}) {
61
81
  if (opts.json) {
62
82
  console.log(JSON.stringify(result, null, 2));
63
83
  }
64
- else if (!result.configured) {
65
- console.log(result.reason);
66
- }
67
84
  else {
68
- console.log(`${opts.dryRun ? "[dry run] " : ""}memory sync — ${result.graphPath}`);
69
- if (result.plan.items.length === 0) {
70
- console.log(" nothing in canonical memories/ yet");
71
- }
72
- for (const item of result.plan.items) {
73
- console.log(` [${item.action}] "${item.name}" — ${item.detail}`);
74
- }
85
+ printMemorySyncResult(result, opts.dryRun ?? false);
75
86
  }
76
87
  const hasConflict = result.configured && result.plan.items.some((i) => i.action === "conflict");
77
88
  return { exitCode: hasConflict ? 1 : 0 };
@@ -17,6 +17,7 @@ import type { MigrateKind, MigratePlan } from "./migrate.js";
17
17
  import type { SyncReport } from "./sync.js";
18
18
  import type { McpSyncReport } from "./mcp.js";
19
19
  import type { SecretsAuditReport } from "./secretsAudit.js";
20
+ import type { MemorySyncResult } from "./memory.js";
20
21
  import type { ConfirmAndInstallOptions } from "../lib/installAgent.js";
21
22
  export interface OnboardAgentSummary {
22
23
  agent: AgentId;
@@ -27,6 +28,13 @@ export interface OnboardAgentSummary {
27
28
  * of that agent's own to potentially migrate, not a judgment about
28
29
  * its quality or length. */
29
30
  hasRealInstructions: boolean;
31
+ /** How many real MCP servers `migrate --only mcp` would find for this
32
+ * agent — computed via `collectMigratePlan(agent, homeDir, ["mcp"])`
33
+ * rather than a second reader dispatch, so this always agrees with
34
+ * what migrate itself will do (design.md D1, trellis-onboard-mcp-
35
+ * memory). 0 for pi, which has no MCP reader at all — not a gap here,
36
+ * the same fact `migrate.ts` already establishes. */
37
+ mcpServerCount: number;
30
38
  }
31
39
  export declare function collectOnboardSummary(homeDir?: string): Promise<OnboardAgentSummary[]>;
32
40
  export interface RunOnboardOptions {
@@ -82,6 +90,7 @@ export interface OnboardResult {
82
90
  migrateSkipped?: string;
83
91
  syncReport?: SyncReport;
84
92
  mcpSyncReport?: McpSyncReport;
93
+ memorySyncResult?: MemorySyncResult;
85
94
  secretsAuditReport?: SecretsAuditReport;
86
95
  refusal?: string;
87
96
  /** Only set when no agent is present — the same values `--json` and
@@ -28,6 +28,7 @@ import { applyMigratePlan, collectMigratePlan, printPlan as printMigratePlan } f
28
28
  import { collectSyncReport, printReport as printSyncReport } from "./sync.js";
29
29
  import { collectMcpSyncReport, printReport as printMcpSyncReport } from "./mcp.js";
30
30
  import { collectSecretsAuditReport, printReport as printSecretsAuditReport } from "./secretsAudit.js";
31
+ import { applyMemorySync, collectMemorySyncResult, printMemorySyncResult } from "./memory.js";
31
32
  import { openBackupSession } from "../lib/backup.js";
32
33
  import { confirmAndInstall } from "../lib/installAgent.js";
33
34
  const PROBES = {
@@ -40,19 +41,20 @@ export async function collectOnboardSummary(homeDir = homedir()) {
40
41
  return Promise.all(ALL_AGENTS.map(async (agent) => {
41
42
  const snapshot = await PROBES[agent](homeDir);
42
43
  if (!snapshot.present) {
43
- return { agent, present: false, skillCount: 0, skillNames: [], hasRealInstructions: false };
44
+ return { agent, present: false, skillCount: 0, skillNames: [], hasRealInstructions: false, mcpServerCount: 0 };
44
45
  }
45
46
  const skillNames = snapshot.skillRoots.flatMap((root) => root.skills.map((s) => s.name));
46
47
  const hasRealInstructions = snapshot.instructionsFile !== undefined && !snapshot.instructionsFile.isSymlink;
47
- return { agent, present: true, skillCount: skillNames.length, skillNames, hasRealInstructions };
48
+ const mcpPlan = await collectMigratePlan(agent, homeDir, ["mcp"]);
49
+ return { agent, present: true, skillCount: skillNames.length, skillNames, hasRealInstructions, mcpServerCount: mcpPlan.items.length };
48
50
  }));
49
51
  }
50
52
  function hasContent(s) {
51
- return s.skillCount > 0 || s.hasRealInstructions;
53
+ return s.skillCount > 0 || s.hasRealInstructions || s.mcpServerCount > 0;
52
54
  }
53
55
  function agentSummaryLabel(s) {
54
56
  const skills = s.skillCount > 0 ? ` (${s.skillNames.join(", ")})` : "";
55
- return `${s.agent} — ${s.skillCount} skill(s)${skills}, instructions: ${s.hasRealInstructions ? "yes" : "no"}`;
57
+ return `${s.agent} — ${s.skillCount} skill(s)${skills}, instructions: ${s.hasRealInstructions ? "yes" : "no"}, mcp: ${s.mcpServerCount}`;
56
58
  }
57
59
  /** Numbered-typing fallback (trellis-onboard-interactive-picker design.md
58
60
  * D2) — used only when the terminal can't support the raw-mode picker
@@ -144,48 +146,47 @@ async function promptForManagedAgentsReal(candidates, alreadyManaged) {
144
146
  }
145
147
  /**
146
148
  * Resolves which categories (skills, instructions) to migrate from a
147
- * resolved source (trellis-migrate-category-selection). Unlike the two
148
- * pickers above, there is no numbered-text fallback to preserve parity
149
- * with — this concept never existed before this change, so "can't
150
- * prompt" simply means "default to whichever kind(s) actually have real
151
- * content, silently" (design.md D4). The picker itself is only offered
152
- * when the choice is meaningful — both kinds present, a capable
153
- * terminal, and not a `--json` run (design.md D5). An empty result is a
154
- * valid answer: "skip migrate for this run" (design.md D6), left for
155
- * the caller to act on.
149
+ * resolved source (trellis-migrate-category-selection, extended to a
150
+ * third category by trellis-onboard-mcp-memory design.md D2). Unlike the
151
+ * two pickers above, there is no numbered-text fallback to preserve
152
+ * parity with — this concept never existed before this change, so
153
+ * "can't prompt" simply means "default to whichever kind(s) actually
154
+ * have real content, silently" (design.md D4). The picker itself is
155
+ * only offered when the choice is meaningful — two or more kinds
156
+ * present, a capable terminal, and not a `--json` run (design.md D5).
157
+ * An empty result is a valid answer: "skip migrate for this run"
158
+ * (design.md D6), left for the caller to act on.
156
159
  */
157
160
  async function resolveMigrateCategories(source, opts) {
158
- const wantsSkills = source.skillCount > 0;
159
- const wantsInstructions = source.hasRealInstructions;
160
- // The choice is only meaningful when both kinds are real — same gate
161
- // for the injected test seam as for the real picker, mirroring how
162
- // `promptForAgent`/`promptForManagedAgents` are only ever consulted
163
- // when their own real prompt would actually apply.
164
- if (wantsSkills && wantsInstructions && !opts.json) {
161
+ // Built dynamically, in skill/instructions/mcp display order — not a
162
+ // fixed two- or three-slot structure, so a fourth category some day
163
+ // would only need an entry here, not a rewritten branch (design.md D2).
164
+ const candidates = [];
165
+ if (source.skillCount > 0)
166
+ candidates.push({ kind: "skill", label: "skills" });
167
+ if (source.hasRealInstructions)
168
+ candidates.push({ kind: "instructions", label: "instructions" });
169
+ if (source.mcpServerCount > 0)
170
+ candidates.push({ kind: "mcp", label: "mcp" });
171
+ // The choice is only meaningful when two or more kinds are real — same
172
+ // gate for the injected test seam as for the real picker, mirroring
173
+ // how `promptForAgent`/`promptForManagedAgents` are only ever
174
+ // consulted when their own real prompt would actually apply.
175
+ if (candidates.length > 1 && !opts.json) {
165
176
  if (opts.promptForMigrateCategories) {
166
177
  return opts.promptForMigrateCategories(source);
167
178
  }
168
179
  if (canUseInteractivePicker()) {
169
180
  console.log(`Which categories should be migrated from ${source.agent}? Space to toggle, Enter to confirm:`);
170
- const indices = await runMultiSelectPicker(["skills", "instructions"], [true, true]);
181
+ const indices = await runMultiSelectPicker(candidates.map((c) => c.label), candidates.map(() => true));
171
182
  if (indices === null) {
172
183
  console.log("cancelled, no changes made");
173
184
  process.exit(1);
174
185
  }
175
- const kinds = [];
176
- if (indices.includes(0))
177
- kinds.push("skill");
178
- if (indices.includes(1))
179
- kinds.push("instructions");
180
- return kinds;
186
+ return indices.map((i) => candidates[i].kind);
181
187
  }
182
188
  }
183
- const kinds = [];
184
- if (wantsSkills)
185
- kinds.push("skill");
186
- if (wantsInstructions)
187
- kinds.push("instructions");
188
- return kinds;
189
+ return candidates.map((c) => c.kind);
189
190
  }
190
191
  /** Shared by `--manage` and the interactive prompt's answer — same
191
192
  * grammar either way (design.md D4): a real agent id list, comma- or
@@ -343,6 +344,15 @@ export async function collectOnboardPlan(opts = {}) {
343
344
  const syncReport = await collectSyncReport({ homeDir, dryRun: opts.dryRun, managedAgents, backupSession });
344
345
  const mcpSyncReport = await collectMcpSyncReport({ homeDir, dryRun: opts.dryRun, managedAgents, backupSession });
345
346
  backupSession?.finalize();
347
+ // Independent of `managedAgents` — the shared memory server is not
348
+ // per-agent (design.md D4, trellis-onboard-mcp-memory), unlike
349
+ // sync/mcp-sync above. Runs unconditionally once reached; an
350
+ // unconfigured `memory` server is a legitimate "nothing to do yet"
351
+ // state (design.md D5), not gated on any prior step here.
352
+ const memorySyncResult = collectMemorySyncResult(homeDir);
353
+ if (!opts.dryRun) {
354
+ applyMemorySync(memorySyncResult);
355
+ }
346
356
  // Read-only, no dryRun concept — same report either way, run last since
347
357
  // it audits the config mcp sync just wrote (or, on --dry-run, whatever
348
358
  // was already there before this run).
@@ -357,6 +367,7 @@ export async function collectOnboardPlan(opts = {}) {
357
367
  migrateSkipped,
358
368
  syncReport,
359
369
  mcpSyncReport,
370
+ memorySyncResult,
360
371
  secretsAuditReport,
361
372
  };
362
373
  }
@@ -370,9 +381,11 @@ export async function runOnboard(opts = {}) {
370
381
  }
371
382
  if (result.refusal)
372
383
  return { exitCode: 1 };
384
+ const memorySyncConflict = result.memorySyncResult?.configured === true && result.memorySyncResult.plan.items.some((i) => i.action === "conflict");
373
385
  const hasConflict = (result.migratePlan?.items.some((i) => i.action === "conflict") ?? false) ||
374
386
  (result.syncReport?.reports.some((r) => r.items.some((i) => i.action === "conflict")) ?? false) ||
375
387
  (result.mcpSyncReport?.reports.some((r) => r.items.some((i) => i.action === "conflict")) ?? false) ||
388
+ memorySyncConflict ||
376
389
  (result.secretsAuditReport?.findings.length ?? 0) > 0;
377
390
  return { exitCode: hasConflict ? 1 : 0 };
378
391
  }
@@ -431,6 +444,10 @@ function printResult(result, dryRun) {
431
444
  console.log("\nmcp sync");
432
445
  printMcpSyncReport(result.mcpSyncReport, false);
433
446
  }
447
+ if (result.memorySyncResult) {
448
+ console.log("\nmemory sync");
449
+ printMemorySyncResult(result.memorySyncResult, false);
450
+ }
434
451
  if (result.secretsAuditReport) {
435
452
  console.log("\nsecrets audit");
436
453
  printSecretsAuditReport(result.secretsAuditReport);
@@ -17,17 +17,23 @@ $ trellis onboard
17
17
 
18
18
  Runs `init`, detects which of Claude Code/Codex/Kiro/pi are on this machine,
19
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:
20
+ `mcp sync`, `memory sync`, and `secrets audit` — the whole onboarding path,
21
+ no follow-up commands to type by hand:
22
22
 
23
23
  1. **Migration source** — read from, at most one, never written back to.
24
+ "Real content" means skills, custom instructions, *or* real MCP servers
25
+ — an agent whose only real content is its MCP servers is still a valid
26
+ source.
24
27
  - **No agent has real content**: skipped — canonical starts from `init`'s
25
28
  placeholder.
26
29
  - **Exactly one agent has real content**: auto-selected, no prompt.
27
30
  - **Two or more**: prompts you to choose (Up/Down or j/k, Enter to confirm,
28
31
  on a real terminal that supports it — falls back to a numbered
29
32
  type-a-digit prompt otherwise), or pass `--agent <id>` to skip the
30
- prompt entirely.
33
+ prompt entirely. Which *categories* to bring in from that source
34
+ (skills, instructions, mcp) is its own checkbox, shown only when the
35
+ source has real content in two or more of them — with just one, that
36
+ one is migrated unprompted.
31
37
  2. **Managed set** — zero or more agents to actually write to. Always an
32
38
  explicit choice: pass `--manage <ids>` (comma-separated, e.g. `--manage
33
39
  pi,codex`) or `--manage none`, or answer the interactive checkbox prompt
@@ -54,6 +60,9 @@ sync
54
60
  mcp sync
55
61
  ✅ pi — already in sync
56
62
 
63
+ memory sync
64
+ no "memory" MCP server with static_env.MEMORY_FILE_PATH configured in servers.yaml — see schema/servers.example.yaml
65
+
57
66
  secrets audit
58
67
  ✅ no findings — every present agent's real config and every declared env var passed all checks
59
68
  ```
@@ -63,9 +72,9 @@ it's present and was the migration source, but it isn't managed, so it's
63
72
  never even probed as a sync target, not just left with zero items.
64
73
 
65
74
  Add `--dry-run` to preview the entire chain — init/migrate/sync/mcp
66
- sync, including what would be written to `~/.trellis/managed.yaml` — with
67
- zero writes anywhere (secrets audit is always read-only, with or without
68
- the flag).
75
+ sync/memory sync, including what would be written to
76
+ `~/.trellis/managed.yaml` — with zero writes anywhere (secrets audit is
77
+ always read-only, with or without the flag).
69
78
 
70
79
  **A managed agent's own real content still isn't overwritten.** If you
71
80
  explicitly include the source in `--manage`, sync still never overwrites
package/docs/roadmap.md CHANGED
@@ -899,6 +899,35 @@ byte-identical for both transports, now covered by its own test), so
899
899
  that distinction was never stored in the first place. 350/350 tests
900
900
  passing.
901
901
 
902
+ **P17 (✅ done, archived
903
+ [2026-09-13-trellis-onboard-mcp-memory](../openspec/changes/archive/2026-09-13-trellis-onboard-mcp-memory/)):
904
+ `onboard` catches up to P15/P16 — mcp and memory were never wired into
905
+ its own orchestration.** Found by live dogfooding on a real machine:
906
+ `onboard`'s migrate-category picker hardcoded `["skills",
907
+ "instructions"]` — `mcp` was invisible to it regardless of how many real
908
+ MCP servers the source agent had — and `trellis memory sync` was never
909
+ called anywhere in `onboard`'s chain at all, despite its own docstring
910
+ naming a complete chain. `OnboardAgentSummary` gains an
911
+ `mcpServerCount`, computed via the already-exported
912
+ `collectMigratePlan(agent, homeDir, ["mcp"])` rather than a second reader
913
+ dispatch — so it always agrees with what `migrate` itself would do, and
914
+ an agent whose only real content is MCP servers is now a valid migration
915
+ source (previously invisible to `hasContent` entirely).
916
+ `resolveMigrateCategories` was rewritten from a fixed two-slot structure
917
+ to a dynamically-built list of whichever of skill/instructions/mcp have
918
+ real content, offering the checkbox only when two or more do — not a
919
+ bolted-on special case for a third option. `memory sync` runs as a new
920
+ final chained stage, after `mcp sync` and before `secrets audit`,
921
+ independent of the resolved managed-agent set (the memory server is
922
+ shared, not per-agent); `runMemorySync`'s inline printing was extracted
923
+ into an exported `printMemorySyncResult` so `onboard` reuses the exact
924
+ same output format rather than a second, drifting copy of it —
925
+ `memory.ts`'s own standalone command behavior is unchanged, confirmed by
926
+ its existing tests passing unmodified. No change to `migrate.ts`,
927
+ `mcp.ts`, or `memory.ts`'s own command behavior — purely `onboard`'s
928
+ orchestration layer catching up to commands that already existed. 357/357
929
+ tests passing.
930
+
902
931
  | Phase | Deliverable | Depends on |
903
932
  |---|---|---|
904
933
  | P0 | ✅ `trellis doctor` — read-only, opt-in-for-handshakes scan of all four agents' current skills/MCP/instructions state, reports drift and duplicates | nothing |
@@ -918,6 +947,7 @@ passing.
918
947
  | P14 | ✅ MCP server lifecycle parity with skills: ownership-tracked safe removal on sync (migrate-in closed separately by P16) | P2, P13 |
919
948
  | 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
949
  | 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 |
950
+ | P17 | ✅ `onboard` orchestration catches up to P15/P16: three-way migrate-category selection (mcp added), `memory sync` wired in as a chained stage | P15, P16 |
921
951
 
922
952
  No dates. This is scoped by verification milestones, not calendar time.
923
953
  Execution order for P11–P16: P11 → P12 → P13 → P14 → P15 → P16 — CRUD
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-trellis",
3
- "version": "0.3.0",
3
+ "version": "0.4.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",