agents.yaml 0.1.0 → 0.2.1

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/AGENTS.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  This package provides the `agents` CLI for maintaining an `agents.yaml` file.
4
4
 
5
- `agents.yaml` is a curated table of contents for active agent-readable documentation. It does not define a new instruction language, replace `AGENTS.md`, or automatically load every dependency document.
5
+ `agents.yaml` is a curated table of contents for promoted agent-readable documentation. It does not define a new instruction language, replace `AGENTS.md`, or automatically load every dependency document.
6
6
 
7
7
  The file format is intentionally small:
8
8
 
@@ -11,9 +11,10 @@ version: 1
11
11
 
12
12
  documents:
13
13
  - path: ./node_modules/example-package/AGENTS.md
14
+ description: Useful package context from example-package's package.json.
14
15
  ```
15
16
 
16
- Agents should treat only the paths listed in `documents` as active supplemental guidance for the project.
17
+ Agents should treat paths listed in `documents` as promoted supplemental guidance for the project. Descriptions are human-readable breadcrumbs that explain why the package guidance may be relevant; they are not additional instructions.
17
18
 
18
19
  The CLI can help discover package and local `AGENTS.md` files, add selected paths to `agents.yaml`, remove paths, initialize the root breadcrumb, and validate that referenced files still exist.
19
20
 
package/README.md CHANGED
@@ -30,12 +30,13 @@ version: 1
30
30
 
31
31
  documents:
32
32
  - path: ./node_modules/react/AGENTS.md
33
+ description: React is a JavaScript library for building user interfaces.
33
34
  ```
34
35
 
36
+ Descriptions are optional breadcrumbs, usually copied from the package's `package.json`, that make lesser-known package guidance easier to recognize at a glance. Only `path` activates a supplemental guidance document.
37
+
35
38
  Add this breadcrumb to your root `AGENTS.md`:
36
39
 
37
40
  ```md
38
- For dependency-specific and supplemental guidance, consult `./agents.yaml`.
39
-
40
- Only the documents listed there should be considered active external guidance for this project.
41
+ Consult `./agents.yaml` when working with outside dependencies.
41
42
  ```
package/dist/index.mjs CHANGED
@@ -1,5 +1,7 @@
1
1
  #!/usr/bin/env node
2
+ import { MultiSelectPrompt } from "@clack/core";
2
3
  import * as clack from "@clack/prompts";
4
+ import { styleText } from "node:util";
3
5
  import { access, opendir, readFile, writeFile } from "node:fs/promises";
4
6
  import path from "node:path";
5
7
  import * as YAML from "yaml";
