@gordon.gan/specflow 1.0.1 → 1.1.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/README.md +275 -317
- package/dist/cli/commands/change-archive.js +1 -1
- package/dist/cli/commands/change-new.js +1 -1
- package/dist/cli/commands/change-phase.d.ts +1 -1
- package/dist/cli/commands/change-phase.js +7 -6
- package/dist/core/archive.d.ts +2 -2
- package/dist/core/archive.js +5 -5
- package/dist/core/artifact-graph/types.d.ts +2 -2
- package/dist/integrations/claude/adapter.js +2 -0
- package/dist/integrations/codex/adapter.js +4 -1
- package/dist/integrations/cursor/adapter.js +4 -1
- package/dist/integrations/shared/capability-evidence.js +0 -2
- package/dist/integrations/shared/command-catalog.js +0 -1
- package/dist/integrations/shared/parity-manifest.js +0 -2
- package/dist/integrations/shared/phase-context.d.ts +6 -0
- package/dist/integrations/shared/phase-context.js +9 -0
- package/dist/integrations/shared/retired-commands.d.ts +10 -0
- package/dist/integrations/shared/retired-commands.js +42 -0
- package/dist/utils/change-metadata.d.ts +11 -3
- package/dist/utils/change-metadata.js +33 -5
- package/dist/utils/change-utils.d.ts +1 -1
- package/dist/utils/change-utils.js +1 -1
- package/package.json +1 -4
- package/prompts/apply/phase-a-plan.md +1 -1
- package/prompts/propose/design-draft.md +4 -4
- package/prompts/propose/tasks-draft.md +1 -1
- package/prompts/reference/specflow/example-design.md +1 -1
- package/prompts/refine/brainstorm.md +2 -2
- package/schemas/specflow/schema.yaml +1 -6
- package/skills/specflow-apply/SKILL.md +2 -2
- package/skills/specflow-archive/SKILL.md +1 -1
- package/skills/specflow-explore/SKILL.md +1 -1
- package/skills/specflow-fix/SKILL.md +2 -2
- package/skills/specflow-propose/SKILL.md +1 -1
- package/skills/specflow-refine/SKILL.md +5 -5
- package/skills/specflow-snap/SKILL.md +1 -1
- package/skills/specflow-scan/SKILL.md +0 -48
|
@@ -39,7 +39,7 @@ export function registerChangeArchiveCommand(changeCmd) {
|
|
|
39
39
|
changeCmd
|
|
40
40
|
.command('archive <name>')
|
|
41
41
|
.description('Archive a completed change')
|
|
42
|
-
.option('--force', 'Archive even if the change is not in phase=
|
|
42
|
+
.option('--force', 'Archive even if the change is not in phase=apply')
|
|
43
43
|
.action(async (name, opts) => {
|
|
44
44
|
const projectRoot = requireProjectRoot();
|
|
45
45
|
const result = await archiveChangeCommand(name, projectRoot, {
|
|
@@ -28,6 +28,6 @@ export declare function setPhase(name: string, phase: string, projectRoot: strin
|
|
|
28
28
|
*
|
|
29
29
|
* Usage:
|
|
30
30
|
* specflow change phase <name> # print current phase
|
|
31
|
-
* specflow change phase <name> --set
|
|
31
|
+
* specflow change phase <name> --set refined # update phase
|
|
32
32
|
*/
|
|
33
33
|
export declare function registerChangePhaseCommand(changeCmd: Command): void;
|
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Read or update the lifecycle phase of a change.
|
|
5
5
|
*/
|
|
6
|
-
import { CHANGE_PHASES, getChangeMetadata, updatePhase, } from '../../utils/change-utils.js';
|
|
6
|
+
import { CHANGE_PHASES, getChangeMetadata, normalizeChangePhase, updatePhase, } from '../../utils/change-utils.js';
|
|
7
7
|
import { requireProjectRoot } from '../../utils/project-root.js';
|
|
8
8
|
/**
|
|
9
9
|
* Returns true if the candidate value is a valid ChangePhase.
|
|
10
10
|
*/
|
|
11
|
-
function
|
|
12
|
-
return
|
|
11
|
+
function resolveChangePhase(value) {
|
|
12
|
+
return normalizeChangePhase(value);
|
|
13
13
|
}
|
|
14
14
|
/**
|
|
15
15
|
* Reads the current phase of a change.
|
|
@@ -35,18 +35,19 @@ export async function getPhase(name, projectRoot) {
|
|
|
35
35
|
* @throws When the phase is invalid or the change does not exist
|
|
36
36
|
*/
|
|
37
37
|
export async function setPhase(name, phase, projectRoot) {
|
|
38
|
-
|
|
38
|
+
const resolved = resolveChangePhase(phase);
|
|
39
|
+
if (resolved === null) {
|
|
39
40
|
const valid = CHANGE_PHASES.join(' | ');
|
|
40
41
|
throw new Error(`Invalid phase "${phase}": expected one of ${valid}`);
|
|
41
42
|
}
|
|
42
|
-
await updatePhase(name,
|
|
43
|
+
await updatePhase(name, resolved, projectRoot);
|
|
43
44
|
}
|
|
44
45
|
/**
|
|
45
46
|
* Registers the `change phase` subcommand with Commander.
|
|
46
47
|
*
|
|
47
48
|
* Usage:
|
|
48
49
|
* specflow change phase <name> # print current phase
|
|
49
|
-
* specflow change phase <name> --set
|
|
50
|
+
* specflow change phase <name> --set refined # update phase
|
|
50
51
|
*/
|
|
51
52
|
export function registerChangePhaseCommand(changeCmd) {
|
|
52
53
|
changeCmd
|
package/dist/core/archive.d.ts
CHANGED
|
@@ -13,7 +13,7 @@ export interface ArchiveResult {
|
|
|
13
13
|
}
|
|
14
14
|
export interface ArchiveOptions {
|
|
15
15
|
/**
|
|
16
|
-
* If true, archive even when the change is not in phase=
|
|
16
|
+
* If true, archive even when the change is not in phase=apply.
|
|
17
17
|
* A warning is emitted to stderr when force is applied.
|
|
18
18
|
*/
|
|
19
19
|
readonly force?: boolean;
|
|
@@ -22,7 +22,7 @@ export interface ArchiveOptions {
|
|
|
22
22
|
* Archive a completed change.
|
|
23
23
|
*
|
|
24
24
|
* Steps:
|
|
25
|
-
* 0. Validate phase gate: require phase=
|
|
25
|
+
* 0. Validate phase gate: require phase=apply unless options.force is true
|
|
26
26
|
* 1. Find all delta spec files in `specflow/changes/<name>/specs/`
|
|
27
27
|
* 2. Validate each delta spec
|
|
28
28
|
* 3. For each delta spec, find the corresponding main spec in `specflow/specs/`
|
package/dist/core/archive.js
CHANGED
|
@@ -54,7 +54,7 @@ function todayDatePrefix() {
|
|
|
54
54
|
* Archive a completed change.
|
|
55
55
|
*
|
|
56
56
|
* Steps:
|
|
57
|
-
* 0. Validate phase gate: require phase=
|
|
57
|
+
* 0. Validate phase gate: require phase=apply unless options.force is true
|
|
58
58
|
* 1. Find all delta spec files in `specflow/changes/<name>/specs/`
|
|
59
59
|
* 2. Validate each delta spec
|
|
60
60
|
* 3. For each delta spec, find the corresponding main spec in `specflow/specs/`
|
|
@@ -72,19 +72,19 @@ export async function archiveChange(changeName, projectRoot, options = {}) {
|
|
|
72
72
|
const changeDir = join(projectRoot, 'specflow', 'changes', changeName);
|
|
73
73
|
const deltaSpecsDir = join(changeDir, 'specs');
|
|
74
74
|
const mainSpecsDir = join(projectRoot, 'specflow', 'specs');
|
|
75
|
-
// 0. Phase gate: require phase=
|
|
75
|
+
// 0. Phase gate: require phase=apply unless --force
|
|
76
76
|
const metadata = await readChangeMetadata(changeDir);
|
|
77
77
|
const currentPhase = metadata?.phase;
|
|
78
|
-
if (currentPhase !== '
|
|
78
|
+
if (currentPhase !== 'apply' && !options.force) {
|
|
79
79
|
const phaseLabel = currentPhase ?? 'unknown';
|
|
80
80
|
return {
|
|
81
81
|
success: false,
|
|
82
82
|
errors: [
|
|
83
|
-
`Cannot archive: change '${changeName}' is in phase '${phaseLabel}', expected '
|
|
83
|
+
`Cannot archive: change '${changeName}' is in phase '${phaseLabel}', expected 'apply'. Complete '/specflow:apply' first, or pass '--force' to archive anyway.`,
|
|
84
84
|
],
|
|
85
85
|
};
|
|
86
86
|
}
|
|
87
|
-
if (options.force && currentPhase !== '
|
|
87
|
+
if (options.force && currentPhase !== 'apply') {
|
|
88
88
|
const phaseLabel = currentPhase ?? 'unknown';
|
|
89
89
|
console.warn(`Warning: archiving "${changeName}" in phase ${phaseLabel} with --force. ` +
|
|
90
90
|
`Consider running /specflow:apply first.`);
|
|
@@ -86,12 +86,12 @@ export declare const SchemaYamlSchema: z.ZodObject<{
|
|
|
86
86
|
requires: string[];
|
|
87
87
|
instruction?: string | undefined;
|
|
88
88
|
}[];
|
|
89
|
-
description?: string | undefined;
|
|
90
89
|
apply?: {
|
|
91
90
|
requires: string[];
|
|
92
91
|
instruction?: string | undefined;
|
|
93
92
|
tracks?: string | null | undefined;
|
|
94
93
|
} | undefined;
|
|
94
|
+
description?: string | undefined;
|
|
95
95
|
}, {
|
|
96
96
|
name: string;
|
|
97
97
|
version: number;
|
|
@@ -102,12 +102,12 @@ export declare const SchemaYamlSchema: z.ZodObject<{
|
|
|
102
102
|
instruction?: string | undefined;
|
|
103
103
|
requires?: string[] | undefined;
|
|
104
104
|
}[];
|
|
105
|
-
description?: string | undefined;
|
|
106
105
|
apply?: {
|
|
107
106
|
requires: string[];
|
|
108
107
|
instruction?: string | undefined;
|
|
109
108
|
tracks?: string | null | undefined;
|
|
110
109
|
} | undefined;
|
|
110
|
+
description?: string | undefined;
|
|
111
111
|
}>;
|
|
112
112
|
/** A single artifact definition. */
|
|
113
113
|
export type Artifact = z.infer<typeof ArtifactSchema>;
|
|
@@ -4,6 +4,7 @@ import { COMMAND_CATALOG } from '../shared/command-catalog.js';
|
|
|
4
4
|
import { directoryExists, hashDirectoryTree } from '../shared/asset-hash.js';
|
|
5
5
|
import { deriveCapabilityIds } from '../shared/capability-evidence.js';
|
|
6
6
|
import { copyRuntimeAssets } from '../shared/runtime-assets.js';
|
|
7
|
+
import { removeRetiredManagedAssets } from '../shared/retired-commands.js';
|
|
7
8
|
import { renderIdeContent } from '../shared/skill-renderer.js';
|
|
8
9
|
function extractSkillDescription(content, fallback) {
|
|
9
10
|
const frontmatterMatch = content.match(/^---\s*\n([\s\S]*?)\n---/);
|
|
@@ -56,6 +57,7 @@ async function generateClaude(ctx) {
|
|
|
56
57
|
await fs.writeFile(join(commandsDestDir, `${command.id}.md`), generateCommandAlias(command.id, description), 'utf-8');
|
|
57
58
|
}
|
|
58
59
|
const runtimePaths = await copyRuntimeAssets(packageRoot, projectRoot, 'claude');
|
|
60
|
+
await removeRetiredManagedAssets(projectRoot, 'claude');
|
|
59
61
|
return {
|
|
60
62
|
ide: 'claude',
|
|
61
63
|
generatedPaths: ['.claude/skills', '.claude/commands/specflow', ...runtimePaths],
|
|
@@ -5,7 +5,9 @@ import { directoryExists, hashDirectoryTree, hashFile } from '../shared/asset-ha
|
|
|
5
5
|
import { deriveCapabilityIds } from '../shared/capability-evidence.js';
|
|
6
6
|
import { managedBlockPresent, upsertManagedBlock } from '../shared/marker-write.js';
|
|
7
7
|
import { copyRuntimeAssets } from '../shared/runtime-assets.js';
|
|
8
|
+
import { removeRetiredManagedAssets } from '../shared/retired-commands.js';
|
|
8
9
|
import { renderIdeContent, validateSkillFrontmatter } from '../shared/skill-renderer.js';
|
|
10
|
+
import { renderPhaseGateBullet } from '../shared/phase-context.js';
|
|
9
11
|
const CODEX_AGENTS_MARKER_START = '# Added by specflow init (Codex context)';
|
|
10
12
|
const CODEX_AGENTS_MARKER_END = '# End specflow init (Codex context)';
|
|
11
13
|
function extractSkillDescription(content, fallback) {
|
|
@@ -33,7 +35,7 @@ function renderCodexAgentsBlock() {
|
|
|
33
35
|
return [
|
|
34
36
|
'# SpecFlow Codex Context',
|
|
35
37
|
'',
|
|
36
|
-
|
|
38
|
+
renderPhaseGateBullet(),
|
|
37
39
|
'- Never bypass CLI validation and archive checks',
|
|
38
40
|
'- Use artifacts under specflow/changes/<change>/ before coding',
|
|
39
41
|
'- Workflow skills: $specflow-explore, $specflow-propose, $specflow-refine, $specflow-apply, $specflow-review, $specflow-test, $specflow-verify, $specflow-archive, $specflow-fix, $specflow-snap',
|
|
@@ -82,6 +84,7 @@ async function generateCodex(ctx) {
|
|
|
82
84
|
}
|
|
83
85
|
const runtimePaths = await copyRuntimeAssets(packageRoot, projectRoot, 'codex');
|
|
84
86
|
await upsertManagedBlock(join(projectRoot, 'AGENTS.md'), CODEX_AGENTS_MARKER_START, CODEX_AGENTS_MARKER_END, renderCodexAgentsBlock().split('\n'));
|
|
87
|
+
await removeRetiredManagedAssets(projectRoot, 'codex');
|
|
85
88
|
return {
|
|
86
89
|
ide: 'codex',
|
|
87
90
|
generatedPaths: ['.agents/skills', 'AGENTS.md', ...runtimePaths],
|
|
@@ -4,7 +4,9 @@ import { COMMAND_CATALOG } from '../shared/command-catalog.js';
|
|
|
4
4
|
import { directoryExists, fileExists, hashDirectoryTree, hashFile } from '../shared/asset-hash.js';
|
|
5
5
|
import { deriveCapabilityIds } from '../shared/capability-evidence.js';
|
|
6
6
|
import { copyRuntimeAssets } from '../shared/runtime-assets.js';
|
|
7
|
+
import { removeRetiredManagedAssets } from '../shared/retired-commands.js';
|
|
7
8
|
import { renderIdeContent } from '../shared/skill-renderer.js';
|
|
9
|
+
import { renderPhaseGateBullet } from '../shared/phase-context.js';
|
|
8
10
|
function extractSkillDescription(content, fallback) {
|
|
9
11
|
const frontmatterMatch = content.match(/^---\s*\n([\s\S]*?)\n---/);
|
|
10
12
|
if (!frontmatterMatch) {
|
|
@@ -26,7 +28,7 @@ function renderCursorRule() {
|
|
|
26
28
|
return [
|
|
27
29
|
'# SpecFlow Cursor Context',
|
|
28
30
|
'',
|
|
29
|
-
|
|
31
|
+
renderPhaseGateBullet(),
|
|
30
32
|
'- Never bypass CLI validation and archive checks',
|
|
31
33
|
'- Use artifacts under specflow/changes/<change>/ before coding',
|
|
32
34
|
'',
|
|
@@ -69,6 +71,7 @@ async function generateCursor(ctx) {
|
|
|
69
71
|
}
|
|
70
72
|
await fs.writeFile(join(rulesDir, 'specflow-context.mdc'), renderCursorRule(), 'utf-8');
|
|
71
73
|
const runtimePaths = await copyRuntimeAssets(packageRoot, projectRoot, 'cursor');
|
|
74
|
+
await removeRetiredManagedAssets(projectRoot, 'cursor');
|
|
72
75
|
return {
|
|
73
76
|
ide: 'cursor',
|
|
74
77
|
generatedPaths: [
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { COMMAND_IDS } from './command-catalog.js';
|
|
2
2
|
const MARKER_RULES = [
|
|
3
|
-
{ id: 'sub.scan.planned_v03_notice', skill: 'scan', includes: ['[PLANNED v0.3]', 'NOT YET FUNCTIONAL'] },
|
|
4
3
|
{ id: 'sub.explore.conversation_first', skill: 'explore', includes: ['think-before-propose', 'Do NOT write proposal'] },
|
|
5
4
|
{ id: 'sub.explore.handoff_propose', skill: 'explore', includes: ['explore.md', 'handoff'] },
|
|
6
5
|
{ id: 'sub.explore.mid_change_reexplore', skill: 'explore', includes: ['Re-explore Mid-Change', 'stuck during'] },
|
|
@@ -17,7 +16,6 @@ const MARKER_RULES = [
|
|
|
17
16
|
{ id: 'sub.archive.archive_merge', skill: 'archive', includes: ['specflow change archive', 'delta specs into main specs'] },
|
|
18
17
|
{ id: 'sub.fix.urgent_mode', skill: 'fix', includes: ['--urgent', 'skip'] },
|
|
19
18
|
{ id: 'sub.snap.posthoc', skill: 'snap', includes: ['post-hoc', 'archive'] },
|
|
20
|
-
{ id: 'failure.scan.not_implemented_guard', skill: 'scan', includes: ['do NOT attempt to run `specflow scan`'] },
|
|
21
19
|
{ id: 'failure.explore.proposal_exists_redirect', skill: 'explore', includes: ['explore is too late', 'refine'] },
|
|
22
20
|
{ id: 'failure.propose.explore_draft_gate', skill: 'propose', includes: ['Status: draft', 'REFUSE to proceed'] },
|
|
23
21
|
{ id: 'failure.apply.phase_gate', skill: 'apply', includes: ['HARD GATE (prerequisite)', 'phase is not `refined`, REFUSE'] },
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
export const COMMAND_CATALOG = [
|
|
2
|
-
{ id: 'scan', description: 'Brownfield project scanner via code-review-graph' },
|
|
3
2
|
{ id: 'explore', description: 'Think-before-propose exploration when requirements are fuzzy' },
|
|
4
3
|
{ id: 'propose', description: 'Create proposal and spec artifacts for a change' },
|
|
5
4
|
{ id: 'refine', description: 'Refine and iterate on spec artifacts' },
|
|
@@ -6,7 +6,6 @@ export const CAPABILITY_MANIFEST = [
|
|
|
6
6
|
level: 'command',
|
|
7
7
|
required: true,
|
|
8
8
|
})),
|
|
9
|
-
{ id: 'sub.scan.planned_v03_notice', commandId: 'scan', level: 'sub-capability', required: true },
|
|
10
9
|
{ id: 'sub.explore.conversation_first', commandId: 'explore', level: 'sub-capability', required: true },
|
|
11
10
|
{ id: 'sub.explore.handoff_propose', commandId: 'explore', level: 'sub-capability', required: true },
|
|
12
11
|
{ id: 'sub.explore.mid_change_reexplore', commandId: 'explore', level: 'sub-capability', required: true },
|
|
@@ -23,7 +22,6 @@ export const CAPABILITY_MANIFEST = [
|
|
|
23
22
|
{ id: 'sub.archive.archive_merge', commandId: 'archive', level: 'sub-capability', required: true },
|
|
24
23
|
{ id: 'sub.fix.urgent_mode', commandId: 'fix', level: 'sub-capability', required: true },
|
|
25
24
|
{ id: 'sub.snap.posthoc', commandId: 'snap', level: 'sub-capability', required: true },
|
|
26
|
-
{ id: 'failure.scan.not_implemented_guard', commandId: 'scan', level: 'failure-path', required: true },
|
|
27
25
|
{ id: 'failure.explore.proposal_exists_redirect', commandId: 'explore', level: 'failure-path', required: true },
|
|
28
26
|
{ id: 'failure.propose.explore_draft_gate', commandId: 'propose', level: 'failure-path', required: true },
|
|
29
27
|
{ id: 'failure.apply.phase_gate', commandId: 'apply', level: 'failure-path', required: true },
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { CHANGE_PHASES } from '../../utils/change-metadata.js';
|
|
2
|
+
/**
|
|
3
|
+
* Human-readable phase gate chain for IDE context blocks (AGENTS.md, Cursor rules).
|
|
4
|
+
* Must stay aligned with {@link CHANGE_PHASES}.
|
|
5
|
+
*/
|
|
6
|
+
export const PHASE_GATE_CHAIN = CHANGE_PHASES.join(' -> ');
|
|
7
|
+
export function renderPhaseGateBullet() {
|
|
8
|
+
return `- Respect specflow phase gates: ${PHASE_GATE_CHAIN}`;
|
|
9
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { IdeTarget } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Command IDs removed from the public catalog (v1.0.1+ rename, v1.0.2 scan removal).
|
|
4
|
+
* Init/sync must delete these managed assets so upgraded projects do not keep stale skills.
|
|
5
|
+
*/
|
|
6
|
+
export declare const RETIRED_COMMAND_IDS: readonly ["plan", "build", "done", "scan"];
|
|
7
|
+
/**
|
|
8
|
+
* Removes obsolete specflow command aliases and skills left from pre-v1.0.1 installs.
|
|
9
|
+
*/
|
|
10
|
+
export declare function removeRetiredManagedAssets(projectRoot: string, ide: IdeTarget): Promise<void>;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
/**
|
|
4
|
+
* Command IDs removed from the public catalog (v1.0.1+ rename, v1.0.2 scan removal).
|
|
5
|
+
* Init/sync must delete these managed assets so upgraded projects do not keep stale skills.
|
|
6
|
+
*/
|
|
7
|
+
export const RETIRED_COMMAND_IDS = ['plan', 'build', 'done', 'scan'];
|
|
8
|
+
async function removeIfExists(path) {
|
|
9
|
+
try {
|
|
10
|
+
await fs.rm(path, { recursive: true, force: true });
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
// ignore missing paths
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Removes obsolete specflow command aliases and skills left from pre-v1.0.1 installs.
|
|
18
|
+
*/
|
|
19
|
+
export async function removeRetiredManagedAssets(projectRoot, ide) {
|
|
20
|
+
if (ide === 'claude') {
|
|
21
|
+
const commandsDir = join(projectRoot, '.claude', 'commands', 'specflow');
|
|
22
|
+
const skillsDir = join(projectRoot, '.claude', 'skills');
|
|
23
|
+
for (const id of RETIRED_COMMAND_IDS) {
|
|
24
|
+
await removeIfExists(join(commandsDir, `${id}.md`));
|
|
25
|
+
await removeIfExists(join(skillsDir, `specflow-${id}`));
|
|
26
|
+
}
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (ide === 'cursor') {
|
|
30
|
+
const commandsDir = join(projectRoot, '.cursor', 'commands', 'specflow');
|
|
31
|
+
const skillsDir = join(projectRoot, '.cursor', 'skills');
|
|
32
|
+
for (const id of RETIRED_COMMAND_IDS) {
|
|
33
|
+
await removeIfExists(join(commandsDir, `${id}.md`));
|
|
34
|
+
await removeIfExists(join(skillsDir, `specflow-${id}`));
|
|
35
|
+
}
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const skillsDir = join(projectRoot, '.agents', 'skills');
|
|
39
|
+
for (const id of RETIRED_COMMAND_IDS) {
|
|
40
|
+
await removeIfExists(join(skillsDir, `specflow-${id}`));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* All valid lifecycle phases a change can be in.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Aligned with workflow command names (propose / refine / apply / archive).
|
|
5
|
+
*
|
|
6
|
+
* - `propose`: Initial phase, created by `specflow change new`.
|
|
5
7
|
* - `refined`: After `/specflow:refine` iterative deep review completes.
|
|
6
|
-
* - `
|
|
8
|
+
* - `apply`: After `/specflow:apply` Phase B execution finishes.
|
|
7
9
|
* - `archived`: Post-archive state; assigned automatically on successful archive.
|
|
8
10
|
*/
|
|
9
|
-
export declare const CHANGE_PHASES: readonly ["
|
|
11
|
+
export declare const CHANGE_PHASES: readonly ["propose", "refined", "apply", "archived"];
|
|
12
|
+
/** Legacy phase values from v1.0.x and earlier. */
|
|
13
|
+
export declare const LEGACY_CHANGE_PHASES: readonly ["plan", "built"];
|
|
10
14
|
/**
|
|
11
15
|
* Union type of valid lifecycle phases.
|
|
12
16
|
*/
|
|
@@ -20,6 +24,10 @@ export interface ChangeMetadata {
|
|
|
20
24
|
readonly phase?: ChangePhase;
|
|
21
25
|
readonly [key: string]: unknown;
|
|
22
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* Normalizes a phase string for CLI input (accepts legacy aliases).
|
|
29
|
+
*/
|
|
30
|
+
export declare function normalizeChangePhase(phase: string): ChangePhase | null;
|
|
23
31
|
/**
|
|
24
32
|
* Reads change metadata from .specflow.yaml in the change directory.
|
|
25
33
|
*
|
|
@@ -6,12 +6,28 @@ const METADATA_FILENAME = '.specflow.yaml';
|
|
|
6
6
|
/**
|
|
7
7
|
* All valid lifecycle phases a change can be in.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* Aligned with workflow command names (propose / refine / apply / archive).
|
|
10
|
+
*
|
|
11
|
+
* - `propose`: Initial phase, created by `specflow change new`.
|
|
10
12
|
* - `refined`: After `/specflow:refine` iterative deep review completes.
|
|
11
|
-
* - `
|
|
13
|
+
* - `apply`: After `/specflow:apply` Phase B execution finishes.
|
|
12
14
|
* - `archived`: Post-archive state; assigned automatically on successful archive.
|
|
13
15
|
*/
|
|
14
|
-
export const CHANGE_PHASES = ['
|
|
16
|
+
export const CHANGE_PHASES = ['propose', 'refined', 'apply', 'archived'];
|
|
17
|
+
/** Legacy phase values from v1.0.x and earlier. */
|
|
18
|
+
export const LEGACY_CHANGE_PHASES = ['plan', 'built'];
|
|
19
|
+
function normalizePhaseValue(value) {
|
|
20
|
+
if (typeof value !== 'string') {
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
23
|
+
if (value === 'plan') {
|
|
24
|
+
return 'propose';
|
|
25
|
+
}
|
|
26
|
+
if (value === 'built') {
|
|
27
|
+
return 'apply';
|
|
28
|
+
}
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
15
31
|
/**
|
|
16
32
|
* Zod schema for validating change metadata.
|
|
17
33
|
*
|
|
@@ -22,9 +38,21 @@ const changeMetadataSchema = z
|
|
|
22
38
|
.object({
|
|
23
39
|
schema: z.string(),
|
|
24
40
|
created: z.string(),
|
|
25
|
-
phase: z.enum(CHANGE_PHASES).optional(),
|
|
41
|
+
phase: z.preprocess(normalizePhaseValue, z.enum(CHANGE_PHASES).optional()),
|
|
26
42
|
})
|
|
27
43
|
.passthrough();
|
|
44
|
+
/**
|
|
45
|
+
* Normalizes a phase string for CLI input (accepts legacy aliases).
|
|
46
|
+
*/
|
|
47
|
+
export function normalizeChangePhase(phase) {
|
|
48
|
+
const normalized = normalizePhaseValue(phase);
|
|
49
|
+
if (typeof normalized !== 'string') {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
return CHANGE_PHASES.includes(normalized)
|
|
53
|
+
? normalized
|
|
54
|
+
: null;
|
|
55
|
+
}
|
|
28
56
|
/**
|
|
29
57
|
* Formats a Zod error into a user-friendly message.
|
|
30
58
|
*
|
|
@@ -34,7 +62,7 @@ const changeMetadataSchema = z
|
|
|
34
62
|
function formatZodError(error, metaPath) {
|
|
35
63
|
const hasInvalidPhase = error.issues.some((issue) => issue.path[0] === 'phase' && issue.code === 'invalid_enum_value');
|
|
36
64
|
if (hasInvalidPhase) {
|
|
37
|
-
return new Error('Invalid phase in metadata: expected one of
|
|
65
|
+
return new Error('Invalid phase in metadata: expected one of propose|refined|apply|archived');
|
|
38
66
|
}
|
|
39
67
|
return new Error(`Invalid metadata format in ${metaPath}: ${error.message}`);
|
|
40
68
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ChangeMetadata, ChangePhase } from './change-metadata.js';
|
|
2
|
-
export { CHANGE_PHASES } from './change-metadata.js';
|
|
2
|
+
export { CHANGE_PHASES, normalizeChangePhase } from './change-metadata.js';
|
|
3
3
|
export type { ChangePhase } from './change-metadata.js';
|
|
4
4
|
/**
|
|
5
5
|
* Validates that a change name uses only lowercase alphanumeric characters
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { promises as fs } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { readChangeMetadata, writeChangeMetadata as writeMetaInternal } from './change-metadata.js';
|
|
4
|
-
export { CHANGE_PHASES } from './change-metadata.js';
|
|
4
|
+
export { CHANGE_PHASES, normalizeChangePhase } from './change-metadata.js';
|
|
5
5
|
const CHANGES_REL_PATH = 'specflow/changes';
|
|
6
6
|
const VALID_CHANGE_NAME = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
7
7
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gordon.gan/specflow",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "SpecFlow — unified spec-driven development: OpenSpec planning + Superpowers execution in one CLI and cross-IDE workflow",
|
|
6
6
|
"keywords": [
|
|
@@ -60,9 +60,6 @@
|
|
|
60
60
|
"js-yaml": "^4.1.0",
|
|
61
61
|
"zod": "^3.24.4"
|
|
62
62
|
},
|
|
63
|
-
"optionalDependencies": {
|
|
64
|
-
"code-review-graph": "github:tirth8205/code-review-graph"
|
|
65
|
-
},
|
|
66
63
|
"devDependencies": {
|
|
67
64
|
"@types/js-yaml": "^4.0.9",
|
|
68
65
|
"@types/node": "^22.15.3",
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
## Purpose Declaration
|
|
8
8
|
|
|
9
9
|
**This prompt is for REWRITING an existing tasks.md into writing-plans precision.**
|
|
10
|
-
**NOT for generating tasks.md from scratch.** The tasks.md already exists from the `
|
|
10
|
+
**NOT for generating tasks.md from scratch.** The tasks.md already exists from the `propose` or `refine` phase. Your job is to take a coarse-grained first-iteration propose (or a refine-updated version of it) and transform each checkbox into a 2-5 minute atomic task that a fresh subagent can execute without additional context.
|
|
11
11
|
|
|
12
12
|
**Announce at start:** "I'm using specflow:apply Phase A to rewrite the existing propose into writing-plans precision."
|
|
13
13
|
|
|
@@ -67,7 +67,7 @@ Signals that you've found a real decision:
|
|
|
67
67
|
- "The naive approach is X, but it breaks when Z"
|
|
68
68
|
- "The proposal implies X but there's a tension with Y"
|
|
69
69
|
|
|
70
|
-
If during this first iteration you are genuinely uncertain about an option, **mark the decision `(
|
|
70
|
+
If during this first iteration you are genuinely uncertain about an option, **mark the decision `(propose-phase analysis, may revise in refine)`**. Honesty about uncertainty is better than false certainty — refine exists precisely to close these gaps. Do NOT use this marker as an escape hatch for every decision; use it only where you actually lack information.
|
|
71
71
|
|
|
72
72
|
### 4. Risks / Trade-offs
|
|
73
73
|
|
|
@@ -111,13 +111,13 @@ Save the document to:
|
|
|
111
111
|
specflow/changes/<change-name>/design.md
|
|
112
112
|
```
|
|
113
113
|
|
|
114
|
-
**Do NOT ask the user to confirm this draft at this stage.** The end-of-propose summary (handled by the orchestrating SKILL) will present all four
|
|
114
|
+
**Do NOT ask the user to confirm this draft at this stage.** The end-of-propose summary (handled by the orchestrating SKILL) will present all four propose-phase artifacts together for one cohesive user review. Write the best first-iteration design you can, save it, and move on to the next propose-phase artifact.
|
|
115
115
|
|
|
116
116
|
## Remember
|
|
117
117
|
|
|
118
118
|
- First iteration, not placeholder
|
|
119
119
|
- Substance over structure — skip sections with "N/A" rather than fake-filling them
|
|
120
120
|
- Reference example is a depth guide, not a copy target
|
|
121
|
-
- Honest uncertainty > false certainty — use the `(
|
|
121
|
+
- Honest uncertainty > false certainty — use the `(propose-phase analysis, may revise in refine)` marker when genuinely uncertain
|
|
122
122
|
- No quantity gates — quality is judged by user review
|
|
123
|
-
- No confirmation gate here — the SKILL handles overall
|
|
123
|
+
- No confirmation gate here — the SKILL handles overall propose-phase review
|
|
@@ -112,7 +112,7 @@ Save the document to:
|
|
|
112
112
|
specflow/changes/<change-name>/tasks.md
|
|
113
113
|
```
|
|
114
114
|
|
|
115
|
-
**Do NOT ask the user to confirm this draft at this stage.** The orchestrating SKILL will present all
|
|
115
|
+
**Do NOT ask the user to confirm this draft at this stage.** The orchestrating SKILL will present all propose-phase artifacts together at the end of the propose phase. Refine will challenge and restructure; Apply Phase A will rewrite to precision. Your job is to hand off a substantive first pass.
|
|
116
116
|
|
|
117
117
|
## Remember
|
|
118
118
|
|
|
@@ -216,7 +216,7 @@ my-project/
|
|
|
216
216
|
|
|
217
217
|
### D7: Scan via code-review-graph
|
|
218
218
|
|
|
219
|
-
**Choice
|
|
219
|
+
**Choice (deferred):** A future `/specflow:scan` may use code-review-graph to analyze codebase structure and convert output into OpenSpec specs format. Scan is **not shipped** in public releases; brownfield onboarding uses `/specflow:explore` or `/specflow:propose` instead.
|
|
220
220
|
|
|
221
221
|
**Why**: Code analysis is a hard problem. Rather than building a custom AST parser for every language, leverage an existing tool that generates structured project knowledge. SpecFlow's value-add is converting that knowledge into actionable specs.
|
|
222
222
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
# Refine-Phase Brainstorming: Attacking Deep Review
|
|
6
6
|
|
|
7
|
-
> This prompt is for **ATTACKING DEEP REVIEW** of
|
|
7
|
+
> This prompt is for **ATTACKING DEEP REVIEW** of propose-phase artifacts.
|
|
8
8
|
> NOT from-scratch brainstorming. The 4 artifacts (proposal, specs, design, tasks)
|
|
9
9
|
> already exist and must be challenged, deepened, and possibly updated.
|
|
10
10
|
|
|
@@ -48,7 +48,7 @@ injected; steps 3–5 carry the discussion into artifacts; steps 6–8 close the
|
|
|
48
48
|
|
|
49
49
|
### Step 1 — Examine existing artifacts (inject Challenge Behavior #1)
|
|
50
50
|
|
|
51
|
-
Re-read all 4
|
|
51
|
+
Re-read all 4 propose-phase artifacts with fresh eyes. This is not a summary exercise — look for
|
|
52
52
|
what the propose *did not* say. Explicitly INJECT **Challenge Behavior #1 (challenge plan's
|
|
53
53
|
assumptions)** here: list assumptions the propose glossed over and explain why each matters. Each
|
|
54
54
|
round MUST produce this list of challenged assumptions, even if shorter than the previous round.
|
|
@@ -1,13 +1,8 @@
|
|
|
1
1
|
name: specflow
|
|
2
2
|
version: 3
|
|
3
|
-
description: SpecFlow workflow —
|
|
3
|
+
description: SpecFlow workflow — explore → propose → refine → apply → review → test → verify → archive
|
|
4
4
|
|
|
5
5
|
artifacts:
|
|
6
|
-
- id: scan
|
|
7
|
-
generates: "specflow/specs/**/*.md"
|
|
8
|
-
description: Brownfield project scan - generates specs baseline
|
|
9
|
-
requires: []
|
|
10
|
-
|
|
11
6
|
- id: explore
|
|
12
7
|
generates: "explore.md"
|
|
13
8
|
description: Pre-plan exploration - problem space, options, and recommended direction
|
|
@@ -115,7 +115,7 @@ If the user rejects, return to Stage B2a for the same task.
|
|
|
115
115
|
### Stage B3: Phase Transition
|
|
116
116
|
|
|
117
117
|
Once all tasks are confirmed:
|
|
118
|
-
- Invoke `specflow change phase <name> --set
|
|
118
|
+
- Invoke `specflow change phase <name> --set apply` to transition the phase.
|
|
119
119
|
- Summarize the build results and overall coverage.
|
|
120
120
|
- Inform the user they can now run `/specflow:review` or `/specflow:verify`.
|
|
121
121
|
|
|
@@ -123,7 +123,7 @@ Once all tasks are confirmed:
|
|
|
123
123
|
|
|
124
124
|
## Phase Transition Notes
|
|
125
125
|
|
|
126
|
-
- **On Phase B success**: phase → `
|
|
126
|
+
- **On Phase B success**: phase → `apply` (automatic via Stage B3 CLI call).
|
|
127
127
|
- **On Phase A gap halt (Outcome 3)**: phase remains `refined`. No transition. User runs `/specflow:refine` to close gaps, then re-runs `/specflow:apply`.
|
|
128
128
|
- **On Phase A reorganization choice C**: phase remains `refined`. No transition. User edits groups manually, then re-runs `/specflow:apply`.
|
|
129
129
|
- **On Phase B interruption or failing tests**: phase remains `refined`. User resumes Phase B later.
|
|
@@ -11,7 +11,7 @@ description: "Archive change + merge specs + git branch cleanup"
|
|
|
11
11
|
|
|
12
12
|
- An active change must exist with completed implementation.
|
|
13
13
|
- `specflow` CLI must be available on PATH.
|
|
14
|
-
- **Phase must be `
|
|
14
|
+
- **Phase must be `apply`** in `.specflow.yaml`. This is set automatically when `/specflow:apply` Phase B completes. `specflow change archive` refuses to archive changes whose phase is not `apply` unless the caller passes `--force` explicitly. Do NOT pass `--force` from this skill — the flag is reserved for explicit user discretion. If archive fails with a phase check error, route the user back to `/specflow:apply` to complete the missing phase transition rather than forcing past the guard.
|
|
15
15
|
|
|
16
16
|
## Stage 1: Test Gate
|
|
17
17
|
|
|
@@ -41,7 +41,7 @@ If no active change directory exists for this work:
|
|
|
41
41
|
specflow change new <name>
|
|
42
42
|
```
|
|
43
43
|
|
|
44
|
-
The CLI sets `phase=
|
|
44
|
+
The CLI sets `phase=propose` in `.specflow.yaml`. Explore does not change phase — `explore.md` with `Status: confirmed` signals that exploration completed.
|
|
45
45
|
|
|
46
46
|
If `explore.md` already exists and is confirmed, suggest `/specflow:propose` instead of re-exploring unless the user wants to revise.
|
|
47
47
|
|
|
@@ -61,9 +61,9 @@ If the fix addresses a genuinely new scenario that existing specs did not cover,
|
|
|
61
61
|
|
|
62
62
|
### Stage 7a: Mark Phase Built
|
|
63
63
|
|
|
64
|
-
Before archive, invoke `specflow change phase fix-<desc> --set
|
|
64
|
+
Before archive, invoke `specflow change phase fix-<desc> --set apply` to mark the fix as apply-complete.
|
|
65
65
|
|
|
66
|
-
Rationale: a fix change is created with `phase=
|
|
66
|
+
Rationale: a fix change is created with `phase=propose` (same as any other change) but the fix flow covers the equivalent of the propose → refine → apply pipeline end-to-end inside Stages 3–6 (debug → TDD → review → verify). Archive rejects non-`apply` phases unless `--force` is passed, so setting the phase explicitly here keeps the guard meaningful without needing `--force`.
|
|
67
67
|
|
|
68
68
|
### Stage 7b: Run Archive
|
|
69
69
|
|
|
@@ -49,7 +49,7 @@ ls specflow/changes/<name>/explore.md 2>/dev/null
|
|
|
49
49
|
|
|
50
50
|
Run `specflow change new <name>` to initialize a new change directory.
|
|
51
51
|
|
|
52
|
-
The CLI automatically sets `phase=
|
|
52
|
+
The CLI automatically sets `phase=propose` in `.specflow.yaml` on creation (no separate phase call needed here).
|
|
53
53
|
|
|
54
54
|
## Stage 2: Generate Proposal
|
|
55
55
|
|