@gordon.gan/specflow 1.0.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 (178) hide show
  1. package/LICENSE +31 -0
  2. package/README.md +442 -0
  3. package/bin/specflow.js +2 -0
  4. package/dist/cli/commands/change-archive.d.ts +26 -0
  5. package/dist/cli/commands/change-archive.js +50 -0
  6. package/dist/cli/commands/change-new.d.ts +18 -0
  7. package/dist/cli/commands/change-new.js +53 -0
  8. package/dist/cli/commands/change-phase.d.ts +33 -0
  9. package/dist/cli/commands/change-phase.js +66 -0
  10. package/dist/cli/commands/change-status.d.ts +35 -0
  11. package/dist/cli/commands/change-status.js +95 -0
  12. package/dist/cli/commands/doctor.d.ts +9 -0
  13. package/dist/cli/commands/doctor.js +48 -0
  14. package/dist/cli/commands/init.d.ts +16 -0
  15. package/dist/cli/commands/init.js +122 -0
  16. package/dist/cli/commands/instructions.d.ts +21 -0
  17. package/dist/cli/commands/instructions.js +75 -0
  18. package/dist/cli/commands/parity-report.d.ts +10 -0
  19. package/dist/cli/commands/parity-report.js +67 -0
  20. package/dist/cli/commands/sync.d.ts +4 -0
  21. package/dist/cli/commands/sync.js +40 -0
  22. package/dist/cli/commands/validate.d.ts +21 -0
  23. package/dist/cli/commands/validate.js +46 -0
  24. package/dist/cli/index.d.ts +1 -0
  25. package/dist/cli/index.js +52 -0
  26. package/dist/core/archive.d.ts +39 -0
  27. package/dist/core/archive.js +154 -0
  28. package/dist/core/artifact-graph/graph.d.ts +60 -0
  29. package/dist/core/artifact-graph/graph.js +141 -0
  30. package/dist/core/artifact-graph/index.d.ts +10 -0
  31. package/dist/core/artifact-graph/index.js +7 -0
  32. package/dist/core/artifact-graph/instruction-loader.d.ts +45 -0
  33. package/dist/core/artifact-graph/instruction-loader.js +37 -0
  34. package/dist/core/artifact-graph/outputs.d.ts +35 -0
  35. package/dist/core/artifact-graph/outputs.js +48 -0
  36. package/dist/core/artifact-graph/resolver.d.ts +54 -0
  37. package/dist/core/artifact-graph/resolver.js +139 -0
  38. package/dist/core/artifact-graph/schema.d.ts +23 -0
  39. package/dist/core/artifact-graph/schema.js +115 -0
  40. package/dist/core/artifact-graph/state.d.ts +9 -0
  41. package/dist/core/artifact-graph/state.js +93 -0
  42. package/dist/core/artifact-graph/types.d.ts +123 -0
  43. package/dist/core/artifact-graph/types.js +29 -0
  44. package/dist/core/parsers/change-parser.d.ts +20 -0
  45. package/dist/core/parsers/change-parser.js +22 -0
  46. package/dist/core/parsers/index.d.ts +7 -0
  47. package/dist/core/parsers/index.js +4 -0
  48. package/dist/core/parsers/markdown-parser.d.ts +35 -0
  49. package/dist/core/parsers/markdown-parser.js +193 -0
  50. package/dist/core/parsers/requirement-blocks.d.ts +21 -0
  51. package/dist/core/parsers/requirement-blocks.js +57 -0
  52. package/dist/core/parsers/spec-structure.d.ts +22 -0
  53. package/dist/core/parsers/spec-structure.js +105 -0
  54. package/dist/core/specs-apply.d.ts +22 -0
  55. package/dist/core/specs-apply.js +165 -0
  56. package/dist/core/templates/index.d.ts +31 -0
  57. package/dist/core/templates/index.js +80 -0
  58. package/dist/core/templates/types.d.ts +4 -0
  59. package/dist/core/templates/types.js +1 -0
  60. package/dist/core/validation/constants.d.ts +14 -0
  61. package/dist/core/validation/constants.js +14 -0
  62. package/dist/core/validation/types.d.ts +13 -0
  63. package/dist/core/validation/types.js +6 -0
  64. package/dist/core/validation/validator.d.ts +24 -0
  65. package/dist/core/validation/validator.js +250 -0
  66. package/dist/integrations/claude/adapter.d.ts +2 -0
  67. package/dist/integrations/claude/adapter.js +106 -0
  68. package/dist/integrations/codex/adapter.d.ts +2 -0
  69. package/dist/integrations/codex/adapter.js +141 -0
  70. package/dist/integrations/cursor/adapter.d.ts +2 -0
  71. package/dist/integrations/cursor/adapter.js +126 -0
  72. package/dist/integrations/shared/adapter-registry.d.ts +4 -0
  73. package/dist/integrations/shared/adapter-registry.js +27 -0
  74. package/dist/integrations/shared/asset-copy.d.ts +2 -0
  75. package/dist/integrations/shared/asset-copy.js +44 -0
  76. package/dist/integrations/shared/asset-hash.d.ts +4 -0
  77. package/dist/integrations/shared/asset-hash.js +54 -0
  78. package/dist/integrations/shared/capability-evidence.d.ts +1 -0
  79. package/dist/integrations/shared/capability-evidence.js +42 -0
  80. package/dist/integrations/shared/command-catalog.d.ts +6 -0
  81. package/dist/integrations/shared/command-catalog.js +13 -0
  82. package/dist/integrations/shared/managed-assets.d.ts +2 -0
  83. package/dist/integrations/shared/managed-assets.js +21 -0
  84. package/dist/integrations/shared/marker-write.d.ts +3 -0
  85. package/dist/integrations/shared/marker-write.js +48 -0
  86. package/dist/integrations/shared/migration-state.d.ts +2 -0
  87. package/dist/integrations/shared/migration-state.js +67 -0
  88. package/dist/integrations/shared/parity-comparator.d.ts +6 -0
  89. package/dist/integrations/shared/parity-comparator.js +97 -0
  90. package/dist/integrations/shared/parity-manifest.d.ts +13 -0
  91. package/dist/integrations/shared/parity-manifest.js +58 -0
  92. package/dist/integrations/shared/runtime-assets.d.ts +2 -0
  93. package/dist/integrations/shared/runtime-assets.js +21 -0
  94. package/dist/integrations/shared/skill-renderer.d.ts +5 -0
  95. package/dist/integrations/shared/skill-renderer.js +77 -0
  96. package/dist/integrations/shared/types.d.ts +35 -0
  97. package/dist/integrations/shared/types.js +1 -0
  98. package/dist/utils/change-metadata.d.ts +38 -0
  99. package/dist/utils/change-metadata.js +82 -0
  100. package/dist/utils/change-utils.d.ts +53 -0
  101. package/dist/utils/change-utils.js +97 -0
  102. package/dist/utils/file-system.d.ts +35 -0
  103. package/dist/utils/file-system.js +67 -0
  104. package/dist/utils/index.d.ts +7 -0
  105. package/dist/utils/index.js +5 -0
  106. package/dist/utils/project-root.d.ts +22 -0
  107. package/dist/utils/project-root.js +44 -0
  108. package/dist/utils/task-progress.d.ts +39 -0
  109. package/dist/utils/task-progress.js +62 -0
  110. package/package.json +72 -0
  111. package/prompts/build/ecc-go-reviewer.md +80 -0
  112. package/prompts/build/ecc-java-reviewer.md +185 -0
  113. package/prompts/build/ecc-kotlin-reviewer.md +163 -0
  114. package/prompts/build/ecc-python-reviewer.md +102 -0
  115. package/prompts/build/ecc-rust-reviewer.md +98 -0
  116. package/prompts/build/ecc-typescript-reviewer.md +116 -0
  117. package/prompts/build/language-router.md +22 -0
  118. package/prompts/build/phase-a-plan.md +288 -0
  119. package/prompts/build/phase-b-execute.md +276 -0
  120. package/prompts/build/phase-b-review.md +119 -0
  121. package/prompts/build/phase-b-worktree.md +215 -0
  122. package/prompts/build/plan-document-reviewer.md +51 -0
  123. package/prompts/build/tdd.md +184 -0
  124. package/prompts/done/branch-finish.md +204 -0
  125. package/prompts/fix/debug.md +249 -0
  126. package/prompts/plan/design-draft.md +123 -0
  127. package/prompts/plan/proposal.md +85 -0
  128. package/prompts/plan/specs.md +114 -0
  129. package/prompts/plan/tasks-draft.md +123 -0
  130. package/prompts/reference/openspec/apply-instruction.md +2 -0
  131. package/prompts/reference/openspec/design-instruction.md +20 -0
  132. package/prompts/reference/openspec/proposal-instruction.md +18 -0
  133. package/prompts/reference/openspec/specs-instruction.md +47 -0
  134. package/prompts/reference/openspec/tasks-instruction.md +26 -0
  135. package/prompts/reference/specflow/example-design.md +284 -0
  136. package/prompts/reference/superpowers/anthropic-best-practices.md +1150 -0
  137. package/prompts/reference/superpowers/codex-tools.md +29 -0
  138. package/prompts/reference/superpowers/condition-based-waiting-example.ts +158 -0
  139. package/prompts/reference/superpowers/condition-based-waiting.md +115 -0
  140. package/prompts/reference/superpowers/defense-in-depth.md +122 -0
  141. package/prompts/reference/superpowers/find-polluter.sh +63 -0
  142. package/prompts/reference/superpowers/graphviz-conventions.dot +172 -0
  143. package/prompts/reference/superpowers/persuasion-principles.md +187 -0
  144. package/prompts/reference/superpowers/root-cause-tracing.md +169 -0
  145. package/prompts/reference/superpowers/testing-anti-patterns.md +299 -0
  146. package/prompts/reference/superpowers/testing-skills-with-subagents.md +384 -0
  147. package/prompts/reference/superpowers/using-superpowers.md +117 -0
  148. package/prompts/reference/superpowers/writing-skills.md +655 -0
  149. package/prompts/refine/brainstorm.md +241 -0
  150. package/prompts/refine/design-output.md +126 -0
  151. package/prompts/refine/spec-document-reviewer.md +51 -0
  152. package/prompts/refine/update-artifacts.md +185 -0
  153. package/prompts/review/code-review.md +223 -0
  154. package/prompts/shared/code-reviewer-prompt.md +98 -0
  155. package/prompts/shared/dispatching-parallel-agents.md +143 -0
  156. package/prompts/shared/executing-plans.md +67 -0
  157. package/prompts/shared/implementer-prompt.md +115 -0
  158. package/prompts/shared/receiving-code-review.md +174 -0
  159. package/prompts/shared/spec-reviewer-prompt.md +63 -0
  160. package/prompts/test/tdd.md +236 -0
  161. package/prompts/test/verification.md +129 -0
  162. package/prompts/verify/ecc-security-reviewer.md +112 -0
  163. package/prompts/verify/verification.md +196 -0
  164. package/schemas/specflow/schema.yaml +48 -0
  165. package/skills/specflow-build/SKILL.md +129 -0
  166. package/skills/specflow-done/SKILL.md +68 -0
  167. package/skills/specflow-fix/SKILL.md +74 -0
  168. package/skills/specflow-plan/SKILL.md +82 -0
  169. package/skills/specflow-refine/SKILL.md +128 -0
  170. package/skills/specflow-review/SKILL.md +40 -0
  171. package/skills/specflow-scan/SKILL.md +48 -0
  172. package/skills/specflow-snap/SKILL.md +46 -0
  173. package/skills/specflow-test/SKILL.md +48 -0
  174. package/skills/specflow-verify/SKILL.md +77 -0
  175. package/templates/design.md +19 -0
  176. package/templates/proposal.md +23 -0
  177. package/templates/spec.md +42 -0
  178. package/templates/tasks.md +9 -0
