agent-trellis 0.14.1 → 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 (55) 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/zcode.d.ts +12 -1
  7. package/dist/adapters/zcode.js +7 -6
  8. package/dist/cli.js +38 -5
  9. package/dist/commands/doctor.d.ts +9 -1
  10. package/dist/commands/doctor.js +43 -6
  11. package/dist/commands/init.d.ts +6 -0
  12. package/dist/commands/init.js +24 -0
  13. package/dist/commands/mcp.d.ts +44 -1
  14. package/dist/commands/mcp.js +145 -33
  15. package/dist/commands/migrate.d.ts +12 -0
  16. package/dist/commands/migrate.js +8 -4
  17. package/dist/commands/migrateWorkspace.d.ts +40 -0
  18. package/dist/commands/migrateWorkspace.js +197 -0
  19. package/dist/commands/rollback.js +65 -35
  20. package/dist/commands/skill.d.ts +14 -0
  21. package/dist/commands/skill.js +71 -0
  22. package/dist/commands/sync.d.ts +16 -1
  23. package/dist/commands/sync.js +48 -15
  24. package/dist/core/adapter.d.ts +14 -0
  25. package/dist/core/canonical.d.ts +20 -34
  26. package/dist/core/canonical.js +18 -92
  27. package/dist/core/mcpServerDef.d.ts +39 -0
  28. package/dist/core/mcpServerDef.js +93 -0
  29. package/dist/core/types.d.ts +39 -4
  30. package/dist/lib/mcpMigrateRead.d.ts +25 -0
  31. package/dist/lib/mcpMigrateRead.js +9 -2
  32. package/dist/lib/skillProvider.js +18 -5
  33. package/dist/lib/workspace.d.ts +39 -0
  34. package/dist/lib/workspace.js +158 -0
  35. package/dist/lib/workspaceConfig.d.ts +45 -0
  36. package/dist/lib/workspaceConfig.js +98 -0
  37. package/dist/lib/workspaceDoctor.d.ts +44 -0
  38. package/dist/lib/workspaceDoctor.js +94 -0
  39. package/dist/lib/workspaceFiles.d.ts +38 -0
  40. package/dist/lib/workspaceFiles.js +264 -0
  41. package/dist/lib/workspaceMcp.d.ts +57 -0
  42. package/dist/lib/workspaceMcp.js +277 -0
  43. package/dist/lib/workspaceMerge.d.ts +37 -0
  44. package/dist/lib/workspaceMerge.js +98 -0
  45. package/dist/lib/workspaceMigrate.d.ts +118 -0
  46. package/dist/lib/workspaceMigrate.js +423 -0
  47. package/dist/pi-bridge/bundle.js +258 -96
  48. package/docs/architecture.md +111 -49
  49. package/docs/getting-started.md +86 -14
  50. package/docs/mcp-oauth-matrix.md +12 -1
  51. package/docs/overview.zh-CN.md +17 -0
  52. package/docs/research.md +28 -6
  53. package/docs/roadmap.md +17 -5
  54. package/package.json +1 -1
  55. package/docs/implementation-plan.md +0 -341
