@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
@@ -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>;
@@ -0,0 +1,102 @@
1
+ import { checkbox } from "@inquirer/prompts";
2
+ import { loadConfig } from "../core/config.js";
3
+ import { runCheck } from "./check.js";
4
+ import { listDomainDirs } from "../validators/domain-validator.js";
5
+ import { isHighRisk } from "../core/risk.js";
6
+ import { resolveDocScopes } from "../core/doc-scopes.js";
7
+ import { printChecklist, checklistToJson } from "../core/checklist.js";
8
+ import { logger } from "../core/logger.js";
9
+ /**
10
+ * Periodic repository review checklist. Items that map to deterministic
11
+ * `meta check` rules are evaluated live; the rest is a manual routine for
12
+ * the reviewing human (or agent) covering docs, code, tests and security.
13
+ */
14
+ /**
15
+ * @param selectedScopes doc-scope keys confirmed by the human; undefined
16
+ * means every scope is listed (missing ones marked as such).
17
+ */
18
+ export function buildReviewChecklist(root, selectedScopes) {
19
+ const { config } = loadConfig(root);
20
+ const result = runCheck(root, {});
21
+ const files = (codes) => result.issues
22
+ .filter((i) => codes.includes(i.code))
23
+ .map((i) => i.file ?? i.message);
24
+ const highRisk = listDomainDirs(root, config.paths.domain_docs).filter((d) => isHighRisk(d, config.risk_keywords));
25
+ const auto = (id, section, text, offending) => ({
26
+ id,
27
+ section,
28
+ text,
29
+ kind: "auto",
30
+ status: offending.length === 0 ? "pass" : "fail",
31
+ details: offending,
32
+ });
33
+ const manual = (id, section, text, details) => ({
34
+ id,
35
+ section,
36
+ text,
37
+ kind: "manual",
38
+ status: "pending",
39
+ details,
40
+ });
41
+ const DOCS = "Docs(优化文档)";
42
+ const CODE = "Code(review 代码)";
43
+ const TESTS = "Tests(review 测试用例)";
44
+ const SECURITY = "Security(review 安全)";
45
+ return [
46
+ // Docs
47
+ auto("doc-stale", DOCS, "All constrained docs reviewed within their interval", files(["DOC_STALE"])),
48
+ auto("doc-owner", DOCS, "Every constrained doc has an owner", files(["DOC_FRONTMATTER_MISSING_OWNER", "DOMAIN_NO_OWNER"])),
49
+ auto("doc-hard-constraints", DOCS, "Every high-risk domain declares hard constraints", files(["HIGH_RISK_DOMAIN_MISSING_HARD_CONSTRAINTS"])),
50
+ manual("doc-hard-drift", DOCS, "Check for constraint drift: hard constraints enforced in code but not declared in rules.md / constraints.md"),
51
+ auto("doc-agents-budget", DOCS, "AGENTS.md files are within their line budget", files(["AGENTS_TOO_LONG"])),
52
+ auto("doc-duplicates", DOCS, "No duplicated or conflicting agent rules", files(["AGENTS_DUPLICATE_RULE", "AGENTS_POTENTIAL_CONFLICT"])),
53
+ // One item per documentation scope; the human confirms which scopes to
54
+ // tidy this round via --pick-docs.
55
+ ...resolveDocScopes(root, config)
56
+ .filter((s) => !selectedScopes || selectedScopes.includes(s.key))
57
+ .map((s) => manual(`doc-scope-${s.key}`, DOCS, s.exists
58
+ ? `整理 ${s.title}:re-read, fix drift, bump last_reviewed(${s.files.length} file(s))`
59
+ : `整理 ${s.title}:not present yet(${s.paths.join(" / ")})— create it if this project needs it`, s.exists ? s.files : undefined)),
60
+ manual("doc-indexes", DOCS, "Run `meta sync --indexes` after the docs are tidied"),
61
+ // Code
62
+ manual("code-highrisk", CODE, "Review high-risk domain code against its rules.md", highRisk.length > 0 ? highRisk : undefined),
63
+ manual("code-local-agents", CODE, "Verify local AGENTS.md rules still match the code"),
64
+ manual("code-thirdparty", CODE, "Confirm no third-party or vendored code was modified"),
65
+ // Tests
66
+ manual("test-highrisk", TESTS, "High-risk domains have tests for normal, boundary, retry and idempotency paths", highRisk.length > 0 ? highRisk : undefined),
67
+ manual("test-flaky", TESTS, "Review skipped, flaky and long-quarantined tests"),
68
+ manual("test-conventions", TESTS, "Test suite still follows docs/engineering/testing.md"),
69
+ // Security
70
+ manual("sec-permissions", SECURITY, "Permission and authorization checks match the documented rules"),
71
+ manual("sec-audit-log", SECURITY, "Audit logging still covers all high-risk flows"),
72
+ manual("sec-deps", SECURITY, "Run a dependency audit (e.g. pnpm audit) and triage findings"),
73
+ manual("sec-secrets", SECURITY, "No secrets or credentials committed; .env handling reviewed"),
74
+ manual("sec-callbacks", SECURITY, "Webhook/callback signature validation and deduplication are intact"),
75
+ ];
76
+ }
77
+ export async function runReview(root, opts = {}, pick = (scopes) => checkbox({
78
+ message: "本轮整理哪些文档?(空格勾选,回车确认)",
79
+ choices: scopes.map((s) => ({
80
+ name: s.exists ? `${s.title}(${s.files.length} file(s))` : `${s.title}(未创建)`,
81
+ value: s.key,
82
+ checked: s.exists,
83
+ })),
84
+ })) {
85
+ let selected;
86
+ if (opts.pickDocs) {
87
+ const { config } = loadConfig(root);
88
+ selected = await pick(resolveDocScopes(root, config));
89
+ if (selected.length === 0)
90
+ logger.info("No doc scopes selected — docs items omitted.");
91
+ }
92
+ const items = buildReviewChecklist(root, selected);
93
+ if (opts.json) {
94
+ console.log(JSON.stringify(checklistToJson(items), null, 2));
95
+ return;
96
+ }
97
+ printChecklist("Periodic Repository Review Checklist", items);
98
+ const failed = items.filter((i) => i.status === "fail");
99
+ if (failed.length > 0) {
100
+ console.log(`Run \`meta check\` for details on the ${failed.length} failing item(s).`);
101
+ }
102
+ }
@@ -5,6 +5,7 @@ import { FileWriter } from "../core/filesystem.js";
5
5
  import { logger } from "../core/logger.js";