@@ -20,11 +22,12 @@ function formatProjectPath(root, target) {
20
22
  //#region src/agents-file.ts
21
23
  const fileSchema = z.object({
22
24
  version: z.literal(1),
23
- documents: z.array(z.object({ path: z.string().min(1) }))
25
+ documents: z.array(z.object({
26
+ path: z.string().min(1),
27
+ description: z.string().min(1).optional()
28
+ }))
24
29
  });
25
- const breadcrumb = `For dependency-specific and supplemental guidance, consult \`./agents.yaml\`.
26
-
27
- Only the documents listed there should be considered active external guidance for this project.`;
30
+ const breadcrumb = `Consult \`./agents.yaml\` when working with outside dependencies.`;
28
31
  async function loadAgentsFile(root) {
29
32
  const filePath = agentsPath(root);
30
33
  try {
@@ -43,14 +46,24 @@ async function loadAgentsFile(root) {
43
46
  async function saveAgentsFile(root, file) {
44
47
  const normalized = {
45
48
  version: file.version,
46
- documents: file.documents.map((doc) => ({ path: doc.path }))
49
+ documents: file.documents.map((doc) => ({
50
+ path: doc.path,
51
+ ...doc.description ? { description: doc.description } : {}
52
+ }))
47
53
  };
48
54
  await writeFile(agentsPath(root), YAML.stringify(normalized, { lineWidth: 0 }), "utf8");
49
55
  }
50
56
  async function addDocuments(root, documents) {
51
57
  const file = await loadAgentsFile(root);
52
58
  const byPath = new Map(file.documents.map((doc) => [doc.path, doc]));
53
- for (const document of documents) byPath.set(document.path, document);
59
+ for (const document of documents) {
60
+ const existing = byPath.get(document.path);
61
+ byPath.set(document.path, {
62
+ path: document.path,
63
+ ...existing?.description ? { description: existing.description } : {},
64
+ ...document.description ? { description: document.description } : {}
65
+ });
66
+ }
54
67
  const next = {
55
68
  version: 1,
56
69
  documents: [...byPath.values()].sort((left, right) => left.path.localeCompare(right.path))
@@ -166,6 +179,10 @@ async function discoverAgentDocuments(root) {
166
179
  await walk(root, root, found);
167
180
  return found.filter((document) => document.path !== "./AGENTS.md").sort((left, right) => left.path.localeCompare(right.path));
168
181
  }
182
+ async function describeAgentDocument(root, agentsDocumentPath) {
183
+ const absolutePath = resolveFromRoot(root, agentsDocumentPath);
184
+ return documentEntry(root, absolutePath, await readPackageDescription(path.dirname(absolutePath)));
185
+ }
169
186
  async function walk(root, directory, found) {
170
187
  let handle;
171
188
  try {
@@ -183,7 +200,7 @@ async function walk(root, directory, found) {
183
200
  if (!skippedDirectories.has(entry.name)) await walk(root, absolutePath, found);
184
201
  continue;
185
202
  }
186
- if (entry.isFile() && entry.name === "AGENTS.md") found.push({ path: formatProjectPath(root, absolutePath) });
203
+ if (entry.isFile() && entry.name === "AGENTS.md") found.push(await documentEntry(root, absolutePath, await readPackageDescription(path.dirname(absolutePath))));
187
204
  }
188
205
  }
189
206
  async function scanDirectNodeModules(root, nodeModulesPath, found) {
@@ -216,9 +233,29 @@ async function addPackageAgentsDocument(root, packagePath, found) {
216
233
  const agentsPath = path.join(packagePath, "AGENTS.md");
217
234
  try {
218
235
  await access(agentsPath);
219
- found.push({ path: formatProjectPath(root, agentsPath) });
236
+ found.push(await documentEntry(root, agentsPath, await readPackageDescription(packagePath)));
220
237
  } catch {}
221
238
  }
239
+ async function documentEntry(root, agentsPath, description) {
240
+ return {
241
+ path: formatProjectPath(root, agentsPath),
242
+ ...description ? { description } : {}
243
+ };
244
+ }
245
+ async function readPackageDescription(packagePath) {
246
+ try {
247
+ const source = await readFile(path.join(packagePath, "package.json"), "utf8");
248
+ const parsed = JSON.parse(source);
249
+ if (!isPackageJson(parsed) || typeof parsed.description !== "string") return void 0;
250
+ const description = parsed.description.trim();
251
+ return description && description.length > 0 ? description : void 0;
252
+ } catch {
253
+ return;
254
+ }
255
+ }
256
+ function isPackageJson(value) {
257
+ return typeof value === "object" && value !== null;
258
+ }
222
259
  //#endregion
223
260
  //#region src/run.ts
224
261
  const helpText = `agents
@@ -231,7 +268,7 @@ Usage:
231
268
  agents remove <path...>
232
269
  agents validate [--json]
233
270
 
234
- agents.yaml is a curated table of contents for active external AGENTS.md guidance.`;
271
+ agents.yaml is a curated table of contents for promoted AGENTS.md guidance.`;
235
272
  async function run(argv) {
236
273
  const parsed = parseArgs(argv);
237
274
  const root = cwd();
@@ -327,23 +364,26 @@ async function commandDiscover(root, json) {
327
364
  clack.outro("No supplemental AGENTS.md files found.");
328
365
  return;
329
366
  }
330
- clack.note(documents.map((doc) => doc.path).join("\n"), `Found ${documents.length}`);
367
+ clack.note(formatDocumentList(documents), `Found ${documents.length}`);
331
368
  clack.outro("Use agents add <path> to enable one.");
332
369
  }
333
370
  async function commandAdd(root, paths) {
334
371
  if (paths.length === 0) throw new Error("add requires at least one AGENTS.md path");
335
- const documents = paths.map((path) => ({ path: formatProjectPath(root, resolveFromRoot(root, path)) }));
372
+ const documents = await Promise.all(paths.map((path) => describeAgentDocument(root, formatProjectPath(root, resolveFromRoot(root, path)))));
336
373
  const file = await addDocuments(root, documents);
337
374
  clack.intro("agents add");
338
- clack.note(file.documents.map((doc) => doc.path).join("\n"), "Active documents");
375
+ clack.note(formatDocumentList(file.documents), "Promoted documents");
339
376
  clack.outro(`Added ${documents.length} document${documents.length === 1 ? "" : "s"}.`);
340
377
  }
378
+ function formatDocumentList(documents) {
379
+ return documents.map((doc) => doc.description ? `${doc.path}\n ${doc.description}` : doc.path).join("\n");
380
+ }
341
381
  async function commandRemove(root, paths) {
342
382
  if (paths.length === 0) throw new Error("remove requires at least one path");
343
383
  const result = await removeDocuments(root, paths.map((path) => formatProjectPath(root, resolveFromRoot(root, path))));
344
384
  clack.intro("agents remove");
345
- clack.note(result.removed.join("\n") || "No matching documents were active.", "Removed");
346
- clack.outro(`agents.yaml now has ${result.file.documents.length} active document${result.file.documents.length === 1 ? "" : "s"}.`);
385
+ clack.note(result.removed.join("\n") || "No matching documents were listed.", "Removed");
386
+ clack.outro(`agents.yaml now has ${result.file.documents.length} promoted document${result.file.documents.length === 1 ? "" : "s"}.`);
347
387
  }
348
388
  async function commandValidate(root, json) {
349
389
  const result = await validateAgentsFile(root);
@@ -391,23 +431,48 @@ async function interactive(root) {
391
431
  const existing = await loadAgentsFile(root);
392
432
  const candidates = (await discoverAgentDocuments(root)).filter((doc) => !existing.documents.some((active) => active.path === doc.path));
393
433
  if (candidates.length === 0) {
394
- clack.outro("No inactive supplemental AGENTS.md files found.");
434
+ clack.outro("No unlisted supplemental AGENTS.md files found.");
395
435
  return;
396
436
  }
397
- const selected = await clack.multiselect({
398
- message: "Choose documents to enable",
399
- options: candidates.map((doc) => ({
400
- value: doc.path,
401
- label: doc.path
402
- })),
403
- required: false
404
- });
405
- if (clack.isCancel(selected) || selected.length === 0) {
437
+ const selected = await chooseDocumentsToEnable(candidates.map((doc) => ({
438
+ value: doc.path,
439
+ label: doc.path
440
+ })));
441
+ if (clack.isCancel(selected) || selected === void 0 || selected.length === 0) {
406
442
  clack.cancel("No documents selected.");
407
443
  return;
408
444
  }
409
445
  await commandAdd(root, selected);
410
446
  }
447
+ function chooseDocumentsToEnable(options) {
448
+ return new MultiSelectPrompt({
449
+ options,
450
+ required: false,
451
+ render() {
452
+ const prefix = `${styleText("cyan", clack.S_BAR)} `;
453
+ const selected = this.value ?? [];
454
+ return `${styleText("gray", clack.S_BAR)}
455
+ ${clack.symbol(this.state)} Choose documents to enable
456
+ ${prefix}${clack.limitOptions({
457
+ options: this.options,
458
+ cursor: this.cursor,
459
+ columnPadding: prefix.length,
460
+ style: (option, active) => styleDocumentOption(option, {
461
+ active,
462
+ selected: selected.includes(option.value)
463
+ })
464
+ }).join(`
465
+ ${prefix}`)}
466
+ ${styleText("cyan", clack.S_BAR_END)}
467
+ `;
468
+ }
469
+ }).prompt();
470
+ }
471
+ function styleDocumentOption(option, state) {
472
+ if (option.disabled) return `${styleText("gray", clack.S_CHECKBOX_INACTIVE)} ${styleText(["strikethrough", "gray"], option.label)}`;
473
+ const checkbox = state.selected ? styleText("green", clack.S_CHECKBOX_SELECTED) : state.active ? styleText("cyan", clack.S_CHECKBOX_ACTIVE) : styleText("dim", clack.S_CHECKBOX_INACTIVE);
474
+ return `${state.active ? styleText("cyan", ">") : " "} ${checkbox} ${state.active ? option.label : styleText("dim", option.label)}`;
475
+ }
411
476
  //#endregion
412
477
  //#region src/index.ts
413
478
  run(process.argv.slice(2)).catch((error) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agents.yaml",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "A CLI for discovering and curating agent-readable documentation in agents.yaml.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -16,25 +16,23 @@
16
16
  "AGENTS.md"
17
17
  ],
18
18
  "type": "module",
19
- "scripts": {
20
- "build": "tsdown",
21
- "dev": "src/index.ts",
22
- "typecheck": "tsc --noEmit",
23
- "check": "tsc --noEmit && tsdown"
24
- },
25
19
  "dependencies": {
20
+ "@clack/core": "1.4.1",
26
21
  "@clack/prompts": "1.5.1",
27
22
  "yaml": "2.9.0",
28
23
  "zod": "4.4.3"
29
24
  },
30
25
  "devDependencies": {
31
26
  "@types/node": "25.9.2",
32
- "tsdown": "0.22.2",
33
27
  "typescript": "6.0.3"
34
28
  },
35
29
  "engines": {
36
30
  "node": "26.3.0",
37
31
  "pnpm": "11.5.2"
38
32
  },
39
- "packageManager": "pnpm@11.5.2"
40
- }
33
+ "scripts": {
34
+ "build": "vp pack",
35
+ "dev": "src/index.ts",
36
+ "test": "vp test"
37
+ }
38
+ }
@@ -1,201 +1,224 @@
1
- import { access, readFile, writeFile } from 'node:fs/promises'
2
- import path from 'node:path'
3
- import * as YAML from 'yaml'
4
- import { z } from 'zod'
5
- import { resolveFromRoot } from './paths.ts'
1
+ import { access, readFile, writeFile } from "node:fs/promises"
2
+ import path from "node:path"
3
+ import * as YAML from "yaml"
4
+ import { z } from "zod"
5
+ import { resolveFromRoot } from "./paths.ts"
6
6
 
7
7
  export type AgentsDocumentEntry = {
8
- path: string
8
+ path: string
9
+ description?: string | undefined
9
10
  }
10
11
 
11
12
  export type AgentsFile = {
12
- version: 1
13
- documents: AgentsDocumentEntry[]
13
+ version: 1
14
+ documents: AgentsDocumentEntry[]
14
15
  }
15
16
 
16
17
  export type ValidationResult = {
17
- ok: boolean
18
- errors: string[]
19
- warnings: string[]
18
+ ok: boolean
19
+ errors: string[]
20
+ warnings: string[]
20
21
  }
21
22
 
22
23
  const fileSchema = z.object({
23
- version: z.literal(1),
24
- documents: z.array(
25
- z.object({
26
- path: z.string().min(1),
27
- }),
28
- ),
24
+ version: z.literal(1),
25
+ documents: z.array(
26
+ z.object({
27
+ path: z.string().min(1),
28
+ description: z.string().min(1).optional(),
29
+ }),
30
+ ),
29
31
  })
30
32
 
31
- const breadcrumb = `For dependency-specific and supplemental guidance, consult \`./agents.yaml\`.
32
-
33
- Only the documents listed there should be considered active external guidance for this project.`
33
+ const breadcrumb = `Consult \`./agents.yaml\` when working with outside dependencies.`
34
34
 
35
35
  export async function loadAgentsFile(root: string): Promise<AgentsFile> {
36
- const filePath = agentsPath(root)
37
-
38
- try {
39
- const source = await readFile(filePath, 'utf8')
40
- const parsed = YAML.parse(source) as unknown
41
- return fileSchema.parse(parsed)
42
- } catch (error) {
43
- if (isNotFound(error)) {
44
- return { version: 1, documents: [] }
45
- }
46
-
47
- if (error instanceof z.ZodError) {
48
- throw new Error(
49
- `Invalid agents.yaml: ${error.issues.map((issue) => issue.message).join(', ')}`,
50
- )
51
- }
52
-
53
- throw error
54
- }
36
+ const filePath = agentsPath(root)
37
+
38
+ try {
39
+ const source = await readFile(filePath, "utf8")
40
+ const parsed = YAML.parse(source) as unknown
41
+ return fileSchema.parse(parsed)
42
+ } catch (error) {
43
+ if (isNotFound(error)) {
44
+ return { version: 1, documents: [] }
45
+ }
46
+
47
+ if (error instanceof z.ZodError) {
48
+ throw new Error(
49
+ `Invalid agents.yaml: ${error.issues.map((issue) => issue.message).join(", ")}`,
50
+ )
51
+ }
52
+
53
+ throw error
54
+ }
55
55
  }
56
56
 
57
- export async function saveAgentsFile(root: string, file: AgentsFile): Promise<void> {
58
- const normalized = {
59
- version: file.version,
60
- documents: file.documents.map((doc) => ({ path: doc.path })),
61
- }
62
-
63
- await writeFile(agentsPath(root), YAML.stringify(normalized, { lineWidth: 0 }), 'utf8')
57
+ export async function saveAgentsFile(
58
+ root: string,
59
+ file: AgentsFile,
60
+ ): Promise<void> {
61
+ const normalized = {
62
+ version: file.version,
63
+ documents: file.documents.map((doc) => ({
64
+ path: doc.path,
65
+ ...(doc.description ? { description: doc.description } : {}),
66
+ })),
67
+ }
68
+
69
+ await writeFile(
70
+ agentsPath(root),
71
+ YAML.stringify(normalized, { lineWidth: 0 }),
72
+ "utf8",
73
+ )
64
74
  }
65
75
 
66
76
  export async function addDocuments(
67
- root: string,
68
- documents: AgentsDocumentEntry[],
77
+ root: string,
78
+ documents: AgentsDocumentEntry[],
69
79
  ): Promise<AgentsFile> {
70
- const file = await loadAgentsFile(root)
71
- const byPath = new Map(file.documents.map((doc) => [doc.path, doc]))
72
-
73
- for (const document of documents) {
74
- byPath.set(document.path, document)
75
- }
76
-
77
- const next = {
78
- version: 1 as const,
79
- documents: [...byPath.values()].sort((left, right) => left.path.localeCompare(right.path)),
80
- }
81
-
82
- await saveAgentsFile(root, next)
83
- return next
80
+ const file = await loadAgentsFile(root)
81
+ const byPath = new Map(file.documents.map((doc) => [doc.path, doc]))
82
+
83
+ for (const document of documents) {
84
+ const existing = byPath.get(document.path)
85
+ byPath.set(document.path, {
86
+ path: document.path,
87
+ ...(existing?.description ? { description: existing.description } : {}),
88
+ ...(document.description ? { description: document.description } : {}),
89
+ })
90
+ }
91
+
92
+ const next = {
93
+ version: 1 as const,
94
+ documents: [...byPath.values()].sort((left, right) =>
95
+ left.path.localeCompare(right.path),
96
+ ),
97
+ }
98
+
99
+ await saveAgentsFile(root, next)
100
+ return next
84
101
  }
85
102
 
86
103
  export async function removeDocuments(
87
- root: string,
88
- paths: string[],
104
+ root: string,
105
+ paths: string[],
89
106
  ): Promise<{ file: AgentsFile; removed: string[] }> {
90
- const file = await loadAgentsFile(root)
91
- const pathSet = new Set(paths)
92
- const removed: string[] = []
93
- const documents = file.documents.filter((doc) => {
94
- if (pathSet.has(doc.path)) {
95
- removed.push(doc.path)
96
- return false
97
- }
98
-
99
- return true
100
- })
101
-
102
- const next = { version: 1 as const, documents }
103
- await saveAgentsFile(root, next)
104
- return { file: next, removed }
107
+ const file = await loadAgentsFile(root)
108
+ const pathSet = new Set(paths)
109
+ const removed: string[] = []
110
+ const documents = file.documents.filter((doc) => {
111
+ if (pathSet.has(doc.path)) {
112
+ removed.push(doc.path)
113
+ return false
114
+ }
115
+
116
+ return true
117
+ })
118
+
119
+ const next = { version: 1 as const, documents }
120
+ await saveAgentsFile(root, next)
121
+ return { file: next, removed }
105
122
  }
106
123
 
107
- export async function validateAgentsFile(root: string): Promise<ValidationResult> {
108
- const errors: string[] = []
109
- const warnings: string[] = []
110
- let file: AgentsFile
111
-
112
- try {
113
- file = await loadAgentsFile(root)
114
- } catch (error) {
115
- return {
116
- ok: false,
117
- errors: [error instanceof Error ? error.message : String(error)],
118
- warnings,
119
- }
120
- }
121
-
122
- const seen = new Set<string>()
123
- for (const [index, document] of file.documents.entries()) {
124
- const label = `documents[${index}] ${document.path}`
125
-
126
- if (seen.has(document.path)) {
127
- errors.push(`${label}: duplicate path`)
128
- }
129
- seen.add(document.path)
130
-
131
- if (path.basename(document.path) !== 'AGENTS.md') {
132
- warnings.push(`${label}: path does not end with AGENTS.md`)
133
- }
134
-
135
- try {
136
- await access(resolveFromRoot(root, document.path))
137
- } catch {
138
- errors.push(`${label}: file does not exist`)
139
- }
140
- }
141
-
142
- try {
143
- const source = await readFile(path.join(root, 'AGENTS.md'), 'utf8')
144
- if (!source.includes('./agents.yaml') && !source.includes('agents.yaml')) {
145
- warnings.push('AGENTS.md does not mention agents.yaml')
146
- }
147
- } catch {
148
- warnings.push('AGENTS.md is missing the agents.yaml breadcrumb')
149
- }
150
-
151
- return {
152
- ok: errors.length === 0,
153
- errors,
154
- warnings,
155
- }
124
+ export async function validateAgentsFile(
125
+ root: string,
126
+ ): Promise<ValidationResult> {
127
+ const errors: string[] = []
128
+ const warnings: string[] = []
129
+ let file: AgentsFile
130
+
131
+ try {
132
+ file = await loadAgentsFile(root)
133
+ } catch (error) {
134
+ return {
135
+ ok: false,
136
+ errors: [error instanceof Error ? error.message : String(error)],
137
+ warnings,
138
+ }
139
+ }
140
+
141
+ const seen = new Set<string>()
142
+ for (const [index, document] of file.documents.entries()) {
143
+ const label = `documents[${index}] ${document.path}`
144
+
145
+ if (seen.has(document.path)) {
146
+ errors.push(`${label}: duplicate path`)
147
+ }
148
+ seen.add(document.path)
149
+
150
+ if (path.basename(document.path) !== "AGENTS.md") {
151
+ warnings.push(`${label}: path does not end with AGENTS.md`)
152
+ }
153
+
154
+ try {
155
+ await access(resolveFromRoot(root, document.path))
156
+ } catch {
157
+ errors.push(`${label}: file does not exist`)
158
+ }
159
+ }
160
+
161
+ try {
162
+ const source = await readFile(path.join(root, "AGENTS.md"), "utf8")
163
+ if (!source.includes("./agents.yaml") && !source.includes("agents.yaml")) {
164
+ warnings.push("AGENTS.md does not mention agents.yaml")
165
+ }
166
+ } catch {
167
+ warnings.push("AGENTS.md is missing the agents.yaml breadcrumb")
168
+ }
169
+
170
+ return {
171
+ ok: errors.length === 0,
172
+ errors,
173
+ warnings,
174
+ }
156
175
  }
157
176
 
158
177
  export async function initProject(
159
- root: string,
160
- options: { force: boolean },
178
+ root: string,
179
+ options: { force: boolean },
161
180
  ): Promise<{ messages: string[] }> {
162
- const messages: string[] = []
163
-
164
- try {
165
- await access(agentsPath(root))
166
- messages.push('agents.yaml already exists')
167
- } catch {
168
- await saveAgentsFile(root, { version: 1, documents: [] })
169
- messages.push('created agents.yaml')
170
- }
171
-
172
- const projectAgentsPath = path.join(root, 'AGENTS.md')
173
- try {
174
- const source = await readFile(projectAgentsPath, 'utf8')
175
- if (source.includes('agents.yaml') && !options.force) {
176
- messages.push('AGENTS.md already mentions agents.yaml')
177
- return { messages }
178
- }
179
-
180
- const next =
181
- source.trimEnd().length === 0
182
- ? `# Project Instructions\n\n${breadcrumb}\n`
183
- : `${source.trimEnd()}\n\n${breadcrumb}\n`
184
- await writeFile(projectAgentsPath, next, 'utf8')
185
- messages.push('updated AGENTS.md')
186
- } catch (error) {
187
- if (!isNotFound(error)) throw error
188
- await writeFile(projectAgentsPath, `# Project Instructions\n\n${breadcrumb}\n`, 'utf8')
189
- messages.push('created AGENTS.md')
190
- }
191
-
192
- return { messages }
181
+ const messages: string[] = []
182
+
183
+ try {
184
+ await access(agentsPath(root))
185
+ messages.push("agents.yaml already exists")
186
+ } catch {
187
+ await saveAgentsFile(root, { version: 1, documents: [] })
188
+ messages.push("created agents.yaml")
189
+ }
190
+
191
+ const projectAgentsPath = path.join(root, "AGENTS.md")
192
+ try {
193
+ const source = await readFile(projectAgentsPath, "utf8")
194
+ if (source.includes("agents.yaml") && !options.force) {
195
+ messages.push("AGENTS.md already mentions agents.yaml")
196
+ return { messages }
197
+ }
198
+
199
+ const next =
200
+ source.trimEnd().length === 0
201
+ ? `# Project Instructions\n\n${breadcrumb}\n`
202
+ : `${source.trimEnd()}\n\n${breadcrumb}\n`
203
+ await writeFile(projectAgentsPath, next, "utf8")
204
+ messages.push("updated AGENTS.md")
205
+ } catch (error) {
206
+ if (!isNotFound(error)) throw error
207
+ await writeFile(
208
+ projectAgentsPath,
209
+ `# Project Instructions\n\n${breadcrumb}\n`,
210
+ "utf8",
211
+ )
212
+ messages.push("created AGENTS.md")
213
+ }
214
+
215
+ return { messages }
193
216
  }
194
217
 
195
218
  function agentsPath(root: string): string {
196
- return path.join(root, 'agents.yaml')
219
+ return path.join(root, "agents.yaml")
197
220
  }
198
221
 
199
222
  function isNotFound(error: unknown): boolean {
200
- return error instanceof Error && 'code' in error && error.code === 'ENOENT'
223
+ return error instanceof Error && "code" in error && error.code === "ENOENT"
201
224
  }