@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.
Files changed (35) hide show
  1. package/README.md +46 -2
  2. package/assets/skills/ironbank/ironbank-citations/SKILL.md +195 -0
  3. package/assets/skills/ironbank/ironbank-docs-research/SKILL.md +205 -0
  4. package/assets/skills/ironbank/ironbank-mcp-reference/SKILL.md +413 -0
  5. package/dist/cli.js +72 -1
  6. package/dist/commands/config.d.ts +5 -0
  7. package/dist/commands/config.js +50 -0
  8. package/dist/commands/doctor.d.ts +14 -0
  9. package/dist/commands/doctor.js +187 -0
  10. package/dist/commands/init.d.ts +2 -0
  11. package/dist/commands/init.js +39 -1
  12. package/dist/commands/new-change.d.ts +12 -0
  13. package/dist/commands/new-change.js +125 -0
  14. package/dist/commands/new-domain.js +2 -0
  15. package/dist/commands/preflight.d.ts +14 -0
  16. package/dist/commands/preflight.js +103 -0
  17. package/dist/commands/review.d.ts +18 -0
  18. package/dist/commands/review.js +102 -0
  19. package/dist/core/checklist.d.ts +31 -0
  20. package/dist/core/checklist.js +43 -0
  21. package/dist/core/doc-scopes.d.ts +18 -0
  22. package/dist/core/doc-scopes.js +55 -0
  23. package/dist/core/frontmatter.d.ts +7 -0
  24. package/dist/core/frontmatter.js +35 -0
  25. package/dist/core/mcp-packs.d.ts +26 -0
  26. package/dist/core/mcp-packs.js +83 -0
  27. package/dist/generators/hook-generator.d.ts +14 -0
  28. package/dist/generators/hook-generator.js +55 -0
  29. package/dist/generators/mcp-generator.d.ts +9 -0
  30. package/dist/generators/mcp-generator.js +68 -0
  31. package/dist/templates/index.d.ts +7 -2
  32. package/dist/templates/index.js +125 -9
  33. package/dist/validators/domain-validator.js +30 -1
  34. package/dist/validators/openspec-validator.js +33 -4
  35. 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
+ }
@@ -1,6 +1,8 @@
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;
4
6
  dryRun?: boolean;
5
7
  }
6
8
  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,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>;