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 +3 -2
- package/README.md +4 -3
- package/dist/index.mjs +89 -24
- package/package.json +8 -10
- package/src/agents-file.ts +186 -163
- package/src/discover.test.ts +137 -55
- package/src/discover.ts +165 -93
- package/src/index.ts +4 -4
- package/src/paths.ts +13 -8
- package/src/run.ts +290 -191
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
|
|
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
|
|
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
|
-
|
|
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({
|
|
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 = `
|
|
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) => ({
|
|
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)
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
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) => (
|
|
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
|
|
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
|
|
346
|
-
clack.outro(`agents.yaml now has ${result.file.documents.length}
|
|
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
|
|
434
|
+
clack.outro("No unlisted supplemental AGENTS.md files found.");
|
|
395
435
|
return;
|
|
396
436
|
}
|
|
397
|
-
const selected = await
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
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
|
|
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
|
-
"
|
|
40
|
-
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "vp pack",
|
|
35
|
+
"dev": "src/index.ts",
|
|
36
|
+
"test": "vp test"
|
|
37
|
+
}
|
|
38
|
+
}
|
package/src/agents-file.ts
CHANGED
|
@@ -1,201 +1,224 @@
|
|
|
1
|
-
import { access, readFile, writeFile } from
|
|
2
|
-
import path from
|
|
3
|
-
import * as YAML from
|
|
4
|
-
import { z } from
|
|
5
|
-
import { resolveFromRoot } from
|
|
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
|
-
|
|
8
|
+
path: string
|
|
9
|
+
description?: string | undefined
|
|
9
10
|
}
|
|
10
11
|
|
|
11
12
|
export type AgentsFile = {
|
|
12
|
-
|
|
13
|
-
|
|
13
|
+
version: 1
|
|
14
|
+
documents: AgentsDocumentEntry[]
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
export type ValidationResult = {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
ok: boolean
|
|
19
|
+
errors: string[]
|
|
20
|
+
warnings: string[]
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
const fileSchema = z.object({
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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 = `
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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(
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
68
|
-
|
|
77
|
+
root: string,
|
|
78
|
+
documents: AgentsDocumentEntry[],
|
|
69
79
|
): Promise<AgentsFile> {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
88
|
-
|
|
104
|
+
root: string,
|
|
105
|
+
paths: string[],
|
|
89
106
|
): Promise<{ file: AgentsFile; removed: string[] }> {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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(
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
160
|
-
|
|
178
|
+
root: string,
|
|
179
|
+
options: { force: boolean },
|
|
161
180
|
): Promise<{ messages: string[] }> {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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
|
-
|
|
219
|
+
return path.join(root, "agents.yaml")
|
|
197
220
|
}
|
|
198
221
|
|
|
199
222
|
function isNotFound(error: unknown): boolean {
|
|
200
|
-
|
|
223
|
+
return error instanceof Error && "code" in error && error.code === "ENOENT"
|
|
201
224
|
}
|