@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,66 @@
1
+ /**
2
+ * CLI command: specflow change phase <name> [--set <value>]
3
+ *
4
+ * Read or update the lifecycle phase of a change.
5
+ */
6
+ import { CHANGE_PHASES, getChangeMetadata, updatePhase, } from '../../utils/change-utils.js';
7
+ import { requireProjectRoot } from '../../utils/project-root.js';
8
+ /**
9
+ * Returns true if the candidate value is a valid ChangePhase.
10
+ */
11
+ function isChangePhase(value) {
12
+ return CHANGE_PHASES.includes(value);
13
+ }
14
+ /**
15
+ * Reads the current phase of a change.
16
+ *
17
+ * @param name - Name of the change
18
+ * @param projectRoot - Absolute path to the project root
19
+ * @returns The current phase, or undefined for legacy files without a phase field
20
+ * @throws When the change does not exist
21
+ */
22
+ export async function getPhase(name, projectRoot) {
23
+ const metadata = await getChangeMetadata(name, projectRoot);
24
+ if (metadata === null) {
25
+ throw new Error(`Change not found: "${name}"`);
26
+ }
27
+ return metadata.phase;
28
+ }
29
+ /**
30
+ * Sets the phase of a change, validating the phase value first.
31
+ *
32
+ * @param name - Name of the change
33
+ * @param phase - The new phase value (validated against CHANGE_PHASES)
34
+ * @param projectRoot - Absolute path to the project root
35
+ * @throws When the phase is invalid or the change does not exist
36
+ */
37
+ export async function setPhase(name, phase, projectRoot) {
38
+ if (!isChangePhase(phase)) {
39
+ const valid = CHANGE_PHASES.join(' | ');
40
+ throw new Error(`Invalid phase "${phase}": expected one of ${valid}`);
41
+ }
42
+ await updatePhase(name, phase, projectRoot);
43
+ }
44
+ /**
45
+ * Registers the `change phase` subcommand with Commander.
46
+ *
47
+ * Usage:
48
+ * specflow change phase <name> # print current phase
49
+ * specflow change phase <name> --set plan # update phase
50
+ */
51
+ export function registerChangePhaseCommand(changeCmd) {
52
+ changeCmd
53
+ .command('phase <name>')
54
+ .description('Show or set the lifecycle phase of a change')
55
+ .option('--set <value>', 'Set the phase to the given value')
56
+ .action(async (name, opts) => {
57
+ const projectRoot = requireProjectRoot();
58
+ if (opts.set !== undefined) {
59
+ await setPhase(name, opts.set, projectRoot);
60
+ console.info(`Change "${name}" phase set to ${opts.set}`);
61
+ return;
62
+ }
63
+ const phase = await getPhase(name, projectRoot);
64
+ console.info(phase ?? '(unset)');
65
+ });
66
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * CLI command: specflow change status <name>
3
+ *
4
+ * Shows the status of artifacts for a given change.
5
+ */
6
+ import type { Command } from 'commander';
7
+ import type { ArtifactStatusEntry } from '../../core/artifact-graph/outputs.js';
8
+ /**
9
+ * Extended status output with change-level metadata.
10
+ */
11
+ export interface ChangeStatusOutput {
12
+ readonly changeName: string;
13
+ readonly artifacts: readonly ArtifactStatusEntry[];
14
+ readonly isComplete: boolean;
15
+ }
16
+ /**
17
+ * Gets the status of all artifacts for a given change.
18
+ *
19
+ * @param changeName - The name of the change
20
+ * @param projectRoot - Absolute path to the project root
21
+ * @returns Status output with per-artifact state and completion flag
22
+ * @throws When the change directory does not exist
23
+ */
24
+ export declare function getChangeStatus(changeName: string, projectRoot: string): Promise<ChangeStatusOutput>;
25
+ /**
26
+ * Formats a ChangeStatusOutput as human-readable checkbox text.
27
+ *
28
+ * @param status - The status output to format
29
+ * @returns Formatted string with [x] and [ ] checkboxes
30
+ */
31
+ export declare function formatChangeStatus(status: ChangeStatusOutput): string;
32
+ /**
33
+ * Registers the `change status` subcommand with Commander.
34
+ */
35
+ export declare function registerChangeStatusCommand(changeCmd: Command): void;
@@ -0,0 +1,95 @@
1
+ /**
2
+ * CLI command: specflow change status <name>
3
+ *
4
+ * Shows the status of artifacts for a given change.
5
+ */
6
+ import { join } from 'node:path';
7
+ import * as fs from 'node:fs';
8
+ import yaml from 'js-yaml';
9
+ import { resolveSchema } from '../../core/artifact-graph/resolver.js';
10
+ import { getCompletedArtifacts } from '../../core/artifact-graph/state.js';
11
+ import { formatStatus } from '../../core/artifact-graph/outputs.js';
12
+ import { requireProjectRoot } from '../../utils/project-root.js';
13
+ import { readChangeMetadata } from '../../utils/change-metadata.js';
14
+ /**
15
+ * Reads the project config to determine the schema name.
16
+ */
17
+ function readSchemaName(projectRoot) {
18
+ const configPath = join(projectRoot, 'specflow', 'config.yaml');
19
+ try {
20
+ const content = fs.readFileSync(configPath, 'utf-8');
21
+ const parsed = yaml.load(content);
22
+ if (typeof parsed?.schema === 'string') {
23
+ return parsed.schema;
24
+ }
25
+ }
26
+ catch {
27
+ // Fall through to default
28
+ }
29
+ return 'specflow';
30
+ }
31
+ /**
32
+ * Gets the status of all artifacts for a given change.
33
+ *
34
+ * @param changeName - The name of the change
35
+ * @param projectRoot - Absolute path to the project root
36
+ * @returns Status output with per-artifact state and completion flag
37
+ * @throws When the change directory does not exist
38
+ */
39
+ export async function getChangeStatus(changeName, projectRoot) {
40
+ const changeDir = join(projectRoot, 'specflow', 'changes', changeName);
41
+ if (!fs.existsSync(changeDir)) {
42
+ throw new Error(`Change "${changeName}" not found at ${changeDir}`);
43
+ }
44
+ // Validate change metadata (including phase enum) before proceeding.
45
+ // If .specflow.yaml contains an invalid phase, Zod will throw a clear error
46
+ // listing the valid enum values. Missing metadata is tolerated (null return).
47
+ await readChangeMetadata(changeDir);
48
+ const schemaName = readSchemaName(projectRoot);
49
+ // Resolve schema from project-local schemas/ directory, falling back to package schemas
50
+ const schema = resolveSchema(schemaName, projectRoot);
51
+ const completed = getCompletedArtifacts(changeDir, schema);
52
+ const statusOutput = formatStatus(schema.artifacts, completed);
53
+ const completedSet = new Set(completed);
54
+ const allDone = schema.artifacts.every((a) => completedSet.has(a.id));
55
+ return {
56
+ changeName,
57
+ artifacts: statusOutput.artifacts,
58
+ isComplete: allDone,
59
+ };
60
+ }
61
+ /**
62
+ * Formats a ChangeStatusOutput as human-readable checkbox text.
63
+ *
64
+ * @param status - The status output to format
65
+ * @returns Formatted string with [x] and [ ] checkboxes
66
+ */
67
+ export function formatChangeStatus(status) {
68
+ const lines = status.artifacts.map((entry) => {
69
+ const checkbox = entry.status === 'done' ? '[x]' : '[ ]';
70
+ const suffix = entry.missingDeps && entry.missingDeps.length > 0
71
+ ? ` (blocked by: ${entry.missingDeps.join(', ')})`
72
+ : '';
73
+ return `${checkbox} ${entry.id}${suffix}`;
74
+ });
75
+ return lines.join('\n');
76
+ }
77
+ /**
78
+ * Registers the `change status` subcommand with Commander.
79
+ */
80
+ export function registerChangeStatusCommand(changeCmd) {
81
+ changeCmd
82
+ .command('status <name>')
83
+ .description('Show artifact status for a change')
84
+ .option('--json', 'Output as JSON')
85
+ .action(async (name, opts) => {
86
+ const projectRoot = requireProjectRoot();
87
+ const status = await getChangeStatus(name, projectRoot);
88
+ if (opts.json) {
89
+ console.info(JSON.stringify(status, null, 2));
90
+ }
91
+ else {
92
+ console.info(formatChangeStatus(status));
93
+ }
94
+ });
95
+ }
@@ -0,0 +1,9 @@
1
+ import type { Command } from 'commander';
2
+ export interface DoctorOutput {
3
+ readonly ok: boolean;
4
+ readonly migrationState: string;
5
+ readonly checks: readonly string[];
6
+ readonly errors: readonly string[];
7
+ }
8
+ export declare function runDoctor(projectRoot: string, parity: boolean): Promise<DoctorOutput>;
9
+ export declare function registerDoctorCommand(program: Command): void;
@@ -0,0 +1,48 @@
1
+ import { ALL_ADAPTERS } from '../../integrations/shared/adapter-registry.js';
2
+ import { compareHealthyReports, validateReportAgainstManifest } from '../../integrations/shared/parity-comparator.js';
3
+ import { detectMigrationState } from '../../integrations/shared/migration-state.js';
4
+ import { requireProjectRoot } from '../../utils/project-root.js';
5
+ export async function runDoctor(projectRoot, parity) {
6
+ const reports = await Promise.all(ALL_ADAPTERS.map((adapter) => adapter.inspect(projectRoot)));
7
+ const checks = [];
8
+ const errors = [];
9
+ const migrationState = await detectMigrationState(projectRoot);
10
+ checks.push(`migration state: ${migrationState}`);
11
+ for (const report of reports) {
12
+ const status = validateReportAgainstManifest(report);
13
+ checks.push(`${report.ide} capabilities: ${status.ok ? 'ok' : 'fail'}`);
14
+ errors.push(...status.deltas.map((d) => d.message));
15
+ }
16
+ if (parity) {
17
+ const healthyReports = reports.filter((report) => validateReportAgainstManifest(report).ok);
18
+ const parityStatus = compareHealthyReports(healthyReports);
19
+ checks.push(`cross-ide parity: ${parityStatus.ok ? 'ok' : 'fail'}`);
20
+ errors.push(...parityStatus.deltas.map((d) => d.message));
21
+ }
22
+ return { ok: errors.length === 0, migrationState, checks, errors };
23
+ }
24
+ export function registerDoctorCommand(program) {
25
+ program
26
+ .command('doctor')
27
+ .description('Diagnose specflow IDE assets and parity')
28
+ .option('--json', 'Output as JSON')
29
+ .option('--parity', 'Include cross-IDE parity diagnostics')
30
+ .action(async (opts) => {
31
+ const projectRoot = requireProjectRoot();
32
+ const result = await runDoctor(projectRoot, opts.parity ?? false);
33
+ if (opts.json) {
34
+ console.info(JSON.stringify(result, null, 2));
35
+ }
36
+ else {
37
+ for (const line of result.checks) {
38
+ console.info(`- ${line}`);
39
+ }
40
+ for (const err of result.errors) {
41
+ console.error(` error: ${err}`);
42
+ }
43
+ }
44
+ if (!result.ok) {
45
+ process.exitCode = 1;
46
+ }
47
+ });
48
+ }
@@ -0,0 +1,16 @@
1
+ import type { Command } from 'commander';
2
+ import type { InitIdeTarget } from '../../integrations/shared/types.js';
3
+ /**
4
+ * Result of an init operation.
5
+ */
6
+ export interface InitResult {
7
+ readonly status: 'initialized' | 'already_initialized' | 'updated_assets';
8
+ readonly message: string;
9
+ }
10
+ export interface InitOptions {
11
+ readonly ide?: InitIdeTarget;
12
+ readonly forceAssets?: boolean;
13
+ readonly parityStrict?: boolean;
14
+ }
15
+ export declare function initProject(projectRoot: string, packageRoot: string, options?: InitOptions): Promise<InitResult>;
16
+ export declare function registerInitCommand(program: Command): void;
@@ -0,0 +1,122 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import * as fsSync from 'node:fs';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { getAdapter, resolveAdapters } from '../../integrations/shared/adapter-registry.js';
6
+ import { validateReportAgainstManifest } from '../../integrations/shared/parity-comparator.js';
7
+ import { appendManagedBlock } from '../../integrations/shared/marker-write.js';
8
+ import { getRegeneratableIgnoreLines } from '../../integrations/shared/managed-assets.js';
9
+ import { detectMigrationState } from '../../integrations/shared/migration-state.js';
10
+ const CONFIG_YAML = `schema: specflow
11
+
12
+ # Project context (customize for your project)
13
+ # context: |
14
+ # Tech stack: ...
15
+ # Architecture: ...
16
+ `;
17
+ function findPackageRoot() {
18
+ const currentFile = fileURLToPath(import.meta.url);
19
+ let dir = join(currentFile, '..');
20
+ const root = '/';
21
+ while (dir !== root) {
22
+ if (fsSync.existsSync(join(dir, 'package.json'))) {
23
+ return dir;
24
+ }
25
+ dir = join(dir, '..');
26
+ }
27
+ throw new Error('Could not find package root (no package.json found in parent directories)');
28
+ }
29
+ async function createDirectoryStructure(projectRoot) {
30
+ await Promise.all([
31
+ fs.mkdir(join(projectRoot, 'specflow', 'changes'), { recursive: true }),
32
+ fs.mkdir(join(projectRoot, 'specflow', 'specs'), { recursive: true }),
33
+ ]);
34
+ }
35
+ async function writeConfig(projectRoot) {
36
+ await fs.writeFile(join(projectRoot, 'specflow', 'config.yaml'), CONFIG_YAML, 'utf-8');
37
+ }
38
+ async function pathExists(path) {
39
+ try {
40
+ await fs.stat(path);
41
+ return true;
42
+ }
43
+ catch {
44
+ return false;
45
+ }
46
+ }
47
+ async function isProjectInitialized(projectRoot) {
48
+ return pathExists(join(projectRoot, 'specflow', 'config.yaml'));
49
+ }
50
+ async function hasIdeAssets(projectRoot, ide) {
51
+ const report = await getAdapter(ide).inspect(projectRoot);
52
+ return validateReportAgainstManifest(report).ok;
53
+ }
54
+ export async function initProject(projectRoot, packageRoot, options = {}) {
55
+ const ide = options.ide ?? 'both';
56
+ const forceAssets = options.forceAssets ?? false;
57
+ const parityStrict = options.parityStrict ?? true;
58
+ const adapters = resolveAdapters(ide);
59
+ const initialized = await isProjectInitialized(projectRoot);
60
+ const migrationState = await detectMigrationState(projectRoot);
61
+ if (!initialized) {
62
+ await createDirectoryStructure(projectRoot);
63
+ await writeConfig(projectRoot);
64
+ }
65
+ const selectedIdeTargets = adapters.map((a) => a.ide);
66
+ const allTargetsHealthy = (await Promise.all(selectedIdeTargets.map((target) => hasIdeAssets(projectRoot, target)))).every(Boolean);
67
+ if (initialized && !forceAssets && allTargetsHealthy) {
68
+ return {
69
+ status: 'already_initialized',
70
+ message: `Project at ${projectRoot} is already initialized for ide=${ide} (state=${migrationState}). Use --force-assets to refresh managed assets.`,
71
+ };
72
+ }
73
+ for (const adapter of adapters) {
74
+ await adapter.generate({ projectRoot, packageRoot, forceAssets });
75
+ }
76
+ await updateGitignore(projectRoot, selectedIdeTargets);
77
+ if (parityStrict) {
78
+ for (const adapter of adapters) {
79
+ const report = await adapter.inspect(projectRoot);
80
+ const status = validateReportAgainstManifest(report);
81
+ if (!status.ok) {
82
+ throw new Error(`Parity strict check failed for ${adapter.ide}: ${status.deltas.map((d) => d.message).join('; ')}`);
83
+ }
84
+ }
85
+ }
86
+ return {
87
+ status: initialized ? 'updated_assets' : 'initialized',
88
+ message: initialized
89
+ ? `Updated ${ide} assets at ${projectRoot}`
90
+ : `Initialized specflow project at ${projectRoot}`,
91
+ };
92
+ }
93
+ async function updateGitignore(projectRoot, ideTargets) {
94
+ const gitignorePath = join(projectRoot, '.gitignore');
95
+ const markerStart = '# Added by specflow init (regeneratable assets)';
96
+ const markerEnd = '# End specflow init';
97
+ const lines = ideTargets.flatMap((target) => getRegeneratableIgnoreLines(target));
98
+ await appendManagedBlock(gitignorePath, markerStart, markerEnd, lines);
99
+ }
100
+ export function registerInitCommand(program) {
101
+ program
102
+ .command('init')
103
+ .description('Initialize a project with specflow directory structure and assets')
104
+ .option('--ide <target>', 'Target IDE assets: claude | cursor | codex | both | all', 'both')
105
+ .option('--force-assets', 'Refresh managed IDE assets even when project is initialized')
106
+ .option('--no-parity-strict', 'Disable strict parity validation after asset generation')
107
+ .action(async (opts) => {
108
+ const projectRoot = process.cwd();
109
+ const packageRoot = findPackageRoot();
110
+ const result = await initProject(projectRoot, packageRoot, {
111
+ ide: opts.ide ?? 'both',
112
+ forceAssets: opts.forceAssets ?? false,
113
+ parityStrict: opts.parityStrict ?? true,
114
+ });
115
+ if (result.status === 'already_initialized') {
116
+ console.warn(`Warning: ${result.message}`);
117
+ }
118
+ else {
119
+ console.info(result.message);
120
+ }
121
+ });
122
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * CLI command: specflow instructions <artifact-id> <change-name>
3
+ *
4
+ * Loads and displays instructions for creating an artifact.
5
+ */
6
+ import type { Command } from 'commander';
7
+ import type { ArtifactInstructions } from '../../core/artifact-graph/instruction-loader.js';
8
+ /**
9
+ * Gets instructions for creating an artifact within a change.
10
+ *
11
+ * @param artifactId - The artifact ID to get instructions for
12
+ * @param changeName - The name of the change
13
+ * @param projectRoot - Absolute path to the project root
14
+ * @returns The artifact instructions with dependencies and output path
15
+ * @throws When the artifact or change is not found
16
+ */
17
+ export declare function getInstructions(artifactId: string, changeName: string, projectRoot: string): Promise<ArtifactInstructions>;
18
+ /**
19
+ * Registers the `instructions` command with Commander.
20
+ */
21
+ export declare function registerInstructionsCommand(program: Command): void;
@@ -0,0 +1,75 @@
1
+ /**
2
+ * CLI command: specflow instructions <artifact-id> <change-name>
3
+ *
4
+ * Loads and displays instructions for creating an artifact.
5
+ */
6
+ import { join } from 'node:path';
7
+ import * as fs from 'node:fs';
8
+ import yaml from 'js-yaml';
9
+ import { resolveSchema } from '../../core/artifact-graph/resolver.js';
10
+ import { loadInstructions } from '../../core/artifact-graph/instruction-loader.js';
11
+ import { requireProjectRoot } from '../../utils/project-root.js';
12
+ /**
13
+ * Reads the project config to determine the schema name and optional context.
14
+ */
15
+ function readProjectConfig(projectRoot) {
16
+ const configPath = join(projectRoot, 'specflow', 'config.yaml');
17
+ try {
18
+ const content = fs.readFileSync(configPath, 'utf-8');
19
+ const parsed = yaml.load(content);
20
+ const schemaName = typeof parsed?.schema === 'string' ? parsed.schema : 'specflow';
21
+ const config = { context: typeof parsed?.context === 'string' ? parsed.context : undefined };
22
+ return { schemaName, config };
23
+ }
24
+ catch {
25
+ return { schemaName: 'specflow' };
26
+ }
27
+ }
28
+ /**
29
+ * Gets instructions for creating an artifact within a change.
30
+ *
31
+ * @param artifactId - The artifact ID to get instructions for
32
+ * @param changeName - The name of the change
33
+ * @param projectRoot - Absolute path to the project root
34
+ * @returns The artifact instructions with dependencies and output path
35
+ * @throws When the artifact or change is not found
36
+ */
37
+ export async function getInstructions(artifactId, changeName, projectRoot) {
38
+ const changeDir = join(projectRoot, 'specflow', 'changes', changeName);
39
+ if (!fs.existsSync(changeDir)) {
40
+ throw new Error(`Change "${changeName}" not found at ${changeDir}`);
41
+ }
42
+ const { schemaName, config } = readProjectConfig(projectRoot);
43
+ const schema = resolveSchema(schemaName, projectRoot);
44
+ return loadInstructions(artifactId, changeDir, schema, config);
45
+ }
46
+ /**
47
+ * Registers the `instructions` command with Commander.
48
+ */
49
+ export function registerInstructionsCommand(program) {
50
+ program
51
+ .command('instructions <artifact-id> <change-name>')
52
+ .description('Show instructions for creating an artifact')
53
+ .option('--json', 'Output as JSON')
54
+ .action(async (artifactId, changeName, opts) => {
55
+ const projectRoot = requireProjectRoot();
56
+ const instructions = await getInstructions(artifactId, changeName, projectRoot);
57
+ if (opts.json) {
58
+ console.info(JSON.stringify(instructions, null, 2));
59
+ }
60
+ else {
61
+ console.info(`Artifact: ${artifactId}`);
62
+ console.info(`Generates: ${instructions.generates}`);
63
+ console.info(`Description: ${instructions.description}`);
64
+ if (instructions.instruction) {
65
+ console.info(`\nInstruction:\n${instructions.instruction}`);
66
+ }
67
+ if (instructions.dependencies.length > 0) {
68
+ console.info('\nDependencies:');
69
+ for (const dep of instructions.dependencies) {
70
+ console.info(` - ${dep.id}: ${dep.description}`);
71
+ }
72
+ }
73
+ }
74
+ });
75
+ }
@@ -0,0 +1,10 @@
1
+ import type { Command } from 'commander';
2
+ import type { IdeTarget, ParityStatus } from '../../integrations/shared/types.js';
3
+ export interface ParityReportOutput {
4
+ readonly migrationState: string;
5
+ readonly reports: Readonly<Record<IdeTarget, ParityStatus>>;
6
+ readonly parity: ParityStatus;
7
+ readonly ok: boolean;
8
+ }
9
+ export declare function getParityReport(projectRoot: string): Promise<ParityReportOutput>;
10
+ export declare function registerParityReportCommand(program: Command): void;
@@ -0,0 +1,67 @@
1
+ import { ALL_ADAPTERS } from '../../integrations/shared/adapter-registry.js';
2
+ import { directoryExists } from '../../integrations/shared/asset-hash.js';
3
+ import { compareHealthyReports, validateReportAgainstManifest } from '../../integrations/shared/parity-comparator.js';
4
+ import { detectMigrationState } from '../../integrations/shared/migration-state.js';
5
+ import { join } from 'node:path';
6
+ import { requireProjectRoot } from '../../utils/project-root.js';
7
+ async function isIdeInstalled(projectRoot, ide) {
8
+ if (ide === 'claude') {
9
+ return directoryExists(join(projectRoot, '.claude'));
10
+ }
11
+ if (ide === 'cursor') {
12
+ return directoryExists(join(projectRoot, '.cursor'));
13
+ }
14
+ return directoryExists(join(projectRoot, '.agents'));
15
+ }
16
+ export async function getParityReport(projectRoot) {
17
+ const migrationState = await detectMigrationState(projectRoot);
18
+ const inspected = await Promise.all(ALL_ADAPTERS.map((adapter) => adapter.inspect(projectRoot)));
19
+ const reports = {
20
+ claude: validateReportAgainstManifest(inspected[0]),
21
+ cursor: validateReportAgainstManifest(inspected[1]),
22
+ codex: validateReportAgainstManifest(inspected[2]),
23
+ };
24
+ const installedFlags = await Promise.all(ALL_ADAPTERS.map((adapter) => isIdeInstalled(projectRoot, adapter.ide)));
25
+ const healthyReports = inspected.filter((report) => reports[report.ide].ok);
26
+ const parity = compareHealthyReports(healthyReports);
27
+ const manifestOk = ALL_ADAPTERS.every((adapter, index) => {
28
+ if (!installedFlags[index]) {
29
+ return true;
30
+ }
31
+ return reports[adapter.ide].ok;
32
+ });
33
+ return {
34
+ migrationState,
35
+ reports,
36
+ parity,
37
+ ok: manifestOk && parity.ok,
38
+ };
39
+ }
40
+ export function registerParityReportCommand(program) {
41
+ program
42
+ .command('parity-report')
43
+ .description('Compare Claude, Cursor, and Codex capability parity')
44
+ .option('--json', 'Output as JSON')
45
+ .action(async (opts) => {
46
+ const projectRoot = requireProjectRoot();
47
+ const report = await getParityReport(projectRoot);
48
+ if (opts.json) {
49
+ console.info(JSON.stringify(report, null, 2));
50
+ }
51
+ else {
52
+ console.info(`Migration state: ${report.migrationState}`);
53
+ console.info(`Parity: ${report.ok ? 'PASS' : 'FAIL'}`);
54
+ for (const delta of [
55
+ ...report.reports.claude.deltas,
56
+ ...report.reports.cursor.deltas,
57
+ ...report.reports.codex.deltas,
58
+ ...report.parity.deltas,
59
+ ]) {
60
+ console.info(`- [${delta.severity}] ${delta.message}`);
61
+ }
62
+ }
63
+ if (!report.ok) {
64
+ process.exitCode = 1;
65
+ }
66
+ });
67
+ }
@@ -0,0 +1,4 @@
1
+ import type { Command } from 'commander';
2
+ import { type InitOptions } from './init.js';
3
+ export declare function syncProjectAssets(projectRoot: string, options: InitOptions): Promise<void>;
4
+ export declare function registerSyncCommand(program: Command): void;
@@ -0,0 +1,40 @@
1
+ import { fileURLToPath } from 'node:url';
2
+ import { join } from 'node:path';
3
+ import * as fsSync from 'node:fs';
4
+ import { requireProjectRoot } from '../../utils/project-root.js';
5
+ import { initProject } from './init.js';
6
+ function findPackageRoot() {
7
+ const currentFile = fileURLToPath(import.meta.url);
8
+ let dir = join(currentFile, '..');
9
+ const root = '/';
10
+ while (dir !== root) {
11
+ if (fsSync.existsSync(join(dir, 'package.json'))) {
12
+ return dir;
13
+ }
14
+ dir = join(dir, '..');
15
+ }
16
+ throw new Error('Could not find package root (no package.json found in parent directories)');
17
+ }
18
+ export async function syncProjectAssets(projectRoot, options) {
19
+ const packageRoot = findPackageRoot();
20
+ await initProject(projectRoot, packageRoot, {
21
+ ide: options.ide ?? 'both',
22
+ forceAssets: true,
23
+ parityStrict: options.parityStrict ?? true,
24
+ });
25
+ }
26
+ export function registerSyncCommand(program) {
27
+ program
28
+ .command('sync')
29
+ .description('Sync managed IDE assets from specflow package')
30
+ .option('--ide <target>', 'Target IDE assets: claude | cursor | codex | both | all', 'both')
31
+ .option('--no-parity-strict', 'Disable strict parity validation after sync')
32
+ .action(async (opts) => {
33
+ const projectRoot = requireProjectRoot();
34
+ await syncProjectAssets(projectRoot, {
35
+ ide: opts.ide ?? 'both',
36
+ parityStrict: opts.parityStrict ?? true,
37
+ });
38
+ console.info(`Synced ${opts.ide ?? 'both'} assets`);
39
+ });
40
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * CLI command: specflow validate <file>
3
+ *
4
+ * Validates a spec file for structural correctness.
5
+ */
6
+ import type { Command } from 'commander';
7
+ import type { ValidationResult } from '../../core/validation/types.js';
8
+ /**
9
+ * Validates a spec file at the given path.
10
+ *
11
+ * Reads the file and runs the spec validator, returning structured results.
12
+ *
13
+ * @param filePath - Absolute path to the spec file
14
+ * @returns Validation result with valid flag and any errors
15
+ * @throws When the file cannot be read
16
+ */
17
+ export declare function validateSpecFile(filePath: string): Promise<ValidationResult>;
18
+ /**
19
+ * Registers the `validate` command with Commander.
20
+ */
21
+ export declare function registerValidateCommand(program: Command): void;