@ghostbyte/agent-docs-kit 4.0.2 → 5.0.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 (120) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +43 -37
  3. package/SECURITY.md +11 -0
  4. package/content/catalog.json +191 -22
  5. package/content/documentation/01-system-documentation.md +13 -0
  6. package/content/documentation/02-project-governance.md +13 -0
  7. package/content/documentation/03-development-records.md +13 -0
  8. package/content/documentation/04-testing-evidence.md +13 -0
  9. package/content/documentation/05-operations-runbooks.md +13 -0
  10. package/content/documentation/06-future-proposals.md +13 -0
  11. package/content/documentation/07-archive-governance.md +13 -0
  12. package/content/documentation/full-reference.md +24 -0
  13. package/content/engineering/01-query-performance.md +17 -27
  14. package/content/engineering/02-networking-fundamentals.md +17 -27
  15. package/content/engineering/03-rate-limiting-algorithms.md +18 -28
  16. package/content/engineering/04-production-debugging.md +17 -27
  17. package/content/engineering/05-code-review-practices.md +17 -27
  18. package/content/engineering/06-data-modeling.md +17 -27
  19. package/content/engineering/07-cloud-cost-saving.md +17 -27
  20. package/content/engineering/08-testing-types.md +17 -27
  21. package/content/engineering/09-message-queues.md +17 -27
  22. package/content/engineering/10-api-design-compatibility.md +14 -0
  23. package/content/engineering/11-reliability-slos.md +14 -0
  24. package/content/engineering/12-observability-telemetry.md +14 -0
  25. package/content/engineering/13-deployment-change-safety.md +14 -0
  26. package/content/engineering/14-capacity-concurrency.md +14 -0
  27. package/content/engineering/full-reference.md +24 -144
  28. package/content/security/01-design-architecture.md +11 -19
  29. package/content/security/02-local-development.md +13 -21
  30. package/content/security/03-database-backend.md +18 -26
  31. package/content/security/04-api-layer.md +23 -31
  32. package/content/security/05-frontend-ui.md +16 -24
  33. package/content/security/06-application-security-testing.md +22 -28
  34. package/content/security/07-cicd-release-engineering.md +13 -21
  35. package/content/security/08-cloud-infrastructure.md +17 -25
  36. package/content/security/09-edge-cdn-dns.md +16 -24
  37. package/content/security/10-release-rollout.md +13 -21
  38. package/content/security/11-post-release-monitoring-ir.md +15 -23
  39. package/content/security/12-ongoing-operations.md +24 -32
  40. package/content/security/full-reference.md +30 -3163
  41. package/docs/ARCHITECTURE.md +31 -92
  42. package/docs/CONTENT_MAINTENANCE.md +5 -1
  43. package/docs/GITHUB_AND_NPM_SETUP.md +1 -1
  44. package/docs/RELEASE_PROCESS.md +5 -5
  45. package/package.json +12 -5
  46. package/scripts/build-site.mjs +13 -0
  47. package/scripts/build-skill.mjs +24 -0
  48. package/scripts/check-links.mjs +56 -0
  49. package/scripts/check-review-dates.mjs +21 -0
  50. package/scripts/preview-site.mjs +43 -0
  51. package/scripts/smoke-package.mjs +58 -0
  52. package/scripts/validate-content.mjs +85 -0
  53. package/site/app.js +121 -0
  54. package/site/index.html +346 -0
  55. package/site/styles.css +159 -0
  56. package/skills/manage-project-quality/SKILL.md +18 -2
  57. package/skills/manage-project-quality/assets/documentation-report.md +3 -3
  58. package/skills/manage-project-quality/assets/engineering-report.md +3 -3
  59. package/skills/manage-project-quality/assets/security-report.md +3 -3
  60. package/skills/manage-project-quality/references/documentation-01-system-documentation.md +13 -0
  61. package/skills/manage-project-quality/references/documentation-02-project-governance.md +13 -0
  62. package/skills/manage-project-quality/references/documentation-03-development-records.md +13 -0
  63. package/skills/manage-project-quality/references/documentation-04-testing-evidence.md +13 -0
  64. package/skills/manage-project-quality/references/documentation-05-operations-runbooks.md +13 -0
  65. package/skills/manage-project-quality/references/documentation-06-future-proposals.md +13 -0
  66. package/skills/manage-project-quality/references/documentation-07-archive-governance.md +13 -0
  67. package/skills/manage-project-quality/references/documentation-full-reference.md +24 -0
  68. package/skills/manage-project-quality/references/documentation-governance.md +2 -2
  69. package/skills/manage-project-quality/references/engineering-01-query-performance.md +17 -27
  70. package/skills/manage-project-quality/references/engineering-02-networking-fundamentals.md +17 -27
  71. package/skills/manage-project-quality/references/engineering-03-rate-limiting-algorithms.md +18 -28
  72. package/skills/manage-project-quality/references/engineering-04-production-debugging.md +17 -27
  73. package/skills/manage-project-quality/references/engineering-05-code-review-practices.md +17 -27
  74. package/skills/manage-project-quality/references/engineering-06-data-modeling.md +17 -27
  75. package/skills/manage-project-quality/references/engineering-07-cloud-cost-saving.md +17 -27
  76. package/skills/manage-project-quality/references/engineering-08-testing-types.md +17 -27
  77. package/skills/manage-project-quality/references/engineering-09-message-queues.md +17 -27
  78. package/skills/manage-project-quality/references/engineering-10-api-design-compatibility.md +14 -0
  79. package/skills/manage-project-quality/references/engineering-11-reliability-slos.md +14 -0
  80. package/skills/manage-project-quality/references/engineering-12-observability-telemetry.md +14 -0
  81. package/skills/manage-project-quality/references/engineering-13-deployment-change-safety.md +14 -0
  82. package/skills/manage-project-quality/references/engineering-14-capacity-concurrency.md +14 -0
  83. package/skills/manage-project-quality/references/engineering-full-reference.md +24 -144
  84. package/skills/manage-project-quality/references/engineering-protocol.md +1 -1
  85. package/skills/manage-project-quality/references/security-01-design-architecture.md +11 -19
  86. package/skills/manage-project-quality/references/security-02-local-development.md +13 -21
  87. package/skills/manage-project-quality/references/security-03-database-backend.md +18 -26
  88. package/skills/manage-project-quality/references/security-04-api-layer.md +23 -31
  89. package/skills/manage-project-quality/references/security-05-frontend-ui.md +16 -24
  90. package/skills/manage-project-quality/references/security-06-application-security-testing.md +22 -28
  91. package/skills/manage-project-quality/references/security-07-cicd-release-engineering.md +13 -21
  92. package/skills/manage-project-quality/references/security-08-cloud-infrastructure.md +17 -25
  93. package/skills/manage-project-quality/references/security-09-edge-cdn-dns.md +16 -24
  94. package/skills/manage-project-quality/references/security-10-release-rollout.md +13 -21
  95. package/skills/manage-project-quality/references/security-11-post-release-monitoring-ir.md +15 -23
  96. package/skills/manage-project-quality/references/security-12-ongoing-operations.md +24 -32
  97. package/skills/manage-project-quality/references/security-full-reference.md +30 -3163
  98. package/skills/manage-project-quality/scripts/check-report.mjs +67 -16
  99. package/src/api/index.mjs +4 -0
  100. package/src/cli/arguments.mjs +27 -3
  101. package/src/cli/help.mjs +53 -18
  102. package/src/cli/main.mjs +66 -9
  103. package/src/commands/audit.mjs +146 -31
  104. package/src/commands/doctor.mjs +1 -1
  105. package/src/commands/guide.mjs +12 -0
  106. package/src/commands/list.mjs +1 -1
  107. package/src/commands/setup.mjs +9 -4
  108. package/src/commands/skill.mjs +1 -0
  109. package/src/commands/template.mjs +32 -0
  110. package/src/commands/validate.mjs +36 -4
  111. package/src/commands/website.mjs +26 -0
  112. package/src/core/config.mjs +61 -0
  113. package/src/core/files.mjs +47 -6
  114. package/templates/adr.md +20 -0
  115. package/templates/change-record.md +18 -0
  116. package/templates/incident-review.md +20 -0
  117. package/templates/runbook.md +18 -0
  118. package/templates/test-evidence.md +18 -0
  119. package/tests/cli.test.mjs +226 -0
  120. package/tests/content.test.mjs +18 -0