6
6
  import { loadConfig } from "../core/config.js";
7
7
  import { updateAdrIndex, updateDomainIndex } from "../generators/index-generator.js";
8
+ import { EXIT_CODES, MetaError } from "../core/errors.js";
8
9
  export function runSync(root, opts) {
9
10
  const { config } = loadConfig(root);
10
11
  const all = !opts.indexes && !opts.openspec && !opts.templates;
@@ -35,8 +36,16 @@ export function runSync(root, opts) {
35
36
  execSync("openspec update", { cwd: root, stdio: "inherit" });
36
37
  logger.ok("OpenSpec updated");
37
38
  }
38
- catch {
39
- logger.warn("Could not run `openspec update` (is the OpenSpec CLI installed?). Skipped.");
39
+ catch (e) {
40
+ const err = e;
41
+ // ENOENT / exit 127: the binary is missing. Anything else is a real
42
+ // failure of an installed CLI and must fail the sync.
43
+ if (err.code === "ENOENT" || err.status === 127) {
44
+ logger.warn("Could not run `openspec update` (is the OpenSpec CLI installed?). Skipped.");
45
+ }
46
+ else {
47
+ throw new MetaError(`\`openspec update\` failed${typeof err.status === "number" ? ` with exit code ${err.status}` : ""}. Fix the OpenSpec errors above and re-run \`meta sync\`.`, EXIT_CODES.CHECK_FAILED);
48
+ }
40
49
  }
41
50
  }
42
51
  }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * A checklist mixes two kinds of items: "auto" items are evaluated by the
