@bossmissing/agent-meta 0.1.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/LICENSE +21 -0
- package/README.md +91 -0
- package/dist/analyzers/duplicate-rule-analyzer.d.ts +31 -0
- package/dist/analyzers/duplicate-rule-analyzer.js +80 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +177 -0
- package/dist/commands/audit.d.ts +7 -0
- package/dist/commands/audit.js +187 -0
- package/dist/commands/check.d.ts +35 -0
- package/dist/commands/check.js +109 -0
- package/dist/commands/config.d.ts +2 -0
- package/dist/commands/config.js +59 -0
- package/dist/commands/init.d.ts +8 -0
- package/dist/commands/init.js +179 -0
- package/dist/commands/new-adr.d.ts +8 -0
- package/dist/commands/new-adr.js +65 -0
- package/dist/commands/new-domain.d.ts +13 -0
- package/dist/commands/new-domain.js +106 -0
- package/dist/commands/sync.d.ts +7 -0
- package/dist/commands/sync.js +46 -0
- package/dist/core/config.d.ts +15 -0
- package/dist/core/config.js +40 -0
- package/dist/core/errors.d.ts +12 -0
- package/dist/core/errors.js +16 -0
- package/dist/core/filesystem.d.ts +33 -0
- package/dist/core/filesystem.js +92 -0
- package/dist/core/frontmatter.d.ts +19 -0
- package/dist/core/frontmatter.js +84 -0
- package/dist/core/logger.d.ts +8 -0
- package/dist/core/logger.js +22 -0
- package/dist/core/paths.d.ts +8 -0
- package/dist/core/paths.js +39 -0
- package/dist/core/risk.d.ts +3 -0
- package/dist/core/risk.js +8 -0
- package/dist/core/templates.d.ts +10 -0
- package/dist/core/templates.js +31 -0
- package/dist/core/types.d.ts +21 -0
- package/dist/core/types.js +7 -0
- package/dist/generators/adr-generator.d.ts +11 -0
- package/dist/generators/adr-generator.js +25 -0
- package/dist/generators/index-generator.d.ts +6 -0
- package/dist/generators/index-generator.js +87 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +10 -0
- package/dist/schemas/config.schema.d.ts +208 -0
- package/dist/schemas/config.schema.js +83 -0
- package/dist/templates/index.d.ts +23 -0
- package/dist/templates/index.js +445 -0
- package/dist/validators/adr-validator.d.ts +3 -0
- package/dist/validators/adr-validator.js +182 -0
- package/dist/validators/agents-validator.d.ts +3 -0
- package/dist/validators/agents-validator.js +82 -0
- package/dist/validators/context.d.ts +12 -0
- package/dist/validators/context.js +1 -0
- package/dist/validators/docs-validator.d.ts +3 -0
- package/dist/validators/docs-validator.js +129 -0
- package/dist/validators/domain-validator.d.ts +3 -0
- package/dist/validators/domain-validator.js +132 -0
- package/dist/validators/openspec-validator.d.ts +2 -0
- package/dist/validators/openspec-validator.js +76 -0
- package/dist/validators/project-validator.d.ts +2 -0
- package/dist/validators/project-validator.js +41 -0
- package/package.json +64 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 agent-meta contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Agent Meta CLI
|
|
2
|
+
|
|
3
|
+
面向 AI 编程工作流的项目元数据管理工具。在项目内建立并维护一套统一、可校验、可扩展的 Agent 协作元数据:根/局部 `AGENTS.md`、领域文档、ADR、文档 frontmatter、OpenSpec 基础结构检查等。
|
|
4
|
+
|
|
5
|
+
完整需求见 [docs/prd.md](./docs/prd.md)。
|
|
6
|
+
|
|
7
|
+
## 安装
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pnpm add -D @bossmissing/agent-meta
|
|
11
|
+
pnpm exec meta init
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
本仓库内开发:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pnpm install
|
|
18
|
+
pnpm dev -- init # tsx 直接运行
|
|
19
|
+
pnpm build # 编译到 dist/
|
|
20
|
+
pnpm test # Vitest
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## 命令
|
|
24
|
+
|
|
25
|
+
| 命令 | 用途 |
|
|
26
|
+
| --- | --- |
|
|
27
|
+
| `meta init` | 初始化 Agent Meta 基础结构(`--yes` 非交互,`--type`,`--dry-run`) |
|
|
28
|
+
| `meta new-domain <name>` | 创建领域文档目录与局部 `AGENTS.md`(`--title` `--owner` `--with-state-machine` `--with-glossary` `--no-local-agents` `--force` `--dry-run`) |
|
|
29
|
+
| `meta new-adr <slug>` | 自动编号创建 ADR(`--title` `--owner` `--status` `--path` `--dry-run`) |
|
|
30
|
+
| `meta check` | 确定性元数据校验(`--strict` `--docs` `--adr` `--agents` `--openspec` `--json`) |
|
|
31
|
+
| `meta audit` | 治理分析报告,不阻塞、不改文件(`--agents` `--docs` `--risk` `--json`) |
|
|
32
|
+
| `meta sync` | 重建 `docs/domain` / `docs/adr` 索引,调用 `openspec update`(`--indexes` `--openspec` `--dry-run`) |
|
|
33
|
+
| `meta config list / set` | 查看与修改 `agent-meta.config.yaml` |
|
|
34
|
+
|
|
35
|
+
## 退出码
|
|
36
|
+
|
|
37
|
+
| 退出码 | 含义 |
|
|
38
|
+
| --- | --- |
|
|
39
|
+
| 0 | 无 error(warning 不阻塞;`--strict` 除外) |
|
|
40
|
+
| 1 | 存在校验 error |
|
|
41
|
+
| 2 | 命令参数错误 |
|
|
42
|
+
| 3 | 配置文件无法解析 |
|
|
43
|
+
| 4 | 文件系统 IO 错误 |
|
|
44
|
+
| 5 | 模板缺失或损坏 |
|
|
45
|
+
|
|
46
|
+
## 典型流程
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
meta init --yes
|
|
50
|
+
|
|
51
|
+
meta new-domain withdrawal \
|
|
52
|
+
--title "提现与打款" \
|
|
53
|
+
--owner payments \
|
|
54
|
+
--with-state-machine
|
|
55
|
+
|
|
56
|
+
meta new-adr withdrawal-idempotency \
|
|
57
|
+
--title "提现打款采用业务幂等键" \
|
|
58
|
+
--owner payments
|
|
59
|
+
|
|
60
|
+
meta check --strict
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## 模板覆盖
|
|
64
|
+
|
|
65
|
+
在项目内创建 `templates/agent-meta/<name>.tmpl` 可覆盖内置模板(仅支持 `{{variable}}` 替换):
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
templates/agent-meta/adr.md.tmpl
|
|
69
|
+
templates/agent-meta/domain-readme.md.tmpl
|
|
70
|
+
templates/agent-meta/domain-rules.md.tmpl
|
|
71
|
+
templates/agent-meta/domain-state-machine.md.tmpl
|
|
72
|
+
templates/agent-meta/domain-agents.md.tmpl
|
|
73
|
+
templates/agent-meta/agents.md.tmpl
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
可用变量:`{{project_name}}` `{{project_type}}` `{{date}}` `{{owner}}` `{{domain_name}}` `{{domain_title}}` `{{adr_number}}` `{{slug}}` `{{status}}`。
|
|
77
|
+
|
|
78
|
+
## CI 集成
|
|
79
|
+
|
|
80
|
+
```yaml
|
|
81
|
+
- run: pnpm exec meta check --strict
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Git Hook(lefthook):
|
|
85
|
+
|
|
86
|
+
```yaml
|
|
87
|
+
pre-commit:
|
|
88
|
+
commands:
|
|
89
|
+
meta-check:
|
|
90
|
+
run: pnpm exec meta check
|
|
91
|
+
```
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export interface RuleLine {
|
|
2
|
+
raw: string;
|
|
3
|
+
normalized: string;
|
|
4
|
+
line: number;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Normalize a markdown line for heuristic rule comparison: strip list
|
|
8
|
+
* markers, inline code/emphasis markers, punctuation, collapse whitespace,
|
|
9
|
+
* lowercase. Returns "" for lines that are not rule-like (headings, blanks,
|
|
10
|
+
* comments, table rows, fences).
|
|
11
|
+
*/
|
|
12
|
+
export declare function normalizeRuleLine(line: string): string;
|
|
13
|
+
export declare function extractRuleLines(content: string): RuleLine[];
|
|
14
|
+
export interface DuplicateRule {
|
|
15
|
+
rule: string;
|
|
16
|
+
rootLine: number;
|
|
17
|
+
localLine: number;
|
|
18
|
+
}
|
|
19
|
+
export interface ConflictRule {
|
|
20
|
+
rootRule: string;
|
|
21
|
+
localRule: string;
|
|
22
|
+
rootLine: number;
|
|
23
|
+
localLine: number;
|
|
24
|
+
}
|
|
25
|
+
export interface AgentsComparison {
|
|
26
|
+
duplicates: DuplicateRule[];
|
|
27
|
+
conflicts: ConflictRule[];
|
|
28
|
+
}
|
|
29
|
+
/** Compare a local AGENTS.md against the root AGENTS.md. */
|
|
30
|
+
export declare function compareAgentsRules(rootContent: string, localContent: string): AgentsComparison;
|
|
31
|
+
export declare function countLines(absPath: string): number;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
/**
|
|
3
|
+
* Normalize a markdown line for heuristic rule comparison: strip list
|
|
4
|
+
* markers, inline code/emphasis markers, punctuation, collapse whitespace,
|
|
5
|
+
* lowercase. Returns "" for lines that are not rule-like (headings, blanks,
|
|
6
|
+
* comments, table rows, fences).
|
|
7
|
+
*/
|
|
8
|
+
export function normalizeRuleLine(line) {
|
|
9
|
+
const trimmed = line.trim();
|
|
10
|
+
if (trimmed === "" ||
|
|
11
|
+
trimmed.startsWith("#") ||
|
|
12
|
+
trimmed.startsWith("<!--") ||
|
|
13
|
+
trimmed.startsWith("|") ||
|
|
14
|
+
trimmed.startsWith("```") ||
|
|
15
|
+
trimmed.startsWith("~~~") ||
|
|
16
|
+
trimmed.startsWith("---")) {
|
|
17
|
+
return "";
|
|
18
|
+
}
|
|
19
|
+
const normalized = trimmed
|
|
20
|
+
.replace(/^[-*+]\s+/, "")
|
|
21
|
+
.replace(/^\d+\.\s+/, "")
|
|
22
|
+
.replace(/[`*_]/g, "")
|
|
23
|
+
.toLowerCase()
|
|
24
|
+
.replace(/[^\p{L}\p{N}\s]/gu, "")
|
|
25
|
+
.replace(/\s+/g, " ")
|
|
26
|
+
.trim();
|
|
27
|
+
// Very short fragments produce false positives.
|
|
28
|
+
return normalized.length >= 12 ? normalized : "";
|
|
29
|
+
}
|
|
30
|
+
export function extractRuleLines(content) {
|
|
31
|
+
const out = [];
|
|
32
|
+
let inFence = false;
|
|
33
|
+
content.split(/\r?\n/).forEach((raw, idx) => {
|
|
34
|
+
if (/^\s*(```|~~~)/.test(raw)) {
|
|
35
|
+
inFence = !inFence;
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (inFence)
|
|
39
|
+
return;
|
|
40
|
+
const normalized = normalizeRuleLine(raw);
|
|
41
|
+
if (normalized)
|
|
42
|
+
out.push({ raw: raw.trim(), normalized, line: idx + 1 });
|
|
43
|
+
});
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
/** Compare a local AGENTS.md against the root AGENTS.md. */
|
|
47
|
+
export function compareAgentsRules(rootContent, localContent) {
|
|
48
|
+
const rootRules = extractRuleLines(rootContent);
|
|
49
|
+
const localRules = extractRuleLines(localContent);
|
|
50
|
+
const rootByNorm = new Map(rootRules.map((r) => [r.normalized, r]));
|
|
51
|
+
const duplicates = [];
|
|
52
|
+
const conflicts = [];
|
|
53
|
+
for (const local of localRules) {
|
|
54
|
+
const dup = rootByNorm.get(local.normalized);
|
|
55
|
+
if (dup) {
|
|
56
|
+
duplicates.push({ rule: local.raw, rootLine: dup.line, localLine: local.line });
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
// Potential conflict: local "must not X" vs root "must X" (and vice versa).
|
|
60
|
+
const localWithoutNot = local.normalized
|
|
61
|
+
.replace(/\bmust not\b/, "must")
|
|
62
|
+
.replace(/\bdo not\b/, "do")
|
|
63
|
+
.replace(/\bnever\b/, "always");
|
|
64
|
+
if (localWithoutNot !== local.normalized) {
|
|
65
|
+
const conflicting = rootByNorm.get(localWithoutNot);
|
|
66
|
+
if (conflicting) {
|
|
67
|
+
conflicts.push({
|
|
68
|
+
rootRule: conflicting.raw,
|
|
69
|
+
localRule: local.raw,
|
|
70
|
+
rootLine: conflicting.line,
|
|
71
|
+
localLine: local.line,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return { duplicates, conflicts };
|
|
77
|
+
}
|
|
78
|
+
export function countLines(absPath) {
|
|
79
|
+
return fs.readFileSync(absPath, "utf8").split(/\r?\n/).length;
|
|
80
|
+
}
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { Command } from "commander";
|
|
4
|
+
import { EXIT_CODES, MetaError } from "./core/errors.js";
|
|
5
|
+
import { logger } from "./core/logger.js";
|
|
6
|
+
import { runInit } from "./commands/init.js";
|
|
7
|
+
import { runNewDomain } from "./commands/new-domain.js";
|
|
8
|
+
import { runNewAdr } from "./commands/new-adr.js";
|
|
9
|
+
import { runCheck, printCheckResult, checkExitCode, toJsonOutput } from "./commands/check.js";
|
|
10
|
+
import { runAudit } from "./commands/audit.js";
|
|
11
|
+
import { runSync } from "./commands/sync.js";
|
|
12
|
+
import { runConfigList, runConfigSet } from "./commands/config.js";
|
|
13
|
+
const require = createRequire(import.meta.url);
|
|
14
|
+
const pkg = require("../package.json");
|
|
15
|
+
const program = new Command();
|
|
16
|
+
const cwd = process.cwd();
|
|
17
|
+
function handleError(e) {
|
|
18
|
+
if (e instanceof MetaError) {
|
|
19
|
+
logger.error(e.message);
|
|
20
|
+
process.exit(e.exitCode);
|
|
21
|
+
}
|
|
22
|
+
if (e instanceof Error && e.name === "ExitPromptError") {
|
|
23
|
+
// User pressed Ctrl+C inside an inquirer prompt.
|
|
24
|
+
process.exit(EXIT_CODES.OK);
|
|
25
|
+
}
|
|
26
|
+
const err = e;
|
|
27
|
+
if (err?.code === "EACCES" || err?.code === "EPERM" || err?.code === "ENOSPC") {
|
|
28
|
+
logger.error(`File system error: ${err.message}`);
|
|
29
|
+
process.exit(EXIT_CODES.IO_ERROR);
|
|
30
|
+
}
|
|
31
|
+
logger.error(e?.message ?? String(e));
|
|
32
|
+
process.exit(EXIT_CODES.CHECK_FAILED);
|
|
33
|
+
}
|
|
34
|
+
program
|
|
35
|
+
.name("meta")
|
|
36
|
+
.description("Agent Meta CLI — manage AI agent collaboration metadata")
|
|
37
|
+
.version(pkg.version, "-v, --version", "output the version number")
|
|
38
|
+
.exitOverride((err) => {
|
|
39
|
+
if (err.code === "commander.helpDisplayed" || err.code === "commander.version") {
|
|
40
|
+
process.exit(EXIT_CODES.OK);
|
|
41
|
+
}
|
|
42
|
+
process.exit(EXIT_CODES.BAD_ARGS);
|
|
43
|
+
});
|
|
44
|
+
program
|
|
45
|
+
.command("init")
|
|
46
|
+
.description("Initialize agent metadata infrastructure in the current project")
|
|
47
|
+
.option("-y, --yes", "non-interactive mode, accept all defaults")
|
|
48
|
+
.option("--type <type>", "project type: node | monorepo | nextjs | cocos | backend | other")
|
|
49
|
+
.option("--dry-run", "show what would be created without writing files")
|
|
50
|
+
.action(async (opts) => {
|
|
51
|
+
try {
|
|
52
|
+
await runInit(cwd, opts);
|
|
53
|
+
}
|
|
54
|
+
catch (e) {
|
|
55
|
+
handleError(e);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
program
|
|
59
|
+
.command("new-domain")
|
|
60
|
+
.description("Create a domain documentation directory (and optional local AGENTS.md)")
|
|
61
|
+
.argument("<name>", "domain name, e.g. withdrawal or 'Payment Callback'")
|
|
62
|
+
.option("--title <title>", "human-readable title (Chinese allowed)")
|
|
63
|
+
.option("--owner <owner>", "owner team or person")
|
|
64
|
+
.option("--path <path>", "custom target directory")
|
|
65
|
+
.option("--with-state-machine", "also create state-machine.md")
|
|
66
|
+
.option("--with-glossary", "also create glossary.md")
|
|
67
|
+
.option("--with-local-agents", "also create a local AGENTS.md")
|
|
68
|
+
.option("--no-local-agents", "do not create a local AGENTS.md")
|
|
69
|
+
.option("--force", "create missing files in an existing domain directory (never overwrites)")
|
|
70
|
+
.option("--dry-run", "show what would be created without writing files")
|
|
71
|
+
.action((name, opts) => {
|
|
72
|
+
try {
|
|
73
|
+
runNewDomain(cwd, name, opts);
|
|
74
|
+
}
|
|
75
|
+
catch (e) {
|
|
76
|
+
handleError(e);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
program
|
|
80
|
+
.command("new-adr")
|
|
81
|
+
.description("Create a new ADR with an auto-incremented number")
|
|
82
|
+
.argument("<slug>", "ADR slug, e.g. payment-callback-idempotency")
|
|
83
|
+
.option("--title <title>", "ADR title (Chinese allowed)")
|
|
84
|
+
.option("--owner <owner>", "owner team or person")
|
|
85
|
+
.option("--status <status>", "proposed | accepted | deprecated | superseded | rejected")
|
|
86
|
+
.option("--path <path>", "custom ADR directory")
|
|
87
|
+
.option("--dry-run", "show what would be created without writing files")
|
|
88
|
+
.action((slug, opts) => {
|
|
89
|
+
try {
|
|
90
|
+
runNewAdr(cwd, slug, opts);
|
|
91
|
+
}
|
|
92
|
+
catch (e) {
|
|
93
|
+
handleError(e);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
program
|
|
97
|
+
.command("check")
|
|
98
|
+
.description("Run deterministic metadata checks (for local dev, git hooks and CI)")
|
|
99
|
+
.option("--strict", "treat warnings as errors")
|
|
100
|
+
.option("--docs", "only check document frontmatter")
|
|
101
|
+
.option("--adr", "only check ADRs")
|
|
102
|
+
.option("--agents", "only check AGENTS.md files")
|
|
103
|
+
.option("--openspec", "only check OpenSpec structure")
|
|
104
|
+
.option("--json", "machine-readable JSON output")
|
|
105
|
+
.action((opts) => {
|
|
106
|
+
try {
|
|
107
|
+
const result = runCheck(cwd, opts);
|
|
108
|
+
if (opts.json) {
|
|
109
|
+
console.log(JSON.stringify(toJsonOutput(result), null, 2));
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
printCheckResult(result, Boolean(opts.strict));
|
|
113
|
+
}
|
|
114
|
+
process.exit(checkExitCode(result, Boolean(opts.strict)));
|
|
115
|
+
}
|
|
116
|
+
catch (e) {
|
|
117
|
+
handleError(e);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
program
|
|
121
|
+
.command("audit")
|
|
122
|
+
.description("Governance-oriented analysis report (never blocks, never edits files)")
|
|
123
|
+
.option("--json", "machine-readable JSON output")
|
|
124
|
+
.option("--agents", "only audit AGENTS.md files")
|
|
125
|
+
.option("--docs", "only audit documentation health")
|
|
126
|
+
.option("--risk", "only audit high-risk domain coverage")
|
|
127
|
+
.action((opts) => {
|
|
128
|
+
try {
|
|
129
|
+
runAudit(cwd, opts);
|
|
130
|
+
}
|
|
131
|
+
catch (e) {
|
|
132
|
+
handleError(e);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
program
|
|
136
|
+
.command("sync")
|
|
137
|
+
.description("Regenerate documentation indexes and sync OpenSpec")
|
|
138
|
+
.option("--indexes", "only regenerate docs/domain and docs/adr indexes")
|
|
139
|
+
.option("--openspec", "only run openspec update")
|
|
140
|
+
.option("--templates", "sync team template packs (planned)")
|
|
141
|
+
.option("--dry-run", "show what would be updated without writing files")
|
|
142
|
+
.action((opts) => {
|
|
143
|
+
try {
|
|
144
|
+
runSync(cwd, opts);
|
|
145
|
+
}
|
|
146
|
+
catch (e) {
|
|
147
|
+
handleError(e);
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
const configCmd = program
|
|
151
|
+
.command("config")
|
|
152
|
+
.description("View and modify agent-meta.config.yaml");
|
|
153
|
+
configCmd
|
|
154
|
+
.command("list")
|
|
155
|
+
.description("Print the effective configuration")
|
|
156
|
+
.action(() => {
|
|
157
|
+
try {
|
|
158
|
+
runConfigList(cwd);
|
|
159
|
+
}
|
|
160
|
+
catch (e) {
|
|
161
|
+
handleError(e);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
configCmd
|
|
165
|
+
.command("set")
|
|
166
|
+
.description("Set a configuration value, e.g. meta config set defaults.agents_max_lines 220")
|
|
167
|
+
.argument("<key>", "dot-separated key, e.g. defaults.review_interval_days")
|
|
168
|
+
.argument("<value>", "value")
|
|
169
|
+
.action((key, value) => {
|
|
170
|
+
try {
|
|
171
|
+
runConfigSet(cwd, key, value);
|
|
172
|
+
}
|
|
173
|
+
catch (e) {
|
|
174
|
+
handleError(e);
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
program.parseAsync(process.argv).catch(handleError);
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import pc from "picocolors";
|
|
4
|
+
import { loadConfig } from "../core/config.js";
|
|
5
|
+
import { parseDocFile } from "../core/frontmatter.js";
|
|
6
|
+
import { dateValueToString, daysSince } from "../core/paths.js";
|
|
7
|
+
import { matchRiskKeywords } from "../core/risk.js";
|
|
8
|
+
import { compareAgentsRules } from "../analyzers/duplicate-rule-analyzer.js";
|
|
9
|
+
import { findLocalAgentsFiles } from "../validators/agents-validator.js";
|
|
10
|
+
import { listDomainDirs } from "../validators/domain-validator.js";
|
|
11
|
+
import { listAdrFiles } from "../generators/adr-generator.js";
|
|
12
|
+
import fg from "fast-glob";
|
|
13
|
+
export function runAudit(root, opts) {
|
|
14
|
+
const { config } = loadConfig(root);
|
|
15
|
+
const all = !opts.agents && !opts.docs && !opts.risk;
|
|
16
|
+
const report = {};
|
|
17
|
+
// --- Agents audit -------------------------------------------------------
|
|
18
|
+
let agentsAudit = null;
|
|
19
|
+
if (all || opts.agents) {
|
|
20
|
+
const rootAgentsAbs = path.join(root, config.paths.root_agents);
|
|
21
|
+
const rootContent = fs.existsSync(rootAgentsAbs)
|
|
22
|
+
? fs.readFileSync(rootAgentsAbs, "utf8")
|
|
23
|
+
: null;
|
|
24
|
+
const localFiles = findLocalAgentsFiles(root, config.paths.root_agents);
|
|
25
|
+
let duplicateRules = 0;
|
|
26
|
+
let conflictingRules = 0;
|
|
27
|
+
if (rootContent) {
|
|
28
|
+
for (const rel of localFiles) {
|
|
29
|
+
const { duplicates, conflicts } = compareAgentsRules(rootContent, fs.readFileSync(path.join(root, rel), "utf8"));
|
|
30
|
+
duplicateRules += duplicates.length;
|
|
31
|
+
conflictingRules += conflicts.length;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
agentsAudit = {
|
|
35
|
+
rootLines: rootContent ? rootContent.split(/\r?\n/).length : null,
|
|
36
|
+
rootMaxLines: config.defaults.agents_max_lines,
|
|
37
|
+
localFiles,
|
|
38
|
+
duplicateRules,
|
|
39
|
+
conflictingRules,
|
|
40
|
+
};
|
|
41
|
+
report.agents = agentsAudit;
|
|
42
|
+
}
|
|
43
|
+
// --- Docs health --------------------------------------------------------
|
|
44
|
+
let docsAudit = null;
|
|
45
|
+
if (all || opts.docs) {
|
|
46
|
+
const files = fg
|
|
47
|
+
.sync(config.validation.require_doc_frontmatter, { cwd: root, ignore: ["**/AGENTS.md"] })
|
|
48
|
+
.sort();
|
|
49
|
+
const byStatus = {};
|
|
50
|
+
let noOwner = 0;
|
|
51
|
+
let stale = 0;
|
|
52
|
+
for (const rel of files) {
|
|
53
|
+
const doc = parseDocFile(path.join(root, rel));
|
|
54
|
+
const status = typeof doc.data["status"] === "string" ? doc.data["status"] : "unknown";
|
|
55
|
+
byStatus[status] = (byStatus[status] ?? 0) + 1;
|
|
56
|
+
const owner = doc.data["owner"];
|
|
57
|
+
if (!owner || (Array.isArray(owner) && owner.length === 0))
|
|
58
|
+
noOwner++;
|
|
59
|
+
const reviewed = dateValueToString(doc.data["last_reviewed"]);
|
|
60
|
+
const staleDays = status === "active"
|
|
61
|
+
? config.validation.stale_review_days.active
|
|
62
|
+
: status === "draft"
|
|
63
|
+
? config.validation.stale_review_days.draft
|
|
64
|
+
: null;
|
|
65
|
+
if (reviewed && staleDays !== null && daysSince(reviewed) > staleDays)
|
|
66
|
+
stale++;
|
|
67
|
+
}
|
|
68
|
+
// Orphan ADRs: not referenced by any other markdown file in the repo.
|
|
69
|
+
const adrDir = path.join(root, config.paths.adr_docs);
|
|
70
|
+
const adrs = listAdrFiles(adrDir);
|
|
71
|
+
const allMd = fg.sync("**/*.md", {
|
|
72
|
+
cwd: root,
|
|
73
|
+
ignore: ["node_modules/**", ".git/**", "dist/**"],
|
|
74
|
+
});
|
|
75
|
+
const corpus = allMd
|
|
76
|
+
.filter((rel) => !rel.startsWith(config.paths.adr_docs))
|
|
77
|
+
.map((rel) => fs.readFileSync(path.join(root, rel), "utf8"))
|
|
78
|
+
.join("\n");
|
|
79
|
+
const orphanAdrs = adrs
|
|
80
|
+
.filter((a) => !corpus.includes(path.basename(a.file)))
|
|
81
|
+
.map((a) => path.basename(a.file));
|
|
82
|
+
const domainsWithoutReadme = listDomainDirs(root, config.paths.domain_docs).filter((d) => !fs.existsSync(path.join(root, config.paths.domain_docs, d, "README.md")));
|
|
83
|
+
docsAudit = {
|
|
84
|
+
totalDocs: files.length,
|
|
85
|
+
byStatus,
|
|
86
|
+
noOwner,
|
|
87
|
+
stale,
|
|
88
|
+
adrTotal: adrs.length,
|
|
89
|
+
orphanAdrs,
|
|
90
|
+
domainsWithoutReadme,
|
|
91
|
+
};
|
|
92
|
+
report.docs = docsAudit;
|
|
93
|
+
}
|
|
94
|
+
// --- Risk coverage ------------------------------------------------------
|
|
95
|
+
let riskAudit = null;
|
|
96
|
+
if (all || opts.risk) {
|
|
97
|
+
riskAudit = [];
|
|
98
|
+
for (const domain of listDomainDirs(root, config.paths.domain_docs)) {
|
|
99
|
+
const matched = matchRiskKeywords(domain, config.risk_keywords);
|
|
100
|
+
if (matched.length === 0)
|
|
101
|
+
continue;
|
|
102
|
+
const dir = path.join(root, config.paths.domain_docs, domain);
|
|
103
|
+
const coverage = {
|
|
104
|
+
"README.md": fs.existsSync(path.join(dir, "README.md")),
|
|
105
|
+
"rules.md": fs.existsSync(path.join(dir, "rules.md")),
|
|
106
|
+
"local AGENTS.md": fs.existsSync(path.join(dir, "AGENTS.md")),
|
|
107
|
+
"state-machine.md": fs.existsSync(path.join(dir, "state-machine.md")),
|
|
108
|
+
};
|
|
109
|
+
const recommendations = [];
|
|
110
|
+
if (!coverage["rules.md"])
|
|
111
|
+
recommendations.push("Add rules.md with invariants and boundaries.");
|
|
112
|
+
if (!coverage["local AGENTS.md"])
|
|
113
|
+
recommendations.push("Add a local AGENTS.md.");
|
|
114
|
+
if (!coverage["state-machine.md"])
|
|
115
|
+
recommendations.push("Add a state machine.");
|
|
116
|
+
recommendations.push(`Consider an ADR for ${domain} idempotency / retry / audit logging.`);
|
|
117
|
+
riskAudit.push({ domain, matched, coverage, recommendations });
|
|
118
|
+
}
|
|
119
|
+
report.risk = riskAudit;
|
|
120
|
+
}
|
|
121
|
+
if (opts.json) {
|
|
122
|
+
console.log(JSON.stringify(report, null, 2));
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
// --- Text report --------------------------------------------------------
|
|
126
|
+
console.log(pc.bold("Agent Meta Audit"));
|
|
127
|
+
console.log();
|
|
128
|
+
if (agentsAudit) {
|
|
129
|
+
console.log(pc.bold("AGENTS.md Audit"));
|
|
130
|
+
console.log();
|
|
131
|
+
if (agentsAudit.rootLines === null) {
|
|
132
|
+
console.log("Root AGENTS.md: missing");
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
console.log(`Root AGENTS.md: ${agentsAudit.rootLines} lines`);
|
|
136
|
+
if (agentsAudit.rootLines > agentsAudit.rootMaxLines) {
|
|
137
|
+
console.log("Recommendation: split into architecture, domain, and engineering documentation.");
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
console.log();
|
|
141
|
+
console.log(`Local AGENTS.md files: ${agentsAudit.localFiles.length}`);
|
|
142
|
+
for (const f of agentsAudit.localFiles)
|
|
143
|
+
console.log(`- ${f}`);
|
|
144
|
+
console.log();
|
|
145
|
+
console.log(`Potential duplicate rules: ${agentsAudit.duplicateRules}`);
|
|
146
|
+
console.log(`Potential conflicting rules: ${agentsAudit.conflictingRules}`);
|
|
147
|
+
console.log();
|
|
148
|
+
}
|
|
149
|
+
if (docsAudit) {
|
|
150
|
+
console.log(pc.bold("Docs Health"));
|
|
151
|
+
console.log();
|
|
152
|
+
console.log(`Total documents: ${docsAudit.totalDocs}`);
|
|
153
|
+
const statusLine = Object.entries(docsAudit.byStatus)
|
|
154
|
+
.map(([k, v]) => `${k}: ${v}`)
|
|
155
|
+
.join(", ");
|
|
156
|
+
console.log(`Status distribution: ${statusLine || "n/a"}`);
|
|
157
|
+
console.log(`Documents without owner: ${docsAudit.noOwner}`);
|
|
158
|
+
console.log(`Stale documents: ${docsAudit.stale}`);
|
|
159
|
+
console.log(`ADRs: ${docsAudit.adrTotal} (orphan: ${docsAudit.orphanAdrs.length})`);
|
|
160
|
+
for (const o of docsAudit.orphanAdrs)
|
|
161
|
+
console.log(`- orphan: ${o}`);
|
|
162
|
+
if (docsAudit.domainsWithoutReadme.length > 0) {
|
|
163
|
+
console.log(`Domains without README: ${docsAudit.domainsWithoutReadme.join(", ")}`);
|
|
164
|
+
}
|
|
165
|
+
console.log();
|
|
166
|
+
}
|
|
167
|
+
if (riskAudit) {
|
|
168
|
+
console.log(pc.bold("High-Risk Domains"));
|
|
169
|
+
console.log();
|
|
170
|
+
if (riskAudit.length === 0) {
|
|
171
|
+
console.log("No high-risk domains detected.");
|
|
172
|
+
}
|
|
173
|
+
for (const r of riskAudit) {
|
|
174
|
+
console.log(`High-risk domain detected: ${r.domain} (matched: ${r.matched.join(", ")})`);
|
|
175
|
+
console.log();
|
|
176
|
+
console.log("Current coverage:");
|
|
177
|
+
for (const [file, ok] of Object.entries(r.coverage)) {
|
|
178
|
+
console.log(`${ok ? pc.green("✓") : pc.red("✗")} ${file}`);
|
|
179
|
+
}
|
|
180
|
+
console.log();
|
|
181
|
+
console.log("Recommendation:");
|
|
182
|
+
for (const rec of r.recommendations)
|
|
183
|
+
console.log(`- ${rec}`);
|
|
184
|
+
console.log();
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type MetaCheckResult } from "../core/types.js";
|
|
2
|
+
export interface CheckOptions {
|
|
3
|
+
strict?: boolean;
|
|
4
|
+
docs?: boolean;
|
|
5
|
+
adr?: boolean;
|
|
6
|
+
agents?: boolean;
|
|
7
|
+
openspec?: boolean;
|
|
8
|
+
json?: boolean;
|
|
9
|
+
}
|
|
10
|
+
export declare function runCheck(root: string, opts?: CheckOptions): MetaCheckResult;
|
|
11
|
+
export declare function printCheckResult(result: MetaCheckResult, strict: boolean): void;
|
|
12
|
+
export declare function checkExitCode(result: MetaCheckResult, strict: boolean): number;
|
|
13
|
+
export declare function toJsonOutput(result: MetaCheckResult): {
|
|
14
|
+
status: import("../core/types.js").CheckStatus;
|
|
15
|
+
errors: {
|
|
16
|
+
code: string;
|
|
17
|
+
file: string | undefined;
|
|
18
|
+
line: number | undefined;
|
|
19
|
+
message: string;
|
|
20
|
+
suggestion: string | undefined;
|
|
21
|
+
}[];
|
|
22
|
+
warnings: {
|
|
23
|
+
code: string;
|
|
24
|
+
file: string | undefined;
|
|
25
|
+
line: number | undefined;
|
|
26
|
+
message: string;
|
|
27
|
+
suggestion: string | undefined;
|
|
28
|
+
}[];
|
|
29
|
+
summary: {
|
|
30
|
+
errors: number;
|
|
31
|
+
warnings: number;
|
|
32
|
+
infos: number;
|
|
33
|
+
checkedFiles: number;
|
|
34
|
+
};
|
|
35
|
+
};
|