agent-trellis 0.14.0 → 0.15.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 (57) hide show
  1. package/dist/adapters/claude-code.js +4 -3
  2. package/dist/adapters/codex.js +4 -3
  3. package/dist/adapters/kimi-code.js +4 -3
  4. package/dist/adapters/kiro.js +5 -4
  5. package/dist/adapters/pi.js +4 -3
  6. package/dist/adapters/piMcp.d.ts +6 -3
  7. package/dist/adapters/piMcp.js +10 -3
  8. package/dist/adapters/zcode.d.ts +12 -1
  9. package/dist/adapters/zcode.js +7 -6
  10. package/dist/cli.js +38 -5
  11. package/dist/commands/doctor.d.ts +9 -1
  12. package/dist/commands/doctor.js +43 -6
  13. package/dist/commands/init.d.ts +6 -0
  14. package/dist/commands/init.js +24 -0
  15. package/dist/commands/mcp.d.ts +44 -1
  16. package/dist/commands/mcp.js +145 -33
  17. package/dist/commands/migrate.d.ts +12 -0
  18. package/dist/commands/migrate.js +8 -4
  19. package/dist/commands/migrateWorkspace.d.ts +40 -0
  20. package/dist/commands/migrateWorkspace.js +197 -0
  21. package/dist/commands/rollback.js +65 -35
  22. package/dist/commands/skill.d.ts +14 -0
  23. package/dist/commands/skill.js +71 -0
  24. package/dist/commands/sync.d.ts +16 -1
  25. package/dist/commands/sync.js +48 -15
  26. package/dist/core/adapter.d.ts +14 -0
  27. package/dist/core/canonical.d.ts +20 -32
  28. package/dist/core/canonical.js +18 -86
  29. package/dist/core/mcpServerDef.d.ts +39 -0
  30. package/dist/core/mcpServerDef.js +93 -0
  31. package/dist/core/types.d.ts +43 -4
  32. package/dist/lib/mcpMigrateRead.d.ts +25 -0
  33. package/dist/lib/mcpMigrateRead.js +9 -2
  34. package/dist/lib/skillProvider.js +18 -5
  35. package/dist/lib/workspace.d.ts +39 -0
  36. package/dist/lib/workspace.js +158 -0
  37. package/dist/lib/workspaceConfig.d.ts +45 -0
  38. package/dist/lib/workspaceConfig.js +98 -0
  39. package/dist/lib/workspaceDoctor.d.ts +44 -0
  40. package/dist/lib/workspaceDoctor.js +94 -0
  41. package/dist/lib/workspaceFiles.d.ts +38 -0
  42. package/dist/lib/workspaceFiles.js +264 -0
  43. package/dist/lib/workspaceMcp.d.ts +57 -0
  44. package/dist/lib/workspaceMcp.js +277 -0
  45. package/dist/lib/workspaceMerge.d.ts +37 -0
  46. package/dist/lib/workspaceMerge.js +98 -0
  47. package/dist/lib/workspaceMigrate.d.ts +118 -0
  48. package/dist/lib/workspaceMigrate.js +423 -0
  49. package/dist/pi-bridge/bundle.js +262 -96
  50. package/docs/architecture.md +111 -49
  51. package/docs/getting-started.md +102 -14
  52. package/docs/mcp-oauth-matrix.md +12 -1
  53. package/docs/overview.zh-CN.md +17 -0
  54. package/docs/research.md +28 -6
  55. package/docs/roadmap.md +17 -5
  56. package/package.json +1 -1
  57. package/docs/implementation-plan.md +0 -341
@@ -33,14 +33,23 @@ export interface RunMcpSyncOptions {
33
33
  /** Onboard-only seam — see RunSyncOptions.backupSession. Never a CLI
34
34
  * flag. */
35
35
  backupSession?: BackupSession;
36
+ /** Force global-only behavior even inside a workspace (`--global-only`). */
37
+ globalOnly?: boolean;
38
+ /** Explicit workspace root — the test/sandbox seam (see RunSyncOptions). */
39
+ workspaceDir?: string | null;
40
+ /** `--check`: plan only, write nothing, exit non-zero on any stale or
41
+ * missing rendered artifact (trellis-workspace-scope design D7). */
42
+ check?: boolean;
36
43
  }