@@ -0,0 +1,423 @@
1
+ /**
2
+ * `trellis migrate --workspace`'s scanner + planner (trellis-workspace-
3
+ * scope tasks 4.1–4.5): inventories every managed agent's project-level
4
+ * MCP config plus the root `AGENTS.md`, classifies each finding, and
5
+ * produces a pure plan — no writes anywhere, so `--dry-run` prints the
6
+ * byte-identical plan a real run would execute.
7
+ *
8
+ * Classification (one `action` per item):
9
+ * - `already-managed` — the in-repo manifest (`.agents/.trellis-
10
+ * rendered.json`) records Trellis having rendered
11
+ * this entry, or `.agents/` already holds it
12
+ * - `skip-local` — a `*.local.*` sibling of a scanned config path;
13
+ * listed, never read (task 4.5)
14
+ * - `skip-unsupported` — malformed entry (e.g. stdio without command)
15
+ * - `extract-secret` — a reject-pattern literal in `staticEnv`: value
16
+ * goes to the global secret store, the committed
17
+ * `.agents/config.json` only ever gets `${VAR}`
18
+ * - `identical-to-global` — deep-equal to the global canonical entry;
19
+ * default drop, `--dedup override` imports it as
20
+ * an intentional shadow (task 4.4)
21
+ * - `conflict` — same name with differing defs across agents
22
+ * (task 4.3, resolved only by `--choose
23
+ * name=agentId`), or a reject-pattern literal in
24
+ * a field other than `staticEnv` (unlike the
25
+ * global migrate, `.agents/config.json` is
26
+ * COMMITTED — a plaintext literal must never be
27
+ * written into it)
28
+ * - `create` — will import into `.agents/`
29
+ *
30
+ * Secret handling invariant (task 4.2): no literal secret value ever rides
31
+ * on a plan item — extraction re-reads the source file at apply time,
32
+ * exactly like the global migrate's design.md D9.
33
+ */
34
+ import { execFileSync } from "node:child_process";
35
+ import { existsSync, readFileSync } from "node:fs";
36
+ import { basename, join } from "node:path";
37
+ import { ALL_AGENTS } from "../core/types.js";
38
+ import { loadCanonicalSource } from "../core/canonical.js";
39
+ import { findLiteralSecret } from "../adapters/mcpPlan.js";
40
+ import { fromServerDefYaml } from "../core/mcpServerDef.js";
41
+ import { deepEqual } from "./deepEqual.js";
42
+ import { fromRichJsonServerDef, readRichJsonMcpDefs } from "./mcpMigrateRead.js";
43
+ import { findSection, readServerEnvTable } from "./tomlSection.js";
44
+ import { loadWorkspaceRendered, workspaceMcpConfigPath } from "./workspaceMcp.js";
45
+ import { readWorkspaceConfigJson, workspaceConfigPath, WORKSPACE_AGENTS_MD_TEMPLATE } from "./workspaceConfig.js";
46
+ import { INSTRUCTIONS_MARKER } from "./workspaceFiles.js";
47
+ const VAR_REF_RE = /^\$\{([A-Za-z_][A-Za-z0-9_]*)\}$/;
48
+ /**
49
+ * Inverse of `renderServerSection` for a project-level `.codex/config.toml`
50
+ * (task 4.1): no TOML parser dependency exists in this repo (design D2,
51
+ * trellis-mcp-sync-p2), and the global codex reader shells out to `codex
52
+ * mcp list --json` — useless for an arbitrary project file. Hand-rolled
53
+ * against the exact bounded shape `renderServerSection` emits, tolerant of
54
+ * anything else (unknown keys skipped, malformed lines skipped).
55
+ */
56
+ const CODEX_SERVER_HEADER_RE = /^\[mcp_servers\.((?:"(?:[^"\\]|\\.)*"|[A-Za-z0-9_-]+))\]\s*$/gm;
57
+ const CODEX_KV_RE = /^([A-Za-z0-9_-]+)\s*=\s*(.+)$/;
58
+ function readCodexTomlMcpDefs(configPath) {
59
+ const result = { entries: [], unsupported: [] };
60
+ if (!existsSync(configPath))
61
+ return result;
62
+ const content = readFileSync(configPath, "utf-8");
63
+ for (const match of content.matchAll(CODEX_SERVER_HEADER_RE)) {
64
+ const segment = match[1];
65
+ const name = segment.startsWith('"') ? JSON.parse(segment) : segment;
66
+ const range = findSection(content, `mcp_servers.${segment}`);
67
+ if (!range)
68
+ continue;
69
+ const fields = {};
70
+ for (const line of content.split("\n").slice(range.start + 1, range.end + 1)) {
71
+ const kv = CODEX_KV_RE.exec(line.trim());
72
+ if (!kv)
73
+ continue;
74
+ try {
75
+ fields[kv[1]] = JSON.parse(kv[2]);
76
+ }
77
+ catch {
78
+ // not a JSON-shaped literal (a date, a bare string…) — not a shape
79
+ // renderServerSection ever emits; skip the key, keep the rest
80
+ }
81
+ }
82
+ let def;
83
+ if (typeof fields.url === "string") {
84
+ def = { transport: "http", url: fields.url };
85
+ if (typeof fields.bearer_token_env_var === "string") {
86
+ def.headers = { Authorization: `Bearer \${${fields.bearer_token_env_var}}` };
87
+ }
88
+ }
89
+ else if (typeof fields.command === "string") {
90
+ def = { transport: "stdio", command: fields.command };
91
+ if (Array.isArray(fields.args) && fields.args.every((a) => typeof a === "string") && fields.args.length > 0) {
92
+ def.args = fields.args;
93
+ }
94
+ if (Array.isArray(fields.env_vars) && fields.env_vars.every((a) => typeof a === "string") && fields.env_vars.length > 0) {
95
+ def.env = fields.env_vars;
96
+ }
97
+ }
98
+ else {
99
+ result.unsupported.push({ name, reason: "entry has neither url nor command — malformed, skipped" });
100
+ continue;
101
+ }
102
+ // The adjacent `[mcp_servers.<name>.env]` table inverts to envAliases
103
+ // (`${VAR}`-shaped values) + staticEnv (everything else) — the exact
104
+ // split renderServerSection performs on the way out.
105
+ const envTable = readServerEnvTable(content, name);
106
+ for (const [key, value] of Object.entries(envTable ?? {})) {
107
+ const ref = VAR_REF_RE.exec(value);
108
+ if (ref) {
109
+ def.envAliases = { ...(def.envAliases ?? {}), [key]: ref[1] };
110
+ }
111
+ else {
112
+ def.staticEnv = { ...(def.staticEnv ?? {}), [key]: value };
113
+ }
114
+ }
115
+ result.entries.push({ name, def });
116
+ }
117
+ return result;
118
+ }
119
+ /** zcode's project config nests the rich-JSON shape under `mcp.servers`. */
120
+ function readZcodeWorkspaceMcpDefs(configPath) {
121
+ const result = { entries: [], unsupported: [] };
122
+ if (!existsSync(configPath))
123
+ return result;
124
+ let parsed;
125
+ try {
126
+ parsed = JSON.parse(readFileSync(configPath, "utf-8"));
127
+ }
128
+ catch {
129
+ return result; // unparsable file — nothing to scan, the sync guard reports it
130
+ }
131
+ const servers = parsed?.mcp?.servers ?? {};
132
+ for (const [name, raw] of Object.entries(servers)) {
133
+ const def = fromRichJsonServerDef(raw);
134
+ if (def) {
135
+ result.entries.push({ name, def });
136
+ }
137
+ else {
138
+ result.unsupported.push({ name, reason: "stdio entry has no command — malformed, skipped" });
139
+ }
140
+ }
141
+ return result;
142
+ }
143
+ function readAgentProjectMcpDefs(agentId, configPath) {
144
+ if (!existsSync(configPath))
145
+ return { entries: [], unsupported: [] };
146
+ if (agentId === "codex")
147
+ return readCodexTomlMcpDefs(configPath);
148
+ if (agentId === "zcode")
149
+ return readZcodeWorkspaceMcpDefs(configPath);
150
+ return readRichJsonMcpDefs(configPath);
151
+ }
152
+ /** `.mcp.json` → `.mcp.local.json`; `config.toml` → `config.local.toml`. */
153
+ function localSiblingPath(configPath) {
154
+ return configPath.replace(/\.(json|toml)$/, ".local.$1");
155
+ }
156
+ /**
157
+ * Apply-time re-read of one agent's project MCP defs (task 4.2, mirroring
158
+ * the global migrate's D9): an extraction's real value is recovered from
159
+ * the source file HERE, at apply time — it never rides on the plan object
160
+ * a `--json`/`--dry-run` caller might log or serialize.
161
+ */
162
+ export function readWorkspaceAgentMcpDefsForApply(workspaceRoot, agentId) {
163
+ return readAgentProjectMcpDefs(agentId, workspaceMcpConfigPath(agentId, workspaceRoot));
164
+ }
165
+ /**
166
+ * Inventories all six agents' project-level MCP config paths (the same
167
+ * paths workspace delivery writes, so migration and delivery can never
168
+ * disagree about where an agent reads) plus their `*.local.*` siblings.
169
+ * Pure: reads files, writes nothing.
170
+ */
171
+ export function scanWorkspaceMcp(workspaceRoot) {
172
+ const scan = { entries: [], unsupported: [], localFiles: [] };
173
+ for (const agentId of ALL_AGENTS) {
174
+ const configPath = workspaceMcpConfigPath(agentId, workspaceRoot);
175
+ const localPath = localSiblingPath(configPath);
176
+ if (existsSync(localPath))
177
+ scan.localFiles.push(localPath);
178
+ const { entries, unsupported } = readAgentProjectMcpDefs(agentId, configPath);
179
+ for (const { name, def } of entries)
180
+ scan.entries.push({ agent: agentId, name, def, sourcePath: configPath });
181
+ for (const { name, reason } of unsupported)
182
+ scan.unsupported.push({ agent: agentId, name, reason, sourcePath: configPath });
183
+ }
184
+ return scan;
185
+ }
186
+ /** `TRELLIS_<SERVER>_<KEY>` — the global migrate's D11 scheme, reused
187
+ * verbatim so a server extracted at either scope follows one convention. */
188
+ function synthesizeVarName(serverName, key) {
189
+ const part = (s) => s.toUpperCase().replace(/[^A-Z0-9]+/g, "_").replace(/^_+|_+$/g, "") || "X";
190
+ return `TRELLIS_${part(serverName)}_${part(key)}`;
191
+ }
192
+ /** Moves `sourceKey` out of `staticEnv` into a name-only `env` reference —
193
+ * the same safe-def transform as the global migrate's `extractedMcpDef`. */
194
+ function extractedWorkspaceMcpDef(def, sourceKey, varName) {
195
+ const remainingStaticEnv = { ...(def.staticEnv ?? {}) };
196
+ delete remainingStaticEnv[sourceKey];
197
+ const result = { ...def, env: [...(def.env ?? []), varName] };
198
+ if (Object.keys(remainingStaticEnv).length > 0) {
199
+ result.staticEnv = remainingStaticEnv;
200
+ }
201
+ else {
202
+ delete result.staticEnv;
203
+ }
204
+ return result;
205
+ }
206
+ /** Task 4.2: is this source file git-tracked? A literal secret in a
207
+ * tracked file has already been published to every clone — the report
208
+ * must advise rotation. Any failure (not a repo, git absent) means "not
209
+ * tracked", never an error. */
210
+ function isGitTracked(workspaceRoot, path) {
211
+ try {
212
+ execFileSync("git", ["ls-files", "--error-unmatch", "--", path], { cwd: workspaceRoot, stdio: "pipe" });
213
+ return true;
214
+ }
215
+ catch {
216
+ return false;
217
+ }
218
+ }
219
+ export function collectWorkspaceMigratePlan(opts) {
220
+ const { workspaceRoot, homeDir, secretsPolicy } = opts;
221
+ const choose = opts.choose ?? {};
222
+ const dedup = opts.dedup ?? "drop";
223
+ const items = [];
224
+ const scan = scanWorkspaceMcp(workspaceRoot);
225
+ const manifest = loadWorkspaceRendered(workspaceRoot);
226
+ const workspaceConfig = readWorkspaceConfigJson(workspaceRoot);
227
+ const existingWorkspaceServers = workspaceConfig.mcp?.servers ?? {};
228
+ let globalServers = {};
229
+ try {
230
+ globalServers = loadCanonicalSource({ homeDir, workspaceDir: null }).mcp.servers;
231
+ }
232
+ catch {
233
+ // No usable global canonical — dedup simply has nothing to compare
234
+ // against; migration into `.agents/` still works.
235
+ }
236
+ for (const localPath of scan.localFiles) {
237
+ items.push({
238
+ kind: "mcp",
239
+ name: basename(localPath),
240
+ action: "skip-local",
241
+ detail: "personal local file — listed, never read into canonical",
242
+ sources: [],
243
+ });
244
+ }
245
+ for (const entry of scan.unsupported) {
246
+ items.push({
247
+ kind: "mcp",
248
+ name: entry.name,
249
+ action: "skip-unsupported",
250
+ detail: `${entry.agent}: ${entry.reason}`,
251
+ sources: [entry.agent],
252
+ });
253
+ }
254
+ // Group scanned entries by server name; classification then happens
255
+ // once per name, not once per (agent, name) pair.
256
+ const byName = new Map();
257
+ for (const entry of scan.entries) {
258
+ byName.set(entry.name, [...(byName.get(entry.name) ?? []), entry]);
259
+ }
260
+ for (const [name, entries] of [...byName.entries()].sort(([a], [b]) => a.localeCompare(b))) {
261
+ const sources = entries.map((e) => e.agent);
262
+ // Task 4.1: entries Trellis itself rendered (in-repo manifest) or
263
+ // already imported (`.agents/config.json`) need no migration.
264
+ const unrecorded = entries.filter((e) => manifest.mcp?.[e.agent]?.[e.name] === undefined);
265
+ if (unrecorded.length === 0) {
266
+ items.push({ kind: "mcp", name, action: "already-managed", detail: "Trellis-rendered already (in-repo manifest)", sources });
267
+ continue;
268
+ }
269
+ // Task 4.3: one name, differing defs across agents → the user must
270
+ // pick a side explicitly; the plan shows them side by side.
271
+ const distinct = unrecorded.filter((e, i) => unrecorded.findIndex((o) => deepEqual(o.def, e.def)) === i);
272
+ let chosen;
273
+ if (distinct.length > 1) {
274
+ const picked = choose[name];
275
+ const pickedEntry = picked !== undefined ? unrecorded.find((e) => e.agent === picked) : undefined;
276
+ if (!pickedEntry) {
277
+ const defs = {};
278
+ for (const e of distinct)
279
+ defs[e.agent] = e.def;
280
+ items.push({
281
+ kind: "mcp",
282
+ name,
283
+ action: "conflict",
284
+ detail: `defined differently by ${distinct.map((e) => e.agent).join(" and ")} — no def is imported until one is chosen`,
285
+ remediation: `re-run with \`--choose ${name}=<${distinct.map((e) => e.agent).join("|")}>\` after comparing the side-by-side definitions`,
286
+ sources,
287
+ defs,
288
+ });
289
+ continue;
290
+ }
291
+ chosen = pickedEntry;
292
+ }
293
+ else {
294
+ chosen = distinct[0];
295
+ }
296
+ const def = chosen.def;
297
+ const chosenSource = distinct.length > 1 ? chosen.agent : undefined;
298
+ // Already in `.agents/config.json` with an identical def → nothing to
299
+ // do. The file stores the snake_case yaml shape — convert before
300
+ // comparing against the scanned `McpServerDef`.
301
+ const existingYaml = existingWorkspaceServers[name];
302
+ if (existingYaml !== undefined && deepEqual(fromServerDefYaml(name, existingYaml, workspaceConfigPath(workspaceRoot)), def)) {
303
+ items.push({ kind: "mcp", name, action: "already-managed", detail: "already in .agents/config.json with an identical definition", sources, ...(chosenSource ? { chosenSource } : {}) });
304
+ continue;
305
+ }
306
+ // Task 4.2: reject-pattern literal interception. `staticEnv` has a
307
+ // natural extraction target (its own dict key → TRELLIS_<SERVER>_<KEY>
308
+ // in the global secret store); a literal anywhere else can never be
309
+ // written into the COMMITTED `.agents/config.json`, so it conflicts.
310
+ const secretMatch = findLiteralSecret(def);
311
+ if (secretMatch && secretMatch.field !== "staticEnv") {
312
+ items.push({
313
+ kind: "mcp",
314
+ name,
315
+ action: "conflict",
316
+ detail: `${secretMatch.field} holds a value matching a secrets reject pattern (${secretMatch.label}) — .agents/config.json is committed, so the literal is never written`,
317
+ remediation: `rotate the value, replace it with a \${VAR} reference in the source file, then re-run migrate`,
318
+ sources,
319
+ ...(chosenSource ? { chosenSource } : {}),
320
+ });
321
+ continue;
322
+ }
323
+ const tracked = isGitTracked(workspaceRoot, chosen.sourcePath);
324
+ const rotationAdvisory = secretMatch && tracked
325
+ ? `${chosen.sourcePath} is git-tracked — the ${secretMatch.label} value in it was already committed; rotate it`
326
+ : undefined;
327
+ if (secretMatch) {
328
+ const key = secretMatch.key;
329
+ const varName = synthesizeVarName(name, key);
330
+ items.push({
331
+ kind: "mcp",
332
+ name,
333
+ action: "extract-secret",
334
+ detail: `will extract "${key}" to the global secret store as "${varName}" and import the server with a \${${varName}} reference — the literal never lands under .agents/`,
335
+ sources,
336
+ ...(chosenSource ? { chosenSource } : {}),
337
+ mcpDef: extractedWorkspaceMcpDef(def, key, varName),
338
+ extractVarName: varName,
339
+ extractTargetPath: secretsPolicy.envFile ?? join(homeDir, ".trellis", "mcp", "servers.local.env"),
340
+ extractSourceKey: key,
341
+ extractSourceAgent: chosen.agent,
342
+ ...(rotationAdvisory ? { rotationAdvisory } : {}),
343
+ });
344
+ continue;
345
+ }
346
+ // Task 4.4: deep-equal to the global layer — importing would create a
347
+ // redundant shadow. Default drop; `--dedup override` records the
348
+ // intentional-shadow choice and imports anyway.
349
+ const global = globalServers[name];
350
+ if (global !== undefined && deepEqual(global, def)) {
351
+ if (dedup === "override") {
352
+ items.push({
353
+ kind: "mcp",
354
+ name,
355
+ action: "create",
356
+ detail: "identical to the global entry — imported anyway as an intentional shadow (--dedup override)",
357
+ sources,
358
+ ...(chosenSource ? { chosenSource } : {}),
359
+ mcpDef: def,
360
+ dedupChoice: "override",
361
+ });
362
+ }
363
+ else {
364
+ items.push({
365
+ kind: "mcp",
366
+ name,
367
+ action: "identical-to-global",
368
+ detail: "identical to the global entry — dropped (the global layer already provides it); re-run with --dedup override to import it as an intentional shadow",
369
+ sources,
370
+ dedupChoice: "drop",
371
+ });
372
+ }
373
+ continue;
374
+ }
375
+ items.push({
376
+ kind: "mcp",
377
+ name,
378
+ action: "create",
379
+ detail: `will import into .agents/config.json (from ${chosen.agent}${unrecorded.length > 1 ? "; other agents agree" : ""})`,
380
+ sources,
381
+ ...(chosenSource ? { chosenSource } : {}),
382
+ mcpDef: def,
383
+ });
384
+ }
385
+ // Root AGENTS.md → .agents/AGENTS.md. A marker-headed file is Trellis's
386
+ // own rendering — already managed. An unmarked one is imported, unless
387
+ // `.agents/AGENTS.md` already holds real (non-template) content.
388
+ const rootAgentsMd = join(workspaceRoot, "AGENTS.md");
389
+ if (existsSync(rootAgentsMd)) {
390
+ const content = readFileSync(rootAgentsMd, "utf-8");
391
+ if (content.startsWith(INSTRUCTIONS_MARKER)) {
392
+ items.push({ kind: "instructions", name: "AGENTS.md", action: "already-managed", detail: "root AGENTS.md is already Trellis-rendered", sources: [] });
393
+ }
394
+ else {
395
+ const targetPath = join(workspaceRoot, ".agents", "AGENTS.md");
396
+ const existingInstructions = existsSync(targetPath) ? readFileSync(targetPath, "utf-8") : undefined;
397
+ if (existingInstructions === content) {
398
+ items.push({ kind: "instructions", name: "AGENTS.md", action: "already-managed", detail: ".agents/AGENTS.md already holds the same content", sources: [] });
399
+ }
400
+ else if (existingInstructions !== undefined && existingInstructions !== WORKSPACE_AGENTS_MD_TEMPLATE) {
401
+ items.push({
402
+ kind: "instructions",
403
+ name: "AGENTS.md",
404
+ action: "conflict",
405
+ detail: "both root AGENTS.md and .agents/AGENTS.md hold real content — not merged automatically",
406
+ remediation: "merge the two by hand into .agents/AGENTS.md, remove the root file, then re-run migrate",
407
+ sources: [],
408
+ });
409
+ }
410
+ else {
411
+ items.push({
412
+ kind: "instructions",
413
+ name: "AGENTS.md",
414
+ action: "create",
415
+ detail: "will import root AGENTS.md into .agents/AGENTS.md and re-render the root file marker-headed",
416
+ sources: [],
417
+ sourceContent: content,
418
+ });
419
+ }
420
+ }
421
+ }
422
+ return { workspaceRoot, items };
423
+ }