@gordon.gan/specflow 1.1.1 → 1.2.0-beta
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 +6 -0
- package/dist/cli/commands/change-archive.js +4 -4
- package/dist/cli/commands/change-new.js +5 -5
- package/dist/cli/commands/change-phase.js +4 -4
- package/dist/cli/commands/change-status.js +4 -4
- package/dist/cli/commands/context.d.ts +18 -0
- package/dist/cli/commands/context.js +125 -0
- package/dist/cli/commands/instructions.d.ts +4 -1
- package/dist/cli/commands/instructions.js +58 -9
- package/dist/cli/commands/show.d.ts +22 -0
- package/dist/cli/commands/show.js +92 -0
- package/dist/cli/commands/store.d.ts +16 -0
- package/dist/cli/commands/store.js +221 -0
- package/dist/cli/commands/validate.d.ts +16 -0
- package/dist/cli/commands/validate.js +34 -4
- package/dist/cli/commands/workset.d.ts +12 -0
- package/dist/cli/commands/workset.js +235 -0
- package/dist/cli/index.js +8 -0
- package/dist/cli/shared/store-option.d.ts +11 -0
- package/dist/cli/shared/store-option.js +40 -0
- package/dist/core/artifact-graph/instruction-loader.d.ts +17 -0
- package/dist/core/artifact-graph/instruction-loader.js +2 -0
- package/dist/core/artifact-graph/types.d.ts +2 -2
- package/dist/core/context-assembly.d.ts +9 -0
- package/dist/core/context-assembly.js +68 -0
- package/dist/core/diagnostics.d.ts +11 -0
- package/dist/core/diagnostics.js +18 -0
- package/dist/core/file-state.d.ts +23 -0
- package/dist/core/file-state.js +101 -0
- package/dist/core/global-config.d.ts +26 -0
- package/dist/core/global-config.js +77 -0
- package/dist/core/opener-launch.d.ts +3 -0
- package/dist/core/opener-launch.js +20 -0
- package/dist/core/openers.d.ts +23 -0
- package/dist/core/openers.js +20 -0
- package/dist/core/project-config.d.ts +14 -0
- package/dist/core/project-config.js +73 -0
- package/dist/core/reference-index.d.ts +8 -0
- package/dist/core/reference-index.js +80 -0
- package/dist/core/references.d.ts +17 -0
- package/dist/core/references.js +51 -0
- package/dist/core/relationship-health.d.ts +22 -0
- package/dist/core/relationship-health.js +68 -0
- package/dist/core/root-selection.d.ts +26 -0
- package/dist/core/root-selection.js +197 -0
- package/dist/core/store/errors.d.ts +2 -0
- package/dist/core/store/errors.js +1 -0
- package/dist/core/store/foundation.d.ts +141 -0
- package/dist/core/store/foundation.js +79 -0
- package/dist/core/store/health.d.ts +13 -0
- package/dist/core/store/health.js +117 -0
- package/dist/core/store/operations.d.ts +56 -0
- package/dist/core/store/operations.js +268 -0
- package/dist/core/store/registry.d.ts +15 -0
- package/dist/core/store/registry.js +128 -0
- package/dist/core/working-set.d.ts +30 -0
- package/dist/core/working-set.js +26 -0
- package/dist/core/worksets.d.ts +71 -0
- package/dist/core/worksets.js +134 -0
- package/dist/integrations/shared/skill-renderer.d.ts +6 -0
- package/dist/integrations/shared/skill-renderer.js +22 -0
- package/package.json +1 -1
- package/skills/specflow-apply/SKILL.md +5 -27
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export interface WorksetMember {
|
|
3
|
+
readonly name: string;
|
|
4
|
+
readonly path: string;
|
|
5
|
+
}
|
|
6
|
+
export interface Workset {
|
|
7
|
+
readonly name: string;
|
|
8
|
+
readonly members: readonly WorksetMember[];
|
|
9
|
+
readonly tool?: string;
|
|
10
|
+
}
|
|
11
|
+
declare const WorksetsStateSchema: z.ZodObject<{
|
|
12
|
+
version: z.ZodLiteral<1>;
|
|
13
|
+
worksets: z.ZodArray<z.ZodObject<{
|
|
14
|
+
name: z.ZodString;
|
|
15
|
+
members: z.ZodArray<z.ZodObject<{
|
|
16
|
+
name: z.ZodString;
|
|
17
|
+
path: z.ZodString;
|
|
18
|
+
}, "strict", z.ZodTypeAny, {
|
|
19
|
+
path: string;
|
|
20
|
+
name: string;
|
|
21
|
+
}, {
|
|
22
|
+
path: string;
|
|
23
|
+
name: string;
|
|
24
|
+
}>, "many">;
|
|
25
|
+
tool: z.ZodOptional<z.ZodString>;
|
|
26
|
+
}, "strict", z.ZodTypeAny, {
|
|
27
|
+
name: string;
|
|
28
|
+
members: {
|
|
29
|
+
path: string;
|
|
30
|
+
name: string;
|
|
31
|
+
}[];
|
|
32
|
+
tool?: string | undefined;
|
|
33
|
+
}, {
|
|
34
|
+
name: string;
|
|
35
|
+
members: {
|
|
36
|
+
path: string;
|
|
37
|
+
name: string;
|
|
38
|
+
}[];
|
|
39
|
+
tool?: string | undefined;
|
|
40
|
+
}>, "many">;
|
|
41
|
+
}, "strip", z.ZodTypeAny, {
|
|
42
|
+
worksets: {
|
|
43
|
+
name: string;
|
|
44
|
+
members: {
|
|
45
|
+
path: string;
|
|
46
|
+
name: string;
|
|
47
|
+
}[];
|
|
48
|
+
tool?: string | undefined;
|
|
49
|
+
}[];
|
|
50
|
+
version: 1;
|
|
51
|
+
}, {
|
|
52
|
+
worksets: {
|
|
53
|
+
name: string;
|
|
54
|
+
members: {
|
|
55
|
+
path: string;
|
|
56
|
+
name: string;
|
|
57
|
+
}[];
|
|
58
|
+
tool?: string | undefined;
|
|
59
|
+
}[];
|
|
60
|
+
version: 1;
|
|
61
|
+
}>;
|
|
62
|
+
export type WorksetsState = z.infer<typeof WorksetsStateSchema>;
|
|
63
|
+
export declare function validateWorksetName(name: string): void;
|
|
64
|
+
export declare function validateWorksetMembers(members: readonly WorksetMember[]): void;
|
|
65
|
+
export declare function readWorksetsState(statePath: string): Promise<WorksetsState>;
|
|
66
|
+
export declare function updateWorksetsState(statePath: string, updater: (state: WorksetsState) => WorksetsState): Promise<WorksetsState>;
|
|
67
|
+
export declare function withWorkset(state: WorksetsState, workset: Workset): WorksetsState;
|
|
68
|
+
export declare function getWorksetCodeWorkspacePath(dataDir: string, name: string): string;
|
|
69
|
+
export declare function defaultWorksetsStatePath(dataDir?: string): string;
|
|
70
|
+
export declare function removeWorksetByName(statePath: string, name: string, dataDir: string): Promise<boolean>;
|
|
71
|
+
export {};
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import yaml from 'js-yaml';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { acquireFileLock, releaseFileLock, writeFileAtomically, makeLockErrorFactory } from './file-state.js';
|
|
6
|
+
import { getWorksetsStatePath } from './global-config.js';
|
|
7
|
+
import { StoreError } from './store/foundation.js';
|
|
8
|
+
const KEBAB_CASE_NAME = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
|
|
9
|
+
const WorksetMemberSchema = z.object({ name: z.string(), path: z.string() }).strict();
|
|
10
|
+
const WorksetSchema = z
|
|
11
|
+
.object({
|
|
12
|
+
name: z.string(),
|
|
13
|
+
members: z.array(WorksetMemberSchema).min(1),
|
|
14
|
+
tool: z.string().optional(),
|
|
15
|
+
})
|
|
16
|
+
.strict();
|
|
17
|
+
const WorksetsStateSchema = z.object({
|
|
18
|
+
version: z.literal(1),
|
|
19
|
+
worksets: z.array(WorksetSchema),
|
|
20
|
+
});
|
|
21
|
+
function lockPathFor(statePath) {
|
|
22
|
+
return `${statePath}.lock`;
|
|
23
|
+
}
|
|
24
|
+
function makeWorksetLockError() {
|
|
25
|
+
return makeLockErrorFactory({
|
|
26
|
+
createSubject: 'the workset lock file',
|
|
27
|
+
busyMessage: 'Workset state is busy.',
|
|
28
|
+
code: 'workset_busy',
|
|
29
|
+
target: 'worksets.state',
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
export function validateWorksetName(name) {
|
|
33
|
+
if (!KEBAB_CASE_NAME.test(name)) {
|
|
34
|
+
throw new StoreError(`Invalid workset name '${name}'.`, {
|
|
35
|
+
severity: 'error',
|
|
36
|
+
code: 'invalid_workset_name',
|
|
37
|
+
message: 'Workset names must be kebab-case.',
|
|
38
|
+
target: 'workset.name',
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export function validateWorksetMembers(members) {
|
|
43
|
+
if (members.length === 0) {
|
|
44
|
+
throw new StoreError('Workset must contain at least one member.', {
|
|
45
|
+
severity: 'error',
|
|
46
|
+
code: 'invalid_workset_members',
|
|
47
|
+
message: 'Worksets require at least one member.',
|
|
48
|
+
target: 'workset.members',
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
const labels = new Set();
|
|
52
|
+
for (const member of members) {
|
|
53
|
+
if (!path.isAbsolute(member.path)) {
|
|
54
|
+
throw new StoreError(`Member path must be absolute: ${member.path}`, {
|
|
55
|
+
severity: 'error',
|
|
56
|
+
code: 'invalid_workset_path',
|
|
57
|
+
message: 'Workset member paths must be absolute.',
|
|
58
|
+
target: `workset.members.${member.name}`,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
if (labels.has(member.name)) {
|
|
62
|
+
throw new StoreError(`Duplicate workset member label '${member.name}'.`, {
|
|
63
|
+
severity: 'error',
|
|
64
|
+
code: 'duplicate_workset_member',
|
|
65
|
+
message: 'Workset member labels must be unique.',
|
|
66
|
+
target: `workset.members.${member.name}`,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
labels.add(member.name);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
export async function readWorksetsState(statePath) {
|
|
73
|
+
try {
|
|
74
|
+
const content = await fs.readFile(statePath, 'utf-8');
|
|
75
|
+
const parsed = yaml.load(content);
|
|
76
|
+
return WorksetsStateSchema.parse(parsed);
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
if (error.code === 'ENOENT') {
|
|
80
|
+
return { version: 1, worksets: [] };
|
|
81
|
+
}
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
async function writeWorksetsState(statePath, state) {
|
|
86
|
+
const content = yaml.dump(state, { lineWidth: -1, noRefs: true });
|
|
87
|
+
await writeFileAtomically(statePath, content);
|
|
88
|
+
}
|
|
89
|
+
export async function updateWorksetsState(statePath, updater) {
|
|
90
|
+
const lock = await acquireFileLock({
|
|
91
|
+
lockPath: lockPathFor(statePath),
|
|
92
|
+
errorFor: makeWorksetLockError(),
|
|
93
|
+
});
|
|
94
|
+
try {
|
|
95
|
+
const current = await readWorksetsState(statePath);
|
|
96
|
+
const next = WorksetsStateSchema.parse(updater(current));
|
|
97
|
+
await writeWorksetsState(statePath, next);
|
|
98
|
+
return next;
|
|
99
|
+
}
|
|
100
|
+
finally {
|
|
101
|
+
await releaseFileLock(lock, lockPathFor(statePath));
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
export function withWorkset(state, workset) {
|
|
105
|
+
validateWorksetName(workset.name);
|
|
106
|
+
validateWorksetMembers(workset.members);
|
|
107
|
+
const without = state.worksets.filter((entry) => entry.name !== workset.name);
|
|
108
|
+
return { version: 1, worksets: [...without, workset].sort((a, b) => a.name.localeCompare(b.name)) };
|
|
109
|
+
}
|
|
110
|
+
export function getWorksetCodeWorkspacePath(dataDir, name) {
|
|
111
|
+
return path.join(dataDir, 'worksets', `${name}.code-workspace`);
|
|
112
|
+
}
|
|
113
|
+
export function defaultWorksetsStatePath(dataDir) {
|
|
114
|
+
return getWorksetsStatePath(dataDir);
|
|
115
|
+
}
|
|
116
|
+
export async function removeWorksetByName(statePath, name, dataDir) {
|
|
117
|
+
let removed = false;
|
|
118
|
+
await updateWorksetsState(statePath, (state) => {
|
|
119
|
+
const exists = state.worksets.some((entry) => entry.name === name);
|
|
120
|
+
if (!exists) {
|
|
121
|
+
return state;
|
|
122
|
+
}
|
|
123
|
+
removed = true;
|
|
124
|
+
return {
|
|
125
|
+
version: 1,
|
|
126
|
+
worksets: state.worksets.filter((entry) => entry.name !== name),
|
|
127
|
+
};
|
|
128
|
+
});
|
|
129
|
+
if (removed) {
|
|
130
|
+
const workspacePath = getWorksetCodeWorkspacePath(dataDir, name);
|
|
131
|
+
await fs.rm(workspacePath, { force: true }).catch(() => undefined);
|
|
132
|
+
}
|
|
133
|
+
return removed;
|
|
134
|
+
}
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import type { IdeTarget } from './types.js';
|
|
2
|
+
import type { ReferencedStoresSection } from '../../core/artifact-graph/instruction-loader.js';
|
|
3
|
+
export interface ReferenceGuidanceInput {
|
|
4
|
+
readonly referencedStores?: ReferencedStoresSection;
|
|
5
|
+
}
|
|
6
|
+
/** Renders identical reference guidance for every IDE target. */
|
|
7
|
+
export declare function collectReferenceGuidance(input: ReferenceGuidanceInput): Record<IdeTarget, string>;
|
|
2
8
|
export declare function renderIdeContent(content: string, ide: IdeTarget): string;
|
|
3
9
|
/** @deprecated Use renderIdeContent */
|
|
4
10
|
export declare const renderSkillContent: typeof renderIdeContent;
|
|
@@ -1,4 +1,26 @@
|
|
|
1
1
|
import { COMMAND_CATALOG } from './command-catalog.js';
|
|
2
|
+
function formatReferenceBlock(section) {
|
|
3
|
+
if (!section || section.rendered.trim().length === 0) {
|
|
4
|
+
return '';
|
|
5
|
+
}
|
|
6
|
+
const lines = [
|
|
7
|
+
'## Referenced store index (read-only)',
|
|
8
|
+
section.rendered,
|
|
9
|
+
];
|
|
10
|
+
if (section.truncated) {
|
|
11
|
+
lines.push('Index truncated. Use `specflow show <spec-id> --type spec --store <store-id>` to fetch full specs.');
|
|
12
|
+
}
|
|
13
|
+
return lines.join('\n');
|
|
14
|
+
}
|
|
15
|
+
/** Renders identical reference guidance for every IDE target. */
|
|
16
|
+
export function collectReferenceGuidance(input) {
|
|
17
|
+
const block = formatReferenceBlock(input.referencedStores);
|
|
18
|
+
return {
|
|
19
|
+
claude: block,
|
|
20
|
+
cursor: block,
|
|
21
|
+
codex: block,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
2
24
|
function runtimePrefix(ide) {
|
|
3
25
|
if (ide === 'claude') {
|
|
4
26
|
return '.claude/specflow/';
|
package/package.json
CHANGED
|
@@ -5,15 +5,6 @@ description: "Two-phase apply — task rewrite + subagent TDD execution"
|
|
|
5
5
|
|
|
6
6
|
# SpecFlow: Apply
|
|
7
7
|
|
|
8
|
-
## Invocation mode
|
|
9
|
-
|
|
10
|
-
Inspect the current user invocation before any stage work:
|
|
11
|
-
|
|
12
|
-
- **Default mode:** `/specflow:apply` preserves every existing user confirmation gate and the default interactive behavior.
|
|
13
|
-
- **Yes mode:** `/specflow:apply --yes` records non-interactive confirmation mode for the current invocation only. It auto-accepts only successful acknowledgement gates; it never bypasses prerequisites, gap detection, failing verification, or a review `Block`.
|
|
14
|
-
|
|
15
|
-
Report the selected mode once at the start of the session. Do not persist it to future invocations.
|
|
16
|
-
|
|
17
8
|
> **HARD GATE (prerequisite)**: phase must be `refined`. Run /specflow:refine first if not.
|
|
18
9
|
> **HARD GATE (Phase A)**: rewritten tasks.md must be user-confirmed before Phase B.
|
|
19
10
|
> **HARD GATE (Phase B)**: each task must be reviewed (spec + code quality) and user-confirmed before next task.
|
|
@@ -65,10 +56,7 @@ Present the audit summary to the user: per-group task count (coarse → atomic),
|
|
|
65
56
|
|
|
66
57
|
### Gate A: Rewrite Confirmation
|
|
67
58
|
|
|
68
|
-
|
|
69
|
-
- **Yes mode:** Present the rewrite audit and continue directly to Phase B after recording the rewrite as automatically accepted for this invocation.
|
|
70
|
-
|
|
71
|
-
If Phase A reports a reorganization choice or a design gap, stop regardless of mode; `--yes` does not choose a reorganization or fill a missing design decision.
|
|
59
|
+
Present the rewritten `tasks.md` to the user. **Ask the user to confirm the rewrite.** Do NOT proceed to Phase B until the user explicitly confirms.
|
|
72
60
|
|
|
73
61
|
---
|
|
74
62
|
|
|
@@ -120,26 +108,16 @@ ECC reviewer verdict routing:
|
|
|
120
108
|
|
|
121
109
|
#### Gate B: Per-task Confirmation
|
|
122
110
|
|
|
123
|
-
|
|
124
|
-
- **Yes mode:** Present the task output and both review reports. If spec review passes and code-quality review returns `Approve` or `Warning`, record the task as automatically accepted and start the next task without waiting.
|
|
111
|
+
Present the task output and both review reports to the user. **Ask the user to confirm the task is complete.** Do NOT proceed to the next task until confirmation is received.
|
|
125
112
|
|
|
126
|
-
If
|
|
113
|
+
If the user rejects, return to Stage B2a for the same task.
|
|
127
114
|
|
|
128
115
|
### Stage B3: Phase Transition
|
|
129
116
|
|
|
130
|
-
Once all tasks are confirmed
|
|
117
|
+
Once all tasks are confirmed:
|
|
131
118
|
- Invoke `specflow change phase <name> --set apply` to transition the phase.
|
|
132
119
|
- Summarize the build results and overall coverage.
|
|
133
|
-
|
|
134
|
-
#### Yes-mode downstream sequence
|
|
135
|
-
|
|
136
|
-
When yes mode reaches this point:
|
|
137
|
-
1. Invoke `/specflow:review`. Stop if it reports a CRITICAL or HIGH finding.
|
|
138
|
-
2. Invoke `/specflow:test` only after review has no CRITICAL/HIGH findings. Stop if tests cannot be made green.
|
|
139
|
-
3. Invoke `/specflow:verify` only after test succeeds. Stop if verify returns `FAIL`.
|
|
140
|
-
4. Report review, test, and verification evidence. Do NOT invoke `/specflow:archive` automatically; archive remains the user's manual choice.
|
|
141
|
-
|
|
142
|
-
When default mode reaches this point, inform the user that they can now run `/specflow:review` or `/specflow:verify`.
|
|
120
|
+
- Inform the user they can now run `/specflow:review` or `/specflow:verify`.
|
|
143
121
|
|
|
144
122
|
---
|
|
145
123
|
|