@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,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
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,8 @@ 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";
|
|
12
14
|
import { EXIT_CODES, MetaError } from "../core/errors.js";
|
|
13
15
|
const PROJECT_TYPES = ["node", "monorepo", "nextjs", "cocos", "backend", "other"];
|
|
14
16
|
export function detectProjectType(root) {
|
|
@@ -73,6 +75,21 @@ export async function runInit(root, opts) {
|
|
|
73
75
|
});
|
|
74
76
|
}
|
|
75
77
|
}
|
|
78
|
+
// Optional MCP packs (project-scoped .mcp.json + .claude/skills/).
|
|
79
|
+
let mcpPacks = [
|
|
80
|
+
...new Set((opts.withMcp ?? "")
|
|
81
|
+
.split(",")
|
|
82
|
+
.map((s) => s.trim())
|
|
83
|
+
.filter(Boolean)),
|
|
84
|
+
].map(getMcpPack);
|
|
85
|
+
if (mcpPacks.length === 0 && !opts.yes) {
|
|
86
|
+
const picks = await checkbox({
|
|
87
|
+
message: "选择要安装的工具包(MCP / 插件 / skills,全部项目级,回车跳过):",
|
|
88
|
+
choices: Object.values(MCP_PACKS).map((p) => ({ name: p.title, value: p.name })),
|
|
89
|
+
});
|
|
90
|
+
mcpPacks = picks.map(getMcpPack);
|
|
91
|
+
}
|
|
92
|
+
const packRules = mcpPacks.flatMap((p) => p.agentRules ?? []);
|
|
76
93
|
// Existing AGENTS.md handling.
|
|
77
94
|
const agentsRel = DEFAULT_CONFIG.paths.root_agents;
|
|
78
95
|
let agentsAction = writer.exists(agentsRel) ? "keep" : "create";
|
|
@@ -110,6 +127,7 @@ export async function runInit(root, opts) {
|
|
|
110
127
|
project_type: projectType,
|
|
111
128
|
package_manager: packageManager,
|
|
112
129
|
main_paths: "src/",
|
|
130
|
+
extra_rules: packRules.length > 0 ? packRules.map((r) => `- ${r}`).join("\n") + "\n" : "",
|
|
113
131
|
};
|
|
114
132
|
if (agentsAction === "backup") {
|
|
115
133
|
writer.backup(agentsRel);
|
|
@@ -132,10 +150,14 @@ export async function runInit(root, opts) {
|
|
|
132
150
|
writer.write("docs/architecture/system-overview.md", renderNamedTemplate("system-overview.md", root, templatesDir, vars));
|
|
133
151
|
writer.write("docs/api/conventions.md", renderNamedTemplate("api-conventions.md", root, templatesDir, vars));
|
|
134
152
|
writer.write("docs/engineering/testing.md", renderNamedTemplate("engineering-testing.md", root, templatesDir, vars));
|
|
153
|
+
writer.write("docs/engineering/constraints.md", renderNamedTemplate("engineering-constraints.md", root, templatesDir, vars));
|
|
135
154
|
writer.write("docs/runbooks/README.md", renderNamedTemplate("runbooks-readme.md", root, templatesDir, vars));
|
|
136
155
|
// 4. First ADR
|
|
137
156
|
const adrNumber = padAdrNumber(1, DEFAULT_CONFIG.defaults.adr_padding);
|
|
138
157
|
writer.write(`docs/adr/${adrNumber}-repository-conventions.md`, renderNamedTemplate("adr-0001.md", root, templatesDir, { adr_number: adrNumber, owner, date }));
|
|
158
|
+
// 5. Optional MCP packs
|
|
159
|
+
for (const pack of mcpPacks)
|
|
160
|
+
installMcpPack(writer, root, pack);
|
|
139
161
|
// Report
|
|
140
162
|
if (dryRun) {
|
|
141
163
|
const created = writer.created();
|
|
@@ -171,6 +193,22 @@ export async function runInit(root, opts) {
|
|
|
171
193
|
if (fs.existsSync(path.join(root, DEFAULT_CONFIG.paths.openspec))) {
|
|
172
194
|
logger.ok(`OpenSpec detected at ${DEFAULT_CONFIG.paths.openspec}/`);
|
|
173
195
|
}
|
|
196
|
+
for (const pack of mcpPacks) {
|
|
197
|
+
logger.ok(`Pack '${pack.name}' installed: ${packServerSummary(pack)}`);
|
|
198
|
+
}
|
|
199
|
+
if (mcpPacks.length > 0) {
|
|
200
|
+
const artifacts = [
|
|
201
|
+
mcpPacks.some((p) => p.server) ? ".mcp.json" : "",
|
|
202
|
+
mcpPacks.some((p) => p.settings) ? ".claude/settings.json" : "",
|
|
203
|
+
mcpPacks.some((p) => p.hasSkills) ? ".claude/skills/" : "",
|
|
204
|
+
].filter(Boolean);
|
|
205
|
+
logger.info(` Commit ${artifacts.join(", ")} so the whole team gets the same tools.`);
|
|
206
|
+
}
|
|
207
|
+
if (packRules.length > 0 && agentsAction === "keep") {
|
|
208
|
+
logger.warn("AGENTS.md was kept unchanged. Add the pack rules to it manually:");
|
|
209
|
+
for (const r of packRules)
|
|
210
|
+
logger.info(` - ${r}`);
|
|
211
|
+
}
|
|
174
212
|
logger.blank();
|
|
175
213
|
logger.info("Next steps:");
|
|
176
214
|
logger.info("1. Fill in AGENTS.md project commands.");
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type ResolvedDocScope } from "../core/doc-scopes.js";
|
|
2
|
+
import { type ChecklistItem } from "../core/checklist.js";
|
|
3
|
+
export interface ReviewOptions {
|
|
4
|
+
json?: boolean;
|
|
5
|
+
/** Interactively confirm which documentation scopes to tidy this round. */
|
|
6
|
+
pickDocs?: boolean;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Periodic repository review checklist. Items that map to deterministic
|
|
10
|
+
* `meta check` rules are evaluated live; the rest is a manual routine for
|
|
11
|
+
* the reviewing human (or agent) covering docs, code, tests and security.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* @param selectedScopes doc-scope keys confirmed by the human; undefined
|
|
15
|
+
* means every scope is listed (missing ones marked as such).
|
|
16
|
+
*/
|
|
17
|
+
export declare function buildReviewChecklist(root: string, selectedScopes?: string[]): ChecklistItem[];
|
|
18
|
+
export declare function runReview(root: string, opts?: ReviewOptions, pick?: (scopes: ResolvedDocScope[]) => Promise<string[]>): Promise<void>;
|