@bossmissing/agent-meta 0.1.0 → 0.3.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 +61 -2
- package/assets/skills/deploy-ansible/deploy-ansible/SKILL.md +71 -0
- package/assets/skills/deploy-cloudflare/deploy-cloudflare/SKILL.md +51 -0
- package/assets/skills/deploy-github-actions/deploy-github-actions/SKILL.md +50 -0
- package/assets/skills/ironbank/ironbank-citations/SKILL.md +197 -0
- package/assets/skills/ironbank/ironbank-docs-research/SKILL.md +205 -0
- package/assets/skills/ironbank/ironbank-mcp-reference/SKILL.md +414 -0
- package/assets/skills/ironbank/ironbank-note-links/SKILL.md +87 -0
- package/dist/cli.js +74 -2
- 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 +4 -0
- package/dist/commands/init.js +57 -1
- package/dist/commands/new-adr.js +2 -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/commands/sync.js +11 -2
- package/dist/core/checklist.d.ts +31 -0
- package/dist/core/checklist.js +43 -0
- package/dist/core/deploy-packs.d.ts +11 -0
- package/dist/core/deploy-packs.js +42 -0
- package/dist/core/doc-scopes.d.ts +18 -0
- package/dist/core/doc-scopes.js +55 -0
- package/dist/core/filesystem.js +9 -1
- package/dist/core/frontmatter.d.ts +7 -0
- package/dist/core/frontmatter.js +45 -9
- package/dist/core/mcp-packs.d.ts +26 -0
- package/dist/core/mcp-packs.js +86 -0
- package/dist/generators/adr-generator.js +3 -1
- package/dist/generators/hook-generator.d.ts +14 -0
- package/dist/generators/hook-generator.js +55 -0
- package/dist/generators/index-generator.d.ts +2 -2
- package/dist/generators/index-generator.js +15 -8
- package/dist/generators/mcp-generator.d.ts +9 -0
- package/dist/generators/mcp-generator.js +68 -0
- package/dist/schemas/config.schema.d.ts +0 -10
- package/dist/schemas/config.schema.js +0 -2
- package/dist/templates/index.d.ts +7 -2
- package/dist/templates/index.js +125 -9
- package/dist/validators/adr-validator.js +4 -4
- package/dist/validators/domain-validator.js +30 -1
- package/dist/validators/openspec-validator.js +33 -4
- package/package.json +9 -15
package/dist/commands/config.js
CHANGED
|
@@ -5,6 +5,8 @@ import { loadConfig, CONFIG_FILENAME } from "../core/config.js";
|
|
|
5
5
|
import { configSchema } from "../schemas/config.schema.js";
|
|
6
6
|
import { logger } from "../core/logger.js";
|
|
7
7
|
import { EXIT_CODES, MetaError } from "../core/errors.js";
|
|
8
|
+
import { generateLefthookConfig } from "../generators/hook-generator.js";
|
|
9
|
+
import { detectPackageManager } from "./init.js";
|
|
8
10
|
function flatten(obj, prefix = "") {
|
|
9
11
|
if (obj === null || typeof obj !== "object" || Array.isArray(obj)) {
|
|
10
12
|
return [[prefix, obj]];
|
|
@@ -57,3 +59,51 @@ export function runConfigSet(root, key, value) {
|
|
|
57
59
|
fs.writeFileSync(configPath, YAML.stringify(doc), "utf8");
|
|
58
60
|
logger.ok(`Set ${key} = ${value}`);
|
|
59
61
|
}
|
|
62
|
+
/** `meta config hooks lefthook` — generate or extend the lefthook config. */
|
|
63
|
+
export function runConfigHooks(root, tool, opts = {}) {
|
|
64
|
+
if (tool !== "lefthook") {
|
|
65
|
+
throw new MetaError(`Unsupported hook tool '${tool}'. Only 'lefthook' is supported in this version.`, EXIT_CODES.BAD_ARGS);
|
|
66
|
+
}
|
|
67
|
+
const dryRun = Boolean(opts.dryRun);
|
|
68
|
+
const result = generateLefthookConfig(root, dryRun);
|
|
69
|
+
if (result.action === "unchanged") {
|
|
70
|
+
logger.info(`- ${result.path} already has a pre-commit meta-check command (${result.command})`);
|
|
71
|
+
}
|
|
72
|
+
else if (dryRun) {
|
|
73
|
+
logger.info(`Would ${result.action === "created" ? "create" : "update"}: ${result.path}`);
|
|
74
|
+
logger.info(` pre-commit meta-check: ${result.command}`);
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
logger.ok(`${result.action === "created" ? "Created" : "Updated"} ${result.path}`);
|
|
78
|
+
logger.info(` pre-commit meta-check: ${result.command}`);
|
|
79
|
+
}
|
|
80
|
+
// Record the choice in agent-meta.config.yaml when it exists.
|
|
81
|
+
const configPath = path.join(root, CONFIG_FILENAME);
|
|
82
|
+
if (!dryRun && fs.existsSync(configPath)) {
|
|
83
|
+
const doc = YAML.parse(fs.readFileSync(configPath, "utf8")) ?? {};
|
|
84
|
+
if (typeof doc.features !== "object" || doc.features === null)
|
|
85
|
+
doc.features = {};
|
|
86
|
+
if (doc.features.hooks !== true) {
|
|
87
|
+
doc.features.hooks = true;
|
|
88
|
+
fs.writeFileSync(configPath, YAML.stringify(doc), "utf8");
|
|
89
|
+
logger.ok("Set features.hooks = true");
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (result.action !== "unchanged") {
|
|
93
|
+
const pm = detectPackageManager(root);
|
|
94
|
+
logger.blank();
|
|
95
|
+
logger.info("Next steps:");
|
|
96
|
+
if (pm === "pnpm") {
|
|
97
|
+
logger.info(" pnpm add -D lefthook && pnpm exec lefthook install");
|
|
98
|
+
}
|
|
99
|
+
else if (pm === "yarn") {
|
|
100
|
+
logger.info(" yarn add -D lefthook && yarn exec lefthook install");
|
|
101
|
+
}
|
|
102
|
+
else if (pm === "npm") {
|
|
103
|
+
logger.info(" npm i -D lefthook && npx lefthook install");
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
logger.info(" Install lefthook and run: lefthook install");
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type DoctorStatus = "ok" | "warn" | "fail";
|
|
2
|
+
export interface DoctorCheck {
|
|
3
|
+
name: string;
|
|
4
|
+
status: DoctorStatus;
|
|
5
|
+
message: string;
|
|
6
|
+
suggestion?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface DoctorOptions {
|
|
9
|
+
json?: boolean;
|
|
10
|
+
/** Skip external binary probes (used by tests). */
|
|
11
|
+
execChecks?: boolean;
|
|
12
|
+
}
|
|
13
|
+
export declare function collectDoctorChecks(root: string, opts?: DoctorOptions): DoctorCheck[];
|
|
14
|
+
export declare function runDoctor(root: string, opts?: DoctorOptions): number;
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { execSync } from "node:child_process";
|
|
4
|
+
import pc from "picocolors";
|
|
5
|
+
import { loadConfig, CONFIG_FILENAME } from "../core/config.js";
|
|
6
|
+
import { DEFAULT_CONFIG } from "../schemas/config.schema.js";
|
|
7
|
+
import { detectPackageManager, detectProjectType } from "./init.js";
|
|
8
|
+
import { MetaError } from "../core/errors.js";
|
|
9
|
+
/** Version string of an external CLI, or null when it is not available. */
|
|
10
|
+
function commandVersion(cmd) {
|
|
11
|
+
try {
|
|
12
|
+
const out = execSync(`${cmd} --version`, { stdio: ["ignore", "pipe", "ignore"] })
|
|
13
|
+
.toString()
|
|
14
|
+
.trim();
|
|
15
|
+
return out.split("\n")[0] || "installed";
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export function collectDoctorChecks(root, opts = {}) {
|
|
22
|
+
const execChecks = opts.execChecks !== false;
|
|
23
|
+
const checks = [];
|
|
24
|
+
// Runtime
|
|
25
|
+
const nodeMajor = parseInt(process.versions.node.split(".")[0], 10);
|
|
26
|
+
checks.push(nodeMajor >= 20
|
|
27
|
+
? { name: "node", status: "ok", message: `Node.js ${process.versions.node}` }
|
|
28
|
+
: {
|
|
29
|
+
name: "node",
|
|
30
|
+
status: "fail",
|
|
31
|
+
message: `Node.js ${process.versions.node} is too old`,
|
|
32
|
+
suggestion: "Agent Meta CLI requires Node.js >= 20.",
|
|
33
|
+
});
|
|
34
|
+
const pm = detectPackageManager(root);
|
|
35
|
+
checks.push({
|
|
36
|
+
name: "package-manager",
|
|
37
|
+
status: "ok",
|
|
38
|
+
message: pm === "none" ? "No package.json (bare repository)" : `Package manager: ${pm}`,
|
|
39
|
+
});
|
|
40
|
+
checks.push({
|
|
41
|
+
name: "project-type",
|
|
42
|
+
status: "ok",
|
|
43
|
+
message: `Project type: ${detectProjectType(root)}`,
|
|
44
|
+
});
|
|
45
|
+
// Config
|
|
46
|
+
let config = DEFAULT_CONFIG;
|
|
47
|
+
let configFound = false;
|
|
48
|
+
try {
|
|
49
|
+
const loaded = loadConfig(root);
|
|
50
|
+
config = loaded.config;
|
|
51
|
+
configFound = loaded.configPath !== null;
|
|
52
|
+
checks.push(configFound
|
|
53
|
+
? { name: "config", status: "ok", message: `${CONFIG_FILENAME} is valid` }
|
|
54
|
+
: {
|
|
55
|
+
name: "config",
|
|
56
|
+
status: "warn",
|
|
57
|
+
message: `${CONFIG_FILENAME} not found (using built-in defaults)`,
|
|
58
|
+
suggestion: "Run `meta init` to create it.",
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
catch (e) {
|
|
62
|
+
checks.push({
|
|
63
|
+
name: "config",
|
|
64
|
+
status: "fail",
|
|
65
|
+
message: e instanceof MetaError ? e.message : `Cannot load ${CONFIG_FILENAME}`,
|
|
66
|
+
suggestion: `Fix or regenerate ${CONFIG_FILENAME}.`,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
// Declared paths
|
|
70
|
+
const pathChecks = [
|
|
71
|
+
["paths.root_agents", config.paths.root_agents, "Run `meta init` to create it."],
|
|
72
|
+
["paths.docs", config.paths.docs, "Run `meta init` to create the docs skeleton."],
|
|
73
|
+
["paths.domain_docs", config.paths.domain_docs, "Run `meta new-domain <name>` to create the first domain."],
|
|
74
|
+
["paths.adr_docs", config.paths.adr_docs, "Run `meta new-adr <slug>` to create the first ADR."],
|
|
75
|
+
];
|
|
76
|
+
for (const [name, rel, suggestion] of pathChecks) {
|
|
77
|
+
checks.push(fs.existsSync(path.join(root, rel))
|
|
78
|
+
? { name, status: "ok", message: `${rel} exists` }
|
|
79
|
+
: { name, status: "warn", message: `${rel} does not exist`, suggestion });
|
|
80
|
+
}
|
|
81
|
+
const templatesAbs = path.join(root, config.paths.templates);
|
|
82
|
+
checks.push({
|
|
83
|
+
name: "paths.templates",
|
|
84
|
+
status: "ok",
|
|
85
|
+
message: fs.existsSync(templatesAbs)
|
|
86
|
+
? `Project template overrides found at ${config.paths.templates}`
|
|
87
|
+
: "No template overrides (using built-in templates)",
|
|
88
|
+
});
|
|
89
|
+
// Git & hooks
|
|
90
|
+
const isGitRepo = fs.existsSync(path.join(root, ".git"));
|
|
91
|
+
checks.push(isGitRepo
|
|
92
|
+
? { name: "git", status: "ok", message: "Git repository detected" }
|
|
93
|
+
: {
|
|
94
|
+
name: "git",
|
|
95
|
+
status: "warn",
|
|
96
|
+
message: "Not a git repository",
|
|
97
|
+
suggestion: "Run `git init` to enable git hooks and CI checks.",
|
|
98
|
+
});
|
|
99
|
+
const lefthookFile = ["lefthook.yml", "lefthook.yaml"].find((f) => fs.existsSync(path.join(root, f)));
|
|
100
|
+
if (lefthookFile) {
|
|
101
|
+
const version = execChecks ? commandVersion("lefthook") : "skipped";
|
|
102
|
+
checks.push(version
|
|
103
|
+
? { name: "hooks", status: "ok", message: `${lefthookFile} present (lefthook: ${version})` }
|
|
104
|
+
: {
|
|
105
|
+
name: "hooks",
|
|
106
|
+
status: "warn",
|
|
107
|
+
message: `${lefthookFile} present but the lefthook CLI is not installed`,
|
|
108
|
+
suggestion: "Install it: pnpm add -D lefthook && pnpm exec lefthook install",
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
else if (fs.existsSync(path.join(root, ".husky"))) {
|
|
112
|
+
checks.push({ name: "hooks", status: "ok", message: ".husky directory detected" });
|
|
113
|
+
}
|
|
114
|
+
else if (config.features.hooks) {
|
|
115
|
+
checks.push({
|
|
116
|
+
name: "hooks",
|
|
117
|
+
status: "warn",
|
|
118
|
+
message: "features.hooks is enabled but no hook configuration was found",
|
|
119
|
+
suggestion: "Run `meta config hooks lefthook`.",
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
checks.push({
|
|
124
|
+
name: "hooks",
|
|
125
|
+
status: "ok",
|
|
126
|
+
message: "Git hooks not configured (optional)",
|
|
127
|
+
suggestion: "Run `meta config hooks lefthook` to run `meta check` on pre-commit.",
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
// OpenSpec
|
|
131
|
+
const openspecAbs = path.join(root, config.paths.openspec);
|
|
132
|
+
if (fs.existsSync(openspecAbs)) {
|
|
133
|
+
const hasConfig = fs.existsSync(path.join(openspecAbs, "config.yaml"));
|
|
134
|
+
checks.push(hasConfig
|
|
135
|
+
? { name: "openspec", status: "ok", message: `OpenSpec detected at ${config.paths.openspec}/` }
|
|
136
|
+
: {
|
|
137
|
+
name: "openspec",
|
|
138
|
+
status: "warn",
|
|
139
|
+
message: `${config.paths.openspec}/ exists but has no config.yaml`,
|
|
140
|
+
suggestion: "Initialize it with the OpenSpec CLI (`openspec init`).",
|
|
141
|
+
});
|
|
142
|
+
const version = execChecks ? commandVersion("openspec") : "skipped";
|
|
143
|
+
if (execChecks && !version) {
|
|
144
|
+
checks.push({
|
|
145
|
+
name: "openspec-cli",
|
|
146
|
+
status: "warn",
|
|
147
|
+
message: "OpenSpec directory exists but the `openspec` CLI is not installed",
|
|
148
|
+
suggestion: "`meta sync --openspec` needs the OpenSpec CLI on PATH.",
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
else if (config.features.openspec === true) {
|
|
153
|
+
checks.push({
|
|
154
|
+
name: "openspec",
|
|
155
|
+
status: "warn",
|
|
156
|
+
message: `features.openspec is enabled but ${config.paths.openspec}/ does not exist`,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
checks.push({ name: "openspec", status: "ok", message: "OpenSpec not detected (optional)" });
|
|
161
|
+
}
|
|
162
|
+
return checks;
|
|
163
|
+
}
|
|
164
|
+
export function runDoctor(root, opts = {}) {
|
|
165
|
+
const checks = collectDoctorChecks(root, opts);
|
|
166
|
+
const summary = {
|
|
167
|
+
ok: checks.filter((c) => c.status === "ok").length,
|
|
168
|
+
warnings: checks.filter((c) => c.status === "warn").length,
|
|
169
|
+
failures: checks.filter((c) => c.status === "fail").length,
|
|
170
|
+
};
|
|
171
|
+
if (opts.json) {
|
|
172
|
+
console.log(JSON.stringify({ checks, summary }, null, 2));
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
console.log(pc.bold("Agent Meta Doctor"));
|
|
176
|
+
console.log();
|
|
177
|
+
for (const c of checks) {
|
|
178
|
+
const mark = c.status === "ok" ? pc.green("✓") : c.status === "warn" ? pc.yellow("WARN") : pc.red("✗");
|
|
179
|
+
console.log(`${mark} ${c.message}`);
|
|
180
|
+
if (c.suggestion && c.status !== "ok")
|
|
181
|
+
console.log(` → ${c.suggestion}`);
|
|
182
|
+
}
|
|
183
|
+
console.log();
|
|
184
|
+
console.log(`Checks: ${checks.length} OK: ${summary.ok} Warnings: ${summary.warnings} Failures: ${summary.failures}`);
|
|
185
|
+
}
|
|
186
|
+
return summary.failures > 0 ? 1 : 0;
|
|
187
|
+
}
|
package/dist/commands/init.d.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
export interface InitOptions {
|
|
2
2
|
yes?: boolean;
|
|
3
3
|
type?: string;
|
|
4
|
+
/** Comma-separated MCP pack names to install, e.g. "ironbank,playwright". */
|
|
5
|
+
withMcp?: string;
|
|
6
|
+
/** Deployment integration: cloudflare | ansible | github-actions | none. */
|
|
7
|
+
withDeploy?: string;
|
|
4
8
|
dryRun?: boolean;
|
|
5
9
|
}
|
|
6
10
|
export declare function detectProjectType(root: string): string;
|
package/dist/commands/init.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import YAML from "yaml";
|
|
4
|
-
import { input, select } from "@inquirer/prompts";
|
|
4
|
+
import { checkbox, input, select } from "@inquirer/prompts";
|
|
5
5
|
import { FileWriter } from "../core/filesystem.js";
|
|
6
6
|
import { logger } from "../core/logger.js";
|
|
7
7
|
import { renderNamedTemplate } from "../core/templates.js";
|
|
@@ -9,6 +9,9 @@ import { todayISO } from "../core/paths.js";
|
|
|
9
9
|
import { configSchema, DEFAULT_CONFIG } from "../schemas/config.schema.js";
|
|
10
10
|
import { CONFIG_FILENAME, configExists } from "../core/config.js";
|
|
11
11
|
import { padAdrNumber } from "../generators/adr-generator.js";
|
|
12
|
+
import { installMcpPack } from "../generators/mcp-generator.js";
|
|
13
|
+
import { getMcpPack, packServerSummary, MCP_PACKS } from "../core/mcp-packs.js";
|
|
14
|
+
import { getDeployPack, DEPLOY_PACKS } from "../core/deploy-packs.js";
|
|
12
15
|
import { EXIT_CODES, MetaError } from "../core/errors.js";
|
|
13
16
|
const PROJECT_TYPES = ["node", "monorepo", "nextjs", "cocos", "backend", "other"];
|
|
14
17
|
export function detectProjectType(root) {
|
|
@@ -73,6 +76,38 @@ export async function runInit(root, opts) {
|
|
|
73
76
|
});
|
|
74
77
|
}
|
|
75
78
|
}
|
|
79
|
+
// Optional MCP packs (project-scoped .mcp.json + .claude/skills/).
|
|
80
|
+
let mcpPacks = [
|
|
81
|
+
...new Set((opts.withMcp ?? "")
|
|
82
|
+
.split(",")
|
|
83
|
+
.map((s) => s.trim())
|
|
84
|
+
.filter(Boolean)),
|
|
85
|
+
].map(getMcpPack);
|
|
86
|
+
if (mcpPacks.length === 0 && !opts.yes) {
|
|
87
|
+
const picks = await checkbox({
|
|
88
|
+
message: "选择要安装的工具包(MCP / 插件 / skills,全部项目级,回车跳过):",
|
|
89
|
+
choices: Object.values(MCP_PACKS).map((p) => ({ name: p.title, value: p.name })),
|
|
90
|
+
});
|
|
91
|
+
mcpPacks = picks.map(getMcpPack);
|
|
92
|
+
}
|
|
93
|
+
// Deployment integration: installs a deploy skill into .claude/skills/.
|
|
94
|
+
let deployTarget = opts.withDeploy?.trim();
|
|
95
|
+
if (deployTarget && deployTarget !== "none") {
|
|
96
|
+
getDeployPack(deployTarget); // fail fast on unknown targets, before any write
|
|
97
|
+
}
|
|
98
|
+
if (!deployTarget && !opts.yes) {
|
|
99
|
+
deployTarget = await select({
|
|
100
|
+
message: "部署方式(安装对应的部署 skill,可稍后再加):",
|
|
101
|
+
choices: [
|
|
102
|
+
...Object.entries(DEPLOY_PACKS).map(([value, p]) => ({ name: p.title, value })),
|
|
103
|
+
{ name: "暂时不集成部署", value: "none" },
|
|
104
|
+
],
|
|
105
|
+
default: "none",
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
const deployPack = deployTarget && deployTarget !== "none" ? getDeployPack(deployTarget) : null;
|
|
109
|
+
const allPacks = deployPack ? [...mcpPacks, deployPack] : mcpPacks;
|
|
110
|
+
const packRules = allPacks.flatMap((p) => p.agentRules ?? []);
|
|
76
111
|
// Existing AGENTS.md handling.
|
|
77
112
|
const agentsRel = DEFAULT_CONFIG.paths.root_agents;
|
|
78
113
|
let agentsAction = writer.exists(agentsRel) ? "keep" : "create";
|
|
@@ -110,6 +145,7 @@ export async function runInit(root, opts) {
|
|
|
110
145
|
project_type: projectType,
|
|
111
146
|
package_manager: packageManager,
|
|
112
147
|
main_paths: "src/",
|
|
148
|
+
extra_rules: packRules.length > 0 ? packRules.map((r) => `- ${r}`).join("\n") + "\n" : "",
|
|
113
149
|
};
|
|
114
150
|
if (agentsAction === "backup") {
|
|
115
151
|
writer.backup(agentsRel);
|
|
@@ -132,10 +168,14 @@ export async function runInit(root, opts) {
|
|
|
132
168
|
writer.write("docs/architecture/system-overview.md", renderNamedTemplate("system-overview.md", root, templatesDir, vars));
|
|
133
169
|
writer.write("docs/api/conventions.md", renderNamedTemplate("api-conventions.md", root, templatesDir, vars));
|
|
134
170
|
writer.write("docs/engineering/testing.md", renderNamedTemplate("engineering-testing.md", root, templatesDir, vars));
|
|
171
|
+
writer.write("docs/engineering/constraints.md", renderNamedTemplate("engineering-constraints.md", root, templatesDir, vars));
|
|
135
172
|
writer.write("docs/runbooks/README.md", renderNamedTemplate("runbooks-readme.md", root, templatesDir, vars));
|
|
136
173
|
// 4. First ADR
|
|
137
174
|
const adrNumber = padAdrNumber(1, DEFAULT_CONFIG.defaults.adr_padding);
|
|
138
175
|
writer.write(`docs/adr/${adrNumber}-repository-conventions.md`, renderNamedTemplate("adr-0001.md", root, templatesDir, { adr_number: adrNumber, owner, date }));
|
|
176
|
+
// 5. Optional MCP packs and deploy skill
|
|
177
|
+
for (const pack of allPacks)
|
|
178
|
+
installMcpPack(writer, root, pack);
|
|
139
179
|
// Report
|
|
140
180
|
if (dryRun) {
|
|
141
181
|
const created = writer.created();
|
|
@@ -171,6 +211,22 @@ export async function runInit(root, opts) {
|
|
|
171
211
|
if (fs.existsSync(path.join(root, DEFAULT_CONFIG.paths.openspec))) {
|
|
172
212
|
logger.ok(`OpenSpec detected at ${DEFAULT_CONFIG.paths.openspec}/`);
|
|
173
213
|
}
|
|
214
|
+
for (const pack of allPacks) {
|
|
215
|
+
logger.ok(`Pack '${pack.name}' installed: ${packServerSummary(pack)}`);
|
|
216
|
+
}
|
|
217
|
+
if (allPacks.length > 0) {
|
|
218
|
+
const artifacts = [
|
|
219
|
+
allPacks.some((p) => p.server) ? ".mcp.json" : "",
|
|
220
|
+
allPacks.some((p) => p.settings) ? ".claude/settings.json" : "",
|
|
221
|
+
allPacks.some((p) => p.hasSkills) ? ".claude/skills/" : "",
|
|
222
|
+
].filter(Boolean);
|
|
223
|
+
logger.info(` Commit ${artifacts.join(", ")} so the whole team gets the same tools.`);
|
|
224
|
+
}
|
|
225
|
+
if (packRules.length > 0 && agentsAction === "keep") {
|
|
226
|
+
logger.warn("AGENTS.md was kept unchanged. Add the pack rules to it manually:");
|
|
227
|
+
for (const r of packRules)
|
|
228
|
+
logger.info(` - ${r}`);
|
|
229
|
+
}
|
|
174
230
|
logger.blank();
|
|
175
231
|
logger.info("Next steps:");
|
|
176
232
|
logger.info("1. Fill in AGENTS.md project commands.");
|
package/dist/commands/new-adr.js
CHANGED
|
@@ -46,7 +46,8 @@ export function runNewAdr(root, name, opts) {
|
|
|
46
46
|
throw new MetaError(e.message, EXIT_CODES.CHECK_FAILED);
|
|
47
47
|
}
|
|
48
48
|
if (config.features.generate_indexes && !opts.dryRun) {
|
|
49
|
-
|
|
49
|
+
// Keep the index next to the ADRs, including when --path overrides the directory.
|
|
50
|
+
updateAdrIndex(writer, root, config, relDir);
|
|
50
51
|
}
|
|
51
52
|
if (opts.dryRun) {
|
|
52
53
|
logger.info("Would create:");
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface NewChangeOptions {
|
|
2
|
+
domain?: string;
|
|
3
|
+
schema?: string;
|
|
4
|
+
dryRun?: boolean;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Lightweight OpenSpec entry point: creates a change directory compatible
|
|
8
|
+
* with the OpenSpec layout (proposal.md / design.md / tasks.md / specs/) and
|
|
9
|
+
* adds risk-review material for high-risk domains. It never rewrites files
|
|
10
|
+
* that already exist.
|
|
11
|
+
*/
|
|
12
|
+
export declare function runNewChange(root: string, name: string, opts: NewChangeOptions): void;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { FileWriter } from "../core/filesystem.js";
|
|
4
|
+
import { logger } from "../core/logger.js";
|
|
5
|
+
import { loadConfig } from "../core/config.js";
|
|
6
|
+
import { renderNamedTemplate } from "../core/templates.js";
|
|
7
|
+
import { slugify, todayISO } from "../core/paths.js";
|
|
8
|
+
import { matchRiskKeywords } from "../core/risk.js";
|
|
9
|
+
import { extractSection, parseDocFile, HARD_CONSTRAINT_HEADINGS } from "../core/frontmatter.js";
|
|
10
|
+
import { EXIT_CODES, MetaError } from "../core/errors.js";
|
|
11
|
+
const RISK_REVIEW_POINTS = [
|
|
12
|
+
"Money representation",
|
|
13
|
+
"Idempotency",
|
|
14
|
+
"Retry behavior",
|
|
15
|
+
"Callback deduplication",
|
|
16
|
+
"Audit logging",
|
|
17
|
+
"Permission boundaries",
|
|
18
|
+
"Rollback plan",
|
|
19
|
+
];
|
|
20
|
+
/**
|
|
21
|
+
* Lightweight OpenSpec entry point: creates a change directory compatible
|
|
22
|
+
* with the OpenSpec layout (proposal.md / design.md / tasks.md / specs/) and
|
|
23
|
+
* adds risk-review material for high-risk domains. It never rewrites files
|
|
24
|
+
* that already exist.
|
|
25
|
+
*/
|
|
26
|
+
export function runNewChange(root, name, opts) {
|
|
27
|
+
const { config } = loadConfig(root);
|
|
28
|
+
if (config.features.openspec === false) {
|
|
29
|
+
throw new MetaError("features.openspec is disabled in agent-meta.config.yaml. Enable it before creating changes.", EXIT_CODES.BAD_ARGS);
|
|
30
|
+
}
|
|
31
|
+
const slug = slugify(name);
|
|
32
|
+
if (!slug) {
|
|
33
|
+
throw new MetaError(`Cannot derive a kebab-case change name from '${name}'. Use an ASCII name, e.g. add-withdrawal-review.`, EXIT_CODES.BAD_ARGS);
|
|
34
|
+
}
|
|
35
|
+
const domainSlug = opts.domain ? slugify(opts.domain) : "";
|
|
36
|
+
const openspecRel = config.paths.openspec;
|
|
37
|
+
const changeRel = path.join(openspecRel, "changes", slug);
|
|
38
|
+
const riskHits = [
|
|
39
|
+
...new Set([
|
|
40
|
+
...matchRiskKeywords(slug, config.risk_keywords),
|
|
41
|
+
...matchRiskKeywords(domainSlug, config.risk_keywords),
|
|
42
|
+
]),
|
|
43
|
+
];
|
|
44
|
+
const withRiskReview = Boolean(opts.schema) || riskHits.length > 0;
|
|
45
|
+
const vars = {
|
|
46
|
+
change_name: slug,
|
|
47
|
+
change_title: slug,
|
|
48
|
+
domain: domainSlug,
|
|
49
|
+
date: todayISO(),
|
|
50
|
+
};
|
|
51
|
+
const writer = new FileWriter(root, Boolean(opts.dryRun));
|
|
52
|
+
const templatesDir = config.paths.templates;
|
|
53
|
+
// Existing files are skipped, never overwritten (FileWriter default).
|
|
54
|
+
writer.write(path.join(changeRel, "proposal.md"), renderNamedTemplate("change-proposal.md", root, templatesDir, vars));
|
|
55
|
+
writer.write(path.join(changeRel, "design.md"), renderNamedTemplate("change-design.md", root, templatesDir, vars));
|
|
56
|
+
writer.write(path.join(changeRel, "tasks.md"), renderNamedTemplate("change-tasks.md", root, templatesDir, vars));
|
|
57
|
+
writer.write(path.join(changeRel, "specs", ".gitkeep"), "");
|
|
58
|
+
if (withRiskReview) {
|
|
59
|
+
writer.write(path.join(changeRel, "risk-review.md"), renderNamedTemplate("change-risk-review.md", root, templatesDir, vars));
|
|
60
|
+
}
|
|
61
|
+
if (opts.schema) {
|
|
62
|
+
writer.write(path.join(changeRel, ".openspec.yaml"), `schema: ${opts.schema}\n${domainSlug ? `domain: ${domainSlug}\n` : ""}`);
|
|
63
|
+
}
|
|
64
|
+
if (opts.dryRun) {
|
|
65
|
+
logger.info("Would create:");
|
|
66
|
+
for (const a of writer.created())
|
|
67
|
+
logger.info(` ${a.path}`);
|
|
68
|
+
for (const a of writer.skipped())
|
|
69
|
+
logger.info(` (skip, exists) ${a.path}`);
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
for (const a of writer.created())
|
|
73
|
+
logger.ok(`Created ${a.path}`);
|
|
74
|
+
for (const a of writer.skipped())
|
|
75
|
+
logger.info(`- Skipped ${a.path} (already exists)`);
|
|
76
|
+
}
|
|
77
|
+
if (!fs.existsSync(path.join(root, openspecRel, "config.yaml"))) {
|
|
78
|
+
logger.blank();
|
|
79
|
+
logger.warn(`${openspecRel}/config.yaml not found. If this project uses OpenSpec, initialize it with the OpenSpec CLI (\`openspec init\`).`);
|
|
80
|
+
}
|
|
81
|
+
// Domain association: point the author at the required reading.
|
|
82
|
+
if (domainSlug) {
|
|
83
|
+
const domainRel = path.join(config.paths.domain_docs, domainSlug);
|
|
84
|
+
const domainAbs = path.join(root, domainRel);
|
|
85
|
+
logger.blank();
|
|
86
|
+
if (!fs.existsSync(domainAbs)) {
|
|
87
|
+
logger.warn(`Domain directory not found: ${domainRel}\nCreate it first: meta new-domain ${domainSlug}`);
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
const reading = ["README.md", "rules.md", "state-machine.md", "AGENTS.md"].filter((f) => fs.existsSync(path.join(domainAbs, f)));
|
|
91
|
+
logger.info("Required reading before implementing this change:");
|
|
92
|
+
for (const f of reading)
|
|
93
|
+
logger.info(` ${path.join(domainRel, f)}`);
|
|
94
|
+
// Feed the declared hard constraints straight into the author's context.
|
|
95
|
+
const rulesAbs = path.join(domainAbs, "rules.md");
|
|
96
|
+
if (fs.existsSync(rulesAbs)) {
|
|
97
|
+
const hard = extractSection(parseDocFile(rulesAbs).content, HARD_CONSTRAINT_HEADINGS);
|
|
98
|
+
const hardLines = (hard ?? "")
|
|
99
|
+
.split("\n")
|
|
100
|
+
.map((l) => l.trim())
|
|
101
|
+
.filter((l) => l && l !== "-");
|
|
102
|
+
if (hardLines.length > 0) {
|
|
103
|
+
logger.blank();
|
|
104
|
+
logger.info(`Declared hard constraints for '${domainSlug}' (must not be violated):`);
|
|
105
|
+
for (const l of hardLines)
|
|
106
|
+
logger.info(` ${l}`);
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
logger.blank();
|
|
110
|
+
logger.warn(`Domain '${domainSlug}' declares no hard constraints in rules.md (## 硬限制). Declare them before implementing this change.`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (riskHits.length > 0) {
|
|
116
|
+
logger.blank();
|
|
117
|
+
logger.warn(`⚠ Change is associated with high-risk domain: ${riskHits.join(", ")}`);
|
|
118
|
+
logger.blank();
|
|
119
|
+
logger.info("Recommended review points:");
|
|
120
|
+
for (const p of RISK_REVIEW_POINTS)
|
|
121
|
+
logger.info(` - ${p}`);
|
|
122
|
+
logger.blank();
|
|
123
|
+
logger.info(`A risk-review.md checklist was ${opts.dryRun ? "included" : "created"} in the change directory.`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -95,6 +95,8 @@ export function runNewDomain(root, name, opts) {
|
|
|
95
95
|
logger.info(m);
|
|
96
96
|
logger.blank();
|
|
97
97
|
}
|
|
98
|
+
logger.info("Declare the domain's hard constraints in rules.md (## 硬限制) before implementation — undeclared limits will be treated as negotiable.");
|
|
99
|
+
logger.blank();
|
|
98
100
|
logger.info("Also consider creating ADRs for:");
|
|
99
101
|
logger.info(" - idempotency");
|
|
100
102
|
logger.info(" - audit logging");
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type ChecklistItem } from "../core/checklist.js";
|
|
2
|
+
export interface PreflightOptions {
|
|
3
|
+
json?: boolean;
|
|
4
|
+
/** Interactively require a human to confirm each manual item. */
|
|
5
|
+
confirm?: boolean;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Pre-release checklist. `meta check --strict` runs for real; the manual
|
|
9
|
+
* items — most importantly a human confirming the code review — stay
|
|
10
|
+
* pending unless walked through with --confirm.
|
|
11
|
+
*/
|
|
12
|
+
export declare function buildPreflightChecklist(root: string): ChecklistItem[];
|
|
13
|
+
/** Returns the process exit code. */
|
|
14
|
+
export declare function runPreflight(root: string, opts?: PreflightOptions, ask?: (item: ChecklistItem) => Promise<boolean>): Promise<number>;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { confirm } from "@inquirer/prompts";
|
|
4
|
+
import { loadConfig } from "../core/config.js";
|
|
5
|
+
import { runCheck } from "./check.js";
|
|
6
|
+
import { listDomainDirs } from "../validators/domain-validator.js";
|
|
7
|
+
import { isHighRisk } from "../core/risk.js";
|
|
8
|
+
import { parseDocFile, sectionHasContent, HARD_CONSTRAINT_HEADINGS, } from "../core/frontmatter.js";
|
|
9
|
+
import { printChecklist, checklistToJson } from "../core/checklist.js";
|
|
10
|
+
import { logger } from "../core/logger.js";
|
|
11
|
+
const AUTO = "Deterministic checks";
|
|
12
|
+
const MANUAL = "Human sign-off required";
|
|
13
|
+
/**
|
|
14
|
+
* Pre-release checklist. `meta check --strict` runs for real; the manual
|
|
15
|
+
* items — most importantly a human confirming the code review — stay
|
|
16
|
+
* pending unless walked through with --confirm.
|
|
17
|
+
*/
|
|
18
|
+
export function buildPreflightChecklist(root) {
|
|
19
|
+
const { config } = loadConfig(root);
|
|
20
|
+
const result = runCheck(root, {});
|
|
21
|
+
const strictPass = result.summary.errors === 0 && result.summary.warnings === 0;
|
|
22
|
+
const domains = listDomainDirs(root, config.paths.domain_docs);
|
|
23
|
+
const highRisk = domains.filter((d) => isHighRisk(d, config.risk_keywords));
|
|
24
|
+
// Files that declare hard constraints — the reviewer checks the release
|
|
25
|
+
// against exactly these.
|
|
26
|
+
const hardConstraintFiles = [];
|
|
27
|
+
for (const d of domains) {
|
|
28
|
+
const rel = path.join(config.paths.domain_docs, d, "rules.md");
|
|
29
|
+
const abs = path.join(root, rel);
|
|
30
|
+
if (fs.existsSync(abs) && sectionHasContent(parseDocFile(abs).content, HARD_CONSTRAINT_HEADINGS)) {
|
|
31
|
+
hardConstraintFiles.push(rel);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const constraintsRel = path.join(config.paths.docs, "engineering", "constraints.md");
|
|
35
|
+
const constraintsAbs = path.join(root, constraintsRel);
|
|
36
|
+
if (fs.existsSync(constraintsAbs) &&
|
|
37
|
+
sectionHasContent(parseDocFile(constraintsAbs).content, HARD_CONSTRAINT_HEADINGS)) {
|
|
38
|
+
hardConstraintFiles.push(constraintsRel);
|
|
39
|
+
}
|
|
40
|
+
const manual = (id, text, details) => ({
|
|
41
|
+
id,
|
|
42
|
+
section: MANUAL,
|
|
43
|
+
text,
|
|
44
|
+
kind: "manual",
|
|
45
|
+
status: "pending",
|
|
46
|
+
details,
|
|
47
|
+
});
|
|
48
|
+
return [
|
|
49
|
+
{
|
|
50
|
+
id: "pre-meta-check",
|
|
51
|
+
section: AUTO,
|
|
52
|
+
text: "`meta check --strict` passes",
|
|
53
|
+
kind: "auto",
|
|
54
|
+
status: strictPass ? "pass" : "fail",
|
|
55
|
+
details: strictPass
|
|
56
|
+
? []
|
|
57
|
+
: [`errors: ${result.summary.errors}, warnings: ${result.summary.warnings}`],
|
|
58
|
+
},
|
|
59
|
+
manual("pre-code-review", "A human has reviewed and approved the code changes (not only an AI review)"),
|
|
60
|
+
manual("pre-tests", "Full test suite passes (CI is green on the release commit)"),
|
|
61
|
+
manual("pre-hard-constraints", "No change violates a declared hard constraint", hardConstraintFiles.length > 0 ? hardConstraintFiles : undefined),
|
|
62
|
+
manual("pre-highrisk", "Domain rules re-read for every touched high-risk domain", highRisk.length > 0 ? highRisk : undefined),
|
|
63
|
+
manual("pre-docs", "Docs and OpenSpec specs updated for externally observable changes"),
|
|
64
|
+
manual("pre-adr", "Long-term decisions made during this release are recorded as ADRs"),
|
|
65
|
+
manual("pre-rollback", "Rollback plan and failure paths are prepared"),
|
|
66
|
+
manual("pre-monitoring", "Monitoring, alerts and audit logging are in place for new behavior"),
|
|
67
|
+
];
|
|
68
|
+
}
|
|
69
|
+
/** Returns the process exit code. */
|
|
70
|
+
export async function runPreflight(root, opts = {}, ask = (item) => confirm({ message: item.text, default: false })) {
|
|
71
|
+
const items = buildPreflightChecklist(root);
|
|
72
|
+
const autoFailed = items.some((i) => i.kind === "auto" && i.status === "fail");
|
|
73
|
+
if (opts.confirm) {
|
|
74
|
+
printChecklist("Pre-release Checklist", items);
|
|
75
|
+
if (autoFailed) {
|
|
76
|
+
logger.error("Deterministic checks failed — fix them before asking for human sign-off.");
|
|
77
|
+
return 1;
|
|
78
|
+
}
|
|
79
|
+
let declined = false;
|
|
80
|
+
for (const item of items.filter((i) => i.kind === "manual")) {
|
|
81
|
+
const yes = await ask(item);
|
|
82
|
+
item.status = yes ? "confirmed" : "declined";
|
|
83
|
+
if (!yes)
|
|
84
|
+
declined = true;
|
|
85
|
+
}
|
|
86
|
+
console.log();
|
|
87
|
+
printChecklist("Pre-release Checklist — result", items);
|
|
88
|
+
if (declined) {
|
|
89
|
+
logger.error("Not ready to release: some items were not confirmed.");
|
|
90
|
+
return 1;
|
|
91
|
+
}
|
|
92
|
+
logger.ok("All items confirmed. Ready to release.");
|
|
93
|
+
return 0;
|
|
94
|
+
}
|
|
95
|
+
if (opts.json) {
|
|
96
|
+
console.log(JSON.stringify(checklistToJson(items), null, 2));
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
printChecklist("Pre-release Checklist", items);
|
|
100
|
+
console.log("Run `meta preflight --confirm` to walk through human sign-off.");
|
|
101
|
+
}
|
|
102
|
+
return autoFailed ? 1 : 0;
|
|
103
|
+
}
|