3
+ * CLI against the repository (pass/fail), "manual" items require a human
4
+ * and stay pending until confirmed.
5
+ */
6
+ export interface ChecklistItem {
7
+ id: string;
8
+ section: string;
9
+ text: string;
10
+ kind: "auto" | "manual";
11
+ status: "pass" | "fail" | "pending" | "confirmed" | "declined";
12
+ /** Supporting evidence, e.g. offending files for a failed auto item. */
13
+ details?: string[];
14
+ }
15
+ export declare function printChecklist(title: string, items: ChecklistItem[]): void;
16
+ export declare function checklistToJson(items: ChecklistItem[]): {
17
+ items: {
18
+ id: string;
19
+ section: string;
20
+ text: string;
21
+ kind: "auto" | "manual";
22
+ status: "fail" | "pass" | "pending" | "confirmed" | "declined";
23
+ details: string[];
24
+ }[];
25
+ summary: {
26
+ total: number;
27
+ auto_passed: number;
28
+ auto_failed: number;
29
+ pending: number;
30
+ };
31
+ };
@@ -0,0 +1,43 @@
1
+ import pc from "picocolors";
2
+ const MAX_DETAILS = 10;
3
+ export function printChecklist(title, items) {
4
+ console.log(pc.bold(title));
5
+ const sections = [...new Set(items.map((i) => i.section))];
6
+ for (const section of sections) {
7
+ console.log();
8
+ console.log(pc.bold(section));
9
+ for (const item of items.filter((i) => i.section === section)) {
10
+ const mark = item.status === "pass" || item.status === "confirmed"
11
+ ? pc.green("✓")
12
+ : item.status === "fail" || item.status === "declined"
13
+ ? pc.red("✗")
14
+ : pc.yellow("☐");
15
+ console.log(` ${mark} ${item.text}`);
16
+ const details = item.details ?? [];
17
+ for (const d of details.slice(0, MAX_DETAILS))
18
+ console.log(` - ${d}`);
19
+ if (details.length > MAX_DETAILS) {
20
+ console.log(` … and ${details.length - MAX_DETAILS} more`);
21
+ }
22
+ }
23
+ }
24
+ console.log();
25
+ }
26
+ export function checklistToJson(items) {
27
+ return {
28
+ items: items.map(({ id, section, text, kind, status, details }) => ({
29
+ id,
30
+ section,
31
+ text,
32
+ kind,
33
+ status,
34
+ details: details ?? [],
35
+ })),
36
+ summary: {
37
+ total: items.length,
38
+ auto_passed: items.filter((i) => i.status === "pass").length,
39
+ auto_failed: items.filter((i) => i.status === "fail").length,
40
+ pending: items.filter((i) => i.status === "pending").length,
41
+ },
42
+ };
43
+ }
@@ -0,0 +1,11 @@
1
+ import type { McpPack } from "./mcp-packs.js";
2
+ /**
3
+ * Deployment packs are skills-only packs (no MCP server, no plugin settings):
4
+ * installing one copies a deploy skill into `.claude/skills/` and appends its
5
+ * working rules to a freshly generated AGENTS.md. They reuse the McpPack
6
+ * shape so `installMcpPack` / `listPackSkillFiles` work unchanged.
7
+ */
8
+ export declare const DEPLOY_PACKS: Record<string, McpPack>;
9
+ /** Valid values for `--with-deploy`; 'none' means skip deployment integration. */
10
+ export declare const DEPLOY_TARGETS: string[];
11
+ export declare function getDeployPack(target: string): McpPack;
@@ -0,0 +1,42 @@
1
+ import { EXIT_CODES, MetaError } from "./errors.js";
2
+ /**
3
+ * Deployment packs are skills-only packs (no MCP server, no plugin settings):
4
+ * installing one copies a deploy skill into `.claude/skills/` and appends its
5
+ * working rules to a freshly generated AGENTS.md. They reuse the McpPack
6
+ * shape so `installMcpPack` / `listPackSkillFiles` work unchanged.
7
+ */
8
+ export const DEPLOY_PACKS = {
9
+ cloudflare: {
10
+ name: "deploy-cloudflare",
11
+ title: "Cloudflare(本地 wrangler CLI)",
12
+ hasSkills: true,
13
+ agentRules: [
14
+ "Deploy via the deploy-cloudflare skill (local wrangler CLI). Run the project's build and tests before any deploy; manage secrets with `wrangler secret put`, never commit them.",
15
+ ],
16
+ },
17
+ ansible: {
18
+ name: "deploy-ansible",
19
+ title: "云主机(Ansible playbook;Windows 下用 Docker 运行 Ansible)",
20
+ hasSkills: true,
21
+ agentRules: [
22
+ "Deploy to cloud hosts via the deploy-ansible skill (ansible-playbook; on Windows run Ansible through Docker). Always dry-run with `--check` first; keep secrets in ansible-vault.",
23
+ ],
24
+ },
25
+ "github-actions": {
26
+ name: "deploy-github-actions",
27
+ title: "GitHub Workflow(GitHub Actions CI/CD)",
28
+ hasSkills: true,
29
+ agentRules: [
30
+ "Deploy via the deploy-github-actions skill (GitHub Actions). The deploy job must depend on passing build and tests; repository secrets belong in GitHub Actions secrets, never in workflow files.",
31
+ ],
32
+ },
33
+ };
34
+ /** Valid values for `--with-deploy`; 'none' means skip deployment integration. */
35
+ export const DEPLOY_TARGETS = [...Object.keys(DEPLOY_PACKS), "none"];
36
+ export function getDeployPack(target) {
37
+ const pack = DEPLOY_PACKS[target];
38
+ if (!pack) {
39
+ throw new MetaError(`Unknown deploy target '${target}'. Valid targets: ${DEPLOY_TARGETS.join(", ")}`, EXIT_CODES.BAD_ARGS);
40
+ }
41
+ return pack;
42
+ }
@@ -0,0 +1,18 @@
1
+ import type { MetaConfig } from "../schemas/config.schema.js";
2
+ /**
3
+ * Named documentation scopes for repository maintenance. The human confirms
4
+ * which scopes to tidy in a given review round (`meta review --pick-docs`).
5
+ */
6
+ export interface DocScope {
7
+ key: string;
8
+ title: string;
9
+ /** Paths relative to the repo root (resolved from config where possible). */
10
+ paths: string[];
11
+ }
12
+ export interface ResolvedDocScope extends DocScope {
13
+ exists: boolean;
14
+ /** Markdown files currently in the scope. */
15
+ files: string[];
16
+ }
17
+ export declare function docScopes(config: MetaConfig): DocScope[];
18
+ export declare function resolveDocScopes(root: string, config: MetaConfig): ResolvedDocScope[];
@@ -0,0 +1,55 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { listDomainDirs } from "../validators/domain-validator.js";
4
+ function listMarkdownFiles(abs) {
5
+ if (!fs.existsSync(abs))
6
+ return [];
7
+ if (fs.statSync(abs).isFile())
8
+ return abs.endsWith(".md") ? [abs] : [];
9
+ const out = [];
10
+ for (const entry of fs.readdirSync(abs, { withFileTypes: true })) {
11
+ const child = path.join(abs, entry.name);
12
+ if (entry.isDirectory())
13
+ out.push(...listMarkdownFiles(child));
14
+ else if (entry.name.endsWith(".md"))
15
+ out.push(child);
16
+ }
17
+ return out;
18
+ }
19
+ export function docScopes(config) {
20
+ const docs = config.paths.docs;
21
+ return [
22
+ {
23
+ key: "product",
24
+ title: "产品文档(PRD / 需求)",
25
+ paths: [path.join(docs, "product"), path.join(docs, "prd.md")],
26
+ },
27
+ { key: "architecture", title: "技术架构文档", paths: [path.join(docs, "architecture")] },
28
+ { key: "api", title: "接口文档(API)", paths: [path.join(docs, "api")] },
29
+ { key: "domain", title: "领域文档(业务规则)", paths: [config.paths.domain_docs] },
30
+ { key: "adr", title: "架构决策记录(ADR)", paths: [config.paths.adr_docs] },
31
+ {
32
+ key: "engineering",
33
+ title: "工程规范(测试 / 硬限制)",
34
+ paths: [path.join(docs, "engineering")],
35
+ },
36
+ { key: "runbooks", title: "运维手册(Runbooks)", paths: [path.join(docs, "runbooks")] },
37
+ { key: "agents", title: "Agent 规则(AGENTS.md)", paths: [config.paths.root_agents] },
38
+ { key: "openspec", title: "OpenSpec specs", paths: [path.join(config.paths.openspec, "specs")] },
39
+ ];
40
+ }
41
+ export function resolveDocScopes(root, config) {
42
+ return docScopes(config).map((scope) => {
43
+ const files = scope.paths.flatMap((rel) => listMarkdownFiles(path.join(root, rel)).map((abs) => path.relative(root, abs)));
44
+ // Local AGENTS.md files belong to the agents scope.
45
+ if (scope.key === "agents") {
46
+ for (const domain of listDomainDirs(root, config.paths.domain_docs)) {
47
+ const rel = path.join(config.paths.domain_docs, domain, "AGENTS.md");
48
+ if (fs.existsSync(path.join(root, rel)))
49
+ files.push(rel);
50
+ }
51
+ }
52
+ const exists = scope.paths.some((rel) => fs.existsSync(path.join(root, rel)));
53
+ return { ...scope, exists, files: files.sort() };
54
+ });
55
+ }
@@ -1,5 +1,6 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
+ import { EXIT_CODES, MetaError } from "./errors.js";
3
4
  /**
4
5
  * All file writes go through this class so that --dry-run can report the
5
6
  * exact set of files that would be created or updated without touching disk.
@@ -16,7 +17,14 @@ export class FileWriter {
16
17
  return path.isAbsolute(p) ? path.relative(this.root, p) : p;
17
18
  }
18
19
  abs(p) {
19
- return path.isAbsolute(p) ? p : path.join(this.root, p);
20
+ const abs = path.resolve(this.root, p);
21
+ const root = path.resolve(this.root);
22
+ // Paths come from user config (paths.*) and CLI flags; never let a
23
+ // `../`-style value escape the project root.
24
+ if (abs !== root && !abs.startsWith(root + path.sep)) {
25
+ throw new MetaError(`Path '${p}' resolves outside the project root. Check paths.* in agent-meta.config.yaml.`, EXIT_CODES.BAD_ARGS);
26
+ }
27
+ return abs;
20
28
  }
21
29
  exists(p) {
22
30
  return fs.existsSync(this.abs(p));
@@ -1,3 +1,5 @@
1
+ /** Heading names (contains-match, case-insensitive) that declare hard constraints. */
2
+ export declare const HARD_CONSTRAINT_HEADINGS: string[];
1
3
  export interface ParsedDoc {
2
4
  file: string;
3
5
  data: Record<string, unknown>;
@@ -10,6 +12,11 @@ export declare function parseDocFile(absPath: string): ParsedDoc;
10
12
  export declare function extractHeadings(content: string): string[];
11
13
  /** True if any heading contains one of the given texts (case-insensitive). */
12
14
  export declare function hasSection(content: string, names: string[]): boolean;
15
+ /**
16
+ * Body text of the first section whose heading matches one of the given
17
+ * names (HTML comments stripped, trimmed). Null when no heading matches.
18
+ */
19
+ export declare function extractSection(content: string, names: string[]): string | null;
13
20
  /**
14
21
  * True if there is any non-empty, non-comment body text under a matching
15
22
  * heading. Sub-headings (deeper levels) belong to the section, so their body
@@ -1,5 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import matter from "gray-matter";
3
+ /** Heading names (contains-match, case-insensitive) that declare hard constraints. */
4
+ export const HARD_CONSTRAINT_HEADINGS = ["硬限制", "hard constraint"];
3
5
  export function parseDocFile(absPath) {
4
6
  const raw = fs.readFileSync(absPath, "utf8");
5
7
  const hasFrontmatter = /^---\r?\n/.test(raw);
@@ -45,6 +47,39 @@ export function hasSection(content, names) {
45
47
  const headings = extractHeadings(content).map((h) => h.toLowerCase());
46
48
  return names.some((n) => headings.some((h) => h.includes(n.toLowerCase())));
47
49
  }
50
+ /**
51
+ * Body text of the first section whose heading matches one of the given
52
+ * names (HTML comments stripped, trimmed). Null when no heading matches.
53
+ */
54
+ export function extractSection(content, names) {
55
+ const lines = content.split(/\r?\n/);
56
+ let capturing = false;
57
+ let sectionLevel = 0;
58
+ let inFence = false;
59
+ let found = false;
60
+ const out = [];
61
+ for (const line of lines) {
62
+ if (/^\s*(```|~~~)/.test(line))
63
+ inFence = !inFence;
64
+ const m = !inFence ? /^(#{1,6})\s+(.+?)\s*$/.exec(line) : null;
65
+ if (m) {
66
+ const level = m[1].length;
67
+ if (!found && names.some((n) => m[2].toLowerCase().includes(n.toLowerCase()))) {
68
+ capturing = true;
69
+ found = true;
70
+ sectionLevel = level;
71
+ continue;
72
+ }
73
+ if (capturing && level <= sectionLevel)
74
+ capturing = false;
75
+ }
76
+ if (capturing)
77
+ out.push(line);
78
+ }
79
+ if (!found)
80
+ return null;
81
+ return out.join("\n").replace(/<!--[\s\S]*?-->/g, "").trim();
82
+ }
48
83
  /**
49
84
  * True if there is any non-empty, non-comment body text under a matching
50
85
  * heading. Sub-headings (deeper levels) belong to the section, so their body
@@ -56,6 +91,7 @@ export function sectionHasContent(content, names) {
56
91
  let capturing = false;
57
92
  let sectionLevel = 0;
58
93
  let inFence = false;
94
+ const captured = [];
59
95
  for (const line of lines) {
60
96
  if (/^\s*(```|~~~)/.test(line))
61
97
  inFence = !inFence;
@@ -71,14 +107,14 @@ export function sectionHasContent(content, names) {
71
107
  }
72
108
  continue;
73
109
  }
74
- if (!capturing)
75
- continue;
76
- const text = line
77
- .replace(/<!--[\s\S]*?-->/g, "")
78
- .replace(/^[-*+]\s*/, "")
79
- .trim();
80
- if (text.length > 0 && !/^<!--/.test(line.trim()))
81
- return true;
110
+ if (capturing)
111
+ captured.push(line);
82
112
  }
83
- return false;
113
+ // Strip comments across line boundaries (like extractSection does), so a
114
+ // comment spanning multiple lines never counts as content.
115
+ return captured
116
+ .join("\n")
117
+ .replace(/<!--[\s\S]*?-->/g, "")
118
+ .split(/\r?\n/)
119
+ .some((line) => line.replace(/^[-*+]\s*/, "").trim().length > 0);
84
120
  }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * An MCP pack bundles a remote MCP server endpoint with the project-level
3
+ * skills that teach agents how to use it. Installing a pack writes the
4
+ * server into `.mcp.json` and copies the skills into `.claude/skills/`,
5
+ * keeping everything repository-scoped (no user-level/global tooling).
6
+ */
7
+ export interface McpPack {
8
+ name: string;
9
+ title: string;
10
+ /** Entry written verbatim into `.mcp.json` under mcpServers[name]. */
11
+ server?: Record<string, unknown>;
12
+ /** Whether the pack bundles skills under assets/skills/<name>/. */
13
+ hasSkills: boolean;
14
+ /** Merged into `.claude/settings.json` (existing values always win). */
15
+ settings?: Record<string, unknown>;
16
+ /** Rules appended to the root AGENTS.md Working Rules when installed via init. */
17
+ agentRules?: string[];
18
+ }
19
+ export declare const MCP_PACKS: Record<string, McpPack>;
20
+ /** Human-readable one-liner of what the pack installs. */
21
+ export declare function packServerSummary(pack: McpPack): string;
22
+ export declare function getMcpPack(name: string): McpPack;
23
+ /** Bundled skills live under <package root>/assets/skills/<pack>/. */
24
+ export declare function packSkillsDir(pack: McpPack): string;
25
+ /** Skill files of a pack as [relative path under .claude/skills/, absolute source path]. */
26
+ export declare function listPackSkillFiles(pack: McpPack): [string, string][];
@@ -0,0 +1,86 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { EXIT_CODES, MetaError } from "./errors.js";
5
+ export const MCP_PACKS = {
6
+ ironbank: {
7
+ name: "ironbank",
8
+ title: "IronBank MCP (hillinsight)",
9
+ server: { type: "http", url: "https://test-ib-mcp.hillinsight.tech/mcp" },
10
+ hasSkills: true,
11
+ agentRules: [
12
+ "Project docs and bug reports live in IronBank and are referenced by `hillinsight://ib_note?...` deep links. When you encounter one, resolve it with the IronBank MCP (see the ironbank-note-links skill) instead of skipping it; when citing IronBank notes in docs or replies, emit links in the same format.",
13
+ ],
14
+ },
15
+ playwright: {
16
+ name: "playwright",
17
+ title: "Playwright MCP (browser automation, microsoft/playwright-mcp)",
18
+ server: { command: "npx", args: ["@playwright/mcp@latest"] },
19
+ hasSkills: false,
20
+ },
21
+ superpowers: {
22
+ name: "superpowers",
23
+ title: "Superpowers (obra/superpowers, Claude Code plugin: planning/TDD/debugging skills)",
24
+ hasSkills: false,
25
+ settings: {
26
+ extraKnownMarketplaces: {
27
+ "superpowers-marketplace": {
28
+ source: { source: "github", repo: "obra/superpowers-marketplace" },
29
+ },
30
+ },
31
+ enabledPlugins: {
32
+ "superpowers@superpowers-marketplace": true,
33
+ },
34
+ },
35
+ agentRules: [
36
+ "Do not use the Superpowers brainstorming skill during development unless the human explicitly asks for it.",
37
+ ],
38
+ },
39
+ };
40
+ /** Human-readable one-liner of what the pack installs. */
41
+ export function packServerSummary(pack) {
42
+ if (pack.server) {
43
+ const { url, command, args } = pack.server;
44
+ if (url)
45
+ return url;
46
+ return [command, ...(args ?? [])].filter(Boolean).join(" ");
47
+ }
48
+ if (pack.settings)
49
+ return "Claude Code plugin (.claude/settings.json)";
50
+ return "skills only";
51
+ }
52
+ export function getMcpPack(name) {
53
+ const pack = MCP_PACKS[name];
54
+ if (!pack) {
55
+ throw new MetaError(`Unknown MCP pack '${name}'. Available packs: ${Object.keys(MCP_PACKS).join(", ")}`, EXIT_CODES.BAD_ARGS);
56
+ }
57
+ return pack;
58
+ }
59
+ /** Bundled skills live under <package root>/assets/skills/<pack>/. */
60
+ export function packSkillsDir(pack) {
61
+ return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../assets/skills", pack.name);
62
+ }
63
+ /** Skill files of a pack as [relative path under .claude/skills/, absolute source path]. */
64
+ export function listPackSkillFiles(pack) {
65
+ if (!pack.hasSkills)
66
+ return [];
67
+ const dir = packSkillsDir(pack);
68
+ if (!fs.existsSync(dir)) {
69
+ throw new MetaError(`Bundled skills for MCP pack '${pack.name}' not found at ${dir}. The CLI installation may be corrupted.`, EXIT_CODES.TEMPLATE_ERROR);
70
+ }
71
+ const out = [];
72
+ const walk = (abs, rel) => {
73
+ for (const entry of fs.readdirSync(abs, { withFileTypes: true })) {
74
+ if (entry.name === ".DS_Store")
75
+ continue;
76
+ const childAbs = path.join(abs, entry.name);
77
+ const childRel = rel ? path.join(rel, entry.name) : entry.name;
78
+ if (entry.isDirectory())
79
+ walk(childAbs, childRel);
80
+ else
81
+ out.push([childRel, childAbs]);
82
+ }
83
+ };
84
+ walk(dir, "");
85
+ return out.sort((a, b) => a[0].localeCompare(b[0]));
86
+ }
@@ -1,6 +1,8 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
- export const ADR_FILE_RE = /^(\d{4,})-(.+)\.md$/;
3
+ // Any digit count, so files generated under a custom defaults.adr_padding
4
+ // (schema minimum: 1) are recognized by numbering, indexing and validation.
5
+ export const ADR_FILE_RE = /^(\d+)-(.+)\.md$/;
4
6
  export function listAdrFiles(adrDirAbs) {
5
7
  if (!fs.existsSync(adrDirAbs))
6
8
  return [];
@@ -0,0 +1,14 @@
1
+ /** The `meta check` invocation appropriate for the project's package manager. */
2
+ export declare function metaCheckCommand(root: string): string;
3
+ export interface LefthookResult {
4
+ action: "created" | "updated" | "unchanged";
5
+ /** Path relative to root. */
6
+ path: string;
7
+ command: string;
8
+ }
9
+ /**
10
+ * Create lefthook.yml with a pre-commit meta-check command, or add the
11
+ * command to an existing file. An existing meta-check entry is never
12
+ * overwritten.
13
+ */
14
+ export declare function generateLefthookConfig(root: string, dryRun: boolean): LefthookResult;