agents.yaml 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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
@@ -20,11 +20,12 @@ function formatProjectPath(root, target) {
20
20
  //#region src/agents-file.ts
21
21
  const fileSchema = z.object({
22
22
  version: z.literal(1),
23
- documents: z.array(z.object({ path: z.string().min(1) }))
23
+ documents: z.array(z.object({
24
+ path: z.string().min(1),
25
+ description: z.string().min(1).optional()
26
+ }))
24
27
  });
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.`;
28
+ const breadcrumb = `Consult \`./agents.yaml\` when working with outside dependencies.`;
28
29
  async function loadAgentsFile(root) {
29
30
  const filePath = agentsPath(root);
30
31
  try {
@@ -43,14 +44,24 @@ async function loadAgentsFile(root) {
43
44
  async function saveAgentsFile(root, file) {
44
45
  const normalized = {
45
46
  version: file.version,
46
- documents: file.documents.map((doc) => ({ path: doc.path }))
47
+ documents: file.documents.map((doc) => ({
48
+ path: doc.path,
49
+ ...doc.description ? { description: doc.description } : {}
50
+ }))
47
51
  };
48
52
  await writeFile(agentsPath(root), YAML.stringify(normalized, { lineWidth: 0 }), "utf8");
49
53
  }
50
54
  async function addDocuments(root, documents) {
51
55
  const file = await loadAgentsFile(root);
52
56
  const byPath = new Map(file.documents.map((doc) => [doc.path, doc]));
53
- for (const document of documents) byPath.set(document.path, document);
57
+ for (const document of documents) {
58
+ const existing = byPath.get(document.path);
59
+ byPath.set(document.path, {
60
+ path: document.path,
61
+ ...existing?.description ? { description: existing.description } : {},
62
+ ...document.description ? { description: document.description } : {}
63
+ });
64
+ }
54
65
  const next = {
55
66
  version: 1,
56
67
  documents: [...byPath.values()].sort((left, right) => left.path.localeCompare(right.path))
@@ -166,6 +177,10 @@ async function discoverAgentDocuments(root) {
166
177
  await walk(root, root, found);
167
178
  return found.filter((document) => document.path !== "./AGENTS.md").sort((left, right) => left.path.localeCompare(right.path));
168
179
  }
180
+ async function describeAgentDocument(root, agentsDocumentPath) {
181
+ const absolutePath = resolveFromRoot(root, agentsDocumentPath);
182
+ return documentEntry(root, absolutePath, await readPackageDescription(path.dirname(absolutePath)));
183
+ }
169
184
  async function walk(root, directory, found) {
170
185
  let handle;
171
186
  try {
@@ -183,7 +198,7 @@ async function walk(root, directory, found) {
183
198
  if (!skippedDirectories.has(entry.name)) await walk(root, absolutePath, found);
184
199
  continue;
185
200
  }
186
- if (entry.isFile() && entry.name === "AGENTS.md") found.push({ path: formatProjectPath(root, absolutePath) });
201
+ if (entry.isFile() && entry.name === "AGENTS.md") found.push(await documentEntry(root, absolutePath, await readPackageDescription(path.dirname(absolutePath))));
187
202
  }
188
203
  }
189
204
  async function scanDirectNodeModules(root, nodeModulesPath, found) {
@@ -216,9 +231,29 @@ async function addPackageAgentsDocument(root, packagePath, found) {
216
231
  const agentsPath = path.join(packagePath, "AGENTS.md");
217
232
  try {
218
233
  await access(agentsPath);
219
- found.push({ path: formatProjectPath(root, agentsPath) });
234
+ found.push(await documentEntry(root, agentsPath, await readPackageDescription(packagePath)));
220
235
  } catch {}
221
236
  }
237
+ async function documentEntry(root, agentsPath, description) {
238
+ return {
239
+ path: formatProjectPath(root, agentsPath),
240
+ ...description ? { description } : {}
241
+ };
242
+ }
243
+ async function readPackageDescription(packagePath) {
244
+ try {
245
+ const source = await readFile(path.join(packagePath, "package.json"), "utf8");
246
+ const parsed = JSON.parse(source);
247
+ if (!isPackageJson(parsed) || typeof parsed.description !== "string") return void 0;
248
+ const description = parsed.description.trim();
249
+ return description && description.length > 0 ? description : void 0;
250
+ } catch {
251
+ return;
252
+ }
253
+ }
254
+ function isPackageJson(value) {
255
+ return typeof value === "object" && value !== null;
256
+ }
222
257
  //#endregion
223
258
  //#region src/run.ts
224
259
  const helpText = `agents
@@ -231,7 +266,7 @@ Usage:
231
266
  agents remove <path...>
232
267
  agents validate [--json]
233
268
 
234
- agents.yaml is a curated table of contents for active external AGENTS.md guidance.`;
269
+ agents.yaml is a curated table of contents for promoted AGENTS.md guidance.`;
235
270
  async function run(argv) {
236
271
  const parsed = parseArgs(argv);
237
272
  const root = cwd();
@@ -327,23 +362,26 @@ async function commandDiscover(root, json) {
327
362
  clack.outro("No supplemental AGENTS.md files found.");
328
363
  return;
329
364
  }
330
- clack.note(documents.map((doc) => doc.path).join("\n"), `Found ${documents.length}`);
365
+ clack.note(formatDocumentList(documents), `Found ${documents.length}`);
331
366
  clack.outro("Use agents add <path> to enable one.");
332
367
  }
333
368
  async function commandAdd(root, paths) {
334
369
  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)) }));
370
+ const documents = await Promise.all(paths.map((path) => describeAgentDocument(root, formatProjectPath(root, resolveFromRoot(root, path)))));
336
371
  const file = await addDocuments(root, documents);
337
372
  clack.intro("agents add");
338
- clack.note(file.documents.map((doc) => doc.path).join("\n"), "Active documents");
373
+ clack.note(formatDocumentList(file.documents), "Promoted documents");
339
374
  clack.outro(`Added ${documents.length} document${documents.length === 1 ? "" : "s"}.`);
340
375
  }
376
+ function formatDocumentList(documents) {
377
+ return documents.map((doc) => doc.description ? `${doc.path}\n ${doc.description}` : doc.path).join("\n");
378
+ }
341
379
  async function commandRemove(root, paths) {
342
380
  if (paths.length === 0) throw new Error("remove requires at least one path");
343
381
  const result = await removeDocuments(root, paths.map((path) => formatProjectPath(root, resolveFromRoot(root, path))));
344
382
  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"}.`);
383
+ clack.note(result.removed.join("\n") || "No matching documents were listed.", "Removed");
384
+ clack.outro(`agents.yaml now has ${result.file.documents.length} promoted document${result.file.documents.length === 1 ? "" : "s"}.`);
347
385
  }
348
386
  async function commandValidate(root, json) {
349
387
  const result = await validateAgentsFile(root);
@@ -391,7 +429,7 @@ async function interactive(root) {
391
429
  const existing = await loadAgentsFile(root);
392
430
  const candidates = (await discoverAgentDocuments(root)).filter((doc) => !existing.documents.some((active) => active.path === doc.path));
393
431
  if (candidates.length === 0) {
394
- clack.outro("No inactive supplemental AGENTS.md files found.");
432
+ clack.outro("No unlisted supplemental AGENTS.md files found.");
395
433
  return;
396
434
  }
397
435
  const selected = await clack.multiselect({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agents.yaml",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "A CLI for discovering and curating agent-readable documentation in agents.yaml.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -16,12 +16,6 @@
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": {
26
20
  "@clack/prompts": "1.5.1",
27
21
  "yaml": "2.9.0",
@@ -29,12 +23,15 @@
29
23
  },
30
24
  "devDependencies": {
31
25
  "@types/node": "25.9.2",
32
- "tsdown": "0.22.2",
33
26
  "typescript": "6.0.3"
34
27
  },
35
28
  "engines": {
36
29
  "node": "26.3.0",
37
30
  "pnpm": "11.5.2"
38
31
  },
39
- "packageManager": "pnpm@11.5.2"
40
- }
32
+ "scripts": {
33
+ "build": "vp pack",
34
+ "dev": "src/index.ts",
35
+ "test": "vp test"
36
+ }
37
+ }
@@ -6,6 +6,7 @@ import { resolveFromRoot } from './paths.ts'
6
6
 
7
7
  export type AgentsDocumentEntry = {
8
8
  path: string
9
+ description?: string | undefined
9
10
  }
10
11
 
11
12
  export type AgentsFile = {
@@ -24,13 +25,12 @@ const fileSchema = z.object({
24
25
  documents: z.array(
25
26
  z.object({
26
27
  path: z.string().min(1),
28
+ description: z.string().min(1).optional(),
27
29
  }),
28
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
36
  const filePath = agentsPath(root)
@@ -57,7 +57,10 @@ export async function loadAgentsFile(root: string): Promise<AgentsFile> {
57
57
  export async function saveAgentsFile(root: string, file: AgentsFile): Promise<void> {
58
58
  const normalized = {
59
59
  version: file.version,
60
- documents: file.documents.map((doc) => ({ path: doc.path })),
60
+ documents: file.documents.map((doc) => ({
61
+ path: doc.path,
62
+ ...(doc.description ? { description: doc.description } : {}),
63
+ })),
61
64
  }
62
65
 
63
66
  await writeFile(agentsPath(root), YAML.stringify(normalized, { lineWidth: 0 }), 'utf8')
@@ -71,7 +74,12 @@ export async function addDocuments(
71
74
  const byPath = new Map(file.documents.map((doc) => [doc.path, doc]))
72
75
 
73
76
  for (const document of documents) {
74
- byPath.set(document.path, document)
77
+ const existing = byPath.get(document.path)
78
+ byPath.set(document.path, {
79
+ path: document.path,
80
+ ...(existing?.description ? { description: existing.description } : {}),
81
+ ...(document.description ? { description: document.description } : {}),
82
+ })
75
83
  }
76
84
 
77
85
  const next = {
@@ -17,21 +17,42 @@ describe('agents.yaml dependency discovery', () => {
17
17
  const dependencyAgentsPath = path.join(root, 'node_modules', 'direct-lib', 'AGENTS.md')
18
18
  await mkdir(path.dirname(dependencyAgentsPath), { recursive: true })
19
19
  await writeFile(dependencyAgentsPath, '# Direct dependency guidance\n', 'utf8')
20
+ await writeFile(
21
+ path.join(root, 'node_modules', 'direct-lib', 'package.json'),
22
+ JSON.stringify({
23
+ name: 'direct-lib',
24
+ description: 'Direct fixtures for testing agent guidance.',
25
+ }),
26
+ 'utf8',
27
+ )
20
28
 
21
29
  const discovered = await discoverAgentDocuments(root)
22
- expect(discovered).toEqual([{ path: './node_modules/direct-lib/AGENTS.md' }])
23
-
24
- await addDocuments(root, [
30
+ expect(discovered).toEqual([
25
31
  {
26
- path: discovered[0]!.path,
32
+ path: './node_modules/direct-lib/AGENTS.md',
33
+ description: 'Direct fixtures for testing agent guidance.',
27
34
  },
28
35
  ])
29
36
 
37
+ await addDocuments(root, [discovered[0]!])
38
+
30
39
  await expect(loadAgentsFile(root)).resolves.toEqual({
31
40
  version: 1,
32
41
  documents: [
33
42
  {
34
43
  path: './node_modules/direct-lib/AGENTS.md',
44
+ description: 'Direct fixtures for testing agent guidance.',
45
+ },
46
+ ],
47
+ })
48
+
49
+ await addDocuments(root, [{ path: './node_modules/direct-lib/AGENTS.md' }])
50
+ await expect(loadAgentsFile(root)).resolves.toEqual({
51
+ version: 1,
52
+ documents: [
53
+ {
54
+ path: './node_modules/direct-lib/AGENTS.md',
55
+ description: 'Direct fixtures for testing agent guidance.',
35
56
  },
36
57
  ],
37
58
  })
@@ -58,6 +79,28 @@ describe('agents.yaml dependency discovery', () => {
58
79
  { path: './node_modules/direct-lib/AGENTS.md' },
59
80
  ])
60
81
  })
82
+
83
+ it('includes scoped package descriptions when present', async () => {
84
+ const root = await createTempProject()
85
+ const dependencyPath = path.join(root, 'node_modules', '@scope', 'direct-lib')
86
+ await mkdir(dependencyPath, { recursive: true })
87
+ await writeFile(path.join(dependencyPath, 'AGENTS.md'), '# Scoped guidance\n', 'utf8')
88
+ await writeFile(
89
+ path.join(dependencyPath, 'package.json'),
90
+ JSON.stringify({
91
+ name: '@scope/direct-lib',
92
+ description: 'Scoped package guidance breadcrumbs.',
93
+ }),
94
+ 'utf8',
95
+ )
96
+
97
+ await expect(discoverAgentDocuments(root)).resolves.toEqual([
98
+ {
99
+ path: './node_modules/@scope/direct-lib/AGENTS.md',
100
+ description: 'Scoped package guidance breadcrumbs.',
101
+ },
102
+ ])
103
+ })
61
104
  })
62
105
 
63
106
  async function createTempProject(): Promise<string> {
package/src/discover.ts CHANGED
@@ -1,9 +1,10 @@
1
- import { access, opendir } from 'node:fs/promises'
1
+ import { access, opendir, readFile } from 'node:fs/promises'
2
2
  import path from 'node:path'
3
- import { formatProjectPath } from './paths.ts'
3
+ import { formatProjectPath, resolveFromRoot } from './paths.ts'
4
4
 
5
5
  export type DiscoveredDocument = {
6
6
  path: string
7
+ description?: string
7
8
  }
8
9
 
9
10
  const skippedDirectories = new Set([
@@ -25,6 +26,14 @@ export async function discoverAgentDocuments(root: string): Promise<DiscoveredDo
25
26
  .sort((left, right) => left.path.localeCompare(right.path))
26
27
  }
27
28
 
29
+ export async function describeAgentDocument(
30
+ root: string,
31
+ agentsDocumentPath: string,
32
+ ): Promise<DiscoveredDocument> {
33
+ const absolutePath = resolveFromRoot(root, agentsDocumentPath)
34
+ return documentEntry(root, absolutePath, await readPackageDescription(path.dirname(absolutePath)))
35
+ }
36
+
28
37
  async function walk(root: string, directory: string, found: DiscoveredDocument[]): Promise<void> {
29
38
  let handle
30
39
  try {
@@ -49,7 +58,13 @@ async function walk(root: string, directory: string, found: DiscoveredDocument[]
49
58
  }
50
59
 
51
60
  if (entry.isFile() && entry.name === 'AGENTS.md') {
52
- found.push({ path: formatProjectPath(root, absolutePath) })
61
+ found.push(
62
+ await documentEntry(
63
+ root,
64
+ absolutePath,
65
+ await readPackageDescription(path.dirname(absolutePath)),
66
+ ),
67
+ )
53
68
  }
54
69
  }
55
70
  }
@@ -108,8 +123,36 @@ async function addPackageAgentsDocument(
108
123
  const agentsPath = path.join(packagePath, 'AGENTS.md')
109
124
  try {
110
125
  await access(agentsPath)
111
- found.push({ path: formatProjectPath(root, agentsPath) })
126
+ found.push(await documentEntry(root, agentsPath, await readPackageDescription(packagePath)))
112
127
  } catch {
113
128
  // Packages without AGENTS.md are simply not candidates.
114
129
  }
115
130
  }
131
+
132
+ async function documentEntry(
133
+ root: string,
134
+ agentsPath: string,
135
+ description: string | undefined,
136
+ ): Promise<DiscoveredDocument> {
137
+ return {
138
+ path: formatProjectPath(root, agentsPath),
139
+ ...(description ? { description } : {}),
140
+ }
141
+ }
142
+
143
+ async function readPackageDescription(packagePath: string): Promise<string | undefined> {
144
+ try {
145
+ const source = await readFile(path.join(packagePath, 'package.json'), 'utf8')
146
+ const parsed = JSON.parse(source) as unknown
147
+ if (!isPackageJson(parsed) || typeof parsed.description !== 'string') return undefined
148
+
149
+ const description = parsed.description.trim()
150
+ return description && description.length > 0 ? description : undefined
151
+ } catch {
152
+ return undefined
153
+ }
154
+ }
155
+
156
+ function isPackageJson(value: unknown): value is Record<string, unknown> {
157
+ return typeof value === 'object' && value !== null
158
+ }
package/src/index.ts CHANGED
File without changes
package/src/run.ts CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  removeDocuments,
7
7
  validateAgentsFile,
8
8
  } from './agents-file.ts'
9
- import { discoverAgentDocuments } from './discover.ts'
9
+ import { describeAgentDocument, discoverAgentDocuments } from './discover.ts'
10
10
  import { cwd, formatProjectPath, resolveFromRoot } from './paths.ts'
11
11
 
12
12
  type Command = 'add' | 'discover' | 'help' | 'init' | 'remove' | 'validate' | 'version'
@@ -27,7 +27,7 @@ Usage:
27
27
  agents remove <path...>
28
28
  agents validate [--json]
29
29
 
30
- agents.yaml is a curated table of contents for active external AGENTS.md guidance.`
30
+ agents.yaml is a curated table of contents for promoted AGENTS.md guidance.`
31
31
 
32
32
  export async function run(argv: string[]): Promise<void> {
33
33
  const parsed = parseArgs(argv)
@@ -127,7 +127,7 @@ async function commandDiscover(root: string, json: boolean): Promise<void> {
127
127
  return
128
128
  }
129
129
 
130
- clack.note(documents.map((doc) => doc.path).join('\n'), `Found ${documents.length}`)
130
+ clack.note(formatDocumentList(documents), `Found ${documents.length}`)
131
131
  clack.outro('Use agents add <path> to enable one.')
132
132
  }
133
133
 
@@ -136,16 +136,26 @@ async function commandAdd(root: string, paths: string[]): Promise<void> {
136
136
  throw new Error('add requires at least one AGENTS.md path')
137
137
  }
138
138
 
139
- const documents = paths.map((path) => ({
140
- path: formatProjectPath(root, resolveFromRoot(root, path)),
141
- }))
139
+ const documents = await Promise.all(
140
+ paths.map((path) =>
141
+ describeAgentDocument(root, formatProjectPath(root, resolveFromRoot(root, path))),
142
+ ),
143
+ )
142
144
 
143
145
  const file = await addDocuments(root, documents)
144
146
  clack.intro('agents add')
145
- clack.note(file.documents.map((doc) => doc.path).join('\n'), 'Active documents')
147
+ clack.note(formatDocumentList(file.documents), 'Promoted documents')
146
148
  clack.outro(`Added ${documents.length} document${documents.length === 1 ? '' : 's'}.`)
147
149
  }
148
150
 
151
+ function formatDocumentList(
152
+ documents: { path: string; description?: string | undefined }[],
153
+ ): string {
154
+ return documents
155
+ .map((doc) => (doc.description ? `${doc.path}\n ${doc.description}` : doc.path))
156
+ .join('\n')
157
+ }
158
+
149
159
  async function commandRemove(root: string, paths: string[]): Promise<void> {
150
160
  if (paths.length === 0) {
151
161
  throw new Error('remove requires at least one path')
@@ -154,9 +164,9 @@ async function commandRemove(root: string, paths: string[]): Promise<void> {
154
164
  const normalizedPaths = paths.map((path) => formatProjectPath(root, resolveFromRoot(root, path)))
155
165
  const result = await removeDocuments(root, normalizedPaths)
156
166
  clack.intro('agents remove')
157
- clack.note(result.removed.join('\n') || 'No matching documents were active.', 'Removed')
167
+ clack.note(result.removed.join('\n') || 'No matching documents were listed.', 'Removed')
158
168
  clack.outro(
159
- `agents.yaml now has ${result.file.documents.length} active document${result.file.documents.length === 1 ? '' : 's'}.`,
169
+ `agents.yaml now has ${result.file.documents.length} promoted document${result.file.documents.length === 1 ? '' : 's'}.`,
160
170
  )
161
171
  }
162
172
 
@@ -214,7 +224,7 @@ async function interactive(root: string): Promise<void> {
214
224
  )
215
225
 
216
226
  if (candidates.length === 0) {
217
- clack.outro('No inactive supplemental AGENTS.md files found.')
227
+ clack.outro('No unlisted supplemental AGENTS.md files found.')
218
228
  return
219
229
  }
220
230