@@ -1,23 +1,74 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { readFile } from "node:fs/promises";
4
+ import { resolve } from "node:path";
5
+ import { pathToFileURL } from "node:url";
4
6
 
5
- const file = process.argv[2];
6
- if (!file) throw new Error("Usage: node check-report.mjs <report.md>");
7
- const text = await readFile(file, "utf8");
8
- const allowed = new Set(["Pass", "Fail", "Unverified", "N/A", "Applied", "Partial", "Recommended"]);
9
- const errors = [];
10
- for (const [index, line] of text.split(/\r?\n/).entries()) {
11
- if (!line.startsWith("|") || /^\|[-: |]+\|$/.test(line)) continue;
12
- const cells = line.split("|").slice(1, -1).map((cell) => cell.trim());
13
- const status = cells.find((cell) => allowed.has(cell));
14
- if (status && status !== "Unverified" && !cells.some((cell, cellIndex) => cellIndex > 1 && cell.length > 3)) {
15
- errors.push(`Line ${index + 1}: status '${status}' has no supporting detail.`);
7
+ const statuses = {
8
+ security: new Set(["Pass", "Fail", "Unverified", "N/A"]),
9
+ engineering: new Set(["Applied", "Partial", "Recommended", "Unverified", "N/A"]),
10
+ documentation: new Set(["Pass", "Fail", "Unverified", "N/A"]),
11
+ generic: new Set(["Pass", "Fail", "Unverified", "N/A", "Applied", "Partial", "Recommended"])
12
+ };
13
+ const placeholders = /^(?:|_|-+|none|not verified|unassigned)$/i;
14
+
15
+ function splitRow(line) {
16
+ const cells = [];
17
+ let cell = "";
18
+ for (let index = 1; index < line.length - 1; index += 1) {
19
+ if (line[index] === "|" && line[index - 1] !== "\\") {
20
+ cells.push(cell.trim());
21
+ cell = "";
22
+ } else {
23
+ cell += line[index];
24
+ }
25
+ }
26
+ cells.push(cell.trim());
27
+ return cells;
28
+ }
29
+
30
+ export function validateReport(text) {
31
+ const errors = [];
32
+ let header;
33
+ let allowed = statuses.generic;
34
+ let foundFindings = false;
35
+ const lines = text.split(/\r?\n/);
36
+ for (const [index, line] of lines.entries()) {
37
+ if (!line.startsWith("|")) { header = undefined; continue; }
38
+ if (/^\|[-: |]+\|$/.test(line)) continue;
39
+ const cells = splitRow(line);
40
+ if (/^\|[-: |]+\|$/.test(lines[index + 1] ?? "")) {
41
+ header = cells.map((cell) => cell.toLowerCase());
42
+ if (!header.includes("status")) { header = undefined; continue; }
43
+ foundFindings = true;
44
+ allowed = header.includes("control id") ? statuses.security : header.includes("practice id") ? statuses.engineering : statuses.generic;
45
+ continue;
46
+ }
47
+ if (!header) continue;
48
+ const statusIndex = header.indexOf("status");
49
+ const status = cells[statusIndex];
50
+ if (!status) { errors.push(`Line ${index + 1}: status is empty.`); continue; }
51
+ if (!allowed.has(status)) { errors.push(`Line ${index + 1}: invalid status '${status}'.`); continue; }
52
+ if (!["Unverified", "N/A"].includes(status)) {
53
+ for (const name of ["evidence", "owner", "verification"]) {
54
+ const column = header.findIndex((cell) => cell === name || cell.includes(name));
55
+ if (column >= 0 && placeholders.test(cells[column] ?? "")) errors.push(`Line ${index + 1}: status '${status}' requires ${name}.`);
56
+ }
57
+ }
16
58
  }
59
+ if (!foundFindings) errors.push("No findings table with a Status column was found.");
60
+ return errors;
17
61
  }
18
- if (errors.length) {
19
- console.error(errors.join("\n"));
20
- process.exitCode = 1;
21
- } else {
22
- console.log("Report structure check passed.");
62
+
63
+ export async function checkReport(file) {
64
+ const errors = validateReport(await readFile(file, "utf8"));
65
+ if (errors.length) throw new Error(errors.join("\n"));
66
+ return true;
67
+ }
68
+
69
+ if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) {
70
+ const file = process.argv[2];
71
+ if (!file) throw new Error("Usage: node check-report.mjs <report.md>");
72
+ try { await checkReport(file); console.log("Report structure check passed."); }
73
+ catch (error) { console.error(error.message); process.exitCode = 1; }
23
74
  }
package/src/api/index.mjs CHANGED
@@ -1,4 +1,8 @@
1
1
  export { setupProject } from "../commands/setup.mjs";
2
2
  export { auditDocumentation, auditSecurity, reviewEngineering } from "../commands/audit.mjs";
3
3
  export { installSkill } from "../commands/skill.mjs";
4
+ export { createTemplate, listTemplates, templateTypes } from "../commands/template.mjs";
5
+ export { showDocumentationGuide } from "../commands/guide.mjs";
6
+ export { openWebsite, websiteUrl } from "../commands/website.mjs";
4
7
  export { loadCatalog } from "../core/catalog.mjs";
8
+ export { loadProjectConfig, defaultConfig } from "../core/config.mjs";
@@ -1,3 +1,8 @@
1
+ const booleanFlags = new Set(["dry-run", "force", "report", "strict", "help", "version"]);
2
+ const valueFlags = new Set(["cwd", "style", "target", "phase", "topic", "format", "type", "name"]);
3
+ const allowedStyles = new Set(["standard", "learning"]);
4
+ const allowedFormats = new Set(["text", "markdown", "json"]);
5
+
1
6
  export function parseArguments(argv) {
2
7
  const positionals = [];
3
8
  const flags = {};
@@ -8,10 +13,16 @@ export function parseArguments(argv) {
8
13
  continue;
9
14
  }
10
15
  const [rawKey, inline] = token.slice(2).split("=", 2);
16
+ if (!booleanFlags.has(rawKey) && !valueFlags.has(rawKey)) {
17
+ throw new Error(`Unknown option '--${rawKey}'. Run 'agent-docs --help'.`);
18
+ }
11
19
  if (inline !== undefined) {
20
+ if (booleanFlags.has(rawKey)) throw new Error(`Option '--${rawKey}' does not accept a value.`);
12
21
  flags[rawKey] = inline;
13
- } else if (argv[index + 1] && !argv[index + 1].startsWith("--")) {
22
+ } else if (valueFlags.has(rawKey) && argv[index + 1] && !argv[index + 1].startsWith("--")) {
14
23
  flags[rawKey] = argv[++index];
24
+ } else if (valueFlags.has(rawKey)) {
25
+ throw new Error(`Option '--${rawKey}' requires a value.`);
15
26
  } else {
16
27
  flags[rawKey] = true;
17
28
  }
@@ -20,11 +31,24 @@ export function parseArguments(argv) {
20
31
  }
21
32
 
22
33
  export function optionsFromFlags(flags) {
34
+ const style = flags.style;
35
+ const format = flags.format ?? "text";
36
+ if (style !== undefined && !allowedStyles.has(style)) throw new Error(`Invalid style '${style}'. Use standard or learning.`);
37
+ if (!allowedFormats.has(format)) throw new Error(`Invalid format '${format}'. Use text, markdown, or json.`);
38
+ if (flags.target && !["project", "codex"].includes(flags.target)) {
39
+ throw new Error(`Invalid target '${flags.target}'. Use project or codex.`);
40
+ }
41
+ if (flags.name && !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(flags.name)) {
42
+ throw new Error(`Invalid name '${flags.name}'. Use a lowercase hyphenated slug.`);
43
+ }
23
44
  return {
24
45
  cwd: flags.cwd,
25
46
  dryRun: Boolean(flags["dry-run"]),
26
47
  force: Boolean(flags.force),
27
- style: flags.style ?? "standard",
28
- report: Boolean(flags.report)
48
+ style,
49
+ format,
50
+ report: Boolean(flags.report),
51
+ strict: Boolean(flags.strict),
52
+ name: flags.name
29
53
  };
30
54
  }
package/src/cli/help.mjs CHANGED
@@ -1,24 +1,59 @@
1
1
  export const helpText = `Agent Docs & Quality Kit
2
2
 
3
- Usage:
4
- agent-docs Open the guided menu (TTY only)
5
- agent-docs setup [--dry-run] Install the documentation workspace
6
- agent-docs audit documentation Check documentation health
7
- agent-docs audit security [phase] Create an evidence-based security worksheet
8
- agent-docs review engineering [topic]
9
- Create an engineering review worksheet
10
- agent-docs list [security|engineering]
11
- agent-docs skill install [--target project|codex] [--dry-run]
12
- agent-docs validate Validate this package's catalog and skill
13
- agent-docs doctor Check local prerequisites
14
- agent-docs --version Show the package version
3
+ Run without installing:
4
+ npx @ghostbyte/agent-docs-kit@latest [command] [options]
5
+
6
+ After installing in a project:
7
+ npx agent-docs [command] [options]
8
+
9
+ After installing globally:
10
+ agent-docs [command] [options]
11
+
12
+ Commands:
13
+ (no command) Open the guided menu (TTY only)
14
+ setup [--dry-run] Create the documentation workspace
15
+ audit documentation [--strict] Check documentation health
16
+ documentation guide <area> Print one documentation guidance area
17
+ security worksheet <phase> Create a scoped security worksheet
18
+ engineering worksheet <topic> Create a scoped engineering worksheet
19
+ audit security <phase> Legacy alias for security worksheet
20
+ review engineering <topic> Legacy alias for engineering worksheet
21
+ list [documentation|security|engineering]
22
+ List available areas, phases, or topics
23
+ template list List reusable documentation templates
24
+ template create <type> [--name <slug>]
25
+ Create a uniquely named documentation record
26
+ skill install --target project|codex
27
+ Install the unified quality skill
28
+ validate Validate the distributed library
29
+ doctor Check Node.js and Git prerequisites
30
+ website Open the documentation website
31
+ --version Show the package version
32
+
33
+ Examples:
34
+ npx @ghostbyte/agent-docs-kit@latest setup --dry-run
35
+ npx @ghostbyte/agent-docs-kit@latest audit documentation --strict --format text
36
+ npx @ghostbyte/agent-docs-kit@latest documentation guide system-documentation
37
+ npx @ghostbyte/agent-docs-kit@latest security worksheet api-layer
38
+ npx @ghostbyte/agent-docs-kit@latest engineering worksheet testing-types
39
+ npx @ghostbyte/agent-docs-kit@latest template create adr --name cache-policy
40
+ npx @ghostbyte/agent-docs-kit@latest skill install --target project
41
+ npx @ghostbyte/agent-docs-kit@latest website
42
+
43
+ Check or update a global installation:
44
+ agent-docs --version
45
+ npm view @ghostbyte/agent-docs-kit version
46
+ npm install --global @ghostbyte/agent-docs-kit@latest
15
47
 
16
48
  Options:
17
- --cwd <path> Target project (default: current directory)
18
- --style standard|learning
19
- --dry-run Preview writes without changing files
20
- --force Replace files that this command owns
21
- --report Write documentation audit results
22
- --help Show this help
49
+ --cwd <path> Target project (default: current directory)
50
+ --style standard|learning Select concise or teaching-oriented output
51
+ --format text|markdown|json Select documentation-audit output
52
+ --name <slug> Give a template a lowercase hyphenated name
53
+ --dry-run Preview writes without changing files
54
+ --force Replace files owned by the command
55
+ --report Persist documentation audit results
56
+ --strict Fail when documentation checks fail
57
+ --help Show this help
23
58
 
24
59
  Installing the npm package performs no project setup. Run a command explicitly.`;
package/src/cli/main.mjs CHANGED
@@ -8,13 +8,26 @@ import { listCatalog } from "../commands/list.mjs";
8
8
  import { installSkill } from "../commands/skill.mjs";
9
9
  import { doctor } from "../commands/doctor.mjs";
10
10
  import { validatePackage } from "../commands/validate.mjs";
11
+ import { openWebsite } from "../commands/website.mjs";
12
+ import { createTemplate, listTemplates } from "../commands/template.mjs";
13
+ import { showDocumentationGuide } from "../commands/guide.mjs";
11
14
  import { readFile } from "node:fs/promises";
12
15
  import { resolveFromPackage } from "../core/paths.mjs";
13
16
 
17
+ function assertInvocation(positionals, flags, { counts, allowedFlags = [], usage, exclusive = [] }) {
18
+ if (!counts.includes(positionals.length)) throw new Error(`Invalid arguments. Usage: ${usage}`);
19
+ const allowed = new Set(allowedFlags);
20
+ const unexpected = Object.keys(flags).filter((flag) => !allowed.has(flag));
21
+ if (unexpected.length) throw new Error(`Option '--${unexpected[0]}' is not valid here. Usage: ${usage}`);
22
+ for (const [positionalIndex, flag] of exclusive) {
23
+ if (positionals[positionalIndex] && flags[flag]) throw new Error(`Choose either '${positionals[positionalIndex]}' or '--${flag}', not both.`);
24
+ }
25
+ }
26
+
14
27
  async function menu() {
15
28
  const ui = createInterface({ input: stdin, output: stdout });
16
29
  try {
17
- console.log(`\nAgent Docs & Quality Kit\n\n1. Set up documentation\n2. Audit documentation\n3. Start security audit\n4. Start engineering review\n5. Install project skill\n6. Validate package\n0. Exit`);
30
+ console.log(`\nAgent Docs & Quality Kit\n\n1. Set up documentation\n2. Audit documentation\n3. Create security worksheet\n4. Create engineering worksheet\n5. Install project skill\n6. Validate package\n7. Open documentation website\n8. List documentation templates\n0. Exit`);
18
31
  const choice = await ui.question("\nChoose an action: ");
19
32
  if (choice === "1") return setupProject();
20
33
  if (choice === "2") return auditDocumentation();
@@ -28,6 +41,8 @@ async function menu() {
28
41
  }
29
42
  if (choice === "5") return installSkill("project");
30
43
  if (choice === "6") return validatePackage();
44
+ if (choice === "7") return openWebsite();
45
+ if (choice === "8") return listTemplates();
31
46
  if (choice !== "0") throw new Error("Unknown menu choice.");
32
47
  } finally {
33
48
  ui.close();
@@ -37,27 +52,69 @@ async function menu() {
37
52
  export async function main(argv) {
38
53
  const { positionals, flags } = parseArguments(argv);
39
54
  const [command, subject, selector] = positionals;
40
- const options = optionsFromFlags(flags);
41
55
  if (flags.version || command === "version") {
56
+ if (!flags.version) assertInvocation(positionals, flags, { counts: [1], usage: "agent-docs version" });
42
57
  const metadata = JSON.parse(await readFile(resolveFromPackage("package.json"), "utf8"));
43
58
  return console.log(metadata.version);
44
59
  }
45
60
  if (flags.help || command === "help") return console.log(helpText);
61
+ const options = optionsFromFlags(flags);
46
62
  if (!command) {
63
+ assertInvocation(positionals, flags, { counts: [0], usage: "agent-docs" });
47
64
  if (!stdin.isTTY) {
48
65
  console.log(helpText);
49
66
  return;
50
67
  }
51
68
  return menu();
52
69
  }
53
- if (command === "setup") return setupProject(options);
54
- if (command === "audit" && subject === "documentation") return auditDocumentation(options);
55
- if (command === "audit" && subject === "security") return auditSecurity(selector ?? flags.phase, options);
56
- if (command === "review" && subject === "engineering") return reviewEngineering(selector ?? flags.topic, options);
57
- if (command === "list") return listCatalog(subject);
58
- if (command === "skill" && subject === "install") return installSkill(flags.target ?? "project", options);
59
- if (command === "validate") return validatePackage();
70
+ if (command === "setup") {
71
+ assertInvocation(positionals, flags, { counts: [1], allowedFlags: ["cwd", "dry-run", "force"], usage: "agent-docs setup [--cwd <path>] [--dry-run] [--force]" });
72
+ return setupProject(options);
73
+ }
74
+ if (command === "audit" && subject === "documentation") {
75
+ assertInvocation(positionals, flags, { counts: [2], allowedFlags: ["cwd", "dry-run", "force", "report", "strict", "format"], usage: "agent-docs audit documentation [--strict] [--report] [--format text|markdown|json]" });
76
+ const result = await auditDocumentation(options);
77
+ if (options.strict && !result.pass) process.exitCode = 1;
78
+ return result;
79
+ }
80
+ if ((command === "audit" && subject === "security") || (command === "security" && subject === "worksheet")) {
81
+ assertInvocation(positionals, flags, { counts: [2, 3], allowedFlags: ["cwd", "dry-run", "force", "style", "phase"], usage: "agent-docs security worksheet <phase>", exclusive: [[2, "phase"]] });
82
+ return auditSecurity(selector ?? flags.phase, options);
83
+ }
84
+ if ((command === "review" && subject === "engineering") || (command === "engineering" && subject === "worksheet")) {
85
+ assertInvocation(positionals, flags, { counts: [2, 3], allowedFlags: ["cwd", "dry-run", "force", "style", "topic"], usage: "agent-docs engineering worksheet <topic>", exclusive: [[2, "topic"]] });
86
+ return reviewEngineering(selector ?? flags.topic, options);
87
+ }
88
+ if (command === "list") {
89
+ assertInvocation(positionals, flags, { counts: [2], usage: "agent-docs list documentation|security|engineering" });
90
+ return listCatalog(subject);
91
+ }
92
+ if (command === "documentation" && subject === "guide") {
93
+ assertInvocation(positionals, flags, { counts: [3], usage: "agent-docs documentation guide <area>" });
94
+ return showDocumentationGuide(selector);
95
+ }
96
+ if (command === "skill" && subject === "install") {
97
+ assertInvocation(positionals, flags, { counts: [2], allowedFlags: ["cwd", "dry-run", "force", "target"], usage: "agent-docs skill install --target project|codex" });
98
+ return installSkill(flags.target ?? "project", options);
99
+ }
100
+ if (command === "template" && subject === "list") {
101
+ assertInvocation(positionals, flags, { counts: [2], usage: "agent-docs template list" });
102
+ return listTemplates();
103
+ }
104
+ if (command === "template" && subject === "create") {
105
+ assertInvocation(positionals, flags, { counts: [2, 3], allowedFlags: ["cwd", "dry-run", "force", "type", "name"], usage: "agent-docs template create <type> [--name <slug>]", exclusive: [[2, "type"]] });
106
+ return createTemplate(selector ?? flags.type, options);
107
+ }
108
+ if (command === "validate") {
109
+ assertInvocation(positionals, flags, { counts: [1], usage: "agent-docs validate" });
110
+ return validatePackage();
111
+ }
112
+ if (command === "website") {
113
+ assertInvocation(positionals, flags, { counts: [1], usage: "agent-docs website" });
114
+ return openWebsite();
115
+ }
60
116
  if (command === "doctor") {
117
+ assertInvocation(positionals, flags, { counts: [1], usage: "agent-docs doctor" });
61
118
  if (!doctor()) process.exitCode = 1;
62
119
  return;
63
120
  }
@@ -1,49 +1,164 @@
1
- import { mkdir, readFile, writeFile } from "node:fs/promises";
2
- import { resolve } from "node:path";
1
+ import { readdir, readFile, stat } from "node:fs/promises";
2
+ import { dirname, extname, relative, resolve } from "node:path";
3
3
  import { loadCatalog, selectEntry } from "../core/catalog.mjs";
4
- import { exists, writeSafe } from "../core/files.mjs";
4
+ import { loadProjectConfig } from "../core/config.mjs";
5
+ import { exists, writeUnique } from "../core/files.mjs";
5
6
  import { resolveFromPackage } from "../core/paths.mjs";
6
7
 
7
- const date = () => new Date().toISOString().slice(0, 10);
8
+ export function localDate(value = new Date()) {
9
+ const year = value.getFullYear();
10
+ const month = String(value.getMonth() + 1).padStart(2, "0");
11
+ const day = String(value.getDate()).padStart(2, "0");
12
+ return `${year}-${month}-${day}`;
13
+ }
8
14
 
9
15
  async function reportFor(kind, selector, options = {}) {
10
16
  const catalog = await loadCatalog();
11
- const entries = catalog[kind];
12
- const entry = selectEntry(entries, selector);
17
+ const entry = selectEntry(catalog[kind], selector);
13
18
  if (!entry) throw new Error(`Unknown ${kind} selection '${selector ?? ""}'. Run 'agent-docs list ${kind}'.`);
19
+ const { root, config } = await loadProjectConfig(options.cwd);
14
20
  const source = await readFile(resolveFromPackage(entry.source), "utf8");
15
- const status = kind === "security" ? "Unverified" : "Unverified";
16
- const heading = kind === "security" ? "Security audit" : "Engineering review";
17
- const destinationRoot = kind === "security"
18
- ? "docs/04-testing/security-audits"
19
- : "docs/03-development/engineering-reviews";
20
- const output = resolve(options.cwd ?? process.cwd(), destinationRoot, `${date()}-${entry.slug}.md`);
21
- const body = `# ${heading}: ${entry.title}\n\n- Date: ${date()}\n- Style: ${options.style ?? "standard"}\n- Overall status: ${status}\n- Reviewer: _Unassigned_\n\n## Scope and authorization\n\n- Target: _Describe the code, configuration, or authorized environment_\n- Authorization: _Required before active security testing_\n- Exclusions: _List exclusions_\n\n## Findings\n\n| Check | Status | Evidence | Action |\n|---|---|---|---|\n| Add each applicable check | ${status} | File/line, command output, or external evidence | Required follow-up |\n\n## Reference checklist\n\n${source}\n`;
22
- const result = await writeSafe(output, body, options);
23
- console.log(`${result.status} ${output}`);
21
+ const reference = source.replace(/^#\s+(.+)$/m, "### $1");
22
+ const isSecurity = kind === "security";
23
+ const heading = isSecurity ? "Security worksheet" : "Engineering worksheet";
24
+ const destinationRoot = isSecurity ? config.securityReports : config.engineeringReports;
25
+ const output = resolve(root, destinationRoot, `${localDate()}-${entry.slug}.md`);
26
+ const context = isSecurity
27
+ ? `## Scope and authorization\n\n- Target: _Describe the code, configuration, or authorized test environment_\n- Authorization: _Required before active testing_\n- Exclusions: _List exclusions_\n- Test safety: _Record rate limits, test data, and stop conditions_`
28
+ : `## Review context\n\n- System or change: _Describe the reviewed scope_\n- Goal: _State the desired engineering outcome_\n- Constraints: _List material technical or delivery constraints_`;
29
+ const columns = isSecurity
30
+ ? "| Control ID | Priority | Status | Evidence | Remediation | Owner | Verification |\n|---|---|---|---|---|---|---|\n| Add each applicable control | Medium | Unverified | File/line, command output, or approved observation | Required follow-up | Unassigned | Not verified |"
31
+ : "| Practice ID | Status | Evidence | Impact | Recommendation | Owner | Verification |\n|---|---|---|---|---|---|---|\n| Add each applicable practice | Unverified | File/line, metric, trace, or test output | State the measurable impact | Recommended follow-up | Unassigned | Not verified |";
32
+ const body = `# ${heading}: ${entry.title}\n\n- Date: ${localDate()}\n- Style: ${options.style ?? config.interactionStyle}\n- Overall status: Unverified\n- Reviewer: _Unassigned_\n\n${context}\n\n## Findings\n\n${columns}\n\n## Reference checklist\n\n${reference.trim()}\n`;
33
+ const result = await writeUnique(output, body, options);
34
+ console.log(`${result.status} ${result.path}`);
24
35
  return result;
25
36
  }
26
37
 
27
38
  export const auditSecurity = (selector, options) => reportFor("security", selector, options);
28
39
  export const reviewEngineering = (selector, options) => reportFor("engineering", selector, options);
29
40
 
41
+ async function markdownFiles(directory) {
42
+ if (!(await exists(directory))) return [];
43
+ const found = [];
44
+ for (const item of await readdir(directory, { withFileTypes: true })) {
45
+ const path = resolve(directory, item.name);
46
+ if (item.isDirectory()) found.push(...await markdownFiles(path));
47
+ else if (item.isFile() && extname(item.name).toLowerCase() === ".md") found.push(path);
48
+ }
49
+ return found;
50
+ }
51
+
52
+ function anchors(markdown) {
53
+ return new Set([...markdown.matchAll(/^#{1,6}\s+(.+)$/gm)].map(([, heading]) => heading.toLowerCase().trim().replace(/[^\p{L}\p{N}\s-]/gu, "").replace(/\s+/g, "-")));
54
+ }
55
+
56
+ function validDate(value) {
57
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(value ?? "")) return false;
58
+ const parsed = new Date(`${value}T00:00:00Z`);
59
+ return !Number.isNaN(parsed.getTime()) && parsed.toISOString().slice(0, 10) === value;
60
+ }
61
+
62
+ function frontMatterValues(block) {
63
+ return Object.fromEntries([...block.matchAll(/^([A-Za-z][\w-]*):\s*(.*?)\s*$/gm)].map((match) => [match[1], match[2].replace(/^['"]|['"]$/g, "")]));
64
+ }
65
+
66
+ function withoutFencedCode(value) {
67
+ return value.replace(/^(```|~~~)[\s\S]*?^\1\s*$/gm, "");
68
+ }
69
+
70
+ async function inspectMarkdown(file, root, config, cache) {
71
+ const readCached = async (path) => {
72
+ if (!cache.has(path)) cache.set(path, await readFile(path, "utf8"));
73
+ return cache.get(path);
74
+ };
75
+ const value = await readCached(file);
76
+ const inspectable = withoutFencedCode(value);
77
+ const issues = [];
78
+ const frontMatter = value.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n/);
79
+ const h1Count = (inspectable.match(/^#\s+/gm) ?? []).length;
80
+ if (h1Count !== 1) issues.push({ rule: "single-h1", path: relative(root, file), detail: `Expected one H1; found ${h1Count}.` });
81
+ const headingCounts = new Map();
82
+ for (const [, heading] of inspectable.matchAll(/^#{1,6}\s+(.+)$/gm)) {
83
+ const key = heading.toLowerCase().trim();
84
+ headingCounts.set(key, (headingCounts.get(key) ?? 0) + 1);
85
+ }
86
+ for (const [heading, count] of headingCounts) {
87
+ if (count > 1) issues.push({ rule: "duplicate-heading", path: relative(root, file), detail: `Heading '${heading}' appears ${count} times.` });
88
+ }
89
+ if (/\b(?:TODO|TBD|FIXME)\b/i.test(inspectable)) issues.push({ rule: "placeholder", path: relative(root, file), detail: "Contains TODO, TBD, or FIXME outside a fenced code block." });
90
+ if (config.requireMetadata && !frontMatter) issues.push({ rule: "metadata", path: relative(root, file), detail: "Required front matter is missing." });
91
+ if (frontMatter) {
92
+ const metadata = frontMatterValues(frontMatter[1]);
93
+ if (config.requireMetadata) {
94
+ for (const key of ["owner", "status", "lastReviewed", "reviewDue"]) {
95
+ if (!metadata[key]) issues.push({ rule: "metadata", path: relative(root, file), detail: `Required front matter field '${key}' is missing.` });
96
+ }
97
+ }
98
+ for (const key of ["lastReviewed", "reviewDue"]) {
99
+ if (metadata[key] && !validDate(metadata[key])) issues.push({ rule: "metadata-date", path: relative(root, file), detail: `Front matter field '${key}' is not a valid YYYY-MM-DD date.` });
100
+ }
101
+ const { lastReviewed, reviewDue } = metadata;
102
+ if (validDate(reviewDue) && reviewDue < localDate()) issues.push({ rule: "review-overdue", path: relative(root, file), detail: `Review was due ${reviewDue}.` });
103
+ if (validDate(lastReviewed)) {
104
+ const ageDays = Math.floor((Date.now() - new Date(`${lastReviewed}T00:00:00`).getTime()) / 86400000);
105
+ if (ageDays > config.maxAgeDays) issues.push({ rule: "stale-document", path: relative(root, file), detail: `Last reviewed ${ageDays} days ago; limit is ${config.maxAgeDays}.` });
106
+ }
107
+ }
108
+ for (const match of inspectable.matchAll(/\[[^\]]+\]\((<[^>]+>|[^\s)]+)(?:\s+["'][^"']*["'])?\)/g)) {
109
+ const rawTarget = match[1].trim().replace(/^<|>$/g, "");
110
+ if (!rawTarget || /^(?:https?:|mailto:)/i.test(rawTarget)) continue;
111
+ const [target, fragment] = rawTarget.split("#", 2);
112
+ let decodedTarget;
113
+ let decodedFragment;
114
+ try {
115
+ decodedTarget = target ? decodeURIComponent(target) : "";
116
+ decodedFragment = fragment ? decodeURIComponent(fragment).toLowerCase() : "";
117
+ } catch {
118
+ issues.push({ rule: "invalid-link", path: relative(root, file), detail: `Invalid URL encoding: ${rawTarget}` });
119
+ continue;
120
+ }
121
+ const linked = decodedTarget ? resolve(dirname(file), decodedTarget) : file;
122
+ if (!(await exists(linked))) issues.push({ rule: "broken-link", path: relative(root, file), detail: `Missing target: ${rawTarget}` });
123
+ else if (fragment && extname(linked).toLowerCase() === ".md") {
124
+ const targetValue = await readCached(linked);
125
+ if (!anchors(targetValue).has(decodedFragment)) issues.push({ rule: "broken-anchor", path: relative(root, file), detail: `Missing anchor: ${rawTarget}` });
126
+ }
127
+ }
128
+ return issues;
129
+ }
130
+
131
+ function renderAudit(result, format) {
132
+ if (format === "json") return JSON.stringify(result, null, 2);
133
+ if (format === "markdown") {
134
+ const rows = result.checks.map((row) => `| ${row.pass ? "PASS" : "FAIL"} | ${row.rule} | ${row.path} | ${row.detail} |`).join("\n");
135
+ return `# Documentation audit\n\n- Checked: ${result.checkedAt}\n- Result: ${result.pass ? "PASS" : "FAIL"}\n\n| Status | Rule | Path | Detail |\n|---|---|---|---|\n${rows}\n`;
136
+ }
137
+ return result.checks.map((row) => `${row.pass ? "PASS" : "FAIL"} ${row.rule} ${row.path}${row.detail ? ` — ${row.detail}` : ""}`).join("\n");
138
+ }
139
+
30
140
  export async function auditDocumentation(options = {}) {
31
- const root = resolve(options.cwd ?? process.cwd());
32
- const required = [
33
- ".agent-docs/config.json",
34
- "docs/README.md",
35
- "docs/01-system/overview.md",
36
- "docs/02-project/README.md",
37
- "docs/05-operations/README.md"
38
- ];
39
- const rows = await Promise.all(required.map(async (path) => ({ path, present: await exists(resolve(root, path)) })));
40
- for (const row of rows) console.log(`${row.present ? "PASS" : "FAIL"} ${row.path}`);
41
- const result = { checkedAt: new Date().toISOString(), pass: rows.every((row) => row.present), files: rows };
42
- if (options.report && !options.dryRun) {
43
- const output = resolve(root, "docs/03-development/audits", `${date()}-documentation.json`);
44
- await mkdir(resolve(output, ".."), { recursive: true });
45
- await writeFile(output, `${JSON.stringify(result, null, 2)}\n`, "utf8");
46
- console.log(`created ${output}`);
141
+ const { root, config, configPath } = await loadProjectConfig(options.cwd);
142
+ const checks = [];
143
+ checks.push({ rule: "configuration", path: relative(root, configPath), pass: await exists(configPath), detail: "Project audit configuration" });
144
+ for (const path of config.requiredDocuments) {
145
+ let pass = false;
146
+ try { pass = (await stat(resolve(root, path))).isFile(); } catch {}
147
+ checks.push({ rule: "required-document", path, pass, detail: "Required file from configuration" });
148
+ }
149
+ const cache = new Map();
150
+ for (const file of await markdownFiles(resolve(root, config.documentationRoot))) {
151
+ for (const issue of await inspectMarkdown(file, root, config, cache)) checks.push({ ...issue, pass: false });
152
+ }
153
+ const result = { checkedAt: new Date().toISOString(), pass: checks.every((row) => row.pass), checks };
154
+ console.log(renderAudit(result, options.format));
155
+ if (options.report) {
156
+ const extension = options.format === "json" ? "json" : options.format === "markdown" ? "md" : "txt";
157
+ const output = resolve(root, config.documentationReports, `${localDate()}-documentation.${extension}`);
158
+ const body = `${renderAudit(result, options.format)}\n`;
159
+ const written = await writeUnique(output, body, options);
160
+ console.log(`${written.status} ${written.path}`);
161
+ result.reportPath = written.path;
47
162
  }
48
163
  return result;
49
164
  }
@@ -1,7 +1,7 @@
1
1
  import { execFileSync } from "node:child_process";
2
2
 
3
3
  export function doctor() {
4
- const checks = [{ name: "Node.js >=20", pass: Number(process.versions.node.split(".")[0]) >= 20, value: process.version }];
4
+ const checks = [{ name: "Node.js >=22", pass: Number(process.versions.node.split(".")[0]) >= 22, value: process.version }];
5
5
  try {
6
6
  checks.push({ name: "Git", pass: true, value: execFileSync("git", ["--version"], { encoding: "utf8" }).trim() });
7
7
  } catch {
@@ -0,0 +1,12 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { loadCatalog, selectEntry } from "../core/catalog.mjs";
3
+ import { resolveFromPackage } from "../core/paths.mjs";
4
+
5
+ export async function showDocumentationGuide(selector) {
6
+ const catalog = await loadCatalog();
7
+ const entry = selectEntry(catalog.documentation, selector);
8
+ if (!entry) throw new Error(`Unknown documentation area '${selector ?? ""}'. Run 'agent-docs list documentation'.`);
9
+ const contents = await readFile(resolveFromPackage(entry.source), "utf8");
10
+ console.log(contents.trim());
11
+ return { entry, contents };
12
+ }
@@ -2,7 +2,7 @@ import { loadCatalog } from "../core/catalog.mjs";
2
2
 
3
3
  export async function listCatalog(kind) {
4
4
  const catalog = await loadCatalog();
5
- const groups = kind ? [kind] : ["security", "engineering"];
5
+ const groups = kind ? [kind] : ["documentation", "security", "engineering"];
6
6
  for (const group of groups) {
7
7
  if (!catalog[group]) throw new Error(`Unknown catalog '${group}'.`);
8
8
  console.log(`\n${group.toUpperCase()}`);
@@ -3,11 +3,16 @@ import { displayResults, writeSafe } from "../core/files.mjs";
3
3
 
4
4
  const files = {
5
5
  ".agent-docs/config.json": `{
6
- "schemaVersion": 1,
6
+ "schemaVersion": 2,
7
7
  "documentationRoot": "docs",
8
+ "documentationReports": "docs/03-development/audits",
8
9
  "securityReports": "docs/04-testing/security-audits",
9
10
  "engineeringReports": "docs/03-development/engineering-reviews",
10
- "interactionStyle": "standard"
11
+ "templateOutput": "docs",
12
+ "interactionStyle": "standard",
13
+ "maxAgeDays": 180,
14
+ "requireMetadata": false,
15
+ "requiredDocuments": ["docs/README.md", "docs/01-system/overview.md", "docs/02-project/README.md", "docs/05-operations/README.md"]
11
16
  }\n`,
12
17
  "docs/README.md": `# Project documentation
13
18
 
@@ -18,7 +23,7 @@ This directory is the maintained project record. Keep decisions and system facts
18
23
  - \`03-development/\`: implementation notes and engineering reviews
19
24
  - \`04-testing/\`: test plans, security audits, and evidence
20
25
  - \`05-operations/\`: setup, deployment, runbooks, and agent operations
21
- - \`06-future-enhancements/\`: proposals that are not current commitments
26
+ - \`06-future-proposals/\`: proposals that are not current commitments
22
27
  - \`archive/\`: superseded material retained for traceability
23
28
  `,
24
29
  "docs/01-system/overview.md": "# System overview\n\nStatus: Needs update\n\nDescribe the system boundary, users, dependencies, and authoritative sources.\n",
@@ -27,7 +32,7 @@ This directory is the maintained project record. Keep decisions and system facts
27
32
  "docs/04-testing/security-audits/.gitkeep": "",
28
33
  "docs/04-testing/evidence/.gitkeep": "",
29
34
  "docs/05-operations/README.md": "# Operations\n\nDocument setup, deployment, observability, incident response, and recovery procedures.\n",
30
- "docs/06-future-enhancements/README.md": "# Future enhancements\n\nIdeas here are proposals, not part of the current scope until accepted.\n",
35
+ "docs/06-future-proposals/README.md": "# Future proposals\n\nIdeas here are proposals, not part of the current scope until accepted.\n",
31
36
  "docs/archive/README.md": "# Archive\n\nKeep superseded documents here with replacement links and archive dates.\n"
32
37
  };
33
38
 
@@ -4,6 +4,7 @@ import { copySafe } from "../core/files.mjs";
4
4
  import { resolveFromPackage } from "../core/paths.mjs";
5
5
 
6
6
  export async function installSkill(target = "project", options = {}) {
7
+ if (!["project", "codex"].includes(target)) throw new Error(`Invalid target '${target}'. Use project or codex.`);
7
8
  const root = resolve(options.cwd ?? process.cwd());
8
9
  const destination = target === "codex"
9
10
  ? resolve(homedir(), ".codex", "skills", "manage-project-quality")