37
44
  export interface AgentMcpSyncReport {
38
45
  agent: AgentId;
39
46
  present: boolean;
40
47
  items: AdapterPlanItem[];
48
+ warnings?: string[];
41
49
  }
42
50
  export interface McpSyncReport {
43
51
  reports: AgentMcpSyncReport[];
52
+ workspaceRoot?: string;
44
53
  }
45
54
  export declare function collectMcpSyncReport(opts?: RunMcpSyncOptions): Promise<McpSyncReport>;
46
55
  export declare function runMcpSync(opts?: RunMcpSyncOptions): Promise<{
@@ -49,7 +58,7 @@ export declare function runMcpSync(opts?: RunMcpSyncOptions): Promise<{
49
58
  /** Exported so `onboard` prints an mcp-sync report identically to running
50
59
  * `mcp sync` standalone, instead of a second, easily-drifting copy of this
51
60
  * formatting. */
52
- export declare function printReport(report: McpSyncReport, dryRun: boolean): void;
61
+ export declare function printReport(report: McpSyncReport, dryRun: boolean, check?: boolean): void;
53
62
  export interface McpListEntry {
54
63
  name: string;
55
64
  transport: Transport;
@@ -147,8 +156,30 @@ export interface McpAddPlan {
147
156
  * write failure — design.md's open question, resolved in favor of
148
157
  * refusing early with a specific reason. No `--force`: an existing name
149
158
  * always refuses, ever (design.md D4).
159
+ *
160
+ * Split into two steps so `mcp add --workspace` reuses the identical
161
+ * flag validation/def construction and only swaps which file's existing
162
+ * entries it conflicts against (trellis-workspace-scope task 3.4):
163
+ * `buildMcpAddDef` is the pure part, `collectMcpAddPlan` adds the
164
+ * global-canonical existence check.
150
165
  */
166
+ export declare function buildMcpAddDef(name: string | undefined, raw: McpAddRawArgs): {
167
+ ok: true;
168
+ name: string;
169
+ def: McpServerDef;
170
+ } | {
171
+ ok: false;
172
+ plan: McpAddPlan;
173
+ };
151
174
  export declare function collectMcpAddPlan(name: string | undefined, raw: McpAddRawArgs, homeDir?: string): McpAddPlan;
175
+ /** The workspace twin of `collectMcpAddPlan` (trellis-workspace-scope
176
+ * task 3.4): identical validation, but the existence check runs against
177
+ * `.agents/config.json` — compared through the same snake_case
178
+ * serialization the file stores, so a re-add of the same definition is
179
+ * a clean "already-present" no-op. A global same-name entry is NOT a
180
+ * conflict here (workspace shadowing a global entry is a legal,
181
+ * documented merge outcome), just noted in the detail. */
182
+ export declare function collectMcpAddWorkspacePlan(name: string | undefined, raw: McpAddRawArgs, workspaceRoot: string, homeDir?: string): McpAddPlan;
152
183
  export type McpSetAuthAction = "updated" | "already-set" | "not-found" | "invalid-input";
153
184
  export interface McpSetAuthPlan {
154
185
  name: string;
@@ -269,10 +300,22 @@ export declare function applyMcpAddWithSync(plan: McpAddPlan, opts?: {
269
300
  dryRun?: boolean;
270
301
  backupSession?: BackupSession;
271
302
  }): Promise<McpAddOutcome>;
303
+ /** Workspace twin of `applyMcpAddWithSync` (trellis-workspace-scope task
304
+ * 3.4): writes `.agents/config.json` instead of `servers.yaml`, then
305
+ * cascades the same mcp sync — which, run inside the workspace, renders
306
+ * only the project-level targets for the workspace layer (plus the usual
307
+ * global-layer targets; the agent does its own layering). */
308
+ export declare function applyMcpAddWorkspaceWithSync(plan: McpAddPlan, workspaceRoot: string, opts?: {
309
+ homeDir?: string;
310
+ dryRun?: boolean;
311
+ backupSession?: BackupSession;
312
+ }): Promise<McpAddOutcome>;
272
313
  export declare function runMcpAdd(name: string | undefined, raw: McpAddRawArgs, opts?: {
273
314
  homeDir?: string;
274
315
  json?: boolean;
275
316
  dryRun?: boolean;
317
+ workspace?: boolean;
318
+ workspaceDir?: string;
276
319
  }): Promise<{
277
320
  exitCode: number;
278
321
  }>;
@@ -27,6 +27,10 @@ import { PiAdapter } from "../adapters/pi.js";
27
27
  import { KimiCodeAdapter } from "../adapters/kimi-code.js";
28
28
  import { ZcodeAdapter } from "../adapters/zcode.js";
29
29
  import { openBackupSession } from "../lib/backup.js";
30
+ import { findWorkspaceRoot } from "../lib/workspace.js";
31
+ import { applyWorkspaceMcp, planWorkspaceMcp, workspacePrecedenceWarnings } from "../lib/workspaceMcp.js";
32
+ import { readWorkspaceConfigJson, upsertWorkspaceServer } from "../lib/workspaceConfig.js";
33
+ import { toServerDefYaml } from "../core/mcpServerDef.js";
30
34
  const ADAPTER_FACTORY = {
31
35
  "claude-code": (homeDir) => new ClaudeCodeAdapter(homeDir),
32
36
  codex: (homeDir) => new CodexAdapter(homeDir),
@@ -42,14 +46,16 @@ function buildAdapters(homeDir, managedAgents) {
42
46
  }
43
47
  export async function collectMcpSyncReport(opts = {}) {
44
48
  const homeDir = opts.homeDir ?? homedir();
45
- const loaded = loadCanonicalSource(homeDir);
49
+ const workspaceDir = opts.globalOnly ? null : (opts.workspaceDir !== undefined ? opts.workspaceDir : findWorkspaceRoot(process.cwd()));
50
+ const loaded = loadCanonicalSource({ homeDir, workspaceDir });
46
51
  const canonical = {
47
52
  ...loaded,
48
53
  ...(opts.managedAgents ? { managedAgents: opts.managedAgents } : {}),
49
54
  ...(opts.mcp ? { mcp: opts.mcp } : {}),
50
55
  };
51
56
  const reports = [];
52
- const ownSession = !opts.dryRun && !opts.backupSession ? openBackupSession(homeDir, "mcp-sync") : undefined;
57
+ const writeMode = !opts.dryRun && !opts.check;
58
+ const ownSession = writeMode && !opts.backupSession ? openBackupSession(homeDir, "mcp-sync") : undefined;
53
59
  const backup = opts.backupSession ?? ownSession;
54
60
  for (const adapter of buildAdapters(homeDir, canonical.managedAgents)) {
55
61
  const probeResult = await adapter.probe();
@@ -57,14 +63,17 @@ export async function collectMcpSyncReport(opts = {}) {
57
63
  reports.push({ agent: adapter.id, present: false, items: [] });
58
64
  continue;
59
65
  }
60
- const items = (await adapter.plan(canonical)).filter((item) => item.kind === "mcp");
61
- if (!opts.dryRun) {
62
- await adapter.apply(items, backup);
66
+ const globalItems = (await adapter.plan(canonical)).filter((item) => item.kind === "mcp");
67
+ const wsItems = planWorkspaceMcp(adapter.id, canonical);
68
+ if (writeMode) {
69
+ await adapter.apply(globalItems, backup);
70
+ applyWorkspaceMcp(adapter.id, canonical, wsItems, backup);
63
71
  }
64
- reports.push({ agent: adapter.id, present: true, items });
72
+ const warnings = workspacePrecedenceWarnings(adapter.id, canonical);
73
+ reports.push({ agent: adapter.id, present: true, items: [...globalItems, ...wsItems], ...(warnings.length > 0 ? { warnings } : {}) });
65
74
  }
66
75
  ownSession?.finalize();
67
- return { reports };
76
+ return { reports, ...(canonical.workspace ? { workspaceRoot: canonical.workspace.root } : {}) };
68
77
  }
69
78
  export async function runMcpSync(opts = {}) {
70
79
  let report;
@@ -79,22 +88,31 @@ export async function runMcpSync(opts = {}) {
79
88
  console.log(JSON.stringify(report, null, 2));
80
89
  }
81
90
  else {
82
- printReport(report, opts.dryRun ?? false);
91
+ printReport(report, opts.dryRun ?? false, opts.check ?? false);
83
92
  }
84
93
  const hasConflict = report.reports.some((r) => r.items.some((i) => i.action === "conflict"));
94
+ if (opts.check) {
95
+ const hasPending = report.reports.some((r) => r.items.some((i) => i.action !== "conflict"));
96
+ return { exitCode: hasPending || hasConflict ? 1 : 0 };
97
+ }
85
98
  return { exitCode: hasConflict ? 1 : 0 };
86
99
  }
87
100
  /** Exported so `onboard` prints an mcp-sync report identically to running
88
101
  * `mcp sync` standalone, instead of a second, easily-drifting copy of this
89
102
  * formatting. */
90
- export function printReport(report, dryRun) {
103
+ export function printReport(report, dryRun, check = false) {
91
104
  if (dryRun)
92
105
  console.log("[dry run]");
106
+ if (check)
107
+ console.log("[check]");
108
+ if (report.workspaceRoot) {
109
+ console.log(`Workspace layer: ${report.workspaceRoot} (.agents/)`);
110
+ }
93
111
  if (report.reports.length === 0) {
94
112
  console.log("No managed agents yet — run `trellis onboard` or list agent ids in ~/.trellis/managed.yaml.");
95
113
  return;
96
114
  }
97
- for (const { agent, present, items } of report.reports) {
115
+ for (const { agent, present, items, warnings } of report.reports) {
98
116
  if (!present) {
99
117
  console.log(`— ${agent} (not installed)`);
100
118
  continue;
@@ -104,18 +122,27 @@ export function printReport(report, dryRun) {
104
122
  const conflicts = items.filter((i) => i.action === "conflict");
105
123
  if (created.length === 0 && removed.length === 0 && conflicts.length === 0) {
106
124
  console.log(`✅ ${agent} — already in sync`);
107
- continue;
108
125
  }
109
- const icon = conflicts.length > 0 ? "⚠️ " : "✅";
110
- console.log(`${icon} ${agent} — ${created.length} created/updated, ${removed.length} removed, ${conflicts.length} conflict(s)`);
111
- for (const item of [...created, ...removed, ...conflicts]) {
112
- console.log(` - [${item.action}] ${item.description}`);
126
+ else {
127
+ const icon = conflicts.length > 0 ? "⚠️ " : "✅";
128
+ console.log(`${icon} ${agent} — ${created.length} created/updated, ${removed.length} removed, ${conflicts.length} conflict(s)`);
129
+ for (const item of [...created, ...removed, ...conflicts]) {
130
+ console.log(` - [${item.action}]${item.layer === "workspace" ? " (workspace)" : ""} ${item.description}`);
131
+ // Conflicts carry the actionable fix (e.g. the unmarked-overwrite
132
+ // guard's "run migrate --workspace or remove the entry") — printing
133
+ // only the description would leave the user stuck at "left untouched".
134
+ if (item.remediation)
135
+ console.log(` → ${item.remediation}`);
136
+ }
137
+ // pi does not watch mcp.json (verified 2026-10-11): without this hint
138
+ // a changed config silently only takes effect on the next pi launch
139
+ // (trellis-pi-native-mcp-migration design.md D5).
140
+ if (agent === "pi" && (created.length > 0 || removed.length > 0)) {
141
+ console.log(" pi does not watch mcp.json — restart pi or run /reload to pick up MCP changes");
142
+ }
113
143
  }
114
- // pi does not watch mcp.json (verified 2026-10-11): without this hint
115
- // a changed config silently only takes effect on the next pi launch
116
- // (trellis-pi-native-mcp-migration design.md D5).
117
- if (agent === "pi" && (created.length > 0 || removed.length > 0)) {
118
- console.log(" pi does not watch mcp.json — restart pi or run /reload to pick up MCP changes");
144
+ for (const warning of warnings ?? []) {
145
+ console.log(` ⚠ ${warning}`);
119
146
  }
120
147
  }
121
148
  }
@@ -260,37 +287,43 @@ const TRANSPORTS = ["stdio", "http", "sse"];
260
287
  * write failure — design.md's open question, resolved in favor of
261
288
  * refusing early with a specific reason. No `--force`: an existing name
262
289
  * always refuses, ever (design.md D4).
290
+ *
291
+ * Split into two steps so `mcp add --workspace` reuses the identical
292
+ * flag validation/def construction and only swaps which file's existing
293
+ * entries it conflicts against (trellis-workspace-scope task 3.4):
294
+ * `buildMcpAddDef` is the pure part, `collectMcpAddPlan` adds the
295
+ * global-canonical existence check.
263
296
  */
264
- export function collectMcpAddPlan(name, raw, homeDir = homedir()) {
297
+ export function buildMcpAddDef(name, raw) {
265
298
  if (!name) {
266
- return { name: "(none)", action: "invalid-input", detail: "a name is required: trellis mcp add <name> --transport ..." };
299
+ return { ok: false, plan: { name: "(none)", action: "invalid-input", detail: "a name is required: trellis mcp add <name> --transport ..." } };
267
300
  }
268
301
  if (!raw.transport || !TRANSPORTS.includes(raw.transport)) {
269
- return { name, action: "invalid-input", detail: `--transport must be one of: ${TRANSPORTS.join(", ")}` };
302
+ return { ok: false, plan: { name, action: "invalid-input", detail: `--transport must be one of: ${TRANSPORTS.join(", ")}` } };
270
303
  }
271
304
  const transport = raw.transport;
272
305
  if (transport === "stdio" && !raw.command) {
273
- return { name, action: "invalid-input", detail: "--transport stdio requires --command" };
306
+ return { ok: false, plan: { name, action: "invalid-input", detail: "--transport stdio requires --command" } };
274
307
  }
275
308
  if ((transport === "http" || transport === "sse") && !raw.url) {
276
- return { name, action: "invalid-input", detail: `--transport ${transport} requires --url` };
309
+ return { ok: false, plan: { name, action: "invalid-input", detail: `--transport ${transport} requires --url` } };
277
310
  }
278
311
  if (raw.auth !== undefined && raw.auth !== "oauth") {
279
- return { name, action: "invalid-input", detail: `--auth must be "oauth" when provided (got ${raw.auth})` };
312
+ return { ok: false, plan: { name, action: "invalid-input", detail: `--auth must be "oauth" when provided (got ${raw.auth})` } };
280
313
  }
281
314
  if (raw.auth === "oauth" && transport === "stdio") {
282
- return { name, action: "invalid-input", detail: "--auth oauth requires an http or sse MCP server" };
315
+ return { ok: false, plan: { name, action: "invalid-input", detail: "--auth oauth requires an http or sse MCP server" } };
283
316
  }
284
317
  const agentsList = parseCsv(raw.agents);
285
318
  if (agentsList) {
286
319
  for (const id of agentsList) {
287
320
  if (!ALL_AGENTS.includes(id)) {
288
- return { name, action: "invalid-input", detail: `--agents "${id}" is not a recognized agent id (${ALL_AGENTS.join(", ")})` };
321
+ return { ok: false, plan: { name, action: "invalid-input", detail: `--agents "${id}" is not a recognized agent id (${ALL_AGENTS.join(", ")})` } };
289
322
  }
290
323
  }
291
324
  }
292
325
  if (raw.enabled !== undefined && raw.enabled !== "true" && raw.enabled !== "false") {
293
- return { name, action: "invalid-input", detail: `--enabled must be "true" or "false" (got ${raw.enabled})` };
326
+ return { ok: false, plan: { name, action: "invalid-input", detail: `--enabled must be "true" or "false" (got ${raw.enabled})` } };
294
327
  }
295
328
  const def = {
296
329
  transport,
@@ -304,15 +337,46 @@ export function collectMcpAddPlan(name, raw, homeDir = homedir()) {
304
337
  ...(raw.enabled !== undefined ? { enabled: raw.enabled === "true" } : {}),
305
338
  ...(agentsList ? { agents: agentsList } : {}),
306
339
  };
340
+ return { ok: true, name, def };
341
+ }
342
+ export function collectMcpAddPlan(name, raw, homeDir = homedir()) {
343
+ const built = buildMcpAddDef(name, raw);
344
+ if (!built.ok)
345
+ return built.plan;
346
+ const def = built.def;
307
347
  const canonical = loadCanonicalSource(homeDir);
308
- const existing = canonical.mcp.servers[name];
348
+ const existing = canonical.mcp.servers[built.name];
309
349
  if (existing) {
310
350
  if (JSON.stringify(existing) === JSON.stringify(def)) {
311
- return { name, action: "already-present", detail: "canonical entry is already identical" };
351
+ return { name: built.name, action: "already-present", detail: "canonical entry is already identical" };
312
352
  }
313
- return { name, action: "conflict", detail: `servers.yaml already has "${name}" with different settings — resolve by hand (no --force, design.md D4)` };
353
+ return { name: built.name, action: "conflict", detail: `servers.yaml already has "${built.name}" with different settings — resolve by hand (no --force, design.md D4)` };
314
354
  }
315
- return { name, action: "create", detail: "will add to servers.yaml", def };
355
+ return { name: built.name, action: "create", detail: "will add to servers.yaml", def };
356
+ }
357
+ /** The workspace twin of `collectMcpAddPlan` (trellis-workspace-scope
358
+ * task 3.4): identical validation, but the existence check runs against
359
+ * `.agents/config.json` — compared through the same snake_case
360
+ * serialization the file stores, so a re-add of the same definition is
361
+ * a clean "already-present" no-op. A global same-name entry is NOT a
362
+ * conflict here (workspace shadowing a global entry is a legal,
363
+ * documented merge outcome), just noted in the detail. */
364
+ export function collectMcpAddWorkspacePlan(name, raw, workspaceRoot, homeDir = homedir()) {
365
+ const built = buildMcpAddDef(name, raw);
366
+ if (!built.ok)
367
+ return built.plan;
368
+ const def = built.def;
369
+ const config = readWorkspaceConfigJson(workspaceRoot);
370
+ const existing = config.mcp?.servers?.[built.name];
371
+ if (existing) {
372
+ if (JSON.stringify(existing) === JSON.stringify(toServerDefYaml(def))) {
373
+ return { name: built.name, action: "already-present", detail: ".agents/config.json entry is already identical" };
374
+ }
375
+ return { name: built.name, action: "conflict", detail: `.agents/config.json already has "${built.name}" with different settings — resolve by hand (no --force)` };
376
+ }
377
+ const global = loadCanonicalSource(homeDir).mcp.servers[built.name];
378
+ const shadowNote = global ? ` (shadows the global "${built.name}" for this project)` : "";
379
+ return { name: built.name, action: "create", detail: `will add to .agents/config.json${shadowNote}`, def };
316
380
  }
317
381
  /**
318
382
  * Sets or clears the OAuth classification, and optionally the
@@ -587,8 +651,56 @@ export async function applyMcpAddWithSync(plan, opts = {}) {
587
651
  }
588
652
  return { plan, ...(writeError ? { writeError } : {}), ...(syncReport ? { sync: syncReport } : {}) };
589
653
  }
654
+ /** Workspace twin of `applyMcpAddWithSync` (trellis-workspace-scope task
655
+ * 3.4): writes `.agents/config.json` instead of `servers.yaml`, then
656
+ * cascades the same mcp sync — which, run inside the workspace, renders
657
+ * only the project-level targets for the workspace layer (plus the usual
658
+ * global-layer targets; the agent does its own layering). */
659
+ export async function applyMcpAddWorkspaceWithSync(plan, workspaceRoot, opts = {}) {
660
+ const homeDir = opts.homeDir ?? homedir();
661
+ const ownSession = !opts.dryRun && !opts.backupSession && plan.action === "create" ? openBackupSession(homeDir, "mcp-add") : undefined;
662
+ const session = opts.backupSession ?? ownSession;
663
+ if (!opts.dryRun && plan.action === "create" && plan.def) {
664
+ upsertWorkspaceServer(workspaceRoot, plan.name, plan.def, session);
665
+ }
666
+ ownSession?.finalize();
667
+ const planFailed = plan.action === "conflict" || plan.action === "invalid-input";
668
+ let syncReport;
669
+ if (!planFailed && plan.action === "create" && existsSync(join(homeDir, ".trellis"))) {
670
+ syncReport = await collectMcpSyncReport({ homeDir, workspaceDir: workspaceRoot, dryRun: opts.dryRun });
671
+ }
672
+ return { plan, ...(syncReport ? { sync: syncReport } : {}) };
673
+ }
590
674
  export async function runMcpAdd(name, raw, opts = {}) {
591
675
  const homeDir = opts.homeDir ?? homedir();
676
+ if (opts.workspace) {
677
+ const workspaceRoot = opts.workspaceDir ?? findWorkspaceRoot(process.cwd());
678
+ if (!workspaceRoot) {
679
+ console.error("no .agents/ found here or above — run `trellis init --workspace` first (or pass --workspace from inside the repo)");
680
+ return { exitCode: 1 };
681
+ }
682
+ let plan;
683
+ try {
684
+ plan = collectMcpAddWorkspacePlan(name, raw, workspaceRoot, homeDir);
685
+ }
686
+ catch (err) {
687
+ console.error(err instanceof Error ? err.message : String(err));
688
+ return { exitCode: 1 };
689
+ }
690
+ const { sync: syncReport } = await applyMcpAddWorkspaceWithSync(plan, workspaceRoot, { homeDir, dryRun: opts.dryRun });
691
+ const planFailed = plan.action === "conflict" || plan.action === "invalid-input";
692
+ if (opts.json) {
693
+ console.log(JSON.stringify(syncReport ? { ...plan, workspace: workspaceRoot, sync: syncReport } : { ...plan, workspace: workspaceRoot }, null, 2));
694
+ }
695
+ else {
696
+ console.log(`${opts.dryRun ? "[dry run] " : ""}mcp add ${plan.name} --workspace`);
697
+ console.log(` [${plan.action}] ${plan.detail}`);
698
+ if (syncReport)
699
+ printReport(syncReport, opts.dryRun ?? false);
700
+ }
701
+ const syncConflict = syncReport?.reports.some((r) => r.items.some((i) => i.action === "conflict")) ?? false;
702
+ return { exitCode: planFailed || syncConflict ? 1 : 0 };
703
+ }
592
704
  let plan;
593
705
  try {
594
706
  plan = collectMcpAddPlan(name, raw, homeDir);
@@ -7,6 +7,18 @@
7
7
  import type { AgentId, McpServerDef } from "../core/types.js";
8
8
  import type { BackupSession } from "../lib/backup.js";
9
9
  import { type NativeMemoryCandidate, type NativeMemoryDiscovery } from "../lib/nativeMemory.js";
10
+ /** Where a `staticEnv` literal secret's real value goes when
11
+ * `secrets.policy.yaml` doesn't already have its own `env_file`
12
+ * (trellis-migrate-extract-static-env-secrets design.md D3) — a sibling
13
+ * of `servers.yaml`, so "the mcp stuff" stays one directory rather than
14
+ * a second, disconnected location. `~`-form for what gets written into
15
+ * `secrets.policy.yaml` itself (portable across machines); resolved form
16
+ * for real file I/O. */
17
+ export declare const DEFAULT_LOCAL_SECRETS_ENV_FILE_TILDE = "~/.trellis/mcp/servers.local.env";
18
+ /** Exported for `migrate --workspace` (trellis-workspace-scope task 4.2) —
19
+ * its staticEnv extraction lands in the same default store under the same
20
+ * naming scheme, never a second location. */
21
+ export declare function defaultLocalSecretsEnvFilePath(homeDir: string): string;
10
22
  export type MigrateAction = "create" | "skip-symlink" | "skip-case-broken" | "skip-unsupported" | "already-migrated" | "reclassify" | "conflict" | "extract-secret";
11
23
  /** Internal kind naming, unchanged since before `--only` existed
12
24
  * (trellis-migrate-category-selection design.md D2) — the CLI-facing
@@ -29,8 +29,11 @@ import { discoverNativeMemory, MAX_NATIVE_MEMORY_BYTES, renderNativeMemory } fro
29
29
  * a second, disconnected location. `~`-form for what gets written into
30
30
  * `secrets.policy.yaml` itself (portable across machines); resolved form
31
31
  * for real file I/O. */
32
- const DEFAULT_LOCAL_SECRETS_ENV_FILE_TILDE = "~/.trellis/mcp/servers.local.env";
33
- function defaultLocalSecretsEnvFilePath(homeDir) {
32
+ export const DEFAULT_LOCAL_SECRETS_ENV_FILE_TILDE = "~/.trellis/mcp/servers.local.env";
33
+ /** Exported for `migrate --workspace` (trellis-workspace-scope task 4.2) —
34
+ * its staticEnv extraction lands in the same default store under the same
35
+ * naming scheme, never a second location. */
36
+ export function defaultLocalSecretsEnvFilePath(homeDir) {
34
37
  return join(homeDir, ".trellis", "mcp", "servers.local.env");
35
38
  }
36
39
  /** Picks the rc file `ensureShellEnvSource` writes its one pointer block
@@ -54,8 +57,9 @@ const PROBES = {
54
57
  "kimi-code": (homeDir) => kimiCodeProbe.probe(homeDir),
55
58
  zcode: (homeDir) => zcodeProbe.probe(homeDir),
56
59
  };
57
- /** pi has no static MCP config to read at all (roadmap.md P14/
58
- * trellis-migrate-mcp-servers) — deliberately absent, not an oversight;
60
+ /** pi's mcp.json migrate-in is not implemented (roadmap.md P14/
61
+ * trellis-migrate-mcp-servers predates pi 1.1.0's native MCP config) —
62
+ * deliberately absent, not an oversight;
59
63
  * `collectMigratePlan` skips the `mcp` category entirely for pi. */
60
64
  const MCP_READERS = {
61
65
  "claude-code": readClaudeCodeMcpDefs,
@@ -0,0 +1,40 @@
1
+ /**
2
+ * `trellis migrate --workspace` — absorbs a repo's existing project-level
3
+ * agent config (all six agents' MCP files + root AGENTS.md) into
4
+ * `.agents/`, then re-renders (trellis-workspace-scope group 4).
5
+ *
6
+ * Flow: plan (pure, `src/lib/workspaceMigrate.ts`) → refuse on unresolved
7
+ * conflicts → one backup session named `workspace-<repo-hash>` covering
8
+ * every write (task 4.6 — `.agents/` writes, the secret-store extraction,
9
+ * the marker-headed AGENTS.md rewrite, and the cascade render, so a
10
+ * single `trellis rollback` restores originals and removes generated
11
+ * `.agents/` content) → post-migration equivalence report (task 4.7).
12
+ */
13
+ import type { AgentId } from "../core/types.js";
14
+ import type { WorkspaceMigratePlan } from "../lib/workspaceMigrate.js";
15
+ export interface RunMigrateWorkspaceOptions {
16
+ /** Test/sandbox seam — CLI resolves via findWorkspaceRoot(cwd), falling
17
+ * back to cwd itself so a legacy project without `.agents/` yet can
18
+ * still be scanned (apply then creates `.agents/` through backup). */
19
+ workspaceDir?: string;
20
+ /** Raw `name=agentId` strings from repeated `--choose`. */
21
+ choose?: string[];
22
+ /** Raw `--dedup` value: `drop` (default) or `override`. */
23
+ dedup?: string;
24
+ dryRun?: boolean;
25
+ json?: boolean;
26
+ homeDir?: string;
27
+ }
28
+ export interface WorkspaceMigrateEquivalence {
29
+ /** Per agent: server names found in its project config before the
30
+ * migration vs. what the post-migration render now delivers. */
31
+ agents: {
32
+ agent: AgentId;
33
+ before: string[];
34
+ after: string[];
35
+ }[];
36
+ }
37
+ export declare function runMigrateWorkspace(opts?: RunMigrateWorkspaceOptions): Promise<{
38
+ exitCode: number;
39
+ }>;
40
+ export declare function printWorkspaceMigratePlan(plan: WorkspaceMigratePlan, dryRun: boolean): void;