@gordon.gan/specflow 1.2.1-beta → 1.3.1-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 +88 -20
- package/dist/cli/commands/change-new.d.ts +11 -1
- package/dist/cli/commands/change-new.js +33 -2
- package/dist/cli/commands/init.d.ts +2 -0
- package/dist/cli/commands/init.js +40 -4
- package/dist/core/archive.js +22 -4
- package/dist/core/artifact-graph/types.d.ts +2 -2
- package/dist/core/opener-launch.d.ts +15 -0
- package/dist/core/opener-launch.js +36 -5
- package/dist/core/project-config.d.ts +4 -0
- package/dist/core/project-config.js +67 -0
- package/dist/core/store/foundation.d.ts +4 -4
- package/dist/core/upstream-spec.d.ts +14 -0
- package/dist/core/upstream-spec.js +74 -0
- package/dist/core/worksets.d.ts +2 -2
- package/package.json +1 -1
- package/skills/specflow-archive/SKILL.md +13 -3
- package/skills/specflow-propose/SKILL.md +62 -1
package/README.md
CHANGED
|
@@ -8,17 +8,19 @@
|
|
|
8
8
|
|
|
9
9
|
SpecFlow 把 **OpenSpec**(结构化需求规划)和 **Superpowers**(TDD、调试、代码审查等工程纪律)合并为一个工具:一个 CLI 负责确定性操作,一套跨 IDE 工作流技能负责 AI 编排,覆盖从探索需求到归档合并的完整生命周期。
|
|
10
10
|
|
|
11
|
-
|
|
|
12
|
-
|
|
13
|
-
|
|
|
14
|
-
| Cursor |
|
|
15
|
-
|
|
|
11
|
+
| 场景 | 入口 | 详细指南 |
|
|
12
|
+
|------|------|----------|
|
|
13
|
+
| **单仓(默认)** | Claude `/specflow:*` · Cursor `specflow:*` · Codex `$specflow-*` | **本文**(含中文产物、`apply --yes`) |
|
|
14
|
+
| Cursor 打包与安装 | Cursor 命令与资产 | [Cursor 指南](./CURSOR_PACKAGING_AND_USAGE_GUIDE.md) |
|
|
15
|
+
| Codex 打包与安装 | Codex 技能与资产 | [Codex 指南](./CODEX_PACKAGING_AND_USAGE_GUIDE.md) |
|
|
16
16
|
| 多语言 / Go Profile | SpecFlow + 语言专项 skills | [语言 Profile 指南](./LANGUAGE_PROFILE_USAGE_GUIDE.md) |
|
|
17
|
-
|
|
|
18
|
-
|
|
|
19
|
-
| 多仓场景示例(TALOS) | contracts + api + web
|
|
20
|
-
| TALOS 公共需求详例 |
|
|
21
|
-
| 产物与作用 | explore / proposal / delta / design / tasks
|
|
17
|
+
| 多仓(**权威**) | Contract Hub + Spokes | [多仓完整指南](./MULTI_REPO_GUIDE.md) |
|
|
18
|
+
| 多仓技术方案 | Store / References / Workset | [多仓技术方案](./MULTI_REPO_TECHNICAL_DESIGN.md) |
|
|
19
|
+
| 多仓场景示例(TALOS) | contracts + api + web | [TALOS 多仓教程](./TALOS_MULTI_REPO_TUTORIAL.md) |
|
|
20
|
+
| TALOS 公共需求详例 | Hub→BE→FE→联调 | [公共需求 Walkthrough](./docs/TALOS_SHARED_REQUIREMENT_WALKTHROUGH.md) |
|
|
21
|
+
| 产物与作用 | explore / proposal / delta / design / tasks | [产物说明](./SPECFLOW_ARTIFACTS.md) |
|
|
22
|
+
|
|
23
|
+
未配置 `store` / `references` / `workflow.profile` 时,行为就是**单仓 SpecFlow**(`standalone`);多仓为可选增强。
|
|
22
24
|
|
|
23
25
|
---
|
|
24
26
|
|
|
@@ -44,12 +46,16 @@ explore(可选)→ propose → refine → apply → review → test → veri
|
|
|
44
46
|
| 能力 | 说明 |
|
|
45
47
|
|------|------|
|
|
46
48
|
| **结构化规划** | 一次 propose 产出 proposal、delta specs、design、tasks 四个 artifact |
|
|
49
|
+
| **中文 / 英文产物** | `specflow init --artifact-language zh-CN`(别名 `zh`)写入 `artifacts.language`;叙述可中文,协议标记保持英文 |
|
|
47
50
|
| **多轮精化** | refine 内部循环(≥2 轮),攻击性审查假设、边界与 scope |
|
|
48
51
|
| **纪律化构建** | apply 先重写 tasks.md,再逐任务 TDD;design 有漏则回 refine |
|
|
52
|
+
| **`apply --yes`** | 当前调用内自动放行成功确认门禁,构建成功后串行 review → test → verify;不绕过 Block / 失败,不自动 archive |
|
|
53
|
+
| **语言感知审查** | apply 按 `go.mod` / `package.json` 等路由到 ECC reviewer(TS / Python / Go / Rust / Kotlin / Java) |
|
|
49
54
|
| **双重验收** | verify 对照 delta specs 与主 specs 做回归检查 |
|
|
50
55
|
| **变更归档** | archive 自动 merge delta、移入 archive、可做 git 分支清理 |
|
|
51
56
|
| **跨 IDE 一致** | Claude / Cursor / Codex 共享同一套技能与 prompt,parity 可校验 |
|
|
52
57
|
| **确定性 CLI** | 校验、合并、状态追踪、资产同步 — 不依赖 AI 猜 |
|
|
58
|
+
| **可选语言 Profile** | SpecFlow 管流程门禁;Go/TS 等专项 skills 按仓库挂载,见 [语言 Profile 指南](./LANGUAGE_PROFILE_USAGE_GUIDE.md) |
|
|
53
59
|
|
|
54
60
|
---
|
|
55
61
|
|
|
@@ -67,6 +73,7 @@ explore(可选)→ propose → refine → apply → review → test → veri
|
|
|
67
73
|
│ refine ≥2 轮精化:挑战假设 / 新方案 / 探边界 / 质疑 scope │
|
|
68
74
|
│ ↓ │
|
|
69
75
|
│ apply Phase A 重写 tasks.md → Phase B 逐任务 TDD 执行 │
|
|
76
|
+
│ (可选 /specflow:apply --yes:少打断,成功后串行验收) │
|
|
70
77
|
│ ↓ │
|
|
71
78
|
│ review → test → verify │
|
|
72
79
|
│ ↓ │
|
|
@@ -117,7 +124,7 @@ npm install -g @gordon.gan/specflow
|
|
|
117
124
|
npm install -g github:Gordon-Gan-Jiang/specflow
|
|
118
125
|
|
|
119
126
|
# 验证
|
|
120
|
-
specflow --version # 以 npm / package.json
|
|
127
|
+
specflow --version # 以 npm / package.json 为准(当前 1.3.1-beta)
|
|
121
128
|
specflow --help
|
|
122
129
|
```
|
|
123
130
|
|
|
@@ -160,23 +167,45 @@ artifacts:
|
|
|
160
167
|
|
|
161
168
|
### 跑通第一个变更
|
|
162
169
|
|
|
163
|
-
|
|
170
|
+
**需求已清晰(交互确认,默认):**
|
|
164
171
|
|
|
165
172
|
```
|
|
166
173
|
/specflow:propose "给用户管理模块加批量导入"
|
|
167
174
|
/specflow:refine
|
|
168
175
|
/specflow:apply
|
|
176
|
+
/specflow:review
|
|
177
|
+
/specflow:test
|
|
169
178
|
/specflow:verify
|
|
170
179
|
/specflow:archive
|
|
171
180
|
```
|
|
172
181
|
|
|
173
|
-
|
|
182
|
+
**少打断(`apply --yes`):**
|
|
183
|
+
|
|
184
|
+
```
|
|
185
|
+
/specflow:propose "给用户管理模块加批量导入"
|
|
186
|
+
/specflow:refine
|
|
187
|
+
/specflow:apply --yes
|
|
188
|
+
# 构建成功后自动串行:review → test → verify
|
|
189
|
+
# 全部通过后你再手动:
|
|
190
|
+
/specflow:archive
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
`/specflow:apply --yes` 只影响**当前这一次**调用:
|
|
194
|
+
|
|
195
|
+
| 会自动做 | 不会自动做 / 仍会停下 |
|
|
196
|
+
|----------|------------------------|
|
|
197
|
+
| 接受成功的 Phase A 重写确认(Gate A) | 设计缺口、重组选项需人决策 |
|
|
198
|
+
| 任务审查为 `Approve` / `Warning` 时进入下一任务 | 审查 `Block`、测试失败、前置 phase 不对 |
|
|
199
|
+
| Phase B 成功后串行 review → test → verify | 自动 `/specflow:archive` |
|
|
200
|
+
| | CRITICAL / HIGH review、test 红、verify `FAIL` 时中断序列 |
|
|
201
|
+
|
|
202
|
+
Cursor:`specflow:apply --yes`;Codex:`$specflow-apply --yes`。
|
|
174
203
|
|
|
175
204
|
**需求模糊:**
|
|
176
205
|
|
|
177
206
|
```
|
|
178
207
|
/specflow:explore "不确定用 CSV 还是 Excel,也不清楚现有用户表结构"
|
|
179
|
-
# 确认 explore.md 后:
|
|
208
|
+
# 确认 explore.md 的 Status 为 confirmed 后:
|
|
180
209
|
/specflow:propose
|
|
181
210
|
/specflow:refine
|
|
182
211
|
...
|
|
@@ -248,14 +277,14 @@ specflow init
|
|
|
248
277
|
| 命令 | 做什么 |
|
|
249
278
|
|------|--------|
|
|
250
279
|
| **explore** | 读代码、比方案、定边界;产出 `explore.md`,confirmed 后 handoff 到 propose |
|
|
251
|
-
| **propose** | 一次产出 proposal、delta specs、design、tasks
|
|
280
|
+
| **propose** | 一次产出 proposal、delta specs、design、tasks(第一轮深度思考,非占位骨架);叙述语言跟 `artifacts.language` |
|
|
252
281
|
| **refine** | 内部多轮循环(≥2 轮,AI 判断收敛);可更新任意 artifact |
|
|
253
|
-
| **apply** | Phase A
|
|
282
|
+
| **apply** | Phase A 重写 tasks.md;Phase B 按语言路由 ECC 审查 + TDD 逐任务执行;可选 `--yes` |
|
|
254
283
|
| **review** | 代码审查,对照 specs 检查回归 |
|
|
255
284
|
| **test** | 单元 + 集成 + E2E + 回归测试 |
|
|
256
|
-
| **verify** | Pass 1:delta specs 验收;Pass 2:主 specs 回归(无基线时显式 skipped
|
|
257
|
-
| **archive** | 归档变更、delta merge、git
|
|
258
|
-
| **fix** | 修 Bug
|
|
285
|
+
| **verify** | Pass 1:delta specs 验收;Pass 2:主 specs 回归(无基线时显式 skipped);Web 另有安全 Pass |
|
|
286
|
+
| **archive** | 归档变更、delta merge、git 分支清理;单仓默认要求 phase=`apply` |
|
|
287
|
+
| **fix** | 修 Bug 一条龙(可加 `--urgent` 跳过审查) |
|
|
259
288
|
| **snap** | 从 git 历史反推变更并归档 |
|
|
260
289
|
|
|
261
290
|
> **v1.0.1 起命令已重命名**(对齐 OpenSpec 术语):`plan→propose`、`build→apply`、`done→archive`。`.specflow.yaml` 的 phase 枚举同步为 `propose/refined/apply/archived`(读取时兼容旧值 `plan`/`built`)。
|
|
@@ -290,7 +319,46 @@ CLI 从当前目录**向上查找**项目根(识别 `specflow/config.yaml`)
|
|
|
290
319
|
| 命令 | 说明 |
|
|
291
320
|
|------|------|
|
|
292
321
|
| `specflow validate <文件>` | 校验 spec 文件格式(WHEN/THEN scenario 等) |
|
|
293
|
-
| `specflow instructions <artifact> <change>` | 查看某 artifact
|
|
322
|
+
| `specflow instructions <artifact> <change>` | 查看某 artifact 的创建指令(含 `artifactLanguage` / `languageGuidance`) |
|
|
323
|
+
|
|
324
|
+
---
|
|
325
|
+
|
|
326
|
+
## 单仓常用能力速查
|
|
327
|
+
|
|
328
|
+
### 产物语言(中文 / 英文)
|
|
329
|
+
|
|
330
|
+
```bash
|
|
331
|
+
specflow init --artifact-language zh-CN # 推荐国内团队
|
|
332
|
+
# 已有项目:直接改 specflow/config.yaml → artifacts.language(init 不覆盖已有 config)
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
| 会本地化 | 保持英文 / 不变 |
|
|
336
|
+
|----------|-----------------|
|
|
337
|
+
| Why / What、场景叙述、design 说明、tasks 描述 | `ADDED Requirements`、`WHEN` / `THEN`、`Requirement:` / `Scenario:` |
|
|
338
|
+
| | Change ID、capability id、路径、命令、代码符号 |
|
|
339
|
+
|
|
340
|
+
切换语言只影响**之后新建或主动重写**的内容,不会自动翻译历史产物。
|
|
341
|
+
|
|
342
|
+
### `apply --yes`(非交互确认)
|
|
343
|
+
|
|
344
|
+
```text
|
|
345
|
+
/specflow:apply --yes
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
适合任务清单已确认、希望少打断连续落地的场景。质量门禁仍在:Block / 失败 / 设计缺口会停;通过后需你手动 archive。
|
|
349
|
+
|
|
350
|
+
### 语言感知代码审查(内置)
|
|
351
|
+
|
|
352
|
+
apply Phase B 在项目根检测语言信号(如 `go.mod`、`tsconfig.json`、`pyproject.toml`),选用对应 ECC reviewer;无匹配时用通用 reviewer。更深的语言惯用法可再挂 [语言 Profile](./LANGUAGE_PROFILE_USAGE_GUIDE.md)。
|
|
353
|
+
|
|
354
|
+
### 升级与健康检查
|
|
355
|
+
|
|
356
|
+
```bash
|
|
357
|
+
npm install -g @gordon.gan/specflow@beta # 或指定版本
|
|
358
|
+
specflow sync --ide all
|
|
359
|
+
specflow doctor --parity
|
|
360
|
+
specflow parity-report
|
|
361
|
+
```
|
|
294
362
|
|
|
295
363
|
---
|
|
296
364
|
|
|
@@ -4,6 +4,11 @@
|
|
|
4
4
|
* Creates a new change directory with .specflow.yaml metadata.
|
|
5
5
|
*/
|
|
6
6
|
import type { Command } from 'commander';
|
|
7
|
+
import { type UpstreamSpecSource } from '../../core/upstream-spec.js';
|
|
8
|
+
import type { StoreRegistry } from '../../core/store/foundation.js';
|
|
9
|
+
export interface CreateChangeOptions {
|
|
10
|
+
readonly source?: UpstreamSpecSource;
|
|
11
|
+
}
|
|
7
12
|
/**
|
|
8
13
|
* Creates a new change directory with initial metadata.
|
|
9
14
|
*
|
|
@@ -11,7 +16,12 @@ import type { Command } from 'commander';
|
|
|
11
16
|
* @param projectRoot - Absolute path to the project root
|
|
12
17
|
* @throws When the name is invalid or the change already exists
|
|
13
18
|
*/
|
|
14
|
-
export declare function createChange(name: string, projectRoot: string): Promise<void>;
|
|
19
|
+
export declare function createChange(name: string, projectRoot: string, options?: CreateChangeOptions): Promise<void>;
|
|
20
|
+
/**
|
|
21
|
+
* Creates a same-name implementation Spoke change bound to an archived
|
|
22
|
+
* baseline spec from the configured upstream store.
|
|
23
|
+
*/
|
|
24
|
+
export declare function createChangeFromSpec(specId: string, projectRoot: string, registry: StoreRegistry): Promise<void>;
|
|
15
25
|
/**
|
|
16
26
|
* Registers the `change new` subcommand with Commander.
|
|
17
27
|
*/
|
|
@@ -6,6 +6,10 @@
|
|
|
6
6
|
import { join } from 'node:path';
|
|
7
7
|
import { validateChangeName, writeChangeMetadata } from '../../utils/change-utils.js';
|
|
8
8
|
import { directoryExists } from '../../utils/file-system.js';
|
|
9
|
+
import { requireProjectRoot } from '../../utils/project-root.js';
|
|
10
|
+
import { resolveUpstreamSpec, } from '../../core/upstream-spec.js';
|
|
11
|
+
import { readRegistry } from '../../core/store/registry.js';
|
|
12
|
+
import { getStoreRegistryPath } from '../../core/global-config.js';
|
|
9
13
|
import { addStoreOption, resolveRootFromCommandOptions } from '../shared/store-option.js';
|
|
10
14
|
const CHANGES_REL_PATH = 'specflow/changes';
|
|
11
15
|
/**
|
|
@@ -25,7 +29,7 @@ function todayDate() {
|
|
|
25
29
|
* @param projectRoot - Absolute path to the project root
|
|
26
30
|
* @throws When the name is invalid or the change already exists
|
|
27
31
|
*/
|
|
28
|
-
export async function createChange(name, projectRoot) {
|
|
32
|
+
export async function createChange(name, projectRoot, options = {}) {
|
|
29
33
|
validateChangeName(name);
|
|
30
34
|
const changeDir = join(projectRoot, CHANGES_REL_PATH, name);
|
|
31
35
|
if (await directoryExists(changeDir)) {
|
|
@@ -35,17 +39,44 @@ export async function createChange(name, projectRoot) {
|
|
|
35
39
|
schema: 'specflow',
|
|
36
40
|
created: todayDate(),
|
|
37
41
|
phase: 'propose',
|
|
42
|
+
...(options.source ? { source: options.source } : {}),
|
|
38
43
|
};
|
|
39
44
|
await writeChangeMetadata(name, metadata, projectRoot);
|
|
40
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* Creates a same-name implementation Spoke change bound to an archived
|
|
48
|
+
* baseline spec from the configured upstream store.
|
|
49
|
+
*/
|
|
50
|
+
export async function createChangeFromSpec(specId, projectRoot, registry) {
|
|
51
|
+
validateChangeName(specId);
|
|
52
|
+
const resolved = await resolveUpstreamSpec(projectRoot, specId, registry);
|
|
53
|
+
await createChange(specId, projectRoot, { source: resolved.source });
|
|
54
|
+
}
|
|
41
55
|
/**
|
|
42
56
|
* Registers the `change new` subcommand with Commander.
|
|
43
57
|
*/
|
|
44
58
|
export function registerChangeNewCommand(changeCmd) {
|
|
45
59
|
addStoreOption(changeCmd
|
|
46
|
-
.command('new
|
|
60
|
+
.command('new [name]')
|
|
47
61
|
.description('Create a new change directory')
|
|
62
|
+
.option('--spec <id>', 'Create a same-name Spoke change from an upstream baseline spec')
|
|
48
63
|
.action(async (name, opts) => {
|
|
64
|
+
if (opts.spec) {
|
|
65
|
+
if (name) {
|
|
66
|
+
throw new Error('Do not pass a change name with --spec; the change name is the spec ID.');
|
|
67
|
+
}
|
|
68
|
+
if (opts.store) {
|
|
69
|
+
throw new Error('--store cannot be combined with --spec; workflow.upstream selects the Hub.');
|
|
70
|
+
}
|
|
71
|
+
const projectRoot = requireProjectRoot();
|
|
72
|
+
const registry = await readRegistry(getStoreRegistryPath());
|
|
73
|
+
await createChangeFromSpec(opts.spec, projectRoot, registry);
|
|
74
|
+
console.info(`Created change from upstream spec: ${opts.spec}`);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (!name) {
|
|
78
|
+
throw new Error('Change name is required unless --spec <id> is provided.');
|
|
79
|
+
}
|
|
49
80
|
const projectRoot = await resolveRootFromCommandOptions({ store: opts.store });
|
|
50
81
|
await createChange(name, projectRoot);
|
|
51
82
|
console.info(`Created change: ${name}`);
|
|
@@ -10,6 +10,8 @@ export interface InitResult {
|
|
|
10
10
|
export interface InitOptions {
|
|
11
11
|
readonly ide?: InitIdeTarget;
|
|
12
12
|
readonly artifactLanguage?: string;
|
|
13
|
+
readonly workflowProfile?: string;
|
|
14
|
+
readonly workflowUpstream?: string;
|
|
13
15
|
readonly forceAssets?: boolean;
|
|
14
16
|
readonly parityStrict?: boolean;
|
|
15
17
|
}
|
|
@@ -8,8 +8,24 @@ import { appendManagedBlock } from '../../integrations/shared/marker-write.js';
|
|
|
8
8
|
import { getRegeneratableIgnoreLines } from '../../integrations/shared/managed-assets.js';
|
|
9
9
|
import { detectMigrationState } from '../../integrations/shared/migration-state.js';
|
|
10
10
|
import { DEFAULT_ARTIFACT_LANGUAGE, requireArtifactLanguage, } from '../../core/artifact-language.js';
|
|
11
|
-
|
|
11
|
+
import { WORKFLOW_PROFILES, } from '../../core/project-config.js';
|
|
12
|
+
import { KEBAB_CASE_STORE_ID } from '../../core/store/foundation.js';
|
|
13
|
+
function requireWorkflowProfile(profile) {
|
|
14
|
+
if (WORKFLOW_PROFILES.includes(profile)) {
|
|
15
|
+
return profile;
|
|
16
|
+
}
|
|
17
|
+
throw new Error(`Unsupported workflow profile "${profile}". Supported values: ${WORKFLOW_PROFILES.join(', ')}.`);
|
|
18
|
+
}
|
|
19
|
+
function renderConfigYaml(artifactLanguage, workflowProfile, workflowUpstream) {
|
|
20
|
+
const workflowBlock = workflowProfile
|
|
21
|
+
? `
|
|
22
|
+
workflow:
|
|
23
|
+
profile: ${workflowProfile}${workflowUpstream ? `\n upstream: ${workflowUpstream}` : ''}
|
|
24
|
+
${workflowUpstream ? `\nreferences:\n - ${workflowUpstream}` : ''}
|
|
25
|
+
`
|
|
26
|
+
: '';
|
|
12
27
|
return `schema: specflow
|
|
28
|
+
${workflowBlock}
|
|
13
29
|
|
|
14
30
|
artifacts:
|
|
15
31
|
language: ${artifactLanguage}
|
|
@@ -38,8 +54,8 @@ async function createDirectoryStructure(projectRoot) {
|
|
|
38
54
|
fs.mkdir(join(projectRoot, 'specflow', 'specs'), { recursive: true }),
|
|
39
55
|
]);
|
|
40
56
|
}
|
|
41
|
-
async function writeConfig(projectRoot, artifactLanguage) {
|
|
42
|
-
await fs.writeFile(join(projectRoot, 'specflow', 'config.yaml'), renderConfigYaml(artifactLanguage), 'utf-8');
|
|
57
|
+
async function writeConfig(projectRoot, artifactLanguage, workflowProfile, workflowUpstream) {
|
|
58
|
+
await fs.writeFile(join(projectRoot, 'specflow', 'config.yaml'), renderConfigYaml(artifactLanguage, workflowProfile, workflowUpstream), 'utf-8');
|
|
43
59
|
}
|
|
44
60
|
async function pathExists(path) {
|
|
45
61
|
try {
|
|
@@ -61,6 +77,22 @@ export async function initProject(projectRoot, packageRoot, options = {}) {
|
|
|
61
77
|
const artifactLanguage = options.artifactLanguage === undefined
|
|
62
78
|
? DEFAULT_ARTIFACT_LANGUAGE
|
|
63
79
|
: requireArtifactLanguage(options.artifactLanguage);
|
|
80
|
+
const workflowProfile = options.workflowProfile === undefined
|
|
81
|
+
? undefined
|
|
82
|
+
: requireWorkflowProfile(options.workflowProfile);
|
|
83
|
+
const workflowUpstream = options.workflowUpstream;
|
|
84
|
+
if (workflowProfile === 'implementation-spoke' &&
|
|
85
|
+
workflowUpstream === undefined) {
|
|
86
|
+
throw new Error('The implementation-spoke workflow profile requires --upstream <store-id>.');
|
|
87
|
+
}
|
|
88
|
+
if (workflowUpstream !== undefined &&
|
|
89
|
+
!KEBAB_CASE_STORE_ID.test(workflowUpstream)) {
|
|
90
|
+
throw new Error('Workflow upstream must be a kebab-case store ID.');
|
|
91
|
+
}
|
|
92
|
+
if (workflowUpstream !== undefined &&
|
|
93
|
+
workflowProfile !== 'implementation-spoke') {
|
|
94
|
+
throw new Error('--upstream is only valid with --workflow-profile implementation-spoke.');
|
|
95
|
+
}
|
|
64
96
|
const ide = options.ide ?? 'both';
|
|
65
97
|
const forceAssets = options.forceAssets ?? false;
|
|
66
98
|
const parityStrict = options.parityStrict ?? true;
|
|
@@ -69,7 +101,7 @@ export async function initProject(projectRoot, packageRoot, options = {}) {
|
|
|
69
101
|
const migrationState = await detectMigrationState(projectRoot);
|
|
70
102
|
if (!initialized) {
|
|
71
103
|
await createDirectoryStructure(projectRoot);
|
|
72
|
-
await writeConfig(projectRoot, artifactLanguage);
|
|
104
|
+
await writeConfig(projectRoot, artifactLanguage, workflowProfile, workflowUpstream);
|
|
73
105
|
}
|
|
74
106
|
const languageEditMessage = options.artifactLanguage
|
|
75
107
|
? ' Edit specflow/config.yaml explicitly to change artifact language in an initialized project.'
|
|
@@ -115,6 +147,8 @@ export function registerInitCommand(program) {
|
|
|
115
147
|
.description('Initialize a project with specflow directory structure and assets')
|
|
116
148
|
.option('--ide <target>', 'Target IDE assets: claude | cursor | codex | both | all', 'both')
|
|
117
149
|
.option('--artifact-language <language>', 'Artifact content language: en | zh-CN (alias: zh)')
|
|
150
|
+
.option('--workflow-profile <profile>', 'Workflow profile: standalone | contract-hub | implementation-spoke')
|
|
151
|
+
.option('--upstream <store-id>', 'Primary contract store for implementation-spoke projects')
|
|
118
152
|
.option('--force-assets', 'Refresh managed IDE assets even when project is initialized')
|
|
119
153
|
.option('--no-parity-strict', 'Disable strict parity validation after asset generation')
|
|
120
154
|
.action(async (opts) => {
|
|
@@ -123,6 +157,8 @@ export function registerInitCommand(program) {
|
|
|
123
157
|
const result = await initProject(projectRoot, packageRoot, {
|
|
124
158
|
ide: opts.ide ?? 'both',
|
|
125
159
|
artifactLanguage: opts.artifactLanguage,
|
|
160
|
+
workflowProfile: opts.workflowProfile,
|
|
161
|
+
workflowUpstream: opts.upstream,
|
|
126
162
|
forceAssets: opts.forceAssets ?? false,
|
|
127
163
|
parityStrict: opts.parityStrict ?? true,
|
|
128
164
|
});
|
package/dist/core/archive.js
CHANGED
|
@@ -8,8 +8,10 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { promises as fs } from 'node:fs';
|
|
10
10
|
import { join, relative } from 'node:path';
|
|
11
|
+
import yaml from 'js-yaml';
|
|
11
12
|
import { validateSpec } from './validation/validator.js';
|
|
12
13
|
import { applyDeltaSpec } from './specs-apply.js';
|
|
14
|
+
import { parseProjectConfig } from './project-config.js';
|
|
13
15
|
import { readChangeMetadata, writeChangeMetadata, } from '../utils/change-metadata.js';
|
|
14
16
|
/**
|
|
15
17
|
* Recursively list all `.md` files in a directory, returning paths relative to the base dir.
|
|
@@ -50,6 +52,15 @@ function todayDatePrefix() {
|
|
|
50
52
|
const day = String(now.getDate()).padStart(2, '0');
|
|
51
53
|
return `${year}-${month}-${day}`;
|
|
52
54
|
}
|
|
55
|
+
async function readWorkflowProfile(projectRoot) {
|
|
56
|
+
try {
|
|
57
|
+
const content = await fs.readFile(join(projectRoot, 'specflow', 'config.yaml'), 'utf-8');
|
|
58
|
+
return parseProjectConfig(yaml.load(content)).workflowProfile;
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return 'standalone';
|
|
62
|
+
}
|
|
63
|
+
}
|
|
53
64
|
/**
|
|
54
65
|
* Archive a completed change.
|
|
55
66
|
*
|
|
@@ -72,19 +83,26 @@ export async function archiveChange(changeName, projectRoot, options = {}) {
|
|
|
72
83
|
const changeDir = join(projectRoot, 'specflow', 'changes', changeName);
|
|
73
84
|
const deltaSpecsDir = join(changeDir, 'specs');
|
|
74
85
|
const mainSpecsDir = join(projectRoot, 'specflow', 'specs');
|
|
75
|
-
// 0. Phase gate:
|
|
86
|
+
// 0. Phase gate: contract Hubs may archive refined contracts; all other
|
|
87
|
+
// profiles retain the existing phase=apply requirement.
|
|
76
88
|
const metadata = await readChangeMetadata(changeDir);
|
|
77
89
|
const currentPhase = metadata?.phase;
|
|
78
|
-
|
|
90
|
+
const workflowProfile = await readWorkflowProfile(projectRoot);
|
|
91
|
+
const phaseAllowed = currentPhase === 'apply' ||
|
|
92
|
+
(workflowProfile === 'contract-hub' && currentPhase === 'refined');
|
|
93
|
+
if (!phaseAllowed && !options.force) {
|
|
79
94
|
const phaseLabel = currentPhase ?? 'unknown';
|
|
95
|
+
const expectation = workflowProfile === 'contract-hub'
|
|
96
|
+
? "expected 'refined' or 'apply'. Complete '/specflow:refine' first"
|
|
97
|
+
: "expected 'apply'. Complete '/specflow:apply' first";
|
|
80
98
|
return {
|
|
81
99
|
success: false,
|
|
82
100
|
errors: [
|
|
83
|
-
`Cannot archive: change '${changeName}' is in phase '${phaseLabel}',
|
|
101
|
+
`Cannot archive: change '${changeName}' is in phase '${phaseLabel}', ${expectation}, or pass '--force' to archive anyway.`,
|
|
84
102
|
],
|
|
85
103
|
};
|
|
86
104
|
}
|
|
87
|
-
if (options.force &&
|
|
105
|
+
if (options.force && !phaseAllowed) {
|
|
88
106
|
const phaseLabel = currentPhase ?? 'unknown';
|
|
89
107
|
console.warn(`Warning: archiving "${changeName}" in phase ${phaseLabel} with --force. ` +
|
|
90
108
|
`Consider running /specflow:apply first.`);
|
|
@@ -77,7 +77,6 @@ export declare const SchemaYamlSchema: z.ZodObject<{
|
|
|
77
77
|
tracks?: string | null | undefined;
|
|
78
78
|
}>>;
|
|
79
79
|
}, "strip", z.ZodTypeAny, {
|
|
80
|
-
version: number;
|
|
81
80
|
artifacts: {
|
|
82
81
|
id: string;
|
|
83
82
|
generates: string;
|
|
@@ -85,6 +84,7 @@ export declare const SchemaYamlSchema: z.ZodObject<{
|
|
|
85
84
|
requires: string[];
|
|
86
85
|
instruction?: string | undefined;
|
|
87
86
|
}[];
|
|
87
|
+
version: number;
|
|
88
88
|
name: string;
|
|
89
89
|
apply?: {
|
|
90
90
|
requires: string[];
|
|
@@ -93,7 +93,6 @@ export declare const SchemaYamlSchema: z.ZodObject<{
|
|
|
93
93
|
} | undefined;
|
|
94
94
|
description?: string | undefined;
|
|
95
95
|
}, {
|
|
96
|
-
version: number;
|
|
97
96
|
artifacts: {
|
|
98
97
|
id: string;
|
|
99
98
|
generates: string;
|
|
@@ -101,6 +100,7 @@ export declare const SchemaYamlSchema: z.ZodObject<{
|
|
|
101
100
|
instruction?: string | undefined;
|
|
102
101
|
requires?: string[] | undefined;
|
|
103
102
|
}[];
|
|
103
|
+
version: number;
|
|
104
104
|
name: string;
|
|
105
105
|
apply?: {
|
|
106
106
|
requires: string[];
|
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
import { type SpawnSyncOptions } from 'node:child_process';
|
|
1
2
|
import type { OpenerDefinition } from './openers.js';
|
|
3
|
+
export interface OpenerSpawnPlan {
|
|
4
|
+
readonly command: string;
|
|
5
|
+
readonly args: string[];
|
|
6
|
+
readonly options: SpawnSyncOptions;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Build spawn argv for launching a workspace-file opener.
|
|
10
|
+
*
|
|
11
|
+
* On Windows, Cursor/VS Code ship as `*.cmd` shims. Node's spawn with
|
|
12
|
+
* `shell: false` cannot execute those directly (ENOENT). Follow Node's
|
|
13
|
+
* recommended pattern: `cmd.exe /d /s /c` with a single quoted command line
|
|
14
|
+
* and `windowsVerbatimArguments`, without enabling shell interpolation.
|
|
15
|
+
*/
|
|
16
|
+
export declare function buildOpenerSpawn(opener: OpenerDefinition, workspacePath: string, primaryPath: string, platform?: NodeJS.Platform, comSpec?: string): OpenerSpawnPlan;
|
|
2
17
|
export declare function isOpenerAvailable(opener: OpenerDefinition): boolean;
|
|
3
18
|
export declare function launchOpener(opener: OpenerDefinition, workspacePath: string, primaryPath: string): void;
|
|
@@ -1,16 +1,47 @@
|
|
|
1
1
|
import { spawnSync } from 'node:child_process';
|
|
2
|
+
/**
|
|
3
|
+
* Build spawn argv for launching a workspace-file opener.
|
|
4
|
+
*
|
|
5
|
+
* On Windows, Cursor/VS Code ship as `*.cmd` shims. Node's spawn with
|
|
6
|
+
* `shell: false` cannot execute those directly (ENOENT). Follow Node's
|
|
7
|
+
* recommended pattern: `cmd.exe /d /s /c` with a single quoted command line
|
|
8
|
+
* and `windowsVerbatimArguments`, without enabling shell interpolation.
|
|
9
|
+
*/
|
|
10
|
+
export function buildOpenerSpawn(opener, workspacePath, primaryPath, platform = process.platform, comSpec = process.env.ComSpec || 'cmd.exe') {
|
|
11
|
+
const baseOptions = {
|
|
12
|
+
cwd: primaryPath,
|
|
13
|
+
shell: false,
|
|
14
|
+
stdio: 'inherit',
|
|
15
|
+
};
|
|
16
|
+
if (platform !== 'win32') {
|
|
17
|
+
return {
|
|
18
|
+
command: opener.command,
|
|
19
|
+
args: [workspacePath],
|
|
20
|
+
options: baseOptions,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
// Escape " for cmd.exe by doubling; wrap each token so spaces are preserved.
|
|
24
|
+
const quote = (value) => `"${value.replace(/"/g, '""')}"`;
|
|
25
|
+
return {
|
|
26
|
+
command: comSpec,
|
|
27
|
+
args: ['/d', '/s', '/c', `${quote(opener.command)} ${quote(workspacePath)}`],
|
|
28
|
+
options: {
|
|
29
|
+
...baseOptions,
|
|
30
|
+
windowsVerbatimArguments: true,
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
2
34
|
export function isOpenerAvailable(opener) {
|
|
35
|
+
// `where.exe` resolves .cmd/.bat shims the same way cmd does.
|
|
3
36
|
const result = spawnSync(process.platform === 'win32' ? 'where' : 'which', [opener.command], {
|
|
4
37
|
stdio: 'ignore',
|
|
38
|
+
shell: false,
|
|
5
39
|
});
|
|
6
40
|
return result.status === 0;
|
|
7
41
|
}
|
|
8
42
|
export function launchOpener(opener, workspacePath, primaryPath) {
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
shell: false,
|
|
12
|
-
stdio: 'inherit',
|
|
13
|
-
});
|
|
43
|
+
const plan = buildOpenerSpawn(opener, workspacePath, primaryPath);
|
|
44
|
+
const child = spawnSync(plan.command, plan.args, plan.options);
|
|
14
45
|
if (child.error) {
|
|
15
46
|
throw child.error;
|
|
16
47
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { Diagnostic } from './diagnostics.js';
|
|
2
2
|
import { type ArtifactLanguage } from './artifact-language.js';
|
|
3
|
+
export declare const WORKFLOW_PROFILES: readonly ["standalone", "contract-hub", "implementation-spoke"];
|
|
4
|
+
export type WorkflowProfile = (typeof WORKFLOW_PROFILES)[number];
|
|
3
5
|
export interface NormalizedReference {
|
|
4
6
|
readonly id: string;
|
|
5
7
|
readonly remote?: string;
|
|
@@ -8,6 +10,8 @@ export interface ParsedProjectConfig {
|
|
|
8
10
|
readonly schema: string;
|
|
9
11
|
readonly context?: string;
|
|
10
12
|
readonly artifactLanguage: ArtifactLanguage;
|
|
13
|
+
readonly workflowProfile: WorkflowProfile;
|
|
14
|
+
readonly workflowUpstream?: string;
|
|
11
15
|
readonly store?: string;
|
|
12
16
|
readonly references: readonly NormalizedReference[];
|
|
13
17
|
readonly diagnostics: readonly Diagnostic[];
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { DEFAULT_ARTIFACT_LANGUAGE, isArtifactLanguage, } from './artifact-language.js';
|
|
3
3
|
const KEBAB_CASE_ID = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
|
|
4
|
+
export const WORKFLOW_PROFILES = [
|
|
5
|
+
'standalone',
|
|
6
|
+
'contract-hub',
|
|
7
|
+
'implementation-spoke',
|
|
8
|
+
];
|
|
4
9
|
const ReferenceObjectSchema = z
|
|
5
10
|
.object({
|
|
6
11
|
id: z.string().regex(KEBAB_CASE_ID),
|
|
@@ -22,6 +27,8 @@ export function parseProjectConfig(raw) {
|
|
|
22
27
|
const schema = typeof record.schema === 'string' ? record.schema : 'specflow';
|
|
23
28
|
const context = typeof record.context === 'string' ? record.context : undefined;
|
|
24
29
|
let artifactLanguage = DEFAULT_ARTIFACT_LANGUAGE;
|
|
30
|
+
let workflowProfile = 'standalone';
|
|
31
|
+
let workflowUpstream;
|
|
25
32
|
if (record.artifacts !== undefined) {
|
|
26
33
|
const artifacts = typeof record.artifacts === 'object' &&
|
|
27
34
|
record.artifacts !== null &&
|
|
@@ -90,10 +97,70 @@ export function parseProjectConfig(raw) {
|
|
|
90
97
|
});
|
|
91
98
|
}
|
|
92
99
|
}
|
|
100
|
+
if (record.workflow !== undefined) {
|
|
101
|
+
const workflow = typeof record.workflow === 'object' &&
|
|
102
|
+
record.workflow !== null &&
|
|
103
|
+
!Array.isArray(record.workflow)
|
|
104
|
+
? record.workflow
|
|
105
|
+
: undefined;
|
|
106
|
+
if (!workflow) {
|
|
107
|
+
diagnostics.push({
|
|
108
|
+
severity: 'error',
|
|
109
|
+
code: 'invalid_workflow_config',
|
|
110
|
+
message: 'Project workflow configuration must be an object.',
|
|
111
|
+
target: 'config.workflow',
|
|
112
|
+
fix: 'Set workflow.profile to standalone, contract-hub, or implementation-spoke.',
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
const configuredProfile = workflow.profile;
|
|
117
|
+
if (typeof configuredProfile === 'string' &&
|
|
118
|
+
WORKFLOW_PROFILES.includes(configuredProfile)) {
|
|
119
|
+
workflowProfile = configuredProfile;
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
diagnostics.push({
|
|
123
|
+
severity: 'error',
|
|
124
|
+
code: 'invalid_workflow_profile',
|
|
125
|
+
message: "Workflow profile must be one of: 'standalone', 'contract-hub', 'implementation-spoke'.",
|
|
126
|
+
target: 'config.workflow.profile',
|
|
127
|
+
fix: 'Choose a supported workflow profile.',
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
if (workflow.upstream !== undefined) {
|
|
131
|
+
if (typeof workflow.upstream === 'string' &&
|
|
132
|
+
KEBAB_CASE_ID.test(workflow.upstream)) {
|
|
133
|
+
workflowUpstream = workflow.upstream;
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
diagnostics.push({
|
|
137
|
+
severity: 'error',
|
|
138
|
+
code: 'invalid_workflow_upstream',
|
|
139
|
+
message: 'Workflow upstream must be a kebab-case store ID.',
|
|
140
|
+
target: 'config.workflow.upstream',
|
|
141
|
+
fix: 'Set workflow.upstream to a referenced store ID.',
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (workflowProfile === 'implementation-spoke' &&
|
|
148
|
+
workflowUpstream !== undefined &&
|
|
149
|
+
!references.some((reference) => reference.id === workflowUpstream)) {
|
|
150
|
+
diagnostics.push({
|
|
151
|
+
severity: 'error',
|
|
152
|
+
code: 'workflow_upstream_not_referenced',
|
|
153
|
+
message: `Workflow upstream '${workflowUpstream}' must also be declared in references.`,
|
|
154
|
+
target: 'config.workflow.upstream',
|
|
155
|
+
fix: `Add '${workflowUpstream}' to specflow/config.yaml references.`,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
93
158
|
return {
|
|
94
159
|
schema,
|
|
95
160
|
context,
|
|
96
161
|
artifactLanguage,
|
|
162
|
+
workflowProfile,
|
|
163
|
+
workflowUpstream,
|
|
97
164
|
store,
|
|
98
165
|
references,
|
|
99
166
|
diagnostics,
|
|
@@ -6,12 +6,12 @@ export declare const StoreIdentitySchema: z.ZodObject<{
|
|
|
6
6
|
id: z.ZodString;
|
|
7
7
|
remote: z.ZodOptional<z.ZodString>;
|
|
8
8
|
}, "strict", z.ZodTypeAny, {
|
|
9
|
-
version: 1;
|
|
10
9
|
id: string;
|
|
10
|
+
version: 1;
|
|
11
11
|
remote?: string | undefined;
|
|
12
12
|
}, {
|
|
13
|
-
version: 1;
|
|
14
13
|
id: string;
|
|
14
|
+
version: 1;
|
|
15
15
|
remote?: string | undefined;
|
|
16
16
|
}>;
|
|
17
17
|
export type StoreIdentity = z.infer<typeof StoreIdentitySchema>;
|
|
@@ -105,6 +105,7 @@ export declare const StoreRegistrySchema: z.ZodObject<{
|
|
|
105
105
|
};
|
|
106
106
|
}>>;
|
|
107
107
|
}, "strict", z.ZodTypeAny, {
|
|
108
|
+
version: 1;
|
|
108
109
|
stores: Record<string, {
|
|
109
110
|
provenance: "managed" | "external";
|
|
110
111
|
backend: {
|
|
@@ -114,8 +115,8 @@ export declare const StoreRegistrySchema: z.ZodObject<{
|
|
|
114
115
|
branch?: string | undefined;
|
|
115
116
|
};
|
|
116
117
|
}>;
|
|
117
|
-
version: 1;
|
|
118
118
|
}, {
|
|
119
|
+
version: 1;
|
|
119
120
|
stores: Record<string, {
|
|
120
121
|
provenance: "managed" | "external";
|
|
121
122
|
backend: {
|
|
@@ -125,7 +126,6 @@ export declare const StoreRegistrySchema: z.ZodObject<{
|
|
|
125
126
|
branch?: string | undefined;
|
|
126
127
|
};
|
|
127
128
|
}>;
|
|
128
|
-
version: 1;
|
|
129
129
|
}>;
|
|
130
130
|
export type StoreRegistry = z.infer<typeof StoreRegistrySchema>;
|
|
131
131
|
export type RegistryEntry = z.infer<typeof RegistryEntrySchema>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { StoreRegistry } from './store/foundation.js';
|
|
2
|
+
export interface UpstreamSpecSource {
|
|
3
|
+
readonly store: string;
|
|
4
|
+
readonly spec: string;
|
|
5
|
+
readonly digest: string;
|
|
6
|
+
readonly commit?: string;
|
|
7
|
+
readonly tag?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface ResolvedUpstreamSpec {
|
|
10
|
+
readonly source: UpstreamSpecSource;
|
|
11
|
+
readonly content: string;
|
|
12
|
+
readonly path: string;
|
|
13
|
+
}
|
|
14
|
+
export declare function resolveUpstreamSpec(projectRoot: string, specId: string, registry: StoreRegistry): Promise<ResolvedUpstreamSpec>;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { execFileSync } from 'node:child_process';
|
|
3
|
+
import { promises as fs } from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import yaml from 'js-yaml';
|
|
6
|
+
import { parseProjectConfig } from './project-config.js';
|
|
7
|
+
function readGitValue(root, args) {
|
|
8
|
+
try {
|
|
9
|
+
const value = execFileSync('git', ['-C', root, ...args], {
|
|
10
|
+
encoding: 'utf-8',
|
|
11
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
12
|
+
}).trim();
|
|
13
|
+
return value || undefined;
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export async function resolveUpstreamSpec(projectRoot, specId, registry) {
|
|
20
|
+
const configPath = join(projectRoot, 'specflow', 'config.yaml');
|
|
21
|
+
let rawConfig;
|
|
22
|
+
try {
|
|
23
|
+
rawConfig = yaml.load(await fs.readFile(configPath, 'utf-8'));
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
throw new Error(`Cannot read Spoke configuration at ${configPath}.`);
|
|
27
|
+
}
|
|
28
|
+
const config = parseProjectConfig(rawConfig);
|
|
29
|
+
if (config.workflowProfile !== 'implementation-spoke') {
|
|
30
|
+
throw new Error("The --spec mode requires workflow.profile 'implementation-spoke'. " +
|
|
31
|
+
'Without --spec, use the existing single-repository flow.');
|
|
32
|
+
}
|
|
33
|
+
if (!config.workflowUpstream) {
|
|
34
|
+
throw new Error('The --spec mode requires workflow.upstream in specflow/config.yaml.');
|
|
35
|
+
}
|
|
36
|
+
const blockingDiagnostic = config.diagnostics.find((diagnostic) => diagnostic.severity === 'error');
|
|
37
|
+
if (blockingDiagnostic) {
|
|
38
|
+
throw new Error(blockingDiagnostic.message);
|
|
39
|
+
}
|
|
40
|
+
const storeId = config.workflowUpstream;
|
|
41
|
+
const store = registry.stores[storeId];
|
|
42
|
+
if (!store) {
|
|
43
|
+
throw new Error(`Upstream store '${storeId}' is not registered. ` +
|
|
44
|
+
`Run specflow store register <path> --id ${storeId} --yes.`);
|
|
45
|
+
}
|
|
46
|
+
const specPath = join(store.backend.local_path, 'specflow', 'specs', specId, 'spec.md');
|
|
47
|
+
let content;
|
|
48
|
+
try {
|
|
49
|
+
content = await fs.readFile(specPath, 'utf-8');
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
throw new Error(`Upstream spec '${specId}' was not found in store '${storeId}'. ` +
|
|
53
|
+
'The Hub must archive the spec before a Spoke can propose from it.');
|
|
54
|
+
}
|
|
55
|
+
const digest = `sha256:${createHash('sha256').update(content).digest('hex')}`;
|
|
56
|
+
const commit = readGitValue(store.backend.local_path, ['rev-parse', 'HEAD']);
|
|
57
|
+
const tag = readGitValue(store.backend.local_path, [
|
|
58
|
+
'describe',
|
|
59
|
+
'--tags',
|
|
60
|
+
'--exact-match',
|
|
61
|
+
'HEAD',
|
|
62
|
+
]);
|
|
63
|
+
return {
|
|
64
|
+
source: {
|
|
65
|
+
store: storeId,
|
|
66
|
+
spec: specId,
|
|
67
|
+
digest,
|
|
68
|
+
...(commit ? { commit } : {}),
|
|
69
|
+
...(tag ? { tag } : {}),
|
|
70
|
+
},
|
|
71
|
+
content,
|
|
72
|
+
path: specPath,
|
|
73
|
+
};
|
|
74
|
+
}
|
package/dist/core/worksets.d.ts
CHANGED
|
@@ -39,6 +39,7 @@ declare const WorksetsStateSchema: z.ZodObject<{
|
|
|
39
39
|
tool?: string | undefined;
|
|
40
40
|
}>, "many">;
|
|
41
41
|
}, "strip", z.ZodTypeAny, {
|
|
42
|
+
version: 1;
|
|
42
43
|
worksets: {
|
|
43
44
|
name: string;
|
|
44
45
|
members: {
|
|
@@ -47,8 +48,8 @@ declare const WorksetsStateSchema: z.ZodObject<{
|
|
|
47
48
|
}[];
|
|
48
49
|
tool?: string | undefined;
|
|
49
50
|
}[];
|
|
50
|
-
version: 1;
|
|
51
51
|
}, {
|
|
52
|
+
version: 1;
|
|
52
53
|
worksets: {
|
|
53
54
|
name: string;
|
|
54
55
|
members: {
|
|
@@ -57,7 +58,6 @@ declare const WorksetsStateSchema: z.ZodObject<{
|
|
|
57
58
|
}[];
|
|
58
59
|
tool?: string | undefined;
|
|
59
60
|
}[];
|
|
60
|
-
version: 1;
|
|
61
61
|
}>;
|
|
62
62
|
export type WorksetsState = z.infer<typeof WorksetsStateSchema>;
|
|
63
63
|
export declare function validateWorksetName(name: string): void;
|
package/package.json
CHANGED
|
@@ -9,13 +9,23 @@ description: "Archive change + merge specs + git branch cleanup"
|
|
|
9
9
|
|
|
10
10
|
## Prerequisites
|
|
11
11
|
|
|
12
|
-
- An active change must exist
|
|
12
|
+
- An active change must exist.
|
|
13
13
|
- `specflow` CLI must be available on PATH.
|
|
14
|
-
-
|
|
14
|
+
- Read `specflow/config.yaml` and apply its workflow profile gate:
|
|
15
|
+
- Default, `workflow.profile: standalone`, and `implementation-spoke` require phase `apply`.
|
|
16
|
+
- `workflow.profile: contract-hub` permits phase `refined` or `apply`.
|
|
17
|
+
- A contract Hub publishes validated contract/specification artifacts and does not require business implementation. Do not route a refined contract Hub through a fake `/specflow:apply`.
|
|
18
|
+
- `specflow change archive` enforces these profile-aware gates. Do NOT pass `--force` from this skill; the flag remains reserved for explicit user discretion.
|
|
15
19
|
|
|
16
20
|
## Stage 1: Test Gate
|
|
17
21
|
|
|
18
|
-
|
|
22
|
+
For standalone and implementation Spoke projects, run the project test suite
|
|
23
|
+
for all affected modules. If implementation was done in a git worktree (common
|
|
24
|
+
after `/specflow:apply`), run tests inside that worktree.
|
|
25
|
+
|
|
26
|
+
For a contract Hub archiving from `refined`, run SpecFlow validation for every
|
|
27
|
+
delta spec and the repository's contract/schema tests. Business implementation
|
|
28
|
+
tests are not required when the repository has no business implementation.
|
|
19
29
|
|
|
20
30
|
If tests fail:
|
|
21
31
|
- Report failures to the user.
|
|
@@ -13,6 +13,53 @@ Propose is the **first-iteration deep-analysis pass**: in a single invocation it
|
|
|
13
13
|
|
|
14
14
|
Treat every artifact here as "v1, to be iterated on" — depth matters, but so does moving through all four stages in one pass.
|
|
15
15
|
|
|
16
|
+
## Invocation Modes
|
|
17
|
+
|
|
18
|
+
### Existing single-repository flow (default)
|
|
19
|
+
|
|
20
|
+
Without `--spec`, preserve the existing single-repository flow exactly:
|
|
21
|
+
|
|
22
|
+
```text
|
|
23
|
+
/specflow:propose <change-name>
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Do not require workflow profiles, references, or an upstream store in this mode.
|
|
27
|
+
|
|
28
|
+
### Upstream Spec mode (implementation Spoke)
|
|
29
|
+
|
|
30
|
+
An implementation Spoke may derive a same-name local change from one archived
|
|
31
|
+
Hub baseline spec:
|
|
32
|
+
|
|
33
|
+
```text
|
|
34
|
+
/specflow:propose --spec <spec-id>
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`--spec` is explicit and takes exactly one kebab-case spec ID. In this mode:
|
|
38
|
+
|
|
39
|
+
1. Read `specflow/config.yaml`.
|
|
40
|
+
2. Require `workflow.profile: implementation-spoke`.
|
|
41
|
+
3. Require `workflow.upstream` and require that store ID in `references`.
|
|
42
|
+
4. Run `specflow change new --spec <spec-id>`. The CLI resolves the registered
|
|
43
|
+
upstream, requires `specflow/specs/<spec-id>/spec.md`, creates a same-name
|
|
44
|
+
local change, and records the source store/spec/digest plus Git commit/tag
|
|
45
|
+
when available.
|
|
46
|
+
5. Run `specflow show <spec-id> --type spec --store <workflow.upstream>` and
|
|
47
|
+
read the complete baseline spec before generating artifacts.
|
|
48
|
+
|
|
49
|
+
If the Hub has not archived the spec into its baseline `specs/` directory,
|
|
50
|
+
stop. Do not read an active Hub change as a stable dependency.
|
|
51
|
+
|
|
52
|
+
The Hub baseline spec is authoritative for cross-repository behavior, but all
|
|
53
|
+
four generated artifacts belong to the current Spoke:
|
|
54
|
+
|
|
55
|
+
- proposal: cite the source binding and limit impact to this repository;
|
|
56
|
+
- delta specs: express only this repository's testable behavior;
|
|
57
|
+
- design: describe this repository's implementation decisions;
|
|
58
|
+
- tasks: contain only this repository's implementation work.
|
|
59
|
+
|
|
60
|
+
Do not copy Hub proposal/design/tasks into the Spoke and do not redefine the
|
|
61
|
+
cross-repository contract.
|
|
62
|
+
|
|
16
63
|
## Prerequisites
|
|
17
64
|
|
|
18
65
|
- `specflow/specs/` directory should exist, indicating this is a specflow-initialized project. For a brand-new greenfield change with no existing specs, proceed — the prompt handles that case. If `specflow/` itself does not exist, suggest running `specflow init` first.
|
|
@@ -26,6 +73,10 @@ or tasks. Reuse the resolved policy for all four artifacts.
|
|
|
26
73
|
|
|
27
74
|
## Stage 0: Explore Handoff (when explore.md exists)
|
|
28
75
|
|
|
76
|
+
In Upstream Spec mode, use the resolved Hub baseline spec as the requirements
|
|
77
|
+
handoff and skip the local explore requirement. Otherwise follow the existing
|
|
78
|
+
explore handoff below.
|
|
79
|
+
|
|
29
80
|
Before creating a new change or generating a proposal, check for an existing exploration artifact:
|
|
30
81
|
|
|
31
82
|
```bash
|
|
@@ -53,7 +104,11 @@ ls specflow/changes/<name>/explore.md 2>/dev/null
|
|
|
53
104
|
|
|
54
105
|
## Stage 1: Create Change
|
|
55
106
|
|
|
56
|
-
|
|
107
|
+
- Upstream Spec mode: if the same-name change does not exist, run
|
|
108
|
+
`specflow change new --spec <spec-id>`. If it already exists, read
|
|
109
|
+
`.specflow.yaml`, require its `source.store` and `source.spec` to match the
|
|
110
|
+
current configuration and argument, and resume without overwriting artifacts.
|
|
111
|
+
- Existing single-repository flow: run `specflow change new <name>`.
|
|
57
112
|
|
|
58
113
|
The CLI automatically sets `phase=propose` in `.specflow.yaml` on creation (no separate phase call needed here).
|
|
59
114
|
|
|
@@ -65,6 +120,9 @@ Read the file at `.claude/specflow/prompts/propose/proposal.md` and follow its i
|
|
|
65
120
|
|
|
66
121
|
Generate the proposal document inside the change directory at `specflow/changes/<name>/proposal.md`.
|
|
67
122
|
|
|
123
|
+
In Upstream Spec mode, the proposal MUST name the bound upstream store, spec,
|
|
124
|
+
digest, and commit/tag when present in `.specflow.yaml`.
|
|
125
|
+
|
|
68
126
|
### Gate: Proposal Confirmation (HARD GATE)
|
|
69
127
|
|
|
70
128
|
Present the proposal to the user.
|
|
@@ -77,6 +135,9 @@ Read the file at `.claude/specflow/prompts/propose/specs.md` and follow its inst
|
|
|
77
135
|
|
|
78
136
|
Generate delta specs inside the change directory at `specflow/changes/<name>/specs/<capability>/spec.md`.
|
|
79
137
|
|
|
138
|
+
In Upstream Spec mode, derive these scenarios from the Hub baseline while
|
|
139
|
+
keeping only behavior testable in the current Spoke.
|
|
140
|
+
|
|
80
141
|
### Gate: Specs Confirmation (optional light gate)
|
|
81
142
|
|
|
82
143
|
Briefly summarize the delta specs generated. Accept a quick acknowledgement from the user ("looks good" / "continue") and proceed. If the user raises substantive objections, pause and revise — otherwise continue directly to Stage 4. This is an optional checkpoint, not a full hard gate; deeper scrutiny will happen in `/specflow:refine`.
|