@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 CHANGED
@@ -8,17 +8,19 @@
8
8
 
9
9
  SpecFlow 把 **OpenSpec**(结构化需求规划)和 **Superpowers**(TDD、调试、代码审查等工程纪律)合并为一个工具:一个 CLI 负责确定性操作,一套跨 IDE 工作流技能负责 AI 编排,覆盖从探索需求到归档合并的完整生命周期。
10
10
 
11
- | 环境 | 命令形式 | 详细指南 |
12
- |------|----------|----------|
13
- | Claude Code | `/specflow:propose` 等斜杠命令 | 本文 |
14
- | Cursor | `specflow:propose` 等命令 | [Cursor 指南](./CURSOR_PACKAGING_AND_USAGE_GUIDE.md) |
15
- | OpenAI Codex | `$specflow-propose` 等技能 | [Codex 指南](./CODEX_PACKAGING_AND_USAGE_GUIDE.md) |
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
- | 多仓 / 前后端一体需求(**权威**) | Contract Hub + Spokes + 产物与 CLI | [多仓完整指南](./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 / 主 specs | [产物说明](./SPECFLOW_ARTIFACTS.md) |
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 为准(开发中可见 1.2.0-beta)
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
- 需要少打断确认时,可用 `/specflow:apply --yes`:自动接受成功的 Gate A/B 确认,并在构建成功后串行执行 review → test → verify;失败、Block、设计缺口仍会停下,且不会自动 archive。
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 writing-plans 规则重写 tasks.md;Phase B subagent TDD 逐任务执行 |
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 分支清理;默认要求 phase=apply |
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 <name>')
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
- function renderConfigYaml(artifactLanguage) {
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
  });
@@ -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: require phase=apply unless --force
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
- if (currentPhase !== 'apply' && !options.force) {
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}', expected 'apply'. Complete '/specflow:apply' first, or pass '--force' to archive anyway.`,
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 && currentPhase !== 'apply') {
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 child = spawnSync(opener.command, [workspacePath], {
10
- cwd: primaryPath,
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
+ }
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gordon.gan/specflow",
3
- "version": "1.2.1-beta",
3
+ "version": "1.3.1-beta",
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": [
@@ -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 with completed implementation.
12
+ - An active change must exist.
13
13
  - `specflow` CLI must be available on PATH.
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.
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
- Run the project test suite for all affected modules. If implementation was done in a git worktree (common after `/specflow:apply`), run tests inside that worktree.
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
- Run `specflow change new <name>` to initialize a new change directory.
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`.