bigpowers 2.60.0 → 2.61.1

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 (57) hide show
  1. package/.mcp.json +9 -0
  2. package/.pi/package.json +1 -1
  3. package/CHANGELOG.md +14 -0
  4. package/bigpowers-mcp/build/config.d.ts +7 -0
  5. package/bigpowers-mcp/build/config.js +35 -0
  6. package/bigpowers-mcp/build/config.js.map +1 -0
  7. package/bigpowers-mcp/build/index.d.ts +2 -0
  8. package/bigpowers-mcp/build/index.js +20 -0
  9. package/bigpowers-mcp/build/index.js.map +1 -0
  10. package/bigpowers-mcp/build/lib/git-context.d.ts +12 -0
  11. package/bigpowers-mcp/build/lib/git-context.js +92 -0
  12. package/bigpowers-mcp/build/lib/git-context.js.map +1 -0
  13. package/bigpowers-mcp/build/lib/graph/builder.d.ts +4 -0
  14. package/bigpowers-mcp/build/lib/graph/builder.js +85 -0
  15. package/bigpowers-mcp/build/lib/graph/builder.js.map +1 -0
  16. package/bigpowers-mcp/build/lib/graph/store.d.ts +11 -0
  17. package/bigpowers-mcp/build/lib/graph/store.js +77 -0
  18. package/bigpowers-mcp/build/lib/graph/store.js.map +1 -0
  19. package/bigpowers-mcp/build/lib/graph/types.d.ts +24 -0
  20. package/bigpowers-mcp/build/lib/graph/types.js +2 -0
  21. package/bigpowers-mcp/build/lib/graph/types.js.map +1 -0
  22. package/bigpowers-mcp/build/lib/paths.d.ts +8 -0
  23. package/bigpowers-mcp/build/lib/paths.js +40 -0
  24. package/bigpowers-mcp/build/lib/paths.js.map +1 -0
  25. package/bigpowers-mcp/build/lib/phase-map.d.ts +3 -0
  26. package/bigpowers-mcp/build/lib/phase-map.js +77 -0
  27. package/bigpowers-mcp/build/lib/phase-map.js.map +1 -0
  28. package/bigpowers-mcp/build/lib/skill-parser.d.ts +31 -0
  29. package/bigpowers-mcp/build/lib/skill-parser.js +159 -0
  30. package/bigpowers-mcp/build/lib/skill-parser.js.map +1 -0
  31. package/bigpowers-mcp/build/lib/validate-skill.d.ts +13 -0
  32. package/bigpowers-mcp/build/lib/validate-skill.js +63 -0
  33. package/bigpowers-mcp/build/lib/validate-skill.js.map +1 -0
  34. package/bigpowers-mcp/build/tools/index.d.ts +9 -0
  35. package/bigpowers-mcp/build/tools/index.js +172 -0
  36. package/bigpowers-mcp/build/tools/index.js.map +1 -0
  37. package/bigpowers-mcp/package-lock.json +3685 -0
  38. package/bigpowers-mcp/package.json +30 -0
  39. package/bigpowers-mcp/src/config.ts +44 -0
  40. package/bigpowers-mcp/src/index.ts +23 -0
  41. package/bigpowers-mcp/src/lib/git-context.ts +99 -0
  42. package/bigpowers-mcp/src/lib/graph/builder.ts +95 -0
  43. package/bigpowers-mcp/src/lib/graph/store.ts +87 -0
  44. package/bigpowers-mcp/src/lib/graph/types.ts +28 -0
  45. package/bigpowers-mcp/src/lib/paths.ts +47 -0
  46. package/bigpowers-mcp/src/lib/phase-map.ts +77 -0
  47. package/bigpowers-mcp/src/lib/skill-parser.ts +191 -0
  48. package/bigpowers-mcp/src/lib/validate-skill.ts +91 -0
  49. package/bigpowers-mcp/src/tools/index.ts +227 -0
  50. package/bigpowers-mcp/test/catalog.test.ts +148 -0
  51. package/bigpowers-mcp/test/fixtures/graph/test-graph.jsonl +5 -0
  52. package/bigpowers-mcp/test/fixtures/skills/broken-link/SKILL.md +11 -0
  53. package/bigpowers-mcp/test/fixtures/skills/missing-verify/SKILL.md +9 -0
  54. package/bigpowers-mcp/test/fixtures/skills/valid-skill/SKILL.md +14 -0
  55. package/bigpowers-mcp/tsconfig.json +16 -0
  56. package/bigpowers-mcp/vitest.config.ts +8 -0
  57. package/package.json +1 -1
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "bigpowers-mcp",
3
+ "version": "0.1.0",
4
+ "description": "MCP semantic context server for bigpowers skills catalog",
5
+ "type": "module",
6
+ "main": "build/index.js",
7
+ "scripts": {
8
+ "build": "tsc",
9
+ "start": "node build/index.js",
10
+ "test": "vitest run",
11
+ "test:watch": "vitest"
12
+ },
13
+ "dependencies": {
14
+ "@modelcontextprotocol/sdk": "^1.29.0",
15
+ "js-yaml": "^4.1.0",
16
+ "remark-frontmatter": "^5.0.0",
17
+ "remark-parse": "^11.0.0",
18
+ "unified": "^11.0.5",
19
+ "zod": "^3.24.2"
20
+ },
21
+ "devDependencies": {
22
+ "@types/js-yaml": "^4.0.9",
23
+ "@types/node": "^22.13.10",
24
+ "typescript": "^5.8.2",
25
+ "vitest": "^3.0.9"
26
+ },
27
+ "engines": {
28
+ "node": ">=20"
29
+ }
30
+ }
@@ -0,0 +1,44 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ const packageDir = path.dirname(fileURLToPath(import.meta.url));
6
+
7
+ /** Repo root: cwd when launched from workspace, else parent of bigpowers-mcp. */
8
+ export function getRepoRoot(): string {
9
+ const fromEnv = process.env.BIGPOWERS_ROOT;
10
+ if (fromEnv) return path.resolve(fromEnv);
11
+
12
+ const cwd = process.cwd();
13
+ if (isRepoRoot(cwd)) return cwd;
14
+
15
+ const parentOfPackage = path.resolve(packageDir, "../..");
16
+ if (isRepoRoot(parentOfPackage)) return parentOfPackage;
17
+
18
+ return cwd;
19
+ }
20
+
21
+ function isRepoRoot(dir: string): boolean {
22
+ return (
23
+ fs.existsSync(path.join(dir, "skills")) && fs.existsSync(path.join(dir, "specs"))
24
+ );
25
+ }
26
+
27
+ export function getSkillsDir(repoRoot: string): string {
28
+ return path.join(repoRoot, "skills");
29
+ }
30
+
31
+ export function getGraphPath(repoRoot: string): string {
32
+ return path.join(repoRoot, "bigpowers-mcp", "graph.jsonl");
33
+ }
34
+
35
+ export const MAX_READ_SKILL_BYTES = 512 * 1024;
36
+
37
+ export const SECRET_DENYLIST = [
38
+ /\.env/i,
39
+ /\.pem$/i,
40
+ /secret/i,
41
+ /credentials/i,
42
+ ];
43
+
44
+ export const GIT_SCOPE_DIRS = ["skills", "specs"];
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { createContext, registerTools } from "./tools/index.js";
5
+
6
+ async function main(): Promise<void> {
7
+ const ctx = createContext();
8
+ const server = new McpServer({
9
+ name: "bigpowers-mcp",
10
+ version: "0.1.0",
11
+ });
12
+
13
+ registerTools(server, ctx);
14
+
15
+ const transport = new StdioServerTransport();
16
+ await server.connect(transport);
17
+ console.error("bigpowers-mcp started");
18
+ }
19
+
20
+ main().catch((err) => {
21
+ console.error("Fatal error:", err);
22
+ process.exit(1);
23
+ });
@@ -0,0 +1,99 @@
1
+ // story: e32s04
2
+ import { execFileSync } from "node:child_process";
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import { GIT_SCOPE_DIRS, SECRET_DENYLIST } from "../config.js";
6
+ import { isSecretPath } from "./paths.js";
7
+
8
+ export interface GitContextResult {
9
+ action: string;
10
+ changed_files: Record<string, string[]>;
11
+ commits?: Array<{ hash: string; subject: string; files: string[] }>;
12
+ diffs?: Record<string, string>;
13
+ }
14
+
15
+ function inScope(filePath: string): boolean {
16
+ const norm = filePath.replace(/\\/g, "/");
17
+ if (isSecretPath(norm)) return false;
18
+ return GIT_SCOPE_DIRS.some((d) => norm === d || norm.startsWith(`${d}/`));
19
+ }
20
+
21
+ function groupByDir(files: string[]): Record<string, string[]> {
22
+ const grouped: Record<string, string[]> = {};
23
+ for (const f of files) {
24
+ const parts = f.split("/");
25
+ const dir = parts.length > 1 ? parts[0] : ".";
26
+ if (!grouped[dir]) grouped[dir] = [];
27
+ grouped[dir].push(f);
28
+ }
29
+ return grouped;
30
+ }
31
+
32
+ function git(repoRoot: string, args: string[]): string {
33
+ return execFileSync("git", args, {
34
+ cwd: repoRoot,
35
+ encoding: "utf8",
36
+ maxBuffer: 10 * 1024 * 1024,
37
+ }).trim();
38
+ }
39
+
40
+ export function getGitContext(repoRoot: string, action: "status" | "log" | "diff"): GitContextResult {
41
+ if (action === "status") {
42
+ const out = git(repoRoot, ["status", "--porcelain"]);
43
+ const files = out
44
+ .split("\n")
45
+ .filter(Boolean)
46
+ .map((line) => line.slice(3).trim())
47
+ .filter(inScope);
48
+ return { action, changed_files: groupByDir(files) };
49
+ }
50
+
51
+ if (action === "log") {
52
+ const out = git(repoRoot, [
53
+ "log",
54
+ "-10",
55
+ "--pretty=format:%H|%s",
56
+ "--name-only",
57
+ "--",
58
+ ...GIT_SCOPE_DIRS,
59
+ ]);
60
+ const commits: GitContextResult["commits"] = [];
61
+ let current: { hash: string; subject: string; files: string[] } | null = null;
62
+ for (const line of out.split("\n")) {
63
+ if (line.includes("|")) {
64
+ if (current) commits.push(current);
65
+ const [hash, subject] = line.split("|");
66
+ current = { hash, subject, files: [] };
67
+ } else if (line.trim() && current && inScope(line.trim())) {
68
+ current.files.push(line.trim());
69
+ }
70
+ }
71
+ if (current) commits.push(current);
72
+ return { action, changed_files: {}, commits };
73
+ }
74
+
75
+ const diffOut = git(repoRoot, ["diff", "--", ...GIT_SCOPE_DIRS]);
76
+ const diffs: Record<string, string> = {};
77
+ let currentFile = "";
78
+ let buf: string[] = [];
79
+ for (const line of diffOut.split("\n")) {
80
+ if (line.startsWith("diff --git")) {
81
+ if (currentFile && buf.length) diffs[currentFile] = buf.join("\n");
82
+ const match = / b\/(.+)$/.exec(line);
83
+ currentFile = match?.[1] ?? "";
84
+ buf = [line];
85
+ } else {
86
+ buf.push(line);
87
+ }
88
+ }
89
+ if (currentFile && buf.length && inScope(currentFile) && !isSecretPath(currentFile)) {
90
+ diffs[currentFile] = buf.join("\n");
91
+ }
92
+
93
+ const changed = Object.keys(diffs);
94
+ return { action, changed_files: groupByDir(changed), diffs };
95
+ }
96
+
97
+ export function isGitRepo(repoRoot: string): boolean {
98
+ return fs.existsSync(path.join(repoRoot, ".git"));
99
+ }
@@ -0,0 +1,95 @@
1
+ import type { ParsedSkill } from "../skill-parser.js";
2
+ import type { GraphEntity, GraphRelation, SkillGraph } from "./types.js";
3
+
4
+ const HANDOFF_AFTER = /run\s+(\S+)\s+after\s+(\S+)/gi;
5
+ const HARD_GATE = /HARD GATE:\s*(.+)/i;
6
+ const SKILL_REF = /see\s+skills\/(\S+)\/SKILL\.md/gi;
7
+ const CONVENTIONS_REF = /CONVENTIONS\.md(?:\s*[§#]\s*(\S+))?/gi;
8
+
9
+ export function buildGraphFromSkills(parsedSkills: ParsedSkill[]): SkillGraph {
10
+ const graph: SkillGraph = { entities: new Map(), relations: [] };
11
+
12
+ for (const skill of parsedSkills) {
13
+ const fm = skill.frontmatter;
14
+ const observations: string[] = [];
15
+ if (fm.model) observations.push(`model: ${String(fm.model)}`);
16
+ if (fm.effort) observations.push(`effort: ${String(fm.effort)}`);
17
+ if (fm.description) observations.push(`description: ${String(fm.description)}`);
18
+
19
+ const entity: GraphEntity = {
20
+ type: "entity",
21
+ name: skill.name,
22
+ entityType: "Skill",
23
+ observations,
24
+ };
25
+ graph.entities.set(skill.name, entity);
26
+
27
+ const prose = skill.rawProse + "\n" + skill.sections.map((s) => s.prose).join("\n");
28
+
29
+ let m: RegExpExecArray | null;
30
+ HANDOFF_AFTER.lastIndex = 0;
31
+ while ((m = HANDOFF_AFTER.exec(prose)) !== null) {
32
+ graph.relations.push({
33
+ type: "relation",
34
+ from: m[1],
35
+ to: m[2],
36
+ relationType: "depends_on",
37
+ });
38
+ }
39
+
40
+ const gateMatch = HARD_GATE.exec(prose);
41
+ if (gateMatch) {
42
+ const gateText = gateMatch[1].trim();
43
+ const targetMatch = /(\S+-?\S+)/.exec(gateText);
44
+ if (targetMatch) {
45
+ graph.relations.push({
46
+ type: "relation",
47
+ from: skill.name,
48
+ to: targetMatch[1].replace(/[`'"]/g, ""),
49
+ relationType: "gates",
50
+ });
51
+ }
52
+ }
53
+
54
+ SKILL_REF.lastIndex = 0;
55
+ while ((m = SKILL_REF.exec(prose)) !== null) {
56
+ graph.relations.push({
57
+ type: "relation",
58
+ from: skill.name,
59
+ to: m[1],
60
+ relationType: "references",
61
+ });
62
+ }
63
+
64
+ CONVENTIONS_REF.lastIndex = 0;
65
+ while ((m = CONVENTIONS_REF.exec(prose)) !== null) {
66
+ graph.relations.push({
67
+ type: "relation",
68
+ from: skill.name,
69
+ to: m[1] ? `CONVENTIONS.md §${m[1]}` : "CONVENTIONS.md",
70
+ relationType: "enforces",
71
+ });
72
+ }
73
+
74
+ const desc = String(fm.description ?? "");
75
+ const handoffMatch = /handoff.*?(\S+-?\S+)/i.exec(desc);
76
+ if (handoffMatch) {
77
+ graph.relations.push({
78
+ type: "relation",
79
+ from: skill.name,
80
+ to: handoffMatch[1],
81
+ relationType: "handoff_to",
82
+ });
83
+ }
84
+ }
85
+
86
+ return graph;
87
+ }
88
+
89
+ export function mergeGraphs(base: SkillGraph, incoming: SkillGraph): SkillGraph {
90
+ for (const [name, entity] of incoming.entities) {
91
+ base.entities.set(name, entity);
92
+ }
93
+ base.relations.push(...incoming.relations);
94
+ return base;
95
+ }
@@ -0,0 +1,87 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import type { GraphEntity, GraphLine, GraphRelation, SkillGraph } from "./types.js";
4
+
5
+ export function emptyGraph(): SkillGraph {
6
+ return { entities: new Map(), relations: [] };
7
+ }
8
+
9
+ export function loadGraph(graphPath: string): SkillGraph {
10
+ const graph = emptyGraph();
11
+ if (!fs.existsSync(graphPath)) return graph;
12
+
13
+ const lines = fs.readFileSync(graphPath, "utf8").split("\n").filter(Boolean);
14
+ for (const line of lines) {
15
+ const parsed = JSON.parse(line) as GraphLine;
16
+ if (parsed.type === "entity") {
17
+ graph.entities.set(parsed.name, parsed);
18
+ } else if (parsed.type === "relation") {
19
+ graph.relations.push(parsed);
20
+ }
21
+ }
22
+ return graph;
23
+ }
24
+
25
+ export function saveGraph(graphPath: string, graph: SkillGraph): void {
26
+ fs.mkdirSync(path.dirname(graphPath), { recursive: true });
27
+ const lines: string[] = [];
28
+ for (const entity of graph.entities.values()) {
29
+ lines.push(JSON.stringify(entity));
30
+ }
31
+ for (const rel of graph.relations) {
32
+ lines.push(JSON.stringify(rel));
33
+ }
34
+ fs.writeFileSync(graphPath, lines.join("\n") + (lines.length ? "\n" : ""));
35
+ }
36
+
37
+ export function searchNodes(graph: SkillGraph, query: string): GraphEntity[] {
38
+ const q = query.toLowerCase();
39
+ return [...graph.entities.values()].filter((e) => {
40
+ const hay = [e.name, e.entityType, ...e.observations].join(" ").toLowerCase();
41
+ return hay.includes(q);
42
+ });
43
+ }
44
+
45
+ export function openNodes(graph: SkillGraph, names: string[]): GraphEntity[] {
46
+ return names
47
+ .map((n) => graph.entities.get(n))
48
+ .filter((e): e is GraphEntity => e !== undefined);
49
+ }
50
+
51
+ export function getRelationsFor(graph: SkillGraph, skillName: string): GraphRelation[] {
52
+ return graph.relations.filter((r) => r.from === skillName || r.to === skillName);
53
+ }
54
+
55
+ export function getForwardDeps(graph: SkillGraph, skillName: string): string[] {
56
+ return graph.relations
57
+ .filter((r) => r.from === skillName && r.relationType === "depends_on")
58
+ .map((r) => r.to);
59
+ }
60
+
61
+ export function getReverseDeps(graph: SkillGraph, skillName: string): string[] {
62
+ return graph.relations
63
+ .filter((r) => r.to === skillName && r.relationType === "depends_on")
64
+ .map((r) => r.from);
65
+ }
66
+
67
+ export function getHandoffChain(graph: SkillGraph, skillName: string): string[] {
68
+ const chain: string[] = [skillName];
69
+ let current = skillName;
70
+ const seen = new Set<string>([skillName]);
71
+ for (let i = 0; i < 20; i++) {
72
+ const next = graph.relations.find(
73
+ (r) => r.from === current && r.relationType === "handoff_to",
74
+ );
75
+ if (!next || seen.has(next.to)) break;
76
+ chain.push(next.to);
77
+ seen.add(next.to);
78
+ current = next.to;
79
+ }
80
+ return chain;
81
+ }
82
+
83
+ export function getConventions(graph: SkillGraph, skillName: string): string[] {
84
+ return graph.relations
85
+ .filter((r) => r.from === skillName && r.relationType === "enforces")
86
+ .map((r) => r.to);
87
+ }
@@ -0,0 +1,28 @@
1
+ export interface GraphEntity {
2
+ type: "entity";
3
+ name: string;
4
+ entityType: string;
5
+ observations: string[];
6
+ }
7
+
8
+ export interface GraphRelation {
9
+ type: "relation";
10
+ from: string;
11
+ to: string;
12
+ relationType: string;
13
+ }
14
+
15
+ export type GraphLine = GraphEntity | GraphRelation;
16
+
17
+ export interface SkillGraph {
18
+ entities: Map<string, GraphEntity>;
19
+ relations: GraphRelation[];
20
+ }
21
+
22
+ export interface DependencyReport {
23
+ skill: string;
24
+ depends_on: string[];
25
+ depended_by: string[];
26
+ handoff_chain: string[];
27
+ conventions: string[];
28
+ }
@@ -0,0 +1,47 @@
1
+ import path from "node:path";
2
+
3
+ export class PathGuardError extends Error {
4
+ constructor(message: string) {
5
+ super(message);
6
+ this.name = "PathGuardError";
7
+ }
8
+ }
9
+
10
+ /** Resolve userPath strictly within allowedRoot; reject traversal. */
11
+ export function resolveWithin(allowedRoot: string, userPath: string): string {
12
+ const normalized = userPath.replace(/\\/g, "/").trim();
13
+ if (!normalized || normalized.startsWith("/") || normalized.includes("..")) {
14
+ throw new PathGuardError(`Invalid path: ${userPath}`);
15
+ }
16
+
17
+ const resolved = path.resolve(allowedRoot, normalized);
18
+ const rootWithSep = allowedRoot.endsWith(path.sep)
19
+ ? allowedRoot
20
+ : allowedRoot + path.sep;
21
+
22
+ if (!resolved.startsWith(rootWithSep) && resolved !== allowedRoot) {
23
+ throw new PathGuardError(`Path escapes allowed root: ${userPath}`);
24
+ }
25
+ return resolved;
26
+ }
27
+
28
+ /** Resolve a skill name to skills/<name>/SKILL.md under repo root. */
29
+ export function resolveSkillPath(repoRoot: string, skillName: string): string {
30
+ const name = skillName.trim();
31
+ if (!name || name.includes("/") || name.includes("\\") || name.includes("..")) {
32
+ throw new PathGuardError(`Invalid skill name: ${skillName}`);
33
+ }
34
+ const skillsDir = path.join(repoRoot, "skills");
35
+ const skillDir = resolveWithin(skillsDir, name);
36
+ return path.join(skillDir, "SKILL.md");
37
+ }
38
+
39
+ export function isSecretPath(filePath: string): boolean {
40
+ const base = path.basename(filePath);
41
+ return (
42
+ base.startsWith(".env") ||
43
+ /\.pem$/i.test(base) ||
44
+ /secret/i.test(base) ||
45
+ /credentials/i.test(base)
46
+ );
47
+ }
@@ -0,0 +1,77 @@
1
+ /** Lifecycle phase map — mirrors scripts/mcp-server.js PHASE_MAP. */
2
+ export const PHASE_MAP: Record<string, string> = {
3
+ "survey-context": "Discover",
4
+ "research-first": "Discover",
5
+ "search-skills": "Discover",
6
+ "using-bigpowers": "Discover",
7
+ "map-codebase": "Discover",
8
+ "elaborate-spec": "Discover",
9
+ "model-domain": "Design",
10
+ "define-language": "Design",
11
+ "grill-me": "Design",
12
+ "grill-with-docs": "Design",
13
+ "deepen-architecture": "Design",
14
+ "design-interface": "Design",
15
+ "define-success": "Design",
16
+ "scope-work": "Plan",
17
+ "slice-tasks": "Plan",
18
+ "plan-work": "Plan",
19
+ "plan-release": "Plan",
20
+ "plan-refactor": "Plan",
21
+ "assess-impact": "Plan",
22
+ "change-request": "Plan",
23
+ "run-planning": "Plan",
24
+ "seed-conventions": "Plan",
25
+ "develop-tdd": "Build",
26
+ "kickoff-branch": "Build",
27
+ "execute-plan": "Build",
28
+ "build-epic": "Build",
29
+ "spike-prototype": "Build",
30
+ "craft-skill": "Build",
31
+ "quick-fix": "Build",
32
+ "setup-environment": "Build",
33
+ "wire-observability": "Build",
34
+ "wire-ci": "Build",
35
+ "publish-package": "Build",
36
+ "align-grid": "Build",
37
+ "orchestrate-project": "Build",
38
+ "guard-git": "Build",
39
+ "hook-commits": "Build",
40
+ "deploy": "Build",
41
+ "smoke-test": "Build",
42
+ "validate-contracts": "Build",
43
+ "verify-work": "Verify",
44
+ "validate-fix": "Verify",
45
+ "audit-code": "Verify",
46
+ "enforce-first": "Verify",
47
+ "run-evals": "Verify",
48
+ "investigate-bug": "Verify",
49
+ "diagnose-root": "Verify",
50
+ "fix-bug": "Verify",
51
+ "inspect-quality": "Verify",
52
+ "request-review": "Verify",
53
+ "respond-review": "Verify",
54
+ "trace-requirement": "Verify",
55
+ "release-branch": "Release",
56
+ "commit-message": "Release",
57
+ "session-state": "Sustain",
58
+ "terse-mode": "Sustain",
59
+ "compose-workflow": "Sustain",
60
+ "delegate-task": "Sustain",
61
+ "dispatch-agents": "Sustain",
62
+ "edit-document": "Sustain",
63
+ "evolve-skill": "Sustain",
64
+ "migrate-spec": "Sustain",
65
+ "organize-workspace": "Sustain",
66
+ "reset-baseline": "Sustain",
67
+ "simulate-agents": "Sustain",
68
+ "stocktake-skills": "Sustain",
69
+ "write-document": "Sustain",
70
+ "plan-tests": "Plan",
71
+ "gate-trace": "Verify",
72
+ "security-review": "Verify",
73
+ };
74
+
75
+ export function phaseForSkill(name: string): string {
76
+ return PHASE_MAP[name] ?? "Other";
77
+ }