@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.
Files changed (50) hide show
  1. package/README.md +61 -2
  2. package/assets/skills/deploy-ansible/deploy-ansible/SKILL.md +71 -0
  3. package/assets/skills/deploy-cloudflare/deploy-cloudflare/SKILL.md +51 -0
  4. package/assets/skills/deploy-github-actions/deploy-github-actions/SKILL.md +50 -0
  5. package/assets/skills/ironbank/ironbank-citations/SKILL.md +197 -0
  6. package/assets/skills/ironbank/ironbank-docs-research/SKILL.md +205 -0
  7. package/assets/skills/ironbank/ironbank-mcp-reference/SKILL.md +414 -0
  8. package/assets/skills/ironbank/ironbank-note-links/SKILL.md +87 -0
  9. package/dist/cli.js +74 -2
  10. package/dist/commands/config.d.ts +5 -0
  11. package/dist/commands/config.js +50 -0
  12. package/dist/commands/doctor.d.ts +14 -0
  13. package/dist/commands/doctor.js +187 -0
  14. package/dist/commands/init.d.ts +4 -0
  15. package/dist/commands/init.js +57 -1
  16. package/dist/commands/new-adr.js +2 -1
  17. package/dist/commands/new-change.d.ts +12 -0
  18. package/dist/commands/new-change.js +125 -0
  19. package/dist/commands/new-domain.js +2 -0
  20. package/dist/commands/preflight.d.ts +14 -0
  21. package/dist/commands/preflight.js +103 -0
  22. package/dist/commands/review.d.ts +18 -0
  23. package/dist/commands/review.js +102 -0
  24. package/dist/commands/sync.js +11 -2
  25. package/dist/core/checklist.d.ts +31 -0
  26. package/dist/core/checklist.js +43 -0
  27. package/dist/core/deploy-packs.d.ts +11 -0
  28. package/dist/core/deploy-packs.js +42 -0
  29. package/dist/core/doc-scopes.d.ts +18 -0
  30. package/dist/core/doc-scopes.js +55 -0
  31. package/dist/core/filesystem.js +9 -1
  32. package/dist/core/frontmatter.d.ts +7 -0
  33. package/dist/core/frontmatter.js +45 -9
  34. package/dist/core/mcp-packs.d.ts +26 -0
  35. package/dist/core/mcp-packs.js +86 -0
  36. package/dist/generators/adr-generator.js +3 -1
  37. package/dist/generators/hook-generator.d.ts +14 -0
  38. package/dist/generators/hook-generator.js +55 -0
  39. package/dist/generators/index-generator.d.ts +2 -2
  40. package/dist/generators/index-generator.js +15 -8
  41. package/dist/generators/mcp-generator.d.ts +9 -0
  42. package/dist/generators/mcp-generator.js +68 -0
  43. package/dist/schemas/config.schema.d.ts +0 -10
  44. package/dist/schemas/config.schema.js +0 -2
  45. package/dist/templates/index.d.ts +7 -2
  46. package/dist/templates/index.js +125 -9
  47. package/dist/validators/adr-validator.js +4 -4
  48. package/dist/validators/domain-validator.js +30 -1
  49. package/dist/validators/openspec-validator.js +33 -4
  50. package/package.json +9 -15
@@ -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
+ }
@@ -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;
@@ -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.");
@@ -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
- updateAdrIndex(writer, root, config);
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
+ }