@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.
- package/LICENSE +31 -0
- package/README.md +442 -0
- package/bin/specflow.js +2 -0
- package/dist/cli/commands/change-archive.d.ts +26 -0
- package/dist/cli/commands/change-archive.js +50 -0
- package/dist/cli/commands/change-new.d.ts +18 -0
- package/dist/cli/commands/change-new.js +53 -0
- package/dist/cli/commands/change-phase.d.ts +33 -0
- package/dist/cli/commands/change-phase.js +66 -0
- package/dist/cli/commands/change-status.d.ts +35 -0
- package/dist/cli/commands/change-status.js +95 -0
- package/dist/cli/commands/doctor.d.ts +9 -0
- package/dist/cli/commands/doctor.js +48 -0
- package/dist/cli/commands/init.d.ts +16 -0
- package/dist/cli/commands/init.js +122 -0
- package/dist/cli/commands/instructions.d.ts +21 -0
- package/dist/cli/commands/instructions.js +75 -0
- package/dist/cli/commands/parity-report.d.ts +10 -0
- package/dist/cli/commands/parity-report.js +67 -0
- package/dist/cli/commands/sync.d.ts +4 -0
- package/dist/cli/commands/sync.js +40 -0
- package/dist/cli/commands/validate.d.ts +21 -0
- package/dist/cli/commands/validate.js +46 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +52 -0
- package/dist/core/archive.d.ts +39 -0
- package/dist/core/archive.js +154 -0
- package/dist/core/artifact-graph/graph.d.ts +60 -0
- package/dist/core/artifact-graph/graph.js +141 -0
- package/dist/core/artifact-graph/index.d.ts +10 -0
- package/dist/core/artifact-graph/index.js +7 -0
- package/dist/core/artifact-graph/instruction-loader.d.ts +45 -0
- package/dist/core/artifact-graph/instruction-loader.js +37 -0
- package/dist/core/artifact-graph/outputs.d.ts +35 -0
- package/dist/core/artifact-graph/outputs.js +48 -0
- package/dist/core/artifact-graph/resolver.d.ts +54 -0
- package/dist/core/artifact-graph/resolver.js +139 -0
- package/dist/core/artifact-graph/schema.d.ts +23 -0
- package/dist/core/artifact-graph/schema.js +115 -0
- package/dist/core/artifact-graph/state.d.ts +9 -0
- package/dist/core/artifact-graph/state.js +93 -0
- package/dist/core/artifact-graph/types.d.ts +123 -0
- package/dist/core/artifact-graph/types.js +29 -0
- package/dist/core/parsers/change-parser.d.ts +20 -0
- package/dist/core/parsers/change-parser.js +22 -0
- package/dist/core/parsers/index.d.ts +7 -0
- package/dist/core/parsers/index.js +4 -0
- package/dist/core/parsers/markdown-parser.d.ts +35 -0
- package/dist/core/parsers/markdown-parser.js +193 -0
- package/dist/core/parsers/requirement-blocks.d.ts +21 -0
- package/dist/core/parsers/requirement-blocks.js +57 -0
- package/dist/core/parsers/spec-structure.d.ts +22 -0
- package/dist/core/parsers/spec-structure.js +105 -0
- package/dist/core/specs-apply.d.ts +22 -0
- package/dist/core/specs-apply.js +165 -0
- package/dist/core/templates/index.d.ts +31 -0
- package/dist/core/templates/index.js +80 -0
- package/dist/core/templates/types.d.ts +4 -0
- package/dist/core/templates/types.js +1 -0
- package/dist/core/validation/constants.d.ts +14 -0
- package/dist/core/validation/constants.js +14 -0
- package/dist/core/validation/types.d.ts +13 -0
- package/dist/core/validation/types.js +6 -0
- package/dist/core/validation/validator.d.ts +24 -0
- package/dist/core/validation/validator.js +250 -0
- package/dist/integrations/claude/adapter.d.ts +2 -0
- package/dist/integrations/claude/adapter.js +106 -0
- package/dist/integrations/codex/adapter.d.ts +2 -0
- package/dist/integrations/codex/adapter.js +141 -0
- package/dist/integrations/cursor/adapter.d.ts +2 -0
- package/dist/integrations/cursor/adapter.js +126 -0
- package/dist/integrations/shared/adapter-registry.d.ts +4 -0
- package/dist/integrations/shared/adapter-registry.js +27 -0
- package/dist/integrations/shared/asset-copy.d.ts +2 -0
- package/dist/integrations/shared/asset-copy.js +44 -0
- package/dist/integrations/shared/asset-hash.d.ts +4 -0
- package/dist/integrations/shared/asset-hash.js +54 -0
- package/dist/integrations/shared/capability-evidence.d.ts +1 -0
- package/dist/integrations/shared/capability-evidence.js +42 -0
- package/dist/integrations/shared/command-catalog.d.ts +6 -0
- package/dist/integrations/shared/command-catalog.js +13 -0
- package/dist/integrations/shared/managed-assets.d.ts +2 -0
- package/dist/integrations/shared/managed-assets.js +21 -0
- package/dist/integrations/shared/marker-write.d.ts +3 -0
- package/dist/integrations/shared/marker-write.js +48 -0
- package/dist/integrations/shared/migration-state.d.ts +2 -0
- package/dist/integrations/shared/migration-state.js +67 -0
- package/dist/integrations/shared/parity-comparator.d.ts +6 -0
- package/dist/integrations/shared/parity-comparator.js +97 -0
- package/dist/integrations/shared/parity-manifest.d.ts +13 -0
- package/dist/integrations/shared/parity-manifest.js +58 -0
- package/dist/integrations/shared/runtime-assets.d.ts +2 -0
- package/dist/integrations/shared/runtime-assets.js +21 -0
- package/dist/integrations/shared/skill-renderer.d.ts +5 -0
- package/dist/integrations/shared/skill-renderer.js +77 -0
- package/dist/integrations/shared/types.d.ts +35 -0
- package/dist/integrations/shared/types.js +1 -0
- package/dist/utils/change-metadata.d.ts +38 -0
- package/dist/utils/change-metadata.js +82 -0
- package/dist/utils/change-utils.d.ts +53 -0
- package/dist/utils/change-utils.js +97 -0
- package/dist/utils/file-system.d.ts +35 -0
- package/dist/utils/file-system.js +67 -0
- package/dist/utils/index.d.ts +7 -0
- package/dist/utils/index.js +5 -0
- package/dist/utils/project-root.d.ts +22 -0
- package/dist/utils/project-root.js +44 -0
- package/dist/utils/task-progress.d.ts +39 -0
- package/dist/utils/task-progress.js +62 -0
- package/package.json +72 -0
- package/prompts/build/ecc-go-reviewer.md +80 -0
- package/prompts/build/ecc-java-reviewer.md +185 -0
- package/prompts/build/ecc-kotlin-reviewer.md +163 -0
- package/prompts/build/ecc-python-reviewer.md +102 -0
- package/prompts/build/ecc-rust-reviewer.md +98 -0
- package/prompts/build/ecc-typescript-reviewer.md +116 -0
- package/prompts/build/language-router.md +22 -0
- package/prompts/build/phase-a-plan.md +288 -0
- package/prompts/build/phase-b-execute.md +276 -0
- package/prompts/build/phase-b-review.md +119 -0
- package/prompts/build/phase-b-worktree.md +215 -0
- package/prompts/build/plan-document-reviewer.md +51 -0
- package/prompts/build/tdd.md +184 -0
- package/prompts/done/branch-finish.md +204 -0
- package/prompts/fix/debug.md +249 -0
- package/prompts/plan/design-draft.md +123 -0
- package/prompts/plan/proposal.md +85 -0
- package/prompts/plan/specs.md +114 -0
- package/prompts/plan/tasks-draft.md +123 -0
- package/prompts/reference/openspec/apply-instruction.md +2 -0
- package/prompts/reference/openspec/design-instruction.md +20 -0
- package/prompts/reference/openspec/proposal-instruction.md +18 -0
- package/prompts/reference/openspec/specs-instruction.md +47 -0
- package/prompts/reference/openspec/tasks-instruction.md +26 -0
- package/prompts/reference/specflow/example-design.md +284 -0
- package/prompts/reference/superpowers/anthropic-best-practices.md +1150 -0
- package/prompts/reference/superpowers/codex-tools.md +29 -0
- package/prompts/reference/superpowers/condition-based-waiting-example.ts +158 -0
- package/prompts/reference/superpowers/condition-based-waiting.md +115 -0
- package/prompts/reference/superpowers/defense-in-depth.md +122 -0
- package/prompts/reference/superpowers/find-polluter.sh +63 -0
- package/prompts/reference/superpowers/graphviz-conventions.dot +172 -0
- package/prompts/reference/superpowers/persuasion-principles.md +187 -0
- package/prompts/reference/superpowers/root-cause-tracing.md +169 -0
- package/prompts/reference/superpowers/testing-anti-patterns.md +299 -0
- package/prompts/reference/superpowers/testing-skills-with-subagents.md +384 -0
- package/prompts/reference/superpowers/using-superpowers.md +117 -0
- package/prompts/reference/superpowers/writing-skills.md +655 -0
- package/prompts/refine/brainstorm.md +241 -0
- package/prompts/refine/design-output.md +126 -0
- package/prompts/refine/spec-document-reviewer.md +51 -0
- package/prompts/refine/update-artifacts.md +185 -0
- package/prompts/review/code-review.md +223 -0
- package/prompts/shared/code-reviewer-prompt.md +98 -0
- package/prompts/shared/dispatching-parallel-agents.md +143 -0
- package/prompts/shared/executing-plans.md +67 -0
- package/prompts/shared/implementer-prompt.md +115 -0
- package/prompts/shared/receiving-code-review.md +174 -0
- package/prompts/shared/spec-reviewer-prompt.md +63 -0
- package/prompts/test/tdd.md +236 -0
- package/prompts/test/verification.md +129 -0
- package/prompts/verify/ecc-security-reviewer.md +112 -0
- package/prompts/verify/verification.md +196 -0
- package/schemas/specflow/schema.yaml +48 -0
- package/skills/specflow-build/SKILL.md +129 -0
- package/skills/specflow-done/SKILL.md +68 -0
- package/skills/specflow-fix/SKILL.md +74 -0
- package/skills/specflow-plan/SKILL.md +82 -0
- package/skills/specflow-refine/SKILL.md +128 -0
- package/skills/specflow-review/SKILL.md +40 -0
- package/skills/specflow-scan/SKILL.md +48 -0
- package/skills/specflow-snap/SKILL.md +46 -0
- package/skills/specflow-test/SKILL.md +48 -0
- package/skills/specflow-verify/SKILL.md +77 -0
- package/templates/design.md +19 -0
- package/templates/proposal.md +23 -0
- package/templates/spec.md +42 -0
- package/templates/tasks.md +9 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validation constants.
|
|
3
|
+
*
|
|
4
|
+
* Regex patterns and heading markers used during spec validation.
|
|
5
|
+
*/
|
|
6
|
+
export declare const SECTION_HEADER: RegExp;
|
|
7
|
+
export declare const REQUIREMENT_HEADER: RegExp;
|
|
8
|
+
export declare const SCENARIO_HEADER_CORRECT: RegExp;
|
|
9
|
+
export declare const SCENARIO_HEADER_WRONG_LEVEL: RegExp;
|
|
10
|
+
export declare const WHEN_LINE: RegExp;
|
|
11
|
+
export declare const THEN_LINE: RegExp;
|
|
12
|
+
export declare const REASON_LINE: RegExp;
|
|
13
|
+
export declare const FROM_LINE: RegExp;
|
|
14
|
+
export declare const TO_LINE: RegExp;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validation constants.
|
|
3
|
+
*
|
|
4
|
+
* Regex patterns and heading markers used during spec validation.
|
|
5
|
+
*/
|
|
6
|
+
export const SECTION_HEADER = /^##\s+(ADDED|MODIFIED|REMOVED|RENAMED)\s+Requirements\s*$/i;
|
|
7
|
+
export const REQUIREMENT_HEADER = /^###\s+Requirement:\s*(.+)\s*$/;
|
|
8
|
+
export const SCENARIO_HEADER_CORRECT = /^####\s+Scenario:\s*(.+)\s*$/;
|
|
9
|
+
export const SCENARIO_HEADER_WRONG_LEVEL = /^###\s+Scenario:\s*(.+)\s*$/;
|
|
10
|
+
export const WHEN_LINE = /^-\s+\*\*WHEN\*\*\s+(.+)$/;
|
|
11
|
+
export const THEN_LINE = /^-\s+\*\*THEN\*\*\s+(.+)$/;
|
|
12
|
+
export const REASON_LINE = /^\*\*Reason\*\*:\s*(.+)$/;
|
|
13
|
+
export const FROM_LINE = /^FROM:\s*(.+)$/;
|
|
14
|
+
export const TO_LINE = /^TO:\s*(.+)$/;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validation types.
|
|
3
|
+
*
|
|
4
|
+
* Defines the result and error shapes returned by spec validation.
|
|
5
|
+
*/
|
|
6
|
+
export interface ValidationError {
|
|
7
|
+
readonly message: string;
|
|
8
|
+
readonly line?: number;
|
|
9
|
+
}
|
|
10
|
+
export interface ValidationResult {
|
|
11
|
+
readonly valid: boolean;
|
|
12
|
+
readonly errors: readonly ValidationError[];
|
|
13
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spec validator.
|
|
3
|
+
*
|
|
4
|
+
* Validates delta-spec markdown content for structural correctness:
|
|
5
|
+
* - ADDED/MODIFIED sections must have requirements with scenarios
|
|
6
|
+
* - Scenarios must use #### heading (not ###)
|
|
7
|
+
* - Scenarios must have WHEN and THEN clauses
|
|
8
|
+
* - REMOVED sections must have a Reason
|
|
9
|
+
* - RENAMED sections must have FROM and TO lines
|
|
10
|
+
*/
|
|
11
|
+
import type { ValidationResult } from './types.js';
|
|
12
|
+
/**
|
|
13
|
+
* Validate a delta-spec markdown content string.
|
|
14
|
+
*
|
|
15
|
+
* Checks structural rules including:
|
|
16
|
+
* - ADDED/MODIFIED requirements must have scenarios with WHEN/THEN
|
|
17
|
+
* - Scenarios must use #### heading level
|
|
18
|
+
* - REMOVED requirements must have a Reason
|
|
19
|
+
* - RENAMED entries must have FROM and TO
|
|
20
|
+
*
|
|
21
|
+
* @param content - The raw markdown content of a delta spec
|
|
22
|
+
* @returns A ValidationResult with valid flag and any errors found
|
|
23
|
+
*/
|
|
24
|
+
export declare function validateSpec(content: string): ValidationResult;
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spec validator.
|
|
3
|
+
*
|
|
4
|
+
* Validates delta-spec markdown content for structural correctness:
|
|
5
|
+
* - ADDED/MODIFIED sections must have requirements with scenarios
|
|
6
|
+
* - Scenarios must use #### heading (not ###)
|
|
7
|
+
* - Scenarios must have WHEN and THEN clauses
|
|
8
|
+
* - REMOVED sections must have a Reason
|
|
9
|
+
* - RENAMED sections must have FROM and TO lines
|
|
10
|
+
*/
|
|
11
|
+
import { SECTION_HEADER, REQUIREMENT_HEADER, SCENARIO_HEADER_CORRECT, SCENARIO_HEADER_WRONG_LEVEL, WHEN_LINE, THEN_LINE, REASON_LINE, FROM_LINE, TO_LINE, } from './constants.js';
|
|
12
|
+
function splitIntoSections(allLines) {
|
|
13
|
+
const sections = [];
|
|
14
|
+
let current = null;
|
|
15
|
+
for (let i = 0; i < allLines.length; i++) {
|
|
16
|
+
const match = SECTION_HEADER.exec(allLines[i]);
|
|
17
|
+
if (match) {
|
|
18
|
+
if (current) {
|
|
19
|
+
sections.push({ kind: current.kind, startLine: current.startLine, lines: current.lines });
|
|
20
|
+
}
|
|
21
|
+
current = {
|
|
22
|
+
kind: match[1].toUpperCase(),
|
|
23
|
+
startLine: i,
|
|
24
|
+
lines: [],
|
|
25
|
+
};
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (current) {
|
|
29
|
+
current.lines.push(allLines[i]);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (current) {
|
|
33
|
+
sections.push({ kind: current.kind, startLine: current.startLine, lines: current.lines });
|
|
34
|
+
}
|
|
35
|
+
return sections;
|
|
36
|
+
}
|
|
37
|
+
function validateRequirementSection(section, errors) {
|
|
38
|
+
const { lines, startLine } = section;
|
|
39
|
+
// Check for scenarios at wrong heading level (### instead of ####)
|
|
40
|
+
for (let i = 0; i < lines.length; i++) {
|
|
41
|
+
if (SCENARIO_HEADER_WRONG_LEVEL.test(lines[i])) {
|
|
42
|
+
errors.push({
|
|
43
|
+
message: `Scenario has incorrect heading level (use #### not ###)`,
|
|
44
|
+
line: startLine + 1 + i + 1,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// Check for orphan scenarios before any requirement header
|
|
49
|
+
let foundFirstReq = false;
|
|
50
|
+
for (let i = 0; i < lines.length; i++) {
|
|
51
|
+
if (REQUIREMENT_HEADER.test(lines[i])) {
|
|
52
|
+
foundFirstReq = true;
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
if (SCENARIO_HEADER_CORRECT.test(lines[i])) {
|
|
56
|
+
errors.push({
|
|
57
|
+
message: `Scenario found with missing requirement header`,
|
|
58
|
+
line: startLine + 1 + i + 1,
|
|
59
|
+
});
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// If section has no requirement headers at all but has content, skip req parsing
|
|
64
|
+
const hasAnyRequirement = lines.some((l) => REQUIREMENT_HEADER.test(l));
|
|
65
|
+
if (!hasAnyRequirement) {
|
|
66
|
+
// If there are scenarios without a requirement, we already flagged it above
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
// Parse requirements and validate each has at least one scenario with WHEN/THEN
|
|
70
|
+
let currentReqName = null;
|
|
71
|
+
let currentReqLine = 0;
|
|
72
|
+
let scenarioCount = 0;
|
|
73
|
+
let scenariosForCurrent = [];
|
|
74
|
+
const flushReq = () => {
|
|
75
|
+
if (currentReqName !== null) {
|
|
76
|
+
if (scenarioCount === 0) {
|
|
77
|
+
errors.push({
|
|
78
|
+
message: `Requirement "${currentReqName}" has no scenarios`,
|
|
79
|
+
line: currentReqLine,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
for (const sc of scenariosForCurrent) {
|
|
83
|
+
if (!sc.hasWhen) {
|
|
84
|
+
errors.push({
|
|
85
|
+
message: `Scenario "${sc.name}" is missing WHEN clause`,
|
|
86
|
+
line: sc.line,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
if (!sc.hasThen) {
|
|
90
|
+
errors.push({
|
|
91
|
+
message: `Scenario "${sc.name}" is missing THEN clause`,
|
|
92
|
+
line: sc.line,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
let currentScenario = null;
|
|
99
|
+
for (let i = 0; i < lines.length; i++) {
|
|
100
|
+
const lineNum = startLine + 1 + i + 1;
|
|
101
|
+
const line = lines[i];
|
|
102
|
+
const reqMatch = REQUIREMENT_HEADER.exec(line);
|
|
103
|
+
if (reqMatch) {
|
|
104
|
+
// Flush previous scenario into list
|
|
105
|
+
if (currentScenario) {
|
|
106
|
+
scenariosForCurrent.push(currentScenario);
|
|
107
|
+
currentScenario = null;
|
|
108
|
+
}
|
|
109
|
+
flushReq();
|
|
110
|
+
currentReqName = reqMatch[1].trim();
|
|
111
|
+
currentReqLine = lineNum;
|
|
112
|
+
scenarioCount = 0;
|
|
113
|
+
scenariosForCurrent = [];
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
const scMatch = SCENARIO_HEADER_CORRECT.exec(line);
|
|
117
|
+
if (scMatch && currentReqName !== null) {
|
|
118
|
+
if (currentScenario) {
|
|
119
|
+
scenariosForCurrent.push(currentScenario);
|
|
120
|
+
}
|
|
121
|
+
scenarioCount++;
|
|
122
|
+
currentScenario = { name: scMatch[1].trim(), line: lineNum, hasWhen: false, hasThen: false };
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (currentScenario !== null) {
|
|
126
|
+
const hasWhen = currentScenario.hasWhen || WHEN_LINE.test(line);
|
|
127
|
+
const hasThen = currentScenario.hasThen || THEN_LINE.test(line);
|
|
128
|
+
if (hasWhen !== currentScenario.hasWhen || hasThen !== currentScenario.hasThen) {
|
|
129
|
+
currentScenario = {
|
|
130
|
+
name: currentScenario.name,
|
|
131
|
+
line: currentScenario.line,
|
|
132
|
+
hasWhen,
|
|
133
|
+
hasThen,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// Flush last scenario and requirement
|
|
139
|
+
if (currentScenario) {
|
|
140
|
+
scenariosForCurrent.push(currentScenario);
|
|
141
|
+
}
|
|
142
|
+
flushReq();
|
|
143
|
+
}
|
|
144
|
+
function validateRemovedSection(section, errors) {
|
|
145
|
+
const { lines, startLine } = section;
|
|
146
|
+
let currentReqName = null;
|
|
147
|
+
let currentReqLine = 0;
|
|
148
|
+
let hasReason = false;
|
|
149
|
+
const flushRemoved = () => {
|
|
150
|
+
if (currentReqName !== null && !hasReason) {
|
|
151
|
+
errors.push({
|
|
152
|
+
message: `REMOVED requirement "${currentReqName}" is missing Reason`,
|
|
153
|
+
line: currentReqLine,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
for (let i = 0; i < lines.length; i++) {
|
|
158
|
+
const lineNum = startLine + 1 + i + 1;
|
|
159
|
+
const line = lines[i];
|
|
160
|
+
const reqMatch = REQUIREMENT_HEADER.exec(line);
|
|
161
|
+
if (reqMatch) {
|
|
162
|
+
flushRemoved();
|
|
163
|
+
currentReqName = reqMatch[1].trim();
|
|
164
|
+
currentReqLine = lineNum;
|
|
165
|
+
hasReason = false;
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (REASON_LINE.test(line)) {
|
|
169
|
+
hasReason = true;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
flushRemoved();
|
|
173
|
+
}
|
|
174
|
+
function validateRenamedSection(section, errors) {
|
|
175
|
+
const { lines, startLine } = section;
|
|
176
|
+
let hasFrom = false;
|
|
177
|
+
let hasTo = false;
|
|
178
|
+
let fromLine = 0;
|
|
179
|
+
for (let i = 0; i < lines.length; i++) {
|
|
180
|
+
const line = lines[i];
|
|
181
|
+
if (FROM_LINE.test(line)) {
|
|
182
|
+
// If we had a previous FROM without TO, report it
|
|
183
|
+
if (hasFrom && !hasTo) {
|
|
184
|
+
errors.push({
|
|
185
|
+
message: `RENAMED entry is missing TO (has FROM but no TO)`,
|
|
186
|
+
line: startLine + 1 + fromLine + 1,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
hasFrom = true;
|
|
190
|
+
hasTo = false;
|
|
191
|
+
fromLine = i;
|
|
192
|
+
}
|
|
193
|
+
if (TO_LINE.test(line)) {
|
|
194
|
+
hasTo = true;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
// Check last pair
|
|
198
|
+
if (hasFrom && !hasTo) {
|
|
199
|
+
errors.push({
|
|
200
|
+
message: `RENAMED entry is missing TO (has FROM but no TO)`,
|
|
201
|
+
line: startLine + 1 + fromLine + 1,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
// If no FROM found at all
|
|
205
|
+
if (!hasFrom) {
|
|
206
|
+
const hasContent = lines.some((l) => l.trim().length > 0);
|
|
207
|
+
if (hasContent) {
|
|
208
|
+
errors.push({
|
|
209
|
+
message: `RENAMED section is missing FROM/TO entries`,
|
|
210
|
+
line: startLine + 1,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Validate a delta-spec markdown content string.
|
|
217
|
+
*
|
|
218
|
+
* Checks structural rules including:
|
|
219
|
+
* - ADDED/MODIFIED requirements must have scenarios with WHEN/THEN
|
|
220
|
+
* - Scenarios must use #### heading level
|
|
221
|
+
* - REMOVED requirements must have a Reason
|
|
222
|
+
* - RENAMED entries must have FROM and TO
|
|
223
|
+
*
|
|
224
|
+
* @param content - The raw markdown content of a delta spec
|
|
225
|
+
* @returns A ValidationResult with valid flag and any errors found
|
|
226
|
+
*/
|
|
227
|
+
export function validateSpec(content) {
|
|
228
|
+
const normalized = content.replace(/\r\n?/g, '\n');
|
|
229
|
+
const allLines = normalized.split('\n');
|
|
230
|
+
const errors = [];
|
|
231
|
+
const sections = splitIntoSections(allLines);
|
|
232
|
+
for (const section of sections) {
|
|
233
|
+
switch (section.kind) {
|
|
234
|
+
case 'ADDED':
|
|
235
|
+
case 'MODIFIED':
|
|
236
|
+
validateRequirementSection(section, errors);
|
|
237
|
+
break;
|
|
238
|
+
case 'REMOVED':
|
|
239
|
+
validateRemovedSection(section, errors);
|
|
240
|
+
break;
|
|
241
|
+
case 'RENAMED':
|
|
242
|
+
validateRenamedSection(section, errors);
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return {
|
|
247
|
+
valid: errors.length === 0,
|
|
248
|
+
errors,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
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, hashDirectoryTree } 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 generateCommandAlias(commandName, description) {
|
|
17
|
+
return [
|
|
18
|
+
'---',
|
|
19
|
+
`description: "${description}"`,
|
|
20
|
+
'---',
|
|
21
|
+
`Invoke the specflow:${commandName} skill.`,
|
|
22
|
+
'',
|
|
23
|
+
].join('\n');
|
|
24
|
+
}
|
|
25
|
+
async function generateClaude(ctx) {
|
|
26
|
+
const { projectRoot, packageRoot } = ctx;
|
|
27
|
+
const skillsSourceDir = join(packageRoot, 'skills');
|
|
28
|
+
const skillsDestDir = join(projectRoot, '.claude', 'skills');
|
|
29
|
+
const commandsDestDir = join(projectRoot, '.claude', 'commands', 'specflow');
|
|
30
|
+
await fs.mkdir(commandsDestDir, { recursive: true });
|
|
31
|
+
for (const command of COMMAND_CATALOG) {
|
|
32
|
+
const skillDirName = `specflow-${command.id}`;
|
|
33
|
+
const srcSkillMd = join(skillsSourceDir, skillDirName, 'SKILL.md');
|
|
34
|
+
const destSkillDir = join(skillsDestDir, skillDirName);
|
|
35
|
+
await fs.mkdir(destSkillDir, { recursive: true });
|
|
36
|
+
let description = command.description;
|
|
37
|
+
try {
|
|
38
|
+
const content = await fs.readFile(srcSkillMd, 'utf-8');
|
|
39
|
+
await fs.writeFile(join(destSkillDir, 'SKILL.md'), renderIdeContent(content, 'claude'), 'utf-8');
|
|
40
|
+
description = extractSkillDescription(content, command.description);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
const placeholder = [
|
|
44
|
+
'---',
|
|
45
|
+
`name: ${skillDirName}`,
|
|
46
|
+
`description: "${command.description}"`,
|
|
47
|
+
'---',
|
|
48
|
+
'',
|
|
49
|
+
`# SpecFlow: ${command.id}`,
|
|
50
|
+
'',
|
|
51
|
+
'Skill content pending.',
|
|
52
|
+
'',
|
|
53
|
+
].join('\n');
|
|
54
|
+
await fs.writeFile(join(destSkillDir, 'SKILL.md'), renderIdeContent(placeholder, 'claude'), 'utf-8');
|
|
55
|
+
}
|
|
56
|
+
await fs.writeFile(join(commandsDestDir, `${command.id}.md`), generateCommandAlias(command.id, description), 'utf-8');
|
|
57
|
+
}
|
|
58
|
+
const runtimePaths = await copyRuntimeAssets(packageRoot, projectRoot, 'claude');
|
|
59
|
+
return {
|
|
60
|
+
ide: 'claude',
|
|
61
|
+
generatedPaths: ['.claude/skills', '.claude/commands/specflow', ...runtimePaths],
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
async function inspectClaude(projectRoot) {
|
|
65
|
+
const commandDir = join(projectRoot, '.claude', 'commands', 'specflow');
|
|
66
|
+
const skillDir = join(projectRoot, '.claude', 'skills');
|
|
67
|
+
const promptsDir = join(projectRoot, '.claude', 'specflow', 'prompts');
|
|
68
|
+
const schemasDir = join(projectRoot, '.claude', 'specflow', 'schemas');
|
|
69
|
+
const templatesDir = join(projectRoot, '.claude', 'specflow', 'templates');
|
|
70
|
+
const supported = [];
|
|
71
|
+
const skillContentsByCommand = {};
|
|
72
|
+
for (const command of COMMAND_CATALOG) {
|
|
73
|
+
try {
|
|
74
|
+
const stat = await fs.stat(join(commandDir, `${command.id}.md`));
|
|
75
|
+
if (stat.isFile()) {
|
|
76
|
+
supported.push(command.id);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
// ignore
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
skillContentsByCommand[command.id] = await fs.readFile(join(skillDir, `specflow-${command.id}`, 'SKILL.md'), 'utf-8');
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
skillContentsByCommand[command.id] = '';
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
ide: 'claude',
|
|
91
|
+
supportedCommandIds: supported,
|
|
92
|
+
supportedCapabilityIds: deriveCapabilityIds(supported, skillContentsByCommand),
|
|
93
|
+
assets: {
|
|
94
|
+
commandsDir: { exists: await directoryExists(commandDir), hash: await hashDirectoryTree(commandDir) },
|
|
95
|
+
skillsDir: { exists: await directoryExists(skillDir), hash: await hashDirectoryTree(skillDir) },
|
|
96
|
+
runtimePromptsDir: { exists: await directoryExists(promptsDir), hash: await hashDirectoryTree(promptsDir) },
|
|
97
|
+
runtimeSchemasDir: { exists: await directoryExists(schemasDir), hash: await hashDirectoryTree(schemasDir) },
|
|
98
|
+
runtimeTemplatesDir: { exists: await directoryExists(templatesDir), hash: await hashDirectoryTree(templatesDir) },
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
export const claudeAdapter = {
|
|
103
|
+
ide: 'claude',
|
|
104
|
+
generate: generateClaude,
|
|
105
|
+
inspect: inspectClaude,
|
|
106
|
+
};
|
|
@@ -0,0 +1,141 @@
|
|
|
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, hashDirectoryTree, hashFile } from '../shared/asset-hash.js';
|
|
5
|
+
import { deriveCapabilityIds } from '../shared/capability-evidence.js';
|
|
6
|
+
import { managedBlockPresent, upsertManagedBlock } from '../shared/marker-write.js';
|
|
7
|
+
import { copyRuntimeAssets } from '../shared/runtime-assets.js';
|
|
8
|
+
import { renderIdeContent, validateSkillFrontmatter } from '../shared/skill-renderer.js';
|
|
9
|
+
const CODEX_AGENTS_MARKER_START = '# Added by specflow init (Codex context)';
|
|
10
|
+
const CODEX_AGENTS_MARKER_END = '# End specflow init (Codex context)';
|
|
11
|
+
function extractSkillDescription(content, fallback) {
|
|
12
|
+
const frontmatterMatch = content.match(/^---\s*\n([\s\S]*?)\n---/);
|
|
13
|
+
if (!frontmatterMatch) {
|
|
14
|
+
return fallback;
|
|
15
|
+
}
|
|
16
|
+
const descMatch = frontmatterMatch[1].match(/description:\s*["']?(.+?)["']?\s*$/m);
|
|
17
|
+
return descMatch ? descMatch[1] : fallback;
|
|
18
|
+
}
|
|
19
|
+
function titleCaseCommandId(commandId) {
|
|
20
|
+
return commandId.charAt(0).toUpperCase() + commandId.slice(1);
|
|
21
|
+
}
|
|
22
|
+
function renderOpenAiYaml(commandId, description) {
|
|
23
|
+
return [
|
|
24
|
+
'interface:',
|
|
25
|
+
` display_name: "SpecFlow: ${titleCaseCommandId(commandId)}"`,
|
|
26
|
+
` short_description: "${description.replace(/"/g, '\\"')}"`,
|
|
27
|
+
'policy:',
|
|
28
|
+
' allow_implicit_invocation: true',
|
|
29
|
+
'',
|
|
30
|
+
].join('\n');
|
|
31
|
+
}
|
|
32
|
+
function renderCodexAgentsBlock() {
|
|
33
|
+
return [
|
|
34
|
+
'# SpecFlow Codex Context',
|
|
35
|
+
'',
|
|
36
|
+
'- Respect specflow phase gates: plan -> refined -> built -> archived',
|
|
37
|
+
'- Never bypass CLI validation and archive checks',
|
|
38
|
+
'- Use artifacts under specflow/changes/<change>/ before coding',
|
|
39
|
+
'- Workflow skills: $specflow-plan, $specflow-refine, $specflow-build, $specflow-review, $specflow-test, $specflow-verify, $specflow-done, $specflow-fix, $specflow-snap',
|
|
40
|
+
'- Runtime prompts live under .agents/specflow/prompts/',
|
|
41
|
+
'',
|
|
42
|
+
].join('\n');
|
|
43
|
+
}
|
|
44
|
+
async function generateCodex(ctx) {
|
|
45
|
+
const { projectRoot, packageRoot } = ctx;
|
|
46
|
+
const skillsSourceDir = join(packageRoot, 'skills');
|
|
47
|
+
const skillsDestDir = join(projectRoot, '.agents', 'skills');
|
|
48
|
+
for (const command of COMMAND_CATALOG) {
|
|
49
|
+
const skillDirName = `specflow-${command.id}`;
|
|
50
|
+
const srcSkillMd = join(skillsSourceDir, skillDirName, 'SKILL.md');
|
|
51
|
+
const destSkillDir = join(skillsDestDir, skillDirName);
|
|
52
|
+
const agentsMetaDir = join(destSkillDir, 'agents');
|
|
53
|
+
await fs.mkdir(agentsMetaDir, { recursive: true });
|
|
54
|
+
let description = command.description;
|
|
55
|
+
try {
|
|
56
|
+
const content = await fs.readFile(srcSkillMd, 'utf-8');
|
|
57
|
+
const rendered = renderIdeContent(content, 'codex');
|
|
58
|
+
validateSkillFrontmatter(rendered, skillDirName);
|
|
59
|
+
await fs.writeFile(join(destSkillDir, 'SKILL.md'), rendered, 'utf-8');
|
|
60
|
+
description = extractSkillDescription(content, command.description);
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
if (error instanceof Error && error.message.includes('frontmatter')) {
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
const placeholder = [
|
|
67
|
+
'---',
|
|
68
|
+
`name: ${skillDirName}`,
|
|
69
|
+
`description: "${command.description}"`,
|
|
70
|
+
'---',
|
|
71
|
+
'',
|
|
72
|
+
`# SpecFlow: ${command.id}`,
|
|
73
|
+
'',
|
|
74
|
+
'Skill content pending.',
|
|
75
|
+
'',
|
|
76
|
+
].join('\n');
|
|
77
|
+
const rendered = renderIdeContent(placeholder, 'codex');
|
|
78
|
+
validateSkillFrontmatter(rendered, skillDirName);
|
|
79
|
+
await fs.writeFile(join(destSkillDir, 'SKILL.md'), rendered, 'utf-8');
|
|
80
|
+
}
|
|
81
|
+
await fs.writeFile(join(agentsMetaDir, 'openai.yaml'), renderOpenAiYaml(command.id, description), 'utf-8');
|
|
82
|
+
}
|
|
83
|
+
const runtimePaths = await copyRuntimeAssets(packageRoot, projectRoot, 'codex');
|
|
84
|
+
await upsertManagedBlock(join(projectRoot, 'AGENTS.md'), CODEX_AGENTS_MARKER_START, CODEX_AGENTS_MARKER_END, renderCodexAgentsBlock().split('\n'));
|
|
85
|
+
return {
|
|
86
|
+
ide: 'codex',
|
|
87
|
+
generatedPaths: ['.agents/skills', 'AGENTS.md', ...runtimePaths],
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
async function inspectCodex(projectRoot) {
|
|
91
|
+
const skillDir = join(projectRoot, '.agents', 'skills');
|
|
92
|
+
const agentsFile = join(projectRoot, 'AGENTS.md');
|
|
93
|
+
const promptsDir = join(projectRoot, '.agents', 'specflow', 'prompts');
|
|
94
|
+
const schemasDir = join(projectRoot, '.agents', 'specflow', 'schemas');
|
|
95
|
+
const templatesDir = join(projectRoot, '.agents', 'specflow', 'templates');
|
|
96
|
+
let agentsFileExists = false;
|
|
97
|
+
try {
|
|
98
|
+
const agentsContent = await fs.readFile(agentsFile, 'utf-8');
|
|
99
|
+
agentsFileExists = managedBlockPresent(agentsContent, CODEX_AGENTS_MARKER_START, CODEX_AGENTS_MARKER_END);
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
agentsFileExists = false;
|
|
103
|
+
}
|
|
104
|
+
const supported = [];
|
|
105
|
+
const skillContentsByCommand = {};
|
|
106
|
+
for (const command of COMMAND_CATALOG) {
|
|
107
|
+
const skillPath = join(skillDir, `specflow-${command.id}`, 'SKILL.md');
|
|
108
|
+
try {
|
|
109
|
+
const stat = await fs.stat(skillPath);
|
|
110
|
+
if (stat.isFile()) {
|
|
111
|
+
supported.push(command.id);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
// ignore
|
|
116
|
+
}
|
|
117
|
+
try {
|
|
118
|
+
skillContentsByCommand[command.id] = await fs.readFile(skillPath, 'utf-8');
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
skillContentsByCommand[command.id] = '';
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
ide: 'codex',
|
|
126
|
+
supportedCommandIds: supported,
|
|
127
|
+
supportedCapabilityIds: deriveCapabilityIds(supported, skillContentsByCommand),
|
|
128
|
+
assets: {
|
|
129
|
+
skillsDir: { exists: await directoryExists(skillDir), hash: await hashDirectoryTree(skillDir) },
|
|
130
|
+
agentsFile: { exists: agentsFileExists, hash: agentsFileExists ? await hashFile(agentsFile) : undefined },
|
|
131
|
+
runtimePromptsDir: { exists: await directoryExists(promptsDir), hash: await hashDirectoryTree(promptsDir) },
|
|
132
|
+
runtimeSchemasDir: { exists: await directoryExists(schemasDir), hash: await hashDirectoryTree(schemasDir) },
|
|
133
|
+
runtimeTemplatesDir: { exists: await directoryExists(templatesDir), hash: await hashDirectoryTree(templatesDir) },
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
export const codexAdapter = {
|
|
138
|
+
ide: 'codex',
|
|
139
|
+
generate: generateCodex,
|
|
140
|
+
inspect: inspectCodex,
|
|
141
|
+
};
|