@bossmissing/agent-meta 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -2
- package/assets/skills/ironbank/ironbank-citations/SKILL.md +195 -0
- package/assets/skills/ironbank/ironbank-docs-research/SKILL.md +205 -0
- package/assets/skills/ironbank/ironbank-mcp-reference/SKILL.md +413 -0
- package/dist/cli.js +72 -1
- package/dist/commands/config.d.ts +5 -0
- package/dist/commands/config.js +50 -0
- package/dist/commands/doctor.d.ts +14 -0
- package/dist/commands/doctor.js +187 -0
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.js +39 -1
- package/dist/commands/new-change.d.ts +12 -0
- package/dist/commands/new-change.js +125 -0
- package/dist/commands/new-domain.js +2 -0
- package/dist/commands/preflight.d.ts +14 -0
- package/dist/commands/preflight.js +103 -0
- package/dist/commands/review.d.ts +18 -0
- package/dist/commands/review.js +102 -0
- package/dist/core/checklist.d.ts +31 -0
- package/dist/core/checklist.js +43 -0
- package/dist/core/doc-scopes.d.ts +18 -0
- package/dist/core/doc-scopes.js +55 -0
- package/dist/core/frontmatter.d.ts +7 -0
- package/dist/core/frontmatter.js +35 -0
- package/dist/core/mcp-packs.d.ts +26 -0
- package/dist/core/mcp-packs.js +83 -0
- package/dist/generators/hook-generator.d.ts +14 -0
- package/dist/generators/hook-generator.js +55 -0
- package/dist/generators/mcp-generator.d.ts +9 -0
- package/dist/generators/mcp-generator.js +68 -0
- package/dist/templates/index.d.ts +7 -2
- package/dist/templates/index.js +125 -9
- package/dist/validators/domain-validator.js +30 -1
- package/dist/validators/openspec-validator.js +33 -4
- package/package.json +9 -15
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { checkbox } from "@inquirer/prompts";
|
|
2
|
+
import { loadConfig } from "../core/config.js";
|
|
3
|
+
import { runCheck } from "./check.js";
|
|
4
|
+
import { listDomainDirs } from "../validators/domain-validator.js";
|
|
5
|
+
import { isHighRisk } from "../core/risk.js";
|
|
6
|
+
import { resolveDocScopes } from "../core/doc-scopes.js";
|
|
7
|
+
import { printChecklist, checklistToJson } from "../core/checklist.js";
|
|
8
|
+
import { logger } from "../core/logger.js";
|
|
9
|
+
/**
|
|
10
|
+
* Periodic repository review checklist. Items that map to deterministic
|
|
11
|
+
* `meta check` rules are evaluated live; the rest is a manual routine for
|
|
12
|
+
* the reviewing human (or agent) covering docs, code, tests and security.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* @param selectedScopes doc-scope keys confirmed by the human; undefined
|
|
16
|
+
* means every scope is listed (missing ones marked as such).
|
|
17
|
+
*/
|
|
18
|
+
export function buildReviewChecklist(root, selectedScopes) {
|
|
19
|
+
const { config } = loadConfig(root);
|
|
20
|
+
const result = runCheck(root, {});
|
|
21
|
+
const files = (codes) => result.issues
|
|
22
|
+
.filter((i) => codes.includes(i.code))
|
|
23
|
+
.map((i) => i.file ?? i.message);
|
|
24
|
+
const highRisk = listDomainDirs(root, config.paths.domain_docs).filter((d) => isHighRisk(d, config.risk_keywords));
|
|
25
|
+
const auto = (id, section, text, offending) => ({
|
|
26
|
+
id,
|
|
27
|
+
section,
|
|
28
|
+
text,
|
|
29
|
+
kind: "auto",
|
|
30
|
+
status: offending.length === 0 ? "pass" : "fail",
|
|
31
|
+
details: offending,
|
|
32
|
+
});
|
|
33
|
+
const manual = (id, section, text, details) => ({
|
|
34
|
+
id,
|
|
35
|
+
section,
|
|
36
|
+
text,
|
|
37
|
+
kind: "manual",
|
|
38
|
+
status: "pending",
|
|
39
|
+
details,
|
|
40
|
+
});
|
|
41
|
+
const DOCS = "Docs(优化文档)";
|
|
42
|
+
const CODE = "Code(review 代码)";
|
|
43
|
+
const TESTS = "Tests(review 测试用例)";
|
|
44
|
+
const SECURITY = "Security(review 安全)";
|
|
45
|
+
return [
|
|
46
|
+
// Docs
|
|
47
|
+
auto("doc-stale", DOCS, "All constrained docs reviewed within their interval", files(["DOC_STALE"])),
|
|
48
|
+
auto("doc-owner", DOCS, "Every constrained doc has an owner", files(["DOC_FRONTMATTER_MISSING_OWNER", "DOMAIN_NO_OWNER"])),
|
|
49
|
+
auto("doc-hard-constraints", DOCS, "Every high-risk domain declares hard constraints", files(["HIGH_RISK_DOMAIN_MISSING_HARD_CONSTRAINTS"])),
|
|
50
|
+
manual("doc-hard-drift", DOCS, "Check for constraint drift: hard constraints enforced in code but not declared in rules.md / constraints.md"),
|
|
51
|
+
auto("doc-agents-budget", DOCS, "AGENTS.md files are within their line budget", files(["AGENTS_TOO_LONG"])),
|
|
52
|
+
auto("doc-duplicates", DOCS, "No duplicated or conflicting agent rules", files(["AGENTS_DUPLICATE_RULE", "AGENTS_POTENTIAL_CONFLICT"])),
|
|
53
|
+
// One item per documentation scope; the human confirms which scopes to
|
|
54
|
+
// tidy this round via --pick-docs.
|
|
55
|
+
...resolveDocScopes(root, config)
|
|
56
|
+
.filter((s) => !selectedScopes || selectedScopes.includes(s.key))
|
|
57
|
+
.map((s) => manual(`doc-scope-${s.key}`, DOCS, s.exists
|
|
58
|
+
? `整理 ${s.title}:re-read, fix drift, bump last_reviewed(${s.files.length} file(s))`
|
|
59
|
+
: `整理 ${s.title}:not present yet(${s.paths.join(" / ")})— create it if this project needs it`, s.exists ? s.files : undefined)),
|
|
60
|
+
manual("doc-indexes", DOCS, "Run `meta sync --indexes` after the docs are tidied"),
|
|
61
|
+
// Code
|
|
62
|
+
manual("code-highrisk", CODE, "Review high-risk domain code against its rules.md", highRisk.length > 0 ? highRisk : undefined),
|
|
63
|
+
manual("code-local-agents", CODE, "Verify local AGENTS.md rules still match the code"),
|
|
64
|
+
manual("code-thirdparty", CODE, "Confirm no third-party or vendored code was modified"),
|
|
65
|
+
// Tests
|
|
66
|
+
manual("test-highrisk", TESTS, "High-risk domains have tests for normal, boundary, retry and idempotency paths", highRisk.length > 0 ? highRisk : undefined),
|
|
67
|
+
manual("test-flaky", TESTS, "Review skipped, flaky and long-quarantined tests"),
|
|
68
|
+
manual("test-conventions", TESTS, "Test suite still follows docs/engineering/testing.md"),
|
|
69
|
+
// Security
|
|
70
|
+
manual("sec-permissions", SECURITY, "Permission and authorization checks match the documented rules"),
|
|
71
|
+
manual("sec-audit-log", SECURITY, "Audit logging still covers all high-risk flows"),
|
|
72
|
+
manual("sec-deps", SECURITY, "Run a dependency audit (e.g. pnpm audit) and triage findings"),
|
|
73
|
+
manual("sec-secrets", SECURITY, "No secrets or credentials committed; .env handling reviewed"),
|
|
74
|
+
manual("sec-callbacks", SECURITY, "Webhook/callback signature validation and deduplication are intact"),
|
|
75
|
+
];
|
|
76
|
+
}
|
|
77
|
+
export async function runReview(root, opts = {}, pick = (scopes) => checkbox({
|
|
78
|
+
message: "本轮整理哪些文档?(空格勾选,回车确认)",
|
|
79
|
+
choices: scopes.map((s) => ({
|
|
80
|
+
name: s.exists ? `${s.title}(${s.files.length} file(s))` : `${s.title}(未创建)`,
|
|
81
|
+
value: s.key,
|
|
82
|
+
checked: s.exists,
|
|
83
|
+
})),
|
|
84
|
+
})) {
|
|
85
|
+
let selected;
|
|
86
|
+
if (opts.pickDocs) {
|
|
87
|
+
const { config } = loadConfig(root);
|
|
88
|
+
selected = await pick(resolveDocScopes(root, config));
|
|
89
|
+
if (selected.length === 0)
|
|
90
|
+
logger.info("No doc scopes selected — docs items omitted.");
|
|
91
|
+
}
|
|
92
|
+
const items = buildReviewChecklist(root, selected);
|
|
93
|
+
if (opts.json) {
|
|
94
|
+
console.log(JSON.stringify(checklistToJson(items), null, 2));
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
printChecklist("Periodic Repository Review Checklist", items);
|
|
98
|
+
const failed = items.filter((i) => i.status === "fail");
|
|
99
|
+
if (failed.length > 0) {
|
|
100
|
+
console.log(`Run \`meta check\` for details on the ${failed.length} failing item(s).`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A checklist mixes two kinds of items: "auto" items are evaluated by the
|
|
3
|
+
* CLI against the repository (pass/fail), "manual" items require a human
|
|
4
|
+
* and stay pending until confirmed.
|
|
5
|
+
*/
|
|
6
|
+
export interface ChecklistItem {
|
|
7
|
+
id: string;
|
|
8
|
+
section: string;
|
|
9
|
+
text: string;
|
|
10
|
+
kind: "auto" | "manual";
|
|
11
|
+
status: "pass" | "fail" | "pending" | "confirmed" | "declined";
|
|
12
|
+
/** Supporting evidence, e.g. offending files for a failed auto item. */
|
|
13
|
+
details?: string[];
|
|
14
|
+
}
|
|
15
|
+
export declare function printChecklist(title: string, items: ChecklistItem[]): void;
|
|
16
|
+
export declare function checklistToJson(items: ChecklistItem[]): {
|
|
17
|
+
items: {
|
|
18
|
+
id: string;
|
|
19
|
+
section: string;
|
|
20
|
+
text: string;
|
|
21
|
+
kind: "auto" | "manual";
|
|
22
|
+
status: "fail" | "pass" | "pending" | "confirmed" | "declined";
|
|
23
|
+
details: string[];
|
|
24
|
+
}[];
|
|
25
|
+
summary: {
|
|
26
|
+
total: number;
|
|
27
|
+
auto_passed: number;
|
|
28
|
+
auto_failed: number;
|
|
29
|
+
pending: number;
|
|
30
|
+
};
|
|
31
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import pc from "picocolors";
|
|
2
|
+
const MAX_DETAILS = 10;
|
|
3
|
+
export function printChecklist(title, items) {
|
|
4
|
+
console.log(pc.bold(title));
|
|
5
|
+
const sections = [...new Set(items.map((i) => i.section))];
|
|
6
|
+
for (const section of sections) {
|
|
7
|
+
console.log();
|
|
8
|
+
console.log(pc.bold(section));
|
|
9
|
+
for (const item of items.filter((i) => i.section === section)) {
|
|
10
|
+
const mark = item.status === "pass" || item.status === "confirmed"
|
|
11
|
+
? pc.green("✓")
|
|
12
|
+
: item.status === "fail" || item.status === "declined"
|
|
13
|
+
? pc.red("✗")
|
|
14
|
+
: pc.yellow("☐");
|
|
15
|
+
console.log(` ${mark} ${item.text}`);
|
|
16
|
+
const details = item.details ?? [];
|
|
17
|
+
for (const d of details.slice(0, MAX_DETAILS))
|
|
18
|
+
console.log(` - ${d}`);
|
|
19
|
+
if (details.length > MAX_DETAILS) {
|
|
20
|
+
console.log(` … and ${details.length - MAX_DETAILS} more`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
console.log();
|
|
25
|
+
}
|
|
26
|
+
export function checklistToJson(items) {
|
|
27
|
+
return {
|
|
28
|
+
items: items.map(({ id, section, text, kind, status, details }) => ({
|
|
29
|
+
id,
|
|
30
|
+
section,
|
|
31
|
+
text,
|
|
32
|
+
kind,
|
|
33
|
+
status,
|
|
34
|
+
details: details ?? [],
|
|
35
|
+
})),
|
|
36
|
+
summary: {
|
|
37
|
+
total: items.length,
|
|
38
|
+
auto_passed: items.filter((i) => i.status === "pass").length,
|
|
39
|
+
auto_failed: items.filter((i) => i.status === "fail").length,
|
|
40
|
+
pending: items.filter((i) => i.status === "pending").length,
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { MetaConfig } from "../schemas/config.schema.js";
|
|
2
|
+
/**
|
|
3
|
+
* Named documentation scopes for repository maintenance. The human confirms
|
|
4
|
+
* which scopes to tidy in a given review round (`meta review --pick-docs`).
|
|
5
|
+
*/
|
|
6
|
+
export interface DocScope {
|
|
7
|
+
key: string;
|
|
8
|
+
title: string;
|
|
9
|
+
/** Paths relative to the repo root (resolved from config where possible). */
|
|
10
|
+
paths: string[];
|
|
11
|
+
}
|
|
12
|
+
export interface ResolvedDocScope extends DocScope {
|
|
13
|
+
exists: boolean;
|
|
14
|
+
/** Markdown files currently in the scope. */
|
|
15
|
+
files: string[];
|
|
16
|
+
}
|
|
17
|
+
export declare function docScopes(config: MetaConfig): DocScope[];
|
|
18
|
+
export declare function resolveDocScopes(root: string, config: MetaConfig): ResolvedDocScope[];
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { listDomainDirs } from "../validators/domain-validator.js";
|
|
4
|
+
function listMarkdownFiles(abs) {
|
|
5
|
+
if (!fs.existsSync(abs))
|
|
6
|
+
return [];
|
|
7
|
+
if (fs.statSync(abs).isFile())
|
|
8
|
+
return abs.endsWith(".md") ? [abs] : [];
|
|
9
|
+
const out = [];
|
|
10
|
+
for (const entry of fs.readdirSync(abs, { withFileTypes: true })) {
|
|
11
|
+
const child = path.join(abs, entry.name);
|
|
12
|
+
if (entry.isDirectory())
|
|
13
|
+
out.push(...listMarkdownFiles(child));
|
|
14
|
+
else if (entry.name.endsWith(".md"))
|
|
15
|
+
out.push(child);
|
|
16
|
+
}
|
|
17
|
+
return out;
|
|
18
|
+
}
|
|
19
|
+
export function docScopes(config) {
|
|
20
|
+
const docs = config.paths.docs;
|
|
21
|
+
return [
|
|
22
|
+
{
|
|
23
|
+
key: "product",
|
|
24
|
+
title: "产品文档(PRD / 需求)",
|
|
25
|
+
paths: [path.join(docs, "product"), path.join(docs, "prd.md")],
|
|
26
|
+
},
|
|
27
|
+
{ key: "architecture", title: "技术架构文档", paths: [path.join(docs, "architecture")] },
|
|
28
|
+
{ key: "api", title: "接口文档(API)", paths: [path.join(docs, "api")] },
|
|
29
|
+
{ key: "domain", title: "领域文档(业务规则)", paths: [config.paths.domain_docs] },
|
|
30
|
+
{ key: "adr", title: "架构决策记录(ADR)", paths: [config.paths.adr_docs] },
|
|
31
|
+
{
|
|
32
|
+
key: "engineering",
|
|
33
|
+
title: "工程规范(测试 / 硬限制)",
|
|
34
|
+
paths: [path.join(docs, "engineering")],
|
|
35
|
+
},
|
|
36
|
+
{ key: "runbooks", title: "运维手册(Runbooks)", paths: [path.join(docs, "runbooks")] },
|
|
37
|
+
{ key: "agents", title: "Agent 规则(AGENTS.md)", paths: [config.paths.root_agents] },
|
|
38
|
+
{ key: "openspec", title: "OpenSpec specs", paths: [path.join(config.paths.openspec, "specs")] },
|
|
39
|
+
];
|
|
40
|
+
}
|
|
41
|
+
export function resolveDocScopes(root, config) {
|
|
42
|
+
return docScopes(config).map((scope) => {
|
|
43
|
+
const files = scope.paths.flatMap((rel) => listMarkdownFiles(path.join(root, rel)).map((abs) => path.relative(root, abs)));
|
|
44
|
+
// Local AGENTS.md files belong to the agents scope.
|
|
45
|
+
if (scope.key === "agents") {
|
|
46
|
+
for (const domain of listDomainDirs(root, config.paths.domain_docs)) {
|
|
47
|
+
const rel = path.join(config.paths.domain_docs, domain, "AGENTS.md");
|
|
48
|
+
if (fs.existsSync(path.join(root, rel)))
|
|
49
|
+
files.push(rel);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const exists = scope.paths.some((rel) => fs.existsSync(path.join(root, rel)));
|
|
53
|
+
return { ...scope, exists, files: files.sort() };
|
|
54
|
+
});
|
|
55
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/** Heading names (contains-match, case-insensitive) that declare hard constraints. */
|
|
2
|
+
export declare const HARD_CONSTRAINT_HEADINGS: string[];
|
|
1
3
|
export interface ParsedDoc {
|
|
2
4
|
file: string;
|
|
3
5
|
data: Record<string, unknown>;
|
|
@@ -10,6 +12,11 @@ export declare function parseDocFile(absPath: string): ParsedDoc;
|
|
|
10
12
|
export declare function extractHeadings(content: string): string[];
|
|
11
13
|
/** True if any heading contains one of the given texts (case-insensitive). */
|
|
12
14
|
export declare function hasSection(content: string, names: string[]): boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Body text of the first section whose heading matches one of the given
|
|
17
|
+
* names (HTML comments stripped, trimmed). Null when no heading matches.
|
|
18
|
+
*/
|
|
19
|
+
export declare function extractSection(content: string, names: string[]): string | null;
|
|
13
20
|
/**
|
|
14
21
|
* True if there is any non-empty, non-comment body text under a matching
|
|
15
22
|
* heading. Sub-headings (deeper levels) belong to the section, so their body
|
package/dist/core/frontmatter.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import matter from "gray-matter";
|
|
3
|
+
/** Heading names (contains-match, case-insensitive) that declare hard constraints. */
|
|
4
|
+
export const HARD_CONSTRAINT_HEADINGS = ["硬限制", "hard constraint"];
|
|
3
5
|
export function parseDocFile(absPath) {
|
|
4
6
|
const raw = fs.readFileSync(absPath, "utf8");
|
|
5
7
|
const hasFrontmatter = /^---\r?\n/.test(raw);
|
|
@@ -45,6 +47,39 @@ export function hasSection(content, names) {
|
|
|
45
47
|
const headings = extractHeadings(content).map((h) => h.toLowerCase());
|
|
46
48
|
return names.some((n) => headings.some((h) => h.includes(n.toLowerCase())));
|
|
47
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* Body text of the first section whose heading matches one of the given
|
|
52
|
+
* names (HTML comments stripped, trimmed). Null when no heading matches.
|
|
53
|
+
*/
|
|
54
|
+
export function extractSection(content, names) {
|
|
55
|
+
const lines = content.split(/\r?\n/);
|
|
56
|
+
let capturing = false;
|
|
57
|
+
let sectionLevel = 0;
|
|
58
|
+
let inFence = false;
|
|
59
|
+
let found = false;
|
|
60
|
+
const out = [];
|
|
61
|
+
for (const line of lines) {
|
|
62
|
+
if (/^\s*(```|~~~)/.test(line))
|
|
63
|
+
inFence = !inFence;
|
|
64
|
+
const m = !inFence ? /^(#{1,6})\s+(.+?)\s*$/.exec(line) : null;
|
|
65
|
+
if (m) {
|
|
66
|
+
const level = m[1].length;
|
|
67
|
+
if (!found && names.some((n) => m[2].toLowerCase().includes(n.toLowerCase()))) {
|
|
68
|
+
capturing = true;
|
|
69
|
+
found = true;
|
|
70
|
+
sectionLevel = level;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (capturing && level <= sectionLevel)
|
|
74
|
+
capturing = false;
|
|
75
|
+
}
|
|
76
|
+
if (capturing)
|
|
77
|
+
out.push(line);
|
|
78
|
+
}
|
|
79
|
+
if (!found)
|
|
80
|
+
return null;
|
|
81
|
+
return out.join("\n").replace(/<!--[\s\S]*?-->/g, "").trim();
|
|
82
|
+
}
|
|
48
83
|
/**
|
|
49
84
|
* True if there is any non-empty, non-comment body text under a matching
|
|
50
85
|
* heading. Sub-headings (deeper levels) belong to the section, so their body
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An MCP pack bundles a remote MCP server endpoint with the project-level
|
|
3
|
+
* skills that teach agents how to use it. Installing a pack writes the
|
|
4
|
+
* server into `.mcp.json` and copies the skills into `.claude/skills/`,
|
|
5
|
+
* keeping everything repository-scoped (no user-level/global tooling).
|
|
6
|
+
*/
|
|
7
|
+
export interface McpPack {
|
|
8
|
+
name: string;
|
|
9
|
+
title: string;
|
|
10
|
+
/** Entry written verbatim into `.mcp.json` under mcpServers[name]. */
|
|
11
|
+
server?: Record<string, unknown>;
|
|
12
|
+
/** Whether the pack bundles skills under assets/skills/<name>/. */
|
|
13
|
+
hasSkills: boolean;
|
|
14
|
+
/** Merged into `.claude/settings.json` (existing values always win). */
|
|
15
|
+
settings?: Record<string, unknown>;
|
|
16
|
+
/** Rules appended to the root AGENTS.md Working Rules when installed via init. */
|
|
17
|
+
agentRules?: string[];
|
|
18
|
+
}
|
|
19
|
+
export declare const MCP_PACKS: Record<string, McpPack>;
|
|
20
|
+
/** Human-readable one-liner of what the pack installs. */
|
|
21
|
+
export declare function packServerSummary(pack: McpPack): string;
|
|
22
|
+
export declare function getMcpPack(name: string): McpPack;
|
|
23
|
+
/** Bundled skills live under <package root>/assets/skills/<pack>/. */
|
|
24
|
+
export declare function packSkillsDir(pack: McpPack): string;
|
|
25
|
+
/** Skill files of a pack as [relative path under .claude/skills/, absolute source path]. */
|
|
26
|
+
export declare function listPackSkillFiles(pack: McpPack): [string, string][];
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { EXIT_CODES, MetaError } from "./errors.js";
|
|
5
|
+
export const MCP_PACKS = {
|
|
6
|
+
ironbank: {
|
|
7
|
+
name: "ironbank",
|
|
8
|
+
title: "IronBank MCP (hillinsight)",
|
|
9
|
+
server: { type: "http", url: "https://test-ib-mcp.hillinsight.tech/mcp" },
|
|
10
|
+
hasSkills: true,
|
|
11
|
+
},
|
|
12
|
+
playwright: {
|
|
13
|
+
name: "playwright",
|
|
14
|
+
title: "Playwright MCP (browser automation, microsoft/playwright-mcp)",
|
|
15
|
+
server: { command: "npx", args: ["@playwright/mcp@latest"] },
|
|
16
|
+
hasSkills: false,
|
|
17
|
+
},
|
|
18
|
+
superpowers: {
|
|
19
|
+
name: "superpowers",
|
|
20
|
+
title: "Superpowers (obra/superpowers, Claude Code plugin: planning/TDD/debugging skills)",
|
|
21
|
+
hasSkills: false,
|
|
22
|
+
settings: {
|
|
23
|
+
extraKnownMarketplaces: {
|
|
24
|
+
"superpowers-marketplace": {
|
|
25
|
+
source: { source: "github", repo: "obra/superpowers-marketplace" },
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
enabledPlugins: {
|
|
29
|
+
"superpowers@superpowers-marketplace": true,
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
agentRules: [
|
|
33
|
+
"Do not use the Superpowers brainstorming skill during development unless the human explicitly asks for it.",
|
|
34
|
+
],
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
/** Human-readable one-liner of what the pack installs. */
|
|
38
|
+
export function packServerSummary(pack) {
|
|
39
|
+
if (pack.server) {
|
|
40
|
+
const { url, command, args } = pack.server;
|
|
41
|
+
if (url)
|
|
42
|
+
return url;
|
|
43
|
+
return [command, ...(args ?? [])].filter(Boolean).join(" ");
|
|
44
|
+
}
|
|
45
|
+
if (pack.settings)
|
|
46
|
+
return "Claude Code plugin (.claude/settings.json)";
|
|
47
|
+
return "skills only";
|
|
48
|
+
}
|
|
49
|
+
export function getMcpPack(name) {
|
|
50
|
+
const pack = MCP_PACKS[name];
|
|
51
|
+
if (!pack) {
|
|
52
|
+
throw new MetaError(`Unknown MCP pack '${name}'. Available packs: ${Object.keys(MCP_PACKS).join(", ")}`, EXIT_CODES.BAD_ARGS);
|
|
53
|
+
}
|
|
54
|
+
return pack;
|
|
55
|
+
}
|
|
56
|
+
/** Bundled skills live under <package root>/assets/skills/<pack>/. */
|
|
57
|
+
export function packSkillsDir(pack) {
|
|
58
|
+
return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../assets/skills", pack.name);
|
|
59
|
+
}
|
|
60
|
+
/** Skill files of a pack as [relative path under .claude/skills/, absolute source path]. */
|
|
61
|
+
export function listPackSkillFiles(pack) {
|
|
62
|
+
if (!pack.hasSkills)
|
|
63
|
+
return [];
|
|
64
|
+
const dir = packSkillsDir(pack);
|
|
65
|
+
if (!fs.existsSync(dir)) {
|
|
66
|
+
throw new MetaError(`Bundled skills for MCP pack '${pack.name}' not found at ${dir}. The CLI installation may be corrupted.`, EXIT_CODES.TEMPLATE_ERROR);
|
|
67
|
+
}
|
|
68
|
+
const out = [];
|
|
69
|
+
const walk = (abs, rel) => {
|
|
70
|
+
for (const entry of fs.readdirSync(abs, { withFileTypes: true })) {
|
|
71
|
+
if (entry.name === ".DS_Store")
|
|
72
|
+
continue;
|
|
73
|
+
const childAbs = path.join(abs, entry.name);
|
|
74
|
+
const childRel = rel ? path.join(rel, entry.name) : entry.name;
|
|
75
|
+
if (entry.isDirectory())
|
|
76
|
+
walk(childAbs, childRel);
|
|
77
|
+
else
|
|
78
|
+
out.push([childRel, childAbs]);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
walk(dir, "");
|
|
82
|
+
return out.sort((a, b) => a[0].localeCompare(b[0]));
|
|
83
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** The `meta check` invocation appropriate for the project's package manager. */
|
|
2
|
+
export declare function metaCheckCommand(root: string): string;
|
|
3
|
+
export interface LefthookResult {
|
|
4
|
+
action: "created" | "updated" | "unchanged";
|
|
5
|
+
/** Path relative to root. */
|
|
6
|
+
path: string;
|
|
7
|
+
command: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Create lefthook.yml with a pre-commit meta-check command, or add the
|
|
11
|
+
* command to an existing file. An existing meta-check entry is never
|
|
12
|
+
* overwritten.
|
|
13
|
+
*/
|
|
14
|
+
export declare function generateLefthookConfig(root: string, dryRun: boolean): LefthookResult;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import YAML from "yaml";
|
|
4
|
+
import { EXIT_CODES, MetaError } from "../core/errors.js";
|
|
5
|
+
import { detectPackageManager } from "../commands/init.js";
|
|
6
|
+
/** The `meta check` invocation appropriate for the project's package manager. */
|
|
7
|
+
export function metaCheckCommand(root) {
|
|
8
|
+
switch (detectPackageManager(root)) {
|
|
9
|
+
case "pnpm":
|
|
10
|
+
return "pnpm exec meta check";
|
|
11
|
+
case "yarn":
|
|
12
|
+
return "yarn exec meta check";
|
|
13
|
+
case "npm":
|
|
14
|
+
return "npx meta check";
|
|
15
|
+
default:
|
|
16
|
+
return "meta check";
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
const LEFTHOOK_FILENAMES = ["lefthook.yml", "lefthook.yaml"];
|
|
20
|
+
/**
|
|
21
|
+
* Create lefthook.yml with a pre-commit meta-check command, or add the
|
|
22
|
+
* command to an existing file. An existing meta-check entry is never
|
|
23
|
+
* overwritten.
|
|
24
|
+
*/
|
|
25
|
+
export function generateLefthookConfig(root, dryRun) {
|
|
26
|
+
const existing = LEFTHOOK_FILENAMES.find((f) => fs.existsSync(path.join(root, f)));
|
|
27
|
+
const rel = existing ?? "lefthook.yml";
|
|
28
|
+
const abs = path.join(root, rel);
|
|
29
|
+
const run = metaCheckCommand(root);
|
|
30
|
+
let doc = {};
|
|
31
|
+
if (existing) {
|
|
32
|
+
try {
|
|
33
|
+
doc = YAML.parse(fs.readFileSync(abs, "utf8")) ?? {};
|
|
34
|
+
}
|
|
35
|
+
catch (e) {
|
|
36
|
+
throw new MetaError(`Cannot parse ${rel}: ${e.message}`, EXIT_CODES.CONFIG_UNPARSABLE);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (typeof doc["pre-commit"] !== "object" || doc["pre-commit"] === null) {
|
|
40
|
+
doc["pre-commit"] = {};
|
|
41
|
+
}
|
|
42
|
+
const preCommit = doc["pre-commit"];
|
|
43
|
+
if (typeof preCommit["commands"] !== "object" || preCommit["commands"] === null) {
|
|
44
|
+
preCommit["commands"] = {};
|
|
45
|
+
}
|
|
46
|
+
const commands = preCommit["commands"];
|
|
47
|
+
const current = commands["meta-check"];
|
|
48
|
+
if (current) {
|
|
49
|
+
return { action: "unchanged", path: rel, command: current.run ?? "" };
|
|
50
|
+
}
|
|
51
|
+
commands["meta-check"] = { run };
|
|
52
|
+
if (!dryRun)
|
|
53
|
+
fs.writeFileSync(abs, YAML.stringify(doc), "utf8");
|
|
54
|
+
return { action: existing ? "updated" : "created", path: rel, command: run };
|
|
55
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { FileWriter } from "../core/filesystem.js";
|
|
2
|
+
import { type McpPack } from "../core/mcp-packs.js";
|
|
3
|
+
/**
|
|
4
|
+
* Install a tool pack into the target repository:
|
|
5
|
+
* - merge the MCP server into `.mcp.json` (existing servers are never touched)
|
|
6
|
+
* - merge plugin settings into `.claude/settings.json` (existing values always win)
|
|
7
|
+
* - copy the pack's skills into `.claude/skills/` (existing files are skipped)
|
|
8
|
+
*/
|
|
9
|
+
export declare function installMcpPack(writer: FileWriter, root: string, pack: McpPack): void;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { EXIT_CODES, MetaError } from "../core/errors.js";
|
|
4
|
+
import { listPackSkillFiles } from "../core/mcp-packs.js";
|
|
5
|
+
const MCP_JSON = ".mcp.json";
|
|
6
|
+
const CLAUDE_SETTINGS = ".claude/settings.json";
|
|
7
|
+
function isPlainObject(v) {
|
|
8
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
9
|
+
}
|
|
10
|
+
/** Deep-merge source into target, adding missing keys only — existing values always win. */
|
|
11
|
+
function mergeMissing(target, source) {
|
|
12
|
+
let changed = false;
|
|
13
|
+
for (const [key, value] of Object.entries(source)) {
|
|
14
|
+
if (!(key in target)) {
|
|
15
|
+
target[key] = value;
|
|
16
|
+
changed = true;
|
|
17
|
+
}
|
|
18
|
+
else if (isPlainObject(target[key]) && isPlainObject(value)) {
|
|
19
|
+
changed = mergeMissing(target[key], value) || changed;
|
|
20
|
+
}
|
|
21
|
+
// Existing non-object values are kept as-is.
|
|
22
|
+
}
|
|
23
|
+
return changed;
|
|
24
|
+
}
|
|
25
|
+
function readJson(root, rel) {
|
|
26
|
+
const abs = path.join(root, rel);
|
|
27
|
+
if (!fs.existsSync(abs))
|
|
28
|
+
return {};
|
|
29
|
+
try {
|
|
30
|
+
return JSON.parse(fs.readFileSync(abs, "utf8")) ?? {};
|
|
31
|
+
}
|
|
32
|
+
catch (e) {
|
|
33
|
+
throw new MetaError(`Cannot parse ${rel}: ${e.message}`, EXIT_CODES.CONFIG_UNPARSABLE);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Install a tool pack into the target repository:
|
|
38
|
+
* - merge the MCP server into `.mcp.json` (existing servers are never touched)
|
|
39
|
+
* - merge plugin settings into `.claude/settings.json` (existing values always win)
|
|
40
|
+
* - copy the pack's skills into `.claude/skills/` (existing files are skipped)
|
|
41
|
+
*/
|
|
42
|
+
export function installMcpPack(writer, root, pack) {
|
|
43
|
+
if (pack.server) {
|
|
44
|
+
const doc = readJson(root, MCP_JSON);
|
|
45
|
+
if (!isPlainObject(doc.mcpServers))
|
|
46
|
+
doc.mcpServers = {};
|
|
47
|
+
const servers = doc.mcpServers;
|
|
48
|
+
if (servers[pack.name]) {
|
|
49
|
+
writer.actions.push({ type: "skip", path: MCP_JSON });
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
servers[pack.name] = pack.server;
|
|
53
|
+
writer.write(MCP_JSON, JSON.stringify(doc, null, 2) + "\n", { overwrite: true });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (pack.settings) {
|
|
57
|
+
const doc = readJson(root, CLAUDE_SETTINGS);
|
|
58
|
+
if (mergeMissing(doc, pack.settings)) {
|
|
59
|
+
writer.write(CLAUDE_SETTINGS, JSON.stringify(doc, null, 2) + "\n", { overwrite: true });
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
writer.actions.push({ type: "skip", path: CLAUDE_SETTINGS });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
for (const [rel, srcAbs] of listPackSkillFiles(pack)) {
|
|
66
|
+
writer.write(path.join(".claude", "skills", rel), fs.readFileSync(srcAbs, "utf8"));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
* paths.templates). Only {{variable}} substitution is supported.
|
|
5
5
|
*/
|
|
6
6
|
export declare const BUILTIN_TEMPLATES: {
|
|
7
|
-
readonly "agents.md": "# AGENTS.md\n\n## Project Overview\n\n- Project type: {{project_type}}\n- Package manager: {{package_manager}}\n- Main application paths: {{main_paths}}\n\n## Important Documentation\n\n- Architecture: `docs/architecture/`\n- Domain rules: `docs/domain/`\n- API conventions: `docs/api/`\n- Engineering rules: `docs/engineering/`\n- Architecture decisions: `docs/adr/`\n- Product capability specs: `openspec/specs/`\n- Active changes: `openspec/changes/`\n\n## Working Rules\n\n- Read the nearest applicable `AGENTS.md` before modifying files.\n- Read related domain documentation before changing business behavior.\n- Do not duplicate domain rules in this file.\n- Update OpenSpec specs when externally observable behavior changes.\n- Add or update tests for behavior changes.\n- Do not mix unrelated refactors into a feature change.\n\n## High-Risk Areas\n\nBefore changing payment, order, commission, settlement, withdrawal, permissions, webhooks, inventory, or audit logging, read the applicable documentation under `docs/domain
|
|
7
|
+
readonly "agents.md": "# AGENTS.md\n\n## Project Overview\n\n- Project type: {{project_type}}\n- Package manager: {{package_manager}}\n- Main application paths: {{main_paths}}\n\n## Important Documentation\n\n- Architecture: `docs/architecture/`\n- Domain rules: `docs/domain/`\n- API conventions: `docs/api/`\n- Engineering rules: `docs/engineering/`\n- Architecture decisions: `docs/adr/`\n- Product capability specs: `openspec/specs/`\n- Active changes: `openspec/changes/`\n\n## Working Rules\n\n- Read the nearest applicable `AGENTS.md` before modifying files.\n- Read related domain documentation before changing business behavior.\n- Do not modify third-party or vendored code (`node_modules/`, `vendor/`, `third_party/`, `patches/`). Wrap or extend it instead; if patching is unavoidable, record an ADR first.\n- Do not use user-level (global) skills, plugins, or MCP tools unless the human explicitly asks for them. Prefer tools, scripts, and skills defined inside this repository.\n- Never violate declared hard constraints (`docs/domain/*/rules.md` 硬限制 sections and `docs/engineering/constraints.md`). If a limit is not declared, ask the human instead of assuming it is negotiable.\n- If you are guessing, fabricating, or cannot verify a conclusion, stop and ask the human.\n- Do not duplicate domain rules in this file.\n- Update OpenSpec specs when externally observable behavior changes.\n- Add or update tests for behavior changes.\n- Do not mix unrelated refactors into a feature change.\n{{extra_rules}}\n## High-Risk Areas\n\nBefore changing payment, order, commission, settlement, withdrawal, permissions, webhooks, inventory, or audit logging, read the applicable documentation under `docs/domain/`. Hard constraints for these areas are declared in each domain's `rules.md` under 硬限制(不可违反).\n\n## Definition of Done\n\n- Relevant tests pass.\n- Type checking and lint pass where configured.\n- A human has reviewed the change.\n- All declared hard constraints hold.\n- Behavior changes include documentation or specification updates where applicable.\n- High-risk changes include rollback and failure-path consideration.\n";
|
|
8
8
|
readonly "domain-readme.md": "---\ntitle: {{domain_title}}\nstatus: active\nowner: {{owner}}\nlast_reviewed: {{date}}\nsource_of_truth: true\napplies_to: []\n---\n\n# {{domain_title}}\n\n## 职责\n\n<!-- 描述该领域负责的业务能力。 -->\n\n## 非职责\n\n<!-- 明确该领域不负责什么,避免模块边界扩散。 -->\n\n## 核心对象\n\n| 对象 | 说明 |\n|---|---|\n| | |\n\n## 上游依赖\n\n<!-- 哪些模块、事件、服务会驱动该领域。 -->\n\n## 下游影响\n\n<!-- 该领域变化会影响哪些模块、数据、接口或用户角色。 -->\n\n## 关键规则入口\n\n- [业务规则](./rules.md)\n{{state_machine_link}}{{glossary_link}}\n\n## 关联 ADR\n\n<!-- 例如:../adr/0004-withdrawal-idempotency.md -->\n";
|
|
9
|
-
readonly "domain-rules.md": "---\ntitle: {{domain_title}}业务规则\nstatus: active\nowner: {{owner}}\nlast_reviewed: {{date}}\nsource_of_truth: true\napplies_to: []\n---\n\n# {{domain_title}}业务规则\n\n## 术语\n\n<!-- 仅列出该领域特有术语;通用术语应放到 docs/domain/glossary.md。 -->\n\n##
|
|
9
|
+
readonly "domain-rules.md": "---\ntitle: {{domain_title}}业务规则\nstatus: active\nowner: {{owner}}\nlast_reviewed: {{date}}\nsource_of_truth: true\napplies_to: []\n---\n\n# {{domain_title}}业务规则\n\n## 术语\n\n<!-- 仅列出该领域特有术语;通用术语应放到 docs/domain/glossary.md。 -->\n\n## 硬限制(不可违反)\n\n<!-- 安全、数据一致性、API 契约、性能预算、兼容性、合规。 -->\n<!-- 没写在这里的限制,AI 会当成可协商的。 -->\n\n-\n-\n-\n\n## 软限制(可权衡)\n\n<!-- 代码风格偏好、非关键路径的实现细节、可后续优化的部分。 -->\n\n-\n\n## 权限规则\n\n| 角色 | 可执行操作 | 限制 |\n|---|---|---|\n| | | |\n\n## 边界条件\n\n<!-- 写清重复请求、失败重试、并发、撤销、退款、异常状态等行为。 -->\n\n## 审计与追踪\n\n<!-- 是否需要审计日志、操作人、时间、状态变更原因等。 -->\n";
|
|
10
10
|
readonly "domain-state-machine.md": "---\ntitle: {{domain_title}}状态机\nstatus: active\nowner: {{owner}}\nlast_reviewed: {{date}}\nsource_of_truth: true\napplies_to: []\n---\n\n# {{domain_title}}状态机\n\n## 状态列表\n\n| 状态 | 含义 | 是否终态 |\n|---|---|---|\n| | | 否 |\n\n## 状态流转\n\n```mermaid\nstateDiagram-v2\n [*] --> pending\n pending --> completed\n pending --> cancelled\n```\n\n## 流转规则\n\n| 当前状态 | 触发事件 | 下一个状态 | 前置条件 | 副作用 |\n| ---- | ---- | ----- | ---- | --- |\n| | | | | |\n\n## 幂等规则\n\n<!-- 对重复请求、重复事件、重复回调的处理规则。 -->\n";
|
|
11
11
|
readonly "domain-glossary.md": "---\ntitle: {{domain_title}}术语表\nstatus: active\nowner: {{owner}}\nlast_reviewed: {{date}}\nsource_of_truth: true\napplies_to: []\n---\n\n# {{domain_title}}术语表\n\n| 术语 | 定义 | 备注 |\n|---|---|---|\n| | | |\n";
|
|
12
12
|
readonly "domain-agents.md": "# {{domain_title}} Module Rules\n\n## Scope\n\nThis directory contains the {{domain_title}} domain.\n\n## Required Reading\n\n- `README.md`\n- `rules.md`\n{{state_machine_reading}}\n\n## Rules for Agents\n\n- Do not change domain behavior without updating the relevant domain documentation.\n- Do not introduce new states without updating `state-machine.md`.\n- Preserve existing audit and idempotency behavior.\n- Keep business rules out of controllers and UI components.\n- Add tests for normal, boundary, and retry scenarios.\n\n## Review Checklist\n\n- Are permissions affected?\n- Are money, inventory, or externally triggered callbacks affected?\n- Is retry or duplicate handling required?\n- Does this change require an ADR?\n";
|
|
@@ -18,6 +18,11 @@ export declare const BUILTIN_TEMPLATES: {
|
|
|
18
18
|
readonly "system-overview.md": "---\ntitle: 系统架构总览\nstatus: draft\nowner: {{owner}}\nlast_reviewed: {{date}}\nsource_of_truth: true\napplies_to: []\n---\n\n# 系统架构总览\n\n## 系统边界\n\n<!-- 系统包含哪些应用 / 服务。 -->\n\n## 模块划分\n\n<!-- 主要模块与职责。 -->\n\n## 关键依赖\n\n<!-- 数据库、消息队列、第三方服务。 -->\n";
|
|
19
19
|
readonly "api-conventions.md": "---\ntitle: API 约定\nstatus: draft\nowner: {{owner}}\nlast_reviewed: {{date}}\nsource_of_truth: true\napplies_to: []\n---\n\n# API 约定\n\n## 命名与版本\n\n## 错误码规范\n\n## 鉴权约定\n";
|
|
20
20
|
readonly "engineering-testing.md": "---\ntitle: 测试规范\nstatus: draft\nowner: {{owner}}\nlast_reviewed: {{date}}\nsource_of_truth: true\napplies_to: []\n---\n\n# 测试规范\n\n## 必须覆盖的场景\n\n- 正常路径\n- 边界条件\n- 重试与幂等\n\n## 工具与命令\n";
|
|
21
|
+
readonly "engineering-constraints.md": "---\ntitle: 项目级硬限制\nstatus: draft\nowner: {{owner}}\nlast_reviewed: {{date}}\nsource_of_truth: true\napplies_to: []\n---\n\n# 项目级硬限制\n\n<!-- 跨领域的硬限制放这里;单个领域的硬限制放 docs/domain/<domain>/rules.md。 -->\n\n## 硬限制(不可违反)\n\n<!-- 类别参考:安全、数据一致性、API 契约、性能预算、兼容性、合规。 -->\n<!-- 没写在这里的限制,AI 会当成可协商的。 -->\n\n-\n-\n\n## 软限制(可权衡)\n\n<!-- 代码风格偏好、非关键路径的实现细节、可后续优化的部分。 -->\n\n-\n";
|
|
21
22
|
readonly "runbooks-readme.md": "---\ntitle: Runbooks\nstatus: draft\nowner: {{owner}}\nlast_reviewed: {{date}}\nsource_of_truth: true\napplies_to: []\n---\n\n# Runbooks\n\n<!-- 运维手册与故障处理流程。 -->\n";
|
|
23
|
+
readonly "change-proposal.md": "# {{change_title}}\n\n## Why\n\n<!-- 为什么要做这个变更?解决什么问题? -->\n\n## What Changes\n\n<!-- 变更内容概述:新增/修改/移除哪些能力。 -->\n\n## Impact\n\n- Affected domains: {{domain}}\n- Affected specs:\n- Affected code:\n";
|
|
24
|
+
readonly "change-design.md": "# {{change_title}} — Design\n\n## Context\n\n<!-- 现状与约束。 -->\n\n## Goals / Non-Goals\n\n- Goals:\n- Non-Goals:\n\n## Decisions\n\n<!-- 关键技术决策;长期决策请沉淀为 ADR(meta new-adr)。 -->\n\n## Hard Constraints(硬限制核对)\n\n<!-- 列出本变更涉及的已声明硬限制与核对结论。 -->\n<!-- 来源:docs/domain/<domain>/rules.md 硬限制章节、docs/engineering/constraints.md。 -->\n\n| 硬限制 | 来源 | 核对结论 |\n|---|---|---|\n| | | |\n\n## Risks / Trade-offs\n\n<!-- 风险与权衡。 -->\n";
|
|
25
|
+
readonly "change-tasks.md": "# {{change_title}} — Tasks\n\n## 1. Implementation\n\n- [ ] 1.1\n- [ ] 1.2\n\n## 2. Validation\n\n- [ ] 2.1 Tests updated\n- [ ] 2.2 `meta check` passes\n";
|
|
26
|
+
readonly "change-risk-review.md": "# {{change_title}} — Risk Review\n\n- Related domain: {{domain}}\n- Date: {{date}}\n\n## Checklist\n\n- [ ] Money representation(金额表示与精度)\n- [ ] Idempotency(幂等)\n- [ ] Retry behavior(重试行为)\n- [ ] Callback deduplication(回调去重)\n- [ ] Audit logging(审计日志)\n- [ ] Permission boundaries(权限边界)\n- [ ] Rollback plan(回滚方案)\n- [ ] Declared hard constraints checked(已声明的硬限制全部核对)\n\n## Notes\n\n<!-- 每项勾选前写清结论或“不适用”的原因。 -->\n";
|
|
22
27
|
};
|
|
23
28
|
export type TemplateName = keyof typeof BUILTIN_TEMPLATES;
|