@@ -0,0 +1,126 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { COMMAND_CATALOG } from '../shared/command-catalog.js';
4
+ import { directoryExists, fileExists, hashDirectoryTree, hashFile } from '../shared/asset-hash.js';
5
+ import { deriveCapabilityIds } from '../shared/capability-evidence.js';
6
+ import { copyRuntimeAssets } from '../shared/runtime-assets.js';
7
+ import { renderIdeContent } from '../shared/skill-renderer.js';
8
+ function extractSkillDescription(content, fallback) {
9
+ const frontmatterMatch = content.match(/^---\s*\n([\s\S]*?)\n---/);
10
+ if (!frontmatterMatch) {
11
+ return fallback;
12
+ }
13
+ const descMatch = frontmatterMatch[1].match(/description:\s*["']?(.+?)["']?\s*$/m);
14
+ return descMatch ? descMatch[1] : fallback;
15
+ }
16
+ function generateCursorCommandAlias(commandName, description) {
17
+ return [
18
+ '---',
19
+ `description: "${description}"`,
20
+ '---',
21
+ `Invoke the specflow:${commandName} skill.`,
22
+ '',
23
+ ].join('\n');
24
+ }
25
+ function renderCursorRule() {
26
+ return [
27
+ '# SpecFlow Cursor Context',
28
+ '',
29
+ '- Respect specflow phase gates: plan -> refined -> built -> archived',
30
+ '- Never bypass CLI validation and archive checks',
31
+ '- Use artifacts under specflow/changes/<change>/ before coding',
32
+ '',
33
+ ].join('\n');
34
+ }
35
+ async function generateCursor(ctx) {
36
+ const { projectRoot, packageRoot } = ctx;
37
+ const skillsSourceDir = join(packageRoot, 'skills');
38
+ const skillsDestDir = join(projectRoot, '.cursor', 'skills');
39
+ const commandsDestDir = join(projectRoot, '.cursor', 'commands', 'specflow');
40
+ const rulesDir = join(projectRoot, '.cursor', 'rules');
41
+ await fs.mkdir(commandsDestDir, { recursive: true });
42
+ await fs.mkdir(rulesDir, { recursive: true });
43
+ for (const command of COMMAND_CATALOG) {
44
+ const skillDirName = `specflow-${command.id}`;
45
+ const srcSkillMd = join(skillsSourceDir, skillDirName, 'SKILL.md');
46
+ const destSkillDir = join(skillsDestDir, skillDirName);
47
+ await fs.mkdir(destSkillDir, { recursive: true });
48
+ let description = command.description;
49
+ try {
50
+ const content = await fs.readFile(srcSkillMd, 'utf-8');
51
+ await fs.writeFile(join(destSkillDir, 'SKILL.md'), renderIdeContent(content, 'cursor'), 'utf-8');
52
+ description = extractSkillDescription(content, command.description);
53
+ }
54
+ catch {
55
+ const placeholder = [
56
+ '---',
57
+ `name: ${skillDirName}`,
58
+ `description: "${command.description}"`,
59
+ '---',
60
+ '',
61
+ `# SpecFlow: ${command.id}`,
62
+ '',
63
+ 'Skill content pending.',
64
+ '',
65
+ ].join('\n');
66
+ await fs.writeFile(join(destSkillDir, 'SKILL.md'), renderIdeContent(placeholder, 'cursor'), 'utf-8');
67
+ }
68
+ await fs.writeFile(join(commandsDestDir, `${command.id}.md`), generateCursorCommandAlias(command.id, description), 'utf-8');
69
+ }
70
+ await fs.writeFile(join(rulesDir, 'specflow-context.mdc'), renderCursorRule(), 'utf-8');
71
+ const runtimePaths = await copyRuntimeAssets(packageRoot, projectRoot, 'cursor');
72
+ return {
73
+ ide: 'cursor',
74
+ generatedPaths: [
75
+ '.cursor/skills',
76
+ '.cursor/commands/specflow',
77
+ '.cursor/rules/specflow-context.mdc',
78
+ ...runtimePaths,
79
+ ],
80
+ };
81
+ }
82
+ async function inspectCursor(projectRoot) {
83
+ const commandDir = join(projectRoot, '.cursor', 'commands', 'specflow');
84
+ const skillDir = join(projectRoot, '.cursor', 'skills');
85
+ const ruleFile = join(projectRoot, '.cursor', 'rules', 'specflow-context.mdc');
86
+ const promptsDir = join(projectRoot, '.cursor', 'specflow', 'prompts');
87
+ const schemasDir = join(projectRoot, '.cursor', 'specflow', 'schemas');
88
+ const templatesDir = join(projectRoot, '.cursor', 'specflow', 'templates');
89
+ const supported = [];
90
+ const skillContentsByCommand = {};
91
+ for (const command of COMMAND_CATALOG) {
92
+ try {
93
+ const stat = await fs.stat(join(commandDir, `${command.id}.md`));
94
+ if (stat.isFile()) {
95
+ supported.push(command.id);
96
+ }
97
+ }
98
+ catch {
99
+ // ignore
100
+ }
101
+ try {
102
+ skillContentsByCommand[command.id] = await fs.readFile(join(skillDir, `specflow-${command.id}`, 'SKILL.md'), 'utf-8');
103
+ }
104
+ catch {
105
+ skillContentsByCommand[command.id] = '';
106
+ }
107
+ }
108
+ return {
109
+ ide: 'cursor',
110
+ supportedCommandIds: supported,
111
+ supportedCapabilityIds: deriveCapabilityIds(supported, skillContentsByCommand),
112
+ assets: {
113
+ commandsDir: { exists: await directoryExists(commandDir), hash: await hashDirectoryTree(commandDir) },
114
+ skillsDir: { exists: await directoryExists(skillDir), hash: await hashDirectoryTree(skillDir) },
115
+ rulesFile: { exists: await fileExists(ruleFile), hash: await hashFile(ruleFile) },
116
+ runtimePromptsDir: { exists: await directoryExists(promptsDir), hash: await hashDirectoryTree(promptsDir) },
117
+ runtimeSchemasDir: { exists: await directoryExists(schemasDir), hash: await hashDirectoryTree(schemasDir) },
118
+ runtimeTemplatesDir: { exists: await directoryExists(templatesDir), hash: await hashDirectoryTree(templatesDir) },
119
+ },
120
+ };
121
+ }
122
+ export const cursorAdapter = {
123
+ ide: 'cursor',
124
+ generate: generateCursor,
125
+ inspect: inspectCursor,
126
+ };
@@ -0,0 +1,4 @@
1
+ import type { Adapter, IdeTarget, InitIdeTarget } from './types.js';
2
+ export declare const ALL_ADAPTERS: readonly Adapter[];
3
+ export declare function getAdapter(ide: IdeTarget): Adapter;
4
+ export declare function resolveAdapters(ide: InitIdeTarget): readonly Adapter[];
@@ -0,0 +1,27 @@
1
+ import { claudeAdapter } from '../claude/adapter.js';
2
+ import { codexAdapter } from '../codex/adapter.js';
3
+ import { cursorAdapter } from '../cursor/adapter.js';
4
+ export const ALL_ADAPTERS = [claudeAdapter, cursorAdapter, codexAdapter];
5
+ export function getAdapter(ide) {
6
+ const adapter = ALL_ADAPTERS.find((a) => a.ide === ide);
7
+ if (!adapter) {
8
+ throw new Error(`Unknown IDE target: ${ide}`);
9
+ }
10
+ return adapter;
11
+ }
12
+ export function resolveAdapters(ide) {
13
+ switch (ide) {
14
+ case 'claude':
15
+ return [claudeAdapter];
16
+ case 'cursor':
17
+ return [cursorAdapter];
18
+ case 'codex':
19
+ return [codexAdapter];
20
+ case 'both':
21
+ return [claudeAdapter, cursorAdapter];
22
+ case 'all':
23
+ return ALL_ADAPTERS;
24
+ default:
25
+ throw new Error(`Unknown init IDE target: ${ide}`);
26
+ }
27
+ }
@@ -0,0 +1,2 @@
1
+ export declare function copyDirRecursive(src: string, dest: string): Promise<void>;
2
+ export declare function copyDirRecursiveRendered(src: string, dest: string, render: (content: string, relativePath: string) => string): Promise<void>;
@@ -0,0 +1,44 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ const RENDERABLE_EXTENSIONS = new Set(['.md', '.yaml', '.yml', '.dot', '.txt']);
4
+ function shouldRenderFile(fileName) {
5
+ const dot = fileName.lastIndexOf('.');
6
+ if (dot === -1) {
7
+ return false;
8
+ }
9
+ return RENDERABLE_EXTENSIONS.has(fileName.slice(dot));
10
+ }
11
+ export async function copyDirRecursive(src, dest) {
12
+ await fs.mkdir(dest, { recursive: true });
13
+ const entries = await fs.readdir(src, { withFileTypes: true });
14
+ await Promise.all(entries.map(async (entry) => {
15
+ const srcPath = join(src, entry.name);
16
+ const destPath = join(dest, entry.name);
17
+ if (entry.isDirectory()) {
18
+ await copyDirRecursive(srcPath, destPath);
19
+ }
20
+ else if (entry.isFile()) {
21
+ await fs.copyFile(srcPath, destPath);
22
+ }
23
+ }));
24
+ }
25
+ export async function copyDirRecursiveRendered(src, dest, render) {
26
+ await fs.mkdir(dest, { recursive: true });
27
+ const entries = await fs.readdir(src, { withFileTypes: true });
28
+ await Promise.all(entries.map(async (entry) => {
29
+ const srcPath = join(src, entry.name);
30
+ const destPath = join(dest, entry.name);
31
+ if (entry.isDirectory()) {
32
+ await copyDirRecursiveRendered(srcPath, destPath, render);
33
+ }
34
+ else if (entry.isFile()) {
35
+ if (shouldRenderFile(entry.name)) {
36
+ const content = await fs.readFile(srcPath, 'utf-8');
37
+ await fs.writeFile(destPath, render(content, entry.name), 'utf-8');
38
+ }
39
+ else {
40
+ await fs.copyFile(srcPath, destPath);
41
+ }
42
+ }
43
+ }));
44
+ }
@@ -0,0 +1,4 @@
1
+ export declare function hashFile(path: string): Promise<string | undefined>;
2
+ export declare function directoryExists(path: string): Promise<boolean>;
3
+ export declare function fileExists(path: string): Promise<boolean>;
4
+ export declare function hashDirectoryTree(path: string): Promise<string | undefined>;
@@ -0,0 +1,54 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { promises as fs } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ export async function hashFile(path) {
5
+ try {
6
+ const content = await fs.readFile(path);
7
+ return createHash('sha256').update(content).digest('hex');
8
+ }
9
+ catch {
10
+ return undefined;
11
+ }
12
+ }
13
+ export async function directoryExists(path) {
14
+ try {
15
+ const stat = await fs.stat(path);
16
+ return stat.isDirectory();
17
+ }
18
+ catch {
19
+ return false;
20
+ }
21
+ }
22
+ export async function fileExists(path) {
23
+ try {
24
+ const stat = await fs.stat(path);
25
+ return stat.isFile();
26
+ }
27
+ catch {
28
+ return false;
29
+ }
30
+ }
31
+ export async function hashDirectoryTree(path) {
32
+ if (!(await directoryExists(path))) {
33
+ return undefined;
34
+ }
35
+ const hash = createHash('sha256');
36
+ await walkAndHash(path, hash, path);
37
+ return hash.digest('hex');
38
+ }
39
+ async function walkAndHash(dir, hash, root) {
40
+ const entries = await fs.readdir(dir, { withFileTypes: true });
41
+ const sorted = [...entries].sort((a, b) => a.name.localeCompare(b.name));
42
+ for (const entry of sorted) {
43
+ const full = join(dir, entry.name);
44
+ const rel = full.slice(root.length);
45
+ hash.update(rel);
46
+ if (entry.isDirectory()) {
47
+ await walkAndHash(full, hash, root);
48
+ }
49
+ else if (entry.isFile()) {
50
+ const content = await fs.readFile(full);
51
+ hash.update(content);
52
+ }
53
+ }
54
+ }
@@ -0,0 +1 @@
1
+ export declare function deriveCapabilityIds(supportedCommandIds: readonly string[], skillContentsByCommand: Readonly<Record<string, string>>): string[];
@@ -0,0 +1,42 @@
1
+ import { COMMAND_IDS } from './command-catalog.js';
2
+ const MARKER_RULES = [
3
+ { id: 'sub.scan.planned_v03_notice', skill: 'scan', includes: ['[PLANNED v0.3]', 'NOT YET FUNCTIONAL'] },
4
+ { id: 'sub.plan.first_iteration', skill: 'plan', includes: ['first-iteration deep-analysis pass'] },
5
+ { id: 'sub.refine.multi_round', skill: 'refine', includes: ['auto-multi-round loop', '## LOOP (Stages 2-4)'] },
6
+ { id: 'sub.refine.challenge_scope', skill: 'refine', includes: ['Challenge Behaviors #3 and #4', 'question scope'] },
7
+ { id: 'sub.build.phase_a', skill: 'build', includes: ['## Phase A: Task Rewrite'] },
8
+ { id: 'sub.build.phase_b', skill: 'build', includes: ['## Phase B: Subagent TDD Execution'] },
9
+ { id: 'sub.build.gap_detection', skill: 'build', includes: ['Gap Detection'] },
10
+ { id: 'sub.review.spec_regression', skill: 'review', includes: ['regression'] },
11
+ { id: 'sub.test.full_scope', skill: 'test', includes: ['unit, integration, e2e', 'verification report'] },
12
+ { id: 'sub.verify.dual_validation', skill: 'verify', includes: ['Dual validation', 'delta specs'] },
13
+ { id: 'sub.done.archive_merge', skill: 'done', includes: ['specflow change archive', 'delta specs into main specs'] },
14
+ { id: 'sub.fix.urgent_mode', skill: 'fix', includes: ['--urgent', 'skip'] },
15
+ { id: 'sub.snap.posthoc', skill: 'snap', includes: ['post-hoc', 'archive'] },
16
+ { id: 'failure.scan.not_implemented_guard', skill: 'scan', includes: ['do NOT attempt to run `specflow scan`'] },
17
+ { id: 'failure.build.phase_gate', skill: 'build', includes: ['HARD GATE (prerequisite)', 'phase is not `refined`, REFUSE'] },
18
+ { id: 'failure.verify.explicit_skip_marker', skill: 'verify', includes: ['Pass 2: skipped (no baseline'] },
19
+ ];
20
+ export function deriveCapabilityIds(supportedCommandIds, skillContentsByCommand) {
21
+ const capabilityIds = new Set();
22
+ for (const commandId of supportedCommandIds) {
23
+ capabilityIds.add(`command.${commandId}`);
24
+ }
25
+ for (const rule of MARKER_RULES) {
26
+ const content = skillContentsByCommand[rule.skill] ?? '';
27
+ const ok = rule.includes.every((needle) => content.includes(needle));
28
+ if (ok) {
29
+ capabilityIds.add(rule.id);
30
+ }
31
+ }
32
+ // Safety: never emit capabilities for unknown commands.
33
+ for (const id of [...capabilityIds]) {
34
+ if (id.startsWith('command.')) {
35
+ const command = id.replace('command.', '');
36
+ if (!COMMAND_IDS.includes(command)) {
37
+ capabilityIds.delete(id);
38
+ }
39
+ }
40
+ }
41
+ return [...capabilityIds].sort();
42
+ }
@@ -0,0 +1,6 @@
1
+ export interface CommandInfo {
2
+ readonly id: string;
3
+ readonly description: string;
4
+ }
5
+ export declare const COMMAND_CATALOG: readonly CommandInfo[];
6
+ export declare const COMMAND_IDS: readonly string[];
@@ -0,0 +1,13 @@
1
+ export const COMMAND_CATALOG = [
2
+ { id: 'scan', description: 'Brownfield project scanner via code-review-graph' },
3
+ { id: 'plan', description: 'Create proposal and spec artifacts for a change' },
4
+ { id: 'refine', description: 'Refine and iterate on spec artifacts' },
5
+ { id: 'build', description: 'Implement tasks from a change spec' },
6
+ { id: 'review', description: 'Review code changes against spec' },
7
+ { id: 'test', description: 'Run tests and verify coverage' },
8
+ { id: 'verify', description: 'Verify implementation matches spec' },
9
+ { id: 'done', description: 'Finalize and archive a completed change' },
10
+ { id: 'fix', description: 'Debug and fix failing tests or builds' },
11
+ { id: 'snap', description: 'Snapshot current project state' },
12
+ ];
13
+ export const COMMAND_IDS = COMMAND_CATALOG.map((c) => c.id);
@@ -0,0 +1,2 @@
1
+ import type { IdeTarget } from './types.js';
2
+ export declare function getRegeneratableIgnoreLines(ide: IdeTarget): readonly string[];
@@ -0,0 +1,21 @@
1
+ export function getRegeneratableIgnoreLines(ide) {
2
+ if (ide === 'claude') {
3
+ return [
4
+ '.claude/specflow/prompts/',
5
+ '.claude/specflow/schemas/',
6
+ '.claude/specflow/templates/',
7
+ ];
8
+ }
9
+ if (ide === 'cursor') {
10
+ return [
11
+ '.cursor/specflow/prompts/',
12
+ '.cursor/specflow/schemas/',
13
+ '.cursor/specflow/templates/',
14
+ ];
15
+ }
16
+ return [
17
+ '.agents/specflow/prompts/',
18
+ '.agents/specflow/schemas/',
19
+ '.agents/specflow/templates/',
20
+ ];
21
+ }
@@ -0,0 +1,3 @@
1
+ export declare function appendManagedBlock(targetPath: string, markerStart: string, markerEnd: string, lines: readonly string[]): Promise<void>;
2
+ export declare function upsertManagedBlock(targetPath: string, markerStart: string, markerEnd: string, lines: readonly string[]): Promise<void>;
3
+ export declare function managedBlockPresent(content: string, markerStart: string, markerEnd: string): boolean;
@@ -0,0 +1,48 @@
1
+ import { promises as fs } from 'node:fs';
2
+ function buildBlock(markerStart, lines, markerEnd) {
3
+ return [markerStart, ...lines, markerEnd].join('\n');
4
+ }
5
+ export async function appendManagedBlock(targetPath, markerStart, markerEnd, lines) {
6
+ const block = `${buildBlock(markerStart, lines, markerEnd)}\n`;
7
+ let existing = '';
8
+ try {
9
+ existing = await fs.readFile(targetPath, 'utf-8');
10
+ }
11
+ catch (error) {
12
+ if (!(error instanceof Error && 'code' in error && error.code === 'ENOENT')) {
13
+ throw error;
14
+ }
15
+ }
16
+ if (existing.includes(markerStart)) {
17
+ return;
18
+ }
19
+ const separator = existing.length > 0 && !existing.endsWith('\n') ? '\n' : '';
20
+ await fs.writeFile(targetPath, `${existing}${separator}\n${block}`, 'utf-8');
21
+ }
22
+ export async function upsertManagedBlock(targetPath, markerStart, markerEnd, lines) {
23
+ const block = buildBlock(markerStart, lines, markerEnd);
24
+ let existing = '';
25
+ try {
26
+ existing = await fs.readFile(targetPath, 'utf-8');
27
+ }
28
+ catch (error) {
29
+ if (!(error instanceof Error && 'code' in error && error.code === 'ENOENT')) {
30
+ throw error;
31
+ }
32
+ }
33
+ if (existing.includes(markerStart) && existing.includes(markerEnd)) {
34
+ const startIdx = existing.indexOf(markerStart);
35
+ const endIdx = existing.indexOf(markerEnd, startIdx);
36
+ const afterEnd = endIdx + markerEnd.length;
37
+ const before = existing.slice(0, startIdx).replace(/\s+$/, '');
38
+ const after = existing.slice(afterEnd).replace(/^\s+/, '');
39
+ const parts = [before, block, after].filter((part) => part.length > 0);
40
+ await fs.writeFile(targetPath, `${parts.join('\n\n')}\n`, 'utf-8');
41
+ return;
42
+ }
43
+ const separator = existing.length > 0 && !existing.endsWith('\n') ? '\n' : '';
44
+ await fs.writeFile(targetPath, `${existing}${separator}\n${block}\n`, 'utf-8');
45
+ }
46
+ export function managedBlockPresent(content, markerStart, markerEnd) {
47
+ return content.includes(markerStart) && content.includes(markerEnd);
48
+ }
@@ -0,0 +1,2 @@
1
+ export type MigrationState = 'fresh' | 'claude_only' | 'cursor_only' | 'codex_only' | 'both' | 'both_partial' | 'all_three' | 'all_partial' | 'customized';
2
+ export declare function detectMigrationState(projectRoot: string): Promise<MigrationState>;
@@ -0,0 +1,67 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { directoryExists } from './asset-hash.js';
4
+ async function hasIdeRuntime(projectRoot, ide) {
5
+ if (ide === 'claude') {
6
+ return directoryExists(join(projectRoot, '.claude', 'specflow'));
7
+ }
8
+ if (ide === 'cursor') {
9
+ return directoryExists(join(projectRoot, '.cursor', 'specflow'));
10
+ }
11
+ return directoryExists(join(projectRoot, '.agents', 'specflow'));
12
+ }
13
+ export async function detectMigrationState(projectRoot) {
14
+ const hasClaude = await directoryExists(join(projectRoot, '.claude'));
15
+ const hasCursor = await directoryExists(join(projectRoot, '.cursor'));
16
+ const hasCodex = await directoryExists(join(projectRoot, '.agents'));
17
+ const hasSpecflow = await directoryExists(join(projectRoot, 'specflow'));
18
+ if (!hasClaude && !hasCursor && !hasCodex && !hasSpecflow) {
19
+ return 'fresh';
20
+ }
21
+ const ideCount = [hasClaude, hasCursor, hasCodex].filter(Boolean).length;
22
+ if (ideCount === 1) {
23
+ if (hasClaude) {
24
+ return 'claude_only';
25
+ }
26
+ if (hasCursor) {
27
+ return 'cursor_only';
28
+ }
29
+ return 'codex_only';
30
+ }
31
+ if (ideCount === 2 && hasClaude && hasCursor && !hasCodex) {
32
+ const hasClaudeCore = await hasIdeRuntime(projectRoot, 'claude');
33
+ const hasCursorCore = await hasIdeRuntime(projectRoot, 'cursor');
34
+ if (hasClaudeCore && hasCursorCore) {
35
+ const customized = await hasCursorCustomizations(projectRoot);
36
+ return customized ? 'customized' : 'both';
37
+ }
38
+ return 'both_partial';
39
+ }
40
+ if (ideCount === 3) {
41
+ const [hasClaudeCore, hasCursorCore, hasCodexCore] = await Promise.all([
42
+ hasIdeRuntime(projectRoot, 'claude'),
43
+ hasIdeRuntime(projectRoot, 'cursor'),
44
+ hasIdeRuntime(projectRoot, 'codex'),
45
+ ]);
46
+ if (hasClaudeCore && hasCursorCore && hasCodexCore) {
47
+ const customized = await hasCursorCustomizations(projectRoot);
48
+ return customized ? 'customized' : 'all_three';
49
+ }
50
+ return 'all_partial';
51
+ }
52
+ return 'all_partial';
53
+ }
54
+ async function hasCursorCustomizations(projectRoot) {
55
+ const cursorRoot = join(projectRoot, '.cursor');
56
+ if (!(await directoryExists(cursorRoot))) {
57
+ return false;
58
+ }
59
+ const managedNames = new Set(['commands', 'skills', 'rules', 'specflow']);
60
+ try {
61
+ const entries = await fs.readdir(cursorRoot, { withFileTypes: true });
62
+ return entries.some((entry) => !managedNames.has(entry.name));
63
+ }
64
+ catch {
65
+ return false;
66
+ }
67
+ }
@@ -0,0 +1,6 @@
1
+ import type { CapabilityReport, ParityStatus } from './types.js';
2
+ export declare function validateReportAgainstManifest(report: CapabilityReport): ParityStatus;
3
+ export declare function compareReports(left: CapabilityReport, right: CapabilityReport): ParityStatus;
4
+ export declare function compareIdeParity(claude: CapabilityReport, cursor: CapabilityReport): ParityStatus;
5
+ export declare function compareTriIdeParity(claude: CapabilityReport, cursor: CapabilityReport, codex: CapabilityReport): ParityStatus;
6
+ export declare function compareHealthyReports(reports: readonly CapabilityReport[]): ParityStatus;
@@ -0,0 +1,97 @@
1
+ import { CAPABILITY_MANIFEST, IDE_ASSET_MANIFEST } from './parity-manifest.js';
2
+ // Prompts are rendered per IDE (paths and invocation syntax differ), so only
3
+ // compare runtime assets that are copied verbatim across adapters.
4
+ const PARITY_COMPARABLE_ASSET_KEYS = [
5
+ 'runtimeSchemasDir',
6
+ 'runtimeTemplatesDir',
7
+ ];
8
+ export function validateReportAgainstManifest(report) {
9
+ const deltas = [];
10
+ const supportedCapabilities = new Set(report.supportedCapabilityIds);
11
+ for (const capability of CAPABILITY_MANIFEST) {
12
+ if (!capability.required) {
13
+ continue;
14
+ }
15
+ if (!supportedCapabilities.has(capability.id)) {
16
+ deltas.push({
17
+ id: `missing-capability:${report.ide}:${capability.id}`,
18
+ severity: 'P0',
19
+ message: `${report.ide} missing required capability ${capability.id}`,
20
+ });
21
+ }
22
+ }
23
+ const assetSpec = IDE_ASSET_MANIFEST.find((s) => s.ide === report.ide);
24
+ if (assetSpec) {
25
+ for (const key of assetSpec.requiredAssetKeys) {
26
+ if (!report.assets[key]?.exists) {
27
+ deltas.push({
28
+ id: `missing-asset:${report.ide}:${key}`,
29
+ severity: 'P0',
30
+ message: `${report.ide} missing required asset ${key}`,
31
+ });
32
+ }
33
+ }
34
+ }
35
+ return { ok: deltas.length === 0, deltas };
36
+ }
37
+ function compareCapabilitySets(baseline, target) {
38
+ const deltas = [];
39
+ const baselineSupported = new Set(baseline.supportedCapabilityIds);
40
+ const targetSupported = new Set(target.supportedCapabilityIds);
41
+ for (const capability of baselineSupported) {
42
+ if (!targetSupported.has(capability)) {
43
+ deltas.push({
44
+ id: `${target.ide}-missing-capability:${capability}`,
45
+ severity: 'P0',
46
+ message: `${target.ide} missing capability present in ${baseline.ide}: ${capability}`,
47
+ });
48
+ }
49
+ }
50
+ for (const capability of targetSupported) {
51
+ if (!baselineSupported.has(capability)) {
52
+ deltas.push({
53
+ id: `${baseline.ide}-missing-capability:${capability}`,
54
+ severity: 'P0',
55
+ message: `${baseline.ide} missing capability present in ${target.ide}: ${capability}`,
56
+ });
57
+ }
58
+ }
59
+ return deltas;
60
+ }
61
+ function compareAssetHashes(left, right) {
62
+ const deltas = [];
63
+ for (const key of PARITY_COMPARABLE_ASSET_KEYS) {
64
+ const l = left.assets[key];
65
+ const r = right.assets[key];
66
+ if (!l?.exists || !r?.exists) {
67
+ continue;
68
+ }
69
+ if (l.hash !== undefined && r.hash !== undefined && l.hash !== r.hash) {
70
+ deltas.push({
71
+ id: `asset-drift:${left.ide}-${right.ide}:${key}`,
72
+ severity: 'P1',
73
+ message: `Asset content drift for ${key} between ${left.ide} and ${right.ide}`,
74
+ });
75
+ }
76
+ }
77
+ return deltas;
78
+ }
79
+ export function compareReports(left, right) {
80
+ const deltas = [...compareCapabilitySets(left, right), ...compareAssetHashes(left, right)];
81
+ return { ok: deltas.length === 0, deltas };
82
+ }
83
+ export function compareIdeParity(claude, cursor) {
84
+ return compareReports(claude, cursor);
85
+ }
86
+ export function compareTriIdeParity(claude, cursor, codex) {
87
+ return compareHealthyReports([claude, cursor, codex]);
88
+ }
89
+ export function compareHealthyReports(reports) {
90
+ const deltas = [];
91
+ for (let i = 0; i < reports.length; i += 1) {
92
+ for (let j = i + 1; j < reports.length; j += 1) {
93
+ deltas.push(...compareReports(reports[i], reports[j]).deltas);
94
+ }
95
+ }
96
+ return { ok: deltas.length === 0, deltas };
97
+ }
@@ -0,0 +1,13 @@
1
+ import type { IdeTarget } from './types.js';
2
+ export interface CapabilitySpec {
3
+ readonly id: string;
4
+ readonly commandId: string;
5
+ readonly level: 'command' | 'sub-capability' | 'failure-path';
6
+ readonly required: boolean;
7
+ }
8
+ export interface IdeAssetSpec {
9
+ readonly ide: IdeTarget;
10
+ readonly requiredAssetKeys: readonly string[];
11
+ }
12
+ export declare const CAPABILITY_MANIFEST: readonly CapabilitySpec[];
13
+ export declare const IDE_ASSET_MANIFEST: readonly IdeAssetSpec[];